Skip to main content

ppoppo_token/access_token/
claims.rs

1//! Verified claim payload returned by the JWT engine after `verify`.
2//!
3//! Surface discipline (Phase 2 Decision 1, extended Phase 4):
4//!
5//! - **Hidden** — claims the engine fully resolves with no caller
6//!   participation:
7//!     - `aud` (M20-M22 validates against `cfg.audience`)
8//!     - `cat` (M29 validates against `cfg.expected_cat`)
9//!     - `dlg_depth` (M43 validates ≤ 4)
10//!     - `sv` (Phase 5 cache compares against `sv:{sub}`)
11//! - **Surfaced** — claims callers legitimately need post-verify:
12//!     - `client_id` (M28a — audit logs, per-client rate limits)
13//!     - `account_type` (M40 — admin gate code reads it)
14//!     - `caps`, `scopes` (M41/M42 — capability check is post-verify)
15//!     - `delegator` (Token Exchange audit logs)
16//!     - `cid` (forensic / selective-session-kill)
17//!     - `active_ppnum` (UI display)
18//!     - `admin` (admin RPC handlers gate on it)
19//!
20//! Surfacing wrongly is a forward-compat tax (every caller must handle
21//! the new field). Hiding wrongly leaves callers without info they need.
22//! When in doubt, hide — adding later is cheap; removing later is a
23//! breaking change.
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Claims {
27    pub iss: String,
28    pub sub: String,
29    pub exp: i64,
30    pub iat: i64,
31    pub nbf: Option<i64>,
32    pub jti: String,
33    pub client_id: String,
34
35    // ── Phase 4 surfaced domain claims (M40+) ───────────────────────────
36    /// `account_type` (M40) — `"human"` | `"ai_agent"` | `"programmable"` |
37    /// `None` (legacy admit). Whitelist enforced verifier-side; arbitrary
38    /// strings are rejected with `AuthError::AccountTypeInvalid` before this
39    /// struct is constructed. `"programmable"` = External Developer app
40    /// client_credentials token.
41    pub account_type: Option<String>,
42
43    /// `caps` (M41) — capability list. Empty when absent or empty-array
44    /// on the wire (the engine collapses both to the same surface so
45    /// callers' default-deny check is `caps.contains(&"x")`). Wire-shape
46    /// validation (must be a JSON array of strings) lives in
47    /// `engine::check_domain`; semantic interpretation of each
48    /// capability string is per-surface (PAS, PCS, RCW each own their
49    /// vocabulary).
50    pub caps: Vec<String>,
51
52    /// `scopes` (M42) — OAuth scope list. Empty when absent or empty-
53    /// array (same collapse as `caps`). Engine bounds the array length
54    /// at ≤ 256; entries beyond that are a forgery / misconfiguration
55    /// signal that pessimizes per-request scope checks. Conceptually
56    /// distinct from `caps` (scopes are externally granted via OAuth;
57    /// caps are internally minted by PAS), so the surfacing is duplicated
58    /// rather than unified — collapsing them would force callers to
59    /// untangle two authorization vectors at every check site.
60    pub scopes: Vec<String>,
61
62    /// `admin` (M44) — token claims admin authority. **Admin authority
63    /// is DB-determined** (STANDARDS_AUTH_PPOPPO §3.2: `is_admin = TRUE
64    /// AND lifecycle_state = 'active'` AND active passkey ≥ 1). This
65    /// claim is the *fast pre-flight signal* — when `admin == true`,
66    /// the engine has already proven `active_ppnum` falls in the admin
67    /// band (defense in depth against stolen-signing-key forgeries).
68    /// Callers MUST still call the DB-side `is_admin` invariant — this
69    /// flag tells them whether to even bother.
70    pub admin: bool,
71    /// `active_ppnum` (M44 + UI display) — the digit-form ppnum the
72    /// session is currently active under. UI surfaces render this;
73    /// `sub` (ULID) is the immutable authorization axis. Engine reads
74    /// it for the M44 admin-band check and surfaces it unchanged.
75    pub active_ppnum: Option<String>,
76
77    /// `delegator` — Token Exchange chain's delegating principal
78    /// `ppnum_id`. Surfaced for audit logs (which human authorized the
79    /// delegated session). `None` for tokens that aren't part of a
80    /// chain. Wire name is `delegator` (the matrix's earlier `actor`
81    /// was retired — RFC 8693 reserves `actor` for token-exchange
82    /// chain semantics that don't apply here).
83    pub delegator: Option<String>,
84
85    /// `cid` — WebAuthn credential id that authenticated this session
86    /// (passkey path only). Surfaces for forensic provenance and
87    /// future selective-session-kill flows. `None` on every non-
88    /// passkey path so audit logs distinguish authentication methods
89    /// without a per-row lookup.
90    pub cid: Option<String>,
91
92    /// `sid` (M36) — session row id (`user_sessions.session_id`). When
93    /// present, the engine queries the substrate via
94    /// `cfg.session_revocation.is_active(sub, sid)` and refuses if the
95    /// row is absent (STANDARDS_JWT_DETAILS_MITIGATION §E "row deletion
96    /// = revocation"). `None` on machine tokens / AI-agent flows that
97    /// have no session row to check; engine short-circuits the gate
98    /// when `None` so non-session-bound tokens admit (legacy /
99    /// pre-Phase-5 tokens included).
100    pub sid: Option<String>,
101}