@did-btcr2/method 0.56.0 → 0.58.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 (56) hide show
  1. package/README.md +10 -4
  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 +93 -37
  6. package/dist/esm/core/beacon/beacon.js +11 -1
  7. package/dist/esm/core/beacon/beacon.js.map +1 -1
  8. package/dist/esm/core/beacon/cas-beacon.js +7 -6
  9. package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
  10. package/dist/esm/core/beacon/factory.js +6 -4
  11. package/dist/esm/core/beacon/factory.js.map +1 -1
  12. package/dist/esm/core/beacon/singleton-beacon.js +2 -2
  13. package/dist/esm/core/beacon/singleton-beacon.js.map +1 -1
  14. package/dist/esm/core/beacon/smt-beacon.js +7 -6
  15. package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
  16. package/dist/esm/core/did-sender-resolver.js +8 -2
  17. package/dist/esm/core/did-sender-resolver.js.map +1 -1
  18. package/dist/esm/core/resolver.js +5 -28
  19. package/dist/esm/core/resolver.js.map +1 -1
  20. package/dist/esm/core/updater.js +17 -4
  21. package/dist/esm/core/updater.js.map +1 -1
  22. package/dist/esm/did-btcr2.js +23 -7
  23. package/dist/esm/did-btcr2.js.map +1 -1
  24. package/dist/esm/utils/appendix.js +36 -5
  25. package/dist/esm/utils/appendix.js.map +1 -1
  26. package/dist/types/core/beacon/beacon.d.ts +10 -1
  27. package/dist/types/core/beacon/beacon.d.ts.map +1 -1
  28. package/dist/types/core/beacon/cas-beacon.d.ts +2 -1
  29. package/dist/types/core/beacon/cas-beacon.d.ts.map +1 -1
  30. package/dist/types/core/beacon/factory.d.ts +3 -1
  31. package/dist/types/core/beacon/factory.d.ts.map +1 -1
  32. package/dist/types/core/beacon/singleton-beacon.d.ts +1 -1
  33. package/dist/types/core/beacon/singleton-beacon.d.ts.map +1 -1
  34. package/dist/types/core/beacon/smt-beacon.d.ts +2 -1
  35. package/dist/types/core/beacon/smt-beacon.d.ts.map +1 -1
  36. package/dist/types/core/did-sender-resolver.d.ts.map +1 -1
  37. package/dist/types/core/interfaces.d.ts.map +1 -1
  38. package/dist/types/core/resolver.d.ts.map +1 -1
  39. package/dist/types/core/updater.d.ts +11 -4
  40. package/dist/types/core/updater.d.ts.map +1 -1
  41. package/dist/types/did-btcr2.d.ts.map +1 -1
  42. package/dist/types/utils/appendix.d.ts +30 -4
  43. package/dist/types/utils/appendix.d.ts.map +1 -1
  44. package/package.json +3 -3
  45. package/src/core/beacon/beacon.ts +12 -1
  46. package/src/core/beacon/cas-beacon.ts +7 -6
  47. package/src/core/beacon/factory.ts +6 -4
  48. package/src/core/beacon/interfaces.ts +1 -1
  49. package/src/core/beacon/singleton-beacon.ts +2 -2
  50. package/src/core/beacon/smt-beacon.ts +7 -6
  51. package/src/core/did-sender-resolver.ts +10 -2
  52. package/src/core/interfaces.ts +1 -0
  53. package/src/core/resolver.ts +12 -30
  54. package/src/core/updater.ts +25 -5
  55. package/src/did-btcr2.ts +28 -8
  56. package/src/utils/appendix.ts +37 -5
@@ -8,11 +8,37 @@ import type { RootCapability } from '../core/interfaces.js';
8
8
  */
