pub trait SessionLiveness:
Debug
+ Send
+ Sync {
// Required method
fn check<'life0, 'life1, 'async_trait>(
&'life0 self,
sid: &'life1 SessionId,
) -> Pin<Box<dyn Future<Output = Result<(), SessionLivenessError>> + Send + 'async_trait>>
where 'life0: 'async_trait,
'life1: 'async_trait,
Self: 'async_trait;
}Expand description
Per-request session-row liveness check.
Wired into [crate::PasJwtVerifier::with_session_liveness] as a
verifier slot symmetric to
[crate::PasJwtVerifier::with_epoch_revocation]. With no port wired,
the verifier short-circuits the L2 check (matches pre-0.10.0
behavior).
§3-state contract
Ok(())→ session is live; admit the token.Err(SessionLivenessError::Revoked)→ session row absent ORrevoked_atis set; the verifier maps tocrate::VerifyError::SessionRevoked. Actionable forLogoutAll/ per-session-revoke flows.Err(SessionLivenessError::Transient)→ substrate down (DB connection lost, schema unavailable, etc.); the verifier maps tocrate::VerifyError::SessionLivenessLookupUnavailable(HTTP 503). Fail-closed perSTANDARDS_AUTH_INVALIDATION§3.
§Lenient on no-sid claim
When the bearer’s sid claim is None (machine credentials,
AI-agent flows, R6 legacy admit per
[crate::AuthSession::session_id]), the verifier admits without
consulting this port — non-session-bound tokens have no row to look
up. RFC_2026-05-08 §4.2 lock decision (lenient — matches the existing
AuthSession::session_id invariant).
§Implementations
Consumer-side adapters: RCW ships PgSessionLiveness over
scrcall.user_sessions; CTW ships the same shape over
scctime.user_sessions. Each is ~10 lines of consumer-local
code — schema name + DB pool are deployment-specific and never
shipped from the SDK.
use async_trait::async_trait;
use pas_external::session_liveness::{SessionLiveness, SessionLivenessError};
use pas_external::types::SessionId;
use sqlx::PgPool;
pub struct PgSessionLiveness { pool: PgPool }
#[async_trait]
impl SessionLiveness for PgSessionLiveness {
async fn check(&self, sid: &SessionId) -> Result<(), SessionLivenessError> {
let row: Option<(Option<time::OffsetDateTime>,)> =
sqlx::query_as("SELECT revoked_at FROM scrcall.user_sessions WHERE id = $1")
.bind(&sid.0)
.fetch_optional(&self.pool)
.await
.map_err(|e| SessionLivenessError::Transient(format!("session lookup: {e}")))?;
match row {
None | Some((Some(_),)) => Err(SessionLivenessError::Revoked),
Some((None,)) => Ok(()),
}
}
}