@did-btcr2/method 0.29.0 → 0.32.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 (104) hide show
  1. package/README.md +13 -5
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/browser.js +8174 -7157
  4. package/dist/browser.mjs +8174 -7157
  5. package/dist/cjs/index.js +1845 -455
  6. package/dist/esm/core/aggregation/transport/factory.js +15 -6
  7. package/dist/esm/core/aggregation/transport/factory.js.map +1 -1
  8. package/dist/esm/core/aggregation/transport/http/client.js +350 -0
  9. package/dist/esm/core/aggregation/transport/http/client.js.map +1 -0
  10. package/dist/esm/core/aggregation/transport/http/envelope.js +126 -0
  11. package/dist/esm/core/aggregation/transport/http/envelope.js.map +1 -0
  12. package/dist/esm/core/aggregation/transport/http/errors.js +11 -0
  13. package/dist/esm/core/aggregation/transport/http/errors.js.map +1 -0
  14. package/dist/esm/core/aggregation/transport/http/inbox-buffer.js +45 -0
  15. package/dist/esm/core/aggregation/transport/http/inbox-buffer.js.map +1 -0
  16. package/dist/esm/core/aggregation/transport/http/index.js +12 -0
  17. package/dist/esm/core/aggregation/transport/http/index.js.map +1 -0
  18. package/dist/esm/core/aggregation/transport/http/nonce-cache.js +38 -0
  19. package/dist/esm/core/aggregation/transport/http/nonce-cache.js.map +1 -0
  20. package/dist/esm/core/aggregation/transport/http/protocol.js +28 -0
  21. package/dist/esm/core/aggregation/transport/http/protocol.js.map +1 -0
  22. package/dist/esm/core/aggregation/transport/http/rate-limiter.js +45 -0
  23. package/dist/esm/core/aggregation/transport/http/rate-limiter.js.map +1 -0
  24. package/dist/esm/core/aggregation/transport/http/request-auth.js +100 -0
  25. package/dist/esm/core/aggregation/transport/http/request-auth.js.map +1 -0
  26. package/dist/esm/core/aggregation/transport/http/server.js +481 -0
  27. package/dist/esm/core/aggregation/transport/http/server.js.map +1 -0
  28. package/dist/esm/core/aggregation/transport/http/sse-stream.js +110 -0
  29. package/dist/esm/core/aggregation/transport/http/sse-stream.js.map +1 -0
  30. package/dist/esm/core/aggregation/transport/http/sse-writer.js +25 -0
  31. package/dist/esm/core/aggregation/transport/http/sse-writer.js.map +1 -0
  32. package/dist/esm/core/aggregation/transport/index.js +1 -0
  33. package/dist/esm/core/aggregation/transport/index.js.map +1 -1
  34. package/dist/esm/core/beacon/beacon.js +197 -51
  35. package/dist/esm/core/beacon/beacon.js.map +1 -1
  36. package/dist/esm/core/beacon/cas-beacon.js +3 -3
  37. package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
  38. package/dist/esm/core/beacon/singleton-beacon.js +3 -3
  39. package/dist/esm/core/beacon/singleton-beacon.js.map +1 -1
  40. package/dist/esm/core/beacon/smt-beacon.js +3 -3
  41. package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
  42. package/dist/esm/core/updater.js +63 -55
  43. package/dist/esm/core/updater.js.map +1 -1
  44. package/dist/types/core/aggregation/transport/factory.d.ts +22 -7
  45. package/dist/types/core/aggregation/transport/factory.d.ts.map +1 -1
  46. package/dist/types/core/aggregation/transport/http/client.d.ts +48 -0
  47. package/dist/types/core/aggregation/transport/http/client.d.ts.map +1 -0
  48. package/dist/types/core/aggregation/transport/http/envelope.d.ts +64 -0
  49. package/dist/types/core/aggregation/transport/http/envelope.d.ts.map +1 -0
  50. package/dist/types/core/aggregation/transport/http/errors.d.ts +9 -0
  51. package/dist/types/core/aggregation/transport/http/errors.d.ts.map +1 -0
  52. package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts +32 -0
  53. package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts.map +1 -0
  54. package/dist/types/core/aggregation/transport/http/index.d.ts +12 -0
  55. package/dist/types/core/aggregation/transport/http/index.d.ts.map +1 -0
  56. package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts +26 -0
  57. package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts.map +1 -0
  58. package/dist/types/core/aggregation/transport/http/protocol.d.ts +53 -0
  59. package/dist/types/core/aggregation/transport/http/protocol.d.ts.map +1 -0
  60. package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts +41 -0
  61. package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts.map +1 -0
  62. package/dist/types/core/aggregation/transport/http/request-auth.d.ts +50 -0
  63. package/dist/types/core/aggregation/transport/http/request-auth.d.ts.map +1 -0
  64. package/dist/types/core/aggregation/transport/http/server.d.ts +110 -0
  65. package/dist/types/core/aggregation/transport/http/server.d.ts.map +1 -0
  66. package/dist/types/core/aggregation/transport/http/sse-stream.d.ts +34 -0
  67. package/dist/types/core/aggregation/transport/http/sse-stream.d.ts.map +1 -0
  68. package/dist/types/core/aggregation/transport/http/sse-writer.d.ts +12 -0
  69. package/dist/types/core/aggregation/transport/http/sse-writer.d.ts.map +1 -0
  70. package/dist/types/core/aggregation/transport/index.d.ts +1 -0
  71. package/dist/types/core/aggregation/transport/index.d.ts.map +1 -1
  72. package/dist/types/core/aggregation/transport/transport.d.ts +1 -1
  73. package/dist/types/core/aggregation/transport/transport.d.ts.map +1 -1
  74. package/dist/types/core/beacon/beacon.d.ts +72 -12
  75. package/dist/types/core/beacon/beacon.d.ts.map +1 -1
  76. package/dist/types/core/beacon/cas-beacon.d.ts +3 -3
  77. package/dist/types/core/beacon/cas-beacon.d.ts.map +1 -1
  78. package/dist/types/core/beacon/singleton-beacon.d.ts +3 -3
  79. package/dist/types/core/beacon/singleton-beacon.d.ts.map +1 -1
  80. package/dist/types/core/beacon/smt-beacon.d.ts +3 -3
  81. package/dist/types/core/beacon/smt-beacon.d.ts.map +1 -1
  82. package/dist/types/core/updater.d.ts +27 -12
  83. package/dist/types/core/updater.d.ts.map +1 -1
  84. package/package.json +5 -5
  85. package/src/core/aggregation/transport/factory.ts +48 -12
  86. package/src/core/aggregation/transport/http/client.ts +409 -0
  87. package/src/core/aggregation/transport/http/envelope.ts +204 -0
  88. package/src/core/aggregation/transport/http/errors.ts +11 -0
  89. package/src/core/aggregation/transport/http/inbox-buffer.ts +53 -0
  90. package/src/core/aggregation/transport/http/index.ts +11 -0
  91. package/src/core/aggregation/transport/http/nonce-cache.ts +43 -0
  92. package/src/core/aggregation/transport/http/protocol.ts +57 -0
  93. package/src/core/aggregation/transport/http/rate-limiter.ts +75 -0
  94. package/src/core/aggregation/transport/http/request-auth.ts +164 -0
  95. package/src/core/aggregation/transport/http/server.ts +615 -0
  96. package/src/core/aggregation/transport/http/sse-stream.ts +121 -0
  97. package/src/core/aggregation/transport/http/sse-writer.ts +23 -0
  98. package/src/core/aggregation/transport/index.ts +1 -0
  99. package/src/core/aggregation/transport/transport.ts +1 -1
  100. package/src/core/beacon/beacon.ts +255 -64
  101. package/src/core/beacon/cas-beacon.ts +4 -4
  102. package/src/core/beacon/singleton-beacon.ts +4 -4
  103. package/src/core/beacon/smt-beacon.ts +4 -4
  104. package/src/core/updater.ts +113 -67
