mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
fix(web): stop /api/auth/me 401 from triggering a reload loop
In loopback mode the dashboard's identity probe (/api/auth/me) returns 401 by design — AuthWidget swallows it and renders nothing. But the probe routed through fetchJSON, whose loopback 401 handler treats a 401 as a rotated session token and full-page-reloads to pick up a fresh one. That reload is guarded by a one-shot sessionStorage flag which every *successful* request clears, so with auth/me reliably 401ing and the other dashboard calls (status/config/sessions) reliably succeeding, the guard never sticks and the page reload-loops indefinitely (the "boot flash"). Add an allowUnauthorized option to fetchJSON that skips only the loopback stale-token reload (the 401 still throws so AuthWidget can catch it, and the gated-mode login_url envelope redirect is unaffected), and use it for getAuthMe. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0acb7f4583
commit
a5c1f925b5
1 changed files with 27 additions and 3 deletions
|
|
@ -41,7 +41,11 @@ function setSessionHeader(headers: Headers, token: string): void {
|
|||
}
|
||||
}
|
||||
|
||||
export async function fetchJSON<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
export async function fetchJSON<T>(
|
||||
url: string,
|
||||
init?: RequestInit,
|
||||
options?: FetchJSONOptions,
|
||||
): Promise<T> {
|
||||
// Inject the session token into all /api/ requests.
|
||||
const headers = new Headers(init?.headers);
|
||||
const token = window.__HERMES_SESSION_TOKEN__;
|
||||
|
|
@ -100,7 +104,7 @@ export async function fetchJSON<T>(url: string, init?: RequestInit): Promise<T>
|
|||
// 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__) {
|
||||
if (!window.__HERMES_AUTH_REQUIRED__ && !options?.allowUnauthorized) {
|
||||
let alreadyReloaded = false;
|
||||
try {
|
||||
alreadyReloaded =
|
||||
|
|
@ -198,8 +202,19 @@ export const api = {
|
|||
* still exists but is never useful there (no Session, no cookie). The
|
||||
* AuthWidget component swallows 401s from this call: if the gate isn't
|
||||
* engaged, /api/auth/me returns 401 and the widget renders nothing.
|
||||
*
|
||||
* ``allowUnauthorized`` is load-bearing: in loopback mode this endpoint
|
||||
* 401s by design, and fetchJSON's default loopback behaviour treats a
|
||||
* 401 as a rotated session token and full-page-reloads to pick up a
|
||||
* fresh one. Because every *other* dashboard request succeeds (and so
|
||||
* clears the one-shot reload guard), that turns this expected 401 into
|
||||
* an infinite reload loop. Opting out keeps the 401 a plain throw the
|
||||
* widget can catch.
|
||||
*/
|
||||
getAuthMe: () => fetchJSON<AuthMeResponse>("/api/auth/me"),
|
||||
getAuthMe: () =>
|
||||
fetchJSON<AuthMeResponse>("/api/auth/me", undefined, {
|
||||
allowUnauthorized: true,
|
||||
}),
|
||||
logout: () =>
|
||||
fetch(`${BASE}/auth/logout`, {
|
||||
method: "POST",
|
||||
|
|
@ -514,6 +529,15 @@ export interface ActionResponse {
|
|||
pid: number;
|
||||
}
|
||||
|
||||
/** Per-call overrides for {@link fetchJSON}. */
|
||||
interface FetchJSONOptions {
|
||||
/** When true, a 401 response is surfaced as a normal thrown error rather
|
||||
* than triggering the loopback stale-token page reload. Use for probes
|
||||
* whose 401 is an expected signal (e.g. /api/auth/me in non-gated mode)
|
||||
* rather than evidence of a rotated session token. */
|
||||
allowUnauthorized?: boolean;
|
||||
}
|
||||
|
||||
export interface ActionStatusResponse {
|
||||
exit_code: number | null;
|
||||
lines: string[];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue