@did-btcr2/cli 0.15.0 → 0.17.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 +836 -122
- 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 +69 -0
- package/dist/esm/src/commands/init.js.map +1 -0
- package/dist/esm/src/commands/keystore.js +180 -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 +109 -31
- package/dist/esm/src/config.js.map +1 -1
- package/dist/esm/src/keystore/file-key-store.js +388 -32
- package/dist/esm/src/keystore/file-key-store.js.map +1 -1
- package/dist/esm/src/keystore/passphrase.js +53 -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/keystore/session.js +250 -0
- package/dist/esm/src/keystore/session.js.map +1 -0
- package/dist/esm/src/paths.js +71 -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 +11 -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 +102 -10
- package/dist/types/src/keystore/file-key-store.d.ts.map +1 -1
- package/dist/types/src/keystore/passphrase.d.ts +26 -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/keystore/session.d.ts +105 -0
- package/dist/types/src/keystore/session.d.ts.map +1 -0
- package/dist/types/src/paths.d.ts +64 -0
- package/dist/types/src/paths.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +53 -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 +80 -0
- package/src/commands/keystore.ts +232 -0
- package/src/commands/profile.ts +1 -1
- package/src/commands/update.ts +3 -1
- package/src/config.ts +118 -35
- package/src/keystore/file-key-store.ts +498 -43
- package/src/keystore/passphrase.ts +71 -8
- package/src/keystore/paths.ts +6 -19
- package/src/keystore/session.ts +303 -0
- package/src/paths.ts +92 -0
- package/src/types.ts +16 -1
package/src/config.ts
CHANGED
|
@@ -3,14 +3,18 @@ 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, keystoreVerifierId } 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 { readLiveSessionPassphrase } from './keystore/session.js';
|
|
14
|
+
import { defaultConfigPath, defaultSessionPath } from './paths.js';
|
|
15
|
+
import { blankToUndef, SUPPORTED_NETWORKS, type KeystoreProtectionLabel, type NetworkOption, type OutputFormat } from './types.js';
|
|
16
|
+
|
|
17
|
+
export { defaultConfigPath };
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* Endpoint overrides provided via CLI flags, env vars, or config file.
|
|
@@ -39,9 +43,11 @@ export type ConnectionOverrides = {
|
|
|
39
43
|
btcRpcWallet? : string;
|
|
40
44
|
/** Extra Bitcoin Core RPC headers as raw `Key: Value` flag values (repeatable). */
|
|
41
45
|
btcRpcHeader? : string[];
|
|
46
|
+
/** CLI home root from `--home`. Colocates config.json + keystore.json (ADR 079). */
|
|
47
|
+
home? : string;
|
|
42
48
|
config? : string;
|
|
43
49
|
profile? : string;
|
|
44
|
-
/** Keystore file path. Overrides the default
|
|
50
|
+
/** Keystore file path. Overrides the home default `<home>/keystore.json`. */
|
|
45
51
|
keystore? : string;
|
|
46
52
|
/** Path to a file holding the keystore passphrase (for unattended use). */
|
|
47
53
|
passphraseFile? : string;
|
|
@@ -144,6 +150,22 @@ export function writeConfigFile(path: string, mutate: (raw: Record<string, unkno
|
|
|
144
150
|
writeFileAtomic(path, `${JSON.stringify(raw, null, 2)}\n`, 0o600);
|
|
145
151
|
}
|
|
146
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Writes a default config scaffold to `path`: schema version, a `text` output
|
|
155
|
+
* default, and one empty profile per supported network. Shared by `config init`
|
|
156
|
+
* and `btcr2 init` so the seeded config is identical. Writes atomically (file
|
|
157
|
+
* 0600, dir 0700); the caller decides whether to overwrite an existing file.
|
|
158
|
+
*/
|
|
159
|
+
export function writeDefaultConfigFile(path: string): void {
|
|
160
|
+
const scaffold = {
|
|
161
|
+
schemaVersion : CONFIG_SCHEMA_VERSION,
|
|
162
|
+
defaults : { output: 'text' },
|
|
163
|
+
profiles : Object.fromEntries(SUPPORTED_NETWORKS.map(n => [ n, {} ])),
|
|
164
|
+
};
|
|
165
|
+
ensureDir(dirname(path), 0o700);
|
|
166
|
+
writeFileAtomic(path, `${JSON.stringify(scaffold, null, 2)}\n`, 0o600);
|
|
167
|
+
}
|
|
168
|
+
|
|
147
169
|
/** Reads the value at a dotted path (e.g. `profiles.regtest.btc.rest`). */
|
|
148
170
|
export function getConfigPath(config: Record<string, unknown>, path: string): unknown {
|
|
149
171
|
return path.split('.').reduce<unknown>(
|
|
@@ -244,21 +266,6 @@ export function readEnvOverrides(): ConnectionOverrides {
|
|
|
244
266
|
};
|
|
245
267
|
}
|
|
246
268
|
|
|
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
269
|
/**
|
|
263
270
|
* Reads and JSON-parses a config file without applying the schema-version
|
|
264
271
|
* ceiling check. Returns `undefined` only for a genuinely absent file (ENOENT).
|
|
@@ -379,7 +386,7 @@ export function resolveActiveProfile(
|
|
|
379
386
|
* identifier encodes.
|
|
380
387
|
*/
|
|
381
388
|
export function resolveDefaultNetwork(overrides?: ConnectionOverrides): NetworkOption {
|
|
382
|
-
const configPath = overrides?.config ?? defaultConfigPath();
|
|
389
|
+
const configPath = overrides?.config ?? defaultConfigPath(overrides);
|
|
383
390
|
const file = readConfigFile(configPath);
|
|
384
391
|
|
|
385
392
|
const explicit = file?.defaults?.network;
|
|
@@ -407,7 +414,7 @@ export function profileNetworkMismatch(
|
|
|
407
414
|
// command that actually resolves a connection.
|
|
408
415
|
let file: ConfigFile | undefined;
|
|
409
416
|
try {
|
|
410
|
-
file = readConfigFile(overrides?.config ?? defaultConfigPath());
|
|
417
|
+
file = readConfigFile(overrides?.config ?? defaultConfigPath(overrides));
|
|
411
418
|
} catch {
|
|
412
419
|
return undefined;
|
|
413
420
|
}
|
|
@@ -422,13 +429,13 @@ export function profileNetworkMismatch(
|
|
|
422
429
|
* then `'text'`. A malformed config never blocks output resolution (the command's
|
|
423
430
|
* own read path surfaces it); output format falls back to `'text'` instead.
|
|
424
431
|
*/
|
|
425
|
-
export function resolveOutputFormat(options: { output?: string; config?: string }): OutputFormat {
|
|
432
|
+
export function resolveOutputFormat(options: { output?: string; config?: string; home?: string }): OutputFormat {
|
|
426
433
|
const candidates: Array<string | undefined> = [
|
|
427
434
|
blankToUndef(options.output),
|
|
428
435
|
process.env.BTCR2_OUTPUT || undefined,
|
|
429
436
|
];
|
|
430
437
|
try {
|
|
431
|
-
candidates.push(readConfigFile(options.config ?? defaultConfigPath())?.defaults?.output);
|
|
438
|
+
candidates.push(readConfigFile(options.config ?? defaultConfigPath(options))?.defaults?.output);
|
|
432
439
|
} catch {
|
|
433
440
|
// Output format is cosmetic; a broken config is reported by the command
|
|
434
441
|
// itself rather than aborting here (which would block a recovery command).
|
|
@@ -501,7 +508,7 @@ export function resolveConnectionConfig(
|
|
|
501
508
|
// Layer 1: Config file profile (lowest precedence of the three override layers).
|
|
502
509
|
// The active-profile name is resolved through the same shared helper as
|
|
503
510
|
// resolveDefaultNetwork so the two cannot disagree about which profile is live.
|
|
504
|
-
const configPath = overrides?.config ?? defaultConfigPath();
|
|
511
|
+
const configPath = overrides?.config ?? defaultConfigPath(overrides);
|
|
505
512
|
const file = readConfigFile(configPath);
|
|
506
513
|
const { name: activeProfile } = resolveActiveProfile(file, overrides);
|
|
507
514
|
const profileName = activeProfile ?? network;
|
|
@@ -708,7 +715,7 @@ export function resolveBroadcastOptions(
|
|
|
708
715
|
overrides: ConnectionOverrides | undefined,
|
|
709
716
|
flags : { feeRate?: string; changeAddress?: string },
|
|
710
717
|
): BroadcastOptions | undefined {
|
|
711
|
-
const file = readConfigFile(overrides?.config ?? defaultConfigPath());
|
|
718
|
+
const file = readConfigFile(overrides?.config ?? defaultConfigPath(overrides));
|
|
712
719
|
const { name: activeProfile } = resolveActiveProfile(file, overrides);
|
|
713
720
|
const profileBtc = file?.profiles?.[activeProfile ?? network]?.btc;
|
|
714
721
|
|
|
@@ -778,7 +785,7 @@ export interface EffectiveConfig {
|
|
|
778
785
|
* and each `source` is derived by the same precedence order the merge uses.
|
|
779
786
|
*/
|
|
780
787
|
export function resolveEffectiveConfig(network: NetworkOption, overrides?: ConnectionOverrides): EffectiveConfig {
|
|
781
|
-
const file = readConfigFile(overrides?.config ?? defaultConfigPath());
|
|
788
|
+
const file = readConfigFile(overrides?.config ?? defaultConfigPath(overrides));
|
|
782
789
|
const { name: activeProfile } = resolveActiveProfile(file, overrides);
|
|
783
790
|
const profileName = activeProfile ?? network;
|
|
784
791
|
const fileOv = file ? profileToOverrides(file, profileName) : {};
|
|
@@ -941,20 +948,89 @@ export function defaultApiFactory(network?: NetworkOption, overrides?: Connectio
|
|
|
941
948
|
* or opened. The persisted active-key pointer is re-applied (a non-decrypting
|
|
942
949
|
* existence check) so "the active key" survives across invocations.
|
|
943
950
|
*/
|
|
944
|
-
function buildKeystoreKms(overrides?: ConnectionOverrides): KeyManager {
|
|
951
|
+
function buildKeystoreKms(overrides?: ConnectionOverrides, network?: NetworkOption): KeyManager {
|
|
952
|
+
const keystorePath = resolveKeystorePath(overrides);
|
|
953
|
+
const sessionPath = defaultSessionPath(overrides);
|
|
954
|
+
// The network the operation will sign under, known here because the factory
|
|
955
|
+
// receives it. A `bitcoin` operation must not consume a session that was not
|
|
956
|
+
// unlocked with `--allow-mainnet`, so mainnet keeps per-use authentication even
|
|
957
|
+
// while a session is live (ADR 081). Key commands pass no network, so the
|
|
958
|
+
// session serves them as before.
|
|
959
|
+
const isMainnetOperation = network === 'bitcoin';
|
|
945
960
|
return new FileBackedKeyManager({
|
|
946
|
-
path :
|
|
947
|
-
|
|
961
|
+
path : keystorePath,
|
|
962
|
+
// The store decides when to confirm: it passes `confirm: true` only while
|
|
963
|
+
// establishing a fresh keystore's passphrase, so a first-key typo is caught
|
|
964
|
+
// by a second entry (ADR 080). confirm is a no-op for env/file sources.
|
|
965
|
+
//
|
|
966
|
+
// On the non-establishing path, a cached session (ADR 081) is consulted below
|
|
967
|
+
// the env var / --passphrase-file and above the interactive prompt. It is
|
|
968
|
+
// wired ONLY when not confirming, so establishment never consults the session
|
|
969
|
+
// and a first passphrase is always entered fresh and twice. The session is
|
|
970
|
+
// bound to this keystore's verifier, so a rotated passphrase invalidates it.
|
|
971
|
+
getPassphrase : (opts) => acquirePassphrase({
|
|
972
|
+
passphraseFile : overrides?.passphraseFile,
|
|
973
|
+
confirm : opts?.confirm,
|
|
974
|
+
...(opts?.confirm ? {} : {
|
|
975
|
+
beforePrompt : (): string | undefined =>
|
|
976
|
+
readLiveSessionPassphrase(sessionPath, keystorePath, keystoreVerifierId(keystorePath), isMainnetOperation),
|
|
977
|
+
}),
|
|
978
|
+
}),
|
|
948
979
|
});
|
|
949
980
|
}
|
|
950
981
|
|
|
982
|
+
/**
|
|
983
|
+
* The protection mode of the resolved keystore, read without decrypting or
|
|
984
|
+
* prompting: `encrypted`, `dev` (plaintext), or `absent`. Used by `keystore
|
|
985
|
+
* status`, `config path`, and the mainnet guard.
|
|
986
|
+
*/
|
|
987
|
+
export function resolveKeystoreProtection(overrides?: ConnectionOverrides): KeystoreProtectionLabel {
|
|
988
|
+
return keystoreProtection(resolveKeystorePath(overrides));
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Hard-refuses using an unencrypted dev keystore for a mainnet operation (ADR
|
|
993
|
+
* 080). A plaintext key must never sign or seal a `bitcoin` did:btcr2; the check
|
|
994
|
+
* reads only the keystore's protection header, so it never decrypts or prompts.
|
|
995
|
+
* A no-op for every other network and for encrypted/absent keystores.
|
|
996
|
+
*/
|
|
997
|
+
export function assertKeystoreAllowedForNetwork(network: NetworkOption, overrides?: ConnectionOverrides): void {
|
|
998
|
+
if (network !== 'bitcoin') return;
|
|
999
|
+
if (resolveKeystoreProtection(overrides) !== 'dev') return;
|
|
1000
|
+
const path = resolveKeystorePath(overrides);
|
|
1001
|
+
throw new CLIError(
|
|
1002
|
+
`Refusing a mainnet (bitcoin) operation with the unencrypted dev keystore at ${path}. `
|
|
1003
|
+
+ 'Dev keystores hold plaintext keys and are for testnet/regtest throwaway material only. '
|
|
1004
|
+
+ 'Establish an encrypted keystore (btcr2 keystore init) for mainnet keys.',
|
|
1005
|
+
'DEV_KEYSTORE_MAINNET_ERROR',
|
|
1006
|
+
{ path, network },
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
951
1010
|
/**
|
|
952
1011
|
* 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.
|
|
1012
|
+
* profile's `identity.keystore`, else the default `<home>/keystore.json` (ADR
|
|
1013
|
+
* 079). The flag always wins over the profile default and never reads the config.
|
|
1014
|
+
*
|
|
1015
|
+
* A malformed config aborts loudly by default so a keystore-mutating command
|
|
1016
|
+
* never silently reads or writes the wrong store. Pass `lenient: true` only for
|
|
1017
|
+
* diagnostic/recovery commands (`config path`, `keystore status`) that must still
|
|
1018
|
+
* report a path instead of crashing on the very config you ran them to fix; those
|
|
1019
|
+
* fall back to the home default when the profile identity cannot be read.
|
|
955
1020
|
*/
|
|
956
|
-
export function resolveKeystorePath(overrides?: ConnectionOverrides): string {
|
|
957
|
-
|
|
1021
|
+
export function resolveKeystorePath(overrides?: ConnectionOverrides, options?: { lenient?: boolean }): string {
|
|
1022
|
+
// The flag wins outright and short-circuits before any config read. A blank
|
|
1023
|
+
// flag defers to the profile, and a blank profile `identity.keystore` defers to
|
|
1024
|
+
// the default, so neither resolves the keystore to an empty path.
|
|
1025
|
+
const fromFlag = blankToUndef(overrides?.keystore);
|
|
1026
|
+
if (fromFlag) return fromFlag;
|
|
1027
|
+
let identity: { keystore?: string; default?: string } | undefined;
|
|
1028
|
+
try {
|
|
1029
|
+
identity = activeProfileIdentity(overrides);
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
if (!options?.lenient) throw error;
|
|
1032
|
+
}
|
|
1033
|
+
return blankToUndef(identity?.keystore) ?? defaultKeystorePath(overrides);
|
|
958
1034
|
}
|
|
959
1035
|
|
|
960
1036
|
/**
|
|
@@ -963,7 +1039,14 @@ export function resolveKeystorePath(overrides?: ConnectionOverrides): string {
|
|
|
963
1039
|
* selected by `--profile` or the config's `defaults.profile`.
|
|
964
1040
|
*/
|
|
965
1041
|
function activeProfileIdentity(overrides?: ConnectionOverrides): { keystore?: string; default?: string } | undefined {
|
|
966
|
-
|
|
1042
|
+
// Intentionally propagates a malformed-config error rather than swallowing it.
|
|
1043
|
+
// This feeds resolveKeystorePath for keystore-mutating commands (key generate/
|
|
1044
|
+
// import, keystore init/change-passphrase) and the mainnet dev-keystore guard,
|
|
1045
|
+
// so a broken config must abort loudly instead of silently resolving to the
|
|
1046
|
+
// default keystore and stranding key material there. Diagnostic-only commands
|
|
1047
|
+
// opt into a graceful fallback via resolveKeystorePath's `lenient` option; do
|
|
1048
|
+
// not add a try/catch here (it would re-hide the keystore misdirection).
|
|
1049
|
+
const file = readConfigFile(overrides?.config ?? defaultConfigPath(overrides));
|
|
967
1050
|
const { name } = resolveActiveProfile(file, overrides);
|
|
968
1051
|
return name ? file?.profiles?.[name]?.identity : undefined;
|
|
969
1052
|
}
|
|
@@ -987,7 +1070,7 @@ export function resolveSigningKeyRef(overrides?: ConnectionOverrides): string |
|
|
|
987
1070
|
export function keystoreApiFactory(network?: NetworkOption, overrides?: ConnectionOverrides): DidBtcr2Api {
|
|
988
1071
|
return createApi({
|
|
989
1072
|
...resolveConnectionConfig(network, overrides),
|
|
990
|
-
kms : buildKeystoreKms(overrides),
|
|
1073
|
+
kms : buildKeystoreKms(overrides, network),
|
|
991
1074
|
});
|
|
992
1075
|
}
|
|
993
1076
|
|