ppoppo_token/access_token/epoch_revocation.rs
1//! Per-account `session_version` epoch revocation port
2//! (RFC_2026-05-04_jwt-full-adoption Phase 5 — sv-port).
3//!
4//! Account-wide revocation axis: token's `sv` claim must be `>=` the
5//! substrate's current value. break-glass and `LogoutAll` bump the
6//! current value, invalidating every prior token within the substrate's
7//! TTL window. STANDARDS_AUTH_PPOPPO §17.7 + STANDARDS_AUTH_INVALIDATION
8//! §2.3.
9//!
10//! ── Deep module — composition lives behind the port ────────────────────
11//!
12//! chat-auth's existing two-trait split (`SessionVersionCache` +
13//! `SessionVersionFetcher`) — best-effort cache layer + authoritative
14//! fetcher — is a *production-validated* decomposition. It moves into
15//! the implementation of this port (an adapter that internally composes
16//! cache + fetcher with fail-closed fall-through), NOT onto the engine
17//! boundary. The engine sees ONE port; impls decide their own internal
18//! structure (chat-auth: cache + DB fetcher; pas-external: cache +
19//! HTTP-userinfo fetcher; in-memory: HashMap).
20//!
21//! This is the deep-module win: the engine's contract is "give me the
22//! current epoch for this subject"; substrates with different latency /
23//! consistency profiles each compose their own internal stack and
24//! satisfy the same trait.
25//!
26//! ── Failure-mode contract (fail-closed) ────────────────────────────────
27//!
28//! Returns `Ok(i64)` always when a value can be determined — adapters
29//! choose the genesis convention (typical: 0 means "no break-glass yet"
30//! and `claims.sv = None` legacy-admits anyway). `Err(Transient)`
31//! signals substrate failure; engine maps to
32//! `AuthError::SessionVersionLookupUnavailable` and refuses the token.
33//!
34//! ── Phase 10 split (RFC §6.11) ──────────────────────────────────────────
35//!
36//! Moves to `access_token::EpochRevocation` in Phase 10 D1. id_token
37//! does not carry an `sv` claim — break-glass invalidates bearer
38//! credentials, not the assertion-of-authentication that id_token
39//! represents.
40
41/// Current per-account `session_version` lookup.
42///
43/// `sub` is the subject claim (ppnum_id ULID for human paths). For
44/// non-human subjects (AI agent, client_credentials), the engine never
45/// calls this — `claims.sv` is `None` on those paths and the gate
46/// short-circuits.
47#[async_trait::async_trait]
48pub trait EpochRevocation: std::fmt::Debug + Send + Sync {
49 async fn current(&self, sub: &str) -> Result<i64, EpochRevocationError>;
50}
51
52#[derive(Debug, thiserror::Error)]
53pub enum EpochRevocationError {
54 /// Substrate composition failure (cache miss + fetcher error,
55 /// HTTP userinfo timeout, etc.). Engine maps to
56 /// `AuthError::SessionVersionLookupUnavailable`.
57 #[error("session_version lookup transient failure: {0}")]
58 Transient(String),
59}