Skip to main content

tandem_core/
engine_api_token.rs

1use std::path::PathBuf;
2use uuid::Uuid;
3
4use crate::resolve_shared_paths;
5
6const TOKEN_SERVICE: &str = "ai.frumu.tandem";
7const TOKEN_ACCOUNT: &str = "engine_api_token";
8
9#[derive(Debug, Clone)]
10pub struct EngineApiTokenMaterial {
11    pub token: String,
12    pub backend: String,
13    pub file_path: PathBuf,
14}
15
16pub fn engine_api_token_file_path() -> PathBuf {
17    if let Ok(paths) = resolve_shared_paths() {
18        return paths
19            .canonical_root
20            .join("security")
21            .join("engine_api_token");
22    }
23    if let Some(data_dir) = dirs::data_dir() {
24        return data_dir
25            .join("tandem")
26            .join("security")
27            .join("engine_api_token");
28    }
29    dirs::home_dir()
30        .map(|home| {
31            home.join(".tandem")
32                .join("security")
33                .join("engine_api_token")
34        })
35        .unwrap_or_else(|| PathBuf::from("engine_api_token"))
36}
37
38fn new_token() -> String {
39    format!("tk_{}", Uuid::new_v4().simple())
40}
41
42fn keyring_entry() -> Option<keyring::Entry> {
43    keyring::Entry::new(TOKEN_SERVICE, TOKEN_ACCOUNT).ok()
44}
45
46fn read_file_token(path: &PathBuf) -> Option<String> {
47    let existing = std::fs::read_to_string(path).ok()?;
48    let token = existing.trim();
49    if token.is_empty() {
50        None
51    } else {
52        Some(token.to_string())
53    }
54}
55
56fn write_file_token(path: &PathBuf, token: &str) -> bool {
57    if let Some(parent) = path.parent() {
58        if std::fs::create_dir_all(parent).is_err() {
59            return false;
60        }
61    }
62    std::fs::write(path, token).is_ok()
63}
64
65pub fn load_or_create_engine_api_token() -> EngineApiTokenMaterial {
66    let file_path = engine_api_token_file_path();
67
68    if let Some(entry) = keyring_entry() {
69        if let Ok(token) = entry.get_password() {
70            let token = token.trim().to_string();
71            if !token.is_empty() {
72                return EngineApiTokenMaterial {
73                    token,
74                    backend: "keychain".to_string(),
75                    file_path,
76                };
77            }
78        }
79    }
80
81    if let Some(token) = read_file_token(&file_path) {
82        if let Some(entry) = keyring_entry() {
83            let _ = entry.set_password(&token);
84        }
85        return EngineApiTokenMaterial {
86            token,
87            backend: "file".to_string(),
88            file_path,
89        };
90    }
91
92    let token = new_token();
93    if let Some(entry) = keyring_entry() {
94        if entry.set_password(&token).is_ok() {
95            return EngineApiTokenMaterial {
96                token,
97                backend: "keychain".to_string(),
98                file_path,
99            };
100        }
101    }
102
103    let wrote_file = write_file_token(&file_path, &token);
104    EngineApiTokenMaterial {
105        token,
106        backend: if wrote_file {
107            "file".to_string()
108        } else {
109            "memory".to_string()
110        },
111        file_path,
112    }
113}