@wlfi-agent/cli 1.4.16 → 1.4.18

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 (97) hide show
  1. package/Cargo.lock +26 -20
  2. package/Cargo.toml +1 -1
  3. package/README.md +61 -28
  4. package/crates/vault-cli-admin/src/io_utils.rs +149 -1
  5. package/crates/vault-cli-admin/src/main.rs +639 -16
  6. package/crates/vault-cli-admin/src/shared_config.rs +18 -18
  7. package/crates/vault-cli-admin/src/tui/token_rpc.rs +190 -3
  8. package/crates/vault-cli-admin/src/tui/utils.rs +59 -0
  9. package/crates/vault-cli-admin/src/tui.rs +1205 -120
  10. package/crates/vault-cli-agent/Cargo.toml +1 -0
  11. package/crates/vault-cli-agent/src/io_utils.rs +163 -2
  12. package/crates/vault-cli-agent/src/main.rs +648 -32
  13. package/crates/vault-cli-daemon/Cargo.toml +4 -0
  14. package/crates/vault-cli-daemon/src/main.rs +617 -67
  15. package/crates/vault-cli-daemon/src/relay_sync.rs +776 -4
  16. package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +5 -0
  17. package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +32 -1
  18. package/crates/vault-daemon/src/persistence.rs +637 -100
  19. package/crates/vault-daemon/src/tests.rs +1013 -3
  20. package/crates/vault-daemon/src/tests_parts/part2.rs +99 -0
  21. package/crates/vault-daemon/src/tests_parts/part4.rs +11 -7
  22. package/crates/vault-domain/src/nonce.rs +4 -0
  23. package/crates/vault-domain/src/tests.rs +616 -0
  24. package/crates/vault-policy/src/engine.rs +55 -32
  25. package/crates/vault-policy/src/tests.rs +195 -0
  26. package/crates/vault-sdk-agent/src/lib.rs +415 -22
  27. package/crates/vault-signer/Cargo.toml +3 -0
  28. package/crates/vault-signer/src/lib.rs +266 -40
  29. package/crates/vault-transport-unix/src/lib.rs +653 -5
  30. package/crates/vault-transport-xpc/src/tests.rs +531 -3
  31. package/crates/vault-transport-xpc/tests/e2e_flow.rs +3 -0
  32. package/dist/cli.cjs +663 -190
  33. package/dist/cli.cjs.map +1 -1
  34. package/package.json +5 -2
  35. package/packages/cache/.turbo/turbo-build.log +53 -52
  36. package/packages/cache/coverage/clover.xml +529 -394
  37. package/packages/cache/coverage/coverage-final.json +2 -2
  38. package/packages/cache/coverage/index.html +21 -21
  39. package/packages/cache/coverage/src/client/index.html +1 -1
  40. package/packages/cache/coverage/src/client/index.ts.html +1 -1
  41. package/packages/cache/coverage/src/errors/index.html +1 -1
  42. package/packages/cache/coverage/src/errors/index.ts.html +12 -12
  43. package/packages/cache/coverage/src/index.html +1 -1
  44. package/packages/cache/coverage/src/index.ts.html +1 -1
  45. package/packages/cache/coverage/src/service/index.html +21 -21
  46. package/packages/cache/coverage/src/service/index.ts.html +769 -313
  47. package/packages/cache/dist/{chunk-QNK6GOTI.js → chunk-KC53LH5Z.js} +35 -2
  48. package/packages/cache/dist/chunk-KC53LH5Z.js.map +1 -0
  49. package/packages/cache/dist/{chunk-QF4XKEIA.cjs → chunk-UVU7VFE3.cjs} +35 -2
  50. package/packages/cache/dist/chunk-UVU7VFE3.cjs.map +1 -0
  51. package/packages/cache/dist/index.cjs +2 -2
  52. package/packages/cache/dist/index.js +1 -1
  53. package/packages/cache/dist/service/index.cjs +2 -2
  54. package/packages/cache/dist/service/index.js +1 -1
  55. package/packages/cache/node_modules/.bin/tsc +2 -2
  56. package/packages/cache/node_modules/.bin/tsserver +2 -2
  57. package/packages/cache/node_modules/.bin/tsup +2 -2
  58. package/packages/cache/node_modules/.bin/tsup-node +2 -2
  59. package/packages/cache/node_modules/.bin/vitest +4 -4
  60. package/packages/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  61. package/packages/cache/src/service/index.test.ts +165 -19
  62. package/packages/cache/src/service/index.ts +38 -1
  63. package/packages/config/.turbo/turbo-build.log +18 -17
  64. package/packages/config/dist/index.cjs +0 -17
  65. package/packages/config/dist/index.cjs.map +1 -1
  66. package/packages/config/src/index.ts +0 -17
  67. package/packages/rpc/.turbo/turbo-build.log +32 -31
  68. package/packages/rpc/dist/index.cjs +0 -17
  69. package/packages/rpc/dist/index.cjs.map +1 -1
  70. package/packages/rpc/src/index.js +1 -0
  71. package/packages/ui/.turbo/turbo-build.log +44 -43
  72. package/packages/ui/dist/components/badge.d.ts +1 -1
  73. package/packages/ui/dist/components/button.d.ts +1 -1
  74. package/packages/ui/node_modules/.bin/tsc +2 -2
  75. package/packages/ui/node_modules/.bin/tsserver +2 -2
  76. package/packages/ui/node_modules/.bin/tsup +2 -2
  77. package/packages/ui/node_modules/.bin/tsup-node +2 -2
  78. package/scripts/install-cli-launcher.mjs +37 -0
  79. package/scripts/install-rust-binaries.mjs +112 -0
  80. package/scripts/run-tests-isolated.mjs +210 -0
  81. package/src/cli.ts +310 -50
  82. package/src/lib/admin-reset.ts +15 -30
  83. package/src/lib/admin-setup.ts +246 -55
  84. package/src/lib/agent-auth-migrate.ts +5 -1
  85. package/src/lib/asset-broadcast.ts +15 -4
  86. package/src/lib/config-amounts.ts +6 -4
  87. package/src/lib/hidden-tty-prompt.js +1 -0
  88. package/src/lib/hidden-tty-prompt.ts +105 -0
  89. package/src/lib/keychain.ts +1 -0
  90. package/src/lib/local-admin-access.ts +4 -29
  91. package/src/lib/rust.ts +129 -33
  92. package/src/lib/signed-tx.ts +1 -0
  93. package/src/lib/sudo.ts +15 -5
  94. package/src/lib/wallet-profile.ts +3 -0
  95. package/src/lib/wallet-setup.ts +52 -0
  96. package/packages/cache/dist/chunk-QF4XKEIA.cjs.map +0 -1
  97. package/packages/cache/dist/chunk-QNK6GOTI.js.map +0 -1
package/dist/cli.cjs CHANGED
@@ -12981,6 +12981,9 @@ var require_commander = __commonJS({
12981
12981
  }
12982
12982
  });
12983
12983
 
12984
+ // src/cli.ts
12985
+ var import_promises2 = require("timers/promises");
12986
+
12984
12987
  // packages/config/src/index.js
12985
12988
  var src_exports = {};
