@did-btcr2/cli 0.16.0 → 0.18.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.
Files changed (74) hide show
  1. package/README.md +33 -8
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/cjs/index.js +640 -128
  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 +89 -40
  13. package/dist/esm/src/commands/init.js.map +1 -1
  14. package/dist/esm/src/commands/keystore.js +126 -8
  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 +93 -6
  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/esm/src/keystore/file-key-store.js +48 -0
  25. package/dist/esm/src/keystore/file-key-store.js.map +1 -1
  26. package/dist/esm/src/keystore/passphrase.js +13 -0
  27. package/dist/esm/src/keystore/passphrase.js.map +1 -1
  28. package/dist/esm/src/keystore/session.js +250 -0
  29. package/dist/esm/src/keystore/session.js.map +1 -0
  30. package/dist/esm/src/paths.js +12 -0
  31. package/dist/esm/src/paths.js.map +1 -1
  32. package/dist/esm/src/types.js.map +1 -1
  33. package/dist/types/src/cli.d.ts.map +1 -1
  34. package/dist/types/src/commands/create.d.ts.map +1 -1
  35. package/dist/types/src/commands/deactivate.d.ts.map +1 -1
  36. package/dist/types/src/commands/index.d.ts +1 -0
  37. package/dist/types/src/commands/index.d.ts.map +1 -1
  38. package/dist/types/src/commands/init.d.ts +52 -5
  39. package/dist/types/src/commands/init.d.ts.map +1 -1
  40. package/dist/types/src/commands/keystore.d.ts +41 -4
  41. package/dist/types/src/commands/keystore.d.ts.map +1 -1
  42. package/dist/types/src/commands/quickstart.d.ts +12 -0
  43. package/dist/types/src/commands/quickstart.d.ts.map +1 -0
  44. package/dist/types/src/commands/update.d.ts.map +1 -1
  45. package/dist/types/src/config.d.ts +35 -0
  46. package/dist/types/src/config.d.ts.map +1 -1
  47. package/dist/types/src/hints.d.ts +24 -0
  48. package/dist/types/src/hints.d.ts.map +1 -0
  49. package/dist/types/src/keystore/file-key-store.d.ts +16 -0
  50. package/dist/types/src/keystore/file-key-store.d.ts.map +1 -1
  51. package/dist/types/src/keystore/passphrase.d.ts +10 -0
  52. package/dist/types/src/keystore/passphrase.d.ts.map +1 -1
  53. package/dist/types/src/keystore/session.d.ts +105 -0
  54. package/dist/types/src/keystore/session.d.ts.map +1 -0
  55. package/dist/types/src/paths.d.ts +10 -0
  56. package/dist/types/src/paths.d.ts.map +1 -1
  57. package/dist/types/src/types.d.ts +32 -0
  58. package/dist/types/src/types.d.ts.map +1 -1
  59. package/package.json +5 -5
  60. package/src/cli.ts +2 -0
  61. package/src/commands/create.ts +4 -0
  62. package/src/commands/deactivate.ts +2 -0
  63. package/src/commands/index.ts +1 -0
  64. package/src/commands/init.ts +148 -50
  65. package/src/commands/keystore.ts +183 -9
  66. package/src/commands/quickstart.ts +209 -0
  67. package/src/commands/update.ts +2 -0
  68. package/src/config.ts +103 -6
  69. package/src/hints.ts +51 -0
  70. package/src/keystore/file-key-store.ts +43 -0
  71. package/src/keystore/passphrase.ts +23 -0
  72. package/src/keystore/session.ts +303 -0
  73. package/src/paths.ts +13 -0
  74. package/src/types.ts +16 -2
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,
@@ -82,13 +86,13 @@ var import_common3 = require("@did-btcr2/common");
82
86
  var import_commander = require("commander");
83
87
 
84
88
  // src/commands/init.ts
85
- var import_node_fs6 = require("fs");
89
+ var import_node_fs7 = require("fs");
86
90
 
87
91
  // src/config.ts
88
92
  var import_api = require("@did-btcr2/api");
89
93
  var import_method = require("@did-btcr2/method");
90
- var import_node_fs5 = require("fs");
91
- var import_node_path4 = require("path");
94
+ var import_node_fs6 = require("fs");
95
+ var import_node_path5 = require("path");
92
96
 
93
97
  // src/error.ts
94
98
  var import_common = require("@did-btcr2/common");
@@ -167,6 +171,7 @@ var import_key_manager = require("@did-btcr2/key-manager");
167
171
  // src/keystore/file-key-store.ts
168
172
  var import_node_fs3 = require("fs");
169
173
  var import_node_path3 = require("path");
174
+ var import_sha2 = require("@noble/hashes/sha2.js");
170
175
  var import_utils2 = require("@noble/hashes/utils.js");
171
176
  var import_base2 = require("@scure/base");
172
177
 
