@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
@@ -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");
@@ -72,9 +74,11 @@ const rbac_1 = require("../core/rbac");
72
74
  exports.UNPARSEABLE_QUERY_PLACEHOLDER = '?<redacted-unparseable>';
73
75
  /**
74
76
  * F3: req.url is logged on every request; with PRD #581 the query string may
75
- * carry `?repo=<user-supplied-url>` whose value can include credentials. This
76
- * helper rewrites the `repo` value to its credential-scrubbed form so the
77
- * raw token doesn't reach the log. Everything else is preserved verbatim.
77
+ * carry `?repo=<user-supplied-url>` whose value can include credentials, and
78
+ * PRD #647 adds `?source=<identifier>` which is equally credential-bearing (it
79
+ * may be a `https://user:tok@host` git URL). This helper rewrites BOTH values
80
+ * to their credential-scrubbed form so the raw token doesn't reach the log.
81
+ * Everything else is preserved verbatim.
78
82
  *
79
83
  * CodeRabbit Major B: on parse failure, we no longer return the input
80
84
  * verbatim — an unparseable URL is more likely than a parseable one to hide
@@ -86,8 +90,19 @@ exports.UNPARSEABLE_QUERY_PLACEHOLDER = '?<redacted-unparseable>';
86
90
  function sanitizeRequestUrlForLogging(url) {
87
91
  if (!url)
88
92
  return url;
89
- if (!url.includes('repo='))
93
+ // Fast path: only walk the URL when it carries a credential-bearing param
94
+ // we know how to scrub (PRD #581 `repo=`, PRD #647 `source=`).
95
+ //
96
+ // CodeRabbit C3: a percent-encoded param NAME (e.g. `r%65po=`, `s%6Frce=`)
97
+ // decodes to `repo`/`source` once parsed, so the literal-substring fast path
98
+ // would early-return WITHOUT scrubbing and leak the credential into the log.
99
+ // Any `%` means a name could be encoded, so fall through to the full
100
+ // parse-and-scrub below (URLSearchParams decodes the name, catching it).
101
+ if (!url.includes('repo=') &&
102
+ !url.includes('source=') &&
103
+ !url.includes('%')) {
90
104
  return url;
105
+ }
91
106
  try {
92
107
  // req.url is path-relative; provide a dummy base for URL parsing.
93
108
  const parsed = new node_url_1.URL(url, 'http://internal.invalid');
@@ -95,6 +110,13 @@ function sanitizeRequestUrlForLogging(url) {
95
110
  if (repo) {
96
111
  parsed.searchParams.set('repo', (0, user_prompts_loader_1.sanitizeUrlForLogging)(repo));
97
112
  }
113
+ // PRD #647 M5 (F2): `?source=` is scrubbed with the same deep helper used
114
+ // for the echoed `source` (userinfo + credential-bearing query params), so
115
+ // `?source=https://user:tok@host` never appears unscrubbed in the log.
116
+ const source = parsed.searchParams.get('source');
117
+ if (source) {
118
+ parsed.searchParams.set('source', (0, user_prompts_loader_1.scrubSourceUrl)(source));
119
+ }
98
120
  // Return path + search only (drop the dummy base).
99
121
  return parsed.pathname + parsed.search + parsed.hash;
100
122
  }
@@ -108,6 +130,150 @@ function sanitizeRequestUrlForLogging(url) {
108
130
  return url.slice(0, qIdx) + exports.UNPARSEABLE_QUERY_PLACEHOLDER;
109
131
  }
110
132
  }
133
+ /**
134
+ * Coerce an optional override string param (path/branch) supplied via query
135
+ * string or JSON body. Mirrors the `repo` guard in extractPromptsOverride:
136
+ * - non-string (array, number, object, boolean) → 400 (avoids a 500 from a
137
+ * malformed body reaching downstream code).
138
+ * - absent (null/undefined) or empty/whitespace-only → `undefined`, i.e.
139
+ * treated as not supplied so the downstream default (subPath '' / branch
140
+ * 'main') is preserved and an empty-string branch never reaches
141
+ * isValidGitBranch (which would otherwise reject it).
142
+ * - otherwise → the trimmed value.
143
+ */
144
+ function coerceOverrideStringParam(value, name) {
145
+ if (value === undefined || value === null) {
146
+ return { ok: true, value: undefined };
147
+ }
148
+ if (typeof value !== 'string') {
149
+ return {
150
+ ok: false,
151
+ message: `${name} must be a string (got ${Array.isArray(value) ? 'array' : typeof value})`,
152
+ };
153
+ }
154
+ const trimmed = value.trim();
155
+ return { ok: true, value: trimmed.length > 0 ? trimmed : undefined };
156
+ }
157
+ /**
158
+ * Read the per-request git credential from the X-Dot-AI-Git-Token header
159
+ * (PRD #621 M2). Node lowercases incoming header names and may present a
160
+ * repeated header as an array; normalize to a single non-empty string or
161
+ * undefined. The value is a secret, so it is never logged here.
162
+ */
163
+ function readGitTokenHeader(req) {
164
+ const raw = req.headers[cors_headers_1.GIT_TOKEN_HEADER_LC];
165
+ const value = Array.isArray(raw) ? raw[0] : raw;
166
+ if (typeof value !== 'string') {
167
+ return undefined;
168
+ }
169
+ const trimmed = value.trim();
170
+ return trimmed.length > 0 ? trimmed : undefined;
171
+ }
172
+ /**
173
+ * Extract and validate the per-request prompts override.
174
+ *
175
+ * Threads three optional, additive inputs from the request into a
176
+ * UserPromptsOverride (PRD #581 introduced `repo`; PRD #621 M1 adds `path`
177
+ * and `branch`):
178
+ * - repoParam → override.repoUrl (GET ?repo= / POST body `repo`)
179
+ * - pathParam → override.subPath (GET ?path= / POST body `path`)
180
+ * - branchParam → override.branch (GET ?branch= / POST body `branch`)
181
+ * - gitToken → override.gitToken (X-Dot-AI-Git-Token request header; M2)
182
+ *
183
+ * Returns:
184
+ * - { ok: true, override } when no `repo` is supplied (override undefined;
185
+ * any path/branch/token are ignored, since they only qualify an override —
186
+ * this keeps the no-`repo` / env-var-configured path unchanged).
187
+ * - { ok: true, override } when a syntactically valid override is supplied.
188
+ * - { ok: false, message } when the override fails validation (HTTP 400).
189
+ *
190
+ * The validation message is run through scrubCredentials so embedded tokens
191
+ * never reach the wire response.
192
+ *
193
+ * Backward compatibility (PRD #621, non-negotiable): when path/branch are
194
+ * absent or empty, the override carries `repoUrl` only — byte-identical to
195
+ * the PRD #581 behavior (same clone target: repo root, `main`). subPath and
196
+ * branch are populated ONLY for a non-empty value, so downstream defaults are
197
+ * untouched. The credential header is INERT unless a `?repo=` override is
198
+ * present: without a repo this returns `override: undefined` before the token
199
+ * is ever read, so the env-var path is unaffected by a forwarded header.
200
+ *
201
+ * Validation is delegated to getUserPromptsConfigFromOverride (scheme,
202
+ * sanitizeRelativePath for subPath, isValidGitBranch for branch) and happens
203
+ * BEFORE any clone or shared-cache mutation, so a rejected override can never
204
+ * corrupt the env-var-configured cache.
205
+ */
206
+ function extractPromptsOverride(repoParam, pathParam, branchParam, gitToken, sourceParam) {
207
+ // PRD #647 D1: an explicit `?source=<identifier>` selects an already-ingested
208
+ // (CLI-uploaded) source. It is the explicit ingested signal, so it takes
209
+ // precedence over `?repo=` and the clone-qualifying params (path/branch/token)
210
+ // do not apply — they only describe a git clone, which an ingested source is
211
+ // not. Resolution against the in-memory ingested cache (and the "never clone"
212
+ // guarantee) happens in loadUserPrompts via override.ingestedSource. The raw
213
+ // identifier doubles as repoUrl ONLY so computePromptsSource echoes the
214
+ // scrubbed source; it is never cloned or logged unscrubbed on this path.
215
+ if (typeof sourceParam === 'string' && sourceParam.trim() !== '') {
216
+ const identifier = sourceParam.trim();
217
+ return {
218
+ ok: true,
219
+ override: { repoUrl: identifier, ingestedSource: identifier },
220
+ };
221
+ }
222
+ // Treat absent (null/undefined) repo as no override. path/branch/token only
223
+ // qualify an override, so without a repo they are ignored (the credential
224
+ // header is inert on the env-var path).
225
+ if (repoParam === undefined || repoParam === null) {
226
+ return { ok: true, override: undefined };
227
+ }
228
+ // The wire contract says `repo` is a string. Anything else (array,
229
+ // number, object, boolean) is a 400 — otherwise downstream code
230
+ // would crash to a 500.
231
+ if (typeof repoParam !== 'string') {
232
+ return {
233
+ ok: false,
234
+ message: `repo must be a string (got ${Array.isArray(repoParam) ? 'array' : typeof repoParam})`,
235
+ };
236
+ }
237
+ const trimmed = repoParam.trim();
238
+ if (!trimmed) {
239
+ return { ok: true, override: undefined };
240
+ }
241
+ const candidate = { repoUrl: trimmed };
242
+ // PRD #621 M1: thread ?path= / body `path` into subPath (validated
243
+ // downstream by sanitizeRelativePath).
244
+ const pathResult = coerceOverrideStringParam(pathParam, 'path');
245
+ if (!pathResult.ok) {
246
+ return pathResult;
247
+ }
248
+ if (pathResult.value !== undefined) {
249
+ candidate.subPath = pathResult.value;
250
+ }
251
+ // PRD #621 M1: thread ?branch= / body `branch` into branch (validated
252
+ // downstream by isValidGitBranch).
253
+ const branchResult = coerceOverrideStringParam(branchParam, 'branch');
254
+ if (!branchResult.ok) {
255
+ return branchResult;
256
+ }
257
+ if (branchResult.value !== undefined) {
258
+ candidate.branch = branchResult.value;
259
+ }
260
+ // PRD #621 M2: the X-Dot-AI-Git-Token header (read by the handler and passed
261
+ // in already-normalized to a non-empty string or undefined) authenticates
262
+ // THIS override clone. It travels only as a header — never query/body — and
263
+ // is never echoed (computePromptsSource uses repoUrl only).
264
+ if (gitToken) {
265
+ candidate.gitToken = gitToken;
266
+ }
267
+ try {
268
+ // Throws on invalid scheme / subPath / branch.
269
+ (0, user_prompts_loader_1.getUserPromptsConfigFromOverride)(candidate);
270
+ return { ok: true, override: candidate };
271
+ }
272
+ catch (error) {
273
+ const raw = error instanceof Error ? error.message : 'Invalid override';
274
+ return { ok: false, message: (0, git_utils_1.scrubCredentials)(raw) };
275
+ }
276
+ }
111
277
  /**
112
278
  * HTTP status codes for REST responses
113
279
  */
