@vellumai/assistant 0.4.37 → 0.4.41

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 (169) hide show
  1. package/ARCHITECTURE.md +3 -3
  2. package/README.md +13 -13
  3. package/bun.lock +80 -24
  4. package/docs/architecture/integrations.md +126 -128
  5. package/docs/runbook-trusted-contacts.md +1 -1
  6. package/docs/trusted-contact-access.md +12 -12
  7. package/package.json +3 -1
  8. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -14
  9. package/src/__tests__/app-bundler.test.ts +209 -0
  10. package/src/__tests__/app-compiler.test.ts +279 -0
  11. package/src/__tests__/app-executors.test.ts +293 -483
  12. package/src/__tests__/app-migration.test.ts +148 -0
  13. package/src/__tests__/app-routes-csp.test.ts +202 -0
  14. package/src/__tests__/avatar-e2e.test.ts +452 -0
  15. package/src/__tests__/avatar-generator.test.ts +193 -0
  16. package/src/__tests__/avatar-router.test.ts +186 -0
  17. package/src/__tests__/browser-download-timeout.test.ts +28 -0
  18. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +9 -9
  19. package/src/__tests__/call-domain.test.ts +3 -7
  20. package/src/__tests__/credential-security-e2e.test.ts +19 -12
  21. package/src/__tests__/credentials-cli.test.ts +30 -4
  22. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +1 -1
  23. package/src/__tests__/handlers-slack-config.test.ts +0 -72
  24. package/src/__tests__/handlers-telegram-config.test.ts +19 -12
  25. package/src/__tests__/handlers-twitter-config.test.ts +105 -48
  26. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  27. package/src/__tests__/integration-status.test.ts +15 -5
  28. package/src/__tests__/integrations-cli.test.ts +1 -1
  29. package/src/__tests__/invite-redemption-service.test.ts +62 -7
  30. package/src/__tests__/ipc-snapshot.test.ts +0 -8
  31. package/src/__tests__/managed-avatar-client.test.ts +280 -0
  32. package/src/__tests__/mcp-cli.test.ts +3 -3
  33. package/src/__tests__/oauth-cli.test.ts +203 -0
  34. package/src/__tests__/relay-server.test.ts +3 -3
  35. package/src/__tests__/secret-onetime-send.test.ts +19 -12
  36. package/src/__tests__/secure-keys.test.ts +78 -0
  37. package/src/__tests__/session-messaging-secret-redirect.test.ts +3 -0
  38. package/src/__tests__/slack-channel-config.test.ts +23 -16
  39. package/src/__tests__/slack-share-routes.test.ts +263 -0
  40. package/src/__tests__/sms-messaging-provider.test.ts +3 -1
  41. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +7 -7
  42. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  43. package/src/__tests__/trusted-contact-verification.test.ts +10 -10
  44. package/src/__tests__/twilio-config.test.ts +15 -36
  45. package/src/__tests__/twilio-provider.test.ts +4 -0
  46. package/src/__tests__/twitter-auth-handler.test.ts +27 -14
  47. package/src/__tests__/twitter-cli-error-shaping.test.ts +1 -1
  48. package/src/__tests__/twitter-cli-routing.test.ts +38 -53
  49. package/src/__tests__/twitter-oauth-client.test.ts +18 -47
  50. package/src/__tests__/voice-invite-redemption.test.ts +27 -3
  51. package/src/amazon/cart.ts +1 -1
  52. package/src/amazon/client.ts +89 -7
  53. package/src/approvals/guardian-request-resolvers.ts +2 -2
  54. package/src/bundler/app-bundler.ts +77 -32
  55. package/src/bundler/app-compiler.ts +195 -0
  56. package/src/bundler/manifest.ts +1 -1
  57. package/src/bundler/package-resolver.ts +185 -0
  58. package/src/calls/call-domain.ts +4 -14
  59. package/src/calls/relay-server.ts +2 -2
  60. package/src/calls/twilio-config.ts +5 -24
  61. package/src/calls/twilio-rest.ts +19 -5
  62. package/src/cli/amazon.ts +74 -249
  63. package/src/cli/audit.ts +2 -2
  64. package/src/cli/autonomy.ts +9 -9
  65. package/src/cli/channels.ts +5 -5
  66. package/src/cli/completions.ts +27 -27
  67. package/src/cli/config.ts +14 -14
  68. package/src/cli/contacts.ts +27 -27
  69. package/src/cli/credentials.ts +28 -28
  70. package/src/cli/dev.ts +2 -2
  71. package/src/cli/doctor.ts +2 -2
  72. package/src/cli/email.ts +82 -82
  73. package/src/cli/influencer.ts +13 -13
  74. package/src/cli/integrations.ts +19 -144
  75. package/src/cli/keys.ts +10 -10
  76. package/src/cli/map.ts +4 -4
  77. package/src/cli/mcp.ts +17 -17
  78. package/src/cli/memory.ts +18 -18
  79. package/src/cli/notifications.ts +13 -13
  80. package/src/cli/oauth.ts +77 -0
  81. package/src/cli/program.ts +2 -0
  82. package/src/cli/sequence.ts +27 -27
  83. package/src/cli/sessions.ts +12 -12
  84. package/src/cli/trust.ts +8 -8
  85. package/src/cli/twitter.ts +124 -70
  86. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  87. package/src/config/bundled-skills/agentmail/SKILL.md +34 -34
  88. package/src/config/bundled-skills/amazon/SKILL.md +54 -54
  89. package/src/config/bundled-skills/app-builder/SKILL.md +137 -3
  90. package/src/config/bundled-skills/app-builder/tools/app-create.ts +10 -4
  91. package/src/config/bundled-skills/configure-settings/SKILL.md +18 -18
  92. package/src/config/bundled-skills/contacts/SKILL.md +12 -12
  93. package/src/config/bundled-skills/doordash/lib/client.ts +7 -9
  94. package/src/config/bundled-skills/email-setup/SKILL.md +4 -4
  95. package/src/config/bundled-skills/frontend-design/icon.svg +16 -0
  96. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +143 -162
  97. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +4 -4
  98. package/src/config/bundled-skills/influencer/SKILL.md +13 -13
  99. package/src/config/bundled-skills/mcp-setup/SKILL.md +11 -11
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +48 -54
  101. package/src/config/bundled-skills/public-ingress/SKILL.md +6 -6
  102. package/src/config/bundled-skills/slack-app-setup/SKILL.md +1 -1
  103. package/src/config/bundled-skills/sms-setup/SKILL.md +3 -3
  104. package/src/config/bundled-skills/telegram-setup/SKILL.md +2 -2
  105. package/src/config/bundled-skills/twilio-setup/SKILL.md +136 -225
  106. package/src/config/bundled-skills/twitter/SKILL.md +68 -44
  107. package/src/config/bundled-skills/voice-setup/SKILL.md +2 -2
  108. package/src/config/core-schema.ts +26 -0
  109. package/src/config/env.ts +4 -0
  110. package/src/config/feature-flag-registry.json +9 -1
  111. package/src/config/schema.ts +8 -0
  112. package/src/config/system-prompt.ts +6 -3
  113. package/src/config/templates/BOOTSTRAP.md +7 -5
  114. package/src/contacts/contacts-write.ts +5 -1
  115. package/src/daemon/handlers/apps.ts +31 -4
  116. package/src/daemon/handlers/config-ingress.ts +3 -3
  117. package/src/daemon/handlers/config-integrations.ts +120 -49
  118. package/src/daemon/handlers/config-slack-channel.ts +26 -7
  119. package/src/daemon/handlers/config-slack.ts +1 -54
  120. package/src/daemon/handlers/config-telegram.ts +28 -10
  121. package/src/daemon/handlers/config.ts +1 -4
  122. package/src/daemon/handlers/twitter-auth.ts +11 -4
  123. package/src/daemon/ipc-contract/apps.ts +0 -13
  124. package/src/daemon/ipc-contract-inventory.json +0 -2
  125. package/src/daemon/lifecycle.ts +8 -1
  126. package/src/daemon/session-messaging.ts +2 -2
  127. package/src/daemon/tool-side-effects.ts +30 -0
  128. package/src/email/providers/agentmail.ts +1 -1
  129. package/src/email/providers/index.ts +1 -1
  130. package/src/email/service.ts +1 -1
  131. package/src/gallery/default-gallery.ts +538 -0
  132. package/src/gallery/gallery-manifest.ts +5 -1
  133. package/src/influencer/client.ts +8 -6
  134. package/src/mcp/client.ts +1 -1
  135. package/src/media/avatar-router.ts +99 -0
  136. package/src/media/avatar-types.ts +60 -0
  137. package/src/media/managed-avatar-client.ts +189 -0
  138. package/src/memory/app-migration.ts +114 -0
  139. package/src/memory/app-store.ts +11 -0
  140. package/src/memory/qdrant-client.ts +1 -1
  141. package/src/messaging/providers/slack/client.ts +12 -2
  142. package/src/messaging/providers/sms/adapter.ts +6 -10
  143. package/src/migrations/data-layout.ts +8 -1
  144. package/src/oauth/token-persistence.ts +9 -6
  145. package/src/runtime/assistant-scope.ts +5 -0
  146. package/src/runtime/auth/route-policy.ts +4 -0
  147. package/src/runtime/channel-readiness-service.ts +9 -4
  148. package/src/runtime/gateway-internal-client.ts +11 -3
  149. package/src/runtime/http-server.ts +2 -0
  150. package/src/runtime/invite-redemption-service.ts +23 -13
  151. package/src/runtime/middleware/twilio-validation.ts +2 -2
  152. package/src/runtime/routes/app-routes.ts +131 -3
  153. package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -3
  154. package/src/runtime/routes/integration-routes.ts +2 -2
  155. package/src/runtime/routes/slack-share-routes.ts +235 -0
  156. package/src/runtime/routes/twilio-routes.ts +47 -34
  157. package/src/schedule/integration-status.ts +2 -3
  158. package/src/security/token-manager.ts +11 -3
  159. package/src/tools/apps/executors.ts +116 -8
  160. package/src/tools/browser/browser-manager.ts +30 -2
  161. package/src/tools/browser/chrome-cdp.ts +31 -3
  162. package/src/tools/credentials/vault.ts +9 -7
  163. package/src/tools/executor.ts +4 -0
  164. package/src/tools/system/avatar-generator.ts +55 -34
  165. package/src/twitter/client.ts +1 -1
  166. package/src/twitter/oauth-client.ts +31 -43
  167. package/src/twitter/router.ts +25 -23
  168. package/src/util/platform.ts +5 -0
  169. package/src/slack/slack-webhook.ts +0 -66
