@interop/did-method-webvh 3.2.0 → 3.3.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/CHANGELOG.md +186 -0
- package/README.md +2 -11
- package/dist/assertions.js +19 -2
- package/dist/constants.d.ts +21 -0
- package/dist/constants.js +28 -0
- package/dist/cryptography.d.ts +5 -3
- package/dist/cryptography.js +4 -2
- package/dist/interfaces.d.ts +8 -7
- package/dist/method.d.ts +7 -5
- package/dist/method.js +3 -3
- package/dist/method_versions/method.v1.0.d.ts +6 -5
- package/dist/method_versions/method.v1.0.js +296 -133
- package/dist/utils/iso8601-datetime.d.ts +55 -0
- package/dist/utils/iso8601-datetime.js +116 -0
- package/dist/utils/multiformats.d.ts +2 -0
- package/dist/utils/multiformats.js +4 -0
- package/dist/utils.d.ts +15 -2
- package/dist/utils.js +182 -95
- package/dist/witness.js +12 -5
- package/package.json +2 -6
- package/dist/cli.d.ts +0 -21
- package/dist/cli.js +0 -533
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
import { documentStateIsValid, hashChainValid, newKeysAreInNextKeys, scidIsFromHash } from '../assertions.js';
|
|
2
|
-
import { METHOD, PLACEHOLDER } from '../constants.js';
|
|
2
|
+
import { CONTEXT_LINKED_VP, ERROR_TYPE_INVALID_DID, ERROR_TYPE_NOT_FOUND, METHOD, METHOD_PARAMETER_KEYS, METHOD_PROTOCOL_V1_0, PLACEHOLDER, SERVICE_TYPE_LINKED_VP, SERVICE_TYPE_RELATIVE_REF, ServiceFragment, } from '../constants.js';
|
|
3
3
|
import { DidResolutionError } from '../interfaces.js';
|
|
4
|
-
import {
|
|
4
|
+
import { createNextVersionTime, parseUtcIso8601VersionTime, validateUtcIso8601NotInFuture, } from '../utils/iso8601-datetime.js';
|
|
5
|
+
import { createDate, createDIDDoc, createSCID, deepClone, deriveHash, enrichAlsoKnownAs, findVerificationMethod, generateParallelDidWeb, getBaseUrl, parseCanonicalAddress, parseDidWebvhIdentifier, replaceCreateDidPlaceholders, replaceValueInObject, serviceFragmentExists, validateCreateDidDocument, validateMethodSpecificPathSegments, } from '../utils.js';
|
|
5
6
|
import { countVerifiedWitnessApprovals, fetchWitnessProofs, validateWitnessParameter } from '../witness.js';
|
|
6
|
-
const
|
|
7
|
-
const PROTOCOL = `did:${METHOD}:${VERSION}`;
|
|
7
|
+
const MAX_FUTURE_SKEW_MS = 5 * 60 * 1000;
|
|
8
8
|
const requireDidId = (id) => {
|
|
9
9
|
if (!id) {
|
|
10
10
|
throw new Error('DID document id is missing');
|
|
11
11
|
}
|
|
12
12
|
return id;
|
|
13
13
|
};
|
|
14
|
+
const parseAndValidateVersionId = (versionId, expectedVersionNumber) => {
|
|
15
|
+
const firstDashIndex = versionId.indexOf('-');
|
|
16
|
+
const lastDashIndex = versionId.lastIndexOf('-');
|
|
17
|
+
if (firstDashIndex === -1 || firstDashIndex !== lastDashIndex) {
|
|
18
|
+
throw new Error(`versionId '${versionId}' must contain exactly one '-' separator`);
|
|
19
|
+
}
|
|
20
|
+
const version = versionId.slice(0, firstDashIndex);
|
|
21
|
+
const entryHash = versionId.slice(firstDashIndex + 1);
|
|
22
|
+
if (!/^\d+$/.test(version)) {
|
|
23
|
+
throw new Error(`versionId '${versionId}' must have a numeric version prefix`);
|
|
24
|
+
}
|
|
25
|
+
if (entryHash.length === 0) {
|
|
26
|
+
throw new Error(`versionId '${versionId}' must have a non-empty hash component`);
|
|
27
|
+
}
|
|
28
|
+
const versionNumber = Number(version);
|
|
29
|
+
if (versionNumber !== expectedVersionNumber) {
|
|
30
|
+
throw new Error(`version '${version}' in log doesn't match expected '${expectedVersionNumber}'.`);
|
|
31
|
+
}
|
|
32
|
+
return { version, versionNumber, entryHash };
|
|
33
|
+
};
|
|
14
34
|
export const createDID = async (options) => {
|
|
15
35
|
if (!options.updateKeys) {
|
|
16
36
|
throw new Error('Update keys not supplied');
|
|
@@ -26,8 +46,12 @@ export const createDID = async (options) => {
|
|
|
26
46
|
const parsed = parseCanonicalAddress(addressInput);
|
|
27
47
|
const didDomainComponent = parsed.didDomainComponent;
|
|
28
48
|
const allPaths = [...(parsed.paths || []), ...(options.paths || [])];
|
|
49
|
+
validateMethodSpecificPathSegments(allPaths, 'createDID path segments');
|
|
29
50
|
const path = allPaths.length > 0 ? allPaths.join(':') : undefined;
|
|
30
51
|
const controller = `did:${METHOD}:${PLACEHOLDER}:${didDomainComponent}${path ? `:${path}` : ''}`;
|
|
52
|
+
if (options.created) {
|
|
53
|
+
validateUtcIso8601NotInFuture(options.created, 'createDID created');
|
|
54
|
+
}
|
|
31
55
|
const createdDate = createDate(options.created);
|
|
32
56
|
// Safety guard: Strip secret keys from verification methods before creating DID document
|
|
33
57
|
const safeVerificationMethods = options.verificationMethods?.map((vm) => {
|
|
@@ -72,7 +96,7 @@ export const createDID = async (options) => {
|
|
|
72
96
|
versionId: PLACEHOLDER,
|
|
73
97
|
versionTime: createdDate,
|
|
74
98
|
parameters: {
|
|
75
|
-
method:
|
|
99
|
+
method: METHOD_PROTOCOL_V1_0,
|
|
76
100
|
...params,
|
|
77
101
|
},
|
|
78
102
|
state: doc,
|
|
@@ -103,6 +127,9 @@ export const createDID = async (options) => {
|
|
|
103
127
|
throw new Error(`version ${prelimEntry.versionId} is invalid.`);
|
|
104
128
|
}
|
|
105
129
|
const didId = requireDidId(prelimEntry.state.id);
|
|
130
|
+
if (didId !== didWithScid) {
|
|
131
|
+
throw new Error(`Created DID document id must match expected DID '${didWithScid}', got '${didId}'`);
|
|
132
|
+
}
|
|
106
133
|
const webDoc = options.alsoKnownAsWeb ? generateParallelDidWeb(didId, prelimEntry.state) : undefined;
|
|
107
134
|
return {
|
|
108
135
|
did: didId,
|
|
@@ -123,14 +150,19 @@ export const resolveDIDFromLog = async (log, options = {}) => {
|
|
|
123
150
|
throw new Error('Cannot specify both verificationMethod and version number/id');
|
|
124
151
|
}
|
|
125
152
|
const resolutionLog = log.map((l) => deepClone(l));
|
|
153
|
+
if (resolutionLog.length === 0) {
|
|
154
|
+
throw new Error(`Log identity binding check failed: no entries to process`);
|
|
155
|
+
}
|
|
126
156
|
const protocol = resolutionLog[0]?.parameters?.method;
|
|
127
|
-
if (protocol !==
|
|
157
|
+
if (protocol !== METHOD_PROTOCOL_V1_0) {
|
|
128
158
|
throw new Error(`'${protocol}' is not a supported method version.`);
|
|
129
159
|
}
|
|
130
160
|
let did = '';
|
|
131
161
|
let doc = null;
|
|
132
162
|
let resolvedDoc = null;
|
|
163
|
+
let resolvedDid = null;
|
|
133
164
|
let lastValidDoc = null;
|
|
165
|
+
let lastValidDid = null;
|
|
134
166
|
const meta = {
|
|
135
167
|
versionId: '',
|
|
136
168
|
created: '',
|
|
@@ -147,31 +179,41 @@ export const resolveDIDFromLog = async (log, options = {}) => {
|
|
|
147
179
|
let resolvedMeta = null;
|
|
148
180
|
let lastValidMeta = null;
|
|
149
181
|
let i = 0;
|
|
182
|
+
const hasExplicitHistoricalSelector = options.versionNumber !== undefined ||
|
|
183
|
+
options.versionId !== undefined ||
|
|
184
|
+
options.versionTime !== undefined ||
|
|
185
|
+
options.verificationMethod !== undefined;
|
|
186
|
+
let didIdMatchCount = 0;
|
|
150
187
|
let host = '';
|
|
188
|
+
let previousVersionTime;
|
|
151
189
|
const requiredWitnessChecks = [];
|
|
152
|
-
|
|
153
|
-
const fastResolve = options.fastResolve ?? false;
|
|
154
|
-
const isFirstEntry = (idx) => idx === 0;
|
|
155
|
-
const isLastFewEntries = (idx) => idx >= resolutionLog.length - 10; // Verify last 10 entries
|
|
156
|
-
const shouldVerifyEntry = (idx) => !fastResolve || isFirstEntry(idx) || isLastFewEntries(idx);
|
|
190
|
+
let witnessThresholdFailure = false;
|
|
157
191
|
try {
|
|
158
192
|
while (i < resolutionLog.length) {
|
|
159
193
|
const { versionId, versionTime, parameters, state, proof } = resolutionLog[i];
|
|
160
|
-
const
|
|
194
|
+
const { version, versionNumber, entryHash } = parseAndValidateVersionId(versionId, i + 1);
|
|
161
195
|
const previousWitness = meta.witness ? deepClone(meta.witness) : undefined;
|
|
162
|
-
if (parseInt(version, 10) !== i + 1) {
|
|
163
|
-
throw new Error(`version '${version}' in log doesn't match expected '${i + 1}'.`);
|
|
164
|
-
}
|
|
165
196
|
meta.versionId = versionId;
|
|
166
|
-
if (versionTime) {
|
|
167
|
-
|
|
197
|
+
if (!versionTime) {
|
|
198
|
+
throw new Error(`version '${version}' is missing versionTime`);
|
|
199
|
+
}
|
|
200
|
+
const currentVersionTime = parseUtcIso8601VersionTime(versionTime, `version '${version}' versionTime`);
|
|
201
|
+
if (previousVersionTime && currentVersionTime.getTime() <= previousVersionTime.getTime()) {
|
|
202
|
+
throw new Error(`versionTime for version '${version}' must be greater than previous entry time`);
|
|
168
203
|
}
|
|
204
|
+
// Check against resolver's current time for each entry per spec normative language
|
|
205
|
+
const maxAllowedFutureTime = Date.now() + MAX_FUTURE_SKEW_MS;
|
|
206
|
+
if (currentVersionTime.getTime() > maxAllowedFutureTime) {
|
|
207
|
+
throw new Error(`versionTime for version '${version}' must not be more than 5 minutes in the future`);
|
|
208
|
+
}
|
|
209
|
+
previousVersionTime = currentVersionTime;
|
|
169
210
|
meta.updated = versionTime;
|
|
170
211
|
let newDoc = state;
|
|
212
|
+
const parsedStateDid = parseDidWebvhIdentifier(requireDidId(newDoc.id), `version '${version}' state.id`);
|
|
171
213
|
if (version === '1') {
|
|
172
214
|
meta.created = versionTime;
|
|
173
215
|
newDoc = state;
|
|
174
|
-
host =
|
|
216
|
+
host = parsedStateDid.locationKey;
|
|
175
217
|
meta.scid = parameters.scid;
|
|
176
218
|
if (options.scid && options.scid !== meta.scid) {
|
|
177
219
|
throw new Error(`SCID in DID '${options.scid}' does not match SCID in log '${meta.scid}'`);
|
|
@@ -182,81 +224,101 @@ export const resolveDIDFromLog = async (log, options = {}) => {
|
|
|
182
224
|
meta.prerotation = meta.nextKeyHashes.length > 0;
|
|
183
225
|
meta.witness = parameters.witness || meta.witness;
|
|
184
226
|
meta.watchers = parameters.watchers ?? null;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
227
|
+
// Optimized: Use efficient object manipulation instead of JSON stringify/parse
|
|
228
|
+
const logEntry = {
|
|
229
|
+
versionId: PLACEHOLDER,
|
|
230
|
+
versionTime: meta.created,
|
|
231
|
+
parameters: replaceValueInObject(parameters, meta.scid, PLACEHOLDER),
|
|
232
|
+
state: replaceValueInObject(newDoc, meta.scid, PLACEHOLDER),
|
|
233
|
+
};
|
|
234
|
+
const logEntryHash = await deriveHash(logEntry);
|
|
235
|
+
meta.previousLogEntryHash = logEntryHash;
|
|
236
|
+
if (!(await scidIsFromHash(meta.scid, logEntryHash))) {
|
|
237
|
+
throw new Error(`SCID '${meta.scid}' not derived from logEntryHash '${logEntryHash}'`);
|
|
238
|
+
}
|
|
239
|
+
if (parsedStateDid.scid !== meta.scid) {
|
|
240
|
+
throw new Error(`SCID in state.id '${parsedStateDid.scid}' does not match SCID in log '${meta.scid}'`);
|
|
241
|
+
}
|
|
242
|
+
// Optimized: Direct object manipulation instead of JSON stringify/parse
|
|
243
|
+
const prelimEntry = replaceValueInObject(logEntry, PLACEHOLDER, meta.scid);
|
|
244
|
+
const logEntryHash2 = await deriveHash(prelimEntry);
|
|
245
|
+
const verified = await documentStateIsValid({ ...prelimEntry, versionId: `1-${logEntryHash2}`, proof }, meta.updateKeys, meta.witness, false, options.verifier);
|
|
246
|
+
if (!verified) {
|
|
247
|
+
throw new Error(`version ${meta.versionId} failed verification of the proof.`);
|
|
205
248
|
}
|
|
206
249
|
}
|
|
207
250
|
else {
|
|
208
251
|
// version number > 1
|
|
209
|
-
|
|
210
|
-
if (
|
|
252
|
+
// Validate method parameter: must not be present or must equal the supported method
|
|
253
|
+
if (Object.hasOwn(parameters, METHOD_PARAMETER_KEYS.method)) {
|
|
254
|
+
const entryMethod = parameters.method;
|
|
255
|
+
if (entryMethod !== METHOD_PROTOCOL_V1_0) {
|
|
256
|
+
throw new Error(`version '${version}' has unsupported or downgraded method '${entryMethod}'; ` +
|
|
257
|
+
`expected '${METHOD_PROTOCOL_V1_0}'`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// scid MUST NOT appear in later entries
|
|
261
|
+
if (Object.hasOwn(parameters, METHOD_PARAMETER_KEYS.scid)) {
|
|
262
|
+
throw new Error(`version '${version}' must not contain SCID parameter`);
|
|
263
|
+
}
|
|
264
|
+
// portable: true cannot be introduced after the first entry — it can only remain
|
|
265
|
+
// true if it was already enabled in the first entry
|
|
266
|
+
if (parameters.portable === true && !meta.portable) {
|
|
267
|
+
throw new Error(`version '${version}' cannot set portable: true; portability can only be enabled in the first entry`);
|
|
268
|
+
}
|
|
269
|
+
// Setting portable: false in a later entry permanently locks portability off
|
|
270
|
+
if (Object.hasOwn(parameters, METHOD_PARAMETER_KEYS.portable) && parameters.portable === false) {
|
|
271
|
+
meta.portable = false;
|
|
272
|
+
}
|
|
273
|
+
if (parsedStateDid.scid !== meta.scid) {
|
|
274
|
+
throw new Error(`SCID in state.id '${parsedStateDid.scid}' does not match SCID in log '${meta.scid}'`);
|
|
275
|
+
}
|
|
276
|
+
const newLocation = parsedStateDid.locationKey;
|
|
277
|
+
if (!meta.portable && newLocation !== host) {
|
|
211
278
|
throw new Error('Cannot move DID: portability is disabled');
|
|
212
279
|
}
|
|
213
|
-
else if (
|
|
214
|
-
host =
|
|
280
|
+
else if (newLocation !== host) {
|
|
281
|
+
host = newLocation;
|
|
215
282
|
}
|
|
216
|
-
// Hash chain
|
|
283
|
+
// Hash chain
|
|
217
284
|
const { proof: _proof, ...entryWithoutProof } = resolutionLog[i];
|
|
218
285
|
const recomputedHash = await deriveHash({ ...entryWithoutProof, versionId: resolutionLog[i - 1].versionId });
|
|
219
286
|
if (!hashChainValid(recomputedHash, entryHash)) {
|
|
220
287
|
throw new Error(`Hash chain broken at '${meta.versionId}'`);
|
|
221
288
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
throw new Error(`version ${meta.versionId} failed verification of the proof.`);
|
|
228
|
-
}
|
|
229
|
-
if (meta.prerotation) {
|
|
230
|
-
await newKeysAreInNextKeys(parameters.updateKeys ?? [], meta.nextKeyHashes ?? []);
|
|
231
|
-
}
|
|
289
|
+
// Signature verification
|
|
290
|
+
const keys = meta.prerotation ? parameters.updateKeys : meta.updateKeys;
|
|
291
|
+
const verified = await documentStateIsValid(resolutionLog[i], keys, meta.witness, false, options.verifier);
|
|
292
|
+
if (!verified) {
|
|
293
|
+
throw new Error(`version ${meta.versionId} failed verification of the proof.`);
|
|
232
294
|
}
|
|
233
|
-
if (
|
|
234
|
-
|
|
295
|
+
if (meta.prerotation) {
|
|
296
|
+
await newKeysAreInNextKeys(parameters.updateKeys ?? [], meta.nextKeyHashes ?? []);
|
|
297
|
+
}
|
|
298
|
+
if (Object.hasOwn(parameters, METHOD_PARAMETER_KEYS.updateKeys)) {
|
|
299
|
+
meta.updateKeys = parameters.updateKeys ?? [];
|
|
235
300
|
}
|
|
236
301
|
if (parameters.deactivated === true) {
|
|
237
302
|
meta.deactivated = true;
|
|
238
303
|
}
|
|
239
|
-
if (parameters
|
|
240
|
-
meta.nextKeyHashes = parameters.nextKeyHashes;
|
|
241
|
-
meta.prerotation =
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
meta.nextKeyHashes = [];
|
|
245
|
-
meta.prerotation = false;
|
|
304
|
+
if (Object.hasOwn(parameters, METHOD_PARAMETER_KEYS.nextKeyHashes)) {
|
|
305
|
+
meta.nextKeyHashes = parameters.nextKeyHashes ?? [];
|
|
306
|
+
meta.prerotation = meta.nextKeyHashes.length > 0;
|
|
246
307
|
}
|
|
247
|
-
|
|
308
|
+
const legacyParameters = parameters;
|
|
309
|
+
if (Object.hasOwn(parameters, METHOD_PARAMETER_KEYS.witness)) {
|
|
248
310
|
meta.witness = parameters.witness;
|
|
249
311
|
}
|
|
250
|
-
else if (
|
|
312
|
+
else if (legacyParameters.witnesses) {
|
|
251
313
|
meta.witness = {
|
|
252
|
-
witnesses:
|
|
253
|
-
threshold:
|
|
314
|
+
witnesses: legacyParameters.witnesses,
|
|
315
|
+
threshold: legacyParameters.witnessThreshold || legacyParameters.witnesses.length,
|
|
254
316
|
};
|
|
255
317
|
}
|
|
256
318
|
if (meta.witness?.witnesses?.length) {
|
|
257
319
|
validateWitnessParameter(meta.witness);
|
|
258
320
|
}
|
|
259
|
-
if (
|
|
321
|
+
if (Object.hasOwn(parameters, METHOD_PARAMETER_KEYS.watchers)) {
|
|
260
322
|
meta.watchers = parameters.watchers ?? null;
|
|
261
323
|
}
|
|
262
324
|
}
|
|
@@ -264,64 +326,55 @@ export const resolveDIDFromLog = async (log, options = {}) => {
|
|
|
264
326
|
if (requiredWitness) {
|
|
265
327
|
requiredWitnessChecks.push({
|
|
266
328
|
targetVersionId: meta.versionId,
|
|
267
|
-
targetVersionNumber:
|
|
329
|
+
targetVersionNumber: versionNumber,
|
|
268
330
|
witness: requiredWitness,
|
|
269
331
|
});
|
|
270
332
|
}
|
|
271
333
|
// Optimized: Use efficient cloning instead of clone() function
|
|
272
334
|
doc = deepClone(newDoc);
|
|
273
|
-
did = doc.id;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// Add default services if they don't exist
|
|
277
|
-
doc.service = Array.isArray(doc.service) ? doc.service : [];
|
|
278
|
-
const baseUrl = getBaseUrl(did);
|
|
279
|
-
if (!doc.service.some((s) => s.id === '#files')) {
|
|
280
|
-
doc.service.push({
|
|
281
|
-
id: '#files',
|
|
282
|
-
type: 'relativeRef',
|
|
283
|
-
serviceEndpoint: baseUrl,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
if (!doc.service.some((s) => s.id === '#whois')) {
|
|
287
|
-
doc.service.push({
|
|
288
|
-
'@context': 'https://identity.foundation/linked-vp/contexts/v1',
|
|
289
|
-
id: '#whois',
|
|
290
|
-
type: 'LinkedVerifiablePresentation',
|
|
291
|
-
serviceEndpoint: `${baseUrl}/whois.vp`,
|
|
292
|
-
});
|
|
293
|
-
}
|
|
335
|
+
did = requireDidId(doc.id);
|
|
336
|
+
if (options.requestedDid && did === options.requestedDid) {
|
|
337
|
+
didIdMatchCount++;
|
|
294
338
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
339
|
+
// Add default services if they don't exist
|
|
340
|
+
doc.service = Array.isArray(doc.service) ? doc.service : [];
|
|
341
|
+
const baseUrl = getBaseUrl(did);
|
|
342
|
+
if (!serviceFragmentExists(doc.service, ServiceFragment.Files, did)) {
|
|
343
|
+
doc.service.push({
|
|
344
|
+
id: '#files',
|
|
345
|
+
type: SERVICE_TYPE_RELATIVE_REF,
|
|
346
|
+
serviceEndpoint: baseUrl,
|
|
347
|
+
});
|
|
300
348
|
}
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
349
|
+
if (!serviceFragmentExists(doc.service, ServiceFragment.Whois, did)) {
|
|
350
|
+
doc.service.push({
|
|
351
|
+
'@context': CONTEXT_LINKED_VP,
|
|
352
|
+
id: '#whois',
|
|
353
|
+
type: SERVICE_TYPE_LINKED_VP,
|
|
354
|
+
serviceEndpoint: `${baseUrl}/whois.vp`,
|
|
355
|
+
});
|
|
306
356
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
357
|
+
// Latch the first entry matching the requested selector as the resolved result.
|
|
358
|
+
let matchesSelector = (!!options.verificationMethod && !!findVerificationMethod(doc, options.verificationMethod)) ||
|
|
359
|
+
options.versionNumber === versionNumber ||
|
|
360
|
+
options.versionId === meta.versionId;
|
|
361
|
+
if (!matchesSelector && options.versionTime && options.versionTime > new Date(meta.updated)) {
|
|
362
|
+
const nextEntry = resolutionLog[i + 1];
|
|
363
|
+
matchesSelector = !nextEntry || options.versionTime < new Date(nextEntry.versionTime);
|
|
364
|
+
}
|
|
365
|
+
if (matchesSelector && !resolvedDoc) {
|
|
366
|
+
resolvedDoc = deepClone(doc);
|
|
367
|
+
resolvedDid = did;
|
|
368
|
+
resolvedMeta = { ...meta };
|
|
320
369
|
}
|
|
321
370
|
lastValidDoc = deepClone(doc);
|
|
371
|
+
lastValidDid = did;
|
|
322
372
|
lastValidMeta = { ...meta };
|
|
323
373
|
i++;
|
|
324
374
|
}
|
|
375
|
+
if (options.requestedDid && didIdMatchCount === 0) {
|
|
376
|
+
throw new Error(`Requested DID '${options.requestedDid}' does not match state.id in any valid log version`);
|
|
377
|
+
}
|
|
325
378
|
if (requiredWitnessChecks.length > 0) {
|
|
326
379
|
if (!options.witnessProofs) {
|
|
327
380
|
options.witnessProofs = await fetchWitnessProofs(did);
|
|
@@ -335,6 +388,7 @@ export const resolveDIDFromLog = async (log, options = {}) => {
|
|
|
335
388
|
const approvals = await countVerifiedWitnessApprovals(resolutionLog[check.targetVersionNumber - 1], candidateProofs, check.witness, options.verifier);
|
|
336
389
|
const threshold = parseInt((check.witness.threshold ?? 0).toString(), 10);
|
|
337
390
|
if (approvals < threshold) {
|
|
391
|
+
witnessThresholdFailure = true;
|
|
338
392
|
throw new Error(`Witness threshold not met for version ${check.targetVersionId}: got ${approvals}, need ${check.witness.threshold}`);
|
|
339
393
|
}
|
|
340
394
|
}
|
|
@@ -344,25 +398,59 @@ export const resolveDIDFromLog = async (log, options = {}) => {
|
|
|
344
398
|
if (!resolvedDoc) {
|
|
345
399
|
throw e;
|
|
346
400
|
}
|
|
347
|
-
if (resolvedMeta) {
|
|
401
|
+
if (resolvedMeta && (!hasExplicitHistoricalSelector || witnessThresholdFailure)) {
|
|
348
402
|
const message = e instanceof Error ? e.message : String(e);
|
|
349
403
|
resolvedMeta.error = DidResolutionError.InvalidDid;
|
|
350
404
|
resolvedMeta.problemDetails = {
|
|
351
|
-
type:
|
|
405
|
+
type: ERROR_TYPE_INVALID_DID,
|
|
352
406
|
title: 'The resolved DID is invalid.',
|
|
353
407
|
detail: message,
|
|
354
408
|
};
|
|
355
409
|
}
|
|
356
410
|
}
|
|
357
411
|
if (!resolvedDoc) {
|
|
358
|
-
|
|
412
|
+
if (hasExplicitHistoricalSelector) {
|
|
413
|
+
if (!lastValidMeta || !lastValidDid) {
|
|
414
|
+
throw new Error('DID resolution failed: No valid result available for explicit selector');
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
did: lastValidDid,
|
|
418
|
+
doc: null,
|
|
419
|
+
meta: {
|
|
420
|
+
...lastValidMeta,
|
|
421
|
+
error: DidResolutionError.NotFound,
|
|
422
|
+
problemDetails: {
|
|
423
|
+
type: ERROR_TYPE_NOT_FOUND,
|
|
424
|
+
title: 'The requested DID version was not found.',
|
|
425
|
+
detail: 'The supplied explicit version selector did not match any entry in the DID log.',
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
}
|
|
359
430
|
resolvedMeta = lastValidMeta;
|
|
431
|
+
resolvedDid = lastValidDid;
|
|
432
|
+
if (resolvedMeta && !(resolvedMeta.deactivated && !hasExplicitHistoricalSelector)) {
|
|
433
|
+
resolvedDoc = lastValidDoc;
|
|
434
|
+
}
|
|
360
435
|
}
|
|
361
436
|
if (!resolvedMeta) {
|
|
362
437
|
throw new Error('DID resolution failed: No valid metadata found');
|
|
363
438
|
}
|
|
439
|
+
if (!resolvedDid) {
|
|
440
|
+
throw new Error('DID resolution failed: No valid identifier found');
|
|
441
|
+
}
|
|
442
|
+
if (resolvedMeta.deactivated && !hasExplicitHistoricalSelector) {
|
|
443
|
+
return {
|
|
444
|
+
did: resolvedDid,
|
|
445
|
+
doc: null,
|
|
446
|
+
meta: resolvedMeta,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
if (!resolvedDoc) {
|
|
450
|
+
throw new Error('DID resolution failed: No valid document found');
|
|
451
|
+
}
|
|
364
452
|
return {
|
|
365
|
-
did,
|
|
453
|
+
did: resolvedDid,
|
|
366
454
|
doc: resolvedDoc,
|
|
367
455
|
meta: resolvedMeta,
|
|
368
456
|
};
|
|
@@ -370,14 +458,25 @@ export const resolveDIDFromLog = async (log, options = {}) => {
|
|
|
370
458
|
export const updateDID = async (options) => {
|
|
371
459
|
const log = options.log;
|
|
372
460
|
const lastEntry = log[log.length - 1];
|
|
461
|
+
const lastEntryDid = requireDidId(lastEntry.state.id);
|
|
462
|
+
const parsedLastEntryDid = parseDidWebvhIdentifier(lastEntryDid, 'last entry state.id');
|
|
373
463
|
const lastMeta = (await resolveDIDFromLog(log, { verifier: options.verifier, witnessProofs: options.witnessProofs }))
|
|
374
464
|
.meta;
|
|
465
|
+
const currentUpdateKeys = options.updateKeys;
|
|
375
466
|
if (lastMeta.deactivated) {
|
|
376
467
|
throw new Error('Cannot update deactivated DID');
|
|
377
468
|
}
|
|
469
|
+
if (lastMeta.prerotation && currentUpdateKeys === undefined) {
|
|
470
|
+
throw new Error('updateKeys must be provided while pre-rotation is active');
|
|
471
|
+
}
|
|
378
472
|
const versionNumber = log.length + 1;
|
|
379
|
-
|
|
473
|
+
// Validate user-provided timestamp with skew tolerance before creating the versionTime
|
|
474
|
+
if (options.updated) {
|
|
475
|
+
validateUtcIso8601NotInFuture(options.updated, 'updateDID updated', MAX_FUTURE_SKEW_MS);
|
|
476
|
+
}
|
|
477
|
+
const createdDate = createNextVersionTime(lastMeta.updated, options.updated, createDate);
|
|
380
478
|
const watchersValue = options.watchers !== undefined ? options.watchers : lastMeta.watchers;
|
|
479
|
+
const resolvedNextKeyHashes = options.nextKeyHashes ?? lastMeta.nextKeyHashes ?? [];
|
|
381
480
|
const witnessInput = options.witness;
|
|
382
481
|
const witness = witnessInput?.witnesses?.length
|
|
383
482
|
? {
|
|
@@ -385,15 +484,24 @@ export const updateDID = async (options) => {
|
|
|
385
484
|
threshold: witnessInput.threshold ?? 0,
|
|
386
485
|
}
|
|
387
486
|
: {};
|
|
487
|
+
if (options.portable === true) {
|
|
488
|
+
throw new Error('portable: true cannot be set in an update entry; portability can only be enabled in the first entry');
|
|
489
|
+
}
|
|
388
490
|
const params = {
|
|
389
|
-
|
|
390
|
-
|
|
491
|
+
...(options.updateKeys !== undefined || lastMeta.prerotation
|
|
492
|
+
? { updateKeys: options.updateKeys ?? lastMeta.updateKeys }
|
|
493
|
+
: {}),
|
|
494
|
+
...(options.nextKeyHashes !== undefined ? { nextKeyHashes: options.nextKeyHashes } : {}),
|
|
495
|
+
...(options.portable === false ? { portable: false } : {}),
|
|
391
496
|
witness,
|
|
392
497
|
watchers: watchersValue ?? [],
|
|
393
498
|
};
|
|
394
499
|
if (params.witness?.witnesses?.length) {
|
|
395
500
|
validateWitnessParameter(params.witness);
|
|
396
501
|
}
|
|
502
|
+
if (lastMeta.prerotation) {
|
|
503
|
+
await newKeysAreInNextKeys(currentUpdateKeys ?? [], lastMeta.nextKeyHashes ?? []);
|
|
504
|
+
}
|
|
397
505
|
// Safety guard: Strip secret keys from verification methods before creating DID document
|
|
398
506
|
const safeVerificationMethods = options.verificationMethods?.map((vm) => {
|
|
399
507
|
if (vm.secretKeyMultibase) {
|
|
@@ -403,26 +511,77 @@ export const updateDID = async (options) => {
|
|
|
403
511
|
}
|
|
404
512
|
return vm;
|
|
405
513
|
});
|
|
406
|
-
|
|
514
|
+
// Determine the controller (the DID id) for this update. When a new location
|
|
515
|
+
// (address/domain) is supplied, rebuild the controller from that location while
|
|
516
|
+
// preserving the SCID, so a portable DID can actually move. The SCID is the
|
|
517
|
+
// stable part of the identifier; only the location component changes.
|
|
518
|
+
const requestedAddress = options.address || options.domain;
|
|
519
|
+
let controller;
|
|
520
|
+
let controllerPaths = parsedLastEntryDid.paths;
|
|
521
|
+
if (options.controller) {
|
|
522
|
+
controller = options.controller;
|
|
523
|
+
}
|
|
524
|
+
else if (requestedAddress) {
|
|
525
|
+
const parsedNewAddress = parseCanonicalAddress(requestedAddress);
|
|
526
|
+
// Paths: explicit options.paths (combined with any address-embedded paths) take
|
|
527
|
+
// precedence; otherwise use address-embedded paths; otherwise inherit the prior
|
|
528
|
+
// paths so re-passing a bare domain on a pathed DID doesn't silently drop them.
|
|
529
|
+
const addressPaths = parsedNewAddress.paths || [];
|
|
530
|
+
const newLocationPaths = options.paths !== undefined
|
|
531
|
+
? [...addressPaths, ...options.paths]
|
|
532
|
+
: addressPaths.length
|
|
533
|
+
? addressPaths
|
|
534
|
+
: (parsedLastEntryDid.paths ?? []);
|
|
535
|
+
const newLocationKey = newLocationPaths.length
|
|
536
|
+
? `${parsedNewAddress.didDomainComponent}:${newLocationPaths.join(':')}`
|
|
537
|
+
: parsedNewAddress.didDomainComponent;
|
|
538
|
+
controller = `did:${METHOD}:${parsedLastEntryDid.scid}:${newLocationKey}`;
|
|
539
|
+
controllerPaths = newLocationPaths.length ? newLocationPaths : undefined;
|
|
540
|
+
if (controller !== lastEntryDid && !lastMeta.portable) {
|
|
541
|
+
throw new Error('Cannot move DID: portability is disabled');
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
controller = lastEntryDid;
|
|
546
|
+
}
|
|
547
|
+
const { doc: normalizedUpdateDoc } = await createDIDDoc({
|
|
407
548
|
...options,
|
|
408
|
-
controller
|
|
549
|
+
controller,
|
|
409
550
|
context: options.context || lastEntry.state['@context'],
|
|
410
|
-
domain:
|
|
551
|
+
domain: requestedAddress ?? parsedLastEntryDid.didDomainComponent,
|
|
552
|
+
paths: controllerPaths,
|
|
411
553
|
updateKeys: options.updateKeys ?? [],
|
|
412
554
|
verificationMethods: safeVerificationMethods ?? [],
|
|
413
555
|
});
|
|
414
|
-
//
|
|
415
|
-
|
|
556
|
+
// Carry the prior DID document forward and selectively overlay only the fields
|
|
557
|
+
// this update actually supplies, so a sparse updateDID() preserves prior state.
|
|
558
|
+
const doc = deepClone(lastEntry.state);
|
|
559
|
+
doc['@context'] = normalizedUpdateDoc['@context'];
|
|
560
|
+
doc.id = normalizedUpdateDoc.id;
|
|
561
|
+
doc.controller = normalizedUpdateDoc.controller;
|
|
562
|
+
if (safeVerificationMethods !== undefined) {
|
|
563
|
+
doc.verificationMethod = normalizedUpdateDoc.verificationMethod;
|
|
564
|
+
doc.authentication = normalizedUpdateDoc.authentication;
|
|
565
|
+
doc.assertionMethod = normalizedUpdateDoc.assertionMethod;
|
|
566
|
+
doc.keyAgreement = normalizedUpdateDoc.keyAgreement;
|
|
567
|
+
doc.capabilityDelegation = normalizedUpdateDoc.capabilityDelegation;
|
|
568
|
+
doc.capabilityInvocation = normalizedUpdateDoc.capabilityInvocation;
|
|
569
|
+
}
|
|
570
|
+
if (options.services !== undefined) {
|
|
416
571
|
doc.service = options.services;
|
|
417
572
|
}
|
|
418
|
-
|
|
419
|
-
|
|
573
|
+
if (options.authentication !== undefined) {
|
|
574
|
+
doc.authentication = options.authentication;
|
|
575
|
+
}
|
|
576
|
+
if (options.assertionMethod !== undefined) {
|
|
420
577
|
doc.assertionMethod = options.assertionMethod;
|
|
421
578
|
}
|
|
422
|
-
|
|
423
|
-
if (options.keyAgreement) {
|
|
579
|
+
if (options.keyAgreement !== undefined) {
|
|
424
580
|
doc.keyAgreement = options.keyAgreement;
|
|
425
581
|
}
|
|
582
|
+
if (options.alsoKnownAs !== undefined) {
|
|
583
|
+
doc.alsoKnownAs = options.alsoKnownAs;
|
|
584
|
+
}
|
|
426
585
|
const logEntry = {
|
|
427
586
|
versionId: lastEntry.versionId,
|
|
428
587
|
versionTime: createdDate,
|
|
@@ -442,7 +601,10 @@ export const updateDID = async (options) => {
|
|
|
442
601
|
const signedProof = await options.signer.sign({ document: prelimEntry, proof: proofTemplate });
|
|
443
602
|
const allProofs = [{ ...proofTemplate, proofValue: signedProof.proofValue }];
|
|
444
603
|
prelimEntry.proof = allProofs;
|
|
445
|
-
const keysToVerify = lastMeta.prerotation ?
|
|
604
|
+
const keysToVerify = lastMeta.prerotation ? currentUpdateKeys : lastMeta.updateKeys;
|
|
605
|
+
if (!keysToVerify) {
|
|
606
|
+
throw new Error('updateKeys could not be determined for update verification');
|
|
607
|
+
}
|
|
446
608
|
const verified = await documentStateIsValid(prelimEntry, keysToVerify, lastMeta.witness, true, options.verifier);
|
|
447
609
|
if (!verified) {
|
|
448
610
|
throw new Error(`version ${prelimEntry.versionId} is invalid.`);
|
|
@@ -451,7 +613,7 @@ export const updateDID = async (options) => {
|
|
|
451
613
|
...lastMeta,
|
|
452
614
|
versionId: prelimEntry.versionId,
|
|
453
615
|
updated: prelimEntry.versionTime,
|
|
454
|
-
prerotation:
|
|
616
|
+
prerotation: resolvedNextKeyHashes.length > 0,
|
|
455
617
|
...params,
|
|
456
618
|
};
|
|
457
619
|
const hasWebAlias = (prelimEntry.state.alsoKnownAs ?? []).some((alias) => alias.startsWith('did:web:'));
|
|
@@ -473,7 +635,7 @@ export const deactivateDID = async (options) => {
|
|
|
473
635
|
throw new Error('DID already deactivated');
|
|
474
636
|
}
|
|
475
637
|
const versionNumber = log.length + 1;
|
|
476
|
-
const createdDate = createDate
|
|
638
|
+
const createdDate = createNextVersionTime(lastMeta.updated, undefined, createDate);
|
|
477
639
|
const params = {
|
|
478
640
|
updateKeys: options.updateKeys ?? lastMeta.updateKeys,
|
|
479
641
|
deactivated: true,
|
|
@@ -522,9 +684,10 @@ const getEntryWitnessParameter = (parameters) => {
|
|
|
522
684
|
return parameters.witness ?? {};
|
|
523
685
|
}
|
|
524
686
|
if (parameters.witnesses) {
|
|
687
|
+
const legacyParameters = parameters;
|
|
525
688
|
return {
|
|
526
|
-
witnesses:
|
|
527
|
-
threshold:
|
|
689
|
+
witnesses: legacyParameters.witnesses,
|
|
690
|
+
threshold: legacyParameters.witnessThreshold || legacyParameters.witnesses.length,
|
|
528
691
|
};
|
|
529
692
|
}
|
|
530
693
|
return undefined;
|