@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/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
- const signals = data;
119772
- for (const [service, serviceSignals] of signals) {
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
- const announcement = data;
119779
- this.#sidecarData.casMap.set(canonicalHash(announcement, { encoding: "hex" }), announcement);
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
- const update = data;
119784
- this.#sidecarData.updateMap.set(canonicalHash(update, { encoding: "hex" }), update);
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
- const smtNeed = need;
119789
- const proof = data;
119790
- const proofIdHex = encode(decode(proof.id, "base64urlnopad"), "hex");
119791
- if (proofIdHex !== smtNeed.smtRootHash) {
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 ${smtNeed.smtRootHash}, got ${proofIdHex}`,
119868
+ `SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex}`,
119794
119869
  INVALID_DID_UPDATE,
119795
- { expected: smtNeed.smtRootHash, actual: proofIdHex }
119870
+ { expected: need.smtRootHash, actual: proofIdHex }
119796
119871
  );
119797
119872
  }
119798
- this.#sidecarData.smtMap.set(smtNeed.smtRootHash, proof);
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
- const signals = data;
119725
- for (const [service, serviceSignals] of signals) {
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
- const announcement = data;
119732
- this.#sidecarData.casMap.set(canonicalHash(announcement, { encoding: "hex" }), announcement);
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
- const update = data;
119737
- this.#sidecarData.updateMap.set(canonicalHash(update, { encoding: "hex" }), update);
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
- const smtNeed = need;
119742
- const proof = data;
119743
- const proofIdHex = encode(decode(proof.id, "base64urlnopad"), "hex");
119744
- if (proofIdHex !== smtNeed.smtRootHash) {
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 ${smtNeed.smtRootHash}, got ${proofIdHex}`,
119820
+ `SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex}`,
119747
119821
  INVALID_DID_UPDATE,
119748
- { expected: smtNeed.smtRootHash, actual: proofIdHex }
119822
+ { expected: need.smtRootHash, actual: proofIdHex }
119749
119823
  );
119750
119824
  }
119751
- this.#sidecarData.smtMap.set(smtNeed.smtRootHash, proof);
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
- const signals = data;
3396
- for (const [service, serviceSignals] of signals) {
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
- const announcement = data;
3403
- this.#sidecarData.casMap.set((0, import_common12.canonicalHash)(announcement, { encoding: "hex" }), announcement);
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
- const update = data;
3408
- this.#sidecarData.updateMap.set((0, import_common12.canonicalHash)(update, { encoding: "hex" }), update);
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
- const smtNeed = need;
3413
- const proof = data;
3414
- const proofIdHex = (0, import_common12.encode)((0, import_common12.decode)(proof.id, "base64urlnopad"), "hex");
3415
- if (proofIdHex !== smtNeed.smtRootHash) {
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 ${smtNeed.smtRootHash}, got ${proofIdHex}`,
3492
+ `SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex}`,
3418
3493
  import_common12.INVALID_DID_UPDATE,
3419
- { expected: smtNeed.smtRootHash, actual: proofIdHex }
3494
+ { expected: need.smtRootHash, actual: proofIdHex }
3420
3495
  );
3421
3496
  }
3422
- this.#sidecarData.smtMap.set(smtNeed.smtRootHash, proof);
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
- const signals = data;
465
- for (const [service, serviceSignals] of signals) {
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
- const announcement = data;
472
- this.#sidecarData.casMap.set(canonicalHash(announcement, { encoding: 'hex' }), announcement);
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
- const update = data;
477
- this.#sidecarData.updateMap.set(canonicalHash(update, { encoding: 'hex' }), update);
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
- const smtNeed = need;
482
- const proof = data;
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(proof.id, 'base64urlnopad'), 'hex');
485
- if (proofIdHex !== smtNeed.smtRootHash) {
486
- throw new ResolveError(`SMT proof root hash mismatch: expected ${smtNeed.smtRootHash}, got ${proofIdHex}`, INVALID_DID_UPDATE, { expected: smtNeed.smtRootHash, actual: proofIdHex });
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(smtNeed.smtRootHash, proof);
558
+ this.#sidecarData.smtMap.set(need.smtRootHash, data);
489
559
  break;
490
560
  }
491
561
  }