@@ -245,6 +411,7 @@ class RestApiRouter {
245
411
  'GET:/api/v1/logs': () => this.handleGetLogs(req, res, requestId, searchParams),
246
412
  'GET:/api/v1/prompts': () => this.handlePromptsListRequest(req, res, requestId, searchParams),
247
413
  'POST:/api/v1/prompts/refresh': () => this.handlePromptsCacheRefresh(req, res, requestId, body),
414
+ 'POST:/api/v1/prompts/sources': () => this.handlePromptsSourceIngest(req, res, requestId, body),
248
415
  'POST:/api/v1/prompts/:promptName': () => this.handlePromptsGetRequest(req, res, requestId, params.promptName, body, searchParams),
249
416
  'GET:/api/v1/visualize/:sessionId': () => this.handleVisualize(req, res, requestId, params.sessionId, searchParams),
250
417
  'GET:/api/v1/events/remediations': () => this.handleRemediationSSE(req, res, requestId),
@@ -1086,53 +1253,21 @@ class RestApiRouter {
1086
1253
  await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'LOGS_ERROR', 'Failed to retrieve logs', { error: errorMessage });
1087
1254
  }
1088
1255
  }
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
1256
  /**
1130
1257
  * Handle prompts list requests
1131
1258
  */
1132
1259
  async handlePromptsListRequest(req, res, requestId, searchParams) {
1133
1260
  try {
1134
1261
  this.logger.info('Processing prompts list request', { requestId });
1135
- const overrideResult = this.extractPromptsOverride(searchParams.get('repo'));
1262
+ // PRD #581: ?repo= override. PRD #621 M1: ?path= / ?branch= thread into
1263
+ // candidate.subPath / candidate.branch (absent → unchanged behavior).
1264
+ // PRD #621 M2: X-Dot-AI-Git-Token header authenticates the override clone
1265
+ // (inert when no ?repo= override is present).
1266
+ // PRD #647 list-by-source: ?source= selects an already-ingested source,
1267
+ // resolved from the in-memory upload cache with no git operation (same
1268
+ // signal as the render path). Absent → byte-identical to today (env-var /
1269
+ // built-in set), so the no-?source= behavior is unchanged.
1270
+ const overrideResult = extractPromptsOverride(searchParams.get('repo'), searchParams.get('path'), searchParams.get('branch'), readGitTokenHeader(req), searchParams.get('source'));
1136
1271
  if (!overrideResult.ok) {
1137
1272
  await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'VALIDATION_ERROR', overrideResult.message);
1138
1273
  return;
@@ -1154,6 +1289,24 @@ class RestApiRouter {
1154
1289
  });
1155
1290
  }
