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
//! Test-only `tracing` Layer that captures events into an in-memory
//! buffer. Used by `tests/revocation_observability.rs` to assert that
//! the engine's port call sites emit the M37 `revocation.checked`
//! events with the right `port` / `outcome` / `sub` field shape.
//!
//! Pattern follows `tracing_subscriber::Layer` composition — install
//! via `tracing::subscriber::set_default(Registry::default().with(layer))`
//! and the guard's drop restores the previous subscriber. Tokio
//! `current_thread` runtime (default for `#[tokio::test]`) keeps the
//! per-thread default valid across `.await` points.

use std::collections::HashMap;
use std::sync::{Arc, Mutex};

use tracing::Subscriber;
use tracing::field::{Field, Visit};
use tracing_subscriber::layer::{Context, Layer};
use tracing_subscriber::registry::LookupSpan;

#[derive(Clone, Default)]
pub struct EventCapture {
    events: Arc<Mutex<Vec<CapturedEvent>>>,
}

#[derive(Debug, Clone)]
pub struct CapturedEvent {
    pub name: String,
    pub target: String,
    pub level: String,
    pub fields: HashMap<String, String>,
}

impl EventCapture {
    /// Snapshot of all events captured so far.
    pub fn snapshot(&self) -> Vec<CapturedEvent> {
        self.events
            .lock()
            .unwrap_or_else(|p| p.into_inner())
            .clone()
    }

    /// Filtered snapshot — only events from the engine's M37 target.
    pub fn revocation_events(&self) -> Vec<CapturedEvent> {
        self.snapshot()
            .into_iter()
            .filter(|e| e.target == "ppoppo_token::revocation")
            .collect()
    }
}

#[derive(Default)]
struct FieldVisitor {
    fields: HashMap<String, String>,
}

impl Visit for FieldVisitor {
    fn record_str(&mut self, field: &Field, value: &str) {
        self.fields.insert(field.name().to_string(), value.to_string());
    }

    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
        self.fields
            .insert(field.name().to_string(), format!("{value:?}"));
    }

    fn record_i64(&mut self, field: &Field, value: i64) {
        self.fields.insert(field.name().to_string(), value.to_string());
    }

    fn record_u64(&mut self, field: &Field, value: u64) {
        self.fields.insert(field.name().to_string(), value.to_string());
    }

    fn record_bool(&mut self, field: &Field, value: bool) {
        self.fields.insert(field.name().to_string(), value.to_string());
    }
}

impl<S> Layer<S> for EventCapture
where
    S: Subscriber + for<'a> LookupSpan<'a>,
{
    fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
        let metadata = event.metadata();
        let mut visitor = FieldVisitor::default();
        event.record(&mut visitor);
        self.events
            .lock()
            .unwrap_or_else(|p| p.into_inner())
            .push(CapturedEvent {
                name: metadata.name().to_string(),
                target: metadata.target().to_string(),
                level: metadata.level().to_string(),
                fields: visitor.fields,
            });
    }
}