@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.
Files changed (114) hide show
  1. package/README.md +229 -2
  2. package/index.mjs +23 -0
  3. package/package.json +71 -4
  4. package/source/application/Kernel/index.mjs +1137 -0
  5. package/source/application/Server/index.mjs +1201 -0
  6. package/source/application/configurations/Http2CleartextServerConfiguration/index.mjs +168 -0
  7. package/source/application/configurations/Http2SecureServerConfiguration/index.mjs +172 -0
  8. package/source/application/configurations/HttpServerConfiguration/index.mjs +169 -0
  9. package/source/application/configurations/HttpsServerConfiguration/index.mjs +172 -0
  10. package/source/application/configurations/ServerConfiguration/Options/CORSOptions/CORSOptionsValue.mjs +158 -0
  11. package/source/application/configurations/ServerConfiguration/Options/CORSOptions/index.mjs +87 -0
  12. package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/CSRFOptionsValue.mjs +178 -0
  13. package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/index.mjs +80 -0
  14. package/source/application/configurations/ServerConfiguration/Options/CompressOptions/CompressOptionsValue.mjs +126 -0
  15. package/source/application/configurations/ServerConfiguration/Options/CompressOptions/index.mjs +81 -0
  16. package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/CookieParserOptionsValue.mjs +107 -0
  17. package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/index.mjs +82 -0
  18. package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/CookieSessionOptionsValue.mjs +182 -0
  19. package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/index.mjs +86 -0
  20. package/source/application/configurations/ServerConfiguration/Options/ListenOptions/ListenOptionsValue.mjs +95 -0
  21. package/source/application/configurations/ServerConfiguration/Options/ListenOptions/index.mjs +47 -0
  22. package/source/application/configurations/ServerConfiguration/Options/MiddlewareOption.mjs +102 -0
  23. package/source/application/configurations/ServerConfiguration/Options/Options.mjs +105 -0
  24. package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/RateLimitOptionsValue.mjs +164 -0
  25. package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/index.mjs +84 -0
  26. package/source/application/configurations/ServerConfiguration/Options/RequestOptions/RequestOptionsValue.mjs +108 -0
  27. package/source/application/configurations/ServerConfiguration/Options/RequestOptions/index.mjs +47 -0
  28. package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/ResponseOptionsValue.mjs +101 -0
  29. package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/index.mjs +47 -0
  30. package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/SecurityHeadersOptionsValue.mjs +272 -0
  31. package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/index.mjs +85 -0
  32. package/source/application/configurations/ServerConfiguration/Options/ServerOption.mjs +50 -0
  33. package/source/application/configurations/ServerConfiguration/Options/StaticOptions/StaticOptionsValue.mjs +164 -0
  34. package/source/application/configurations/ServerConfiguration/Options/StaticOptions/index.mjs +89 -0
  35. package/source/application/configurations/ServerConfiguration/index.mjs +781 -0
  36. package/source/application/resolvers/TransportResolver/index.mjs +119 -0
  37. package/source/application/use-cases/CloseServer/index.mjs +85 -0
  38. package/source/application/use-cases/ListenServer/index.mjs +98 -0
  39. package/source/core/EventSystem/index.mjs +110 -0
  40. package/source/core/Exchange/ExchangeContext.mjs +291 -0
  41. package/source/core/Exchange/Telemetry.mjs +193 -0
  42. package/source/core/Exchange/index.mjs +604 -0
  43. package/source/core/ExchangeLayerBuilder/index.mjs +133 -0
  44. package/source/core/IPCMain/IPCMain.mjs +507 -0
  45. package/source/core/IPCMain/IPCProtocol.mjs +441 -0
  46. package/source/core/IPCMain/IPCServerChannel.mjs +331 -0
  47. package/source/core/IPCMain/WebSocketAdapter.mjs +189 -0
  48. package/source/core/Request/BodyParser/BodyParser.mjs +197 -0
  49. package/source/core/Request/BodyParser/JSONParser.mjs +70 -0
  50. package/source/core/Request/BodyParser/MultipartParser.mjs +612 -0
  51. package/source/core/Request/BodyParser/URLEncodedParser.mjs +71 -0
  52. package/source/core/Request/RequestHeaders.mjs +177 -0
  53. package/source/core/Request/index.mjs +805 -0
  54. package/source/core/Response/ResponseHeaders.mjs +402 -0
  55. package/source/core/Response/index.mjs +1370 -0
  56. package/source/core/middlewares/CORSMiddleware/index.mjs +289 -0
  57. package/source/core/middlewares/CSRFMiddleware/index.mjs +284 -0
  58. package/source/core/middlewares/CompressMiddleware/index.mjs +460 -0
  59. package/source/core/middlewares/CookieParserMiddleware/index.mjs +194 -0
  60. package/source/core/middlewares/CookieSessionMiddleware/index.mjs +460 -0
  61. package/source/core/middlewares/RateLimitMiddleware/index.mjs +245 -0
  62. package/source/core/middlewares/SecurityHeadersMiddleware/index.mjs +151 -0
  63. package/source/core/middlewares/StaticMiddleware/index.mjs +628 -0
  64. package/source/core/routing/ExchangeLayer/ExchangeMatcher.mjs +127 -0
  65. package/source/core/routing/ExchangeLayer/index.mjs +254 -0
  66. package/source/core/routing/Pipeline/index.mjs +157 -0
  67. package/source/core/routing/Router/Route.mjs +184 -0
  68. package/source/core/routing/Router/index.mjs +523 -0
  69. package/source/core/routing/RouterRegister/OptionsMiddleware.mjs +147 -0
  70. package/source/core/routing/RouterRegister/index.mjs +150 -0
  71. package/source/core/routing/RouterSystem/index.mjs +225 -0
  72. package/source/core/value-objects/Address/index.mjs +281 -0
  73. package/source/core/value-objects/Endpoint/index.mjs +293 -0
  74. package/source/index.mjs +62 -0
  75. package/source/infrastructure/adapters/Http2Adapter/index.mjs +144 -0
  76. package/source/infrastructure/adapters/Http2CleartextAdapter/index.mjs +147 -0
  77. package/source/infrastructure/adapters/HttpAdapter/index.mjs +124 -0
  78. package/source/infrastructure/adapters/HttpsAdapter/index.mjs +124 -0
  79. package/source/infrastructure/adapters/TransportAdapter/createWebSocketOpenRequest.mjs +843 -0
  80. package/source/infrastructure/adapters/TransportAdapter/index.mjs +555 -0
  81. package/source/shared/Identifier/index.mjs +671 -0
  82. package/source/shared/constants/symbols/brands/index.mjs +45 -0
  83. package/source/shared/domain/UUIDV7/index.mjs +346 -0
  84. package/source/shared/errors/BaseError/index.mjs +157 -0
  85. package/source/shared/errors/IPCError/index.mjs +97 -0
  86. package/source/shared/errors/MiddlewareError/index.mjs +106 -0
  87. package/source/shared/errors/PathnamePatternError/index.mjs +103 -0
  88. package/source/shared/errors/RequestError/index.mjs +116 -0
  89. package/source/shared/errors/ResponseError/index.mjs +140 -0
  90. package/source/shared/errors/RouterError/index.mjs +90 -0
  91. package/source/shared/errors/ServerConfigurationError/index.mjs +551 -0
  92. package/source/shared/errors/ServerError/index.mjs +127 -0
  93. package/source/shared/errors/WebSocketError/index.mjs +58 -0
  94. package/source/shared/outcome/ErrorSchema.mjs +361 -0
  95. package/source/shared/outcome/Result/index.mjs +239 -0
  96. package/source/shared/outcome/Validation/index.mjs +208 -0
  97. package/source/shared/validations/Middleware/index.mjs +160 -0
  98. package/source/shared/validations/PathnamePattern/index.mjs +256 -0
  99. package/source/shared/validations/Server/NotFoundHandler/index.mjs +94 -0
  100. package/source/shared/validations/Server/Router/index.mjs +68 -0
  101. package/source/shared/validations/Server/ServerConfiguration/index.mjs +68 -0
  102. package/source/shared/validations/Server/SystemErrorHandler/index.mjs +94 -0
  103. package/source/shared/validations/ServerConfiguration/CORSOptions/index.mjs +310 -0
  104. package/source/shared/validations/ServerConfiguration/CSRFOptions/index.mjs +299 -0
  105. package/source/shared/validations/ServerConfiguration/CompressOptions/index.mjs +204 -0
  106. package/source/shared/validations/ServerConfiguration/CookieParserOptions/index.mjs +102 -0
  107. package/source/shared/validations/ServerConfiguration/CookieSessionOptions/index.mjs +332 -0
  108. package/source/shared/validations/ServerConfiguration/ListenOptions/index.mjs +178 -0
  109. package/source/shared/validations/ServerConfiguration/RateLimitOptions/index.mjs +229 -0
  110. package/source/shared/validations/ServerConfiguration/RequestOptions/index.mjs +132 -0
  111. package/source/shared/validations/ServerConfiguration/ResponseOptions/index.mjs +122 -0
  112. package/source/shared/validations/ServerConfiguration/SecurityHeadersOptions/index.mjs +137 -0
  113. package/source/shared/validations/ServerConfiguration/StaticOptions/index.mjs +248 -0
  114. 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
+ }