act-credentials 0.12.0

Credential profiles, field definitions and storage backends for the ACT host
Documentation
use crate::record::{SecretInfo, SecretRecord};

#[derive(Debug, thiserror::Error)]
pub enum StoreError {
    #[error("credential store io: {0}")]
    Io(#[from] std::io::Error),
    #[error("credential store encoding: {0}")]
    Encoding(String),
    #[error("credential store is read-only; {0}")]
    ReadOnly(String),
    #[error("credential store unavailable: {0}")]
    Unavailable(String),
}

/// Keys are `(component, key)`. `component` is the resolved reference the
/// operator used, which is what makes the profile a per-component namespace.
pub trait CredentialStore: Send + Sync {
    fn get(&self, component: &str, key: &str) -> Result<Option<SecretRecord>, StoreError>;
    fn put(&self, component: &str, key: &str, rec: &SecretRecord) -> Result<(), StoreError>;
    fn erase(&self, component: &str, key: &str) -> Result<(), StoreError>;
    /// Metadata only, never values. `None` lists every component.
    fn list(&self, component: Option<&str>) -> Result<Vec<SecretInfo>, StoreError>;
    /// The components holding at least one credential.
    ///
    /// `list(None)` flattens the profile away, which is the one thing a
    /// listing across components must not lose: profile keys are normalised
    /// (`act-cli`'s `resolve::profile_key`), so this is also how an operator
    /// sees which key a `set` actually landed under. Component names, like
    /// `SecretInfo`, cannot carry a value.
    fn components(&self) -> Result<Vec<String>, StoreError>;

    /// Read, mutate and write one record **atomically against other processes**.
    ///
    /// `Ok(None)` when the key does not exist; the mutation is not run.
    ///
    /// This exists because `get` then `put` is not the same thing. Two `act`
    /// processes sharing the default store โ€” two MCP servers under one client
    /// is the ordinary case โ€” can interleave there, and the loser's write wins
    /// with data read before the winner's. For an OAuth refresh that is not a
    /// lost update but a dead credential: with refresh-token rotation the
    /// overwritten token is the only one the server still honours, and the user
    /// is sent back to `act login` with nothing to explain it.
    ///
    /// An implementation MUST hold an exclusive advisory lock across the whole
    /// read-modify-write, and the caller MUST re-check inside the closure what
    /// it decided outside it: by the time the lock is held, the work may already
    /// have been done by whoever held it first.
    fn update(
        &self,
        component: &str,
        key: &str,
        mutate: &mut dyn FnMut(&mut SecretRecord),
    ) -> Result<Option<SecretRecord>, StoreError>;
}

// There is deliberately no `writable()`. It existed for the read-only reference
// backends of design ยง7.1, which are an open question rather than a feature, and
// nothing consulted it: `put` and `erase` never asked, so a backend answering
// `false` would have been written to anyway. A trait method that every
// implementor must write, that no caller reads, and that reads as a promise the
// crate does not keep is worse than its absence. It comes back with the first
// backend that can answer `false` โ€” and with the check that honours it.