ppoppo-token 0.1.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 shared by all entry points.
//!
//! SSOT for the policy values that drive `engine::verify`. The same
//! struct is consumed by PAS self-verify, PCS service-side check, and the
//! `pas-external` consumer middleware so policy never drifts between the
//! three surfaces (STANDARDS_JWT_DETAILS §3).
//!
//! ── Phase 5 — orthogonal port slots ────────────────────────────────────
//!
//! Three optional ports model the orthogonal revocation axes (M35-M38 +
//! sv). 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).
//!
//! - `replay`  — M35 jti uniqueness window
//! - `session` — M36 per-session row liveness
//! - `epoch`   — sv per-account version (chat-auth migration target)
//!
//! Phase 10 split: these slots stay on `access_token::VerifyConfig`;
//! `id_token::VerifyConfig` carries its own (`expected_nonce`,
//! `max_age`, `acr_values`) and never imports these traits.

use std::sync::Arc;

use crate::epoch_revocation::EpochRevocation;
use crate::replay_defense::ReplayDefense;
use crate::session_revocation::SessionRevocation;

/// Sealed JWS signature algorithm whitelist (Phase 7 §6.8 — structural M51/M52/M54).
///
/// Only `EdDSA` exists. Consumer attempts to construct `Algorithm::HS256`
/// or any other variant fail at compile time (`variant not found`),
/// making M51/M52/M54 enforcement structural rather than lint-based.
/// `jsonwebtoken::Algorithm` is no longer re-exported — `crates/shared/ppoppo-token`
/// owns the algorithm vocabulary.
///
/// Adding a new variant (e.g., for OIDC interop in Phase 10) is a deliberate
/// spec change — the matrix M02/M06 rows must be revisited and the negative
/// regression in `tests/jwt_negative.rs` reinstated to cover the cfg-vs-header
/// SSOT invariant.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Algorithm {
    /// EdDSA over Ed25519 — RFC 9068 access-token profile.
    EdDSA,
}

impl std::str::FromStr for Algorithm {
    type Err = ();

    /// Parse the `alg` header field. Anything other than `"EdDSA"` is
    /// rejected — family-level rejections (HS/RS/ES) fire earlier in
    /// `check_algorithm::run` to give audit logs the family signal.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "EdDSA" => Ok(Algorithm::EdDSA),
            _ => Err(()),
        }
    }
}

#[derive(Debug, Clone)]
#[allow(dead_code)] // fields consumed across commits 1.2-1.16 + Phase 2+
pub struct VerifyConfig {
    pub(crate) issuer: String,
    pub(crate) audience: String,
    pub(crate) expected_typ: &'static str,
    pub(crate) max_token_size: usize,
    pub(crate) algorithms: Vec<Algorithm>,

    // ── Phase 5 revocation port slots ──────────────────────────────────
    /// M35 jti replay defense (Phase 5 commit 5.1). `None` skips the
    /// gate — appropriate for tests / non-revocation-aware deployments.
    pub(crate) replay: Option<Arc<dyn ReplayDefense>>,

    /// M36 session-row liveness (Phase 5 commit 5.2). `None` skips —
    /// appropriate when issuance hasn't started emitting `sid` yet
    /// (gradual-rollout pattern).
    pub(crate) session: Option<Arc<dyn SessionRevocation>>,

    /// sv-port per-account epoch (Phase 5 commits 5.5-5.7). `None` skips
    /// — chat-auth migration sets this to the substrate adapter that
    /// internally composes its existing cache + fetcher.
    pub(crate) epoch: Option<Arc<dyn EpochRevocation>>,
}

impl VerifyConfig {
    /// Build the canonical access-token config: `at+jwt` typ, EdDSA-only
    /// algorithm whitelist, 8 KB token size cap (M34). All revocation
    /// port slots default to `None`; callers opt in via the
    /// `with_replay_defense` / `with_session_revocation` /
    /// `with_epoch_revocation` builders.
    pub fn access_token(issuer: impl Into<String>, audience: impl Into<String>) -> Self {
        Self {
            issuer: issuer.into(),
            audience: audience.into(),
            expected_typ: "at+jwt",
            max_token_size: 8 * 1024,
            algorithms: vec![Algorithm::EdDSA],
            replay: None,
            session: None,
            epoch: None,
        }
    }

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

    /// Wire the M35 jti replay defense port. Call site (PCS chat-auth /
    /// pas-external SDK) constructs the substrate adapter (KVRocks,
    /// in-memory test stand-in) and hands the `Arc<dyn ...>` here.
    #[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
    }

    /// Wire the sv-port per-account epoch revocation. Implementations
    /// internally compose their cache + fetcher (e.g. chat-auth's
    /// existing `SessionVersionCache` + `SessionVersionFetcher` pair) —
    /// the engine boundary sees a single port.
    #[must_use]
    pub fn with_epoch_revocation(mut self, port: Arc<dyn EpochRevocation>) -> Self {
        self.epoch = Some(port);
        self
    }
}