@wlfi-agent/cli 1.4.17 → 1.4.18

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 (93) hide show
  1. package/Cargo.lock +5 -0
  2. package/README.md +61 -28
  3. package/crates/vault-cli-admin/src/io_utils.rs +149 -1
  4. package/crates/vault-cli-admin/src/main.rs +639 -16
  5. package/crates/vault-cli-admin/src/shared_config.rs +18 -18
  6. package/crates/vault-cli-admin/src/tui/token_rpc.rs +190 -3
  7. package/crates/vault-cli-admin/src/tui/utils.rs +59 -0
  8. package/crates/vault-cli-admin/src/tui.rs +1205 -120
  9. package/crates/vault-cli-agent/Cargo.toml +1 -0
  10. package/crates/vault-cli-agent/src/io_utils.rs +163 -2
  11. package/crates/vault-cli-agent/src/main.rs +648 -32
  12. package/crates/vault-cli-daemon/Cargo.toml +4 -0
  13. package/crates/vault-cli-daemon/src/main.rs +617 -67
  14. package/crates/vault-cli-daemon/src/relay_sync.rs +776 -4
  15. package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +5 -0
  16. package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +32 -1
  17. package/crates/vault-daemon/src/persistence.rs +637 -100
  18. package/crates/vault-daemon/src/tests.rs +1013 -3
  19. package/crates/vault-daemon/src/tests_parts/part2.rs +99 -0
  20. package/crates/vault-daemon/src/tests_parts/part4.rs +11 -7
  21. package/crates/vault-domain/src/nonce.rs +4 -0
  22. package/crates/vault-domain/src/tests.rs +616 -0
  23. package/crates/vault-policy/src/engine.rs +55 -32
  24. package/crates/vault-policy/src/tests.rs +195 -0
  25. package/crates/vault-sdk-agent/src/lib.rs +415 -22
  26. package/crates/vault-signer/Cargo.toml +3 -0
  27. package/crates/vault-signer/src/lib.rs +266 -40
  28. package/crates/vault-transport-unix/src/lib.rs +653 -5
  29. package/crates/vault-transport-xpc/src/tests.rs +531 -3
  30. package/crates/vault-transport-xpc/tests/e2e_flow.rs +3 -0
  31. package/dist/cli.cjs +663 -190
  32. package/dist/cli.cjs.map +1 -1
  33. package/package.json +5 -2
  34. package/packages/cache/.turbo/turbo-build.log +20 -20
  35. package/packages/cache/coverage/clover.xml +529 -394
  36. package/packages/cache/coverage/coverage-final.json +2 -2
  37. package/packages/cache/coverage/index.html +21 -21
  38. package/packages/cache/coverage/src/client/index.html +1 -1
  39. package/packages/cache/coverage/src/client/index.ts.html +1 -1
  40. package/packages/cache/coverage/src/errors/index.html +1 -1
  41. package/packages/cache/coverage/src/errors/index.ts.html +12 -12
  42. package/packages/cache/coverage/src/index.html +1 -1
  43. package/packages/cache/coverage/src/index.ts.html +1 -1
  44. package/packages/cache/coverage/src/service/index.html +21 -21
  45. package/packages/cache/coverage/src/service/index.ts.html +769 -313
  46. package/packages/cache/dist/{chunk-QNK6GOTI.js → chunk-KC53LH5Z.js} +35 -2
  47. package/packages/cache/dist/chunk-KC53LH5Z.js.map +1 -0
  48. package/packages/cache/dist/{chunk-QF4XKEIA.cjs → chunk-UVU7VFE3.cjs} +35 -2
  49. package/packages/cache/dist/chunk-UVU7VFE3.cjs.map +1 -0
  50. package/packages/cache/dist/index.cjs +2 -2
  51. package/packages/cache/dist/index.js +1 -1
  52. package/packages/cache/dist/service/index.cjs +2 -2
  53. package/packages/cache/dist/service/index.js +1 -1
  54. package/packages/cache/node_modules/.bin/tsc +2 -2
  55. package/packages/cache/node_modules/.bin/tsserver +2 -2
  56. package/packages/cache/node_modules/.bin/tsup +2 -2
  57. package/packages/cache/node_modules/.bin/tsup-node +2 -2
  58. package/packages/cache/node_modules/.bin/vitest +4 -4
  59. package/packages/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  60. package/packages/cache/src/service/index.test.ts +165 -19
  61. package/packages/cache/src/service/index.ts +38 -1
  62. package/packages/config/.turbo/turbo-build.log +4 -4
  63. package/packages/config/dist/index.cjs +0 -17
  64. package/packages/config/dist/index.cjs.map +1 -1
  65. package/packages/config/src/index.ts +0 -17
  66. package/packages/rpc/.turbo/turbo-build.log +11 -11
  67. package/packages/rpc/dist/index.cjs +0 -17
  68. package/packages/rpc/dist/index.cjs.map +1 -1
  69. package/packages/rpc/src/index.js +1 -0
  70. package/packages/ui/node_modules/.bin/tsc +2 -2
  71. package/packages/ui/node_modules/.bin/tsserver +2 -2
  72. package/packages/ui/node_modules/.bin/tsup +2 -2
  73. package/packages/ui/node_modules/.bin/tsup-node +2 -2
  74. package/scripts/install-cli-launcher.mjs +37 -0
  75. package/scripts/install-rust-binaries.mjs +47 -0
  76. package/scripts/run-tests-isolated.mjs +210 -0
  77. package/src/cli.ts +310 -50
  78. package/src/lib/admin-reset.ts +15 -30
  79. package/src/lib/admin-setup.ts +246 -55
  80. package/src/lib/agent-auth-migrate.ts +5 -1
  81. package/src/lib/asset-broadcast.ts +15 -4
  82. package/src/lib/config-amounts.ts +6 -4
  83. package/src/lib/hidden-tty-prompt.js +1 -0
  84. package/src/lib/hidden-tty-prompt.ts +105 -0
  85. package/src/lib/keychain.ts +1 -0
  86. package/src/lib/local-admin-access.ts +4 -29
  87. package/src/lib/rust.ts +129 -33
  88. package/src/lib/signed-tx.ts +1 -0
  89. package/src/lib/sudo.ts +15 -5
  90. package/src/lib/wallet-profile.ts +3 -0
  91. package/src/lib/wallet-setup.ts +52 -0
  92. package/packages/cache/dist/chunk-QF4XKEIA.cjs.map +0 -1
  93. package/packages/cache/dist/chunk-QNK6GOTI.js.map +0 -1
@@ -9,14 +9,20 @@ use uuid::Uuid;
9
9
  use vault_domain::{
10
10
  AgentAction, AgentCredentials, AssetId, BroadcastTx, EntityScope, EvmAddress, KeySource, Lease,
11
11
  ManualApprovalDecision, ManualApprovalStatus, NonceReleaseRequest, NonceReservationRequest,
12
- PolicyAttachment, PolicyType, SignRequest, SpendingPolicy,
12
+ NonceReservation, PolicyAttachment, PolicyType, RelayConfig, SignRequest, Signature,
13
+ SpendingPolicy, VaultKey,
13
14
  };