1156
1291
  catch (error) {
1292
+ // A per-request override (?repo=) whose source can't be loaded is a
1293
+ // bad-gateway condition, not a server fault: surface it (issue #575)
1294
+ // instead of silently serving built-in prompts with HTTP 200. The
1295
+ // message is already credential-scrubbed by the loader.
1296
+ if (error instanceof user_prompts_loader_1.UserPromptsOverrideError) {
1297
+ await this.sendErrorResponse(res, requestId, HttpStatus.BAD_GATEWAY, 'PROMPTS_SOURCE_ERROR', error.message);
1298
+ return;
1299
+ }
1300
+ // PRD #647 list-by-source (D2): an unknown/evicted ?source= identifier is
1301
+ // a caller-actionable validation error — surface the re-upload guidance as
1302
+ // a 400 (same mapping the render handler uses), NOT a generic 500 or a
1303
+ // silent success-with-builtins. The message names POST
1304
+ // /api/v1/prompts/sources and carries no clone/git/scheme vocabulary.
1305
+ const listErrorMessage = error instanceof Error ? error.message : String(error);
1306
+ if (listErrorMessage.includes('Ingested source not found')) {
1307
+ await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'VALIDATION_ERROR', listErrorMessage);
1308
+ return;
1309
+ }
1157
1310
  this.logger.error('Prompts list request failed', error instanceof Error ? error : new Error(String(error)), {
1158
1311
  requestId,
1159
1312
  });
