ppoppo-token 0.3.0

JWT (RFC 9068, EdDSA) issuance + verification engine for the Ppoppo ecosystem. Single deep module with a small interface (issue, verify) hiding RFC 8725 mitigations M01-M45, JWKS handling, and substrate ports (epoch, session, replay).
Documentation
//! In-memory `SessionRevocation` adapter for engine integration tests.

use std::collections::HashSet;
use std::sync::Mutex;

use ppoppo_token::access_token::{SessionRevocation, SessionRevocationError};
#[derive(Debug)]
pub struct MemorySessionRevocation {
    revoked: Mutex<HashSet<(String, String)>>,
    failing: bool,
}

impl MemorySessionRevocation {
    /// Working substrate — sessions are active by default; explicitly
    /// revoked `(sub, sid)` pairs return `Ok(false)`.
    pub fn new() -> Self {
        Self {
            revoked: Mutex::new(HashSet::new()),
            failing: false,
        }
    }

    /// Always-failing substrate — every call returns `Transient`.
    pub fn failing() -> Self {
        Self {
            revoked: Mutex::new(HashSet::new()),
            failing: true,
        }
    }

    /// Mark `(sub, sid)` as revoked. Mirrors `DELETE FROM user_sessions
    /// WHERE (sub, sid) = ($1, $2)` — STANDARDS_JWT_DETAILS_MITIGATION
    /// §E "row deletion = revocation".
    pub fn revoke(&self, sub: &str, sid: &str) {
        let mut rev = self.revoked.lock().unwrap_or_else(|p| p.into_inner());
        rev.insert((sub.to_string(), sid.to_string()));
    }
}

impl Default for MemorySessionRevocation {
    fn default() -> Self {
        Self::new()
    }
}

#[async_trait::async_trait]
impl SessionRevocation for MemorySessionRevocation {
    async fn is_active(
        &self,
        sub: &str,
        sid: &str,
    ) -> Result<bool, SessionRevocationError> {
        if self.failing {
            return Err(SessionRevocationError::Transient(
                "MemorySessionRevocation::failing()".to_string(),
            ));
        }
        let rev = self.revoked.lock().unwrap_or_else(|p| p.into_inner());
        Ok(!rev.contains(&(sub.to_string(), sid.to_string())))
    }
}