14
15
  use vault_signer::SoftwareSignerBackend;
16
+ use vault_policy::{PolicyDecision, PolicyEvaluation, PolicyExplanation};
15
17
 
16
18
  use super::{
17
- validate_loaded_state, AdminSession, DaemonConfig, DaemonError, DaemonRpcRequest,
19
+ ensure_relay_identity, ethereum_address_from_public_key_hex, manual_approval_frontend_url,
20
+ payload_hash_hex, AdminSession, DaemonConfig, DaemonError, DaemonRpcRequest,
18
21
  DaemonRpcResponse, InMemoryDaemon, KeyCreateRequest, KeyManagerDaemonApi,
19
- PersistentStoreConfig, PolicyError,
22
+ PersistedDaemonState, PersistentStoreConfig, PolicyError, constant_time_eq,
23
+ generate_agent_auth_token, hash_agent_auth_token, map_domain_to_signer_error,
24
+ normalize_optional_url, parse_verifying_key, validate_admin_password, validate_config,
25
+ validate_loaded_state, validate_policy,
20
26
  };
21
27
 
22
28
  fn policy_all_per_tx(max: u128) -> SpendingPolicy {
@@ -70,6 +76,7 @@ async fn reserve_nonce_for_agent(
70
76
  agent_auth_token: credentials.auth_token.clone(),
71
77
  chain_id,
72
78
  min_nonce: nonce,
79
+ exact_nonce: false,
73
80
  requested_at: now,
74
81
  expires_at: now + time::Duration::minutes(2),
75
82
  })
@@ -89,6 +96,100 @@ fn unique_state_path(test_name: &str) -> std::path::PathBuf {
89
96
  ))
90
97
  }
91
98
 
99
+ fn sample_lease() -> Lease {
100
+ let now = time::OffsetDateTime::now_utc();
101
+ Lease {
102
+ lease_id: Uuid::new_v4(),
103
+ issued_at: now,
104
+ expires_at: now + time::Duration::minutes(1),
105
+ }
106
+ }
107
+
108
+ fn sample_session() -> AdminSession {
109
+ AdminSession {
110
+ vault_password: "vault-password".to_string(),
111
+ lease: sample_lease(),
112
+ }
113
+ }
114
+
115
+ fn sample_agent_credentials() -> AgentCredentials {
116
+ AgentCredentials {
117
+ agent_key: vault_domain::AgentKey {
118
+ id: Uuid::new_v4(),
119
+ vault_key_id: Uuid::new_v4(),
120
+ policies: PolicyAttachment::AllPolicies,
121
+ created_at: time::OffsetDateTime::now_utc(),
122
+ },
123
+ auth_token: "agent-secret-token".to_string(),
124
+ }
125
+ }
126
+
127
+ fn sample_vault_key() -> VaultKey {
128
+ VaultKey {
129
+ id: Uuid::new_v4(),
130
+ source: KeySource::Generated,
131
+ public_key_hex: "11".repeat(33),
132
+ created_at: time::OffsetDateTime::now_utc(),
133
+ }
134
+ }
135
+
136
+ fn sample_manual_approval_request() -> vault_domain::ManualApprovalRequest {
137
+ vault_domain::ManualApprovalRequest {
138
+ id: Uuid::new_v4(),
139
+ agent_key_id: Uuid::new_v4(),
140
+ vault_key_id: Uuid::new_v4(),
141
+ request_payload_hash_hex: "aa".repeat(32),
142
+ action: AgentAction::TransferNative {
143
+ chain_id: 1,
144
+ to: "0x1111111111111111111111111111111111111111"
145
+ .parse()
146
+ .expect("recipient"),
147
+ amount_wei: 42,
148
+ },
149
+ chain_id: 1,
150
+ asset: AssetId::NativeEth,
151
+ recipient: "0x1111111111111111111111111111111111111111"
152
+ .parse()
153
+ .expect("recipient"),
154
+ amount_wei: 42,
155
+ created_at: time::OffsetDateTime::now_utc(),
156
+ updated_at: time::OffsetDateTime::now_utc(),
157
+ status: ManualApprovalStatus::Pending,
158
+ triggered_by_policy_ids: vec![Uuid::new_v4()],
159
+ completed_at: None,
160
+ rejection_reason: None,
161
+ }
162
+ }
163
+
164
+ fn sample_nonce_reservation() -> NonceReservation {
165
+ let now = time::OffsetDateTime::now_utc();
166
+ NonceReservation {
167
+ reservation_id: Uuid::new_v4(),
168
+ agent_key_id: Uuid::new_v4(),
169
+ vault_key_id: Uuid::new_v4(),
170
+ chain_id: 1,
171
+ nonce: 7,
172
+ issued_at: now,
173
+ expires_at: now + time::Duration::minutes(1),
174
+ }
175
+ }
176
+
177
+ fn sample_policy_evaluation() -> PolicyEvaluation {
178
+ PolicyEvaluation {
179
+ evaluated_policy_ids: vec![Uuid::new_v4()],
180
+ }
181
+ }
182
+
183
+ fn sample_policy_explanation() -> PolicyExplanation {
184
+ let policy_id = Uuid::new_v4();
185
+ PolicyExplanation {
186
+ attached_policy_ids: vec![policy_id],
187
+ applicable_policy_ids: vec![policy_id],
188
+ evaluated_policy_ids: vec![policy_id],
189
+ decision: PolicyDecision::Allow,
190
+ }
191
+ }
192
+
92
193
  #[test]
