1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct ApiKeyFile {
7 pub id: String,
8 pub name: String,
9 pub token_hash: String,
11 pub created_at: String,
12 pub wallet_ids: Vec<String>,
14 pub policy_ids: Vec<String>,
16 #[serde(skip_serializing_if = "Option::is_none")]
18 pub expires_at: Option<String>,
19 pub wallet_secrets: HashMap<String, serde_json::Value>,
22}
23
24#[cfg(test)]
25mod tests {
26 use super::*;
27
28 #[test]
29 fn test_api_key_file_serde_roundtrip() {
30 let key = ApiKeyFile {
31 id: "7a2f1b3c-4d5e-6f7a-8b9c-0d1e2f3a4b5c".into(),
32 name: "claude-agent".into(),
33 token_hash: "e3b0c44298fc1c149afbf4c8996fb924".into(),
34 created_at: "2026-03-22T10:30:00Z".into(),
35 wallet_ids: vec!["3198bc9c-6672-5ab3-d995-4942343ae5b6".into()],
36 policy_ids: vec!["base-agent-limits".into()],
37 expires_at: None,
38 wallet_secrets: HashMap::from([(
39 "3198bc9c-6672-5ab3-d995-4942343ae5b6".into(),
40 serde_json::json!({
41 "cipher": "aes-256-gcm",
42 "cipherparams": { "iv": "aabbccdd" },
43 "ciphertext": "deadbeef",
44 "auth_tag": "cafebabe",
45 "kdf": "hkdf-sha256",
46 "kdfparams": { "dklen": 32, "salt": "0011", "info": "ows-api-key-v1" }
47 }),
48 )]),
49 };
50
51 let json = serde_json::to_string_pretty(&key).unwrap();
52 let deserialized: ApiKeyFile = serde_json::from_str(&json).unwrap();
53 assert_eq!(deserialized.id, key.id);
54 assert_eq!(deserialized.name, "claude-agent");
55 assert_eq!(deserialized.wallet_ids.len(), 1);
56 assert_eq!(deserialized.policy_ids, vec!["base-agent-limits"]);
57 assert!(deserialized.expires_at.is_none());
58 assert!(deserialized
59 .wallet_secrets
60 .contains_key("3198bc9c-6672-5ab3-d995-4942343ae5b6"));
61 }
62
63 #[test]
64 fn test_api_key_file_with_expiry() {
65 let key = ApiKeyFile {
66 id: "test-id".into(),
67 name: "expiring-key".into(),
68 token_hash: "abc123".into(),
69 created_at: "2026-03-22T10:30:00Z".into(),
70 wallet_ids: vec![],
71 policy_ids: vec![],
72 expires_at: Some("2026-04-01T00:00:00Z".into()),
73 wallet_secrets: HashMap::new(),
74 };
75
76 let json = serde_json::to_string(&key).unwrap();
77 assert!(json.contains("expires_at"));
78 let deserialized: ApiKeyFile = serde_json::from_str(&json).unwrap();
79 assert_eq!(
80 deserialized.expires_at.as_deref(),
81 Some("2026-04-01T00:00:00Z")
82 );
83 }
84
85 #[test]
86 fn test_api_key_file_no_expiry_omits_field() {
87 let key = ApiKeyFile {
88 id: "test-id".into(),
89 name: "no-expiry".into(),
90 token_hash: "abc123".into(),
91 created_at: "2026-03-22T10:30:00Z".into(),
92 wallet_ids: vec![],
93 policy_ids: vec![],
94 expires_at: None,
95 wallet_secrets: HashMap::new(),
96 };
97
98 let json = serde_json::to_string(&key).unwrap();
99 assert!(!json.contains("expires_at"));
100 }
101}