@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.
Files changed (42) hide show
  1. package/ARCHITECTURE.md +23 -6
  2. package/bun.lock +51 -0
  3. package/docs/trusted-contact-access.md +8 -0
  4. package/package.json +2 -1
  5. package/src/__tests__/actor-token-service.test.ts +4 -4
  6. package/src/__tests__/call-controller.test.ts +37 -0
  7. package/src/__tests__/channel-delivery-store.test.ts +2 -2
  8. package/src/__tests__/gateway-client-managed-outbound.test.ts +147 -0
  9. package/src/__tests__/guardian-dispatch.test.ts +39 -1
  10. package/src/__tests__/guardian-routing-state.test.ts +8 -30
  11. package/src/__tests__/non-member-access-request.test.ts +7 -0
  12. package/src/__tests__/notification-decision-fallback.test.ts +232 -0
  13. package/src/__tests__/notification-decision-strategy.test.ts +304 -8
  14. package/src/__tests__/notification-guardian-path.test.ts +38 -1
  15. package/src/__tests__/relay-server.test.ts +65 -5
  16. package/src/__tests__/send-endpoint-busy.test.ts +29 -1
  17. package/src/__tests__/tool-grant-request-escalation.test.ts +1 -0
  18. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -0
  19. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -2
  20. package/src/__tests__/trusted-contact-multichannel.test.ts +1 -1
  21. package/src/calls/call-controller.ts +15 -0
  22. package/src/calls/relay-server.ts +45 -11
  23. package/src/calls/types.ts +1 -0
  24. package/src/daemon/providers-setup.ts +0 -8
  25. package/src/daemon/session-slash.ts +35 -2
  26. package/src/memory/db-init.ts +4 -0
  27. package/src/memory/migrations/039-actor-refresh-token-records.ts +51 -0
  28. package/src/memory/migrations/index.ts +1 -0
  29. package/src/memory/migrations/registry.ts +1 -1
  30. package/src/memory/schema.ts +19 -0
  31. package/src/notifications/README.md +8 -1
  32. package/src/notifications/copy-composer.ts +160 -30
  33. package/src/notifications/decision-engine.ts +98 -1
  34. package/src/runtime/actor-refresh-token-service.ts +309 -0
  35. package/src/runtime/actor-refresh-token-store.ts +157 -0
  36. package/src/runtime/actor-token-service.ts +3 -3
  37. package/src/runtime/gateway-client.ts +239 -0
  38. package/src/runtime/http-server.ts +2 -0
  39. package/src/runtime/routes/guardian-bootstrap-routes.ts +10 -24
  40. package/src/runtime/routes/guardian-refresh-routes.ts +53 -0
  41. package/src/runtime/routes/pairing-routes.ts +60 -50
  42. 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 { mintActorToken } from '../actor-token-service.js';
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 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.
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 mintPairingActorToken(deviceId: string, platform: string): string | null {
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
- // Revoke previous tokens for this device
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 actor token during pairing');
60
- return token;
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 actor token during pairing — continuing without it');
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 -> { 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
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 TOKEN_RETRIEVAL_TTL_MS = 5 * 60 * 1000; // 5 minutes
85
- const approvedActorTokens = new Map<string, { actorToken: string; approvedAt: number }>();
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 actor tokens map.
86
+ * Sweep stale entries from the approved credentials map.
89
87
  * Called lazily on each status poll.
90
88
  */
91
- function sweepApprovedTokens(): void {
89
+ function sweepApprovedCredentials(): void {
92
90
  const now = Date.now();
93
- for (const [id, entry] of approvedActorTokens) {
94
- if (now - entry.approvedAt > TOKEN_RETRIEVAL_TTL_MS) {
95
- approvedActorTokens.delete(id);
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
- approvedActorTokens.delete(pairingRequestId);
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 actorToken = mintPairingActorToken(deviceId, 'ios');
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
- ...(actorToken ? { actorToken } : {}),
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
- sweepApprovedTokens();
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 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
+ // 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 tokenEntry = approvedActorTokens.get(id);
281
- if (!tokenEntry && !mintingInFlight.has(id)) {
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 actorToken = mintPairingActorToken(mintDeviceId, 'ios');
293
- if (actorToken) {
296
+ const credentials = mintPairingCredentials(mintDeviceId, 'ios');
297
+ if (credentials) {
294
298
  pendingDeviceIds.delete(id);
295
- tokenEntry = { actorToken, approvedAt: Date.now() };
296
- approvedActorTokens.set(id, tokenEntry);
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
- ...(tokenEntry ? { actorToken: tokenEntry.actorToken } : {}),
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
 
@@ -0,0 +1,10 @@
1
+ declare module 'qrcode' {
2
+ interface QRCodeToBufferOptions {
3
+ type?: 'png';
4
+ width?: number;
5
+ }
6
+
7
+ function toBuffer(text: string, options?: QRCodeToBufferOptions): Promise<Buffer>;
8
+
9
+ export default { toBuffer };
10
+ }