tranc-cli 0.1.1

Tranc CLI — trade indicator queries from the command line.
//! OS keyring abstraction for storing the Tranc auth token.
//!
//! Uses the `keyring` crate which delegates to:
//! - macOS: Keychain Services
//! - Linux: Secret Service (via D-Bus / libsecret)
//! - Windows: Windows Credential Manager
//!
//! Tokens never touch disk outside of the OS keyring store.
//! Resolves issue #22.

use anyhow::{Context, Result};
use keyring::Entry;

const SERVICE: &str = "tranc-cli";
const ACCOUNT: &str = "auth-token";

/// Retrieve the stored auth token from the OS keyring.
///
/// Returns `None` if no token has been stored yet.
pub fn get_token() -> Result<Option<String>> {
    let entry = Entry::new(SERVICE, ACCOUNT).context("failed to open keyring entry")?;
    match entry.get_password() {
        Ok(token) => Ok(Some(token)),
        Err(keyring::Error::NoEntry) => Ok(None),
        Err(e) => Err(anyhow::anyhow!("keyring read error: {e}")),
    }
}

/// Store `token` in the OS keyring.
pub fn set_token(token: &str) -> Result<()> {
    let entry = Entry::new(SERVICE, ACCOUNT).context("failed to open keyring entry")?;
    entry
        .set_password(token)
        .context("failed to save token to keyring")?;
    Ok(())
}

/// Delete the stored auth token from the OS keyring.
///
/// Returns `Ok(())` even if no token was stored.
pub fn delete_token() -> Result<()> {
    let entry = Entry::new(SERVICE, ACCOUNT).context("failed to open keyring entry")?;
    match entry.delete_credential() {
        Ok(()) => Ok(()),
        Err(keyring::Error::NoEntry) => Ok(()),
        Err(e) => Err(anyhow::anyhow!("keyring delete error: {e}")),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Smoke test: round-trip a token through the OS keyring.
    ///
    /// This test is `#[ignore]` because it requires a live keyring daemon
    /// (Keychain / Secret Service) which may not be available in headless CI.
    /// Run manually: `cargo test -p tranc-cli -- --ignored keyring_roundtrip`
    #[test]
    #[ignore]
    fn keyring_roundtrip() {
        let token = "tranc_test_token_12345";
        set_token(token).expect("set_token failed");

        let retrieved = get_token().expect("get_token failed");
        assert_eq!(retrieved.as_deref(), Some(token));

        delete_token().expect("delete_token failed");
        let after_delete = get_token().expect("get_token after delete failed");
        assert!(after_delete.is_none());
    }
}