@vellumai/assistant 0.4.34 → 0.4.36

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 (251) hide show
  1. package/AGENTS.md +1 -1
  2. package/ARCHITECTURE.md +44 -49
  3. package/README.md +32 -20
  4. package/docs/architecture/keychain-broker.md +186 -0
  5. package/docs/architecture/security.md +110 -116
  6. package/docs/runbook-trusted-contacts.md +2 -2
  7. package/docs/skills.md +25 -25
  8. package/package.json +4 -1
  9. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
  10. package/src/__tests__/actor-token-service.test.ts +1 -0
  11. package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
  12. package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
  13. package/src/__tests__/assistant-id-boundary-guard.test.ts +91 -43
  14. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  15. package/src/__tests__/bundle-scanner.test.ts +1 -1
  16. package/src/__tests__/channel-guardian.test.ts +102 -102
  17. package/src/__tests__/channel-invite-transport.test.ts +155 -256
  18. package/src/__tests__/channel-readiness-routes.test.ts +336 -0
  19. package/src/__tests__/checker.test.ts +6 -6
  20. package/src/__tests__/chrome-cdp.test.ts +350 -0
  21. package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
  22. package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
  23. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
  24. package/src/__tests__/config-loader-migration.test.ts +85 -0
  25. package/src/__tests__/conversation-pairing.test.ts +370 -5
  26. package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
  27. package/src/__tests__/credential-broker-server-use.test.ts +1 -10
  28. package/src/__tests__/credential-security-e2e.test.ts +7 -1
  29. package/src/__tests__/credential-security-invariants.test.ts +14 -20
  30. package/src/__tests__/credential-vault-unit.test.ts +1 -11
  31. package/src/__tests__/credential-vault.test.ts +5 -19
  32. package/src/__tests__/credentials-cli.test.ts +806 -0
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
  34. package/src/__tests__/email-invite-adapter.test.ts +78 -0
  35. package/src/__tests__/email-service-config-fallback.test.ts +102 -0
  36. package/src/__tests__/encrypted-store.test.ts +6 -6
  37. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  38. package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
  39. package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
  40. package/src/__tests__/guardian-outbound-http.test.ts +53 -47
  41. package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
  42. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
  43. package/src/__tests__/handlers-telegram-config.test.ts +8 -2
  44. package/src/__tests__/handlers-twitter-config.test.ts +2 -2
  45. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
  46. package/src/__tests__/ingress-reconcile.test.ts +6 -0
  47. package/src/__tests__/intent-routing.test.ts +23 -4
  48. package/src/__tests__/invite-routes-http.test.ts +12 -0
  49. package/src/__tests__/ipc-snapshot.test.ts +8 -2
  50. package/src/__tests__/keychain-broker-client.test.ts +543 -0
  51. package/src/__tests__/llm-usage-store.test.ts +344 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +2 -2
  53. package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
  54. package/src/__tests__/migration-transport.test.ts +49 -0
  55. package/src/__tests__/notification-broadcaster.test.ts +205 -5
  56. package/src/__tests__/notification-deep-link.test.ts +365 -1
  57. package/src/__tests__/oauth-connect-handler.test.ts +2 -2
  58. package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
  59. package/src/__tests__/proxy-approval-callback.test.ts +1 -1
  60. package/src/__tests__/recording-handler.test.ts +1 -1
  61. package/src/__tests__/recording-intent-handler.test.ts +6 -1
  62. package/src/__tests__/recording-state-machine.test.ts +1 -1
  63. package/src/__tests__/relay-server.test.ts +9 -1
  64. package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
  65. package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
  66. package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
  67. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
  68. package/src/__tests__/secret-onetime-send.test.ts +8 -2
  69. package/src/__tests__/secure-keys.test.ts +175 -216
  70. package/src/__tests__/session-confirmation-signals.test.ts +1 -1
  71. package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
  72. package/src/__tests__/session-queue.test.ts +2 -1
  73. package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
  75. package/src/__tests__/skill-feature-flags.test.ts +12 -9
  76. package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
  77. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  78. package/src/__tests__/skills.test.ts +34 -4
  79. package/src/__tests__/slack-channel-config.test.ts +2 -2
  80. package/src/__tests__/system-prompt.test.ts +26 -4
  81. package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
  82. package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  84. package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
  85. package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
  86. package/src/__tests__/twitter-auth-handler.test.ts +2 -2
  87. package/src/__tests__/twitter-oauth-client.test.ts +1 -1
  88. package/src/__tests__/usage-routes.test.ts +339 -0
  89. package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
  90. package/src/agent/loop.ts +3 -0
  91. package/src/amazon/checkout.ts +0 -1
  92. package/src/approvals/guardian-request-resolvers.ts +9 -1
  93. package/src/bundler/app-bundler.ts +28 -12
  94. package/src/bundler/bundle-scanner.ts +1 -1
  95. package/src/bundler/bundle-signer.ts +3 -3
  96. package/src/bundler/manifest.ts +1 -1
  97. package/src/bundler/signature-verifier.ts +3 -3
  98. package/src/channels/config.ts +1 -1
  99. package/src/cli/AGENTS.md +63 -0
  100. package/src/cli/__tests__/notifications.test.ts +470 -0
  101. package/src/cli/amazon.ts +344 -167
  102. package/src/cli/audit.ts +85 -0
  103. package/src/cli/autonomy.ts +369 -0
  104. package/src/cli/channels.ts +51 -0
  105. package/src/cli/completions.ts +208 -0
  106. package/src/cli/config.ts +220 -0
  107. package/src/cli/contacts.ts +471 -0
  108. package/src/cli/credentials.ts +564 -0
  109. package/src/cli/default-action.ts +14 -0
  110. package/src/cli/dev.ts +131 -0
  111. package/src/cli/doctor.ts +398 -0
  112. package/src/cli/email.ts +491 -0
  113. package/src/cli/influencer.ts +72 -0
  114. package/src/cli/integrations.ts +248 -57
  115. package/src/cli/keys.ts +114 -0
  116. package/src/cli/map.ts +46 -54
  117. package/src/cli/mcp.ts +111 -3
  118. package/src/cli/{config-commands.ts → memory.ts} +133 -242
  119. package/src/cli/notifications.ts +407 -0
  120. package/src/cli/program.ts +65 -0
  121. package/src/cli/reference.ts +48 -0
  122. package/src/cli/sequence.ts +154 -0
  123. package/src/cli/sessions.ts +262 -0
  124. package/src/cli/trust.ts +177 -0
  125. package/src/cli/twitter.ts +323 -106
  126. package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
  127. package/src/config/bundled-skills/amazon/SKILL.md +2 -2
  128. package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
  129. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
  130. package/src/config/bundled-skills/contacts/SKILL.md +178 -10
  131. package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
  132. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +175 -145
  133. package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
  134. package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
  135. package/src/config/bundled-tool-registry.ts +2 -0
  136. package/src/config/core-schema.ts +7 -0
  137. package/src/config/feature-flag-registry.json +16 -0
  138. package/src/config/loader.ts +26 -0
  139. package/src/config/schema.ts +4 -0
  140. package/src/config/skill-state.ts +0 -13
  141. package/src/config/system-prompt.ts +27 -0
  142. package/src/contacts/contact-store.ts +25 -0
  143. package/src/daemon/computer-use-session.ts +1 -1
  144. package/src/daemon/handlers/apps.ts +1 -0
  145. package/src/daemon/handlers/config-channels.ts +3 -3
  146. package/src/daemon/handlers/config-dispatch.ts +29 -0
  147. package/src/daemon/handlers/config-inbox.ts +4 -3
  148. package/src/daemon/handlers/config.ts +3 -43
  149. package/src/daemon/handlers/contacts.ts +34 -0
  150. package/src/daemon/handlers/index.ts +17 -3
  151. package/src/daemon/handlers/session-user-message.ts +7 -0
  152. package/src/daemon/handlers/sessions.ts +21 -2
  153. package/src/daemon/handlers/shared.ts +17 -0
  154. package/src/daemon/ipc-contract/apps.ts +2 -0
  155. package/src/daemon/ipc-contract/computer-use.ts +9 -0
  156. package/src/daemon/ipc-contract/contacts.ts +3 -3
  157. package/src/daemon/ipc-contract/inbox.ts +2 -0
  158. package/src/daemon/ipc-contract/messages.ts +4 -0
  159. package/src/daemon/ipc-contract/sessions.ts +8 -0
  160. package/src/daemon/ipc-contract-inventory.json +1 -0
  161. package/src/daemon/lifecycle.ts +0 -5
  162. package/src/daemon/ride-shotgun-handler.ts +139 -25
  163. package/src/daemon/session-agent-loop-handlers.ts +100 -0
  164. package/src/daemon/session-agent-loop.ts +72 -0
  165. package/src/daemon/session-tool-setup.ts +7 -0
  166. package/src/daemon/session.ts +23 -1
  167. package/src/daemon/tool-side-effects.ts +39 -1
  168. package/src/email/service.ts +59 -2
  169. package/src/index.ts +2 -60
  170. package/src/mcp/mcp-oauth-provider.ts +90 -8
  171. package/src/media/app-icon-generator.ts +86 -0
  172. package/src/memory/db-init.ts +12 -1
  173. package/src/memory/llm-usage-store.ts +186 -0
  174. package/src/memory/migrations/026-guardian-verification-sessions.ts +28 -9
  175. package/src/memory/migrations/027a-guardian-bootstrap-token.ts +16 -3
  176. package/src/memory/migrations/038-actor-token-records.ts +8 -1
  177. package/src/memory/migrations/039-actor-refresh-token-records.ts +11 -2
  178. package/src/memory/migrations/110-channel-guardian.ts +27 -6
  179. package/src/memory/migrations/112-assistant-inbox.ts +39 -15
  180. package/src/memory/migrations/114-notifications.ts +37 -15
  181. package/src/memory/migrations/117-conversation-attention.ts +33 -9
  182. package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
  183. package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
  184. package/src/memory/migrations/index.ts +2 -0
  185. package/src/memory/migrations/schema-introspection.ts +18 -0
  186. package/src/memory/schema-migration.ts +1 -0
  187. package/src/memory/shared-app-links-store.ts +1 -1
  188. package/src/messaging/registry.ts +27 -0
  189. package/src/notifications/README.md +79 -70
  190. package/src/notifications/broadcaster.ts +2 -1
  191. package/src/notifications/conversation-pairing.ts +147 -13
  192. package/src/notifications/copy-composer.ts +7 -3
  193. package/src/notifications/destination-resolver.ts +14 -1
  194. package/src/notifications/emit-signal.ts +3 -2
  195. package/src/notifications/signal.ts +105 -1
  196. package/src/notifications/types.ts +16 -0
  197. package/src/permissions/checker.ts +29 -3
  198. package/src/permissions/prompter.ts +11 -3
  199. package/src/runtime/access-request-helper.ts +2 -1
  200. package/src/runtime/auth/route-policy.ts +7 -1
  201. package/src/runtime/channel-invite-transport.ts +40 -63
  202. package/src/runtime/channel-invite-transports/email.ts +13 -39
  203. package/src/runtime/channel-invite-transports/slack.ts +5 -34
  204. package/src/runtime/channel-invite-transports/sms.ts +8 -29
  205. package/src/runtime/channel-invite-transports/telegram.ts +69 -28
  206. package/src/runtime/channel-invite-transports/voice.ts +0 -7
  207. package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
  208. package/src/runtime/channel-readiness-service.ts +202 -45
  209. package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
  210. package/src/runtime/guardian-outbound-actions.ts +8 -5
  211. package/src/runtime/http-server.ts +5 -9
  212. package/src/runtime/http-types.ts +13 -1
  213. package/src/runtime/invite-instruction-generator.ts +178 -0
  214. package/src/runtime/invite-service.ts +22 -25
  215. package/src/runtime/migrations/migration-transport.ts +13 -0
  216. package/src/runtime/routes/app-routes.ts +1 -1
  217. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
  218. package/src/runtime/routes/channel-readiness-routes.ts +30 -11
  219. package/src/runtime/routes/contact-routes.ts +54 -26
  220. package/src/runtime/routes/guardian-bootstrap-routes.ts +1 -1
  221. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
  222. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
  223. package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
  224. package/src/runtime/routes/integration-routes.ts +1 -1
  225. package/src/runtime/routes/invite-routes.ts +1 -1
  226. package/src/runtime/routes/secret-routes.ts +31 -7
  227. package/src/runtime/routes/surface-content-routes.ts +104 -0
  228. package/src/runtime/routes/twilio-routes.ts +32 -1
  229. package/src/runtime/routes/usage-routes.ts +114 -0
  230. package/src/runtime/tool-grant-request-helper.ts +2 -1
  231. package/src/security/encrypted-store.ts +9 -5
  232. package/src/security/keychain-broker-client.ts +393 -0
  233. package/src/security/secure-keys.ts +106 -321
  234. package/src/tools/apps/executors.ts +73 -0
  235. package/src/tools/browser/auto-navigate.ts +15 -6
  236. package/src/tools/browser/chrome-cdp.ts +211 -0
  237. package/src/tools/browser/network-recorder.test.ts +83 -0
  238. package/src/tools/browser/network-recorder.ts +8 -7
  239. package/src/tools/browser/x-auto-navigate.ts +12 -6
  240. package/src/tools/credentials/policy-types.ts +24 -0
  241. package/src/tools/credentials/vault.ts +22 -27
  242. package/src/tools/network/script-proxy/session-manager.ts +47 -3
  243. package/src/tools/permission-checker.ts +1 -0
  244. package/src/tools/types.ts +2 -0
  245. package/src/tools/ui-surface/definitions.ts +1 -2
  246. package/src/tools/watch/watch-state.ts +2 -0
  247. package/src/__tests__/key-migration.test.ts +0 -240
  248. package/src/__tests__/keychain.test.ts +0 -286
  249. package/src/cli/core-commands.ts +0 -899
  250. package/src/security/keychain-to-encrypted-migration.ts +0 -66
  251. package/src/security/keychain.ts +0 -490
