@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
@@ -52,25 +52,32 @@ mock.module("../util/logger.js", () => ({
52
52
  // Mock secure key storage
53
53
  let secureKeyStore: Record<string, string> = {};
54
54
 
55
- mock.module("../security/secure-keys.js", () => ({
56
- getSecureKey: (account: string) => secureKeyStore[account] ?? undefined,
57
- setSecureKey: (account: string, value: string) => {
55
+ mock.module("../security/secure-keys.js", () => {
56
+ const syncSet = (account: string, value: string) => {
58
57
  secureKeyStore[account] = value;
59
58
  return true;
60
- },
61
- deleteSecureKey: (account: string) => {
59
+ };
60
+ const syncDelete = (account: string) => {
62
61
  if (account in secureKeyStore) {
63
62
  delete secureKeyStore[account];
64
- return "deleted";
63
+ return "deleted" as const;
65
64
  }
66
- return "not-found";
67
- },
68
- listSecureKeys: () => Object.keys(secureKeyStore),
69
- getBackendType: () => "encrypted",
70
- isDowngradedFromKeychain: () => false,
71
- _resetBackend: () => {},
72
- _setBackend: () => {},
73
- }));
65
+ return "not-found" as const;
66
+ };
67
+ return {
68
+ getSecureKey: (account: string) => secureKeyStore[account] ?? undefined,
69
+ setSecureKey: syncSet,
70
+ deleteSecureKey: syncDelete,
71
+ setSecureKeyAsync: async (account: string, value: string) =>
72
+ syncSet(account, value),
73
+ deleteSecureKeyAsync: async (account: string) => syncDelete(account),
74
+ listSecureKeys: () => Object.keys(secureKeyStore),
75
+ getBackendType: () => "encrypted",
76
+ isDowngradedFromKeychain: () => false,
77
+ _resetBackend: () => {},
78
+ _setBackend: () => {},
79
+ };
80
+ });
74
81
 
75
82
  // Mock credential metadata store
76
83
  let credentialMetadataStore: Array<{
@@ -245,7 +252,7 @@ describe("Slack channel config handler", () => {
245
252
  expect(result.error).toContain("invalid_auth");
246
253
  });
247
254
 
248
- test("DELETE clears credentials", () => {
255
+ test("DELETE clears credentials", async () => {
249
256
  secureKeyStore["credential:slack_channel:bot_token"] = "xoxb-test";
250
257
  secureKeyStore["credential:slack_channel:app_token"] = "xapp-test";
251
258
  credentialMetadataStore.push({
@@ -257,7 +264,7 @@ describe("Slack channel config handler", () => {
257
264
  field: "app_token",
258
265
  });
259
266
 
260
- const result = clearSlackChannelConfig();
267
+ const result = await clearSlackChannelConfig();
261
268
  expect(result.success).toBe(true);
262
269
  expect(result.hasBotToken).toBe(false);
263
270
  expect(result.hasAppToken).toBe(false);
@@ -0,0 +1,263 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Mocks — must be declared before any imports that pull in mocked modules
5
+ // ---------------------------------------------------------------------------
6
+
7
+ const secureKeyValues = new Map<string, string>();
8
+ mock.module("../security/secure-keys.js", () => ({
9
+ getSecureKey: (key: string) => secureKeyValues.get(key),
10
+ setSecureKeyAsync: async () => {},
11
+ }));
12
+
13
+ let listConversationsResult: unknown = { ok: true, channels: [] };
14
+ let postMessageResult: unknown = {
15
+ ok: true,
16
+ ts: "1234567890.123456",
17
+ channel: "C123",
18
+ message: { ts: "1234567890.123456", text: "", type: "message" },
19
+ };
20
+ let userInfoResults: Map<string, unknown> = new Map();
21
+
22
+ mock.module("../messaging/providers/slack/client.js", () => ({
23
+ listConversations: async () => listConversationsResult,
24
+ postMessage: async (
25
+ _token: string,
26
+ _channel: string,
27
+ _text: string,
28
+ _opts?: unknown,
29
+ ) => postMessageResult,
30
+ userInfo: async (_token: string, userId: string) => {
31
+ const result = userInfoResults.get(userId);
32
+ if (result) return result;
33
+ throw new Error(`User not found: ${userId}`);
34
+ },
35
+ }));
36
+
37
+ let appStoreResult: unknown = null;
38
+ mock.module("../memory/app-store.js", () => ({
39
+ getApp: (_id: string) => appStoreResult,
40
+ getAppsDir: () => "/tmp/apps",
41
+ isMultifileApp: () => false,
42
+ listApps: () => [],
43
+ }));
44
+
45
+ mock.module("../util/logger.js", () => ({
46
+ getLogger: () => ({
47
+ info: () => {},
48
+ warn: () => {},
49
+ error: () => {},
50
+ debug: () => {},
51
+ trace: () => {},
52
+ fatal: () => {},
53
+ child: () => ({
54
+ info: () => {},
55
+ warn: () => {},
56
+ error: () => {},
57
+ debug: () => {},
58
+ }),
59
+ }),
60
+ }));
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Import under test (after mocks)
64
+ // ---------------------------------------------------------------------------
65
+
66
+ const { handleListSlackChannels, handleShareToSlackChannel } =
67
+ await import("../runtime/routes/slack-share-routes.js");
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Helpers
71
+ // ---------------------------------------------------------------------------
72
+
73
+ function makeRequest(body: unknown): Request {
74
+ return new Request("http://localhost/v1/slack/share", {
75
+ method: "POST",
76
+ headers: { "Content-Type": "application/json" },
77
+ body: JSON.stringify(body),
78
+ });
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Tests
83
+ // ---------------------------------------------------------------------------
84
+
85
+ beforeEach(() => {
86
+ secureKeyValues.clear();
87
+ listConversationsResult = { ok: true, channels: [] };
88
+ userInfoResults = new Map();
89
+ appStoreResult = null;
90
+ postMessageResult = {
91
+ ok: true,
92
+ ts: "1234567890.123456",
93
+ channel: "C123",
94
+ message: { ts: "1234567890.123456", text: "", type: "message" },
95
+ };
96
+ });
97
+
98
+ describe("handleListSlackChannels", () => {
99
+ test("returns 503 when no token is configured", async () => {
100
+ const res = await handleListSlackChannels();
101
+ expect(res.status).toBe(503);
102
+ const json = (await res.json()) as { error: { code: string } };
103
+ expect(json.error.code).toBe("SERVICE_UNAVAILABLE");
104
+ });
105
+
106
+ test("returns channels sorted by type then name", async () => {
107
+ secureKeyValues.set(
108
+ "credential:integration:slack:access_token",
109
+ "xoxb-test",
110
+ );
111
+
112
+ listConversationsResult = {
113
+ ok: true,
114
+ channels: [
115
+ {
116
+ id: "D1",
117
+ name: undefined,
118
+ is_im: true,
119
+ user: "U1",
120
+ is_private: true,
121
+ },
122
+ { id: "C2", name: "beta-channel", is_channel: true },
123
+ { id: "C1", name: "alpha-channel", is_channel: true },
124
+ { id: "G1", name: "group-chat", is_mpim: true, is_private: true },
125
+ ],
126
+ };
127
+
128
+ userInfoResults.set("U1", {
129
+ ok: true,
130
+ user: {
131
+ id: "U1",
132
+ name: "alice",
133
+ profile: { display_name: "Alice Smith" },
134
+ },
135
+ });
136
+
137
+ const res = await handleListSlackChannels();
138
+ expect(res.status).toBe(200);
139
+ const json = (await res.json()) as {
140
+ channels: Array<{
141
+ id: string;
142
+ name: string;
143
+ type: string;
144
+ isPrivate: boolean;
145
+ }>;
146
+ };
147
+
148
+ expect(json.channels).toHaveLength(4);
149
+ // Channels first (alphabetical)
150
+ expect(json.channels[0]).toEqual({
151
+ id: "C1",
152
+ name: "alpha-channel",
153
+ type: "channel",
154
+ isPrivate: false,
155
+ });
156
+ expect(json.channels[1]).toEqual({
157
+ id: "C2",
158
+ name: "beta-channel",
159
+ type: "channel",
160
+ isPrivate: false,
161
+ });
162
+ // Groups
163
+ expect(json.channels[2]).toEqual({
164
+ id: "G1",
165
+ name: "group-chat",
166
+ type: "group",
167
+ isPrivate: true,
168
+ });
169
+ // DMs (name resolved from userInfo)
170
+ expect(json.channels[3]).toEqual({
171
+ id: "D1",
172
+ name: "Alice Smith",
173
+ type: "dm",
174
+ isPrivate: true,
175
+ });
176
+ });
177
+
178
+ test("falls back to legacy bot token", async () => {
179
+ secureKeyValues.set("credential:slack_channel:bot_token", "xoxb-legacy");
180
+
181
+ listConversationsResult = { ok: true, channels: [] };
182
+
183
+ const res = await handleListSlackChannels();
184
+ expect(res.status).toBe(200);
185
+ });
186
+ });
187
+
188
+ describe("handleShareToSlackChannel", () => {
189
+ test("returns 503 when no token is configured", async () => {
190
+ const req = makeRequest({ appId: "app1", channelId: "C1" });
191
+ const res = await handleShareToSlackChannel(req);
192
+ expect(res.status).toBe(503);
193
+ });
194
+
195
+ test("returns 400 for malformed JSON", async () => {
196
+ secureKeyValues.set(
197
+ "credential:integration:slack:access_token",
198
+ "xoxb-test",
199
+ );
200
+ const req = new Request("http://localhost/v1/slack/share", {
201
+ method: "POST",
202
+ headers: { "Content-Type": "application/json" },
203
+ body: "not json",
204
+ });
205
+ const res = await handleShareToSlackChannel(req);
206
+ expect(res.status).toBe(400);
207
+ });
208
+
209
+ test("returns 400 when missing required fields", async () => {
210
+ secureKeyValues.set(
211
+ "credential:integration:slack:access_token",
212
+ "xoxb-test",
213
+ );
214
+ const req = makeRequest({ appId: "app1" });
215
+ const res = await handleShareToSlackChannel(req);
216
+ expect(res.status).toBe(400);
217
+ const json = (await res.json()) as { error: { message: string } };
218
+ expect(json.error.message).toContain("Missing required fields");
219
+ });
220
+
221
+ test("returns 404 when app not found", async () => {
222
+ secureKeyValues.set(
223
+ "credential:integration:slack:access_token",
224
+ "xoxb-test",
225
+ );
226
+ appStoreResult = null;
227
+ const req = makeRequest({ appId: "missing-app", channelId: "C1" });
228
+ const res = await handleShareToSlackChannel(req);
229
+ expect(res.status).toBe(404);
230
+ });
231
+
232
+ test("posts message and returns success", async () => {
233
+ secureKeyValues.set(
234
+ "credential:integration:slack:access_token",
235
+ "xoxb-test",
236
+ );
237
+ appStoreResult = {
238
+ id: "app1",
239
+ name: "My App",
240
+ description: "A great app",
241
+ htmlDefinition: "<div></div>",
242
+ schemaJson: "{}",
243
+ createdAt: 0,
244
+ updatedAt: 0,
245
+ };
246
+
247
+ const req = makeRequest({
248
+ appId: "app1",
249
+ channelId: "C123",
250
+ message: "Check this out!",
251
+ });
252
+ const res = await handleShareToSlackChannel(req);
253
+ expect(res.status).toBe(200);
254
+ const json = (await res.json()) as {
255
+ ok: boolean;
256
+ ts: string;
257
+ channel: string;
258
+ };
259
+ expect(json.ok).toBe(true);
260
+ expect(json.ts).toBe("1234567890.123456");
261
+ expect(json.channel).toBe("C123");
262
+ });
263
+ });
@@ -21,11 +21,13 @@ let secureKeys: Record<string, string | undefined> = {
21
21
  };
22
22
 
23
23
  let configState: {
24
+ twilio?: { accountSid?: string };
24
25
  sms?: {
25
26
  phoneNumber?: string;
26
27
  assistantPhoneNumbers?: Record<string, string>;
27
28
  };
28
29
  } = {
30
+ twilio: { accountSid: "AC1234567890" },
29
31
  sms: {},
30
32
  };
31
33
 
@@ -71,7 +73,7 @@ describe("smsMessagingProvider", () => {
71
73
  "credential:twilio:account_sid": "AC1234567890",
72
74
  "credential:twilio:auth_token": "auth-token",
73
75
  };
74
- configState = { sms: {} };
76
+ configState = { twilio: { accountSid: "AC1234567890" }, sms: {} };
75
77
  delete process.env.TWILIO_PHONE_NUMBER;
76
78
  delete process.env.GATEWAY_INTERNAL_BASE_URL;
77
79
  delete process.env.GATEWAY_PORT;
@@ -91,7 +91,7 @@ import { getResolver } from "../approvals/guardian-request-resolvers.js";
91
91
  import { findContactChannel } from "../contacts/contact-store.js";
92
92
  import {
93
93
  createGuardianBinding,
94
- upsertMember,
94
+ upsertContactChannel,
95
95
  } from "../contacts/contacts-write.js";
96
96
  import { createApprovalRequest } from "../memory/channel-guardian-store.js";
97
97
  import { getDb, initializeDb, resetDb } from "../memory/db.js";
@@ -174,7 +174,7 @@ describe("trusted contact lifecycle notification signals", () => {
174
174
  guardianPrincipalId: "guardian-user-789",
175
175
  verifiedVia: "test",
176
176
  });
177
- upsertMember({
177
+ upsertContactChannel({
178
178
  sourceChannel: "telegram",
179
179
  externalUserId: "guardian-user-789",
180
180
  externalChatId: "guardian-chat-789",
@@ -253,7 +253,7 @@ describe("trusted contact lifecycle notification signals", () => {
253
253
  guardianPrincipalId: "guardian-user-789",
254
254
  verifiedVia: "test",
255
255
  });
256
- upsertMember({
256
+ upsertContactChannel({
257
257
  sourceChannel: "telegram",
258
258
  externalUserId: "guardian-user-789",
259
259
  externalChatId: "guardian-chat-789",
@@ -328,7 +328,7 @@ describe("trusted contact lifecycle notification signals", () => {
328
328
  guardianPrincipalId: "guardian-user-789",
329
329
  verifiedVia: "test",
330
330
  });
331
- upsertMember({
331
+ upsertContactChannel({
332
332
  sourceChannel: "telegram",
333
333
  externalUserId: "guardian-user-789",
334
334
  externalChatId: "guardian-chat-789",
@@ -448,7 +448,7 @@ describe("trusted contact activated notification signal", () => {
448
448
  verifiedVia: "test",
449
449
  });
450
450
 
451
- upsertMember({
451
+ upsertContactChannel({
452
452
  sourceChannel: "telegram",
453
453
  externalUserId: "requester-user-456",
454
454
  externalChatId: "chat-123",
@@ -509,7 +509,7 @@ describe("trusted contact activated notification signal", () => {
509
509
  test("voice access_request resolver has registered handler for access_request kind", () => {
510
510
  // The access_request resolver is registered during module load. When the
511
511
  // source channel is 'voice', it should directly activate the member via
512
- // upsertMember (no verification session). This test validates the resolver
512
+ // upsertContactChannel (no verification session). This test validates the resolver
513
513
  // is registered and accessible.
514
514
  const resolver = getResolver("access_request");
515
515
  expect(resolver).toBeDefined();
@@ -549,7 +549,7 @@ describe("trusted contact activated notification signal", () => {
549
549
  );
550
550
  expect(activatedSignals.length).toBe(1);
551
551
 
552
- // Verify the member was already persisted (the signal fires after upsertMember)
552
+ // Verify the member was already persisted (the signal fires after upsertContactChannel)
553
553
  const result = findContactChannel({
554
554
  channelType: "telegram",
555
555
  externalUserId: "requester-user-456",
@@ -98,7 +98,7 @@ mock.module("../runtime/approval-message-composer.js", () => ({
98
98
  import { findContactChannel } from "../contacts/contact-store.js";
99
99
  import {
100
100
  createGuardianBinding,
101
- upsertMember,
101
+ upsertContactChannel,
102
102
  } from "../contacts/contacts-write.js";
103
103
  import { getDb, initializeDb, resetDb } from "../memory/db.js";
104
104
  import {
@@ -279,7 +279,7 @@ for (const config of CHANNEL_CONFIGS) {
279
279
  expect(challengeResult.verificationType).toBe("trusted_contact");
280
280
  }
281
281
 
282
- upsertMember({
282
+ upsertContactChannel({
283
283
  sourceChannel: config.channel,
284
284
  externalUserId: config.senderExternalUserId,
285
285
  externalChatId: config.externalChatId,
@@ -302,7 +302,7 @@ for (const config of CHANNEL_CONFIGS) {
302
302
 
303
303
  test("no cross-channel leakage between member records", () => {
304
304
  // Create a member for this channel
305
- upsertMember({
305
+ upsertContactChannel({
306
306
  sourceChannel: config.channel,
307
307
  externalUserId: config.senderExternalUserId,
308
308
  externalChatId: config.externalChatId,
@@ -47,7 +47,7 @@ import {
47
47
  import {
48
48
  createGuardianBinding,
49
49
  revokeMember,
50
- upsertMember,
50
+ upsertContactChannel,
51
51
  } from "../contacts/contacts-write.js";
52
52
  import { getDb, initializeDb, resetDb } from "../memory/db.js";
53
53
  import { resolveActorTrust } from "../runtime/actor-trust-resolver.js";
@@ -115,7 +115,7 @@ describe("trusted contact verification → member activation", () => {
115
115
  }
116
116
 
117
117
  // Simulate the member upsert that inbound-message-handler performs on success
118
- upsertMember({
118
+ upsertContactChannel({
119
119
  sourceChannel: "telegram",
120
120
  externalUserId: "requester-user-123",
121
121
  externalChatId: "requester-chat-123",
@@ -141,7 +141,7 @@ describe("trusted contact verification → member activation", () => {
141
141
  });
142
142
 
143
143
  test("resolveActorTrust surfaces member displayName when sender displayName is missing", () => {
144
- upsertMember({
144
+ upsertContactChannel({
145
145
  sourceChannel: "telegram",
146
146
  externalUserId: "requester-user-jeff",
147
147
  externalChatId: "requester-chat-jeff",
@@ -169,7 +169,7 @@ describe("trusted contact verification → member activation", () => {
169
169
  });
170
170
 
171
171
  test("resolveActorTrust prioritizes member displayName over sender displayName", () => {
172
- upsertMember({
172
+ upsertContactChannel({
173
173
  sourceChannel: "telegram",
174
174
  externalUserId: "requester-user-jeff-priority",
175
175
  externalChatId: "requester-chat-jeff-priority",
@@ -201,7 +201,7 @@ describe("trusted contact verification → member activation", () => {
201
201
  test("resolveActorTrust falls back to sender metadata when member record matches chat but not sender (group chat)", () => {
202
202
  // Simulate a group chat: member record exists for a different user who
203
203
  // shares the same externalChatId (e.g., Telegram group).
204
- upsertMember({
204
+ upsertContactChannel({
205
205
  sourceChannel: "telegram",
206
206
  externalUserId: "other-user-in-group",
207
207
  externalChatId: "shared-group-chat",
@@ -251,7 +251,7 @@ describe("trusted contact verification → member activation", () => {
251
251
  );
252
252
 
253
253
  // Simulate member upsert on verification success
254
- upsertMember({
254
+ upsertContactChannel({
255
255
  sourceChannel: "telegram",
256
256
  externalUserId: "requester-user-456",
257
257
  externalChatId: "requester-chat-456",
@@ -289,7 +289,7 @@ describe("trusted contact verification → member activation", () => {
289
289
  "chat-cross-test",
290
290
  );
291
291
 
292
- upsertMember({
292
+ upsertContactChannel({
293
293
  sourceChannel: "telegram",
294
294
  externalUserId: "user-cross-test",
295
295
  externalChatId: "chat-cross-test",
@@ -315,7 +315,7 @@ describe("trusted contact verification → member activation", () => {
315
315
 
316
316
  test("re-verification of previously revoked member reactivates them", () => {
317
317
  // Create and activate a member
318
- const member = upsertMember({
318
+ const member = upsertContactChannel({
319
319
  sourceChannel: "telegram",
320
320
  externalUserId: "user-revoked",
321
321
  externalChatId: "chat-revoked",
@@ -359,8 +359,8 @@ describe("trusted contact verification → member activation", () => {
359
359
  expect(result.verificationType).toBe("trusted_contact");
360
360
  }
361
361
 
362
- // upsertMember reactivates the existing record
363
- upsertMember({
362
+ // upsertContactChannel reactivates the existing record
363
+ upsertContactChannel({
364
364
  sourceChannel: "telegram",
365
365
  externalUserId: "user-revoked",
366
366
  externalChatId: "chat-revoked",
@@ -3,7 +3,6 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
3
3
  // ── Mocks (must come before source imports) ──────────────────────────
4
4
 
5
5
  let mockSecureKeys: Record<string, string | null> = {};
6
- let mockPhoneNumberEnv: string | undefined;
7
6
  let mockLoadConfigResult: Record<string, unknown> = {};
8
7
 
9
8
  mock.module("../util/logger.js", () => ({
@@ -17,11 +16,6 @@ mock.module("../security/secure-keys.js", () => ({
17
16
  getSecureKey: (key: string) => mockSecureKeys[key] ?? null,
18
17
  }));
19
18
 
20
- mock.module("../config/env.js", () => ({
21
- isHttpAuthDisabled: () => true,
22
- getTwilioPhoneNumberEnv: () => mockPhoneNumberEnv,
23
- }));
24
-
25
19
  mock.module("../config/loader.js", () => ({
26
20
  loadConfig: () => mockLoadConfigResult,
27
21
  }));
@@ -36,12 +30,13 @@ import { getTwilioConfig } from "../calls/twilio-config.js";
36
30
  describe("twilio-config", () => {
37
31
  beforeEach(() => {
38
32
  mockSecureKeys = {
39
- "credential:twilio:account_sid": "AC_test_sid",
40
33
  "credential:twilio:auth_token": "test_auth_token",
41
34
  };
42
- mockPhoneNumberEnv = undefined;
43
35
  mockLoadConfigResult = {
44
- sms: { phoneNumber: "+15551234567" },
36
+ twilio: {
37
+ accountSid: "AC_test_sid",
38
+ phoneNumber: "+15551234567",
39
+ },
45
40
  };
46
41
  });
47
42
 
@@ -55,7 +50,9 @@ describe("twilio-config", () => {
55
50
  });
56
51
 
57
52
  test("throws ConfigError when account SID is missing", () => {
58
- mockSecureKeys["credential:twilio:account_sid"] = null;
53
+ mockLoadConfigResult = {
54
+ twilio: { accountSid: "", phoneNumber: "+15551234567" },
55
+ };
59
56
  expect(() => getTwilioConfig()).toThrow(
60
57
  /Twilio credentials not configured/,
61
58
  );
@@ -69,36 +66,18 @@ describe("twilio-config", () => {
69
66
  });
70
67
 
71
68
  test("throws ConfigError when phone number is missing", () => {
72
- mockLoadConfigResult = { sms: {} };
73
- mockPhoneNumberEnv = undefined;
74
- mockSecureKeys["credential:twilio:phone_number"] = null;
69
+ mockLoadConfigResult = {
70
+ twilio: { accountSid: "AC_test_sid", phoneNumber: "" },
71
+ };
75
72
  expect(() => getTwilioConfig()).toThrow(
76
73
  /Twilio phone number not configured/,
77
74
  );
78
75
  });
79
76
 
80
- test("prefers TWILIO_PHONE_NUMBER env var over config phone number", () => {
81
- mockPhoneNumberEnv = "+15559999999";
82
- const config = getTwilioConfig();
83
- expect(config.phoneNumber).toBe("+15559999999");
84
- });
85
-
86
- test("falls back to secure key for phone number", () => {
87
- mockLoadConfigResult = { sms: {} };
88
- mockPhoneNumberEnv = undefined;
89
- mockSecureKeys["credential:twilio:phone_number"] = "+15558888888";
90
- const config = getTwilioConfig();
91
- expect(config.phoneNumber).toBe("+15558888888");
92
- });
93
-
94
- test("returns global phone number when assistantPhoneNumbers mapping exists", () => {
95
- mockLoadConfigResult = {
96
- sms: {
97
- phoneNumber: "+15551234567",
98
- assistantPhoneNumbers: { "ast-1": "+15557777777" },
99
- },
100
- };
101
- const config = getTwilioConfig();
102
- expect(config.phoneNumber).toBe("+15551234567");
77
+ test("throws ConfigError when twilio config section is absent", () => {
78
+ mockLoadConfigResult = {};
79
+ expect(() => getTwilioConfig()).toThrow(
80
+ /Twilio credentials not configured/,
81
+ );
103
82
  });
104
83
  });
@@ -33,6 +33,10 @@ mock.module("../util/logger.js", () => ({
33
33
  let mockAuthToken: string | undefined = "test-auth-token-secret";
34
34
  let mockAccountSid: string | undefined = "AC_test_account";
35
35
 
36
+ mock.module("../config/loader.js", () => ({
37
+ loadConfig: () => ({ twilio: { accountSid: mockAccountSid } }),
38
+ }));
39
+
36
40
  mock.module("../security/secure-keys.js", () => ({
37
41
  getSecureKey: (account: string) => {
38
42
  if (account === "credential:twilio:auth_token") return mockAuthToken;