| /** | |
| * 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 | |
| } | |
| } | |