@did-btcr2/cli 0.14.0 → 0.15.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 (52) hide show
  1. package/README.md +61 -13
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/cjs/index.js +637 -54
  4. package/dist/esm/src/cli.js +23 -2
  5. package/dist/esm/src/cli.js.map +1 -1
  6. package/dist/esm/src/commands/config.js +124 -8
  7. package/dist/esm/src/commands/config.js.map +1 -1
  8. package/dist/esm/src/commands/create.js +10 -1
  9. package/dist/esm/src/commands/create.js.map +1 -1
  10. package/dist/esm/src/commands/deactivate.js +14 -2
  11. package/dist/esm/src/commands/deactivate.js.map +1 -1
  12. package/dist/esm/src/commands/profile.js +5 -3
  13. package/dist/esm/src/commands/profile.js.map +1 -1
  14. package/dist/esm/src/commands/update.js +14 -2
  15. package/dist/esm/src/commands/update.js.map +1 -1
  16. package/dist/esm/src/config-schema.js +149 -0
  17. package/dist/esm/src/config-schema.js.map +1 -0
  18. package/dist/esm/src/config.js +506 -39
  19. package/dist/esm/src/config.js.map +1 -1
  20. package/dist/esm/src/keystore/paths.js +3 -2
  21. package/dist/esm/src/keystore/paths.js.map +1 -1
  22. package/dist/esm/src/output.js +49 -0
  23. package/dist/esm/src/output.js.map +1 -1
  24. package/dist/esm/src/types.js +11 -0
  25. package/dist/esm/src/types.js.map +1 -1
  26. package/dist/types/src/cli.d.ts.map +1 -1
  27. package/dist/types/src/commands/config.d.ts.map +1 -1
  28. package/dist/types/src/commands/create.d.ts.map +1 -1
  29. package/dist/types/src/commands/deactivate.d.ts.map +1 -1
  30. package/dist/types/src/commands/profile.d.ts.map +1 -1
  31. package/dist/types/src/commands/update.d.ts.map +1 -1
  32. package/dist/types/src/config-schema.d.ts +24 -0
  33. package/dist/types/src/config-schema.d.ts.map +1 -0
  34. package/dist/types/src/config.d.ts +219 -5
  35. package/dist/types/src/config.d.ts.map +1 -1
  36. package/dist/types/src/keystore/paths.d.ts.map +1 -1
  37. package/dist/types/src/output.d.ts +22 -0
  38. package/dist/types/src/output.d.ts.map +1 -1
  39. package/dist/types/src/types.d.ts +32 -0
  40. package/dist/types/src/types.d.ts.map +1 -1
  41. package/package.json +5 -5
  42. package/src/cli.ts +25 -2
  43. package/src/commands/config.ts +135 -8
  44. package/src/commands/create.ts +13 -1
  45. package/src/commands/deactivate.ts +22 -2
  46. package/src/commands/profile.ts +5 -3
  47. package/src/commands/update.ts +22 -2
  48. package/src/config-schema.ts +178 -0
  49. package/src/config.ts +676 -43
  50. package/src/keystore/paths.ts +3 -2
  51. package/src/output.ts +53 -0
  52. package/src/types.ts +22 -0
package/dist/cjs/index.js CHANGED
@@ -23,18 +23,26 @@ __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
+ blankToUndef: () => blankToUndef,
29
32
  defaultApiFactory: () => defaultApiFactory,
30
33
  defaultConfigPath: () => defaultConfigPath,
31
34
  deriveNetwork: () => deriveNetwork,
32
35
  formatResult: () => formatResult,
33
36
  getConfigPath: () => getConfigPath,
37
+ isSecretKey: () => isSecretKey,
34
38
  keystoreApiFactory: () => keystoreApiFactory,
39
+ parseConfigFileRaw: () => parseConfigFileRaw,
40
+ parseHeaderList: () => parseHeaderList,
41
+ profileNetworkMismatch: () => profileNetworkMismatch,
35
42
  profileToOverrides: () => profileToOverrides,
36
43
  readConfigFile: () => readConfigFile,
37
44
  readEnvOverrides: () => readEnvOverrides,
45
+ redactSecrets: () => redactSecrets,
38
46
  registerCompletionCommand: () => registerCompletionCommand,
39
47
  registerConfigCommand: () => registerConfigCommand,
40
48
  registerCreateCommand: () => registerCreateCommand,
