@wlfi-agent/cli 1.4.13 → 1.4.14

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 (289) hide show
  1. package/Cargo.lock +3968 -0
  2. package/Cargo.toml +50 -0
  3. package/README.md +426 -6
  4. package/crates/vault-cli-admin/Cargo.toml +26 -0
  5. package/crates/vault-cli-admin/src/io_utils.rs +500 -0
  6. package/crates/vault-cli-admin/src/main.rs +3990 -0
  7. package/crates/vault-cli-admin/src/shared_config.rs +624 -0
  8. package/crates/vault-cli-admin/src/tui/amounts.rs +180 -0
  9. package/crates/vault-cli-admin/src/tui/token_rpc.rs +250 -0
  10. package/crates/vault-cli-admin/src/tui/utils.rs +82 -0
  11. package/crates/vault-cli-admin/src/tui.rs +3410 -0
  12. package/crates/vault-cli-agent/Cargo.toml +24 -0
  13. package/crates/vault-cli-agent/src/io_utils.rs +576 -0
  14. package/crates/vault-cli-agent/src/main.rs +833 -0
  15. package/crates/vault-cli-daemon/Cargo.toml +28 -0
  16. package/crates/vault-cli-daemon/src/bin/wlfi-agent-system-keychain.rs +216 -0
  17. package/crates/vault-cli-daemon/src/main.rs +644 -0
  18. package/crates/vault-cli-daemon/src/relay_sync.rs +894 -0
  19. package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +167 -0
  20. package/crates/vault-daemon/Cargo.toml +32 -0
  21. package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +1041 -0
  22. package/crates/vault-daemon/src/daemon_parts/core_helpers.rs +1256 -0
  23. package/crates/vault-daemon/src/daemon_parts/types_api_rpc.rs +622 -0
  24. package/crates/vault-daemon/src/lib.rs +54 -0
  25. package/crates/vault-daemon/src/persistence.rs +441 -0
  26. package/crates/vault-daemon/src/tests.rs +237 -0
  27. package/crates/vault-daemon/src/tests_parts/part1.rs +1224 -0
  28. package/crates/vault-daemon/src/tests_parts/part2.rs +1021 -0
  29. package/crates/vault-daemon/src/tests_parts/part3.rs +835 -0
  30. package/crates/vault-daemon/src/tests_parts/part4.rs +604 -0
  31. package/crates/vault-domain/Cargo.toml +20 -0
  32. package/crates/vault-domain/src/action.rs +849 -0
  33. package/crates/vault-domain/src/address.rs +51 -0
  34. package/crates/vault-domain/src/approval.rs +90 -0
  35. package/crates/vault-domain/src/constants.rs +4 -0
  36. package/crates/vault-domain/src/error.rs +54 -0
  37. package/crates/vault-domain/src/keys.rs +71 -0
  38. package/crates/vault-domain/src/lib.rs +42 -0
  39. package/crates/vault-domain/src/nonce.rs +102 -0
  40. package/crates/vault-domain/src/policy.rs +172 -0
  41. package/crates/vault-domain/src/request.rs +53 -0
  42. package/crates/vault-domain/src/scope.rs +24 -0
  43. package/crates/vault-domain/src/session.rs +50 -0
  44. package/crates/vault-domain/src/signature.rs +34 -0
  45. package/crates/vault-domain/src/tests.rs +651 -0
  46. package/crates/vault-domain/src/u128_as_decimal_string.rs +44 -0
  47. package/crates/vault-policy/Cargo.toml +17 -0
  48. package/crates/vault-policy/src/engine.rs +301 -0
  49. package/crates/vault-policy/src/error.rs +81 -0
  50. package/crates/vault-policy/src/lib.rs +17 -0
  51. package/crates/vault-policy/src/report.rs +34 -0
  52. package/crates/vault-policy/src/tests.rs +891 -0
  53. package/crates/vault-policy/src/tests_explain.rs +78 -0
  54. package/crates/vault-sdk-agent/Cargo.toml +21 -0
  55. package/crates/vault-sdk-agent/src/lib.rs +711 -0
  56. package/crates/vault-signer/Cargo.toml +25 -0
  57. package/crates/vault-signer/src/lib.rs +731 -0
  58. package/crates/vault-signer/tests/secure_enclave_acl.rs +54 -0
  59. package/crates/vault-transport-unix/Cargo.toml +24 -0
  60. package/crates/vault-transport-unix/src/lib.rs +1640 -0
  61. package/crates/vault-transport-xpc/Cargo.toml +25 -0
  62. package/crates/vault-transport-xpc/src/client_codec_api.rs +635 -0
  63. package/crates/vault-transport-xpc/src/lib.rs +680 -0
  64. package/crates/vault-transport-xpc/src/tests.rs +818 -0
  65. package/crates/vault-transport-xpc/tests/e2e_flow.rs +773 -0
  66. package/dist/cli.cjs +35088 -0
  67. package/dist/cli.cjs.map +1 -0
  68. package/package.json +49 -43
  69. package/packages/cache/.turbo/turbo-build.log +52 -0
  70. package/packages/cache/dist/chunk-2QFWMUXT.cjs +43 -0
  71. package/packages/cache/dist/chunk-2QFWMUXT.cjs.map +1 -0
  72. package/packages/cache/dist/chunk-4U63TZTQ.js +43 -0
  73. package/packages/cache/dist/chunk-4U63TZTQ.js.map +1 -0
  74. package/packages/cache/dist/chunk-ALQ6H7KG.cjs +404 -0
  75. package/packages/cache/dist/chunk-ALQ6H7KG.cjs.map +1 -0
  76. package/packages/cache/dist/chunk-FGJEEF5N.js +404 -0
  77. package/packages/cache/dist/chunk-FGJEEF5N.js.map +1 -0
  78. package/packages/cache/dist/chunk-UYNEHZHB.cjs +45 -0
  79. package/packages/cache/dist/chunk-UYNEHZHB.cjs.map +1 -0
  80. package/packages/cache/dist/chunk-VXVMPG3W.js +45 -0
  81. package/packages/cache/dist/chunk-VXVMPG3W.js.map +1 -0
  82. package/packages/cache/dist/client/index.cjs +11 -0
  83. package/packages/cache/dist/client/index.cjs.map +1 -0
  84. package/packages/cache/dist/client/index.d.cts +15 -0
  85. package/packages/cache/dist/client/index.d.ts +15 -0
  86. package/packages/cache/dist/client/index.js +11 -0
  87. package/packages/cache/dist/client/index.js.map +1 -0
  88. package/packages/cache/dist/errors/index.cjs +11 -0
  89. package/packages/cache/dist/errors/index.cjs.map +1 -0
  90. package/packages/cache/dist/errors/index.d.cts +26 -0
  91. package/packages/cache/dist/errors/index.d.ts +26 -0
  92. package/packages/cache/dist/errors/index.js +11 -0
  93. package/packages/cache/dist/errors/index.js.map +1 -0
  94. package/packages/cache/dist/index.cjs +29 -0
  95. package/packages/cache/dist/index.cjs.map +1 -0
  96. package/packages/cache/dist/index.d.cts +4 -0
  97. package/packages/cache/dist/index.d.ts +4 -0
  98. package/packages/cache/dist/index.js +29 -0
  99. package/packages/cache/dist/index.js.map +1 -0
  100. package/packages/cache/dist/service/index.cjs +15 -0
  101. package/packages/cache/dist/service/index.cjs.map +1 -0
  102. package/packages/cache/dist/service/index.d.cts +184 -0
  103. package/packages/cache/dist/service/index.d.ts +184 -0
  104. package/packages/cache/dist/service/index.js +15 -0
  105. package/packages/cache/dist/service/index.js.map +1 -0
  106. package/packages/cache/node_modules/.bin/jiti +17 -0
  107. package/packages/cache/node_modules/.bin/tsc +17 -0
  108. package/packages/cache/node_modules/.bin/tsserver +17 -0
  109. package/packages/cache/node_modules/.bin/tsup +17 -0
  110. package/packages/cache/node_modules/.bin/tsup-node +17 -0
  111. package/packages/cache/node_modules/.bin/tsx +17 -0
  112. package/packages/cache/node_modules/.bin/vitest +17 -0
  113. package/packages/cache/package.json +48 -0
  114. package/packages/cache/src/client/index.ts +56 -0
  115. package/packages/cache/src/errors/index.ts +53 -0
  116. package/packages/cache/src/index.ts +3 -0
  117. package/packages/cache/src/service/index.test.ts +263 -0
  118. package/packages/cache/src/service/index.ts +678 -0
  119. package/packages/cache/tsconfig.json +13 -0
  120. package/packages/cache/tsup.config.ts +13 -0
  121. package/packages/cache/vitest.config.ts +16 -0
  122. package/packages/config/.turbo/turbo-build.log +18 -0
  123. package/packages/config/dist/index.cjs +1037 -0
  124. package/packages/config/dist/index.cjs.map +1 -0
  125. package/packages/config/dist/index.d.ts +131 -0
  126. package/packages/config/node_modules/.bin/jiti +17 -0
  127. package/packages/config/node_modules/.bin/tsc +17 -0
  128. package/packages/config/node_modules/.bin/tsserver +17 -0
  129. package/packages/config/node_modules/.bin/tsup +17 -0
  130. package/packages/config/node_modules/.bin/tsup-node +17 -0
  131. package/packages/config/node_modules/.bin/tsx +17 -0
  132. package/packages/config/package.json +21 -0
  133. package/packages/config/src/index.js +1 -0
  134. package/packages/config/src/index.ts +1282 -0
  135. package/packages/config/tsconfig.json +4 -0
  136. package/packages/rpc/.turbo/turbo-build.log +32 -0
  137. package/packages/rpc/dist/_esm-BCLXDO2R.cjs +3660 -0
  138. package/packages/rpc/dist/_esm-BCLXDO2R.cjs.map +1 -0
  139. package/packages/rpc/dist/ccip-OWJLAW55.cjs +16 -0
  140. package/packages/rpc/dist/ccip-OWJLAW55.cjs.map +1 -0
  141. package/packages/rpc/dist/chunk-APQIFZ3B.cjs +6247 -0
  142. package/packages/rpc/dist/chunk-APQIFZ3B.cjs.map +1 -0
  143. package/packages/rpc/dist/chunk-CDO2GWRD.cjs +410 -0
  144. package/packages/rpc/dist/chunk-CDO2GWRD.cjs.map +1 -0
  145. package/packages/rpc/dist/chunk-QGTNTFJ7.cjs +2249 -0
  146. package/packages/rpc/dist/chunk-QGTNTFJ7.cjs.map +1 -0
  147. package/packages/rpc/dist/chunk-TZDTAHWR.cjs +44 -0
  148. package/packages/rpc/dist/chunk-TZDTAHWR.cjs.map +1 -0
  149. package/packages/rpc/dist/index.cjs +7342 -0
  150. package/packages/rpc/dist/index.cjs.map +1 -0
  151. package/packages/rpc/dist/index.d.ts +3857 -0
  152. package/packages/rpc/dist/secp256k1-WCNM675D.cjs +18 -0
  153. package/packages/rpc/dist/secp256k1-WCNM675D.cjs.map +1 -0
  154. package/packages/rpc/node_modules/.bin/jiti +17 -0
  155. package/packages/rpc/node_modules/.bin/tsc +17 -0
  156. package/packages/rpc/node_modules/.bin/tsserver +17 -0
  157. package/packages/rpc/node_modules/.bin/tsup +17 -0
  158. package/packages/rpc/node_modules/.bin/tsup-node +17 -0
  159. package/packages/rpc/node_modules/.bin/tsx +17 -0
  160. package/packages/rpc/package.json +25 -0
  161. package/packages/rpc/src/index.ts +206 -0
  162. package/packages/rpc/tsconfig.json +4 -0
  163. package/packages/typescript/base.json +36 -0
  164. package/packages/typescript/nextjs.json +17 -0
  165. package/packages/typescript/package.json +10 -0
  166. package/packages/ui/.turbo/turbo-build.log +44 -0
  167. package/packages/ui/dist/chunk-MOAFBKSA.js +11 -0
  168. package/packages/ui/dist/chunk-MOAFBKSA.js.map +1 -0
  169. package/packages/ui/dist/components/badge.d.ts +12 -0
  170. package/packages/ui/dist/components/badge.js +31 -0
  171. package/packages/ui/dist/components/badge.js.map +1 -0
  172. package/packages/ui/dist/components/button.d.ts +13 -0
  173. package/packages/ui/dist/components/button.js +40 -0
  174. package/packages/ui/dist/components/button.js.map +1 -0
  175. package/packages/ui/dist/components/card.d.ts +10 -0
  176. package/packages/ui/dist/components/card.js +39 -0
  177. package/packages/ui/dist/components/card.js.map +1 -0
  178. package/packages/ui/dist/components/input.d.ts +5 -0
  179. package/packages/ui/dist/components/input.js +28 -0
  180. package/packages/ui/dist/components/input.js.map +1 -0
  181. package/packages/ui/dist/components/label.d.ts +5 -0
  182. package/packages/ui/dist/components/label.js +13 -0
  183. package/packages/ui/dist/components/label.js.map +1 -0
  184. package/packages/ui/dist/components/separator.d.ts +5 -0
  185. package/packages/ui/dist/components/separator.js +13 -0
  186. package/packages/ui/dist/components/separator.js.map +1 -0
  187. package/packages/ui/dist/components/textarea.d.ts +5 -0
  188. package/packages/ui/dist/components/textarea.js +27 -0
  189. package/packages/ui/dist/components/textarea.js.map +1 -0
  190. package/packages/ui/dist/tailwind.d.ts +56 -0
  191. package/packages/ui/dist/tailwind.js +60 -0
  192. package/packages/ui/dist/tailwind.js.map +1 -0
  193. package/packages/ui/dist/utils/cn.d.ts +5 -0
  194. package/packages/ui/dist/utils/cn.js +7 -0
  195. package/packages/ui/dist/utils/cn.js.map +1 -0
  196. package/packages/ui/node_modules/.bin/jiti +17 -0
  197. package/packages/ui/node_modules/.bin/tsc +17 -0
  198. package/packages/ui/node_modules/.bin/tsserver +17 -0
  199. package/packages/ui/node_modules/.bin/tsup +17 -0
  200. package/packages/ui/node_modules/.bin/tsup-node +17 -0
  201. package/packages/ui/node_modules/.bin/tsx +17 -0
  202. package/packages/ui/package.json +69 -0
  203. package/packages/ui/src/components/badge.tsx +27 -0
  204. package/packages/ui/src/components/button.tsx +40 -0
  205. package/packages/ui/src/components/card.tsx +31 -0
  206. package/packages/ui/src/components/input.tsx +21 -0
  207. package/packages/ui/src/components/label.tsx +6 -0
  208. package/packages/ui/src/components/separator.tsx +6 -0
  209. package/packages/ui/src/components/textarea.tsx +20 -0
  210. package/packages/ui/src/globals.css +70 -0
  211. package/packages/ui/src/tailwind.ts +56 -0
  212. package/packages/ui/src/utils/cn.ts +6 -0
  213. package/packages/ui/tsconfig.json +20 -0
  214. package/packages/ui/tsup.config.ts +20 -0
  215. package/pnpm-workspace.yaml +4 -0
  216. package/scripts/install-rust-binaries.mjs +84 -0
  217. package/scripts/launchd/install-user-daemon.sh +358 -0
  218. package/scripts/launchd/run-vault-daemon.sh +5 -0
  219. package/scripts/launchd/run-wlfi-agent-daemon.sh +73 -0
  220. package/scripts/launchd/uninstall-user-daemon.sh +103 -0
  221. package/src/cli.ts +2121 -0
  222. package/src/lib/admin-guard.js +1 -0
  223. package/src/lib/admin-guard.ts +185 -0
  224. package/src/lib/admin-passthrough.ts +33 -0
  225. package/src/lib/admin-reset.ts +751 -0
  226. package/src/lib/admin-setup.ts +1612 -0
  227. package/src/lib/agent-auth-clear.js +1 -0
  228. package/src/lib/agent-auth-clear.ts +58 -0
  229. package/src/lib/agent-auth-forwarding.js +1 -0
  230. package/src/lib/agent-auth-forwarding.ts +149 -0
  231. package/src/lib/agent-auth-migrate.js +1 -0
  232. package/src/lib/agent-auth-migrate.ts +150 -0
  233. package/src/lib/agent-auth-revoke.ts +103 -0
  234. package/src/lib/agent-auth-rotate.ts +107 -0
  235. package/src/lib/agent-auth-token.js +1 -0
  236. package/src/lib/agent-auth-token.ts +25 -0
  237. package/src/lib/agent-auth.ts +89 -0
  238. package/src/lib/asset-broadcast.js +1 -0
  239. package/src/lib/asset-broadcast.ts +285 -0
  240. package/src/lib/bootstrap-artifacts.js +1 -0
  241. package/src/lib/bootstrap-artifacts.ts +205 -0
  242. package/src/lib/bootstrap-credentials.js +1 -0
  243. package/src/lib/bootstrap-credentials.ts +832 -0
  244. package/src/lib/config-amounts.js +1 -0
  245. package/src/lib/config-amounts.ts +189 -0
  246. package/src/lib/config-mutation.ts +27 -0
  247. package/src/lib/fs-trust.js +1 -0
  248. package/src/lib/fs-trust.ts +537 -0
  249. package/src/lib/keychain.js +1 -0
  250. package/src/lib/keychain.ts +225 -0
  251. package/src/lib/local-admin-access.ts +106 -0
  252. package/src/lib/network-selection.js +1 -0
  253. package/src/lib/network-selection.ts +71 -0
  254. package/src/lib/passthrough-security.js +1 -0
  255. package/src/lib/passthrough-security.ts +114 -0
  256. package/src/lib/rpc-guard.js +1 -0
  257. package/src/lib/rpc-guard.ts +7 -0
  258. package/src/lib/rust-spawn-options.js +1 -0
  259. package/src/lib/rust-spawn-options.ts +98 -0
  260. package/src/lib/rust.js +1 -0
  261. package/src/lib/rust.ts +143 -0
  262. package/src/lib/signed-tx.js +1 -0
  263. package/src/lib/signed-tx.ts +116 -0
  264. package/src/lib/status-repair-cli.ts +116 -0
  265. package/src/lib/sudo.js +1 -0
  266. package/src/lib/sudo.ts +172 -0
  267. package/src/lib/vault-password-forwarding.js +1 -0
  268. package/src/lib/vault-password-forwarding.ts +155 -0
  269. package/src/lib/wallet-profile.js +1 -0
  270. package/src/lib/wallet-profile.ts +332 -0
  271. package/src/lib/wallet-repair.js +1 -0
  272. package/src/lib/wallet-repair.ts +304 -0
  273. package/src/lib/wallet-setup.js +1 -0
  274. package/src/lib/wallet-setup.ts +1466 -0
  275. package/src/lib/wallet-status.js +1 -0
  276. package/src/lib/wallet-status.ts +640 -0
  277. package/tsconfig.base.json +17 -0
  278. package/tsconfig.json +10 -0
  279. package/tsup.config.ts +25 -0
  280. package/turbo.json +41 -0
  281. package/LICENSE.md +0 -1
  282. package/dist/wlfa/index.cjs +0 -250
  283. package/dist/wlfa/index.d.cts +0 -1
  284. package/dist/wlfa/index.d.ts +0 -1
  285. package/dist/wlfa/index.js +0 -250
  286. package/dist/wlfc/index.cjs +0 -1839
  287. package/dist/wlfc/index.d.cts +0 -1
  288. package/dist/wlfc/index.d.ts +0 -1
  289. package/dist/wlfc/index.js +0 -1839
