@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.
- package/README.md +61 -13
- package/dist/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +637 -54
- package/dist/esm/src/cli.js +23 -2
- package/dist/esm/src/cli.js.map +1 -1
- package/dist/esm/src/commands/config.js +124 -8
- package/dist/esm/src/commands/config.js.map +1 -1
- package/dist/esm/src/commands/create.js +10 -1
- package/dist/esm/src/commands/create.js.map +1 -1
- package/dist/esm/src/commands/deactivate.js +14 -2
- package/dist/esm/src/commands/deactivate.js.map +1 -1
- package/dist/esm/src/commands/profile.js +5 -3
- package/dist/esm/src/commands/profile.js.map +1 -1
- package/dist/esm/src/commands/update.js +14 -2
- package/dist/esm/src/commands/update.js.map +1 -1
- package/dist/esm/src/config-schema.js +149 -0
- package/dist/esm/src/config-schema.js.map +1 -0
- package/dist/esm/src/config.js +506 -39
- package/dist/esm/src/config.js.map +1 -1
- package/dist/esm/src/keystore/paths.js +3 -2
- package/dist/esm/src/keystore/paths.js.map +1 -1
- package/dist/esm/src/output.js +49 -0
- package/dist/esm/src/output.js.map +1 -1
- package/dist/esm/src/types.js +11 -0
- package/dist/esm/src/types.js.map +1 -1
- package/dist/types/src/cli.d.ts.map +1 -1
- package/dist/types/src/commands/config.d.ts.map +1 -1
- package/dist/types/src/commands/create.d.ts.map +1 -1
- package/dist/types/src/commands/deactivate.d.ts.map +1 -1
- package/dist/types/src/commands/profile.d.ts.map +1 -1
- package/dist/types/src/commands/update.d.ts.map +1 -1
- package/dist/types/src/config-schema.d.ts +24 -0
- package/dist/types/src/config-schema.d.ts.map +1 -0
- package/dist/types/src/config.d.ts +219 -5
- package/dist/types/src/config.d.ts.map +1 -1
- package/dist/types/src/keystore/paths.d.ts.map +1 -1
- package/dist/types/src/output.d.ts +22 -0
- package/dist/types/src/output.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +32 -0
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/cli.ts +25 -2
- package/src/commands/config.ts +135 -8
- package/src/commands/create.ts +13 -1
- package/src/commands/deactivate.ts +22 -2
- package/src/commands/profile.ts +5 -3
- package/src/commands/update.ts +22 -2
- package/src/config-schema.ts +178 -0
- package/src/config.ts +676 -43
- package/src/keystore/paths.ts +3 -2
- package/src/output.ts +53 -0
- 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
|
|
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
|
-
|
|
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
|
|
757
|
-
if (
|
|
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
|
|
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
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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:
|
|
784
|
-
username:
|
|
785
|
-
password:
|
|
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 (
|
|
790
|
-
if (
|
|
791
|
-
|
|
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
|
|
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()
|
|
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()
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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>
|
|
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
|