@boo-labs/server 0.0.0-stage → 1.0.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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +287 -2
  3. package/index.mjs +23 -0
  4. package/package.json +71 -4
  5. package/source/application/Kernel/index.mjs +1137 -0
  6. package/source/application/Server/index.mjs +1201 -0
  7. package/source/application/configurations/Http2CleartextServerConfiguration/index.mjs +168 -0
  8. package/source/application/configurations/Http2SecureServerConfiguration/index.mjs +172 -0
  9. package/source/application/configurations/HttpServerConfiguration/index.mjs +169 -0
  10. package/source/application/configurations/HttpsServerConfiguration/index.mjs +172 -0
  11. package/source/application/configurations/ServerConfiguration/Options/CORSOptions/CORSOptionsValue.mjs +158 -0
  12. package/source/application/configurations/ServerConfiguration/Options/CORSOptions/index.mjs +87 -0
  13. package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/CSRFOptionsValue.mjs +178 -0
  14. package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/index.mjs +80 -0
  15. package/source/application/configurations/ServerConfiguration/Options/CompressOptions/CompressOptionsValue.mjs +126 -0
  16. package/source/application/configurations/ServerConfiguration/Options/CompressOptions/index.mjs +81 -0
  17. package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/CookieParserOptionsValue.mjs +107 -0
  18. package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/index.mjs +82 -0
  19. package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/CookieSessionOptionsValue.mjs +182 -0
  20. package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/index.mjs +86 -0
  21. package/source/application/configurations/ServerConfiguration/Options/ListenOptions/ListenOptionsValue.mjs +95 -0
  22. package/source/application/configurations/ServerConfiguration/Options/ListenOptions/index.mjs +47 -0
  23. package/source/application/configurations/ServerConfiguration/Options/MiddlewareOption.mjs +102 -0
  24. package/source/application/configurations/ServerConfiguration/Options/Options.mjs +105 -0
  25. package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/RateLimitOptionsValue.mjs +164 -0
  26. package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/index.mjs +84 -0
  27. package/source/application/configurations/ServerConfiguration/Options/RequestOptions/RequestOptionsValue.mjs +108 -0
  28. package/source/application/configurations/ServerConfiguration/Options/RequestOptions/index.mjs +47 -0
  29. package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/ResponseOptionsValue.mjs +101 -0
  30. package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/index.mjs +47 -0
  31. package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/SecurityHeadersOptionsValue.mjs +272 -0
  32. package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/index.mjs +85 -0
  33. package/source/application/configurations/ServerConfiguration/Options/ServerOption.mjs +50 -0
  34. package/source/application/configurations/ServerConfiguration/Options/StaticOptions/StaticOptionsValue.mjs +164 -0
  35. package/source/application/configurations/ServerConfiguration/Options/StaticOptions/index.mjs +89 -0
  36. package/source/application/configurations/ServerConfiguration/index.mjs +781 -0
  37. package/source/application/resolvers/TransportResolver/index.mjs +119 -0
  38. package/source/application/use-cases/CloseServer/index.mjs +85 -0
  39. package/source/application/use-cases/ListenServer/index.mjs +98 -0
  40. package/source/core/EventSystem/index.mjs +110 -0
  41. package/source/core/Exchange/ExchangeContext.mjs +291 -0
  42. package/source/core/Exchange/Telemetry.mjs +193 -0
  43. package/source/core/Exchange/index.mjs +604 -0
  44. package/source/core/ExchangeLayerBuilder/index.mjs +133 -0
  45. package/source/core/IPCMain/IPCMain.mjs +507 -0
  46. package/source/core/IPCMain/IPCProtocol.mjs +441 -0
  47. package/source/core/IPCMain/IPCServerChannel.mjs +331 -0
  48. package/source/core/IPCMain/WebSocketAdapter.mjs +189 -0
  49. package/source/core/Request/BodyParser/BodyParser.mjs +197 -0
  50. package/source/core/Request/BodyParser/JSONParser.mjs +70 -0
  51. package/source/core/Request/BodyParser/MultipartParser.mjs +612 -0
  52. package/source/core/Request/BodyParser/URLEncodedParser.mjs +71 -0
  53. package/source/core/Request/RequestHeaders.mjs +177 -0
  54. package/source/core/Request/index.mjs +805 -0
  55. package/source/core/Response/ResponseHeaders.mjs +402 -0
  56. package/source/core/Response/index.mjs +1370 -0
  57. package/source/core/middlewares/CORSMiddleware/index.mjs +289 -0
  58. package/source/core/middlewares/CSRFMiddleware/index.mjs +284 -0
  59. package/source/core/middlewares/CompressMiddleware/index.mjs +460 -0
  60. package/source/core/middlewares/CookieParserMiddleware/index.mjs +194 -0
  61. package/source/core/middlewares/CookieSessionMiddleware/index.mjs +460 -0
  62. package/source/core/middlewares/RateLimitMiddleware/index.mjs +245 -0
  63. package/source/core/middlewares/SecurityHeadersMiddleware/index.mjs +151 -0
  64. package/source/core/middlewares/StaticMiddleware/index.mjs +628 -0
  65. package/source/core/routing/ExchangeLayer/ExchangeMatcher.mjs +127 -0
  66. package/source/core/routing/ExchangeLayer/index.mjs +254 -0
  67. package/source/core/routing/Pipeline/index.mjs +157 -0
  68. package/source/core/routing/Router/Route.mjs +184 -0
  69. package/source/core/routing/Router/index.mjs +523 -0
  70. package/source/core/routing/RouterRegister/OptionsMiddleware.mjs +147 -0
  71. package/source/core/routing/RouterRegister/index.mjs +150 -0
  72. package/source/core/routing/RouterSystem/index.mjs +225 -0
  73. package/source/core/value-objects/Address/index.mjs +281 -0
  74. package/source/core/value-objects/Endpoint/index.mjs +293 -0
  75. package/source/index.mjs +62 -0
  76. package/source/infrastructure/adapters/Http2Adapter/index.mjs +144 -0
  77. package/source/infrastructure/adapters/Http2CleartextAdapter/index.mjs +147 -0
  78. package/source/infrastructure/adapters/HttpAdapter/index.mjs +124 -0
  79. package/source/infrastructure/adapters/HttpsAdapter/index.mjs +124 -0
  80. package/source/infrastructure/adapters/TransportAdapter/createWebSocketOpenRequest.mjs +843 -0
  81. package/source/infrastructure/adapters/TransportAdapter/index.mjs +555 -0
  82. package/source/shared/Identifier/index.mjs +671 -0
  83. package/source/shared/constants/symbols/brands/index.mjs +45 -0
  84. package/source/shared/domain/UUIDV7/index.mjs +346 -0
  85. package/source/shared/errors/BaseError/index.mjs +157 -0
  86. package/source/shared/errors/IPCError/index.mjs +97 -0
  87. package/source/shared/errors/MiddlewareError/index.mjs +106 -0
  88. package/source/shared/errors/PathnamePatternError/index.mjs +103 -0
  89. package/source/shared/errors/RequestError/index.mjs +116 -0
  90. package/source/shared/errors/ResponseError/index.mjs +140 -0
  91. package/source/shared/errors/RouterError/index.mjs +90 -0
  92. package/source/shared/errors/ServerConfigurationError/index.mjs +551 -0
  93. package/source/shared/errors/ServerError/index.mjs +127 -0
  94. package/source/shared/errors/WebSocketError/index.mjs +58 -0
  95. package/source/shared/outcome/ErrorSchema.mjs +361 -0
  96. package/source/shared/outcome/Result/index.mjs +239 -0
  97. package/source/shared/outcome/Validation/index.mjs +208 -0
  98. package/source/shared/validations/Middleware/index.mjs +160 -0
  99. package/source/shared/validations/PathnamePattern/index.mjs +256 -0
  100. package/source/shared/validations/Server/NotFoundHandler/index.mjs +94 -0
  101. package/source/shared/validations/Server/Router/index.mjs +68 -0
  102. package/source/shared/validations/Server/ServerConfiguration/index.mjs +68 -0
  103. package/source/shared/validations/Server/SystemErrorHandler/index.mjs +94 -0
  104. package/source/shared/validations/ServerConfiguration/CORSOptions/index.mjs +310 -0
  105. package/source/shared/validations/ServerConfiguration/CSRFOptions/index.mjs +299 -0
  106. package/source/shared/validations/ServerConfiguration/CompressOptions/index.mjs +204 -0
  107. package/source/shared/validations/ServerConfiguration/CookieParserOptions/index.mjs +102 -0
  108. package/source/shared/validations/ServerConfiguration/CookieSessionOptions/index.mjs +332 -0
  109. package/source/shared/validations/ServerConfiguration/ListenOptions/index.mjs +178 -0
  110. package/source/shared/validations/ServerConfiguration/RateLimitOptions/index.mjs +229 -0
  111. package/source/shared/validations/ServerConfiguration/RequestOptions/index.mjs +132 -0
  112. package/source/shared/validations/ServerConfiguration/ResponseOptions/index.mjs +122 -0
  113. package/source/shared/validations/ServerConfiguration/SecurityHeadersOptions/index.mjs +137 -0
  114. package/source/shared/validations/ServerConfiguration/StaticOptions/index.mjs +248 -0
  115. package/types/index.d.ts +719 -0
