@wlfi-agent/cli 1.4.13 → 1.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/Cargo.lock +3968 -0
  2. package/Cargo.toml +50 -0
  3. package/README.md +426 -6
  4. package/crates/vault-cli-admin/Cargo.toml +26 -0
  5. package/crates/vault-cli-admin/src/io_utils.rs +500 -0
  6. package/crates/vault-cli-admin/src/main.rs +3990 -0
  7. package/crates/vault-cli-admin/src/shared_config.rs +624 -0
  8. package/crates/vault-cli-admin/src/tui/amounts.rs +180 -0
  9. package/crates/vault-cli-admin/src/tui/token_rpc.rs +250 -0
  10. package/crates/vault-cli-admin/src/tui/utils.rs +82 -0
  11. package/crates/vault-cli-admin/src/tui.rs +3410 -0
  12. package/crates/vault-cli-agent/Cargo.toml +24 -0
  13. package/crates/vault-cli-agent/src/io_utils.rs +576 -0
  14. package/crates/vault-cli-agent/src/main.rs +833 -0
  15. package/crates/vault-cli-daemon/Cargo.toml +28 -0
  16. package/crates/vault-cli-daemon/src/bin/wlfi-agent-system-keychain.rs +216 -0
  17. package/crates/vault-cli-daemon/src/main.rs +644 -0
  18. package/crates/vault-cli-daemon/src/relay_sync.rs +894 -0
  19. package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +167 -0
  20. package/crates/vault-daemon/Cargo.toml +32 -0
  21. package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +1041 -0
  22. package/crates/vault-daemon/src/daemon_parts/core_helpers.rs +1256 -0
  23. package/crates/vault-daemon/src/daemon_parts/types_api_rpc.rs +622 -0
  24. package/crates/vault-daemon/src/lib.rs +54 -0
  25. package/crates/vault-daemon/src/persistence.rs +441 -0
  26. package/crates/vault-daemon/src/tests.rs +237 -0
  27. package/crates/vault-daemon/src/tests_parts/part1.rs +1224 -0
  28. package/crates/vault-daemon/src/tests_parts/part2.rs +1021 -0
  29. package/crates/vault-daemon/src/tests_parts/part3.rs +835 -0
  30. package/crates/vault-daemon/src/tests_parts/part4.rs +604 -0
  31. package/crates/vault-domain/Cargo.toml +20 -0
  32. package/crates/vault-domain/src/action.rs +849 -0
  33. package/crates/vault-domain/src/address.rs +51 -0
  34. package/crates/vault-domain/src/approval.rs +90 -0
  35. package/crates/vault-domain/src/constants.rs +4 -0
  36. package/crates/vault-domain/src/error.rs +54 -0
  37. package/crates/vault-domain/src/keys.rs +71 -0
  38. package/crates/vault-domain/src/lib.rs +42 -0
  39. package/crates/vault-domain/src/nonce.rs +102 -0
  40. package/crates/vault-domain/src/policy.rs +172 -0
  41. package/crates/vault-domain/src/request.rs +53 -0
  42. package/crates/vault-domain/src/scope.rs +24 -0
  43. package/crates/vault-domain/src/session.rs +50 -0
  44. package/crates/vault-domain/src/signature.rs +34 -0
  45. package/crates/vault-domain/src/tests.rs +651 -0
  46. package/crates/vault-domain/src/u128_as_decimal_string.rs +44 -0
  47. package/crates/vault-policy/Cargo.toml +17 -0
  48. package/crates/vault-policy/src/engine.rs +301 -0
  49. package/crates/vault-policy/src/error.rs +81 -0
  50. package/crates/vault-policy/src/lib.rs +17 -0
  51. package/crates/vault-policy/src/report.rs +34 -0
  52. package/crates/vault-policy/src/tests.rs +891 -0
  53. package/crates/vault-policy/src/tests_explain.rs +78 -0
  54. package/crates/vault-sdk-agent/Cargo.toml +21 -0
  55. package/crates/vault-sdk-agent/src/lib.rs +711 -0
  56. package/crates/vault-signer/Cargo.toml +25 -0
  57. package/crates/vault-signer/src/lib.rs +731 -0
  58. package/crates/vault-signer/tests/secure_enclave_acl.rs +54 -0
  59. package/crates/vault-transport-unix/Cargo.toml +24 -0
  60. package/crates/vault-transport-unix/src/lib.rs +1640 -0
  61. package/crates/vault-transport-xpc/Cargo.toml +25 -0
  62. package/crates/vault-transport-xpc/src/client_codec_api.rs +635 -0
  63. package/crates/vault-transport-xpc/src/lib.rs +680 -0
  64. package/crates/vault-transport-xpc/src/tests.rs +818 -0
  65. package/crates/vault-transport-xpc/tests/e2e_flow.rs +773 -0
  66. package/dist/cli.cjs +35088 -0
  67. package/dist/cli.cjs.map +1 -0
  68. package/package.json +49 -43
  69. package/packages/cache/.turbo/turbo-build.log +52 -0
  70. package/packages/cache/dist/chunk-2QFWMUXT.cjs +43 -0
  71. package/packages/cache/dist/chunk-2QFWMUXT.cjs.map +1 -0
  72. package/packages/cache/dist/chunk-4U63TZTQ.js +43 -0
  73. package/packages/cache/dist/chunk-4U63TZTQ.js.map +1 -0
  74. package/packages/cache/dist/chunk-ALQ6H7KG.cjs +404 -0
  75. package/packages/cache/dist/chunk-ALQ6H7KG.cjs.map +1 -0
  76. package/packages/cache/dist/chunk-FGJEEF5N.js +404 -0
  77. package/packages/cache/dist/chunk-FGJEEF5N.js.map +1 -0
  78. package/packages/cache/dist/chunk-UYNEHZHB.cjs +45 -0
  79. package/packages/cache/dist/chunk-UYNEHZHB.cjs.map +1 -0
  80. package/packages/cache/dist/chunk-VXVMPG3W.js +45 -0
  81. package/packages/cache/dist/chunk-VXVMPG3W.js.map +1 -0
  82. package/packages/cache/dist/client/index.cjs +11 -0
  83. package/packages/cache/dist/client/index.cjs.map +1 -0
  84. package/packages/cache/dist/client/index.d.cts +15 -0
  85. package/packages/cache/dist/client/index.d.ts +15 -0
  86. package/packages/cache/dist/client/index.js +11 -0
  87. package/packages/cache/dist/client/index.js.map +1 -0
  88. package/packages/cache/dist/errors/index.cjs +11 -0
  89. package/packages/cache/dist/errors/index.cjs.map +1 -0
  90. package/packages/cache/dist/errors/index.d.cts +26 -0
  91. package/packages/cache/dist/errors/index.d.ts +26 -0
  92. package/packages/cache/dist/errors/index.js +11 -0
  93. package/packages/cache/dist/errors/index.js.map +1 -0
  94. package/packages/cache/dist/index.cjs +29 -0
  95. package/packages/cache/dist/index.cjs.map +1 -0
  96. package/packages/cache/dist/index.d.cts +4 -0
  97. package/packages/cache/dist/index.d.ts +4 -0
  98. package/packages/cache/dist/index.js +29 -0
  99. package/packages/cache/dist/index.js.map +1 -0
  100. package/packages/cache/dist/service/index.cjs +15 -0
  101. package/packages/cache/dist/service/index.cjs.map +1 -0
  102. package/packages/cache/dist/service/index.d.cts +184 -0
  103. package/packages/cache/dist/service/index.d.ts +184 -0
  104. package/packages/cache/dist/service/index.js +15 -0
  105. package/packages/cache/dist/service/index.js.map +1 -0
  106. package/packages/cache/node_modules/.bin/jiti +17 -0
  107. package/packages/cache/node_modules/.bin/tsc +17 -0
  108. package/packages/cache/node_modules/.bin/tsserver +17 -0
  109. package/packages/cache/node_modules/.bin/tsup +17 -0
  110. package/packages/cache/node_modules/.bin/tsup-node +17 -0
  111. package/packages/cache/node_modules/.bin/tsx +17 -0
  112. package/packages/cache/node_modules/.bin/vitest +17 -0
  113. package/packages/cache/package.json +48 -0
  114. package/packages/cache/src/client/index.ts +56 -0
  115. package/packages/cache/src/errors/index.ts +53 -0
  116. package/packages/cache/src/index.ts +3 -0
  117. package/packages/cache/src/service/index.test.ts +263 -0
  118. package/packages/cache/src/service/index.ts +678 -0
  119. package/packages/cache/tsconfig.json +13 -0
  120. package/packages/cache/tsup.config.ts +13 -0
  121. package/packages/cache/vitest.config.ts +16 -0
  122. package/packages/config/.turbo/turbo-build.log +18 -0
  123. package/packages/config/dist/index.cjs +1037 -0
  124. package/packages/config/dist/index.cjs.map +1 -0
  125. package/packages/config/dist/index.d.ts +131 -0
  126. package/packages/config/node_modules/.bin/jiti +17 -0
  127. package/packages/config/node_modules/.bin/tsc +17 -0
  128. package/packages/config/node_modules/.bin/tsserver +17 -0
  129. package/packages/config/node_modules/.bin/tsup +17 -0
  130. package/packages/config/node_modules/.bin/tsup-node +17 -0
  131. package/packages/config/node_modules/.bin/tsx +17 -0
  132. package/packages/config/package.json +21 -0
  133. package/packages/config/src/index.js +1 -0
  134. package/packages/config/src/index.ts +1282 -0
  135. package/packages/config/tsconfig.json +4 -0
  136. package/packages/rpc/.turbo/turbo-build.log +32 -0
  137. package/packages/rpc/dist/_esm-BCLXDO2R.cjs +3660 -0
  138. package/packages/rpc/dist/_esm-BCLXDO2R.cjs.map +1 -0
  139. package/packages/rpc/dist/ccip-OWJLAW55.cjs +16 -0
  140. package/packages/rpc/dist/ccip-OWJLAW55.cjs.map +1 -0
  141. package/packages/rpc/dist/chunk-APQIFZ3B.cjs +6247 -0
  142. package/packages/rpc/dist/chunk-APQIFZ3B.cjs.map +1 -0
  143. package/packages/rpc/dist/chunk-CDO2GWRD.cjs +410 -0
  144. package/packages/rpc/dist/chunk-CDO2GWRD.cjs.map +1 -0
  145. package/packages/rpc/dist/chunk-QGTNTFJ7.cjs +2249 -0
  146. package/packages/rpc/dist/chunk-QGTNTFJ7.cjs.map +1 -0
  147. package/packages/rpc/dist/chunk-TZDTAHWR.cjs +44 -0
  148. package/packages/rpc/dist/chunk-TZDTAHWR.cjs.map +1 -0
  149. package/packages/rpc/dist/index.cjs +7342 -0
  150. package/packages/rpc/dist/index.cjs.map +1 -0
  151. package/packages/rpc/dist/index.d.ts +3857 -0
  152. package/packages/rpc/dist/secp256k1-WCNM675D.cjs +18 -0
  153. package/packages/rpc/dist/secp256k1-WCNM675D.cjs.map +1 -0
  154. package/packages/rpc/node_modules/.bin/jiti +17 -0
  155. package/packages/rpc/node_modules/.bin/tsc +17 -0
  156. package/packages/rpc/node_modules/.bin/tsserver +17 -0
  157. package/packages/rpc/node_modules/.bin/tsup +17 -0
  158. package/packages/rpc/node_modules/.bin/tsup-node +17 -0
  159. package/packages/rpc/node_modules/.bin/tsx +17 -0
  160. package/packages/rpc/package.json +25 -0
  161. package/packages/rpc/src/index.ts +206 -0
  162. package/packages/rpc/tsconfig.json +4 -0
  163. package/packages/typescript/base.json +36 -0
  164. package/packages/typescript/nextjs.json +17 -0
  165. package/packages/typescript/package.json +10 -0
  166. package/packages/ui/.turbo/turbo-build.log +44 -0
  167. package/packages/ui/dist/chunk-MOAFBKSA.js +11 -0
  168. package/packages/ui/dist/chunk-MOAFBKSA.js.map +1 -0
  169. package/packages/ui/dist/components/badge.d.ts +12 -0
  170. package/packages/ui/dist/components/badge.js +31 -0
  171. package/packages/ui/dist/components/badge.js.map +1 -0
  172. package/packages/ui/dist/components/button.d.ts +13 -0
  173. package/packages/ui/dist/components/button.js +40 -0
  174. package/packages/ui/dist/components/button.js.map +1 -0
  175. package/packages/ui/dist/components/card.d.ts +10 -0
  176. package/packages/ui/dist/components/card.js +39 -0
  177. package/packages/ui/dist/components/card.js.map +1 -0
  178. package/packages/ui/dist/components/input.d.ts +5 -0
  179. package/packages/ui/dist/components/input.js +28 -0
  180. package/packages/ui/dist/components/input.js.map +1 -0
  181. package/packages/ui/dist/components/label.d.ts +5 -0
  182. package/packages/ui/dist/components/label.js +13 -0
  183. package/packages/ui/dist/components/label.js.map +1 -0
  184. package/packages/ui/dist/components/separator.d.ts +5 -0
  185. package/packages/ui/dist/components/separator.js +13 -0
  186. package/packages/ui/dist/components/separator.js.map +1 -0
  187. package/packages/ui/dist/components/textarea.d.ts +5 -0
  188. package/packages/ui/dist/components/textarea.js +27 -0
  189. package/packages/ui/dist/components/textarea.js.map +1 -0
  190. package/packages/ui/dist/tailwind.d.ts +56 -0
  191. package/packages/ui/dist/tailwind.js +60 -0
  192. package/packages/ui/dist/tailwind.js.map +1 -0
  193. package/packages/ui/dist/utils/cn.d.ts +5 -0
  194. package/packages/ui/dist/utils/cn.js +7 -0
  195. package/packages/ui/dist/utils/cn.js.map +1 -0
  196. package/packages/ui/node_modules/.bin/jiti +17 -0
  197. package/packages/ui/node_modules/.bin/tsc +17 -0
  198. package/packages/ui/node_modules/.bin/tsserver +17 -0
  199. package/packages/ui/node_modules/.bin/tsup +17 -0
  200. package/packages/ui/node_modules/.bin/tsup-node +17 -0
  201. package/packages/ui/node_modules/.bin/tsx +17 -0
  202. package/packages/ui/package.json +69 -0
  203. package/packages/ui/src/components/badge.tsx +27 -0
  204. package/packages/ui/src/components/button.tsx +40 -0
  205. package/packages/ui/src/components/card.tsx +31 -0
  206. package/packages/ui/src/components/input.tsx +21 -0
  207. package/packages/ui/src/components/label.tsx +6 -0
  208. package/packages/ui/src/components/separator.tsx +6 -0
  209. package/packages/ui/src/components/textarea.tsx +20 -0
  210. package/packages/ui/src/globals.css +70 -0
  211. package/packages/ui/src/tailwind.ts +56 -0
  212. package/packages/ui/src/utils/cn.ts +6 -0
  213. package/packages/ui/tsconfig.json +20 -0
  214. package/packages/ui/tsup.config.ts +20 -0
  215. package/pnpm-workspace.yaml +4 -0
  216. package/scripts/install-rust-binaries.mjs +84 -0
  217. package/scripts/launchd/install-user-daemon.sh +358 -0
  218. package/scripts/launchd/run-vault-daemon.sh +5 -0
  219. package/scripts/launchd/run-wlfi-agent-daemon.sh +73 -0
  220. package/scripts/launchd/uninstall-user-daemon.sh +103 -0
  221. package/src/cli.ts +2121 -0
  222. package/src/lib/admin-guard.js +1 -0
  223. package/src/lib/admin-guard.ts +185 -0
  224. package/src/lib/admin-passthrough.ts +33 -0
  225. package/src/lib/admin-reset.ts +751 -0
  226. package/src/lib/admin-setup.ts +1612 -0
  227. package/src/lib/agent-auth-clear.js +1 -0
  228. package/src/lib/agent-auth-clear.ts +58 -0
  229. package/src/lib/agent-auth-forwarding.js +1 -0
  230. package/src/lib/agent-auth-forwarding.ts +149 -0
  231. package/src/lib/agent-auth-migrate.js +1 -0
  232. package/src/lib/agent-auth-migrate.ts +150 -0
  233. package/src/lib/agent-auth-revoke.ts +103 -0
  234. package/src/lib/agent-auth-rotate.ts +107 -0
  235. package/src/lib/agent-auth-token.js +1 -0
  236. package/src/lib/agent-auth-token.ts +25 -0
  237. package/src/lib/agent-auth.ts +89 -0
  238. package/src/lib/asset-broadcast.js +1 -0
  239. package/src/lib/asset-broadcast.ts +285 -0
  240. package/src/lib/bootstrap-artifacts.js +1 -0
  241. package/src/lib/bootstrap-artifacts.ts +205 -0
  242. package/src/lib/bootstrap-credentials.js +1 -0
  243. package/src/lib/bootstrap-credentials.ts +832 -0
  244. package/src/lib/config-amounts.js +1 -0
  245. package/src/lib/config-amounts.ts +189 -0
  246. package/src/lib/config-mutation.ts +27 -0
  247. package/src/lib/fs-trust.js +1 -0
  248. package/src/lib/fs-trust.ts +537 -0
  249. package/src/lib/keychain.js +1 -0
  250. package/src/lib/keychain.ts +225 -0
  251. package/src/lib/local-admin-access.ts +106 -0
  252. package/src/lib/network-selection.js +1 -0
  253. package/src/lib/network-selection.ts +71 -0
  254. package/src/lib/passthrough-security.js +1 -0
  255. package/src/lib/passthrough-security.ts +114 -0
  256. package/src/lib/rpc-guard.js +1 -0
  257. package/src/lib/rpc-guard.ts +7 -0
  258. package/src/lib/rust-spawn-options.js +1 -0
  259. package/src/lib/rust-spawn-options.ts +98 -0
  260. package/src/lib/rust.js +1 -0
  261. package/src/lib/rust.ts +143 -0
  262. package/src/lib/signed-tx.js +1 -0
  263. package/src/lib/signed-tx.ts +116 -0
  264. package/src/lib/status-repair-cli.ts +116 -0
  265. package/src/lib/sudo.js +1 -0
  266. package/src/lib/sudo.ts +172 -0
  267. package/src/lib/vault-password-forwarding.js +1 -0
  268. package/src/lib/vault-password-forwarding.ts +155 -0
  269. package/src/lib/wallet-profile.js +1 -0
  270. package/src/lib/wallet-profile.ts +332 -0
  271. package/src/lib/wallet-repair.js +1 -0
  272. package/src/lib/wallet-repair.ts +304 -0
  273. package/src/lib/wallet-setup.js +1 -0
  274. package/src/lib/wallet-setup.ts +1466 -0
  275. package/src/lib/wallet-status.js +1 -0
  276. package/src/lib/wallet-status.ts +640 -0
  277. package/tsconfig.base.json +17 -0
  278. package/tsconfig.json +10 -0
  279. package/tsup.config.ts +25 -0
  280. package/turbo.json +41 -0
  281. package/LICENSE.md +0 -1
  282. package/dist/wlfa/index.cjs +0 -250
  283. package/dist/wlfa/index.d.cts +0 -1
  284. package/dist/wlfa/index.d.ts +0 -1
  285. package/dist/wlfa/index.js +0 -250
  286. package/dist/wlfc/index.cjs +0 -1839
  287. package/dist/wlfc/index.d.cts +0 -1
  288. package/dist/wlfc/index.d.ts +0 -1
  289. package/dist/wlfc/index.js +0 -1839
