Pete Dunn
Harden auth flows, scope user memory, and fix battery shortlist
8bcc42c
Raw
History Blame Contribute Delete
4.15 kB
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 "";
}