@@ -1169,7 +1322,15 @@ class RestApiRouter {
1169
1322
  requestId,
1170
1323
  promptName,
1171
1324
  });
1172
- const overrideResult = this.extractPromptsOverride(searchParams.get('repo'));
1325
+ // PRD #581: ?repo= override. PRD #621 M1: ?path= / ?branch= thread into
1326
+ // candidate.subPath / candidate.branch (absent → unchanged behavior).
1327
+ // PRD #621 M2: X-Dot-AI-Git-Token header authenticates the override clone
1328
+ // (inert when no ?repo= override is present).
1329
+ // PRD #647 D1/M3: ?source= selects an already-ingested source, resolved
1330
+ // from the in-memory upload cache with no git operation (precedence over
1331
+ // ?repo=). Absent → unchanged behavior, so the env-var/clone paths are
1332
+ // byte-identical to today.
1333
+ const overrideResult = extractPromptsOverride(searchParams.get('repo'), searchParams.get('path'), searchParams.get('branch'), readGitTokenHeader(req), searchParams.get('source'));
1173
1334
  if (!overrideResult.ok) {
1174
1335
  await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'VALIDATION_ERROR', overrideResult.message);
1175
1336
  return;
@@ -1192,14 +1353,24 @@ class RestApiRouter {
1192
1353
  });
1193
1354
  }
1194
1355
  catch (error) {
1356
+ // A per-request override (?repo=) whose source can't be loaded is a
1357
+ // bad-gateway condition (issue #575); surface it before the generic
1358
+ // validation/500 mapping. The message is already credential-scrubbed.
1359
+ if (error instanceof user_prompts_loader_1.UserPromptsOverrideError) {
1360
+ await this.sendErrorResponse(res, requestId, HttpStatus.BAD_GATEWAY, 'PROMPTS_SOURCE_ERROR', error.message);
1361
+ return;
1362
+ }
1195
1363
  const errorMessage = error instanceof Error ? error.message : String(error);
1196
1364
  this.logger.error('Prompt get request failed', error instanceof Error ? error : new Error(String(error)), {
1197
1365
  requestId,
1198
1366
  promptName,
1199
1367
  });
1200
- // Check if it's a validation error (missing required arguments or prompt not found)
1368
+ // Check if it's a validation error (missing required arguments, prompt not
1369
+ // found, or a missing ingested ?source= identifier — PRD #647 D2, which
1370
+ // carries re-upload guidance and must be a 400, not a 500).
1201
1371
  const isValidationError = errorMessage.includes('Missing required arguments') ||
1202
- errorMessage.includes('Prompt not found');
1372
+ errorMessage.includes('Prompt not found') ||
1373
+ errorMessage.includes('Ingested source not found');
1203
1374
  await this.sendErrorResponse(res, requestId, isValidationError
1204
1375
  ? HttpStatus.BAD_REQUEST
1205
1376
  : HttpStatus.INTERNAL_SERVER_ERROR, isValidationError ? 'VALIDATION_ERROR' : 'PROMPT_GET_ERROR', errorMessage);
@@ -1214,9 +1385,13 @@ class RestApiRouter {
1214
1385
  requestId,
1215
1386
  });
