@vfarcic/dot-ai 1.21.1 → 1.23.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.
Files changed (34) hide show
  1. package/dist/core/git-utils.d.ts +46 -0
  2. package/dist/core/git-utils.d.ts.map +1 -1
  3. package/dist/core/git-utils.js +192 -1
  4. package/dist/core/user-prompts-loader.d.ts +129 -1
  5. package/dist/core/user-prompts-loader.d.ts.map +1 -1
  6. package/dist/core/user-prompts-loader.js +654 -68
  7. package/dist/interfaces/cors-headers.d.ts +41 -0
  8. package/dist/interfaces/cors-headers.d.ts.map +1 -0
  9. package/dist/interfaces/cors-headers.js +43 -0
  10. package/dist/interfaces/header-redaction.d.ts +28 -0
  11. package/dist/interfaces/header-redaction.d.ts.map +1 -0
  12. package/dist/interfaces/header-redaction.js +48 -0
  13. package/dist/interfaces/mcp.d.ts +22 -0
  14. package/dist/interfaces/mcp.d.ts.map +1 -1
  15. package/dist/interfaces/mcp.js +131 -7
  16. package/dist/interfaces/openapi-generator.d.ts.map +1 -1
  17. package/dist/interfaces/openapi-generator.js +11 -5
  18. package/dist/interfaces/rest-api.d.ts +59 -15
  19. package/dist/interfaces/rest-api.d.ts.map +1 -1
  20. package/dist/interfaces/rest-api.js +288 -52
  21. package/dist/interfaces/routes/index.d.ts.map +1 -1
  22. package/dist/interfaces/routes/index.js +35 -0
  23. package/dist/interfaces/schemas/common.d.ts +33 -0
  24. package/dist/interfaces/schemas/common.d.ts.map +1 -1
  25. package/dist/interfaces/schemas/common.js +20 -1
  26. package/dist/interfaces/schemas/index.d.ts +2 -2
  27. package/dist/interfaces/schemas/index.d.ts.map +1 -1
  28. package/dist/interfaces/schemas/index.js +14 -3
  29. package/dist/interfaces/schemas/prompts.d.ts +155 -0
  30. package/dist/interfaces/schemas/prompts.d.ts.map +1 -1
  31. package/dist/interfaces/schemas/prompts.js +134 -1
  32. package/dist/tools/prompts.d.ts.map +1 -1
  33. package/dist/tools/prompts.js +37 -2
  34. package/package.json +6 -4
@@ -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);
@@ -32,6 +32,24 @@ 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;
44
+ /**
45
+ * PRD #647 D1: when set, this override resolves to an already-ingested source
46
+ * (uploaded via POST /api/v1/prompts/sources) identified by this string —
47
+ * NOT a git clone. loadUserPrompts reads the cached uploaded files directly
48
+ * and NEVER calls cloneRepo, so a `local:<label>` or server-unreachable
49
+ * `?repo=` URL is served without any git operation. `repoUrl` carries the
50
+ * same identifier only for the scrubbed `source` echo (computePromptsSource).
51
+ */
52
+ ingestedSource?: string;
35
53
  }
