@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.
- package/LICENSE +21 -0
- package/README.md +287 -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,299 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file CSRF options validation utilities.
|
|
4
|
+
*
|
|
5
|
+
* @description Validates CSRF middleware options used by
|
|
6
|
+
* ServerConfiguration. Enforces the SameSite=None → Secure relationship.
|
|
7
|
+
*
|
|
8
|
+
* @module Validation/ServerConfiguration/CSRFOptions
|
|
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
|
+
* Valid values for the SameSite cookie attribute.
|
|
19
|
+
*
|
|
20
|
+
* @private
|
|
21
|
+
* @type {readonly string[]}
|
|
22
|
+
*/
|
|
23
|
+
const SAME_SITE_VALUES = Object.freeze(['Strict', 'Lax', 'None']);
|
|
24
|
+
|
|
25
|
+
// ━━ FUNCTIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
26
|
+
/**
|
|
27
|
+
* Determines whether a value is a non-null plain object.
|
|
28
|
+
*
|
|
29
|
+
* @private
|
|
30
|
+
* @param {unknown} value - Value to inspect.
|
|
31
|
+
* @returns {value is Record<string, unknown>} `true` if the value is a plain object.
|
|
32
|
+
*/
|
|
33
|
+
function isPlainObject(value) {
|
|
34
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Determines whether a value is a non-empty string.
|
|
39
|
+
*
|
|
40
|
+
* @private
|
|
41
|
+
* @param {unknown} value - Value to inspect.
|
|
42
|
+
* @returns {value is string} `true` if the value is a non-empty string.
|
|
43
|
+
*/
|
|
44
|
+
function isNonEmptyString(value) {
|
|
45
|
+
return typeof value === 'string' && value.length > 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Determines whether every item in an array is a non-empty string.
|
|
50
|
+
*
|
|
51
|
+
* @private
|
|
52
|
+
* @param {unknown[]} values - Array of values to inspect.
|
|
53
|
+
* @returns {boolean} `true` if every item is a non-empty string.
|
|
54
|
+
*/
|
|
55
|
+
function everyNonEmptyString(values) {
|
|
56
|
+
return values.every(isNonEmptyString);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validates CSRF options.
|
|
61
|
+
*
|
|
62
|
+
* Validation is performed in the following order:
|
|
63
|
+
*
|
|
64
|
+
* 1. `options` may be `undefined`, in which case validation succeeds.
|
|
65
|
+
* 2. `options` must be a plain object.
|
|
66
|
+
* 3. `cookieName`, if present, must be a non-empty string.
|
|
67
|
+
* 4. `headerName`, if present, must be a non-empty string.
|
|
68
|
+
* 5. `safeMethods`, if present, must be an array of non-empty strings.
|
|
69
|
+
* 6. `tokenBytes`, if present, must be a positive integer.
|
|
70
|
+
* 7. `path`, if present, must be a non-empty string.
|
|
71
|
+
* 8. `domain`, if present, must be a non-empty string.
|
|
72
|
+
* 9. `secure`, if present, must be a boolean.
|
|
73
|
+
* 10. `sameSite`, if present, must be `'Strict'`, `'Lax'`, or `'None'`.
|
|
74
|
+
* 11. `sameSite: 'None'` requires an effective `secure: true`.
|
|
75
|
+
*
|
|
76
|
+
* @param {unknown} options - CSRF options to validate.
|
|
77
|
+
* @returns {Validation} Validation result.
|
|
78
|
+
* @example
|
|
79
|
+
* ```js
|
|
80
|
+
* const validation = validateCSRFOptions({ cookieName: 'csrf' });
|
|
81
|
+
*
|
|
82
|
+
* console.log(validation.ok);
|
|
83
|
+
* // => true
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```js
|
|
88
|
+
* const validation = validateCSRFOptions({ tokenBytes: 0 });
|
|
89
|
+
*
|
|
90
|
+
* console.log(validation.ok);
|
|
91
|
+
* // => false
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export function validateCSRFOptions(options) {
|
|
95
|
+
if (options === undefined) {
|
|
96
|
+
return Validation.success();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!isPlainObject(options)) {
|
|
100
|
+
return Validation.failure({
|
|
101
|
+
error: ServerConfigurationError,
|
|
102
|
+
code: 'INVALID_CSRF_OPTIONS',
|
|
103
|
+
custom: true,
|
|
104
|
+
options: { values: { received: typeof options } },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if ('cookieName' in options) {
|
|
109
|
+
const { cookieName } = options;
|
|
110
|
+
|
|
111
|
+
if (typeof cookieName !== 'string') {
|
|
112
|
+
return Validation.failure({
|
|
113
|
+
error: ServerConfigurationError,
|
|
114
|
+
code: 'INVALID_CSRF_COOKIE_NAME_TYPE',
|
|
115
|
+
custom: true,
|
|
116
|
+
options: { values: { received: typeof cookieName } },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (cookieName.length === 0) {
|
|
121
|
+
return Validation.failure({
|
|
122
|
+
error: ServerConfigurationError,
|
|
123
|
+
code: 'INVALID_CSRF_COOKIE_NAME_VALUE',
|
|
124
|
+
custom: true,
|
|
125
|
+
options: { details: { cookieName } },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if ('headerName' in options) {
|
|
131
|
+
const { headerName } = options;
|
|
132
|
+
|
|
133
|
+
if (typeof headerName !== 'string') {
|
|
134
|
+
return Validation.failure({
|
|
135
|
+
error: ServerConfigurationError,
|
|
136
|
+
code: 'INVALID_CSRF_HEADER_NAME_TYPE',
|
|
137
|
+
custom: true,
|
|
138
|
+
options: { values: { received: typeof headerName } },
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (headerName.length === 0) {
|
|
143
|
+
return Validation.failure({
|
|
144
|
+
error: ServerConfigurationError,
|
|
145
|
+
code: 'INVALID_CSRF_HEADER_NAME_VALUE',
|
|
146
|
+
custom: true,
|
|
147
|
+
options: { details: { headerName } },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if ('safeMethods' in options) {
|
|
153
|
+
const { safeMethods } = options;
|
|
154
|
+
|
|
155
|
+
if (!Array.isArray(safeMethods)) {
|
|
156
|
+
return Validation.failure({
|
|
157
|
+
error: ServerConfigurationError,
|
|
158
|
+
code: 'INVALID_CSRF_SAFE_METHODS_TYPE',
|
|
159
|
+
custom: true,
|
|
160
|
+
options: { values: { received: typeof safeMethods } },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!everyNonEmptyString(safeMethods)) {
|
|
165
|
+
return Validation.failure({
|
|
166
|
+
error: ServerConfigurationError,
|
|
167
|
+
code: 'INVALID_CSRF_SAFE_METHODS_ITEM',
|
|
168
|
+
custom: true,
|
|
169
|
+
options: { details: { safeMethods } },
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if ('tokenBytes' in options) {
|
|
175
|
+
const { tokenBytes } = options;
|
|
176
|
+
|
|
177
|
+
if (typeof tokenBytes !== 'number') {
|
|
178
|
+
return Validation.failure({
|
|
179
|
+
error: ServerConfigurationError,
|
|
180
|
+
code: 'INVALID_CSRF_TOKEN_BYTES_TYPE',
|
|
181
|
+
custom: true,
|
|
182
|
+
options: { values: { received: typeof tokenBytes } },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!Number.isInteger(tokenBytes) || tokenBytes <= 0) {
|
|
187
|
+
return Validation.failure({
|
|
188
|
+
error: ServerConfigurationError,
|
|
189
|
+
code: 'INVALID_CSRF_TOKEN_BYTES_RANGE',
|
|
190
|
+
custom: true,
|
|
191
|
+
options: { details: { tokenBytes } },
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if ('path' in options) {
|
|
197
|
+
const { path } = options;
|
|
198
|
+
|
|
199
|
+
if (typeof path !== 'string') {
|
|
200
|
+
return Validation.failure({
|
|
201
|
+
error: ServerConfigurationError,
|
|
202
|
+
code: 'INVALID_CSRF_PATH_TYPE',
|
|
203
|
+
custom: true,
|
|
204
|
+
options: { values: { received: typeof path } },
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (path.length === 0) {
|
|
209
|
+
return Validation.failure({
|
|
210
|
+
error: ServerConfigurationError,
|
|
211
|
+
code: 'INVALID_CSRF_PATH_VALUE',
|
|
212
|
+
custom: true,
|
|
213
|
+
options: { details: { path } },
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if ('domain' in options) {
|
|
219
|
+
const { domain } = options;
|
|
220
|
+
|
|
221
|
+
if (typeof domain !== 'string') {
|
|
222
|
+
return Validation.failure({
|
|
223
|
+
error: ServerConfigurationError,
|
|
224
|
+
code: 'INVALID_CSRF_DOMAIN_TYPE',
|
|
225
|
+
custom: true,
|
|
226
|
+
options: { values: { received: typeof domain } },
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (domain.length === 0) {
|
|
231
|
+
return Validation.failure({
|
|
232
|
+
error: ServerConfigurationError,
|
|
233
|
+
code: 'INVALID_CSRF_DOMAIN_VALUE',
|
|
234
|
+
custom: true,
|
|
235
|
+
options: { details: { domain } },
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if ('secure' in options) {
|
|
241
|
+
const { secure } = options;
|
|
242
|
+
|
|
243
|
+
if (typeof secure !== 'boolean') {
|
|
244
|
+
return Validation.failure({
|
|
245
|
+
error: ServerConfigurationError,
|
|
246
|
+
code: 'INVALID_CSRF_SECURE_TYPE',
|
|
247
|
+
custom: true,
|
|
248
|
+
options: { values: { received: typeof secure } },
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if ('sameSite' in options) {
|
|
254
|
+
const { sameSite } = options;
|
|
255
|
+
|
|
256
|
+
if (sameSite !== undefined && !SAME_SITE_VALUES.includes(String(sameSite))) {
|
|
257
|
+
return Validation.failure({
|
|
258
|
+
error: ServerConfigurationError,
|
|
259
|
+
code: 'INVALID_CSRF_SAME_SITE_VALUE',
|
|
260
|
+
custom: true,
|
|
261
|
+
options: { details: { sameSite } },
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Effective values (after defaults) are used for the cross-field rule.
|
|
267
|
+
const effectiveSameSite = options.sameSite ?? 'Lax';
|
|
268
|
+
const effectiveSecure = options.secure ?? true;
|
|
269
|
+
|
|
270
|
+
if (effectiveSameSite === 'None' && effectiveSecure === false) {
|
|
271
|
+
return Validation.failure({
|
|
272
|
+
error: ServerConfigurationError,
|
|
273
|
+
code: 'CSRF_NONE_REQUIRES_SECURE',
|
|
274
|
+
custom: true,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return Validation.success();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Asserts that CSRF options are valid.
|
|
283
|
+
*
|
|
284
|
+
* @param {unknown} options - CSRF options to validate.
|
|
285
|
+
* @throws {ServerConfigurationError} If `options` is not a plain object.
|
|
286
|
+
* @throws {ServerConfigurationError} If any option is invalid.
|
|
287
|
+
* @throws {ServerConfigurationError} If `sameSite: 'None'` is used without `secure: true`.
|
|
288
|
+
* @example
|
|
289
|
+
* ```js
|
|
290
|
+
* assertCSRFOptions({ cookieName: 'csrf' });
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
export function assertCSRFOptions(options) {
|
|
294
|
+
const validation = validateCSRFOptions(options);
|
|
295
|
+
|
|
296
|
+
if (!validation.ok) {
|
|
297
|
+
throw validation.error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Compress options validation utilities.
|
|
4
|
+
*
|
|
5
|
+
* @description Validates compression middleware options used by
|
|
6
|
+
* ServerConfiguration, including size threshold, compression level, and
|
|
7
|
+
* compressible media types.
|
|
8
|
+
*
|
|
9
|
+
* @module Validation/ServerConfiguration/CompressOptions
|
|
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
|
+
* Minimum valid compression level.
|
|
20
|
+
*
|
|
21
|
+
* `-1` delegates to the zlib default for the selected algorithm.
|
|
22
|
+
*
|
|
23
|
+
* @private
|
|
24
|
+
* @type {number}
|
|
25
|
+
*/
|
|
26
|
+
const MIN_LEVEL = -1;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Maximum valid compression level.
|
|
30
|
+
*
|
|
31
|
+
* Brotli accepts up to `11`; gzip accepts up to `9`. The widest range is
|
|
32
|
+
* used here so that brotli levels are not rejected.
|
|
33
|
+
*
|
|
34
|
+
* @private
|
|
35
|
+
* @type {number}
|
|
36
|
+
*/
|
|
37
|
+
const MAX_LEVEL = 11;
|
|
38
|
+
|
|
39
|
+
// ━━ FUNCTIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
40
|
+
/**
|
|
41
|
+
* Determines whether a value is a non-null plain object.
|
|
42
|
+
*
|
|
43
|
+
* @private
|
|
44
|
+
* @param {unknown} value - Value to inspect.
|
|
45
|
+
* @returns {value is Record<string, unknown>} `true` if the value is a plain object.
|
|
46
|
+
*/
|
|
47
|
+
function isPlainObject(value) {
|
|
48
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Determines whether a value is a non-empty string.
|
|
53
|
+
*
|
|
54
|
+
* @private
|
|
55
|
+
* @param {unknown} value - Value to inspect.
|
|
56
|
+
* @returns {value is string} `true` if the value is a non-empty string.
|
|
57
|
+
*/
|
|
58
|
+
function isNonEmptyString(value) {
|
|
59
|
+
return typeof value === 'string' && value.length > 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Determines whether every item in an array is a non-empty string.
|
|
64
|
+
*
|
|
65
|
+
* @private
|
|
66
|
+
* @param {unknown[]} values - Array of values to inspect.
|
|
67
|
+
* @returns {boolean} `true` if every item is a non-empty string.
|
|
68
|
+
*/
|
|
69
|
+
function everyNonEmptyString(values) {
|
|
70
|
+
return values.every(isNonEmptyString);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Validates compression options.
|
|
75
|
+
*
|
|
76
|
+
* Validation is performed in the following order:
|
|
77
|
+
*
|
|
78
|
+
* 1. `options` may be `undefined`, in which case validation succeeds.
|
|
79
|
+
* 2. `options` must be a plain object.
|
|
80
|
+
* 3. `threshold`, if present, must be a non-negative integer.
|
|
81
|
+
* 4. `level`, if present, must be an integer between `-1` and `11`.
|
|
82
|
+
* 5. `types`, if present, must be an array of non-empty strings.
|
|
83
|
+
*
|
|
84
|
+
* @param {unknown} options - Compression options to validate.
|
|
85
|
+
* @returns {Validation} Validation result.
|
|
86
|
+
* @example
|
|
87
|
+
* ```js
|
|
88
|
+
* const validation = validateCompressOptions({ threshold: 1024, level: 6 });
|
|
89
|
+
*
|
|
90
|
+
* console.log(validation.ok);
|
|
91
|
+
* // => true
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```js
|
|
96
|
+
* const validation = validateCompressOptions({ threshold: -1 });
|
|
97
|
+
*
|
|
98
|
+
* console.log(validation.ok);
|
|
99
|
+
* // => false
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export function validateCompressOptions(options) {
|
|
103
|
+
if (options === undefined) {
|
|
104
|
+
return Validation.success();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!isPlainObject(options)) {
|
|
108
|
+
return Validation.failure({
|
|
109
|
+
error: ServerConfigurationError,
|
|
110
|
+
code: 'INVALID_COMPRESS_OPTIONS',
|
|
111
|
+
custom: true,
|
|
112
|
+
options: { values: { received: typeof options } },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if ('threshold' in options) {
|
|
117
|
+
const { threshold } = options;
|
|
118
|
+
|
|
119
|
+
if (typeof threshold !== 'number') {
|
|
120
|
+
return Validation.failure({
|
|
121
|
+
error: ServerConfigurationError,
|
|
122
|
+
code: 'INVALID_COMPRESS_THRESHOLD_TYPE',
|
|
123
|
+
custom: true,
|
|
124
|
+
options: { values: { received: typeof threshold } },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!Number.isInteger(threshold) || threshold < 0) {
|
|
129
|
+
return Validation.failure({
|
|
130
|
+
error: ServerConfigurationError,
|
|
131
|
+
code: 'INVALID_COMPRESS_THRESHOLD_RANGE',
|
|
132
|
+
custom: true,
|
|
133
|
+
options: { details: { threshold } },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if ('level' in options) {
|
|
139
|
+
const { level } = options;
|
|
140
|
+
|
|
141
|
+
if (typeof level !== 'number') {
|
|
142
|
+
return Validation.failure({
|
|
143
|
+
error: ServerConfigurationError,
|
|
144
|
+
code: 'INVALID_COMPRESS_LEVEL_TYPE',
|
|
145
|
+
custom: true,
|
|
146
|
+
options: { values: { received: typeof level } },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!Number.isInteger(level) || level < MIN_LEVEL || level > MAX_LEVEL) {
|
|
151
|
+
return Validation.failure({
|
|
152
|
+
error: ServerConfigurationError,
|
|
153
|
+
code: 'INVALID_COMPRESS_LEVEL_RANGE',
|
|
154
|
+
custom: true,
|
|
155
|
+
options: { details: { level } },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if ('types' in options) {
|
|
161
|
+
const { types } = options;
|
|
162
|
+
|
|
163
|
+
if (!Array.isArray(types)) {
|
|
164
|
+
return Validation.failure({
|
|
165
|
+
error: ServerConfigurationError,
|
|
166
|
+
code: 'INVALID_COMPRESS_TYPES_TYPE',
|
|
167
|
+
custom: true,
|
|
168
|
+
options: { values: { received: typeof types } },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!everyNonEmptyString(types)) {
|
|
173
|
+
return Validation.failure({
|
|
174
|
+
error: ServerConfigurationError,
|
|
175
|
+
code: 'INVALID_COMPRESS_TYPES_ITEM',
|
|
176
|
+
custom: true,
|
|
177
|
+
options: { details: { types } },
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return Validation.success();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Asserts that compression options are valid.
|
|
187
|
+
*
|
|
188
|
+
* @param {unknown} options - Compression options to validate.
|
|
189
|
+
* @throws {ServerConfigurationError} If `options` is not a plain object.
|
|
190
|
+
* @throws {ServerConfigurationError} If `threshold` is not a non-negative integer.
|
|
191
|
+
* @throws {ServerConfigurationError} If `level` is not an integer between `-1` and `11`.
|
|
192
|
+
* @throws {ServerConfigurationError} If `types` is not an array of non-empty strings.
|
|
193
|
+
* @example
|
|
194
|
+
* ```js
|
|
195
|
+
* assertCompressOptions({ threshold: 1024, level: 6 });
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
export function assertCompressOptions(options) {
|
|
199
|
+
const validation = validateCompressOptions(options);
|
|
200
|
+
|
|
201
|
+
if (!validation.ok) {
|
|
202
|
+
throw validation.error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Cookie parser options validation utilities.
|
|
4
|
+
*
|
|
5
|
+
* @description Validates cookie parser middleware options used by
|
|
6
|
+
* ServerConfiguration.
|
|
7
|
+
*
|
|
8
|
+
* @module Validation/ServerConfiguration/CookieParserOptions
|
|
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
|
+
// ━━ FUNCTIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
17
|
+
/**
|
|
18
|
+
* Determines whether a value is a non-null plain object.
|
|
19
|
+
*
|
|
20
|
+
* @private
|
|
21
|
+
* @param {unknown} value - Value to inspect.
|
|
22
|
+
* @returns {value is Record<string, unknown>} `true` if the value is a plain object.
|
|
23
|
+
*/
|
|
24
|
+
function isPlainObject(value) {
|
|
25
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validates cookie parser options.
|
|
30
|
+
*
|
|
31
|
+
* Validation is performed in the following order:
|
|
32
|
+
*
|
|
33
|
+
* 1. `options` may be `undefined`, in which case validation succeeds.
|
|
34
|
+
* 2. `options` must be a plain object.
|
|
35
|
+
* 3. `decode`, if present, must be a boolean.
|
|
36
|
+
*
|
|
37
|
+
* @param {unknown} options - Cookie parser options to validate.
|
|
38
|
+
* @returns {Validation} Validation result.
|
|
39
|
+
* @example
|
|
40
|
+
* ```js
|
|
41
|
+
* const validation = validateCookieParserOptions({ decode: true });
|
|
42
|
+
*
|
|
43
|
+
* console.log(validation.ok);
|
|
44
|
+
* // => true
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```js
|
|
49
|
+
* const validation = validateCookieParserOptions({ decode: 'yes' });
|
|
50
|
+
*
|
|
51
|
+
* console.log(validation.ok);
|
|
52
|
+
* // => false
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function validateCookieParserOptions(options) {
|
|
56
|
+
if (options === undefined) {
|
|
57
|
+
return Validation.success();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!isPlainObject(options)) {
|
|
61
|
+
return Validation.failure({
|
|
62
|
+
error: ServerConfigurationError,
|
|
63
|
+
code: 'INVALID_COOKIE_PARSER_OPTIONS',
|
|
64
|
+
custom: true,
|
|
65
|
+
options: { values: { received: typeof options } },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if ('decode' in options) {
|
|
70
|
+
const { decode } = options;
|
|
71
|
+
|
|
72
|
+
if (typeof decode !== 'boolean') {
|
|
73
|
+
return Validation.failure({
|
|
74
|
+
error: ServerConfigurationError,
|
|
75
|
+
code: 'INVALID_COOKIE_PARSER_DECODE_TYPE',
|
|
76
|
+
custom: true,
|
|
77
|
+
options: { values: { received: typeof decode } },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return Validation.success();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Asserts that cookie parser options are valid.
|
|
87
|
+
*
|
|
88
|
+
* @param {unknown} options - Cookie parser options to validate.
|
|
89
|
+
* @throws {ServerConfigurationError} If `options` is not a plain object.
|
|
90
|
+
* @throws {ServerConfigurationError} If `decode` is not a boolean.
|
|
91
|
+
* @example
|
|
92
|
+
* ```js
|
|
93
|
+
* assertCookieParserOptions({ decode: true });
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export function assertCookieParserOptions(options) {
|
|
97
|
+
const validation = validateCookieParserOptions(options);
|
|
98
|
+
|
|
99
|
+
if (!validation.ok) {
|
|
100
|
+
throw validation.error;
|
|
101
|
+
}
|
|
102
|
+
}
|