ppoppo-token 0.2.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
//! Issuance errors for the OIDC id_token engine.
//!
//! Mirror of `access_token::IssueError` shape: 1 variant per named failure
//! mode so audit logs read the cause off the variant name without a lookup
//! table (see `project_jwt_phase2_design` Decision 2). Variants are
//! disjoint from the access-token enum because the failure modes don't
//! overlap (id_token has no `KeyParse` because key construction lives in
//! `crate::SigningKey` shared between profiles; the access-token enum
//! retains it for legacy reasons).
//!
//! ── Why a separate enum from `access_token::IssueError` ─────────────────
//!
//! Same reasoning as `id_token::AuthError` vs `access_token::AuthError`:
//! collapsing both into one enum forces every variant to carry "applies
//! to which profile?" metadata, when the carrying enum's *type* already
//! tells the reader which profile rejected. Profile-disjoint enums let
//! each surface stay narrow (only id-token-specific failure modes
//! enumerated here) while reusing the shared engine primitives
//! (`SigningKey`, `KeySet`, `Algorithm`).

/// id_token-specific issuance failure modes (Phase 10.10).
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum IssueError {
    /// `IssueConfig.kid` does not match the kid associated with the
    /// supplied `SigningKey`. Emitted before any encoding work happens
    /// so a misconfigured pipeline fails closed instead of issuing
    /// id_tokens against an unrecognized kid. Mirrors
    /// `access_token::IssueError::KeyMismatch` exactly — the engine's
    /// kid-matching pre-flight is profile-agnostic, but each profile
    /// owns its own variant so audit logs don't have to disambiguate
    /// "kid mismatch on which profile".
    #[error("issue: cfg kid '{cfg_kid}' does not match signer kid '{signer_kid}'")]
    KeyMismatch { cfg_kid: String, signer_kid: String },

    /// `jsonwebtoken::encode` failed at the JSON serialization step. In
    /// practice this only fires when an `IssueRequest<S>` field overflows
    /// the JSON number range — the registered claims are all integers
    /// within `i64`, so production paths cannot hit it. Listed for
    /// completeness so the engine never panics on serialization.
    #[error("issue: failed to encode id_token JWT ({0})")]
    JsonEncode(String),

    /// System clock is before UNIX_EPOCH. Cannot happen on a correctly
    /// configured machine; surfaces only on hardware-reset / NTP-broken
    /// edge cases. Listed for completeness so the engine refuses to
    /// emit garbage timestamps rather than panicking.
    #[error("issue: system clock is before UNIX_EPOCH")]
    ClockBackwards,

    /// β1 runtime allowlist guard (Phase 10.10) — `IssueRequest<S>`
    /// carries a populated PII field whose wire name is NOT in
    /// `S::names()`. The HasX-gated builders prevent this at the *typed*
    /// API surface (calling `.with_email(...)` on an `IssueRequest<Openid>`
    /// is a compile error); this variant is the *runtime* mirror —
    /// fires when intra-crate code bypasses the builders via
    /// `pub(crate)` struct-literal access and pushes a PII field through
    /// anyway. Symmetric to verify-side M72 (`AuthError::UnknownClaim`)
    /// — the engine refuses to *emit* a claim it would refuse to
    /// *accept*. Carries the offending wire name so audit logs see WHICH
    /// claim tripped the gate.
    #[error("issue: emission disallowed for claim '{0}' at this scope")]
    EmissionDisallowed(String),
}