| 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 { |
| |
| } |
|
|
| try { |
| if (typeof Request !== "undefined" && input instanceof Request) { |
| return input.headers.has("Authorization"); |
| } |
| } catch { |
| |
| } |
| 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<ApiUnauthorizedDetail>(API_UNAUTHORIZED_EVENT, { detail })); |
| } |
|
|
| async function parseErrorMessage(response: Response): Promise<string> { |
| 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<string, unknown>; |
| 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 { |
| |
| } |
| 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<Response> => { |
| 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<ApiUnauthorizedDetail>; |
| handler(custom.detail); |
| }; |
| window.addEventListener(API_UNAUTHORIZED_EVENT, wrapped as EventListener); |
| return () => window.removeEventListener(API_UNAUTHORIZED_EVENT, wrapped as EventListener); |
| } |
|
|