@@ -1,7 +1,7 @@
1
1
  import type { BitcoinConnection } from '@did-btcr2/bitcoin';
2
- import type { KeyBytes } from '@did-btcr2/common';
3
2
  import { canonicalize, hash } from '@did-btcr2/common';
4
3
  import type { SignedBTCR2Update } from '@did-btcr2/cryptosuite';
4
+ import type { Signer } from '@did-btcr2/keypair';
5
5
  import type { BeaconProcessResult, DataNeed } from '../resolver.js';
6
6
  import type { SidecarData } from '../types.js';
7
7
  import type { BroadcastOptions } from './beacon.js';
@@ -67,7 +67,7 @@ export class SingletonBeacon extends Beacon {
67
67
  * {@link Beacon.buildSignAndBroadcast}.
68
68
  *
69
69
  * @param {SignedBTCR2Update} signedUpdate The signed BTCR2 update to broadcast.
70
- * @param {KeyBytes} secretKey The secret key for signing the Bitcoin transaction.
70
+ * @param {Signer} signer Signer that produces the ECDSA signature for the Bitcoin transaction.
71
71
  * @param {BitcoinConnection} bitcoin The Bitcoin network connection.
72
72
  * @param {BroadcastOptions} [options] Optional broadcast configuration (e.g. fee estimator).
73
73
  * @returns {Promise<SignedBTCR2Update>} The signed update that was broadcast.
@@ -75,12 +75,12 @@ export class SingletonBeacon extends Beacon {
75
75
  */
76
76
  async broadcastSignal(
77
77
  signedUpdate: SignedBTCR2Update,
78
- secretKey: KeyBytes,
78
+ signer: Signer,
79
79
  bitcoin: BitcoinConnection,
80
80
  options?: BroadcastOptions
81
81
  ): Promise<SignedBTCR2Update> {
82
82
  const signalBytes = hash(canonicalize(signedUpdate));
83
- await this.buildSignAndBroadcast(signalBytes, secretKey, bitcoin, options);
83
+ await this.buildSignAndBroadcast(signalBytes, signer, bitcoin, options);
84
84
  return signedUpdate;
85
85
  }
86
86
  }
@@ -1,7 +1,7 @@
1
1
  import type { BitcoinConnection } from '@did-btcr2/bitcoin';
2
2
  import { canonicalize } from '@did-btcr2/common';
3
- import type { KeyBytes } from '@did-btcr2/common';
4
3
  import type { SignedBTCR2Update } from '@did-btcr2/cryptosuite';
4
+ import type { Signer } from '@did-btcr2/keypair';
5
5
  import { blockHash, BTCR2MerkleTree, didToIndex, hexToHash, verifySerializedProof } from '@did-btcr2/smt';
6
6
  import { randomBytes } from '@noble/hashes/utils';
7
7
  import type { BeaconProcessResult, DataNeed } from '../resolver.js';
@@ -122,7 +122,7 @@ export class SMTBeacon extends Beacon {
122
122
  * signing, and broadcast are delegated to {@link Beacon.buildSignAndBroadcast}.
123
123
  *
124
124
  * @param {SignedBTCR2Update} signedUpdate The signed BTCR2 update to broadcast.
125
- * @param {KeyBytes} secretKey The secret key for signing the Bitcoin transaction.
125
+ * @param {Signer} signer Signer that produces the ECDSA signature for the Bitcoin transaction.
126
126
  * @param {BitcoinConnection} bitcoin The Bitcoin network connection.
127
127
  * @param {BroadcastOptions} [options] Optional broadcast configuration (e.g. fee estimator).
128
128
  * @return {Promise<SignedBTCR2Update>} The signed update that was broadcast.
@@ -130,7 +130,7 @@ export class SMTBeacon extends Beacon {
130
130
  */
131
131
  async broadcastSignal(
132
132
  signedUpdate: SignedBTCR2Update,
133
- secretKey: KeyBytes,
133
+ signer: Signer,
134
134
  bitcoin: BitcoinConnection,
135
135
  options?: BroadcastOptions
136
136
  ): Promise<SignedBTCR2Update> {
@@ -145,7 +145,7 @@ export class SMTBeacon extends Beacon {
145
145
  tree.finalize();
146
146
 
147
147
  // Root hash is the signal bytes for the OP_RETURN output
148
- await this.buildSignAndBroadcast(tree.rootHash, secretKey, bitcoin, options);
148
+ await this.buildSignAndBroadcast(tree.rootHash, signer, bitcoin, options);
149
149
 
150
150
  return signedUpdate;
151
151
  }
@@ -1,7 +1,8 @@
1
1
  import type { BitcoinConnection } from '@did-btcr2/bitcoin';
2
- import type { KeyBytes, PatchOperation } from '@did-btcr2/common';
2
+ import type { PatchOperation } from '@did-btcr2/common';
3
3
  import { canonicalHash, INVALID_DID_UPDATE, JSONPatch, UpdateError } from '@did-btcr2/common';
4
4
  import { SchnorrMultikey, type DataIntegrityConfig, type SignedBTCR2Update, type UnsignedBTCR2Update } from '@did-btcr2/cryptosuite';
5
+ import type { Signer } from '@did-btcr2/keypair';
5
6
  import { DidDocument, type Btcr2DidDocument, type DidVerificationMethod } from '../utils/did-document.js';
6
7
  import { BeaconFactory } from './beacon/factory.js';
7
8
  import type { BeaconService } from './beacon/interfaces.js';
@@ -9,9 +10,10 @@ import type { BeaconService } from './beacon/interfaces.js';
9
10
  // ─── DataNeed types ──────────────────────────────────────────────────────────
10
11
 
11
12
  /**
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.
13
+ * The updater needs the caller to supply a {@link Signer} for the given
14
+ * verification method. The unsigned update is attached so the caller can
15
+ * inspect it before producing a signature. The signer can wrap a local secret
16
+ * key (`LocalSigner`), a KMS-managed key (`KeyManagerSigner`), or any custom backend.
15
17
  */
16
18
  export interface NeedSigningKey {
17
19
  readonly kind: 'NeedSigningKey';
@@ -36,6 +38,19 @@ export interface NeedFunding {
36
38
  readonly beaconService: BeaconService;
37
39
  }
38
40
 
41
+ /**
42
+ * Optional proof the caller passes when fulfilling {@link NeedFunding}. The
43
+ * state machine asserts the proof before transitioning to Broadcast. Sans-I/O
44
+ * is preserved: the caller still performs the UTXO lookup; this is just a
45
+ * contract-level handshake.
46
+ */
47
+ export interface FundingProof {
48
+ /** Number of spendable UTXOs the caller observed at the beacon address. Must be >= 1. */
49
+ utxoCount: number;
50
+ /** Optional txid the caller funded with, for diagnostics. */
51
+ txid?: string;
52
+ }
53
+
39
54
  /**
40
55
  * The updater needs the caller to broadcast the signed update via the beacon.
41
56
  *
@@ -75,16 +90,18 @@ export type UpdaterState =
75
90
  | { status: 'complete'; result: UpdaterResult };
76
91
 
77
92
  /**
78
- * Internal phases of the Updater state machine.
93
+ * Discriminated union of the updater's internal state. Each phase tag pins the
94
+ * exact set of values the state machine has computed so far, so consumers of
95
+ * `#state` narrow correctly under `switch (this.#state.phase)`. No nullable
96
+ * scratch slots, no `!`-asserts.
79
97
  * @internal
80
98
  */
81
- enum UpdaterPhase {
82
- Construct = 'Construct',
83
- Sign = 'Sign',
84
- Fund = 'Fund',
85
- Broadcast = 'Broadcast',
86
- Complete = 'Complete',
87
- }
99
+ type InternalState =
100
+ | { phase: 'Construct' }
101
+ | { phase: 'Sign'; unsignedUpdate: UnsignedBTCR2Update }
102
+ | { phase: 'Fund'; unsignedUpdate: UnsignedBTCR2Update; signedUpdate: SignedBTCR2Update }
103
+ | { phase: 'Broadcast'; unsignedUpdate: UnsignedBTCR2Update; signedUpdate: SignedBTCR2Update }
104
+ | { phase: 'Complete'; signedUpdate: SignedBTCR2Update };
88
105
 
89
106
  /**
90
107
  * Parameters for constructing an {@link Updater}. Built by
@@ -106,20 +123,21 @@ export interface UpdaterParams {
106
123
  *
107
124
  * ```typescript
108
125
  * const updater = DidBtcr2.update({ sourceDocument, patches, ... });
126
+ * const signer = new LocalSigner(secretKeyBytes); // or KeyManagerSigner / custom
109
127
  * let state = updater.advance();
110
128
  *
111
129
  * while(state.status === 'action-required') {
112
130
  * for(const need of state.needs) {
113
131
  * switch(need.kind) {
114
132
  * case 'NeedSigningKey':
115
- * updater.provide(need, secretKeyBytes);
133
+ * updater.provide(need, signer);
116
134
  * break;
117
135
  * case 'NeedFunding':
118
136
  * // Check UTXOs at need.beaconAddress, fund if needed
119
137
  * updater.provide(need);
120
138
  * break;
121
139
  * case 'NeedBroadcast':
122
- * await Updater.announce(need.beaconService, need.signedUpdate, secretKey, bitcoin);
140
+ * await Updater.announce(need.beaconService, need.signedUpdate, signer, bitcoin);
123
141
  * updater.provide(need);
124
142
  * break;
125
143
  * }
@@ -142,16 +160,13 @@ export interface UpdaterParams {
142
160
  * @class Updater
143
161
  */
144
162
  export class Updater {
145
- #phase: UpdaterPhase = UpdaterPhase.Construct;
163
+ #state: InternalState = { phase: 'Construct' };
146
164
  readonly #sourceDocument: Btcr2DidDocument;
147
165
  readonly #patches: PatchOperation[];
148
166
  readonly #sourceVersionId: number;
149
167
  readonly #verificationMethod: DidVerificationMethod;
150
168
  readonly #beaconService: BeaconService;
151
169
 
152
- #unsignedUpdate: UnsignedBTCR2Update | null = null;
153
- #signedUpdate: SignedBTCR2Update | null = null;
154
-
155
170
  /**
156
171
  * @internal — Use {@link DidBtcr2.update} to create instances.
157
172
  */
@@ -196,6 +211,16 @@ export class Updater {
196
211
 
197
212
  const targetDocument = JSONPatch.apply(sourceDocument, patches);
198
213
 
214
+ // Spec (operations/update.md): "An INVALID_DID_UPDATE error MUST be raised if
215
+ // didTargetDocument.id is not equal to didSourceDocument.id." `DidDocument.isValid`
216
+ // checks W3C conformance but not this equality, so it's enforced explicitly here.
217
+ if(targetDocument.id !== sourceDocument.id) {
218
+ throw new UpdateError(
219
+ `Patches must not change the DID document id (source "${sourceDocument.id}" → target "${targetDocument.id}").`,
220
+ INVALID_DID_UPDATE, { sourceId: sourceDocument.id, targetId: targetDocument.id }
221
+ );
222
+ }
223
+
199
224
  try {
200
225
  DidDocument.isValid(targetDocument);
201
226
  } catch (error) {
@@ -215,18 +240,31 @@ export class Updater {
215
240
  * @param {string} did The did-btcr2 identifier to derive the root capability from.
216
241
  * @param {UnsignedBTCR2Update} unsignedUpdate The unsigned update to sign.
217
242
  * @param {DidVerificationMethod} verificationMethod The verification method for signing.
218
- * @param {KeyBytes} secretKey The secret key bytes.
243
+ * @param {Signer} signer Signer that produces the BIP-340 Schnorr signature.
219
244
  * @returns {SignedBTCR2Update} The signed update with a Data Integrity proof.
220
245
  */
221
246
  static sign(
222
247
  did: string,
223
248
  unsignedUpdate: UnsignedBTCR2Update,
224
249
  verificationMethod: DidVerificationMethod,
225
- secretKey: KeyBytes,
250
+ signer: Signer,
226
251
  ): SignedBTCR2Update {
252
+ if(!did.startsWith('did:btcr2:')) {
253
+ throw new UpdateError(
254
+ `Expected a did:btcr2 identifier for the root capability; got "${did}".`,
255
+ INVALID_DID_UPDATE, { did }
256
+ );
257
+ }
227
258
  const controller = verificationMethod.controller;
228
- const id = verificationMethod.id.slice(verificationMethod.id.indexOf('#'));
229
- const multikey = SchnorrMultikey.fromSecretKey(id, controller, secretKey);
259
+ const hashIdx = verificationMethod.id.indexOf('#');
260
+ if(hashIdx < 0) {
261
+ throw new UpdateError(
262
+ `Verification method id must contain a fragment (e.g. "${verificationMethod.id}#initialKey"); got "${verificationMethod.id}".`,
263
+ INVALID_DID_UPDATE, { verificationMethodId: verificationMethod.id }
264
+ );
265
+ }
266
+ const id = verificationMethod.id.slice(hashIdx);
267
+ const multikey = SchnorrMultikey.fromSigner(id, controller, signer);
230
268
 
231
269
  const config: DataIntegrityConfig = {
232
270
  '@context' : [
@@ -253,21 +291,21 @@ export class Updater {
253
291
  *
254
292
  * @param {BeaconService} beaconService The beacon service to broadcast through.
255
293
  * @param {SignedBTCR2Update} update The signed update to announce.
256
- * @param {KeyBytes} secretKey The secret key for signing the Bitcoin transaction.
294
+ * @param {Signer} signer Signer that produces the ECDSA signature for the Bitcoin transaction.
257
295
  * @param {BitcoinConnection} bitcoin The Bitcoin network connection.
258
296
  * @returns {Promise<SignedBTCR2Update>} The signed update that was broadcast.
259
297
  */
260
298
  static async announce(
261
299
  beaconService: BeaconService,
262
300
  update: SignedBTCR2Update,
263
- secretKey: KeyBytes,
301
+ signer: Signer,
264
302
  bitcoin: BitcoinConnection
265
303
  ): Promise<SignedBTCR2Update> {
266
304
  const beacon = BeaconFactory.establish(beaconService);
267
- return beacon.broadcastSignal(update, secretKey, bitcoin);
305
+ return beacon.broadcastSignal(update, signer, bitcoin);
268
306
  }
269
307
 
270
- // ─── Private instance wrappers ─────────────────────────────────────────────
308
+ // Private instance wrappers
271
309
  // Delegate to the public statics with bound instance fields for cleaner
272
310
  // advance/provide code.
273
311
 
@@ -275,12 +313,6 @@ export class Updater {
275
313
  return Updater.construct(this.#sourceDocument, this.#patches, this.#sourceVersionId);
276
314
  }
277
315
 
278
- #sign(secretKey: KeyBytes): SignedBTCR2Update {
279
- return Updater.sign(this.#sourceDocument.id, this.#unsignedUpdate!, this.#verificationMethod, secretKey);
280
- }
281
-
282
- // ─── State machine ─────────────────────────────────────────────────────────
283
-
284
316
  /**
285
317
  * Advance the state machine. Returns either:
286
318
  * - `{ status: 'action-required', needs }` — caller must provide data via {@link provide}
@@ -288,25 +320,25 @@ export class Updater {
288
320
  */
289
321
  advance(): UpdaterState {
290
322
  while(true) {
291
- switch(this.#phase) {
323
+ switch(this.#state.phase) {
292
324
 
293
325
  // Phase: Construct
294
326
  // Build the unsigned update from source doc + patches. Pure, synchronous.
295
- case UpdaterPhase.Construct: {
296
- this.#unsignedUpdate = this.#construct();
297
- this.#phase = UpdaterPhase.Sign;
327
+ case 'Construct': {
328
+ const unsignedUpdate = this.#construct();
329
+ this.#state = { phase: 'Sign', unsignedUpdate };
298
330
  continue;
299
331
  }
300
332
 
301
333
  // Phase: Sign
302
334
  // Emit NeedSigningKey — the caller supplies the secret key (or a KMS signature).
303
- case UpdaterPhase.Sign: {
335
+ case 'Sign': {
304
336
  return {
305
337
  status : 'action-required',
306
338
  needs : [{
307
339
  kind : 'NeedSigningKey',
308
340
  verificationMethodId : this.#verificationMethod.id,
309
- unsignedUpdate : this.#unsignedUpdate!,
341
+ unsignedUpdate : this.#state.unsignedUpdate,
310
342
  }],
311
343
  };
312
344
  }
@@ -314,7 +346,7 @@ export class Updater {
314
346
  // Phase: Fund
315
347
  // Emit NeedFunding with the beacon address. The caller checks UTXOs,
316
348
  // funds the address if needed, and provides to continue.
317
- case UpdaterPhase.Fund: {
349
+ case 'Fund': {
318
350
  const beaconAddress = this.#beaconService.serviceEndpoint.replace('bitcoin:', '');
319
351
  return {
320
352
  status : 'action-required',
@@ -329,22 +361,22 @@ export class Updater {
329
361
  // Phase: Broadcast
330
362
  // Emit NeedBroadcast with the signed update + beacon service. The caller performs
331
363
  // the actual on-chain announcement (or hands off to the aggregation protocol).
332
- case UpdaterPhase.Broadcast: {
364
+ case 'Broadcast': {
333
365
  return {
334
366
  status : 'action-required',
335
367
  needs : [{
336
368
  kind : 'NeedBroadcast',
337
369
  beaconService : this.#beaconService,
338
- signedUpdate : this.#signedUpdate!,
370
+ signedUpdate : this.#state.signedUpdate,
339
371
  }],
340
372
  };
341
373
  }
342
374
 
343
375
  // Phase: Complete
344
- case UpdaterPhase.Complete: {
376
+ case 'Complete': {
345
377
  return {
346
378
  status : 'complete',
347
- result : { signedUpdate: this.#signedUpdate! },
379
+ result : { signedUpdate: this.#state.signedUpdate },
348
380
  };
349
381
  }
350
382
  }
@@ -358,56 +390,70 @@ export class Updater {
358
390
  * @param need The DataNeed being fulfilled (from the `needs` array).
359
391
  * @param data The data payload corresponding to the need kind (omit for NeedFunding/NeedBroadcast).
360
392
  */
361
- provide(need: NeedSigningKey, data: KeyBytes): void;
362
- provide(need: NeedFunding): void;
393
+ provide(need: NeedSigningKey, data: Signer): void;
394
+ provide(need: NeedFunding, proof?: FundingProof): void;
363
395
  provide(need: NeedBroadcast): void;
364
- provide(need: UpdaterDataNeed, data?: KeyBytes): void {
396
+ provide(need: UpdaterDataNeed, data?: Signer | FundingProof): void {
365
397
  switch(need.kind) {
366
398
  case 'NeedSigningKey': {
367
- if(this.#phase !== UpdaterPhase.Sign) {
399
+ if(this.#state.phase !== 'Sign') {
368
400
  throw new UpdateError(
369
- `Cannot provide NeedSigningKey: updater phase is ${this.#phase}, expected Sign.`,
370
- INVALID_DID_UPDATE, { phase: this.#phase }
401
+ `Cannot provide NeedSigningKey: updater phase is ${this.#state.phase}, expected Sign.`,
402
+ INVALID_DID_UPDATE, { phase: this.#state.phase }
371
403
  );
372
404
  }
373
405
  if(!data) {
374
406
  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.',
407
+ 'NeedSigningKey requires a Signer.',
382
408
  INVALID_DID_UPDATE
383
409
  );
384
410
  }
385
- this.#signedUpdate = this.#sign(data);
386
- this.#phase = UpdaterPhase.Fund;
411
+ const unsignedUpdate = this.#state.unsignedUpdate;
412
+ const signedUpdate = Updater.sign(
413
+ this.#sourceDocument.id, unsignedUpdate, this.#verificationMethod, data as Signer,
414
+ );
415
+ this.#state = { phase: 'Fund', unsignedUpdate, signedUpdate };
387
416
  break;
388
417
  }
389
418
 
390
419
  case 'NeedFunding': {
391
- if(this.#phase !== UpdaterPhase.Fund) {
420
+ if(this.#state.phase !== 'Fund') {
392
421
  throw new UpdateError(
393
- `Cannot provide NeedFunding: updater phase is ${this.#phase}, expected Fund.`,
394
- INVALID_DID_UPDATE, { phase: this.#phase }
422
+ `Cannot provide NeedFunding: updater phase is ${this.#state.phase}, expected Fund.`,
423
+ INVALID_DID_UPDATE, { phase: this.#state.phase }
395
424
  );
396
425
  }
397
- // Caller has confirmed funding (or it was already funded). Continue.
398
- this.#phase = UpdaterPhase.Broadcast;
426
+ // If the caller supplies a FundingProof, assert it before transitioning.
427
+ // Optional payload preserves the sans-I/O contract: the caller still does
428
+ // the actual UTXO lookup; this is a contract-level handshake that catches
429
+ // a class of caller bugs (forgot to fund, race with mempool, etc.) at the
430
+ // state-machine boundary rather than at broadcast time.
431
+ if(data !== undefined) {
432
+ const proof = data as FundingProof;
433
+ if(typeof proof.utxoCount !== 'number' || !Number.isFinite(proof.utxoCount) || proof.utxoCount < 1) {
434
+ throw new UpdateError(
435
+ `NeedFunding proof must have utxoCount >= 1; got ${String(proof.utxoCount)}.`,
436
+ INVALID_DID_UPDATE, { utxoCount: proof.utxoCount }
437
+ );
438
+ }
439
+ }
440
+ this.#state = {
441
+ phase : 'Broadcast',
442
+ unsignedUpdate : this.#state.unsignedUpdate,
443
+ signedUpdate : this.#state.signedUpdate,
444
+ };
399
445
  break;
400
446
  }
401
447
 
402
448
  case 'NeedBroadcast': {
403
- if(this.#phase !== UpdaterPhase.Broadcast) {
449
+ if(this.#state.phase !== 'Broadcast') {
404
450
  throw new UpdateError(
405
- `Cannot provide NeedBroadcast: updater phase is ${this.#phase}, expected Broadcast.`,
406
- INVALID_DID_UPDATE, { phase: this.#phase }
451
+ `Cannot provide NeedBroadcast: updater phase is ${this.#state.phase}, expected Broadcast.`,
452
+ INVALID_DID_UPDATE, { phase: this.#state.phase }
407
453
  );
408
454
  }
409
455
  // Caller has broadcast externally. Transition to Complete.
410
- this.#phase = UpdaterPhase.Complete;
456
+ this.#state = { phase: 'Complete', signedUpdate: this.#state.signedUpdate };
411
457
  break;
412
458
  }
413
459
  }