@interop/did-cli 0.10.0 → 0.10.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 (109) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/commands/collection-command.d.ts +169 -0
  3. package/dist/commands/collection-command.d.ts.map +1 -0
  4. package/dist/commands/collection-command.js +241 -0
  5. package/dist/commands/collection-command.js.map +1 -0
  6. package/dist/commands/did/create.d.ts +63 -0
  7. package/dist/commands/did/create.d.ts.map +1 -0
  8. package/dist/commands/did/create.js +625 -0
  9. package/dist/commands/did/create.js.map +1 -0
  10. package/dist/commands/did/manage.d.ts +68 -0
  11. package/dist/commands/did/manage.d.ts.map +1 -0
  12. package/dist/commands/did/manage.js +287 -0
  13. package/dist/commands/did/manage.js.map +1 -0
  14. package/dist/commands/did/service.d.ts +93 -0
  15. package/dist/commands/did/service.d.ts.map +1 -0
  16. package/dist/commands/did/service.js +324 -0
  17. package/dist/commands/did/service.js.map +1 -0
  18. package/dist/commands/did/webvh-update.d.ts +170 -0
  19. package/dist/commands/did/webvh-update.d.ts.map +1 -0
  20. package/dist/commands/did/webvh-update.js +402 -0
  21. package/dist/commands/did/webvh-update.js.map +1 -0
  22. package/dist/commands/did.d.ts +8 -8
  23. package/dist/commands/did.d.ts.map +1 -1
  24. package/dist/commands/did.js +27 -1519
  25. package/dist/commands/did.js.map +1 -1
  26. package/dist/commands/edv.d.ts +15 -6
  27. package/dist/commands/edv.d.ts.map +1 -1
  28. package/dist/commands/edv.js +38 -28
  29. package/dist/commands/edv.js.map +1 -1
  30. package/dist/commands/key.d.ts +104 -0
  31. package/dist/commands/key.d.ts.map +1 -1
  32. package/dist/commands/key.js +432 -402
  33. package/dist/commands/key.js.map +1 -1
  34. package/dist/commands/vc.d.ts +2 -1
  35. package/dist/commands/vc.d.ts.map +1 -1
  36. package/dist/commands/vc.js +88 -145
  37. package/dist/commands/vc.js.map +1 -1
  38. package/dist/commands/was/collection.d.ts.map +1 -1
  39. package/dist/commands/was/collection.js +31 -85
  40. package/dist/commands/was/collection.js.map +1 -1
  41. package/dist/commands/was/publish.d.ts.map +1 -1
  42. package/dist/commands/was/publish.js +16 -44
  43. package/dist/commands/was/publish.js.map +1 -1
  44. package/dist/commands/was/resource.d.ts.map +1 -1
  45. package/dist/commands/was/resource.js +24 -51
  46. package/dist/commands/was/resource.js.map +1 -1
  47. package/dist/commands/was/shared.d.ts +78 -0
  48. package/dist/commands/was/shared.d.ts.map +1 -1
  49. package/dist/commands/was/shared.js +84 -0
  50. package/dist/commands/was/shared.js.map +1 -1
  51. package/dist/commands/was/space.d.ts.map +1 -1
  52. package/dist/commands/was/space.js +10 -16
  53. package/dist/commands/was/space.js.map +1 -1
  54. package/dist/commands/was/tree.d.ts.map +1 -1
  55. package/dist/commands/was/tree.js +4 -6
  56. package/dist/commands/was/tree.js.map +1 -1
  57. package/dist/commands/was.d.ts.map +1 -1
  58. package/dist/commands/was.js +60 -74
  59. package/dist/commands/was.js.map +1 -1
  60. package/dist/commands/zcap.d.ts +0 -18
  61. package/dist/commands/zcap.d.ts.map +1 -1
  62. package/dist/commands/zcap.js +63 -155
  63. package/dist/commands/zcap.js.map +1 -1
  64. package/dist/documentLoader.d.ts +5 -0
  65. package/dist/documentLoader.d.ts.map +1 -0
  66. package/dist/documentLoader.js +29 -0
  67. package/dist/documentLoader.js.map +1 -0
  68. package/dist/edv/hmac.d.ts.map +1 -1
  69. package/dist/edv/hmac.js +12 -12
  70. package/dist/edv/hmac.js.map +1 -1
  71. package/dist/edv/recipients.d.ts +2 -0
  72. package/dist/edv/recipients.d.ts.map +1 -1
  73. package/dist/edv/recipients.js +12 -15
  74. package/dist/edv/recipients.js.map +1 -1
  75. package/dist/meta.d.ts +36 -11
  76. package/dist/meta.d.ts.map +1 -1
  77. package/dist/meta.js +95 -73
  78. package/dist/meta.js.map +1 -1
  79. package/dist/storage.d.ts +48 -13
  80. package/dist/storage.d.ts.map +1 -1
  81. package/dist/storage.js +40 -16
  82. package/dist/storage.js.map +1 -1
  83. package/dist/vc/issue.d.ts.map +1 -1
  84. package/dist/vc/issue.js +4 -7
  85. package/dist/vc/issue.js.map +1 -1
  86. package/dist/vc/verify.d.ts +10 -0
  87. package/dist/vc/verify.d.ts.map +1 -1
  88. package/dist/vc/verify.js +38 -1
  89. package/dist/vc/verify.js.map +1 -1
  90. package/dist/was/client.d.ts.map +1 -1
  91. package/dist/was/client.js +8 -2
  92. package/dist/was/client.js.map +1 -1
  93. package/dist/was/grant.d.ts +44 -0
  94. package/dist/was/grant.d.ts.map +1 -0
  95. package/dist/was/grant.js +78 -0
  96. package/dist/was/grant.js.map +1 -0
  97. package/dist/was/registry.d.ts.map +1 -1
  98. package/dist/was/registry.js +9 -2
  99. package/dist/was/registry.js.map +1 -1
  100. package/dist/zcap/delegate.d.ts.map +1 -1
  101. package/dist/zcap/delegate.js +1 -6
  102. package/dist/zcap/delegate.js.map +1 -1
  103. package/dist/zcap/signer.d.ts.map +1 -1
  104. package/dist/zcap/signer.js.map +1 -1
  105. package/package.json +5 -4
  106. package/dist/keys/webvh-driver.d.ts +0 -24
  107. package/dist/keys/webvh-driver.d.ts.map +0 -1
  108. package/dist/keys/webvh-driver.js +0 -73
  109. package/dist/keys/webvh-driver.js.map +0 -1
