Skip to main content

microscope_memory/
keystore.rs

1//! Binary key store — `keys.bin`
2//!
3//! Zero-JSON, pure bincode. ORA védi a kulcsokat.
4//! Formátum: MKEY magic + version + KeyEntry[].
5//!
6//! Nem titkosítva — ORA maga a védelem (bináris formátum, fájlrendszer szintű hozzáférés).
7
8use serde::{Deserialize, Serialize};
9use std::fs;
10use std::path::Path;
11
12/// MKEY magic bytes
13const MAGIC: [u8; 4] = [0x4D, 0x4B, 0x45, 0x59]; // "MKEY"
14const VERSION: u8 = 1;
15
16/// A teljes kulcstár
17#[derive(Debug, Serialize, Deserialize, Clone)]
18pub struct KeyStore {
19    pub magic: [u8; 4],
20    pub version: u8,
21    pub entries: Vec<KeyEntry>,
22}
23
24impl Default for KeyStore {
25    fn default() -> Self {
26        Self {
27            magic: MAGIC,
28            version: VERSION,
29            entries: Vec::new(),
30        }
31    }
32}
33
34/// Egy API kulcs
35#[derive(Debug, Serialize, Deserialize, Clone)]
36pub struct KeyEntry {
37    /// "openai" | "gemini" | "ollama" | ...
38    pub service: String,
39    /// Maga a kulcs (pl. "sk-..." vagy "AIza...")
40    pub key: String,
41    /// 0 = elsődleges, 1 = másodlagos, 2 = harmadlagos...
42    pub priority: u8,
43    /// Ha tudjuk, mennyi quota maradt (None = nem tudjuk)
44    pub quota_remaining: Option<f64>,
45    /// Utolsó hibaüzenet (pl. "429 Too Many Requests")
46    #[serde(default)]
47    pub last_error: Option<String>,
48    /// Letiltva, mert többször is 429-et kapott
49    #[serde(default)]
50    pub disabled: bool,
51    /// Létrehozás időpontja (unix timestamp)
52    pub created_at: i64,
53}
54
55impl KeyStore {
56    /// Betölti a kulcstárat a megadott útvonalról.
57    /// Ha a fájl nem létezik, üres kulcstárat ad vissza.
58    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
59        if !path.as_ref().exists() {
60            return Ok(Self {
61                magic: MAGIC,
62                version: VERSION,
63                entries: Vec::new(),
64            });
65        }
66        let bytes = fs::read(path.as_ref())?;
67        let store: KeyStore = bincode::deserialize(&bytes)?;
68        if store.magic != MAGIC {
69            return Err("Invalid keys.bin magic bytes".into());
70        }
71        if store.version != VERSION {
72            return Err(format!("Unsupported keys.bin version: {}", store.version).into());
73        }
74        Ok(store)
75    }
76
77    /// Mentés bincode formátumban
78    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), Box<dyn std::error::Error>> {
79        let bytes = bincode::serialize(self)?;
80        let tmp = path.as_ref().with_extension("bin.tmp");
81        fs::write(&tmp, &bytes)?;
82        fs::rename(&tmp, path.as_ref())?;
83        Ok(())
84    }
85
86    /// Kulcs lekérése service név alapján, priority szerint (legkisebb szám = legmagasabb priority)
87    pub fn get(&self, service: &str) -> Option<&KeyEntry> {
88        self.entries
89            .iter()
90            .filter(|e| e.service == service && !e.disabled)
91            .min_by_key(|e| e.priority)
92    }
93
94    /// Az összes nem letiltott kulcs lekérése egy service-hez, priority sorrendben
95    pub fn get_all(&self, service: &str) -> Vec<&KeyEntry> {
96        let mut v: Vec<_> = self
97            .entries
98            .iter()
99            .filter(|e| e.service == service && !e.disabled)
100            .collect();
101        v.sort_by_key(|e| e.priority);
102        v
103    }
104
105    /// Kulcs beállítása (hozzáadás vagy frissítés)
106    pub fn set(&mut self, service: &str, key: String, priority: u8) {
107        // Ha már van ilyen service + priority, frissítsük
108        if let Some(existing) = self
109            .entries
110            .iter_mut()
111            .find(|e| e.service == service && e.priority == priority)
112        {
113            existing.key = key;
114            existing.last_error = None;
115            existing.disabled = false;
116            existing.created_at = std::time::SystemTime::now()
117                .duration_since(std::time::UNIX_EPOCH)
118                .unwrap_or_default()
119                .as_secs() as i64;
120            return;
121        }
122        // Különben új bejegyzés
123        self.entries.push(KeyEntry {
124            service: service.to_string(),
125            key,
126            priority,
127            quota_remaining: None,
128            last_error: None,
129            disabled: false,
130            created_at: std::time::SystemTime::now()
131                .duration_since(std::time::UNIX_EPOCH)
132                .unwrap_or_default()
133                .as_secs() as i64,
134        });
135    }
136
137    /// Kulcs eltávolítása
138    pub fn remove(&mut self, service: &str, priority: u8) {
139        self.entries
140            .retain(|e| !(e.service == service && e.priority == priority));
141    }
142
143    /// Hiba bejegyzése egy kulcshoz — ha quota hiba, letiltja
144    pub fn record_error(&mut self, service: &str, priority: u8, error: &str) {
145        if let Some(entry) = self
146            .entries
147            .iter_mut()
148            .find(|e| e.service == service && e.priority == priority)
149        {
150            entry.last_error = Some(error.to_string());
151            // 429 = quota elfogyott, 401 = auth hiba → letiltjuk
152            if error.contains("429") || error.contains("401") || error.contains("403") {
153                entry.disabled = true;
154            }
155        }
156    }
157
158    /// Sikeres hívás után: töröljük a hibát, opcionálisan frissítjük a quota-t
159    pub fn record_success(&mut self, service: &str, priority: u8) {
160        if let Some(entry) = self
161            .entries
162            .iter_mut()
163            .find(|e| e.service == service && e.priority == priority)
164        {
165            entry.last_error = None;
166            // Ha újra működik, engedélyezzük
167            entry.disabled = false;
168        }
169    }
170
171    /// Összes kulcs újraengedélyezése (pl. időzítő alapján)
172    pub fn reset_all(&mut self) {
173        for entry in &mut self.entries {
174            entry.disabled = false;
175            entry.last_error = None;
176        }
177    }
178
179    /// Kulcsok listázása (key nélkül, csak metaadatok)
180    pub fn list(&self) -> Vec<KeyEntryInfo> {
181        self.entries
182            .iter()
183            .map(|e| KeyEntryInfo {
184                service: e.service.clone(),
185                priority: e.priority,
186                quota_remaining: e.quota_remaining,
187                last_error: e.last_error.clone(),
188                disabled: e.disabled,
189                created_at: e.created_at,
190                key_preview: if e.key.len() > 8 {
191                    format!("{}...", &e.key[..8])
192                } else {
193                    "***".to_string()
194                },
195            })
196            .collect()
197    }
198}
199
200/// Publikus kulcs info (key nélkül)
201#[derive(Debug, Serialize, Deserialize, Clone)]
202pub struct KeyEntryInfo {
203    pub service: String,
204    pub priority: u8,
205    pub quota_remaining: Option<f64>,
206    pub last_error: Option<String>,
207    pub disabled: bool,
208    pub created_at: i64,
209    pub key_preview: String,
210}
211
212/// Alapértelmezett útvonal a keys.bin-hez
213pub fn default_keys_path(output_dir: &str) -> String {
214    format!("{}/keys.bin", output_dir)
215}