@@ -10,18 +10,49 @@ const testDir = mkdtempSync(join(tmpdir(), "handlers-twitter-cfg-test-"));
10
10
  let rawConfigStore: Record<string, unknown> = {};
11
11
  const saveRawConfigCalls: Record<string, unknown>[] = [];
12
12
 
13
+ function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
14
+ const keys = path.split(".");
15
+ let current: unknown = obj;
16
+ for (const key of keys) {
17
+ if (current == null || typeof current !== "object") {
18
+ return undefined;
19
+ }
20
+ current = (current as Record<string, unknown>)[key];
21
+ }
22
+ return current;
23
+ }
24
+
25
+ function setNestedValue(
26
+ obj: Record<string, unknown>,
27
+ path: string,
28
+ value: unknown,
29
+ ): void {
30
+ const keys = path.split(".");
31
+ let current = obj;
32
+ for (let i = 0; i < keys.length - 1; i++) {
33
+ const key = keys[i]!;
34
+ if (current[key] == null || typeof current[key] !== "object") {
35
+ current[key] = {};
36
+ }
37
+ current = current[key] as Record<string, unknown>;
38
+ }
39
+ current[keys[keys.length - 1]!] = value;
40
+ }
41
+
13
42
  mock.module("../config/loader.js", () => ({
14
43
  getConfig: () => ({
15
44
  ui: {},
16
45
  }),
17
46
  loadConfig: () => ({}),
18
- loadRawConfig: () => ({ ...rawConfigStore }),
47
+ loadRawConfig: () => structuredClone(rawConfigStore),
19
48
  saveRawConfig: (cfg: Record<string, unknown>) => {
20
49
  saveRawConfigCalls.push(cfg);
21
- rawConfigStore = { ...cfg };
50
+ rawConfigStore = structuredClone(cfg);
22
51
  },
23
52
  saveConfig: () => {},
24
53
  invalidateConfigCache: () => {},
54
+ getNestedValue,
55
+ setNestedValue,
25
56
  }));
