// The account a browser TAB is acting as (per-tab, in sessionStorage — see api.ts). Its OWN module // (imports nothing internal) so it can be shared by `storage.ts`, which keys per-account localStorage // on it, without a circular import — the account id is a lower-level primitive than the storage // helpers that consume it. // // Deliberately NOT reactive: BootGate pins the account before any provider mounts, and a switch reloads // the page (handing the new pin over through `pinOnNextPage`), so nothing mounted ever sees the id change // underneath it. The one exception is state read at // module import (the notification and hints stores), which in a new tab runs before the pin: it reads // again through `onAccountPinned`. // // ACTIVE_ACCOUNT_KEY is duplicated here rather than imported from api.ts so this stays dependency-free. export const ACTIVE_ACCOUNT_KEY = "siftlode.activeAccount"; // Re-readers for module-level per-account state, run each time the tab is pinned. const pinListeners = new Set<() => void>(); export function onAccountPinned(reload: () => void): void { pinListeners.add(reload); } export function pinActiveAccount(id: number): void { try { sessionStorage.setItem(ACTIVE_ACCOUNT_KEY, String(id)); } catch { /* ignore */ } pinListeners.forEach((reload) => reload()); } // The account the tab's NEXT page is pinned to, handed over by a switch or sign-out that is about to leave. // The pin itself stays until the page is gone: the unload still runs as the account the page was built for // (the players' resume beacons read the pin as they fire), and with the pin already moved it would save one // account's position into the other. const NEXT_ACCOUNT_KEY = "siftlode.nextAccount"; /** Pin the tab's next page to `id` — or to none, so it adopts the session's default account. */ export function pinOnNextPage(id: number | null): void { try { sessionStorage.setItem(NEXT_ACCOUNT_KEY, id == null ? "" : String(id)); } catch { /* ignore */ } } // Taken over at import, before anything reads the pin. try { const next = sessionStorage.getItem(NEXT_ACCOUNT_KEY); if (next != null) { sessionStorage.removeItem(NEXT_ACCOUNT_KEY); if (next) sessionStorage.setItem(ACTIVE_ACCOUNT_KEY, next); else sessionStorage.removeItem(ACTIVE_ACCOUNT_KEY); } } catch { /* ignore */ } export function activeAccountId(): number | null { try { const raw = sessionStorage.getItem(ACTIVE_ACCOUNT_KEY); if (!raw) return null; const n = Number(raw); return Number.isFinite(n) ? n : null; } catch { return null; } } // Append `?account=` to a top-level navigation URL (SEC-11). XHRs carry the active account in // the X-Siftlode-Account header, but a full-page navigation (window.location) can't, so the OAuth // link/upgrade GETs would resolve to the SESSION DEFAULT account instead of the tab's active one — the // identity/token would then land on the wrong account. This threads the active account through the URL // so resolved_user_id picks it. No-op when there's no distinct active account pinned yet. export function withActiveAccount(path: string): string { const id = activeAccountId(); if (id == null) return path; return `${path}${path.includes("?") ? "&" : "?"}account=${id}`; }