export function decodeAuthMessage(raw: string | null): string | null { if (!raw) return null; try { return decodeURIComponent(String(raw).replace(/\+/g, " ")); } catch { return raw; } } function redactSensitiveAuthText(input: string): string { let text = String(input || ""); // Redact token-like and key-like fragments that should never be displayed to users. text = text.replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+\b/g, "[redacted-jwt]"); text = text.replace(/\bsk-[A-Za-z0-9_-]{20,}\b/g, "[redacted-key]"); text = text.replace(/\b(Bearer)\s+[A-Za-z0-9\-._~+/=]+\b/gi, "$1 [redacted]"); text = text.replace( /\b(code|state|access_token|id_token|refresh_token)=([^&\s]+)/gi, (_m, key) => `${String(key)}=[redacted]` ); return text; } function clampAuthText(input: string, maxLen: number = 360): string { const text = String(input || ""); if (text.length <= maxLen) return text; return `${text.slice(0, maxLen).trimEnd()}...`; } export function toUserFacingAuthError(message: string): string { const raw = String(message || "").replace(/\s+/g, " ").trim(); if (!raw) return "Authentication error."; if (/service not found:\s*https:\/\/masters-toolkit-api\/?/i.test(raw)) { return "Auth0 was asked for legacy audience `https://masters-toolkit-api`, which this app no longer uses. Remove `VITE_AUTH0_AUDIENCE`/`AUTH0_AUDIENCE` unless you have a real Auth0 API Identifier configured."; } if (/invalid state/i.test(raw)) { return "Invalid state. Your login session expired or became stale. Retry login to start a fresh session."; } if (/^access_denied$/i.test(raw)) { return "Access denied by Auth0 policy. Confirm allowed email domains and API/application access, then retry login."; } if (/^invalid_request$/i.test(raw)) { return "Authentication request is invalid. Check callback URL and Auth0 application settings. If this deployment does not use a custom API, leave `VITE_AUTH0_AUDIENCE` unset."; } if (/^unauthorized_client$/i.test(raw)) { return "Auth0 client is not authorized for this request. Verify callback URLs and API Application Access settings."; } return clampAuthText(redactSensitiveAuthText(raw)); } function parseAuthCallbackParams(urlValue: string): { err: string | null; desc: string | null } { const url = new URL(String(urlValue || "")); const errRaw = url.searchParams.get("error"); const descRaw = url.searchParams.get("error_description"); if (errRaw || descRaw) { return { err: decodeAuthMessage(errRaw), desc: decodeAuthMessage(descRaw) }; } const hash = String(url.hash || "").replace(/^#/, "").trim(); if (!hash || !hash.includes("=")) { return { err: null, desc: null }; } const hp = new URLSearchParams(hash); const errHash = hp.get("error"); const descHash = hp.get("error_description"); return { err: decodeAuthMessage(errHash), desc: decodeAuthMessage(descHash) }; } export function getCallbackErrorFromUrl(urlValue: string): string | null { try { const { err, desc } = parseAuthCallbackParams(urlValue); if (!err && !desc) return null; const combined = toUserFacingAuthError(String(desc || err || "")); return combined || null; } catch { return null; } } export function getAuthErrorMessage(error: unknown): string { if (!error) return ""; if (typeof error === "string") return clampAuthText(redactSensitiveAuthText(error)); const obj = error as Record; const candidates: unknown[] = [ obj.error_description, obj.description, obj.message, obj.error, ]; for (const c of candidates) { const text = String(c || "").trim(); if (text) return clampAuthText(redactSensitiveAuthText(text)); } // Some wrappers place useful fields under cause. const cause = obj.cause as Record | undefined; if (cause && typeof cause === "object") { for (const c of [cause.error_description, cause.description, cause.message, cause.error]) { const text = String(c || "").trim(); if (text) return clampAuthText(redactSensitiveAuthText(text)); } } return ""; }