1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//! M36 — session-row liveness port (RFC_2026-05-04_jwt-full-adoption Phase 5).
//!
//! The *textbook* revocation gate: even with valid signature +
//! non-expired + non-replayed, the verifier checks `user_sessions(sub,
//! sid)` and refuses if the row is gone. STANDARDS_JWT_DETAILS_MITIGATION
//! §E M36 — "Row deletion = revocation. This makes the system **stateful
//! by design** — the OVERVIEW §6 note 'stateless 환상 폐기' lives here."
//!
//! ── Why a third axis (vs sv epoch + jti replay) ─────────────────────────
//!
//! sv (`EpochRevocation`) bumps account-wide on break-glass / LogoutAll
//! — it cannot kick a single device while leaving siblings alive. M35
//! (`ReplayDefense`) defends against re-presenting the same token —
//! it does not invalidate other tokens. M36 (`SessionRevocation`) is
//! the per-session axis: "this device's session row was deleted, this
//! token must die, all the user's other sessions stay alive". Single
//! "Sign out this device" UX maps to LogoutSession primitive in
//! STANDARDS_AUTH_INVALIDATION §2.1, which deletes only the target
//! row.
//!
//! ── Failure-mode contract (fail-closed) ────────────────────────────────
//!
//! `is_active` returns `Ok(true)` for live sessions, `Ok(false)` for
//! deleted/missing rows (= revoked), `Err(Transient)` for substrate
//! failure. Engine maps `Ok(false)` → `AuthError::SessionRevoked`
//! and `Err(Transient)` → `AuthError::SessionLookupUnavailable`. As
//! with `ReplayDefense`, the two are distinct audit signals — admitting
//! on substrate failure would let revoked sessions persist past their
//! invalidation window.
//!
//! ── Phase 10 split (RFC §6.11) ──────────────────────────────────────────
//!
//! Moves to `access_token::SessionRevocation` in Phase 10 D1. id_token
//! is not a bearer credential — it has no session row to check.
/// Per-session liveness check.
///
/// `sub` is the subject claim (ppnum_id ULID); `sid` is the session
/// row id from `Claims.sid`. When `Claims.sid` is `None`, the engine
/// skips this gate (legacy / non-session-bound tokens admit) — this
/// trait is never called with an empty `sid`.