@did-btcr2/method 0.27.0 → 0.29.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 (114) hide show
  1. package/README.md +38 -9
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/browser.js +20181 -31588
  4. package/dist/browser.mjs +20110 -31517
  5. package/dist/cjs/index.js +1355 -422
  6. package/dist/esm/core/aggregation/beacon-strategy.js +62 -0
  7. package/dist/esm/core/aggregation/beacon-strategy.js.map +1 -0
  8. package/dist/esm/core/aggregation/cohort.js +31 -8
  9. package/dist/esm/core/aggregation/cohort.js.map +1 -1
  10. package/dist/esm/core/aggregation/logger.js +15 -0
  11. package/dist/esm/core/aggregation/logger.js.map +1 -0
  12. package/dist/esm/core/aggregation/messages/base.js +12 -1
  13. package/dist/esm/core/aggregation/messages/base.js.map +1 -1
  14. package/dist/esm/core/aggregation/messages/bodies.js +90 -0
  15. package/dist/esm/core/aggregation/messages/bodies.js.map +1 -0
  16. package/dist/esm/core/aggregation/messages/factories.js.map +1 -1
  17. package/dist/esm/core/aggregation/messages/index.js +1 -0
  18. package/dist/esm/core/aggregation/messages/index.js.map +1 -1
  19. package/dist/esm/core/aggregation/participant.js +39 -46
  20. package/dist/esm/core/aggregation/participant.js.map +1 -1
  21. package/dist/esm/core/aggregation/runner/participant-runner.js +34 -4
  22. package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
  23. package/dist/esm/core/aggregation/runner/service-runner.js +198 -19
  24. package/dist/esm/core/aggregation/runner/service-runner.js.map +1 -1
  25. package/dist/esm/core/aggregation/service.js +143 -15
  26. package/dist/esm/core/aggregation/service.js.map +1 -1
  27. package/dist/esm/core/aggregation/signing-session.js +44 -5
  28. package/dist/esm/core/aggregation/signing-session.js.map +1 -1
  29. package/dist/esm/core/aggregation/transport/didcomm.js +9 -0
  30. package/dist/esm/core/aggregation/transport/didcomm.js.map +1 -1
  31. package/dist/esm/core/aggregation/transport/nostr.js +245 -16
  32. package/dist/esm/core/aggregation/transport/nostr.js.map +1 -1
  33. package/dist/esm/core/beacon/beacon.js +147 -61
  34. package/dist/esm/core/beacon/beacon.js.map +1 -1
  35. package/dist/esm/core/beacon/utils.js +14 -9
  36. package/dist/esm/core/beacon/utils.js.map +1 -1
  37. package/dist/esm/core/updater.js +269 -0
  38. package/dist/esm/core/updater.js.map +1 -0
  39. package/dist/esm/did-btcr2.js +30 -46
  40. package/dist/esm/did-btcr2.js.map +1 -1
  41. package/dist/esm/index.js +4 -1
  42. package/dist/esm/index.js.map +1 -1
  43. package/dist/esm/utils/did-document.js +2 -2
  44. package/dist/esm/utils/did-document.js.map +1 -1
  45. package/dist/types/core/aggregation/beacon-strategy.d.ts +52 -0
  46. package/dist/types/core/aggregation/beacon-strategy.d.ts.map +1 -0
  47. package/dist/types/core/aggregation/cohort.d.ts +20 -3
  48. package/dist/types/core/aggregation/cohort.d.ts.map +1 -1
  49. package/dist/types/core/aggregation/logger.d.ts +22 -0
  50. package/dist/types/core/aggregation/logger.d.ts.map +1 -0
  51. package/dist/types/core/aggregation/messages/base.d.ts +13 -1
  52. package/dist/types/core/aggregation/messages/base.d.ts.map +1 -1
  53. package/dist/types/core/aggregation/messages/bodies.d.ts +130 -0
  54. package/dist/types/core/aggregation/messages/bodies.d.ts.map +1 -0
  55. package/dist/types/core/aggregation/messages/factories.d.ts +1 -0
  56. package/dist/types/core/aggregation/messages/factories.d.ts.map +1 -1
  57. package/dist/types/core/aggregation/messages/index.d.ts +1 -0
  58. package/dist/types/core/aggregation/messages/index.d.ts.map +1 -1
  59. package/dist/types/core/aggregation/participant.d.ts +2 -0
  60. package/dist/types/core/aggregation/participant.d.ts.map +1 -1
  61. package/dist/types/core/aggregation/runner/events.d.ts +32 -6
  62. package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
  63. package/dist/types/core/aggregation/runner/participant-runner.d.ts +8 -2
  64. package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
  65. package/dist/types/core/aggregation/runner/service-runner.d.ts +33 -3
  66. package/dist/types/core/aggregation/runner/service-runner.d.ts.map +1 -1
  67. package/dist/types/core/aggregation/service.d.ts +33 -2
  68. package/dist/types/core/aggregation/service.d.ts.map +1 -1
  69. package/dist/types/core/aggregation/signing-session.d.ts +5 -1
  70. package/dist/types/core/aggregation/signing-session.d.ts.map +1 -1
  71. package/dist/types/core/aggregation/transport/didcomm.d.ts +3 -0
  72. package/dist/types/core/aggregation/transport/didcomm.d.ts.map +1 -1
  73. package/dist/types/core/aggregation/transport/nostr.d.ts +99 -1
  74. package/dist/types/core/aggregation/transport/nostr.d.ts.map +1 -1
  75. package/dist/types/core/aggregation/transport/transport.d.ts +25 -0
  76. package/dist/types/core/aggregation/transport/transport.d.ts.map +1 -1
  77. package/dist/types/core/beacon/beacon.d.ts +85 -18
  78. package/dist/types/core/beacon/beacon.d.ts.map +1 -1
  79. package/dist/types/core/beacon/utils.d.ts +2 -2
  80. package/dist/types/core/beacon/utils.d.ts.map +1 -1
  81. package/dist/types/core/updater.d.ts +178 -0
  82. package/dist/types/core/updater.d.ts.map +1 -0
  83. package/dist/types/did-btcr2.d.ts +23 -23
  84. package/dist/types/did-btcr2.d.ts.map +1 -1
  85. package/dist/types/index.d.ts +4 -1
  86. package/dist/types/index.d.ts.map +1 -1
  87. package/package.json +4 -6
  88. package/src/core/aggregation/beacon-strategy.ts +123 -0
  89. package/src/core/aggregation/cohort.ts +34 -8
  90. package/src/core/aggregation/logger.ts +33 -0
  91. package/src/core/aggregation/messages/base.ts +20 -5
  92. package/src/core/aggregation/messages/bodies.ts +223 -0
  93. package/src/core/aggregation/messages/factories.ts +1 -0
  94. package/src/core/aggregation/messages/index.ts +1 -0
  95. package/src/core/aggregation/participant.ts +40 -46
  96. package/src/core/aggregation/runner/events.ts +27 -3
  97. package/src/core/aggregation/runner/participant-runner.ts +42 -4
  98. package/src/core/aggregation/runner/service-runner.ts +227 -19
  99. package/src/core/aggregation/service.ts +189 -20
  100. package/src/core/aggregation/signing-session.ts +65 -7
  101. package/src/core/aggregation/transport/didcomm.ts +17 -0
  102. package/src/core/aggregation/transport/nostr.ts +266 -23
  103. package/src/core/aggregation/transport/transport.ts +33 -0
  104. package/src/core/beacon/beacon.ts +217 -76
  105. package/src/core/beacon/utils.ts +16 -11
  106. package/src/core/updater.ts +415 -0
  107. package/src/did-btcr2.ts +36 -71
  108. package/src/index.ts +4 -1
  109. package/src/utils/did-document.ts +2 -2
  110. package/dist/esm/core/update.js +0 -112
  111. package/dist/esm/core/update.js.map +0 -1
  112. package/dist/types/core/update.d.ts +0 -52
  113. package/dist/types/core/update.d.ts.map +0 -1
  114. package/src/core/update.ts +0 -158
