pigeon-cli 0.1.0

Pigeon: authenticate, sink, and transform personal data from external services.
Documentation
/// Distinct from `email::credentials`'s `"pigeon"` service name so a
/// bucket-config and an email identity that happen to share an alias (e.g.
/// both called "email") can never collide in the OS keychain.
const SERVICE_NAME: &str = "pigeon-dataops";

/// Stores `secret` (the S3 secret access key) in the OS-native secure
/// credential store, keyed by bucket-config `alias`. Per ADR-0009, this is
/// the only place a secret access key is ever written -- never to
/// `bucket-configs.toml`.
pub fn set_secret(alias: &str, secret: &str) -> Result<(), String> {
    let entry = keyring::Entry::new(SERVICE_NAME, alias)
        .map_err(|err| format!("failed to open keychain entry for '{alias}': {err}"))?;
    entry
        .set_password(secret)
        .map_err(|err| format!("failed to store secret for '{alias}': {err}"))
}

/// Reads back the secret stored for `alias` via `set_secret`.
pub fn get_secret(alias: &str) -> Result<String, String> {
    let entry = keyring::Entry::new(SERVICE_NAME, alias)
        .map_err(|err| format!("failed to open keychain entry for '{alias}': {err}"))?;
    entry
        .get_password()
        .map_err(|err| format!("failed to read secret for '{alias}': {err}"))
}

/// Removes the stored secret for `alias`, if any. A missing entry
/// (`keyring::Error::NoEntry`) is treated as success, not a failure -- used
/// both to roll back a partially completed `bucket-config new`/`edit` and by
/// `bucket-config remove`, which should cleanly no-op if there was never a
/// secret to begin with (e.g. a hand-edited `bucket-configs.toml` entry).
pub fn delete_secret(alias: &str) -> Result<(), String> {
    let entry = keyring::Entry::new(SERVICE_NAME, alias)
        .map_err(|err| format!("failed to open keychain entry for '{alias}': {err}"))?;
    match entry.delete_credential() {
        Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
        Err(err) => Err(format!("failed to delete secret for '{alias}': {err}")),
    }
}