@did-btcr2/api 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/api.js CHANGED
@@ -1,139 +1,802 @@
1
- import { BitcoinCoreRpcClient, BitcoinRestClient } from '@did-btcr2/bitcoin';
2
- import { DEFAULT_BLOCK_CONFIRMATIONS, DEFAULT_REST_CONFIG, DEFAULT_RPC_CONFIG, IdentifierTypes, NotImplementedError } from '@did-btcr2/common';
3
- import { SchnorrMultikey } from '@did-btcr2/cryptosuite';
4
- import { SchnorrKeyPair, Secp256k1SecretKey } from '@did-btcr2/keypair';
1
+ import { BitcoinConnection } from '@did-btcr2/bitcoin';
2
+ import { IdentifierTypes, NotImplementedError } from '@did-btcr2/common';
3
+ import { BIP340Cryptosuite, BIP340DataIntegrityProof, SchnorrMultikey } from '@did-btcr2/cryptosuite';
4
+ import { CompressedSecp256k1PublicKey, SchnorrKeyPair, Secp256k1SecretKey } from '@did-btcr2/keypair';
5
+ import { Kms, } from '@did-btcr2/kms';
5
6
  import { DidBtcr2, DidDocument, DidDocumentBuilder, Identifier } from '@did-btcr2/method';
7
+ import { Did } from '@web5/dids';
6
8
  export { DidDocument, DidDocumentBuilder, Identifier, IdentifierTypes };
