@vellumai/assistant 0.3.15 → 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 (290) hide show
  1. package/ARCHITECTURE.md +142 -0
  2. package/Dockerfile +1 -1
  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-outbound-http.test.ts +194 -2
  40. package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
  41. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
  42. package/src/__tests__/handlers-telegram-config.test.ts +6 -6
  43. package/src/__tests__/hooks-runner.test.ts +13 -4
  44. package/src/__tests__/ingress-routes-http.test.ts +443 -0
  45. package/src/__tests__/intent-routing.test.ts +14 -0
  46. package/src/__tests__/ipc-snapshot.test.ts +2 -5
  47. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  48. package/src/__tests__/memory-regressions.test.ts +16 -12
  49. package/src/__tests__/non-member-access-request.test.ts +282 -0
  50. package/src/__tests__/notification-decision-strategy.test.ts +136 -0
  51. package/src/__tests__/notification-routing-intent.test.ts +11 -1
  52. package/src/__tests__/notification-thread-candidates.test.ts +166 -0
  53. package/src/__tests__/recording-intent.test.ts +1 -0
  54. package/src/__tests__/recording-state-machine.test.ts +328 -17
  55. package/src/__tests__/registry.test.ts +17 -8
  56. package/src/__tests__/relay-server.test.ts +105 -0
  57. package/src/__tests__/reminder.test.ts +13 -0
  58. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
  59. package/src/__tests__/scheduler-recurrence.test.ts +50 -0
  60. package/src/__tests__/server-history-render.test.ts +8 -8
  61. package/src/__tests__/session-agent-loop.test.ts +1 -0
  62. package/src/__tests__/session-runtime-assembly.test.ts +49 -0
  63. package/src/__tests__/session-skill-tools.test.ts +1 -0
  64. package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
  65. package/src/__tests__/slack-channel-config.test.ts +230 -0
  66. package/src/__tests__/subagent-manager-notify.test.ts +4 -4
  67. package/src/__tests__/swarm-session-integration.test.ts +2 -2
  68. package/src/__tests__/system-prompt.test.ts +43 -0
  69. package/src/__tests__/task-management-tools.test.ts +3 -3
  70. package/src/__tests__/task-tools.test.ts +3 -3
  71. package/src/__tests__/trust-store.test.ts +17 -1
  72. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
  73. package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
  74. package/src/__tests__/trusted-contact-verification.test.ts +360 -0
  75. package/src/__tests__/update-bulletin-format.test.ts +119 -0
  76. package/src/__tests__/update-bulletin-state.test.ts +129 -0
  77. package/src/__tests__/update-bulletin.test.ts +260 -0
  78. package/src/__tests__/update-template-contract.test.ts +29 -0
  79. package/src/agent/loop.ts +2 -2
  80. package/src/amazon/client.ts +2 -3
  81. package/src/calls/call-controller.ts +115 -34
  82. package/src/calls/call-conversation-messages.ts +2 -2
  83. package/src/calls/call-domain.ts +10 -3
  84. package/src/calls/call-pointer-messages.ts +17 -5
  85. package/src/calls/guardian-action-sweep.ts +77 -36
  86. package/src/calls/relay-server.ts +51 -12
  87. package/src/calls/twilio-routes.ts +3 -1
  88. package/src/calls/types.ts +1 -1
  89. package/src/calls/voice-session-bridge.ts +4 -4
  90. package/src/cli/core-commands.ts +3 -3
  91. package/src/cli/map.ts +8 -5
  92. package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
  93. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  94. package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
  95. package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
  96. package/src/config/computer-use-prompt.ts +1 -0
  97. package/src/config/core-schema.ts +16 -0
  98. package/src/config/env-registry.ts +1 -0
  99. package/src/config/env.ts +16 -1
  100. package/src/config/memory-schema.ts +5 -0
  101. package/src/config/schema.ts +4 -0
  102. package/src/config/system-prompt.ts +69 -2
  103. package/src/config/templates/BOOTSTRAP.md +1 -1
  104. package/src/config/templates/IDENTITY.md +8 -4
  105. package/src/config/templates/SOUL.md +14 -0
  106. package/src/config/templates/UPDATES.md +16 -0
  107. package/src/config/templates/USER.md +5 -1
  108. package/src/config/types.ts +1 -0
  109. package/src/config/update-bulletin-format.ts +52 -0
  110. package/src/config/update-bulletin-state.ts +49 -0
  111. package/src/config/update-bulletin.ts +82 -0
  112. package/src/config/vellum-skills/catalog.json +6 -0
  113. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  114. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
  115. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  116. package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
  117. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  118. package/src/context/window-manager.ts +43 -3
  119. package/src/daemon/config-watcher.ts +1 -0
  120. package/src/daemon/connection-policy.ts +21 -1
  121. package/src/daemon/daemon-control.ts +164 -7
  122. package/src/daemon/date-context.ts +174 -1
  123. package/src/daemon/guardian-action-generators.ts +175 -0
  124. package/src/daemon/guardian-verification-intent.ts +120 -0
  125. package/src/daemon/handlers/apps.ts +1 -3
  126. package/src/daemon/handlers/config-channels.ts +2 -2
  127. package/src/daemon/handlers/config-heartbeat.ts +1 -1
  128. package/src/daemon/handlers/config-inbox.ts +55 -159
  129. package/src/daemon/handlers/config-ingress.ts +1 -1
  130. package/src/daemon/handlers/config-integrations.ts +1 -1
  131. package/src/daemon/handlers/config-platform.ts +1 -1
  132. package/src/daemon/handlers/config-scheduling.ts +2 -2
  133. package/src/daemon/handlers/config-slack-channel.ts +190 -0
  134. package/src/daemon/handlers/config-telegram.ts +1 -1
  135. package/src/daemon/handlers/config-twilio.ts +1 -1
  136. package/src/daemon/handlers/config-voice.ts +100 -0
  137. package/src/daemon/handlers/config.ts +3 -0
  138. package/src/daemon/handlers/misc.ts +83 -5
  139. package/src/daemon/handlers/navigate-settings.ts +27 -0
  140. package/src/daemon/handlers/recording.ts +270 -144
  141. package/src/daemon/handlers/sessions.ts +100 -17
  142. package/src/daemon/handlers/subagents.ts +3 -3
  143. package/src/daemon/handlers/work-items.ts +10 -7
  144. package/src/daemon/ipc-contract/integrations.ts +9 -1
  145. package/src/daemon/ipc-contract/messages.ts +4 -0
  146. package/src/daemon/ipc-contract/sessions.ts +1 -1
  147. package/src/daemon/ipc-contract/settings.ts +26 -0
  148. package/src/daemon/ipc-contract/shared.ts +2 -0
  149. package/src/daemon/ipc-contract/work-items.ts +1 -7
  150. package/src/daemon/ipc-contract-inventory.json +5 -1
  151. package/src/daemon/ipc-contract.ts +5 -1
  152. package/src/daemon/lifecycle.ts +306 -266
  153. package/src/daemon/recording-intent.ts +0 -41
  154. package/src/daemon/response-tier.ts +2 -2
  155. package/src/daemon/server.ts +6 -6
  156. package/src/daemon/session-agent-loop-handlers.ts +34 -9
  157. package/src/daemon/session-agent-loop.ts +15 -8
  158. package/src/daemon/session-history.ts +3 -2
  159. package/src/daemon/session-media-retry.ts +3 -0
  160. package/src/daemon/session-messaging.ts +38 -4
  161. package/src/daemon/session-notifiers.ts +2 -2
  162. package/src/daemon/session-process.ts +256 -23
  163. package/src/daemon/session-queue-manager.ts +2 -0
  164. package/src/daemon/session-runtime-assembly.ts +39 -0
  165. package/src/daemon/session-skill-tools.ts +13 -4
  166. package/src/daemon/session-tool-setup.ts +5 -6
  167. package/src/daemon/session.ts +19 -8
  168. package/src/daemon/tls-certs.ts +55 -13
  169. package/src/daemon/tool-side-effects.ts +13 -5
  170. package/src/gallery/default-gallery.ts +32 -9
  171. package/src/influencer/client.ts +2 -1
  172. package/src/memory/channel-delivery-store.ts +37 -567
  173. package/src/memory/channel-guardian-store.ts +66 -1317
  174. package/src/memory/conflict-store.ts +4 -4
  175. package/src/memory/conversation-attention-store.ts +0 -3
  176. package/src/memory/conversation-crud.ts +668 -0
  177. package/src/memory/conversation-queries.ts +361 -0
  178. package/src/memory/conversation-store.ts +45 -983
  179. package/src/memory/db-connection.ts +3 -0
  180. package/src/memory/db-init.ts +25 -0
  181. package/src/memory/delivery-channels.ts +175 -0
  182. package/src/memory/delivery-crud.ts +211 -0
  183. package/src/memory/delivery-status.ts +199 -0
  184. package/src/memory/embedding-backend.ts +70 -4
  185. package/src/memory/embedding-local.ts +12 -2
  186. package/src/memory/entity-extractor.ts +3 -8
  187. package/src/memory/fts-reconciler.ts +121 -0
  188. package/src/memory/guardian-action-store.ts +366 -3
  189. package/src/memory/guardian-approvals.ts +569 -0
  190. package/src/memory/guardian-bindings.ts +130 -0
  191. package/src/memory/guardian-rate-limits.ts +196 -0
  192. package/src/memory/guardian-verification.ts +520 -0
  193. package/src/memory/job-handlers/index-maintenance.ts +2 -1
  194. package/src/memory/job-utils.ts +8 -5
  195. package/src/memory/jobs-store.ts +66 -6
  196. package/src/memory/jobs-worker.ts +23 -1
  197. package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
  198. package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
  199. package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
  200. package/src/memory/migrations/100-core-tables.ts +1 -1
  201. package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
  202. package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
  203. package/src/memory/migrations/112-assistant-inbox.ts +1 -1
  204. package/src/memory/migrations/113-late-migrations.ts +1 -1
  205. package/src/memory/migrations/116-messages-fts.ts +13 -0
  206. package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
  207. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
  208. package/src/memory/migrations/index.ts +8 -3
  209. package/src/memory/migrations/validate-migration-state.ts +114 -15
  210. package/src/memory/qdrant-circuit-breaker.ts +105 -0
  211. package/src/memory/retriever.ts +46 -13
  212. package/src/memory/schema-migration.ts +3 -0
  213. package/src/memory/schema.ts +25 -7
  214. package/src/memory/search/semantic.ts +8 -90
  215. package/src/notifications/README.md +1 -1
  216. package/src/notifications/broadcaster.ts +20 -2
  217. package/src/notifications/conversation-pairing.ts +3 -3
  218. package/src/notifications/decision-engine.ts +173 -8
  219. package/src/notifications/deliveries-store.ts +27 -8
  220. package/src/notifications/preferences-store.ts +7 -7
  221. package/src/notifications/thread-candidates.ts +234 -0
  222. package/src/notifications/types.ts +18 -0
  223. package/src/permissions/defaults.ts +11 -1
  224. package/src/permissions/prompter.ts +17 -0
  225. package/src/permissions/trust-store.ts +2 -0
  226. package/src/providers/failover.ts +19 -0
  227. package/src/providers/registry.ts +46 -1
  228. package/src/runtime/approval-message-composer.ts +1 -1
  229. package/src/runtime/channel-guardian-service.ts +15 -3
  230. package/src/runtime/channel-retry-sweep.ts +7 -2
  231. package/src/runtime/guardian-action-conversation-turn.ts +85 -0
  232. package/src/runtime/guardian-action-followup-executor.ts +301 -0
  233. package/src/runtime/guardian-action-message-composer.ts +245 -0
  234. package/src/runtime/guardian-outbound-actions.ts +26 -6
  235. package/src/runtime/guardian-verification-templates.ts +15 -9
  236. package/src/runtime/http-errors.ts +93 -0
  237. package/src/runtime/http-server.ts +133 -44
  238. package/src/runtime/http-types.ts +53 -0
  239. package/src/runtime/ingress-service.ts +237 -0
  240. package/src/runtime/middleware/error-handler.ts +4 -3
  241. package/src/runtime/middleware/rate-limiter.ts +160 -0
  242. package/src/runtime/middleware/request-logger.ts +71 -0
  243. package/src/runtime/middleware/twilio-validation.ts +7 -6
  244. package/src/runtime/pending-interactions.ts +12 -0
  245. package/src/runtime/routes/access-request-decision.ts +215 -0
  246. package/src/runtime/routes/app-routes.ts +25 -18
  247. package/src/runtime/routes/approval-routes.ts +18 -47
  248. package/src/runtime/routes/attachment-routes.ts +15 -41
  249. package/src/runtime/routes/call-routes.ts +20 -20
  250. package/src/runtime/routes/channel-delivery-routes.ts +6 -5
  251. package/src/runtime/routes/contact-routes.ts +4 -9
  252. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  253. package/src/runtime/routes/conversation-routes.ts +26 -57
  254. package/src/runtime/routes/debug-routes.ts +71 -0
  255. package/src/runtime/routes/events-routes.ts +3 -2
  256. package/src/runtime/routes/guardian-approval-interception.ts +221 -0
  257. package/src/runtime/routes/identity-routes.ts +14 -10
  258. package/src/runtime/routes/inbound-conversation.ts +3 -2
  259. package/src/runtime/routes/inbound-message-handler.ts +527 -62
  260. package/src/runtime/routes/ingress-routes.ts +174 -0
  261. package/src/runtime/routes/integration-routes.ts +78 -16
  262. package/src/runtime/routes/pairing-routes.ts +11 -10
  263. package/src/runtime/routes/secret-routes.ts +10 -18
  264. package/src/runtime/verification-rate-limiter.ts +83 -0
  265. package/src/schedule/schedule-store.ts +13 -1
  266. package/src/schedule/scheduler.ts +1 -1
  267. package/src/security/secret-ingress.ts +5 -2
  268. package/src/security/secret-scanner.ts +72 -6
  269. package/src/subagent/manager.ts +6 -4
  270. package/src/swarm/plan-validator.ts +4 -1
  271. package/src/tasks/task-runner.ts +3 -1
  272. package/src/tools/browser/api-map.ts +9 -6
  273. package/src/tools/calls/call-start.ts +20 -0
  274. package/src/tools/executor.ts +50 -568
  275. package/src/tools/permission-checker.ts +272 -0
  276. package/src/tools/registry.ts +14 -6
  277. package/src/tools/reminder/reminder-store.ts +7 -7
  278. package/src/tools/reminder/reminder.ts +6 -3
  279. package/src/tools/secret-detection-handler.ts +301 -0
  280. package/src/tools/subagent/message.ts +1 -1
  281. package/src/tools/system/voice-config.ts +62 -0
  282. package/src/tools/tasks/index.ts +3 -3
  283. package/src/tools/tasks/work-item-list.ts +3 -3
  284. package/src/tools/tasks/work-item-update.ts +4 -5
  285. package/src/tools/tool-approval-handler.ts +192 -0
  286. package/src/tools/tool-manifest.ts +2 -0
  287. package/src/watcher/watcher-store.ts +9 -9
  288. package/src/work-items/work-item-runner.ts +9 -6
  289. /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
  290. /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
