@did-btcr2/cli 0.13.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 +95 -14
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/cjs/index.js +674 -54
  4. package/dist/esm/src/cli.js +25 -3
  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 +33 -6
  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 +33 -6
  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 +515 -35
  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 +228 -6
  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 +33 -0
  40. package/dist/types/src/types.d.ts.map +1 -1
  41. package/package.json +4 -4
  42. package/src/cli.ts +27 -3
  43. package/src/commands/config.ts +135 -8
  44. package/src/commands/create.ts +13 -1
  45. package/src/commands/deactivate.ts +53 -6
  46. package/src/commands/profile.ts +5 -3
  47. package/src/commands/update.ts +53 -6
  48. package/src/config-schema.ts +178 -0
  49. package/src/config.ts +693 -43
  50. package/src/keystore/paths.ts +3 -2
  51. package/src/output.ts +53 -0
  52. package/src/types.ts +23 -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;
@@ -710,7 +743,11 @@ var ENV_VARS = {
710
743
  BTC_RPC_URL: "BTCR2_BTC_RPC_URL",
711
744
  BTC_RPC_USER: "BTCR2_BTC_RPC_USER",
712
745
  BTC_RPC_PASS: "BTCR2_BTC_RPC_PASS",
713
- CAS_GATEWAY: "BTCR2_CAS_GATEWAY"
746
+ CAS_GATEWAY: "BTCR2_CAS_GATEWAY",
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"
714
751
  };
715
752
  function readEnvOverrides() {
716
753
  const env = (key) => process.env[key] || void 0;
@@ -719,19 +756,50 @@ function readEnvOverrides() {
719
756
  btcRpcUrl: env(ENV_VARS.BTC_RPC_URL),
720
757
  btcRpcUser: env(ENV_VARS.BTC_RPC_USER),
721
758
  btcRpcPass: env(ENV_VARS.BTC_RPC_PASS),
722
- casGateway: env(ENV_VARS.CAS_GATEWAY)
759
+ casGateway: env(ENV_VARS.CAS_GATEWAY),
760
+ casRpcUrl: env(ENV_VARS.CAS_RPC_URL)
723
761
  };
724
762
  }
725
763
  function defaultConfigPath() {
726
- 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");
727
765
  return (0, import_node_path4.join)(base, "btcr2", "config.json");
728
766
  }
729
- 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
+ }
730
779
  try {
731
- const content = (0, import_node_fs5.readFileSync)(path, "utf-8");
732
780
  return JSON.parse(content);
733
- } catch {
734
- 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
+ );
735
803
  }
736
804
  }
737
805
  function profileToOverrides(config, profileName) {
@@ -742,57 +810,306 @@ function profileToOverrides(config, profileName) {
742
810
  btcRpcUrl: profile.btc?.rpcUrl,
743
811
  btcRpcUser: profile.btc?.rpcUser,
744
812
  btcRpcPass: profile.btc?.rpcPass,
745
- casGateway: profile.cas?.gateway
813
+ casGateway: profile.cas?.gateway,
814
+ casRpcUrl: profile.cas?.rpcUrl
746
815
  };
747
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
+ }
748
824
  function resolveDefaultNetwork(overrides) {
749
825
  const configPath = overrides?.config ?? defaultConfigPath();
750
826
  const file = readConfigFile(configPath);
751
827
  const explicit = file?.defaults?.network;
752
828
  if (explicit && SUPPORTED_NETWORKS.includes(explicit)) return explicit;
753
- const profile = overrides?.profile ?? file?.defaults?.profile;
754
- if (profile && SUPPORTED_NETWORKS.includes(profile)) {
755
- return profile;
756
- }
829
+ const { network } = resolveActiveProfile(file, overrides);
830
+ if (network) return network;
757
831
  return "regtest";
758
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
+ }
759
874
  function resolveConnectionConfig(network, overrides) {
760
875
  if (!network) return {};
761
876
  const configPath = overrides?.config ?? defaultConfigPath();
762
877
  const file = readConfigFile(configPath);
763
- const profileName = overrides?.profile ?? file?.defaults?.profile ?? network;
878
+ const { name: activeProfile } = resolveActiveProfile(file, overrides);
879
+ const profileName = activeProfile ?? network;
764
880
  const fileOverrides = file ? profileToOverrides(file, profileName) : {};
765
881
  const env = readEnvOverrides();
766
- const merged = {
767
- btcRest: overrides?.btcRest ?? env.btcRest ?? fileOverrides.btcRest,
768
- btcRpcUrl: overrides?.btcRpcUrl ?? env.btcRpcUrl ?? fileOverrides.btcRpcUrl,
769
- btcRpcUser: overrides?.btcRpcUser ?? env.btcRpcUser ?? fileOverrides.btcRpcUser,
770
- btcRpcPass: overrides?.btcRpcPass ?? env.btcRpcPass ?? fileOverrides.btcRpcPass,
771
- casGateway: overrides?.casGateway ?? env.casGateway ?? fileOverrides.casGateway
772
- };
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;
773
885
  const btc = { network };
774
- if (merged.btcRest) {
775
- btc.rest = { host: merged.btcRest };
776
- }
777
- 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();
778
899
  btc.rpc = {
779
- host: merged.btcRpcUrl,
780
- username: merged.btcRpcUser,
781
- 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 } : {}
782
905
  };
