@viewportai/daemon 0.5.3 → 0.6.1

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 (118) 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 +575 -38
  11. package/dist/cli/context-command.js.map +1 -1
  12. package/dist/cli/context-vault-metadata-command.d.ts.map +1 -1
  13. package/dist/cli/context-vault-metadata-command.js +6 -1
  14. package/dist/cli/context-vault-metadata-command.js.map +1 -1
  15. package/dist/cli/lifecycle-commands.d.ts.map +1 -1
  16. package/dist/cli/lifecycle-commands.js +2 -8
  17. package/dist/cli/lifecycle-commands.js.map +1 -1
  18. package/dist/cli/skills-command.js +3 -3
  19. package/dist/cli/unlock-command.d.ts +2 -0
  20. package/dist/cli/unlock-command.d.ts.map +1 -0
  21. package/dist/cli/unlock-command.js +35 -0
  22. package/dist/cli/unlock-command.js.map +1 -0
  23. package/dist/context/local-edge-auto-sync.d.ts +17 -0
  24. package/dist/context/local-edge-auto-sync.d.ts.map +1 -0
  25. package/dist/context/local-edge-auto-sync.js +94 -0
  26. package/dist/context/local-edge-auto-sync.js.map +1 -0
  27. package/dist/context/local-edge-store.d.ts +11 -0
  28. package/dist/context/local-edge-store.d.ts.map +1 -1
  29. package/dist/context/local-edge-store.js +25 -0
  30. package/dist/context/local-edge-store.js.map +1 -1
  31. package/dist/context/local-edge-sync.d.ts +2 -15
  32. package/dist/context/local-edge-sync.d.ts.map +1 -1
  33. package/dist/context/local-edge-sync.js +306 -86
  34. package/dist/context/local-edge-sync.js.map +1 -1
  35. package/dist/context/local-edge-types.d.ts +12 -0
  36. package/dist/context/local-edge-types.d.ts.map +1 -1
  37. package/dist/context-providers/viewport-vault-provider.d.ts.map +1 -1
  38. package/dist/context-providers/viewport-vault-provider.js +11 -0
  39. package/dist/context-providers/viewport-vault-provider.js.map +1 -1
  40. package/dist/core/session-context-prompt.d.ts.map +1 -1
  41. package/dist/core/session-context-prompt.js +8 -0
  42. package/dist/core/session-context-prompt.js.map +1 -1
  43. package/dist/hooks/trusted-edge-plan-artifacts.d.ts +30 -27
  44. package/dist/hooks/trusted-edge-plan-artifacts.d.ts.map +1 -1
  45. package/dist/hooks/trusted-edge-plan-artifacts.js +71 -89
  46. package/dist/hooks/trusted-edge-plan-artifacts.js.map +1 -1
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +3 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/relay/bridge-daemon-key-registration.d.ts.map +1 -1
  52. package/dist/relay/bridge-daemon-key-registration.js +27 -7
  53. package/dist/relay/bridge-daemon-key-registration.js.map +1 -1
  54. package/dist/security/epoch-enrollment.d.ts +48 -0
  55. package/dist/security/epoch-enrollment.d.ts.map +1 -0
  56. package/dist/security/epoch-enrollment.js +290 -0
  57. package/dist/security/epoch-enrollment.js.map +1 -0
  58. package/dist/security/epoch-protocol.d.ts +181 -0
  59. package/dist/security/epoch-protocol.d.ts.map +1 -0
  60. package/dist/security/epoch-protocol.js +285 -0
  61. package/dist/security/epoch-protocol.js.map +1 -0
  62. package/dist/security/epoch-public-pins.d.ts +19 -0
  63. package/dist/security/epoch-public-pins.d.ts.map +1 -0
  64. package/dist/security/epoch-public-pins.js +129 -0
  65. package/dist/security/epoch-public-pins.js.map +1 -0
  66. package/dist/security/epoch-recovery.d.ts +56 -0
  67. package/dist/security/epoch-recovery.d.ts.map +1 -0
  68. package/dist/security/epoch-recovery.js +314 -0
  69. package/dist/security/epoch-recovery.js.map +1 -0
  70. package/dist/security/epoch-store.d.ts +111 -0
  71. package/dist/security/epoch-store.d.ts.map +1 -0
  72. package/dist/security/epoch-store.js +224 -0
  73. package/dist/security/epoch-store.js.map +1 -0
  74. package/dist/security/epoch-sync.d.ts +47 -0
  75. package/dist/security/epoch-sync.d.ts.map +1 -0
  76. package/dist/security/epoch-sync.js +371 -0
  77. package/dist/security/epoch-sync.js.map +1 -0
  78. package/dist/security/team-epoch-grant-payloads.d.ts +44 -0
  79. package/dist/security/team-epoch-grant-payloads.d.ts.map +1 -0
  80. package/dist/security/team-epoch-grant-payloads.js +100 -0
  81. package/dist/security/team-epoch-grant-payloads.js.map +1 -0
  82. package/dist/security/team-epoch-grants.d.ts +31 -0
  83. package/dist/security/team-epoch-grants.d.ts.map +1 -0
  84. package/dist/security/team-epoch-grants.js +194 -0
  85. package/dist/security/team-epoch-grants.js.map +1 -0
  86. package/dist/server/http-context-routes.d.ts +2 -1
  87. package/dist/server/http-context-routes.d.ts.map +1 -1
  88. package/dist/server/http-context-routes.js +57 -15
  89. package/dist/server/http-context-routes.js.map +1 -1
  90. package/dist/server/http-server.js +1 -1
  91. package/dist/server/http-server.js.map +1 -1
  92. package/dist/server/rate-limiter.d.ts.map +1 -1
  93. package/dist/server/rate-limiter.js +2 -1
  94. package/dist/server/rate-limiter.js.map +1 -1
  95. package/dist/server/trusted-edge-command-capability.d.ts +2 -1
  96. package/dist/server/trusted-edge-command-capability.d.ts.map +1 -1
  97. package/dist/server/trusted-edge-command-capability.js +15 -0
  98. package/dist/server/trusted-edge-command-capability.js.map +1 -1
  99. package/dist/server/ws-command-handlers.d.ts.map +1 -1
  100. package/dist/server/ws-command-handlers.js +200 -28
  101. package/dist/server/ws-command-handlers.js.map +1 -1
  102. package/dist/server/ws-protocol.d.ts +281 -44
  103. package/dist/server/ws-protocol.d.ts.map +1 -1
  104. package/dist/server/ws-protocol.js +89 -19
  105. package/dist/server/ws-protocol.js.map +1 -1
  106. package/dist/startup.d.ts.map +1 -1
  107. package/dist/startup.js +0 -17
  108. package/dist/startup.js.map +1 -1
  109. package/docs/README.md +18 -0
  110. package/docs/configuration.md +3 -3
  111. package/docs/protocol-matrix.json +53 -8
  112. package/docs/security.md +11 -8
  113. package/node_modules/@viewportai/context-engine/src/repo/identities.js +7 -3
  114. package/node_modules/@viewportai/context-engine/src/repo/materializer.js +20 -5
  115. package/node_modules/@viewportai/context-engine/src/repo/membership.js +15 -0
  116. package/node_modules/@viewportai/context-engine/src/repo/sync.js +4 -4
  117. package/node_modules/@viewportai/context-engine/src/repo/vault.js +8 -3
  118. 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 { processPendingContextGrants, processPendingContextRevocations, publishContextPublicIdentity, 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
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, grantTeamEpochToWorkspaceUserEpochs, 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,8 +80,56 @@ export async function context() {
72
80
  await contextSyncPull();
73
81
  return;
74
82
  }
75
- if (subcommand === 'identity-publish') {
76
- await contextIdentityPublish();
83
+ if (subcommand === 'sync-all') {
84
+ await contextSyncAll();
85
+ return;
86
+ }
87
+ if (subcommand === 'dev-reset-crypto') {
88
+ await contextDevResetCrypto();
89
+ return;
90
+ }
91
+ if (subcommand === 'epoch-publish') {
92
+ await contextEpochPublish();
93
+ return;
94
+ }
95
+ if (subcommand === 'epoch-rotate') {
96
+ await contextEpochRotate();
97
+ return;
98
+ }
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();
77
133
  return;
78
134
  }
79
135
  if (subcommand === 'grants-process') {
@@ -104,34 +160,10 @@ export async function context() {
104
160
  await contextJoin();
105
161
  return;
106
162
  }
107
- if (subcommand === 'identity-export') {
108
- await contextIdentityExport();
109
- return;
110
- }
111
- if (subcommand === 'identity-import') {
112
- await contextIdentityImport();
113
- return;
114
- }
115
- if (subcommand === 'device-request') {
116
- await contextDeviceRequest();
117
- return;
118
- }
119
- if (subcommand === 'device-approve') {
120
- await contextDeviceApprove();
121
- return;
122
- }
123
- if (subcommand === 'device-accept') {
124
- await contextDeviceAccept();
125
- return;
126
- }
127
- if (subcommand === 'grant') {
128
- await contextGrant();
129
- return;
130
- }
131
163
  throw new Error(contextUsage());
132
164
  }
133
165
  function contextUsage() {
134
- return 'Usage: vpd context <create|vaults|use|init|status|add|search|get|propose|resolve|sync-push|sync-pull|identity-publish|grants-process|revokes-process|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> ...';
135
167
  }
136
168
  function showContextHelp() {
137
169
  console.log(contextUsage());
@@ -263,25 +295,530 @@ async function contextSyncPull() {
263
295
  console.log(`Context events pulled: ${result.imported}/${result.pulled}`);
264
296
  console.log(`Repo: ${result.repoId}`);
265
297
  }
266
- async function contextIdentityPublish() {
267
- const target = await resolveWorkspaceSyncTarget('identity-publish');
268
- const result = await publishContextPublicIdentity({
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
+ accepted: acceptedTeamGrants.accepted,
396
+ teamEpochs: acceptedTeamGrants.teamEpochs.map(publicEpochForOutput),
397
+ },
398
+ results,
399
+ });
400
+ return;
401
+ }
402
+ console.log(`Context vaults synced: ${summary.vaults}`);
403
+ console.log(`Context events pulled: ${summary.imported}/${summary.pulled}`);
404
+ if (summary.materializedGrants > 0) {
405
+ console.log(`Context grants materialized: ${summary.materializedGrants}`);
406
+ }
407
+ if (summary.rotationsProcessed > 0) {
408
+ console.log(`Crypto rotations processed: ${summary.rotationsProcessed}`);
409
+ }
410
+ if (summary.teamEpochGrantsAccepted > 0) {
411
+ console.log(`Team epoch grants accepted: ${summary.teamEpochGrantsAccepted}`);
412
+ }
413
+ if (summary.revocationsProcessed > 0) {
414
+ console.log(`Context revocations processed: ${summary.revocationsProcessed}`);
415
+ }
416
+ if (summary.grantsEmitted > 0) {
417
+ console.log(`Context grants emitted: ${summary.grantsEmitted}`);
418
+ }
419
+ }
420
+ async function contextDevResetCrypto() {
421
+ if (!hasFlag('i-understand')) {
422
+ throw new Error('vpd context dev-reset-crypto removes local encrypted context, epoch, and plan key material. Re-run with --i-understand to continue.');
423
+ }
424
+ const home = getFlag('home') ?? configDir();
425
+ const targets = [
426
+ path.join(home, 'crypto', 'epochs.json'),
427
+ path.join(home, 'context', 'canonical-resources'),
428
+ path.join(home, 'context', 'candidate-decision-applications'),
429
+ path.join(home, 'repos'),
430
+ path.join(home, 'identities'),
431
+ path.join(home, 'plans', 'trusted-edge-keys.json'),
432
+ ];
433
+ const removed = [];
434
+ for (const target of targets) {
435
+ try {
436
+ await fs.rm(target, { recursive: true, force: true });
437
+ removed.push(path.relative(home, target) || target);
438
+ }
439
+ catch (error) {
440
+ throw new Error(`Failed to remove local crypto state at ${target}: ${error.message}`);
441
+ }
442
+ }
443
+ if (isJsonMode()) {
444
+ printJson({ command: 'context dev-reset-crypto', ok: true, home, removed });
445
+ return;
446
+ }
447
+ console.log(`Local encrypted collaboration state reset under ${home}`);
448
+ for (const item of removed) {
449
+ console.log(`Removed: ${item}`);
450
+ }
451
+ }
452
+ async function fetchVisibleContextVaults(target) {
453
+ const query = new URLSearchParams({ credential: target.credential });
454
+ const response = await transportFetch(`${target.serverUrl.replace(/\/+$/, '')}/api/runtime/workspaces/${encodeURIComponent(target.workspaceId)}/context-vaults?${query.toString()}`, {
455
+ method: 'GET',
456
+ headers: {
457
+ accept: 'application/json',
458
+ 'X-Viewport-Crypto-Protocol': 'viewport.trusted_edge_crypto/v2',
459
+ },
460
+ timeoutMs: 5_000,
461
+ tlsVerify: target.tlsVerify,
462
+ caCertPath: target.caCertPath,
463
+ tlsPins: target.tlsPins,
464
+ });
465
+ const payload = (await response.json());
466
+ if (!response.ok) {
467
+ throw new Error(`Failed to list context vaults for sync-all: HTTP ${response.status}`);
468
+ }
469
+ if (!Array.isArray(payload.data))
470
+ return [];
471
+ return payload.data
472
+ .filter((item) => !!item && typeof item === 'object' && !Array.isArray(item))
473
+ .map((item) => ({
474
+ vault_id: String(item.vault_id ?? ''),
475
+ access: item.access && typeof item.access === 'object' && !Array.isArray(item.access)
476
+ ? item.access
477
+ : null,
478
+ }))
479
+ .filter((item) => item.vault_id.length > 0);
480
+ }
481
+ async function contextEpochPublish() {
482
+ const target = await resolveWorkspaceSyncTarget('epoch-publish');
483
+ const syncTarget = {
484
+ workspaceId: target.workspaceId,
485
+ serverUrl: target.serverUrl,
486
+ credential: target.credential,
487
+ tlsVerify: target.tlsVerify,
488
+ caCertPath: target.caCertPath,
489
+ tlsPins: target.tlsPins,
490
+ };
491
+ const teamId = getFlag('team');
492
+ let teamMemberGrants = null;
493
+ const epoch = teamId
494
+ ? await ensureTeamCryptoEpoch({
495
+ target: syncTarget,
496
+ teamId,
497
+ home: getFlag('home'),
498
+ })
499
+ : await ensureUserCryptoEpoch({
500
+ target: syncTarget,
501
+ home: getFlag('home'),
502
+ });
503
+ if (teamId && 'platformEpochId' in epoch && epoch.platformEpochId) {
504
+ teamMemberGrants = await grantTeamEpochToWorkspaceUserEpochs({
505
+ target: syncTarget,
506
+ teamCryptoEpochId: epoch.platformEpochId,
507
+ home: getFlag('home'),
508
+ });
509
+ }
510
+ if (isJsonMode()) {
511
+ printJson({
512
+ command: 'context epoch-publish',
513
+ ok: true,
514
+ scope: teamId ? 'team' : 'user',
515
+ epoch: publicEpochForOutput(epoch),
516
+ ...(teamMemberGrants
517
+ ? {
518
+ teamMemberGrants: {
519
+ attempted: teamMemberGrants.attempted,
520
+ granted: teamMemberGrants.granted,
521
+ skipped: teamMemberGrants.skipped,
522
+ },
523
+ }
524
+ : {}),
525
+ });
526
+ return;
527
+ }
528
+ console.log(`${teamId ? 'Team' : 'User'} crypto epoch ready: ${epoch.fingerprint}`);
529
+ console.log(`Epoch: ${epoch.epoch}`);
530
+ if (teamMemberGrants) {
531
+ console.log(`Team epoch member grants: ${teamMemberGrants.granted}/${teamMemberGrants.attempted}`);
532
+ }
533
+ }
534
+ function publicEpochForOutput(epoch) {
535
+ return {
536
+ workspaceId: epoch.workspaceId,
537
+ userId: 'userId' in epoch ? epoch.userId : undefined,
538
+ teamId: 'teamId' in epoch ? epoch.teamId : undefined,
539
+ platformTeamId: 'platformTeamId' in epoch ? (epoch.platformTeamId ?? null) : undefined,
540
+ platformEpochId: epoch.platformEpochId ?? null,
541
+ epoch: epoch.epoch,
542
+ schema: epoch.schema,
543
+ status: epoch.status,
544
+ encryptionPublicKeyJwk: epoch.encryptionPublicKeyJwk,
545
+ signingPublicKeyJwk: epoch.signingPublicKeyJwk,
546
+ fingerprint: epoch.fingerprint,
547
+ previousEpochFingerprint: epoch.previousEpochFingerprint ?? null,
548
+ createdAt: epoch.createdAt,
549
+ updatedAt: epoch.updatedAt,
550
+ };
551
+ }
552
+ async function contextEpochRotate() {
553
+ const target = await resolveWorkspaceSyncTarget('epoch-rotate');
554
+ const syncTarget = {
269
555
  workspaceId: target.workspaceId,
270
556
  serverUrl: target.serverUrl,
271
557
  credential: target.credential,
272
- identityName: requiredFlag('name', 'vpd context identity-publish --name <identity>'),
273
558
  tlsVerify: target.tlsVerify,
274
559
  caCertPath: target.caCertPath,
275
560
  tlsPins: target.tlsPins,
561
+ };
562
+ const reason = epochRotationReason(getFlag('reason') ?? 'manual_rotation');
563
+ const teamId = getFlag('team');
564
+ const epoch = teamId
565
+ ? await rotateTeamCryptoEpoch({
566
+ target: syncTarget,
567
+ teamId,
568
+ reason,
569
+ home: getFlag('home'),
570
+ })
571
+ : await rotateUserCryptoEpoch({
572
+ target: syncTarget,
573
+ reason,
574
+ home: getFlag('home'),
575
+ });
576
+ if (isJsonMode()) {
577
+ printJson({
578
+ command: 'context epoch-rotate',
579
+ ok: true,
580
+ scope: teamId ? 'team' : 'user',
581
+ reason,
582
+ epoch,
583
+ });
584
+ return;
585
+ }
586
+ console.log(`${teamId ? 'Team' : 'User'} crypto epoch rotated: ${epoch.fingerprint}`);
587
+ console.log(`Epoch: ${epoch.epoch}`);
588
+ console.log(`Reason: ${reason}`);
589
+ }
590
+ async function contextRecoveryBackup() {
591
+ const target = await resolveWorkspaceSyncTarget('recovery-backup');
592
+ const generatedRecoveryKey = getFlag('recovery-key') ? null : generateUserEpochRecoveryKey();
593
+ const recoveryKey = getFlag('recovery-key') ?? generatedRecoveryKey;
594
+ if (!recoveryKey) {
595
+ throw new Error('vpd context recovery-backup requires --recovery-key <key>');
596
+ }
597
+ const backup = await createUserEpochRecoveryBackup({
598
+ target: {
599
+ workspaceId: target.workspaceId,
600
+ serverUrl: target.serverUrl,
601
+ credential: target.credential,
602
+ tlsVerify: target.tlsVerify,
603
+ caCertPath: target.caCertPath,
604
+ tlsPins: target.tlsPins,
605
+ },
606
+ recoveryKey,
607
+ home: getFlag('home'),
608
+ });
609
+ if (isJsonMode()) {
610
+ printJson({
611
+ command: 'context recovery-backup',
612
+ ok: true,
613
+ backup,
614
+ generatedRecoveryKey,
615
+ });
616
+ return;
617
+ }
618
+ console.log(`Recovery backup stored: ${backup.id}`);
619
+ console.log(`User epoch: ${backup.user_crypto_epoch_id}`);
620
+ if (generatedRecoveryKey) {
621
+ console.log('Recovery key generated. Store it somewhere private; Viewport cannot recover it.');
622
+ console.log(generatedRecoveryKey);
623
+ }
624
+ }
625
+ async function contextRecoveryRestore() {
626
+ const target = await resolveWorkspaceSyncTarget('recovery-restore');
627
+ const recoveryKey = requiredFlag('recovery-key', 'vpd context recovery-restore --recovery-key <key>');
628
+ const result = await restoreUserEpochFromRecoveryBackup({
629
+ target: {
630
+ workspaceId: target.workspaceId,
631
+ serverUrl: target.serverUrl,
632
+ credential: target.credential,
633
+ tlsVerify: target.tlsVerify,
634
+ caCertPath: target.caCertPath,
635
+ tlsPins: target.tlsPins,
636
+ },
637
+ recoveryKey,
638
+ home: getFlag('home'),
639
+ });
640
+ if (isJsonMode()) {
641
+ printJson({ command: 'context recovery-restore', ok: true, ...result });
642
+ return;
643
+ }
644
+ console.log(`Recovery backup restored: ${result.backup.id}`);
645
+ console.log(`Recovered epoch: ${result.restoredEpoch.fingerprint}`);
646
+ console.log(`Rotated epoch: ${result.rotatedEpoch.fingerprint}`);
647
+ console.log(`Fresh recovery backup stored: ${result.rotatedBackup.id}`);
648
+ }
649
+ async function contextRotationsProcess() {
650
+ const target = await resolveWorkspaceSyncTarget('rotations-process');
651
+ const result = await processPendingCryptoRotationRequests({
652
+ target: {
653
+ workspaceId: target.workspaceId,
654
+ serverUrl: target.serverUrl,
655
+ credential: target.credential,
656
+ tlsVerify: target.tlsVerify,
657
+ caCertPath: target.caCertPath,
658
+ tlsPins: target.tlsPins,
659
+ },
660
+ home: getFlag('home'),
661
+ });
662
+ if (isJsonMode()) {
663
+ printJson({ command: 'context rotations-process', ok: true, ...result });
664
+ return;
665
+ }
666
+ console.log(`Crypto rotation requests processed: ${result.processed}`);
667
+ if (result.userRotations > 0) {
668
+ console.log(`User epoch rotations: ${result.userRotations}`);
669
+ }
670
+ if (result.teamRotations > 0) {
671
+ console.log(`Team epoch rotations: ${result.teamRotations}`);
672
+ }
673
+ if (result.teamMemberGrants > 0) {
674
+ console.log(`Team epoch member grants created: ${result.teamMemberGrants}`);
675
+ }
676
+ if (result.skipped > 0) {
677
+ console.log(`Skipped rotation requests: ${result.skipped}`);
678
+ }
679
+ }
680
+ function epochRotationReason(value) {
681
+ if (value === 'device_revoked' ||
682
+ value === 'member_added' ||
683
+ value === 'member_revoked' ||
684
+ value === 'manual_rotation' ||
685
+ value === 'recovery') {
686
+ return value;
687
+ }
688
+ throw new Error('Epoch rotation reason must be device_revoked, member_added, member_revoked, manual_rotation, or recovery.');
689
+ }
690
+ async function contextDeviceEnrollRequest() {
691
+ const target = await resolveWorkspaceSyncTarget('device-enroll-request');
692
+ const deviceId = requiredFlag('device', 'vpd context device-enroll-request --device <id>');
693
+ const enrollment = await requestDeviceEpochEnrollment({
694
+ target: {
695
+ workspaceId: target.workspaceId,
696
+ serverUrl: target.serverUrl,
697
+ credential: target.credential,
698
+ tlsVerify: target.tlsVerify,
699
+ caCertPath: target.caCertPath,
700
+ tlsPins: target.tlsPins,
701
+ },
702
+ deviceId,
703
+ deviceLabel: getFlag('label') ?? deviceId,
704
+ home: getFlag('home'),
705
+ });
706
+ if (isJsonMode()) {
707
+ printJson({ command: 'context device-enroll-request', ok: true, enrollment });
708
+ return;
709
+ }
710
+ console.log(`Device enrollment requested: ${enrollment.enrollmentId}`);
711
+ console.log(`Fingerprint: ${enrollment.fingerprint}`);
712
+ }
713
+ async function contextDeviceEnrollApprove() {
714
+ const target = await resolveWorkspaceSyncTarget('device-enroll-approve');
715
+ const enrollment = await approveDeviceEpochEnrollment({
716
+ target: {
717
+ workspaceId: target.workspaceId,
718
+ serverUrl: target.serverUrl,
719
+ credential: target.credential,
720
+ tlsVerify: target.tlsVerify,
721
+ caCertPath: target.caCertPath,
722
+ tlsPins: target.tlsPins,
723
+ },
724
+ enrollmentId: requiredFlag('enrollment', 'vpd context device-enroll-approve --enrollment <id>'),
725
+ home: getFlag('home'),
726
+ });
727
+ if (isJsonMode()) {
728
+ printJson({ command: 'context device-enroll-approve', ok: true, enrollment });
729
+ return;
730
+ }
731
+ console.log(`Device enrollment approved: ${enrollment.id}`);
732
+ console.log(`Status: ${enrollment.status}`);
733
+ }
734
+ async function contextDeviceEnrollAccept() {
735
+ const target = await resolveWorkspaceSyncTarget('device-enroll-accept');
736
+ const epoch = await acceptDeviceEpochEnrollment({
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
+ enrollmentId: requiredFlag('enrollment', 'vpd context device-enroll-accept --enrollment <id>'),
746
+ home: getFlag('home'),
747
+ });
748
+ if (isJsonMode()) {
749
+ printJson({ command: 'context device-enroll-accept', ok: true, epoch });
750
+ return;
751
+ }
752
+ console.log(`Device enrollment accepted. User crypto epoch ready: ${epoch.fingerprint}`);
753
+ }
754
+ async function contextDeviceEnrollments() {
755
+ const target = await resolveWorkspaceSyncTarget('device-enrollments');
756
+ const enrollments = await listDeviceEpochEnrollments({
757
+ target: {
758
+ workspaceId: target.workspaceId,
759
+ serverUrl: target.serverUrl,
760
+ credential: target.credential,
761
+ tlsVerify: target.tlsVerify,
762
+ caCertPath: target.caCertPath,
763
+ tlsPins: target.tlsPins,
764
+ },
765
+ });
766
+ if (isJsonMode()) {
767
+ printJson({ command: 'context device-enrollments', ok: true, enrollments });
768
+ return;
769
+ }
770
+ if (enrollments.length === 0) {
771
+ console.log('No device enrollments found.');
772
+ return;
773
+ }
774
+ for (const enrollment of enrollments) {
775
+ console.log(`${enrollment.id} ${enrollment.status} ${enrollment.device_label} ${enrollment.fingerprint}`);
776
+ }
777
+ }
778
+ async function contextTeamGrantCreate() {
779
+ const target = await resolveWorkspaceSyncTarget('team-grant-create');
780
+ const grant = await grantTeamEpochToUserEpoch({
781
+ target: {
782
+ workspaceId: target.workspaceId,
783
+ serverUrl: target.serverUrl,
784
+ credential: target.credential,
785
+ tlsVerify: target.tlsVerify,
786
+ caCertPath: target.caCertPath,
787
+ tlsPins: target.tlsPins,
788
+ },
789
+ teamCryptoEpochId: requiredFlag('team-epoch', 'vpd context team-grant-create --team-epoch <id> --recipient-epoch <id>'),
790
+ recipientUserCryptoEpochId: requiredFlag('recipient-epoch', 'vpd context team-grant-create --team-epoch <id> --recipient-epoch <id>'),
791
+ home: getFlag('home'),
792
+ });
793
+ if (isJsonMode()) {
794
+ printJson({ command: 'context team-grant-create', ok: true, grant });
795
+ return;
796
+ }
797
+ console.log(`Team epoch member grant created: ${grant.id}`);
798
+ }
799
+ async function contextTeamGrantsAccept() {
800
+ const target = await resolveWorkspaceSyncTarget('team-grants-accept');
801
+ const result = await acceptTeamEpochMemberGrants({
802
+ target: {
803
+ workspaceId: target.workspaceId,
804
+ serverUrl: target.serverUrl,
805
+ credential: target.credential,
806
+ tlsVerify: target.tlsVerify,
807
+ caCertPath: target.caCertPath,
808
+ tlsPins: target.tlsPins,
809
+ },
276
810
  home: getFlag('home'),
277
811
  });
278
812
  if (isJsonMode()) {
279
- printJson({ command: 'context identity-publish', ok: true, ...result });
813
+ printJson({
814
+ command: 'context team-grants-accept',
815
+ ok: true,
816
+ accepted: result.accepted,
817
+ teamEpochs: result.teamEpochs.map(publicEpochForOutput),
818
+ });
280
819
  return;
281
820
  }
282
- console.log(`Context public identity published: ${result.identityId}`);
283
- if (result.fingerprint)
284
- console.log(`Fingerprint: ${result.fingerprint}`);
821
+ console.log(`Team epoch grants accepted: ${result.accepted}`);
285
822
  }
286
823
  async function contextGrantsProcess() {
287
824
  const target = await resolveContextSyncTarget('grants-process');