@@ -1,595 +1,19 @@
1
- import { createInterface } from 'node:readline/promises';
2
- import { stdin, stdout } from 'node:process';
3
- import { Command } from 'commander';
4
- import { decodeSecretKeySeed, generateSecretKeySeed } from '@digitalcredentials/bnid';
5
- import { driver } from '@interop/did-method-key';
6
- import * as didWeb from '@interop/did-web-resolver';
7
- import { createDID, resolveDIDFromLog, updateDID } from '@interop/did-method-webvh';
8
- import { createDefaultDidResolver, securityLoader } from '@interop/security-document-loader';
9
- import { Ed25519VerificationKey } from '@interop/ed25519-verification-key';
10
- import * as EcdsaMultikey from '@interop/ecdsa-multikey';
11
- import { X25519KeyAgreementKey2020 } from '@interop/x25519-key-agreement-key';
12
- import { listDids, loadDidDocument, loadDidKeys, loadDidLog, loadDidMeta, loadDidUpdateKeys, removeDidFiles, saveDidLog, saveDidMeta, saveDidUpdateKeys, saveToDids } from '../storage.js';
13
- import { recordKeyDidAssociation, removeKeyDidAssociation, resolveDidRef } from '../meta.js';
14
- import { renderTable } from '../table.js';
15
- import { normalizeEcdsaCurve, SUPPORTED_ECDSA_CURVES, warnIfNotVcIssuanceCapable } from '../keys/ecdsa.js';
16
- import { makeWebvhSigner } from '../keys/webvh-signer.js';
17
- import { makeWebvhDriver, webvhLogVerifier } from '../keys/webvh-driver.js';
18
- import { exportUpdateKey, generateStagedKey, loadUpdateKey } from '../keys/webvh-update.js';
19
- /**
20
- * Save the artifacts of a newly created DID: the DID document, its keys file,
21
- * a metadata sidecar (creation timestamp plus the handle and description when
22
- * given), and the key-to-DID association cache of any matching wallet keys.
23
- *
24
- * @param options {object}
25
- * @param options.method {string}
26
- * @param options.didDocument {object}
27
- * @param options.exportedKeys {object}
28
- * @param options.fingerprints {(string | undefined)[]}
29
- * @param [options.handle] {string}
30
- * @param [options.description] {string}
31
- * @returns {Promise<void>}
32
- */
33
- async function saveDidArtifacts({ method, didDocument, exportedKeys, fingerprints, handle, description }) {
34
- const did = didDocument.id;
35
- const docPath = await saveToDids({ method, did, data: didDocument });
36
- await saveToDids({ method, did, suffix: 'keys', data: exportedKeys });
37
- const meta = { created: new Date().toISOString() };
38
- if (handle) {
39
- meta.handle = handle;
40
- }
41
- if (description) {
42
- meta.description = description;
43
- }
44
- await saveDidMeta({ did, meta });
45
- for (const fingerprint of fingerprints) {
46
- if (fingerprint) {
47
- await recordKeyDidAssociation({ publicKeyMultibase: fingerprint, did });
48
- }
49
- }
50
- console.error(`DID saved to ${docPath}`);
51
- }
52
- /**
53
- * Parse a raw did:webvh history log (newline-delimited JSON) into the entry
54
- * array the library's resolver/updater expect, ignoring blank lines.
55
- *
56
- * @param logText {string}
57
- * @returns {DIDLog}
58
- */
59
- export function parseDidLog(logText) {
60
- return logText
61
- .split('\n')
62
- .filter(line => line.trim().length > 0)
63
- .map(line => JSON.parse(line));
64
- }
65
- /**
66
- * Ask the user to confirm a hard-to-undo action. Returns true immediately when
67
- * `--yes` was passed or when stdin is not an interactive TTY (so scripts and
68
- * tests are not blocked); otherwise prompts and requires an explicit `y`.
69
- *
70
- * @param options {object}
71
- * @param options.message {string}
72
- * @param options.yes {boolean} the value of the `--yes` flag.
73
- * @returns {Promise<boolean>}
74
- */
75
- async function confirmAction({ message, yes }) {
76
- if (yes || !stdin.isTTY) {
77
- return true;
78
- }
79
- const rl = createInterface({ input: stdin, output: stdout });
80
- try {
81
- const answer = await rl.question(`${message} [y/N] `);
82
- return answer.trim().toLowerCase() === 'y';
83
- }
84
- finally {
85
- rl.close();
86
- }
87
- }
88
- /**
89
- * Load and resolve a locally stored did:webvh history log in preparation for an
90
- * update, asserting it is updatable. The log is the source of truth for a
91
- * stored did:webvh, so its absence is what "not locally stored" means.
92
- *
93
- * @param options {object}
94
- * @param options.targetDid {string} the resolved did:webvh DID.
95
- * @param options.action {string} verb used in error messages (e.g.
96
- * `rotate keys`, `update services`).
97
- * @returns {Promise<{ log, doc, meta }>}
98
- * @throws {Error} with a user-facing message if the log is missing, fails to
99
- * resolve, or the DID is deactivated.
100
- */
101
- async function resolveWebvhForUpdate({ targetDid, action }) {
102
- let logText;
103
- try {
104
- logText = await loadDidLog(targetDid);
105
- }
106
- catch {
107
- throw new Error(`No locally stored did:webvh found for ${targetDid}`);
108
- }
109
- const log = parseDidLog(logText);
110
- let resolved;
111
- try {
112
- resolved = await resolveDIDFromLog(log, { verifier: webvhLogVerifier });
113
- }
114
- catch (err) {
115
- throw new Error(`Could not resolve the DID log: ${err.message}`, {
116
- cause: err
117
- });
118
- }
119
- if (resolved.meta.deactivated) {
120
- throw new Error(`Cannot ${action}: the DID is deactivated.`);
121
- }
122
- return { log, doc: resolved.doc, meta: resolved.meta };
123
- }
124
1
  /**
125
- * Resolve a locally stored did:webvh DID from its history log (`.jsonl`) -- the
126
- * source of truth for the current document and its accumulated parameters.
127
- * Unlike `resolveWebvhForUpdate` this does not reject a deactivated DID, since
128
- * `show` should still display it; it returns `undefined` when no log is stored
129
- * so the caller can fall back to the stored document snapshot.
130
- *
131
- * @param did {string} the resolved did:webvh DID.
132
- * @returns {Promise<{ doc, meta } | undefined>}
133
- * @throws {Error} if a log exists but fails to resolve/verify.
2
+ * `did` command wiring. Defines the commander surface (create, add-key,
3
+ * add-service, remove-service, get, show, list, meta, remove, and the webvh
4
+ * subcommand) and delegates each action to a `run*` function in the `did/`
5
+ * modules: create/add-key in `./did/create.js`, service edits in
6
+ * `./did/service.js`, read/manage operations in `./did/manage.js`, and the
7
+ * did:webvh update-key plumbing plus rotate-keys in `./did/webvh-update.js`.
134
8
  */