93
194
  fn daemon_rpc_request_debug_redacts_vault_password() {
94
195
  let rendered = format!(
@@ -180,6 +281,7 @@ fn daemon_rpc_request_zeroize_secrets_clears_nested_secret_material() {
180
281
  agent_auth_token: "nonce-secret".to_string(),
181
282
  chain_id: 1,
182
283
  min_nonce: 7,
284
+ exact_nonce: false,
183
285
  requested_at: now,
184
286
  expires_at: now + time::Duration::minutes(2),
185
287
  },
@@ -231,6 +333,914 @@ fn daemon_rpc_response_zeroize_secrets_clears_auth_tokens() {
231
333
  }
232
334
  }
233
335
 
336
+ #[test]
337
+ fn daemon_rpc_request_debug_covers_all_variants() {
338
+ let session = sample_session();
339
+ let credentials = sample_agent_credentials();
340
+ let request = sign_request(
341
+ &credentials,
342
+ AgentAction::TransferNative {
343
+ chain_id: 1,
344
+ to: "0x1111111111111111111111111111111111111111"
345
+ .parse()
346
+ .expect("recipient"),
347
+ amount_wei: 7,
348
+ },
349
+ );
350
+ let nonce_request = NonceReservationRequest {
351
+ request_id: Uuid::new_v4(),
352
+ agent_key_id: credentials.agent_key.id,
353
+ agent_auth_token: credentials.auth_token.clone(),
354
+ chain_id: 1,
355
+ min_nonce: 9,
356
+ exact_nonce: false,
357
+ requested_at: time::OffsetDateTime::now_utc(),
358
+ expires_at: time::OffsetDateTime::now_utc() + time::Duration::minutes(1),
359
+ };
360
+ let release_request = NonceReleaseRequest {
361
+ request_id: Uuid::new_v4(),
362
+ agent_key_id: credentials.agent_key.id,
363
+ agent_auth_token: credentials.auth_token.clone(),
364
+ reservation_id: Uuid::new_v4(),
365
+ requested_at: time::OffsetDateTime::now_utc(),
366
+ expires_at: time::OffsetDateTime::now_utc() + time::Duration::minutes(1),
367
+ };
368
+ let requests = vec![
369
+ DaemonRpcRequest::IssueLease {
370
+ vault_password: "super-secret-password".to_string(),
371
+ },
372
+ DaemonRpcRequest::AddPolicy {
373
+ session: session.clone(),
374
+ policy: policy_all_per_tx(100),
375
+ },
376
+ DaemonRpcRequest::ListPolicies {
377
+ session: session.clone(),
378
+ },
379
+ DaemonRpcRequest::DisablePolicy {
380
+ session: session.clone(),
381
+ policy_id: Uuid::new_v4(),
382
+ },
383
+ DaemonRpcRequest::CreateVaultKey {
384
+ session: session.clone(),
385
+ request: KeyCreateRequest::Generate,
386
+ },
387
+ DaemonRpcRequest::CreateAgentKey {
388
+ session: session.clone(),
389
+ vault_key_id: Uuid::new_v4(),
390
+ attachment: PolicyAttachment::AllPolicies,
391
+ },
392
+ DaemonRpcRequest::ExportVaultPrivateKey {
393
+ session: session.clone(),
394
+ vault_key_id: Uuid::new_v4(),
395
+ },
396
+ DaemonRpcRequest::RotateAgentAuthToken {
397
+ session: session.clone(),
398
+ agent_key_id: Uuid::new_v4(),
399
+ },
400
+ DaemonRpcRequest::RevokeAgentKey {
401
+ session: session.clone(),
402
+ agent_key_id: Uuid::new_v4(),
403
+ },
404
+ DaemonRpcRequest::ListManualApprovalRequests {
405
+ session: session.clone(),
406
+ },
407
+ DaemonRpcRequest::DecideManualApprovalRequest {
408
+ session: session.clone(),
409
+ approval_request_id: Uuid::new_v4(),
410
+ decision: ManualApprovalDecision::Reject,
411
+ rejection_reason: Some("denied".to_string()),
412
+ },
413
+ DaemonRpcRequest::SetRelayConfig {
414
+ session: session.clone(),
415
+ relay_url: Some("https://relay.example".to_string()),
416
+ frontend_url: Some("https://frontend.example".to_string()),
417
+ },
418
+ DaemonRpcRequest::GetRelayConfig {
419
+ session: session.clone(),
420
+ },
421
+ DaemonRpcRequest::EvaluateForAgent {
422
+ request: request.clone(),
423
+ },
424
+ DaemonRpcRequest::ExplainForAgent {
425
+ request: request.clone(),
426
+ },
427
+ DaemonRpcRequest::ReserveNonce {
428
+ request: nonce_request,
429
+ },
430
+ DaemonRpcRequest::ReleaseNonce {
431
+ request: release_request,
432
+ },
433
+ DaemonRpcRequest::SignForAgent { request },
434
+ ];
435
+
436
+ for request in requests {
437
+ let rendered = format!("{request:?}");
438
+ assert!(!rendered.is_empty());
439
+ }
440
+ }
441
+
442
+ #[test]
443
+ fn daemon_rpc_request_zeroize_covers_remaining_admin_and_agent_variants() {
444
+ let session = sample_session();
445
+ let credentials = sample_agent_credentials();
446
+ let request = sign_request(
447
+ &credentials,
448
+ AgentAction::TransferNative {
449
+ chain_id: 1,
450
+ to: "0x1111111111111111111111111111111111111111"
451
+ .parse()
452
+ .expect("recipient"),
453
+ amount_wei: 7,
454
+ },
455
+ );
456
+
457
+ let mut admin_variants = vec![
458
+ DaemonRpcRequest::ExportVaultPrivateKey {
459
+ session: session.clone(),
460
+ vault_key_id: Uuid::new_v4(),
461
+ },
462
+ DaemonRpcRequest::ListManualApprovalRequests {
463
+ session: session.clone(),
464
+ },
465
+ DaemonRpcRequest::DecideManualApprovalRequest {
466
+ session: session.clone(),
467
+ approval_request_id: Uuid::new_v4(),
468
+ decision: ManualApprovalDecision::Approve,
469
+ rejection_reason: None,
470
+ },
471
+ DaemonRpcRequest::SetRelayConfig {
472
+ session: session.clone(),
473
+ relay_url: Some("https://relay.example".to_string()),
474
+ frontend_url: Some("https://frontend.example".to_string()),
475
+ },
476
+ ];
477
+ for request in &mut admin_variants {
478
+ request.zeroize_secrets();
479
+ match request {
480
+ DaemonRpcRequest::ExportVaultPrivateKey { session, .. }
481
+ | DaemonRpcRequest::ListManualApprovalRequests { session }
482
+ | DaemonRpcRequest::DecideManualApprovalRequest { session, .. }
483
+ | DaemonRpcRequest::SetRelayConfig { session, .. } => {
484
+ assert!(session
485
+ .vault_password
486
+ .as_bytes()
487
+ .iter()
488
+ .all(|byte| *byte == 0));
489
+ }
490
+ other => panic!("unexpected request variant: {other:?}"),
491
+ }
492
+ }
493
+
494
+ let mut explain = DaemonRpcRequest::ExplainForAgent { request };
495
+ explain.zeroize_secrets();
496
+ match explain {
497
+ DaemonRpcRequest::ExplainForAgent { request } => {
498
+ assert!(request
499
+ .agent_auth_token
500
+ .as_bytes()
501
+ .iter()
502
+ .all(|byte| *byte == 0));
503
+ }
504
+ other => panic!("unexpected request variant: {other:?}"),
505
+ }
506
+ }
507
+
508
+ #[test]
509
+ fn daemon_rpc_response_debug_and_zeroize_cover_remaining_variants() {
510
+ let lease = sample_lease();
511
+ let manual_request = sample_manual_approval_request();
512
+ let reservation = sample_nonce_reservation();
513
+ let responses = vec![
514
+ DaemonRpcResponse::Unit,
515
+ DaemonRpcResponse::Lease(lease),
516
+ DaemonRpcResponse::Policies(vec![policy_all_per_tx(100)]),
517
+ DaemonRpcResponse::PolicyEvaluation(sample_policy_evaluation()),
518
+ DaemonRpcResponse::PolicyExplanation(sample_policy_explanation()),
519
+ DaemonRpcResponse::VaultKey(sample_vault_key()),
520
+ DaemonRpcResponse::PrivateKey(Some("super-secret-private-key".to_string())),
521
+ DaemonRpcResponse::PrivateKey(None),
522
+ DaemonRpcResponse::ManualApprovalRequests(vec![manual_request.clone()]),
523
+ DaemonRpcResponse::ManualApprovalRequest(manual_request),
524
+ DaemonRpcResponse::RelayConfig(vault_domain::RelayConfig {
525
+ relay_url: Some("https://relay.example".to_string()),
526
+ frontend_url: Some("https://frontend.example".to_string()),
527
+ daemon_id_hex: "aa".repeat(32),
528
+ daemon_public_key_hex: "bb".repeat(33),
529
+ }),
530
+ DaemonRpcResponse::NonceReservation(reservation),
531
+ DaemonRpcResponse::Signature(Signature::from_der(vec![1, 2, 3])),
532
+ ];
533
+
534
+ for response in responses {
535
+ let rendered = format!("{response:?}");
536
+ assert!(!rendered.is_empty());
537
+ }
538
+
539
+ let rendered = format!("{:?}", DaemonRpcResponse::AgentCredentials(sample_agent_credentials()));
540
+ assert!(rendered.contains("AgentCredentials"));
541
+
542
+ let mut private_key = DaemonRpcResponse::PrivateKey(Some("secret".to_string()));
543
+ private_key.zeroize_secrets();
544
+ match &private_key {
545
+ DaemonRpcResponse::PrivateKey(Some(value)) => {
546
+ assert!(value.as_bytes().iter().all(|byte| *byte == 0));
547
+ }
548
+ other => panic!("unexpected response variant: {other:?}"),
549
+ }
550
+
551
+ let mut none_private_key = DaemonRpcResponse::PrivateKey(None);
552
+ none_private_key.zeroize_secrets();
553
+ assert!(matches!(none_private_key, DaemonRpcResponse::PrivateKey(None)));
554
+ }
555
+
556
+ #[test]
557
+ fn ensure_relay_identity_populates_missing_fields_and_preserves_existing_values() {
558
+ let mut generated = PersistedDaemonState::default();
559
+ ensure_relay_identity(&mut generated);
560
+ assert_eq!(
561
+ generated.relay_config.relay_url.as_deref(),
562
+ Some("http://localhost:8787")
563
+ );
564
+ assert_eq!(generated.relay_private_key_hex.len(), 64);
565
+ assert_eq!(generated.relay_config.daemon_id_hex.len(), 64);
566
+ assert_eq!(generated.relay_config.daemon_public_key_hex.len(), 64);
567
+
568
+ let mut preserved = PersistedDaemonState {
569
+ relay_config: RelayConfig {
570
+ relay_url: Some("https://relay.example".to_string()),
571
+ frontend_url: Some("https://frontend.example".to_string()),
572
+ daemon_id_hex: "11".repeat(32),
573
+ daemon_public_key_hex: "22".repeat(32),
574
+ },
575
+ relay_private_key_hex: "33".repeat(32),
576
+ ..PersistedDaemonState::default()
577
+ };
578
+ ensure_relay_identity(&mut preserved);
579
+ assert_eq!(preserved.relay_private_key_hex, "33".repeat(32));
580
+ assert_eq!(
581
+ preserved.relay_config.relay_url.as_deref(),
582
+ Some("https://relay.example")
583
+ );
584
+ assert_eq!(preserved.relay_config.daemon_id_hex, "11".repeat(32));
585
+ assert_eq!(preserved.relay_config.daemon_public_key_hex, "22".repeat(32));
586
+ }
587
+
588
+ #[test]
589
+ fn manual_approval_frontend_url_prefers_frontend_and_falls_back_to_relay() {
590
+ let approval_request_id =
591
+ Uuid::parse_str("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa").expect("uuid");
592
+
593
+ let preferred = manual_approval_frontend_url(
594
+ &RelayConfig {
595
+ relay_url: Some("https://relay.example".to_string()),
596
+ frontend_url: Some("https://frontend.example/".to_string()),
597
+ daemon_id_hex: "11".repeat(32),
598
+ daemon_public_key_hex: "22".repeat(32),
599
+ },
600
+ approval_request_id,
601
+ "capability-token",
602
+ )
603
+ .expect("frontend url");
604
+ assert_eq!(
605
+ preferred,
606
+ format!(
607
+ "https://frontend.example/approvals/{approval_request_id}?daemonId={}&approvalCapability=capability-token",
608
+ "11".repeat(32)
609
+ )
610
+ );
611
+
612
+ let fallback = manual_approval_frontend_url(
613
+ &RelayConfig {
614
+ relay_url: Some("https://relay.example".to_string()),
615
+ frontend_url: None,
616
+ daemon_id_hex: " ".to_string(),
617
+ daemon_public_key_hex: "22".repeat(32),
618
+ },
619
+ approval_request_id,
620
+ "capability-token",
621
+ )
622
+ .expect("relay fallback");
623
+ assert_eq!(
624
+ fallback,
625
+ format!(
626
+ "https://relay.example/approvals/{approval_request_id}?approvalCapability=capability-token"
627
+ )
628
+ );
629
+ }
630
+
631
+ #[test]
632
+ fn payload_hash_hex_is_stable_and_input_sensitive() {
633
+ let first = payload_hash_hex(b"hello world");
634
+ let second = payload_hash_hex(b"hello world");
635
+ let different = payload_hash_hex(b"hello world!");
636
+
637
+ assert_eq!(first, second);
638
+ assert_ne!(first, different);
639
+ assert_eq!(first.len(), 64);
640
+ }
641
+
642
+ #[tokio::test]
643
+ async fn relay_registration_snapshot_and_address_helpers_use_latest_vault_key() {
644
+ let daemon = InMemoryDaemon::new(
645
+ "vault-password",
646
+ SoftwareSignerBackend::default(),
647
+ DaemonConfig::default(),
648
+ )
649
+ .expect("daemon");
650
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
651
+ let session = AdminSession {
652
+ vault_password: "vault-password".to_string(),
653
+ lease,
654
+ };
655
+
656
+ let first = daemon
657
+ .create_vault_key(&session, KeyCreateRequest::Generate)
658
+ .await
659
+ .expect("first key");
660
+ let second = daemon
661
+ .create_vault_key(&session, KeyCreateRequest::Generate)
662
+ .await
663
+ .expect("second key");
664
+
665
+ let expected_address = {
666
+ let public_bytes = hex::decode(second.public_key_hex.trim_start_matches("0x"))
667
+ .expect("decode public key");
668
+ let digest = keccak256(&public_bytes[1..]);
669
+ format!("0x{}", hex::encode(&digest.0[12..]))
670
+ };
671
+
672
+ assert_eq!(
673
+ ethereum_address_from_public_key_hex(&second.public_key_hex).expect("address"),
674
+ expected_address
675
+ );
676
+ assert!(
677
+ matches!(
678
+ ethereum_address_from_public_key_hex("zz"),
679
+ Err(DaemonError::Signer(_))
680
+ ),
681
+ "invalid hex should be rejected"
682
+ );
683
+
684
+ let snapshot = daemon
685
+ .relay_registration_snapshot()
686
+ .expect("relay registration snapshot");
687
+ assert_eq!(snapshot.vault_public_key_hex.as_deref(), Some(second.public_key_hex.as_str()));
688
+ assert_eq!(snapshot.ethereum_address.as_deref(), Some(expected_address.as_str()));
689
+ assert_ne!(snapshot.vault_public_key_hex.as_deref(), Some(first.public_key_hex.as_str()));
690
+ }
691
+
692
+ #[test]
693
+ fn decrypt_relay_envelope_round_trips_and_validates_inputs() {
694
+ use chacha20poly1305::aead::Aead;
695
+ use chacha20poly1305::{KeyInit, XChaCha20Poly1305};
696
+
697
+ let daemon = InMemoryDaemon::new(
698
+ "vault-password",
699
+ SoftwareSignerBackend::default(),
700
+ DaemonConfig::default(),
701
+ )
702
+ .expect("daemon");
703
+ let snapshot = daemon.snapshot_state().expect("snapshot");
704
+ let daemon_public_bytes =
705
+ hex::decode(snapshot.relay_config.daemon_public_key_hex).expect("daemon public key");
706
+ let daemon_public = x25519_dalek::PublicKey::from(
707
+ <[u8; 32]>::try_from(daemon_public_bytes.as_slice()).expect("public key bytes"),
708
+ );
709
+ let secret = x25519_dalek::StaticSecret::from([9u8; 32]);
710
+ let shared_secret = secret.diffie_hellman(&daemon_public);
711
+ let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
712
+ let nonce = [4u8; 24];
713
+ let plaintext = br#"{"update":"payload"}"#;
714
+ let ciphertext = cipher
715
+ .encrypt((&nonce).into(), plaintext.as_slice())
716
+ .expect("encrypt");
717
+ let encapsulated_key_hex = hex::encode(x25519_dalek::PublicKey::from(&secret).as_bytes());
718
+
719
+ let decrypted = daemon
720
+ .decrypt_relay_envelope(
721
+ "x25519-xchacha20poly1305-v1",
722
+ &encapsulated_key_hex,
723
+ &hex::encode(nonce),
724
+ &hex::encode(ciphertext),
725
+ )
726
+ .expect("decrypt");
727
+ assert_eq!(decrypted, plaintext);
728
+
729
+ assert!(
730
+ matches!(
731
+ daemon.decrypt_relay_envelope("invalid", &encapsulated_key_hex, &hex::encode(nonce), "00"),
732
+ Err(DaemonError::InvalidRelayConfig(message)) if message.contains("unsupported relay encryption algorithm")
733
+ )
734
+ );
735
+ assert!(
736
+ matches!(
737
+ daemon.decrypt_relay_envelope("x25519-xchacha20poly1305-v1", "aa", &hex::encode(nonce), "00"),
738
+ Err(DaemonError::InvalidRelayConfig(message)) if message.contains("encapsulated key must be 32 bytes")
739
+ )
740
+ );
741
+ assert!(
742
+ matches!(
743
+ daemon.decrypt_relay_envelope("x25519-xchacha20poly1305-v1", &encapsulated_key_hex, "aa", "00"),
744
+ Err(DaemonError::InvalidRelayConfig(message)) if message.contains("nonce must be 24 bytes")
745
+ )
746
+ );
747
+ assert!(
748
+ matches!(
749
+ daemon.decrypt_relay_envelope(
750
+ "x25519-xchacha20poly1305-v1",
751
+ &encapsulated_key_hex,
752
+ &hex::encode(nonce),
753
+ "zz"
754
+ ),
755
+ Err(DaemonError::InvalidRelayConfig(message)) if message.contains("ciphertext is invalid hex")
756
+ )
757
+ );
758
+ }
759
+
760
+ #[tokio::test]
761
+ async fn snapshot_restore_and_non_persistent_helpers_cover_state_management_paths() {
762
+ let daemon = InMemoryDaemon::new(
763
+ "vault-password",
764
+ SoftwareSignerBackend::default(),
765
+ DaemonConfig::default(),
766
+ )
767
+ .expect("daemon");
768
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
769
+ let session = AdminSession {
770
+ vault_password: "vault-password".to_string(),
771
+ lease,
772
+ };
773
+ daemon
774
+ .add_policy(&session, policy_all_per_tx(77))
775
+ .await
776
+ .expect("add policy");
777
+ let vault_key = daemon
778
+ .create_vault_key(&session, KeyCreateRequest::Generate)
779
+ .await
780
+ .expect("vault key");
781
+ let agent_credentials = daemon
782
+ .create_agent_key(&session, vault_key.id, PolicyAttachment::AllPolicies)
783
+ .await
784
+ .expect("agent credentials");
785
+ reserve_nonce_for_agent(&daemon, &agent_credentials, 1, 5).await;
786
+ let snapshot = daemon.snapshot_state().expect("snapshot");
787
+
788
+ daemon.policies.write().expect("policies").clear();
789
+ daemon.vault_keys.write().expect("vault keys").clear();
790
+ daemon.agent_keys.write().expect("agent keys").clear();
791
+ daemon.agent_auth_tokens.write().expect("auth tokens").clear();
792
+ daemon.nonce_reservations.write().expect("reservations").clear();
793
+ daemon
794
+ .restore_state(snapshot.clone())
795
+ .expect("restore snapshot");
796
+
797
+ assert_eq!(
798
+ daemon.snapshot_state().expect("restored snapshot").policies,
799
+ snapshot.policies
800
+ );
801
+ assert_eq!(
802
+ daemon.snapshot_state().expect("restored snapshot").vault_keys,
803
+ snapshot.vault_keys
804
+ );
805
+ assert_eq!(
806
+ daemon
807
+ .snapshot_state()
808
+ .expect("restored snapshot")
809
+ .agent_auth_tokens,
810
+ snapshot.agent_auth_tokens
811
+ );
812
+ assert_eq!(
813
+ daemon
814
+ .snapshot_state()
815
+ .expect("restored snapshot")
816
+ .nonce_reservations,
817
+ snapshot.nonce_reservations
818
+ );
819
+
820
+ assert!(daemon.backup_state_if_persistent().expect("backup").is_none());
821
+ daemon.persist_state_if_enabled().expect("persist noop");
822
+ daemon.persist_or_revert(None).expect("persist or revert noop");
823
+ }
824
+
825
+ #[test]
826
+ fn api_validation_helpers_cover_url_password_config_and_token_paths() {
827
+ assert_eq!(normalize_optional_url("relay_url", None).expect("none"), None);
828
+ assert_eq!(
829
+ normalize_optional_url("relay_url", Some(" ".to_string())).expect("blank"),
830
+ None
831
+ );
832
+ assert_eq!(
833
+ normalize_optional_url(
834
+ "relay_url",
835
+ Some("https://relay.example/path".to_string())
836
+ )
837
+ .expect("https"),
838
+ Some("https://relay.example/path".to_string())
839
+ );
840
+ assert_eq!(
841
+ normalize_optional_url(
842
+ "relay_url",
843
+ Some("http://localhost:8787".to_string())
844
+ )
845
+ .expect("localhost http"),
846
+ Some("http://localhost:8787".to_string())
847
+ );
848
+ assert_eq!(
849
+ normalize_optional_url(
850
+ "relay_url",
851
+ Some("http://127.0.0.1:8787".to_string())
852
+ )
853
+ .expect("loopback http"),
854
+ Some("http://127.0.0.1:8787".to_string())
855
+ );
856
+ assert!(matches!(
857
+ normalize_optional_url("relay_url", Some("ftp://relay.example".to_string())),
858
+ Err(DaemonError::InvalidRelayConfig(message)) if message.contains("must use http or https")
859
+ ));
860
+ assert!(matches!(
861
+ normalize_optional_url("relay_url", Some("https://user@example.com".to_string())),
862
+ Err(DaemonError::InvalidRelayConfig(message)) if message.contains("embedded username or password")
863
+ ));
864
+ assert!(matches!(
865
+ normalize_optional_url("relay_url", Some("https://relay.example?q=1".to_string())),
866
+ Err(DaemonError::InvalidRelayConfig(message)) if message.contains("query string")
867
+ ));
868
+ assert!(matches!(
869
+ normalize_optional_url("relay_url", Some("https://relay.example#frag".to_string())),
870
+ Err(DaemonError::InvalidRelayConfig(message)) if message.contains("fragment")
871
+ ));
872
+ assert!(matches!(
873
+ normalize_optional_url("relay_url", Some("http://relay.example".to_string())),
874
+ Err(DaemonError::InvalidRelayConfig(message)) if message.contains("must use https unless it targets localhost or a loopback address")
875
+ ));
876
+
877
+ validate_admin_password("vault-password").expect("valid password");
878
+ assert!(matches!(
879
+ validate_admin_password(" "),
880
+ Err(DaemonError::InvalidConfig(message)) if message.contains("must not be empty")
881
+ ));
882
+ assert!(matches!(
883
+ validate_admin_password(&"x".repeat(16 * 1024 + 1)),
884
+ Err(DaemonError::InvalidConfig(message)) if message.contains("must not exceed")
885
+ ));
886
+
887
+ validate_config(&DaemonConfig::default()).expect("valid config");
888
+ assert!(matches!(
889
+ validate_config(&DaemonConfig {
890
+ lease_ttl: time::Duration::ZERO,
891
+ ..DaemonConfig::default()
892
+ }),
893
+ Err(DaemonError::InvalidConfig(message)) if message.contains("lease_ttl")
894
+ ));
895
+ assert!(matches!(
896
+ validate_config(&DaemonConfig {
897
+ max_active_leases: 0,
898
+ ..DaemonConfig::default()
899
+ }),
900
+ Err(DaemonError::InvalidConfig(message)) if message.contains("max_active_leases")
901
+ ));
902
+ assert!(matches!(
903
+ validate_config(&DaemonConfig {
904
+ max_sign_payload_bytes: 0,
905
+ ..DaemonConfig::default()
906
+ }),
907
+ Err(DaemonError::InvalidConfig(message)) if message.contains("max_sign_payload_bytes")
908
+ ));
909
+ assert!(matches!(
910
+ validate_config(&DaemonConfig {
911
+ max_request_ttl: time::Duration::ZERO,
912
+ ..DaemonConfig::default()
913
+ }),
914
+ Err(DaemonError::InvalidConfig(message)) if message.contains("max_request_ttl")
915
+ ));
916
+ assert!(matches!(
917
+ validate_config(&DaemonConfig {
918
+ max_request_clock_skew: time::Duration::seconds(-1),
919
+ ..DaemonConfig::default()
920
+ }),
921
+ Err(DaemonError::InvalidConfig(message)) if message.contains("max_request_clock_skew")
922
+ ));
923
+ assert!(matches!(
924
+ validate_config(&DaemonConfig {
925
+ nonce_reservation_ttl: time::Duration::ZERO,
926
+ ..DaemonConfig::default()
927
+ }),
928
+ Err(DaemonError::InvalidConfig(message)) if message.contains("nonce_reservation_ttl")
929
+ ));
930
+ assert!(matches!(
931
+ validate_config(&DaemonConfig {
932
+ max_failed_admin_auth_attempts: 0,
933
+ ..DaemonConfig::default()
934
+ }),
935
+ Err(DaemonError::InvalidConfig(message)) if message.contains("max_failed_admin_auth_attempts")
936
+ ));
937
+ assert!(matches!(
938
+ validate_config(&DaemonConfig {
939
+ admin_auth_lockout: time::Duration::ZERO,
940
+ ..DaemonConfig::default()
941
+ }),
942
+ Err(DaemonError::InvalidConfig(message)) if message.contains("admin_auth_lockout")
943
+ ));
944
+ assert!(matches!(
945
+ validate_config(&DaemonConfig {
946
+ argon2_memory_kib: 0,
947
+ ..DaemonConfig::default()
948
+ }),
949
+ Err(DaemonError::InvalidConfig(message)) if message.contains("argon2_memory_kib")
950
+ ));
951
+ assert!(matches!(
952
+ validate_config(&DaemonConfig {
953
+ argon2_time_cost: 0,
954
+ ..DaemonConfig::default()
955
+ }),
956
+ Err(DaemonError::InvalidConfig(message)) if message.contains("argon2_time_cost")
957
+ ));
958
+ assert!(matches!(
959
+ validate_config(&DaemonConfig {
960
+ argon2_parallelism: 0,
961
+ ..DaemonConfig::default()
962
+ }),
963
+ Err(DaemonError::InvalidConfig(message)) if message.contains("argon2_parallelism")
964
+ ));
965
+
966
+ let token = generate_agent_auth_token();
967
+ assert!(token.contains('.'));
968
+ let left = hash_agent_auth_token(&token);
969
+ let same = hash_agent_auth_token(&token);
970
+ let right = hash_agent_auth_token("different-token");
971
+ assert!(constant_time_eq(&left, &same));
972
+ assert!(!constant_time_eq(&left, &right));
973
+ assert!(!constant_time_eq(&left, &[1u8; 31]));
974
+ }
975
+
976
+ #[tokio::test]
977
+ async fn parsing_policy_and_loaded_state_helpers_cover_remaining_error_paths() {
978
+ let daemon = InMemoryDaemon::new(
979
+ "vault-password",
980
+ SoftwareSignerBackend::default(),
981
+ DaemonConfig::default(),
982
+ )
983
+ .expect("daemon");
984
+ let lease = daemon.issue_lease("vault-password").await.expect("lease");
985
+ let session = AdminSession {
986
+ vault_password: "vault-password".to_string(),
987
+ lease,
988
+ };
989
+ let key = daemon
990
+ .create_vault_key(&session, KeyCreateRequest::Generate)
991
+ .await
992
+ .expect("key");
993
+
994
+ let parsed_key = parse_verifying_key(&key.public_key_hex).expect("valid public key");
995
+ assert_eq!(
996
+ parsed_key.to_encoded_point(false).as_bytes(),
997
+ &hex::decode(&key.public_key_hex).expect("hex")
998
+ );
999
+ assert!(matches!(parse_verifying_key("zz"), Err(DaemonError::Signer(_))));
1000
+ assert!(matches!(
1001
+ parse_verifying_key(&hex::encode([0u8; 33])),
1002
+ Err(DaemonError::Signer(_))
1003
+ ));
1004
+ assert!(matches!(
1005
+ map_domain_to_signer_error(vault_domain::DomainError::UnsupportedTransactionType(0x03)),
1006
+ DaemonError::Signer(_)
1007
+ ));
1008
+
1009
+ validate_policy(&policy_all_per_tx(1)).expect("valid policy");
1010
+ let mut zero_policy = policy_all_per_tx(1);
1011
+ zero_policy.max_amount_wei = 0;
1012
+ assert!(matches!(
1013
+ validate_policy(&zero_policy),
1014
+ Err(DaemonError::InvalidPolicy(message)) if message.contains("max_amount_wei")
1015
+ ));
1016
+ let mut bad_networks = policy_all_per_tx(1);
1017
+ bad_networks.networks = EntityScope::Set(BTreeSet::from([0]));
1018
+ assert!(matches!(
1019
+ validate_policy(&bad_networks),
1020
+ Err(DaemonError::InvalidPolicy(message)) if message.contains("network set scope")
1021
+ ));
1022
+
1023
+ let valid_state = daemon.snapshot_state().expect("snapshot");
1024
+ validate_loaded_state(&valid_state).expect("valid loaded state");
1025
+
1026
+ let mut state = valid_state.clone();
1027
+ state
1028
+ .software_signer_private_keys
1029
+ .insert(Uuid::new_v4(), "11".repeat(32));
1030
+ assert!(matches!(
1031
+ validate_loaded_state(&state),
1032
+ Err(DaemonError::Persistence(message)) if message.contains("signer key material")
1033
+ ));
1034
+
1035
+ let mut state = valid_state.clone();
1036
+ let agent_key = sample_agent_credentials().agent_key;
1037
+ state.agent_keys.insert(agent_key.id, agent_key.clone());
1038
+ assert!(matches!(
1039
+ validate_loaded_state(&state),
1040
+ Err(DaemonError::Persistence(message)) if message.contains("references unknown vault key")
1041
+ ));
1042
+
1043
+ let mut state = valid_state.clone();
1044
+ let mut attached_agent = sample_agent_credentials().agent_key;
1045
+ attached_agent.vault_key_id = key.id;
1046
+ attached_agent.policies = PolicyAttachment::PolicySet(BTreeSet::new());
1047
+ state.agent_keys.insert(attached_agent.id, attached_agent);
1048
+ assert!(matches!(
1049
+ validate_loaded_state(&state),
1050
+ Err(DaemonError::Persistence(message)) if message.contains("empty policy attachment")
1051
+ ));
1052
+
1053
+ let mut state = valid_state.clone();
1054
+ state.agent_auth_tokens.insert(Uuid::new_v4(), [7u8; 32]);
1055
+ assert!(matches!(
1056
+ validate_loaded_state(&state),
1057
+ Err(DaemonError::Persistence(message)) if message.contains("auth token for unknown agent")
1058
+ ));
1059
+
1060
+ let mut state = valid_state.clone();
1061
+ state.nonce_heads.insert(Uuid::new_v4(), std::collections::HashMap::new());
1062
+ assert!(matches!(
1063
+ validate_loaded_state(&state),
1064
+ Err(DaemonError::Persistence(message)) if message.contains("nonce head for unknown vault key")
1065
+ ));
1066
+
1067
+ let mut state = valid_state.clone();
1068
+ let mut valid_agent = sample_agent_credentials().agent_key;
1069
+ valid_agent.vault_key_id = key.id;
1070
+ state.agent_keys.insert(valid_agent.id, valid_agent.clone());
1071
+ let mut reservation = sample_nonce_reservation();
1072
+ reservation.agent_key_id = valid_agent.id;
1073
+ reservation.vault_key_id = key.id;
1074
+ reservation.chain_id = 0;
1075
+ state
1076
+ .nonce_reservations
1077
+ .insert(reservation.reservation_id, reservation);
1078
+ assert!(matches!(
1079
+ validate_loaded_state(&state),
1080
+ Err(DaemonError::Persistence(message)) if message.contains("invalid chain_id 0")
1081
+ ));
1082
+
1083
+ let mut state = valid_state.clone();
1084
+ let manual_policy = SpendingPolicy::new_manual_approval(
1085
+ 1,
1086
+ 1,
1087
+ 100,
1088
+ EntityScope::All,
1089
+ EntityScope::All,
1090
+ EntityScope::All,
1091
+ )
1092
+ .expect("manual approval policy");
1093
+ let manual_policy_id = manual_policy.id;
1094
+ state.policies.insert(manual_policy.id, manual_policy);
1095
+ let mut request = sample_manual_approval_request();
1096
+ request.agent_key_id = Uuid::new_v4();
1097
+ request.triggered_by_policy_ids = vec![manual_policy_id];
1098
+ state.manual_approval_requests.insert(request.id, request);
1099
+ assert!(matches!(
1100
+ validate_loaded_state(&state),
1101
+ Err(DaemonError::Persistence(message)) if message.contains("references unknown agent")
1102
+ ));
1103
+
1104
+ let mut state = valid_state.clone();
1105
+ let mut valid_agent = sample_agent_credentials().agent_key;
1106
+ valid_agent.vault_key_id = key.id;
1107
+ state.agent_keys.insert(valid_agent.id, valid_agent.clone());
1108
+ let mut request = sample_manual_approval_request();
1109
+ request.agent_key_id = valid_agent.id;
1110
+ request.vault_key_id = Uuid::new_v4();
1111
+ state.manual_approval_requests.insert(request.id, request);
1112
+ assert!(matches!(
1113
+ validate_loaded_state(&state),
1114
+ Err(DaemonError::Persistence(message)) if message.contains("vault key mismatch")
1115
+ ));
1116
+
1117
+ let mut state = valid_state.clone();
1118
+ let mut valid_agent = sample_agent_credentials().agent_key;
1119
+ valid_agent.vault_key_id = key.id;
1120
+ state.agent_keys.insert(valid_agent.id, valid_agent.clone());
1121
+ let mut request = sample_manual_approval_request();
1122
+ request.agent_key_id = valid_agent.id;
1123
+ request.vault_key_id = key.id;
1124
+ request.chain_id = 0;
1125
+ state.manual_approval_requests.insert(request.id, request);
1126
+ assert!(matches!(
1127
+ validate_loaded_state(&state),
1128
+ Err(DaemonError::Persistence(message)) if message.contains("invalid chain_id 0")
1129
+ ));
1130
+
1131
+ let mut state = valid_state.clone();
1132
+ let mut valid_agent = sample_agent_credentials().agent_key;
1133
+ valid_agent.vault_key_id = key.id;
1134
+ state.agent_keys.insert(valid_agent.id, valid_agent.clone());
1135
+ let mut request = sample_manual_approval_request();
1136
+ request.agent_key_id = valid_agent.id;
1137
+ request.vault_key_id = key.id;
1138
+ request.request_payload_hash_hex = " ".to_string();
1139
+ state.manual_approval_requests.insert(request.id, request);
1140
+ assert!(matches!(
1141
+ validate_loaded_state(&state),
1142
+ Err(DaemonError::Persistence(message)) if message.contains("empty payload hash")
1143
+ ));
1144
+
1145
+ let mut state = valid_state.clone();
1146
+ let mut valid_agent = sample_agent_credentials().agent_key;
1147
+ valid_agent.vault_key_id = key.id;
1148
+ state.agent_keys.insert(valid_agent.id, valid_agent.clone());
1149
+ let mut request = sample_manual_approval_request();
1150
+ request.agent_key_id = valid_agent.id;
1151
+ request.vault_key_id = key.id;
1152
+ request.updated_at = request.created_at - time::Duration::seconds(1);
1153
+ state.manual_approval_requests.insert(request.id, request);
1154
+ assert!(matches!(
1155
+ validate_loaded_state(&state),
1156
+ Err(DaemonError::Persistence(message)) if message.contains("invalid timestamps")
1157
+ ));
1158
+
1159
+ let mut state = valid_state.clone();
1160
+ let mut valid_agent = sample_agent_credentials().agent_key;
1161
+ valid_agent.vault_key_id = key.id;
1162
+ state.agent_keys.insert(valid_agent.id, valid_agent.clone());
1163
+ let mut request = sample_manual_approval_request();
1164
+ request.agent_key_id = valid_agent.id;
1165
+ request.vault_key_id = key.id;
1166
+ request.status = ManualApprovalStatus::Completed;
1167
+ request.completed_at = None;
1168
+ state.manual_approval_requests.insert(request.id, request);
1169
+ assert!(matches!(
1170
+ validate_loaded_state(&state),
1171
+ Err(DaemonError::Persistence(message)) if message.contains("completed without completed_at")
1172
+ ));
1173
+
1174
+ let mut state = valid_state.clone();
1175
+ let mut valid_agent = sample_agent_credentials().agent_key;
1176
+ valid_agent.vault_key_id = key.id;
1177
+ state.agent_keys.insert(valid_agent.id, valid_agent.clone());
1178
+ let mut request = sample_manual_approval_request();
1179
+ request.agent_key_id = valid_agent.id;
1180
+ request.vault_key_id = key.id;
1181
+ request.triggered_by_policy_ids = vec![Uuid::new_v4()];
1182
+ state.manual_approval_requests.insert(request.id, request);
1183
+ assert!(matches!(
1184
+ validate_loaded_state(&state),
1185
+ Err(DaemonError::Persistence(message)) if message.contains("references unknown policy")
1186
+ ));
1187
+
1188
+ let mut state = valid_state.clone();
1189
+ let mut valid_agent = sample_agent_credentials().agent_key;
1190
+ valid_agent.vault_key_id = key.id;
1191
+ state.agent_keys.insert(valid_agent.id, valid_agent.clone());
1192
+ let mut non_manual_policy = policy_all_per_tx(1);
1193
+ non_manual_policy.id = Uuid::new_v4();
1194
+ let non_manual_policy_id = non_manual_policy.id;
1195
+ state.policies.insert(non_manual_policy.id, non_manual_policy);
1196
+ let mut request = sample_manual_approval_request();
1197
+ request.agent_key_id = valid_agent.id;
1198
+ request.vault_key_id = key.id;
1199
+ request.triggered_by_policy_ids = vec![non_manual_policy_id];
1200
+ state.manual_approval_requests.insert(request.id, request);
1201
+ assert!(matches!(
1202
+ validate_loaded_state(&state),
1203
+ Err(DaemonError::Persistence(message)) if message.contains("references non-manual policy")
1204
+ ));
1205
+
1206
+ let mut state = valid_state.clone();
1207
+ state.relay_config.frontend_url = Some("http://frontend.example".to_string());
1208
+ assert!(matches!(
1209
+ validate_loaded_state(&state),
1210
+ Err(DaemonError::InvalidRelayConfig(message))
1211
+ | Err(DaemonError::Persistence(message))
1212
+ if message.contains("https unless it targets localhost or a loopback address")
1213
+ ));
1214
+
1215
+ let mut state = valid_state.clone();
1216
+ state.relay_private_key_hex = "zz".to_string();
1217
+ assert!(matches!(
1218
+ validate_loaded_state(&state),
1219
+ Err(DaemonError::Persistence(message)) if message.contains("invalid hex")
1220
+ ));
1221
+
1222
+ let mut state = valid_state.clone();
1223
+ state.relay_private_key_hex = "11".repeat(31);
1224
+ assert!(matches!(
1225
+ validate_loaded_state(&state),
1226
+ Err(DaemonError::Persistence(message)) if message.contains("must be 32 bytes")
1227
+ ));
1228
+
1229
+ let mut state = valid_state.clone();
1230
+ state.relay_config.daemon_public_key_hex = "11".repeat(32);
1231
+ assert!(matches!(
1232
+ validate_loaded_state(&state),
1233
+ Err(DaemonError::Persistence(message)) if message.contains("public key does not match")
1234
+ ));
1235
+
1236
+ let mut state = valid_state.clone();
1237
+ state.relay_config.daemon_id_hex = "11".repeat(31);
1238
+ assert!(matches!(
1239
+ validate_loaded_state(&state),
1240
+ Err(DaemonError::Persistence(message)) if message.contains("daemon id must be 32 bytes")
1241
+ ));
1242
+ }
1243
+
234
1244
  include!("tests_parts/part1.rs");
235
1245
  include!("tests_parts/part2.rs");
236
1246
  include!("tests_parts/part3.rs");