@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,291 +1,190 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
2
-
3
- // Mock the credential metadata store so the Telegram adapter can resolve
4
- // the bot username without touching the filesystem.
5
- let mockBotUsername: string | undefined = "test_invite_bot";
6
- mock.module("../tools/credentials/metadata-store.js", () => ({
7
- getCredentialMetadata: (service: string, field: string) => {
8
- if (service === "telegram" && field === "bot_token" && mockBotUsername) {
9
- return { accountInfo: mockBotUsername };
10
- }
11
- return undefined;
12
- },
13
- upsertCredentialMetadata: () => {},
14
- deleteCredentialMetadata: () => {},
15
- listCredentialMetadata: () => [],
16
- }));
1
+ /**
2
+ * Tests for channel invite adapter resolution and registry wiring.
3
+ *
4
+ * Verifies that `resolveAdapterHandle()` correctly prefers the async
5
+ * path when available and falls back to the sync path for adapters
6
+ * that only implement `resolveChannelHandle`. Also covers the
7
+ * canonical registry APIs (`createInviteAdapterRegistry`,
8
+ * `getInviteAdapterRegistry`) that replaced the deprecated shims.
9
+ */
10
+ import { describe, expect, test } from "bun:test";
17
11
 
18
12
  import {
19
- _resetRegistry,
20
13
  type ChannelInviteAdapter,
14
+ createInviteAdapterRegistry,
21
15
  getInviteAdapterRegistry,
22
- getTransport,
23
- registerTransport,
16
+ InviteAdapterRegistry,
17
+ resolveAdapterHandle,
24
18
  } from "../runtime/channel-invite-transport.js";
