claude_usage/credentials/
mod.rs1#[cfg(target_os = "macos")]
15mod macos;
16
17#[cfg(target_os = "linux")]
18mod linux;
19
20use crate::error::CredentialError;
21
22pub const KEYCHAIN_SERVICE: &str = "Claude Code-credentials";
24
25pub const LINUX_CREDENTIALS_PATH: &str = ".claude/.credentials.json";
27
28pub const ENV_VAR_TOKEN: &str = "CLAUDE_CODE_OAUTH_TOKEN";
30
31pub fn get_token() -> Result<String, CredentialError> {
47 if let Ok(token) = std::env::var(ENV_VAR_TOKEN) {
49 if !token.is_empty() {
50 return Ok(token);
51 }
52 }
53
54 #[cfg(target_os = "macos")]
55 {
56 macos::get_token_macos()
57 }
58
59 #[cfg(target_os = "linux")]
60 {
61 linux::get_token_linux()
62 }
63
64 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
65 {
66 Err(CredentialError::NotFound)
67 }
68}
69
70pub(crate) fn parse_credential_json(content: &str) -> Result<String, CredentialError> {
86 let json: serde_json::Value =
87 serde_json::from_str(content).map_err(|e| CredentialError::Parse(e.to_string()))?;
88
89 let oauth = json
90 .get("claudeAiOauth")
91 .ok_or(CredentialError::MissingField("claudeAiOauth"))?;
92
93 if let Some(expires_at) = oauth.get("expiresAt").and_then(|v| v.as_i64()) {
95 let now = std::time::SystemTime::now()
96 .duration_since(std::time::UNIX_EPOCH)
97 .expect("system time should be after Unix epoch")
98 .as_millis() as i64;
99
100 if now > expires_at {
101 return Err(CredentialError::Expired);
102 }
103 }
104
105 let token = oauth
106 .get("accessToken")
107 .and_then(|v| v.as_str())
108 .ok_or(CredentialError::MissingField("accessToken"))?;
109
110 Ok(token.to_string())
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn test_parse_valid_credentials() {
119 let json = r#"{
120 "claudeAiOauth": {
121 "accessToken": "sk-ant-oat01-test-token",
122 "refreshToken": "sk-ant-ort01-refresh",
123 "expiresAt": 9999999999999,
124 "scopes": ["user:inference", "user:profile"]
125 }
126 }"#;
127
128 let token = parse_credential_json(json).expect("should parse valid JSON");
129 assert_eq!(token, "sk-ant-oat01-test-token");
130 }
131
132 #[test]
133 fn test_parse_missing_claude_ai_oauth() {
134 let json = r#"{"other": "data"}"#;
135 let result = parse_credential_json(json);
136 assert!(matches!(
137 result,
138 Err(CredentialError::MissingField("claudeAiOauth"))
139 ));
140 }
141
142 #[test]
143 fn test_parse_missing_access_token() {
144 let json = r#"{
145 "claudeAiOauth": {
146 "refreshToken": "sk-ant-ort01-refresh"
147 }
148 }"#;
149 let result = parse_credential_json(json);
150 assert!(matches!(
151 result,
152 Err(CredentialError::MissingField("accessToken"))
153 ));
154 }
155
156 #[test]
157 fn test_parse_expired_token() {
158 let json = r#"{
159 "claudeAiOauth": {
160 "accessToken": "sk-ant-oat01-test-token",
161 "expiresAt": 1000
162 }
163 }"#;
164 let result = parse_credential_json(json);
165 assert!(matches!(result, Err(CredentialError::Expired)));
166 }
167
168 #[test]
169 fn test_parse_invalid_json() {
170 let json = "not valid json";
171 let result = parse_credential_json(json);
172 assert!(matches!(result, Err(CredentialError::Parse(_))));
173 }
174
175 #[test]
176 fn test_parse_no_expires_at_is_valid() {
177 let json = r#"{
179 "claudeAiOauth": {
180 "accessToken": "sk-ant-oat01-no-expiry"
181 }
182 }"#;
183 let token = parse_credential_json(json).expect("should parse without expiresAt");
184 assert_eq!(token, "sk-ant-oat01-no-expiry");
185 }
186
187 #[test]
188 fn test_env_var_takes_precedence() {
189 let token = "test-env-token-value";
192 std::env::set_var(ENV_VAR_TOKEN, token);
193
194 assert_eq!(std::env::var(ENV_VAR_TOKEN).ok(), Some(token.to_string()));
196
197 let result = get_token();
199 std::env::remove_var(ENV_VAR_TOKEN);
200
201 assert_eq!(result.expect("should use env var"), token);
202 }
203
204 #[test]
205 fn test_empty_env_var_behavior() {
206 std::env::set_var(ENV_VAR_TOKEN, "");
209
210 let env_value = std::env::var(ENV_VAR_TOKEN).ok();
212 assert_eq!(env_value, Some(String::new()));
213
214 std::env::remove_var(ENV_VAR_TOKEN);
215 }
216}