135
- async function resolveStoredWebvh(did) {
136
- let logText;
137
- try {
138
- logText = await loadDidLog(did);
139
- }
140
- catch {
141
- return undefined;
142
- }
143
- const { doc, meta } = await resolveDIDFromLog(parseDidLog(logText), {
144
- verifier: webvhLogVerifier
145
- });
146
- return { doc, meta };
147
- }
148
- /**
149
- * Load a did:webvh DID's update-keys sidecar, treating an absent file as
150
- * `undefined` (no stored secrets) rather than an error.
151
- *
152
- * @param did {string}
153
- * @returns {Promise<WebvhUpdateKeys | undefined>}
154
- */
155
- async function loadStoredUpdateKeys(did) {
156
- try {
157
- return await loadDidUpdateKeys(did);
158
- }
159
- catch (err) {
160
- if (err.code !== 'ENOENT') {
161
- throw err;
162
- }
163
- return undefined;
164
- }
165
- }
166
- /**
167
- * Pre-rotation reveal: validate and load the staged (pre-committed) update key
168
- * that must sign this entry, returning the signer key pair plus the records
169
- * that make it the new active key and retire the current one.
170
- *
171
- * @param options {object}
172
- * @param options.stored {WebvhUpdateKeys | undefined} the update-keys sidecar.
173
- * @param options.meta {Awaited<ReturnType<typeof resolveDIDFromLog>>['meta']}
174
- * @param options.targetDid {string}
175
- * @param options.action {string} verb used in error messages.
176
- * @returns {Promise<{ signerKeyPair, newActive, retiredActive }>}
177
- * @throws {Error} if the staged secret is missing or has diverged from the
178
- * log's committed nextKeyHashes.
179
- */
180
- async function revealStagedSigner({ stored, meta, targetDid, action }) {
181
- if (!stored?.staged) {
182
- throw new Error(`Cannot ${action}: the pre-committed next-key secret was not found. ` +
183
- 'While pre-rotation is armed the staged key must sign this entry; ' +
184
- `check for a backup of the ${targetDid}.update-keys.json sidecar.`);
185
- }
186
- if (!meta.nextKeyHashes.includes(stored.staged.nextKeyHash)) {
187
- throw new Error(`Cannot ${action}: the staged key's hash is not among the log's ` +
188
- 'committed nextKeyHashes. The local update-keys record has diverged ' +
189
- 'from the published log (an update may have happened elsewhere); ' +
190
- 're-resolve before retrying.');
191
- }
192
- return {
193
- signerKeyPair: await loadUpdateKey(stored.staged),
194
- newActive: {
195
- publicKeyMultibase: stored.staged.publicKeyMultibase,
196
- secretKeyMultibase: stored.staged.secretKeyMultibase
197
- },
198
- retiredActive: stored.active
199
- };
200
- }
201
- /**
202
- * Ordinary (non-pre-rotation) signer: the current active update key signs.
203
- * Returns the signer key pair and the active record it was loaded from.
204
- *
205
- * @param options {object}
206
- * @param options.stored {WebvhUpdateKeys | undefined} the update-keys sidecar.
207
- * @param options.meta {Awaited<ReturnType<typeof resolveDIDFromLog>>['meta']}
208
- * @param options.targetDid {string}
209
- * @param options.action {string} verb used in error messages.
210
- * @returns {Promise<{ signerKeyPair, activeRecord }>}
211
- * @throws {Error} if the active secret is missing or no longer matches the
212
- * log's updateKeys.
213
- */
214
- async function loadActiveSigner({ stored, meta, targetDid, action }) {
215
- const activeRecord = stored?.active.secretKeyMultibase &&
216
- meta.updateKeys.includes(stored.active.publicKeyMultibase)
217
- ? stored.active
218
- : undefined;
219
- if (!activeRecord) {
220
- throw new Error(`Cannot ${action}: the current active update-key secret was not found ` +
221
- `in ${targetDid}.update-keys.json.`);
222
- }
223
- return { signerKeyPair: await loadUpdateKey(activeRecord), activeRecord };
224
- }
225
- /**
226
- * Build the entry signer for a did:webvh update from an update key pair.
227
- *
228
- * @param keyPair {Ed25519VerificationKey} mutated: its `id` is set in place.
229
- * @returns the signer to pass to `updateDID`.
230
- */
231
- function makeWebvhEntrySigner(keyPair) {
232
- const signer = makeWebvhSigner({ keyPair });
233
- // `keyPair.signer()` requires an id to be set before signing.
234
- keyPair.id = signer.getVerificationMethodId();
235
- return signer;
236
- }
237
- /**
238
- * Write the update-keys sidecar: the active key, the staged next key (or
239
- * none), and any retired secrets. At creation there is no prior sidecar; after
240
- * an advance/rotation the superseded active key is appended to `retired` only
241
- * when `keepOldKey` asks to preserve it.
242
- *
243
- * @param options {object}
244
- * @param options.did {string}
245
- * @param options.newActive {WebvhUpdateKey}
246
- * @param [options.newStaged] {WebvhUpdateKey & { nextKeyHash: string }}
247
- * @param [options.retiredActive] {WebvhUpdateKey} the superseded active key.
248
- * @param [options.stored] {WebvhUpdateKeys} the prior sidecar, if any.
249
- * @param [options.keepOldKey] {boolean} retain the retired secret.
250
- * @returns {Promise<string>} the saved sidecar path.
251
- */
252
- async function persistUpdateKeysSidecar({ did, newActive, newStaged, retiredActive, stored, keepOldKey }) {
253
- const updated = { active: newActive };
254
- if (newStaged) {
255
- updated.staged = newStaged;
256
- }
257
- const retiredList = stored?.retired ? [...stored.retired] : [];
258
- // retiredActive is undefined on the stage-only path, so this also skips that
259
- // case without a separate guard.
260
- if (keepOldKey && retiredActive?.secretKeyMultibase) {
261
- retiredList.push(retiredActive);
262
- }
263
- if (retiredList.length > 0) {
264
- updated.retired = retiredList;
265
- }
266
- return saveDidUpdateKeys({ did, updateKeys: updated });
267
- }
268
- /**
269
- * Expand a service id to a full DID URL. A bare fragment (`files`) or a
270
- * leading-`#` fragment (`#files`) is resolved against the DID; a value that is
271
- * already a full DID URL, or any absolute URI carrying a fragment, is returned
272
- * unchanged.
273
- *
274
- * @param options {object}
275
- * @param options.did {string}
276
- * @param options.id {string}
277
- * @returns {string}
278
- */
279
- function normalizeServiceId({ did, id }) {
280
- if (id.startsWith('#')) {
281
- return `${did}${id}`;
282
- }
283
- if (id.startsWith('did:') || id.includes('#')) {
284
- return id;
285
- }
286
- return `${did}#${id}`;
287
- }
288
- /**
289
- * Build a service-endpoint entry from add-service options. Exactly one of
290
- * `endpoint` (one or more endpoint values -- a single value stays a string,
291
- * several become an array) or `endpointJson` (a raw JSON value) supplies the
292
- * serviceEndpoint; a single `type` likewise stays a string.
293
- *
294
- * @param options {object}
295
- * @param options.did {string}
296
- * @param options.id {string}
297
- * @param options.type {string[]}
298
- * @param [options.endpoint] {string[]}
299
- * @param [options.endpointJson] {string}
300
- * @returns {ServiceEndpoint}
301
- */
302
- function buildServiceEntry({ did, id, type, endpoint, endpointJson }) {
303
- const hasEndpoint = Boolean(endpoint?.length);
304
- const hasEndpointJson = Boolean(endpointJson);
305
- // True when both are supplied or neither is -- i.e. not exactly one.
306
- if (hasEndpoint === hasEndpointJson) {
307
- throw new Error('Provide exactly one of --endpoint or --endpoint-json.');
308
- }
309
- let serviceEndpoint;
310
- if (endpointJson) {
311
- try {
312
- serviceEndpoint = JSON.parse(endpointJson);
313
- }
314
- catch {
315
- throw new Error('--endpoint-json must be valid JSON.');
316
- }
317
- }
318
- else {
319
- serviceEndpoint = endpoint.length === 1 ? endpoint[0] : endpoint;
320
- }
321
- return {
322
- id: normalizeServiceId({ did, id }),
323
- type: type.length === 1 ? type[0] : type,
324
- serviceEndpoint
325
- };
326
- }
327
- /**
328
- * Whether a stored service entry has the given (already-normalized, absolute)
329
- * id. Service ids in a DID document may be relative (`#files`) or absolute, so
330
- * the stored id is normalized against the DID before comparing.
331
- *
332
- * @param options {object}
333
- * @param options.service {ServiceEndpoint}
334
- * @param options.id {string} the normalized id to match.
335
- * @param options.did {string}
336
- * @returns {boolean}
337
- */
338
- function serviceHasId({ service, id, did }) {
339
- return (service.id !== undefined &&
340
- normalizeServiceId({ did, id: service.id }) === id);
341
- }
342
- /**
343
- * Append a service entry to the current array, rejecting a duplicate id.
344
- *
345
- * @param options {object}
346
- * @param options.current {ServiceEndpoint[]}
347
- * @param options.entry {ServiceEndpoint} its `id` is already normalized.
348
- * @param options.did {string}
349
- * @returns {ServiceEndpoint[]}
350
- */
351
- function addServiceEntry({ current, entry, did }) {
352
- if (current.some(service => serviceHasId({ service, id: entry.id, did }))) {
353
- throw new Error(`A service with id "${entry.id}" already exists.`);
354
- }
355
- return [...current, entry];
356
- }
357
- /**
358
- * Remove the service entry with the given (normalized) id, rejecting a missing
359
- * id.
360
- *
361
- * @param options {object}
362
- * @param options.current {ServiceEndpoint[]}
363
- * @param options.id {string} the normalized id to remove.
364
- * @param options.did {string}
365
- * @returns {ServiceEndpoint[]}
366
- */
367
- function removeServiceEntry({ current, id, did }) {
368
- const next = current.filter(service => !serviceHasId({ service, id, did }));
369
- if (next.length === current.length) {
370
- throw new Error(`No service with id "${id}" found on the DID document.`);
371
- }
372
- return next;
373
- }
374
- /**
375
- * Apply a service-array transform to a locally stored did:web document and
376
- * re-save it. did:web has no history log, so this is a direct document edit;
377
- * the `service` property is dropped entirely when the array empties out.
378
- *
379
- * @param options {object}
380
- * @param options.did {string}
381
- * @param options.transform {(current: ServiceEndpoint[], did: string) => ServiceEndpoint[]}
382
- * @returns {Promise<number>} the process exit code
383
- */
384
- async function runWebServiceUpdate({ did, transform }) {
385
- let didDocument;
386
- try {
387
- didDocument = await loadDidDocument(did);
388
- }
389
- catch {
390
- console.error(`No locally stored did:web found for ${did}`);
391
- return 1;
392
- }
393
- const current = Array.isArray(didDocument.service)
394
- ? didDocument.service
395
- : [];
396
- let next;
397
- try {
398
- next = transform(current, did);
399
- }
400
- catch (err) {
401
- console.error(err.message);
402
- return 1;
403
- }
404
- if (next.length > 0) {
405
- didDocument.service = next;
406
- }
407
- else {
408
- delete didDocument.service;
409
- }
410
- const docPath = await saveToDids({ method: 'web', did, data: didDocument });
411
- console.error(`DID saved to ${docPath}`);
412
- console.log(JSON.stringify({ id: didDocument.id, didDocument }, null, 2));
413
- return 0;
414
- }
415
- /**
416
- * Apply a service-array transform to a locally stored did:webvh DID by
417
- * appending a sparse log entry that overlays only the `service` array. Update
418
- * keys and document verification methods are carried forward unchanged -- with
419
- * one exception: a pre-rotation-armed DID cannot author a key-neutral update
420
- * (the library requires the staged key to sign), so the update-key ratchet is
421
- * advanced as part of the change -- the staged key is revealed to sign and a
422
- * fresh next key is staged.
423
- *
424
- * @param options {object}
425
- * @param options.targetDid {string} the resolved did:webvh DID.
426
- * @param options.transform {(current: ServiceEndpoint[], did: string) => ServiceEndpoint[]}
427
- * @param [options.yes] {boolean} skip the confirmation prompt.
428
- * @param [options.keepOldKey] {boolean} retain the retired update key secret
429
- * (pre-rotation path only; default is to drop it).
430
- * @returns {Promise<number>} the process exit code
431
- */
432
- async function runWebvhServiceUpdate({ targetDid, transform, yes, keepOldKey }) {
433
- let log;
434
- let doc;
435
- let meta;
436
- try {
437
- ;
438
- ({ log, doc, meta } = await resolveWebvhForUpdate({
439
- targetDid,
440
- action: 'update services'
441
- }));
442
- }
443
- catch (err) {
444
- console.error(err.message);
445
- return 1;
446
- }
447
- // Compute the new service array from the current resolved document.
448
- const current = Array.isArray(doc?.service) ? doc.service : [];
449
- let services;
450
- try {
451
- services = transform(current, targetDid);
452
- }
453
- catch (err) {
454
- console.error(err.message);
455
- return 1;
456
- }
457
- const stored = await loadStoredUpdateKeys(targetDid);
458
- // Choose the signer and key parameters. A sparse update normally omits
459
- // updateKeys/nextKeyHashes so the keys carry forward untouched; a
460
- // pre-rotation DID instead must reveal its staged key (which signs) and stage
461
- // a fresh one in the same entry.
462
- let signerKeyPair;
463
- let updateKeys;
464
- let nextKeyHashes;
465
- let newActive;
466
- let newStaged;
467
- let retiredActive;
468
- try {
469
- if (meta.prerotation) {
470
- ;
471
- ({ signerKeyPair, newActive, retiredActive } = await revealStagedSigner({
472
- stored,
473
- meta,
474
- targetDid,
475
- action: 'update services'
476
- }));
477
- newStaged = await generateStagedKey();
478
- updateKeys = [newActive.publicKeyMultibase];
479
- nextKeyHashes = [newStaged.nextKeyHash];
480
- }
481
- else {
482
- ;
483
- ({ signerKeyPair } = await loadActiveSigner({
484
- stored,
485
- meta,
486
- targetDid,
487
- action: 'update services'
488
- }));
489
- }
490
- }
491
- catch (err) {
492
- console.error(err.message);
493
- return 1;
494
- }
495
- const confirmed = await confirmAction({
496
- message: `Update the services of ${targetDid}? This appends a new log entry ` +
497
- 'and is hard to undo.',
498
- yes
499
- });
500
- if (!confirmed) {
501
- console.error('Aborted.');
502
- return 0;
503
- }
504
- const signer = makeWebvhEntrySigner(signerKeyPair);
505
- let result;
506
- try {
507
- result = await updateDID({
508
- log,
509
- signer,
510
- verifier: webvhLogVerifier,
511
- services,
512
- ...(updateKeys ? { updateKeys } : {}),
513
- ...(nextKeyHashes ? { nextKeyHashes } : {})
514
- });
515
- }
516
- catch (err) {
517
- console.error(`Service update failed: ${err.message}`);
518
- return 1;
519
- }
520
- const logPath = await saveDidLog({ did: result.did, log: result.log });
521
- const docPath = await saveToDids({
522
- method: 'webvh',
523
- did: result.did,
524
- data: result.doc
525
- });
526
- // Persist the advanced ratchet only on the pre-rotation path; an ordinary
527
- // service update leaves the update-keys sidecar untouched.
528
- if (meta.prerotation && newActive) {
529
- const updateKeysPath = await persistUpdateKeysSidecar({
530
- did: result.did,
531
- newActive,
532
- newStaged,
533
- retiredActive,
534
- stored,
535
- keepOldKey
536
- });
537
- console.error(`Update keys saved to ${updateKeysPath}`);
538
- console.error('Pre-rotation: the update key was advanced as part of this change.');
539
- }
540
- console.error(`DID document saved to ${docPath}`);
541
- console.error(`DID history log saved to ${logPath}`);
542
- console.log(JSON.stringify({ id: result.did, didDocument: result.doc }, null, 2));
543
- return 0;
544
- }
545
- /**
546
- * Resolve a DID reference, then route a service-array transform to the right
547
- * per-method runner and return its exit code. did:web is a direct document
548
- * edit; did:webvh appends a log entry.
549
- *
550
- * @param options {object}
551
- * @param options.ref {string} a DID or a local metadata handle.
552
- * @param options.transform {(current: ServiceEndpoint[], did: string) => ServiceEndpoint[]}
553
- * @param [options.yes] {boolean} skip the did:webvh confirmation prompt.
554
- * @param [options.keepOldKey] {boolean} did:webvh pre-rotation path only.
555
- * @returns {Promise<number>} the process exit code
556
- */
557
- async function dispatchServiceUpdate({ ref, transform, yes, keepOldKey }) {
558
- let resolved;
559
- try {
560
- resolved = await resolveDidRef({ ref });
561
- }
562
- catch (err) {
563
- console.error(err.message);
564
- return 1;
565
- }
566
- const did = resolved ?? ref;
567
- if (did.startsWith('did:web:')) {
568
- return runWebServiceUpdate({ did, transform });
569
- }
570
- if (did.startsWith('did:webvh:')) {
571
- return runWebvhServiceUpdate({ targetDid: did, transform, yes, keepOldKey });
572
- }
573
- console.error('add-service/remove-service are only supported for did:web and ' +
574
- 'did:webvh DIDs');
575
- return 1;
576
- }
577
- /**
578
- * Document loader for DID / DID-URL resolution. A bare DID resolves to its DID
579
- * document; a `did#fragment` URL is dereferenced straight to its
580
- * verification-method node. Works for did:key (offline), did:web, and did:webvh
581
- * (both fetched). Built once and reused. (Per project convention, DID/JSON-LD
582
- * resolution goes through `@interop/security-document-loader`, never a
583
- * hand-rolled loader.) The loader's default resolver only knows did:key and
584
- * did:web, so the did:webvh driver is registered onto a copy of the defaults
585
- * and injected -- keeping the did:webvh dependency out of the shared loader.
586
- */
587
- const didResolver = createDefaultDidResolver();
588
- // `CachedResolver.use` types its argument as the full generation-capable
589
- // `DidMethodDriver`, but only reads `.method` (and later calls `.get`) for
590
- // resolution; the webvh driver implements just that resolution subset.
591
- didResolver.use(makeWebvhDriver());
592
- const documentLoader = securityLoader({ didResolver }).build();
9
+ import { Command } from 'commander';
10
+ import { SUPPORTED_ECDSA_CURVES } from '../keys/ecdsa.js';
11
+ import { runAndExit } from './was/shared.js';
12
+ import { DEFAULT_VERIFICATION_PURPOSES, runAddKey, runCreate } from './did/create.js';
13
+ import { addServiceEntry, buildServiceEntry, dispatchServiceUpdate, normalizeServiceId, removeServiceEntry } from './did/service.js';
14
+ import { runGet, runList, runMeta, runRemove, runShow } from './did/manage.js';
15
+ import { runRotateKeys } from './did/webvh-update.js';
16
+ export { parseDidLog } from './did/webvh-update.js';
593
17
  export function makeDidCommand() {
594
18
  const did = new Command('did').description('Manage DIDs');
595
19
  did
@@ -615,388 +39,7 @@ export function makeDidCommand() {
615
39
  .option('--save', 'save the DID document to local storage (~/.config/did-cli-wallet/dids/)')
616
40
  .option('--handle <handle>', 'short tag for the saved DID (requires --save)')
617
41
  .option('--description <description>', 'longer description of the saved DID (requires --save)')
618
- .action(async (method = 'key', options) => {
619
- if ((options.handle !== undefined || options.description !== undefined) &&
620
- !options.save) {
621
- console.error('--handle and --description require --save');
622
- process.exit(1);
623
- return;
624
- }
625
- switch (method) {
626
- case 'key': {
627
- switch (options.type) {
628
- case 'ed25519': {
629
- const envSeed = process.env.SECRET_KEY_SEED;
630
- const secretKeySeed = options.withSeed
631
- ? (envSeed ?? (await generateSecretKeySeed()))
632
- : envSeed;
633
- const seedBytes = secretKeySeed
634
- ? decodeSecretKeySeed({ secretKeySeed })
635
- : undefined;
636
- const keyPair = await Ed25519VerificationKey.generate({
637
- seed: seedBytes
638
- });
639
- const didDriver = driver();
640
- didDriver.use({ keyPairClass: Ed25519VerificationKey });
641
- const { didDocument } = await didDriver.fromKeyPair({
642
- verificationKeyPair: keyPair
643
- });
644
- if (options.save) {
645
- const exported = await keyPair.export({
646
- publicKey: true,
647
- secretKey: true
648
- });
649
- await saveDidArtifacts({
650
- method: 'key',
651
- didDocument,
652
- exportedKeys: exported,
653
- fingerprints: [
654
- exported
655
- .publicKeyMultibase
656
- ],
657
- handle: options.handle,
658
- description: options.description
659
- });
660
- }
661
- const output = { id: didDocument.id };
662
- if (options.withSeed) {
663
- output.secretKeySeed = secretKeySeed;
664
- }
665
- output.didDocument = didDocument;
666
- console.log(JSON.stringify(output, null, 2));
667
- break;
668
- }
669
- case 'ecdsa': {
670
- if (options.withSeed) {
671
- console.error('--with-seed is not supported for ecdsa keys; ECDSA key ' +
672
- 'generation is non-deterministic and cannot be derived ' +
673
- 'from a seed.');
674
- process.exit(1);
675
- return;
676
- }
677
- const curve = normalizeEcdsaCurve({ curve: options.curve });
678
- if (!curve) {
679
- console.error(`Unknown ecdsa curve: ${options.curve}. ` +
680
- `Supported: ${SUPPORTED_ECDSA_CURVES}`);
681
- process.exit(1);
682
- return;
683
- }
684
- warnIfNotVcIssuanceCapable({ curve });
685
- const keyPair = await EcdsaMultikey.generate({ curve });
686
- const didDriver = driver();
687
- const { didDocument } = await didDriver.fromKeyPair({
688
- verificationKeyPair: keyPair
689
- });
690
- if (options.save) {
691
- const exported = await keyPair.export({
692
- publicKey: true,
693
- secretKey: true
694
- });
695
- await saveDidArtifacts({
696
- method: 'key',
697
- didDocument,
698
- exportedKeys: exported,
699
- fingerprints: [
700
- exported
701
- .publicKeyMultibase
702
- ],
703
- handle: options.handle,
704
- description: options.description
705
- });
706
- }
707
- const output = { id: didDocument.id };
708
- output.didDocument = didDocument;
709
- console.log(JSON.stringify(output, null, 2));
710
- break;
711
- }
712
- default:
713
- console.error(`Unknown key type: ${options.type}. Supported: ed25519, ecdsa`);
714
- process.exit(1);
715
- }
716
- break;
717
- }
718
- case 'web': {
719
- switch (options.type) {
720
- case 'ed25519': {
721
- if (!options.url) {
722
- console.error('did:web requires --url (e.g. --url https://example.com)');
723
- process.exit(1);
724
- return;
725
- }
726
- const envSeed = process.env.SECRET_KEY_SEED;
727
- const secretKeySeed = options.withSeed
728
- ? (envSeed ?? (await generateSecretKeySeed()))
729
- : envSeed;
730
- const seedBytes = secretKeySeed
731
- ? decodeSecretKeySeed({ secretKeySeed })
732
- : undefined;
733
- const didWebDriver = didWeb.driver();
734
- didWebDriver.use({ keyPairClass: Ed25519VerificationKey });
735
- const { didDocument, keyPairs } = await didWebDriver.generate({
736
- url: options.url,
737
- seed: seedBytes
738
- });
739
- if (options.save) {
740
- const exported = {};
741
- const fingerprints = [];
742
- for (const [methodId, keyPair] of keyPairs) {
743
- const exportedKey = (await keyPair.export({
744
- publicKey: true,
745
- secretKey: true
746
- }));
747
- exported[methodId] = exportedKey;
748
- fingerprints.push(exportedKey.publicKeyMultibase);
749
- }
750
- await saveDidArtifacts({
751
- method: 'web',
752
- didDocument,
753
- exportedKeys: exported,
754
- fingerprints,
755
- handle: options.handle,
756
- description: options.description
757
- });
758
- }
759
- const output = { id: didDocument.id };
760
- if (options.withSeed) {
761
- output.secretKeySeed = secretKeySeed;
762
- }
763
- output.didDocument = didDocument;
764
- console.log(JSON.stringify(output, null, 2));
765
- break;
766
- }
767
- case 'ecdsa': {
768
- if (!options.url) {
769
- console.error('did:web requires --url (e.g. --url https://example.com)');
770
- process.exit(1);
771
- return;
772
- }
773
- if (options.withSeed) {
774
- console.error('--with-seed is not supported for ecdsa keys; ECDSA key ' +
775
- 'generation is non-deterministic and cannot be derived ' +
776
- 'from a seed.');
777
- process.exit(1);
778
- return;
779
- }
780
- const curve = normalizeEcdsaCurve({ curve: options.curve });
781
- if (!curve) {
782
- console.error(`Unknown ecdsa curve: ${options.curve}. ` +
783
- `Supported: ${SUPPORTED_ECDSA_CURVES}`);
784
- process.exit(1);
785
- return;
786
- }
787
- warnIfNotVcIssuanceCapable({ curve });
788
- const keyPair = await EcdsaMultikey.generate({ curve });
789
- const didWebDriver = didWeb.driver();
790
- const { didDocument, keyPairs } = await didWebDriver.generate({
791
- url: options.url,
792
- verificationKeyPair: keyPair
793
- });
794
- if (options.save) {
795
- const exported = {};
796
- const fingerprints = [];
797
- for (const [methodId, savedKey] of keyPairs) {
798
- const exportedKey = (await savedKey.export({
799
- publicKey: true,
800
- secretKey: true
801
- }));
802
- exported[methodId] = exportedKey;
803
- fingerprints.push(exportedKey.publicKeyMultibase);
804
- }
805
- await saveDidArtifacts({
806
- method: 'web',
807
- didDocument,
808
- exportedKeys: exported,
809
- fingerprints,
810
- handle: options.handle,
811
- description: options.description
812
- });
813
- }
814
- const output = { id: didDocument.id };
815
- output.didDocument = didDocument;
816
- console.log(JSON.stringify(output, null, 2));
817
- break;
818
- }
819
- default:
820
- console.error(`Unknown key type: ${options.type}. ` +
821
- `Supported: ed25519, ecdsa`);
822
- process.exit(1);
823
- }
824
- break;
825
- }
826
- case 'webvh': {
827
- if (!options.url) {
828
- console.error('did:webvh requires --url (e.g. --url https://example.com)');
829
- process.exit(1);
830
- return;
831
- }
832
- // The webvh library hardcodes the `eddsa-jcs-2022` cryptosuite, so
833
- // only Ed25519 update keys are supported for now.
834
- if (options.type !== 'ed25519') {
835
- console.error(`did:webvh only supports --type ed25519 (got ${options.type}); ` +
836
- 'the eddsa-jcs-2022 cryptosuite requires an Ed25519 key.');
837
- process.exit(1);
838
- return;
839
- }
840
- // Pre-rotation is the default; --no-prerotation opts out. With no
841
- // flag, commander leaves `prerotation` undefined, which is on.
842
- const prerotation = options.prerotation !== false;
843
- // Portability is the default; --no-portable opts out (same shape).
844
- const portable = options.portable !== false;
845
- // Witness declarations (declaration only -- generating the witness
846
- // proofs / did-witness.json sidecar is out of scope for now).
847
- const witnessDids = options.witness ?? [];
848
- if (options.witnessThreshold !== undefined &&
849
- witnessDids.length === 0) {
850
- console.error('--witness-threshold requires at least one --witness');
851
- process.exit(1);
852
- return;
853
- }
854
- for (const witnessDid of witnessDids) {
855
- if (!witnessDid.startsWith('did:key:')) {
856
- console.error(`Invalid witness "${witnessDid}": witnesses must be ` +
857
- 'did:key DIDs');
858
- process.exit(1);
859
- return;
860
- }
861
- }
862
- let witness;
863
- if (witnessDids.length > 0) {
864
- let threshold = witnessDids.length;
865
- if (options.witnessThreshold !== undefined) {
866
- threshold = Number.parseInt(options.witnessThreshold, 10);
867
- if (!Number.isInteger(threshold) ||
868
- threshold < 1 ||
869
- threshold > witnessDids.length) {
870
- console.error('--witness-threshold must be an integer between 1 and the ' +
871
- `number of witnesses (${witnessDids.length})`);
872
- process.exit(1);
873
- return;
874
- }
875
- }
876
- witness = {
877
- threshold,
878
- witnesses: witnessDids.map(id => ({ id }))
879
- };
880
- }
881
- // Watcher URLs: https:// (or http://localhost for local testing).
882
- const watchers = options.watcher ?? [];
883
- for (const watcher of watchers) {
884
- let watcherUrl;
885
- try {
886
- watcherUrl = new URL(watcher);
887
- }
888
- catch {
889
- console.error(`Invalid watcher URL: ${watcher}`);
890
- process.exit(1);
891
- return;
892
- }
893
- const isLocalhost = watcherUrl.protocol === 'http:' &&
894
- (watcherUrl.hostname === 'localhost' ||
895
- watcherUrl.hostname === '127.0.0.1');
896
- if (watcherUrl.protocol !== 'https:' && !isLocalhost) {
897
- console.error(`Invalid watcher URL "${watcher}": must be https:// ` +
898
- '(or http://localhost)');
899
- process.exit(1);
900
- return;
901
- }
902
- }
903
- const envSeed = process.env.SECRET_KEY_SEED;
904
- const secretKeySeed = options.withSeed
905
- ? (envSeed ?? (await generateSecretKeySeed()))
906
- : envSeed;
907
- const seedBytes = secretKeySeed
908
- ? decodeSecretKeySeed({ secretKeySeed })
909
- : undefined;
910
- // Update key A: the active authorization key. It is what the DID
911
- // identifier (SCID) derives from, so it is the seed-derived one.
912
- const updateKey = await Ed25519VerificationKey.generate({
913
- seed: seedBytes
914
- });
915
- const signer = makeWebvhEntrySigner(updateKey);
916
- // Document verification key V, decoupled from the update keys so
917
- // that rotating the update key never disturbs the document.
918
- const docKey = await Ed25519VerificationKey.generate();
919
- // Staged next update key B: when pre-rotation is on, commit its
920
- // hash now so the next update must reveal it.
921
- const stagedKey = prerotation
922
- ? await generateStagedKey()
923
- : undefined;
924
- const result = await createDID({
925
- address: options.url,
926
- signer,
927
- verifier: signer,
928
- // A portable DID (the default) can later be moved to a different
929
- // domain; --no-portable pins it to its origin.
930
- portable,
931
- updateKeys: [updateKey.publicKeyMultibase],
932
- ...(stagedKey ? { nextKeyHashes: [stagedKey.nextKeyHash] } : {}),
933
- ...(witness ? { witness } : {}),
934
- ...(watchers.length > 0 ? { watchers } : {}),
935
- verificationMethods: [
936
- {
937
- type: 'Multikey',
938
- publicKeyMultibase: docKey.publicKeyMultibase,
939
- // Wire the document key into the same relationships as did:web
940
- // (everything but keyAgreement, which needs an X25519 key).
941
- purpose: [
942
- 'authentication',
943
- 'assertionMethod',
944
- 'capabilityDelegation',
945
- 'capabilityInvocation'
946
- ]
947
- }
948
- ]
949
- });
950
- if (options.save) {
951
- // Persist the document key V keyed by its document verification
952
- // method id (so it can be selected for signing), and set its id
953
- // to match.
954
- const docVmId = result.doc.verificationMethod?.[0]?.id;
955
- if (!docVmId) {
956
- console.error('Created did:webvh document is missing a verification method id');
957
- process.exit(1);
958
- return;
959
- }
960
- docKey.id = docVmId;
961
- const exportedDoc = (await docKey.export({
962
- publicKey: true,
963
- secretKey: true
964
- }));
965
- await saveDidArtifacts({
966
- method: 'webvh',
967
- didDocument: result.doc,
968
- exportedKeys: { [docVmId]: exportedDoc },
969
- fingerprints: [exportedDoc.publicKeyMultibase],
970
- handle: options.handle,
971
- description: options.description
972
- });
973
- // Persist the update keys (active A, and staged B when armed) in
974
- // a sidecar distinct from the document's keys file.
975
- const updateKeysPath = await persistUpdateKeysSidecar({
976
- did: result.did,
977
- newActive: await exportUpdateKey(updateKey),
978
- newStaged: stagedKey
979
- });
980
- const logPath = await saveDidLog({
981
- did: result.did,
982
- log: result.log
983
- });
984
- console.error(`DID history log saved to ${logPath}`);
985
- console.error(`Update keys saved to ${updateKeysPath}`);
986
- }
987
- const output = { id: result.did };
988
- if (options.withSeed) {
989
- output.secretKeySeed = secretKeySeed;
990
- }
991
- output.didDocument = result.doc;
992
- console.log(JSON.stringify(output, null, 2));
993
- break;
994
- }
995
- default:
996
- console.error(`Unknown method: ${method}. Supported: key, web, webvh`);
997
- process.exit(1);
998
- }
999
- });
42
+ .action((method = 'key', options) => runAndExit(runCreate({ method, ...options })));
1000
43
  did
1001
44
  .command('add-key <did>')
1002
45
  .description('Add a verification key to an existing (locally stored) did:web')
@@ -1004,140 +47,9 @@ export function makeDidCommand() {
1004
47
  'into keyAgreement only', 'ed25519')
1005
48
  .option('--curve <curve>', `ECDSA curve for --type ecdsa (supported: ${SUPPORTED_ECDSA_CURVES})`, 'p256')
1006
49
  .option('--purpose <purpose...>', 'verification relationship(s) to wire the key into ' +
1007
- '(default: authentication, assertionMethod, capabilityDelegation, capabilityInvocation)')
50
+ `(default: ${DEFAULT_VERIFICATION_PURPOSES.join(', ')})`)
1008
51
  .option('--with-seed', 'include the new secret key seed in output (generated if SECRET_KEY_SEED is not set)')
1009
- .action(async (did, options) => {
1010
- if (!did.startsWith('did:web:')) {
1011
- console.error('add-key is only supported for did:web DIDs');
1012
- process.exit(1);
1013
- }
1014
- if (options.type !== 'ed25519' &&
1015
- options.type !== 'ecdsa' &&
1016
- options.type !== 'x25519') {
1017
- console.error(`Unknown key type: ${options.type}. ` +
1018
- 'Supported: ed25519, ecdsa, x25519');
1019
- process.exit(1);
1020
- }
1021
- // x25519 keys are key agreement keys; they can only be wired into the
1022
- // keyAgreement verification relationship.
1023
- let purposes = options.purpose;
1024
- if (options.type === 'x25519') {
1025
- if (purposes &&
1026
- purposes.some(purpose => purpose !== 'keyAgreement')) {
1027
- console.error('x25519 keys can only be added to the keyAgreement ' +
1028
- 'verification relationship');
1029
- process.exit(1);
1030
- return;
1031
- }
1032
- purposes = ['keyAgreement'];
1033
- }
1034
- let didDocument;
1035
- let exportedKeys;
1036
- try {
1037
- didDocument = await loadDidDocument(did);
1038
- exportedKeys = await loadDidKeys(did);
1039
- }
1040
- catch {
1041
- console.error(`No locally stored did:web found for ${did}`);
1042
- process.exit(1);
1043
- }
1044
- // ed25519 keys are derived from a seed; ecdsa and x25519 keys are
1045
- // generated non-deterministically and have no seed.
1046
- let secretKeySeed;
1047
- let keyPair;
1048
- const didWebDriver = didWeb.driver();
1049
- if (options.type === 'ed25519') {
1050
- const envSeed = process.env.SECRET_KEY_SEED;
1051
- secretKeySeed = options.withSeed
1052
- ? (envSeed ?? (await generateSecretKeySeed()))
1053
- : envSeed;
1054
- const seedBytes = secretKeySeed
1055
- ? decodeSecretKeySeed({ secretKeySeed })
1056
- : undefined;
1057
- keyPair = await Ed25519VerificationKey.generate({ seed: seedBytes });
1058
- didWebDriver.use({ keyPairClass: Ed25519VerificationKey });
1059
- }
1060
- else if (options.type === 'ecdsa') {
1061
- if (options.withSeed) {
1062
- console.error('--with-seed is not supported for ecdsa keys; ECDSA key ' +
1063
- 'generation is non-deterministic and cannot be derived ' +
1064
- 'from a seed.');
1065
- process.exit(1);
1066
- return;
1067
- }
1068
- const curve = normalizeEcdsaCurve({ curve: options.curve });
1069
- if (!curve) {
1070
- console.error(`Unknown ecdsa curve: ${options.curve}. ` +
1071
- `Supported: ${SUPPORTED_ECDSA_CURVES}`);
1072
- process.exit(1);
1073
- return;
1074
- }
1075
- warnIfNotVcIssuanceCapable({ curve });
1076
- keyPair = await EcdsaMultikey.generate({ curve });
1077
- }
1078
- else {
1079
- if (options.withSeed) {
1080
- console.error('--with-seed is not supported for x25519 keys; X25519 key ' +
1081
- 'generation is non-deterministic and cannot be derived ' +
1082
- 'from a seed.');
1083
- process.exit(1);
1084
- return;
1085
- }
1086
- keyPair = await X25519KeyAgreementKey2020.generate();
1087
- }
1088
- const keyPairs = new Map();
1089
- try {
1090
- await didWebDriver.addVerificationMethod({
1091
- didDocument,
1092
- keyPairs,
1093
- keyPair,
1094
- purposes
1095
- });
1096
- }
1097
- catch (err) {
1098
- console.error(err.message);
1099
- process.exit(1);
1100
- }
1101
- // Merge the new key into the stored keys file (keyed by VM id). X25519
1102
- // keys export their private half as `privateKeyMultibase`, the other
1103
- // suites as `secretKeyMultibase`.
1104
- const addedFingerprints = [];
1105
- for (const [methodId, addedKey] of keyPairs) {
1106
- const exportedKey = (await addedKey.export(options.type === 'x25519'
1107
- ? { publicKey: true, privateKey: true }
1108
- : { publicKey: true, secretKey: true }));
1109
- exportedKeys[methodId] = exportedKey;
1110
- addedFingerprints.push(exportedKey.publicKeyMultibase);
1111
- }
1112
- const docPath = await saveToDids({
1113
- method: 'web',
1114
- did,
1115
- data: didDocument
1116
- });
1117
- await saveToDids({
1118
- method: 'web',
1119
- did,
1120
- suffix: 'keys',
1121
- data: exportedKeys
1122
- });
1123
- // Update the key-to-DID cache of any matching wallet keys; the DID's
1124
- // own metadata sidecar is left untouched.
1125
- for (const fingerprint of addedFingerprints) {
1126
- if (fingerprint) {
1127
- await recordKeyDidAssociation({
1128
- publicKeyMultibase: fingerprint,
1129
- did
1130
- });
1131
- }
1132
- }
1133
- console.error(`DID saved to ${docPath}`);
1134
- const output = { id: didDocument.id };
1135
- if (options.withSeed) {
1136
- output.secretKeySeed = secretKeySeed;
1137
- }
1138
- output.didDocument = didDocument;
1139
- console.log(JSON.stringify(output, null, 2));
1140
- });
52
+ .action((did, options) => runAndExit(runAddKey({ did, ...options })));
1141
53
  did
1142
54
  .command('add-service <did>')
1143
55
  .description('Add a service entry to a locally stored did:web or did:webvh DID. The ' +
@@ -1163,15 +75,12 @@ export function makeDidCommand() {
1163
75
  });
1164
76
  return addServiceEntry({ current, entry, did: resolvedDid });
1165
77
  };
