1
6
7export type ErrorCode =
8 | "VALIDATION_ERROR"
9 | "UNAUTHORIZED"
10 | "INVALID_TOKEN"
11 | "TOKEN_EXPIRED"
12 | "FORBIDDEN"
13 | "SCOPE_DENIED"
14 | "NOT_FOUND"
15 | "CONFLICT"
16 | "PRECONDITION_FAILED"
17 | "GONE"
18 | "UNPROCESSABLE_ENTITY"
19 | "RATE_LIMITED"
20 | "PAYLOAD_TOO_LARGE"
21 | "QUOTA_EXCEEDED"
22 | "AI_BUDGET_EXCEEDED"
23 | "AI_NOT_CONFIGURED"
24 | "SEARCH_UNAVAILABLE"
25 | "STORAGE_ERROR"
26
27 | "FILE_TOO_LARGE"
28 | "MIME_MISMATCH"
29 | "UPLOAD_INCOMPLETE"
30 | "UPLOAD_EXPIRED"
31 | "FILE_QUARANTINED"
32 | "SERVICE_UNAVAILABLE"
33 | "NOT_IMPLEMENTED"
34 | "UNSUPPORTED_MEDIA_TYPE"
35
36
37
38 | "WEBHOOK_SIGNATURE_INVALID"
39 | "WEBHOOK_TIMESTAMP_STALE"
40 | "WEBHOOK_RATE_LIMITED"
41 | "WEBHOOK_REPLAY_DETECTED"
42
43
44
45
46
47 | "FORMULA_TIMEOUT"
48
49
50
51
52
53 | "INVALID_GRANT"
54 | "INVALID_CLIENT"
55 | "INVALID_SCOPE"
56 | "INTERNAL_ERROR";
57
58export interface ErrorDetail {
59 field?: string;
60 issue?: string;
61 reason?: string;
62 received?: unknown;
63 retryAfterSeconds?: number;
64 [key: string]: unknown;
65}
66
67export class ApiError extends Error {
68 constructor(
69 public readonly code: ErrorCode,
70 public readonly httpStatus: number,
71 message: string,
72 public readonly details?: ErrorDetail[]
73 ) {
74 super(message);
75 this.name = "ApiError";
76 }
77
78 static validation(message = "Request validation failed.", details?: ErrorDetail[]): ApiError {
79 return new ApiError("VALIDATION_ERROR", 400, message, details);
80 }
81 static unauthorized(message = "Authentication required.", details?: ErrorDetail[]): ApiError {
82 return new ApiError("UNAUTHORIZED", 401, message, details);
83 }
84 static forbidden(message = "Permission denied.", details?: ErrorDetail[]): ApiError {
85 return new ApiError("FORBIDDEN", 403, message, details);
86 }
87 static notFound(message = "Resource not found."): ApiError {
88 return new ApiError("NOT_FOUND", 404, message);
89 }
90 static conflict(message = "Resource conflict.", details?: ErrorDetail[]): ApiError {
91 return new ApiError("CONFLICT", 409, message, details);
92 }
93
96 static unprocessable(message = "Request is not processable.", details?: ErrorDetail[]): ApiError {
97 return new ApiError("UNPROCESSABLE_ENTITY", 422, message, details);
98 }
99 static webhookSignatureInvalid(message = "Invalid webhook signature."): ApiError {
100 return new ApiError("WEBHOOK_SIGNATURE_INVALID", 401, message, [{ reason: "signature_invalid" }]);
101 }
102 static webhookTimestampStale(message = "Webhook timestamp outside the ±5 minute window."): ApiError {
103 return new ApiError("WEBHOOK_TIMESTAMP_STALE", 401, message, [{ reason: "timestamp_expired" }]);
104 }
105 static webhookRateLimited(message = "Webhook rate limit exceeded (60/min).", retryAfterSeconds = 60): ApiError {
106 return new ApiError("WEBHOOK_RATE_LIMITED", 429, message, [{ retryAfterSeconds }]);
107 }
108
110 static webhookReplayDetected(message = "Webhook replay detected (duplicate signature nonce)."): ApiError {
111 return new ApiError("WEBHOOK_REPLAY_DETECTED", 401, message, [{ reason: "replay_detected" }]);
112 }
113 static internal(message = "Internal server error."): ApiError {
114 return new ApiError("INTERNAL_ERROR", 500, message);
115 }
116}
117