agentmux_srv/identity/
auth_diag.rs1use std::path::Path;
24
25fn fingerprint(secret: &str) -> String {
33 const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
34 const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
35 let mut h = FNV_OFFSET;
36 for b in secret.as_bytes() {
37 h ^= *b as u64;
38 h = h.wrapping_mul(FNV_PRIME);
39 }
40 format!("{:08x}", (h & 0xffff_ffff) as u32)
41}
42
43pub fn snapshot(config_dir: &str) -> String {
54 let dir = Path::new(config_dir);
55 let creds = dir.join(".credentials.json");
56 let seeded = dir.join(".agentmux-cred-seeded").exists();
57
58 let (present, token_fp, refresh) = match std::fs::read_to_string(&creds) {
59 Ok(content) => {
60 let (mut fp, mut has_refresh) = ("none".to_string(), false);
61 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
62 let oauth = json.get("claudeAiOauth");
63 if let Some(tok) = oauth
64 .and_then(|o| o.get("accessToken"))
65 .and_then(|v| v.as_str())
66 .filter(|s| !s.is_empty())
67 {
68 fp = fingerprint(tok);
69 }
70 has_refresh = oauth
71 .and_then(|o| o.get("refreshToken"))
72 .and_then(|v| v.as_str())
73 .map(|s| !s.is_empty())
74 .unwrap_or(false);
75 }
76 (true, fp, has_refresh)
77 }
78 Err(_) => (false, "none".to_string(), false),
79 };
80
81 let mtime = std::fs::metadata(&creds)
82 .and_then(|m| m.modified())
83 .ok()
84 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
85 .map(|d| d.as_secs())
86 .unwrap_or(0);
87
88 format!(
89 "dir={config_dir} present={present} token={token_fp} refresh={refresh} seeded={seeded} mtime={mtime}"
90 )
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn fingerprint_is_stable_and_distinct() {
99 assert_eq!(fingerprint("sk-ant-oat-AAAA"), fingerprint("sk-ant-oat-AAAA"));
101 assert_ne!(fingerprint("sk-ant-oat-AAAA"), fingerprint("sk-ant-oat-BBBB"));
102 let fp = fingerprint("sk-ant-oat-secretsecret");
103 assert_eq!(fp.len(), 8);
104 assert!(!fp.contains("secret"));
105 }
106
107 #[test]
108 fn snapshot_absent_dir() {
109 let snap = snapshot("/nonexistent/agentmux-test-dir-xyz");
110 assert!(snap.contains("present=false"));
111 assert!(snap.contains("token=none"));
112 assert!(snap.contains("refresh=false"));
113 assert!(snap.contains("seeded=false"));
114 }
115
116 #[test]
117 fn snapshot_reports_token_fingerprint_and_refresh() {
118 let dir = std::env::temp_dir().join(format!("amx-auth-diag-{}", std::process::id()));
119 std::fs::create_dir_all(&dir).unwrap();
120 let creds = dir.join(".credentials.json");
121 std::fs::write(
122 &creds,
123 r#"{"claudeAiOauth":{"accessToken":"sk-ant-oat-TESTTOKEN","refreshToken":"rt-xyz"}}"#,
124 )
125 .unwrap();
126
127 let snap = snapshot(dir.to_str().unwrap());
128 assert!(snap.contains("present=true"), "{snap}");
129 assert!(snap.contains("refresh=true"), "{snap}");
130 assert!(!snap.contains("TESTTOKEN"), "token must never appear: {snap}");
131 assert!(
132 snap.contains(&format!("token={}", fingerprint("sk-ant-oat-TESTTOKEN"))),
133 "{snap}"
134 );
135
136 let _ = std::fs::remove_dir_all(&dir);
137 }
138}