1216
1387
  // body.repo type is checked inside extractPromptsOverride (it accepts
1217
- // unknown and rejects non-string values with 400 — see F2).
1388
+ // unknown and rejects non-string values with 400 — see F2). PRD #621 M1:
1389
+ // body `path` / `branch` thread into candidate.subPath / candidate.branch
1390
+ // (absent → unchanged behavior); both are likewise type-checked.
1218
1391
  const bodyObj = body;
1219
- const overrideResult = this.extractPromptsOverride(bodyObj?.repo);
1392
+ // PRD #621 M2: the credential always travels as the X-Dot-AI-Git-Token
1393
+ // header — never the body — and is inert without a repo override.
1394
+ const overrideResult = extractPromptsOverride(bodyObj?.repo, bodyObj?.path, bodyObj?.branch, readGitTokenHeader(req));
1220
1395
  if (!overrideResult.ok) {
1221
1396
  await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'VALIDATION_ERROR', overrideResult.message);
1222
1397
  return;
@@ -1244,10 +1419,67 @@ class RestApiRouter {
1244
1419
  });
1245
1420
  }
1246
1421
  catch (error) {
1422
+ // A per-request override (body.repo) whose source can't be loaded is a
1423
+ // bad-gateway condition (issue #575); surface it instead of a generic
1424
+ // 500. The message is already credential-scrubbed by the loader.
1425
+ if (error instanceof user_prompts_loader_1.UserPromptsOverrideError) {
1426
+ await this.sendErrorResponse(res, requestId, HttpStatus.BAD_GATEWAY, 'PROMPTS_SOURCE_ERROR', error.message);
1427
+ return;
1428
+ }
1247
1429
  this.logger.error('Prompts cache refresh failed', error instanceof Error ? error : new Error(String(error)), { requestId });
1248
1430
  await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'PROMPTS_CACHE_REFRESH_ERROR', 'Failed to refresh prompts cache');
1249
1431
  }
1250
1432
  }
