@did-btcr2/cli 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +33 -8
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/cjs/index.js +421 -177
  4. package/dist/esm/src/cli.js +2 -1
  5. package/dist/esm/src/cli.js.map +1 -1
  6. package/dist/esm/src/commands/create.js +4 -0
  7. package/dist/esm/src/commands/create.js.map +1 -1
  8. package/dist/esm/src/commands/deactivate.js +2 -0
  9. package/dist/esm/src/commands/deactivate.js.map +1 -1
  10. package/dist/esm/src/commands/index.js +1 -0
  11. package/dist/esm/src/commands/index.js.map +1 -1
  12. package/dist/esm/src/commands/init.js +87 -44
  13. package/dist/esm/src/commands/init.js.map +1 -1
  14. package/dist/esm/src/commands/keystore.js +61 -42
  15. package/dist/esm/src/commands/keystore.js.map +1 -1
  16. package/dist/esm/src/commands/quickstart.js +158 -0
  17. package/dist/esm/src/commands/quickstart.js.map +1 -0
  18. package/dist/esm/src/commands/update.js +2 -0
  19. package/dist/esm/src/commands/update.js.map +1 -1
  20. package/dist/esm/src/config.js +66 -0
  21. package/dist/esm/src/config.js.map +1 -1
  22. package/dist/esm/src/hints.js +54 -0
  23. package/dist/esm/src/hints.js.map +1 -0
  24. package/dist/types/src/cli.d.ts.map +1 -1
  25. package/dist/types/src/commands/create.d.ts.map +1 -1
  26. package/dist/types/src/commands/deactivate.d.ts.map +1 -1
  27. package/dist/types/src/commands/index.d.ts +1 -0
  28. package/dist/types/src/commands/index.d.ts.map +1 -1
  29. package/dist/types/src/commands/init.d.ts +52 -5
  30. package/dist/types/src/commands/init.d.ts.map +1 -1
  31. package/dist/types/src/commands/keystore.d.ts +37 -1
  32. package/dist/types/src/commands/keystore.d.ts.map +1 -1
  33. package/dist/types/src/commands/quickstart.d.ts +12 -0
  34. package/dist/types/src/commands/quickstart.d.ts.map +1 -0
  35. package/dist/types/src/commands/update.d.ts.map +1 -1
  36. package/dist/types/src/config.d.ts +35 -0
  37. package/dist/types/src/config.d.ts.map +1 -1
  38. package/dist/types/src/hints.d.ts +24 -0
  39. package/dist/types/src/hints.d.ts.map +1 -0
  40. package/dist/types/src/types.d.ts +17 -0
  41. package/dist/types/src/types.d.ts.map +1 -1
  42. package/package.json +5 -5
  43. package/src/cli.ts +2 -0
  44. package/src/commands/create.ts +4 -0
  45. package/src/commands/deactivate.ts +2 -0
  46. package/src/commands/index.ts +1 -0
  47. package/src/commands/init.ts +146 -54
  48. package/src/commands/keystore.ts +95 -55
  49. package/src/commands/quickstart.ts +209 -0
  50. package/src/commands/update.ts +2 -0
  51. package/src/config.ts +75 -0
  52. package/src/hints.ts +51 -0
  53. package/src/types.ts +12 -1
package/dist/cjs/index.js CHANGED
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  SUPPORTED_NETWORKS: () => SUPPORTED_NETWORKS,
30
30
  VERSION: () => VERSION,
31
31
  assertKeystoreAllowedForNetwork: () => assertKeystoreAllowedForNetwork,
32
+ assertSupportedNetwork: () => assertSupportedNetwork,
32
33
  blankToUndef: () => blankToUndef,
33
34
  defaultApiFactory: () => defaultApiFactory,
34
35
  defaultConfigPath: () => defaultConfigPath,
