Skip to main content

sparrow/auth/
mod.rs

1use chrono::{DateTime, Utc};
2use secrecy::{ExposeSecret, SecretString};
3
4pub mod store;
5// ─── Credential types ───────────────────────────────────────────────────────────
6
7#[derive(Debug, Clone)]
8pub enum Credential {
9    ApiKey(SecretString),
10    OAuth {
11        access: SecretString,
12        refresh: Option<SecretString>,
13        expires: Option<DateTime<Utc>>,
14    },
15}
16
17impl Credential {
18    pub fn api_key(key: impl Into<String>) -> Self {
19        Credential::ApiKey(SecretString::new(key.into().into_boxed_str()))
20    }
21
22    pub fn expose_key(&self) -> Option<&str> {
23        match self {
24            Credential::ApiKey(k) => Some(k.expose_secret()),
25            Credential::OAuth { access, .. } => Some(access.expose_secret()),
26        }
27    }
28
29    pub fn is_expired(&self) -> bool {
30        match self {
31            Credential::ApiKey(_) => false,
32            Credential::OAuth {
33                expires: Some(exp), ..
34            } => *exp <= Utc::now(),
35            _ => false,
36        }
37    }
38}
39
40// ─── AuthStore trait ────────────────────────────────────────────────────────────
41
42/// Stores provider credentials.
43/// Backend priority: OS keychain → encrypted file → env.
44pub trait AuthStore: Send + Sync {
45    fn get(&self, provider: &str) -> Option<Credential>;
46    fn set(&self, provider: &str, c: Credential) -> anyhow::Result<()>;
47    fn list(&self) -> Vec<String>;
48    fn remove(&self, provider: &str) -> anyhow::Result<()>;
49}
50
51// ─── In-memory + env-based implementation (no keychain for initial build) ────────
52
53use std::collections::HashMap;
54use std::sync::RwLock;
55
56pub struct MemoryAuthStore {
57    credentials: RwLock<HashMap<String, Credential>>,
58}
59
60impl MemoryAuthStore {
61    pub fn new() -> Self {
62        let mut store = Self {
63            credentials: RwLock::new(HashMap::new()),
64        };
65        store.load_from_env();
66        store
67    }
68
69    /// Scan environment for provider keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)
70    fn load_from_env(&mut self) {
71        let env_map: Vec<(&str, &str)> = vec![
72            ("ANTHROPIC_API_KEY", "anthropic"),
73            ("OPENAI_API_KEY", "openai"),
74            ("GROQ_API_KEY", "groq"),
75            ("TOGETHER_API_KEY", "together"),
76            ("NVIDIA_API_KEY", "nvidia"),
77            ("CEREBRAS_API_KEY", "cerebras"),
78            ("OPENROUTER_API_KEY", "openrouter"),
79        ];
80
81        let mut creds = self.credentials.write().unwrap();
82        for (env_var, provider) in env_map {
83            if let Ok(key) = std::env::var(env_var) {
84                if !key.is_empty() {
85                    creds.insert(provider.to_string(), Credential::api_key(key));
86                }
87            }
88        }
89    }
90}
91
92impl AuthStore for MemoryAuthStore {
93    fn get(&self, provider: &str) -> Option<Credential> {
94        let creds = self.credentials.read().unwrap();
95        creds.get(provider).cloned()
96    }
97
98    fn set(&self, provider: &str, c: Credential) -> anyhow::Result<()> {
99        let mut creds = self.credentials.write().unwrap();
100        creds.insert(provider.to_string(), c);
101        Ok(())
102    }
103
104    fn list(&self) -> Vec<String> {
105        let creds = self.credentials.read().unwrap();
106        creds.keys().cloned().collect()
107    }
108
109    fn remove(&self, provider: &str) -> anyhow::Result<()> {
110        let mut creds = self.credentials.write().unwrap();
111        creds.remove(provider);
112        Ok(())
113    }
114}
115
116impl Default for MemoryAuthStore {
117    fn default() -> Self {
118        Self::new()
119    }
120}