@did-btcr2/cli 0.10.3 → 0.11.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 (89) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/cjs/index.js +889 -43
  3. package/dist/esm/src/cli.js +30 -12
  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/deactivate.js +21 -8
  10. package/dist/esm/src/commands/deactivate.js.map +1 -1
  11. package/dist/esm/src/commands/index.js +4 -0
  12. package/dist/esm/src/commands/index.js.map +1 -1
  13. package/dist/esm/src/commands/key.js +175 -0
  14. package/dist/esm/src/commands/key.js.map +1 -0
  15. package/dist/esm/src/commands/profile.js +63 -0
  16. package/dist/esm/src/commands/profile.js.map +1 -0
  17. package/dist/esm/src/commands/update.js +19 -9
  18. package/dist/esm/src/commands/update.js.map +1 -1
  19. package/dist/esm/src/config.js +99 -12
  20. package/dist/esm/src/config.js.map +1 -1
  21. package/dist/esm/src/keystore/atomic.js +64 -0
  22. package/dist/esm/src/keystore/atomic.js.map +1 -0
  23. package/dist/esm/src/keystore/envelope.js +123 -0
  24. package/dist/esm/src/keystore/envelope.js.map +1 -0
  25. package/dist/esm/src/keystore/error.js +16 -0
  26. package/dist/esm/src/keystore/error.js.map +1 -0
  27. package/dist/esm/src/keystore/file-backed-key-manager.js +78 -0
  28. package/dist/esm/src/keystore/file-backed-key-manager.js.map +1 -0
  29. package/dist/esm/src/keystore/file-key-store.js +184 -0
  30. package/dist/esm/src/keystore/file-key-store.js.map +1 -0
  31. package/dist/esm/src/keystore/passphrase.js +87 -0
  32. package/dist/esm/src/keystore/passphrase.js.map +1 -0
  33. package/dist/esm/src/keystore/paths.js +20 -0
  34. package/dist/esm/src/keystore/paths.js.map +1 -0
  35. package/dist/esm/src/keystore/resolve-key-ref.js +47 -0
  36. package/dist/esm/src/keystore/resolve-key-ref.js.map +1 -0
  37. package/dist/types/src/cli.d.ts +6 -2
  38. package/dist/types/src/cli.d.ts.map +1 -1
  39. package/dist/types/src/commands/completion.d.ts +5 -0
  40. package/dist/types/src/commands/completion.d.ts.map +1 -0
  41. package/dist/types/src/commands/config.d.ts +5 -0
  42. package/dist/types/src/commands/config.d.ts.map +1 -0
  43. package/dist/types/src/commands/deactivate.d.ts.map +1 -1
  44. package/dist/types/src/commands/index.d.ts +4 -0
  45. package/dist/types/src/commands/index.d.ts.map +1 -1
  46. package/dist/types/src/commands/key.d.ts +10 -0
  47. package/dist/types/src/commands/key.d.ts.map +1 -0
  48. package/dist/types/src/commands/profile.d.ts +5 -0
  49. package/dist/types/src/commands/profile.d.ts.map +1 -0
  50. package/dist/types/src/commands/update.d.ts.map +1 -1
  51. package/dist/types/src/config.d.ts +48 -5
  52. package/dist/types/src/config.d.ts.map +1 -1
  53. package/dist/types/src/keystore/atomic.d.ts +19 -0
  54. package/dist/types/src/keystore/atomic.d.ts.map +1 -0
  55. package/dist/types/src/keystore/envelope.d.ts +64 -0
  56. package/dist/types/src/keystore/envelope.d.ts.map +1 -0
  57. package/dist/types/src/keystore/error.d.ts +14 -0
  58. package/dist/types/src/keystore/error.d.ts.map +1 -0
  59. package/dist/types/src/keystore/file-backed-key-manager.d.ts +41 -0
  60. package/dist/types/src/keystore/file-backed-key-manager.d.ts.map +1 -0
  61. package/dist/types/src/keystore/file-key-store.d.ts +52 -0
  62. package/dist/types/src/keystore/file-key-store.d.ts.map +1 -0
  63. package/dist/types/src/keystore/passphrase.d.ts +20 -0
  64. package/dist/types/src/keystore/passphrase.d.ts.map +1 -0
  65. package/dist/types/src/keystore/paths.d.ts +13 -0
  66. package/dist/types/src/keystore/paths.d.ts.map +1 -0
  67. package/dist/types/src/keystore/resolve-key-ref.d.ts +19 -0
  68. package/dist/types/src/keystore/resolve-key-ref.d.ts.map +1 -0
  69. package/dist/types/src/types.d.ts +91 -0
  70. package/dist/types/src/types.d.ts.map +1 -1
  71. package/package.json +9 -4
  72. package/src/cli.ts +36 -11
  73. package/src/commands/completion.ts +40 -0
  74. package/src/commands/config.ts +84 -0
  75. package/src/commands/deactivate.ts +25 -12
  76. package/src/commands/index.ts +4 -0
  77. package/src/commands/key.ts +193 -0
  78. package/src/commands/profile.ts +65 -0
  79. package/src/commands/update.ts +23 -13
  80. package/src/config.ts +142 -20
  81. package/src/keystore/atomic.ts +73 -0
  82. package/src/keystore/envelope.ts +172 -0
  83. package/src/keystore/error.ts +16 -0
  84. package/src/keystore/file-backed-key-manager.ts +99 -0
  85. package/src/keystore/file-key-store.ts +242 -0
  86. package/src/keystore/passphrase.ts +99 -0
  87. package/src/keystore/paths.ts +20 -0
  88. package/src/keystore/resolve-key-ref.ts +62 -0
  89. package/src/types.ts +30 -11
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,21 +30,31 @@ __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
+ setConfigPath: () => setConfigPath,
47
+ unsetConfigPath: () => unsetConfigPath,
48
+ writeConfigFile: () => writeConfigFile
39
49
  });
40
50
  module.exports = __toCommonJS(index_exports);
41
51
 
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
52
+ // ../../node_modules/.pnpm/tsup@8.5.1_typescript@5.7.3_yaml@2.8.3/node_modules/tsup/assets/cjs_shims.js
43
53
  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
54
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
45
55
 
46
56
  // src/cli.ts
57
+ var import_common3 = require("@did-btcr2/common");
47
58
  var import_commander = require("commander");
48
59
 
49
60
  // src/error.ts
@@ -139,9 +150,503 @@ var import_promises = require("fs/promises");
139
150
 
140
151
  // src/config.ts
141
152
  var import_api = require("@did-btcr2/api");
153
+ var import_node_fs4 = require("fs");
154
+ var import_node_os2 = require("os");
155
+ var import_node_path4 = require("path");
156
+
157
+ // src/keystore/atomic.ts
142
158
  var import_node_fs = require("fs");
143
- var import_node_os = require("os");
144
159
  var import_node_path = require("path");
