@vellumai/assistant 0.4.6 → 0.4.7
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/ARCHITECTURE.md +23 -6
- package/bun.lock +51 -0
- package/docs/trusted-contact-access.md +8 -0
- package/package.json +2 -1
- package/src/__tests__/actor-token-service.test.ts +4 -4
- package/src/__tests__/call-controller.test.ts +37 -0
- package/src/__tests__/channel-delivery-store.test.ts +2 -2
- package/src/__tests__/gateway-client-managed-outbound.test.ts +147 -0
- package/src/__tests__/guardian-dispatch.test.ts +39 -1
- package/src/__tests__/guardian-routing-state.test.ts +8 -30
- package/src/__tests__/non-member-access-request.test.ts +7 -0
- package/src/__tests__/notification-decision-fallback.test.ts +232 -0
- package/src/__tests__/notification-decision-strategy.test.ts +304 -8
- package/src/__tests__/notification-guardian-path.test.ts +38 -1
- package/src/__tests__/relay-server.test.ts +65 -5
- package/src/__tests__/send-endpoint-busy.test.ts +29 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +1 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -2
- package/src/__tests__/trusted-contact-multichannel.test.ts +1 -1
- package/src/calls/call-controller.ts +15 -0
- package/src/calls/relay-server.ts +45 -11
- package/src/calls/types.ts +1 -0
- package/src/daemon/providers-setup.ts +0 -8
- package/src/daemon/session-slash.ts +35 -2
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/039-actor-refresh-token-records.ts +51 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +1 -1
- package/src/memory/schema.ts +19 -0
- package/src/notifications/README.md +8 -1
- package/src/notifications/copy-composer.ts +160 -30
- package/src/notifications/decision-engine.ts +98 -1
- package/src/runtime/actor-refresh-token-service.ts +309 -0
- package/src/runtime/actor-refresh-token-store.ts +157 -0
- package/src/runtime/actor-token-service.ts +3 -3
- package/src/runtime/gateway-client.ts +239 -0
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +10 -24
- package/src/runtime/routes/guardian-refresh-routes.ts +53 -0
- package/src/runtime/routes/pairing-routes.ts +60 -50
- package/src/types/qrcode.d.ts +10 -0
|
@@ -10,24 +10,28 @@ 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 {
|
|
14
|
-
import {
|
|
15
|
-
createActorTokenRecord,
|
|
16
|
-
revokeByDeviceBinding,
|
|
17
|
-
} from '../actor-token-store.js';
|
|
13
|
+
import { mintCredentialPair } from '../actor-refresh-token-service.js';
|
|
18
14
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../assistant-scope.js';
|
|
19
15
|
import { ensureVellumGuardianBinding } from '../guardian-vellum-migration.js';
|
|
20
16
|
import { httpError } from '../http-errors.js';
|
|
21
17
|
|
|
22
18
|
const log = getLogger('runtime-http');
|
|
23
19
|
|
|
20
|
+
interface PairingCredentials {
|
|
21
|
+
actorToken: string;
|
|
22
|
+
actorTokenExpiresAt: number;
|
|
23
|
+
refreshToken: string;
|
|
24
|
+
refreshTokenExpiresAt: number;
|
|
25
|
+
refreshAfter: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
24
28
|
/**
|
|
25
|
-
* Mint
|
|
26
|
-
* Returns the
|
|
29
|
+
* Mint credentials (access token + refresh token) for a paired device.
|
|
30
|
+
* Returns the full credential set, or null if minting fails.
|
|
27
31
|
*
|
|
28
32
|
* NOTE: This function MUST remain synchronous — the mintingInFlight guard depends on it.
|
|
29
33
|
*/
|
|
30
|
-
function
|
|
34
|
+
function mintPairingCredentials(deviceId: string, platform: string): PairingCredentials | null {
|
|
31
35
|
try {
|
|
32
36
|
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
33
37
|
// Pairing can run before a local client has touched the actor-token
|
|
@@ -36,30 +40,24 @@ function mintPairingActorToken(deviceId: string, platform: string): string | nul
|
|
|
36
40
|
const guardianPrincipalId = ensureVellumGuardianBinding(assistantId);
|
|
37
41
|
const hashedDeviceId = hashDeviceId(deviceId);
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
revokeByDeviceBinding(assistantId, guardianPrincipalId, hashedDeviceId);
|
|
41
|
-
|
|
42
|
-
const { token, tokenHash, claims } = mintActorToken({
|
|
43
|
+
const credentials = mintCredentialPair({
|
|
43
44
|
assistantId,
|
|
44
45
|
platform,
|
|
45
46
|
deviceId,
|
|
46
47
|
guardianPrincipalId,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
createActorTokenRecord({
|
|
50
|
-
tokenHash,
|
|
51
|
-
assistantId,
|
|
52
|
-
guardianPrincipalId,
|
|
53
48
|
hashedDeviceId,
|
|
54
|
-
platform,
|
|
55
|
-
issuedAt: claims.iat,
|
|
56
|
-
expiresAt: claims.exp,
|
|
57
49
|
});
|
|
58
50
|
|
|
59
|
-
log.info({ assistantId, platform }, 'Minted
|
|
60
|
-
return
|
|
51
|
+
log.info({ assistantId, platform }, 'Minted credentials during pairing');
|
|
52
|
+
return {
|
|
53
|
+
actorToken: credentials.actorToken,
|
|
54
|
+
actorTokenExpiresAt: credentials.actorTokenExpiresAt,
|
|
55
|
+
refreshToken: credentials.refreshToken,
|
|
56
|
+
refreshTokenExpiresAt: credentials.refreshTokenExpiresAt,
|
|
57
|
+
refreshAfter: credentials.refreshAfter,
|
|
58
|
+
};
|
|
61
59
|
} catch (err) {
|
|
62
|
-
log.warn({ err }, 'Failed to mint
|
|
60
|
+
log.warn({ err }, 'Failed to mint credentials during pairing — continuing without them');
|
|
63
61
|
return null;
|
|
64
62
|
}
|
|
65
63
|
}
|
|
@@ -75,24 +73,24 @@ const PENDING_DEVICE_ID_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
|
75
73
|
const pendingDeviceIds = new Map<string, { deviceId: string; createdAt: number }>();
|
|
76
74
|
|
|
77
75
|
/**
|
|
78
|
-
* Transient in-memory map of pairingRequestId -> {
|
|
79
|
-
* Populated when a pairing is approved and
|
|
80
|
-
* Entries are kept for
|
|
81
|
-
* subsequent polls can still retrieve
|
|
76
|
+
* Transient in-memory map of pairingRequestId -> { credentials, approvedAt }.
|
|
77
|
+
* Populated when a pairing is approved and credentials are minted.
|
|
78
|
+
* Entries are kept for CREDENTIAL_RETRIEVAL_TTL_MS after approval so that
|
|
79
|
+
* subsequent polls can still retrieve them if the first response
|
|
82
80
|
* was dropped or timed out.
|
|
83
81
|
*/
|
|
84
|
-
const
|
|
85
|
-
const
|
|
82
|
+
const CREDENTIAL_RETRIEVAL_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
83
|
+
const approvedCredentials = new Map<string, { credentials: PairingCredentials; approvedAt: number }>();
|
|
86
84
|
|
|
87
85
|
/**
|
|
88
|
-
* Sweep stale entries from the approved
|
|
86
|
+
* Sweep stale entries from the approved credentials map.
|
|
89
87
|
* Called lazily on each status poll.
|
|
90
88
|
*/
|
|
91
|
-
function
|
|
89
|
+
function sweepApprovedCredentials(): void {
|
|
92
90
|
const now = Date.now();
|
|
93
|
-
for (const [id, entry] of
|
|
94
|
-
if (now - entry.approvedAt >
|
|
95
|
-
|
|
91
|
+
for (const [id, entry] of approvedCredentials) {
|
|
92
|
+
if (now - entry.approvedAt > CREDENTIAL_RETRIEVAL_TTL_MS) {
|
|
93
|
+
approvedCredentials.delete(id);
|
|
96
94
|
}
|
|
97
95
|
}
|
|
98
96
|
}
|
|
@@ -127,7 +125,7 @@ const mintingInFlight = new Set<string>();
|
|
|
127
125
|
*/
|
|
128
126
|
export function cleanupPairingState(pairingRequestId: string): void {
|
|
129
127
|
pendingDeviceIds.delete(pairingRequestId);
|
|
130
|
-
|
|
128
|
+
approvedCredentials.delete(pairingRequestId);
|
|
131
129
|
mintingInFlight.delete(pairingRequestId);
|
|
132
130
|
}
|
|
133
131
|
|
|
@@ -207,14 +205,20 @@ export async function handlePairingRequest(req: Request, ctx: PairingHandlerCont
|
|
|
207
205
|
refreshDevice(hashedDeviceId, deviceName);
|
|
208
206
|
ctx.pairingStore.approve(pairingRequestId, ctx.bearerToken);
|
|
209
207
|
log.info({ pairingRequestId, hashedDeviceId }, 'Auto-approved allowlisted device');
|
|
210
|
-
const
|
|
208
|
+
const credentials = mintPairingCredentials(deviceId, 'ios');
|
|
211
209
|
return Response.json({
|
|
212
210
|
status: 'approved',
|
|
213
211
|
bearerToken: ctx.bearerToken,
|
|
214
212
|
gatewayUrl: entry.gatewayUrl,
|
|
215
213
|
localLanUrl: entry.localLanUrl,
|
|
216
214
|
...(ctx.featureFlagToken ? { featureFlagToken: ctx.featureFlagToken } : {}),
|
|
217
|
-
...(
|
|
215
|
+
...(credentials ? {
|
|
216
|
+
actorToken: credentials.actorToken,
|
|
217
|
+
actorTokenExpiresAt: credentials.actorTokenExpiresAt,
|
|
218
|
+
refreshToken: credentials.refreshToken,
|
|
219
|
+
refreshTokenExpiresAt: credentials.refreshTokenExpiresAt,
|
|
220
|
+
refreshAfter: credentials.refreshAfter,
|
|
221
|
+
} : {}),
|
|
218
222
|
});
|
|
219
223
|
}
|
|
220
224
|
|
|
@@ -260,7 +264,7 @@ export function handlePairingStatus(url: URL, ctx: PairingHandlerContext): Respo
|
|
|
260
264
|
|
|
261
265
|
// Sweep stale transient entries on every poll — not just approved ones —
|
|
262
266
|
// so abandoned pairing attempts don't accumulate indefinitely.
|
|
263
|
-
|
|
267
|
+
sweepApprovedCredentials();
|
|
264
268
|
sweepPendingDeviceIds();
|
|
265
269
|
|
|
266
270
|
const entry = ctx.pairingStore.get(id);
|
|
@@ -271,14 +275,14 @@ export function handlePairingStatus(url: URL, ctx: PairingHandlerContext): Respo
|
|
|
271
275
|
}
|
|
272
276
|
|
|
273
277
|
if (entry.status === 'approved') {
|
|
274
|
-
// Mint
|
|
275
|
-
// raw deviceId from the pairing request. Once minted,
|
|
276
|
-
// cached in
|
|
277
|
-
// still retrieve
|
|
278
|
+
// Mint credentials on first approved poll if we still have the
|
|
279
|
+
// raw deviceId from the pairing request. Once minted, credentials are
|
|
280
|
+
// cached in approvedCredentials with a TTL so subsequent polls can
|
|
281
|
+
// still retrieve them if the first response was dropped.
|
|
278
282
|
// The pending deviceId is only removed after a successful mint so
|
|
279
283
|
// transient failures allow retries on subsequent polls.
|
|
280
|
-
let
|
|
281
|
-
if (!
|
|
284
|
+
let credentialEntry = approvedCredentials.get(id);
|
|
285
|
+
if (!credentialEntry && !mintingInFlight.has(id)) {
|
|
282
286
|
const pending = pendingDeviceIds.get(id);
|
|
283
287
|
const deviceIdMatchesEntry = Boolean(
|
|
284
288
|
deviceId
|
|
@@ -289,11 +293,11 @@ export function handlePairingStatus(url: URL, ctx: PairingHandlerContext): Respo
|
|
|
289
293
|
if (mintDeviceId) {
|
|
290
294
|
mintingInFlight.add(id);
|
|
291
295
|
try {
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
296
|
+
const credentials = mintPairingCredentials(mintDeviceId, 'ios');
|
|
297
|
+
if (credentials) {
|
|
294
298
|
pendingDeviceIds.delete(id);
|
|
295
|
-
|
|
296
|
-
|
|
299
|
+
credentialEntry = { credentials, approvedAt: Date.now() };
|
|
300
|
+
approvedCredentials.set(id, credentialEntry);
|
|
297
301
|
}
|
|
298
302
|
} finally {
|
|
299
303
|
mintingInFlight.delete(id);
|
|
@@ -307,7 +311,13 @@ export function handlePairingStatus(url: URL, ctx: PairingHandlerContext): Respo
|
|
|
307
311
|
gatewayUrl: entry.gatewayUrl,
|
|
308
312
|
localLanUrl: entry.localLanUrl,
|
|
309
313
|
...(ctx.featureFlagToken ? { featureFlagToken: ctx.featureFlagToken } : {}),
|
|
310
|
-
...(
|
|
314
|
+
...(credentialEntry ? {
|
|
315
|
+
actorToken: credentialEntry.credentials.actorToken,
|
|
316
|
+
actorTokenExpiresAt: credentialEntry.credentials.actorTokenExpiresAt,
|
|
317
|
+
refreshToken: credentialEntry.credentials.refreshToken,
|
|
318
|
+
refreshTokenExpiresAt: credentialEntry.credentials.refreshTokenExpiresAt,
|
|
319
|
+
refreshAfter: credentialEntry.credentials.refreshAfter,
|
|
320
|
+
} : {}),
|
|
311
321
|
});
|
|
312
322
|
}
|
|
313
323
|
|