7
- /* =========================
8
- * Sub-facade: KeyPair
9
- * ========================= */
9
+ const noopFn = () => { };
10
+ /** @internal */
11
+ const NOOP_LOGGER = {
12
+ debug: noopFn,
13
+ info: noopFn,
14
+ warn: noopFn,
15
+ error: noopFn,
16
+ };
17
+ // ---------------------------------------------------------------------------
18
+ // Validation helpers (module-private)
19
+ // ---------------------------------------------------------------------------
20
+ /** @internal */
21
+ function assertString(value, name) {
22
+ if (typeof value !== 'string' || value.length === 0) {
23
+ throw new Error(`${name} must be a non-empty string.`);
24
+ }
25
+ }
26
+ /** @internal */
27
+ function assertBytes(value, name) {
28
+ if (!(value instanceof Uint8Array) || value.length === 0) {
29
+ throw new Error(`${name} must be a non-empty Uint8Array.`);
30
+ }
31
+ }
32
+ /** @internal */
33
+ function assertCompressedPubkey(value, name) {
34
+ assertBytes(value, name);
35
+ if (value.length !== 33) {
36
+ throw new Error(`${name} must be a 33-byte compressed public key, got ${value.length} bytes.`);
37
+ }
38
+ }
39
+ // ---------------------------------------------------------------------------
40
+ // KeyPair sub-facade
41
+ // ---------------------------------------------------------------------------
42
+ /**
43
+ * Schnorr keypair operations.
44
+ * @public
45
+ */
10
46
  export class KeyPairApi {
11
- /** Generate a new Schnorr keypair (secp256k1). */
12
- static generate() {
13
- return new SchnorrKeyPair();
47
+ /**
48
+ * Generate a new Schnorr keypair.
49
+ * @returns The generated Schnorr keypair.
50
+ */
51
+ generate() {
52
+ return SchnorrKeyPair.generate();
53
+ }
54
+ /**
55
+ * Create a Schnorr keypair from secret key bytes or hex string.
56
+ * @param data The secret key bytes or hex string.
57
+ * @returns The created Schnorr keypair.
58
+ */
59
+ fromSecret(data) {
60
+ return SchnorrKeyPair.fromSecret(data);
61
+ }
62
+ /** Create a secret key from entropy (bytes or bigint). */
63
+ secretKeyFrom(ent) {
64
+ return new Secp256k1SecretKey(ent);
65
+ }
66
+ /** Create a compressed public key from bytes. */
67
+ publicKeyFrom(byt) {
68
+ return new CompressedSecp256k1PublicKey(byt);
69
+ }
70
+ /** Deserialize a keypair from a JSON object. */
71
+ fromJSON(obj) {
72
+ return SchnorrKeyPair.fromJSON(obj);
73
+ }
74
+ /** Serialize a keypair to a JSON object. */
75
+ toJSON(kp) {
76
+ return kp.exportJSON();
77
+ }
78
+ /** Compare two keypairs for equality. */
79
+ equals(kp1, kp2) {
80
+ return SchnorrKeyPair.equals(kp1, kp2);
81
+ }
82
+ }
83
+ // ---------------------------------------------------------------------------
84
+ // Cryptosuite sub-facade
85
+ // ---------------------------------------------------------------------------
86
+ /**
87
+ * Schnorr cryptosuite operations.
88
+ *
89
+ * Optionally stateful: call {@link use} to set a current cryptosuite, then
90
+ * call {@link createProof}, {@link verifyProof}, or {@link toDataIntegrityProof}
91
+ * without passing an explicit instance. Pass an explicit instance to any
92
+ * method to override the current one for that call.
93
+ * @public
94
+ */
95
+ export class CryptosuiteApi {
96
+ #current;
97
+ /** The currently active cryptosuite, or `undefined` if none is set. */
98
+ get current() {
99
+ return this.#current;
100
+ }
101
+ /**
102
+ * Set the current cryptosuite for subsequent operations.
103
+ * @param cs The cryptosuite to activate.
104
+ * @returns `this` for chaining.
105
+ */
106
+ use(cs) {
107
+ this.#current = cs;
108
+ return this;
109
+ }
110
+ /** Clear the current cryptosuite. */
111
+ clear() {
112
+ this.#current = undefined;
113
+ }
114
+ /**
115
+ * Create a new Schnorr cryptosuite from a multikey.
116
+ * @param multikey The Schnorr multikey to use.
117
+ * @returns The created Schnorr cryptosuite.
118
+ */
119
+ create(multikey) {
120
+ return new BIP340Cryptosuite(multikey);
121
+ }
122
+ /**
123
+ * Convenience: resolve a key from the KMS and create a cryptosuite in one step.
124
+ * @param id The multikey ID (e.g. '#initialKey').
125
+ * @param controller The DID that controls this key.
126
+ * @param keyId The KMS key identifier to resolve.
127
+ * @param kms The KeyManagerApi instance holding the key.
128
+ * @returns The created Schnorr cryptosuite.
129
+ */
130
+ createFromKms(id, controller, keyId, kms) {
131
+ const pubBytes = kms.getPublicKey(keyId);
132
+ const mk = SchnorrMultikey.fromPublicKey({ id, controller, publicKeyBytes: pubBytes });
133
+ return new BIP340Cryptosuite(mk);
134
+ }
135
+ /**
136
+ * Convert a cryptosuite to a Data Integrity Proof instance.
137
+ * Uses the current cryptosuite when `cryptosuite` is omitted.
138
+ * @param cryptosuite Optional explicit cryptosuite to convert.
139
+ * @returns The Data Integrity Proof instance.
140
+ */
141
+ toDataIntegrityProof(cryptosuite) {
142
+ const cs = cryptosuite ?? this.#requireCurrent();
143
+ return cs.toDataIntegrityProof();
144
+ }
145
+ /**
146
+ * Create a proof for a document.
147
+ * Uses the current cryptosuite when `cryptosuite` is omitted.
148
+ * @param document The document to create the proof for.
149
+ * @param config Configuration for the proof creation.
150
+ * @param cryptosuite Optional explicit cryptosuite; defaults to current.
151
+ * @returns The created proof.
152
+ */
153
+ createProof(document, config, cryptosuite) {
154
+ const cs = cryptosuite ?? this.#requireCurrent();
155
+ return cs.createProof(document, config);
156
+ }
157
+ /**
158
+ * Verify a proof for a document.
159
+ * Uses the current cryptosuite when `cryptosuite` is omitted.
160
+ * @param document The document to verify the proof for.
161
+ * @param cryptosuite Optional explicit cryptosuite; defaults to current.
162
+ * @returns The full verification result.
163
+ */
164
+ verifyProof(document, cryptosuite) {
165
+ const cs = cryptosuite ?? this.#requireCurrent();
166
+ return cs.verifyProof(document);
167
+ }
168
+ #requireCurrent() {
169
+ if (!this.#current) {
170
+ throw new Error('No current cryptosuite set. Call cryptosuite.use(cs) first, or pass an explicit instance.');
171
+ }
172
+ return this.#current;
173
+ }
174
+ }
175
+ // ---------------------------------------------------------------------------
176
+ // Data Integrity Proof sub-facade
177
+ // ---------------------------------------------------------------------------
178
+ /**
179
+ * Data Integrity Proof operations.
180
+ *
181
+ * Optionally stateful: call {@link use} to set a current proof instance, then
182
+ * call {@link addProof} or {@link verifyProof} without passing an explicit
183
+ * instance. Pass an explicit instance to override for that call.
184
+ * @public
185
+ */
186
+ export class DataIntegrityProofApi {
187
+ #current;
188
+ /** The currently active proof instance, or `undefined` if none is set. */
189
+ get current() {
190
+ return this.#current;
191
+ }
192
+ /**
193
+ * Set the current proof instance for subsequent operations.
194
+ * @param p The proof instance to activate.
195
+ * @returns `this` for chaining.
196
+ */
197
+ use(p) {
198
+ this.#current = p;
199
+ return this;
200
+ }
201
+ /** Clear the current proof instance. */
202
+ clear() {
203
+ this.#current = undefined;
204
+ }
205
+ /**
206
+ * Create a BIP340DataIntegrityProof instance with the given cryptosuite.
207
+ * @param cryptosuite The cryptosuite to use for proof operations.
208
+ * @returns The created BIP340DataIntegrityProof instance.
209
+ */
210
+ create(cryptosuite) {
211
+ return new BIP340DataIntegrityProof(cryptosuite);
212
+ }
213
+ /**
214
+ * Add a proof to a document.
215
+ * Uses the current proof instance when `proof` is omitted.
216
+ * @param document The document to add the proof to.
217
+ * @param config Configuration for adding the proof.
218
+ * @param proof Optional explicit proof instance; defaults to current.
219
+ * @returns A document with a proof added.
220
+ */
221
+ addProof(document, config, proof) {
222
+ const p = proof ?? this.#requireCurrent();
223
+ return p.addProof(document, config);
224
+ }
225
+ /**
226
+ * Convenience: create a cryptosuite, proof instance, and sign a document
227
+ * in one call. Requires a multikey with signing capability.
228
+ * @param multikey The Schnorr multikey (must include secret key).
229
+ * @param document The unsigned document to sign.
230
+ * @param config The Data Integrity proof configuration.
231
+ * @returns The signed document with proof attached.
232
+ */
233
+ signDocument(multikey, document, config) {
234
+ const cs = new BIP340Cryptosuite(multikey);
235
+ const proofInst = new BIP340DataIntegrityProof(cs);
236
+ return proofInst.addProof(document, config);
237
+ }
238
+ /**
239
+ * Verify a proof using a BIP340DataIntegrityProof instance.
240
+ * Uses the current proof instance when `proof` is omitted.
241
+ * @param document The document to verify the proof for.
242
+ * @param expectedPurpose The expected proof purpose.
243
+ * @param mediaType The media type of the document.
244
+ * @param expectedDomain The expected domain for the proof.
245
+ * @param expectedChallenge The expected challenge for the proof.
246
+ * @param proof Optional explicit proof instance; defaults to current.
247
+ * @returns The result of verifying the proof.
248
+ */
249
+ verifyProof(document, expectedPurpose, mediaType, expectedDomain, expectedChallenge, proof) {
250
+ const p = proof ?? this.#requireCurrent();
251
+ return p.verifyProof(document, expectedPurpose, mediaType, expectedDomain, expectedChallenge);
14
252
  }
15
- /** Import from secret key bytes or bigint. */
16
- static fromSecret(ent) {
17
- const sk = new Secp256k1SecretKey(ent);
18
- return new SchnorrKeyPair({ secretKey: sk });
253
+ #requireCurrent() {
254
+ if (!this.#current) {
255
+ throw new Error('No current proof instance set. Call proof.use(p) first, or pass an explicit instance.');
256
+ }
257
+ return this.#current;
19
258
  }
20
259
  }
