@aexol/spectral 0.9.245 → 0.9.247

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 (43) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/cli.js +6 -0
  3. package/dist/commands/secret.d.ts +16 -0
  4. package/dist/commands/secret.d.ts.map +1 -0
  5. package/dist/commands/secret.js +153 -0
  6. package/dist/commands/serve.d.ts.map +1 -1
  7. package/dist/commands/serve.js +21 -0
  8. package/dist/extensions/list-secrets.d.ts +14 -0
  9. package/dist/extensions/list-secrets.d.ts.map +1 -0
  10. package/dist/extensions/list-secrets.js +63 -0
  11. package/dist/generated/zeus/const.d.ts.map +1 -1
  12. package/dist/generated/zeus/const.js +16 -0
  13. package/dist/generated/zeus/index.d.ts +73 -0
  14. package/dist/generated/zeus/index.d.ts.map +1 -1
  15. package/dist/relay/routing.d.ts +1 -1
  16. package/dist/relay/routing.d.ts.map +1 -1
  17. package/dist/relay/routing.js +93 -17
  18. package/dist/sdk/coding-agent/utils/shell.d.ts.map +1 -1
  19. package/dist/sdk/coding-agent/utils/shell.js +15 -1
  20. package/dist/secrets/index.d.ts +11 -0
  21. package/dist/secrets/index.d.ts.map +1 -0
  22. package/dist/secrets/index.js +10 -0
  23. package/dist/secrets/store.d.ts +109 -0
  24. package/dist/secrets/store.d.ts.map +1 -0
  25. package/dist/secrets/store.js +415 -0
  26. package/dist/server/agent-bridge.d.ts.map +1 -1
  27. package/dist/server/agent-bridge.js +20 -2
  28. package/dist/server/attachments/cleanup.d.ts +42 -0
  29. package/dist/server/attachments/cleanup.d.ts.map +1 -0
  30. package/dist/server/attachments/cleanup.js +100 -0
  31. package/dist/server/attachments/persist.d.ts +131 -0
  32. package/dist/server/attachments/persist.d.ts.map +1 -0
  33. package/dist/server/attachments/persist.js +406 -0
  34. package/dist/server/handlers/secrets.d.ts +40 -0
  35. package/dist/server/handlers/secrets.d.ts.map +1 -0
  36. package/dist/server/handlers/secrets.js +78 -0
  37. package/dist/server/session-stream.d.ts +5 -0
  38. package/dist/server/session-stream.d.ts.map +1 -1
  39. package/dist/server/session-stream.js +51 -1
  40. package/dist/server/storage.d.ts +6 -0
  41. package/dist/server/storage.d.ts.map +1 -1
  42. package/dist/server/storage.js +9 -0
  43. package/package.json +1 -1
@@ -20,7 +20,7 @@ import { type WizardLoginResult } from "./wizard-login.js";
20
20
  * `id` is populated when the route had an `:id` placeholder.
21
21
  */
