@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.
- package/docs/architecture/memory.md +1 -1
- package/package.json +1 -1
- package/src/__tests__/access-request-decision.test.ts +85 -4
- package/src/__tests__/actor-token-service.test.ts +4 -12
- package/src/__tests__/approval-primitive.test.ts +0 -45
- package/src/__tests__/approval-routes-http.test.ts +0 -1
- package/src/__tests__/assistant-id-boundary-guard.test.ts +150 -0
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-routes-http.test.ts +0 -1
- package/src/__tests__/callback-handoff-copy.test.ts +0 -1
- package/src/__tests__/channel-approval-routes.test.ts +5 -45
- package/src/__tests__/channel-guardian.test.ts +122 -346
- package/src/__tests__/channel-invite-transport.test.ts +52 -40
- package/src/__tests__/commit-message-enrichment-service.test.ts +4 -38
- package/src/__tests__/computer-use-session-working-dir.test.ts +0 -1
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +4 -3
- package/src/__tests__/contacts-tools.test.ts +4 -5
- package/src/__tests__/conversation-attention-store.test.ts +2 -65
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -2
- package/src/__tests__/conversation-pairing.test.ts +0 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -3
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -7
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -74
- package/src/__tests__/guardian-action-late-reply.test.ts +1 -8
- package/src/__tests__/guardian-dispatch.test.ts +0 -1
- package/src/__tests__/guardian-grant-minting.test.ts +0 -1
- package/src/__tests__/guardian-outbound-http.test.ts +0 -1
- package/src/__tests__/guardian-routing-state.test.ts +0 -3
- package/src/__tests__/handlers-telegram-config.test.ts +0 -1
- package/src/__tests__/inbound-invite-redemption.test.ts +1 -7
- package/src/__tests__/ingress-reconcile.test.ts +3 -36
- package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
- package/src/__tests__/migration-export-http.test.ts +0 -1
- package/src/__tests__/migration-import-commit-http.test.ts +0 -1
- package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
- package/src/__tests__/migration-validate-http.test.ts +0 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -8
- package/src/__tests__/notification-broadcaster.test.ts +1 -2
- package/src/__tests__/notification-decision-fallback.test.ts +0 -2
- package/src/__tests__/notification-decision-strategy.test.ts +0 -1
- package/src/__tests__/notification-guardian-path.test.ts +0 -1
- package/src/__tests__/notification-telegram-adapter.test.ts +0 -4
- package/src/__tests__/relay-server.test.ts +151 -80
- package/src/__tests__/sandbox-host-parity.test.ts +5 -2
- package/src/__tests__/scoped-approval-grants.test.ts +9 -40
- package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -36
- package/src/__tests__/send-endpoint-busy.test.ts +0 -1
- package/src/__tests__/send-notification-tool.test.ts +0 -1
- package/src/__tests__/session-init.benchmark.test.ts +0 -2
- package/src/__tests__/slack-channel-config.test.ts +0 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -5
- package/src/__tests__/sms-messaging-provider.test.ts +0 -4
- package/src/__tests__/terminal-tools.test.ts +5 -2
- package/src/__tests__/thread-seed-composer.test.ts +0 -1
- package/src/__tests__/tool-approval-handler.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +0 -4
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +65 -77
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +1 -18
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -14
- package/src/__tests__/trusted-contact-verification.test.ts +3 -16
- package/src/__tests__/twilio-routes.test.ts +2 -3
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/user-reference.test.ts +47 -1
- package/src/__tests__/voice-invite-redemption.test.ts +0 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -38
- package/src/__tests__/workspace-git-service.test.ts +2 -2
- package/src/approvals/approval-primitive.ts +0 -15
- package/src/approvals/guardian-decision-primitive.ts +0 -3
- package/src/approvals/guardian-request-resolvers.ts +0 -5
- package/src/calls/call-domain.ts +0 -3
- package/src/calls/call-store.ts +0 -3
- package/src/calls/guardian-action-sweep.ts +2 -1
- package/src/calls/guardian-dispatch.ts +1 -2
- package/src/calls/relay-access-wait.ts +0 -4
- package/src/calls/relay-server.ts +8 -66
- package/src/calls/relay-setup-router.ts +1 -2
- package/src/calls/relay-verification.ts +0 -1
- package/src/calls/twilio-routes.ts +0 -3
- package/src/calls/types.ts +0 -1
- package/src/calls/voice-session-bridge.ts +0 -1
- package/src/channels/config.ts +41 -2
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -1
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/slack-digest-setup/SKILL.md +164 -0
- package/src/config/env.ts +0 -4
- package/src/config/feature-flag-registry.json +4 -4
- package/src/config/user-reference.ts +47 -9
- package/src/contacts/contact-store.ts +13 -88
- package/src/contacts/contacts-write.ts +3 -11
- package/src/contacts/types.ts +0 -1
- package/src/daemon/handlers/config-channels.ts +19 -44
- package/src/daemon/handlers/config-inbox.ts +6 -6
- package/src/daemon/handlers/contacts.ts +8 -12
- package/src/daemon/handlers/index.ts +0 -2
- package/src/daemon/lifecycle.ts +18 -26
- package/src/daemon/session-process.ts +0 -4
- package/src/memory/channel-delivery-store.ts +1 -0
- package/src/memory/conversation-attention-store.ts +4 -19
- package/src/memory/conversation-crud.ts +0 -2
- package/src/memory/db-init.ts +8 -0
- package/src/memory/delivery-crud.ts +13 -0
- package/src/memory/guardian-action-store.ts +0 -12
- package/src/memory/guardian-approvals.ts +35 -80
- package/src/memory/guardian-rate-limits.ts +1 -14
- package/src/memory/guardian-verification.ts +6 -34
- package/src/memory/invite-store.ts +76 -15
- package/src/memory/migrations/040-invite-code-hash-column.ts +16 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +64 -45
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +263 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/registry.ts +14 -1
- package/src/memory/schema/calls.ts +0 -7
- package/src/memory/schema/contacts.ts +2 -8
- package/src/memory/schema/guardian.ts +0 -5
- package/src/memory/schema/infrastructure.ts +0 -2
- package/src/memory/schema/notifications.ts +3 -17
- package/src/memory/scoped-approval-grants.ts +2 -24
- package/src/notifications/adapters/sms.ts +2 -1
- package/src/notifications/broadcaster.ts +1 -6
- package/src/notifications/decision-engine.ts +3 -4
- package/src/notifications/deliveries-store.ts +0 -4
- package/src/notifications/destination-resolver.ts +4 -6
- package/src/notifications/deterministic-checks.ts +1 -6
- package/src/notifications/emit-signal.ts +4 -11
- package/src/notifications/events-store.ts +7 -17
- package/src/notifications/preference-summary.ts +2 -2
- package/src/notifications/preferences-store.ts +2 -9
- package/src/notifications/signal.ts +0 -1
- package/src/notifications/thread-candidates.ts +1 -11
- package/src/notifications/types.ts +0 -3
- package/src/runtime/access-request-helper.ts +3 -10
- package/src/runtime/actor-refresh-token-store.ts +0 -6
- package/src/runtime/actor-token-store.ts +3 -16
- package/src/runtime/actor-trust-resolver.ts +1 -4
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -9
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -3
- package/src/runtime/auth/credential-service.ts +1 -15
- package/src/runtime/auth/require-bound-guardian.ts +1 -4
- package/src/runtime/auth/token-service.ts +50 -0
- package/src/runtime/channel-guardian-service.ts +16 -49
- package/src/runtime/channel-invite-transport.ts +129 -34
- package/src/runtime/channel-invite-transports/email.ts +54 -0
- package/src/runtime/channel-invite-transports/slack.ts +87 -0
- package/src/runtime/channel-invite-transports/sms.ts +74 -0
- package/src/runtime/channel-invite-transports/telegram.ts +35 -11
- package/src/runtime/channel-invite-transports/voice.ts +12 -12
- package/src/runtime/confirmation-request-guardian-bridge.ts +0 -1
- package/src/runtime/guardian-action-followup-executor.ts +3 -2
- package/src/runtime/guardian-action-grant-minter.ts +0 -1
- package/src/runtime/guardian-outbound-actions.ts +2 -12
- package/src/runtime/guardian-vellum-migration.ts +2 -3
- package/src/runtime/http-server.ts +0 -1
- package/src/runtime/invite-redemption-service.ts +191 -11
- package/src/runtime/invite-redemption-templates.ts +6 -6
- package/src/runtime/invite-service.ts +81 -11
- package/src/runtime/local-actor-identity.ts +2 -5
- package/src/runtime/routes/access-request-decision.ts +52 -7
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -9
- package/src/runtime/routes/channel-readiness-routes.ts +29 -18
- package/src/runtime/routes/contact-routes.ts +48 -46
- package/src/runtime/routes/conversation-attention-routes.ts +0 -2
- package/src/runtime/routes/global-search-routes.ts +0 -2
- package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -12
- package/src/runtime/routes/guardian-expiry-sweep.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +1 -6
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +296 -47
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +6 -42
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -6
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +10 -0
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +0 -1
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +0 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -7
- package/src/runtime/routes/invite-routes.ts +1 -0
- package/src/runtime/routes/pairing-routes.ts +4 -4
- package/src/runtime/tool-grant-request-helper.ts +0 -1
- package/src/tools/browser/browser-manager.ts +22 -12
- package/src/tools/browser/runtime-check.ts +110 -3
- package/src/tools/calls/call-start.ts +1 -3
- package/src/tools/followups/followup_create.ts +1 -2
- package/src/tools/shared/shell-output.ts +7 -2
- package/src/tools/tool-approval-handler.ts +0 -2
- package/src/util/platform.ts +0 -4
- 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
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
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(
|
|
373
|
+
return revokeGuardianBinding(channel);
|
|
391
374
|
}
|
|
392
375
|
|
|
393
376
|
/**
|
|
394
|
-
* Revoke all pending challenges for a given
|
|
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
|
-
|
|
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
|
-
*
|
|
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(
|
|
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(
|
|
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(
|
|
555
|
+
return storeFindSessionByBootstrapTokenHash(channel, tokenHash);
|
|
589
556
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Channel invite
|
|
2
|
+
* Channel invite adapter abstraction.
|
|
3
3
|
*
|
|
4
|
-
* Defines
|
|
5
|
-
* extracting inbound invite tokens
|
|
6
|
-
* channel (Telegram,
|
|
7
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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
|
|
28
|
-
/**
|
|
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
|
|
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
|
-
|
|
44
|
+
buildShareLink?(params: {
|
|
40
45
|
rawToken: string;
|
|
41
46
|
sourceChannel: ChannelId;
|
|
42
|
-
}):
|
|
47
|
+
}): InviteShareLink;
|
|
43
48
|
|
|
44
49
|
/**
|
|
45
|
-
* Extract
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
-
|
|
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
|
|
65
|
-
*
|
|
159
|
+
* Register a channel invite adapter on the default registry.
|
|
160
|
+
* @deprecated Prefer `getInviteAdapterRegistry().register(adapter)`.
|
|
66
161
|
*/
|
|
67
|
-
export function registerTransport(transport:
|
|
68
|
-
|
|
162
|
+
export function registerTransport(transport: ChannelInviteAdapter): void {
|
|
163
|
+
defaultRegistry.register(transport);
|
|
69
164
|
}
|
|
70
165
|
|
|
71
166
|
/**
|
|
72
|
-
* Look up the registered
|
|
73
|
-
*
|
|
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
|
-
):
|
|
78
|
-
return
|
|
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
|
-
|
|
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
|
+
};
|