// Account-bound requests cannot outlive the document's verified owner. if (!window.__fetchAuthIntercepted) { window.__fetchAuthIntercepted = true; var rawFetch = window.fetch.bind(window); var boundary = window.AccountBoundary; var authPaths = new Set([ '/api/auth/login', '/api/auth/register', '/api/auth/logout', '/api/auth/forgot-password', '/api/auth/reset-password', '/api/auth/verify-email', '/api/auth/resend-verification', '/api/auth/oidc', '/api/auth/oidc-status', '/api/auth/registration-status' ]); // Narrow transition requests bypass the frozen clinical transport. Logout // headers are captured before freezing; its verification is cookie-only. boundary.logoutRequest = function(headers) { return rawFetch('/api/auth/logout', { method: 'POST', headers: headers, credentials: 'same-origin', keepalive: true }); }; boundary.cookieSessionRequest = function() { return rawFetch('/api/auth/me', { credentials: 'same-origin', cache: 'no-store' }); }; if (navigator.sendBeacon) { var sendBeacon = navigator.sendBeacon.bind(navigator); navigator.sendBeacon = function(input, data) { var url = new URL(input, window.location.href); if (url.origin === window.location.origin && url.pathname.startsWith('/api/') && !boundary.active()) return false; return sendBeacon(input, data); }; } window.fetch = function(input, init) { var url; try { url = new URL(typeof input === 'string' || input instanceof URL ? input : input.url, window.location.href); } catch (e) { return rawFetch(input, init); } if (url.origin !== window.location.origin || !url.pathname.startsWith('/api/')) return rawFetch(input, init); if (boundary.blocked()) return Promise.reject(boundary.error()); var ticket = boundary.capture(); // Also detects shared-cookie changes before storage events arrive. if (boundary.blocked()) return Promise.reject(boundary.error()); // No clinical module may preload a previous cookie's data on the login screen. if (!ticket && !authPaths.has(url.pathname) && url.pathname !== '/api/auth/me' && url.pathname !== '/api/models') { return Promise.reject(boundary.error()); } var login = (url.pathname === '/api/auth/login' || url.pathname === '/api/auth/register') && String((init && init.method) || (input && input.method) || 'GET').toUpperCase() === 'POST'; try { if (login) boundary.startLogin(); } catch (e) { return Promise.reject(e); } var revision = boundary.revision(); var shared = boundary.read(); var generation = shared && shared.generation; var abort = new AbortController(); var callerSignal = (init && init.signal) || (input && input.signal); var accountSignal = boundary.signal(); function cancel() { abort.abort(); } if (callerSignal) { if (callerSignal.aborted) cancel(); else callerSignal.addEventListener('abort', cancel, { once: true }); } if (!login) accountSignal.addEventListener('abort', cancel, { once: true }); function check() { var now = boundary.read(); if (revision !== boundary.revision() || generation !== (now && now.generation) || (!login && (boundary.blocked() || (ticket && !boundary.valid(ticket))))) throw boundary.error(); } function guardResponse(resp) { check(); if (resp.status === 401 && ticket && !authPaths.has(url.pathname)) { boundary.end(); boundary.reload(); throw boundary.error(); } // Body parsing can finish after fetch itself; gate those continuations too. ['json', 'text', 'blob', 'arrayBuffer', 'formData'].forEach(function(method) { if (!resp[method]) return; var original = resp[method].bind(resp); resp[method] = function() { check(); return original().then(function(value) { check(); return value; }); }; }); if (resp.clone) { var clone = resp.clone.bind(resp); resp.clone = function() { check(); return guardResponse(clone()); }; } return resp; } return rawFetch(input, Object.assign({}, init, { signal: abort.signal })) .then(guardResponse).finally(function() { if (callerSignal) callerSignal.removeEventListener('abort', cancel); // Keep the account abort listener until document disposal: it also // cancels a response body that has not finished streaming yet. }); }; }