@interop/wallet-core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/enrollment/enrollment.d.ts +144 -0
- package/dist/enrollment/enrollment.d.ts.map +1 -0
- package/dist/enrollment/enrollment.js +296 -0
- package/dist/enrollment/enrollment.js.map +1 -0
- package/dist/enrollment/index.d.ts +20 -0
- package/dist/enrollment/index.d.ts.map +1 -0
- package/dist/enrollment/index.js +19 -0
- package/dist/enrollment/index.js.map +1 -0
- package/dist/identity/index.d.ts +0 -5
- package/dist/identity/index.d.ts.map +1 -1
- package/dist/identity/index.js +0 -4
- package/dist/identity/index.js.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/keyring/fetch.d.ts +22 -0
- package/dist/keyring/fetch.d.ts.map +1 -0
- package/dist/keyring/fetch.js +35 -0
- package/dist/keyring/fetch.js.map +1 -0
- package/dist/keyring/index.d.ts +30 -0
- package/dist/keyring/index.d.ts.map +1 -0
- package/dist/keyring/index.js +28 -0
- package/dist/keyring/index.js.map +1 -0
- package/dist/keyring/kdf.d.ts +102 -0
- package/dist/keyring/kdf.d.ts.map +1 -0
- package/dist/keyring/kdf.js +148 -0
- package/dist/keyring/kdf.js.map +1 -0
- package/dist/keyring/record.d.ts +98 -0
- package/dist/keyring/record.d.ts.map +1 -0
- package/dist/keyring/record.js +119 -0
- package/dist/keyring/record.js.map +1 -0
- package/dist/keyring/unlockSpace.d.ts +101 -0
- package/dist/keyring/unlockSpace.d.ts.map +1 -0
- package/dist/keyring/unlockSpace.js +226 -0
- package/dist/keyring/unlockSpace.js.map +1 -0
- package/dist/keys/index.d.ts +24 -0
- package/dist/keys/index.d.ts.map +1 -0
- package/dist/keys/index.js +22 -0
- package/dist/keys/index.js.map +1 -0
- package/dist/keys/puk.d.ts +42 -0
- package/dist/keys/puk.d.ts.map +1 -0
- package/dist/keys/puk.js +56 -0
- package/dist/keys/puk.js.map +1 -0
- package/dist/keys/pukRoster.d.ts +210 -0
- package/dist/keys/pukRoster.d.ts.map +1 -0
- package/dist/keys/pukRoster.js +263 -0
- package/dist/keys/pukRoster.js.map +1 -0
- package/dist/keys/rosterStore.d.ts +18 -0
- package/dist/keys/rosterStore.d.ts.map +1 -0
- package/dist/keys/rosterStore.js +38 -0
- package/dist/keys/rosterStore.js.map +1 -0
- package/dist/space/collections.d.ts +57 -0
- package/dist/space/collections.d.ts.map +1 -1
- package/dist/space/collections.js +48 -0
- package/dist/space/collections.js.map +1 -1
- package/dist/space/index.d.ts +4 -0
- package/dist/space/index.d.ts.map +1 -1
- package/dist/space/index.js +4 -0
- package/dist/space/index.js.map +1 -1
- package/dist/webvh/didWeb.d.ts +41 -0
- package/dist/webvh/didWeb.d.ts.map +1 -0
- package/dist/webvh/didWeb.js +24 -0
- package/dist/webvh/didWeb.js.map +1 -0
- package/dist/webvh/didWebvh.d.ts +251 -0
- package/dist/webvh/didWebvh.d.ts.map +1 -0
- package/dist/webvh/didWebvh.js +775 -0
- package/dist/webvh/didWebvh.js.map +1 -0
- package/dist/webvh/index.d.ts +27 -0
- package/dist/webvh/index.d.ts.map +1 -0
- package/dist/webvh/index.js +24 -0
- package/dist/webvh/index.js.map +1 -0
- package/dist/webvh/zcap.d.ts +104 -0
- package/dist/webvh/zcap.d.ts.map +1 -0
- package/dist/webvh/zcap.js +117 -0
- package/dist/webvh/zcap.js.map +1 -0
- package/package.json +30 -4
- package/dist/identity/collectionKeys.d.ts +0 -36
- package/dist/identity/collectionKeys.d.ts.map +0 -1
- package/dist/identity/collectionKeys.js +0 -92
- package/dist/identity/collectionKeys.js.map +0 -1
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* did:webvh hosting: provisions and publishes a hash-chained, self-certifying
|
|
6
|
+
* did:webvh DID log alongside the did:web document, in the same `id`
|
|
7
|
+
* collection of the user's WAS Space, with the log's update keys held by the
|
|
8
|
+
* wallet client itself (never by the KMS).
|
|
9
|
+
*
|
|
10
|
+
* The log (`did.jsonl`) is one more WAS Resource in the world-readable `id`
|
|
11
|
+
* collection (which carries a collection-level `PublicCanRead` policy), so
|
|
12
|
+
* hosting needs zero server changes:
|
|
13
|
+
* `did:webvh:<scid>:<host>:space:<spaceId>:id` resolves to
|
|
14
|
+
* `https://<host>/space/<spaceId>/id/did.jsonl`. Adopting the parallel
|
|
15
|
+
* `webDoc` (`did:web:` projection with `alsoKnownAs` cross-links) as the new
|
|
16
|
+
* `did.json` makes the log the single source of truth.
|
|
17
|
+
*
|
|
18
|
+
* The document is the enrolled-client roster: each enrolled client contributes
|
|
19
|
+
* its Ed25519 signing key (published under `authentication`,
|
|
20
|
+
* `assertionMethod`, `capabilityInvocation` and `capabilityDelegation`) and
|
|
21
|
+
* its X25519 key-agreement key (under `keyAgreement`, the source of record for
|
|
22
|
+
* PUK-wrap recipient keys -- which is why no server-held key appears there).
|
|
23
|
+
* The KMS-held `authentication` and `assertionMethod` keys stay as
|
|
24
|
+
* server-side conveniences.
|
|
25
|
+
*
|
|
26
|
+
* All protocol logic lives in `@interop/did-method-webvh`; this module is the
|
|
27
|
+
* glue: a `Signer` bridge over a client-held update-key seed, the idempotent,
|
|
28
|
+
* crash-resumable provisioning flow (`ensureDidWebvh`), the per-client
|
|
29
|
+
* update-key rotation ceremony (`rotateWebvhUpdateKey`), the two-entry client
|
|
30
|
+
* enrollment ceremony (`enrollWebvhClient`), and the lost-`keys.json` recovery
|
|
31
|
+
* path for the did:web relationship bindings (`repairKeyBindings`). Update-key
|
|
32
|
+
* seeds are minted here but persisted by the caller -- with client-held keys a
|
|
33
|
+
* lost seed is lost update authority, so every publish is preceded by a
|
|
34
|
+
* caller-durable write.
|
|
35
|
+
*
|
|
36
|
+
* The Space-side I/O runs through the narrow {@link WebvhIdStore} seam, which
|
|
37
|
+
* each wallet app satisfies with its own remote-store class.
|
|
38
|
+
*
|
|
39
|
+
* Prerotation convention: `nextKeyHashes` commits the hash of every client's
|
|
40
|
+
* ACTIVE update key alongside every staged key (the carry-over commitments).
|
|
41
|
+
* The resolver re-checks each entry's full (re-stated) `updateKeys` against
|
|
42
|
+
* the previous entry's `nextKeyHashes`, so without the active-key hashes no
|
|
43
|
+
* entry could ever keep a key authorized -- a multi-client log (an enrollment
|
|
44
|
+
* commit, a rotation that preserves the other clients' keys) depends on them.
|
|
45
|
+
* The trade is deliberate: an active update key can author non-rotating
|
|
46
|
+
* entries, where the original single-key convention forced every entry to
|
|
47
|
+
* reveal the staged key.
|
|
48
|
+
*/
|
|
49
|
+
import { createDID, deriveNextKeyHash, logToJsonlString, readLogFromString, resolveDIDFromLog, SCID_PLACEHOLDER, signerFromExternalKey, updateDID } from '@interop/did-method-webvh';
|
|
50
|
+
import { Ed25519VerificationKey } from '@interop/ed25519-verification-key';
|
|
51
|
+
import { DID_DOCUMENT_RESOURCE, DID_LOG_RESOURCE, ID_COLLECTION } from '../space/collections.js';
|
|
52
|
+
import { multibaseOf } from './didWeb.js';
|
|
53
|
+
/**
|
|
54
|
+
* The Multikey verification-method type the did:webvh data model uses for both
|
|
55
|
+
* the Ed25519 (authentication/assertionMethod/capability*) and X25519
|
|
56
|
+
* (keyAgreement) keys. The same key material and multibase are carried as by
|
|
57
|
+
* the 2020 suite types, only `type` and `@context` change, and credential
|
|
58
|
+
* verifiers verify `Ed25519Signature2020` / `eddsa-rdfc-2022` proofs against
|
|
59
|
+
* it.
|
|
60
|
+
*/
|
|
61
|
+
const MULTIKEY_VM_TYPE = 'Multikey';
|
|
62
|
+
/**
|
|
63
|
+
* The byte length of a did:webvh update-key seed (an Ed25519 secret seed).
|
|
64
|
+
*/
|
|
65
|
+
const UPDATE_SEED_BYTES = 32;
|
|
66
|
+
/**
|
|
67
|
+
* The `did:webvh:{SCID}:<host>:space:<spaceId>:id` controller template, with
|
|
68
|
+
* the literal `{SCID}` placeholder the library replaces at creation. The host
|
|
69
|
+
* segment percent-encodes a port (`localhost:8080` becomes `localhost%3A8080`),
|
|
70
|
+
* matching the library's `toDidDomainComponent`.
|
|
71
|
+
*
|
|
72
|
+
* @param options {object}
|
|
73
|
+
* @param options.wasServerUrl {string}
|
|
74
|
+
* @param options.spaceId {string}
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
export function didWebvhControllerTemplate({ wasServerUrl, spaceId }) {
|
|
78
|
+
const { host } = new URL(wasServerUrl);
|
|
79
|
+
return `did:webvh:${SCID_PLACEHOLDER}:${encodeURIComponent(host)}:space:${spaceId}:id`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Mints a fresh pair of client-held update-key seeds (active + staged) for a
|
|
83
|
+
* brand-new did:webvh log. The caller owns persistence: these seeds are the
|
|
84
|
+
* only update authority the log will ever accept, so they must be durable
|
|
85
|
+
* before {@link ensureDidWebvh} publishes anything.
|
|
86
|
+
*
|
|
87
|
+
* @returns {Promise<ClientWebvhUpdateKeys>}
|
|
88
|
+
*/
|
|
89
|
+
export async function mintClientWebvhUpdateKeys() {
|
|
90
|
+
return {
|
|
91
|
+
updateSeed: randomSeed(),
|
|
92
|
+
stagedSeed: randomSeed()
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A fresh 32-byte Ed25519 update-key seed.
|
|
97
|
+
*
|
|
98
|
+
* @returns {Uint8Array}
|
|
99
|
+
*/
|
|
100
|
+
function randomSeed() {
|
|
101
|
+
return crypto.getRandomValues(new Uint8Array(UPDATE_SEED_BYTES));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The `publicKeyMultibase` of the Ed25519 update key a seed derives, as it
|
|
105
|
+
* appears in the log's `parameters.updateKeys`.
|
|
106
|
+
*
|
|
107
|
+
* @param options {object}
|
|
108
|
+
* @param options.seed {Uint8Array} a 32-byte Ed25519 seed
|
|
109
|
+
* @returns {Promise<string>}
|
|
110
|
+
*/
|
|
111
|
+
export async function updateKeyMultibase({ seed }) {
|
|
112
|
+
const keyPair = await Ed25519VerificationKey.generate({ seed });
|
|
113
|
+
return keyPair.publicKeyMultibase;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Bridges a client-held update-key seed to the did:webvh `Signer` interface via
|
|
117
|
+
* the library's `signerFromExternalKey`. The only wallet-side seam is the shape
|
|
118
|
+
* adapter: the key pair's `signer().sign({ data })` matches the factory's
|
|
119
|
+
* `sign({ data })` bridge exactly, so the proof-value multibase encoding and
|
|
120
|
+
* the load-bearing `did:key:<pkm>#<pkm>` verification-method id (which the
|
|
121
|
+
* resolver matches against the entry's authorized `updateKeys`) are owned by
|
|
122
|
+
* the library, not duplicated here.
|
|
123
|
+
*
|
|
124
|
+
* @param options {object}
|
|
125
|
+
* @param options.seed {Uint8Array} the 32-byte update-key seed
|
|
126
|
+
* @returns {Promise<Signer>}
|
|
127
|
+
*/
|
|
128
|
+
async function updateKeySigner({ seed }) {
|
|
129
|
+
const keyPair = await Ed25519VerificationKey.generate({ seed });
|
|
130
|
+
const { publicKeyMultibase } = keyPair;
|
|
131
|
+
// The key pair refuses to hand out a signer without an id; the did:key form
|
|
132
|
+
// is also the verification-method id the resolver matches against the log's
|
|
133
|
+
// authorized updateKeys.
|
|
134
|
+
keyPair.id = `did:key:${publicKeyMultibase}#${publicKeyMultibase}`;
|
|
135
|
+
const keySigner = keyPair.signer();
|
|
136
|
+
return signerFromExternalKey({
|
|
137
|
+
publicKeyMultibase,
|
|
138
|
+
sign: async ({ data }) => {
|
|
139
|
+
const signature = await keySigner.sign({ data });
|
|
140
|
+
// Re-wrap as a plain Uint8Array: a signer may return a Node Buffer (or
|
|
141
|
+
// a cross-realm view), which the library's strict byte check rejects.
|
|
142
|
+
return new Uint8Array(signature.buffer, signature.byteOffset, signature.byteLength);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Assembles the document's verification methods as `{SCID}`-templated Multikey
|
|
148
|
+
* entries for the create entry: the KMS-held authentication and assertionMethod
|
|
149
|
+
* keys (server-side conveniences), plus the enrolled client's own key set --
|
|
150
|
+
* its Ed25519 signing key under all four Ed25519 relationships and its X25519
|
|
151
|
+
* key-agreement key as the sole `keyAgreement` entry. No server-held key is a
|
|
152
|
+
* wrap target, so the KMS keyAgreement key is deliberately absent.
|
|
153
|
+
*
|
|
154
|
+
* Each id carries the full `publicKeyMultibase` fragment, so `createDID` mints
|
|
155
|
+
* `did:webvh:<scid>:...#<multibase>` ids -- no KMS read.
|
|
156
|
+
*
|
|
157
|
+
* @param options {object}
|
|
158
|
+
* @param options.controllerTemplate {string} the `{SCID}` controller id
|
|
159
|
+
* @param options.didWebKeys {DidWebKeyMap}
|
|
160
|
+
* @param options.clientKeys {WebvhClientKeys}
|
|
161
|
+
* @returns {object} `verificationMethods` + relationship arrays for createDID
|
|
162
|
+
*/
|
|
163
|
+
function assembleWebvhVerificationMethods({ controllerTemplate, didWebKeys, clientKeys }) {
|
|
164
|
+
const vmId = (publicKeyMultibase) => `${controllerTemplate}#${publicKeyMultibase}`;
|
|
165
|
+
const method = (publicKeyMultibase) => ({
|
|
166
|
+
id: vmId(publicKeyMultibase),
|
|
167
|
+
type: MULTIKEY_VM_TYPE,
|
|
168
|
+
controller: controllerTemplate,
|
|
169
|
+
publicKeyMultibase
|
|
170
|
+
});
|
|
171
|
+
const kmsMultibase = (key) => multibaseOf(key.vmId);
|
|
172
|
+
const kmsAuthentication = kmsMultibase(didWebKeys.authentication);
|
|
173
|
+
const kmsAssertionMethod = kmsMultibase(didWebKeys.assertionMethod);
|
|
174
|
+
const { signingKeyMultibase, keyAgreementKeyMultibase } = clientKeys;
|
|
175
|
+
return {
|
|
176
|
+
verificationMethods: [
|
|
177
|
+
method(kmsAuthentication),
|
|
178
|
+
method(kmsAssertionMethod),
|
|
179
|
+
method(signingKeyMultibase),
|
|
180
|
+
method(keyAgreementKeyMultibase)
|
|
181
|
+
],
|
|
182
|
+
authentication: [vmId(kmsAuthentication), vmId(signingKeyMultibase)],
|
|
183
|
+
assertionMethod: [vmId(kmsAssertionMethod), vmId(signingKeyMultibase)],
|
|
184
|
+
keyAgreement: [vmId(keyAgreementKeyMultibase)],
|
|
185
|
+
capabilityInvocation: [vmId(signingKeyMultibase)],
|
|
186
|
+
capabilityDelegation: [vmId(signingKeyMultibase)]
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Creates the one-entry did:webvh log and its parallel `webDoc`. `portable:
|
|
191
|
+
* true` is set at entry 1 (it can only be enabled there); the document's
|
|
192
|
+
* verification methods come from {@link assembleWebvhVerificationMethods},
|
|
193
|
+
* signed by the client-held active update key with prerotation committed to
|
|
194
|
+
* the staged key.
|
|
195
|
+
*
|
|
196
|
+
* @param options {object}
|
|
197
|
+
* @param options.wasServerUrl {string}
|
|
198
|
+
* @param options.spaceId {string}
|
|
199
|
+
* @param options.didWebKeys {DidWebKeyMap}
|
|
200
|
+
* @param options.clientKeys {WebvhClientKeys}
|
|
201
|
+
* @param options.updateKeyPublicKeyMultibase {string}
|
|
202
|
+
* @param options.nextKeyHashes {string[]}
|
|
203
|
+
* @param options.signer {Signer}
|
|
204
|
+
* @returns {Promise<{ log: DIDLog; webDoc: object; did: string }>}
|
|
205
|
+
*/
|
|
206
|
+
async function createWebvhLog({ wasServerUrl, spaceId, didWebKeys, clientKeys, updateKeyPublicKeyMultibase, nextKeyHashes, signer }) {
|
|
207
|
+
const { host } = new URL(wasServerUrl);
|
|
208
|
+
const controllerTemplate = didWebvhControllerTemplate({
|
|
209
|
+
wasServerUrl,
|
|
210
|
+
spaceId
|
|
211
|
+
});
|
|
212
|
+
const { verificationMethods, authentication, assertionMethod, keyAgreement, capabilityInvocation, capabilityDelegation } = assembleWebvhVerificationMethods({
|
|
213
|
+
controllerTemplate,
|
|
214
|
+
didWebKeys,
|
|
215
|
+
clientKeys
|
|
216
|
+
});
|
|
217
|
+
const result = await createDID({
|
|
218
|
+
address: host,
|
|
219
|
+
paths: ['space', spaceId, ID_COLLECTION.id],
|
|
220
|
+
signer,
|
|
221
|
+
updateKeys: [updateKeyPublicKeyMultibase],
|
|
222
|
+
nextKeyHashes,
|
|
223
|
+
verificationMethods,
|
|
224
|
+
authentication,
|
|
225
|
+
assertionMethod,
|
|
226
|
+
keyAgreement,
|
|
227
|
+
capabilityInvocation,
|
|
228
|
+
capabilityDelegation,
|
|
229
|
+
alsoKnownAsWeb: true,
|
|
230
|
+
portable: true
|
|
231
|
+
});
|
|
232
|
+
if (!result.webDoc) {
|
|
233
|
+
throw new Error('createDID did not return a webDoc despite alsoKnownAsWeb.');
|
|
234
|
+
}
|
|
235
|
+
return { log: result.log, webDoc: result.webDoc, did: result.did };
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Publishes an already-created log: PUT `did.jsonl` (`text/jsonl`) then PUT
|
|
239
|
+
* `did.json` from `webDoc` (`application/did+json`, adopting the webvh
|
|
240
|
+
* projection). Both land in the `id` collection, whose collection-level
|
|
241
|
+
* `PublicCanRead` policy (set at provisioning) makes them world-readable, so
|
|
242
|
+
* the publish tail no longer sets per-resource policies. The shared publish
|
|
243
|
+
* tail of the create and rotate paths.
|
|
244
|
+
*
|
|
245
|
+
* @param options {object}
|
|
246
|
+
* @param options.idStore {WebvhIdStore}
|
|
247
|
+
* @param options.log {DIDLog}
|
|
248
|
+
* @param options.webDoc {object}
|
|
249
|
+
* @returns {Promise<void>}
|
|
250
|
+
*/
|
|
251
|
+
async function publishWebvhLog({ idStore, log, webDoc }) {
|
|
252
|
+
await idStore.putIdResource({
|
|
253
|
+
resourceId: DID_LOG_RESOURCE,
|
|
254
|
+
content: logToJsonlString(log),
|
|
255
|
+
contentType: 'text/jsonl'
|
|
256
|
+
});
|
|
257
|
+
await idStore.putIdResource({
|
|
258
|
+
resourceId: DID_DOCUMENT_RESOURCE,
|
|
259
|
+
content: webDoc,
|
|
260
|
+
contentType: 'application/did+json'
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Writes `keys.json` v2: the did:web relationship map plus the `webvh` block,
|
|
265
|
+
* preserving the three did:web relationships.
|
|
266
|
+
*
|
|
267
|
+
* @param options {object}
|
|
268
|
+
* @param options.idStore {WebvhIdStore}
|
|
269
|
+
* @param options.didWebKeys {DidWebKeyMap}
|
|
270
|
+
* @param options.webvh {DidWebvhBlock}
|
|
271
|
+
* @returns {Promise<void>}
|
|
272
|
+
*/
|
|
273
|
+
async function writeKeysJson({ idStore, didWebKeys, webvh }) {
|
|
274
|
+
const content = { ...didWebKeys, webvh };
|
|
275
|
+
await idStore.putKeyMap({ content });
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Reads and resolves the published `did.jsonl`, or returns `undefined` when
|
|
279
|
+
* the log is not published. A log that exists but fails to resolve throws --
|
|
280
|
+
* a published-but-broken log is never silently re-created over.
|
|
281
|
+
*
|
|
282
|
+
* @param options {object}
|
|
283
|
+
* @param options.idStore {WebvhIdStore}
|
|
284
|
+
* @returns {Promise<PublishedWebvhLog | undefined>}
|
|
285
|
+
*/
|
|
286
|
+
async function readPublishedLog({ idStore }) {
|
|
287
|
+
const logText = await idStore.getIdResourceRaw({
|
|
288
|
+
resourceId: DID_LOG_RESOURCE
|
|
289
|
+
});
|
|
290
|
+
if (logText === undefined) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
const log = readLogFromString(logText);
|
|
294
|
+
const resolved = await resolveDIDFromLog(log);
|
|
295
|
+
if (resolved.meta.error || !resolved.did || !resolved.doc) {
|
|
296
|
+
throw new Error(`did:webvh: existing did.jsonl failed to resolve (${resolved.meta.error}).`);
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
log,
|
|
300
|
+
did: resolved.did,
|
|
301
|
+
doc: resolved.doc,
|
|
302
|
+
updateKeys: resolved.meta.updateKeys ?? [],
|
|
303
|
+
nextKeyHashes: resolved.meta.nextKeyHashes ?? []
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* The `publicKeyMultibase` of every update-key seed this client holds, in
|
|
308
|
+
* role order (active, staged, pending).
|
|
309
|
+
*
|
|
310
|
+
* @param options {object}
|
|
311
|
+
* @param options.updateKeys {ClientWebvhUpdateKeys}
|
|
312
|
+
* @returns {Promise<{ update: string; staged: string; pendingStaged?: string }>}
|
|
313
|
+
*/
|
|
314
|
+
async function updateKeyMultibases({ updateKeys }) {
|
|
315
|
+
return {
|
|
316
|
+
update: await updateKeyMultibase({ seed: updateKeys.updateSeed }),
|
|
317
|
+
staged: await updateKeyMultibase({ seed: updateKeys.stagedSeed }),
|
|
318
|
+
pendingStaged: updateKeys.pendingStagedSeed
|
|
319
|
+
? await updateKeyMultibase({ seed: updateKeys.pendingStagedSeed })
|
|
320
|
+
: undefined
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The published-but-not-finalized state of a rotation, if the log is in one:
|
|
325
|
+
* the seed the log has already promoted to active, plus the seed it committed
|
|
326
|
+
* as the next key (absent when this client no longer holds it, which is
|
|
327
|
+
* unrecoverable).
|
|
328
|
+
*
|
|
329
|
+
* @param options {object}
|
|
330
|
+
* @param options.published {object} the resolved log's authorized updateKeys
|
|
331
|
+
* @param options.multibases {object} this client's update-key multibases
|
|
332
|
+
* @param options.updateKeys {ClientWebvhUpdateKeys}
|
|
333
|
+
* @returns {object | undefined}
|
|
334
|
+
*/
|
|
335
|
+
function advancedSeeds({ published, multibases, updateKeys }) {
|
|
336
|
+
if (published.updateKeys.includes(multibases.staged)) {
|
|
337
|
+
return {
|
|
338
|
+
updateSeed: updateKeys.stagedSeed,
|
|
339
|
+
stagedSeed: updateKeys.pendingStagedSeed
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (multibases.pendingStaged &&
|
|
343
|
+
updateKeys.pendingStagedSeed &&
|
|
344
|
+
published.updateKeys.includes(multibases.pendingStaged)) {
|
|
345
|
+
return { updateSeed: updateKeys.pendingStagedSeed };
|
|
346
|
+
}
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Idempotently provisions and publishes the user's did:webvh DID log, run
|
|
351
|
+
* directly after the did:web provisioning (non-fatal). The durable anchor is
|
|
352
|
+
* the caller-persisted update-key seeds, so the flow is a simple probe:
|
|
353
|
+
*
|
|
354
|
+
* - `did.jsonl` published: sanity-check that the log's authorized `updateKeys`
|
|
355
|
+
* still name one of this client's seeds (active, staged, or pending -- a
|
|
356
|
+
* rotation in flight finalizes separately), adopt the resolved DID, and
|
|
357
|
+
* write the `keys.json` webvh block if it is missing or stale.
|
|
358
|
+
* - `did.jsonl` absent: create the log with the active update key, prerotation
|
|
359
|
+
* committed to the staged key, publish log + `did.json`, then record the DID
|
|
360
|
+
* in `keys.json`.
|
|
361
|
+
*
|
|
362
|
+
* A published log whose `updateKeys` match none of the seeds is fatal: with
|
|
363
|
+
* client-held update keys a lost seed is lost update authority, and no KMS
|
|
364
|
+
* repair path exists by design.
|
|
365
|
+
*
|
|
366
|
+
* @param options {object}
|
|
367
|
+
* @param options.idStore {WebvhIdStore}
|
|
368
|
+
* @param options.wasServerUrl {string}
|
|
369
|
+
* @param options.spaceId {string}
|
|
370
|
+
* @param options.didWebKeys {DidWebKeyMapV2} the parsed keys.json (with any
|
|
371
|
+
* webvh block) returned by the did:web provisioning.
|
|
372
|
+
* @param options.clientKeys {WebvhClientKeys} this client's published keys
|
|
373
|
+
* @param options.updateKeys {ClientWebvhUpdateKeys} already durably persisted
|
|
374
|
+
* @returns {Promise<{ did: string }>}
|
|
375
|
+
*/
|
|
376
|
+
export async function ensureDidWebvh({ idStore, wasServerUrl, spaceId, didWebKeys, clientKeys, updateKeys }) {
|
|
377
|
+
const published = await readPublishedLog({ idStore });
|
|
378
|
+
const multibases = await updateKeyMultibases({ updateKeys });
|
|
379
|
+
if (published) {
|
|
380
|
+
// Adoption: the log is already public (this client provisioned it, or a
|
|
381
|
+
// torn earlier run published before recording the did). Accept any of the
|
|
382
|
+
// three roles -- an interrupted rotation leaves the log at the staged or
|
|
383
|
+
// pending key, and rotateWebvhUpdateKey finalizes it.
|
|
384
|
+
const authorized = [
|
|
385
|
+
multibases.update,
|
|
386
|
+
multibases.staged,
|
|
387
|
+
multibases.pendingStaged
|
|
388
|
+
].some(publicKeyMultibase => publicKeyMultibase !== undefined &&
|
|
389
|
+
published.updateKeys.includes(publicKeyMultibase));
|
|
390
|
+
if (!authorized) {
|
|
391
|
+
throw new Error("did:webvh: the published did.jsonl authorizes none of this client's " +
|
|
392
|
+
'update keys -- the update-key seed is lost and the log can never ' +
|
|
393
|
+
'be updated again.');
|
|
394
|
+
}
|
|
395
|
+
if (didWebKeys.webvh?.did !== published.did) {
|
|
396
|
+
await writeKeysJson({
|
|
397
|
+
idStore,
|
|
398
|
+
didWebKeys,
|
|
399
|
+
webvh: { did: published.did }
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return { did: published.did };
|
|
403
|
+
}
|
|
404
|
+
const signer = await updateKeySigner({ seed: updateKeys.updateSeed });
|
|
405
|
+
const created = await createWebvhLog({
|
|
406
|
+
wasServerUrl,
|
|
407
|
+
spaceId,
|
|
408
|
+
didWebKeys,
|
|
409
|
+
clientKeys,
|
|
410
|
+
updateKeyPublicKeyMultibase: multibases.update,
|
|
411
|
+
// The active key's own hash is committed beside the staged key's (the
|
|
412
|
+
// carry-over convention in the module doc): the resolver checks every
|
|
413
|
+
// later entry's re-stated updateKeys against these commitments, so
|
|
414
|
+
// without it no non-rotating entry (an enrollment commit) could follow.
|
|
415
|
+
nextKeyHashes: [
|
|
416
|
+
await deriveNextKeyHash(multibases.update),
|
|
417
|
+
await deriveNextKeyHash(multibases.staged)
|
|
418
|
+
],
|
|
419
|
+
signer
|
|
420
|
+
});
|
|
421
|
+
await publishWebvhLog({
|
|
422
|
+
idStore,
|
|
423
|
+
log: created.log,
|
|
424
|
+
webDoc: created.webDoc
|
|
425
|
+
});
|
|
426
|
+
await writeKeysJson({
|
|
427
|
+
idStore,
|
|
428
|
+
didWebKeys,
|
|
429
|
+
webvh: { did: created.did }
|
|
430
|
+
});
|
|
431
|
+
return { did: created.did };
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Rotates this client's did:webvh update key (the user-triggered ceremony on
|
|
435
|
+
* the wallet's settings screen). The staged key is revealed to sign its own
|
|
436
|
+
* activation and become the sole active update key, a freshly minted staged
|
|
437
|
+
* key is committed as the new `nextKeyHashes`, and the caller's persisted
|
|
438
|
+
* seeds roll forward. No KMS and no `keys.json` involvement: the update keys
|
|
439
|
+
* are client-held, and the published DID does not change.
|
|
440
|
+
*
|
|
441
|
+
* The persist-before-publish invariant is load-bearing: the new staged seed is
|
|
442
|
+
* handed to `persistUpdateKeys` (as `pendingStagedSeed`) and awaited BEFORE the
|
|
443
|
+
* log entry committing it is published, so no published log can ever depend on
|
|
444
|
+
* a seed that is not durable. A crash between publish and finalize is
|
|
445
|
+
* recovered on the next run: the log already sits at the staged (or pending)
|
|
446
|
+
* key, and the seeds are simply rolled forward locally without touching it.
|
|
447
|
+
*
|
|
448
|
+
* @param options {object}
|
|
449
|
+
* @param options.idStore {WebvhIdStore}
|
|
450
|
+
* @param options.updateKeys {ClientWebvhUpdateKeys} the current seeds
|
|
451
|
+
* @param options.persistUpdateKeys {Function} awaited before every publish
|
|
452
|
+
* that changes the log's authorized update keys
|
|
453
|
+
* @returns {Promise<{ did: string }>}
|
|
454
|
+
*/
|
|
455
|
+
export async function rotateWebvhUpdateKey({ idStore, updateKeys, persistUpdateKeys }) {
|
|
456
|
+
const published = await readPublishedLog({ idStore });
|
|
457
|
+
if (!published) {
|
|
458
|
+
throw new Error('did:webvh: did.jsonl is missing; nothing to rotate.');
|
|
459
|
+
}
|
|
460
|
+
const multibases = await updateKeyMultibases({ updateKeys });
|
|
461
|
+
// Crash recovery: a prior ceremony published the extended log but never
|
|
462
|
+
// finalized the local seeds, so the log already sits at a key this client
|
|
463
|
+
// holds without having promoted it. Roll the seeds forward and stop --
|
|
464
|
+
// re-signing would fork the log.
|
|
465
|
+
const advanced = advancedSeeds({ published, multibases, updateKeys });
|
|
466
|
+
if (advanced) {
|
|
467
|
+
if (!advanced.stagedSeed) {
|
|
468
|
+
// The active key is recovered, but the next key that entry committed was
|
|
469
|
+
// never persisted here, so no future entry can reveal it.
|
|
470
|
+
throw new Error('did:webvh: the published log has advanced to a key whose committed ' +
|
|
471
|
+
'next key this client does not hold; the log can no longer be ' +
|
|
472
|
+
'rotated.');
|
|
473
|
+
}
|
|
474
|
+
await persistUpdateKeys({
|
|
475
|
+
updateSeed: advanced.updateSeed,
|
|
476
|
+
stagedSeed: advanced.stagedSeed
|
|
477
|
+
});
|
|
478
|
+
return { did: published.did };
|
|
479
|
+
}
|
|
480
|
+
// Diverged-state guard: the log must still be at this client's active update
|
|
481
|
+
// key, or another client has rotated it out from under us.
|
|
482
|
+
if (!published.updateKeys.includes(multibases.update)) {
|
|
483
|
+
throw new Error("did:webvh: the published log has diverged from this client's update " +
|
|
484
|
+
'keys (it authorizes neither the active nor the staged key); ' +
|
|
485
|
+
'refusing to rotate.');
|
|
486
|
+
}
|
|
487
|
+
// Mint the NEW staged seed and persist it BEFORE publishing anything, keeping
|
|
488
|
+
// the current roles intact until the ceremony finalizes.
|
|
489
|
+
const newStagedSeed = randomSeed();
|
|
490
|
+
await persistUpdateKeys({
|
|
491
|
+
updateSeed: updateKeys.updateSeed,
|
|
492
|
+
stagedSeed: updateKeys.stagedSeed,
|
|
493
|
+
pendingStagedSeed: newStagedSeed
|
|
494
|
+
});
|
|
495
|
+
// Extend the log. The revealed staged key signs its own activation and
|
|
496
|
+
// replaces THIS client's active update key; every other enrolled client's
|
|
497
|
+
// key is preserved (their hashes ride the carry-over commitments, so the
|
|
498
|
+
// re-stated set still resolves). The retired key's hash is dropped and the
|
|
499
|
+
// new staged key's committed. updateDID is sparse (no document directives)
|
|
500
|
+
// so the verification methods are preserved -- a key-only rotation.
|
|
501
|
+
const signer = await updateKeySigner({ seed: updateKeys.stagedSeed });
|
|
502
|
+
const retiredKeyHash = await deriveNextKeyHash(multibases.update);
|
|
503
|
+
const updated = await updateDID({
|
|
504
|
+
log: published.log,
|
|
505
|
+
signer,
|
|
506
|
+
updateKeys: [
|
|
507
|
+
...published.updateKeys.filter(key => key !== multibases.update),
|
|
508
|
+
multibases.staged
|
|
509
|
+
],
|
|
510
|
+
nextKeyHashes: [
|
|
511
|
+
...new Set([
|
|
512
|
+
...published.nextKeyHashes.filter(hash => hash !== retiredKeyHash),
|
|
513
|
+
await deriveNextKeyHash(multibases.staged),
|
|
514
|
+
await deriveNextKeyHash(await updateKeyMultibase({ seed: newStagedSeed }))
|
|
515
|
+
])
|
|
516
|
+
]
|
|
517
|
+
});
|
|
518
|
+
if (!updated.webDoc) {
|
|
519
|
+
throw new Error('did:webvh: updateDID returned no webDoc despite the did:web alsoKnownAs.');
|
|
520
|
+
}
|
|
521
|
+
// Publish the extended log and republish did.json (its did:web projection),
|
|
522
|
+
// then finalize the local seeds: the staged key is now active, the pending
|
|
523
|
+
// one is the new staged key.
|
|
524
|
+
await publishWebvhLog({
|
|
525
|
+
idStore,
|
|
526
|
+
log: updated.log,
|
|
527
|
+
webDoc: updated.webDoc
|
|
528
|
+
});
|
|
529
|
+
await persistUpdateKeys({
|
|
530
|
+
updateSeed: updateKeys.stagedSeed,
|
|
531
|
+
stagedSeed: newStagedSeed
|
|
532
|
+
});
|
|
533
|
+
return { did: updated.did };
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* The relationship references of a resolved document as verification-method
|
|
537
|
+
* ids, tolerating embedded objects beside string references.
|
|
538
|
+
*
|
|
539
|
+
* @param relation {Array} the relationship array, when present
|
|
540
|
+
* @returns {string[]}
|
|
541
|
+
*/
|
|
542
|
+
function relationIds(relation) {
|
|
543
|
+
const ids = [];
|
|
544
|
+
for (const entry of relation ?? []) {
|
|
545
|
+
const id = typeof entry === 'string' ? entry : entry?.id;
|
|
546
|
+
if (id) {
|
|
547
|
+
ids.push(id);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return ids;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Enrolls a second wallet client into the published did:webvh document -- the
|
|
554
|
+
* log half of the enrollment ceremony (the PUK roster wrap happens first,
|
|
555
|
+
* outside this module). Two entries, forced by prerotation (a new update key
|
|
556
|
+
* must hash into the PREVIOUS entry's `nextKeyHashes`):
|
|
557
|
+
*
|
|
558
|
+
* 1. **Commit**: a sparse entry extending `nextKeyHashes` with the new
|
|
559
|
+
* client's update-key and staged-key hashes (document and `updateKeys`
|
|
560
|
+
* untouched).
|
|
561
|
+
* 2. **Add**: an entry adding the new client's verification methods (its
|
|
562
|
+
* Ed25519 key under the four signing relationships, its X25519 twin under
|
|
563
|
+
* `keyAgreement`) and its update key to `updateKeys`.
|
|
564
|
+
*
|
|
565
|
+
* Both entries are signed by THIS client's active update key (quorum-of-one:
|
|
566
|
+
* any single enrolled client can enroll). The ceremony is resumable from
|
|
567
|
+
* durable state alone: a tear after the commit is detected by its hashes
|
|
568
|
+
* already standing in `nextKeyHashes` (skip to the add entry), and a
|
|
569
|
+
* completed enrollment is detected by the update key already being authorized
|
|
570
|
+
* (no-op). Re-running with the same key set converges without forking the
|
|
571
|
+
* log.
|
|
572
|
+
*
|
|
573
|
+
* @param options {object}
|
|
574
|
+
* @param options.idStore {WebvhIdStore}
|
|
575
|
+
* @param options.updateKeys {ClientWebvhUpdateKeys} THIS client's seeds
|
|
576
|
+
* @param options.newClient {WebvhEnrollmentKeys} the enrollee's public halves
|
|
577
|
+
* @returns {Promise<{ did: string }>}
|
|
578
|
+
*/
|
|
579
|
+
export async function enrollWebvhClient({ idStore, updateKeys, newClient }) {
|
|
580
|
+
let published = await readPublishedLog({ idStore });
|
|
581
|
+
if (!published) {
|
|
582
|
+
throw new Error('did:webvh: did.jsonl is missing; nothing to enroll into.');
|
|
583
|
+
}
|
|
584
|
+
const multibases = await updateKeyMultibases({ updateKeys });
|
|
585
|
+
// Already enrolled (a completed earlier run): the new client's update key is
|
|
586
|
+
// authorized, which only the add entry writes. Idempotent no-op.
|
|
587
|
+
if (published.updateKeys.includes(newClient.updateKeyMultibase)) {
|
|
588
|
+
return { did: published.did };
|
|
589
|
+
}
|
|
590
|
+
// Both entries are signed by this client's active update key; a log that
|
|
591
|
+
// does not authorize it (a rotation torn elsewhere) must heal first.
|
|
592
|
+
if (!published.updateKeys.includes(multibases.update)) {
|
|
593
|
+
throw new Error("did:webvh: the published log does not authorize this client's active " +
|
|
594
|
+
'update key; finalize the pending rotation before enrolling.');
|
|
595
|
+
}
|
|
596
|
+
const newUpdateKeyHash = await deriveNextKeyHash(newClient.updateKeyMultibase);
|
|
597
|
+
const newStagedKeyHash = await deriveNextKeyHash(newClient.stagedUpdateKeyMultibase);
|
|
598
|
+
// The commit entry (skipped when a torn earlier run already published it).
|
|
599
|
+
const committed = published.nextKeyHashes.includes(newUpdateKeyHash) &&
|
|
600
|
+
published.nextKeyHashes.includes(newStagedKeyHash);
|
|
601
|
+
if (!committed) {
|
|
602
|
+
// A sparse entry re-states the authorized updateKeys, and the resolver
|
|
603
|
+
// checks each against the PREVIOUS entry's commitments -- so every
|
|
604
|
+
// currently authorized key's hash must already stand in nextKeyHashes
|
|
605
|
+
// (the carry-over convention). A log minted before the convention cannot
|
|
606
|
+
// take a non-rotating entry.
|
|
607
|
+
for (const key of published.updateKeys) {
|
|
608
|
+
if (!published.nextKeyHashes.includes(await deriveNextKeyHash(key))) {
|
|
609
|
+
throw new Error('did:webvh: the published log does not carry the active update ' +
|
|
610
|
+
"keys' own hashes in nextKeyHashes (it predates the carry-over " +
|
|
611
|
+
'commitment convention); re-provision the account before ' +
|
|
612
|
+
'enrolling.');
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const signer = await updateKeySigner({ seed: updateKeys.updateSeed });
|
|
616
|
+
const updated = await updateDID({
|
|
617
|
+
log: published.log,
|
|
618
|
+
signer,
|
|
619
|
+
// Re-stated unchanged (the library requires them explicitly while
|
|
620
|
+
// prerotation is active); the carry-over commitments are what make the
|
|
621
|
+
// re-statement resolvable.
|
|
622
|
+
updateKeys: published.updateKeys,
|
|
623
|
+
nextKeyHashes: [
|
|
624
|
+
...new Set([
|
|
625
|
+
...published.nextKeyHashes,
|
|
626
|
+
newUpdateKeyHash,
|
|
627
|
+
newStagedKeyHash
|
|
628
|
+
])
|
|
629
|
+
]
|
|
630
|
+
});
|
|
631
|
+
if (!updated.webDoc) {
|
|
632
|
+
throw new Error('did:webvh: updateDID returned no webDoc despite the did:web alsoKnownAs.');
|
|
633
|
+
}
|
|
634
|
+
await publishWebvhLog({
|
|
635
|
+
idStore,
|
|
636
|
+
log: updated.log,
|
|
637
|
+
webDoc: updated.webDoc
|
|
638
|
+
});
|
|
639
|
+
// Re-read through the same verifying path the resume case uses, so the
|
|
640
|
+
// add entry below always builds on the published, resolved state.
|
|
641
|
+
published = await readPublishedLog({ idStore });
|
|
642
|
+
if (!published) {
|
|
643
|
+
throw new Error('did:webvh: did.jsonl vanished mid-enrollment.');
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// The add entry: the new client's two verification methods and its update
|
|
647
|
+
// key, on top of the full existing document (updateDID replaces the
|
|
648
|
+
// verification-method set and relationship arrays wholesale).
|
|
649
|
+
const { did, doc, log, updateKeys: authorizedKeys, nextKeyHashes } = published;
|
|
650
|
+
const vmId = (publicKeyMultibase) => `${did}#${publicKeyMultibase}`;
|
|
651
|
+
const addedMethods = [
|
|
652
|
+
newClient.signingKeyMultibase,
|
|
653
|
+
newClient.keyAgreementKeyMultibase
|
|
654
|
+
].map(publicKeyMultibase => ({
|
|
655
|
+
id: vmId(publicKeyMultibase),
|
|
656
|
+
type: MULTIKEY_VM_TYPE,
|
|
657
|
+
controller: did,
|
|
658
|
+
publicKeyMultibase
|
|
659
|
+
}));
|
|
660
|
+
const existingMethods = (doc.verificationMethod ?? []);
|
|
661
|
+
const verificationMethods = [
|
|
662
|
+
...existingMethods.filter(method => !addedMethods.some(added => added.id === method.id)),
|
|
663
|
+
...addedMethods
|
|
664
|
+
];
|
|
665
|
+
const withReference = (relation, id) => [...new Set([...relationIds(relation), id])];
|
|
666
|
+
const signingVmId = vmId(newClient.signingKeyMultibase);
|
|
667
|
+
const signer = await updateKeySigner({ seed: updateKeys.updateSeed });
|
|
668
|
+
const updated = await updateDID({
|
|
669
|
+
log,
|
|
670
|
+
signer,
|
|
671
|
+
updateKeys: [...new Set([...authorizedKeys, newClient.updateKeyMultibase])],
|
|
672
|
+
nextKeyHashes,
|
|
673
|
+
verificationMethods,
|
|
674
|
+
authentication: withReference(doc.authentication, signingVmId),
|
|
675
|
+
assertionMethod: withReference(doc.assertionMethod, signingVmId),
|
|
676
|
+
keyAgreement: withReference(doc.keyAgreement, vmId(newClient.keyAgreementKeyMultibase)),
|
|
677
|
+
capabilityInvocation: withReference(doc.capabilityInvocation, signingVmId),
|
|
678
|
+
capabilityDelegation: withReference(doc.capabilityDelegation, signingVmId)
|
|
679
|
+
});
|
|
680
|
+
if (!updated.webDoc) {
|
|
681
|
+
throw new Error('did:webvh: updateDID returned no webDoc despite the did:web alsoKnownAs.');
|
|
682
|
+
}
|
|
683
|
+
await publishWebvhLog({
|
|
684
|
+
idStore,
|
|
685
|
+
log: updated.log,
|
|
686
|
+
webDoc: updated.webDoc
|
|
687
|
+
});
|
|
688
|
+
return { did: updated.did };
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Rebuilds `keys.json` from the published artifacts plus a WebKMS key listing
|
|
692
|
+
* -- the recovery path for a lost or rolled-back `keys.json`. List Keys is
|
|
693
|
+
* authorized as `read` against the keystore controller, which only the
|
|
694
|
+
* root-controlled keystore agent can invoke.
|
|
695
|
+
*
|
|
696
|
+
* KMS key local ids are server-generated random and appear in no published
|
|
697
|
+
* artifact, so the bindings are rediscovered by public key material instead.
|
|
698
|
+
* List the keystore once -- each listed description carries `keyUrl`, the
|
|
699
|
+
* key's canonical invocation URL (the signable handle its alias-overridden
|
|
700
|
+
* `id` erases) -- then match `did.json`'s three relationship verification
|
|
701
|
+
* methods by `publicKeyMultibase` and rewrite `keys.json` from what matched.
|
|
702
|
+
* When `did.jsonl` is published, its resolved DID is recorded in the `webvh`
|
|
703
|
+
* block; there is nothing else to repair there, since the log's update keys are
|
|
704
|
+
* client-held seeds that no keystore listing could recover.
|
|
705
|
+
*
|
|
706
|
+
* An unmatchable binding is unrepairable and throws: a published artifact
|
|
707
|
+
* depends on a key the keystore no longer lists.
|
|
708
|
+
*
|
|
709
|
+
* @param options {object}
|
|
710
|
+
* @param options.keystoreAgent {KeystoreAgent}
|
|
711
|
+
* @param options.idStore {WebvhIdStore}
|
|
712
|
+
* @returns {Promise<DidWebKeyMapV2>} the rebuilt, persisted keys.json
|
|
713
|
+
*/
|
|
714
|
+
export async function repairKeyBindings({ keystoreAgent, idStore }) {
|
|
715
|
+
const didDoc = (await idStore.getIdResource({
|
|
716
|
+
resourceId: DID_DOCUMENT_RESOURCE
|
|
717
|
+
}));
|
|
718
|
+
if (!didDoc) {
|
|
719
|
+
throw new Error('keys.json repair: did.json is not published; there is nothing to ' +
|
|
720
|
+
'match key bindings against.');
|
|
721
|
+
}
|
|
722
|
+
// One listing, matched by public key material below. `keyUrl` is the
|
|
723
|
+
// list-only projection field (webkms-client >= 14.7.1 types it; an older
|
|
724
|
+
// server omits it, so entries without one are skipped and simply fail to
|
|
725
|
+
// match).
|
|
726
|
+
const listed = (await keystoreAgent.listKeys());
|
|
727
|
+
const keyUrlByMultibase = new Map();
|
|
728
|
+
for (const description of listed) {
|
|
729
|
+
if (description.publicKeyMultibase && description.keyUrl) {
|
|
730
|
+
keyUrlByMultibase.set(description.publicKeyMultibase, description.keyUrl);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// The three did:web relationships, matched from the published document. A
|
|
734
|
+
// relationship can name several verification methods now that enrolled
|
|
735
|
+
// clients publish their own keys beside the KMS ones, so every reference is
|
|
736
|
+
// tried and the first keystore-backed one wins; a client-held key simply
|
|
737
|
+
// fails to match and is skipped.
|
|
738
|
+
const bind = (relationship) => {
|
|
739
|
+
const references = didDoc[relationship] ?? [];
|
|
740
|
+
if (references.length === 0) {
|
|
741
|
+
throw new Error(`keys.json repair: did.json declares no ${relationship} verification method.`);
|
|
742
|
+
}
|
|
743
|
+
const tried = [];
|
|
744
|
+
for (const reference of references) {
|
|
745
|
+
const vmId = typeof reference === 'string' ? reference : reference?.id;
|
|
746
|
+
if (!vmId) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
const method = didDoc.verificationMethod?.find(entry => entry.id === vmId);
|
|
750
|
+
const publicKeyMultibase = method?.publicKeyMultibase ?? multibaseOf(vmId);
|
|
751
|
+
tried.push(publicKeyMultibase);
|
|
752
|
+
const kmsKeyId = keyUrlByMultibase.get(publicKeyMultibase);
|
|
753
|
+
if (kmsKeyId) {
|
|
754
|
+
return { vmId, kmsKeyId };
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
throw new Error(`keys.json repair: no keystore key matches the ${relationship} ` +
|
|
758
|
+
`verification method (${tried.join(', ')}).`);
|
|
759
|
+
};
|
|
760
|
+
const repaired = {
|
|
761
|
+
authentication: bind('authentication'),
|
|
762
|
+
assertionMethod: bind('assertionMethod'),
|
|
763
|
+
keyAgreement: bind('keyAgreement')
|
|
764
|
+
};
|
|
765
|
+
// The webvh block, recovered from the published log: the DID and nothing
|
|
766
|
+
// else, since the update keys never left the client that minted them.
|
|
767
|
+
const published = await readPublishedLog({ idStore });
|
|
768
|
+
if (published) {
|
|
769
|
+
repaired.webvh = { did: published.did };
|
|
770
|
+
}
|
|
771
|
+
// Persist the rebuilt anchor in one write.
|
|
772
|
+
await idStore.putKeyMap({ content: repaired });
|
|
773
|
+
return repaired;
|
|
774
|
+
}
|
|
775
|
+
//# sourceMappingURL=didWebvh.js.map
|