91 lines
4.3 KiB
JavaScript
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.
|
|
});
|
|
};
|
|
}
|