@did-btcr2/cli 0.15.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 (71) hide show
  1. package/README.md +47 -6
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/cjs/index.js +563 -117
  4. package/dist/esm/src/cli.js +7 -4
  5. package/dist/esm/src/cli.js.map +1 -1
  6. package/dist/esm/src/commands/config.js +11 -15
  7. package/dist/esm/src/commands/config.js.map +1 -1
  8. package/dist/esm/src/commands/create.js +4 -1
  9. package/dist/esm/src/commands/create.js.map +1 -1
  10. package/dist/esm/src/commands/deactivate.js +3 -1
  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 +1 -1
  19. package/dist/esm/src/commands/profile.js.map +1 -1
  20. package/dist/esm/src/commands/update.js +3 -1
  21. package/dist/esm/src/commands/update.js.map +1 -1
  22. package/dist/esm/src/config.js +85 -28
  23. package/dist/esm/src/config.js.map +1 -1
  24. package/dist/esm/src/keystore/file-key-store.js +340 -32
  25. package/dist/esm/src/keystore/file-key-store.js.map +1 -1
  26. package/dist/esm/src/keystore/passphrase.js +40 -10
  27. package/dist/esm/src/keystore/passphrase.js.map +1 -1
  28. package/dist/esm/src/keystore/paths.js +6 -18
  29. package/dist/esm/src/keystore/paths.js.map +1 -1
  30. package/dist/esm/src/paths.js +59 -0
  31. package/dist/esm/src/paths.js.map +1 -0
  32. package/dist/esm/src/types.js.map +1 -1
  33. package/dist/types/src/cli.d.ts.map +1 -1
  34. package/dist/types/src/commands/config.d.ts.map +1 -1
  35. package/dist/types/src/commands/create.d.ts.map +1 -1
  36. package/dist/types/src/commands/deactivate.d.ts.map +1 -1
  37. package/dist/types/src/commands/index.d.ts +2 -0
  38. package/dist/types/src/commands/index.d.ts.map +1 -1
  39. package/dist/types/src/commands/init.d.ts +13 -0
  40. package/dist/types/src/commands/init.d.ts.map +1 -0
  41. package/dist/types/src/commands/keystore.d.ts +10 -0
  42. package/dist/types/src/commands/keystore.d.ts.map +1 -0
  43. package/dist/types/src/commands/update.d.ts.map +1 -1
  44. package/dist/types/src/config.d.ts +38 -14
  45. package/dist/types/src/config.d.ts.map +1 -1
  46. package/dist/types/src/keystore/file-key-store.d.ts +86 -10
  47. package/dist/types/src/keystore/file-key-store.d.ts.map +1 -1
  48. package/dist/types/src/keystore/passphrase.d.ts +16 -1
  49. package/dist/types/src/keystore/passphrase.d.ts.map +1 -1
  50. package/dist/types/src/keystore/paths.d.ts +6 -10
  51. package/dist/types/src/keystore/paths.d.ts.map +1 -1
  52. package/dist/types/src/paths.d.ts +54 -0
  53. package/dist/types/src/paths.d.ts.map +1 -0
  54. package/dist/types/src/types.d.ts +38 -0
  55. package/dist/types/src/types.d.ts.map +1 -1
  56. package/package.json +3 -3
  57. package/src/cli.ts +8 -3
  58. package/src/commands/config.ts +12 -14
  59. package/src/commands/create.ts +4 -1
  60. package/src/commands/deactivate.ts +3 -1
  61. package/src/commands/index.ts +2 -0
  62. package/src/commands/init.ts +74 -0
  63. package/src/commands/keystore.ts +98 -0
  64. package/src/commands/profile.ts +1 -1
  65. package/src/commands/update.ts +3 -1
  66. package/src/config.ts +93 -32
  67. package/src/keystore/file-key-store.ts +455 -43
  68. package/src/keystore/passphrase.ts +48 -8
  69. package/src/keystore/paths.ts +6 -19
  70. package/src/paths.ts +79 -0
  71. package/src/types.ts +13 -1
