earl 0.5.2

AI-safe CLI for AI agents
use anyhow::{Result, anyhow};
use keyring::{Entry, Error as KeyringError};
use secrecy::{ExposeSecret, SecretString};

use super::store::SecretStore;

const SERVICE_NAME: &str = "earl";

#[derive(Debug, Default)]
pub struct KeychainSecretStore;

impl SecretStore for KeychainSecretStore {
    fn set_secret(&self, key: &str, secret: SecretString) -> Result<()> {
        let entry = Entry::new(SERVICE_NAME, key)
            .map_err(|err| anyhow!("failed creating keyring entry for `{key}`: {err}"))?;
        entry
            .set_password(secret.expose_secret())
            .map_err(|err| anyhow!("failed storing secret `{key}` in keychain: {err}"))?;
        Ok(())
    }

    fn get_secret(&self, key: &str) -> Result<Option<SecretString>> {
        let entry = Entry::new(SERVICE_NAME, key)
            .map_err(|err| anyhow!("failed creating keyring entry for `{key}`: {err}"))?;
        match entry.get_password() {
            Ok(value) => Ok(Some(SecretString::new(value.into()))),
            Err(KeyringError::NoEntry) => Ok(None),
            Err(err) => Err(anyhow!(
                "failed reading secret `{key}` from keychain: {err}"
            )),
        }
    }

    fn delete_secret(&self, key: &str) -> Result<bool> {
        let entry = Entry::new(SERVICE_NAME, key)
            .map_err(|err| anyhow!("failed creating keyring entry for `{key}`: {err}"))?;
        match entry.delete_credential() {
            Ok(_) => Ok(true),
            Err(KeyringError::NoEntry) => Ok(false),
            Err(err) => Err(anyhow!(
                "failed deleting secret `{key}` from keychain: {err}"
            )),
        }
    }
}