-
Notifications
You must be signed in to change notification settings - Fork 80
Expand file tree
/
Copy patherrors.ts
More file actions
287 lines (272 loc) · 8.7 KB
/
Copy patherrors.ts
File metadata and controls
287 lines (272 loc) · 8.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
/**
* Centralized application error taxonomy.
*
* All custom errors extend AppError so the global error handler can
* identify them and map them to consistent HTTP responses.
*/
export enum ErrorCode {
VALIDATION_ERROR = "VALIDATION_ERROR",
AUTHENTICATION_ERROR = "AUTHENTICATION_ERROR",
AUTHORIZATION_ERROR = "AUTHORIZATION_ERROR",
NOT_FOUND = "NOT_FOUND",
CONFLICT = "CONFLICT",
BUSINESS_RULE_VIOLATION = "BUSINESS_RULE_VIOLATION",
EXTERNAL_SERVICE_ERROR = "EXTERNAL_SERVICE_ERROR",
CONFIGURATION_ERROR = "CONFIGURATION_ERROR",
INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR",
// Domain specific errors
INVALID_CHALLENGE = "INVALID_CHALLENGE",
CHALLENGE_EXPIRED = "CHALLENGE_EXPIRED",
CHALLENGE_USED = "CHALLENGE_USED",
INVALID_SIGNATURE = "INVALID_SIGNATURE",
INSUFFICIENT_FUNDS = "INSUFFICIENT_FUNDS",
ROUND_NOT_ACTIVE = "ROUND_NOT_ACTIVE",
ROUND_LOCKED = "ROUND_LOCKED",
ROUND_ALREADY_RESOLVED = "ROUND_ALREADY_RESOLVED",
DUPLICATE_PREDICTION = "DUPLICATE_PREDICTION",
ACTIVE_ROUND_EXISTS = "ACTIVE_ROUND_EXISTS",
IDEMPOTENCY_KEY_CONFLICT = "IDEMPOTENCY_KEY_CONFLICT",
}
export interface ErrorDetail {
field: string;
message: string;
}
export class AppError extends Error {
readonly statusCode: number;
/** Machine-readable error code */
readonly code: ErrorCode | string;
readonly details?: ErrorDetail[];
constructor(
message: string,
statusCode: number,
code: ErrorCode | string,
details?: ErrorDetail[],
) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.code = code;
this.details = details;
Error.captureStackTrace(this, this.constructor);
}
}
/** 400 – request body / query / params failed schema validation */
export class ValidationError extends AppError {
constructor(message: string, details?: ErrorDetail[]) {
super(message, 400, ErrorCode.VALIDATION_ERROR, details);
}
}
/** 401 – missing or invalid credentials */
export class AuthenticationError extends AppError {
constructor(message: string, code: ErrorCode | string = ErrorCode.AUTHENTICATION_ERROR) {
super(message, 401, code);
}
}
/** 403 – authenticated but not permitted */
export class AuthorizationError extends AppError {
constructor(message: string, code: ErrorCode | string = ErrorCode.AUTHORIZATION_ERROR) {
super(message, 403, code);
}
}
/** 404 – requested resource does not exist */
export class NotFoundError extends AppError {
constructor(message: string, code: ErrorCode | string = ErrorCode.NOT_FOUND) {
super(message, 404, code);
}
}
/** 409 – request conflicts with current server state */
export class ConflictError extends AppError {
constructor(message: string, code: ErrorCode | string = ErrorCode.CONFLICT) {
super(message, 409, code);
}
}
/** 422 – business-rule violation (request was well-formed but semantically invalid) */
export class BusinessRuleError extends AppError {
constructor(message: string, code: ErrorCode | string = ErrorCode.BUSINESS_RULE_VIOLATION) {
super(message, 422, code);
}
}
/** 503 – upstream / external service failure */
export class ExternalServiceError extends AppError {
constructor(message: string, code: ErrorCode | string = ErrorCode.EXTERNAL_SERVICE_ERROR) {
super(message, 503, code);
}
}
/** 500 – misconfiguration detected at runtime */
export class ConfigurationError extends AppError {
constructor(message: string, code: ErrorCode | string = ErrorCode.CONFIGURATION_ERROR) {
super(message, 500, code);
}
}
/**
* Unified backend error code catalog (#196).
*
* Single source of truth that maps every {@link ErrorCode} to the
* HTTP status the API will use, the error class that produces it, and
* a short, human-readable description an API consumer can act on.
*
* Consumed by:
* - The README for the public error reference table.
* - The OpenAPI doc so each error appears alongside the endpoints
* that can return it.
* - The unit test that pins the catalog and prevents drift from
* the actual ErrorCode enum.
*/
export interface ErrorCatalogEntry {
/** Machine-readable code shipped on the wire in `error.code`. */
code: ErrorCode;
/** HTTP status this error maps to in responses. */
status: number;
/** Name of the AppError subclass that emits this code. */
errorClass: string;
/** Short, action-oriented description for API consumers. */
description: string;
}
export const ERROR_CATALOG: readonly ErrorCatalogEntry[] = [
{
code: ErrorCode.VALIDATION_ERROR,
status: 400,
errorClass: "ValidationError",
description:
"Request body, query string, or path params failed schema validation. " +
"Fix the offending fields described in `error.details`.",
},
{
code: ErrorCode.AUTHENTICATION_ERROR,
status: 401,
errorClass: "AuthenticationError",
description:
"Missing, malformed, or invalid credentials. Acquire a fresh JWT via " +
"the wallet challenge → connect flow and retry.",
},
{
code: ErrorCode.AUTHORIZATION_ERROR,
status: 403,
errorClass: "AuthorizationError",
description:
"Authenticated, but not permitted for this resource or action.",
},
{
code: ErrorCode.NOT_FOUND,
status: 404,
errorClass: "NotFoundError",
description: "The requested resource does not exist.",
},
{
code: ErrorCode.CONFLICT,
status: 409,
errorClass: "ConflictError",
description:
"The request conflicts with current server state. Re-read the resource " +
"and retry.",
},
{
code: ErrorCode.BUSINESS_RULE_VIOLATION,
status: 422,
errorClass: "BusinessRuleError",
description:
"Request was well-formed but violated a domain rule (e.g. round closed, " +
"duplicate prediction).",
},
{
code: ErrorCode.EXTERNAL_SERVICE_ERROR,
status: 503,
errorClass: "ExternalServiceError",
description:
"Upstream dependency (database, Soroban RPC, oracle) is unavailable or " +
"returned an unexpected error. Retry with backoff.",
},
{
code: ErrorCode.CONFIGURATION_ERROR,
status: 500,
errorClass: "ConfigurationError",
description:
"Server misconfiguration detected at runtime. Operators must intervene; " +
"client retry will not help.",
},
{
code: ErrorCode.INTERNAL_SERVER_ERROR,
status: 500,
errorClass: "AppError",
description:
"Unexpected server error. Retry; if it persists, include the response's " +
"`requestId` when filing a bug.",
},
{
code: ErrorCode.INVALID_CHALLENGE,
status: 401,
errorClass: "AuthenticationError",
description:
"The signed challenge does not match any known issued challenge. Restart " +
"the wallet sign-in from the challenge endpoint.",
},
{
code: ErrorCode.CHALLENGE_EXPIRED,
status: 401,
errorClass: "AuthenticationError",
description: "Challenge TTL has elapsed. Request a new challenge and sign it again.",
},
{
code: ErrorCode.CHALLENGE_USED,
status: 401,
errorClass: "AuthenticationError",
description:
"Challenge has already been consumed (challenges are one-shot). Request " +
"a fresh one.",
},
{
code: ErrorCode.INVALID_SIGNATURE,
status: 401,
errorClass: "AuthenticationError",
description:
"Signature does not verify against the supplied wallet address and " +
"challenge payload.",
},
{
code: ErrorCode.INSUFFICIENT_FUNDS,
status: 422,
errorClass: "BusinessRuleError",
description:
"Wallet does not have the required balance for the requested operation.",
},
{
code: ErrorCode.ROUND_NOT_ACTIVE,
status: 422,
errorClass: "BusinessRuleError",
description: "Target round is not in ACTIVE status and cannot accept this action.",
},
{
code: ErrorCode.ROUND_LOCKED,
status: 422,
errorClass: "BusinessRuleError",
description:
"Round has been locked ahead of resolution; predictions are no longer accepted.",
},
{
code: ErrorCode.ROUND_ALREADY_RESOLVED,
status: 409,
errorClass: "ConflictError",
description: "Round has already been resolved and its outcome is final.",
},
{
code: ErrorCode.DUPLICATE_PREDICTION,
status: 409,
errorClass: "ConflictError",
description: "User already has a prediction for this round.",
},
{
code: ErrorCode.ACTIVE_ROUND_EXISTS,
status: 409,
errorClass: "ConflictError",
description:
"An active round of the requested mode already exists; start cannot proceed.",
},
{
code: ErrorCode.IDEMPOTENCY_KEY_CONFLICT,
status: 409,
errorClass: "ConflictError",
description:
"The Idempotency-Key was already used for a different request payload. " +
"Retry the original payload or generate a new key.",
},
];