car-ffi-common 0.22.1

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
Documentation
//! Shared JSON wrapper for `car-secrets` operations, consumed by the CLI,
//! NAPI + PyO3 bindings, and the WebSocket server.
//!
//! Each typed `SecretError` variant maps to a stable machine-readable
//! `code` so callers can branch programmatically instead of parsing
//! message strings. The error JSON shape is:
//!
//! ```json
//! {
//!   "code": "not_found | unavailable | backend | invalid_json",
//!   "message": "...",
//!   "context": {"service": "...", "key": "..."}
//! }
//! ```
//!
//! FFI surfaces (NAPI, PyO3, JSON-RPC) surface this as the error message
//! body — consumers parse the JSON to read `code` programmatically.

use car_secrets::{SecretError, SecretRef, SecretStatus, SecretStore, DEFAULT_SERVICE};
use serde_json::{json, Value};

fn make_ref(service: Option<&str>, key: &str) -> SecretRef {
    SecretRef::new(service.unwrap_or(DEFAULT_SERVICE), key)
}

/// Stable error codes the FFI surface exposes.
fn code_for(e: &SecretError) -> &'static str {
    match e {
        SecretError::NotFound { .. } => "not_found",
        SecretError::Unavailable(_) => "unavailable",
        SecretError::Backend(_) => "backend",
        SecretError::InvalidJson(_) => "invalid_json",
    }
}

fn json_err(e: SecretError) -> String {
    let mut body = json!({
        "code": code_for(&e),
        "message": e.to_string(),
    });
    if let SecretError::NotFound { service, key } = &e {
        body["context"] = json!({"service": service, "key": key});
    }
    body.to_string()
}

pub fn put(service: Option<&str>, key: &str, value: &str) -> Result<Value, String> {
    let store = SecretStore::new();
    store
        .put(&make_ref(service, key), value)
        .map_err(json_err)?;
    Ok(json!({"service": service.unwrap_or(DEFAULT_SERVICE), "key": key, "stored": true}))
}

pub fn get(service: Option<&str>, key: &str) -> Result<Value, String> {
    let store = SecretStore::new();
    let v = store.get(&make_ref(service, key)).map_err(json_err)?;
    Ok(json!({"service": service.unwrap_or(DEFAULT_SERVICE), "key": key, "value": v}))
}

pub fn delete(service: Option<&str>, key: &str) -> Result<Value, String> {
    let store = SecretStore::new();
    store.delete(&make_ref(service, key)).map_err(json_err)?;
    Ok(json!({"service": service.unwrap_or(DEFAULT_SERVICE), "key": key, "deleted": true}))
}

pub fn status(service: Option<&str>, key: &str) -> Result<Value, String> {
    let store = SecretStore::new();
    let st: SecretStatus = store.status(&make_ref(service, key)).map_err(json_err)?;
    serde_json::to_value(&st)
        .map_err(|e| json!({"code": "backend", "message": e.to_string()}).to_string())
}

pub fn is_available() -> Value {
    let check = SecretStore::new().availability();
    serde_json::to_value(&check).unwrap_or_else(|_| json!({"available": check.available}))
}

/// Internal accessor used by other car-ffi-common modules (e.g. the browser
/// session_ref path). Returns the raw string value.
pub fn read_raw(service: Option<&str>, key: &str) -> Result<String, String> {
    SecretStore::new()
        .get(&make_ref(service, key))
        .map_err(json_err)
}