9
9
  export declare class Appendix {
10
10
  /**
11
- * Extracts a DID fragment from a given input
12
- * @param {unknown} input The input to extract the DID fragment from
13
- * @returns {string | undefined} The extracted DID fragment or undefined if not found
11
+ * Resolves a DID URL that may be written as a relative reference against the
12
+ * DID it appears under. DID Core permits the `id` of a verification method or
13
+ * service, and the entries of a verification relationship, to be a relative
14
+ * DID URL such as `#initialKey`; it denotes that fragment of `did`. Absolute
15
+ * DID URLs are returned unchanged, so both spellings of the same reference
16
+ * compare equal without discarding the DID being compared.
17
+ *
18
+ * @param {unknown} input The DID URL to resolve. Non-strings yield `undefined`.
19
+ * @param {string} did The DID the relative reference is resolved against.
20
+ * @returns {string | undefined} The absolute DID URL, or `undefined` if `input` is not a usable string.
21
+ */
22
+ static absoluteDidUrl(input: unknown, did: string): string | undefined;
23
+ /**
24
+ * Resolves a verification relationship entry to the absolute DID URL of the method it
25
+ * names. A relationship entry is either a reference to a method defined elsewhere in the
26
+ * document, or a method embedded inline, which names itself with its own `id`; either
27
+ * spelling may be a relative DID URL.
28
+ *
29
+ * Every comparison of a relationship entry against a method id goes through this helper,
30
+ * on both the read path and the write path, so the two admit exactly the same spellings.
31
+ *
32
+ * Malformed entries yield `undefined` rather than a placeholder string, so two unusable
33
+ * values never compare equal to each other. Callers comparing a resolved entry must still
34
+ * reject an `undefined` target before comparing, or an unusable target matches an
35
+ * unusable entry.
36
+ *
37
+ * @param {unknown} entry The relationship entry: a reference, or an embedded method.
38
+ * @param {string} did The DID the relative reference is resolved against.
39
+ * @returns {string | undefined} The absolute DID URL, or `undefined` if unusable.
14
40
  */
15
- static extractDidFragment(input: unknown): string | undefined;
41
+ static relationshipMethodId(entry: unknown, did: string): string | undefined;
16
42
  /**
17
43
  * Validates that the given object is a DidVerificationMethod
18
44
  * @param {unknown} obj The object to validate
@@ -1 +1 @@
1
- {"version":3,"file":"appendix.d.ts","sourceRoot":"","sources":["../../../src/utils/appendix.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,qBAAqB,EAAC,MAAM,YAAY,CAAC;AAO3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D;;;;;GAKG;AACH,qBAAa,QAAQ;IACnB;;;;OAIG;WACW,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS;IAMpE;;;;OAIG;WACW,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,qBAAqB;IAcjF;;;;OAIG;WACW,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,UAAU;IAW3D;;;;;OAKG;WACW,sBAAsB,CAAC,WAAW,EAAE,WAAW,GAAG,qBAAqB,EAAE;IAgBvF;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,CAAC,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc;IAU/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAuCG;WACW,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,cAAc;CA8CtE"}
1
+ {"version":3,"file":"appendix.d.ts","sourceRoot":"","sources":["../../../src/utils/appendix.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACV,qBAAqB,EAAC,MAAM,YAAY,CAAC;AAO3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D;;;;;GAKG;AACH,qBAAa,QAAQ;IACnB;;;;;;;;;;;OAWG;WACW,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAM7E;;;;;;;;;;;;;;;;;OAiBG;WACW,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAOnF;;;;OAIG;WACW,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,qBAAqB;IAcjF;;;;OAIG;WACW,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,UAAU;IAW3D;;;;;OAKG;WACW,sBAAsB,CAAC,WAAW,EAAE,WAAW,GAAG,qBAAqB,EAAE;IAgBvF;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,CAAC,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc;IAU/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAuCG;WACW,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,cAAc;CA8CtE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@did-btcr2/method",
3
- "version": "0.56.0",
3
+ "version": "0.58.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",
@@ -69,11 +69,11 @@
69
69
  "@scure/base": "^1.2.6",
70
70
  "@scure/btc-signer": "^1.8.1",
71
71
  "@web5/dids": "^1.2.0",
72
- "@did-btcr2/common": "^9.3.0",
73
72
  "@did-btcr2/bitcoin": "^0.10.0",
74
73
  "@did-btcr2/smt": "^0.3.0",
75
74
  "@did-btcr2/cryptosuite": "^10.0.0",
76
- "@did-btcr2/keypair": "^0.13.1"
75
+ "@did-btcr2/keypair": "^0.13.1",
76
+ "@did-btcr2/common": "^9.3.0"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@eslint/js": "^9.39.4",
@@ -544,8 +544,19 @@ export abstract class SinglePartyBeacon {
544
544
  */
545
545
  readonly service: BeaconService;
546
546
 
547
- constructor(service: BeaconService) {
547
+ /**
548
+ * The absolute did:btcr2 identifier this beacon instance serves.
549
+ *
550
+ * Injected by the caller rather than recovered from {@link service}. A beacon
551
+ * service `id` is permitted to be a relative DID URL (`#beacon-1`), which
552
+ * carries no DID to strip a fragment from, so deriving the subject from it is
553
+ * only correct for the absolute spelling.
554
+ */
555
+ readonly did: string;
556
+
557
+ constructor(service: BeaconService, did: string) {
548
558
  this.service = service;
559
+ this.did = did;
549
560
  }
550
561
 
551
562
  /**
@@ -57,9 +57,10 @@ export class CASBeacon extends SinglePartyBeacon {
57
57
  /**
58
58
  * Creates an instance of CASBeacon.
59
59
  * @param {BeaconService} service The service of the Beacon.
60
+ * @param {string} did The absolute did:btcr2 identifier this beacon serves.
60
61
  */
61
- constructor(service: BeaconService) {
62
- super({ ...service, type: 'CASBeacon' });
62
+ constructor(service: BeaconService, did: string) {
63
+ super({ ...service, type: 'CASBeacon' }, did);
63
64
  }
64
65
 
65
66
  /**
@@ -82,8 +83,8 @@ export class CASBeacon extends SinglePartyBeacon {
82
83
  const updates = new Array<[SignedBTCR2Update, BlockMetadata]>();
83
84
  const needs = new Array<DataNeed>();
84
85
 
85
- // Extract the DID from the beacon service id (strip the #fragment)
86
- const did = this.service.id.split('#')[0];
86
+ // The DID under resolution keys this beacon's announcement entry.
87
+ const did = this.did;
87
88
 
88
89
  for(const signal of signals) {
89
90
  // Signal bytes are hex, matches hex-keyed sidecar maps directly
@@ -160,8 +161,8 @@ export class CASBeacon extends SinglePartyBeacon {
160
161
  bitcoin: BitcoinConnection,
161
162
  options?: CASBroadcastOptions
162
163
  ): Promise<BroadcastResult> {
163
- // Extract the DID from the beacon service id (strip the #fragment)
164
- const did = this.service.id.split('#')[0];
164
+ // The DID this beacon serves keys its announcement entry.
165
+ const did = this.did;
165
166
 
166
167
  // Hash the signed update (base64urlnopad for the CAS Announcement entry per spec)
167
168
  const updateHash = canonicalHash(signedUpdate);
@@ -14,16 +14,18 @@ export class BeaconFactory {
14
14
  /**
15
15
  * Establish a Beacon instance based on the provided service and optional sidecar data.
16
16
  * @param {BeaconService} service The beacon service configuration.
17
+ * @param {string} did The absolute did:btcr2 identifier the beacon serves. Supplied
18
+ * by the caller because a beacon service `id` may be a relative DID URL.
17
19
  * @returns {SinglePartyBeacon} The established Beacon instance.
18
20
  */
19
- static establish(service: BeaconService): SinglePartyBeacon {
21
+ static establish(service: BeaconService, did: string): SinglePartyBeacon {
20
22
  switch (service.type) {
21
23
  case 'SingletonBeacon':
22
- return new SingletonBeacon(service);
24
+ return new SingletonBeacon(service, did);
23
25
  case 'CASBeacon':
24
- return new CASBeacon(service);
26
+ return new CASBeacon(service, did);
25
27
  case 'SMTBeacon':
26
- return new SMTBeacon(service);
28
+ return new SMTBeacon(service, did);
27
29
  default:
28
30
  throw new MethodError('Invalid Beacon Type', 'INVALID_BEACON_ERROR', service);
29
31
  }
@@ -76,4 +76,4 @@ export interface BeaconSignal {
76
76
  *
77
77
  * @param announcement The CAS Announcement object (DID to update hash mapping).
78
78
  */
79
- export type CasPublishFn = (announcement: Record<string, string>) => Promise<void>;
79
+ export type CasPublishFn = (announcement: Record<string, string>) => Promise<void>;
@@ -20,8 +20,8 @@ export class SingletonBeacon extends SinglePartyBeacon {
20
20
  * Creates an instance of SingletonBeacon.
21
21
  * @param {BeaconService} service The BeaconService object representing the funded beacon to announce the update to.
22
22
  */
23
- constructor(service: BeaconService) {
24
- super({ ...service, type: 'SingletonBeacon' });
23
+ constructor(service: BeaconService, did: string) {
24
+ super({ ...service, type: 'SingletonBeacon' }, did);
25
25
  }
26
26
 
27
27
  /**
@@ -27,9 +27,10 @@ export class SMTBeacon extends SinglePartyBeacon {
27
27
  /**
28
28
  * Creates an instance of SMTBeacon.
29
29
  * @param {BeaconService} service The Beacon service.
30
+ * @param {string} did The absolute did:btcr2 identifier this beacon serves.
30
31
  */
31
- constructor(service: BeaconService) {
32
- super({ ...service, type: 'SMTBeacon' });
32
+ constructor(service: BeaconService, did: string) {
33
+ super({ ...service, type: 'SMTBeacon' }, did);
33
34
  }
34
35
 
35
36
  /**
@@ -52,8 +53,8 @@ export class SMTBeacon extends SinglePartyBeacon {
52
53
  const updates = new Array<[SignedBTCR2Update, BlockMetadata]>();
53
54
  const needs = new Array<DataNeed>();
54
55
 
55
- // Extract the DID from the beacon service id (strip the #fragment)
56
- const did = this.service.id.split('#')[0];
56
+ // The DID under resolution keys this beacon's leaf index.
57
+ const did = this.did;
57
58
 
58
59
  for(const signal of signals) {
59
60
  // Signal bytes are the hex-encoded SMT root hash; smtMap is keyed by proof.id (also hex)
@@ -144,8 +145,8 @@ export class SMTBeacon extends SinglePartyBeacon {
144
145
  bitcoin: BitcoinConnection,
145
146
  options?: BroadcastOptions
146
147
  ): Promise<BroadcastResult> {
147
- // Extract the DID from the beacon service id (strip the #fragment)
148
- const did = this.service.id.split('#')[0];
148
+ // The DID keys this beacon's leaf index in the tree.
149
+ const did = this.did;
149
150
 
150
151
  // Build a single-entry SMT from the signed update
151
152
  const canonicalBytes = new TextEncoder().encode(canonicalize(signedUpdate));
@@ -1,6 +1,7 @@
1
1
  import { DidDocumentError, INVALID_DID_DOCUMENT } from '@did-btcr2/common';
2
2
  import { SchnorrMultikey } from '@did-btcr2/cryptosuite';
3
3
  import { CompressedSecp256k1PublicKey } from '@did-btcr2/keypair';
4
+ import { Appendix } from '../utils/appendix.js';
4
5
  import type { DidDocument, DidVerificationMethod } from '../utils/did-document.js';
5
6
  import { Identifier } from './identifier.js';
6
7
  import { Resolver } from './resolver.js';
@@ -40,9 +41,16 @@ export function getAggregationCommunicationKey(
40
41
  // Resolve capabilityInvocation[0] to a verification method: a string reference is
41
42
  // dereferenced by id against verificationMethod; an embedded method is used directly.
42
43
  // A local id lookup is used rather than getSigningMethod, which defaults to #initialKey
43
- // and does not resolve embedded methods.
44
+ // and does not resolve embedded methods. Reference and method id are each resolved to an
45
+ // absolute DID URL first, because either may be spelled as a relative DID URL; an
46
+ // unusable reference resolves to nothing rather than to the first unusable method id.
47
+ const invocationId = Appendix.absoluteDidUrl(invocation, document.id);
44
48
  const vm: DidVerificationMethod | undefined = typeof invocation === 'string'
45
- ? document.verificationMethod?.find(method => method.id === invocation)
49
+ ? (invocationId === undefined
50
+ ? undefined
51
+ : document.verificationMethod?.find(
52
+ method => Appendix.absoluteDidUrl(method.id, document.id) === invocationId
53
+ ))
46
54
  : invocation;
47
55
 
48
56
  if(!vm) {
@@ -46,6 +46,7 @@ export interface ResolutionOptions extends DidResolutionOptions {
46
46
  * document is well-formed, the resolver simply stopped at the caller's limit.
47
47
  */
48
48
  maxDiscoveryRounds?: number;
49
+
49
50
  }
50
51
 
51
52
  /**
@@ -149,32 +149,6 @@ function isSMTProof(value: unknown): value is SMTProof {
149
149
  && Array.isArray(value.hashes);
150
150
  }
151
151
 
152
- // ─── verification relationship membership ────────────────────────────────────
153
-
154
- /**
155
- * Resolves a verification relationship entry to the absolute DID URL of the
156
- * method it names, or `undefined` when the entry names no method.
157
- *
158
- * An entry is either a reference (a bare fragment such as `#key-0`, or an
159
- * absolute DID URL) or an embedded verification method object. A bare fragment
160
- * is relative to the document that carries it, so it is resolved against
161
- * `documentId` before comparison; every other string is returned unchanged, so
162
- * a reference naming a different DID can never collapse onto this document's.
163
- *
164
- * Malformed entries yield `undefined` rather than a placeholder string, so two
165
- * unusable values never compare equal to each other.
166
- *
167
- * @param {string} documentId The `id` of the DID document carrying the entry.
168
- * @param {unknown} entry The relationship entry: a reference or an embedded method.
169
- * @returns {string | undefined} The absolute DID URL, or undefined if unusable.
170
- */
171
- function relationshipMethodId(documentId: string, entry: unknown): string | undefined {
172
- // An embedded method names itself with its `id`; a reference is the id itself.
173
- const id = isRecord(entry) ? entry.id : entry;
174
- if(typeof id !== 'string' || id.length === 0) return undefined;
175
- return id.startsWith('#') ? `${documentId}${id}` : id;
176
- }
177
-
178
152
  /**
179
153
  * Different possible Resolver states representing phases in the resolution process.
180
154
  */
@@ -251,6 +225,7 @@ export class Resolver {
251
225
  /** Count of beacon-discovery passes driven by updates adding new beacon services. */
252
226
  #discoveryRounds = 0;
253
227
 
228
+
254
229
  /**
255
230
  * @internal Use {@link DidBtcr2.resolve} to create instances.
256
231
  */
@@ -258,7 +233,12 @@ export class Resolver {
258
233
  didComponents: DidComponents,
259
234
  sidecarData: SidecarData,
260
235
  currentDocument: DidDocument | null,
261
- options?: { versionId?: string; versionTime?: string; genesisDocument?: object; maxDiscoveryRounds?: number }
236
+ options?: {
237
+ versionId?: string;
238
+ versionTime?: string;
239
+ genesisDocument?: object;
240
+ maxDiscoveryRounds?: number;
241
+ }
262
242
  ) {
263
243
  this.#didComponents = didComponents;
264
244
  this.#sidecarData = sidecarData;
@@ -636,9 +616,9 @@ export class Resolver {
636
616
  // without it here the read path applies an update signed by any key in the document.
637
617
  // Checked before the method is located so an unauthorized method always fails with
638
618
  // this typed error, whether or not it also appears in verificationMethod[].
639
- const authorizedMethodId = relationshipMethodId(currentDocument.id, verificationMethodId);
619
+ const authorizedMethodId = Appendix.relationshipMethodId(verificationMethodId, currentDocument.id);
640
620
  const authorized = authorizedMethodId !== undefined && currentDocument.capabilityInvocation?.some(
641
- entry => relationshipMethodId(currentDocument.id, entry) === authorizedMethodId
621
+ entry => Appendix.relationshipMethodId(entry, currentDocument.id) === authorizedMethodId
642
622
  );
643
623
  if(!authorized) {
644
624
  throw new ResolveError(
@@ -773,7 +753,9 @@ export class Resolver {
773
753
  if(this.#processedServices.has(service.id) || !signals.length) continue;
774
754
 
775
755
  // Establish a typed beacon and process its signals
776
- const beacon = BeaconFactory.establish(service);
756
+ // The beacon is bound to the DID under resolution: a beacon service
757
+ // `id` may be a relative DID URL, so it cannot supply the subject.
758
+ const beacon = BeaconFactory.establish(service, this.#currentDocument!.id);
777
759
  const result = beacon.processSignals(signals, this.#sidecarData);
778
760
 
779
761
  if(result.needs.length > 0) {
@@ -58,8 +58,8 @@ export interface FundingProof {
58
58
  * The updater needs the caller to broadcast the signed update via the beacon.
59
59
  *
60
60
  * The caller decides how: for single-party beacons, call
61
- * `Updater.announce(beaconService, signedUpdate, signer, bitcoin, options?)` or
62
- * `BeaconFactory.establish(beaconService).broadcastSignal(...)`. For multi-party
61
+ * `Updater.announce(beaconService, did, signedUpdate, signer, bitcoin, options?)` or
62
+ * `BeaconFactory.establish(beaconService, did).broadcastSignal(...)`. For multi-party
63
63
  * aggregate beacons, hand off to the aggregation protocol.
64
64
  *
65
65
  * Both single-party paths return a `BroadcastResult`; capture it. Beyond the txid,
@@ -76,6 +76,11 @@ export interface NeedBroadcast {
76
76
  readonly beaconService: BeaconService;
77
77
  /** The signed update ready for broadcast. */
78
78
  readonly signedUpdate: SignedBTCR2Update;
79
+ /**
80
+ * The DID being updated. Supplied because a beacon service `id` may be a
81
+ * relative DID URL and so cannot be used to recover the subject.
82
+ */
83
+ readonly did: string;
79
84
  }
80
85
 
81
86
  /** Discriminated union of all data needs the updater may request from the caller. */
@@ -148,7 +153,7 @@ export interface UpdaterParams {
148
153
  * // Capture the BroadcastResult: broadcast.txid, plus broadcast.announcement
149
154
  * // (CAS beacons) and broadcast.proof (SMT beacons; must be retained, the
150
155
  * // proof's nonce exists nowhere else).
151
- * const broadcast = await Updater.announce(need.beaconService, need.signedUpdate, signer, bitcoin);
156
+ * const broadcast = await Updater.announce(need.beaconService, need.did, need.signedUpdate, signer, bitcoin);
152
157
  * updater.provide(need);
153
158
  * break;
154
159
  * }
@@ -278,6 +283,12 @@ export class Updater {
278
283
  const id = verificationMethod.id.slice(hashIdx);
279
284
  const multikey = SchnorrMultikey.fromSigner(id, controller, signer);
280
285
 
286
+ // The absolute spelling of the method id, for the proof to name it by. `hashIdx === 0`
287
+ // means the document writes this method's own id as a relative DID URL, which per DID
288
+ // Core denotes that fragment of the document's `id`: the same rule as
289
+ // Appendix.absoluteDidUrl, whose `undefined` case the fragment check above excludes.
290
+ const absoluteMethodId = hashIdx === 0 ? `${did}${id}` : verificationMethod.id;
291
+
281
292
  // Fail fast if the signer's public key is not the key published in the named
282
293
  // verification method. Signing with the wrong key yields a Data Integrity
283
294
  // proof that cannot verify against the method, so the resulting on-chain
@@ -308,7 +319,12 @@ export class Updater {
308
319
  ],
309
320
  cryptosuite : 'bip340-jcs-2025',
310
321
  type : 'DataIntegrityProof',
311
- verificationMethod : verificationMethod.id,
322
+ // The proof names the signing method by absolute DID URL, even when the document
323
+ // spells that method's own id relatively: a proof travels apart from the document
324
+ // that defines the method, so a bare `#initialKey` in it resolves against nothing.
325
+ // For a document that already spells its ids absolutely this is the id unchanged,
326
+ // so proofs over such documents are byte-identical to before.
327
+ verificationMethod : absoluteMethodId,
312
328
  proofPurpose : 'capabilityInvocation',
313
329
  capability : `urn:zcap:root:${encodeURIComponent(did)}`,
314
330
  capabilityAction : 'Write',
@@ -323,6 +339,8 @@ export class Updater {
323
339
  * Announces a signed update to the Bitcoin blockchain via the specified beacon.
324
340
  *
325
341
  * @param {BeaconService} beaconService The beacon service to broadcast through.
342
+ * @param {string} did The DID being updated. Required because a beacon service
343
+ * `id` may be a relative DID URL and so cannot supply the subject.
326
344
  * @param {SignedBTCR2Update} update The signed update to announce.
327
345
  * @param {Signer} signer Signer that produces the ECDSA signature for the Bitcoin transaction.
328
346
  * @param {BitcoinConnection} bitcoin The Bitcoin network connection.
@@ -334,12 +352,13 @@ export class Updater {
334
352
  */
335
353
  static async announce(
336
354
  beaconService: BeaconService,
355
+ did: string,
337
356
  update: SignedBTCR2Update,
338
357
  signer: Signer,
339
358
  bitcoin: BitcoinConnection,
340
359
  options?: CASBroadcastOptions
341
360
  ): Promise<BroadcastResult> {
342
- const beacon = BeaconFactory.establish(beaconService);
361
+ const beacon = BeaconFactory.establish(beaconService, did);
343
362
  return beacon.broadcastSignal(update, signer, bitcoin, options);
344
363
  }
345
364
 
@@ -406,6 +425,7 @@ export class Updater {
406
425
  kind : 'NeedBroadcast',
407
426
  beaconService : this.#beaconService,
408
427
  signedUpdate : this.#state.signedUpdate,
428
+ did : this.#sourceDocument.id,
409
429
  }],
410
430
  };
411
431
  }
package/src/did-btcr2.ts CHANGED
@@ -171,8 +171,16 @@ export class DidBtcr2 implements DidMethod {
171
171
  verificationMethodId: string;
172
172
  beaconId: string;
173
173
  }): Updater {
174
- // Validate that the verificationMethodId is authorized for capabilityInvocation
175
- if(!sourceDocument.capabilityInvocation?.some(vr => vr === verificationMethodId)) {
174
+ // Validate that the verificationMethodId is authorized for capabilityInvocation.
175
+ // Both sides are resolved to absolute DID URLs first, so the caller's spelling of the
176
+ // reference and the document's spelling of the entry may differ: either is legal per
177
+ // DID Core. This is the same rule the read path applies to an update's proof, so an
178
+ // update this factory authorizes is one the resolver will also accept.
179
+ const authorizedMethodId = Appendix.relationshipMethodId(verificationMethodId, sourceDocument.id);
180
+ const authorized = authorizedMethodId !== undefined && sourceDocument.capabilityInvocation?.some(
181
+ entry => Appendix.relationshipMethodId(entry, sourceDocument.id) === authorizedMethodId
182
+ );
183
+ if(!authorized) {
176
184
  throw new UpdateError(
177
185
  'Invalid verificationMethodId: not authorized for capabilityInvocation',
178
186
  INVALID_DID_DOCUMENT, sourceDocument
@@ -206,9 +214,14 @@ export class DidBtcr2 implements DidMethod {
206
214
  );
207
215
  }
208
216
 
209
- // Find the beacon service matching the given beaconId
217
+ // Find the beacon service matching the given beaconId. A service `id` may be a relative
218
+ // DID URL, so both sides are resolved against the document before comparison; an
219
+ // unusable beaconId matches nothing rather than matching an unusable service id.
220
+ const targetBeaconId = Appendix.absoluteDidUrl(beaconId, sourceDocument.id);
210
221
  const beaconService = sourceDocument.service
211
- .filter((service: BeaconService) => service.id === beaconId)
222
+ .filter((service: BeaconService) =>
223
+ targetBeaconId !== undefined
224
+ && Appendix.absoluteDidUrl(service.id, sourceDocument.id) === targetBeaconId)
212
225
  .filter((service: BeaconService): service is BeaconService => !!service)
213
226
  .shift();
214
227
 
@@ -250,10 +263,17 @@ export class DidBtcr2 implements DidMethod {
250
263
  }
251
264
 
252
265
  // Attempt to find a verification method that matches the given method ID, or if not given,
253
- // find the first verification method intended for signing claims.
254
- const verificationMethod = didDocument.verificationMethod?.find(
255
- (vm: DidVerificationMethod) => Appendix.extractDidFragment(vm.id) === (Appendix.extractDidFragment(methodId)
256
- ?? Appendix.extractDidFragment(didDocument.assertionMethod?.[0]))
266
+ // find the first verification method intended for signing claims. Both sides are resolved
267
+ // to absolute DID URLs first: a document may spell either the verification method `id` or
268
+ // the reference to it as a relative DID URL (`#initialKey`), and the two must still match.
269
+ const targetId = Appendix.absoluteDidUrl(methodId, didDocument.id)
270
+ ?? Appendix.relationshipMethodId(didDocument.assertionMethod?.[0], didDocument.id);
271
+
272
+ // An unusable target matches nothing: without this guard it compares equal to every
273
+ // method whose own id is unusable, and the document's first malformed method is
274
+ // returned as the signing method.
275
+ const verificationMethod = targetId === undefined ? undefined : didDocument.verificationMethod?.find(
276
+ (vm: DidVerificationMethod) => Appendix.absoluteDidUrl(vm.id, didDocument.id) === targetId
257
277
  );
258
278
 
259
279
  // If no verification method is found, throw an error
@@ -18,14 +18,46 @@ import type { RootCapability } from '../core/interfaces.js';
18
18
  */
19
19
  export class Appendix {
20
20
  /**
21
- * Extracts a DID fragment from a given input
22
- * @param {unknown} input The input to extract the DID fragment from
23
- * @returns {string | undefined} The extracted DID fragment or undefined if not found
21
+ * Resolves a DID URL that may be written as a relative reference against the
22
+ * DID it appears under. DID Core permits the `id` of a verification method or
23
+ * service, and the entries of a verification relationship, to be a relative
24
+ * DID URL such as `#initialKey`; it denotes that fragment of `did`. Absolute
25
+ * DID URLs are returned unchanged, so both spellings of the same reference
26
+ * compare equal without discarding the DID being compared.
27
+ *
28
+ * @param {unknown} input The DID URL to resolve. Non-strings yield `undefined`.
29
+ * @param {string} did The DID the relative reference is resolved against.
30
+ * @returns {string | undefined} The absolute DID URL, or `undefined` if `input` is not a usable string.
24
31
  */
25
- public static extractDidFragment(input: unknown): string | undefined {
32
+ public static absoluteDidUrl(input: unknown, did: string): string | undefined {
26
33
  if (typeof input !== 'string') return undefined;
27
34
  if (input.length === 0) return undefined;
28
- return input;
35
+ return input.startsWith('#') ? `${did}${input}` : input;
36
+ }
37
+
38
+ /**
39
+ * Resolves a verification relationship entry to the absolute DID URL of the method it
40
+ * names. A relationship entry is either a reference to a method defined elsewhere in the
41
+ * document, or a method embedded inline, which names itself with its own `id`; either
42
+ * spelling may be a relative DID URL.
43
+ *
44
+ * Every comparison of a relationship entry against a method id goes through this helper,
45
+ * on both the read path and the write path, so the two admit exactly the same spellings.
46
+ *
47
+ * Malformed entries yield `undefined` rather than a placeholder string, so two unusable
48
+ * values never compare equal to each other. Callers comparing a resolved entry must still
49
+ * reject an `undefined` target before comparing, or an unusable target matches an
50
+ * unusable entry.
51
+ *
52
+ * @param {unknown} entry The relationship entry: a reference, or an embedded method.
53
+ * @param {string} did The DID the relative reference is resolved against.
54
+ * @returns {string | undefined} The absolute DID URL, or `undefined` if unusable.
55
+ */
56
+ public static relationshipMethodId(entry: unknown, did: string): string | undefined {
57
+ const id = (typeof entry === 'object' && entry !== null && !Array.isArray(entry))
58
+ ? (entry as { id?: unknown }).id
59
+ : entry;
60
+ return Appendix.absoluteDidUrl(id, did);
29
61
  }
30
62
 
31
63
  /**