@did-btcr2/cli 0.10.3 → 0.12.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 (94) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/cjs/index.js +1028 -114
  3. package/dist/esm/src/cli.js +31 -13
  4. package/dist/esm/src/cli.js.map +1 -1
  5. package/dist/esm/src/commands/completion.js +36 -0
  6. package/dist/esm/src/commands/completion.js.map +1 -0
  7. package/dist/esm/src/commands/config.js +69 -0
  8. package/dist/esm/src/commands/config.js.map +1 -0
  9. package/dist/esm/src/commands/create.js +109 -30
  10. package/dist/esm/src/commands/create.js.map +1 -1
  11. package/dist/esm/src/commands/deactivate.js +21 -8
  12. package/dist/esm/src/commands/deactivate.js.map +1 -1
  13. package/dist/esm/src/commands/index.js +4 -0
  14. package/dist/esm/src/commands/index.js.map +1 -1
  15. package/dist/esm/src/commands/key.js +175 -0
  16. package/dist/esm/src/commands/key.js.map +1 -0
  17. package/dist/esm/src/commands/profile.js +63 -0
  18. package/dist/esm/src/commands/profile.js.map +1 -0
  19. package/dist/esm/src/commands/update.js +19 -9
  20. package/dist/esm/src/commands/update.js.map +1 -1
  21. package/dist/esm/src/config.js +119 -12
  22. package/dist/esm/src/config.js.map +1 -1
  23. package/dist/esm/src/keystore/atomic.js +64 -0
  24. package/dist/esm/src/keystore/atomic.js.map +1 -0
  25. package/dist/esm/src/keystore/envelope.js +123 -0
  26. package/dist/esm/src/keystore/envelope.js.map +1 -0
  27. package/dist/esm/src/keystore/error.js +16 -0
  28. package/dist/esm/src/keystore/error.js.map +1 -0
  29. package/dist/esm/src/keystore/file-backed-key-manager.js +78 -0
  30. package/dist/esm/src/keystore/file-backed-key-manager.js.map +1 -0
  31. package/dist/esm/src/keystore/file-key-store.js +184 -0
  32. package/dist/esm/src/keystore/file-key-store.js.map +1 -0
  33. package/dist/esm/src/keystore/passphrase.js +87 -0
  34. package/dist/esm/src/keystore/passphrase.js.map +1 -0
  35. package/dist/esm/src/keystore/paths.js +20 -0
  36. package/dist/esm/src/keystore/paths.js.map +1 -0
  37. package/dist/esm/src/keystore/resolve-key-ref.js +47 -0
  38. package/dist/esm/src/keystore/resolve-key-ref.js.map +1 -0
  39. package/dist/types/src/cli.d.ts +6 -2
  40. package/dist/types/src/cli.d.ts.map +1 -1
  41. package/dist/types/src/commands/completion.d.ts +5 -0
  42. package/dist/types/src/commands/completion.d.ts.map +1 -0
  43. package/dist/types/src/commands/config.d.ts +5 -0
  44. package/dist/types/src/commands/config.d.ts.map +1 -0
  45. package/dist/types/src/commands/create.d.ts +19 -1
  46. package/dist/types/src/commands/create.d.ts.map +1 -1
  47. package/dist/types/src/commands/deactivate.d.ts.map +1 -1
  48. package/dist/types/src/commands/index.d.ts +4 -0
  49. package/dist/types/src/commands/index.d.ts.map +1 -1
  50. package/dist/types/src/commands/key.d.ts +10 -0
  51. package/dist/types/src/commands/key.d.ts.map +1 -0
  52. package/dist/types/src/commands/profile.d.ts +5 -0
  53. package/dist/types/src/commands/profile.d.ts.map +1 -0
  54. package/dist/types/src/commands/update.d.ts.map +1 -1
  55. package/dist/types/src/config.d.ts +57 -5
  56. package/dist/types/src/config.d.ts.map +1 -1
  57. package/dist/types/src/keystore/atomic.d.ts +19 -0
  58. package/dist/types/src/keystore/atomic.d.ts.map +1 -0
  59. package/dist/types/src/keystore/envelope.d.ts +64 -0
  60. package/dist/types/src/keystore/envelope.d.ts.map +1 -0
  61. package/dist/types/src/keystore/error.d.ts +14 -0
  62. package/dist/types/src/keystore/error.d.ts.map +1 -0
  63. package/dist/types/src/keystore/file-backed-key-manager.d.ts +41 -0
  64. package/dist/types/src/keystore/file-backed-key-manager.d.ts.map +1 -0
  65. package/dist/types/src/keystore/file-key-store.d.ts +52 -0
  66. package/dist/types/src/keystore/file-key-store.d.ts.map +1 -0
  67. package/dist/types/src/keystore/passphrase.d.ts +20 -0
  68. package/dist/types/src/keystore/passphrase.d.ts.map +1 -0
  69. package/dist/types/src/keystore/paths.d.ts +13 -0
  70. package/dist/types/src/keystore/paths.d.ts.map +1 -0
  71. package/dist/types/src/keystore/resolve-key-ref.d.ts +19 -0
  72. package/dist/types/src/keystore/resolve-key-ref.d.ts.map +1 -0
  73. package/dist/types/src/types.d.ts +93 -5
  74. package/dist/types/src/types.d.ts.map +1 -1
  75. package/package.json +9 -4
  76. package/src/cli.ts +37 -12
  77. package/src/commands/completion.ts +40 -0
  78. package/src/commands/config.ts +84 -0
  79. package/src/commands/create.ts +140 -52
  80. package/src/commands/deactivate.ts +25 -12
  81. package/src/commands/index.ts +4 -0
  82. package/src/commands/key.ts +193 -0
  83. package/src/commands/profile.ts +65 -0
  84. package/src/commands/update.ts +23 -13
  85. package/src/config.ts +165 -20
  86. package/src/keystore/atomic.ts +73 -0
  87. package/src/keystore/envelope.ts +172 -0
  88. package/src/keystore/error.ts +16 -0
  89. package/src/keystore/file-backed-key-manager.ts +99 -0
  90. package/src/keystore/file-key-store.ts +242 -0
  91. package/src/keystore/passphrase.ts +99 -0
  92. package/src/keystore/paths.ts +20 -0
  93. package/src/keystore/resolve-key-ref.ts +62 -0
  94. package/src/types.ts +31 -18
package/dist/cjs/index.js CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  CLIError: () => CLIError,
24
+ CONFIG_SCHEMA_VERSION: () => CONFIG_SCHEMA_VERSION,
24
25
  DidBtcr2Cli: () => DidBtcr2Cli,
25
26
  ENV_VARS: () => ENV_VARS,
26
27
  SUPPORTED_NETWORKS: () => SUPPORTED_NETWORKS,
@@ -29,23 +30,43 @@ __export(index_exports, {
29
30
  defaultConfigPath: () => defaultConfigPath,
30
31
  deriveNetwork: () => deriveNetwork,
31
32
  formatResult: () => formatResult,
33
+ getConfigPath: () => getConfigPath,
34
+ keystoreApiFactory: () => keystoreApiFactory,
32
35
  profileToOverrides: () => profileToOverrides,
33
36
  readConfigFile: () => readConfigFile,
34
37
  readEnvOverrides: () => readEnvOverrides,
38
+ registerCompletionCommand: () => registerCompletionCommand,
39
+ registerConfigCommand: () => registerConfigCommand,
35
40
  registerCreateCommand: () => registerCreateCommand,
36
41
  registerDeactivateCommand: () => registerDeactivateCommand,
42
+ registerKeyCommand: () => registerKeyCommand,
43
+ registerProfileCommand: () => registerProfileCommand,
37
44
  registerResolveCommand: () => registerResolveCommand,
38
- registerUpdateCommand: () => registerUpdateCommand
45
+ registerUpdateCommand: () => registerUpdateCommand,
46
+ resolveDefaultNetwork: () => resolveDefaultNetwork,
47
+ setConfigPath: () => setConfigPath,
48
+ unsetConfigPath: () => unsetConfigPath,
49
+ writeConfigFile: () => writeConfigFile
39
50
  });
40
51
  module.exports = __toCommonJS(index_exports);
41
52
 
42
- // ../../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.6_typescript@5.7.3_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js
53
+ // ../../node_modules/.pnpm/tsup@8.5.1_typescript@5.7.3_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js
43
54
  var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
44
55
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
45
56
 
46
57
  // src/cli.ts
58
+ var import_common3 = require("@did-btcr2/common");
47
59
  var import_commander = require("commander");
48
60
 
61
+ // src/commands/create.ts
62
+ var import_utils2 = require("@noble/hashes/utils.js");
63
+
64
+ // src/config.ts
65
+ var import_api = require("@did-btcr2/api");
66
+ var import_node_fs4 = require("fs");
67
+ var import_node_os2 = require("os");
68
+ var import_node_path4 = require("path");
69
+
49
70
  // src/error.ts
50
71
  var import_common = require("@did-btcr2/common");
51
72
  var CLIError = class extends import_common.DidMethodError {
@@ -54,13 +75,459 @@ var CLIError = class extends import_common.DidMethodError {
54
75
  }
55
76
  };
56
77
 
57
- // src/output.ts
58
- function formatResult(result, options) {
59
- if (options.output === "json") {
60
- return JSON.stringify(result, null, 2);
78
+ // src/keystore/atomic.ts
79
+ var import_node_fs = require("fs");
80
+ var import_node_path = require("path");
81
+
82
+ // src/keystore/error.ts
83
+ var import_common2 = require("@did-btcr2/common");
84
+ var KeyStoreError = class extends import_common2.DidMethodError {
85
+ constructor(message, type = "KeyStoreError", data) {
86
+ super(message, { type, name: type, data });
61
87
  }
62
- const { data } = result;
63
- return typeof data === "string" ? data : JSON.stringify(data, null, 2);
88
+ };
89
+
90
+ // src/keystore/atomic.ts
91
+ var isWindows = process.platform === "win32";
92
+ var permsWarned = false;
93
+ var tmpCounter = 0;
94
+ function ensureDir(dir, mode) {
95
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true, mode });
96
+ if (!isWindows) {
97
+ try {
98
+ (0, import_node_fs.chmodSync)(dir, mode);
99
+ } catch {
100
+ }
101
+ }
102
+ }
103
+ function writeFileAtomic(path, data, mode) {
104
+ const tmp = (0, import_node_path.join)((0, import_node_path.dirname)(path), `.${(0, import_node_path.basename)(path)}.${process.pid}.${tmpCounter++}.tmp`);
105
+ try {
106
+ (0, import_node_fs.writeFileSync)(tmp, data, { mode });
107
+ if (!isWindows) (0, import_node_fs.chmodSync)(tmp, mode);
108
+ (0, import_node_fs.renameSync)(tmp, path);
109
+ } catch (error) {
110
+ try {
111
+ (0, import_node_fs.rmSync)(tmp, { force: true });
112
+ } catch {
113
+ }
114
+ throw new KeyStoreError(
115
+ `Failed to write keystore at ${path}.`,
116
+ "ATOMIC_WRITE_ERROR",
117
+ { path, cause: error instanceof Error ? error.message : String(error) }
118
+ );
119
+ }
120
+ }
121
+ function assertSecurePerms(path) {
122
+ if (isWindows) {
123
+ if (!permsWarned) {
124
+ process.stderr.write(
125
+ "warning: file permissions are not enforced on Windows; protect the keystore directory manually.\n"
126
+ );
127
+ permsWarned = true;
128
+ }
129
+ return;
130
+ }
131
+ const mode = (0, import_node_fs.statSync)(path).mode & 511;
132
+ if ((mode & 63) !== 0) {
133
+ throw new KeyStoreError(
134
+ `Keystore at ${path} has insecure permissions 0${mode.toString(8)}; expected 0600.`,
135
+ "KEYSTORE_PERMISSION_ERROR",
136
+ { path, mode: `0${mode.toString(8)}` }
137
+ );
138
+ }
139
+ }
140
+
141
+ // src/keystore/file-backed-key-manager.ts
142
+ var import_key_manager = require("@did-btcr2/key-manager");
143
+
144
+ // src/keystore/file-key-store.ts
145
+ var import_node_fs2 = require("fs");
146
+ var import_node_path3 = require("path");
147
+ var import_base2 = require("@scure/base");
148
+
149
+ // src/keystore/envelope.ts
150
+ var import_chacha = require("@noble/ciphers/chacha.js");
151
+ var import_argon2 = require("@noble/hashes/argon2.js");
152
+ var import_utils = require("@noble/hashes/utils.js");
153
+ var import_base = require("@scure/base");
154
+ var ENVELOPE_VERSION = 1;
155
+ var SALT_BYTES = 16;
156
+ var NONCE_BYTES = 24;
157
+ var KEY_BYTES = 32;
158
+ var DEFAULT_ARGON_PARAMS = { t: 3, m: 65536, p: 4, dkLen: KEY_BYTES };
159
+ function buildHeader(saltB64, params) {
160
+ return {
161
+ v: ENVELOPE_VERSION,
162
+ kdf: {
163
+ alg: "argon2id",
164
+ salt: saltB64,
165
+ t: params.t,
166
+ m: params.m,
167
+ p: params.p,
168
+ dkLen: params.dkLen
169
+ },
170
+ cipher: "xchacha20poly1305"
171
+ };
172
+ }
173
+ function headerAad(header) {
174
+ return (0, import_utils.utf8ToBytes)(JSON.stringify(header));
175
+ }
176
+ function deriveKey(passphrase, salt, params) {
177
+ const password = (0, import_utils.utf8ToBytes)(passphrase);
178
+ try {
179
+ return (0, import_argon2.argon2id)(password, salt, { t: params.t, m: params.m, p: params.p, dkLen: params.dkLen });
180
+ } finally {
181
+ password.fill(0);
182
+ }
183
+ }
184
+ function encryptSecret(secret, passphrase, params = DEFAULT_ARGON_PARAMS) {
185
+ if (secret.length === 0) {
186
+ throw new KeyStoreError("Cannot encrypt an empty secret.", "ENVELOPE_ENCRYPT_ERROR");
187
+ }
188
+ const salt = (0, import_utils.randomBytes)(SALT_BYTES);
189
+ const nonce = (0, import_utils.randomBytes)(NONCE_BYTES);
190
+ const header = buildHeader(import_base.base64urlnopad.encode(salt), params);
191
+ const key = deriveKey(passphrase, salt, params);
192
+ try {
193
+ const ciphertext = (0, import_chacha.xchacha20poly1305)(key, nonce, headerAad(header)).encrypt(secret);
194
+ return {
195
+ ...header,
196
+ nonce: import_base.base64urlnopad.encode(nonce),
197
+ ciphertext: import_base.base64urlnopad.encode(ciphertext)
198
+ };
199
+ } finally {
200
+ key.fill(0);
201
+ }
202
+ }
203
+ function decryptSecret(env, passphrase) {
204
+ if (env.v !== ENVELOPE_VERSION) {
205
+ throw new KeyStoreError(
206
+ `Unsupported keystore envelope version: ${String(env.v)}.`,
207
+ "ENVELOPE_VERSION_ERROR",
208
+ { version: env.v }
209
+ );
210
+ }
211
+ if (env.kdf?.alg !== "argon2id" || env.cipher !== "xchacha20poly1305") {
212
+ throw new KeyStoreError("Unsupported keystore envelope algorithm.", "ENVELOPE_VERSION_ERROR");
213
+ }
214
+ const params = { t: env.kdf.t, m: env.kdf.m, p: env.kdf.p, dkLen: env.kdf.dkLen };
215
+ const salt = import_base.base64urlnopad.decode(env.kdf.salt);
216
+ const nonce = import_base.base64urlnopad.decode(env.nonce);
217
+ const ciphertext = import_base.base64urlnopad.decode(env.ciphertext);
218
+ const header = buildHeader(env.kdf.salt, params);
219
+ const key = deriveKey(passphrase, salt, params);
220
+ try {
221
+ return (0, import_chacha.xchacha20poly1305)(key, nonce, headerAad(header)).decrypt(ciphertext);
222
+ } catch (error) {
223
+ if (error instanceof KeyStoreError) throw error;
224
+ throw new KeyStoreError(
225
+ "Keystore decryption failed: wrong passphrase or corrupted keystore.",
226
+ "DECRYPT_ERROR"
227
+ );
228
+ } finally {
229
+ key.fill(0);
230
+ }
231
+ }
232
+
233
+ // src/keystore/paths.ts
234
+ var import_node_os = require("os");
235
+ var import_node_path2 = require("path");
236
+ function defaultKeystorePath() {
237
+ const base = process.env.XDG_DATA_HOME ?? process.env.LOCALAPPDATA ?? (0, import_node_path2.join)((0, import_node_os.homedir)(), ".local", "share");
238
+ return (0, import_node_path2.join)(base, "btcr2", "keystore.json");
239
+ }
240
+
241
+ // src/keystore/file-key-store.ts
242
+ var KEYSTORE_VERSION = 1;
243
+ var FileKeyStore = class {
244
+ #path;
245
+ #getPassphrase;
246
+ #argonParams;
247
+ #cache = /* @__PURE__ */ new Map();
248
+ #active;
249
+ constructor(options) {
250
+ this.#path = options.path ?? defaultKeystorePath();
251
+ this.#getPassphrase = options.getPassphrase;
252
+ this.#argonParams = options.argonParams ?? DEFAULT_ARGON_PARAMS;
253
+ ensureDir((0, import_node_path3.dirname)(this.#path), 448);
254
+ this.#load();
255
+ }
256
+ #load() {
257
+ if (!(0, import_node_fs2.existsSync)(this.#path)) return;
258
+ assertSecurePerms(this.#path);
259
+ let parsed;
260
+ try {
261
+ parsed = JSON.parse((0, import_node_fs2.readFileSync)(this.#path, "utf-8"));
262
+ } catch {
263
+ throw new KeyStoreError(
264
+ `Keystore at ${this.#path} is corrupt or unreadable.`,
265
+ "KEYSTORE_CORRUPT_ERROR",
266
+ { path: this.#path }
267
+ );
268
+ }
269
+ if (parsed.v !== KEYSTORE_VERSION) {
270
+ throw new KeyStoreError(
271
+ `Unsupported keystore version: ${String(parsed.v)}.`,
272
+ "KEYSTORE_VERSION_ERROR",
273
+ { version: parsed.v }
274
+ );
275
+ }
276
+ this.#active = parsed.active;
277
+ for (const [id, stored] of Object.entries(parsed.keys ?? {})) {
278
+ let publicKey;
279
+ try {
280
+ if (typeof stored.publicKey !== "string") throw new Error("missing publicKey");
281
+ publicKey = import_base2.base64urlnopad.decode(stored.publicKey);
282
+ } catch {
283
+ throw new KeyStoreError(
284
+ `Keystore entry ${id} has a malformed public key.`,
285
+ "KEYSTORE_CORRUPT_ERROR",
286
+ { path: this.#path, keyId: id }
287
+ );
288
+ }
289
+ if (publicKey.length !== 33) {
290
+ throw new KeyStoreError(
291
+ `Keystore entry ${id} has a ${publicKey.length}-byte public key; expected 33.`,
292
+ "KEYSTORE_CORRUPT_ERROR",
293
+ { path: this.#path, keyId: id }
294
+ );
295
+ }
296
+ this.#cache.set(id, {
297
+ publicKey,
298
+ ...stored.tags && { tags: stored.tags },
299
+ ...stored.secret && { secret: stored.secret }
300
+ });
301
+ }
302
+ }
303
+ #flush() {
304
+ const keys = {};
305
+ for (const [id, entry] of this.#cache) {
306
+ keys[id] = {
307
+ publicKey: import_base2.base64urlnopad.encode(entry.publicKey),
308
+ ...entry.tags && { tags: entry.tags },
309
+ ...entry.secret && { secret: entry.secret }
310
+ };
311
+ }
312
+ const file = {
313
+ v: KEYSTORE_VERSION,
314
+ ...this.#active && { active: this.#active },
315
+ keys
316
+ };
317
+ writeFileAtomic(this.#path, `${JSON.stringify(file, null, 2)}
318
+ `, 384);
319
+ }
320
+ get(id) {
321
+ const entry = this.#cache.get(id);
322
+ if (!entry) return void 0;
323
+ const result = {
324
+ publicKey: entry.publicKey,
325
+ ...entry.tags && { tags: entry.tags }
326
+ };
327
+ if (entry.secret) {
328
+ const sealed = entry.secret;
329
+ Object.defineProperty(result, "secretKey", {
330
+ configurable: true,
331
+ enumerable: false,
332
+ get: () => {
333
+ entry.decrypted ??= decryptSecret(sealed, this.#getPassphrase());
334
+ return entry.decrypted;
335
+ }
336
+ });
337
+ }
338
+ return result;
339
+ }
340
+ has(id) {
341
+ return this.#cache.has(id);
342
+ }
343
+ set(id, value) {
344
+ const secret = value.secretKey ? encryptSecret(value.secretKey, this.#getPassphrase(), this.#argonParams) : void 0;
345
+ this.#cache.set(id, {
346
+ publicKey: value.publicKey,
347
+ ...value.tags && { tags: value.tags },
348
+ ...secret && { secret },
349
+ ...value.secretKey && { decrypted: value.secretKey }
350
+ });
351
+ this.#flush();
352
+ }
353
+ delete(id) {
354
+ const existed = this.#cache.delete(id);
355
+ if (existed) {
356
+ if (this.#active === id) this.#active = void 0;
357
+ this.#flush();
358
+ }
359
+ return existed;
360
+ }
361
+ clear() {
362
+ this.#cache.clear();
363
+ this.#active = void 0;
364
+ this.#flush();
365
+ }
366
+ /** All stored values with secret keys omitted. Never decrypts, never prompts. */
367
+ list() {
368
+ return this.entries().map(([, value]) => value);
369
+ }
370
+ /**
371
+ * All entries as id-value tuples with secret keys omitted. Never decrypts,
372
+ * never prompts: {@link FileKeyStore.get} is the only secret-materializing
373
+ * path, so callers that only need identifiers (such as `listKeys`) do not
374
+ * force a passphrase prompt. This deviates intentionally from the in-memory
375
+ * store, which returns stored values verbatim.
376
+ */
377
+ entries() {
378
+ const out = [];
379
+ for (const [id, entry] of this.#cache) {
380
+ out.push([id, {
381
+ publicKey: entry.publicKey,
382
+ ...entry.tags && { tags: entry.tags }
383
+ }]);
384
+ }
385
+ return out;
386
+ }
387
+ close() {
388
+ for (const entry of this.#cache.values()) {
389
+ entry.decrypted?.fill(0);
390
+ entry.decrypted = void 0;
391
+ }
392
+ this.#cache.clear();
393
+ }
394
+ /** The persisted active-key identifier, or undefined if none is set. */
395
+ getActive() {
396
+ return this.#active;
397
+ }
398
+ /**
399
+ * Persists the active-key pointer in the keystore file. Passing undefined
400
+ * clears it. Throws if the identifier is not a known key.
401
+ */
402
+ setActive(id) {
403
+ if (id !== void 0 && !this.#cache.has(id)) {
404
+ throw new KeyStoreError(`Cannot set unknown key as active: ${id}.`, "KEY_NOT_FOUND_ERROR", { keyId: id });
405
+ }
406
+ this.#active = id;
407
+ this.#flush();
408
+ }
409
+ };
410
+
411
+ // src/keystore/file-backed-key-manager.ts
412
+ var FileBackedKeyManager = class {
413
+ /** Capability probe: the local store supports exporting secret material. */
414
+ canExport = true;
415
+ #store;
416
+ #inner;
417
+ constructor(options) {
418
+ this.#store = new FileKeyStore(options);
419
+ this.#inner = new import_key_manager.LocalKeyManager(this.#store);
420
+ const active = this.#store.getActive();
421
+ if (active && this.#store.has(active)) this.#inner.setActiveKey(active);
422
+ }
423
+ get activeKeyId() {
424
+ return this.#inner.activeKeyId;
425
+ }
426
+ setActiveKey(id) {
427
+ this.#inner.setActiveKey(id);
428
+ this.#store.setActive(id);
429
+ }
430
+ importKey(keyPair, options) {
431
+ const id = this.#inner.importKey(keyPair, options);
432
+ if (options?.setActive) this.#store.setActive(id);
433
+ return id;
434
+ }
435
+ generateKey(options) {
436
+ const id = this.#inner.generateKey(options);
437
+ if (options?.setActive) this.#store.setActive(id);
438
+ return id;
439
+ }
440
+ removeKey(id, options) {
441
+ this.#inner.removeKey(id, options);
442
+ }
443
+ listKeys() {
444
+ return this.#inner.listKeys();
445
+ }
446
+ getPublicKey(id) {
447
+ return this.#inner.getPublicKey(id);
448
+ }
449
+ getEntry(id) {
450
+ return this.#inner.getEntry(id);
451
+ }
452
+ sign(data, id, options) {
453
+ return this.#inner.sign(data, id, options);
454
+ }
455
+ verify(signature, data, id, options) {
456
+ return this.#inner.verify(signature, data, id, options);
457
+ }
458
+ digest(data) {
459
+ return this.#inner.digest(data);
460
+ }
461
+ exportKey(id) {
462
+ return this.#inner.exportKey(id);
463
+ }
464
+ };
465
+
466
+ // src/keystore/passphrase.ts
467
+ var import_node_fs3 = require("fs");
468
+ var ENV_KEYSTORE_PASSPHRASE = "BTCR2_KEYSTORE_PASSPHRASE";
469
+ function acquirePassphrase(options = {}) {
470
+ const fromEnv = process.env[ENV_KEYSTORE_PASSPHRASE];
471
+ if (fromEnv) return assertNonEmpty(fromEnv.replace(/\r?\n$/, ""));
472
+ if (options.passphraseFile) {
473
+ return assertNonEmpty((0, import_node_fs3.readFileSync)(options.passphraseFile, "utf-8").replace(/\r?\n$/, ""));
474
+ }
475
+ if (!process.stdin.isTTY) {
476
+ throw new KeyStoreError(
477
+ `No passphrase available. Set ${ENV_KEYSTORE_PASSPHRASE}, pass --passphrase-file, or run in a terminal.`,
478
+ "PASSPHRASE_REQUIRED_ERROR"
479
+ );
480
+ }
481
+ const passphrase = promptHidden(options.prompt ?? "Keystore passphrase: ");
482
+ if (options.confirm) {
483
+ const again = promptHidden("Confirm passphrase: ");
484
+ if (passphrase !== again) {
485
+ throw new KeyStoreError("Passphrases did not match.", "PASSPHRASE_MISMATCH_ERROR");
486
+ }
487
+ }
488
+ return assertNonEmpty(passphrase);
489
+ }
490
+ function assertNonEmpty(passphrase) {
491
+ if (passphrase.trim() === "") {
492
+ throw new KeyStoreError("A non-empty keystore passphrase is required.", "PASSPHRASE_REQUIRED_ERROR");
493
+ }
494
+ return passphrase;
495
+ }
496
+ function promptHidden(label) {
497
+ process.stderr.write(label);
498
+ const stdin = process.stdin;
499
+ const wasRaw = stdin.isRaw ?? false;
500
+ stdin.setRawMode(true);
501
+ const byte = Buffer.alloc(1);
502
+ const bytes = [];
503
+ try {
504
+ for (; ; ) {
505
+ let read = 0;
506
+ try {
507
+ read = (0, import_node_fs3.readSync)(stdin.fd, byte, 0, 1, null);
508
+ } catch (error) {
509
+ const code = error.code;
510
+ if (code === "EAGAIN") continue;
511
+ if (code === "EOF") break;
512
+ throw error;
513
+ }
514
+ if (read === 0) break;
515
+ const ch = byte[0];
516
+ if (ch === 10 || ch === 13) break;
517
+ if (ch === 3) {
518
+ throw new KeyStoreError("Passphrase entry aborted.", "PASSPHRASE_REQUIRED_ERROR");
519
+ }
520
+ if (ch === 127 || ch === 8) {
521
+ bytes.pop();
522
+ continue;
523
+ }
524
+ bytes.push(ch);
525
+ }
526
+ } finally {
527
+ stdin.setRawMode(wasRaw);
528
+ process.stderr.write("\n");
529
+ }
530
+ return Buffer.from(bytes).toString("utf-8");
64
531
  }
65
532
 
66
533
  // src/types.ts
@@ -73,75 +540,44 @@ var SUPPORTED_NETWORKS = [
73
540
  "regtest"
74
541
  ];
75
542
 
76
- // src/commands/create.ts
77
- var EXPECTED_BYTES = {
78
- k: { length: 33, label: "secp256k1 compressed public key (33 bytes)" },
79
- x: { length: 32, label: "SHA-256 hash (32 bytes)" }
80
- };
81
- function registerCreateCommand(program, factory, globals) {
82
- program.command("create").description("Create an identifier and initial DID document").requiredOption("-t, --type <type>", "Identifier type <k|x>", "k").requiredOption(
83
- "-n, --network <network>",
84
- "Identifier bitcoin network <bitcoin|testnet3|testnet4|signet|mutinynet|regtest>"
85
- ).requiredOption(
86
- "-b, --bytes <bytes>",
87
- "Genesis bytes as a hex string. If type=k, MUST be secp256k1 public key. If type=x, MUST be SHA-256 hash of a genesis document"
88
- ).action(async (options) => {
89
- const parsed = validateCreateOptions(options);
90
- const api = factory();
91
- const type = parsed.type === "k" ? "deterministic" : "external";
92
- const genesisBytes = Buffer.from(parsed.bytes, "hex");
93
- const data = api.createDid(type, genesisBytes, { network: parsed.network });
94
- const result = { action: "create", data };
95
- console.log(formatResult(result, globals()));
96
- });
543
+ // src/config.ts
544
+ var CONFIG_SCHEMA_VERSION = 1;
545
+ function writeConfigFile(path, mutate) {
546
+ const raw = readConfigFile(path) ?? {};
547
+ mutate(raw);
548
+ raw.schemaVersion = CONFIG_SCHEMA_VERSION;
549
+ ensureDir((0, import_node_path4.dirname)(path), 448);
550
+ writeFileAtomic(path, `${JSON.stringify(raw, null, 2)}
551
+ `, 384);
97
552
  }
98
- function validateCreateOptions(options) {
99
- if (!["k", "x"].includes(options.type)) {
100
- throw new CLIError(
101
- 'Invalid type. Must be "k" or "x".',
102
- "INVALID_ARGUMENT_ERROR",
103
- options
104
- );
105
- }
106
- if (!SUPPORTED_NETWORKS.includes(options.network)) {
107
- throw new CLIError(
108
- 'Invalid network. Must be one of "bitcoin", "testnet3", "testnet4", "signet", "mutinynet", or "regtest".',
109
- "INVALID_ARGUMENT_ERROR",
110
- options
111
- );
112
- }
113
- const buf = Buffer.from(options.bytes, "hex");
114
- if (buf.length === 0) {
115
- throw new CLIError(
116
- "Invalid bytes. Must be a non-empty hex string.",
117
- "INVALID_ARGUMENT_ERROR",
118
- options
119
- );
553
+ function getConfigPath(config, path) {
554
+ return path.split(".").reduce(
555
+ (node, key) => node?.[key],
556
+ config
557
+ );
558
+ }
559
+ function setConfigPath(config, path, value) {
560
+ const keys = path.split(".");
561
+ const last = keys.pop();
562
+ if (!last) throw new CLIError("Config path must be non-empty.", "INVALID_ARGUMENT_ERROR");
563
+ let node = config;
564
+ for (const key of keys) {
565
+ if (typeof node[key] !== "object" || node[key] === null) node[key] = {};
566
+ node = node[key];
120
567
  }
121
- const expected = EXPECTED_BYTES[options.type];
122
- if (buf.length !== expected.length) {
123
- throw new CLIError(
124
- `Invalid bytes length for type="${options.type}": expected ${expected.label}, got ${buf.length} bytes.`,
125
- "INVALID_ARGUMENT_ERROR",
126
- options
127
- );
568
+ node[last] = value;
569
+ }
570
+ function unsetConfigPath(config, path) {
571
+ const keys = path.split(".");
572
+ const last = keys.pop();
573
+ if (!last) return;
574
+ let node = config;
575
+ for (const key of keys) {
576
+ node = node?.[key];
577
+ if (!node) return;
128
578
  }
129
- return {
130
- type: options.type,
131
- network: options.network,
132
- bytes: options.bytes
133
- };
579
+ delete node[last];
134
580
  }
135
-
136
- // src/commands/resolve.ts
137
- var import_api2 = require("@did-btcr2/api");
138
- var import_promises = require("fs/promises");
139
-
140
- // src/config.ts
141
- var import_api = require("@did-btcr2/api");
142
- var import_node_fs = require("fs");
143
- var import_node_os = require("os");
144
- var import_node_path = require("path");
145
581
  var ENV_VARS = {
146
582
  BTC_REST: "BTCR2_BTC_REST",
147
583
  BTC_RPC_URL: "BTCR2_BTC_RPC_URL",
@@ -160,12 +596,12 @@ function readEnvOverrides() {
160
596
  };
161
597
  }
162
598
  function defaultConfigPath() {
163
- const base = process.env.XDG_CONFIG_HOME ?? process.env.APPDATA ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".config");
164
- return (0, import_node_path.join)(base, "btcr2", "config.json");
599
+ const base = process.env.XDG_CONFIG_HOME ?? process.env.APPDATA ?? (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".config");
600
+ return (0, import_node_path4.join)(base, "btcr2", "config.json");
165
601
  }
166
602
  function readConfigFile(path) {
167
603
  try {
168
- const content = (0, import_node_fs.readFileSync)(path, "utf-8");
604
+ const content = (0, import_node_fs4.readFileSync)(path, "utf-8");
169
605
  return JSON.parse(content);
170
606
  } catch {
171
607
  return void 0;
@@ -182,11 +618,22 @@ function profileToOverrides(config, profileName) {
182
618
  casGateway: profile.cas?.gateway
183
619
  };
184
620
  }
185
- function defaultApiFactory(network, overrides) {
186
- if (!network) return (0, import_api.createApi)();
621
+ function resolveDefaultNetwork(overrides) {
622
+ const configPath = overrides?.config ?? defaultConfigPath();
623
+ const file = readConfigFile(configPath);
624
+ const explicit = file?.defaults?.network;
625
+ if (explicit && SUPPORTED_NETWORKS.includes(explicit)) return explicit;
626
+ const profile = overrides?.profile ?? file?.defaults?.profile;
627
+ if (profile && SUPPORTED_NETWORKS.includes(profile)) {
628
+ return profile;
629
+ }
630
+ return "regtest";
631
+ }
632
+ function resolveConnectionConfig(network, overrides) {
633
+ if (!network) return {};
187
634
  const configPath = overrides?.config ?? defaultConfigPath();
188
- const profileName = overrides?.profile ?? network;
189
635
  const file = readConfigFile(configPath);
636
+ const profileName = overrides?.profile ?? file?.defaults?.profile ?? network;
190
637
  const fileOverrides = file ? profileToOverrides(file, profileName) : {};
191
638
  const env = readEnvOverrides();
192
639
  const merged = {
@@ -208,7 +655,22 @@ function defaultApiFactory(network, overrides) {
208
655
  };
209
656
  }
210
657
  const cas = merged.casGateway ? { gateway: merged.casGateway } : void 0;
211
- return (0, import_api.createApi)({ btc, ...cas && { cas } });
658
+ return { btc, ...cas && { cas } };
659
+ }
660
+ function defaultApiFactory(network, overrides) {
661
+ return (0, import_api.createApi)(resolveConnectionConfig(network, overrides));
662
+ }
663
+ function buildKeystoreKms(overrides) {
664
+ return new FileBackedKeyManager({
665
+ path: overrides?.keystore ?? defaultKeystorePath(),
666
+ getPassphrase: () => acquirePassphrase({ passphraseFile: overrides?.passphraseFile })
667
+ });
668
+ }
669
+ function keystoreApiFactory(network, overrides) {
670
+ return (0, import_api.createApi)({
671
+ ...resolveConnectionConfig(network, overrides),
672
+ kms: buildKeystoreKms(overrides)
673
+ });
212
674
  }
213
675
  function deriveNetwork(did) {
214
676
  const { network } = import_api.Identifier.decode(did);
@@ -222,7 +684,172 @@ function deriveNetwork(did) {
222
684
  return network;
223
685
  }
224
686
 
687
+ // src/keystore/resolve-key-ref.ts
688
+ function fingerprintOf(id) {
689
+ return /^urn:kms:secp256k1:([0-9a-f]{32})$/.exec(id)?.[1];
690
+ }
691
+ function resolveKeyRef(kms, ref) {
692
+ if (!ref) {
693
+ if (!kms.activeKeyId) {
694
+ throw new CLIError(
695
+ "No key specified and no active key is set. Use --key <ref> or set one with `btcr2 key use <ref>`.",
696
+ "INVALID_ARGUMENT_ERROR"
697
+ );
698
+ }
699
+ return kms.activeKeyId;
700
+ }
701
+ const ids = kms.listKeys();
702
+ if (ids.includes(ref)) return ref;
703
+ const prefix = ref.toLowerCase();
704
+ const byPrefix = ids.filter((id) => fingerprintOf(id)?.startsWith(prefix));
705
+ if (byPrefix.length === 1) return byPrefix[0];
706
+ if (byPrefix.length > 1) {
707
+ throw new CLIError(
708
+ `Ambiguous key reference "${ref}" matches ${byPrefix.length} keys by fingerprint.`,
709
+ "KEY_REF_AMBIGUOUS_ERROR",
710
+ { ref }
711
+ );
712
+ }
713
+ const byName = ids.filter((id) => kms.getEntry(id).tags?.name === ref);
714
+ if (byName.length === 1) return byName[0];
715
+ if (byName.length > 1) {
716
+ throw new CLIError(
717
+ `Ambiguous key name "${ref}" matches ${byName.length} keys.`,
718
+ "KEY_REF_AMBIGUOUS_ERROR",
719
+ { ref }
720
+ );
721
+ }
722
+ throw new CLIError(`No key matches reference "${ref}".`, "KEY_NOT_FOUND_ERROR", { ref });
723
+ }
724
+
725
+ // src/output.ts
726
+ function formatResult(result, options) {
727
+ if (options.output === "json") {
728
+ return JSON.stringify(result, null, 2);
729
+ }
730
+ const { data } = result;
731
+ return typeof data === "string" ? data : JSON.stringify(data, null, 2);
732
+ }
733
+
734
+ // src/commands/create.ts
735
+ var EXPECTED_BYTES = {
736
+ k: { length: 33, label: "secp256k1 compressed public key (33 bytes)" },
737
+ x: { length: 32, label: "SHA-256 hash (32 bytes)" }
738
+ };
739
+ function registerCreateCommand(program, factory, keystoreFactory, globals) {
740
+ program.command("create").description("Create an identifier and initial DID document").option("-t, --type <type>", "Identifier type <k|x>", "k").option(
741
+ "-n, --network <network>",
742
+ "Identifier bitcoin network <bitcoin|testnet3|testnet4|signet|mutinynet|regtest> (default: config defaults.network, else regtest)"
743
+ ).option(
744
+ "-b, --bytes <bytes>",
745
+ "Genesis bytes as a hex string. For type=k, a 33-byte secp256k1 public key (omit to generate a key). For type=x, the 32-byte SHA-256 hash of a genesis document."
746
+ ).action(async (options) => {
747
+ const g = globals();
748
+ if (options.type !== "k" && options.type !== "x") {
749
+ throw new CLIError('Invalid type. Must be "k" or "x".', "INVALID_ARGUMENT_ERROR", options);
750
+ }
751
+ const overrides = overridesFromGlobals(g);
752
+ const network = resolveNetwork(options.network, overrides);
753
+ const signingKey = g.signingKey;
754
+ const print = (result, note) => {
755
+ console.log(formatResult(result, g));
756
+ if (note && g.output !== "json") process.stderr.write(`${note}
757
+ `);
758
+ };
759
+ if (options.type === "x") {
760
+ if (signingKey) {
761
+ throw new CLIError(
762
+ "--signing-key applies only to deterministic identifiers (-t k).",
763
+ "INVALID_ARGUMENT_ERROR"
764
+ );
765
+ }
766
+ if (options.bytes === void 0) {
767
+ throw new CLIError(
768
+ "External identifiers (-t x) require --bytes <hex>, the 32-byte genesis document hash. Key generation is only available for -t k.",
769
+ "INVALID_ARGUMENT_ERROR"
770
+ );
771
+ }
772
+ const genesisBytes = parseGenesisBytes(options.bytes, "x");
773
+ const did2 = factory().createDid("external", genesisBytes, { network });
774
+ print({ action: "create", data: did2 });
775
+ return;
776
+ }
777
+ if (options.bytes !== void 0 && signingKey) {
778
+ throw new CLIError(
779
+ "Provide at most one of --bytes or --signing-key.",
780
+ "INVALID_ARGUMENT_ERROR"
781
+ );
782
+ }
783
+ if (options.bytes !== void 0) {
784
+ const genesisBytes = parseGenesisBytes(options.bytes, "k");
785
+ const did2 = factory().createDid("deterministic", genesisBytes, { network });
786
+ print({ action: "create", data: did2 });
787
+ return;
788
+ }
789
+ if (signingKey) {
790
+ const api2 = keystoreFactory(void 0, overrides);
791
+ const keyId2 = resolveKeyRef(api2.kms.kms, signingKey);
792
+ const publicKey2 = api2.kms.getPublicKey(keyId2);
793
+ const did2 = api2.createDid("deterministic", publicKey2, { network });
794
+ print(
795
+ { action: "create", data: did2, keyId: keyId2, publicKey: (0, import_utils2.bytesToHex)(publicKey2) },
796
+ `Using stored key ${keyId2}.`
797
+ );
798
+ return;
799
+ }
800
+ const api = keystoreFactory(void 0, overrides);
801
+ const { did, keyId } = api.generateDid({ network, setActive: true });
802
+ const publicKey = (0, import_utils2.bytesToHex)(api.kms.getPublicKey(keyId));
803
+ print(
804
+ { action: "create", data: did, keyId, publicKey },
805
+ `Generated and stored key ${keyId} (now the active key).`
806
+ );
807
+ });
808
+ }
809
+ function overridesFromGlobals(g) {
810
+ return {
811
+ config: g.config,
812
+ profile: g.profile,
813
+ keystore: g.keystore,
814
+ passphraseFile: g.passphraseFile
815
+ };
816
+ }
817
+ function resolveNetwork(explicit, overrides) {
818
+ if (!explicit) return resolveDefaultNetwork(overrides);
819
+ if (!SUPPORTED_NETWORKS.includes(explicit)) {
820
+ throw new CLIError(
821
+ 'Invalid network. Must be one of "bitcoin", "testnet3", "testnet4", "signet", "mutinynet", or "regtest".',
822
+ "INVALID_ARGUMENT_ERROR",
823
+ { network: explicit }
824
+ );
825
+ }
826
+ return explicit;
827
+ }
828
+ function parseGenesisBytes(hex, type) {
829
+ const expected = EXPECTED_BYTES[type];
830
+ let bytes;
831
+ try {
832
+ bytes = (0, import_utils2.hexToBytes)(hex.trim());
833
+ } catch {
834
+ throw new CLIError(
835
+ `Invalid bytes: not valid hex. Expected ${expected.label}.`,
836
+ "INVALID_ARGUMENT_ERROR",
837
+ { bytes: hex }
838
+ );
839
+ }
840
+ if (bytes.length !== expected.length) {
841
+ throw new CLIError(
842
+ `Invalid bytes length for type="${type}": expected ${expected.label}, got ${bytes.length} bytes.`,
843
+ "INVALID_ARGUMENT_ERROR",
844
+ { bytes: hex }
845
+ );
846
+ }
847
+ return bytes;
848
+ }
849
+
225
850
  // src/commands/resolve.ts
851
+ var import_api2 = require("@did-btcr2/api");
852
+ var import_promises = require("fs/promises");
226
853
  function registerResolveCommand(program, factory, globals) {
227
854
  program.command("resolve").alias("read").description("Resolve the DID document of the identifier.").requiredOption("-i, --identifier <identifier>", "did:btcr2 identifier").option("-r, --resolution-options <json>", "JSON string containing resolution options").option("-p, --resolution-options-path <path>", "Path to a JSON file containing resolution options").action(async (options) => {
228
855
  const parsed = await validateResolveOptions(options);
@@ -262,6 +889,7 @@ async function validateResolveOptions(options) {
262
889
  }
263
890
 
264
891
  // src/commands/update.ts
892
+ var import_key_manager2 = require("@did-btcr2/key-manager");
265
893
  function registerUpdateCommand(program, factory, globals) {
266
894
  program.command("update").description("Update a did:btcr2 document.").requiredOption(
267
895
  "-s, --source-document <json>",
@@ -282,6 +910,13 @@ function registerUpdateCommand(program, factory, globals) {
282
910
  "Beacon ID as a JSON string",
283
911
  parseJsonArg("--beacon-id")
284
912
  ).action(async (options) => {
913
+ if (!/^\d+$/.test(options.sourceVersionId)) {
914
+ throw new CLIError(
915
+ "--source-version-id must be a non-negative integer.",
916
+ "INVALID_ARGUMENT_ERROR",
917
+ { value: options.sourceVersionId }
918
+ );
919
+ }
285
920
  const parsed = {
286
921
  sourceDocument: options.sourceDocument,
287
922
  patches: options.patches,
@@ -297,15 +932,19 @@ function registerUpdateCommand(program, factory, globals) {
297
932
  options
298
933
  );
299
934
  }
300
- void deriveNetwork(did);
301
- void factory;
302
- void globals;
303
- void parsed;
304
- throw new CLIError(
305
- "CLI signing is not yet implemented. Use @did-btcr2/api with a Signer directly.",
306
- "NOT_IMPLEMENTED_ERROR",
307
- { command: "update" }
308
- );
935
+ const network = deriveNetwork(did);
936
+ const api = factory(network, globals());
937
+ const keyId = resolveKeyRef(api.kms.kms, globals().signingKey);
938
+ const signer = new import_key_manager2.KeyManagerSigner(api.kms.kms, keyId);
939
+ const data = await api.btcr2.update({
940
+ sourceDocument: parsed.sourceDocument,
941
+ patches: parsed.patches,
942
+ sourceVersionId: parsed.sourceVersionId,
943
+ verificationMethodId: parsed.verificationMethodId,
944
+ beaconId: parsed.beaconId,
945
+ signer
946
+ });
947
+ console.log(formatResult({ action: "update", data }, globals()));
309
948
  });
310
949
  }
311
950
  function parseJsonArg(flagName) {
@@ -323,6 +962,7 @@ function parseJsonArg(flagName) {
323
962
  }
324
963
 
325
964
  // src/commands/deactivate.ts
965
+ var import_key_manager3 = require("@did-btcr2/key-manager");
326
966
  var DEACTIVATION_PATCH = [{ op: "add", path: "/deactivated", value: true }];
327
967
  function registerDeactivateCommand(program, factory, globals) {
328
968
  program.command("deactivate").alias("delete").description("Deactivate the did:btcr2 identifier permanently. This is irreversible.").requiredOption(
@@ -340,6 +980,13 @@ function registerDeactivateCommand(program, factory, globals) {
340
980
  "Beacon ID as a JSON string",
341
981
  parseJsonArg2("--beacon-id")
342
982
  ).action(async (options) => {
983
+ if (!/^\d+$/.test(options.sourceVersionId)) {
984
+ throw new CLIError(
985
+ "--source-version-id must be a non-negative integer.",
986
+ "INVALID_ARGUMENT_ERROR",
987
+ { value: options.sourceVersionId }
988
+ );
989
+ }
343
990
  const parsed = {
344
991
  sourceDocument: options.sourceDocument,
345
992
  patches: DEACTIVATION_PATCH,
@@ -355,15 +1002,19 @@ function registerDeactivateCommand(program, factory, globals) {
355
1002
  options
356
1003
  );
357
1004
  }
358
- void deriveNetwork(did);
359
- void factory;
360
- void globals;
361
- void parsed;
362
- throw new CLIError(
363
- "CLI signing is not yet implemented. Use @did-btcr2/api with a Signer directly.",
364
- "NOT_IMPLEMENTED_ERROR",
365
- { command: "deactivate" }
366
- );
1005
+ const network = deriveNetwork(did);
1006
+ const api = factory(network, globals());
1007
+ const keyId = resolveKeyRef(api.kms.kms, globals().signingKey);
1008
+ const signer = new import_key_manager3.KeyManagerSigner(api.kms.kms, keyId);
1009
+ const data = await api.btcr2.update({
1010
+ sourceDocument: parsed.sourceDocument,
1011
+ patches: parsed.patches,
1012
+ sourceVersionId: parsed.sourceVersionId,
1013
+ verificationMethodId: parsed.verificationMethodId,
1014
+ beaconId: parsed.beaconId,
1015
+ signer
1016
+ });
1017
+ console.log(formatResult({ action: "deactivate", data }, globals()));
367
1018
  });
368
1019
  }
369
1020
  function parseJsonArg2(flagName) {
@@ -380,19 +1031,263 @@ function parseJsonArg2(flagName) {
380
1031
  };
381
1032
  }
382
1033
 
1034
+ // src/commands/key.ts
1035
+ var import_keypair = require("@did-btcr2/keypair");
1036
+ var import_utils3 = require("@noble/hashes/utils.js");
1037
+ var import_node_fs5 = require("fs");
1038
+ function registerKeyCommand(program, factory, globals) {
1039
+ const key = program.command("key").description("Manage keypairs in the encrypted keystore.");
1040
+ const print = (result) => console.log(formatResult(result, globals()));
1041
+ key.command("generate").description("Generate a new keypair and store it.").option("--name <name>", "A human-friendly name, stored as a tag and usable as a key reference.").option("--set-active", "Make this the active key.", false).action((options) => {
1042
+ const api = factory(void 0, globals());
1043
+ assertNameAvailable(api.kms.kms, options.name);
1044
+ const setActive = options.setActive ?? false;
1045
+ const id = api.kms.generateKey({ ...options.name && { tags: { name: options.name } }, setActive });
1046
+ print({ action: "key-generate", data: { keyId: id, publicKey: (0, import_utils3.bytesToHex)(api.kms.getPublicKey(id)), active: setActive } });
1047
+ });
1048
+ key.command("list").alias("ls").description("List stored keys.").action(() => {
1049
+ const kms = factory(void 0, globals()).kms.kms;
1050
+ const active = kms.activeKeyId;
1051
+ const data = kms.listKeys().map((id) => {
1052
+ const entry = kms.getEntry(id);
1053
+ return {
1054
+ keyId: id,
1055
+ fingerprint: id.split(":").pop() ?? id,
1056
+ ...entry.tags?.name && { name: entry.tags.name },
1057
+ active: id === active
1058
+ };
1059
+ });
1060
+ print({ action: "key-list", data });
1061
+ });
1062
+ key.command("show <ref>").description("Show a key's public material and tags. Never prints the secret.").action((ref) => {
1063
+ const kms = factory(void 0, globals()).kms.kms;
1064
+ const id = resolveKeyRef(kms, ref);
1065
+ const entry = kms.getEntry(id);
1066
+ print({ action: "key-show", data: { keyId: id, publicKey: (0, import_utils3.bytesToHex)(entry.publicKey), ...entry.tags && { tags: entry.tags } } });
1067
+ });
1068
+ key.command("import").description("Import a key: a secret from a hex file, or a public key as watch-only.").option("--secret-file <path>", "Path to a file containing a 32-byte secret key as hex.").option("--public <hex>", "A 33-byte compressed public key as hex (imported watch-only).").option("--name <name>", "A human-friendly name, stored as a tag.").option("--set-active", "Make this the active key.", false).action((options) => {
1069
+ if (Boolean(options.secretFile) === Boolean(options.public)) {
1070
+ throw new CLIError("Provide exactly one of --secret-file or --public.", "INVALID_ARGUMENT_ERROR");
1071
+ }
1072
+ const api = factory(void 0, globals());
1073
+ assertNameAvailable(api.kms.kms, options.name);
1074
+ const keyPair = options.secretFile ? new import_keypair.SchnorrKeyPair({ secretKey: readHexFile(options.secretFile, 32, "--secret-file") }) : new import_keypair.SchnorrKeyPair({ publicKey: parseHex(options.public ?? "", 33, "--public") });
1075
+ const setActive = options.setActive ?? false;
1076
+ const id = api.kms.import(keyPair, { ...options.name && { tags: { name: options.name } }, setActive });
1077
+ print({ action: "key-import", data: { keyId: id, publicKey: (0, import_utils3.bytesToHex)(api.kms.getPublicKey(id)), watchOnly: !options.secretFile, active: setActive } });
1078
+ });
1079
+ key.command("export <ref>").description("Export a key. Public material by default; --secret writes the secret to a file.").option("--secret", "Export the secret key. Requires --out.", false).option("--out <path>", "Write the exported secret to this file (created 0600).").action((ref, options) => {
1080
+ const api = factory(void 0, globals());
1081
+ const id = resolveKeyRef(api.kms.kms, ref);
1082
+ if (!options.secret) {
1083
+ print({ action: "key-export", data: { keyId: id, publicKey: (0, import_utils3.bytesToHex)(api.kms.getPublicKey(id)) } });
1084
+ return;
1085
+ }
1086
+ if (!options.out) {
1087
+ throw new CLIError("Exporting a secret requires --out <file> so it is not written to the terminal.", "INVALID_ARGUMENT_ERROR");
1088
+ }
1089
+ const keyPair = api.kms.export(id);
1090
+ if (!keyPair.hasSecretKey) {
1091
+ throw new CLIError(`Key ${id} is watch-only and has no secret to export.`, "INVALID_ARGUMENT_ERROR", { keyId: id });
1092
+ }
1093
+ process.stderr.write("warning: writing an unencrypted secret key to disk. Protect this file and delete it when done.\n");
1094
+ writeSecretFile(options.out, (0, import_utils3.bytesToHex)(keyPair.secretKey.bytes));
1095
+ print({ action: "key-export", data: { keyId: id, secretWrittenTo: options.out } });
1096
+ });
1097
+ key.command("delete <ref>").alias("rm").description("Delete a key from the keystore.").option("--force", "Delete even if it is the active key.", false).action((ref, options) => {
1098
+ const api = factory(void 0, globals());
1099
+ const id = resolveKeyRef(api.kms.kms, ref);
1100
+ api.kms.removeKey(id, { force: options.force ?? false });
1101
+ print({ action: "key-delete", data: { keyId: id, deleted: true } });
1102
+ });
1103
+ key.command("use <ref>").description("Set the active key, persisted across invocations.").action((ref) => {
1104
+ const api = factory(void 0, globals());
1105
+ const id = resolveKeyRef(api.kms.kms, ref);
1106
+ api.kms.setActive(id);
1107
+ print({ action: "key-use", data: { keyId: id, active: true } });
1108
+ });
1109
+ }
1110
+ function assertNameAvailable(kms, name) {
1111
+ if (!name) return;
1112
+ if (kms.listKeys().some((id) => kms.getEntry(id).tags?.name === name)) {
1113
+ throw new CLIError(`A key named "${name}" already exists.`, "INVALID_ARGUMENT_ERROR", { name });
1114
+ }
1115
+ }
1116
+ function parseHex(hex, expectedBytes, label) {
1117
+ let bytes;
1118
+ try {
1119
+ bytes = (0, import_utils3.hexToBytes)(hex.trim());
1120
+ } catch {
1121
+ throw new CLIError(`Invalid hex for ${label}.`, "INVALID_ARGUMENT_ERROR", { label });
1122
+ }
1123
+ if (bytes.length !== expectedBytes) {
1124
+ throw new CLIError(
1125
+ `${label} must be ${expectedBytes} bytes (${expectedBytes * 2} hex chars), got ${bytes.length}.`,
1126
+ "INVALID_ARGUMENT_ERROR",
1127
+ { label }
1128
+ );
1129
+ }
1130
+ return bytes;
1131
+ }
1132
+ function readHexFile(path, expectedBytes, label) {
1133
+ let content;
1134
+ try {
1135
+ content = (0, import_node_fs5.readFileSync)(path, "utf-8");
1136
+ } catch {
1137
+ throw new CLIError(`Cannot read ${label} at ${path}.`, "INVALID_ARGUMENT_ERROR", { label, path });
1138
+ }
1139
+ return parseHex(content, expectedBytes, label);
1140
+ }
1141
+ function writeSecretFile(path, contents) {
1142
+ let fd;
1143
+ try {
1144
+ fd = (0, import_node_fs5.openSync)(path, "wx", 384);
1145
+ } catch (error) {
1146
+ if (error.code === "EEXIST") {
1147
+ throw new CLIError(`Refusing to overwrite existing file ${path}. Choose a new --out path.`, "INVALID_ARGUMENT_ERROR", { path });
1148
+ }
1149
+ throw error;
1150
+ }
1151
+ try {
1152
+ (0, import_node_fs5.writeFileSync)(fd, contents);
1153
+ } finally {
1154
+ (0, import_node_fs5.closeSync)(fd);
1155
+ }
1156
+ }
1157
+
1158
+ // src/commands/config.ts
1159
+ var import_node_fs6 = require("fs");
1160
+ var import_node_path5 = require("path");
1161
+ function registerConfigCommand(program, globals) {
1162
+ const config = program.command("config").description("Read and write CLI configuration.");
1163
+ const path = () => globals().config ?? defaultConfigPath();
1164
+ const print = (result) => console.log(formatResult(result, globals()));
1165
+ config.command("init").description("Create a default config file with one profile per network.").option("--force", "Overwrite an existing config file.", false).action((options) => {
1166
+ const p = path();
1167
+ if ((0, import_node_fs6.existsSync)(p) && !options.force) {
1168
+ throw new CLIError(`Config already exists at ${p}. Use --force to overwrite.`, "INVALID_ARGUMENT_ERROR", { path: p });
1169
+ }
1170
+ const scaffold = {
1171
+ schemaVersion: CONFIG_SCHEMA_VERSION,
1172
+ defaults: { output: "text" },
1173
+ profiles: Object.fromEntries(SUPPORTED_NETWORKS.map((n) => [n, {}]))
1174
+ };
1175
+ ensureDir((0, import_node_path5.dirname)(p), 448);
1176
+ writeFileAtomic(p, `${JSON.stringify(scaffold, null, 2)}
1177
+ `, 384);
1178
+ print({ action: "config-init", data: { path: p } });
1179
+ });
1180
+ config.command("get [path]").description("Print a value at a dotted path, or the whole config.").action((dotted) => {
1181
+ const file = readConfigFile(path()) ?? {};
1182
+ print({ action: "config-get", data: (dotted ? getConfigPath(file, dotted) : file) ?? null });
1183
+ });
1184
+ config.command("set <path> <value>").description("Set a value at a dotted path. The value is parsed as JSON when valid, else stored as a string.").action((dotted, value) => {
1185
+ writeConfigFile(path(), (raw) => setConfigPath(raw, dotted, parseValue(value)));
1186
+ print({ action: "config-set", data: { path: dotted } });
1187
+ });
1188
+ config.command("unset <path>").description("Delete a value at a dotted path.").action((dotted) => {
1189
+ writeConfigFile(path(), (raw) => unsetConfigPath(raw, dotted));
1190
+ print({ action: "config-unset", data: { path: dotted } });
1191
+ });
1192
+ config.command("list").alias("ls").description("Print the entire config file.").action(() => {
1193
+ print({ action: "config-list", data: readConfigFile(path()) ?? {} });
1194
+ });
1195
+ }
1196
+ function parseValue(value) {
1197
+ try {
1198
+ return JSON.parse(value);
1199
+ } catch {
1200
+ return value;
1201
+ }
1202
+ }
1203
+
1204
+ // src/commands/profile.ts
1205
+ function registerProfileCommand(program, globals) {
1206
+ const profile = program.command("profile").description("Manage configuration profiles.");
1207
+ const path = () => globals().config ?? defaultConfigPath();
1208
+ const print = (result) => console.log(formatResult(result, globals()));
1209
+ profile.command("add <name>").description("Add an empty profile.").action((name) => {
1210
+ writeConfigFile(path(), (raw) => {
1211
+ if (raw.profiles === void 0 || raw.profiles === null) raw.profiles = {};
1212
+ const profiles = raw.profiles;
1213
+ if (profiles[name]) throw new CLIError(`Profile "${name}" already exists.`, "INVALID_ARGUMENT_ERROR", { name });
1214
+ profiles[name] = {};
1215
+ });
1216
+ print({ action: "profile-add", data: { profile: name } });
1217
+ });
1218
+ profile.command("use <name>").description("Set the active profile (writes defaults.profile).").action((name) => {
1219
+ writeConfigFile(path(), (raw) => {
1220
+ if (raw.defaults === void 0 || raw.defaults === null) raw.defaults = {};
1221
+ raw.defaults.profile = name;
1222
+ });
1223
+ print({ action: "profile-use", data: { profile: name } });
1224
+ });
1225
+ profile.command("show [name]").description("Show a profile (defaults to the active profile).").action((name) => {
1226
+ const file = readConfigFile(path()) ?? {};
1227
+ const target = name ?? file.defaults?.profile;
1228
+ if (!target) {
1229
+ throw new CLIError("No profile specified and no active profile is set.", "INVALID_ARGUMENT_ERROR");
1230
+ }
1231
+ const data = file.profiles?.[target];
1232
+ if (!data) {
1233
+ throw new CLIError(`Profile "${target}" not found.`, "INVALID_ARGUMENT_ERROR", { profile: target });
1234
+ }
1235
+ print({ action: "profile-show", data: { profile: target, ...data } });
1236
+ });
1237
+ profile.command("remove <name>").alias("rm").description("Remove a profile.").action((name) => {
1238
+ writeConfigFile(path(), (raw) => {
1239
+ const profiles = raw.profiles;
1240
+ if (!profiles?.[name]) throw new CLIError(`Profile "${name}" not found.`, "INVALID_ARGUMENT_ERROR", { name });
1241
+ delete profiles[name];
1242
+ });
1243
+ print({ action: "profile-remove", data: { profile: name } });
1244
+ });
1245
+ }
1246
+
1247
+ // src/commands/completion.ts
1248
+ var COMMANDS = "create resolve read update deactivate delete key config profile completion";
1249
+ function registerCompletionCommand(program, _globals) {
1250
+ program.command("completion [shell]").description("Print a shell completion script (bash, zsh, or fish) to stdout.").action((shell = "bash") => {
1251
+ console.log(completionScript(shell));
1252
+ });
1253
+ }
1254
+ function completionScript(shell) {
1255
+ switch (shell) {
1256
+ case "bash":
1257
+ return [
1258
+ '# btcr2 bash completion. Install with: eval "$(btcr2 completion bash)"',
1259
+ '_btcr2() { COMPREPLY=( $(compgen -W "' + COMMANDS + '" -- "${COMP_WORDS[COMP_CWORD]}") ); }',
1260
+ "complete -F _btcr2 btcr2"
1261
+ ].join("\n");
1262
+ case "zsh":
1263
+ return [
1264
+ '# btcr2 zsh completion. Install with: eval "$(btcr2 completion zsh)"',
1265
+ "_btcr2() { compadd " + COMMANDS + " }",
1266
+ "compdef _btcr2 btcr2"
1267
+ ].join("\n");
1268
+ case "fish":
1269
+ return [
1270
+ "# btcr2 fish completion. Save to ~/.config/fish/completions/btcr2.fish",
1271
+ 'complete -c btcr2 -f -a "' + COMMANDS + '"'
1272
+ ].join("\n");
1273
+ default:
1274
+ throw new CLIError(`Unsupported shell "${shell}". Use bash, zsh, or fish.`, "INVALID_ARGUMENT_ERROR", { shell });
1275
+ }
1276
+ }
1277
+
383
1278
  // src/version.ts
384
- var import_node_fs2 = require("fs");
385
- var import_node_path2 = require("path");
1279
+ var import_node_fs7 = require("fs");
1280
+ var import_node_path6 = require("path");
386
1281
  var import_node_url = require("url");
387
1282
  function readVersion() {
388
- let dir = (0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
1283
+ let dir = (0, import_node_path6.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
389
1284
  for (let i = 0; i < 5; i++) {
390
1285
  try {
391
- const pkg = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path2.join)(dir, "package.json"), "utf-8"));
1286
+ const pkg = JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path6.join)(dir, "package.json"), "utf-8"));
392
1287
  if (pkg.name === "@did-btcr2/cli") return pkg.version;
393
1288
  } catch {
394
1289
  }
395
- dir = (0, import_node_path2.dirname)(dir);
1290
+ dir = (0, import_node_path6.dirname)(dir);
396
1291
  }
397
1292
  return "0.0.0";
398
1293
  }
@@ -409,15 +1304,23 @@ var DidBtcr2Cli = class {
409
1304
  * {@link defaultApiFactory} which uses public endpoints (mempool.space)
410
1305
  * for known networks and localhost Polar for regtest.
411
1306
  *
412
- * @param factory - Optional API factory. Defaults to {@link defaultApiFactory}.
1307
+ * @param factory - Optional API factory for keystore-free commands (create,
1308
+ * resolve). Defaults to {@link defaultApiFactory}.
1309
+ * @param keystoreFactory - Optional keystore-aware API factory for commands
1310
+ * that need a signing identity (key, update, deactivate). Defaults to
1311
+ * {@link keystoreApiFactory}.
413
1312
  */
414
- constructor(factory = defaultApiFactory) {
415
- this.program = new import_commander.Command("btcr2").version(`btcr2 ${VERSION}`, "-v, --version", "Output the current version").description("CLI tool for the did:btcr2 method").option("-o, --output <format>", "Output format <json|text>", "text").option("--verbose", "Verbose output", false).option("--quiet", "Suppress non-essential output", false).option("-c, --config <path>", "Path to config file (default: $XDG_CONFIG_HOME/btcr2/config.json)").option("--profile <name>", "Config profile name (default: auto-detected from network)").option("--btc-rest <url>", "Override Bitcoin REST endpoint (Esplora API)").option("--btc-rpc-url <url>", "Override Bitcoin Core RPC endpoint").option("--btc-rpc-user <user>", "Bitcoin Core RPC username").option("--btc-rpc-pass <pass>", "Bitcoin Core RPC password").option("--cas-gateway <url>", "IPFS HTTP gateway for CAS reads");
1313
+ constructor(factory = defaultApiFactory, keystoreFactory = keystoreApiFactory) {
1314
+ this.program = new import_commander.Command("btcr2").version(`btcr2 ${VERSION}`, "-v, --version", "Output the current version").description("CLI tool for the did:btcr2 method").option("-o, --output <format>", "Output format <json|text>", "text").option("--verbose", "Verbose output", false).option("--quiet", "Suppress non-essential output", false).option("-c, --config <path>", "Path to config file (default: $XDG_CONFIG_HOME/btcr2/config.json)").option("--profile <name>", "Config profile name (default: auto-detected from network)").option("--btc-rest <url>", "Override Bitcoin REST endpoint (Esplora API)").option("--btc-rpc-url <url>", "Override Bitcoin Core RPC endpoint").option("--btc-rpc-user <user>", "Bitcoin Core RPC username").option("--btc-rpc-pass <pass>", "Bitcoin Core RPC password").option("--cas-gateway <url>", "IPFS HTTP gateway for CAS reads").option("--keystore <path>", "Path to the keystore file (default: $XDG_DATA_HOME/btcr2/keystore.json)").option("--passphrase-file <path>", "Read the keystore passphrase from a file (unattended use)").option("--signing-key <ref>", "Key for update/deactivate signing: a URN, fingerprint prefix, or name");
416
1315
  const globals = () => this.program.opts();
417
- registerCreateCommand(this.program, factory, globals);
1316
+ registerCreateCommand(this.program, factory, keystoreFactory, globals);
418
1317
  registerResolveCommand(this.program, factory, globals);
419
- registerUpdateCommand(this.program, factory, globals);
420
- registerDeactivateCommand(this.program, factory, globals);
1318
+ registerUpdateCommand(this.program, keystoreFactory, globals);
1319
+ registerDeactivateCommand(this.program, keystoreFactory, globals);
1320
+ registerKeyCommand(this.program, keystoreFactory, globals);
1321
+ registerConfigCommand(this.program, globals);
1322
+ registerProfileCommand(this.program, globals);
1323
+ registerCompletionCommand(this.program, globals);
421
1324
  }
422
1325
  /**
423
1326
  * Runs the CLI with the provided argv or process.argv.
@@ -430,7 +1333,7 @@ var DidBtcr2Cli = class {
430
1333
  await this.program.parseAsync(normalized, { from: "node" });
431
1334
  if (!this.program.args.length) this.program.outputHelp();
432
1335
  } catch (error) {
433
- handleError(error);
1336
+ handleError(error, Boolean(this.program.opts().verbose));
434
1337
  }
435
1338
  }
436
1339
  };
@@ -439,12 +1342,12 @@ function normalizeArgv(argv) {
439
1342
  if (argv.length === 1) return ["node", argv[0]];
440
1343
  return ["node", "btcr2"];
441
1344
  }
442
- function handleError(error) {
1345
+ function handleError(error, verbose) {
443
1346
  if (error instanceof import_commander.CommanderError && (error.code === "commander.helpDisplayed" || error.code === "commander.help")) {
444
1347
  return;
445
1348
  }
446
- if (error instanceof CLIError) {
447
- console.error(error.message);
1349
+ if (error instanceof import_common3.DidMethodError) {
1350
+ console.error(verbose ? error : error.message);
448
1351
  process.exitCode ??= 1;
449
1352
  return;
450
1353
  }
@@ -454,6 +1357,7 @@ function handleError(error) {
454
1357
  // Annotate the CommonJS export names for ESM import in node:
455
1358
  0 && (module.exports = {
456
1359
  CLIError,
1360
+ CONFIG_SCHEMA_VERSION,
457
1361
  DidBtcr2Cli,
458
1362
  ENV_VARS,
459
1363
  SUPPORTED_NETWORKS,
@@ -462,11 +1366,21 @@ function handleError(error) {
462
1366
  defaultConfigPath,
463
1367
  deriveNetwork,
464
1368
  formatResult,
1369
+ getConfigPath,
1370
+ keystoreApiFactory,
465
1371
  profileToOverrides,
466
1372
  readConfigFile,
467
1373
  readEnvOverrides,
1374
+ registerCompletionCommand,
1375
+ registerConfigCommand,
468
1376
  registerCreateCommand,
469
1377
  registerDeactivateCommand,
1378
+ registerKeyCommand,
1379
+ registerProfileCommand,
470
1380
  registerResolveCommand,
471
- registerUpdateCommand
1381
+ registerUpdateCommand,
1382
+ resolveDefaultNetwork,
1383
+ setConfigPath,
1384
+ unsetConfigPath,
1385
+ writeConfigFile
472
1386
  });