@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.
- package/dist/core/git-utils.d.ts +46 -0
- package/dist/core/git-utils.d.ts.map +1 -1
- package/dist/core/git-utils.js +192 -1
- package/dist/core/user-prompts-loader.d.ts +129 -1
- package/dist/core/user-prompts-loader.d.ts.map +1 -1
- package/dist/core/user-prompts-loader.js +654 -68
- package/dist/interfaces/cors-headers.d.ts +41 -0
- package/dist/interfaces/cors-headers.d.ts.map +1 -0
- package/dist/interfaces/cors-headers.js +43 -0
- package/dist/interfaces/header-redaction.d.ts +28 -0
- package/dist/interfaces/header-redaction.d.ts.map +1 -0
- package/dist/interfaces/header-redaction.js +48 -0
- package/dist/interfaces/mcp.d.ts +22 -0
- package/dist/interfaces/mcp.d.ts.map +1 -1
- package/dist/interfaces/mcp.js +131 -7
- package/dist/interfaces/openapi-generator.d.ts.map +1 -1
- package/dist/interfaces/openapi-generator.js +11 -5
- package/dist/interfaces/rest-api.d.ts +59 -15
- package/dist/interfaces/rest-api.d.ts.map +1 -1
- package/dist/interfaces/rest-api.js +288 -52
- package/dist/interfaces/routes/index.d.ts.map +1 -1
- package/dist/interfaces/routes/index.js +35 -0
- package/dist/interfaces/schemas/common.d.ts +33 -0
- package/dist/interfaces/schemas/common.d.ts.map +1 -1
- package/dist/interfaces/schemas/common.js +20 -1
- package/dist/interfaces/schemas/index.d.ts +2 -2
- package/dist/interfaces/schemas/index.d.ts.map +1 -1
- package/dist/interfaces/schemas/index.js +14 -3
- package/dist/interfaces/schemas/prompts.d.ts +155 -0
- package/dist/interfaces/schemas/prompts.d.ts.map +1 -1
- package/dist/interfaces/schemas/prompts.js +134 -1
- package/dist/tools/prompts.d.ts.map +1 -1
- package/dist/tools/prompts.js +37 -2
- package/package.json +6 -4
package/dist/core/git-utils.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/core/git-utils.js
CHANGED
|
@@ -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
|
-
*
|
|
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;
|
|
1
|
+
{"version":3,"file":"user-prompts-loader.d.ts","sourceRoot":"","sources":["../../src/core/user-prompts-loader.ts"],"names":[],"mappings":"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"}
|