1166
- const code = await dispatchServiceUpdate({
78
+ return runAndExit(dispatchServiceUpdate({
1167
79
  ref: did,
1168
80
  transform,
1169
81
  yes: options.yes,
1170
82
  keepOldKey: options.keepOldKey
1171
- });
1172
- if (code !== 0) {
1173
- process.exit(code);
1174
- }
83
+ }));
1175
84
  });
1176
85
  did
1177
86
  .command('remove-service <did>')
@@ -1190,34 +99,19 @@ export function makeDidCommand() {
1190
99
  id: normalizeServiceId({ did: resolvedDid, id: options.id }),
1191
100
  did: resolvedDid
1192
101
  });
1193
- const code = await dispatchServiceUpdate({
102
+ return runAndExit(dispatchServiceUpdate({
1194
103
  ref: did,
1195
104
  transform,
1196
105
  yes: options.yes,
1197
106
  keepOldKey: options.keepOldKey
1198
- });
1199
- if (code !== 0) {
1200
- process.exit(code);
1201
- }
107
+ }));
1202
108
  });
1203
109
  did
1204
110
  .command('get <did>')
1205
111
  .aliases(['resolve'])
1206
112
  .description('Resolve a DID to its DID document, or a DID URL (a did#fragment key ' +
1207
113
  'id) to its verification method, via the security document loader')
