@vellumai/assistant 0.4.32 → 0.4.34

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 (186) hide show
  1. package/docs/architecture/memory.md +1 -1
  2. package/package.json +1 -1
  3. package/src/__tests__/access-request-decision.test.ts +85 -4
  4. package/src/__tests__/actor-token-service.test.ts +4 -12
  5. package/src/__tests__/approval-primitive.test.ts +0 -45
  6. package/src/__tests__/approval-routes-http.test.ts +0 -1
  7. package/src/__tests__/assistant-id-boundary-guard.test.ts +150 -0
  8. package/src/__tests__/call-controller.test.ts +0 -1
  9. package/src/__tests__/call-routes-http.test.ts +0 -1
  10. package/src/__tests__/callback-handoff-copy.test.ts +0 -1
  11. package/src/__tests__/channel-approval-routes.test.ts +5 -45
  12. package/src/__tests__/channel-guardian.test.ts +122 -346
  13. package/src/__tests__/channel-invite-transport.test.ts +52 -40
  14. package/src/__tests__/commit-message-enrichment-service.test.ts +4 -38
  15. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -1
  16. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +4 -3
  17. package/src/__tests__/contacts-tools.test.ts +4 -5
  18. package/src/__tests__/conversation-attention-store.test.ts +2 -65
  19. package/src/__tests__/conversation-attention-telegram.test.ts +0 -2
  20. package/src/__tests__/conversation-pairing.test.ts +0 -1
  21. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  22. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -3
  23. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -7
  24. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
  25. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -74
  26. package/src/__tests__/guardian-action-late-reply.test.ts +1 -8
  27. package/src/__tests__/guardian-dispatch.test.ts +0 -1
  28. package/src/__tests__/guardian-grant-minting.test.ts +0 -1
  29. package/src/__tests__/guardian-outbound-http.test.ts +0 -1
  30. package/src/__tests__/guardian-routing-state.test.ts +0 -3
  31. package/src/__tests__/handlers-telegram-config.test.ts +0 -1
  32. package/src/__tests__/inbound-invite-redemption.test.ts +1 -7
  33. package/src/__tests__/ingress-reconcile.test.ts +3 -36
  34. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
  35. package/src/__tests__/migration-export-http.test.ts +0 -1
  36. package/src/__tests__/migration-import-commit-http.test.ts +0 -1
  37. package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
  38. package/src/__tests__/migration-validate-http.test.ts +0 -1
  39. package/src/__tests__/non-member-access-request.test.ts +0 -8
  40. package/src/__tests__/notification-broadcaster.test.ts +1 -2
  41. package/src/__tests__/notification-decision-fallback.test.ts +0 -2
  42. package/src/__tests__/notification-decision-strategy.test.ts +0 -1
  43. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  44. package/src/__tests__/notification-telegram-adapter.test.ts +0 -4
  45. package/src/__tests__/relay-server.test.ts +151 -80
  46. package/src/__tests__/sandbox-host-parity.test.ts +5 -2
  47. package/src/__tests__/scoped-approval-grants.test.ts +9 -40
  48. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -36
  49. package/src/__tests__/send-endpoint-busy.test.ts +0 -1
  50. package/src/__tests__/send-notification-tool.test.ts +0 -1
  51. package/src/__tests__/session-init.benchmark.test.ts +0 -2
  52. package/src/__tests__/slack-channel-config.test.ts +0 -1
  53. package/src/__tests__/slack-inbound-verification.test.ts +2 -5
  54. package/src/__tests__/sms-messaging-provider.test.ts +0 -4
  55. package/src/__tests__/terminal-tools.test.ts +5 -2
  56. package/src/__tests__/thread-seed-composer.test.ts +0 -1
  57. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  58. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -4
  59. package/src/__tests__/trusted-contact-approval-notifier.test.ts +65 -77
  60. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  61. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +1 -18
  62. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -14
  63. package/src/__tests__/trusted-contact-verification.test.ts +3 -16
  64. package/src/__tests__/twilio-routes.test.ts +2 -3
  65. package/src/__tests__/update-bulletin.test.ts +0 -2
  66. package/src/__tests__/user-reference.test.ts +47 -1
  67. package/src/__tests__/voice-invite-redemption.test.ts +0 -1
  68. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -38
  69. package/src/__tests__/workspace-git-service.test.ts +2 -2
  70. package/src/approvals/approval-primitive.ts +0 -15
  71. package/src/approvals/guardian-decision-primitive.ts +0 -3
  72. package/src/approvals/guardian-request-resolvers.ts +0 -5
  73. package/src/calls/call-domain.ts +0 -3
  74. package/src/calls/call-store.ts +0 -3
  75. package/src/calls/guardian-action-sweep.ts +2 -1
  76. package/src/calls/guardian-dispatch.ts +1 -2
  77. package/src/calls/relay-access-wait.ts +0 -4
  78. package/src/calls/relay-server.ts +8 -66
  79. package/src/calls/relay-setup-router.ts +1 -2
  80. package/src/calls/relay-verification.ts +0 -1
  81. package/src/calls/twilio-routes.ts +0 -3
  82. package/src/calls/types.ts +0 -1
  83. package/src/calls/voice-session-bridge.ts +0 -1
  84. package/src/channels/config.ts +41 -2
  85. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -1
  86. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  87. package/src/config/bundled-skills/slack-digest-setup/SKILL.md +164 -0
  88. package/src/config/env.ts +0 -4
  89. package/src/config/feature-flag-registry.json +4 -4
  90. package/src/config/user-reference.ts +47 -9
  91. package/src/contacts/contact-store.ts +13 -88
  92. package/src/contacts/contacts-write.ts +3 -11
  93. package/src/contacts/types.ts +0 -1
  94. package/src/daemon/handlers/config-channels.ts +19 -44
  95. package/src/daemon/handlers/config-inbox.ts +6 -6
  96. package/src/daemon/handlers/contacts.ts +8 -12
  97. package/src/daemon/handlers/index.ts +0 -2
  98. package/src/daemon/lifecycle.ts +18 -26
  99. package/src/daemon/session-process.ts +0 -4
  100. package/src/memory/channel-delivery-store.ts +1 -0
  101. package/src/memory/conversation-attention-store.ts +4 -19
  102. package/src/memory/conversation-crud.ts +0 -2
  103. package/src/memory/db-init.ts +8 -0
  104. package/src/memory/delivery-crud.ts +13 -0
  105. package/src/memory/guardian-action-store.ts +0 -12
  106. package/src/memory/guardian-approvals.ts +35 -80
  107. package/src/memory/guardian-rate-limits.ts +1 -14
  108. package/src/memory/guardian-verification.ts +6 -34
  109. package/src/memory/invite-store.ts +76 -15
  110. package/src/memory/migrations/040-invite-code-hash-column.ts +16 -0
  111. package/src/memory/migrations/134-contacts-notes-column.ts +64 -45
  112. package/src/memory/migrations/136-drop-assistant-id-columns.ts +263 -0
  113. package/src/memory/migrations/index.ts +2 -0
  114. package/src/memory/migrations/registry.ts +14 -1
  115. package/src/memory/schema/calls.ts +0 -7
  116. package/src/memory/schema/contacts.ts +2 -8
  117. package/src/memory/schema/guardian.ts +0 -5
  118. package/src/memory/schema/infrastructure.ts +0 -2
  119. package/src/memory/schema/notifications.ts +3 -17
  120. package/src/memory/scoped-approval-grants.ts +2 -24
  121. package/src/notifications/adapters/sms.ts +2 -1
  122. package/src/notifications/broadcaster.ts +1 -6
  123. package/src/notifications/decision-engine.ts +3 -4
  124. package/src/notifications/deliveries-store.ts +0 -4
  125. package/src/notifications/destination-resolver.ts +4 -6
  126. package/src/notifications/deterministic-checks.ts +1 -6
  127. package/src/notifications/emit-signal.ts +4 -11
  128. package/src/notifications/events-store.ts +7 -17
  129. package/src/notifications/preference-summary.ts +2 -2
  130. package/src/notifications/preferences-store.ts +2 -9
  131. package/src/notifications/signal.ts +0 -1
  132. package/src/notifications/thread-candidates.ts +1 -11
  133. package/src/notifications/types.ts +0 -3
  134. package/src/runtime/access-request-helper.ts +3 -10
  135. package/src/runtime/actor-refresh-token-store.ts +0 -6
  136. package/src/runtime/actor-token-store.ts +3 -16
  137. package/src/runtime/actor-trust-resolver.ts +1 -4
  138. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -9
  139. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -3
  140. package/src/runtime/auth/credential-service.ts +1 -15
  141. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  142. package/src/runtime/auth/token-service.ts +50 -0
  143. package/src/runtime/channel-guardian-service.ts +16 -49
  144. package/src/runtime/channel-invite-transport.ts +129 -34
  145. package/src/runtime/channel-invite-transports/email.ts +54 -0
  146. package/src/runtime/channel-invite-transports/slack.ts +87 -0
  147. package/src/runtime/channel-invite-transports/sms.ts +74 -0
  148. package/src/runtime/channel-invite-transports/telegram.ts +35 -11
  149. package/src/runtime/channel-invite-transports/voice.ts +12 -12
  150. package/src/runtime/confirmation-request-guardian-bridge.ts +0 -1
  151. package/src/runtime/guardian-action-followup-executor.ts +3 -2
  152. package/src/runtime/guardian-action-grant-minter.ts +0 -1
  153. package/src/runtime/guardian-outbound-actions.ts +2 -12
  154. package/src/runtime/guardian-vellum-migration.ts +2 -3
  155. package/src/runtime/http-server.ts +0 -1
  156. package/src/runtime/invite-redemption-service.ts +191 -11
  157. package/src/runtime/invite-redemption-templates.ts +6 -6
  158. package/src/runtime/invite-service.ts +81 -11
  159. package/src/runtime/local-actor-identity.ts +2 -5
  160. package/src/runtime/routes/access-request-decision.ts +52 -7
  161. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -9
  162. package/src/runtime/routes/channel-readiness-routes.ts +29 -18
  163. package/src/runtime/routes/contact-routes.ts +48 -46
  164. package/src/runtime/routes/conversation-attention-routes.ts +0 -2
  165. package/src/runtime/routes/global-search-routes.ts +0 -2
  166. package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -12
  167. package/src/runtime/routes/guardian-expiry-sweep.ts +3 -2
  168. package/src/runtime/routes/inbound-message-handler.ts +1 -6
  169. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +296 -47
  170. package/src/runtime/routes/inbound-stages/background-dispatch.ts +6 -42
  171. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -6
  172. package/src/runtime/routes/inbound-stages/edit-intercept.ts +10 -0
  173. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +0 -1
  174. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +0 -1
  175. package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -7
  176. package/src/runtime/routes/invite-routes.ts +1 -0
  177. package/src/runtime/routes/pairing-routes.ts +4 -4
  178. package/src/runtime/tool-grant-request-helper.ts +0 -1
  179. package/src/tools/browser/browser-manager.ts +22 -12
  180. package/src/tools/browser/runtime-check.ts +110 -3
  181. package/src/tools/calls/call-start.ts +1 -3
  182. package/src/tools/followups/followup_create.ts +1 -2
  183. package/src/tools/shared/shell-output.ts +7 -2
  184. package/src/tools/tool-approval-handler.ts +0 -2
  185. package/src/util/platform.ts +0 -4
  186. package/src/workspace/git-service.ts +10 -4