36
54
  /**
37
55
  * Cache state for tracking repository freshness.
@@ -45,6 +63,88 @@ interface CacheState {
45
63
  branch: string;
46
64
  subPath: string;
47
65
  }
66
+ /**
67
+ * PRD #647 F5 / D2-M4 — max number of distinct ingested sources held in memory.
68
+ * The cache is push-populated by authenticated uploads, so without a bound it
69
+ * grows unbounded across distinct identifiers (a memory-growth vector). This
70
+ * is a simple access-ordered LRU cap: on overflow the least-recently-used entry
71
+ * (oldest `uploadedAt`, refreshed on every successful render) is evicted, and a
72
+ * later `?source=` render of an evicted identifier hits the existing D2
73
+ * render-miss guidance (re-upload required). Correctness over tuning per the
74
+ * frozen contract — 50 distinct sources is ample for the CLI's per-host usage.
75
+ */
76
+ export declare const MAX_INGESTED_SOURCES = 50;
77
+ /**
78
+ * Thrown by ingestPromptsSource on a malformed/unsafe upload so the REST handler
79
+ * can map it to a 400 (vs. a 500 for unexpected IO failures).
80
+ */
81
+ export declare class PromptsSourceValidationError extends Error {
82
+ constructor(message: string);
83
+ }
84
+ /**
85
+ * PRD #647 D2 — thrown when a render names an ingested `?source=<identifier>`
86
+ * that is not (or no longer) cached. It carries actionable re-upload guidance
87
+ * and is mapped to a 400 VALIDATION_ERROR by the REST handler. Deliberately
88
+ * distinct from the generic "Prompt not found" so the caller knows to re-upload
89
+ * the source rather than wonder why a known skill name is missing — and it never
90
+ * triggers (nor mentions) a git clone, since ingested identifiers are never
91
+ * cloned.
92
+ */
93
+ export declare class IngestedSourceNotFoundError extends Error {
94
+ constructor(message: string);
95
+ }
96
+ /**
97
+ * PRD #647 D5 — sanitize an untrusted POSIX `mode` string from an uploaded
98
+ * manifest into a safe numeric file mode. Strips the special bits
99
+ * (setuid 04000, setgid 02000, sticky 01000) so an uploaded skill file can never
100
+ * carry an exec-escalation surprise, and keeps only the standard rwx permission
101
+ * bits (07777 → 0777). Anything unparseable falls back to a sane 0644.
102
+ */
103
+ export declare function sanitizeIngestFileMode(mode: unknown): number;
104
+ /** Shape of the uploaded manifest (validated at runtime; all fields untrusted). */
105
+ export interface IngestPromptsSourceInput {
106
+ source: unknown;
107
+ contentHash?: unknown;
108
+ files: unknown;
109
+ }
110
+ /** Result echoed back to the caller after a successful ingest. */
111
+ export interface IngestPromptsSourceResult {
112
+ /** Credential-scrubbed identifier the render path uses via ?source=. */
113
+ source: string;
114
+ contentHash?: string;
115
+ fileCount: number;
116
+ /**
117
+ * 'ingested' — the manifest was decoded, hardened, and (re)written.
118
+ * 'unchanged' — PRD #647 D3 short-circuit: an identical contentHash was
119
+ * already cached for this identifier, so nothing was re-decoded or rewritten.
120
+ */
121
+ status: 'ingested' | 'unchanged';
122
+ }
123
+ /**
124
+ * PRD #647 M2/M4/D5 — validate, base64-decode, harden, and cache an uploaded
125
+ * skill source.
126
+ *
127
+ * The decoded files are written to a fresh 0700 staging directory, then
128
+ * atomically promoted into the per-identifier slot (a hash of the identifier)
129
+ * and registered in the in-memory ingested cache so the existing render path
130
+ * (loadUserPrompts → loadPromptsFromDir) resolves them with no git operation.
131
+ * The atomic promote (F3) means a failed/invalid re-upload never destroys the
132
+ * prior cached entry.
133
+ *
134
+ * Hardening, all applied BEFORE the prior slot is touched so a rejected upload
135
+ * is never partially cached:
136
+ * - D3 dedup: an identical `contentHash` already cached for this identifier
137
+ * short-circuits with status 'unchanged' — nothing is re-decoded or rewritten.
138
+ * - D5 file-count cap: more than MAX_INGEST_FILES files is rejected up front.
139
+ * - D5 total-size cap: summed DECODED bytes over MAX_INGEST_TOTAL_BYTES is rejected.
140
+ * - D5 zip-slip: every file path goes through sanitizeRelativePath (reused
141
+ * from git-utils) to reject traversal/absolute paths.
142
+ * - F3 NUL byte: a `\0` in a file path is rejected with a 400 BEFORE any fs
143
+ * call (sanitizeRelativePath does not catch it).
144
+ * - D5 mode bits: each file's POSIX `mode` is sanitized (setuid/setgid/sticky
145
+ * stripped) via sanitizeIngestFileMode before the file is written.
146
+ */
147
+ export declare function ingestPromptsSource(input: IngestPromptsSourceInput, logger: Logger): IngestPromptsSourceResult;
48
148
  /**
49
149
  * Read user prompts configuration from environment variables
50
150
  * Returns null if DOT_AI_USER_PROMPTS_REPO is not set
@@ -107,18 +207,46 @@ export declare function sanitizeUrlForLogging(url: string): string;
107
207
  * helper was deferred per the M2 follow-up scope.
108
208
  */
109
209
  export declare function scrubSourceUrl(url: string): string;
210
+ /**
211
+ * Raised when a PER-REQUEST prompts-repo override (PRD #581/#621) fails to load —
212
+ * e.g. the clone is rejected (missing/wrong forwarded credential) or the source is
213
+ * unreachable. An env-var-configured repo failure falls back to built-in prompts,
214
+ * but an override is an explicit caller request, so its failure must surface as an
215
+ * error instead of silently returning fewer skills ("fail open" — issue #575).
216
+ *
217
+ * The `message` is already credential-scrubbed by the thrower, so callers may
218
+ * surface it to clients directly.
219
+ */
220
+ export declare class UserPromptsOverrideError extends Error {
221
+ constructor(message: string);
222
+ }
110
223
  /**
111
224
  * Load user prompts from a git repository.
112
225
  *
113
226
  * When `override` is supplied, fetches from that repository for this call only,
114
227
  * ignoring DOT_AI_USER_PROMPTS_REPO env vars. Otherwise, uses env-var configuration.
115
- * Returns empty array if not configured or on error.
228
+ *
229
+ * Returns an empty array when not configured, or when an ENV-VAR-configured repo
230
+ * fails to load (graceful fallback to built-in prompts). When a per-request
231
+ * `override` fails to load, throws {@link UserPromptsOverrideError} instead — the
232
+ * caller asked for that specific source, so the failure must not be swallowed.
116
233
  */
117
234
  export declare function loadUserPrompts(logger: Logger, forceRefresh?: boolean, override?: UserPromptsOverride): Promise<Prompt[]>;
118
235
  /**
119
236
  * Clear the cache state (useful for testing)
120
237
  */
121
238
  export declare function clearUserPromptsCache(): void;
239
+ /**
240
+ * Clear the ingested-source cache (PRD #647, useful for testing).
241
+ * Only drops the in-memory registry; on-disk directories are left to be
242
+ * overwritten by the next upload or cleaned with the tmp directory.
243
+ */
244
+ export declare function clearIngestedPromptsSources(): void;
245
+ /**
246
+ * Inspect the ingested-source cache (PRD #647, for testing/debugging).
247
+ * Returns the scrubbed identifiers currently cached.
248
+ */
249
+ export declare function getIngestedPromptsSources(): string[];
122
250
  /**
123
251
  * Get current cache state (for testing/debugging)
124
252
  */
@@ -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":"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"}