# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/what-invariants-bound-session-token-scope-to-the-permission-revocation-window.md
# Remnant Warrant:    https://corpusfetch.com/c/what-invariants-bound-session-token-scope-to-the-permission-revocation-window.warrant.json
# Markdown SHA-256:   f2e0287ac0c9d22b3282998878a339f0a0bf5503e0713e6ee84c3c0a21d1f616
# Code AST SHA-256:   87accd1db3b788278e6a094af84e26dbbfd7a03c76bddf6ab8b4cdac5b7296e0
# Verified Classes:   StaleGrantError, SessionTokenGate
# Defensive Guards:   1 fail-closed invariants (zero external deps)

from dataclasses import dataclass


class StaleGrantError(RuntimeError):
    """Raised when a token's TTL exceeds the revocation propagation window and no per-request check exists."""


@dataclass(frozen=True)
class SessionTokenGate:
    token_ttl_seconds: int
    revocation_propagation_seconds: int
    per_request_revocation_check: bool
    rotate_on_permission_change: bool = False

    def verify(self) -> bool:
        if self.per_request_revocation_check or self.rotate_on_permission_change:
            return True
        if self.token_ttl_seconds > self.revocation_propagation_seconds:
            raise StaleGrantError(
                f"token TTL {self.token_ttl_seconds}s > revocation propagation "
                f"{self.revocation_propagation_seconds}s: demoted principal retains permissions"
            )
        return True


def dispatch_token_mode(gate: SessionTokenGate) -> str:
    try:
        gate.verify()
    except StaleGrantError:
        return "check-revocation-per-request"
    return "token-authoritative"
