From 73ad8e4eb566fe682107fa7e50ef624591199e99 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 15 Sep 2026 02:16:39 +0200 Subject: feat(hub): session lifetime is an admin setting, and a browser signs out when idle Browser idle sign-out (media playback counts as activity; not the desktop app), refresh idle window and maximum session length, in hours. Sign-out now revokes on the hub, and the profile has "sign out everywhere". Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XuNrwLf5EFWCMHzfoEvnpm --- .../meshbay-hub/src/meshbay_hub/hub_settings.py | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) (limited to 'packages/meshbay-hub/src/meshbay_hub/hub_settings.py') diff --git a/packages/meshbay-hub/src/meshbay_hub/hub_settings.py b/packages/meshbay-hub/src/meshbay_hub/hub_settings.py index 1dcff84..c397f58 100644 --- a/packages/meshbay-hub/src/meshbay_hub/hub_settings.py +++ b/packages/meshbay-hub/src/meshbay_hub/hub_settings.py @@ -116,6 +116,46 @@ async def login_limits(db: AsyncSession) -> dict[str, int]: for k in LOGIN_KEYS} +# ── Session lifetime ───────────────────────────────────────────────────────── +# +# `browser_idle_hours`: a browser tab signs itself out after this long with no +# input and nothing playing (`static/idle.js`). The hub cannot measure that — it +# hears a renewal from any open tab, attended or not — so the page does, and +# reads the number from `/v1/hub/info`. The desktop application is exempt: it is +# its owner's machine and signs back in with its device key. +# +# `refresh_idle_hours`: a refresh token unused for this long stops renewing. The +# hub's own backstop, for a token that left the browser it was issued to. +# +# `max_hours`: no session renews past this since its sign-in, used or not. + +SESSION_KEYS = ("browser_idle_hours", "refresh_idle_hours", "max_hours") + +SESSION_DEFAULTS: dict[str, int] = { + "browser_idle_hours": 1, + "refresh_idle_hours": 24, + "max_hours": 720, +} + +SESSION_BOUNDS: dict[str, tuple[int, int]] = { + "browser_idle_hours": (1, 168), # a week + "refresh_idle_hours": (1, 720), # 30 days + "max_hours": (1, 8_760), # a year +} + + +def clamp_session_value(key: str, value: int) -> int: + low, high = SESSION_BOUNDS[key] + return max(low, min(high, int(value))) + + +async def session_limits(db: AsyncSession) -> dict[str, int]: + """The three session numbers, stored value or built-in default.""" + return {k: clamp_session_value( + k, await get_int(db, f"session.{k}", SESSION_DEFAULTS[k])) + for k in SESSION_KEYS} + + async def get_raw(db: AsyncSession, key: str) -> str | None: row = await db.get(HubSetting, key) return row.value if row else None -- cgit v1.2.3