@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,229 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Rate limit options validation utilities.
|
|
4
|
+
*
|
|
5
|
+
* @description Validates rate limit middleware options used by
|
|
6
|
+
* ServerConfiguration, including window duration, request budget,
|
|
7
|
+
* rejection response, and header emission.
|
|
8
|
+
*
|
|
9
|
+
* @module Validation/ServerConfiguration/RateLimitOptions
|
|
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 HTTP status code for a rate-limit rejection.
|
|
20
|
+
*
|
|
21
|
+
* @private
|
|
22
|
+
* @type {number}
|
|
23
|
+
*/
|
|
24
|
+
const MIN_STATUS_CODE = 400;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Maximum valid HTTP status code for a rate-limit rejection.
|
|
28
|
+
*
|
|
29
|
+
* @private
|
|
30
|
+
* @type {number}
|
|
31
|
+
*/
|
|
32
|
+
const MAX_STATUS_CODE = 599;
|
|
33
|
+
|
|
34
|
+
// ━━ FUNCTIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
35
|
+
/**
|
|
36
|
+
* Determines whether a value is a non-null plain object.
|
|
37
|
+
*
|
|
38
|
+
* @private
|
|
39
|
+
* @param {unknown} value - Value to inspect.
|
|
40
|
+
* @returns {value is Record<string, unknown>} `true` if the value is a plain object.
|
|
41
|
+
*/
|
|
42
|
+
function isPlainObject(value) {
|
|
43
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validates rate limit options.
|
|
48
|
+
*
|
|
49
|
+
* Validation is performed in the following order:
|
|
50
|
+
*
|
|
51
|
+
* 1. `options` may be `undefined`, in which case validation succeeds.
|
|
52
|
+
* 2. `options` must be a plain object.
|
|
53
|
+
* 3. `windowMs`, if present, must be a number.
|
|
54
|
+
* 4. `windowMs`, if present, must be a positive integer.
|
|
55
|
+
* 5. `max`, if present, must be a number.
|
|
56
|
+
* 6. `max`, if present, must be a positive integer.
|
|
57
|
+
* 7. `statusCode`, if present, must be a number.
|
|
58
|
+
* 8. `statusCode`, if present, must be an integer between 400 and 599.
|
|
59
|
+
* 9. `message`, if present, must be a plain object.
|
|
60
|
+
* 10. `standardHeaders`, if present, must be a boolean.
|
|
61
|
+
* 11. `legacyHeaders`, if present, must be a boolean.
|
|
62
|
+
*
|
|
63
|
+
* @param {unknown} options - Rate limit options to validate.
|
|
64
|
+
* @returns {Validation} Validation result.
|
|
65
|
+
* @example
|
|
66
|
+
* ```js
|
|
67
|
+
* const validation = validateRateLimitOptions({ windowMs: 60_000, max: 100 });
|
|
68
|
+
*
|
|
69
|
+
* console.log(validation.ok);
|
|
70
|
+
* // => true
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```js
|
|
75
|
+
* const validation = validateRateLimitOptions({ windowMs: -1 });
|
|
76
|
+
*
|
|
77
|
+
* console.log(validation.ok);
|
|
78
|
+
* // => false
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export function validateRateLimitOptions(options) {
|
|
82
|
+
if (options === undefined) {
|
|
83
|
+
return Validation.success();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!isPlainObject(options)) {
|
|
87
|
+
return Validation.failure({
|
|
88
|
+
error: ServerConfigurationError,
|
|
89
|
+
code: 'INVALID_RATE_LIMIT_OPTIONS',
|
|
90
|
+
custom: true,
|
|
91
|
+
options: { values: { received: typeof options } },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if ('windowMs' in options) {
|
|
96
|
+
const { windowMs } = options;
|
|
97
|
+
|
|
98
|
+
if (typeof windowMs !== 'number') {
|
|
99
|
+
return Validation.failure({
|
|
100
|
+
error: ServerConfigurationError,
|
|
101
|
+
code: 'INVALID_RATE_LIMIT_WINDOW_MS_TYPE',
|
|
102
|
+
custom: true,
|
|
103
|
+
options: { values: { received: typeof windowMs } },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!Number.isInteger(windowMs) || windowMs <= 0) {
|
|
108
|
+
return Validation.failure({
|
|
109
|
+
error: ServerConfigurationError,
|
|
110
|
+
code: 'INVALID_RATE_LIMIT_WINDOW_MS_RANGE',
|
|
111
|
+
custom: true,
|
|
112
|
+
options: { details: { windowMs } },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if ('max' in options) {
|
|
118
|
+
const { max } = options;
|
|
119
|
+
|
|
120
|
+
if (typeof max !== 'number') {
|
|
121
|
+
return Validation.failure({
|
|
122
|
+
error: ServerConfigurationError,
|
|
123
|
+
code: 'INVALID_RATE_LIMIT_MAX_TYPE',
|
|
124
|
+
custom: true,
|
|
125
|
+
options: { values: { received: typeof max } },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!Number.isInteger(max) || max <= 0) {
|
|
130
|
+
return Validation.failure({
|
|
131
|
+
error: ServerConfigurationError,
|
|
132
|
+
code: 'INVALID_RATE_LIMIT_MAX_RANGE',
|
|
133
|
+
custom: true,
|
|
134
|
+
options: { details: { max } },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if ('statusCode' in options) {
|
|
140
|
+
const { statusCode } = options;
|
|
141
|
+
|
|
142
|
+
if (typeof statusCode !== 'number') {
|
|
143
|
+
return Validation.failure({
|
|
144
|
+
error: ServerConfigurationError,
|
|
145
|
+
code: 'INVALID_RATE_LIMIT_STATUS_CODE_TYPE',
|
|
146
|
+
custom: true,
|
|
147
|
+
options: { values: { received: typeof statusCode } },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
!Number.isInteger(statusCode) ||
|
|
153
|
+
statusCode < MIN_STATUS_CODE ||
|
|
154
|
+
statusCode > MAX_STATUS_CODE
|
|
155
|
+
) {
|
|
156
|
+
return Validation.failure({
|
|
157
|
+
error: ServerConfigurationError,
|
|
158
|
+
code: 'INVALID_RATE_LIMIT_STATUS_CODE_RANGE',
|
|
159
|
+
custom: true,
|
|
160
|
+
options: { details: { statusCode } },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if ('message' in options) {
|
|
166
|
+
const { message } = options;
|
|
167
|
+
|
|
168
|
+
if (!isPlainObject(message)) {
|
|
169
|
+
return Validation.failure({
|
|
170
|
+
error: ServerConfigurationError,
|
|
171
|
+
code: 'INVALID_RATE_LIMIT_MESSAGE_TYPE',
|
|
172
|
+
custom: true,
|
|
173
|
+
options: { values: { received: typeof message } },
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if ('standardHeaders' in options) {
|
|
179
|
+
const { standardHeaders } = options;
|
|
180
|
+
|
|
181
|
+
if (typeof standardHeaders !== 'boolean') {
|
|
182
|
+
return Validation.failure({
|
|
183
|
+
error: ServerConfigurationError,
|
|
184
|
+
code: 'INVALID_RATE_LIMIT_STANDARD_HEADERS_TYPE',
|
|
185
|
+
custom: true,
|
|
186
|
+
options: { values: { received: typeof standardHeaders } },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if ('legacyHeaders' in options) {
|
|
192
|
+
const { legacyHeaders } = options;
|
|
193
|
+
|
|
194
|
+
if (typeof legacyHeaders !== 'boolean') {
|
|
195
|
+
return Validation.failure({
|
|
196
|
+
error: ServerConfigurationError,
|
|
197
|
+
code: 'INVALID_RATE_LIMIT_LEGACY_HEADERS_TYPE',
|
|
198
|
+
custom: true,
|
|
199
|
+
options: { values: { received: typeof legacyHeaders } },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return Validation.success();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Asserts that rate limit options are valid.
|
|
209
|
+
*
|
|
210
|
+
* @param {unknown} options - Rate limit options to validate.
|
|
211
|
+
* @throws {ServerConfigurationError} If `options` is not a plain object.
|
|
212
|
+
* @throws {ServerConfigurationError} If `windowMs` is not a positive integer.
|
|
213
|
+
* @throws {ServerConfigurationError} If `max` is not a positive integer.
|
|
214
|
+
* @throws {ServerConfigurationError} If `statusCode` is not an integer between 400 and 599.
|
|
215
|
+
* @throws {ServerConfigurationError} If `message` is not a plain object.
|
|
216
|
+
* @throws {ServerConfigurationError} If `standardHeaders` is not a boolean.
|
|
217
|
+
* @throws {ServerConfigurationError} If `legacyHeaders` is not a boolean.
|
|
218
|
+
* @example
|
|
219
|
+
* ```js
|
|
220
|
+
* assertRateLimitOptions({ windowMs: 60_000, max: 100 });
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
export function assertRateLimitOptions(options) {
|
|
224
|
+
const validation = validateRateLimitOptions(options);
|
|
225
|
+
|
|
226
|
+
if (!validation.ok) {
|
|
227
|
+
throw validation.error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Request options validation utilities.
|
|
4
|
+
*
|
|
5
|
+
* @description Validates request options used by ServerConfiguration,
|
|
6
|
+
* including body limit and proxy trust constraints.
|
|
7
|
+
*
|
|
8
|
+
* @module Validation/ServerConfiguration/RequestOptions
|
|
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 request 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. `bodyLimit`, if defined, must be a positive safe integer.
|
|
36
|
+
* 4. `trustProxy`, if defined, must be a boolean or a non-negative integer.
|
|
37
|
+
*
|
|
38
|
+
* @param {unknown} options - Request options to validate.
|
|
39
|
+
* @returns {Validation} Validation result.
|
|
40
|
+
*/
|
|
41
|
+
export function validateRequestOptions(options) {
|
|
42
|
+
if (options === undefined) {
|
|
43
|
+
return Validation.success();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!isPlainObject(options)) {
|
|
47
|
+
return Validation.failure({
|
|
48
|
+
error: ServerConfigurationError,
|
|
49
|
+
code: 'INVALID_REQUEST_OPTIONS',
|
|
50
|
+
custom: true,
|
|
51
|
+
options: { values: { received: typeof options } },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if ('bodyLimit' in options) {
|
|
56
|
+
const { bodyLimit } = options;
|
|
57
|
+
|
|
58
|
+
if (bodyLimit !== undefined) {
|
|
59
|
+
if (typeof bodyLimit !== 'number') {
|
|
60
|
+
return Validation.failure({
|
|
61
|
+
error: ServerConfigurationError,
|
|
62
|
+
code: 'INVALID_BODY_LIMIT_TYPE',
|
|
63
|
+
custom: true,
|
|
64
|
+
options: { values: { received: typeof bodyLimit } },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!Number.isSafeInteger(bodyLimit) || bodyLimit <= 0) {
|
|
69
|
+
return Validation.failure({
|
|
70
|
+
error: ServerConfigurationError,
|
|
71
|
+
code: 'INVALID_BODY_LIMIT_RANGE',
|
|
72
|
+
custom: true,
|
|
73
|
+
options: { details: { bodyLimit } },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if ('trustProxy' in options) {
|
|
80
|
+
const { trustProxy } = options;
|
|
81
|
+
|
|
82
|
+
if (trustProxy !== undefined) {
|
|
83
|
+
const isBoolean = typeof trustProxy === 'boolean';
|
|
84
|
+
const isNumber = typeof trustProxy === 'number';
|
|
85
|
+
|
|
86
|
+
if (!isBoolean && !isNumber) {
|
|
87
|
+
return Validation.failure({
|
|
88
|
+
error: ServerConfigurationError,
|
|
89
|
+
code: 'INVALID_TRUST_PROXY_TYPE',
|
|
90
|
+
custom: true,
|
|
91
|
+
options: { values: { received: typeof trustProxy } },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isNumber && (!Number.isInteger(trustProxy) || trustProxy < 0)) {
|
|
96
|
+
return Validation.failure({
|
|
97
|
+
error: ServerConfigurationError,
|
|
98
|
+
code: 'INVALID_TRUST_PROXY_RANGE',
|
|
99
|
+
custom: true,
|
|
100
|
+
options: { details: { trustProxy } },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return Validation.success();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Asserts that request options are valid.
|
|
111
|
+
*
|
|
112
|
+
* @param {unknown} options - Request options to validate.
|
|
113
|
+
* @throws {ServerConfigurationError} If `options` is not a plain object.
|
|
114
|
+
* @throws {ServerConfigurationError} If `bodyLimit` is defined and is not a number.
|
|
115
|
+
* @throws {ServerConfigurationError} If `bodyLimit` is defined and is not a positive safe integer.
|
|
116
|
+
* @throws {ServerConfigurationError} If `trustProxy` is defined and is not a boolean or number.
|
|
117
|
+
* @throws {ServerConfigurationError} If numeric `trustProxy` is defined and is negative or not an integer.
|
|
118
|
+
* @example
|
|
119
|
+
* ```js
|
|
120
|
+
* assertRequestOptions({
|
|
121
|
+
* bodyLimit: 10 * 1024 * 1024,
|
|
122
|
+
* trustProxy: true,
|
|
123
|
+
* });
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export function assertRequestOptions(options) {
|
|
127
|
+
const validation = validateRequestOptions(options);
|
|
128
|
+
|
|
129
|
+
if (!validation.ok) {
|
|
130
|
+
throw validation.error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Response options validation utilities.
|
|
4
|
+
*
|
|
5
|
+
* @description Validates response options used by ServerConfiguration,
|
|
6
|
+
* including ETag generation and default charset settings.
|
|
7
|
+
*
|
|
8
|
+
* @module Validation/ServerConfiguration/ResponseOptions
|
|
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 response 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. `etag`, if defined, must be a boolean.
|
|
36
|
+
* 4. `defaultCharset`, if defined, must be a non-empty string.
|
|
37
|
+
*
|
|
38
|
+
* @param {unknown} options - Response options to validate.
|
|
39
|
+
* @returns {Validation} Validation result.
|
|
40
|
+
* @example
|
|
41
|
+
* ```js
|
|
42
|
+
* const validation = validateResponseOptions({ etag: true });
|
|
43
|
+
*
|
|
44
|
+
* console.log(validation.ok);
|
|
45
|
+
* // => true
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function validateResponseOptions(options) {
|
|
49
|
+
if (options === undefined) {
|
|
50
|
+
return Validation.success();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!isPlainObject(options)) {
|
|
54
|
+
return Validation.failure({
|
|
55
|
+
error: ServerConfigurationError,
|
|
56
|
+
code: 'INVALID_RESPONSE_OPTIONS',
|
|
57
|
+
custom: true,
|
|
58
|
+
options: { values: { received: typeof options } },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if ('etag' in options) {
|
|
63
|
+
const { etag } = options;
|
|
64
|
+
|
|
65
|
+
if (etag !== undefined) {
|
|
66
|
+
if (typeof etag !== 'boolean') {
|
|
67
|
+
return Validation.failure({
|
|
68
|
+
error: ServerConfigurationError,
|
|
69
|
+
code: 'INVALID_ETAG_TYPE',
|
|
70
|
+
custom: true,
|
|
71
|
+
options: { values: { received: typeof etag } },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if ('defaultCharset' in options) {
|
|
78
|
+
const { defaultCharset } = options;
|
|
79
|
+
|
|
80
|
+
if (defaultCharset !== undefined) {
|
|
81
|
+
if (typeof defaultCharset !== 'string') {
|
|
82
|
+
return Validation.failure({
|
|
83
|
+
error: ServerConfigurationError,
|
|
84
|
+
code: 'INVALID_DEFAULT_CHARSET_TYPE',
|
|
85
|
+
custom: true,
|
|
86
|
+
options: { values: { received: typeof defaultCharset } },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (defaultCharset.trim().length === 0) {
|
|
91
|
+
return Validation.failure({
|
|
92
|
+
error: ServerConfigurationError,
|
|
93
|
+
code: 'INVALID_DEFAULT_CHARSET_VALUE',
|
|
94
|
+
custom: true,
|
|
95
|
+
options: { details: { defaultCharset } },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return Validation.success();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Asserts that response options are valid.
|
|
106
|
+
*
|
|
107
|
+
* @param {unknown} options - Response options to validate.
|
|
108
|
+
* @throws {ServerConfigurationError} If `options` is not a plain object.
|
|
109
|
+
* @throws {ServerConfigurationError} If `etag` is defined and is not a boolean.
|
|
110
|
+
* @throws {ServerConfigurationError} If `defaultCharset` is defined and is not a string.
|
|
111
|
+
* @throws {ServerConfigurationError} If `defaultCharset` is defined and is an empty string.
|
|
112
|
+
* @example
|
|
113
|
+
* ```js
|
|
114
|
+
* assertResponseOptions({ etag: true, defaultCharset: 'utf-8' });
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export function assertResponseOptions(options) {
|
|
118
|
+
const validation = validateResponseOptions(options);
|
|
119
|
+
if (!validation.ok) {
|
|
120
|
+
throw validation.error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Security headers options validation utilities.
|
|
4
|
+
*
|
|
5
|
+
* @description Validates security headers middleware options used by
|
|
6
|
+
* ServerConfiguration. Every recognized security header option must be a
|
|
7
|
+
* string or `false`.
|
|
8
|
+
*
|
|
9
|
+
* @module Validation/ServerConfiguration/SecurityHeadersOptions
|
|
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
|
+
* Property names recognized as security header options.
|
|
20
|
+
*
|
|
21
|
+
* @private
|
|
22
|
+
* @type {readonly string[]}
|
|
23
|
+
*/
|
|
24
|
+
const SECURITY_HEADER_KEYS = Object.freeze([
|
|
25
|
+
'contentSecurityPolicy',
|
|
26
|
+
'crossOriginEmbedderPolicy',
|
|
27
|
+
'crossOriginOpenerPolicy',
|
|
28
|
+
'crossOriginResourcePolicy',
|
|
29
|
+
'originAgentCluster',
|
|
30
|
+
'permissionsPolicy',
|
|
31
|
+
'referrerPolicy',
|
|
32
|
+
'strictTransportSecurity',
|
|
33
|
+
'xContentTypeOptions',
|
|
34
|
+
'xDnsPrefetchControl',
|
|
35
|
+
'xDownloadOptions',
|
|
36
|
+
'xFrameOptions',
|
|
37
|
+
'xPermittedCrossDomainPolicies',
|
|
38
|
+
'xXssProtection',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
// ━━ FUNCTIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
42
|
+
/**
|
|
43
|
+
* Determines whether a value is a non-null plain object.
|
|
44
|
+
*
|
|
45
|
+
* @private
|
|
46
|
+
* @param {unknown} value - Value to inspect.
|
|
47
|
+
* @returns {value is Record<string, unknown>} `true` if the value is a plain object.
|
|
48
|
+
*/
|
|
49
|
+
function isPlainObject(value) {
|
|
50
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validates security headers options.
|
|
55
|
+
*
|
|
56
|
+
* Validation is performed in the following order:
|
|
57
|
+
*
|
|
58
|
+
* 1. `options` may be `undefined`, in which case validation succeeds.
|
|
59
|
+
* 2. `options` must be a plain object.
|
|
60
|
+
* 3. Every recognized security header option present in `options` must be a
|
|
61
|
+
* string or `false`.
|
|
62
|
+
*
|
|
63
|
+
* Unknown properties are ignored.
|
|
64
|
+
*
|
|
65
|
+
* @param {unknown} options - Security headers options to validate.
|
|
66
|
+
* @returns {Validation} Validation result.
|
|
67
|
+
* @example
|
|
68
|
+
* ```js
|
|
69
|
+
* const validation = validateSecurityHeadersOptions({ xFrameOptions: 'DENY' });
|
|
70
|
+
*
|
|
71
|
+
* console.log(validation.ok);
|
|
72
|
+
* // => true
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```js
|
|
77
|
+
* const validation = validateSecurityHeadersOptions({ xFrameOptions: 123 });
|
|
78
|
+
*
|
|
79
|
+
* console.log(validation.ok);
|
|
80
|
+
* // => false
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function validateSecurityHeadersOptions(options) {
|
|
84
|
+
if (options === undefined) {
|
|
85
|
+
return Validation.success();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!isPlainObject(options)) {
|
|
89
|
+
return Validation.failure({
|
|
90
|
+
error: ServerConfigurationError,
|
|
91
|
+
code: 'INVALID_SECURITY_HEADERS_OPTIONS',
|
|
92
|
+
custom: true,
|
|
93
|
+
options: { values: { received: typeof options } },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const key of SECURITY_HEADER_KEYS) {
|
|
98
|
+
if (!(key in options)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const value = options[key];
|
|
103
|
+
|
|
104
|
+
if (typeof value !== 'string' && value !== false) {
|
|
105
|
+
return Validation.failure({
|
|
106
|
+
error: ServerConfigurationError,
|
|
107
|
+
code: 'INVALID_SECURITY_HEADERS_OPTION_TYPE',
|
|
108
|
+
custom: true,
|
|
109
|
+
options: {
|
|
110
|
+
values: { key, received: typeof value },
|
|
111
|
+
details: { key, value },
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return Validation.success();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Asserts that security headers options are valid.
|
|
122
|
+
*
|
|
123
|
+
* @param {unknown} options - Security headers options to validate.
|
|
124
|
+
* @throws {ServerConfigurationError} If `options` is not a plain object.
|
|
125
|
+
* @throws {ServerConfigurationError} If a recognized security header option is not a string or `false`.
|
|
126
|
+
* @example
|
|
127
|
+
* ```js
|
|
128
|
+
* assertSecurityHeadersOptions({ xFrameOptions: 'DENY' });
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function assertSecurityHeadersOptions(options) {
|
|
132
|
+
const validation = validateSecurityHeadersOptions(options);
|
|
133
|
+
|
|
134
|
+
if (!validation.ok) {
|
|
135
|
+
throw validation.error;
|
|
136
|
+
}
|
|
137
|
+
}
|