@vellumai/assistant 0.4.35 → 0.4.37

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 (239) 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 +5 -2
  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 +29 -0
  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 +814 -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 +494 -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} +134 -245
  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 +175 -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 +135 -34
  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 +11 -0
  173. package/src/memory/llm-usage-store.ts +186 -0
  174. package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
  175. package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
  176. package/src/memory/migrations/index.ts +2 -0
  177. package/src/memory/schema-migration.ts +1 -0
  178. package/src/memory/shared-app-links-store.ts +1 -1
  179. package/src/messaging/registry.ts +27 -0
  180. package/src/notifications/README.md +79 -70
  181. package/src/notifications/broadcaster.ts +2 -1
  182. package/src/notifications/conversation-pairing.ts +147 -13
  183. package/src/notifications/copy-composer.ts +7 -3
  184. package/src/notifications/destination-resolver.ts +14 -1
  185. package/src/notifications/emit-signal.ts +3 -2
  186. package/src/notifications/signal.ts +105 -1
  187. package/src/notifications/types.ts +16 -0
  188. package/src/permissions/checker.ts +29 -3
  189. package/src/permissions/prompter.ts +11 -3
  190. package/src/runtime/access-request-helper.ts +2 -1
  191. package/src/runtime/auth/route-policy.ts +7 -1
  192. package/src/runtime/channel-invite-transport.ts +40 -63
  193. package/src/runtime/channel-invite-transports/email.ts +13 -39
  194. package/src/runtime/channel-invite-transports/slack.ts +5 -34
  195. package/src/runtime/channel-invite-transports/sms.ts +8 -29
  196. package/src/runtime/channel-invite-transports/telegram.ts +69 -28
  197. package/src/runtime/channel-invite-transports/voice.ts +0 -7
  198. package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
  199. package/src/runtime/channel-readiness-service.ts +202 -45
  200. package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
  201. package/src/runtime/guardian-outbound-actions.ts +8 -5
  202. package/src/runtime/http-server.ts +2 -0
  203. package/src/runtime/invite-instruction-generator.ts +178 -0
  204. package/src/runtime/invite-service.ts +22 -25
  205. package/src/runtime/migrations/migration-transport.ts +13 -0
  206. package/src/runtime/routes/app-routes.ts +1 -1
  207. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
  208. package/src/runtime/routes/channel-readiness-routes.ts +30 -11
  209. package/src/runtime/routes/contact-routes.ts +54 -26
  210. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
  211. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
  212. package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
  213. package/src/runtime/routes/integration-routes.ts +1 -1
  214. package/src/runtime/routes/invite-routes.ts +1 -1
  215. package/src/runtime/routes/secret-routes.ts +31 -7
  216. package/src/runtime/routes/twilio-routes.ts +32 -1
  217. package/src/runtime/routes/usage-routes.ts +114 -0
  218. package/src/runtime/tool-grant-request-helper.ts +2 -1
  219. package/src/security/encrypted-store.ts +9 -5
  220. package/src/security/keychain-broker-client.ts +393 -0
  221. package/src/security/secure-keys.ts +106 -321
  222. package/src/tools/apps/executors.ts +73 -0
  223. package/src/tools/browser/auto-navigate.ts +15 -6
  224. package/src/tools/browser/chrome-cdp.ts +211 -0
  225. package/src/tools/browser/network-recorder.test.ts +83 -0
  226. package/src/tools/browser/network-recorder.ts +8 -7
  227. package/src/tools/browser/x-auto-navigate.ts +12 -6
  228. package/src/tools/credentials/policy-types.ts +24 -0
  229. package/src/tools/credentials/vault.ts +22 -27
  230. package/src/tools/network/script-proxy/session-manager.ts +47 -3
  231. package/src/tools/permission-checker.ts +1 -0
  232. package/src/tools/types.ts +2 -0
  233. package/src/tools/ui-surface/definitions.ts +1 -2
  234. package/src/tools/watch/watch-state.ts +2 -0
  235. package/src/__tests__/key-migration.test.ts +0 -240
  236. package/src/__tests__/keychain.test.ts +0 -286
  237. package/src/cli/core-commands.ts +0 -899
  238. package/src/security/keychain-to-encrypted-migration.ts +0 -66
  239. package/src/security/keychain.ts +0 -490
@@ -315,7 +315,7 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
315
315
  },
316
316
  open_bundle: {
317
317
  type: "open_bundle",
318
- filePath: "/tmp/My_App.vellumapp",
318
+ filePath: "/tmp/My_App.vellum",
319
319
  },
320
320
  sign_bundle_payload_response: {
321
321
  type: "sign_bundle_payload_response",
@@ -1041,6 +1041,12 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
1041
1041
  sessionId: "sess-routed-001",
1042
1042
  interactionType: "computer_use",
1043
1043
  },
1044
+ ride_shotgun_error: {
1045
+ type: "ride_shotgun_error",
1046
+ watchId: "watch-shotgun-001",
1047
+ sessionId: "sess-shotgun-001",
1048
+ message: "Failed to start browser — Chrome CDP could not be launched.",
1049
+ },
1044
1050
  ride_shotgun_progress: {
1045
1051
  type: "ride_shotgun_progress",
1046
1052
  watchId: "watch-shotgun-001",
@@ -1266,7 +1272,7 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
1266
1272
  },
1267
1273
  bundle_app_response: {
1268
1274
  type: "bundle_app_response",
1269
- bundlePath: "/tmp/My_App-abc12345.vellumapp",
1275
+ bundlePath: "/tmp/My_App-abc12345.vellum",
1270
1276
  manifest: {
1271
1277
  format_version: 1,
1272
1278
  name: "My App",
@@ -0,0 +1,543 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import type { Server } from "node:net";
4
+ import { createServer } from "node:net";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import {
8
+ afterAll,
9
+ afterEach,
10
+ beforeAll,
11
+ beforeEach,
12
+ describe,
13
+ expect,
14
+ mock,
15
+ test,
16
+ } from "bun:test";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Mock logger to silence output
20
+ // ---------------------------------------------------------------------------
21
+
22
+ mock.module("../util/logger.js", () => ({
23
+ getLogger: () =>
24
+ new Proxy({} as Record<string, unknown>, {
25
+ get: () => () => {},
26
+ }),
27
+ }));
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Test fixtures
31
+ // ---------------------------------------------------------------------------
32
+
33
+ const TEST_DIR = join(
34
+ tmpdir(),
35
+ `vellum-broker-test-${randomBytes(4).toString("hex")}`,
36
+ );
37
+ const TOKEN_DIR = join(TEST_DIR, ".vellum", "protected");
38
+ const TOKEN_PATH = join(TOKEN_DIR, "keychain-broker.token");
39
+ const SOCKET_PATH = join(TEST_DIR, "broker.sock");
40
+ const TEST_TOKEN = "test-auth-token-abc123";
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Helpers
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /**
47
+ * Create a mock UDS server that speaks the broker protocol.
48
+ * Returns the server and a handler setter for customizing responses.
49
+ */
50
+ function createMockBroker(): {
51
+ server: Server;
52
+ setHandler: (
53
+ fn: (request: Record<string, unknown>) => Record<string, unknown>,
54
+ ) => void;
55
+ start: () => Promise<void>;
56
+ stop: () => Promise<void>;
57
+ } {
58
+ let handler: (
59
+ request: Record<string, unknown>,
60
+ ) => Record<string, unknown> = () => ({ ok: true });
61
+
62
+ const connections = new Set<import("node:net").Socket>();
63
+
64
+ const server = createServer((conn) => {
65
+ connections.add(conn);
66
+ conn.on("close", () => connections.delete(conn));
67
+ let buffer = "";
68
+ conn.on("data", (chunk) => {
69
+ buffer += chunk.toString();
70
+ let idx: number;
71
+ while ((idx = buffer.indexOf("\n")) !== -1) {
72
+ const line = buffer.slice(0, idx).trim();
73
+ buffer = buffer.slice(idx + 1);
74
+ if (!line) continue;
75
+ try {
76
+ const request = JSON.parse(line);
77
+ const response = handler(request);
78
+ conn.write(JSON.stringify({ id: request.id, ...response }) + "\n");
79
+ } catch {
80
+ // Malformed request — ignore
81
+ }
82
+ }
83
+ });
84
+ });
85
+
86
+ return {
87
+ server,
88
+ setHandler: (fn) => {
89
+ handler = fn;
90
+ },
91
+ start: () =>
92
+ new Promise<void>((resolve) => {
93
+ server.listen(SOCKET_PATH, () => resolve());
94
+ }),
95
+ stop: () =>
96
+ new Promise<void>((resolve) => {
97
+ // Destroy active connections so server.close() can complete
98
+ for (const conn of connections) conn.destroy();
99
+ connections.clear();
100
+ server.close(() => resolve());
101
+ }),
102
+ };
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Setup / teardown
107
+ // ---------------------------------------------------------------------------
108
+
109
+ let originalEnv: string | undefined;
110
+
111
+ beforeAll(() => {
112
+ mkdirSync(TOKEN_DIR, { recursive: true });
113
+ });
114
+
115
+ beforeEach(() => {
116
+ originalEnv = process.env.VELLUM_KEYCHAIN_BROKER_SOCKET;
117
+ // Clean up socket file from prior test
118
+ try {
119
+ rmSync(SOCKET_PATH, { force: true });
120
+ } catch {
121
+ /* ignore */
122
+ }
123
+ });
124
+
125
+ afterEach(() => {
126
+ if (originalEnv === undefined) {
127
+ delete process.env.VELLUM_KEYCHAIN_BROKER_SOCKET;
128
+ } else {
129
+ process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = originalEnv;
130
+ }
131
+ });
132
+
133
+ afterAll(() => {
134
+ rmSync(TEST_DIR, { recursive: true, force: true });
135
+ });
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Mock platform to point getRootDir at our test directory
139
+ // ---------------------------------------------------------------------------
140
+
141
+ mock.module("../util/platform.js", () => ({
142
+ getRootDir: () => join(TEST_DIR, ".vellum"),
143
+ isMacOS: () => true,
144
+ getPlatformName: () => "darwin",
145
+ }));
146
+
147
+ // Import after mocks are set up
148
+ const { createBrokerClient } =
149
+ await import("../security/keychain-broker-client.js");
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Tests
153
+ // ---------------------------------------------------------------------------
154
+
155
+ describe("keychain-broker-client", () => {
156
+ // -----------------------------------------------------------------------
157
+ // isAvailable()
158
+ // -----------------------------------------------------------------------
159
+ describe("isAvailable", () => {
160
+ test("returns false when env var is unset", () => {
161
+ delete process.env.VELLUM_KEYCHAIN_BROKER_SOCKET;
162
+ writeFileSync(TOKEN_PATH, TEST_TOKEN);
163
+ const client = createBrokerClient();
164
+ expect(client.isAvailable()).toBe(false);
165
+ });
166
+
167
+ test("returns false when token file does not exist", () => {
168
+ process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
169
+ try {
170
+ rmSync(TOKEN_PATH, { force: true });
171
+ } catch {
172
+ /* ignore */
173
+ }
174
+ const client = createBrokerClient();
175
+ expect(client.isAvailable()).toBe(false);
176
+ });
177
+
178
+ test("returns true when both env var and token file exist", () => {
179
+ process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
180
+ writeFileSync(TOKEN_PATH, TEST_TOKEN);
181
+ const client = createBrokerClient();
182
+ expect(client.isAvailable()).toBe(true);
183
+ });
184
+ });
185
+
186
+ // -----------------------------------------------------------------------
187
+ // Request/response serialization
188
+ // -----------------------------------------------------------------------
189
+ describe("request/response", () => {
190
+ let broker: ReturnType<typeof createMockBroker>;
191
+
192
+ beforeEach(async () => {
193
+ process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
194
+ writeFileSync(TOKEN_PATH, TEST_TOKEN);
195
+ broker = createMockBroker();
196
+ });
197
+
198
+ afterEach(async () => {
199
+ await broker.stop();
200
+ });
201
+
202
+ test("ping returns pong from broker", async () => {
203
+ broker.setHandler((req) => {
204
+ expect(req.v).toBe(1);
205
+ if (req.method === "broker.ping") {
206
+ return { ok: true, result: { pong: true } };
207
+ }
208
+ return {
209
+ ok: false,
210
+ error: { code: "INVALID_REQUEST", message: "unknown method" },
211
+ };
212
+ });
213
+ await broker.start();
214
+
215
+ const client = createBrokerClient();
216
+ const result = await client.ping();
217
+ expect(result).toEqual({ pong: true });
218
+ });
219
+
220
+ test("get returns found result from broker", async () => {
221
+ broker.setHandler((req) => {
222
+ expect(req.v).toBe(1);
223
+ const params = req.params as { account?: string } | undefined;
224
+ if (req.method === "key.get" && params?.account === "my-key") {
225
+ return { ok: true, result: { found: true, value: "secret-value" } };
226
+ }
227
+ return {
228
+ ok: false,
229
+ error: { code: "INVALID_REQUEST", message: "not found" },
230
+ };
231
+ });
232
+ await broker.start();
233
+
234
+ const client = createBrokerClient();
235
+ const result = await client.get("my-key");
236
+ expect(result).toEqual({ found: true, value: "secret-value" });
237
+ });
238
+
239
+ test("get returns not-found result from broker", async () => {
240
+ broker.setHandler((req) => {
241
+ expect(req.v).toBe(1);
242
+ if (req.method === "key.get") {
243
+ return { ok: true, result: { found: false } };
244
+ }
245
+ return {
246
+ ok: false,
247
+ error: { code: "INVALID_REQUEST", message: "bad" },
248
+ };
249
+ });
250
+ await broker.start();
251
+
252
+ const client = createBrokerClient();
253
+ const result = await client.get("missing-key");
254
+ expect(result).toEqual({ found: false, value: undefined });
255
+ });
256
+
257
+ test("set returns true on success", async () => {
258
+ broker.setHandler((req) => {
259
+ expect(req.v).toBe(1);
260
+ const params = req.params as
261
+ | { account?: string; value?: string }
262
+ | undefined;
263
+ if (
264
+ req.method === "key.set" &&
265
+ params?.account === "my-key" &&
266
+ params?.value === "new-value"
267
+ ) {
268
+ return { ok: true, result: { stored: true } };
269
+ }
270
+ return {
271
+ ok: false,
272
+ error: { code: "INVALID_REQUEST", message: "failed" },
273
+ };
274
+ });
275
+ await broker.start();
276
+
277
+ const client = createBrokerClient();
278
+ const result = await client.set("my-key", "new-value");
279
+ expect(result).toBe(true);
280
+ });
281
+
282
+ test("del returns true on success", async () => {
283
+ broker.setHandler((req) => {
284
+ expect(req.v).toBe(1);
285
+ const params = req.params as { account?: string } | undefined;
286
+ if (req.method === "key.delete" && params?.account === "my-key") {
287
+ return { ok: true, result: { deleted: true } };
288
+ }
289
+ return {
290
+ ok: false,
291
+ error: { code: "INVALID_REQUEST", message: "not found" },
292
+ };
293
+ });
294
+ await broker.start();
295
+
296
+ const client = createBrokerClient();
297
+ const result = await client.del("my-key");
298
+ expect(result).toBe(true);
299
+ });
300
+
301
+ test("list returns account names", async () => {
302
+ broker.setHandler((req) => {
303
+ expect(req.v).toBe(1);
304
+ if (req.method === "key.list") {
305
+ return {
306
+ ok: true,
307
+ result: { accounts: ["key-a", "key-b", "key-c"] },
308
+ };
309
+ }
310
+ return {
311
+ ok: false,
312
+ error: { code: "INVALID_REQUEST", message: "failed" },
313
+ };
314
+ });
315
+ await broker.start();
316
+
317
+ const client = createBrokerClient();
318
+ const result = await client.list();
319
+ expect(result).toEqual(["key-a", "key-b", "key-c"]);
320
+ });
321
+
322
+ test("sends auth token and v:1 with every request", async () => {
323
+ let receivedToken: unknown;
324
+ let receivedVersion: unknown;
325
+ broker.setHandler((req) => {
326
+ receivedToken = req.token;
327
+ receivedVersion = req.v;
328
+ return { ok: true, result: { pong: true } };
329
+ });
330
+ await broker.start();
331
+
332
+ const client = createBrokerClient();
333
+ await client.ping();
334
+ expect(receivedToken).toBe(TEST_TOKEN);
335
+ expect(receivedVersion).toBe(1);
336
+ });
337
+ });
338
+
339
+ // -----------------------------------------------------------------------
340
+ // Timeout handling
341
+ // -----------------------------------------------------------------------
342
+ describe("timeout", () => {
343
+ let broker: ReturnType<typeof createMockBroker>;
344
+
345
+ beforeEach(async () => {
346
+ process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
347
+ writeFileSync(TOKEN_PATH, TEST_TOKEN);
348
+ broker = createMockBroker();
349
+ });
350
+
351
+ afterEach(async () => {
352
+ await broker.stop();
353
+ });
354
+
355
+ test("returns graceful fallback when broker does not respond within timeout", async () => {
356
+ // Handler that never responds
357
+ broker.setHandler(() => {
358
+ // Intentionally do not return a response — the broker mock won't send anything
359
+ return undefined as unknown as Record<string, unknown>;
360
+ });
361
+
362
+ // Override handler at the server level to swallow requests
363
+ broker.server.removeAllListeners("connection");
364
+ broker.server.on("connection", (_conn) => {
365
+ // Accept connection but never respond
366
+ });
367
+ await broker.start();
368
+
369
+ const client = createBrokerClient();
370
+
371
+ // get should return null on timeout (broker error)
372
+ const result = await client.get("test-key");
373
+ expect(result).toBeNull();
374
+ }, 10_000);
375
+ });
376
+
377
+ // -----------------------------------------------------------------------
378
+ // UNAUTHORIZED -> token re-read -> retry
379
+ // -----------------------------------------------------------------------
380
+ describe("UNAUTHORIZED retry", () => {
381
+ let broker: ReturnType<typeof createMockBroker>;
382
+
383
+ beforeEach(async () => {
384
+ process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
385
+ writeFileSync(TOKEN_PATH, "old-token");
386
+ broker = createMockBroker();
387
+ });
388
+
389
+ afterEach(async () => {
390
+ await broker.stop();
391
+ });
392
+
393
+ test("re-reads token and retries on UNAUTHORIZED", async () => {
394
+ let callCount = 0;
395
+ broker.setHandler((req) => {
396
+ callCount++;
397
+ if (req.token === "new-token") {
398
+ return { ok: true, result: { pong: true } };
399
+ }
400
+ return {
401
+ ok: false,
402
+ error: { code: "UNAUTHORIZED", message: "Invalid auth token" },
403
+ };
404
+ });
405
+ await broker.start();
406
+
407
+ const client = createBrokerClient();
408
+
409
+ // First call will use "old-token" and get UNAUTHORIZED.
410
+ // Simulate the token file being updated before the retry.
411
+ // We need to update it after the first request but before the retry.
412
+ // Since the handler runs synchronously, update the file in the handler.
413
+ broker.setHandler((req) => {
414
+ callCount++;
415
+ if (callCount === 1) {
416
+ // First request with old token — write new token file and return UNAUTHORIZED
417
+ writeFileSync(TOKEN_PATH, "new-token");
418
+ return {
419
+ ok: false,
420
+ error: { code: "UNAUTHORIZED", message: "Invalid auth token" },
421
+ };
422
+ }
423
+ // Retry with re-read token
424
+ if (req.token === "new-token") {
425
+ return { ok: true, result: { pong: true } };
426
+ }
427
+ return {
428
+ ok: false,
429
+ error: { code: "UNAUTHORIZED", message: "Invalid auth token" },
430
+ };
431
+ });
432
+ callCount = 0;
433
+
434
+ const result = await client.ping();
435
+ expect(result).toEqual({ pong: true });
436
+ expect(callCount).toBe(2);
437
+ });
438
+ });
439
+
440
+ // -----------------------------------------------------------------------
441
+ // Graceful degradation
442
+ // -----------------------------------------------------------------------
443
+ describe("graceful degradation", () => {
444
+ test("get returns null when broker is not running", async () => {
445
+ process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
446
+ writeFileSync(TOKEN_PATH, TEST_TOKEN);
447
+ const client = createBrokerClient();
448
+ const result = await client.get("test-key");
449
+ expect(result).toBeNull();
450
+ });
451
+
452
+ test("set returns false when broker is not running", async () => {
453
+ process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
454
+ writeFileSync(TOKEN_PATH, TEST_TOKEN);
455
+ const client = createBrokerClient();
456
+ const result = await client.set("test-key", "value");
457
+ expect(result).toBe(false);
458
+ });
459
+
460
+ test("del returns false when broker is not running", async () => {
461
+ process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
462
+ writeFileSync(TOKEN_PATH, TEST_TOKEN);
463
+ const client = createBrokerClient();
464
+ const result = await client.del("test-key");
465
+ expect(result).toBe(false);
466
+ });
467
+
468
+ test("list returns empty array when broker is not running", async () => {
469
+ process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
470
+ writeFileSync(TOKEN_PATH, TEST_TOKEN);
471
+ const client = createBrokerClient();
472
+ const result = await client.list();
473
+ expect(result).toEqual([]);
474
+ });
475
+
476
+ test("ping returns null when broker is not running", async () => {
477
+ process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
478
+ writeFileSync(TOKEN_PATH, TEST_TOKEN);
479
+ const client = createBrokerClient();
480
+ const result = await client.ping();
481
+ expect(result).toBeNull();
482
+ });
483
+
484
+ test("returns fallbacks when socket path env var is unset", async () => {
485
+ delete process.env.VELLUM_KEYCHAIN_BROKER_SOCKET;
486
+ writeFileSync(TOKEN_PATH, TEST_TOKEN);
487
+ const client = createBrokerClient();
488
+ expect(await client.get("key")).toBeNull();
489
+ expect(await client.set("key", "val")).toBe(false);
490
+ expect(await client.del("key")).toBe(false);
491
+ expect(await client.list()).toEqual([]);
492
+ expect(await client.ping()).toBeNull();
493
+ });
494
+
495
+ test("returns fallbacks when token file is missing", async () => {
496
+ process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
497
+ try {
498
+ rmSync(TOKEN_PATH, { force: true });
499
+ } catch {
500
+ /* ignore */
501
+ }
502
+ const client = createBrokerClient();
503
+ expect(await client.get("key")).toBeNull();
504
+ expect(await client.set("key", "val")).toBe(false);
505
+ expect(await client.del("key")).toBe(false);
506
+ expect(await client.list()).toEqual([]);
507
+ expect(await client.ping()).toBeNull();
508
+ });
509
+ });
510
+
511
+ // -----------------------------------------------------------------------
512
+ // Connection persistence
513
+ // -----------------------------------------------------------------------
514
+ describe("connection persistence", () => {
515
+ let broker: ReturnType<typeof createMockBroker>;
516
+
517
+ beforeEach(async () => {
518
+ process.env.VELLUM_KEYCHAIN_BROKER_SOCKET = SOCKET_PATH;
519
+ writeFileSync(TOKEN_PATH, TEST_TOKEN);
520
+ broker = createMockBroker();
521
+ });
522
+
523
+ afterEach(async () => {
524
+ await broker.stop();
525
+ });
526
+
527
+ test("reuses the same connection across multiple requests", async () => {
528
+ let connectionCount = 0;
529
+ broker.server.on("connection", () => {
530
+ connectionCount++;
531
+ });
532
+ broker.setHandler(() => ({ ok: true, result: { pong: true } }));
533
+ await broker.start();
534
+
535
+ const client = createBrokerClient();
536
+ await client.ping();
537
+ await client.ping();
538
+ await client.ping();
539
+
540
+ expect(connectionCount).toBe(1);
541
+ });
542
+ });
543
+ });