Skip to main content

claude_agent/auth/storage/
mod.rs

1//! Credential storage implementations.
2
3mod file;
4#[cfg(target_os = "macos")]
5mod keychain;
6
7use serde::{Deserialize, Serialize};
8
9use super::OAuthCredential;
10use crate::Result;
11
12pub use file::FileStorage;
13#[cfg(target_os = "macos")]
14pub use keychain::KeychainStorage;
15
16/// Claude Code CLI credentials file structure.
17#[derive(Clone, Debug, Default, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct CliCredentials {
20    /// OAuth credentials.
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub claude_ai_oauth: Option<OAuthCredential>,
23}
24
25impl CliCredentials {
26    /// Get OAuth credentials if present.
27    pub fn oauth(&self) -> Option<&OAuthCredential> {
28        self.claude_ai_oauth.as_ref()
29    }
30}
31
32/// Load CLI credentials from platform-specific storage.
33pub async fn load_cli_credentials() -> Result<Option<CliCredentials>> {
34    #[cfg(target_os = "macos")]
35    {
36        if let Some(creds) = KeychainStorage::load().await? {
37            return Ok(Some(creds));
38        }
39    }
40
41    FileStorage::load().await
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use secrecy::ExposeSecret;
48
49    #[test]
50    fn test_cli_credentials_parse() {
51        let json = r#"{
52            "claudeAiOauth": {
53                "accessToken": "sk-ant-oat01-test",
54                "refreshToken": "sk-ant-ort01-test",
55                "expiresAt": 1234567890,
56                "scopes": ["user:inference"],
57                "subscriptionType": "pro"
58            }
59        }"#;
60
61        let creds: CliCredentials = serde_json::from_str(json).unwrap();
62        let oauth = creds.oauth().unwrap();
63        assert_eq!(oauth.access_token.expose_secret(), "sk-ant-oat01-test");
64        assert_eq!(oauth.subscription_type, Some("pro".to_string()));
65    }
66}