diff --git a/lib/rspace-header.ts b/lib/rspace-header.ts index 9ea66b2..cfe0f8a 100644 --- a/lib/rspace-header.ts +++ b/lib/rspace-header.ts @@ -47,7 +47,22 @@ function getSession(): SessionState | null { } } + function clearSession(): void { + // Notify server so all other browser sessions get revoked + try { + const stored = localStorage.getItem(SESSION_KEY); + if (stored) { + const session = JSON.parse(stored) as SessionState; + if (session.accessToken) { + fetch(`${ENCRYPTID_URL}/api/session/logout`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${session.accessToken}` }, + }).catch(() => { /* best-effort */ }); + } + } + } catch { /* ignore */ } + localStorage.removeItem(SESSION_KEY); localStorage.removeItem('rspace-username'); } @@ -921,6 +936,24 @@ export function mountHeader(options: HeaderOptions): void { } } catch { /* silently ignore */ } })(); + + // Validate session with server — detects logout from another browser session + (async () => { + const session = getSession(); + if (!session) return; + try { + const res = await fetch(`${ENCRYPTID_URL}/api/session/verify`, { + headers: { 'Authorization': `Bearer ${session.accessToken}` }, + }); + if (!res.ok) { + // Session revoked or invalid — clear locally and re-render + localStorage.removeItem(SESSION_KEY); + localStorage.removeItem('rspace-username'); + renderHeader(); + onAuthChange?.(); + } + } catch { /* network error — let token expire naturally */ } + })(); } /**