26
57
 
27
58
  mock.module("../util/platform.js", () => ({
@@ -61,20 +92,27 @@ let secureKeyStore: Record<string, string> = {};
61
92
  let setSecureKeyOverride: ((account: string, value: string) => boolean) | null =
62
93
  null;
63
94
 
95
+ function syncSet(account: string, value: string): boolean {
96
+ if (setSecureKeyOverride) return setSecureKeyOverride(account, value);
97
+ secureKeyStore[account] = value;
98
+ return true;
99
+ }
100
+
101
+ function syncDelete(account: string): "deleted" | "not-found" {
102
+ if (account in secureKeyStore) {
103
+ delete secureKeyStore[account];
104
+ return "deleted";
105
+ }
106
+ return "not-found";
107
+ }
108
+
64
109
  mock.module("../security/secure-keys.js", () => ({
65
110
  getSecureKey: (account: string) => secureKeyStore[account] ?? undefined,
66
- setSecureKey: (account: string, value: string) => {
67
- if (setSecureKeyOverride) return setSecureKeyOverride(account, value);
68
- secureKeyStore[account] = value;
69
- return true;
70
- },
71
- deleteSecureKey: (account: string) => {
72
- if (account in secureKeyStore) {
73
- delete secureKeyStore[account];
74
- return "deleted";
75
- }
76
- return "not-found";
77
- },
111
+ setSecureKey: syncSet,
112
+ deleteSecureKey: syncDelete,
113
+ setSecureKeyAsync: async (account: string, value: string) =>
114
+ syncSet(account, value),
115
+ deleteSecureKeyAsync: async (account: string) => syncDelete(account),
78
116
  listSecureKeys: () => Object.keys(secureKeyStore),
79
117
  getBackendType: () => "encrypted",
80
118
  isDowngradedFromKeychain: () => false,
@@ -134,11 +172,26 @@ mock.module("../tools/credentials/metadata-store.js", () => ({
134
172
 
135
173
  import { handleMessage, type HandlerContext } from "../daemon/handlers.js";
136
174
  import type {
175
+ ClientMessage,
137
176
  ServerMessage,
138
177
  TwitterIntegrationConfigRequest,
139
178
  } from "../daemon/ipc-contract.js";
140
179
  import { DebouncerMap } from "../util/debounce.js";
141
180
 
181
+ /**
182
+ * Wrapper around handleMessage that flushes the microtask queue so async
183
+ * handlers complete before assertions run. handleMessage() returns void
184
+ * and swallows the promise, so we need a macrotask tick to settle.
185
+ */
186
+ async function handleMessageAsync(
187
+ msg: ClientMessage,
188
+ socket: net.Socket,
189
+ ctx: HandlerContext,
190
+ ): Promise<void> {
191
+ handleMessage(msg, socket, ctx);
192
+ await new Promise<void>((r) => setTimeout(r, 0));
193
+ }
194
+
142
195
  function createTestContext(): { ctx: HandlerContext; sent: ServerMessage[] } {
143
196
  const sent: ServerMessage[] = [];
144
197
  const ctx: HandlerContext = {
@@ -203,7 +256,7 @@ describe("Twitter integration config handler", () => {
203
256
  });
204
257
 
205
258
  test("get action returns correct status when configured and connected", () => {
206
- rawConfigStore = { twitterIntegrationMode: "local_byo" };
259
+ rawConfigStore = { twitter: { integrationMode: "local_byo" } };
207
260
  secureKeyStore["credential:integration:twitter:oauth_client_id"] =
208
261
  "test-client-id";
209
262
  secureKeyStore["credential:integration:twitter:access_token"] =
@@ -256,10 +309,12 @@ describe("Twitter integration config handler", () => {
256
309
  expect(res.mode).toBe("managed");
257
310
 
258
311
  expect(saveRawConfigCalls).toHaveLength(1);
259
- expect(saveRawConfigCalls[0]!.twitterIntegrationMode).toBe("managed");
312
+ expect(
313
+ getNestedValue(saveRawConfigCalls[0]!, "twitter.integrationMode"),
314
+ ).toBe("managed");
260
315
  });
261
316
 
262
- test("set_local_client stores credentials in secure storage", () => {
317
+ test("set_local_client stores credentials in secure storage", async () => {
263
318
  const msg: TwitterIntegrationConfigRequest = {
264
319
  type: "twitter_integration_config",
265
320
  action: "set_local_client",
@@ -268,7 +323,7 @@ describe("Twitter integration config handler", () => {
268
323
  };
269
324
 
270
325
  const { ctx, sent } = createTestContext();
271
- handleMessage(msg, {} as net.Socket, ctx);
326
+ await handleMessageAsync(msg, {} as net.Socket, ctx);
272
327
 
273
328
  expect(sent).toHaveLength(1);
274
329
  const res = sent[0] as {
@@ -288,14 +343,14 @@ describe("Twitter integration config handler", () => {
288
343
  ).toBe("my-client-secret");
289
344
  });
290
345
 
291
- test("set_local_client without clientId returns error", () => {
346
+ test("set_local_client without clientId returns error", async () => {
292
347
  const msg: TwitterIntegrationConfigRequest = {
293
348
  type: "twitter_integration_config",
294
349
  action: "set_local_client",
295
350
  };
296
351
 
297
352
  const { ctx, sent } = createTestContext();
298
- handleMessage(msg, {} as net.Socket, ctx);
353
+ await handleMessageAsync(msg, {} as net.Socket, ctx);
299
354
 
300
355
  expect(sent).toHaveLength(1);
301
356
  const res = sent[0] as { type: string; success: boolean; error: string };
@@ -303,7 +358,7 @@ describe("Twitter integration config handler", () => {
303
358
  expect(res.error).toContain("clientId is required");
304
359
  });
305
360
 
306
- test("clear_local_client removes credentials", () => {
361
+ test("clear_local_client removes credentials", async () => {
307
362
  secureKeyStore["credential:integration:twitter:oauth_client_id"] =
308
363
  "my-client-id";
309
364
  secureKeyStore["credential:integration:twitter:oauth_client_secret"] =
@@ -315,7 +370,7 @@ describe("Twitter integration config handler", () => {
315
370
  };
316
371
 
317
372
  const { ctx, sent } = createTestContext();
318
- handleMessage(msg, {} as net.Socket, ctx);
373
+ await handleMessageAsync(msg, {} as net.Socket, ctx);
319
374
 
320
375
  expect(sent).toHaveLength(1);
321
376
  const res = sent[0] as {
@@ -336,7 +391,7 @@ describe("Twitter integration config handler", () => {
336
391
  ).toBeUndefined();
337
392
  });
338
393
 
339
- test("clear_local_client also disconnects if connected", () => {
394
+ test("clear_local_client also disconnects if connected", async () => {
340
395
  secureKeyStore["credential:integration:twitter:oauth_client_id"] =
341
396
  "my-client-id";
342
397
  secureKeyStore["credential:integration:twitter:access_token"] =
@@ -355,7 +410,7 @@ describe("Twitter integration config handler", () => {
355
410
  };
356
411
 
357
412
  const { ctx, sent } = createTestContext();
358
- handleMessage(msg, {} as net.Socket, ctx);
413
+ await handleMessageAsync(msg, {} as net.Socket, ctx);
359
414
 
360
415
  expect(sent).toHaveLength(1);
361
416
  const res = sent[0] as {
@@ -378,7 +433,7 @@ describe("Twitter integration config handler", () => {
378
433
  });
379
434
  });
380
435
 
381
- test("disconnect removes tokens and metadata", () => {
436
+ test("disconnect removes tokens and metadata", async () => {
382
437
  secureKeyStore["credential:integration:twitter:oauth_client_id"] =
383
438
  "my-client-id";
384
439
  secureKeyStore["credential:integration:twitter:access_token"] =
@@ -397,7 +452,7 @@ describe("Twitter integration config handler", () => {
397
452
  };
398
453
 
399
454
  const { ctx, sent } = createTestContext();
400
- handleMessage(msg, {} as net.Socket, ctx);
455
+ await handleMessageAsync(msg, {} as net.Socket, ctx);
401
456
 
402
457
  expect(sent).toHaveLength(1);
403
458
  const res = sent[0] as {
@@ -426,7 +481,7 @@ describe("Twitter integration config handler", () => {
426
481
  });
427
482
  });
428
483
 
429
- test("set_local_client returns error when setSecureKey fails for client ID", () => {
484
+ test("set_local_client returns error when setSecureKey fails for client ID", async () => {
430
485
  // Override setSecureKey to return false (storage unavailable, not throwing)
431
486
  setSecureKeyOverride = () => false;
432
487
 
@@ -438,7 +493,7 @@ describe("Twitter integration config handler", () => {
438
493
  };
439
494
 
440
495
  const { ctx, sent } = createTestContext();
441
- handleMessage(msg, {} as net.Socket, ctx);
496
+ await handleMessageAsync(msg, {} as net.Socket, ctx);
442
497
 
443
498
  expect(sent).toHaveLength(1);
444
499
  const res = sent[0] as {
@@ -453,7 +508,7 @@ describe("Twitter integration config handler", () => {
453
508
  expect(res.error).toContain("Failed to store client ID");
454
509
  });
455
510
 
456
- test("set_local_client returns error when setSecureKey fails for client secret", () => {
511
+ test("set_local_client returns error when setSecureKey fails for client secret", async () => {
457
512
  // Override setSecureKey to fail only for the secret
458
513
  setSecureKeyOverride = (account: string, value: string) => {
459
514
  if (account.includes("client_secret")) return false;
@@ -469,7 +524,7 @@ describe("Twitter integration config handler", () => {
469
524
  };
470
525
 
471
526
  const { ctx, sent } = createTestContext();
472
- handleMessage(msg, {} as net.Socket, ctx);
527
+ await handleMessageAsync(msg, {} as net.Socket, ctx);
473
528
 
474
529
  expect(sent).toHaveLength(1);
475
530
  const res = sent[0] as {
@@ -484,7 +539,7 @@ describe("Twitter integration config handler", () => {
484
539
  expect(res.error).toContain("Failed to store client secret");
485
540
  });
486
541
 
487
- test("set_local_client without secret clears stale secret", () => {
542
+ test("set_local_client without secret clears stale secret", async () => {
488
543
  // Pre-populate an old client secret
489
544
  secureKeyStore["credential:integration:twitter:oauth_client_id"] = "old-id";
490
545
  secureKeyStore["credential:integration:twitter:oauth_client_secret"] =
@@ -497,7 +552,7 @@ describe("Twitter integration config handler", () => {
497
552
  };
498
553
 
499
554
  const { ctx, sent } = createTestContext();
500
- handleMessage(msg, {} as net.Socket, ctx);
555
+ await handleMessageAsync(msg, {} as net.Socket, ctx);
501
556
 
502
557
  expect(sent).toHaveLength(1);
503
558
  const res = sent[0] as {
@@ -576,7 +631,7 @@ describe("Twitter integration config handler", () => {
576
631
  expect((sent4[0] as { mode: string }).mode).toBe("local_byo");
577
632
  });
578
633
 
579
- test("set_local_client with only clientId (no secret)", () => {
634
+ test("set_local_client with only clientId (no secret)", async () => {
580
635
  const msg: TwitterIntegrationConfigRequest = {
581
636
  type: "twitter_integration_config",
582
637
  action: "set_local_client",
@@ -584,7 +639,7 @@ describe("Twitter integration config handler", () => {
584
639
  };
585
640
 
586
641
  const { ctx, sent } = createTestContext();
587
- handleMessage(msg, {} as net.Socket, ctx);
642
+ await handleMessageAsync(msg, {} as net.Socket, ctx);
588
643
 
589
644
  expect(sent).toHaveLength(1);
590
645
  const res = sent[0] as {
@@ -604,7 +659,7 @@ describe("Twitter integration config handler", () => {
604
659
  ).toBeUndefined();
605
660
  });
606
661
 
607
- test("set_local_client overwrites existing credentials", () => {
662
+ test("set_local_client overwrites existing credentials", async () => {
608
663
  // Set initial credentials
609
664
  secureKeyStore["credential:integration:twitter:oauth_client_id"] = "old-id";
610
665
  secureKeyStore["credential:integration:twitter:oauth_client_secret"] =
@@ -618,7 +673,7 @@ describe("Twitter integration config handler", () => {
618
673
  };
619
674
 
620
675
  const { ctx, sent } = createTestContext();
621
- handleMessage(msg, {} as net.Socket, ctx);
676
+ await handleMessageAsync(msg, {} as net.Socket, ctx);
622
677
 
623
678
  expect(sent).toHaveLength(1);
624
679
  const res = sent[0] as {
@@ -638,7 +693,7 @@ describe("Twitter integration config handler", () => {
638
693
  ).toBe("new-secret");
639
694
  });
640
695
 
641
- test("clear_local_client when no credentials exist (idempotent)", () => {
696
+ test("clear_local_client when no credentials exist (idempotent)", async () => {
642
697
  // No credentials set at all
643
698
  const msg: TwitterIntegrationConfigRequest = {
644
699
  type: "twitter_integration_config",
@@ -646,7 +701,7 @@ describe("Twitter integration config handler", () => {
646
701
  };
647
702
 
648
703
  const { ctx, sent } = createTestContext();
649
- handleMessage(msg, {} as net.Socket, ctx);
704
+ await handleMessageAsync(msg, {} as net.Socket, ctx);
650
705
 
651
706
  expect(sent).toHaveLength(1);
652
707
  const res = sent[0] as {
@@ -661,7 +716,7 @@ describe("Twitter integration config handler", () => {
661
716
  expect(res.connected).toBe(false);
662
717
  });
663
718
 
664
- test("disconnect when not connected (idempotent) preserves client credentials", () => {
719
+ test("disconnect when not connected (idempotent) preserves client credentials", async () => {
665
720
  // Only client credentials, no access token
666
721
  secureKeyStore["credential:integration:twitter:oauth_client_id"] =
667
722
  "my-client-id";
@@ -674,7 +729,7 @@ describe("Twitter integration config handler", () => {
674
729
  };
675
730
 
676
731
  const { ctx, sent } = createTestContext();
677
- handleMessage(msg, {} as net.Socket, ctx);
732
+ await handleMessageAsync(msg, {} as net.Socket, ctx);
678
733
 
679
734
  expect(sent).toHaveLength(1);
680
735
  const res = sent[0] as {
@@ -696,7 +751,7 @@ describe("Twitter integration config handler", () => {
696
751
  ).toBe("my-client-secret");
697
752
  });
698
753
 
699
- test("disconnect preserves client credentials when access token exists", () => {
754
+ test("disconnect preserves client credentials when access token exists", async () => {
700
755
  // Set up both client credentials and tokens
701
756
  secureKeyStore["credential:integration:twitter:oauth_client_id"] =
702
757
  "my-client-id";
@@ -718,7 +773,7 @@ describe("Twitter integration config handler", () => {
718
773
  };
719
774
 
720
775
  const { ctx, sent } = createTestContext();
721
- handleMessage(msg, {} as net.Socket, ctx);
776
+ await handleMessageAsync(msg, {} as net.Socket, ctx);
722
777
 
723
778
  expect(sent).toHaveLength(1);
724
779
  const res = sent[0] as {
@@ -752,7 +807,7 @@ describe("Twitter integration config handler", () => {
752
807
  });
753
808
  });
754
809
 
755
- test("clear_local_client cascades to remove tokens and metadata", () => {
810
+ test("clear_local_client cascades to remove tokens and metadata", async () => {
756
811
  // Set up client credentials, tokens, and metadata
757
812
  secureKeyStore["credential:integration:twitter:oauth_client_id"] =
758
813
  "my-client-id";
@@ -774,7 +829,7 @@ describe("Twitter integration config handler", () => {
774
829
  };
775
830
 
776
831
  const { ctx, sent } = createTestContext();
777
- handleMessage(msg, {} as net.Socket, ctx);
832
+ await handleMessageAsync(msg, {} as net.Socket, ctx);
778
833
 
779
834
  expect(sent).toHaveLength(1);
780
835
  const res = sent[0] as {
@@ -860,7 +915,7 @@ describe("Twitter integration config handler", () => {
860
915
  expect(res.connected).toBe(false);
861
916
  });
862
917
 
863
- test("error in secure storage throws and returns error response", () => {
918
+ test("error in secure storage throws and returns error response", async () => {
864
919
  // Override setSecureKey to throw an error, simulating a storage failure
865
920
  setSecureKeyOverride = () => {
866
921
  throw new Error("Keychain access denied");
@@ -874,7 +929,7 @@ describe("Twitter integration config handler", () => {
874
929
  };
875
930
 
876
931
  const { ctx, sent } = createTestContext();
877
- handleMessage(msg, {} as net.Socket, ctx);
932
+ await handleMessageAsync(msg, {} as net.Socket, ctx);
878
933
 
879
934
  expect(sent).toHaveLength(1);
880
935
  const res = sent[0] as { type: string; success: boolean; error?: string };
@@ -984,7 +1039,9 @@ describe("Twitter integration config handler", () => {
984
1039
  // Verify persistence via saveRawConfig
985
1040
  expect(saveRawConfigCalls.length).toBeGreaterThan(0);
986
1041
  const lastSaved = saveRawConfigCalls[saveRawConfigCalls.length - 1]!;
987
- expect(lastSaved.twitterOperationStrategy).toBe("oauth");
1042
+ expect(getNestedValue(lastSaved, "twitter.operationStrategy")).toBe(
1043
+ "oauth",
1044
+ );
988
1045
  });
989
1046
 
990
1047
  test("set_strategy with invalid value returns error", () => {
@@ -1022,7 +1079,7 @@ describe("Twitter integration config handler", () => {
1022
1079
 
1023
1080
  test("get action includes strategy field with strategyConfigured=true when set", () => {
1024
1081
  // Set a specific strategy first
1025
- rawConfigStore = { twitterOperationStrategy: "browser" };
1082
+ rawConfigStore = { twitter: { operationStrategy: "browser" } };
1026
1083
 
1027
1084
  const msg: TwitterIntegrationConfigRequest = {
1028
1085
  type: "twitter_integration_config",
@@ -87,7 +87,7 @@ mock.module("../runtime/approval-message-composer.js", () => ({
87
87
  }));
88
88
 
89
89
  import { findContactChannel } from "../contacts/contact-store.js";
90
- import { upsertMember } from "../contacts/contacts-write.js";
90
+ import { upsertContactChannel } from "../contacts/contacts-write.js";
91
91
  import { getDb, initializeDb, resetDb } from "../memory/db.js";
92
92
  import { createInvite, revokeInvite } from "../memory/invite-store.js";
93
93
  import { handleChannelInbound } from "../runtime/routes/channel-routes.js";
@@ -328,7 +328,7 @@ describe("inbound invite redemption intercept", () => {
328
328
 
329
329
  test("existing active member sending normal message is unaffected", async () => {
330
330
  // Pre-create an active member
331
- upsertMember({
331
+ upsertContactChannel({
332
332
  sourceChannel: "telegram",
333
333
  externalUserId: "user-active-member",
334
334
  externalChatId: "chat-active",
@@ -377,7 +377,7 @@ describe("inbound invite redemption intercept", () => {
377
377
  });
378
378
 
379
379
  // Pre-create an active member that will click the invite link
380
- upsertMember({
380
+ upsertContactChannel({
381
381
  sourceChannel: "telegram",
382
382
  externalUserId: "user-already-active",
383
383
  externalChatId: "chat-invite-test",
@@ -403,7 +403,7 @@ describe("inbound invite redemption intercept", () => {
403
403
  maxUses: 5,
404
404
  });
405
405
 
406
- upsertMember({
406
+ upsertContactChannel({
407
407
  sourceChannel: "telegram",
408
408
  externalUserId: "user-invite-123",
409
409
  externalChatId: "chat-invite-test",
@@ -1,17 +1,27 @@
1
1
  import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  const secureKeyValues = new Map<string, string>();
4
+ let mockTwilioAccountSid: string | undefined;
4
5
 
5
6
  mock.module("../security/secure-keys.js", () => ({
6
7
  getSecureKey: (account: string) => secureKeyValues.get(account),
7
8
  }));
8
9
 
10
+ mock.module("../config/loader.js", () => ({
11
+ loadConfig: () => ({
12
+ twilio: mockTwilioAccountSid
13
+ ? { accountSid: mockTwilioAccountSid }
14
+ : undefined,
15
+ }),
16
+ }));
17
+
9
18
  const { getIntegrationSummary, formatIntegrationSummary, hasCapability } =
10
19
  await import("../schedule/integration-status.js");
11
20
 
12
21
  describe("integration-status", () => {
13
22
  beforeEach(() => {
14
23
  secureKeyValues.clear();
24
+ mockTwilioAccountSid = undefined;
15
25
  });
16
26
 
17
27
  describe("getIntegrationSummary", () => {
@@ -30,7 +40,7 @@ describe("integration-status", () => {
30
40
  secureKeyValues.set("credential:integration:gmail:access_token", "tok");
31
41
  secureKeyValues.set("credential:integration:twitter:access_token", "tok");
32
42
  secureKeyValues.set("credential:integration:slack:access_token", "tok");
33
- secureKeyValues.set("credential:twilio:account_sid", "sid");
43
+ mockTwilioAccountSid = "sid";
34
44
  secureKeyValues.set("credential:twilio:auth_token", "auth");
35
45
  secureKeyValues.set("credential:telegram:bot_token", "tok");
36
46
  secureKeyValues.set("credential:telegram:webhook_secret", "secret");
@@ -42,7 +52,7 @@ describe("integration-status", () => {
42
52
  });
43
53
 
44
54
  test("returns mixed status", () => {
45
- secureKeyValues.set("credential:twilio:account_sid", "sid");
55
+ mockTwilioAccountSid = "sid";
46
56
  secureKeyValues.set("credential:twilio:auth_token", "auth");
47
57
  secureKeyValues.set("credential:telegram:bot_token", "tok");
48
58
  secureKeyValues.set("credential:telegram:webhook_secret", "secret");
@@ -67,7 +77,7 @@ describe("integration-status", () => {
67
77
  });
68
78
 
69
79
  test("SMS disconnected when only account_sid is set (missing auth_token)", () => {
70
- secureKeyValues.set("credential:twilio:account_sid", "sid");
80
+ mockTwilioAccountSid = "sid";
71
81
 
72
82
  const summary = getIntegrationSummary();
73
83
  const sms = summary.find((s: { name: string }) => s.name === "SMS");
@@ -87,7 +97,7 @@ describe("integration-status", () => {
87
97
 
88
98
  describe("formatIntegrationSummary", () => {
89
99
  test("shows checkmarks and crosses", () => {
90
- secureKeyValues.set("credential:twilio:account_sid", "sid");
100
+ mockTwilioAccountSid = "sid";
91
101
  secureKeyValues.set("credential:twilio:auth_token", "auth");
92
102
  secureKeyValues.set("credential:telegram:bot_token", "tok");
93
103
  secureKeyValues.set("credential:telegram:webhook_secret", "secret");
@@ -109,7 +119,7 @@ describe("integration-status", () => {
109
119
  secureKeyValues.set("credential:integration:gmail:access_token", "tok");
110
120
  secureKeyValues.set("credential:integration:twitter:access_token", "tok");
111
121
  secureKeyValues.set("credential:integration:slack:access_token", "tok");
112
- secureKeyValues.set("credential:twilio:account_sid", "sid");
122
+ mockTwilioAccountSid = "sid";
113
123
  secureKeyValues.set("credential:twilio:auth_token", "auth");
114
124
  secureKeyValues.set("credential:telegram:bot_token", "tok");
115
125
  secureKeyValues.set("credential:telegram:webhook_secret", "secret");
@@ -94,7 +94,7 @@ async function runCli(
94
94
  };
95
95
  }
96
96
 
97
- describe("vellum integrations CLI", () => {
97
+ describe("assistant integrations CLI", () => {
98
98
  beforeEach(() => {
99
99
  gatewayBase = "http://gateway.test";
100
100
  signingKeyInitialized = false;
@@ -24,7 +24,8 @@ mock.module("../util/logger.js", () => ({
24
24
  }),
25
25
  }));
26
26
 
27
- import { upsertMember } from "../contacts/contacts-write.js";
27
+ import { findContactChannel } from "../contacts/contact-store.js";
28
+ import { upsertContactChannel } from "../contacts/contacts-write.js";
28
29
  import { getSqlite, initializeDb, resetDb } from "../memory/db.js";
29
30
  import {
30
31
  createInvite,
@@ -33,7 +34,9 @@ import {
33
34
  import {
34
35
  type InviteRedemptionOutcome,
35
36
  redeemInvite,
37
+ redeemInviteByCode,
36
38
  } from "../runtime/invite-redemption-service.js";
39
+ import { hashVoiceCode } from "../util/voice-code.js";
37
40
 
38
41
  initializeDb();
39
42
 
@@ -76,6 +79,58 @@ describe("invite-redemption-service", () => {
76
79
  });
77
80
  });
78
81
 
82
+ test("marks channel as verified via invite on redemption", () => {
83
+ const { rawToken } = createInvite({
84
+ sourceChannel: "telegram",
85
+ maxUses: 1,
86
+ });
87
+
88
+ const outcome = redeemInvite({
89
+ rawToken,
90
+ sourceChannel: "telegram",
91
+ externalUserId: "user-1",
92
+ });
93
+
94
+ expect(outcome.ok).toBe(true);
95
+
96
+ const result = findContactChannel({
97
+ channelType: "telegram",
98
+ externalUserId: "user-1",
99
+ });
100
+
101
+ expect(result).not.toBeNull();
102
+ expect(result!.channel.verifiedAt).toBeGreaterThan(0);
103
+ expect(result!.channel.verifiedVia).toBe("invite");
104
+ expect(result!.channel.status).toBe("active");
105
+ });
106
+
107
+ test("marks channel as verified via invite on 6-digit code redemption", () => {
108
+ const inviteCode = "123456";
109
+ createInvite({
110
+ sourceChannel: "telegram",
111
+ maxUses: 1,
112
+ inviteCodeHash: hashVoiceCode(inviteCode),
113
+ });
114
+
115
+ const outcome = redeemInviteByCode({
116
+ code: inviteCode,
117
+ sourceChannel: "telegram",
118
+ externalUserId: "code-user-1",
119
+ });
120
+
121
+ expect(outcome.ok).toBe(true);
122
+
123
+ const result = findContactChannel({
124
+ channelType: "telegram",
125
+ externalUserId: "code-user-1",
126
+ });
127
+
128
+ expect(result).not.toBeNull();
129
+ expect(result!.channel.verifiedAt).toBeGreaterThan(0);
130
+ expect(result!.channel.verifiedVia).toBe("invite");
131
+ expect(result!.channel.status).toBe("active");
132
+ });
133
+
79
134
  test("returns invalid_token for a bogus token", () => {
80
135
  const outcome = redeemInvite({
81
136
  rawToken: "totally-bogus-token",
@@ -179,7 +234,7 @@ describe("invite-redemption-service", () => {
179
234
  });
180
235
 
181
236
  // Pre-create an active member
182
- upsertMember({
237
+ upsertContactChannel({
183
238
  sourceChannel: "telegram",
184
239
  externalUserId: "existing-user",
185
240
  status: "active",
@@ -209,7 +264,7 @@ describe("invite-redemption-service", () => {
209
264
  });
210
265
 
211
266
  // Pre-create a blocked member — simulates a guardian-initiated block
212
- upsertMember({
267
+ upsertContactChannel({
213
268
  sourceChannel: "telegram",
214
269
  externalUserId: "blocked-user",
215
270
  status: "blocked",
@@ -231,7 +286,7 @@ describe("invite-redemption-service", () => {
231
286
  });
232
287
 
233
288
  // Pre-create a revoked member
234
- const member = upsertMember({
289
+ const member = upsertContactChannel({
235
290
  sourceChannel: "telegram",
236
291
  externalUserId: "revoked-user",
237
292
  status: "revoked",
@@ -282,7 +337,7 @@ describe("invite-redemption-service", () => {
282
337
 
283
338
  test("returns invalid_token for an active member with a bogus token (no membership probing)", () => {
284
339
  // Pre-create an active member
285
- upsertMember({
340
+ upsertContactChannel({
286
341
  sourceChannel: "telegram",
287
342
  externalUserId: "probed-user",
288
343
  status: "active",
@@ -307,7 +362,7 @@ describe("invite-redemption-service", () => {
307
362
  });
308
363
 
309
364
  // Pre-create an active member
310
- upsertMember({
365
+ upsertContactChannel({
311
366
  sourceChannel: "telegram",
312
367
  externalUserId: "expired-token-user",
313
368
  status: "active",
@@ -331,7 +386,7 @@ describe("invite-redemption-service", () => {
331
386
  });
332
387
 
333
388
  // Pre-create an active member on telegram
334
- upsertMember({
389
+ upsertContactChannel({
335
390
  sourceChannel: "telegram",
336
391
  externalUserId: "cross-channel-user",
337
392
  status: "active",
@@ -398,10 +398,6 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
398
398
  type: "share_app_cloud",
399
399
  appId: "app-001",
400
400
  },
401
- share_to_slack: {
402
- type: "share_to_slack",
403
- appId: "app-001",
404
- },
405
401
  slack_webhook_config: {
406
402
  type: "slack_webhook_config",
407
403
  action: "get",
@@ -1450,10 +1446,6 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
1450
1446
  shareToken: "abc123def456",
1451
1447
  shareUrl: "http://localhost:7821/v1/apps/shared/abc123def456",
1452
1448
  },
1453
- share_to_slack_response: {
1454
- type: "share_to_slack_response",
1455
- success: true,
1456
- },
1457
1449
  slack_webhook_config_response: {
1458
1450
  type: "slack_webhook_config_response",
1459
1451
  webhookUrl: "https://hooks.slack.com/services/T00/B00/xxx",