@viewportai/daemon 0.5.2 → 0.6.0

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 (109) hide show
  1. package/dist/cli/commands.d.ts +1 -0
  2. package/dist/cli/commands.d.ts.map +1 -1
  3. package/dist/cli/commands.js +1 -0
  4. package/dist/cli/commands.js.map +1 -1
  5. package/dist/cli/context-access-command.d.ts +0 -6
  6. package/dist/cli/context-access-command.d.ts.map +1 -1
  7. package/dist/cli/context-access-command.js +1 -71
  8. package/dist/cli/context-access-command.js.map +1 -1
  9. package/dist/cli/context-command.d.ts.map +1 -1
  10. package/dist/cli/context-command.js +593 -27
  11. package/dist/cli/context-command.js.map +1 -1
  12. package/dist/cli/context-sync-target.d.ts +2 -1
  13. package/dist/cli/context-sync-target.d.ts.map +1 -1
  14. package/dist/cli/context-sync-target.js +28 -0
  15. package/dist/cli/context-sync-target.js.map +1 -1
  16. package/dist/cli/context-vault-metadata-command.d.ts.map +1 -1
  17. package/dist/cli/context-vault-metadata-command.js +6 -1
  18. package/dist/cli/context-vault-metadata-command.js.map +1 -1
  19. package/dist/cli/lifecycle-commands.d.ts.map +1 -1
  20. package/dist/cli/lifecycle-commands.js +6 -6
  21. package/dist/cli/lifecycle-commands.js.map +1 -1
  22. package/dist/cli/unlock-command.d.ts +2 -0
  23. package/dist/cli/unlock-command.d.ts.map +1 -0
  24. package/dist/cli/unlock-command.js +35 -0
  25. package/dist/cli/unlock-command.js.map +1 -0
  26. package/dist/context/local-edge-store.d.ts +23 -1
  27. package/dist/context/local-edge-store.d.ts.map +1 -1
  28. package/dist/context/local-edge-store.js +51 -0
  29. package/dist/context/local-edge-store.js.map +1 -1
  30. package/dist/context/local-edge-sync.d.ts +63 -0
  31. package/dist/context/local-edge-sync.d.ts.map +1 -1
  32. package/dist/context/local-edge-sync.js +464 -4
  33. package/dist/context/local-edge-sync.js.map +1 -1
  34. package/dist/context/local-edge-types.d.ts +21 -0
  35. package/dist/context/local-edge-types.d.ts.map +1 -1
  36. package/dist/hooks/platform-plan-sync.d.ts +4 -1
  37. package/dist/hooks/platform-plan-sync.d.ts.map +1 -1
  38. package/dist/hooks/platform-plan-sync.js +20 -5
  39. package/dist/hooks/platform-plan-sync.js.map +1 -1
  40. package/dist/hooks/trusted-edge-plan-artifacts.d.ts +117 -0
  41. package/dist/hooks/trusted-edge-plan-artifacts.d.ts.map +1 -0
  42. package/dist/hooks/trusted-edge-plan-artifacts.js +371 -0
  43. package/dist/hooks/trusted-edge-plan-artifacts.js.map +1 -0
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +3 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/relay/bridge-token-issuer.d.ts +1 -0
  49. package/dist/relay/bridge-token-issuer.d.ts.map +1 -1
  50. package/dist/relay/bridge-token-issuer.js +1 -1
  51. package/dist/relay/bridge-token-issuer.js.map +1 -1
  52. package/dist/security/epoch-enrollment.d.ts +48 -0
  53. package/dist/security/epoch-enrollment.d.ts.map +1 -0
  54. package/dist/security/epoch-enrollment.js +290 -0
  55. package/dist/security/epoch-enrollment.js.map +1 -0
  56. package/dist/security/epoch-protocol.d.ts +181 -0
  57. package/dist/security/epoch-protocol.d.ts.map +1 -0
  58. package/dist/security/epoch-protocol.js +285 -0
  59. package/dist/security/epoch-protocol.js.map +1 -0
  60. package/dist/security/epoch-public-pins.d.ts +19 -0
  61. package/dist/security/epoch-public-pins.d.ts.map +1 -0
  62. package/dist/security/epoch-public-pins.js +129 -0
  63. package/dist/security/epoch-public-pins.js.map +1 -0
  64. package/dist/security/epoch-recovery.d.ts +56 -0
  65. package/dist/security/epoch-recovery.d.ts.map +1 -0
  66. package/dist/security/epoch-recovery.js +314 -0
  67. package/dist/security/epoch-recovery.js.map +1 -0
  68. package/dist/security/epoch-store.d.ts +111 -0
  69. package/dist/security/epoch-store.d.ts.map +1 -0
  70. package/dist/security/epoch-store.js +224 -0
  71. package/dist/security/epoch-store.js.map +1 -0
  72. package/dist/security/epoch-sync.d.ts +47 -0
  73. package/dist/security/epoch-sync.d.ts.map +1 -0
  74. package/dist/security/epoch-sync.js +371 -0
  75. package/dist/security/epoch-sync.js.map +1 -0
  76. package/dist/security/team-epoch-grants.d.ts +28 -0
  77. package/dist/security/team-epoch-grants.d.ts.map +1 -0
  78. package/dist/security/team-epoch-grants.js +256 -0
  79. package/dist/security/team-epoch-grants.js.map +1 -0
  80. package/dist/server/context-preview-service.d.ts +26 -0
  81. package/dist/server/context-preview-service.d.ts.map +1 -0
  82. package/dist/server/context-preview-service.js +71 -0
  83. package/dist/server/context-preview-service.js.map +1 -0
  84. package/dist/server/http-context-routes.d.ts +2 -1
  85. package/dist/server/http-context-routes.d.ts.map +1 -1
  86. package/dist/server/http-context-routes.js +65 -30
  87. package/dist/server/http-context-routes.js.map +1 -1
  88. package/dist/server/http-server.js +1 -1
  89. package/dist/server/http-server.js.map +1 -1
  90. package/dist/server/rate-limiter.d.ts.map +1 -1
  91. package/dist/server/rate-limiter.js +6 -1
  92. package/dist/server/rate-limiter.js.map +1 -1
  93. package/dist/server/trusted-edge-command-capability.d.ts +14 -0
  94. package/dist/server/trusted-edge-command-capability.d.ts.map +1 -0
  95. package/dist/server/trusted-edge-command-capability.js +114 -0
  96. package/dist/server/trusted-edge-command-capability.js.map +1 -0
  97. package/dist/server/ws-command-handlers.d.ts.map +1 -1
  98. package/dist/server/ws-command-handlers.js +231 -27
  99. package/dist/server/ws-command-handlers.js.map +1 -1
  100. package/dist/server/ws-protocol.d.ts +419 -5
  101. package/dist/server/ws-protocol.d.ts.map +1 -1
  102. package/dist/server/ws-protocol.js +141 -4
  103. package/dist/server/ws-protocol.js.map +1 -1
  104. package/docs/protocol-matrix.json +93 -5
  105. package/node_modules/@viewportai/context-engine/src/repo/materializer.js +20 -5
  106. package/node_modules/@viewportai/context-engine/src/repo/membership.js +15 -0
  107. package/node_modules/@viewportai/context-engine/src/repo/sync.js +4 -4
  108. package/node_modules/@viewportai/context-engine/src/repo/vault.js +8 -3
  109. package/package.json +1 -1
