use rand::RngCore;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::RwLock;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct ApiKey {
pub id: String,
pub name: String,
pub created_at: u64,
}
pub struct KeyStore {
keys: Vec<ApiKey>,
last_used: RwLock<HashMap<String, u64>>,
path: Option<PathBuf>,
}
impl KeyStore {
pub fn load() -> Self {
let path = config_path();
let keys = path
.as_ref()
.and_then(|p| std::fs::read_to_string(p).ok())
.and_then(|s| serde_json::from_str::<Vec<ApiKey>>(&s).ok())
.unwrap_or_default();
Self {
keys,
last_used: RwLock::new(HashMap::new()),
path,
}
}
pub fn is_enabled(&self) -> bool {
!self.keys.is_empty()
}
pub fn validate(&self, provided: &str) -> Option<String> {
let key = self.keys.iter().find(|k| k.id == provided)?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
self.last_used
.write()
.unwrap()
.insert(provided.to_string(), now);
Some(key.name.clone())
}
pub fn list_redacted(&self) -> Vec<RedactedKey> {
let last = self.last_used.read().unwrap();
self.keys
.iter()
.map(|k| RedactedKey {
name: k.name.clone(),
prefix: k.id.chars().take(12).collect::<String>() + "…",
created_at: k.created_at,
last_used_at: last.get(&k.id).copied(),
})
.collect()
}
pub fn create(&mut self, name: &str) -> Result<String, String> {
if self.keys.iter().any(|k| k.name == name) {
return Err(format!("key with name '{}' already exists", name));
}
let mut bytes = [0u8; 32];
rand::rng().fill_bytes(&mut bytes);
let id = format!("mr_{}", hex::encode(bytes));
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
self.keys.push(ApiKey {
id: id.clone(),
name: name.to_string(),
created_at: now,
});
self.save()?;
Ok(id)
}
pub fn revoke(&mut self, name: &str) -> Result<(), String> {
let before = self.keys.len();
self.keys.retain(|k| k.name != name);
if self.keys.len() == before {
return Err(format!("key '{}' not found", name));
}
self.save()?;
Ok(())
}
fn save(&self) -> Result<(), String> {
let Some(ref p) = self.path else {
return Ok(());
};
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let json = serde_json::to_string_pretty(&self.keys).map_err(|e| e.to_string())?;
std::fs::write(p, json).map_err(|e| e.to_string())?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(p, std::fs::Permissions::from_mode(0o600));
}
Ok(())
}
}
#[derive(serde::Serialize)]
pub struct RedactedKey {
pub name: String,
pub prefix: String,
pub created_at: u64,
pub last_used_at: Option<u64>,
}
fn config_path() -> Option<PathBuf> {
directories::ProjectDirs::from("sh", "gladius", "microresolve")
.map(|pd| pd.config_dir().join("keys.json"))
}