1433
+ /**
1434
+ * Handle prompts source ingestion (PRD #647 M2).
1435
+ *
1436
+ * Accepts a JSON manifest { source, contentHash, files:[{path, content(base64),
1437
+ * mode}] }, base64-decodes and caches the uploaded skill source keyed by its
1438
+ * `source` identifier in the in-memory ingested cache. A later
1439
+ * POST /api/v1/prompts/:promptName?source=<identifier> renders it through the
1440
+ * existing render path with no git operation. The (scrubbed) source is echoed
1441
+ * back. Bearer-gated by the same checkBearerAuth path as every non-OpenAPI
1442
+ * request.
1443
+ */
1444
+ async handlePromptsSourceIngest(req, res, requestId, body) {
1445
+ try {
1446
+ this.logger.info('Processing prompts source ingest request', {
1447
+ requestId,
1448
+ });
1449
+ // All fields are untrusted; ingestPromptsSource validates and decodes
1450
+ // them, throwing PromptsSourceValidationError (→ 400) on a malformed or
1451
+ // unsafe manifest before anything is cached.
1452
+ const manifest = body;
1453
+ const result = (0, user_prompts_loader_1.ingestPromptsSource)({
1454
+ source: manifest?.source,
1455
+ contentHash: manifest?.contentHash,
1456
+ files: manifest?.files,
1457
+ }, this.logger);
1458
+ const response = {
1459
+ success: true,
1460
+ data: result,
1461
+ meta: {
1462
+ timestamp: new Date().toISOString(),
1463
+ requestId,
1464
+ version: this.config.version,
1465
+ },
1466
+ };
1467
+ await this.sendJsonResponse(res, HttpStatus.OK, response);
1468
+ this.logger.info('Prompts source ingest completed', {
1469
+ requestId,
1470
+ source: result.source,
1471
+ fileCount: result.fileCount,
1472
+ });
1473
+ }
1474
+ catch (error) {
1475
+ const isValidationError = error instanceof user_prompts_loader_1.PromptsSourceValidationError;
1476
+ const errorMessage = error instanceof Error ? error.message : String(error);
1477
+ this.logger.error('Prompts source ingest failed', error instanceof Error ? error : new Error(String(error)), { requestId });
1478
+ await this.sendErrorResponse(res, requestId, isValidationError
1479
+ ? HttpStatus.BAD_REQUEST
1480
+ : HttpStatus.INTERNAL_SERVER_ERROR, isValidationError ? 'VALIDATION_ERROR' : 'PROMPTS_SOURCE_INGEST_ERROR', isValidationError ? errorMessage : 'Failed to ingest prompts source');
1481
+ }
1482
+ }
1251
1483
  /**
1252
1484
  * Handle visualization requests (PRD #317)
1253
1485
  * Returns structured visualization data for a query session
@@ -1599,7 +1831,9 @@ class RestApiRouter {
1599
1831
  };
1600
1832
  if (this.config.enableCors) {
1601
1833
  headers['Access-Control-Allow-Origin'] = '*';
1602
- headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization';
1834
+ // R-2: use the shared allowlist (single source of truth in cors-headers.ts)
1835
+ // so the SSE preflight includes X-Dot-AI-Git-Token like every other route.
1836
+ headers['Access-Control-Allow-Headers'] = cors_headers_1.REST_CORS_ALLOW_HEADERS;
1603
1837
  }
1604
1838
  res.writeHead(HttpStatus.OK, headers);
1605
1839
  this.logger.info('SSE connection established', { requestId });
@@ -2187,7 +2421,9 @@ class RestApiRouter {
2187
2421
  setCorsHeaders(res) {
2188
2422
  res.setHeader('Access-Control-Allow-Origin', '*');
2189
2423
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
2190
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
2424
+ // PRD #621 M2 / Decision 1: advertise the X-Dot-AI-Git-Token credential
2425
+ // header (shared with the mcp.ts allowlist via cors-headers.ts).
2426
+ res.setHeader('Access-Control-Allow-Headers', cors_headers_1.REST_CORS_ALLOW_HEADERS);
2191
2427
  res.setHeader('Access-Control-Max-Age', '86400');
2192
2428
  }
2193
2429
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/interfaces/routes/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAuM5E;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,eAAe,CAC5C,OAAO,EACP,OAAO,EACP,OAAO,EACP,OAAO,CACR,EAyVA,CAAC;AAEF;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAInE;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/interfaces/routes/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAgN5E;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,eAAe,CAC5C,OAAO,EACP,OAAO,EACP,OAAO,EACP,OAAO,CACR,EA6XA,CAAC;AAEF;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAInE;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
@@ -286,8 +286,17 @@ exports.routeDefinitions = [
286
286
  method: 'GET',
287
287
  description: 'List all available prompts',
288
288
  tags: ['Prompts'],
289
+ // PRD #647 list-by-source: declare the per-request query params the list
290
+ // endpoint accepts. `?source=` enumerates a previously-ingested source (no
291
+ // git clone); `?repo=`/`?path=`/`?branch=` are the existing git overrides.
292
+ query: schemas_1.PromptsListQuerySchema,
289
293
  response: schemas_1.PromptsListResponseSchema,
290
294
  errorResponses: {
295
+ // PRD #647 list-by-source (D2): an unknown/evicted ?source= identifier
296
+ // returns 400 VALIDATION_ERROR with re-upload guidance — same contract as
297
+ // the render endpoint's ?source= miss.
298
+ 400: schemas_1.PromptValidationErrorSchema,
299
+ 502: schemas_1.PromptsSourceErrorSchema,
291
300
  500: schemas_1.InternalServerErrorSchema,
292
301
  },
293
302
  },
@@ -298,19 +307,45 @@ exports.routeDefinitions = [
298
307
  tags: ['Prompts'],
299
308
  response: schemas_1.PromptsCacheRefreshResponseSchema,
300
309
  errorResponses: {
310
+ 502: schemas_1.PromptsSourceErrorSchema,
301
311
  500: schemas_1.PromptsCacheRefreshErrorSchema,
302
312
  },
303
313
  },
314
+ {
315
+ path: '/api/v1/prompts/sources',
316
+ method: 'POST',
317
+ description: 'Ingest (upload) a skill source the server caches and renders via POST /api/v1/prompts/:promptName?source=<identifier> with no git clone (PRD #647)',
318
+ tags: ['Prompts'],
319
+ body: schemas_1.PromptsSourceIngestRequestSchema,
320
+ response: schemas_1.PromptsSourceIngestResponseSchema,
321
+ errorResponses: {
322
+ 400: schemas_1.PromptsSourceIngestErrorSchema,
323
+ // PRD #647 A7/A9 (CodeRabbit): the 512 KiB raw-body cap returns 413
324
+ // (PAYLOAD_TOO_LARGE) from mcp.ts before route dispatch — document it.
325
+ 413: schemas_1.PromptsSourceIngestPayloadTooLargeErrorSchema,
326
+ // PRD #647 A8 (CodeRabbit): narrowed to the only 500 code this handler
327
+ // emits, instead of the shared broad InternalServerErrorSchema union.
328
+ 500: schemas_1.PromptsSourceIngestServerErrorSchema,
329
+ },
330
+ },
304
331
  {
305
332
  path: '/api/v1/prompts/:promptName',
306
333
  method: 'POST',
307
334
  description: 'Get a prompt with rendered template arguments',
308
335
  tags: ['Prompts'],
309
336
  params: PromptNameParamsSchema,
337
+ // PRD #647 A6 (CodeRabbit): declare the per-request render query params
338
+ // (?source= ingest-render, plus ?repo=/?path=/?branch= overrides).
339
+ query: schemas_1.PromptGetQuerySchema,
310
340
  body: schemas_1.PromptGetRequestSchema,
311
341
  response: schemas_1.PromptGetResponseSchema,
312
342
  errorResponses: {
343
+ // PRD #647 (CodeRabbit): the render handler returns 400 VALIDATION_ERROR
344
+ // when ?source= resolution fails (unresolvable ingested source → D2) or
345
+ // required arguments are missing — document that contract.
346
+ 400: schemas_1.PromptValidationErrorSchema,
313
347
  404: schemas_1.PromptNotFoundErrorSchema,
348
+ 502: schemas_1.PromptsSourceErrorSchema,
314
349
  500: schemas_1.InternalServerErrorSchema,
315
350
  },
316
351
  },
@@ -119,6 +119,23 @@ export declare const MethodNotAllowedErrorSchema: z.ZodObject<{
119
119
  code: z.ZodLiteral<"METHOD_NOT_ALLOWED">;
120
120
  }, z.core.$strip>;
121
121
  }, z.core.$strip>;
122
+ /**
123
+ * 413 returned when a request body exceeds a per-route raw-body cap (PRD #647:
124
+ * the 512 KiB prompts source ingest cap). The handler emits PAYLOAD_TOO_LARGE.
125
+ */
126
+ export declare const PayloadTooLargeErrorSchema: z.ZodObject<{
127
+ success: z.ZodLiteral<false>;
128
+ meta: z.ZodOptional<z.ZodObject<{
129
+ timestamp: z.ZodString;
130
+ requestId: z.ZodOptional<z.ZodString>;
131
+ version: z.ZodString;
132
+ }, z.core.$strip>>;
133
+ error: z.ZodObject<{
134
+ message: z.ZodString;
135
+ details: z.ZodOptional<z.ZodAny>;
136
+ code: z.ZodLiteral<"PAYLOAD_TOO_LARGE">;
137
+ }, z.core.$strip>;
138
+ }, z.core.$strip>;
122
139
  export declare const ServiceUnavailableErrorSchema: z.ZodObject<{
123
140
  success: z.ZodLiteral<false>;
124
141
  meta: z.ZodOptional<z.ZodObject<{
@@ -137,6 +154,21 @@ export declare const ServiceUnavailableErrorSchema: z.ZodObject<{
137
154
  }>;
138
155
  }, z.core.$strip>;
139
156
  }, z.core.$strip>;
