tandem_core/
engine_api_token.rs1use 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}