use neurogrim_secrets::SecretKey;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use uuid::Uuid;
pub const DEFAULT_TTL_SECS: u64 = 60;
#[derive(Debug, Clone)]
pub struct ProxyToken {
pub token_id: String,
pub secret_key: SecretKey,
pub minted_at: Instant,
pub expires_at: Instant,
pub scope: Option<String>,
pub used: bool,
}
impl ProxyToken {
pub fn is_expired(&self, now: Instant) -> bool {
now >= self.expires_at
}
pub fn audit_summary(&self) -> String {
format!(
"ProxyToken(id={}, brain={}, secret_id={}, scope={:?}, used={})",
self.token_id, self.secret_key.brain_id, self.secret_key.secret_id, self.scope, self.used
)
}
}
#[derive(Debug, Clone, Default)]
pub struct ProxyTokenStore {
tokens: Arc<Mutex<HashMap<String, ProxyToken>>>,
}
impl ProxyTokenStore {
pub fn new() -> Self {
Self::default()
}
pub fn mint(
&self,
secret_key: SecretKey,
scope: Option<String>,
ttl_secs: Option<u64>,
) -> ProxyToken {
let ttl = Duration::from_secs(ttl_secs.unwrap_or(DEFAULT_TTL_SECS));
let now = Instant::now();
let token_id = Uuid::new_v4().to_string();
let token = ProxyToken {
token_id: token_id.clone(),
secret_key,
minted_at: now,
expires_at: now + ttl,
scope,
used: false,
};
self.tokens.lock().unwrap().insert(token_id, token.clone());
token
}
pub fn redeem(&self, token_id: &str) -> Option<ProxyToken> {
let mut map = self.tokens.lock().unwrap();
let token = map.get_mut(token_id)?;
let now = Instant::now();
if token.used || token.is_expired(now) {
return None;
}
token.used = true;
Some(token.clone())
}
pub fn sweep_expired(&self) -> usize {
let now = Instant::now();
let mut map = self.tokens.lock().unwrap();
let before = map.len();
map.retain(|_, t| !t.used && !t.is_expired(now));
before - map.len()
}
pub fn outstanding(&self) -> usize {
let now = Instant::now();
let map = self.tokens.lock().unwrap();
map.values()
.filter(|t| !t.used && !t.is_expired(now))
.count()
}
}
#[cfg(test)]
mod tests {
use super::*;
use neurogrim_secrets::SecretKey;
fn key() -> SecretKey {
SecretKey::new("test-brain", "test-secret")
}
#[test]
fn mint_returns_token_with_uuid_and_60s_ttl() {
let store = ProxyTokenStore::new();
let token = store.mint(key(), None, None);
assert!(!token.token_id.is_empty());
assert_eq!(token.token_id.len(), 36);
assert_eq!(token.token_id.matches('-').count(), 4);
let ttl = token.expires_at.duration_since(token.minted_at);
assert_eq!(ttl, Duration::from_secs(60));
}
#[test]
fn mint_with_custom_ttl() {
let store = ProxyTokenStore::new();
let token = store.mint(key(), None, Some(120));
let ttl = token.expires_at.duration_since(token.minted_at);
assert_eq!(ttl, Duration::from_secs(120));
}
#[test]
fn redeem_returns_token_then_marks_used() {
let store = ProxyTokenStore::new();
let minted = store.mint(key(), Some("once".to_string()), None);
let redeemed = store.redeem(&minted.token_id).expect("first redeem");
assert_eq!(redeemed.token_id, minted.token_id);
assert!(redeemed.used, "redeem flips the used flag");
assert_eq!(redeemed.scope.as_deref(), Some("once"));
}
#[test]
fn redeem_twice_returns_none_second_time() {
let store = ProxyTokenStore::new();
let minted = store.mint(key(), None, None);
store.redeem(&minted.token_id).expect("first ok");
assert!(
store.redeem(&minted.token_id).is_none(),
"single-use guarantee: second redeem must return None"
);
}
#[test]
fn redeem_unknown_token_returns_none() {
let store = ProxyTokenStore::new();
assert!(store.redeem("not-a-real-token-id").is_none());
}
#[test]
fn redeem_expired_token_returns_none() {
let store = ProxyTokenStore::new();
let minted = store.mint(key(), None, Some(0));
std::thread::sleep(Duration::from_millis(10));
assert!(store.redeem(&minted.token_id).is_none());
}
#[test]
fn sweep_expired_removes_used_and_expired() {
let store = ProxyTokenStore::new();
let live = store.mint(key(), None, Some(60));
let used = store.mint(key(), None, Some(60));
let _expired = store.mint(key(), None, Some(0));
store.redeem(&used.token_id);
std::thread::sleep(Duration::from_millis(10));
let removed = store.sweep_expired();
assert_eq!(removed, 2, "used + expired removed; live remains");
assert_eq!(store.outstanding(), 1);
assert!(store.redeem(&live.token_id).is_some());
}
#[test]
fn outstanding_counts_only_unused_unexpired() {
let store = ProxyTokenStore::new();
let _live = store.mint(key(), None, Some(60));
let used = store.mint(key(), None, Some(60));
let _expired = store.mint(key(), None, Some(0));
store.redeem(&used.token_id);
std::thread::sleep(Duration::from_millis(10));
assert_eq!(store.outstanding(), 1);
}
#[test]
fn audit_summary_redacts_value_metadata_only() {
let store = ProxyTokenStore::new();
let token = store.mint(SecretKey::new("alpha", "anthropic"), None, None);
let summary = token.audit_summary();
assert!(summary.contains("alpha"));
assert!(summary.contains("anthropic"));
assert!(summary.contains(&token.token_id));
}
#[test]
fn token_store_is_clone_for_sharing_across_handlers() {
let a = ProxyTokenStore::new();
let b = a.clone();
let token = a.mint(key(), None, None);
let redeemed = b.redeem(&token.token_id);
assert!(redeemed.is_some(), "clone shares state with original");
}
}