caretta 0.11.1

caretta agent
use anyhow::{Context, Result};
#[cfg(not(target_arch = "wasm32"))]
use keyring::Entry;
use std::hash::{Hash, Hasher};

const SERVICE_PREFIX: &str = "caretta";
const BOT_TOKEN_SLOT: &str = "bot-token";
const BOT_PEM_SLOT: &str = "bot-private-key-pem";
const LOCAL_INFERENCE_API_KEY_SLOT: &str = "local-inference-api-key";

fn root_scope(root: &str) -> String {
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    root.hash(&mut hasher);
    format!("{:016x}", hasher.finish())
}

#[cfg(not(target_arch = "wasm32"))]
fn entry(root: &str, slot: &str) -> Result<Entry> {
    Entry::new(
        &format!("{SERVICE_PREFIX}.{slot}"),
        &format!("project:{}", root_scope(root)),
    )
    .context("failed to open OS credential store entry")
}

#[cfg(not(target_arch = "wasm32"))]
pub fn load_secret(root: &str, slot: &str) -> Option<String> {
    let entry = entry(root, slot).ok()?;
    match entry.get_password() {
        Ok(secret) if !secret.trim().is_empty() => Some(secret),
        Ok(_) => None,
        Err(_) => None,
    }
}

#[cfg(target_arch = "wasm32")]
pub fn load_secret(_root: &str, _slot: &str) -> Option<String> {
    None
}

#[cfg(not(target_arch = "wasm32"))]
pub fn store_secret(root: &str, slot: &str, value: &str) -> Result<()> {
    let entry = entry(root, slot)?;
    entry
        .set_password(value)
        .with_context(|| format!("failed to store secret for {slot}"))
}

#[cfg(target_arch = "wasm32")]
pub fn store_secret(_root: &str, _slot: &str, _value: &str) -> Result<()> {
    Ok(())
}

#[cfg(not(target_arch = "wasm32"))]
pub fn clear_secret(root: &str, slot: &str) -> Result<()> {
    let entry = entry(root, slot)?;
    match entry.delete_credential() {
        Ok(()) => Ok(()),
        Err(_) => Ok(()),
    }
}

#[cfg(target_arch = "wasm32")]
pub fn clear_secret(_root: &str, _slot: &str) -> Result<()> {
    Ok(())
}

pub fn load_bot_token(root: &str) -> Option<String> {
    load_secret(root, BOT_TOKEN_SLOT)
}

pub fn store_bot_token(root: &str, token: &str) -> Result<()> {
    store_secret(root, BOT_TOKEN_SLOT, token)
}

pub fn clear_bot_token(root: &str) -> Result<()> {
    clear_secret(root, BOT_TOKEN_SLOT)
}

pub fn load_bot_private_key_pem(root: &str) -> Option<String> {
    load_secret(root, BOT_PEM_SLOT)
}

pub fn store_bot_private_key_pem(root: &str, pem: &str) -> Result<()> {
    store_secret(root, BOT_PEM_SLOT, pem)
}

pub fn clear_bot_private_key_pem(root: &str) -> Result<()> {
    clear_secret(root, BOT_PEM_SLOT)
}

pub fn load_local_inference_api_key(root: &str) -> Option<String> {
    load_secret(root, LOCAL_INFERENCE_API_KEY_SLOT)
}

pub fn store_local_inference_api_key(root: &str, api_key: &str) -> Result<()> {
    store_secret(root, LOCAL_INFERENCE_API_KEY_SLOT, api_key)
}

pub fn clear_local_inference_api_key(root: &str) -> Result<()> {
    clear_secret(root, LOCAL_INFERENCE_API_KEY_SLOT)
}

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

    #[test]
    fn root_scope_is_stable() {
        let a = root_scope("/tmp/project-a");
        let b = root_scope("/tmp/project-a");
        let c = root_scope("/tmp/project-b");
        assert_eq!(a, b);
        assert_ne!(a, c);
    }
}