use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct TrustedKey {
pub org: String,
pub public_key: String,
pub added_at: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct TrustStore {
pub keys: Vec<TrustedKey>,
}
pub fn trust_path() -> Result<PathBuf, String> {
let dir = crate::core::paths::data_dir()?;
Ok(dir.join("org-trust.toml"))
}
pub(crate) fn trust_path_read_only() -> Result<PathBuf, String> {
Ok(crate::core::data_dir::resolve_data_dir()?.join("org-trust.toml"))
}
pub fn load() -> Result<TrustStore, String> {
let path = trust_path()?;
let Some(text) = crate::core::policy::files::read(&path, false)? else {
return Ok(TrustStore::default());
};
toml::from_str(&text).map_err(|_| "org trust input is invalid".into())
}
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 {}: {e}", parent.display()))?;
}
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(crate) fn load_read_only() -> Result<TrustStore, String> {
let Some(text) = crate::core::policy::files::read(&trust_path_read_only()?, false)? else {
return Ok(TrustStore::default());
};
toml::from_str(&text).map_err(|_| "org trust input is invalid".into())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PinOutcome {
Added,
AlreadyPinned,
PinnedForAnother(String),
}
pub fn pin_for(org: &str, public_key: &str) -> Result<PinOutcome, String> {
let mut store = load()?;
if let Some(existing) = store.keys.iter().find(|k| k.public_key == public_key) {
return Ok(if existing.org == org {
PinOutcome::AlreadyPinned
} else {
PinOutcome::PinnedForAnother(existing.org.clone())
});
}
store.keys.push(TrustedKey {
org: org.to_string(),
public_key: public_key.to_string(),
added_at: chrono::Utc::now().to_rfc3339(),
});
save(&store)?;
Ok(PinOutcome::Added)
}
pub fn pin(org: &str, public_key: &str) -> Result<bool, String> {
pin_for(org, public_key).map(|outcome| outcome == PinOutcome::Added)
}
pub fn is_trusted_for(org: &str, public_key: &str) -> bool {
load_read_only()
.map(|store| {
store
.keys
.iter()
.any(|key| key.public_key == public_key && key.org == org)
})
.unwrap_or(false)
}
pub fn pinned_organization(public_key: &str) -> Option<String> {
load_read_only()
.ok()?
.keys
.into_iter()
.find(|key| key.public_key == public_key)
.map(|key| key.org)
}
pub fn remove(public_key: &str) -> Result<bool, String> {
let mut store = load()?;
let before = store.keys.len();
store.keys.retain(|k| k.public_key != public_key);
if store.keys.len() < before {
save(&store)?;
Ok(true)
} else {
Ok(false)
}
}
pub fn remove_org(org: &str) -> Result<Vec<String>, String> {
let mut store = load()?;
let (removed, kept): (Vec<TrustedKey>, Vec<TrustedKey>) =
store.keys.into_iter().partition(|k| k.org == org);
store.keys = kept;
if !removed.is_empty() {
save(&store)?;
}
Ok(removed.into_iter().map(|k| k.public_key).collect())
}
pub fn trusted_keys() -> Vec<TrustedKey> {
load().unwrap_or_default().keys
}
pub fn is_trusted(public_key: &str) -> bool {
load()
.map(|s| s.keys.iter().any(|k| k.public_key == public_key))
.unwrap_or(false)
}
pub fn any_pinned() -> bool {
load().map(|s| !s.keys.is_empty()).unwrap_or(false)
}