use leviath_core::{CredentialStore, CredentialStoreKind};
pub const PROVIDER_KEYS: &[&str] = &["anthropic", "openai", "google", "openrouter"];
pub struct KeychainStore {
service: String,
}
impl KeychainStore {
pub fn new(service: &str) -> Self {
Self {
service: service.to_string(),
}
}
}
impl CredentialStore for KeychainStore {
fn get(&self, account: &str) -> Result<Option<String>, String> {
leviath_sys::keychain::get(&self.service, account)
}
fn set(&self, account: &str, secret: &str) -> Result<(), String> {
leviath_sys::keychain::set(&self.service, account, secret)
}
fn delete(&self, account: &str) -> Result<bool, String> {
leviath_sys::keychain::delete(&self.service, account)
}
}
pub fn store_for(kind: CredentialStoreKind) -> Resolved {
store_for_with(kind, leviath_sys::keychain::probe)
}
pub type Resolved = Result<Option<Box<dyn CredentialStore>>, String>;
fn store_for_with(kind: CredentialStoreKind, probe: fn(&str) -> Result<(), String>) -> Resolved {
match kind {
CredentialStoreKind::File => Ok(None),
CredentialStoreKind::Keychain => {
let service = leviath_core::credentials::SERVICE;
probe(service).map_err(|e| {
format!("`[security] credential_store = \"keychain\"` is set, but {e}")
})?;
Ok(Some(Box::new(KeychainStore::new(service))))
}
}
}
#[cfg(test)]
pub(crate) fn no_store_available(_service: &str) -> Result<(), String> {
Err("OS credential store unavailable: no default store".to_string())
}
#[cfg(test)]
pub(crate) mod test_store {
static STORE: std::sync::Mutex<()> = std::sync::Mutex::new(());
pub(crate) fn lock() -> std::sync::MutexGuard<'static, ()> {
STORE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub(crate) fn with_mock() -> std::sync::MutexGuard<'static, ()> {
let guard = lock();
keyring_core::set_default_store(keyring_core::mock::Store::new().expect("mock store"));
guard
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::credentials::test_store;
fn with_mock_store() -> std::sync::MutexGuard<'static, ()> {
test_store::with_mock()
}
#[test]
fn the_keychain_adapter_round_trips_a_secret() {
let _guard = with_mock_store();
let store = KeychainStore::new("dev.leviath.test.adapter");
let account = leviath_core::provider_account("anthropic");
assert_eq!(store.get(&account).unwrap(), None);
store.set(&account, "sk-ant-x").unwrap();
assert_eq!(store.get(&account).unwrap().as_deref(), Some("sk-ant-x"));
assert!(store.delete(&account).unwrap());
assert!(!store.delete(&account).unwrap());
}
#[test]
fn the_file_backend_never_probes() {
static PROBED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
fn record(_: &str) -> Result<(), String> {
PROBED.store(true, std::sync::atomic::Ordering::Relaxed);
Ok(())
}
assert!(
store_for_with(CredentialStoreKind::File, record)
.unwrap()
.is_none(),
"the file backend is `None`, not a store"
);
assert!(
!PROBED.load(std::sync::atomic::Ordering::Relaxed),
"the file backend must not probe for an OS credential store"
);
assert!(store_for_with(CredentialStoreKind::Keychain, record).is_ok());
assert!(PROBED.load(std::sync::atomic::Ordering::Relaxed));
}
#[test]
fn asking_for_an_unavailable_keychain_names_the_setting() {
let err = store_for_with(CredentialStoreKind::Keychain, no_store_available)
.err()
.expect("a failing probe must not yield a store");
assert!(err.contains(r#"credential_store = "keychain""#), "{err}");
assert!(err.contains("credential store unavailable"), "{err}");
}
#[test]
fn the_keychain_backend_resolves_to_a_store() {
let _guard = with_mock_store();
assert!(
store_for(CredentialStoreKind::Keychain).unwrap().is_some(),
"with a store available the keychain backend resolves"
);
}
#[test]
fn every_provider_with_a_config_key_is_listed() {
for p in ["anthropic", "openai", "google", "openrouter"] {
assert!(PROVIDER_KEYS.contains(&p), "{p} must be migratable");
}
}
}