@did-btcr2/api 0.4.0 → 0.7.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.
@@ -1,12 +1,11 @@
1
1
  import type { HttpExecutor, NetworkName, RestConfig, RpcConfig } from '@did-btcr2/bitcoin';
2
- import type { KeyManager } from '@did-btcr2/kms';
2
+ import type { KeyManager } from '@did-btcr2/key-manager';
3
3
  import type { Btcr2DidDocument } from '@did-btcr2/method';
4
4
  import type { DidResolutionResult } from '@web5/dids';
5
5
  import type { CasConfig } from './cas.js';
6
6
  /**
7
7
  * Pluggable logger interface. All methods are optional-call; the default
8
8
  * implementation is a silent no-op.
9
- * @public
10
9
  */
11
10
  export type Logger = {
12
11
  debug(message: string, ...args: unknown[]): void;
@@ -21,7 +20,6 @@ export type Logger = {
21
20
  * than a union. This local alias provides compile-time safety at the API
22
21
  * facade level. Upstream runtime validation in `Identifier.encode()` still
23
22
  * catches invalid values.
24
- * @public
25
23
  */
26
24
  export type IdType = 'KEY' | 'EXTERNAL';
27
25
  /**
@@ -35,14 +33,12 @@ export type IdType = 'KEY' | 'EXTERNAL';
35
33
  * api.resolveDid(did); // OK
36
34
  * api.btc.getTransaction(did); // Type error — DidString is not TxId
37
35
  * ```
38
- * @public
39
36
  */
40
37
  export type DidString = string & {
41
38
  readonly __brand: 'DidString';
42
39
  };
43
40
  /**
44
41
  * A branded string representing a Bitcoin transaction ID (64-char hex).
45
- * @public
46
42
  */
47
43
  export type TxId = string & {
48
44
  readonly __brand: 'TxId';
@@ -61,7 +57,6 @@ export type TxId = string & {
61
57
  * console.log(result.error, result.errorMessage);
62
58
  * }
63
59
  * ```
64
- * @public
65
60
  */
66
61
  export type ResolutionResult = {
67
62
  ok: true;
@@ -91,7 +86,6 @@ export type ResolutionResult = {
91
86
  * // Use regtest with custom RPC credentials, default REST
92
87
  * { network: 'regtest', rpc: { host: 'http://mynode:18443', username: 'u', password: 'p' } }
93
88
  * ```
94
- * @public
95
89
  */
96
90
  export type BitcoinApiConfig = {
97
91
  /** Bitcoin network name (e.g., 'regtest', 'testnet4', 'bitcoin'). */
@@ -116,7 +110,6 @@ export type BitcoinApiConfig = {
116
110
  };
117
111
  /**
118
112
  * Top-level API configuration options.
119
- * @public
120
113
  */
121
114
  export type ApiConfig = {
122
115
  btc?: BitcoinApiConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC3F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C;;;;GAIG;AACH,MAAM,MAAM,MAAM,GAAG;IACnB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACjD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAClD,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,UAAU,CAAC;AAExC;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;CAAE,CAAC;AAEnE;;;GAGG;AACH,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAE,QAAQ,EAAE,gBAAgB,CAAC;IAAC,QAAQ,EAAE,mBAAmB,CAAC,qBAAqB,CAAC,CAAC;IAAC,GAAG,EAAE,mBAAmB,CAAA;CAAE,GACzH;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,mBAAmB,CAAA;CAAE,CAAC;AAElF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qEAAqE;IACrE,OAAO,EAAE,WAAW,CAAC;IACrB,gEAAgE;IAChE,IAAI,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAC3B,+DAA+D;IAC/D,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,CAAC,EAAE,gBAAgB,CAAC;IACvB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC3F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C;;;GAGG;AACH,MAAM,MAAM,MAAM,GAAG;IACnB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACjD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAChD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAClD,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,UAAU,CAAC;AAExC;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;CAAE,CAAC;AAEnE;;GAEG;AACH,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAE,QAAQ,EAAE,gBAAgB,CAAC;IAAC,QAAQ,EAAE,mBAAmB,CAAC,qBAAqB,CAAC,CAAC;IAAC,GAAG,EAAE,mBAAmB,CAAA;CAAE,GACzH;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,mBAAmB,CAAA;CAAE,CAAC;AAElF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qEAAqE;IACrE,OAAO,EAAE,WAAW,CAAC;IACrB,gEAAgE;IAChE,IAAI,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAC3B,+DAA+D;IAC/D,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG;IACtB,GAAG,CAAC,EAAE,gBAAgB,CAAC;IACvB,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@did-btcr2/api",
3
- "version": "0.4.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "SDK for accessing the did:btcr2 method functionality.",
6
6
  "main": "./dist/cjs/index.js",
@@ -65,10 +65,9 @@
65
65
  "api"
66
66
  ],
67
67
  "dependencies": {
68
- "@bitcoinerlab/secp256k1": "^1.2.0",
69
68
  "@helia/strings": "^4.0.2",
70
- "@noble/curves": "^1.8.1",
71
- "@noble/hashes": "^1.5.0",
69
+ "@noble/curves": "^2.0.1",
70
+ "@noble/hashes": "^2.0.1",
72
71
  "@noble/secp256k1": "^2.1.0",
73
72
  "@scure/base": "^1.1.9",
74
73
  "@scure/bip32": "^1.5.0",
@@ -77,19 +76,17 @@
77
76
  "@web5/common": "^1.1.0",
78
77
  "@web5/crypto": "^1.0.6",
79
78
  "@web5/dids": "^1.2.0",
80
- "bitcoinjs-lib": "7.0.0-rc.0",
81
79
  "canonicalize": "^2.1.0",
82
80
  "dotenv": "^16.5.0",
83
81
  "helia": "^5.2.1",
84
82
  "multiformats": "^13.3.1",
85
83
  "nostr-tools": "^2.15.0",
86
- "tiny-secp256k1": "^2.2.3",
87
- "@did-btcr2/common": "^9.0.0",
88
- "@did-btcr2/bitcoin": "^0.5.3",
89
- "@did-btcr2/cryptosuite": "^6.0.6",
90
- "@did-btcr2/keypair": "^0.11.4",
91
- "@did-btcr2/kms": "^0.4.2",
92
- "@did-btcr2/method": "^0.26.0"
84
+ "@did-btcr2/common": "^9.1.0",
85
+ "@did-btcr2/keypair": "^0.13.0",
86
+ "@did-btcr2/method": "^0.32.0",
87
+ "@did-btcr2/cryptosuite": "^8.0.0",
88
+ "@did-btcr2/bitcoin": "^0.6.0",
89
+ "@did-btcr2/key-manager": "^0.6.0"
93
90
  },
94
91
  "devDependencies": {
95
92
  "@eslint/js": "^9.22.0",
package/src/api.ts CHANGED
@@ -1,16 +1,17 @@
1
1
  import type { NetworkName } from '@did-btcr2/bitcoin';
2
2
  import type { DocumentBytes, KeyBytes, PatchOperation } from '@did-btcr2/common';
3
3
  import type { SignedBTCR2Update } from '@did-btcr2/cryptosuite';
4
+ import type { Signer } from '@did-btcr2/keypair';
4
5
  import { SchnorrKeyPair } from '@did-btcr2/keypair';
5
- import type { KeyIdentifier } from '@did-btcr2/kms';
6
+ import type { KeyIdentifier } from '@did-btcr2/key-manager';
6
7
  import type { Btcr2DidDocument, DidCreateOptions, ResolutionOptions } from '@did-btcr2/method';
7
8
  import type { DidResolutionResult } from '@web5/dids';
8
9
  import { BitcoinApi } from './bitcoin.js';
9
- import { CasApi, type CasConfig } from './cas.js';
10
+ import { CasApi, DEFAULT_CAS_GATEWAY, type CasConfig } from './cas.js';
10
11
  import { CryptoApi } from './crypto.js';
11
12
  import { DidApi } from './did.js';
12
13
  import { assertString, NOOP_LOGGER } from './helpers.js';
13
- import { KeyManagerApi } from './kms.js';
14
+ import { KeyManagerApi } from './key-manager.js';
14
15
  import { DidMethodApi } from './method.js';
15
16
  import type { ApiConfig, BitcoinApiConfig, Logger, ResolutionResult } from './types.js';
16
17
 
@@ -67,19 +68,16 @@ export class DidBtcr2Api {
67
68
 
68
69
  /**
69
70
  * CAS API sub-facade (lazily initialized).
70
- * Only available when `cas` config was provided to the constructor.
71
- * @throws {Error} If the instance has been disposed or no CAS config was provided.
71
+ *
72
+ * When no `cas` config was provided to the constructor, defaults to a
73
+ * read-only {@link HttpGatewayCasExecutor} backed by the public IPFS
74
+ * gateway (`https://ipfs.io`). Override via `createApi({ cas: { ... } })`.
75
+ * @throws {Error} If the instance has been disposed.
72
76
  */
73
77
  get cas(): CasApi {
74
78
  this.#assertNotDisposed();
75
79
  if (!this.#cas) {
76
- if (!this.#casConfig) {
77
- throw new Error(
78
- 'CAS not configured. Pass a cas config to createApi(), e.g.: '
79
- + 'createApi({ cas: { helia: await createHelia() } })'
80
- );
81
- }
82
- this.#cas = new CasApi(this.#casConfig);
80
+ this.#cas = new CasApi(this.#casConfig ?? { gateway: DEFAULT_CAS_GATEWAY });
83
81
  }
84
82
  return this.#cas;
85
83
  }
@@ -93,7 +91,7 @@ export class DidBtcr2Api {
93
91
  if (!this.#btcr2) {
94
92
  this.#btcr2 = new DidMethodApi(
95
93
  this.#btcConfig ? this.btc : undefined,
96
- this.#casConfig ? this.cas : undefined,
94
+ this.cas,
97
95
  this.#log
98
96
  );
99
97
  }
@@ -207,6 +205,7 @@ export class DidBtcr2Api {
207
205
  patches,
208
206
  verificationMethodId,
209
207
  beaconId,
208
+ signer,
210
209
  sourceDocument,
211
210
  sourceVersionId,
212
211
  }: {
@@ -214,6 +213,7 @@ export class DidBtcr2Api {
214
213
  patches: PatchOperation[];
215
214
  verificationMethodId: string;
216
215
  beaconId: string;
216
+ signer: Signer;
217
217
  sourceDocument?: Btcr2DidDocument;
218
218
  sourceVersionId?: number;
219
219
  }): Promise<SignedBTCR2Update> {
@@ -260,6 +260,7 @@ export class DidBtcr2Api {
260
260
  sourceVersionId : versionId,
261
261
  verificationMethodId,
262
262
  beaconId,
263
+ signer,
263
264
  });
264
265
  }
265
266
 
package/src/cas.ts CHANGED
@@ -6,6 +6,9 @@ import * as raw from 'multiformats/codecs/raw';
6
6
  import { create as createDigest } from 'multiformats/hashes/digest';
7
7
  import { sha256 } from 'multiformats/hashes/sha2';
8
8
 
9
+ /** Default IPFS HTTP gateway used for CAS reads when no CAS config is provided. */
10
+ export const DEFAULT_CAS_GATEWAY = 'https://ipfs.io';
11
+
9
12
  /**
10
13
  * Executor interface for content-addressed storage.
11
14
  *
@@ -55,15 +58,70 @@ export class IpfsCasExecutor implements CasExecutor {
55
58
  }
56
59
  }
57
60
 
61
+ /**
62
+ * Read-only {@link CasExecutor} backed by an IPFS HTTP gateway.
63
+ *
64
+ * Converts the base64url SHA-256 hash to a CIDv1 (raw codec) and fetches
65
+ * the raw block via the
66
+ * {@link https://specs.ipfs.tech/http-gateways/trustless-gateway/ | Trustless Gateway}
67
+ * protocol.
68
+ *
69
+ * Publishing is not supported — use {@link IpfsCasExecutor} with a Helia
70
+ * instance for writes.
71
+ * @public
72
+ */
73
+ export class HttpGatewayCasExecutor implements CasExecutor {
74
+ readonly #gatewayUrl: string;
75
+
76
+ constructor(gatewayUrl: string) {
77
+ this.#gatewayUrl = gatewayUrl.replace(/\/+$/, '');
78
+ }
79
+
80
+ async retrieve(hash: string): Promise<Uint8Array | null> {
81
+ const hashBytes = decodeHash(hash, 'base64urlnopad');
82
+ const cid = CID.create(1, raw.code, createDigest(sha256.code, hashBytes));
83
+ try {
84
+ const res = await fetch(`${this.#gatewayUrl}/ipfs/${cid.toString()}?format=raw`, {
85
+ headers : { Accept: 'application/vnd.ipld.raw' },
86
+ });
87
+ if (!res.ok) return null;
88
+ return new Uint8Array(await res.arrayBuffer());
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ async publish(): Promise<string> {
95
+ throw new Error(
96
+ 'HttpGatewayCasExecutor is read-only. '
97
+ + 'Publishing requires a full IPFS node (use IpfsCasExecutor with Helia).'
98
+ );
99
+ }
100
+ }
101
+
102
+ /** Default timeout (ms) for CAS operations. */
103
+ export const DEFAULT_CAS_TIMEOUT_MS = 30_000;
104
+
58
105
  /**
59
106
  * Configuration for the CAS (Content-Addressed Storage) driver.
107
+ *
108
+ * Provide exactly one of `executor`, `helia`, or `gateway`.
109
+ * Priority if multiple are set: `executor` > `helia` > `gateway`.
60
110
  * @public
61
111
  */
62
112
  export type CasConfig = {
63
- /** Custom executor implementation (overrides the default IPFS executor). */
113
+ /** Custom executor implementation (overrides all other options). */
64
114
  executor?: CasExecutor;
65
115
  /** Pre-existing Helia instance for the default IPFS executor. */
66
116
  helia?: Helia;
117
+ /** IPFS HTTP gateway URL for read-only CAS access (e.g. `'https://ipfs.io'`). */
118
+ gateway?: string;
119
+ /**
120
+ * Timeout in milliseconds for CAS operations. Prevents indefinite hangs
121
+ * when a Helia DHT lookup or gateway request stalls. Default: 30 000 ms.
122
+ * Set to `0` to disable.
123
+ */
124
+ timeoutMs?: number;
67
125
  };
68
126
 
69
127
  /**
@@ -81,18 +139,22 @@ export type CasConfig = {
81
139
  */
82
140
  export class CasApi {
83
141
  readonly #executor: CasExecutor;
142
+ readonly #timeoutMs: number;
84
143
 
85
144
  constructor(config: CasConfig) {
86
145
  if (config.executor) {
87
146
  this.#executor = config.executor;
88
147
  } else if (config.helia) {
89
148
  this.#executor = new IpfsCasExecutor(config.helia);
149
+ } else if (config.gateway) {
150
+ this.#executor = new HttpGatewayCasExecutor(config.gateway);
90
151
  } else {
91
152
  throw new Error(
92
- 'CAS configuration requires either an executor or a Helia instance. '
93
- + 'Example: createApi({ cas: { helia: await createHelia() } })'
153
+ 'CAS configuration requires an executor, Helia instance, or gateway URL. '
154
+ + 'Example: createApi({ cas: { gateway: \'https://ipfs.io\' } })'
94
155
  );
95
156
  }
157
+ this.#timeoutMs = config.timeoutMs ?? DEFAULT_CAS_TIMEOUT_MS;
96
158
  }
97
159
 
98
160
  /**
@@ -102,7 +164,7 @@ export class CasApi {
102
164
  */
103
165
  async retrieve(hashBytes: HashBytes): Promise<object | null> {
104
166
  const hash = encodeHash(hashBytes, 'base64urlnopad');
105
- const bytes = await this.#executor.retrieve(hash);
167
+ const bytes = await this.#withTimeout(this.#executor.retrieve(hash));
106
168
  if (!bytes) return null;
107
169
  return JSON.parse(new TextDecoder().decode(bytes)) as object;
108
170
  }
@@ -116,6 +178,23 @@ export class CasApi {
116
178
  */
117
179
  async publish(object: object): Promise<string> {
118
180
  const bytes = new TextEncoder().encode(canonicalize(object as Record<string, any>));
119
- return await this.#executor.publish(bytes);
181
+ return await this.#withTimeout(this.#executor.publish(bytes));
182
+ }
183
+
184
+ /**
185
+ * Wraps a promise with a timeout. If `#timeoutMs` is 0, no timeout is applied.
186
+ */
187
+ #withTimeout<T>(promise: Promise<T>): Promise<T> {
188
+ if (!this.#timeoutMs) return promise;
189
+ return new Promise<T>((resolve, reject) => {
190
+ const timer = setTimeout(
191
+ () => reject(new Error(`CAS operation timed out after ${this.#timeoutMs}ms`)),
192
+ this.#timeoutMs
193
+ );
194
+ promise.then(
195
+ (val) => { clearTimeout(timer); resolve(val); },
196
+ (err) => { clearTimeout(timer); reject(err); },
197
+ );
198
+ });
120
199
  }
121
200
  }
package/src/crypto.ts CHANGED
@@ -15,9 +15,9 @@ import {
15
15
  SchnorrMultikey
16
16
  } from '@did-btcr2/cryptosuite';
17
17
  import { CompressedSecp256k1PublicKey, SchnorrKeyPair, Secp256k1SecretKey } from '@did-btcr2/keypair';
18
- import type { KeyIdentifier } from '@did-btcr2/kms';
18
+ import type { KeyIdentifier } from '@did-btcr2/key-manager';
19
19
  import type { DidVerificationMethod } from '@web5/dids';
20
- import type { KeyManagerApi } from './kms.js';
20
+ import type { KeyManagerApi } from './key-manager.js';
21
21
 
22
22
  /**
23
23
  * Schnorr keypair operations.
package/src/index.ts CHANGED
@@ -30,7 +30,7 @@ export * from './types.js';
30
30
  export * from './helpers.js';
31
31
  export * from './bitcoin.js';
32
32
  export * from './cas.js';
33
- export * from './kms.js';
33
+ export * from './key-manager.js';
34
34
  export * from './crypto.js';
35
35
  export * from './did.js';
36
36
  export * from './method.js';
@@ -1,22 +1,27 @@
1
1
  import type { Bytes, HashBytes, SignatureBytes } from '@did-btcr2/common';
2
2
  import type { SchnorrKeyPair } from '@did-btcr2/keypair';
3
- import type {
4
- KeyIdentifier,
5
- KeyManager} from '@did-btcr2/kms';
6
3
  import {
4
+ type KeyIdentifier,
5
+ type KeyManager,
7
6
  type GenerateKeyOptions,
8
7
  type ImportKeyOptions,
9
- Kms,
8
+ LocalKeyManager,
10
9
  type SignOptions,
11
- } from '@did-btcr2/kms';
10
+ type VerifyOptions,
11
+ } from '@did-btcr2/key-manager';
12
12
  import { assertBytes } from './helpers.js';
13
13
 
14
14
  /**
15
15
  * Key management operations sub-facade.
16
16
  *
17
- * Wraps a {@link KeyManager} interface. By default uses the built-in
18
- * {@link Kms} implementation; a custom implementation can be injected
19
- * via {@link ApiConfig}.
17
+ * Wraps any {@link KeyManager} interface implementation. By default uses the
18
+ * bundled {@link LocalKeyManager} (in-process reference implementation); a
19
+ * custom implementation (AWS KMS, GCP KMS, HashiCorp Vault, HSM, etc.) can
20
+ * be injected via {@link ApiConfig}.
21
+ *
22
+ * The field is named `kms` because that's the category label callers use
23
+ * conversationally ("plug in your KMS"); the actual contract is the
24
+ * {@link KeyManager} interface.
20
25
  * @public
21
26
  */
22
27
  export class KeyManagerApi {
@@ -25,7 +30,7 @@ export class KeyManagerApi {
25
30
 
26
31
  /** Create a new KeyManagerApi, optionally backed by a custom KeyManager. */
27
32
  constructor(kms?: KeyManager) {
28
- this.kms = kms ?? new Kms();
33
+ this.kms = kms ?? new LocalKeyManager();
29
34
  }
30
35
 
31
36
  /** Generate a new key directly in the KMS. */
@@ -50,14 +55,18 @@ export class KeyManagerApi {
50
55
 
51
56
  /**
52
57
  * Export a Schnorr keypair from the KMS.
53
- * Only supported when the backing KMS is the built-in {@link Kms} class.
54
- * @throws {Error} If the backing KMS does not support key export.
58
+ * Routes through the KeyManager's declared capability (`canExport`) rather
59
+ * than an `instanceof LocalKeyManager` check, so third-party adapters can
60
+ * opt in to export support without coupling to a specific implementation.
61
+ * External adapters (AWS, Vault, HSM) typically advertise `canExport: false`.
62
+ * @throws {Error} If the backing KeyManager does not advertise canExport=true,
63
+ * or omits the optional `exportKey` method.
55
64
  */
56
65
  export(id: KeyIdentifier): SchnorrKeyPair {
57
- if (!(this.kms instanceof Kms)) {
66
+ if (!this.kms.canExport || !this.kms.exportKey) {
58
67
  throw new Error(
59
68
  'Key export is not supported by the current KeyManager implementation. '
60
- + 'Export is only available with the built-in Kms class.'
69
+ + 'The adapter must advertise `canExport: true` and provide an `exportKey` method.'
61
70
  );
62
71
  }
63
72
  return this.kms.exportKey(id);
@@ -77,15 +86,15 @@ export class KeyManagerApi {
77
86
  * Sign data via the KMS.
78
87
  * @param data The data to sign (must be non-empty).
79
88
  * @param id Optional key identifier; uses the active key if omitted.
80
- * @param options Signing options (scheme defaults to 'schnorr').
89
+ * @param options Signing options. Defaults: `scheme: 'bip340'`.
81
90
  */
82
91
  sign(data: Bytes, id?: KeyIdentifier, options?: SignOptions): SignatureBytes {
83
92
  assertBytes(data, 'data');
84
93
  return this.kms.sign(data, id, options);
85
94
  }
86
95
 
87
- /** Verify a signature via the KMS. */
88
- verify(signature: SignatureBytes, data: Bytes, id?: KeyIdentifier, options?: SignOptions): boolean {
96
+ /** Verify a signature via the KMS. Defaults: `scheme: 'bip340'`. */
97
+ verify(signature: SignatureBytes, data: Bytes, id?: KeyIdentifier, options?: VerifyOptions): boolean {
89
98
  return this.kms.verify(signature, data, id, options);
90
99
  }
91
100
 
package/src/method.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import type { BitcoinConnection } from '@did-btcr2/bitcoin';
2
- import type { DocumentBytes, HexString, KeyBytes, PatchOperation } from '@did-btcr2/common';
3
- import { decode as decodeHash, IdentifierTypes, NotImplementedError } from '@did-btcr2/common';
2
+ import type { DocumentBytes, KeyBytes, PatchOperation } from '@did-btcr2/common';
3
+ import { decode as decodeHash, IdentifierTypes, INVALID_DID_UPDATE, NotImplementedError, UpdateError } from '@did-btcr2/common';
4
4
  import type { SignedBTCR2Update } from '@did-btcr2/cryptosuite';
5
+ import type { Signer } from '@did-btcr2/keypair';
5
6
  import type { Btcr2DidDocument, CASAnnouncement, DidCreateOptions, NeedCASAnnouncement, NeedGenesisDocument, NeedSignedUpdate, ResolutionOptions } from '@did-btcr2/method';
6
- import { BeaconSignalDiscovery, DidBtcr2 } from '@did-btcr2/method';
7
+ import { BeaconFactory, BeaconSignalDiscovery, DidBtcr2 } from '@did-btcr2/method';
7
8
  import type { DidResolutionResult, DidVerificationMethod } from '@web5/dids';
8
9
  import type { BitcoinApi } from './bitcoin.js';
9
10
  import type { CasApi } from './cas.js';
@@ -162,8 +163,16 @@ export class DidMethodApi {
162
163
  }
163
164
 
164
165
  /**
165
- * Update an existing DID document. If a Bitcoin connection is configured on
166
- * the API, it is injected automatically.
166
+ * Update an existing DID document by driving the sans-I/O {@link Updater} state
167
+ * machine (from @did-btcr2/method). This method handles the I/O side:
168
+ * - Signing: supplies the {@link Signer} to `NeedSigningKey`.
169
+ * - Broadcast: establishes a beacon via {@link BeaconFactory} and calls
170
+ * `broadcastSignal()` with the bitcoin connection configured on the API.
171
+ *
172
+ * For multi-party aggregation of SMT/CAS beacons, the caller should drive the
173
+ * Updater directly and delegate `NeedBroadcast` to the aggregation runner
174
+ * rather than using this high-level method.
175
+ *
167
176
  * @param params The update parameters.
168
177
  * @returns The signed update.
169
178
  */
@@ -173,7 +182,7 @@ export class DidMethodApi {
173
182
  sourceVersionId,
174
183
  verificationMethodId,
175
184
  beaconId,
176
- signingMaterial,
185
+ signer,
177
186
  bitcoin,
178
187
  }: {
179
188
  sourceDocument: Btcr2DidDocument;
@@ -181,19 +190,73 @@ export class DidMethodApi {
181
190
  sourceVersionId: number;
182
191
  verificationMethodId: string;
183
192
  beaconId: string;
184
- signingMaterial?: KeyBytes | HexString;
193
+ signer: Signer;
185
194
  bitcoin?: BitcoinConnection;
186
195
  }): Promise<SignedBTCR2Update> {
187
- const btcConnection = bitcoin ?? this.#btc?.connection ?? undefined;
188
- return await DidBtcr2.update({
196
+ // Bitcoin connection resolution order: per-call `bitcoin` param wins over the
197
+ // BitcoinApi injected at DidBtcr2Api construction time. One of the two must
198
+ // be present; this can't be encoded in the type system, so it's a runtime check.
199
+ const btcConnection = bitcoin ?? this.#btc?.connection;
200
+ if(!btcConnection) {
201
+ throw new UpdateError(
202
+ 'Bitcoin connection required for update. Pass a configured `bitcoin` parameter '
203
+ + 'or configure a BitcoinApi on the DidBtcr2Api instance.',
204
+ INVALID_DID_UPDATE, { beaconId }
205
+ );
206
+ }
207
+
208
+ this.#log.debug('Updating DID', sourceDocument.id, { beaconId, verificationMethodId });
209
+
210
+ // Factory validates and returns a sans-I/O state machine
211
+ const updater = DidBtcr2.update({
189
212
  sourceDocument,
190
213
  patches,
191
214
  sourceVersionId,
192
215
  verificationMethodId,
193
216
  beaconId,
194
- signingMaterial,
195
- bitcoin : btcConnection,
196
217
  });
218
+
219
+ // Drive the state machine. All I/O (signing delegation, Bitcoin broadcast)
220
+ // happens inside the need-handlers below — the Updater itself is pure.
221
+ let state = updater.advance();
222
+ while(state.status === 'action-required') {
223
+ for(const need of state.needs) {
224
+ switch(need.kind) {
225
+ case 'NeedSigningKey': {
226
+ this.#log.debug('Providing signer for', need.verificationMethodId);
227
+ updater.provide(need, signer);
228
+ break;
229
+ }
230
+ case 'NeedFunding': {
231
+ this.#log.debug('Checking funding for beacon address %s', need.beaconAddress);
232
+ const utxos = await btcConnection.rest.address.getUtxos(need.beaconAddress);
233
+ if(!utxos.length) {
234
+ throw new UpdateError(
235
+ `Beacon address ${need.beaconAddress} is unfunded. `
236
+ + 'Send BTC to this address before broadcasting the update.',
237
+ INVALID_DID_UPDATE, { beaconAddress: need.beaconAddress }
238
+ );
239
+ }
240
+ this.#log.debug('Beacon address funded (%d UTXOs)', utxos.length);
241
+ updater.provide(need);
242
+ break;
243
+ }
244
+ case 'NeedBroadcast': {
245
+ this.#log.debug(
246
+ 'Broadcasting signed update via %s beacon', need.beaconService.type
247
+ );
248
+ const beacon = BeaconFactory.establish(need.beaconService);
249
+ await beacon.broadcastSignal(need.signedUpdate, signer, btcConnection);
250
+ updater.provide(need);
251
+ break;
252
+ }
253
+ }
254
+ }
255
+ state = updater.advance();
256
+ }
257
+
258
+ this.#log.debug('DID update complete', sourceDocument.id);
259
+ return state.result.signedUpdate;
197
260
  }
198
261
 
199
262
  /**
@@ -217,8 +280,9 @@ export class DidMethodApi {
217
280
  * .buildUpdate(currentDoc)
218
281
  * .patch({ op: 'add', path: '/service/1', value: newService })
219
282
  * .version(2)
220
- * .signer('#initialKey')
283
+ * .verificationMethodId('#initialKey')
221
284
  * .beacon('#beacon-0')
285
+ * .signer(new LocalSigner(secretKey))
222
286
  * .execute();
223
287
  * ```
224
288
  */
@@ -252,7 +316,7 @@ export class UpdateBuilder {
252
316
  #sourceVersionId?: number;
253
317
  #verificationMethodId?: string;
254
318
  #beaconId?: string;
255
- #signingMaterial?: KeyBytes | HexString;
319
+ #signer?: Signer;
256
320
  #bitcoin?: BitcoinConnection;
257
321
 
258
322
  /** @internal */
@@ -279,8 +343,8 @@ export class UpdateBuilder {
279
343
  return this;
280
344
  }
281
345
 
282
- /** Set the verification method ID used for signing. */
283
- signer(methodId: string): this {
346
+ /** Set the verification method ID used for signing the update. */
347
+ verificationMethodId(methodId: string): this {
284
348
  this.#verificationMethodId = methodId;
285
349
  return this;
286
350
  }
@@ -291,32 +355,44 @@ export class UpdateBuilder {
291
355
  return this;
292
356
  }
293
357
 
294
- /** Set the signing material (secret key bytes or hex). */
295
- signingMaterial(material: KeyBytes | HexString): this {
296
- this.#signingMaterial = material;
358
+ /**
359
+ * Set the {@link Signer} that produces the update's BIP-340 Schnorr proof
360
+ * and the beacon transaction's ECDSA input signature. Use `LocalSigner`
361
+ * for in-process secret keys, `KeyManagerSigner` for KMS-managed keys
362
+ * (AWS, Vault, HSM, etc.), or any custom adapter implementing the `Signer`
363
+ * interface.
364
+ */
365
+ signer(s: Signer): this {
366
+ this.#signer = s;
297
367
  return this;
298
368
  }
299
369
 
300
370
  /** Override the Bitcoin connection for this update. */
301
- withBitcoin(connection: BitcoinConnection): this {
371
+ bitcoin(connection: BitcoinConnection): this {
302
372
  this.#bitcoin = connection;
303
373
  return this;
304
374
  }
305
375
 
306
376
  /**
307
377
  * Execute the update.
308
- * @throws {Error} If required fields (version, signer, beacon) are missing.
378
+ * @throws {Error} If required fields (version, verificationMethodId, beacon, signer) are missing.
309
379
  */
310
380
  async execute(): Promise<SignedBTCR2Update> {
311
381
  if (this.#sourceVersionId === undefined) {
312
382
  throw new Error('UpdateBuilder: sourceVersionId is required. Call .version(id) before .execute().');
313
383
  }
314
384
  if (!this.#verificationMethodId) {
315
- throw new Error('UpdateBuilder: verificationMethodId is required. Call .signer(id) before .execute().');
385
+ throw new Error(
386
+ 'UpdateBuilder: verificationMethodId is required. '
387
+ + 'Call .verificationMethodId(id) before .execute().'
388
+ );
316
389
  }
317
390
  if (!this.#beaconId) {
318
391
  throw new Error('UpdateBuilder: beaconId is required. Call .beacon(id) before .execute().');
319
392
  }
393
+ if (!this.#signer) {
394
+ throw new Error('UpdateBuilder: signer is required. Call .signer(s) before .execute().');
395
+ }
320
396
 
321
397
  return this.#methodApi.update({
322
398
  sourceDocument : this.#sourceDocument,
@@ -324,7 +400,7 @@ export class UpdateBuilder {
324
400
  sourceVersionId : this.#sourceVersionId,
325
401
  verificationMethodId : this.#verificationMethodId,
326
402
  beaconId : this.#beaconId,
327
- signingMaterial : this.#signingMaterial,
403
+ signer : this.#signer,
328
404
  bitcoin : this.#bitcoin,
329
405
  });
330
406
  }