1208
- .action(async (didOrKeyId) => {
1209
- let document;
1210
- try {
1211
- ;
1212
- ({ document } = (await documentLoader(didOrKeyId)));
1213
- }
1214
- catch (err) {
1215
- console.error(`Could not resolve "${didOrKeyId}": ${err.message}`);
1216
- process.exit(1);
1217
- return;
1218
- }
1219
- console.log(JSON.stringify(document, null, 2));
1220
- });
114
+ .action((didOrKeyId) => runAndExit(runGet({ didOrKeyId })));
1221
115
  did
1222
116
  .command('show <did>')
1223
117
  .aliases(['view', 'cat'])
@@ -1226,253 +120,26 @@ export function makeDidCommand() {
1226
120
  'log -- the source of truth -- rather than the stored snapshot.')
1227
121
  .option('--meta', 'show the DID metadata instead of the DID document')
1228
122
  .option('--json', 'with --meta, output the metadata as JSON')
1229
- .action(async (didRef, options) => {
1230
- let did;
1231
- try {
1232
- did = await resolveDidRef({ ref: didRef });
1233
- }
1234
- catch (err) {
1235
- console.error(err.message);
1236
- process.exit(1);
1237
- return;
1238
- }
1239
- const targetDid = did ?? didRef;
1240
- // For did:webvh the history log is the source of truth, so resolve the
1241
- // current document (and its accumulated parameters) from it rather than
1242
- // trusting the stored snapshot. A DID with no stored log falls through
1243
- // to the stored document below.
1244
- let webvhMeta;
1245
- let resolvedDoc;
1246
- if (targetDid.startsWith('did:webvh:')) {
1247
- let resolved;
1248
- try {
1249
- resolved = await resolveStoredWebvh(targetDid);
1250
- }
1251
- catch (err) {
1252
- console.error(`Could not resolve the DID log for ${targetDid}: ` +
1253
- err.message);
1254
- process.exit(1);
1255
- return;
1256
- }
1257
- if (resolved) {
1258
- resolvedDoc = resolved.doc;
1259
- webvhMeta = resolved.meta;
1260
- }
1261
- }
1262
- let didDocument;
1263
- if (resolvedDoc) {
1264
- didDocument = resolvedDoc;
1265
- }
1266
- else {
1267
- try {
1268
- didDocument = await loadDidDocument(targetDid);
1269
- }
1270
- catch {
1271
- console.error(`No locally stored DID found for ${didRef}`);
1272
- process.exit(1);
1273
- return;
1274
- }
1275
- }
1276
- if (options.meta) {
1277
- const docDid = didDocument.id;
1278
- const meta = await loadDidMeta({ did: docDid });
1279
- const keyCount = Array.isArray(didDocument.verificationMethod)
1280
- ? didDocument.verificationMethod.length
1281
- : 0;
1282
- // Parameters resolved from the did:webvh log (absent for other
1283
- // methods, and for a webvh DID with no stored log).
1284
- const witnessCount = webvhMeta?.witness?.witnesses?.length ?? 0;
1285
- const watcherCount = webvhMeta?.watchers?.length ?? 0;
1286
- if (options.json) {
1287
- const output = {
1288
- did: docDid,
1289
- method: docDid.split(':')[1],
1290
- ...(meta?.created && { created: meta.created }),
1291
- ...(meta?.handle && { handle: meta.handle }),
1292
- ...(meta?.description && { description: meta.description }),
1293
- keys: keyCount,
1294
- ...(webvhMeta && {
1295
- versionId: webvhMeta.versionId,
1296
- updated: webvhMeta.updated,
1297
- portable: webvhMeta.portable,
1298
- prerotation: webvhMeta.prerotation,
1299
- deactivated: webvhMeta.deactivated,
1300
- updateKeys: webvhMeta.updateKeys.length,
1301
- witnesses: witnessCount,
1302
- watchers: watcherCount
1303
- })
1304
- };
1305
- console.log(JSON.stringify(output, null, 2));
1306
- return;
1307
- }
1308
- const rows = [
1309
- ['DID', docDid],
1310
- ['Method', docDid.split(':')[1]],
1311
- ['Handle', meta?.handle ?? ''],
1312
- ['Created', meta?.created ?? ''],
1313
- ['Description', meta?.description ?? ''],
1314
- ['Keys', String(keyCount)]
1315
- ];
1316
- if (webvhMeta) {
1317
- rows.push(['Version', webvhMeta.versionId], ['Updated', webvhMeta.updated], ['Portable', webvhMeta.portable ? 'yes' : 'no'], ['Prerotation', webvhMeta.prerotation ? 'yes' : 'no'], ['Deactivated', webvhMeta.deactivated ? 'yes' : 'no'], ['Update keys', String(webvhMeta.updateKeys.length)], ['Witnesses', String(witnessCount)], ['Watchers', String(watcherCount)]);
1318
- }
1319
- console.log(renderTable({
1320
- columns: [{ header: 'FIELD' }, { header: 'VALUE' }],
1321
- rows
1322
- }));
1323
- return;
1324
- }
1325
- // The DID document holds no secret material -- signing keys live in the
1326
- // separate `<did>.keys.json` file -- so it is safe to print as-is.
1327
- console.log(JSON.stringify(didDocument, null, 2));
1328
- });
123
+ .action((didRef, options) => runAndExit(runShow({ didRef, ...options })));
1329
124
  did
