@did-btcr2/method 0.50.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/index.js CHANGED
@@ -61,6 +61,7 @@ __export(index_exports, {
61
61
  deriveSingletonAddress: () => deriveSingletonAddress,
62
62
  detectSingletonScriptKind: () => detectSingletonScriptKind,
63
63
  extractOpReturnSignal: () => extractOpReturnSignal,
64
+ getAggregationCommunicationKey: () => getAggregationCommunicationKey,
64
65
  isMultikeyVerificationMethod: () => isMultikeyVerificationMethod,
65
66
  opReturnScript: () => opReturnScript,
66
67
  resolveBtcr2SenderPk: () => resolveBtcr2SenderPk,
@@ -1285,23 +1286,15 @@ var BeaconSignalDiscovery = class {
1285
1286
  };
1286
1287
 
1287
1288
  // src/core/did-sender-resolver.ts
1288
- var import_keypair2 = require("@did-btcr2/keypair");
1289
- function resolveBtcr2SenderPk(did) {
1290
- try {
1291
- const components = Identifier.decode(did);
1292
- if (components.idType === "KEY") {
1293
- return new import_keypair2.CompressedSecp256k1PublicKey(components.genesisBytes);
1294
- }
1295
- } catch {
1296
- }
1297
- return void 0;
1298
- }
1289
+ var import_common13 = require("@did-btcr2/common");
1290
+ var import_cryptosuite3 = require("@did-btcr2/cryptosuite");
1291
+ var import_keypair4 = require("@did-btcr2/keypair");
1299
1292
 
1300
1293
  // src/core/resolver.ts
1301
1294
  var import_bitcoin5 = require("@did-btcr2/bitcoin");
1302
1295
  var import_common12 = require("@did-btcr2/common");
1303
1296
  var import_cryptosuite2 = require("@did-btcr2/cryptosuite");
1304
- var import_keypair4 = require("@did-btcr2/keypair");
1297
+ var import_keypair3 = require("@did-btcr2/keypair");
1305
1298
 
1306
1299
  // src/did-btcr2.ts
1307
1300
  var import_common11 = require("@did-btcr2/common");
@@ -1314,7 +1307,7 @@ var import_cryptosuite = require("@did-btcr2/cryptosuite");
1314
1307
  // src/utils/did-document.ts
1315
1308
  var import_bitcoin4 = require("@did-btcr2/bitcoin");
1316
1309
  var import_common9 = require("@did-btcr2/common");
1317
- var import_keypair3 = require("@did-btcr2/keypair");
1310
+ var import_keypair2 = require("@did-btcr2/keypair");
1318
1311
  var import_utils4 = require("@web5/dids/utils");
1319
1312
  var import_btc_signer3 = require("@scure/btc-signer");
1320
1313
  var BTCR2_DID_DOCUMENT_CONTEXT = [
@@ -1652,7 +1645,7 @@ var GenesisDocument = class _GenesisDocument extends DidDocument {
1652
1645
  * @returns {GenesisDocument} A new GenesisDocument with the placeholder ID.
1653
1646
  */
1654
1647
  static fromPublicKey(publicKey, network) {
1655
- const pk = new import_keypair3.CompressedSecp256k1PublicKey(publicKey);
1648
+ const pk = new import_keypair2.CompressedSecp256k1PublicKey(publicKey);
1656
1649
  const id = ID_PLACEHOLDER_VALUE;
1657
1650
  const address = (0, import_btc_signer3.p2pkh)(pk.compressed, (0, import_bitcoin4.getNetwork)(network)).address;
1658
1651
  const services = [{
@@ -2212,7 +2205,7 @@ var Resolver = class _Resolver {
2212
2205
  static deterministic(didComponents) {
2213
2206
  const genesisBytes = didComponents.genesisBytes;
2214
2207
  const did = Identifier.encode(genesisBytes, didComponents);
2215
- const { multibase } = new import_keypair4.CompressedSecp256k1PublicKey(genesisBytes);
2208
+ const { multibase } = new import_keypair3.CompressedSecp256k1PublicKey(genesisBytes);
2216
2209
  const service = BeaconUtils.generateBeaconServices({
2217
2210
  id: did,
2218
2211
  publicKey: genesisBytes,
@@ -2635,13 +2628,48 @@ var Resolver = class _Resolver {
2635
2628
  }
2636
2629
  };
2637
2630
 
2631
+ // src/core/did-sender-resolver.ts
2632
+ function getAggregationCommunicationKey(document) {
2633
+ const invocation = document.capabilityInvocation?.[0];
2634
+ if (invocation === void 0) {
2635
+ throw new import_common13.DidDocumentError(
2636
+ "Cannot derive aggregation communication key: capabilityInvocation is absent",
2637
+ import_common13.INVALID_DID_DOCUMENT,
2638
+ { id: document.id }
2639
+ );
2640
+ }
2641
+ const vm = typeof invocation === "string" ? document.verificationMethod?.find((method) => method.id === invocation) : invocation;
2642
+ if (!vm) {
2643
+ throw new import_common13.DidDocumentError(
2644
+ `Cannot derive aggregation communication key: capabilityInvocation[0] "${invocation}" does not resolve to a verification method`,
2645
+ import_common13.INVALID_DID_DOCUMENT,
2646
+ { id: document.id, invocation }
2647
+ );
2648
+ }
2649
+ return import_cryptosuite3.SchnorrMultikey.fromVerificationMethod(vm).publicKey;
2650
+ }
2651
+ function resolveBtcr2SenderPk(did, opts) {
2652
+ try {
2653
+ const components = Identifier.decode(did);
2654
+ if (components.idType === "KEY") {
2655
+ return new import_keypair4.CompressedSecp256k1PublicKey(components.genesisBytes);
2656
+ }
2657
+ if (opts?.genesisDocument) {
2658
+ const document = Resolver.external(components, opts.genesisDocument);
2659
+ return getAggregationCommunicationKey(document);
2660
+ }
2661
+ } catch {
2662
+ }
2663
+ return void 0;
2664
+ }
2665
+
2638
2666
  // src/utils/did-document-builder.ts
2639
- var import_common13 = require("@did-btcr2/common");
2667
+ var import_common14 = require("@did-btcr2/common");
2640
2668
  var DidDocumentBuilder = class {
2641
2669
  document = {};
2642
2670
  constructor(initialDocument) {
2643
2671
  if (!initialDocument.id) {
2644
- throw new import_common13.DidDocumentError('Missing required "id" property', import_common13.INVALID_DID_DOCUMENT, initialDocument);
2672
+ throw new import_common14.DidDocumentError('Missing required "id" property', import_common14.INVALID_DID_DOCUMENT, initialDocument);
2645
2673
  }
2646
2674
  this.document.id = initialDocument.id;
2647
2675
  this.document.verificationMethod = initialDocument.verificationMethod ?? [];
@@ -2732,6 +2760,7 @@ var DidDocumentBuilder = class {
2732
2760
  deriveSingletonAddress,
2733
2761
  detectSingletonScriptKind,
2734
2762
  extractOpReturnSignal,
2763
+ getAggregationCommunicationKey,
2735
2764
  isMultikeyVerificationMethod,
2736
2765
  opReturnScript,
2737
2766
  resolveBtcr2SenderPk,
@@ -1,25 +1,90 @@
1
+ import { DidDocumentError, INVALID_DID_DOCUMENT } from '@did-btcr2/common';
2
+ import { SchnorrMultikey } from '@did-btcr2/cryptosuite';
1
3
  import { CompressedSecp256k1PublicKey } from '@did-btcr2/keypair';
2
4
  import { Identifier } from './identifier.js';
5
+ import { Resolver } from './resolver.js';
6
+ /**
7
+ * Derive the aggregation communication public key from a resolved did:btcr2 DID Document.
8
+ *
9
+ * The communication key is the verification method referenced by
10
+ * `capabilityInvocation[0]`, resolved to its public key. This is the exact relationship
11
+ * the method already enforces for DID updates (construct and sign require the signing
12
+ * method to be in `capabilityInvocation`, the update proof is built and verified with
13
+ * `proofPurpose: 'capabilityInvocation'`), so binding the transport communication key to
14
+ * it yields the invariant "transport-authenticated as D implies authorized to update D."
15
+ * For a KEY (`k1`) document the single deterministic key is already the sole
16
+ * `capabilityInvocation` entry, so this is a no-op for KEY DIDs.
17
+ *
18
+ * There is deliberately no `verificationMethod[0]` fallback: a document without
19
+ * `capabilityInvocation` cannot be updated at all, so it is useless for aggregation and is
20
+ * rejected here rather than bound to an unrelated key.
21
+ *
22
+ * @param {DidDocument} document The resolved DID Document (placeholder id already replaced).
23
+ * @returns {CompressedSecp256k1PublicKey} The compressed public key of the communication method.
24
+ * @throws {DidDocumentError} If `capabilityInvocation` is absent or its first entry does not
25
+ * resolve to a verification method in the document.
26
+ */
27
+ export function getAggregationCommunicationKey(document) {
28
+ const invocation = document.capabilityInvocation?.[0];
29
+ if (invocation === undefined) {
30
+ throw new DidDocumentError('Cannot derive aggregation communication key: capabilityInvocation is absent', INVALID_DID_DOCUMENT, { id: document.id });
31
+ }
32
+ // Resolve capabilityInvocation[0] to a verification method: a string reference is
33
+ // dereferenced by id against verificationMethod; an embedded method is used directly.
34
+ // A local id lookup is used rather than getSigningMethod, which defaults to #initialKey
35
+ // and does not resolve embedded methods.
36
+ const vm = typeof invocation === 'string'
37
+ ? document.verificationMethod?.find(method => method.id === invocation)
38
+ : invocation;
39
+ if (!vm) {
40
+ throw new DidDocumentError(`Cannot derive aggregation communication key: capabilityInvocation[0] "${invocation}" `
41
+ + 'does not resolve to a verification method', INVALID_DID_DOCUMENT, { id: document.id, invocation });
42
+ }
43
+ return SchnorrMultikey.fromVerificationMethod(vm).publicKey;
44
+ }
3
45
  /**
4
46
  * Resolve a did:btcr2 sender's communication public key from its DID, for the
5
- * aggregation HTTP transport's `resolveSenderPk` option: a KEY identifier decodes to
6
- * its genesis public key. The aggregation transport is DID-method-agnostic and does
7
- * not name `Identifier`; method supplies this resolver when it wires the transport so
8
- * a sender that is not a pre-registered peer can still be authenticated from its DID.
47
+ * aggregation HTTP transport's `resolveSenderPk` option.
48
+ *
49
+ * A KEY (`k1`) identifier decodes directly to its genesis public key: the DID string is
50
+ * the key. An EXTERNAL (`x1`) identifier is a commitment to the hash of a genesis
51
+ * document, so there is no key in the DID string. When the genesis document is supplied
52
+ * in-band (via `opts.genesisDocument`), it is self-verifying against the DID: `Resolver`'s
53
+ * external path recomputes its canonical hash, compares it to the identifier's genesis
54
+ * bytes (throwing on mismatch), and resolves it, after which the communication key is
55
+ * derived from `capabilityInvocation[0]` ({@link getAggregationCommunicationKey}). Without
56
+ * a genesis document, an `x1` DID still resolves to `undefined`, so callers that pass no
57
+ * second argument behave exactly as before.
9
58
  *
10
- * @param did The sender's DID.
11
- * @returns The sender's compressed public key, or `undefined` when the DID is not a
12
- * decodable did:btcr2 KEY identifier (resolution then falls back to registered peers).
59
+ * The aggregation transport is DID-method-agnostic and does not name `Identifier`; method
60
+ * supplies this resolver when it wires the transport so a sender that is not a
61
+ * pre-registered peer can still be authenticated from its DID.
62
+ *
63
+ * @param {string} did The sender's DID.
64
+ * @param {object} [opts] Optional resolution inputs.
65
+ * @param {object} [opts.genesisDocument] The `x1` sender's genesis document, carried in-band
66
+ * on the bootstrap opt-in. Ignored for KEY identifiers.
67
+ * @returns {CompressedSecp256k1PublicKey | undefined} The sender's compressed public key, or
68
+ * `undefined` when the DID is not a decodable did:btcr2 identifier, is an `x1` identifier
69
+ * with no (or a non-matching) genesis document, or has no usable communication key.
13
70
  */
14
- export function resolveBtcr2SenderPk(did) {
71
+ export function resolveBtcr2SenderPk(did, opts) {
15
72
  try {
16
73
  const components = Identifier.decode(did);
17
74
  if (components.idType === 'KEY') {
18
75
  return new CompressedSecp256k1PublicKey(components.genesisBytes);
19
76
  }
77
+ // EXTERNAL (x1): the DID commits to the hash of a genesis document. With the genesis
78
+ // supplied in-band, verify it hashes to the DID and derive the communication key from
79
+ // it; without it, there is no key to return.
80
+ if (opts?.genesisDocument) {
81
+ const document = Resolver.external(components, opts.genesisDocument);
82
+ return getAggregationCommunicationKey(document);
83
+ }
20
84
  }
21
85
  catch {
22
- // Not a decodable did:btcr2 KEY identifier.
86
+ // Not a decodable did:btcr2 identifier, the genesis does not hash to the DID, or the
87
+ // document has no usable capabilityInvocation communication key.
23
88
  }
24
89
  return undefined;
25
90
  }
@@ -1 +1 @@
1
- {"version":3,"file":"did-sender-resolver.js","sourceRoot":"","sources":["../../../src/core/did-sender-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1C,IAAG,UAAU,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;YAC/B,OAAO,IAAI,4BAA4B,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;QACnE,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,4CAA4C;IAC9C,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
1
+ {"version":3,"file":"did-sender-resolver.js","sourceRoot":"","sources":["../../../src/core/did-sender-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAElE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,8BAA8B,CAC5C,QAAqB;IAErB,MAAM,UAAU,GAAG,QAAQ,CAAC,oBAAoB,EAAE,CAAC,CAAC,CAAC,CAAC;IACtD,IAAG,UAAU,KAAK,SAAS,EAAE,CAAC;QAC5B,MAAM,IAAI,gBAAgB,CACxB,6EAA6E,EAC7E,oBAAoB,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,CAC1C,CAAC;IACJ,CAAC;IAED,kFAAkF;IAClF,sFAAsF;IACtF,wFAAwF;IACxF,yCAAyC;IACzC,MAAM,EAAE,GAAsC,OAAO,UAAU,KAAK,QAAQ;QAC1E,CAAC,CAAC,QAAQ,CAAC,kBAAkB,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,UAAU,CAAC;QACvE,CAAC,CAAC,UAAU,CAAC;IAEf,IAAG,CAAC,EAAE,EAAE,CAAC;QACP,MAAM,IAAI,gBAAgB,CACxB,yEAAyE,UAAU,IAAI;cACnF,2CAA2C,EAC/C,oBAAoB,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,UAAU,EAAE,CACtD,CAAC;IACJ,CAAC;IAED,OAAO,eAAe,CAAC,sBAAsB,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,oBAAoB,CAClC,GAAW,EACX,IAAmC;IAEnC,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1C,IAAG,UAAU,CAAC,MAAM,KAAK,KAAK,EAAE,CAAC;YAC/B,OAAO,IAAI,4BAA4B,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;QACnE,CAAC;QACD,qFAAqF;QACrF,sFAAsF;QACtF,6CAA6C;QAC7C,IAAG,IAAI,EAAE,eAAe,EAAE,CAAC;YACzB,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC;YACrE,OAAO,8BAA8B,CAAC,QAAQ,CAAC,CAAC;QAClD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,qFAAqF;QACrF,iEAAiE;IACnE,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -1,14 +1,54 @@
1
1
  import { CompressedSecp256k1PublicKey } from '@did-btcr2/keypair';
2
+ import type { DidDocument } from '../utils/did-document.js';
3
+ /**
4
+ * Derive the aggregation communication public key from a resolved did:btcr2 DID Document.
5
+ *
6
+ * The communication key is the verification method referenced by
7
+ * `capabilityInvocation[0]`, resolved to its public key. This is the exact relationship
8
+ * the method already enforces for DID updates (construct and sign require the signing
9
+ * method to be in `capabilityInvocation`, the update proof is built and verified with
10
+ * `proofPurpose: 'capabilityInvocation'`), so binding the transport communication key to
11
+ * it yields the invariant "transport-authenticated as D implies authorized to update D."
12
+ * For a KEY (`k1`) document the single deterministic key is already the sole
13
+ * `capabilityInvocation` entry, so this is a no-op for KEY DIDs.
14
+ *
15
+ * There is deliberately no `verificationMethod[0]` fallback: a document without
16
+ * `capabilityInvocation` cannot be updated at all, so it is useless for aggregation and is
17
+ * rejected here rather than bound to an unrelated key.
18
+ *
19
+ * @param {DidDocument} document The resolved DID Document (placeholder id already replaced).
20
+ * @returns {CompressedSecp256k1PublicKey} The compressed public key of the communication method.
21
+ * @throws {DidDocumentError} If `capabilityInvocation` is absent or its first entry does not
22
+ * resolve to a verification method in the document.
23
+ */
24
+ export declare function getAggregationCommunicationKey(document: DidDocument): CompressedSecp256k1PublicKey;
2
25
  /**
3
26
  * Resolve a did:btcr2 sender's communication public key from its DID, for the
4
- * aggregation HTTP transport's `resolveSenderPk` option: a KEY identifier decodes to
5
- * its genesis public key. The aggregation transport is DID-method-agnostic and does
6
- * not name `Identifier`; method supplies this resolver when it wires the transport so
7
- * a sender that is not a pre-registered peer can still be authenticated from its DID.
8
- *
9
- * @param did The sender's DID.
10
- * @returns The sender's compressed public key, or `undefined` when the DID is not a
11
- * decodable did:btcr2 KEY identifier (resolution then falls back to registered peers).
27
+ * aggregation HTTP transport's `resolveSenderPk` option.
28
+ *
29
+ * A KEY (`k1`) identifier decodes directly to its genesis public key: the DID string is
30
+ * the key. An EXTERNAL (`x1`) identifier is a commitment to the hash of a genesis
31
+ * document, so there is no key in the DID string. When the genesis document is supplied
32
+ * in-band (via `opts.genesisDocument`), it is self-verifying against the DID: `Resolver`'s
33
+ * external path recomputes its canonical hash, compares it to the identifier's genesis
34
+ * bytes (throwing on mismatch), and resolves it, after which the communication key is
35
+ * derived from `capabilityInvocation[0]` ({@link getAggregationCommunicationKey}). Without
36
+ * a genesis document, an `x1` DID still resolves to `undefined`, so callers that pass no
37
+ * second argument behave exactly as before.
38
+ *
39
+ * The aggregation transport is DID-method-agnostic and does not name `Identifier`; method
40
+ * supplies this resolver when it wires the transport so a sender that is not a
41
+ * pre-registered peer can still be authenticated from its DID.
42
+ *
43
+ * @param {string} did The sender's DID.
44
+ * @param {object} [opts] Optional resolution inputs.
45
+ * @param {object} [opts.genesisDocument] The `x1` sender's genesis document, carried in-band
46
+ * on the bootstrap opt-in. Ignored for KEY identifiers.
47
+ * @returns {CompressedSecp256k1PublicKey | undefined} The sender's compressed public key, or
48
+ * `undefined` when the DID is not a decodable did:btcr2 identifier, is an `x1` identifier
49
+ * with no (or a non-matching) genesis document, or has no usable communication key.
12
50
  */
13
- export declare function resolveBtcr2SenderPk(did: string): CompressedSecp256k1PublicKey | undefined;
51
+ export declare function resolveBtcr2SenderPk(did: string, opts?: {
52
+ genesisDocument?: object;
53
+ }): CompressedSecp256k1PublicKey | undefined;
14
54
  //# sourceMappingURL=did-sender-resolver.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"did-sender-resolver.d.ts","sourceRoot":"","sources":["../../../src/core/did-sender-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAGlE;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,4BAA4B,GAAG,SAAS,CAU1F"}
1
+ {"version":3,"file":"did-sender-resolver.d.ts","sourceRoot":"","sources":["../../../src/core/did-sender-resolver.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,KAAK,EAAE,WAAW,EAAyB,MAAM,0BAA0B,CAAC;AAInF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,WAAW,GACpB,4BAA4B,CA0B9B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GAClC,4BAA4B,GAAG,SAAS,CAkB1C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@did-btcr2/method",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "type": "module",
5
5
  "description": "Reference implementation for the did:btcr2 DID method written in TypeScript and JavaScript. did:btcr2 is a censorship resistant DID Method using the Bitcoin blockchain as a Verifiable Data Registry to announce changes to the DID document. This is the core method implementation for the did-btcr2-js monorepo.",
6
6
  "main": "./dist/cjs/index.js",
@@ -66,49 +66,35 @@
66
66
  "dependencies": {
67
67
  "@noble/curves": "^1.9.7",
68
68
  "@noble/hashes": "^1.8.0",
69
- "@noble/secp256k1": "^2.3.0",
70
69
  "@scure/base": "^1.2.6",
71
- "@scure/bip32": "^1.7.0",
72
- "@scure/bip39": "^1.6.0",
73
70
  "@scure/btc-signer": "^1.8.1",
74
- "@web5/common": "^1.1.0",
75
- "@web5/crypto": "^1.0.6",
76
71
  "@web5/dids": "^1.2.0",
77
- "canonicalize": "^2.1.0",
78
- "dotenv": "^16.6.1",
79
- "nostr-tools": "^2.23.3",
80
- "@did-btcr2/common": "^9.1.0",
81
- "@did-btcr2/keypair": "^0.13.1",
72
+ "@did-btcr2/bitcoin": "^0.8.0",
82
73
  "@did-btcr2/smt": "^0.3.0",
83
- "@did-btcr2/cryptosuite": "^9.0.0",
84
- "@did-btcr2/bitcoin": "^0.8.0"
74
+ "@did-btcr2/keypair": "^0.13.1",
75
+ "@did-btcr2/common": "^9.1.0",
76
+ "@did-btcr2/cryptosuite": "^9.0.0"
85
77
  },
86
78
  "devDependencies": {
87
79
  "@eslint/js": "^9.39.4",
88
80
  "@types/chai": "^5.2.3",
89
- "@types/chai-as-promised": "^8.0.2",
90
- "@types/eslint": "^9.6.1",
91
81
  "@types/mocha": "^10.0.10",
92
82
  "@types/node": "^25.5.0",
93
83
  "@typescript-eslint/eslint-plugin": "^8.58.0",
94
84
  "@typescript-eslint/parser": "^8.58.0",
95
85
  "c8": "^10.1.3",
96
86
  "chai": "^5.3.3",
97
- "chai-as-promised": "^8.0.2",
98
87
  "commander": "^13.1.0",
99
88
  "esbuild": "^0.24.2",
100
89
  "eslint": "^9.39.4",
101
90
  "eslint-plugin-mocha": "^10.5.0",
102
91
  "globals": "^15.15.0",
103
92
  "mocha": "^10.8.2",
104
- "mocha-junit-reporter": "^2.2.1",
105
93
  "multiformats": "^13.4.2",
106
94
  "node-stdlib-browser": "^1.3.1",
107
95
  "rimraf": "^6.1.3",
108
96
  "tsup": "^8.5.1",
109
- "typedoc-plugin-markdown": "^4.11.0",
110
- "typescript": "^5.9.3",
111
- "typescript-eslint": "^8.58.0"
97
+ "typescript": "^5.9.3"
112
98
  },
113
99
  "scripts": {
114
100
  "clean": "rimraf dist coverage tests/compiled",
@@ -1,25 +1,106 @@
1
+ import { DidDocumentError, INVALID_DID_DOCUMENT } from '@did-btcr2/common';
2
+ import { SchnorrMultikey } from '@did-btcr2/cryptosuite';
1
3
  import { CompressedSecp256k1PublicKey } from '@did-btcr2/keypair';
4
+ import type { DidDocument, DidVerificationMethod } from '../utils/did-document.js';
2
5
  import { Identifier } from './identifier.js';
6
+ import { Resolver } from './resolver.js';
7
+
8
+ /**
9
+ * Derive the aggregation communication public key from a resolved did:btcr2 DID Document.
10
+ *
11
+ * The communication key is the verification method referenced by
12
+ * `capabilityInvocation[0]`, resolved to its public key. This is the exact relationship
13
+ * the method already enforces for DID updates (construct and sign require the signing
14
+ * method to be in `capabilityInvocation`, the update proof is built and verified with
15
+ * `proofPurpose: 'capabilityInvocation'`), so binding the transport communication key to
16
+ * it yields the invariant "transport-authenticated as D implies authorized to update D."
17
+ * For a KEY (`k1`) document the single deterministic key is already the sole
18
+ * `capabilityInvocation` entry, so this is a no-op for KEY DIDs.
19
+ *
20
+ * There is deliberately no `verificationMethod[0]` fallback: a document without
21
+ * `capabilityInvocation` cannot be updated at all, so it is useless for aggregation and is
22
+ * rejected here rather than bound to an unrelated key.
23
+ *
24
+ * @param {DidDocument} document The resolved DID Document (placeholder id already replaced).
25
+ * @returns {CompressedSecp256k1PublicKey} The compressed public key of the communication method.
26
+ * @throws {DidDocumentError} If `capabilityInvocation` is absent or its first entry does not
27
+ * resolve to a verification method in the document.
28
+ */
29
+ export function getAggregationCommunicationKey(
30
+ document: DidDocument,
31
+ ): CompressedSecp256k1PublicKey {
32
+ const invocation = document.capabilityInvocation?.[0];
33
+ if(invocation === undefined) {
34
+ throw new DidDocumentError(
35
+ 'Cannot derive aggregation communication key: capabilityInvocation is absent',
36
+ INVALID_DID_DOCUMENT, { id: document.id }
37
+ );
38
+ }
39
+
40
+ // Resolve capabilityInvocation[0] to a verification method: a string reference is
41
+ // dereferenced by id against verificationMethod; an embedded method is used directly.
42
+ // A local id lookup is used rather than getSigningMethod, which defaults to #initialKey
43
+ // and does not resolve embedded methods.
44
+ const vm: DidVerificationMethod | undefined = typeof invocation === 'string'
45
+ ? document.verificationMethod?.find(method => method.id === invocation)
46
+ : invocation;
47
+
48
+ if(!vm) {
49
+ throw new DidDocumentError(
50
+ `Cannot derive aggregation communication key: capabilityInvocation[0] "${invocation}" `
51
+ + 'does not resolve to a verification method',
52
+ INVALID_DID_DOCUMENT, { id: document.id, invocation }
53
+ );
54
+ }
55
+
56
+ return SchnorrMultikey.fromVerificationMethod(vm).publicKey;
57
+ }
3
58
 
4
59
  /**
5
60
  * Resolve a did:btcr2 sender's communication public key from its DID, for the
6
- * aggregation HTTP transport's `resolveSenderPk` option: a KEY identifier decodes to
7
- * its genesis public key. The aggregation transport is DID-method-agnostic and does
8
- * not name `Identifier`; method supplies this resolver when it wires the transport so
9
- * a sender that is not a pre-registered peer can still be authenticated from its DID.
61
+ * aggregation HTTP transport's `resolveSenderPk` option.
10
62
  *
11
- * @param did The sender's DID.
12
- * @returns The sender's compressed public key, or `undefined` when the DID is not a
13
- * decodable did:btcr2 KEY identifier (resolution then falls back to registered peers).
63
+ * A KEY (`k1`) identifier decodes directly to its genesis public key: the DID string is
64
+ * the key. An EXTERNAL (`x1`) identifier is a commitment to the hash of a genesis
65
+ * document, so there is no key in the DID string. When the genesis document is supplied
66
+ * in-band (via `opts.genesisDocument`), it is self-verifying against the DID: `Resolver`'s
67
+ * external path recomputes its canonical hash, compares it to the identifier's genesis
68
+ * bytes (throwing on mismatch), and resolves it, after which the communication key is
69
+ * derived from `capabilityInvocation[0]` ({@link getAggregationCommunicationKey}). Without
70
+ * a genesis document, an `x1` DID still resolves to `undefined`, so callers that pass no
71
+ * second argument behave exactly as before.
72
+ *
73
+ * The aggregation transport is DID-method-agnostic and does not name `Identifier`; method
74
+ * supplies this resolver when it wires the transport so a sender that is not a
75
+ * pre-registered peer can still be authenticated from its DID.
76
+ *
77
+ * @param {string} did The sender's DID.
78
+ * @param {object} [opts] Optional resolution inputs.
79
+ * @param {object} [opts.genesisDocument] The `x1` sender's genesis document, carried in-band
80
+ * on the bootstrap opt-in. Ignored for KEY identifiers.
81
+ * @returns {CompressedSecp256k1PublicKey | undefined} The sender's compressed public key, or
82
+ * `undefined` when the DID is not a decodable did:btcr2 identifier, is an `x1` identifier
83
+ * with no (or a non-matching) genesis document, or has no usable communication key.
14
84
  */
15
- export function resolveBtcr2SenderPk(did: string): CompressedSecp256k1PublicKey | undefined {
85
+ export function resolveBtcr2SenderPk(
86
+ did: string,
87
+ opts?: { genesisDocument?: object },
88
+ ): CompressedSecp256k1PublicKey | undefined {
16
89
  try {
17
90
  const components = Identifier.decode(did);
18
91
  if(components.idType === 'KEY') {
19
92
  return new CompressedSecp256k1PublicKey(components.genesisBytes);
20
93
  }
94
+ // EXTERNAL (x1): the DID commits to the hash of a genesis document. With the genesis
95
+ // supplied in-band, verify it hashes to the DID and derive the communication key from
96
+ // it; without it, there is no key to return.
97
+ if(opts?.genesisDocument) {
98
+ const document = Resolver.external(components, opts.genesisDocument);
99
+ return getAggregationCommunicationKey(document);
100
+ }
21
101
  } catch {
22
- // Not a decodable did:btcr2 KEY identifier.
102
+ // Not a decodable did:btcr2 identifier, the genesis does not hash to the DID, or the
103
+ // document has no usable capabilityInvocation communication key.
23
104
  }
24
105
  return undefined;
25
106
  }