@vfarcic/dot-ai 1.21.0 → 1.22.0

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.
@@ -11,6 +11,19 @@
11
11
  * - GITHUB_APP_ENABLED: Enable GitHub App authentication
12
12
  * - GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, GITHUB_APP_INSTALLATION_ID: GitHub App config
13
13
  */
14
+ /**
15
+ * Environment variable name through which a per-request override credential
16
+ * (PRD #621 M3) is handed to the GIT_ASKPASS helper. The token travels in the
17
+ * git child process's ENVIRONMENT — never on its argv (ps/proc) and never
18
+ * embedded in the clone URL written to `.git/config`.
19
+ */
20
+ export declare const ASKPASS_TOKEN_ENV = "DOT_AI_GIT_ASKPASS_TOKEN";
21
+ /**
22
+ * Environment variable naming the host the override token is bound to. The
23
+ * GIT_ASKPASS helper emits the token ONLY when git's credential prompt names
24
+ * this host, so a cross-host HTTP redirect can never obtain it (Decision 3).
25
+ */
26
+ export declare const ASKPASS_HOST_ENV = "DOT_AI_GIT_ASKPASS_HOST";
14
27
  export interface GitAuthConfig {
15
28
  pat?: string;
16
29
  githubApp?: {
@@ -31,7 +44,40 @@ export declare function sanitizeRelativePath(relativePath: string): string;
31
44
  export interface CloneOptions {
32
45
  branch?: string;
33
46
  depth?: number;
47
+ /**
48
+ * Per-call git credential (PRD #621 M3). When supplied it OVERRIDES the
49
+ * env/GitHub-App auth (`getGitAuthConfigFromEnv`) for this clone only
50
+ * (Decision 4) and is scoped to the host in `repoUrl` with cross-host
51
+ * redirect forwarding disabled (Decision 3 — see buildOverrideCloneAuth).
52
+ * When omitted, the clone uses env auth exactly as before.
53
+ */
54
+ token?: string;
34
55
  }
56
+ /**
57
+ * PRD #621 M3 / Decision 3: build the clone URL + intended host for a
58
+ * per-request override credential.
59
+ *
60
+ * The credential itself is NOT in the returned URL — it is the bare
61
+ * `x-access-token` username only (the token is passed via a HOST-BOUND
62
+ * GIT_ASKPASS helper, see cloneRepo / createAskpassScript). So the token never
63
+ * lands on the git argv or in the cloned `.git/config` remote URL (MEDIUM-2/3).
64
+ *
65
+ * No `-c` git config is returned: the earlier `-c credential.helper=` was
66
+ * REJECTED by simple-git's safety guard (allowUnsafeCredentialHelper), which
67
+ * aborted the clone entirely; and `-c http.followRedirects=false` is dropped
68
+ * per review finding R-1 (it blocked legitimate same-host redirects too). The
69
+ * host-bound askpass makes following redirects provably safe — the token is
70
+ * emitted ONLY for `host`, and libcurl already strips credentials on a
71
+ * cross-host redirect by default.
72
+ *
73
+ * Returned as plain data so the auth decision is unit-testable without spawning
74
+ * git. The token is intentionally NOT a parameter — it never influences this
75
+ * (URL/argv) surface.
76
+ */
77
+ export declare function buildOverrideCloneAuth(repoUrl: string): {
78
+ cloneUrl: string;
79
+ host: string;
80
+ };
35
81
  export declare function cloneRepo(repoUrl: string, targetDir: string, opts?: CloneOptions): Promise<{
36
82
  localPath: string;
37
83
  branch: string;
@@ -1 +1 @@
1
- {"version":3,"file":"git-utils.d.ts","sourceRoot":"","sources":["../../src/core/git-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAYH,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE;QACV,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AASD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK1E;AA2ED,wBAAsB,YAAY,CAAC,UAAU,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAc7E;AAED,wBAAgB,uBAAuB,IAAI,aAAa,CAyBvD;AAeD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CASjE;AAID,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,SAAS,CAC7B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,YAAY,GAClB,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA6BhD;AAID,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA8B5E;AAID,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1C;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,wBAAsB,QAAQ,CAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,EAC/C,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE,WAAW,GACjB,OAAO,CAAC,UAAU,CAAC,CA4ErB"}
1
+ {"version":3,"file":"git-utils.d.ts","sourceRoot":"","sources":["../../src/core/git-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAYH;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,6BAA6B,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,4BAA4B,CAAC;AAI1D,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE;QACV,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AASD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAK1E;AA2ED,wBAAsB,YAAY,CAAC,UAAU,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAc7E;AAED,wBAAgB,uBAAuB,IAAI,aAAa,CAyBvD;AAeD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CASjE;AAID,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd,CAMA;AAkJD,wBAAsB,SAAS,CAC7B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,YAAY,GAClB,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAsChD;AAID,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA8B5E;AAID,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1C;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,wBAAsB,QAAQ,CAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,EAC/C,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE,WAAW,GACjB,OAAO,CAAC,UAAU,CAAC,CA4ErB"}
@@ -49,20 +49,37 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
49
49
  return (mod && mod.__esModule) ? mod : { "default": mod };
50
50
  };
51
51
  Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.ASKPASS_HOST_ENV = exports.ASKPASS_TOKEN_ENV = void 0;
52
53
  exports.scrubCredentials = scrubCredentials;
53
54
  exports.getAuthenticatedUrl = getAuthenticatedUrl;
54
55
  exports.getAuthToken = getAuthToken;
55
56
  exports.getGitAuthConfigFromEnv = getGitAuthConfigFromEnv;
56
57
  exports.sanitizeRelativePath = sanitizeRelativePath;
58
+ exports.buildOverrideCloneAuth = buildOverrideCloneAuth;
57
59
  exports.cloneRepo = cloneRepo;
58
60
  exports.pullRepo = pullRepo;
59
61
  exports.pushRepo = pushRepo;
60
62
  const simple_git_1 = __importDefault(require("simple-git"));
63
+ const node_child_process_1 = require("node:child_process");
61
64
  const jwt = __importStar(require("jsonwebtoken"));
62
65
  const fs = __importStar(require("fs"));
63
66
  const path = __importStar(require("path"));
67
+ const os = __importStar(require("os"));
64
68
  const FETCH_TIMEOUT_MS = 30000;
65
69
  const GIT_TIMEOUT_MS = 120000; // 2 minutes for git operations
70
+ /**
71
+ * Environment variable name through which a per-request override credential
72
+ * (PRD #621 M3) is handed to the GIT_ASKPASS helper. The token travels in the
73
+ * git child process's ENVIRONMENT — never on its argv (ps/proc) and never
74
+ * embedded in the clone URL written to `.git/config`.
75
+ */
76
+ exports.ASKPASS_TOKEN_ENV = 'DOT_AI_GIT_ASKPASS_TOKEN';
77
+ /**
78
+ * Environment variable naming the host the override token is bound to. The
79
+ * GIT_ASKPASS helper emits the token ONLY when git's credential prompt names
80
+ * this host, so a cross-host HTTP redirect can never obtain it (Decision 3).
81
+ */
82
+ exports.ASKPASS_HOST_ENV = 'DOT_AI_GIT_ASKPASS_HOST';
66
83
  // ─── Auth helpers ───
67
84
  function scrubCredentials(message) {
68
85
  return message
@@ -177,10 +194,184 @@ function sanitizeRelativePath(relativePath) {
177
194
  }
178
195
  return normalized;
179
196
  }
197
+ /**
198
+ * PRD #621 M3 / Decision 3: build the clone URL + intended host for a
199
+ * per-request override credential.
200
+ *
201
+ * The credential itself is NOT in the returned URL — it is the bare
202
+ * `x-access-token` username only (the token is passed via a HOST-BOUND
203
+ * GIT_ASKPASS helper, see cloneRepo / createAskpassScript). So the token never
204
+ * lands on the git argv or in the cloned `.git/config` remote URL (MEDIUM-2/3).
205
+ *
206
+ * No `-c` git config is returned: the earlier `-c credential.helper=` was
207
+ * REJECTED by simple-git's safety guard (allowUnsafeCredentialHelper), which
208
+ * aborted the clone entirely; and `-c http.followRedirects=false` is dropped
209
+ * per review finding R-1 (it blocked legitimate same-host redirects too). The
210
+ * host-bound askpass makes following redirects provably safe — the token is
211
+ * emitted ONLY for `host`, and libcurl already strips credentials on a
212
+ * cross-host redirect by default.
213
+ *
214
+ * Returned as plain data so the auth decision is unit-testable without spawning
215
+ * git. The token is intentionally NOT a parameter — it never influences this
216
+ * (URL/argv) surface.
217
+ */
218
+ function buildOverrideCloneAuth(repoUrl) {
219
+ const url = new URL(repoUrl);
220
+ const host = url.host;
221
+ url.username = 'x-access-token';
222
+ url.password = '';
223
+ return { cloneUrl: url.toString(), host };
224
+ }
225
+ /**
226
+ * Create a throwaway, HOST-BOUND GIT_ASKPASS helper script. The script holds NO
227
+ * secret — it echoes the token from the environment (ASKPASS_TOKEN_ENV) ONLY
228
+ * when git's credential prompt (passed as $1) names the intended host
229
+ * (ASKPASS_HOST_ENV), delimited by `@`/`//` before and a closing quote after.
230
+ * For any other host — e.g. after an HTTP redirect, or a look-alike like
231
+ * `github.com.evil.test` — it emits nothing, so the token can never reach a
232
+ * different host (Decision 3). The token never touches disk. The script lives
233
+ * in its own 0700 temp dir; `cleanup` removes it.
234
+ */
235
+ function createAskpassScript() {
236
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dot-ai-askpass-'));
237
+ try {
238
+ fs.chmodSync(dir, 0o700);
239
+ }
240
+ catch {
241
+ /* best-effort hardening */
242
+ }
243
+ const scriptPath = path.join(dir, 'askpass.sh');
244
+ // Host-bound match: require the intended host immediately after `@` or `//`
245
+ // and immediately before the closing `'` git puts around the URL, so neither
246
+ // a different redirect host nor a look-alike suffix matches.
247
+ const script = [
248
+ '#!/bin/sh',
249
+ 'case "$1" in',
250
+ ` *"@$${exports.ASKPASS_HOST_ENV}'"*|*"//$${exports.ASKPASS_HOST_ENV}'"*)`,
251
+ ` printf '%s\\n' "$${exports.ASKPASS_TOKEN_ENV}"`,
252
+ ' ;;',
253
+ 'esac',
254
+ '',
255
+ ].join('\n');
256
+ fs.writeFileSync(scriptPath, script, { mode: 0o700 });
257
+ return {
258
+ scriptPath,
259
+ cleanup: () => {
260
+ try {
261
+ fs.rmSync(dir, { recursive: true, force: true });
262
+ }
263
+ catch {
264
+ /* best-effort cleanup; the script holds no secret */
265
+ }
266
+ },
267
+ };
268
+ }
269
+ /**
270
+ * PRD #621 M3: clone an OVERRIDE repo using a per-request token, via a
271
+ * HOST-BOUND GIT_ASKPASS helper.
272
+ *
273
+ * This deliberately spawns `git` DIRECTLY rather than going through simple-git:
274
+ * simple-git's safety scanner rejects the env vars this approach relies on
275
+ * (GIT_ASKPASS → allowUnsafeAskPass) and even flags inherited vars like EDITOR
276
+ * / PAGER, which aborts the clone before it starts. A direct spawn lets us pass
277
+ * the full process.env (PATH/HOME/proxy/TLS) plus the askpass wiring with no
278
+ * argument/env guard interference, while still keeping:
279
+ * - the token OFF the argv (the URL carries only the `x-access-token`
280
+ * username) and OUT of .git/config (MEDIUM-2/MEDIUM-3);
281
+ * - the token bound to the source host so a cross-host redirect can't obtain
282
+ * it (Decision 3 — host-bound askpass + libcurl's default cross-host
283
+ * credential stripping). Redirects are NOT disabled (review finding R-1),
284
+ * so legitimate same-host redirects still work.
285
+ */
286
+ async function cloneWithOverrideToken(repoUrl, targetDir, opts) {
287
+ const { cloneUrl, host } = buildOverrideCloneAuth(repoUrl);
288
+ const askpass = createAskpassScript();
289
+ const args = ['clone'];
290
+ if (opts.branch) {
291
+ args.push('--branch', opts.branch);
292
+ }
293
+ if (opts.depth) {
294
+ args.push('--depth', String(opts.depth));
295
+ }
296
+ // `--` terminates option parsing so the URL/dir can never be read as flags.
297
+ args.push('--', cloneUrl, targetDir);
298
+ const env = {
299
+ ...process.env,
300
+ GIT_ASKPASS: askpass.scriptPath,
301
+ // Never fall back to an interactive terminal prompt if askpass yields nothing.
302
+ GIT_TERMINAL_PROMPT: '0',
303
+ [exports.ASKPASS_TOKEN_ENV]: opts.token,
304
+ [exports.ASKPASS_HOST_ENV]: host,
305
+ };
306
+ try {
307
+ await new Promise((resolve, reject) => {
308
+ const child = (0, node_child_process_1.spawn)('git', args, {
309
+ env,
310
+ stdio: ['ignore', 'ignore', 'pipe'],
311
+ });
312
+ let stderr = '';
313
+ // The 'error', 'close', and timeout handlers can each race to settle this
314
+ // promise (e.g. 'close' still fires after a kill or a spawn 'error'). A
315
+ // promise only settles once, but the LATER handlers would still run their
316
+ // logic on an already-settled promise. Guard so the FIRST settle wins and
317
+ // every subsequent handler is a no-op, and clear the timeout on settle so
318
+ // no dangling timer fires afterwards.
319
+ let settled = false;
320
+ const timerRef = {};
321
+ const settle = (action) => {
322
+ if (settled)
323
+ return;
324
+ settled = true;
325
+ if (timerRef.id)
326
+ clearTimeout(timerRef.id);
327
+ action();
328
+ };
329
+ timerRef.id = setTimeout(() => {
330
+ settle(() => {
331
+ child.kill('SIGKILL');
332
+ reject(new Error(`git clone timed out after ${GIT_TIMEOUT_MS}ms`));
333
+ });
334
+ }, GIT_TIMEOUT_MS);
335
+ child.stderr?.on('data', chunk => {
336
+ stderr += chunk.toString();
337
+ });
338
+ child.on('error', err => {
339
+ settle(() => reject(err));
340
+ });
341
+ child.on('close', code => {
342
+ settle(() => {
343
+ if (code === 0) {
344
+ resolve();
345
+ }
346
+ else {
347
+ // stderr carries only the username-only URL (no token), so it is
348
+ // safe to surface; the caller scrubs it again as defense-in-depth.
349
+ reject(new Error(`git clone exited with code ${code}: ${stderr.trim()}`));
350
+ }
351
+ });
352
+ });
353
+ });
354
+ }
355
+ finally {
356
+ // Remove the askpass helper as soon as the clone finishes (success or
357
+ // failure). It holds no secret, but leaving temp files around is untidy.
358
+ askpass.cleanup();
359
+ }
360
+ return { localPath: targetDir, branch: opts.branch || 'main' };
361
+ }
180
362
  async function cloneRepo(repoUrl, targetDir, opts) {
363
+ // PRD #621 M3 / Decision 4: a per-request override credential takes precedence
364
+ // over env auth for THIS clone only and uses the host-bound GIT_ASKPASS path.
365
+ if (opts?.token) {
366
+ return cloneWithOverrideToken(repoUrl, targetDir, {
367
+ ...opts,
368
+ token: opts.token,
369
+ });
370
+ }
371
+ // Env/GitHub-App auth path (unchanged): credentials come from
372
+ // getGitAuthConfigFromEnv and are embedded in the URL as before.
181
373
  const authConfig = getGitAuthConfigFromEnv();
182
374
  let cloneUrl;
183
- // Use authenticated URL if credentials are available, otherwise clone unauthenticated (public repos)
184
375
  if (authConfig.pat || authConfig.githubApp) {
185
376
  const token = await getAuthToken(authConfig);
186
377
  cloneUrl = getAuthenticatedUrl(repoUrl, token);
@@ -7,9 +7,8 @@
7
7
  * (api.github.com/copilot_internal/v2/token) can return 404 for some account
8
8
  * types, so it is intentionally NOT used here.
9
9
  *
10
- * Supported token types (classic PATs / ghp_* are NOT accepted):
10
+ * Supported token types (PATs / ghp_* and github_pat_* are NOT accepted):
11
11
  * gho_* OAuth token (recommended — via `gh auth login`)
12
- * github_pat_* Fine-grained PAT (needs Copilot Requests permission)
13
12
  * ghu_* GitHub App installation token
14
13
  *
15
14
  * Token resolution priority:
@@ -1 +1 @@
1
- {"version":3,"file":"copilot-token-exchanger.d.ts","sourceRoot":"","sources":["../../../src/core/providers/copilot-token-exchanger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAOH,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,OAAO,IAAI,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,aAAa,CAAC,EAAE,MAAM,GACrB,yBAAyB,CAuB3B;AAID,MAAM,MAAM,qBAAqB,GAAG,yBAAyB,CAAC"}
1
+ {"version":3,"file":"copilot-token-exchanger.d.ts","sourceRoot":"","sources":["../../../src/core/providers/copilot-token-exchanger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,OAAO,IAAI,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,aAAa,CAAC,EAAE,MAAM,GACrB,yBAAyB,CAwB3B;AAID,MAAM,MAAM,qBAAqB,GAAG,yBAAyB,CAAC"}
@@ -8,9 +8,8 @@
8
8
  * (api.github.com/copilot_internal/v2/token) can return 404 for some account
9
9
  * types, so it is intentionally NOT used here.
10
10
  *
11
- * Supported token types (classic PATs / ghp_* are NOT accepted):
11
+ * Supported token types (PATs / ghp_* and github_pat_* are NOT accepted):
12
12
  * gho_* OAuth token (recommended — via `gh auth login`)
13
- * github_pat_* Fine-grained PAT (needs Copilot Requests permission)
14
13
  * ghu_* GitHub App installation token
15
14
  *
16
15
  * Token resolution priority:
@@ -25,7 +24,7 @@
25
24
  */
26
25
  Object.defineProperty(exports, "__esModule", { value: true });
27
26
  exports.makeCopilotCredentialResolver = makeCopilotCredentialResolver;
28
- const SUPPORTED_PREFIXES = ['gho_', 'github_pat_', 'ghu_'];
27
+ const SUPPORTED_PREFIXES = ['gho_', 'ghu_'];
29
28
  function isSupported(token) {
30
29
  return SUPPORTED_PREFIXES.some((p) => token.startsWith(p));
31
30
  }
@@ -51,7 +50,8 @@ function makeCopilotCredentialResolver(overrideToken) {
51
50
  }
52
51
  }
53
52
  throw new Error('No supported GitHub token found for Copilot. ' +
54
- 'Set GITHUB_COPILOT_TOKEN (gho_*, github_pat_*, or ghu_*). ' +
53
+ 'Set GITHUB_COPILOT_TOKEN (gho_* or ghu_*). ' +
54
+ 'Personal access tokens (github_pat_* and ghp_*) are not supported by api.githubcopilot.com. ' +
55
55
  'GH_TOKEN and GITHUB_TOKEN are also checked in that order.');
56
56
  },
57
57
  };
@@ -173,7 +173,7 @@ class VercelProvider {
173
173
  return; // Early return - model instance already set
174
174
  case 'copilot': {
175
175
  // PRD #587: GitHub Copilot provider
176
- // Uses the raw GitHub token (gho_*, github_pat_*, ghu_*) directly as a
176
+ // Uses the raw GitHub token (gho_* or ghu_*) directly as a
177
177
  // Bearer credential against api.githubcopilot.com — no token-exchange step.
178
178
  //
179
179
  // Routing (mirrors Hermes Agent):
@@ -32,6 +32,15 @@ export interface UserPromptsOverride {
32
32
  repoUrl: string;
33
33
  branch?: string;
34
34
  subPath?: string;
35
+ /**
36
+ * Per-request git credential forwarded by the caller (PRD #621 M2/M3, via the
37
+ * X-Dot-AI-Git-Token header). When present it takes precedence over
38
+ * DOT_AI_GIT_TOKEN for THIS request only (Decision 4) and triggers per-request
39
+ * cache isolation so a private authenticated clone is never served to/from the
40
+ * shared unauthenticated cache slot (Decision 2). Never enters the cache key
41
+ * or any log/error/source surface.
42
+ */
43
+ gitToken?: string;
35
44
  }
36
45
  /**
37
46
  * Cache state for tracking repository freshness.
@@ -1 +1 @@
1
- {"version":3,"file":"user-prompts-loader.d.ts","sourceRoot":"","sources":["../../src/core/user-prompts-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAO1C,OAAO,EAAE,MAAM,EAA8B,MAAM,kBAAkB,CAAC;AAEtE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,UAAU,UAAU;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAKD;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,iBAAiB,GAAG,IAAI,CAsB/D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAS3E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,mBAAmB,GAC5B,iBAAiB,CAkDnB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAqB1C;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAUzD;AAQD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAoBlD;AA2PD;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,YAAY,GAAE,OAAe,EAC7B,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,MAAM,EAAE,CAAC,CA0HnB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED;;GAEG;AACH,wBAAgB,wBAAwB,IAAI,UAAU,GAAG,IAAI,CAE5D"}
1
+ {"version":3,"file":"user-prompts-loader.d.ts","sourceRoot":"","sources":["../../src/core/user-prompts-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAO1C,OAAO,EAAE,MAAM,EAA8B,MAAM,kBAAkB,CAAC;AAEtE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,UAAU,UAAU;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAKD;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,iBAAiB,GAAG,IAAI,CAsB/D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAS3E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,mBAAmB,GAC5B,iBAAiB,CAqDnB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAqB1C;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAUzD;AAQD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAoBlD;AA6VD;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,YAAY,GAAE,OAAe,EAC7B,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAAC,MAAM,EAAE,CAAC,CA0JnB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED;;GAEG;AACH,wBAAgB,wBAAwB,IAAI,UAAU,GAAG,IAAI,CAE5D"}
@@ -58,6 +58,7 @@ exports.getUserPromptsCacheState = getUserPromptsCacheState;
58
58
  const fs = __importStar(require("fs"));
59
59
  const path = __importStar(require("path"));
60
60
  const os = __importStar(require("os"));
61
+ const crypto = __importStar(require("crypto"));
61
62
  const git_utils_1 = require("./git-utils");
62
63
  const prompts_1 = require("../tools/prompts");
63
64
  // In-memory cache state (persists across requests within same process)
@@ -160,7 +161,10 @@ function getUserPromptsConfigFromOverride(override) {
160
161
  repoUrl: override.repoUrl,
161
162
  branch,
162
163
  subPath: normalizedSubPath,
163
- gitToken: process.env.DOT_AI_GIT_TOKEN,
164
+ // PRD #621 M2 / Decision 4: a request-supplied token (override.gitToken)
165
+ // takes precedence over the server env credential for this request only;
166
+ // the env credential remains the fallback when no header is present.
167
+ gitToken: override.gitToken ?? process.env.DOT_AI_GIT_TOKEN,
164
168
  cacheTtlSeconds,
165
169
  };
166
170
  }
@@ -254,9 +258,36 @@ function isValidGitBranch(branch) {
254
258
  return /^[a-zA-Z0-9_.\-/]+$/.test(branch);
255
259
  }
256
260
  /**
257
- * Clone the user prompts repository
261
+ * Create a unique, throwaway ROOT directory for a token-bearing override
262
+ * request (PRD #621 M3 / Decision 2). It lives alongside the shared cache
263
+ * directory but is NOT the shared slot, so an authenticated private clone is
264
+ * never written to (or served from) the unauthenticated cache. The caller
265
+ * removes it after reading.
266
+ *
267
+ * Hardening (LOW-4): created atomically via fs.mkdtempSync with a CSPRNG
268
+ * (crypto.randomUUID) name component and mode 0700, so the authenticated clone
269
+ * cannot land in a predictable, world-readable location.
270
+ */
271
+ function createIsolatedCloneRoot() {
272
+ const parent = path.dirname(getCacheDirectory());
273
+ const root = fs.mkdtempSync(path.join(parent, `user-prompts-override-${crypto.randomUUID()}-`));
274
+ try {
275
+ fs.chmodSync(root, 0o700);
276
+ }
277
+ catch {
278
+ /* best-effort hardening (mkdtempSync already creates with 0700) */
279
+ }
280
+ return root;
281
+ }
282
+ /**
283
+ * Clone the user prompts repository.
284
+ *
285
+ * `overrideToken` (PRD #621 M3) is a per-request credential that, when present,
286
+ * overrides env auth for this clone only and is scoped to the source host with
287
+ * no cross-host redirect forwarding (handled in cloneRepo). When omitted, the
288
+ * clone authenticates via env exactly as before.
258
289
  */
259
- async function cloneRepository(config, localPath, logger) {
290
+ async function cloneRepository(config, localPath, logger, overrideToken) {
260
291
  // Validate branch name as defense-in-depth
261
292
  if (!isValidGitBranch(config.branch)) {
262
293
  throw new Error(`Invalid branch name: ${config.branch}`);
@@ -280,6 +311,9 @@ async function cloneRepository(config, localPath, logger) {
280
311
  await (0, git_utils_1.cloneRepo)(config.repoUrl, localPath, {
281
312
  branch: config.branch,
282
313
  depth: 1,
314
+ // Per-request override credential (PRD #621 M3). undefined → cloneRepo
315
+ // falls back to env auth, i.e. today's behavior unchanged.
316
+ token: overrideToken,
283
317
  });
284
318
  logger.info('Successfully cloned user prompts repository', {
285
319
  url: sanitizedUrl,
@@ -287,14 +321,24 @@ async function cloneRepository(config, localPath, logger) {
287
321
  });
288
322
  }
289
323
  catch (error) {
324
+ const scrub = (raw) => (0, git_utils_1.scrubCredentials)(config.gitToken ? raw.replaceAll(config.gitToken, '***') : raw);
290
325
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
291
- const sanitizedError = (0, git_utils_1.scrubCredentials)(config.gitToken
292
- ? errorMessage.replaceAll(config.gitToken, '***')
293
- : errorMessage);
326
+ const sanitizedError = scrub(errorMessage);
294
327
  logger.error('Failed to clone user prompts repository', new Error(sanitizedError), {
295
328
  url: sanitizedUrl,
296
329
  branch: config.branch,
297
330
  });
331
+ // LOW-5: scrub the caught error IN PLACE (message + stack) before attaching
332
+ // it as `cause`, so a serialized `.cause` cannot leak the token. (With the
333
+ // GIT_ASKPASS rework the override token is no longer on the git argv/URL, so
334
+ // it cannot appear here in the first place — this is defense-in-depth, esp.
335
+ // for the env-credential path which still embeds its token in the URL.)
336
+ if (error instanceof Error) {
337
+ error.message = scrub(error.message);
338
+ if (error.stack) {
339
+ error.stack = scrub(error.stack);
340
+ }
341
+ }
298
342
  throw new Error(`Failed to clone user prompts repository: ${sanitizedError}`, { cause: error });
299
343
  }
300
344
  }
@@ -326,10 +370,56 @@ async function pullRepository(config, localPath, logger) {
326
370
  }
327
371
  }
328
372
  /**
329
- * Ensure the repository is cloned and up-to-date
330
- * Returns the path to the prompts directory within the repository
373
+ * Ensure the repository is cloned and up-to-date.
374
+ *
375
+ * Returns the path to the prompts directory within the repository, plus an
376
+ * optional `isolatedRoot` the caller must remove after reading (set only for
377
+ * the token-bearing isolation path below).
378
+ *
379
+ * PRD #621 M3 / Decision 2 (cache isolation): when `overrideToken` is present
380
+ * (a request forwarded an X-Dot-AI-Git-Token), the clone is performed into a
381
+ * unique throwaway directory and the shared `cacheState` is neither read nor
382
+ * written. This guarantees an authenticated private clone is never served from
383
+ * — nor written into — the shared unauthenticated cache slot for the same
384
+ * (repoUrl, branch, subPath) coordinate, and that the token never enters the
385
+ * cache key. Token-less requests use the shared cache exactly as before.
331
386
  */
332
- async function ensureRepository(config, logger, forceRefresh = false) {
387
+ async function ensureRepository(config, logger, forceRefresh = false, overrideToken) {
388
+ if (overrideToken) {
389
+ const isolatedRoot = createIsolatedCloneRoot();
390
+ // Clone into a subdirectory of the 0700 root so the root's restrictive
391
+ // permissions cover the authenticated clone (git creates `cloneDir` itself).
392
+ const cloneDir = path.join(isolatedRoot, 'repo');
393
+ logger.debug('Token-bearing override: cloning in isolation', {
394
+ url: sanitizeUrlForLogging(config.repoUrl),
395
+ branch: config.branch,
396
+ });
397
+ try {
398
+ await cloneRepository(config, cloneDir, logger, overrideToken);
399
+ }
400
+ catch (error) {
401
+ // Remove any partial clone before propagating so a failed authenticated
402
+ // request leaves no isolated directory behind. With GIT_ASKPASS the token
403
+ // is never written to disk, so a cleanup failure cannot leave a PAT
404
+ // behind — but warn (don't swallow) for observability.
405
+ try {
406
+ fs.rmSync(isolatedRoot, { recursive: true, force: true });
407
+ }
408
+ catch (cleanupError) {
409
+ logger.warn('Failed to remove isolated clone directory after clone failure', {
410
+ path: isolatedRoot,
411
+ error: cleanupError instanceof Error
412
+ ? cleanupError.message
413
+ : String(cleanupError),
414
+ });
415
+ }
416
+ throw error;
417
+ }
418
+ const promptsDir = config.subPath
419
+ ? path.join(cloneDir, config.subPath)
420
+ : cloneDir;
421
+ return { promptsDir, isolatedRoot };
422
+ }
333
423
  const localPath = getCacheDirectory();
334
424
  const now = Date.now();
335
425
  const ttlMs = config.cacheTtlSeconds * 1000;
@@ -375,7 +465,11 @@ async function ensureRepository(config, logger, forceRefresh = false) {
375
465
  });
376
466
  }
377
467
  // Return path to prompts directory (with optional subPath)
378
- return config.subPath ? path.join(localPath, config.subPath) : localPath;
468
+ return {
469
+ promptsDir: config.subPath
470
+ ? path.join(localPath, config.subPath)
471
+ : localPath,
472
+ };
379
473
  }
380
474
  const SKILL_FILE_MAX_BYTES = 5 * 1024 * 1024; // 5 MB per file (before base64 encoding)
381
475
  const SKILL_FILENAME = 'SKILL.md';
@@ -458,8 +552,16 @@ async function loadUserPrompts(logger, forceRefresh = false, override) {
458
552
  logger.debug('User prompts not configured (DOT_AI_USER_PROMPTS_REPO not set)');
459
553
  return [];
460
554
  }
555
+ // PRD #621 M3 / Decision 2: a request-forwarded credential (override.gitToken)
556
+ // triggers per-request cache isolation. Track the throwaway clone directory so
557
+ // it is removed after the read (success-path cleanup; the failure path is
558
+ // cleaned up inside ensureRepository).
559
+ const overrideToken = override?.gitToken;
560
+ let isolatedRoot;
461
561
  try {
462
- const promptsDir = await ensureRepository(config, logger, forceRefresh);
562
+ const ensured = await ensureRepository(config, logger, forceRefresh, overrideToken);
563
+ const promptsDir = ensured.promptsDir;
564
+ isolatedRoot = ensured.isolatedRoot;
463
565
  if (!fs.existsSync(promptsDir)) {
464
566
  logger.warn('User prompts directory not found in repository', {
465
567
  path: promptsDir,
@@ -539,6 +641,25 @@ async function loadUserPrompts(logger, forceRefresh = false, override) {
539
641
  logger.error('Failed to load user prompts, falling back to built-in only', new Error(safeMessage));
540
642
  return [];
541
643
  }
644
+ finally {
645
+ // PRD #621 M3 / Decision 2: remove the per-request isolated clone (if any)
646
+ // so token-bearing override clones leave no on-disk residue. With
647
+ // GIT_ASKPASS the token is never written to disk, so a failed cleanup
648
+ // cannot leave a PAT behind — but warn (don't swallow) for observability.
649
+ if (isolatedRoot) {
650
+ try {
651
+ fs.rmSync(isolatedRoot, { recursive: true, force: true });
652
+ }
653
+ catch (cleanupError) {
654
+ logger.warn('Failed to remove isolated clone directory', {
655
+ path: isolatedRoot,
656
+ error: cleanupError instanceof Error
657
+ ? cleanupError.message
658
+ : String(cleanupError),
659
+ });
660
+ }
661
+ }
662
+ }
542
663
  }
543
664
  /**
544
665
  * Clear the cache state (useful for testing)
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Shared CORS allow-header lists (PRD #621 M2, Decision 1).
3
+ *
4
+ * The REST (src/interfaces/rest-api.ts) and front HTTP (src/interfaces/mcp.ts)
5
+ * layers each answer CORS preflight with their own `Access-Control-Allow-Headers`
6
+ * value, and the two were historically OUT OF SYNC. Centralizing the credential
7
+ * header name here guarantees the new `X-Dot-AI-Git-Token` header is advertised
8
+ * by BOTH preflight responses (Decision 1) and that the two lists can never
9
+ * silently drift apart on this header again.
10
+ *
11
+ * The two lists intentionally differ on the OTHER headers (mcp.ts also allows
12
+ * X-Session-Id / X-Dot-AI-Authorization), which is preserved.
13
+ */
14
+ /**
15
+ * Per-request git credential header (PRD #621 M2/M3, Decision 1). The CLI
16
+ * forwards its `DOT_AI_GIT_TOKEN` here so the server can authenticate an
17
+ * overridden (`?repo=`) clone against a second auth realm. Always a request
18
+ * header — never a query param or body field.
19
+ */
20
+ export declare const GIT_TOKEN_HEADER = "X-Dot-AI-Git-Token";
21
+ /**
22
+ * Lowercased form for reading the header off Node's `req.headers` (Node
23
+ * lowercases all incoming header names).
24
+ */
25
+ export declare const GIT_TOKEN_HEADER_LC: string;
26
+ /**
27
+ * `Access-Control-Allow-Headers` value for the REST API layer
28
+ * (rest-api.ts `setCorsHeaders`).
29
+ */
30
+ export declare const REST_CORS_ALLOW_HEADERS = "Content-Type, Authorization, X-Dot-AI-Git-Token";
31
+ /**
32
+ * `Access-Control-Allow-Headers` value for the front HTTP layer (mcp.ts).
33
+ * Retains X-Session-Id and X-Dot-AI-Authorization from the pre-existing list.
34
+ *
35
+ * Includes `Mcp-Session-Id` because the MCP session router in mcp.ts routes
36
+ * requests by `req.headers['mcp-session-id']` (the Streamable HTTP transport's
37
+ * session header); without it a browser preflight requesting that header would
38
+ * fail (CodeRabbit Finding 3). `X-Session-Id` is kept for backward compat.
39
+ */
40
+ export declare const MCP_CORS_ALLOW_HEADERS = "Content-Type, X-Session-Id, Mcp-Session-Id, Authorization, X-Dot-AI-Authorization, X-Dot-AI-Git-Token";
41
+ //# sourceMappingURL=cors-headers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cors-headers.d.ts","sourceRoot":"","sources":["../../src/interfaces/cors-headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,uBAAuB,CAAC;AAErD;;;GAGG;AACH,eAAO,MAAM,mBAAmB,QAAiC,CAAC;AAElE;;;GAGG;AACH,eAAO,MAAM,uBAAuB,oDAAqD,CAAC;AAE1F;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,0GAA2G,CAAC"}
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * Shared CORS allow-header lists (PRD #621 M2, Decision 1).
4
+ *
5
+ * The REST (src/interfaces/rest-api.ts) and front HTTP (src/interfaces/mcp.ts)
6
+ * layers each answer CORS preflight with their own `Access-Control-Allow-Headers`
7
+ * value, and the two were historically OUT OF SYNC. Centralizing the credential
8
+ * header name here guarantees the new `X-Dot-AI-Git-Token` header is advertised
9
+ * by BOTH preflight responses (Decision 1) and that the two lists can never
10
+ * silently drift apart on this header again.
11
+ *
12
+ * The two lists intentionally differ on the OTHER headers (mcp.ts also allows
13
+ * X-Session-Id / X-Dot-AI-Authorization), which is preserved.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.MCP_CORS_ALLOW_HEADERS = exports.REST_CORS_ALLOW_HEADERS = exports.GIT_TOKEN_HEADER_LC = exports.GIT_TOKEN_HEADER = void 0;
17
+ /**
18
+ * Per-request git credential header (PRD #621 M2/M3, Decision 1). The CLI
19
+ * forwards its `DOT_AI_GIT_TOKEN` here so the server can authenticate an
20
+ * overridden (`?repo=`) clone against a second auth realm. Always a request
21
+ * header — never a query param or body field.
22
+ */
23
+ exports.GIT_TOKEN_HEADER = 'X-Dot-AI-Git-Token';
24
+ /**
25
+ * Lowercased form for reading the header off Node's `req.headers` (Node
26
+ * lowercases all incoming header names).
27
+ */
28
+ exports.GIT_TOKEN_HEADER_LC = exports.GIT_TOKEN_HEADER.toLowerCase();
29
+ /**
30
+ * `Access-Control-Allow-Headers` value for the REST API layer
31
+ * (rest-api.ts `setCorsHeaders`).
32
+ */
33
+ exports.REST_CORS_ALLOW_HEADERS = `Content-Type, Authorization, ${exports.GIT_TOKEN_HEADER}`;
34
+ /**
35
+ * `Access-Control-Allow-Headers` value for the front HTTP layer (mcp.ts).
36
+ * Retains X-Session-Id and X-Dot-AI-Authorization from the pre-existing list.
37
+ *
38
+ * Includes `Mcp-Session-Id` because the MCP session router in mcp.ts routes
39
+ * requests by `req.headers['mcp-session-id']` (the Streamable HTTP transport's
40
+ * session header); without it a browser preflight requesting that header would
41
+ * fail (CodeRabbit Finding 3). `X-Session-Id` is kept for backward compat.
42
+ */
43
+ exports.MCP_CORS_ALLOW_HEADERS = `Content-Type, X-Session-Id, Mcp-Session-Id, Authorization, X-Dot-AI-Authorization, ${exports.GIT_TOKEN_HEADER}`;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Redaction of credential-bearing request headers before they are logged.
3
+ *
4
+ * The front HTTP layer (mcp.ts) logs incoming requests at debug level. Logging
5
+ * `req.headers` verbatim would leak credentials — `Authorization`,
6
+ * `X-Dot-AI-Authorization`, and (PRD #621) the per-request `X-Dot-AI-Git-Token`
7
+ * — bypassing the loader-level scrubbing. PRD #621 requires the forwarded token
8
+ * NEVER appear in logs, so these header values are replaced with a fixed
9
+ * placeholder before logging.
10
+ *
11
+ * Kept in its own module so the redaction is unit-testable without importing
12
+ * the heavy mcp.ts server module.
13
+ */
14
+ /** Fixed placeholder substituted for a redacted header value. */
15
+ export declare const REDACTED_PLACEHOLDER = "***REDACTED***";
16
+ /**
17
+ * Lowercased names of headers whose values are credential-bearing and must be
18
+ * redacted before logging. Node lowercases all incoming header names, so the
19
+ * comparison is done in lowercase.
20
+ */
21
+ export declare const SENSITIVE_HEADER_NAMES: ReadonlySet<string>;
22
+ /**
23
+ * Return a shallow copy of `headers` with the value of every credential-bearing
24
+ * header replaced by REDACTED_PLACEHOLDER. Non-sensitive headers are preserved
25
+ * verbatim. The input object is never mutated.
26
+ */
27
+ export declare function redactSensitiveHeaders(headers: Record<string, unknown> | undefined): Record<string, unknown>;
28
+ //# sourceMappingURL=header-redaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"header-redaction.d.ts","sourceRoot":"","sources":["../../src/interfaces/header-redaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,iEAAiE;AACjE,eAAO,MAAM,oBAAoB,mBAAmB,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAMrD,CAAC;AAEH;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC3C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB"}
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ /**
3
+ * Redaction of credential-bearing request headers before they are logged.
4
+ *
5
+ * The front HTTP layer (mcp.ts) logs incoming requests at debug level. Logging
6
+ * `req.headers` verbatim would leak credentials — `Authorization`,
7
+ * `X-Dot-AI-Authorization`, and (PRD #621) the per-request `X-Dot-AI-Git-Token`
8
+ * — bypassing the loader-level scrubbing. PRD #621 requires the forwarded token
9
+ * NEVER appear in logs, so these header values are replaced with a fixed
10
+ * placeholder before logging.
11
+ *
12
+ * Kept in its own module so the redaction is unit-testable without importing
13
+ * the heavy mcp.ts server module.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.SENSITIVE_HEADER_NAMES = exports.REDACTED_PLACEHOLDER = void 0;
17
+ exports.redactSensitiveHeaders = redactSensitiveHeaders;
18
+ const cors_headers_1 = require("./cors-headers");
19
+ /** Fixed placeholder substituted for a redacted header value. */
20
+ exports.REDACTED_PLACEHOLDER = '***REDACTED***';
21
+ /**
22
+ * Lowercased names of headers whose values are credential-bearing and must be
23
+ * redacted before logging. Node lowercases all incoming header names, so the
24
+ * comparison is done in lowercase.
25
+ */
26
+ exports.SENSITIVE_HEADER_NAMES = new Set([
27
+ 'authorization',
28
+ 'x-dot-ai-authorization',
29
+ cors_headers_1.GIT_TOKEN_HEADER_LC,
30
+ 'cookie',
31
+ 'proxy-authorization',
32
+ ]);
33
+ /**
34
+ * Return a shallow copy of `headers` with the value of every credential-bearing
35
+ * header replaced by REDACTED_PLACEHOLDER. Non-sensitive headers are preserved
36
+ * verbatim. The input object is never mutated.
37
+ */
38
+ function redactSensitiveHeaders(headers) {
39
+ if (!headers)
40
+ return {};
41
+ const redacted = {};
42
+ for (const [name, value] of Object.entries(headers)) {
43
+ redacted[name] = exports.SENSITIVE_HEADER_NAMES.has(name.toLowerCase())
44
+ ? exports.REDACTED_PLACEHOLDER
45
+ : value;
46
+ }
47
+ return redacted;
48
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/interfaces/mcp.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AA8EtC,OAAO,EAAgB,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAcvD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAmBD,qBAAa,SAAS;IACpB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,UAAU,CAAC,CAAkC;IACrD,4EAA4E;IAC5E,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,cAAc,CAAC,CAAiC;IACxD,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,aAAa,CAAC,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAC,CAA6B;IAC9C,OAAO,CAAC,aAAa,CAAC,CAAqB;IAC3C,OAAO,CAAC,SAAS,CAAC,CAAM;gBAEZ,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe;IA6BjD;;;OAGG;IACH,gBAAgB,IAAI,aAAa,GAAG,SAAS;IAQ7C;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAuBxB;;OAEG;IACH,OAAO,CAAC,eAAe;IA2CvB;;OAEG;IACH,OAAO,CAAC,WAAW;IA8KnB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAmBzB;;;;OAIG;YACW,mBAAmB;IA6CjC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAkCzB,OAAO,CAAC,qBAAqB;IAS7B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,iBAAiB;IAInB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAed,kBAAkB;YAwRlB,gBAAgB;IAexB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoC3B,OAAO,IAAI,OAAO;CAGnB"}
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/interfaces/mcp.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAgFtC,OAAO,EAAgB,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAcvD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAmBD,qBAAa,SAAS;IACpB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,WAAW,CAAkB;IACrC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,UAAU,CAAC,CAAkC;IACrD,4EAA4E;IAC5E,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,cAAc,CAAC,CAAiC;IACxD,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,aAAa,CAAC,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAC,CAA6B;IAC9C,OAAO,CAAC,aAAa,CAAC,CAAqB;IAC3C,OAAO,CAAC,SAAS,CAAC,CAAM;gBAEZ,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe;IA6BjD;;;OAGG;IACH,gBAAgB,IAAI,aAAa,GAAG,SAAS;IAQ7C;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAuBxB;;OAEG;IACH,OAAO,CAAC,eAAe;IA2CvB;;OAEG;IACH,OAAO,CAAC,WAAW;IA8KnB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAmBzB;;;;OAIG;YACW,mBAAmB;IA6CjC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAkCzB,OAAO,CAAC,qBAAqB;IAS7B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,iBAAiB;IAInB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAed,kBAAkB;YAgSlB,gBAAgB;IAexB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoC3B,OAAO,IAAI,OAAO;CAGnB"}
@@ -28,6 +28,8 @@ const impact_analysis_1 = require("../tools/impact-analysis");
28
28
  const prompts_1 = require("../tools/prompts");
29
29
  const rest_registry_1 = require("./rest-registry");
30
30
  const rest_api_1 = require("./rest-api");
31
+ const cors_headers_1 = require("./cors-headers");
32
+ const header_redaction_1 = require("./header-redaction");
31
33
  const oauth_1 = require("./oauth");
32
34
  const request_context_1 = require("./request-context");
33
35
  const rbac_1 = require("../core/rbac");
@@ -423,15 +425,23 @@ class MCPServer {
423
425
  // Execute entire request within the span's context for proper propagation
424
426
  await api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), async () => {
425
427
  try {
428
+ // PRD #621: the forwarded token must never appear in logs. Redact
429
+ // credential-bearing headers (HIGH-1) AND scrub credentials from the
430
+ // request URL — a ?repo=https://user:token@host or a
431
+ // credential-bearing query param would otherwise leak verbatim
432
+ // (CodeRabbit Finding 1). sanitizeRequestUrlForLogging is the shared
433
+ // helper already used by the REST layer.
426
434
  this.logger.debug('HTTP request received', {
427
435
  method: req.method,
428
- url: req.url,
429
- headers: req.headers,
436
+ url: (0, rest_api_1.sanitizeRequestUrlForLogging)(req.url),
437
+ headers: (0, header_redaction_1.redactSensitiveHeaders)(req.headers),
430
438
  });
431
439
  // Handle CORS for browser-based clients
432
440
  res.setHeader('Access-Control-Allow-Origin', '*');
433
441
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
434
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Session-Id, Authorization, X-Dot-AI-Authorization');
442
+ // PRD #621 M2 / Decision 1: includes X-Dot-AI-Git-Token, kept in
443
+ // sync with the REST allowlist via cors-headers.ts.
444
+ res.setHeader('Access-Control-Allow-Headers', cors_headers_1.MCP_CORS_ALLOW_HEADERS);
435
445
  if (req.method === 'OPTIONS') {
436
446
  res.writeHead(204);
437
447
  res.end();
@@ -9,6 +9,7 @@ import { RestToolRegistry, ToolInfo } from './rest-registry';
9
9
  import { RestRouteRegistry } from './rest-route-registry';
10
10
  import { Logger } from '../core/error-handling';
11
11
  import { DotAI } from '../core/index';
12
+ import { UserPromptsOverride } from '../core/user-prompts-loader';
12
13
  import { PluginManager } from '../core/plugin-manager';
13
14
  /**
14
15
  * Constant placeholder used when the request URL fails to parse and the
@@ -31,6 +32,47 @@ export declare const UNPARSEABLE_QUERY_PLACEHOLDER = "?<redacted-unparseable>";
31
32
  * '?' are pass-through (no risk).
32
33
  */
33
34
  export declare function sanitizeRequestUrlForLogging(url: string | undefined): string | undefined;
35
+ /**
36
+ * Extract and validate the per-request prompts override.
37
+ *
38
+ * Threads three optional, additive inputs from the request into a
39
+ * UserPromptsOverride (PRD #581 introduced `repo`; PRD #621 M1 adds `path`
40
+ * and `branch`):
41
+ * - repoParam → override.repoUrl (GET ?repo= / POST body `repo`)
42
+ * - pathParam → override.subPath (GET ?path= / POST body `path`)
43
+ * - branchParam → override.branch (GET ?branch= / POST body `branch`)
44
+ * - gitToken → override.gitToken (X-Dot-AI-Git-Token request header; M2)
45
+ *
46
+ * Returns:
47
+ * - { ok: true, override } when no `repo` is supplied (override undefined;
48
+ * any path/branch/token are ignored, since they only qualify an override —
49
+ * this keeps the no-`repo` / env-var-configured path unchanged).
50
+ * - { ok: true, override } when a syntactically valid override is supplied.
51
+ * - { ok: false, message } when the override fails validation (HTTP 400).
52
+ *
53
+ * The validation message is run through scrubCredentials so embedded tokens
54
+ * never reach the wire response.
55
+ *
56
+ * Backward compatibility (PRD #621, non-negotiable): when path/branch are
57
+ * absent or empty, the override carries `repoUrl` only — byte-identical to
58
+ * the PRD #581 behavior (same clone target: repo root, `main`). subPath and
59
+ * branch are populated ONLY for a non-empty value, so downstream defaults are
60
+ * untouched. The credential header is INERT unless a `?repo=` override is
61
+ * present: without a repo this returns `override: undefined` before the token
62
+ * is ever read, so the env-var path is unaffected by a forwarded header.
63
+ *
64
+ * Validation is delegated to getUserPromptsConfigFromOverride (scheme,
65
+ * sanitizeRelativePath for subPath, isValidGitBranch for branch) and happens
66
+ * BEFORE any clone or shared-cache mutation, so a rejected override can never
67
+ * corrupt the env-var-configured cache.
68
+ */
69
+ export declare function extractPromptsOverride(repoParam: unknown, pathParam?: unknown, branchParam?: unknown, gitToken?: string): {
70
+ ok: true;
71
+ override?: UserPromptsOverride;
72
+ } | {
73
+ ok: false;
74
+ message: string;
75
+ };
34
76
  /**
35
77
  * HTTP status codes for REST responses
36
78
  */
@@ -233,18 +275,6 @@ export declare class RestApiRouter {
233
275
  * Returns container logs for a pod
234
276
  */
235
277
  private handleGetLogs;
236
- /**
237
- * Extract and validate the per-request `repo` override (PRD #581).
238
- *
239
- * Returns:
240
- * - { ok: true, override } when no repo param is supplied, override is undefined.
241
- * - { ok: true, override } when a syntactically valid override URL is supplied.
242
- * - { ok: false, message } when the override fails validation (HTTP 400).
243
- *
244
- * The validation message is run through scrubCredentials so embedded tokens
245
- * never reach the wire response.
246
- */
247
- private extractPromptsOverride;
248
278
  /**
249
279
  * Handle prompts list requests
250
280
  */
@@ -1 +1 @@
1
- {"version":3,"file":"rest-api.d.ts","sourceRoot":"","sources":["../../src/interfaces/rest-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,EAAE,iBAAiB,EAAc,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAgDtC,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAevD;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,4BAA4B,CAAC;AAEvE;;;;;;;;;;;;GAYG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,MAAM,GAAG,SAAS,GACtB,MAAM,GAAG,SAAS,CAoBpB;AAED;;GAEG;AACH,oBAAY,UAAU;IACpB,EAAE,MAAM;IACR,WAAW,MAAM;IACjB,SAAS,MAAM;IACf,kBAAkB,MAAM;IACxB,qBAAqB,MAAM;IAC3B,WAAW,MAAM;IACjB,mBAAmB,MAAM;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;IACF,IAAI,CAAC,EAAE;QACL,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,IAAI,CAAC,EAAE;QACL,MAAM,EAAE,OAAO,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,IAAI,CAAC,EAAE;QACL,KAAK,EAAE,QAAQ,EAAE,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QACtB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;CACH;AAED;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GACzB,SAAS,GACT,OAAO,GACP,MAAM,GACN,OAAO,GACP,MAAM,GACN,WAAW,CAAC;AAEhB;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,gBAAgB,EAAE,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,YAAY,GAAG,UAAU,CAAC;CACzC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,iBAAiB,CAAC;IACxB,OAAO,EACH,MAAM,GACN;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAClC;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAA;KAAE,GACvC,KAAK,CAAC;QACJ,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC,GACF,wBAAwB,GACxB,4BAA4B,CAAC;CAClC;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;GAEG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAmB;IACnC,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,cAAc,CAAa;IACnC,OAAO,CAAC,aAAa,CAAC,CAAgB;gBAGpC,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,aAAa,EAC7B,MAAM,GAAE,OAAO,CAAC,aAAa,CAAM;IAkCrC;;;;OAIG;IACG,aAAa,CACjB,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,IAAI,CAAC,EAAE,OAAO,GACb,OAAO,CAAC,IAAI,CAAC;IAsGhB;;;OAGG;YACW,aAAa;IA0G3B;;OAEG;YACW,mBAAmB;IAuEjC;;OAEG;YACW,mBAAmB;IAuJjC;;OAEG;YACW,iBAAiB;IAqC/B;;OAEG;YACW,yBAAyB;IA2EvC;;;;OAIG;YACW,sBAAsB;IAyDpC;;;OAGG;YACW,qBAAqB;IAmKnC;;;;OAIG;YACW,mBAAmB;IAoLjC;;;OAGG;YACW,mBAAmB;IAmDjC;;;OAGG;YACW,iBAAiB;IAuL/B;;;OAGG;YACW,eAAe;IA0J7B;;;OAGG;YACW,aAAa;IAuK3B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,sBAAsB;IAqC9B;;OAEG;YACW,wBAAwB;IAiEtC;;OAEG;YACW,uBAAuB;IAmFrC;;OAEG;YACW,yBAAyB;IAyEvC;;;;OAIG;YACW,eAAe;IAwW7B;;;;OAIG;YACW,kBAAkB;IAkHhC;;;OAGG;YACW,oBAAoB;IA+ClC;;;;OAIG;YACW,sBAAsB;IAuEpC;;;;OAIG;YACW,2BAA2B;IAyQzC;;;;OAIG;YACW,kBAAkB;IA6PhC;;OAEG;YACW,+BAA+B;IA8D7C;;OAEG;YACW,gBAAgB;IAwG9B;;OAEG;YACW,eAAe;IAkD7B;;OAEG;YACW,gBAAgB;IA6E9B;;OAEG;IACH,OAAO,CAAC,cAAc;IAUtB;;OAEG;YACW,gBAAgB;IAS9B;;OAEG;YACW,iBAAiB;IAyB/B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAIzB;;OAEG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAMvC;;OAEG;IACH,SAAS,IAAI,aAAa;IAI1B;;;OAGG;IACH,gBAAgB,IAAI,iBAAiB;CAGtC"}
1
+ {"version":3,"file":"rest-api.d.ts","sourceRoot":"","sources":["../../src/interfaces/rest-api.ts"],"names":[],"mappings":"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"}
@@ -41,6 +41,7 @@ var __importStar = (this && this.__importStar) || (function () {
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.RestApiRouter = exports.HttpStatus = exports.UNPARSEABLE_QUERY_PLACEHOLDER = void 0;
43
43
  exports.sanitizeRequestUrlForLogging = sanitizeRequestUrlForLogging;
44
+ exports.extractPromptsOverride = extractPromptsOverride;
44
45
  const node_url_1 = require("node:url");
45
46
  const openapi_generator_1 = require("./openapi-generator");
46
47
  const rest_route_registry_1 = require("./rest-route-registry");
@@ -50,6 +51,7 @@ const embedding_migration_handler_1 = require("./embedding-migration-handler");
50
51
  const prompts_1 = require("../tools/prompts");
51
52
  const user_prompts_loader_1 = require("../core/user-prompts-loader");
52
53
  const git_utils_1 = require("../core/git-utils");
54
+ const cors_headers_1 = require("./cors-headers");
53
55
  const generic_session_manager_1 = require("../core/generic-session-manager");
54
56
  const session_events_1 = require("../core/session-events");
55
57
  const shared_prompt_loader_1 = require("../core/shared-prompt-loader");
@@ -108,6 +110,135 @@ function sanitizeRequestUrlForLogging(url) {
108
110
  return url.slice(0, qIdx) + exports.UNPARSEABLE_QUERY_PLACEHOLDER;
109
111
  }
110
112
  }
113
+ /**
114
+ * Coerce an optional override string param (path/branch) supplied via query
115
+ * string or JSON body. Mirrors the `repo` guard in extractPromptsOverride:
116
+ * - non-string (array, number, object, boolean) → 400 (avoids a 500 from a
117
+ * malformed body reaching downstream code).
118
+ * - absent (null/undefined) or empty/whitespace-only → `undefined`, i.e.
119
+ * treated as not supplied so the downstream default (subPath '' / branch
120
+ * 'main') is preserved and an empty-string branch never reaches
121
+ * isValidGitBranch (which would otherwise reject it).
122
+ * - otherwise → the trimmed value.
123
+ */
124
+ function coerceOverrideStringParam(value, name) {
125
+ if (value === undefined || value === null) {
126
+ return { ok: true, value: undefined };
127
+ }
128
+ if (typeof value !== 'string') {
129
+ return {
130
+ ok: false,
131
+ message: `${name} must be a string (got ${Array.isArray(value) ? 'array' : typeof value})`,
132
+ };
133
+ }
134
+ const trimmed = value.trim();
135
+ return { ok: true, value: trimmed.length > 0 ? trimmed : undefined };
136
+ }
137
+ /**
138
+ * Read the per-request git credential from the X-Dot-AI-Git-Token header
139
+ * (PRD #621 M2). Node lowercases incoming header names and may present a
140
+ * repeated header as an array; normalize to a single non-empty string or
141
+ * undefined. The value is a secret, so it is never logged here.
142
+ */
143
+ function readGitTokenHeader(req) {
144
+ const raw = req.headers[cors_headers_1.GIT_TOKEN_HEADER_LC];
145
+ const value = Array.isArray(raw) ? raw[0] : raw;
146
+ if (typeof value !== 'string') {
147
+ return undefined;
148
+ }
149
+ const trimmed = value.trim();
150
+ return trimmed.length > 0 ? trimmed : undefined;
151
+ }
152
+ /**
153
+ * Extract and validate the per-request prompts override.
154
+ *
155
+ * Threads three optional, additive inputs from the request into a
156
+ * UserPromptsOverride (PRD #581 introduced `repo`; PRD #621 M1 adds `path`
157
+ * and `branch`):
158
+ * - repoParam → override.repoUrl (GET ?repo= / POST body `repo`)
159
+ * - pathParam → override.subPath (GET ?path= / POST body `path`)
160
+ * - branchParam → override.branch (GET ?branch= / POST body `branch`)
161
+ * - gitToken → override.gitToken (X-Dot-AI-Git-Token request header; M2)
162
+ *
163
+ * Returns:
164
+ * - { ok: true, override } when no `repo` is supplied (override undefined;
165
+ * any path/branch/token are ignored, since they only qualify an override —
166
+ * this keeps the no-`repo` / env-var-configured path unchanged).
167
+ * - { ok: true, override } when a syntactically valid override is supplied.
168
+ * - { ok: false, message } when the override fails validation (HTTP 400).
169
+ *
170
+ * The validation message is run through scrubCredentials so embedded tokens
171
+ * never reach the wire response.
172
+ *
173
+ * Backward compatibility (PRD #621, non-negotiable): when path/branch are
174
+ * absent or empty, the override carries `repoUrl` only — byte-identical to
175
+ * the PRD #581 behavior (same clone target: repo root, `main`). subPath and
176
+ * branch are populated ONLY for a non-empty value, so downstream defaults are
177
+ * untouched. The credential header is INERT unless a `?repo=` override is
178
+ * present: without a repo this returns `override: undefined` before the token
179
+ * is ever read, so the env-var path is unaffected by a forwarded header.
180
+ *
181
+ * Validation is delegated to getUserPromptsConfigFromOverride (scheme,
182
+ * sanitizeRelativePath for subPath, isValidGitBranch for branch) and happens
183
+ * BEFORE any clone or shared-cache mutation, so a rejected override can never
184
+ * corrupt the env-var-configured cache.
185
+ */
186
+ function extractPromptsOverride(repoParam, pathParam, branchParam, gitToken) {
187
+ // Treat absent (null/undefined) repo as no override. path/branch/token only
188
+ // qualify an override, so without a repo they are ignored (the credential
189
+ // header is inert on the env-var path).
190
+ if (repoParam === undefined || repoParam === null) {
191
+ return { ok: true, override: undefined };
192
+ }
193
+ // The wire contract says `repo` is a string. Anything else (array,
194
+ // number, object, boolean) is a 400 — otherwise downstream code
195
+ // would crash to a 500.
196
+ if (typeof repoParam !== 'string') {
197
+ return {
198
+ ok: false,
199
+ message: `repo must be a string (got ${Array.isArray(repoParam) ? 'array' : typeof repoParam})`,
200
+ };
201
+ }
202
+ const trimmed = repoParam.trim();
203
+ if (!trimmed) {
204
+ return { ok: true, override: undefined };
205
+ }
206
+ const candidate = { repoUrl: trimmed };
207
+ // PRD #621 M1: thread ?path= / body `path` into subPath (validated
208
+ // downstream by sanitizeRelativePath).
209
+ const pathResult = coerceOverrideStringParam(pathParam, 'path');
210
+ if (!pathResult.ok) {
211
+ return pathResult;
212
+ }
213
+ if (pathResult.value !== undefined) {
214
+ candidate.subPath = pathResult.value;
215
+ }
216
+ // PRD #621 M1: thread ?branch= / body `branch` into branch (validated
217
+ // downstream by isValidGitBranch).
218
+ const branchResult = coerceOverrideStringParam(branchParam, 'branch');
219
+ if (!branchResult.ok) {
220
+ return branchResult;
221
+ }
222
+ if (branchResult.value !== undefined) {
223
+ candidate.branch = branchResult.value;
224
+ }
225
+ // PRD #621 M2: the X-Dot-AI-Git-Token header (read by the handler and passed
226
+ // in already-normalized to a non-empty string or undefined) authenticates
227
+ // THIS override clone. It travels only as a header — never query/body — and
228
+ // is never echoed (computePromptsSource uses repoUrl only).
229
+ if (gitToken) {
230
+ candidate.gitToken = gitToken;
231
+ }
232
+ try {
233
+ // Throws on invalid scheme / subPath / branch.
234
+ (0, user_prompts_loader_1.getUserPromptsConfigFromOverride)(candidate);
235
+ return { ok: true, override: candidate };
236
+ }
237
+ catch (error) {
238
+ const raw = error instanceof Error ? error.message : 'Invalid override';
239
+ return { ok: false, message: (0, git_utils_1.scrubCredentials)(raw) };
240
+ }
241
+ }
111
242
  /**
112
243
  * HTTP status codes for REST responses
113
244
  */
@@ -1086,53 +1217,17 @@ class RestApiRouter {
1086
1217
  await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'LOGS_ERROR', 'Failed to retrieve logs', { error: errorMessage });
1087
1218
  }
1088
1219
  }
1089
- /**
1090
- * Extract and validate the per-request `repo` override (PRD #581).
1091
- *
1092
- * Returns:
1093
- * - { ok: true, override } when no repo param is supplied, override is undefined.
1094
- * - { ok: true, override } when a syntactically valid override URL is supplied.
1095
- * - { ok: false, message } when the override fails validation (HTTP 400).
1096
- *
1097
- * The validation message is run through scrubCredentials so embedded tokens
1098
- * never reach the wire response.
1099
- */
1100
- extractPromptsOverride(repoParam) {
1101
- // Treat absent (null/undefined) as no override.
1102
- if (repoParam === undefined || repoParam === null) {
1103
- return { ok: true, override: undefined };
1104
- }
1105
- // The wire contract says `repo` is a string. Anything else (array,
1106
- // number, object, boolean) is a 400 — otherwise downstream code
1107
- // would crash to a 500.
1108
- if (typeof repoParam !== 'string') {
1109
- return {
1110
- ok: false,
1111
- message: `repo must be a string (got ${Array.isArray(repoParam) ? 'array' : typeof repoParam})`,
1112
- };
1113
- }
1114
- const trimmed = repoParam.trim();
1115
- if (!trimmed) {
1116
- return { ok: true, override: undefined };
1117
- }
1118
- const candidate = { repoUrl: trimmed };
1119
- try {
1120
- // Throws on invalid scheme / subPath / branch.
1121
- (0, user_prompts_loader_1.getUserPromptsConfigFromOverride)(candidate);
1122
- return { ok: true, override: candidate };
1123
- }
1124
- catch (error) {
1125
- const raw = error instanceof Error ? error.message : 'Invalid override';
1126
- return { ok: false, message: (0, git_utils_1.scrubCredentials)(raw) };
1127
- }
1128
- }
1129
1220
  /**
1130
1221
  * Handle prompts list requests
1131
1222
  */
1132
1223
  async handlePromptsListRequest(req, res, requestId, searchParams) {
1133
1224
  try {
1134
1225
  this.logger.info('Processing prompts list request', { requestId });
1135
- const overrideResult = this.extractPromptsOverride(searchParams.get('repo'));
1226
+ // PRD #581: ?repo= override. PRD #621 M1: ?path= / ?branch= thread into
1227
+ // candidate.subPath / candidate.branch (absent → unchanged behavior).
1228
+ // PRD #621 M2: X-Dot-AI-Git-Token header authenticates the override clone
1229
+ // (inert when no ?repo= override is present).
1230
+ const overrideResult = extractPromptsOverride(searchParams.get('repo'), searchParams.get('path'), searchParams.get('branch'), readGitTokenHeader(req));
1136
1231
  if (!overrideResult.ok) {
1137
1232
  await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'VALIDATION_ERROR', overrideResult.message);
1138
1233
  return;
@@ -1169,7 +1264,11 @@ class RestApiRouter {
1169
1264
  requestId,
1170
1265
  promptName,
1171
1266
  });
1172
- const overrideResult = this.extractPromptsOverride(searchParams.get('repo'));
1267
+ // PRD #581: ?repo= override. PRD #621 M1: ?path= / ?branch= thread into
1268
+ // candidate.subPath / candidate.branch (absent → unchanged behavior).
1269
+ // PRD #621 M2: X-Dot-AI-Git-Token header authenticates the override clone
1270
+ // (inert when no ?repo= override is present).
1271
+ const overrideResult = extractPromptsOverride(searchParams.get('repo'), searchParams.get('path'), searchParams.get('branch'), readGitTokenHeader(req));
1173
1272
  if (!overrideResult.ok) {
1174
1273
  await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'VALIDATION_ERROR', overrideResult.message);
1175
1274
  return;
@@ -1214,9 +1313,13 @@ class RestApiRouter {
1214
1313
  requestId,
1215
1314
  });
1216
1315
  // body.repo type is checked inside extractPromptsOverride (it accepts
1217
- // unknown and rejects non-string values with 400 — see F2).
1316
+ // unknown and rejects non-string values with 400 — see F2). PRD #621 M1:
1317
+ // body `path` / `branch` thread into candidate.subPath / candidate.branch
1318
+ // (absent → unchanged behavior); both are likewise type-checked.
1218
1319
  const bodyObj = body;
1219
- const overrideResult = this.extractPromptsOverride(bodyObj?.repo);
1320
+ // PRD #621 M2: the credential always travels as the X-Dot-AI-Git-Token
1321
+ // header — never the body — and is inert without a repo override.
1322
+ const overrideResult = extractPromptsOverride(bodyObj?.repo, bodyObj?.path, bodyObj?.branch, readGitTokenHeader(req));
1220
1323
  if (!overrideResult.ok) {
1221
1324
  await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'VALIDATION_ERROR', overrideResult.message);
1222
1325
  return;
@@ -1599,7 +1702,9 @@ class RestApiRouter {
1599
1702
  };
1600
1703
  if (this.config.enableCors) {
1601
1704
  headers['Access-Control-Allow-Origin'] = '*';
1602
- headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization';
1705
+ // R-2: use the shared allowlist (single source of truth in cors-headers.ts)
1706
+ // so the SSE preflight includes X-Dot-AI-Git-Token like every other route.
1707
+ headers['Access-Control-Allow-Headers'] = cors_headers_1.REST_CORS_ALLOW_HEADERS;
1603
1708
  }
1604
1709
  res.writeHead(HttpStatus.OK, headers);
1605
1710
  this.logger.info('SSE connection established', { requestId });
@@ -2187,7 +2292,9 @@ class RestApiRouter {
2187
2292
  setCorsHeaders(res) {
2188
2293
  res.setHeader('Access-Control-Allow-Origin', '*');
2189
2294
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
2190
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
2295
+ // PRD #621 M2 / Decision 1: advertise the X-Dot-AI-Git-Token credential
2296
+ // header (shared with the mcp.ts allowlist via cors-headers.ts).
2297
+ res.setHeader('Access-Control-Allow-Headers', cors_headers_1.REST_CORS_ALLOW_HEADERS);
2191
2298
  res.setHeader('Access-Control-Max-Age', '86400');
2192
2299
  }
2193
2300
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"operate-analysis.d.ts","sourceRoot":"","sources":["../../src/tools/operate-analysis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAGvD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAGhD,OAAO,EAEL,kBAAkB,EAElB,eAAe,EAKhB,MAAM,WAAW,CAAC;AAwBnB;;GAEG;AACH,UAAU,qBAAqB;IAC7B,MAAM,EAAE,wBAAwB,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE;QACR,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,OAAO,CAAC;QACtB,eAAe,EAAE,eAAe,CAAC;QACjC,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,gBAAgB,EAAE;YAChB,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAC;YAC7B,OAAO,EAAE,MAAM,CAAC;SACjB,CAAC;QACF,eAAe,EAAE,MAAM,EAAE,CAAC;QAC1B,gBAAgB,EAAE,MAAM,EAAE,CAAC;QAC3B,eAAe,EAAE,MAAM,EAAE,CAAC;QAC1B,KAAK,EAAE;YACL,KAAK,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;YACjC,WAAW,EAAE,MAAM,CAAC;SACrB,CAAC;QACF,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,qBAAqB,CAAC,kBAAkB,CAAC,EACzD,aAAa,EAAE,aAAa,EAC5B,SAAS,CAAC,EAAE,MAAM,EAClB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,qBAAqB,CAAC,CA2DhC"}
1
+ {"version":3,"file":"operate-analysis.d.ts","sourceRoot":"","sources":["../../src/tools/operate-analysis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAGvD,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAIhD,OAAO,EAEL,kBAAkB,EAElB,eAAe,EAKhB,MAAM,WAAW,CAAC;AAwBnB;;GAEG;AACH,UAAU,qBAAqB;IAC7B,MAAM,EAAE,wBAAwB,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE;QACR,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,OAAO,CAAC;QACtB,eAAe,EAAE,eAAe,CAAC;QACjC,QAAQ,EAAE,MAAM,EAAE,CAAC;QACnB,gBAAgB,EAAE;YAChB,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAC;YAC7B,OAAO,EAAE,MAAM,CAAC;SACjB,CAAC;QACF,eAAe,EAAE,MAAM,EAAE,CAAC;QAC1B,gBAAgB,EAAE,MAAM,EAAE,CAAC;QAC3B,eAAe,EAAE,MAAM,EAAE,CAAC;QAC1B,KAAK,EAAE;YACL,KAAK,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;YACjC,WAAW,EAAE,MAAM,CAAC;SACrB,CAAC;QACF,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,qBAAqB,CAAC,kBAAkB,CAAC,EACzD,aAAa,EAAE,aAAa,EAC5B,SAAS,CAAC,EAAE,MAAM,EAClB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC,qBAAqB,CAAC,CA2DhC"}
@@ -5,6 +5,7 @@ const mcp_client_registry_1 = require("../core/mcp-client-registry");
5
5
  const ai_provider_factory_1 = require("../core/ai-provider-factory");
6
6
  const shared_prompt_loader_1 = require("../core/shared-prompt-loader");
7
7
  const visualization_1 = require("../core/visualization");
8
+ const platform_utils_1 = require("../core/platform-utils");
8
9
  const operate_1 = require("./operate");
9
10
  /**
10
11
  * Analyzes user intent and generates operational proposal using AI tool loop
@@ -171,28 +172,14 @@ async function executeToolLoop(systemPrompt, userMessage, logger, pluginManager,
171
172
  */
172
173
  function parseAIResponse(response, logger) {
173
174
  logger.debug('Parsing AI response');
174
- // Try to extract JSON from code block first (Claude format)
175
- const jsonMatch = response.match(/```json\n([\s\S]+?)\n```/);
176
- let jsonContent;
177
- if (jsonMatch) {
178
- jsonContent = jsonMatch[1];
179
- }
180
- else {
181
- // Fallback: try to parse raw JSON response (Gemini format)
182
- // Look for JSON object starting with { and ending with }
183
- const rawJsonMatch = response.match(/^\s*(\{[\s\S]*\})\s*$/);
184
- if (rawJsonMatch) {
185
- jsonContent = rawJsonMatch[1];
186
- logger.debug('Parsing raw JSON response (no code block wrapper)');
187
- }
188
- else {
189
- const truncatedResponse = response.substring(0, 500);
190
- logger.error(`AI response not valid JSON. Response: ${truncatedResponse}`);
191
- throw new Error('AI did not return structured JSON response. Expected JSON object or ```json code block.');
192
- }
193
- }
194
175
  try {
195
- const parsed = JSON.parse(jsonContent);
176
+ // Robustly extract the JSON object from the AI response. Reuses the shared
177
+ // extractor (platform-utils), also used by recommend/schema/evaluators: it
178
+ // handles ```json / ``` code fences AND tolerates prose before or after the
179
+ // JSON object. Previously a stray sentence the model appended inside the
180
+ // ```json block made JSON.parse fail with "Unexpected non-whitespace
181
+ // character after JSON" — an intermittent operate flake.
182
+ const parsed = (0, platform_utils_1.extractJsonFromAIResponse)(response);
196
183
  // Validate required fields
197
184
  if (!parsed.analysis || typeof parsed.analysis !== 'string') {
198
185
  throw new Error('AI response missing required "analysis" field (string)');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vfarcic/dot-ai",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance",
5
5
  "mcpName": "io.github.vfarcic/dot-ai",
6
6
  "main": "dist/index.js",
@@ -115,7 +115,7 @@
115
115
  "@ai-sdk/openai": "^3.0.41",
116
116
  "@ai-sdk/openai-compatible": "^2.0.35",
117
117
  "@ai-sdk/xai": "^3.0.67",
118
- "@grpc/grpc-js": "^1.14.3",
118
+ "@grpc/grpc-js": "^1.14.4",
119
119
  "@grpc/proto-loader": "^0.8.0",
120
120
  "@kubernetes/client-node": "^1.3.0",
121
121
  "@modelcontextprotocol/sdk": "^1.27.1",
@@ -139,7 +139,8 @@
139
139
  "overrides": {
140
140
  "express-rate-limit": "^8.2.2",
141
141
  "fast-uri": ">=3.1.2",
142
- "protobufjs": "^8.2.0"
142
+ "protobufjs": "^8.2.0",
143
+ "esbuild": "^0.28.1"
143
144
  },
144
145
  "optionalDependencies": {
145
146
  "@rollup/rollup-linux-x64-gnu": "4.60.3"