/** * Typed API error with automatic HTTP response conversion. * Mirrors `apps/interfaces/api/src/error.rs`. * * Each kind maps to a status code: * - BadRequest → 400 * - Unauthorized → 401 * - NotFound → 404 * - Conflict → 409 * - TooManyRequests → 429 * - Internal → 500 * - ChatProxy → 502 */ export type ApiErrorKind = | "BadRequest" | "Unauthorized" | "NotFound" | "Conflict" | "TooManyRequests" | "Internal" | "ChatProxy"; export const HTTP_STATUS: Record = { BadRequest: 400, Unauthorized: 401, NotFound: 404, Conflict: 409, TooManyRequests: 429, Internal: 500, ChatProxy: 502, }; export class ApiError extends Error { constructor(public kind: ApiErrorKind, message: string) { super(message); this.name = "ApiError"; } get status(): number { return HTTP_STATUS[this.kind]; } static badRequest(msg: string): ApiError { return new ApiError("BadRequest", msg); } static unauthorized(msg: string): ApiError { return new ApiError("Unauthorized", msg); } static notFound(msg: string): ApiError { return new ApiError("NotFound", msg); } static conflict(msg: string): ApiError { return new ApiError("Conflict", msg); } static tooManyRequests(msg: string): ApiError { return new ApiError("TooManyRequests", msg); } static internal(msg: string): ApiError { return new ApiError("Internal", msg); } static chatProxy(msg: string): ApiError { return new ApiError("ChatProxy", msg); } /** Whether an internal error's details should be hidden from the client. */ get exposeDetails(): boolean { // Internal errors hide details; all others surface the message. return this.kind !== "Internal"; } }