Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 49s
Forgejo Docker Build / Root app tests (push) Successful in 49s
Forgejo Android APK / Build signed APK (push) Successful in 1m51s
Forgejo Docker Build / Build Docker image (push) Successful in 18s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
The server side has worked since this morning, but no browser could reach it. authFetch rejects every /api request that has no account before it is sent, and the four preview endpoints were not on its short list — so the status call that decides whether to show the login screen never left the browser, and the screen was always shown. The list now mirrors the server's own allow-list exactly: status, examples, chat, chat/stream, and nothing else. Preview now begins from any path. A visitor landing on the root met the login wall while /assistant did not, which read as "preview doesn't work"; both now enter the assistant, and the URL follows. Reaching for anything that needs an account raises the sign-in screen through one hook in authFetch rather than a check on every control — but only for something the visitor did. The page also fetches saved chats and config in the background on load, and the first version raised the screen for those too, burying the assistant before a word was typed. The hook is gated on navigator.userActivation. The HIPAA notice is hidden on that screen in preview: it is an invitation to sign in, not the compliance notice a clinician sees on first login. Verified in a browser: landing on / and on /assistant both show the assistant with no login wall and no HIPAA text; clicking Workspace raises sign-in. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
108 lines
5.6 KiB
JavaScript
108 lines
5.6 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'
|
|
]);
|
|
// The signed-out preview. Same four paths the server allow-lists; nothing
|
|
// else may leave the browser without an account. Until these were here the
|
|
// status call was rejected before it was ever sent, so the server-side
|
|
// preview could never be reached from the page.
|
|
var previewPaths = new Set([
|
|
'/api/clinical-assistant/status', '/api/clinical-assistant/examples',
|
|
'/api/clinical-assistant/chat', '/api/clinical-assistant/chat/stream'
|
|
]);
|
|
function inPreview() { return document.body && document.body.classList.contains('assistant-preview'); }
|
|
|
|
// 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') {
|
|
if (previewPaths.has(url.pathname)) return rawFetch(input, Object.assign({}, init, { credentials: 'same-origin' }));
|
|
// A preview visitor reaching for anything else is exactly the moment to
|
|
// ask them to sign in — one hook here, not a check on every button. Only
|
|
// for something the visitor did, though: the page also fetches saved
|
|
// chats and config in the background on load, and raising the sign-in
|
|
// screen for those buried the assistant under it before a word was typed.
|
|
var gesture = navigator.userActivation ? navigator.userActivation.isActive : false;
|
|
if (inPreview() && gesture) document.dispatchEvent(new CustomEvent('preview-needs-account', { detail: { path: url.pathname } }));
|
|
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.
|
|
});
|
|
};
|
|
}
|