pediatric-ai-scribe-v3/public/js/authFetch.js

91 lines
4.3 KiB
JavaScript

// 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.
});
};
}