#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "linux")]
mod linux;
use crate::error::CredentialError;
pub const KEYCHAIN_SERVICE: &str = "Claude Code-credentials";
pub const LINUX_CREDENTIALS_PATH: &str = ".claude/.credentials.json";
pub const ENV_VAR_TOKEN: &str = "CLAUDE_CODE_OAUTH_TOKEN";
pub fn get_token() -> Result<String, CredentialError> {
if let Ok(token) = std::env::var(ENV_VAR_TOKEN) {
if !token.is_empty() {
return Ok(token);
}
}
#[cfg(target_os = "macos")]
{
macos::get_token_macos()
}
#[cfg(target_os = "linux")]
{
linux::get_token_linux()
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
Err(CredentialError::NotFound)
}
}
pub(crate) fn parse_credential_json(content: &str) -> Result<String, CredentialError> {
let json: serde_json::Value =
serde_json::from_str(content).map_err(|e| CredentialError::Parse(e.to_string()))?;
let oauth = json
.get("claudeAiOauth")
.ok_or(CredentialError::MissingField("claudeAiOauth"))?;
if let Some(expires_at_ms) = oauth.get("expiresAt").and_then(|v| v.as_i64()) {
let now_ms = chrono::Utc::now().timestamp_millis();
if now_ms > expires_at_ms {
return Err(CredentialError::Expired);
}
}
let token = oauth
.get("accessToken")
.and_then(|v| v.as_str())
.ok_or(CredentialError::MissingField("accessToken"))?;
Ok(token.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid_credentials() {
let json = r#"{
"claudeAiOauth": {
"accessToken": "sk-ant-oat01-test-token",
"refreshToken": "sk-ant-ort01-refresh",
"expiresAt": 9999999999999,
"scopes": ["user:inference", "user:profile"]
}
}"#;
let token = parse_credential_json(json).expect("should parse valid JSON");
assert_eq!(token, "sk-ant-oat01-test-token");
}
#[test]
fn test_parse_missing_claude_ai_oauth() {
let json = r#"{"other": "data"}"#;
let result = parse_credential_json(json);
assert!(matches!(
result,
Err(CredentialError::MissingField("claudeAiOauth"))
));
}
#[test]
fn test_parse_missing_access_token() {
let json = r#"{
"claudeAiOauth": {
"refreshToken": "sk-ant-ort01-refresh"
}
}"#;
let result = parse_credential_json(json);
assert!(matches!(
result,
Err(CredentialError::MissingField("accessToken"))
));
}
#[test]
fn test_parse_expired_token() {
let json = r#"{
"claudeAiOauth": {
"accessToken": "sk-ant-oat01-test-token",
"expiresAt": 1000
}
}"#;
let result = parse_credential_json(json);
assert!(matches!(result, Err(CredentialError::Expired)));
}
#[test]
fn test_parse_invalid_json() {
let json = "not valid json";
let result = parse_credential_json(json);
assert!(matches!(result, Err(CredentialError::Parse(_))));
}
#[test]
fn test_parse_no_expires_at_is_valid() {
let json = r#"{
"claudeAiOauth": {
"accessToken": "sk-ant-oat01-no-expiry"
}
}"#;
let token = parse_credential_json(json).expect("should parse without expiresAt");
assert_eq!(token, "sk-ant-oat01-no-expiry");
}
#[test]
fn test_env_var_behavior() {
use std::sync::Mutex;
static ENV_MUTEX: Mutex<()> = Mutex::new(());
let _guard = ENV_MUTEX.lock().expect("env mutex");
let token = "test-env-token-value";
std::env::set_var(ENV_VAR_TOKEN, token);
assert_eq!(std::env::var(ENV_VAR_TOKEN).ok(), Some(token.to_string()));
let result = get_token();
assert_eq!(result.expect("should use env var"), token);
std::env::set_var(ENV_VAR_TOKEN, "");
let env_value = std::env::var(ENV_VAR_TOKEN).ok();
assert_eq!(env_value, Some(String::new()));
std::env::remove_var(ENV_VAR_TOKEN);
}
}