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
//! `VerifiedClaims` — opaque verified bearer-token outcome.
//!
//! Phase A audit decision G renamed the SDK's 4-field `AuthSession` →
//! `VerifiedClaims` to disambiguate from consumer-side AuthSession
//! types (chat-auth's 5-field rich `AuthSession`, RCW/CTW's 3-field
//! lean `BearerAuthSession`). The new name makes the SDK's role
//! explicit: this type carries *verified claims* projected from the
//! engine's typed payload; the consumer projects further into
//! whatever session shape its perimeter logic needs.

use time::OffsetDateTime;

use crate::types::{Ppnum, PpnumId, SessionId};

/// Verified bearer-token outcome, opaque to the underlying token format.
///
/// Internal storage is the engine's typed `Claims` payload, but no
/// consumer ever touches it — accessors return SDK-shaped types
/// (`Ppnum`, `PpnumId`, `SessionId`, `OffsetDateTime`) that are stable
/// across format migrations (PASETO → JWT just happened; future
/// formats re-implement [`super::BearerVerifier`] and ship a new
/// `VerifiedClaims` constructor).
///
/// No `into_inner` escape hatch by design (Phase 6.1 audit Finding 4):
/// every claim consumer code might need is exposed as a typed accessor.
/// If a future field is needed, add an accessor here before the consumer
/// ships — never widen to raw claims.
#[derive(Debug, Clone)]
pub struct VerifiedClaims {
    ppnum_id: PpnumId,
    ppnum: Option<Ppnum>,
    session_id: Option<SessionId>,
    expires_at: OffsetDateTime,
}

impl VerifiedClaims {
    /// Build from typed components. SDK-internal — [`super::JwtVerifier`]
    /// constructs after engine `verify` returns;
    /// [`super::MemoryBearerVerifier`] constructs in test setup. Marked
    /// `pub(crate)` so external adapters cannot fabricate verified
    /// claims outside the SDK's verification path.
    ///
    /// `dead_code` allowed because under just `feature = "token"` (no
    /// `well-known-fetch`, no `test-support`) the constructor has no
    /// caller — yet the type itself is still part of the
    /// [`super::BearerVerifier`] trait surface that consumers may
    /// implement directly. Removing the constructor would break the
    /// symmetry with [`Self::for_test`].
    #[allow(dead_code)]
    pub(crate) fn new(
        ppnum_id: PpnumId,
        ppnum: Option<Ppnum>,
        session_id: Option<SessionId>,
        expires_at: OffsetDateTime,
    ) -> Self {
        Self {
            ppnum_id,
            ppnum,
            session_id,
            expires_at,
        }
    }

    /// Test-support constructor — same shape as [`Self::new`] but
    /// available outside sdk-core when the `test-support` feature is
    /// enabled. Consumers writing integration tests wire in
    /// pre-built sessions through
    /// [`super::MemoryBearerVerifier::insert`].
    #[cfg(any(test, feature = "test-support"))]
    #[must_use]
    pub fn for_test(
        ppnum_id: PpnumId,
        ppnum: Option<Ppnum>,
        session_id: Option<SessionId>,
        expires_at: OffsetDateTime,
    ) -> Self {
        Self::new(ppnum_id, ppnum, session_id, expires_at)
    }

    /// Stable subject identifier (ULID, `sub` claim).
    #[must_use]
    pub fn ppnum_id(&self) -> &PpnumId {
        &self.ppnum_id
    }

    /// Digit-form ppnum carried in the `active_ppnum` claim. `None`
    /// for AI-agent / machine tokens that have no human ppnum, and
    /// for any token where the issuer omitted the claim. Consumer
    /// code defaults to display-only use; trust decisions key off
    /// `ppnum_id` (immutable ULID).
    #[must_use]
    pub fn ppnum(&self) -> Option<&Ppnum> {
        self.ppnum.as_ref()
    }

    /// Session row identifier (`sid` claim) when the issuer bound the
    /// token to a stored session. `None` for non-session-bound tokens
    /// (machine credentials, AI-agent flows, R6 legacy admit). When
    /// present, consumer middleware uses it for per-row liveness
    /// checks via `SessionStore::find`.
    #[must_use]
    pub fn session_id(&self) -> Option<&SessionId> {
        self.session_id.as_ref()
    }

    /// Expiry (`exp` claim) as a wall-clock instant. Caller code can
    /// compare against `OffsetDateTime::now_utc()` for soft-refresh
    /// logic; the engine has already enforced expiry in
    /// [`super::BearerVerifier::verify`] so this value is informational
    /// by the time it reaches consumer code.
    #[must_use]
    pub fn expires_at(&self) -> OffsetDateTime {
        self.expires_at
    }
}