@@ -0,0 +1,332 @@
1
+ // @ts-check
2
+ /**
3
+ * @file Cookie session options validation utilities.
4
+ *
5
+ * @description Validates cookie session middleware options used by
6
+ * ServerConfiguration. Enforces the dependency on a configured cookie
7
+ * parser and the SameSite=None → Secure relationship.
8
+ *
9
+ * @module Validation/ServerConfiguration/CookieSessionOptions
10
+ */
11
+
12
+ // ━━ IMPORT MODULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13
+ // » IMPORT LOCAL MODULES
14
+ import Validation from '#shared/outcome/Validation';
15
+ import ServerConfigurationError from '#shared/errors/ServerConfigurationError';
16
+
17
+ // ━━ CONSTANTS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18
+ /**
19
+ * Valid values for the SameSite cookie attribute.
20
+ *
21
+ * @private
22
+ * @type {readonly string[]}
23
+ */
24
+ const SAME_SITE_VALUES = Object.freeze(['Strict', 'Lax', 'None']);
25
+
26
+ // ━━ FUNCTIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
27
+ /**
28
+ * Determines whether a value is a non-null plain object.
29
+ *
30
+ * @private
31
+ * @param {unknown} value - Value to inspect.
32
+ * @returns {value is Record<string, unknown>} `true` if the value is a plain object.
33
+ */
34
+ function isPlainObject(value) {
35
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
36
+ }
37
+
38
+ /**
39
+ * Determines whether a value is a non-empty string.
40
+ *
41
+ * @private
42
+ * @param {unknown} value - Value to inspect.
43
+ * @returns {value is string} `true` if the value is a non-empty string.
44
+ */
45
+ function isNonEmptyString(value) {
46
+ return typeof value === 'string' && value.length > 0;
47
+ }
48
+
49
+ /**
50
+ * Determines whether every item in an array is a non-empty string.
51
+ *
52
+ * @private
53
+ * @param {unknown[]} values - Array of values to inspect.
54
+ * @returns {boolean} `true` if every item is a non-empty string.
55
+ */
56
+ function everyNonEmptyString(values) {
57
+ return values.every(isNonEmptyString);
58
+ }
59
+
60
+ /**
61
+ * Validates cookie session options.
62
+ *
63
+ * Validation is performed in the following order:
64
+ *
65
+ * 1. `options` may be `undefined`, in which case validation succeeds.
66
+ * 2. `options` must be a plain object.
67
+ * 3. `hasParser` must be `true`; cookie session requires a cookie parser.
68
+ * 4. `name`, if present, must be a non-empty string.
69
+ * 5. `keys` is required when `options` is present and must be a non-empty array of non-empty strings.
70
+ * 6. `maxAge`, if present, must be a positive integer.
71
+ * 7. `path`, if present, must be a non-empty string.
72
+ * 8. `domain`, if present, must be a non-empty string.
73
+ * 9. `secure`, if present, must be a boolean.
74
+ * 10. `httpOnly`, if present, must be a boolean.
75
+ * 11. `sameSite`, if present, must be `'Strict'`, `'Lax'`, or `'None'`.
76
+ * 12. `overwrite`, if present, must be a boolean.
77
+ * 13. `sameSite: 'None'` requires an effective `secure: true`.
78
+ *
79
+ * @param {unknown} options - Cookie session options to validate.
80
+ * @param {boolean} [hasParser] - Whether a cookie parser is configured.
81
+ * @returns {Validation} Validation result.
82
+ * @example
83
+ * ```js
84
+ * const validation = validateCookieSessionOptions({ keys: ['secret'] }, true);
85
+ *
86
+ * console.log(validation.ok);
87
+ * // => true
88
+ * ```
89
+ *
90
+ * @example
91
+ * ```js
92
+ * const validation = validateCookieSessionOptions({ keys: ['secret'] }, false);
93
+ *
94
+ * console.log(validation.ok);
95
+ * // => false
96
+ * ```
97
+ */
98
+ export function validateCookieSessionOptions(options, hasParser) {
99
+ if (options === undefined) {
100
+ return Validation.success();
101
+ }
102
+
103
+ if (!isPlainObject(options)) {
104
+ return Validation.failure({
105
+ error: ServerConfigurationError,
106
+ code: 'INVALID_COOKIE_SESSION_OPTIONS',
107
+ custom: true,
108
+ options: { values: { received: typeof options } },
109
+ });
110
+ }
111
+
112
+ if (hasParser !== true) {
113
+ return Validation.failure({
114
+ error: ServerConfigurationError,
115
+ code: 'COOKIE_SESSION_REQUIRES_COOKIE_PARSER',
116
+ custom: true,
117
+ });
118
+ }
119
+
120
+ if ('name' in options) {
121
+ const { name } = options;
122
+
123
+ if (typeof name !== 'string') {
124
+ return Validation.failure({
125
+ error: ServerConfigurationError,
126
+ code: 'INVALID_COOKIE_SESSION_NAME_TYPE',
127
+ custom: true,
128
+ options: { values: { received: typeof name } },
129
+ });
130
+ }
131
+
132
+ if (name.length === 0) {
133
+ return Validation.failure({
134
+ error: ServerConfigurationError,
135
+ code: 'INVALID_COOKIE_SESSION_NAME_VALUE',
136
+ custom: true,
137
+ options: { details: { name } },
138
+ });
139
+ }
140
+ }
141
+
142
+ const { keys } = options;
143
+
144
+ if (keys === undefined) {
145
+ return Validation.failure({
146
+ error: ServerConfigurationError,
147
+ code: 'MISSING_COOKIE_SESSION_KEYS',
148
+ custom: true,
149
+ });
150
+ }
151
+
152
+ if (!Array.isArray(keys)) {
153
+ return Validation.failure({
154
+ error: ServerConfigurationError,
155
+ code: 'INVALID_COOKIE_SESSION_KEYS_TYPE',
156
+ custom: true,
157
+ options: { values: { received: typeof keys } },
158
+ });
159
+ }
160
+
161
+ if (keys.length === 0) {
162
+ return Validation.failure({
163
+ error: ServerConfigurationError,
164
+ code: 'MISSING_COOKIE_SESSION_KEYS',
165
+ custom: true,
166
+ });
167
+ }
168
+
169
+ if (!everyNonEmptyString(keys)) {
170
+ return Validation.failure({
171
+ error: ServerConfigurationError,
172
+ code: 'INVALID_COOKIE_SESSION_KEYS_ITEM',
173
+ custom: true,
174
+ options: { details: { keys } },
175
+ });
176
+ }
177
+
178
+ if ('maxAge' in options) {
179
+ const { maxAge } = options;
180
+
181
+ if (typeof maxAge !== 'number') {
182
+ return Validation.failure({
183
+ error: ServerConfigurationError,
184
+ code: 'INVALID_COOKIE_SESSION_MAX_AGE_TYPE',
185
+ custom: true,
186
+ options: { values: { received: typeof maxAge } },
187
+ });
188
+ }
189
+
190
+ if (!Number.isInteger(maxAge) || maxAge <= 0) {
191
+ return Validation.failure({
192
+ error: ServerConfigurationError,
193
+ code: 'INVALID_COOKIE_SESSION_MAX_AGE_RANGE',
194
+ custom: true,
195
+ options: { details: { maxAge } },
196
+ });
197
+ }
198
+ }
199
+
200
+ if ('path' in options) {
201
+ const { path } = options;
202
+
203
+ if (typeof path !== 'string') {
204
+ return Validation.failure({
205
+ error: ServerConfigurationError,
206
+ code: 'INVALID_COOKIE_SESSION_PATH_TYPE',
207
+ custom: true,
208
+ options: { values: { received: typeof path } },
209
+ });
210
+ }
211
+
212
+ if (path.length === 0) {
213
+ return Validation.failure({
214
+ error: ServerConfigurationError,
215
+ code: 'INVALID_COOKIE_SESSION_PATH_VALUE',
216
+ custom: true,
217
+ options: { details: { path } },
218
+ });
219
+ }
220
+ }
221
+
222
+ if ('domain' in options) {
223
+ const { domain } = options;
224
+
225
+ if (typeof domain !== 'string') {
226
+ return Validation.failure({
227
+ error: ServerConfigurationError,
228
+ code: 'INVALID_COOKIE_SESSION_DOMAIN_TYPE',
229
+ custom: true,
230
+ options: { values: { received: typeof domain } },
231
+ });
232
+ }
233
+
234
+ if (domain.length === 0) {
235
+ return Validation.failure({
236
+ error: ServerConfigurationError,
237
+ code: 'INVALID_COOKIE_SESSION_DOMAIN_VALUE',
238
+ custom: true,
239
+ options: { details: { domain } },
240
+ });
241
+ }
242
+ }
243
+
244
+ if ('secure' in options) {
245
+ const { secure } = options;
246
+
247
+ if (typeof secure !== 'boolean') {
248
+ return Validation.failure({
249
+ error: ServerConfigurationError,
250
+ code: 'INVALID_COOKIE_SESSION_SECURE_TYPE',
251
+ custom: true,
252
+ options: { values: { received: typeof secure } },
253
+ });
254
+ }
255
+ }
256
+
257
+ if ('httpOnly' in options) {
258
+ const { httpOnly } = options;
259
+
260
+ if (typeof httpOnly !== 'boolean') {
261
+ return Validation.failure({
262
+ error: ServerConfigurationError,
263
+ code: 'INVALID_COOKIE_SESSION_HTTP_ONLY_TYPE',
264
+ custom: true,
265
+ options: { values: { received: typeof httpOnly } },
266
+ });
267
+ }
268
+ }
269
+
270
+ if ('sameSite' in options) {
271
+ const { sameSite } = options;
272
+
273
+ if (sameSite !== undefined && !SAME_SITE_VALUES.includes(String(sameSite))) {
274
+ return Validation.failure({
275
+ error: ServerConfigurationError,
276
+ code: 'INVALID_COOKIE_SESSION_SAME_SITE_VALUE',
277
+ custom: true,
278
+ options: { details: { sameSite } },
279
+ });
280
+ }
281
+ }
282
+
283
+ if ('overwrite' in options) {
284
+ const { overwrite } = options;
285
+
286
+ if (typeof overwrite !== 'boolean') {
287
+ return Validation.failure({
288
+ error: ServerConfigurationError,
289
+ code: 'INVALID_COOKIE_SESSION_OVERWRITE_TYPE',
290
+ custom: true,
291
+ options: { values: { received: typeof overwrite } },
292
+ });
293
+ }
294
+ }
295
+
296
+ // Effective values (after defaults) are used for the cross-field rule.
297
+ const effectiveSameSite = options.sameSite ?? 'Lax';
298
+ const effectiveSecure = options.secure ?? true;
299
+
300
+ if (effectiveSameSite === 'None' && effectiveSecure === false) {
301
+ return Validation.failure({
302
+ error: ServerConfigurationError,
303
+ code: 'COOKIE_SESSION_NONE_REQUIRES_SECURE',
304
+ custom: true,
305
+ });
306
+ }
307
+
308
+ return Validation.success();
309
+ }
310
+
311
+ /**
312
+ * Asserts that cookie session options are valid.
313
+ *
314
+ * @param {unknown} options - Cookie session options to validate.
315
+ * @param {boolean} [hasParser] - Whether a cookie parser is configured.
316
+ * @throws {ServerConfigurationError} If `options` is not a plain object.
317
+ * @throws {ServerConfigurationError} If no cookie parser is configured.
318
+ * @throws {ServerConfigurationError} If `keys` is missing or invalid.
319
+ * @throws {ServerConfigurationError} If any other option is invalid.
320
+ * @throws {ServerConfigurationError} If `sameSite: 'None'` is used without `secure: true`.
321
+ * @example
322
+ * ```js
323
+ * assertCookieSessionOptions({ keys: ['secret'] }, true);
324
+ * ```
325
+ */
326
+ export function assertCookieSessionOptions(options, hasParser) {
327
+ const validation = validateCookieSessionOptions(options, hasParser);
328
+
329
+ if (!validation.ok) {
330
+ throw validation.error;
331
+ }
332
+ }
@@ -0,0 +1,178 @@
1
+ // @ts-check
2
+ /**
3
+ * @file Listen options validation utilities.
4
+ *
5
+ * @description Validates listen options used by ServerConfiguration,
6
+ * including port range, host presence, and backlog constraints.
7
+ *
8
+ * @module Validation/ServerConfiguration/ListenOptions
9
+ */
10
+
11
+ // ━━ IMPORT MODULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12
+ // » IMPORT LOCAL MODULES
13
+ import Validation from '#shared/outcome/Validation';
14
+ import ServerConfigurationError from '#shared/errors/ServerConfigurationError';
15
+
16
+ // ━━ CONSTANTS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17
+ /**
18
+ * Minimum valid TCP port.
19
+ *
20
+ * @private
21
+ * @type {number}
22
+ */
23
+ const MIN_PORT = 0;
24
+
25
+ /**
26
+ * Maximum valid TCP port.
27
+ *
28
+ * @private
29
+ * @type {number}
30
+ */
31
+ const MAX_PORT = 65535;
32
+
33
+ // ━━ FUNCTIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
34
+ /**
35
+ * Determines whether a value is a non-null plain object.
36
+ *
37
+ * @private
38
+ * @param {unknown} value - Value to inspect.
39
+ * @returns {value is Record<string, unknown>} `true` if the value is a plain object.
40
+ */
41
+ function isPlainObject(value) {
42
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
43
+ }
44
+
45
+ /**
46
+ * Validates listen options.
47
+ *
48
+ * Validation is performed in the following order:
49
+ *
50
+ * 1. `options` may be `undefined`, in which case validation succeeds.
51
+ * 2. `options` must be a plain object.
52
+ * 3. `port`, if present, must be an integer between 0 and 65535.
53
+ * 4. `host`, if present, must be a non-empty string.
54
+ * 5. `backlog`, if present, must be a number.
55
+ *
56
+ * @param {unknown} options - Listen options to validate.
57
+ * @returns {Validation} Validation result.
58
+ * @example
59
+ * ```js
60
+ * const validation = validateListenOptions({ port: 3000 });
61
+ *
62
+ * console.log(validation.ok);
63
+ * // => true
64
+ * ```
65
+ *
66
+ * @example
67
+ * ```js
68
+ * const validation = validateListenOptions({});
69
+ *
70
+ * console.log(validation.ok);
71
+ * // => false
72
+ * ```
73
+ */
74
+ export function validateListenOptions(options) {
75
+ if (options === undefined) {
76
+ return Validation.success();
77
+ }
78
+
79
+ if (!isPlainObject(options)) {
80
+ return Validation.failure({
81
+ error: ServerConfigurationError,
82
+ code: 'INVALID_LISTEN_OPTIONS',
83
+ custom: true,
84
+ options: { values: { received: typeof options } },
85
+ });
86
+ }
87
+
88
+ if ('port' in options) {
89
+ const { port } = options;
90
+
91
+ if (typeof port !== 'number') {
92
+ return Validation.failure({
93
+ error: ServerConfigurationError,
94
+ code: 'INVALID_PORT_TYPE',
95
+ custom: true,
96
+ options: { values: { received: typeof port } },
97
+ });
98
+ }
99
+
100
+ if (!Number.isInteger(port) || port < MIN_PORT || port > MAX_PORT) {
101
+ return Validation.failure({
102
+ error: ServerConfigurationError,
103
+ code: 'INVALID_PORT_RANGE',
104
+ custom: true,
105
+ options: { details: { port } },
106
+ });
107
+ }
108
+ }
109
+
110
+ if ('host' in options) {
111
+ const { host } = options;
112
+
113
+ if (typeof host !== 'string') {
114
+ return Validation.failure({
115
+ error: ServerConfigurationError,
116
+ code: 'INVALID_HOST_TYPE',
117
+ custom: true,
118
+ options: { values: { received: typeof host } },
119
+ });
120
+ }
121
+
122
+ if (host.length === 0) {
123
+ return Validation.failure({
124
+ error: ServerConfigurationError,
125
+ code: 'INVALID_HOST_VALUE',
126
+ custom: true,
127
+ options: { details: { host } },
128
+ });
129
+ }
130
+ }
131
+
132
+ if ('backlog' in options) {
133
+ const { backlog } = options;
134
+
135
+ if (typeof backlog !== 'number') {
136
+ return Validation.failure({
137
+ error: ServerConfigurationError,
138
+ code: 'INVALID_BACKLOG_TYPE',
139
+ custom: true,
140
+ options: { values: { received: typeof backlog } },
141
+ });
142
+ }
143
+
144
+ if (!Number.isInteger(backlog) || backlog <= 0) {
145
+ return Validation.failure({
146
+ error: ServerConfigurationError,
147
+ code: 'INVALID_BACKLOG_RANGE',
148
+ custom: true,
149
+ options: { details: { backlog } },
150
+ });
151
+ }
152
+ }
153
+
154
+ return Validation.success();
155
+ }
156
+
157
+ /**
158
+ * Asserts that listen options are valid.
159
+ *
160
+ * @param {unknown} options - Listen options to validate.
161
+ * @throws {ServerConfigurationError} If `options` is not a plain object.
162
+ * @throws {ServerConfigurationError} If `port` is not a number.
163
+ * @throws {ServerConfigurationError} If `port` is outside the range 0–65535 or is not an integer.
164
+ * @throws {ServerConfigurationError} If `host` is not a string.
165
+ * @throws {ServerConfigurationError} If `host` is an empty string.
166
+ * @throws {ServerConfigurationError} If `backlog` is not a number.
167
+ * @example
168
+ * ```js
169
+ * assertListenOptions({ port: 3000, host: '127.0.0.1' });
170
+ * ```
171
+ */
172
+ export function assertListenOptions(options) {
173
+ const validation = validateListenOptions(options);
174
+
175
+ if (!validation.ok) {
176
+ throw validation.error;
177
+ }
178
+ }