@did-btcr2/cli 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -6
- package/dist/.tsbuildinfo +1 -1
- package/dist/cjs/index.js +563 -117
- package/dist/esm/src/cli.js +7 -4
- package/dist/esm/src/cli.js.map +1 -1
- package/dist/esm/src/commands/config.js +11 -15
- package/dist/esm/src/commands/config.js.map +1 -1
- package/dist/esm/src/commands/create.js +4 -1
- package/dist/esm/src/commands/create.js.map +1 -1
- package/dist/esm/src/commands/deactivate.js +3 -1
- package/dist/esm/src/commands/deactivate.js.map +1 -1
- package/dist/esm/src/commands/index.js +2 -0
- package/dist/esm/src/commands/index.js.map +1 -1
- package/dist/esm/src/commands/init.js +63 -0
- package/dist/esm/src/commands/init.js.map +1 -0
- package/dist/esm/src/commands/keystore.js +81 -0
- package/dist/esm/src/commands/keystore.js.map +1 -0
- package/dist/esm/src/commands/profile.js +1 -1
- package/dist/esm/src/commands/profile.js.map +1 -1
- package/dist/esm/src/commands/update.js +3 -1
- package/dist/esm/src/commands/update.js.map +1 -1
- package/dist/esm/src/config.js +85 -28
- package/dist/esm/src/config.js.map +1 -1
- package/dist/esm/src/keystore/file-key-store.js +340 -32
- package/dist/esm/src/keystore/file-key-store.js.map +1 -1
- package/dist/esm/src/keystore/passphrase.js +40 -10
- package/dist/esm/src/keystore/passphrase.js.map +1 -1
- package/dist/esm/src/keystore/paths.js +6 -18
- package/dist/esm/src/keystore/paths.js.map +1 -1
- package/dist/esm/src/paths.js +59 -0
- package/dist/esm/src/paths.js.map +1 -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/index.d.ts +2 -0
- package/dist/types/src/commands/index.d.ts.map +1 -1
- package/dist/types/src/commands/init.d.ts +13 -0
- package/dist/types/src/commands/init.d.ts.map +1 -0
- package/dist/types/src/commands/keystore.d.ts +10 -0
- package/dist/types/src/commands/keystore.d.ts.map +1 -0
- package/dist/types/src/commands/update.d.ts.map +1 -1
- package/dist/types/src/config.d.ts +38 -14
- package/dist/types/src/config.d.ts.map +1 -1
- package/dist/types/src/keystore/file-key-store.d.ts +86 -10
- package/dist/types/src/keystore/file-key-store.d.ts.map +1 -1
- package/dist/types/src/keystore/passphrase.d.ts +16 -1
- package/dist/types/src/keystore/passphrase.d.ts.map +1 -1
- package/dist/types/src/keystore/paths.d.ts +6 -10
- package/dist/types/src/keystore/paths.d.ts.map +1 -1
- package/dist/types/src/paths.d.ts +54 -0
- package/dist/types/src/paths.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +38 -0
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/cli.ts +8 -3
- package/src/commands/config.ts +12 -14
- package/src/commands/create.ts +4 -1
- package/src/commands/deactivate.ts +3 -1
- package/src/commands/index.ts +2 -0
- package/src/commands/init.ts +74 -0
- package/src/commands/keystore.ts +98 -0
- package/src/commands/profile.ts +1 -1
- package/src/commands/update.ts +3 -1
- package/src/config.ts +93 -32
- package/src/keystore/file-key-store.ts +455 -43
- package/src/keystore/passphrase.ts +48 -8
- package/src/keystore/paths.ts +6 -19
- package/src/paths.ts +79 -0
- package/src/types.ts +13 -1
package/src/config.ts
CHANGED
|
@@ -3,14 +3,17 @@ import type { KeyManager } from '@did-btcr2/key-manager';
|
|
|
3
3
|
import { StaticFeeEstimator } from '@did-btcr2/method';
|
|
4
4
|
import type { BroadcastOptions } from '@did-btcr2/method';
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
6
|
-
import {
|
|
7
|
-
import { dirname, join } from 'node:path';
|
|
6
|
+
import { dirname } from 'node:path';
|
|
8
7
|
import { CLIError } from './error.js';
|
|
9
8
|
import { ensureDir, writeFileAtomic } from './keystore/atomic.js';
|
|
10
9
|
import { FileBackedKeyManager } from './keystore/file-backed-key-manager.js';
|
|
10
|
+
import { keystoreProtection } from './keystore/file-key-store.js';
|
|
11
11
|
import { defaultKeystorePath } from './keystore/paths.js';
|
|
12
12
|
import { acquirePassphrase } from './keystore/passphrase.js';
|
|
13
|
-
import {
|
|
13
|
+
import { defaultConfigPath } from './paths.js';
|
|
14
|
+
import { blankToUndef, SUPPORTED_NETWORKS, type KeystoreProtectionLabel, type NetworkOption, type OutputFormat } from './types.js';
|
|
15
|
+
|
|
16
|
+
export { defaultConfigPath };
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* Endpoint overrides provided via CLI flags, env vars, or config file.
|
|
@@ -39,9 +42,11 @@ export type ConnectionOverrides = {
|
|
|
39
42
|
btcRpcWallet? : string;
|
|
40
43
|
/** Extra Bitcoin Core RPC headers as raw `Key: Value` flag values (repeatable). */
|
|
41
44
|
btcRpcHeader? : string[];
|
|
45
|
+
/** CLI home root from `--home`. Colocates config.json + keystore.json (ADR 079). */
|
|
46
|
+
home? : string;
|
|
42
47
|
config? : string;
|
|
43
48
|
profile? : string;
|
|
44
|
-
/** Keystore file path. Overrides the default
|
|
49
|
+
/** Keystore file path. Overrides the home default `<home>/keystore.json`. */
|
|
45
50
|
keystore? : string;
|
|
46
51
|
/** Path to a file holding the keystore passphrase (for unattended use). */
|
|
47
52
|
passphraseFile? : string;
|
|
@@ -144,6 +149,22 @@ export function writeConfigFile(path: string, mutate: (raw: Record<string, unkno
|
|
|
144
149
|
writeFileAtomic(path, `${JSON.stringify(raw, null, 2)}\n`, 0o600);
|
|
145
150
|
}
|
|
146
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Writes a default config scaffold to `path`: schema version, a `text` output
|
|
154
|
+
* default, and one empty profile per supported network. Shared by `config init`
|
|
155
|
+
* and `btcr2 init` so the seeded config is identical. Writes atomically (file
|
|
156
|
+
* 0600, dir 0700); the caller decides whether to overwrite an existing file.
|
|
157
|
+
*/
|
|
158
|
+
export function writeDefaultConfigFile(path: string): void {
|
|
159
|
+
const scaffold = {
|
|
160
|
+
schemaVersion : CONFIG_SCHEMA_VERSION,
|
|
161
|
+
defaults : { output: 'text' },
|
|
162
|
+
profiles : Object.fromEntries(SUPPORTED_NETWORKS.map(n => [ n, {} ])),
|
|
163
|
+
};
|
|
164
|
+
ensureDir(dirname(path), 0o700);
|
|
165
|
+
writeFileAtomic(path, `${JSON.stringify(scaffold, null, 2)}\n`, 0o600);
|
|
166
|
+
}
|
|
167
|
+
|
|
147
168
|
/** Reads the value at a dotted path (e.g. `profiles.regtest.btc.rest`). */
|
|
148
169
|
export function getConfigPath(config: Record<string, unknown>, path: string): unknown {
|
|
149
170
|
return path.split('.').reduce<unknown>(
|
|
@@ -244,21 +265,6 @@ export function readEnvOverrides(): ConnectionOverrides {
|
|
|
244
265
|
};
|
|
245
266
|
}
|
|
246
267
|
|
|
247
|
-
/**
|
|
248
|
-
* Default config file path following the XDG Base Directory Specification.
|
|
249
|
-
*
|
|
250
|
-
* Resolution order:
|
|
251
|
-
* 1. `$XDG_CONFIG_HOME/btcr2/config.json`
|
|
252
|
-
* 2. `%APPDATA%/btcr2/config.json` (Windows)
|
|
253
|
-
* 3. `~/.config/btcr2/config.json` (fallback)
|
|
254
|
-
*/
|
|
255
|
-
export function defaultConfigPath(): string {
|
|
256
|
-
const base = blankToUndef(process.env.XDG_CONFIG_HOME)
|
|
257
|
-
?? blankToUndef(process.env.APPDATA)
|
|
258
|
-
?? join(homedir(), '.config');
|
|
259
|
-
return join(base, 'btcr2', 'config.json');
|
|
260
|
-
}
|
|
261
|
-
|
|
262
268
|
/**
|
|
263
269
|
* Reads and JSON-parses a config file without applying the schema-version
|
|
264
270
|
* ceiling check. Returns `undefined` only for a genuinely absent file (ENOENT).
|
|
@@ -379,7 +385,7 @@ export function resolveActiveProfile(
|
|
|
379
385
|
* identifier encodes.
|
|
380
386
|
*/
|
|
381
387
|
export function resolveDefaultNetwork(overrides?: ConnectionOverrides): NetworkOption {
|
|
382
|
-
const configPath = overrides?.config ?? defaultConfigPath();
|
|
388
|
+
const configPath = overrides?.config ?? defaultConfigPath(overrides);
|
|
383
389
|
const file = readConfigFile(configPath);
|
|
384
390
|
|
|
385
391
|
const explicit = file?.defaults?.network;
|
|
@@ -407,7 +413,7 @@ export function profileNetworkMismatch(
|
|
|
407
413
|
// command that actually resolves a connection.
|
|
408
414
|
let file: ConfigFile | undefined;
|
|
409
415
|
try {
|
|
410
|
-
file = readConfigFile(overrides?.config ?? defaultConfigPath());
|
|
416
|
+
file = readConfigFile(overrides?.config ?? defaultConfigPath(overrides));
|
|
411
417
|
} catch {
|
|
412
418
|
return undefined;
|
|
413
419
|
}
|
|
@@ -422,13 +428,13 @@ export function profileNetworkMismatch(
|
|
|
422
428
|
* then `'text'`. A malformed config never blocks output resolution (the command's
|
|
423
429
|
* own read path surfaces it); output format falls back to `'text'` instead.
|
|
424
430
|
*/
|
|
425
|
-
export function resolveOutputFormat(options: { output?: string; config?: string }): OutputFormat {
|
|
431
|
+
export function resolveOutputFormat(options: { output?: string; config?: string; home?: string }): OutputFormat {
|
|
426
432
|
const candidates: Array<string | undefined> = [
|
|
427
433
|
blankToUndef(options.output),
|
|
428
434
|
process.env.BTCR2_OUTPUT || undefined,
|
|
429
435
|
];
|
|
430
436
|
try {
|
|
431
|
-
candidates.push(readConfigFile(options.config ?? defaultConfigPath())?.defaults?.output);
|
|
437
|
+
candidates.push(readConfigFile(options.config ?? defaultConfigPath(options))?.defaults?.output);
|
|
432
438
|
} catch {
|
|
433
439
|
// Output format is cosmetic; a broken config is reported by the command
|
|
434
440
|
// itself rather than aborting here (which would block a recovery command).
|
|
@@ -501,7 +507,7 @@ export function resolveConnectionConfig(
|
|
|
501
507
|
// Layer 1: Config file profile (lowest precedence of the three override layers).
|
|
502
508
|
// The active-profile name is resolved through the same shared helper as
|
|
503
509
|
// resolveDefaultNetwork so the two cannot disagree about which profile is live.
|
|
504
|
-
const configPath = overrides?.config ?? defaultConfigPath();
|
|
510
|
+
const configPath = overrides?.config ?? defaultConfigPath(overrides);
|
|
505
511
|
const file = readConfigFile(configPath);
|
|
506
512
|
const { name: activeProfile } = resolveActiveProfile(file, overrides);
|
|
507
513
|
const profileName = activeProfile ?? network;
|
|
@@ -708,7 +714,7 @@ export function resolveBroadcastOptions(
|
|
|
708
714
|
overrides: ConnectionOverrides | undefined,
|
|
709
715
|
flags : { feeRate?: string; changeAddress?: string },
|
|
710
716
|
): BroadcastOptions | undefined {
|
|
711
|
-
const file = readConfigFile(overrides?.config ?? defaultConfigPath());
|
|
717
|
+
const file = readConfigFile(overrides?.config ?? defaultConfigPath(overrides));
|
|
712
718
|
const { name: activeProfile } = resolveActiveProfile(file, overrides);
|
|
713
719
|
const profileBtc = file?.profiles?.[activeProfile ?? network]?.btc;
|
|
714
720
|
|
|
@@ -778,7 +784,7 @@ export interface EffectiveConfig {
|
|
|
778
784
|
* and each `source` is derived by the same precedence order the merge uses.
|
|
779
785
|
*/
|
|
780
786
|
export function resolveEffectiveConfig(network: NetworkOption, overrides?: ConnectionOverrides): EffectiveConfig {
|
|
781
|
-
const file = readConfigFile(overrides?.config ?? defaultConfigPath());
|
|
787
|
+
const file = readConfigFile(overrides?.config ?? defaultConfigPath(overrides));
|
|
782
788
|
const { name: activeProfile } = resolveActiveProfile(file, overrides);
|
|
783
789
|
const profileName = activeProfile ?? network;
|
|
784
790
|
const fileOv = file ? profileToOverrides(file, profileName) : {};
|
|
@@ -944,17 +950,65 @@ export function defaultApiFactory(network?: NetworkOption, overrides?: Connectio
|
|
|
944
950
|
function buildKeystoreKms(overrides?: ConnectionOverrides): KeyManager {
|
|
945
951
|
return new FileBackedKeyManager({
|
|
946
952
|
path : resolveKeystorePath(overrides),
|
|
947
|
-
|
|
953
|
+
// The store decides when to confirm: it passes `confirm: true` only while
|
|
954
|
+
// establishing a fresh keystore's passphrase, so a first-key typo is caught
|
|
955
|
+
// by a second entry (ADR 080). confirm is a no-op for env/file sources.
|
|
956
|
+
getPassphrase : (opts) => acquirePassphrase({ passphraseFile: overrides?.passphraseFile, confirm: opts?.confirm }),
|
|
948
957
|
});
|
|
949
958
|
}
|
|
950
959
|
|
|
960
|
+
/**
|
|
961
|
+
* The protection mode of the resolved keystore, read without decrypting or
|
|
962
|
+
* prompting: `encrypted`, `dev` (plaintext), or `absent`. Used by `keystore
|
|
963
|
+
* status`, `config path`, and the mainnet guard.
|
|
964
|
+
*/
|
|
965
|
+
export function resolveKeystoreProtection(overrides?: ConnectionOverrides): KeystoreProtectionLabel {
|
|
966
|
+
return keystoreProtection(resolveKeystorePath(overrides));
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Hard-refuses using an unencrypted dev keystore for a mainnet operation (ADR
|
|
971
|
+
* 080). A plaintext key must never sign or seal a `bitcoin` did:btcr2; the check
|
|
972
|
+
* reads only the keystore's protection header, so it never decrypts or prompts.
|
|
973
|
+
* A no-op for every other network and for encrypted/absent keystores.
|
|
974
|
+
*/
|
|
975
|
+
export function assertKeystoreAllowedForNetwork(network: NetworkOption, overrides?: ConnectionOverrides): void {
|
|
976
|
+
if (network !== 'bitcoin') return;
|
|
977
|
+
if (resolveKeystoreProtection(overrides) !== 'dev') return;
|
|
978
|
+
const path = resolveKeystorePath(overrides);
|
|
979
|
+
throw new CLIError(
|
|
980
|
+
`Refusing a mainnet (bitcoin) operation with the unencrypted dev keystore at ${path}. `
|
|
981
|
+
+ 'Dev keystores hold plaintext keys and are for testnet/regtest throwaway material only. '
|
|
982
|
+
+ 'Establish an encrypted keystore (btcr2 keystore init) for mainnet keys.',
|
|
983
|
+
'DEV_KEYSTORE_MAINNET_ERROR',
|
|
984
|
+
{ path, network },
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
|
|
951
988
|
/**
|
|
952
989
|
* Resolves the keystore file path: the `--keystore` flag, else the active
|
|
953
|
-
* profile's `identity.keystore`, else the default
|
|
954
|
-
* always wins over the profile default.
|
|
990
|
+
* profile's `identity.keystore`, else the default `<home>/keystore.json` (ADR
|
|
991
|
+
* 079). The flag always wins over the profile default and never reads the config.
|
|
992
|
+
*
|
|
993
|
+
* A malformed config aborts loudly by default so a keystore-mutating command
|
|
994
|
+
* never silently reads or writes the wrong store. Pass `lenient: true` only for
|
|
995
|
+
* diagnostic/recovery commands (`config path`, `keystore status`) that must still
|
|
996
|
+
* report a path instead of crashing on the very config you ran them to fix; those
|
|
997
|
+
* fall back to the home default when the profile identity cannot be read.
|
|
955
998
|
*/
|
|
956
|
-
export function resolveKeystorePath(overrides?: ConnectionOverrides): string {
|
|
957
|
-
|
|
999
|
+
export function resolveKeystorePath(overrides?: ConnectionOverrides, options?: { lenient?: boolean }): string {
|
|
1000
|
+
// The flag wins outright and short-circuits before any config read. A blank
|
|
1001
|
+
// flag defers to the profile, and a blank profile `identity.keystore` defers to
|
|
1002
|
+
// the default, so neither resolves the keystore to an empty path.
|
|
1003
|
+
const fromFlag = blankToUndef(overrides?.keystore);
|
|
1004
|
+
if (fromFlag) return fromFlag;
|
|
1005
|
+
let identity: { keystore?: string; default?: string } | undefined;
|
|
1006
|
+
try {
|
|
1007
|
+
identity = activeProfileIdentity(overrides);
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
if (!options?.lenient) throw error;
|
|
1010
|
+
}
|
|
1011
|
+
return blankToUndef(identity?.keystore) ?? defaultKeystorePath(overrides);
|
|
958
1012
|
}
|
|
959
1013
|
|
|
960
1014
|
/**
|
|
@@ -963,7 +1017,14 @@ export function resolveKeystorePath(overrides?: ConnectionOverrides): string {
|
|
|
963
1017
|
* selected by `--profile` or the config's `defaults.profile`.
|
|
964
1018
|
*/
|
|
965
1019
|
function activeProfileIdentity(overrides?: ConnectionOverrides): { keystore?: string; default?: string } | undefined {
|
|
966
|
-
|
|
1020
|
+
// Intentionally propagates a malformed-config error rather than swallowing it.
|
|
1021
|
+
// This feeds resolveKeystorePath for keystore-mutating commands (key generate/
|
|
1022
|
+
// import, keystore init/change-passphrase) and the mainnet dev-keystore guard,
|
|
1023
|
+
// so a broken config must abort loudly instead of silently resolving to the
|
|
1024
|
+
// default keystore and stranding key material there. Diagnostic-only commands
|
|
1025
|
+
// opt into a graceful fallback via resolveKeystorePath's `lenient` option; do
|
|
1026
|
+
// not add a try/catch here (it would re-hide the keystore misdirection).
|
|
1027
|
+
const file = readConfigFile(overrides?.config ?? defaultConfigPath(overrides));
|
|
967
1028
|
const { name } = resolveActiveProfile(file, overrides);
|
|
968
1029
|
return name ? file?.profiles?.[name]?.identity : undefined;
|
|
969
1030
|
}
|