@@ -39,9 +40,11 @@ __export(index_exports, {
39
40
  keystoreApiFactory: () => keystoreApiFactory,
40
41
  parseConfigFileRaw: () => parseConfigFileRaw,
41
42
  parseHeaderList: () => parseHeaderList,
43
+ persistDefaultNetwork: () => persistDefaultNetwork,
42
44
  profileNetworkMismatch: () => profileNetworkMismatch,
43
45
  profileToOverrides: () => profileToOverrides,
44
46
  readConfigFile: () => readConfigFile,
47
+ readConfiguredDefaultNetwork: () => readConfiguredDefaultNetwork,
45
48
  readEnvOverrides: () => readEnvOverrides,
46
49
  redactSecrets: () => redactSecrets,
47
50
  registerCompletionCommand: () => registerCompletionCommand,
@@ -52,6 +55,7 @@ __export(index_exports, {
52
55
  registerKeyCommand: () => registerKeyCommand,
53
56
  registerKeystoreCommand: () => registerKeystoreCommand,
54
57
  registerProfileCommand: () => registerProfileCommand,
58
+ registerQuickstartCommand: () => registerQuickstartCommand,
55
59
  registerResolveCommand: () => registerResolveCommand,
56
60
  registerUpdateCommand: () => registerUpdateCommand,
57
61
  resolveActiveProfile: () => resolveActiveProfile,
@@ -1310,6 +1314,45 @@ function resolveDefaultNetwork(overrides) {
1310
1314
  if (network) return network;
1311
1315
  return "regtest";
1312
1316
  }
1317
+ function readConfiguredDefaultNetwork(overrides) {
1318
+ const configPath = overrides?.config ?? defaultConfigPath(overrides);
1319
+ let raw;
1320
+ try {
1321
+ raw = parseConfigFileRaw(configPath);
1322
+ } catch {
1323
+ return void 0;
1324
+ }
1325
+ const value = raw ? getConfigPath(raw, "defaults.network") : void 0;
1326
+ return typeof value === "string" && SUPPORTED_NETWORKS.includes(value) ? value : void 0;
1327
+ }
1328
+ function persistDefaultNetwork(configPath, opts) {
1329
+ const raw = parseConfigFileRaw(configPath);
1330
+ const rawValue = raw ? getConfigPath(raw, "defaults.network") : void 0;
1331
+ const rawNetwork = typeof rawValue === "string" && SUPPORTED_NETWORKS.includes(rawValue) ? rawValue : void 0;
1332
+ if (opts.explicit) {
1333
+ if (opts.explicit !== rawNetwork) {
1334
+ writeConfigFile(configPath, (r) => setConfigPath(r, "defaults.network", opts.explicit));
1335
+ return { network: opts.explicit, wrote: true };
1336
+ }
1337
+ return { network: opts.explicit, wrote: false };
1338
+ }
1339
+ if (rawNetwork) return { network: rawNetwork, wrote: false };
1340
+ if (opts.fallback) {
1341
+ writeConfigFile(configPath, (r) => setConfigPath(r, "defaults.network", opts.fallback));
1342
+ return { network: opts.fallback, wrote: true };
1343
+ }
1344
+ return { network: resolveDefaultNetwork(opts.overrides), wrote: false };
1345
+ }
1346
+ function assertSupportedNetwork(value) {
1347
+ if (!SUPPORTED_NETWORKS.includes(value)) {
1348
+ throw new CLIError(
1349
+ `Invalid network "${value}". Must be one of ${SUPPORTED_NETWORKS.join(", ")}.`,
1350
+ "INVALID_ARGUMENT_ERROR",
1351
+ { network: value }
1352
+ );
1353
+ }
1354
+ return value;
1355
+ }
1313
1356
  function profileNetworkMismatch(network, overrides) {
1314
1357
  let file;
1315
1358
  try {
@@ -1682,51 +1725,384 @@ function formatResult(result, options) {
1682
1725
  }
1683
1726
 
1684
1727
  // src/commands/init.ts
1728
+ function runInit(g, options = {}) {
1729
+ const home = resolveHome(g);
1730
+ const configPath = g.config ?? defaultConfigPath(g);
1731
+ const keystorePath = resolveKeystorePath(g);
1732
+ ensureDir(home, 448);
1733
+ const created = [];
1734
+ if (!(0, import_node_fs7.existsSync)(configPath) || options.force) {
1735
+ writeDefaultConfigFile(configPath);
1736
+ created.push("config");
1737
+ }
1738
+ const keystoreExists = (0, import_node_fs7.existsSync)(keystorePath);
1739
+ if (keystoreExists && options.force && !g.quiet) {
1740
+ process.stderr.write(
1741
+ `note: a keystore already exists at ${keystorePath} and was left intact. To re-establish it (discarding its keys), run "btcr2 keystore init --force".
1742
+ `
1743
+ );
1744
+ }
1745
+ let establishedPassphrase;
1746
+ if (!keystoreExists) {
1747
+ if (options.dev && !g.quiet) {
1748
+ process.stderr.write(
1749
+ "warning: establishing an UNENCRYPTED dev keystore. Keys are stored in plaintext. Use it only for disposable testnet material; mainnet operations will be refused.\n"
1750
+ );
1751
+ }
1752
+ initKeystore(keystorePath, {
1753
+ protection: options.dev ? "none" : "passphrase",
1754
+ getPassphrase: (opts) => {
1755
+ const passphrase = acquirePassphrase({
1756
+ passphraseFile: g.passphraseFile,
1757
+ confirm: opts?.confirm,
1758
+ prompt: "New keystore passphrase: "
1759
+ });
1760
+ if (options.captureEstablishedPassphrase && !options.dev) establishedPassphrase = passphrase;
1761
+ return passphrase;
1762
+ }
1763
+ });
1764
+ clearSession(defaultSessionPath(g));
1765
+ created.push("keystore");
1766
+ }
1767
+ const { network } = persistDefaultNetwork(configPath, {
1768
+ explicit: options.network,
1769
+ fallback: options.fallbackNetwork,
1770
+ overrides: g
1771
+ });
1772
+ const protection = keystoreSummary(keystorePath).protection;
1773
+ return { home, config: configPath, keystore: keystorePath, network, created, protection, establishedPassphrase };
1774
+ }
1685
1775
  function registerInitCommand(program, globals) {
1686
1776
  const print = (result) => console.log(formatResult(result, globals()));
1687
- program.command("init").description("Set up the btcr2 home: create the directory, a default config, and establish the keystore.").option("--dev", "Establish an UNENCRYPTED dev keystore: plaintext keys, no passphrase. Testnet only.", false).option("--force", "Re-create the config and keystore even if they already exist.", false).action((options) => {
1777
+ program.command("init").description("Set up the btcr2 home: create the directory, a default config, and establish the keystore.").option(
1778
+ "-n, --network <network>",
1779
+ "Bitcoin network to record as defaults.network <bitcoin|testnet3|testnet4|signet|mutinynet|regtest>"
1780
+ ).option("--dev", "Establish an UNENCRYPTED dev keystore: plaintext keys, no passphrase. Testnet only.", false).option("--force", "Re-create the config even if it already exists (never the keystore).", false).action((options) => {
1688
1781
  const g = globals();
1689
- const home = resolveHome(g);
1690
- const configPath = g.config ?? defaultConfigPath(g);
1691
- const keystorePath = resolveKeystorePath(g);
1692
- ensureDir(home, 448);
1693
- const created = [];
1694
- if (!(0, import_node_fs7.existsSync)(configPath) || options.force) {
1695
- writeDefaultConfigFile(configPath);
1696
- created.push("config");
1697
- }
1698
- const keystoreExists = (0, import_node_fs7.existsSync)(keystorePath);
1699
- if (keystoreExists && options.force && !g.quiet) {
1700
- process.stderr.write(
1701
- `note: a keystore already exists at ${keystorePath} and was left intact. To re-establish it (discarding its keys), run "btcr2 keystore init --force".
1702
- `
1782
+ const network = options.network ? assertSupportedNetwork(options.network) : void 0;
1783
+ const result = runInit(g, { dev: options.dev, force: options.force, network });
1784
+ print({
1785
+ action: "init",
1786
+ data: {
1787
+ home: result.home,
1788
+ config: result.config,
1789
+ keystore: result.keystore,
1790
+ network: result.network,
1791
+ created: result.created,
1792
+ protection: result.protection
1793
+ }
1794
+ });
1795
+ if (!g.quiet && g.output !== "json") {
1796
+ process.stderr.write(`btcr2 home ready at ${result.home} on ${result.network}. Next: btcr2 key generate --set-active
1797
+ `);
1798
+ }
1799
+ });
1800
+ }
1801
+
1802
+ // src/commands/quickstart.ts
1803
+ var import_api2 = require("@did-btcr2/api");
1804
+
1805
+ // src/commands/keystore.ts
1806
+ var import_node_fs8 = require("fs");
1807
+ function registerKeystoreCommand(program, globals) {
1808
+ const keystore = program.command("keystore").description("Establish, inspect, re-key, and unlock the keystore.");
1809
+ const print = (result) => console.log(formatResult(result, globals()));
1810
+ keystore.command("init").description("Establish the keystore (encrypted by default). Prompts for a passphrase and confirms it.").option("--dev", "Create an UNENCRYPTED dev keystore: plaintext keys, no passphrase. Testnet only; mainnet operations are refused.", false).option("--force", "Re-establish even if a keystore already exists (discards its keys).", false).action((options) => {
1811
+ const g = globals();
1812
+ const path = resolveKeystorePath(g);
1813
+ if ((0, import_node_fs8.existsSync)(path) && !options.force) {
1814
+ throw new CLIError(
1815
+ `A keystore already exists at ${path}. Use --force to re-establish it (this discards its keys).`,
1816
+ "INVALID_ARGUMENT_ERROR",
1817
+ { path }
1703
1818
  );
1704
1819
  }
1705
- if (!keystoreExists) {
1706
- if (options.dev && !g.quiet) {
1820
+ if ((0, import_node_fs8.existsSync)(path) && options.force && !g.quiet) {
1821
+ const { keyCount } = keystoreSummary(path);
1822
+ if (keyCount > 0) {
1707
1823
  process.stderr.write(
1708
- "warning: establishing an UNENCRYPTED dev keystore. Keys are stored in plaintext. Use it only for disposable testnet material; mainnet operations will be refused.\n"
1824
+ `warning: re-establishing the keystore at ${path} permanently discards its ${keyCount} existing key(s).
1825
+ `
1709
1826
  );
1710
1827
  }
1711
- initKeystore(keystorePath, {
1712
- protection: options.dev ? "none" : "passphrase",
1713
- getPassphrase: (opts) => acquirePassphrase({ passphraseFile: g.passphraseFile, confirm: opts?.confirm, prompt: "New keystore passphrase: " })
1714
- });
1715
- clearSession(defaultSessionPath(g));
1716
- created.push("keystore");
1717
1828
  }
1718
- const protection = keystoreSummary(keystorePath).protection;
1719
- print({ action: "init", data: { home, config: configPath, keystore: keystorePath, created, protection } });
1829
+ if (options.dev && !g.quiet) {
1830
+ process.stderr.write(
1831
+ "warning: creating an UNENCRYPTED dev keystore. Keys are stored in plaintext. Use it only for disposable testnet material; mainnet operations will be refused.\n"
1832
+ );
1833
+ }
1834
+ initKeystore(path, {
1835
+ protection: options.dev ? "none" : "passphrase",
1836
+ getPassphrase: (opts) => acquirePassphrase({ passphraseFile: g.passphraseFile, confirm: opts?.confirm, prompt: "New keystore passphrase: " })
1837
+ });
1838
+ clearSession(defaultSessionPath(g));
1839
+ print({ action: "keystore-init", data: { path, protection: options.dev ? "dev" : "encrypted" } });
1840
+ });
1841
+ keystore.command("status").description("Show the keystore path, protection mode, key count, and session state. Never decrypts or prompts.").action(() => {
1842
+ const g = globals();
1843
+ const path = resolveKeystorePath(g, { lenient: true });
1844
+ const summary = keystoreSummary(path);
1845
+ const session = readSessionStatus(defaultSessionPath(g), path, keystoreVerifierId(path));
1846
+ if (summary.protection === "dev" && !g.quiet && g.output !== "json") {
1847
+ process.stderr.write("warning: this is an UNENCRYPTED dev keystore; keys are stored in plaintext.\n");
1848
+ }
1849
+ print({ action: "keystore-status", data: { path, ...summary, session } });
1850
+ });
1851
+ keystore.command("change-passphrase").alias("passwd").description("Change the keystore passphrase, re-sealing every key under the new one. Encrypted keystores only.").action(() => {
1852
+ const g = globals();
1853
+ const path = resolveKeystorePath(g);
1854
+ const summary = keystoreSummary(path);
1855
+ if (summary.protection === "absent") {
1856
+ throw new CLIError(`No keystore at ${path}. Run "btcr2 keystore init" first.`, "INVALID_ARGUMENT_ERROR", { path });
1857
+ }
1858
+ if (summary.protection === "dev") {
1859
+ throw new CLIError(
1860
+ `The keystore at ${path} is an unencrypted dev keystore; there is no passphrase to change.`,
1861
+ "INVALID_ARGUMENT_ERROR",
1862
+ { path }
1863
+ );
1864
+ }
1865
+ const oldPassphrase = acquirePassphrase({ passphraseFile: g.passphraseFile, prompt: "Current keystore passphrase: " });
1866
+ const newPassphrase = acquirePassphrase({ forcePrompt: true, confirm: true, prompt: "New keystore passphrase: " });
1867
+ const rekeyed = changeKeystorePassphrase(path, oldPassphrase, newPassphrase);
1868
+ clearSession(defaultSessionPath(g));
1869
+ print({ action: "keystore-change-passphrase", data: { path, rekeyed } });
1870
+ });
1871
+ keystore.command("unlock").description("Cache the keystore passphrase for a session so later commands do not re-prompt (ADR 081).").option("--ttl <duration>", `Session lifetime: bare seconds or an s/m/h suffix (default 1h, max 24h). Also $${ENV_KEYSTORE_TTL}.`).option("--allow-mainnet", "Permit unlocking when the active network is mainnet (bitcoin); this suspends per-use passphrase auth for the session.", false).action((options) => {
1872
+ const g = globals();
1873
+ const path = resolveKeystorePath(g);
1874
+ const ttlMs = resolveSessionTtl(options.ttl);
1875
+ const session = unlockSession({
1876
+ g,
1877
+ keystorePath: path,
1878
+ network: resolveDefaultNetwork(g),
1879
+ allowMainnet: !!options.allowMainnet,
1880
+ ttlMs
1881
+ });
1882
+ print({ action: "keystore-unlock", data: { keystore: path, expiresAt: session.expiresAt, ttlSeconds: session.ttlSeconds } });
1883
+ });
1884
+ keystore.command("lock").description("Revoke the cached session so later commands prompt for the passphrase again (ADR 081).").action(() => {
1885
+ const g = globals();
1886
+ const sessionPath = defaultSessionPath(g);
1887
+ const cleared = clearSession(sessionPath);
1888
+ print({ action: "keystore-lock", data: { path: sessionPath, cleared } });
1889
+ });
1890
+ }
1891
+ function unlockSession(input) {
1892
+ const { g, keystorePath: path, network, allowMainnet, ttlMs } = input;
1893
+ const summary = keystoreSummary(path);
1894
+ if (summary.protection === "absent") {
1895
+ throw new CLIError(`No keystore at ${path}. Run "btcr2 init" or "btcr2 keystore init" first.`, "INVALID_ARGUMENT_ERROR", { path });
1896
+ }
1897
+ if (summary.protection === "dev") {
1898
+ throw new CLIError(
1899
+ `The keystore at ${path} is an unencrypted dev keystore; it has no passphrase to cache, so no unlock is needed.`,
1900
+ "INVALID_ARGUMENT_ERROR",
1901
+ { path }
1902
+ );
1903
+ }
1904
+ if (!summary.established) {
1905
+ throw new CLIError(
1906
+ `The keystore at ${path} has no passphrase established yet. Establish one with "btcr2 keystore init" or the first "btcr2 key generate".`,
1907
+ "INVALID_ARGUMENT_ERROR",
1908
+ { path }
1909
+ );
1910
+ }
1911
+ if (!allowMainnet && network === "bitcoin") {
1912
+ throw new CLIError(
1913
+ "Refusing to unlock for a mainnet (bitcoin) context: caching the passphrase suspends per-use authentication for the session. Pass --allow-mainnet to override, or keep signing mainnet updates with a per-use passphrase prompt.",
1914
+ "MAINNET_UNLOCK_REFUSED_ERROR",
1915
+ { path }
1916
+ );
1917
+ }
1918
+ const passphrase = input.passphrase ?? acquirePassphrase({ passphraseFile: g.passphraseFile, prompt: "Keystore passphrase: " });
1919
+ if (!verifyKeystorePassphrase(path, passphrase)) {
1920
+ throw new CLIError(`Incorrect passphrase for the keystore at ${path}; no session was created.`, "DECRYPT_ERROR", { path });
1921
+ }
1922
+ const verifierId = keystoreVerifierId(path);
1923
+ if (!verifierId) {
1924
+ throw new CLIError(`The keystore at ${path} has no verifier to bind a session to.`, "INVALID_ARGUMENT_ERROR", { path });
1925
+ }
1926
+ return writeSession(defaultSessionPath(g), {
1927
+ keystorePath: path,
1928
+ verifierId,
1929
+ passphrase,
1930
+ ttlMs,
1931
+ allowMainnet
1932
+ });
1933
+ }
1934
+ function resolveSessionTtl(flag) {
1935
+ const fromFlag = blankToUndef(flag);
1936
+ const raw = fromFlag ?? blankToUndef(process.env[ENV_KEYSTORE_TTL]);
1937
+ if (raw === void 0) return DEFAULT_SESSION_TTL_MS;
1938
+ const source = fromFlag !== void 0 ? "--ttl" : `$${ENV_KEYSTORE_TTL}`;
1939
+ const ms = parseTtlToMs(raw);
1940
+ if (ms === void 0 || ms <= 0) {
1941
+ throw new CLIError(
1942
+ `Invalid ${source} "${raw}": expected seconds or a value with an s/m/h suffix, e.g. 3600, 45m, or 2h.`,
1943
+ "INVALID_ARGUMENT_ERROR",
1944
+ { value: raw, source }
1945
+ );
1946
+ }
1947
+ if (ms > MAX_SESSION_TTL_MS) {
1948
+ throw new CLIError(
1949
+ `${source} "${raw}" exceeds the 24h maximum for a cached passphrase.`,
1950
+ "INVALID_ARGUMENT_ERROR",
1951
+ { value: raw, source }
1952
+ );
1953
+ }
1954
+ return ms;
1955
+ }
1956
+
1957
+ // src/commands/quickstart.ts
1958
+ var QUICKSTART_DEFAULT_NETWORK = "mutinynet";
1959
+ function registerQuickstartCommand(program, globals) {
1960
+ const print = (result) => console.log(formatResult(result, globals()));
1961
+ program.command("quickstart").description("One-command onboarding: create the home + config + keystore, record the network, and (optionally) cache the session and probe endpoints.").option(
1962
+ "-n, --network <network>",
1963
+ "Bitcoin network to set up <bitcoin|testnet3|testnet4|signet|mutinynet|regtest> (default: mutinynet)"
1964
+ ).option("--dev", "Establish an UNENCRYPTED dev keystore: plaintext keys, no passphrase. Testnet only.", false).option("--unlock", "Cache the passphrase for the session so later commands do not re-prompt (ADR 081).", false).option("--ttl <duration>", `Session lifetime with --unlock: bare seconds or an s/m/h suffix (default 1h, max 24h). Also $${ENV_KEYSTORE_TTL}.`).option("--no-doctor", "Skip the endpoint reachability probe.").option("--allow-mainnet", "Permit -n bitcoin (records mainnet as the default; dev keystores are still refused).", false).option("--force", "Re-create the config even if it already exists (never the keystore).", false).action(async (options) => {
1965
+ const g = globals();
1966
+ const explicit = options.network ? assertSupportedNetwork(options.network) : void 0;
1967
+ const network = explicit ?? readConfiguredDefaultNetwork(g) ?? QUICKSTART_DEFAULT_NETWORK;
1968
+ if (network === "bitcoin") {
1969
+ if (options.dev) {
1970
+ throw new CLIError(
1971
+ "Refusing to quickstart a mainnet (bitcoin) dev keystore: dev keystores store keys in plaintext and never operate on mainnet. Drop --dev, or choose a testnet with -n.",
1972
+ "MAINNET_QUICKSTART_REFUSED_ERROR",
1973
+ { network }
1974
+ );
1975
+ }
1976
+ if (!options.allowMainnet) {
1977
+ throw new CLIError(
1978
+ "Refusing to quickstart on mainnet (bitcoin) without --allow-mainnet. Pass --allow-mainnet to record mainnet as the default, or choose a testnet with -n (the default is mutinynet).",
1979
+ "MAINNET_QUICKSTART_REFUSED_ERROR",
1980
+ { network }
1981
+ );
1982
+ }
1983
+ }
1984
+ const init = runInit(g, {
1985
+ dev: options.dev,
1986
+ force: options.force,
1987
+ network: explicit,
1988
+ fallbackNetwork: explicit ? void 0 : QUICKSTART_DEFAULT_NETWORK,
1989
+ captureEstablishedPassphrase: !!options.unlock && !options.dev
1990
+ });
1991
+ let unlocked = false;
1992
+ let session;
1993
+ if (options.unlock && !options.dev) {
1994
+ const outcome = cacheSession(g, init, options.ttl, !!options.allowMainnet);
1995
+ unlocked = outcome.unlocked;
1996
+ session = outcome.session;
1997
+ }
1998
+ let doctor;
1999
+ if (options.doctor) {
2000
+ doctor = await runDoctor(init.network, g);
2001
+ }
2002
+ print({
2003
+ action: "quickstart",
2004
+ data: {
2005
+ home: init.home,
2006
+ config: init.config,
2007
+ keystore: init.keystore,
2008
+ network: init.network,
2009
+ created: init.created,
2010
+ protection: init.protection,
2011
+ unlocked,
2012
+ ...session ? { session } : {},
2013
+ ...doctor ? { doctor } : {}
2014
+ }
2015
+ });
1720
2016
  if (!g.quiet && g.output !== "json") {
1721
- process.stderr.write(`btcr2 home ready at ${home}. Next: btcr2 key generate --set-active
1722
- `);
2017
+ printNextSteps(init, unlocked, session, doctor);
1723
2018
  }
1724
2019
  });
1725
2020
  }
2021
+ function cacheSession(g, init, ttlFlag, allowMainnet) {
2022
+ const ttlMs = resolveSessionTtl(ttlFlag);
2023
+ const freshlyEstablished = init.created.includes("keystore");
2024
+ if (!freshlyEstablished) {
2025
+ const status = readSessionStatus(defaultSessionPath(g), init.keystore, keystoreVerifierId(init.keystore));
2026
+ if (status.active && status.expiresAt !== void 0) {
2027
+ return { unlocked: true, session: { expiresAt: status.expiresAt, ttlSeconds: status.secondsRemaining ?? 0 } };
2028
+ }
2029
+ }
2030
+ try {
2031
+ const written = unlockSession({
2032
+ g,
2033
+ keystorePath: init.keystore,
2034
+ network: init.network,
2035
+ allowMainnet,
2036
+ ttlMs,
2037
+ // Reuse the establish-time passphrase on a fresh keystore: no second prompt.
2038
+ passphrase: freshlyEstablished ? init.establishedPassphrase : void 0
2039
+ });
2040
+ return { unlocked: true, session: { expiresAt: written.expiresAt, ttlSeconds: written.ttlSeconds } };
2041
+ } catch (error) {
2042
+ const type = error.type;
2043
+ if (!freshlyEstablished && !process.stdin.isTTY && type === "PASSPHRASE_REQUIRED_ERROR") {
2044
+ if (!g.quiet) {
2045
+ process.stderr.write(
2046
+ 'note: no passphrase source and no terminal; skipped caching the session. Run "btcr2 keystore unlock" later to cache it.\n'
2047
+ );
2048
+ }
2049
+ return { unlocked: false };
2050
+ }
2051
+ throw error;
2052
+ }
2053
+ }
2054
+ function printNextSteps(init, unlocked, session, doctor) {
2055
+ const lines = [`btcr2 home ready at ${init.home} on ${init.network}.`];
2056
+ if (unlocked && session) {
2057
+ lines.push(`Session cached until ${new Date(session.expiresAt).toISOString()}; signing will not re-prompt until it expires.`);
2058
+ }
2059
+ if (init.protection === "dev") {
2060
+ lines.push("Dev keystore: keys are stored in plaintext; mainnet operations are refused.");
2061
+ }
2062
+ if (doctor && doctor.checks.some((c) => !c.ok)) {
2063
+ lines.push('Warning: one or more endpoints were unreachable (see the doctor report). Re-run "btcr2 config doctor" for detail.');
2064
+ }
2065
+ lines.push("Next: btcr2 key generate --name demo --set-active");
2066
+ const faucet = (0, import_api2.faucetUrl)(init.network);
2067
+ if (faucet) lines.push(`Faucet (fund your beacon after "btcr2 create"): ${faucet}`);
2068
+ process.stderr.write(`${lines.join("\n")}
2069
+ `);
2070
+ }
1726
2071
 
1727
2072
  // src/commands/create.ts
1728
2073
  var import_utils4 = require("@noble/hashes/utils.js");
1729
2074
 
2075
+ // src/hints.ts
2076
+ var import_api3 = require("@did-btcr2/api");
2077
+ var import_method2 = require("@did-btcr2/method");
2078
+ function printCreateFundingHint(g, network, did) {
2079
+ if (g.quiet || g.output === "json") return;
2080
+ const faucet = (0, import_api3.faucetUrl)(network);
2081
+ if (!faucet) return;
2082
+ let beaconAddress;
2083
+ try {
2084
+ const { serviceEndpoint } = import_method2.BeaconUtils.createBeaconService(did, "p2wpkh", "SingletonBeacon");
2085
+ beaconAddress = serviceEndpoint.replace(/^bitcoin:/, "");
2086
+ } catch {
2087
+ return;
2088
+ }
2089
+ const explorer = (0, import_api3.explorerAddressUrl)(network, beaconAddress);
2090
+ const lines = [
2091
+ "Fund the initial beacon to anchor updates:",
2092
+ ` Beacon: ${beaconAddress}`,
2093
+ ` Faucet: ${faucet}`
2094
+ ];
2095
+ if (explorer) lines.push(` Explorer: ${explorer}`);
2096
+ process.stderr.write(`${lines.join("\n")}
2097
+ `);
2098
+ }
2099
+ function printWatchHint(g, network, txid) {
2100
+ if (g.quiet || g.output === "json") return;
2101
+ const url = (0, import_api3.explorerTxUrl)(network, txid);
2102
+ if (url) process.stderr.write(`Watch: ${url}
2103
+ `);
2104
+ }
2105
+
1730
2106
  // src/keystore/resolve-key-ref.ts
1731
2107
  function fingerprintOf(id) {
1732
2108
  return /^urn:kms:secp256k1:([0-9a-f]{32})$/.exec(id)?.[1];
@@ -1825,6 +2201,7 @@ function registerCreateCommand(program, factory, keystoreFactory, globals) {
1825
2201
  const genesisBytes = parseGenesisBytes(options.bytes, "k");
1826
2202
  const did2 = factory().createDid("deterministic", genesisBytes, { network });
1827
2203
  print({ action: "create", data: did2 });
2204
+ printCreateFundingHint(g, network, did2);
1828
2205
  return;
1829
2206
  }
1830
2207
  if (signingKey) {
@@ -1836,6 +2213,7 @@ function registerCreateCommand(program, factory, keystoreFactory, globals) {
1836
2213
  { action: "create", data: did2, keyId: keyId2, publicKey: (0, import_utils4.bytesToHex)(publicKey2) },
1837
2214
  `Using stored key ${keyId2}.`
1838
2215
  );
2216
+ printCreateFundingHint(g, network, did2);
1839
2217
  return;
1840
2218
  }
1841
2219
  assertKeystoreAllowedForNetwork(network, overrides);
@@ -1846,6 +2224,7 @@ function registerCreateCommand(program, factory, keystoreFactory, globals) {
1846
2224
  { action: "create", data: did, keyId, publicKey },
1847
2225
  `Generated and stored key ${keyId} (now the active key).`
1848
2226
  );
2227
+ printCreateFundingHint(g, network, did);
1849
2228
  });
1850
2229
  }
1851
2230
  function overridesFromGlobals(g) {
@@ -1891,7 +2270,7 @@ function parseGenesisBytes(hex, type) {
1891
2270
  }
1892
2271
 
1893
2272
  // src/commands/resolve.ts
1894
- var import_api2 = require("@did-btcr2/api");
2273
+ var import_api4 = require("@did-btcr2/api");
1895
2274
  var import_promises = require("fs/promises");
1896
2275
  function registerResolveCommand(program, factory, globals) {
1897
2276
  program.command("resolve").alias("read").description("Resolve the DID document of the identifier.").requiredOption("-i, --identifier <identifier>", "did:btcr2 identifier").option("-r, --resolution-options <json>", "JSON string containing resolution options").option("-p, --resolution-options-path <path>", "Path to a JSON file containing resolution options").action(async (options) => {
@@ -1904,7 +2283,7 @@ function registerResolveCommand(program, factory, globals) {
1904
2283
  });
1905
2284
  }
1906
2285
  async function validateResolveOptions(options) {
1907
- import_api2.Identifier.decode(options.identifier);
2286
+ import_api4.Identifier.decode(options.identifier);
1908
2287
  let resolutionOptions = void 0;
1909
2288
  if (options.resolutionOptions) {
1910
2289
  try {
@@ -2006,6 +2385,7 @@ function registerUpdateCommand(program, factory, globals) {
2006
2385
  ...broadcastOptions ? { broadcastOptions } : {}
2007
2386
  });
2008
2387
  console.log(formatResult({ action: "update", data }, globals()));
2388
+ printWatchHint(globals(), network, data.txid);
2009
2389
  });
2010
2390
  }
2011
2391
  function parsePublishToCasMode(value) {
@@ -2104,6 +2484,7 @@ function registerDeactivateCommand(program, factory, globals) {
2104
2484
  ...broadcastOptions ? { broadcastOptions } : {}
2105
2485
  });
2106
2486
  console.log(formatResult({ action: "deactivate", data }, globals()));
2487
+ printWatchHint(globals(), network, data.txid);
2107
2488
  });
2108
2489
  }
2109
2490
  function parsePublishToCasMode2(value) {
@@ -2133,7 +2514,7 @@ function parseJsonArg2(flagName) {
2133
2514
  // src/commands/key.ts
2134
2515
  var import_keypair = require("@did-btcr2/keypair");
2135
2516
  var import_utils5 = require("@noble/hashes/utils.js");
2136
- var import_node_fs8 = require("fs");
2517
+ var import_node_fs9 = require("fs");
2137
2518
  function registerKeyCommand(program, factory, globals) {
2138
2519
  const key = program.command("key").description("Manage keypairs in the encrypted keystore.");
2139
2520
  const print = (result) => console.log(formatResult(result, globals()));
@@ -2231,7 +2612,7 @@ function parseHex(hex, expectedBytes, label) {
2231
2612
  function readHexFile(path, expectedBytes, label) {
2232
2613
  let content;
2233
2614
  try {
2234
- content = (0, import_node_fs8.readFileSync)(path, "utf-8");
2615
+ content = (0, import_node_fs9.readFileSync)(path, "utf-8");
2235
2616
  } catch {
2236
2617
  throw new CLIError(`Cannot read ${label} at ${path}.`, "INVALID_ARGUMENT_ERROR", { label, path });
2237
2618
  }
@@ -2240,7 +2621,7 @@ function readHexFile(path, expectedBytes, label) {
2240
2621
  function writeSecretFile(path, contents) {
2241
2622
  let fd;
2242
2623
  try {
2243
- fd = (0, import_node_fs8.openSync)(path, "wx", 384);
2624
+ fd = (0, import_node_fs9.openSync)(path, "wx", 384);
2244
2625
  } catch (error) {
2245
2626
  if (error.code === "EEXIST") {
2246
2627
  throw new CLIError(`Refusing to overwrite existing file ${path}. Choose a new --out path.`, "INVALID_ARGUMENT_ERROR", { path });
@@ -2248,152 +2629,10 @@ function writeSecretFile(path, contents) {
2248
2629
  throw error;
2249
2630
  }
2250
2631
  try {
2251
- (0, import_node_fs8.writeFileSync)(fd, contents);
2632
+ (0, import_node_fs9.writeFileSync)(fd, contents);
2252
2633
  } finally {
2253
- (0, import_node_fs8.closeSync)(fd);
2254
- }
2255
- }
2256
-
2257
- // src/commands/keystore.ts
2258
- var import_node_fs9 = require("fs");
2259
- function registerKeystoreCommand(program, globals) {
2260
- const keystore = program.command("keystore").description("Establish, inspect, re-key, and unlock the keystore.");
2261
- const print = (result) => console.log(formatResult(result, globals()));
2262
- keystore.command("init").description("Establish the keystore (encrypted by default). Prompts for a passphrase and confirms it.").option("--dev", "Create an UNENCRYPTED dev keystore: plaintext keys, no passphrase. Testnet only; mainnet operations are refused.", false).option("--force", "Re-establish even if a keystore already exists (discards its keys).", false).action((options) => {
2263
- const g = globals();
2264
- const path = resolveKeystorePath(g);
2265
- if ((0, import_node_fs9.existsSync)(path) && !options.force) {
2266
- throw new CLIError(
2267
- `A keystore already exists at ${path}. Use --force to re-establish it (this discards its keys).`,
2268
- "INVALID_ARGUMENT_ERROR",
2269
- { path }
2270
- );
2271
- }
2272
- if ((0, import_node_fs9.existsSync)(path) && options.force && !g.quiet) {
2273
- const { keyCount } = keystoreSummary(path);
2274
- if (keyCount > 0) {
2275
- process.stderr.write(
2276
- `warning: re-establishing the keystore at ${path} permanently discards its ${keyCount} existing key(s).
2277
- `
2278
- );
2279
- }
2280
- }
2281
- if (options.dev && !g.quiet) {
2282
- process.stderr.write(
2283
- "warning: creating an UNENCRYPTED dev keystore. Keys are stored in plaintext. Use it only for disposable testnet material; mainnet operations will be refused.\n"
2284
- );
2285
- }
2286
- initKeystore(path, {
2287
- protection: options.dev ? "none" : "passphrase",
2288
- getPassphrase: (opts) => acquirePassphrase({ passphraseFile: g.passphraseFile, confirm: opts?.confirm, prompt: "New keystore passphrase: " })
2289
- });
2290
- clearSession(defaultSessionPath(g));
2291
- print({ action: "keystore-init", data: { path, protection: options.dev ? "dev" : "encrypted" } });
2292
- });
2293
- keystore.command("status").description("Show the keystore path, protection mode, key count, and session state. Never decrypts or prompts.").action(() => {
2294
- const g = globals();
2295
- const path = resolveKeystorePath(g, { lenient: true });
2296
- const summary = keystoreSummary(path);
2297
- const session = readSessionStatus(defaultSessionPath(g), path, keystoreVerifierId(path));
2298
- if (summary.protection === "dev" && !g.quiet && g.output !== "json") {
2299
- process.stderr.write("warning: this is an UNENCRYPTED dev keystore; keys are stored in plaintext.\n");
2300
- }
2301
- print({ action: "keystore-status", data: { path, ...summary, session } });
2302
- });
2303
- keystore.command("change-passphrase").alias("passwd").description("Change the keystore passphrase, re-sealing every key under the new one. Encrypted keystores only.").action(() => {
2304
- const g = globals();
2305
- const path = resolveKeystorePath(g);
2306
- const summary = keystoreSummary(path);
2307
- if (summary.protection === "absent") {
2308
- throw new CLIError(`No keystore at ${path}. Run "btcr2 keystore init" first.`, "INVALID_ARGUMENT_ERROR", { path });
2309
- }
2310
- if (summary.protection === "dev") {
2311
- throw new CLIError(
2312
- `The keystore at ${path} is an unencrypted dev keystore; there is no passphrase to change.`,
2313
- "INVALID_ARGUMENT_ERROR",
2314
- { path }
2315
- );
2316
- }
2317
- const oldPassphrase = acquirePassphrase({ passphraseFile: g.passphraseFile, prompt: "Current keystore passphrase: " });
2318
- const newPassphrase = acquirePassphrase({ forcePrompt: true, confirm: true, prompt: "New keystore passphrase: " });
2319
- const rekeyed = changeKeystorePassphrase(path, oldPassphrase, newPassphrase);
2320
- clearSession(defaultSessionPath(g));
2321
- print({ action: "keystore-change-passphrase", data: { path, rekeyed } });
2322
- });
2323
- keystore.command("unlock").description("Cache the keystore passphrase for a session so later commands do not re-prompt (ADR 081).").option("--ttl <duration>", `Session lifetime: bare seconds or an s/m/h suffix (default 1h, max 24h). Also $${ENV_KEYSTORE_TTL}.`).option("--allow-mainnet", "Permit unlocking when the active network is mainnet (bitcoin); this suspends per-use passphrase auth for the session.", false).action((options) => {
2324
- const g = globals();
2325
- const path = resolveKeystorePath(g);
2326
- const summary = keystoreSummary(path);
2327
- if (summary.protection === "absent") {
2328
- throw new CLIError(`No keystore at ${path}. Run "btcr2 init" or "btcr2 keystore init" first.`, "INVALID_ARGUMENT_ERROR", { path });
2329
- }
2330
- if (summary.protection === "dev") {
2331
- throw new CLIError(
2332
- `The keystore at ${path} is an unencrypted dev keystore; it has no passphrase to cache, so no unlock is needed.`,
2333
- "INVALID_ARGUMENT_ERROR",
2334
- { path }
2335
- );
2336
- }
2337
- if (!summary.established) {
2338
- throw new CLIError(
2339
- `The keystore at ${path} has no passphrase established yet. Establish one with "btcr2 keystore init" or the first "btcr2 key generate".`,
2340
- "INVALID_ARGUMENT_ERROR",
2341
- { path }
2342
- );
2343
- }
2344
- if (!options.allowMainnet && resolveDefaultNetwork(g) === "bitcoin") {
2345
- throw new CLIError(
2346
- `Refusing to unlock for a mainnet (bitcoin) context: caching the passphrase suspends per-use authentication for the session. Pass --allow-mainnet to override, or keep signing mainnet updates with a per-use passphrase prompt.`,
2347
- "MAINNET_UNLOCK_REFUSED_ERROR",
2348
- { path }
2349
- );
2350
- }
2351
- const ttlMs = resolveSessionTtl(options.ttl);
2352
- const passphrase = acquirePassphrase({ passphraseFile: g.passphraseFile, prompt: "Keystore passphrase: " });
2353
- if (!verifyKeystorePassphrase(path, passphrase)) {
2354
- throw new CLIError(`Incorrect passphrase for the keystore at ${path}; no session was created.`, "DECRYPT_ERROR", { path });
2355
- }
2356
- const verifierId = keystoreVerifierId(path);
2357
- if (!verifierId) {
2358
- throw new CLIError(`The keystore at ${path} has no verifier to bind a session to.`, "INVALID_ARGUMENT_ERROR", { path });
2359
- }
2360
- const session = writeSession(defaultSessionPath(g), {
2361
- keystorePath: path,
2362
- verifierId,
2363
- passphrase,
2364
- ttlMs,
2365
- allowMainnet: !!options.allowMainnet
2366
- });
2367
- print({ action: "keystore-unlock", data: { keystore: path, expiresAt: session.expiresAt, ttlSeconds: session.ttlSeconds } });
2368
- });
2369
- keystore.command("lock").description("Revoke the cached session so later commands prompt for the passphrase again (ADR 081).").action(() => {
2370
- const g = globals();
2371
- const sessionPath = defaultSessionPath(g);
2372
- const cleared = clearSession(sessionPath);
2373
- print({ action: "keystore-lock", data: { path: sessionPath, cleared } });
2374
- });
2375
- }
2376
- function resolveSessionTtl(flag) {
2377
- const fromFlag = blankToUndef(flag);
2378
- const raw = fromFlag ?? blankToUndef(process.env[ENV_KEYSTORE_TTL]);
2379
- if (raw === void 0) return DEFAULT_SESSION_TTL_MS;
2380
- const source = fromFlag !== void 0 ? "--ttl" : `$${ENV_KEYSTORE_TTL}`;
2381
- const ms = parseTtlToMs(raw);
2382
- if (ms === void 0 || ms <= 0) {
2383
- throw new CLIError(
2384
- `Invalid ${source} "${raw}": expected seconds or a value with an s/m/h suffix, e.g. 3600, 45m, or 2h.`,
2385
- "INVALID_ARGUMENT_ERROR",
2386
- { value: raw, source }
2387
- );
2388
- }
2389
- if (ms > MAX_SESSION_TTL_MS) {
2390
- throw new CLIError(
2391
- `${source} "${raw}" exceeds the 24h maximum for a cached passphrase.`,
2392
- "INVALID_ARGUMENT_ERROR",
2393
- { value: raw, source }
2394
- );
2634
+ (0, import_node_fs9.closeSync)(fd);
2395
2635
  }
2396
- return ms;
2397
2636
  }
2398
2637
 
2399
2638
  // src/commands/config.ts
@@ -2749,6 +2988,7 @@ var DidBtcr2Cli = class {
2749
2988
  opts.output = resolveOutputFormat({ output: opts.output, config: opts.config, home: opts.home });
2750
2989
  });
2751
2990
  registerInitCommand(this.program, globals);
2991
+ registerQuickstartCommand(this.program, globals);
2752
2992
  registerCreateCommand(this.program, factory, keystoreFactory, globals);
2753
2993
  registerResolveCommand(this.program, factory, globals);
2754
2994
  registerUpdateCommand(this.program, keystoreFactory, globals);
@@ -2805,6 +3045,7 @@ function handleError(error, verbose) {
2805
3045
  SUPPORTED_NETWORKS,
2806
3046
  VERSION,
2807
3047
  assertKeystoreAllowedForNetwork,
3048
+ assertSupportedNetwork,
2808
3049
  blankToUndef,
2809
3050
  defaultApiFactory,
2810
3051
  defaultConfigPath,
@@ -2815,9 +3056,11 @@ function handleError(error, verbose) {
2815
3056
  keystoreApiFactory,
2816
3057
  parseConfigFileRaw,
2817
3058
  parseHeaderList,
3059
+ persistDefaultNetwork,
2818
3060
  profileNetworkMismatch,
2819
3061
  profileToOverrides,
2820
3062
  readConfigFile,
3063
+ readConfiguredDefaultNetwork,
2821
3064
  readEnvOverrides,
2822
3065
  redactSecrets,
2823
3066
  registerCompletionCommand,
@@ -2828,6 +3071,7 @@ function handleError(error, verbose) {
2828
3071
  registerKeyCommand,
2829
3072
  registerKeystoreCommand,
2830
3073
  registerProfileCommand,
3074
+ registerQuickstartCommand,
2831
3075
  registerResolveCommand,
2832
3076
  registerUpdateCommand,
2833
3077
  resolveActiveProfile,