import { TRPCClientError } from "@trpc/client"; function errorMessage(error: unknown): string { if (error instanceof TRPCClientError) return error.message; if (error instanceof Error) return error.message; if (typeof error === "object" && error !== null && "message" in error) { const message = (error as { message: unknown }).message; if (typeof message === "string") return message; } return ""; } function errorStatus(error: unknown): number | undefined { if (typeof error !== "object" || error === null || !("status" in error)) return undefined; const status = (error as { status: unknown }).status; return typeof status === "number" ? status : undefined; } export function parseRetryAfterSeconds(value: string | number | null | undefined): number | null { if (value == null || value === "") return null; const seconds = typeof value === "number" ? value : Number(value); if (!Number.isFinite(seconds) || seconds <= 0) return null; return Math.ceil(seconds); } export function isRateLimitError(error: unknown): boolean { if (errorStatus(error) === 429) return true; if (error instanceof TRPCClientError && error.data?.code === "TOO_MANY_REQUESTS") { return true; } const message = errorMessage(error).toLowerCase(); return ( message.includes("too many") || message.includes("rate limit") || message.includes("try again later") ); } export function isUnauthorizedError(error: unknown): boolean { if (isRateLimitError(error)) return false; if (error instanceof TRPCClientError) { if (error.data?.code === "UNAUTHORIZED") return true; } if (errorStatus(error) === 401) return true; const message = errorMessage(error).toLowerCase(); return message === "unauthorized" || message.includes("not authenticated"); } export function formatRateLimitMessage(retryAfterSeconds?: number | null): string { const retryAfter = parseRetryAfterSeconds(retryAfterSeconds ?? null); if (retryAfter != null) { if (retryAfter < 60) { return `Too many attempts. Wait ${retryAfter} second${retryAfter === 1 ? "" : "s"} and try again.`; } const minutes = Math.ceil(retryAfter / 60); return `Too many attempts. Wait about ${minutes} minute${minutes === 1 ? "" : "s"} and try again.`; } return "Too many attempts. Please wait a moment and try again."; } export function formatTrpcErrorMessage(error: unknown, fallback = "Something went wrong"): string { if (isRateLimitError(error)) { return formatRateLimitMessage(); } if (isUnauthorizedError(error)) { return "Your session expired. Sign in again to continue."; } if (error instanceof TRPCClientError) { return error.message || fallback; } if (error instanceof Error) { return error.message || fallback; } return fallback; } type AuthLikeError = { message?: string; status?: number; }; export function formatAuthErrorMessage(error: AuthLikeError | null | undefined): string { if (!error) return "Something went wrong"; if (isRateLimitError(error)) { return formatRateLimitMessage(); } const message = error.message ?? ""; if (message.toLowerCase().includes("internal") || message.includes("500")) { return "Server error — is the API running with Postgres? Check beenvoice dev + docker."; } return message || "Invalid email or password"; } export async function readHttpErrorMessage(response: Response): Promise { if (response.status === 429) { const retryAfter = parseRetryAfterSeconds( response.headers.get("x-retry-after") ?? response.headers.get("retry-after"), ); return formatRateLimitMessage(retryAfter); } const data = (await response.json().catch(() => ({}))) as { error?: string; message?: string; }; const message = data.error ?? data.message; if (message && isRateLimitError({ message, status: response.status })) { return formatRateLimitMessage(); } return message ?? "Something went wrong"; }