@wlfi-agent/cli 1.4.12 → 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 -1894
  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 -1894
@@ -0,0 +1,1466 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { isAddress } from 'viem';
4
+ import {
5
+ assertSafeRpcUrl,
6
+ defaultDaemonSocketPath,
7
+ deleteConfigKey,
8
+ ensureWlfiHome,
9
+ listBuiltinChains,
10
+ readConfig,
11
+ redactConfig,
12
+ resolveWlfiHome,
13
+ type WlfiConfig,
14
+ writeConfig,
15
+ } from '../../packages/config/src/index.js';
16
+ import {
17
+ type AdminAccessGuardDeps,
18
+ type AdminAccessMode,
19
+ resolveAdminAccess,
20
+ } from './admin-guard.js';
21
+ import {
22
+ assertBootstrapSetupSummaryLeaseIsActive,
23
+ type BootstrapSetupSummary,
24
+ cleanupBootstrapAgentCredentialsFile,
25
+ readBootstrapSetupFile,
26
+ } from './bootstrap-credentials.js';
27
+ import {
28
+ assertPrivateFileStats,
29
+ assertTrustedAdminDaemonSocketPath,
30
+ assertTrustedDirectoryPath,
31
+ assertTrustedOwner,
32
+ } from './fs-trust.js';
33
+ import { AGENT_AUTH_TOKEN_KEYCHAIN_SERVICE, storeAgentAuthTokenInKeychain } from './keychain.js';
34
+ import { walletProfileFromBootstrapSummary } from './wallet-profile.js';
35
+
36
+ const PRIVATE_DIR_MODE = 0o700;
37
+ const GROUP_OTHER_WRITE_MODE_MASK = 0o022;
38
+ const STICKY_BIT_MODE = 0o1000;
39
+ const RUST_DEFAULT_VALUE = 'rust-default';
40
+ const AUTO_GENERATED_BOOTSTRAP_FILENAME = 'bootstrap-<pid>-<timestamp>.json';
41
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
42
+
43
+ type BootstrapCleanupAction = 'deleted' | 'redacted';
44
+
45
+ export interface WalletSetupAdminArgsInput {
46
+ vaultPassword?: string;
47
+ vaultPasswordStdin?: boolean;
48
+ nonInteractive?: boolean;
49
+ daemonSocket?: string;
50
+ perTxMaxWei?: string;
51
+ dailyMaxWei?: string;
52
+ weeklyMaxWei?: string;
53
+ maxGasPerChainWei?: string;
54
+ dailyMaxTxCount?: string;
55
+ perTxMaxFeePerGasWei?: string;
56
+ perTxMaxPriorityFeePerGasWei?: string;
57
+ perTxMaxCalldataBytes?: string;
58
+ token?: string[];
59
+ allowNativeEth?: boolean;
60
+ network?: string;
61
+ recipient?: string;
62
+ attachPolicyId?: string[];
63
+ attachBootstrapPolicies?: boolean;
64
+ fromSharedConfig?: boolean;
65
+ bootstrapOutputPath: string;
66
+ }
67
+
68
+ export interface WalletSetupBootstrapOutput {
69
+ path: string;
70
+ autoGenerated: boolean;
71
+ }
72
+
73
+ export interface CompleteWalletSetupOptions {
74
+ bootstrapOutputPath: string;
75
+ cleanupAction: BootstrapCleanupAction;
76
+ daemonSocket?: string;
77
+ perTxMaxWei?: string;
78
+ dailyMaxWei?: string;
79
+ weeklyMaxWei?: string;
80
+ maxGasPerChainWei?: string;
81
+ dailyMaxTxCount?: string;
82
+ perTxMaxFeePerGasWei?: string;
83
+ perTxMaxPriorityFeePerGasWei?: string;
84
+ perTxMaxCalldataBytes?: string;
85
+ token?: string[];
86
+ allowNativeEth?: boolean;
87
+ network?: number;
88
+ recipient?: string;
89
+ attachPolicyId?: string[];
90
+ attachBootstrapPolicies?: boolean;
91
+ rpcUrl?: string;
92
+ chainName?: string;
93
+ }
94
+
95
+ export interface CompleteWalletSetupResult extends BootstrapSetupSummary {
96
+ agentAuthToken: string;
97
+ sourceCleanup: BootstrapCleanupAction;
98
+ keychain: {
99
+ stored: true;
100
+ service: string;
101
+ };
102
+ config: Record<string, unknown>;
103
+ }
104
+
105
+ export interface WalletSetupPlanOptions
106
+ extends Omit<WalletSetupAdminArgsInput, 'bootstrapOutputPath'> {
107
+ bootstrapOutputPath?: string;
108
+ deleteBootstrapOutput?: boolean;
109
+ rpcUrl?: string;
110
+ chainName?: string;
111
+ }
112
+
113
+ export interface WalletSetupPlan {
114
+ adminAccess: {
115
+ permitted: boolean;
116
+ mode: AdminAccessMode;
117
+ reason: string;
118
+ nonInteractive: boolean;
119
+ };
120
+ rustCommand: {
121
+ binary: 'wlfi-agent-admin';
122
+ args: string[];
123
+ };
124
+ bootstrapOutput: {
125
+ path: string;
126
+ autoGenerated: boolean;
127
+ cleanupAction: BootstrapCleanupAction;
128
+ };
129
+ daemonSocket: string;
130
+ policyLimits: {
131
+ perTxMaxWei: string;
132
+ dailyMaxWei: string;
133
+ weeklyMaxWei: string;
134
+ maxGasPerChainWei: string;
135
+ dailyMaxTxCount: string | null;
136
+ perTxMaxFeePerGasWei: string | null;
137
+ perTxMaxPriorityFeePerGasWei: string | null;
138
+ perTxMaxCalldataBytes: string | null;
139
+ };
140
+ policyScope: {
141
+ network: number | null;
142
+ chainName: string | null;
143
+ recipient: string | null;
144
+ assets: {
145
+ mode: 'daemon-default' | 'native-only' | 'erc20-only' | 'mixed';
146
+ allowNativeEth: boolean;
147
+ erc20Tokens: string[];
148
+ };
149
+ };
150
+ policyAttachment: {
151
+ mode: 'none' | 'bootstrap-only' | 'explicit-only' | 'bootstrap-and-explicit';
152
+ attachBootstrapPolicies: boolean;
153
+ explicitPolicyIds: string[];
154
+ };
155
+ configAfterSetup: {
156
+ agentKeyId: '<issued during bootstrap>';
157
+ daemonSocket: string;
158
+ chainId: number | null;
159
+ chainName: string | null;
160
+ rpcUrl: string | null;
161
+ };
162
+ preflight: {
163
+ daemonSocketTrusted: boolean;
164
+ daemonSocketError: string | null;
165
+ rpcUrlTrusted: boolean | null;
166
+ rpcUrlError: string | null;
167
+ bootstrapOutputReady: boolean;
168
+ bootstrapOutputError: string | null;
169
+ };
170
+ security: {
171
+ rustPasswordTransport: 'interactive-prompt' | 'stdin-relay' | 'not-available';
172
+ childArgvContainsVaultPassword: false;
173
+ childEnvContainsVaultPassword: false;
174
+ keyMaterialStorage: 'macOS Keychain';
175
+ bootstrapCredentialCleanup: BootstrapCleanupAction;
176
+ notes: string[];
177
+ };
178
+ }
179
+
180
+ interface CompleteWalletSetupDeps {
181
+ platform?: NodeJS.Platform;
182
+ storeAgentAuthToken?: (agentKeyId: string, token: string) => void;
183
+ readConfig?: () => WlfiConfig;
184
+ writeConfig?: (nextConfig: WlfiConfig) => WlfiConfig;
185
+ deleteConfigKey?: (key: keyof WlfiConfig) => WlfiConfig;
186
+ assertTrustedDaemonSocketPath?: (targetPath: string, label?: string) => string;
187
+ }
188
+
189
+ interface CreateWalletSetupPlanDeps extends AdminAccessGuardDeps {
190
+ readConfig?: () => WlfiConfig;
191
+ assertTrustedDaemonSocketPath?: (targetPath: string, label?: string) => string;
192
+ }
193
+
194
+ function presentString(value: string | undefined): string | undefined {
195
+ const normalized = value?.trim();
196
+ return normalized ? normalized : undefined;
197
+ }
198
+
199
+ function renderError(error: unknown): string {
200
+ return error instanceof Error ? error.message : String(error);
201
+ }
202
+
203
+ function tryChmod(targetPath: string, mode: number): void {
204
+ try {
205
+ fs.chmodSync(targetPath, mode);
206
+ } catch {}
207
+ }
208
+
209
+ function assertWritableDirectoryAccess(targetPath: string, label: string): void {
210
+ try {
211
+ fs.accessSync(targetPath, fs.constants.W_OK | fs.constants.X_OK);
212
+ } catch {
213
+ throw new Error(`${label} '${targetPath}' must be writable by the current process`);
214
+ }
215
+ }
216
+
217
+ function assertWritableFileAccess(targetPath: string, label: string): void {
218
+ try {
219
+ fs.accessSync(targetPath, fs.constants.W_OK);
220
+ } catch {
221
+ throw new Error(`${label} '${targetPath}' must be writable by the current process`);
222
+ }
223
+ }
224
+
225
+ function readLstat(targetPath: string): fs.Stats | null {
226
+ try {
227
+ return fs.lstatSync(targetPath);
228
+ } catch (error) {
229
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
230
+ return null;
231
+ }
232
+ throw error;
233
+ }
234
+ }
235
+
236
+ function assertNoSymlinkAncestorDirectories(targetPath: string, label: string): void {
237
+ const resolvedPath = path.resolve(targetPath);
238
+ const parentPath = path.dirname(resolvedPath);
239
+ if (parentPath === resolvedPath) {
240
+ return;
241
+ }
242
+
243
+ const { root } = path.parse(parentPath);
244
+ const relativeParent = parentPath.slice(root.length);
245
+ if (!relativeParent) {
246
+ return;
247
+ }
248
+
249
+ let currentPath = root;
250
+ for (const segment of relativeParent.split(path.sep).filter(Boolean)) {
251
+ currentPath = path.join(currentPath, segment);
252
+ const stats = readLstat(currentPath);
253
+ if (!stats) {
254
+ break;
255
+ }
256
+ if (stats.isSymbolicLink()) {
257
+ if (isStableRootOwnedSymlink(stats, currentPath)) {
258
+ continue;
259
+ }
260
+ throw new Error(
261
+ `${label} '${resolvedPath}' must not traverse symlinked ancestor directories`,
262
+ );
263
+ }
264
+ }
265
+ }
266
+
267
+ function isStableRootOwnedSymlink(stats: fs.Stats, targetPath: string): boolean {
268
+ if (process.platform === 'win32' || typeof stats.uid !== 'number' || stats.uid !== 0) {
269
+ return false;
270
+ }
271
+
272
+ const parentPath = path.dirname(targetPath);
273
+ const parentStats = readLstat(parentPath);
274
+ return Boolean(
275
+ parentStats?.isDirectory() &&
276
+ typeof parentStats.uid === 'number' &&
277
+ parentStats.uid === 0 &&
278
+ (parentStats.mode & GROUP_OTHER_WRITE_MODE_MASK) === 0,
279
+ );
280
+ }
281
+
282
+ function currentEffectiveUid(): number | null {
283
+ if (typeof process.geteuid === 'function') {
284
+ return process.geteuid();
285
+ }
286
+ if (typeof process.getuid === 'function') {
287
+ return process.getuid();
288
+ }
289
+ return null;
290
+ }
291
+
292
+ function canCurrentProcessTightenDirectoryMode(stats: fs.Stats): boolean {
293
+ if (process.platform === 'win32' || typeof stats.uid !== 'number') {
294
+ return true;
295
+ }
296
+
297
+ const effectiveUid = currentEffectiveUid();
298
+ return effectiveUid === 0 || effectiveUid === stats.uid;
299
+ }
300
+
301
+ function isStickyDirectory(stats: fs.Stats): boolean {
302
+ return process.platform !== 'win32' && (stats.mode & STICKY_BIT_MODE) !== 0;
303
+ }
304
+
305
+ function assertSecureExistingPreviewDirectory(
306
+ targetPath: string,
307
+ stats: fs.Stats,
308
+ label: string,
309
+ ): void {
310
+ if (stats.isSymbolicLink()) {
311
+ throw new Error(`${label} '${targetPath}' must not be a symlink`);
312
+ }
313
+ if (!stats.isDirectory()) {
314
+ throw new Error(`${label} '${targetPath}' must be a directory`);
315
+ }
316
+
317
+ assertTrustedOwner(stats, targetPath, label);
318
+ if (
319
+ process.platform !== 'win32' &&
320
+ (stats.mode & GROUP_OTHER_WRITE_MODE_MASK) !== 0 &&
321
+ !canCurrentProcessTightenDirectoryMode(stats)
322
+ ) {
323
+ throw new Error(`${label} '${targetPath}' must not be writable by group/other`);
324
+ }
325
+
326
+ assertWritableDirectoryAccess(targetPath, label);
327
+ }
328
+
329
+ function assertTrustedAncestorDirectories(targetPath: string, label: string): void {
330
+ let currentPath = fs.realpathSync.native(path.resolve(targetPath));
331
+
332
+ while (true) {
333
+ const parentPath = path.dirname(currentPath);
334
+ if (parentPath === currentPath) {
335
+ break;
336
+ }
337
+
338
+ const stats = fs.lstatSync(parentPath);
339
+ if (!stats.isDirectory()) {
340
+ throw new Error(`${label} '${parentPath}' must be a directory`);
341
+ }
342
+
343
+ assertTrustedOwner(stats, parentPath, label);
344
+ if (process.platform !== 'win32' && (stats.mode & GROUP_OTHER_WRITE_MODE_MASK) !== 0) {
345
+ if (!isStickyDirectory(stats)) {
346
+ throw new Error(`${label} '${parentPath}' must not be writable by group/other`);
347
+ }
348
+ }
349
+
350
+ currentPath = parentPath;
351
+ }
352
+ }
353
+
354
+ function findNearestExistingPath(targetPath: string): string {
355
+ let currentPath = path.resolve(targetPath);
356
+
357
+ while (true) {
358
+ if (readLstat(currentPath)) {
359
+ return currentPath;
360
+ }
361
+
362
+ const parentPath = path.dirname(currentPath);
363
+ if (parentPath === currentPath) {
364
+ return currentPath;
365
+ }
366
+
367
+ currentPath = parentPath;
368
+ }
369
+ }
370
+
371
+ function assertPreviewBootstrapOutputDirectory(targetPath: string, label: string): void {
372
+ const resolvedPath = path.resolve(targetPath);
373
+ assertNoSymlinkAncestorDirectories(resolvedPath, label);
374
+ const existingPath = findNearestExistingPath(resolvedPath);
375
+ const stats = fs.lstatSync(existingPath);
376
+
377
+ assertSecureExistingPreviewDirectory(existingPath, stats, label);
378
+ assertTrustedAncestorDirectories(existingPath, label);
379
+ }
380
+
381
+ function resolveBootstrapOutputPreflight(targetPath: string): {
382
+ bootstrapOutputReady: boolean;
383
+ bootstrapOutputError: string | null;
384
+ } {
385
+ try {
386
+ assertPreviewBootstrapOutputDirectory(path.dirname(targetPath), 'bootstrap output directory');
387
+ assertSecureBootstrapOutputTarget(targetPath);
388
+ return {
389
+ bootstrapOutputReady: true,
390
+ bootstrapOutputError: null,
391
+ };
392
+ } catch (error) {
393
+ return {
394
+ bootstrapOutputReady: false,
395
+ bootstrapOutputError: renderError(error),
396
+ };
397
+ }
398
+ }
399
+
400
+ function ensurePrivateOutputDirectory(targetPath: string, label: string): string {
401
+ const resolvedPath = path.resolve(targetPath);
402
+ assertNoSymlinkAncestorDirectories(resolvedPath, label);
403
+ const stats = readLstat(resolvedPath);
404
+ if (stats?.isSymbolicLink()) {
405
+ throw new Error(`${label} '${resolvedPath}' must not be a symlink`);
406
+ }
407
+
408
+ if (stats) {
409
+ if (!stats.isDirectory()) {
410
+ throw new Error(`${label} '${resolvedPath}' must be a directory`);
411
+ }
412
+ tryChmod(resolvedPath, PRIVATE_DIR_MODE);
413
+ assertTrustedDirectoryPath(resolvedPath, label);
414
+ assertWritableDirectoryAccess(resolvedPath, label);
415
+ return resolvedPath;
416
+ }
417
+
418
+ const parentPath = path.dirname(resolvedPath);
419
+ if (parentPath !== resolvedPath) {
420
+ ensurePrivateOutputDirectory(parentPath, `${label} parent`);
421
+ }
422
+
423
+ fs.mkdirSync(resolvedPath, { mode: PRIVATE_DIR_MODE });
424
+ tryChmod(resolvedPath, PRIVATE_DIR_MODE);
425
+ assertTrustedDirectoryPath(resolvedPath, label);
426
+ assertWritableDirectoryAccess(resolvedPath, label);
427
+ return resolvedPath;
428
+ }
429
+
430
+ function assertSecureBootstrapOutputTarget(targetPath: string): void {
431
+ const resolvedPath = path.resolve(targetPath);
432
+ const stats = readLstat(resolvedPath);
433
+ if (!stats) {
434
+ return;
435
+ }
436
+ if (stats.isSymbolicLink()) {
437
+ throw new Error(`bootstrap output file '${resolvedPath}' must not be a symlink`);
438
+ }
439
+ if (!stats.isFile()) {
440
+ throw new Error(`bootstrap output file '${resolvedPath}' must be a regular file`);
441
+ }
442
+
443
+ assertPrivateFileStats(stats, resolvedPath, 'bootstrap output file');
444
+ assertWritableFileAccess(resolvedPath, 'bootstrap output file');
445
+ }
446
+
447
+ function configuredChainName(chainId: number, currentConfig: WlfiConfig): string | null {
448
+ for (const profile of Object.values(currentConfig.chains ?? {})) {
449
+ if (profile.chainId === chainId && profile.name.trim().length > 0) {
450
+ return profile.name.trim();
451
+ }
452
+ }
453
+
454
+ const builtin = listBuiltinChains().find((profile) => profile.chainId === chainId);
455
+ return builtin?.name ?? null;
456
+ }
457
+
458
+ function configuredRpcUrl(chainId: number, currentConfig: WlfiConfig): string | undefined {
459
+ if (currentConfig.chainId === chainId) {
460
+ const currentRpcUrl = presentString(currentConfig.rpcUrl);
461
+ if (currentRpcUrl) {
462
+ return currentRpcUrl;
463
+ }
464
+ }
465
+
466
+ for (const profile of Object.values(currentConfig.chains ?? {})) {
467
+ if (profile.chainId === chainId) {
468
+ const candidate = presentString(profile.rpcUrl);
469
+ if (candidate) {
470
+ return candidate;
471
+ }
472
+ }
473
+ }
474
+
475
+ return undefined;
476
+ }
477
+
478
+ function resolvePlannedRpcUrl(
479
+ chainId: number | null,
480
+ rpcUrl: string | undefined,
481
+ currentConfig: WlfiConfig,
482
+ ): string | null {
483
+ if (chainId === null) {
484
+ return null;
485
+ }
486
+
487
+ return presentString(rpcUrl) ?? configuredRpcUrl(chainId, currentConfig) ?? null;
488
+ }
489
+
490
+ function resolveWalletSetupPreflight(
491
+ daemonSocket: string,
492
+ rpcUrl: string | null,
493
+ bootstrapOutputPath: string,
494
+ trustDaemonSocketPath: (targetPath: string, label?: string) => string,
495
+ ): WalletSetupPlan['preflight'] {
496
+ let daemonSocketTrusted = true;
497
+ let daemonSocketError: string | null = null;
498
+ try {
499
+ trustDaemonSocketPath(daemonSocket);
500
+ } catch (error) {
501
+ daemonSocketTrusted = false;
502
+ daemonSocketError = renderError(error);
503
+ }
504
+
505
+ const bootstrapOutput = resolveBootstrapOutputPreflight(bootstrapOutputPath);
506
+
507
+ if (rpcUrl === null) {
508
+ return {
509
+ daemonSocketTrusted,
510
+ daemonSocketError,
511
+ rpcUrlTrusted: null,
512
+ rpcUrlError: null,
513
+ ...bootstrapOutput,
514
+ };
515
+ }
516
+
517
+ try {
518
+ assertSafeRpcUrl(rpcUrl, 'rpcUrl');
519
+ return {
520
+ daemonSocketTrusted,
521
+ daemonSocketError,
522
+ rpcUrlTrusted: true,
523
+ rpcUrlError: null,
524
+ ...bootstrapOutput,
525
+ };
526
+ } catch (error) {
527
+ return {
528
+ daemonSocketTrusted,
529
+ daemonSocketError,
530
+ rpcUrlTrusted: false,
531
+ rpcUrlError: renderError(error),
532
+ ...bootstrapOutput,
533
+ };
534
+ }
535
+ }
536
+
537
+ export function assertWalletSetupExecutionPreconditions(
538
+ options: Pick<CompleteWalletSetupOptions, 'daemonSocket' | 'network' | 'rpcUrl'>,
539
+ currentConfig: WlfiConfig,
540
+ deps: Pick<CompleteWalletSetupDeps, 'assertTrustedDaemonSocketPath'> = {},
541
+ ): void {
542
+ const trustDaemonSocketPath =
543
+ deps.assertTrustedDaemonSocketPath ?? assertTrustedAdminDaemonSocketPath;
544
+
545
+ const daemonSocket =
546
+ presentString(options.daemonSocket) ?? presentString(currentConfig.daemonSocket);
547
+ if (options.network === undefined) {
548
+ if (daemonSocket) {
549
+ trustDaemonSocketPath(daemonSocket);
550
+ }
551
+ return;
552
+ }
553
+
554
+ const plannedRpcUrl = resolvePlannedRpcUrl(
555
+ assertPositiveChainId(options.network),
556
+ presentString(options.rpcUrl),
557
+ currentConfig,
558
+ );
559
+ if (plannedRpcUrl !== null) {
560
+ assertSafeRpcUrl(plannedRpcUrl, 'rpcUrl');
561
+ }
562
+
563
+ if (daemonSocket) {
564
+ trustDaemonSocketPath(daemonSocket);
565
+ }
566
+ }
567
+
568
+ function assertPositiveChainId(value: number): number {
569
+ if (!Number.isSafeInteger(value) || value <= 0) {
570
+ throw new Error('network must be a positive safe integer');
571
+ }
572
+
573
+ return value;
574
+ }
575
+
576
+ function parsePositiveChainIdString(value: string): number {
577
+ const normalized = value.trim();
578
+ if (!/^[1-9][0-9]*$/u.test(normalized)) {
579
+ throw new Error('network must be a positive safe integer');
580
+ }
581
+
582
+ return assertPositiveChainId(Number(normalized));
583
+ }
584
+
585
+ function normalizedList(values: string[] | undefined): string[] {
586
+ return (values ?? []).map((value) => value.trim()).filter((value) => value.length > 0);
587
+ }
588
+
589
+ function dedupeCanonicalList(values: string[]): string[] {
590
+ const deduped: string[] = [];
591
+ const seen = new Set<string>();
592
+
593
+ for (const value of values) {
594
+ const canonical = value.toLowerCase();
595
+ if (seen.has(canonical)) {
596
+ continue;
597
+ }
598
+
599
+ seen.add(canonical);
600
+ deduped.push(canonical);
601
+ }
602
+
603
+ return deduped;
604
+ }
605
+
606
+ function assertWalletSetupAddress(value: string, label: string): string {
607
+ const normalized = value.trim();
608
+ if (!isAddress(normalized)) {
609
+ throw new Error(`${label} must be a valid EVM address`);
610
+ }
611
+ return normalized;
612
+ }
613
+
614
+ function assertWalletSetupUuid(value: string, label: string): string {
615
+ const normalized = value.trim();
616
+ if (!UUID_PATTERN.test(normalized)) {
617
+ throw new Error(`${label} must be a valid UUID`);
618
+ }
619
+ return normalized;
620
+ }
621
+
622
+ function resolveValidatedWalletSetupRecipient(value: string | undefined): string | undefined {
623
+ const normalized = presentString(value);
624
+ return normalized ? assertWalletSetupAddress(normalized, 'recipient') : undefined;
625
+ }
626
+
627
+ function resolveValidatedWalletSetupTokenList(values: string[] | undefined): string[] {
628
+ return dedupeCanonicalList(
629
+ normalizedList(values).map((value) => assertWalletSetupAddress(value, 'token')),
630
+ );
631
+ }
632
+
633
+ function resolveValidatedWalletSetupPolicyIds(values: string[] | undefined): string[] {
634
+ return dedupeCanonicalList(
635
+ normalizedList(values).map((value) => assertWalletSetupUuid(value, 'attachPolicyId')),
636
+ );
637
+ }
638
+
639
+ function previewBootstrapOutputPath(inputPath?: string): WalletSetupBootstrapOutput {
640
+ const explicitPath = presentString(inputPath);
641
+ if (explicitPath) {
642
+ return {
643
+ path: path.resolve(explicitPath),
644
+ autoGenerated: false,
645
+ };
646
+ }
647
+
648
+ return {
649
+ path: path.join(resolveWlfiHome(), AUTO_GENERATED_BOOTSTRAP_FILENAME),
650
+ autoGenerated: true,
651
+ };
652
+ }
653
+
654
+ function resolvePreviewDaemonSocket(input: string | undefined, currentConfig: WlfiConfig): string {
655
+ return path.resolve(
656
+ presentString(input) ?? presentString(currentConfig.daemonSocket) ?? defaultDaemonSocketPath(),
657
+ );
658
+ }
659
+
660
+ function resolveOptionalLimit(value: string | undefined): string | null {
661
+ return presentString(value) ?? null;
662
+ }
663
+
664
+ function resolveRequiredLimit(value: string | undefined): string {
665
+ return presentString(value) ?? RUST_DEFAULT_VALUE;
666
+ }
667
+
668
+ function resolveAssetMode(
669
+ tokens: string[],
670
+ allowNativeEth: boolean,
671
+ ): WalletSetupPlan['policyScope']['assets']['mode'] {
672
+ if (tokens.length > 0 && allowNativeEth) {
673
+ return 'mixed';
674
+ }
675
+ if (tokens.length > 0) {
676
+ return 'erc20-only';
677
+ }
678
+ if (allowNativeEth) {
679
+ return 'native-only';
680
+ }
681
+ return 'daemon-default';
682
+ }
683
+
684
+ function resolveAttachmentMode(
685
+ _attachBootstrapPolicies: boolean,
686
+ explicitPolicyIds: string[],
687
+ ): WalletSetupPlan['policyAttachment']['mode'] {
688
+ if (explicitPolicyIds.length > 0) {
689
+ return 'bootstrap-and-explicit';
690
+ }
691
+ return 'bootstrap-only';
692
+ }
693
+
694
+ function shouldBootstrapFromSharedConfig(
695
+ input: Pick<
696
+ WalletSetupAdminArgsInput,
697
+ | 'fromSharedConfig'
698
+ | 'perTxMaxWei'
699
+ | 'dailyMaxWei'
700
+ | 'weeklyMaxWei'
701
+ | 'maxGasPerChainWei'
702
+ | 'dailyMaxTxCount'
703
+ | 'perTxMaxFeePerGasWei'
704
+ | 'perTxMaxPriorityFeePerGasWei'
705
+ | 'perTxMaxCalldataBytes'
706
+ | 'token'
707
+ | 'allowNativeEth'
708
+ | 'recipient'
709
+ >,
710
+ ): boolean {
711
+ if (typeof input.fromSharedConfig === 'boolean') {
712
+ return input.fromSharedConfig;
713
+ }
714
+
715
+ const hasExplicitLegacyPolicyScope = Boolean(
716
+ presentString(input.perTxMaxWei) ||
717
+ presentString(input.dailyMaxWei) ||
718
+ presentString(input.weeklyMaxWei) ||
719
+ presentString(input.maxGasPerChainWei) ||
720
+ presentString(input.dailyMaxTxCount) ||
721
+ presentString(input.perTxMaxFeePerGasWei) ||
722
+ presentString(input.perTxMaxPriorityFeePerGasWei) ||
723
+ presentString(input.perTxMaxCalldataBytes) ||
724
+ resolveValidatedWalletSetupTokenList(input.token).length > 0 ||
725
+ input.allowNativeEth ||
726
+ resolveValidatedWalletSetupRecipient(input.recipient),
727
+ );
728
+
729
+ return !hasExplicitLegacyPolicyScope;
730
+ }
731
+
732
+ function normalizePositiveIntegerString(value: string | undefined, label: string): string | null {
733
+ const normalized = presentString(value);
734
+ if (!normalized) {
735
+ return null;
736
+ }
737
+ if (!/^[1-9][0-9]*$/u.test(normalized)) {
738
+ throw new Error(`${label} must be a positive integer string`);
739
+ }
740
+ return normalized;
741
+ }
742
+
743
+ function normalizeOptionalPositiveIntegerString(
744
+ value: string | undefined,
745
+ label: string,
746
+ ): string | null {
747
+ const normalized = presentString(value);
748
+ if (!normalized) {
749
+ return null;
750
+ }
751
+ if (!/^(0|[1-9][0-9]*)$/u.test(normalized)) {
752
+ throw new Error(`${label} must be a non-negative integer string`);
753
+ }
754
+ return normalized === '0' ? null : normalized;
755
+ }
756
+
757
+ function expectedWalletSetupNetworkScope(network: number | undefined): string {
758
+ return network === undefined ? 'all networks' : String(assertPositiveChainId(network));
759
+ }
760
+
761
+ function expectedWalletSetupAssetScope(
762
+ tokens: string[] | undefined,
763
+ allowNativeEth: boolean,
764
+ ): string {
765
+ const normalizedTokens = resolveValidatedWalletSetupTokenList(tokens)
766
+ .map((token) => token.toLowerCase())
767
+ .sort((left, right) => left.localeCompare(right));
768
+
769
+ if (normalizedTokens.length === 0 && !allowNativeEth) {
770
+ return 'all assets';
771
+ }
772
+
773
+ const assets = [
774
+ ...(allowNativeEth ? ['native_eth'] : []),
775
+ ...normalizedTokens.map((token) => `erc20:${token}`),
776
+ ];
777
+ return assets.join(',');
778
+ }
779
+
780
+ function expectedWalletSetupRecipientScope(recipient: string | undefined): string {
781
+ const normalizedRecipient = resolveValidatedWalletSetupRecipient(recipient);
782
+ return normalizedRecipient ? normalizedRecipient.toLowerCase() : 'all recipients';
783
+ }
784
+
785
+ function createdBootstrapPolicyIds(summary: BootstrapSetupSummary): string[] {
786
+ const ids = [
787
+ summary.perTxPolicyId,
788
+ summary.dailyPolicyId,
789
+ summary.weeklyPolicyId,
790
+ summary.gasPolicyId,
791
+ summary.dailyTxCountPolicyId,
792
+ summary.perTxMaxFeePerGasPolicyId,
793
+ summary.perTxMaxPriorityFeePerGasPolicyId,
794
+ summary.perTxMaxCalldataBytesPolicyId,
795
+ ].filter((value): value is string => typeof value === 'string' && value.length > 0);
796
+
797
+ for (const destinationOverride of summary.destinationOverrides ?? []) {
798
+ ids.push(
799
+ destinationOverride.perTxPolicyId,
800
+ destinationOverride.dailyPolicyId,
801
+ destinationOverride.weeklyPolicyId,
802
+ );
803
+ if (destinationOverride.gasPolicyId) {
804
+ ids.push(destinationOverride.gasPolicyId);
805
+ }
806
+ if (destinationOverride.dailyTxCountPolicyId) {
807
+ ids.push(destinationOverride.dailyTxCountPolicyId);
808
+ }
809
+ if (destinationOverride.perTxMaxFeePerGasPolicyId) {
810
+ ids.push(destinationOverride.perTxMaxFeePerGasPolicyId);
811
+ }
812
+ if (destinationOverride.perTxMaxPriorityFeePerGasPolicyId) {
813
+ ids.push(destinationOverride.perTxMaxPriorityFeePerGasPolicyId);
814
+ }
815
+ if (destinationOverride.perTxMaxCalldataBytesPolicyId) {
816
+ ids.push(destinationOverride.perTxMaxCalldataBytesPolicyId);
817
+ }
818
+ }
819
+
820
+ for (const tokenPolicy of summary.tokenPolicies ?? []) {
821
+ ids.push(tokenPolicy.perTxPolicyId, tokenPolicy.dailyPolicyId, tokenPolicy.weeklyPolicyId);
822
+ if (tokenPolicy.gasPolicyId) {
823
+ ids.push(tokenPolicy.gasPolicyId);
824
+ }
825
+ if (tokenPolicy.dailyTxCountPolicyId) {
826
+ ids.push(tokenPolicy.dailyTxCountPolicyId);
827
+ }
828
+ if (tokenPolicy.perTxMaxFeePerGasPolicyId) {
829
+ ids.push(tokenPolicy.perTxMaxFeePerGasPolicyId);
830
+ }
831
+ if (tokenPolicy.perTxMaxPriorityFeePerGasPolicyId) {
832
+ ids.push(tokenPolicy.perTxMaxPriorityFeePerGasPolicyId);
833
+ }
834
+ if (tokenPolicy.perTxMaxCalldataBytesPolicyId) {
835
+ ids.push(tokenPolicy.perTxMaxCalldataBytesPolicyId);
836
+ }
837
+ }
838
+
839
+ for (const destinationOverride of summary.tokenDestinationOverrides ?? []) {
840
+ ids.push(
841
+ destinationOverride.perTxPolicyId,
842
+ destinationOverride.dailyPolicyId,
843
+ destinationOverride.weeklyPolicyId,
844
+ );
845
+ if (destinationOverride.gasPolicyId) {
846
+ ids.push(destinationOverride.gasPolicyId);
847
+ }
848
+ if (destinationOverride.dailyTxCountPolicyId) {
849
+ ids.push(destinationOverride.dailyTxCountPolicyId);
850
+ }
851
+ if (destinationOverride.perTxMaxFeePerGasPolicyId) {
852
+ ids.push(destinationOverride.perTxMaxFeePerGasPolicyId);
853
+ }
854
+ if (destinationOverride.perTxMaxPriorityFeePerGasPolicyId) {
855
+ ids.push(destinationOverride.perTxMaxPriorityFeePerGasPolicyId);
856
+ }
857
+ if (destinationOverride.perTxMaxCalldataBytesPolicyId) {
858
+ ids.push(destinationOverride.perTxMaxCalldataBytesPolicyId);
859
+ }
860
+ }
861
+
862
+ for (const manualApproval of summary.tokenManualApprovalPolicies ?? []) {
863
+ ids.push(manualApproval.policyId);
864
+ }
865
+
866
+ return ids;
867
+ }
868
+
869
+ function normalizeStringSet(values: string[]): Set<string> {
870
+ return new Set(values.map((value) => value.toLowerCase()));
871
+ }
872
+
873
+ function assertRequestedRequiredLimit(
874
+ requestedValue: string | undefined,
875
+ summaryValue: string | null,
876
+ label: string,
877
+ ): void {
878
+ const normalizedRequested = normalizePositiveIntegerString(requestedValue, label);
879
+ if (normalizedRequested !== null && normalizedRequested !== summaryValue) {
880
+ throw new Error(`bootstrap summary ${label} does not match the requested wallet setup limit`);
881
+ }
882
+ }
883
+
884
+ function assertRequestedOptionalLimit(
885
+ requestedValue: string | undefined,
886
+ summaryValue: string | null,
887
+ summaryPolicyId: string | null,
888
+ label: string,
889
+ ): void {
890
+ const normalizedRequested = normalizeOptionalPositiveIntegerString(requestedValue, label);
891
+
892
+ if (normalizedRequested === null) {
893
+ if (summaryValue !== null || summaryPolicyId !== null) {
894
+ throw new Error(`bootstrap summary ${label} was enabled unexpectedly`);
895
+ }
896
+ return;
897
+ }
898
+
899
+ if (summaryValue !== normalizedRequested) {
900
+ throw new Error(`bootstrap summary ${label} does not match the requested wallet setup limit`);
901
+ }
902
+ if (summaryPolicyId === null) {
903
+ throw new Error(`bootstrap summary ${label} is missing its policy id`);
904
+ }
905
+ }
906
+
907
+ function assertWalletSetupSummaryMatchesRequest(
908
+ summary: BootstrapSetupSummary,
909
+ options: CompleteWalletSetupOptions,
910
+ ): void {
911
+ const usesPerTokenBootstrap =
912
+ summary.tokenPolicies.length > 0 ||
913
+ summary.tokenDestinationOverrides.length > 0 ||
914
+ summary.tokenManualApprovalPolicies.length > 0;
915
+
916
+ if (
917
+ !usesPerTokenBootstrap &&
918
+ summary.networkScope !== expectedWalletSetupNetworkScope(options.network)
919
+ ) {
920
+ throw new Error(
921
+ 'bootstrap summary network_scope does not match the requested wallet setup scope',
922
+ );
923
+ }
924
+
925
+ if (
926
+ !usesPerTokenBootstrap &&
927
+ summary.assetScope !==
928
+ expectedWalletSetupAssetScope(options.token, Boolean(options.allowNativeEth))
929
+ ) {
930
+ throw new Error(
931
+ 'bootstrap summary asset_scope does not match the requested wallet setup scope',
932
+ );
933
+ }
934
+
935
+ if (
936
+ !usesPerTokenBootstrap &&
937
+ summary.recipientScope !== expectedWalletSetupRecipientScope(options.recipient)
938
+ ) {
939
+ throw new Error(
940
+ 'bootstrap summary recipient_scope does not match the requested wallet setup scope',
941
+ );
942
+ }
943
+
944
+ if (!usesPerTokenBootstrap) {
945
+ assertRequestedRequiredLimit(options.perTxMaxWei, summary.perTxMaxWei, 'perTxMaxWei');
946
+ assertRequestedRequiredLimit(options.dailyMaxWei, summary.dailyMaxWei, 'dailyMaxWei');
947
+ assertRequestedRequiredLimit(options.weeklyMaxWei, summary.weeklyMaxWei, 'weeklyMaxWei');
948
+ assertRequestedRequiredLimit(
949
+ options.maxGasPerChainWei,
950
+ summary.maxGasPerChainWei,
951
+ 'maxGasPerChainWei',
952
+ );
953
+
954
+ assertRequestedOptionalLimit(
955
+ options.dailyMaxTxCount,
956
+ summary.dailyMaxTxCount,
957
+ summary.dailyTxCountPolicyId,
958
+ 'dailyMaxTxCount',
959
+ );
960
+ assertRequestedOptionalLimit(
961
+ options.perTxMaxFeePerGasWei,
962
+ summary.perTxMaxFeePerGasWei,
963
+ summary.perTxMaxFeePerGasPolicyId,
964
+ 'perTxMaxFeePerGasWei',
965
+ );
966
+ assertRequestedOptionalLimit(
967
+ options.perTxMaxPriorityFeePerGasWei,
968
+ summary.perTxMaxPriorityFeePerGasWei,
969
+ summary.perTxMaxPriorityFeePerGasPolicyId,
970
+ 'perTxMaxPriorityFeePerGasWei',
971
+ );
972
+ assertRequestedOptionalLimit(
973
+ options.perTxMaxCalldataBytes,
974
+ summary.perTxMaxCalldataBytes,
975
+ summary.perTxMaxCalldataBytesPolicyId,
976
+ 'perTxMaxCalldataBytes',
977
+ );
978
+ }
979
+
980
+ const explicitPolicyIds = normalizeStringSet(
981
+ resolveValidatedWalletSetupPolicyIds(options.attachPolicyId),
982
+ );
983
+ const expectedAttachment = 'policy_set';
984
+ if (summary.policyAttachment !== expectedAttachment) {
985
+ throw new Error(
986
+ 'bootstrap summary policy_attachment does not match the requested wallet setup attachment',
987
+ );
988
+ }
989
+
990
+ const actualAttachedPolicyIds = normalizeStringSet(summary.attachedPolicyIds);
991
+ const expectedAttachedPolicyIds = new Set(
992
+ createdBootstrapPolicyIds(summary).map((policyId) => policyId.toLowerCase()),
993
+ );
994
+ for (const policyId of explicitPolicyIds) {
995
+ expectedAttachedPolicyIds.add(policyId);
996
+ }
997
+
998
+ if (actualAttachedPolicyIds.size !== expectedAttachedPolicyIds.size) {
999
+ throw new Error(
1000
+ 'bootstrap summary attached_policy_ids do not match the requested wallet setup attachment',
1001
+ );
1002
+ }
1003
+ for (const policyId of expectedAttachedPolicyIds) {
1004
+ if (!actualAttachedPolicyIds.has(policyId)) {
1005
+ throw new Error(
1006
+ 'bootstrap summary attached_policy_ids do not match the requested wallet setup attachment',
1007
+ );
1008
+ }
1009
+ }
1010
+ }
1011
+
1012
+ function previewWalletSetupRustArgs(
1013
+ input: Omit<WalletSetupAdminArgsInput, 'bootstrapOutputPath'> & { bootstrapOutputPath?: string },
1014
+ _adminAccessMode: AdminAccessMode,
1015
+ bootstrapOutputPath: string,
1016
+ ): string[] {
1017
+ return buildWalletSetupAdminArgs({
1018
+ ...input,
1019
+ bootstrapOutputPath,
1020
+ });
1021
+ }
1022
+
1023
+ export function createWalletSetupPlan(
1024
+ input: WalletSetupPlanOptions,
1025
+ deps: CreateWalletSetupPlanDeps = {},
1026
+ ): WalletSetupPlan {
1027
+ const loadConfig = deps.readConfig ?? readConfig;
1028
+ const trustDaemonSocketPath =
1029
+ deps.assertTrustedDaemonSocketPath ?? assertTrustedAdminDaemonSocketPath;
1030
+ const currentConfig = loadConfig();
1031
+ const network = presentString(input.network);
1032
+ const rpcUrl = presentString(input.rpcUrl);
1033
+ const chainName = presentString(input.chainName);
1034
+
1035
+ if (rpcUrl && !network) {
1036
+ throw new Error('--rpc-url requires --network');
1037
+ }
1038
+ if (chainName && !network) {
1039
+ throw new Error('--chain-name requires --network');
1040
+ }
1041
+
1042
+ const chainId = network ? parsePositiveChainIdString(network) : null;
1043
+ const resolvedRpcUrl = resolvePlannedRpcUrl(chainId, rpcUrl, currentConfig);
1044
+ const resolvedChainName =
1045
+ chainId === null
1046
+ ? null
1047
+ : chainName
1048
+ ? chainName
1049
+ : (configuredChainName(chainId, currentConfig) ?? `chain-${chainId}`);
1050
+
1051
+ const bootstrapOutput = previewBootstrapOutputPath(input.bootstrapOutputPath);
1052
+ const cleanupAction = resolveWalletSetupCleanupAction(
1053
+ bootstrapOutput.autoGenerated,
1054
+ Boolean(input.deleteBootstrapOutput),
1055
+ );
1056
+ const explicitPolicyIds = resolveValidatedWalletSetupPolicyIds(input.attachPolicyId);
1057
+ const erc20Tokens = resolveValidatedWalletSetupTokenList(input.token);
1058
+ const recipient = resolveValidatedWalletSetupRecipient(input.recipient) ?? null;
1059
+ const daemonSocket = resolvePreviewDaemonSocket(input.daemonSocket, currentConfig);
1060
+ const preflight = resolveWalletSetupPreflight(
1061
+ daemonSocket,
1062
+ resolvedRpcUrl,
1063
+ bootstrapOutput.path,
1064
+ trustDaemonSocketPath,
1065
+ );
1066
+ const rustArgs = previewWalletSetupRustArgs(input, 'blocked', bootstrapOutput.path);
1067
+ const adminAccess = resolveAdminAccess('wlfi-agent-admin', rustArgs, deps);
1068
+
1069
+ const notes = [
1070
+ 'Rust still performs the real password check and policy creation.',
1071
+ 'TypeScript imports the emitted agent auth token into macOS Keychain after bootstrap.',
1072
+ 'TypeScript re-checks the emitted bootstrap summary against the requested setup scope before importing credentials.',
1073
+ cleanupAction === 'deleted'
1074
+ ? 'The bootstrap JSON is deleted after Keychain import.'
1075
+ : 'The bootstrap JSON is redacted after Keychain import so the plaintext token is removed.',
1076
+ ];
1077
+
1078
+ if (chainId !== null && resolvedRpcUrl === null) {
1079
+ notes.push(
1080
+ 'No rpcUrl is persisted for this network unless you pass --rpc-url or configure one already.',
1081
+ );
1082
+ } else if (preflight.rpcUrlTrusted === false) {
1083
+ notes.push(
1084
+ 'The rpcUrl that would be persisted for this setup is not trusted and wallet setup would fail until it is fixed.',
1085
+ );
1086
+ }
1087
+ if (adminAccess.mode === 'vault-password-stdin') {
1088
+ notes.push(
1089
+ 'The vault password is passed to Rust through stdin and scrubbed from child argv/env.',
1090
+ );
1091
+ } else if (adminAccess.mode === 'interactive-prompt') {
1092
+ notes.push(
1093
+ 'A local tty is expected so a human can enter the vault password securely during bootstrap.',
1094
+ );
1095
+ } else if (!adminAccess.permitted) {
1096
+ notes.push(
1097
+ 'This environment would not be allowed to run wallet setup until a vault password source is provided or a local tty prompt is available.',
1098
+ );
1099
+ }
1100
+ if (!preflight.daemonSocketTrusted) {
1101
+ notes.push(
1102
+ 'The daemon socket is not currently trusted or reachable, so wallet setup would fail until it is fixed.',
1103
+ );
1104
+ }
1105
+ if (!preflight.bootstrapOutputReady) {
1106
+ notes.push(
1107
+ 'The bootstrap output path is not currently safe or writable, so wallet setup would fail before bootstrap starts.',
1108
+ );
1109
+ }
1110
+
1111
+ const rustPasswordTransport =
1112
+ adminAccess.mode === 'interactive-prompt'
1113
+ ? 'interactive-prompt'
1114
+ : adminAccess.mode === 'blocked'
1115
+ ? 'not-available'
1116
+ : 'stdin-relay';
1117
+
1118
+ return {
1119
+ adminAccess: {
1120
+ permitted: adminAccess.permitted,
1121
+ mode: adminAccess.mode,
1122
+ reason: adminAccess.reason,
1123
+ nonInteractive: adminAccess.nonInteractive,
1124
+ },
1125
+ rustCommand: {
1126
+ binary: 'wlfi-agent-admin',
1127
+ args: previewWalletSetupRustArgs(input, adminAccess.mode, bootstrapOutput.path),
1128
+ },
1129
+ bootstrapOutput: {
1130
+ path: bootstrapOutput.path,
1131
+ autoGenerated: bootstrapOutput.autoGenerated,
1132
+ cleanupAction,
1133
+ },
1134
+ daemonSocket,
1135
+ policyLimits: {
1136
+ perTxMaxWei: resolveRequiredLimit(input.perTxMaxWei),
1137
+ dailyMaxWei: resolveRequiredLimit(input.dailyMaxWei),
1138
+ weeklyMaxWei: resolveRequiredLimit(input.weeklyMaxWei),
1139
+ maxGasPerChainWei: resolveRequiredLimit(input.maxGasPerChainWei),
1140
+ dailyMaxTxCount: resolveOptionalLimit(input.dailyMaxTxCount),
1141
+ perTxMaxFeePerGasWei: resolveOptionalLimit(input.perTxMaxFeePerGasWei),
1142
+ perTxMaxPriorityFeePerGasWei: resolveOptionalLimit(input.perTxMaxPriorityFeePerGasWei),
1143
+ perTxMaxCalldataBytes: resolveOptionalLimit(input.perTxMaxCalldataBytes),
1144
+ },
1145
+ policyScope: {
1146
+ network: chainId,
1147
+ chainName: resolvedChainName,
1148
+ recipient,
1149
+ assets: {
1150
+ mode: resolveAssetMode(erc20Tokens, Boolean(input.allowNativeEth)),
1151
+ allowNativeEth: Boolean(input.allowNativeEth),
1152
+ erc20Tokens,
1153
+ },
1154
+ },
1155
+ policyAttachment: {
1156
+ mode: resolveAttachmentMode(Boolean(input.attachBootstrapPolicies), explicitPolicyIds),
1157
+ attachBootstrapPolicies: Boolean(input.attachBootstrapPolicies),
1158
+ explicitPolicyIds,
1159
+ },
1160
+ configAfterSetup: {
1161
+ agentKeyId: '<issued during bootstrap>',
1162
+ daemonSocket,
1163
+ chainId,
1164
+ chainName: resolvedChainName,
1165
+ rpcUrl: resolvedRpcUrl,
1166
+ },
1167
+ preflight,
1168
+ security: {
1169
+ rustPasswordTransport,
1170
+ childArgvContainsVaultPassword: false,
1171
+ childEnvContainsVaultPassword: false,
1172
+ keyMaterialStorage: 'macOS Keychain',
1173
+ bootstrapCredentialCleanup: cleanupAction,
1174
+ notes,
1175
+ },
1176
+ };
1177
+ }
1178
+
1179
+ function formatOptionalPlanValue(value: string | null, fallback = 'none'): string {
1180
+ return value ?? fallback;
1181
+ }
1182
+
1183
+ function formatBooleanPlanValue(value: boolean): string {
1184
+ return value ? 'yes' : 'no';
1185
+ }
1186
+
1187
+ function formatWalletSetupScope(plan: WalletSetupPlan): string[] {
1188
+ return [
1189
+ `- Network: ${plan.policyScope.network === null ? 'daemon default' : String(plan.policyScope.network)}`,
1190
+ `- Chain Name: ${plan.policyScope.chainName ?? 'daemon default'}`,
1191
+ `- Recipient: ${plan.policyScope.recipient ?? 'all recipients'}`,
1192
+ `- Asset Mode: ${plan.policyScope.assets.mode}`,
1193
+ `- Native Transfers Allowed: ${formatBooleanPlanValue(plan.policyScope.assets.allowNativeEth)}`,
1194
+ `- ERC20 Tokens: ${
1195
+ plan.policyScope.assets.erc20Tokens.length > 0
1196
+ ? plan.policyScope.assets.erc20Tokens.join(', ')
1197
+ : 'daemon default'
1198
+ }`,
1199
+ ];
1200
+ }
1201
+
1202
+ function formatWalletSetupLimits(plan: WalletSetupPlan): string[] {
1203
+ return [
1204
+ `- Per Tx Max Wei: ${plan.policyLimits.perTxMaxWei}`,
1205
+ `- Daily Max Wei: ${plan.policyLimits.dailyMaxWei}`,
1206
+ `- Weekly Max Wei: ${plan.policyLimits.weeklyMaxWei}`,
1207
+ `- Max Gas Per Chain Wei: ${plan.policyLimits.maxGasPerChainWei}`,
1208
+ `- Daily Max Tx Count: ${formatOptionalPlanValue(plan.policyLimits.dailyMaxTxCount)}`,
1209
+ `- Per Tx Max Fee Per Gas Wei: ${formatOptionalPlanValue(plan.policyLimits.perTxMaxFeePerGasWei)}`,
1210
+ `- Per Tx Max Priority Fee Per Gas Wei: ${formatOptionalPlanValue(
1211
+ plan.policyLimits.perTxMaxPriorityFeePerGasWei,
1212
+ )}`,
1213
+ `- Per Tx Max Calldata Bytes: ${formatOptionalPlanValue(plan.policyLimits.perTxMaxCalldataBytes)}`,
1214
+ ];
1215
+ }
1216
+
1217
+ function formatWalletSetupPreflight(plan: WalletSetupPlan): string[] {
1218
+ return [
1219
+ `- Daemon Socket Trusted: ${formatBooleanPlanValue(plan.preflight.daemonSocketTrusted)}`,
1220
+ plan.preflight.daemonSocketError ? ` ${plan.preflight.daemonSocketError}` : null,
1221
+ `- RPC URL Trusted: ${
1222
+ plan.preflight.rpcUrlTrusted === null
1223
+ ? 'not applicable'
1224
+ : formatBooleanPlanValue(plan.preflight.rpcUrlTrusted)
1225
+ }`,
1226
+ plan.preflight.rpcUrlError ? ` ${plan.preflight.rpcUrlError}` : null,
1227
+ `- Bootstrap Output Ready: ${formatBooleanPlanValue(plan.preflight.bootstrapOutputReady)}`,
1228
+ plan.preflight.bootstrapOutputError ? ` ${plan.preflight.bootstrapOutputError}` : null,
1229
+ ].filter((line): line is string => Boolean(line));
1230
+ }
1231
+
1232
+ export function formatWalletSetupPlanText(plan: WalletSetupPlan): string {
1233
+ const bootstrapOutputLabel = `${plan.bootstrapOutput.path} (${plan.bootstrapOutput.autoGenerated ? 'auto-generated' : 'explicit'}, ${plan.bootstrapOutput.cleanupAction} after import)`;
1234
+
1235
+ const lines = [
1236
+ 'Wallet Setup Preview',
1237
+ `Admin Access: ${plan.adminAccess.permitted ? 'allowed' : 'blocked'} (${plan.adminAccess.mode})`,
1238
+ `Admin Access Reason: ${plan.adminAccess.reason}`,
1239
+ `Daemon Socket: ${plan.daemonSocket}`,
1240
+ `Bootstrap Output: ${bootstrapOutputLabel}`,
1241
+ `Rust Command: ${plan.rustCommand.binary} ${plan.rustCommand.args.join(' ')}`,
1242
+ '',
1243
+ 'Policy Scope',
1244
+ ...formatWalletSetupScope(plan),
1245
+ '',
1246
+ 'Policy Limits',
1247
+ ...formatWalletSetupLimits(plan),
1248
+ '',
1249
+ 'Policy Attachment',
1250
+ `- Mode: ${plan.policyAttachment.mode}`,
1251
+ `- Attach Bootstrap Policies: ${formatBooleanPlanValue(
1252
+ plan.policyAttachment.attachBootstrapPolicies,
1253
+ )}`,
1254
+ `- Explicit Policy IDs: ${
1255
+ plan.policyAttachment.explicitPolicyIds.length > 0
1256
+ ? plan.policyAttachment.explicitPolicyIds.join(', ')
1257
+ : 'none'
1258
+ }`,
1259
+ '',
1260
+ 'Config After Setup',
1261
+ `- Agent Key ID: ${plan.configAfterSetup.agentKeyId}`,
1262
+ `- Daemon Socket: ${plan.configAfterSetup.daemonSocket}`,
1263
+ `- Chain ID: ${plan.configAfterSetup.chainId === null ? 'unchanged' : String(plan.configAfterSetup.chainId)}`,
1264
+ `- Chain Name: ${plan.configAfterSetup.chainName ?? 'unchanged'}`,
1265
+ `- RPC URL: ${plan.configAfterSetup.rpcUrl ?? 'unchanged'}`,
1266
+ '',
1267
+ 'Preflight',
1268
+ ...formatWalletSetupPreflight(plan),
1269
+ '',
1270
+ 'Security',
1271
+ `- Rust Password Transport: ${plan.security.rustPasswordTransport}`,
1272
+ `- Child Argv Contains Vault Password: ${formatBooleanPlanValue(
1273
+ plan.security.childArgvContainsVaultPassword,
1274
+ )}`,
1275
+ `- Child Env Contains Vault Password: ${formatBooleanPlanValue(
1276
+ plan.security.childEnvContainsVaultPassword,
1277
+ )}`,
1278
+ `- Key Material Storage: ${plan.security.keyMaterialStorage}`,
1279
+ `- Bootstrap Cleanup: ${plan.security.bootstrapCredentialCleanup}`,
1280
+ 'Notes:',
1281
+ ...plan.security.notes.map((note) => `- ${note}`),
1282
+ ];
1283
+
1284
+ return lines.join('\n');
1285
+ }
1286
+
1287
+ export function resolveWalletSetupBootstrapOutputPath(
1288
+ inputPath?: string,
1289
+ ): WalletSetupBootstrapOutput {
1290
+ const explicitPath = presentString(inputPath);
1291
+ if (!explicitPath) {
1292
+ const wlfiHome = ensureWlfiHome();
1293
+ return {
1294
+ path: path.join(wlfiHome, `bootstrap-${process.pid}-${Date.now()}.json`),
1295
+ autoGenerated: true,
1296
+ };
1297
+ }
1298
+
1299
+ const resolvedPath = path.resolve(explicitPath);
1300
+ ensurePrivateOutputDirectory(path.dirname(resolvedPath), 'bootstrap output directory');
1301
+ assertSecureBootstrapOutputTarget(resolvedPath);
1302
+ return {
1303
+ path: resolvedPath,
1304
+ autoGenerated: false,
1305
+ };
1306
+ }
1307
+
1308
+ export function resolveWalletSetupCleanupAction(
1309
+ autoGeneratedOutput: boolean,
1310
+ deleteBootstrapOutput: boolean,
1311
+ ): BootstrapCleanupAction {
1312
+ return autoGeneratedOutput || deleteBootstrapOutput ? 'deleted' : 'redacted';
1313
+ }
1314
+
1315
+ export function buildWalletSetupAdminArgs(input: WalletSetupAdminArgsInput): string[] {
1316
+ const args = ['--json', '--quiet', '--output', input.bootstrapOutputPath];
1317
+ const recipient = resolveValidatedWalletSetupRecipient(input.recipient);
1318
+ const tokens = resolveValidatedWalletSetupTokenList(input.token);
1319
+ const policyIds = resolveValidatedWalletSetupPolicyIds(input.attachPolicyId);
1320
+ const fromSharedConfig = shouldBootstrapFromSharedConfig(input);
1321
+
1322
+ if (input.vaultPassword) {
1323
+ throw new Error(
1324
+ 'insecure vaultPassword is disabled; use vaultPasswordStdin or an interactive prompt',
1325
+ );
1326
+ }
1327
+ if (input.vaultPasswordStdin) {
1328
+ args.push('--vault-password-stdin');
1329
+ }
1330
+ if (input.nonInteractive) {
1331
+ args.push('--non-interactive');
1332
+ }
1333
+ if (input.daemonSocket) {
1334
+ args.push('--daemon-socket', input.daemonSocket);
1335
+ }
1336
+
1337
+ args.push('bootstrap', '--print-agent-auth-token');
1338
+ if (fromSharedConfig) {
1339
+ args.push('--from-shared-config');
1340
+ }
1341
+
1342
+ const appendValue = (flag: string, value: string | undefined) => {
1343
+ if (value) {
1344
+ args.push(flag, value);
1345
+ }
1346
+ };
1347
+
1348
+ if (!fromSharedConfig) {
1349
+ appendValue('--per-tx-max-wei', input.perTxMaxWei);
1350
+ appendValue('--daily-max-wei', input.dailyMaxWei);
1351
+ appendValue('--weekly-max-wei', input.weeklyMaxWei);
1352
+ appendValue('--max-gas-per-chain-wei', input.maxGasPerChainWei);
1353
+ appendValue('--daily-max-tx-count', input.dailyMaxTxCount);
1354
+ appendValue('--per-tx-max-fee-per-gas-wei', input.perTxMaxFeePerGasWei);
1355
+ appendValue('--per-tx-max-priority-fee-per-gas-wei', input.perTxMaxPriorityFeePerGasWei);
1356
+ appendValue('--per-tx-max-calldata-bytes', input.perTxMaxCalldataBytes);
1357
+ appendValue('--network', input.network);
1358
+ appendValue('--recipient', recipient);
1359
+
1360
+ for (const token of tokens) {
1361
+ appendValue('--token', token);
1362
+ }
1363
+ if (input.allowNativeEth) {
1364
+ args.push('--allow-native-eth');
1365
+ }
1366
+ }
1367
+ for (const policyId of policyIds) {
1368
+ appendValue('--attach-policy-id', policyId);
1369
+ }
1370
+ args.push('--attach-bootstrap-policies');
1371
+
1372
+ return args;
1373
+ }
1374
+
1375
+ export function completeWalletSetup(
1376
+ options: CompleteWalletSetupOptions,
1377
+ deps: CompleteWalletSetupDeps = {},
1378
+ ): CompleteWalletSetupResult {
1379
+ const platform = deps.platform ?? process.platform;
1380
+ if (platform !== 'darwin') {
1381
+ throw new Error(
1382
+ 'wallet setup requires macOS because agent auth tokens are imported into macOS Keychain',
1383
+ );
1384
+ }
1385
+
1386
+ const normalizedRpcUrl = presentString(options.rpcUrl);
1387
+ const normalizedChainName = presentString(options.chainName);
1388
+ if (normalizedRpcUrl && options.network === undefined) {
1389
+ throw new Error('--rpc-url requires --network');
1390
+ }
1391
+ if (normalizedChainName && options.network === undefined) {
1392
+ throw new Error('--chain-name requires --network');
1393
+ }
1394
+
1395
+ const storeAgentAuthToken = deps.storeAgentAuthToken ?? storeAgentAuthTokenInKeychain;
1396
+ const loadConfig = deps.readConfig ?? readConfig;
1397
+ const persistConfig = deps.writeConfig ?? writeConfig;
1398
+ const clearLegacyConfigKey = deps.deleteConfigKey ?? deleteConfigKey;
1399
+ const trustDaemonSocketPath =
1400
+ deps.assertTrustedDaemonSocketPath ?? assertTrustedAdminDaemonSocketPath;
1401
+
1402
+ const currentConfig = loadConfig();
1403
+ assertWalletSetupExecutionPreconditions(
1404
+ {
1405
+ daemonSocket: options.daemonSocket,
1406
+ network: options.network,
1407
+ rpcUrl: normalizedRpcUrl,
1408
+ },
1409
+ currentConfig,
1410
+ {
1411
+ assertTrustedDaemonSocketPath: trustDaemonSocketPath,
1412
+ },
1413
+ );
1414
+
1415
+ try {
1416
+ const { summary, credentials } = readBootstrapSetupFile(options.bootstrapOutputPath);
1417
+ assertBootstrapSetupSummaryLeaseIsActive(summary);
1418
+
1419
+ if (summary.agentKeyId !== credentials.agentKeyId) {
1420
+ throw new Error(
1421
+ 'bootstrap credentials file agent_key_id does not match setup summary agent_key_id',
1422
+ );
1423
+ }
1424
+
1425
+ assertWalletSetupSummaryMatchesRequest(summary, options);
1426
+
1427
+ const nextConfig: WlfiConfig = {
1428
+ agentKeyId: credentials.agentKeyId,
1429
+ wallet: walletProfileFromBootstrapSummary(summary),
1430
+ };
1431
+
1432
+ const daemonSocket = presentString(options.daemonSocket);
1433
+ if (daemonSocket) {
1434
+ nextConfig.daemonSocket = trustDaemonSocketPath(daemonSocket);
1435
+ }
1436
+
1437
+ if (options.network !== undefined) {
1438
+ nextConfig.chainId = assertPositiveChainId(options.network);
1439
+ nextConfig.chainName =
1440
+ normalizedChainName ??
1441
+ configuredChainName(nextConfig.chainId, currentConfig) ??
1442
+ `chain-${nextConfig.chainId}`;
1443
+ nextConfig.rpcUrl = normalizedRpcUrl ?? configuredRpcUrl(nextConfig.chainId, currentConfig);
1444
+ }
1445
+
1446
+ storeAgentAuthToken(credentials.agentKeyId, credentials.agentAuthToken);
1447
+
1448
+ let updated = persistConfig(nextConfig);
1449
+ if (updated.agentAuthToken !== undefined) {
1450
+ updated = clearLegacyConfigKey('agentAuthToken');
1451
+ }
1452
+
1453
+ return {
1454
+ ...summary,
1455
+ agentAuthToken: credentials.agentAuthToken,
1456
+ sourceCleanup: options.cleanupAction,
1457
+ keychain: {
1458
+ stored: true,
1459
+ service: AGENT_AUTH_TOKEN_KEYCHAIN_SERVICE,
1460
+ },
1461
+ config: redactConfig(updated),
1462
+ };
1463
+ } finally {
1464
+ cleanupBootstrapAgentCredentialsFile(options.bootstrapOutputPath, options.cleanupAction);
1465
+ }
1466
+ }