@vellumai/assistant 0.3.14 → 0.3.16

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