157
+ export declare const BadGatewayErrorSchema: z.ZodObject<{
158
+ success: z.ZodLiteral<false>;
159
+ meta: z.ZodOptional<z.ZodObject<{
160
+ timestamp: z.ZodString;
161
+ requestId: z.ZodOptional<z.ZodString>;
162
+ version: z.ZodString;
163
+ }, z.core.$strip>>;
164
+ error: z.ZodObject<{
165
+ message: z.ZodString;
166
+ details: z.ZodOptional<z.ZodAny>;
167
+ code: z.ZodEnum<{
168
+ PROMPTS_SOURCE_ERROR: "PROMPTS_SOURCE_ERROR";
169
+ }>;
170
+ }, z.core.$strip>;
171
+ }, z.core.$strip>;
140
172
  export declare const InternalServerErrorSchema: z.ZodObject<{
141
173
  success: z.ZodLiteral<false>;
142
174
  meta: z.ZodOptional<z.ZodObject<{
@@ -166,6 +198,7 @@ export declare const InternalServerErrorSchema: z.ZodObject<{
166
198
  SESSION_RETRIEVAL_ERROR: "SESSION_RETRIEVAL_ERROR";
167
199
  MIGRATION_ERROR: "MIGRATION_ERROR";
168
200
  PROMPTS_CACHE_REFRESH_ERROR: "PROMPTS_CACHE_REFRESH_ERROR";
201
+ PROMPTS_SOURCE_INGEST_ERROR: "PROMPTS_SOURCE_INGEST_ERROR";
169
202
  USER_MANAGEMENT_ERROR: "USER_MANAGEMENT_ERROR";
170
203
  }>;
171
204
  }, z.core.$strip>;
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/interfaces/schemas/common.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;GAEG;AACH,eAAO,MAAM,UAAU;;;;iBAOrB,CAAC;AAEH,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAE9C;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;iBAI7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;GAGG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;iBAOhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAChE,UAAU,EAAE,CAAC;;;;;;;;kBAOd;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;iBAI9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;iBAI9B,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;iBAUhC,CAAC;AAEH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;iBAItC,CAAC;AAEH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;iBASxC,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwBpC,CAAC"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/interfaces/schemas/common.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;GAEG;AACH,eAAO,MAAM,UAAU;;;;iBAOrB,CAAC;AAEH,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAE9C;;GAEG;AACH,eAAO,MAAM,kBAAkB;;;;iBAI7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;GAGG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;iBAOhC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;GAEG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAChE,UAAU,EAAE,CAAC;;;;;;;;kBAOd;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;iBAI9B,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;iBAI9B,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;iBAUhC,CAAC;AAEH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;iBAItC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;iBAIrC,CAAC;AAEH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;iBASxC,CAAC;AAEH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;iBAQhC,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyBpC,CAAC"}
@@ -6,7 +6,7 @@
6
6
  * PRD #354: REST API Route Registry with Auto-Generated OpenAPI and Test Fixtures
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.InternalServerErrorSchema = exports.ServiceUnavailableErrorSchema = exports.MethodNotAllowedErrorSchema = exports.BadRequestErrorSchema = exports.NotFoundErrorSchema = exports.ErrorResponseSchema = exports.RestApiResponseSchema = exports.ErrorDetailsSchema = exports.MetaSchema = void 0;
9
+ exports.InternalServerErrorSchema = exports.BadGatewayErrorSchema = exports.ServiceUnavailableErrorSchema = exports.PayloadTooLargeErrorSchema = exports.MethodNotAllowedErrorSchema = exports.BadRequestErrorSchema = exports.NotFoundErrorSchema = exports.ErrorResponseSchema = exports.RestApiResponseSchema = exports.ErrorDetailsSchema = exports.MetaSchema = void 0;
10
10
  exports.createSuccessResponseSchema = createSuccessResponseSchema;
