@did-btcr2/cli 0.14.0 → 0.16.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 (83) hide show
  1. package/README.md +106 -17
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/cjs/index.js +1160 -131
  4. package/dist/esm/src/cli.js +29 -5
  5. package/dist/esm/src/cli.js.map +1 -1
  6. package/dist/esm/src/commands/config.js +130 -18
  7. package/dist/esm/src/commands/config.js.map +1 -1
  8. package/dist/esm/src/commands/create.js +13 -1
  9. package/dist/esm/src/commands/create.js.map +1 -1
  10. package/dist/esm/src/commands/deactivate.js +16 -2
  11. package/dist/esm/src/commands/deactivate.js.map +1 -1
  12. package/dist/esm/src/commands/index.js +2 -0
  13. package/dist/esm/src/commands/index.js.map +1 -1
  14. package/dist/esm/src/commands/init.js +63 -0
  15. package/dist/esm/src/commands/init.js.map +1 -0
  16. package/dist/esm/src/commands/keystore.js +81 -0
  17. package/dist/esm/src/commands/keystore.js.map +1 -0
  18. package/dist/esm/src/commands/profile.js +6 -4
  19. package/dist/esm/src/commands/profile.js.map +1 -1
  20. package/dist/esm/src/commands/update.js +16 -2
  21. package/dist/esm/src/commands/update.js.map +1 -1
  22. package/dist/esm/src/config-schema.js +149 -0
  23. package/dist/esm/src/config-schema.js.map +1 -0
  24. package/dist/esm/src/config.js +579 -55
  25. package/dist/esm/src/config.js.map +1 -1
  26. package/dist/esm/src/keystore/file-key-store.js +340 -32
  27. package/dist/esm/src/keystore/file-key-store.js.map +1 -1
  28. package/dist/esm/src/keystore/passphrase.js +40 -10
  29. package/dist/esm/src/keystore/passphrase.js.map +1 -1
  30. package/dist/esm/src/keystore/paths.js +6 -17
  31. package/dist/esm/src/keystore/paths.js.map +1 -1
  32. package/dist/esm/src/output.js +49 -0
  33. package/dist/esm/src/output.js.map +1 -1
  34. package/dist/esm/src/paths.js +59 -0
  35. package/dist/esm/src/paths.js.map +1 -0
  36. package/dist/esm/src/types.js +11 -0
  37. package/dist/esm/src/types.js.map +1 -1
  38. package/dist/types/src/cli.d.ts.map +1 -1
  39. package/dist/types/src/commands/config.d.ts.map +1 -1
  40. package/dist/types/src/commands/create.d.ts.map +1 -1
  41. package/dist/types/src/commands/deactivate.d.ts.map +1 -1
  42. package/dist/types/src/commands/index.d.ts +2 -0
  43. package/dist/types/src/commands/index.d.ts.map +1 -1
  44. package/dist/types/src/commands/init.d.ts +13 -0
  45. package/dist/types/src/commands/init.d.ts.map +1 -0
  46. package/dist/types/src/commands/keystore.d.ts +10 -0
  47. package/dist/types/src/commands/keystore.d.ts.map +1 -0
  48. package/dist/types/src/commands/profile.d.ts.map +1 -1
  49. package/dist/types/src/commands/update.d.ts.map +1 -1
  50. package/dist/types/src/config-schema.d.ts +24 -0
  51. package/dist/types/src/config-schema.d.ts.map +1 -0
  52. package/dist/types/src/config.d.ts +252 -14
  53. package/dist/types/src/config.d.ts.map +1 -1
  54. package/dist/types/src/keystore/file-key-store.d.ts +86 -10
  55. package/dist/types/src/keystore/file-key-store.d.ts.map +1 -1
  56. package/dist/types/src/keystore/passphrase.d.ts +16 -1
  57. package/dist/types/src/keystore/passphrase.d.ts.map +1 -1
  58. package/dist/types/src/keystore/paths.d.ts +6 -10
  59. package/dist/types/src/keystore/paths.d.ts.map +1 -1
  60. package/dist/types/src/output.d.ts +22 -0
  61. package/dist/types/src/output.d.ts.map +1 -1
  62. package/dist/types/src/paths.d.ts +54 -0
  63. package/dist/types/src/paths.d.ts.map +1 -0
  64. package/dist/types/src/types.d.ts +70 -0
  65. package/dist/types/src/types.d.ts.map +1 -1
  66. package/package.json +5 -5
  67. package/src/cli.ts +32 -4
  68. package/src/commands/config.ts +143 -18
  69. package/src/commands/create.ts +16 -1
  70. package/src/commands/deactivate.ts +24 -2
  71. package/src/commands/index.ts +2 -0
  72. package/src/commands/init.ts +74 -0
  73. package/src/commands/keystore.ts +98 -0
  74. package/src/commands/profile.ts +6 -4
  75. package/src/commands/update.ts +24 -2
  76. package/src/config-schema.ts +178 -0
  77. package/src/config.ts +752 -58
  78. package/src/keystore/file-key-store.ts +455 -43
  79. package/src/keystore/passphrase.ts +48 -8
  80. package/src/keystore/paths.ts +6 -18
  81. package/src/output.ts +53 -0
  82. package/src/paths.ts +79 -0
  83. package/src/types.ts +34 -0
package/dist/cjs/index.js CHANGED
@@ -23,30 +23,53 @@ __export(index_exports, {
23
23
  CLIError: () => CLIError,
24
24
  CONFIG_SCHEMA_VERSION: () => CONFIG_SCHEMA_VERSION,
25
25
  DidBtcr2Cli: () => DidBtcr2Cli,
26
+ ENV_RPC_PASS_FILE: () => ENV_RPC_PASS_FILE,
26
27
  ENV_VARS: () => ENV_VARS,
28
+ REDACTED: () => REDACTED,
27
29
  SUPPORTED_NETWORKS: () => SUPPORTED_NETWORKS,
28
30
  VERSION: () => VERSION,
31
+ assertKeystoreAllowedForNetwork: () => assertKeystoreAllowedForNetwork,
32
+ blankToUndef: () => blankToUndef,
29
33
  defaultApiFactory: () => defaultApiFactory,
30
34
  defaultConfigPath: () => defaultConfigPath,
31
35
  deriveNetwork: () => deriveNetwork,
32
36
  formatResult: () => formatResult,
33
37
  getConfigPath: () => getConfigPath,
38
+ isSecretKey: () => isSecretKey,
34
39
  keystoreApiFactory: () => keystoreApiFactory,
40
+ parseConfigFileRaw: () => parseConfigFileRaw,
41
+ parseHeaderList: () => parseHeaderList,
42
+ profileNetworkMismatch: () => profileNetworkMismatch,
35
43
  profileToOverrides: () => profileToOverrides,
36
44
  readConfigFile: () => readConfigFile,
37
45
  readEnvOverrides: () => readEnvOverrides,
46
+ redactSecrets: () => redactSecrets,
38
47
  registerCompletionCommand: () => registerCompletionCommand,
39
48
  registerConfigCommand: () => registerConfigCommand,
40
49
  registerCreateCommand: () => registerCreateCommand,
41
50
  registerDeactivateCommand: () => registerDeactivateCommand,
51
+ registerInitCommand: () => registerInitCommand,
42
52
  registerKeyCommand: () => registerKeyCommand,
53
+ registerKeystoreCommand: () => registerKeystoreCommand,
43
54
  registerProfileCommand: () => registerProfileCommand,
44
55
  registerResolveCommand: () => registerResolveCommand,
45
56
  registerUpdateCommand: () => registerUpdateCommand,
57
+ resolveActiveProfile: () => resolveActiveProfile,
58
+ resolveBroadcastOptions: () => resolveBroadcastOptions,
59
+ resolveConnectionConfig: () => resolveConnectionConfig,
46
60
  resolveDefaultNetwork: () => resolveDefaultNetwork,
61
+ resolveEffectiveConfig: () => resolveEffectiveConfig,
62
+ resolveKeystorePath: () => resolveKeystorePath,
63
+ resolveKeystoreProtection: () => resolveKeystoreProtection,
64
+ resolveOutputFormat: () => resolveOutputFormat,
65
+ resolveSecretRef: () => resolveSecretRef,
66
+ resolveSigningKeyRef: () => resolveSigningKeyRef,
67
+ runDoctor: () => runDoctor,
68
+ scrubUrlUserinfo: () => scrubUrlUserinfo,
47
69
  setConfigPath: () => setConfigPath,
48
70
  unsetConfigPath: () => unsetConfigPath,
49
- writeConfigFile: () => writeConfigFile
71
+ writeConfigFile: () => writeConfigFile,
72
+ writeDefaultConfigFile: () => writeDefaultConfigFile
50
73
  });
51
74
  module.exports = __toCommonJS(index_exports);
52
75
 
@@ -58,13 +81,13 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
58
81
  var import_common3 = require("@did-btcr2/common");
59
82
  var import_commander = require("commander");
60
83
 
61
- // src/commands/create.ts
62
- var import_utils2 = require("@noble/hashes/utils.js");
84
+ // src/commands/init.ts
85
+ var import_node_fs6 = require("fs");
63
86
 
64
87
  // src/config.ts
65
88
  var import_api = require("@did-btcr2/api");
89
+ var import_method = require("@did-btcr2/method");
66
90
  var import_node_fs5 = require("fs");
67
- var import_node_os2 = require("os");
68
91
  var import_node_path4 = require("path");
69
92
 
70
93
  // src/error.ts
@@ -144,6 +167,7 @@ var import_key_manager = require("@did-btcr2/key-manager");
144
167
  // src/keystore/file-key-store.ts
145
168
  var import_node_fs3 = require("fs");
146
169
  var import_node_path3 = require("path");
170
+ var import_utils2 = require("@noble/hashes/utils.js");
147
171
  var import_base2 = require("@scure/base");
148
172
 
149
173
  // src/keystore/envelope.ts
@@ -314,16 +338,57 @@ function withFileLock(lockPath, fn, options = {}) {
314
338
  }
315
339
  }
316
340
 
317
- // src/keystore/paths.ts
341
+ // src/paths.ts
318
342
  var import_node_os = require("os");
319
343
  var import_node_path2 = require("path");