package/dist/cjs/index.js CHANGED
@@ -28,6 +28,7 @@ __export(index_exports, {
28
28
  REDACTED: () => REDACTED,
29
29
  SUPPORTED_NETWORKS: () => SUPPORTED_NETWORKS,
30
30
  VERSION: () => VERSION,
31
+ assertKeystoreAllowedForNetwork: () => assertKeystoreAllowedForNetwork,
31
32
  blankToUndef: () => blankToUndef,
32
33
  defaultApiFactory: () => defaultApiFactory,
33
34
  defaultConfigPath: () => defaultConfigPath,
@@ -47,7 +48,9 @@ __export(index_exports, {
47
48
  registerConfigCommand: () => registerConfigCommand,
48
49
  registerCreateCommand: () => registerCreateCommand,
49
50
  registerDeactivateCommand: () => registerDeactivateCommand,
51
+ registerInitCommand: () => registerInitCommand,
50
52
  registerKeyCommand: () => registerKeyCommand,
53
+ registerKeystoreCommand: () => registerKeystoreCommand,
51
54
  registerProfileCommand: () => registerProfileCommand,
52
55
  registerResolveCommand: () => registerResolveCommand,
53
56
  registerUpdateCommand: () => registerUpdateCommand,
@@ -57,6 +60,7 @@ __export(index_exports, {
57
60
  resolveDefaultNetwork: () => resolveDefaultNetwork,
58
61
  resolveEffectiveConfig: () => resolveEffectiveConfig,
59
62
  resolveKeystorePath: () => resolveKeystorePath,
63
+ resolveKeystoreProtection: () => resolveKeystoreProtection,
60
64
  resolveOutputFormat: () => resolveOutputFormat,
61
65
  resolveSecretRef: () => resolveSecretRef,
62
66
  resolveSigningKeyRef: () => resolveSigningKeyRef,
@@ -64,7 +68,8 @@ __export(index_exports, {
64
68
  scrubUrlUserinfo: () => scrubUrlUserinfo,
65
69
  setConfigPath: () => setConfigPath,
66
70
  unsetConfigPath: () => unsetConfigPath,
67
- writeConfigFile: () => writeConfigFile
71
+ writeConfigFile: () => writeConfigFile,
72
+ writeDefaultConfigFile: () => writeDefaultConfigFile
68
73
  });
69
74
  module.exports = __toCommonJS(index_exports);
70
75
 
@@ -76,14 +81,13 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
76
81
  var import_common3 = require("@did-btcr2/common");
77
82
  var import_commander = require("commander");
78
83
 
79
- // src/commands/create.ts
80
- var import_utils2 = require("@noble/hashes/utils.js");
84
+ // src/commands/init.ts
85
+ var import_node_fs6 = require("fs");
81
86
 
82
87
  // src/config.ts
83
88
  var import_api = require("@did-btcr2/api");
84
89
  var import_method = require("@did-btcr2/method");
85
90
  var import_node_fs5 = require("fs");
86
- var import_node_os2 = require("os");
87
91
  var import_node_path4 = require("path");
88
92
 
89
93
  // src/error.ts
@@ -163,6 +167,7 @@ var import_key_manager = require("@did-btcr2/key-manager");
163
167
  // src/keystore/file-key-store.ts
164
168
  var import_node_fs3 = require("fs");
165
169
  var import_node_path3 = require("path");
170
+ var import_utils2 = require("@noble/hashes/utils.js");
166
171
  var import_base2 = require("@scure/base");
167
172
 
168
173
  // src/keystore/envelope.ts
@@ -333,7 +338,7 @@ function withFileLock(lockPath, fn, options = {}) {
333
338
  }
334
339
  }
335
340
 
336
- // src/keystore/paths.ts
341
+ // src/paths.ts
337
342
  var import_node_os = require("os");
338
343
  var import_node_path2 = require("path");
339
344
 
@@ -351,14 +356,39 @@ function blankToUndef(value) {
351
356
  return value.trim() === "" ? void 0 : value;
352
357
  }
353
358
 
354
- // src/keystore/paths.ts
355
- function defaultKeystorePath() {
356
- const base = blankToUndef(process.env.XDG_DATA_HOME) ?? blankToUndef(process.env.LOCALAPPDATA) ?? (0, import_node_path2.join)((0, import_node_os.homedir)(), ".local", "share");
357
- return (0, import_node_path2.join)(base, "btcr2", "keystore.json");
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);
358
378
  }
359
379
 
360
380
  // src/keystore/file-key-store.ts
361
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
+ }
362
392
  var FileKeyStore = class {
363
393
  #path;
364
394
  #lockPath;
@@ -367,12 +397,15 @@ var FileKeyStore = class {
367
397
  #argonParams;
368
398
  #cache = /* @__PURE__ */ new Map();
369
399
  #active;
400
+ #protection;
401
+ #verifier;
370
402
  constructor(options) {
371
403
  this.#path = options.path ?? defaultKeystorePath();
372
404
  this.#lockPath = `${this.#path}.lock`;
373
405
  this.#lockOptions = options.lock ?? {};
374
406
  this.#getPassphrase = options.getPassphrase;
375
407
  this.#argonParams = options.argonParams ?? DEFAULT_ARGON_PARAMS;
408
+ this.#protection = options.protection ?? "passphrase";
376
409
  ensureDir((0, import_node_path3.dirname)(this.#path), 448);
377
410
  this.#loadFromDisk();
378
411
  }
@@ -396,7 +429,26 @@ var FileKeyStore = class {
396
429
  { version: parsed.v }
397
430
  );
398
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;
399
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
+ }
400
452
  for (const [id, stored] of Object.entries(parsed.keys ?? {})) {
401
453
  let publicKey;
402
454
  try {
@@ -416,24 +468,67 @@ var FileKeyStore = class {
416
468
  { path: this.#path, keyId: id }
417
469
  );
418
470
  }
419
- this.#cache.set(id, {
420
- publicKey,
421
- ...stored.tags && { tags: stored.tags },
422
- ...stored.secret && { secret: stored.secret }
423
- });
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);
424
493
  }
425
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
+ );
513
+ }
514
+ return secret;
515
+ }
426
516
  #flush() {
427
517
  const keys = {};
428
518
  for (const [id, entry] of this.#cache) {
429
- keys[id] = {
430
- publicKey: import_base2.base64urlnopad.encode(entry.publicKey),
431
- ...entry.tags && { tags: entry.tags },
432
- ...entry.secret && { secret: entry.secret }
433
- };
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;
434
527
  }
435
528
  const file = {
436
529
  v: KEYSTORE_VERSION,
530
+ protection: this.#protection,
531
+ ...this.#verifier && { verifier: this.#verifier },
437
532
  ...this.#active && { active: this.#active },
438
533
  keys
439
534
  };
@@ -445,7 +540,8 @@ var FileKeyStore = class {
445
540
  * mutation applies on top of whatever other processes have written. Secrets
446
541
  * already decrypted this session are carried over for entries whose sealed
447
542
  * envelope is byte-identical on disk, so a mid-session write does not force a
448
- * 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.)
449
545
  */
450
546
  #reload() {
451
547
  const carried = /* @__PURE__ */ new Map();
@@ -476,6 +572,53 @@ var FileKeyStore = class {
476
572
  this.#flush();
477
573
  }, this.#lockOptions);
478
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
+ }
479
622
  get(id) {
480
623
  const entry = this.#cache.get(id);
481
624
  if (!entry) return void 0;
@@ -483,13 +626,20 @@ var FileKeyStore = class {
483
626
  publicKey: entry.publicKey,
484
627
  ...entry.tags && { tags: entry.tags }
485
628
  };
486
- 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) {
487
637
  const sealed = entry.secret;
488
638
  Object.defineProperty(result, "secretKey", {
489
639
  configurable: true,
490
640
  enumerable: false,
491
641
  get: () => {
492
- entry.decrypted ??= decryptSecret(sealed, this.#getPassphrase());
642
+ entry.decrypted ??= decryptSecret(sealed, this.#openPassphrase());
493
643
  return entry.decrypted;
494
644
  }
495
645
  });
@@ -500,12 +650,45 @@ var FileKeyStore = class {
500
650
  return this.#cache.has(id);
501
651
  }
502
652
  set(id, value) {
503
- 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
+ }
504
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
+ }
505
688
  this.#cache.set(id, {
506
689
  publicKey: value.publicKey,
507
690
  ...value.tags && { tags: value.tags },
508
- ...secret && { secret },
691
+ ...sealed && { secret: sealed },
509
692
  ...value.secretKey && { decrypted: value.secretKey }
510
693
  });
