zero-http 0.2.4 → 0.3.1

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.
Files changed (89) hide show
  1. package/README.md +1250 -283
  2. package/documentation/config/db.js +25 -0
  3. package/documentation/config/middleware.js +44 -0
  4. package/documentation/config/tls.js +12 -0
  5. package/documentation/controllers/cookies.js +34 -0
  6. package/documentation/controllers/tasks.js +108 -0
  7. package/documentation/full-server.js +28 -177
  8. package/documentation/models/Task.js +21 -0
  9. package/documentation/public/data/api.json +404 -24
  10. package/documentation/public/data/docs.json +1139 -0
  11. package/documentation/public/data/examples.json +80 -2
  12. package/documentation/public/data/options.json +23 -8
  13. package/documentation/public/index.html +138 -99
  14. package/documentation/public/scripts/app.js +1 -3
  15. package/documentation/public/scripts/custom-select.js +189 -0
  16. package/documentation/public/scripts/data-sections.js +233 -250
  17. package/documentation/public/scripts/playground.js +270 -0
  18. package/documentation/public/scripts/ui.js +4 -3
  19. package/documentation/public/styles.css +56 -5
  20. package/documentation/public/vendor/icons/compress.svg +17 -17
  21. package/documentation/public/vendor/icons/database.svg +21 -0
  22. package/documentation/public/vendor/icons/env.svg +21 -0
  23. package/documentation/public/vendor/icons/fetch.svg +11 -14
  24. package/documentation/public/vendor/icons/security.svg +15 -0
  25. package/documentation/public/vendor/icons/sse.svg +12 -13
  26. package/documentation/public/vendor/icons/static.svg +12 -26
  27. package/documentation/public/vendor/icons/stream.svg +7 -13
  28. package/documentation/public/vendor/icons/validate.svg +17 -0
  29. package/documentation/routes/api.js +41 -0
  30. package/documentation/routes/core.js +20 -0
  31. package/documentation/routes/playground.js +29 -0
  32. package/documentation/routes/realtime.js +49 -0
  33. package/documentation/routes/uploads.js +71 -0
  34. package/index.js +62 -1
  35. package/lib/app.js +200 -8
  36. package/lib/body/json.js +28 -5
  37. package/lib/body/multipart.js +29 -1
  38. package/lib/body/raw.js +1 -1
  39. package/lib/body/sendError.js +1 -0
  40. package/lib/body/text.js +1 -1
  41. package/lib/body/typeMatch.js +6 -2
  42. package/lib/body/urlencoded.js +5 -2
  43. package/lib/debug.js +345 -0
  44. package/lib/env/index.js +440 -0
  45. package/lib/errors.js +231 -0
  46. package/lib/http/request.js +219 -1
  47. package/lib/http/response.js +410 -6
  48. package/lib/middleware/compress.js +39 -6
  49. package/lib/middleware/cookieParser.js +237 -0
  50. package/lib/middleware/cors.js +13 -2
  51. package/lib/middleware/csrf.js +135 -0
  52. package/lib/middleware/errorHandler.js +90 -0
  53. package/lib/middleware/helmet.js +176 -0
  54. package/lib/middleware/index.js +7 -2
  55. package/lib/middleware/rateLimit.js +12 -1
  56. package/lib/middleware/requestId.js +54 -0
  57. package/lib/middleware/static.js +95 -11
  58. package/lib/middleware/timeout.js +72 -0
  59. package/lib/middleware/validator.js +257 -0
  60. package/lib/orm/adapters/json.js +215 -0
  61. package/lib/orm/adapters/memory.js +383 -0
  62. package/lib/orm/adapters/mongo.js +444 -0
  63. package/lib/orm/adapters/mysql.js +272 -0
  64. package/lib/orm/adapters/postgres.js +394 -0
  65. package/lib/orm/adapters/sql-base.js +142 -0
  66. package/lib/orm/adapters/sqlite.js +311 -0
  67. package/lib/orm/index.js +276 -0
  68. package/lib/orm/model.js +895 -0
  69. package/lib/orm/query.js +807 -0
  70. package/lib/orm/schema.js +172 -0
  71. package/lib/router/index.js +136 -47
  72. package/lib/sse/stream.js +15 -3
  73. package/lib/ws/connection.js +19 -3
  74. package/lib/ws/handshake.js +3 -0
  75. package/lib/ws/index.js +3 -1
  76. package/lib/ws/room.js +222 -0
  77. package/package.json +15 -5
  78. package/types/app.d.ts +120 -0
  79. package/types/env.d.ts +80 -0
  80. package/types/errors.d.ts +147 -0
  81. package/types/fetch.d.ts +43 -0
  82. package/types/index.d.ts +135 -0
  83. package/types/middleware.d.ts +292 -0
  84. package/types/orm.d.ts +610 -0
  85. package/types/request.d.ts +99 -0
  86. package/types/response.d.ts +142 -0
  87. package/types/router.d.ts +78 -0
  88. package/types/sse.d.ts +78 -0
  89. package/types/websocket.d.ts +119 -0