12986
12989
  __export(src_exports, {
@@ -13069,23 +13072,6 @@ var BUILTIN_TOKENS = {
13069
13072
  arbitrum: { chainId: 42161, isNative: true, decimals: 18 }
13070
13073
  }
13071
13074
  },
13072
- usdc: {
13073
- symbol: "USDC",
13074
- chains: {
13075
- ethereum: {
13076
- chainId: 1,
13077
- isNative: false,
13078
- address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
13079
- decimals: 6
13080
- },
13081
- base: {
13082
- chainId: 8453,
13083
- isNative: false,
13084
- address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
13085
- decimals: 6
13086
- }
13087
- }
13088
- },
13089
13075
  usd1: {
13090
13076
  name: "USD1",
13091
13077
  symbol: "USD1",
@@ -14019,7 +14005,7 @@ function resolveRustBinaryPath(binaryName, config = readConfig()) {
14019
14005
  return import_node_path.default.join(config.rustBinDir ?? defaultRustBinDir(), binaryName + (process.platform === "win32" ? ".exe" : ""));
14020
14006
  }
14021
14007
 
14022
- // packages/rpc/src/index.ts
14008
+ // packages/rpc/src/index.js
14023
14009
  var src_exports2 = {};
14024
14010
  __export(src_exports2, {
14025
14011
  TRANSFER_EVENT: () => TRANSFER_EVENT,
@@ -23709,6 +23695,78 @@ function resolveLaunchDaemonHelperScriptPath(scriptName, config) {
23709
23695
  return candidates[0];
23710
23696
  }
23711
23697
 
23698
+ // src/lib/hidden-tty-prompt.ts
23699
+ async function promptHiddenTty(query, nonInteractiveError, deps = {}) {
23700
+ const input = deps.input ?? process.stdin;
23701
+ const output = deps.output ?? process.stderr;
23702
+ if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") {
23703
+ throw new Error(nonInteractiveError);
23704
+ }
23705
+ output.write("\r\x1B[2K");
23706
+ output.write(query);
23707
+ const wasRaw = Boolean(input.isRaw);
23708
+ if (!wasRaw) {
23709
+ input.setRawMode(true);
23710
+ }
23711
+ input.resume?.();
23712
+ return await new Promise((resolve, reject) => {
23713
+ let answer = "";
23714
+ let settled = false;
23715
+ const cleanup = () => {
23716
+ input.removeListener("data", onData);
23717
+ input.removeListener("error", onError);
23718
+ if (!wasRaw) {
23719
+ input.setRawMode?.(false);
23720
+ }
23721
+ input.pause?.();
23722
+ output.write("\n");
23723
+ };
23724
+ const finish = (callback) => {
23725
+ if (settled) {
23726
+ return;
23727
+ }
23728
+ settled = true;
23729
+ cleanup();
23730
+ callback();
23731
+ };
23732
+ const onError = (error) => {
23733
+ finish(() => reject(error));
23734
+ };
23735
+ const onData = (chunk) => {
23736
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
23737
+ if (text.includes("\x1B")) {
23738
+ return;
23739
+ }
23740
+ for (const char of text) {
23741
+ if (char === "\r" || char === "\n") {
23742
+ finish(() => resolve(answer));
23743
+ return;
23744
+ }
23745
+ if (char === "") {
23746
+ finish(() => reject(new Error("prompt canceled")));
23747
+ return;
23748
+ }
23749
+ if (char === "\x7F" || char === "\b") {
23750
+ const chars = Array.from(answer);
23751
+ chars.pop();
23752
+ answer = chars.join("");
23753
+ continue;
23754
+ }
23755
+ if (char === "") {
23756
+ answer = "";
23757
+ continue;
23758
+ }
23759
+ if (/[\u0000-\u001f\u007f]/u.test(char)) {
23760
+ continue;
23761
+ }
23762
+ answer += char;
23763
+ }
23764
+ };
23765
+ input.on("data", onData);
23766
+ input.on("error", onError);
23767
+ });
23768
+ }
23769
+
23712
23770
  // src/lib/sudo.ts
23713
23771
  var import_node_child_process2 = require("child_process");
23714
23772
  function currentProcessIsRoot() {
@@ -23717,6 +23775,14 @@ function currentProcessIsRoot() {
23717
23775
  function isSudoAuthenticationFailure(output) {
23718
23776
  return /sudo: .*password is required/iu.test(output) || /sudo: .*incorrect password/iu.test(output) || /sudo: no password was provided/iu.test(output) || /sorry, try again\./iu.test(output);
23719
23777
  }
23778
+ function formatSudoFailureMessage(result) {
23779
+ const combinedOutput = `${result.stderr}
23780
+ ${result.stdout}`;
23781
+ if (isSudoAuthenticationFailure(combinedOutput)) {
23782
+ return "sudo authentication failed. Enter your system admin password used for sudo, not the WLFI vault password.";
23783
+ }
23784
+ return result.stderr.trim() || result.stdout.trim() || `sudo credential check failed (exit code ${result.code})`;
23785
+ }
23720
23786
  async function runCommand(command, args, options, deps) {
23721
23787
  return await new Promise((resolve, reject) => {
23722
23788
  const spawnOptions = {
@@ -23773,9 +23839,7 @@ function createSudoSession(deps) {
23773
23839
  }
23774
23840
  );
23775
23841
  if (result.code !== 0) {
23776
- throw new Error(
23777
- result.stderr.trim() || result.stdout.trim() || `sudo credential check failed (exit code ${result.code})`
23778
- );
23842
+ throw new Error(formatSudoFailureMessage(result));
23779
23843
  }
23780
23844
  primed = true;
23781
23845
  }
@@ -23853,29 +23917,7 @@ function validateSecret(value, label) {
23853
23917
  return value;
23854
23918
  }
23855
23919
  async function promptHidden(query, label) {
23856
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
23857
- throw new Error(`${label} is required; rerun on a local TTY`);
23858
- }
23859
- const rl = import_node_readline.default.createInterface({
23860
- input: process.stdin,
23861
- output: process.stdout,
23862
- terminal: true
23863
- });
23864
- rl.stdoutMuted = true;
23865
- rl._writeToOutput = (value) => {
23866
- if (value.includes(query)) {
23867
- rl.output.write(value);
23868
- return;
23869
- }
23870
- if (!rl.stdoutMuted) {
23871
- rl.output.write(value);
23872
- }
23873
- };
23874
- const answer = await new Promise((resolve) => {
23875
- rl.question(query, resolve);
23876
- });
23877
- rl.close();
23878
- process.stdout.write("\n");
23920
+ const answer = await promptHiddenTty(query, `${label} is required; rerun on a local TTY`);
23879
23921
  return validateSecret(answer, label);
23880
23922
  }
23881
23923
  async function promptVisible(query) {
@@ -23895,8 +23937,8 @@ async function promptVisible(query) {
23895
23937
  }
23896
23938
  var sudoSession = createSudoSession({
23897
23939
  promptPassword: async () => await promptHidden(
23898
- "Root password (input hidden; required to uninstall the root daemon and delete its state): ",
23899
- "root password"
23940
+ "macOS admin password for sudo (input hidden; required to uninstall the root daemon and delete its state): ",
23941
+ "macOS admin password for sudo"
23900
23942
  )
23901
23943
  });
23902
23944
  function createProgress(message, enabled = true) {
@@ -24155,7 +24197,7 @@ async function runAdminReset(options) {
24155
24197
  const keychainAccount = import_node_os2.default.userInfo().username;
24156
24198
  if (!options.json && typeof process.geteuid === "function" && process.geteuid() !== 0) {
24157
24199
  process.stderr.write(
24158
- "Root password required: reset must uninstall the root LaunchDaemon and delete the root-managed daemon state.\n"
24200
+ "macOS admin password required: reset uses sudo to uninstall the root LaunchDaemon and delete the root-managed daemon state.\n"
24159
24201
  );
24160
24202
  }
24161
24203
  await sudoSession.prime();
@@ -24261,7 +24303,9 @@ function printUninstallSummary(result) {
24261
24303
  `managed state directory removed: ${result.daemon.stateDir}`,
24262
24304
  `managed log directory removed: ${result.daemon.logDir}`,
24263
24305
  result.local.agentKeyId ? `old agent key cleared: ${result.local.agentKeyId}` : "old agent key cleared: no configured agent key was found",
24306
+ /* c8 ignore start -- public uninstall summary only reaches here when a WLFI home existed or config provided the helper paths */
24264
24307
  result.local.wlfiHome.existed ? `local WLFI home removed: ${result.local.wlfiHome.path}` : `local WLFI home not found: ${result.local.wlfiHome.path}`,
24308
+ /* c8 ignore stop */
24265
24309
  result.local.config.existed ? `config removed: ${result.local.config.path}` : `config not found: ${result.local.config.path}`,
24266
24310
  "next: reinstall with `wlfi-agent admin setup` only if you want a fresh managed wallet again"
24267
24311
  ];
@@ -24273,7 +24317,7 @@ async function runAdminUninstall(options) {
24273
24317
  const keychainAccount = import_node_os2.default.userInfo().username;
24274
24318
  if (!options.json && typeof process.geteuid === "function" && process.geteuid() !== 0) {
24275
24319
  process.stderr.write(
24276
- "Root password required: uninstall must remove the root LaunchDaemon and all managed root-owned files.\n"
24320
+ "macOS admin password required: uninstall uses sudo to remove the root LaunchDaemon and all managed root-owned files.\n"
24277
24321
  );
24278
24322
  }
24279
24323
  await sudoSession.prime();
@@ -24935,6 +24979,7 @@ async function prepareSpawnOptions(binaryName, args, options) {
24935
24979
 
24936
24980
  // src/lib/rust.ts
24937
24981
  var { readConfig: readConfig2, resolveRustBinaryPath: resolveRustBinaryPath2 } = src_exports;
24982
+ var MAX_STDOUT_SNIPPET_CHARS = 200;
24938
24983
  var RustBinaryExitError = class extends Error {
24939
24984
  binaryName;
24940
24985
  code;
@@ -24949,6 +24994,20 @@ var RustBinaryExitError = class extends Error {
24949
24994
  this.stderr = stderr;
24950
24995
  }
24951
24996
  };
24997
+ var RustBinaryJsonParseError = class extends Error {
24998
+ binaryName;
24999
+ stdout;
25000
+ cause;
25001
+ constructor(binaryName, stdout, cause) {
25002
+ const snippet = stdout.length > MAX_STDOUT_SNIPPET_CHARS ? `${stdout.slice(0, MAX_STDOUT_SNIPPET_CHARS)}...` : stdout;
25003
+ const message = cause instanceof Error ? `${binaryName} produced invalid JSON: ${cause.message}. stdout: ${snippet}` : `${binaryName} produced invalid JSON. stdout: ${snippet}`;
25004
+ super(message);
25005
+ this.name = "RustBinaryJsonParseError";
25006
+ this.binaryName = binaryName;
25007
+ this.stdout = stdout;
25008
+ this.cause = cause;
25009
+ }
25010
+ };
24952
25011
  function ensureBinary(binaryName, config) {
24953
25012
  const resolved = resolveRustBinaryPath2(binaryName, config ?? readConfig2());
24954
25013
  if (!import_node_fs7.default.existsSync(resolved)) {
@@ -24964,6 +25023,37 @@ function forwardedArgsIncludeDaemonSocket(args) {
24964
25023
  (arg, _index) => arg === "--daemon-socket" || arg.startsWith("--daemon-socket=")
24965
25024
  );
24966
25025
  }
25026
+ async function writeChildStdin(child, stdin) {
25027
+ const stream = child.stdin;
25028
+ if (!stream) {
25029
+ return;
25030
+ }
25031
+ await new Promise((resolve, reject) => {
25032
+ let settled = false;
25033
+ const finish = () => {
25034
+ if (settled) {
25035
+ return;
25036
+ }
25037
+ settled = true;
25038
+ stream.off("error", handleError);
25039
+ resolve();
25040
+ };
25041
+ const handleError = (error) => {
25042
+ if (settled) {
25043
+ return;
25044
+ }
25045
+ if (error?.code === "EPIPE" || error?.code === "ERR_STREAM_DESTROYED") {
25046
+ finish();
25047
+ return;
25048
+ }
25049
+ settled = true;
25050
+ stream.off("error", handleError);
25051
+ reject(error);
25052
+ };
25053
+ stream.on("error", handleError);
25054
+ stream.end(stdin, finish);
25055
+ });
25056
+ }
24967
25057
  async function passthroughRustBinary(binaryName, args, config) {
24968
25058
  const resolvedConfig = config ?? readConfig2();
24969
25059
  const resolvedDaemonSocket = resolveValidatedPassthroughDaemonSocket(
@@ -24974,20 +25064,25 @@ async function passthroughRustBinary(binaryName, args, config) {
24974
25064
  const executable = ensureBinary(binaryName, resolvedConfig);
24975
25065
  const prepared = await prepareSpawnOptions(binaryName, args, {});
24976
25066
  const env = { ...prepared.env };
24977
- if (resolvedDaemonSocket && !forwardedArgsIncludeDaemonSocket(args) && !env.WLFI_DAEMON_SOCKET?.trim()) {
25067
+ if (resolvedDaemonSocket && !forwardedArgsIncludeDaemonSocket(args) && /* c8 ignore next -- explicit env override is exercised, but c8 misattributes this optional-chain/nullish check */
25068
+ !env.WLFI_DAEMON_SOCKET?.trim()) {
24978
25069
  env.WLFI_DAEMON_SOCKET = resolvedDaemonSocket;
24979
25070
  }
24980
25071
  const child = (0, import_node_child_process3.spawn)(executable, prepared.args, {
24981
- stdio: [prepared.stdin !== void 0 ? "pipe" : "inherit", "inherit", "inherit"],
25072
+ stdio: [
25073
+ prepared.stdin !== void 0 ? "pipe" : "inherit",
25074
+ "inherit",
25075
+ "inherit"
25076
+ ],
24982
25077
  env
24983
25078
  });
24984
- if (prepared.stdin !== void 0) {
24985
- child.stdin?.end(prepared.stdin);
24986
- }
24987
- return await new Promise((resolve, reject) => {
25079
+ const codePromise = new Promise((resolve, reject) => {
24988
25080
  child.on("error", reject);
24989
- child.on("close", (code) => resolve(code ?? 1));
25081
+ child.on("close", (code2) => resolve(code2 ?? 1));
24990
25082
  });
25083
+ const stdinPromise = prepared.stdin !== void 0 ? writeChildStdin(child, prepared.stdin) : Promise.resolve();
25084
+ const [code] = await Promise.all([codePromise, stdinPromise]);
25085
+ return code;
24991
25086
  }
24992
25087
  async function runRustBinary(binaryName, args, config, options = {}) {
24993
25088
  const resolvedConfig = config ?? readConfig2();
@@ -25014,13 +25109,20 @@ async function runRustBinary(binaryName, args, config, options = {}) {
25014
25109
  child.stderr?.on("data", (chunk) => {
25015
25110
  stderr += chunk.toString();
25016
25111
  });
25017
- if (prepared.stdin !== void 0) {
25018
- child.stdin?.end(prepared.stdin);
25019
- }
25020
- const code = await new Promise((resolve, reject) => {
25112
+ const codePromise = new Promise((resolve, reject) => {
25021
25113
  child.on("error", reject);
25022
- child.on("close", (value) => resolve(value ?? 1));
25114
+ child.on("close", (code2, signal) => {
25115
+ if (code2 !== null && code2 !== void 0) {
25116
+ resolve(code2);
25117
+ } else {
25118
+ resolve(
25119
+ signal ? 128 + (require("os").constants.errno?.SIGTERM ?? 143) : 1
25120
+ );
25121
+ }
25122
+ });
25023
25123
  });
25124
+ const stdinPromise = prepared.stdin !== void 0 ? writeChildStdin(child, prepared.stdin) : Promise.resolve();
25125
+ const [code] = await Promise.all([codePromise, stdinPromise]);
25024
25126
  if (code !== 0) {
25025
25127
  throw new RustBinaryExitError(binaryName, code, stdout, stderr);
25026
25128
  }
@@ -25028,7 +25130,11 @@ async function runRustBinary(binaryName, args, config, options = {}) {
25028
25130
  }
25029
25131
  async function runRustBinaryJson(binaryName, args, config, options = {}) {
25030
25132
  const { stdout } = await runRustBinary(binaryName, args, config, options);
25031
- return JSON.parse(stdout);
25133
+ try {
25134
+ return JSON.parse(stdout);
25135
+ } catch (error) {
25136
+ throw new RustBinaryJsonParseError(binaryName, stdout, error);
25137
+ }
25032
25138
  }
25033
25139
 
25034
25140
  // src/lib/wallet-profile.ts
@@ -25126,6 +25232,7 @@ function collectWalletBalanceTargets(config) {
25126
25232
  symbol: tokenProfile.symbol,
25127
25233
  name: tokenProfile.name,
25128
25234
  chainKey,
25235
+ /* c8 ignore next -- resolveChainProfile only yields usable balance targets when a chain name is available */
25129
25236
  chainName: resolvedChain?.name ?? chainKey,
25130
25237
  chainId: chainProfile.chainId,
25131
25238
  rpcUrl,
@@ -25623,6 +25730,22 @@ function resolveValidatedWalletSetupPolicyIds(values) {
25623
25730
  normalizedList(values).map((value) => assertWalletSetupUuid(value, "attachPolicyId"))
25624
25731
  );
25625
25732
  }
25733
+ function resolveValidatedExistingVaultReuse(input) {
25734
+ const keyId = presentString4(input.existingVaultKeyId);
25735
+ const publicKey = presentString4(input.existingVaultPublicKey);
25736
+ if (!keyId && !publicKey) {
25737
+ return {};
25738
+ }
25739
+ if (!keyId || !publicKey) {
25740
+ throw new Error(
25741
+ "existingVaultKeyId and existingVaultPublicKey must be provided together to reuse an existing wallet"
25742
+ );
25743
+ }
25744
+ return {
25745
+ keyId: assertWalletSetupUuid(keyId, "existingVaultKeyId"),
25746
+ publicKey
25747
+ };
25748
+ }
25626
25749
  function previewBootstrapOutputPath(inputPath) {
25627
25750
  const explicitPath = presentString4(inputPath);
25628
25751
  if (explicitPath) {
@@ -26040,7 +26163,9 @@ function formatBooleanPlanValue(value) {
26040
26163
  }
26041
26164
  function formatWalletSetupScope(plan) {
26042
26165
  return [
26166
+ /* c8 ignore next -- both default-network and explicit-network renderings are exercised, but c8 misattributes this ternary under --experimental-strip-types */
26043
26167
  `- Network: ${plan.policyScope.network === null ? "daemon default" : String(plan.policyScope.network)}`,
26168
+ /* c8 ignore next -- both default and explicit chain-name renderings are exercised, but c8 misattributes this nullish expression */
26044
26169
  `- Chain Name: ${plan.policyScope.chainName ?? "daemon default"}`,
26045
26170
  `- Recipient: ${plan.policyScope.recipient ?? "all recipients"}`,
26046
26171
  `- Asset Mode: ${plan.policyScope.assets.mode}`,
@@ -26066,6 +26191,7 @@ function formatWalletSetupPreflight(plan) {
26066
26191
  return [
26067
26192
  `- Daemon Socket Trusted: ${formatBooleanPlanValue(plan.preflight.daemonSocketTrusted)}`,
26068
26193
  plan.preflight.daemonSocketError ? ` ${plan.preflight.daemonSocketError}` : null,
26194
+ /* c8 ignore next -- both null and boolean RPC preflight states are exercised, but c8 misattributes this ternary */
26069
26195
  `- RPC URL Trusted: ${plan.preflight.rpcUrlTrusted === null ? "not applicable" : formatBooleanPlanValue(plan.preflight.rpcUrlTrusted)}`,
26070
26196
  plan.preflight.rpcUrlError ? ` ${plan.preflight.rpcUrlError}` : null,
26071
26197
  `- Bootstrap Output Ready: ${formatBooleanPlanValue(plan.preflight.bootstrapOutputReady)}`,
@@ -26076,6 +26202,7 @@ function formatWalletSetupPlanText(plan) {
26076
26202
  const bootstrapOutputLabel = `${plan.bootstrapOutput.path} (${plan.bootstrapOutput.autoGenerated ? "auto-generated" : "explicit"}, ${plan.bootstrapOutput.cleanupAction} after import)`;
26077
26203
  const lines = [
26078
26204
  "Wallet Setup Preview",
26205
+ /* c8 ignore next -- allowed and blocked admin access renderings are both exercised, but c8 misattributes this ternary */
26079
26206
  `Admin Access: ${plan.adminAccess.permitted ? "allowed" : "blocked"} (${plan.adminAccess.mode})`,
26080
26207
  `Admin Access Reason: ${plan.adminAccess.reason}`,
26081
26208
  `Daemon Socket: ${plan.daemonSocket}`,
@@ -26098,8 +26225,11 @@ function formatWalletSetupPlanText(plan) {
26098
26225
  "Config After Setup",
26099
26226
  `- Agent Key ID: ${plan.configAfterSetup.agentKeyId}`,
26100
26227
  `- Daemon Socket: ${plan.configAfterSetup.daemonSocket}`,
26228
+ /* c8 ignore next -- unchanged and explicit chain-id renderings are both exercised, but c8 misattributes this ternary */
26101
26229
  `- Chain ID: ${plan.configAfterSetup.chainId === null ? "unchanged" : String(plan.configAfterSetup.chainId)}`,
26230
+ /* c8 ignore next -- unchanged and explicit chain-name renderings are both exercised, but c8 misattributes this nullish expression */
26102
26231
  `- Chain Name: ${plan.configAfterSetup.chainName ?? "unchanged"}`,
26232
+ /* c8 ignore next -- unchanged and explicit RPC URL renderings are both exercised, but c8 misattributes this nullish expression */
26103
26233
  `- RPC URL: ${plan.configAfterSetup.rpcUrl ?? "unchanged"}`,
26104
26234
  "",
26105
26235
  "Preflight",
@@ -26146,6 +26276,7 @@ function buildWalletSetupAdminArgs(input) {
26146
26276
  const tokens = resolveValidatedWalletSetupTokenList(input.token);
26147
26277
  const policyIds = resolveValidatedWalletSetupPolicyIds(input.attachPolicyId);
26148
26278
  const fromSharedConfig = shouldBootstrapFromSharedConfig(input);
26279
+ const existingVaultReuse = resolveValidatedExistingVaultReuse(input);
26149
26280
  if (input.vaultPassword) {
26150
26281
  throw new Error(
26151
26282
  "insecure vaultPassword is disabled; use vaultPasswordStdin or an interactive prompt"
@@ -26164,6 +26295,14 @@ function buildWalletSetupAdminArgs(input) {
26164
26295
  if (fromSharedConfig) {
26165
26296
  args.push("--from-shared-config");
26166
26297
  }
26298
+ if (existingVaultReuse.keyId && existingVaultReuse.publicKey) {
26299
+ args.push(
26300
+ "--existing-vault-key-id",
26301
+ existingVaultReuse.keyId,
26302
+ "--existing-vault-public-key",
26303
+ existingVaultReuse.publicKey
26304
+ );
26305
+ }
26167
26306
  const appendValue = (flag, value) => {
26168
26307
  if (value) {
26169
26308
  args.push(flag, value);
@@ -26244,7 +26383,8 @@ function completeWalletSetup(options, deps = {}) {
26244
26383
  }
26245
26384
  if (options.network !== void 0) {
26246
26385
  nextConfig.chainId = assertPositiveChainId(options.network);
26247
- nextConfig.chainName = normalizedChainName ?? configuredChainName(nextConfig.chainId, currentConfig) ?? `chain-${nextConfig.chainId}`;
26386
+ nextConfig.chainName = normalizedChainName ?? /* c8 ignore next -- configured-chain and synthetic fallback label paths are exercised, but c8 misattributes this nullish expression */
26387
+ configuredChainName(nextConfig.chainId, currentConfig) ?? `chain-${nextConfig.chainId}`;
26248
26388
  nextConfig.rpcUrl = normalizedRpcUrl ?? configuredRpcUrl(nextConfig.chainId, currentConfig);
26249
26389
  }
26250
26390
  storeAgentAuthToken(credentials.agentKeyId, credentials.agentAuthToken);
@@ -26302,31 +26442,9 @@ async function readTrimmedStdin(label) {
26302
26442
  }
26303
26443
  return validateSecret4(raw.replace(/[\r\n]+$/u, ""), label);
26304
26444
  }
26305
- async function promptHidden2(query) {
26306
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
26307
- throw new Error("vault password is required; use --vault-password-stdin or a local TTY prompt");
26308
- }
26309
- const rl = import_node_readline2.default.createInterface({
26310
- input: process.stdin,
26311
- output: process.stdout,
26312
- terminal: true
26313
- });
26314
- rl.stdoutMuted = true;
26315
- rl._writeToOutput = (value) => {
26316
- if (value.includes(query)) {
26317
- rl.output.write(value);
26318
- return;
26319
- }
26320
- if (!rl.stdoutMuted) {
26321
- rl.output.write(value);
26322
- }
26323
- };
26324
- const answer = await new Promise((resolve) => {
26325
- rl.question(query, resolve);
26326
- });
26327
- rl.close();
26328
- process.stdout.write("\n");
26329
- return validateSecret4(answer, "vault password");
26445
+ async function promptHidden2(query, label = "vault password", nonInteractiveError = "vault password is required; use --vault-password-stdin or a local TTY prompt") {
26446
+ const answer = await promptHiddenTty(query, nonInteractiveError);
26447
+ return validateSecret4(answer, label);
26330
26448
  }
26331
26449
  async function promptVisible2(query, nonInteractiveError) {
26332
26450
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
@@ -26369,6 +26487,28 @@ function resolveExistingWalletSetupTarget(config) {
26369
26487
  hasLegacyAgentAuthToken
26370
26488
  };
26371
26489
  }
26490
+ function resolveReusableWalletSetupTarget(config) {
26491
+ let walletProfile;
26492
+ try {
26493
+ walletProfile = resolveWalletProfile(config);
26494
+ } catch (error) {
26495
+ throw new Error(
26496
+ `--reuse-existing-wallet requires a local wallet to reuse; ${renderError5(error)}`
26497
+ );
26498
+ }
26499
+ const existingVaultKeyId = walletProfile.vaultKeyId?.trim();
26500
+ const existingVaultPublicKey = walletProfile.vaultPublicKey?.trim();
26501
+ if (!existingVaultKeyId || !existingVaultPublicKey) {
26502
+ throw new Error(
26503
+ "--reuse-existing-wallet requires wallet.vaultKeyId and wallet.vaultPublicKey in the local wallet profile"
26504
+ );
26505
+ }
26506
+ return {
26507
+ address: walletProfile.address?.trim() || deriveWalletAddress2(existingVaultPublicKey),
26508
+ existingVaultKeyId,
26509
+ existingVaultPublicKey
26510
+ };
26511
+ }
26372
26512
  async function confirmAdminSetupOverwrite(options, config, deps = {}) {
26373
26513
  const existing = resolveExistingWalletSetupTarget(config);
26374
26514
  if (!existing || options.yes) {
@@ -26376,12 +26516,12 @@ async function confirmAdminSetupOverwrite(options, config, deps = {}) {
26376
26516
  }
26377
26517
  if (options.nonInteractive) {
26378
26518
  throw new Error(
26379
- "`wlfi-agent admin setup` would overwrite the existing wallet; rerun with --yes in non-interactive mode"
26519
+ options.reuseExistingWallet ? "`wlfi-agent admin setup --reuse-existing-wallet` would refresh the existing local wallet metadata and agent credentials; rerun with --yes in non-interactive mode" : "`wlfi-agent admin setup` would overwrite the existing wallet; rerun with --yes in non-interactive mode"
26380
26520
  );
26381
26521
  }
26382
26522
  const stderr = deps.stderr ?? process.stderr;
26383
26523
  stderr.write(
26384
- "warning: admin setup will overwrite the current local wallet metadata and agent credentials.\n"
26524
+ options.reuseExistingWallet ? "warning: admin setup will reuse the current vault and refresh the local wallet metadata and agent credentials.\n" : "warning: admin setup will overwrite the current local wallet metadata and agent credentials.\n"
26385
26525
  );
26386
26526
  if (existing.address) {
26387
26527
  stderr.write(`current address: ${existing.address}
@@ -26394,11 +26534,13 @@ async function confirmAdminSetupOverwrite(options, config, deps = {}) {
26394
26534
  if (existing.hasLegacyAgentAuthToken) {
26395
26535
  stderr.write("legacy agent auth token is still present in config.json\n");
26396
26536
  }
26537
+ const confirmationToken = options.reuseExistingWallet ? "REUSE" : "OVERWRITE";
26538
+ const confirmationPrompt = options.reuseExistingWallet ? "Type REUSE to reattach the current local vault: " : "Type OVERWRITE to replace the current local wallet: ";
26397
26539
  const confirmation = await (deps.prompt ?? ((query) => promptVisible2(
26398
26540
  query,
26399
- "`wlfi-agent admin setup` requires --yes in non-interactive environments when overwriting an existing wallet"
26400
- )))("Type OVERWRITE to replace the current local wallet: ");
26401
- if (confirmation !== "OVERWRITE") {
26541
+ options.reuseExistingWallet ? "`wlfi-agent admin setup --reuse-existing-wallet` requires --yes in non-interactive environments when reusing an existing wallet" : "`wlfi-agent admin setup` requires --yes in non-interactive environments when overwriting an existing wallet"
26542
+ )))(confirmationPrompt);
26543
+ if (confirmation !== confirmationToken) {
26402
26544
  throw new Error("admin setup aborted");
26403
26545
  }
26404
26546
  }
@@ -26429,7 +26571,9 @@ async function resolveAdminSetupVaultPassword(options, deps = {}) {
26429
26571
  "vault password is required in non-interactive mode; use --vault-password-stdin"
26430
26572
  );
26431
26573
  }
26432
- return promptForVaultPassword("Vault password (input hidden; nothing will be echoed): ");
26574
+ return promptForVaultPassword(
26575
+ "Vault password (input hidden; this unlocks the wallet, not sudo): "
26576
+ );
26433
26577
  }
26434
26578
  function resolveDaemonSocket(optionValue) {
26435
26579
  return import_node_path8.default.resolve(optionValue?.trim() || DEFAULT_MANAGED_DAEMON_SOCKET2);
@@ -26460,6 +26604,7 @@ function createAdminSetupPlan(options, deps = {}) {
26460
26604
  installError = renderError5(error);
26461
26605
  }
26462
26606
  const existingWallet = resolveExistingWalletSetupTarget(config);
26607
+ const reusableWallet = options.reuseExistingWallet ? resolveReusableWalletSetupTarget(config) : null;
26463
26608
  return {
26464
26609
  command: "setup",
26465
26610
  mode: "plan",
@@ -26502,6 +26647,8 @@ function createAdminSetupPlan(options, deps = {}) {
26502
26647
  recipient: options.recipient,
26503
26648
  attachPolicyId: options.attachPolicyId,
26504
26649
  attachBootstrapPolicies: options.attachBootstrapPolicies,
26650
+ existingVaultKeyId: reusableWallet?.existingVaultKeyId,
26651
+ existingVaultPublicKey: reusableWallet?.existingVaultPublicKey,
26505
26652
  bootstrapOutputPath: options.bootstrapOutput,
26506
26653
  deleteBootstrapOutput: options.deleteBootstrapOutput
26507
26654
  },
@@ -26826,7 +26973,9 @@ ${error.stdout}`;
26826
26973
  }
26827
26974
  var sudoSession2 = createSudoSession({
26828
26975
  promptPassword: async () => await promptHidden2(
26829
- "Root password (input hidden; required to install or recover the root daemon): "
26976
+ "macOS admin password for sudo (input hidden; required to install or recover the root daemon): ",
26977
+ "macOS admin password for sudo",
26978
+ "macOS admin password for sudo is required; rerun on a local TTY"
26830
26979
  )
26831
26980
  });
26832
26981
  async function managedStateFileExists(stateFile) {
@@ -26841,6 +26990,134 @@ async function managedStateFileExists(stateFile) {
26841
26990
  result.stderr.trim() || result.stdout.trim() || `failed to inspect managed daemon state file '${stateFile}' (exit code ${result.code})`
26842
26991
  );
26843
26992
  }
26993
+ async function inspectManagedState(stateFile, showProgress, message = "Inspecting managed daemon state") {
26994
+ await sudoSession2.prime();
26995
+ const stateProbeProgress = createProgress2(message, showProgress);
26996
+ let stateExists;
26997
+ try {
26998
+ stateExists = await managedStateFileExists(stateFile);
26999
+ stateProbeProgress.succeed(
27000
+ stateExists ? "Managed daemon state already exists" : "No managed daemon state found"
27001
+ );
27002
+ } catch (error) {
27003
+ stateProbeProgress.fail();
27004
+ throw error;
27005
+ }
27006
+ return stateExists;
27007
+ }
27008
+ function createManagedStatePasswordMismatchError(stateFile) {
27009
+ return new Error(
27010
+ `managed daemon state already exists at ${stateFile} and is encrypted with a different vault password. Re-run setup with the original vault password, or remove/reset the managed daemon state before initializing a fresh wallet.`
27011
+ );
27012
+ }
27013
+ function isManagedStatePasswordMismatch(output) {
27014
+ return /failed to decrypt state|wrong password or tampered file|authentication failed/iu.test(
27015
+ output
27016
+ );
27017
+ }
27018
+ async function managedStateAcceptsRequestedVaultPassword(config, daemonSocket, stateFile, vaultPassword) {
27019
+ const installPreconditions = assertManagedDaemonInstallPreconditions(
27020
+ config,
27021
+ daemonSocket,
27022
+ stateFile
27023
+ );
27024
+ const tempRoot = import_node_fs9.default.mkdtempSync(import_node_path8.default.join(import_node_os3.default.tmpdir(), "wlfi-managed-state-probe-"));
27025
+ const tempSocket = import_node_path8.default.join(tempRoot, "daemon.sock");
27026
+ const allowedUid = String(process.getuid?.() ?? process.geteuid?.() ?? 0);
27027
+ const probeScript = [
27028
+ "set -euo pipefail",
27029
+ 'vault_password="$(cat)"',
27030
+ 'daemon_bin="$1"',
27031
+ 'state_file="$2"',
27032
+ 'daemon_socket="$3"',
27033
+ 'admin_uid="$4"',
27034
+ 'agent_uid="$5"',
27035
+ 'signer_backend="$6"',
27036
+ '"$daemon_bin" \\',
27037
+ " --non-interactive \\",
27038
+ " --vault-password-stdin \\",
27039
+ ' --state-file "$state_file" \\',
27040
+ ' --daemon-socket "$daemon_socket" \\',
27041
+ ' --signer-backend "$signer_backend" \\',
27042
+ ' --allow-admin-euid "$admin_uid" \\',
27043
+ ' --allow-agent-euid "$agent_uid" <<<"$vault_password" &',
27044
+ 'child="$!"',
27045
+ "for _ in $(seq 1 20); do",
27046
+ ' if ! kill -0 "$child" 2>/dev/null; then',
27047
+ ' wait "$child"',
27048
+ " exit $?",
27049
+ " fi",
27050
+ " sleep 0.25",
27051
+ "done",
27052
+ 'kill "$child" >/dev/null 2>&1 || true',
27053
+ 'wait "$child" >/dev/null 2>&1 || true',
27054
+ "exit 0"
27055
+ ].join("\n");
27056
+ try {
27057
+ const result = await sudoSession2.run(
27058
+ [
27059
+ "/bin/bash",
27060
+ "-lc",
27061
+ probeScript,
27062
+ "--",
27063
+ installPreconditions.daemonBin,
27064
+ stateFile,
27065
+ tempSocket,
27066
+ allowedUid,
27067
+ allowedUid,
27068
+ DEFAULT_SIGNER_BACKEND
27069
+ ],
27070
+ {
27071
+ stdin: `${vaultPassword}
27072
+ `
27073
+ }
27074
+ );
27075
+ if (result.code === 0) {
27076
+ return true;
27077
+ }
27078
+ const combinedOutput = `${result.stderr}
27079
+ ${result.stdout}`.trim();
27080
+ if (isManagedStatePasswordMismatch(combinedOutput)) {
27081
+ return false;
27082
+ }
27083
+ throw new Error(
27084
+ combinedOutput || `failed to validate the managed daemon state with the requested vault password (exit code ${result.code})`
27085
+ );
27086
+ } finally {
27087
+ import_node_fs9.default.rmSync(tempRoot, { recursive: true, force: true });
27088
+ }
27089
+ }
27090
+ async function assertManagedStateMatchesRequestedVaultPasswordBeforeInstall(config, daemonSocket, stateFile, vaultPassword, showProgress) {
27091
+ const stateExists = await inspectManagedState(
27092
+ stateFile,
27093
+ showProgress,
27094
+ "Inspecting managed daemon state before install"
27095
+ );
27096
+ if (!stateExists) {
27097
+ return;
27098
+ }
27099
+ const verifyProgress = createProgress2(
27100
+ "Verifying the requested vault password against the managed daemon state",
27101
+ showProgress
27102
+ );
27103
+ let accepted;
27104
+ try {
27105
+ accepted = await managedStateAcceptsRequestedVaultPassword(
27106
+ config,
27107
+ daemonSocket,
27108
+ stateFile,
27109
+ vaultPassword
27110
+ );
27111
+ } catch (error) {
27112
+ verifyProgress.fail();
27113
+ throw error;
27114
+ }
27115
+ if (!accepted) {
27116
+ verifyProgress.fail("Existing daemon password does not unlock the stored daemon state");
27117
+ throw createManagedStatePasswordMismatchError(stateFile);
27118
+ }
27119
+ verifyProgress.succeed("Requested vault password unlocks the existing managed daemon state");
27120
+ }
26844
27121
  async function waitForTrustedDaemonSocket(targetPath, timeoutMs = 15e3) {
26845
27122
  async function canConnect(socketPath) {
26846
27123
  return new Promise((resolve) => {
@@ -27019,6 +27296,7 @@ async function runAdminSetup(options) {
27019
27296
  const config = readConfig();
27020
27297
  const daemonSocket = resolveDaemonSocket(options.daemonSocket);
27021
27298
  const stateFile = resolveStateFile();
27299
+ const reusableWallet = options.reuseExistingWallet ? resolveReusableWalletSetupTarget(config) : null;
27022
27300
  assertManagedDaemonInstallPreconditions(config, daemonSocket, stateFile);
27023
27301
  await confirmAdminSetupOverwrite(options, config);
27024
27302
  const vaultPassword = await resolveAdminSetupVaultPassword(options);
@@ -27076,10 +27354,17 @@ async function runAdminSetup(options) {
27076
27354
  }
27077
27355
  if (!options.json && typeof process.geteuid === "function" && process.geteuid() !== 0) {
27078
27356
  process.stderr.write(
27079
- "Root password required: setup must install or recover the root LaunchDaemon and store the daemon password in System Keychain.\n"
27357
+ "macOS admin password required: setup uses sudo to install or recover the root LaunchDaemon and store the daemon password in System Keychain.\n"
27080
27358
  );
27081
27359
  }
27082
27360
  await sudoSession2.prime();
27361
+ await assertManagedStateMatchesRequestedVaultPasswordBeforeInstall(
27362
+ config,
27363
+ daemonSocket,
27364
+ stateFile,
27365
+ vaultPassword,
27366
+ showProgress
27367
+ );
27083
27368
  const installProgress = createProgress2("Installing and restarting daemon", showProgress);
27084
27369
  try {
27085
27370
  daemon = await installLaunchDaemon(config, daemonSocket, stateFile, vaultPassword);
@@ -27109,34 +27394,20 @@ async function runAdminSetup(options) {
27109
27394
  if (!daemonAcceptedPassword) {
27110
27395
  if (existingDaemonRejectedPassword) {
27111
27396
  authProgress.fail("Existing daemon password does not unlock the stored daemon state");
27112
- throw new Error(
27113
- `managed daemon state already exists at ${stateFile} and is encrypted with a different vault password. Re-run setup with the original vault password, or remove/reset the managed daemon state before initializing a fresh wallet.`
27114
- );
27397
+ throw createManagedStatePasswordMismatchError(stateFile);
27115
27398
  }
27116
27399
  authProgress.info("Daemon rejected the requested vault password; inspecting managed state");
27117
- const stateProbeProgress = createProgress2("Inspecting managed daemon state", showProgress);
27118
- let stateExists;
27119
- try {
27120
- stateExists = await managedStateFileExists(stateFile);
27121
- stateProbeProgress.succeed(
27122
- stateExists ? "Managed daemon state already exists" : "No managed daemon state found"
27123
- );
27124
- } catch (error) {
27125
- stateProbeProgress.fail();
27126
- throw error;
27127
- }
27400
+ const stateExists = await inspectManagedState(stateFile, showProgress);
27128
27401
  if (stateExists) {
27129
27402
  authProgress.fail("Existing daemon password does not unlock the stored daemon state");
27130
- throw new Error(
27131
- `managed daemon state already exists at ${stateFile} and is encrypted with a different vault password. Re-run setup with the original vault password, or remove/reset the managed daemon state before initializing a fresh wallet.`
27132
- );
27403
+ throw createManagedStatePasswordMismatchError(stateFile);
27133
27404
  }
27134
27405
  authProgress.fail(
27135
27406
  "Existing daemon password differs; reinstalling with the requested vault password"
27136
27407
  );
27137
27408
  if (!options.json && typeof process.geteuid === "function" && process.geteuid() !== 0) {
27138
27409
  process.stderr.write(
27139
- "Root password required: setup must reinstall the root LaunchDaemon to rotate the managed daemon password.\n"
27410
+ "macOS admin password required: setup uses sudo to reinstall the root LaunchDaemon and rotate the managed daemon password.\n"
27140
27411
  );
27141
27412
  }
27142
27413
  await sudoSession2.prime();
@@ -27216,6 +27487,8 @@ async function runAdminSetup(options) {
27216
27487
  recipient: options.recipient,
27217
27488
  attachPolicyId: options.attachPolicyId,
27218
27489
  attachBootstrapPolicies: options.attachBootstrapPolicies,
27490
+ existingVaultKeyId: reusableWallet?.existingVaultKeyId,
27491
+ existingVaultPublicKey: reusableWallet?.existingVaultPublicKey,
27219
27492
  bootstrapOutputPath: bootstrapOutput.path
27220
27493
  });
27221
27494
  const bootstrapProgress = createProgress2("Setting up wallet access", showProgress);
@@ -27338,6 +27611,8 @@ function buildAdminSetupBootstrapInvocation(input) {
27338
27611
  recipient: input.recipient,
27339
27612
  attachPolicyId: input.attachPolicyId,
27340
27613
  attachBootstrapPolicies: input.attachBootstrapPolicies,
27614
+ existingVaultKeyId: input.existingVaultKeyId,
27615
+ existingVaultPublicKey: input.existingVaultPublicKey,
27341
27616
  bootstrapOutputPath: input.bootstrapOutputPath
27342
27617
  }),
27343
27618
  stdin: `${validateSecret4(input.vaultPassword, "vault password")}
@@ -27396,7 +27671,11 @@ async function runAdminSetupCli(argv) {
27396
27671
  const program2 = new Command();
27397
27672
  program2.name("wlfi-agent admin setup").description(
27398
27673
  "Store the vault password, install the root daemon autostart, set up wallet access, and print the wallet address"
27399
- ).option("--plan", "Print a sanitized setup preview without prompting or mutating state", false).option("--vault-password-stdin", "Read vault password from stdin", false).option("--non-interactive", "Disable password prompts", false).option("-y, --yes", "Skip the overwrite confirmation prompt", false).option("--daemon-socket <path>", "Daemon unix socket path").option("--per-tx-max-wei <wei>", "Per-transaction max spend in wei").option("--daily-max-wei <wei>", "Daily max spend in wei").option("--weekly-max-wei <wei>", "Weekly max spend in wei").option("--max-gas-per-chain-wei <wei>", "Per-chain gas-spend ceiling in wei").option("--daily-max-tx-count <count>", "Optional daily tx-count cap").option("--per-tx-max-fee-per-gas-wei <wei>", "Optional max fee-per-gas cap").option("--per-tx-max-priority-fee-per-gas-wei <wei>", "Optional max priority fee-per-gas cap").option("--per-tx-max-calldata-bytes <bytes>", "Optional calldata size cap").option(
27674
+ ).option("--plan", "Print a sanitized setup preview without prompting or mutating state", false).option("--vault-password-stdin", "Read vault password from stdin", false).option("--non-interactive", "Disable password prompts", false).option("-y, --yes", "Skip the overwrite confirmation prompt", false).option(
27675
+ "--reuse-existing-wallet",
27676
+ "Reuse the current local vault instead of generating a fresh wallet",
27677
+ false
27678
+ ).option("--daemon-socket <path>", "Daemon unix socket path").option("--per-tx-max-wei <wei>", "Per-transaction max spend in wei").option("--daily-max-wei <wei>", "Daily max spend in wei").option("--weekly-max-wei <wei>", "Weekly max spend in wei").option("--max-gas-per-chain-wei <wei>", "Per-chain gas-spend ceiling in wei").option("--daily-max-tx-count <count>", "Optional daily tx-count cap").option("--per-tx-max-fee-per-gas-wei <wei>", "Optional max fee-per-gas cap").option("--per-tx-max-priority-fee-per-gas-wei <wei>", "Optional max priority fee-per-gas cap").option("--per-tx-max-calldata-bytes <bytes>", "Optional calldata size cap").option(
27400
27679
  "--token <address>",
27401
27680
  "Allowed ERC-20 token address",
27402
27681
  (value, acc) => {
@@ -27531,8 +27810,12 @@ function resolveConfiguredAgentKeyId(config, explicitAgentKeyId) {
27531
27810
  if (explicitAgentKeyId) {
27532
27811
  return void 0;
27533
27812
  }
27813
+ const renderedError = error instanceof Error ? error.message : (
27814
+ /* c8 ignore next -- assertValidAgentKeyId throws Error objects */
27815
+ String(error)
27816
+ );
27534
27817
  throw new Error(
27535
- (error instanceof Error ? error.message : String(error)) + "; pass --agent-key-id to migrate the legacy config secret explicitly"
27818
+ renderedError + "; pass --agent-key-id to migrate the legacy config secret explicitly"
27536
27819
  );
27537
27820
  }
27538
27821
  }
@@ -27818,11 +28101,26 @@ function rewriteAmountPolicyErrorMessage(message, asset) {
27818
28101
  (_match, usedAmountWei, requestedAmountWei, maxAmountWei) => `window usage ${formatAmount(usedAmountWei)} + requested ${formatAmount(requestedAmountWei)} > max ${formatAmount(maxAmountWei)}`
27819
28102
  ).replace(
27820
28103
  /requires manual approval for requested amount (\d+) within range (None|Some\((\d+)\))\.\.=(\d+)/gu,
27821
- (_match, requestedAmountWei, minAmountLiteral, minAmountWei, maxAmountWei) => `requires manual approval for requested amount ${formatAmount(requestedAmountWei)} within range ${minAmountLiteral === "None" ? "None" : `Some(${formatAmount(minAmountWei ?? "0")})`}..=${formatAmount(maxAmountWei)}`
28104
+ (_match, requestedAmountWei, minAmountLiteral, minAmountWei, maxAmountWei) => {
28105
+ const minAmountDisplay = (
28106
+ /* c8 ignore next -- both None and Some(...) paths are exercised, but c8 misattributes this ternary under --experimental-strip-types */
28107
+ minAmountLiteral === "None" ? "None" : `Some(${formatAmount(minAmountWei ?? "0")})`
28108
+ );
28109
+ return `requires manual approval for requested amount ${formatAmount(requestedAmountWei)} within range ${minAmountDisplay}..=${formatAmount(maxAmountWei)}`;
28110
+ }
27822
28111
  );
27823
28112
  }
27824
28113
 
27825
28114
  // src/lib/asset-broadcast.ts
28115
+ function resolveEstimatedPriorityFeePerGasWei(fees) {
28116
+ const resolved = fees.maxPriorityFeePerGas ?? fees.gasPrice;
28117
+ if (resolved === null || resolved <= 0n) {
28118
+ throw new Error(
28119
+ "Could not determine maxPriorityFeePerGas; pass --max-priority-fee-per-gas-wei"
28120
+ );
28121
+ }
28122
+ return resolved;
28123
+ }
27826
28124
  function encodeErc20TransferData(recipient, amountWei) {
27827
28125
  return encodeFunctionData({
27828
28126
  abi: erc20Abi,
@@ -27851,10 +28149,10 @@ async function resolveAssetBroadcastPlan(input, deps) {
27851
28149
  const fees = await deps.estimateFees(input.rpcUrl);
27852
28150
  const resolvedFees = fees;
27853
28151
  const maxFeePerGasWei = input.maxFeePerGasWei ?? (resolvedFees.maxFeePerGas ?? resolvedFees.gasPrice);
27854
- const maxPriorityFeePerGasWei = input.maxPriorityFeePerGasWei ?? (resolvedFees.maxPriorityFeePerGas ?? resolvedFees.gasPrice ?? 0n);
27855
28152
  if (maxFeePerGasWei === null || maxFeePerGasWei <= 0n) {
27856
28153
  throw new Error("Could not determine maxFeePerGas; pass --max-fee-per-gas-wei");
27857
28154
  }
28155
+ const maxPriorityFeePerGasWei = input.maxPriorityFeePerGasWei ?? resolveEstimatedPriorityFeePerGasWei(resolvedFees);
27858
28156
  return {
27859
28157
  rpcUrl: input.rpcUrl,
27860
28158
  chainId: input.chainId,
@@ -27879,7 +28177,7 @@ async function completeAssetBroadcast(plan, signed, deps) {
27879
28177
  to: plan.to,
27880
28178
  chainId: plan.chainId,
27881
28179
  nonce: plan.nonce,
27882
- allowHigherNonce: true,
28180
+ allowHigherNonce: false,
27883
28181
  value: plan.valueWei,
27884
28182
  data: plan.dataHex,
27885
28183
  gasLimit: plan.gasLimit,
@@ -27986,7 +28284,6 @@ function resolveConfigMutationCommandLabel(command, key) {
27986
28284
  }
27987
28285
 
27988
28286
  // src/lib/local-admin-access.ts
27989
- var import_node_readline3 = __toESM(require("readline"), 1);
27990
28287
  var MAX_SECRET_STDIN_BYTES4 = 16 * 1024;
27991
28288
  function renderError6(error) {
27992
28289
  return error instanceof Error ? error.message : String(error);
@@ -28004,35 +28301,13 @@ function currentProcessIsRoot2() {
28004
28301
  return typeof process.geteuid === "function" && process.geteuid() === 0;
28005
28302
  }
28006
28303
  async function promptHidden3(query, label) {
28007
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
28008
- throw new Error(`${label} is required; rerun on a local TTY`);
28009
- }
28010
- const rl = import_node_readline3.default.createInterface({
28011
- input: process.stdin,
28012
- output: process.stdout,
28013
- terminal: true
28014
- });
28015
- rl.stdoutMuted = true;
28016
- rl._writeToOutput = (value) => {
28017
- if (value.includes(query)) {
28018
- rl.output.write(value);
28019
- return;
28020
- }
28021
- if (!rl.stdoutMuted) {
28022
- rl.output.write(value);
28023
- }
28024
- };
28025
- const answer = await new Promise((resolve) => {
28026
- rl.question(query, resolve);
28027
- });
28028
- rl.close();
28029
- process.stdout.write("\n");
28304
+ const answer = await promptHiddenTty(query, `${label} is required; rerun on a local TTY`);
28030
28305
  return validateSecret5(answer, label);
28031
28306
  }
28032
28307
  var sudoSession3 = createSudoSession({
28033
28308
  promptPassword: async () => await promptHidden3(
28034
- "Root password (input hidden; required to change local admin chain and token configuration): ",
28035
- "root password"
28309
+ "macOS admin password for sudo (input hidden; required to change local admin chain and token configuration): ",
28310
+ "macOS admin password for sudo"
28036
28311
  )
28037
28312
  });
28038
28313
  async function requireLocalAdminMutationAccess(commandLabel, deps = {}) {
@@ -28915,10 +29190,9 @@ function isManualApprovalRequiredOutput(value) {
28915
29190
  const candidate = value;
28916
29191
  return typeof candidate.command === "string" && typeof candidate.approval_request_id === "string" && typeof candidate.cli_approval_command === "string";
28917
29192
  }
28918
- function printManualApprovalRequired(output, asJson) {
29193
+ function renderManualApprovalRequired(output, asJson) {
28919
29194
  if (asJson) {
28920
- print2(output, true);
28921
- return;
29195
+ return formatJson(output);
28922
29196
  }
28923
29197
  const lines = [
28924
29198
  `Command: ${output.command}`,
@@ -28931,7 +29205,27 @@ function printManualApprovalRequired(output, asJson) {
28931
29205
  lines.push(`Relay URL: ${output.relay_url}`);
28932
29206
  }
28933
29207
  lines.push(`CLI Approval Command: ${output.cli_approval_command}`);
28934
- print2(lines.join("\n"), false);
29208
+ return lines.join("\n");
29209
+ }
29210
+ function printManualApprovalRequired(output, asJson, useStderr = false) {
29211
+ const rendered = renderManualApprovalRequired(output, asJson);
29212
+ if (useStderr) {
29213
+ console.error(rendered);
29214
+ return;
29215
+ }
29216
+ console.log(rendered);
29217
+ }
29218
+ function printManualApprovalWaiting(output, asJson) {
29219
+ if (asJson) {
29220
+ console.error(
29221
+ formatJson({
29222
+ event: "manualApprovalPending",
29223
+ approvalRequestId: output.approval_request_id
29224
+ })
29225
+ );
29226
+ return;
29227
+ }
29228
+ console.error(`Waiting for manual approval decision: ${output.approval_request_id}`);
28935
29229
  }
28936
29230
  var MAX_SECRET_STDIN_BYTES5 = 16 * 1024;
28937
29231
  var AGENT_KEY_ID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
@@ -29052,6 +29346,15 @@ function print2(payload, asJson) {
29052
29346
  }
29053
29347
  var ONCHAIN_RECEIPT_TIMEOUT_MS = 3e4;
29054
29348
  var ONCHAIN_RECEIPT_POLL_INTERVAL_MS = 2e3;
29349
+ var MANUAL_APPROVAL_POLL_INTERVAL_MS = 2e3;
29350
+ var MANUAL_APPROVAL_WAIT_TIMEOUT_MS = (() => {
29351
+ const raw = process.env.WLFI_TEST_MANUAL_APPROVAL_TIMEOUT_MS;
29352
+ if (!raw) {
29353
+ return 5 * 6e4;
29354
+ }
29355
+ const parsed = Number(raw);
29356
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 5 * 6e4;
29357
+ })();
29055
29358
  async function reportOnchainReceiptStatus(input) {
29056
29359
  if (input.asJson) {
29057
29360
  console.error(
@@ -29193,39 +29496,40 @@ async function resolveAgentCommandContext(options, config) {
29193
29496
  warnForAgentAuthTokenSource(agentAuthTokenSource, agentKeyId);
29194
29497
  return { agentKeyId, agentAuthToken, daemonSocket };
29195
29498
  }
29196
- async function runAgentCommandJson(input) {
29197
- const { agentKeyId, agentAuthToken, daemonSocket } = await resolveAgentCommandContext(
29198
- input.auth,
29199
- input.config
29200
- );
29499
+ async function runAgentCommandJsonOnce(input) {
29201
29500
  try {
29202
- return await runRustBinaryJson(
29203
- "wlfi-agent-agent",
29204
- [
29205
- "--json",
29206
- "--agent-key-id",
29207
- agentKeyId,
29208
- "--agent-auth-token-stdin",
29209
- "--daemon-socket",
29210
- daemonSocket,
29211
- ...input.commandArgs
29212
- ],
29213
- input.config,
29214
- {
29215
- stdin: `${agentAuthToken}
29501
+ return {
29502
+ type: "success",
29503
+ value: await runRustBinaryJson(
29504
+ "wlfi-agent-agent",
29505
+ [
29506
+ "--json",
29507
+ "--agent-key-id",
29508
+ input.agentKeyId,
29509
+ "--agent-auth-token-stdin",
29510
+ "--daemon-socket",
29511
+ input.daemonSocket,
29512
+ ...input.commandArgs
29513
+ ],
29514
+ input.config,
29515
+ {
29516
+ stdin: `${input.agentAuthToken}
29216
29517
  `,
29217
- preSuppliedSecretStdin: "agentAuthToken",
29218
- scrubSensitiveEnv: true
29219
- }
29220
- );
29518
+ preSuppliedSecretStdin: "agentAuthToken",
29519
+ scrubSensitiveEnv: true
29520
+ }
29521
+ )
29522
+ };
29221
29523
  } catch (error) {
29222
29524
  if (error instanceof RustBinaryExitError && error.stdout.trim()) {
29223
29525
  try {
29224
29526
  const parsed = JSON.parse(error.stdout);
29225
29527
  if (isManualApprovalRequiredOutput(parsed)) {
29226
- printManualApprovalRequired(parsed, input.asJson);
29227
- process.exitCode = error.code;
29228
- return null;
29528
+ return {
29529
+ type: "manualApproval",
29530
+ output: parsed,
29531
+ exitCode: error.code
29532
+ };
29229
29533
  }
29230
29534
  } catch {
29231
29535
  }
@@ -29233,6 +29537,49 @@ async function runAgentCommandJson(input) {
29233
29537
  throw error;
29234
29538
  }
29235
29539
  }
29540
+ async function runAgentCommandJson(input) {
29541
+ const { agentKeyId, agentAuthToken, daemonSocket } = await resolveAgentCommandContext(
29542
+ input.auth,
29543
+ input.config
29544
+ );
29545
+ const runOnce = () => runAgentCommandJsonOnce({
29546
+ commandArgs: input.commandArgs,
29547
+ config: input.config,
29548
+ agentKeyId,
29549
+ agentAuthToken,
29550
+ daemonSocket
29551
+ });
29552
+ const firstAttempt = await runOnce();
29553
+ if (firstAttempt.type === "success") {
29554
+ return firstAttempt.value;
29555
+ }
29556
+ if (!input.waitForManualApproval) {
29557
+ printManualApprovalRequired(firstAttempt.output, input.asJson);
29558
+ process.exitCode = firstAttempt.exitCode;
29559
+ return null;
29560
+ }
29561
+ printManualApprovalRequired(firstAttempt.output, input.asJson, true);
29562
+ printManualApprovalWaiting(firstAttempt.output, input.asJson);
29563
+ const pendingApprovalRequestId = firstAttempt.output.approval_request_id;
29564
+ const startedWaitingAt = Date.now();
29565
+ for (; ; ) {
29566
+ await (0, import_promises2.setTimeout)(MANUAL_APPROVAL_POLL_INTERVAL_MS);
29567
+ const nextAttempt = await runOnce();
29568
+ if (nextAttempt.type === "success") {
29569
+ return nextAttempt.value;
29570
+ }
29571
+ if (nextAttempt.output.approval_request_id !== pendingApprovalRequestId) {
29572
+ throw new Error(
29573
+ `manual approval request changed while waiting for a decision (${pendingApprovalRequestId} -> ${nextAttempt.output.approval_request_id}); stop and rerun the command after checking the approval status.`
29574
+ );
29575
+ }
29576
+ if (Date.now() - startedWaitingAt >= MANUAL_APPROVAL_WAIT_TIMEOUT_MS) {
29577
+ throw new Error(
29578
+ `Timed out after ${Math.ceil(MANUAL_APPROVAL_WAIT_TIMEOUT_MS / 1e3)}s waiting for manual approval decision: ${pendingApprovalRequestId}`
29579
+ );
29580
+ }
29581
+ }
29582
+ }
29236
29583
  function legacyAmountToDecimalString(value) {
29237
29584
  if (value === void 0) {
29238
29585
  return void 0;
@@ -29870,7 +30217,8 @@ async function main() {
29870
30217
  ],
29871
30218
  auth: options,
29872
30219
  config,
29873
- asJson: options.json
30220
+ asJson: options.json,
30221
+ waitForManualApproval: true
29874
30222
  });
29875
30223
  if (!signed) {
29876
30224
  return;
@@ -29926,20 +30274,108 @@ async function main() {
29926
30274
  }
29927
30275
  });
29928
30276
  addAgentCommandAuthOptions(
29929
- program2.command("transfer-native").description("Submit a native ETH transfer request through policy checks").requiredOption("--network <name>", "Network name").requiredOption("--to <address>", "Recipient address").requiredOption("--amount <amount>", "Transfer amount in configured native token units").addOption(new Option("--amount-wei <wei>").hideHelp())
30277
+ program2.command("transfer-native").description("Submit a native ETH transfer request through policy checks").requiredOption("--network <name>", "Network name").requiredOption("--to <address>", "Recipient address").requiredOption("--amount <amount>", "Transfer amount in configured native token units").option("--broadcast", "Broadcast the signed transaction through RPC", false).option("--rpc-url <url>", "Ethereum RPC URL override used only for broadcast").option(
30278
+ "--from <address>",
30279
+ "Sender address override for broadcast; defaults to configured wallet address"
30280
+ ).option("--nonce <nonce>", "Explicit nonce override for broadcast").option("--gas-limit <gas>", "Gas limit override for broadcast").option("--max-fee-per-gas-wei <wei>", "Max fee per gas override for broadcast").option("--max-priority-fee-per-gas-wei <wei>", "Priority fee per gas override for broadcast").option("--tx-type <type>", "Typed tx value for broadcast", "0x02").option("--no-wait", "Do not wait up to 30s for an on-chain receipt after broadcast").option(
30281
+ "--reveal-raw-tx",
30282
+ "Include the signed raw transaction bytes in broadcast output",
30283
+ false
30284
+ ).option("--reveal-signature", "Include signer r/s/v fields in broadcast output", false).addOption(new Option("--amount-wei <wei>").hideHelp())
29930
30285
  ).action(async (options) => {
29931
30286
  const config = readConfig3();
29932
30287
  const network = resolveCliNetworkProfile(options.network, config).chainId;
29933
30288
  const asset = resolveConfiguredNativeAsset(config, network);
30289
+ const recipient = assertAddress(options.to, "to");
29934
30290
  const amountWei = options.amount ? parseConfiguredAmount(options.amount, asset.decimals, "amount") : parseBigIntString(options.amountWei, "amountWei");
29935
30291
  try {
30292
+ if (options.broadcast) {
30293
+ const plan = await resolveAssetBroadcastPlan(
30294
+ {
30295
+ rpcUrl: resolveCliRpcUrl(options.rpcUrl, options.network, config),
30296
+ chainId: network,
30297
+ from: options.from ? assertAddress(options.from, "from") : resolveWalletAddress(config),
30298
+ to: recipient,
30299
+ valueWei: amountWei,
30300
+ dataHex: "0x",
30301
+ nonce: options.nonce ? parseIntegerString(options.nonce, "nonce") : void 0,
30302
+ gasLimit: options.gasLimit ? parsePositiveBigIntString(options.gasLimit, "gasLimit") : void 0,
30303
+ maxFeePerGasWei: options.maxFeePerGasWei ? parsePositiveBigIntString(options.maxFeePerGasWei, "maxFeePerGasWei") : void 0,
30304
+ maxPriorityFeePerGasWei: options.maxPriorityFeePerGasWei ? parseBigIntString(options.maxPriorityFeePerGasWei, "maxPriorityFeePerGasWei") : void 0,
30305
+ txType: options.txType
30306
+ },
30307
+ {
30308
+ getChainInfo: getChainInfo2,
30309
+ assertRpcChainIdMatches,
30310
+ getNonce: getNonce2,
30311
+ estimateGas: estimateGas3,
30312
+ estimateFees: estimateFees2
30313
+ }
30314
+ );
30315
+ const signed = await runAgentCommandJson({
30316
+ commandArgs: [
30317
+ "broadcast",
30318
+ "--network",
30319
+ String(plan.chainId),
30320
+ "--nonce",
30321
+ String(plan.nonce),
30322
+ "--to",
30323
+ recipient,
30324
+ "--value-wei",
30325
+ amountWei.toString(),
30326
+ "--data-hex",
30327
+ "0x",
30328
+ "--gas-limit",
30329
+ plan.gasLimit.toString(),
30330
+ "--max-fee-per-gas-wei",
30331
+ plan.maxFeePerGasWei.toString(),
30332
+ "--max-priority-fee-per-gas-wei",
30333
+ plan.maxPriorityFeePerGasWei.toString(),
30334
+ "--tx-type",
30335
+ plan.txType
30336
+ ],
30337
+ auth: options,
30338
+ config,
30339
+ asJson: options.json,
30340
+ waitForManualApproval: true
30341
+ });
30342
+ if (!signed) {
30343
+ return;
30344
+ }
30345
+ const completed = await completeAssetBroadcast(plan, signed, {
30346
+ assertSignedBroadcastTransactionMatchesRequest,
30347
+ broadcastRawTransaction: broadcastRawTransaction2
30348
+ });
30349
+ print2(
30350
+ formatBroadcastedAssetOutput({
30351
+ command: "transfer-native",
30352
+ counterparty: recipient,
30353
+ asset,
30354
+ signed,
30355
+ plan,
30356
+ signedNonce: completed.signedNonce,
30357
+ networkTxHash: completed.networkTxHash,
30358
+ revealRawTx: options.revealRawTx,
30359
+ revealSignature: options.revealSignature
30360
+ }),
30361
+ options.json
30362
+ );
30363
+ if (options.wait) {
30364
+ await reportOnchainReceiptStatus({
30365
+ rpcUrl: plan.rpcUrl,
30366
+ txHash: completed.networkTxHash,
30367
+ asJson: options.json
30368
+ });
30369
+ }
30370
+ return;
30371
+ }
29936
30372
  const result = await runAgentCommandJson({
29937
30373
  commandArgs: [
29938
30374
  "transfer-native",
29939
30375
  "--network",
29940
30376
  String(network),
29941
30377
  "--to",
29942
- assertAddress(options.to, "to"),
30378
+ recipient,
29943
30379
  "--amount-wei",
29944
30380
  amountWei.toString()
29945
30381
  ],
@@ -30018,7 +30454,8 @@ async function main() {
30018
30454
  ],
30019
30455
  auth: options,
30020
30456
  config,
30021
- asJson: options.json
30457
+ asJson: options.json,
30458
+ waitForManualApproval: true
30022
30459
  });
30023
30460
  if (!signed) {
30024
30461
  return;
@@ -30074,29 +30511,46 @@ async function main() {
30074
30511
  }
30075
30512
  });
30076
30513
  addAgentCommandAuthOptions(
30077
- program2.command("broadcast").description("Submit a raw transaction broadcast request through policy checks").requiredOption("--network <name>", "Network name").requiredOption("--to <address>", "Recipient or target contract").requiredOption("--gas-limit <gas>", "Gas limit").requiredOption("--max-fee-per-gas-wei <wei>", "Max fee per gas in wei").option("--nonce <nonce>", "Explicit nonce override", "0").option("--value-wei <wei>", "Value in wei", "0").option("--data-hex <hex>", "Calldata hex", "0x").option("--max-priority-fee-per-gas-wei <wei>", "Priority fee per gas in wei", "0").option("--tx-type <type>", "Typed tx value", "2").option("--delegation-enabled", "Forward delegation flag to Rust signing request", false)
30514
+ program2.command("broadcast").description("Submit a raw transaction broadcast request through policy checks").requiredOption("--network <name>", "Network name").requiredOption("--to <address>", "Recipient or target contract").requiredOption("--gas-limit <gas>", "Gas limit").requiredOption("--max-fee-per-gas-wei <wei>", "Max fee per gas in wei").option("--nonce <nonce>", "Explicit nonce override").option("--value-wei <wei>", "Value in wei", "0").option("--data-hex <hex>", "Calldata hex", "0x").option("--max-priority-fee-per-gas-wei <wei>", "Priority fee per gas in wei").option("--tx-type <type>", "Typed tx value", "0x02").option("--delegation-enabled", "Forward delegation flag to Rust signing request", false)
30078
30515
  ).action(async (options) => {
30079
30516
  const config = readConfig3();
30080
30517
  const network = resolveCliNetworkProfile(options.network, config);
30081
- const result = await runAgentCommandJson({
30518
+ const to = assertAddress(options.to, "to");
30519
+ const valueWei = parseBigIntString(options.valueWei, "valueWei");
30520
+ const dataHex = assertHex(options.dataHex, "dataHex");
30521
+ const gasLimit = parsePositiveBigIntString(options.gasLimit, "gasLimit");
30522
+ const maxFeePerGasWei = parsePositiveBigIntString(options.maxFeePerGasWei, "maxFeePerGasWei");
30523
+ const explicitNonce = options.nonce ? parseIntegerString(options.nonce, "nonce") : void 0;
30524
+ const rpcUrl = resolveCliRpcUrl(void 0, options.network, config);
30525
+ const from14 = resolveWalletAddress(config);
30526
+ const chainInfo = await getChainInfo2(rpcUrl);
30527
+ assertRpcChainIdMatches(network.chainId, chainInfo.chainId);
30528
+ const nonce = explicitNonce ?? await getNonce2(rpcUrl, from14);
30529
+ const fees = options.maxPriorityFeePerGasWei ? null : await estimateFees2(rpcUrl);
30530
+ const maxPriorityFeePerGasWei = options.maxPriorityFeePerGasWei ? parseBigIntString(options.maxPriorityFeePerGasWei, "maxPriorityFeePerGasWei") : resolveEstimatedPriorityFeePerGasWei({
30531
+ gasPrice: fees?.gasPrice ?? null,
30532
+ maxFeePerGas: fees?.maxFeePerGas ?? null,
30533
+ maxPriorityFeePerGas: fees?.maxPriorityFeePerGas ?? null
30534
+ });
30535
+ const signed = await runAgentCommandJson({
30082
30536
  commandArgs: [
30083
30537
  "broadcast",
30084
30538
  "--network",
30085
30539
  String(network.chainId),
30086
30540
  "--nonce",
30087
- String(parseIntegerString(options.nonce, "nonce")),
30541
+ String(nonce),
30088
30542
  "--to",
30089
- assertAddress(options.to, "to"),
30543
+ to,
30090
30544
  "--value-wei",
30091
- parseBigIntString(options.valueWei, "valueWei").toString(),
30545
+ valueWei.toString(),
30092
30546
  "--data-hex",
30093
- assertHex(options.dataHex, "dataHex"),
30547
+ dataHex,
30094
30548
  "--gas-limit",
30095
- parsePositiveBigIntString(options.gasLimit, "gasLimit").toString(),
30549
+ gasLimit.toString(),
30096
30550
  "--max-fee-per-gas-wei",
30097
- parsePositiveBigIntString(options.maxFeePerGasWei, "maxFeePerGasWei").toString(),
30551
+ maxFeePerGasWei.toString(),
30098
30552
  "--max-priority-fee-per-gas-wei",
30099
- parseBigIntString(options.maxPriorityFeePerGasWei, "maxPriorityFeePerGasWei").toString(),
30553
+ maxPriorityFeePerGasWei.toString(),
30100
30554
  "--tx-type",
30101
30555
  options.txType,
30102
30556
  ...options.delegationEnabled ? ["--delegation-enabled"] : []
@@ -30105,9 +30559,28 @@ async function main() {
30105
30559
  config,
30106
30560
  asJson: options.json
30107
30561
  });
30108
- if (result) {
30109
- print2(result, options.json);
30562
+ if (!signed) {
30563
+ return;
30564
+ }
30565
+ if (!signed.raw_tx_hex) {
30566
+ throw new Error("Rust agent did not return raw_tx_hex for broadcast signing");
30110
30567
  }
30568
+ await assertSignedBroadcastTransactionMatchesRequest({
30569
+ rawTxHex: signed.raw_tx_hex,
30570
+ from: from14,
30571
+ to,
30572
+ chainId: network.chainId,
30573
+ nonce,
30574
+ allowHigherNonce: false,
30575
+ value: valueWei,
30576
+ data: dataHex,
30577
+ gasLimit,
30578
+ maxFeePerGas: maxFeePerGasWei,
30579
+ maxPriorityFeePerGas: maxPriorityFeePerGasWei,
30580
+ txType: options.txType
30581
+ });
30582
+ await broadcastRawTransaction2(rpcUrl, signed.raw_tx_hex);
30583
+ print2(signed, options.json);
30111
30584
  });
30112
30585
  const rpc = program2.command("rpc").description("RPC methods implemented in TypeScript");
30113
30586
  rpc.command("chain").option("--rpc-url <url>", "Ethereum RPC URL").option("--json", "Print JSON output", false).action(async (options) => {
@@ -30319,7 +30792,7 @@ async function main() {
30319
30792
  to,
30320
30793
  chainId,
30321
30794
  nonce,
30322
- allowHigherNonce: true,
30795
+ allowHigherNonce: false,
30323
30796
  value: valueWei,
30324
30797
  data: dataHex,
30325
30798
  gasLimit,