use async_trait::async_trait;
use keyring::Entry;
use origin_domain::{AppError, Result};
use origin_secrets::{Secret, SecretKey, SecretStore};
#[derive(Debug, Clone)]
pub struct SystemSecretStore {
service_prefix: String,
}
impl SystemSecretStore {
pub fn new(service_prefix: impl Into<String>) -> Self {
Self {
service_prefix: service_prefix.into(),
}
}
fn entry(&self, key: &SecretKey) -> Result<Entry> {
let service = format!("{}.{}", self.service_prefix, key.namespace());
Entry::new(&service, key.name()).map_err(to_app_error)
}
}
#[async_trait]
impl SecretStore for SystemSecretStore {
async fn get(&self, key: &SecretKey) -> Result<Option<Secret>> {
let entry = self.entry(key)?;
blocking(move || match entry.get_password() {
Ok(password) => Ok(Some(Secret::new(password))),
Err(keyring::Error::NoEntry) => Ok(None),
Err(error) => Err(to_app_error(error)),
})
.await
}
async fn set(&self, key: &SecretKey, value: Secret) -> Result<()> {
let entry = self.entry(key)?;
blocking(move || entry.set_password(value.expose()).map_err(to_app_error)).await
}
async fn delete(&self, key: &SecretKey) -> Result<()> {
let entry = self.entry(key)?;
blocking(move || match entry.delete_credential() {
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
Err(error) => Err(to_app_error(error)),
})
.await
}
}
async fn blocking<T, F>(operation: F) -> Result<T>
where
T: Send + 'static,
F: FnOnce() -> Result<T> + Send + 'static,
{
tokio::task::spawn_blocking(operation)
.await
.map_err(|error| AppError::internal(format!("credential task failed: {error}")))?
}
fn to_app_error(error: keyring::Error) -> AppError {
match error {
keyring::Error::NoEntry => AppError::Authentication("no credential stored".to_owned()),
keyring::Error::NoDefaultStore => {
AppError::Configuration("no credential store available on this system".to_owned())
}
keyring::Error::Ambiguous(_) => AppError::Storage(
"several credentials matched — the credential store needs manual cleanup".to_owned(),
),
keyring::Error::PlatformFailure(inner) => {
AppError::Storage(format!("credential store unavailable: {inner}"))
}
keyring::Error::NoStorageAccess(inner) => {
AppError::Permission(format!("credential store access denied: {inner}"))
}
other => AppError::Storage(other.to_string()),
}
}