1330
125
  .command('list')
1331
126
  .description('List locally stored DIDs with their metadata')
1332
127
  .option('--json', 'output the list as a JSON array of objects with metadata')
1333
128
  .option('--plain', 'output one DID per line, sorted (no metadata)')
1334
- .action(async (options) => {
1335
- const dids = await listDids();
1336
- if (options.plain) {
1337
- for (const did of dids) {
1338
- console.log(did);
1339
- }
1340
- return;
1341
- }
1342
- const entries = [];
1343
- for (const did of dids) {
1344
- const meta = await loadDidMeta({ did });
1345
- entries.push({ did, method: did.split(':')[1], ...meta });
1346
- }
1347
- if (options.json) {
1348
- const output = entries.map(entry => ({
1349
- did: entry.did,
1350
- method: entry.method,
1351
- ...(entry.created && { created: entry.created }),
1352
- ...(entry.handle && { handle: entry.handle }),
1353
- ...(entry.description && { description: entry.description })
1354
- }));
1355
- console.log(JSON.stringify(output, null, 2));
1356
- return;
1357
- }
1358
- if (entries.length === 0) {
1359
- return;
1360
- }
1361
- const rows = entries.map(entry => [
1362
- entry.handle ?? '',
1363
- entry.method,
1364
- entry.created?.slice(0, 10) ?? '',
1365
- entry.did,
1366
- entry.description ?? ''
1367
- ]);
1368
- console.log(renderTable({
1369
- columns: [
1370
- { header: 'HANDLE', maxWidth: 16 },
1371
- { header: 'METHOD' },
1372
- { header: 'CREATED' },
1373
- { header: 'DID', maxWidth: 44 },
1374
- { header: 'DESCRIPTION', maxWidth: 40 }
1375
- ],
1376
- rows
1377
- }));
1378
- });
129
+ .action((options) => runAndExit(runList(options)));
1379
130
  did