@@ -360,6 +365,7 @@ function blankToUndef(value) {
360
365
  var ENV_HOME = "BTCR2_HOME";
361
366
  var CONFIG_FILENAME = "config.json";
362
367
  var KEYSTORE_FILENAME = "keystore.json";
368
+ var SESSION_FILENAME = "session.json";
363
369
  function resolveHome(overrides) {
364
370
  return blankToUndef(overrides?.home) ?? blankToUndef(process.env[ENV_HOME]) ?? platformDefaultHome();
365
371
  }
@@ -376,6 +382,9 @@ function defaultConfigPath(overrides) {
376
382
  function defaultKeystorePath(overrides) {
377
383
  return (0, import_node_path2.join)(resolveHome(overrides), KEYSTORE_FILENAME);
378
384
  }
385
+ function defaultSessionPath(overrides) {
386
+ return (0, import_node_path2.join)(resolveHome(overrides), SESSION_FILENAME);
387
+ }
379
388
 
380
389
  // src/keystore/file-key-store.ts
381
390
  var KEYSTORE_VERSION = 1;
@@ -830,6 +839,32 @@ function keystoreSummary(path) {
830
839
  function keystoreProtection(path) {
831
840
  return keystoreSummary(path).protection;
832
841
  }
842
+ function keystoreVerifierId(path) {
843
+ if (!(0, import_node_fs3.existsSync)(path)) return void 0;
844
+ let parsed;
845
+ try {
846
+ parsed = JSON.parse((0, import_node_fs3.readFileSync)(path, "utf-8"));
847
+ } catch {
848
+ return void 0;
849
+ }
850
+ if (!parsed.verifier) return void 0;
851
+ return import_base2.base64urlnopad.encode((0, import_sha2.sha256)((0, import_utils2.utf8ToBytes)(JSON.stringify(parsed.verifier))));
852
+ }
853
+ function verifyKeystorePassphrase(path, passphrase) {
854
+ if (!(0, import_node_fs3.existsSync)(path)) return false;
855
+ let parsed;
856
+ try {
857
+ parsed = JSON.parse((0, import_node_fs3.readFileSync)(path, "utf-8"));
858
+ } catch {
859
+ return false;
860
+ }
861
+ if (parsed.protection !== "passphrase" || !parsed.verifier) return false;
862
+ try {
863
+ return bytesEqual(decryptSecret(parsed.verifier, passphrase), VERIFIER_PLAINTEXT);
864
+ } catch {
865
+ return false;
866
+ }
867
+ }
833
868
  function initKeystore(path, options) {
834
869
  ensureDir((0, import_node_path3.dirname)(path), 448);
835
870
  let file;
@@ -923,6 +958,8 @@ function acquirePassphrase(options = {}) {
923
958
  if (options.passphraseFile) {
924
959
  return assertNonEmpty((0, import_node_fs4.readFileSync)(options.passphraseFile, "utf-8").replace(/\r?\n$/, ""));
925
960
  }
961
+ const fromSession = options.beforePrompt?.();
962
+ if (fromSession) return assertNonEmpty(fromSession);
926
963
  }
927
964
  if (!process.stdin.isTTY) {
928
965
  throw new KeyStoreError(
@@ -991,13 +1028,155 @@ function promptHidden(label) {
991
1028
  return Buffer.from(bytes).toString("utf-8");
992
1029
  }
993
1030
 
1031
+ // src/keystore/session.ts
1032
+ var import_node_fs5 = require("fs");
1033
+ var import_node_path4 = require("path");
1034
+ var import_utils3 = require("@noble/hashes/utils.js");
1035
+ var import_base3 = require("@scure/base");
1036
+ var SESSION_VERSION = 1;
1037
+ var DEFAULT_SESSION_TTL_MS = 60 * 60 * 1e3;
1038
+ var MAX_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
1039
+ var ENV_KEYSTORE_TTL = "BTCR2_KEYSTORE_TTL";
1040
+ function writeSession(sessionPath, input) {
1041
+ const createdAt = Date.now();
1042
+ const session = {
1043
+ v: SESSION_VERSION,
1044
+ keystore: (0, import_node_path4.resolve)(input.keystorePath),
1045
+ verifierId: input.verifierId,
1046
+ passphrase: import_base3.base64urlnopad.encode((0, import_utils3.utf8ToBytes)(input.passphrase)),
1047
+ allowMainnet: input.allowMainnet ?? false,
1048
+ createdAt,
1049
+ expiresAt: createdAt + input.ttlMs,
1050
+ ttlSeconds: Math.round(input.ttlMs / 1e3)
1051
+ };
1052
+ ensureDir((0, import_node_path4.dirname)(sessionPath), 448);
1053
+ writeFileAtomic(sessionPath, `${JSON.stringify(session, null, 2)}
1054
+ `, 384);
1055
+ return session;
1056
+ }
1057
+ function clearSession(sessionPath) {
1058
+ let existed = false;
1059
+ try {
1060
+ existed = (0, import_node_fs5.existsSync)(sessionPath);
1061
+ if (existed) (0, import_node_fs5.rmSync)(sessionPath, { force: true });
1062
+ } catch {
1063
+ existed = false;
1064
+ }
1065
+ sweepSessionTemps(sessionPath);
1066
+ return existed;
1067
+ }
1068
+ function sweepSessionTemps(sessionPath) {
1069
+ const dir = (0, import_node_path4.dirname)(sessionPath);
1070
+ const prefix = `.${(0, import_node_path4.basename)(sessionPath)}.`;
1071
+ try {
1072
+ for (const name of (0, import_node_fs5.readdirSync)(dir)) {
1073
+ if (name.startsWith(prefix) && name.endsWith(".tmp")) {
1074
+ try {
1075
+ (0, import_node_fs5.rmSync)((0, import_node_path4.join)(dir, name), { force: true });
1076
+ } catch {
1077
+ }
1078
+ }
1079
+ }
1080
+ } catch {
1081
+ }
1082
+ }
1083
+ function readLiveSessionPassphrase(sessionPath, keystorePath, currentVerifierId, isMainnetOperation = false) {
1084
+ const verdict = evaluateSession(sessionPath, keystorePath, currentVerifierId, Date.now());
1085
+ if (verdict.status === "live") {
1086
+ if (isMainnetOperation && !verdict.session.allowMainnet) return void 0;
1087
+ try {
1088
+ return Buffer.from(import_base3.base64urlnopad.decode(verdict.session.passphrase)).toString("utf-8");
1089
+ } catch {
1090
+ clearSession(sessionPath);
1091
+ return void 0;
1092
+ }
1093
+ }
1094
+ if (verdict.status === "expired" || verdict.status === "stale" || verdict.status === "future" || verdict.status === "malformed") {
1095
+ clearSession(sessionPath);
1096
+ }
1097
+ return void 0;
1098
+ }
1099
+ function readSessionStatus(sessionPath, keystorePath, currentVerifierId) {
1100
+ const now = Date.now();
1101
+ const verdict = evaluateSession(sessionPath, keystorePath, currentVerifierId, now);
1102
+ if (verdict.status !== "live") return { active: false };
1103
+ return {
1104
+ active: true,
1105
+ expiresAt: verdict.session.expiresAt,
1106
+ secondsRemaining: Math.max(0, Math.round((verdict.session.expiresAt - now) / 1e3)),
1107
+ allowMainnet: verdict.session.allowMainnet
1108
+ };
1109
+ }
1110
+ function parseTtlToMs(raw) {
1111
+ const match = /^(\d+)([smh]?)$/.exec(raw.trim());
1112
+ if (!match) return void 0;
1113
+ const n = Number(match[1]);
1114
+ if (!Number.isFinite(n)) return void 0;
1115
+ const unitMs = match[2] === "h" ? 36e5 : match[2] === "m" ? 6e4 : 1e3;
1116
+ return n * unitMs;
1117
+ }
1118
+ function evaluateSession(sessionPath, keystorePath, currentVerifierId, now) {
1119
+ const session = secureReadSession(sessionPath);
1120
+ if (!session) return { status: "none" };
1121
+ if (session.v !== SESSION_VERSION) return { status: "malformed" };
1122
+ if (typeof session.passphrase !== "string" || typeof session.keystore !== "string" || typeof session.verifierId !== "string" || typeof session.allowMainnet !== "boolean" || typeof session.createdAt !== "number" || typeof session.expiresAt !== "number") {
1123
+ return { status: "malformed" };
1124
+ }
1125
+ if (session.createdAt > now) return { status: "future" };
1126
+ if (now >= session.expiresAt) return { status: "expired" };
1127
+ if ((0, import_node_path4.resolve)(session.keystore) !== (0, import_node_path4.resolve)(keystorePath)) return { status: "foreign" };
1128
+ if (currentVerifierId === void 0 || session.verifierId !== currentVerifierId) return { status: "stale" };
1129
+ return { status: "live", session };
1130
+ }
1131
+ function secureReadSession(sessionPath) {
1132
+ let raw;
1133
+ if (process.platform === "win32") {
1134
+ try {
1135
+ raw = (0, import_node_fs5.readFileSync)(sessionPath, "utf-8");
1136
+ } catch {
1137
+ return void 0;
1138
+ }
1139
+ } else {
1140
+ let fd;
1141
+ try {
1142
+ fd = (0, import_node_fs5.openSync)(sessionPath, import_node_fs5.constants.O_RDONLY | import_node_fs5.constants.O_NOFOLLOW);
1143
+ } catch {
1144
+ return void 0;
1145
+ }
1146
+ try {
1147
+ const st = (0, import_node_fs5.fstatSync)(fd);
1148
+ const myUid = typeof process.getuid === "function" ? process.getuid() : void 0;
1149
+ if (!st.isFile() || myUid !== void 0 && st.uid !== myUid || (st.mode & 63) !== 0) {
1150
+ try {
1151
+ (0, import_node_fs5.rmSync)(sessionPath, { force: true });
1152
+ } catch {
1153
+ }
1154
+ return void 0;
1155
+ }
1156
+ raw = (0, import_node_fs5.readFileSync)(fd, "utf-8");
1157
+ } catch {
1158
+ return void 0;
1159
+ } finally {
1160
+ try {
1161
+ (0, import_node_fs5.closeSync)(fd);
1162
+ } catch {
1163
+ }
1164
+ }
1165
+ }
1166
+ try {
1167
+ return JSON.parse(raw);
1168
+ } catch {
1169
+ return void 0;
1170
+ }
1171
+ }
1172
+
994
1173
  // src/config.ts
995
1174
  var CONFIG_SCHEMA_VERSION = 1;
996
1175
  function writeConfigFile(path, mutate) {
997
1176
  const raw = readConfigFile(path) ?? {};
998
1177
  mutate(raw);
999
1178
  raw.schemaVersion = CONFIG_SCHEMA_VERSION;
1000
- ensureDir((0, import_node_path4.dirname)(path), 448);
1179
+ ensureDir((0, import_node_path5.dirname)(path), 448);
1001
1180
  writeFileAtomic(path, `${JSON.stringify(raw, null, 2)}
1002
1181
  `, 384);
1003
1182
  }
@@ -1007,7 +1186,7 @@ function writeDefaultConfigFile(path) {
1007
1186
  defaults: { output: "text" },
1008
1187
  profiles: Object.fromEntries(SUPPORTED_NETWORKS.map((n) => [n, {}]))
1009
1188
  };
1010
- ensureDir((0, import_node_path4.dirname)(path), 448);
1189
+ ensureDir((0, import_node_path5.dirname)(path), 448);
1011
1190
  writeFileAtomic(path, `${JSON.stringify(scaffold, null, 2)}
1012
1191
  `, 384);
1013
1192
  }
@@ -1072,7 +1251,7 @@ function readEnvOverrides() {
1072
1251
  function parseConfigFileRaw(path) {
1073
1252
  let content;
1074
1253
  try {
1075
- content = (0, import_node_fs5.readFileSync)(path, "utf-8");
1254
+ content = (0, import_node_fs6.readFileSync)(path, "utf-8");
1076
1255
  } catch (error) {
1077
1256
  if (error.code === "ENOENT") return void 0;
1078
1257
  throw new CLIError(
@@ -1135,6 +1314,45 @@ function resolveDefaultNetwork(overrides) {
1135
1314
  if (network) return network;
1136
1315
  return "regtest";
1137
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
+ }
1138
1356
  function profileNetworkMismatch(network, overrides) {
1139
1357
  let file;
1140
1358
  try {
@@ -1264,7 +1482,7 @@ function trimTrailingNewline(value) {
1264
1482
  }
1265
1483
  function readSecretFile(path, source) {
1266
1484
  try {
1267
- return trimTrailingNewline((0, import_node_fs5.readFileSync)(path, "utf-8"));
1485
+ return trimTrailingNewline((0, import_node_fs6.readFileSync)(path, "utf-8"));
1268
1486
  } catch (error) {
1269
1487
  throw new CLIError(
1270
1488
  `Could not read the RPC password ${source} at ${path}: ${error.message}`,
@@ -1398,13 +1616,28 @@ async function runDoctor(network, overrides) {
1398
1616
  function defaultApiFactory(network, overrides) {
1399
1617
  return (0, import_api.createApi)(resolveConnectionConfig(network, overrides));
1400
1618
  }
1401
- function buildKeystoreKms(overrides) {
1619
+ function buildKeystoreKms(overrides, network) {
1620
+ const keystorePath = resolveKeystorePath(overrides);
1621
+ const sessionPath = defaultSessionPath(overrides);
1622
+ const isMainnetOperation = network === "bitcoin";
1402
1623
  return new FileBackedKeyManager({
1403
- path: resolveKeystorePath(overrides),
1624
+ path: keystorePath,
1404
1625
  // The store decides when to confirm: it passes `confirm: true` only while
1405
1626
  // establishing a fresh keystore's passphrase, so a first-key typo is caught
1406
1627
  // by a second entry (ADR 080). confirm is a no-op for env/file sources.
1407
- getPassphrase: (opts) => acquirePassphrase({ passphraseFile: overrides?.passphraseFile, confirm: opts?.confirm })
1628
+ //
1629
+ // On the non-establishing path, a cached session (ADR 081) is consulted below
1630
+ // the env var / --passphrase-file and above the interactive prompt. It is
1631
+ // wired ONLY when not confirming, so establishment never consults the session
1632
+ // and a first passphrase is always entered fresh and twice. The session is
1633
+ // bound to this keystore's verifier, so a rotated passphrase invalidates it.
1634
+ getPassphrase: (opts) => acquirePassphrase({
1635
+ passphraseFile: overrides?.passphraseFile,
1636
+ confirm: opts?.confirm,
1637
+ ...opts?.confirm ? {} : {
1638
+ beforePrompt: () => readLiveSessionPassphrase(sessionPath, keystorePath, keystoreVerifierId(keystorePath), isMainnetOperation)
1639
+ }
1640
+ })
1408
1641
  });
1409
1642
  }
1410
1643
  function resolveKeystoreProtection(overrides) {
@@ -1442,7 +1675,7 @@ function resolveSigningKeyRef(overrides) {
1442
1675
  function keystoreApiFactory(network, overrides) {
1443
1676
  return (0, import_api.createApi)({
1444
1677
  ...resolveConnectionConfig(network, overrides),
1445
- kms: buildKeystoreKms(overrides)
1678
+ kms: buildKeystoreKms(overrides, network)
1446
1679
  });
1447
1680
  }
1448
1681
  function deriveNetwork(did) {
@@ -1492,49 +1725,383 @@ function formatResult(result, options) {
1492
1725
  }
1493
1726
 
1494
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
+ }
1495
1775
  function registerInitCommand(program, globals) {
1496
1776
  const print = (result) => console.log(formatResult(result, globals()));
1497
- 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) => {
1498
1781
  const g = globals();
1499
- const home = resolveHome(g);
1500
- const configPath = g.config ?? defaultConfigPath(g);
1501
- const keystorePath = resolveKeystorePath(g);
1502
- ensureDir(home, 448);
1503
- const created = [];
1504
- if (!(0, import_node_fs6.existsSync)(configPath) || options.force) {
1505
- writeDefaultConfigFile(configPath);
1506
- created.push("config");
1507
- }
1508
- const keystoreExists = (0, import_node_fs6.existsSync)(keystorePath);
1509
- if (keystoreExists && options.force && !g.quiet) {
1510
- process.stderr.write(
1511
- `note: a keystore already exists at ${keystorePath} and was left intact. To re-establish it (discarding its keys), run "btcr2 keystore init --force".
1512
- `
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 }
1513
1818
  );
1514
1819
  }
1515
- if (!keystoreExists) {
1516
- 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) {
1517
1823
  process.stderr.write(
1518
- "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
+ `
1519
1826
  );
1520
1827
  }
1521
- initKeystore(keystorePath, {
1522
- protection: options.dev ? "none" : "passphrase",
1523
- getPassphrase: (opts) => acquirePassphrase({ passphraseFile: g.passphraseFile, confirm: opts?.confirm, prompt: "New keystore passphrase: " })
1524
- });
1525
- created.push("keystore");
1526
1828
  }
1527
- const protection = keystoreSummary(keystorePath).protection;
1528
- 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
+ });
1529
2016
  if (!g.quiet && g.output !== "json") {
1530
- process.stderr.write(`btcr2 home ready at ${home}. Next: btcr2 key generate --set-active
1531
- `);
2017
+ printNextSteps(init, unlocked, session, doctor);
1532
2018
  }
1533
2019
  });
1534
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
+ }
1535
2071
 
1536
2072
  // src/commands/create.ts
1537
- var import_utils3 = require("@noble/hashes/utils.js");
2073
+ var import_utils4 = require("@noble/hashes/utils.js");
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
+ }
1538
2105
 
1539
2106
  // src/keystore/resolve-key-ref.ts
1540
2107
  function fingerprintOf(id) {
@@ -1634,6 +2201,7 @@ function registerCreateCommand(program, factory, keystoreFactory, globals) {
1634
2201
  const genesisBytes = parseGenesisBytes(options.bytes, "k");
1635
2202
  const did2 = factory().createDid("deterministic", genesisBytes, { network });
1636
2203
  print({ action: "create", data: did2 });
2204
+ printCreateFundingHint(g, network, did2);
1637
2205
  return;
1638
2206
  }
1639
2207
  if (signingKey) {
@@ -1642,19 +2210,21 @@ function registerCreateCommand(program, factory, keystoreFactory, globals) {
1642
2210
  const publicKey2 = api2.kms.getPublicKey(keyId2);
1643
2211
  const did2 = api2.createDid("deterministic", publicKey2, { network });
1644
2212
  print(
1645
- { action: "create", data: did2, keyId: keyId2, publicKey: (0, import_utils3.bytesToHex)(publicKey2) },
2213
+ { action: "create", data: did2, keyId: keyId2, publicKey: (0, import_utils4.bytesToHex)(publicKey2) },
1646
2214
  `Using stored key ${keyId2}.`
1647
2215
  );
2216
+ printCreateFundingHint(g, network, did2);
1648
2217
  return;
1649
2218
  }
1650
2219
  assertKeystoreAllowedForNetwork(network, overrides);
1651
2220
  const api = keystoreFactory(void 0, overrides);
1652
2221
  const { did, keyId } = api.generateDid({ network, setActive: true });
1653
- const publicKey = (0, import_utils3.bytesToHex)(api.kms.getPublicKey(keyId));
2222
+ const publicKey = (0, import_utils4.bytesToHex)(api.kms.getPublicKey(keyId));
1654
2223
  print(
1655
2224
  { action: "create", data: did, keyId, publicKey },
1656
2225
  `Generated and stored key ${keyId} (now the active key).`
1657
2226
  );
2227
+ printCreateFundingHint(g, network, did);
1658
2228
  });
1659
2229
  }
1660
2230
  function overridesFromGlobals(g) {
@@ -1681,7 +2251,7 @@ function parseGenesisBytes(hex, type) {
1681
2251
  const expected = EXPECTED_BYTES[type];
1682
2252
  let bytes;
1683
2253
  try {
1684
- bytes = (0, import_utils3.hexToBytes)(hex.trim());
2254
+ bytes = (0, import_utils4.hexToBytes)(hex.trim());
1685
2255
  } catch {
1686
2256
  throw new CLIError(
1687
2257
  `Invalid bytes: not valid hex. Expected ${expected.label}.`,
@@ -1700,7 +2270,7 @@ function parseGenesisBytes(hex, type) {
1700
2270
  }
1701
2271
 
1702
2272
  // src/commands/resolve.ts
1703
- var import_api2 = require("@did-btcr2/api");
2273
+ var import_api4 = require("@did-btcr2/api");
1704
2274
  var import_promises = require("fs/promises");
1705
2275
  function registerResolveCommand(program, factory, globals) {
1706
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) => {
@@ -1713,7 +2283,7 @@ function registerResolveCommand(program, factory, globals) {
1713
2283
  });
1714
2284
  }
1715
2285
  async function validateResolveOptions(options) {
1716
- import_api2.Identifier.decode(options.identifier);
2286
+ import_api4.Identifier.decode(options.identifier);
1717
2287
  let resolutionOptions = void 0;
1718
2288
  if (options.resolutionOptions) {
1719
2289
  try {
@@ -1815,6 +2385,7 @@ function registerUpdateCommand(program, factory, globals) {
1815
2385
  ...broadcastOptions ? { broadcastOptions } : {}
1816
2386
  });
1817
2387
  console.log(formatResult({ action: "update", data }, globals()));
2388
+ printWatchHint(globals(), network, data.txid);
1818
2389
  });
1819
2390
  }
1820
2391
  function parsePublishToCasMode(value) {
@@ -1913,6 +2484,7 @@ function registerDeactivateCommand(program, factory, globals) {
1913
2484
  ...broadcastOptions ? { broadcastOptions } : {}
1914
2485
  });
1915
2486
  console.log(formatResult({ action: "deactivate", data }, globals()));
2487
+ printWatchHint(globals(), network, data.txid);
1916
2488
  });
1917
2489
  }
1918
2490
  function parsePublishToCasMode2(value) {
@@ -1941,8 +2513,8 @@ function parseJsonArg2(flagName) {
1941
2513
 
1942
2514
  // src/commands/key.ts
1943
2515
  var import_keypair = require("@did-btcr2/keypair");
1944
- var import_utils4 = require("@noble/hashes/utils.js");
1945
- var import_node_fs7 = require("fs");
2516
+ var import_utils5 = require("@noble/hashes/utils.js");
2517
+ var import_node_fs9 = require("fs");
1946
2518
  function registerKeyCommand(program, factory, globals) {
1947
2519
  const key = program.command("key").description("Manage keypairs in the encrypted keystore.");
1948
2520
  const print = (result) => console.log(formatResult(result, globals()));
@@ -1951,7 +2523,7 @@ function registerKeyCommand(program, factory, globals) {
1951
2523
  assertNameAvailable(api.kms.kms, options.name);
1952
2524
  const setActive = options.setActive ?? false;
1953
2525
  const id = api.kms.generateKey({ ...options.name && { tags: { name: options.name } }, setActive });
1954
- print({ action: "key-generate", data: { keyId: id, publicKey: (0, import_utils4.bytesToHex)(api.kms.getPublicKey(id)), active: setActive } });
2526
+ print({ action: "key-generate", data: { keyId: id, publicKey: (0, import_utils5.bytesToHex)(api.kms.getPublicKey(id)), active: setActive } });
1955
2527
  });
1956
2528
  key.command("list").alias("ls").description("List stored keys.").action(() => {
1957
2529
  const kms = factory(void 0, globals()).kms.kms;
@@ -1971,7 +2543,7 @@ function registerKeyCommand(program, factory, globals) {
1971
2543
  const kms = factory(void 0, globals()).kms.kms;
1972
2544
  const id = resolveKeyRef(kms, ref);
1973
2545
  const entry = kms.getEntry(id);
1974
- print({ action: "key-show", data: { keyId: id, publicKey: (0, import_utils4.bytesToHex)(entry.publicKey), ...entry.tags && { tags: entry.tags } } });
2546
+ print({ action: "key-show", data: { keyId: id, publicKey: (0, import_utils5.bytesToHex)(entry.publicKey), ...entry.tags && { tags: entry.tags } } });
1975
2547
  });
1976
2548
  key.command("import").description("Import a key: a secret from a hex file, or a public key as watch-only.").option("--secret-file <path>", "Path to a file containing a 32-byte secret key as hex.").option("--public <hex>", "A 33-byte compressed public key as hex (imported watch-only).").option("--name <name>", "A human-friendly name, stored as a tag.").option("--set-active", "Make this the active key.", false).action((options) => {
1977
2549
  if (Boolean(options.secretFile) === Boolean(options.public)) {
@@ -1982,13 +2554,13 @@ function registerKeyCommand(program, factory, globals) {
1982
2554
  const keyPair = options.secretFile ? new import_keypair.SchnorrKeyPair({ secretKey: readHexFile(options.secretFile, 32, "--secret-file") }) : new import_keypair.SchnorrKeyPair({ publicKey: parseHex(options.public ?? "", 33, "--public") });
1983
2555
  const setActive = options.setActive ?? false;
1984
2556
  const id = api.kms.import(keyPair, { ...options.name && { tags: { name: options.name } }, setActive });
1985
- print({ action: "key-import", data: { keyId: id, publicKey: (0, import_utils4.bytesToHex)(api.kms.getPublicKey(id)), watchOnly: !options.secretFile, active: setActive } });
2557
+ print({ action: "key-import", data: { keyId: id, publicKey: (0, import_utils5.bytesToHex)(api.kms.getPublicKey(id)), watchOnly: !options.secretFile, active: setActive } });
1986
2558
  });
1987
2559
  key.command("export <ref>").description("Export a key. Public material by default; --secret writes the secret to a file.").option("--secret", "Export the secret key. Requires --out.", false).option("--out <path>", "Write the exported secret to this file (created 0600).").action((ref, options) => {
1988
2560
  const api = factory(void 0, globals());
1989
2561
  const id = resolveKeyRef(api.kms.kms, ref);
1990
2562
  if (!options.secret) {
1991
- print({ action: "key-export", data: { keyId: id, publicKey: (0, import_utils4.bytesToHex)(api.kms.getPublicKey(id)) } });
2563
+ print({ action: "key-export", data: { keyId: id, publicKey: (0, import_utils5.bytesToHex)(api.kms.getPublicKey(id)) } });
1992
2564
  return;
1993
2565
  }
1994
2566
  if (!options.out) {
@@ -1999,7 +2571,7 @@ function registerKeyCommand(program, factory, globals) {
1999
2571
  throw new CLIError(`Key ${id} is watch-only and has no secret to export.`, "INVALID_ARGUMENT_ERROR", { keyId: id });
2000
2572
  }
2001
2573
  process.stderr.write("warning: writing an unencrypted secret key to disk. Protect this file and delete it when done.\n");
2002
- writeSecretFile(options.out, (0, import_utils4.bytesToHex)(keyPair.secretKey.bytes));
2574
+ writeSecretFile(options.out, (0, import_utils5.bytesToHex)(keyPair.secretKey.bytes));
2003
2575
  print({ action: "key-export", data: { keyId: id, secretWrittenTo: options.out } });
2004
2576
  });
2005
2577
  key.command("delete <ref>").alias("rm").description("Delete a key from the keystore.").option("--force", "Delete even if it is the active key.", false).action((ref, options) => {
@@ -2024,7 +2596,7 @@ function assertNameAvailable(kms, name) {
2024
2596
  function parseHex(hex, expectedBytes, label) {
2025
2597
  let bytes;
2026
2598
  try {
2027
- bytes = (0, import_utils4.hexToBytes)(hex.trim());
2599
+ bytes = (0, import_utils5.hexToBytes)(hex.trim());
2028
2600
  } catch {
2029
2601
  throw new CLIError(`Invalid hex for ${label}.`, "INVALID_ARGUMENT_ERROR", { label });
2030
2602
  }
@@ -2040,7 +2612,7 @@ function parseHex(hex, expectedBytes, label) {
2040
2612
  function readHexFile(path, expectedBytes, label) {
2041
2613
  let content;
2042
2614
  try {
2043
- content = (0, import_node_fs7.readFileSync)(path, "utf-8");
2615
+ content = (0, import_node_fs9.readFileSync)(path, "utf-8");
2044
2616
  } catch {
2045
2617
  throw new CLIError(`Cannot read ${label} at ${path}.`, "INVALID_ARGUMENT_ERROR", { label, path });
2046
2618
  }
@@ -2049,7 +2621,7 @@ function readHexFile(path, expectedBytes, label) {
2049
2621
  function writeSecretFile(path, contents) {
2050
2622
  let fd;
2051
2623
  try {
2052
- fd = (0, import_node_fs7.openSync)(path, "wx", 384);
2624
+ fd = (0, import_node_fs9.openSync)(path, "wx", 384);
2053
2625
  } catch (error) {
2054
2626
  if (error.code === "EEXIST") {
2055
2627
  throw new CLIError(`Refusing to overwrite existing file ${path}. Choose a new --out path.`, "INVALID_ARGUMENT_ERROR", { path });
@@ -2057,79 +2629,14 @@ function writeSecretFile(path, contents) {
2057
2629
  throw error;
2058
2630
  }
2059
2631
  try {
2060
- (0, import_node_fs7.writeFileSync)(fd, contents);
2632
+ (0, import_node_fs9.writeFileSync)(fd, contents);
2061
2633
  } finally {
2062
- (0, import_node_fs7.closeSync)(fd);
2634
+ (0, import_node_fs9.closeSync)(fd);
2063
2635
  }
2064
2636
  }
2065
2637
 
2066
- // src/commands/keystore.ts
2067
- var import_node_fs8 = require("fs");
2068
- function registerKeystoreCommand(program, globals) {
2069
- const keystore = program.command("keystore").description("Establish, inspect, and re-key the keystore.");
2070
- const print = (result) => console.log(formatResult(result, globals()));
2071
- 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) => {
2072
- const g = globals();
2073
- const path = resolveKeystorePath(g);
2074
- if ((0, import_node_fs8.existsSync)(path) && !options.force) {
2075
- throw new CLIError(
2076
- `A keystore already exists at ${path}. Use --force to re-establish it (this discards its keys).`,
2077
- "INVALID_ARGUMENT_ERROR",
2078
- { path }
2079
- );
2080
- }
2081
- if ((0, import_node_fs8.existsSync)(path) && options.force && !g.quiet) {
2082
- const { keyCount } = keystoreSummary(path);
2083
- if (keyCount > 0) {
2084
- process.stderr.write(
2085
- `warning: re-establishing the keystore at ${path} permanently discards its ${keyCount} existing key(s).
2086
- `
2087
- );
2088
- }
2089
- }
2090
- if (options.dev && !g.quiet) {
2091
- process.stderr.write(
2092
- "warning: creating an UNENCRYPTED dev keystore. Keys are stored in plaintext. Use it only for disposable testnet material; mainnet operations will be refused.\n"
2093
- );
2094
- }
2095
- initKeystore(path, {
2096
- protection: options.dev ? "none" : "passphrase",
2097
- getPassphrase: (opts) => acquirePassphrase({ passphraseFile: g.passphraseFile, confirm: opts?.confirm, prompt: "New keystore passphrase: " })
2098
- });
2099
- print({ action: "keystore-init", data: { path, protection: options.dev ? "dev" : "encrypted" } });
2100
- });
2101
- keystore.command("status").description("Show the keystore path, protection mode, and key count. Never decrypts or prompts.").action(() => {
2102
- const g = globals();
2103
- const path = resolveKeystorePath(g, { lenient: true });
2104
- const summary = keystoreSummary(path);
2105
- if (summary.protection === "dev" && !g.quiet && g.output !== "json") {
2106
- process.stderr.write("warning: this is an UNENCRYPTED dev keystore; keys are stored in plaintext.\n");
2107
- }
2108
- print({ action: "keystore-status", data: { path, ...summary } });
2109
- });
2110
- keystore.command("change-passphrase").alias("passwd").description("Change the keystore passphrase, re-sealing every key under the new one. Encrypted keystores only.").action(() => {
2111
- const g = globals();
2112
- const path = resolveKeystorePath(g);
2113
- const summary = keystoreSummary(path);
2114
- if (summary.protection === "absent") {
2115
- throw new CLIError(`No keystore at ${path}. Run "btcr2 keystore init" first.`, "INVALID_ARGUMENT_ERROR", { path });
2116
- }
2117
- if (summary.protection === "dev") {
2118
- throw new CLIError(
2119
- `The keystore at ${path} is an unencrypted dev keystore; there is no passphrase to change.`,
2120
- "INVALID_ARGUMENT_ERROR",
2121
- { path }
2122
- );
2123
- }
2124
- const oldPassphrase = acquirePassphrase({ passphraseFile: g.passphraseFile, prompt: "Current keystore passphrase: " });
2125
- const newPassphrase = acquirePassphrase({ forcePrompt: true, confirm: true, prompt: "New keystore passphrase: " });
2126
- const rekeyed = changeKeystorePassphrase(path, oldPassphrase, newPassphrase);
2127
- print({ action: "keystore-change-passphrase", data: { path, rekeyed } });
2128
- });
2129
- }
2130
-
2131
2638
  // src/commands/config.ts
2132
- var import_node_fs9 = require("fs");
2639
+ var import_node_fs10 = require("fs");
2133
2640
 
2134
2641
  // src/config-schema.ts
2135
2642
  var CONFIG_SCHEMA = {
@@ -2258,7 +2765,7 @@ function registerConfigCommand(program, globals) {
2258
2765
  const print = (result) => console.log(formatResult(result, globals()));
2259
2766
  config.command("init").description("Create a default config file with one profile per network.").option("--force", "Overwrite an existing config file.", false).action((options) => {
2260
2767
  const p = path();
2261
- if ((0, import_node_fs9.existsSync)(p) && !options.force) {
2768
+ if ((0, import_node_fs10.existsSync)(p) && !options.force) {
2262
2769
  throw new CLIError(`Config already exists at ${p}. Use --force to overwrite.`, "INVALID_ARGUMENT_ERROR", { path: p });
2263
2770
  }
2264
2771
  writeDefaultConfigFile(p);
@@ -2439,18 +2946,18 @@ function completionScript(shell) {
2439
2946
  }
2440
2947
 
2441
2948
  // src/version.ts
2442
- var import_node_fs10 = require("fs");
2443
- var import_node_path5 = require("path");
2949
+ var import_node_fs11 = require("fs");
2950
+ var import_node_path6 = require("path");
2444
2951
  var import_node_url = require("url");
2445
2952
  function readVersion() {
2446
- let dir = (0, import_node_path5.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
2953
+ let dir = (0, import_node_path6.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
2447
2954
  for (let i = 0; i < 5; i++) {
2448
2955
  try {
2449
- const pkg = JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path5.join)(dir, "package.json"), "utf-8"));
2956
+ const pkg = JSON.parse((0, import_node_fs11.readFileSync)((0, import_node_path6.join)(dir, "package.json"), "utf-8"));
2450
2957
  if (pkg.name === "@did-btcr2/cli") return pkg.version;
2451
2958
  } catch {
2452
2959
  }
2453
- dir = (0, import_node_path5.dirname)(dir);
2960
+ dir = (0, import_node_path6.dirname)(dir);
2454
2961
  }
2455
2962
  return "0.0.0";
2456
2963
  }
@@ -2481,6 +2988,7 @@ var DidBtcr2Cli = class {
2481
2988
  opts.output = resolveOutputFormat({ output: opts.output, config: opts.config, home: opts.home });
2482
2989
  });
2483
2990
  registerInitCommand(this.program, globals);
2991
+ registerQuickstartCommand(this.program, globals);
2484
2992
  registerCreateCommand(this.program, factory, keystoreFactory, globals);
2485
2993
  registerResolveCommand(this.program, factory, globals);
2486
2994
  registerUpdateCommand(this.program, keystoreFactory, globals);
@@ -2537,6 +3045,7 @@ function handleError(error, verbose) {
2537
3045
  SUPPORTED_NETWORKS,
2538
3046
  VERSION,
2539
3047
  assertKeystoreAllowedForNetwork,
3048
+ assertSupportedNetwork,
2540
3049
  blankToUndef,
2541
3050
  defaultApiFactory,
2542
3051
  defaultConfigPath,
@@ -2547,9 +3056,11 @@ function handleError(error, verbose) {
2547
3056
  keystoreApiFactory,
2548
3057
  parseConfigFileRaw,
2549
3058
  parseHeaderList,
3059
+ persistDefaultNetwork,
2550
3060
  profileNetworkMismatch,
2551
3061
  profileToOverrides,
2552
3062
  readConfigFile,
3063
+ readConfiguredDefaultNetwork,
2553
3064
  readEnvOverrides,
2554
3065
  redactSecrets,
2555
3066
  registerCompletionCommand,
@@ -2560,6 +3071,7 @@ function handleError(error, verbose) {
2560
3071
  registerKeyCommand,
2561
3072
  registerKeystoreCommand,
2562
3073
  registerProfileCommand,
3074
+ registerQuickstartCommand,
2563
3075
  registerResolveCommand,
2564
3076
  registerUpdateCommand,
2565
3077
  resolveActiveProfile,