@vellumai/assistant 0.3.15 → 0.3.18

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 (306) hide show
  1. package/ARCHITECTURE.md +211 -12
  2. package/Dockerfile +1 -1
  3. package/README.md +11 -5
  4. package/docs/architecture/http-token-refresh.md +274 -0
  5. package/docs/architecture/memory.md +5 -4
  6. package/docs/architecture/scheduling.md +4 -88
  7. package/docs/runbook-trusted-contacts.md +283 -0
  8. package/docs/trusted-contact-access.md +247 -0
  9. package/package.json +1 -1
  10. package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
  11. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
  12. package/src/__tests__/access-request-decision.test.ts +328 -0
  13. package/src/__tests__/asset-materialize-tool.test.ts +7 -7
  14. package/src/__tests__/asset-search-tool.test.ts +15 -15
  15. package/src/__tests__/attachments-store.test.ts +13 -13
  16. package/src/__tests__/call-controller.test.ts +150 -4
  17. package/src/__tests__/call-conversation-messages.test.ts +2 -2
  18. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  19. package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +108 -12
  21. package/src/__tests__/channel-guardian.test.ts +19 -15
  22. package/src/__tests__/checker.test.ts +103 -48
  23. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
  24. package/src/__tests__/config-watcher.test.ts +356 -0
  25. package/src/__tests__/conversation-pairing.test.ts +127 -27
  26. package/src/__tests__/conversation-store.test.ts +36 -36
  27. package/src/__tests__/date-context.test.ts +179 -1
  28. package/src/__tests__/db-migration-rollback.test.ts +4 -7
  29. package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
  30. package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
  31. package/src/__tests__/gateway-only-guard.test.ts +188 -0
  32. package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
  33. package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
  34. package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
  35. package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
  36. package/src/__tests__/guardian-action-late-reply.test.ts +425 -0
  37. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
  38. package/src/__tests__/guardian-action-store.test.ts +182 -0
  39. package/src/__tests__/guardian-action-sweep.test.ts +9 -9
  40. package/src/__tests__/guardian-dispatch.test.ts +120 -0
  41. package/src/__tests__/guardian-outbound-http.test.ts +194 -2
  42. package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
  43. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
  44. package/src/__tests__/handlers-telegram-config.test.ts +6 -6
  45. package/src/__tests__/hooks-runner.test.ts +13 -4
  46. package/src/__tests__/ingress-routes-http.test.ts +443 -0
  47. package/src/__tests__/intent-routing.test.ts +14 -0
  48. package/src/__tests__/ipc-snapshot.test.ts +23 -5
  49. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  50. package/src/__tests__/memory-regressions.test.ts +16 -12
  51. package/src/__tests__/non-member-access-request.test.ts +281 -0
  52. package/src/__tests__/notification-broadcaster.test.ts +115 -4
  53. package/src/__tests__/notification-decision-strategy.test.ts +138 -1
  54. package/src/__tests__/notification-deep-link.test.ts +44 -1
  55. package/src/__tests__/notification-guardian-path.test.ts +157 -0
  56. package/src/__tests__/notification-routing-intent.test.ts +11 -1
  57. package/src/__tests__/notification-thread-candidate-validation.test.ts +215 -0
  58. package/src/__tests__/notification-thread-candidates.test.ts +166 -0
  59. package/src/__tests__/recording-intent.test.ts +1 -0
  60. package/src/__tests__/recording-state-machine.test.ts +328 -17
  61. package/src/__tests__/registry.test.ts +17 -8
  62. package/src/__tests__/relay-server.test.ts +105 -0
  63. package/src/__tests__/reminder.test.ts +13 -0
  64. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
  65. package/src/__tests__/scheduler-recurrence.test.ts +50 -0
  66. package/src/__tests__/server-history-render.test.ts +8 -8
  67. package/src/__tests__/session-agent-loop.test.ts +1 -0
  68. package/src/__tests__/session-runtime-assembly.test.ts +49 -0
  69. package/src/__tests__/session-skill-tools.test.ts +1 -0
  70. package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
  71. package/src/__tests__/slack-channel-config.test.ts +230 -0
  72. package/src/__tests__/subagent-manager-notify.test.ts +4 -4
  73. package/src/__tests__/swarm-session-integration.test.ts +2 -2
  74. package/src/__tests__/system-prompt.test.ts +43 -0
  75. package/src/__tests__/task-management-tools.test.ts +3 -3
  76. package/src/__tests__/task-tools.test.ts +3 -3
  77. package/src/__tests__/trust-store.test.ts +38 -22
  78. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +489 -0
  79. package/src/__tests__/trusted-contact-multichannel.test.ts +405 -0
  80. package/src/__tests__/trusted-contact-verification.test.ts +360 -0
  81. package/src/__tests__/update-bulletin-format.test.ts +119 -0
  82. package/src/__tests__/update-bulletin-state.test.ts +129 -0
  83. package/src/__tests__/update-bulletin.test.ts +323 -0
  84. package/src/__tests__/update-template-contract.test.ts +24 -0
  85. package/src/__tests__/voice-session-bridge.test.ts +109 -9
  86. package/src/agent/loop.ts +2 -2
  87. package/src/amazon/client.ts +2 -3
  88. package/src/calls/call-controller.ts +241 -39
  89. package/src/calls/call-conversation-messages.ts +2 -2
  90. package/src/calls/call-domain.ts +10 -3
  91. package/src/calls/call-pointer-messages.ts +17 -5
  92. package/src/calls/guardian-action-sweep.ts +77 -36
  93. package/src/calls/guardian-dispatch.ts +8 -0
  94. package/src/calls/relay-server.ts +51 -12
  95. package/src/calls/twilio-routes.ts +3 -1
  96. package/src/calls/types.ts +1 -1
  97. package/src/calls/voice-session-bridge.ts +8 -6
  98. package/src/cli/core-commands.ts +43 -3
  99. package/src/cli/map.ts +8 -5
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
  101. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  102. package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
  103. package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
  104. package/src/config/computer-use-prompt.ts +1 -0
  105. package/src/config/core-schema.ts +16 -0
  106. package/src/config/env-registry.ts +1 -0
  107. package/src/config/env.ts +16 -1
  108. package/src/config/memory-schema.ts +5 -0
  109. package/src/config/schema.ts +4 -0
  110. package/src/config/system-prompt.ts +69 -2
  111. package/src/config/templates/BOOTSTRAP.md +1 -1
  112. package/src/config/templates/IDENTITY.md +8 -4
  113. package/src/config/templates/SOUL.md +14 -0
  114. package/src/config/templates/UPDATES.md +15 -0
  115. package/src/config/templates/USER.md +5 -1
  116. package/src/config/types.ts +1 -0
  117. package/src/config/update-bulletin-format.ts +54 -0
  118. package/src/config/update-bulletin-state.ts +49 -0
  119. package/src/config/update-bulletin-template-path.ts +6 -0
  120. package/src/config/update-bulletin.ts +97 -0
  121. package/src/config/vellum-skills/catalog.json +6 -0
  122. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  123. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
  124. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  125. package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
  126. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  127. package/src/context/window-manager.ts +43 -3
  128. package/src/daemon/config-watcher.ts +4 -2
  129. package/src/daemon/connection-policy.ts +21 -1
  130. package/src/daemon/daemon-control.ts +219 -8
  131. package/src/daemon/date-context.ts +174 -1
  132. package/src/daemon/guardian-action-generators.ts +175 -0
  133. package/src/daemon/guardian-verification-intent.ts +120 -0
  134. package/src/daemon/handlers/apps.ts +1 -3
  135. package/src/daemon/handlers/config-channels.ts +2 -2
  136. package/src/daemon/handlers/config-heartbeat.ts +1 -1
  137. package/src/daemon/handlers/config-inbox.ts +55 -159
  138. package/src/daemon/handlers/config-ingress.ts +1 -1
  139. package/src/daemon/handlers/config-integrations.ts +1 -1
  140. package/src/daemon/handlers/config-platform.ts +1 -1
  141. package/src/daemon/handlers/config-scheduling.ts +2 -2
  142. package/src/daemon/handlers/config-slack-channel.ts +190 -0
  143. package/src/daemon/handlers/config-telegram.ts +1 -1
  144. package/src/daemon/handlers/config-twilio.ts +1 -1
  145. package/src/daemon/handlers/config-voice.ts +100 -0
  146. package/src/daemon/handlers/config.ts +3 -0
  147. package/src/daemon/handlers/identity.ts +45 -25
  148. package/src/daemon/handlers/misc.ts +83 -5
  149. package/src/daemon/handlers/navigate-settings.ts +27 -0
  150. package/src/daemon/handlers/recording.ts +270 -144
  151. package/src/daemon/handlers/sessions.ts +100 -17
  152. package/src/daemon/handlers/subagents.ts +3 -3
  153. package/src/daemon/handlers/work-items.ts +10 -7
  154. package/src/daemon/ipc-contract/integrations.ts +9 -1
  155. package/src/daemon/ipc-contract/messages.ts +4 -0
  156. package/src/daemon/ipc-contract/sessions.ts +1 -1
  157. package/src/daemon/ipc-contract/settings.ts +26 -0
  158. package/src/daemon/ipc-contract/shared.ts +2 -0
  159. package/src/daemon/ipc-contract/work-items.ts +1 -7
  160. package/src/daemon/ipc-contract/workspace.ts +12 -1
  161. package/src/daemon/ipc-contract-inventory.json +6 -1
  162. package/src/daemon/ipc-contract.ts +5 -1
  163. package/src/daemon/lifecycle.ts +314 -266
  164. package/src/daemon/recording-intent.ts +0 -41
  165. package/src/daemon/response-tier.ts +2 -2
  166. package/src/daemon/server.ts +31 -9
  167. package/src/daemon/session-agent-loop-handlers.ts +34 -9
  168. package/src/daemon/session-agent-loop.ts +15 -8
  169. package/src/daemon/session-history.ts +3 -2
  170. package/src/daemon/session-media-retry.ts +3 -0
  171. package/src/daemon/session-messaging.ts +38 -4
  172. package/src/daemon/session-notifiers.ts +2 -2
  173. package/src/daemon/session-process.ts +546 -59
  174. package/src/daemon/session-queue-manager.ts +2 -0
  175. package/src/daemon/session-runtime-assembly.ts +39 -0
  176. package/src/daemon/session-skill-tools.ts +13 -4
  177. package/src/daemon/session-tool-setup.ts +5 -6
  178. package/src/daemon/session.ts +19 -8
  179. package/src/daemon/tls-certs.ts +60 -13
  180. package/src/daemon/tool-side-effects.ts +13 -5
  181. package/src/gallery/default-gallery.ts +32 -9
  182. package/src/influencer/client.ts +2 -1
  183. package/src/memory/channel-delivery-store.ts +35 -567
  184. package/src/memory/channel-guardian-store.ts +63 -1317
  185. package/src/memory/conflict-store.ts +4 -4
  186. package/src/memory/conversation-attention-store.ts +0 -3
  187. package/src/memory/conversation-crud.ts +668 -0
  188. package/src/memory/conversation-queries.ts +361 -0
  189. package/src/memory/conversation-store.ts +44 -983
  190. package/src/memory/db-connection.ts +3 -0
  191. package/src/memory/db-init.ts +33 -0
  192. package/src/memory/delivery-channels.ts +175 -0
  193. package/src/memory/delivery-crud.ts +211 -0
  194. package/src/memory/delivery-status.ts +199 -0
  195. package/src/memory/embedding-backend.ts +70 -4
  196. package/src/memory/embedding-local.ts +12 -2
  197. package/src/memory/entity-extractor.ts +3 -8
  198. package/src/memory/fts-reconciler.ts +136 -0
  199. package/src/memory/guardian-action-store.ts +418 -5
  200. package/src/memory/guardian-approvals.ts +569 -0
  201. package/src/memory/guardian-bindings.ts +130 -0
  202. package/src/memory/guardian-rate-limits.ts +196 -0
  203. package/src/memory/guardian-verification.ts +521 -0
  204. package/src/memory/job-handlers/index-maintenance.ts +2 -1
  205. package/src/memory/job-utils.ts +8 -5
  206. package/src/memory/jobs-store.ts +66 -6
  207. package/src/memory/jobs-worker.ts +23 -1
  208. package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
  209. package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
  210. package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
  211. package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +15 -0
  212. package/src/memory/migrations/032-notification-delivery-thread-decision.ts +20 -0
  213. package/src/memory/migrations/100-core-tables.ts +1 -1
  214. package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
  215. package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
  216. package/src/memory/migrations/112-assistant-inbox.ts +1 -1
  217. package/src/memory/migrations/113-late-migrations.ts +1 -1
  218. package/src/memory/migrations/116-messages-fts.ts +13 -0
  219. package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
  220. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
  221. package/src/memory/migrations/index.ts +10 -3
  222. package/src/memory/migrations/validate-migration-state.ts +114 -15
  223. package/src/memory/qdrant-circuit-breaker.ts +105 -0
  224. package/src/memory/retriever.ts +46 -13
  225. package/src/memory/schema-migration.ts +4 -0
  226. package/src/memory/schema.ts +31 -8
  227. package/src/memory/search/semantic.ts +8 -90
  228. package/src/notifications/README.md +159 -18
  229. package/src/notifications/broadcaster.ts +69 -33
  230. package/src/notifications/conversation-pairing.ts +99 -21
  231. package/src/notifications/decision-engine.ts +176 -8
  232. package/src/notifications/deliveries-store.ts +39 -8
  233. package/src/notifications/emit-signal.ts +1 -0
  234. package/src/notifications/preferences-store.ts +7 -7
  235. package/src/notifications/thread-candidates.ts +269 -0
  236. package/src/notifications/types.ts +19 -0
  237. package/src/permissions/checker.ts +1 -16
  238. package/src/permissions/defaults.ts +25 -5
  239. package/src/permissions/prompter.ts +17 -0
  240. package/src/permissions/trust-store.ts +2 -0
  241. package/src/providers/failover.ts +19 -0
  242. package/src/providers/registry.ts +46 -1
  243. package/src/runtime/approval-message-composer.ts +1 -1
  244. package/src/runtime/channel-guardian-service.ts +15 -3
  245. package/src/runtime/channel-retry-sweep.ts +7 -2
  246. package/src/runtime/guardian-action-conversation-turn.ts +85 -0
  247. package/src/runtime/guardian-action-followup-executor.ts +301 -0
  248. package/src/runtime/guardian-action-message-composer.ts +245 -0
  249. package/src/runtime/guardian-outbound-actions.ts +26 -6
  250. package/src/runtime/guardian-verification-templates.ts +15 -9
  251. package/src/runtime/http-errors.ts +93 -0
  252. package/src/runtime/http-server.ts +133 -44
  253. package/src/runtime/http-types.ts +53 -0
  254. package/src/runtime/ingress-service.ts +237 -0
  255. package/src/runtime/middleware/error-handler.ts +4 -3
  256. package/src/runtime/middleware/rate-limiter.ts +160 -0
  257. package/src/runtime/middleware/request-logger.ts +71 -0
  258. package/src/runtime/middleware/twilio-validation.ts +7 -6
  259. package/src/runtime/pending-interactions.ts +12 -0
  260. package/src/runtime/routes/access-request-decision.ts +215 -0
  261. package/src/runtime/routes/app-routes.ts +25 -18
  262. package/src/runtime/routes/approval-routes.ts +18 -47
  263. package/src/runtime/routes/attachment-routes.ts +15 -41
  264. package/src/runtime/routes/call-routes.ts +20 -20
  265. package/src/runtime/routes/channel-delivery-routes.ts +6 -5
  266. package/src/runtime/routes/contact-routes.ts +4 -9
  267. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  268. package/src/runtime/routes/conversation-routes.ts +26 -57
  269. package/src/runtime/routes/debug-routes.ts +71 -0
  270. package/src/runtime/routes/events-routes.ts +3 -2
  271. package/src/runtime/routes/guardian-approval-interception.ts +221 -0
  272. package/src/runtime/routes/identity-routes.ts +14 -10
  273. package/src/runtime/routes/inbound-conversation.ts +3 -2
  274. package/src/runtime/routes/inbound-message-handler.ts +527 -62
  275. package/src/runtime/routes/ingress-routes.ts +174 -0
  276. package/src/runtime/routes/integration-routes.ts +78 -16
  277. package/src/runtime/routes/pairing-routes.ts +11 -10
  278. package/src/runtime/routes/secret-routes.ts +10 -18
  279. package/src/runtime/verification-rate-limiter.ts +83 -0
  280. package/src/schedule/schedule-store.ts +13 -1
  281. package/src/schedule/scheduler.ts +1 -1
  282. package/src/security/secret-ingress.ts +5 -2
  283. package/src/security/secret-scanner.ts +72 -6
  284. package/src/subagent/manager.ts +6 -4
  285. package/src/swarm/plan-validator.ts +4 -1
  286. package/src/tasks/task-runner.ts +3 -1
  287. package/src/tools/browser/api-map.ts +9 -6
  288. package/src/tools/calls/call-start.ts +20 -0
  289. package/src/tools/executor.ts +50 -568
  290. package/src/tools/permission-checker.ts +271 -0
  291. package/src/tools/registry.ts +14 -6
  292. package/src/tools/reminder/reminder-store.ts +7 -7
  293. package/src/tools/reminder/reminder.ts +6 -3
  294. package/src/tools/secret-detection-handler.ts +301 -0
  295. package/src/tools/subagent/message.ts +1 -1
  296. package/src/tools/system/voice-config.ts +62 -0
  297. package/src/tools/tasks/index.ts +3 -3
  298. package/src/tools/tasks/work-item-list.ts +3 -3
  299. package/src/tools/tasks/work-item-update.ts +4 -5
  300. package/src/tools/tool-approval-handler.ts +192 -0
  301. package/src/tools/tool-manifest.ts +2 -0
  302. package/src/version.ts +29 -2
  303. package/src/watcher/watcher-store.ts +9 -9
  304. package/src/work-items/work-item-runner.ts +9 -6
  305. /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
  306. /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