@@ -9,11 +9,16 @@ import type { ResolvedCredential } from "../tools/credentials/resolve.js";
9
9
 
10
10
  // Track resolveById return values per credential ID
11
11
  let resolveByIdResults = new Map<string, ResolvedCredential | undefined>();
12
+ let resolveByServiceFieldResults = new Map<
13
+ string,
14
+ ResolvedCredential | undefined
15
+ >();
12
16
  let credentialMetadataList: CredentialMetadata[] = [];
13
17
 
14
18
  mock.module("../tools/credentials/resolve.js", () => ({
15
19
  resolveById: (credentialId: string) => resolveByIdResults.get(credentialId),
16
- resolveByServiceField: () => undefined,
20
+ resolveByServiceField: (service: string, field: string) =>
21
+ resolveByServiceFieldResults.get(`${service}:${field}`),
17
22
  resolveForDomain: () => [],
18
23
  }));
19
24
 
@@ -27,7 +32,7 @@ let secureKeyValues = new Map<string, string | undefined>();
27
32
  mock.module("../security/secure-keys.js", () => ({
28
33
  getSecureKey: (account: string) => secureKeyValues.get(account),
29
34
  setSecureKey: () => true,
30
- deleteSecureKey: () => true,
35
+ deleteSecureKey: () => "deleted",
31
36
  listSecureKeys: () => [],
32
37
  getBackendType: () => "encrypted",
33
38
  _resetBackend: () => {},
@@ -54,6 +59,7 @@ import {
54
59
  afterEach(async () => {
55
60
  await stopAllSessions();
56
61
  resolveByIdResults = new Map();
62
+ resolveByServiceFieldResults = new Map();
57
63
  secureKeyValues = new Map();
58
64
  credentialMetadataList = [];
59
65
  });
@@ -427,3 +433,294 @@ describe("injected header values never appear in sanitized log entries", () => {
427
433
  expect(sanitized["x-custom-key"]).toBe("[REDACTED]");
428
434
  });
429
435
  });
436
+
437
+ // ---------------------------------------------------------------------------
438
+ // composeWith injection — multi-value credential composition and transforms
439
+ // ---------------------------------------------------------------------------
440
+
441
+ describe("composeWith injection", () => {
442
+ const CONV_ID = "conv-compose-test";
443
+ const DATA_DIR = "/tmp/vellum-compose-test";
444
+
445
+ test("composes two credential values with separator via policyCallback", async () => {
446
+ let receivedHeaders: http.IncomingHttpHeaders = {};
447
+ const echo = http.createServer((req, res) => {
448
+ receivedHeaders = req.headers;
449
+ res.writeHead(200);
450
+ res.end("ok");
451
+ });
452
+ await new Promise<void>((resolve) => echo.listen(0, "127.0.0.1", resolve));
453
+ const echoPort = (echo.address() as { port: number }).port;
454
+
455
+ try {
456
+ const tpl: CredentialInjectionTemplate = {
457
+ hostPattern: "127.0.0.1",
458
+ injectionType: "header",
459
+ headerName: "Authorization",
460
+ valuePrefix: "Basic ",
461
+ valueTransform: "base64",
462
+ composeWith: { service: "twilio", field: "auth_token", separator: ":" },
463
+ };
464
+
465
+ const primaryResolved = makeResolved(
466
+ "cred-primary",
467
+ [tpl],
468
+ "twilio",
469
+ "account_sid",
470
+ );
471
+ resolveByIdResults.set("cred-primary", primaryResolved);
472
+ credentialMetadataList.push(primaryResolved.metadata);
473
+
474
+ const composedResolved = makeResolved(
475
+ "cred-composed",
476
+ [],
477
+ "twilio",
478
+ "auth_token",
479
+ );
480
+ resolveByServiceFieldResults.set("twilio:auth_token", composedResolved);
481
+
482
+ secureKeyValues.set("credential:twilio:account_sid", "ACtest123");
483
+ secureKeyValues.set("credential:twilio:auth_token", "secret456");
484
+
485
+ const session = createSession(
486
+ CONV_ID,
487
+ ["cred-primary"],
488
+ undefined,
489
+ DATA_DIR,
490
+ );
491
+ const started = await startSession(session.id);
492
+ expect(started.status).toBe("active");
493
+
494
+ const status = await proxyRequest(
495
+ started.port!,
496
+ `http://127.0.0.1:${echoPort}/test`,
497
+ );
498
+
499
+ expect(status).toBe(200);
500
+ const expectedValue =
501
+ "Basic " + Buffer.from("ACtest123:secret456").toString("base64");
502
+ expect(receivedHeaders["authorization"]).toBe(expectedValue);
503
+ } finally {
504
+ echo.close();
505
+ }
506
+ });
507
+
508
+ test("composeWith with missing composed credential blocks request", async () => {
509
+ const echo = http.createServer((_req, res) => {
510
+ res.writeHead(200);
511
+ res.end("ok");
512
+ });
513
+ await new Promise<void>((resolve) => echo.listen(0, "127.0.0.1", resolve));
514
+ const echoPort = (echo.address() as { port: number }).port;
515
+
516
+ try {
517
+ const tpl: CredentialInjectionTemplate = {
518
+ hostPattern: "127.0.0.1",
519
+ injectionType: "header",
520
+ headerName: "Authorization",
521
+ valuePrefix: "Basic ",
522
+ valueTransform: "base64",
523
+ composeWith: { service: "twilio", field: "auth_token", separator: ":" },
524
+ };
525
+
526
+ const primaryResolved = makeResolved(
527
+ "cred-primary",
528
+ [tpl],
529
+ "twilio",
530
+ "account_sid",
531
+ );
532
+ resolveByIdResults.set("cred-primary", primaryResolved);
533
+ credentialMetadataList.push(primaryResolved.metadata);
534
+ secureKeyValues.set("credential:twilio:account_sid", "ACtest123");
535
+
536
+ // Do NOT register the composed credential in resolveByServiceFieldResults
537
+
538
+ const session = createSession(
539
+ CONV_ID,
540
+ ["cred-primary"],
541
+ undefined,
542
+ DATA_DIR,
543
+ );
544
+ const started = await startSession(session.id);
545
+ expect(started.status).toBe("active");
546
+
547
+ const status = await proxyRequest(
548
+ started.port!,
549
+ `http://127.0.0.1:${echoPort}/test`,
550
+ );
551
+
552
+ // Missing composeWith credential blocks the request (fail-closed)
553
+ expect(status).toBe(403);
554
+ } finally {
555
+ echo.close();
556
+ }
557
+ });
558
+
559
+ test("valueTransform base64 without composeWith", async () => {
560
+ let receivedHeaders: http.IncomingHttpHeaders = {};
561
+ const echo = http.createServer((req, res) => {
562
+ receivedHeaders = req.headers;
563
+ res.writeHead(200);
564
+ res.end("ok");
565
+ });
566
+ await new Promise<void>((resolve) => echo.listen(0, "127.0.0.1", resolve));
567
+ const echoPort = (echo.address() as { port: number }).port;
568
+
569
+ try {
570
+ const tpl: CredentialInjectionTemplate = {
571
+ hostPattern: "127.0.0.1",
572
+ injectionType: "header",
573
+ headerName: "Authorization",
574
+ valuePrefix: "Token ",
575
+ valueTransform: "base64",
576
+ };
577
+
578
+ const resolved = makeResolved("cred-b64", [tpl]);
579
+ resolveByIdResults.set("cred-b64", resolved);
580
+ credentialMetadataList.push(resolved.metadata);
581
+ secureKeyValues.set("credential:test-service:api-key", "plaintext");
582
+
583
+ const session = createSession(CONV_ID, ["cred-b64"], undefined, DATA_DIR);
584
+ const started = await startSession(session.id);
585
+ expect(started.status).toBe("active");
586
+
587
+ const status = await proxyRequest(
588
+ started.port!,
589
+ `http://127.0.0.1:${echoPort}/test`,
590
+ );
591
+
592
+ expect(status).toBe(200);
593
+ const expectedValue =
594
+ "Token " + Buffer.from("plaintext").toString("base64");
595
+ expect(receivedHeaders["authorization"]).toBe(expectedValue);
596
+ } finally {
597
+ echo.close();
598
+ }
599
+ });
600
+
601
+ test("composeWith blocks when composed credential resolves but secret value is missing", async () => {
602
+ const echo = http.createServer((_req, res) => {
603
+ res.writeHead(200);
604
+ res.end("ok");
605
+ });
606
+ await new Promise<void>((resolve) => echo.listen(0, "127.0.0.1", resolve));
607
+ const echoPort = (echo.address() as { port: number }).port;
608
+
609
+ try {
610
+ const tpl: CredentialInjectionTemplate = {
611
+ hostPattern: "127.0.0.1",
612
+ injectionType: "header",
613
+ headerName: "Authorization",
614
+ valuePrefix: "Basic ",
615
+ valueTransform: "base64",
616
+ composeWith: { service: "twilio", field: "auth_token", separator: ":" },
617
+ };
618
+
619
+ const primaryResolved = makeResolved(
620
+ "cred-primary",
621
+ [tpl],
622
+ "twilio",
623
+ "account_sid",
624
+ );
625
+ resolveByIdResults.set("cred-primary", primaryResolved);
626
+ credentialMetadataList.push(primaryResolved.metadata);
627
+ secureKeyValues.set("credential:twilio:account_sid", "ACtest123");
628
+
629
+ // Composed credential metadata resolves, but no secret value stored
630
+ const composedResolved = makeResolved(
631
+ "cred-composed",
632
+ [],
633
+ "twilio",
634
+ "auth_token",
635
+ );
636
+ resolveByServiceFieldResults.set("twilio:auth_token", composedResolved);
637
+ // Do NOT set secureKeyValues for "credential:twilio:auth_token"
638
+
639
+ const session = createSession(
640
+ CONV_ID,
641
+ ["cred-primary"],
642
+ undefined,
643
+ DATA_DIR,
644
+ );
645
+ const started = await startSession(session.id);
646
+ expect(started.status).toBe("active");
647
+
648
+ const status = await proxyRequest(
649
+ started.port!,
650
+ `http://127.0.0.1:${echoPort}/test`,
651
+ );
652
+
653
+ // Missing secret value for composed credential blocks the request
654
+ expect(status).toBe(403);
655
+ } finally {
656
+ echo.close();
657
+ }
658
+ });
659
+
660
+ test("composeWith without valueTransform concatenates raw", async () => {
661
+ let receivedHeaders: http.IncomingHttpHeaders = {};
662
+ const echo = http.createServer((req, res) => {
663
+ receivedHeaders = req.headers;
664
+ res.writeHead(200);
665
+ res.end("ok");
666
+ });
667
+ await new Promise<void>((resolve) => echo.listen(0, "127.0.0.1", resolve));
668
+ const echoPort = (echo.address() as { port: number }).port;
669
+
670
+ try {
671
+ const tpl: CredentialInjectionTemplate = {
672
+ hostPattern: "127.0.0.1",
673
+ injectionType: "header",
674
+ headerName: "X-Composed",
675
+ valuePrefix: "Raw ",
676
+ composeWith: {
677
+ service: "my-service",
678
+ field: "secondary-key",
679
+ separator: ":",
680
+ },
681
+ };
682
+
683
+ const primaryResolved = makeResolved(
684
+ "cred-raw-primary",
685
+ [tpl],
686
+ "my-service",
687
+ "primary-key",
688
+ );
689
+ resolveByIdResults.set("cred-raw-primary", primaryResolved);
690
+ credentialMetadataList.push(primaryResolved.metadata);
691
+
692
+ const composedResolved = makeResolved(
693
+ "cred-raw-composed",
694
+ [],
695
+ "my-service",
696
+ "secondary-key",
697
+ );
698
+ resolveByServiceFieldResults.set(
699
+ "my-service:secondary-key",
700
+ composedResolved,
701
+ );
702
+
703
+ secureKeyValues.set("credential:my-service:primary-key", "value1");
704
+ secureKeyValues.set("credential:my-service:secondary-key", "value2");
705
+
706
+ const session = createSession(
707
+ CONV_ID,
708
+ ["cred-raw-primary"],
709
+ undefined,
710
+ DATA_DIR,
711
+ );
712
+ const started = await startSession(session.id);
713
+ expect(started.status).toBe("active");
714
+
715
+ const status = await proxyRequest(
716
+ started.port!,
717
+ `http://127.0.0.1:${echoPort}/test`,
718
+ );
719
+
720
+ expect(status).toBe(200);
721
+ expect(receivedHeaders["x-composed"]).toBe("Raw value1:value2");
722
+ } finally {
723
+ echo.close();
724
+ }
725
+ });
726
+ });
@@ -23,7 +23,7 @@ mock.module("../tools/credentials/metadata-store.js", () => ({
23
23
  mock.module("../security/secure-keys.js", () => ({
24
24
  getSecureKey: (account: string) => secureKeyValues.get(account),
25
25
  setSecureKey: () => true,
26
- deleteSecureKey: () => true,
26
+ deleteSecureKey: () => "deleted",
27
27
  listSecureKeys: () => [],
28
28
  getBackendType: () => "encrypted",
29
29
  _resetBackend: () => {},
@@ -35,9 +35,15 @@ mock.module("../security/secure-keys.js", () => ({
35
35
  storedKeys.set(key, value);
36
36
  return true;
37
37
  },
38
- deleteSecureKey: (key: string) => storedKeys.delete(key),
38
+ deleteSecureKey: (key: string) => {
39
+ if (storedKeys.has(key)) {
40
+ storedKeys.delete(key);
41
+ return "deleted";
42
+ }
43
+ return "not-found";
44
+ },
39
45
  listSecureKeys: () => [],
40
- getBackendType: () => "keychain",
46
+ getBackendType: () => "encrypted",
41
47
  isDowngradedFromKeychain: () => false,
42
48
  }));
43
49