@@ -0,0 +1,440 @@
1
+ /**
2
+ * @module env
3
+ * @description Zero-dependency typed environment variable system.
4
+ * Loads `.env` files, validates against a typed schema, and
5
+ * exposes a fast accessor with built-in type coercion.
6
+ *
7
+ * Supports: string, number, boolean, integer, array, json, url, port, enum.
8
+ * Multi-environment: `.env`, `.env.local`, `.env.{NODE_ENV}`, `.env.{NODE_ENV}.local`.
9
+ *
10
+ * @example
11
+ * const { env } = require('zero-http');
12
+ *
13
+ * env.load({
14
+ * PORT: { type: 'port', default: 3000 },
15
+ * DATABASE_URL: { type: 'string', required: true },
16
+ * DEBUG: { type: 'boolean', default: false },
17
+ * ALLOWED_ORIGINS: { type: 'array', separator: ',' },
18
+ * LOG_LEVEL: { type: 'enum', values: ['debug','info','warn','error'], default: 'info' },
19
+ * });
20
+ *
21
+ * env.PORT // => 3000 (number)
22
+ * env('PORT') // => 3000
23
+ * env.DEBUG // => false (boolean)
24
+ * env.require('DATABASE_URL') // throws if missing
25
+ */
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+
29
+ // ═══════════════════════════════════════════════════════════
30
+ // .env file parser
31
+ // ═══════════════════════════════════════════════════════════
32
+
33
+ /**
34
+ * Parse a `.env` file string into key-value pairs.
35
+ * Supports `#` comments, single/double/backtick quotes, multiline values,
36
+ * inline comments, interpolation `${VAR}`, and `export` prefix.
37
+ *
38
+ * @param {string} src - Raw file contents.
39
+ * @returns {Object<string, string>} Parsed key-value pairs.
40
+ */
41
+ function parse(src)
42
+ {
43
+ const result = {};
44
+ const lines = src.replace(/\r\n?/g, '\n').split('\n');
45
+
46
+ let i = 0;
47
+ while (i < lines.length)
48
+ {
49
+ let line = lines[i].trim();
50
+ i++;
51
+
52
+ // Skip comments and blank lines
53
+ if (!line || line.startsWith('#')) continue;
54
+
55
+ // Strip optional `export ` prefix
56
+ if (line.startsWith('export ')) line = line.slice(7).trim();
57
+
58
+ const eqIdx = line.indexOf('=');
59
+ if (eqIdx === -1) continue;
60
+
61
+ const key = line.slice(0, eqIdx).trim();
62
+ let value = line.slice(eqIdx + 1).trim();
63
+
64
+ // Validate key name — only word chars + dots
65
+ if (!/^[\w.]+$/.test(key)) continue;
66
+
67
+ // Quoted values
68
+ const q = value[0];
69
+ if ((q === '"' || q === "'" || q === '`') && value.endsWith(q) && value.length >= 2)
70
+ {
71
+ value = value.slice(1, -1);
72
+ }
73
+ else if (q === '"' || q === "'" || q === '`')
74
+ {
75
+ // Multiline — read until closing quote
76
+ let multiline = value.slice(1);
77
+ while (i < lines.length)
78
+ {
79
+ const nextLine = lines[i];
80
+ i++;
81
+ if (nextLine.trimEnd().endsWith(q))
82
+ {
83
+ multiline += '\n' + nextLine.trimEnd().slice(0, -1);
84
+ break;
85
+ }
86
+ multiline += '\n' + nextLine;
87
+ }
88
+ value = multiline;
89
+ }
90
+ else
91
+ {
92
+ // Unquoted — strip inline comment
93
+ const hashIdx = value.indexOf(' #');
94
+ if (hashIdx !== -1) value = value.slice(0, hashIdx).trim();
95
+ }
96
+
97
+ // Variable interpolation: ${VAR} → process.env.VAR or already-parsed value
98
+ value = value.replace(/\$\{([^}]+)\}/g, (_, name) =>
99
+ {
100
+ return result[name] || process.env[name] || '';
101
+ });
102
+
103
+ result[key] = value;
104
+ }
105
+
106
+ return result;
107
+ }
108
+
109
+ // ═══════════════════════════════════════════════════════════
110
+ // Type coercion
111
+ // ═══════════════════════════════════════════════════════════
112
+
113
+ /**
114
+ * Coerce a string value to the specified type.
115
+ *
116
+ * @param {string} raw - Raw string value.
117
+ * @param {object} fieldDef - Schema field definition.
118
+ * @param {string} key - Variable name (for error messages).
119
+ * @returns {*} Coerced value.
120
+ * @throws {Error} When the value cannot be coerced or fails validation.
121
+ */
122
+ function coerce(raw, fieldDef, key)
123
+ {
124
+ const type = fieldDef.type || 'string';
125
+
126
+ switch (type)
127
+ {
128
+ case 'string':
129
+ {
130
+ const val = String(raw);
131
+ if (fieldDef.min !== undefined && val.length < fieldDef.min)
132
+ throw new Error(`env "${key}" must be at least ${fieldDef.min} characters`);
133
+ if (fieldDef.max !== undefined && val.length > fieldDef.max)
134
+ throw new Error(`env "${key}" must be at most ${fieldDef.max} characters`);
135
+ if (fieldDef.match && !fieldDef.match.test(val))
136
+ throw new Error(`env "${key}" does not match pattern ${fieldDef.match}`);
137
+ return val;
138
+ }
139
+ case 'number':
140
+ {
141
+ const val = Number(raw);
142
+ if (isNaN(val)) throw new Error(`env "${key}" must be a number, got "${raw}"`);
143
+ if (fieldDef.min !== undefined && val < fieldDef.min)
144
+ throw new Error(`env "${key}" must be >= ${fieldDef.min}`);
145
+ if (fieldDef.max !== undefined && val > fieldDef.max)
146
+ throw new Error(`env "${key}" must be <= ${fieldDef.max}`);
147
+ return val;
148
+ }
149
+ case 'integer':
150
+ {
151
+ const val = parseInt(raw, 10);
152
+ if (isNaN(val)) throw new Error(`env "${key}" must be an integer, got "${raw}"`);
153
+ if (fieldDef.min !== undefined && val < fieldDef.min)
154
+ throw new Error(`env "${key}" must be >= ${fieldDef.min}`);
155
+ if (fieldDef.max !== undefined && val > fieldDef.max)
156
+ throw new Error(`env "${key}" must be <= ${fieldDef.max}`);
157
+ return val;
158
+ }
159
+ case 'port':
160
+ {
161
+ const val = parseInt(raw, 10);
162
+ if (isNaN(val) || val < 0 || val > 65535)
163
+ throw new Error(`env "${key}" must be a valid port (0-65535), got "${raw}"`);
164
+ return val;
165
+ }
166
+ case 'boolean':
167
+ {
168
+ const lower = String(raw).toLowerCase().trim();
169
+ if (['true', '1', 'yes', 'on'].includes(lower)) return true;
170
+ if (['false', '0', 'no', 'off', ''].includes(lower)) return false;
171
+ throw new Error(`env "${key}" must be a boolean, got "${raw}"`);
172
+ }
173
+ case 'array':
174
+ {
175
+ const sep = fieldDef.separator || ',';
176
+ return String(raw).split(sep).map(s => s.trim()).filter(Boolean);
177
+ }
178
+ case 'json':
179
+ {
180
+ try { return JSON.parse(raw); }
181
+ catch (e) { throw new Error(`env "${key}" must be valid JSON: ${e.message}`); }
182
+ }
183
+ case 'url':
184
+ {
185
+ try { new URL(raw); return raw; }
186
+ catch (e) { throw new Error(`env "${key}" must be a valid URL, got "${raw}"`); }
187
+ }
188
+ case 'enum':
189
+ {
190
+ const values = fieldDef.values || [];
191
+ if (!values.includes(raw))
192
+ throw new Error(`env "${key}" must be one of [${values.join(', ')}], got "${raw}"`);
193
+ return raw;
194
+ }
195
+ default:
196
+ return raw;
197
+ }
198
+ }
199
+
200
+ // ═══════════════════════════════════════════════════════════
201
+ // Env store
202
+ // ═══════════════════════════════════════════════════════════
203
+
204
+ /** @type {Object<string, *>} Typed/validated values store */
205
+ const _store = {};
206
+
207
+ /** @type {Object<string, object>} Schema definitions */
208
+ let _schema = null;
209
+
210
+ /** @type {boolean} */
211
+ let _loaded = false;
212
+
213
+ /**
214
+ * Load environment variables from `.env` files and validate against a typed schema.
215
+ *
216
+ * Files are loaded in precedence order (later overrides earlier):
217
+ * 1. `.env` — shared defaults
218
+ * 2. `.env.local` — local overrides (gitignored)
219
+ * 3. `.env.{NODE_ENV}` — environment-specific (e.g. `.env.production`)
220
+ * 4. `.env.{NODE_ENV}.local` — env-specific local overrides
221
+ *
222
+ * Process environment variables (`process.env`) always take precedence.
223
+ *
224
+ * @param {Object<string, object>} [schema] - Typed schema definition.
225
+ * @param {object} [options]
226
+ * @param {string} [options.path] - Custom directory to load from (default: `process.cwd()`).
227
+ * @param {boolean} [options.override=false] - Override `process.env` with file values.
228
+ * @returns {Object<string, *>} The validated env store.
229
+ *
230
+ * @throws {Error} On validation failures (missing required vars, bad types, etc.).
231
+ */
232
+ function load(schema, options = {})
233
+ {
234
+ const dir = options.path || process.cwd();
235
+ const nodeEnv = process.env.NODE_ENV || 'development';
236
+
237
+ // Load files in precedence order
238
+ const files = [
239
+ '.env',
240
+ '.env.local',
241
+ `.env.${nodeEnv}`,
242
+ `.env.${nodeEnv}.local`,
243
+ ];
244
+
245
+ const raw = {};
246
+
247
+ for (const file of files)
248
+ {
249
+ const filePath = path.resolve(dir, file);
250
+ try
251
+ {
252
+ if (fs.existsSync(filePath))
253
+ {
254
+ const content = fs.readFileSync(filePath, 'utf8');
255
+ Object.assign(raw, parse(content));
256
+ }
257
+ }
258
+ catch (e) { /* silently skip unreadable files */ }
259
+ }
260
+
261
+ // Process.env always wins
262
+ const merged = {};
263
+ if (schema)
264
+ {
265
+ _schema = schema;
266
+ for (const key of Object.keys(schema))
267
+ {
268
+ if (process.env[key] !== undefined) merged[key] = process.env[key];
269
+ else if (raw[key] !== undefined) merged[key] = raw[key];
270
+ }
271
+ }
272
+ else
273
+ {
274
+ // No schema — load everything
275
+ Object.assign(merged, raw);
276
+ for (const key of Object.keys(raw))
277
+ {
278
+ if (process.env[key] !== undefined) merged[key] = process.env[key];
279
+ }
280
+ }
281
+
282
+ // If override is true, write file values into process.env
283
+ if (options.override)
284
+ {
285
+ for (const [k, v] of Object.entries(raw))
286
+ {
287
+ if (process.env[k] === undefined) process.env[k] = v;
288
+ }
289
+ }
290
+
291
+ // Validate and coerce
292
+ const errors = [];
293
+
294
+ if (schema)
295
+ {
296
+ for (const [key, def] of Object.entries(schema))
297
+ {
298
+ const rawVal = merged[key];
299
+
300
+ if (rawVal === undefined || rawVal === '')
301
+ {
302
+ if (def.required)
303
+ {
304
+ errors.push(`env "${key}" is required but not set`);
305
+ continue;
306
+ }
307
+ if (def.default !== undefined)
308
+ {
309
+ _store[key] = typeof def.default === 'function' ? def.default() : def.default;
310
+ }
311
+ else
312
+ {
313
+ _store[key] = undefined;
314
+ }
315
+ continue;
316
+ }
317
+
318
+ try
319
+ {
320
+ _store[key] = coerce(rawVal, def, key);
321
+ }
322
+ catch (e)
323
+ {
324
+ errors.push(e.message);
325
+ }
326
+ }
327
+ }
328
+ else
329
+ {
330
+ // No schema — store raw strings
331
+ Object.assign(_store, merged);
332
+ }
333
+
334
+ if (errors.length > 0)
335
+ {
336
+ throw new Error('Environment validation failed:\n • ' + errors.join('\n • '));
337
+ }
338
+
339
+ _loaded = true;
340
+ return _store;
341
+ }
342
+
343
+ /**
344
+ * Get a typed environment variable.
345
+ * Can also be called as `env(key)`.
346
+ *
347
+ * @param {string} key - Variable name.
348
+ * @returns {*} The typed value.
349
+ */
350
+ function get(key)
351
+ {
352
+ if (_store.hasOwnProperty(key)) return _store[key];
353
+ return process.env[key];
354
+ }
355
+
356
+ /**
357
+ * Get a required environment variable. Throws if missing.
358
+ *
359
+ * @param {string} key - Variable name.
360
+ * @returns {*} The typed value.
361
+ * @throws {Error} If the variable is not set.
362
+ */
363
+ function require_(key)
364
+ {
365
+ const val = get(key);
366
+ if (val === undefined || val === null || val === '')
367
+ {
368
+ throw new Error(`Required environment variable "${key}" is not set`);
369
+ }
370
+ return val;
371
+ }
372
+
373
+ /**
374
+ * Check if a variable is set (not undefined).
375
+ *
376
+ * @param {string} key - Variable name.
377
+ * @returns {boolean}
378
+ */
379
+ function has(key)
380
+ {
381
+ return _store.hasOwnProperty(key) || process.env.hasOwnProperty(key);
382
+ }
383
+
384
+ /**
385
+ * Get all loaded values as a plain object.
386
+ *
387
+ * @returns {Object<string, *>}
388
+ */
389
+ function all()
390
+ {
391
+ return { ..._store };
392
+ }
393
+
394
+ /**
395
+ * Reset the env store (useful for testing).
396
+ */
397
+ function reset()
398
+ {
399
+ for (const k of Object.keys(_store)) delete _store[k];
400
+ _schema = null;
401
+ _loaded = false;
402
+ }
403
+
404
+ // ═══════════════════════════════════════════════════════════
405
+ // Proxy-based accessor — env.PORT, env('PORT'), env.get('PORT')
406
+ // ═══════════════════════════════════════════════════════════
407
+
408
+ /**
409
+ * The env function — callable as `env(key)` or `env.key`.
410
+ *
411
+ * @param {string} key
412
+ * @returns {*}
413
+ */
414
+ function envFn(key)
415
+ {
416
+ return get(key);
417
+ }
418
+
419
+ // Attach methods
420
+ envFn.load = load;
421
+ envFn.get = get;
422
+ envFn.require = require_;
423
+ envFn.has = has;
424
+ envFn.all = all;
425
+ envFn.reset = reset;
426
+ envFn.parse = parse;
427
+
428
+ // Proxy for dotenv.PORT style access
429
+ const envProxy = new Proxy(envFn, {
430
+ get(target, prop)
431
+ {
432
+ // Return own methods first
433
+ if (prop in target) return target[prop];
434
+ // Then check the store
435
+ if (typeof prop === 'string') return get(prop);
436
+ return undefined;
437
+ },
438
+ });
439
+
440
+ module.exports = envProxy;
package/lib/errors.js ADDED
@@ -0,0 +1,231 @@
1
+ /**
2
+ * @module errors
3
+ * @description HTTP error classes with status codes, error codes, and structured details.
4
+ * Every error extends HttpError which carries a statusCode, code, and optional details.
5
+ */
6
+
7
+ // --- Status Text Map ---------------------------------------------
8
+
9
+ const STATUS_TEXT = {
10
+ 400: 'Bad Request',
11
+ 401: 'Unauthorized',
12
+ 402: 'Payment Required',
13
+ 403: 'Forbidden',
14
+ 404: 'Not Found',
15
+ 405: 'Method Not Allowed',
16
+ 406: 'Not Acceptable',
17
+ 408: 'Request Timeout',
18
+ 409: 'Conflict',
19
+ 410: 'Gone',
20
+ 413: 'Payload Too Large',
21
+ 415: 'Unsupported Media Type',
22
+ 418: "I'm a Teapot",
23
+ 422: 'Unprocessable Entity',
24
+ 429: 'Too Many Requests',
25
+ 500: 'Internal Server Error',
26
+ 501: 'Not Implemented',
27
+ 502: 'Bad Gateway',
28
+ 503: 'Service Unavailable',
29
+ 504: 'Gateway Timeout',
30
+ };
31
+
32
+ // --- Base HttpError ----------------------------------------------
33
+
34
+ class HttpError extends Error
35
+ {
36
+ /**
37
+ * @param {number} statusCode - HTTP status code.
38
+ * @param {string} [message] - Human-readable message.
39
+ * @param {object} [opts]
40
+ * @param {string} [opts.code] - Machine-readable error code (e.g. 'VALIDATION_FAILED').
41
+ * @param {*} [opts.details] - Extra data (field errors, debug info, etc.).
42
+ */
43
+ constructor(statusCode, message, opts = {})
44
+ {
45
+ super(message || STATUS_TEXT[statusCode] || 'Error');
46
+ this.name = this.constructor.name;
47
+ this.statusCode = statusCode;
48
+ this.code = opts.code || this._defaultCode();
49
+ if (opts.details !== undefined) this.details = opts.details;
50
+ Error.captureStackTrace(this, this.constructor);
51
+ }
52
+
53
+ /** @private */
54
+ _defaultCode()
55
+ {
56
+ return (STATUS_TEXT[this.statusCode] || 'ERROR')
57
+ .toUpperCase()
58
+ .replace(/[^A-Z0-9]+/g, '_')
59
+ .replace(/(^_|_$)/g, '');
60
+ }
61
+
62
+ /**
63
+ * Serialize for JSON responses.
64
+ * @returns {{ error: string, code: string, statusCode: number, details?: * }}
65
+ */
66
+ toJSON()
67
+ {
68
+ const obj = { error: this.message, code: this.code, statusCode: this.statusCode };
69
+ if (this.details !== undefined) obj.details = this.details;
70
+ return obj;
71
+ }
72
+ }
73
+
74
+ // --- Specific Error Classes --------------------------------------
75
+
76
+ class BadRequestError extends HttpError
77
+ {
78
+ constructor(message, opts) { super(400, message, opts); }
79
+ }
80
+
81
+ class UnauthorizedError extends HttpError
82
+ {
83
+ constructor(message, opts) { super(401, message, opts); }
84
+ }
85
+
86
+ class ForbiddenError extends HttpError
87
+ {
88
+ constructor(message, opts) { super(403, message, opts); }
89
+ }
90
+
91
+ class NotFoundError extends HttpError
92
+ {
93
+ constructor(message, opts) { super(404, message, opts); }
94
+ }
95
+
96
+ class MethodNotAllowedError extends HttpError
97
+ {
98
+ constructor(message, opts) { super(405, message, opts); }
99
+ }
100
+
101
+ class ConflictError extends HttpError
102
+ {
103
+ constructor(message, opts) { super(409, message, opts); }
104
+ }
105
+
106
+ class GoneError extends HttpError
107
+ {
108
+ constructor(message, opts) { super(410, message, opts); }
109
+ }
110
+
111
+ class PayloadTooLargeError extends HttpError
112
+ {
113
+ constructor(message, opts) { super(413, message, opts); }
114
+ }
115
+
116
+ class UnprocessableEntityError extends HttpError
117
+ {
118
+ constructor(message, opts) { super(422, message, opts); }
119
+ }
120
+
121
+ /**
122
+ * Validation error with field-level details.
123
+ */
124
+ class ValidationError extends HttpError
125
+ {
126
+ /**
127
+ * @param {string} [message] - Summary message.
128
+ * @param {object|Array} [errors] - Field errors, e.g. { email: 'required', age: 'must be >= 18' }.
129
+ * @param {object} [opts]
130
+ */
131
+ constructor(message, errors, opts = {})
132
+ {
133
+ super(422, message || 'Validation Failed', { code: 'VALIDATION_FAILED', ...opts, details: errors });
134
+ this.errors = errors || {};
135
+ }
136
+ }
137
+
138
+ class TooManyRequestsError extends HttpError
139
+ {
140
+ constructor(message, opts) { super(429, message, opts); }
141
+ }
142
+
143
+ class InternalError extends HttpError
144
+ {
145
+ constructor(message, opts) { super(500, message, opts); }
146
+ }
147
+
148
+ class NotImplementedError extends HttpError
149
+ {
150
+ constructor(message, opts) { super(501, message, opts); }
151
+ }
152
+
153
+ class BadGatewayError extends HttpError
154
+ {
155
+ constructor(message, opts) { super(502, message, opts); }
156
+ }
157
+
158
+ class ServiceUnavailableError extends HttpError
159
+ {
160
+ constructor(message, opts) { super(503, message, opts); }
161
+ }
162
+
163
+ // --- Factory -----------------------------------------------------
164
+
165
+ /**
166
+ * Create an HttpError by status code.
167
+ *
168
+ * @param {number} statusCode
169
+ * @param {string} [message]
170
+ * @param {object} [opts]
171
+ * @returns {HttpError}
172
+ *
173
+ * @example
174
+ * throw createError(404, 'User not found');
175
+ * throw createError(422, 'Invalid input', { details: { email: 'required' } });
176
+ */
177
+ function createError(statusCode, message, opts)
178
+ {
179
+ const map = {
180
+ 400: BadRequestError,
181
+ 401: UnauthorizedError,
182
+ 403: ForbiddenError,
183
+ 404: NotFoundError,
184
+ 405: MethodNotAllowedError,
185
+ 409: ConflictError,
186
+ 410: GoneError,
187
+ 413: PayloadTooLargeError,
188
+ 422: UnprocessableEntityError,
189
+ 429: TooManyRequestsError,
190
+ 500: InternalError,
191
+ 501: NotImplementedError,
192
+ 502: BadGatewayError,
193
+ 503: ServiceUnavailableError,
194
+ };
195
+
196
+ const Cls = map[statusCode];
197
+ if (Cls) return new Cls(message, opts);
198
+ return new HttpError(statusCode, message, opts);
199
+ }
200
+
201
+ /**
202
+ * Check if a value is an HttpError (or duck-typed equivalent).
203
+ * @param {*} err
204
+ * @returns {boolean}
205
+ */
206
+ function isHttpError(err)
207
+ {
208
+ if (!err || !(err instanceof Error)) return false;
209
+ return err instanceof HttpError || typeof err.statusCode === 'number';
210
+ }
211
+
212
+ module.exports = {
213
+ HttpError,
214
+ BadRequestError,
215
+ UnauthorizedError,
216
+ ForbiddenError,
217
+ NotFoundError,
218
+ MethodNotAllowedError,
219
+ ConflictError,
220
+ GoneError,
221
+ PayloadTooLargeError,
222
+ UnprocessableEntityError,
223
+ ValidationError,
224
+ TooManyRequestsError,
225
+ InternalError,
226
+ NotImplementedError,
227
+ BadGatewayError,
228
+ ServiceUnavailableError,
229
+ createError,
230
+ isHttpError,
231
+ };