Fix response.ok JSDoc (2xx not 200), remove stale token refresh claim, remove non-actionable comment, fix CSRF comment placement, add 403 mapping rationale. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
167 lines
5.1 KiB
TypeScript
167 lines
5.1 KiB
TypeScript
/**
|
|
* 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<AuthErrorCode, string> = {
|
|
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<AuthErrorCode> = new Set<AuthErrorCode>([
|
|
"network_error",
|
|
"server_error",
|
|
]);
|
|
|
|
/** Set of recognised error code strings for fast membership testing. */
|
|
const KNOWN_CODES: ReadonlySet<string> = new Set<string>(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];
|
|
}
|