Skip to main content

aiclient_api/auth/
token_store.rs

1use super::{TokenData, TokenStore};
2use anyhow::{Context, Result};
3use async_trait::async_trait;
4use std::path::PathBuf;
5
6pub struct XdgTokenStore {
7    base_dir: PathBuf,
8}
9
10impl Default for XdgTokenStore {
11    fn default() -> Self {
12        Self::new(crate::util::xdg::config_dir())
13    }
14}
15
16impl XdgTokenStore {
17    pub fn new(base_dir: PathBuf) -> Self {
18        Self { base_dir }
19    }
20
21    fn token_path(&self, provider: &str) -> PathBuf {
22        self.base_dir.join(provider).join("token.json")
23    }
24}
25
26#[async_trait]
27impl TokenStore for XdgTokenStore {
28    async fn load(&self, provider: &str) -> Result<TokenData> {
29        let path = self.token_path(provider);
30        let content = tokio::fs::read_to_string(&path)
31            .await
32            .with_context(|| format!("No token found for provider: {}", provider))?;
33        let data: TokenData = serde_json::from_str(&content)
34            .with_context(|| format!("Invalid token file for: {}", provider))?;
35        Ok(data)
36    }
37
38    async fn save(&self, provider: &str, data: &TokenData) -> Result<()> {
39        let path = self.token_path(provider);
40        if let Some(parent) = path.parent() {
41            tokio::fs::create_dir_all(parent).await?;
42        }
43        let json = serde_json::to_string_pretty(data)?;
44        tokio::fs::write(&path, &json).await?;
45
46        #[cfg(unix)]
47        {
48            use std::os::unix::fs::PermissionsExt;
49            let perms = std::fs::Permissions::from_mode(0o600);
50            tokio::fs::set_permissions(&path, perms).await?;
51        }
52
53        Ok(())
54    }
55
56    async fn delete(&self, provider: &str) -> Result<()> {
57        let path = self.token_path(provider);
58        match tokio::fs::remove_file(&path).await {
59            Ok(()) => {}
60            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
61            Err(e) => return Err(e.into()),
62        }
63        let dir = self.base_dir.join(provider);
64        // Ignore: dir may not exist or not be empty
65        let _ = tokio::fs::remove_dir(&dir).await;
66        Ok(())
67    }
68
69    fn is_expired(&self, data: &TokenData) -> bool {
70        let now = chrono::Utc::now().timestamp();
71        match data {
72            TokenData::Copilot { expires_at, .. } => {
73                expires_at.is_some_and(|exp| now >= exp)
74            }
75            TokenData::Kiro { expires_at, .. } => now >= *expires_at,
76        }
77    }
78}