783
906
  }
784
- const cas = merged.casGateway ? { gateway: merged.casGateway } : void 0;
785
- return { btc, ...cas && { cas } };
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");
912
+ const cas = {};
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;
920
+ return { btc, ...hasCas && { cas } };
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
+ };
786
1092
  }
787
1093
  function defaultApiFactory(network, overrides) {
788
1094
  return (0, import_api.createApi)(resolveConnectionConfig(network, overrides));
789
1095
  }
790
1096
  function buildKeystoreKms(overrides) {
791
1097
  return new FileBackedKeyManager({
792
- path: overrides?.keystore ?? defaultKeystorePath(),
1098
+ path: resolveKeystorePath(overrides),
793
1099
  getPassphrase: () => acquirePassphrase({ passphraseFile: overrides?.passphraseFile })
794
1100
  });
795
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
+ }
796
1113
  function keystoreApiFactory(network, overrides) {
797
1114
  return (0, import_api.createApi)({
798
1115
  ...resolveConnectionConfig(network, overrides),
@@ -850,6 +1167,31 @@ function resolveKeyRef(kms, ref) {
850
1167
  }
851
1168
 
852
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
+ }
853
1195
  function formatResult(result, options) {
854
1196
  if (options.output === "json") {
855
1197
  return JSON.stringify(result, null, 2);
@@ -878,6 +1220,13 @@ function registerCreateCommand(program, factory, keystoreFactory, globals) {
878
1220
  const overrides = overridesFromGlobals(g);
879
1221
  const network = resolveNetwork(options.network, overrides);
880
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
+ }
881
1230
  const print = (result, note) => {
882
1231
  console.log(formatResult(result, g));
883
1232
  if (note && g.output !== "json") process.stderr.write(`${note}
@@ -1036,6 +1385,17 @@ function registerUpdateCommand(program, factory, globals) {
1036
1385
  "-b, --beacon-id <json>",
1037
1386
  "Beacon ID as a JSON string",
1038
1387
  parseJsonArg("--beacon-id")
1388
+ ).option(
1389
+ "--publish-to-cas <mode>",
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.",
1391
+ parsePublishToCasMode,
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)."
1039
1399
  ).action(async (options) => {
1040
1400
  if (!/^\d+$/.test(options.sourceVersionId)) {
1041
1401
  throw new CLIError(
@@ -1061,8 +1421,12 @@ function registerUpdateCommand(program, factory, globals) {
1061
1421
  }
1062
1422
  const network = deriveNetwork(did);
1063
1423
  const api = factory(network, globals());
1064
- const keyId = resolveKeyRef(api.kms.kms, globals().signingKey);
1424
+ const keyId = resolveKeyRef(api.kms.kms, resolveSigningKeyRef(globals()));
1065
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
+ });
1066
1430
  const data = await api.btcr2.update({
1067
1431
  sourceDocument: parsed.sourceDocument,
1068
1432
  patches: parsed.patches,
@@ -1070,11 +1434,22 @@ function registerUpdateCommand(program, factory, globals) {
1070
1434
  verificationMethodId: parsed.verificationMethodId,
1071
1435
  beaconId: parsed.beaconId,
1072
1436
  signer,
1073
- publishToCas: "never"
1437
+ publishToCas: options.publishToCas,
1438
+ ...broadcastOptions ? { broadcastOptions } : {}
1074
1439
  });
1075
1440
  console.log(formatResult({ action: "update", data }, globals()));
1076
1441
  });
1077
1442
  }
1443
+ function parsePublishToCasMode(value) {
1444
+ if (value !== "auto" && value !== "always" && value !== "never") {
1445
+ throw new CLIError(
1446
+ '--publish-to-cas must be one of "auto", "always", or "never".',
1447
+ "INVALID_ARGUMENT_ERROR",
1448
+ { value }
1449
+ );
1450
+ }
1451
+ return value;
1452
+ }
1078
1453
  function parseJsonArg(flagName) {
1079
1454
  return (value) => {
1080
1455
  try {
@@ -1107,6 +1482,17 @@ function registerDeactivateCommand(program, factory, globals) {
1107
1482
  "-b, --beacon-id <json>",
1108
1483
  "Beacon ID as a JSON string",
1109
1484
  parseJsonArg2("--beacon-id")
1485
+ ).option(
1486
+ "--publish-to-cas <mode>",
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.",
1488
+ parsePublishToCasMode2,
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)."
1110
1496
  ).action(async (options) => {
1111
1497
  if (!/^\d+$/.test(options.sourceVersionId)) {
1112
1498
  throw new CLIError(
@@ -1132,8 +1518,12 @@ function registerDeactivateCommand(program, factory, globals) {
1132
1518
  }
1133
1519
  const network = deriveNetwork(did);
1134
1520
  const api = factory(network, globals());
1135
- const keyId = resolveKeyRef(api.kms.kms, globals().signingKey);
1521
+ const keyId = resolveKeyRef(api.kms.kms, resolveSigningKeyRef(globals()));
1136
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
+ });
1137
1527
  const data = await api.btcr2.update({
1138
1528
  sourceDocument: parsed.sourceDocument,
1139
1529
  patches: parsed.patches,
@@ -1141,11 +1531,22 @@ function registerDeactivateCommand(program, factory, globals) {
1141
1531
  verificationMethodId: parsed.verificationMethodId,
1142
1532
  beaconId: parsed.beaconId,
1143
1533
  signer,
1144
- publishToCas: "never"
1534
+ publishToCas: options.publishToCas,
1535
+ ...broadcastOptions ? { broadcastOptions } : {}
1145
1536
  });
1146
1537
  console.log(formatResult({ action: "deactivate", data }, globals()));
1147
1538
  });
1148
1539
  }
1540
+ function parsePublishToCasMode2(value) {
1541
+ if (value !== "auto" && value !== "always" && value !== "never") {
1542
+ throw new CLIError(
1543
+ '--publish-to-cas must be one of "auto", "always", or "never".',
1544
+ "INVALID_ARGUMENT_ERROR",
1545
+ { value }
1546
+ );
1547
+ }
1548
+ return value;
1549
+ }
1149
1550
  function parseJsonArg2(flagName) {
1150
1551
  return (value) => {
1151
1552
  try {
@@ -1287,6 +1688,128 @@ function writeSecretFile(path, contents) {
1287
1688
  // src/commands/config.ts
1288
1689
  var import_node_fs7 = require("fs");
1289
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
1290
1813
  function registerConfigCommand(program, globals) {
1291
1814
  const config = program.command("config").description("Read and write CLI configuration.");
1292
1815
  const path = () => globals().config ?? defaultConfigPath();
@@ -1306,29 +1829,100 @@ function registerConfigCommand(program, globals) {
1306
1829
  `, 384);
1307
1830
  print({ action: "config-init", data: { path: p } });
1308
1831
  });
1309
- 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) => {
1310
1833
  const file = readConfigFile(path()) ?? {};
1311
- 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) });
1312
1837
  });
