use std::path::PathBuf;
use serde::{Deserialize, Serialize};
const TRUST_ENV: &str = "LEANCTX_ORG_TRUST_KEY";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrustedKey {
pub org: String,
pub public_key: String,
pub added_at: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TrustStore {
#[serde(default, rename = "trusted_key", skip_serializing_if = "Vec::is_empty")]
pub trusted_keys: Vec<TrustedKey>,
}
pub fn trust_path() -> Result<PathBuf, String> {
Ok(crate::core::paths::config_dir()?.join("org-trust.toml"))
}
pub fn load() -> Result<TrustStore, String> {
let path = trust_path()?;
if !path.exists() {
return Ok(TrustStore::default());
}
let text =
std::fs::read_to_string(&path).map_err(|e| format!("read {}: {e}", path.display()))?;
toml::from_str(&text).map_err(|e| format!("parse {}: {e}", path.display()))
}
pub fn save(store: &TrustStore) -> Result<(), String> {
let path = trust_path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| format!("mkdir config: {e}"))?;
}
let text = toml::to_string_pretty(store).map_err(|e| format!("serialize trust store: {e}"))?;
std::fs::write(&path, text).map_err(|e| format!("write {}: {e}", path.display()))
}
pub fn pin(org: &str, public_key: &str) -> Result<bool, String> {
let public_key = normalize_key(public_key)?;
let mut store = load()?;
if let Some(existing) = store
.trusted_keys
.iter_mut()
.find(|k| k.public_key == public_key)
{
let changed = existing.org != org;
existing.org = org.to_string();
existing.added_at = now();
save(&store)?;
return Ok(changed);
}
store.trusted_keys.push(TrustedKey {
org: org.to_string(),
public_key,
added_at: now(),
});
save(&store)?;
Ok(true)
}
pub fn remove(public_key: &str) -> Result<bool, String> {
let public_key = normalize_key(public_key)?;
let mut store = load()?;
let before = store.trusted_keys.len();
store.trusted_keys.retain(|k| k.public_key != public_key);
let removed = store.trusted_keys.len() != before;
if removed {
save(&store)?;
}
Ok(removed)
}
pub fn trusted_keys() -> Vec<TrustedKey> {
let mut keys: Vec<TrustedKey> = env_keys()
.into_iter()
.map(|public_key| TrustedKey {
org: "env".to_string(),
public_key,
added_at: String::new(),
})
.collect();
if let Ok(store) = load() {
for k in store.trusted_keys {
if !keys.iter().any(|e| e.public_key == k.public_key) {
keys.push(k);
}
}
}
keys
}
#[must_use]
pub fn is_trusted(public_key: &str) -> bool {
let Ok(key) = normalize_key(public_key) else {
return false;
};
env_keys().contains(&key)
|| load().is_ok_and(|s| s.trusted_keys.iter().any(|k| k.public_key == key))
}
#[must_use]
pub fn any_pinned() -> bool {
!env_keys().is_empty() || load().is_ok_and(|s| !s.trusted_keys.is_empty())
}
fn env_keys() -> Vec<String> {
std::env::var(TRUST_ENV)
.ok()
.into_iter()
.flat_map(|v| {
v.split(',')
.filter_map(|k| normalize_key(k).ok())
.collect::<Vec<_>>()
})
.collect()
}
fn normalize_key(key: &str) -> Result<String, String> {
let k = key.trim().to_ascii_lowercase();
if k.len() != 64 || !k.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(format!(
"invalid public key (expected 64 hex chars, got {})",
k.len()
));
}
Ok(k)
}
fn now() -> String {
chrono::Utc::now().to_rfc3339()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::data_dir::isolated_data_dir;
fn sample_key() -> String {
"ab".repeat(32)
}
#[test]
fn normalize_rejects_bad_keys() {
assert!(normalize_key("xyz").is_err());
assert!(normalize_key(&"zz".repeat(32)).is_err());
assert!(normalize_key(&sample_key()).is_ok());
}
#[test]
fn pin_then_trusted_then_remove() {
let _iso = isolated_data_dir();
let key = sample_key();
assert!(!is_trusted(&key));
assert!(pin("acme", &key).unwrap());
assert!(is_trusted(&key));
assert!(any_pinned());
assert!(remove(&key).unwrap());
assert!(!is_trusted(&key));
}
#[test]
fn repin_same_key_updates_org() {
let _iso = isolated_data_dir();
let key = sample_key();
pin("acme", &key).unwrap();
assert!(pin("acme-renamed", &key).unwrap());
assert_eq!(
trusted_keys()
.iter()
.filter(|k| k.public_key == key)
.count(),
1
);
}
#[test]
fn env_override_is_trusted() {
let _iso = isolated_data_dir();
let key = sample_key();
crate::test_env::set_var(TRUST_ENV, &key);
assert!(is_trusted(&key));
assert!(any_pinned());
crate::test_env::remove_var(TRUST_ENV);
}
}