ppoppo_sdk_core/verifier/claims.rs
1//! `VerifiedClaims` — opaque verified bearer-token outcome.
2//!
3//! Phase A audit decision G renamed the SDK's 4-field `AuthSession` →
4//! `VerifiedClaims` to disambiguate from consumer-side AuthSession
5//! types (chat-auth's 5-field rich `AuthSession`, RCW/CTW's 3-field
6//! lean `BearerAuthSession`). The new name makes the SDK's role
7//! explicit: this type carries *verified claims* projected from the
8//! engine's typed payload; the consumer projects further into
9//! whatever session shape its perimeter logic needs.
10
11use time::OffsetDateTime;
12
13use crate::types::{Ppnum, PpnumId, SessionId};
14
15/// Verified bearer-token outcome, opaque to the underlying token format.
16///
17/// Internal storage is the engine's typed `Claims` payload, but no
18/// consumer ever touches it — accessors return SDK-shaped types
19/// (`Ppnum`, `PpnumId`, `SessionId`, `OffsetDateTime`) that are stable
20/// across format migrations (PASETO → JWT just happened; future
21/// formats re-implement [`super::BearerVerifier`] and ship a new
22/// `VerifiedClaims` constructor).
23///
24/// No `into_inner` escape hatch by design (Phase 6.1 audit Finding 4):
25/// every claim consumer code might need is exposed as a typed accessor.
26/// If a future field is needed, add an accessor here before the consumer
27/// ships — never widen to raw claims.
28#[derive(Debug, Clone)]
29pub struct VerifiedClaims {
30 ppnum_id: PpnumId,
31 ppnum: Option<Ppnum>,
32 session_id: Option<SessionId>,
33 expires_at: OffsetDateTime,
34}
35
36impl VerifiedClaims {
37 /// Build from typed components. SDK-internal — [`super::JwtVerifier`]
38 /// constructs after engine `verify` returns;
39 /// [`super::MemoryBearerVerifier`] constructs in test setup. Marked
40 /// `pub(crate)` so external adapters cannot fabricate verified
41 /// claims outside the SDK's verification path.
42 ///
43 /// `dead_code` allowed because under just `feature = "token"` (no
44 /// `well-known-fetch`, no `test-support`) the constructor has no
45 /// caller — yet the type itself is still part of the
46 /// [`super::BearerVerifier`] trait surface that consumers may
47 /// implement directly. Removing the constructor would break the
48 /// symmetry with [`Self::for_test`].
49 #[allow(dead_code)]
50 pub(crate) fn new(
51 ppnum_id: PpnumId,
52 ppnum: Option<Ppnum>,
53 session_id: Option<SessionId>,
54 expires_at: OffsetDateTime,
55 ) -> Self {
56 Self {
57 ppnum_id,
58 ppnum,
59 session_id,
60 expires_at,
61 }
62 }
63
64 /// Test-support constructor — same shape as [`Self::new`] but
65 /// available outside sdk-core when the `test-support` feature is
66 /// enabled. Consumers writing integration tests wire in
67 /// pre-built sessions through
68 /// [`super::MemoryBearerVerifier::insert`].
69 #[cfg(any(test, feature = "test-support"))]
70 #[must_use]
71 pub fn for_test(
72 ppnum_id: PpnumId,
73 ppnum: Option<Ppnum>,
74 session_id: Option<SessionId>,
75 expires_at: OffsetDateTime,
76 ) -> Self {
77 Self::new(ppnum_id, ppnum, session_id, expires_at)
78 }
79
80 /// Stable subject identifier (ULID, `sub` claim).
81 #[must_use]
82 pub fn ppnum_id(&self) -> &PpnumId {
83 &self.ppnum_id
84 }
85
86 /// Digit-form ppnum carried in the `active_ppnum` claim. `None`
87 /// for AI-agent / machine tokens that have no human ppnum, and
88 /// for any token where the issuer omitted the claim. Consumer
89 /// code defaults to display-only use; trust decisions key off
90 /// `ppnum_id` (immutable ULID).
91 #[must_use]
92 pub fn ppnum(&self) -> Option<&Ppnum> {
93 self.ppnum.as_ref()
94 }
95
96 /// Session row identifier (`sid` claim) when the issuer bound the
97 /// token to a stored session. `None` for non-session-bound tokens
98 /// (machine credentials, AI-agent flows, R6 legacy admit). When
99 /// present, consumer middleware uses it for per-row liveness
100 /// checks via `SessionStore::find`.
101 #[must_use]
102 pub fn session_id(&self) -> Option<&SessionId> {
103 self.session_id.as_ref()
104 }
105
106 /// Expiry (`exp` claim) as a wall-clock instant. Caller code can
107 /// compare against `OffsetDateTime::now_utc()` for soft-refresh
108 /// logic; the engine has already enforced expiry in
109 /// [`super::BearerVerifier::verify`] so this value is informational
110 /// by the time it reaches consumer code.
111 #[must_use]
112 pub fn expires_at(&self) -> OffsetDateTime {
113 self.expires_at
114 }
115}