mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-05 07:41:39 +00:00
fix(dashboard): auto-reload SPA on stale-token 401 in loopback mode (#33861)
The dashboard's loopback auth uses an ephemeral '_SESSION_TOKEN' that rotates on every server restart (hermes update, hermes gateway restart, etc.). A tab kept open across the restart holds the OLD token in window.__HERMES_SESSION_TOKEN__ from the previous HTML render, so every '/api/*' fetch returns '401 Unauthorized' — surfacing in the UI as 'Failed to load Kanban board: 401: Unauthorized', 'Analytics 401', etc. (#24186, #25275). Before this patch the workaround was to manually clear site data or hard-reload — annoying enough that users reported it as a regression even though the token rotation is by design (security property: stolen tokens can't survive a server restart). The HTML response already sets 'Cache-Control: no-store, no-cache, must-revalidate', so a reload reliably picks up the freshly-injected token. fetchJSON now triggers that reload automatically on the first loopback-mode 401, guarded by a sessionStorage flag so a genuine auth bug (where even the new token fails) falls through to throw on the second attempt instead of reload-looping. The flag is cleared on any 2xx so a subsequent server restart in the same tab gets its own reload cycle. Gated mode is unaffected — that path already redirects to login_url via the structured 401 envelope (Phase 6), and the new code is explicitly skipped when window.__HERMES_AUTH_REQUIRED__ is set. Refs #24186, #25275
This commit is contained in:
parent
0c859a1c04
commit
fe5c8ec4ad
1 changed files with 37 additions and 0 deletions
|
|
@ -91,6 +91,43 @@ export async function fetchJSON<T>(url: string, init?: RequestInit): Promise<T>
|
|||
// Never resolve — the page is about to unload.
|
||||
return new Promise<T>(() => {});
|
||||
}
|
||||
// Loopback mode: ``_SESSION_TOKEN`` rotates on every server restart
|
||||
// (``hermes update``, ``hermes gateway restart``, etc.). A tab kept
|
||||
// open across the restart holds the OLD token in
|
||||
// ``window.__HERMES_SESSION_TOKEN__`` from the previous HTML render,
|
||||
// so every fetch returns 401. The HTML is served ``Cache-Control:
|
||||
// no-store`` so a reload picks up the freshly-injected token. Trigger
|
||||
// that reload once on the first stale-token 401 — gated mode is
|
||||
// handled above, so reaching here in gated mode means a real
|
||||
// middleware failure that should not reload-loop.
|
||||
if (!window.__HERMES_AUTH_REQUIRED__) {
|
||||
let alreadyReloaded = false;
|
||||
try {
|
||||
alreadyReloaded =
|
||||
sessionStorage.getItem("hermes.tokenReloadAttempted") === "1";
|
||||
} catch {
|
||||
/* SSR / privacy mode — fall through to throw */
|
||||
}
|
||||
if (!alreadyReloaded) {
|
||||
try {
|
||||
sessionStorage.setItem("hermes.tokenReloadAttempted", "1");
|
||||
} catch {
|
||||
/* SSR / privacy mode — best effort */
|
||||
}
|
||||
window.location.reload();
|
||||
return new Promise<T>(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (res.ok) {
|
||||
// Clear the stale-token reload guard: a successful 2xx proves the
|
||||
// current ``window.__HERMES_SESSION_TOKEN__`` is valid, so the next
|
||||
// 401 — if any — should be allowed to trigger its own reload cycle.
|
||||
try {
|
||||
sessionStorage.removeItem("hermes.tokenReloadAttempted");
|
||||
} catch {
|
||||
/* SSR / privacy mode — ignore */
|
||||
}
|
||||
}
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => res.statusText);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue