@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
@@ -1,240 +0,0 @@
1
- import { randomBytes } from "node:crypto";
2
- import {
3
- existsSync,
4
- mkdirSync,
5
- readFileSync,
6
- rmSync,
7
- writeFileSync,
8
- } from "node:fs";
9
- import { tmpdir } from "node:os";
10
- import { join } from "node:path";
11
- import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
12
-
13
- // ---------------------------------------------------------------------------
14
- // Mocks — declared before imports
15
- // ---------------------------------------------------------------------------
16
-
17
- const TEST_DIR = join(
18
- tmpdir(),
19
- `vellum-migration-test-${randomBytes(4).toString("hex")}`,
20
- );
21
- const WORKSPACE_DIR = join(TEST_DIR, "workspace");
22
- const CONFIG_PATH = join(WORKSPACE_DIR, "config.json");
23
- const STORE_PATH = join(TEST_DIR, "keys.enc");
24
-
25
- mock.module("../util/logger.js", () => ({
26
- getLogger: () =>
27
- new Proxy({} as Record<string, unknown>, {
28
- get: () => () => {},
29
- }),
30
- }));
31
-
32
- mock.module("../util/platform.js", () => ({
33
- getRootDir: () => TEST_DIR,
34
- getWorkspaceDir: () => WORKSPACE_DIR,
35
- getWorkspaceConfigPath: () => CONFIG_PATH,
36
- getDataDir: () => join(TEST_DIR, "data"),
37
- getLogPath: () => join(TEST_DIR, "logs", "vellum.log"),
38
- ensureDataDir: () => {
39
- if (!existsSync(TEST_DIR)) mkdirSync(TEST_DIR, { recursive: true });
40
- if (!existsSync(WORKSPACE_DIR))
41
- mkdirSync(WORKSPACE_DIR, { recursive: true });
42
- const logsDir = join(TEST_DIR, "logs");
43
- if (!existsSync(logsDir)) mkdirSync(logsDir, { recursive: true });
44
- },
45
- migrateToWorkspaceLayout: () => {},
46
- migrateToDataLayout: () => {},
47
- migratePath: () => {},
48
- isMacOS: () => false,
49
- isLinux: () => false,
50
- isWindows: () => false,
51
- }));
52
-
53
- import { invalidateConfigCache, loadConfig } from "../config/loader.js";
54
- import { _setStorePath } from "../security/encrypted-store.js";
55
- import { _setBackend } from "../security/secure-keys.js";
56
- import { getSecureKey } from "../security/secure-keys.js";
57
-
58
- // ---------------------------------------------------------------------------
59
- // Tests
60
- // ---------------------------------------------------------------------------
61
-
62
- // API key env vars that loadConfig checks — must be cleared during tests
63
- // so they don't override the migrated values under test.
64
- const API_KEY_ENV_VARS = [
65
- "ANTHROPIC_API_KEY",
66
- "OPENAI_API_KEY",
67
- "GEMINI_API_KEY",
68
- "OLLAMA_API_KEY",
69
- "FIREWORKS_API_KEY",
70
- "OPENROUTER_API_KEY",
71
- "BRAVE_API_KEY",
72
- "PERPLEXITY_API_KEY",
73
- ];
74
-
75
- describe("key migration", () => {
76
- const savedEnv: Record<string, string | undefined> = {};
77
-
78
- beforeEach(() => {
79
- // Save and clear API key env vars
80
- for (const key of API_KEY_ENV_VARS) {
81
- savedEnv[key] = process.env[key];
82
- delete process.env[key];
83
- }
84
- if (existsSync(TEST_DIR)) {
85
- rmSync(TEST_DIR, { recursive: true, force: true });
86
- }
87
- mkdirSync(TEST_DIR, { recursive: true });
88
- mkdirSync(WORKSPACE_DIR, { recursive: true });
89
- mkdirSync(join(TEST_DIR, "logs"), { recursive: true });
90
- _setStorePath(STORE_PATH);
91
- _setBackend("encrypted");
92
- invalidateConfigCache();
93
- });
94
-
95
- afterEach(() => {
96
- _setStorePath(null);
97
- _setBackend(undefined);
98
- invalidateConfigCache();
99
- // Restore API key env vars
100
- for (const key of API_KEY_ENV_VARS) {
101
- if (savedEnv[key] === undefined) delete process.env[key];
102
- else process.env[key] = savedEnv[key]!;
103
- }
104
- });
105
-
106
- test("[experimental] migrates plaintext apiKeys from config.json to secure storage", () => {
107
- const configPath = CONFIG_PATH;
108
- writeFileSync(
109
- configPath,
110
- JSON.stringify({
111
- provider: "anthropic",
112
- apiKeys: {
113
- anthropic: "sk-ant-test-key-123",
114
- openai: "sk-openai-test-456",
115
- },
116
- }),
117
- );
118
-
119
- const config = loadConfig();
120
-
121
- // Keys should be in the loaded config (from secure storage)
122
- expect(config.apiKeys.anthropic).toBe("sk-ant-test-key-123");
123
- expect(config.apiKeys.openai).toBe("sk-openai-test-456");
124
-
125
- // Keys should be in secure storage
126
- expect(getSecureKey("anthropic")).toBe("sk-ant-test-key-123");
127
- expect(getSecureKey("openai")).toBe("sk-openai-test-456");
128
-
129
- // Keys should be removed from config.json
130
- const rawJson = JSON.parse(readFileSync(configPath, "utf-8"));
131
- expect(rawJson.apiKeys).toBeUndefined();
132
- // Other config should still be there
133
- expect(rawJson.provider).toBe("anthropic");
134
- });
135
-
136
- test("does not migrate when no apiKeys in config.json", () => {
137
- const configPath = CONFIG_PATH;
138
- writeFileSync(
139
- configPath,
140
- JSON.stringify({
141
- provider: "anthropic",
142
- model: "claude-sonnet-4-6",
143
- }),
144
- );
145
-
146
- const config = loadConfig();
147
- expect(config.provider).toBe("anthropic");
148
-
149
- // Config file should be unchanged
150
- const rawJson = JSON.parse(readFileSync(configPath, "utf-8"));
151
- expect(rawJson.provider).toBe("anthropic");
152
- expect(rawJson.model).toBe("claude-sonnet-4-6");
153
- });
154
-
155
- test("does not migrate empty apiKeys object", () => {
156
- const configPath = CONFIG_PATH;
157
- writeFileSync(
158
- configPath,
159
- JSON.stringify({
160
- provider: "anthropic",
161
- apiKeys: {},
162
- }),
163
- );
164
-
165
- loadConfig();
166
-
167
- // Config file should still have the empty apiKeys
168
- const rawJson = JSON.parse(readFileSync(configPath, "utf-8"));
169
- expect(rawJson.apiKeys).toEqual({});
170
- });
171
-
172
- test("preserves other config fields during migration", () => {
173
- const configPath = CONFIG_PATH;
174
- writeFileSync(
175
- configPath,
176
- JSON.stringify({
177
- provider: "openai",
178
- model: "gpt-4",
179
- maxTokens: 4096,
180
- apiKeys: { anthropic: "sk-ant-test" },
181
- timeouts: { shellDefaultTimeoutSec: 30 },
182
- }),
183
- );
184
-
185
- loadConfig();
186
-
187
- const rawJson = JSON.parse(readFileSync(configPath, "utf-8"));
188
- expect(rawJson.provider).toBe("openai");
189
- expect(rawJson.model).toBe("gpt-4");
190
- expect(rawJson.maxTokens).toBe(4096);
191
- expect(rawJson.timeouts.shellDefaultTimeoutSec).toBe(30);
192
- expect(rawJson.apiKeys).toBeUndefined();
193
- });
194
-
195
- test("[experimental] migration only happens once (idempotent)", () => {
196
- const configPath = CONFIG_PATH;
197
- writeFileSync(
198
- configPath,
199
- JSON.stringify({
200
- provider: "anthropic",
201
- apiKeys: { anthropic: "sk-ant-test-key" },
202
- }),
203
- );
204
-
205
- // First load — triggers migration
206
- loadConfig();
207
- expect(getSecureKey("anthropic")).toBe("sk-ant-test-key");
208
-
209
- // Verify config.json no longer has apiKeys
210
- const rawAfter = JSON.parse(readFileSync(configPath, "utf-8"));
211
- expect(rawAfter.apiKeys).toBeUndefined();
212
-
213
- // Second load — should not error or duplicate
214
- invalidateConfigCache();
215
- const config2 = loadConfig();
216
- expect(config2.apiKeys.anthropic).toBe("sk-ant-test-key");
217
- });
218
-
219
- test("skips non-string values in apiKeys during migration", () => {
220
- const configPath = CONFIG_PATH;
221
- writeFileSync(
222
- configPath,
223
- JSON.stringify({
224
- provider: "anthropic",
225
- apiKeys: {
226
- anthropic: "sk-ant-valid",
227
- broken: 123,
228
- empty: "",
229
- },
230
- }),
231
- );
232
-
233
- loadConfig();
234
-
235
- // Only the valid key should be migrated
236
- expect(getSecureKey("anthropic")).toBe("sk-ant-valid");
237
- expect(getSecureKey("broken")).toBeUndefined();
238
- expect(getSecureKey("empty")).toBeUndefined();
239
- });
240
- });
@@ -1,286 +0,0 @@
1
- import { afterAll, beforeEach, describe, expect, test } from "bun:test";
2
-
3
- import {
4
- _overrideDeps,
5
- _resetDeps,
6
- deleteKey,
7
- getKey,
8
- isKeychainAvailable,
9
- setKey,
10
- } from "../security/keychain.js";
11
-
12
- // ---------------------------------------------------------------------------
13
- // Test state — uses _overrideDeps instead of mock.module to avoid
14
- // process-global mock leakage between test files in Bun's shared runner.
15
- // ---------------------------------------------------------------------------
16
-
17
- let mockPlatform = "darwin";
18
- let execFileCalls: Array<{
19
- cmd: string;
20
- args: string[];
21
- opts: Record<string, unknown>;
22
- }> = [];
23
- let execFileResults: Map<string, string | Error> = new Map();
24
-
25
- // ---------------------------------------------------------------------------
26
- // Tests
27
- // ---------------------------------------------------------------------------
28
-
29
- describe("keychain", () => {
30
- beforeEach(() => {
31
- execFileCalls = [];
32
- execFileResults = new Map();
33
- mockPlatform = "darwin";
34
-
35
- _overrideDeps({
36
- isMacOS: () => mockPlatform === "darwin",
37
- isLinux: () => mockPlatform === "linux",
38
- execFileSync: ((
39
- cmd: string,
40
- args: string[],
41
- opts: Record<string, unknown>,
42
- ) => {
43
- execFileCalls.push({ cmd, args: [...args], opts: { ...opts } });
44
-
45
- const key = `${cmd} ${args.join(" ")}`;
46
- for (const [pattern, result] of execFileResults) {
47
- if (key.includes(pattern)) {
48
- if (result instanceof Error) throw result;
49
- return result;
50
- }
51
- }
52
- return "";
53
- }) as typeof import("node:child_process").execFileSync,
54
- });
55
- });
56
-
57
- afterAll(() => {
58
- _resetDeps();
59
- });
60
-
61
- // -----------------------------------------------------------------------
62
- // isKeychainAvailable
63
- // -----------------------------------------------------------------------
64
- describe("isKeychainAvailable", () => {
65
- test("returns true on macOS when security CLI works", () => {
66
- mockPlatform = "darwin";
67
- expect(isKeychainAvailable()).toBe(true);
68
- expect(execFileCalls[0].cmd).toBe("security");
69
- expect(execFileCalls[0].args).toContain("list-keychains");
70
- });
71
-
72
- test("returns true on Linux when secret-tool exists", () => {
73
- mockPlatform = "linux";
74
- expect(isKeychainAvailable()).toBe(true);
75
- expect(execFileCalls[0].cmd).toBe("which");
76
- expect(execFileCalls[0].args).toContain("secret-tool");
77
- });
78
-
79
- test("returns false on unsupported platform", () => {
80
- mockPlatform = "win32";
81
- expect(isKeychainAvailable()).toBe(false);
82
- });
83
-
84
- test("returns false when CLI command fails", () => {
85
- mockPlatform = "darwin";
86
- execFileResults.set("list-keychains", new Error("not found"));
87
- expect(isKeychainAvailable()).toBe(false);
88
- });
89
- });
90
-
91
- // -----------------------------------------------------------------------
92
- // macOS getKey / setKey / deleteKey
93
- // -----------------------------------------------------------------------
94
- describe("macOS", () => {
95
- beforeEach(() => {
96
- mockPlatform = "darwin";
97
- });
98
-
99
- test("getKey calls security find-generic-password", () => {
100
- execFileResults.set("find-generic-password", "my-secret-value\n");
101
- const result = getKey("anthropic");
102
- expect(result).toBe("my-secret-value");
103
- const call = execFileCalls.find((c) =>
104
- c.args.includes("find-generic-password"),
105
- );
106
- expect(call).toBeDefined();
107
- expect(call!.args).toContain("-s");
108
- expect(call!.args).toContain("vellum-assistant");
109
- expect(call!.args).toContain("-a");
110
- expect(call!.args).toContain("anthropic");
111
- expect(call!.args).toContain("-w");
112
- });
113
-
114
- test("getKey returns null when key not found", () => {
115
- const err = Object.assign(new Error("item not found"), { status: 44 });
116
- execFileResults.set("find-generic-password", err);
117
- expect(getKey("nonexistent")).toBeNull();
118
- });
119
-
120
- test("getKey throws on runtime errors", () => {
121
- const err = Object.assign(new Error("keychain locked"), { status: 1 });
122
- execFileResults.set("find-generic-password", err);
123
- expect(() => getKey("test")).toThrow("keychain locked");
124
- });
125
-
126
- test("setKey calls security add-generic-password with -U flag", () => {
127
- const result = setKey("anthropic", "sk-ant-key123");
128
- expect(result).toBe(true);
129
- const addCall = execFileCalls.find((c) =>
130
- c.args.includes("add-generic-password"),
131
- );
132
- expect(addCall).toBeDefined();
133
- expect(addCall!.args).toContain("-U");
134
- expect(addCall!.args).toContain("-w");
135
- });
136
-
137
- test("setKey passes secret as -w argument", () => {
138
- setKey("anthropic", "sk-ant-key123");
139
- const addCall = execFileCalls.find((c) =>
140
- c.args.includes("add-generic-password"),
141
- );
142
- expect(addCall).toBeDefined();
143
- const wIndex = addCall!.args.indexOf("-w");
144
- expect(wIndex).toBeGreaterThanOrEqual(0);
145
- expect(addCall!.args[wIndex + 1]).toBe("sk-ant-key123");
146
- });
147
-
148
- test("setKey does not delete before adding (relies on -U flag)", () => {
149
- setKey("anthropic", "new-value");
150
- const deleteCall = execFileCalls.find((c) =>
151
- c.args.includes("delete-generic-password"),
152
- );
153
- expect(deleteCall).toBeUndefined();
154
- });
155
-
156
- test("getKey preserves internal whitespace", () => {
157
- execFileResults.set("find-generic-password", " value with spaces \n");
158
- const result = getKey("test");
159
- expect(result).toBe(" value with spaces ");
160
- });
161
-
162
- test("deleteKey calls security delete-generic-password", () => {
163
- const result = deleteKey("anthropic");
164
- expect(result).toBe(true);
165
- const call = execFileCalls.find((c) =>
166
- c.args.includes("delete-generic-password"),
167
- );
168
- expect(call).toBeDefined();
169
- expect(call!.args).toContain("vellum-assistant");
170
- expect(call!.args).toContain("anthropic");
171
- });
172
-
173
- test("deleteKey returns false on error", () => {
174
- execFileResults.set(
175
- "delete-generic-password",
176
- new Error("item not found"),
177
- );
178
- expect(deleteKey("nonexistent")).toBe(false);
179
- });
180
- });
181
-
182
- // -----------------------------------------------------------------------
183
- // Linux getKey / setKey / deleteKey
184
- // -----------------------------------------------------------------------
185
- describe("Linux", () => {
186
- beforeEach(() => {
187
- mockPlatform = "linux";
188
- });
189
-
190
- test("getKey calls secret-tool lookup", () => {
191
- execFileResults.set("lookup", "linux-secret-value\n");
192
- const result = getKey("openai");
193
- expect(result).toBe("linux-secret-value");
194
- const call = execFileCalls.find(
195
- (c) => c.cmd === "secret-tool" && c.args.includes("lookup"),
196
- );
197
- expect(call).toBeDefined();
198
- expect(call!.args).toContain("service");
199
- expect(call!.args).toContain("vellum-assistant");
200
- expect(call!.args).toContain("account");
201
- expect(call!.args).toContain("openai");
202
- });
203
-
204
- test("getKey returns null when key not found", () => {
205
- const err = Object.assign(new Error("not found"), { status: 1 });
206
- execFileResults.set("lookup", err);
207
- expect(getKey("missing")).toBeNull();
208
- });
209
-
210
- test("getKey throws on runtime errors (exit code 1 with stderr)", () => {
211
- const err = Object.assign(new Error("D-Bus error"), {
212
- status: 1,
213
- stderr: "Cannot autolaunch D-Bus without X11",
214
- });
215
- execFileResults.set("lookup", err);
216
- expect(() => getKey("test")).toThrow("D-Bus error");
217
- });
218
-
219
- test("getKey preserves internal whitespace", () => {
220
- execFileResults.set("lookup", " value with spaces \n");
221
- const result = getKey("test");
222
- expect(result).toBe(" value with spaces ");
223
- });
224
-
225
- test("setKey calls secret-tool store with input", () => {
226
- const result = setKey("gemini", "gemini-key-123");
227
- expect(result).toBe(true);
228
- const call = execFileCalls.find(
229
- (c) => c.cmd === "secret-tool" && c.args.includes("store"),
230
- );
231
- expect(call).toBeDefined();
232
- expect(call!.args).toContain("--label");
233
- expect(call!.opts.input).toBe("gemini-key-123");
234
- });
235
-
236
- test("deleteKey calls secret-tool clear", () => {
237
- const result = deleteKey("gemini");
238
- expect(result).toBe(true);
239
- const call = execFileCalls.find(
240
- (c) => c.cmd === "secret-tool" && c.args.includes("clear"),
241
- );
242
- expect(call).toBeDefined();
243
- expect(call!.args).toContain("vellum-assistant");
244
- expect(call!.args).toContain("gemini");
245
- });
246
- });
247
-
248
- // -----------------------------------------------------------------------
249
- // Unsupported platform
250
- // -----------------------------------------------------------------------
251
- describe("unsupported platform", () => {
252
- beforeEach(() => {
253
- mockPlatform = "win32";
254
- });
255
-
256
- test("getKey returns null", () => {
257
- expect(getKey("any")).toBeNull();
258
- });
259
-
260
- test("setKey returns false", () => {
261
- expect(setKey("any", "value")).toBe(false);
262
- });
263
-
264
- test("deleteKey returns false", () => {
265
- expect(deleteKey("any")).toBe(false);
266
- });
267
- });
268
-
269
- // -----------------------------------------------------------------------
270
- // Error handling
271
- // -----------------------------------------------------------------------
272
- describe("error handling", () => {
273
- test("getKey throws on unexpected runtime errors", () => {
274
- mockPlatform = "darwin";
275
- const err = Object.assign(new Error("unexpected"), { status: 1 });
276
- execFileResults.set("find-generic-password", err);
277
- expect(() => getKey("key")).toThrow("unexpected");
278
- });
279
-
280
- test("setKey gracefully handles unexpected errors", () => {
281
- mockPlatform = "darwin";
282
- execFileResults.set("add-generic-password", new Error("unexpected"));
283
- expect(setKey("key", "val")).toBe(false);
284
- });
285
- });
286
- });