git-daemon 0.1.5 → 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
@@ -39,7 +39,8 @@ npm install
39
39
  npm run daemon
40
40
  ```
41
41
 
42
- The daemon listens on `http://127.0.0.1:8790` by default.
42
+ The daemon listens on `http://127.0.0.1:8790` by default, and can also expose
43
+ HTTPS on `https://127.0.0.1:8791` when enabled.
43
44
 
44
45
  ## HTTPS support
45
46
 
@@ -175,6 +176,10 @@ Key settings live in `config.json`:
175
176
 
176
177
  Tokens are stored (hashed) in `tokens.json`. Logs are written under the configured `logging.directory` with rotation.
177
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
+
178
183
  ## Development
179
184
 
180
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/design.md CHANGED
@@ -64,6 +64,7 @@ These components are **not part of this project**; they are listed only to defin
64
64
 
65
65
  * Binds to `127.0.0.1` only
66
66
  * Implements a small HTTP JSON API + streaming logs (SSE)
67
+ * Optional HTTPS listener can be enabled alongside HTTP
67
68
  * Runs **system git** for native credentials & compatibility
68
69
  * Runs **package manager installs** in sandboxed repos
69
70
  * Provides OS integrations: open folder/terminal/VS Code
@@ -85,6 +86,7 @@ These components are **not part of this project**; they are listed only to defin
85
86
  #### 1) Bind to loopback only
86
87
 
87
88
  * Listen on `127.0.0.1:<daemonPort>` (not `0.0.0.0`)
89
+ * HTTPS listener may also bind to loopback when enabled
88
90
  * Reject requests to non-loopback interfaces.
89
91
 
90
92
  #### 2) Origin allowlist (hard gate)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-daemon",
3
- "version": "0.1.5",
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
  };