@wlfi-agent/cli 1.4.13 → 1.4.15

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 +45 -41
  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,143 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import type { WlfiConfig } from '../../packages/config/src/index.js';
4
+ import * as configModule from '../../packages/config/src/index.js';
5
+ import { assertTrustedExecutablePath } from './fs-trust.js';
6
+ import { resolveValidatedPassthroughDaemonSocket } from './passthrough-security.js';
7
+ import { prepareSpawnOptions, type RunRustBinaryOptions } from './rust-spawn-options.js';
8
+
9
+ const { readConfig, resolveRustBinaryPath } =
10
+ configModule as typeof import('../../packages/config/src/index.js');
11
+
12
+ export type RustBinaryName = 'wlfi-agent-daemon' | 'wlfi-agent-admin' | 'wlfi-agent-agent';
13
+
14
+ export class RustBinaryExitError extends Error {
15
+ readonly binaryName: RustBinaryName;
16
+ readonly code: number;
17
+ readonly stdout: string;
18
+ readonly stderr: string;
19
+
20
+ constructor(binaryName: RustBinaryName, code: number, stdout: string, stderr: string) {
21
+ super(stderr.trim() || `${binaryName} exited with code ${code}`);
22
+ this.name = 'RustBinaryExitError';
23
+ this.binaryName = binaryName;
24
+ this.code = code;
25
+ this.stdout = stdout;
26
+ this.stderr = stderr;
27
+ }
28
+ }
29
+
30
+ function ensureBinary(binaryName: RustBinaryName, config?: WlfiConfig): string {
31
+ const resolved = resolveRustBinaryPath(binaryName, config ?? readConfig());
32
+ if (!fs.existsSync(resolved)) {
33
+ throw new Error(
34
+ `${binaryName} is not installed at ${resolved}. Re-run npm install -g @wlfi-agent/cli or npm run install:rust-binaries.`,
35
+ );
36
+ }
37
+ assertTrustedExecutablePath(resolved);
38
+ return resolved;
39
+ }
40
+
41
+ function forwardedArgsIncludeDaemonSocket(args: string[]): boolean {
42
+ return args.some(
43
+ (arg, _index) => arg === '--daemon-socket' || arg.startsWith('--daemon-socket='),
44
+ );
45
+ }
46
+
47
+ export async function passthroughRustBinary(
48
+ binaryName: RustBinaryName,
49
+ args: string[],
50
+ config?: WlfiConfig,
51
+ ): Promise<number> {
52
+ const resolvedConfig = config ?? readConfig();
53
+ const resolvedDaemonSocket = resolveValidatedPassthroughDaemonSocket(
54
+ binaryName,
55
+ args,
56
+ resolvedConfig,
57
+ );
58
+ const executable = ensureBinary(binaryName, resolvedConfig);
59
+ const prepared = await prepareSpawnOptions(binaryName, args, {});
60
+ const env = { ...prepared.env };
61
+ if (
62
+ resolvedDaemonSocket &&
63
+ !forwardedArgsIncludeDaemonSocket(args) &&
64
+ !env.WLFI_DAEMON_SOCKET?.trim()
65
+ ) {
66
+ env.WLFI_DAEMON_SOCKET = resolvedDaemonSocket;
67
+ }
68
+ const child = spawn(executable, prepared.args, {
69
+ stdio: [prepared.stdin !== undefined ? 'pipe' : 'inherit', 'inherit', 'inherit'],
70
+ env,
71
+ });
72
+
73
+ if (prepared.stdin !== undefined) {
74
+ child.stdin?.end(prepared.stdin);
75
+ }
76
+
77
+ return await new Promise((resolve, reject) => {
78
+ child.on('error', reject);
79
+ child.on('close', (code) => resolve(code ?? 1));
80
+ });
81
+ }
82
+
83
+ export async function runRustBinary(
84
+ binaryName: RustBinaryName,
85
+ args: string[],
86
+ config?: WlfiConfig,
87
+ options: RunRustBinaryOptions = {},
88
+ ): Promise<{ stdout: string; stderr: string; code: number }> {
89
+ const resolvedConfig = config ?? readConfig();
90
+ const resolvedDaemonSocket = resolveValidatedPassthroughDaemonSocket(
91
+ binaryName,
92
+ args,
93
+ resolvedConfig,
94
+ );
95
+ const executable = ensureBinary(binaryName, resolvedConfig);
96
+ const prepared = await prepareSpawnOptions(binaryName, args, options);
97
+ const env = { ...prepared.env };
98
+ if (
99
+ resolvedDaemonSocket &&
100
+ !forwardedArgsIncludeDaemonSocket(args) &&
101
+ !env.WLFI_DAEMON_SOCKET?.trim()
102
+ ) {
103
+ env.WLFI_DAEMON_SOCKET = resolvedDaemonSocket;
104
+ }
105
+ const child = spawn(executable, prepared.args, {
106
+ stdio: [prepared.stdin !== undefined ? 'pipe' : 'ignore', 'pipe', 'pipe'],
107
+ env,
108
+ });
109
+
110
+ let stdout = '';
111
+ let stderr = '';
112
+ child.stdout?.on('data', (chunk) => {
113
+ stdout += chunk.toString();
114
+ });
115
+ child.stderr?.on('data', (chunk) => {
116
+ stderr += chunk.toString();
117
+ });
118
+
119
+ if (prepared.stdin !== undefined) {
120
+ child.stdin?.end(prepared.stdin);
121
+ }
122
+
123
+ const code = await new Promise<number>((resolve, reject) => {
124
+ child.on('error', reject);
125
+ child.on('close', (value) => resolve(value ?? 1));
126
+ });
127
+
128
+ if (code !== 0) {
129
+ throw new RustBinaryExitError(binaryName, code, stdout, stderr);
130
+ }
131
+
132
+ return { stdout, stderr, code };
133
+ }
134
+
135
+ export async function runRustBinaryJson<T>(
136
+ binaryName: RustBinaryName,
137
+ args: string[],
138
+ config?: WlfiConfig,
139
+ options: RunRustBinaryOptions = {},
140
+ ): Promise<T> {
141
+ const { stdout } = await runRustBinary(binaryName, args, config, options);
142
+ return JSON.parse(stdout) as T;
143
+ }
@@ -0,0 +1 @@
1
+ export * from './signed-tx.ts';
@@ -0,0 +1,116 @@
1
+ import {
2
+ isAddressEqual,
3
+ parseTransaction,
4
+ recoverTransactionAddress,
5
+ type Address,
6
+ type Hex
7
+ } from 'viem';
8
+
9
+ export interface ExpectedSignedBroadcastTransaction {
10
+ rawTxHex: Hex;
11
+ from: Address;
12
+ to: Address;
13
+ chainId: number;
14
+ nonce: number;
15
+ allowHigherNonce?: boolean;
16
+ value: bigint;
17
+ data: Hex;
18
+ gasLimit: bigint;
19
+ maxFeePerGas: bigint;
20
+ maxPriorityFeePerGas: bigint;
21
+ txType: string;
22
+ }
23
+
24
+ export interface SignedBroadcastInspection {
25
+ nonce: number;
26
+ }
27
+
28
+ function normalizeHex(value: Hex): string {
29
+ return value.toLowerCase();
30
+ }
31
+
32
+ function normalizeTxType(value: string): string {
33
+ const normalized = value.trim().toLowerCase();
34
+ if (!normalized) {
35
+ throw new Error('txType is required');
36
+ }
37
+
38
+ let parsed: bigint;
39
+ try {
40
+ parsed = normalized.startsWith('0x') ? BigInt(normalized) : BigInt(normalized);
41
+ } catch {
42
+ throw new Error(`Unsupported txType '${value}'`);
43
+ }
44
+
45
+ switch (parsed) {
46
+ case 0n:
47
+ return 'legacy';
48
+ case 1n:
49
+ return 'eip2930';
50
+ case 2n:
51
+ return 'eip1559';
52
+ case 3n:
53
+ return 'eip4844';
54
+ case 4n:
55
+ return 'eip7702';
56
+ default:
57
+ throw new Error(`Unsupported txType '${value}'`);
58
+ }
59
+ }
60
+
61
+ function assertEqual<T>(actual: T, expected: T, label: string): void {
62
+ if (actual !== expected) {
63
+ throw new Error(`signed raw transaction ${label} mismatch: expected ${expected}, received ${actual}`);
64
+ }
65
+ }
66
+
67
+ export async function assertSignedBroadcastTransactionMatchesRequest(
68
+ expected: ExpectedSignedBroadcastTransaction
69
+ ): Promise<SignedBroadcastInspection> {
70
+ const parsed = parseTransaction(expected.rawTxHex);
71
+ const recoveredFrom = await recoverTransactionAddress({
72
+ serializedTransaction:
73
+ expected.rawTxHex as Parameters<typeof recoverTransactionAddress>[0]['serializedTransaction']
74
+ });
75
+
76
+ if (!isAddressEqual(recoveredFrom, expected.from)) {
77
+ throw new Error(
78
+ `signed raw transaction from mismatch: expected ${expected.from}, received ${recoveredFrom}`
79
+ );
80
+ }
81
+
82
+ const parsedTo = parsed.to;
83
+ if (!parsedTo || !isAddressEqual(parsedTo, expected.to)) {
84
+ throw new Error(`signed raw transaction to mismatch: expected ${expected.to}, received ${parsedTo ?? 'null'}`);
85
+ }
86
+
87
+ if (parsed.nonce === undefined) {
88
+ throw new Error('signed raw transaction nonce is missing');
89
+ }
90
+
91
+ assertEqual(parsed.chainId, expected.chainId, 'chainId');
92
+ if (expected.allowHigherNonce) {
93
+ if (parsed.nonce < expected.nonce) {
94
+ throw new Error(
95
+ `signed raw transaction nonce mismatch: expected at least ${expected.nonce}, received ${parsed.nonce}`
96
+ );
97
+ }
98
+ } else {
99
+ assertEqual(parsed.nonce, expected.nonce, 'nonce');
100
+ }
101
+ assertEqual(parsed.value ?? 0n, expected.value, 'value');
102
+ assertEqual(parsed.gas, expected.gasLimit, 'gasLimit');
103
+ assertEqual(parsed.maxFeePerGas, expected.maxFeePerGas, 'maxFeePerGas');
104
+ assertEqual(parsed.maxPriorityFeePerGas, expected.maxPriorityFeePerGas, 'maxPriorityFeePerGas');
105
+ assertEqual(parsed.type, normalizeTxType(expected.txType), 'txType');
106
+
107
+ const parsedData = normalizeHex((parsed.data ?? '0x') as Hex);
108
+ const expectedData = normalizeHex(expected.data);
109
+ if (parsedData !== expectedData) {
110
+ throw new Error(`signed raw transaction data mismatch: expected ${expectedData}, received ${parsedData}`);
111
+ }
112
+
113
+ return {
114
+ nonce: parsed.nonce,
115
+ };
116
+ }
@@ -0,0 +1,116 @@
1
+ import type { Command } from 'commander';
2
+ import {
3
+ formatWalletRepairText,
4
+ repairWalletState,
5
+ type WalletRepairResult,
6
+ } from './wallet-repair.js';
7
+ import {
8
+ formatWalletStatusText,
9
+ getWalletStatus,
10
+ resolveWalletStatusExitCode,
11
+ type WalletStatusExitOptions,
12
+ type WalletStatusResult,
13
+ } from './wallet-status.js';
14
+
15
+ interface CliOutputOptions {
16
+ asJson: boolean;
17
+ }
18
+
19
+ export interface StatusCommandDeps {
20
+ getWalletStatus?: () => WalletStatusResult;
21
+ print?: (payload: unknown, options: CliOutputOptions) => void;
22
+ setExitCode?: (code: number) => void;
23
+ resolveWalletStatusExitCode?: (
24
+ result: WalletStatusResult,
25
+ options?: WalletStatusExitOptions,
26
+ ) => number;
27
+ }
28
+
29
+ export interface RepairCommandDeps {
30
+ repairWalletState?: (input?: {
31
+ agentKeyId?: string;
32
+ overwriteKeychain?: boolean;
33
+ redactBootstrap?: boolean;
34
+ }) => WalletRepairResult;
35
+ print?: (payload: unknown, options: CliOutputOptions) => void;
36
+ setExitCode?: (code: number) => void;
37
+ resolveWalletStatusExitCode?: (
38
+ result: WalletStatusResult,
39
+ options?: WalletStatusExitOptions,
40
+ ) => number;
41
+ }
42
+
43
+ function defaultPrint(payload: unknown, options: CliOutputOptions): void {
44
+ if (options.asJson) {
45
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
46
+ return;
47
+ }
48
+ process.stdout.write(`${String(payload)}\n`);
49
+ }
50
+
51
+ function defaultSetExitCode(code: number): void {
52
+ process.exitCode = code;
53
+ }
54
+
55
+ export function registerStatusCommand(program: Command, deps: StatusCommandDeps = {}): Command {
56
+ const loadStatus = deps.getWalletStatus ?? getWalletStatus;
57
+ const print = deps.print ?? defaultPrint;
58
+ const setExitCode = deps.setExitCode ?? defaultSetExitCode;
59
+ const resolveExitCode = deps.resolveWalletStatusExitCode ?? resolveWalletStatusExitCode;
60
+
61
+ return program
62
+ .command('status')
63
+ .description('Inspect local wallet security, daemon, and credential health')
64
+ .option('--strict', 'Exit with status 1 when warnings are present', false)
65
+ .option('--json', 'Print JSON output', false)
66
+ .action((options) => {
67
+ const result = loadStatus();
68
+ print(options.json ? result : formatWalletStatusText(result), {
69
+ asJson: options.json,
70
+ });
71
+ setExitCode(
72
+ resolveExitCode(result, {
73
+ strict: options.strict,
74
+ }),
75
+ );
76
+ });
77
+ }
78
+
79
+ export function registerRepairCommand(program: Command, deps: RepairCommandDeps = {}): Command {
80
+ const runRepair = deps.repairWalletState ?? repairWalletState;
81
+ const print = deps.print ?? defaultPrint;
82
+ const setExitCode = deps.setExitCode ?? defaultSetExitCode;
83
+ const resolveExitCode = deps.resolveWalletStatusExitCode ?? resolveWalletStatusExitCode;
84
+
85
+ return program
86
+ .command('repair')
87
+ .description('Repair non-privileged local wallet issues and clean plaintext artifacts')
88
+ .option('--agent-key-id <uuid>', 'Agent key id override for legacy token migration')
89
+ .option(
90
+ '--overwrite-keychain',
91
+ 'Replace a different existing Keychain token for this agent when migrating plaintext config storage',
92
+ false,
93
+ )
94
+ .option(
95
+ '--redact-bootstrap',
96
+ 'Redact auto-generated bootstrap files instead of deleting them',
97
+ false,
98
+ )
99
+ .option('--strict', 'Exit with status 1 when warnings remain after repair', false)
100
+ .option('--json', 'Print JSON output', false)
101
+ .action((options) => {
102
+ const result = runRepair({
103
+ agentKeyId: options.agentKeyId,
104
+ overwriteKeychain: options.overwriteKeychain,
105
+ redactBootstrap: options.redactBootstrap,
106
+ });
107
+ print(options.json ? result : formatWalletRepairText(result), {
108
+ asJson: options.json,
109
+ });
110
+ setExitCode(
111
+ resolveExitCode(result.after, {
112
+ strict: options.strict,
113
+ }),
114
+ );
115
+ });
116
+ }
@@ -0,0 +1 @@
1
+ export * from './sudo.ts';
@@ -0,0 +1,172 @@
1
+ import { spawn, type SpawnOptions } from 'node:child_process';
2
+
3
+ export interface SudoCommandResult {
4
+ code: number;
5
+ stdout: string;
6
+ stderr: string;
7
+ }
8
+
9
+ export interface RunSudoCommandOptions {
10
+ stdin?: string;
11
+ inheritOutput?: boolean;
12
+ }
13
+
14
+ interface CreateSudoSessionDeps {
15
+ promptPassword: () => Promise<string>;
16
+ isRoot?: () => boolean;
17
+ spawnCommand?: typeof spawn;
18
+ stdout?: Pick<NodeJS.WriteStream, 'write'>;
19
+ stderr?: Pick<NodeJS.WriteStream, 'write'>;
20
+ }
21
+
22
+ function currentProcessIsRoot(): boolean {
23
+ return typeof process.geteuid === 'function' && process.geteuid() === 0;
24
+ }
25
+
26
+ function isSudoAuthenticationFailure(output: string): boolean {
27
+ return (
28
+ /sudo: .*password is required/iu.test(output) ||
29
+ /sudo: .*incorrect password/iu.test(output) ||
30
+ /sudo: no password was provided/iu.test(output) ||
31
+ /sorry, try again\./iu.test(output)
32
+ );
33
+ }
34
+
35
+ async function runCommand(
36
+ command: string,
37
+ args: string[],
38
+ options: RunSudoCommandOptions,
39
+ deps: {
40
+ spawnCommand: typeof spawn;
41
+ stdout: Pick<NodeJS.WriteStream, 'write'>;
42
+ stderr: Pick<NodeJS.WriteStream, 'write'>;
43
+ },
44
+ ): Promise<SudoCommandResult> {
45
+ return await new Promise((resolve, reject) => {
46
+ const spawnOptions: SpawnOptions = {
47
+ stdio: ['pipe', 'pipe', 'pipe'],
48
+ };
49
+ const child = deps.spawnCommand(command, args, spawnOptions);
50
+
51
+ let stdout = '';
52
+ let stderr = '';
53
+
54
+ child.stdout?.on('data', (chunk: Buffer | string) => {
55
+ const text = chunk.toString();
56
+ stdout += text;
57
+ if (options.inheritOutput) {
58
+ deps.stdout.write(text);
59
+ }
60
+ });
61
+ child.stderr?.on('data', (chunk: Buffer | string) => {
62
+ const text = chunk.toString();
63
+ stderr += text;
64
+ if (options.inheritOutput) {
65
+ deps.stderr.write(text);
66
+ }
67
+ });
68
+
69
+ child.on('error', reject);
70
+ child.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
71
+ child.stdin?.end(options.stdin ?? '');
72
+ });
73
+ }
74
+
75
+ export function createSudoSession(deps: CreateSudoSessionDeps) {
76
+ const spawnCommand = deps.spawnCommand ?? spawn;
77
+ const isRoot = deps.isRoot ?? currentProcessIsRoot;
78
+ const stdout = deps.stdout ?? process.stdout;
79
+ const stderr = deps.stderr ?? process.stderr;
80
+ let primed = false;
81
+
82
+ async function prime(): Promise<void> {
83
+ if (isRoot()) {
84
+ primed = true;
85
+ return;
86
+ }
87
+ if (primed) {
88
+ return;
89
+ }
90
+
91
+ const password = await deps.promptPassword();
92
+ const result = await runCommand(
93
+ 'sudo',
94
+ ['-S', '-p', '', '-v'],
95
+ {
96
+ stdin: `${password}\n`,
97
+ },
98
+ {
99
+ spawnCommand,
100
+ stdout,
101
+ stderr,
102
+ },
103
+ );
104
+
105
+ if (result.code !== 0) {
106
+ throw new Error(
107
+ result.stderr.trim() ||
108
+ result.stdout.trim() ||
109
+ `sudo credential check failed (exit code ${result.code})`,
110
+ );
111
+ }
112
+
113
+ primed = true;
114
+ }
115
+
116
+ async function run(
117
+ args: string[],
118
+ options: RunSudoCommandOptions = {},
119
+ ): Promise<SudoCommandResult> {
120
+ if (args.length === 0) {
121
+ throw new Error('sudo command arguments are required');
122
+ }
123
+
124
+ if (isRoot()) {
125
+ return await runCommand(args[0], args.slice(1), options, {
126
+ spawnCommand,
127
+ stdout,
128
+ stderr,
129
+ });
130
+ }
131
+
132
+ await prime();
133
+ let result = await runCommand(
134
+ 'sudo',
135
+ ['-n', ...args],
136
+ options,
137
+ {
138
+ spawnCommand,
139
+ stdout,
140
+ stderr,
141
+ },
142
+ );
143
+
144
+ if (result.code === 0) {
145
+ return result;
146
+ }
147
+
148
+ const combinedOutput = `${result.stderr}\n${result.stdout}`;
149
+ if (!isSudoAuthenticationFailure(combinedOutput)) {
150
+ return result;
151
+ }
152
+
153
+ primed = false;
154
+ await prime();
155
+ result = await runCommand(
156
+ 'sudo',
157
+ ['-n', ...args],
158
+ options,
159
+ {
160
+ spawnCommand,
161
+ stdout,
162
+ stderr,
163
+ },
164
+ );
165
+ return result;
166
+ }
167
+
168
+ return {
169
+ prime,
170
+ run,
171
+ };
172
+ }
@@ -0,0 +1 @@
1
+ export * from './vault-password-forwarding.ts';
@@ -0,0 +1,155 @@
1
+ const MAX_SECRET_STDIN_BYTES = 16 * 1024
2
+
3
+ export interface VaultPasswordRelayOptions {
4
+ env?: NodeJS.ProcessEnv
5
+ readFromStdin?: (label: string) => Promise<string>
6
+ }
7
+
8
+ export interface PreparedVaultPasswordRelay {
9
+ args: string[]
10
+ stdin?: string
11
+ scrubSensitiveEnv: boolean
12
+ }
13
+
14
+ const HELP_FLAGS = new Set(['-h', '--help', '-V', '--version'])
15
+
16
+ function forwardedArgsPrefix(args: string[]): string[] {
17
+ const terminatorIndex = args.indexOf('--')
18
+ return terminatorIndex >= 0 ? args.slice(0, terminatorIndex) : args
19
+ }
20
+
21
+ function skipEnvRelayForArgs(args: string[]): boolean {
22
+ const forwarded = forwardedArgsPrefix(args)
23
+ return forwarded[0] === 'help' || forwarded.some((arg) => HELP_FLAGS.has(arg))
24
+ }
25
+
26
+ function resolveInlineVaultPasswordArg(args: string[]): {
27
+ index: number
28
+ value: string
29
+ } | null {
30
+ let match: { index: number; value: string } | null = null
31
+
32
+ for (let index = 0; index < forwardedArgsPrefix(args).length; index += 1) {
33
+ const current = args[index]
34
+ let nextMatch: { index: number; value: string } | null = null
35
+ if (current === '--vault-password') {
36
+ const value = args[index + 1]
37
+ if (value === undefined || value === '--' || value.startsWith('-')) {
38
+ throw new Error(
39
+ '--vault-password requires a value; use --vault-password=<value> if the value starts with -'
40
+ )
41
+ }
42
+ nextMatch = { index, value }
43
+ index += 1
44
+ } else if (current.startsWith('--vault-password=')) {
45
+ nextMatch = {
46
+ index,
47
+ value: current.slice('--vault-password='.length)
48
+ }
49
+ }
50
+
51
+ if (nextMatch) {
52
+ if (match) {
53
+ throw new Error('--vault-password may only be provided once')
54
+ }
55
+ match = nextMatch
56
+ }
57
+ }
58
+
59
+ return match
60
+ }
61
+
62
+ function countVaultPasswordStdinFlags(args: string[]): number {
63
+ let matches = 0
64
+
65
+ for (const current of forwardedArgsPrefix(args)) {
66
+ if (current === '--vault-password-stdin') {
67
+ matches += 1
68
+ }
69
+ }
70
+
71
+ return matches
72
+ }
73
+
74
+ function withTrailingNewline(secret: string): string {
75
+ return `${secret}\n`
76
+ }
77
+
78
+ function validateSecret(secret: string, label: string): string {
79
+ if (Buffer.byteLength(secret, 'utf8') > MAX_SECRET_STDIN_BYTES) {
80
+ throw new Error(`${label} must not exceed ${MAX_SECRET_STDIN_BYTES} bytes`)
81
+ }
82
+
83
+ const trimmed = secret.replace(/[\r\n]+$/u, '')
84
+ if (!trimmed.trim()) {
85
+ throw new Error(`${label} is required`)
86
+ }
87
+
88
+ return trimmed
89
+ }
90
+
91
+ async function readTrimmedSecretFromProcessStdin(label: string): Promise<string> {
92
+ process.stdin.setEncoding('utf8')
93
+ let raw = ''
94
+ for await (const chunk of process.stdin) {
95
+ raw += chunk
96
+ if (Buffer.byteLength(raw, 'utf8') > MAX_SECRET_STDIN_BYTES) {
97
+ throw new Error(`${label} must not exceed ${MAX_SECRET_STDIN_BYTES} bytes`)
98
+ }
99
+ }
100
+
101
+ return validateSecret(raw, label)
102
+ }
103
+
104
+ export async function prepareVaultPasswordRelay(
105
+ args: string[],
106
+ options: VaultPasswordRelayOptions = {}
107
+ ): Promise<PreparedVaultPasswordRelay> {
108
+ const env = options.env ?? process.env
109
+ const readFromStdin = options.readFromStdin ?? readTrimmedSecretFromProcessStdin
110
+ const inlineArg = resolveInlineVaultPasswordArg(args)
111
+ const stdinFlagCount = countVaultPasswordStdinFlags(args)
112
+ const usesStdinFlag = stdinFlagCount > 0
113
+
114
+ if (stdinFlagCount > 1) {
115
+ throw new Error('--vault-password-stdin may only be provided once')
116
+ }
117
+
118
+ if (inlineArg && usesStdinFlag) {
119
+ throw new Error('--vault-password conflicts with --vault-password-stdin')
120
+ }
121
+
122
+ if (inlineArg) {
123
+ validateSecret(inlineArg.value, 'vaultPassword')
124
+ throw new Error(
125
+ 'insecure --vault-password is disabled; use --vault-password-stdin or a local TTY prompt'
126
+ )
127
+ }
128
+
129
+ if (usesStdinFlag) {
130
+ return {
131
+ args: [...args],
132
+ stdin: withTrailingNewline(await readFromStdin('vaultPassword')),
133
+ scrubSensitiveEnv: true
134
+ }
135
+ }
136
+
137
+ if (Object.prototype.hasOwnProperty.call(env, 'WLFI_VAULT_PASSWORD')) {
138
+ if (skipEnvRelayForArgs(args)) {
139
+ return {
140
+ args: [...args],
141
+ scrubSensitiveEnv: true
142
+ }
143
+ }
144
+
145
+ validateSecret(env.WLFI_VAULT_PASSWORD ?? '', 'vaultPassword')
146
+ throw new Error(
147
+ 'WLFI_VAULT_PASSWORD is disabled for security; use --vault-password-stdin or a local TTY prompt'
148
+ )
149
+ }
150
+
151
+ return {
152
+ args: [...args],
153
+ scrubSensitiveEnv: true
154
+ }
155
+ }