@@ -1,1320 +1,69 @@
1
1
  /**
2
- * CRUD store for channel guardian bindings, verification challenges,
3
- * and guardian approval requests.
2
+ * Re-export hub for channel guardian store modules.
4
3
  *
5
- * Guardian bindings record which external user is the designated guardian
6
- * for a given (assistantId, channel) pair. Verification challenges track
7
- * the cryptographic handshake used to prove guardian identity. Approval
8
- * requests track per-run guardian approval decisions.
9
- */
10
-
11
- import { and, count, desc, eq, gt, gte, inArray, lte, or } from 'drizzle-orm';
12
- import { v4 as uuid } from 'uuid';
13
-
14
- import { getDb } from './db.js';
15
- import {
16
- channelGuardianApprovalRequests,
17
- channelGuardianBindings,
18
- channelGuardianRateLimits,
19
- channelGuardianVerificationChallenges,
20
- } from './schema.js';
21
-
22
- // ---------------------------------------------------------------------------
23
- // Types
24
- // ---------------------------------------------------------------------------
25
-
26
- export type BindingStatus = 'active' | 'revoked';
27
- export type ChallengeStatus = 'pending' | 'consumed' | 'expired' | 'revoked';
28
- export type SessionStatus = 'pending' | 'consumed' | 'pending_bootstrap' | 'awaiting_response' | 'verified' | 'expired' | 'revoked' | 'locked';
29
- export type IdentityBindingStatus = 'pending_bootstrap' | 'bound';
30
- export type ApprovalRequestStatus = 'pending' | 'approved' | 'denied' | 'expired' | 'cancelled';
31
-
32
- export interface GuardianBinding {
33
- id: string;
34
- assistantId: string;
35
- channel: string;
36
- guardianExternalUserId: string;
37
- guardianDeliveryChatId: string;
38
- status: BindingStatus;
39
- verifiedAt: number;
40
- verifiedVia: string;
41
- metadataJson: string | null;
42
- createdAt: number;
43
- updatedAt: number;
44
- }
45
-
46
- export interface VerificationChallenge {
47
- id: string;
48
- assistantId: string;
49
- channel: string;
50
- challengeHash: string;
51
- expiresAt: number;
52
- status: SessionStatus;
53
- createdBySessionId: string | null;
54
- consumedByExternalUserId: string | null;
55
- consumedByChatId: string | null;
56
- // Outbound session: expected-identity binding
57
- expectedExternalUserId: string | null;
58
- expectedChatId: string | null;
59
- expectedPhoneE164: string | null;
60
- identityBindingStatus: IdentityBindingStatus | null;
61
- // Outbound session: delivery tracking
62
- destinationAddress: string | null;
63
- lastSentAt: number | null;
64
- sendCount: number;
65
- nextResendAt: number | null;
66
- // Session configuration
67
- codeDigits: number;
68
- maxAttempts: number;
69
- // Telegram bootstrap deep-link token hash
70
- bootstrapTokenHash: string | null;
71
- createdAt: number;
72
- updatedAt: number;
73
- }
74
-
75
- export interface GuardianApprovalRequest {
76
- id: string;
77
- runId: string;
78
- requestId: string | null;
79
- conversationId: string;
80
- assistantId: string;
81
- channel: string;
82
- requesterExternalUserId: string;
83
- requesterChatId: string;
84
- guardianExternalUserId: string;
85
- guardianChatId: string;
86
- toolName: string;
87
- riskLevel: string | null;
88
- reason: string | null;
89
- status: ApprovalRequestStatus;
90
- decidedByExternalUserId: string | null;
91
- expiresAt: number;
92
- createdAt: number;
93
- updatedAt: number;
94
- }
95
-
96
- // ---------------------------------------------------------------------------
97
- // Helpers
98
- // ---------------------------------------------------------------------------
99
-
100
- function rowToBinding(row: typeof channelGuardianBindings.$inferSelect): GuardianBinding {
101
- return {
102
- id: row.id,
103
- assistantId: row.assistantId,
104
- channel: row.channel,
105
- guardianExternalUserId: row.guardianExternalUserId,
106
- guardianDeliveryChatId: row.guardianDeliveryChatId,
107
- status: row.status as BindingStatus,
108
- verifiedAt: row.verifiedAt,
109
- verifiedVia: row.verifiedVia,
110
- metadataJson: row.metadataJson,
111
- createdAt: row.createdAt,
112
- updatedAt: row.updatedAt,
113
- };
114
- }
115
-
116
- function rowToChallenge(row: typeof channelGuardianVerificationChallenges.$inferSelect): VerificationChallenge {
117
- return {
118
- id: row.id,
119
- assistantId: row.assistantId,
120
- channel: row.channel,
121
- challengeHash: row.challengeHash,
122
- expiresAt: row.expiresAt,
123
- status: row.status as SessionStatus,
124
- createdBySessionId: row.createdBySessionId,
125
- consumedByExternalUserId: row.consumedByExternalUserId,
126
- consumedByChatId: row.consumedByChatId,
127
- expectedExternalUserId: row.expectedExternalUserId ?? null,
128
- expectedChatId: row.expectedChatId ?? null,
129
- expectedPhoneE164: row.expectedPhoneE164 ?? null,
130
- identityBindingStatus: (row.identityBindingStatus as IdentityBindingStatus) ?? null,
131
- destinationAddress: row.destinationAddress ?? null,
132
- lastSentAt: row.lastSentAt ?? null,
133
- sendCount: row.sendCount ?? 0,
134
- nextResendAt: row.nextResendAt ?? null,
135
- codeDigits: row.codeDigits ?? 6,
136
- maxAttempts: row.maxAttempts ?? 3,
137
- bootstrapTokenHash: row.bootstrapTokenHash ?? null,
138
- createdAt: row.createdAt,
139
- updatedAt: row.updatedAt,
140
- };
141
- }
142
-
143
- function rowToApprovalRequest(row: typeof channelGuardianApprovalRequests.$inferSelect): GuardianApprovalRequest {
144
- return {
145
- id: row.id,
146
- runId: row.runId,
147
- requestId: row.requestId ?? null,
148
- conversationId: row.conversationId,
149
- assistantId: row.assistantId,
150
- channel: row.channel,
151
- requesterExternalUserId: row.requesterExternalUserId,
152
- requesterChatId: row.requesterChatId,
153
- guardianExternalUserId: row.guardianExternalUserId,
154
- guardianChatId: row.guardianChatId,
155
- toolName: row.toolName,
156
- riskLevel: row.riskLevel,
157
- reason: row.reason,
158
- status: row.status as ApprovalRequestStatus,
159
- decidedByExternalUserId: row.decidedByExternalUserId,
160
- expiresAt: row.expiresAt,
161
- createdAt: row.createdAt,
162
- updatedAt: row.updatedAt,
163
- };
164
- }
165
-
166
- // ---------------------------------------------------------------------------
167
- // Guardian Bindings
168
- // ---------------------------------------------------------------------------
169
-
170
- export function createBinding(params: {
171
- assistantId: string;
172
- channel: string;
173
- guardianExternalUserId: string;
174
- guardianDeliveryChatId: string;
175
- verifiedVia?: string;
176
- metadataJson?: string | null;
177
- }): GuardianBinding {
178
- const db = getDb();
179
- const now = Date.now();
180
- const id = uuid();
181
-
182
- const row = {
183
- id,
184
- assistantId: params.assistantId,
185
- channel: params.channel,
186
- guardianExternalUserId: params.guardianExternalUserId,
187
- guardianDeliveryChatId: params.guardianDeliveryChatId,
188
- status: 'active' as const,
189
- verifiedAt: now,
190
- verifiedVia: params.verifiedVia ?? 'challenge',
191
- metadataJson: params.metadataJson ?? null,
192
- createdAt: now,
193
- updatedAt: now,
194
- };
195
-
196
- db.insert(channelGuardianBindings).values(row).run();
197
-
198
- return rowToBinding(row);
199
- }
200
-
201
- export function getActiveBinding(assistantId: string, channel: string): GuardianBinding | null {
202
- const db = getDb();
203
- const row = db
204
- .select()
205
- .from(channelGuardianBindings)
206
- .where(
207
- and(
208
- eq(channelGuardianBindings.assistantId, assistantId),
209
- eq(channelGuardianBindings.channel, channel),
210
- eq(channelGuardianBindings.status, 'active'),
211
- ),
212
- )
213
- .get();
214
-
215
- return row ? rowToBinding(row) : null;
216
- }
217
-
218
- export function revokeBinding(assistantId: string, channel: string): boolean {
219
- const db = getDb();
220
- const now = Date.now();
221
-
222
- const existing = db
223
- .select({ id: channelGuardianBindings.id })
224
- .from(channelGuardianBindings)
225
- .where(
226
- and(
227
- eq(channelGuardianBindings.assistantId, assistantId),
228
- eq(channelGuardianBindings.channel, channel),
229
- eq(channelGuardianBindings.status, 'active'),
230
- ),
231
- )
232
- .get();
233
-
234
- if (!existing) return false;
235
-
236
- db.update(channelGuardianBindings)
237
- .set({ status: 'revoked', updatedAt: now })
238
- .where(eq(channelGuardianBindings.id, existing.id))
239
- .run();
240
-
241
- return true;
242
- }
243
-
244
- // ---------------------------------------------------------------------------
245
- // Verification Challenges
246
- // ---------------------------------------------------------------------------
247
-
248
- export function createChallenge(params: {
249
- id: string;
250
- assistantId: string;
251
- channel: string;
252
- challengeHash: string;
253
- expiresAt: number;
254
- createdBySessionId?: string;
255
- }): VerificationChallenge {
256
- const db = getDb();
257
- const now = Date.now();
258
-
259
- // Revoke any prior pending challenges for the same (assistantId, channel)
260
- // to close the replay window — only the latest challenge should be valid.
261
- db.update(channelGuardianVerificationChallenges)
262
- .set({ status: 'revoked', updatedAt: now })
263
- .where(
264
- and(
265
- eq(channelGuardianVerificationChallenges.assistantId, params.assistantId),
266
- eq(channelGuardianVerificationChallenges.channel, params.channel),
267
- eq(channelGuardianVerificationChallenges.status, 'pending'),
268
- ),
269
- )
270
- .run();
271
-
272
- const row = {
273
- id: params.id,
274
- assistantId: params.assistantId,
275
- channel: params.channel,
276
- challengeHash: params.challengeHash,
277
- expiresAt: params.expiresAt,
278
- status: 'pending' as const,
279
- createdBySessionId: params.createdBySessionId ?? null,
280
- consumedByExternalUserId: null,
281
- consumedByChatId: null,
282
- expectedExternalUserId: null,
283
- expectedChatId: null,
284
- expectedPhoneE164: null,
285
- identityBindingStatus: 'bound' as const,
286
- destinationAddress: null,
287
- lastSentAt: null,
288
- sendCount: 0,
289
- nextResendAt: null,
290
- codeDigits: 6,
291
- maxAttempts: 3,
292
- bootstrapTokenHash: null,
293
- createdAt: now,
294
- updatedAt: now,
295
- };
296
-
297
- db.insert(channelGuardianVerificationChallenges).values(row).run();
298
-
299
- return rowToChallenge(row);
300
- }
301
-
302
- export function revokePendingChallenges(assistantId: string, channel: string): void {
303
- const db = getDb();
304
- db.update(channelGuardianVerificationChallenges)
305
- .set({ status: 'revoked', updatedAt: Date.now() })
306
- .where(
307
- and(
308
- eq(channelGuardianVerificationChallenges.assistantId, assistantId),
309
- eq(channelGuardianVerificationChallenges.channel, channel),
310
- eq(channelGuardianVerificationChallenges.status, 'pending'),
311
- ),
312
- )
313
- .run();
314
- }
315
-
316
- export function findPendingChallengeByHash(
317
- assistantId: string,
318
- channel: string,
319
- challengeHash: string,
320
- ): VerificationChallenge | null {
321
- const db = getDb();
322
- const now = Date.now();
323
-
324
- // Match any consumable status: 'pending' (inbound), 'pending_bootstrap', 'awaiting_response' (outbound)
325
- const row = db
326
- .select()
327
- .from(channelGuardianVerificationChallenges)
328
- .where(
329
- and(
330
- eq(channelGuardianVerificationChallenges.assistantId, assistantId),
331
- eq(channelGuardianVerificationChallenges.channel, channel),
332
- eq(channelGuardianVerificationChallenges.challengeHash, challengeHash),
333
- inArray(channelGuardianVerificationChallenges.status, ['pending', 'pending_bootstrap', 'awaiting_response']),
334
- gt(channelGuardianVerificationChallenges.expiresAt, now),
335
- ),
336
- )
337
- .get();
338
-
339
- return row ? rowToChallenge(row) : null;
340
- }
341
-
342
- /**
343
- * Find any pending inbound (non-expired) challenge for a given (assistantId, channel).
344
- * Scoped to 'pending' status only — this is the inbound verification path used by
345
- * the relay-server to gate incoming voice calls. Outbound session states
346
- * (pending_bootstrap, awaiting_response) are excluded so that an active outbound
347
- * verification does not inadvertently force unrelated inbound callers into the
348
- * guardian verification flow.
349
- */
350
- export function findPendingChallengeForChannel(
351
- assistantId: string,
352
- channel: string,
353
- ): VerificationChallenge | null {
354
- const db = getDb();
355
- const now = Date.now();
356
-
357
- const row = db
358
- .select()
359
- .from(channelGuardianVerificationChallenges)
360
- .where(
361
- and(
362
- eq(channelGuardianVerificationChallenges.assistantId, assistantId),
363
- eq(channelGuardianVerificationChallenges.channel, channel),
364
- eq(channelGuardianVerificationChallenges.status, 'pending'),
365
- gt(channelGuardianVerificationChallenges.expiresAt, now),
366
- ),
367
- )
368
- .get();
369
-
370
- return row ? rowToChallenge(row) : null;
371
- }
372
-
373
- export function consumeChallenge(
374
- id: string,
375
- consumedByExternalUserId: string,
376
- consumedByChatId: string,
377
- ): void {
378
- const db = getDb();
379
- const now = Date.now();
380
-
381
- db.update(channelGuardianVerificationChallenges)
382
- .set({
383
- status: 'consumed',
384
- consumedByExternalUserId,
385
- consumedByChatId,
386
- updatedAt: now,
387
- })
388
- .where(eq(channelGuardianVerificationChallenges.id, id))
389
- .run();
390
- }
391
-
392
- // ---------------------------------------------------------------------------
393
- // Verification Sessions (outbound identity-bound)
394
- // ---------------------------------------------------------------------------
395
-
396
- /**
397
- * Create an outbound verification session with expected-identity binding.
398
- * Auto-revokes prior pending/awaiting_response sessions for the same
399
- * (assistantId, channel) to close the replay window.
400
- */
401
- export function createVerificationSession(params: {
402
- id: string;
403
- assistantId: string;
404
- channel: string;
405
- challengeHash: string;
406
- expiresAt: number;
407
- status: SessionStatus;
408
- createdBySessionId?: string;
409
- expectedExternalUserId?: string | null;
410
- expectedChatId?: string | null;
411
- expectedPhoneE164?: string | null;
412
- identityBindingStatus?: IdentityBindingStatus;
413
- destinationAddress?: string | null;
414
- codeDigits?: number;
415
- maxAttempts?: number;
416
- bootstrapTokenHash?: string | null;
417
- }): VerificationChallenge {
418
- const db = getDb();
419
- const now = Date.now();
420
-
421
- // Revoke any prior pending/awaiting_response sessions for the same (assistantId, channel)
422
- db.update(channelGuardianVerificationChallenges)
423
- .set({ status: 'revoked', updatedAt: now })
424
- .where(
425
- and(
426
- eq(channelGuardianVerificationChallenges.assistantId, params.assistantId),
427
- eq(channelGuardianVerificationChallenges.channel, params.channel),
428
- inArray(channelGuardianVerificationChallenges.status, ['pending', 'pending_bootstrap', 'awaiting_response']),
429
- ),
430
- )
431
- .run();
432
-
433
- const row = {
434
- id: params.id,
435
- assistantId: params.assistantId,
436
- channel: params.channel,
437
- challengeHash: params.challengeHash,
438
- expiresAt: params.expiresAt,
439
- status: params.status as string,
440
- createdBySessionId: params.createdBySessionId ?? null,
441
- consumedByExternalUserId: null,
442
- consumedByChatId: null,
443
- expectedExternalUserId: params.expectedExternalUserId ?? null,
444
- expectedChatId: params.expectedChatId ?? null,
445
- expectedPhoneE164: params.expectedPhoneE164 ?? null,
446
- identityBindingStatus: params.identityBindingStatus ?? 'bound',
447
- destinationAddress: params.destinationAddress ?? null,
448
- lastSentAt: null,
449
- sendCount: 0,
450
- nextResendAt: null,
451
- codeDigits: params.codeDigits ?? 6,
452
- maxAttempts: params.maxAttempts ?? 3,
453
- bootstrapTokenHash: params.bootstrapTokenHash ?? null,
454
- createdAt: now,
455
- updatedAt: now,
456
- };
457
-
458
- db.insert(channelGuardianVerificationChallenges).values(row).run();
459
-
460
- return rowToChallenge(row);
461
- }
462
-
463
- /**
464
- * Find the most recent pending_bootstrap or awaiting_response session
465
- * for a given (assistantId, channel).
466
- */
467
- export function findActiveSession(
468
- assistantId: string,
469
- channel: string,
470
- ): VerificationChallenge | null {
471
- const db = getDb();
472
- const now = Date.now();
473
-
474
- const row = db
475
- .select()
476
- .from(channelGuardianVerificationChallenges)
477
- .where(
478
- and(
479
- eq(channelGuardianVerificationChallenges.assistantId, assistantId),
480
- eq(channelGuardianVerificationChallenges.channel, channel),
481
- inArray(channelGuardianVerificationChallenges.status, ['pending_bootstrap', 'awaiting_response']),
482
- gt(channelGuardianVerificationChallenges.expiresAt, now),
483
- ),
484
- )
485
- .orderBy(desc(channelGuardianVerificationChallenges.createdAt))
486
- .get();
487
-
488
- return row ? rowToChallenge(row) : null;
489
- }
490
-
491
- /**
492
- * Look up a pending_bootstrap session by its bootstrap token hash.
493
- * Used by the Telegram /start gv_<token> bootstrap flow.
494
- */
495
- export function findSessionByBootstrapTokenHash(
496
- assistantId: string,
497
- channel: string,
498
- tokenHash: string,
499
- ): VerificationChallenge | null {
500
- const db = getDb();
501
- const now = Date.now();
502
-
503
- const row = db
504
- .select()
505
- .from(channelGuardianVerificationChallenges)
506
- .where(
507
- and(
508
- eq(channelGuardianVerificationChallenges.assistantId, assistantId),
509
- eq(channelGuardianVerificationChallenges.channel, channel),
510
- eq(channelGuardianVerificationChallenges.bootstrapTokenHash, tokenHash),
511
- eq(channelGuardianVerificationChallenges.status, 'pending_bootstrap'),
512
- gt(channelGuardianVerificationChallenges.expiresAt, now),
513
- ),
514
- )
515
- .get();
516
-
517
- return row ? rowToChallenge(row) : null;
518
- }
519
-
520
- /**
521
- * Identity-bound lookup for the consume path. Finds a session matching the
522
- * given identity fields with an active status.
523
- */
524
- export function findSessionByIdentity(
525
- assistantId: string,
526
- channel: string,
527
- externalUserId?: string,
528
- chatId?: string,
529
- phoneE164?: string,
530
- ): VerificationChallenge | null {
531
- // Require at least one identity parameter to avoid accidentally matching
532
- // an unrelated session when the caller has no parsed identity fields.
533
- if (!externalUserId && !chatId && !phoneE164) {
534
- return null;
535
- }
536
-
537
- const db = getDb();
538
- const now = Date.now();
539
-
540
- const conditions = [
541
- eq(channelGuardianVerificationChallenges.assistantId, assistantId),
542
- eq(channelGuardianVerificationChallenges.channel, channel),
543
- inArray(channelGuardianVerificationChallenges.status, ['pending_bootstrap', 'awaiting_response']),
544
- gt(channelGuardianVerificationChallenges.expiresAt, now),
545
- ];
546
-
547
- // Build identity match conditions
548
- const identityConditions = [];
549
- if (externalUserId) {
550
- identityConditions.push(eq(channelGuardianVerificationChallenges.expectedExternalUserId, externalUserId));
551
- }
552
- if (chatId) {
553
- identityConditions.push(eq(channelGuardianVerificationChallenges.expectedChatId, chatId));
554
- }
555
- if (phoneE164) {
556
- identityConditions.push(eq(channelGuardianVerificationChallenges.expectedPhoneE164, phoneE164));
557
- }
558
-
559
- if (identityConditions.length > 0) {
560
- conditions.push(or(...identityConditions)!);
561
- }
562
-
563
- const row = db
564
- .select()
565
- .from(channelGuardianVerificationChallenges)
566
- .where(and(...conditions))
567
- .orderBy(desc(channelGuardianVerificationChallenges.createdAt))
568
- .get();
569
-
570
- return row ? rowToChallenge(row) : null;
571
- }
572
-
573
- /**
574
- * Transition a session's status with optional extra field updates.
575
- */
576
- export function updateSessionStatus(
577
- id: string,
578
- status: SessionStatus,
579
- extraFields?: Partial<{
580
- consumedByExternalUserId: string;
581
- consumedByChatId: string;
582
- }>,
583
- ): void {
584
- const db = getDb();
585
- const now = Date.now();
586
-
587
- db.update(channelGuardianVerificationChallenges)
588
- .set({
589
- status,
590
- updatedAt: now,
591
- ...(extraFields?.consumedByExternalUserId !== undefined
592
- ? { consumedByExternalUserId: extraFields.consumedByExternalUserId }
593
- : {}),
594
- ...(extraFields?.consumedByChatId !== undefined
595
- ? { consumedByChatId: extraFields.consumedByChatId }
596
- : {}),
597
- })
598
- .where(eq(channelGuardianVerificationChallenges.id, id))
599
- .run();
600
- }
601
-
602
- /**
603
- * Update outbound delivery tracking fields on a session.
604
- */
605
- export function updateSessionDelivery(
606
- id: string,
607
- lastSentAt: number,
608
- sendCount: number,
609
- nextResendAt: number | null,
610
- ): void {
611
- const db = getDb();
612
- const now = Date.now();
613
-
614
- db.update(channelGuardianVerificationChallenges)
615
- .set({
616
- lastSentAt,
617
- sendCount,
618
- nextResendAt,
619
- updatedAt: now,
620
- })
621
- .where(eq(channelGuardianVerificationChallenges.id, id))
622
- .run();
623
- }
624
-
625
- /**
626
- * Count actual sends to a specific destination across all sessions within a
627
- * rolling time window. Uses COUNT of rows with a last_sent_at timestamp
628
- * inside the window rather than SUM(send_count) to avoid double-counting
629
- * cumulative session counters when resend creates new sessions that carry
630
- * forward the cumulative count.
631
- */
632
- export function countRecentSendsToDestination(
633
- channel: string,
634
- destinationAddress: string,
635
- windowMs: number,
636
- ): number {
637
- const db = getDb();
638
- const cutoff = Date.now() - windowMs;
639
-
640
- const result = db
641
- .select({ total: count() })
642
- .from(channelGuardianVerificationChallenges)
643
- .where(
644
- and(
645
- eq(channelGuardianVerificationChallenges.channel, channel),
646
- eq(channelGuardianVerificationChallenges.destinationAddress, destinationAddress),
647
- gte(channelGuardianVerificationChallenges.lastSentAt, cutoff),
648
- ),
649
- )
650
- .get();
651
-
652
- return result?.total ?? 0;
653
- }
654
-
655
- /**
656
- * Telegram bootstrap completion: bind the expected identity fields and
657
- * transition identity_binding_status from pending_bootstrap to bound.
658
- */
659
- export function bindSessionIdentity(
660
- id: string,
661
- externalUserId: string,
662
- chatId: string,
663
- ): void {
664
- const db = getDb();
665
- const now = Date.now();
666
-
667
- db.update(channelGuardianVerificationChallenges)
668
- .set({
669
- expectedExternalUserId: externalUserId,
670
- expectedChatId: chatId,
671
- identityBindingStatus: 'bound',
672
- updatedAt: now,
673
- })
674
- .where(eq(channelGuardianVerificationChallenges.id, id))
675
- .run();
676
- }
677
-
678
- // ---------------------------------------------------------------------------
679
- // Guardian Approval Requests
680
- // ---------------------------------------------------------------------------
681
-
682
- export function createApprovalRequest(params: {
683
- runId: string;
684
- requestId?: string;
685
- conversationId: string;
686
- assistantId?: string;
687
- channel: string;
688
- requesterExternalUserId: string;
689
- requesterChatId: string;
690
- guardianExternalUserId: string;
691
- guardianChatId: string;
692
- toolName: string;
693
- riskLevel?: string;
694
- reason?: string;
695
- expiresAt: number;
696
- }): GuardianApprovalRequest {
697
- const db = getDb();
698
- const now = Date.now();
699
- const id = uuid();
700
-
701
- const row = {
702
- id,
703
- runId: params.runId,
704
- requestId: params.requestId ?? null,
705
- conversationId: params.conversationId,
706
- assistantId: params.assistantId ?? 'self',
707
- channel: params.channel,
708
- requesterExternalUserId: params.requesterExternalUserId,
709
- requesterChatId: params.requesterChatId,
710
- guardianExternalUserId: params.guardianExternalUserId,
711
- guardianChatId: params.guardianChatId,
712
- toolName: params.toolName,
713
- riskLevel: params.riskLevel ?? null,
714
- reason: params.reason ?? null,
715
- status: 'pending' as const,
716
- decidedByExternalUserId: null,
717
- expiresAt: params.expiresAt,
718
- createdAt: now,
719
- updatedAt: now,
720
- };
721
-
722
- db.insert(channelGuardianApprovalRequests).values(row).run();
723
-
724
- return rowToApprovalRequest(row);
725
- }
726
-
727
- export function getPendingApprovalForRun(runId: string): GuardianApprovalRequest | null {
728
- const db = getDb();
729
- const now = Date.now();
730
-
731
- const row = db
732
- .select()
733
- .from(channelGuardianApprovalRequests)
734
- .where(
735
- and(
736
- eq(channelGuardianApprovalRequests.runId, runId),
737
- eq(channelGuardianApprovalRequests.status, 'pending'),
738
- gt(channelGuardianApprovalRequests.expiresAt, now),
739
- ),
740
- )
741
- .get();
742
-
743
- return row ? rowToApprovalRequest(row) : null;
744
- }
745
-
746
- export function getPendingApprovalForRequest(requestId: string): GuardianApprovalRequest | null {
747
- const db = getDb();
748
- const now = Date.now();
749
-
750
- const row = db
751
- .select()
752
- .from(channelGuardianApprovalRequests)
753
- .where(
754
- and(
755
- eq(channelGuardianApprovalRequests.requestId, requestId),
756
- eq(channelGuardianApprovalRequests.status, 'pending'),
757
- gt(channelGuardianApprovalRequests.expiresAt, now),
758
- ),
759
- )
760
- .get();
761
-
762
- return row ? rowToApprovalRequest(row) : null;
763
- }
764
-
765
- /**
766
- * Find a pending (status = 'pending') guardian approval request for a run
767
- * regardless of whether it has expired. Used by the non-guardian gate to
768
- * detect expired-but-unresolved approvals that should still block the
769
- * requester from self-approving.
770
- */
771
- export function getUnresolvedApprovalForRun(runId: string): GuardianApprovalRequest | null {
772
- const db = getDb();
773
-
774
- const row = db
775
- .select()
776
- .from(channelGuardianApprovalRequests)
777
- .where(
778
- and(
779
- eq(channelGuardianApprovalRequests.runId, runId),
780
- eq(channelGuardianApprovalRequests.status, 'pending'),
781
- ),
782
- )
783
- .get();
784
-
785
- return row ? rowToApprovalRequest(row) : null;
786
- }
787
-
788
- export function getUnresolvedApprovalForRequest(requestId: string): GuardianApprovalRequest | null {
789
- const db = getDb();
790
-
791
- const row = db
792
- .select()
793
- .from(channelGuardianApprovalRequests)
794
- .where(
795
- and(
796
- eq(channelGuardianApprovalRequests.requestId, requestId),
797
- eq(channelGuardianApprovalRequests.status, 'pending'),
798
- ),
799
- )
800
- .get();
801
-
802
- return row ? rowToApprovalRequest(row) : null;
803
- }
804
-
805
- /**
806
- * Find a pending guardian approval request by the guardian's chat ID.
807
- * Used when the guardian sends a decision from their chat.
808
- *
809
- * When `assistantId` is provided, the lookup is scoped to that assistant,
810
- * preventing cross-assistant approval consumption in shared guardian chats.
811
- */
812
- export function getPendingApprovalByGuardianChat(
813
- channel: string,
814
- guardianChatId: string,
815
- assistantId?: string,
816
- ): GuardianApprovalRequest | null {
817
- const db = getDb();
818
- const now = Date.now();
819
-
820
- const conditions = [
821
- eq(channelGuardianApprovalRequests.channel, channel),
822
- eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
823
- eq(channelGuardianApprovalRequests.status, 'pending'),
824
- gt(channelGuardianApprovalRequests.expiresAt, now),
825
- ];
826
- if (assistantId) {
827
- conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
828
- }
829
-
830
- const row = db
831
- .select()
832
- .from(channelGuardianApprovalRequests)
833
- .where(and(...conditions))
834
- .orderBy(desc(channelGuardianApprovalRequests.createdAt))
835
- .get();
836
-
837
- return row ? rowToApprovalRequest(row) : null;
838
- }
839
-
840
- /**
841
- * Find a pending guardian approval request scoped to a specific run,
842
- * guardian chat, and channel. Used when a callback button provides a run ID,
843
- * so the decision is applied to exactly the right approval even when
844
- * multiple approvals target the same guardian chat.
845
- *
846
- * When `assistantId` is provided, the lookup is further scoped to that
847
- * assistant to prevent cross-assistant approval consumption.
848
- */
849
- export function getPendingApprovalByRunAndGuardianChat(
850
- runId: string,
851
- channel: string,
852
- guardianChatId: string,
853
- assistantId?: string,
854
- ): GuardianApprovalRequest | null {
855
- const db = getDb();
856
- const now = Date.now();
857
-
858
- const conditions = [
859
- eq(channelGuardianApprovalRequests.runId, runId),
860
- eq(channelGuardianApprovalRequests.channel, channel),
861
- eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
862
- eq(channelGuardianApprovalRequests.status, 'pending'),
863
- gt(channelGuardianApprovalRequests.expiresAt, now),
864
- ];
865
- if (assistantId) {
866
- conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
867
- }
868
-
869
- const row = db
870
- .select()
871
- .from(channelGuardianApprovalRequests)
872
- .where(and(...conditions))
873
- .get();
874
-
875
- return row ? rowToApprovalRequest(row) : null;
876
- }
877
-
878
- /**
879
- * Find a pending guardian approval request scoped to a specific requestId,
880
- * guardian chat, and channel. Used when a callback button provides a requestId,
881
- * so the decision is applied to exactly the right approval even when
882
- * multiple approvals target the same guardian chat.
883
- */
884
- export function getPendingApprovalByRequestAndGuardianChat(
885
- requestId: string,
886
- channel: string,
887
- guardianChatId: string,
888
- assistantId?: string,
889
- ): GuardianApprovalRequest | null {
890
- const db = getDb();
891
- const now = Date.now();
892
-
893
- const conditions = [
894
- eq(channelGuardianApprovalRequests.requestId, requestId),
895
- eq(channelGuardianApprovalRequests.channel, channel),
896
- eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
897
- eq(channelGuardianApprovalRequests.status, 'pending'),
898
- gt(channelGuardianApprovalRequests.expiresAt, now),
899
- ];
900
- if (assistantId) {
901
- conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
902
- }
903
-
904
- const row = db
905
- .select()
906
- .from(channelGuardianApprovalRequests)
907
- .where(and(...conditions))
908
- .get();
909
-
910
- return row ? rowToApprovalRequest(row) : null;
911
- }
912
-
913
- /**
914
- * Return all pending (non-expired) guardian approval requests for a given
915
- * guardian chat and channel. Used to detect ambiguity when a guardian sends
916
- * a plain-text decision while multiple approvals are pending.
917
- *
918
- * When `assistantId` is provided, the results are scoped to that assistant
919
- * to prevent cross-assistant approval consumption.
920
- */
921
- export function getAllPendingApprovalsByGuardianChat(
922
- channel: string,
923
- guardianChatId: string,
924
- assistantId?: string,
925
- ): GuardianApprovalRequest[] {
926
- const db = getDb();
927
- const now = Date.now();
928
-
929
- const conditions = [
930
- eq(channelGuardianApprovalRequests.channel, channel),
931
- eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
932
- eq(channelGuardianApprovalRequests.status, 'pending'),
933
- gt(channelGuardianApprovalRequests.expiresAt, now),
934
- ];
935
- if (assistantId) {
936
- conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
937
- }
938
-
939
- const rows = db
940
- .select()
941
- .from(channelGuardianApprovalRequests)
942
- .where(and(...conditions))
943
- .orderBy(desc(channelGuardianApprovalRequests.createdAt))
944
- .all();
945
-
946
- return rows.map(rowToApprovalRequest);
947
- }
948
-
949
- /**
950
- * Return all pending approval requests whose expiresAt has passed.
951
- * Used by the proactive expiry sweep to auto-deny expired approvals
952
- * without waiting for requester follow-up traffic.
953
- */
954
- export function getExpiredPendingApprovals(): GuardianApprovalRequest[] {
955
- const db = getDb();
956
- const now = Date.now();
957
-
958
- const rows = db
959
- .select()
960
- .from(channelGuardianApprovalRequests)
961
- .where(
962
- and(
963
- eq(channelGuardianApprovalRequests.status, 'pending'),
964
- lte(channelGuardianApprovalRequests.expiresAt, now),
965
- ),
966
- )
967
- .all();
968
-
969
- return rows.map(rowToApprovalRequest);
970
- }
971
-
972
- export function updateApprovalDecision(
973
- id: string,
974
- decision: { status: ApprovalRequestStatus; decidedByExternalUserId?: string },
975
- ): void {
976
- const db = getDb();
977
- const now = Date.now();
978
-
979
- db.update(channelGuardianApprovalRequests)
980
- .set({
981
- status: decision.status,
982
- decidedByExternalUserId: decision.decidedByExternalUserId ?? null,
983
- updatedAt: now,
984
- })
985
- .where(eq(channelGuardianApprovalRequests.id, id))
986
- .run();
987
- }
988
-
989
- // ---------------------------------------------------------------------------
990
- // Escalation Query Helpers
991
- // ---------------------------------------------------------------------------
992
-
993
- /**
994
- * List approval requests filtered by assistant, and optionally by channel,
995
- * conversation, and status. Returns a paginated list of escalations.
996
- */
997
- export function listPendingApprovalRequests(params: {
998
- assistantId?: string;
999
- channel?: string;
1000
- conversationId?: string;
1001
- status?: ApprovalRequestStatus;
1002
- limit?: number;
1003
- offset?: number;
1004
- }): GuardianApprovalRequest[] {
1005
- const db = getDb();
1006
-
1007
- const conditions = [
1008
- eq(channelGuardianApprovalRequests.assistantId, params.assistantId ?? 'self'),
1009
- ];
1010
- if (params.channel) {
1011
- conditions.push(eq(channelGuardianApprovalRequests.channel, params.channel));
1012
- }
1013
- if (params.conversationId) {
1014
- conditions.push(eq(channelGuardianApprovalRequests.conversationId, params.conversationId));
1015
- }
1016
- conditions.push(
1017
- eq(channelGuardianApprovalRequests.status, params.status ?? 'pending'),
1018
- );
1019
-
1020
- let query = db
1021
- .select()
1022
- .from(channelGuardianApprovalRequests)
1023
- .where(and(...conditions))
1024
- .orderBy(desc(channelGuardianApprovalRequests.createdAt));
1025
-
1026
- if (params.limit !== undefined) {
1027
- query = query.limit(params.limit) as typeof query;
1028
- }
1029
- if (params.offset !== undefined) {
1030
- query = query.offset(params.offset) as typeof query;
1031
- }
1032
-
1033
- return query.all().map(rowToApprovalRequest);
1034
- }
1035
-
1036
- /**
1037
- * Fetch a single approval request by its primary key.
1038
- */
1039
- export function getApprovalRequestById(id: string): GuardianApprovalRequest | null {
1040
- const db = getDb();
1041
-
1042
- const row = db
1043
- .select()
1044
- .from(channelGuardianApprovalRequests)
1045
- .where(eq(channelGuardianApprovalRequests.id, id))
1046
- .get();
1047
-
1048
- return row ? rowToApprovalRequest(row) : null;
1049
- }
1050
-
1051
- /**
1052
- * Fetch a single approval request by run ID (any status).
1053
- * Useful for checking whether a run has an associated approval request.
1054
- */
1055
- export function getApprovalRequestByRunId(runId: string): GuardianApprovalRequest | null {
1056
- const db = getDb();
1057
-
1058
- const row = db
1059
- .select()
1060
- .from(channelGuardianApprovalRequests)
1061
- .where(eq(channelGuardianApprovalRequests.runId, runId))
1062
- .orderBy(desc(channelGuardianApprovalRequests.createdAt))
1063
- .get();
1064
-
1065
- return row ? rowToApprovalRequest(row) : null;
1066
- }
1067
-
1068
- /**
1069
- * Resolve a pending approval request with a decision.
4
+ * The implementation has been split into focused modules:
5
+ * - guardian-bindings.ts — channel binding CRUD
6
+ * - guardian-verification.ts verification challenge/session management
7
+ * - guardian-approvals.ts — approval request tracking
8
+ * - guardian-rate-limits.ts — verification rate limiting
1070
9
  *
1071
- * Idempotent: if the request is already resolved with the same decision,
1072
- * the existing record is returned unchanged. Returns null if the request
1073
- * does not exist or was resolved with a *different* decision.
1074
- */
1075
- export function resolveApprovalRequest(
1076
- id: string,
1077
- decision: 'approved' | 'denied',
1078
- decidedByExternalUserId?: string,
1079
- ): GuardianApprovalRequest | null {
1080
- const db = getDb();
1081
-
1082
- const existing = db
1083
- .select()
1084
- .from(channelGuardianApprovalRequests)
1085
- .where(eq(channelGuardianApprovalRequests.id, id))
1086
- .get();
1087
-
1088
- if (!existing) return null;
1089
-
1090
- // Idempotent: already resolved with the same decision
1091
- if (existing.status === decision) {
1092
- return rowToApprovalRequest(existing);
1093
- }
1094
-
1095
- // Only resolve if currently pending
1096
- if (existing.status !== 'pending') {
1097
- return null;
1098
- }
1099
-
1100
- const now = Date.now();
1101
-
1102
- db.update(channelGuardianApprovalRequests)
1103
- .set({
1104
- status: decision,
1105
- decidedByExternalUserId: decidedByExternalUserId ?? null,
1106
- updatedAt: now,
1107
- })
1108
- .where(eq(channelGuardianApprovalRequests.id, id))
1109
- .run();
1110
-
1111
- return rowToApprovalRequest({
1112
- ...existing,
1113
- status: decision,
1114
- decidedByExternalUserId: decidedByExternalUserId ?? null,
1115
- updatedAt: now,
1116
- });
1117
- }
1118
-
1119
- /**
1120
- * Count pending approval requests for a given conversation.
1121
- * Used by thread state projection to compute `pending_escalation_count`.
1122
- */
1123
- export function countPendingByConversation(
1124
- conversationId: string,
1125
- assistantId?: string,
1126
- ): number {
1127
- const db = getDb();
1128
-
1129
- const conditions = [
1130
- eq(channelGuardianApprovalRequests.conversationId, conversationId),
1131
- eq(channelGuardianApprovalRequests.status, 'pending'),
1132
- ];
1133
- if (assistantId) {
1134
- conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
1135
- }
1136
-
1137
- const result = db
1138
- .select({ count: count() })
1139
- .from(channelGuardianApprovalRequests)
1140
- .where(and(...conditions))
1141
- .get();
1142
-
1143
- return result?.count ?? 0;
1144
- }
1145
-
1146
- // ---------------------------------------------------------------------------
1147
- // Verification Rate Limits
1148
- // ---------------------------------------------------------------------------
1149
-
1150
- export interface VerificationRateLimit {
1151
- id: string;
1152
- assistantId: string;
1153
- channel: string;
1154
- actorExternalUserId: string;
1155
- actorChatId: string;
1156
- /** Individual attempt timestamps (epoch-ms) within the sliding window. */
1157
- attemptTimestamps: number[];
1158
- /** Total stored attempt count (may include expired timestamps; use lockedUntil for enforcement decisions). */
1159
- invalidAttempts: number;
1160
- lockedUntil: number | null;
1161
- createdAt: number;
1162
- updatedAt: number;
1163
- }
1164
-
1165
- function parseTimestamps(json: string): number[] {
1166
- try {
1167
- const arr = JSON.parse(json);
1168
- return Array.isArray(arr) ? arr : [];
1169
- } catch {
1170
- return [];
1171
- }
1172
- }
1173
-
1174
- function rowToRateLimit(row: typeof channelGuardianRateLimits.$inferSelect): VerificationRateLimit {
1175
- const timestamps = parseTimestamps(row.attemptTimestampsJson);
1176
- return {
1177
- id: row.id,
1178
- assistantId: row.assistantId,
1179
- channel: row.channel,
1180
- actorExternalUserId: row.actorExternalUserId,
1181
- actorChatId: row.actorChatId,
1182
- attemptTimestamps: timestamps,
1183
- invalidAttempts: timestamps.length,
1184
- lockedUntil: row.lockedUntil,
1185
- createdAt: row.createdAt,
1186
- updatedAt: row.updatedAt,
1187
- };
1188
- }
1189
-
1190
- /**
1191
- * Get the rate-limit record for a given actor on a specific channel.
1192
- */
1193
- export function getRateLimit(
1194
- assistantId: string,
1195
- channel: string,
1196
- actorExternalUserId: string,
1197
- actorChatId: string,
1198
- ): VerificationRateLimit | null {
1199
- const db = getDb();
1200
- const row = db
1201
- .select()
1202
- .from(channelGuardianRateLimits)
1203
- .where(
1204
- and(
1205
- eq(channelGuardianRateLimits.assistantId, assistantId),
1206
- eq(channelGuardianRateLimits.channel, channel),
1207
- eq(channelGuardianRateLimits.actorExternalUserId, actorExternalUserId),
1208
- eq(channelGuardianRateLimits.actorChatId, actorChatId),
1209
- ),
1210
- )
1211
- .get();
1212
-
1213
- return row ? rowToRateLimit(row) : null;
1214
- }
1215
-
1216
- /**
1217
- * Record an invalid verification attempt using a true sliding window.
1218
- *
1219
- * Each individual attempt timestamp is stored; on every new attempt we
1220
- * discard timestamps older than `windowMs`, append the current one, and
1221
- * check whether the count exceeds `maxAttempts`. This avoids the
1222
- * inactivity-timeout pitfall where attempts spaced just under the window
1223
- * accumulate indefinitely.
1224
- */
1225
- export function recordInvalidAttempt(
1226
- assistantId: string,
1227
- channel: string,
1228
- actorExternalUserId: string,
1229
- actorChatId: string,
1230
- windowMs: number,
1231
- maxAttempts: number,
1232
- lockoutMs: number,
1233
- ): VerificationRateLimit {
1234
- const db = getDb();
1235
- const now = Date.now();
1236
- const cutoff = now - windowMs;
1237
-
1238
- const existing = getRateLimit(assistantId, channel, actorExternalUserId, actorChatId);
1239
-
1240
- if (existing) {
1241
- // Keep only timestamps within the sliding window, then add the new one
1242
- const recentTimestamps = existing.attemptTimestamps.filter((ts) => ts > cutoff);
1243
- recentTimestamps.push(now);
1244
-
1245
- const newLockedUntil =
1246
- recentTimestamps.length >= maxAttempts ? now + lockoutMs : existing.lockedUntil;
1247
-
1248
- const timestampsJson = JSON.stringify(recentTimestamps);
1249
-
1250
- db.update(channelGuardianRateLimits)
1251
- .set({
1252
- attemptTimestampsJson: timestampsJson,
1253
- lockedUntil: newLockedUntil,
1254
- updatedAt: now,
1255
- })
1256
- .where(eq(channelGuardianRateLimits.id, existing.id))
1257
- .run();
1258
-
1259
- return {
1260
- ...existing,
1261
- attemptTimestamps: recentTimestamps,
1262
- invalidAttempts: recentTimestamps.length,
1263
- lockedUntil: newLockedUntil,
1264
- updatedAt: now,
1265
- };
1266
- }
1267
-
1268
- // First attempt — create the row
1269
- const id = uuid();
1270
- const timestamps = [now];
1271
- const lockedUntil = 1 >= maxAttempts ? now + lockoutMs : null;
1272
- const row = {
1273
- id,
1274
- assistantId,
1275
- channel,
1276
- actorExternalUserId,
1277
- actorChatId,
1278
- // Legacy columns kept for backward compatibility with upgraded databases
1279
- invalidAttempts: 0,
1280
- windowStartedAt: 0,
1281
- attemptTimestampsJson: JSON.stringify(timestamps),
1282
- lockedUntil,
1283
- createdAt: now,
1284
- updatedAt: now,
1285
- };
1286
-
1287
- db.insert(channelGuardianRateLimits).values(row).run();
1288
-
1289
- return rowToRateLimit(row);
1290
- }
1291
-
1292
- /**
1293
- * Reset the rate-limit counter for a given actor (e.g. after a
1294
- * successful verification).
1295
- */
1296
- export function resetRateLimit(
1297
- assistantId: string,
1298
- channel: string,
1299
- actorExternalUserId: string,
1300
- actorChatId: string,
1301
- ): void {
1302
- const db = getDb();
1303
- const now = Date.now();
1304
-
1305
- db.update(channelGuardianRateLimits)
1306
- .set({
1307
- attemptTimestampsJson: '[]',
1308
- lockedUntil: null,
1309
- updatedAt: now,
1310
- })
1311
- .where(
1312
- and(
1313
- eq(channelGuardianRateLimits.assistantId, assistantId),
1314
- eq(channelGuardianRateLimits.channel, channel),
1315
- eq(channelGuardianRateLimits.actorExternalUserId, actorExternalUserId),
1316
- eq(channelGuardianRateLimits.actorChatId, actorChatId),
1317
- ),
1318
- )
1319
- .run();
1320
- }
10
+ * This file re-exports everything for backward compatibility.
11
+ */
12
+
13
+ export {
14
+ type BindingStatus,
15
+ type GuardianBinding,
16
+ createBinding,
17
+ getActiveBinding,
18
+ revokeBinding,
19
+ } from './guardian-bindings.js';
20
+
21
+ export {
22
+ type ChallengeStatus,
23
+ type SessionStatus,
24
+ type IdentityBindingStatus,
25
+ type VerificationPurpose,
26
+ type VerificationChallenge,
27
+ createChallenge,
28
+ revokePendingChallenges,
29
+ findPendingChallengeByHash,
30
+ findPendingChallengeForChannel,
31
+ consumeChallenge,
32
+ createVerificationSession,
33
+ findActiveSession,
34
+ findSessionByBootstrapTokenHash,
35
+ findSessionByIdentity,
36
+ updateSessionStatus,
37
+ updateSessionDelivery,
38
+ countRecentSendsToDestination,
39
+ bindSessionIdentity,
40
+ } from './guardian-verification.js';
41
+
42
+ export {
43
+ type ApprovalRequestStatus,
44
+ type GuardianApprovalRequest,
45
+ createApprovalRequest,
46
+ getPendingApprovalForRun,
47
+ getPendingApprovalForRequest,
48
+ getUnresolvedApprovalForRun,
49
+ getUnresolvedApprovalForRequest,
50
+ getPendingApprovalByGuardianChat,
51
+ getPendingApprovalByRunAndGuardianChat,
52
+ getPendingApprovalByRequestAndGuardianChat,
53
+ getAllPendingApprovalsByGuardianChat,
54
+ getExpiredPendingApprovals,
55
+ updateApprovalDecision,
56
+ listPendingApprovalRequests,
57
+ getApprovalRequestById,
58
+ getApprovalRequestByRunId,
59
+ resolveApprovalRequest,
60
+ countPendingByConversation,
61
+ findPendingAccessRequestForRequester,
62
+ } from './guardian-approvals.js';
63
+
64
+ export {
65
+ type VerificationRateLimit,
66
+ getRateLimit,
67
+ recordInvalidAttempt,
68
+ resetRateLimit,
69
+ } from './guardian-rate-limits.js';