@@ -1,19 +1,27 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
1
3
  import { getArgs, getFlag, hasFlag } from './args.js';
2
4
  import { isJsonMode, printJson } from './command-shared.js';
3
- import { initContextResource, isResolverPinMismatch, readContextStatus, resolveContextBundle, } from '../context/local-edge-store.js';
5
+ import { initContextResource, isResolverPinMismatch, joinContextResource, readContextStatus, resolveContextBundle, } from '../context/local-edge-store.js';
4
6
  import { proposeContextEntry } from '../context/local-edge-candidates.js';
5
7
  import { readCandidateDecisionApplications } from '../context/local-edge-decision-applications.js';
6
- import { pullContextEvents, pushContextEvents } from '../context/local-edge-sync.js';
8
+ import { processPendingContextGrants, processPendingContextRevocations, pullContextEvents, pushContextEvents, } from '../context/local-edge-sync.js';
7
9
  import { resolveContextKeyStore } from '../context/local-edge-key-store.js';
8
- import { resolveContextSyncTarget } from './context-sync-target.js';
10
+ import { resolveContextSyncTarget, resolveWorkspaceSyncTarget } from './context-sync-target.js';
9
11
  import { parseLimit, parseMaxItems, parseSince } from './context-command-parsers.js';
12
+ import { transportFetch } from './network.js';
10
13
  import { contextAdd } from './context-add-command.js';
11
14
  import { contextGet, contextProviderPropose, contextSearch } from './context-provider-command.js';
12
15
  import { contextVaultCreate, contextVaultsList } from './context-vault-metadata-command.js';
13
16
  import { contextVaultUse } from './context-vault-use-command.js';
14
17
  import { contextCandidatePreview } from './context-candidate-preview-command.js';
15
18
  import { contextRulesInstall } from './context-rules-command.js';