@@ -0,0 +1,1612 @@
1
+ import fs from 'node:fs';
2
+ import net from 'node:net';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import readline from 'node:readline';
6
+ import { Command } from 'commander';
7
+ import type { Hex } from 'viem';
8
+ import { publicKeyToAddress } from 'viem/accounts';
9
+ import {
10
+ defaultRustBinDir,
11
+ readConfig,
12
+ redactConfig,
13
+ type WlfiConfig,
14
+ writeConfig,
15
+ } from '../../packages/config/src/index.js';
16
+ import { cleanupBootstrapAgentCredentialsFile } from './bootstrap-credentials.js';
17
+ import {
18
+ assertTrustedAdminDaemonSocketPath,
19
+ assertTrustedExecutablePath,
20
+ assertTrustedRootPlannedDaemonSocketPath,
21
+ assertTrustedRootPlannedPrivateFilePath,
22
+ } from './fs-trust.js';
23
+ import { DAEMON_PASSWORD_KEYCHAIN_SERVICE } from './keychain.js';
24
+ import { resolveCliNetworkProfile } from './network-selection.js';
25
+ import { passthroughRustBinary, RustBinaryExitError, runRustBinary } from './rust.js';
26
+ import { createSudoSession } from './sudo.js';
27
+ import { resolveWalletProfile } from './wallet-profile.js';
28
+ import {
29
+ assertWalletSetupExecutionPreconditions,
30
+ buildWalletSetupAdminArgs,
31
+ type CompleteWalletSetupResult,
32
+ completeWalletSetup,
33
+ createWalletSetupPlan,
34
+ formatWalletSetupPlanText,
35
+ resolveWalletSetupBootstrapOutputPath,
36
+ resolveWalletSetupCleanupAction,
37
+ type WalletSetupPlan,
38
+ } from './wallet-setup.js';
39
+
40
+ const DEFAULT_LAUNCH_DAEMON_LABEL = 'com.wlfi.agent.daemon';
41
+ const DEFAULT_SIGNER_BACKEND = 'software';
42
+ const DEFAULT_MANAGED_BIN_DIR = '/Library/WLFI/bin';
43
+ const DEFAULT_MANAGED_DAEMON_SOCKET = '/Library/WLFI/run/daemon.sock';
44
+ const DEFAULT_MANAGED_STATE_FILE = '/var/db/wlfi-agent/daemon-state.enc';
45
+ const DEFAULT_MANAGED_KEYCHAIN_HELPER = path.join(
46
+ DEFAULT_MANAGED_BIN_DIR,
47
+ `wlfi-agent-system-keychain${process.platform === 'win32' ? '.exe' : ''}`,
48
+ );
49
+ const MAX_SECRET_STDIN_BYTES = 16 * 1024;
50
+ const DEFAULT_LAUNCH_DAEMON_PLIST = `/Library/LaunchDaemons/${DEFAULT_LAUNCH_DAEMON_LABEL}.plist`;
51
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
52
+ const REDACTED_SECRET_PLACEHOLDER = '<redacted>';
53
+ const KEYCHAIN_STORED_AGENT_AUTH_TOKEN = 'stored in macOS Keychain';
54
+
55
+ interface ProgressHandle {
56
+ succeed(message?: string): void;
57
+ fail(message?: string): void;
58
+ info(message?: string): void;
59
+ }
60
+
61
+ interface AdminSetupOptions {
62
+ vaultPassword?: string;
63
+ vaultPasswordStdin?: boolean;
64
+ nonInteractive?: boolean;
65
+ plan?: boolean;
66
+ yes?: boolean;
67
+ printAgentAuthToken?: boolean;
68
+ daemonSocket?: string;
69
+ perTxMaxWei?: string;
70
+ dailyMaxWei?: string;
71
+ weeklyMaxWei?: string;
72
+ maxGasPerChainWei?: string;
73
+ dailyMaxTxCount?: string;
74
+ perTxMaxFeePerGasWei?: string;
75
+ perTxMaxPriorityFeePerGasWei?: string;
76
+ perTxMaxCalldataBytes?: string;
77
+ token: string[];
78
+ allowNativeEth?: boolean;
79
+ network?: string;
80
+ rpcUrl?: string;
81
+ chainName?: string;
82
+ recipient?: string;
83
+ attachPolicyId: string[];
84
+ attachBootstrapPolicies?: boolean;
85
+ bootstrapOutput?: string;
86
+ deleteBootstrapOutput?: boolean;
87
+ json?: boolean;
88
+ }
89
+
90
+ interface AdminTuiOptions {
91
+ daemonSocket?: string;
92
+ bootstrapOutput?: string;
93
+ deleteBootstrapOutput?: boolean;
94
+ json?: boolean;
95
+ printAgentAuthToken?: boolean;
96
+ }
97
+
98
+ export interface AdminSetupPlan {
99
+ command: 'setup';
100
+ mode: 'plan';
101
+ daemon: {
102
+ launchdLabel: string;
103
+ socket: string;
104
+ stateFile: string;
105
+ installReady: boolean;
106
+ installError: string | null;
107
+ sourceRunnerPath: string;
108
+ sourceDaemonBin: string;
109
+ sourceKeychainHelperBin: string;
110
+ managedRunnerPath: string;
111
+ managedDaemonBin: string;
112
+ managedKeychainHelperBin: string;
113
+ };
114
+ existingWallet: ExistingWalletSetupTarget | null;
115
+ overwrite: {
116
+ required: boolean;
117
+ requiresYesInNonInteractive: boolean;
118
+ };
119
+ walletSetup: WalletSetupPlan;
120
+ }
121
+
122
+ interface LaunchDaemonAssetPaths {
123
+ runnerPath: string;
124
+ daemonBin: string;
125
+ keychainHelperBin: string;
126
+ }
127
+
128
+ interface ManagedDaemonInstallPreconditionDeps {
129
+ existsSync?: (targetPath: string) => boolean;
130
+ assertTrustedExecutablePath?: (targetPath: string) => void;
131
+ assertTrustedRootPlannedDaemonSocketPath?: (targetPath: string, label?: string) => string;
132
+ assertTrustedRootPlannedPrivateFilePath?: (targetPath: string, label?: string) => string;
133
+ resolveInstallScriptPath?: () => string;
134
+ }
135
+
136
+ interface ManagedDaemonInstallPreconditions extends LaunchDaemonAssetPaths {
137
+ installScript: string;
138
+ managedRunnerPath: string;
139
+ managedDaemonBin: string;
140
+ managedKeychainHelperBin: string;
141
+ }
142
+
143
+ interface ManagedDaemonInstallResult {
144
+ label: string;
145
+ runnerPath: string;
146
+ daemonBin: string;
147
+ stateFile: string;
148
+ keychainAccount: string;
149
+ keychainService: string;
150
+ }
151
+
152
+ interface ManagedLaunchDaemonAssetMatchDeps {
153
+ filesHaveIdenticalContents?: (leftPath: string, rightPath: string) => boolean;
154
+ resolveManagedPaths?: () => LaunchDaemonAssetPaths;
155
+ }
156
+
157
+ interface CreateAdminSetupPlanDeps {
158
+ readConfig?: () => WlfiConfig;
159
+ assertManagedDaemonInstallPreconditions?: typeof assertManagedDaemonInstallPreconditions;
160
+ }
161
+
162
+ export interface ExistingWalletSetupTarget {
163
+ address?: string;
164
+ agentKeyId?: string;
165
+ hasLegacyAgentAuthToken: boolean;
166
+ }
167
+
168
+ interface ConfirmAdminSetupOverwriteDeps {
169
+ prompt?: (query: string) => Promise<string>;
170
+ stderr?: Pick<NodeJS.WriteStream, 'write'>;
171
+ }
172
+
173
+ interface ResolveAdminSetupVaultPasswordDeps {
174
+ env?: NodeJS.ProcessEnv;
175
+ readTrimmedStdin?: (label: string) => Promise<string>;
176
+ promptHidden?: (query: string) => Promise<string>;
177
+ }
178
+
179
+ function validateSecret(value: string, label: string): string {
180
+ if (Buffer.byteLength(value, 'utf8') > MAX_SECRET_STDIN_BYTES) {
181
+ throw new Error(`${label} must not exceed ${MAX_SECRET_STDIN_BYTES} bytes`);
182
+ }
183
+ if (!value.trim()) {
184
+ throw new Error(`${label} must not be empty or whitespace`);
185
+ }
186
+ return value;
187
+ }
188
+
189
+ async function readTrimmedStdin(label: string): Promise<string> {
190
+ process.stdin.setEncoding('utf8');
191
+ let raw = '';
192
+ for await (const chunk of process.stdin) {
193
+ raw += chunk;
194
+ if (Buffer.byteLength(raw, 'utf8') > MAX_SECRET_STDIN_BYTES) {
195
+ throw new Error(`${label} must not exceed ${MAX_SECRET_STDIN_BYTES} bytes`);
196
+ }
197
+ }
198
+ return validateSecret(raw.replace(/[\r\n]+$/u, ''), label);
199
+ }
200
+
201
+ async function promptHidden(query: string): Promise<string> {
202
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
203
+ throw new Error('vault password is required; use --vault-password-stdin or a local TTY prompt');
204
+ }
205
+
206
+ const rl = readline.createInterface({
207
+ input: process.stdin,
208
+ output: process.stdout,
209
+ terminal: true,
210
+ }) as readline.Interface & { stdoutMuted?: boolean; _writeToOutput?: (value: string) => void };
211
+
212
+ rl.stdoutMuted = true;
213
+ rl._writeToOutput = (value: string) => {
214
+ if (value.includes(query)) {
215
+ (rl as unknown as { output: NodeJS.WritableStream }).output.write(value);
216
+ return;
217
+ }
218
+ if (!rl.stdoutMuted) {
219
+ (rl as unknown as { output: NodeJS.WritableStream }).output.write(value);
220
+ }
221
+ };
222
+
223
+ const answer = await new Promise<string>((resolve) => {
224
+ rl.question(query, resolve);
225
+ });
226
+ rl.close();
227
+ process.stdout.write('\n');
228
+ return validateSecret(answer, 'vault password');
229
+ }
230
+
231
+ async function promptVisible(query: string, nonInteractiveError: string): Promise<string> {
232
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
233
+ throw new Error(nonInteractiveError);
234
+ }
235
+
236
+ const rl = readline.createInterface({
237
+ input: process.stdin,
238
+ output: process.stdout,
239
+ terminal: true,
240
+ });
241
+
242
+ const answer = await new Promise<string>((resolve) => {
243
+ rl.question(query, resolve);
244
+ });
245
+ rl.close();
246
+ return answer.trim();
247
+ }
248
+
249
+ function deriveWalletAddress(vaultPublicKey: string | undefined): string | undefined {
250
+ const normalized = vaultPublicKey?.trim();
251
+ if (!normalized) {
252
+ return undefined;
253
+ }
254
+ try {
255
+ return publicKeyToAddress(
256
+ (normalized.startsWith('0x') ? normalized : `0x${normalized}`) as Hex,
257
+ );
258
+ } catch {
259
+ return undefined;
260
+ }
261
+ }
262
+
263
+ export function resolveExistingWalletSetupTarget(
264
+ config: WlfiConfig,
265
+ ): ExistingWalletSetupTarget | null {
266
+ const address =
267
+ config.wallet?.address?.trim() || deriveWalletAddress(config.wallet?.vaultPublicKey);
268
+ const agentKeyId = config.agentKeyId?.trim() || config.wallet?.agentKeyId?.trim();
269
+ const hasLegacyAgentAuthToken = Boolean(config.agentAuthToken?.trim());
270
+
271
+ if (!address && !agentKeyId && !hasLegacyAgentAuthToken) {
272
+ return null;
273
+ }
274
+
275
+ return {
276
+ address: address || undefined,
277
+ agentKeyId: agentKeyId || undefined,
278
+ hasLegacyAgentAuthToken,
279
+ };
280
+ }
281
+
282
+ export async function confirmAdminSetupOverwrite(
283
+ options: Pick<AdminSetupOptions, 'yes' | 'nonInteractive'>,
284
+ config: WlfiConfig,
285
+ deps: ConfirmAdminSetupOverwriteDeps = {},
286
+ ): Promise<void> {
287
+ const existing = resolveExistingWalletSetupTarget(config);
288
+ if (!existing || options.yes) {
289
+ return;
290
+ }
291
+ if (options.nonInteractive) {
292
+ throw new Error(
293
+ '`wlfi-agent admin setup` would overwrite the existing wallet; rerun with --yes in non-interactive mode',
294
+ );
295
+ }
296
+
297
+ const stderr = deps.stderr ?? process.stderr;
298
+ stderr.write(
299
+ 'warning: admin setup will overwrite the current local wallet metadata and agent credentials.\n',
300
+ );
301
+ if (existing.address) {
302
+ stderr.write(`current address: ${existing.address}\n`);
303
+ }
304
+ if (existing.agentKeyId) {
305
+ stderr.write(`current agent key id: ${existing.agentKeyId}\n`);
306
+ }
307
+ if (existing.hasLegacyAgentAuthToken) {
308
+ stderr.write('legacy agent auth token is still present in config.json\n');
309
+ }
310
+
311
+ const confirmation = await (
312
+ deps.prompt ??
313
+ ((query: string) =>
314
+ promptVisible(
315
+ query,
316
+ '`wlfi-agent admin setup` requires --yes in non-interactive environments when overwriting an existing wallet',
317
+ ))
318
+ )('Type OVERWRITE to replace the current local wallet: ');
319
+ if (confirmation !== 'OVERWRITE') {
320
+ throw new Error('admin setup aborted');
321
+ }
322
+ }
323
+
324
+ export async function resolveAdminSetupVaultPassword(
325
+ options: Pick<AdminSetupOptions, 'vaultPassword' | 'vaultPasswordStdin' | 'nonInteractive'>,
326
+ deps: ResolveAdminSetupVaultPasswordDeps = {},
327
+ ): Promise<string> {
328
+ const env = deps.env ?? process.env;
329
+ const readVaultPasswordFromStdin = deps.readTrimmedStdin ?? readTrimmedStdin;
330
+ const promptForVaultPassword = deps.promptHidden ?? promptHidden;
331
+
332
+ if (options.vaultPassword && options.vaultPasswordStdin) {
333
+ throw new Error('--vault-password conflicts with --vault-password-stdin');
334
+ }
335
+ if (options.vaultPassword) {
336
+ validateSecret(options.vaultPassword, 'vault password');
337
+ throw new Error(
338
+ 'insecure --vault-password is disabled; use --vault-password-stdin or a local TTY prompt',
339
+ );
340
+ }
341
+ if (options.vaultPasswordStdin) {
342
+ return readVaultPasswordFromStdin('vault password');
343
+ }
344
+ if (Object.hasOwn(env, 'WLFI_VAULT_PASSWORD')) {
345
+ validateSecret(env.WLFI_VAULT_PASSWORD ?? '', 'vault password');
346
+ throw new Error(
347
+ 'WLFI_VAULT_PASSWORD is disabled for security; use --vault-password-stdin or a local TTY prompt',
348
+ );
349
+ }
350
+ if (options.nonInteractive) {
351
+ throw new Error(
352
+ 'vault password is required in non-interactive mode; use --vault-password-stdin',
353
+ );
354
+ }
355
+ return promptForVaultPassword('Vault password (input hidden; nothing will be echoed): ');
356
+ }
357
+
358
+ function resolveDaemonSocket(optionValue: string | undefined): string {
359
+ return path.resolve(optionValue?.trim() || DEFAULT_MANAGED_DAEMON_SOCKET);
360
+ }
361
+
362
+ function resolveStateFile(): string {
363
+ return path.resolve(DEFAULT_MANAGED_STATE_FILE);
364
+ }
365
+
366
+ export function createAdminSetupPlan(
367
+ options: AdminSetupOptions,
368
+ deps: CreateAdminSetupPlanDeps = {},
369
+ ): AdminSetupPlan {
370
+ if (options.rpcUrl && !options.network) {
371
+ throw new Error('--rpc-url requires --network');
372
+ }
373
+ if (options.chainName && !options.network) {
374
+ throw new Error('--chain-name requires --network');
375
+ }
376
+
377
+ const loadConfig = deps.readConfig ?? readConfig;
378
+ const checkManagedDaemonInstallPreconditions =
379
+ deps.assertManagedDaemonInstallPreconditions ?? assertManagedDaemonInstallPreconditions;
380
+ const config = loadConfig();
381
+ const daemonSocket = resolveDaemonSocket(options.daemonSocket);
382
+ const stateFile = resolveStateFile();
383
+ const sourcePaths = resolveSourceLaunchDaemonPaths(config);
384
+ const managedPaths = resolveManagedLaunchDaemonPaths();
385
+ let installReady = true;
386
+ let installError: string | null = null;
387
+
388
+ try {
389
+ checkManagedDaemonInstallPreconditions(config, daemonSocket, stateFile);
390
+ } catch (error) {
391
+ installReady = false;
392
+ installError = renderError(error);
393
+ }
394
+
395
+ const existingWallet = resolveExistingWalletSetupTarget(config);
396
+
397
+ return {
398
+ command: 'setup',
399
+ mode: 'plan',
400
+ daemon: {
401
+ launchdLabel: DEFAULT_LAUNCH_DAEMON_LABEL,
402
+ socket: daemonSocket,
403
+ stateFile,
404
+ installReady,
405
+ installError,
406
+ sourceRunnerPath: sourcePaths.runnerPath,
407
+ sourceDaemonBin: sourcePaths.daemonBin,
408
+ sourceKeychainHelperBin: sourcePaths.keychainHelperBin,
409
+ managedRunnerPath: managedPaths.runnerPath,
410
+ managedDaemonBin: managedPaths.daemonBin,
411
+ managedKeychainHelperBin: managedPaths.keychainHelperBin,
412
+ },
413
+ existingWallet,
414
+ overwrite: {
415
+ required: existingWallet !== null,
416
+ requiresYesInNonInteractive:
417
+ existingWallet !== null && Boolean(options.nonInteractive) && !options.yes,
418
+ },
419
+ walletSetup: createWalletSetupPlan(
420
+ {
421
+ vaultPasswordStdin: options.vaultPasswordStdin,
422
+ nonInteractive: options.nonInteractive,
423
+ daemonSocket,
424
+ perTxMaxWei: options.perTxMaxWei,
425
+ dailyMaxWei: options.dailyMaxWei,
426
+ weeklyMaxWei: options.weeklyMaxWei,
427
+ maxGasPerChainWei: options.maxGasPerChainWei,
428
+ dailyMaxTxCount: options.dailyMaxTxCount,
429
+ perTxMaxFeePerGasWei: options.perTxMaxFeePerGasWei,
430
+ perTxMaxPriorityFeePerGasWei: options.perTxMaxPriorityFeePerGasWei,
431
+ perTxMaxCalldataBytes: options.perTxMaxCalldataBytes,
432
+ token: options.token,
433
+ allowNativeEth: options.allowNativeEth,
434
+ network: options.network,
435
+ rpcUrl: options.rpcUrl,
436
+ chainName: options.chainName,
437
+ recipient: options.recipient,
438
+ attachPolicyId: options.attachPolicyId,
439
+ attachBootstrapPolicies: options.attachBootstrapPolicies,
440
+ bootstrapOutputPath: options.bootstrapOutput,
441
+ deleteBootstrapOutput: options.deleteBootstrapOutput,
442
+ },
443
+ {
444
+ readConfig: () => config,
445
+ },
446
+ ),
447
+ };
448
+ }
449
+
450
+ function formatAdminSetupExistingWallet(plan: AdminSetupPlan): string[] {
451
+ if (!plan.existingWallet) {
452
+ return ['Existing Wallet', '- none'];
453
+ }
454
+
455
+ return [
456
+ 'Existing Wallet',
457
+ `- Address: ${plan.existingWallet.address ?? 'unknown'}`,
458
+ `- Agent Key ID: ${plan.existingWallet.agentKeyId ?? 'unknown'}`,
459
+ `- Legacy Config Token Present: ${plan.existingWallet.hasLegacyAgentAuthToken ? 'yes' : 'no'}`,
460
+ ];
461
+ }
462
+
463
+ export function formatAdminSetupPlanText(plan: AdminSetupPlan): string {
464
+ const lines = [
465
+ 'Admin Setup Preview',
466
+ `LaunchDaemon Install: ${plan.daemon.installReady ? 'ready' : 'blocked'}`,
467
+ plan.daemon.installError ? `Install Error: ${plan.daemon.installError}` : null,
468
+ `Managed Socket: ${plan.daemon.socket}`,
469
+ `Managed State File: ${plan.daemon.stateFile}`,
470
+ `LaunchDaemon Label: ${plan.daemon.launchdLabel}`,
471
+ '',
472
+ ...formatAdminSetupExistingWallet(plan),
473
+ `Overwrite Confirmation Required: ${plan.overwrite.required ? 'yes' : 'no'}`,
474
+ `Non-interactive Requires --yes: ${plan.overwrite.requiresYesInNonInteractive ? 'yes' : 'no'}`,
475
+ '',
476
+ formatWalletSetupPlanText(plan.walletSetup),
477
+ ];
478
+
479
+ return lines.filter((line): line is string => line !== null).join('\n');
480
+ }
481
+
482
+ function backfillPersistedWalletProfileForTui(config: WlfiConfig): WlfiConfig {
483
+ if (!config.wallet || config.wallet.vaultKeyId) {
484
+ return config;
485
+ }
486
+
487
+ let resolvedWallet: ReturnType<typeof resolveWalletProfile>;
488
+ try {
489
+ resolvedWallet = resolveWalletProfile(config);
490
+ } catch {
491
+ return config;
492
+ }
493
+
494
+ if (!resolvedWallet.vaultKeyId) {
495
+ return config;
496
+ }
497
+
498
+ return writeConfig({
499
+ agentKeyId: config.agentKeyId ?? resolvedWallet.agentKeyId,
500
+ wallet: resolvedWallet,
501
+ });
502
+ }
503
+
504
+ function isRecord(value: unknown): value is Record<string, unknown> {
505
+ return typeof value === 'object' && value !== null;
506
+ }
507
+
508
+ function asOptionalRecord(value: unknown): Record<string, unknown> {
509
+ return isRecord(value) ? value : {};
510
+ }
511
+
512
+ function isFinalAdminCommandPayload(payload: unknown): payload is Record<string, unknown> {
513
+ return (
514
+ isRecord(payload) &&
515
+ (payload.command === 'setup' || payload.command === 'tui') &&
516
+ payload.mode !== 'plan'
517
+ );
518
+ }
519
+
520
+ export function prepareAdminCommandOutputPayload(
521
+ payload: unknown,
522
+ includeSecrets = false,
523
+ ): unknown {
524
+ if (!isRecord(payload)) {
525
+ return payload;
526
+ }
527
+
528
+ const prepared = { ...payload };
529
+ for (const [fieldName, redactedFieldName] of [['vaultPrivateKey', 'vaultPrivateKeyRedacted']] as const) {
530
+ const value = prepared[fieldName];
531
+ if (typeof value === 'string' && value.trim()) {
532
+ prepared[fieldName] = REDACTED_SECRET_PLACEHOLDER;
533
+ prepared[redactedFieldName] = true;
534
+ }
535
+ }
536
+
537
+ if (!includeSecrets) {
538
+ for (const [fieldName, redactedFieldName] of [['agentAuthToken', 'agentAuthTokenRedacted']] as const) {
539
+ const value = prepared[fieldName];
540
+ if (typeof value === 'string' && value.trim()) {
541
+ prepared[fieldName] = REDACTED_SECRET_PLACEHOLDER;
542
+ prepared[redactedFieldName] = true;
543
+ }
544
+ }
545
+ }
546
+
547
+ return prepared;
548
+ }
549
+
550
+ export function formatAdminCommandOutput(
551
+ payload: unknown,
552
+ options: { includeSecrets?: boolean } = {},
553
+ ): string {
554
+ if (typeof payload === 'string') {
555
+ return payload;
556
+ }
557
+
558
+ const prepared = prepareAdminCommandOutputPayload(payload, options.includeSecrets ?? false);
559
+ if (!isRecord(prepared)) {
560
+ return JSON.stringify(prepared, null, 2);
561
+ }
562
+
563
+ if (prepared.command === 'tui' && prepared.canceled === true) {
564
+ return 'tui canceled';
565
+ }
566
+
567
+ const keychain = asOptionalRecord(prepared.keychain);
568
+ const config = asOptionalRecord(prepared.config);
569
+ const daemon = asOptionalRecord(prepared.daemon);
570
+ const vaultPublicKey = String(prepared.vaultPublicKey ?? '').trim();
571
+ const agentAuthToken = String(prepared.agentAuthToken ?? '').trim();
572
+ const includeSecrets = options.includeSecrets ?? false;
573
+
574
+ let addressLine: string | null = null;
575
+ if (vaultPublicKey) {
576
+ const normalizedPublicKey = (
577
+ vaultPublicKey.startsWith('0x') ? vaultPublicKey : `0x${vaultPublicKey}`
578
+ ) as Hex;
579
+ addressLine = `address: ${publicKeyToAddress(normalizedPublicKey)}`;
580
+ }
581
+
582
+ const daemonSocket = String(daemon.daemonSocket ?? config.daemonSocket ?? '').trim();
583
+ const stateFile = String(daemon.stateFile ?? config.stateFile ?? '').trim();
584
+ const chainName = String(config.chainName ?? prepared.networkScope ?? 'unconfigured').trim();
585
+ const keychainService = String(keychain.service ?? '').trim();
586
+ const title = prepared.command === 'tui' ? 'tui complete' : 'setup complete';
587
+
588
+ const lines = [
589
+ title,
590
+ addressLine,
591
+ typeof prepared.vaultKeyId === 'string' && prepared.vaultKeyId.trim()
592
+ ? `vault key id: ${prepared.vaultKeyId}`
593
+ : null,
594
+ typeof prepared.agentKeyId === 'string' && prepared.agentKeyId.trim()
595
+ ? `agent key id: ${prepared.agentKeyId}`
596
+ : null,
597
+ daemonSocket ? `daemon socket: ${daemonSocket}` : null,
598
+ stateFile ? `state file: ${stateFile}` : null,
599
+ `chain: ${chainName || 'unconfigured'}`,
600
+ includeSecrets
601
+ ? agentAuthToken
602
+ ? `agent auth token: ${agentAuthToken}`
603
+ : null
604
+ : `agent auth token: ${KEYCHAIN_STORED_AGENT_AUTH_TOKEN}`,
605
+ keychainService ? `keychain service: ${keychainService}` : null,
606
+ ].filter((line): line is string => Boolean(line && !line.endsWith(': ')));
607
+
608
+ if (includeSecrets && agentAuthToken) {
609
+ lines.push('warning: keep the agent auth token carefully.');
610
+ }
611
+
612
+ return lines.join('\n');
613
+ }
614
+
615
+ function printCliPayload(payload: unknown, asJson: boolean, includeSecrets = false): void {
616
+ const prepared = isFinalAdminCommandPayload(payload)
617
+ ? prepareAdminCommandOutputPayload(payload, includeSecrets)
618
+ : payload;
619
+
620
+ if (asJson) {
621
+ process.stdout.write(`${JSON.stringify(prepared, null, 2)}\n`);
622
+ return;
623
+ }
624
+ if (typeof prepared === 'string') {
625
+ process.stdout.write(`${prepared}\n`);
626
+ return;
627
+ }
628
+ if (isFinalAdminCommandPayload(prepared)) {
629
+ process.stdout.write(`${formatAdminCommandOutput(prepared, { includeSecrets })}\n`);
630
+ return;
631
+ }
632
+ process.stdout.write(`${JSON.stringify(prepared, null, 2)}\n`);
633
+ }
634
+
635
+ function resolveRustBinDir(config: WlfiConfig): string {
636
+ return path.resolve(config.rustBinDir || defaultRustBinDir());
637
+ }
638
+
639
+ function resolveSourceLaunchDaemonPaths(config: WlfiConfig): LaunchDaemonAssetPaths {
640
+ const rustBinDir = resolveRustBinDir(config);
641
+ return {
642
+ runnerPath: path.join(rustBinDir, 'run-wlfi-agent-daemon.sh'),
643
+ daemonBin: path.join(
644
+ rustBinDir,
645
+ `wlfi-agent-daemon${process.platform === 'win32' ? '.exe' : ''}`,
646
+ ),
647
+ keychainHelperBin: path.join(
648
+ rustBinDir,
649
+ `wlfi-agent-system-keychain${process.platform === 'win32' ? '.exe' : ''}`,
650
+ ),
651
+ };
652
+ }
653
+
654
+ export function resolveManagedLaunchDaemonPaths(): LaunchDaemonAssetPaths {
655
+ return {
656
+ runnerPath: path.join(DEFAULT_MANAGED_BIN_DIR, 'run-wlfi-agent-daemon.sh'),
657
+ daemonBin: path.join(
658
+ DEFAULT_MANAGED_BIN_DIR,
659
+ `wlfi-agent-daemon${process.platform === 'win32' ? '.exe' : ''}`,
660
+ ),
661
+ keychainHelperBin: DEFAULT_MANAGED_KEYCHAIN_HELPER,
662
+ };
663
+ }
664
+
665
+ function resolveLaunchDaemonInstallScriptPath(): string {
666
+ const candidates = [
667
+ path.resolve(__dirname, '../scripts/launchd/install-user-daemon.sh'),
668
+ path.resolve(__dirname, '../../scripts/launchd/install-user-daemon.sh'),
669
+ path.resolve(process.cwd(), 'scripts/launchd/install-user-daemon.sh'),
670
+ ];
671
+
672
+ for (const candidate of candidates) {
673
+ if (fs.existsSync(candidate)) {
674
+ return candidate;
675
+ }
676
+ }
677
+
678
+ return candidates[0];
679
+ }
680
+
681
+ function filesHaveIdenticalContents(leftPath: string, rightPath: string): boolean {
682
+ try {
683
+ const leftStats = fs.statSync(leftPath);
684
+ const rightStats = fs.statSync(rightPath);
685
+ if (!leftStats.isFile() || !rightStats.isFile() || leftStats.size !== rightStats.size) {
686
+ return false;
687
+ }
688
+
689
+ return fs.readFileSync(leftPath).equals(fs.readFileSync(rightPath));
690
+ } catch {
691
+ return false;
692
+ }
693
+ }
694
+
695
+ export function managedLaunchDaemonAssetsMatchSource(
696
+ config: WlfiConfig,
697
+ deps: ManagedLaunchDaemonAssetMatchDeps = {},
698
+ ): boolean {
699
+ const sourcePaths = resolveSourceLaunchDaemonPaths(config);
700
+ const managedPaths = (deps.resolveManagedPaths ?? resolveManagedLaunchDaemonPaths)();
701
+ const compareFileContents = deps.filesHaveIdenticalContents ?? filesHaveIdenticalContents;
702
+ return (
703
+ compareFileContents(sourcePaths.runnerPath, managedPaths.runnerPath) &&
704
+ compareFileContents(sourcePaths.daemonBin, managedPaths.daemonBin) &&
705
+ compareFileContents(sourcePaths.keychainHelperBin, managedPaths.keychainHelperBin)
706
+ );
707
+ }
708
+
709
+ export function assertManagedDaemonInstallPreconditions(
710
+ config: WlfiConfig,
711
+ daemonSocket: string,
712
+ stateFile: string,
713
+ deps: ManagedDaemonInstallPreconditionDeps = {},
714
+ ): ManagedDaemonInstallPreconditions {
715
+ const existsSync = deps.existsSync ?? fs.existsSync;
716
+ const trustExecutablePath = deps.assertTrustedExecutablePath ?? assertTrustedExecutablePath;
717
+ const trustDaemonSocketPath =
718
+ deps.assertTrustedRootPlannedDaemonSocketPath ?? assertTrustedRootPlannedDaemonSocketPath;
719
+ const trustStateFilePath =
720
+ deps.assertTrustedRootPlannedPrivateFilePath ?? assertTrustedRootPlannedPrivateFilePath;
721
+ const installScript = (deps.resolveInstallScriptPath ?? resolveLaunchDaemonInstallScriptPath)();
722
+ const sourcePaths = resolveSourceLaunchDaemonPaths(config);
723
+ const managedPaths = resolveManagedLaunchDaemonPaths();
724
+
725
+ if (!existsSync(sourcePaths.runnerPath)) {
726
+ throw new Error(
727
+ `daemon runner is not installed at ${sourcePaths.runnerPath}; rerun npm run install:rust-binaries`,
728
+ );
729
+ }
730
+ if (!existsSync(sourcePaths.daemonBin)) {
731
+ throw new Error(
732
+ `daemon binary is not installed at ${sourcePaths.daemonBin}; rerun npm run install:rust-binaries`,
733
+ );
734
+ }
735
+ if (!existsSync(sourcePaths.keychainHelperBin)) {
736
+ throw new Error(
737
+ `daemon keychain helper is not installed at ${sourcePaths.keychainHelperBin}; rerun npm run install:rust-binaries`,
738
+ );
739
+ }
740
+ if (!existsSync(installScript)) {
741
+ throw new Error(`launchd install helper is not installed at ${installScript}`);
742
+ }
743
+
744
+ trustExecutablePath(sourcePaths.runnerPath);
745
+ trustExecutablePath(sourcePaths.daemonBin);
746
+ trustExecutablePath(sourcePaths.keychainHelperBin);
747
+ trustExecutablePath(installScript);
748
+ trustDaemonSocketPath(daemonSocket, 'Managed daemon socket');
749
+ trustStateFilePath(stateFile, 'Managed daemon state file');
750
+
751
+ return {
752
+ ...sourcePaths,
753
+ installScript,
754
+ managedRunnerPath: managedPaths.runnerPath,
755
+ managedDaemonBin: managedPaths.daemonBin,
756
+ managedKeychainHelperBin: managedPaths.keychainHelperBin,
757
+ };
758
+ }
759
+
760
+ function renderError(error: unknown): string {
761
+ return error instanceof Error ? error.message : String(error);
762
+ }
763
+
764
+ function createProgress(message: string, enabled = true): ProgressHandle {
765
+ if (!enabled) {
766
+ return {
767
+ succeed() {},
768
+ fail() {},
769
+ info() {},
770
+ };
771
+ }
772
+
773
+ if (!process.stderr.isTTY) {
774
+ process.stderr.write(`==> ${message}
775
+ `);
776
+ return {
777
+ succeed(finalMessage = message) {
778
+ process.stderr.write(`✓ ${finalMessage}
779
+ `);
780
+ },
781
+ fail(finalMessage = `${message} failed`) {
782
+ process.stderr.write(`✗ ${finalMessage}
783
+ `);
784
+ },
785
+ info(finalMessage = message) {
786
+ process.stderr.write(`• ${finalMessage}
787
+ `);
788
+ },
789
+ };
790
+ }
791
+
792
+ let frameIndex = 0;
793
+ const render = (prefix: string, currentMessage: string) => {
794
+ process.stderr.write(`\r\u001b[2K${prefix} ${currentMessage}`);
795
+ };
796
+
797
+ render(SPINNER_FRAMES[frameIndex], message);
798
+ const timer = setInterval(() => {
799
+ frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
800
+ render(SPINNER_FRAMES[frameIndex], message);
801
+ }, 80);
802
+
803
+ const stop = (prefix: string, finalMessage: string) => {
804
+ clearInterval(timer);
805
+ render(prefix, finalMessage);
806
+ process.stderr.write('\n');
807
+ };
808
+
809
+ return {
810
+ succeed(finalMessage = message) {
811
+ stop('✓', finalMessage);
812
+ },
813
+ fail(finalMessage = `${message} failed`) {
814
+ stop('✗', finalMessage);
815
+ },
816
+ info(finalMessage = message) {
817
+ stop('•', finalMessage);
818
+ },
819
+ };
820
+ }
821
+
822
+ async function daemonAcceptsVaultPassword(
823
+ config: WlfiConfig,
824
+ daemonSocket: string,
825
+ vaultPassword: string,
826
+ ): Promise<boolean> {
827
+ try {
828
+ await runRustBinary(
829
+ 'wlfi-agent-admin',
830
+ [
831
+ '--json',
832
+ '--non-interactive',
833
+ '--vault-password-stdin',
834
+ '--daemon-socket',
835
+ daemonSocket,
836
+ 'get-relay-config',
837
+ ],
838
+ config,
839
+ {
840
+ stdin: `${vaultPassword}\n`,
841
+ preSuppliedSecretStdin: 'vaultPassword',
842
+ scrubSensitiveEnv: true,
843
+ },
844
+ );
845
+ return true;
846
+ } catch (error) {
847
+ if (error instanceof RustBinaryExitError) {
848
+ const output = `${error.stderr}\n${error.stdout}`;
849
+ if (/authentication failed/iu.test(output)) {
850
+ return false;
851
+ }
852
+ }
853
+ throw error;
854
+ }
855
+ }
856
+
857
+ const sudoSession = createSudoSession({
858
+ promptPassword: async () =>
859
+ await promptHidden(
860
+ 'Root password (input hidden; required to install or recover the root daemon): ',
861
+ ),
862
+ });
863
+
864
+ async function managedStateFileExists(stateFile: string): Promise<boolean> {
865
+ const result = await sudoSession.run(['/bin/test', '-e', stateFile]);
866
+ if (result.code === 0) {
867
+ return true;
868
+ }
869
+ if (
870
+ result.code === 1 &&
871
+ !/password is required|try again|authentication failed|sorry/iu.test(result.stderr)
872
+ ) {
873
+ return false;
874
+ }
875
+ throw new Error(
876
+ result.stderr.trim() ||
877
+ result.stdout.trim() ||
878
+ `failed to inspect managed daemon state file '${stateFile}' (exit code ${result.code})`,
879
+ );
880
+ }
881
+
882
+ async function waitForTrustedDaemonSocket(targetPath: string, timeoutMs = 15_000): Promise<void> {
883
+ async function canConnect(socketPath: string): Promise<boolean> {
884
+ return new Promise<boolean>((resolve) => {
885
+ const socket = net.createConnection(socketPath);
886
+ let settled = false;
887
+
888
+ const finish = (result: boolean) => {
889
+ if (settled) {
890
+ return;
891
+ }
892
+ settled = true;
893
+ socket.destroy();
894
+ resolve(result);
895
+ };
896
+
897
+ socket.once('connect', () => finish(true));
898
+ socket.once('error', (error: NodeJS.ErrnoException) => {
899
+ if (error.code === 'ECONNREFUSED' || error.code === 'ENOENT') {
900
+ finish(false);
901
+ return;
902
+ }
903
+ finish(false);
904
+ });
905
+ socket.setTimeout(500, () => finish(false));
906
+ });
907
+ }
908
+
909
+ const started = Date.now();
910
+ while (Date.now() - started < timeoutMs) {
911
+ try {
912
+ const resolvedPath = assertTrustedAdminDaemonSocketPath(targetPath);
913
+ if (await canConnect(resolvedPath)) {
914
+ return;
915
+ }
916
+ } catch {}
917
+ await new Promise((resolve) => setTimeout(resolve, 250));
918
+ }
919
+
920
+ const resolvedPath = assertTrustedAdminDaemonSocketPath(targetPath);
921
+ if (!(await canConnect(resolvedPath))) {
922
+ throw new Error(`daemon socket '${resolvedPath}' is present but not accepting connections yet`);
923
+ }
924
+ }
925
+
926
+ function plistContainsValue(plistContents: string, value: string): boolean {
927
+ return plistContents.includes(`<string>${value}</string>`);
928
+ }
929
+
930
+ function resolveInstalledLaunchDaemonPaths(
931
+ config: WlfiConfig,
932
+ plistContents: string,
933
+ ): LaunchDaemonAssetPaths | null {
934
+ const sourcePaths = resolveSourceLaunchDaemonPaths(config);
935
+ const managedPaths = resolveManagedLaunchDaemonPaths();
936
+
937
+ if (
938
+ plistContainsValue(plistContents, sourcePaths.runnerPath) &&
939
+ plistContainsValue(plistContents, sourcePaths.daemonBin) &&
940
+ plistContainsValue(plistContents, sourcePaths.keychainHelperBin)
941
+ ) {
942
+ return sourcePaths;
943
+ }
944
+
945
+ if (
946
+ plistContainsValue(plistContents, managedPaths.runnerPath) &&
947
+ plistContainsValue(plistContents, managedPaths.daemonBin) &&
948
+ plistContainsValue(plistContents, managedPaths.keychainHelperBin)
949
+ ) {
950
+ return managedPaths;
951
+ }
952
+
953
+ return null;
954
+ }
955
+
956
+ function isManagedDaemonInstallCurrent(
957
+ config: WlfiConfig,
958
+ daemonSocket: string,
959
+ stateFile: string,
960
+ ): boolean {
961
+ const sourcePaths = resolveSourceLaunchDaemonPaths(config);
962
+ const keychainAccount = os.userInfo().username;
963
+ const expectedAdminUid = String(process.getuid?.() ?? process.geteuid?.() ?? 0);
964
+
965
+ if (!fs.existsSync(DEFAULT_LAUNCH_DAEMON_PLIST)) {
966
+ return false;
967
+ }
968
+ if (
969
+ !fs.existsSync(sourcePaths.runnerPath) ||
970
+ !fs.existsSync(sourcePaths.daemonBin) ||
971
+ !fs.existsSync(sourcePaths.keychainHelperBin)
972
+ ) {
973
+ return false;
974
+ }
975
+
976
+ let plistContents: string;
977
+ try {
978
+ plistContents = fs.readFileSync(DEFAULT_LAUNCH_DAEMON_PLIST, 'utf8');
979
+ } catch {
980
+ return false;
981
+ }
982
+
983
+ const installedPaths = resolveInstalledLaunchDaemonPaths(config, plistContents);
984
+ if (!installedPaths) {
985
+ return false;
986
+ }
987
+ if (
988
+ !fs.existsSync(installedPaths.runnerPath) ||
989
+ !fs.existsSync(installedPaths.daemonBin) ||
990
+ !fs.existsSync(installedPaths.keychainHelperBin)
991
+ ) {
992
+ return false;
993
+ }
994
+ if (!filesHaveIdenticalContents(sourcePaths.runnerPath, installedPaths.runnerPath)) {
995
+ return false;
996
+ }
997
+ if (!filesHaveIdenticalContents(sourcePaths.daemonBin, installedPaths.daemonBin)) {
998
+ return false;
999
+ }
1000
+ if (
1001
+ !filesHaveIdenticalContents(sourcePaths.keychainHelperBin, installedPaths.keychainHelperBin)
1002
+ ) {
1003
+ return false;
1004
+ }
1005
+
1006
+ return [
1007
+ DEFAULT_LAUNCH_DAEMON_LABEL,
1008
+ installedPaths.runnerPath,
1009
+ installedPaths.daemonBin,
1010
+ installedPaths.keychainHelperBin,
1011
+ daemonSocket,
1012
+ stateFile,
1013
+ DAEMON_PASSWORD_KEYCHAIN_SERVICE,
1014
+ keychainAccount,
1015
+ DEFAULT_SIGNER_BACKEND,
1016
+ expectedAdminUid,
1017
+ ].every((value) => plistContainsValue(plistContents, value));
1018
+ }
1019
+
1020
+ async function installLaunchDaemon(
1021
+ config: WlfiConfig,
1022
+ daemonSocket: string,
1023
+ stateFile: string,
1024
+ vaultPassword: string,
1025
+ ): Promise<ManagedDaemonInstallResult> {
1026
+ const installPreconditions = assertManagedDaemonInstallPreconditions(
1027
+ config,
1028
+ daemonSocket,
1029
+ stateFile,
1030
+ );
1031
+ const keychainAccount = os.userInfo().username;
1032
+
1033
+ const installResult = await sudoSession.run(
1034
+ [
1035
+ installPreconditions.installScript,
1036
+ '--label',
1037
+ DEFAULT_LAUNCH_DAEMON_LABEL,
1038
+ '--runner',
1039
+ installPreconditions.runnerPath,
1040
+ '--daemon-bin',
1041
+ installPreconditions.daemonBin,
1042
+ '--keychain-helper',
1043
+ installPreconditions.keychainHelperBin,
1044
+ '--state-file',
1045
+ stateFile,
1046
+ '--daemon-socket',
1047
+ daemonSocket,
1048
+ '--keychain-service',
1049
+ DAEMON_PASSWORD_KEYCHAIN_SERVICE,
1050
+ '--keychain-account',
1051
+ keychainAccount,
1052
+ '--signer-backend',
1053
+ DEFAULT_SIGNER_BACKEND,
1054
+ '--allow-admin-euid',
1055
+ String(process.getuid?.() ?? process.geteuid?.() ?? 0),
1056
+ '--allow-agent-euid',
1057
+ String(process.getuid?.() ?? process.geteuid?.() ?? 0),
1058
+ '--vault-password-stdin',
1059
+ ],
1060
+ {
1061
+ stdin: `${vaultPassword}\n`,
1062
+ inheritOutput: true,
1063
+ },
1064
+ );
1065
+ if (installResult.code !== 0) {
1066
+ throw new Error(
1067
+ installResult.stderr.trim() ||
1068
+ installResult.stdout.trim() ||
1069
+ `sudo exited with code ${installResult.code}`,
1070
+ );
1071
+ }
1072
+
1073
+ return {
1074
+ label: DEFAULT_LAUNCH_DAEMON_LABEL,
1075
+ runnerPath: installPreconditions.managedRunnerPath,
1076
+ daemonBin: installPreconditions.managedDaemonBin,
1077
+ stateFile,
1078
+ keychainAccount,
1079
+ keychainService: DAEMON_PASSWORD_KEYCHAIN_SERVICE,
1080
+ };
1081
+ }
1082
+
1083
+ async function runAdminSetup(options: AdminSetupOptions): Promise<void> {
1084
+ if (options.plan) {
1085
+ const plan = createAdminSetupPlan(options);
1086
+ printCliPayload(options.json ? plan : formatAdminSetupPlanText(plan), options.json ?? false);
1087
+ return;
1088
+ }
1089
+
1090
+ if (options.rpcUrl && !options.network) {
1091
+ throw new Error('--rpc-url requires --network');
1092
+ }
1093
+ if (options.chainName && !options.network) {
1094
+ throw new Error('--chain-name requires --network');
1095
+ }
1096
+
1097
+ const config = readConfig();
1098
+ const daemonSocket = resolveDaemonSocket(options.daemonSocket);
1099
+ const stateFile = resolveStateFile();
1100
+ assertManagedDaemonInstallPreconditions(config, daemonSocket, stateFile);
1101
+ await confirmAdminSetupOverwrite(options, config);
1102
+ const vaultPassword = await resolveAdminSetupVaultPassword(options);
1103
+ const showProgress = !options.json;
1104
+
1105
+ const existingDaemonProgress = createProgress('Checking existing daemon', showProgress);
1106
+ const plistContents = fs.existsSync(DEFAULT_LAUNCH_DAEMON_PLIST)
1107
+ ? fs.readFileSync(DEFAULT_LAUNCH_DAEMON_PLIST, 'utf8')
1108
+ : null;
1109
+ const installedPaths = plistContents
1110
+ ? resolveInstalledLaunchDaemonPaths(config, plistContents)
1111
+ : null;
1112
+
1113
+ let daemon: ManagedDaemonInstallResult | null = null;
1114
+ let installIsCurrent = isManagedDaemonInstallCurrent(config, daemonSocket, stateFile);
1115
+ let existingDaemonRejectedPassword = false;
1116
+ let existingDaemonResponding = false;
1117
+
1118
+ try {
1119
+ await waitForTrustedDaemonSocket(daemonSocket, 1_500);
1120
+ existingDaemonResponding = true;
1121
+ const accepted = await daemonAcceptsVaultPassword(config, daemonSocket, vaultPassword);
1122
+ if (accepted) {
1123
+ installIsCurrent = true;
1124
+ daemon = {
1125
+ label: DEFAULT_LAUNCH_DAEMON_LABEL,
1126
+ runnerPath: installedPaths?.runnerPath ?? resolveManagedLaunchDaemonPaths().runnerPath,
1127
+ daemonBin: installedPaths?.daemonBin ?? resolveManagedLaunchDaemonPaths().daemonBin,
1128
+ stateFile,
1129
+ keychainAccount: os.userInfo().username,
1130
+ keychainService: DAEMON_PASSWORD_KEYCHAIN_SERVICE,
1131
+ };
1132
+ existingDaemonProgress.succeed('Existing daemon is ready and accepted the vault password');
1133
+ } else {
1134
+ existingDaemonRejectedPassword = true;
1135
+ existingDaemonProgress.succeed(
1136
+ 'Existing daemon is running but needs password recovery or reinstall',
1137
+ );
1138
+ }
1139
+ } catch {
1140
+ existingDaemonProgress.succeed('No ready daemon detected; checking installation state');
1141
+ }
1142
+
1143
+ if (!daemon) {
1144
+ if (installIsCurrent && existingDaemonResponding) {
1145
+ const currentPlistContents = fs.readFileSync(DEFAULT_LAUNCH_DAEMON_PLIST, 'utf8');
1146
+ const currentInstalledPaths =
1147
+ resolveInstalledLaunchDaemonPaths(config, currentPlistContents) ??
1148
+ resolveManagedLaunchDaemonPaths();
1149
+ daemon = {
1150
+ label: DEFAULT_LAUNCH_DAEMON_LABEL,
1151
+ runnerPath: currentInstalledPaths.runnerPath,
1152
+ daemonBin: currentInstalledPaths.daemonBin,
1153
+ stateFile,
1154
+ keychainAccount: os.userInfo().username,
1155
+ keychainService: DAEMON_PASSWORD_KEYCHAIN_SERVICE,
1156
+ };
1157
+ const installProgress = createProgress('Checking existing daemon installation', showProgress);
1158
+ installProgress.succeed('Existing daemon install looks current');
1159
+ } else {
1160
+ if (installIsCurrent && !existingDaemonResponding && !options.json) {
1161
+ process.stderr.write(
1162
+ 'Existing daemon install metadata looks current, but the daemon is not responding; setup will recover it now.\n',
1163
+ );
1164
+ }
1165
+ if (!options.json && typeof process.geteuid === 'function' && process.geteuid() !== 0) {
1166
+ process.stderr.write(
1167
+ 'Root password required: setup must install or recover the root LaunchDaemon and store the daemon password in System Keychain.\n',
1168
+ );
1169
+ }
1170
+ await sudoSession.prime();
1171
+ const installProgress = createProgress('Installing and restarting daemon', showProgress);
1172
+ try {
1173
+ daemon = await installLaunchDaemon(config, daemonSocket, stateFile, vaultPassword);
1174
+ installProgress.succeed('Daemon installed and restarted');
1175
+ } catch (error) {
1176
+ installProgress.fail();
1177
+ throw error;
1178
+ }
1179
+ }
1180
+ }
1181
+
1182
+ const readyProgress = createProgress('Waiting for daemon to become ready', showProgress);
1183
+ try {
1184
+ await waitForTrustedDaemonSocket(daemonSocket);
1185
+ readyProgress.succeed('Daemon is ready');
1186
+ } catch (error) {
1187
+ readyProgress.fail();
1188
+ throw error;
1189
+ }
1190
+
1191
+ const authProgress = createProgress('Verifying daemon vault password', showProgress);
1192
+ let daemonAcceptedPassword: boolean;
1193
+ try {
1194
+ daemonAcceptedPassword = await daemonAcceptsVaultPassword(config, daemonSocket, vaultPassword);
1195
+ } catch (error) {
1196
+ authProgress.fail();
1197
+ throw error;
1198
+ }
1199
+
1200
+ if (!daemonAcceptedPassword) {
1201
+ if (existingDaemonRejectedPassword) {
1202
+ authProgress.fail('Existing daemon password does not unlock the stored daemon state');
1203
+ throw new Error(
1204
+ `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.`,
1205
+ );
1206
+ }
1207
+
1208
+ authProgress.info('Daemon rejected the requested vault password; inspecting managed state');
1209
+ const stateProbeProgress = createProgress('Inspecting managed daemon state', showProgress);
1210
+ let stateExists: boolean;
1211
+ try {
1212
+ stateExists = await managedStateFileExists(stateFile);
1213
+ stateProbeProgress.succeed(
1214
+ stateExists ? 'Managed daemon state already exists' : 'No managed daemon state found',
1215
+ );
1216
+ } catch (error) {
1217
+ stateProbeProgress.fail();
1218
+ throw error;
1219
+ }
1220
+
1221
+ if (stateExists) {
1222
+ authProgress.fail('Existing daemon password does not unlock the stored daemon state');
1223
+ throw new Error(
1224
+ `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.`,
1225
+ );
1226
+ }
1227
+
1228
+ authProgress.fail(
1229
+ 'Existing daemon password differs; reinstalling with the requested vault password',
1230
+ );
1231
+
1232
+ if (!options.json && typeof process.geteuid === 'function' && process.geteuid() !== 0) {
1233
+ process.stderr.write(
1234
+ 'Root password required: setup must reinstall the root LaunchDaemon to rotate the managed daemon password.\n',
1235
+ );
1236
+ }
1237
+ await sudoSession.prime();
1238
+ const reinstallProgress = createProgress(
1239
+ 'Reinstalling daemon with the requested vault password',
1240
+ showProgress,
1241
+ );
1242
+ try {
1243
+ daemon = await installLaunchDaemon(config, daemonSocket, stateFile, vaultPassword);
1244
+ reinstallProgress.succeed('Daemon reinstalled and restarted');
1245
+ } catch (error) {
1246
+ reinstallProgress.fail();
1247
+ throw error;
1248
+ }
1249
+
1250
+ const restartedReadyProgress = createProgress(
1251
+ 'Waiting for restarted daemon to become ready',
1252
+ showProgress,
1253
+ );
1254
+ try {
1255
+ await waitForTrustedDaemonSocket(daemonSocket);
1256
+ restartedReadyProgress.succeed('Restarted daemon is ready');
1257
+ } catch (error) {
1258
+ restartedReadyProgress.fail();
1259
+ throw error;
1260
+ }
1261
+
1262
+ const retryAuthProgress = createProgress('Re-checking daemon vault password', showProgress);
1263
+ let retryAcceptedPassword: boolean;
1264
+ try {
1265
+ retryAcceptedPassword = await daemonAcceptsVaultPassword(config, daemonSocket, vaultPassword);
1266
+ } catch (error) {
1267
+ retryAuthProgress.fail();
1268
+ throw error;
1269
+ }
1270
+
1271
+ if (!retryAcceptedPassword) {
1272
+ retryAuthProgress.fail();
1273
+ throw new Error(
1274
+ 'the managed daemon still rejects the requested vault password. Re-run setup with the original vault password, or clear the managed daemon state before initializing a fresh wallet.',
1275
+ );
1276
+ }
1277
+
1278
+ retryAuthProgress.succeed('Daemon accepted the requested vault password');
1279
+ } else {
1280
+ authProgress.succeed('Daemon accepted the requested vault password');
1281
+ }
1282
+
1283
+ writeConfig({
1284
+ daemonSocket,
1285
+ stateFile,
1286
+ });
1287
+
1288
+ const resolvedNetwork = options.network
1289
+ ? resolveCliNetworkProfile(options.network, config)
1290
+ : null;
1291
+ const resolvedNetworkId = resolvedNetwork?.chainId;
1292
+ const resolvedChainName = options.chainName?.trim() || resolvedNetwork?.name;
1293
+
1294
+ assertWalletSetupExecutionPreconditions(
1295
+ {
1296
+ daemonSocket,
1297
+ network: resolvedNetworkId,
1298
+ rpcUrl: options.rpcUrl,
1299
+ },
1300
+ config,
1301
+ {
1302
+ assertTrustedDaemonSocketPath: assertTrustedAdminDaemonSocketPath,
1303
+ },
1304
+ );
1305
+
1306
+ const bootstrapOutput = resolveWalletSetupBootstrapOutputPath(options.bootstrapOutput);
1307
+ const bootstrapInvocation = buildAdminSetupBootstrapInvocation({
1308
+ vaultPassword,
1309
+ daemonSocket,
1310
+ perTxMaxWei: options.perTxMaxWei,
1311
+ dailyMaxWei: options.dailyMaxWei,
1312
+ weeklyMaxWei: options.weeklyMaxWei,
1313
+ maxGasPerChainWei: options.maxGasPerChainWei,
1314
+ dailyMaxTxCount: options.dailyMaxTxCount,
1315
+ perTxMaxFeePerGasWei: options.perTxMaxFeePerGasWei,
1316
+ perTxMaxPriorityFeePerGasWei: options.perTxMaxPriorityFeePerGasWei,
1317
+ perTxMaxCalldataBytes: options.perTxMaxCalldataBytes,
1318
+ token: options.token,
1319
+ allowNativeEth: options.allowNativeEth,
1320
+ network: resolvedNetworkId !== undefined ? String(resolvedNetworkId) : undefined,
1321
+ recipient: options.recipient,
1322
+ attachPolicyId: options.attachPolicyId,
1323
+ attachBootstrapPolicies: options.attachBootstrapPolicies,
1324
+ bootstrapOutputPath: bootstrapOutput.path,
1325
+ });
1326
+
1327
+ const bootstrapProgress = createProgress('Setting up wallet access', showProgress);
1328
+ try {
1329
+ await runRustBinary('wlfi-agent-admin', bootstrapInvocation.args, config, {
1330
+ stdin: bootstrapInvocation.stdin,
1331
+ preSuppliedSecretStdin: 'vaultPassword',
1332
+ scrubSensitiveEnv: true,
1333
+ });
1334
+ } catch (error) {
1335
+ bootstrapProgress.fail();
1336
+ try {
1337
+ cleanupBootstrapAgentCredentialsFile(
1338
+ bootstrapOutput.path,
1339
+ resolveWalletSetupCleanupAction(
1340
+ bootstrapOutput.autoGenerated,
1341
+ options.deleteBootstrapOutput ?? false,
1342
+ ),
1343
+ );
1344
+ } catch (error) {
1345
+ console.error(
1346
+ `warning: failed to scrub bootstrap output after setup failure: ${renderError(error)}`,
1347
+ );
1348
+ }
1349
+ if (error instanceof RustBinaryExitError) {
1350
+ const output = error.stderr || error.stdout;
1351
+ if (output) {
1352
+ process.stderr.write(output.endsWith('\n') ? output : `${output}\n`);
1353
+ }
1354
+ process.exitCode = error.code;
1355
+ return;
1356
+ }
1357
+ throw error;
1358
+ }
1359
+ bootstrapProgress.succeed('Bootstrap completed');
1360
+
1361
+ const finalizeProgress = createProgress('Importing agent token and saving config', showProgress);
1362
+ let summary: CompleteWalletSetupResult;
1363
+ try {
1364
+ summary = completeWalletSetup({
1365
+ bootstrapOutputPath: bootstrapOutput.path,
1366
+ cleanupAction: resolveWalletSetupCleanupAction(
1367
+ bootstrapOutput.autoGenerated,
1368
+ options.deleteBootstrapOutput ?? false,
1369
+ ),
1370
+ daemonSocket,
1371
+ perTxMaxWei: options.perTxMaxWei,
1372
+ dailyMaxWei: options.dailyMaxWei,
1373
+ weeklyMaxWei: options.weeklyMaxWei,
1374
+ maxGasPerChainWei: options.maxGasPerChainWei,
1375
+ dailyMaxTxCount: options.dailyMaxTxCount,
1376
+ perTxMaxFeePerGasWei: options.perTxMaxFeePerGasWei,
1377
+ perTxMaxPriorityFeePerGasWei: options.perTxMaxPriorityFeePerGasWei,
1378
+ perTxMaxCalldataBytes: options.perTxMaxCalldataBytes,
1379
+ token: options.token,
1380
+ allowNativeEth: options.allowNativeEth,
1381
+ network: resolvedNetworkId,
1382
+ recipient: options.recipient,
1383
+ attachPolicyId: options.attachPolicyId,
1384
+ attachBootstrapPolicies: options.attachBootstrapPolicies,
1385
+ rpcUrl: options.rpcUrl,
1386
+ chainName: resolvedChainName,
1387
+ });
1388
+ finalizeProgress.succeed('Agent token imported and config saved');
1389
+ } catch (error) {
1390
+ finalizeProgress.fail();
1391
+ throw error;
1392
+ }
1393
+
1394
+ const persistedConfig = writeConfig({
1395
+ daemonSocket,
1396
+ stateFile,
1397
+ });
1398
+
1399
+ printCliPayload(
1400
+ {
1401
+ command: 'setup',
1402
+ daemon: {
1403
+ autostart: true,
1404
+ label: daemon.label,
1405
+ launchdDomain: 'system',
1406
+ daemonSocket,
1407
+ stateFile: daemon.stateFile,
1408
+ runnerPath: daemon.runnerPath,
1409
+ daemonBinary: daemon.daemonBin,
1410
+ signerBackend: DEFAULT_SIGNER_BACKEND,
1411
+ keychainService: daemon.keychainService,
1412
+ keychainAccount: daemon.keychainAccount,
1413
+ },
1414
+ ...summary,
1415
+ config: redactConfig(persistedConfig),
1416
+ },
1417
+ options.json ?? false,
1418
+ options.printAgentAuthToken ?? false,
1419
+ );
1420
+ }
1421
+
1422
+ export function buildAdminTuiPassthroughArgs(input: {
1423
+ daemonSocket?: string;
1424
+ bootstrapOutputPath: string;
1425
+ }): string[] {
1426
+ const args = ['--json', '--quiet', '--output', input.bootstrapOutputPath];
1427
+ if (input.daemonSocket) {
1428
+ args.push('--daemon-socket', input.daemonSocket);
1429
+ }
1430
+ args.push('tui', '--print-agent-auth-token');
1431
+ return args;
1432
+ }
1433
+
1434
+ export function buildAdminSetupBootstrapInvocation(input: {
1435
+ vaultPassword: string;
1436
+ daemonSocket: string;
1437
+ perTxMaxWei?: string;
1438
+ dailyMaxWei?: string;
1439
+ weeklyMaxWei?: string;
1440
+ maxGasPerChainWei?: string;
1441
+ dailyMaxTxCount?: string;
1442
+ perTxMaxFeePerGasWei?: string;
1443
+ perTxMaxPriorityFeePerGasWei?: string;
1444
+ perTxMaxCalldataBytes?: string;
1445
+ token?: string[];
1446
+ allowNativeEth?: boolean;
1447
+ network?: string;
1448
+ recipient?: string;
1449
+ attachPolicyId?: string[];
1450
+ attachBootstrapPolicies?: boolean;
1451
+ bootstrapOutputPath: string;
1452
+ }): { args: string[]; stdin: string } {
1453
+ return {
1454
+ args: buildWalletSetupAdminArgs({
1455
+ vaultPasswordStdin: true,
1456
+ nonInteractive: true,
1457
+ daemonSocket: input.daemonSocket,
1458
+ perTxMaxWei: input.perTxMaxWei,
1459
+ dailyMaxWei: input.dailyMaxWei,
1460
+ weeklyMaxWei: input.weeklyMaxWei,
1461
+ maxGasPerChainWei: input.maxGasPerChainWei,
1462
+ dailyMaxTxCount: input.dailyMaxTxCount,
1463
+ perTxMaxFeePerGasWei: input.perTxMaxFeePerGasWei,
1464
+ perTxMaxPriorityFeePerGasWei: input.perTxMaxPriorityFeePerGasWei,
1465
+ perTxMaxCalldataBytes: input.perTxMaxCalldataBytes,
1466
+ token: input.token,
1467
+ allowNativeEth: input.allowNativeEth,
1468
+ network: input.network,
1469
+ recipient: input.recipient,
1470
+ attachPolicyId: input.attachPolicyId,
1471
+ attachBootstrapPolicies: input.attachBootstrapPolicies,
1472
+ bootstrapOutputPath: input.bootstrapOutputPath,
1473
+ }),
1474
+ stdin: `${validateSecret(input.vaultPassword, 'vault password')}\n`,
1475
+ };
1476
+ }
1477
+
1478
+ async function runAdminTui(options: AdminTuiOptions): Promise<void> {
1479
+ const config = backfillPersistedWalletProfileForTui(readConfig());
1480
+
1481
+ const daemonSocket = options.daemonSocket ? resolveDaemonSocket(options.daemonSocket) : undefined;
1482
+ const bootstrapOutput = resolveWalletSetupBootstrapOutputPath(options.bootstrapOutput);
1483
+ const cleanupAction = resolveWalletSetupCleanupAction(
1484
+ bootstrapOutput.autoGenerated,
1485
+ options.deleteBootstrapOutput ?? false,
1486
+ );
1487
+
1488
+ const code = await passthroughRustBinary(
1489
+ 'wlfi-agent-admin',
1490
+ buildAdminTuiPassthroughArgs({
1491
+ daemonSocket,
1492
+ bootstrapOutputPath: bootstrapOutput.path,
1493
+ }),
1494
+ config,
1495
+ );
1496
+ if (code !== 0) {
1497
+ try {
1498
+ cleanupBootstrapAgentCredentialsFile(bootstrapOutput.path, cleanupAction);
1499
+ } catch (error) {
1500
+ console.error(
1501
+ `warning: failed to scrub bootstrap output after tui failure: ${renderError(error)}`,
1502
+ );
1503
+ }
1504
+ process.exitCode = code;
1505
+ return;
1506
+ }
1507
+
1508
+ if (!fs.existsSync(bootstrapOutput.path)) {
1509
+ printCliPayload(
1510
+ options.json ? { command: 'tui', canceled: true } : 'tui canceled',
1511
+ options.json ?? false,
1512
+ );
1513
+ return;
1514
+ }
1515
+
1516
+ const summary = completeWalletSetup({
1517
+ bootstrapOutputPath: bootstrapOutput.path,
1518
+ cleanupAction,
1519
+ daemonSocket,
1520
+ });
1521
+
1522
+ printCliPayload(
1523
+ {
1524
+ command: 'tui',
1525
+ ...summary,
1526
+ },
1527
+ options.json ?? false,
1528
+ options.printAgentAuthToken ?? false,
1529
+ );
1530
+ }
1531
+
1532
+ export async function runAdminSetupCli(argv: string[]): Promise<void> {
1533
+ const program = new Command();
1534
+ program
1535
+ .name('wlfi-agent admin setup')
1536
+ .description(
1537
+ 'Store the vault password, install the root daemon autostart, set up wallet access, and print the wallet address',
1538
+ )
1539
+ .option('--plan', 'Print a sanitized setup preview without prompting or mutating state', false)
1540
+ .option('--vault-password-stdin', 'Read vault password from stdin', false)
1541
+ .option('--non-interactive', 'Disable password prompts', false)
1542
+ .option('-y, --yes', 'Skip the overwrite confirmation prompt', false)
1543
+ .option('--daemon-socket <path>', 'Daemon unix socket path')
1544
+ .option('--per-tx-max-wei <wei>', 'Per-transaction max spend in wei')
1545
+ .option('--daily-max-wei <wei>', 'Daily max spend in wei')
1546
+ .option('--weekly-max-wei <wei>', 'Weekly max spend in wei')
1547
+ .option('--max-gas-per-chain-wei <wei>', 'Per-chain gas-spend ceiling in wei')
1548
+ .option('--daily-max-tx-count <count>', 'Optional daily tx-count cap')
1549
+ .option('--per-tx-max-fee-per-gas-wei <wei>', 'Optional max fee-per-gas cap')
1550
+ .option('--per-tx-max-priority-fee-per-gas-wei <wei>', 'Optional max priority fee-per-gas cap')
1551
+ .option('--per-tx-max-calldata-bytes <bytes>', 'Optional calldata size cap')
1552
+ .option(
1553
+ '--token <address>',
1554
+ 'Allowed ERC-20 token address',
1555
+ (value, acc: string[]) => {
1556
+ acc.push(value);
1557
+ return acc;
1558
+ },
1559
+ [],
1560
+ )
1561
+ .option('--allow-native-eth', 'Allow native ETH transfers', false)
1562
+ .option('--network <name>', 'Network name for policy scope and active config')
1563
+ .option('--rpc-url <url>', 'Persist RPC URL for the configured active chain')
1564
+ .option('--chain-name <name>', 'Persist chain display name for the active chain')
1565
+ .option('--recipient <address>', 'Optional allowed recipient scope')
1566
+ .option(
1567
+ '--attach-policy-id <uuid>',
1568
+ 'Attach the new agent key to an existing policy id',
1569
+ (value, acc: string[]) => {
1570
+ acc.push(value);
1571
+ return acc;
1572
+ },
1573
+ [],
1574
+ )
1575
+ .option(
1576
+ '--attach-bootstrap-policies',
1577
+ 'Attach the new agent key to bootstrap-created policies',
1578
+ false,
1579
+ )
1580
+ .option('--bootstrap-output <path>', 'Write temporary bootstrap JSON to this private path')
1581
+ .option('--delete-bootstrap-output', 'Delete the bootstrap JSON after Keychain import', false)
1582
+ .option(
1583
+ '--print-agent-auth-token',
1584
+ 'Print the freshly issued agent auth token after importing it into macOS Keychain',
1585
+ false,
1586
+ )
1587
+ .option('--json', 'Print JSON output', false)
1588
+ .action(runAdminSetup);
1589
+
1590
+ await program.parseAsync(argv, { from: 'user' });
1591
+ }
1592
+
1593
+ export async function runAdminTuiCli(argv: string[]): Promise<void> {
1594
+ const program = new Command();
1595
+ program
1596
+ .name('wlfi-agent admin tui')
1597
+ .description(
1598
+ 'Launch the interactive terminal UI, then import the new agent token and activate the new wallet locally',
1599
+ )
1600
+ .option('--daemon-socket <path>', 'Daemon unix socket path')
1601
+ .option('--bootstrap-output <path>', 'Write temporary bootstrap JSON to this private path')
1602
+ .option('--delete-bootstrap-output', 'Delete the bootstrap JSON after Keychain import', false)
1603
+ .option(
1604
+ '--print-agent-auth-token',
1605
+ 'Print the freshly issued agent auth token after importing it into macOS Keychain',
1606
+ false,
1607
+ )
1608
+ .option('--json', 'Print JSON output', false)
1609
+ .action(runAdminTui);
1610
+
1611
+ await program.parseAsync(argv, { from: 'user' });
1612
+ }