1313
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) => {
1314
- 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));
1315
1845
  print({ action: "config-set", data: { path: dotted } });
1316
1846
  });
1317
1847
  config.command("unset <path>").description("Delete a value at a dotted path.").action((dotted) => {
1318
1848
  writeConfigFile(path(), (raw) => unsetConfigPath(raw, dotted));
1319
1849
  print({ action: "config-unset", data: { path: dotted } });
1320
1850
  });
1321
- config.command("list").alias("ls").description("Print the entire config file.").action(() => {
1322
- 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 });
1323
1886
  });
1324
1887
  }
1325
- 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;
1326
1903
  try {
1327
1904
  return JSON.parse(value);
1328
1905
  } catch {
1329
1906
  return value;
1330
1907
  }
1331
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
+ }
1332
1926
 
1333
1927
  // src/commands/profile.ts
1334
1928
  function registerProfileCommand(program, globals) {
@@ -1351,7 +1945,7 @@ function registerProfileCommand(program, globals) {
1351
1945
  });
1352
1946
  print({ action: "profile-use", data: { profile: name } });
1353
1947
  });
1354
- 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) => {
1355
1949
  const file = readConfigFile(path()) ?? {};
1356
1950
  const target = name ?? file.defaults?.profile;
1357
1951
  if (!target) {
@@ -1361,7 +1955,8 @@ function registerProfileCommand(program, globals) {
1361
1955
  if (!data) {
1362
1956
  throw new CLIError(`Profile "${target}" not found.`, "INVALID_ARGUMENT_ERROR", { profile: target });
1363
1957
  }
1364
- 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) });
1365
1960
  });