320
- function defaultKeystorePath() {
321
- const base = process.env.XDG_DATA_HOME ?? process.env.LOCALAPPDATA ?? (0, import_node_path2.join)((0, import_node_os.homedir)(), ".local", "share");
322
- return (0, import_node_path2.join)(base, "btcr2", "keystore.json");
344
+
345
+ // src/types.ts
346
+ var SUPPORTED_NETWORKS = [
347
+ "bitcoin",
348
+ "testnet3",
349
+ "testnet4",
350
+ "signet",
351
+ "mutinynet",
352
+ "regtest"
353
+ ];
354
+ function blankToUndef(value) {
355
+ if (value === void 0) return void 0;
356
+ return value.trim() === "" ? void 0 : value;
357
+ }
358
+
359
+ // src/paths.ts
360
+ var ENV_HOME = "BTCR2_HOME";
361
+ var CONFIG_FILENAME = "config.json";
362
+ var KEYSTORE_FILENAME = "keystore.json";
363
+ function resolveHome(overrides) {
364
+ return blankToUndef(overrides?.home) ?? blankToUndef(process.env[ENV_HOME]) ?? platformDefaultHome();
365
+ }
366
+ function platformDefaultHome() {
367
+ if (process.platform === "win32") {
368
+ const base = blankToUndef(process.env.LOCALAPPDATA) ?? blankToUndef(process.env.APPDATA) ?? (0, import_node_os.homedir)();
369
+ return (0, import_node_path2.join)(base, "btcr2");
370
+ }
371
+ return (0, import_node_path2.join)((0, import_node_os.homedir)(), ".btcr2");
372
+ }
373
+ function defaultConfigPath(overrides) {
374
+ return (0, import_node_path2.join)(resolveHome(overrides), CONFIG_FILENAME);
375
+ }
376
+ function defaultKeystorePath(overrides) {
377
+ return (0, import_node_path2.join)(resolveHome(overrides), KEYSTORE_FILENAME);
323
378
  }
324
379
 
325
380
  // src/keystore/file-key-store.ts
326
381
  var KEYSTORE_VERSION = 1;
382
+ var VERIFIER_PLAINTEXT = (0, import_utils2.utf8ToBytes)("did-btcr2-keystore-verifier-v1");
383
+ function bytesEqual(a, b) {
384
+ if (a.length !== b.length) return false;
385
+ let diff = 0;
386
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
387
+ return diff === 0;
388
+ }
389
+ function sameEnvelope(a, b) {
390
+ return JSON.stringify(a) === JSON.stringify(b);
391
+ }
327
392
  var FileKeyStore = class {
328
393
  #path;
329
394
  #lockPath;
@@ -332,12 +397,15 @@ var FileKeyStore = class {
332
397
  #argonParams;
333
398
  #cache = /* @__PURE__ */ new Map();
334
399
  #active;
400
+ #protection;
401
+ #verifier;
335
402
  constructor(options) {
336
403
  this.#path = options.path ?? defaultKeystorePath();
337
404
  this.#lockPath = `${this.#path}.lock`;
338
405
  this.#lockOptions = options.lock ?? {};
339
406
  this.#getPassphrase = options.getPassphrase;
340
407
  this.#argonParams = options.argonParams ?? DEFAULT_ARGON_PARAMS;
408
+ this.#protection = options.protection ?? "passphrase";
341
409
  ensureDir((0, import_node_path3.dirname)(this.#path), 448);
342
410
  this.#loadFromDisk();
343
411
  }
@@ -361,7 +429,26 @@ var FileKeyStore = class {
361
429
  { version: parsed.v }
362
430
  );
363
431
  }
432
+ if (parsed.protection !== "none" && parsed.protection !== "passphrase") {
433
+ throw new KeyStoreError(
434
+ `Keystore at ${this.#path} has no recognized protection header; it was not written by this CLI.`,
435
+ "KEYSTORE_CORRUPT_ERROR",
436
+ { path: this.#path }
437
+ );
438
+ }
439
+ this.#protection = parsed.protection;
440
+ this.#verifier = parsed.verifier;
364
441
  this.#active = parsed.active;
442
+ const sealedPresent = Object.values(parsed.keys ?? {}).some(
443
+ (k) => k && typeof k === "object" && k.secret !== void 0
444
+ );
445
+ if (this.#protection === "passphrase" && sealedPresent && this.#verifier === void 0) {
446
+ throw new KeyStoreError(
447
+ `Keystore at ${this.#path} holds sealed keys but no passphrase verifier; it is corrupt or was tampered with.`,
448
+ "KEYSTORE_CORRUPT_ERROR",
449
+ { path: this.#path }
450
+ );
451
+ }
365
452
  for (const [id, stored] of Object.entries(parsed.keys ?? {})) {
366
453
  let publicKey;
367
454
  try {
@@ -381,24 +468,67 @@ var FileKeyStore = class {
381
468
  { path: this.#path, keyId: id }
382
469
  );
383
470
  }
384
- this.#cache.set(id, {
385
- publicKey,
386
- ...stored.tags && { tags: stored.tags },
387
- ...stored.secret && { secret: stored.secret }
388
- });
471
+ const entry = { publicKey, ...stored.tags && { tags: stored.tags } };
472
+ if (stored.plainSecret !== void 0) {
473
+ if (this.#protection !== "none") {
474
+ throw new KeyStoreError(
475
+ `Keystore entry ${id} holds a plaintext secret in an encrypted keystore at ${this.#path}.`,
476
+ "KEYSTORE_CORRUPT_ERROR",
477
+ { path: this.#path, keyId: id }
478
+ );
479
+ }
480
+ entry.plaintext = true;
481
+ entry.decrypted = this.#decodePlainSecret(stored.plainSecret, id);
482
+ } else if (stored.secret) {
483
+ if (this.#protection !== "passphrase") {
484
+ throw new KeyStoreError(
485
+ `Keystore entry ${id} holds a sealed secret in a dev keystore at ${this.#path}.`,
486
+ "KEYSTORE_CORRUPT_ERROR",
487
+ { path: this.#path, keyId: id }
488
+ );
489
+ }
490
+ entry.secret = stored.secret;
491
+ }
492
+ this.#cache.set(id, entry);
493
+ }
494
+ }
495
+ /** Decodes and length-checks a dev-keystore plaintext secret. */
496
+ #decodePlainSecret(encoded, id) {
497
+ let secret;
498
+ try {
499
+ secret = import_base2.base64urlnopad.decode(encoded);
500
+ } catch {
501
+ throw new KeyStoreError(
502
+ `Keystore entry ${id} has a malformed plaintext secret.`,
503
+ "KEYSTORE_CORRUPT_ERROR",
504
+ { path: this.#path, keyId: id }
505
+ );
506
+ }
507
+ if (secret.length !== 32) {
508
+ throw new KeyStoreError(
509
+ `Keystore entry ${id} has a ${secret.length}-byte secret; expected 32.`,
510
+ "KEYSTORE_CORRUPT_ERROR",
511
+ { path: this.#path, keyId: id }
512
+ );
389
513
  }
514
+ return secret;
390
515
  }
391
516
  #flush() {
392
517
  const keys = {};
393
518
  for (const [id, entry] of this.#cache) {
394
- keys[id] = {
395
- publicKey: import_base2.base64urlnopad.encode(entry.publicKey),
396
- ...entry.tags && { tags: entry.tags },
397
- ...entry.secret && { secret: entry.secret }
398
- };
519
+ const stored = { publicKey: import_base2.base64urlnopad.encode(entry.publicKey) };
520
+ if (entry.tags) stored.tags = entry.tags;
521
+ if (entry.plaintext && entry.decrypted) {
522
+ stored.plainSecret = import_base2.base64urlnopad.encode(entry.decrypted);
523
+ } else if (entry.secret) {
524
+ stored.secret = entry.secret;
525
+ }
526
+ keys[id] = stored;
399
527
  }
400
528
  const file = {
401
529
  v: KEYSTORE_VERSION,
530
+ protection: this.#protection,
531
+ ...this.#verifier && { verifier: this.#verifier },
402
532
  ...this.#active && { active: this.#active },
403
533
  keys
404
534
  };
@@ -410,7 +540,8 @@ var FileKeyStore = class {
410
540
  * mutation applies on top of whatever other processes have written. Secrets
411
541
  * already decrypted this session are carried over for entries whose sealed
412
542
  * envelope is byte-identical on disk, so a mid-session write does not force a
413
- * re-prompt for keys it did not touch.
543
+ * re-prompt for keys it did not touch. (Dev-keystore plaintext secrets are
544
+ * reloaded from disk, so they need no carry.)
414
545
  */
415
546
  #reload() {
416
547
  const carried = /* @__PURE__ */ new Map();
@@ -441,6 +572,53 @@ var FileKeyStore = class {
441
572
  this.#flush();
442
573
  }, this.#lockOptions);
443
574
  }
575
+ /**
576
+ * Verifies a candidate passphrase against the keystore verifier, throwing when
577
+ * it does not open the sentinel. A no-op only on a fresh keystore whose
578
+ * passphrase has not been established yet (no verifier to check against).
579
+ */
580
+ #assertPassphrase(passphrase) {
581
+ if (!this.#verifier) return;
582
+ let plain;
583
+ try {
584
+ plain = decryptSecret(this.#verifier, passphrase);
585
+ } catch {
586
+ throw new KeyStoreError(
587
+ `Incorrect passphrase for the keystore at ${this.#path}.`,
588
+ "DECRYPT_ERROR",
589
+ { path: this.#path }
590
+ );
591
+ }
592
+ if (!bytesEqual(plain, VERIFIER_PLAINTEXT)) {
593
+ throw new KeyStoreError(
594
+ `Keystore verifier at ${this.#path} did not match; the file may be corrupt.`,
595
+ "KEYSTORE_CORRUPT_ERROR",
596
+ { path: this.#path }
597
+ );
598
+ }
599
+ }
600
+ /** Passphrase for opening an existing secret: verified when a verifier exists. */
601
+ #openPassphrase() {
602
+ const passphrase = this.#getPassphrase();
603
+ this.#assertPassphrase(passphrase);
604
+ return passphrase;
605
+ }
606
+ /**
607
+ * Passphrase for sealing a secret. When the keystore's passphrase is already
608
+ * established (a verifier exists), prompt once and verify it. Otherwise this is
609
+ * establishment on a fresh keystore: prompt with confirm and mint a verifier for
610
+ * the caller to persist alongside the first sealed key.
611
+ */
612
+ #sealPassphrase() {
613
+ if (this.#verifier) {
614
+ const passphrase2 = this.#getPassphrase();
615
+ this.#assertPassphrase(passphrase2);
616
+ return { passphrase: passphrase2 };
617
+ }
618
+ const passphrase = this.#getPassphrase({ confirm: true });
619
+ const newVerifier = encryptSecret(VERIFIER_PLAINTEXT, passphrase, this.#argonParams);
620
+ return { passphrase, newVerifier };
621
+ }
444
622
  get(id) {
445
623
  const entry = this.#cache.get(id);
446
624
  if (!entry) return void 0;
@@ -448,13 +626,20 @@ var FileKeyStore = class {
448
626
  publicKey: entry.publicKey,
449
627
  ...entry.tags && { tags: entry.tags }
450
628
  };
451
- if (entry.secret) {
629
+ if (entry.plaintext && entry.decrypted) {
630
+ const secret = entry.decrypted;
631
+ Object.defineProperty(result, "secretKey", {
632
+ configurable: true,
633
+ enumerable: false,
634
+ get: () => secret
635
+ });
636
+ } else if (entry.secret) {
452
637
  const sealed = entry.secret;
453
638
  Object.defineProperty(result, "secretKey", {
454
639
  configurable: true,
455
640
  enumerable: false,
456
641
  get: () => {
457
- entry.decrypted ??= decryptSecret(sealed, this.#getPassphrase());
642
+ entry.decrypted ??= decryptSecret(sealed, this.#openPassphrase());
458
643
  return entry.decrypted;
459
644
  }
460
645
  });
@@ -465,12 +650,45 @@ var FileKeyStore = class {
465
650
  return this.#cache.has(id);
466
651
  }
467
652
  set(id, value) {
468
- const secret = value.secretKey ? encryptSecret(value.secretKey, this.#getPassphrase(), this.#argonParams) : void 0;
653
+ if (this.#protection === "none") {
654
+ this.#mutate(() => {
655
+ this.#cache.set(id, {
656
+ publicKey: value.publicKey,
657
+ ...value.tags && { tags: value.tags },
658
+ ...value.secretKey && { plaintext: true, decrypted: value.secretKey }
659
+ });
660
+ });
661
+ return;
662
+ }
663
+ const verifierAtSeal = this.#verifier;
664
+ let sealed;
665
+ let newVerifier;
666
+ let passphrase;
667
+ if (value.secretKey) {
668
+ const resolved = this.#sealPassphrase();
669
+ passphrase = resolved.passphrase;
670
+ newVerifier = resolved.newVerifier;
671
+ sealed = encryptSecret(value.secretKey, passphrase, this.#argonParams);
672
+ }
469
673
  this.#mutate(() => {
674
+ if (value.secretKey) {
675
+ if (this.#verifier === void 0 && newVerifier !== void 0) {
676
+ this.#verifier = newVerifier;
677
+ } else if (this.#verifier !== void 0 && passphrase !== void 0) {
678
+ if (verifierAtSeal !== void 0 && !sameEnvelope(this.#verifier, verifierAtSeal)) {
679
+ throw new KeyStoreError(
680
+ `The keystore passphrase at ${this.#path} changed concurrently; re-run the command.`,
681
+ "KEYSTORE_CONCURRENT_CHANGE_ERROR",
682
+ { path: this.#path }
683
+ );
684
+ }
685
+ this.#assertPassphrase(passphrase);
686
+ }
687
+ }
470
688
  this.#cache.set(id, {
471
689
  publicKey: value.publicKey,
472
690
  ...value.tags && { tags: value.tags },
473
- ...secret && { secret },
691
+ ...sealed && { secret: sealed },
474
692
  ...value.secretKey && { decrypted: value.secretKey }
475
693
  });
476
694
  });
@@ -533,7 +751,112 @@ var FileKeyStore = class {
533
751
  this.#active = id;
534
752
  });
535
753
  }
754
+ /** The keystore's protection mode. */
755
+ get protection() {
756
+ return this.#protection;
757
+ }
758
+ /**
759
+ * Re-seals every sealed secret and the verifier under a new passphrase (ADR
760
+ * 080), returning the number of secrets re-sealed. Verifies the current
761
+ * passphrase against the verifier first. Refused on a dev keystore, which has
762
+ * no passphrase.
763
+ */
764
+ changePassphrase(oldPassphrase, newPassphrase) {
765
+ if (this.#protection === "none") {
766
+ throw new KeyStoreError(
767
+ `The keystore at ${this.#path} is an unencrypted dev keystore; it has no passphrase to change.`,
768
+ "KEYSTORE_PROTECTION_ERROR",
769
+ { path: this.#path }
770
+ );
771
+ }
772
+ if (this.#verifier) this.#assertPassphrase(oldPassphrase);
773
+ const resealed = /* @__PURE__ */ new Map();
774
+ for (const [id, entry] of this.#cache) {
775
+ if (!entry.secret) continue;
776
+ let plain;
777
+ try {
778
+ plain = decryptSecret(entry.secret, oldPassphrase);
779
+ } catch {
780
+ throw new KeyStoreError(
781
+ `Incorrect current passphrase for the keystore at ${this.#path}.`,
782
+ "DECRYPT_ERROR",
783
+ { path: this.#path }
784
+ );
785
+ }
786
+ resealed.set(id, { from: entry.secret, to: encryptSecret(plain, newPassphrase, this.#argonParams), plain });
787
+ }
788
+ const newVerifier = encryptSecret(VERIFIER_PLAINTEXT, newPassphrase, this.#argonParams);
789
+ let rekeyed = 0;
790
+ this.#mutate(() => {
791
+ for (const [id, entry] of this.#cache) {
792
+ if (!entry.secret) continue;
793
+ const pre = resealed.get(id);
794
+ if (!pre || !sameEnvelope(pre.from, entry.secret)) {
795
+ throw new KeyStoreError(
796
+ `The keystore at ${this.#path} changed while its passphrase was being changed; re-run the command.`,
797
+ "KEYSTORE_CONCURRENT_CHANGE_ERROR",
798
+ { path: this.#path }
799
+ );
800
+ }
801
+ entry.secret = pre.to;
802
+ entry.decrypted = pre.plain;
803
+ rekeyed++;
804
+ }
805
+ this.#verifier = newVerifier;
806
+ });
807
+ return rekeyed;
808
+ }
536
809
  };
810
+ function keystoreSummary(path) {
811
+ const absent = { protection: "absent", established: false, keyCount: 0, active: void 0 };
812
+ if (!(0, import_node_fs3.existsSync)(path)) return absent;
813
+ let parsed;
814
+ try {
815
+ parsed = JSON.parse((0, import_node_fs3.readFileSync)(path, "utf-8"));
816
+ } catch {
817
+ return absent;
818
+ }
819
+ const keys = parsed.keys && typeof parsed.keys === "object" ? parsed.keys : {};
820
+ const keyCount = Object.keys(keys).length;
821
+ const active = typeof parsed.active === "string" ? parsed.active : void 0;
822
+ if (parsed.protection === "none") {
823
+ return { protection: "dev", established: true, keyCount, active };
824
+ }
825
+ if (parsed.protection === "passphrase") {
826
+ return { protection: "encrypted", established: parsed.verifier !== void 0, keyCount, active };
827
+ }
828
+ return absent;
829
+ }
830
+ function keystoreProtection(path) {
831
+ return keystoreSummary(path).protection;
832
+ }
833
+ function initKeystore(path, options) {
834
+ ensureDir((0, import_node_path3.dirname)(path), 448);
835
+ let file;
836
+ if (options.protection === "none") {
837
+ file = { v: KEYSTORE_VERSION, protection: "none", keys: {} };
838
+ } else {
839
+ const passphrase = options.getPassphrase({ confirm: true });
840
+ const verifier = encryptSecret(VERIFIER_PLAINTEXT, passphrase, options.argonParams ?? DEFAULT_ARGON_PARAMS);
841
+ file = { v: KEYSTORE_VERSION, protection: "passphrase", verifier, keys: {} };
842
+ }
843
+ writeFileAtomic(path, `${JSON.stringify(file, null, 2)}
844
+ `, 384);
845
+ }
846
+ function changeKeystorePassphrase(path, oldPassphrase, newPassphrase, argonParams) {
847
+ const store = new FileKeyStore({
848
+ path,
849
+ ...argonParams && { argonParams },
850
+ getPassphrase: () => {
851
+ throw new KeyStoreError(
852
+ `Unexpected passphrase prompt while changing the passphrase for ${path}.`,
853
+ "KEYSTORE_INTERNAL_ERROR",
854
+ { path }
855
+ );
856
+ }
857
+ });
858
+ return store.changePassphrase(oldPassphrase, newPassphrase);
859
+ }
537
860
 
538
861
  // src/keystore/file-backed-key-manager.ts
539
862
  var FileBackedKeyManager = class {
@@ -594,10 +917,12 @@ var FileBackedKeyManager = class {
594
917
  var import_node_fs4 = require("fs");
595
918
  var ENV_KEYSTORE_PASSPHRASE = "BTCR2_KEYSTORE_PASSPHRASE";
596
919
  function acquirePassphrase(options = {}) {
597
- const fromEnv = process.env[ENV_KEYSTORE_PASSPHRASE];
598
- if (fromEnv) return assertNonEmpty(fromEnv.replace(/\r?\n$/, ""));
599
- if (options.passphraseFile) {
600
- return assertNonEmpty((0, import_node_fs4.readFileSync)(options.passphraseFile, "utf-8").replace(/\r?\n$/, ""));
920
+ if (!options.forcePrompt) {
921
+ const fromEnv = process.env[ENV_KEYSTORE_PASSPHRASE];
922
+ if (fromEnv) return assertNonEmpty(fromEnv.replace(/\r?\n$/, ""));
923
+ if (options.passphraseFile) {
924
+ return assertNonEmpty((0, import_node_fs4.readFileSync)(options.passphraseFile, "utf-8").replace(/\r?\n$/, ""));
925
+ }
601
926
  }
602
927
  if (!process.stdin.isTTY) {
603
928
  throw new KeyStoreError(
@@ -620,6 +945,12 @@ function assertNonEmpty(passphrase) {
620
945
  }
621
946
  return passphrase;
622
947
  }
948
+ var IDLE_WAIT = new Int32Array(new SharedArrayBuffer(4));
949
+ var IDLE_POLL_MS = 20;
950
+ function dropLastUtf8Char(bytes) {
951
+ while (bytes.length > 0 && (bytes[bytes.length - 1] & 192) === 128) bytes.pop();
952
+ bytes.pop();
953
+ }
623
954
  function promptHidden(label) {
624
955
  process.stderr.write(label);
625
956
  const stdin = process.stdin;
@@ -634,7 +965,10 @@ function promptHidden(label) {
634
965
  read = (0, import_node_fs4.readSync)(stdin.fd, byte, 0, 1, null);
635
966
  } catch (error) {
636
967
  const code = error.code;
637
- if (code === "EAGAIN") continue;
968
+ if (code === "EAGAIN") {
969
+ Atomics.wait(IDLE_WAIT, 0, 0, IDLE_POLL_MS);
970
+ continue;
971
+ }
638
972
  if (code === "EOF") break;
639
973
  throw error;
640
974
  }
@@ -645,7 +979,7 @@ function promptHidden(label) {
645
979
  throw new KeyStoreError("Passphrase entry aborted.", "PASSPHRASE_REQUIRED_ERROR");
646
980
  }
647
981
  if (ch === 127 || ch === 8) {
648
- bytes.pop();
982
+ dropLastUtf8Char(bytes);
649
983
  continue;
650
984
  }
651
985
  bytes.push(ch);
@@ -657,16 +991,6 @@ function promptHidden(label) {
657
991
  return Buffer.from(bytes).toString("utf-8");
658
992
  }
659
993
 
660
- // src/types.ts
661
- var SUPPORTED_NETWORKS = [
662
- "bitcoin",
663
- "testnet3",
664
- "testnet4",
665
- "signet",
666
- "mutinynet",
667
- "regtest"
668
- ];
669
-
670
994
  // src/config.ts
671
995
  var CONFIG_SCHEMA_VERSION = 1;
672
996
  function writeConfigFile(path, mutate) {
@@ -677,14 +1001,31 @@ function writeConfigFile(path, mutate) {
677
1001
  writeFileAtomic(path, `${JSON.stringify(raw, null, 2)}
678
1002
  `, 384);
679
1003
  }
1004
+ function writeDefaultConfigFile(path) {
1005
+ const scaffold = {
1006
+ schemaVersion: CONFIG_SCHEMA_VERSION,
1007
+ defaults: { output: "text" },
1008
+ profiles: Object.fromEntries(SUPPORTED_NETWORKS.map((n) => [n, {}]))
1009
+ };
1010
+ ensureDir((0, import_node_path4.dirname)(path), 448);
1011
+ writeFileAtomic(path, `${JSON.stringify(scaffold, null, 2)}
1012
+ `, 384);
1013
+ }
680
1014
  function getConfigPath(config, path) {
681
1015
  return path.split(".").reduce(
682
1016
  (node, key) => node?.[key],
683
1017
  config
684
1018
  );
685
1019
  }
1020
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1021
+ function assertSafeKey(key, path) {
1022
+ if (UNSAFE_KEYS.has(key)) {
1023
+ throw new CLIError(`Illegal config path segment "${key}" in "${path}".`, "INVALID_ARGUMENT_ERROR", { path, key });
1024
+ }
1025
+ }
686
1026
  function setConfigPath(config, path, value) {
687
1027
  const keys = path.split(".");
1028
+ keys.forEach((key) => assertSafeKey(key, path));
688
1029
  const last = keys.pop();
689
1030
  if (!last) throw new CLIError("Config path must be non-empty.", "INVALID_ARGUMENT_ERROR");
690
1031
  let node = config;
@@ -696,6 +1037,7 @@ function setConfigPath(config, path, value) {
696
1037
  }
697
1038
  function unsetConfigPath(config, path) {
698
1039
  const keys = path.split(".");
1040
+ keys.forEach((key) => assertSafeKey(key, path));
699
1041
  const last = keys.pop();
700
1042
  if (!last) return;
701
1043
  let node = config;
@@ -711,7 +1053,10 @@ var ENV_VARS = {
711
1053
  BTC_RPC_USER: "BTCR2_BTC_RPC_USER",
712
1054
  BTC_RPC_PASS: "BTCR2_BTC_RPC_PASS",
713
1055
  CAS_GATEWAY: "BTCR2_CAS_GATEWAY",
714
- CAS_RPC_URL: "BTCR2_CAS_RPC_URL"
1056
+ CAS_RPC_URL: "BTCR2_CAS_RPC_URL",
1057
+ BTC_TIMEOUT: "BTCR2_BTC_TIMEOUT",
1058
+ CAS_TIMEOUT: "BTCR2_CAS_TIMEOUT",
1059
+ FEE_RATE: "BTCR2_FEE_RATE"
715
1060
  };
716
1061
  function readEnvOverrides() {
717
1062
  const env = (key) => process.env[key] || void 0;
@@ -724,16 +1069,42 @@ function readEnvOverrides() {
724
1069
  casRpcUrl: env(ENV_VARS.CAS_RPC_URL)
725
1070
  };
726
1071
  }
727
- function defaultConfigPath() {
728
- const base = process.env.XDG_CONFIG_HOME ?? process.env.APPDATA ?? (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".config");
729
- return (0, import_node_path4.join)(base, "btcr2", "config.json");
730
- }
731
- function readConfigFile(path) {
1072
+ function parseConfigFileRaw(path) {
1073
+ let content;
1074
+ try {
1075
+ content = (0, import_node_fs5.readFileSync)(path, "utf-8");
1076
+ } catch (error) {
1077
+ if (error.code === "ENOENT") return void 0;
1078
+ throw new CLIError(
1079
+ `Failed to read config file at ${path}: ${error.message}`,
1080
+ "CONFIG_READ_ERROR",
1081
+ { path }
1082
+ );
1083
+ }
732
1084
  try {
733
- const content = (0, import_node_fs5.readFileSync)(path, "utf-8");
734
1085
  return JSON.parse(content);
735
- } catch {
736
- return void 0;
1086
+ } catch (error) {
1087
+ throw new CLIError(
1088
+ `Config file at ${path} is not valid JSON: ${error.message}. Fix the file by hand; the CLI will not overwrite it while it is unparseable.`,
1089
+ "CONFIG_PARSE_ERROR",
1090
+ { path }
1091
+ );
1092
+ }
1093
+ }
1094
+ function readConfigFile(path) {
1095
+ const parsed = parseConfigFileRaw(path);
1096
+ if (parsed === void 0) return void 0;
1097
+ migrateConfigShape(parsed, path);
1098
+ return parsed;
1099
+ }
1100
+ function migrateConfigShape(raw, path) {
1101
+ const version = typeof raw.schemaVersion === "number" ? raw.schemaVersion : 0;
1102
+ if (version > CONFIG_SCHEMA_VERSION) {
1103
+ throw new CLIError(
1104
+ `Config file at ${path} has schemaVersion ${version}, but this CLI supports up to ${CONFIG_SCHEMA_VERSION}. Upgrade the btcr2 CLI to read it.`,
1105
+ "CONFIG_SCHEMA_VERSION_ERROR",
1106
+ { path, fileVersion: version, supported: CONFIG_SCHEMA_VERSION }
1107
+ );
737
1108
  }
738
1109
  }
739
1110
  function profileToOverrides(config, profileName) {
@@ -748,58 +1119,326 @@ function profileToOverrides(config, profileName) {
748
1119
  casRpcUrl: profile.cas?.rpcUrl
749
1120
  };
750
1121
  }
1122
+ function resolveActiveProfile(file, overrides) {
1123
+ const name = blankToUndef(overrides?.profile) ?? file?.defaults?.profile;
1124
+ if (!name) return { name: void 0, network: void 0 };
1125
+ const declared = file?.profiles?.[name]?.network;
1126
+ const network = declared && SUPPORTED_NETWORKS.includes(declared) ? declared : SUPPORTED_NETWORKS.includes(name) ? name : void 0;
1127
+ return { name, network };
1128
+ }
751
1129
  function resolveDefaultNetwork(overrides) {
752
- const configPath = overrides?.config ?? defaultConfigPath();
1130
+ const configPath = overrides?.config ?? defaultConfigPath(overrides);
753
1131
  const file = readConfigFile(configPath);
754
1132
  const explicit = file?.defaults?.network;
755
1133
  if (explicit && SUPPORTED_NETWORKS.includes(explicit)) return explicit;
756
- const profile = overrides?.profile ?? file?.defaults?.profile;
757
- if (profile && SUPPORTED_NETWORKS.includes(profile)) {
758
- return profile;
759
- }
1134
+ const { network } = resolveActiveProfile(file, overrides);
1135
+ if (network) return network;
760
1136
  return "regtest";
761
1137
  }
1138
+ function profileNetworkMismatch(network, overrides) {
1139
+ let file;
1140
+ try {
1141
+ file = readConfigFile(overrides?.config ?? defaultConfigPath(overrides));
1142
+ } catch {
1143
+ return void 0;
1144
+ }
1145
+ const { name, network: declared } = resolveActiveProfile(file, overrides);
1146
+ if (name && declared && declared !== network) return { profile: name, declared };
1147
+ return void 0;
1148
+ }
1149
+ function resolveOutputFormat(options) {
1150
+ const candidates = [
1151
+ blankToUndef(options.output),
1152
+ process.env.BTCR2_OUTPUT || void 0
1153
+ ];
1154
+ try {
1155
+ candidates.push(readConfigFile(options.config ?? defaultConfigPath(options))?.defaults?.output);
1156
+ } catch {
1157
+ }
1158
+ for (const candidate of candidates) {
1159
+ if (candidate === "json" || candidate === "text") return candidate;
1160
+ }
1161
+ return "text";
1162
+ }
1163
+ function resolveRpcUnit(overrides, env, fileOverrides) {
1164
+ const layers = [
1165
+ { src: "flag", url: overrides?.btcRpcUrl, user: overrides?.btcRpcUser, pass: overrides?.btcRpcPass },
1166
+ { src: "env", url: env?.btcRpcUrl, user: env?.btcRpcUser, pass: env?.btcRpcPass },
1167
+ { src: "file", url: fileOverrides?.btcRpcUrl, user: fileOverrides?.btcRpcUser, pass: fileOverrides?.btcRpcPass }
1168
+ ];
1169
+ const withUrl = layers.find((l) => blankToUndef(l.url) !== void 0);
1170
+ if (withUrl) {
1171
+ return { src: withUrl.src, url: blankToUndef(withUrl.url), user: blankToUndef(withUrl.user), pass: blankToUndef(withUrl.pass) };
1172
+ }
1173
+ const withCreds = layers.find((l) => blankToUndef(l.user) !== void 0 || blankToUndef(l.pass) !== void 0);
1174
+ if (withCreds) {
1175
+ return { src: withCreds.src, url: void 0, user: blankToUndef(withCreds.user), pass: blankToUndef(withCreds.pass) };
1176
+ }
1177
+ return void 0;
1178
+ }
762
1179
  function resolveConnectionConfig(network, overrides) {
763
1180
  if (!network) return {};
764
- const configPath = overrides?.config ?? defaultConfigPath();
1181
+ const configPath = overrides?.config ?? defaultConfigPath(overrides);
765
1182
  const file = readConfigFile(configPath);
766
- const profileName = overrides?.profile ?? file?.defaults?.profile ?? network;
1183
+ const { name: activeProfile } = resolveActiveProfile(file, overrides);
1184
+ const profileName = activeProfile ?? network;
767
1185
  const fileOverrides = file ? profileToOverrides(file, profileName) : {};
768
1186
  const env = readEnvOverrides();
769
- const merged = {
770
- btcRest: overrides?.btcRest ?? env.btcRest ?? fileOverrides.btcRest,
771
- btcRpcUrl: overrides?.btcRpcUrl ?? env.btcRpcUrl ?? fileOverrides.btcRpcUrl,
772
- btcRpcUser: overrides?.btcRpcUser ?? env.btcRpcUser ?? fileOverrides.btcRpcUser,
773
- btcRpcPass: overrides?.btcRpcPass ?? env.btcRpcPass ?? fileOverrides.btcRpcPass,
774
- casGateway: overrides?.casGateway ?? env.casGateway ?? fileOverrides.casGateway,
775
- casRpcUrl: overrides?.casRpcUrl ?? env.casRpcUrl ?? fileOverrides.casRpcUrl
776
- };
1187
+ const pick = (flag, envVal, fileVal) => blankToUndef(flag) ?? blankToUndef(envVal) ?? blankToUndef(fileVal);
1188
+ const profileBtc = file?.profiles?.[profileName]?.btc;
1189
+ const profileCas = file?.profiles?.[profileName]?.cas;
777
1190
  const btc = { network };
778
- if (merged.btcRest) {
779
- btc.rest = { host: merged.btcRest };
780
- }
781
- if (merged.btcRpcUrl) {
1191
+ const btcRest = pick(overrides?.btcRest, env.btcRest, fileOverrides.btcRest);
1192
+ const restHeaders = mergeHeaders(profileBtc?.headers, parseHeaderList(overrides?.btcRestHeader, "--btc-rest-header"));
1193
+ if (btcRest || restHeaders) {
1194
+ btc.rest = { ...btcRest ? { host: btcRest } : {}, ...restHeaders ? { headers: restHeaders } : {} };
1195
+ }
1196
+ const rpcUnit = resolveRpcUnit(overrides, env, fileOverrides);
1197
+ const rpcWallet = pick(overrides?.btcRpcWallet, void 0, profileBtc?.wallet);
1198
+ const rpcHeaders = mergeHeaders(profileBtc?.rpcHeaders, parseHeaderList(overrides?.btcRpcHeader, "--btc-rpc-header"));
1199
+ const networkHasDefaultRpc = import_api.DEFAULT_BITCOIN_NETWORK_CONFIG[network].rpc !== void 0;
1200
+ const wantsRpc = rpcUnit !== void 0 || rpcWallet !== void 0 || rpcHeaders !== void 0;
1201
+ const hasRpcHost = rpcUnit?.url !== void 0 || networkHasDefaultRpc;
1202
+ if (wantsRpc && hasRpcHost) {
1203
+ const password = resolveSecretRef(rpcUnit?.pass) ?? readRpcPassFile();
782
1204
  btc.rpc = {
783
- host: merged.btcRpcUrl,
784
- username: merged.btcRpcUser,
785
- password: merged.btcRpcPass
1205
+ ...rpcUnit?.url !== void 0 ? { host: rpcUnit.url } : {},
1206
+ ...rpcUnit?.user !== void 0 ? { username: rpcUnit.user } : {},
1207
+ ...password !== void 0 ? { password } : {},
1208
+ ...rpcWallet ? { wallet: rpcWallet } : {},
1209
+ ...rpcHeaders ? { headers: rpcHeaders } : {}
786
1210
  };
787
1211
  }
1212
+ const btcTimeout = resolveTimeout(overrides?.btcTimeout, process.env[ENV_VARS.BTC_TIMEOUT], profileBtc?.timeoutMs, "--btc-timeout", 1);
1213
+ if (btcTimeout !== void 0) btc.timeoutMs = btcTimeout;
1214
+ const casGateway = pick(overrides?.casGateway, env.casGateway, fileOverrides.casGateway);
1215
+ const casRpcUrl = pick(overrides?.casRpcUrl, env.casRpcUrl, fileOverrides.casRpcUrl);
1216
+ const casTimeout = resolveTimeout(overrides?.casTimeout, process.env[ENV_VARS.CAS_TIMEOUT], profileCas?.timeoutMs, "--cas-timeout");
788
1217
  const cas = {};
789
- if (merged.casGateway) cas.gateway = merged.casGateway;
790
- if (merged.casRpcUrl) cas.rpcUrl = merged.casRpcUrl;
791
- const hasCas = merged.casGateway || merged.casRpcUrl;
1218
+ if (casGateway) cas.gateway = casGateway;
1219
+ if (casRpcUrl) cas.rpcUrl = casRpcUrl;
1220
+ if (casTimeout !== void 0) {
1221
+ cas.timeoutMs = casTimeout;
1222
+ if (!casGateway && !casRpcUrl) cas.gateway = import_api.DEFAULT_CAS_GATEWAY;
1223
+ }
1224
+ const hasCas = casGateway || casRpcUrl || casTimeout !== void 0;
792
1225
  return { btc, ...hasCas && { cas } };
793
1226
  }
1227
+ function resolveTimeout(flag, envVal, fileVal, flagName = "timeout", min = 0) {
1228
+ const raw = blankToUndef(flag) ?? blankToUndef(envVal) ?? (typeof fileVal === "number" ? String(fileVal) : void 0);
1229
+ if (raw === void 0) return void 0;
1230
+ const ms = Number(raw);
1231
+ if (!Number.isFinite(ms) || ms < min) {
1232
+ throw new CLIError(
1233
+ `Invalid ${flagName} value "${raw}": expected a number of milliseconds >= ${min}.`,
1234
+ "INVALID_ARGUMENT_ERROR",
1235
+ { value: raw }
1236
+ );
1237
+ }
1238
+ return ms;
1239
+ }
1240
+ function parseHeaderList(list, flagName = "--header") {
1241
+ if (!list || list.length === 0) return void 0;
1242
+ const headers = {};
1243
+ for (const entry of list) {
1244
+ const idx = entry.indexOf(":");
1245
+ const key = idx === -1 ? "" : entry.slice(0, idx).trim();
1246
+ if (idx === -1 || key === "") {
1247
+ throw new CLIError(
1248
+ `Invalid ${flagName} "${entry}": expected "Key: Value".`,
1249
+ "INVALID_ARGUMENT_ERROR",
1250
+ { header: entry }
1251
+ );
1252
+ }
1253
+ headers[key] = entry.slice(idx + 1).trim();
1254
+ }
1255
+ return headers;
1256
+ }
1257
+ function mergeHeaders(base, override) {
1258
+ if (!base && !override) return void 0;
1259
+ return { ...base, ...override };
1260
+ }
1261
+ var ENV_RPC_PASS_FILE = "BTCR2_BTC_RPC_PASS_FILE";
1262
+ function trimTrailingNewline(value) {
1263
+ return value.replace(/\r?\n$/, "");
1264
+ }
1265
+ function readSecretFile(path, source) {
1266
+ try {
1267
+ return trimTrailingNewline((0, import_node_fs5.readFileSync)(path, "utf-8"));
1268
+ } catch (error) {
1269
+ throw new CLIError(
1270
+ `Could not read the RPC password ${source} at ${path}: ${error.message}`,
1271
+ "CONFIG_READ_ERROR",
1272
+ { path }
1273
+ );
1274
+ }
1275
+ }
1276
+ function resolveSecretRef(value) {
1277
+ if (value === void 0) return void 0;
1278
+ if (value.startsWith("env:")) {
1279
+ const fromEnv = process.env[value.slice(4)];
1280
+ return fromEnv === void 0 ? void 0 : trimTrailingNewline(fromEnv);
1281
+ }
1282
+ if (value.startsWith("file:")) {
1283
+ return readSecretFile(value.slice(5), "file reference");
1284
+ }
1285
+ return value;
1286
+ }
1287
+ function readRpcPassFile() {
1288
+ const path = process.env[ENV_RPC_PASS_FILE];
1289
+ if (!path) return void 0;
1290
+ return readSecretFile(path, `file named by ${ENV_RPC_PASS_FILE}`);
1291
+ }
1292
+ function resolveBroadcastOptions(network, overrides, flags) {
1293
+ const file = readConfigFile(overrides?.config ?? defaultConfigPath(overrides));
1294
+ const { name: activeProfile } = resolveActiveProfile(file, overrides);
1295
+ const profileBtc = file?.profiles?.[activeProfile ?? network]?.btc;
1296
+ const options = {};
1297
+ const feeRateRaw = blankToUndef(flags.feeRate) ?? blankToUndef(process.env[ENV_VARS.FEE_RATE]) ?? (typeof profileBtc?.feeRate === "number" ? String(profileBtc.feeRate) : void 0);
1298
+ if (feeRateRaw !== void 0) options.feeEstimator = new import_method.StaticFeeEstimator(parseFeeRate(feeRateRaw));
1299
+ const changeAddress = blankToUndef(flags.changeAddress) ?? blankToUndef(profileBtc?.changeAddress);
1300
+ if (changeAddress) options.changeAddress = changeAddress;
1301
+ return options.feeEstimator || options.changeAddress ? options : void 0;
1302
+ }
1303
+ function parseFeeRate(raw) {
1304
+ const rate = Number(raw);
1305
+ if (!Number.isFinite(rate) || rate <= 0) {
1306
+ throw new CLIError(
1307
+ `Invalid --fee-rate "${raw}": expected a positive number of sats per vByte.`,
1308
+ "INVALID_ARGUMENT_ERROR",
1309
+ { value: raw }
1310
+ );
1311
+ }
1312
+ return rate;
1313
+ }
1314
+ function resolveEffectiveConfig(network, overrides) {
1315
+ const file = readConfigFile(overrides?.config ?? defaultConfigPath(overrides));
1316
+ const { name: activeProfile } = resolveActiveProfile(file, overrides);
1317
+ const profileName = activeProfile ?? network;
1318
+ const fileOv = file ? profileToOverrides(file, profileName) : {};
1319
+ const profileBtc = file?.profiles?.[profileName]?.btc;
1320
+ const profileCas = file?.profiles?.[profileName]?.cas;
1321
+ const env = readEnvOverrides();
1322
+ const api = defaultApiFactory(network, overrides);
1323
+ const restCfg = api.btc.connection.rest.config;
1324
+ const rpcCfg = api.btc.connection.rpc?.config;
1325
+ const conn = resolveConnectionConfig(network, overrides);
1326
+ const src = (flag, envVal, fileVal) => blankToUndef(flag) !== void 0 ? "flag" : blankToUndef(envVal) !== void 0 ? "env" : fileVal !== void 0 && fileVal !== null && String(fileVal).trim() !== "" ? "file" : "default";
1327
+ const casGatewayVal = conn.cas?.gateway ?? (conn.cas?.rpcUrl ? void 0 : import_api.DEFAULT_CAS_GATEWAY);
1328
+ const rpcUnit = resolveRpcUnit(overrides, env, fileOv);
1329
+ const rpcSrc = rpcUnit?.src ?? "default";
1330
+ const passFromFile = rpcUnit?.pass === void 0 && process.env[ENV_RPC_PASS_FILE] !== void 0 && rpcCfg?.password !== void 0;
1331
+ return {
1332
+ network,
1333
+ profile: activeProfile,
1334
+ btc: {
1335
+ rest: { value: restCfg.host, source: src(overrides?.btcRest, env.btcRest, fileOv.btcRest) },
1336
+ rpcUrl: { value: rpcCfg?.host, source: rpcUnit?.url !== void 0 ? rpcSrc : "default" },
1337
+ rpcUser: { value: rpcCfg?.username, source: rpcUnit?.user !== void 0 ? rpcSrc : "default" },
1338
+ rpcPass: { value: rpcCfg?.password, source: rpcUnit?.pass !== void 0 ? rpcSrc : passFromFile ? "env" : "default" },
1339
+ rpcWallet: { value: rpcCfg?.wallet, source: src(overrides?.btcRpcWallet, void 0, profileBtc?.wallet) },
1340
+ timeoutMs: { value: conn.btc?.timeoutMs, source: src(overrides?.btcTimeout, process.env[ENV_VARS.BTC_TIMEOUT], profileBtc?.timeoutMs) }
1341
+ },
1342
+ cas: {
1343
+ gateway: { value: casGatewayVal, source: src(overrides?.casGateway, env.casGateway, fileOv.casGateway) },
1344
+ rpcUrl: { value: conn.cas?.rpcUrl, source: src(overrides?.casRpcUrl, env.casRpcUrl, fileOv.casRpcUrl) },
1345
+ timeoutMs: { value: conn.cas?.timeoutMs, source: src(overrides?.casTimeout, process.env[ENV_VARS.CAS_TIMEOUT], profileCas?.timeoutMs) }
1346
+ }
1347
+ };
1348
+ }
1349
+ var DOCTOR_PROBE_TIMEOUT_MS = 5e3;
1350
+ async function probeEndpoint(endpoint, target, url, opts) {
1351
+ try {
1352
+ const res = await fetch(url, {
1353
+ method: opts?.method ?? "GET",
1354
+ headers: opts?.headers,
1355
+ signal: AbortSignal.timeout(DOCTOR_PROBE_TIMEOUT_MS)
1356
+ });
1357
+ return res.ok ? { endpoint, target, ok: true } : { endpoint, target, ok: false, detail: `HTTP ${res.status}` };
1358
+ } catch (error) {
1359
+ return { endpoint, target, ok: false, detail: error.message };
1360
+ }
1361
+ }
1362
+ function withProbeTimeout(promise, ms) {
1363
+ let timer;
1364
+ const timeout = new Promise((_, reject) => {
1365
+ timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
1366
+ });
1367
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
1368
+ }
1369
+ async function runDoctor(network, overrides) {
1370
+ const api = defaultApiFactory(network, overrides);
1371
+ const checks = [];
1372
+ const restHost = api.btc.connection.rest.config.host.replace(/\/+$/, "");
1373
+ checks.push(await probeEndpoint("btc-rest", restHost, `${restHost}/blocks/tip/height`, { headers: api.btc.connection.rest.config.headers }));
1374
+ const rpc = api.btc.connection.rpc;
1375
+ if (rpc) {
1376
+ const target = rpc.config.host ?? "(default rpc)";
1377
+ try {
1378
+ await withProbeTimeout(rpc.getBlockchainInfo(), DOCTOR_PROBE_TIMEOUT_MS);
1379
+ checks.push({ endpoint: "btc-rpc", target, ok: true });
1380
+ } catch (error) {
1381
+ checks.push({ endpoint: "btc-rpc", target, ok: false, detail: error.message });
1382
+ }
1383
+ }
1384
+ const conn = resolveConnectionConfig(network, overrides);
1385
+ if (conn.cas?.rpcUrl) {
1386
+ const base = conn.cas.rpcUrl.replace(/\/+$/, "");
1387
+ checks.push(await probeEndpoint("cas", conn.cas.rpcUrl, `${base}/api/v0/version`, { method: "POST" }));
1388
+ } else {
1389
+ const gateway = (conn.cas?.gateway ?? import_api.DEFAULT_CAS_GATEWAY).replace(/\/+$/, "");
1390
+ checks.push(await probeEndpoint("cas", gateway, gateway));
1391
+ }
1392
+ const mismatch = profileNetworkMismatch(network, overrides);
1393
+ return {
1394
+ checks,
1395
+ ...mismatch ? { coherence: { profile: mismatch.profile, declared: mismatch.declared, encoding: network } } : {}
1396
+ };
1397
+ }
794
1398
  function defaultApiFactory(network, overrides) {
795
1399
  return (0, import_api.createApi)(resolveConnectionConfig(network, overrides));
796
1400
  }
797
1401
  function buildKeystoreKms(overrides) {
798
1402
  return new FileBackedKeyManager({
799
- path: overrides?.keystore ?? defaultKeystorePath(),
800
- getPassphrase: () => acquirePassphrase({ passphraseFile: overrides?.passphraseFile })
1403
+ path: resolveKeystorePath(overrides),
1404
+ // The store decides when to confirm: it passes `confirm: true` only while
1405
+ // establishing a fresh keystore's passphrase, so a first-key typo is caught
1406
+ // by a second entry (ADR 080). confirm is a no-op for env/file sources.
1407
+ getPassphrase: (opts) => acquirePassphrase({ passphraseFile: overrides?.passphraseFile, confirm: opts?.confirm })
801
1408
  });
802
1409
  }
1410
+ function resolveKeystoreProtection(overrides) {
1411
+ return keystoreProtection(resolveKeystorePath(overrides));
1412
+ }
1413
+ function assertKeystoreAllowedForNetwork(network, overrides) {
1414
+ if (network !== "bitcoin") return;
1415
+ if (resolveKeystoreProtection(overrides) !== "dev") return;
1416
+ const path = resolveKeystorePath(overrides);
1417
+ throw new CLIError(
1418
+ `Refusing a mainnet (bitcoin) operation with the unencrypted dev keystore at ${path}. Dev keystores hold plaintext keys and are for testnet/regtest throwaway material only. Establish an encrypted keystore (btcr2 keystore init) for mainnet keys.`,
1419
+ "DEV_KEYSTORE_MAINNET_ERROR",
1420
+ { path, network }
1421
+ );
1422
+ }
1423
+ function resolveKeystorePath(overrides, options) {
1424
+ const fromFlag = blankToUndef(overrides?.keystore);
1425
+ if (fromFlag) return fromFlag;
1426
+ let identity;
1427
+ try {
1428
+ identity = activeProfileIdentity(overrides);
1429
+ } catch (error) {
1430
+ if (!options?.lenient) throw error;
1431
+ }
1432
+ return blankToUndef(identity?.keystore) ?? defaultKeystorePath(overrides);
1433
+ }
1434
+ function activeProfileIdentity(overrides) {
1435
+ const file = readConfigFile(overrides?.config ?? defaultConfigPath(overrides));
1436
+ const { name } = resolveActiveProfile(file, overrides);
1437
+ return name ? file?.profiles?.[name]?.identity : void 0;
1438
+ }
1439
+ function resolveSigningKeyRef(overrides) {
1440
+ return blankToUndef(overrides?.signingKey) ?? blankToUndef(activeProfileIdentity(overrides)?.default);
1441
+ }
803
1442
  function keystoreApiFactory(network, overrides) {
804
1443
  return (0, import_api.createApi)({
805
1444
  ...resolveConnectionConfig(network, overrides),
@@ -818,6 +1457,85 @@ function deriveNetwork(did) {
818
1457
  return network;
819
1458
  }
820
1459
 
1460
+ // src/output.ts
1461
+ var REDACTED = "********";
1462
+ var SECRET_KEY = /(pass|secret|token|auth|api[-_]?key|credential|bearer)/i;
1463
+ function isSecretKey(key) {
1464
+ return SECRET_KEY.test(key);
1465
+ }
1466
+ function scrubUrlUserinfo(value) {
1467
+ if (value === void 0) return void 0;
1468
+ return value.replace(/^([a-z][a-z0-9+.-]*:\/\/[^/@:]+):[^/@]*@/i, `$1:${REDACTED}@`);
1469
+ }
1470
+ function redactSecrets(value, keyName) {
1471
+ if (Array.isArray(value)) {
1472
+ return value.map((item) => redactSecrets(item));
1473
+ }
1474
+ if (value !== null && typeof value === "object") {
1475
+ const out = {};
1476
+ for (const [key, val] of Object.entries(value)) {
1477
+ out[key] = redactSecrets(val, key);
1478
+ }
1479
+ return out;
1480
+ }
1481
+ if (keyName !== void 0 && isSecretKey(keyName)) {
1482
+ return value === void 0 ? void 0 : REDACTED;
1483
+ }
1484
+ return typeof value === "string" ? scrubUrlUserinfo(value) : value;
1485
+ }
1486
+ function formatResult(result, options) {
1487
+ if (options.output === "json") {
1488
+ return JSON.stringify(result, null, 2);
1489
+ }
1490
+ const { data } = result;
1491
+ return typeof data === "string" ? data : JSON.stringify(data, null, 2);
1492
+ }
1493
+
1494
+ // src/commands/init.ts
1495
+ function registerInitCommand(program, globals) {
1496
+ const print = (result) => console.log(formatResult(result, globals()));
1497
+ program.command("init").description("Set up the btcr2 home: create the directory, a default config, and establish the keystore.").option("--dev", "Establish an UNENCRYPTED dev keystore: plaintext keys, no passphrase. Testnet only.", false).option("--force", "Re-create the config and keystore even if they already exist.", false).action((options) => {
1498
+ const g = globals();
1499
+ const home = resolveHome(g);
1500
+ const configPath = g.config ?? defaultConfigPath(g);
1501
+ const keystorePath = resolveKeystorePath(g);
1502
+ ensureDir(home, 448);
1503
+ const created = [];
1504
+ if (!(0, import_node_fs6.existsSync)(configPath) || options.force) {
1505
+ writeDefaultConfigFile(configPath);
1506
+ created.push("config");
1507
+ }
1508
+ const keystoreExists = (0, import_node_fs6.existsSync)(keystorePath);
1509
+ if (keystoreExists && options.force && !g.quiet) {
1510
+ process.stderr.write(
1511
+ `note: a keystore already exists at ${keystorePath} and was left intact. To re-establish it (discarding its keys), run "btcr2 keystore init --force".
1512
+ `
1513
+ );
1514
+ }
1515
+ if (!keystoreExists) {
1516
+ if (options.dev && !g.quiet) {
1517
+ process.stderr.write(
1518
+ "warning: establishing an UNENCRYPTED dev keystore. Keys are stored in plaintext. Use it only for disposable testnet material; mainnet operations will be refused.\n"
1519
+ );
1520
+ }
1521
+ initKeystore(keystorePath, {
1522
+ protection: options.dev ? "none" : "passphrase",
1523
+ getPassphrase: (opts) => acquirePassphrase({ passphraseFile: g.passphraseFile, confirm: opts?.confirm, prompt: "New keystore passphrase: " })
1524
+ });
1525
+ created.push("keystore");
1526
+ }
1527
+ const protection = keystoreSummary(keystorePath).protection;
1528
+ print({ action: "init", data: { home, config: configPath, keystore: keystorePath, created, protection } });
1529
+ if (!g.quiet && g.output !== "json") {
1530
+ process.stderr.write(`btcr2 home ready at ${home}. Next: btcr2 key generate --set-active
1531
+ `);
1532
+ }
1533
+ });
1534
+ }
1535
+
1536
+ // src/commands/create.ts
1537
+ var import_utils3 = require("@noble/hashes/utils.js");
1538
+
821
1539
  // src/keystore/resolve-key-ref.ts
822
1540
  function fingerprintOf(id) {
823
1541
  return /^urn:kms:secp256k1:([0-9a-f]{32})$/.exec(id)?.[1];
@@ -856,15 +1574,6 @@ function resolveKeyRef(kms, ref) {
856
1574
  throw new CLIError(`No key matches reference "${ref}".`, "KEY_NOT_FOUND_ERROR", { ref });
857
1575
  }
858
1576
 
859
- // src/output.ts
860
- function formatResult(result, options) {
861
- if (options.output === "json") {
862
- return JSON.stringify(result, null, 2);
863
- }
864
- const { data } = result;
865
- return typeof data === "string" ? data : JSON.stringify(data, null, 2);
866
- }
867
-
868
1577
  // src/commands/create.ts
869
1578
  var EXPECTED_BYTES = {
870
1579
  k: { length: 33, label: "secp256k1 compressed public key (33 bytes)" },
@@ -885,6 +1594,13 @@ function registerCreateCommand(program, factory, keystoreFactory, globals) {
885
1594
  const overrides = overridesFromGlobals(g);
886
1595
  const network = resolveNetwork(options.network, overrides);
887
1596
  const signingKey = g.signingKey;
1597
+ const mismatch = profileNetworkMismatch(network, overrides);
1598
+ if (mismatch && !g.quiet) {
1599
+ process.stderr.write(
1600
+ `Warning: creating a "${network}" identifier while the active profile "${mismatch.profile}" declares network "${mismatch.declared}". The identifier's network and the profile's endpoints may not match.
1601
+ `
1602
+ );
1603
+ }
888
1604
  const print = (result, note) => {
889
1605
  console.log(formatResult(result, g));
890
1606
  if (note && g.output !== "json") process.stderr.write(`${note}
@@ -926,14 +1642,15 @@ function registerCreateCommand(program, factory, keystoreFactory, globals) {
926
1642
  const publicKey2 = api2.kms.getPublicKey(keyId2);
927
1643
  const did2 = api2.createDid("deterministic", publicKey2, { network });
928
1644
  print(
929
- { action: "create", data: did2, keyId: keyId2, publicKey: (0, import_utils2.bytesToHex)(publicKey2) },
1645
+ { action: "create", data: did2, keyId: keyId2, publicKey: (0, import_utils3.bytesToHex)(publicKey2) },
930
1646
  `Using stored key ${keyId2}.`
931
1647
  );
932
1648
  return;
933
1649
  }
1650
+ assertKeystoreAllowedForNetwork(network, overrides);
934
1651
  const api = keystoreFactory(void 0, overrides);
935
1652
  const { did, keyId } = api.generateDid({ network, setActive: true });
936
- const publicKey = (0, import_utils2.bytesToHex)(api.kms.getPublicKey(keyId));
1653
+ const publicKey = (0, import_utils3.bytesToHex)(api.kms.getPublicKey(keyId));
937
1654
  print(
938
1655
  { action: "create", data: did, keyId, publicKey },
939
1656
  `Generated and stored key ${keyId} (now the active key).`
@@ -942,6 +1659,7 @@ function registerCreateCommand(program, factory, keystoreFactory, globals) {
942
1659
  }
943
1660
  function overridesFromGlobals(g) {
944
1661
  return {
1662
+ home: g.home,
945
1663
  config: g.config,
946
1664
  profile: g.profile,
947
1665
  keystore: g.keystore,
@@ -963,7 +1681,7 @@ function parseGenesisBytes(hex, type) {
963
1681
  const expected = EXPECTED_BYTES[type];
964
1682
  let bytes;
965
1683
  try {
966
- bytes = (0, import_utils2.hexToBytes)(hex.trim());
1684
+ bytes = (0, import_utils3.hexToBytes)(hex.trim());
967
1685
  } catch {
968
1686
  throw new CLIError(
969
1687
  `Invalid bytes: not valid hex. Expected ${expected.label}.`,
@@ -1048,6 +1766,12 @@ function registerUpdateCommand(program, factory, globals) {
1048
1766
  "Publish update artifacts to a writable CAS before broadcast: auto|always|never. CAS publication is optional; the default distributes the returned artifacts via sidecar.",
1049
1767
  parsePublishToCasMode,
1050
1768
  "never"
1769
+ ).option(
1770
+ "--fee-rate <satsPerVByte>",
1771
+ "Fee rate in sats/vByte for the beacon transaction (default: 5). Raise it under congestion so the transaction confirms."
1772
+ ).option(
1773
+ "--change-address <address>",
1774
+ "Send transaction change to this address instead of the beacon address, so a DID's announcements are not linked on-chain (ADR 044)."
1051
1775
  ).action(async (options) => {
1052
1776
  if (!/^\d+$/.test(options.sourceVersionId)) {
1053
1777
  throw new CLIError(
@@ -1072,9 +1796,14 @@ function registerUpdateCommand(program, factory, globals) {
1072
1796
  );
1073
1797
  }
1074
1798
  const network = deriveNetwork(did);
1799
+ assertKeystoreAllowedForNetwork(network, globals());
1075
1800
  const api = factory(network, globals());
1076
- const keyId = resolveKeyRef(api.kms.kms, globals().signingKey);
1801
+ const keyId = resolveKeyRef(api.kms.kms, resolveSigningKeyRef(globals()));
1077
1802
  const signer = new import_key_manager2.KeyManagerSigner(api.kms.kms, keyId);
1803
+ const broadcastOptions = resolveBroadcastOptions(network, globals(), {
1804
+ feeRate: options.feeRate,
1805
+ changeAddress: options.changeAddress
1806
+ });
1078
1807
  const data = await api.btcr2.update({
1079
1808
  sourceDocument: parsed.sourceDocument,
1080
1809
  patches: parsed.patches,
@@ -1082,7 +1811,8 @@ function registerUpdateCommand(program, factory, globals) {
1082
1811
  verificationMethodId: parsed.verificationMethodId,
1083
1812
  beaconId: parsed.beaconId,
1084
1813
  signer,
1085
- publishToCas: options.publishToCas
1814
+ publishToCas: options.publishToCas,
1815
+ ...broadcastOptions ? { broadcastOptions } : {}
1086
1816
  });
1087
1817
  console.log(formatResult({ action: "update", data }, globals()));
1088
1818
  });
@@ -1134,6 +1864,12 @@ function registerDeactivateCommand(program, factory, globals) {
1134
1864
  "Publish update artifacts to a writable CAS before broadcast: auto|always|never. CAS publication is optional; the default distributes the returned artifacts via sidecar.",
1135
1865
  parsePublishToCasMode2,
1136
1866
  "never"
1867
+ ).option(
1868
+ "--fee-rate <satsPerVByte>",
1869
+ "Fee rate in sats/vByte for the beacon transaction (default: 5). Raise it under congestion so the transaction confirms."
1870
+ ).option(
1871
+ "--change-address <address>",
1872
+ "Send transaction change to this address instead of the beacon address, so a DID's announcements are not linked on-chain (ADR 044)."
1137
1873
  ).action(async (options) => {
1138
1874
  if (!/^\d+$/.test(options.sourceVersionId)) {
1139
1875
  throw new CLIError(
@@ -1158,9 +1894,14 @@ function registerDeactivateCommand(program, factory, globals) {
1158
1894
  );
1159
1895
  }
1160
1896
  const network = deriveNetwork(did);
1897
+ assertKeystoreAllowedForNetwork(network, globals());
1161
1898
  const api = factory(network, globals());
1162
- const keyId = resolveKeyRef(api.kms.kms, globals().signingKey);
1899
+ const keyId = resolveKeyRef(api.kms.kms, resolveSigningKeyRef(globals()));
1163
1900
  const signer = new import_key_manager3.KeyManagerSigner(api.kms.kms, keyId);
1901
+ const broadcastOptions = resolveBroadcastOptions(network, globals(), {
1902
+ feeRate: options.feeRate,
1903
+ changeAddress: options.changeAddress
1904
+ });
1164
1905
  const data = await api.btcr2.update({
1165
1906
  sourceDocument: parsed.sourceDocument,
1166
1907
  patches: parsed.patches,
@@ -1168,7 +1909,8 @@ function registerDeactivateCommand(program, factory, globals) {
1168
1909
  verificationMethodId: parsed.verificationMethodId,
1169
1910
  beaconId: parsed.beaconId,
1170
1911
  signer,
1171
- publishToCas: options.publishToCas
1912
+ publishToCas: options.publishToCas,
1913
+ ...broadcastOptions ? { broadcastOptions } : {}
1172
1914
  });
1173
1915
  console.log(formatResult({ action: "deactivate", data }, globals()));
1174
1916
  });
@@ -1199,8 +1941,8 @@ function parseJsonArg2(flagName) {
1199
1941
 
1200
1942
  // src/commands/key.ts
1201
1943
  var import_keypair = require("@did-btcr2/keypair");
1202
- var import_utils3 = require("@noble/hashes/utils.js");
1203
- var import_node_fs6 = require("fs");
1944
+ var import_utils4 = require("@noble/hashes/utils.js");
1945
+ var import_node_fs7 = require("fs");
1204
1946
  function registerKeyCommand(program, factory, globals) {
1205
1947
  const key = program.command("key").description("Manage keypairs in the encrypted keystore.");
1206
1948
  const print = (result) => console.log(formatResult(result, globals()));
@@ -1209,7 +1951,7 @@ function registerKeyCommand(program, factory, globals) {
1209
1951
  assertNameAvailable(api.kms.kms, options.name);
1210
1952
  const setActive = options.setActive ?? false;
1211
1953
  const id = api.kms.generateKey({ ...options.name && { tags: { name: options.name } }, setActive });
1212
- print({ action: "key-generate", data: { keyId: id, publicKey: (0, import_utils3.bytesToHex)(api.kms.getPublicKey(id)), active: setActive } });
1954
+ print({ action: "key-generate", data: { keyId: id, publicKey: (0, import_utils4.bytesToHex)(api.kms.getPublicKey(id)), active: setActive } });
1213
1955
  });
1214
1956
  key.command("list").alias("ls").description("List stored keys.").action(() => {
1215
1957
  const kms = factory(void 0, globals()).kms.kms;
@@ -1229,7 +1971,7 @@ function registerKeyCommand(program, factory, globals) {
1229
1971
  const kms = factory(void 0, globals()).kms.kms;
1230
1972
  const id = resolveKeyRef(kms, ref);
1231
1973
  const entry = kms.getEntry(id);
1232
- print({ action: "key-show", data: { keyId: id, publicKey: (0, import_utils3.bytesToHex)(entry.publicKey), ...entry.tags && { tags: entry.tags } } });
1974
+ print({ action: "key-show", data: { keyId: id, publicKey: (0, import_utils4.bytesToHex)(entry.publicKey), ...entry.tags && { tags: entry.tags } } });
1233
1975
  });
1234
1976
  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) => {
1235
1977
  if (Boolean(options.secretFile) === Boolean(options.public)) {
@@ -1240,13 +1982,13 @@ function registerKeyCommand(program, factory, globals) {
1240
1982
  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") });
1241
1983
  const setActive = options.setActive ?? false;
1242
1984
  const id = api.kms.import(keyPair, { ...options.name && { tags: { name: options.name } }, setActive });
1243
- print({ action: "key-import", data: { keyId: id, publicKey: (0, import_utils3.bytesToHex)(api.kms.getPublicKey(id)), watchOnly: !options.secretFile, active: setActive } });
1985
+ print({ action: "key-import", data: { keyId: id, publicKey: (0, import_utils4.bytesToHex)(api.kms.getPublicKey(id)), watchOnly: !options.secretFile, active: setActive } });
1244
1986
  });
1245
1987
  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) => {
1246
1988
  const api = factory(void 0, globals());
1247
1989
  const id = resolveKeyRef(api.kms.kms, ref);
1248
1990
  if (!options.secret) {
1249
- print({ action: "key-export", data: { keyId: id, publicKey: (0, import_utils3.bytesToHex)(api.kms.getPublicKey(id)) } });
1991
+ print({ action: "key-export", data: { keyId: id, publicKey: (0, import_utils4.bytesToHex)(api.kms.getPublicKey(id)) } });
1250
1992
  return;
1251
1993
  }
1252
1994
  if (!options.out) {
@@ -1257,7 +1999,7 @@ function registerKeyCommand(program, factory, globals) {
1257
1999
  throw new CLIError(`Key ${id} is watch-only and has no secret to export.`, "INVALID_ARGUMENT_ERROR", { keyId: id });
1258
2000
  }
1259
2001
  process.stderr.write("warning: writing an unencrypted secret key to disk. Protect this file and delete it when done.\n");
1260
- writeSecretFile(options.out, (0, import_utils3.bytesToHex)(keyPair.secretKey.bytes));
2002
+ writeSecretFile(options.out, (0, import_utils4.bytesToHex)(keyPair.secretKey.bytes));
1261
2003
  print({ action: "key-export", data: { keyId: id, secretWrittenTo: options.out } });
1262
2004
  });
1263
2005
  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) => {
@@ -1282,7 +2024,7 @@ function assertNameAvailable(kms, name) {
1282
2024
  function parseHex(hex, expectedBytes, label) {
1283
2025
  let bytes;
1284
2026
  try {
1285
- bytes = (0, import_utils3.hexToBytes)(hex.trim());
2027
+ bytes = (0, import_utils4.hexToBytes)(hex.trim());
1286
2028
  } catch {
1287
2029
  throw new CLIError(`Invalid hex for ${label}.`, "INVALID_ARGUMENT_ERROR", { label });
1288
2030
  }
@@ -1298,7 +2040,7 @@ function parseHex(hex, expectedBytes, label) {
1298
2040
  function readHexFile(path, expectedBytes, label) {
1299
2041
  let content;
1300
2042
  try {
1301
- content = (0, import_node_fs6.readFileSync)(path, "utf-8");
2043
+ content = (0, import_node_fs7.readFileSync)(path, "utf-8");
1302
2044
  } catch {
1303
2045
  throw new CLIError(`Cannot read ${label} at ${path}.`, "INVALID_ARGUMENT_ERROR", { label, path });
1304
2046
  }
@@ -1307,7 +2049,7 @@ function readHexFile(path, expectedBytes, label) {
1307
2049
  function writeSecretFile(path, contents) {
1308
2050
  let fd;
1309
2051
  try {
1310
- fd = (0, import_node_fs6.openSync)(path, "wx", 384);
2052
+ fd = (0, import_node_fs7.openSync)(path, "wx", 384);
1311
2053
  } catch (error) {
1312
2054
  if (error.code === "EEXIST") {
1313
2055
  throw new CLIError(`Refusing to overwrite existing file ${path}. Choose a new --out path.`, "INVALID_ARGUMENT_ERROR", { path });
@@ -1315,62 +2057,316 @@ function writeSecretFile(path, contents) {
1315
2057
  throw error;
1316
2058
  }
1317
2059
  try {
1318
- (0, import_node_fs6.writeFileSync)(fd, contents);
2060
+ (0, import_node_fs7.writeFileSync)(fd, contents);
1319
2061
  } finally {
1320
- (0, import_node_fs6.closeSync)(fd);
2062
+ (0, import_node_fs7.closeSync)(fd);
2063
+ }
2064
+ }
2065
+
2066
+ // src/commands/keystore.ts
2067
+ var import_node_fs8 = require("fs");
2068
+ function registerKeystoreCommand(program, globals) {
2069
+ const keystore = program.command("keystore").description("Establish, inspect, and re-key the keystore.");
2070
+ const print = (result) => console.log(formatResult(result, globals()));
2071
+ keystore.command("init").description("Establish the keystore (encrypted by default). Prompts for a passphrase and confirms it.").option("--dev", "Create an UNENCRYPTED dev keystore: plaintext keys, no passphrase. Testnet only; mainnet operations are refused.", false).option("--force", "Re-establish even if a keystore already exists (discards its keys).", false).action((options) => {
2072
+ const g = globals();
2073
+ const path = resolveKeystorePath(g);
2074
+ if ((0, import_node_fs8.existsSync)(path) && !options.force) {
2075
+ throw new CLIError(
2076
+ `A keystore already exists at ${path}. Use --force to re-establish it (this discards its keys).`,
2077
+ "INVALID_ARGUMENT_ERROR",
2078
+ { path }
2079
+ );
2080
+ }
2081
+ if ((0, import_node_fs8.existsSync)(path) && options.force && !g.quiet) {
2082
+ const { keyCount } = keystoreSummary(path);
2083
+ if (keyCount > 0) {
2084
+ process.stderr.write(
2085
+ `warning: re-establishing the keystore at ${path} permanently discards its ${keyCount} existing key(s).
2086
+ `
2087
+ );
2088
+ }
2089
+ }
2090
+ if (options.dev && !g.quiet) {
2091
+ process.stderr.write(
2092
+ "warning: creating an UNENCRYPTED dev keystore. Keys are stored in plaintext. Use it only for disposable testnet material; mainnet operations will be refused.\n"
2093
+ );
2094
+ }
2095
+ initKeystore(path, {
2096
+ protection: options.dev ? "none" : "passphrase",
2097
+ getPassphrase: (opts) => acquirePassphrase({ passphraseFile: g.passphraseFile, confirm: opts?.confirm, prompt: "New keystore passphrase: " })
2098
+ });
2099
+ print({ action: "keystore-init", data: { path, protection: options.dev ? "dev" : "encrypted" } });
2100
+ });
2101
+ keystore.command("status").description("Show the keystore path, protection mode, and key count. Never decrypts or prompts.").action(() => {
2102
+ const g = globals();
2103
+ const path = resolveKeystorePath(g, { lenient: true });
2104
+ const summary = keystoreSummary(path);
2105
+ if (summary.protection === "dev" && !g.quiet && g.output !== "json") {
2106
+ process.stderr.write("warning: this is an UNENCRYPTED dev keystore; keys are stored in plaintext.\n");
2107
+ }
2108
+ print({ action: "keystore-status", data: { path, ...summary } });
2109
+ });
2110
+ keystore.command("change-passphrase").alias("passwd").description("Change the keystore passphrase, re-sealing every key under the new one. Encrypted keystores only.").action(() => {
2111
+ const g = globals();
2112
+ const path = resolveKeystorePath(g);
2113
+ const summary = keystoreSummary(path);
2114
+ if (summary.protection === "absent") {
2115
+ throw new CLIError(`No keystore at ${path}. Run "btcr2 keystore init" first.`, "INVALID_ARGUMENT_ERROR", { path });
2116
+ }
2117
+ if (summary.protection === "dev") {
2118
+ throw new CLIError(
2119
+ `The keystore at ${path} is an unencrypted dev keystore; there is no passphrase to change.`,
2120
+ "INVALID_ARGUMENT_ERROR",
2121
+ { path }
2122
+ );
2123
+ }
2124
+ const oldPassphrase = acquirePassphrase({ passphraseFile: g.passphraseFile, prompt: "Current keystore passphrase: " });
2125
+ const newPassphrase = acquirePassphrase({ forcePrompt: true, confirm: true, prompt: "New keystore passphrase: " });
2126
+ const rekeyed = changeKeystorePassphrase(path, oldPassphrase, newPassphrase);
2127
+ print({ action: "keystore-change-passphrase", data: { path, rekeyed } });
2128
+ });
2129
+ }
2130
+
2131
+ // src/commands/config.ts
2132
+ var import_node_fs9 = require("fs");
2133
+
2134
+ // src/config-schema.ts
2135
+ var CONFIG_SCHEMA = {
2136
+ schemaVersion: "number",
2137
+ defaults: {
2138
+ profile: "string",
2139
+ network: "enum:network",
2140
+ output: "enum:output"
2141
+ },
2142
+ profiles: {
2143
+ "*": {
2144
+ network: "enum:network",
2145
+ btc: {
2146
+ rest: "string",
2147
+ rpcUrl: "string",
2148
+ rpcUser: "string",
2149
+ rpcPass: "string",
2150
+ feeRate: "number",
2151
+ changeAddress: "string",
2152
+ timeoutMs: "number",
2153
+ headers: "object",
2154
+ wallet: "string",
2155
+ rpcHeaders: "object"
2156
+ },
2157
+ cas: {
2158
+ gateway: "string",
2159
+ rpcUrl: "string",
2160
+ timeoutMs: "number"
2161
+ },
2162
+ identity: {
2163
+ keystore: "string",
2164
+ default: "string"
2165
+ }
2166
+ }
2167
+ }
2168
+ };
2169
+ function lookupSchemaNode(dotted) {
2170
+ let node = CONFIG_SCHEMA;
2171
+ for (const segment of dotted.split(".")) {
2172
+ if (typeof node !== "object") return void 0;
2173
+ if (Object.hasOwn(node, segment)) {
2174
+ node = node[segment];
2175
+ } else if (Object.hasOwn(node, "*")) {
2176
+ node = node["*"];
2177
+ } else {
2178
+ return void 0;
2179
+ }
2180
+ }
2181
+ return node;
2182
+ }
2183
+ function assertLeafValue(kind, dotted, value) {
2184
+ if (kind === "enum:network" && !SUPPORTED_NETWORKS.includes(value)) {
2185
+ throw new CLIError(
2186
+ `Invalid value for ${dotted}: "${String(value)}". Expected one of ${SUPPORTED_NETWORKS.join(", ")}.`,
2187
+ "INVALID_ARGUMENT_ERROR",
2188
+ { path: dotted, value }
2189
+ );
2190
+ }
2191
+ if (kind === "enum:output" && value !== "json" && value !== "text") {
2192
+ throw new CLIError(
2193
+ `Invalid value for ${dotted}: "${String(value)}". Expected "json" or "text".`,
2194
+ "INVALID_ARGUMENT_ERROR",
2195
+ { path: dotted, value }
2196
+ );
2197
+ }
2198
+ if (kind === "number" && typeof value !== "number") {
2199
+ throw new CLIError(
2200
+ `Invalid value for ${dotted}: expected a number, got ${typeof value}. Pass a bare number (e.g. \`config set ` + dotted + " 5`).",
2201
+ "INVALID_ARGUMENT_ERROR",
2202
+ { path: dotted, value }
2203
+ );
2204
+ }
2205
+ if (kind === "object" && (typeof value !== "object" || value === null || Array.isArray(value))) {
2206
+ throw new CLIError(
2207
+ `Invalid value for ${dotted}: expected a JSON object (e.g. \`config set ${dotted} '{"Key":"Value"}'\`).`,
2208
+ "INVALID_ARGUMENT_ERROR",
2209
+ { path: dotted, value }
2210
+ );
2211
+ }
2212
+ }
2213
+ function validateConfigSet(dotted, value) {
2214
+ const node = lookupSchemaNode(dotted);
2215
+ if (node === void 0) return { unknownPath: true };
2216
+ if (typeof node === "string") {
2217
+ assertLeafValue(node, dotted, value);
2218
+ }
2219
+ return { unknownPath: false };
2220
+ }
2221
+ function findConfigIssues(config, supportedSchemaVersion) {
2222
+ const issues = [];
2223
+ const version = config.schemaVersion;
2224
+ if (typeof version === "number" && version > supportedSchemaVersion) {
2225
+ issues.push({
2226
+ path: "schemaVersion",
2227
+ issue: `newer than supported (${version} > ${supportedSchemaVersion}); upgrade the CLI to use this file`
2228
+ });
2229
+ }
2230
+ walk(config, [], issues);
2231
+ return issues;
2232
+ }
2233
+ function walk(obj, prefix, issues) {
2234
+ for (const [key, value] of Object.entries(obj)) {
2235
+ const path = [...prefix, key];
2236
+ const dotted = path.join(".");
2237
+ const node = lookupSchemaNode(dotted);
2238
+ if (node === void 0) {
2239
+ issues.push({ path: dotted, issue: "unknown key" });
2240
+ continue;
2241
+ }
2242
+ if (typeof node === "string") {
2243
+ try {
2244
+ assertLeafValue(node, dotted, value);
2245
+ } catch (error) {
2246
+ issues.push({ path: dotted, issue: error.message });
2247
+ }
2248
+ } else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
2249
+ walk(value, path, issues);
2250
+ }
1321
2251
  }
1322
2252
  }
1323
2253
 
1324
2254
  // src/commands/config.ts
1325
- var import_node_fs7 = require("fs");
1326
- var import_node_path5 = require("path");
1327
2255
  function registerConfigCommand(program, globals) {
1328
2256
  const config = program.command("config").description("Read and write CLI configuration.");
1329
- const path = () => globals().config ?? defaultConfigPath();
2257
+ const path = () => globals().config ?? defaultConfigPath(globals());
1330
2258
  const print = (result) => console.log(formatResult(result, globals()));
1331
2259
  config.command("init").description("Create a default config file with one profile per network.").option("--force", "Overwrite an existing config file.", false).action((options) => {
1332
2260
  const p = path();
1333
- if ((0, import_node_fs7.existsSync)(p) && !options.force) {
2261
+ if ((0, import_node_fs9.existsSync)(p) && !options.force) {
1334
2262
  throw new CLIError(`Config already exists at ${p}. Use --force to overwrite.`, "INVALID_ARGUMENT_ERROR", { path: p });
1335
2263
  }
1336
- const scaffold = {
1337
- schemaVersion: CONFIG_SCHEMA_VERSION,
1338
- defaults: { output: "text" },
1339
- profiles: Object.fromEntries(SUPPORTED_NETWORKS.map((n) => [n, {}]))
1340
- };
1341
- ensureDir((0, import_node_path5.dirname)(p), 448);
1342
- writeFileAtomic(p, `${JSON.stringify(scaffold, null, 2)}
1343
- `, 384);
2264
+ writeDefaultConfigFile(p);
1344
2265
  print({ action: "config-init", data: { path: p } });
1345
2266
  });
1346
- config.command("get [path]").description("Print a value at a dotted path, or the whole config.").action((dotted) => {
2267
+ config.command("get [path]").description("Print a value at a dotted path, or the whole config.").option("--show-secrets", "Reveal secret values (RPC password, etc.) instead of redacting them.", false).action((dotted, opts) => {
1347
2268
  const file = readConfigFile(path()) ?? {};
1348
- print({ action: "config-get", data: (dotted ? getConfigPath(file, dotted) : file) ?? null });
2269
+ const raw = (dotted ? getConfigPath(file, dotted) : file) ?? null;
2270
+ const leaf = dotted ? dotted.split(".").pop() : void 0;
2271
+ print({ action: "config-get", data: opts.showSecrets ? raw : redactSecrets(raw, leaf) });
1349
2272
  });
1350
2273
  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) => {
1351
- writeConfigFile(path(), (raw) => setConfigPath(raw, dotted, parseValue(value)));
2274
+ const parsed = parseValue(dotted, value);
2275
+ const { unknownPath } = validateConfigSet(dotted, parsed);
2276
+ if (unknownPath && !globals().quiet) {
2277
+ console.error(`Warning: "${dotted}" is not a known config path; writing it anyway.`);
2278
+ }
2279
+ writeConfigFile(path(), (raw) => setConfigPath(raw, dotted, parsed));
1352
2280
  print({ action: "config-set", data: { path: dotted } });
1353
2281
  });
1354
2282
  config.command("unset <path>").description("Delete a value at a dotted path.").action((dotted) => {
1355
2283
  writeConfigFile(path(), (raw) => unsetConfigPath(raw, dotted));
1356
2284
  print({ action: "config-unset", data: { path: dotted } });
1357
2285
  });
1358
- config.command("list").alias("ls").description("Print the entire config file.").action(() => {
1359
- print({ action: "config-list", data: readConfigFile(path()) ?? {} });
2286
+ config.command("list").alias("ls").description("Print the entire config file.").option("--show-secrets", "Reveal secret values (RPC password, etc.) instead of redacting them.", false).action((opts) => {
2287
+ const file = readConfigFile(path()) ?? {};
2288
+ print({ action: "config-list", data: opts.showSecrets ? file : redactSecrets(file) });
2289
+ });
2290
+ config.command("validate").description("Check the config for unknown keys, invalid enum values, and an unsupported schema version.").action(() => {
2291
+ const file = parseConfigFileRaw(path()) ?? {};
2292
+ const issues = findConfigIssues(file, CONFIG_SCHEMA_VERSION);
2293
+ if (issues.length > 0) process.exitCode ??= 1;
2294
+ print({ action: "config-validate", data: { ok: issues.length === 0, issues } });
2295
+ });
2296
+ config.command("effective").description("Print the resolved connection config with per-value provenance (flag|env|file|default).").option("-n, --network <network>", "Network to resolve for (default: config default network)").option("--show-secrets", "Reveal secret values (RPC password) instead of redacting them.", false).action((opts) => {
2297
+ const network = resolveIntrospectionNetwork(opts.network, globals());
2298
+ const data = resolveEffectiveConfig(network, globals());
2299
+ if (!opts.showSecrets) {
2300
+ if (data.btc.rpcPass.value !== void 0) {
2301
+ data.btc.rpcPass = { ...data.btc.rpcPass, value: REDACTED };
2302
+ }
2303
+ for (const entry of [data.btc.rest, data.btc.rpcUrl, data.cas.gateway, data.cas.rpcUrl]) {
2304
+ if (typeof entry.value === "string") entry.value = scrubUrlUserinfo(entry.value);
2305
+ }
2306
+ }
2307
+ print({ action: "config-effective", data });
2308
+ });
2309
+ config.command("path").description("Print the resolved home directory, config-file, and keystore paths.").action(() => {
2310
+ const g = globals();
2311
+ const data = {
2312
+ home: resolveHome(g),
2313
+ config: g.config ?? defaultConfigPath(g),
2314
+ // Diagnostic command: report the path even when the config is malformed
2315
+ // (this is a command you run to find and fix a broken config).
2316
+ keystore: resolveKeystorePath(g, { lenient: true })
2317
+ };
2318
+ print({ action: "config-path", data });
2319
+ });
2320
+ config.command("doctor").description("Probe reachability of the resolved endpoints (read-only; touches the network).").option("-n, --network <network>", "Network to resolve for (default: config default network)").action(async (opts) => {
2321
+ const network = resolveIntrospectionNetwork(opts.network, globals());
2322
+ const report = await runDoctor(network, globals());
2323
+ if (report.checks.some((check) => !check.ok)) process.exitCode ??= 1;
2324
+ print({ action: "config-doctor", data: report });
1360
2325
  });
1361
2326
  }
1362
- function parseValue(value) {
2327
+ function resolveIntrospectionNetwork(explicit, globals) {
2328
+ if (explicit) {
2329
+ if (!SUPPORTED_NETWORKS.includes(explicit)) {
2330
+ throw new CLIError(
2331
+ `Invalid network "${explicit}". Must be one of ${SUPPORTED_NETWORKS.join(", ")}.`,
2332
+ "INVALID_ARGUMENT_ERROR",
2333
+ { network: explicit }
2334
+ );
2335
+ }
2336
+ return explicit;
2337
+ }
2338
+ return resolveDefaultNetwork(globals);
2339
+ }
2340
+ function parseValue(dotted, value) {
2341
+ if (isStringScalarPath(dotted)) return value;
1363
2342
  try {
1364
2343
  return JSON.parse(value);
1365
2344
  } catch {
1366
2345
  return value;
1367
2346
  }
1368
2347
  }
2348
+ function isStringScalarPath(dotted) {
2349
+ const segments = dotted.split(".");
2350
+ if (segments.length === 2 && segments[0] === "defaults") {
2351
+ return ["profile", "network", "output"].includes(segments[1]);
2352
+ }
2353
+ if (segments.length === 3 && segments[0] === "profiles" && segments[2] === "network") {
2354
+ return true;
2355
+ }
2356
+ if (segments.length === 4 && segments[0] === "profiles") {
2357
+ const group = segments[2];
2358
+ const leaf = segments[3];
2359
+ if (group === "btc") return ["rest", "rpcUrl", "rpcUser", "rpcPass", "changeAddress", "wallet"].includes(leaf);
2360
+ if (group === "cas") return ["gateway", "rpcUrl"].includes(leaf);
2361
+ if (group === "identity") return ["keystore", "default"].includes(leaf);
2362
+ }
2363
+ return false;
2364
+ }
1369
2365
 
1370
2366
  // src/commands/profile.ts
1371
2367
  function registerProfileCommand(program, globals) {
1372
2368
  const profile = program.command("profile").description("Manage configuration profiles.");
1373
- const path = () => globals().config ?? defaultConfigPath();
2369
+ const path = () => globals().config ?? defaultConfigPath(globals());
1374
2370
  const print = (result) => console.log(formatResult(result, globals()));
1375
2371
  profile.command("add <name>").description("Add an empty profile.").action((name) => {
1376
2372
  writeConfigFile(path(), (raw) => {
@@ -1388,7 +2384,7 @@ function registerProfileCommand(program, globals) {
1388
2384
  });
1389
2385
  print({ action: "profile-use", data: { profile: name } });
1390
2386
  });
1391
- profile.command("show [name]").description("Show a profile (defaults to the active profile).").action((name) => {
2387
+ profile.command("show [name]").description("Show a profile (defaults to the active profile).").option("--show-secrets", "Reveal secret values (RPC password, etc.) instead of redacting them.", false).action((name, opts) => {
1392
2388
  const file = readConfigFile(path()) ?? {};
1393
2389
  const target = name ?? file.defaults?.profile;
1394
2390
  if (!target) {
@@ -1398,7 +2394,8 @@ function registerProfileCommand(program, globals) {
1398
2394
  if (!data) {
1399
2395
  throw new CLIError(`Profile "${target}" not found.`, "INVALID_ARGUMENT_ERROR", { profile: target });
1400
2396
  }
1401
- print({ action: "profile-show", data: { profile: target, ...data } });
2397
+ const payload = { profile: target, ...data };
2398
+ print({ action: "profile-show", data: opts.showSecrets ? payload : redactSecrets(payload) });
1402
2399
  });
1403
2400
  profile.command("remove <name>").alias("rm").description("Remove a profile.").action((name) => {
1404
2401
  writeConfigFile(path(), (raw) => {
@@ -1442,18 +2439,18 @@ function completionScript(shell) {
1442
2439
  }
1443
2440
 
1444
2441
  // src/version.ts
1445
- var import_node_fs8 = require("fs");
1446
- var import_node_path6 = require("path");
2442
+ var import_node_fs10 = require("fs");
2443
+ var import_node_path5 = require("path");
1447
2444
  var import_node_url = require("url");
1448
2445
  function readVersion() {
1449
- let dir = (0, import_node_path6.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
2446
+ let dir = (0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
1450
2447
  for (let i = 0; i < 5; i++) {
1451
2448
  try {
1452
- const pkg = JSON.parse((0, import_node_fs8.readFileSync)((0, import_node_path6.join)(dir, "package.json"), "utf-8"));
2449
+ const pkg = JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path5.join)(dir, "package.json"), "utf-8"));
1453
2450
  if (pkg.name === "@did-btcr2/cli") return pkg.version;
1454
2451
  } catch {
1455
2452
  }
1456
- dir = (0, import_node_path6.dirname)(dir);
2453
+ dir = (0, import_node_path5.dirname)(dir);
1457
2454
  }
1458
2455
  return "0.0.0";
1459
2456
  }
@@ -1477,13 +2474,19 @@ var DidBtcr2Cli = class {
1477
2474
  * {@link keystoreApiFactory}.
1478
2475
  */
1479
2476
  constructor(factory = defaultApiFactory, keystoreFactory = keystoreApiFactory) {
1480
- 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 (read-only)").option("--cas-rpc-url <url>", "IPFS HTTP RPC endpoint for a writable CAS (reads + writes; enables --publish-to-cas)").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 create/update/deactivate signing: a URN, fingerprint prefix, or name");
2477
+ 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> (default: config defaults.output, else text)").option("--verbose", "Verbose output", false).option("--quiet", "Suppress non-essential output", false).option("--home <dir>", "btcr2 home directory holding config.json + keystore.json (default: ~/.btcr2, %LOCALAPPDATA%\\btcr2 on Windows; overrides $BTCR2_HOME)").option("-c, --config <path>", "Path to config file (default: <home>/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 (read-only)").option("--cas-rpc-url <url>", "IPFS HTTP RPC endpoint for a writable CAS (reads + writes; enables --publish-to-cas)").option("--btc-timeout <ms>", "Bitcoin REST/RPC request timeout in milliseconds (default: unbounded)").option("--cas-timeout <ms>", "CAS request timeout in milliseconds (default: 30000; 0 disables)").option("--btc-rest-header <header>", 'Extra Bitcoin REST header "Key: Value" (repeatable)', collectHeader, []).option("--btc-rpc-wallet <name>", "Bitcoin Core wallet name for wallet-scoped RPCs").option("--btc-rpc-header <header>", 'Extra Bitcoin Core RPC header "Key: Value" (repeatable)', collectHeader, []).option("--keystore <path>", "Path to the keystore file (default: <home>/keystore.json)").option("--passphrase-file <path>", "Read the keystore passphrase from a file (unattended use)").option("--signing-key <ref>", "Key for create/update/deactivate signing: a URN, fingerprint prefix, or name");
1481
2478
  const globals = () => this.program.opts();
2479
+ this.program.hook("preAction", () => {
2480
+ const opts = this.program.opts();
2481
+ opts.output = resolveOutputFormat({ output: opts.output, config: opts.config, home: opts.home });
2482
+ });
2483
+ registerInitCommand(this.program, globals);
1482
2484
  registerCreateCommand(this.program, factory, keystoreFactory, globals);
1483
2485
  registerResolveCommand(this.program, factory, globals);
1484
2486
  registerUpdateCommand(this.program, keystoreFactory, globals);
1485
2487
  registerDeactivateCommand(this.program, keystoreFactory, globals);
1486
2488
  registerKeyCommand(this.program, keystoreFactory, globals);
2489
+ registerKeystoreCommand(this.program, globals);
1487
2490
  registerConfigCommand(this.program, globals);
1488
2491
  registerProfileCommand(this.program, globals);
1489
2492
  registerCompletionCommand(this.program, globals);
@@ -1503,6 +2506,9 @@ var DidBtcr2Cli = class {
1503
2506
  }
1504
2507
  }
1505
2508
  };
2509
+ function collectHeader(value, previous) {
2510
+ return [...previous, value];
2511
+ }
1506
2512
  function normalizeArgv(argv) {
1507
2513
  if (argv.length >= 2) return argv;
1508
2514
  if (argv.length === 1) return ["node", argv[0]];
@@ -1525,28 +2531,51 @@ function handleError(error, verbose) {
1525
2531
  CLIError,
1526
2532
  CONFIG_SCHEMA_VERSION,
1527
2533
  DidBtcr2Cli,
2534
+ ENV_RPC_PASS_FILE,
1528
2535
  ENV_VARS,
2536
+ REDACTED,
1529
2537
  SUPPORTED_NETWORKS,
1530
2538
  VERSION,
2539
+ assertKeystoreAllowedForNetwork,
2540
+ blankToUndef,
1531
2541
  defaultApiFactory,
1532
2542
  defaultConfigPath,
1533
2543
  deriveNetwork,
1534
2544
  formatResult,
1535
2545
  getConfigPath,
2546
+ isSecretKey,
1536
2547
  keystoreApiFactory,
2548
+ parseConfigFileRaw,
2549
+ parseHeaderList,
2550
+ profileNetworkMismatch,
1537
2551
  profileToOverrides,
1538
2552
  readConfigFile,
1539
2553
  readEnvOverrides,
2554
+ redactSecrets,
1540
2555
  registerCompletionCommand,
1541
2556
  registerConfigCommand,
1542
2557
  registerCreateCommand,
1543
2558
  registerDeactivateCommand,
2559
+ registerInitCommand,
1544
2560
  registerKeyCommand,
2561
+ registerKeystoreCommand,
1545
2562
  registerProfileCommand,
1546
2563
  registerResolveCommand,
1547
2564
  registerUpdateCommand,
2565
+ resolveActiveProfile,
2566
+ resolveBroadcastOptions,
2567
+ resolveConnectionConfig,
1548
2568
  resolveDefaultNetwork,
2569
+ resolveEffectiveConfig,
2570
+ resolveKeystorePath,
2571
+ resolveKeystoreProtection,
2572
+ resolveOutputFormat,
2573
+ resolveSecretRef,
2574
+ resolveSigningKeyRef,
2575
+ runDoctor,
2576
+ scrubUrlUserinfo,
1549
2577
  setConfigPath,
1550
2578
  unsetConfigPath,
1551
- writeConfigFile
2579
+ writeConfigFile,
2580
+ writeDefaultConfigFile
1552
2581
  });