Masters-four-Tab-OpenAI / frontend /src /auth /apiAuthEvents.ts
Pete Dunn
Harden auth startup flow and add full HF auth e2e coverage
37211b8
Raw
History Blame Contribute Delete
4.39 kB
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<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 {
// 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<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);
}