use secrecy::SecretString;
#[derive(Clone, Default)]
pub enum Auth {
ApiKey(SecretString),
BearerToken(SecretString),
#[default]
None,
}
impl Auth {
pub fn secret(&self) -> Option<&SecretString> {
match self {
Auth::ApiKey(s) | Auth::BearerToken(s) => Some(s),
Auth::None => None,
}
}
pub fn is_some(&self) -> bool {
!matches!(self, Auth::None)
}
}
impl std::fmt::Debug for Auth {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Auth::ApiKey(_) => f.write_str("Auth::ApiKey(***)"),
Auth::BearerToken(_) => f.write_str("Auth::BearerToken(***)"),
Auth::None => f.write_str("Auth::None"),
}
}
}
pub fn resolve_claude_subscription_auth() -> Auth {
if let Ok(token) = std::env::var("ANTHROPIC_AUTH_TOKEN") {
if !token.trim().is_empty() {
return Auth::BearerToken(SecretString::from(token));
}
}
match dirs_home().map(|h| h.join(".claude/.credentials.json")) {
Some(path) => match read_claude_code_token(&path) {
Some(token) => Auth::BearerToken(SecretString::from(token)),
None => Auth::None,
},
None => Auth::None,
}
}
fn dirs_home() -> Option<std::path::PathBuf> {
std::env::var_os("HOME").map(std::path::PathBuf::from)
}
fn read_claude_code_token(path: &std::path::Path) -> Option<String> {
let contents = std::fs::read_to_string(path).ok()?;
parse_claude_code_token(&contents)
}
fn parse_claude_code_token(contents: &str) -> Option<String> {
let json: serde_json::Value = serde_json::from_str(contents).ok()?;
json["claudeAiOauth"]["accessToken"]
.as_str()
.filter(|s| !s.is_empty())
.map(String::from)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_claude_code_subscription_token() {
let body = r#"{"claudeAiOauth":{"accessToken":"sk-ant-oat01-abc",
"refreshToken":"sk-ant-ort01-x","subscriptionType":"max"}}"#;
assert_eq!(
parse_claude_code_token(body).as_deref(),
Some("sk-ant-oat01-abc")
);
}
#[test]
fn missing_or_empty_token_is_none() {
assert!(parse_claude_code_token(r#"{"claudeAiOauth":{}}"#).is_none());
assert!(parse_claude_code_token(r#"{"claudeAiOauth":{"accessToken":""}}"#).is_none());
assert!(parse_claude_code_token("not json").is_none());
assert!(parse_claude_code_token(r#"{"other":"shape"}"#).is_none());
}
}