type ClientNavigate = (to: string, options: { replace: boolean }) => void | Promise; interface SessionLifecycle { onStart?: () => void | Promise; onEnd?: () => void; } let navigateFn: ClientNavigate | null = null; let sessionLifecycle: SessionLifecycle = {}; export function registerClientNavigate(fn: ClientNavigate): void { navigateFn = fn; } export function registerSessionLifecycle(lifecycle: SessionLifecycle): void { sessionLifecycle = lifecycle; } export function resetClientNavigate(): void { navigateFn = null; sessionLifecycle = {}; } export function notifySessionStart(): void { void sessionLifecycle.onStart?.(); } export function notifySessionEnd(): void { sessionLifecycle.onEnd?.(); } /** * Stay inside the already-loaded SPA when possible. * A full `location.replace` makes nginx look up a real file (e.g. `/auth/login`) and 404. */ export function clientReplace(to: string): void { if (!navigateFn) { window.location.replace(to); return; } void Promise.resolve(navigateFn(to, { replace: true })).catch(() => { window.location.replace(to); }); }