@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,818 @@
1
+ use super::{
2
+ parse_code_sign_requirement, recv_matching_response, validate_wire_lengths,
3
+ IncomingWireMessage, XpcDaemonClient, XpcDaemonServer, XpcTransportError, MAX_WIRE_BODY_BYTES,
4
+ MAX_WIRE_REQUEST_ID_BYTES,
5
+ };
6
+ use std::collections::BTreeSet;
7
+ use std::sync::Arc;
8
+ use std::time::Duration;
9
+ use vault_daemon::{DaemonConfig, DaemonError, InMemoryDaemon, KeyManagerDaemonApi};
10
+ use vault_signer::{KeyCreateRequest, SignerError, SoftwareSignerBackend};
11
+
12
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
13
+ async fn xpc_round_trip_for_issue_lease() {
14
+ #[cfg(not(debug_assertions))]
15
+ if unsafe { libc::geteuid() } != 0 {
16
+ return;
17
+ }
18
+
19
+ let daemon = Arc::new(
20
+ InMemoryDaemon::new(
21
+ "vault-password",
22
+ SoftwareSignerBackend::default(),
23
+ DaemonConfig::default(),
24
+ )
25
+ .expect("daemon"),
26
+ );
27
+
28
+ #[cfg(debug_assertions)]
29
+ let server = XpcDaemonServer::start_inmemory_with_allowed_euid(
30
+ daemon,
31
+ tokio::runtime::Handle::current(),
32
+ unsafe { libc::geteuid() },
33
+ )
34
+ .expect("server");
35
+ #[cfg(not(debug_assertions))]
36
+ let server =
37
+ XpcDaemonServer::start_inmemory(daemon, tokio::runtime::Handle::current()).expect("server");
38
+ let client =
39
+ XpcDaemonClient::connect(&server.endpoint(), Duration::from_secs(5)).expect("client");
40
+
41
+ let lease = client
42
+ .issue_lease("vault-password")
43
+ .await
44
+ .expect("issue_lease");
45
+ assert!(lease.expires_at > lease.issued_at);
46
+ }
47
+
48
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
49
+ async fn start_inmemory_is_root_only_by_default() {
50
+ if unsafe { libc::geteuid() } == 0 {
51
+ return;
52
+ }
53
+
54
+ let daemon = Arc::new(
55
+ InMemoryDaemon::new(
56
+ "vault-password",
57
+ SoftwareSignerBackend::default(),
58
+ DaemonConfig::default(),
59
+ )
60
+ .expect("daemon"),
61
+ );
62
+
63
+ let result = XpcDaemonServer::start_inmemory(daemon, tokio::runtime::Handle::current());
64
+ assert!(matches!(result, Err(XpcTransportError::RequiresRoot)));
65
+ }
66
+
67
+ #[test]
68
+ fn code_sign_requirement_parser_rejects_invalid_syntax() {
69
+ let err = match parse_code_sign_requirement("not valid requirement expression") {
70
+ Ok(_) => panic!("invalid requirement expression must fail"),
71
+ Err(err) => err,
72
+ };
73
+ assert!(matches!(err, XpcTransportError::CodeSigning(_)));
74
+ }
75
+
76
+ #[test]
77
+ fn code_sign_requirement_parser_accepts_valid_syntax() {
78
+ parse_code_sign_requirement("anchor apple")
79
+ .expect("well-formed requirement expression should parse");
80
+ }
81
+
82
+ #[cfg(debug_assertions)]
83
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
84
+ async fn start_inmemory_rejects_invalid_code_sign_requirement() {
85
+ let daemon = Arc::new(
86
+ InMemoryDaemon::new(
87
+ "vault-password",
88
+ SoftwareSignerBackend::default(),
89
+ DaemonConfig::default(),
90
+ )
91
+ .expect("daemon"),
92
+ );
93
+
94
+ let result = XpcDaemonServer::start_inmemory_with_allowed_euid_and_code_sign_requirement(
95
+ daemon,
96
+ tokio::runtime::Handle::current(),
97
+ unsafe { libc::geteuid() },
98
+ "this is not a valid requirement expression",
99
+ );
100
+ assert!(matches!(result, Err(XpcTransportError::CodeSigning(_))));
101
+ }
102
+
103
+ #[cfg(debug_assertions)]
104
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
105
+ async fn start_inmemory_rejects_mismatched_allowed_euid() {
106
+ let daemon = Arc::new(
107
+ InMemoryDaemon::new(
108
+ "vault-password",
109
+ SoftwareSignerBackend::default(),
110
+ DaemonConfig::default(),
111
+ )
112
+ .expect("daemon"),
113
+ );
114
+
115
+ let current_euid = unsafe { libc::geteuid() };
116
+ let mismatched_euid = if current_euid == 0 { 1 } else { 0 };
117
+
118
+ let result = XpcDaemonServer::start_inmemory_with_allowed_euid(
119
+ daemon,
120
+ tokio::runtime::Handle::current(),
121
+ mismatched_euid,
122
+ );
123
+ assert!(matches!(result, Err(XpcTransportError::Internal(_))));
124
+ }
125
+
126
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
127
+ async fn concurrent_client_calls_do_not_cross_responses() {
128
+ #[cfg(not(debug_assertions))]
129
+ if unsafe { libc::geteuid() } != 0 {
130
+ return;
131
+ }
132
+
133
+ let daemon = Arc::new(
134
+ InMemoryDaemon::new(
135
+ "vault-password",
136
+ SoftwareSignerBackend::default(),
137
+ DaemonConfig::default(),
138
+ )
139
+ .expect("daemon"),
140
+ );
141
+
142
+ #[cfg(debug_assertions)]
143
+ let server = XpcDaemonServer::start_inmemory_with_allowed_euid(
144
+ daemon,
145
+ tokio::runtime::Handle::current(),
146
+ unsafe { libc::geteuid() },
147
+ )
148
+ .expect("server");
149
+ #[cfg(not(debug_assertions))]
150
+ let server =
151
+ XpcDaemonServer::start_inmemory(daemon, tokio::runtime::Handle::current()).expect("server");
152
+ let client = Arc::new(
153
+ XpcDaemonClient::connect(&server.endpoint(), Duration::from_secs(5)).expect("client"),
154
+ );
155
+
156
+ let c1 = client.clone();
157
+ let c2 = client.clone();
158
+ let (r1, r2) = tokio::join!(
159
+ c1.issue_lease("vault-password"),
160
+ c2.issue_lease("vault-password")
161
+ );
162
+
163
+ let lease1 = r1.expect("lease 1");
164
+ let lease2 = r2.expect("lease 2");
165
+ assert_ne!(lease1.lease_id, lease2.lease_id);
166
+ }
167
+
168
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
169
+ async fn daemon_error_variants_roundtrip_over_xpc() {
170
+ #[cfg(not(debug_assertions))]
171
+ if unsafe { libc::geteuid() } != 0 {
172
+ return;
173
+ }
174
+
175
+ let daemon = Arc::new(
176
+ InMemoryDaemon::new(
177
+ "vault-password",
178
+ SoftwareSignerBackend::default(),
179
+ DaemonConfig::default(),
180
+ )
181
+ .expect("daemon"),
182
+ );
183
+
184
+ #[cfg(debug_assertions)]
185
+ let server = XpcDaemonServer::start_inmemory_with_allowed_euid(
186
+ daemon,
187
+ tokio::runtime::Handle::current(),
188
+ unsafe { libc::geteuid() },
189
+ )
190
+ .expect("server");
191
+ #[cfg(not(debug_assertions))]
192
+ let server =
193
+ XpcDaemonServer::start_inmemory(daemon, tokio::runtime::Handle::current()).expect("server");
194
+ let client =
195
+ XpcDaemonClient::connect(&server.endpoint(), Duration::from_secs(5)).expect("client");
196
+
197
+ let lease = client.issue_lease("vault-password").await.expect("lease");
198
+ let bad_session = vault_domain::AdminSession {
199
+ vault_password: "wrong-password".to_string(),
200
+ lease,
201
+ };
202
+
203
+ let err = client
204
+ .list_policies(&bad_session)
205
+ .await
206
+ .expect_err("must return auth failure");
207
+ assert!(matches!(err, DaemonError::AuthenticationFailed));
208
+ }
209
+
210
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
211
+ async fn invalid_policy_attachment_roundtrips_over_xpc() {
212
+ #[cfg(not(debug_assertions))]
213
+ if unsafe { libc::geteuid() } != 0 {
214
+ return;
215
+ }
216
+
217
+ let daemon = Arc::new(
218
+ InMemoryDaemon::new(
219
+ "vault-password",
220
+ SoftwareSignerBackend::default(),
221
+ DaemonConfig::default(),
222
+ )
223
+ .expect("daemon"),
224
+ );
225
+
226
+ #[cfg(debug_assertions)]
227
+ let server = XpcDaemonServer::start_inmemory_with_allowed_euid(
228
+ daemon,
229
+ tokio::runtime::Handle::current(),
230
+ unsafe { libc::geteuid() },
231
+ )
232
+ .expect("server");
233
+ #[cfg(not(debug_assertions))]
234
+ let server =
235
+ XpcDaemonServer::start_inmemory(daemon, tokio::runtime::Handle::current()).expect("server");
236
+ let client =
237
+ XpcDaemonClient::connect(&server.endpoint(), Duration::from_secs(5)).expect("client");
238
+
239
+ let lease = client.issue_lease("vault-password").await.expect("lease");
240
+ let admin = vault_domain::AdminSession {
241
+ vault_password: "vault-password".to_string(),
242
+ lease,
243
+ };
244
+
245
+ let vault_key = client
246
+ .create_vault_key(&admin, KeyCreateRequest::Generate)
247
+ .await
248
+ .expect("vault key");
249
+
250
+ let err = client
251
+ .create_agent_key(
252
+ &admin,
253
+ vault_key.id,
254
+ vault_domain::PolicyAttachment::PolicySet(BTreeSet::new()),
255
+ )
256
+ .await
257
+ .expect_err("empty attachment must fail");
258
+ assert!(matches!(err, DaemonError::InvalidPolicyAttachment(_)));
259
+ }
260
+
261
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
262
+ async fn invalid_policy_error_roundtrips_over_xpc() {
263
+ #[cfg(not(debug_assertions))]
264
+ if unsafe { libc::geteuid() } != 0 {
265
+ return;
266
+ }
267
+
268
+ let daemon = Arc::new(
269
+ InMemoryDaemon::new(
270
+ "vault-password",
271
+ SoftwareSignerBackend::default(),
272
+ DaemonConfig::default(),
273
+ )
274
+ .expect("daemon"),
275
+ );
276
+
277
+ #[cfg(debug_assertions)]
278
+ let server = XpcDaemonServer::start_inmemory_with_allowed_euid(
279
+ daemon,
280
+ tokio::runtime::Handle::current(),
281
+ unsafe { libc::geteuid() },
282
+ )
283
+ .expect("server");
284
+ #[cfg(not(debug_assertions))]
285
+ let server =
286
+ XpcDaemonServer::start_inmemory(daemon, tokio::runtime::Handle::current()).expect("server");
287
+ let client =
288
+ XpcDaemonClient::connect(&server.endpoint(), Duration::from_secs(5)).expect("client");
289
+
290
+ let lease = client.issue_lease("vault-password").await.expect("lease");
291
+ let admin = vault_domain::AdminSession {
292
+ vault_password: "vault-password".to_string(),
293
+ lease,
294
+ };
295
+
296
+ let invalid_policy = vault_domain::SpendingPolicy {
297
+ id: uuid::Uuid::new_v4(),
298
+ priority: 0,
299
+ policy_type: vault_domain::PolicyType::PerTxMaxSpending,
300
+ min_amount_wei: None,
301
+ max_amount_wei: 0,
302
+ recipients: vault_domain::EntityScope::All,
303
+ assets: vault_domain::EntityScope::All,
304
+ networks: vault_domain::EntityScope::All,
305
+ enabled: true,
306
+ };
307
+
308
+ let err = client
309
+ .add_policy(&admin, invalid_policy)
310
+ .await
311
+ .expect_err("invalid policy must fail");
312
+ assert!(matches!(err, DaemonError::InvalidPolicy(_)));
313
+ }
314
+
315
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
316
+ async fn signer_error_variants_roundtrip_over_xpc() {
317
+ #[cfg(not(debug_assertions))]
318
+ if unsafe { libc::geteuid() } != 0 {
319
+ return;
320
+ }
321
+
322
+ let daemon = Arc::new(
323
+ InMemoryDaemon::new(
324
+ "vault-password",
325
+ SoftwareSignerBackend::default(),
326
+ DaemonConfig::default(),
327
+ )
328
+ .expect("daemon"),
329
+ );
330
+
331
+ #[cfg(debug_assertions)]
332
+ let server = XpcDaemonServer::start_inmemory_with_allowed_euid(
333
+ daemon,
334
+ tokio::runtime::Handle::current(),
335
+ unsafe { libc::geteuid() },
336
+ )
337
+ .expect("server");
338
+ #[cfg(not(debug_assertions))]
339
+ let server =
340
+ XpcDaemonServer::start_inmemory(daemon, tokio::runtime::Handle::current()).expect("server");
341
+ let client =
342
+ XpcDaemonClient::connect(&server.endpoint(), Duration::from_secs(5)).expect("client");
343
+
344
+ let lease = client.issue_lease("vault-password").await.expect("lease");
345
+ let admin = vault_domain::AdminSession {
346
+ vault_password: "vault-password".to_string(),
347
+ lease,
348
+ };
349
+
350
+ let err = client
351
+ .create_vault_key(
352
+ &admin,
353
+ KeyCreateRequest::Import {
354
+ private_key_hex: "0x1234".to_string(),
355
+ },
356
+ )
357
+ .await
358
+ .expect_err("invalid import key must fail");
359
+ assert!(matches!(
360
+ err,
361
+ DaemonError::Signer(SignerError::InvalidPrivateKey)
362
+ ));
363
+ }
364
+
365
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
366
+ async fn invalid_config_error_roundtrips_over_xpc() {
367
+ #[cfg(not(debug_assertions))]
368
+ if unsafe { libc::geteuid() } != 0 {
369
+ return;
370
+ }
371
+
372
+ let config = DaemonConfig {
373
+ lease_ttl: time::Duration::MAX,
374
+ ..DaemonConfig::default()
375
+ };
376
+ let daemon = Arc::new(
377
+ InMemoryDaemon::new("vault-password", SoftwareSignerBackend::default(), config)
378
+ .expect("daemon"),
379
+ );
380
+
381
+ #[cfg(debug_assertions)]
382
+ let server = XpcDaemonServer::start_inmemory_with_allowed_euid(
383
+ daemon,
384
+ tokio::runtime::Handle::current(),
385
+ unsafe { libc::geteuid() },
386
+ )
387
+ .expect("server");
388
+ #[cfg(not(debug_assertions))]
389
+ let server =
390
+ XpcDaemonServer::start_inmemory(daemon, tokio::runtime::Handle::current()).expect("server");
391
+ let client =
392
+ XpcDaemonClient::connect(&server.endpoint(), Duration::from_secs(5)).expect("client");
393
+
394
+ let err = client
395
+ .issue_lease("vault-password")
396
+ .await
397
+ .expect_err("overflowing ttl must roundtrip as invalid config");
398
+ assert!(matches!(err, DaemonError::InvalidConfig(_)));
399
+ }
400
+
401
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
402
+ async fn unknown_agent_key_is_not_disclosed_over_xpc() {
403
+ use serde_json::to_vec;
404
+ use time::OffsetDateTime;
405
+ use uuid::Uuid;
406
+ use vault_domain::{AgentAction, SignRequest};
407
+
408
+ #[cfg(not(debug_assertions))]
409
+ if unsafe { libc::geteuid() } != 0 {
410
+ return;
411
+ }
412
+
413
+ let daemon = Arc::new(
414
+ InMemoryDaemon::new(
415
+ "vault-password",
416
+ SoftwareSignerBackend::default(),
417
+ DaemonConfig::default(),
418
+ )
419
+ .expect("daemon"),
420
+ );
421
+
422
+ #[cfg(debug_assertions)]
423
+ let server = XpcDaemonServer::start_inmemory_with_allowed_euid(
424
+ daemon,
425
+ tokio::runtime::Handle::current(),
426
+ unsafe { libc::geteuid() },
427
+ )
428
+ .expect("server");
429
+ #[cfg(not(debug_assertions))]
430
+ let server =
431
+ XpcDaemonServer::start_inmemory(daemon, tokio::runtime::Handle::current()).expect("server");
432
+ let client =
433
+ XpcDaemonClient::connect(&server.endpoint(), Duration::from_secs(5)).expect("client");
434
+
435
+ let token: vault_domain::EvmAddress = "0x7200000000000000000000000000000000000000"
436
+ .parse()
437
+ .expect("token");
438
+ let recipient: vault_domain::EvmAddress = "0x8200000000000000000000000000000000000000"
439
+ .parse()
440
+ .expect("recipient");
441
+ let action = AgentAction::Transfer {
442
+ chain_id: 1,
443
+ token: token.clone(),
444
+ to: recipient.clone(),
445
+ amount_wei: 1,
446
+ };
447
+ let request = SignRequest {
448
+ request_id: Uuid::new_v4(),
449
+ agent_key_id: Uuid::new_v4(),
450
+ agent_auth_token: "random-token".to_string(),
451
+ payload: to_vec(&action).expect("payload"),
452
+ action,
453
+ requested_at: OffsetDateTime::now_utc(),
454
+ expires_at: OffsetDateTime::now_utc() + time::Duration::minutes(2),
455
+ };
456
+
457
+ let err = client
458
+ .sign_for_agent(request)
459
+ .await
460
+ .expect_err("unknown key must not leak key existence");
461
+ assert!(matches!(err, DaemonError::AgentAuthenticationFailed));
462
+ }
463
+
464
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
465
+ async fn lifecycle_and_evaluation_roundtrip_over_xpc() {
466
+ use serde_json::to_vec;
467
+ use time::OffsetDateTime;
468
+ use uuid::Uuid;
469
+ use vault_domain::{AgentAction, EntityScope, PolicyType, SignRequest, SpendingPolicy};
470
+
471
+ #[cfg(not(debug_assertions))]
472
+ if unsafe { libc::geteuid() } != 0 {
473
+ return;
474
+ }
475
+
476
+ let daemon = Arc::new(
477
+ InMemoryDaemon::new(
478
+ "vault-password",
479
+ SoftwareSignerBackend::default(),
480
+ DaemonConfig::default(),
481
+ )
482
+ .expect("daemon"),
483
+ );
484
+
485
+ #[cfg(debug_assertions)]
486
+ let server = XpcDaemonServer::start_inmemory_with_allowed_euid(
487
+ daemon,
488
+ tokio::runtime::Handle::current(),
489
+ unsafe { libc::geteuid() },
490
+ )
491
+ .expect("server");
492
+ #[cfg(not(debug_assertions))]
493
+ let server =
494
+ XpcDaemonServer::start_inmemory(daemon, tokio::runtime::Handle::current()).expect("server");
495
+ let client =
496
+ XpcDaemonClient::connect(&server.endpoint(), Duration::from_secs(5)).expect("client");
497
+
498
+ let lease = client.issue_lease("vault-password").await.expect("lease");
499
+ let admin = vault_domain::AdminSession {
500
+ vault_password: "vault-password".to_string(),
501
+ lease,
502
+ };
503
+
504
+ let per_tx_policy = SpendingPolicy::new(
505
+ 0,
506
+ PolicyType::PerTxMaxSpending,
507
+ 10,
508
+ EntityScope::All,
509
+ EntityScope::All,
510
+ EntityScope::All,
511
+ )
512
+ .expect("policy");
513
+ client
514
+ .add_policy(&admin, per_tx_policy.clone())
515
+ .await
516
+ .expect("add policy");
517
+
518
+ let vault_key = client
519
+ .create_vault_key(&admin, KeyCreateRequest::Generate)
520
+ .await
521
+ .expect("vault key");
522
+ let agent_credentials = client
523
+ .create_agent_key(
524
+ &admin,
525
+ vault_key.id,
526
+ vault_domain::PolicyAttachment::AllPolicies,
527
+ )
528
+ .await
529
+ .expect("agent key");
530
+
531
+ let token: vault_domain::EvmAddress = "0x7400000000000000000000000000000000000000"
532
+ .parse()
533
+ .expect("token");
534
+ let recipient: vault_domain::EvmAddress = "0x8400000000000000000000000000000000000000"
535
+ .parse()
536
+ .expect("recipient");
537
+ let action = AgentAction::Transfer {
538
+ chain_id: 1,
539
+ token,
540
+ to: recipient,
541
+ amount_wei: 5,
542
+ };
543
+ let request = SignRequest {
544
+ request_id: Uuid::new_v4(),
545
+ agent_key_id: agent_credentials.agent_key.id,
546
+ agent_auth_token: agent_credentials.auth_token.clone(),
547
+ payload: to_vec(&action).expect("payload"),
548
+ action,
549
+ requested_at: OffsetDateTime::now_utc(),
550
+ expires_at: OffsetDateTime::now_utc() + time::Duration::minutes(2),
551
+ };
552
+
553
+ let evaluation = client
554
+ .evaluate_for_agent(request.clone())
555
+ .await
556
+ .expect("evaluate_for_agent must succeed");
557
+ assert_eq!(
558
+ evaluation.evaluated_policy_ids,
559
+ vec![per_tx_policy.id],
560
+ "evaluation should report matched policy ids"
561
+ );
562
+
563
+ let rotated_auth_token = client
564
+ .rotate_agent_auth_token(&admin, agent_credentials.agent_key.id)
565
+ .await
566
+ .expect("rotate token");
567
+ assert_ne!(
568
+ rotated_auth_token, agent_credentials.auth_token,
569
+ "rotation must issue a fresh token"
570
+ );
571
+
572
+ let old_token_err = client
573
+ .evaluate_for_agent(request.clone())
574
+ .await
575
+ .expect_err("old token must fail after rotation");
576
+ assert!(matches!(
577
+ old_token_err,
578
+ DaemonError::AgentAuthenticationFailed
579
+ ));
580
+
581
+ let mut rotated_request = request.clone();
582
+ rotated_request.agent_auth_token = rotated_auth_token;
583
+ client
584
+ .evaluate_for_agent(rotated_request.clone())
585
+ .await
586
+ .expect("rotated token should evaluate");
587
+
588
+ client
589
+ .revoke_agent_key(&admin, agent_credentials.agent_key.id)
590
+ .await
591
+ .expect("revoke key");
592
+
593
+ let revoked_err = client
594
+ .sign_for_agent(rotated_request)
595
+ .await
596
+ .expect_err("revoked key must not sign");
597
+ assert!(matches!(
598
+ revoked_err,
599
+ DaemonError::AgentAuthenticationFailed
600
+ ));
601
+ }
602
+
603
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
604
+ async fn oversized_wire_request_body_is_rejected() {
605
+ use serde_json::to_vec;
606
+ use time::OffsetDateTime;
607
+ use uuid::Uuid;
608
+ use vault_domain::{AgentAction, SignRequest};
609
+
610
+ #[cfg(not(debug_assertions))]
611
+ if unsafe { libc::geteuid() } != 0 {
612
+ return;
613
+ }
614
+
615
+ let daemon = Arc::new(
616
+ InMemoryDaemon::new(
617
+ "vault-password",
618
+ SoftwareSignerBackend::default(),
619
+ DaemonConfig::default(),
620
+ )
621
+ .expect("daemon"),
622
+ );
623
+
624
+ #[cfg(debug_assertions)]
625
+ let server = XpcDaemonServer::start_inmemory_with_allowed_euid(
626
+ daemon,
627
+ tokio::runtime::Handle::current(),
628
+ unsafe { libc::geteuid() },
629
+ )
630
+ .expect("server");
631
+ #[cfg(not(debug_assertions))]
632
+ let server =
633
+ XpcDaemonServer::start_inmemory(daemon, tokio::runtime::Handle::current()).expect("server");
634
+ let client =
635
+ XpcDaemonClient::connect(&server.endpoint(), Duration::from_secs(5)).expect("client");
636
+
637
+ let token: vault_domain::EvmAddress = "0x7300000000000000000000000000000000000000"
638
+ .parse()
639
+ .expect("token");
640
+ let recipient: vault_domain::EvmAddress = "0x8300000000000000000000000000000000000000"
641
+ .parse()
642
+ .expect("recipient");
643
+ let action = AgentAction::Transfer {
644
+ chain_id: 1,
645
+ token: token.clone(),
646
+ to: recipient.clone(),
647
+ amount_wei: 1,
648
+ };
649
+ let request = SignRequest {
650
+ request_id: Uuid::new_v4(),
651
+ agent_key_id: Uuid::new_v4(),
652
+ // Force the serialized wire-body above transport cap.
653
+ agent_auth_token: "a".repeat(MAX_WIRE_BODY_BYTES + 1),
654
+ payload: to_vec(&action).expect("payload"),
655
+ action,
656
+ requested_at: OffsetDateTime::now_utc(),
657
+ expires_at: OffsetDateTime::now_utc() + time::Duration::minutes(2),
658
+ };
659
+
660
+ let err = client
661
+ .sign_for_agent(request)
662
+ .await
663
+ .expect_err("oversized wire body must fail");
664
+ match err {
665
+ DaemonError::Transport(msg) => {
666
+ assert!(msg.contains("wire body exceeds max bytes"));
667
+ }
668
+ other => panic!("unexpected error variant: {other}"),
669
+ }
670
+ }
671
+
672
+ #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
673
+ async fn oversized_wire_response_body_is_rejected_without_timeout() {
674
+ use vault_domain::{EntityScope, PolicyType, SpendingPolicy};
675
+
676
+ #[cfg(not(debug_assertions))]
677
+ if unsafe { libc::geteuid() } != 0 {
678
+ return;
679
+ }
680
+
681
+ let daemon = Arc::new(
682
+ InMemoryDaemon::new(
683
+ "vault-password",
684
+ SoftwareSignerBackend::default(),
685
+ DaemonConfig::default(),
686
+ )
687
+ .expect("daemon"),
688
+ );
689
+
690
+ let lease = daemon
691
+ .issue_lease("vault-password")
692
+ .await
693
+ .expect("issue lease");
694
+ let admin = vault_domain::AdminSession {
695
+ vault_password: "vault-password".to_string(),
696
+ lease,
697
+ };
698
+
699
+ // Build one oversized policy so list_policies response exceeds the
700
+ // transport body cap and must be downgraded to a bounded error.
701
+ let mut recipients = std::collections::BTreeSet::new();
702
+ for i in 0..8_000_u32 {
703
+ let address = format!("0x{i:040x}").parse().expect("valid address");
704
+ recipients.insert(address);
705
+ }
706
+ let policy = SpendingPolicy::new(
707
+ 0,
708
+ PolicyType::PerTxMaxSpending,
709
+ 1,
710
+ EntityScope::Set(recipients),
711
+ EntityScope::All,
712
+ EntityScope::All,
713
+ )
714
+ .expect("policy");
715
+ daemon.add_policy(&admin, policy).await.expect("add policy");
716
+
717
+ #[cfg(debug_assertions)]
718
+ let server = XpcDaemonServer::start_inmemory_with_allowed_euid(
719
+ daemon,
720
+ tokio::runtime::Handle::current(),
721
+ unsafe { libc::geteuid() },
722
+ )
723
+ .expect("server");
724
+ #[cfg(not(debug_assertions))]
725
+ let server =
726
+ XpcDaemonServer::start_inmemory(daemon, tokio::runtime::Handle::current()).expect("server");
727
+ let client =
728
+ XpcDaemonClient::connect(&server.endpoint(), Duration::from_secs(5)).expect("client");
729
+
730
+ let err = client
731
+ .list_policies(&admin)
732
+ .await
733
+ .expect_err("oversized wire response must fail without timeout");
734
+ match err {
735
+ DaemonError::Transport(msg) => {
736
+ assert!(msg.contains("wire body exceeds max bytes"));
737
+ }
738
+ other => panic!("unexpected error variant: {other}"),
739
+ }
740
+ }
741
+
742
+ #[test]
743
+ fn wire_length_validation_rejects_long_request_id() {
744
+ let request_id = "r".repeat(MAX_WIRE_REQUEST_ID_BYTES + 1);
745
+ let err = validate_wire_lengths(&request_id, "{}").expect_err("must reject");
746
+ assert!(matches!(err, XpcTransportError::Protocol(_)));
747
+ }
748
+
749
+ #[test]
750
+ fn wire_length_validation_rejects_large_body() {
751
+ let body = "x".repeat(MAX_WIRE_BODY_BYTES + 1);
752
+ let err = validate_wire_lengths("ok-id", &body).expect_err("must reject");
753
+ assert!(matches!(err, XpcTransportError::Protocol(_)));
754
+ }
755
+
756
+ #[test]
757
+ fn receive_loop_fails_fast_on_decode_errors() {
758
+ let (tx, rx) = std::sync::mpsc::channel::<IncomingWireMessage>();
759
+ tx.send(IncomingWireMessage::DecodeError(
760
+ XpcTransportError::Protocol("bad response".to_string()),
761
+ ))
762
+ .expect("send");
763
+
764
+ let err = recv_matching_response(
765
+ &rx,
766
+ "request-id",
767
+ std::time::Instant::now() + Duration::from_secs(1),
768
+ )
769
+ .expect_err("decode errors must fail fast");
770
+
771
+ assert!(matches!(err, XpcTransportError::Protocol(_)));
772
+ }
773
+
774
+ #[test]
775
+ fn receive_loop_ignores_stale_mismatched_response_then_accepts_matching() {
776
+ let (tx, rx) = std::sync::mpsc::channel::<IncomingWireMessage>();
777
+ tx.send(IncomingWireMessage::Response(super::WireResponse {
778
+ request_id: "different-id".to_string(),
779
+ ok: true,
780
+ body_json: "{}".to_string(),
781
+ }))
782
+ .expect("send");
783
+ tx.send(IncomingWireMessage::Response(super::WireResponse {
784
+ request_id: "expected-id".to_string(),
785
+ ok: true,
786
+ body_json: "{}".to_string(),
787
+ }))
788
+ .expect("send");
789
+
790
+ let response = recv_matching_response(
791
+ &rx,
792
+ "expected-id",
793
+ std::time::Instant::now() + Duration::from_secs(1),
794
+ )
795
+ .expect("stale mismatched responses should be tolerated");
796
+ assert_eq!(response.request_id, "expected-id");
797
+ }
798
+
799
+ #[test]
800
+ fn receive_loop_times_out_when_only_mismatched_responses_arrive() {
801
+ let (tx, rx) = std::sync::mpsc::channel::<IncomingWireMessage>();
802
+ for i in 0..32 {
803
+ tx.send(IncomingWireMessage::Response(super::WireResponse {
804
+ request_id: format!("wrong-{i}"),
805
+ ok: true,
806
+ body_json: "{}".to_string(),
807
+ }))
808
+ .expect("send");
809
+ }
810
+
811
+ let err = recv_matching_response(
812
+ &rx,
813
+ "expected-id",
814
+ std::time::Instant::now() + Duration::from_secs(1),
815
+ )
816
+ .expect_err("mismatched responses must eventually timeout");
817
+ assert!(matches!(err, XpcTransportError::Timeout));
818
+ }