@vellumai/assistant 0.4.2 → 0.4.4
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/.env.example +3 -0
- package/ARCHITECTURE.md +124 -10
- package/README.md +43 -35
- package/docs/trusted-contact-access.md +20 -0
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +1 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
- package/src/__tests__/access-request-decision.test.ts +0 -1
- package/src/__tests__/actor-token-service.test.ts +1099 -0
- package/src/__tests__/agent-loop.test.ts +51 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
- package/src/__tests__/assistant-id-boundary-guard.test.ts +415 -0
- package/src/__tests__/call-controller.test.ts +49 -0
- package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
- package/src/__tests__/call-pointer-messages.test.ts +93 -3
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
- package/src/__tests__/call-routes-http.test.ts +0 -25
- package/src/__tests__/callback-handoff-copy.test.ts +186 -0
- package/src/__tests__/channel-approval-routes.test.ts +133 -12
- package/src/__tests__/channel-guardian.test.ts +0 -86
- package/src/__tests__/channel-readiness-service.test.ts +10 -16
- package/src/__tests__/checker.test.ts +33 -12
- package/src/__tests__/config-schema.test.ts +6 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
- package/src/__tests__/conversation-routes.test.ts +12 -3
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/daemon-server-session-init.test.ts +4 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +39 -13
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -5
- package/src/__tests__/guardian-question-mode.test.ts +200 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
- package/src/__tests__/guardian-routing-state.test.ts +525 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
- package/src/__tests__/handlers-telegram-config.test.ts +0 -83
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
- package/src/__tests__/headless-browser-navigate.test.ts +2 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
- package/src/__tests__/ingress-routes-http.test.ts +55 -0
- package/src/__tests__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +159 -9
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +106 -2
- package/src/__tests__/notification-guardian-path.test.ts +3 -0
- package/src/__tests__/recording-intent-handler.test.ts +1 -0
- package/src/__tests__/relay-server.test.ts +1475 -33
- package/src/__tests__/send-endpoint-busy.test.ts +5 -0
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-confirmation-signals.test.ts +523 -0
- package/src/__tests__/session-init.benchmark.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +4 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +44 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +21 -2
- package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +11 -1
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +2 -13
- package/src/__tests__/twilio-routes.test.ts +4 -3
- package/src/__tests__/update-bulletin.test.ts +0 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +12 -3
- package/src/approvals/guardian-request-resolvers.ts +169 -11
- package/src/calls/call-constants.ts +29 -0
- package/src/calls/call-controller.ts +11 -3
- package/src/calls/call-domain.ts +33 -11
- package/src/calls/call-pointer-message-composer.ts +154 -0
- package/src/calls/call-pointer-messages.ts +106 -27
- package/src/calls/guardian-dispatch.ts +4 -2
- package/src/calls/relay-server.ts +921 -112
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +4 -6
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-session-bridge.ts +4 -3
- package/src/cli/core-commands.ts +7 -4
- package/src/cli.ts +5 -4
- package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +309 -10
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
- package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
- package/src/config/bundled-skills/messaging/SKILL.md +61 -12
- package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
- package/src/config/bundled-skills/twitter/SKILL.md +3 -3
- package/src/config/bundled-skills/vercel-token-setup/SKILL.md +215 -0
- package/src/config/calls-schema.ts +36 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -8
- package/src/config/schema.ts +2 -2
- package/src/config/skills.ts +11 -0
- package/src/config/system-prompt.ts +11 -1
- package/src/config/templates/SOUL.md +2 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -1
- package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
- package/src/daemon/call-pointer-generators.ts +59 -0
- package/src/daemon/computer-use-session.ts +2 -5
- package/src/daemon/handlers/apps.ts +76 -20
- package/src/daemon/handlers/config-channels.ts +9 -61
- package/src/daemon/handlers/config-inbox.ts +11 -3
- package/src/daemon/handlers/config-ingress.ts +28 -3
- package/src/daemon/handlers/config-telegram.ts +12 -0
- package/src/daemon/handlers/config.ts +2 -6
- package/src/daemon/handlers/index.ts +2 -1
- package/src/daemon/handlers/pairing.ts +2 -0
- package/src/daemon/handlers/publish.ts +11 -46
- package/src/daemon/handlers/sessions.ts +59 -5
- package/src/daemon/handlers/shared.ts +17 -2
- package/src/daemon/ipc-contract/apps.ts +1 -0
- package/src/daemon/ipc-contract/inbox.ts +4 -0
- package/src/daemon/ipc-contract/integrations.ts +1 -97
- package/src/daemon/ipc-contract/messages.ts +47 -1
- package/src/daemon/ipc-contract/notifications.ts +11 -0
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +17 -0
- package/src/daemon/server.ts +16 -2
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +24 -12
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +6 -1
- package/src/daemon/session-surfaces.ts +32 -3
- package/src/daemon/session.ts +88 -1
- package/src/daemon/tool-side-effects.ts +22 -0
- package/src/home-base/prebuilt/brain-graph.html +1483 -0
- package/src/home-base/prebuilt/index.html +40 -0
- package/src/inbound/platform-callback-registration.ts +157 -0
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/conversation-crud.ts +2 -1
- package/src/memory/conversation-title-service.ts +16 -2
- package/src/memory/db-init.ts +8 -0
- package/src/memory/delivery-crud.ts +2 -1
- package/src/memory/guardian-action-store.ts +2 -1
- package/src/memory/guardian-approvals.ts +3 -2
- package/src/memory/ingress-invite-store.ts +12 -2
- package/src/memory/ingress-member-store.ts +4 -3
- package/src/memory/migrations/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema.ts +26 -5
- package/src/messaging/provider-types.ts +24 -0
- package/src/messaging/provider.ts +7 -0
- package/src/messaging/providers/gmail/adapter.ts +127 -0
- package/src/messaging/providers/sms/adapter.ts +40 -37
- package/src/notifications/adapters/macos.ts +45 -2
- package/src/notifications/broadcaster.ts +16 -0
- package/src/notifications/copy-composer.ts +50 -2
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +18 -9
- package/src/notifications/guardian-question-mode.ts +419 -0
- package/src/notifications/signal.ts +14 -3
- package/src/permissions/checker.ts +13 -1
- package/src/permissions/prompter.ts +14 -0
- package/src/providers/anthropic/client.ts +20 -0
- package/src/providers/provider-send-message.ts +15 -3
- package/src/runtime/access-request-helper.ts +82 -4
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -0
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/assistant-scope.ts +10 -0
- package/src/runtime/channel-approvals.ts +5 -3
- package/src/runtime/channel-readiness-service.ts +23 -64
- package/src/runtime/channel-readiness-types.ts +3 -4
- package/src/runtime/channel-retry-sweep.ts +4 -1
- package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-context-resolver.ts +82 -0
- package/src/runtime/guardian-outbound-actions.ts +5 -7
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +75 -31
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/ingress-service.ts +14 -0
- package/src/runtime/invite-redemption-service.ts +10 -1
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/middleware/twilio-validation.ts +2 -4
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- package/src/runtime/routes/channel-route-shared.ts +3 -3
- package/src/runtime/routes/conversation-attention-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +142 -53
- package/src/runtime/routes/events-routes.ts +22 -8
- package/src/runtime/routes/guardian-action-routes.ts +45 -3
- package/src/runtime/routes/guardian-approval-interception.ts +29 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
- package/src/runtime/routes/inbound-conversation.ts +4 -3
- package/src/runtime/routes/inbound-message-handler.ts +147 -5
- package/src/runtime/routes/ingress-routes.ts +2 -0
- package/src/runtime/routes/integration-routes.ts +7 -15
- package/src/runtime/routes/pairing-routes.ts +163 -0
- package/src/runtime/routes/twilio-routes.ts +934 -0
- package/src/runtime/tool-grant-request-helper.ts +3 -1
- package/src/security/oauth2.ts +27 -2
- package/src/security/token-manager.ts +46 -10
- package/src/tools/browser/browser-execution.ts +4 -3
- package/src/tools/browser/browser-handoff.ts +10 -18
- package/src/tools/browser/browser-manager.ts +80 -25
- package/src/tools/browser/browser-screencast.ts +35 -119
- package/src/tools/calls/call-start.ts +2 -1
- package/src/tools/permission-checker.ts +15 -4
- package/src/tools/terminal/parser.ts +12 -0
- package/src/tools/tool-approval-handler.ts +244 -19
- package/src/workspace/git-service.ts +19 -0
- package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
- package/src/daemon/handlers/config-twilio.ts +0 -1082
|
@@ -144,16 +144,15 @@ export function handleClearSlackChannelConfig(): Response {
|
|
|
144
144
|
/**
|
|
145
145
|
* POST /v1/integrations/guardian/challenge
|
|
146
146
|
*
|
|
147
|
-
* Body: { channel?: ChannelId;
|
|
147
|
+
* Body: { channel?: ChannelId; rebind?: boolean; sessionId?: string }
|
|
148
148
|
*/
|
|
149
149
|
export async function handleCreateGuardianChallenge(req: Request): Promise<Response> {
|
|
150
150
|
const body = (await req.json()) as {
|
|
151
151
|
channel?: ChannelId;
|
|
152
|
-
assistantId?: string;
|
|
153
152
|
rebind?: boolean;
|
|
154
153
|
sessionId?: string;
|
|
155
154
|
};
|
|
156
|
-
const result = createGuardianChallenge(body.channel, body.
|
|
155
|
+
const result = createGuardianChallenge(body.channel, body.rebind, body.sessionId);
|
|
157
156
|
const status = result.success ? 200 : 400;
|
|
158
157
|
return Response.json(result, { status });
|
|
159
158
|
}
|
|
@@ -161,12 +160,11 @@ export async function handleCreateGuardianChallenge(req: Request): Promise<Respo
|
|
|
161
160
|
/**
|
|
162
161
|
* GET /v1/integrations/guardian/status
|
|
163
162
|
*
|
|
164
|
-
* Query params: channel
|
|
163
|
+
* Query params: channel?
|
|
165
164
|
*/
|
|
166
165
|
export function handleGetGuardianStatus(url: URL): Response {
|
|
167
166
|
const channel = (url.searchParams.get('channel') as ChannelId | null) ?? undefined;
|
|
168
|
-
const
|
|
169
|
-
const result = getGuardianStatus(channel, assistantId);
|
|
167
|
+
const result = getGuardianStatus(channel);
|
|
170
168
|
return Response.json(result);
|
|
171
169
|
}
|
|
172
170
|
|
|
@@ -177,13 +175,12 @@ export function handleGetGuardianStatus(url: URL): Response {
|
|
|
177
175
|
/**
|
|
178
176
|
* POST /v1/integrations/guardian/outbound/start
|
|
179
177
|
*
|
|
180
|
-
* Body: { channel: ChannelId; destination?: string;
|
|
178
|
+
* Body: { channel: ChannelId; destination?: string; rebind?: boolean; originConversationId?: string }
|
|
181
179
|
*/
|
|
182
180
|
export async function handleStartOutbound(req: Request): Promise<Response> {
|
|
183
181
|
const body = (await req.json()) as {
|
|
184
182
|
channel?: ChannelId;
|
|
185
183
|
destination?: string;
|
|
186
|
-
assistantId?: string;
|
|
187
184
|
rebind?: boolean;
|
|
188
185
|
originConversationId?: string;
|
|
189
186
|
};
|
|
@@ -209,7 +206,6 @@ export async function handleStartOutbound(req: Request): Promise<Response> {
|
|
|
209
206
|
const result = startOutbound({
|
|
210
207
|
channel: body.channel,
|
|
211
208
|
destination: body.destination,
|
|
212
|
-
assistantId: body.assistantId,
|
|
213
209
|
rebind: body.rebind,
|
|
214
210
|
originConversationId: body.originConversationId,
|
|
215
211
|
});
|
|
@@ -225,12 +221,11 @@ export async function handleStartOutbound(req: Request): Promise<Response> {
|
|
|
225
221
|
/**
|
|
226
222
|
* POST /v1/integrations/guardian/outbound/resend
|
|
227
223
|
*
|
|
228
|
-
* Body: { channel: ChannelId;
|
|
224
|
+
* Body: { channel: ChannelId; originConversationId?: string }
|
|
229
225
|
*/
|
|
230
226
|
export async function handleResendOutbound(req: Request): Promise<Response> {
|
|
231
227
|
const body = (await req.json()) as {
|
|
232
228
|
channel?: ChannelId;
|
|
233
|
-
assistantId?: string;
|
|
234
229
|
originConversationId?: string;
|
|
235
230
|
};
|
|
236
231
|
if (!body.channel) {
|
|
@@ -238,7 +233,6 @@ export async function handleResendOutbound(req: Request): Promise<Response> {
|
|
|
238
233
|
}
|
|
239
234
|
const result = resendOutbound({
|
|
240
235
|
channel: body.channel,
|
|
241
|
-
assistantId: body.assistantId,
|
|
242
236
|
originConversationId: body.originConversationId,
|
|
243
237
|
});
|
|
244
238
|
const status = result.success ? 200 : (result.error === 'rate_limited' ? 429 : 400);
|
|
@@ -248,19 +242,17 @@ export async function handleResendOutbound(req: Request): Promise<Response> {
|
|
|
248
242
|
/**
|
|
249
243
|
* POST /v1/integrations/guardian/outbound/cancel
|
|
250
244
|
*
|
|
251
|
-
* Body: { channel: ChannelId
|
|
245
|
+
* Body: { channel: ChannelId }
|
|
252
246
|
*/
|
|
253
247
|
export async function handleCancelOutbound(req: Request): Promise<Response> {
|
|
254
248
|
const body = (await req.json()) as {
|
|
255
249
|
channel?: ChannelId;
|
|
256
|
-
assistantId?: string;
|
|
257
250
|
};
|
|
258
251
|
if (!body.channel) {
|
|
259
252
|
return httpError('BAD_REQUEST', 'The "channel" field is required.', 400);
|
|
260
253
|
}
|
|
261
254
|
const result = cancelOutbound({
|
|
262
255
|
channel: body.channel,
|
|
263
|
-
assistantId: body.assistantId,
|
|
264
256
|
});
|
|
265
257
|
const status = result.success ? 200 : 400;
|
|
266
258
|
return Response.json(result, { status });
|
|
@@ -10,10 +10,127 @@ import {
|
|
|
10
10
|
import type { ServerMessage } from '../../daemon/ipc-contract.js';
|
|
11
11
|
import { PairingStore } from '../../daemon/pairing-store.js';
|
|
12
12
|
import { getLogger } from '../../util/logger.js';
|
|
13
|
+
import { mintActorToken } from '../actor-token-service.js';
|
|
14
|
+
import {
|
|
15
|
+
createActorTokenRecord,
|
|
16
|
+
revokeByDeviceBinding,
|
|
17
|
+
} from '../actor-token-store.js';
|
|
18
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
19
|
+
import { ensureVellumGuardianBinding } from '../guardian-vellum-migration.js';
|
|
13
20
|
import { httpError } from '../http-errors.js';
|
|
14
21
|
|
|
15
22
|
const log = getLogger('runtime-http');
|
|
16
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Mint an actor token for a paired device if a vellum guardian principal exists.
|
|
26
|
+
* Returns the raw actor token string, or null if no vellum binding exists.
|
|
27
|
+
*
|
|
28
|
+
* NOTE: This function MUST remain synchronous — the mintingInFlight guard depends on it.
|
|
29
|
+
*/
|
|
30
|
+
function mintPairingActorToken(deviceId: string, platform: string): string | null {
|
|
31
|
+
try {
|
|
32
|
+
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
33
|
+
// Pairing can run before a local client has touched the actor-token
|
|
34
|
+
// bootstrap path. Ensure the vellum guardian principal exists so iOS
|
|
35
|
+
// pairings always have a mint target.
|
|
36
|
+
const guardianPrincipalId = ensureVellumGuardianBinding(assistantId);
|
|
37
|
+
const hashedDeviceId = hashDeviceId(deviceId);
|
|
38
|
+
|
|
39
|
+
// Revoke previous tokens for this device
|
|
40
|
+
revokeByDeviceBinding(assistantId, guardianPrincipalId, hashedDeviceId);
|
|
41
|
+
|
|
42
|
+
const { token, tokenHash, claims } = mintActorToken({
|
|
43
|
+
assistantId,
|
|
44
|
+
platform,
|
|
45
|
+
deviceId,
|
|
46
|
+
guardianPrincipalId,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
createActorTokenRecord({
|
|
50
|
+
tokenHash,
|
|
51
|
+
assistantId,
|
|
52
|
+
guardianPrincipalId,
|
|
53
|
+
hashedDeviceId,
|
|
54
|
+
platform,
|
|
55
|
+
issuedAt: claims.iat,
|
|
56
|
+
expiresAt: claims.exp,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
log.info({ assistantId, platform }, 'Minted actor token during pairing');
|
|
60
|
+
return token;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
log.warn({ err }, 'Failed to mint actor token during pairing — continuing without it');
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Transient in-memory map of pairingRequestId -> { deviceId, createdAt }.
|
|
69
|
+
* Stored when a pairing request is initiated (we have the raw deviceId)
|
|
70
|
+
* so the token can be minted later when the pairing is actually approved.
|
|
71
|
+
* Entries include a timestamp so stale entries can be swept if the
|
|
72
|
+
* corresponding pairing expires without an explicit deny.
|
|
73
|
+
*/
|
|
74
|
+
const PENDING_DEVICE_ID_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
75
|
+
const pendingDeviceIds = new Map<string, { deviceId: string; createdAt: number }>();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Transient in-memory map of pairingRequestId -> { actorToken, approvedAt }.
|
|
79
|
+
* Populated when a pairing is approved and the actor token is minted.
|
|
80
|
+
* Entries are kept for TOKEN_RETRIEVAL_TTL_MS after approval so that
|
|
81
|
+
* subsequent polls can still retrieve the token if the first response
|
|
82
|
+
* was dropped or timed out.
|
|
83
|
+
*/
|
|
84
|
+
const TOKEN_RETRIEVAL_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
85
|
+
const approvedActorTokens = new Map<string, { actorToken: string; approvedAt: number }>();
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Sweep stale entries from the approved actor tokens map.
|
|
89
|
+
* Called lazily on each status poll.
|
|
90
|
+
*/
|
|
91
|
+
function sweepApprovedTokens(): void {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
for (const [id, entry] of approvedActorTokens) {
|
|
94
|
+
if (now - entry.approvedAt > TOKEN_RETRIEVAL_TTL_MS) {
|
|
95
|
+
approvedActorTokens.delete(id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Sweep stale entries from the pending device IDs map.
|
|
102
|
+
* Entries older than PENDING_DEVICE_ID_TTL_MS are removed to prevent
|
|
103
|
+
* unbounded accumulation of raw device identifiers when pairings expire
|
|
104
|
+
* without an explicit deny.
|
|
105
|
+
*/
|
|
106
|
+
function sweepPendingDeviceIds(): void {
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
for (const [id, entry] of pendingDeviceIds) {
|
|
109
|
+
if (now - entry.createdAt > PENDING_DEVICE_ID_TTL_MS) {
|
|
110
|
+
pendingDeviceIds.delete(id);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* In-flight mint guard — prevents overlapping status polls from triggering
|
|
117
|
+
* concurrent token mints for the same pairing request. The second mint
|
|
118
|
+
* would revoke the first token, leaving the client with an invalid token.
|
|
119
|
+
*
|
|
120
|
+
* MUST remain synchronous — async would break this concurrency guard.
|
|
121
|
+
*/
|
|
122
|
+
const mintingInFlight = new Set<string>();
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Clean up all transient pairing state for a given request.
|
|
126
|
+
* Called when pairing is denied or otherwise finalized.
|
|
127
|
+
*/
|
|
128
|
+
export function cleanupPairingState(pairingRequestId: string): void {
|
|
129
|
+
pendingDeviceIds.delete(pairingRequestId);
|
|
130
|
+
approvedActorTokens.delete(pairingRequestId);
|
|
131
|
+
mintingInFlight.delete(pairingRequestId);
|
|
132
|
+
}
|
|
133
|
+
|
|
17
134
|
export interface PairingHandlerContext {
|
|
18
135
|
pairingStore: PairingStore;
|
|
19
136
|
bearerToken: string | undefined;
|
|
@@ -90,15 +207,22 @@ export async function handlePairingRequest(req: Request, ctx: PairingHandlerCont
|
|
|
90
207
|
refreshDevice(hashedDeviceId, deviceName);
|
|
91
208
|
ctx.pairingStore.approve(pairingRequestId, ctx.bearerToken);
|
|
92
209
|
log.info({ pairingRequestId, hashedDeviceId }, 'Auto-approved allowlisted device');
|
|
210
|
+
const actorToken = mintPairingActorToken(deviceId, 'ios');
|
|
93
211
|
return Response.json({
|
|
94
212
|
status: 'approved',
|
|
95
213
|
bearerToken: ctx.bearerToken,
|
|
96
214
|
gatewayUrl: entry.gatewayUrl,
|
|
97
215
|
localLanUrl: entry.localLanUrl,
|
|
98
216
|
...(ctx.featureFlagToken ? { featureFlagToken: ctx.featureFlagToken } : {}),
|
|
217
|
+
...(actorToken ? { actorToken } : {}),
|
|
99
218
|
});
|
|
100
219
|
}
|
|
101
220
|
|
|
221
|
+
// Store the raw deviceId transiently so we can mint the actor token
|
|
222
|
+
// later when the pairing is actually approved (avoids revoking existing
|
|
223
|
+
// tokens and creating DB records for unapproved devices).
|
|
224
|
+
pendingDeviceIds.set(pairingRequestId, { deviceId, createdAt: Date.now() });
|
|
225
|
+
|
|
102
226
|
// Send IPC to macOS to show approval prompt
|
|
103
227
|
if (ctx.pairingBroadcast) {
|
|
104
228
|
ctx.pairingBroadcast({
|
|
@@ -124,6 +248,7 @@ export function handlePairingStatus(url: URL, ctx: PairingHandlerContext): Respo
|
|
|
124
248
|
const id = url.searchParams.get('id') ?? '';
|
|
125
249
|
// Note: secret is redacted from logs
|
|
126
250
|
const secret = url.searchParams.get('secret') ?? '';
|
|
251
|
+
const deviceId = (url.searchParams.get('deviceId') ?? '').trim();
|
|
127
252
|
|
|
128
253
|
if (!id || !secret) {
|
|
129
254
|
return httpError('BAD_REQUEST', 'Missing required params: id, secret', 400);
|
|
@@ -133,18 +258,56 @@ export function handlePairingStatus(url: URL, ctx: PairingHandlerContext): Respo
|
|
|
133
258
|
return httpError('FORBIDDEN', 'Forbidden', 403);
|
|
134
259
|
}
|
|
135
260
|
|
|
261
|
+
// Sweep stale transient entries on every poll — not just approved ones —
|
|
262
|
+
// so abandoned pairing attempts don't accumulate indefinitely.
|
|
263
|
+
sweepApprovedTokens();
|
|
264
|
+
sweepPendingDeviceIds();
|
|
265
|
+
|
|
136
266
|
const entry = ctx.pairingStore.get(id);
|
|
137
267
|
if (!entry) {
|
|
268
|
+
// Pairing expired or was swept — clean up any lingering pending device ID
|
|
269
|
+
pendingDeviceIds.delete(id);
|
|
138
270
|
return httpError('NOT_FOUND', 'Not found', 404);
|
|
139
271
|
}
|
|
140
272
|
|
|
141
273
|
if (entry.status === 'approved') {
|
|
274
|
+
// Mint the actor token on first approved poll if we still have the
|
|
275
|
+
// raw deviceId from the pairing request. Once minted, the token is
|
|
276
|
+
// cached in approvedActorTokens with a TTL so subsequent polls can
|
|
277
|
+
// still retrieve it if the first response was dropped.
|
|
278
|
+
// The pending deviceId is only removed after a successful mint so
|
|
279
|
+
// transient failures allow retries on subsequent polls.
|
|
280
|
+
let tokenEntry = approvedActorTokens.get(id);
|
|
281
|
+
if (!tokenEntry && !mintingInFlight.has(id)) {
|
|
282
|
+
const pending = pendingDeviceIds.get(id);
|
|
283
|
+
const deviceIdMatchesEntry = Boolean(
|
|
284
|
+
deviceId
|
|
285
|
+
&& entry.hashedDeviceId
|
|
286
|
+
&& hashDeviceId(deviceId) === entry.hashedDeviceId,
|
|
287
|
+
);
|
|
288
|
+
const mintDeviceId = pending?.deviceId ?? (deviceIdMatchesEntry ? deviceId : undefined);
|
|
289
|
+
if (mintDeviceId) {
|
|
290
|
+
mintingInFlight.add(id);
|
|
291
|
+
try {
|
|
292
|
+
const actorToken = mintPairingActorToken(mintDeviceId, 'ios');
|
|
293
|
+
if (actorToken) {
|
|
294
|
+
pendingDeviceIds.delete(id);
|
|
295
|
+
tokenEntry = { actorToken, approvedAt: Date.now() };
|
|
296
|
+
approvedActorTokens.set(id, tokenEntry);
|
|
297
|
+
}
|
|
298
|
+
} finally {
|
|
299
|
+
mintingInFlight.delete(id);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
142
304
|
return Response.json({
|
|
143
305
|
status: 'approved',
|
|
144
306
|
bearerToken: entry.bearerToken,
|
|
145
307
|
gatewayUrl: entry.gatewayUrl,
|
|
146
308
|
localLanUrl: entry.localLanUrl,
|
|
147
309
|
...(ctx.featureFlagToken ? { featureFlagToken: ctx.featureFlagToken } : {}),
|
|
310
|
+
...(tokenEntry ? { actorToken: tokenEntry.actorToken } : {}),
|
|
148
311
|
});
|
|
149
312
|
}
|
|
150
313
|
|