@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.
- package/CHANGELOG.md +58 -0
- package/dist/commands/collection-command.d.ts +169 -0
- package/dist/commands/collection-command.d.ts.map +1 -0
- package/dist/commands/collection-command.js +241 -0
- package/dist/commands/collection-command.js.map +1 -0
- package/dist/commands/did/create.d.ts +63 -0
- package/dist/commands/did/create.d.ts.map +1 -0
- package/dist/commands/did/create.js +625 -0
- package/dist/commands/did/create.js.map +1 -0
- package/dist/commands/did/manage.d.ts +68 -0
- package/dist/commands/did/manage.d.ts.map +1 -0
- package/dist/commands/did/manage.js +287 -0
- package/dist/commands/did/manage.js.map +1 -0
- package/dist/commands/did/service.d.ts +93 -0
- package/dist/commands/did/service.d.ts.map +1 -0
- package/dist/commands/did/service.js +324 -0
- package/dist/commands/did/service.js.map +1 -0
- package/dist/commands/did/webvh-update.d.ts +170 -0
- package/dist/commands/did/webvh-update.d.ts.map +1 -0
- package/dist/commands/did/webvh-update.js +402 -0
- package/dist/commands/did/webvh-update.js.map +1 -0
- package/dist/commands/did.d.ts +8 -8
- package/dist/commands/did.d.ts.map +1 -1
- package/dist/commands/did.js +27 -1519
- package/dist/commands/did.js.map +1 -1
- package/dist/commands/edv.d.ts +15 -6
- package/dist/commands/edv.d.ts.map +1 -1
- package/dist/commands/edv.js +38 -28
- package/dist/commands/edv.js.map +1 -1
- package/dist/commands/key.d.ts +104 -0
- package/dist/commands/key.d.ts.map +1 -1
- package/dist/commands/key.js +432 -402
- package/dist/commands/key.js.map +1 -1
- package/dist/commands/vc.d.ts +2 -1
- package/dist/commands/vc.d.ts.map +1 -1
- package/dist/commands/vc.js +88 -145
- package/dist/commands/vc.js.map +1 -1
- package/dist/commands/was/collection.d.ts.map +1 -1
- package/dist/commands/was/collection.js +31 -85
- package/dist/commands/was/collection.js.map +1 -1
- package/dist/commands/was/publish.d.ts.map +1 -1
- package/dist/commands/was/publish.js +16 -44
- package/dist/commands/was/publish.js.map +1 -1
- package/dist/commands/was/resource.d.ts.map +1 -1
- package/dist/commands/was/resource.js +24 -51
- package/dist/commands/was/resource.js.map +1 -1
- package/dist/commands/was/shared.d.ts +78 -0
- package/dist/commands/was/shared.d.ts.map +1 -1
- package/dist/commands/was/shared.js +84 -0
- package/dist/commands/was/shared.js.map +1 -1
- package/dist/commands/was/space.d.ts.map +1 -1
- package/dist/commands/was/space.js +10 -16
- package/dist/commands/was/space.js.map +1 -1
- package/dist/commands/was/tree.d.ts.map +1 -1
- package/dist/commands/was/tree.js +4 -6
- package/dist/commands/was/tree.js.map +1 -1
- package/dist/commands/was.d.ts.map +1 -1
- package/dist/commands/was.js +60 -74
- package/dist/commands/was.js.map +1 -1
- package/dist/commands/zcap.d.ts +0 -18
- package/dist/commands/zcap.d.ts.map +1 -1
- package/dist/commands/zcap.js +63 -155
- package/dist/commands/zcap.js.map +1 -1
- package/dist/documentLoader.d.ts +5 -0
- package/dist/documentLoader.d.ts.map +1 -0
- package/dist/documentLoader.js +29 -0
- package/dist/documentLoader.js.map +1 -0
- package/dist/edv/hmac.d.ts.map +1 -1
- package/dist/edv/hmac.js +12 -12
- package/dist/edv/hmac.js.map +1 -1
- package/dist/edv/recipients.d.ts +2 -0
- package/dist/edv/recipients.d.ts.map +1 -1
- package/dist/edv/recipients.js +12 -15
- package/dist/edv/recipients.js.map +1 -1
- package/dist/meta.d.ts +36 -11
- package/dist/meta.d.ts.map +1 -1
- package/dist/meta.js +95 -73
- package/dist/meta.js.map +1 -1
- package/dist/storage.d.ts +48 -13
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +40 -16
- package/dist/storage.js.map +1 -1
- package/dist/vc/issue.d.ts.map +1 -1
- package/dist/vc/issue.js +4 -7
- package/dist/vc/issue.js.map +1 -1
- package/dist/vc/verify.d.ts +10 -0
- package/dist/vc/verify.d.ts.map +1 -1
- package/dist/vc/verify.js +38 -1
- package/dist/vc/verify.js.map +1 -1
- package/dist/was/client.d.ts.map +1 -1
- package/dist/was/client.js +8 -2
- package/dist/was/client.js.map +1 -1
- package/dist/was/grant.d.ts +44 -0
- package/dist/was/grant.d.ts.map +1 -0
- package/dist/was/grant.js +78 -0
- package/dist/was/grant.js.map +1 -0
- package/dist/was/registry.d.ts.map +1 -1
- package/dist/was/registry.js +9 -2
- package/dist/was/registry.js.map +1 -1
- package/dist/zcap/delegate.d.ts.map +1 -1
- package/dist/zcap/delegate.js +1 -6
- package/dist/zcap/delegate.js.map +1 -1
- package/dist/zcap/signer.d.ts.map +1 -1
- package/dist/zcap/signer.js.map +1 -1
- package/package.json +5 -4
- package/dist/keys/webvh-driver.d.ts +0 -24
- package/dist/keys/webvh-driver.d.ts.map +0 -1
- package/dist/keys/webvh-driver.js +0 -73
- package/dist/keys/webvh-driver.js.map +0 -1
package/dist/commands/did.js
CHANGED
|
@@ -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
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
}
|