ppoppo-sdk-core 0.1.0

Internal shared primitives for the Ppoppo SDK family (pas-external, pas-plims, pcs-external) — verifier port, audit trait, session liveness port, OIDC discovery, perimeter Bearer-auth Layer kit, identity types. Not a stable public API; do not depend on this crate directly. Consume the SDK crates that re-export from it (e.g. `pas-external`).
Documentation
//! `VerifyError` — verifier crypto-side failure surface.
//!
//! One variant per logical failure class. The PAS-engine variants
//! (`SignatureInvalid`, `Expired`, `IssuerInvalid`, `AudienceInvalid`,
//! `MissingClaim`, `KeysetUnavailable`) reflect the boundary contract:
//! audit logs map them 1:1 to engine `AuthError` rows. Adapter-side
//! variants (`InvalidFormat`) cover failures upstream of engine entry.

#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
pub enum VerifyError {
    /// Bearer string did not parse as a JWS Compact serialization.
    /// Adapter-side reject before engine entry.
    #[error("invalid bearer token format")]
    InvalidFormat,

    /// Cryptographic signature verification failed (engine M16).
    #[error("signature verification failed")]
    SignatureInvalid,

    /// `exp` claim is in the past (engine M19).
    #[error("token expired")]
    Expired,

    /// `iss` did not match `super::VerifyConfig::issuer` (engine M23). The
    /// engine does not expose the *actual* value because the failed
    /// match means we cannot trust any payload field — the SDK
    /// surfaces just "issuer invalid" and the audit log carries the
    /// caller's expected value alongside this variant.
    #[error("issuer invalid (M23)")]
    IssuerInvalid,

    /// `aud` did not match `super::VerifyConfig::audience` (engine M21/M22).
    #[error("audience invalid (M21/M22)")]
    AudienceInvalid,

    /// A required claim was absent or malformed.
    #[error("missing required claim: {0}")]
    MissingClaim(&'static str),

    /// JWKS fetch failed and there is no usable cached snapshot
    /// (initial bootstrap failure or `with_initial` constructed with
    /// an empty key set). Distinct from `SignatureInvalid` so audit
    /// logs distinguish "we couldn't even attempt verification" from
    /// "verification failed."
    #[error("keyset unavailable")]
    KeysetUnavailable,

    /// Engine `check_epoch` rejected: token's `sv` claim is below the
    /// authoritative substrate's current value. Break-glass /
    /// LogoutAll just kicked this token (RFC §3 Row 3 + STANDARDS_AUTH_INVALIDATION
    /// §2.3). Distinct from `Expired` (which is `exp`-bound) — the
    /// caller's UX response is the same (re-authenticate) but audit
    /// logs distinguish revocation from natural expiry.
    ///
    /// Surfaces only when the consumer wired
    /// `JwtVerifier::with_epoch_revocation` at boot. With no port
    /// wired, the engine's epoch gate short-circuits and this variant
    /// is unreachable.
    #[error("session_version stale (engine epoch port reject)")]
    SessionVersionStale,

    /// Engine `check_epoch` could not reach its substrate (cache miss
    /// fell through to fetcher; fetcher returned transient error).
    /// Fail-closed per STANDARDS_AUTH_INVALIDATION §3 — admit-on-
    /// failure would let stale tokens slip during outage windows.
    /// Caller's HTTP response should be `503 Service Unavailable`.
    ///
    /// Surfaces only when the consumer wired
    /// `JwtVerifier::with_epoch_revocation` at boot.
    #[error("session_version lookup substrate unavailable")]
    SessionVersionLookupUnavailable,

    /// Phase 11.Z 0.10.0 (RFC_2026-05-08 §4.2 lock) — L2 session
    /// liveness reject. The token's `sid` claim resolved to a row that
    /// is absent OR `revoked_at` is set. Distinct from
    /// `SessionVersionStale` (L1 sv-axis): L2 is consumer-DB row
    /// revocation; L1 is cross-service break-glass propagation.
    /// Caller's HTTP response is `401` and the browser cookie clears
    /// (the `LogoutAll`/per-session-revoke flow's actionable signal).
    ///
    /// Surfaces only when the consumer wired
    /// `JwtVerifier::with_session_liveness` at boot AND the token
    /// carries a `sid` claim. Tokens without `sid` (machine
    /// credentials, AI-agent flows, R6 legacy admit per
    /// `super::VerifiedClaims::session_id`) admit without consulting
    /// the L2 port (lenient — RFC_2026-05-08 §4.2 lock).
    #[error("session revoked")]
    SessionRevoked,

    /// Phase 11.Z 0.10.0 — L2 session liveness substrate could not
    /// answer (DB connection lost, schema unavailable, query timeout).
    /// Fail-closed per STANDARDS_AUTH_INVALIDATION §3 — admit-on-
    /// failure would let post-revoke tokens slip through during outage
    /// windows. Caller's HTTP response should be `503 Service
    /// Unavailable`. Distinct from `SessionVersionLookupUnavailable`
    /// (L1) so audit dashboards pivot L1 substrate health vs L2
    /// substrate health independently.
    ///
    /// Surfaces only when the consumer wired
    /// `JwtVerifier::with_session_liveness` at boot.
    #[error("session liveness lookup substrate unavailable")]
    SessionLivenessLookupUnavailable,

    /// M73 — id_token presented as a Bearer token. RFC 9068 §1 (negative)
    /// + OIDC Core §1.2 intent: id_tokens authenticate the user *to the
    /// RP*; access_tokens authorize the RP *to the resource server*.
    /// The two are not interchangeable. Many 3rd-party RPs misuse
    /// id_token for API access — the SDK's BearerVerifier surface is for
    /// resource servers, so an id_token-shaped JWT here is always wrong.
    ///
    /// Distinct from `SignatureInvalid` (which is the engine's catch-all
    /// for "token cannot be trusted") so audit logs distinguish a
    /// developer-misuse signal ("you're sending the wrong token class")
    /// from a forgery signal ("the signature didn't verify"). Rejected
    /// BEFORE engine entry so the audit log does not get the same
    /// signal masked by `TypMismatch → SignatureInvalid` collapsing.
    #[error("M73: id_token presented as Bearer — use access_token for resource access")]
    IdTokenAsBearer,

    /// Catch-all for engine variants that don't map to a structural
    /// SDK rejection. Carries the engine's `AuthError` Display so the
    /// audit log retains the M-code.
    #[error("verification failed: {0}")]
    Other(String),
}