@did-btcr2/method 0.62.0 → 0.64.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.
Files changed (46) hide show
  1. package/README.md +14 -1
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/browser.js +3 -3
  4. package/dist/browser.mjs +3 -3
  5. package/dist/cjs/index.js +1269 -986
  6. package/dist/esm/core/beacon/signal-discovery.js +34 -1
  7. package/dist/esm/core/beacon/signal-discovery.js.map +1 -1
  8. package/dist/esm/core/btcr2-update.js +11 -0
  9. package/dist/esm/core/btcr2-update.js.map +1 -1
  10. package/dist/esm/core/resolver.js +345 -252
  11. package/dist/esm/core/resolver.js.map +1 -1
  12. package/dist/esm/core/updater.js +33 -3
  13. package/dist/esm/core/updater.js.map +1 -1
  14. package/dist/esm/did-btcr2.js +53 -20
  15. package/dist/esm/did-btcr2.js.map +1 -1
  16. package/dist/esm/utils/appendix.js +39 -2
  17. package/dist/esm/utils/appendix.js.map +1 -1
  18. package/dist/esm/utils/error-cause.js +16 -0
  19. package/dist/esm/utils/error-cause.js.map +1 -0
  20. package/dist/types/core/beacon/interfaces.d.ts +9 -1
  21. package/dist/types/core/beacon/interfaces.d.ts.map +1 -1
  22. package/dist/types/core/beacon/signal-discovery.d.ts +12 -0
  23. package/dist/types/core/beacon/signal-discovery.d.ts.map +1 -1
  24. package/dist/types/core/btcr2-update.d.ts +15 -8
  25. package/dist/types/core/btcr2-update.d.ts.map +1 -1
  26. package/dist/types/core/interfaces.d.ts +16 -5
  27. package/dist/types/core/interfaces.d.ts.map +1 -1
  28. package/dist/types/core/resolver.d.ts +37 -23
  29. package/dist/types/core/resolver.d.ts.map +1 -1
  30. package/dist/types/core/updater.d.ts.map +1 -1
  31. package/dist/types/did-btcr2.d.ts +24 -3
  32. package/dist/types/did-btcr2.d.ts.map +1 -1
  33. package/dist/types/utils/appendix.d.ts +24 -0
  34. package/dist/types/utils/appendix.d.ts.map +1 -1
  35. package/dist/types/utils/error-cause.d.ts +16 -0
  36. package/dist/types/utils/error-cause.d.ts.map +1 -0
  37. package/package.json +3 -3
  38. package/src/core/beacon/interfaces.ts +10 -1
  39. package/src/core/beacon/signal-discovery.ts +44 -1
  40. package/src/core/btcr2-update.ts +20 -8
  41. package/src/core/interfaces.ts +16 -5
  42. package/src/core/resolver.ts +420 -315
  43. package/src/core/updater.ts +41 -3
  44. package/src/did-btcr2.ts +70 -25
  45. package/src/utils/appendix.ts +48 -2
  46. package/src/utils/error-cause.ts +23 -0