22
22
  interface RouteMatch {
23
- route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "unbind_studio" | "get_project_observations" | "list_project_sessions" | "cleanup_project_sessions" | "cleanup_all_sessions" | "create_session" | "get_session" | "get_session_active_agent" | "update_session_active_agent" | "get_session_messages" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "list_directory" | "get_home_directory" | "create_directory" | "search_project_files" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "reload_mcp_config" | "refresh_native_extensions" | "refresh_cloud_agents" | "get_settings" | "put_settings" | "get_agent_settings" | "put_agent_settings" | "get_prompt_mutation_settings" | "put_prompt_mutation_settings" | "wizard_login" | "list_agents" | "list_dev_processes" | "start_dev_process" | "kill_dev_process" | "restart_dev_process" | "scan_dev_servers" | "kill_dev_server";
23
+ route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "unbind_studio" | "get_project_observations" | "list_project_sessions" | "cleanup_project_sessions" | "cleanup_all_sessions" | "create_session" | "get_session" | "get_session_active_agent" | "update_session_active_agent" | "get_session_messages" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "list_directory" | "get_home_directory" | "create_directory" | "search_project_files" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "reload_mcp_config" | "refresh_native_extensions" | "refresh_cloud_agents" | "get_settings" | "put_settings" | "get_agent_settings" | "put_agent_settings" | "get_prompt_mutation_settings" | "put_prompt_mutation_settings" | "wizard_login" | "list_agents" | "list_dev_processes" | "start_dev_process" | "kill_dev_process" | "restart_dev_process" | "scan_dev_servers" | "kill_dev_server" | "list_secrets" | "create_secret" | "delete_secret";
24
24
  id?: string;
25
25
  /** Parsed query params, if the path carried a `?...` suffix. */
26
26
  query?: URLSearchParams;
@@ -1 +1 @@
1
- {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/relay/routing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAKvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAmDzD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AASzD,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAM7E;;;GAGG;AACH,UAAU,UAAU;IAClB,KAAK,EACD,eAAe,GACf,gBAAgB,GAChB,gBAAgB,GAChB,gBAAgB,GAChB,aAAa,GACb,eAAe,GACf,0BAA0B,GAC1B,uBAAuB,GACvB,0BAA0B,GAC1B,sBAAsB,GACtB,gBAAgB,GAChB,aAAa,GACb,0BAA0B,GAC1B,6BAA6B,GAC7B,sBAAsB,GACtB,gBAAgB,GAChB,gBAAgB,GAChB,oBAAoB,GACpB,4BAA4B,GAC5B,iBAAiB,GACjB,6BAA6B,GAC7B,cAAc,GACd,wBAAwB,GACxB,gBAAgB,GAChB,oBAAoB,GACpB,kBAAkB,GAClB,sBAAsB,GACtB,gBAAgB,GAChB,kBAAkB,GAClB,eAAe,GACf,oBAAoB,GACpB,iBAAiB,GACjB,mBAAmB,GACnB,2BAA2B,GAC3B,sBAAsB,GACtB,cAAc,GACd,cAAc,GACd,oBAAoB,GACpB,oBAAoB,GACpB,8BAA8B,GAC9B,8BAA8B,GAC9B,cAAc,GACd,aAAa,GACb,oBAAoB,GACpB,mBAAmB,GACnB,kBAAkB,GAClB,qBAAqB,GACrB,kBAAkB,GAClB,iBAAiB,CAAC;IACtB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,gEAAgE;IAChE,KAAK,CAAC,EAAE,eAAe,CAAC;IACxB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,UAAU,GAAG,IAAI,CA6PnB;AAMD;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,YAAY,CAAC;IACpB,OAAO,EAAE,oBAAoB,CAAC;IAC9B,6FAA6F;IAC7F,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,uGAAuG;IACvG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,MAAM,CAAC,EAAE;QACP,KAAK,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;QACrC,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;KACrC,CAAC;IACF;;;;OAIG;IACH,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,wEAAwE;IACxE,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC;;;;;;;;;OASG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,uBAAuB,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1D,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACrD;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,OAAO,CAAC,CAgZlB"}
1
+ {"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/relay/routing.ts"],"names":[],"mappings":"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"}
@@ -25,7 +25,26 @@ import { handleGetAgentSettingsAsync, handleListAgents, handlePutAgentSettingsAs
25
25
  import { handleGetSettingsAsync, handlePutSettingsAsync, } from "../server/handlers/settings.js";
26
26
  import { handleGetPromptMutationSettings, handlePutPromptMutationSettings, } from "../server/handlers/prompt-mutation-settings.js";
27
27
  import { handleKillDevProcess, handleKillDetectedDevServer, handleListDevProcesses, handleRestartDevProcess, handleScanDevServers, handleStartDevProcess, } from "../server/handlers/dev-processes.js";
28
+ import { handleDeleteSecret, handleListSecrets, handleSetSecret, } from "../server/handlers/secrets.js";
28
29
  import { startWizardLogin } from "./wizard-login.js";
30
+ // ---------------------------------------------------------------------------
31
+ // Path matching
32
+ // ---------------------------------------------------------------------------
33
+ /**
34
+ * `decodeURIComponent` that never throws. Malformed percent-encoding (e.g.
35
+ * `%ZZ`) returns `null` instead of a URIError escaping `matchRoute` — the
36
+ * matcher sits OUTSIDE the REST try/catch, so a synchronous throw here would
37
+ * surface as an unhandled rejection and could take down `spectral serve`.
38
+ * Callers treat `null` as no-match → 404.
39
+ */
40
+ function decodeSegment(raw) {
41
+ try {
42
+ return decodeURIComponent(raw);
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
29
48
  /**
30
49
  * Inline path matcher. Returns `null` for any path/method combination we
31
50
  * don't recognise; the caller turns that into a `404 Unknown route`.
@@ -134,7 +153,9 @@ export function matchRoute(method, path) {
134
153
  // /api/projects/:id/files/search
135
154
  const projectFilesMatch = /^\/api\/projects\/([^/]+)\/files\/search$/.exec(cleanPath);
136
155
  if (projectFilesMatch) {
137
- const id = decodeURIComponent(projectFilesMatch[1]);
156
+ const id = decodeSegment(projectFilesMatch[1]);
157
+ if (id === null)
158
+ return null;
138
159
  if (method === "GET")
139
160
  return { route: "search_project_files", id, query };
140
161
  return null;
@@ -142,14 +163,18 @@ export function matchRoute(method, path) {
142
163
  // /api/projects/:id/bind-studio and /api/projects/:id/unbind-studio
143
164
  const bindStudioMatch = /^\/api\/projects\/([^/]+)\/bind-studio$/.exec(cleanPath);
144
165
  if (bindStudioMatch) {
145
- const id = decodeURIComponent(bindStudioMatch[1]);
166
+ const id = decodeSegment(bindStudioMatch[1]);
167
+ if (id === null)
168
+ return null;
146
169
  if (method === "POST")
147
170
  return { route: "bind_studio", id };
148
171
  return null;
149
172
  }
150
173
  const unbindStudioMatch = /^\/api\/projects\/([^/]+)\/unbind-studio$/.exec(cleanPath);
151
174
  if (unbindStudioMatch) {
152
- const id = decodeURIComponent(unbindStudioMatch[1]);
175
+ const id = decodeSegment(unbindStudioMatch[1]);
176
+ if (id === null)
177
+ return null;
153
178
  if (method === "POST")
154
179
  return { route: "unbind_studio", id };
155
180
  return null;
@@ -157,7 +182,9 @@ export function matchRoute(method, path) {
157
182
  // /api/projects/:id/observations
158
183
  const projectObsMatch = /^\/api\/projects\/([^/]+)\/observations$/.exec(cleanPath);
159
184
  if (projectObsMatch) {
160
- const id = decodeURIComponent(projectObsMatch[1]);
185
+ const id = decodeSegment(projectObsMatch[1]);
186
+ if (id === null)
187
+ return null;
161
188
  if (method === "GET")
162
189
  return { route: "get_project_observations", id };
163
190
  return null;
@@ -165,7 +192,9 @@ export function matchRoute(method, path) {
165
192
  // /api/projects/:id and /api/projects/:id/sessions
166
193
  const projectMatch = /^\/api\/projects\/([^/]+)(\/sessions)?$/.exec(cleanPath);
167
194
  if (projectMatch) {
168
- const id = decodeURIComponent(projectMatch[1]);
195
+ const id = decodeSegment(projectMatch[1]);
196
+ if (id === null)
197
+ return null;
169
198
  const isSessions = projectMatch[2] === "/sessions";
170
199
  if (isSessions) {
171
200
  if (method === "GET")
@@ -181,7 +210,9 @@ export function matchRoute(method, path) {
181
210
  // /api/projects/:id/cleanup-sessions
182
211
  const projectCleanupMatch = /^\/api\/projects\/([^/]+)\/cleanup-sessions$/.exec(cleanPath);
183
212
  if (projectCleanupMatch) {
184
- const id = decodeURIComponent(projectCleanupMatch[1]);
213
+ const id = decodeSegment(projectCleanupMatch[1]);
214
+ if (id === null)
215
+ return null;
185
216
  if (method === "POST")
186
217
  return { route: "cleanup_project_sessions", id };
187
218
  return null;
@@ -196,14 +227,18 @@ export function matchRoute(method, path) {
196
227
  // /api/sessions/:id
197
228
  const sessionMessagesMatch = /^\/api\/sessions\/([^/]+)\/messages$/.exec(cleanPath);
198
229
  if (sessionMessagesMatch) {
199
- const id = decodeURIComponent(sessionMessagesMatch[1]);
230
+ const id = decodeSegment(sessionMessagesMatch[1]);
231
+ if (id === null)
232
+ return null;
200
233
  if (method === "GET")
201
234
  return { route: "get_session_messages", id, query };
202
235
  return null;
203
236
  }
204
237
  const sessionMatch = /^\/api\/sessions\/([^/]+)$/.exec(cleanPath);
205
238
  if (sessionMatch) {
206
- const id = decodeURIComponent(sessionMatch[1]);
239
+ const id = decodeSegment(sessionMatch[1]);
240
+ if (id === null)
241
+ return null;
207
242
  if (method === "GET")
208
243
  return { route: "get_session", id };
209
244
  if (method === "PATCH")
@@ -214,7 +249,9 @@ export function matchRoute(method, path) {
214
249
  }
215
250
  const sessionAgentMatch = /^\/api\/sessions\/([^/]+)\/active-agent$/.exec(cleanPath);
216
251
  if (sessionAgentMatch) {
217
- const id = decodeURIComponent(sessionAgentMatch[1]);
252
+ const id = decodeSegment(sessionAgentMatch[1]);
253
+ if (id === null)
254
+ return null;
218
255
  if (method === "GET")
219
256
  return { route: "get_session_active_agent", id };
220
257
  if (method === "PUT")
@@ -224,7 +261,9 @@ export function matchRoute(method, path) {
224
261
  // /api/sessions/:id/memory/details and /api/sessions/:id/memory and /api/sessions/:id/compact
225
262
  const sessionActionMatch = /^\/api\/sessions\/([^/]+)\/(memory(?:\/details)?|compact)$/.exec(cleanPath);
226
263
  if (sessionActionMatch) {
227
- const id = decodeURIComponent(sessionActionMatch[1]);
264
+ const id = decodeSegment(sessionActionMatch[1]);
265
+ if (id === null)
266
+ return null;
228
267
  const action = sessionActionMatch[2];
229
268
  if (action === "memory" && method === "GET")
230
269
  return { route: "get_session_memory", id };
@@ -237,7 +276,9 @@ export function matchRoute(method, path) {
237
276
  // /api/sessions/:id/remember-and-delete
238
277
  const rememberDeleteMatch = /^\/api\/sessions\/([^/]+)\/remember-and-delete$/.exec(cleanPath);
239
278
  if (rememberDeleteMatch) {
240
- const id = decodeURIComponent(rememberDeleteMatch[1]);
279
+ const id = decodeSegment(rememberDeleteMatch[1]);
280
+ if (id === null)
281
+ return null;
241
282
  if (method === "POST")
242
283
  return { route: "remember_and_delete_session", id };
243
284
  return null;
@@ -245,7 +286,9 @@ export function matchRoute(method, path) {
245
286
  // /api/sessions/:id/fork
246
287
  const forkMatch = /^\/api\/sessions\/([^/]+)\/fork$/.exec(cleanPath);
247
288
  if (forkMatch) {
248
- const id = decodeURIComponent(forkMatch[1]);
289
+ const id = decodeSegment(forkMatch[1]);
290
+ if (id === null)
291
+ return null;
249
292
  if (method === "POST")
250
293
  return { route: "fork_session", id };
251
294
  return null;
@@ -253,8 +296,12 @@ export function matchRoute(method, path) {
253
296
  // /api/sessions/:id/queue and /api/sessions/:id/queue/:itemId
254
297
  const queueMatch = /^\/api\/sessions\/([^/]+)\/queue(\/([^/]+))?$/.exec(cleanPath);
255
298
  if (queueMatch) {
256
- const id = decodeURIComponent(queueMatch[1]);
257
- const itemId = queueMatch[3] ? decodeURIComponent(queueMatch[3]) : undefined;
299
+ const id = decodeSegment(queueMatch[1]);
300
+ if (id === null)
301
+ return null;
302
+ const itemId = queueMatch[3] ? decodeSegment(queueMatch[3]) : undefined;
303
+ if (itemId === null)
304
+ return null;
258
305
  if (!itemId) {
259
306
  if (method === "GET")
260
307
  return { route: "get_prompt_queue", id };
@@ -279,14 +326,18 @@ export function matchRoute(method, path) {
279
326
  }
280
327
  const restartDevProcessMatch = /^\/api\/dev-processes\/([^/]+)\/restart$/.exec(cleanPath);
281
328
  if (restartDevProcessMatch) {
282
- const id = decodeURIComponent(restartDevProcessMatch[1]);
329
+ const id = decodeSegment(restartDevProcessMatch[1]);
330
+ if (id === null)
331
+ return null;
283
332
  if (method === "POST")
284
333
  return { route: "restart_dev_process", id };
285
334
  return null;
286
335
  }
287
336
  const devProcessMatch = /^\/api\/dev-processes\/([^/]+)$/.exec(cleanPath);
288
337
  if (devProcessMatch) {
289
- const id = decodeURIComponent(devProcessMatch[1]);
338
+ const id = decodeSegment(devProcessMatch[1]);
339
+ if (id === null)
340
+ return null;
290
341
  if (method === "DELETE")
291
342
  return { route: "kill_dev_process", id };
292
343
  return null;
@@ -299,7 +350,9 @@ export function matchRoute(method, path) {
299
350
  }
300
351
  const devServerMatch = /^\/api\/dev-servers\/([^/]+)$/.exec(cleanPath);
301
352
  if (devServerMatch) {
302
- const id = decodeURIComponent(devServerMatch[1]);
353
+ const id = decodeSegment(devServerMatch[1]);
354
+ if (id === null)
355
+ return null;
303
356
  if (method === "DELETE") {
304
357
  const rawPort = query?.get("port");
305
358
  const parsedPort = rawPort === null ? Number.NaN : Number(rawPort);
@@ -310,6 +363,23 @@ export function matchRoute(method, path) {
310
363
  }
311
364
  return null;
312
365
  }
366
+ // /api/secrets and /api/secrets/:name
367
+ if (cleanPath === "/api/secrets") {
368
+ if (method === "GET")
369
+ return { route: "list_secrets" };
370
+ if (method === "POST")
371
+ return { route: "create_secret" };
372
+ return null;
373
+ }
374
+ const secretMatch = /^\/api\/secrets\/([^/]+)$/.exec(cleanPath);
375
+ if (secretMatch) {
376
+ const id = decodeSegment(secretMatch[1]);
377
+ if (id === null)
378
+ return null;
379
+ if (method === "DELETE")
380
+ return { route: "delete_secret", id };
381
+ return null;
382
+ }
313
383
  return null;
314
384
  }
315
385
  /**
@@ -646,6 +716,12 @@ export async function dispatchRoute(match, body, deps) {
646
716
  return handleScanDevServers(store, deps.devProcessRegistry);
647
717
  case "kill_dev_server":
648
718
  return await handleKillDetectedDevServer(store, Number(id), match.port);
719
+ case "list_secrets":
720
+ return handleListSecrets();
721
+ case "create_secret":
722
+ return handleSetSecret(asObject(body));
723
+ case "delete_secret":
724
+ return handleDeleteSecret(id);
649
725
  }
650
726
  }
651
727
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../../../src/sdk/coding-agent/utils/shell.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;CACf;AAyCD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,WAAW,CAqDpE;AAED,wBAAgB,WAAW,IAAI,MAAM,CAAC,UAAU,CAsB/C;AAED;;;;;;;GAOG;AACH,wBAAgB,kCAAkC,IAAI,MAAM,CAU3D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BxD;AAQD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEvD;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAKlD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAyBjD"}
1
+ {"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../../../src/sdk/coding-agent/utils/shell.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;CACf;AAyCD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,WAAW,CAqDpE;AAED,wBAAgB,WAAW,IAAI,MAAM,CAAC,UAAU,CAmC/C;AAED;;;;;;;GAOG;AACH,wBAAgB,kCAAkC,IAAI,MAAM,CAU3D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA8BxD;AAQD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEvD;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED,wBAAgB,2BAA2B,IAAI,IAAI,CAKlD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAyBjD"}
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { delimiter } from "node:path";
3
3
  import { spawn, spawnSync } from "child_process";
4
4
  import { getBinDir } from "../config.js";
5
+ import { getSecretEnvVars } from "../../../secrets/index.js";
5
6
  /**
6
7
  * Find bash executable on PATH (cross-platform)
7
8
  */
@@ -100,7 +101,7 @@ export function getShellEnv() {
100
101
  const pathEntries = currentPath.split(delimiter).filter(Boolean);
101
102
  const hasBinDir = pathEntries.includes(binDir);
102
103
  const updatedPath = hasBinDir ? currentPath : [binDir, currentPath].filter(Boolean).join(delimiter);
103
- return {
104
+ const env = {
104
105
  ...process.env,
105
106
  [pathKey]: updatedPath,
106
107
  // Agent shell hygiene (mirrors mini-swe-agent / SWE-agent environments):
@@ -114,6 +115,19 @@ export function getShellEnv() {
114
115
  TQDM_DISABLE: "1",
115
116
  GIT_TERMINAL_PROMPT: "0",
116
117
  };
118
+ // Secret env-var injection (see src/secrets/store.ts). Synchronous,
119
+ // mtime-cached read of the encrypted store, so new/updated/deleted secrets
120
+ // are picked up on the next bash spawn without an agent restart. Secrets
121
+ // are applied on top of the base environment and intentionally win over any
122
+ // same-named variable from process.env — the store is the managed source.
123
+ // PATH is excluded by the store itself (a secret named PATH would override
124
+ // the rebuilt PATH above and break the bin dir injection). Failures degrade
125
+ // to the base env: spawning must never break, and no values are ever logged.
126
+ const secretEnv = getSecretEnvVars();
127
+ const secretKeys = Object.keys(secretEnv);
128
+ if (secretKeys.length === 0)
129
+ return env;
130
+ return { ...env, ...secretEnv };
117
131
  }
118
132
  /**
119
133
  * Platform-specific instructions injected into the system prompt.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Public surface of the local encrypted secret store.
3
+ *
4
+ * Note what is deliberately NOT exported here: `getSecretValue()` (internal —
5
+ * used only for env-var injection) and `resetSecretStore()` (test/reset
6
+ * helper). Consumers importing from this index can never expose secret values:
7
+ * every export either returns metadata-only views or the raw env map consumed
8
+ * by the bash tool's shell environment.
9
+ */
10
+ export { MAX_SECRET_VALUE_BYTES, SECRET_NAME_PATTERN, SecretNameError, SecretStoreCorruptError, SecretValueError, deleteSecret, getSecretDataPath, getSecretEnvVars, getSecretKeyPath, listSecrets, normalizeSecretName, setSecret, type SecretMeta, } from "./store.js";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/secrets/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACN,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,EACf,uBAAuB,EACvB,gBAAgB,EAChB,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,mBAAmB,EACnB,SAAS,EACT,KAAK,UAAU,GACf,MAAM,YAAY,CAAC"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Public surface of the local encrypted secret store.
3
+ *
4
+ * Note what is deliberately NOT exported here: `getSecretValue()` (internal —
5
+ * used only for env-var injection) and `resetSecretStore()` (test/reset
6
+ * helper). Consumers importing from this index can never expose secret values:
7
+ * every export either returns metadata-only views or the raw env map consumed
8
+ * by the bash tool's shell environment.
9
+ */
10
+ export { MAX_SECRET_VALUE_BYTES, SECRET_NAME_PATTERN, SecretNameError, SecretStoreCorruptError, SecretValueError, deleteSecret, getSecretDataPath, getSecretEnvVars, getSecretKeyPath, listSecrets, normalizeSecretName, setSecret, } from "./store.js";
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Local encrypted secret store.
3
+ *
4
+ * Storage layout (both files under `getConfigDir()`, honoring
5
+ * `SPECTRAL_CONFIG_DIR` for tests):
6
+ * - `<configDir>/secrets.key` 32 random bytes, mode 0600 — AES-256 key
7
+ * - `<configDir>/secrets.json.enc` AES-256-GCM encrypted JSON, mode 0600
8
+ *
9
+ * Encrypted file layout:
10
+ * - bytes 0-7: magic `"SPSECRT1"` (format tag)
11
+ * - bytes 8-19: 12-byte random IV
12
+ * - bytes 20-35: 16-byte GCM auth tag
13
+ * - bytes 36+: ciphertext (UTF-8 JSON)
14
+ *
15
+ * Security rules enforced here and by this module's consumers:
16
+ * - `listSecrets()` returns ONLY `{ name, createdAt, updatedAt }` — values are
17
+ * never part of any listing.
18
+ * - `getSecretValue()` is INTERNAL: it is consumed only by
19
+ * `getSecretEnvVars()` (env-var injection into the agent's bash tool). It
20
+ * is deliberately NOT re-exported from `./index.js`, so neither the relay
21
+ * handlers nor the LLM-facing tool surface can reach it.
22
+ * - No caller ever logs a value; error messages and listings carry names only.
23
+ *
24
+ * All I/O is synchronous by design: `getSecretEnvVars()` must be callable from
25
+ * the sync `getShellEnv()` in `src/sdk/coding-agent/utils/shell.ts`. The store
26
+ * is tiny (values are capped at 64 KiB each), so sync reads are negligible.
27
+ *
28
+ * Keep this module zero-side-effect on import: nothing touches the filesystem
29
+ * until one of the exported functions is called.
30
+ */
31
+ /** Secret names: start with a letter, then letters/digits/underscores, max 64 chars. */
32
+ export declare const SECRET_NAME_PATTERN: RegExp;
33
+ /** Maximum UTF-8 byte size of a secret value (64 KiB). */
34
+ export declare const MAX_SECRET_VALUE_BYTES: number;
35
+ /** Metadata-safe view of a secret: NEVER includes the value. */
36
+ export interface SecretMeta {
37
+ name: string;
38
+ createdAt: string;
39
+ updatedAt: string;
40
+ }
41
+ /** Thrown when a secret name is missing/invalid after normalization. */
42
+ export declare class SecretNameError extends Error {
43
+ constructor(message: string);
44
+ }
45
+ /** Thrown when a secret value exceeds `MAX_SECRET_VALUE_BYTES`. */
46
+ export declare class SecretValueError extends Error {
47
+ constructor(message: string);
48
+ }
49
+ /** Thrown when the encrypted store is unreadable, corrupt, or key-mismatched. */
50
+ export declare class SecretStoreCorruptError extends Error {
51
+ constructor(message: string);
52
+ }
53
+ /** Path to the AES-256 key file (32 random bytes). Lazily computed — see `getConfigDir`. */
54
+ export declare function getSecretKeyPath(): string;
55
+ /** Path to the AES-256-GCM encrypted store. Lazily computed — see `getConfigDir`. */
56
+ export declare function getSecretDataPath(): string;
57
+ /**
58
+ * Normalize a secret name per the shared contract: trim surrounding
59
+ * whitespace, uppercase, then validate against `SECRET_NAME_PATTERN`.
60
+ * Throws `SecretNameError` with a generic message — the offending raw input
61
+ * is never echoed back, so it can't leak into logs.
62
+ */
63
+ export declare function normalizeSecretName(raw: string): string;
64
+ /**
65
+ * Return the secret env vars (name → value) for injection into the agent
66
+ * shell environment. Synchronous, mtime-cached: new/updated/deleted secrets
67
+ * are picked up on the next call without a CLI restart.
68
+ *
69
+ * Failure modes degrade gracefully — a corrupt store, missing key, or unreadable
70
+ * file returns `{}` instead of throwing, because `getShellEnv()` is sync and
71
+ * must never break bash spawning. No values are ever logged.
72
+ */
73
+ export declare function getSecretEnvVars(): Record<string, string>;
74
+ /**
75
+ * Create or update a secret. Returns the metadata-only view (never the value).
76
+ * Throws `SecretNameError` for invalid names, `SecretValueError` for oversized
77
+ * values, and `SecretStoreCorruptError` when the store cannot be read/written.
78
+ *
79
+ * Fully synchronous (no awaits): the read-modify-write below is atomic per
80
+ * event-loop turn, so concurrent relay POSTs in one process can never
81
+ * interleave — no mutex is needed. Cross-process writers are isolated by the
82
+ * unique per-request tmp name + rename in `writeStoreData`; the last rename
83
+ * wins and every writer re-reads the store on its next call.
84
+ */
85
+ export declare function setSecret(rawName: string, value: string): SecretMeta;
86
+ /**
87
+ * Delete a secret. Returns true when a record was removed, false when the
88
+ * name was not stored. Throws `SecretNameError` for invalid names.
89
+ */
90
+ export declare function deleteSecret(rawName: string): boolean;
91
+ /**
92
+ * List secrets as metadata only — `{ name, createdAt, updatedAt }`. Values are
93
+ * never part of the returned shape (this is the source for the relay GET
94
+ * response and the `list_secrets` agent tool).
95
+ */
96
+ export declare function listSecrets(): SecretMeta[];
97
+ /**
98
+ * INTERNAL — read a single secret value. Used ONLY for env-var injection via
99
+ * `getSecretEnvVars()`. Deliberately not re-exported from `./index.js` so it
100
+ * cannot be reached from the relay handlers or the LLM-facing tool surface.
101
+ * Never log the return value.
102
+ */
103
+ export declare function getSecretValue(rawName: string): string | undefined;
104
+ /**
105
+ * Remove the store + key (test helper and future "reset secrets" support).
106
+ * Returns true when either file was deleted.
107
+ */
108
+ export declare function resetSecretStore(): boolean;
109
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/secrets/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAiBH,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,QAA2B,CAAC;AAE5D,0DAA0D;AAC1D,eAAO,MAAM,sBAAsB,QAAY,CAAC;AAQhD,gEAAgE;AAChE,MAAM,WAAW,UAAU;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CAClB;AAaD,wEAAwE;AACxE,qBAAa,eAAgB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI3B;AAED,mEAAmE;AACnE,qBAAa,gBAAiB,SAAQ,KAAK;gBAC9B,OAAO,EAAE,MAAM;CAI3B;AAED,iFAAiF;AACjF,qBAAa,uBAAwB,SAAQ,KAAK;gBACrC,OAAO,EAAE,MAAM;CAI3B;AAMD,4FAA4F;AAC5F,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAMD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAWvD;AA+ND;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAczD;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,CAcpE;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAUrD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,IAAI,UAAU,EAAE,CAS1C;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIlE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAY1C"}