1380
131
  .command('meta <did>')
1381
132
  .description('Show or edit the metadata of a locally stored DID (by DID or handle); ' +
1382
133
  'with no options, prints the current metadata')
1383
134
  .option('--handle <handle>', 'set the handle (an empty string clears it)')
1384
135
  .option('--description <description>', 'set the description (an empty string clears it)')
1385
- .action(async (didRef, options) => {
1386
- let did;
1387
- try {
1388
- did = await resolveDidRef({ ref: didRef });
1389
- }
1390
- catch (err) {
1391
- console.error(err.message);
1392
- process.exit(1);
1393
- return;
1394
- }
1395
- if (did) {
1396
- // Refuse to create metadata for DIDs that are not saved locally.
1397
- try {
1398
- await loadDidDocument(did);
1399
- }
1400
- catch {
1401
- did = undefined;
1402
- }
1403
- }
1404
- if (!did) {
1405
- console.error(`No locally stored DID found for ${didRef}`);
1406
- process.exit(1);
1407
- return;
1408
- }
1409
- const existing = await loadDidMeta({ did });
1410
- const hasEdits = options.handle !== undefined || options.description !== undefined;
1411
- if (!hasEdits) {
1412
- console.log(JSON.stringify(existing ?? {}, null, 2));
1413
- return;
1414
- }
1415
- const meta = { ...(existing ?? {}) };
1416
- if (options.handle !== undefined) {
1417
- if (options.handle === '') {
1418
- delete meta.handle;
1419
- }
1420
- else {
1421
- meta.handle = options.handle;
1422
- }
1423
- }
1424
- if (options.description !== undefined) {
1425
- if (options.description === '') {
1426
- delete meta.description;
1427
- }
1428
- else {
1429
- meta.description = options.description;
1430
- }
1431
- }
1432
- const filePath = await saveDidMeta({ did, meta });
1433
- console.error(`Metadata saved to ${filePath}`);
1434
- console.log(JSON.stringify(meta, null, 2));
1435
- });
136
+ .action((didRef, options) => runAndExit(runMeta({ didRef, ...options })));
1436
137
  did