package/dist/cjs/index.js CHANGED
@@ -32,6 +32,7 @@ __export(index_exports, {
32
32
  CASBeacon: () => CASBeacon,
33
33
  CASBeaconError: () => CASBeaconError,
34
34
  CHANGE_OUTPUT_VBYTES: () => CHANGE_OUTPUT_VBYTES,
35
+ DEACTIVATION_PATCH: () => DEACTIVATION_PATCH,
35
36
  DEFAULT_FEE_ESTIMATOR: () => DEFAULT_FEE_ESTIMATOR,
36
37
  DEFAULT_MIN_CONF: () => DEFAULT_MIN_CONF,
37
38
  DID_REGEX: () => DID_REGEX,
@@ -764,6 +765,43 @@ var Appendix = class _Appendix {
764
765
  const id = typeof entry === "object" && entry !== null && !Array.isArray(entry) ? entry.id : entry;
765
766
  return _Appendix.absoluteDidUrl(id, did);
766
767
  }
768
+ /**
769
+ * Finds the entry of `document.capabilityInvocation` that identifies `methodId`. A
770
+ * reference entry identifies it when the two DID URLs are equal. An embedded verification
771
+ * method object identifies it when its `id` is equal. Both spellings of a DID URL compare
772
+ * equal, as in {@link relationshipMethodId}. This is the lookup of the specification steps
773
+ * "Check `update.proof`" (the read path) and "Construct BTCR2 Signed Update" (the write
774
+ * path); the caller raises `INVALID_DID_UPDATE` when no entry identifies the method.
775
+ *
776
+ * @param {DidDocument} document The DID document.
777
+ * @param {unknown} methodId The verification method id, absolute or relative. A non-string yields `undefined`.
778
+ * @returns {string | DidVerificationMethod | undefined} The entry, or `undefined` if no entry identifies the id.
779
+ */
780
+ static capabilityInvocationEntry(document, methodId) {
781
+ const targetId = _Appendix.relationshipMethodId(methodId, document.id);
782
+ if (targetId === void 0) return void 0;
783
+ return document.capabilityInvocation?.find(
784
+ (entry) => _Appendix.relationshipMethodId(entry, document.id) === targetId
785
+ );
786
+ }
787
+ /**
788
+ * Returns the verification method that a relationship entry denotes: the object itself
789
+ * when the entry embeds the method, else the member of `document.verificationMethod` whose
790
+ * `id` equals the reference. Both spellings of a DID URL compare equal. The caller raises
791
+ * `INVALID_DID_UPDATE` when a reference names no member.
792
+ *
793
+ * @param {DidDocument} document The DID document.
794
+ * @param {string | DidVerificationMethod} entry The relationship entry: a reference, or an embedded method.
795
+ * @returns {DidVerificationMethod | undefined} The method, or `undefined` if a reference names no member.
796
+ */
797
+ static verificationMethodOfEntry(document, entry) {
798
+ if (_Appendix.isDidVerificationMethod(entry)) return entry;
799
+ const targetId = _Appendix.absoluteDidUrl(entry, document.id);
800
+ if (targetId === void 0) return void 0;
801
+ return document.verificationMethod?.find(
802
+ (method) => _Appendix.absoluteDidUrl(method?.id, document.id) === targetId
803
+ );
804
+ }
767
805
  /**
768
806
  * Validates that the given object is a DidVerificationMethod
769
807
  * @param {unknown} obj The object to validate
@@ -879,10 +917,11 @@ var Appendix = class _Appendix {
879
917
  */
880
918
  static dereferenceZcapId(capabilityId) {
881
919
  const rootCapability = {};
882
- const [urn, zcap, root, did] = capabilityId.split(":") ?? [];
883
- if ([urn, zcap, root, did].length !== 4) {
920
+ const components = capabilityId.split(":");
921
+ if (components.length !== 4) {
884
922
  throw new import_dids.DidError(import_dids.DidErrorCode.InvalidDid, `Invalid capabilityId: ${capabilityId}`);
885
923
  }
924
+ const [urn, zcap, root, did] = components;
886
925
  if (!urn || urn !== "urn") {
887
926
  throw new import_dids.DidError(import_dids.DidErrorCode.InvalidDid, `Invalid capabilityId: ${capabilityId}`);
888
927
  }
@@ -1814,6 +1853,7 @@ var BeaconSignalDiscovery = class _BeaconSignalDiscovery {
1814
1853
  static async indexer(beaconServices, bitcoin) {
1815
1854
  const beaconServiceSignals = /* @__PURE__ */ new Map();
1816
1855
  const currentBlockCount = await bitcoin.rest.block.count();
1856
+ const mediantimes = /* @__PURE__ */ new Map();
1817
1857
  for (const beaconService of beaconServices) {
1818
1858
  beaconServiceSignals.set(beaconService, []);
1819
1859
  const beaconAddress = BeaconUtils.parseBitcoinAddress(beaconService.serviceEndpoint);
@@ -1838,19 +1878,49 @@ var BeaconSignalDiscovery = class _BeaconSignalDiscovery {
1838
1878
  continue;
1839
1879
  }
1840
1880
  const confirmations = currentBlockCount - status.block_height + 1;
1881
+ const mediantime = await _BeaconSignalDiscovery.mediantime(status.block_hash, bitcoin, mediantimes);
1841
1882
  beaconServiceSignals.get(beaconService)?.push({
1842
1883
  tx: beaconSignal,
1843
1884
  signalBytes: updateHash,
1844
1885
  blockMetadata: {
1845
1886
  confirmations,
1846
1887
  height: status.block_height,
1847
- time: status.block_time
1888
+ time: status.block_time,
1889
+ mediantime
1848
1890
  }
1849
1891
  });
1850
1892
  }
1851
1893
  }
1852
1894
  return beaconServiceSignals;
1853
1895
  }
1896
+ /**
1897
+ * Return the median time past of a block, from the per-run cache or from the
1898
+ * Esplora block record (`GET /block/:hash`). The specification compares
1899
+ * `versionTime` with the block `mediantime`, and the address listing does not
1900
+ * carry it. One block record serves every signal in that block.
1901
+ * @param {string} blockhash The hash of the block that contains a signal.
1902
+ * @param {BitcoinConnection} bitcoin Bitcoin network connection to use for REST calls.
1903
+ * @param {Map<string, number>} cache The median time past values fetched so far, keyed by block hash.
1904
+ * @returns {Promise<number>} The median time past of the block, in Unix seconds.
1905
+ * @throws {ResolveError} `INTERNAL_ERROR` if the backend returns no block record or no `mediantime` for the hash.
1906
+ */
1907
+ static async mediantime(blockhash, bitcoin, cache) {
1908
+ const cached = cache.get(blockhash);
1909
+ if (cached !== void 0) {
1910
+ return cached;
1911
+ }
1912
+ const block = await bitcoin.rest.block.get({ blockhash });
1913
+ const mediantime = block?.mediantime;
1914
+ if (typeof mediantime !== "number" || !Number.isFinite(mediantime)) {
1915
+ throw new import_common10.ResolveError(
1916
+ `Block ${blockhash} has no mediantime in the block record of the Bitcoin REST backend.`,
1917
+ import_common10.INTERNAL_ERROR,
1918
+ { blockhash, block }
1919
+ );
1920
+ }
1921
+ cache.set(blockhash, mediantime);
1922
+ return mediantime;
1923
+ }
1854
1924
  /**
1855
1925
  * Traverse the full blockchain from genesis to chain top looking for beacon signals.
1856
1926
  * @param {Array<BeaconService>} beaconServices Array of BeaconService objects to search for signals.
@@ -1920,6 +1990,7 @@ var BeaconSignalDiscovery = class _BeaconSignalDiscovery {
1920
1990
  blockMetadata: {
1921
1991
  height: block.height,
1922
1992
  time: block.time,
1993
+ mediantime: block.mediantime,
1923
1994
  confirmations: block.confirmations
1924
1995
  }
1925
1996
  });
@@ -1947,1127 +2018,1127 @@ var BTCR2_UPDATE_CONTEXT = Object.freeze([
1947
2018
  function isBtcr2UpdateContext(value, expected = BTCR2_UPDATE_CONTEXT) {
1948
2019
  return Array.isArray(value) && value.length === expected.length && value.every((url, i) => url === expected[i]);
1949
2020
  }
2021
+ var DEACTIVATION_PATCH = Object.freeze({
2022
+ op: "add",
2023
+ path: "/deactivated",
2024
+ value: true
2025
+ });
1950
2026
 
1951
2027
  // src/core/did-sender-resolver.ts
1952
- var import_common14 = require("@did-btcr2/common");
1953
- var import_cryptosuite3 = require("@did-btcr2/cryptosuite");
2028
+ var import_common13 = require("@did-btcr2/common");
2029
+ var import_cryptosuite2 = require("@did-btcr2/cryptosuite");
1954
2030
  var import_keypair4 = require("@did-btcr2/keypair");
1955
2031
 
1956
2032
  // src/core/resolver.ts
1957
2033
  var import_bitcoin5 = require("@did-btcr2/bitcoin");
1958
- var import_common13 = require("@did-btcr2/common");
1959
- var import_cryptosuite2 = require("@did-btcr2/cryptosuite");
1960
- var import_keypair3 = require("@did-btcr2/keypair");
1961
-
1962
- // src/did-btcr2.ts
1963
2034
  var import_common12 = require("@did-btcr2/common");
1964
- var import_dids2 = require("@web5/dids");
2035
+ var import_cryptosuite = require("@did-btcr2/cryptosuite");
2036
+ var import_keypair3 = require("@did-btcr2/keypair");
1965
2037
 
1966
- // src/core/updater.ts
2038
+ // src/utils/error-cause.ts
1967
2039
  var import_common11 = require("@did-btcr2/common");
1968
- var import_cryptosuite = require("@did-btcr2/cryptosuite");
1969
- var Updater = class _Updater {
1970
- #state = { phase: "Construct" };
1971
- #sourceDocument;
1972
- #patches;
1973
- #sourceVersionId;
1974
- #verificationMethod;
1975
- #beaconService;
2040
+ function errorCause(error) {
2041
+ if (error instanceof import_common11.DidMethodError) return { type: error.type, message: error.message };
2042
+ if (error instanceof Error) return { type: error.name, message: error.message };
2043
+ return { type: "unknown", message: String(error) };
2044
+ }
2045
+
2046
+ // src/core/resolver.ts
2047
+ var import_utils7 = require("@noble/curves/utils.js");
2048
+ var DEFAULT_MIN_CONF = 6;
2049
+ function isRecord(value) {
2050
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2051
+ }
2052
+ function isCASAnnouncement(value) {
2053
+ return isRecord(value) && Object.values(value).every((v) => typeof v === "string");
2054
+ }
2055
+ function isSignedBTCR2Update(value) {
2056
+ if (!isRecord(value)) return false;
2057
+ return Array.isArray(value.patch) && typeof value.sourceHash === "string" && typeof value.targetHash === "string" && Number.isInteger(value.targetVersionId) && value.targetVersionId >= 2 && isRecord(value.proof);
2058
+ }
2059
+ function isSMTProof(value) {
2060
+ if (!isRecord(value)) return false;
2061
+ return typeof value.id === "string" && typeof value.collapsed === "string" && Array.isArray(value.hashes);
2062
+ }
2063
+ function validateMinConf(value) {
2064
+ if (value === void 0) return DEFAULT_MIN_CONF;
2065
+ if (typeof value === "number" && Number.isInteger(value) && value >= 1) return value;
2066
+ throw new import_common12.ResolveError(
2067
+ `Invalid resolution option minConf: expected a positive integer (minimum 1), got ${shown(value)}.`,
2068
+ import_common12.INVALID_OPTIONS,
2069
+ { minConf: value }
2070
+ );
2071
+ }
2072
+ function shown(value) {
2073
+ return typeof value === "string" ? JSON.stringify(value) : String(value);
2074
+ }
2075
+ var ASCII_INTEGER = /^-?[0-9]+$/;
2076
+ function validateVersionId(value) {
2077
+ if (value === void 0) return void 0;
2078
+ if (typeof value === "string" && ASCII_INTEGER.test(value) && Number.isSafeInteger(Number(value))) {
2079
+ return Number(value);
2080
+ }
2081
+ throw new import_common12.ResolveError(
2082
+ `Invalid resolution option versionId: expected an ASCII string of an integer, got ${shown(value)}.`,
2083
+ import_common12.INVALID_OPTIONS,
2084
+ { versionId: value }
2085
+ );
2086
+ }
2087
+ var UTC_XSD_DATETIME = /^-?\d{4,}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
2088
+ var XSD_TIMEZONE = /(Z|[+-]\d{2}:\d{2})$/;
2089
+ function validateVersionTime(value) {
2090
+ if (value === void 0) return void 0;
2091
+ if (typeof value === "string" && UTC_XSD_DATETIME.test(value) && import_common12.DateUtils.isValidXsdDateTime(value)) {
2092
+ const ms = Date.parse(value);
2093
+ if (Number.isFinite(ms)) return ms;
2094
+ }
2095
+ throw new import_common12.ResolveError(
2096
+ `Invalid resolution option versionTime: expected an XML Datetime in UTC without a fraction (for example "2026-07-01T00:00:00Z"), got ${shown(value)}.`,
2097
+ import_common12.INVALID_OPTIONS,
2098
+ { versionTime: value }
2099
+ );
2100
+ }
2101
+ var Resolver = class _Resolver {
2102
+ // --- Immutable inputs ---
2103
+ #didComponents;
2104
+ /** The parsed `ResolutionOptions.versionId`, or `undefined` when the option is absent. */
2105
+ #versionId;
2106
+ /** The parsed `ResolutionOptions.versionTime` in milliseconds since the Unix epoch, or `undefined`. */
2107
+ #versionTime;
1976
2108
  /**
1977
- * @internal Use {@link DidBtcr2.update} to create instances.
2109
+ * The specific phase the Resolver is current in.
1978
2110
  */
1979
- constructor(params) {
1980
- this.#sourceDocument = params.sourceDocument;
1981
- this.#patches = params.patches;
1982
- this.#sourceVersionId = params.sourceVersionId;
1983
- this.#verificationMethod = params.verificationMethod;
1984
- this.#beaconService = params.beaconService;
1985
- }
1986
- // ─── Public static utility methods ─────────────────────────────────────────
1987
- // Used by generate-vector.ts and other scripts that need direct access to
1988
- // individual update steps outside the state machine flow.
2111
+ #phase;
2112
+ #sidecarData;
2113
+ #currentDocument;
2114
+ #providedGenesisDocument = null;
2115
+ #beaconServicesSignals = /* @__PURE__ */ new Map();
2116
+ #processedServices = /* @__PURE__ */ new Set();
2117
+ /** The beacon addresses the resolver requested signals for: `scanned_beacons` of the specification. */
2118
+ #requestCache = /* @__PURE__ */ new Set();
1989
2119
  /**
1990
- * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/update.html#construct-btcr2-unsigned-update | 7.3.b Construct BTCR2 Unsigned Update}.
1991
- *
1992
- * @param {Btcr2DidDocument} sourceDocument The source DID document to be updated.
1993
- * @param {PatchOperation[]} patches The JSON Patch operations to apply.
1994
- * @param {number} sourceVersionId The version ID of the source document.
1995
- * @returns {UnsignedBTCR2Update} The constructed UnsignedBTCR2Update object.
1996
- * @throws {UpdateError} If the target document fails DID Core validation.
2120
+ * The tuples of the specification's `updates` list: a signed update and the metadata of
2121
+ * the block that announced it. BeaconProcess appends; ProcessUpdate sorts the list and
2122
+ * removes one tuple per step. A tuple that one pass does not reach waits for the next.
1997
2123
  */
1998
- static construct(sourceDocument, patches, sourceVersionId) {
1999
- const unsignedUpdate = {
2000
- // The array the specification pins, as a fresh copy: the update is a plain JSON
2001
- // object that callers may edit, and the shared constant is frozen.
2002
- "@context": [...BTCR2_UPDATE_CONTEXT],
2003
- patch: patches,
2004
- targetHash: "",
2005
- targetVersionId: sourceVersionId + 1,
2006
- sourceHash: (0, import_common11.canonicalHash)(sourceDocument)
2007
- };
2008
- const targetDocument = import_common11.JSONPatch.apply(sourceDocument, patches);
2009
- if (targetDocument.id !== sourceDocument.id) {
2010
- throw new import_common11.UpdateError(
2011
- `Patches must not change the DID document id (source "${sourceDocument.id}" to target "${targetDocument.id}").`,
2012
- import_common11.INVALID_DID_UPDATE,
2013
- { sourceId: sourceDocument.id, targetId: targetDocument.id }
2124
+ #unsortedUpdates = [];
2125
+ #resolvedResponse = null;
2126
+ /**
2127
+ * The state of the specification loop, carried across every pass: the version counter
2128
+ * (`current_version_id`), the update-hash history that backs duplicate confirmation
2129
+ * (`update_hash_history`), the confirmations of the block that contains the most
2130
+ * recently applied unique update (`block_confirmations`), and the header time of that
2131
+ * block as `updated`. A pass that finds a new beacon address returns to discovery, so
2132
+ * the state must not restart: a restart would reject a linear history whose later
2133
+ * updates are announced on beacons that earlier updates added.
2134
+ */
2135
+ #currentVersionId = 1;
2136
+ #updateHashHistory = [];
2137
+ #blockConfirmations = 0;
2138
+ #updated;
2139
+ /**
2140
+ * Opt-in upper bound on multi-round beacon-discovery passes. `Infinity` (the
2141
+ * default) leaves discovery unbounded; termination is already guaranteed by
2142
+ * de-duplicating already-queried beacon addresses. A positive value is a
2143
+ * caller-imposed resource guard; a non-positive value or omission means no limit.
2144
+ */
2145
+ #maxDiscoveryRounds;
2146
+ /** Count of beacon-discovery passes driven by updates adding new beacon services. */
2147
+ #discoveryRounds = 0;
2148
+ /**
2149
+ * Minimum block confirmations a Beacon Signal must have before this resolver
2150
+ * processes it: `ResolutionOptions.minConf`, default {@link DEFAULT_MIN_CONF}.
2151
+ * Applied at signal intake in the BeaconProcess phase. A signal below the
2152
+ * threshold is excluded from the resolution; the rest of the signals are
2153
+ * processed.
2154
+ */
2155
+ #minConf;
2156
+ /**
2157
+ * @internal Use {@link DidBtcr2.resolve} to create instances.
2158
+ */
2159
+ constructor(didComponents, sidecarData, currentDocument, options) {
2160
+ this.#didComponents = didComponents;
2161
+ this.#sidecarData = sidecarData;
2162
+ this.#currentDocument = currentDocument;
2163
+ if (options?.versionId !== void 0 && options?.versionTime !== void 0) {
2164
+ throw new import_common12.ResolveError(
2165
+ "Invalid resolution options: versionId and versionTime are mutually exclusive. Pass one of them.",
2166
+ import_common12.INVALID_OPTIONS,
2167
+ { versionId: options.versionId, versionTime: options.versionTime }
2014
2168
  );
2015
2169
  }
2016
- try {
2017
- DidDocument.isValid(targetDocument);
2018
- } catch (error) {
2019
- throw new import_common11.UpdateError(
2020
- "Error validating targetDocument: " + (error instanceof Error ? error.message : String(error)),
2021
- import_common11.INVALID_DID_UPDATE,
2022
- targetDocument
2023
- );
2170
+ this.#versionId = validateVersionId(options?.versionId);
2171
+ this.#versionTime = validateVersionTime(options?.versionTime);
2172
+ const rounds = options?.maxDiscoveryRounds;
2173
+ this.#maxDiscoveryRounds = typeof rounds === "number" && rounds > 0 ? rounds : Infinity;
2174
+ this.#minConf = validateMinConf(options?.minConf);
2175
+ if (options?.genesisDocument) {
2176
+ this.#providedGenesisDocument = options.genesisDocument;
2024
2177
  }
2025
- unsignedUpdate.targetHash = (0, import_common11.canonicalHash)(targetDocument);
2026
- return unsignedUpdate;
2178
+ this.#phase = currentDocument ? "BeaconDiscovery" /* BeaconDiscovery */ : "GenesisDocument" /* GenesisDocument */;
2027
2179
  }
2028
2180
  /**
2029
- * Implements subsection {@link http://dcdpr.github.io/did-btcr2/operations/update.html#construct-btcr2-signed-update | 7.3.c Construct BTCR2 Signed Update }.
2030
- *
2031
- * @param {string} did The did-btcr2 identifier to derive the root capability from.
2032
- * @param {UnsignedBTCR2Update} unsignedUpdate The unsigned update to sign.
2033
- * @param {DidVerificationMethod} verificationMethod The verification method for signing.
2034
- * @param {Signer} signer Signer that produces the BIP-340 Schnorr signature.
2035
- * @returns {SignedBTCR2Update} The signed update with a Data Integrity proof.
2181
+ * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#if-genesis_bytes-is-a-secp256k1-public-key | 7.2.d.1 if genesis bytes is a secp256k1 Public Key}.
2182
+ * @param {DidComponents} didComponents The decoded components of the did.
2183
+ * @returns {DidDocument} The resolved DID Document object.
2036
2184
  */
2037
- static sign(did, unsignedUpdate, verificationMethod, signer) {
2038
- if (!did.startsWith("did:btcr2:")) {
2039
- throw new import_common11.UpdateError(
2040
- `Expected a did:btcr2 identifier for the root capability; got "${did}".`,
2041
- import_common11.INVALID_DID_UPDATE,
2042
- { did }
2185
+ static deterministic(didComponents) {
2186
+ const genesisBytes = didComponents.genesisBytes;
2187
+ const did = Identifier.encode(genesisBytes, didComponents);
2188
+ const { multibase } = new import_keypair3.CompressedSecp256k1PublicKey(genesisBytes);
2189
+ const service = BeaconUtils.generateBeaconServices({
2190
+ id: did,
2191
+ publicKey: genesisBytes,
2192
+ network: (0, import_bitcoin5.getNetwork)(didComponents.network),
2193
+ beaconType: "SingletonBeacon"
2194
+ });
2195
+ return new DidDocument({
2196
+ id: did,
2197
+ verificationMethod: [{
2198
+ id: `${did}#initialKey`,
2199
+ type: "Multikey",
2200
+ controller: did,
2201
+ publicKeyMultibase: multibase.encoded
2202
+ }],
2203
+ service
2204
+ });
2205
+ }
2206
+ /**
2207
+ * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#if-genesis_bytes-is-a-sha-256-hash | 7.2.d.2 if genesis_bytes is a SHA-256 Hash}.
2208
+ * @param {DidComponents} didComponents BTCR2 DID components used to resolve the DID Document
2209
+ * @param {object} genesisDocument The genesis document for resolving the DID Document.
2210
+ * @returns {DidDocument} The resolved DID Document object
2211
+ * @throws {ResolveError} `INVALID_DID` if the hash of the genesis document is not the genesis bytes of the identifier
2212
+ */
2213
+ static external(didComponents, genesisDocument) {
2214
+ const genesisDocumentHash = (0, import_common12.canonicalHashBytes)(genesisDocument);
2215
+ if (!(0, import_utils7.equalBytes)(didComponents.genesisBytes, genesisDocumentHash)) {
2216
+ throw new import_common12.ResolveError(
2217
+ `Initial document mismatch: genesisBytes !== genesisDocumentHash`,
2218
+ import_common12.INVALID_DID,
2219
+ {
2220
+ genesisBytes: (0, import_common12.encode)(didComponents.genesisBytes, "hex"),
2221
+ genesisDocumentHash: (0, import_common12.encode)(genesisDocumentHash, "hex")
2222
+ }
2043
2223
  );
2044
2224
  }
2045
- const controller = verificationMethod.controller;
2046
- const hashIdx = verificationMethod.id.indexOf("#");
2047
- if (hashIdx < 0) {
2048
- throw new import_common11.UpdateError(
2049
- `Verification method id must contain a fragment (e.g. "${verificationMethod.id}#initialKey"); got "${verificationMethod.id}".`,
2050
- import_common11.INVALID_DID_UPDATE,
2051
- { verificationMethodId: verificationMethod.id }
2225
+ const did = Identifier.encode(didComponents.genesisBytes, didComponents);
2226
+ const currentDocument = JSON.parse(
2227
+ JSON.stringify(genesisDocument).replaceAll(ID_PLACEHOLDER_VALUE, did)
2228
+ );
2229
+ return new DidDocument(currentDocument);
2230
+ }
2231
+ /**
2232
+ * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#process-sidecar-data | Process Sidecar Data}
2233
+ * @param {Sidecar} sidecar The sidecar data to process.
2234
+ * @returns {SidecarData} The processed sidecar data containing maps of updates, CAS announcements, and SMT proofs.
2235
+ */
2236
+ static sidecarData(sidecar = {}) {
2237
+ const updateMap = /* @__PURE__ */ new Map();
2238
+ if (sidecar.updates?.length)
2239
+ for (const update of sidecar.updates) {
2240
+ updateMap.set((0, import_common12.canonicalHash)(update, { encoding: "hex" }), update);
2241
+ }
2242
+ const casMap = /* @__PURE__ */ new Map();
2243
+ if (sidecar.casUpdates?.length)
2244
+ for (const update of sidecar.casUpdates) {
2245
+ casMap.set((0, import_common12.canonicalHash)(update, { encoding: "hex" }), update);
2246
+ }
2247
+ const smtMap = /* @__PURE__ */ new Map();
2248
+ if (sidecar.smtProofs?.length)
2249
+ for (const proof of sidecar.smtProofs) {
2250
+ smtMap.set((0, import_common12.encode)((0, import_common12.decode)(proof.id, "base64urlnopad"), "hex"), proof);
2251
+ }
2252
+ return { updateMap, casMap, smtMap };
2253
+ }
2254
+ /**
2255
+ * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#confirm-duplicate-update | Confirm Duplicate Update}.
2256
+ * This step confirms that an update with a lower-than-expected targetVersionId is a true duplicate.
2257
+ * @param {SignedBTCR2Update} update The BTCR2 Signed Update to confirm as a duplicate.
2258
+ * @param {HashBytes[]} updateHashHistory The accumulated hash history for comparison.
2259
+ * @returns {void} Does not return a value, but throws an error if the update is not a valid duplicate.
2260
+ */
2261
+ static confirmDuplicate(update, updateHashHistory) {
2262
+ if (!Number.isInteger(update.targetVersionId) || update.targetVersionId < 2) {
2263
+ throw new import_common12.ResolveError(
2264
+ `Invalid duplicate: targetVersionId must be an integer >= 2`,
2265
+ import_common12.INVALID_DID_UPDATE,
2266
+ { targetVersionId: update.targetVersionId }
2052
2267
  );
2053
2268
  }
2054
- const id = verificationMethod.id.slice(hashIdx);
2055
- const multikey = import_cryptosuite.SchnorrMultikey.fromSigner(id, controller, signer);
2056
- const absoluteMethodId = hashIdx === 0 ? `${did}${id}` : verificationMethod.id;
2057
- const signerKey = multikey.publicKey.multibase.encoded;
2058
- if (verificationMethod.publicKeyMultibase && signerKey !== verificationMethod.publicKeyMultibase) {
2059
- throw new import_common11.UpdateError(
2060
- `Signing key does not match verification method "${verificationMethod.id}": the signer's public key differs from the method's published publicKeyMultibase.`,
2061
- import_common11.INVALID_DID_UPDATE,
2269
+ const { proof: _, ...unsignedUpdate } = update;
2270
+ const unsignedUpdateHash = (0, import_common12.canonicalHashBytes)(unsignedUpdate);
2271
+ const historicalUpdateHash = updateHashHistory[update.targetVersionId - 2];
2272
+ if (historicalUpdateHash === void 0) {
2273
+ throw new import_common12.ResolveError(
2274
+ `Invalid duplicate: no applied update in history for targetVersionId`,
2275
+ import_common12.LATE_PUBLISHING_ERROR,
2062
2276
  {
2063
- verificationMethodId: verificationMethod.id,
2064
- expected: verificationMethod.publicKeyMultibase,
2065
- actual: signerKey
2277
+ targetVersionId: update.targetVersionId,
2278
+ historyLength: updateHashHistory.length
2066
2279
  }
2067
2280
  );
2068
2281
  }
2069
- const config = {
2070
- // The proof must carry the same array as the update. The cryptosuite copies the
2071
- // document @context into the proof when the document has one, so the two arrays
2072
- // are equal by construction; this value is the fallback for a document without one.
2073
- "@context": [...BTCR2_UPDATE_CONTEXT],
2074
- cryptosuite: "bip340-jcs-2025",
2075
- type: "DataIntegrityProof",
2076
- // The proof names the signing method by absolute DID URL, even when the document
2077
- // spells that method's own id relatively: a proof travels apart from the document
2078
- // that defines the method, so a bare `#initialKey` in it resolves against nothing.
2079
- // For a document that already spells its ids absolutely this is the id unchanged,
2080
- // so proofs over such documents are byte-identical to before.
2081
- verificationMethod: absoluteMethodId,
2082
- proofPurpose: "capabilityInvocation",
2083
- capability: `urn:zcap:root:${encodeURIComponent(did)}`,
2084
- capabilityAction: "Write"
2085
- };
2086
- const diproof = multikey.toCryptosuite().toDataIntegrityProof();
2087
- return diproof.addProof(unsignedUpdate, config);
2088
- }
2089
- /**
2090
- * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/update.html#announce-did-update | 7.3.d Announce DID Update}.
2091
- * Announces a signed update to the Bitcoin blockchain via the specified beacon.
2092
- *
2093
- * @param {BeaconService} beaconService The beacon service to broadcast through.
2094
- * @param {string} did The DID being updated. Required because a beacon service
2095
- * `id` may be a relative DID URL and so cannot supply the subject.
2096
- * @param {SignedBTCR2Update} update The signed update to announce.
2097
- * @param {Signer} signer Signer that produces the ECDSA signature for the Bitcoin transaction.
2098
- * @param {BitcoinConnection} bitcoin The Bitcoin network connection.
2099
- * @param {CASBroadcastOptions} [options] Optional broadcast configuration (fee estimator,
2100
- * change address, and, for CAS beacons, a `casPublish` callback invoked before the
2101
- * transaction broadcast; other beacon types ignore `casPublish`).
2102
- * @returns {Promise<BroadcastResult>} The broadcast artifacts: the signed update, the signal
2103
- * txid, and any per-beacon-type sidecar data (CAS announcement / SMT proof).
2104
- */
2105
- static async announce(beaconService, did, update, signer, bitcoin, options) {
2106
- const beacon = BeaconFactory.establish(beaconService, did);
2107
- return beacon.broadcastSignal(update, signer, bitcoin, options);
2108
- }
2109
- // Private instance wrappers
2110
- // Delegate to the public statics with bound instance fields for cleaner
2111
- // advance/provide code.
2112
- #construct() {
2113
- return _Updater.construct(this.#sourceDocument, this.#patches, this.#sourceVersionId);
2114
- }
2115
- /**
2116
- * Advance the state machine. Returns either:
2117
- * - `{ status: 'action-required', needs }` caller must provide data via {@link provide}
2118
- * - `{ status: 'complete', result }` update is signed and broadcast
2119
- */
2120
- advance() {
2121
- while (true) {
2122
- switch (this.#state.phase) {
2123
- // Phase: Construct
2124
- // Build the unsigned update from source doc + patches. Pure, synchronous.
2125
- case "Construct": {
2126
- const unsignedUpdate = this.#construct();
2127
- this.#state = { phase: "Sign", unsignedUpdate };
2128
- continue;
2129
- }
2130
- // Phase: Sign
2131
- // Emit NeedSigningKey: the caller supplies the secret key (or a KMS signature).
2132
- case "Sign": {
2133
- return {
2134
- status: "action-required",
2135
- needs: [{
2136
- kind: "NeedSigningKey",
2137
- verificationMethodId: this.#verificationMethod.id,
2138
- unsignedUpdate: this.#state.unsignedUpdate
2139
- }]
2140
- };
2141
- }
2142
- // Phase: Fund
2143
- // Emit NeedFunding with the beacon address. The caller checks UTXOs,
2144
- // funds the address if needed, and provides to continue.
2145
- case "Fund": {
2146
- const beaconAddress = this.#beaconService.serviceEndpoint.replace("bitcoin:", "");
2147
- return {
2148
- status: "action-required",
2149
- needs: [{
2150
- kind: "NeedFunding",
2151
- beaconAddress,
2152
- beaconService: this.#beaconService
2153
- }]
2154
- };
2155
- }
2156
- // Phase: Broadcast
2157
- // Emit NeedBroadcast with the signed update + beacon service. The caller performs
2158
- // the actual on-chain announcement (or hands off to the aggregation protocol).
2159
- case "Broadcast": {
2160
- return {
2161
- status: "action-required",
2162
- needs: [{
2163
- kind: "NeedBroadcast",
2164
- beaconService: this.#beaconService,
2165
- signedUpdate: this.#state.signedUpdate,
2166
- did: this.#sourceDocument.id
2167
- }]
2168
- };
2169
- }
2170
- // Phase: Complete
2171
- case "Complete": {
2172
- return {
2173
- status: "complete",
2174
- result: { signedUpdate: this.#state.signedUpdate }
2175
- };
2282
+ if (!(0, import_utils7.equalBytes)(historicalUpdateHash, unsignedUpdateHash)) {
2283
+ throw new import_common12.ResolveError(
2284
+ `Invalid duplicate: unsigned update hash does not match historical hash`,
2285
+ import_common12.LATE_PUBLISHING_ERROR,
2286
+ {
2287
+ unsignedUpdateHash: (0, import_common12.encode)(unsignedUpdateHash, "hex"),
2288
+ historicalHash: (0, import_common12.encode)(historicalUpdateHash, "hex")
2176
2289
  }
2177
- }
2290
+ );
2178
2291
  }
2179
2292
  }
2180
- provide(need, data) {
2181
- switch (need.kind) {
2182
- case "NeedSigningKey": {
2183
- if (this.#state.phase !== "Sign") {
2184
- throw new import_common11.UpdateError(
2185
- `Cannot provide NeedSigningKey: updater phase is ${this.#state.phase}, expected Sign.`,
2186
- import_common11.INVALID_DID_UPDATE,
2187
- { phase: this.#state.phase }
2188
- );
2189
- }
2190
- if (!data) {
2191
- throw new import_common11.UpdateError(
2192
- "NeedSigningKey requires a Signer.",
2193
- import_common11.INVALID_DID_UPDATE
2194
- );
2195
- }
2196
- const unsignedUpdate = this.#state.unsignedUpdate;
2197
- const signedUpdate = _Updater.sign(
2198
- this.#sourceDocument.id,
2199
- unsignedUpdate,
2200
- this.#verificationMethod,
2201
- data
2293
+ /**
2294
+ * Decode a hash of a BTCR2 Update (`sourceHash` or `targetHash`). The specification encodes
2295
+ * both with base64url without padding.
2296
+ * @param {unknown} value The encoded hash.
2297
+ * @param {'sourceHash' | 'targetHash'} field The name of the field, for the error.
2298
+ * @returns {HashBytes} The decoded bytes.
2299
+ * @throws {ResolveError} `INVALID_DID_UPDATE` if the value is not a string or does not decode.
2300
+ */
2301
+ static decodeUpdateHash(value, field) {
2302
+ if (typeof value === "string") {
2303
+ try {
2304
+ return (0, import_common12.decode)(value, "base64urlnopad");
2305
+ } catch (error) {
2306
+ throw new import_common12.ResolveError(
2307
+ `Invalid update: ${field} does not decode as base64url: ${errorCause(error).message}`,
2308
+ import_common12.INVALID_DID_UPDATE,
2309
+ { [field]: value, cause: errorCause(error) }
2202
2310
  );
2203
- this.#state = { phase: "Fund", unsignedUpdate, signedUpdate };
2204
- break;
2205
- }
2206
- case "NeedFunding": {
2207
- if (this.#state.phase !== "Fund") {
2208
- throw new import_common11.UpdateError(
2209
- `Cannot provide NeedFunding: updater phase is ${this.#state.phase}, expected Fund.`,
2210
- import_common11.INVALID_DID_UPDATE,
2211
- { phase: this.#state.phase }
2212
- );
2213
- }
2214
- if (data !== void 0) {
2215
- const proof = data;
2216
- if (typeof proof.utxoCount !== "number" || !Number.isFinite(proof.utxoCount) || proof.utxoCount < 1) {
2217
- throw new import_common11.UpdateError(
2218
- `NeedFunding proof must have utxoCount >= 1; got ${String(proof.utxoCount)}.`,
2219
- import_common11.INVALID_DID_UPDATE,
2220
- { utxoCount: proof.utxoCount }
2221
- );
2222
- }
2223
- }
2224
- this.#state = {
2225
- phase: "Broadcast",
2226
- unsignedUpdate: this.#state.unsignedUpdate,
2227
- signedUpdate: this.#state.signedUpdate
2228
- };
2229
- break;
2230
- }
2231
- case "NeedBroadcast": {
2232
- if (this.#state.phase !== "Broadcast") {
2233
- throw new import_common11.UpdateError(
2234
- `Cannot provide NeedBroadcast: updater phase is ${this.#state.phase}, expected Broadcast.`,
2235
- import_common11.INVALID_DID_UPDATE,
2236
- { phase: this.#state.phase }
2237
- );
2238
- }
2239
- this.#state = { phase: "Complete", signedUpdate: this.#state.signedUpdate };
2240
- break;
2241
2311
  }
2242
2312
  }
2313
+ throw new import_common12.ResolveError(`Invalid update: ${field} is not a string`, import_common12.INVALID_DID_UPDATE, { [field]: value });
2243
2314
  }
2244
- };
2245
-
2246
- // src/did-btcr2.ts
2247
- var DidBtcr2 = class {
2248
- /**
2249
- * Name of the DID method, as defined in the DID BTCR2 specification
2250
- */
2251
- static methodName = "btcr2";
2252
2315
  /**
2253
- * Implements section {@link https://dcdpr.github.io/did-btcr2/operations/create.html | 7.1 Create}.
2254
- * @param {KeyBytes | DocumentBytes} genesisBytes The bytes used to create the genesis document for a did:btcr2 identifier.
2255
- * This can be either the bytes of the genesis document itself or the bytes of a key that will be used to create the genesis document.
2256
- * @param {DidCreateOptions} options Options for creating the identifier, including the idType (key or external), version, and network.
2257
- * @param {string} options.idType The type of identifier to create, either 'KEY' or 'EXTERNAL'. Defaults to 'KEY'.
2258
- * @param {number} options.version The version number of the did:btcr2 specification to use for creating the identifier. Defaults to 1.
2259
- * @param {string} options.network The Bitcoin network to use for the identifier, e.g. 'bitcoin', 'testnet', etc. Defaults to 'bitcoin'.
2260
- * @returns {Promise<string>} Promise resolving to an identifier string.
2261
- * @throws {MethodError} if any of the checks fail
2262
- * @example
2263
- * ```ts
2264
- * const genesisBytes = SchnorrKeyPair.generate().publicKey.compressed;
2265
- * const did = DidBtcr2.create(genesisBytes, { idType: 'KEY', network: 'regtest' });
2266
- * ```
2316
+ * Parse a `created` or `expires` value of an update proof. Data Integrity types both as an
2317
+ * XML Schema `dateTimeStamp`: an XML Datetime with a timezone. A value without a timezone
2318
+ * names no fixed instant, so two resolvers would read two instants; it is rejected.
2319
+ * @param {Btcr2DataIntegrityProof} proof The update proof.
2320
+ * @param {'created' | 'expires'} field The field to parse.
2321
+ * @returns {number | undefined} The instant in milliseconds since the Unix epoch, or `undefined` when the field is absent.
2322
+ * @throws {ResolveError} `INVALID_DID_UPDATE` for a value that is not an XML Datetime with a timezone.
2267
2323
  */
2268
- static create(genesisBytes, options) {
2269
- const { idType, version = 1, network = "bitcoin" } = options || {};
2270
- if (!idType) {
2271
- throw new import_common12.MethodError(
2272
- "idType is required for creating a did:btcr2 identifier",
2273
- import_common12.INVALID_DID_DOCUMENT,
2274
- options
2275
- );
2324
+ static proofInstant(proof, field) {
2325
+ const value = proof[field];
2326
+ if (value === void 0) return void 0;
2327
+ if (typeof value === "string" && XSD_TIMEZONE.test(value) && import_common12.DateUtils.isValidXsdDateTime(value)) {
2328
+ const ms = Date.parse(value);
2329
+ if (Number.isFinite(ms)) return ms;
2276
2330
  }
2277
- return Identifier.encode(genesisBytes, { idType, version, network });
2331
+ throw new import_common12.ResolveError(
2332
+ `Invalid update: proof.${field} is not an XML Datetime with a timezone`,
2333
+ import_common12.INVALID_DID_UPDATE,
2334
+ { [field]: value }
2335
+ );
2278
2336
  }
2279
2337
  /**
2280
- * Entry point for section {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html | 7.2 Resolve}.
2281
- *
2282
- * Factory method that performs pure setup and returns a {@link Resolver} state machine.
2283
- * The caller drives resolution by calling `resolver.resolve()` and `resolver.provide()`.
2284
- * Analogous to Rust's `Document::read()`.
2285
- *
2286
- * @param {string} did The did:btcr2 identifier to be resolved.
2287
- * @param {ResolutionOptions} resolutionOptions Options used during the resolution process.
2288
- * @returns {Resolver} A sans-I/O state machine the caller drives to completion.
2289
- * @example
2290
- * ```ts
2291
- * const resolver = DidBtcr2.resolve(did, { sidecar });
2292
- * let state = resolver.resolve();
2293
- * while (state.status === 'action-required') {
2294
- * for (const need of state.needs) { ... provide data ... }
2295
- * state = resolver.resolve();
2296
- * }
2297
- * const { didDocument, metadata } = state.result;
2298
- * ```
2299
- */
2300
- static resolve(did, resolutionOptions = {}) {
2301
- const didComponents = Identifier.decode(did);
2302
- const sidecarData = Resolver.sidecarData(resolutionOptions.sidecar);
2303
- const currentDocument = didComponents.hrp === import_common12.IdentifierHrp.k ? Resolver.deterministic(didComponents) : null;
2304
- return new Resolver(didComponents, sidecarData, currentDocument, {
2305
- versionId: resolutionOptions.versionId,
2306
- versionTime: resolutionOptions.versionTime,
2307
- genesisDocument: resolutionOptions.sidecar?.genesisDocument,
2308
- maxDiscoveryRounds: resolutionOptions.maxDiscoveryRounds,
2309
- minConf: resolutionOptions.minConf
2310
- });
2338
+ * Spec "Check `update.proof`": the proof time window against the block that contains the
2339
+ * Beacon Signal. `created` must not be after the header time of the block: a controller
2340
+ * signs a short time before the block, and on mainnet the header time is about one hour
2341
+ * after the `mediantime`. `expires` must not be before the block `mediantime`: it limits a
2342
+ * replay, and a single miner cannot change `mediantime`. `expires` must not be before
2343
+ * `created`. Each comparison has no tolerance.
2344
+ * @param {Btcr2DataIntegrityProof} proof The update proof.
2345
+ * @param {BlockMetadata} block The block of the Beacon Signal.
2346
+ * @throws {ResolveError} `INVALID_DID_UPDATE` if a value is outside the window.
2347
+ */
2348
+ static checkProofWindow(proof, block) {
2349
+ const created = _Resolver.proofInstant(proof, "created");
2350
+ const expires = _Resolver.proofInstant(proof, "expires");
2351
+ if (created !== void 0 && created > block.time * 1e3) {
2352
+ throw new import_common12.ResolveError(
2353
+ "Invalid update: proof.created is after the header time of the block that contains the Beacon Signal",
2354
+ import_common12.INVALID_DID_UPDATE,
2355
+ { created: proof.created, blockTime: block.time }
2356
+ );
2357
+ }
2358
+ if (expires !== void 0 && expires < block.mediantime * 1e3) {
2359
+ throw new import_common12.ResolveError(
2360
+ "Invalid update: proof.expires is before the mediantime of the block that contains the Beacon Signal",
2361
+ import_common12.INVALID_DID_UPDATE,
2362
+ { expires: proof.expires, mediantime: block.mediantime }
2363
+ );
2364
+ }
2365
+ if (created !== void 0 && expires !== void 0 && expires < created) {
2366
+ throw new import_common12.ResolveError(
2367
+ "Invalid update: proof.expires is before proof.created",
2368
+ import_common12.INVALID_DID_UPDATE,
2369
+ { created: proof.created, expires: proof.expires }
2370
+ );
2371
+ }
2311
2372
  }
2312
2373
  /**
2313
- * Entry point for section {@link https://dcdpr.github.io/did-btcr2/#update | 7.3 Update}.
2314
- *
2315
- * Factory method that validates the update parameters and returns a sans-I/O
2316
- * {@link Updater} state machine. The caller drives the updater through its
2317
- * phases (Construct -> Sign -> Broadcast -> Complete) by calling `advance()` and
2318
- * `provide()`. The method package performs **zero I/O**: signing key retrieval
2319
- * (or KMS delegation) and the on-chain broadcast are the caller's responsibility.
2320
- *
2321
- * For a fully-wired version with Bitcoin broadcast and key handling, see
2322
- * `DidMethodApi.update()` in `@did-btcr2/api`.
2323
- *
2324
- * @param params Update construction parameters.
2325
- * @param {Btcr2DidDocument} params.sourceDocument The DID document being updated.
2326
- * @param {PatchOperation[]} params.patches The JSON Patch operations to apply.
2327
- * @param {number} params.sourceVersionId The version ID before applying the update.
2328
- * @param {string} params.verificationMethodId The verification method ID to sign with.
2329
- * @param {string} params.beaconId The beacon service ID to broadcast through.
2330
- * @returns {Updater} A sans-I/O state machine for driving the update.
2331
- * @throws {UpdateError} If the verification method is not authorized, not found,
2332
- * not of type `Multikey`, or does not have a `zQ3s` publicKeyMultibase prefix.
2333
- * Also throws if the beacon service is not found.
2334
- */
2335
- static update({
2336
- sourceDocument,
2337
- patches,
2338
- sourceVersionId,
2339
- verificationMethodId,
2340
- beaconId
2341
- }) {
2342
- const authorizedMethodId = Appendix.relationshipMethodId(verificationMethodId, sourceDocument.id);
2343
- const authorized = authorizedMethodId !== void 0 && sourceDocument.capabilityInvocation?.some(
2344
- (entry) => Appendix.relationshipMethodId(entry, sourceDocument.id) === authorizedMethodId
2345
- );
2346
- if (!authorized) {
2347
- throw new import_common12.UpdateError(
2348
- "Invalid verificationMethodId: not authorized for capabilityInvocation",
2349
- import_common12.INVALID_DID_DOCUMENT,
2350
- sourceDocument
2374
+ * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#apply-update | Apply update}
2375
+ * and its step {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#check-update-proof | Check update.proof}.
2376
+ * Every failure that the specification names raises `INVALID_DID_UPDATE`. An error of the
2377
+ * cryptosuite, the multikey, the hash decoder, or the patch rides along as `data.cause`.
2378
+ * @param {DidDocument} currentDocument The current DID Document to apply the update to.
2379
+ * @param {SignedBTCR2Update} update The BTCR2 Signed Update to apply.
2380
+ * @param {BlockMetadata} block The block that contains the Beacon Signal that announced the update.
2381
+ * @returns {DidDocument} The updated DID Document after applying the update.
2382
+ * @throws {ResolveError} `INVALID_DID_UPDATE` if the update is invalid or cannot be applied.
2383
+ */
2384
+ static applyUpdate(currentDocument, update, block) {
2385
+ const currentDocumentHash = (0, import_common12.canonicalHashBytes)(currentDocument);
2386
+ const sourceHashBytes = _Resolver.decodeUpdateHash(update.sourceHash, "sourceHash");
2387
+ if (!(0, import_utils7.equalBytes)(sourceHashBytes, currentDocumentHash)) {
2388
+ throw new import_common12.ResolveError(
2389
+ `Hash mismatch: update.sourceHash !== currentDocumentHash`,
2390
+ import_common12.INVALID_DID_UPDATE,
2391
+ {
2392
+ sourceHash: update.sourceHash,
2393
+ currentDocumentHash: (0, import_common12.encode)(currentDocumentHash, "hex")
2394
+ }
2351
2395
  );
2352
2396
  }
2353
- const verificationMethod = this.getSigningMethod(sourceDocument, verificationMethodId);
2354
- if (!verificationMethod) {
2355
- throw new import_common12.UpdateError(
2356
- "Invalid verificationMethod: not found in source document",
2357
- import_common12.INVALID_DID_DOCUMENT,
2358
- { sourceDocument, verificationMethodId }
2397
+ if (!isBtcr2UpdateContext(update["@context"])) {
2398
+ throw new import_common12.ResolveError(
2399
+ "Invalid update: @context is not the array the specification pins for a BTCR2 Update",
2400
+ import_common12.INVALID_DID_UPDATE,
2401
+ { context: update["@context"], expected: [...BTCR2_UPDATE_CONTEXT] }
2359
2402
  );
2360
2403
  }
2361
- if (verificationMethod.type !== MULTIKEY_VERIFICATION_METHOD_TYPE) {
2362
- throw new import_common12.UpdateError(
2363
- `Invalid verificationMethod: verificationMethod.type must be "${MULTIKEY_VERIFICATION_METHOD_TYPE}"`,
2364
- import_common12.INVALID_DID_DOCUMENT,
2365
- verificationMethod
2404
+ if (!isBtcr2UpdateContext(update.proof?.["@context"], update["@context"])) {
2405
+ throw new import_common12.ResolveError(
2406
+ "Invalid update: proof @context does not equal the update @context",
2407
+ import_common12.INVALID_DID_UPDATE,
2408
+ { proofContext: update.proof?.["@context"], context: update["@context"] }
2366
2409
  );
2367
2410
  }
2368
- if (!verificationMethod.publicKeyMultibase?.startsWith(MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX)) {
2369
- throw new import_common12.UpdateError(
2370
- `Invalid verificationMethodId: publicKeyMultibase prefix must start with "${MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX}"`,
2371
- import_common12.INVALID_DID_DOCUMENT,
2372
- verificationMethod
2411
+ const proof = update.proof;
2412
+ const expectedFields = [
2413
+ ["type", "DataIntegrityProof"],
2414
+ ["cryptosuite", "bip340-jcs-2025"],
2415
+ ["proofPurpose", "capabilityInvocation"],
2416
+ ["capabilityAction", "Write"],
2417
+ ["capability", `urn:zcap:root:${encodeURIComponent(currentDocument.id)}`]
2418
+ ];
2419
+ for (const [field, expected] of expectedFields) {
2420
+ const actual = proof[field];
2421
+ if (actual !== expected) {
2422
+ throw new import_common12.ResolveError(
2423
+ `Invalid update: proof.${field} must equal "${expected}"`,
2424
+ import_common12.INVALID_DID_UPDATE,
2425
+ { field, expected, actual }
2426
+ );
2427
+ }
2428
+ }
2429
+ const verificationMethodId = proof.verificationMethod;
2430
+ const entry = Appendix.capabilityInvocationEntry(currentDocument, verificationMethodId);
2431
+ if (entry === void 0) {
2432
+ throw new import_common12.ResolveError(
2433
+ "Invalid update: verificationMethod is not authorized for capabilityInvocation",
2434
+ import_common12.INVALID_DID_UPDATE,
2435
+ {
2436
+ verificationMethodId,
2437
+ capabilityInvocation: currentDocument.capabilityInvocation
2438
+ }
2373
2439
  );
2374
2440
  }
2375
- const targetBeaconId = Appendix.absoluteDidUrl(beaconId, sourceDocument.id);
2376
- const beaconService = sourceDocument.service.filter((service) => targetBeaconId !== void 0 && Appendix.absoluteDidUrl(service.id, sourceDocument.id) === targetBeaconId).filter((service) => !!service).shift();
2377
- if (!beaconService) {
2378
- throw new import_common12.UpdateError(
2379
- "No beacon service found for provided beaconId",
2441
+ const vm = Appendix.verificationMethodOfEntry(currentDocument, entry);
2442
+ if (vm === void 0) {
2443
+ throw new import_common12.ResolveError(
2444
+ "Invalid update: verificationMethod is not found in the verificationMethod of the current document",
2380
2445
  import_common12.INVALID_DID_UPDATE,
2381
- { sourceDocument, beaconId }
2446
+ { verificationMethodId }
2382
2447
  );
2383
2448
  }
2384
- return new Updater({
2385
- sourceDocument,
2386
- patches,
2387
- sourceVersionId,
2388
- verificationMethod,
2389
- beaconService
2390
- });
2391
- }
2392
- /**
2393
- * Given the W3C DID Document of a `did:btcr2` identifier, return the signing verification method that will be used
2394
- * for signing messages and credentials. If given, the `methodId` parameter is used to select the
2395
- * verification method. If not given, the Identity Key's verification method with an ID fragment
2396
- * of '#initialKey' is used.
2397
- * @param {Btcr2DidDocument} didDocument The DID Document of the `did:btcr2` identifier.
2398
- * @param {string} [methodId] Optional verification method ID to be used for signing.
2399
- * @returns {DidVerificationMethod} Promise resolving to the {@link DidVerificationMethod} object used for signing.
2400
- * @throws {DidError} if the parsed did method does not match `btcr2` or signing method could not be determined.
2401
- */
2402
- static getSigningMethod(didDocument, methodId) {
2403
- methodId ??= "#initialKey";
2404
- const parsedDid = import_dids2.Did.parse(didDocument.id);
2405
- if (parsedDid && parsedDid.method !== this.methodName) {
2406
- throw new import_common12.MethodError(`Method not supported: ${parsedDid.method}`, import_common12.METHOD_NOT_SUPPORTED, { identifier: didDocument.id });
2449
+ _Resolver.checkProofWindow(proof, block);
2450
+ let verified;
2451
+ try {
2452
+ const multikey = import_cryptosuite.SchnorrMultikey.fromVerificationMethod({
2453
+ ...vm,
2454
+ id: Appendix.absoluteDidUrl(vm.id, currentDocument.id) ?? vm.id
2455
+ });
2456
+ const diProof = new import_cryptosuite.BIP340DataIntegrityProof(new import_cryptosuite.BIP340Cryptosuite(multikey));
2457
+ verified = diProof.verifyProof((0, import_common12.canonicalize)(update), "capabilityInvocation").verified;
2458
+ } catch (error) {
2459
+ throw new import_common12.ResolveError(
2460
+ `Invalid update: proof verification failed: ${errorCause(error).message}`,
2461
+ import_common12.INVALID_DID_UPDATE,
2462
+ { verificationMethodId, cause: errorCause(error) }
2463
+ );
2407
2464
  }
2408
- const targetId = Appendix.absoluteDidUrl(methodId, didDocument.id) ?? Appendix.relationshipMethodId(didDocument.assertionMethod?.[0], didDocument.id);
2409
- const verificationMethod = targetId === void 0 ? void 0 : didDocument.verificationMethod?.find(
2410
- (vm) => Appendix.absoluteDidUrl(vm.id, didDocument.id) === targetId
2411
- );
2412
- if (!(verificationMethod && verificationMethod.publicKeyMultibase)) {
2413
- throw new import_dids2.DidError(
2414
- import_dids2.DidErrorCode.InternalError,
2415
- "A verification method intended for signing could not be determined from the DID Document"
2465
+ if (!verified) {
2466
+ throw new import_common12.ResolveError("Invalid update: proof not verified", import_common12.INVALID_DID_UPDATE, { verificationMethodId });
2467
+ }
2468
+ let updatedDocument;
2469
+ try {
2470
+ updatedDocument = import_common12.JSONPatch.apply(currentDocument, update.patch, { strict: true });
2471
+ } catch (error) {
2472
+ throw new import_common12.ResolveError(
2473
+ `Invalid update: ${errorCause(error).message}`,
2474
+ import_common12.INVALID_DID_UPDATE,
2475
+ { cause: errorCause(error) }
2416
2476
  );
2417
2477
  }
2418
- return verificationMethod;
2419
- }
2420
- };
2421
-
2422
- // src/core/resolver.ts
2423
- var import_utils7 = require("@noble/curves/utils.js");
2424
- var DEFAULT_MIN_CONF = 6;
2425
- function isRecord(value) {
2426
- return typeof value === "object" && value !== null && !Array.isArray(value);
2427
- }
2428
- function isCASAnnouncement(value) {
2429
- return isRecord(value) && Object.values(value).every((v) => typeof v === "string");
2430
- }
2431
- function isSignedBTCR2Update(value) {
2432
- if (!isRecord(value)) return false;
2433
- return Array.isArray(value.patch) && typeof value.sourceHash === "string" && typeof value.targetHash === "string" && Number.isInteger(value.targetVersionId) && value.targetVersionId >= 2 && isRecord(value.proof);
2434
- }
2435
- function isSMTProof(value) {
2436
- if (!isRecord(value)) return false;
2437
- return typeof value.id === "string" && typeof value.collapsed === "string" && Array.isArray(value.hashes);
2438
- }
2439
- function validateMinConf(value) {
2440
- if (value === void 0) return DEFAULT_MIN_CONF;
2441
- if (typeof value === "number" && Number.isInteger(value) && value >= 1) return value;
2442
- const shown = typeof value === "string" ? JSON.stringify(value) : String(value);
2443
- throw new import_common13.ResolveError(
2444
- `Invalid resolution option minConf: expected a positive integer (minimum 1), got ${shown}.`,
2445
- import_common13.INVALID_OPTIONS,
2446
- { minConf: value }
2447
- );
2448
- }
2449
- var Resolver = class _Resolver {
2450
- // --- Immutable inputs ---
2451
- #didComponents;
2452
- #versionId;
2453
- #versionTime;
2454
- /**
2455
- * The specific phase the Resolver is current in.
2456
- */
2457
- #phase;
2458
- #sidecarData;
2459
- #currentDocument;
2460
- #providedGenesisDocument = null;
2461
- #beaconServicesSignals = /* @__PURE__ */ new Map();
2462
- #processedServices = /* @__PURE__ */ new Set();
2463
- #requestCache = /* @__PURE__ */ new Set();
2464
- #unsortedUpdates = [];
2465
- #resolvedResponse = null;
2466
- /**
2467
- * Monotonic DID-document version counter and the update-hash history that backs
2468
- * duplicate confirmation, both carried across the entire resolution. The spec's
2469
- * read algorithm keeps a single version counter and a single update-hash history
2470
- * for the whole signal-processing loop, re-deriving beacons from the contemporary
2471
- * document on each pass. This sans-I/O resolver splits that one loop into discovery
2472
- * rounds, so the two must persist across rounds rather than restart each pass.
2473
- * Restarting them would reject a legitimate linear history whose later updates are
2474
- * announced on beacons that earlier updates added: round two would forget it had
2475
- * already reached version two, see version three, and raise a late-publishing error.
2476
- */
2477
- #currentVersionId = 1;
2478
- #updateHashHistory = [];
2479
- /**
2480
- * Opt-in upper bound on multi-round beacon-discovery passes. `Infinity` (the
2481
- * default) leaves discovery unbounded; termination is already guaranteed by
2482
- * de-duplicating already-queried beacon addresses. A positive value is a
2483
- * caller-imposed resource guard; a non-positive value or omission means no limit.
2484
- */
2485
- #maxDiscoveryRounds;
2486
- /** Count of beacon-discovery passes driven by updates adding new beacon services. */
2487
- #discoveryRounds = 0;
2488
- /**
2489
- * Minimum block confirmations a Beacon Signal must have before this resolver
2490
- * processes it: `ResolutionOptions.minConf`, default {@link DEFAULT_MIN_CONF}.
2491
- * Applied at signal intake in the BeaconProcess phase. A signal below the
2492
- * threshold is excluded from the resolution; the rest of the signals are
2493
- * processed.
2494
- */
2495
- #minConf;
2496
- /**
2497
- * @internal Use {@link DidBtcr2.resolve} to create instances.
2498
- */
2499
- constructor(didComponents, sidecarData, currentDocument, options) {
2500
- this.#didComponents = didComponents;
2501
- this.#sidecarData = sidecarData;
2502
- this.#currentDocument = currentDocument;
2503
- this.#versionId = options?.versionId;
2504
- this.#versionTime = options?.versionTime;
2505
- const rounds = options?.maxDiscoveryRounds;
2506
- this.#maxDiscoveryRounds = typeof rounds === "number" && rounds > 0 ? rounds : Infinity;
2507
- this.#minConf = validateMinConf(options?.minConf);
2508
- if (options?.genesisDocument) {
2509
- this.#providedGenesisDocument = options.genesisDocument;
2478
+ if (updatedDocument?.id !== currentDocument.id) {
2479
+ throw new import_common12.ResolveError(
2480
+ `Invalid update: the patch changes the document id (from "${currentDocument.id}" to "${String(updatedDocument?.id)}")`,
2481
+ import_common12.INVALID_DID_UPDATE,
2482
+ { sourceId: currentDocument.id, targetId: updatedDocument?.id }
2483
+ );
2510
2484
  }
2511
- this.#phase = currentDocument ? "BeaconDiscovery" /* BeaconDiscovery */ : "GenesisDocument" /* GenesisDocument */;
2485
+ try {
2486
+ DidDocument.validate(updatedDocument);
2487
+ } catch (error) {
2488
+ throw new import_common12.ResolveError(
2489
+ `Invalid update: the patched document does not conform to DID Core: ${errorCause(error).message}`,
2490
+ import_common12.INVALID_DID_UPDATE,
2491
+ { cause: errorCause(error) }
2492
+ );
2493
+ }
2494
+ const updatedDocumentHash = (0, import_common12.canonicalHashBytes)(updatedDocument);
2495
+ const updateTargetHash = _Resolver.decodeUpdateHash(update.targetHash, "targetHash");
2496
+ if (!(0, import_utils7.equalBytes)(updateTargetHash, updatedDocumentHash)) {
2497
+ throw new import_common12.ResolveError(
2498
+ `Invalid update: update.targetHash !== updatedDocumentHash`,
2499
+ import_common12.INVALID_DID_UPDATE,
2500
+ { updateTargetHash, updatedDocumentHash }
2501
+ );
2502
+ }
2503
+ return updatedDocument;
2512
2504
  }
2513
2505
  /**
2514
- * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#if-genesis_bytes-is-a-secp256k1-public-key | 7.2.d.1 if genesis bytes is a secp256k1 Public Key}.
2515
- * @param {DidComponents} didComponents The decoded components of the did.
2516
- * @returns {DidDocument} The resolved DID Document object.
2506
+ * Advance the state machine. Returns either:
2507
+ * - `{ status: 'action-required', needs }` - caller must provide data via {@link provide}
2508
+ * - `{ status: 'resolved', result }` - resolution complete
2509
+ *
2510
+ * Analogous to Rust's `Resolver::resolve()`.
2517
2511
  */
2518
- static deterministic(didComponents) {
2519
- const genesisBytes = didComponents.genesisBytes;
2520
- const did = Identifier.encode(genesisBytes, didComponents);
2521
- const { multibase } = new import_keypair3.CompressedSecp256k1PublicKey(genesisBytes);
2522
- const service = BeaconUtils.generateBeaconServices({
2523
- id: did,
2524
- publicKey: genesisBytes,
2525
- network: (0, import_bitcoin5.getNetwork)(didComponents.network),
2526
- beaconType: "SingletonBeacon"
2527
- });
2528
- return new DidDocument({
2529
- id: did,
2530
- verificationMethod: [{
2531
- id: `${did}#initialKey`,
2532
- type: "Multikey",
2533
- controller: did,
2534
- publicKeyMultibase: multibase.encoded
2535
- }],
2536
- service
2537
- });
2512
+ resolve() {
2513
+ while (true) {
2514
+ switch (this.#phase) {
2515
+ // Phase: GenesisDocument
2516
+ // Only entered for EXTERNAL (x HRP) identifiers when genesis doc was not in sidecar.
2517
+ case "GenesisDocument" /* GenesisDocument */: {
2518
+ if (this.#providedGenesisDocument) {
2519
+ this.#currentDocument = _Resolver.external(
2520
+ this.#didComponents,
2521
+ this.#providedGenesisDocument
2522
+ );
2523
+ this.#providedGenesisDocument = null;
2524
+ this.#phase = "BeaconDiscovery" /* BeaconDiscovery */;
2525
+ continue;
2526
+ }
2527
+ const genesisHash = (0, import_common12.encode)(this.#didComponents.genesisBytes, "hex");
2528
+ return {
2529
+ status: "action-required",
2530
+ needs: [{ kind: "NeedGenesisDocument", genesisHash }]
2531
+ };
2532
+ }
2533
+ // Phase: BeaconDiscovery
2534
+ // Extract beacon services, emit NeedBeaconSignals for addresses not yet queried.
2535
+ case "BeaconDiscovery" /* BeaconDiscovery */: {
2536
+ const beaconServices = BeaconUtils.getBeaconServices(this.#currentDocument);
2537
+ const newServices = beaconServices.filter((service) => {
2538
+ const address = BeaconUtils.parseBitcoinAddress(service.serviceEndpoint);
2539
+ return !this.#requestCache.has(address);
2540
+ });
2541
+ if (newServices.length > 0) {
2542
+ for (const service of newServices) {
2543
+ const address = BeaconUtils.parseBitcoinAddress(service.serviceEndpoint);
2544
+ this.#requestCache.add(address);
2545
+ }
2546
+ return {
2547
+ status: "action-required",
2548
+ needs: [{ kind: "NeedBeaconSignals", beaconServices: newServices }]
2549
+ };
2550
+ }
2551
+ this.#phase = "BeaconProcess" /* BeaconProcess */;
2552
+ continue;
2553
+ }
2554
+ // Phase: BeaconProcess
2555
+ // Process each beacon's signals. Collect updates and data needs.
2556
+ case "BeaconProcess" /* BeaconProcess */: {
2557
+ const allNeeds = [];
2558
+ for (const [service, signals] of this.#beaconServicesSignals) {
2559
+ if (this.#processedServices.has(service.id) || !signals.length) continue;
2560
+ const eligible = this.#eligibleSignals(signals);
2561
+ if (!eligible.length) continue;
2562
+ const beacon = BeaconFactory.establish(service, this.#currentDocument.id);
2563
+ const result = beacon.processSignals(eligible, this.#sidecarData);
2564
+ if (result.needs.length > 0) {
2565
+ allNeeds.push(...result.needs);
2566
+ } else {
2567
+ this.#unsortedUpdates.push(...result.updates);
2568
+ this.#processedServices.add(service.id);
2569
+ }
2570
+ }
2571
+ if (allNeeds.length > 0) {
2572
+ return { status: "action-required", needs: allNeeds };
2573
+ }
2574
+ this.#phase = "ProcessUpdate" /* ProcessUpdate */;
2575
+ continue;
2576
+ }
2577
+ // Phase: ProcessUpdate
2578
+ // Spec "Process Next Update": one tuple per step. The phase repeats until
2579
+ // the document resolves, or until an applied update adds a beacon address
2580
+ // that the resolver did not scan (then the pass returns to BeaconDiscovery).
2581
+ case "ProcessUpdate" /* ProcessUpdate */: {
2582
+ const document = this.#currentDocument;
2583
+ if (this.#versionId !== void 0 && this.#currentVersionId === this.#versionId) {
2584
+ this.#phase = "Complete" /* Complete */;
2585
+ continue;
2586
+ }
2587
+ if (this.#unsortedUpdates.length === 0 || document.deactivated) {
2588
+ if (this.#versionId !== void 0) {
2589
+ throw new import_common12.ResolveError(
2590
+ `Version ${this.#versionId} of the DID does not exist: the history ` + (document.deactivated ? `ends with the deactivation at version ${this.#currentVersionId}.` : `ends at version ${this.#currentVersionId}.`),
2591
+ import_common12.NOT_FOUND,
2592
+ { versionId: this.#versionId, currentVersionId: this.#currentVersionId }
2593
+ );
2594
+ }
2595
+ this.#phase = "Complete" /* Complete */;
2596
+ continue;
2597
+ }
2598
+ this.#unsortedUpdates.sort(
2599
+ ([upd0, blk0], [upd1, blk1]) => upd0.targetVersionId - upd1.targetVersionId || blk0.height - blk1.height
2600
+ );
2601
+ const [update, block] = this.#unsortedUpdates.shift();
2602
+ if (update.targetVersionId <= this.#currentVersionId) {
2603
+ _Resolver.confirmDuplicate(update, this.#updateHashHistory);
2604
+ continue;
2605
+ }
2606
+ if (this.#versionTime !== void 0 && block.mediantime * 1e3 > this.#versionTime) {
2607
+ this.#phase = "Complete" /* Complete */;
2608
+ continue;
2609
+ }
2610
+ if (update.targetVersionId !== this.#currentVersionId + 1) {
2611
+ throw new import_common12.ResolveError(
2612
+ `Version Id Mismatch: targetVersionId cannot be > currentVersionId + 1`,
2613
+ import_common12.LATE_PUBLISHING_ERROR,
2614
+ {
2615
+ targetVersionId: update.targetVersionId,
2616
+ currentVersionId: this.#currentVersionId + 1
2617
+ }
2618
+ );
2619
+ }
2620
+ this.#currentDocument = _Resolver.applyUpdate(document, update, block);
2621
+ const unsignedUpdate = import_common12.JSONUtils.deleteKeys(update, ["proof"]);
2622
+ this.#updateHashHistory.push((0, import_common12.canonicalHashBytes)(unsignedUpdate));
2623
+ this.#currentVersionId++;
2624
+ this.#blockConfirmations = block.confirmations;
2625
+ this.#updated = import_common12.DateUtils.toISOStringNonFractional(import_common12.DateUtils.blocktimeToTimestamp(block.time));
2626
+ if (this.#hasUnscannedBeacons()) {
2627
+ if (++this.#discoveryRounds > this.#maxDiscoveryRounds) {
2628
+ throw new import_common12.ResolveError(
2629
+ `Exceeded the configured maximum of ${this.#maxDiscoveryRounds} beacon-discovery rounds. Raise or remove ResolutionOptions.maxDiscoveryRounds to resolve this DID.`,
2630
+ import_common12.INTERNAL_ERROR,
2631
+ { maxDiscoveryRounds: this.#maxDiscoveryRounds, discoveryRounds: this.#discoveryRounds }
2632
+ );
2633
+ }
2634
+ this.#phase = "BeaconDiscovery" /* BeaconDiscovery */;
2635
+ }
2636
+ continue;
2637
+ }
2638
+ // Phase: Complete
2639
+ // The document metadata of the specification: versionId is current_version_id,
2640
+ // confirmations is block_confirmations (0 when no update applied), deactivated
2641
+ // is the flag of the document. `updated` is present after the first apply.
2642
+ case "Complete" /* Complete */: {
2643
+ this.#resolvedResponse ??= {
2644
+ didDocument: this.#currentDocument,
2645
+ metadata: {
2646
+ versionId: `${this.#currentVersionId}`,
2647
+ confirmations: this.#blockConfirmations,
2648
+ ...this.#updated !== void 0 ? { updated: this.#updated } : {},
2649
+ deactivated: this.#currentDocument.deactivated || false
2650
+ }
2651
+ };
2652
+ return { status: "resolved", result: this.#resolvedResponse };
2653
+ }
2654
+ }
2655
+ }
2538
2656
  }
2539
2657
  /**
2540
- * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#if-genesis_bytes-is-a-sha-256-hash | 7.2.d.2 if genesis_bytes is a SHA-256 Hash}.
2541
- * @param {DidComponents} didComponents BTCR2 DID components used to resolve the DID Document
2542
- * @param {object} genesisDocument The genesis document for resolving the DID Document.
2543
- * @returns {DidDocument} The resolved DID Document object
2544
- * @throws {ResolveError} `INVALID_DID` if the hash of the genesis document is not the genesis bytes of the identifier
2658
+ * True if the current document carries a beacon service whose address the resolver
2659
+ * did not request signals for. "Find Beacon Signals" scans such an address on the
2660
+ * next pass.
2545
2661
  */
2546
- static external(didComponents, genesisDocument) {
2547
- const genesisDocumentHash = (0, import_common13.canonicalHashBytes)(genesisDocument);
2548
- if (!(0, import_utils7.equalBytes)(didComponents.genesisBytes, genesisDocumentHash)) {
2549
- throw new import_common13.ResolveError(
2550
- `Initial document mismatch: genesisBytes !== genesisDocumentHash`,
2551
- import_common13.INVALID_DID,
2552
- {
2553
- genesisBytes: (0, import_common13.encode)(didComponents.genesisBytes, "hex"),
2554
- genesisDocumentHash: (0, import_common13.encode)(genesisDocumentHash, "hex")
2555
- }
2556
- );
2557
- }
2558
- const did = Identifier.encode(didComponents.genesisBytes, didComponents);
2559
- const currentDocument = JSON.parse(
2560
- JSON.stringify(genesisDocument).replaceAll(ID_PLACEHOLDER_VALUE, did)
2662
+ #hasUnscannedBeacons() {
2663
+ return BeaconUtils.getBeaconServices(this.#currentDocument).some(
2664
+ (service) => !this.#requestCache.has(BeaconUtils.parseBitcoinAddress(service.serviceEndpoint))
2561
2665
  );
2562
- return new DidDocument(currentDocument);
2563
2666
  }
2564
2667
  /**
2565
- * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#process-sidecar-data | Process Sidecar Data}
2566
- * @param {Sidecar} sidecar The sidecar data to process.
2567
- * @returns {SidecarData} The processed sidecar data containing maps of updates, CAS announcements, and SMT proofs.
2668
+ * Return the signals of one beacon service that resolution may process: the
2669
+ * signals with at least `#minConf` confirmations. The specification removes a
2670
+ * transaction below the threshold from the set of Beacon Signals, so an
2671
+ * excluded signal emits no data need and applies no update. A signal with no
2672
+ * integer confirmation count is excluded too: that is a mempool transaction
2673
+ * from a driver that did not skip it.
2674
+ *
2675
+ * An eligible signal must carry a finite block height, block time, and block
2676
+ * median time past. A signal that passes the count but lacks them is
2677
+ * malformed. It fails fast here with a typed error, in the style of the
2678
+ * {@link provide} guards, and not later with an invalid date or a false
2679
+ * `versionTime` comparison in the ProcessUpdate phase.
2680
+ * @param {Array<BeaconSignal>} signals The signals the caller provided for one service.
2681
+ * @returns {Array<BeaconSignal>} The signals at or above the threshold, in the given order.
2682
+ * @throws {ResolveError} `INVALID_DID_UPDATE` for an eligible signal with no valid block metadata.
2568
2683
  */
2569
- static sidecarData(sidecar = {}) {
2570
- const updateMap = /* @__PURE__ */ new Map();
2571
- if (sidecar.updates?.length)
2572
- for (const update of sidecar.updates) {
2573
- updateMap.set((0, import_common13.canonicalHash)(update, { encoding: "hex" }), update);
2574
- }
2575
- const casMap = /* @__PURE__ */ new Map();
2576
- if (sidecar.casUpdates?.length)
2577
- for (const update of sidecar.casUpdates) {
2578
- casMap.set((0, import_common13.canonicalHash)(update, { encoding: "hex" }), update);
2684
+ #eligibleSignals(signals) {
2685
+ const eligible = [];
2686
+ for (const signal of signals) {
2687
+ const block = signal.blockMetadata;
2688
+ const confirmations = block?.confirmations;
2689
+ if (!Number.isInteger(confirmations) || confirmations < this.#minConf) {
2690
+ continue;
2579
2691
  }
2580
- const smtMap = /* @__PURE__ */ new Map();
2581
- if (sidecar.smtProofs?.length)
2582
- for (const proof of sidecar.smtProofs) {
2583
- smtMap.set((0, import_common13.encode)((0, import_common13.decode)(proof.id, "base64urlnopad"), "hex"), proof);
2692
+ if (!Number.isFinite(block?.height) || !Number.isFinite(block?.time) || !Number.isFinite(block?.mediantime)) {
2693
+ throw new import_common12.ResolveError(
2694
+ `Beacon signal ${signal.signalBytes} has ${confirmations} confirmations but no valid block height, block time, or block mediantime.`,
2695
+ import_common12.INVALID_DID_UPDATE,
2696
+ {
2697
+ signalBytes: signal.signalBytes,
2698
+ confirmations,
2699
+ height: block?.height,
2700
+ time: block?.time,
2701
+ mediantime: block?.mediantime
2702
+ }
2703
+ );
2584
2704
  }
2585
- return { updateMap, casMap, smtMap };
2705
+ eligible.push(signal);
2706
+ }
2707
+ return eligible;
2586
2708
  }
2587
- /**
2588
- * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#process-updates | 7.2.f Process updates Array}.
2589
- * @param {DidDocument} currentDocument The current DID Document to apply the updates to.
2590
- * @param {Array<[SignedBTCR2Update, BlockMetadata]>} unsortedUpdates The unsorted array of BTCR2 Signed Updates and their associated Block Metadata.
2591
- * @param {string} [versionTime] The optional version time to limit updates to.
2592
- * @param {string} [versionId] The optional version id to limit updates to.
2593
- * @param {{ currentVersionId: number; updateHashHistory: HashBytes[] }} [resolutionState]
2594
- * Version counter and update-hash history carried from earlier discovery rounds.
2595
- * Standalone callers omit it and start fresh at version 1 with an empty history.
2596
- * @returns {DidResolutionResponse} The updated DID Document, number of confirmations, and version id.
2597
- *
2598
- * Confirmation depth is not checked here. The BeaconProcess phase excludes a
2599
- * signal below `ResolutionOptions.minConf` before its update reaches this method,
2600
- * so every tuple here comes from a block at or above the threshold.
2601
- */
2602
- static updates(currentDocument, unsortedUpdates, versionTime, versionId, resolutionState = { currentVersionId: 1, updateHashHistory: [] }) {
2603
- let currentVersionId = resolutionState.currentVersionId;
2604
- const updateHashHistory = resolutionState.updateHashHistory;
2605
- const updates = unsortedUpdates.sort(
2606
- ([upd0, blk0], [upd1, blk1]) => upd0.targetVersionId - upd1.targetVersionId || blk0.height - blk1.height
2607
- );
2608
- const response = {
2609
- didDocument: currentDocument,
2610
- metadata: {
2611
- versionId: `${currentVersionId}`,
2612
- confirmations: 0,
2613
- deactivated: currentDocument.deactivated || false
2614
- }
2615
- };
2616
- for (const [update, block] of updates) {
2617
- const currentDocumentHash = (0, import_common13.canonicalHashBytes)(response.didDocument);
2618
- const blocktime = import_common13.DateUtils.blocktimeToTimestamp(block.time);
2619
- response.metadata.updated = import_common13.DateUtils.toISOStringNonFractional(blocktime);
2620
- response.metadata.confirmations = block.confirmations;
2621
- if (update.targetVersionId <= currentVersionId) {
2622
- this.confirmDuplicate(update, updateHashHistory);
2623
- continue;
2709
+ provide(need, data) {
2710
+ switch (need.kind) {
2711
+ case "NeedGenesisDocument": {
2712
+ if (!isRecord(data)) {
2713
+ throw new import_common12.ResolveError(
2714
+ "Provided data for NeedGenesisDocument must be a document object.",
2715
+ import_common12.INVALID_DID_UPDATE,
2716
+ { kind: need.kind }
2717
+ );
2718
+ }
2719
+ this.#providedGenesisDocument = data;
2720
+ break;
2624
2721
  }
2625
- if (versionTime) {
2626
- if (blocktime > import_common13.DateUtils.dateStringToTimestamp(versionTime)) {
2627
- return response;
2722
+ case "NeedBeaconSignals": {
2723
+ if (!(data instanceof Map)) {
2724
+ throw new import_common12.ResolveError(
2725
+ "Provided data for NeedBeaconSignals must be a Map of beacon services to signals.",
2726
+ import_common12.INVALID_DID_UPDATE,
2727
+ { kind: need.kind }
2728
+ );
2729
+ }
2730
+ for (const [service, serviceSignals] of data) {
2731
+ this.#beaconServicesSignals.set(service, serviceSignals);
2628
2732
  }
2733
+ break;
2629
2734
  }
2630
- if (update.targetVersionId === currentVersionId + 1) {
2631
- const sourceHashBytes = (0, import_common13.decode)(update.sourceHash, "base64urlnopad");
2632
- if (!(0, import_utils7.equalBytes)(sourceHashBytes, currentDocumentHash)) {
2633
- throw new import_common13.ResolveError(
2634
- `Hash mismatch: update.sourceHash !== currentDocumentHash`,
2635
- import_common13.INVALID_DID_UPDATE,
2636
- {
2637
- sourceHash: update.sourceHash,
2638
- currentDocumentHash: (0, import_common13.encode)(currentDocumentHash, "hex")
2639
- }
2735
+ case "NeedCASAnnouncement": {
2736
+ if (!isCASAnnouncement(data)) {
2737
+ throw new import_common12.ResolveError(
2738
+ "Provided data for NeedCASAnnouncement is not a CAS announcement.",
2739
+ import_common12.INVALID_DID_UPDATE,
2740
+ { kind: need.kind }
2640
2741
  );
2641
2742
  }
2642
- response.didDocument = this.applyUpdate(response.didDocument, update);
2643
- const unsignedUpdate = import_common13.JSONUtils.deleteKeys(update, ["proof"]);
2644
- updateHashHistory.push((0, import_common13.canonicalHashBytes)(unsignedUpdate));
2645
- } else {
2646
- throw new import_common13.ResolveError(
2647
- `Version Id Mismatch: targetVersionId cannot be > currentVersionId + 1`,
2648
- import_common13.LATE_PUBLISHING_ERROR,
2649
- {
2650
- targetVersionId: update.targetVersionId,
2651
- currentVersionId: currentVersionId + 1
2652
- }
2653
- );
2743
+ const announcementHash = (0, import_common12.canonicalHash)(data, { encoding: "hex" });
2744
+ if (announcementHash !== need.announcementHash) {
2745
+ throw new import_common12.ResolveError(
2746
+ `CAS announcement hash mismatch: expected ${need.announcementHash}, got ${announcementHash}.`,
2747
+ import_common12.INVALID_DID_UPDATE,
2748
+ { expected: need.announcementHash, actual: announcementHash }
2749
+ );
2750
+ }
2751
+ this.#sidecarData.casMap.set(announcementHash, data);
2752
+ break;
2654
2753
  }
2655
- currentVersionId++;
2656
- response.metadata.versionId = `${currentVersionId}`;
2657
- const versionIdNumber = Number(versionId);
2658
- if (!isNaN(versionIdNumber) && versionIdNumber <= currentVersionId) {
2659
- return response;
2754
+ case "NeedSignedUpdate": {
2755
+ if (!isSignedBTCR2Update(data)) {
2756
+ throw new import_common12.ResolveError(
2757
+ "Provided data for NeedSignedUpdate is not a signed BTCR2 update.",
2758
+ import_common12.INVALID_DID_UPDATE,
2759
+ { kind: need.kind }
2760
+ );
2761
+ }
2762
+ const updateHash = (0, import_common12.canonicalHash)(data, { encoding: "hex" });
2763
+ if (updateHash !== need.updateHash) {
2764
+ throw new import_common12.ResolveError(
2765
+ `Signed update hash mismatch: expected ${need.updateHash}, got ${updateHash}.`,
2766
+ import_common12.INVALID_DID_UPDATE,
2767
+ { expected: need.updateHash, actual: updateHash }
2768
+ );
2769
+ }
2770
+ this.#sidecarData.updateMap.set(updateHash, data);
2771
+ break;
2660
2772
  }
2661
- if (response.didDocument.deactivated) {
2662
- response.metadata.deactivated = response.didDocument.deactivated;
2663
- return response;
2773
+ case "NeedSMTProof": {
2774
+ if (!isSMTProof(data)) {
2775
+ throw new import_common12.ResolveError(
2776
+ "Provided data for NeedSMTProof is not an SMT proof.",
2777
+ import_common12.INVALID_DID_UPDATE,
2778
+ { kind: need.kind }
2779
+ );
2780
+ }
2781
+ const proofIdHex = (0, import_common12.encode)((0, import_common12.decode)(data.id, "base64urlnopad"), "hex");
2782
+ if (proofIdHex !== need.smtRootHash) {
2783
+ throw new import_common12.ResolveError(
2784
+ `SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex}`,
2785
+ import_common12.INVALID_DID_UPDATE,
2786
+ { expected: need.smtRootHash, actual: proofIdHex }
2787
+ );
2788
+ }
2789
+ this.#sidecarData.smtMap.set(need.smtRootHash, data);
2790
+ break;
2664
2791
  }
2665
2792
  }
2666
- return response;
2667
2793
  }
2794
+ };
2795
+
2796
+ // src/core/did-sender-resolver.ts
2797
+ function getAggregationCommunicationKey(document) {
2798
+ const invocation = document.capabilityInvocation?.[0];
2799
+ if (invocation === void 0) {
2800
+ throw new import_common13.DidDocumentError(
2801
+ "Cannot derive aggregation communication key: capabilityInvocation is absent",
2802
+ import_common13.INVALID_DID_DOCUMENT,
2803
+ { id: document.id }
2804
+ );
2805
+ }
2806
+ const invocationId = Appendix.absoluteDidUrl(invocation, document.id);
2807
+ const vm = typeof invocation === "string" ? invocationId === void 0 ? void 0 : document.verificationMethod?.find(
2808
+ (method) => Appendix.absoluteDidUrl(method.id, document.id) === invocationId
2809
+ ) : invocation;
2810
+ if (!vm) {
2811
+ throw new import_common13.DidDocumentError(
2812
+ `Cannot derive aggregation communication key: capabilityInvocation[0] "${invocation}" does not resolve to a verification method`,
2813
+ import_common13.INVALID_DID_DOCUMENT,
2814
+ { id: document.id, invocation }
2815
+ );
2816
+ }
2817
+ return import_cryptosuite2.SchnorrMultikey.fromVerificationMethod(vm).publicKey;
2818
+ }
2819
+ function resolveBtcr2SenderPk(did, opts) {
2820
+ try {
2821
+ const components = Identifier.decode(did);
2822
+ if (components.idType === "KEY") {
2823
+ return new import_keypair4.CompressedSecp256k1PublicKey(components.genesisBytes);
2824
+ }
2825
+ if (opts?.genesisDocument) {
2826
+ const document = Resolver.external(components, opts.genesisDocument);
2827
+ return getAggregationCommunicationKey(document);
2828
+ }
2829
+ } catch {
2830
+ }
2831
+ return void 0;
2832
+ }
2833
+
2834
+ // src/core/updater.ts
2835
+ var import_common14 = require("@did-btcr2/common");
2836
+ var import_cryptosuite3 = require("@did-btcr2/cryptosuite");
2837
+ var Updater = class _Updater {
2838
+ #state = { phase: "Construct" };
2839
+ #sourceDocument;
2840
+ #patches;
2841
+ #sourceVersionId;
2842
+ #verificationMethod;
2843
+ #beaconService;
2668
2844
  /**
2669
- * Implements subsection {@link https://dcdpr.github.io/did-btcr2/#confirm-duplicate-update | 7.2.f.1 Confirm Duplicate Update}.
2670
- * This step confirms that an update with a lower-than-expected targetVersionId is a true duplicate.
2671
- * @param {SignedBTCR2Update} update The BTCR2 Signed Update to confirm as a duplicate.
2672
- * @param {HashBytes[]} updateHashHistory The accumulated hash history for comparison.
2673
- * @returns {void} Does not return a value, but throws an error if the update is not a valid duplicate.
2845
+ * @internal Use {@link DidBtcr2.update} to create instances.
2674
2846
  */
2675
- static confirmDuplicate(update, updateHashHistory) {
2676
- if (!Number.isInteger(update.targetVersionId) || update.targetVersionId < 2) {
2677
- throw new import_common13.ResolveError(
2678
- `Invalid duplicate: targetVersionId must be an integer >= 2`,
2679
- import_common13.INVALID_DID_UPDATE,
2680
- { targetVersionId: update.targetVersionId }
2681
- );
2682
- }
2683
- const { proof: _, ...unsignedUpdate } = update;
2684
- const unsignedUpdateHash = (0, import_common13.canonicalHashBytes)(unsignedUpdate);
2685
- const historicalUpdateHash = updateHashHistory[update.targetVersionId - 2];
2686
- if (historicalUpdateHash === void 0) {
2687
- throw new import_common13.ResolveError(
2688
- `Invalid duplicate: no applied update in history for targetVersionId`,
2689
- import_common13.LATE_PUBLISHING_ERROR,
2690
- {
2691
- targetVersionId: update.targetVersionId,
2692
- historyLength: updateHashHistory.length
2693
- }
2847
+ constructor(params) {
2848
+ this.#sourceDocument = params.sourceDocument;
2849
+ this.#patches = params.patches;
2850
+ this.#sourceVersionId = params.sourceVersionId;
2851
+ this.#verificationMethod = params.verificationMethod;
2852
+ this.#beaconService = params.beaconService;
2853
+ }
2854
+ // ─── Public static utility methods ─────────────────────────────────────────
2855
+ // Used by generate-vector.ts and other scripts that need direct access to
2856
+ // individual update steps outside the state machine flow.
2857
+ /**
2858
+ * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/update.html#construct-btcr2-unsigned-update | 7.3.b Construct BTCR2 Unsigned Update}.
2859
+ *
2860
+ * @param {Btcr2DidDocument} sourceDocument The source DID document to be updated.
2861
+ * @param {PatchOperation[]} patches The JSON Patch operations to apply.
2862
+ * @param {number} sourceVersionId The version ID of the source document.
2863
+ * @returns {UnsignedBTCR2Update} The constructed UnsignedBTCR2Update object.
2864
+ * @throws {UpdateError} If the target document fails DID Core validation.
2865
+ */
2866
+ static construct(sourceDocument, patches, sourceVersionId) {
2867
+ const unsignedUpdate = {
2868
+ // The array the specification pins, as a fresh copy: the update is a plain JSON
2869
+ // object that callers may edit, and the shared constant is frozen.
2870
+ "@context": [...BTCR2_UPDATE_CONTEXT],
2871
+ patch: patches,
2872
+ targetHash: "",
2873
+ targetVersionId: sourceVersionId + 1,
2874
+ sourceHash: (0, import_common14.canonicalHash)(sourceDocument)
2875
+ };
2876
+ let targetDocument;
2877
+ try {
2878
+ targetDocument = import_common14.JSONPatch.apply(sourceDocument, patches, { strict: true });
2879
+ } catch (error) {
2880
+ throw new import_common14.UpdateError(
2881
+ `Invalid patch: ${errorCause(error).message}`,
2882
+ import_common14.INVALID_DID_UPDATE,
2883
+ { cause: errorCause(error) }
2694
2884
  );
2695
2885
  }
2696
- if (!(0, import_utils7.equalBytes)(historicalUpdateHash, unsignedUpdateHash)) {
2697
- throw new import_common13.ResolveError(
2698
- `Invalid duplicate: unsigned update hash does not match historical hash`,
2699
- import_common13.LATE_PUBLISHING_ERROR,
2700
- {
2701
- unsignedUpdateHash: (0, import_common13.encode)(unsignedUpdateHash, "hex"),
2702
- historicalHash: (0, import_common13.encode)(historicalUpdateHash, "hex")
2703
- }
2886
+ if (targetDocument.id !== sourceDocument.id) {
2887
+ throw new import_common14.UpdateError(
2888
+ `Patches must not change the DID document id (source "${sourceDocument.id}" to target "${targetDocument.id}").`,
2889
+ import_common14.INVALID_DID_UPDATE,
2890
+ { sourceId: sourceDocument.id, targetId: targetDocument.id }
2704
2891
  );
2705
2892
  }
2706
- }
2707
- /**
2708
- * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#apply-update | 7.2.f.3 Apply Update}
2709
- * and its step {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#check-update-proof | Check update.proof}.
2710
- * @param {DidDocument} currentDocument The current DID Document to apply the update to.
2711
- * @param {SignedBTCR2Update} update The BTCR2 Signed Update to apply.
2712
- * @returns {DidDocument} The updated DID Document after applying the update.
2713
- * @throws {ResolveError} If the update is invalid or cannot be applied.
2714
- */
2715
- static applyUpdate(currentDocument, update) {
2716
- if (!isBtcr2UpdateContext(update["@context"])) {
2717
- throw new import_common13.ResolveError(
2718
- "Invalid update: @context is not the array the specification pins for a BTCR2 Update",
2719
- import_common13.INVALID_DID_UPDATE,
2720
- { context: update["@context"], expected: [...BTCR2_UPDATE_CONTEXT] }
2893
+ try {
2894
+ DidDocument.isValid(targetDocument);
2895
+ } catch (error) {
2896
+ throw new import_common14.UpdateError(
2897
+ "Error validating targetDocument: " + (error instanceof Error ? error.message : String(error)),
2898
+ import_common14.INVALID_DID_UPDATE,
2899
+ targetDocument
2721
2900
  );
2722
2901
  }
2723
- if (!isBtcr2UpdateContext(update.proof?.["@context"], update["@context"])) {
2724
- throw new import_common13.ResolveError(
2725
- "Invalid update: proof @context does not equal the update @context",
2726
- import_common13.INVALID_DID_UPDATE,
2727
- { proofContext: update.proof?.["@context"], context: update["@context"] }
2902
+ unsignedUpdate.targetHash = (0, import_common14.canonicalHash)(targetDocument);
2903
+ return unsignedUpdate;
2904
+ }
2905
+ /**
2906
+ * Implements subsection {@link http://dcdpr.github.io/did-btcr2/operations/update.html#construct-btcr2-signed-update | 7.3.c Construct BTCR2 Signed Update }.
2907
+ *
2908
+ * @param {string} did The did-btcr2 identifier to derive the root capability from.
2909
+ * @param {UnsignedBTCR2Update} unsignedUpdate The unsigned update to sign.
2910
+ * @param {DidVerificationMethod} verificationMethod The verification method for signing.
2911
+ * @param {Signer} signer Signer that produces the BIP-340 Schnorr signature.
2912
+ * @returns {SignedBTCR2Update} The signed update with a Data Integrity proof.
2913
+ */
2914
+ static sign(did, unsignedUpdate, verificationMethod, signer) {
2915
+ if (!did.startsWith("did:btcr2:")) {
2916
+ throw new import_common14.UpdateError(
2917
+ `Expected a did:btcr2 identifier for the root capability; got "${did}".`,
2918
+ import_common14.INVALID_DID_UPDATE,
2919
+ { did }
2728
2920
  );
2729
2921
  }
2730
- const capabilityId = update.proof?.capability;
2731
- if (!capabilityId) {
2732
- throw new import_common13.ResolveError("No root capability found in update", import_common13.INVALID_DID_UPDATE, update);
2733
- }
2734
- const rootCapability = Appendix.dereferenceZcapId(capabilityId);
2735
- const { invocationTarget, controller: rootController } = rootCapability;
2736
- if (![invocationTarget, rootController].every((id) => id === currentDocument.id)) {
2737
- throw new import_common13.ResolveError(
2738
- "Invalid root capability",
2739
- import_common13.INVALID_DID_UPDATE,
2740
- { rootCapability, currentDocument }
2922
+ const controller = verificationMethod.controller;
2923
+ const hashIdx = verificationMethod.id.indexOf("#");
2924
+ if (hashIdx < 0) {
2925
+ throw new import_common14.UpdateError(
2926
+ `Verification method id must contain a fragment (e.g. "${verificationMethod.id}#initialKey"); got "${verificationMethod.id}".`,
2927
+ import_common14.INVALID_DID_UPDATE,
2928
+ { verificationMethodId: verificationMethod.id }
2741
2929
  );
2742
2930
  }
2743
- const verificationMethodId = update.proof?.verificationMethod;
2744
- if (!verificationMethodId) {
2745
- throw new import_common13.ResolveError("No verificationMethod found in update", import_common13.INVALID_DID_UPDATE, update);
2746
- }
2747
- const authorizedMethodId = Appendix.relationshipMethodId(verificationMethodId, currentDocument.id);
2748
- const authorized = authorizedMethodId !== void 0 && currentDocument.capabilityInvocation?.some(
2749
- (entry) => Appendix.relationshipMethodId(entry, currentDocument.id) === authorizedMethodId
2750
- );
2751
- if (!authorized) {
2752
- throw new import_common13.ResolveError(
2753
- "Invalid update: verificationMethod is not authorized for capabilityInvocation",
2754
- import_common13.INVALID_DID_UPDATE,
2931
+ const id = verificationMethod.id.slice(hashIdx);
2932
+ const multikey = import_cryptosuite3.SchnorrMultikey.fromSigner(id, controller, signer);
2933
+ const absoluteMethodId = hashIdx === 0 ? `${did}${id}` : verificationMethod.id;
2934
+ const signerKey = multikey.publicKey.multibase.encoded;
2935
+ if (verificationMethod.publicKeyMultibase && signerKey !== verificationMethod.publicKeyMultibase) {
2936
+ throw new import_common14.UpdateError(
2937
+ `Signing key does not match verification method "${verificationMethod.id}": the signer's public key differs from the method's published publicKeyMultibase.`,
2938
+ import_common14.INVALID_DID_UPDATE,
2755
2939
  {
2756
- verificationMethodId,
2757
- capabilityInvocation: currentDocument.capabilityInvocation
2940
+ verificationMethodId: verificationMethod.id,
2941
+ expected: verificationMethod.publicKeyMultibase,
2942
+ actual: signerKey
2758
2943
  }
2759
2944
  );
2760
2945
  }
2761
- const vm = DidBtcr2.getSigningMethod(currentDocument, verificationMethodId);
2762
- const multikey = import_cryptosuite2.SchnorrMultikey.fromVerificationMethod(vm);
2763
- const cryptosuite = new import_cryptosuite2.BIP340Cryptosuite(multikey);
2764
- const canonicalUpdate = (0, import_common13.canonicalize)(update);
2765
- const diProof = new import_cryptosuite2.BIP340DataIntegrityProof(cryptosuite);
2766
- const verificationResult = diProof.verifyProof(canonicalUpdate, "capabilityInvocation");
2767
- if (!verificationResult.verified) {
2768
- throw new import_common13.ResolveError(
2769
- "Invalid update: proof not verified",
2770
- import_common13.INVALID_DID_UPDATE,
2771
- verificationResult
2946
+ const config = {
2947
+ // The proof must carry the same array as the update. The cryptosuite copies the
2948
+ // document @context into the proof when the document has one, so the two arrays
2949
+ // are equal by construction; this value is the fallback for a document without one.
2950
+ "@context": [...BTCR2_UPDATE_CONTEXT],
2951
+ cryptosuite: "bip340-jcs-2025",
2952
+ type: "DataIntegrityProof",
2953
+ // The proof names the signing method by absolute DID URL, even when the document
2954
+ // spells that method's own id relatively: a proof travels apart from the document
2955
+ // that defines the method, so a bare `#initialKey` in it resolves against nothing.
2956
+ // For a document that already spells its ids absolutely this is the id unchanged,
2957
+ // so proofs over such documents are byte-identical to before.
2958
+ verificationMethod: absoluteMethodId,
2959
+ proofPurpose: "capabilityInvocation",
2960
+ capability: `urn:zcap:root:${encodeURIComponent(did)}`,
2961
+ capabilityAction: "Write"
2962
+ };
2963
+ const diproof = multikey.toCryptosuite().toDataIntegrityProof();
2964
+ const signedUpdate = diproof.addProof(unsignedUpdate, config);
2965
+ let verified;
2966
+ try {
2967
+ const verifier = import_cryptosuite3.SchnorrMultikey.fromVerificationMethod({ ...verificationMethod, id: absoluteMethodId });
2968
+ verified = verifier.toCryptosuite().toDataIntegrityProof().verifyProof((0, import_common14.canonicalize)(signedUpdate), "capabilityInvocation").verified;
2969
+ } catch (error) {
2970
+ throw new import_common14.UpdateError(
2971
+ `Invalid update: the proof does not verify with the public key of "${verificationMethod.id}": ` + errorCause(error).message,
2972
+ import_common14.INVALID_DID_UPDATE,
2973
+ { verificationMethodId: verificationMethod.id, cause: errorCause(error) }
2772
2974
  );
2773
2975
  }
2774
- const updatedDocument = import_common13.JSONPatch.apply(currentDocument, update.patch);
2775
- DidDocument.validate(updatedDocument);
2776
- const currentDocumentHash = (0, import_common13.canonicalHashBytes)(updatedDocument);
2777
- const updateTargetHash = (0, import_common13.decode)(update.targetHash);
2778
- if (!(0, import_utils7.equalBytes)(updateTargetHash, currentDocumentHash)) {
2779
- throw new import_common13.ResolveError(
2780
- `Invalid update: update.targetHash !== currentDocumentHash`,
2781
- import_common13.INVALID_DID_UPDATE,
2782
- { updateTargetHash, currentDocumentHash }
2976
+ if (!verified) {
2977
+ throw new import_common14.UpdateError(
2978
+ `Invalid update: the proof does not verify with the public key of "${verificationMethod.id}".`,
2979
+ import_common14.INVALID_DID_UPDATE,
2980
+ { verificationMethodId: verificationMethod.id }
2783
2981
  );
2784
2982
  }
2785
- return updatedDocument;
2983
+ return signedUpdate;
2786
2984
  }
2787
2985
  /**
2788
- * Advance the state machine. Returns either:
2789
- * - `{ status: 'action-required', needs }` - caller must provide data via {@link provide}
2790
- * - `{ status: 'resolved', result }` - resolution complete
2986
+ * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/update.html#announce-did-update | 7.3.d Announce DID Update}.
2987
+ * Announces a signed update to the Bitcoin blockchain via the specified beacon.
2791
2988
  *
2792
- * Analogous to Rust's `Resolver::resolve()`.
2989
+ * @param {BeaconService} beaconService The beacon service to broadcast through.
2990
+ * @param {string} did The DID being updated. Required because a beacon service
2991
+ * `id` may be a relative DID URL and so cannot supply the subject.
2992
+ * @param {SignedBTCR2Update} update The signed update to announce.
2993
+ * @param {Signer} signer Signer that produces the ECDSA signature for the Bitcoin transaction.
2994
+ * @param {BitcoinConnection} bitcoin The Bitcoin network connection.
2995
+ * @param {CASBroadcastOptions} [options] Optional broadcast configuration (fee estimator,
2996
+ * change address, and, for CAS beacons, a `casPublish` callback invoked before the
2997
+ * transaction broadcast; other beacon types ignore `casPublish`).
2998
+ * @returns {Promise<BroadcastResult>} The broadcast artifacts: the signed update, the signal
2999
+ * txid, and any per-beacon-type sidecar data (CAS announcement / SMT proof).
2793
3000
  */
2794
- resolve() {
3001
+ static async announce(beaconService, did, update, signer, bitcoin, options) {
3002
+ const beacon = BeaconFactory.establish(beaconService, did);
3003
+ return beacon.broadcastSignal(update, signer, bitcoin, options);
3004
+ }
3005
+ // Private instance wrappers
3006
+ // Delegate to the public statics with bound instance fields for cleaner
3007
+ // advance/provide code.
3008
+ #construct() {
3009
+ return _Updater.construct(this.#sourceDocument, this.#patches, this.#sourceVersionId);
3010
+ }
3011
+ /**
3012
+ * Advance the state machine. Returns either:
3013
+ * - `{ status: 'action-required', needs }` caller must provide data via {@link provide}
3014
+ * - `{ status: 'complete', result }` update is signed and broadcast
3015
+ */
3016
+ advance() {
2795
3017
  while (true) {
2796
- switch (this.#phase) {
2797
- // Phase: GenesisDocument
2798
- // Only entered for EXTERNAL (x HRP) identifiers when genesis doc was not in sidecar.
2799
- case "GenesisDocument" /* GenesisDocument */: {
2800
- if (this.#providedGenesisDocument) {
2801
- this.#currentDocument = _Resolver.external(
2802
- this.#didComponents,
2803
- this.#providedGenesisDocument
2804
- );
2805
- this.#providedGenesisDocument = null;
2806
- this.#phase = "BeaconDiscovery" /* BeaconDiscovery */;
2807
- continue;
2808
- }
2809
- const genesisHash = (0, import_common13.encode)(this.#didComponents.genesisBytes, "hex");
3018
+ switch (this.#state.phase) {
3019
+ // Phase: Construct
3020
+ // Build the unsigned update from source doc + patches. Pure, synchronous.
3021
+ case "Construct": {
3022
+ const unsignedUpdate = this.#construct();
3023
+ this.#state = { phase: "Sign", unsignedUpdate };
3024
+ continue;
3025
+ }
3026
+ // Phase: Sign
3027
+ // Emit NeedSigningKey: the caller supplies the secret key (or a KMS signature).
3028
+ case "Sign": {
2810
3029
  return {
2811
3030
  status: "action-required",
2812
- needs: [{ kind: "NeedGenesisDocument", genesisHash }]
3031
+ needs: [{
3032
+ kind: "NeedSigningKey",
3033
+ verificationMethodId: this.#verificationMethod.id,
3034
+ unsignedUpdate: this.#state.unsignedUpdate
3035
+ }]
2813
3036
  };
2814
3037
  }
2815
- // Phase: BeaconDiscovery
2816
- // Extract beacon services, emit NeedBeaconSignals for addresses not yet queried.
2817
- case "BeaconDiscovery" /* BeaconDiscovery */: {
2818
- const beaconServices = BeaconUtils.getBeaconServices(this.#currentDocument);
2819
- const newServices = beaconServices.filter((service) => {
2820
- const address = BeaconUtils.parseBitcoinAddress(service.serviceEndpoint);
2821
- return !this.#requestCache.has(address);
2822
- });
2823
- if (newServices.length > 0) {
2824
- for (const service of newServices) {
2825
- const address = BeaconUtils.parseBitcoinAddress(service.serviceEndpoint);
2826
- this.#requestCache.add(address);
2827
- }
2828
- return {
2829
- status: "action-required",
2830
- needs: [{ kind: "NeedBeaconSignals", beaconServices: newServices }]
2831
- };
2832
- }
2833
- this.#phase = "BeaconProcess" /* BeaconProcess */;
2834
- continue;
2835
- }
2836
- // Phase: BeaconProcess
2837
- // Process each beacon's signals. Collect updates and data needs.
2838
- case "BeaconProcess" /* BeaconProcess */: {
2839
- const allNeeds = [];
2840
- for (const [service, signals] of this.#beaconServicesSignals) {
2841
- if (this.#processedServices.has(service.id) || !signals.length) continue;
2842
- const eligible = this.#eligibleSignals(signals);
2843
- if (!eligible.length) continue;
2844
- const beacon = BeaconFactory.establish(service, this.#currentDocument.id);
2845
- const result = beacon.processSignals(eligible, this.#sidecarData);
2846
- if (result.needs.length > 0) {
2847
- allNeeds.push(...result.needs);
2848
- } else {
2849
- this.#unsortedUpdates.push(...result.updates);
2850
- this.#processedServices.add(service.id);
2851
- }
2852
- }
2853
- if (allNeeds.length > 0) {
2854
- return { status: "action-required", needs: allNeeds };
2855
- }
2856
- this.#phase = "ApplyUpdates" /* ApplyUpdates */;
2857
- continue;
3038
+ // Phase: Fund
3039
+ // Emit NeedFunding with the beacon address. The caller checks UTXOs,
3040
+ // funds the address if needed, and provides to continue.
3041
+ case "Fund": {
3042
+ const beaconAddress = this.#beaconService.serviceEndpoint.replace("bitcoin:", "");
3043
+ return {
3044
+ status: "action-required",
3045
+ needs: [{
3046
+ kind: "NeedFunding",
3047
+ beaconAddress,
3048
+ beaconService: this.#beaconService
3049
+ }]
3050
+ };
2858
3051
  }
2859
- // Phase: ApplyUpdates
2860
- // Apply collected updates, then check for new beacon services (multi-round).
2861
- case "ApplyUpdates" /* ApplyUpdates */: {
2862
- if (this.#unsortedUpdates.length > 0) {
2863
- this.#resolvedResponse = _Resolver.updates(
2864
- this.#currentDocument,
2865
- this.#unsortedUpdates,
2866
- this.#versionTime,
2867
- this.#versionId,
2868
- { currentVersionId: this.#currentVersionId, updateHashHistory: this.#updateHashHistory }
2869
- );
2870
- this.#currentVersionId = Number(this.#resolvedResponse.metadata.versionId);
2871
- this.#currentDocument = this.#resolvedResponse.didDocument;
2872
- this.#unsortedUpdates = [];
2873
- const beaconServices = BeaconUtils.getBeaconServices(this.#currentDocument);
2874
- const hasNewServices = beaconServices.some((service) => {
2875
- const address = BeaconUtils.parseBitcoinAddress(service.serviceEndpoint);
2876
- return !this.#requestCache.has(address);
2877
- });
2878
- if (hasNewServices) {
2879
- if (++this.#discoveryRounds > this.#maxDiscoveryRounds) {
2880
- throw new import_common13.ResolveError(
2881
- `Exceeded the configured maximum of ${this.#maxDiscoveryRounds} beacon-discovery rounds. Raise or remove ResolutionOptions.maxDiscoveryRounds to resolve this DID.`,
2882
- import_common13.INTERNAL_ERROR,
2883
- { maxDiscoveryRounds: this.#maxDiscoveryRounds, discoveryRounds: this.#discoveryRounds }
2884
- );
2885
- }
2886
- this.#phase = "BeaconDiscovery" /* BeaconDiscovery */;
2887
- continue;
2888
- }
2889
- }
2890
- this.#phase = "Complete" /* Complete */;
2891
- continue;
3052
+ // Phase: Broadcast
3053
+ // Emit NeedBroadcast with the signed update + beacon service. The caller performs
3054
+ // the actual on-chain announcement (or hands off to the aggregation protocol).
3055
+ case "Broadcast": {
3056
+ return {
3057
+ status: "action-required",
3058
+ needs: [{
3059
+ kind: "NeedBroadcast",
3060
+ beaconService: this.#beaconService,
3061
+ signedUpdate: this.#state.signedUpdate,
3062
+ did: this.#sourceDocument.id
3063
+ }]
3064
+ };
2892
3065
  }
2893
3066
  // Phase: Complete
2894
- case "Complete" /* Complete */: {
3067
+ case "Complete": {
2895
3068
  return {
2896
- status: "resolved",
2897
- // No update applied: confirmations is 0 per the specification.
2898
- result: this.#resolvedResponse ?? {
2899
- didDocument: this.#currentDocument,
2900
- metadata: {
2901
- versionId: this.#versionId ?? "1",
2902
- confirmations: 0,
2903
- deactivated: this.#currentDocument.deactivated || false
2904
- }
2905
- }
3069
+ status: "complete",
3070
+ result: { signedUpdate: this.#state.signedUpdate }
2906
3071
  };
2907
3072
  }
2908
3073
  }
2909
3074
  }
2910
3075
  }
2911
- /**
2912
- * Return the signals of one beacon service that resolution may process: the
2913
- * signals with at least `#minConf` confirmations. The specification removes a
2914
- * transaction below the threshold from the set of Beacon Signals, so an
2915
- * excluded signal emits no data need and applies no update. A signal with no
2916
- * integer confirmation count is excluded too: that is a mempool transaction
2917
- * from a driver that did not skip it.
2918
- *
2919
- * An eligible signal must carry a finite block height and block time. A
2920
- * signal that passes the count but lacks them is malformed. It fails fast
2921
- * here with a typed error, in the style of the {@link provide} guards, and
2922
- * not later with an invalid date inside {@link updates}.
2923
- * @param {Array<BeaconSignal>} signals The signals the caller provided for one service.
2924
- * @returns {Array<BeaconSignal>} The signals at or above the threshold, in the given order.
2925
- * @throws {ResolveError} `INVALID_DID_UPDATE` for an eligible signal with no valid block metadata.
2926
- */
2927
- #eligibleSignals(signals) {
2928
- const eligible = [];
2929
- for (const signal of signals) {
2930
- const block = signal.blockMetadata;
2931
- const confirmations = block?.confirmations;
2932
- if (!Number.isInteger(confirmations) || confirmations < this.#minConf) {
2933
- continue;
2934
- }
2935
- if (!Number.isFinite(block?.height) || !Number.isFinite(block?.time)) {
2936
- throw new import_common13.ResolveError(
2937
- `Beacon signal ${signal.signalBytes} has ${confirmations} confirmations but no valid block height or block time.`,
2938
- import_common13.INVALID_DID_UPDATE,
2939
- { signalBytes: signal.signalBytes, confirmations, height: block?.height, time: block?.time }
2940
- );
2941
- }
2942
- eligible.push(signal);
2943
- }
2944
- return eligible;
2945
- }
2946
3076
  provide(need, data) {
2947
3077
  switch (need.kind) {
2948
- case "NeedGenesisDocument": {
2949
- if (!isRecord(data)) {
2950
- throw new import_common13.ResolveError(
2951
- "Provided data for NeedGenesisDocument must be a document object.",
2952
- import_common13.INVALID_DID_UPDATE,
2953
- { kind: need.kind }
2954
- );
2955
- }
2956
- this.#providedGenesisDocument = data;
2957
- break;
2958
- }
2959
- case "NeedBeaconSignals": {
2960
- if (!(data instanceof Map)) {
2961
- throw new import_common13.ResolveError(
2962
- "Provided data for NeedBeaconSignals must be a Map of beacon services to signals.",
2963
- import_common13.INVALID_DID_UPDATE,
2964
- { kind: need.kind }
2965
- );
2966
- }
2967
- for (const [service, serviceSignals] of data) {
2968
- this.#beaconServicesSignals.set(service, serviceSignals);
2969
- }
2970
- break;
2971
- }
2972
- case "NeedCASAnnouncement": {
2973
- if (!isCASAnnouncement(data)) {
2974
- throw new import_common13.ResolveError(
2975
- "Provided data for NeedCASAnnouncement is not a CAS announcement.",
2976
- import_common13.INVALID_DID_UPDATE,
2977
- { kind: need.kind }
3078
+ case "NeedSigningKey": {
3079
+ if (this.#state.phase !== "Sign") {
3080
+ throw new import_common14.UpdateError(
3081
+ `Cannot provide NeedSigningKey: updater phase is ${this.#state.phase}, expected Sign.`,
3082
+ import_common14.INVALID_DID_UPDATE,
3083
+ { phase: this.#state.phase }
2978
3084
  );
2979
3085
  }
2980
- const announcementHash = (0, import_common13.canonicalHash)(data, { encoding: "hex" });
2981
- if (announcementHash !== need.announcementHash) {
2982
- throw new import_common13.ResolveError(
2983
- `CAS announcement hash mismatch: expected ${need.announcementHash}, got ${announcementHash}.`,
2984
- import_common13.INVALID_DID_UPDATE,
2985
- { expected: need.announcementHash, actual: announcementHash }
3086
+ if (!data) {
3087
+ throw new import_common14.UpdateError(
3088
+ "NeedSigningKey requires a Signer.",
3089
+ import_common14.INVALID_DID_UPDATE
2986
3090
  );
2987
3091
  }
2988
- this.#sidecarData.casMap.set(announcementHash, data);
3092
+ const unsignedUpdate = this.#state.unsignedUpdate;
3093
+ const signedUpdate = _Updater.sign(
3094
+ this.#sourceDocument.id,
3095
+ unsignedUpdate,
3096
+ this.#verificationMethod,
3097
+ data
3098
+ );
3099
+ this.#state = { phase: "Fund", unsignedUpdate, signedUpdate };
2989
3100
  break;
2990
3101
  }
2991
- case "NeedSignedUpdate": {
2992
- if (!isSignedBTCR2Update(data)) {
2993
- throw new import_common13.ResolveError(
2994
- "Provided data for NeedSignedUpdate is not a signed BTCR2 update.",
2995
- import_common13.INVALID_DID_UPDATE,
2996
- { kind: need.kind }
3102
+ case "NeedFunding": {
3103
+ if (this.#state.phase !== "Fund") {
3104
+ throw new import_common14.UpdateError(
3105
+ `Cannot provide NeedFunding: updater phase is ${this.#state.phase}, expected Fund.`,
3106
+ import_common14.INVALID_DID_UPDATE,
3107
+ { phase: this.#state.phase }
2997
3108
  );
2998
3109
  }
2999
- const updateHash = (0, import_common13.canonicalHash)(data, { encoding: "hex" });
3000
- if (updateHash !== need.updateHash) {
3001
- throw new import_common13.ResolveError(
3002
- `Signed update hash mismatch: expected ${need.updateHash}, got ${updateHash}.`,
3003
- import_common13.INVALID_DID_UPDATE,
3004
- { expected: need.updateHash, actual: updateHash }
3005
- );
3110
+ if (data !== void 0) {
3111
+ const proof = data;
3112
+ if (typeof proof.utxoCount !== "number" || !Number.isFinite(proof.utxoCount) || proof.utxoCount < 1) {
3113
+ throw new import_common14.UpdateError(
3114
+ `NeedFunding proof must have utxoCount >= 1; got ${String(proof.utxoCount)}.`,
3115
+ import_common14.INVALID_DID_UPDATE,
3116
+ { utxoCount: proof.utxoCount }
3117
+ );
3118
+ }
3006
3119
  }
3007
- this.#sidecarData.updateMap.set(updateHash, data);
3120
+ this.#state = {
3121
+ phase: "Broadcast",
3122
+ unsignedUpdate: this.#state.unsignedUpdate,
3123
+ signedUpdate: this.#state.signedUpdate
3124
+ };
3008
3125
  break;
3009
3126
  }
3010
- case "NeedSMTProof": {
3011
- if (!isSMTProof(data)) {
3012
- throw new import_common13.ResolveError(
3013
- "Provided data for NeedSMTProof is not an SMT proof.",
3014
- import_common13.INVALID_DID_UPDATE,
3015
- { kind: need.kind }
3016
- );
3017
- }
3018
- const proofIdHex = (0, import_common13.encode)((0, import_common13.decode)(data.id, "base64urlnopad"), "hex");
3019
- if (proofIdHex !== need.smtRootHash) {
3020
- throw new import_common13.ResolveError(
3021
- `SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex}`,
3022
- import_common13.INVALID_DID_UPDATE,
3023
- { expected: need.smtRootHash, actual: proofIdHex }
3127
+ case "NeedBroadcast": {
3128
+ if (this.#state.phase !== "Broadcast") {
3129
+ throw new import_common14.UpdateError(
3130
+ `Cannot provide NeedBroadcast: updater phase is ${this.#state.phase}, expected Broadcast.`,
3131
+ import_common14.INVALID_DID_UPDATE,
3132
+ { phase: this.#state.phase }
3024
3133
  );
3025
3134
  }
3026
- this.#sidecarData.smtMap.set(need.smtRootHash, data);
3135
+ this.#state = { phase: "Complete", signedUpdate: this.#state.signedUpdate };
3027
3136
  break;
3028
3137
  }
3029
3138
  }
3030
3139
  }
3031
3140
  };
3032
3141
 
3033
- // src/core/did-sender-resolver.ts
3034
- function getAggregationCommunicationKey(document) {
3035
- const invocation = document.capabilityInvocation?.[0];
3036
- if (invocation === void 0) {
3037
- throw new import_common14.DidDocumentError(
3038
- "Cannot derive aggregation communication key: capabilityInvocation is absent",
3039
- import_common14.INVALID_DID_DOCUMENT,
3040
- { id: document.id }
3041
- );
3042
- }
3043
- const invocationId = Appendix.absoluteDidUrl(invocation, document.id);
3044
- const vm = typeof invocation === "string" ? invocationId === void 0 ? void 0 : document.verificationMethod?.find(
3045
- (method) => Appendix.absoluteDidUrl(method.id, document.id) === invocationId
3046
- ) : invocation;
3047
- if (!vm) {
3048
- throw new import_common14.DidDocumentError(
3049
- `Cannot derive aggregation communication key: capabilityInvocation[0] "${invocation}" does not resolve to a verification method`,
3050
- import_common14.INVALID_DID_DOCUMENT,
3051
- { id: document.id, invocation }
3052
- );
3053
- }
3054
- return import_cryptosuite3.SchnorrMultikey.fromVerificationMethod(vm).publicKey;
3055
- }
3056
- function resolveBtcr2SenderPk(did, opts) {
3057
- try {
3058
- const components = Identifier.decode(did);
3059
- if (components.idType === "KEY") {
3060
- return new import_keypair4.CompressedSecp256k1PublicKey(components.genesisBytes);
3061
- }
3062
- if (opts?.genesisDocument) {
3063
- const document = Resolver.external(components, opts.genesisDocument);
3064
- return getAggregationCommunicationKey(document);
3065
- }
3066
- } catch {
3067
- }
3068
- return void 0;
3069
- }
3070
-
3071
3142
  // src/utils/did-document-builder.ts
3072
3143
  var import_common15 = require("@did-btcr2/common");
3073
3144
  var DidDocumentBuilder = class {
@@ -3122,6 +3193,217 @@ var DidDocumentBuilder = class {
3122
3193
  return didDocument;
3123
3194
  }
3124
3195
  };
3196
+
3197
+ // src/did-btcr2.ts
3198
+ var import_common16 = require("@did-btcr2/common");
3199
+ var import_dids2 = require("@web5/dids");
3200
+ var DidBtcr2 = class {
3201
+ /**
3202
+ * Name of the DID method, as defined in the DID BTCR2 specification
3203
+ */
3204
+ static methodName = "btcr2";
3205
+ /**
3206
+ * Implements section {@link https://dcdpr.github.io/did-btcr2/operations/create.html | 7.1 Create}.
3207
+ * @param {KeyBytes | DocumentBytes} genesisBytes The bytes used to create the genesis document for a did:btcr2 identifier.
3208
+ * This can be either the bytes of the genesis document itself or the bytes of a key that will be used to create the genesis document.
3209
+ * @param {DidCreateOptions} options Options for creating the identifier, including the idType (key or external), version, and network.
3210
+ * @param {string} options.idType The type of identifier to create, either 'KEY' or 'EXTERNAL'. Defaults to 'KEY'.
3211
+ * @param {number} options.version The version number of the did:btcr2 specification to use for creating the identifier. Defaults to 1.
3212
+ * @param {string} options.network The Bitcoin network to use for the identifier, e.g. 'bitcoin', 'testnet', etc. Defaults to 'bitcoin'.
3213
+ * @returns {Promise<string>} Promise resolving to an identifier string.
3214
+ * @throws {MethodError} if any of the checks fail
3215
+ * @example
3216
+ * ```ts
3217
+ * const genesisBytes = SchnorrKeyPair.generate().publicKey.compressed;
3218
+ * const did = DidBtcr2.create(genesisBytes, { idType: 'KEY', network: 'regtest' });
3219
+ * ```
3220
+ */
3221
+ static create(genesisBytes, options) {
3222
+ const { idType, version = 1, network = "bitcoin" } = options || {};
3223
+ if (!idType) {
3224
+ throw new import_common16.MethodError(
3225
+ "idType is required for creating a did:btcr2 identifier",
3226
+ import_common16.INVALID_DID_DOCUMENT,
3227
+ options
3228
+ );
3229
+ }
3230
+ return Identifier.encode(genesisBytes, { idType, version, network });
3231
+ }
3232
+ /**
3233
+ * Entry point for section {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html | 7.2 Resolve}.
3234
+ *
3235
+ * Factory method that performs pure setup and returns a {@link Resolver} state machine.
3236
+ * The caller drives resolution by calling `resolver.resolve()` and `resolver.provide()`.
3237
+ * Analogous to Rust's `Document::read()`.
3238
+ *
3239
+ * @param {string} did The did:btcr2 identifier to be resolved.
3240
+ * @param {ResolutionOptions} resolutionOptions Options used during the resolution process.
3241
+ * @returns {Resolver} A sans-I/O state machine the caller drives to completion.
3242
+ * @example
3243
+ * ```ts
3244
+ * const resolver = DidBtcr2.resolve(did, { sidecar });
3245
+ * let state = resolver.resolve();
3246
+ * while (state.status === 'action-required') {
3247
+ * for (const need of state.needs) { ... provide data ... }
3248
+ * state = resolver.resolve();
3249
+ * }
3250
+ * const { didDocument, metadata } = state.result;
3251
+ * ```
3252
+ */
3253
+ static resolve(did, resolutionOptions = {}) {
3254
+ const didComponents = Identifier.decode(did);
3255
+ const sidecarData = Resolver.sidecarData(resolutionOptions.sidecar);
3256
+ const currentDocument = didComponents.hrp === import_common16.IdentifierHrp.k ? Resolver.deterministic(didComponents) : null;
3257
+ return new Resolver(didComponents, sidecarData, currentDocument, {
3258
+ versionId: resolutionOptions.versionId,
3259
+ versionTime: resolutionOptions.versionTime,
3260
+ genesisDocument: resolutionOptions.sidecar?.genesisDocument,
3261
+ maxDiscoveryRounds: resolutionOptions.maxDiscoveryRounds,
3262
+ minConf: resolutionOptions.minConf
3263
+ });
3264
+ }
3265
+ /**
3266
+ * Entry point for section {@link https://dcdpr.github.io/did-btcr2/#update | 7.3 Update}.
3267
+ *
3268
+ * Factory method that validates the update parameters and returns a sans-I/O
3269
+ * {@link Updater} state machine. The caller drives the updater through its
3270
+ * phases (Construct -> Sign -> Broadcast -> Complete) by calling `advance()` and
3271
+ * `provide()`. The method package performs **zero I/O**: signing key retrieval
3272
+ * (or KMS delegation) and the on-chain broadcast are the caller's responsibility.
3273
+ *
3274
+ * For a fully-wired version with Bitcoin broadcast and key handling, see
3275
+ * `DidMethodApi.update()` in `@did-btcr2/api`.
3276
+ *
3277
+ * @param params Update construction parameters.
3278
+ * @param {Btcr2DidDocument} params.sourceDocument The DID document being updated.
3279
+ * @param {PatchOperation[]} params.patches The JSON Patch operations to apply.
3280
+ * @param {number} params.sourceVersionId The version ID before applying the update.
3281
+ * @param {string} params.verificationMethodId The verification method ID to sign with.
3282
+ * @param {string} params.beaconId The beacon service ID to broadcast through.
3283
+ * @returns {Updater} A sans-I/O state machine for driving the update.
3284
+ * @throws {UpdateError} `INVALID_DID_UPDATE` if `sourceVersionId` is not an integer of at
3285
+ * least 1, if no entry of `capabilityInvocation` identifies the verification method, if a
3286
+ * reference entry names no member of `verificationMethod`, or if the beacon service is not
3287
+ * found. `INVALID_DID_DOCUMENT` if the method is not of type `Multikey` or does not have a
3288
+ * `zQ3s` publicKeyMultibase prefix.
3289
+ */
3290
+ static update({
3291
+ sourceDocument,
3292
+ patches,
3293
+ sourceVersionId,
3294
+ verificationMethodId,
3295
+ beaconId
3296
+ }) {
3297
+ if (!Number.isInteger(sourceVersionId) || sourceVersionId < 1) {
3298
+ throw new import_common16.UpdateError(
3299
+ `Invalid sourceVersionId: expected an integer of at least 1, got ${String(sourceVersionId)}.`,
3300
+ import_common16.INVALID_DID_UPDATE,
3301
+ { sourceVersionId }
3302
+ );
3303
+ }
3304
+ const entry = Appendix.capabilityInvocationEntry(sourceDocument, verificationMethodId);
3305
+ if (entry === void 0) {
3306
+ throw new import_common16.UpdateError(
3307
+ "Invalid verificationMethodId: not authorized for capabilityInvocation",
3308
+ import_common16.INVALID_DID_UPDATE,
3309
+ { verificationMethodId, capabilityInvocation: sourceDocument.capabilityInvocation }
3310
+ );
3311
+ }
3312
+ const verificationMethod = Appendix.verificationMethodOfEntry(sourceDocument, entry);
3313
+ if (!verificationMethod) {
3314
+ throw new import_common16.UpdateError(
3315
+ "Invalid verificationMethodId: not found in source document",
3316
+ import_common16.INVALID_DID_UPDATE,
3317
+ { verificationMethodId }
3318
+ );
3319
+ }
3320
+ if (verificationMethod.type !== MULTIKEY_VERIFICATION_METHOD_TYPE) {
3321
+ throw new import_common16.UpdateError(
3322
+ `Invalid verificationMethod: verificationMethod.type must be "${MULTIKEY_VERIFICATION_METHOD_TYPE}"`,
3323
+ import_common16.INVALID_DID_DOCUMENT,
3324
+ verificationMethod
3325
+ );
3326
+ }
3327
+ if (!verificationMethod.publicKeyMultibase?.startsWith(MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX)) {
3328
+ throw new import_common16.UpdateError(
3329
+ `Invalid verificationMethodId: publicKeyMultibase prefix must start with "${MULTIKEY_PUBLIC_KEY_MULTIBASE_PREFIX}"`,
3330
+ import_common16.INVALID_DID_DOCUMENT,
3331
+ verificationMethod
3332
+ );
3333
+ }
3334
+ const targetBeaconId = Appendix.absoluteDidUrl(beaconId, sourceDocument.id);
3335
+ const beaconService = sourceDocument.service.filter((service) => targetBeaconId !== void 0 && Appendix.absoluteDidUrl(service.id, sourceDocument.id) === targetBeaconId).filter((service) => !!service).shift();
3336
+ if (!beaconService) {
3337
+ throw new import_common16.UpdateError(
3338
+ "No beacon service found for provided beaconId",
3339
+ import_common16.INVALID_DID_UPDATE,
3340
+ { sourceDocument, beaconId }
3341
+ );
3342
+ }
3343
+ return new Updater({
3344
+ sourceDocument,
3345
+ patches,
3346
+ sourceVersionId,
3347
+ verificationMethod,
3348
+ beaconService
3349
+ });
3350
+ }
3351
+ /**
3352
+ * Entry point for section {@link https://dcdpr.github.io/did-btcr2/operations/deactivate.html | 7.4 Deactivate}.
3353
+ *
3354
+ * Deactivate is the Update operation with the predetermined patch {@link DEACTIVATION_PATCH}:
3355
+ * it adds the `deactivated` property with the value `true`. The factory returns the
3356
+ * {@link Updater} that {@link DidBtcr2.update} returns for that patch, and the caller drives
3357
+ * it in the same way. Resolution stops at the deactivation for good. The factory does not
3358
+ * refuse a source document that is deactivated already; the api does (ADR 100).
3359
+ *
3360
+ * @param params Deactivation parameters: the parameters of {@link DidBtcr2.update} without `patches`.
3361
+ * @returns {Updater} A sans-I/O state machine for driving the deactivation.
3362
+ * @throws {UpdateError} As {@link DidBtcr2.update}.
3363
+ */
3364
+ static deactivate({
3365
+ sourceDocument,
3366
+ sourceVersionId,
3367
+ verificationMethodId,
3368
+ beaconId
3369
+ }) {
3370
+ return this.update({
3371
+ sourceDocument,
3372
+ patches: [{ ...DEACTIVATION_PATCH }],
3373
+ sourceVersionId,
3374
+ verificationMethodId,
3375
+ beaconId
3376
+ });
3377
+ }
3378
+ /**
3379
+ * Given the W3C DID Document of a `did:btcr2` identifier, return the signing verification method that will be used
3380
+ * for signing messages and credentials. If given, the `methodId` parameter is used to select the
3381
+ * verification method. If not given, the Identity Key's verification method with an ID fragment
3382
+ * of '#initialKey' is used.
3383
+ * @param {Btcr2DidDocument} didDocument The DID Document of the `did:btcr2` identifier.
3384
+ * @param {string} [methodId] Optional verification method ID to be used for signing.
3385
+ * @returns {DidVerificationMethod} Promise resolving to the {@link DidVerificationMethod} object used for signing.
3386
+ * @throws {DidError} if the parsed did method does not match `btcr2` or signing method could not be determined.
3387
+ */
3388
+ static getSigningMethod(didDocument, methodId) {
3389
+ methodId ??= "#initialKey";
3390
+ const parsedDid = import_dids2.Did.parse(didDocument.id);
3391
+ if (parsedDid && parsedDid.method !== this.methodName) {
3392
+ throw new import_common16.MethodError(`Method not supported: ${parsedDid.method}`, import_common16.METHOD_NOT_SUPPORTED, { identifier: didDocument.id });
3393
+ }
3394
+ const targetId = Appendix.absoluteDidUrl(methodId, didDocument.id) ?? Appendix.relationshipMethodId(didDocument.assertionMethod?.[0], didDocument.id);
3395
+ const verificationMethod = targetId === void 0 ? void 0 : Appendix.getVerificationMethods(didDocument).find(
3396
+ (vm) => Appendix.absoluteDidUrl(vm.id, didDocument.id) === targetId
3397
+ );
3398
+ if (!(verificationMethod && verificationMethod.publicKeyMultibase)) {
3399
+ throw new import_dids2.DidError(
3400
+ import_dids2.DidErrorCode.InternalError,
3401
+ "A verification method intended for signing could not be determined from the DID Document"
3402
+ );
3403
+ }
3404
+ return verificationMethod;
3405
+ }
3406
+ };
3125
3407
  // Annotate the CommonJS export names for ESM import in node:
3126
3408
  0 && (module.exports = {
3127
3409
  AggregateBeaconError,
@@ -3136,6 +3418,7 @@ var DidDocumentBuilder = class {
3136
3418
  CASBeacon,
3137
3419
  CASBeaconError,
3138
3420
  CHANGE_OUTPUT_VBYTES,
3421
+ DEACTIVATION_PATCH,
3139
3422
  DEFAULT_FEE_ESTIMATOR,
3140
3423
  DEFAULT_MIN_CONF,
3141
3424
  DID_REGEX,