511
694
  });
@@ -568,7 +751,112 @@ var FileKeyStore = class {
568
751
  this.#active = id;
569
752
  });
570
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
+ }
571
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
+ }
572
860
 
573
861
  // src/keystore/file-backed-key-manager.ts
574
862
  var FileBackedKeyManager = class {
@@ -629,10 +917,12 @@ var FileBackedKeyManager = class {
629
917
  var import_node_fs4 = require("fs");
630
918
  var ENV_KEYSTORE_PASSPHRASE = "BTCR2_KEYSTORE_PASSPHRASE";
631
919
  function acquirePassphrase(options = {}) {
632
- const fromEnv = process.env[ENV_KEYSTORE_PASSPHRASE];
633
- if (fromEnv) return assertNonEmpty(fromEnv.replace(/\r?\n$/, ""));
634
- if (options.passphraseFile) {
635
- 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
+ }
636
926
  }
637
927
  if (!process.stdin.isTTY) {
638
928
  throw new KeyStoreError(
@@ -655,6 +945,12 @@ function assertNonEmpty(passphrase) {
655
945
  }
656
946
  return passphrase;
657
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
+ }
658
954
  function promptHidden(label) {
659
955
  process.stderr.write(label);
660
956
  const stdin = process.stdin;
@@ -669,7 +965,10 @@ function promptHidden(label) {
669
965
  read = (0, import_node_fs4.readSync)(stdin.fd, byte, 0, 1, null);
670
966
  } catch (error) {
671
967
  const code = error.code;
672
- if (code === "EAGAIN") continue;
968
+ if (code === "EAGAIN") {
969
+ Atomics.wait(IDLE_WAIT, 0, 0, IDLE_POLL_MS);
970
+ continue;
971
+ }
673
972
  if (code === "EOF") break;
674
973
  throw error;
675
974
  }
@@ -680,7 +979,7 @@ function promptHidden(label) {
680
979
  throw new KeyStoreError("Passphrase entry aborted.", "PASSPHRASE_REQUIRED_ERROR");
681
980
  }
682
981
  if (ch === 127 || ch === 8) {
683
- bytes.pop();
982
+ dropLastUtf8Char(bytes);
684
983
  continue;
685
984
  }
686
985
  bytes.push(ch);
@@ -702,6 +1001,16 @@ function writeConfigFile(path, mutate) {
702
1001
  writeFileAtomic(path, `${JSON.stringify(raw, null, 2)}
703
1002
  `, 384);
704
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
+ }
705
1014
  function getConfigPath(config, path) {
706
1015
  return path.split(".").reduce(
707
1016
  (node, key) => node?.[key],
@@ -760,10 +1069,6 @@ function readEnvOverrides() {
760
1069
  casRpcUrl: env(ENV_VARS.CAS_RPC_URL)
761
1070
  };
762
1071
  }
763
- function defaultConfigPath() {
764
- const base = blankToUndef(process.env.XDG_CONFIG_HOME) ?? blankToUndef(process.env.APPDATA) ?? (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".config");
765
- return (0, import_node_path4.join)(base, "btcr2", "config.json");
766
- }
767
1072
  function parseConfigFileRaw(path) {
768
1073
  let content;
769
1074
  try {
@@ -822,7 +1127,7 @@ function resolveActiveProfile(file, overrides) {
822
1127
  return { name, network };
823
1128
  }
824
1129
  function resolveDefaultNetwork(overrides) {
825
- const configPath = overrides?.config ?? defaultConfigPath();
1130
+ const configPath = overrides?.config ?? defaultConfigPath(overrides);
826
1131
  const file = readConfigFile(configPath);
827
1132
  const explicit = file?.defaults?.network;
828
1133
  if (explicit && SUPPORTED_NETWORKS.includes(explicit)) return explicit;
@@ -833,7 +1138,7 @@ function resolveDefaultNetwork(overrides) {
833
1138
  function profileNetworkMismatch(network, overrides) {
834
1139
  let file;
835
1140
  try {
836
- file = readConfigFile(overrides?.config ?? defaultConfigPath());
1141
+ file = readConfigFile(overrides?.config ?? defaultConfigPath(overrides));
837
1142
  } catch {
838
1143
  return void 0;
839
1144
  }
@@ -847,7 +1152,7 @@ function resolveOutputFormat(options) {
847
1152
  process.env.BTCR2_OUTPUT || void 0
848
1153
  ];
849
1154
  try {
850
- candidates.push(readConfigFile(options.config ?? defaultConfigPath())?.defaults?.output);
1155
+ candidates.push(readConfigFile(options.config ?? defaultConfigPath(options))?.defaults?.output);
851
1156
  } catch {
852
1157
  }
853
1158
  for (const candidate of candidates) {
@@ -873,7 +1178,7 @@ function resolveRpcUnit(overrides, env, fileOverrides) {
873
1178
  }
874
1179
  function resolveConnectionConfig(network, overrides) {
875
1180
  if (!network) return {};
876
- const configPath = overrides?.config ?? defaultConfigPath();
1181
+ const configPath = overrides?.config ?? defaultConfigPath(overrides);
877
1182
  const file = readConfigFile(configPath);
878
1183
  const { name: activeProfile } = resolveActiveProfile(file, overrides);
879
1184
  const profileName = activeProfile ?? network;
@@ -985,7 +1290,7 @@ function readRpcPassFile() {
985
1290
  return readSecretFile(path, `file named by ${ENV_RPC_PASS_FILE}`);
986
1291
  }
987
1292
  function resolveBroadcastOptions(network, overrides, flags) {
988
- const file = readConfigFile(overrides?.config ?? defaultConfigPath());
1293
+ const file = readConfigFile(overrides?.config ?? defaultConfigPath(overrides));
989
1294
  const { name: activeProfile } = resolveActiveProfile(file, overrides);
990
1295
  const profileBtc = file?.profiles?.[activeProfile ?? network]?.btc;
991
1296
  const options = {};
@@ -1007,7 +1312,7 @@ function parseFeeRate(raw) {
1007
1312
  return rate;
1008
1313
  }
1009
1314
  function resolveEffectiveConfig(network, overrides) {
1010
- const file = readConfigFile(overrides?.config ?? defaultConfigPath());
1315
+ const file = readConfigFile(overrides?.config ?? defaultConfigPath(overrides));
1011
1316
  const { name: activeProfile } = resolveActiveProfile(file, overrides);
1012
1317
  const profileName = activeProfile ?? network;
1013
1318
  const fileOv = file ? profileToOverrides(file, profileName) : {};
@@ -1096,14 +1401,38 @@ function defaultApiFactory(network, overrides) {
1096
1401
  function buildKeystoreKms(overrides) {
1097
1402
  return new FileBackedKeyManager({
1098
1403
  path: resolveKeystorePath(overrides),
1099
- getPassphrase: () => acquirePassphrase({ passphraseFile: overrides?.passphraseFile })
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 })
1100
1408
  });
1101
1409
  }
1102
- function resolveKeystorePath(overrides) {
1103
- return overrides?.keystore ?? activeProfileIdentity(overrides)?.keystore ?? defaultKeystorePath();
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);
1104
1433
  }
1105
1434
  function activeProfileIdentity(overrides) {
1106
- const file = readConfigFile(overrides?.config ?? defaultConfigPath());
1435
+ const file = readConfigFile(overrides?.config ?? defaultConfigPath(overrides));
1107
1436
  const { name } = resolveActiveProfile(file, overrides);
1108
1437
  return name ? file?.profiles?.[name]?.identity : void 0;
1109
1438
  }
@@ -1128,6 +1457,85 @@ function deriveNetwork(did) {
1128
1457
  return network;
1129
1458
  }
1130
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
+
1131
1539
  // src/keystore/resolve-key-ref.ts
1132
1540
  function fingerprintOf(id) {
1133
1541
  return /^urn:kms:secp256k1:([0-9a-f]{32})$/.exec(id)?.[1];
@@ -1166,40 +1574,6 @@ function resolveKeyRef(kms, ref) {
1166
1574
  throw new CLIError(`No key matches reference "${ref}".`, "KEY_NOT_FOUND_ERROR", { ref });
1167
1575
  }
1168
1576
 
1169
- // src/output.ts
1170
- var REDACTED = "********";
1171
- var SECRET_KEY = /(pass|secret|token|auth|api[-_]?key|credential|bearer)/i;
1172
- function isSecretKey(key) {
1173
- return SECRET_KEY.test(key);
1174
- }
1175
- function scrubUrlUserinfo(value) {
1176
- if (value === void 0) return void 0;
1177
- return value.replace(/^([a-z][a-z0-9+.-]*:\/\/[^/@:]+):[^/@]*@/i, `$1:${REDACTED}@`);
1178
- }
1179
- function redactSecrets(value, keyName) {
1180
- if (Array.isArray(value)) {
1181
- return value.map((item) => redactSecrets(item));
1182
- }
1183
- if (value !== null && typeof value === "object") {
1184
- const out = {};
1185
- for (const [key, val] of Object.entries(value)) {
1186
- out[key] = redactSecrets(val, key);
1187
- }
1188
- return out;
1189
- }
1190
- if (keyName !== void 0 && isSecretKey(keyName)) {
1191
- return value === void 0 ? void 0 : REDACTED;
1192
- }
1193
- return typeof value === "string" ? scrubUrlUserinfo(value) : value;
1194
- }
1195
- function formatResult(result, options) {
1196
- if (options.output === "json") {
1197
- return JSON.stringify(result, null, 2);
1198
- }
1199
- const { data } = result;
1200
- return typeof data === "string" ? data : JSON.stringify(data, null, 2);
1201
- }
1202
-
1203
1577
  // src/commands/create.ts
1204
1578
  var EXPECTED_BYTES = {
1205
1579
  k: { length: 33, label: "secp256k1 compressed public key (33 bytes)" },
@@ -1268,14 +1642,15 @@ function registerCreateCommand(program, factory, keystoreFactory, globals) {
1268
1642
  const publicKey2 = api2.kms.getPublicKey(keyId2);
1269
1643
  const did2 = api2.createDid("deterministic", publicKey2, { network });
1270
1644
  print(
1271
- { 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) },
1272
1646
  `Using stored key ${keyId2}.`
1273
1647
  );
1274
1648
  return;
1275
1649
  }
1650
+ assertKeystoreAllowedForNetwork(network, overrides);
1276
1651
  const api = keystoreFactory(void 0, overrides);
1277
1652
  const { did, keyId } = api.generateDid({ network, setActive: true });
1278
- const publicKey = (0, import_utils2.bytesToHex)(api.kms.getPublicKey(keyId));
1653
+ const publicKey = (0, import_utils3.bytesToHex)(api.kms.getPublicKey(keyId));
1279
1654
  print(
1280
1655
  { action: "create", data: did, keyId, publicKey },
1281
1656
  `Generated and stored key ${keyId} (now the active key).`
@@ -1284,6 +1659,7 @@ function registerCreateCommand(program, factory, keystoreFactory, globals) {
1284
1659
  }
1285
1660
  function overridesFromGlobals(g) {
1286
1661
  return {
1662
+ home: g.home,
1287
1663
  config: g.config,
1288
1664
  profile: g.profile,
1289
1665
  keystore: g.keystore,
@@ -1305,7 +1681,7 @@ function parseGenesisBytes(hex, type) {
1305
1681
  const expected = EXPECTED_BYTES[type];
1306
1682
  let bytes;
1307
1683
  try {
1308
- bytes = (0, import_utils2.hexToBytes)(hex.trim());
1684
+ bytes = (0, import_utils3.hexToBytes)(hex.trim());
1309
1685
  } catch {
1310
1686
  throw new CLIError(
1311
1687
  `Invalid bytes: not valid hex. Expected ${expected.label}.`,
@@ -1420,6 +1796,7 @@ function registerUpdateCommand(program, factory, globals) {
1420
1796
  );
1421
1797
  }
1422
1798
  const network = deriveNetwork(did);
1799
+ assertKeystoreAllowedForNetwork(network, globals());
1423
1800
  const api = factory(network, globals());
1424
1801
  const keyId = resolveKeyRef(api.kms.kms, resolveSigningKeyRef(globals()));
1425
1802
  const signer = new import_key_manager2.KeyManagerSigner(api.kms.kms, keyId);
@@ -1517,6 +1894,7 @@ function registerDeactivateCommand(program, factory, globals) {
1517
1894
  );
1518
1895
  }
1519
1896
  const network = deriveNetwork(did);
1897
+ assertKeystoreAllowedForNetwork(network, globals());
1520
1898
  const api = factory(network, globals());
1521
1899
  const keyId = resolveKeyRef(api.kms.kms, resolveSigningKeyRef(globals()));
1522
1900
  const signer = new import_key_manager3.KeyManagerSigner(api.kms.kms, keyId);
@@ -1563,8 +1941,8 @@ function parseJsonArg2(flagName) {
1563
1941
 
1564
1942
  // src/commands/key.ts
1565
1943
  var import_keypair = require("@did-btcr2/keypair");
1566
- var import_utils3 = require("@noble/hashes/utils.js");
1567
- var import_node_fs6 = require("fs");
1944
+ var import_utils4 = require("@noble/hashes/utils.js");
1945
+ var import_node_fs7 = require("fs");
1568
1946
  function registerKeyCommand(program, factory, globals) {
1569
1947
  const key = program.command("key").description("Manage keypairs in the encrypted keystore.");
1570
1948
  const print = (result) => console.log(formatResult(result, globals()));
@@ -1573,7 +1951,7 @@ function registerKeyCommand(program, factory, globals) {
1573
1951
  assertNameAvailable(api.kms.kms, options.name);
1574
1952
  const setActive = options.setActive ?? false;
1575
1953
  const id = api.kms.generateKey({ ...options.name && { tags: { name: options.name } }, setActive });
1576
- 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 } });
1577
1955
  });
1578
1956
  key.command("list").alias("ls").description("List stored keys.").action(() => {
1579
1957
  const kms = factory(void 0, globals()).kms.kms;
@@ -1593,7 +1971,7 @@ function registerKeyCommand(program, factory, globals) {
1593
1971
  const kms = factory(void 0, globals()).kms.kms;
1594
1972
  const id = resolveKeyRef(kms, ref);
1595
1973
  const entry = kms.getEntry(id);
1596
- 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 } } });
1597
1975
  });
1598
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) => {
1599
1977
  if (Boolean(options.secretFile) === Boolean(options.public)) {
@@ -1604,13 +1982,13 @@ function registerKeyCommand(program, factory, globals) {
1604
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") });
1605
1983
  const setActive = options.setActive ?? false;
1606
1984
  const id = api.kms.import(keyPair, { ...options.name && { tags: { name: options.name } }, setActive });
1607
- 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 } });
1608
1986
  });
1609
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) => {
1610
1988
  const api = factory(void 0, globals());
1611
1989
  const id = resolveKeyRef(api.kms.kms, ref);
1612
1990
  if (!options.secret) {
1613
- 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)) } });
1614
1992
  return;
1615
1993
  }
1616
1994
  if (!options.out) {
@@ -1621,7 +1999,7 @@ function registerKeyCommand(program, factory, globals) {
1621
1999
  throw new CLIError(`Key ${id} is watch-only and has no secret to export.`, "INVALID_ARGUMENT_ERROR", { keyId: id });
1622
2000
  }
1623
2001
  process.stderr.write("warning: writing an unencrypted secret key to disk. Protect this file and delete it when done.\n");
1624
- writeSecretFile(options.out, (0, import_utils3.bytesToHex)(keyPair.secretKey.bytes));
2002
+ writeSecretFile(options.out, (0, import_utils4.bytesToHex)(keyPair.secretKey.bytes));
1625
2003
  print({ action: "key-export", data: { keyId: id, secretWrittenTo: options.out } });
1626
2004
  });
1627
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) => {
@@ -1646,7 +2024,7 @@ function assertNameAvailable(kms, name) {
1646
2024
  function parseHex(hex, expectedBytes, label) {
1647
2025
  let bytes;
1648
2026
  try {
1649
- bytes = (0, import_utils3.hexToBytes)(hex.trim());
2027
+ bytes = (0, import_utils4.hexToBytes)(hex.trim());
1650
2028
  } catch {
1651
2029
  throw new CLIError(`Invalid hex for ${label}.`, "INVALID_ARGUMENT_ERROR", { label });
1652
2030
  }
@@ -1662,7 +2040,7 @@ function parseHex(hex, expectedBytes, label) {
1662
2040
  function readHexFile(path, expectedBytes, label) {
1663
2041
  let content;
1664
2042
  try {
1665
- content = (0, import_node_fs6.readFileSync)(path, "utf-8");
2043
+ content = (0, import_node_fs7.readFileSync)(path, "utf-8");
1666
2044
  } catch {
1667
2045
  throw new CLIError(`Cannot read ${label} at ${path}.`, "INVALID_ARGUMENT_ERROR", { label, path });
1668
2046
  }
@@ -1671,7 +2049,7 @@ function readHexFile(path, expectedBytes, label) {
1671
2049
  function writeSecretFile(path, contents) {
1672
2050
  let fd;
1673
2051
  try {
1674
- fd = (0, import_node_fs6.openSync)(path, "wx", 384);
2052
+ fd = (0, import_node_fs7.openSync)(path, "wx", 384);
1675
2053
  } catch (error) {
1676
2054
  if (error.code === "EEXIST") {
1677
2055
  throw new CLIError(`Refusing to overwrite existing file ${path}. Choose a new --out path.`, "INVALID_ARGUMENT_ERROR", { path });
@@ -1679,15 +2057,79 @@ function writeSecretFile(path, contents) {
1679
2057
  throw error;
1680
2058
  }
1681
2059
  try {
1682
- (0, import_node_fs6.writeFileSync)(fd, contents);
2060
+ (0, import_node_fs7.writeFileSync)(fd, contents);
1683
2061
  } finally {
1684
- (0, import_node_fs6.closeSync)(fd);
2062
+ (0, import_node_fs7.closeSync)(fd);
1685
2063
  }
1686
2064
  }
1687
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
+
1688
2131
  // src/commands/config.ts
1689
- var import_node_fs7 = require("fs");
1690
- var import_node_path5 = require("path");
2132
+ var import_node_fs9 = require("fs");
1691
2133
 
1692
2134
  // src/config-schema.ts
1693
2135
  var CONFIG_SCHEMA = {
@@ -1812,21 +2254,14 @@ function walk(obj, prefix, issues) {
1812
2254
  // src/commands/config.ts
1813
2255
  function registerConfigCommand(program, globals) {
1814
2256
  const config = program.command("config").description("Read and write CLI configuration.");
1815
- const path = () => globals().config ?? defaultConfigPath();
2257
+ const path = () => globals().config ?? defaultConfigPath(globals());
1816
2258
  const print = (result) => console.log(formatResult(result, globals()));
1817
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) => {
1818
2260
  const p = path();
1819
- if ((0, import_node_fs7.existsSync)(p) && !options.force) {
2261
+ if ((0, import_node_fs9.existsSync)(p) && !options.force) {
1820
2262
  throw new CLIError(`Config already exists at ${p}. Use --force to overwrite.`, "INVALID_ARGUMENT_ERROR", { path: p });
1821
2263
  }
1822
- const scaffold = {
1823
- schemaVersion: CONFIG_SCHEMA_VERSION,
1824
- defaults: { output: "text" },
1825
- profiles: Object.fromEntries(SUPPORTED_NETWORKS.map((n) => [n, {}]))
1826
- };
1827
- ensureDir((0, import_node_path5.dirname)(p), 448);
1828
- writeFileAtomic(p, `${JSON.stringify(scaffold, null, 2)}
1829
- `, 384);
2264
+ writeDefaultConfigFile(p);
1830
2265
  print({ action: "config-init", data: { path: p } });
1831
2266
  });
1832
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) => {
@@ -1871,10 +2306,14 @@ function registerConfigCommand(program, globals) {
1871
2306
  }
1872
2307
  print({ action: "config-effective", data });
1873
2308
  });
1874
- config.command("path").description("Print the resolved config-file and keystore paths.").action(() => {
2309
+ config.command("path").description("Print the resolved home directory, config-file, and keystore paths.").action(() => {
2310
+ const g = globals();
1875
2311
  const data = {
1876
- config: globals().config ?? defaultConfigPath(),
1877
- keystore: globals().keystore ?? defaultKeystorePath()
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 })
1878
2317
  };
1879
2318
  print({ action: "config-path", data });
1880
2319
  });
@@ -1927,7 +2366,7 @@ function isStringScalarPath(dotted) {
1927
2366
  // src/commands/profile.ts
1928
2367
  function registerProfileCommand(program, globals) {
1929
2368
  const profile = program.command("profile").description("Manage configuration profiles.");
1930
- const path = () => globals().config ?? defaultConfigPath();
2369
+ const path = () => globals().config ?? defaultConfigPath(globals());
1931
2370
  const print = (result) => console.log(formatResult(result, globals()));
1932
2371
  profile.command("add <name>").description("Add an empty profile.").action((name) => {
1933
2372
  writeConfigFile(path(), (raw) => {
@@ -2000,18 +2439,18 @@ function completionScript(shell) {
2000
2439
  }
2001
2440
 
2002
2441
  // src/version.ts
2003
- var import_node_fs8 = require("fs");
2004
- var import_node_path6 = require("path");
2442
+ var import_node_fs10 = require("fs");
2443
+ var import_node_path5 = require("path");
2005
2444
  var import_node_url = require("url");
2006
2445
  function readVersion() {
2007
- 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));
2008
2447
  for (let i = 0; i < 5; i++) {
2009
2448
  try {
2010
- 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"));
2011
2450
  if (pkg.name === "@did-btcr2/cli") return pkg.version;
2012
2451
  } catch {
2013
2452
  }
2014
- dir = (0, import_node_path6.dirname)(dir);
2453
+ dir = (0, import_node_path5.dirname)(dir);
2015
2454
  }
2016
2455
  return "0.0.0";
2017
2456
  }
@@ -2035,17 +2474,19 @@ var DidBtcr2Cli = class {
2035
2474
  * {@link keystoreApiFactory}.
2036
2475
  */
2037
2476
  constructor(factory = defaultApiFactory, keystoreFactory = keystoreApiFactory) {
2038
- 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("-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("--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: $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");
2039
2478
  const globals = () => this.program.opts();
2040
2479
  this.program.hook("preAction", () => {
2041
2480
  const opts = this.program.opts();
2042
- opts.output = resolveOutputFormat({ output: opts.output, config: opts.config });
2481
+ opts.output = resolveOutputFormat({ output: opts.output, config: opts.config, home: opts.home });
2043
2482
  });
2483
+ registerInitCommand(this.program, globals);
2044
2484
  registerCreateCommand(this.program, factory, keystoreFactory, globals);
2045
2485
  registerResolveCommand(this.program, factory, globals);
2046
2486
  registerUpdateCommand(this.program, keystoreFactory, globals);
2047
2487
  registerDeactivateCommand(this.program, keystoreFactory, globals);
2048
2488
  registerKeyCommand(this.program, keystoreFactory, globals);
2489
+ registerKeystoreCommand(this.program, globals);
2049
2490
  registerConfigCommand(this.program, globals);
2050
2491
  registerProfileCommand(this.program, globals);
2051
2492
  registerCompletionCommand(this.program, globals);
@@ -2095,6 +2536,7 @@ function handleError(error, verbose) {
2095
2536
  REDACTED,
2096
2537
  SUPPORTED_NETWORKS,
2097
2538
  VERSION,
2539
+ assertKeystoreAllowedForNetwork,
2098
2540
  blankToUndef,
2099
2541
  defaultApiFactory,
2100
2542
  defaultConfigPath,
@@ -2114,7 +2556,9 @@ function handleError(error, verbose) {
2114
2556
  registerConfigCommand,
2115
2557
  registerCreateCommand,
2116
2558
  registerDeactivateCommand,
2559
+ registerInitCommand,
2117
2560
  registerKeyCommand,
2561
+ registerKeystoreCommand,
2118
2562
  registerProfileCommand,
2119
2563
  registerResolveCommand,
2120
2564
  registerUpdateCommand,
@@ -2124,6 +2568,7 @@ function handleError(error, verbose) {
2124
2568
  resolveDefaultNetwork,
2125
2569
  resolveEffectiveConfig,
2126
2570
  resolveKeystorePath,
2571
+ resolveKeystoreProtection,
2127
2572
  resolveOutputFormat,
2128
2573
  resolveSecretRef,
2129
2574
  resolveSigningKeyRef,
@@ -2131,5 +2576,6 @@ function handleError(error, verbose) {
2131
2576
  scrubUrlUserinfo,
2132
2577
  setConfigPath,
2133
2578
  unsetConfigPath,
2134
- writeConfigFile
2579
+ writeConfigFile,
2580
+ writeDefaultConfigFile
2135
2581
  });