/** * Auth error codes, PDA-friendly message mapping, and error parsing utilities. * * All user-facing messages follow PDA-friendly language guidelines: * no alarming words like OVERDUE, URGENT, MUST, CRITICAL, REQUIRED, ERROR, FAILED. */ /** Union of all recognised auth error codes. */ export type AuthErrorCode = | "access_denied" | "invalid_credentials" | "server_error" | "network_error" | "rate_limited" | "session_expired" | "unknown"; /** A parsed, UI-ready representation of an auth error. */ export interface ParsedAuthError { code: AuthErrorCode; /** PDA-friendly message suitable for display to the user. */ message: string; /** Whether the operation that caused this can be retried. */ retryable: boolean; } /** * PDA-friendly error messages keyed by error code. * Uses calm, informational language throughout. */ const ERROR_MESSAGES: Record = { access_denied: "Authentication paused. Please try again when ready.", invalid_credentials: "The email and password combination wasn't recognized.", server_error: "The service is taking a break. Please try again in a moment.", network_error: "Unable to connect. Check your network and try again.", rate_limited: "You've tried a few times. Take a moment and try again shortly.", session_expired: "Your session ended. Please sign in again when ready.", unknown: "Authentication didn't complete. Please try again when ready.", }; /** Error codes that are safe to retry automatically. */ const RETRYABLE_CODES: ReadonlySet = new Set([ "network_error", "server_error", ]); /** Set of recognised error code strings for fast membership testing. */ const KNOWN_CODES: ReadonlySet = new Set(Object.keys(ERROR_MESSAGES)); /** * Type-guard: checks whether a string value is a known {@link AuthErrorCode}. */ function isAuthErrorCode(value: string): value is AuthErrorCode { return KNOWN_CODES.has(value); } /** * Type-guard: checks whether a value looks like an HTTP response object * with a numeric `status` property. */ function isHttpResponseLike(value: unknown): value is { status: number } { return ( typeof value === "object" && value !== null && "status" in value && typeof (value as { status: unknown }).status === "number" ); } /** * Map an HTTP status code to an {@link AuthErrorCode}. */ function httpStatusToCode(status: number): AuthErrorCode { // In auth context, both 401 and 403 indicate the user should re-authenticate if (status === 401 || status === 403) { return "invalid_credentials"; } if (status === 429) { return "rate_limited"; } if (status >= 500) { return "server_error"; } return "unknown"; } /** * Build a {@link ParsedAuthError} for the given code. */ function buildParsedError(code: AuthErrorCode): ParsedAuthError { return { code, message: ERROR_MESSAGES[code], retryable: RETRYABLE_CODES.has(code), }; } /** * Parse an unknown error value into a structured, PDA-friendly * {@link ParsedAuthError}. * * Handles: * - `TypeError` whose message contains "fetch" -> `network_error` * - Generic `Error` objects with keyword-based message matching * - HTTP-response-shaped objects with a numeric `status` field * - Plain strings that match a known error code * - Anything else falls back to `unknown` */ export function parseAuthError(error: unknown): ParsedAuthError { // 1. TypeError with "fetch" in message -> network error if (error instanceof TypeError && error.message.toLowerCase().includes("fetch")) { return buildParsedError("network_error"); } // 2. Generic Error objects — match on message keywords if (error instanceof Error) { const msg = error.message.toLowerCase(); if (msg.includes("unauthorized") || msg.includes("forbidden")) { return buildParsedError("invalid_credentials"); } if (msg.includes("rate limit") || msg.includes("too many")) { return buildParsedError("rate_limited"); } if ( msg.includes("internal server") || msg.includes("service unavailable") || msg.includes("bad gateway") || msg.includes("gateway timeout") ) { return buildParsedError("server_error"); } if (msg.includes("session") && msg.includes("expired")) { return buildParsedError("session_expired"); } if (msg.includes("network") || msg.includes("connection")) { return buildParsedError("network_error"); } return buildParsedError("unknown"); } // 3. HTTP response-like objects (e.g. { status: 429 }) if (isHttpResponseLike(error)) { return buildParsedError(httpStatusToCode(error.status)); } // 4. Plain string matching a known error code (e.g. from URL query params) if (typeof error === "string") { if (isAuthErrorCode(error)) { return buildParsedError(error); } return buildParsedError("unknown"); } // 5. Fallback return buildParsedError("unknown"); } /** * Look up the PDA-friendly message for a given {@link AuthErrorCode}. * Returns the `unknown` message for any unrecognised code. */ export function getErrorMessage(code: AuthErrorCode): string { return ERROR_MESSAGES[code]; }