@@ -0,0 +1,415 @@
1
+ import type { BitcoinConnection } from '@did-btcr2/bitcoin';
2
+ import type { KeyBytes, PatchOperation } from '@did-btcr2/common';
3
+ import { canonicalHash, INVALID_DID_UPDATE, JSONPatch, UpdateError } from '@did-btcr2/common';
4
+ import { SchnorrMultikey, type DataIntegrityConfig, type SignedBTCR2Update, type UnsignedBTCR2Update } from '@did-btcr2/cryptosuite';
5
+ import { DidDocument, type Btcr2DidDocument, type DidVerificationMethod } from '../utils/did-document.js';
6
+ import { BeaconFactory } from './beacon/factory.js';
7
+ import type { BeaconService } from './beacon/interfaces.js';
8
+
9
+ // ─── DataNeed types ──────────────────────────────────────────────────────────
10
+
11
+ /**
12
+ * The updater needs the caller to supply a signing key (or a KMS-backed signature)
13
+ * for the given verification method. The unsigned update is attached so the caller
14
+ * can inspect it before producing a signature.
15
+ */
16
+ export interface NeedSigningKey {
17
+ readonly kind: 'NeedSigningKey';
18
+ /** The verification method ID that requires a signing key. */
19
+ readonly verificationMethodId: string;
20
+ /** The unsigned update that will be signed. */
21
+ readonly unsignedUpdate: UnsignedBTCR2Update;
22
+ }
23
+
24
+ /**
25
+ * The updater needs the caller to ensure the beacon address is funded before
26
+ * broadcasting. The caller checks the beacon address for UTXOs, funds it if
27
+ * needed, and then calls `updater.provide(need)` to continue.
28
+ *
29
+ * If the beacon is already funded, the caller can provide immediately (no-op).
30
+ */
31
+ export interface NeedFunding {
32
+ readonly kind: 'NeedFunding';
33
+ /** The Bitcoin address that must have a spendable UTXO for broadcast. */
34
+ readonly beaconAddress: string;
35
+ /** The beacon service this address belongs to. */
36
+ readonly beaconService: BeaconService;
37
+ }
38
+
39
+ /**
40
+ * The updater needs the caller to broadcast the signed update via the beacon.
41
+ *
42
+ * The caller decides how: for single-party beacons, call
43
+ * `Updater.announce(beaconService, signedUpdate, secretKey, bitcoin)` or
44
+ * `BeaconFactory.establish(beaconService).broadcastSignal(...)`. For multi-party
45
+ * aggregate beacons, hand off to the aggregation protocol.
46
+ *
47
+ * After the broadcast succeeds, the caller calls `updater.provide(need)` (with no
48
+ * data) to transition the updater to Complete.
49
+ */
50
+ export interface NeedBroadcast {
51
+ readonly kind: 'NeedBroadcast';
52
+ /** The beacon service to broadcast through. Inspect `beaconService.type` to decide the path. */
53
+ readonly beaconService: BeaconService;
54
+ /** The signed update ready for broadcast. */
55
+ readonly signedUpdate: SignedBTCR2Update;
56
+ }
57
+
58
+ /** Discriminated union of all data needs the updater may request from the caller. */
59
+ export type UpdaterDataNeed = NeedSigningKey | NeedFunding | NeedBroadcast;
60
+
61
+ /**
62
+ * The result returned by the updater when it reaches the Complete phase.
63
+ */
64
+ export interface UpdaterResult {
65
+ /** The signed update that was constructed, signed, and broadcast. */
66
+ signedUpdate: SignedBTCR2Update;
67
+ }
68
+
69
+ /**
70
+ * Output of {@link Updater.advance}. Either the updater needs data from the
71
+ * caller, or the update is complete.
72
+ */
73
+ export type UpdaterState =
74
+ | { status: 'action-required'; needs: ReadonlyArray<UpdaterDataNeed> }
75
+ | { status: 'complete'; result: UpdaterResult };
76
+
77
+ /**
78
+ * Internal phases of the Updater state machine.
79
+ * @internal
80
+ */
81
+ enum UpdaterPhase {
82
+ Construct = 'Construct',
83
+ Sign = 'Sign',
84
+ Fund = 'Fund',
85
+ Broadcast = 'Broadcast',
86
+ Complete = 'Complete',
87
+ }
88
+
89
+ /**
90
+ * Parameters for constructing an {@link Updater}. Built by
91
+ * {@link https://dcdpr.github.io/did-btcr2/operations/update.html | DidBtcr2.update}.
92
+ */
93
+ export interface UpdaterParams {
94
+ sourceDocument: Btcr2DidDocument;
95
+ patches: PatchOperation[];
96
+ sourceVersionId: number;
97
+ verificationMethod: DidVerificationMethod;
98
+ beaconService: BeaconService;
99
+ }
100
+
101
+ /**
102
+ * Sans-I/O state machine for did:btcr2 updates — the counterpart to {@link Resolver}.
103
+ *
104
+ * Created by {@link DidBtcr2.update} (the factory). The caller drives the update by
105
+ * repeatedly calling {@link advance} and {@link provide}:
106
+ *
107
+ * ```typescript
108
+ * const updater = DidBtcr2.update({ sourceDocument, patches, ... });
109
+ * let state = updater.advance();
110
+ *
111
+ * while(state.status === 'action-required') {
112
+ * for(const need of state.needs) {
113
+ * switch(need.kind) {
114
+ * case 'NeedSigningKey':
115
+ * updater.provide(need, secretKeyBytes);
116
+ * break;
117
+ * case 'NeedFunding':
118
+ * // Check UTXOs at need.beaconAddress, fund if needed
119
+ * updater.provide(need);
120
+ * break;
121
+ * case 'NeedBroadcast':
122
+ * await Updater.announce(need.beaconService, need.signedUpdate, secretKey, bitcoin);
123
+ * updater.provide(need);
124
+ * break;
125
+ * }
126
+ * }
127
+ * state = updater.advance();
128
+ * }
129
+ *
130
+ * const { signedUpdate } = state.result;
131
+ * ```
132
+ *
133
+ * The Updater performs **zero I/O**. All external work (signing with a KMS or raw
134
+ * key, funding checks, Bitcoin transaction construction, broadcast) flows through
135
+ * the advance/provide protocol. This mirrors the {@link Resolver} pattern and makes
136
+ * the update path transport-agnostic and KMS-ready.
137
+ *
138
+ * The class also exposes static utility methods ({@link construct}, {@link sign},
139
+ * {@link announce}) for callers that need direct access to individual update steps
140
+ * outside the state machine (e.g., test vector generation scripts).
141
+ *
142
+ * @class Updater
143
+ */
144
+ export class Updater {
145
+ #phase: UpdaterPhase = UpdaterPhase.Construct;
146
+ readonly #sourceDocument: Btcr2DidDocument;
147
+ readonly #patches: PatchOperation[];
148
+ readonly #sourceVersionId: number;
149
+ readonly #verificationMethod: DidVerificationMethod;
150
+ readonly #beaconService: BeaconService;
151
+
152
+ #unsignedUpdate: UnsignedBTCR2Update | null = null;
153
+ #signedUpdate: SignedBTCR2Update | null = null;
154
+
155
+ /**
156
+ * @internal — Use {@link DidBtcr2.update} to create instances.
157
+ */
158
+ constructor(params: UpdaterParams) {
159
+ this.#sourceDocument = params.sourceDocument;
160
+ this.#patches = params.patches;
161
+ this.#sourceVersionId = params.sourceVersionId;
162
+ this.#verificationMethod = params.verificationMethod;
163
+ this.#beaconService = params.beaconService;
164
+ }
165
+
166
+ // ─── Public static utility methods ─────────────────────────────────────────
167
+ // Used by generate-vector.ts and other scripts that need direct access to
168
+ // individual update steps outside the state machine flow.
169
+
170
+ /**
171
+ * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/update.html#construct-btcr2-unsigned-update | 7.3.b Construct BTCR2 Unsigned Update}.
172
+ *
173
+ * @param {Btcr2DidDocument} sourceDocument The source DID document to be updated.
174
+ * @param {PatchOperation[]} patches The JSON Patch operations to apply.
175
+ * @param {number} sourceVersionId The version ID of the source document.
176
+ * @returns {UnsignedBTCR2Update} The constructed UnsignedBTCR2Update object.
177
+ * @throws {UpdateError} If the target document fails DID Core validation.
178
+ */
179
+ static construct(
180
+ sourceDocument: Btcr2DidDocument,
181
+ patches: PatchOperation[],
182
+ sourceVersionId: number,
183
+ ): UnsignedBTCR2Update {
184
+ const unsignedUpdate: UnsignedBTCR2Update = {
185
+ '@context' : [
186
+ 'https://w3id.org/security/v2',
187
+ 'https://w3id.org/zcap/v1',
188
+ 'https://w3id.org/json-ld-patch/v1',
189
+ 'https://btcr2.dev/context/v1'
190
+ ],
191
+ patch : patches,
192
+ targetHash : '',
193
+ targetVersionId : sourceVersionId + 1,
194
+ sourceHash : canonicalHash(sourceDocument),
195
+ };
196
+
197
+ const targetDocument = JSONPatch.apply(sourceDocument, patches);
198
+
199
+ try {
200
+ DidDocument.isValid(targetDocument);
201
+ } catch (error) {
202
+ throw new UpdateError(
203
+ 'Error validating targetDocument: ' + (error instanceof Error ? error.message : String(error)),
204
+ INVALID_DID_UPDATE, targetDocument
205
+ );
206
+ }
207
+
208
+ unsignedUpdate.targetHash = canonicalHash(targetDocument);
209
+ return unsignedUpdate;
210
+ }
211
+
212
+ /**
213
+ * Implements subsection {@link http://dcdpr.github.io/did-btcr2/operations/update.html#construct-btcr2-signed-update | 7.3.c Construct BTCR2 Signed Update }.
214
+ *
215
+ * @param {string} did The did-btcr2 identifier to derive the root capability from.
216
+ * @param {UnsignedBTCR2Update} unsignedUpdate The unsigned update to sign.
217
+ * @param {DidVerificationMethod} verificationMethod The verification method for signing.
218
+ * @param {KeyBytes} secretKey The secret key bytes.
219
+ * @returns {SignedBTCR2Update} The signed update with a Data Integrity proof.
220
+ */
221
+ static sign(
222
+ did: string,
223
+ unsignedUpdate: UnsignedBTCR2Update,
224
+ verificationMethod: DidVerificationMethod,
225
+ secretKey: KeyBytes,
226
+ ): SignedBTCR2Update {
227
+ const controller = verificationMethod.controller;
228
+ const id = verificationMethod.id.slice(verificationMethod.id.indexOf('#'));
229
+ const multikey = SchnorrMultikey.fromSecretKey(id, controller, secretKey);
230
+
231
+ const config: DataIntegrityConfig = {
232
+ '@context' : [
233
+ 'https://w3id.org/security/v2',
234
+ 'https://w3id.org/zcap/v1',
235
+ 'https://w3id.org/json-ld-patch/v1',
236
+ 'https://btcr2.dev/context/v1'
237
+ ],
238
+ cryptosuite : 'bip340-jcs-2025',
239
+ type : 'DataIntegrityProof',
240
+ verificationMethod : verificationMethod.id,
241
+ proofPurpose : 'capabilityInvocation',
242
+ capability : `urn:zcap:root:${encodeURIComponent(did)}`,
243
+ capabilityAction : 'Write',
244
+ };
245
+
246
+ const diproof = multikey.toCryptosuite().toDataIntegrityProof();
247
+ return diproof.addProof(unsignedUpdate, config);
248
+ }
249
+
250
+ /**
251
+ * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/update.html#announce-did-update | 7.3.d Announce DID Update}.
252
+ * Announces a signed update to the Bitcoin blockchain via the specified beacon.
253
+ *
254
+ * @param {BeaconService} beaconService The beacon service to broadcast through.
255
+ * @param {SignedBTCR2Update} update The signed update to announce.
256
+ * @param {KeyBytes} secretKey The secret key for signing the Bitcoin transaction.
257
+ * @param {BitcoinConnection} bitcoin The Bitcoin network connection.
258
+ * @returns {Promise<SignedBTCR2Update>} The signed update that was broadcast.
259
+ */
260
+ static async announce(
261
+ beaconService: BeaconService,
262
+ update: SignedBTCR2Update,
263
+ secretKey: KeyBytes,
264
+ bitcoin: BitcoinConnection
265
+ ): Promise<SignedBTCR2Update> {
266
+ const beacon = BeaconFactory.establish(beaconService);
267
+ return beacon.broadcastSignal(update, secretKey, bitcoin);
268
+ }
269
+
270
+ // ─── Private instance wrappers ─────────────────────────────────────────────
271
+ // Delegate to the public statics with bound instance fields for cleaner
272
+ // advance/provide code.
273
+
274
+ #construct(): UnsignedBTCR2Update {
275
+ return Updater.construct(this.#sourceDocument, this.#patches, this.#sourceVersionId);
276
+ }
277
+
278
+ #sign(secretKey: KeyBytes): SignedBTCR2Update {
279
+ return Updater.sign(this.#sourceDocument.id, this.#unsignedUpdate!, this.#verificationMethod, secretKey);
280
+ }
281
+
282
+ // ─── State machine ─────────────────────────────────────────────────────────
283
+
284
+ /**
285
+ * Advance the state machine. Returns either:
286
+ * - `{ status: 'action-required', needs }` — caller must provide data via {@link provide}
287
+ * - `{ status: 'complete', result }` — update is signed and broadcast
288
+ */
289
+ advance(): UpdaterState {
290
+ while(true) {
291
+ switch(this.#phase) {
292
+
293
+ // Phase: Construct
294
+ // Build the unsigned update from source doc + patches. Pure, synchronous.
295
+ case UpdaterPhase.Construct: {
296
+ this.#unsignedUpdate = this.#construct();
297
+ this.#phase = UpdaterPhase.Sign;
298
+ continue;
299
+ }
300
+
301
+ // Phase: Sign
302
+ // Emit NeedSigningKey — the caller supplies the secret key (or a KMS signature).
303
+ case UpdaterPhase.Sign: {
304
+ return {
305
+ status : 'action-required',
306
+ needs : [{
307
+ kind : 'NeedSigningKey',
308
+ verificationMethodId : this.#verificationMethod.id,
309
+ unsignedUpdate : this.#unsignedUpdate!,
310
+ }],
311
+ };
312
+ }
313
+
314
+ // Phase: Fund
315
+ // Emit NeedFunding with the beacon address. The caller checks UTXOs,
316
+ // funds the address if needed, and provides to continue.
317
+ case UpdaterPhase.Fund: {
318
+ const beaconAddress = this.#beaconService.serviceEndpoint.replace('bitcoin:', '');
319
+ return {
320
+ status : 'action-required',
321
+ needs : [{
322
+ kind : 'NeedFunding',
323
+ beaconAddress,
324
+ beaconService : this.#beaconService,
325
+ }],
326
+ };
327
+ }
328
+
329
+ // Phase: Broadcast
330
+ // Emit NeedBroadcast with the signed update + beacon service. The caller performs
331
+ // the actual on-chain announcement (or hands off to the aggregation protocol).
332
+ case UpdaterPhase.Broadcast: {
333
+ return {
334
+ status : 'action-required',
335
+ needs : [{
336
+ kind : 'NeedBroadcast',
337
+ beaconService : this.#beaconService,
338
+ signedUpdate : this.#signedUpdate!,
339
+ }],
340
+ };
341
+ }
342
+
343
+ // Phase: Complete
344
+ case UpdaterPhase.Complete: {
345
+ return {
346
+ status : 'complete',
347
+ result : { signedUpdate: this.#signedUpdate! },
348
+ };
349
+ }
350
+ }
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Provide data the updater requested in a previous {@link advance} call.
356
+ * Call once per need, then call {@link advance} again to continue.
357
+ *
358
+ * @param need The DataNeed being fulfilled (from the `needs` array).
359
+ * @param data The data payload corresponding to the need kind (omit for NeedFunding/NeedBroadcast).
360
+ */
361
+ provide(need: NeedSigningKey, data: KeyBytes): void;
362
+ provide(need: NeedFunding): void;
363
+ provide(need: NeedBroadcast): void;
364
+ provide(need: UpdaterDataNeed, data?: KeyBytes): void {
365
+ switch(need.kind) {
366
+ case 'NeedSigningKey': {
367
+ if(this.#phase !== UpdaterPhase.Sign) {
368
+ throw new UpdateError(
369
+ `Cannot provide NeedSigningKey: updater phase is ${this.#phase}, expected Sign.`,
370
+ INVALID_DID_UPDATE, { phase: this.#phase }
371
+ );
372
+ }
373
+ if(!data) {
374
+ throw new UpdateError(
375
+ 'NeedSigningKey requires secret key bytes.',
376
+ INVALID_DID_UPDATE
377
+ );
378
+ }
379
+ if(!this.#unsignedUpdate) {
380
+ throw new UpdateError(
381
+ 'Internal error: unsigned update missing in Sign phase.',
382
+ INVALID_DID_UPDATE
383
+ );
384
+ }
385
+ this.#signedUpdate = this.#sign(data);
386
+ this.#phase = UpdaterPhase.Fund;
387
+ break;
388
+ }
389
+
390
+ case 'NeedFunding': {
391
+ if(this.#phase !== UpdaterPhase.Fund) {
392
+ throw new UpdateError(
393
+ `Cannot provide NeedFunding: updater phase is ${this.#phase}, expected Fund.`,
394
+ INVALID_DID_UPDATE, { phase: this.#phase }
395
+ );
396
+ }
397
+ // Caller has confirmed funding (or it was already funded). Continue.
398
+ this.#phase = UpdaterPhase.Broadcast;
399
+ break;
400
+ }
401
+
402
+ case 'NeedBroadcast': {
403
+ if(this.#phase !== UpdaterPhase.Broadcast) {
404
+ throw new UpdateError(
405
+ `Cannot provide NeedBroadcast: updater phase is ${this.#phase}, expected Broadcast.`,
406
+ INVALID_DID_UPDATE, { phase: this.#phase }
407
+ );
408
+ }
409
+ // Caller has broadcast externally. Transition to Complete.
410
+ this.#phase = UpdaterPhase.Complete;
411
+ break;
412
+ }
413
+ }
414
+ }
415
+ }
package/src/did-btcr2.ts CHANGED
@@ -1,7 +1,5 @@
1
- import type { BitcoinConnection } from '@did-btcr2/bitcoin';
2
1
  import type {
3
2
  DocumentBytes,
4
- HexString,
5
3
  KeyBytes,
6
4
  PatchOperation} from '@did-btcr2/common';
7
5
  import {
@@ -12,7 +10,6 @@ import {
12
10
  MethodError,
13
11
  UpdateError
14
12
  } from '@did-btcr2/common';
15
- import type { SignedBTCR2Update } from '@did-btcr2/cryptosuite';
16
13
  import type {
17
14
  DidMethod} from '@web5/dids';
18
15
  import {
@@ -20,14 +17,11 @@ import {
20
17
  DidError,
21
18
  DidErrorCode
22
19
  } from '@web5/dids';
23
- import * as ecc from '@bitcoinerlab/secp256k1';
24
- import { hexToBytes } from '@noble/hashes/utils';
25
- import { initEccLib } from 'bitcoinjs-lib';
26
20
  import type { BeaconService } from './core/beacon/interfaces.js';
27
21
  import { Identifier } from './core/identifier.js';
28
22
  import type { ResolutionOptions } from './core/interfaces.js';
29
23
  import { Resolver } from './core/resolver.js';
30
- import { Update } from './core/update.js';
24
+ import { Updater } from './core/updater.js';
31
25
  import { Appendix } from './utils/appendix.js';
32
26
  import type { Btcr2DidDocument, DidVerificationMethod } from './utils/did-document.js';
33
27
 
@@ -40,9 +34,6 @@ export interface DidCreateOptions {
40
34
  network?: string;
41
35
  }
42
36
 
43
- /** Initialize secp256k1 ECC library */
44
- initEccLib(ecc);
45
-
46
37
  /**
47
38
  * Implements {@link https://dcdpr.github.io/did-btcr2 | did:btcr2 DID Method Specification}.
48
39
  * did:btcr2 is a censorship-resistant Decentralized Identifier (DID) method using
@@ -140,55 +131,41 @@ export class DidBtcr2 implements DidMethod {
140
131
  }
141
132
 
142
133
  /**
143
- * Entry point for section {@link https://dcdpr.github.io/did-btcr2/#read | 7.3 Update}.
144
- * See specification for the {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#process | Resolve Process}.
145
- * See {@link Update | Update (class)} for class implementation.
134
+ * Entry point for section {@link https://dcdpr.github.io/did-btcr2/#update | 7.3 Update}.
135
+ *
136
+ * Factory method that validates the update parameters and returns a sans-I/O
137
+ * {@link Updater} state machine. The caller drives the updater through its
138
+ * phases (Construct → Sign → Broadcast → Complete) by calling `advance()` and
139
+ * `provide()`. The method package performs **zero I/O** — signing key retrieval
140
+ * (or KMS delegation) and the on-chain broadcast are the caller's responsibility.
146
141
  *
147
- * BTCR2 DID documents can be updated by anchoring BTCR2 Updates to Bitcoin transactions. These transactions MAY be
148
- * published to the Bitcoin network. Any property in the DID document may be updated except the id. Doing so would
149
- * invalidate the DID document.
150
- * @param params An object containing the parameters for the update operation.
142
+ * For a fully-wired version with Bitcoin broadcast and key handling, see
143
+ * `DidMethodApi.update()` in `@did-btcr2/api`.
144
+ *
145
+ * @param params Update construction parameters.
151
146
  * @param {Btcr2DidDocument} params.sourceDocument The DID document being updated.
152
- * @param {PatchOperation[]} params.patches The array of JSON Patch operations to apply to the sourceDocument.
153
- * @param {string} params.sourceVersionId The version ID before applying the update.
154
- * @param {string} params.verificationMethodId The verificationMethod ID to sign the update with.
155
- * @param {string} params.beaconId The beacon ID associated with the update.
156
- * @param {KeyBytes | HexString} [params.signingMaterial] Optional signing material (key bytes or hex string).
157
- * @param {BitcoinConnection} [params.bitcoin] Optional Bitcoin network connection for announcing the update. If not provided, a default connection will be initialized.
158
- * @return {Promise<SignedBTCR2Update>} Promise resolving to the signed BTCR2 update.
159
- * @throws {UpdateError} if no verificationMethod, verificationMethod type is not `Multikey` or the publicKeyMultibase
160
- * header is not `zQ3s`
147
+ * @param {PatchOperation[]} params.patches The JSON Patch operations to apply.
148
+ * @param {number} params.sourceVersionId The version ID before applying the update.
149
+ * @param {string} params.verificationMethodId The verification method ID to sign with.
150
+ * @param {string} params.beaconId The beacon service ID to broadcast through.
151
+ * @returns {Updater} A sans-I/O state machine for driving the update.
152
+ * @throws {UpdateError} If the verification method is not authorized, not found,
153
+ * not of type `Multikey`, or does not have a `zQ3s` publicKeyMultibase prefix.
154
+ * Also throws if the beacon service is not found.
161
155
  */
162
- static async update({
156
+ static update({
163
157
  sourceDocument,
164
158
  patches,
165
159
  sourceVersionId,
166
160
  verificationMethodId,
167
161
  beaconId,
168
- signingMaterial,
169
- bitcoin,
170
162
  }: {
171
163
  sourceDocument: Btcr2DidDocument;
172
164
  patches: PatchOperation[];
173
165
  sourceVersionId: number;
174
166
  verificationMethodId: string;
175
167
  beaconId: string;
176
- signingMaterial?: KeyBytes | HexString;
177
- bitcoin?: BitcoinConnection;
178
- }): Promise<SignedBTCR2Update> {
179
- // If no signingMaterial provided, throw an UpdateError with INVALID_DID_UPDATE.
180
- if (!signingMaterial) {
181
- throw new UpdateError(
182
- 'Missing signing material for update',
183
- INVALID_DID_UPDATE, {signingMaterial}
184
- );
185
- }
186
-
187
- // Convert signingMaterial to bytes if it's a hex string
188
- const secretKey = typeof signingMaterial === 'string'
189
- ? hexToBytes(signingMaterial)
190
- : signingMaterial;
191
-
168
+ }): Updater {
192
169
  // Validate that the verificationMethodId is authorized for capabilityInvocation
193
170
  if(!sourceDocument.capabilityInvocation?.some(vr => vr === verificationMethodId)) {
194
171
  throw new UpdateError(
@@ -201,15 +178,15 @@ export class DidBtcr2 implements DidMethod {
201
178
  const verificationMethod = this.getSigningMethod(sourceDocument, verificationMethodId);
202
179
 
203
180
  // Validate the verificationMethod exists in the sourceDocument
204
- if (!verificationMethod) {
181
+ if(!verificationMethod) {
205
182
  throw new UpdateError(
206
183
  'Invalid verificationMethod: not found in source document',
207
- INVALID_DID_DOCUMENT, {sourceDocument, verificationMethodId}
184
+ INVALID_DID_DOCUMENT, { sourceDocument, verificationMethodId }
208
185
  );
209
186
  }
210
187
 
211
188
  // Validate the verificationMethod is of type 'Multikey'
212
- if (verificationMethod.type !== 'Multikey') {
189
+ if(verificationMethod.type !== 'Multikey') {
213
190
  throw new UpdateError(
214
191
  'Invalid verificationMethod: verificationMethod.type must be "Multikey"',
215
192
  INVALID_DID_DOCUMENT, verificationMethod
@@ -217,46 +194,34 @@ export class DidBtcr2 implements DidMethod {
217
194
  }
218
195
 
219
196
  // Validate the publicKeyMultibase prefix is 'zQ3s'
220
- if (verificationMethod.publicKeyMultibase?.slice(0, 4) !== 'zQ3s') {
197
+ if(verificationMethod.publicKeyMultibase?.slice(0, 4) !== 'zQ3s') {
221
198
  throw new UpdateError(
222
199
  'Invalid verificationMethodId: publicKeyMultibase prefix must start with "zQ3s"',
223
200
  INVALID_DID_DOCUMENT, verificationMethod
224
201
  );
225
202
  }
226
203
 
227
- // Construct an unsigned update following the BTCR2 Update construction algorithm
228
- const update = Update.construct(sourceDocument, patches, sourceVersionId);
229
-
230
- // Sign the unsigned update using the specified verification method
231
- const signed = Update.sign(sourceDocument.id, update, verificationMethod, secretKey);
232
-
233
- // Filter sourceDocument services to get beaconServices matching beaconIds
204
+ // Find the beacon service matching the given beaconId
234
205
  const beaconService = sourceDocument.service
235
206
  .filter((service: BeaconService) => service.id === beaconId)
236
207
  .filter((service: BeaconService): service is BeaconService => !!service)
237
208
  .shift();
238
209
 
239
- // If no matching beacon service found, throw an UpdateError with INVALID_DID_UPDATE.
240
210
  if(!beaconService) {
241
211
  throw new UpdateError(
242
212
  'No beacon service found for provided beaconId',
243
- INVALID_DID_UPDATE, {sourceDocument, beaconId}
213
+ INVALID_DID_UPDATE, { sourceDocument, beaconId }
244
214
  );
245
215
  }
246
- // Require an explicit bitcoin connection — no silent env-var fallback
247
- if (!bitcoin) {
248
- throw new UpdateError(
249
- 'Bitcoin connection required for update. Pass a configured `bitcoin` parameter '
250
- + 'or use DidBtcr2Api which injects it automatically.',
251
- INVALID_DID_UPDATE, { beaconId }
252
- );
253
- }
254
-
255
- // Announce the signed update to the blockchain using the specified beacon(s)
256
- await Update.announce(beaconService, signed, secretKey, bitcoin);
257
216
 
258
- // Return signed update if announced successfully
259
- return signed;
217
+ // Return a sans-I/O state machine the caller will drive
218
+ return new Updater({
219
+ sourceDocument,
220
+ patches,
221
+ sourceVersionId,
222
+ verificationMethod,
223
+ beaconService,
224
+ });
260
225
  }
261
226
 
262
227
  /**
package/src/index.ts CHANGED
@@ -5,6 +5,8 @@ export * from './core/aggregation/cohort.js';
5
5
  export * from './core/aggregation/signing-session.js';
6
6
  export * from './core/aggregation/phases.js';
7
7
  export * from './core/aggregation/errors.js';
8
+ export * from './core/aggregation/beacon-strategy.js';
9
+ export * from './core/aggregation/logger.js';
8
10
  export * from './core/aggregation/messages/index.js';
9
11
  export * from './core/aggregation/transport/index.js';
10
12
  export * from './core/aggregation/runner/index.js';
@@ -14,6 +16,7 @@ export * from './core/beacon/beacon.js';
14
16
  export * from './core/beacon/cas-beacon.js';
15
17
  export * from './core/beacon/error.js';
16
18
  export * from './core/beacon/factory.js';
19
+ export * from './core/beacon/fee-estimator.js';
17
20
  export * from './core/beacon/interfaces.js';
18
21
  export * from './core/beacon/signal-discovery.js';
19
22
  export * from './core/beacon/singleton-beacon.js';
@@ -25,7 +28,7 @@ export * from './core/identifier.js';
25
28
  export * from './core/interfaces.js';
26
29
  export * from './core/resolver.js';
27
30
  export * from './core/types.js';
28
- export * from './core/update.js';
31
+ export * from './core/updater.js';
29
32
 
30
33
  // Utils
31
34
  export * from './utils/appendix.js';
@@ -15,7 +15,7 @@ import {
15
15
  import { CompressedSecp256k1PublicKey } from '@did-btcr2/keypair';
16
16
  import type { DidDocument as W3CDidDocument, DidVerificationMethod as W3CDidVerificationMethod } from '@web5/dids';
17
17
  import { isDidService } from '@web5/dids/utils';
18
- import { payments } from 'bitcoinjs-lib';
18
+ import { p2pkh } from '@scure/btc-signer';
19
19
  import type { BeaconService } from '../core/beacon/interfaces.js';
20
20
  import { Identifier } from '../core/identifier.js';
21
21
  import { Appendix } from './appendix.js';
@@ -491,7 +491,7 @@ export class GenesisDocument extends DidDocument {
491
491
  public static fromPublicKey(publicKey: KeyBytes, network: string): GenesisDocument {
492
492
  const pk = new CompressedSecp256k1PublicKey(publicKey);
493
493
  const id = ID_PLACEHOLDER_VALUE;
494
- const address = payments.p2pkh({ pubkey: pk.compressed, network: getNetwork(network) })?.address;
494
+ const address = p2pkh(pk.compressed, getNetwork(network)).address;
495
495
  const services = [{
496
496
  id : `${id}#service-0`,
497
497
  serviceEndpoint : `bitcoin:${address}`,