/** * Global 401 handler. Wraps window.fetch so any protected /api/* call that * comes back unauthorized fires a `tesseract:unauthorized` event — the router * listens for it and bounces the user to /login. Auth endpoints themselves * legitimately return 401 (wrong password, no session) and are exempt so * those errors surface to their own UIs instead of triggering a redirect. * * Monkey-patching window.fetch is intentional: it covers every existing call * site (hooks, components, api modules) without 25 file edits, and the * exemption list is tiny + stable. */ const EXEMPT = new Set([ '/api/auth/login', '/api/auth/signup', '/api/auth/me', '/api/auth/logout', ]) let installed = false export function installApiInterceptor() { if (installed || typeof window === 'undefined') return installed = true const original = window.fetch.bind(window) window.fetch = async (input, init) => { const url = typeof input === 'string' ? input : (input?.url ?? '') const r = await original(input, init) if ( r.status === 401 && url.startsWith('/api/') && !EXEMPT.has(url.split('?')[0]) ) { window.dispatchEvent(new CustomEvent('tesseract:unauthorized')) } return r } }