git-daemon 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/config.schema.json +5 -1
- package/package.json +1 -1
- package/src/app.ts +108 -8
- package/src/approvals.ts +4 -1
- package/src/types.ts +1 -1
package/README.md
CHANGED
|
@@ -176,6 +176,10 @@ Key settings live in `config.json`:
|
|
|
176
176
|
|
|
177
177
|
Tokens are stored (hashed) in `tokens.json`. Logs are written under the configured `logging.directory` with rotation.
|
|
178
178
|
|
|
179
|
+
Approvals can be scoped per repo or origin-wide. To allow a capability for all repos
|
|
180
|
+
from an origin, set `"repoPath": null` in an approvals entry. When a TTY is
|
|
181
|
+
available, the daemon will prompt for approval on first use.
|
|
182
|
+
|
|
179
183
|
## Development
|
|
180
184
|
|
|
181
185
|
Run tests:
|
package/config.schema.json
CHANGED
|
@@ -174,7 +174,11 @@
|
|
|
174
174
|
"format": "uri"
|
|
175
175
|
},
|
|
176
176
|
"repoPath": {
|
|
177
|
-
"type":
|
|
177
|
+
"type": [
|
|
178
|
+
"string",
|
|
179
|
+
"null"
|
|
180
|
+
],
|
|
181
|
+
"description": "Repo path to scope approvals. Null means all repos for the origin."
|
|
178
182
|
},
|
|
179
183
|
"capabilities": {
|
|
180
184
|
"type": "array",
|
package/package.json
CHANGED
package/src/app.ts
CHANGED
|
@@ -3,7 +3,9 @@ import rateLimit from "express-rate-limit";
|
|
|
3
3
|
import type { Logger } from "pino";
|
|
4
4
|
import type { Request, Response, NextFunction } from "express";
|
|
5
5
|
import { promises as fs } from "fs";
|
|
6
|
+
import * as fsSync from "fs";
|
|
6
7
|
import path from "path";
|
|
8
|
+
import readline from "readline";
|
|
7
9
|
import { createHttpLogger } from "./logger";
|
|
8
10
|
import type { AppConfig, Capabilities } from "./types";
|
|
9
11
|
import {
|
|
@@ -48,7 +50,8 @@ import { openTarget } from "./os";
|
|
|
48
50
|
import type { TokenStore } from "./tokens";
|
|
49
51
|
import type { PairingManager } from "./pairing";
|
|
50
52
|
import type { JobManager } from "./jobs";
|
|
51
|
-
import { requireApproval } from "./approvals";
|
|
53
|
+
import { hasApproval, requireApproval } from "./approvals";
|
|
54
|
+
import { saveConfig } from "./config";
|
|
52
55
|
|
|
53
56
|
export type DaemonContext = {
|
|
54
57
|
config: AppConfig;
|
|
@@ -365,8 +368,8 @@ export const createApp = (ctx: DaemonContext) => {
|
|
|
365
368
|
);
|
|
366
369
|
|
|
367
370
|
if (payload.target === "terminal") {
|
|
368
|
-
|
|
369
|
-
ctx
|
|
371
|
+
await ensureApproval(
|
|
372
|
+
ctx,
|
|
370
373
|
origin,
|
|
371
374
|
resolved,
|
|
372
375
|
"open-terminal",
|
|
@@ -374,8 +377,8 @@ export const createApp = (ctx: DaemonContext) => {
|
|
|
374
377
|
);
|
|
375
378
|
}
|
|
376
379
|
if (payload.target === "vscode") {
|
|
377
|
-
|
|
378
|
-
ctx
|
|
380
|
+
await ensureApproval(
|
|
381
|
+
ctx,
|
|
379
382
|
origin,
|
|
380
383
|
resolved,
|
|
381
384
|
"open-vscode",
|
|
@@ -411,8 +414,8 @@ export const createApp = (ctx: DaemonContext) => {
|
|
|
411
414
|
} catch {
|
|
412
415
|
throw repoNotFound();
|
|
413
416
|
}
|
|
414
|
-
|
|
415
|
-
ctx
|
|
417
|
+
await ensureApproval(
|
|
418
|
+
ctx,
|
|
416
419
|
origin,
|
|
417
420
|
resolved,
|
|
418
421
|
"deps/install",
|
|
@@ -462,8 +465,22 @@ export const createApp = (ctx: DaemonContext) => {
|
|
|
462
465
|
}
|
|
463
466
|
});
|
|
464
467
|
|
|
465
|
-
app.use((err: unknown,
|
|
468
|
+
app.use((err: unknown, req: Request, res: Response, _next: NextFunction) => {
|
|
466
469
|
if (err instanceof ApiError) {
|
|
470
|
+
if (err.status === 409) {
|
|
471
|
+
console.warn(
|
|
472
|
+
`[Git Daemon] 409 ${err.body.errorCode} ${req.method} ${req.originalUrl} origin=${req.headers.origin ?? ""}`,
|
|
473
|
+
);
|
|
474
|
+
ctx.logger.warn(
|
|
475
|
+
{
|
|
476
|
+
errorCode: err.body.errorCode,
|
|
477
|
+
method: req.method,
|
|
478
|
+
path: req.originalUrl,
|
|
479
|
+
origin: req.headers.origin,
|
|
480
|
+
},
|
|
481
|
+
"Request rejected with conflict",
|
|
482
|
+
);
|
|
483
|
+
}
|
|
467
484
|
res.status(err.status).json(err.body);
|
|
468
485
|
return;
|
|
469
486
|
}
|
|
@@ -480,3 +497,86 @@ export const createApp = (ctx: DaemonContext) => {
|
|
|
480
497
|
|
|
481
498
|
return app;
|
|
482
499
|
};
|
|
500
|
+
|
|
501
|
+
const ensureApproval = async (
|
|
502
|
+
ctx: DaemonContext,
|
|
503
|
+
origin: string,
|
|
504
|
+
repoPath: string,
|
|
505
|
+
capability: "open-terminal" | "open-vscode" | "deps/install",
|
|
506
|
+
workspaceRoot?: string | null,
|
|
507
|
+
) => {
|
|
508
|
+
if (hasApproval(ctx.config, origin, repoPath, capability, workspaceRoot)) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const approved = await promptApproval(origin, repoPath, capability);
|
|
512
|
+
if (!approved) {
|
|
513
|
+
requireApproval(ctx.config, origin, repoPath, capability, workspaceRoot);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
upsertOriginApproval(ctx, origin, capability);
|
|
517
|
+
await saveConfig(ctx.configDir, ctx.config);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const createPromptInterface = () => {
|
|
521
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
522
|
+
return readline.createInterface({
|
|
523
|
+
input: process.stdin,
|
|
524
|
+
output: process.stdout,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
const ttyPath = process.platform === "win32" ? "CON" : "/dev/tty";
|
|
528
|
+
try {
|
|
529
|
+
const input = fsSync.createReadStream(ttyPath, { encoding: "utf8" });
|
|
530
|
+
const output = fsSync.createWriteStream(ttyPath);
|
|
531
|
+
return readline.createInterface({ input, output });
|
|
532
|
+
} catch {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const askQuestion = (rl: readline.Interface, question: string) =>
|
|
538
|
+
new Promise<string>((resolve) => {
|
|
539
|
+
rl.question(question, (answer) => resolve(answer));
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
const promptApproval = async (
|
|
543
|
+
origin: string,
|
|
544
|
+
repoPath: string,
|
|
545
|
+
capability: string,
|
|
546
|
+
) => {
|
|
547
|
+
const rl = createPromptInterface();
|
|
548
|
+
if (!rl) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
const answer = await askQuestion(
|
|
552
|
+
rl,
|
|
553
|
+
`Approve ${capability} for origin ${origin} (all repos)? [y/N] Requested path: ${repoPath} `,
|
|
554
|
+
);
|
|
555
|
+
rl.close();
|
|
556
|
+
const normalized = answer.trim().toLowerCase();
|
|
557
|
+
return normalized === "y" || normalized === "yes";
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const upsertOriginApproval = (
|
|
561
|
+
ctx: DaemonContext,
|
|
562
|
+
origin: string,
|
|
563
|
+
capability: "open-terminal" | "open-vscode" | "deps/install",
|
|
564
|
+
) => {
|
|
565
|
+
const existing = ctx.config.approvals.entries.find(
|
|
566
|
+
(entry) =>
|
|
567
|
+
entry.origin === origin &&
|
|
568
|
+
(entry.repoPath === null || entry.repoPath === "*"),
|
|
569
|
+
);
|
|
570
|
+
if (existing) {
|
|
571
|
+
if (!existing.capabilities.includes(capability)) {
|
|
572
|
+
existing.capabilities.push(capability);
|
|
573
|
+
}
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
ctx.config.approvals.entries.push({
|
|
577
|
+
origin,
|
|
578
|
+
repoPath: null,
|
|
579
|
+
capabilities: [capability],
|
|
580
|
+
approvedAt: new Date().toISOString(),
|
|
581
|
+
});
|
|
582
|
+
};
|
package/src/approvals.ts
CHANGED
|
@@ -13,10 +13,13 @@ export const hasApproval = (
|
|
|
13
13
|
if (entry.origin !== origin || !entry.capabilities.includes(capability)) {
|
|
14
14
|
return false;
|
|
15
15
|
}
|
|
16
|
+
if (entry.repoPath === null || entry.repoPath === "*") {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
16
19
|
if (entry.repoPath === repoPath) {
|
|
17
20
|
return true;
|
|
18
21
|
}
|
|
19
|
-
if (workspaceRoot && !path.isAbsolute(entry.repoPath)) {
|
|
22
|
+
if (workspaceRoot && entry.repoPath && !path.isAbsolute(entry.repoPath)) {
|
|
20
23
|
return path.resolve(workspaceRoot, entry.repoPath) === repoPath;
|
|
21
24
|
}
|
|
22
25
|
return false;
|