25
- import { telegramInviteAdapter } from "../runtime/channel-invite-transports/telegram.js";
26
-
27
- describe("channel-invite-transport", () => {
28
- beforeEach(() => {
29
- _resetRegistry();
30
- mockBotUsername = "test_invite_bot";
31
- // Re-register after reset so Telegram tests work
32
- registerTransport(telegramInviteAdapter);
33
- });
34
-
35
- // =========================================================================
36
- // Registry
37
- // =========================================================================
38
-
39
- describe("registry", () => {
40
- test("returns the Telegram adapter for telegram channel", () => {
41
- const adapter = getTransport("telegram");
42
- expect(adapter).toBeDefined();
43
- expect(adapter!.channel).toBe("telegram");
44
- });
45
-
46
- test("returns undefined for an unregistered channel", () => {
47
- const adapter = getTransport("sms");
48
- expect(adapter).toBeUndefined();
49
- });
50
19
 
51
- test("overwrites a previously registered adapter for the same channel", () => {
52
- const custom: ChannelInviteAdapter = {
53
- channel: "telegram",
54
- buildShareLink: () => ({ url: "custom", displayText: "custom" }),
55
- extractInboundToken: () => undefined,
56
- };
57
- registerTransport(custom);
58
- const adapter = getTransport("telegram");
59
- expect(
60
- adapter!.buildShareLink!({
61
- rawToken: "x",
62
- sourceChannel: "telegram",
63
- }).url,
64
- ).toBe("custom");
65
- });
20
+ describe("resolveAdapterHandle", () => {
21
+ test("returns sync handle when only resolveChannelHandle is defined", async () => {
22
+ const adapter: ChannelInviteAdapter = {
23
+ channel: "telegram",
24
+ resolveChannelHandle: () => "@mybot",
25
+ };
66
26
 
67
- test("_resetRegistry clears all adapters", () => {
68
- _resetRegistry();
69
- expect(getTransport("telegram")).toBeUndefined();
70
- });
27
+ const handle = await resolveAdapterHandle(adapter);
28
+ expect(handle).toBe("@mybot");
29
+ });
71
30
 
72
- test("getInviteAdapterRegistry returns the singleton registry", () => {
73
- const registry = getInviteAdapterRegistry();
74
- expect(registry.get("telegram")).toBeDefined();
75
- });
31
+ test("returns undefined when sync resolveChannelHandle returns undefined", async () => {
32
+ const adapter: ChannelInviteAdapter = {
33
+ channel: "email",
34
+ resolveChannelHandle: () => undefined,
35
+ };
76
36
 
77
- test("registry.getAll returns all registered adapters", () => {
78
- const registry = getInviteAdapterRegistry();
79
- const all = registry.getAll();
80
- expect(all.length).toBeGreaterThanOrEqual(1);
81
- expect(all.some((a) => a.channel === "telegram")).toBe(true);
82
- });
37
+ const handle = await resolveAdapterHandle(adapter);
38
+ expect(handle).toBeUndefined();
83
39
  });
84
40
 
85
- // =========================================================================
86
- // Telegram adapter buildShareLink
87
- // =========================================================================
88
-
89
- describe("telegram buildShareLink", () => {
90
- test("produces a valid Telegram deep link", () => {
91
- const result = telegramInviteAdapter.buildShareLink!({
92
- rawToken: "abc123_test-token",
93
- sourceChannel: "telegram",
94
- });
41
+ test("returns undefined when adapter has no handle resolution methods", async () => {
42
+ const adapter: ChannelInviteAdapter = {
43
+ channel: "slack",
44
+ };
95
45
 
96
- expect(result.url).toBe(
97
- "https://t.me/test_invite_bot?start=iv_abc123_test-token",
98
- );
99
- expect(result.displayText).toContain(
100
- "https://t.me/test_invite_bot?start=iv_abc123_test-token",
101
- );
102
- });
46
+ const handle = await resolveAdapterHandle(adapter);
47
+ expect(handle).toBeUndefined();
48
+ });
103
49
 
104
- test("deep link is deterministic for the same token", () => {
105
- const a = telegramInviteAdapter.buildShareLink!({
106
- rawToken: "tok1",
107
- sourceChannel: "telegram",
108
- });
109
- const b = telegramInviteAdapter.buildShareLink!({
110
- rawToken: "tok1",
111
- sourceChannel: "telegram",
112
- });
113
- expect(a.url).toBe(b.url);
114
- expect(a.displayText).toBe(b.displayText);
115
- });
50
+ test("returns async handle when only resolveChannelHandleAsync is defined", async () => {
51
+ const adapter: ChannelInviteAdapter = {
52
+ channel: "email",
53
+ resolveChannelHandleAsync: async () => "hello@assistant.agentmail.to",
54
+ };
116
55
 
117
- test("uses the configured bot username", () => {
118
- mockBotUsername = "my_custom_bot";
119
- const result = telegramInviteAdapter.buildShareLink!({
120
- rawToken: "token",
121
- sourceChannel: "telegram",
122
- });
123
- expect(result.url).toBe("https://t.me/my_custom_bot?start=iv_token");
124
- });
56
+ const handle = await resolveAdapterHandle(adapter);
57
+ expect(handle).toBe("hello@assistant.agentmail.to");
58
+ });
125
59
 
126
- test("throws when bot username is not configured", () => {
127
- mockBotUsername = undefined;
128
- // Also clear the env var to ensure no fallback
129
- const prev = process.env.TELEGRAM_BOT_USERNAME;
130
- delete process.env.TELEGRAM_BOT_USERNAME;
131
- try {
132
- expect(() =>
133
- telegramInviteAdapter.buildShareLink!({
134
- rawToken: "token",
135
- sourceChannel: "telegram",
136
- }),
137
- ).toThrow("bot username is not configured");
138
- } finally {
139
- if (prev !== undefined) process.env.TELEGRAM_BOT_USERNAME = prev;
140
- }
141
- });
60
+ test("prefers async over sync when both are defined", async () => {
61
+ const adapter: ChannelInviteAdapter = {
62
+ channel: "email",
63
+ resolveChannelHandle: () => "sync-handle",
64
+ resolveChannelHandleAsync: async () => "async-handle",
65
+ };
142
66
 
143
- test("falls back to TELEGRAM_BOT_USERNAME env var", () => {
144
- mockBotUsername = undefined;
145
- const prev = process.env.TELEGRAM_BOT_USERNAME;
146
- process.env.TELEGRAM_BOT_USERNAME = "env_bot";
147
- try {
148
- const result = telegramInviteAdapter.buildShareLink!({
149
- rawToken: "token",
150
- sourceChannel: "telegram",
151
- });
152
- expect(result.url).toBe("https://t.me/env_bot?start=iv_token");
153
- } finally {
154
- if (prev !== undefined) {
155
- process.env.TELEGRAM_BOT_USERNAME = prev;
156
- } else {
157
- delete process.env.TELEGRAM_BOT_USERNAME;
158
- }
159
- }
160
- });
67
+ const handle = await resolveAdapterHandle(adapter);
68
+ expect(handle).toBe("async-handle");
161
69
  });
162
70
 
163
- // =========================================================================
164
- // Telegram adapter extractInboundToken
165
- // =========================================================================
71
+ test("returns undefined when async resolveChannelHandleAsync returns undefined", async () => {
72
+ const adapter: ChannelInviteAdapter = {
73
+ channel: "whatsapp",
74
+ resolveChannelHandleAsync: async () => undefined,
75
+ };
166
76
 
167
- describe("telegram extractInboundToken", () => {
168
- test("extracts token from structured commandIntent", () => {
169
- const token = telegramInviteAdapter.extractInboundToken!({
170
- commandIntent: { type: "start", payload: "iv_abc123" },
171
- content: "/start iv_abc123",
172
- });
173
- expect(token).toBe("abc123");
174
- });
175
-
176
- test("extracts base64url token from commandIntent", () => {
177
- const token = telegramInviteAdapter.extractInboundToken!({
178
- commandIntent: { type: "start", payload: "iv_YWJjMTIz-_test" },
179
- content: "/start iv_YWJjMTIz-_test",
180
- });
181
- expect(token).toBe("YWJjMTIz-_test");
182
- });
77
+ const handle = await resolveAdapterHandle(adapter);
78
+ expect(handle).toBeUndefined();
79
+ });
183
80
 
184
- test("returns undefined when commandIntent has no payload", () => {
185
- const token = telegramInviteAdapter.extractInboundToken!({
186
- commandIntent: { type: "start" },
187
- content: "/start",
188
- });
189
- expect(token).toBeUndefined();
190
- });
81
+ test("returns undefined when async resolveChannelHandleAsync rejects", async () => {
82
+ const adapter: ChannelInviteAdapter = {
83
+ channel: "email",
84
+ resolveChannelHandleAsync: async () => {
85
+ throw new Error("transient API failure");
86
+ },
87
+ };
191
88
 
192
- test("returns undefined when commandIntent payload has wrong prefix (gv_)", () => {
193
- const token = telegramInviteAdapter.extractInboundToken!({
194
- commandIntent: { type: "start", payload: "gv_abc123" },
195
- content: "/start gv_abc123",
196
- });
197
- expect(token).toBeUndefined();
198
- });
89
+ const handle = await resolveAdapterHandle(adapter);
90
+ expect(handle).toBeUndefined();
91
+ });
199
92
 
200
- test("returns undefined when commandIntent payload has no prefix", () => {
201
- const token = telegramInviteAdapter.extractInboundToken!({
202
- commandIntent: { type: "start", payload: "abc123" },
203
- content: "/start abc123",
204
- });
205
- expect(token).toBeUndefined();
206
- });
93
+ test("returns undefined when sync resolveChannelHandle throws", async () => {
94
+ const adapter: ChannelInviteAdapter = {
95
+ channel: "telegram",
96
+ resolveChannelHandle: () => {
97
+ throw new Error("credential lookup failed");
98
+ },
99
+ };
207
100
 
208
- test("returns undefined when commandIntent type is not start", () => {
209
- const token = telegramInviteAdapter.extractInboundToken!({
210
- commandIntent: { type: "help", payload: "iv_abc123" },
211
- content: "/help iv_abc123",
212
- });
213
- expect(token).toBeUndefined();
214
- });
101
+ const handle = await resolveAdapterHandle(adapter);
102
+ expect(handle).toBeUndefined();
103
+ });
104
+ });
215
105
 
216
- test("returns undefined when commandIntent payload is iv_ with empty token", () => {
217
- const token = telegramInviteAdapter.extractInboundToken!({
218
- commandIntent: { type: "start", payload: "iv_" },
219
- content: "/start iv_",
220
- });
221
- expect(token).toBeUndefined();
222
- });
106
+ // ---------------------------------------------------------------------------
107
+ // Registry APIs
108
+ // ---------------------------------------------------------------------------
109
+
110
+ describe("createInviteAdapterRegistry", () => {
111
+ const builtInChannels = [
112
+ "email",
113
+ "slack",
114
+ "sms",
115
+ "telegram",
116
+ "voice",
117
+ "whatsapp",
118
+ ] as const;
119
+
120
+ test("returns a registry with all built-in adapters registered", () => {
121
+ const registry = createInviteAdapterRegistry();
122
+
123
+ for (const channel of builtInChannels) {
124
+ const adapter = registry.get(channel);
125
+ expect(adapter).toBeDefined();
126
+ expect(adapter!.channel).toBe(channel);
127
+ }
128
+ });
223
129
 
224
- test("returns undefined when commandIntent payload is iv_ with whitespace-only token", () => {
225
- const token = telegramInviteAdapter.extractInboundToken!({
226
- commandIntent: { type: "start", payload: "iv_ " },
227
- content: "/start iv_ ",
228
- });
229
- expect(token).toBeUndefined();
230
- });
130
+ test("getAll returns exactly the built-in adapters", () => {
131
+ const registry = createInviteAdapterRegistry();
132
+ const all = registry.getAll();
231
133
 
232
- test("extracts token from raw content fallback", () => {
233
- const token = telegramInviteAdapter.extractInboundToken!({
234
- content: "/start iv_abc123",
235
- });
236
- expect(token).toBe("abc123");
237
- });
134
+ expect(all).toHaveLength(builtInChannels.length);
135
+ const channels = new Set(all.map((a) => a.channel));
136
+ for (const ch of builtInChannels) {
137
+ expect(channels.has(ch)).toBe(true);
138
+ }
139
+ });
238
140
 
239
- test("extracts token from raw content with extra whitespace", () => {
240
- const token = telegramInviteAdapter.extractInboundToken!({
241
- content: "/start iv_token123",
242
- });
243
- expect(token).toBe("token123");
244
- });
141
+ test("returns a new registry instance on each call", () => {
142
+ const a = createInviteAdapterRegistry();
143
+ const b = createInviteAdapterRegistry();
144
+ expect(a).not.toBe(b);
145
+ });
146
+ });
245
147
 
246
- test("returns undefined for empty content", () => {
247
- const token = telegramInviteAdapter.extractInboundToken!({
248
- content: "",
249
- });
250
- expect(token).toBeUndefined();
251
- });
148
+ describe("getInviteAdapterRegistry", () => {
149
+ test("returns the singleton registry", () => {
150
+ const registry = getInviteAdapterRegistry();
151
+ expect(registry).toBeInstanceOf(InviteAdapterRegistry);
152
+ });
252
153
 
253
- test("returns undefined for content without /start", () => {
254
- const token = telegramInviteAdapter.extractInboundToken!({
255
- content: "hello world",
256
- });
257
- expect(token).toBeUndefined();
258
- });
154
+ test("returns the same instance on repeated calls", () => {
155
+ const first = getInviteAdapterRegistry();
156
+ const second = getInviteAdapterRegistry();
157
+ expect(first).toBe(second);
158
+ });
159
+ });
259
160
 
260
- test("returns undefined for /start without iv_ prefix in content", () => {
261
- const token = telegramInviteAdapter.extractInboundToken!({
262
- content: "/start gv_abc123",
263
- });
264
- expect(token).toBeUndefined();
265
- });
161
+ describe("InviteAdapterRegistry register / get", () => {
162
+ test("register stores and get retrieves a custom adapter", () => {
163
+ const registry = new InviteAdapterRegistry();
164
+ const custom: ChannelInviteAdapter = {
165
+ channel: "telegram",
166
+ resolveChannelHandle: () => "@custom",
167
+ };
266
168
 
267
- test("returns undefined for malformed /start with only iv_ in content", () => {
268
- const token = telegramInviteAdapter.extractInboundToken!({
269
- content: "/start iv_",
270
- });
271
- expect(token).toBeUndefined();
272
- });
169
+ registry.register(custom);
170
+ expect(registry.get("telegram")).toBe(custom);
171
+ });
273
172
 
274
- test("prefers commandIntent over raw content", () => {
275
- const token = telegramInviteAdapter.extractInboundToken!({
276
- commandIntent: { type: "start", payload: "iv_from_intent" },
277
- content: "/start iv_from_content",
278
- });
279
- expect(token).toBe("from_intent");
280
- });
173
+ test("register overwrites a previously registered adapter", () => {
174
+ const registry = new InviteAdapterRegistry();
175
+ const first: ChannelInviteAdapter = { channel: "email" };
176
+ const second: ChannelInviteAdapter = {
177
+ channel: "email",
178
+ resolveChannelHandle: () => "new@example.com",
179
+ };
180
+
181
+ registry.register(first);
182
+ registry.register(second);
183
+ expect(registry.get("email")).toBe(second);
184
+ });
281
185
 
282
- test("returns undefined when commandIntent rejects, even if content has token", () => {
283
- // commandIntent present but payload has wrong prefix
284
- const token = telegramInviteAdapter.extractInboundToken!({
285
- commandIntent: { type: "start", payload: "gv_abc123" },
286
- content: "/start iv_valid_token",
287
- });
288
- expect(token).toBeUndefined();
289
- });
186
+ test("get returns undefined for an unregistered channel", () => {
187
+ const registry = new InviteAdapterRegistry();
188
+ expect(registry.get("slack")).toBeUndefined();
290
189
  });
291
190
  });