16
- import { contextDeviceAccept, contextDeviceApprove, contextDeviceRequest, contextGrant, contextIdentityExport, contextIdentityImport, contextJoin, contextUserInit, } from './context-access-command.js';
19
+ import { acceptDeviceEpochEnrollment, approveDeviceEpochEnrollment, listDeviceEpochEnrollments, requestDeviceEpochEnrollment, } from '../security/epoch-enrollment.js';
20
+ import { ensureTeamCryptoEpoch, ensureUserCryptoEpoch, processPendingCryptoRotationRequests, rotateTeamCryptoEpoch, rotateUserCryptoEpoch, } from '../security/epoch-sync.js';
21
+ import { createUserEpochRecoveryBackup, generateUserEpochRecoveryKey, restoreUserEpochFromRecoveryBackup, } from '../security/epoch-recovery.js';
22
+ import { acceptTeamEpochMemberGrants, grantTeamEpochToUserEpoch, } from '../security/team-epoch-grants.js';
23
+ import { contextJoin, contextUserInit } from './context-access-command.js';
24
+ import { configDir } from '../core/config.js';
17
25
  export async function context() {
18
26
  const subcommand = getArgs()[1];
19
27
  if (!subcommand) {
@@ -72,54 +80,90 @@ export async function context() {
72
80
  await contextSyncPull();
73
81
  return;
74
82
  }
75
- if (subcommand === 'decisions') {
76
- await contextDecisions();
83
+ if (subcommand === 'sync-all') {
84
+ await contextSyncAll();
77
85
  return;
78
86
  }
79
- if (subcommand === 'candidate-preview') {
80
- await contextCandidatePreview();
87
+ if (subcommand === 'dev-reset-crypto') {
88
+ await contextDevResetCrypto();
81
89
  return;
82
90
  }
83
- if (subcommand === 'rules' && getArgs()[2] === 'install') {
84
- await contextRulesInstall();
91
+ if (subcommand === 'epoch-publish') {
92
+ await contextEpochPublish();
85
93
  return;
86
94
  }
87
- if (subcommand === 'user-init') {
88
- await contextUserInit();
95
+ if (subcommand === 'epoch-rotate') {
96
+ await contextEpochRotate();
89
97
  return;
90
98
  }
91
- if (subcommand === 'join') {
92
- await contextJoin();
99
+ if (subcommand === 'recovery-backup') {
100
+ await contextRecoveryBackup();
101
+ return;
102
+ }
103
+ if (subcommand === 'recovery-restore') {
104
+ await contextRecoveryRestore();
105
+ return;
106
+ }
107
+ if (subcommand === 'rotations-process') {
108
+ await contextRotationsProcess();
109
+ return;
110
+ }
111
+ if (subcommand === 'device-enroll-request') {
112
+ await contextDeviceEnrollRequest();
113
+ return;
114
+ }
115
+ if (subcommand === 'device-enroll-approve') {
116
+ await contextDeviceEnrollApprove();
117
+ return;
118
+ }
119
+ if (subcommand === 'device-enroll-accept') {
120
+ await contextDeviceEnrollAccept();
121
+ return;
122
+ }
123
+ if (subcommand === 'device-enrollments' || subcommand === 'device-enroll-status') {
124
+ await contextDeviceEnrollments();
125
+ return;
126
+ }
127
+ if (subcommand === 'team-grant-create') {
128
+ await contextTeamGrantCreate();
129
+ return;
130
+ }
131
+ if (subcommand === 'team-grants-accept') {
132
+ await contextTeamGrantsAccept();
93
133
  return;
94
134
  }
95
- if (subcommand === 'identity-export') {
96
- await contextIdentityExport();
135
+ if (subcommand === 'grants-process') {
136
+ await contextGrantsProcess();
97
137
  return;
98
138
  }
99
- if (subcommand === 'identity-import') {
100
- await contextIdentityImport();
139
+ if (subcommand === 'revokes-process') {
140
+ await contextRevokesProcess();
101
141
  return;
102
142
  }
103
- if (subcommand === 'device-request') {
104
- await contextDeviceRequest();
143
+ if (subcommand === 'decisions') {
144
+ await contextDecisions();
105
145
  return;
106
146
  }
107
- if (subcommand === 'device-approve') {
108
- await contextDeviceApprove();
147
+ if (subcommand === 'candidate-preview') {
148
+ await contextCandidatePreview();
109
149
  return;
110
150
  }
111
- if (subcommand === 'device-accept') {
112
- await contextDeviceAccept();
151
+ if (subcommand === 'rules' && getArgs()[2] === 'install') {
152
+ await contextRulesInstall();
113
153
  return;
114
154
  }
115
- if (subcommand === 'grant') {
116
- await contextGrant();
155
+ if (subcommand === 'user-init') {
156
+ await contextUserInit();
157
+ return;
158
+ }
159
+ if (subcommand === 'join') {
160
+ await contextJoin();
117
161
  return;
118
162
  }
119
163
  throw new Error(contextUsage());
120
164
  }
121
165
  function contextUsage() {
122
- return 'Usage: vpd context <create|vaults|use|init|status|add|search|get|propose|resolve|sync-push|sync-pull|decisions|candidate-preview|rules install|user-init|join|identity-export|identity-import|device-request|device-approve|device-accept|grant> ...';
166
+ return 'Usage: vpd context <create|vaults|use|init|status|add|search|get|propose|resolve|sync-push|sync-pull|sync-all|dev-reset-crypto --i-understand|epoch-publish [--team <team-id>]|epoch-rotate [--team <team-id>] [--reason <reason>]|recovery-backup [--recovery-key <key>]|recovery-restore --recovery-key <key>|rotations-process|device-enroll-request|device-enroll-approve|device-enroll-accept|device-enrollments|team-grant-create|team-grants-accept|grants-process|revokes-process|decisions|candidate-preview|rules install|user-init|join> ...';
123
167
  }
124
168
  function showContextHelp() {
125
169
  console.log(contextUsage());
@@ -251,6 +295,528 @@ async function contextSyncPull() {
251
295
  console.log(`Context events pulled: ${result.imported}/${result.pulled}`);
252
296
  console.log(`Repo: ${result.repoId}`);
253
297
  }
298
+ async function contextSyncAll() {
299
+ const target = await resolveWorkspaceSyncTarget('sync-all');
300
+ const home = getFlag('home');
301
+ const userName = requiredFlag('user', 'vpd context sync-all --user <name> --device <name>');
302
+ const deviceName = requiredFlag('device', 'vpd context sync-all --user <name> --device <name>');
303
+ const credentials = readCredentials({ required: false });
304
+ const keyStore = parseKeyStore(getFlag('key-store'));
305
+ const syncTarget = {
306
+ workspaceId: target.workspaceId,
307
+ serverUrl: target.serverUrl,
308
+ credential: target.credential,
309
+ tlsVerify: target.tlsVerify,
310
+ caCertPath: target.caCertPath,
311
+ tlsPins: target.tlsPins,
312
+ };
313
+ const rotations = await processPendingCryptoRotationRequests({
314
+ target: syncTarget,
315
+ home,
316
+ });
317
+ const acceptedTeamGrants = await acceptTeamEpochMemberGrants({
318
+ target: syncTarget,
319
+ home,
320
+ });
321
+ const vaults = await fetchVisibleContextVaults(target);
322
+ const results = [];
323
+ for (const vault of vaults) {
324
+ const contextResourceId = vault.vault_id;
325
+ if (!contextResourceId || vault.access?.can_view === false)
326
+ continue;
327
+ const status = await readContextStatus({ contextResourceId, home });
328
+ if (status.contexts.length === 0) {
329
+ await joinContextResource({
330
+ contextResourceId,
331
+ userName,
332
+ deviceName,
333
+ credentials,
334
+ keyStore,
335
+ home,
336
+ });
337
+ }
338
+ const pulled = await pullContextEvents({
339
+ contextResourceId,
340
+ workspaceId: target.workspaceId,
341
+ serverUrl: target.serverUrl,
342
+ credential: target.credential,
343
+ tlsVerify: target.tlsVerify,
344
+ caCertPath: target.caCertPath,
345
+ tlsPins: target.tlsPins,
346
+ actorName: deviceName,
347
+ credentials,
348
+ limit: parseLimit(getFlag('limit')),
349
+ home,
350
+ });
351
+ const revoked = await processPendingContextRevocations({
352
+ contextResourceId,
353
+ workspaceId: target.workspaceId,
354
+ serverUrl: target.serverUrl,
355
+ credential: target.credential,
356
+ tlsVerify: target.tlsVerify,
357
+ caCertPath: target.caCertPath,
358
+ tlsPins: target.tlsPins,
359
+ actorName: deviceName,
360
+ credentials,
361
+ home,
362
+ });
363
+ const granted = await processPendingContextGrants({
364
+ contextResourceId,
365
+ workspaceId: target.workspaceId,
366
+ serverUrl: target.serverUrl,
367
+ credential: target.credential,
368
+ tlsVerify: target.tlsVerify,
369
+ caCertPath: target.caCertPath,
370
+ tlsPins: target.tlsPins,
371
+ actorName: deviceName,
372
+ credentials,
373
+ home,
374
+ });
375
+ results.push({ contextResourceId, ...pulled, revoked, granted });
376
+ }
377
+ const summary = {
378
+ vaults: results.length,
379
+ pulled: results.reduce((total, item) => total + item.pulled, 0),
380
+ imported: results.reduce((total, item) => total + item.imported, 0),
381
+ materializedGrants: results.reduce((total, item) => total + item.materializedGrants, 0),
382
+ revocationsProcessed: results.reduce((total, item) => total + item.revoked.revoked, 0),
383
+ grantsEmitted: results.reduce((total, item) => total + item.granted.emitted, 0),
384
+ rotationsProcessed: rotations.processed,
385
+ teamEpochGrantsAccepted: acceptedTeamGrants.accepted,
386
+ };
387
+ if (isJsonMode()) {
388
+ printJson({
389
+ command: 'context sync-all',
390
+ ok: true,
391
+ workspaceId: target.workspaceId,
392
+ ...summary,
393
+ rotations,
394
+ acceptedTeamGrants,
395
+ results,
396
+ });
397
+ return;
398
+ }
399
+ console.log(`Context vaults synced: ${summary.vaults}`);
400
+ console.log(`Context events pulled: ${summary.imported}/${summary.pulled}`);
401
+ if (summary.materializedGrants > 0) {
402
+ console.log(`Context grants materialized: ${summary.materializedGrants}`);
403
+ }
404
+ if (summary.rotationsProcessed > 0) {
405
+ console.log(`Crypto rotations processed: ${summary.rotationsProcessed}`);
406
+ }
407
+ if (summary.teamEpochGrantsAccepted > 0) {
408
+ console.log(`Team epoch grants accepted: ${summary.teamEpochGrantsAccepted}`);
409
+ }
410
+ if (summary.revocationsProcessed > 0) {
411
+ console.log(`Context revocations processed: ${summary.revocationsProcessed}`);
412
+ }
413
+ if (summary.grantsEmitted > 0) {
414
+ console.log(`Context grants emitted: ${summary.grantsEmitted}`);
415
+ }
416
+ }
417
+ async function contextDevResetCrypto() {
418
+ if (!hasFlag('i-understand')) {
419
+ throw new Error('vpd context dev-reset-crypto removes local encrypted context, epoch, and plan key material. Re-run with --i-understand to continue.');
420
+ }
421
+ const home = getFlag('home') ?? configDir();
422
+ const targets = [
423
+ path.join(home, 'crypto', 'epochs.json'),
424
+ path.join(home, 'context', 'canonical-resources'),
425
+ path.join(home, 'context', 'candidate-decision-applications'),
426
+ path.join(home, 'repos'),
427
+ path.join(home, 'identities'),
428
+ path.join(home, 'plans', 'trusted-edge-keys.json'),
429
+ ];
430
+ const removed = [];
431
+ for (const target of targets) {
432
+ try {
433
+ await fs.rm(target, { recursive: true, force: true });
434
+ removed.push(path.relative(home, target) || target);
435
+ }
436
+ catch (error) {
437
+ throw new Error(`Failed to remove local crypto state at ${target}: ${error.message}`);
438
+ }
439
+ }
440
+ if (isJsonMode()) {
441
+ printJson({ command: 'context dev-reset-crypto', ok: true, home, removed });
442
+ return;
443
+ }
444
+ console.log(`Local encrypted collaboration state reset under ${home}`);
445
+ for (const item of removed) {
446
+ console.log(`Removed: ${item}`);
447
+ }
448
+ }
449
+ async function fetchVisibleContextVaults(target) {
450
+ const query = new URLSearchParams({ credential: target.credential });
451
+ const response = await transportFetch(`${target.serverUrl.replace(/\/+$/, '')}/api/runtime/workspaces/${encodeURIComponent(target.workspaceId)}/context-vaults?${query.toString()}`, {
452
+ method: 'GET',
453
+ headers: { accept: 'application/json' },
454
+ timeoutMs: 5_000,
455
+ tlsVerify: target.tlsVerify,
456
+ caCertPath: target.caCertPath,
457
+ tlsPins: target.tlsPins,
458
+ });
459
+ const payload = (await response.json());
460
+ if (!response.ok) {
461
+ throw new Error(`Failed to list context vaults for sync-all: HTTP ${response.status}`);
462
+ }
463
+ if (!Array.isArray(payload.data))
464
+ return [];
465
+ return payload.data
466
+ .filter((item) => !!item && typeof item === 'object' && !Array.isArray(item))
467
+ .map((item) => ({
468
+ vault_id: String(item.vault_id ?? ''),
469
+ access: item.access && typeof item.access === 'object' && !Array.isArray(item.access)
470
+ ? item.access
471
+ : null,
472
+ }))
473
+ .filter((item) => item.vault_id.length > 0);
474
+ }
475
+ async function contextEpochPublish() {
476
+ const target = await resolveWorkspaceSyncTarget('epoch-publish');
477
+ const syncTarget = {
478
+ workspaceId: target.workspaceId,
479
+ serverUrl: target.serverUrl,
480
+ credential: target.credential,
481
+ tlsVerify: target.tlsVerify,
482
+ caCertPath: target.caCertPath,
483
+ tlsPins: target.tlsPins,
484
+ };
485
+ const teamId = getFlag('team');
486
+ const epoch = teamId
487
+ ? await ensureTeamCryptoEpoch({
488
+ target: syncTarget,
489
+ teamId,
490
+ home: getFlag('home'),
491
+ })
492
+ : await ensureUserCryptoEpoch({
493
+ target: syncTarget,
494
+ home: getFlag('home'),
495
+ });
496
+ if (isJsonMode()) {
497
+ printJson({
498
+ command: 'context epoch-publish',
499
+ ok: true,
500
+ scope: teamId ? 'team' : 'user',
501
+ epoch,
502
+ });
503
+ return;
504
+ }
505
+ console.log(`${teamId ? 'Team' : 'User'} crypto epoch ready: ${epoch.fingerprint}`);
506
+ console.log(`Epoch: ${epoch.epoch}`);
507
+ }
508
+ async function contextEpochRotate() {
509
+ const target = await resolveWorkspaceSyncTarget('epoch-rotate');
510
+ const syncTarget = {
511
+ workspaceId: target.workspaceId,
512
+ serverUrl: target.serverUrl,
513
+ credential: target.credential,
514
+ tlsVerify: target.tlsVerify,
515
+ caCertPath: target.caCertPath,
516
+ tlsPins: target.tlsPins,
517
+ };
518
+ const reason = epochRotationReason(getFlag('reason') ?? 'manual_rotation');
519
+ const teamId = getFlag('team');
520
+ const epoch = teamId
521
+ ? await rotateTeamCryptoEpoch({
522
+ target: syncTarget,
523
+ teamId,
524
+ reason,
525
+ home: getFlag('home'),
526
+ })
527
+ : await rotateUserCryptoEpoch({
528
+ target: syncTarget,
529
+ reason,
530
+ home: getFlag('home'),
531
+ });
532
+ if (isJsonMode()) {
533
+ printJson({
534
+ command: 'context epoch-rotate',
535
+ ok: true,
536
+ scope: teamId ? 'team' : 'user',
537
+ reason,
538
+ epoch,
539
+ });
540
+ return;
541
+ }
542
+ console.log(`${teamId ? 'Team' : 'User'} crypto epoch rotated: ${epoch.fingerprint}`);
543
+ console.log(`Epoch: ${epoch.epoch}`);
544
+ console.log(`Reason: ${reason}`);
545
+ }
546
+ async function contextRecoveryBackup() {
547
+ const target = await resolveWorkspaceSyncTarget('recovery-backup');
548
+ const generatedRecoveryKey = getFlag('recovery-key') ? null : generateUserEpochRecoveryKey();
549
+ const recoveryKey = getFlag('recovery-key') ?? generatedRecoveryKey;
550
+ if (!recoveryKey) {
551
+ throw new Error('vpd context recovery-backup requires --recovery-key <key>');
552
+ }
553
+ const backup = await createUserEpochRecoveryBackup({
554
+ target: {
555
+ workspaceId: target.workspaceId,
556
+ serverUrl: target.serverUrl,
557
+ credential: target.credential,
558
+ tlsVerify: target.tlsVerify,
559
+ caCertPath: target.caCertPath,
560
+ tlsPins: target.tlsPins,
561
+ },
562
+ recoveryKey,
563
+ home: getFlag('home'),
564
+ });
565
+ if (isJsonMode()) {
566
+ printJson({
567
+ command: 'context recovery-backup',
568
+ ok: true,
569
+ backup,
570
+ generatedRecoveryKey,
571
+ });
572
+ return;
573
+ }
574
+ console.log(`Recovery backup stored: ${backup.id}`);
575
+ console.log(`User epoch: ${backup.user_crypto_epoch_id}`);
576
+ if (generatedRecoveryKey) {
577
+ console.log('Recovery key generated. Store it somewhere private; Viewport cannot recover it.');
578
+ console.log(generatedRecoveryKey);
579
+ }
580
+ }
581
+ async function contextRecoveryRestore() {
582
+ const target = await resolveWorkspaceSyncTarget('recovery-restore');
583
+ const recoveryKey = requiredFlag('recovery-key', 'vpd context recovery-restore --recovery-key <key>');
584
+ const result = await restoreUserEpochFromRecoveryBackup({
585
+ target: {
586
+ workspaceId: target.workspaceId,
587
+ serverUrl: target.serverUrl,
588
+ credential: target.credential,
589
+ tlsVerify: target.tlsVerify,
590
+ caCertPath: target.caCertPath,
591
+ tlsPins: target.tlsPins,
592
+ },
593
+ recoveryKey,
594
+ home: getFlag('home'),
595
+ });
596
+ if (isJsonMode()) {
597
+ printJson({ command: 'context recovery-restore', ok: true, ...result });
598
+ return;
599
+ }
600
+ console.log(`Recovery backup restored: ${result.backup.id}`);
601
+ console.log(`Recovered epoch: ${result.restoredEpoch.fingerprint}`);
602
+ console.log(`Rotated epoch: ${result.rotatedEpoch.fingerprint}`);
603
+ console.log(`Fresh recovery backup stored: ${result.rotatedBackup.id}`);
604
+ }
605
+ async function contextRotationsProcess() {
606
+ const target = await resolveWorkspaceSyncTarget('rotations-process');
607
+ const result = await processPendingCryptoRotationRequests({
608
+ target: {
609
+ workspaceId: target.workspaceId,
610
+ serverUrl: target.serverUrl,
611
+ credential: target.credential,
612
+ tlsVerify: target.tlsVerify,
613
+ caCertPath: target.caCertPath,
614
+ tlsPins: target.tlsPins,
615
+ },
616
+ home: getFlag('home'),
617
+ });
618
+ if (isJsonMode()) {
619
+ printJson({ command: 'context rotations-process', ok: true, ...result });
620
+ return;
621
+ }
622
+ console.log(`Crypto rotation requests processed: ${result.processed}`);
623
+ if (result.userRotations > 0) {
624
+ console.log(`User epoch rotations: ${result.userRotations}`);
625
+ }
626
+ if (result.teamRotations > 0) {
627
+ console.log(`Team epoch rotations: ${result.teamRotations}`);
628
+ }
629
+ if (result.teamMemberGrants > 0) {
630
+ console.log(`Team epoch member grants created: ${result.teamMemberGrants}`);
631
+ }
632
+ if (result.skipped > 0) {
633
+ console.log(`Skipped rotation requests: ${result.skipped}`);
634
+ }
635
+ }
636
+ function epochRotationReason(value) {
637
+ if (value === 'device_revoked' ||
638
+ value === 'member_added' ||
639
+ value === 'member_revoked' ||
640
+ value === 'manual_rotation' ||
641
+ value === 'recovery') {
642
+ return value;
643
+ }
644
+ throw new Error('Epoch rotation reason must be device_revoked, member_added, member_revoked, manual_rotation, or recovery.');
645
+ }
646
+ async function contextDeviceEnrollRequest() {
647
+ const target = await resolveWorkspaceSyncTarget('device-enroll-request');
648
+ const deviceId = requiredFlag('device', 'vpd context device-enroll-request --device <id>');
649
+ const enrollment = await requestDeviceEpochEnrollment({
650
+ target: {
651
+ workspaceId: target.workspaceId,
652
+ serverUrl: target.serverUrl,
653
+ credential: target.credential,
654
+ tlsVerify: target.tlsVerify,
655
+ caCertPath: target.caCertPath,
656
+ tlsPins: target.tlsPins,
657
+ },
658
+ deviceId,
659
+ deviceLabel: getFlag('label') ?? deviceId,
660
+ home: getFlag('home'),
661
+ });
662
+ if (isJsonMode()) {
663
+ printJson({ command: 'context device-enroll-request', ok: true, enrollment });
664
+ return;
665
+ }
666
+ console.log(`Device enrollment requested: ${enrollment.enrollmentId}`);
667
+ console.log(`Fingerprint: ${enrollment.fingerprint}`);
668
+ }
669
+ async function contextDeviceEnrollApprove() {
670
+ const target = await resolveWorkspaceSyncTarget('device-enroll-approve');
671
+ const enrollment = await approveDeviceEpochEnrollment({
672
+ target: {
673
+ workspaceId: target.workspaceId,
674
+ serverUrl: target.serverUrl,
675
+ credential: target.credential,
676
+ tlsVerify: target.tlsVerify,
677
+ caCertPath: target.caCertPath,
678
+ tlsPins: target.tlsPins,
679
+ },
680
+ enrollmentId: requiredFlag('enrollment', 'vpd context device-enroll-approve --enrollment <id>'),
681
+ home: getFlag('home'),
682
+ });
683
+ if (isJsonMode()) {
684
+ printJson({ command: 'context device-enroll-approve', ok: true, enrollment });
685
+ return;
686
+ }
687
+ console.log(`Device enrollment approved: ${enrollment.id}`);
688
+ console.log(`Status: ${enrollment.status}`);
689
+ }
690
+ async function contextDeviceEnrollAccept() {
691
+ const target = await resolveWorkspaceSyncTarget('device-enroll-accept');
692
+ const epoch = await acceptDeviceEpochEnrollment({
693
+ target: {
694
+ workspaceId: target.workspaceId,
695
+ serverUrl: target.serverUrl,
696
+ credential: target.credential,
697
+ tlsVerify: target.tlsVerify,
698
+ caCertPath: target.caCertPath,
699
+ tlsPins: target.tlsPins,
700
+ },
701
+ enrollmentId: requiredFlag('enrollment', 'vpd context device-enroll-accept --enrollment <id>'),
702
+ home: getFlag('home'),
703
+ });
704
+ if (isJsonMode()) {
705
+ printJson({ command: 'context device-enroll-accept', ok: true, epoch });
706
+ return;
707
+ }
708
+ console.log(`Device enrollment accepted. User crypto epoch ready: ${epoch.fingerprint}`);
709
+ }
710
+ async function contextDeviceEnrollments() {
711
+ const target = await resolveWorkspaceSyncTarget('device-enrollments');
712
+ const enrollments = await listDeviceEpochEnrollments({
713
+ target: {
714
+ workspaceId: target.workspaceId,
715
+ serverUrl: target.serverUrl,
716
+ credential: target.credential,
717
+ tlsVerify: target.tlsVerify,
718
+ caCertPath: target.caCertPath,
719
+ tlsPins: target.tlsPins,
720
+ },
721
+ });
722
+ if (isJsonMode()) {
723
+ printJson({ command: 'context device-enrollments', ok: true, enrollments });
724
+ return;
725
+ }
726
+ if (enrollments.length === 0) {
727
+ console.log('No device enrollments found.');
728
+ return;
729
+ }
730
+ for (const enrollment of enrollments) {
731
+ console.log(`${enrollment.id} ${enrollment.status} ${enrollment.device_label} ${enrollment.fingerprint}`);
732
+ }
733
+ }
734
+ async function contextTeamGrantCreate() {
735
+ const target = await resolveWorkspaceSyncTarget('team-grant-create');
736
+ const grant = await grantTeamEpochToUserEpoch({
737
+ target: {
738
+ workspaceId: target.workspaceId,
739
+ serverUrl: target.serverUrl,
740
+ credential: target.credential,
741
+ tlsVerify: target.tlsVerify,
742
+ caCertPath: target.caCertPath,
743
+ tlsPins: target.tlsPins,
744
+ },
745
+ teamCryptoEpochId: requiredFlag('team-epoch', 'vpd context team-grant-create --team-epoch <id> --recipient-epoch <id>'),
746
+ recipientUserCryptoEpochId: requiredFlag('recipient-epoch', 'vpd context team-grant-create --team-epoch <id> --recipient-epoch <id>'),
747
+ home: getFlag('home'),
748
+ });
749
+ if (isJsonMode()) {
750
+ printJson({ command: 'context team-grant-create', ok: true, grant });
751
+ return;
752
+ }
753
+ console.log(`Team epoch member grant created: ${grant.id}`);
754
+ }
755
+ async function contextTeamGrantsAccept() {
756
+ const target = await resolveWorkspaceSyncTarget('team-grants-accept');
757
+ const result = await acceptTeamEpochMemberGrants({
758
+ target: {
759
+ workspaceId: target.workspaceId,
760
+ serverUrl: target.serverUrl,
761
+ credential: target.credential,
762
+ tlsVerify: target.tlsVerify,
763
+ caCertPath: target.caCertPath,
764
+ tlsPins: target.tlsPins,
765
+ },
766
+ home: getFlag('home'),
767
+ });
768
+ if (isJsonMode()) {
769
+ printJson({ command: 'context team-grants-accept', ok: true, ...result });
770
+ return;
771
+ }
772
+ console.log(`Team epoch grants accepted: ${result.accepted}`);
773
+ }
774
+ async function contextGrantsProcess() {
775
+ const target = await resolveContextSyncTarget('grants-process');
776
+ const result = await processPendingContextGrants({
777
+ contextResourceId: target.contextResourceId,
778
+ workspaceId: target.workspaceId,
779
+ serverUrl: target.serverUrl,
780
+ credential: target.credential,
781
+ tlsVerify: target.tlsVerify,
782
+ caCertPath: target.caCertPath,
783
+ tlsPins: target.tlsPins,
784
+ actorName: getFlag('actor') ?? getFlag('device') ?? 'local-device',
785
+ credentials: readCredentials(),
786
+ home: getFlag('home'),
787
+ });
788
+ if (isJsonMode()) {
789
+ printJson({ command: 'context grants-process', ok: true, ...result });
790
+ return;
791
+ }
792
+ console.log(`Context grants emitted: ${result.emitted}`);
793
+ if (result.missingIdentity > 0) {
794
+ console.log(`Missing recipient identities: ${result.missingIdentity}`);
795
+ }
796
+ }
797
+ async function contextRevokesProcess() {
798
+ const target = await resolveContextSyncTarget('revokes-process');
799
+ const result = await processPendingContextRevocations({
800
+ contextResourceId: target.contextResourceId,
801
+ workspaceId: target.workspaceId,
802
+ serverUrl: target.serverUrl,
803
+ credential: target.credential,
804
+ tlsVerify: target.tlsVerify,
805
+ caCertPath: target.caCertPath,
806
+ tlsPins: target.tlsPins,
807
+ actorName: getFlag('actor') ?? getFlag('device') ?? 'local-device',
808
+ credentials: readCredentials(),
809
+ home: getFlag('home'),
810
+ });
811
+ if (isJsonMode()) {
812
+ printJson({ command: 'context revokes-process', ok: true, ...result });
813
+ return;
814
+ }
815
+ console.log(`Context revocations processed: ${result.revoked}`);
816
+ if (result.missingIdentity > 0) {
817
+ console.log(`Missing recipient identities: ${result.missingIdentity}`);
818
+ }
819
+ }
254
820
  async function contextDecisions() {
255
821
  const since = parseSince(getFlag('since'));
256
822
  const applications = await readCandidateDecisionApplications({