ppoppo-token 0.5.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
//! Per-request verification configuration for the RFC 9068 access-token
//! profile.
//!
//! ── Composition ────────────────────────────────────────────────────────
//!
//! JOSE-shared fields (`issuer`, `audience`, `expected_typ`,
//! `max_token_size`, `algorithms`) live in `engine::SharedVerifyConfig`
//! and are reached via `self.shared`. Access-token-specific axes
//! (replay/session/epoch revocation ports) stay on this struct. The
//! engine submodules `check_algorithm` / `check_header` / `raw` read
//! only from `&SharedVerifyConfig`; `check_claims` / `check_domain` /
//! revocation checks read from this struct (and reach the shared
//! fields via `cfg.shared.*`).
//!
//! ── Phase 5 — orthogonal port slots ────────────────────────────────────
//!
//! Replay / session ports model orthogonal revocation axes (M35-M38).
//! Each is `Option<Arc<dyn ...>>` so callers wire only what their
//! deployment substrate supports — `None` short-circuits the gate
//! (legacy admit / sibling-test config / migration phases).
//!
//! The **epoch axis is different** (RFC_202607150428 §7 Q3): its slot is
//! a required [`EpochEnforcement`] typed stance, not an `Option` — the
//! `epoch: None` silent default is what let four shipped consumers skip
//! the only mechanism that can kill a live access token. Constructing a
//! `VerifyConfig` without declaring the stance no longer compiles.

use std::sync::Arc;

use super::epoch_revocation::EpochEnforcement;
use super::replay_defense::ReplayDefense;
use super::session_revocation::SessionRevocation;
use crate::algorithm::Algorithm;
use crate::engine::shared_config::SharedVerifyConfig;

#[derive(Debug, Clone)]
#[allow(dead_code)] // ports consumed across Phase 5+
pub struct VerifyConfig {
    pub(crate) shared: SharedVerifyConfig,

    // ── Phase 5 revocation port slots ──────────────────────────────────
    /// M35 jti replay defense (Phase 5 commit 5.1).
    pub(crate) replay: Option<Arc<dyn ReplayDefense>>,
    /// M36 session-row liveness (Phase 5 commit 5.2).
    pub(crate) session: Option<Arc<dyn SessionRevocation>>,
    /// sv-axis typed stance (required — RFC_202607150428 Q3).
    pub(crate) epoch: EpochEnforcement,
}

impl VerifyConfig {
    /// Canonical access-token config: `at+jwt` typ, sealed-vocabulary
    /// algorithm whitelist (`Algorithm::ALL` — every alg PAS recognises;
    /// EdDSA-only today), 8 KB token size cap (M34). Replay / session
    /// port slots default to `None` (opt in via the builders); the sv
    /// epoch stance is **required** — there is no silent default.
    pub fn access_token(
        issuer: impl Into<String>,
        audience: impl Into<String>,
        epoch: EpochEnforcement,
    ) -> Self {
        Self {
            shared: SharedVerifyConfig::new(
                issuer,
                audience,
                "at+jwt",
                8 * 1024,
                Algorithm::ALL.to_vec(),
            ),
            replay: None,
            session: None,
            epoch,
        }
    }

    /// Override the algorithm whitelist. Test-only escape hatch — production
    /// callers MUST go through `access_token` so the EdDSA pin is the default.
    #[must_use]
    pub fn with_algorithms(mut self, algorithms: Vec<Algorithm>) -> Self {
        self.shared.algorithms = algorithms;
        self
    }

    /// Wire the M35 jti replay defense port.
    #[must_use]
    pub fn with_replay_defense(mut self, port: Arc<dyn ReplayDefense>) -> Self {
        self.replay = Some(port);
        self
    }

    /// Wire the M36 session-row liveness port.
    #[must_use]
    pub fn with_session_revocation(mut self, port: Arc<dyn SessionRevocation>) -> Self {
        self.session = Some(port);
        self
    }
}