@wlfi-agent/cli 1.4.16 → 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.
- package/Cargo.lock +26 -20
- package/Cargo.toml +1 -1
- package/README.md +61 -28
- package/crates/vault-cli-admin/src/io_utils.rs +149 -1
- package/crates/vault-cli-admin/src/main.rs +639 -16
- package/crates/vault-cli-admin/src/shared_config.rs +18 -18
- package/crates/vault-cli-admin/src/tui/token_rpc.rs +190 -3
- package/crates/vault-cli-admin/src/tui/utils.rs +59 -0
- package/crates/vault-cli-admin/src/tui.rs +1205 -120
- package/crates/vault-cli-agent/Cargo.toml +1 -0
- package/crates/vault-cli-agent/src/io_utils.rs +163 -2
- package/crates/vault-cli-agent/src/main.rs +648 -32
- package/crates/vault-cli-daemon/Cargo.toml +4 -0
- package/crates/vault-cli-daemon/src/main.rs +617 -67
- package/crates/vault-cli-daemon/src/relay_sync.rs +776 -4
- package/crates/vault-cli-daemon/tests/system_keychain_helper_acl.rs +5 -0
- package/crates/vault-daemon/src/daemon_parts/api_impl_and_utils.rs +32 -1
- package/crates/vault-daemon/src/persistence.rs +637 -100
- package/crates/vault-daemon/src/tests.rs +1013 -3
- package/crates/vault-daemon/src/tests_parts/part2.rs +99 -0
- package/crates/vault-daemon/src/tests_parts/part4.rs +11 -7
- package/crates/vault-domain/src/nonce.rs +4 -0
- package/crates/vault-domain/src/tests.rs +616 -0
- package/crates/vault-policy/src/engine.rs +55 -32
- package/crates/vault-policy/src/tests.rs +195 -0
- package/crates/vault-sdk-agent/src/lib.rs +415 -22
- package/crates/vault-signer/Cargo.toml +3 -0
- package/crates/vault-signer/src/lib.rs +266 -40
- package/crates/vault-transport-unix/src/lib.rs +653 -5
- package/crates/vault-transport-xpc/src/tests.rs +531 -3
- package/crates/vault-transport-xpc/tests/e2e_flow.rs +3 -0
- package/dist/cli.cjs +663 -190
- package/dist/cli.cjs.map +1 -1
- package/package.json +5 -2
- package/packages/cache/.turbo/turbo-build.log +53 -52
- package/packages/cache/coverage/clover.xml +529 -394
- package/packages/cache/coverage/coverage-final.json +2 -2
- package/packages/cache/coverage/index.html +21 -21
- package/packages/cache/coverage/src/client/index.html +1 -1
- package/packages/cache/coverage/src/client/index.ts.html +1 -1
- package/packages/cache/coverage/src/errors/index.html +1 -1
- package/packages/cache/coverage/src/errors/index.ts.html +12 -12
- package/packages/cache/coverage/src/index.html +1 -1
- package/packages/cache/coverage/src/index.ts.html +1 -1
- package/packages/cache/coverage/src/service/index.html +21 -21
- package/packages/cache/coverage/src/service/index.ts.html +769 -313
- package/packages/cache/dist/{chunk-QNK6GOTI.js → chunk-KC53LH5Z.js} +35 -2
- package/packages/cache/dist/chunk-KC53LH5Z.js.map +1 -0
- package/packages/cache/dist/{chunk-QF4XKEIA.cjs → chunk-UVU7VFE3.cjs} +35 -2
- package/packages/cache/dist/chunk-UVU7VFE3.cjs.map +1 -0
- package/packages/cache/dist/index.cjs +2 -2
- package/packages/cache/dist/index.js +1 -1
- package/packages/cache/dist/service/index.cjs +2 -2
- package/packages/cache/dist/service/index.js +1 -1
- package/packages/cache/node_modules/.bin/tsc +2 -2
- package/packages/cache/node_modules/.bin/tsserver +2 -2
- package/packages/cache/node_modules/.bin/tsup +2 -2
- package/packages/cache/node_modules/.bin/tsup-node +2 -2
- package/packages/cache/node_modules/.bin/vitest +4 -4
- package/packages/cache/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/packages/cache/src/service/index.test.ts +165 -19
- package/packages/cache/src/service/index.ts +38 -1
- package/packages/config/.turbo/turbo-build.log +18 -17
- package/packages/config/dist/index.cjs +0 -17
- package/packages/config/dist/index.cjs.map +1 -1
- package/packages/config/src/index.ts +0 -17
- package/packages/rpc/.turbo/turbo-build.log +32 -31
- package/packages/rpc/dist/index.cjs +0 -17
- package/packages/rpc/dist/index.cjs.map +1 -1
- package/packages/rpc/src/index.js +1 -0
- package/packages/ui/.turbo/turbo-build.log +44 -43
- package/packages/ui/dist/components/badge.d.ts +1 -1
- package/packages/ui/dist/components/button.d.ts +1 -1
- package/packages/ui/node_modules/.bin/tsc +2 -2
- package/packages/ui/node_modules/.bin/tsserver +2 -2
- package/packages/ui/node_modules/.bin/tsup +2 -2
- package/packages/ui/node_modules/.bin/tsup-node +2 -2
- package/scripts/install-cli-launcher.mjs +37 -0
- package/scripts/install-rust-binaries.mjs +112 -0
- package/scripts/run-tests-isolated.mjs +210 -0
- package/src/cli.ts +310 -50
- package/src/lib/admin-reset.ts +15 -30
- package/src/lib/admin-setup.ts +246 -55
- package/src/lib/agent-auth-migrate.ts +5 -1
- package/src/lib/asset-broadcast.ts +15 -4
- package/src/lib/config-amounts.ts +6 -4
- package/src/lib/hidden-tty-prompt.js +1 -0
- package/src/lib/hidden-tty-prompt.ts +105 -0
- package/src/lib/keychain.ts +1 -0
- package/src/lib/local-admin-access.ts +4 -29
- package/src/lib/rust.ts +129 -33
- package/src/lib/signed-tx.ts +1 -0
- package/src/lib/sudo.ts +15 -5
- package/src/lib/wallet-profile.ts +3 -0
- package/src/lib/wallet-setup.ts +52 -0
- package/packages/cache/dist/chunk-QF4XKEIA.cjs.map +0 -1
- package/packages/cache/dist/chunk-QNK6GOTI.js.map +0 -1
|
@@ -828,18 +828,121 @@ fn format_time(value: OffsetDateTime) -> Result<String> {
|
|
|
828
828
|
|
|
829
829
|
#[cfg(test)]
|
|
830
830
|
mod tests {
|
|
831
|
-
use super::{
|
|
831
|
+
use super::{
|
|
832
|
+
action_name, apply_daemon_auth, approval_metadata, asset_token_address,
|
|
833
|
+
flatten_policy_records, format_time, manual_approval_error_feedback,
|
|
834
|
+
manual_approval_feedback, map_approval_status, normalize_secret, policy_metadata,
|
|
835
|
+
poll_updates, policy_name, process_update, register_snapshot, resolve_relay_daemon_token_with,
|
|
836
|
+
submit_feedback, ProcessedFeedback, RelayEncryptedPayload,
|
|
837
|
+
RelayEncryptedUpdateRecord,
|
|
838
|
+
};
|
|
832
839
|
use std::collections::BTreeMap;
|
|
840
|
+
use std::collections::BTreeSet;
|
|
833
841
|
use std::fs;
|
|
834
842
|
use std::path::Path;
|
|
843
|
+
use std::sync::{Mutex, OnceLock};
|
|
835
844
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
845
|
+
use chacha20poly1305::aead::Aead;
|
|
846
|
+
use chacha20poly1305::{KeyInit, XChaCha20Poly1305};
|
|
847
|
+
use reqwest::Client;
|
|
836
848
|
use time::OffsetDateTime;
|
|
849
|
+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
850
|
+
use tokio::net::TcpListener;
|
|
851
|
+
use tokio::sync::oneshot;
|
|
837
852
|
use uuid::Uuid;
|
|
838
|
-
use vault_daemon::DaemonError;
|
|
853
|
+
use vault_daemon::{DaemonConfig, DaemonError, InMemoryDaemon, RelayRegistrationSnapshot};
|
|
839
854
|
use vault_domain::{
|
|
840
|
-
AgentAction, AssetId,
|
|
841
|
-
ManualApprovalStatus,
|
|
855
|
+
AgentAction, AgentKey, AssetId, BroadcastTx, Eip3009Transfer, EntityScope,
|
|
856
|
+
ManualApprovalDecision, ManualApprovalRequest, ManualApprovalStatus, Permit2Permit,
|
|
857
|
+
PolicyAttachment, PolicyType, RelayConfig, SpendingPolicy,
|
|
842
858
|
};
|
|
859
|
+
use vault_signer::SoftwareSignerBackend;
|
|
860
|
+
|
|
861
|
+
fn env_lock() -> &'static Mutex<()> {
|
|
862
|
+
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
|
863
|
+
ENV_LOCK.get_or_init(|| Mutex::new(()))
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
#[derive(Debug)]
|
|
867
|
+
struct CapturedHttpRequest {
|
|
868
|
+
request_line: String,
|
|
869
|
+
headers: BTreeMap<String, String>,
|
|
870
|
+
body: String,
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
async fn spawn_single_response_server(
|
|
874
|
+
status_line: &str,
|
|
875
|
+
response_body: &str,
|
|
876
|
+
) -> (
|
|
877
|
+
String,
|
|
878
|
+
oneshot::Receiver<CapturedHttpRequest>,
|
|
879
|
+
tokio::task::JoinHandle<()>,
|
|
880
|
+
) {
|
|
881
|
+
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
|
|
882
|
+
let addr = listener.local_addr().expect("addr");
|
|
883
|
+
let status_line = status_line.to_string();
|
|
884
|
+
let response_body = response_body.to_string();
|
|
885
|
+
let (tx, rx) = oneshot::channel();
|
|
886
|
+
|
|
887
|
+
let handle = tokio::spawn(async move {
|
|
888
|
+
let (mut stream, _) = listener.accept().await.expect("accept");
|
|
889
|
+
let mut buffer = Vec::new();
|
|
890
|
+
let header_end = loop {
|
|
891
|
+
let mut chunk = [0u8; 1024];
|
|
892
|
+
let read = stream.read(&mut chunk).await.expect("read request");
|
|
893
|
+
if read == 0 {
|
|
894
|
+
panic!("connection closed before headers");
|
|
895
|
+
}
|
|
896
|
+
buffer.extend_from_slice(&chunk[..read]);
|
|
897
|
+
if let Some(position) = buffer.windows(4).position(|window| window == b"\r\n\r\n") {
|
|
898
|
+
break position + 4;
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
let header_text = String::from_utf8_lossy(&buffer[..header_end]).to_string();
|
|
903
|
+
let mut lines = header_text.split("\r\n");
|
|
904
|
+
let request_line = lines.next().expect("request line").to_string();
|
|
905
|
+
let mut headers = BTreeMap::new();
|
|
906
|
+
let mut content_length = 0usize;
|
|
907
|
+
for line in lines.filter(|line| !line.is_empty()) {
|
|
908
|
+
let (name, value) = line.split_once(':').expect("header");
|
|
909
|
+
let value = value.trim().to_string();
|
|
910
|
+
if name.eq_ignore_ascii_case("content-length") {
|
|
911
|
+
content_length = value.parse::<usize>().expect("content length");
|
|
912
|
+
}
|
|
913
|
+
headers.insert(name.to_ascii_lowercase(), value);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
let mut body_bytes = buffer[header_end..].to_vec();
|
|
917
|
+
while body_bytes.len() < content_length {
|
|
918
|
+
let mut chunk = vec![0u8; content_length - body_bytes.len()];
|
|
919
|
+
let read = stream.read(&mut chunk).await.expect("read body");
|
|
920
|
+
if read == 0 {
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
body_bytes.extend_from_slice(&chunk[..read]);
|
|
924
|
+
}
|
|
925
|
+
let body = String::from_utf8(body_bytes[..content_length].to_vec()).expect("utf8 body");
|
|
926
|
+
|
|
927
|
+
let response = format!(
|
|
928
|
+
"HTTP/1.1 {status_line}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
|
|
929
|
+
response_body.len(),
|
|
930
|
+
response_body
|
|
931
|
+
);
|
|
932
|
+
stream
|
|
933
|
+
.write_all(response.as_bytes())
|
|
934
|
+
.await
|
|
935
|
+
.expect("write response");
|
|
936
|
+
|
|
937
|
+
let _ = tx.send(CapturedHttpRequest {
|
|
938
|
+
request_line,
|
|
939
|
+
headers,
|
|
940
|
+
body,
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
(format!("http://{}", addr), rx, handle)
|
|
945
|
+
}
|
|
843
946
|
|
|
844
947
|
#[test]
|
|
845
948
|
fn approval_metadata_includes_admin_reissue_token_and_public_hash_for_pending_requests() {
|
|
@@ -892,6 +995,113 @@ mod tests {
|
|
|
892
995
|
))
|
|
893
996
|
}
|
|
894
997
|
|
|
998
|
+
fn sample_address(value: &str) -> vault_domain::EvmAddress {
|
|
999
|
+
value.parse().expect("address")
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
fn sample_policy(policy_type: PolicyType) -> SpendingPolicy {
|
|
1003
|
+
SpendingPolicy::new(
|
|
1004
|
+
1,
|
|
1005
|
+
policy_type,
|
|
1006
|
+
1_000,
|
|
1007
|
+
EntityScope::All,
|
|
1008
|
+
EntityScope::All,
|
|
1009
|
+
EntityScope::All,
|
|
1010
|
+
)
|
|
1011
|
+
.expect("policy")
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
fn sample_manual_request(status: ManualApprovalStatus) -> ManualApprovalRequest {
|
|
1015
|
+
ManualApprovalRequest {
|
|
1016
|
+
id: Uuid::parse_str("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa").expect("uuid"),
|
|
1017
|
+
agent_key_id: Uuid::parse_str("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb").expect("uuid"),
|
|
1018
|
+
vault_key_id: Uuid::parse_str("cccccccc-cccc-4ccc-8ccc-cccccccccccc").expect("uuid"),
|
|
1019
|
+
request_payload_hash_hex: "11".repeat(32),
|
|
1020
|
+
action: AgentAction::TransferNative {
|
|
1021
|
+
chain_id: 56,
|
|
1022
|
+
to: sample_address("0x2222222222222222222222222222222222222222"),
|
|
1023
|
+
amount_wei: 1,
|
|
1024
|
+
},
|
|
1025
|
+
chain_id: 56,
|
|
1026
|
+
asset: AssetId::NativeEth,
|
|
1027
|
+
recipient: sample_address("0x2222222222222222222222222222222222222222"),
|
|
1028
|
+
amount_wei: 1,
|
|
1029
|
+
created_at: OffsetDateTime::UNIX_EPOCH,
|
|
1030
|
+
updated_at: OffsetDateTime::UNIX_EPOCH,
|
|
1031
|
+
status,
|
|
1032
|
+
triggered_by_policy_ids: vec![Uuid::nil()],
|
|
1033
|
+
completed_at: None,
|
|
1034
|
+
rejection_reason: None,
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
fn sample_snapshot(relay_url: &str) -> RelayRegistrationSnapshot {
|
|
1039
|
+
RelayRegistrationSnapshot {
|
|
1040
|
+
relay_config: RelayConfig {
|
|
1041
|
+
relay_url: Some(relay_url.to_string()),
|
|
1042
|
+
frontend_url: Some("https://frontend.example".to_string()),
|
|
1043
|
+
daemon_id_hex: "aa".repeat(32),
|
|
1044
|
+
daemon_public_key_hex: "bb".repeat(32),
|
|
1045
|
+
},
|
|
1046
|
+
relay_private_key_hex: "11".repeat(32),
|
|
1047
|
+
vault_public_key_hex: Some("04".repeat(33)),
|
|
1048
|
+
ethereum_address: Some("0x9999999999999999999999999999999999999999".to_string()),
|
|
1049
|
+
policies: vec![sample_policy(PolicyType::PerTxMaxSpending)],
|
|
1050
|
+
agent_keys: vec![AgentKey {
|
|
1051
|
+
id: Uuid::parse_str("dddddddd-dddd-4ddd-8ddd-dddddddddddd").expect("uuid"),
|
|
1052
|
+
vault_key_id: Uuid::parse_str("eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee").expect("uuid"),
|
|
1053
|
+
policies: PolicyAttachment::AllPolicies,
|
|
1054
|
+
created_at: OffsetDateTime::UNIX_EPOCH,
|
|
1055
|
+
}],
|
|
1056
|
+
manual_approval_requests: vec![sample_manual_request(ManualApprovalStatus::Pending)],
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
fn encrypt_payload(
|
|
1061
|
+
daemon_public_key_hex: &str,
|
|
1062
|
+
plaintext: &[u8],
|
|
1063
|
+
) -> RelayEncryptedPayload {
|
|
1064
|
+
let public_bytes = hex::decode(daemon_public_key_hex).expect("decode daemon public key");
|
|
1065
|
+
let peer_public = x25519_dalek::PublicKey::from(
|
|
1066
|
+
<[u8; 32]>::try_from(public_bytes.as_slice()).expect("public key bytes"),
|
|
1067
|
+
);
|
|
1068
|
+
let secret = x25519_dalek::StaticSecret::from([7u8; 32]);
|
|
1069
|
+
let shared_secret = secret.diffie_hellman(&peer_public);
|
|
1070
|
+
let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into());
|
|
1071
|
+
let nonce = [9u8; 24];
|
|
1072
|
+
let ciphertext = cipher
|
|
1073
|
+
.encrypt(chacha20poly1305::XNonce::from_slice(&nonce), plaintext)
|
|
1074
|
+
.expect("encrypt payload");
|
|
1075
|
+
|
|
1076
|
+
RelayEncryptedPayload {
|
|
1077
|
+
algorithm: "x25519-xchacha20poly1305-v1".to_string(),
|
|
1078
|
+
ciphertext_base64: hex::encode(ciphertext),
|
|
1079
|
+
encapsulated_key_base64: hex::encode(x25519_dalek::PublicKey::from(&secret).as_bytes()),
|
|
1080
|
+
nonce_base64: hex::encode(nonce),
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
fn sample_update_record(
|
|
1085
|
+
daemon_id: &str,
|
|
1086
|
+
payload: RelayEncryptedPayload,
|
|
1087
|
+
) -> RelayEncryptedUpdateRecord {
|
|
1088
|
+
RelayEncryptedUpdateRecord {
|
|
1089
|
+
claim_token: Some("claim-token".to_string()),
|
|
1090
|
+
daemon_id: daemon_id.to_string(),
|
|
1091
|
+
payload,
|
|
1092
|
+
target_approval_request_id: None,
|
|
1093
|
+
r#type: "manual_approval_decision".to_string(),
|
|
1094
|
+
update_id: "update-1".to_string(),
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
fn test_runtime() -> tokio::runtime::Runtime {
|
|
1099
|
+
tokio::runtime::Builder::new_current_thread()
|
|
1100
|
+
.enable_all()
|
|
1101
|
+
.build()
|
|
1102
|
+
.expect("runtime")
|
|
1103
|
+
}
|
|
1104
|
+
|
|
895
1105
|
#[test]
|
|
896
1106
|
fn resolve_relay_daemon_token_prefers_explicit_env_value() {
|
|
897
1107
|
let env = BTreeMap::from([
|
|
@@ -1011,4 +1221,566 @@ mod tests {
|
|
|
1011
1221
|
.is_some_and(|message| message.contains("already Rejected"))
|
|
1012
1222
|
);
|
|
1013
1223
|
}
|
|
1224
|
+
|
|
1225
|
+
#[test]
|
|
1226
|
+
fn manual_approval_feedback_and_reject_error_feedback_cover_remaining_paths() {
|
|
1227
|
+
let approval_request_id =
|
|
1228
|
+
Uuid::parse_str("dddddddd-dddd-4ddd-8ddd-dddddddddddd").expect("uuid");
|
|
1229
|
+
|
|
1230
|
+
let feedback = manual_approval_feedback(
|
|
1231
|
+
approval_request_id,
|
|
1232
|
+
ManualApprovalStatus::Approved,
|
|
1233
|
+
Some(" "),
|
|
1234
|
+
"done".to_string(),
|
|
1235
|
+
);
|
|
1236
|
+
assert_eq!(feedback.status, "applied");
|
|
1237
|
+
assert_eq!(feedback.message.as_deref(), Some("done"));
|
|
1238
|
+
assert!(
|
|
1239
|
+
!feedback
|
|
1240
|
+
.details
|
|
1241
|
+
.as_ref()
|
|
1242
|
+
.expect("details")
|
|
1243
|
+
.contains_key("note")
|
|
1244
|
+
);
|
|
1245
|
+
|
|
1246
|
+
let reject_feedback = manual_approval_error_feedback(
|
|
1247
|
+
approval_request_id,
|
|
1248
|
+
ManualApprovalDecision::Reject,
|
|
1249
|
+
Some("because"),
|
|
1250
|
+
&DaemonError::Transport("boom".to_string()),
|
|
1251
|
+
);
|
|
1252
|
+
assert_eq!(reject_feedback.status, "rejected");
|
|
1253
|
+
assert!(reject_feedback.details.is_none());
|
|
1254
|
+
assert_eq!(reject_feedback.message.as_deref(), Some("transport error: boom"));
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
#[test]
|
|
1258
|
+
fn normalize_secret_trims_and_rejects_blank_values() {
|
|
1259
|
+
assert_eq!(normalize_secret(" token ").as_deref(), Some("token"));
|
|
1260
|
+
assert_eq!(normalize_secret("\n\t "), None);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
#[test]
|
|
1264
|
+
fn policy_name_covers_all_policy_types() {
|
|
1265
|
+
assert_eq!(
|
|
1266
|
+
policy_name(&sample_policy(PolicyType::DailyMaxSpending)),
|
|
1267
|
+
"daily_max_spending"
|
|
1268
|
+
);
|
|
1269
|
+
assert_eq!(
|
|
1270
|
+
policy_name(&sample_policy(PolicyType::DailyMaxTxCount)),
|
|
1271
|
+
"daily_max_tx_count"
|
|
1272
|
+
);
|
|
1273
|
+
assert_eq!(
|
|
1274
|
+
policy_name(&sample_policy(PolicyType::WeeklyMaxSpending)),
|
|
1275
|
+
"weekly_max_spending"
|
|
1276
|
+
);
|
|
1277
|
+
assert_eq!(
|
|
1278
|
+
policy_name(&sample_policy(PolicyType::PerTxMaxSpending)),
|
|
1279
|
+
"per_tx_max_spending"
|
|
1280
|
+
);
|
|
1281
|
+
assert_eq!(
|
|
1282
|
+
policy_name(&sample_policy(PolicyType::PerTxMaxFeePerGas)),
|
|
1283
|
+
"per_tx_max_fee_per_gas"
|
|
1284
|
+
);
|
|
1285
|
+
assert_eq!(
|
|
1286
|
+
policy_name(&sample_policy(PolicyType::PerTxMaxPriorityFeePerGas)),
|
|
1287
|
+
"per_tx_max_priority_fee_per_gas"
|
|
1288
|
+
);
|
|
1289
|
+
assert_eq!(
|
|
1290
|
+
policy_name(&sample_policy(PolicyType::PerTxMaxCalldataBytes)),
|
|
1291
|
+
"per_tx_max_calldata_bytes"
|
|
1292
|
+
);
|
|
1293
|
+
assert_eq!(
|
|
1294
|
+
policy_name(&sample_policy(PolicyType::PerChainMaxGasSpend)),
|
|
1295
|
+
"per_chain_max_gas_spend"
|
|
1296
|
+
);
|
|
1297
|
+
assert_eq!(
|
|
1298
|
+
policy_name(&sample_policy(PolicyType::ManualApproval)),
|
|
1299
|
+
"manual_approval"
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
#[test]
|
|
1304
|
+
fn action_name_covers_all_agent_actions() {
|
|
1305
|
+
assert_eq!(
|
|
1306
|
+
action_name(&AgentAction::Approve {
|
|
1307
|
+
chain_id: 1,
|
|
1308
|
+
token: sample_address("0x1111111111111111111111111111111111111111"),
|
|
1309
|
+
spender: sample_address("0x2222222222222222222222222222222222222222"),
|
|
1310
|
+
amount_wei: 1,
|
|
1311
|
+
}),
|
|
1312
|
+
"approve"
|
|
1313
|
+
);
|
|
1314
|
+
assert_eq!(
|
|
1315
|
+
action_name(&AgentAction::Transfer {
|
|
1316
|
+
chain_id: 1,
|
|
1317
|
+
token: sample_address("0x1111111111111111111111111111111111111111"),
|
|
1318
|
+
to: sample_address("0x2222222222222222222222222222222222222222"),
|
|
1319
|
+
amount_wei: 1,
|
|
1320
|
+
}),
|
|
1321
|
+
"transfer"
|
|
1322
|
+
);
|
|
1323
|
+
assert_eq!(
|
|
1324
|
+
action_name(&AgentAction::TransferNative {
|
|
1325
|
+
chain_id: 1,
|
|
1326
|
+
to: sample_address("0x2222222222222222222222222222222222222222"),
|
|
1327
|
+
amount_wei: 1,
|
|
1328
|
+
}),
|
|
1329
|
+
"transfer_native"
|
|
1330
|
+
);
|
|
1331
|
+
assert_eq!(
|
|
1332
|
+
action_name(&AgentAction::Permit2Permit {
|
|
1333
|
+
permit: Permit2Permit {
|
|
1334
|
+
chain_id: 1,
|
|
1335
|
+
permit2_contract: sample_address(
|
|
1336
|
+
"0x4444444444444444444444444444444444444444"
|
|
1337
|
+
),
|
|
1338
|
+
token: sample_address("0x1111111111111111111111111111111111111111"),
|
|
1339
|
+
spender: sample_address("0x2222222222222222222222222222222222222222"),
|
|
1340
|
+
amount_wei: 1,
|
|
1341
|
+
expiration: 1,
|
|
1342
|
+
nonce: 1,
|
|
1343
|
+
sig_deadline: 1,
|
|
1344
|
+
},
|
|
1345
|
+
}),
|
|
1346
|
+
"permit2_permit"
|
|
1347
|
+
);
|
|
1348
|
+
assert_eq!(
|
|
1349
|
+
action_name(&AgentAction::Eip3009TransferWithAuthorization {
|
|
1350
|
+
authorization: Eip3009Transfer {
|
|
1351
|
+
chain_id: 1,
|
|
1352
|
+
token: sample_address("0x1111111111111111111111111111111111111111"),
|
|
1353
|
+
token_name: "USD Coin".to_string(),
|
|
1354
|
+
token_version: Some("2".to_string()),
|
|
1355
|
+
from: sample_address("0x2222222222222222222222222222222222222222"),
|
|
1356
|
+
to: sample_address("0x3333333333333333333333333333333333333333"),
|
|
1357
|
+
amount_wei: 1,
|
|
1358
|
+
valid_after: 1,
|
|
1359
|
+
valid_before: 2,
|
|
1360
|
+
nonce_hex: format!("0x{}", "07".repeat(32)),
|
|
1361
|
+
},
|
|
1362
|
+
}),
|
|
1363
|
+
"eip3009_transfer_with_authorization"
|
|
1364
|
+
);
|
|
1365
|
+
assert_eq!(
|
|
1366
|
+
action_name(&AgentAction::Eip3009ReceiveWithAuthorization {
|
|
1367
|
+
authorization: Eip3009Transfer {
|
|
1368
|
+
chain_id: 1,
|
|
1369
|
+
token: sample_address("0x1111111111111111111111111111111111111111"),
|
|
1370
|
+
token_name: "USD Coin".to_string(),
|
|
1371
|
+
token_version: None,
|
|
1372
|
+
from: sample_address("0x2222222222222222222222222222222222222222"),
|
|
1373
|
+
to: sample_address("0x3333333333333333333333333333333333333333"),
|
|
1374
|
+
amount_wei: 1,
|
|
1375
|
+
valid_after: 1,
|
|
1376
|
+
valid_before: 2,
|
|
1377
|
+
nonce_hex: format!("0x{}", "09".repeat(32)),
|
|
1378
|
+
},
|
|
1379
|
+
}),
|
|
1380
|
+
"eip3009_receive_with_authorization"
|
|
1381
|
+
);
|
|
1382
|
+
assert_eq!(
|
|
1383
|
+
action_name(&AgentAction::BroadcastTx {
|
|
1384
|
+
tx: BroadcastTx {
|
|
1385
|
+
chain_id: 1,
|
|
1386
|
+
nonce: 1,
|
|
1387
|
+
to: sample_address("0x2222222222222222222222222222222222222222"),
|
|
1388
|
+
value_wei: 0,
|
|
1389
|
+
data_hex: "0x".to_string(),
|
|
1390
|
+
gas_limit: 21_000,
|
|
1391
|
+
max_fee_per_gas_wei: 2,
|
|
1392
|
+
max_priority_fee_per_gas_wei: 1,
|
|
1393
|
+
tx_type: 0x02,
|
|
1394
|
+
delegation_enabled: false,
|
|
1395
|
+
},
|
|
1396
|
+
}),
|
|
1397
|
+
"broadcast_tx"
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
#[test]
|
|
1402
|
+
fn map_approval_status_covers_all_statuses() {
|
|
1403
|
+
assert_eq!(map_approval_status(ManualApprovalStatus::Pending), "pending");
|
|
1404
|
+
assert_eq!(map_approval_status(ManualApprovalStatus::Approved), "approved");
|
|
1405
|
+
assert_eq!(map_approval_status(ManualApprovalStatus::Rejected), "rejected");
|
|
1406
|
+
assert_eq!(map_approval_status(ManualApprovalStatus::Completed), "completed");
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
#[test]
|
|
1410
|
+
fn asset_token_address_covers_native_and_erc20_assets() {
|
|
1411
|
+
assert_eq!(asset_token_address(&AssetId::NativeEth), None);
|
|
1412
|
+
assert_eq!(
|
|
1413
|
+
asset_token_address(&AssetId::Erc20(sample_address(
|
|
1414
|
+
"0x1111111111111111111111111111111111111111"
|
|
1415
|
+
)))
|
|
1416
|
+
.as_deref(),
|
|
1417
|
+
Some("0x1111111111111111111111111111111111111111")
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
#[test]
|
|
1422
|
+
fn flatten_policy_records_expands_scopes_and_skips_disabled_policies() {
|
|
1423
|
+
let mut policy = SpendingPolicy::new_manual_approval(
|
|
1424
|
+
5,
|
|
1425
|
+
10,
|
|
1426
|
+
100,
|
|
1427
|
+
EntityScope::Set(BTreeSet::from([
|
|
1428
|
+
sample_address("0x1111111111111111111111111111111111111111"),
|
|
1429
|
+
sample_address("0x2222222222222222222222222222222222222222"),
|
|
1430
|
+
])),
|
|
1431
|
+
EntityScope::Set(BTreeSet::from([
|
|
1432
|
+
AssetId::NativeEth,
|
|
1433
|
+
AssetId::Erc20(sample_address("0x3333333333333333333333333333333333333333")),
|
|
1434
|
+
])),
|
|
1435
|
+
EntityScope::Set(BTreeSet::from([1, 10])),
|
|
1436
|
+
)
|
|
1437
|
+
.expect("policy");
|
|
1438
|
+
let mut disabled = sample_policy(PolicyType::PerTxMaxSpending);
|
|
1439
|
+
disabled.enabled = false;
|
|
1440
|
+
policy.id = Uuid::parse_str("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa").expect("uuid");
|
|
1441
|
+
|
|
1442
|
+
let records = flatten_policy_records(&[policy.clone(), disabled], "2026-03-11T00:00:00Z");
|
|
1443
|
+
assert_eq!(records.len(), 8);
|
|
1444
|
+
assert!(records.iter().all(|record| record.policy_id == policy.id.to_string()));
|
|
1445
|
+
assert!(records.iter().all(|record| record.requires_manual_approval));
|
|
1446
|
+
assert!(records.iter().all(|record| record.scope == "override"));
|
|
1447
|
+
assert!(records.iter().any(|record| record.token_address.is_none()));
|
|
1448
|
+
assert!(records.iter().any(|record| {
|
|
1449
|
+
record.token_address.as_deref()
|
|
1450
|
+
== Some("0x3333333333333333333333333333333333333333")
|
|
1451
|
+
}));
|
|
1452
|
+
assert!(records.iter().all(|record| record.metadata.is_some()));
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
#[test]
|
|
1456
|
+
fn policy_metadata_covers_scope_specific_fields() {
|
|
1457
|
+
let policy = SpendingPolicy::new_manual_approval(
|
|
1458
|
+
1,
|
|
1459
|
+
10,
|
|
1460
|
+
100,
|
|
1461
|
+
EntityScope::Set(BTreeSet::from([sample_address(
|
|
1462
|
+
"0x1111111111111111111111111111111111111111",
|
|
1463
|
+
)])),
|
|
1464
|
+
EntityScope::Set(BTreeSet::from([AssetId::Erc20(sample_address(
|
|
1465
|
+
"0x3333333333333333333333333333333333333333",
|
|
1466
|
+
))])),
|
|
1467
|
+
EntityScope::Set(BTreeSet::from([1])),
|
|
1468
|
+
)
|
|
1469
|
+
.expect("policy");
|
|
1470
|
+
|
|
1471
|
+
let metadata = policy_metadata(
|
|
1472
|
+
&policy,
|
|
1473
|
+
Some(&AssetId::Erc20(sample_address(
|
|
1474
|
+
"0x3333333333333333333333333333333333333333",
|
|
1475
|
+
))),
|
|
1476
|
+
Some(1),
|
|
1477
|
+
"override",
|
|
1478
|
+
);
|
|
1479
|
+
assert_eq!(metadata.get("policyType").map(String::as_str), Some("manual_approval"));
|
|
1480
|
+
assert_eq!(metadata.get("scope").map(String::as_str), Some("override"));
|
|
1481
|
+
assert_eq!(metadata.get("recipientScope").map(String::as_str), Some("set"));
|
|
1482
|
+
assert_eq!(metadata.get("assetScope").map(String::as_str), Some("set"));
|
|
1483
|
+
assert_eq!(metadata.get("networkScope").map(String::as_str), Some("set"));
|
|
1484
|
+
assert_eq!(metadata.get("asset").map(String::as_str), Some("erc20:0x3333333333333333333333333333333333333333"));
|
|
1485
|
+
assert_eq!(metadata.get("chainId").map(String::as_str), Some("1"));
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
#[test]
|
|
1489
|
+
fn approval_metadata_omits_capability_for_non_pending_requests() {
|
|
1490
|
+
let mut request = ManualApprovalRequest {
|
|
1491
|
+
id: Uuid::parse_str("cccccccc-cccc-4ccc-8ccc-cccccccccccc").expect("uuid"),
|
|
1492
|
+
agent_key_id: Uuid::nil(),
|
|
1493
|
+
vault_key_id: Uuid::nil(),
|
|
1494
|
+
request_payload_hash_hex: "22".repeat(32),
|
|
1495
|
+
action: AgentAction::TransferNative {
|
|
1496
|
+
chain_id: 56,
|
|
1497
|
+
to: sample_address("0x2222222222222222222222222222222222222222"),
|
|
1498
|
+
amount_wei: 1,
|
|
1499
|
+
},
|
|
1500
|
+
chain_id: 56,
|
|
1501
|
+
asset: AssetId::NativeEth,
|
|
1502
|
+
recipient: sample_address("0x2222222222222222222222222222222222222222"),
|
|
1503
|
+
amount_wei: 1,
|
|
1504
|
+
created_at: OffsetDateTime::UNIX_EPOCH,
|
|
1505
|
+
updated_at: OffsetDateTime::UNIX_EPOCH,
|
|
1506
|
+
status: ManualApprovalStatus::Completed,
|
|
1507
|
+
triggered_by_policy_ids: vec![Uuid::nil()],
|
|
1508
|
+
completed_at: Some(OffsetDateTime::UNIX_EPOCH),
|
|
1509
|
+
rejection_reason: None,
|
|
1510
|
+
};
|
|
1511
|
+
|
|
1512
|
+
let metadata = approval_metadata(&request, &"11".repeat(32)).expect("metadata");
|
|
1513
|
+
assert_eq!(
|
|
1514
|
+
metadata.get("triggeredPolicyIds").map(String::as_str),
|
|
1515
|
+
Some("00000000-0000-0000-0000-000000000000")
|
|
1516
|
+
);
|
|
1517
|
+
assert_eq!(metadata.get("asset").map(String::as_str), Some("native_eth"));
|
|
1518
|
+
assert!(!metadata.contains_key("approvalCapabilityToken"));
|
|
1519
|
+
assert!(!metadata.contains_key("approvalCapabilityHash"));
|
|
1520
|
+
|
|
1521
|
+
request.status = ManualApprovalStatus::Rejected;
|
|
1522
|
+
let metadata = approval_metadata(&request, " ").expect("metadata");
|
|
1523
|
+
assert!(!metadata.contains_key("approvalCapabilityToken"));
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
#[test]
|
|
1527
|
+
fn format_time_renders_rfc3339() {
|
|
1528
|
+
let rendered = format_time(OffsetDateTime::UNIX_EPOCH).expect("format time");
|
|
1529
|
+
assert_eq!(rendered, "1970-01-01T00:00:00Z");
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
#[test]
|
|
1533
|
+
fn apply_daemon_auth_adds_header_when_token_env_is_present() {
|
|
1534
|
+
let _guard = env_lock().lock().expect("env lock");
|
|
1535
|
+
std::env::set_var("WLFI_RELAY_DAEMON_TOKEN", "relay-secret");
|
|
1536
|
+
std::env::remove_var("WLFI_RELAY_DAEMON_TOKEN_FILE");
|
|
1537
|
+
|
|
1538
|
+
let client = Client::new();
|
|
1539
|
+
let request = apply_daemon_auth(client.get("https://relay.example"))
|
|
1540
|
+
.build()
|
|
1541
|
+
.expect("build request");
|
|
1542
|
+
assert_eq!(
|
|
1543
|
+
request
|
|
1544
|
+
.headers()
|
|
1545
|
+
.get("x-relay-daemon-token")
|
|
1546
|
+
.and_then(|value| value.to_str().ok()),
|
|
1547
|
+
Some("relay-secret")
|
|
1548
|
+
);
|
|
1549
|
+
|
|
1550
|
+
std::env::remove_var("WLFI_RELAY_DAEMON_TOKEN");
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
#[tokio::test]
|
|
1554
|
+
async fn register_snapshot_posts_expected_payload_and_header() {
|
|
1555
|
+
let _guard = env_lock().lock().expect("env lock");
|
|
1556
|
+
std::env::set_var("WLFI_RELAY_DAEMON_TOKEN", "relay-secret");
|
|
1557
|
+
std::env::set_var("HOSTNAME", "daemon-host");
|
|
1558
|
+
|
|
1559
|
+
let (base_url, request_rx, handle) = spawn_single_response_server("200 OK", "{}").await;
|
|
1560
|
+
let snapshot = sample_snapshot(&base_url);
|
|
1561
|
+
let client = Client::new();
|
|
1562
|
+
|
|
1563
|
+
register_snapshot(
|
|
1564
|
+
&client,
|
|
1565
|
+
&base_url,
|
|
1566
|
+
"software",
|
|
1567
|
+
"2026-03-11T00:00:00Z",
|
|
1568
|
+
&snapshot,
|
|
1569
|
+
snapshot.ethereum_address.as_deref().expect("eth address"),
|
|
1570
|
+
)
|
|
1571
|
+
.await
|
|
1572
|
+
.expect("register snapshot");
|
|
1573
|
+
|
|
1574
|
+
let captured = request_rx.await.expect("captured request");
|
|
1575
|
+
handle.await.expect("server");
|
|
1576
|
+
assert_eq!(captured.request_line, "POST /v1/daemon/register HTTP/1.1");
|
|
1577
|
+
assert_eq!(
|
|
1578
|
+
captured.headers.get("x-relay-daemon-token").map(String::as_str),
|
|
1579
|
+
Some("relay-secret")
|
|
1580
|
+
);
|
|
1581
|
+
assert!(captured.body.contains("\"daemonId\":\""));
|
|
1582
|
+
assert!(captured.body.contains("\"signerBackend\":\"software\""));
|
|
1583
|
+
assert!(captured.body.contains("\"label\":\"daemon-host\""));
|
|
1584
|
+
assert!(captured.body.contains("\"approvalRequests\""));
|
|
1585
|
+
|
|
1586
|
+
std::env::remove_var("WLFI_RELAY_DAEMON_TOKEN");
|
|
1587
|
+
std::env::remove_var("HOSTNAME");
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
#[tokio::test]
|
|
1591
|
+
async fn poll_updates_posts_expected_request_and_deserializes_response() {
|
|
1592
|
+
let response_body = r#"{"items":[{"claimToken":"claim-token","daemonId":"daemon-1","payload":{"algorithm":"x25519-xchacha20poly1305-v1","ciphertextBase64":"aa","encapsulatedKeyBase64":"bb","nonceBase64":"cc"},"type":"manual_approval_decision","updateId":"update-1"}]}"#;
|
|
1593
|
+
let (base_url, request_rx, handle) =
|
|
1594
|
+
spawn_single_response_server("200 OK", &response_body).await;
|
|
1595
|
+
let client = Client::new();
|
|
1596
|
+
|
|
1597
|
+
let response = poll_updates(&client, &base_url, "daemon-1")
|
|
1598
|
+
.await
|
|
1599
|
+
.expect("poll response");
|
|
1600
|
+
let captured = request_rx.await.expect("captured request");
|
|
1601
|
+
handle.await.expect("server");
|
|
1602
|
+
|
|
1603
|
+
assert_eq!(captured.request_line, "POST /v1/daemon/poll-updates HTTP/1.1");
|
|
1604
|
+
assert!(captured.body.contains("\"daemonId\":\"daemon-1\""));
|
|
1605
|
+
assert!(captured.body.contains("\"leaseSeconds\":30"));
|
|
1606
|
+
assert!(captured.body.contains("\"limit\":25"));
|
|
1607
|
+
assert_eq!(response.items.len(), 1);
|
|
1608
|
+
assert_eq!(response.items[0].daemon_id, "daemon-1");
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
#[tokio::test]
|
|
1612
|
+
async fn submit_feedback_posts_expected_payload() {
|
|
1613
|
+
let (base_url, request_rx, handle) = spawn_single_response_server("200 OK", "{}").await;
|
|
1614
|
+
let client = Client::new();
|
|
1615
|
+
|
|
1616
|
+
submit_feedback(
|
|
1617
|
+
&client,
|
|
1618
|
+
&base_url,
|
|
1619
|
+
"claim-token",
|
|
1620
|
+
"daemon-1",
|
|
1621
|
+
"update-1",
|
|
1622
|
+
ProcessedFeedback {
|
|
1623
|
+
details: Some(BTreeMap::from([(
|
|
1624
|
+
"approvalRequestId".to_string(),
|
|
1625
|
+
"approval-1".to_string(),
|
|
1626
|
+
)])),
|
|
1627
|
+
message: Some("done".to_string()),
|
|
1628
|
+
status: "applied",
|
|
1629
|
+
},
|
|
1630
|
+
)
|
|
1631
|
+
.await
|
|
1632
|
+
.expect("submit feedback");
|
|
1633
|
+
|
|
1634
|
+
let captured = request_rx.await.expect("captured request");
|
|
1635
|
+
handle.await.expect("server");
|
|
1636
|
+
assert_eq!(captured.request_line, "POST /v1/daemon/submit-feedback HTTP/1.1");
|
|
1637
|
+
assert!(captured.body.contains("\"claimToken\":\"claim-token\""));
|
|
1638
|
+
assert!(captured.body.contains("\"status\":\"applied\""));
|
|
1639
|
+
assert!(captured.body.contains("\"approvalRequestId\":\"approval-1\""));
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
#[tokio::test]
|
|
1643
|
+
async fn process_update_rejects_invalid_payload_variants() {
|
|
1644
|
+
let daemon = InMemoryDaemon::new(
|
|
1645
|
+
"vault-password",
|
|
1646
|
+
SoftwareSignerBackend::default(),
|
|
1647
|
+
DaemonConfig::default(),
|
|
1648
|
+
)
|
|
1649
|
+
.expect("daemon");
|
|
1650
|
+
let snapshot = daemon
|
|
1651
|
+
.relay_registration_snapshot()
|
|
1652
|
+
.expect("relay snapshot");
|
|
1653
|
+
let expected_daemon_id = snapshot.relay_config.daemon_id_hex.clone();
|
|
1654
|
+
|
|
1655
|
+
let mismatch = process_update(
|
|
1656
|
+
&daemon,
|
|
1657
|
+
&expected_daemon_id,
|
|
1658
|
+
&RelayEncryptedUpdateRecord {
|
|
1659
|
+
daemon_id: "wrong-daemon".to_string(),
|
|
1660
|
+
..sample_update_record(
|
|
1661
|
+
&expected_daemon_id,
|
|
1662
|
+
RelayEncryptedPayload {
|
|
1663
|
+
algorithm: "x25519-xchacha20poly1305-v1".to_string(),
|
|
1664
|
+
ciphertext_base64: String::new(),
|
|
1665
|
+
encapsulated_key_base64: String::new(),
|
|
1666
|
+
nonce_base64: String::new(),
|
|
1667
|
+
},
|
|
1668
|
+
)
|
|
1669
|
+
},
|
|
1670
|
+
)
|
|
1671
|
+
.await;
|
|
1672
|
+
assert_eq!(mismatch.status, "rejected");
|
|
1673
|
+
assert!(
|
|
1674
|
+
mismatch
|
|
1675
|
+
.message
|
|
1676
|
+
.as_deref()
|
|
1677
|
+
.is_some_and(|value| value.contains("does not match"))
|
|
1678
|
+
);
|
|
1679
|
+
|
|
1680
|
+
let unsupported = process_update(
|
|
1681
|
+
&daemon,
|
|
1682
|
+
&expected_daemon_id,
|
|
1683
|
+
&RelayEncryptedUpdateRecord {
|
|
1684
|
+
r#type: "other".to_string(),
|
|
1685
|
+
..sample_update_record(
|
|
1686
|
+
&expected_daemon_id,
|
|
1687
|
+
RelayEncryptedPayload {
|
|
1688
|
+
algorithm: "x25519-xchacha20poly1305-v1".to_string(),
|
|
1689
|
+
ciphertext_base64: String::new(),
|
|
1690
|
+
encapsulated_key_base64: String::new(),
|
|
1691
|
+
nonce_base64: String::new(),
|
|
1692
|
+
},
|
|
1693
|
+
)
|
|
1694
|
+
},
|
|
1695
|
+
)
|
|
1696
|
+
.await;
|
|
1697
|
+
assert_eq!(unsupported.status, "failed");
|
|
1698
|
+
assert!(
|
|
1699
|
+
unsupported
|
|
1700
|
+
.message
|
|
1701
|
+
.as_deref()
|
|
1702
|
+
.is_some_and(|value| value.contains("unsupported relay update type"))
|
|
1703
|
+
);
|
|
1704
|
+
|
|
1705
|
+
let invalid_json_payload = encrypt_payload(
|
|
1706
|
+
&snapshot.relay_config.daemon_public_key_hex,
|
|
1707
|
+
br#"{"not":"a manual approval payload"}"#,
|
|
1708
|
+
);
|
|
1709
|
+
let invalid_json = process_update(
|
|
1710
|
+
&daemon,
|
|
1711
|
+
&expected_daemon_id,
|
|
1712
|
+
&sample_update_record(&expected_daemon_id, invalid_json_payload),
|
|
1713
|
+
)
|
|
1714
|
+
.await;
|
|
1715
|
+
assert_eq!(invalid_json.status, "failed");
|
|
1716
|
+
assert!(
|
|
1717
|
+
invalid_json
|
|
1718
|
+
.message
|
|
1719
|
+
.as_deref()
|
|
1720
|
+
.is_some_and(|value| value.contains("invalid relay update payload"))
|
|
1721
|
+
);
|
|
1722
|
+
|
|
1723
|
+
let wrong_payload_daemon = encrypt_payload(
|
|
1724
|
+
&snapshot.relay_config.daemon_public_key_hex,
|
|
1725
|
+
&serde_json::to_vec(&serde_json::json!({
|
|
1726
|
+
"approvalId": Uuid::nil().to_string(),
|
|
1727
|
+
"daemonId": "wrong-daemon",
|
|
1728
|
+
"decision": "approve",
|
|
1729
|
+
"vaultPassword": "vault-password"
|
|
1730
|
+
}))
|
|
1731
|
+
.expect("json"),
|
|
1732
|
+
);
|
|
1733
|
+
let wrong_payload_daemon = process_update(
|
|
1734
|
+
&daemon,
|
|
1735
|
+
&expected_daemon_id,
|
|
1736
|
+
&sample_update_record(&expected_daemon_id, wrong_payload_daemon),
|
|
1737
|
+
)
|
|
1738
|
+
.await;
|
|
1739
|
+
assert_eq!(wrong_payload_daemon.status, "rejected");
|
|
1740
|
+
|
|
1741
|
+
let target_mismatch_payload = encrypt_payload(
|
|
1742
|
+
&snapshot.relay_config.daemon_public_key_hex,
|
|
1743
|
+
&serde_json::to_vec(&serde_json::json!({
|
|
1744
|
+
"approvalId": Uuid::nil().to_string(),
|
|
1745
|
+
"daemonId": expected_daemon_id,
|
|
1746
|
+
"decision": "approve",
|
|
1747
|
+
"vaultPassword": "vault-password"
|
|
1748
|
+
}))
|
|
1749
|
+
.expect("json"),
|
|
1750
|
+
);
|
|
1751
|
+
let target_mismatch = process_update(
|
|
1752
|
+
&daemon,
|
|
1753
|
+
&expected_daemon_id,
|
|
1754
|
+
&RelayEncryptedUpdateRecord {
|
|
1755
|
+
target_approval_request_id: Some(Uuid::new_v4().to_string()),
|
|
1756
|
+
..sample_update_record(&expected_daemon_id, target_mismatch_payload)
|
|
1757
|
+
},
|
|
1758
|
+
)
|
|
1759
|
+
.await;
|
|
1760
|
+
assert_eq!(target_mismatch.status, "rejected");
|
|
1761
|
+
|
|
1762
|
+
let invalid_approval_id_payload = encrypt_payload(
|
|
1763
|
+
&snapshot.relay_config.daemon_public_key_hex,
|
|
1764
|
+
&serde_json::to_vec(&serde_json::json!({
|
|
1765
|
+
"approvalId": "not-a-uuid",
|
|
1766
|
+
"daemonId": expected_daemon_id,
|
|
1767
|
+
"decision": "approve",
|
|
1768
|
+
"vaultPassword": "vault-password"
|
|
1769
|
+
}))
|
|
1770
|
+
.expect("json"),
|
|
1771
|
+
);
|
|
1772
|
+
let invalid_approval_id = process_update(
|
|
1773
|
+
&daemon,
|
|
1774
|
+
&expected_daemon_id,
|
|
1775
|
+
&sample_update_record(&expected_daemon_id, invalid_approval_id_payload),
|
|
1776
|
+
)
|
|
1777
|
+
.await;
|
|
1778
|
+
assert_eq!(invalid_approval_id.status, "failed");
|
|
1779
|
+
assert!(
|
|
1780
|
+
invalid_approval_id
|
|
1781
|
+
.message
|
|
1782
|
+
.as_deref()
|
|
1783
|
+
.is_some_and(|value| value.contains("approval_id is not a UUID"))
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1014
1786
|
}
|