feat: enforce confirmed offline idle deadlines (Closes #523)
CI / lint (pull_request) Successful in 1m15s
CI / build-release (pull_request) Successful in 6s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-11 00:00:19 +00:00
1 parent 100d38cc5c
commit b1890a29ff
8 files changed
+255 -34

No files matched your search

+9 -1
View File
@@ -30,6 +30,7 @@ class Session:
session_id: str
csrf: str
expires_at: int
idle_expires_at: int | None = None
@dataclass(frozen=True)
@@ -174,7 +175,14 @@ def verify_session_with_reason(
return SessionVerification(None, "session_idle")
if status != "active":
return SessionVerification(None, "session_revoked")
return SessionVerification(session)
return SessionVerification(
Session(
session_id=session.session_id,
csrf=session.csrf,
expires_at=session.expires_at,
idle_expires_at=getattr(status, "idle_expires_at", None),
)
)
def verify_session(value: str | None, now: int | None = None) -> Session | None:
+5 -1
View File
@@ -1512,6 +1512,7 @@ async def session_status(request: Request):
}
if session is not None:
payload["expires_at"] = session.expires_at
payload["idle_expires_at"] = session.idle_expires_at
return payload
@@ -1534,7 +1535,10 @@ async def record_session_activity(request: Request):
)
return {
"active": True,
"idle_expires_at": int(time.time()) + dashboard_auth.idle_timeout_seconds(),
"idle_expires_at": min(
session.expires_at,
int(time.time()) + dashboard_auth.idle_timeout_seconds(),
),
}
+16 -4
View File
@@ -12,6 +12,17 @@ class SessionStoreError(RuntimeError):
"""Raised when session state cannot be read or changed safely."""
class SessionStatus(str):
"""String-compatible status carrying the server-confirmed idle deadline."""
idle_expires_at: int | None
def __new__(cls, value: str, idle_expires_at: int | None = None):
instance = super().__new__(cls, value)
instance.idle_expires_at = idle_expires_at
return instance
@dataclass(frozen=True)
class ActiveDevice:
management_id: str
@@ -180,10 +191,11 @@ class SessionStore:
except (OSError, sqlite3.Error) as exc:
raise SessionStoreError("Session registry is temporarily unavailable") from exc
if row is None or row[0] != expires_at or expires_at <= now:
return "revoked"
if row[1] + max(1, idle_timeout_seconds) <= now:
return "idle"
return "active"
return SessionStatus("revoked")
idle_expires_at = min(row[0], row[1] + max(1, idle_timeout_seconds))
if idle_expires_at <= now:
return SessionStatus("idle", idle_expires_at)
return SessionStatus("active", idle_expires_at)
def touch(
self, session_id: str, expires_at: int, *, idle_timeout_seconds: int