File size: 4,146 Bytes
783fcb6 7f2cbc9 783fcb6 b424a2e 783fcb6 b424a2e 8bcc42c b424a2e 783fcb6 b424a2e 7f2cbc9 b424a2e 8bcc42c 7f2cbc9 b424a2e 7f2cbc9 b424a2e 783fcb6 7f2cbc9 783fcb6 7f2cbc9 783fcb6 7f2cbc9 b424a2e 7f2cbc9 b424a2e 7f2cbc9 b424a2e 7f2cbc9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | 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<string, unknown>;
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<string, unknown> | 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 "";
}
|