@chidchanun/bcp 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,516 @@
1
+ export interface HttpErrorOptions {
2
+ code?: string;
3
+ message?: string;
4
+ details?: unknown;
5
+ headers?: HeadersInit;
6
+ }
7
+
8
+ export interface HttpErrorPayload {
9
+ error: {
10
+ status: number;
11
+ code: string;
12
+ message: string;
13
+ details?: unknown;
14
+ };
15
+ }
16
+
17
+ export interface HttpErrorLike {
18
+ name: string;
19
+ status: number;
20
+ code: string;
21
+ message: string;
22
+ details?: unknown;
23
+ headers?: unknown;
24
+ }
25
+
26
+ export class HttpError
27
+ extends Error {
28
+ readonly status: number;
29
+ readonly code: string;
30
+ readonly details: unknown;
31
+ readonly headers: Headers;
32
+
33
+ constructor(
34
+ status: number,
35
+ options: HttpErrorOptions = {}
36
+ ) {
37
+ assertHttpErrorStatus(
38
+ status
39
+ );
40
+
41
+ const defaults =
42
+ getHttpErrorDefaults(
43
+ status
44
+ );
45
+
46
+ super(
47
+ options.message ??
48
+ defaults.message
49
+ );
50
+
51
+ this.name =
52
+ "HttpError";
53
+ this.status =
54
+ status;
55
+ this.code =
56
+ normalizeErrorCode(
57
+ options.code ??
58
+ defaults.code
59
+ );
60
+ this.details =
61
+ options.details;
62
+ this.headers =
63
+ new Headers(
64
+ options.headers
65
+ );
66
+ }
67
+ }
68
+
69
+ export function createHttpError(
70
+ status: number,
71
+ options: HttpErrorOptions = {}
72
+ ): HttpError {
73
+ return new HttpError(
74
+ status,
75
+ options
76
+ );
77
+ }
78
+
79
+ export function throwHttpError(
80
+ status: number,
81
+ options: HttpErrorOptions = {}
82
+ ): never {
83
+ throw createHttpError(
84
+ status,
85
+ options
86
+ );
87
+ }
88
+
89
+ export function isHttpError(
90
+ error: unknown
91
+ ): error is HttpErrorLike {
92
+ return (
93
+ error instanceof
94
+ HttpError ||
95
+ (
96
+ typeof error ===
97
+ "object" &&
98
+ error !== null &&
99
+ "name" in error &&
100
+ error.name ===
101
+ "HttpError" &&
102
+ "status" in error &&
103
+ typeof error.status ===
104
+ "number" &&
105
+ Number.isInteger(
106
+ error.status
107
+ ) &&
108
+ error.status >= 400 &&
109
+ error.status <= 599 &&
110
+ "code" in error &&
111
+ typeof error.code ===
112
+ "string" &&
113
+ "message" in error &&
114
+ typeof error.message ===
115
+ "string"
116
+ )
117
+ );
118
+ }
119
+
120
+ export function toErrorResponse(
121
+ error: unknown,
122
+ fallback: HttpErrorOptions = {}
123
+ ): Response {
124
+ const httpError =
125
+ isHttpError(
126
+ error
127
+ )
128
+ ? normalizeHttpError(
129
+ error
130
+ )
131
+ : createHttpError(
132
+ 500,
133
+ fallback
134
+ );
135
+
136
+ return createHttpErrorResponse(
137
+ httpError
138
+ );
139
+ }
140
+
141
+ export function errorResponse(
142
+ status: number,
143
+ options: HttpErrorOptions = {}
144
+ ): Response {
145
+ return createHttpErrorResponse(
146
+ createHttpError(
147
+ status,
148
+ options
149
+ )
150
+ );
151
+ }
152
+
153
+ export function badRequest(
154
+ message = "Bad Request",
155
+ details?: unknown
156
+ ): Response {
157
+ return errorResponse(
158
+ 400,
159
+ {
160
+ code:
161
+ "bad_request",
162
+ message,
163
+ details,
164
+ }
165
+ );
166
+ }
167
+
168
+ export function unauthorized(
169
+ message = "Unauthorized",
170
+ details?: unknown
171
+ ): Response {
172
+ return errorResponse(
173
+ 401,
174
+ {
175
+ code:
176
+ "unauthorized",
177
+ message,
178
+ details,
179
+ }
180
+ );
181
+ }
182
+
183
+ export function forbidden(
184
+ message = "Forbidden",
185
+ details?: unknown
186
+ ): Response {
187
+ return errorResponse(
188
+ 403,
189
+ {
190
+ code:
191
+ "forbidden",
192
+ message,
193
+ details,
194
+ }
195
+ );
196
+ }
197
+
198
+ export function notFoundResponse(
199
+ message = "Not Found",
200
+ details?: unknown
201
+ ): Response {
202
+ return errorResponse(
203
+ 404,
204
+ {
205
+ code:
206
+ "not_found",
207
+ message,
208
+ details,
209
+ }
210
+ );
211
+ }
212
+
213
+ export function conflict(
214
+ message = "Conflict",
215
+ details?: unknown
216
+ ): Response {
217
+ return errorResponse(
218
+ 409,
219
+ {
220
+ code:
221
+ "conflict",
222
+ message,
223
+ details,
224
+ }
225
+ );
226
+ }
227
+
228
+ export function unprocessableEntity(
229
+ message = "Unprocessable Entity",
230
+ details?: unknown
231
+ ): Response {
232
+ return errorResponse(
233
+ 422,
234
+ {
235
+ code:
236
+ "unprocessable_entity",
237
+ message,
238
+ details,
239
+ }
240
+ );
241
+ }
242
+
243
+ export function tooManyRequests(
244
+ message = "Too Many Requests",
245
+ options: {
246
+ details?: unknown;
247
+ retryAfter?:
248
+ | number
249
+ | string;
250
+ } = {}
251
+ ): Response {
252
+ const headers =
253
+ new Headers();
254
+
255
+ if (
256
+ options.retryAfter !==
257
+ undefined
258
+ ) {
259
+ headers.set(
260
+ "Retry-After",
261
+ String(
262
+ options.retryAfter
263
+ )
264
+ );
265
+ }
266
+
267
+ return errorResponse(
268
+ 429,
269
+ {
270
+ code:
271
+ "too_many_requests",
272
+ message,
273
+ details:
274
+ options.details,
275
+ headers,
276
+ }
277
+ );
278
+ }
279
+
280
+ export function internalServerError(
281
+ message = "Internal Server Error"
282
+ ): Response {
283
+ return errorResponse(
284
+ 500,
285
+ {
286
+ code:
287
+ "internal_server_error",
288
+ message,
289
+ }
290
+ );
291
+ }
292
+
293
+ export function serviceUnavailable(
294
+ message = "Service Unavailable",
295
+ details?: unknown
296
+ ): Response {
297
+ return errorResponse(
298
+ 503,
299
+ {
300
+ code:
301
+ "service_unavailable",
302
+ message,
303
+ details,
304
+ }
305
+ );
306
+ }
307
+
308
+ function createHttpErrorResponse(
309
+ error: HttpError
310
+ ): Response {
311
+ const payload:
312
+ HttpErrorPayload = {
313
+ error: {
314
+ status:
315
+ error.status,
316
+ code:
317
+ error.code,
318
+ message:
319
+ error.message,
320
+ },
321
+ };
322
+
323
+ if (
324
+ error.details !==
325
+ undefined
326
+ ) {
327
+ payload.error.details =
328
+ error.details;
329
+ }
330
+
331
+ const headers =
332
+ new Headers(
333
+ error.headers
334
+ );
335
+
336
+ if (
337
+ !headers.has(
338
+ "Cache-Control"
339
+ )
340
+ ) {
341
+ headers.set(
342
+ "Cache-Control",
343
+ "no-store"
344
+ );
345
+ }
346
+
347
+ return Response.json(
348
+ payload,
349
+ {
350
+ status:
351
+ error.status,
352
+ headers,
353
+ }
354
+ );
355
+ }
356
+
357
+ function normalizeHttpError(
358
+ error: HttpErrorLike
359
+ ): HttpError {
360
+ if (
361
+ error instanceof
362
+ HttpError
363
+ ) {
364
+ return error;
365
+ }
366
+
367
+ return createHttpError(
368
+ error.status,
369
+ {
370
+ code:
371
+ error.code,
372
+ message:
373
+ error.message,
374
+ details:
375
+ error.details,
376
+ headers:
377
+ error.headers instanceof
378
+ Headers
379
+ ? error.headers
380
+ : undefined,
381
+ }
382
+ );
383
+ }
384
+
385
+ function assertHttpErrorStatus(
386
+ status: number
387
+ ): void {
388
+ if (
389
+ !Number.isInteger(
390
+ status
391
+ ) ||
392
+ status < 400 ||
393
+ status > 599
394
+ ) {
395
+ throw new RangeError(
396
+ `BCP Framework: HTTP error status must be an integer from 400 to 599, received ${status}.`
397
+ );
398
+ }
399
+ }
400
+
401
+ function normalizeErrorCode(
402
+ value: string
403
+ ): string {
404
+ const normalized =
405
+ value
406
+ .trim()
407
+ .toLowerCase()
408
+ .replace(
409
+ /[^a-z0-9]+/g,
410
+ "_"
411
+ )
412
+ .replace(
413
+ /^_+|_+$/g,
414
+ ""
415
+ );
416
+
417
+ if (!normalized) {
418
+ throw new Error(
419
+ "BCP Framework: HTTP error code must not be empty."
420
+ );
421
+ }
422
+
423
+ return normalized;
424
+ }
425
+
426
+ function getHttpErrorDefaults(
427
+ status: number
428
+ ): {
429
+ code: string;
430
+ message: string;
431
+ } {
432
+ switch (status) {
433
+ case 400:
434
+ return {
435
+ code: "bad_request",
436
+ message: "Bad Request",
437
+ };
438
+ case 401:
439
+ return {
440
+ code: "unauthorized",
441
+ message: "Unauthorized",
442
+ };
443
+ case 403:
444
+ return {
445
+ code: "forbidden",
446
+ message: "Forbidden",
447
+ };
448
+ case 404:
449
+ return {
450
+ code: "not_found",
451
+ message: "Not Found",
452
+ };
453
+ case 405:
454
+ return {
455
+ code: "method_not_allowed",
456
+ message: "Method Not Allowed",
457
+ };
458
+ case 409:
459
+ return {
460
+ code: "conflict",
461
+ message: "Conflict",
462
+ };
463
+ case 413:
464
+ return {
465
+ code: "payload_too_large",
466
+ message: "Payload Too Large",
467
+ };
468
+ case 415:
469
+ return {
470
+ code: "unsupported_media_type",
471
+ message: "Unsupported Media Type",
472
+ };
473
+ case 422:
474
+ return {
475
+ code: "unprocessable_entity",
476
+ message: "Unprocessable Entity",
477
+ };
478
+ case 429:
479
+ return {
480
+ code: "too_many_requests",
481
+ message: "Too Many Requests",
482
+ };
483
+ case 500:
484
+ return {
485
+ code: "internal_server_error",
486
+ message: "Internal Server Error",
487
+ };
488
+ case 501:
489
+ return {
490
+ code: "not_implemented",
491
+ message: "Not Implemented",
492
+ };
493
+ case 502:
494
+ return {
495
+ code: "bad_gateway",
496
+ message: "Bad Gateway",
497
+ };
498
+ case 503:
499
+ return {
500
+ code: "service_unavailable",
501
+ message: "Service Unavailable",
502
+ };
503
+ case 504:
504
+ return {
505
+ code: "gateway_timeout",
506
+ message: "Gateway Timeout",
507
+ };
508
+ default:
509
+ return {
510
+ code:
511
+ `http_${status}`,
512
+ message:
513
+ `HTTP ${status}`,
514
+ };
515
+ }
516
+ }
@@ -22,6 +22,28 @@ export {
22
22
  type RedirectStatus,
23
23
  } from "../../server/src/server-response.js";
24
24
 
25
+ export {
26
+ badRequest,
27
+ conflict,
28
+ createHttpError,
29
+ errorResponse,
30
+ forbidden,
31
+ HttpError,
32
+ internalServerError,
33
+ isHttpError,
34
+ notFoundResponse,
35
+ serviceUnavailable,
36
+ throwHttpError,
37
+ tooManyRequests,
38
+ toErrorResponse,
39
+ unauthorized,
40
+ unprocessableEntity,
41
+
42
+ type HttpErrorLike,
43
+ type HttpErrorOptions,
44
+ type HttpErrorPayload,
45
+ } from "./http-error.js";
46
+
25
47
  export {
26
48
  createSession,
27
49
  createSessionToken,