@@ -0,0 +1,405 @@
1
+ /**
2
+ * Tests verifying the trusted contact flow is channel-agnostic.
3
+ *
4
+ * The access request -> guardian notification -> verification -> activation
5
+ * flow should work identically across Telegram, SMS, and voice channels.
6
+ * These tests confirm no Telegram-specific assumptions leaked into the
7
+ * trusted contact code paths.
8
+ */
9
+ import { mkdtempSync, rmSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+
13
+ import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Test isolation: in-memory SQLite via temp directory
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const testDir = mkdtempSync(join(tmpdir(), 'trusted-contact-multichannel-'));
20
+
21
+ mock.module('../util/platform.js', () => ({
22
+ getRootDir: () => testDir,
23
+ getDataDir: () => testDir,
24
+ isMacOS: () => process.platform === 'darwin',
25
+ isLinux: () => process.platform === 'linux',
26
+ isWindows: () => process.platform === 'win32',
27
+ getSocketPath: () => join(testDir, 'test.sock'),
28
+ getPidPath: () => join(testDir, 'test.pid'),
29
+ getDbPath: () => join(testDir, 'test.db'),
30
+ getLogPath: () => join(testDir, 'test.log'),
31
+ ensureDataDir: () => {},
32
+ normalizeAssistantId: (id: string) => id === 'self' ? 'self' : id,
33
+ readHttpToken: () => 'test-bearer-token',
34
+ }));
35
+
36
+ mock.module('../util/logger.js', () => ({
37
+ getLogger: () => new Proxy({} as Record<string, unknown>, {
38
+ get: () => () => {},
39
+ }),
40
+ }));
41
+
42
+ mock.module('../security/secret-ingress.js', () => ({
43
+ checkIngressForSecrets: () => ({ blocked: false }),
44
+ }));
45
+
46
+ mock.module('../config/env.js', () => ({
47
+ getGatewayInternalBaseUrl: () => 'http://127.0.0.1:7830',
48
+ }));
49
+
50
+ const emitSignalCalls: Array<Record<string, unknown>> = [];
51
+ mock.module('../notifications/emit-signal.js', () => ({
52
+ emitNotificationSignal: async (params: Record<string, unknown>) => {
53
+ emitSignalCalls.push(params);
54
+ return {
55
+ signalId: 'mock-signal-id',
56
+ deduplicated: false,
57
+ dispatched: true,
58
+ reason: 'mock',
59
+ deliveryResults: [],
60
+ };
61
+ },
62
+ }));
63
+
64
+ const deliverReplyCalls: Array<{ url: string; payload: Record<string, unknown> }> = [];
65
+ mock.module('../runtime/gateway-client.js', () => ({
66
+ deliverChannelReply: async (url: string, payload: Record<string, unknown>) => {
67
+ deliverReplyCalls.push({ url, payload });
68
+ },
69
+ }));
70
+
71
+ mock.module('../runtime/approval-message-composer.js', () => ({
72
+ composeApprovalMessage: () => 'mock approval message',
73
+ composeApprovalMessageGenerative: async () => 'mock generative message',
74
+ }));
75
+
76
+ import {
77
+ createBinding,
78
+ findPendingAccessRequestForRequester,
79
+ } from '../memory/channel-guardian-store.js';
80
+ import { getDb, initializeDb, resetDb } from '../memory/db.js';
81
+ import { findMember, upsertMember } from '../memory/ingress-member-store.js';
82
+ import {
83
+ createOutboundSession,
84
+ validateAndConsumeChallenge,
85
+ } from '../runtime/channel-guardian-service.js';
86
+ import { handleChannelInbound } from '../runtime/routes/channel-routes.js';
87
+
88
+ initializeDb();
89
+
90
+ afterAll(() => {
91
+ resetDb();
92
+ try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
93
+ });
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Helpers
97
+ // ---------------------------------------------------------------------------
98
+
99
+ const TEST_BEARER_TOKEN = 'test-token';
100
+
101
+ function resetState(): void {
102
+ const db = getDb();
103
+ db.run('DELETE FROM channel_guardian_approval_requests');
104
+ db.run('DELETE FROM channel_guardian_bindings');
105
+ db.run('DELETE FROM channel_guardian_verification_challenges');
106
+ db.run('DELETE FROM channel_guardian_rate_limits');
107
+ db.run('DELETE FROM channel_inbound_events');
108
+ db.run('DELETE FROM conversations');
109
+ db.run('DELETE FROM notification_events');
110
+ db.run('DELETE FROM assistant_ingress_members');
111
+ emitSignalCalls.length = 0;
112
+ deliverReplyCalls.length = 0;
113
+ }
114
+
115
+ interface ChannelTestConfig {
116
+ channel: 'telegram' | 'sms' | 'voice';
117
+ deliverEndpoint: string;
118
+ /** SMS/voice use phone E.164 as identifiers */
119
+ senderExternalUserId: string;
120
+ externalChatId: string;
121
+ guardianExternalUserId: string;
122
+ guardianChatId: string;
123
+ }
124
+
125
+ const CHANNEL_CONFIGS: ChannelTestConfig[] = [
126
+ {
127
+ channel: 'telegram',
128
+ deliverEndpoint: '/deliver/telegram',
129
+ senderExternalUserId: 'tg-user-456',
130
+ externalChatId: 'tg-chat-456',
131
+ guardianExternalUserId: 'tg-guardian-789',
132
+ guardianChatId: 'tg-guardian-chat-789',
133
+ },
134
+ {
135
+ channel: 'sms',
136
+ deliverEndpoint: '/deliver/sms',
137
+ senderExternalUserId: '+15551234567',
138
+ externalChatId: '+15551234567',
139
+ guardianExternalUserId: '+15559876543',
140
+ guardianChatId: '+15559876543',
141
+ },
142
+ ];
143
+
144
+ function buildInboundRequest(
145
+ config: ChannelTestConfig,
146
+ overrides: Record<string, unknown> = {},
147
+ ): Request {
148
+ const body: Record<string, unknown> = {
149
+ sourceChannel: config.channel,
150
+ interface: config.channel,
151
+ externalChatId: config.externalChatId,
152
+ externalMessageId: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
153
+ content: 'Hello, can I use this assistant?',
154
+ senderExternalUserId: config.senderExternalUserId,
155
+ senderName: 'Test Requester',
156
+ senderUsername: 'test_requester',
157
+ replyCallbackUrl: `http://localhost:7830${config.deliverEndpoint}`,
158
+ ...overrides,
159
+ };
160
+
161
+ return new Request('http://localhost:8080/channels/inbound', {
162
+ method: 'POST',
163
+ headers: {
164
+ 'Content-Type': 'application/json',
165
+ 'X-Gateway-Origin': TEST_BEARER_TOKEN,
166
+ },
167
+ body: JSON.stringify(body),
168
+ });
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Parameterized tests for each channel
173
+ // ---------------------------------------------------------------------------
174
+
175
+ for (const config of CHANNEL_CONFIGS) {
176
+ describe(`trusted contact flow on ${config.channel} channel`, () => {
177
+ beforeEach(() => {
178
+ resetState();
179
+ });
180
+
181
+ test('non-member message is denied with rejection reply', async () => {
182
+ const req = buildInboundRequest(config);
183
+ const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
184
+ const json = await resp.json() as Record<string, unknown>;
185
+
186
+ expect(json.denied).toBe(true);
187
+ expect(json.reason).toBe('not_a_member');
188
+ expect(deliverReplyCalls.length).toBe(1);
189
+ expect((deliverReplyCalls[0].payload as Record<string, unknown>).text).toContain("you haven't been approved");
190
+ });
191
+
192
+ test('guardian is notified when a non-member messages', async () => {
193
+ createBinding({
194
+ assistantId: 'self',
195
+ channel: config.channel,
196
+ guardianExternalUserId: config.guardianExternalUserId,
197
+ guardianDeliveryChatId: config.guardianChatId,
198
+ });
199
+
200
+ const req = buildInboundRequest(config);
201
+ const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
202
+ const json = await resp.json() as Record<string, unknown>;
203
+
204
+ expect(json.denied).toBe(true);
205
+
206
+ // Notification signal was emitted for the correct channel
207
+ expect(emitSignalCalls.length).toBe(1);
208
+ expect(emitSignalCalls[0].sourceEventName).toBe('ingress.access_request');
209
+ expect(emitSignalCalls[0].sourceChannel).toBe(config.channel);
210
+
211
+ const payload = emitSignalCalls[0].contextPayload as Record<string, unknown>;
212
+ expect(payload.senderExternalUserId).toBe(config.senderExternalUserId);
213
+
214
+ // Approval request was created for the correct channel
215
+ const pending = findPendingAccessRequestForRequester(
216
+ 'self',
217
+ config.channel,
218
+ config.senderExternalUserId,
219
+ 'ingress_access_request',
220
+ );
221
+ expect(pending).not.toBeNull();
222
+ expect(pending!.channel).toBe(config.channel);
223
+ });
224
+
225
+ test('verification creates active member for channel', () => {
226
+ const session = createOutboundSession({
227
+ assistantId: 'self',
228
+ channel: config.channel,
229
+ expectedExternalUserId: config.senderExternalUserId,
230
+ expectedChatId: config.externalChatId,
231
+ identityBindingStatus: 'bound',
232
+ destinationAddress: config.externalChatId,
233
+ verificationPurpose: 'trusted_contact',
234
+ });
235
+
236
+ const result = validateAndConsumeChallenge(
237
+ 'self',
238
+ config.channel,
239
+ session.secret,
240
+ config.senderExternalUserId,
241
+ config.externalChatId,
242
+ 'test_requester',
243
+ 'Test Requester',
244
+ );
245
+
246
+ expect(result.success).toBe(true);
247
+ if (result.success) {
248
+ expect(result.verificationType).toBe('trusted_contact');
249
+ }
250
+
251
+ upsertMember({
252
+ assistantId: 'self',
253
+ sourceChannel: config.channel,
254
+ externalUserId: config.senderExternalUserId,
255
+ externalChatId: config.externalChatId,
256
+ status: 'active',
257
+ policy: 'allow',
258
+ displayName: 'Test Requester',
259
+ username: 'test_requester',
260
+ });
261
+
262
+ const member = findMember({
263
+ assistantId: 'self',
264
+ sourceChannel: config.channel,
265
+ externalUserId: config.senderExternalUserId,
266
+ });
267
+
268
+ expect(member).not.toBeNull();
269
+ expect(member!.status).toBe('active');
270
+ expect(member!.policy).toBe('allow');
271
+ expect(member!.sourceChannel).toBe(config.channel);
272
+ });
273
+
274
+ test('no cross-channel leakage between member records', () => {
275
+ // Create a member for this channel
276
+ upsertMember({
277
+ assistantId: 'self',
278
+ sourceChannel: config.channel,
279
+ externalUserId: config.senderExternalUserId,
280
+ externalChatId: config.externalChatId,
281
+ status: 'active',
282
+ policy: 'allow',
283
+ });
284
+
285
+ // Should be found on this channel
286
+ const sameChanMember = findMember({
287
+ assistantId: 'self',
288
+ sourceChannel: config.channel,
289
+ externalUserId: config.senderExternalUserId,
290
+ });
291
+ expect(sameChanMember).not.toBeNull();
292
+
293
+ // Should NOT be found on a different channel
294
+ const otherChannel = config.channel === 'telegram' ? 'sms' : 'telegram';
295
+ const crossChanMember = findMember({
296
+ assistantId: 'self',
297
+ sourceChannel: otherChannel,
298
+ externalUserId: config.senderExternalUserId,
299
+ });
300
+ expect(crossChanMember).toBeNull();
301
+ });
302
+ });
303
+ }
304
+
305
+ // ---------------------------------------------------------------------------
306
+ // SMS-specific: phone E.164 identity binding
307
+ // ---------------------------------------------------------------------------
308
+
309
+ describe('SMS identity binding with E.164 phone numbers', () => {
310
+ beforeEach(() => {
311
+ resetState();
312
+ });
313
+
314
+ test('SMS verification session binds to phone E.164', () => {
315
+ const phone = '+15551234567';
316
+ const session = createOutboundSession({
317
+ assistantId: 'self',
318
+ channel: 'sms',
319
+ expectedExternalUserId: phone,
320
+ expectedPhoneE164: phone,
321
+ expectedChatId: phone,
322
+ identityBindingStatus: 'bound',
323
+ destinationAddress: phone,
324
+ verificationPurpose: 'trusted_contact',
325
+ });
326
+
327
+ // Verify with matching phone identity
328
+ const result = validateAndConsumeChallenge(
329
+ 'self', 'sms', session.secret,
330
+ phone, phone,
331
+ );
332
+ expect(result.success).toBe(true);
333
+ if (result.success) {
334
+ expect(result.verificationType).toBe('trusted_contact');
335
+ }
336
+ });
337
+
338
+ test('SMS verification rejects mismatched phone identity', () => {
339
+ const expectedPhone = '+15551234567';
340
+ const wrongPhone = '+15559999999';
341
+
342
+ const session = createOutboundSession({
343
+ assistantId: 'self',
344
+ channel: 'sms',
345
+ expectedExternalUserId: expectedPhone,
346
+ expectedPhoneE164: expectedPhone,
347
+ expectedChatId: expectedPhone,
348
+ identityBindingStatus: 'bound',
349
+ destinationAddress: expectedPhone,
350
+ });
351
+
352
+ // Try to verify with a different phone (anti-oracle: same error message)
353
+ const result = validateAndConsumeChallenge(
354
+ 'self', 'sms', session.secret,
355
+ wrongPhone, wrongPhone,
356
+ );
357
+ expect(result.success).toBe(false);
358
+ });
359
+ });
360
+
361
+ // ---------------------------------------------------------------------------
362
+ // Cross-channel: same user on different channels gets separate sessions
363
+ // ---------------------------------------------------------------------------
364
+
365
+ describe('cross-channel isolation', () => {
366
+ beforeEach(() => {
367
+ resetState();
368
+ });
369
+
370
+ test('verification sessions are scoped per channel', () => {
371
+ // Create sessions on both channels
372
+ const telegramSession = createOutboundSession({
373
+ assistantId: 'self',
374
+ channel: 'telegram',
375
+ expectedExternalUserId: 'user-123',
376
+ expectedChatId: 'chat-123',
377
+ identityBindingStatus: 'bound',
378
+ destinationAddress: 'chat-123',
379
+ });
380
+
381
+ const smsSession = createOutboundSession({
382
+ assistantId: 'self',
383
+ channel: 'sms',
384
+ expectedExternalUserId: '+15551234567',
385
+ expectedPhoneE164: '+15551234567',
386
+ expectedChatId: '+15551234567',
387
+ identityBindingStatus: 'bound',
388
+ destinationAddress: '+15551234567',
389
+ });
390
+
391
+ // Telegram code should not work on SMS channel
392
+ const wrongChannelResult = validateAndConsumeChallenge(
393
+ 'self', 'sms', telegramSession.secret,
394
+ '+15551234567', '+15551234567',
395
+ );
396
+ expect(wrongChannelResult.success).toBe(false);
397
+
398
+ // SMS code should work on SMS channel
399
+ const correctChannelResult = validateAndConsumeChallenge(
400
+ 'self', 'sms', smsSession.secret,
401
+ '+15551234567', '+15551234567',
402
+ );
403
+ expect(correctChannelResult.success).toBe(true);
404
+ });
405
+ });