@@ -6,9 +6,7 @@
6
6
  *
7
7
  * 1. Route policy coverage — every dispatched endpoint has a policy.
8
8
  * 2. No X-Actor-Token references in production code.
9
- * 3. No ~/.vellum/http-token file-path references in production code
10
- * (the file itself is still used; the guard prevents new code from
11
- * reading it directly instead of using the platform utility).
9
+ * 3. No legacy gateway-origin proof in production code.
12
10
  * 4. Scope profile contract — every profile resolves to the expected scopes.
13
11
  */
14
12
 
@@ -132,7 +132,6 @@ function mintAccessToken(guardianPrincipalId: string): {
132
132
  // ---------------------------------------------------------------------------
133
133
 
134
134
  function mintRefreshTokenInternal(params: {
135
- assistantId: string;
136
135
  guardianPrincipalId: string;
137
136
  hashedDeviceId: string;
138
137
  platform: string;
@@ -155,7 +154,6 @@ function mintRefreshTokenInternal(params: {
155
154
  createRefreshTokenRecord({
156
155
  tokenHash: refreshTokenHash,
157
156
  familyId,
158
- assistantId: params.assistantId,
159
157
  guardianPrincipalId: params.guardianPrincipalId,
160
158
  hashedDeviceId: params.hashedDeviceId,
161
159
  platform: params.platform,
@@ -184,20 +182,14 @@ function mintRefreshTokenInternal(params: {
184
182
  * Revokes any existing credentials for the device before minting.
185
183
  */
186
184
  export function mintCredentialPair(params: {
187
- assistantId: string;
188
185
  platform: string;
189
186
  deviceId: string;
190
187
  guardianPrincipalId: string;
191
188
  hashedDeviceId: string;
192
189
  }): CredentialPairResult {
193
190
  // Revoke any existing credentials for this device
194
- revokeActorTokensByDevice(
195
- params.assistantId,
196
- params.guardianPrincipalId,
197
- params.hashedDeviceId,
198
- );
191
+ revokeActorTokensByDevice(params.guardianPrincipalId, params.hashedDeviceId);
199
192
  revokeRefreshTokensByDevice(
200
- params.assistantId,
201
193
  params.guardianPrincipalId,
202
194
  params.hashedDeviceId,
203
195
  );
@@ -207,7 +199,6 @@ export function mintCredentialPair(params: {
207
199
 
208
200
  createActorTokenRecord({
209
201
  tokenHash: access.tokenHash,
210
- assistantId: params.assistantId,
211
202
  guardianPrincipalId: params.guardianPrincipalId,
212
203
  hashedDeviceId: params.hashedDeviceId,
213
204
  platform: params.platform,
@@ -217,7 +208,6 @@ export function mintCredentialPair(params: {
217
208
 
218
209
  // Mint new refresh token
219
210
  const refresh = mintRefreshTokenInternal({
220
- assistantId: params.assistantId,
221
211
  guardianPrincipalId: params.guardianPrincipalId,
222
212
  hashedDeviceId: params.hashedDeviceId,
223
213
  platform: params.platform,
@@ -270,7 +260,6 @@ export function rotateCredentials(params: {
270
260
  );
271
261
  revokeFamily(record.familyId);
272
262
  revokeActorTokensByDevice(
273
- record.assistantId,
274
263
  record.guardianPrincipalId,
275
264
  record.hashedDeviceId,
276
265
  );
@@ -314,7 +303,6 @@ export function rotateCredentials(params: {
314
303
 
315
304
  // Revoke old access tokens for this device
316
305
  revokeActorTokensByDevice(
317
- record.assistantId,
318
306
  record.guardianPrincipalId,
319
307
  record.hashedDeviceId,
320
308
  );
@@ -324,7 +312,6 @@ export function rotateCredentials(params: {
324
312
 
325
313
  createActorTokenRecord({
326
314
  tokenHash: access.tokenHash,
327
- assistantId: record.assistantId,
328
315
  guardianPrincipalId: record.guardianPrincipalId,
329
316
  hashedDeviceId: record.hashedDeviceId,
330
317
  platform: params.platform,
@@ -336,7 +323,6 @@ export function rotateCredentials(params: {
336
323
  // absolute expiry so rotation resets inactivity but never extends
337
324
  // the session lifetime.
338
325
  const refresh = mintRefreshTokenInternal({
339
- assistantId: record.assistantId,
340
326
  guardianPrincipalId: record.guardianPrincipalId,
341
327
  hashedDeviceId: record.hashedDeviceId,
342
328
  platform: params.platform,
@@ -22,10 +22,7 @@ export function requireBoundGuardian(
22
22
  403,
23
23
  );
24
24
  }
25
- const guardianResult = findGuardianForChannel(
26
- "vellum",
27
- authContext.assistantId,
28
- );
25
+ const guardianResult = findGuardianForChannel("vellum");
29
26
  if (!guardianResult) {
30
27
  // No guardian yet — in pre-bootstrap state, allow through
31
28
  return null;
@@ -318,6 +318,56 @@ export function mintUiPageToken(): string {
318
318
  });
319
319
  }
320
320
 
321
+ // ---------------------------------------------------------------------------
322
+ // CLI edge token
323
+ // ---------------------------------------------------------------------------
324
+
325
+ /**
326
+ * Mint a long-lived JWT for the CLI to authenticate with the gateway.
327
+ *
328
+ * Written to ~/.vellum/http-token at daemon startup so the CLI can read it
329
+ * and pass it as a Bearer token. Regenerated on each daemon restart. A 30-day
330
+ * TTL avoids expiry between restarts while keeping the window bounded.
331
+ *
332
+ * Uses aud=vellum-gateway so the gateway's edge-auth middleware accepts it.
333
+ */
334
+ export function mintCliEdgeToken(): string {
335
+ return mintToken({
336
+ aud: "vellum-gateway",
337
+ sub: "svc:daemon:self",
338
+ scope_profile: "gateway_service_v1",
339
+ policy_epoch: CURRENT_POLICY_EPOCH,
340
+ ttlSeconds: 86400 * 30,
341
+ });
342
+ }
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // Pairing bearer token
346
+ // ---------------------------------------------------------------------------
347
+
348
+ /**
349
+ * Mint a JWT bearer token for the iOS pairing flow.
350
+ *
351
+ * Minted once at daemon startup and reused for all pairing approvals
352
+ * during this daemon's lifetime. The token is stored on approved pairing
353
+ * entries and returned in HTTP responses as a legacy compatibility field.
354
+ * (iOS clients also receive proper JWT credentials via mintCredentialPair.)
355
+ *
356
+ * The 24-hour TTL covers a typical daemon lifecycle. The daemon re-mints
357
+ * on each restart since the signing key is stable across restarts.
358
+ *
359
+ * aud=vellum-daemon, sub=svc:daemon:pairing, scope_profile=gateway_service_v1
360
+ */
361
+ export function mintPairingBearerToken(): string {
362
+ return mintToken({
363
+ aud: "vellum-daemon",
364
+ sub: "svc:daemon:pairing",
365
+ scope_profile: "gateway_service_v1",
366
+ policy_epoch: CURRENT_POLICY_EPOCH,
367
+ ttlSeconds: 86400, // 24 hours — covers a typical daemon lifecycle
368
+ });
369
+ }
370
+
321
371
  // ---------------------------------------------------------------------------
322
372
  // Hash
323
373
  // ---------------------------------------------------------------------------
@@ -111,7 +111,6 @@ function generateNumericSecret(digits: number = 6): string {
111
111
  * the user; only the hash is persisted.
112
112
  */
113
113
  export function createVerificationChallenge(
114
- assistantId: string,
115
114
  channel: string,
116
115
  sessionId?: string,
117
116
  ): CreateChallengeResult {
@@ -124,7 +123,6 @@ export function createVerificationChallenge(
124
123
 
125
124
  createChallenge({
126
125
  id: challengeId,
127
- assistantId,
128
126
  channel,
129
127
  challengeHash,
130
128
  expiresAt,
@@ -162,7 +160,6 @@ export function createVerificationChallenge(
162
160
  * period. On success the counter resets.
163
161
  */
164
162
  export function validateAndConsumeChallenge(
165
- assistantId: string,
166
163
  channel: string,
167
164
  secret: string,
168
165
  actorExternalUserId: string,
@@ -171,12 +168,7 @@ export function validateAndConsumeChallenge(
171
168
  _actorDisplayName?: string,
172
169
  ): ValidateChallengeResult {
173
170
  // ── Rate-limit check ──
174
- const existing = getRateLimit(
175
- assistantId,
176
- channel,
177
- actorExternalUserId,
178
- actorChatId,
179
- );
171
+ const existing = getRateLimit(channel, actorExternalUserId, actorChatId);
180
172
  if (
181
173
  existing &&
182
174
  existing.lockedUntil != null &&
@@ -195,14 +187,9 @@ export function validateAndConsumeChallenge(
195
187
 
196
188
  const challengeHash = hashSecret(secret);
197
189
 
198
- const challenge = findPendingChallengeByHash(
199
- assistantId,
200
- channel,
201
- challengeHash,
202
- );
190
+ const challenge = findPendingChallengeByHash(channel, challengeHash);
203
191
  if (!challenge) {
204
192
  recordInvalidAttempt(
205
- assistantId,
206
193
  channel,
207
194
  actorExternalUserId,
208
195
  actorChatId,
@@ -221,7 +208,6 @@ export function validateAndConsumeChallenge(
221
208
 
222
209
  if (Date.now() > challenge.expiresAt) {
223
210
  recordInvalidAttempt(
224
- assistantId,
225
211
  channel,
226
212
  actorExternalUserId,
227
213
  actorChatId,
@@ -295,7 +281,6 @@ export function validateAndConsumeChallenge(
295
281
  // Anti-oracle: use the same generic error message to avoid leaking
296
282
  // whether the identity is wrong vs. the code is wrong.
297
283
  recordInvalidAttempt(
298
- assistantId,
299
284
  channel,
300
285
  actorExternalUserId,
301
286
  actorChatId,
@@ -319,7 +304,7 @@ export function validateAndConsumeChallenge(
319
304
  consumeChallenge(challenge.id, actorExternalUserId, actorChatId);
320
305
 
321
306
  // Reset the rate-limit counter on success
322
- resetRateLimit(assistantId, channel, actorExternalUserId, actorChatId);
307
+ resetRateLimit(channel, actorExternalUserId, actorChatId);
323
308
 
324
309
  // Return the verification type — role-specific side effects are
325
310
  // handled by callers: verification-intercept (channel) and
@@ -343,7 +328,7 @@ export function getGuardianBinding(
343
328
  assistantId: string,
344
329
  channel: string,
345
330
  ): GuardianBinding | null {
346
- const result = findGuardianForChannel(channel, assistantId);
331
+ const result = findGuardianForChannel(channel);
347
332
  if (result) {
348
333
  return {
349
334
  id: result.channel.id,
@@ -355,9 +340,7 @@ export function getGuardianBinding(
355
340
  status: "active" as const,
356
341
  verifiedAt: result.channel.verifiedAt ?? 0,
357
342
  verifiedVia: result.channel.verifiedVia ?? "",
358
- metadataJson: result.contact.displayName
359
- ? JSON.stringify({ displayName: result.contact.displayName })
360
- : null,
343
+ metadataJson: null,
361
344
  createdAt: result.channel.createdAt,
362
345
  updatedAt: result.channel.updatedAt ?? result.channel.createdAt,
363
346
  };
@@ -375,7 +358,7 @@ export function isGuardian(
375
358
  channel: string,
376
359
  externalUserId: string,
377
360
  ): boolean {
378
- const result = findGuardianForChannel(channel, assistantId);
361
+ const result = findGuardianForChannel(channel);
379
362
  if (result) {
380
363
  return result.channel.externalUserId === externalUserId;
381
364
  }
@@ -387,31 +370,27 @@ export function isGuardian(
387
370
  * Revoke the active guardian binding for a given assistant and channel.
388
371
  */
389
372
  export function revokeBinding(assistantId: string, channel: string): boolean {
390
- return revokeGuardianBinding(assistantId, channel);
373
+ return revokeGuardianBinding(channel);
391
374
  }
392
375
 
393
376
  /**
394
- * Revoke all pending challenges for a given assistant and channel.
377
+ * Revoke all pending challenges for a given channel.
395
378
  * Called when the user cancels verification so that stale challenges
396
379
  * don't gate inbound calls.
397
380
  */
398
- export function revokePendingChallenges(
399
- assistantId: string,
400
- channel: string,
401
- ): void {
402
- storeRevokePendingChallenges(assistantId, channel);
381
+ export function revokePendingChallenges(channel: string): void {
382
+ storeRevokePendingChallenges(channel);
403
383
  }
404
384
 
405
385
  /**
406
386
  * Look up a pending (non-expired) verification challenge for a given
407
- * assistant and channel. Used by relay setup to detect whether an active
387
+ * channel. Used by relay setup to detect whether an active
408
388
  * voice verification session exists.
409
389
  */
410
390
  export function getPendingChallenge(
411
- assistantId: string,
412
391
  channel: string,
413
392
  ): VerificationChallenge | null {
414
- return findPendingChallengeForChannel(assistantId, channel);
393
+ return findPendingChallengeForChannel(channel);
415
394
  }
416
395
 
417
396
  // ---------------------------------------------------------------------------
@@ -437,7 +416,6 @@ export interface CreateOutboundSessionResult {
437
416
  * the TTL window.
438
417
  */
439
418
  export function createOutboundSession(params: {
440
- assistantId: string;
441
419
  channel: string;
442
420
  expectedExternalUserId?: string;
443
421
  expectedChatId?: string;
@@ -462,7 +440,6 @@ export function createOutboundSession(params: {
462
440
 
463
441
  createVerificationSession({
464
442
  id: sessionId,
465
- assistantId: params.assistantId,
466
443
  channel: params.channel,
467
444
  challengeHash,
468
445
  expiresAt,
@@ -491,33 +468,24 @@ export function createOutboundSession(params: {
491
468
  }
492
469
 
493
470
  /**
494
- * Find the most recent active outbound session for a given
495
- * (assistantId, channel).
471
+ * Find the most recent active outbound session for a given channel.
496
472
  */
497
473
  export function findActiveSession(
498
- assistantId: string,
499
474
  channel: string,
500
475
  ): VerificationChallenge | null {
501
- return storeFindActiveSession(assistantId, channel);
476
+ return storeFindActiveSession(channel);
502
477
  }
503
478
 
504
479
  /**
505
480
  * Identity-bound session lookup for the consume path.
506
481
  */
507
482
  export function findSessionByIdentity(
508
- assistantId: string,
509
483
  channel: string,
510
484
  externalUserId?: string,
511
485
  chatId?: string,
512
486
  phoneE164?: string,
513
487
  ): VerificationChallenge | null {
514
- return storeFindSessionByIdentity(
515
- assistantId,
516
- channel,
517
- externalUserId,
518
- chatId,
519
- phoneE164,
520
- );
488
+ return storeFindSessionByIdentity(channel, externalUserId, chatId, phoneE164);
521
489
  }
522
490
 
523
491
  /**
@@ -580,10 +548,9 @@ export function bindSessionIdentity(
580
548
  * Hashes the raw token with SHA-256 and looks up the session.
581
549
  */
582
550
  export function resolveBootstrapToken(
583
- assistantId: string,
584
551
  channel: string,
585
552
  token: string,
586
553
  ): VerificationChallenge | null {
587
554
  const tokenHash = hashSecret(token);
588
- return storeFindSessionByBootstrapTokenHash(assistantId, channel, tokenHash);
555
+ return storeFindSessionByBootstrapTokenHash(channel, tokenHash);
589
556
  }
@@ -1,13 +1,15 @@
1
1
  /**
2
- * Channel invite transport abstraction.
2
+ * Channel invite adapter abstraction.
3
3
  *
4
- * Defines a transport interface for building shareable invite links and
5
- * extracting inbound invite tokens from channel-specific payloads. Each
6
- * channel (Telegram, SMS, Slack, etc.) registers an adapter that knows
7
- * how to construct deep links and parse incoming tokens for that channel.
4
+ * Defines an adapter interface for building shareable invite links,
5
+ * extracting inbound invite tokens, and generating guardian instructions
6
+ * from channel-specific payloads. Each channel (Telegram, voice, etc.)
7
+ * registers an adapter that knows how to handle invite flows for that
8
+ * channel.
8
9
  *
9
- * The transport layer is intentionally thin: it handles URL construction
10
- * and token extraction only. Redemption logic lives in
10
+ * All methods are optional: a channel that only provides
11
+ * `buildGuardianInstruction` (e.g. SMS) is a valid adapter. The adapter
12
+ * layer is intentionally thin — redemption logic lives in
11
13
  * `invite-redemption-service.ts`.
12
14
  */
13
15
 
@@ -17,71 +19,164 @@ import type { ChannelId } from "../channels/types.js";
17
19
  // Types
18
20
  // ---------------------------------------------------------------------------
19
21
 
20
- export interface InviteSharePayload {
22
+ export interface InviteShareLink {
21
23
  /** The full URL the recipient can open to redeem the invite. */
22
24
  url: string;
23
25
  /** Human-readable text suitable for display alongside the link. */
24
26
  displayText: string;
25
27
  }
26
28
 
27
- export interface ChannelInviteTransport {
28
- /** The channel this transport handles. */
29
+ export interface GuardianInstruction {
30
+ /** Human-readable instruction text for the guardian. */
31
+ instruction: string;
32
+ /** Channel-specific handle to reach the assistant (e.g. "@botName", "+15551234567", "hello@domain.agentmail.to"). */
33
+ channelHandle?: string;
34
+ }
35
+
36
+ export interface ChannelInviteAdapter {
37
+ /** The channel this adapter handles. */
29
38
  channel: ChannelId;
30
39
 
31
40
  /**
32
- * Build a shareable invite payload (URL + display text) from a raw token.
33
- *
34
- * The raw token is the base64url-encoded secret returned by
35
- * `invite-store.createInvite`. The transport wraps it in a
36
- * channel-specific deep link so the recipient can redeem the invite
37
- * by clicking/tapping the link.
41
+ * Build a channel-specific shareable link (e.g. Telegram deep link).
42
+ * Optional — not all channels support link-based invites.
38
43
  */
39
- buildShareableInvite(params: {
44
+ buildShareLink?(params: {
40
45
  rawToken: string;
41
46
  sourceChannel: ChannelId;
42
- }): InviteSharePayload;
47
+ }): InviteShareLink;
43
48
 
44
49
  /**
45
- * Extract an invite token from an inbound channel message.
46
- *
47
- * Returns the raw token string (without the `iv_` prefix) if the
48
- * message contains a valid invite token, or `undefined` otherwise.
50
+ * Extract a channel-specific invite token from an inbound message
51
+ * (e.g. Telegram `/start iv_<token>`). Optional — only needed for
52
+ * channels with link-based invites.
49
53
  */
50
- extractInboundToken(params: {
54
+ extractInboundToken?(params: {
51
55
  commandIntent?: Record<string, unknown>;
52
56
  content: string;
53
57
  sourceMetadata?: Record<string, unknown>;
54
58
  }): string | undefined;
59
+
60
+ /**
61
+ * Build guardian instruction for this channel. Returns structured data
62
+ * with the instruction text and an optional channel-specific handle.
63
+ * Optional — falls back to generic instruction if not implemented.
64
+ */
65
+ buildGuardianInstruction?(params: {
66
+ inviteCode: string;
67
+ contactName?: string;
68
+ }): GuardianInstruction;
69
+
70
+ /**
71
+ * Resolve the channel-specific handle to reach the assistant (e.g.
72
+ * "@botName", "+15551234567", "hello@domain.agentmail.to").
73
+ * Returns `undefined` when the handle cannot be resolved (e.g.
74
+ * credentials not yet configured).
75
+ */
76
+ resolveChannelHandle?(): string | undefined;
55
77
  }
56
78
 
79
+ // ---------------------------------------------------------------------------
80
+ // Backward-compatible type aliases
81
+ // ---------------------------------------------------------------------------
82
+
83
+ /** @deprecated Use `ChannelInviteAdapter` instead. */
84
+ export type ChannelInviteTransport = ChannelInviteAdapter;
85
+
86
+ /** @deprecated Use `InviteShareLink` instead. */
87
+ export type InviteSharePayload = InviteShareLink;
88
+
57
89
  // ---------------------------------------------------------------------------
58
90
  // Registry
59
91
  // ---------------------------------------------------------------------------
60
92
 
61
- const registry = new Map<ChannelId, ChannelInviteTransport>();
93
+ export class InviteAdapterRegistry {
94
+ private adapters = new Map<ChannelId, ChannelInviteAdapter>();
95
+
96
+ /**
97
+ * Register a channel invite adapter. Overwrites any previously
98
+ * registered adapter for the same channel.
99
+ */
100
+ register(adapter: ChannelInviteAdapter): void {
101
+ this.adapters.set(adapter.channel, adapter);
102
+ }
103
+
104
+ /**
105
+ * Look up the registered adapter for a channel. Returns `undefined`
106
+ * when no adapter has been registered for the given channel.
107
+ */
108
+ get(channel: ChannelId): ChannelInviteAdapter | undefined {
109
+ return this.adapters.get(channel);
110
+ }
111
+
112
+ /** Return all registered adapters. */
113
+ getAll(): ChannelInviteAdapter[] {
114
+ return Array.from(this.adapters.values());
115
+ }
116
+
117
+ /**
118
+ * Reset the registry. Intended for tests only.
119
+ * @internal
120
+ */
121
+ _reset(): void {
122
+ this.adapters.clear();
123
+ }
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Singleton registry + backward-compatible free functions
128
+ // ---------------------------------------------------------------------------
129
+
130
+ import { emailInviteAdapter } from "./channel-invite-transports/email.js";
131
+ import { slackInviteAdapter } from "./channel-invite-transports/slack.js";
132
+ import { smsInviteAdapter } from "./channel-invite-transports/sms.js";
133
+ import { telegramInviteAdapter } from "./channel-invite-transports/telegram.js";
134
+ import { voiceInviteAdapter } from "./channel-invite-transports/voice.js";
135
+
136
+ /** Create a registry instance with built-in adapters registered. */
137
+ export function createInviteAdapterRegistry(): InviteAdapterRegistry {
138
+ const registry = new InviteAdapterRegistry();
139
+ registry.register(emailInviteAdapter);
140
+ registry.register(slackInviteAdapter);
141
+ registry.register(smsInviteAdapter);
142
+ registry.register(telegramInviteAdapter);
143
+ registry.register(voiceInviteAdapter);
144
+ return registry;
145
+ }
146
+
147
+ /**
148
+ * Module-level singleton registry, created eagerly so callers that
149
+ * import the free functions continue to work without changes.
150
+ */
151
+ const defaultRegistry = createInviteAdapterRegistry();
152
+
153
+ /** Return the module-level singleton registry. */
154
+ export function getInviteAdapterRegistry(): InviteAdapterRegistry {
155
+ return defaultRegistry;
156
+ }
62
157
 
63
158
  /**
64
- * Register a channel invite transport. Overwrites any previously registered
65
- * transport for the same channel.
159
+ * Register a channel invite adapter on the default registry.
160
+ * @deprecated Prefer `getInviteAdapterRegistry().register(adapter)`.
66
161
  */
67
- export function registerTransport(transport: ChannelInviteTransport): void {
68
- registry.set(transport.channel, transport);
162
+ export function registerTransport(transport: ChannelInviteAdapter): void {
163
+ defaultRegistry.register(transport);
69
164
  }
70
165
 
71
166
  /**
72
- * Look up the registered transport for a channel. Returns `undefined` when
73
- * no transport has been registered for the given channel.
167
+ * Look up the registered adapter for a channel on the default registry.
168
+ * @deprecated Prefer `getInviteAdapterRegistry().get(channel)`.
74
169
  */
75
170
  export function getTransport(
76
171
  channel: ChannelId,
77
- ): ChannelInviteTransport | undefined {
78
- return registry.get(channel);
172
+ ): ChannelInviteAdapter | undefined {
173
+ return defaultRegistry.get(channel);
79
174
  }
80
175
 
81
176
  /**
82
- * Reset the registry. Intended for tests only.
177
+ * Reset the default registry. Intended for tests only.
83
178
  * @internal
84
179
  */
85
180
  export function _resetRegistry(): void {
86
- registry.clear();
181
+ defaultRegistry._reset();
87
182
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Email channel invite adapter.
3
+ *
4
+ * Provides guardian instruction text for email-based invites. Email invites
5
+ * use the universal 6-digit code path for redemption, so this adapter only
6
+ * implements `buildGuardianInstruction` — no `buildShareLink` or
7
+ * `extractInboundToken` needed.
8
+ */
9
+
10
+ import type {
11
+ ChannelInviteAdapter,
12
+ GuardianInstruction,
13
+ } from "../channel-invite-transport.js";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Email address resolution
17
+ // ---------------------------------------------------------------------------
18
+
19
+ // TODO: resolve from AgentMail provider (async — needs caching or pre-resolution)
20
+ // The real implementation requires async inbox lookup via
21
+ // `getActiveEmailProvider().health()` which doesn't fit the sync adapter
22
+ // interface.
23
+ function resolveAssistantEmailAddress(): string | undefined {
24
+ return undefined;
25
+ }
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Adapter implementation
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export const emailInviteAdapter: ChannelInviteAdapter = {
32
+ channel: "email",
33
+
34
+ buildGuardianInstruction(params: {
35
+ inviteCode: string;
36
+ contactName?: string;
37
+ }): GuardianInstruction {
38
+ const address = resolveAssistantEmailAddress();
39
+ const contactLabel = params.contactName || "the contact";
40
+ if (!address) {
41
+ return {
42
+ instruction: `Tell ${contactLabel} to email the assistant and include the code ${params.inviteCode} in the message.`,
43
+ };
44
+ }
45
+ return {
46
+ instruction: `Tell ${contactLabel} to email ${address} and include the code ${params.inviteCode} in the message.`,
47
+ channelHandle: address,
48
+ };
49
+ },
50
+
51
+ resolveChannelHandle(): string | undefined {
52
+ return resolveAssistantEmailAddress();
53
+ },
54
+ };