@@ -43,7 +51,17 @@ __export(index_exports, {
43
51
  registerProfileCommand: () => registerProfileCommand,
44
52
  registerResolveCommand: () => registerResolveCommand,
45
53
  registerUpdateCommand: () => registerUpdateCommand,
54
+ resolveActiveProfile: () => resolveActiveProfile,
55
+ resolveBroadcastOptions: () => resolveBroadcastOptions,
56
+ resolveConnectionConfig: () => resolveConnectionConfig,
46
57
  resolveDefaultNetwork: () => resolveDefaultNetwork,
58
+ resolveEffectiveConfig: () => resolveEffectiveConfig,
59
+ resolveKeystorePath: () => resolveKeystorePath,
60
+ resolveOutputFormat: () => resolveOutputFormat,
61
+ resolveSecretRef: () => resolveSecretRef,
62
+ resolveSigningKeyRef: () => resolveSigningKeyRef,
63
+ runDoctor: () => runDoctor,
64
+ scrubUrlUserinfo: () => scrubUrlUserinfo,
47
65
  setConfigPath: () => setConfigPath,
48
66
  unsetConfigPath: () => unsetConfigPath,
49
67
  writeConfigFile: () => writeConfigFile
@@ -63,6 +81,7 @@ var import_utils2 = require("@noble/hashes/utils.js");
63
81
 
64
82
  // src/config.ts
65
83
  var import_api = require("@did-btcr2/api");
84
+ var import_method = require("@did-btcr2/method");
66
85
  var import_node_fs5 = require("fs");
67
86
  var import_node_os2 = require("os");
68
87
  var import_node_path4 = require("path");
@@ -317,8 +336,24 @@ function withFileLock(lockPath, fn, options = {}) {
317
336
  // src/keystore/paths.ts
318
337
  var import_node_os = require("os");
319
338
  var import_node_path2 = require("path");
339
+
340
+ // src/types.ts
341
+ var SUPPORTED_NETWORKS = [
342
+ "bitcoin",
343
+ "testnet3",
344
+ "testnet4",
345
+ "signet",
346
+ "mutinynet",
347
+ "regtest"
348
+ ];
349
+ function blankToUndef(value) {
350
+ if (value === void 0) return void 0;
351
+ return value.trim() === "" ? void 0 : value;
352
+ }
353
+
354
+ // src/keystore/paths.ts
320
355
  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");
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");
322
357
  return (0, import_node_path2.join)(base, "btcr2", "keystore.json");
323
358
  }
324
359
 
@@ -657,16 +692,6 @@ function promptHidden(label) {
657
692
  return Buffer.from(bytes).toString("utf-8");
658
693
  }
659
694
 
660
- // src/types.ts
661
- var SUPPORTED_NETWORKS = [
662
- "bitcoin",
663
- "testnet3",
664
- "testnet4",
665
- "signet",
666
- "mutinynet",
667
- "regtest"
668
- ];
669
-
670
695
  // src/config.ts
671
696
  var CONFIG_SCHEMA_VERSION = 1;
672
697
  function writeConfigFile(path, mutate) {
@@ -683,8 +708,15 @@ function getConfigPath(config, path) {
683
708
  config
684
709
  );
685
710
  }
711
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
712
+ function assertSafeKey(key, path) {
713
+ if (UNSAFE_KEYS.has(key)) {
714
+ throw new CLIError(`Illegal config path segment "${key}" in "${path}".`, "INVALID_ARGUMENT_ERROR", { path, key });
715
+ }
716
+ }
686
717
  function setConfigPath(config, path, value) {
687
718
  const keys = path.split(".");
719
+ keys.forEach((key) => assertSafeKey(key, path));
688
720
  const last = keys.pop();
689
721
  if (!last) throw new CLIError("Config path must be non-empty.", "INVALID_ARGUMENT_ERROR");
690
722
  let node = config;
@@ -696,6 +728,7 @@ function setConfigPath(config, path, value) {
696
728
  }
697
729
  function unsetConfigPath(config, path) {
698
730
  const keys = path.split(".");
731
+ keys.forEach((key) => assertSafeKey(key, path));
699
732
  const last = keys.pop();
700
733
  if (!last) return;
701
734
  let node = config;
@@ -711,7 +744,10 @@ var ENV_VARS = {
711
744
  BTC_RPC_USER: "BTCR2_BTC_RPC_USER",
712
745
  BTC_RPC_PASS: "BTCR2_BTC_RPC_PASS",
713
746
  CAS_GATEWAY: "BTCR2_CAS_GATEWAY",
714
- CAS_RPC_URL: "BTCR2_CAS_RPC_URL"
747
+ CAS_RPC_URL: "BTCR2_CAS_RPC_URL",
748
+ BTC_TIMEOUT: "BTCR2_BTC_TIMEOUT",
749
+ CAS_TIMEOUT: "BTCR2_CAS_TIMEOUT",
750
+ FEE_RATE: "BTCR2_FEE_RATE"
715
751
  };
716
752
  function readEnvOverrides() {
717
753
  const env = (key) => process.env[key] || void 0;
@@ -725,15 +761,45 @@ function readEnvOverrides() {
725
761
  };
726
762
  }
727
763
  function defaultConfigPath() {
728
- const base = process.env.XDG_CONFIG_HOME ?? process.env.APPDATA ?? (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".config");
764
+ const base = blankToUndef(process.env.XDG_CONFIG_HOME) ?? blankToUndef(process.env.APPDATA) ?? (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".config");
729
765
  return (0, import_node_path4.join)(base, "btcr2", "config.json");
730
766
  }
731
- function readConfigFile(path) {
767
+ function parseConfigFileRaw(path) {
768
+ let content;
769
+ try {
770
+ content = (0, import_node_fs5.readFileSync)(path, "utf-8");
771
+ } catch (error) {
772
+ if (error.code === "ENOENT") return void 0;
773
+ throw new CLIError(
774
+ `Failed to read config file at ${path}: ${error.message}`,
775
+ "CONFIG_READ_ERROR",
776
+ { path }
777
+ );
778
+ }
732
779
  try {
733
- const content = (0, import_node_fs5.readFileSync)(path, "utf-8");
734
780
  return JSON.parse(content);
735
- } catch {
736
- return void 0;
781
+ } catch (error) {
782
+ throw new CLIError(
783
+ `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.`,
784
+ "CONFIG_PARSE_ERROR",
785
+ { path }
786
+ );
787
+ }
788
+ }
789
+ function readConfigFile(path) {
790
+ const parsed = parseConfigFileRaw(path);
791
+ if (parsed === void 0) return void 0;
792
+ migrateConfigShape(parsed, path);
793
+ return parsed;
794
+ }
795
+ function migrateConfigShape(raw, path) {
796
+ const version = typeof raw.schemaVersion === "number" ? raw.schemaVersion : 0;
797
+ if (version > CONFIG_SCHEMA_VERSION) {
798
+ throw new CLIError(
799
+ `Config file at ${path} has schemaVersion ${version}, but this CLI supports up to ${CONFIG_SCHEMA_VERSION}. Upgrade the btcr2 CLI to read it.`,
800
+ "CONFIG_SCHEMA_VERSION_ERROR",
801
+ { path, fileVersion: version, supported: CONFIG_SCHEMA_VERSION }
802
+ );
737
803
  }
738
804
  }
739
805
  function profileToOverrides(config, profileName) {
@@ -748,58 +814,302 @@ function profileToOverrides(config, profileName) {
748
814
  casRpcUrl: profile.cas?.rpcUrl
749
815
  };
750
816
  }
817
+ function resolveActiveProfile(file, overrides) {
818
+ const name = blankToUndef(overrides?.profile) ?? file?.defaults?.profile;
819
+ if (!name) return { name: void 0, network: void 0 };
820
+ const declared = file?.profiles?.[name]?.network;
821
+ const network = declared && SUPPORTED_NETWORKS.includes(declared) ? declared : SUPPORTED_NETWORKS.includes(name) ? name : void 0;
822
+ return { name, network };
823
+ }
751
824
  function resolveDefaultNetwork(overrides) {
752
825
  const configPath = overrides?.config ?? defaultConfigPath();
753
826
  const file = readConfigFile(configPath);
754
827
  const explicit = file?.defaults?.network;
755
828
  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
- }
829
+ const { network } = resolveActiveProfile(file, overrides);
830
+ if (network) return network;
760
831
  return "regtest";
761
832
  }
833
+ function profileNetworkMismatch(network, overrides) {
834
+ let file;
835
+ try {
836
+ file = readConfigFile(overrides?.config ?? defaultConfigPath());
837
+ } catch {
838
+ return void 0;
839
+ }
840
+ const { name, network: declared } = resolveActiveProfile(file, overrides);
841
+ if (name && declared && declared !== network) return { profile: name, declared };
842
+ return void 0;
843
+ }
844
+ function resolveOutputFormat(options) {
845
+ const candidates = [
846
+ blankToUndef(options.output),
847
+ process.env.BTCR2_OUTPUT || void 0
848
+ ];
849
+ try {
850
+ candidates.push(readConfigFile(options.config ?? defaultConfigPath())?.defaults?.output);
851
+ } catch {
852
+ }
853
+ for (const candidate of candidates) {
854
+ if (candidate === "json" || candidate === "text") return candidate;
855
+ }
856
+ return "text";
857
+ }
858
+ function resolveRpcUnit(overrides, env, fileOverrides) {
859
+ const layers = [
860
+ { src: "flag", url: overrides?.btcRpcUrl, user: overrides?.btcRpcUser, pass: overrides?.btcRpcPass },
861
+ { src: "env", url: env?.btcRpcUrl, user: env?.btcRpcUser, pass: env?.btcRpcPass },
862
+ { src: "file", url: fileOverrides?.btcRpcUrl, user: fileOverrides?.btcRpcUser, pass: fileOverrides?.btcRpcPass }
863
+ ];
864
+ const withUrl = layers.find((l) => blankToUndef(l.url) !== void 0);
865
+ if (withUrl) {
866
+ return { src: withUrl.src, url: blankToUndef(withUrl.url), user: blankToUndef(withUrl.user), pass: blankToUndef(withUrl.pass) };
867
+ }
868
+ const withCreds = layers.find((l) => blankToUndef(l.user) !== void 0 || blankToUndef(l.pass) !== void 0);
869
+ if (withCreds) {
870
+ return { src: withCreds.src, url: void 0, user: blankToUndef(withCreds.user), pass: blankToUndef(withCreds.pass) };
871
+ }
872
+ return void 0;
873
+ }
762
874
  function resolveConnectionConfig(network, overrides) {
763
875
  if (!network) return {};
764
876
  const configPath = overrides?.config ?? defaultConfigPath();
765
877
  const file = readConfigFile(configPath);
766
- const profileName = overrides?.profile ?? file?.defaults?.profile ?? network;
878
+ const { name: activeProfile } = resolveActiveProfile(file, overrides);
879
+ const profileName = activeProfile ?? network;
767
880
  const fileOverrides = file ? profileToOverrides(file, profileName) : {};
768
881
  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
- };
882
+ const pick = (flag, envVal, fileVal) => blankToUndef(flag) ?? blankToUndef(envVal) ?? blankToUndef(fileVal);
883
+ const profileBtc = file?.profiles?.[profileName]?.btc;
884
+ const profileCas = file?.profiles?.[profileName]?.cas;
777
885
  const btc = { network };
778
- if (merged.btcRest) {
779
- btc.rest = { host: merged.btcRest };
780
- }
781
- if (merged.btcRpcUrl) {
886
+ const btcRest = pick(overrides?.btcRest, env.btcRest, fileOverrides.btcRest);
887
+ const restHeaders = mergeHeaders(profileBtc?.headers, parseHeaderList(overrides?.btcRestHeader, "--btc-rest-header"));
888
+ if (btcRest || restHeaders) {
889
+ btc.rest = { ...btcRest ? { host: btcRest } : {}, ...restHeaders ? { headers: restHeaders } : {} };
890
+ }
891
+ const rpcUnit = resolveRpcUnit(overrides, env, fileOverrides);
892
+ const rpcWallet = pick(overrides?.btcRpcWallet, void 0, profileBtc?.wallet);
893
+ const rpcHeaders = mergeHeaders(profileBtc?.rpcHeaders, parseHeaderList(overrides?.btcRpcHeader, "--btc-rpc-header"));
894
+ const networkHasDefaultRpc = import_api.DEFAULT_BITCOIN_NETWORK_CONFIG[network].rpc !== void 0;
895
+ const wantsRpc = rpcUnit !== void 0 || rpcWallet !== void 0 || rpcHeaders !== void 0;
896
+ const hasRpcHost = rpcUnit?.url !== void 0 || networkHasDefaultRpc;
897
+ if (wantsRpc && hasRpcHost) {
898
+ const password = resolveSecretRef(rpcUnit?.pass) ?? readRpcPassFile();
782
899
  btc.rpc = {
783
- host: merged.btcRpcUrl,
784
- username: merged.btcRpcUser,
785
- password: merged.btcRpcPass
900
+ ...rpcUnit?.url !== void 0 ? { host: rpcUnit.url } : {},
901
+ ...rpcUnit?.user !== void 0 ? { username: rpcUnit.user } : {},
902
+ ...password !== void 0 ? { password } : {},
903
+ ...rpcWallet ? { wallet: rpcWallet } : {},
904
+ ...rpcHeaders ? { headers: rpcHeaders } : {}
786
905
  };
787
906
  }
907
+ const btcTimeout = resolveTimeout(overrides?.btcTimeout, process.env[ENV_VARS.BTC_TIMEOUT], profileBtc?.timeoutMs, "--btc-timeout", 1);
908
+ if (btcTimeout !== void 0) btc.timeoutMs = btcTimeout;
909
+ const casGateway = pick(overrides?.casGateway, env.casGateway, fileOverrides.casGateway);
910
+ const casRpcUrl = pick(overrides?.casRpcUrl, env.casRpcUrl, fileOverrides.casRpcUrl);
911
+ const casTimeout = resolveTimeout(overrides?.casTimeout, process.env[ENV_VARS.CAS_TIMEOUT], profileCas?.timeoutMs, "--cas-timeout");
788
912
  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;
913
+ if (casGateway) cas.gateway = casGateway;
914
+ if (casRpcUrl) cas.rpcUrl = casRpcUrl;
915
+ if (casTimeout !== void 0) {
916
+ cas.timeoutMs = casTimeout;
917
+ if (!casGateway && !casRpcUrl) cas.gateway = import_api.DEFAULT_CAS_GATEWAY;
918
+ }
919
+ const hasCas = casGateway || casRpcUrl || casTimeout !== void 0;
792
920
  return { btc, ...hasCas && { cas } };
793
921
  }
922
+ function resolveTimeout(flag, envVal, fileVal, flagName = "timeout", min = 0) {
923
+ const raw = blankToUndef(flag) ?? blankToUndef(envVal) ?? (typeof fileVal === "number" ? String(fileVal) : void 0);
924
+ if (raw === void 0) return void 0;
925
+ const ms = Number(raw);
926
+ if (!Number.isFinite(ms) || ms < min) {
927
+ throw new CLIError(
928
+ `Invalid ${flagName} value "${raw}": expected a number of milliseconds >= ${min}.`,
929
+ "INVALID_ARGUMENT_ERROR",
930
+ { value: raw }
931
+ );
932
+ }
933
+ return ms;
934
+ }
935
+ function parseHeaderList(list, flagName = "--header") {
936
+ if (!list || list.length === 0) return void 0;
937
+ const headers = {};
938
+ for (const entry of list) {
939
+ const idx = entry.indexOf(":");
940
+ const key = idx === -1 ? "" : entry.slice(0, idx).trim();
941
+ if (idx === -1 || key === "") {
942
+ throw new CLIError(
943
+ `Invalid ${flagName} "${entry}": expected "Key: Value".`,
944
+ "INVALID_ARGUMENT_ERROR",
945
+ { header: entry }
946
+ );
947
+ }
948
+ headers[key] = entry.slice(idx + 1).trim();
949
+ }
950
+ return headers;
951
+ }
952
+ function mergeHeaders(base, override) {
953
+ if (!base && !override) return void 0;
954
+ return { ...base, ...override };
955
+ }
956
+ var ENV_RPC_PASS_FILE = "BTCR2_BTC_RPC_PASS_FILE";
957
+ function trimTrailingNewline(value) {
958
+ return value.replace(/\r?\n$/, "");
959
+ }
960
+ function readSecretFile(path, source) {
961
+ try {
962
+ return trimTrailingNewline((0, import_node_fs5.readFileSync)(path, "utf-8"));
963
+ } catch (error) {
964
+ throw new CLIError(
965
+ `Could not read the RPC password ${source} at ${path}: ${error.message}`,
966
+ "CONFIG_READ_ERROR",
967
+ { path }
968
+ );
969
+ }
970
+ }
971
+ function resolveSecretRef(value) {
972
+ if (value === void 0) return void 0;
973
+ if (value.startsWith("env:")) {
974
+ const fromEnv = process.env[value.slice(4)];
975
+ return fromEnv === void 0 ? void 0 : trimTrailingNewline(fromEnv);
976
+ }
977
+ if (value.startsWith("file:")) {
978
+ return readSecretFile(value.slice(5), "file reference");
979
+ }
980
+ return value;
981
+ }
982
+ function readRpcPassFile() {
983
+ const path = process.env[ENV_RPC_PASS_FILE];
984
+ if (!path) return void 0;
985
+ return readSecretFile(path, `file named by ${ENV_RPC_PASS_FILE}`);
986
+ }
987
+ function resolveBroadcastOptions(network, overrides, flags) {
988
+ const file = readConfigFile(overrides?.config ?? defaultConfigPath());
989
+ const { name: activeProfile } = resolveActiveProfile(file, overrides);
990
+ const profileBtc = file?.profiles?.[activeProfile ?? network]?.btc;
991
+ const options = {};
992
+ const feeRateRaw = blankToUndef(flags.feeRate) ?? blankToUndef(process.env[ENV_VARS.FEE_RATE]) ?? (typeof profileBtc?.feeRate === "number" ? String(profileBtc.feeRate) : void 0);
993
+ if (feeRateRaw !== void 0) options.feeEstimator = new import_method.StaticFeeEstimator(parseFeeRate(feeRateRaw));
994
+ const changeAddress = blankToUndef(flags.changeAddress) ?? blankToUndef(profileBtc?.changeAddress);
995
+ if (changeAddress) options.changeAddress = changeAddress;
996
+ return options.feeEstimator || options.changeAddress ? options : void 0;
997
+ }
998
+ function parseFeeRate(raw) {
999
+ const rate = Number(raw);
1000
+ if (!Number.isFinite(rate) || rate <= 0) {
1001
+ throw new CLIError(
1002
+ `Invalid --fee-rate "${raw}": expected a positive number of sats per vByte.`,
1003
+ "INVALID_ARGUMENT_ERROR",
1004
+ { value: raw }
1005
+ );
1006
+ }
1007
+ return rate;
1008
+ }
1009
+ function resolveEffectiveConfig(network, overrides) {
1010
+ const file = readConfigFile(overrides?.config ?? defaultConfigPath());
1011
+ const { name: activeProfile } = resolveActiveProfile(file, overrides);
1012
+ const profileName = activeProfile ?? network;
1013
+ const fileOv = file ? profileToOverrides(file, profileName) : {};
1014
+ const profileBtc = file?.profiles?.[profileName]?.btc;
1015
+ const profileCas = file?.profiles?.[profileName]?.cas;
1016
+ const env = readEnvOverrides();
1017
+ const api = defaultApiFactory(network, overrides);
1018
+ const restCfg = api.btc.connection.rest.config;
1019
+ const rpcCfg = api.btc.connection.rpc?.config;
1020
+ const conn = resolveConnectionConfig(network, overrides);
1021
+ 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";
1022
+ const casGatewayVal = conn.cas?.gateway ?? (conn.cas?.rpcUrl ? void 0 : import_api.DEFAULT_CAS_GATEWAY);
1023
+ const rpcUnit = resolveRpcUnit(overrides, env, fileOv);
1024
+ const rpcSrc = rpcUnit?.src ?? "default";
1025
+ const passFromFile = rpcUnit?.pass === void 0 && process.env[ENV_RPC_PASS_FILE] !== void 0 && rpcCfg?.password !== void 0;
1026
+ return {
1027
+ network,
1028
+ profile: activeProfile,
1029
+ btc: {
1030
+ rest: { value: restCfg.host, source: src(overrides?.btcRest, env.btcRest, fileOv.btcRest) },
1031
+ rpcUrl: { value: rpcCfg?.host, source: rpcUnit?.url !== void 0 ? rpcSrc : "default" },
1032
+ rpcUser: { value: rpcCfg?.username, source: rpcUnit?.user !== void 0 ? rpcSrc : "default" },
1033
+ rpcPass: { value: rpcCfg?.password, source: rpcUnit?.pass !== void 0 ? rpcSrc : passFromFile ? "env" : "default" },
1034
+ rpcWallet: { value: rpcCfg?.wallet, source: src(overrides?.btcRpcWallet, void 0, profileBtc?.wallet) },
1035
+ timeoutMs: { value: conn.btc?.timeoutMs, source: src(overrides?.btcTimeout, process.env[ENV_VARS.BTC_TIMEOUT], profileBtc?.timeoutMs) }
1036
+ },
1037
+ cas: {
1038
+ gateway: { value: casGatewayVal, source: src(overrides?.casGateway, env.casGateway, fileOv.casGateway) },
1039
+ rpcUrl: { value: conn.cas?.rpcUrl, source: src(overrides?.casRpcUrl, env.casRpcUrl, fileOv.casRpcUrl) },
1040
+ timeoutMs: { value: conn.cas?.timeoutMs, source: src(overrides?.casTimeout, process.env[ENV_VARS.CAS_TIMEOUT], profileCas?.timeoutMs) }
1041
+ }
1042
+ };
1043
+ }
1044
+ var DOCTOR_PROBE_TIMEOUT_MS = 5e3;
1045
+ async function probeEndpoint(endpoint, target, url, opts) {
1046
+ try {
1047
+ const res = await fetch(url, {
1048
+ method: opts?.method ?? "GET",
1049
+ headers: opts?.headers,
1050
+ signal: AbortSignal.timeout(DOCTOR_PROBE_TIMEOUT_MS)
1051
+ });
1052
+ return res.ok ? { endpoint, target, ok: true } : { endpoint, target, ok: false, detail: `HTTP ${res.status}` };
1053
+ } catch (error) {
1054
+ return { endpoint, target, ok: false, detail: error.message };
1055
+ }
1056
+ }
1057
+ function withProbeTimeout(promise, ms) {
1058
+ let timer;
1059
+ const timeout = new Promise((_, reject) => {
1060
+ timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
1061
+ });
1062
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
1063
+ }
1064
+ async function runDoctor(network, overrides) {
1065
+ const api = defaultApiFactory(network, overrides);
1066
+ const checks = [];
1067
+ const restHost = api.btc.connection.rest.config.host.replace(/\/+$/, "");
1068
+ checks.push(await probeEndpoint("btc-rest", restHost, `${restHost}/blocks/tip/height`, { headers: api.btc.connection.rest.config.headers }));
1069
+ const rpc = api.btc.connection.rpc;
1070
+ if (rpc) {
1071
+ const target = rpc.config.host ?? "(default rpc)";
1072
+ try {
1073
+ await withProbeTimeout(rpc.getBlockchainInfo(), DOCTOR_PROBE_TIMEOUT_MS);
1074
+ checks.push({ endpoint: "btc-rpc", target, ok: true });
1075
+ } catch (error) {
1076
+ checks.push({ endpoint: "btc-rpc", target, ok: false, detail: error.message });
1077
+ }
1078
+ }
1079
+ const conn = resolveConnectionConfig(network, overrides);
1080
+ if (conn.cas?.rpcUrl) {
1081
+ const base = conn.cas.rpcUrl.replace(/\/+$/, "");
1082
+ checks.push(await probeEndpoint("cas", conn.cas.rpcUrl, `${base}/api/v0/version`, { method: "POST" }));
1083
+ } else {
1084
+ const gateway = (conn.cas?.gateway ?? import_api.DEFAULT_CAS_GATEWAY).replace(/\/+$/, "");
1085
+ checks.push(await probeEndpoint("cas", gateway, gateway));
1086
+ }
1087
+ const mismatch = profileNetworkMismatch(network, overrides);
1088
+ return {
1089
+ checks,
1090
+ ...mismatch ? { coherence: { profile: mismatch.profile, declared: mismatch.declared, encoding: network } } : {}
1091
+ };
1092
+ }
794
1093
  function defaultApiFactory(network, overrides) {
795
1094
  return (0, import_api.createApi)(resolveConnectionConfig(network, overrides));
796
1095
  }
797
1096
  function buildKeystoreKms(overrides) {
798
1097
  return new FileBackedKeyManager({
799
- path: overrides?.keystore ?? defaultKeystorePath(),
1098
+ path: resolveKeystorePath(overrides),
800
1099
  getPassphrase: () => acquirePassphrase({ passphraseFile: overrides?.passphraseFile })
801
1100
  });
802
1101
  }
1102
+ function resolveKeystorePath(overrides) {
1103
+ return overrides?.keystore ?? activeProfileIdentity(overrides)?.keystore ?? defaultKeystorePath();
1104
+ }
1105
+ function activeProfileIdentity(overrides) {
1106
+ const file = readConfigFile(overrides?.config ?? defaultConfigPath());
1107
+ const { name } = resolveActiveProfile(file, overrides);
1108
+ return name ? file?.profiles?.[name]?.identity : void 0;
1109
+ }
1110
+ function resolveSigningKeyRef(overrides) {
1111
+ return blankToUndef(overrides?.signingKey) ?? blankToUndef(activeProfileIdentity(overrides)?.default);
1112
+ }
803
1113
  function keystoreApiFactory(network, overrides) {
804
1114
  return (0, import_api.createApi)({
805
1115
  ...resolveConnectionConfig(network, overrides),
@@ -857,6 +1167,31 @@ function resolveKeyRef(kms, ref) {
857
1167
  }
858
1168
 
859
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
+ }
860
1195
  function formatResult(result, options) {
861
1196
  if (options.output === "json") {
862
1197
  return JSON.stringify(result, null, 2);
@@ -885,6 +1220,13 @@ function registerCreateCommand(program, factory, keystoreFactory, globals) {
885
1220
  const overrides = overridesFromGlobals(g);
886
1221
  const network = resolveNetwork(options.network, overrides);
887
1222
  const signingKey = g.signingKey;
1223
+ const mismatch = profileNetworkMismatch(network, overrides);
1224
+ if (mismatch && !g.quiet) {
1225
+ process.stderr.write(
1226
+ `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.
1227
+ `
1228
+ );
1229
+ }
888
1230
  const print = (result, note) => {
889
1231
  console.log(formatResult(result, g));
890
1232
  if (note && g.output !== "json") process.stderr.write(`${note}
@@ -1048,6 +1390,12 @@ function registerUpdateCommand(program, factory, globals) {
1048
1390
  "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
1391
  parsePublishToCasMode,
1050
1392
  "never"
1393
+ ).option(
1394
+ "--fee-rate <satsPerVByte>",
1395
+ "Fee rate in sats/vByte for the beacon transaction (default: 5). Raise it under congestion so the transaction confirms."
1396
+ ).option(
1397
+ "--change-address <address>",
1398
+ "Send transaction change to this address instead of the beacon address, so a DID's announcements are not linked on-chain (ADR 044)."
1051
1399
  ).action(async (options) => {
1052
1400
  if (!/^\d+$/.test(options.sourceVersionId)) {
1053
1401
  throw new CLIError(
@@ -1073,8 +1421,12 @@ function registerUpdateCommand(program, factory, globals) {
1073
1421
  }
1074
1422
  const network = deriveNetwork(did);
1075
1423
  const api = factory(network, globals());
1076
- const keyId = resolveKeyRef(api.kms.kms, globals().signingKey);
1424
+ const keyId = resolveKeyRef(api.kms.kms, resolveSigningKeyRef(globals()));
1077
1425
  const signer = new import_key_manager2.KeyManagerSigner(api.kms.kms, keyId);
1426
+ const broadcastOptions = resolveBroadcastOptions(network, globals(), {
1427
+ feeRate: options.feeRate,
1428
+ changeAddress: options.changeAddress
1429
+ });
1078
1430
  const data = await api.btcr2.update({
1079
1431
  sourceDocument: parsed.sourceDocument,
1080
1432
  patches: parsed.patches,
@@ -1082,7 +1434,8 @@ function registerUpdateCommand(program, factory, globals) {
1082
1434
  verificationMethodId: parsed.verificationMethodId,
1083
1435
  beaconId: parsed.beaconId,
1084
1436
  signer,
1085
- publishToCas: options.publishToCas
1437
+ publishToCas: options.publishToCas,
1438
+ ...broadcastOptions ? { broadcastOptions } : {}
1086
1439
  });
1087
1440
  console.log(formatResult({ action: "update", data }, globals()));
1088
1441
  });
@@ -1134,6 +1487,12 @@ function registerDeactivateCommand(program, factory, globals) {
1134
1487
  "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
1488
  parsePublishToCasMode2,
1136
1489
  "never"
1490
+ ).option(
1491
+ "--fee-rate <satsPerVByte>",
1492
+ "Fee rate in sats/vByte for the beacon transaction (default: 5). Raise it under congestion so the transaction confirms."
1493
+ ).option(
1494
+ "--change-address <address>",
1495
+ "Send transaction change to this address instead of the beacon address, so a DID's announcements are not linked on-chain (ADR 044)."
1137
1496
  ).action(async (options) => {
1138
1497
  if (!/^\d+$/.test(options.sourceVersionId)) {
1139
1498
  throw new CLIError(
@@ -1159,8 +1518,12 @@ function registerDeactivateCommand(program, factory, globals) {
1159
1518
  }
1160
1519
  const network = deriveNetwork(did);
1161
1520
  const api = factory(network, globals());
1162
- const keyId = resolveKeyRef(api.kms.kms, globals().signingKey);
1521
+ const keyId = resolveKeyRef(api.kms.kms, resolveSigningKeyRef(globals()));
1163
1522
  const signer = new import_key_manager3.KeyManagerSigner(api.kms.kms, keyId);
1523
+ const broadcastOptions = resolveBroadcastOptions(network, globals(), {
1524
+ feeRate: options.feeRate,
1525
+ changeAddress: options.changeAddress
1526
+ });
1164
1527
  const data = await api.btcr2.update({
1165
1528
  sourceDocument: parsed.sourceDocument,
1166
1529
  patches: parsed.patches,
@@ -1168,7 +1531,8 @@ function registerDeactivateCommand(program, factory, globals) {
1168
1531
  verificationMethodId: parsed.verificationMethodId,
1169
1532
  beaconId: parsed.beaconId,
1170
1533
  signer,
1171
- publishToCas: options.publishToCas
1534
+ publishToCas: options.publishToCas,
1535
+ ...broadcastOptions ? { broadcastOptions } : {}
1172
1536
  });
1173
1537
  console.log(formatResult({ action: "deactivate", data }, globals()));
1174
1538
  });
@@ -1324,6 +1688,128 @@ function writeSecretFile(path, contents) {
1324
1688
  // src/commands/config.ts
1325
1689
  var import_node_fs7 = require("fs");
1326
1690
  var import_node_path5 = require("path");
1691
+
1692
+ // src/config-schema.ts
1693
+ var CONFIG_SCHEMA = {
1694
+ schemaVersion: "number",
1695
+ defaults: {
1696
+ profile: "string",
1697
+ network: "enum:network",
1698
+ output: "enum:output"
1699
+ },
1700
+ profiles: {
1701
+ "*": {
1702
+ network: "enum:network",
1703
+ btc: {
1704
+ rest: "string",
1705
+ rpcUrl: "string",
1706
+ rpcUser: "string",
1707
+ rpcPass: "string",
1708
+ feeRate: "number",
1709
+ changeAddress: "string",
1710
+ timeoutMs: "number",
1711
+ headers: "object",
1712
+ wallet: "string",
1713
+ rpcHeaders: "object"
1714
+ },
1715
+ cas: {
1716
+ gateway: "string",
1717
+ rpcUrl: "string",
1718
+ timeoutMs: "number"
1719
+ },
1720
+ identity: {
1721
+ keystore: "string",
1722
+ default: "string"
1723
+ }
1724
+ }
1725
+ }
1726
+ };
1727
+ function lookupSchemaNode(dotted) {
1728
+ let node = CONFIG_SCHEMA;
1729
+ for (const segment of dotted.split(".")) {
1730
+ if (typeof node !== "object") return void 0;
1731
+ if (Object.hasOwn(node, segment)) {
1732
+ node = node[segment];
1733
+ } else if (Object.hasOwn(node, "*")) {
1734
+ node = node["*"];
1735
+ } else {
1736
+ return void 0;
1737
+ }
1738
+ }
1739
+ return node;
1740
+ }
1741
+ function assertLeafValue(kind, dotted, value) {
1742
+ if (kind === "enum:network" && !SUPPORTED_NETWORKS.includes(value)) {
1743
+ throw new CLIError(
1744
+ `Invalid value for ${dotted}: "${String(value)}". Expected one of ${SUPPORTED_NETWORKS.join(", ")}.`,
1745
+ "INVALID_ARGUMENT_ERROR",
1746
+ { path: dotted, value }
1747
+ );
1748
+ }
1749
+ if (kind === "enum:output" && value !== "json" && value !== "text") {
1750
+ throw new CLIError(
1751
+ `Invalid value for ${dotted}: "${String(value)}". Expected "json" or "text".`,
1752
+ "INVALID_ARGUMENT_ERROR",
1753
+ { path: dotted, value }
1754
+ );
1755
+ }
1756
+ if (kind === "number" && typeof value !== "number") {
1757
+ throw new CLIError(
1758
+ `Invalid value for ${dotted}: expected a number, got ${typeof value}. Pass a bare number (e.g. \`config set ` + dotted + " 5`).",
1759
+ "INVALID_ARGUMENT_ERROR",
1760
+ { path: dotted, value }
1761
+ );
1762
+ }
1763
+ if (kind === "object" && (typeof value !== "object" || value === null || Array.isArray(value))) {
1764
+ throw new CLIError(
1765
+ `Invalid value for ${dotted}: expected a JSON object (e.g. \`config set ${dotted} '{"Key":"Value"}'\`).`,
1766
+ "INVALID_ARGUMENT_ERROR",
1767
+ { path: dotted, value }
1768
+ );
1769
+ }
1770
+ }
1771
+ function validateConfigSet(dotted, value) {
1772
+ const node = lookupSchemaNode(dotted);
1773
+ if (node === void 0) return { unknownPath: true };
1774
+ if (typeof node === "string") {
1775
+ assertLeafValue(node, dotted, value);
1776
+ }
1777
+ return { unknownPath: false };
1778
+ }
1779
+ function findConfigIssues(config, supportedSchemaVersion) {
1780
+ const issues = [];
1781
+ const version = config.schemaVersion;
1782
+ if (typeof version === "number" && version > supportedSchemaVersion) {
1783
+ issues.push({
1784
+ path: "schemaVersion",
1785
+ issue: `newer than supported (${version} > ${supportedSchemaVersion}); upgrade the CLI to use this file`
1786
+ });
1787
+ }
1788
+ walk(config, [], issues);
1789
+ return issues;
1790
+ }
1791
+ function walk(obj, prefix, issues) {
1792
+ for (const [key, value] of Object.entries(obj)) {
1793
+ const path = [...prefix, key];
1794
+ const dotted = path.join(".");
1795
+ const node = lookupSchemaNode(dotted);
1796
+ if (node === void 0) {
1797
+ issues.push({ path: dotted, issue: "unknown key" });
1798
+ continue;
1799
+ }
1800
+ if (typeof node === "string") {
1801
+ try {
1802
+ assertLeafValue(node, dotted, value);
1803
+ } catch (error) {
1804
+ issues.push({ path: dotted, issue: error.message });
1805
+ }
1806
+ } else if (value !== null && typeof value === "object" && !Array.isArray(value)) {
1807
+ walk(value, path, issues);
1808
+ }
1809
+ }
1810
+ }
1811
+
1812
+ // src/commands/config.ts
1327
1813
  function registerConfigCommand(program, globals) {
1328
1814
  const config = program.command("config").description("Read and write CLI configuration.");
1329
1815
  const path = () => globals().config ?? defaultConfigPath();
@@ -1343,29 +1829,100 @@ function registerConfigCommand(program, globals) {
1343
1829
  `, 384);
1344
1830
  print({ action: "config-init", data: { path: p } });
1345
1831
  });
1346
- config.command("get [path]").description("Print a value at a dotted path, or the whole config.").action((dotted) => {
1832
+ 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
1833
  const file = readConfigFile(path()) ?? {};
1348
- print({ action: "config-get", data: (dotted ? getConfigPath(file, dotted) : file) ?? null });
1834
+ const raw = (dotted ? getConfigPath(file, dotted) : file) ?? null;
1835
+ const leaf = dotted ? dotted.split(".").pop() : void 0;
1836
+ print({ action: "config-get", data: opts.showSecrets ? raw : redactSecrets(raw, leaf) });
1349
1837
  });
1350
1838
  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)));
1839
+ const parsed = parseValue(dotted, value);
1840
+ const { unknownPath } = validateConfigSet(dotted, parsed);
1841
+ if (unknownPath && !globals().quiet) {
1842
+ console.error(`Warning: "${dotted}" is not a known config path; writing it anyway.`);
1843
+ }
1844
+ writeConfigFile(path(), (raw) => setConfigPath(raw, dotted, parsed));
1352
1845
  print({ action: "config-set", data: { path: dotted } });
1353
1846
  });
1354
1847
  config.command("unset <path>").description("Delete a value at a dotted path.").action((dotted) => {
1355
1848
  writeConfigFile(path(), (raw) => unsetConfigPath(raw, dotted));
1356
1849
  print({ action: "config-unset", data: { path: dotted } });
1357
1850
  });
1358
- config.command("list").alias("ls").description("Print the entire config file.").action(() => {
1359
- print({ action: "config-list", data: readConfigFile(path()) ?? {} });
1851
+ 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) => {
1852
+ const file = readConfigFile(path()) ?? {};
1853
+ print({ action: "config-list", data: opts.showSecrets ? file : redactSecrets(file) });
1854
+ });
1855
+ config.command("validate").description("Check the config for unknown keys, invalid enum values, and an unsupported schema version.").action(() => {
1856
+ const file = parseConfigFileRaw(path()) ?? {};
1857
+ const issues = findConfigIssues(file, CONFIG_SCHEMA_VERSION);
1858
+ if (issues.length > 0) process.exitCode ??= 1;
1859
+ print({ action: "config-validate", data: { ok: issues.length === 0, issues } });
1860
+ });
1861
+ 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) => {
1862
+ const network = resolveIntrospectionNetwork(opts.network, globals());
1863
+ const data = resolveEffectiveConfig(network, globals());
1864
+ if (!opts.showSecrets) {
1865
+ if (data.btc.rpcPass.value !== void 0) {
1866
+ data.btc.rpcPass = { ...data.btc.rpcPass, value: REDACTED };
1867
+ }
1868
+ for (const entry of [data.btc.rest, data.btc.rpcUrl, data.cas.gateway, data.cas.rpcUrl]) {
1869
+ if (typeof entry.value === "string") entry.value = scrubUrlUserinfo(entry.value);
1870
+ }
1871
+ }
1872
+ print({ action: "config-effective", data });
1873
+ });
1874
+ config.command("path").description("Print the resolved config-file and keystore paths.").action(() => {
1875
+ const data = {
1876
+ config: globals().config ?? defaultConfigPath(),
1877
+ keystore: globals().keystore ?? defaultKeystorePath()
1878
+ };
1879
+ print({ action: "config-path", data });
1880
+ });
1881
+ 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) => {
1882
+ const network = resolveIntrospectionNetwork(opts.network, globals());
1883
+ const report = await runDoctor(network, globals());
1884
+ if (report.checks.some((check) => !check.ok)) process.exitCode ??= 1;
1885
+ print({ action: "config-doctor", data: report });
1360
1886
  });
1361
1887
  }
1362
- function parseValue(value) {
1888
+ function resolveIntrospectionNetwork(explicit, globals) {
1889
+ if (explicit) {
1890
+ if (!SUPPORTED_NETWORKS.includes(explicit)) {
1891
+ throw new CLIError(
1892
+ `Invalid network "${explicit}". Must be one of ${SUPPORTED_NETWORKS.join(", ")}.`,
1893
+ "INVALID_ARGUMENT_ERROR",
1894
+ { network: explicit }
1895
+ );
1896
+ }
1897
+ return explicit;
1898
+ }
1899
+ return resolveDefaultNetwork(globals);
1900
+ }
1901
+ function parseValue(dotted, value) {
1902
+ if (isStringScalarPath(dotted)) return value;
1363
1903
  try {
1364
1904
  return JSON.parse(value);
1365
1905
  } catch {
1366
1906
  return value;
1367
1907
  }
1368
1908
  }
1909
+ function isStringScalarPath(dotted) {
1910
+ const segments = dotted.split(".");
1911
+ if (segments.length === 2 && segments[0] === "defaults") {
1912
+ return ["profile", "network", "output"].includes(segments[1]);
1913
+ }
1914
+ if (segments.length === 3 && segments[0] === "profiles" && segments[2] === "network") {
1915
+ return true;
1916
+ }
1917
+ if (segments.length === 4 && segments[0] === "profiles") {
1918
+ const group = segments[2];
1919
+ const leaf = segments[3];
1920
+ if (group === "btc") return ["rest", "rpcUrl", "rpcUser", "rpcPass", "changeAddress", "wallet"].includes(leaf);
1921
+ if (group === "cas") return ["gateway", "rpcUrl"].includes(leaf);
1922
+ if (group === "identity") return ["keystore", "default"].includes(leaf);
1923
+ }
1924
+ return false;
1925
+ }
1369
1926
 
1370
1927
  // src/commands/profile.ts
1371
1928
  function registerProfileCommand(program, globals) {
@@ -1388,7 +1945,7 @@ function registerProfileCommand(program, globals) {
1388
1945
  });
1389
1946
  print({ action: "profile-use", data: { profile: name } });
1390
1947
  });
1391
- profile.command("show [name]").description("Show a profile (defaults to the active profile).").action((name) => {
1948
+ 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
1949
  const file = readConfigFile(path()) ?? {};
1393
1950
  const target = name ?? file.defaults?.profile;
1394
1951
  if (!target) {
@@ -1398,7 +1955,8 @@ function registerProfileCommand(program, globals) {
1398
1955
  if (!data) {
1399
1956
  throw new CLIError(`Profile "${target}" not found.`, "INVALID_ARGUMENT_ERROR", { profile: target });
1400
1957
  }
1401
- print({ action: "profile-show", data: { profile: target, ...data } });
1958
+ const payload = { profile: target, ...data };
1959
+ print({ action: "profile-show", data: opts.showSecrets ? payload : redactSecrets(payload) });
1402
1960
  });
1403
1961
  profile.command("remove <name>").alias("rm").description("Remove a profile.").action((name) => {
1404
1962
  writeConfigFile(path(), (raw) => {
@@ -1477,8 +2035,12 @@ var DidBtcr2Cli = class {
1477
2035
  * {@link keystoreApiFactory}.
1478
2036
  */
1479
2037
  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");
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");
1481
2039
  const globals = () => this.program.opts();
2040
+ this.program.hook("preAction", () => {
2041
+ const opts = this.program.opts();
2042
+ opts.output = resolveOutputFormat({ output: opts.output, config: opts.config });
2043
+ });
1482
2044
  registerCreateCommand(this.program, factory, keystoreFactory, globals);
1483
2045
  registerResolveCommand(this.program, factory, globals);
1484
2046
  registerUpdateCommand(this.program, keystoreFactory, globals);
@@ -1503,6 +2065,9 @@ var DidBtcr2Cli = class {
1503
2065
  }
1504
2066
  }
1505
2067
  };
2068
+ function collectHeader(value, previous) {
2069
+ return [...previous, value];
2070
+ }
1506
2071
  function normalizeArgv(argv) {
1507
2072
  if (argv.length >= 2) return argv;
1508
2073
  if (argv.length === 1) return ["node", argv[0]];
@@ -1525,18 +2090,26 @@ function handleError(error, verbose) {
1525
2090
  CLIError,
1526
2091
  CONFIG_SCHEMA_VERSION,
1527
2092
  DidBtcr2Cli,
2093
+ ENV_RPC_PASS_FILE,
1528
2094
  ENV_VARS,
2095
+ REDACTED,
1529
2096
  SUPPORTED_NETWORKS,
1530
2097
  VERSION,
2098
+ blankToUndef,
1531
2099
  defaultApiFactory,
1532
2100
  defaultConfigPath,
1533
2101
  deriveNetwork,
1534
2102
  formatResult,
1535
2103
  getConfigPath,
2104
+ isSecretKey,
1536
2105
  keystoreApiFactory,
2106
+ parseConfigFileRaw,
2107
+ parseHeaderList,
2108
+ profileNetworkMismatch,
1537
2109
  profileToOverrides,
1538
2110
  readConfigFile,
1539
2111
  readEnvOverrides,
2112
+ redactSecrets,
1540
2113
  registerCompletionCommand,
1541
2114
  registerConfigCommand,
1542
2115
  registerCreateCommand,
@@ -1545,7 +2118,17 @@ function handleError(error, verbose) {
1545
2118
  registerProfileCommand,
1546
2119
  registerResolveCommand,
1547
2120
  registerUpdateCommand,
2121
+ resolveActiveProfile,
2122
+ resolveBroadcastOptions,
2123
+ resolveConnectionConfig,
1548
2124
  resolveDefaultNetwork,
2125
+ resolveEffectiveConfig,
2126
+ resolveKeystorePath,
2127
+ resolveOutputFormat,
2128
+ resolveSecretRef,
2129
+ resolveSigningKeyRef,
2130
+ runDoctor,
2131
+ scrubUrlUserinfo,
1549
2132
  setConfigPath,
1550
2133
  unsetConfigPath,
1551
2134
  writeConfigFile