const API_UNAUTHORIZED_EVENT = "masters_toolkit_api_unauthorized"; const FETCH_PATCH_FLAG = "__mastersToolkitAuthFetchPatched"; export type ApiUnauthorizedDetail = { status: number; url: string; message: string; requestId?: string; }; let lastUnauthorizedKey = ""; let lastUnauthorizedAt = 0; function isProtectedPath(pathname: string): boolean { return pathname.startsWith("/api/") || pathname.startsWith("/masters_files/") || pathname.startsWith("/pots_files/"); } function resolveRequestUrl(input: RequestInfo | URL): URL | null { try { if (typeof input === "string") return new URL(input, window.location.origin); if (input instanceof URL) return new URL(input.toString(), window.location.origin); if (typeof Request !== "undefined" && input instanceof Request) { return new URL(input.url, window.location.origin); } return null; } catch { return null; } } function requestHasAuthorization(input: RequestInfo | URL, init?: RequestInit): boolean { try { const initHeaders = new Headers(init?.headers || {}); if (initHeaders.has("Authorization")) return true; } catch { // ignore } try { if (typeof Request !== "undefined" && input instanceof Request) { return input.headers.has("Authorization"); } } catch { // ignore } return false; } function emitUnauthorized(detail: ApiUnauthorizedDetail): void { if (typeof window === "undefined") return; const key = `${detail.status}|${detail.url}|${detail.message}`.slice(0, 512); const now = Date.now(); if (key === lastUnauthorizedKey && now - lastUnauthorizedAt < 1200) return; lastUnauthorizedKey = key; lastUnauthorizedAt = now; window.dispatchEvent(new CustomEvent(API_UNAUTHORIZED_EVENT, { detail })); } async function parseErrorMessage(response: Response): Promise { try { const clone = response.clone(); const contentType = String(clone.headers.get("content-type") || "").toLowerCase(); if (contentType.includes("application/json")) { const payload = (await clone.json()) as Record; const detail = payload?.detail; if (typeof detail === "string" && detail.trim()) return detail.trim(); const err = payload?.error; if (typeof err === "string" && err.trim()) return err.trim(); } const text = (await clone.text()).trim(); if (text) return text; } catch { // ignore parse failures } return ""; } export function installApiAuthResponseMonitor(): void { if (typeof window === "undefined") return; const win = window as Window & { [FETCH_PATCH_FLAG]?: boolean }; if (win[FETCH_PATCH_FLAG]) return; win[FETCH_PATCH_FLAG] = true; const originalFetch = window.fetch.bind(window); window.fetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise => { const response = await originalFetch(input, init); const url = resolveRequestUrl(input); if (!url || !isProtectedPath(url.pathname)) return response; const hadAuthorization = requestHasAuthorization(input, init); if (response.status === 401 || response.status === 403) { const authErrorHeader = String(response.headers.get("x-masters-auth-error") || "").trim().toLowerCase(); const isTaggedAuthError = authErrorHeader === "1" || authErrorHeader === "true"; if (response.status === 401 && !hadAuthorization) return response; if (response.status === 403 && !isTaggedAuthError) return response; void (async () => { const detailMessage = await parseErrorMessage(response); emitUnauthorized({ status: response.status, url: `${url.pathname}${url.search}`, message: detailMessage || `API request failed (${response.status}).`, requestId: response.headers.get("x-request-id") || undefined, }); })(); } return response; }) as typeof window.fetch; } export function onApiUnauthorized(handler: (detail: ApiUnauthorizedDetail) => void): () => void { if (typeof window === "undefined") return () => {}; const wrapped = (event: Event) => { const custom = event as CustomEvent; handler(custom.detail); }; window.addEventListener(API_UNAUTHORIZED_EVENT, wrapped as EventListener); return () => window.removeEventListener(API_UNAUTHORIZED_EVENT, wrapped as EventListener); }