160
+
161
+ // src/keystore/error.ts
162
+ var import_common2 = require("@did-btcr2/common");
163
+ var KeyStoreError = class extends import_common2.DidMethodError {
164
+ constructor(message, type = "KeyStoreError", data) {
165
+ super(message, { type, name: type, data });
166
+ }
167
+ };
168
+
169
+ // src/keystore/atomic.ts
170
+ var isWindows = process.platform === "win32";
171
+ var permsWarned = false;
172
+ var tmpCounter = 0;
173
+ function ensureDir(dir, mode) {
174
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true, mode });
175
+ if (!isWindows) {
176
+ try {
177
+ (0, import_node_fs.chmodSync)(dir, mode);
178
+ } catch {
179
+ }
180
+ }
181
+ }
182
+ function writeFileAtomic(path, data, mode) {
183
+ const tmp = (0, import_node_path.join)((0, import_node_path.dirname)(path), `.${(0, import_node_path.basename)(path)}.${process.pid}.${tmpCounter++}.tmp`);
184
+ try {
185
+ (0, import_node_fs.writeFileSync)(tmp, data, { mode });
186
+ if (!isWindows) (0, import_node_fs.chmodSync)(tmp, mode);
187
+ (0, import_node_fs.renameSync)(tmp, path);
188
+ } catch (error) {
189
+ try {
190
+ (0, import_node_fs.rmSync)(tmp, { force: true });
191
+ } catch {
192
+ }
193
+ throw new KeyStoreError(
194
+ `Failed to write keystore at ${path}.`,
195
+ "ATOMIC_WRITE_ERROR",
196
+ { path, cause: error instanceof Error ? error.message : String(error) }
197
+ );
198
+ }
199
+ }
200
+ function assertSecurePerms(path) {
201
+ if (isWindows) {
202
+ if (!permsWarned) {
203
+ process.stderr.write(
204
+ "warning: file permissions are not enforced on Windows; protect the keystore directory manually.\n"
205
+ );
206
+ permsWarned = true;
207
+ }
208
+ return;
209
+ }
210
+ const mode = (0, import_node_fs.statSync)(path).mode & 511;
211
+ if ((mode & 63) !== 0) {
212
+ throw new KeyStoreError(
213
+ `Keystore at ${path} has insecure permissions 0${mode.toString(8)}; expected 0600.`,
214
+ "KEYSTORE_PERMISSION_ERROR",
215
+ { path, mode: `0${mode.toString(8)}` }
216
+ );
217
+ }
218
+ }
219
+
220
+ // src/keystore/file-backed-key-manager.ts
221
+ var import_key_manager = require("@did-btcr2/key-manager");
222
+
223
+ // src/keystore/file-key-store.ts
224
+ var import_node_fs2 = require("fs");
225
+ var import_node_path3 = require("path");
226
+ var import_base2 = require("@scure/base");
227
+
228
+ // src/keystore/envelope.ts
229
+ var import_chacha = require("@noble/ciphers/chacha.js");
230
+ var import_argon2 = require("@noble/hashes/argon2.js");
231
+ var import_utils = require("@noble/hashes/utils.js");
232
+ var import_base = require("@scure/base");
233
+ var ENVELOPE_VERSION = 1;
234
+ var SALT_BYTES = 16;
235
+ var NONCE_BYTES = 24;
236
+ var KEY_BYTES = 32;
237
+ var DEFAULT_ARGON_PARAMS = { t: 3, m: 65536, p: 4, dkLen: KEY_BYTES };
238
+ function buildHeader(saltB64, params) {
239
+ return {
240
+ v: ENVELOPE_VERSION,
241
+ kdf: {
242
+ alg: "argon2id",
243
+ salt: saltB64,
244
+ t: params.t,
245
+ m: params.m,
246
+ p: params.p,
247
+ dkLen: params.dkLen
248
+ },
249
+ cipher: "xchacha20poly1305"
250
+ };
251
+ }
252
+ function headerAad(header) {
253
+ return (0, import_utils.utf8ToBytes)(JSON.stringify(header));
254
+ }
255
+ function deriveKey(passphrase, salt, params) {
256
+ const password = (0, import_utils.utf8ToBytes)(passphrase);
257
+ try {
258
+ return (0, import_argon2.argon2id)(password, salt, { t: params.t, m: params.m, p: params.p, dkLen: params.dkLen });
259
+ } finally {
260
+ password.fill(0);
261
+ }
262
+ }
263
+ function encryptSecret(secret, passphrase, params = DEFAULT_ARGON_PARAMS) {
264
+ if (secret.length === 0) {
265
+ throw new KeyStoreError("Cannot encrypt an empty secret.", "ENVELOPE_ENCRYPT_ERROR");
266
+ }
267
+ const salt = (0, import_utils.randomBytes)(SALT_BYTES);
268
+ const nonce = (0, import_utils.randomBytes)(NONCE_BYTES);
269
+ const header = buildHeader(import_base.base64urlnopad.encode(salt), params);
270
+ const key = deriveKey(passphrase, salt, params);
271
+ try {
272
+ const ciphertext = (0, import_chacha.xchacha20poly1305)(key, nonce, headerAad(header)).encrypt(secret);
273
+ return {
274
+ ...header,
275
+ nonce: import_base.base64urlnopad.encode(nonce),
276
+ ciphertext: import_base.base64urlnopad.encode(ciphertext)
277
+ };
278
+ } finally {
279
+ key.fill(0);
280
+ }
281
+ }
282
+ function decryptSecret(env, passphrase) {
283
+ if (env.v !== ENVELOPE_VERSION) {
284
+ throw new KeyStoreError(
285
+ `Unsupported keystore envelope version: ${String(env.v)}.`,
286
+ "ENVELOPE_VERSION_ERROR",
287
+ { version: env.v }
288
+ );
289
+ }
290
+ if (env.kdf?.alg !== "argon2id" || env.cipher !== "xchacha20poly1305") {
291
+ throw new KeyStoreError("Unsupported keystore envelope algorithm.", "ENVELOPE_VERSION_ERROR");
292
+ }
293
+ const params = { t: env.kdf.t, m: env.kdf.m, p: env.kdf.p, dkLen: env.kdf.dkLen };
294
+ const salt = import_base.base64urlnopad.decode(env.kdf.salt);
295
+ const nonce = import_base.base64urlnopad.decode(env.nonce);
296
+ const ciphertext = import_base.base64urlnopad.decode(env.ciphertext);
297
+ const header = buildHeader(env.kdf.salt, params);
298
+ const key = deriveKey(passphrase, salt, params);
299
+ try {
300
+ return (0, import_chacha.xchacha20poly1305)(key, nonce, headerAad(header)).decrypt(ciphertext);
301
+ } catch (error) {
302
+ if (error instanceof KeyStoreError) throw error;
303
+ throw new KeyStoreError(
304
+ "Keystore decryption failed: wrong passphrase or corrupted keystore.",
305
+ "DECRYPT_ERROR"
306
+ );
307
+ } finally {
308
+ key.fill(0);
309
+ }
310
+ }
311
+
312
+ // src/keystore/paths.ts
313
+ var import_node_os = require("os");
314
+ var import_node_path2 = require("path");
315
+ function defaultKeystorePath() {
316
+ const base = process.env.XDG_DATA_HOME ?? process.env.LOCALAPPDATA ?? (0, import_node_path2.join)((0, import_node_os.homedir)(), ".local", "share");
317
+ return (0, import_node_path2.join)(base, "btcr2", "keystore.json");
318
+ }
319
+
320
+ // src/keystore/file-key-store.ts
321
+ var KEYSTORE_VERSION = 1;
322
+ var FileKeyStore = class {
323
+ #path;
324
+ #getPassphrase;
325
+ #argonParams;
326
+ #cache = /* @__PURE__ */ new Map();
327
+ #active;
328
+ constructor(options) {
329
+ this.#path = options.path ?? defaultKeystorePath();
330
+ this.#getPassphrase = options.getPassphrase;
331
+ this.#argonParams = options.argonParams ?? DEFAULT_ARGON_PARAMS;
332
+ ensureDir((0, import_node_path3.dirname)(this.#path), 448);
333
+ this.#load();
334
+ }
335
+ #load() {
336
+ if (!(0, import_node_fs2.existsSync)(this.#path)) return;
337
+ assertSecurePerms(this.#path);
338
+ let parsed;
339
+ try {
340
+ parsed = JSON.parse((0, import_node_fs2.readFileSync)(this.#path, "utf-8"));
341
+ } catch {
342
+ throw new KeyStoreError(
343
+ `Keystore at ${this.#path} is corrupt or unreadable.`,
344
+ "KEYSTORE_CORRUPT_ERROR",
345
+ { path: this.#path }
346
+ );
347
+ }
348
+ if (parsed.v !== KEYSTORE_VERSION) {
349
+ throw new KeyStoreError(
350
+ `Unsupported keystore version: ${String(parsed.v)}.`,
351
+ "KEYSTORE_VERSION_ERROR",
352
+ { version: parsed.v }
353
+ );
354
+ }
355
+ this.#active = parsed.active;
356
+ for (const [id, stored] of Object.entries(parsed.keys ?? {})) {
357
+ let publicKey;
358
+ try {
359
+ if (typeof stored.publicKey !== "string") throw new Error("missing publicKey");
360
+ publicKey = import_base2.base64urlnopad.decode(stored.publicKey);
361
+ } catch {
362
+ throw new KeyStoreError(
363
+ `Keystore entry ${id} has a malformed public key.`,
364
+ "KEYSTORE_CORRUPT_ERROR",
365
+ { path: this.#path, keyId: id }
366
+ );
367
+ }
368
+ if (publicKey.length !== 33) {
369
+ throw new KeyStoreError(
370
+ `Keystore entry ${id} has a ${publicKey.length}-byte public key; expected 33.`,
371
+ "KEYSTORE_CORRUPT_ERROR",
372
+ { path: this.#path, keyId: id }
373
+ );
374
+ }
375
+ this.#cache.set(id, {
376
+ publicKey,
377
+ ...stored.tags && { tags: stored.tags },
378
+ ...stored.secret && { secret: stored.secret }
379
+ });
380
+ }
381
+ }
382
+ #flush() {
383
+ const keys = {};
384
+ for (const [id, entry] of this.#cache) {
385
+ keys[id] = {
386
+ publicKey: import_base2.base64urlnopad.encode(entry.publicKey),
387
+ ...entry.tags && { tags: entry.tags },
388
+ ...entry.secret && { secret: entry.secret }
389
+ };
390
+ }
391
+ const file = {
392
+ v: KEYSTORE_VERSION,
393
+ ...this.#active && { active: this.#active },
394
+ keys
395
+ };
396
+ writeFileAtomic(this.#path, `${JSON.stringify(file, null, 2)}
397
+ `, 384);
398
+ }
399
+ get(id) {
400
+ const entry = this.#cache.get(id);
401
+ if (!entry) return void 0;
402
+ const result = {
403
+ publicKey: entry.publicKey,
404
+ ...entry.tags && { tags: entry.tags }
405
+ };
406
+ if (entry.secret) {
407
+ const sealed = entry.secret;
408
+ Object.defineProperty(result, "secretKey", {
409
+ configurable: true,
410
+ enumerable: false,
411
+ get: () => {
412
+ entry.decrypted ??= decryptSecret(sealed, this.#getPassphrase());
413
+ return entry.decrypted;
414
+ }
415
+ });
416
+ }
417
+ return result;
418
+ }
419
+ has(id) {
420
+ return this.#cache.has(id);
421
+ }
422
+ set(id, value) {
423
+ const secret = value.secretKey ? encryptSecret(value.secretKey, this.#getPassphrase(), this.#argonParams) : void 0;
424
+ this.#cache.set(id, {
425
+ publicKey: value.publicKey,
426
+ ...value.tags && { tags: value.tags },
427
+ ...secret && { secret },
428
+ ...value.secretKey && { decrypted: value.secretKey }
429
+ });
430
+ this.#flush();
431
+ }
432
+ delete(id) {
433
+ const existed = this.#cache.delete(id);
434
+ if (existed) {
435
+ if (this.#active === id) this.#active = void 0;
436
+ this.#flush();
437
+ }
438
+ return existed;
439
+ }
440
+ clear() {
441
+ this.#cache.clear();
442
+ this.#active = void 0;
443
+ this.#flush();
444
+ }
445
+ /** All stored values with secret keys omitted. Never decrypts, never prompts. */
446
+ list() {
447
+ return this.entries().map(([, value]) => value);
448
+ }
449
+ /**
450
+ * All entries as id-value tuples with secret keys omitted. Never decrypts,
451
+ * never prompts: {@link FileKeyStore.get} is the only secret-materializing
452
+ * path, so callers that only need identifiers (such as `listKeys`) do not
453
+ * force a passphrase prompt. This deviates intentionally from the in-memory
454
+ * store, which returns stored values verbatim.
455
+ */
456
+ entries() {
457
+ const out = [];
458
+ for (const [id, entry] of this.#cache) {
459
+ out.push([id, {
460
+ publicKey: entry.publicKey,
461
+ ...entry.tags && { tags: entry.tags }
462
+ }]);
463
+ }
464
+ return out;
465
+ }
466
+ close() {
467
+ for (const entry of this.#cache.values()) {
468
+ entry.decrypted?.fill(0);
469
+ entry.decrypted = void 0;
470
+ }
471
+ this.#cache.clear();
472
+ }
473
+ /** The persisted active-key identifier, or undefined if none is set. */
474
+ getActive() {
475
+ return this.#active;
476
+ }
477
+ /**
478
+ * Persists the active-key pointer in the keystore file. Passing undefined
479
+ * clears it. Throws if the identifier is not a known key.
480
+ */
481
+ setActive(id) {
482
+ if (id !== void 0 && !this.#cache.has(id)) {
483
+ throw new KeyStoreError(`Cannot set unknown key as active: ${id}.`, "KEY_NOT_FOUND_ERROR", { keyId: id });
484
+ }
485
+ this.#active = id;
486
+ this.#flush();
487
+ }
488
+ };
489
+
490
+ // src/keystore/file-backed-key-manager.ts
491
+ var FileBackedKeyManager = class {
492
+ /** Capability probe: the local store supports exporting secret material. */
493
+ canExport = true;
494
+ #store;
495
+ #inner;
496
+ constructor(options) {
497
+ this.#store = new FileKeyStore(options);
498
+ this.#inner = new import_key_manager.LocalKeyManager(this.#store);
499
+ const active = this.#store.getActive();
500
+ if (active && this.#store.has(active)) this.#inner.setActiveKey(active);
501
+ }
502
+ get activeKeyId() {
503
+ return this.#inner.activeKeyId;
504
+ }
505
+ setActiveKey(id) {
506
+ this.#inner.setActiveKey(id);
507
+ this.#store.setActive(id);
508
+ }
509
+ importKey(keyPair, options) {
510
+ const id = this.#inner.importKey(keyPair, options);
511
+ if (options?.setActive) this.#store.setActive(id);
512
+ return id;
513
+ }
514
+ generateKey(options) {
515
+ const id = this.#inner.generateKey(options);
516
+ if (options?.setActive) this.#store.setActive(id);
517
+ return id;
518
+ }
519
+ removeKey(id, options) {
520
+ this.#inner.removeKey(id, options);
521
+ }
522
+ listKeys() {
523
+ return this.#inner.listKeys();
524
+ }
525
+ getPublicKey(id) {
526
+ return this.#inner.getPublicKey(id);
527
+ }
528
+ getEntry(id) {
529
+ return this.#inner.getEntry(id);
530
+ }
531
+ sign(data, id, options) {
532
+ return this.#inner.sign(data, id, options);
533
+ }
534
+ verify(signature, data, id, options) {
535
+ return this.#inner.verify(signature, data, id, options);
536
+ }
537
+ digest(data) {
538
+ return this.#inner.digest(data);
539
+ }
540
+ exportKey(id) {
541
+ return this.#inner.exportKey(id);
542
+ }
543
+ };
544
+
545
+ // src/keystore/passphrase.ts
546
+ var import_node_fs3 = require("fs");
547
+ var ENV_KEYSTORE_PASSPHRASE = "BTCR2_KEYSTORE_PASSPHRASE";
548
+ function acquirePassphrase(options = {}) {
549
+ const fromEnv = process.env[ENV_KEYSTORE_PASSPHRASE];
550
+ if (fromEnv) return assertNonEmpty(fromEnv.replace(/\r?\n$/, ""));
551
+ if (options.passphraseFile) {
552
+ return assertNonEmpty((0, import_node_fs3.readFileSync)(options.passphraseFile, "utf-8").replace(/\r?\n$/, ""));
553
+ }
554
+ if (!process.stdin.isTTY) {
555
+ throw new KeyStoreError(
556
+ `No passphrase available. Set ${ENV_KEYSTORE_PASSPHRASE}, pass --passphrase-file, or run in a terminal.`,
557
+ "PASSPHRASE_REQUIRED_ERROR"
558
+ );
559
+ }
560
+ const passphrase = promptHidden(options.prompt ?? "Keystore passphrase: ");
561
+ if (options.confirm) {
562
+ const again = promptHidden("Confirm passphrase: ");
563
+ if (passphrase !== again) {
564
+ throw new KeyStoreError("Passphrases did not match.", "PASSPHRASE_MISMATCH_ERROR");
565
+ }
566
+ }
567
+ return assertNonEmpty(passphrase);
568
+ }
569
+ function assertNonEmpty(passphrase) {
570
+ if (passphrase.trim() === "") {
571
+ throw new KeyStoreError("A non-empty keystore passphrase is required.", "PASSPHRASE_REQUIRED_ERROR");
572
+ }
573
+ return passphrase;
574
+ }
575
+ function promptHidden(label) {
576
+ process.stderr.write(label);
577
+ const stdin = process.stdin;
578
+ const wasRaw = stdin.isRaw ?? false;
579
+ stdin.setRawMode(true);
580
+ const byte = Buffer.alloc(1);
581
+ const bytes = [];
582
+ try {
583
+ for (; ; ) {
584
+ let read = 0;
585
+ try {
586
+ read = (0, import_node_fs3.readSync)(stdin.fd, byte, 0, 1, null);
587
+ } catch (error) {
588
+ const code = error.code;
589
+ if (code === "EAGAIN") continue;
590
+ if (code === "EOF") break;
591
+ throw error;
592
+ }
593
+ if (read === 0) break;
594
+ const ch = byte[0];
595
+ if (ch === 10 || ch === 13) break;
596
+ if (ch === 3) {
597
+ throw new KeyStoreError("Passphrase entry aborted.", "PASSPHRASE_REQUIRED_ERROR");
598
+ }
599
+ if (ch === 127 || ch === 8) {
600
+ bytes.pop();
601
+ continue;
602
+ }
603
+ bytes.push(ch);
604
+ }
605
+ } finally {
606
+ stdin.setRawMode(wasRaw);
607
+ process.stderr.write("\n");
608
+ }
609
+ return Buffer.from(bytes).toString("utf-8");
610
+ }
611
+
612
+ // src/config.ts
613
+ var CONFIG_SCHEMA_VERSION = 1;
614
+ function writeConfigFile(path, mutate) {
615
+ const raw = readConfigFile(path) ?? {};
616
+ mutate(raw);
617
+ raw.schemaVersion = CONFIG_SCHEMA_VERSION;
618
+ ensureDir((0, import_node_path4.dirname)(path), 448);
619
+ writeFileAtomic(path, `${JSON.stringify(raw, null, 2)}
620
+ `, 384);
621
+ }
622
+ function getConfigPath(config, path) {
623
+ return path.split(".").reduce(
624
+ (node, key) => node?.[key],
625
+ config
626
+ );
627
+ }
628
+ function setConfigPath(config, path, value) {
629
+ const keys = path.split(".");
630
+ const last = keys.pop();
631
+ if (!last) throw new CLIError("Config path must be non-empty.", "INVALID_ARGUMENT_ERROR");
632
+ let node = config;
633
+ for (const key of keys) {
634
+ if (typeof node[key] !== "object" || node[key] === null) node[key] = {};
635
+ node = node[key];
636
+ }
637
+ node[last] = value;
638
+ }
639
+ function unsetConfigPath(config, path) {
640
+ const keys = path.split(".");
641
+ const last = keys.pop();
642
+ if (!last) return;
643
+ let node = config;
644
+ for (const key of keys) {
645
+ node = node?.[key];
646
+ if (!node) return;
647
+ }
648
+ delete node[last];
649
+ }
145
650
  var ENV_VARS = {
146
651
  BTC_REST: "BTCR2_BTC_REST",
147
652
  BTC_RPC_URL: "BTCR2_BTC_RPC_URL",
@@ -160,12 +665,12 @@ function readEnvOverrides() {
160
665
  };
161
666
  }
162
667
  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");
668
+ const base = process.env.XDG_CONFIG_HOME ?? process.env.APPDATA ?? (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".config");
669
+ return (0, import_node_path4.join)(base, "btcr2", "config.json");
165
670
  }
166
671
  function readConfigFile(path) {
167
672
  try {
168
- const content = (0, import_node_fs.readFileSync)(path, "utf-8");
673
+ const content = (0, import_node_fs4.readFileSync)(path, "utf-8");
169
674
  return JSON.parse(content);
170
675
  } catch {
171
676
  return void 0;
@@ -182,11 +687,11 @@ function profileToOverrides(config, profileName) {
182
687
  casGateway: profile.cas?.gateway
183
688
  };
184
689
  }
185
- function defaultApiFactory(network, overrides) {
186
- if (!network) return (0, import_api.createApi)();
690
+ function resolveConnectionConfig(network, overrides) {
691
+ if (!network) return {};
187
692
  const configPath = overrides?.config ?? defaultConfigPath();
188
- const profileName = overrides?.profile ?? network;
189
693
  const file = readConfigFile(configPath);
694
+ const profileName = overrides?.profile ?? file?.defaults?.profile ?? network;
190
695
  const fileOverrides = file ? profileToOverrides(file, profileName) : {};
191
696
  const env = readEnvOverrides();
192
697
  const merged = {
@@ -208,7 +713,22 @@ function defaultApiFactory(network, overrides) {
208
713
  };
209
714
  }
210
715
  const cas = merged.casGateway ? { gateway: merged.casGateway } : void 0;
211
- return (0, import_api.createApi)({ btc, ...cas && { cas } });
716
+ return { btc, ...cas && { cas } };
717
+ }
718
+ function defaultApiFactory(network, overrides) {
719
+ return (0, import_api.createApi)(resolveConnectionConfig(network, overrides));
720
+ }
721
+ function buildKeystoreKms(overrides) {
722
+ return new FileBackedKeyManager({
723
+ path: overrides?.keystore ?? defaultKeystorePath(),
724
+ getPassphrase: () => acquirePassphrase({ passphraseFile: overrides?.passphraseFile })
725
+ });
726
+ }
727
+ function keystoreApiFactory(network, overrides) {
728
+ return (0, import_api.createApi)({
729
+ ...resolveConnectionConfig(network, overrides),
730
+ kms: buildKeystoreKms(overrides)
731
+ });
212
732
  }
213
733
  function deriveNetwork(did) {
214
734
  const { network } = import_api.Identifier.decode(did);
@@ -261,6 +781,47 @@ async function validateResolveOptions(options) {
261
781
  return { identifier: options.identifier, options: resolutionOptions };
262
782
  }
263
783
 
784
+ // src/commands/update.ts
785
+ var import_key_manager2 = require("@did-btcr2/key-manager");
786
+
787
+ // src/keystore/resolve-key-ref.ts
788
+ function fingerprintOf(id) {
789
+ return /^urn:kms:secp256k1:([0-9a-f]{32})$/.exec(id)?.[1];
790
+ }
791
+ function resolveKeyRef(kms, ref) {
792
+ if (!ref) {
793
+ if (!kms.activeKeyId) {
794
+ throw new CLIError(
795
+ "No key specified and no active key is set. Use --key <ref> or set one with `btcr2 key use <ref>`.",
796
+ "INVALID_ARGUMENT_ERROR"
797
+ );
798
+ }
799
+ return kms.activeKeyId;
800
+ }
801
+ const ids = kms.listKeys();
802
+ if (ids.includes(ref)) return ref;
803
+ const prefix = ref.toLowerCase();
804
+ const byPrefix = ids.filter((id) => fingerprintOf(id)?.startsWith(prefix));
805
+ if (byPrefix.length === 1) return byPrefix[0];
806
+ if (byPrefix.length > 1) {
807
+ throw new CLIError(
808
+ `Ambiguous key reference "${ref}" matches ${byPrefix.length} keys by fingerprint.`,
809
+ "KEY_REF_AMBIGUOUS_ERROR",
810
+ { ref }
811
+ );
812
+ }
813
+ const byName = ids.filter((id) => kms.getEntry(id).tags?.name === ref);
814
+ if (byName.length === 1) return byName[0];
815
+ if (byName.length > 1) {
816
+ throw new CLIError(
817
+ `Ambiguous key name "${ref}" matches ${byName.length} keys.`,
818
+ "KEY_REF_AMBIGUOUS_ERROR",
819
+ { ref }
820
+ );
821
+ }
822
+ throw new CLIError(`No key matches reference "${ref}".`, "KEY_NOT_FOUND_ERROR", { ref });
823
+ }
824
+
264
825
  // src/commands/update.ts
265
826
  function registerUpdateCommand(program, factory, globals) {
266
827
  program.command("update").description("Update a did:btcr2 document.").requiredOption(
@@ -282,6 +843,13 @@ function registerUpdateCommand(program, factory, globals) {
282
843
  "Beacon ID as a JSON string",
283
844
  parseJsonArg("--beacon-id")
284
845
  ).action(async (options) => {
846
+ if (!/^\d+$/.test(options.sourceVersionId)) {
847
+ throw new CLIError(
848
+ "--source-version-id must be a non-negative integer.",
849
+ "INVALID_ARGUMENT_ERROR",
850
+ { value: options.sourceVersionId }
851
+ );
852
+ }
285
853
  const parsed = {
286
854
  sourceDocument: options.sourceDocument,
287
855
  patches: options.patches,
@@ -297,15 +865,19 @@ function registerUpdateCommand(program, factory, globals) {
297
865
  options
298
866
  );
299
867
  }
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
- );
868
+ const network = deriveNetwork(did);
869
+ const api = factory(network, globals());
870
+ const keyId = resolveKeyRef(api.kms.kms, globals().signingKey);
871
+ const signer = new import_key_manager2.KeyManagerSigner(api.kms.kms, keyId);
872
+ const data = await api.btcr2.update({
873
+ sourceDocument: parsed.sourceDocument,
874
+ patches: parsed.patches,
875
+ sourceVersionId: parsed.sourceVersionId,
876
+ verificationMethodId: parsed.verificationMethodId,
877
+ beaconId: parsed.beaconId,
878
+ signer
879
+ });
880
+ console.log(formatResult({ action: "update", data }, globals()));
309
881
  });
310
882
  }
311
883
  function parseJsonArg(flagName) {
@@ -323,6 +895,7 @@ function parseJsonArg(flagName) {
323
895
  }
324
896
 
325
897
  // src/commands/deactivate.ts
898
+ var import_key_manager3 = require("@did-btcr2/key-manager");
326
899
  var DEACTIVATION_PATCH = [{ op: "add", path: "/deactivated", value: true }];
327
900
  function registerDeactivateCommand(program, factory, globals) {
328
901
  program.command("deactivate").alias("delete").description("Deactivate the did:btcr2 identifier permanently. This is irreversible.").requiredOption(
@@ -340,6 +913,13 @@ function registerDeactivateCommand(program, factory, globals) {
340
913
  "Beacon ID as a JSON string",
341
914
  parseJsonArg2("--beacon-id")
342
915
  ).action(async (options) => {
916
+ if (!/^\d+$/.test(options.sourceVersionId)) {
917
+ throw new CLIError(
918
+ "--source-version-id must be a non-negative integer.",
919
+ "INVALID_ARGUMENT_ERROR",
920
+ { value: options.sourceVersionId }
921
+ );
922
+ }
343
923
  const parsed = {
344
924
  sourceDocument: options.sourceDocument,
345
925
  patches: DEACTIVATION_PATCH,
@@ -355,15 +935,19 @@ function registerDeactivateCommand(program, factory, globals) {
355
935
  options
356
936
  );
357
937
  }
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
- );
938
+ const network = deriveNetwork(did);
939
+ const api = factory(network, globals());
940
+ const keyId = resolveKeyRef(api.kms.kms, globals().signingKey);
941
+ const signer = new import_key_manager3.KeyManagerSigner(api.kms.kms, keyId);
942
+ const data = await api.btcr2.update({
943
+ sourceDocument: parsed.sourceDocument,
944
+ patches: parsed.patches,
945
+ sourceVersionId: parsed.sourceVersionId,
946
+ verificationMethodId: parsed.verificationMethodId,
947
+ beaconId: parsed.beaconId,
948
+ signer
949
+ });
950
+ console.log(formatResult({ action: "deactivate", data }, globals()));
367
951
  });
368
952
  }
369
953
  function parseJsonArg2(flagName) {
@@ -380,19 +964,263 @@ function parseJsonArg2(flagName) {
380
964
  };
381
965
  }
382
966
 
967
+ // src/commands/key.ts
968
+ var import_keypair = require("@did-btcr2/keypair");
969
+ var import_utils2 = require("@noble/hashes/utils.js");
970
+ var import_node_fs5 = require("fs");
971
+ function registerKeyCommand(program, factory, globals) {
972
+ const key = program.command("key").description("Manage keypairs in the encrypted keystore.");
973
+ const print = (result) => console.log(formatResult(result, globals()));
974
+ 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) => {
975
+ const api = factory(void 0, globals());
976
+ assertNameAvailable(api.kms.kms, options.name);
977
+ const setActive = options.setActive ?? false;
978
+ const id = api.kms.generateKey({ ...options.name && { tags: { name: options.name } }, setActive });
979
+ print({ action: "key-generate", data: { keyId: id, publicKey: (0, import_utils2.bytesToHex)(api.kms.getPublicKey(id)), active: setActive } });
980
+ });
981
+ key.command("list").alias("ls").description("List stored keys.").action(() => {
982
+ const kms = factory(void 0, globals()).kms.kms;
983
+ const active = kms.activeKeyId;
984
+ const data = kms.listKeys().map((id) => {
985
+ const entry = kms.getEntry(id);
986
+ return {
987
+ keyId: id,
988
+ fingerprint: id.split(":").pop() ?? id,
989
+ ...entry.tags?.name && { name: entry.tags.name },
990
+ active: id === active
991
+ };
992
+ });
993
+ print({ action: "key-list", data });
994
+ });
995
+ key.command("show <ref>").description("Show a key's public material and tags. Never prints the secret.").action((ref) => {
996
+ const kms = factory(void 0, globals()).kms.kms;
997
+ const id = resolveKeyRef(kms, ref);
998
+ const entry = kms.getEntry(id);
999
+ print({ action: "key-show", data: { keyId: id, publicKey: (0, import_utils2.bytesToHex)(entry.publicKey), ...entry.tags && { tags: entry.tags } } });
1000
+ });
1001
+ 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) => {
1002
+ if (Boolean(options.secretFile) === Boolean(options.public)) {
1003
+ throw new CLIError("Provide exactly one of --secret-file or --public.", "INVALID_ARGUMENT_ERROR");
1004
+ }
1005
+ const api = factory(void 0, globals());
1006
+ assertNameAvailable(api.kms.kms, options.name);
1007
+ 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") });
1008
+ const setActive = options.setActive ?? false;
1009
+ const id = api.kms.import(keyPair, { ...options.name && { tags: { name: options.name } }, setActive });
1010
+ print({ action: "key-import", data: { keyId: id, publicKey: (0, import_utils2.bytesToHex)(api.kms.getPublicKey(id)), watchOnly: !options.secretFile, active: setActive } });
1011
+ });
1012
+ 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) => {
1013
+ const api = factory(void 0, globals());
1014
+ const id = resolveKeyRef(api.kms.kms, ref);
1015
+ if (!options.secret) {
1016
+ print({ action: "key-export", data: { keyId: id, publicKey: (0, import_utils2.bytesToHex)(api.kms.getPublicKey(id)) } });
1017
+ return;
1018
+ }
1019
+ if (!options.out) {
1020
+ throw new CLIError("Exporting a secret requires --out <file> so it is not written to the terminal.", "INVALID_ARGUMENT_ERROR");
1021
+ }
1022
+ const keyPair = api.kms.export(id);
1023
+ if (!keyPair.hasSecretKey) {
1024
+ throw new CLIError(`Key ${id} is watch-only and has no secret to export.`, "INVALID_ARGUMENT_ERROR", { keyId: id });
1025
+ }
1026
+ process.stderr.write("warning: writing an unencrypted secret key to disk. Protect this file and delete it when done.\n");
1027
+ writeSecretFile(options.out, (0, import_utils2.bytesToHex)(keyPair.secretKey.bytes));
1028
+ print({ action: "key-export", data: { keyId: id, secretWrittenTo: options.out } });
1029
+ });
1030
+ 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) => {
1031
+ const api = factory(void 0, globals());
1032
+ const id = resolveKeyRef(api.kms.kms, ref);
1033
+ api.kms.removeKey(id, { force: options.force ?? false });
1034
+ print({ action: "key-delete", data: { keyId: id, deleted: true } });
1035
+ });
1036
+ key.command("use <ref>").description("Set the active key, persisted across invocations.").action((ref) => {
1037
+ const api = factory(void 0, globals());
1038
+ const id = resolveKeyRef(api.kms.kms, ref);
1039
+ api.kms.setActive(id);
1040
+ print({ action: "key-use", data: { keyId: id, active: true } });
1041
+ });
1042
+ }
1043
+ function assertNameAvailable(kms, name) {
1044
+ if (!name) return;
1045
+ if (kms.listKeys().some((id) => kms.getEntry(id).tags?.name === name)) {
1046
+ throw new CLIError(`A key named "${name}" already exists.`, "INVALID_ARGUMENT_ERROR", { name });
1047
+ }
1048
+ }
1049
+ function parseHex(hex, expectedBytes, label) {
1050
+ let bytes;
1051
+ try {
1052
+ bytes = (0, import_utils2.hexToBytes)(hex.trim());
1053
+ } catch {
1054
+ throw new CLIError(`Invalid hex for ${label}.`, "INVALID_ARGUMENT_ERROR", { label });
1055
+ }
1056
+ if (bytes.length !== expectedBytes) {
1057
+ throw new CLIError(
1058
+ `${label} must be ${expectedBytes} bytes (${expectedBytes * 2} hex chars), got ${bytes.length}.`,
1059
+ "INVALID_ARGUMENT_ERROR",
1060
+ { label }
1061
+ );
1062
+ }
1063
+ return bytes;
1064
+ }
1065
+ function readHexFile(path, expectedBytes, label) {
1066
+ let content;
1067
+ try {
1068
+ content = (0, import_node_fs5.readFileSync)(path, "utf-8");
1069
+ } catch {
1070
+ throw new CLIError(`Cannot read ${label} at ${path}.`, "INVALID_ARGUMENT_ERROR", { label, path });
1071
+ }
1072
+ return parseHex(content, expectedBytes, label);
1073
+ }
1074
+ function writeSecretFile(path, contents) {
1075
+ let fd;
1076
+ try {
1077
+ fd = (0, import_node_fs5.openSync)(path, "wx", 384);
1078
+ } catch (error) {
1079
+ if (error.code === "EEXIST") {
1080
+ throw new CLIError(`Refusing to overwrite existing file ${path}. Choose a new --out path.`, "INVALID_ARGUMENT_ERROR", { path });
1081
+ }
1082
+ throw error;
1083
+ }
1084
+ try {
1085
+ (0, import_node_fs5.writeFileSync)(fd, contents);
1086
+ } finally {
1087
+ (0, import_node_fs5.closeSync)(fd);
1088
+ }
1089
+ }
1090
+
1091
+ // src/commands/config.ts
1092
+ var import_node_fs6 = require("fs");
1093
+ var import_node_path5 = require("path");
1094
+ function registerConfigCommand(program, globals) {
1095
+ const config = program.command("config").description("Read and write CLI configuration.");
1096
+ const path = () => globals().config ?? defaultConfigPath();
1097
+ const print = (result) => console.log(formatResult(result, globals()));
1098
+ config.command("init").description("Create a default config file with one profile per network.").option("--force", "Overwrite an existing config file.", false).action((options) => {
1099
+ const p = path();
1100
+ if ((0, import_node_fs6.existsSync)(p) && !options.force) {
1101
+ throw new CLIError(`Config already exists at ${p}. Use --force to overwrite.`, "INVALID_ARGUMENT_ERROR", { path: p });
1102
+ }
1103
+ const scaffold = {
1104
+ schemaVersion: CONFIG_SCHEMA_VERSION,
1105
+ defaults: { output: "text" },
1106
+ profiles: Object.fromEntries(SUPPORTED_NETWORKS.map((n) => [n, {}]))
1107
+ };
1108
+ ensureDir((0, import_node_path5.dirname)(p), 448);
1109
+ writeFileAtomic(p, `${JSON.stringify(scaffold, null, 2)}
1110
+ `, 384);
1111
+ print({ action: "config-init", data: { path: p } });
1112
+ });
1113
+ config.command("get [path]").description("Print a value at a dotted path, or the whole config.").action((dotted) => {
1114
+ const file = readConfigFile(path()) ?? {};
1115
+ print({ action: "config-get", data: (dotted ? getConfigPath(file, dotted) : file) ?? null });
1116
+ });
1117
+ 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) => {
1118
+ writeConfigFile(path(), (raw) => setConfigPath(raw, dotted, parseValue(value)));
1119
+ print({ action: "config-set", data: { path: dotted } });
1120
+ });
1121
+ config.command("unset <path>").description("Delete a value at a dotted path.").action((dotted) => {
1122
+ writeConfigFile(path(), (raw) => unsetConfigPath(raw, dotted));
1123
+ print({ action: "config-unset", data: { path: dotted } });
1124
+ });
1125
+ config.command("list").alias("ls").description("Print the entire config file.").action(() => {
1126
+ print({ action: "config-list", data: readConfigFile(path()) ?? {} });
1127
+ });
1128
+ }
1129
+ function parseValue(value) {
1130
+ try {
1131
+ return JSON.parse(value);
1132
+ } catch {
1133
+ return value;
1134
+ }
1135
+ }
1136
+
1137
+ // src/commands/profile.ts
1138
+ function registerProfileCommand(program, globals) {
1139
+ const profile = program.command("profile").description("Manage configuration profiles.");
1140
+ const path = () => globals().config ?? defaultConfigPath();
1141
+ const print = (result) => console.log(formatResult(result, globals()));
1142
+ profile.command("add <name>").description("Add an empty profile.").action((name) => {
1143
+ writeConfigFile(path(), (raw) => {
1144
+ if (raw.profiles === void 0 || raw.profiles === null) raw.profiles = {};
1145
+ const profiles = raw.profiles;
1146
+ if (profiles[name]) throw new CLIError(`Profile "${name}" already exists.`, "INVALID_ARGUMENT_ERROR", { name });
1147
+ profiles[name] = {};
1148
+ });
1149
+ print({ action: "profile-add", data: { profile: name } });
1150
+ });
1151
+ profile.command("use <name>").description("Set the active profile (writes defaults.profile).").action((name) => {
1152
+ writeConfigFile(path(), (raw) => {
1153
+ if (raw.defaults === void 0 || raw.defaults === null) raw.defaults = {};
1154
+ raw.defaults.profile = name;
1155
+ });
1156
+ print({ action: "profile-use", data: { profile: name } });
1157
+ });
1158
+ profile.command("show [name]").description("Show a profile (defaults to the active profile).").action((name) => {
1159
+ const file = readConfigFile(path()) ?? {};
1160
+ const target = name ?? file.defaults?.profile;
1161
+ if (!target) {
1162
+ throw new CLIError("No profile specified and no active profile is set.", "INVALID_ARGUMENT_ERROR");
1163
+ }
1164
+ const data = file.profiles?.[target];
1165
+ if (!data) {
1166
+ throw new CLIError(`Profile "${target}" not found.`, "INVALID_ARGUMENT_ERROR", { profile: target });
1167
+ }
1168
+ print({ action: "profile-show", data: { profile: target, ...data } });
1169
+ });
1170
+ profile.command("remove <name>").alias("rm").description("Remove a profile.").action((name) => {
1171
+ writeConfigFile(path(), (raw) => {
1172
+ const profiles = raw.profiles;
1173
+ if (!profiles?.[name]) throw new CLIError(`Profile "${name}" not found.`, "INVALID_ARGUMENT_ERROR", { name });
1174
+ delete profiles[name];
1175
+ });
1176
+ print({ action: "profile-remove", data: { profile: name } });
1177
+ });
1178
+ }
1179
+
1180
+ // src/commands/completion.ts
1181
+ var COMMANDS = "create resolve read update deactivate delete key config profile completion";
1182
+ function registerCompletionCommand(program, _globals) {
1183
+ program.command("completion [shell]").description("Print a shell completion script (bash, zsh, or fish) to stdout.").action((shell = "bash") => {
1184
+ console.log(completionScript(shell));
1185
+ });
1186
+ }
1187
+ function completionScript(shell) {
1188
+ switch (shell) {
1189
+ case "bash":
1190
+ return [
1191
+ '# btcr2 bash completion. Install with: eval "$(btcr2 completion bash)"',
1192
+ '_btcr2() { COMPREPLY=( $(compgen -W "' + COMMANDS + '" -- "${COMP_WORDS[COMP_CWORD]}") ); }',
1193
+ "complete -F _btcr2 btcr2"
1194
+ ].join("\n");
1195
+ case "zsh":
1196
+ return [
1197
+ '# btcr2 zsh completion. Install with: eval "$(btcr2 completion zsh)"',
1198
+ "_btcr2() { compadd " + COMMANDS + " }",
1199
+ "compdef _btcr2 btcr2"
1200
+ ].join("\n");
1201
+ case "fish":
1202
+ return [
1203
+ "# btcr2 fish completion. Save to ~/.config/fish/completions/btcr2.fish",
1204
+ 'complete -c btcr2 -f -a "' + COMMANDS + '"'
1205
+ ].join("\n");
1206
+ default:
1207
+ throw new CLIError(`Unsupported shell "${shell}". Use bash, zsh, or fish.`, "INVALID_ARGUMENT_ERROR", { shell });
1208
+ }
1209
+ }
1210
+
383
1211
  // src/version.ts
384
- var import_node_fs2 = require("fs");
385
- var import_node_path2 = require("path");
1212
+ var import_node_fs7 = require("fs");
1213
+ var import_node_path6 = require("path");
386
1214
  var import_node_url = require("url");
387
1215
  function readVersion() {
388
- let dir = (0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
1216
+ let dir = (0, import_node_path6.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
389
1217
  for (let i = 0; i < 5; i++) {
390
1218
  try {
391
- const pkg = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path2.join)(dir, "package.json"), "utf-8"));
1219
+ const pkg = JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path6.join)(dir, "package.json"), "utf-8"));
392
1220
  if (pkg.name === "@did-btcr2/cli") return pkg.version;
393
1221
  } catch {
394
1222
  }
395
- dir = (0, import_node_path2.dirname)(dir);
1223
+ dir = (0, import_node_path6.dirname)(dir);
396
1224
  }
397
1225
  return "0.0.0";
398
1226
  }
@@ -409,15 +1237,23 @@ var DidBtcr2Cli = class {
409
1237
  * {@link defaultApiFactory} which uses public endpoints (mempool.space)
410
1238
  * for known networks and localhost Polar for regtest.
411
1239
  *
412
- * @param factory - Optional API factory. Defaults to {@link defaultApiFactory}.
1240
+ * @param factory - Optional API factory for keystore-free commands (create,
1241
+ * resolve). Defaults to {@link defaultApiFactory}.
1242
+ * @param keystoreFactory - Optional keystore-aware API factory for commands
1243
+ * that need a signing identity (key, update, deactivate). Defaults to
1244
+ * {@link keystoreApiFactory}.
413
1245
  */
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");
1246
+ constructor(factory = defaultApiFactory, keystoreFactory = keystoreApiFactory) {
1247
+ 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
1248
  const globals = () => this.program.opts();
417
1249
  registerCreateCommand(this.program, factory, globals);
418
1250
  registerResolveCommand(this.program, factory, globals);
419
- registerUpdateCommand(this.program, factory, globals);
420
- registerDeactivateCommand(this.program, factory, globals);
1251
+ registerUpdateCommand(this.program, keystoreFactory, globals);
1252
+ registerDeactivateCommand(this.program, keystoreFactory, globals);
1253
+ registerKeyCommand(this.program, keystoreFactory, globals);
1254
+ registerConfigCommand(this.program, globals);
1255
+ registerProfileCommand(this.program, globals);
1256
+ registerCompletionCommand(this.program, globals);
421
1257
  }
422
1258
  /**
423
1259
  * Runs the CLI with the provided argv or process.argv.
@@ -430,7 +1266,7 @@ var DidBtcr2Cli = class {
430
1266
  await this.program.parseAsync(normalized, { from: "node" });
431
1267
  if (!this.program.args.length) this.program.outputHelp();
432
1268
  } catch (error) {
433
- handleError(error);
1269
+ handleError(error, Boolean(this.program.opts().verbose));
434
1270
  }
435
1271
  }
436
1272
  };
@@ -439,12 +1275,12 @@ function normalizeArgv(argv) {
439
1275
  if (argv.length === 1) return ["node", argv[0]];
440
1276
  return ["node", "btcr2"];
441
1277
  }
442
- function handleError(error) {
1278
+ function handleError(error, verbose) {
443
1279
  if (error instanceof import_commander.CommanderError && (error.code === "commander.helpDisplayed" || error.code === "commander.help")) {
444
1280
  return;
445
1281
  }
446
- if (error instanceof CLIError) {
447
- console.error(error.message);
1282
+ if (error instanceof import_common3.DidMethodError) {
1283
+ console.error(verbose ? error : error.message);
448
1284
  process.exitCode ??= 1;
449
1285
  return;
450
1286
  }
@@ -454,6 +1290,7 @@ function handleError(error) {
454
1290
  // Annotate the CommonJS export names for ESM import in node:
455
1291
  0 && (module.exports = {
456
1292
  CLIError,
1293
+ CONFIG_SCHEMA_VERSION,
457
1294
  DidBtcr2Cli,
458
1295
  ENV_VARS,
459
1296
  SUPPORTED_NETWORKS,
@@ -462,11 +1299,20 @@ function handleError(error) {
462
1299
  defaultConfigPath,
463
1300
  deriveNetwork,
464
1301
  formatResult,
1302
+ getConfigPath,
1303
+ keystoreApiFactory,
465
1304
  profileToOverrides,
466
1305
  readConfigFile,
467
1306
  readEnvOverrides,
1307
+ registerCompletionCommand,
1308
+ registerConfigCommand,
468
1309
  registerCreateCommand,
469
1310
  registerDeactivateCommand,
1311
+ registerKeyCommand,
1312
+ registerProfileCommand,
470
1313
  registerResolveCommand,
471
- registerUpdateCommand
1314
+ registerUpdateCommand,
1315
+ setConfigPath,
1316
+ unsetConfigPath,
1317
+ writeConfigFile
472
1318
  });