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
//! Verification errors for the RFC 9068 access-token profile.
//!
//! ── Surface composition ─────────────────────────────────────────────────
//!
//! JOSE wire-format errors (M01-M16a algorithm/header, M31-M34
//! serialization, parse-shape failures) live in
//! `engine::SharedAuthError` and reach this enum via the
//! [`AuthError::Jose`] carrier variant — the engine submodules emit
//! `SharedAuthError`, the access-token verify path re-wraps. This is a
//! deliberate seam: it lets each profile (access vs OIDC id_token)
//! enumerate only its profile-specific variants while sharing the JOSE
//! check pipeline structurally.
//!
//! Profile-specific variants enumerated here:
//! * **C. Claims (M17-M30)** — RFC 9068 / 7519 registered-claim
//!   semantics. Includes `cat` (M29) and `client_id` (M28a) which OIDC
//!   id_tokens do not carry.
//! * **F. Domain (M39-M45)** — ppoppo-specific domain rules.
//! * **E. Replay / Revocation (M35-M38 + sv)** — Phase 5 ports.
//!
//! Variants map 1:1 to mitigation IDs in
//! `0context/STANDARDS_JWT_DETAILS_MITIGATION_PPOPPO.md` §C, §F, §E.
//! Adding a mitigation row here without a variant — or moving a variant
//! between this enum and `SharedAuthError` — is a drift signal for the
//! Standards-Sync table.

use crate::engine::shared_error::SharedAuthError;

#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
pub enum AuthError {
    /// JOSE-shared wire-format errors (algorithm / header / serialization
    /// / parse). The engine submodules `check_algorithm` / `check_header`
    /// / `raw` and the structural rejections in `engine::verify` all
    /// surface through this carrier.
    #[error(transparent)]
    Jose(#[from] SharedAuthError),

    // ── C. Claims (M17–M30) ───────────────────────────────────────────────
    #[error("M17: exp claim missing")]
    ExpMissing,
    #[error("M18: token expired")]
    Expired,
    #[error("M19: exp exceeds upper bound")]
    ExpUpperBound,
    #[error("M20: aud claim missing")]
    AudMissing,
    /// M21 + M22 collapsed (string vs array form) — the audit signal is
    /// identical: the audience binding does not match this verifier.
    #[error("M21/M22: aud value does not match expected audience")]
    AudMismatch,
    /// M23: `iss` is missing OR does not match the pinned issuer.
    /// Single variant — the audit signal is "this token did not come
    /// from the trusted issuer".
    #[error("M23: iss missing or does not match pinned issuer")]
    IssMismatch,
    #[error("M24: iat claim missing")]
    IatMissing,
    /// M24 (future-iat) + M25 collapsed — both enforce `iat > now + 60s`.
    #[error("M24/M25: iat is in the future beyond 60s leeway")]
    IatFuture,
    #[error("M26: nbf is in the future — token not yet valid")]
    NotYetValid,
    #[error("M27: jti claim missing")]
    JtiMissing,
    #[error("M28: sub claim missing")]
    SubMissing,
    /// M28a: RFC 9068 §2.2 — access JWTs MUST carry `client_id`.
    #[error("M28a: client_id claim missing")]
    ClientIdMissing,
    /// M29: `cat` (token category) discriminator. RFC 9068 §2.2 + ppoppo
    /// extension.
    #[error("M29: cat does not match expected token type")]
    TokenTypeMismatch,
    /// M30: numeric claim is not a JSON integer.
    #[error("M30: numeric claim is not a JSON integer")]
    InvalidNumericType,

    // ── F. Domain (M39–M45) ───────────────────────────────────────────────
    #[error("M39: sub is not a valid ULID")]
    SubFormatInvalid,
    #[error("M40: account_type outside whitelist")]
    AccountTypeInvalid,
    #[error("M41: caps is not a JSON array of strings")]
    CapsShapeInvalid,
    #[error("M42: scopes is not a JSON array of strings")]
    ScopesShapeInvalid,
    #[error("M42: scopes exceeds 256-entry cap")]
    ScopesTooLong,
    #[error("M43: dlg_depth invalid (non-integer, negative, or > 4)")]
    DlgDepthInvalid,
    #[error("M44: admin claim requires active_ppnum in admin band")]
    AdminBandRejected,
    /// M45: payload contains a claim outside the engine's strict
    /// allowlist. The variant carries the offending claim name for audit.
    #[error("M45: unknown claim '{0}'")]
    UnknownClaim(String),

    // ── E. Replay / revocation (M35–M38 + sv) — Phase 5 ────────────────────
    #[error("M35: jti replayed within TTL")]
    JtiReplayed,
    #[error("M35: replay cache substrate unavailable")]
    ReplayCacheUnavailable,
    #[error("M36: session revoked (user_sessions row absent)")]
    SessionRevoked,
    #[error("M36: session lookup substrate unavailable")]
    SessionLookupUnavailable,
    #[error("session_version stale: token < current epoch")]
    SessionVersionStale,
    #[error("session_version lookup substrate unavailable")]
    SessionVersionLookupUnavailable,
}