11
11
  const zod_1 = require("zod");
12
12
  /**
@@ -80,6 +80,15 @@ exports.MethodNotAllowedErrorSchema = exports.ErrorResponseSchema.extend({
80
80
  code: zod_1.z.literal('METHOD_NOT_ALLOWED'),
81
81
  }),
82
82
  });
83
+ /**
84
+ * 413 returned when a request body exceeds a per-route raw-body cap (PRD #647:
85
+ * the 512 KiB prompts source ingest cap). The handler emits PAYLOAD_TOO_LARGE.
86
+ */
87
+ exports.PayloadTooLargeErrorSchema = exports.ErrorResponseSchema.extend({
88
+ error: exports.ErrorDetailsSchema.extend({
89
+ code: zod_1.z.literal('PAYLOAD_TOO_LARGE'),
90
+ }),
91
+ });
83
92
  exports.ServiceUnavailableErrorSchema = exports.ErrorResponseSchema.extend({
84
93
  error: exports.ErrorDetailsSchema.extend({
85
94
  code: zod_1.z.enum([
@@ -90,6 +99,15 @@ exports.ServiceUnavailableErrorSchema = exports.ErrorResponseSchema.extend({
90
99
  ]),
91
100
  }),
92
101
  });
102
+ exports.BadGatewayErrorSchema = exports.ErrorResponseSchema.extend({
103
+ error: exports.ErrorDetailsSchema.extend({
104
+ code: zod_1.z.enum([
105
+ // A per-request prompts-repo override (?repo=) whose source could not be
106
+ // cloned — bad/missing forwarded credential or unreachable host (issue #575).
107
+ 'PROMPTS_SOURCE_ERROR',
108
+ ]),
109
+ }),
110
+ });
93
111
  exports.InternalServerErrorSchema = exports.ErrorResponseSchema.extend({
94
112
  error: exports.ErrorDetailsSchema.extend({
95
113
  code: zod_1.z.enum([
@@ -111,6 +129,7 @@ exports.InternalServerErrorSchema = exports.ErrorResponseSchema.extend({
111
129
  'SESSION_RETRIEVAL_ERROR',
112
130
  'MIGRATION_ERROR',
113
131
  'PROMPTS_CACHE_REFRESH_ERROR',
132
+ 'PROMPTS_SOURCE_INGEST_ERROR',
114
133
  'USER_MANAGEMENT_ERROR',
115
134
  ]),
116
135
  }),