@boo-labs/server 0.0.0-stage → 1.0.0
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.
- package/README.md +229 -2
- package/index.mjs +23 -0
- package/package.json +71 -4
- package/source/application/Kernel/index.mjs +1137 -0
- package/source/application/Server/index.mjs +1201 -0
- package/source/application/configurations/Http2CleartextServerConfiguration/index.mjs +168 -0
- package/source/application/configurations/Http2SecureServerConfiguration/index.mjs +172 -0
- package/source/application/configurations/HttpServerConfiguration/index.mjs +169 -0
- package/source/application/configurations/HttpsServerConfiguration/index.mjs +172 -0
- package/source/application/configurations/ServerConfiguration/Options/CORSOptions/CORSOptionsValue.mjs +158 -0
- package/source/application/configurations/ServerConfiguration/Options/CORSOptions/index.mjs +87 -0
- package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/CSRFOptionsValue.mjs +178 -0
- package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/index.mjs +80 -0
- package/source/application/configurations/ServerConfiguration/Options/CompressOptions/CompressOptionsValue.mjs +126 -0
- package/source/application/configurations/ServerConfiguration/Options/CompressOptions/index.mjs +81 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/CookieParserOptionsValue.mjs +107 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/index.mjs +82 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/CookieSessionOptionsValue.mjs +182 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/index.mjs +86 -0
- package/source/application/configurations/ServerConfiguration/Options/ListenOptions/ListenOptionsValue.mjs +95 -0
- package/source/application/configurations/ServerConfiguration/Options/ListenOptions/index.mjs +47 -0
- package/source/application/configurations/ServerConfiguration/Options/MiddlewareOption.mjs +102 -0
- package/source/application/configurations/ServerConfiguration/Options/Options.mjs +105 -0
- package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/RateLimitOptionsValue.mjs +164 -0
- package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/index.mjs +84 -0
- package/source/application/configurations/ServerConfiguration/Options/RequestOptions/RequestOptionsValue.mjs +108 -0
- package/source/application/configurations/ServerConfiguration/Options/RequestOptions/index.mjs +47 -0
- package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/ResponseOptionsValue.mjs +101 -0
- package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/index.mjs +47 -0
- package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/SecurityHeadersOptionsValue.mjs +272 -0
- package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/index.mjs +85 -0
- package/source/application/configurations/ServerConfiguration/Options/ServerOption.mjs +50 -0
- package/source/application/configurations/ServerConfiguration/Options/StaticOptions/StaticOptionsValue.mjs +164 -0
- package/source/application/configurations/ServerConfiguration/Options/StaticOptions/index.mjs +89 -0
- package/source/application/configurations/ServerConfiguration/index.mjs +781 -0
- package/source/application/resolvers/TransportResolver/index.mjs +119 -0
- package/source/application/use-cases/CloseServer/index.mjs +85 -0
- package/source/application/use-cases/ListenServer/index.mjs +98 -0
- package/source/core/EventSystem/index.mjs +110 -0
- package/source/core/Exchange/ExchangeContext.mjs +291 -0
- package/source/core/Exchange/Telemetry.mjs +193 -0
- package/source/core/Exchange/index.mjs +604 -0
- package/source/core/ExchangeLayerBuilder/index.mjs +133 -0
- package/source/core/IPCMain/IPCMain.mjs +507 -0
- package/source/core/IPCMain/IPCProtocol.mjs +441 -0
- package/source/core/IPCMain/IPCServerChannel.mjs +331 -0
- package/source/core/IPCMain/WebSocketAdapter.mjs +189 -0
- package/source/core/Request/BodyParser/BodyParser.mjs +197 -0
- package/source/core/Request/BodyParser/JSONParser.mjs +70 -0
- package/source/core/Request/BodyParser/MultipartParser.mjs +612 -0
- package/source/core/Request/BodyParser/URLEncodedParser.mjs +71 -0
- package/source/core/Request/RequestHeaders.mjs +177 -0
- package/source/core/Request/index.mjs +805 -0
- package/source/core/Response/ResponseHeaders.mjs +402 -0
- package/source/core/Response/index.mjs +1370 -0
- package/source/core/middlewares/CORSMiddleware/index.mjs +289 -0
- package/source/core/middlewares/CSRFMiddleware/index.mjs +284 -0
- package/source/core/middlewares/CompressMiddleware/index.mjs +460 -0
- package/source/core/middlewares/CookieParserMiddleware/index.mjs +194 -0
- package/source/core/middlewares/CookieSessionMiddleware/index.mjs +460 -0
- package/source/core/middlewares/RateLimitMiddleware/index.mjs +245 -0
- package/source/core/middlewares/SecurityHeadersMiddleware/index.mjs +151 -0
- package/source/core/middlewares/StaticMiddleware/index.mjs +628 -0
- package/source/core/routing/ExchangeLayer/ExchangeMatcher.mjs +127 -0
- package/source/core/routing/ExchangeLayer/index.mjs +254 -0
- package/source/core/routing/Pipeline/index.mjs +157 -0
- package/source/core/routing/Router/Route.mjs +184 -0
- package/source/core/routing/Router/index.mjs +523 -0
- package/source/core/routing/RouterRegister/OptionsMiddleware.mjs +147 -0
- package/source/core/routing/RouterRegister/index.mjs +150 -0
- package/source/core/routing/RouterSystem/index.mjs +225 -0
- package/source/core/value-objects/Address/index.mjs +281 -0
- package/source/core/value-objects/Endpoint/index.mjs +293 -0
- package/source/index.mjs +62 -0
- package/source/infrastructure/adapters/Http2Adapter/index.mjs +144 -0
- package/source/infrastructure/adapters/Http2CleartextAdapter/index.mjs +147 -0
- package/source/infrastructure/adapters/HttpAdapter/index.mjs +124 -0
- package/source/infrastructure/adapters/HttpsAdapter/index.mjs +124 -0
- package/source/infrastructure/adapters/TransportAdapter/createWebSocketOpenRequest.mjs +843 -0
- package/source/infrastructure/adapters/TransportAdapter/index.mjs +555 -0
- package/source/shared/Identifier/index.mjs +671 -0
- package/source/shared/constants/symbols/brands/index.mjs +45 -0
- package/source/shared/domain/UUIDV7/index.mjs +346 -0
- package/source/shared/errors/BaseError/index.mjs +157 -0
- package/source/shared/errors/IPCError/index.mjs +97 -0
- package/source/shared/errors/MiddlewareError/index.mjs +106 -0
- package/source/shared/errors/PathnamePatternError/index.mjs +103 -0
- package/source/shared/errors/RequestError/index.mjs +116 -0
- package/source/shared/errors/ResponseError/index.mjs +140 -0
- package/source/shared/errors/RouterError/index.mjs +90 -0
- package/source/shared/errors/ServerConfigurationError/index.mjs +551 -0
- package/source/shared/errors/ServerError/index.mjs +127 -0
- package/source/shared/errors/WebSocketError/index.mjs +58 -0
- package/source/shared/outcome/ErrorSchema.mjs +361 -0
- package/source/shared/outcome/Result/index.mjs +239 -0
- package/source/shared/outcome/Validation/index.mjs +208 -0
- package/source/shared/validations/Middleware/index.mjs +160 -0
- package/source/shared/validations/PathnamePattern/index.mjs +256 -0
- package/source/shared/validations/Server/NotFoundHandler/index.mjs +94 -0
- package/source/shared/validations/Server/Router/index.mjs +68 -0
- package/source/shared/validations/Server/ServerConfiguration/index.mjs +68 -0
- package/source/shared/validations/Server/SystemErrorHandler/index.mjs +94 -0
- package/source/shared/validations/ServerConfiguration/CORSOptions/index.mjs +310 -0
- package/source/shared/validations/ServerConfiguration/CSRFOptions/index.mjs +299 -0
- package/source/shared/validations/ServerConfiguration/CompressOptions/index.mjs +204 -0
- package/source/shared/validations/ServerConfiguration/CookieParserOptions/index.mjs +102 -0
- package/source/shared/validations/ServerConfiguration/CookieSessionOptions/index.mjs +332 -0
- package/source/shared/validations/ServerConfiguration/ListenOptions/index.mjs +178 -0
- package/source/shared/validations/ServerConfiguration/RateLimitOptions/index.mjs +229 -0
- package/source/shared/validations/ServerConfiguration/RequestOptions/index.mjs +132 -0
- package/source/shared/validations/ServerConfiguration/ResponseOptions/index.mjs +122 -0
- package/source/shared/validations/ServerConfiguration/SecurityHeadersOptions/index.mjs +137 -0
- package/source/shared/validations/ServerConfiguration/StaticOptions/index.mjs +248 -0
- 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
|
+
}
|