@did-btcr2/method 0.41.0 → 0.42.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/.tsbuildinfo +1 -1
- package/dist/browser.js +89 -14
- package/dist/browser.mjs +89 -14
- package/dist/cjs/index.js +90 -14
- package/dist/esm/core/resolver.js +82 -12
- package/dist/esm/core/resolver.js.map +1 -1
- package/dist/esm/did-btcr2.js +2 -1
- package/dist/esm/did-btcr2.js.map +1 -1
- package/dist/types/core/interfaces.d.ts +7 -0
- package/dist/types/core/interfaces.d.ts.map +1 -1
- package/dist/types/core/resolver.d.ts +8 -0
- package/dist/types/core/resolver.d.ts.map +1 -1
- package/dist/types/did-btcr2.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/core/interfaces.ts +8 -0
- package/src/core/resolver.ts +114 -14
- package/src/did-btcr2.ts +4 -3
package/dist/browser.js
CHANGED
|
@@ -93159,6 +93159,7 @@ a=end-of-candidates
|
|
|
93159
93159
|
CASBeaconError: () => CASBeaconError,
|
|
93160
93160
|
CHANGE_OUTPUT_VBYTES: () => CHANGE_OUTPUT_VBYTES,
|
|
93161
93161
|
DEFAULT_FEE_ESTIMATOR: () => DEFAULT_FEE_ESTIMATOR,
|
|
93162
|
+
DEFAULT_MAX_DISCOVERY_ROUNDS: () => DEFAULT_MAX_DISCOVERY_ROUNDS,
|
|
93162
93163
|
DID_REGEX: () => DID_REGEX,
|
|
93163
93164
|
DUST_LIMIT_SATS: () => DUST_LIMIT_SATS,
|
|
93164
93165
|
DidBtcr2: () => DidBtcr2,
|
|
@@ -119289,7 +119290,8 @@ a=end-of-candidates
|
|
|
119289
119290
|
return new Resolver(didComponents, sidecarData, currentDocument, {
|
|
119290
119291
|
versionId: resolutionOptions.versionId,
|
|
119291
119292
|
versionTime: resolutionOptions.versionTime,
|
|
119292
|
-
genesisDocument: resolutionOptions.sidecar?.genesisDocument
|
|
119293
|
+
genesisDocument: resolutionOptions.sidecar?.genesisDocument,
|
|
119294
|
+
maxDiscoveryRounds: resolutionOptions.maxDiscoveryRounds
|
|
119293
119295
|
});
|
|
119294
119296
|
}
|
|
119295
119297
|
/**
|
|
@@ -119398,6 +119400,21 @@ a=end-of-candidates
|
|
|
119398
119400
|
|
|
119399
119401
|
// src/core/resolver.ts
|
|
119400
119402
|
init_utils2();
|
|
119403
|
+
var DEFAULT_MAX_DISCOVERY_ROUNDS = 10;
|
|
119404
|
+
function isRecord(value2) {
|
|
119405
|
+
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
119406
|
+
}
|
|
119407
|
+
function isCASAnnouncement(value2) {
|
|
119408
|
+
return isRecord(value2) && Object.values(value2).every((v) => typeof v === "string");
|
|
119409
|
+
}
|
|
119410
|
+
function isSignedBTCR2Update(value2) {
|
|
119411
|
+
if (!isRecord(value2)) return false;
|
|
119412
|
+
return Array.isArray(value2.patch) && typeof value2.sourceHash === "string" && typeof value2.targetHash === "string" && typeof value2.targetVersionId === "number" && isRecord(value2.proof);
|
|
119413
|
+
}
|
|
119414
|
+
function isSMTProof(value2) {
|
|
119415
|
+
if (!isRecord(value2)) return false;
|
|
119416
|
+
return typeof value2.id === "string" && typeof value2.collapsed === "string" && Array.isArray(value2.hashes);
|
|
119417
|
+
}
|
|
119401
119418
|
var Resolver = class _Resolver {
|
|
119402
119419
|
// --- Immutable inputs ---
|
|
119403
119420
|
#didComponents;
|
|
@@ -119415,6 +119432,10 @@ a=end-of-candidates
|
|
|
119415
119432
|
#requestCache = /* @__PURE__ */ new Set();
|
|
119416
119433
|
#unsortedUpdates = [];
|
|
119417
119434
|
#resolvedResponse = null;
|
|
119435
|
+
/** Upper bound on multi-round beacon-discovery passes; see {@link DEFAULT_MAX_DISCOVERY_ROUNDS}. */
|
|
119436
|
+
#maxDiscoveryRounds;
|
|
119437
|
+
/** Count of beacon-discovery passes driven by updates adding new beacon services. */
|
|
119438
|
+
#discoveryRounds = 0;
|
|
119418
119439
|
/**
|
|
119419
119440
|
* @internal Use {@link DidBtcr2.resolve} to create instances.
|
|
119420
119441
|
*/
|
|
@@ -119424,6 +119445,7 @@ a=end-of-candidates
|
|
|
119424
119445
|
this.#currentDocument = currentDocument;
|
|
119425
119446
|
this.#versionId = options2?.versionId;
|
|
119426
119447
|
this.#versionTime = options2?.versionTime;
|
|
119448
|
+
this.#maxDiscoveryRounds = options2?.maxDiscoveryRounds ?? DEFAULT_MAX_DISCOVERY_ROUNDS;
|
|
119427
119449
|
if (options2?.genesisDocument) {
|
|
119428
119450
|
this.#providedGenesisDocument = options2.genesisDocument;
|
|
119429
119451
|
}
|
|
@@ -119738,6 +119760,13 @@ a=end-of-candidates
|
|
|
119738
119760
|
return !this.#requestCache.has(address);
|
|
119739
119761
|
});
|
|
119740
119762
|
if (hasNewServices) {
|
|
119763
|
+
if (++this.#discoveryRounds > this.#maxDiscoveryRounds) {
|
|
119764
|
+
throw new ResolveError(
|
|
119765
|
+
`Exceeded maximum beacon-discovery rounds (${this.#maxDiscoveryRounds}); the DID document may chain beacon services without terminating.`,
|
|
119766
|
+
INVALID_DID_DOCUMENT,
|
|
119767
|
+
{ maxDiscoveryRounds: this.#maxDiscoveryRounds }
|
|
119768
|
+
);
|
|
119769
|
+
}
|
|
119741
119770
|
this.#phase = "BeaconDiscovery" /* BeaconDiscovery */;
|
|
119742
119771
|
continue;
|
|
119743
119772
|
}
|
|
@@ -119764,38 +119793,84 @@ a=end-of-candidates
|
|
|
119764
119793
|
provide(need, data) {
|
|
119765
119794
|
switch (need.kind) {
|
|
119766
119795
|
case "NeedGenesisDocument": {
|
|
119796
|
+
if (!isRecord(data)) {
|
|
119797
|
+
throw new ResolveError(
|
|
119798
|
+
"Provided data for NeedGenesisDocument must be a document object.",
|
|
119799
|
+
INVALID_DID_UPDATE,
|
|
119800
|
+
{ kind: need.kind }
|
|
119801
|
+
);
|
|
119802
|
+
}
|
|
119767
119803
|
this.#providedGenesisDocument = data;
|
|
119768
119804
|
break;
|
|
119769
119805
|
}
|
|
119770
119806
|
case "NeedBeaconSignals": {
|
|
119771
|
-
|
|
119772
|
-
|
|
119807
|
+
if (!(data instanceof Map)) {
|
|
119808
|
+
throw new ResolveError(
|
|
119809
|
+
"Provided data for NeedBeaconSignals must be a Map of beacon services to signals.",
|
|
119810
|
+
INVALID_DID_UPDATE,
|
|
119811
|
+
{ kind: need.kind }
|
|
119812
|
+
);
|
|
119813
|
+
}
|
|
119814
|
+
for (const [service, serviceSignals] of data) {
|
|
119773
119815
|
this.#beaconServicesSignals.set(service, serviceSignals);
|
|
119774
119816
|
}
|
|
119775
119817
|
break;
|
|
119776
119818
|
}
|
|
119777
119819
|
case "NeedCASAnnouncement": {
|
|
119778
|
-
|
|
119779
|
-
|
|
119820
|
+
if (!isCASAnnouncement(data)) {
|
|
119821
|
+
throw new ResolveError(
|
|
119822
|
+
"Provided data for NeedCASAnnouncement is not a CAS announcement.",
|
|
119823
|
+
INVALID_DID_UPDATE,
|
|
119824
|
+
{ kind: need.kind }
|
|
119825
|
+
);
|
|
119826
|
+
}
|
|
119827
|
+
const announcementHash = canonicalHash(data, { encoding: "hex" });
|
|
119828
|
+
if (announcementHash !== need.announcementHash) {
|
|
119829
|
+
throw new ResolveError(
|
|
119830
|
+
`CAS announcement hash mismatch: expected ${need.announcementHash}, got ${announcementHash}.`,
|
|
119831
|
+
INVALID_DID_UPDATE,
|
|
119832
|
+
{ expected: need.announcementHash, actual: announcementHash }
|
|
119833
|
+
);
|
|
119834
|
+
}
|
|
119835
|
+
this.#sidecarData.casMap.set(announcementHash, data);
|
|
119780
119836
|
break;
|
|
119781
119837
|
}
|
|
119782
119838
|
case "NeedSignedUpdate": {
|
|
119783
|
-
|
|
119784
|
-
|
|
119839
|
+
if (!isSignedBTCR2Update(data)) {
|
|
119840
|
+
throw new ResolveError(
|
|
119841
|
+
"Provided data for NeedSignedUpdate is not a signed BTCR2 update.",
|
|
119842
|
+
INVALID_DID_UPDATE,
|
|
119843
|
+
{ kind: need.kind }
|
|
119844
|
+
);
|
|
119845
|
+
}
|
|
119846
|
+
const updateHash = canonicalHash(data, { encoding: "hex" });
|
|
119847
|
+
if (updateHash !== need.updateHash) {
|
|
119848
|
+
throw new ResolveError(
|
|
119849
|
+
`Signed update hash mismatch: expected ${need.updateHash}, got ${updateHash}.`,
|
|
119850
|
+
INVALID_DID_UPDATE,
|
|
119851
|
+
{ expected: need.updateHash, actual: updateHash }
|
|
119852
|
+
);
|
|
119853
|
+
}
|
|
119854
|
+
this.#sidecarData.updateMap.set(updateHash, data);
|
|
119785
119855
|
break;
|
|
119786
119856
|
}
|
|
119787
119857
|
case "NeedSMTProof": {
|
|
119788
|
-
|
|
119789
|
-
|
|
119790
|
-
|
|
119791
|
-
|
|
119858
|
+
if (!isSMTProof(data)) {
|
|
119859
|
+
throw new ResolveError(
|
|
119860
|
+
"Provided data for NeedSMTProof is not an SMT proof.",
|
|
119861
|
+
INVALID_DID_UPDATE,
|
|
119862
|
+
{ kind: need.kind }
|
|
119863
|
+
);
|
|
119864
|
+
}
|
|
119865
|
+
const proofIdHex = encode(decode(data.id, "base64urlnopad"), "hex");
|
|
119866
|
+
if (proofIdHex !== need.smtRootHash) {
|
|
119792
119867
|
throw new ResolveError(
|
|
119793
|
-
`SMT proof root hash mismatch: expected ${
|
|
119868
|
+
`SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex}`,
|
|
119794
119869
|
INVALID_DID_UPDATE,
|
|
119795
|
-
{ expected:
|
|
119870
|
+
{ expected: need.smtRootHash, actual: proofIdHex }
|
|
119796
119871
|
);
|
|
119797
119872
|
}
|
|
119798
|
-
this.#sidecarData.smtMap.set(
|
|
119873
|
+
this.#sidecarData.smtMap.set(need.smtRootHash, data);
|
|
119799
119874
|
break;
|
|
119800
119875
|
}
|
|
119801
119876
|
}
|
package/dist/browser.mjs
CHANGED
|
@@ -119242,7 +119242,8 @@ var DidBtcr2 = class {
|
|
|
119242
119242
|
return new Resolver(didComponents, sidecarData, currentDocument, {
|
|
119243
119243
|
versionId: resolutionOptions.versionId,
|
|
119244
119244
|
versionTime: resolutionOptions.versionTime,
|
|
119245
|
-
genesisDocument: resolutionOptions.sidecar?.genesisDocument
|
|
119245
|
+
genesisDocument: resolutionOptions.sidecar?.genesisDocument,
|
|
119246
|
+
maxDiscoveryRounds: resolutionOptions.maxDiscoveryRounds
|
|
119246
119247
|
});
|
|
119247
119248
|
}
|
|
119248
119249
|
/**
|
|
@@ -119351,6 +119352,21 @@ var DidBtcr2 = class {
|
|
|
119351
119352
|
|
|
119352
119353
|
// src/core/resolver.ts
|
|
119353
119354
|
init_utils2();
|
|
119355
|
+
var DEFAULT_MAX_DISCOVERY_ROUNDS = 10;
|
|
119356
|
+
function isRecord(value2) {
|
|
119357
|
+
return typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
|
|
119358
|
+
}
|
|
119359
|
+
function isCASAnnouncement(value2) {
|
|
119360
|
+
return isRecord(value2) && Object.values(value2).every((v) => typeof v === "string");
|
|
119361
|
+
}
|
|
119362
|
+
function isSignedBTCR2Update(value2) {
|
|
119363
|
+
if (!isRecord(value2)) return false;
|
|
119364
|
+
return Array.isArray(value2.patch) && typeof value2.sourceHash === "string" && typeof value2.targetHash === "string" && typeof value2.targetVersionId === "number" && isRecord(value2.proof);
|
|
119365
|
+
}
|
|
119366
|
+
function isSMTProof(value2) {
|
|
119367
|
+
if (!isRecord(value2)) return false;
|
|
119368
|
+
return typeof value2.id === "string" && typeof value2.collapsed === "string" && Array.isArray(value2.hashes);
|
|
119369
|
+
}
|
|
119354
119370
|
var Resolver = class _Resolver {
|
|
119355
119371
|
// --- Immutable inputs ---
|
|
119356
119372
|
#didComponents;
|
|
@@ -119368,6 +119384,10 @@ var Resolver = class _Resolver {
|
|
|
119368
119384
|
#requestCache = /* @__PURE__ */ new Set();
|
|
119369
119385
|
#unsortedUpdates = [];
|
|
119370
119386
|
#resolvedResponse = null;
|
|
119387
|
+
/** Upper bound on multi-round beacon-discovery passes; see {@link DEFAULT_MAX_DISCOVERY_ROUNDS}. */
|
|
119388
|
+
#maxDiscoveryRounds;
|
|
119389
|
+
/** Count of beacon-discovery passes driven by updates adding new beacon services. */
|
|
119390
|
+
#discoveryRounds = 0;
|
|
119371
119391
|
/**
|
|
119372
119392
|
* @internal Use {@link DidBtcr2.resolve} to create instances.
|
|
119373
119393
|
*/
|
|
@@ -119377,6 +119397,7 @@ var Resolver = class _Resolver {
|
|
|
119377
119397
|
this.#currentDocument = currentDocument;
|
|
119378
119398
|
this.#versionId = options2?.versionId;
|
|
119379
119399
|
this.#versionTime = options2?.versionTime;
|
|
119400
|
+
this.#maxDiscoveryRounds = options2?.maxDiscoveryRounds ?? DEFAULT_MAX_DISCOVERY_ROUNDS;
|
|
119380
119401
|
if (options2?.genesisDocument) {
|
|
119381
119402
|
this.#providedGenesisDocument = options2.genesisDocument;
|
|
119382
119403
|
}
|
|
@@ -119691,6 +119712,13 @@ var Resolver = class _Resolver {
|
|
|
119691
119712
|
return !this.#requestCache.has(address);
|
|
119692
119713
|
});
|
|
119693
119714
|
if (hasNewServices) {
|
|
119715
|
+
if (++this.#discoveryRounds > this.#maxDiscoveryRounds) {
|
|
119716
|
+
throw new ResolveError(
|
|
119717
|
+
`Exceeded maximum beacon-discovery rounds (${this.#maxDiscoveryRounds}); the DID document may chain beacon services without terminating.`,
|
|
119718
|
+
INVALID_DID_DOCUMENT,
|
|
119719
|
+
{ maxDiscoveryRounds: this.#maxDiscoveryRounds }
|
|
119720
|
+
);
|
|
119721
|
+
}
|
|
119694
119722
|
this.#phase = "BeaconDiscovery" /* BeaconDiscovery */;
|
|
119695
119723
|
continue;
|
|
119696
119724
|
}
|
|
@@ -119717,38 +119745,84 @@ var Resolver = class _Resolver {
|
|
|
119717
119745
|
provide(need, data) {
|
|
119718
119746
|
switch (need.kind) {
|
|
119719
119747
|
case "NeedGenesisDocument": {
|
|
119748
|
+
if (!isRecord(data)) {
|
|
119749
|
+
throw new ResolveError(
|
|
119750
|
+
"Provided data for NeedGenesisDocument must be a document object.",
|
|
119751
|
+
INVALID_DID_UPDATE,
|
|
119752
|
+
{ kind: need.kind }
|
|
119753
|
+
);
|
|
119754
|
+
}
|
|
119720
119755
|
this.#providedGenesisDocument = data;
|
|
119721
119756
|
break;
|
|
119722
119757
|
}
|
|
119723
119758
|
case "NeedBeaconSignals": {
|
|
119724
|
-
|
|
119725
|
-
|
|
119759
|
+
if (!(data instanceof Map)) {
|
|
119760
|
+
throw new ResolveError(
|
|
119761
|
+
"Provided data for NeedBeaconSignals must be a Map of beacon services to signals.",
|
|
119762
|
+
INVALID_DID_UPDATE,
|
|
119763
|
+
{ kind: need.kind }
|
|
119764
|
+
);
|
|
119765
|
+
}
|
|
119766
|
+
for (const [service, serviceSignals] of data) {
|
|
119726
119767
|
this.#beaconServicesSignals.set(service, serviceSignals);
|
|
119727
119768
|
}
|
|
119728
119769
|
break;
|
|
119729
119770
|
}
|
|
119730
119771
|
case "NeedCASAnnouncement": {
|
|
119731
|
-
|
|
119732
|
-
|
|
119772
|
+
if (!isCASAnnouncement(data)) {
|
|
119773
|
+
throw new ResolveError(
|
|
119774
|
+
"Provided data for NeedCASAnnouncement is not a CAS announcement.",
|
|
119775
|
+
INVALID_DID_UPDATE,
|
|
119776
|
+
{ kind: need.kind }
|
|
119777
|
+
);
|
|
119778
|
+
}
|
|
119779
|
+
const announcementHash = canonicalHash(data, { encoding: "hex" });
|
|
119780
|
+
if (announcementHash !== need.announcementHash) {
|
|
119781
|
+
throw new ResolveError(
|
|
119782
|
+
`CAS announcement hash mismatch: expected ${need.announcementHash}, got ${announcementHash}.`,
|
|
119783
|
+
INVALID_DID_UPDATE,
|
|
119784
|
+
{ expected: need.announcementHash, actual: announcementHash }
|
|
119785
|
+
);
|
|
119786
|
+
}
|
|
119787
|
+
this.#sidecarData.casMap.set(announcementHash, data);
|
|
119733
119788
|
break;
|
|
119734
119789
|
}
|
|
119735
119790
|
case "NeedSignedUpdate": {
|
|
119736
|
-
|
|
119737
|
-
|
|
119791
|
+
if (!isSignedBTCR2Update(data)) {
|
|
119792
|
+
throw new ResolveError(
|
|
119793
|
+
"Provided data for NeedSignedUpdate is not a signed BTCR2 update.",
|
|
119794
|
+
INVALID_DID_UPDATE,
|
|
119795
|
+
{ kind: need.kind }
|
|
119796
|
+
);
|
|
119797
|
+
}
|
|
119798
|
+
const updateHash = canonicalHash(data, { encoding: "hex" });
|
|
119799
|
+
if (updateHash !== need.updateHash) {
|
|
119800
|
+
throw new ResolveError(
|
|
119801
|
+
`Signed update hash mismatch: expected ${need.updateHash}, got ${updateHash}.`,
|
|
119802
|
+
INVALID_DID_UPDATE,
|
|
119803
|
+
{ expected: need.updateHash, actual: updateHash }
|
|
119804
|
+
);
|
|
119805
|
+
}
|
|
119806
|
+
this.#sidecarData.updateMap.set(updateHash, data);
|
|
119738
119807
|
break;
|
|
119739
119808
|
}
|
|
119740
119809
|
case "NeedSMTProof": {
|
|
119741
|
-
|
|
119742
|
-
|
|
119743
|
-
|
|
119744
|
-
|
|
119810
|
+
if (!isSMTProof(data)) {
|
|
119811
|
+
throw new ResolveError(
|
|
119812
|
+
"Provided data for NeedSMTProof is not an SMT proof.",
|
|
119813
|
+
INVALID_DID_UPDATE,
|
|
119814
|
+
{ kind: need.kind }
|
|
119815
|
+
);
|
|
119816
|
+
}
|
|
119817
|
+
const proofIdHex = encode(decode(data.id, "base64urlnopad"), "hex");
|
|
119818
|
+
if (proofIdHex !== need.smtRootHash) {
|
|
119745
119819
|
throw new ResolveError(
|
|
119746
|
-
`SMT proof root hash mismatch: expected ${
|
|
119820
|
+
`SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex}`,
|
|
119747
119821
|
INVALID_DID_UPDATE,
|
|
119748
|
-
{ expected:
|
|
119822
|
+
{ expected: need.smtRootHash, actual: proofIdHex }
|
|
119749
119823
|
);
|
|
119750
119824
|
}
|
|
119751
|
-
this.#sidecarData.smtMap.set(
|
|
119825
|
+
this.#sidecarData.smtMap.set(need.smtRootHash, data);
|
|
119752
119826
|
break;
|
|
119753
119827
|
}
|
|
119754
119828
|
}
|
|
@@ -119825,6 +119899,7 @@ export {
|
|
|
119825
119899
|
CASBeaconError,
|
|
119826
119900
|
CHANGE_OUTPUT_VBYTES,
|
|
119827
119901
|
DEFAULT_FEE_ESTIMATOR,
|
|
119902
|
+
DEFAULT_MAX_DISCOVERY_ROUNDS,
|
|
119828
119903
|
DID_REGEX,
|
|
119829
119904
|
DUST_LIMIT_SATS,
|
|
119830
119905
|
DidBtcr2,
|
package/dist/cjs/index.js
CHANGED
|
@@ -42,6 +42,7 @@ __export(index_exports, {
|
|
|
42
42
|
CASBeaconError: () => CASBeaconError,
|
|
43
43
|
CHANGE_OUTPUT_VBYTES: () => CHANGE_OUTPUT_VBYTES,
|
|
44
44
|
DEFAULT_FEE_ESTIMATOR: () => DEFAULT_FEE_ESTIMATOR,
|
|
45
|
+
DEFAULT_MAX_DISCOVERY_ROUNDS: () => DEFAULT_MAX_DISCOVERY_ROUNDS,
|
|
45
46
|
DID_REGEX: () => DID_REGEX,
|
|
46
47
|
DUST_LIMIT_SATS: () => DUST_LIMIT_SATS,
|
|
47
48
|
DidBtcr2: () => DidBtcr2,
|
|
@@ -2913,7 +2914,8 @@ var DidBtcr2 = class {
|
|
|
2913
2914
|
return new Resolver(didComponents, sidecarData, currentDocument, {
|
|
2914
2915
|
versionId: resolutionOptions.versionId,
|
|
2915
2916
|
versionTime: resolutionOptions.versionTime,
|
|
2916
|
-
genesisDocument: resolutionOptions.sidecar?.genesisDocument
|
|
2917
|
+
genesisDocument: resolutionOptions.sidecar?.genesisDocument,
|
|
2918
|
+
maxDiscoveryRounds: resolutionOptions.maxDiscoveryRounds
|
|
2917
2919
|
});
|
|
2918
2920
|
}
|
|
2919
2921
|
/**
|
|
@@ -3022,6 +3024,21 @@ var DidBtcr2 = class {
|
|
|
3022
3024
|
|
|
3023
3025
|
// src/core/resolver.ts
|
|
3024
3026
|
var import_utils6 = require("@noble/curves/utils.js");
|
|
3027
|
+
var DEFAULT_MAX_DISCOVERY_ROUNDS = 10;
|
|
3028
|
+
function isRecord(value) {
|
|
3029
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3030
|
+
}
|
|
3031
|
+
function isCASAnnouncement(value) {
|
|
3032
|
+
return isRecord(value) && Object.values(value).every((v) => typeof v === "string");
|
|
3033
|
+
}
|
|
3034
|
+
function isSignedBTCR2Update(value) {
|
|
3035
|
+
if (!isRecord(value)) return false;
|
|
3036
|
+
return Array.isArray(value.patch) && typeof value.sourceHash === "string" && typeof value.targetHash === "string" && typeof value.targetVersionId === "number" && isRecord(value.proof);
|
|
3037
|
+
}
|
|
3038
|
+
function isSMTProof(value) {
|
|
3039
|
+
if (!isRecord(value)) return false;
|
|
3040
|
+
return typeof value.id === "string" && typeof value.collapsed === "string" && Array.isArray(value.hashes);
|
|
3041
|
+
}
|
|
3025
3042
|
var Resolver = class _Resolver {
|
|
3026
3043
|
// --- Immutable inputs ---
|
|
3027
3044
|
#didComponents;
|
|
@@ -3039,6 +3056,10 @@ var Resolver = class _Resolver {
|
|
|
3039
3056
|
#requestCache = /* @__PURE__ */ new Set();
|
|
3040
3057
|
#unsortedUpdates = [];
|
|
3041
3058
|
#resolvedResponse = null;
|
|
3059
|
+
/** Upper bound on multi-round beacon-discovery passes; see {@link DEFAULT_MAX_DISCOVERY_ROUNDS}. */
|
|
3060
|
+
#maxDiscoveryRounds;
|
|
3061
|
+
/** Count of beacon-discovery passes driven by updates adding new beacon services. */
|
|
3062
|
+
#discoveryRounds = 0;
|
|
3042
3063
|
/**
|
|
3043
3064
|
* @internal Use {@link DidBtcr2.resolve} to create instances.
|
|
3044
3065
|
*/
|
|
@@ -3048,6 +3069,7 @@ var Resolver = class _Resolver {
|
|
|
3048
3069
|
this.#currentDocument = currentDocument;
|
|
3049
3070
|
this.#versionId = options?.versionId;
|
|
3050
3071
|
this.#versionTime = options?.versionTime;
|
|
3072
|
+
this.#maxDiscoveryRounds = options?.maxDiscoveryRounds ?? DEFAULT_MAX_DISCOVERY_ROUNDS;
|
|
3051
3073
|
if (options?.genesisDocument) {
|
|
3052
3074
|
this.#providedGenesisDocument = options.genesisDocument;
|
|
3053
3075
|
}
|
|
@@ -3362,6 +3384,13 @@ var Resolver = class _Resolver {
|
|
|
3362
3384
|
return !this.#requestCache.has(address);
|
|
3363
3385
|
});
|
|
3364
3386
|
if (hasNewServices) {
|
|
3387
|
+
if (++this.#discoveryRounds > this.#maxDiscoveryRounds) {
|
|
3388
|
+
throw new import_common12.ResolveError(
|
|
3389
|
+
`Exceeded maximum beacon-discovery rounds (${this.#maxDiscoveryRounds}); the DID document may chain beacon services without terminating.`,
|
|
3390
|
+
import_common12.INVALID_DID_DOCUMENT,
|
|
3391
|
+
{ maxDiscoveryRounds: this.#maxDiscoveryRounds }
|
|
3392
|
+
);
|
|
3393
|
+
}
|
|
3365
3394
|
this.#phase = "BeaconDiscovery" /* BeaconDiscovery */;
|
|
3366
3395
|
continue;
|
|
3367
3396
|
}
|
|
@@ -3388,38 +3417,84 @@ var Resolver = class _Resolver {
|
|
|
3388
3417
|
provide(need, data) {
|
|
3389
3418
|
switch (need.kind) {
|
|
3390
3419
|
case "NeedGenesisDocument": {
|
|
3420
|
+
if (!isRecord(data)) {
|
|
3421
|
+
throw new import_common12.ResolveError(
|
|
3422
|
+
"Provided data for NeedGenesisDocument must be a document object.",
|
|
3423
|
+
import_common12.INVALID_DID_UPDATE,
|
|
3424
|
+
{ kind: need.kind }
|
|
3425
|
+
);
|
|
3426
|
+
}
|
|
3391
3427
|
this.#providedGenesisDocument = data;
|
|
3392
3428
|
break;
|
|
3393
3429
|
}
|
|
3394
3430
|
case "NeedBeaconSignals": {
|
|
3395
|
-
|
|
3396
|
-
|
|
3431
|
+
if (!(data instanceof Map)) {
|
|
3432
|
+
throw new import_common12.ResolveError(
|
|
3433
|
+
"Provided data for NeedBeaconSignals must be a Map of beacon services to signals.",
|
|
3434
|
+
import_common12.INVALID_DID_UPDATE,
|
|
3435
|
+
{ kind: need.kind }
|
|
3436
|
+
);
|
|
3437
|
+
}
|
|
3438
|
+
for (const [service, serviceSignals] of data) {
|
|
3397
3439
|
this.#beaconServicesSignals.set(service, serviceSignals);
|
|
3398
3440
|
}
|
|
3399
3441
|
break;
|
|
3400
3442
|
}
|
|
3401
3443
|
case "NeedCASAnnouncement": {
|
|
3402
|
-
|
|
3403
|
-
|
|
3444
|
+
if (!isCASAnnouncement(data)) {
|
|
3445
|
+
throw new import_common12.ResolveError(
|
|
3446
|
+
"Provided data for NeedCASAnnouncement is not a CAS announcement.",
|
|
3447
|
+
import_common12.INVALID_DID_UPDATE,
|
|
3448
|
+
{ kind: need.kind }
|
|
3449
|
+
);
|
|
3450
|
+
}
|
|
3451
|
+
const announcementHash = (0, import_common12.canonicalHash)(data, { encoding: "hex" });
|
|
3452
|
+
if (announcementHash !== need.announcementHash) {
|
|
3453
|
+
throw new import_common12.ResolveError(
|
|
3454
|
+
`CAS announcement hash mismatch: expected ${need.announcementHash}, got ${announcementHash}.`,
|
|
3455
|
+
import_common12.INVALID_DID_UPDATE,
|
|
3456
|
+
{ expected: need.announcementHash, actual: announcementHash }
|
|
3457
|
+
);
|
|
3458
|
+
}
|
|
3459
|
+
this.#sidecarData.casMap.set(announcementHash, data);
|
|
3404
3460
|
break;
|
|
3405
3461
|
}
|
|
3406
3462
|
case "NeedSignedUpdate": {
|
|
3407
|
-
|
|
3408
|
-
|
|
3463
|
+
if (!isSignedBTCR2Update(data)) {
|
|
3464
|
+
throw new import_common12.ResolveError(
|
|
3465
|
+
"Provided data for NeedSignedUpdate is not a signed BTCR2 update.",
|
|
3466
|
+
import_common12.INVALID_DID_UPDATE,
|
|
3467
|
+
{ kind: need.kind }
|
|
3468
|
+
);
|
|
3469
|
+
}
|
|
3470
|
+
const updateHash = (0, import_common12.canonicalHash)(data, { encoding: "hex" });
|
|
3471
|
+
if (updateHash !== need.updateHash) {
|
|
3472
|
+
throw new import_common12.ResolveError(
|
|
3473
|
+
`Signed update hash mismatch: expected ${need.updateHash}, got ${updateHash}.`,
|
|
3474
|
+
import_common12.INVALID_DID_UPDATE,
|
|
3475
|
+
{ expected: need.updateHash, actual: updateHash }
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3478
|
+
this.#sidecarData.updateMap.set(updateHash, data);
|
|
3409
3479
|
break;
|
|
3410
3480
|
}
|
|
3411
3481
|
case "NeedSMTProof": {
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3482
|
+
if (!isSMTProof(data)) {
|
|
3483
|
+
throw new import_common12.ResolveError(
|
|
3484
|
+
"Provided data for NeedSMTProof is not an SMT proof.",
|
|
3485
|
+
import_common12.INVALID_DID_UPDATE,
|
|
3486
|
+
{ kind: need.kind }
|
|
3487
|
+
);
|
|
3488
|
+
}
|
|
3489
|
+
const proofIdHex = (0, import_common12.encode)((0, import_common12.decode)(data.id, "base64urlnopad"), "hex");
|
|
3490
|
+
if (proofIdHex !== need.smtRootHash) {
|
|
3416
3491
|
throw new import_common12.ResolveError(
|
|
3417
|
-
`SMT proof root hash mismatch: expected ${
|
|
3492
|
+
`SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex}`,
|
|
3418
3493
|
import_common12.INVALID_DID_UPDATE,
|
|
3419
|
-
{ expected:
|
|
3494
|
+
{ expected: need.smtRootHash, actual: proofIdHex }
|
|
3420
3495
|
);
|
|
3421
3496
|
}
|
|
3422
|
-
this.#sidecarData.smtMap.set(
|
|
3497
|
+
this.#sidecarData.smtMap.set(need.smtRootHash, data);
|
|
3423
3498
|
break;
|
|
3424
3499
|
}
|
|
3425
3500
|
}
|
|
@@ -3494,6 +3569,7 @@ var DidDocumentBuilder = class {
|
|
|
3494
3569
|
CASBeaconError,
|
|
3495
3570
|
CHANGE_OUTPUT_VBYTES,
|
|
3496
3571
|
DEFAULT_FEE_ESTIMATOR,
|
|
3572
|
+
DEFAULT_MAX_DISCOVERY_ROUNDS,
|
|
3497
3573
|
DID_REGEX,
|
|
3498
3574
|
DUST_LIMIT_SATS,
|
|
3499
3575
|
DidBtcr2,
|
|
@@ -9,6 +9,42 @@ import { BeaconFactory } from './beacon/factory.js';
|
|
|
9
9
|
import { BeaconUtils } from './beacon/utils.js';
|
|
10
10
|
import { Identifier } from './identifier.js';
|
|
11
11
|
import { equalBytes } from '@noble/curves/utils.js';
|
|
12
|
+
/**
|
|
13
|
+
* Default upper bound on multi-round beacon discovery. Each round applies the
|
|
14
|
+
* updates found so far and looks for beacon services those updates added; a
|
|
15
|
+
* document whose updates keep adding services would otherwise loop unbounded.
|
|
16
|
+
* Overridable via `ResolutionOptions.maxDiscoveryRounds`.
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_MAX_DISCOVERY_ROUNDS = 10;
|
|
19
|
+
// ─── provide() payload guards ────────────────────────────────────────────────
|
|
20
|
+
// Runtime shape checks so a malformed payload fails fast at the provide()
|
|
21
|
+
// boundary rather than flowing downstream as an unchecked `as` cast.
|
|
22
|
+
/** True if `value` is a non-null, non-array object. */
|
|
23
|
+
function isRecord(value) {
|
|
24
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
/** True if `value` has the shape of a CAS Announcement (a flat record of string hashes). */
|
|
27
|
+
function isCASAnnouncement(value) {
|
|
28
|
+
return isRecord(value) && Object.values(value).every(v => typeof v === 'string');
|
|
29
|
+
}
|
|
30
|
+
/** True if `value` has the shape of a signed BTCR2 update. */
|
|
31
|
+
function isSignedBTCR2Update(value) {
|
|
32
|
+
if (!isRecord(value))
|
|
33
|
+
return false;
|
|
34
|
+
return Array.isArray(value.patch)
|
|
35
|
+
&& typeof value.sourceHash === 'string'
|
|
36
|
+
&& typeof value.targetHash === 'string'
|
|
37
|
+
&& typeof value.targetVersionId === 'number'
|
|
38
|
+
&& isRecord(value.proof);
|
|
39
|
+
}
|
|
40
|
+
/** True if `value` has the shape of an SMT inclusion / non-inclusion proof. */
|
|
41
|
+
function isSMTProof(value) {
|
|
42
|
+
if (!isRecord(value))
|
|
43
|
+
return false;
|
|
44
|
+
return typeof value.id === 'string'
|
|
45
|
+
&& typeof value.collapsed === 'string'
|
|
46
|
+
&& Array.isArray(value.hashes);
|
|
47
|
+
}
|
|
12
48
|
/**
|
|
13
49
|
* Different possible Resolver states representing phases in the resolution process.
|
|
14
50
|
*/
|
|
@@ -59,6 +95,10 @@ export class Resolver {
|
|
|
59
95
|
#requestCache = new Set();
|
|
60
96
|
#unsortedUpdates = [];
|
|
61
97
|
#resolvedResponse = null;
|
|
98
|
+
/** Upper bound on multi-round beacon-discovery passes; see {@link DEFAULT_MAX_DISCOVERY_ROUNDS}. */
|
|
99
|
+
#maxDiscoveryRounds;
|
|
100
|
+
/** Count of beacon-discovery passes driven by updates adding new beacon services. */
|
|
101
|
+
#discoveryRounds = 0;
|
|
62
102
|
/**
|
|
63
103
|
* @internal Use {@link DidBtcr2.resolve} to create instances.
|
|
64
104
|
*/
|
|
@@ -68,6 +108,7 @@ export class Resolver {
|
|
|
68
108
|
this.#currentDocument = currentDocument;
|
|
69
109
|
this.#versionId = options?.versionId;
|
|
70
110
|
this.#versionTime = options?.versionTime;
|
|
111
|
+
this.#maxDiscoveryRounds = options?.maxDiscoveryRounds ?? DEFAULT_MAX_DISCOVERY_ROUNDS;
|
|
71
112
|
// If a genesis document was provided (from sidecar), pre-seed it for validation
|
|
72
113
|
if (options?.genesisDocument) {
|
|
73
114
|
this.#providedGenesisDocument = options.genesisDocument;
|
|
@@ -430,6 +471,13 @@ export class Resolver {
|
|
|
430
471
|
return !this.#requestCache.has(address);
|
|
431
472
|
});
|
|
432
473
|
if (hasNewServices) {
|
|
474
|
+
// Bound multi-round discovery: a document whose updates keep adding
|
|
475
|
+
// beacon services would otherwise loop without end. Fail closed once
|
|
476
|
+
// the configured number of rounds is exceeded.
|
|
477
|
+
if (++this.#discoveryRounds > this.#maxDiscoveryRounds) {
|
|
478
|
+
throw new ResolveError(`Exceeded maximum beacon-discovery rounds (${this.#maxDiscoveryRounds}); `
|
|
479
|
+
+ 'the DID document may chain beacon services without terminating.', INVALID_DID_DOCUMENT, { maxDiscoveryRounds: this.#maxDiscoveryRounds });
|
|
480
|
+
}
|
|
433
481
|
// Loop back to discover signals for new beacon services
|
|
434
482
|
this.#phase = ResolverPhase.BeaconDiscovery;
|
|
435
483
|
continue;
|
|
@@ -457,35 +505,57 @@ export class Resolver {
|
|
|
457
505
|
provide(need, data) {
|
|
458
506
|
switch (need.kind) {
|
|
459
507
|
case 'NeedGenesisDocument': {
|
|
508
|
+
if (!isRecord(data)) {
|
|
509
|
+
throw new ResolveError('Provided data for NeedGenesisDocument must be a document object.', INVALID_DID_UPDATE, { kind: need.kind });
|
|
510
|
+
}
|
|
460
511
|
this.#providedGenesisDocument = data;
|
|
461
512
|
break;
|
|
462
513
|
}
|
|
463
514
|
case 'NeedBeaconSignals': {
|
|
464
|
-
|
|
465
|
-
|
|
515
|
+
if (!(data instanceof Map)) {
|
|
516
|
+
throw new ResolveError('Provided data for NeedBeaconSignals must be a Map of beacon services to signals.', INVALID_DID_UPDATE, { kind: need.kind });
|
|
517
|
+
}
|
|
518
|
+
for (const [service, serviceSignals] of data) {
|
|
466
519
|
this.#beaconServicesSignals.set(service, serviceSignals);
|
|
467
520
|
}
|
|
468
521
|
break;
|
|
469
522
|
}
|
|
470
523
|
case 'NeedCASAnnouncement': {
|
|
471
|
-
|
|
472
|
-
|
|
524
|
+
if (!isCASAnnouncement(data)) {
|
|
525
|
+
throw new ResolveError('Provided data for NeedCASAnnouncement is not a CAS announcement.', INVALID_DID_UPDATE, { kind: need.kind });
|
|
526
|
+
}
|
|
527
|
+
// Fail fast if the provided announcement is not the one the on-chain
|
|
528
|
+
// signal requested: its canonical hash must equal the need's hash.
|
|
529
|
+
const announcementHash = canonicalHash(data, { encoding: 'hex' });
|
|
530
|
+
if (announcementHash !== need.announcementHash) {
|
|
531
|
+
throw new ResolveError(`CAS announcement hash mismatch: expected ${need.announcementHash}, got ${announcementHash}.`, INVALID_DID_UPDATE, { expected: need.announcementHash, actual: announcementHash });
|
|
532
|
+
}
|
|
533
|
+
this.#sidecarData.casMap.set(announcementHash, data);
|
|
473
534
|
break;
|
|
474
535
|
}
|
|
475
536
|
case 'NeedSignedUpdate': {
|
|
476
|
-
|
|
477
|
-
|
|
537
|
+
if (!isSignedBTCR2Update(data)) {
|
|
538
|
+
throw new ResolveError('Provided data for NeedSignedUpdate is not a signed BTCR2 update.', INVALID_DID_UPDATE, { kind: need.kind });
|
|
539
|
+
}
|
|
540
|
+
// Fail fast if the provided update is not the one the on-chain signal
|
|
541
|
+
// requested: its canonical hash must equal the need's hash.
|
|
542
|
+
const updateHash = canonicalHash(data, { encoding: 'hex' });
|
|
543
|
+
if (updateHash !== need.updateHash) {
|
|
544
|
+
throw new ResolveError(`Signed update hash mismatch: expected ${need.updateHash}, got ${updateHash}.`, INVALID_DID_UPDATE, { expected: need.updateHash, actual: updateHash });
|
|
545
|
+
}
|
|
546
|
+
this.#sidecarData.updateMap.set(updateHash, data);
|
|
478
547
|
break;
|
|
479
548
|
}
|
|
480
549
|
case 'NeedSMTProof': {
|
|
481
|
-
|
|
482
|
-
|
|
550
|
+
if (!isSMTProof(data)) {
|
|
551
|
+
throw new ResolveError('Provided data for NeedSMTProof is not an SMT proof.', INVALID_DID_UPDATE, { kind: need.kind });
|
|
552
|
+
}
|
|
483
553
|
// proof.id is base64url per spec; smtRootHash is the hex on-chain signal.
|
|
484
|
-
const proofIdHex = encodeHash(decodeHash(
|
|
485
|
-
if (proofIdHex !==
|
|
486
|
-
throw new ResolveError(`SMT proof root hash mismatch: expected ${
|
|
554
|
+
const proofIdHex = encodeHash(decodeHash(data.id, 'base64urlnopad'), 'hex');
|
|
555
|
+
if (proofIdHex !== need.smtRootHash) {
|
|
556
|
+
throw new ResolveError(`SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex}`, INVALID_DID_UPDATE, { expected: need.smtRootHash, actual: proofIdHex });
|
|
487
557
|
}
|
|
488
|
-
this.#sidecarData.smtMap.set(
|
|
558
|
+
this.#sidecarData.smtMap.set(need.smtRootHash, data);
|
|
489
559
|
break;
|
|
490
560
|
}
|
|
491
561
|
}
|