export const USER_MESSAGE_KEYS = { unknown: "messages.common.unknown", network: "messages.network.unavailable", badRequest: "messages.http.bad_request", unauthorized: "messages.http.unauthorized", forbidden: "messages.http.forbidden", notFound: "messages.http.not_found", conflict: "messages.http.conflict", payloadTooLarge: "messages.http.payload_too_large", timeout: "messages.http.timeout", tooManyRequests: "messages.http.too_many_requests", server: "messages.http.server", promptRequired: "messages.validation.prompt_required", authLoginRequired: "messages.auth.login_required", authCodeInvalid: "messages.auth.code_invalid", authCodeExpired: "messages.auth.code_expired", authInvalidCredentials: "messages.auth.invalid_credentials", authInvalidRegion: "messages.auth.invalid_region", authProviderNotReady: "messages.auth.provider_not_ready", authHumanVerificationRequired: "messages.auth.human_verification_required", authHumanVerificationFailed: "messages.auth.human_verification_failed", shareSelfInvite: "messages.share.self_invite", shareMemberExists: "messages.share.member_exists" } as const; const HTTP_STATUS_MESSAGE_KEYS: Record = { 400: USER_MESSAGE_KEYS.badRequest, 401: USER_MESSAGE_KEYS.unauthorized, 403: USER_MESSAGE_KEYS.forbidden, 404: USER_MESSAGE_KEYS.notFound, 408: USER_MESSAGE_KEYS.timeout, 409: USER_MESSAGE_KEYS.conflict, 413: USER_MESSAGE_KEYS.payloadTooLarge, 429: USER_MESSAGE_KEYS.tooManyRequests }; const BACKEND_STATUS_MESSAGE_KEYS: Record = { "auth.login_required": USER_MESSAGE_KEYS.authLoginRequired, "auth.code_invalid": USER_MESSAGE_KEYS.authCodeInvalid, "auth.code_expired": USER_MESSAGE_KEYS.authCodeExpired, "auth.invalid_credentials": USER_MESSAGE_KEYS.authInvalidCredentials, "auth.invalid_region": USER_MESSAGE_KEYS.authInvalidRegion, "auth.provider_not_ready": USER_MESSAGE_KEYS.authProviderNotReady, "auth.human_verification_required": USER_MESSAGE_KEYS.authHumanVerificationRequired, "auth.human_verification_failed": USER_MESSAGE_KEYS.authHumanVerificationFailed, "share.self_invite": USER_MESSAGE_KEYS.shareSelfInvite, "share.member_exists": USER_MESSAGE_KEYS.shareMemberExists }; const RAW_STATUS_MESSAGE_KEYS: Array<{ pattern: RegExp; key: string }> = [ { pattern: /verification code is invalid/i, key: USER_MESSAGE_KEYS.authCodeInvalid }, { pattern: /verification code is expired/i, key: USER_MESSAGE_KEYS.authCodeExpired }, { pattern: /login required/i, key: USER_MESSAGE_KEYS.authLoginRequired }, { pattern: /prompt is required/i, key: USER_MESSAGE_KEYS.promptRequired } ]; export type BackendStatus = string | number; export type BackendStatusBody = { error?: string; message?: string; errorCode?: string; code?: number; requestId?: string; traceId?: string; }; export type RequestDomain = "auth" | "design" | "api"; export type Translate = (key: string, values?: Record) => string; export class ApiFailure extends Error { status: number; backendStatus: BackendStatus; userMessageKey: string; rawMessage: string; body: unknown; domain: RequestDomain; requestId?: string; traceId?: string; constructor(domain: RequestDomain, status: number, statusText: string, body: unknown) { const parsed = parseStatusBody(body); const backendStatus = parsed.errorCode ?? parsed.code ?? status; const userMessageKey = resolveUserMessageKey(backendStatus, status, parsed.rawMessage); super(userMessageKey); this.name = "ApiFailure"; this.domain = domain; this.status = status; this.backendStatus = backendStatus; this.userMessageKey = userMessageKey; this.rawMessage = parsed.rawMessage || statusText; this.body = body; this.requestId = parsed.requestId; this.traceId = parsed.traceId; } } export class NetworkFailure extends Error { backendStatus = "network.unavailable"; userMessageKey = USER_MESSAGE_KEYS.network; domain: RequestDomain; cause?: unknown; constructor(domain: RequestDomain, cause?: unknown) { super(USER_MESSAGE_KEYS.network); this.name = "NetworkFailure"; this.domain = domain; this.cause = cause; } } export function createApiFailure(domain: RequestDomain, response: Response, body: unknown) { return new ApiFailure(domain, response.status, response.statusText, body); } export function createNetworkFailure(domain: RequestDomain, cause?: unknown) { return new NetworkFailure(domain, cause); } export function backendStatusOf(value: unknown): BackendStatus | undefined { if (value instanceof ApiFailure || value instanceof NetworkFailure) { return value.backendStatus; } return undefined; } export function userMessageKeyOf(value: unknown) { if (value instanceof ApiFailure || value instanceof NetworkFailure) { return value.userMessageKey; } return USER_MESSAGE_KEYS.unknown; } export function httpStatusOf(value: unknown) { return value instanceof ApiFailure ? value.status : undefined; } export function isBackendStatus(value: unknown, status: BackendStatus) { return backendStatusOf(value) === status; } export function isUserMessageKey(value: unknown, key: string) { return userMessageKeyOf(value) === key; } export function toUserMessage(value: unknown, t: Translate, fallbackKey: string = USER_MESSAGE_KEYS.unknown) { const key = userMessageKeyOf(value); const message = t(key); if (message !== key) return message; return t(fallbackKey); } function parseStatusBody(body: unknown) { if (!body || typeof body !== "object") { return {}; } const data = body as BackendStatusBody; return { rawMessage: stringValue(data.message) || stringValue(data.error), errorCode: stringValue(data.errorCode), code: typeof data.code === "number" ? data.code : undefined, requestId: stringValue(data.requestId), traceId: stringValue(data.traceId) }; } function resolveUserMessageKey(backendStatus: BackendStatus, httpStatus: number, rawMessage: string | undefined) { if (typeof backendStatus === "string" && BACKEND_STATUS_MESSAGE_KEYS[backendStatus]) { return BACKEND_STATUS_MESSAGE_KEYS[backendStatus]; } for (const item of RAW_STATUS_MESSAGE_KEYS) { if (item.pattern.test(rawMessage ?? "")) return item.key; } if (typeof backendStatus === "number" && HTTP_STATUS_MESSAGE_KEYS[backendStatus]) { return HTTP_STATUS_MESSAGE_KEYS[backendStatus]; } if (HTTP_STATUS_MESSAGE_KEYS[httpStatus]) { return HTTP_STATUS_MESSAGE_KEYS[httpStatus]; } return httpStatus >= 500 ? USER_MESSAGE_KEYS.server : USER_MESSAGE_KEYS.unknown; } function stringValue(value: unknown) { return typeof value === "string" && value.trim() ? value : undefined; }