@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.
- package/README.md +95 -14
- package/dist/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +674 -54
- package/dist/esm/src/cli.js +25 -3
- 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 +33 -6
- 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 +33 -6
- 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 +515 -35
- 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 +228 -6
- 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 +33 -0
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/cli.ts +27 -3
- package/src/commands/config.ts +135 -8
- package/src/commands/create.ts +13 -1
- package/src/commands/deactivate.ts +53 -6
- package/src/commands/profile.ts +5 -3
- package/src/commands/update.ts +53 -6
- package/src/config-schema.ts +178 -0
- package/src/config.ts +693 -43
- package/src/keystore/paths.ts +3 -2
- package/src/output.ts +53 -0
- 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
|
|
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
|
-
|
|
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
|
|
754
|
-
if (
|
|
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
|
|
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
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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:
|
|
780
|
-
username:
|
|
781
|
-
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 } : {}
|
|
782
905
|
};
|
|
783
906
|
}
|
|
784
|
-
const
|
|
785
|
-
|
|
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
|
|
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()
|
|
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:
|
|
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()
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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>
|
|
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
|