@boo-labs/server 1.0.1 → 1.0.2

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 (24) hide show
  1. package/package.json +1 -1
  2. package/source/core/IPCMain/IPCProtocol.mjs +80 -75
  3. package/source/shared/domain/UUIDV7/index.mjs +31 -39
  4. package/source/shared/errors/ServerError/index.mjs +48 -2
  5. package/source/shared/outcome/Result/index.mjs +77 -130
  6. package/source/shared/outcome/Validation/index.mjs +56 -114
  7. package/source/shared/validations/Middleware/index.mjs +32 -23
  8. package/source/shared/validations/PathnamePattern/index.mjs +38 -39
  9. package/source/shared/validations/Server/NotFoundHandler/index.mjs +25 -15
  10. package/source/shared/validations/Server/Router/index.mjs +24 -14
  11. package/source/shared/validations/Server/ServerConfiguration/index.mjs +24 -14
  12. package/source/shared/validations/Server/SystemErrorHandler/index.mjs +25 -15
  13. package/source/shared/validations/ServerConfiguration/CORSOptions/index.mjs +95 -91
  14. package/source/shared/validations/ServerConfiguration/CSRFOptions/index.mjs +96 -96
  15. package/source/shared/validations/ServerConfiguration/CompressOptions/index.mjs +55 -43
  16. package/source/shared/validations/ServerConfiguration/CookieParserOptions/index.mjs +28 -13
  17. package/source/shared/validations/ServerConfiguration/CookieSessionOptions/index.mjs +101 -111
  18. package/source/shared/validations/ServerConfiguration/ListenOptions/index.mjs +55 -43
  19. package/source/shared/validations/ServerConfiguration/RateLimitOptions/index.mjs +71 -61
  20. package/source/shared/validations/ServerConfiguration/RequestOptions/index.mjs +44 -31
  21. package/source/shared/validations/ServerConfiguration/ResponseOptions/index.mjs +39 -25
  22. package/source/shared/validations/ServerConfiguration/SecurityHeadersOptions/index.mjs +27 -14
  23. package/source/shared/validations/ServerConfiguration/StaticOptions/index.mjs +85 -84
  24. package/source/shared/outcome/ErrorSchema.mjs +0 -361
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boo-labs/server",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "A zero-dependency, frameworkless HTTP server for Node.js, built around Clean Architecture principles.",
5
5
  "keywords": [
6
6
  "http-server",
@@ -17,6 +17,20 @@
17
17
  import Result from '#shared/outcome/Result';
18
18
 
19
19
  // ━━ TYPE DEFINITIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
20
+ /**
21
+ * A successful {@link Result} instance.
22
+ *
23
+ * @template T
24
+ * @typedef {import('#shared/outcome/Result').SuccessResult<T>} SuccessResult
25
+ */
26
+
27
+ /**
28
+ * A failed {@link Result} instance.
29
+ *
30
+ * @template {Error} E
31
+ * @typedef {import('#shared/outcome/Result').FailureResult<E>} FailureResult
32
+ */
33
+
20
34
  /**
21
35
  * Represents an IPC invocation request.
22
36
  *
@@ -99,6 +113,18 @@ function isPlainObject(value) {
99
113
  return typeof value === 'object' && value !== null && !Array.isArray(value);
100
114
  }
101
115
 
116
+ /**
117
+ * Attaches a machine-readable code to an Error instance.
118
+ *
119
+ * @template {Error} E
120
+ * @param {E} error - Error instance to annotate.
121
+ * @param {string} code - Machine-readable error code.
122
+ * @returns {E & { code: string }} The annotated error.
123
+ */
124
+ function withCode(error, code) {
125
+ return Object.assign(error, { code });
126
+ }
127
+
102
128
  // ━━ MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
103
129
  /**
104
130
  * Implements the IPC wire protocol for decoding requests and serializing responses.
@@ -243,25 +269,19 @@ class IPCProtocol {
243
269
  * a failure describing the first structural violation encountered.
244
270
  *
245
271
  * @param {unknown} message - Parsed IPC payload.
246
- * @returns {Result<IPCRequest>} Successful result containing the normalized request, or a failure describing the validation error.
272
+ * @returns {SuccessResult<IPCRequest>|FailureResult<TypeError|ReferenceError>} Successful result containing the normalized request, or a failure describing the validation error.
247
273
  */
248
274
  #ensureRequest(message) {
249
275
  if (!isPlainObject(message)) {
250
- return Result.failure({
251
- error: TypeError,
252
- message: 'IPC message must be a plain object.',
253
- code: 'IPC_INVALID_MESSAGE',
254
- custom: false,
255
- });
276
+ return Result.failure(
277
+ withCode(new TypeError('IPC message must be a plain object.'), 'IPC_INVALID_MESSAGE'),
278
+ );
256
279
  }
257
280
 
258
281
  if (!('type' in message)) {
259
- return Result.failure({
260
- error: ReferenceError,
261
- message: 'IPC message requires a "type" property.',
262
- code: 'IPC_MISSING_TYPE',
263
- custom: false,
264
- });
282
+ return Result.failure(
283
+ withCode(new ReferenceError('IPC message requires a "type" property.'), 'IPC_MISSING_TYPE'),
284
+ );
265
285
  }
266
286
 
267
287
  if (message.type === 'invoke') {
@@ -272,73 +292,61 @@ class IPCProtocol {
272
292
  return this.#ensureEventRequest(message);
273
293
  }
274
294
 
275
- return Result.failure({
276
- error: TypeError,
277
- message: 'IPC message type must be "invoke" or "event".',
278
- code: 'IPC_INVALID_TYPE',
279
- custom: false,
280
- });
295
+ return Result.failure(
296
+ withCode(new TypeError('IPC message type must be "invoke" or "event".'), 'IPC_INVALID_TYPE'),
297
+ );
281
298
  }
282
299
 
283
300
  /**
284
301
  * Ensures a parsed IPC payload is a valid invocation request.
285
302
  *
286
303
  * @param {Record<string, unknown>} message - Parsed IPC payload.
287
- * @returns {Result<IPCInvokeRequest>} Successful result containing the normalized request, or a failure describing the validation error.
304
+ * @returns {SuccessResult<IPCInvokeRequest>|FailureResult<TypeError|ReferenceError|RangeError>} Successful result containing the normalized request, or a failure describing the validation error.
288
305
  */
289
306
  #ensureInvokeRequest(message) {
290
307
  if (!('channel' in message)) {
291
- return Result.failure({
292
- error: ReferenceError,
293
- message: 'IPC message requires a "channel" property.',
294
- code: 'IPC_MISSING_CHANNEL',
295
- custom: false,
296
- });
308
+ return Result.failure(
309
+ withCode(
310
+ new ReferenceError('IPC message requires a "channel" property.'),
311
+ 'IPC_MISSING_CHANNEL',
312
+ ),
313
+ );
297
314
  }
298
315
 
299
316
  if (typeof message.channel !== 'string') {
300
- return Result.failure({
301
- error: TypeError,
302
- message: 'IPC channel must be a string.',
303
- code: 'IPC_INVALID_CHANNEL_TYPE',
304
- custom: false,
305
- });
317
+ return Result.failure(
318
+ withCode(new TypeError('IPC channel must be a string.'), 'IPC_INVALID_CHANNEL_TYPE'),
319
+ );
306
320
  }
307
321
 
308
322
  if (message.channel.trim().length === 0) {
309
- return Result.failure({
310
- error: RangeError,
311
- message: 'IPC channel must be a non-empty string.',
312
- code: 'IPC_INVALID_CHANNEL_RANGE',
313
- custom: false,
314
- });
323
+ return Result.failure(
324
+ withCode(
325
+ new RangeError('IPC channel must be a non-empty string.'),
326
+ 'IPC_INVALID_CHANNEL_RANGE',
327
+ ),
328
+ );
315
329
  }
316
330
 
317
331
  if (!('id' in message)) {
318
- return Result.failure({
319
- error: ReferenceError,
320
- message: 'IPC invocation requires an "id" property.',
321
- code: 'IPC_MISSING_ID',
322
- custom: false,
323
- });
332
+ return Result.failure(
333
+ withCode(new ReferenceError('IPC invocation requires an "id" property.'), 'IPC_MISSING_ID'),
334
+ );
324
335
  }
325
336
 
326
337
  if (typeof message.id !== 'string') {
327
- return Result.failure({
328
- error: TypeError,
329
- message: 'IPC invocation id must be a string.',
330
- code: 'IPC_INVALID_ID_TYPE',
331
- custom: false,
332
- });
338
+ return Result.failure(
339
+ withCode(new TypeError('IPC invocation id must be a string.'), 'IPC_INVALID_ID_TYPE'),
340
+ );
333
341
  }
334
342
 
335
343
  if (message.id.trim().length === 0) {
336
- return Result.failure({
337
- error: RangeError,
338
- message: 'IPC invocation id must be a non-empty string.',
339
- code: 'IPC_INVALID_ID_RANGE',
340
- custom: false,
341
- });
344
+ return Result.failure(
345
+ withCode(
346
+ new RangeError('IPC invocation id must be a non-empty string.'),
347
+ 'IPC_INVALID_ID_RANGE',
348
+ ),
349
+ );
342
350
  }
343
351
 
344
352
  return Result.success({
@@ -353,34 +361,31 @@ class IPCProtocol {
353
361
  * Ensures a parsed IPC payload is a valid event request.
354
362
  *
355
363
  * @param {Record<string, unknown>} message - Parsed IPC payload.
356
- * @returns {Result<IPCEventRequest>} Successful result containing the normalized request, or a failure describing the validation error.
364
+ * @returns {SuccessResult<IPCEventRequest>|FailureResult<TypeError|ReferenceError|RangeError>} Successful result containing the normalized request, or a failure describing the validation error.
357
365
  */
358
366
  #ensureEventRequest(message) {
359
367
  if (!('channel' in message)) {
360
- return Result.failure({
361
- error: ReferenceError,
362
- message: 'IPC message requires a "channel" property.',
363
- code: 'IPC_MISSING_CHANNEL',
364
- custom: false,
365
- });
368
+ return Result.failure(
369
+ withCode(
370
+ new ReferenceError('IPC message requires a "channel" property.'),
371
+ 'IPC_MISSING_CHANNEL',
372
+ ),
373
+ );
366
374
  }
367
375
 
368
376
  if (typeof message.channel !== 'string') {
369
- return Result.failure({
370
- error: TypeError,
371
- message: 'IPC channel must be a string.',
372
- code: 'IPC_INVALID_CHANNEL_TYPE',
373
- custom: false,
374
- });
377
+ return Result.failure(
378
+ withCode(new TypeError('IPC channel must be a string.'), 'IPC_INVALID_CHANNEL_TYPE'),
379
+ );
375
380
  }
376
381
 
377
382
  if (message.channel.trim().length === 0) {
378
- return Result.failure({
379
- error: RangeError,
380
- message: 'IPC channel must be a non-empty string.',
381
- code: 'IPC_INVALID_CHANNEL_RANGE',
382
- custom: false,
383
- });
383
+ return Result.failure(
384
+ withCode(
385
+ new RangeError('IPC channel must be a non-empty string.'),
386
+ 'IPC_INVALID_CHANNEL_RANGE',
387
+ ),
388
+ );
384
389
  }
385
390
 
386
391
  return Result.success({
@@ -13,6 +13,20 @@
13
13
  * @module UUIDV7
14
14
  */
15
15
 
16
+ // ━━ TYPE DEFINITIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17
+ /**
18
+ * A successful {@link Validation} instance.
19
+ *
20
+ * @typedef {import('#shared/outcome/Validation').SuccessValidation} SuccessValidation
21
+ */
22
+
23
+ /**
24
+ * A failed {@link Validation} instance.
25
+ *
26
+ * @template {Error} E
27
+ * @typedef {import('#shared/outcome/Validation').FailureValidation<E>} FailureValidation
28
+ */
29
+
16
30
  // ━━ IMPORT MODULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17
31
  // » IMPORT NATIVE NODEJS MODULES
18
32
  import { randomBytes } from 'node:crypto';
@@ -42,6 +56,11 @@ const UUIDV7_REGEXP = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[
42
56
  * sortable.
43
57
  */
44
58
  class UUIDV7 {
59
+ /**
60
+ * The UUID value.
61
+ *
62
+ * @type {string}
63
+ */
45
64
  #uuid;
46
65
 
47
66
  /**
@@ -235,8 +254,9 @@ class UUIDV7 {
235
254
  * timestamp, and a UUID v7 string represents the timestamp encoded in that
236
255
  * UUID.
237
256
  *
238
- * @param {*} value - Value to validate.
239
- * @returns {Validation} Validation result.
257
+ * @param {unknown} value - Value to validate.
258
+ * @returns {SuccessValidation|FailureValidation<RangeError|TypeError>} Validation result.
259
+ *
240
260
  * @example
241
261
  * ```js
242
262
  * const result = UUIDV7.validate();
@@ -249,41 +269,27 @@ class UUIDV7 {
249
269
 
250
270
  if (value instanceof Date) {
251
271
  if (Number.isNaN(value.getTime())) {
252
- return Validation.failure({
253
- error: RangeError,
254
- message: 'Invalid timestamp.',
255
- code: 'INVALID_TIMESTAMP',
256
- custom: false,
257
- });
272
+ return Validation.failure(new RangeError('Invalid timestamp.'));
258
273
  }
259
274
  return Validation.success();
260
275
  }
261
276
 
262
277
  if (typeof value === 'string') {
263
278
  if (!UUIDV7_REGEXP.test(value)) {
264
- return Validation.failure({
265
- error: RangeError,
266
- message: 'Invalid UUID v7.',
267
- code: 'INVALID_UUID',
268
- custom: false,
269
- });
279
+ return Validation.failure(new RangeError('Invalid UUID v7.'));
270
280
  }
271
281
  return Validation.success();
272
282
  }
273
283
 
274
- return Validation.failure({
275
- error: TypeError,
276
- message: 'Timestamp must be a Date, string, or undefined.',
277
- code: 'INVALID_TIMESTAMP_SOURCE',
278
- custom: false,
279
- });
284
+ return Validation.failure(new TypeError('Timestamp must be a Date, string, or undefined.'));
280
285
  }
281
286
 
282
287
  /**
283
288
  * Validates that a value is a valid UUID v7 string.
284
289
  *
285
290
  * @param {unknown} value - Value to validate.
286
- * @returns {Validation} Validation result.
291
+ * @returns {SuccessValidation|FailureValidation<ReferenceError|RangeError|TypeError>} Validation result.
292
+ *
287
293
  * @example
288
294
  * ```js
289
295
  * const result = UUIDV7.validateUUIDString('019937f2-1234-7abc-8def-123456789abc');
@@ -291,30 +297,15 @@ class UUIDV7 {
291
297
  */
292
298
  static validateUUIDString(value) {
293
299
  if (value === undefined) {
294
- return Validation.failure({
295
- error: ReferenceError,
296
- message: 'UUID must be a string.',
297
- custom: false,
298
- code: 'MISSING_UUID',
299
- });
300
+ return Validation.failure(new ReferenceError('UUID must be a string.'));
300
301
  }
301
302
 
302
303
  if (typeof value !== 'string') {
303
- return Validation.failure({
304
- error: TypeError,
305
- message: 'UUID must be a string.',
306
- custom: false,
307
- code: 'INVALID_UUID_TYPE',
308
- });
304
+ return Validation.failure(new TypeError('UUID must be a string.'));
309
305
  }
310
306
 
311
307
  if (!UUIDV7_REGEXP.test(value)) {
312
- return Validation.failure({
313
- error: RangeError,
314
- message: 'Invalid UUID v7 format.',
315
- custom: false,
316
- code: 'INVALID_UUID',
317
- });
308
+ return Validation.failure(new RangeError('Invalid UUID v7 format.'));
318
309
  }
319
310
 
320
311
  return Validation.success();
@@ -328,6 +319,7 @@ class UUIDV7 {
328
319
  * @param {unknown} value - Value to validate.
329
320
  * @throws {TypeError} If the value has an unsupported type.
330
321
  * @throws {RangeError} If the value is invalid.
322
+ *
331
323
  * @example
332
324
  * ```js
333
325
  * UUIDV7.assert(new Date());
@@ -34,7 +34,15 @@ import BaseError from '../BaseError/index.mjs';
34
34
  * 'SERVER_CLOSED' |
35
35
  * 'SERVER_ERROR' |
36
36
  * 'SERVER_INVALID_STATE_TRANSITION' |
37
- * 'SERVER_EXCHANGE_INIT_FAILED'
37
+ * 'SERVER_EXCHANGE_INIT_FAILED' |
38
+ * 'INVALID_ROUTER_TYPE' |
39
+ * 'INVALID_ROUTER_INSTANCE' |
40
+ * 'INVALID_SYSTEM_ERROR_HANDLER_TYPE' |
41
+ * 'INVALID_SYSTEM_ERROR_HANDLER_ARITY' |
42
+ * 'INVALID_NOT_FOUND_HANDLER_TYPE' |
43
+ * 'INVALID_NOT_FOUND_HANDLER_ARITY' |
44
+ * 'INVALID_SERVER_CONFIGURATION_TYPE' |
45
+ * 'INVALID_SERVER_CONFIGURATION_INSTANCE'
38
46
  * >} ServerErrorIdentifier
39
47
  */
40
48
 
@@ -88,13 +96,29 @@ class ServerError extends BaseError {
88
96
  /**
89
97
  * Collection of predefined server error messages.
90
98
  *
91
- * Available error identifiers.
99
+ * Available error identifiers:
92
100
  *
93
101
  * - `SERVER_LISTEN_FAILED`.
94
102
  * - `SERVER_CLOSE_FAILED`.
103
+ * - `SERVER_ADAPTER_INIT_FAILED`.
104
+ * - `SERVER_ALREADY_STARTING`.
95
105
  * - `SERVER_ALREADY_LISTENING`.
106
+ * - `SERVER_ALREADY_STOPPING`.
96
107
  * - `SERVER_ALREADY_CLOSED`.
108
+ * - `SERVER_NOT_LISTENING`.
109
+ * - `SERVER_STOPPING`.
110
+ * - `SERVER_CLOSED`.
111
+ * - `SERVER_ERROR`.
97
112
  * - `SERVER_INVALID_STATE_TRANSITION`.
113
+ * - `SERVER_EXCHANGE_INIT_FAILED`.
114
+ * - `INVALID_ROUTER_TYPE`.
115
+ * - `INVALID_ROUTER_INSTANCE`.
116
+ * - `INVALID_SYSTEM_ERROR_HANDLER_TYPE`.
117
+ * - `INVALID_SYSTEM_ERROR_HANDLER_ARITY`.
118
+ * - `INVALID_NOT_FOUND_HANDLER_TYPE`.
119
+ * - `INVALID_NOT_FOUND_HANDLER_ARITY`.
120
+ * - `INVALID_SERVER_CONFIGURATION_TYPE`.
121
+ * - `INVALID_SERVER_CONFIGURATION_INSTANCE`.
98
122
  *
99
123
  * @private
100
124
  * @static
@@ -121,6 +145,28 @@ class ServerError extends BaseError {
121
145
 
122
146
  // Exchange
123
147
  ['SERVER_EXCHANGE_INIT_FAILED', 'Failed to create the exchange for the incoming request.'],
148
+
149
+ // Router
150
+ ['INVALID_ROUTER_TYPE', 'Expected a Router instance, received: {received}.'],
151
+ ['INVALID_ROUTER_INSTANCE', 'Expected a Router instance.'],
152
+
153
+ // SystemErrorHandler
154
+ ['INVALID_SYSTEM_ERROR_HANDLER_TYPE', 'Handler must be a function, received: {received}.'],
155
+ [
156
+ 'INVALID_SYSTEM_ERROR_HANDLER_ARITY',
157
+ 'Handler must declare 2 parameters, received: {received}.',
158
+ ],
159
+
160
+ // NotFoundHandler
161
+ ['INVALID_NOT_FOUND_HANDLER_TYPE', 'Handler must be a function, received: {received}.'],
162
+ ['INVALID_NOT_FOUND_HANDLER_ARITY', 'Handler must declare 1 parameter, received: {received}.'],
163
+
164
+ // ServerConfiguration
165
+ [
166
+ 'INVALID_SERVER_CONFIGURATION_TYPE',
167
+ 'Expected a ServerConfiguration instance, received: {received}.',
168
+ ],
169
+ ['INVALID_SERVER_CONFIGURATION_INSTANCE', 'Expected a ServerConfiguration instance.'],
124
170
  ]);
125
171
  }
126
172
  // ━━ EXPORT MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━