1366
1961
  profile.command("remove <name>").alias("rm").description("Remove a profile.").action((name) => {
1367
1962
  writeConfigFile(path(), (raw) => {
@@ -1440,8 +2035,12 @@ var DidBtcr2Cli = class {
1440
2035
  * {@link keystoreApiFactory}.
1441
2036
  */
1442
2037
  constructor(factory = defaultApiFactory, keystoreFactory = keystoreApiFactory) {
1443
- this.program = new import_commander.Command("btcr2").version(`btcr2 ${VERSION}`, "-v, --version", "Output the current version").description("CLI tool for the did:btcr2 method").option("-o, --output <format>", "Output format <json|text>", "text").option("--verbose", "Verbose output", false).option("--quiet", "Suppress non-essential output", false).option("-c, --config <path>", "Path to config file (default: $XDG_CONFIG_HOME/btcr2/config.json)").option("--profile <name>", "Config profile name (default: auto-detected from network)").option("--btc-rest <url>", "Override Bitcoin REST endpoint (Esplora API)").option("--btc-rpc-url <url>", "Override Bitcoin Core RPC endpoint").option("--btc-rpc-user <user>", "Bitcoin Core RPC username").option("--btc-rpc-pass <pass>", "Bitcoin Core RPC password").option("--cas-gateway <url>", "IPFS HTTP gateway for CAS reads").option("--keystore <path>", "Path to the keystore file (default: $XDG_DATA_HOME/btcr2/keystore.json)").option("--passphrase-file <path>", "Read the keystore passphrase from a file (unattended use)").option("--signing-key <ref>", "Key for 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");
1444
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
+ });
1445
2044
  registerCreateCommand(this.program, factory, keystoreFactory, globals);
1446
2045
  registerResolveCommand(this.program, factory, globals);
1447
2046
  registerUpdateCommand(this.program, keystoreFactory, globals);
@@ -1466,6 +2065,9 @@ var DidBtcr2Cli = class {
1466
2065
  }
1467
2066
  }
1468
2067
  };
2068
+ function collectHeader(value, previous) {
2069
+ return [...previous, value];
2070
+ }
1469
2071
  function normalizeArgv(argv) {
1470
2072
  if (argv.length >= 2) return argv;
1471
2073
  if (argv.length === 1) return ["node", argv[0]];
@@ -1488,18 +2090,26 @@ function handleError(error, verbose) {
1488
2090
  CLIError,
1489
2091
  CONFIG_SCHEMA_VERSION,
1490
2092
  DidBtcr2Cli,
2093
+ ENV_RPC_PASS_FILE,
1491
2094
  ENV_VARS,
2095
+ REDACTED,
1492
2096
  SUPPORTED_NETWORKS,
1493
2097
  VERSION,
2098
+ blankToUndef,
1494
2099
  defaultApiFactory,
1495
2100
  defaultConfigPath,
1496
2101
  deriveNetwork,
1497
2102
  formatResult,
1498
2103
  getConfigPath,
2104
+ isSecretKey,
1499
2105
  keystoreApiFactory,
2106
+ parseConfigFileRaw,
2107
+ parseHeaderList,
2108
+ profileNetworkMismatch,
1500
2109
  profileToOverrides,
1501
2110
  readConfigFile,
1502
2111
  readEnvOverrides,
2112
+ redactSecrets,
1503
2113
  registerCompletionCommand,
1504
2114
  registerConfigCommand,
1505
2115
  registerCreateCommand,
@@ -1508,7 +2118,17 @@ function handleError(error, verbose) {
1508
2118
  registerProfileCommand,
1509
2119
  registerResolveCommand,
1510
2120
  registerUpdateCommand,
2121
+ resolveActiveProfile,
2122
+ resolveBroadcastOptions,
2123
+ resolveConnectionConfig,
1511
2124
  resolveDefaultNetwork,
2125
+ resolveEffectiveConfig,
2126
+ resolveKeystorePath,
2127
+ resolveOutputFormat,
2128
+ resolveSecretRef,
2129
+ resolveSigningKeyRef,
2130
+ runDoctor,
2131
+ scrubUrlUserinfo,
1512
2132
  setConfigPath,
1513
2133
  unsetConfigPath,
1514
2134
  writeConfigFile