1437
138
  .command('remove <did>')
1438
139
  .aliases(['delete', 'rm'])
1439
140
  .description('Remove a locally stored DID document, its keys file, and its ' +
1440
141
  'metadata sidecar (by DID or handle)')
1441
- .action(async (didRef) => {
1442
- let did;
1443
- try {
1444
- did = await resolveDidRef({ ref: didRef });
1445
- }
1446
- catch (err) {
1447
- console.error(err.message);
1448
- process.exit(1);
1449
- return;
1450
- }
1451
- let didDocument;
1452
- try {
1453
- didDocument = await loadDidDocument(did ?? didRef);
1454
- }
1455
- catch {
1456
- console.error(`No locally stored DID found for ${didRef}`);
1457
- process.exit(1);
1458
- return;
1459
- }
1460
- const docDid = did ?? didRef;
1461
- // Scrub the cached key-to-DID associations of any matching wallet keys
1462
- // before the DID document (the source of truth) is deleted.
1463
- for (const method of didDocument.verificationMethod ?? []) {
1464
- if (method.publicKeyMultibase) {
1465
- await removeKeyDidAssociation({
1466
- publicKeyMultibase: method.publicKeyMultibase,
1467
- did: docDid
1468
- });
1469
- }
1470
- }
1471
- const removed = await removeDidFiles({ did: docDid });
1472
- for (const filePath of removed) {
1473
- console.error(`Removed ${filePath}`);
1474
- }
1475
- });
142
+ .action((didRef) => runAndExit(runRemove({ didRef })));
1476
143
  const webvh = new Command('webvh').description('Manage did:webvh DIDs: rotate update (authorization) keys');
1477
144
  webvh
1478
145
  .command('rotate-keys <did>')
@@ -1489,166 +156,7 @@ export function makeDidCommand() {
1489
156
  .option('--stop-prerotation', 'do not stage a next key; pre-rotation turns off after this rotation')
1490
157
  .option('--keep-old-key', 'retain the retired update key secret in the sidecar (default: drop it)')
1491
158
  .option('-y, --yes', 'skip the confirmation prompt')
1492
- .action(async (didRef, options) => {
1493
- let resolved;
1494
- try {
1495
- resolved = await resolveDidRef({ ref: didRef });
1496
- }
1497
- catch (err) {
1498
- console.error(err.message);
1499
- process.exit(1);
1500
- return;
1501
- }
1502
- const targetDid = resolved ?? didRef;
1503
- if (!targetDid.startsWith('did:webvh:')) {
1504
- console.error('rotate-keys is only supported for did:webvh DIDs');
1505
- process.exit(1);
1506
- return;
1507
- }
1508
- // The history log is the source of truth for a stored did:webvh (the
1509
- // current document is just its last entry's state), so a key-only
1510
- // rotation ignores the resolved document.
1511
- let log;
1512
- let meta;
1513
- try {
1514
- ;
1515
- ({ log, meta } = await resolveWebvhForUpdate({
1516
- targetDid,
1517
- action: 'rotate keys'
1518
- }));
1519
- }
1520
- catch (err) {
1521
- console.error(err.message);
1522
- process.exit(1);
1523
- return;
1524
- }
1525
- // Flag validation against the current pre-rotation state.
1526
- if (options.enablePrerotation && options.stopPrerotation) {
1527
- console.error('--enable-prerotation and --stop-prerotation are mutually exclusive');
1528
- process.exit(1);
1529
- return;
1530
- }
1531
- if (meta.prerotation) {
1532
- if (options.updateKey) {
1533
- console.error('--update-key is not allowed while pre-rotation is armed; the ' +
1534
- 'next update keys are fixed by the committed nextKeyHashes');
1535
- process.exit(1);
1536
- return;
1537
- }
1538
- if (options.enablePrerotation) {
1539
- console.error('pre-rotation is already enabled for this DID');
1540
- process.exit(1);
1541
- return;
1542
- }
1543
- }
1544
- else if (options.stopPrerotation) {
1545
- console.error('pre-rotation is already off for this DID');
1546
- process.exit(1);
1547
- return;
1548
- }
1549
- const stored = await loadStoredUpdateKeys(targetDid);
1550
- // Inbound: who signs this entry, and what becomes the active key.
1551
- let signerKeyPair;
1552
- let newActive;
1553
- let retiredActive;
1554
- try {
1555
- if (meta.prerotation) {
1556
- // Pre-rotation reveal: the staged key signs its own activation.
1557
- ;
1558
- ({ signerKeyPair, newActive, retiredActive } =
1559
- await revealStagedSigner({
1560
- stored,
1561
- meta,
1562
- targetDid,
1563
- action: 'rotate keys'
1564
- }));
1565
- }
1566
- else {
1567
- // Ordinary rotation: the current active key signs.
1568
- let activeRecord;
1569
- ({ signerKeyPair, activeRecord } = await loadActiveSigner({
1570
- stored,
1571
- meta,
1572
- targetDid,
1573
- action: 'rotate keys'
1574
- }));
1575
- if (options.enablePrerotation && !options.updateKey) {
1576
- // Stage only: arm pre-rotation without changing the active key.
1577
- newActive = activeRecord;
1578
- }
1579
- else if (options.updateKey) {
1580
- // Rotate to externally-held key(s); the secret is not ours to store.
1581
- newActive = { publicKeyMultibase: options.updateKey[0] };
1582
- retiredActive = activeRecord;
1583
- }
1584
- else {
1585
- newActive = await exportUpdateKey(await Ed25519VerificationKey.generate());
1586
- retiredActive = activeRecord;
1587
- }
1588
- }
1589
- }
1590
- catch (err) {
1591
- console.error(err.message);
1592
- process.exit(1);
1593
- return;
1594
- }
1595
- // Outbound: keep the ratchet armed (stage a fresh next key) or not.
1596
- const arm = meta.prerotation
1597
- ? !options.stopPrerotation
1598
- : Boolean(options.enablePrerotation);
1599
- const newStaged = arm ? await generateStagedKey() : undefined;
1600
- const nextKeyHashes = newStaged ? [newStaged.nextKeyHash] : [];
1601
- const confirmed = await confirmAction({
1602
- message: `Rotate the update key of ${targetDid}? This appends a new log ` +
1603
- 'entry and is hard to undo.',
1604
- yes: options.yes
1605
- });
1606
- if (!confirmed) {
1607
- console.error('Aborted.');
1608
- return;
1609
- }
1610
- const signer = makeWebvhEntrySigner(signerKeyPair);
1611
- // A sparse updateDID() carries the prior DID document state forward and
1612
- // only overlays the fields an update actually supplies, so a key-only
1613
- // rotation omits all document directives to leave the document
1614
- // unchanged.
1615
- let result;
1616
- try {
1617
- result = await updateDID({
1618
- log,
1619
- signer,
1620
- verifier: webvhLogVerifier,
1621
- updateKeys: [newActive.publicKeyMultibase],
1622
- nextKeyHashes
1623
- });
1624
- }
1625
- catch (err) {
1626
- console.error(`Key rotation failed: ${err.message}`);
1627
- process.exit(1);
1628
- return;
1629
- }
1630
- const logPath = await saveDidLog({ did: result.did, log: result.log });
1631
- const docPath = await saveToDids({
1632
- method: 'webvh',
1633
- did: result.did,
1634
- data: result.doc
1635
- });
1636
- const updateKeysPath = await persistUpdateKeysSidecar({
1637
- did: result.did,
1638
- newActive,
1639
- newStaged,
1640
- retiredActive,
1641
- stored,
1642
- keepOldKey: options.keepOldKey
1643
- });
1644
- console.error(`DID document saved to ${docPath}`);
1645
- console.error(`DID history log saved to ${logPath}`);
1646
- console.error(`Update keys saved to ${updateKeysPath}`);
1647
- console.error(nextKeyHashes.length > 0
1648
- ? 'Pre-rotation is armed: a next update key is staged.'
1649
- : 'Pre-rotation is off after this rotation.');
1650
- console.log(JSON.stringify({ id: result.did, didDocument: result.doc }, null, 2));
1651
- });
159
+ .action((didRef, options) => runAndExit(runRotateKeys({ didRef, ...options })));
1652
160
  did.addCommand(webvh);
1653
161
  return did;
1654
162
  }