@@ -0,0 +1,1224 @@
1
+ #[tokio::test]
2
+ async fn daemon_enforces_per_tx_limit() {
3
+ let daemon = InMemoryDaemon::new(
4
+ "vault-password",
5
+ SoftwareSignerBackend::default(),
6
+ DaemonConfig::default(),
7
+ )
8
+ .expect("daemon");
9
+
10
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
11
+ let session = AdminSession {
12
+ vault_password: "vault-password".to_string(),
13
+ lease,
14
+ };
15
+
16
+ daemon
17
+ .add_policy(&session, policy_all_per_tx(100))
18
+ .await
19
+ .expect("add policy");
20
+
21
+ let key = daemon
22
+ .create_vault_key(&session, KeyCreateRequest::Generate)
23
+ .await
24
+ .expect("key");
25
+ assert!(matches!(key.source, KeySource::Generated));
26
+
27
+ let agent_credentials = daemon
28
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
29
+ .await
30
+ .expect("agent");
31
+
32
+ let token = "0x1000000000000000000000000000000000000000"
33
+ .parse::<EvmAddress>()
34
+ .expect("token");
35
+ let recipient = "0x2000000000000000000000000000000000000000"
36
+ .parse::<EvmAddress>()
37
+ .expect("recipient");
38
+ let request = sign_request(
39
+ &agent_credentials,
40
+ AgentAction::Transfer {
41
+ chain_id: 1,
42
+ token,
43
+ to: recipient,
44
+ amount_wei: 101,
45
+ },
46
+ );
47
+
48
+ let result = daemon.sign_for_agent(request).await;
49
+ assert!(result.is_err());
50
+ }
51
+
52
+ #[tokio::test]
53
+ async fn wrong_password_is_rejected() {
54
+ let daemon = InMemoryDaemon::new(
55
+ "vault-password",
56
+ SoftwareSignerBackend::default(),
57
+ DaemonConfig::default(),
58
+ )
59
+ .expect("daemon");
60
+
61
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
62
+ let bad_session = AdminSession {
63
+ vault_password: "wrong".to_string(),
64
+ lease,
65
+ };
66
+
67
+ let err = daemon
68
+ .add_policy(&bad_session, policy_all_per_tx(100))
69
+ .await
70
+ .expect_err("must reject bad session");
71
+
72
+ assert!(matches!(err, DaemonError::AuthenticationFailed));
73
+ }
74
+
75
+ #[tokio::test]
76
+ async fn issue_lease_requires_correct_password() {
77
+ let daemon = InMemoryDaemon::new(
78
+ "vault-password",
79
+ SoftwareSignerBackend::default(),
80
+ DaemonConfig::default(),
81
+ )
82
+ .expect("daemon");
83
+
84
+ let err = daemon
85
+ .issue_lease("wrong-password")
86
+ .await
87
+ .expect_err("must reject bad password");
88
+ assert!(matches!(err, DaemonError::AuthenticationFailed));
89
+ }
90
+
91
+ #[tokio::test]
92
+ async fn oversized_admin_password_is_rejected() {
93
+ let daemon = InMemoryDaemon::new(
94
+ "vault-password",
95
+ SoftwareSignerBackend::default(),
96
+ DaemonConfig::default(),
97
+ )
98
+ .expect("daemon");
99
+
100
+ let err = daemon
101
+ .issue_lease(&"a".repeat((16 * 1024) + 1))
102
+ .await
103
+ .expect_err("must reject oversized password");
104
+ assert!(matches!(err, DaemonError::AuthenticationFailed));
105
+ }
106
+
107
+ #[tokio::test]
108
+ async fn repeated_failed_admin_password_attempts_trigger_temporary_lockout() {
109
+ let config = DaemonConfig {
110
+ max_failed_admin_auth_attempts: 2,
111
+ admin_auth_lockout: time::Duration::hours(1),
112
+ ..DaemonConfig::default()
113
+ };
114
+ let daemon = InMemoryDaemon::new("vault-password", SoftwareSignerBackend::default(), config)
115
+ .expect("daemon");
116
+
117
+ let first = daemon
118
+ .issue_lease("wrong-password")
119
+ .await
120
+ .expect_err("first bad password must fail");
121
+ assert!(matches!(first, DaemonError::AuthenticationFailed));
122
+
123
+ let second = daemon
124
+ .issue_lease("wrong-password")
125
+ .await
126
+ .expect_err("second bad password must fail");
127
+ assert!(matches!(second, DaemonError::AuthenticationFailed));
128
+
129
+ let locked = daemon
130
+ .issue_lease("vault-password")
131
+ .await
132
+ .expect_err("lockout should reject even correct password until it expires");
133
+ assert!(matches!(locked, DaemonError::AuthenticationFailed));
134
+ }
135
+
136
+ #[tokio::test]
137
+ async fn successful_admin_auth_resets_failed_password_counter() {
138
+ let config = DaemonConfig {
139
+ max_failed_admin_auth_attempts: 2,
140
+ admin_auth_lockout: time::Duration::hours(1),
141
+ ..DaemonConfig::default()
142
+ };
143
+ let daemon = InMemoryDaemon::new("vault-password", SoftwareSignerBackend::default(), config)
144
+ .expect("daemon");
145
+
146
+ let first = daemon
147
+ .issue_lease("wrong-password")
148
+ .await
149
+ .expect_err("bad password must fail");
150
+ assert!(matches!(first, DaemonError::AuthenticationFailed));
151
+
152
+ daemon
153
+ .issue_lease("vault-password")
154
+ .await
155
+ .expect("correct password should reset failure counter");
156
+
157
+ let next = daemon
158
+ .issue_lease("wrong-password")
159
+ .await
160
+ .expect_err("later bad password must still fail without immediate lockout");
161
+ assert!(matches!(next, DaemonError::AuthenticationFailed));
162
+
163
+ daemon
164
+ .issue_lease("vault-password")
165
+ .await
166
+ .expect("single later failure should not lock out correct password");
167
+ }
168
+
169
+ #[tokio::test]
170
+ async fn create_agent_key_validates_policy_set() {
171
+ let daemon = InMemoryDaemon::new(
172
+ "vault-password",
173
+ SoftwareSignerBackend::default(),
174
+ DaemonConfig::default(),
175
+ )
176
+ .expect("daemon");
177
+
178
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
179
+ let session = AdminSession {
180
+ vault_password: "vault-password".to_string(),
181
+ lease,
182
+ };
183
+
184
+ let key = daemon
185
+ .create_vault_key(&session, KeyCreateRequest::Generate)
186
+ .await
187
+ .expect("key");
188
+
189
+ let mut ids = BTreeSet::new();
190
+ ids.insert(Uuid::new_v4());
191
+
192
+ let err = daemon
193
+ .create_agent_key(&session, key.id, PolicyAttachment::PolicySet(ids))
194
+ .await
195
+ .expect_err("unknown policy must fail");
196
+
197
+ assert!(matches!(err, DaemonError::UnknownPolicy(_)));
198
+ }
199
+
200
+ #[tokio::test]
201
+ async fn create_agent_key_rejects_empty_policy_set() {
202
+ let daemon = InMemoryDaemon::new(
203
+ "vault-password",
204
+ SoftwareSignerBackend::default(),
205
+ DaemonConfig::default(),
206
+ )
207
+ .expect("daemon");
208
+
209
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
210
+ let session = AdminSession {
211
+ vault_password: "vault-password".to_string(),
212
+ lease,
213
+ };
214
+
215
+ let key = daemon
216
+ .create_vault_key(&session, KeyCreateRequest::Generate)
217
+ .await
218
+ .expect("key");
219
+
220
+ let empty = std::collections::BTreeSet::new();
221
+ let err = daemon
222
+ .create_agent_key(&session, key.id, PolicyAttachment::PolicySet(empty))
223
+ .await
224
+ .expect_err("empty policy set must fail");
225
+
226
+ assert!(matches!(err, DaemonError::InvalidPolicyAttachment(_)));
227
+ }
228
+
229
+ #[tokio::test]
230
+ async fn daemon_backfills_default_relay_url() {
231
+ let daemon = InMemoryDaemon::new(
232
+ "vault-password",
233
+ SoftwareSignerBackend::default(),
234
+ DaemonConfig::default(),
235
+ )
236
+ .expect("daemon");
237
+
238
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
239
+ let session = AdminSession {
240
+ vault_password: "vault-password".to_string(),
241
+ lease,
242
+ };
243
+
244
+ let relay_config = daemon
245
+ .get_relay_config(&session)
246
+ .await
247
+ .expect("get relay config");
248
+
249
+ assert_eq!(
250
+ relay_config.relay_url.as_deref(),
251
+ Some("http://localhost:8787")
252
+ );
253
+ assert_eq!(relay_config.frontend_url, None);
254
+ assert!(!relay_config.daemon_id_hex.trim().is_empty());
255
+ assert!(!relay_config.daemon_public_key_hex.trim().is_empty());
256
+ }
257
+
258
+ #[tokio::test]
259
+ async fn set_relay_config_allows_loopback_http_and_remote_https() {
260
+ let daemon = InMemoryDaemon::new(
261
+ "vault-password",
262
+ SoftwareSignerBackend::default(),
263
+ DaemonConfig::default(),
264
+ )
265
+ .expect("daemon");
266
+
267
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
268
+ let session = AdminSession {
269
+ vault_password: "vault-password".to_string(),
270
+ lease,
271
+ };
272
+
273
+ let relay_config = daemon
274
+ .set_relay_config(
275
+ &session,
276
+ Some("http://127.0.0.1:8787".to_string()),
277
+ Some("https://relay.example".to_string()),
278
+ )
279
+ .await
280
+ .expect("set relay config");
281
+
282
+ assert_eq!(
283
+ relay_config.relay_url.as_deref(),
284
+ Some("http://127.0.0.1:8787")
285
+ );
286
+ assert_eq!(
287
+ relay_config.frontend_url.as_deref(),
288
+ Some("https://relay.example")
289
+ );
290
+ }
291
+
292
+ #[tokio::test]
293
+ async fn set_relay_config_rejects_remote_http() {
294
+ let daemon = InMemoryDaemon::new(
295
+ "vault-password",
296
+ SoftwareSignerBackend::default(),
297
+ DaemonConfig::default(),
298
+ )
299
+ .expect("daemon");
300
+
301
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
302
+ let session = AdminSession {
303
+ vault_password: "vault-password".to_string(),
304
+ lease,
305
+ };
306
+
307
+ let err = daemon
308
+ .set_relay_config(&session, Some("http://relay.example".to_string()), None)
309
+ .await
310
+ .expect_err("remote http relay URL must fail");
311
+
312
+ assert!(
313
+ matches!(err, DaemonError::InvalidRelayConfig(message) if message.contains("must use https unless it targets localhost or a loopback address"))
314
+ );
315
+ }
316
+
317
+ #[tokio::test]
318
+ async fn set_relay_config_rejects_userinfo_query_or_fragment() {
319
+ let daemon = InMemoryDaemon::new(
320
+ "vault-password",
321
+ SoftwareSignerBackend::default(),
322
+ DaemonConfig::default(),
323
+ )
324
+ .expect("daemon");
325
+
326
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
327
+ let session = AdminSession {
328
+ vault_password: "vault-password".to_string(),
329
+ lease,
330
+ };
331
+
332
+ let userinfo_err = daemon
333
+ .set_relay_config(
334
+ &session,
335
+ Some("https://admin:secret@relay.example".to_string()),
336
+ None,
337
+ )
338
+ .await
339
+ .expect_err("userinfo must fail");
340
+ assert!(
341
+ matches!(userinfo_err, DaemonError::InvalidRelayConfig(message) if message.contains("must not include embedded username or password"))
342
+ );
343
+
344
+ let query_err = daemon
345
+ .set_relay_config(
346
+ &session,
347
+ None,
348
+ Some("https://relay.example/ui?debug=1".to_string()),
349
+ )
350
+ .await
351
+ .expect_err("query must fail");
352
+ assert!(
353
+ matches!(query_err, DaemonError::InvalidRelayConfig(message) if message.contains("must not include a query string"))
354
+ );
355
+
356
+ let fragment_err = daemon
357
+ .set_relay_config(
358
+ &session,
359
+ None,
360
+ Some("https://relay.example/ui#approval".to_string()),
361
+ )
362
+ .await
363
+ .expect_err("fragment must fail");
364
+ assert!(
365
+ matches!(fragment_err, DaemonError::InvalidRelayConfig(message) if message.contains("must not include a fragment"))
366
+ );
367
+ }
368
+
369
+ #[tokio::test]
370
+ async fn rpc_roundtrip_for_issue_lease() {
371
+ let daemon = InMemoryDaemon::new(
372
+ "vault-password",
373
+ SoftwareSignerBackend::default(),
374
+ DaemonConfig::default(),
375
+ )
376
+ .expect("daemon");
377
+
378
+ let response = daemon
379
+ .handle_rpc(DaemonRpcRequest::IssueLease {
380
+ vault_password: "vault-password".to_string(),
381
+ })
382
+ .await
383
+ .expect("rpc must succeed");
384
+
385
+ match response {
386
+ DaemonRpcResponse::Lease(lease) => {
387
+ assert!(lease.expires_at > lease.issued_at);
388
+ }
389
+ _ => panic!("unexpected rpc response"),
390
+ }
391
+ }
392
+
393
+ #[tokio::test]
394
+ async fn list_policies_requires_authenticated_session() {
395
+ let daemon = InMemoryDaemon::new(
396
+ "vault-password",
397
+ SoftwareSignerBackend::default(),
398
+ DaemonConfig::default(),
399
+ )
400
+ .expect("daemon");
401
+
402
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
403
+ let bad_session = AdminSession {
404
+ vault_password: "wrong".to_string(),
405
+ lease,
406
+ };
407
+
408
+ let err = daemon
409
+ .list_policies(&bad_session)
410
+ .await
411
+ .expect_err("must reject unauthenticated list request");
412
+ assert!(matches!(err, DaemonError::AuthenticationFailed));
413
+ }
414
+
415
+ #[tokio::test]
416
+ async fn add_policy_rejects_invalid_policy_payloads() {
417
+ let daemon = InMemoryDaemon::new(
418
+ "vault-password",
419
+ SoftwareSignerBackend::default(),
420
+ DaemonConfig::default(),
421
+ )
422
+ .expect("daemon");
423
+
424
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
425
+ let session = AdminSession {
426
+ vault_password: "vault-password".to_string(),
427
+ lease,
428
+ };
429
+
430
+ let zero_max = SpendingPolicy {
431
+ id: Uuid::new_v4(),
432
+ priority: 0,
433
+ policy_type: PolicyType::PerTxMaxSpending,
434
+ min_amount_wei: None,
435
+ max_amount_wei: 0,
436
+ recipients: EntityScope::All,
437
+ assets: EntityScope::All,
438
+ networks: EntityScope::All,
439
+ enabled: true,
440
+ };
441
+ let err = daemon
442
+ .add_policy(&session, zero_max)
443
+ .await
444
+ .expect_err("zero max amount must be rejected");
445
+ assert!(matches!(err, DaemonError::InvalidPolicy(_)));
446
+
447
+ let empty_recipient_scope = SpendingPolicy {
448
+ id: Uuid::new_v4(),
449
+ priority: 0,
450
+ policy_type: PolicyType::PerTxMaxSpending,
451
+ min_amount_wei: None,
452
+ max_amount_wei: 1,
453
+ recipients: EntityScope::Set(BTreeSet::new()),
454
+ assets: EntityScope::All,
455
+ networks: EntityScope::All,
456
+ enabled: true,
457
+ };
458
+ let err = daemon
459
+ .add_policy(&session, empty_recipient_scope)
460
+ .await
461
+ .expect_err("empty recipient scope must be rejected");
462
+ assert!(matches!(err, DaemonError::InvalidPolicy(_)));
463
+
464
+ let empty_asset_scope = SpendingPolicy {
465
+ id: Uuid::new_v4(),
466
+ priority: 0,
467
+ policy_type: PolicyType::PerTxMaxSpending,
468
+ min_amount_wei: None,
469
+ max_amount_wei: 1,
470
+ recipients: EntityScope::All,
471
+ assets: EntityScope::Set(BTreeSet::new()),
472
+ networks: EntityScope::All,
473
+ enabled: true,
474
+ };
475
+ let err = daemon
476
+ .add_policy(&session, empty_asset_scope)
477
+ .await
478
+ .expect_err("empty asset scope must be rejected");
479
+ assert!(matches!(err, DaemonError::InvalidPolicy(_)));
480
+ }
481
+
482
+ #[tokio::test]
483
+ async fn list_policies_is_deterministic_for_equal_priority() {
484
+ let daemon = InMemoryDaemon::new(
485
+ "vault-password",
486
+ SoftwareSignerBackend::default(),
487
+ DaemonConfig::default(),
488
+ )
489
+ .expect("daemon");
490
+
491
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
492
+ let session = AdminSession {
493
+ vault_password: "vault-password".to_string(),
494
+ lease,
495
+ };
496
+
497
+ let mut high_id = policy_all_per_tx(100);
498
+ high_id.priority = 7;
499
+ high_id.id = Uuid::parse_str("ffffffff-ffff-ffff-ffff-ffffffffffff").expect("uuid");
500
+
501
+ let mut low_id = policy_all_per_tx(100);
502
+ low_id.priority = 7;
503
+ low_id.id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").expect("uuid");
504
+
505
+ daemon
506
+ .add_policy(&session, high_id.clone())
507
+ .await
508
+ .expect("add high id");
509
+ daemon
510
+ .add_policy(&session, low_id.clone())
511
+ .await
512
+ .expect("add low id");
513
+
514
+ let listed = daemon.list_policies(&session).await.expect("list policies");
515
+ let equal_priority: Vec<Uuid> = listed
516
+ .into_iter()
517
+ .filter(|p| p.priority == 7)
518
+ .map(|p| p.id)
519
+ .collect();
520
+ assert_eq!(equal_priority, vec![low_id.id, high_id.id]);
521
+ }
522
+
523
+ #[tokio::test]
524
+ async fn lease_capacity_is_enforced() {
525
+ let config = DaemonConfig {
526
+ max_active_leases: 1,
527
+ ..DaemonConfig::default()
528
+ };
529
+ let daemon = InMemoryDaemon::new("vault-password", SoftwareSignerBackend::default(), config)
530
+ .expect("daemon");
531
+
532
+ daemon
533
+ .issue_lease("vault-password")
534
+ .await
535
+ .expect("first lease");
536
+ let err = daemon
537
+ .issue_lease("vault-password")
538
+ .await
539
+ .expect_err("second lease must fail at capacity");
540
+ assert!(matches!(err, DaemonError::TooManyActiveLeases));
541
+ }
542
+
543
+ #[tokio::test]
544
+ async fn expired_leases_are_pruned_before_capacity_check() {
545
+ let config = DaemonConfig {
546
+ max_active_leases: 1,
547
+ ..DaemonConfig::default()
548
+ };
549
+ let daemon = InMemoryDaemon::new("vault-password", SoftwareSignerBackend::default(), config)
550
+ .expect("daemon");
551
+
552
+ let now = time::OffsetDateTime::now_utc();
553
+ let expired_id = Uuid::new_v4();
554
+ daemon.leases.write().expect("leases write").insert(
555
+ expired_id,
556
+ Lease {
557
+ lease_id: expired_id,
558
+ issued_at: now - time::Duration::hours(2),
559
+ expires_at: now - time::Duration::hours(1),
560
+ },
561
+ );
562
+
563
+ let fresh = daemon
564
+ .issue_lease("vault-password")
565
+ .await
566
+ .expect("must prune and issue");
567
+ assert_eq!(daemon.leases.read().expect("leases read").len(), 1);
568
+ assert_eq!(
569
+ daemon
570
+ .leases
571
+ .read()
572
+ .expect("leases read")
573
+ .keys()
574
+ .next()
575
+ .copied(),
576
+ Some(fresh.lease_id)
577
+ );
578
+ }
579
+
580
+ #[tokio::test]
581
+ async fn not_yet_valid_leases_are_pruned_before_capacity_check() {
582
+ let config = DaemonConfig {
583
+ max_active_leases: 1,
584
+ ..DaemonConfig::default()
585
+ };
586
+ let daemon = InMemoryDaemon::new("vault-password", SoftwareSignerBackend::default(), config)
587
+ .expect("daemon");
588
+
589
+ let now = time::OffsetDateTime::now_utc();
590
+ let future_id = Uuid::new_v4();
591
+ daemon.leases.write().expect("leases write").insert(
592
+ future_id,
593
+ Lease {
594
+ lease_id: future_id,
595
+ issued_at: now + time::Duration::hours(2),
596
+ expires_at: now + time::Duration::hours(3),
597
+ },
598
+ );
599
+
600
+ let fresh = daemon
601
+ .issue_lease("vault-password")
602
+ .await
603
+ .expect("must prune invalid future lease and issue");
604
+ assert_eq!(daemon.leases.read().expect("leases read").len(), 1);
605
+ assert_eq!(
606
+ daemon
607
+ .leases
608
+ .read()
609
+ .expect("leases read")
610
+ .keys()
611
+ .next()
612
+ .copied(),
613
+ Some(fresh.lease_id)
614
+ );
615
+ }
616
+
617
+ #[test]
618
+ fn daemon_new_rejects_invalid_runtime_limits() {
619
+ let config = DaemonConfig {
620
+ max_active_leases: 0,
621
+ ..DaemonConfig::default()
622
+ };
623
+ let result = InMemoryDaemon::new("vault-password", SoftwareSignerBackend::default(), config);
624
+ assert!(
625
+ matches!(result, Err(DaemonError::InvalidConfig(_))),
626
+ "zero lease capacity must be rejected"
627
+ );
628
+
629
+ let config = DaemonConfig {
630
+ max_sign_payload_bytes: 0,
631
+ ..DaemonConfig::default()
632
+ };
633
+ let result = InMemoryDaemon::new("vault-password", SoftwareSignerBackend::default(), config);
634
+ assert!(
635
+ matches!(result, Err(DaemonError::InvalidConfig(_))),
636
+ "zero payload cap must be rejected"
637
+ );
638
+
639
+ let config = DaemonConfig {
640
+ max_request_ttl: time::Duration::ZERO,
641
+ ..DaemonConfig::default()
642
+ };
643
+ let result = InMemoryDaemon::new("vault-password", SoftwareSignerBackend::default(), config);
644
+ assert!(
645
+ matches!(result, Err(DaemonError::InvalidConfig(_))),
646
+ "non-positive request ttl must be rejected"
647
+ );
648
+
649
+ let config = DaemonConfig {
650
+ nonce_reservation_ttl: time::Duration::ZERO,
651
+ ..DaemonConfig::default()
652
+ };
653
+ let result = InMemoryDaemon::new("vault-password", SoftwareSignerBackend::default(), config);
654
+ assert!(
655
+ matches!(result, Err(DaemonError::InvalidConfig(_))),
656
+ "non-positive nonce reservation ttl must be rejected"
657
+ );
658
+
659
+ let config = DaemonConfig {
660
+ max_failed_admin_auth_attempts: 0,
661
+ ..DaemonConfig::default()
662
+ };
663
+ let result = InMemoryDaemon::new("vault-password", SoftwareSignerBackend::default(), config);
664
+ assert!(
665
+ matches!(result, Err(DaemonError::InvalidConfig(_))),
666
+ "zero admin auth attempt budget must be rejected"
667
+ );
668
+
669
+ let config = DaemonConfig {
670
+ admin_auth_lockout: time::Duration::ZERO,
671
+ ..DaemonConfig::default()
672
+ };
673
+ let result = InMemoryDaemon::new("vault-password", SoftwareSignerBackend::default(), config);
674
+ assert!(
675
+ matches!(result, Err(DaemonError::InvalidConfig(_))),
676
+ "non-positive admin auth lockout must be rejected"
677
+ );
678
+
679
+ let config = DaemonConfig {
680
+ lease_ttl: time::Duration::ZERO,
681
+ ..DaemonConfig::default()
682
+ };
683
+ let result = InMemoryDaemon::new("vault-password", SoftwareSignerBackend::default(), config);
684
+ assert!(
685
+ matches!(result, Err(DaemonError::InvalidConfig(_))),
686
+ "non-positive lease ttl must be rejected"
687
+ );
688
+ }
689
+
690
+ #[tokio::test]
691
+ async fn issue_lease_fails_closed_when_ttl_overflows_timestamp() {
692
+ let config = DaemonConfig {
693
+ lease_ttl: time::Duration::MAX,
694
+ ..DaemonConfig::default()
695
+ };
696
+ let daemon = InMemoryDaemon::new("vault-password", SoftwareSignerBackend::default(), config)
697
+ .expect("daemon should construct");
698
+
699
+ let err = daemon
700
+ .issue_lease("vault-password")
701
+ .await
702
+ .expect_err("overflowing ttl must fail safely");
703
+ assert!(matches!(err, DaemonError::InvalidConfig(_)));
704
+ }
705
+
706
+ #[tokio::test]
707
+ async fn client_requested_at_cannot_bypass_window_limits() {
708
+ let daemon = InMemoryDaemon::new(
709
+ "vault-password",
710
+ SoftwareSignerBackend::default(),
711
+ DaemonConfig::default(),
712
+ )
713
+ .expect("daemon");
714
+
715
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
716
+ let session = AdminSession {
717
+ vault_password: "vault-password".to_string(),
718
+ lease,
719
+ };
720
+
721
+ let policy = SpendingPolicy::new(
722
+ 0,
723
+ PolicyType::DailyMaxSpending,
724
+ 100,
725
+ EntityScope::All,
726
+ EntityScope::All,
727
+ EntityScope::All,
728
+ )
729
+ .expect("policy");
730
+ daemon
731
+ .add_policy(&session, policy)
732
+ .await
733
+ .expect("add policy");
734
+
735
+ let key = daemon
736
+ .create_vault_key(&session, KeyCreateRequest::Generate)
737
+ .await
738
+ .expect("key");
739
+ let agent_credentials = daemon
740
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
741
+ .await
742
+ .expect("agent");
743
+
744
+ let token = "0x3000000000000000000000000000000000000000"
745
+ .parse::<EvmAddress>()
746
+ .expect("token");
747
+ let recipient = "0x4000000000000000000000000000000000000000"
748
+ .parse::<EvmAddress>()
749
+ .expect("recipient");
750
+
751
+ let mut first = sign_request(
752
+ &agent_credentials,
753
+ AgentAction::Transfer {
754
+ chain_id: 1,
755
+ token: token.clone(),
756
+ to: recipient.clone(),
757
+ amount_wei: 60,
758
+ },
759
+ );
760
+ // Intentionally stale; daemon must not trust this for spend accounting.
761
+ first.requested_at = time::OffsetDateTime::now_utc() - time::Duration::seconds(30);
762
+ daemon.sign_for_agent(first).await.expect("first sign");
763
+
764
+ let mut second = sign_request(
765
+ &agent_credentials,
766
+ AgentAction::Transfer {
767
+ chain_id: 1,
768
+ token,
769
+ to: recipient,
770
+ amount_wei: 60,
771
+ },
772
+ );
773
+ second.requested_at = time::OffsetDateTime::now_utc() - time::Duration::seconds(30);
774
+ let err = daemon
775
+ .sign_for_agent(second)
776
+ .await
777
+ .expect_err("daily limit must reject second request");
778
+
779
+ assert!(matches!(err, DaemonError::Policy(_)));
780
+ }
781
+
782
+ #[tokio::test]
783
+ async fn wrong_agent_auth_token_is_rejected() {
784
+ let daemon = InMemoryDaemon::new(
785
+ "vault-password",
786
+ SoftwareSignerBackend::default(),
787
+ DaemonConfig::default(),
788
+ )
789
+ .expect("daemon");
790
+
791
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
792
+ let session = AdminSession {
793
+ vault_password: "vault-password".to_string(),
794
+ lease,
795
+ };
796
+ daemon
797
+ .add_policy(&session, policy_all_per_tx(100))
798
+ .await
799
+ .expect("add policy");
800
+
801
+ let key = daemon
802
+ .create_vault_key(&session, KeyCreateRequest::Generate)
803
+ .await
804
+ .expect("key");
805
+ let mut agent_credentials = daemon
806
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
807
+ .await
808
+ .expect("agent");
809
+ agent_credentials.auth_token = "wrong-token".to_string();
810
+
811
+ let token = "0x7000000000000000000000000000000000000000"
812
+ .parse::<EvmAddress>()
813
+ .expect("token");
814
+ let recipient = "0x8000000000000000000000000000000000000000"
815
+ .parse::<EvmAddress>()
816
+ .expect("recipient");
817
+
818
+ let err = daemon
819
+ .sign_for_agent(sign_request(
820
+ &agent_credentials,
821
+ AgentAction::Transfer {
822
+ chain_id: 1,
823
+ token,
824
+ to: recipient,
825
+ amount_wei: 1,
826
+ },
827
+ ))
828
+ .await
829
+ .expect_err("must reject bad auth token");
830
+
831
+ assert!(matches!(err, DaemonError::AgentAuthenticationFailed));
832
+ }
833
+
834
+ #[tokio::test]
835
+ async fn oversized_agent_auth_token_is_rejected() {
836
+ let daemon = InMemoryDaemon::new(
837
+ "vault-password",
838
+ SoftwareSignerBackend::default(),
839
+ DaemonConfig::default(),
840
+ )
841
+ .expect("daemon");
842
+
843
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
844
+ let session = AdminSession {
845
+ vault_password: "vault-password".to_string(),
846
+ lease,
847
+ };
848
+ daemon
849
+ .add_policy(&session, policy_all_per_tx(100))
850
+ .await
851
+ .expect("add policy");
852
+
853
+ let key = daemon
854
+ .create_vault_key(&session, KeyCreateRequest::Generate)
855
+ .await
856
+ .expect("key");
857
+ let mut agent_credentials = daemon
858
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
859
+ .await
860
+ .expect("agent");
861
+ agent_credentials.auth_token = "a".repeat((16 * 1024) + 1);
862
+
863
+ let token = "0x7000000000000000000000000000000000000000"
864
+ .parse::<EvmAddress>()
865
+ .expect("token");
866
+ let recipient = "0x8000000000000000000000000000000000000000"
867
+ .parse::<EvmAddress>()
868
+ .expect("recipient");
869
+
870
+ let err = daemon
871
+ .sign_for_agent(sign_request(
872
+ &agent_credentials,
873
+ AgentAction::Transfer {
874
+ chain_id: 1,
875
+ token,
876
+ to: recipient,
877
+ amount_wei: 1,
878
+ },
879
+ ))
880
+ .await
881
+ .expect_err("must reject oversized auth token");
882
+
883
+ assert!(matches!(err, DaemonError::AgentAuthenticationFailed));
884
+ }
885
+
886
+ #[tokio::test]
887
+ async fn replayed_request_id_is_rejected() {
888
+ let daemon = InMemoryDaemon::new(
889
+ "vault-password",
890
+ SoftwareSignerBackend::default(),
891
+ DaemonConfig::default(),
892
+ )
893
+ .expect("daemon");
894
+
895
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
896
+ let session = AdminSession {
897
+ vault_password: "vault-password".to_string(),
898
+ lease,
899
+ };
900
+ daemon
901
+ .add_policy(&session, policy_all_per_tx(100))
902
+ .await
903
+ .expect("add policy");
904
+
905
+ let key = daemon
906
+ .create_vault_key(&session, KeyCreateRequest::Generate)
907
+ .await
908
+ .expect("key");
909
+ let agent_credentials = daemon
910
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
911
+ .await
912
+ .expect("agent");
913
+
914
+ let request = sign_request(
915
+ &agent_credentials,
916
+ AgentAction::Transfer {
917
+ chain_id: 1,
918
+ token: "0x7100000000000000000000000000000000000000"
919
+ .parse()
920
+ .expect("token"),
921
+ to: "0x8100000000000000000000000000000000000000"
922
+ .parse()
923
+ .expect("recipient"),
924
+ amount_wei: 1,
925
+ },
926
+ );
927
+
928
+ daemon
929
+ .sign_for_agent(request.clone())
930
+ .await
931
+ .expect("first request should pass");
932
+ let err = daemon
933
+ .sign_for_agent(request)
934
+ .await
935
+ .expect_err("replayed request id must fail");
936
+ assert!(matches!(err, DaemonError::RequestReplayDetected));
937
+ }
938
+
939
+ #[tokio::test]
940
+ async fn expired_request_is_rejected() {
941
+ let daemon = InMemoryDaemon::new(
942
+ "vault-password",
943
+ SoftwareSignerBackend::default(),
944
+ DaemonConfig::default(),
945
+ )
946
+ .expect("daemon");
947
+
948
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
949
+ let session = AdminSession {
950
+ vault_password: "vault-password".to_string(),
951
+ lease,
952
+ };
953
+ daemon
954
+ .add_policy(&session, policy_all_per_tx(100))
955
+ .await
956
+ .expect("add policy");
957
+
958
+ let key = daemon
959
+ .create_vault_key(&session, KeyCreateRequest::Generate)
960
+ .await
961
+ .expect("key");
962
+ let agent_credentials = daemon
963
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
964
+ .await
965
+ .expect("agent");
966
+
967
+ let mut request = sign_request(
968
+ &agent_credentials,
969
+ AgentAction::Transfer {
970
+ chain_id: 1,
971
+ token: "0x7200000000000000000000000000000000000000"
972
+ .parse()
973
+ .expect("token"),
974
+ to: "0x8200000000000000000000000000000000000000"
975
+ .parse()
976
+ .expect("recipient"),
977
+ amount_wei: 1,
978
+ },
979
+ );
980
+ request.expires_at = time::OffsetDateTime::now_utc() - time::Duration::seconds(1);
981
+
982
+ let err = daemon
983
+ .sign_for_agent(request)
984
+ .await
985
+ .expect_err("expired request should fail");
986
+ assert!(matches!(err, DaemonError::RequestExpired));
987
+ }
988
+
989
+ #[tokio::test]
990
+ async fn future_requested_at_beyond_allowed_clock_skew_is_rejected() {
991
+ let daemon = InMemoryDaemon::new(
992
+ "vault-password",
993
+ SoftwareSignerBackend::default(),
994
+ DaemonConfig::default(),
995
+ )
996
+ .expect("daemon");
997
+
998
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
999
+ let session = AdminSession {
1000
+ vault_password: "vault-password".to_string(),
1001
+ lease,
1002
+ };
1003
+ daemon
1004
+ .add_policy(&session, policy_all_per_tx(100))
1005
+ .await
1006
+ .expect("add policy");
1007
+
1008
+ let key = daemon
1009
+ .create_vault_key(&session, KeyCreateRequest::Generate)
1010
+ .await
1011
+ .expect("key");
1012
+ let agent_credentials = daemon
1013
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
1014
+ .await
1015
+ .expect("agent");
1016
+
1017
+ let now = time::OffsetDateTime::now_utc();
1018
+ let mut request = sign_request(
1019
+ &agent_credentials,
1020
+ AgentAction::Transfer {
1021
+ chain_id: 1,
1022
+ token: "0x7300000000000000000000000000000000000000"
1023
+ .parse()
1024
+ .expect("token"),
1025
+ to: "0x8300000000000000000000000000000000000000"
1026
+ .parse()
1027
+ .expect("recipient"),
1028
+ amount_wei: 1,
1029
+ },
1030
+ );
1031
+ request.requested_at = now + time::Duration::seconds(31);
1032
+ request.expires_at = request.requested_at + time::Duration::minutes(2);
1033
+
1034
+ let err = daemon
1035
+ .sign_for_agent(request)
1036
+ .await
1037
+ .expect_err("future-dated request must fail");
1038
+ assert!(matches!(err, DaemonError::InvalidRequestTimestamps));
1039
+ }
1040
+
1041
+ #[tokio::test]
1042
+ async fn request_ttl_longer_than_max_is_rejected() {
1043
+ let daemon = InMemoryDaemon::new(
1044
+ "vault-password",
1045
+ SoftwareSignerBackend::default(),
1046
+ DaemonConfig::default(),
1047
+ )
1048
+ .expect("daemon");
1049
+
1050
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
1051
+ let session = AdminSession {
1052
+ vault_password: "vault-password".to_string(),
1053
+ lease,
1054
+ };
1055
+ daemon
1056
+ .add_policy(&session, policy_all_per_tx(100))
1057
+ .await
1058
+ .expect("add policy");
1059
+
1060
+ let key = daemon
1061
+ .create_vault_key(&session, KeyCreateRequest::Generate)
1062
+ .await
1063
+ .expect("key");
1064
+ let agent_credentials = daemon
1065
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
1066
+ .await
1067
+ .expect("agent");
1068
+
1069
+ let now = time::OffsetDateTime::now_utc();
1070
+ let mut request = sign_request(
1071
+ &agent_credentials,
1072
+ AgentAction::Transfer {
1073
+ chain_id: 1,
1074
+ token: "0x7400000000000000000000000000000000000000"
1075
+ .parse()
1076
+ .expect("token"),
1077
+ to: "0x8400000000000000000000000000000000000000"
1078
+ .parse()
1079
+ .expect("recipient"),
1080
+ amount_wei: 1,
1081
+ },
1082
+ );
1083
+ request.requested_at = now;
1084
+ request.expires_at = now + time::Duration::minutes(6);
1085
+
1086
+ let err = daemon
1087
+ .sign_for_agent(request)
1088
+ .await
1089
+ .expect_err("overlong request ttl must fail");
1090
+ assert!(matches!(err, DaemonError::InvalidRequestTimestamps));
1091
+ }
1092
+
1093
+ #[test]
1094
+ fn daemon_rejects_blank_admin_passwords_in_core_constructors() {
1095
+ let err = match InMemoryDaemon::new(
1096
+ " \t\n ",
1097
+ SoftwareSignerBackend::default(),
1098
+ DaemonConfig::default(),
1099
+ ) {
1100
+ Ok(_) => panic!("blank admin password must be rejected"),
1101
+ Err(err) => err,
1102
+ };
1103
+ assert!(
1104
+ matches!(err, DaemonError::InvalidConfig(message) if message.contains("admin_password"))
1105
+ );
1106
+
1107
+ let state_path = unique_state_path("blank-admin-password");
1108
+ let err = match InMemoryDaemon::new_with_persistent_store(
1109
+ "\n\r\t",
1110
+ SoftwareSignerBackend::default(),
1111
+ DaemonConfig::default(),
1112
+ PersistentStoreConfig::new(state_path.clone()),
1113
+ ) {
1114
+ Ok(_) => panic!("blank persistent-store admin password must be rejected"),
1115
+ Err(err) => err,
1116
+ };
1117
+ assert!(
1118
+ matches!(err, DaemonError::InvalidConfig(message) if message.contains("admin_password"))
1119
+ );
1120
+ assert!(
1121
+ !state_path.exists(),
1122
+ "persistent store must not be created for blank passwords"
1123
+ );
1124
+ }
1125
+
1126
+ #[test]
1127
+ fn daemon_rejects_oversized_admin_passwords_in_core_constructors() {
1128
+ let oversized = "a".repeat((16 * 1024) + 1);
1129
+ let err = match InMemoryDaemon::new(
1130
+ &oversized,
1131
+ SoftwareSignerBackend::default(),
1132
+ DaemonConfig::default(),
1133
+ ) {
1134
+ Ok(_) => panic!("oversized admin password must be rejected"),
1135
+ Err(err) => err,
1136
+ };
1137
+ assert!(
1138
+ matches!(err, DaemonError::InvalidConfig(message) if message.contains("must not exceed"))
1139
+ );
1140
+
1141
+ let state_path = unique_state_path("oversized-admin-password");
1142
+ let err = match InMemoryDaemon::new_with_persistent_store(
1143
+ &oversized,
1144
+ SoftwareSignerBackend::default(),
1145
+ DaemonConfig::default(),
1146
+ PersistentStoreConfig::new(state_path.clone()),
1147
+ ) {
1148
+ Ok(_) => panic!("oversized persistent-store admin password must be rejected"),
1149
+ Err(err) => err,
1150
+ };
1151
+ assert!(
1152
+ matches!(err, DaemonError::InvalidConfig(message) if message.contains("must not exceed"))
1153
+ );
1154
+ assert!(
1155
+ !state_path.exists(),
1156
+ "persistent store must not be created for oversized passwords"
1157
+ );
1158
+ }
1159
+
1160
+ #[tokio::test]
1161
+ async fn privileged_admin_mutators_require_authenticated_session() {
1162
+ let daemon = InMemoryDaemon::new(
1163
+ "vault-password",
1164
+ SoftwareSignerBackend::default(),
1165
+ DaemonConfig::default(),
1166
+ )
1167
+ .expect("daemon");
1168
+
1169
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
1170
+ let session = AdminSession {
1171
+ vault_password: "vault-password".to_string(),
1172
+ lease: lease.clone(),
1173
+ };
1174
+ let bad_session = AdminSession {
1175
+ vault_password: "wrong".to_string(),
1176
+ lease,
1177
+ };
1178
+
1179
+ let policy = policy_all_per_tx(100);
1180
+ daemon
1181
+ .add_policy(&session, policy.clone())
1182
+ .await
1183
+ .expect("add policy");
1184
+
1185
+ let key = daemon
1186
+ .create_vault_key(&session, KeyCreateRequest::Generate)
1187
+ .await
1188
+ .expect("key");
1189
+
1190
+ let agent_credentials = daemon
1191
+ .create_agent_key(&session, key.id, PolicyAttachment::AllPolicies)
1192
+ .await
1193
+ .expect("agent");
1194
+
1195
+ let err = daemon
1196
+ .disable_policy(&bad_session, policy.id)
1197
+ .await
1198
+ .expect_err("must reject disable_policy for bad session");
1199
+ assert!(matches!(err, DaemonError::AuthenticationFailed));
1200
+
1201
+ let err = daemon
1202
+ .create_vault_key(&bad_session, KeyCreateRequest::Generate)
1203
+ .await
1204
+ .expect_err("must reject create_vault_key for bad session");
1205
+ assert!(matches!(err, DaemonError::AuthenticationFailed));
1206
+
1207
+ let err = daemon
1208
+ .create_agent_key(&bad_session, key.id, PolicyAttachment::AllPolicies)
1209
+ .await
1210
+ .expect_err("must reject create_agent_key for bad session");
1211
+ assert!(matches!(err, DaemonError::AuthenticationFailed));
1212
+
1213
+ let err = daemon
1214
+ .rotate_agent_auth_token(&bad_session, agent_credentials.agent_key.id)
1215
+ .await
1216
+ .expect_err("must reject rotate_agent_auth_token for bad session");
1217
+ assert!(matches!(err, DaemonError::AuthenticationFailed));
1218
+
1219
+ let err = daemon
1220
+ .revoke_agent_key(&bad_session, agent_credentials.agent_key.id)
1221
+ .await
1222
+ .expect_err("must reject revoke_agent_key for bad session");
1223
+ assert!(matches!(err, DaemonError::AuthenticationFailed));
1224
+ }