use serde::{Deserialize, Serialize};
use super::{Cred, Secret};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AmbientSpec {
pub format: AmbientFormat,
pub path: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AmbientFormat {
ClaudeCode,
ApiKeyEnv,
}
pub fn parse_ambient(format: AmbientFormat, bytes: &[u8]) -> Option<Cred> {
match format {
AmbientFormat::ClaudeCode => parse_claude_code(bytes),
AmbientFormat::ApiKeyEnv => parse_api_key_env(bytes),
}
}
fn parse_api_key_env(bytes: &[u8]) -> Option<Cred> {
let key = std::str::from_utf8(bytes).ok()?.trim();
(!key.is_empty()).then(|| Cred::ApiKey {
key: Secret::new(key),
})
}
fn parse_claude_code(bytes: &[u8]) -> Option<Cred> {
let v: serde_json::Value = serde_json::from_slice(bytes).ok()?;
let oauth = v.get("claudeAiOauth")?;
let access_token = Secret::new(oauth.get("accessToken")?.as_str()?);
let refresh_token = Secret::new(oauth.get("refreshToken")?.as_str()?);
let expires_at = oauth.get("expiresAt")?.as_u64()? / 1000;
let scope = oauth
.get("scopes")
.and_then(|s| s.as_array())
.and_then(|a| {
let joined: Vec<&str> = a.iter().filter_map(serde_json::Value::as_str).collect();
(!joined.is_empty()).then(|| joined.join(" "))
});
Some(Cred::OAuth2 {
access_token,
refresh_token,
expires_at,
scope,
account_id: None,
})
}