260
+ // ---------------------------------------------------------------------------
261
+ // Multikey sub-facade
262
+ // ---------------------------------------------------------------------------
263
+ /**
264
+ * Schnorr multikey operations.
265
+ *
266
+ * Optionally stateful: call {@link use} to set a current multikey, then
267
+ * call {@link sign}, {@link verify}, or {@link toVerificationMethod} without
268
+ * passing an explicit instance. Pass an explicit instance to any method to
269
+ * override the current one for that call.
270
+ * @public
271
+ */
21
272
  export class MultikeyApi {
273
+ #current;
274
+ /** The currently active multikey, or `undefined` if none is set. */
275
+ get current() {
276
+ return this.#current;
277
+ }
278
+ /**
279
+ * Set the current multikey for subsequent operations.
280
+ * @param mk The multikey to activate.
281
+ * @returns `this` for chaining.
282
+ */
283
+ use(mk) {
284
+ this.#current = mk;
285
+ return this;
286
+ }
287
+ /** Clear the current multikey. */
288
+ clear() {
289
+ this.#current = undefined;
290
+ }
291
+ /**
292
+ * Create a new Schnorr multikey from a keypair.
293
+ * @param id The multikey ID.
294
+ * @param controller The multikey controller.
295
+ * @param keyPair The Schnorr keypair to use.
296
+ * @returns The created Schnorr multikey.
297
+ */
298
+ create(id, controller, keyPair) {
299
+ return new SchnorrMultikey({ id, controller, keyPair });
300
+ }
301
+ /**
302
+ * Create a Schnorr multikey from raw secret key bytes.
303
+ * @param id The multikey ID.
304
+ * @param controller The multikey controller.
305
+ * @param secretKeyBytes The secret key bytes.
306
+ * @returns The created Schnorr multikey.
307
+ */
308
+ fromSecretKey(id, controller, secretKeyBytes) {
309
+ return SchnorrMultikey.fromSecretKey(id, controller, secretKeyBytes);
310
+ }
22
311
  /**
23
- * Create a Schnorr Multikey wrapper (includes verificationMethod, sign/verify).
24
- * If secret is present, the multikey can sign.
312
+ * Create a verification-only multikey from public key bytes.
313
+ * @param params The id, controller, and publicKeyBytes.
314
+ * @returns The created Multikey.
25
315
  */
26
- static create(params) {
27
- return new SchnorrMultikey(params);
316
+ fromPublicKey(params) {
317
+ return SchnorrMultikey.fromPublicKey(params);
28
318
  }
29
- /** Produce a DID Verification Method JSON from a multikey. */
30
- static toVerificationMethod(mk) {
31
- return mk.toVerificationMethod();
319
+ /**
320
+ * Convenience: resolve a key from the KMS and create a multikey in one step.
321
+ * @param id The multikey ID.
322
+ * @param controller The multikey controller DID.
323
+ * @param keyId The KMS key identifier to resolve.
324
+ * @param kms The KeyManagerApi instance holding the key.
325
+ * @returns The created Multikey (verification-only; public key from KMS).
326
+ */
327
+ fromKms(id, controller, keyId, kms) {
328
+ const pubBytes = kms.getPublicKey(keyId);
329
+ return SchnorrMultikey.fromPublicKey({ id, controller, publicKeyBytes: pubBytes });
330
+ }
331
+ /**
332
+ * Reconstruct a multikey from a DID document's verification method.
333
+ * @param verificationMethod The verification method to convert.
334
+ * @returns The reconstructed multikey.
335
+ */
336
+ fromVerificationMethod(verificationMethod) {
337
+ return SchnorrMultikey.fromVerificationMethod(verificationMethod);
338
+ }
339
+ /**
340
+ * Produce a DID Verification Method JSON from a multikey.
341
+ * Uses the current multikey when `mk` is omitted.
342
+ * @param mk Optional explicit multikey; defaults to current.
343
+ */
344
+ toVerificationMethod(mk) {
345
+ const m = mk ?? this.#requireCurrent();
346
+ return m.toVerificationMethod();
347
+ }
348
+ /**
349
+ * Sign bytes via the multikey (requires secret).
350
+ * Uses the current multikey when `mk` is omitted.
351
+ * @param data The data to sign.
352
+ * @param mk Optional explicit multikey; defaults to current.
353
+ */
354
+ sign(data, mk) {
355
+ const m = mk ?? this.#requireCurrent();
356
+ return m.sign(data);
32
357
  }
33
- /** Sign bytes via the multikey (requires secret). */
34
- static async sign(mk, data) {
35
- return mk.sign(data);
358
+ /**
359
+ * Verify signature via multikey.
360
+ * Uses the current multikey when `mk` is omitted.
361
+ * @param data The data that was signed.
362
+ * @param signature The signature to verify.
363
+ * @param mk Optional explicit multikey; defaults to current.
364
+ */
365
+ verify(data, signature, mk) {
366
+ const m = mk ?? this.#requireCurrent();
367
+ return m.verify(signature, data);
36
368
  }
37
- /** Verify signature via multikey. */
38
- static async verify(mk, data, signature) {
39
- return mk.verify(data, signature);
369
+ #requireCurrent() {
370
+ if (!this.#current) {
371
+ throw new Error('No current multikey set. Call multikey.use(mk) first, or pass an explicit instance.');
372
+ }
373
+ return this.#current;
40
374
  }
41
375
  }
42
- /* =========================
43
- * Sub-facade: Crypto
44
- * ========================= */
376
+ // ---------------------------------------------------------------------------
377
+ // Crypto sub-facade (aggregates keypair, multikey, cryptosuite, proof)
378
+ // ---------------------------------------------------------------------------
379
+ /**
380
+ * Aggregated cryptographic operations sub-facade.
381
+ *
382
+ * Provides direct access to the four sub-facades ({@link keypair},
383
+ * {@link multikey}, {@link cryptosuite}, {@link proof}) plus top-level
384
+ * convenience methods that orchestrate the full signing/verification
385
+ * pipeline using their stateful defaults.
386
+ *
387
+ * @example Stateful pipeline
388
+ * ```ts
389
+ * const api = createApi();
390
+ * const kp = api.crypto.keypair.generate();
391
+ * const mk = api.crypto.multikey.create('#key-1', 'did:btcr2:test', kp);
392
+ *
393
+ * // Set the active multikey — flows through to cryptosuite and proof
394
+ * api.crypto.activate(mk);
395
+ *
396
+ * // Now sign without threading instances
397
+ * const signed = api.crypto.signDocument(unsignedDoc, proofConfig);
398
+ * ```
399
+ * @public
400
+ */
45
401
  export class CryptoApi {
46
- static keyPairApi = new KeyPairApi();
47
- static multikeyApi = new MultikeyApi();
402
+ /** Schnorr keypair operations. */
403
+ keypair = new KeyPairApi();
404
+ /** Schnorr Multikey operations (optionally stateful). */
405
+ multikey = new MultikeyApi();
406
+ /** Schnorr Cryptosuite operations (optionally stateful). */
407
+ cryptosuite = new CryptosuiteApi();
408
+ /** Data Integrity Proof operations (optionally stateful). */
409
+ proof = new DataIntegrityProofApi();
410
+ /**
411
+ * Activate a multikey and propagate through the full pipeline.
412
+ * Sets the current multikey, creates a cryptosuite from it, and creates
413
+ * a proof instance from the cryptosuite — all three sub-facades become
414
+ * ready for stateful operations.
415
+ * @param mk The multikey to activate (must include a secret key for signing).
416
+ * @returns `this` for chaining.
417
+ */
418
+ activate(mk) {
419
+ this.multikey.use(mk);
420
+ const cs = this.cryptosuite.create(mk);
421
+ this.cryptosuite.use(cs);
422
+ const p = this.proof.create(cs);
423
+ this.proof.use(p);
424
+ return this;
425
+ }
426
+ /**
427
+ * Clear stateful defaults from all sub-facades.
428
+ */
429
+ deactivate() {
430
+ this.multikey.clear();
431
+ this.cryptosuite.clear();
432
+ this.proof.clear();
433
+ }
434
+ /**
435
+ * Sign data using the current multikey.
436
+ * Shorthand for `crypto.multikey.sign(data)`.
437
+ * @param data The data to sign.
438
+ * @returns The signature bytes.
439
+ */
440
+ sign(data) {
441
+ return this.multikey.sign(data);
442
+ }
443
+ /**
444
+ * Verify a signature using the current multikey.
445
+ * Shorthand for `crypto.multikey.verify(data, signature)`.
446
+ * @param data The data that was signed.
447
+ * @param signature The signature to verify.
448
+ * @returns `true` if the signature is valid.
449
+ */
450
+ verify(data, signature) {
451
+ return this.multikey.verify(data, signature);
452
+ }
453
+ /**
454
+ * Sign a BTCR2 update document using the current proof instance.
455
+ * Shorthand for `crypto.proof.addProof(document, config)`.
456
+ *
457
+ * Requires {@link activate} to have been called first, or the three
458
+ * sub-facades to have been configured individually.
459
+ * @param document The unsigned BTCR2 update document.
460
+ * @param config The Data Integrity proof configuration.
461
+ * @returns The signed document with proof attached.
462
+ */
463
+ signDocument(document, config) {
464
+ return this.proof.addProof(document, config);
465
+ }
466
+ /**
467
+ * Verify a signed BTCR2 update document using the current cryptosuite.
468
+ * Shorthand for `crypto.cryptosuite.verifyProof(document)`.
469
+ * @param document The signed document to verify.
470
+ * @returns The full verification result.
471
+ */
472
+ verifyDocument(document) {
473
+ return this.cryptosuite.verifyProof(document);
474
+ }
48
475
  }
49
- /* =========================
50
- * Sub-facade: Bitcoin
51
- * ========================= */
476
+ // ---------------------------------------------------------------------------
477
+ // Bitcoin sub-facade
478
+ // ---------------------------------------------------------------------------
479
+ /**
480
+ * Bitcoin network operations sub-facade.
481
+ * Always backed by a {@link BitcoinConnection} so it can be passed to
482
+ * resolve/update without extra configuration.
483
+ *
484
+ * Lazily initialized by {@link DidBtcr2Api} to avoid connection overhead
485
+ * when Bitcoin features are not used.
486
+ * @public
487
+ */
52
488
  export class BitcoinApi {
53
- rest;
54
- rpc;
55
- defaultConfirmations;
489
+ /** The underlying BitcoinConnection used for all operations. */
490
+ connection;
491
+ /** REST client for the active network. */
492
+ get rest() {
493
+ return this.connection.rest;
494
+ }
495
+ /**
496
+ * RPC client for the active network, or `undefined` if not configured.
497
+ * Use {@link requireRpc} when RPC is expected to be available.
498
+ */
499
+ get rpc() {
500
+ return this.connection.rpc;
501
+ }
502
+ /** Whether an RPC client is available for this network. */
503
+ get hasRpc() {
504
+ return this.connection.rpc !== undefined;
505
+ }
506
+ /**
507
+ * RPC client for the active network.
508
+ * @throws {Error} If RPC was not configured for this network.
509
+ */
510
+ requireRpc() {
511
+ const client = this.connection.rpc;
512
+ if (!client) {
513
+ throw new Error('RPC client not configured. Pass an rpc config when creating the BitcoinApi, e.g.: '
514
+ + '{ network: \'regtest\', rpc: { host: \'http://localhost:18443\', username: \'u\', password: \'p\' } }');
515
+ }
516
+ return client;
517
+ }
518
+ /**
519
+ * Create a BitcoinApi for a specific network with optional endpoint overrides.
520
+ * Uses BitcoinConnection.forNetwork() — no env vars consulted.
521
+ * @param cfg The network and optional REST/RPC overrides.
522
+ */
56
523
  constructor(cfg) {
57
- const restCfg = {
58
- host: cfg?.rest?.host ?? DEFAULT_REST_CONFIG.host,
59
- ...cfg?.rest
60
- };
61
- const rpcCfg = {
62
- ...DEFAULT_RPC_CONFIG,
63
- ...cfg?.rpc
64
- };
65
- this.rest = new BitcoinRestClient(restCfg);
66
- this.rpc = new BitcoinCoreRpcClient(rpcCfg);
67
- this.defaultConfirmations = cfg?.defaultConfirmations ?? DEFAULT_BLOCK_CONFIRMATIONS;
68
- }
69
- /** Fetch a transaction by txid via REST. */
524
+ let executor = cfg.executor;
525
+ // Wrap the default fetch with a timeout if configured and no custom
526
+ // executor was provided.
527
+ if (!executor && cfg.timeoutMs !== undefined) {
528
+ const ms = cfg.timeoutMs;
529
+ executor = (req) => fetch(req.url, {
530
+ method: req.method,
531
+ headers: req.headers,
532
+ body: req.body,
533
+ signal: AbortSignal.timeout(ms),
534
+ });
535
+ }
536
+ this.connection = BitcoinConnection.forNetwork(cfg.network, {
537
+ rest: cfg.rest,
538
+ rpc: cfg.rpc,
539
+ executor,
540
+ });
541
+ }
542
+ /**
543
+ * Fetch a transaction by txid via REST.
544
+ * @param txid The transaction ID (64-character hex string).
545
+ * @returns The fetched transaction.
546
+ */
70
547
  async getTransaction(txid) {
548
+ assertString(txid, 'txid');
71
549
  return await this.rest.transaction.get(txid);
72
550
  }
73
- /** Broadcast a raw tx (hex) via REST. */
551
+ /**
552
+ * Broadcast a raw tx (hex) via REST.
553
+ * @param rawTxHex The raw transaction hex string.
554
+ */
74
555
  async send(rawTxHex) {
556
+ assertString(rawTxHex, 'rawTxHex');
75
557
  return await this.rest.transaction.send(rawTxHex);
76
558
  }
77
- /** Get UTXOs for an address via REST. */
559
+ /**
560
+ * Get UTXOs for an address via REST.
561
+ * @param address The Bitcoin address.
562
+ */
78
563
  async getUtxos(address) {
564
+ assertString(address, 'address');
79
565
  return await this.rest.address.getUtxos(address);
80
566
  }
81
- /** Get a block by hash or height via REST. */
567
+ /**
568
+ * Get a block by hash or height via REST.
569
+ * @param params Block identifier — at least one of `hash` or `height` is required.
570
+ */
82
571
  async getBlock(params) {
572
+ if (!params.hash && params.height === undefined) {
573
+ throw new Error('getBlock requires at least one of hash or height.');
574
+ }
83
575
  return await this.rest.block.get({ blockhash: params.hash, height: params.height });
84
576
  }
577
+ /** Convert BTC to satoshis (integer-safe string-split arithmetic). */
578
+ static btcToSats(btc) {
579
+ return BitcoinConnection.btcToSats(btc);
580
+ }
581
+ /** Convert satoshis to BTC (integer-safe string-split arithmetic). */
582
+ static satsToBtc(sats) {
583
+ return BitcoinConnection.satsToBtc(sats);
584
+ }
585
+ }
586
+ // ---------------------------------------------------------------------------
587
+ // KeyManager sub-facade
588
+ // ---------------------------------------------------------------------------
589
+ /**
590
+ * Key management operations sub-facade.
591
+ *
592
+ * Wraps a {@link KeyManager} interface. By default uses the built-in
593
+ * {@link Kms} implementation; a custom implementation can be injected
594
+ * via {@link ApiConfig}.
595
+ * @public
596
+ */
597
+ export class KeyManagerApi {
598
+ /** The backing KeyManager instance. */
599
+ kms;
600
+ /** Create a new KeyManagerApi, optionally backed by a custom KeyManager. */
601
+ constructor(kms) {
602
+ this.kms = kms ?? new Kms();
603
+ }
604
+ /** Generate a new key directly in the KMS. */
605
+ generateKey(options) {
606
+ return this.kms.generateKey(options);
607
+ }
608
+ /** Set the active key by its identifier. */
609
+ setActive(id) {
610
+ this.kms.setActiveKey(id);
611
+ }
612
+ /** Get the public key bytes for a key identifier. */
613
+ getPublicKey(id) {
614
+ return this.kms.getPublicKey(id);
615
+ }
616
+ /** Import a Schnorr keypair into the KMS. */
617
+ import(kp, options) {
618
+ return this.kms.importKey(kp, options);
619
+ }
620
+ /**
621
+ * Export a Schnorr keypair from the KMS.
622
+ * Only supported when the backing KMS is the built-in {@link Kms} class.
623
+ * @throws {Error} If the backing KMS does not support key export.
624
+ */
625
+ export(id) {
626
+ if (!(this.kms instanceof Kms)) {
627
+ throw new Error('Key export is not supported by the current KeyManager implementation. '
628
+ + 'Export is only available with the built-in Kms class.');
629
+ }
630
+ return this.kms.exportKey(id);
631
+ }
632
+ /** List all managed key identifiers. */
633
+ listKeys() {
634
+ return this.kms.listKeys();
635
+ }
636
+ /** Remove a key from the KMS. */
637
+ removeKey(id, options = {}) {
638
+ return this.kms.removeKey(id, options);
639
+ }
640
+ /**
641
+ * Sign data via the KMS.
642
+ * @param data The data to sign (must be non-empty).
643
+ * @param id Optional key identifier; uses the active key if omitted.
644
+ * @param options Signing options (scheme defaults to 'schnorr').
645
+ */
646
+ sign(data, id, options) {
647
+ assertBytes(data, 'data');
648
+ return this.kms.sign(data, id, options);
649
+ }
650
+ /** Verify a signature via the KMS. */
651
+ verify(signature, data, id, options) {
652
+ return this.kms.verify(signature, data, id, options);
653
+ }
654
+ /** Compute a SHA-256 digest. */
655
+ digest(data) {
656
+ return this.kms.digest(data);
657
+ }
85
658
  }
86
- /* =========================
87
- * Sub-facade: KeyManager
88
- * ========================= */
89
- // export class KeyManagerApi {
90
- // readonly impl: IMethodKeyManager;
91
- // constructor(params?: ApiKeyManagerConfig) {
92
- // this.impl = new MethodKeyManager(params);
93
- // }
94
- // setActive(keyUri: string) {
95
- // this.impl.activeKeyUri = keyUri;
96
- // }
97
- // export(keyUri: string) {
98
- // return this.impl.export(keyUri);
99
- // }
100
- // import(mk: SchnorrMultikey, opts?: { importKey?: boolean; active?: boolean }) {
101
- // return this.impl.import(mk, opts);
102
- // }
103
- // sign(keyUri: string, hash: HashBytes): Promise<SignatureBytes> {
104
- // return this.impl.sign(keyUri, hash);
105
- // }
106
- // }
107
- /* =========================
108
- * Sub-facade: DID / CRUD
109
- * ========================= */
659
+ // ---------------------------------------------------------------------------
660
+ // DID sub-facade
661
+ // ---------------------------------------------------------------------------
662
+ /**
663
+ * DID identifier operations sub-facade (encode, decode, generate, parse).
664
+ * @public
665
+ */
110
666
  export class DidApi {
111
667
  /**
112
- * Create a deterministic DID from a public key (bytes).
668
+ * Encode a DID from genesis bytes and options.
669
+ * @param genesisBytes The genesis document bytes.
670
+ * @param options The creation options.
671
+ * @returns The encoded DID string.
672
+ */
673
+ encode(genesisBytes, options) {
674
+ assertBytes(genesisBytes, 'genesisBytes');
675
+ return Identifier.encode(genesisBytes, options);
676
+ }
677
+ /**
678
+ * Decode a DID into its components.
679
+ * @param did The DID string to decode.
680
+ * @returns The decoded identifier components.
681
+ */
682
+ decode(did) {
683
+ assertString(did, 'did');
684
+ return Identifier.decode(did);
685
+ }
686
+ /**
687
+ * Generate a new DID along with its keypair.
688
+ *
689
+ * When no `network` is given, defaults to `'regtest'` (upstream default).
690
+ * Pass an explicit network to generate DIDs for other networks.
691
+ *
692
+ * @param network Optional network to generate the DID for.
693
+ * @returns The generated keypair and DID string.
694
+ */
695
+ generate(network) {
696
+ if (!network)
697
+ return Identifier.generate();
698
+ const kp = SchnorrKeyPair.generate();
699
+ const did = Identifier.encode(kp.publicKey.compressed, {
700
+ idType: IdentifierTypes.KEY,
701
+ network,
702
+ });
703
+ return { keyPair: kp.exportJSON(), did };
704
+ }
705
+ /**
706
+ * Check if a DID string is valid.
707
+ * @param did The DID string to validate.
708
+ * @returns `true` if valid, `false` otherwise.
113
709
  */
114
- async createDeterministic({ genesisBytes, options }) {
115
- return DidBtcr2.create(genesisBytes, options);
710
+ isValid(did) {
711
+ if (typeof did !== 'string' || did.length === 0)
712
+ return false;
713
+ return Identifier.isValid(did);
116
714
  }
117
715
  /**
118
- * Create from an intermediate DID document (external genesis).
716
+ * Parse a DID string into a Did instance.
717
+ * @param did The DID string to parse.
718
+ * @returns The parsed Did instance, or `null` if parsing failed.
119
719
  */
120
- async createExternal({ genesisBytes, options }) {
121
- return DidBtcr2.create(genesisBytes, options);
720
+ parse(did) {
721
+ if (typeof did !== 'string' || did.length === 0)
722
+ return null;
723
+ return Did.parse(did);
724
+ }
725
+ }
726
+ // ---------------------------------------------------------------------------
727
+ // DID Method sub-facade
728
+ // ---------------------------------------------------------------------------
729
+ /**
730
+ * DID method operations sub-facade: create, resolve, update, deactivate.
731
+ *
732
+ * Lazily initialized by {@link DidBtcr2Api} because it depends on
733
+ * {@link BitcoinApi} which requires network configuration.
734
+ * @public
735
+ */
736
+ export class DidMethodApi {
737
+ #btc;
738
+ #log;
739
+ constructor(btc, logger) {
740
+ this.#btc = btc;
741
+ this.#log = logger ?? NOOP_LOGGER;
122
742
  }
123
743
  /**
124
- * Resolve DID document from DID (did:btcr2:...).
744
+ * Create a deterministic (k1) DID from a public key.
745
+ * Sets idType to KEY automatically.
746
+ * @param genesisBytes The compressed public key bytes (33 bytes).
747
+ * @param options Creation options (idType is set for you).
748
+ * @returns The created DID identifier string.
749
+ */
750
+ createDeterministic(genesisBytes, options = {}) {
751
+ assertCompressedPubkey(genesisBytes, 'genesisBytes');
752
+ return DidBtcr2.create(genesisBytes, { ...options, idType: IdentifierTypes.KEY });
753
+ }
754
+ /**
755
+ * Create a non-deterministic (x1) DID from external genesis document bytes.
756
+ * Sets idType to EXTERNAL automatically.
757
+ * @param genesisBytes The genesis document bytes.
758
+ * @param options Creation options (idType is set for you).
759
+ * @returns The created DID identifier string.
760
+ */
761
+ createExternal(genesisBytes, options = {}) {
762
+ assertBytes(genesisBytes, 'genesisBytes');
763
+ return DidBtcr2.create(genesisBytes, { ...options, idType: IdentifierTypes.EXTERNAL });
764
+ }
765
+ /**
766
+ * Resolve a DID. If a Bitcoin connection is configured on the API, it is
767
+ * injected automatically as the driver — unless the caller explicitly
768
+ * provides `drivers.bitcoin` (even as `undefined`) in the options.
769
+ * @param did The DID to resolve.
770
+ * @param options Resolution options.
771
+ * @returns The resolution result.
125
772
  */
126
773
  async resolve(did, options) {
127
- return await DidBtcr2.resolve(did, options);
774
+ assertString(did, 'did');
775
+ const opts = { ...options };
776
+ // Only inject the configured connection when the caller did not
777
+ // explicitly provide the `bitcoin` driver key at all.
778
+ const hasExplicitDriver = options?.drivers !== undefined
779
+ && Object.prototype.hasOwnProperty.call(options.drivers, 'bitcoin');
780
+ if (!hasExplicitDriver && this.#btc) {
781
+ opts.drivers = { ...opts.drivers, bitcoin: this.#btc.connection };
782
+ }
783
+ this.#log.debug('Resolving DID', did);
784
+ try {
785
+ return await DidBtcr2.resolve(did, opts);
786
+ }
787
+ catch (err) {
788
+ this.#log.error('DID resolution failed', did, err);
789
+ throw new Error(`Failed to resolve DID: ${did}`, { cause: err });
790
+ }
128
791
  }
129
792
  /**
130
- * Update a DID Document using a JSON Patch, signed as capabilityInvocation.
131
- * You provide the prior DID Document (to pick VM), a JSON Patch, and a signer multikey.
132
- * This delegates to MethodUpdate (which follows the cryptosuite rules internally).
793
+ * Update an existing DID document. If a Bitcoin connection is configured on
794
+ * the API, it is injected automatically.
795
+ * @param params The update parameters.
796
+ * @returns The signed update.
133
797
  */
134
798
  async update({ sourceDocument, patches, sourceVersionId, verificationMethodId, beaconId, signingMaterial, bitcoin, }) {
135
- // The Update class exposes the algorithm that creates a DID Update Payload and proof;
136
- // keep this wrapper narrow so testing can mock MethodUpdate directly.
799
+ const btcConnection = bitcoin ?? this.#btc?.connection ?? undefined;
137
800
  return await DidBtcr2.update({
138
801
  sourceDocument,
139
802
  patches,
@@ -141,39 +804,339 @@ export class DidApi {
141
804
  verificationMethodId,
142
805
  beaconId,
143
806
  signingMaterial,
144
- bitcoin,
807
+ bitcoin: btcConnection,
145
808
  });
146
809
  }
147
- /** Deactivate convenience: applies the standard `deactivated: true` patch. */
810
+ /**
811
+ * Get the signing method from a DID document by method ID.
812
+ * @param didDocument The DID document.
813
+ * @param methodId The method ID (if omitted, the first signing method is returned).
814
+ * @returns The found signing method.
815
+ */
816
+ getSigningMethod(didDocument, methodId) {
817
+ return DidBtcr2.getSigningMethod(didDocument, methodId);
818
+ }
819
+ /**
820
+ * Create a fluent builder for a DID update operation.
821
+ * @param sourceDocument The current DID document to update.
822
+ * @returns An {@link UpdateBuilder} for chaining update parameters.
823
+ *
824
+ * @example
825
+ * ```ts
826
+ * const signed = await api.btcr2
827
+ * .buildUpdate(currentDoc)
828
+ * .patch({ op: 'add', path: '/service/1', value: newService })
829
+ * .version(2)
830
+ * .signer('#initialKey')
831
+ * .beacon('#beacon-0')
832
+ * .execute();
833
+ * ```
834
+ */
835
+ buildUpdate(sourceDocument) {
836
+ return new UpdateBuilder(this, sourceDocument);
837
+ }
838
+ /** Deactivate a DID (not yet implemented in the core method). */
148
839
  async deactivate() {
149
- // This class is a stub in method right now; expose a narrow wrapper for future expansion.
150
- // return DidBtcr2.deactivate({ identifier, patch }); // No-op holder; implement when core adds behavior.
151
- throw new NotImplementedError('DidApi.deactivate is not implemented yet.', {
840
+ throw new NotImplementedError('DidMethodApi.deactivate is not implemented yet.', {
152
841
  type: 'DID_API_METHOD_NOT_IMPLEMENTED',
153
842
  name: 'NOT_IMPLEMENTED_ERROR'
154
843
  });
155
844
  }
156
845
  }
157
- /* =========================
158
- * Root facade
159
- * ========================= */
846
+ // ---------------------------------------------------------------------------
847
+ // Update builder
848
+ // ---------------------------------------------------------------------------
849
+ /**
850
+ * Fluent builder for DID update operations. Reduces the cognitive load of
851
+ * the 7-parameter `update()` call by letting callers chain named steps.
852
+ *
853
+ * Created via {@link DidMethodApi.buildUpdate}.
854
+ * @public
855
+ */
856
+ export class UpdateBuilder {
857
+ #methodApi;
858
+ #sourceDocument;
859
+ #patches = [];
860
+ #sourceVersionId;
861
+ #verificationMethodId;
862
+ #beaconId;
863
+ #signingMaterial;
864
+ #bitcoin;
865
+ /** @internal */
866
+ constructor(methodApi, sourceDocument) {
867
+ this.#methodApi = methodApi;
868
+ this.#sourceDocument = sourceDocument;
869
+ }
870
+ /** Add a single JSON Patch operation. Can be called multiple times. */
871
+ patch(op) {
872
+ this.#patches.push(op);
873
+ return this;
874
+ }
875
+ /** Set all patches at once (replaces any previously added). */
876
+ patches(ops) {
877
+ this.#patches = [...ops];
878
+ return this;
879
+ }
880
+ /** Set the source version ID. */
881
+ version(id) {
882
+ this.#sourceVersionId = id;
883
+ return this;
884
+ }
885
+ /** Set the verification method ID used for signing. */
886
+ signer(methodId) {
887
+ this.#verificationMethodId = methodId;
888
+ return this;
889
+ }
890
+ /** Set the beacon ID for the update announcement. */
891
+ beacon(beaconId) {
892
+ this.#beaconId = beaconId;
893
+ return this;
894
+ }
895
+ /** Set the signing material (secret key bytes or hex). */
896
+ signingMaterial(material) {
897
+ this.#signingMaterial = material;
898
+ return this;
899
+ }
900
+ /** Override the Bitcoin connection for this update. */
901
+ withBitcoin(connection) {
902
+ this.#bitcoin = connection;
903
+ return this;
904
+ }
905
+ /**
906
+ * Execute the update.
907
+ * @throws {Error} If required fields (version, signer, beacon) are missing.
908
+ */
909
+ async execute() {
910
+ if (this.#sourceVersionId === undefined) {
911
+ throw new Error('UpdateBuilder: sourceVersionId is required. Call .version(id) before .execute().');
912
+ }
913
+ if (!this.#verificationMethodId) {
914
+ throw new Error('UpdateBuilder: verificationMethodId is required. Call .signer(id) before .execute().');
915
+ }
916
+ if (!this.#beaconId) {
917
+ throw new Error('UpdateBuilder: beaconId is required. Call .beacon(id) before .execute().');
918
+ }
919
+ return this.#methodApi.update({
920
+ sourceDocument: this.#sourceDocument,
921
+ patches: this.#patches,
922
+ sourceVersionId: this.#sourceVersionId,
923
+ verificationMethodId: this.#verificationMethodId,
924
+ beaconId: this.#beaconId,
925
+ signingMaterial: this.#signingMaterial,
926
+ bitcoin: this.#bitcoin,
927
+ });
928
+ }
929
+ }
930
+ // ---------------------------------------------------------------------------
931
+ // Main facade
932
+ // ---------------------------------------------------------------------------
933
+ /**
934
+ * Main DidBtcr2Api facade — the primary entry point for the SDK.
935
+ *
936
+ * Exposes sub-facades for Bitcoin, DID Method, KeyPair, Crypto, and
937
+ * KeyManager operations. Created via the {@link createApi} factory.
938
+ * @public
939
+ */
160
940
  export class DidBtcr2Api {
161
- bitcoin;
162
- did;
163
- keys;
941
+ /** Cryptographic operations (keypair, multikey, cryptosuite, proof). */
164
942
  crypto;
165
- // readonly keyManager: KeyManagerApi;
943
+ /** DID identifier operations (encode, decode, generate, parse). */
944
+ did;
945
+ /** Key management operations. */
946
+ kms;
947
+ #btcConfig;
948
+ #btc;
949
+ #btcr2;
950
+ #log;
951
+ #disposed = false;
166
952
  constructor(config) {
167
- this.bitcoin = new BitcoinApi(config?.bitcoin);
953
+ this.#btcConfig = config?.btc;
954
+ this.#log = config?.logger ?? NOOP_LOGGER;
955
+ this.kms = new KeyManagerApi(config?.kms);
168
956
  this.did = new DidApi();
169
- this.keys = new KeyPairApi();
170
957
  this.crypto = new CryptoApi();
171
- // this.keyManager = new KeyManagerApi(config?.keyManager);
958
+ }
959
+ /**
960
+ * Bitcoin API sub-facade (lazily initialized).
961
+ * Only available when `btc` config was provided to the constructor.
962
+ * @throws {Error} If the instance has been disposed or no Bitcoin config was provided.
963
+ */
964
+ get btc() {
965
+ this.#assertNotDisposed();
966
+ if (!this.#btc) {
967
+ if (!this.#btcConfig) {
968
+ throw new Error('Bitcoin not configured. Pass a btc config to createApi(), e.g.: '
969
+ + 'createApi({ btc: { network: \'regtest\' } })');
970
+ }
971
+ this.#btc = new BitcoinApi(this.#btcConfig);
972
+ }
973
+ return this.#btc;
974
+ }
975
+ /**
976
+ * DID Method API sub-facade (lazily initialized with bitcoin wiring).
977
+ * @throws {Error} If the instance has been disposed.
978
+ */
979
+ get btcr2() {
980
+ this.#assertNotDisposed();
981
+ if (!this.#btcr2) {
982
+ this.#btcr2 = new DidMethodApi(this.#btcConfig ? this.btc : undefined, this.#log);
983
+ }
984
+ return this.#btcr2;
985
+ }
986
+ /**
987
+ * Whether this API instance has been disposed.
988
+ */
989
+ get disposed() {
990
+ return this.#disposed;
991
+ }
992
+ /**
993
+ * Create a DID using either deterministic (KEY) or external (EXTERNAL) mode.
994
+ * @param type The creation mode.
995
+ * @param genesisBytes Public key bytes (deterministic) or document bytes (external).
996
+ * @param options Creation options (idType is set for you).
997
+ * @returns The created DID identifier string.
998
+ */
999
+ createDid(type, genesisBytes, options) {
1000
+ this.#assertNotDisposed();
1001
+ return type === 'deterministic'
1002
+ ? this.btcr2.createDeterministic(genesisBytes, options)
1003
+ : this.btcr2.createExternal(genesisBytes, options);
1004
+ }
1005
+ /**
1006
+ * Generate a new DID, create the keypair, and import it into the KMS.
1007
+ * @param options Optional settings.
1008
+ * @param options.setActive Whether to set the imported key as active in the KMS (default `true`).
1009
+ * @param options.network Network for the generated DID (default `'regtest'`).
1010
+ * @returns The generated DID string and KMS key identifier.
1011
+ */
1012
+ generateDid(options) {
1013
+ this.#assertNotDisposed();
1014
+ const { keyPair, did } = this.did.generate(options?.network);
1015
+ const kp = SchnorrKeyPair.fromJSON(keyPair);
1016
+ const keyId = this.kms.import(kp, { setActive: options?.setActive ?? true });
1017
+ return { did, keyId };
1018
+ }
1019
+ /**
1020
+ * Resolve a DID, automatically injecting the configured Bitcoin connection.
1021
+ * @param did The DID to resolve.
1022
+ * @param options Optional resolution options.
1023
+ * @returns The resolution result.
1024
+ */
1025
+ async resolveDid(did, options) {
1026
+ this.#assertNotDisposed();
1027
+ return await this.btcr2.resolve(did, options);
1028
+ }
1029
+ /**
1030
+ * Resolve a DID and return a discriminated result instead of throwing.
1031
+ * Useful when resolution failure is an expected outcome (e.g. checking
1032
+ * whether a DID exists before creating it).
1033
+ * @param did The DID to resolve.
1034
+ * @param options Optional resolution options.
1035
+ * @returns A {@link ResolutionResult} with `ok: true` on success or
1036
+ * `ok: false` with error details on failure.
1037
+ */
1038
+ async tryResolveDid(did, options) {
1039
+ this.#assertNotDisposed();
1040
+ assertString(did, 'did');
1041
+ try {
1042
+ const raw = await this.btcr2.resolve(did, options);
1043
+ if (raw.didDocument) {
1044
+ return {
1045
+ ok: true,
1046
+ document: raw.didDocument,
1047
+ metadata: raw.didDocumentMetadata,
1048
+ raw,
1049
+ };
1050
+ }
1051
+ return {
1052
+ ok: false,
1053
+ error: raw.didResolutionMetadata?.error ?? 'unknown',
1054
+ errorMessage: raw.didResolutionMetadata?.errorMessage,
1055
+ raw,
1056
+ };
1057
+ }
1058
+ catch (err) {
1059
+ return {
1060
+ ok: false,
1061
+ error: 'internalError',
1062
+ errorMessage: err.message,
1063
+ raw: {
1064
+ didDocument: null,
1065
+ didDocumentMetadata: {},
1066
+ didResolutionMetadata: { error: 'internalError', errorMessage: err.message },
1067
+ },
1068
+ };
1069
+ }
1070
+ }
1071
+ /**
1072
+ * Update a DID document: resolve the current state, apply patches, sign, and announce.
1073
+ * Automatically injects the configured Bitcoin connection.
1074
+ *
1075
+ * If `sourceDocument` and `sourceVersionId` are both provided, resolution
1076
+ * is skipped. Otherwise the DID is resolved first to obtain them.
1077
+ * @param params The update parameters.
1078
+ * @returns The signed update.
1079
+ */
1080
+ async updateDid({ did, patches, verificationMethodId, beaconId, sourceDocument, sourceVersionId, }) {
1081
+ this.#assertNotDisposed();
1082
+ assertString(did, 'did');
1083
+ let doc = sourceDocument;
1084
+ let versionId = sourceVersionId;
1085
+ if (!doc || versionId === undefined) {
1086
+ const resolution = await this.resolveDid(did);
1087
+ if (!resolution.didDocument) {
1088
+ const meta = resolution.didResolutionMetadata;
1089
+ const detail = meta?.error ? `: ${meta.error}` : '.';
1090
+ const extra = meta?.errorMessage ? ` ${meta.errorMessage}` : '';
1091
+ throw new Error(`Failed to resolve DID ${did} for update${detail}${extra}`, { cause: meta });
1092
+ }
1093
+ doc = doc ?? resolution.didDocument;
1094
+ if (versionId === undefined) {
1095
+ const rawVersionId = resolution.didDocumentMetadata?.versionId;
1096
+ if (rawVersionId === undefined || rawVersionId === null) {
1097
+ throw new Error(`Resolution of DID ${did} succeeded but returned no versionId in metadata. `
1098
+ + 'Provide sourceVersionId explicitly.');
1099
+ }
1100
+ const parsed = Number(rawVersionId);
1101
+ if (!Number.isFinite(parsed)) {
1102
+ throw new Error(`Resolution of DID ${did} returned a non-numeric versionId: ${String(rawVersionId)}.`);
1103
+ }
1104
+ versionId = parsed;
1105
+ }
1106
+ }
1107
+ return await this.btcr2.update({
1108
+ sourceDocument: doc,
1109
+ patches,
1110
+ sourceVersionId: versionId,
1111
+ verificationMethodId,
1112
+ beaconId,
1113
+ });
1114
+ }
1115
+ /**
1116
+ * Release internal references. After disposal, accessing `btc`, `btcr2`,
1117
+ * or calling top-level methods will throw.
1118
+ *
1119
+ * Note: the underlying {@link BitcoinConnection} does not hold persistent
1120
+ * connections, so this is primarily a guard against accidental reuse.
1121
+ */
1122
+ dispose() {
1123
+ this.#btc = undefined;
1124
+ this.#btcr2 = undefined;
1125
+ this.#btcConfig = undefined;
1126
+ this.#disposed = true;
1127
+ }
1128
+ #assertNotDisposed() {
1129
+ if (this.#disposed) {
1130
+ throw new Error('This DidBtcr2Api instance has been disposed and can no longer be used.');
1131
+ }
172
1132
  }
173
1133
  }
174
- /* =========================
175
- * Factory
176
- * ========================= */
1134
+ /**
1135
+ * Create a new {@link DidBtcr2Api} instance with the given configuration.
1136
+ * @param config Optional configuration for the API.
1137
+ * @returns The created DidBtcr2Api instance.
1138
+ * @public
1139
+ */
177
1140
  export function createApi(config) {
178
1141
  return new DidBtcr2Api(config);
179
1142
  }