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 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:
@@ -174,7 +174,11 @@
174
174
  "format": "uri"
175
175
  },
176
176
  "repoPath": {
177
- "type": "string"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-daemon",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "type": "commonjs",
6
6
  "main": "dist/daemon.js",
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
- requireApproval(
369
- ctx.config,
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
- requireApproval(
378
- ctx.config,
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
- requireApproval(
415
- ctx.config,
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, _req: Request, res: Response, _next: NextFunction) => {
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;
package/src/types.ts CHANGED
@@ -2,7 +2,7 @@ export type Capability = "open-terminal" | "open-vscode" | "deps/install";
2
2
 
3
3
  export type ApprovalEntry = {
4
4
  origin: string;
5
- repoPath: string;
5
+ repoPath: string | null;
6
6
  capabilities: Capability[];
7
7
  approvedAt: string;
8
8
  };