@boo-labs/server 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +287 -2
  3. package/index.mjs +23 -0
  4. package/package.json +71 -4
  5. package/source/application/Kernel/index.mjs +1137 -0
  6. package/source/application/Server/index.mjs +1201 -0
  7. package/source/application/configurations/Http2CleartextServerConfiguration/index.mjs +168 -0
  8. package/source/application/configurations/Http2SecureServerConfiguration/index.mjs +172 -0
  9. package/source/application/configurations/HttpServerConfiguration/index.mjs +169 -0
  10. package/source/application/configurations/HttpsServerConfiguration/index.mjs +172 -0
  11. package/source/application/configurations/ServerConfiguration/Options/CORSOptions/CORSOptionsValue.mjs +158 -0
  12. package/source/application/configurations/ServerConfiguration/Options/CORSOptions/index.mjs +87 -0
  13. package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/CSRFOptionsValue.mjs +178 -0
  14. package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/index.mjs +80 -0
  15. package/source/application/configurations/ServerConfiguration/Options/CompressOptions/CompressOptionsValue.mjs +126 -0
  16. package/source/application/configurations/ServerConfiguration/Options/CompressOptions/index.mjs +81 -0
  17. package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/CookieParserOptionsValue.mjs +107 -0
  18. package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/index.mjs +82 -0
  19. package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/CookieSessionOptionsValue.mjs +182 -0
  20. package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/index.mjs +86 -0
  21. package/source/application/configurations/ServerConfiguration/Options/ListenOptions/ListenOptionsValue.mjs +95 -0
  22. package/source/application/configurations/ServerConfiguration/Options/ListenOptions/index.mjs +47 -0
  23. package/source/application/configurations/ServerConfiguration/Options/MiddlewareOption.mjs +102 -0
  24. package/source/application/configurations/ServerConfiguration/Options/Options.mjs +105 -0
  25. package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/RateLimitOptionsValue.mjs +164 -0
  26. package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/index.mjs +84 -0
  27. package/source/application/configurations/ServerConfiguration/Options/RequestOptions/RequestOptionsValue.mjs +108 -0
  28. package/source/application/configurations/ServerConfiguration/Options/RequestOptions/index.mjs +47 -0
  29. package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/ResponseOptionsValue.mjs +101 -0
  30. package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/index.mjs +47 -0
  31. package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/SecurityHeadersOptionsValue.mjs +272 -0
  32. package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/index.mjs +85 -0
  33. package/source/application/configurations/ServerConfiguration/Options/ServerOption.mjs +50 -0
  34. package/source/application/configurations/ServerConfiguration/Options/StaticOptions/StaticOptionsValue.mjs +164 -0
  35. package/source/application/configurations/ServerConfiguration/Options/StaticOptions/index.mjs +89 -0
  36. package/source/application/configurations/ServerConfiguration/index.mjs +781 -0
  37. package/source/application/resolvers/TransportResolver/index.mjs +119 -0
  38. package/source/application/use-cases/CloseServer/index.mjs +85 -0
  39. package/source/application/use-cases/ListenServer/index.mjs +98 -0
  40. package/source/core/EventSystem/index.mjs +110 -0
  41. package/source/core/Exchange/ExchangeContext.mjs +291 -0
  42. package/source/core/Exchange/Telemetry.mjs +193 -0
  43. package/source/core/Exchange/index.mjs +604 -0
  44. package/source/core/ExchangeLayerBuilder/index.mjs +133 -0
  45. package/source/core/IPCMain/IPCMain.mjs +507 -0
  46. package/source/core/IPCMain/IPCProtocol.mjs +441 -0
  47. package/source/core/IPCMain/IPCServerChannel.mjs +331 -0
  48. package/source/core/IPCMain/WebSocketAdapter.mjs +189 -0
  49. package/source/core/Request/BodyParser/BodyParser.mjs +197 -0
  50. package/source/core/Request/BodyParser/JSONParser.mjs +70 -0
  51. package/source/core/Request/BodyParser/MultipartParser.mjs +612 -0
  52. package/source/core/Request/BodyParser/URLEncodedParser.mjs +71 -0
  53. package/source/core/Request/RequestHeaders.mjs +177 -0
  54. package/source/core/Request/index.mjs +805 -0
  55. package/source/core/Response/ResponseHeaders.mjs +402 -0
  56. package/source/core/Response/index.mjs +1370 -0
  57. package/source/core/middlewares/CORSMiddleware/index.mjs +289 -0
  58. package/source/core/middlewares/CSRFMiddleware/index.mjs +284 -0
  59. package/source/core/middlewares/CompressMiddleware/index.mjs +460 -0
  60. package/source/core/middlewares/CookieParserMiddleware/index.mjs +194 -0
  61. package/source/core/middlewares/CookieSessionMiddleware/index.mjs +460 -0
  62. package/source/core/middlewares/RateLimitMiddleware/index.mjs +245 -0
  63. package/source/core/middlewares/SecurityHeadersMiddleware/index.mjs +151 -0
  64. package/source/core/middlewares/StaticMiddleware/index.mjs +628 -0
  65. package/source/core/routing/ExchangeLayer/ExchangeMatcher.mjs +127 -0
  66. package/source/core/routing/ExchangeLayer/index.mjs +254 -0
  67. package/source/core/routing/Pipeline/index.mjs +157 -0
  68. package/source/core/routing/Router/Route.mjs +184 -0
  69. package/source/core/routing/Router/index.mjs +523 -0
  70. package/source/core/routing/RouterRegister/OptionsMiddleware.mjs +147 -0
  71. package/source/core/routing/RouterRegister/index.mjs +150 -0
  72. package/source/core/routing/RouterSystem/index.mjs +225 -0
  73. package/source/core/value-objects/Address/index.mjs +281 -0
  74. package/source/core/value-objects/Endpoint/index.mjs +293 -0
  75. package/source/index.mjs +62 -0
  76. package/source/infrastructure/adapters/Http2Adapter/index.mjs +144 -0
  77. package/source/infrastructure/adapters/Http2CleartextAdapter/index.mjs +147 -0
  78. package/source/infrastructure/adapters/HttpAdapter/index.mjs +124 -0
  79. package/source/infrastructure/adapters/HttpsAdapter/index.mjs +124 -0
  80. package/source/infrastructure/adapters/TransportAdapter/createWebSocketOpenRequest.mjs +843 -0
  81. package/source/infrastructure/adapters/TransportAdapter/index.mjs +555 -0
  82. package/source/shared/Identifier/index.mjs +671 -0
  83. package/source/shared/constants/symbols/brands/index.mjs +45 -0
  84. package/source/shared/domain/UUIDV7/index.mjs +346 -0
  85. package/source/shared/errors/BaseError/index.mjs +157 -0
  86. package/source/shared/errors/IPCError/index.mjs +97 -0
  87. package/source/shared/errors/MiddlewareError/index.mjs +106 -0
  88. package/source/shared/errors/PathnamePatternError/index.mjs +103 -0
  89. package/source/shared/errors/RequestError/index.mjs +116 -0
  90. package/source/shared/errors/ResponseError/index.mjs +140 -0
  91. package/source/shared/errors/RouterError/index.mjs +90 -0
  92. package/source/shared/errors/ServerConfigurationError/index.mjs +551 -0
  93. package/source/shared/errors/ServerError/index.mjs +127 -0
  94. package/source/shared/errors/WebSocketError/index.mjs +58 -0
  95. package/source/shared/outcome/ErrorSchema.mjs +361 -0
  96. package/source/shared/outcome/Result/index.mjs +239 -0
  97. package/source/shared/outcome/Validation/index.mjs +208 -0
  98. package/source/shared/validations/Middleware/index.mjs +160 -0
  99. package/source/shared/validations/PathnamePattern/index.mjs +256 -0
  100. package/source/shared/validations/Server/NotFoundHandler/index.mjs +94 -0
  101. package/source/shared/validations/Server/Router/index.mjs +68 -0
  102. package/source/shared/validations/Server/ServerConfiguration/index.mjs +68 -0
  103. package/source/shared/validations/Server/SystemErrorHandler/index.mjs +94 -0
  104. package/source/shared/validations/ServerConfiguration/CORSOptions/index.mjs +310 -0
  105. package/source/shared/validations/ServerConfiguration/CSRFOptions/index.mjs +299 -0
  106. package/source/shared/validations/ServerConfiguration/CompressOptions/index.mjs +204 -0
  107. package/source/shared/validations/ServerConfiguration/CookieParserOptions/index.mjs +102 -0
  108. package/source/shared/validations/ServerConfiguration/CookieSessionOptions/index.mjs +332 -0
  109. package/source/shared/validations/ServerConfiguration/ListenOptions/index.mjs +178 -0
  110. package/source/shared/validations/ServerConfiguration/RateLimitOptions/index.mjs +229 -0
  111. package/source/shared/validations/ServerConfiguration/RequestOptions/index.mjs +132 -0
  112. package/source/shared/validations/ServerConfiguration/ResponseOptions/index.mjs +122 -0
  113. package/source/shared/validations/ServerConfiguration/SecurityHeadersOptions/index.mjs +137 -0
  114. package/source/shared/validations/ServerConfiguration/StaticOptions/index.mjs +248 -0
  115. package/types/index.d.ts +719 -0
@@ -0,0 +1,208 @@
1
+ // @ts-check
2
+ /**
3
+ * @file Core validation module for representing strict validation outcomes.
4
+ *
5
+ * @description Provides the {@link Validation} class for explicitly representing
6
+ * successful and failed operations without relying on
7
+ * ambiguous `undefined` states.
8
+ *
9
+ * @module Validation
10
+ */
11
+
12
+ // ━━ IMPORT MODULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13
+ // » IMPORT LOCAL MODULES
14
+ import ErrorSchema from '../ErrorSchema.mjs';
15
+
16
+ // ━━ TYPE DEFINITIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17
+ /**
18
+ * Represents an Error constructor.
19
+ *
20
+ * @typedef {new (identifier: string, options?: Record<string, unknown>) => Error} ErrorConstructor
21
+ */
22
+
23
+ /**
24
+ * Represents options passed to a native Error constructor.
25
+ *
26
+ * @typedef {object} NativeErrorOptions
27
+ * @property {unknown} [cause] - Cause associated with the error.
28
+ */
29
+
30
+ /**
31
+ * Represents a native Error schema definition.
32
+ *
33
+ * @typedef {object} NativeErrorSchema
34
+ * @property {ErrorConstructor} error - Error constructor used to create the error.
35
+ * @property {string} message - Literal error message.
36
+ * @property {NativeErrorOptions} [options] - Options passed to the Error constructor.
37
+ * @property {false} custom - Indicates that the constructor represents a native Error.
38
+ * @property {string} code - Error identifier.
39
+ */
40
+
41
+ /**
42
+ * Represents options passed to a custom Error constructor.
43
+ *
44
+ * @typedef {object} CustomErrorOptions
45
+ * @property {Record<string, unknown>} [values] - Values used to resolve message placeholders.
46
+ * @property {Record<string, unknown>} [details] - Additional contextual information about the error.
47
+ * @property {unknown} [cause] - Cause associated with the error.
48
+ */
49
+
50
+ /**
51
+ * Represents a custom Error schema definition.
52
+ *
53
+ * @typedef {object} CustomErrorSchema
54
+ * @property {ErrorConstructor} error - Error constructor used to create the error.
55
+ * @property {string} code - Error code used to resolve the error message and error identifier.
56
+ * @property {string} [message] - Literal error message.
57
+ * @property {CustomErrorOptions} [options] - Options passed to the Error constructor.
58
+ * @property {true} custom - Indicates that the constructor represents a custom Error.
59
+ */
60
+
61
+ /**
62
+ * Represents a supported Error schema definition.
63
+ *
64
+ * @typedef {NativeErrorSchema|CustomErrorSchema} ErrorSchemaDefinition
65
+ */
66
+
67
+ /**
68
+ * Represents the successful state of a {@link Validation} instance.
69
+ *
70
+ * @typedef {object} ValidationStateSuccess
71
+ * @property {true} ok - Indicates that the validation succeeded.
72
+ * @property {undefined} [error] - Always undefined for a successful validation.
73
+ */
74
+
75
+ /**
76
+ * Represents the failed state of a {@link Validation} instance.
77
+ *
78
+ * @typedef {object} ValidationStateFailure
79
+ * @property {false} ok - Indicates that the validation failed.
80
+ * @property {ErrorSchema} error - Error schema describing the validation failure.
81
+ */
82
+
83
+ /**
84
+ * Represents the internal state of a {@link Validation} instance.
85
+ *
86
+ * @typedef {ValidationStateSuccess|ValidationStateFailure} ValidationState
87
+ */
88
+
89
+ // ━━ MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
90
+ /**
91
+ * Represents the strict outcome of a validation operation (pass/fail).
92
+ *
93
+ * Unlike Result, it does not carry a successful payload, only an error on failure.
94
+ */
95
+ class Validation {
96
+ /**
97
+ * Internal validation state.
98
+ *
99
+ * @type {ValidationState}
100
+ */
101
+ #validation;
102
+
103
+ /**
104
+ * Creates a Validation instance.
105
+ *
106
+ * @param {boolean} ok - Whether the validation succeeded.
107
+ * @param {ErrorSchemaDefinition} [error] - Required when ok is false.
108
+ * @throws {TypeError} If the combination of ok and error is invalid.
109
+ * @example
110
+ * ```js
111
+ * const validation = new Validation(true);
112
+ * ```
113
+ *
114
+ * @example
115
+ * ```js
116
+ * const validation = new Validation(false, {
117
+ * error: TypeError,
118
+ * message: 'Expected a string.',
119
+ * code: 'INVALID_TYPE',
120
+ * });
121
+ * ```
122
+ */
123
+ constructor(ok, error) {
124
+ if (typeof ok !== 'boolean') {
125
+ throw new TypeError('Validation status (ok) must be a boolean.');
126
+ }
127
+
128
+ if (ok === true) {
129
+ if (error !== undefined) {
130
+ throw new TypeError('A successful Validation cannot contain an error.');
131
+ }
132
+
133
+ this.#validation = { ok, error };
134
+ } else {
135
+ if (error === undefined) {
136
+ throw new TypeError('A failed Validation must contain an error.');
137
+ }
138
+ this.#validation = { ok, error: new ErrorSchema(error) };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Indicates whether the validation succeeded.
144
+ *
145
+ * @readonly
146
+ * @type {boolean}
147
+ */
148
+ get ok() {
149
+ return this.#validation.ok;
150
+ }
151
+
152
+ /**
153
+ * The instantiated error of the failed validation.
154
+ *
155
+ * @readonly
156
+ * @type {Error}
157
+ * @throws {Error} If the validation succeeded.
158
+ */
159
+ get error() {
160
+ if (this.#validation.ok === true) {
161
+ throw new Error('Cannot access error of a successful Validation.');
162
+ }
163
+
164
+ return this.#validation.error.error;
165
+ }
166
+
167
+ /**
168
+ * Creates a successful {@link Validation}.
169
+ *
170
+ * @returns {Validation} A successful Validation instance.
171
+ * @example
172
+ * ```js
173
+ * const validation = Validation.success();
174
+ *
175
+ * if (validation.ok) {
176
+ * console.log('Validation passed.');
177
+ * }
178
+ * ```
179
+ */
180
+ static success() {
181
+ return new Validation(true);
182
+ }
183
+
184
+ /**
185
+ * Creates a failed {@link Validation}.
186
+ *
187
+ * @param {ErrorSchemaDefinition} error - The error details describing the failure.
188
+ * @returns {Validation} A failed Validation instance.
189
+ * @example
190
+ * ```js
191
+ * const validation = Validation.failure({
192
+ * error: RangeError,
193
+ * message: 'Value is out of range.',
194
+ * code: 'VALUE_OUT_OF_RANGE',
195
+ * });
196
+ *
197
+ * if (!validation.ok) {
198
+ * console.log(validation.error);
199
+ * }
200
+ * ```
201
+ */
202
+ static failure(error) {
203
+ return new Validation(false, error);
204
+ }
205
+ }
206
+
207
+ // ━━ EXPORT MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
208
+ export default Validation;
@@ -0,0 +1,160 @@
1
+ // @ts-check
2
+ /**
3
+ * @file Middleware validation utilities.
4
+ *
5
+ * @description Validates middleware functions used by the HTTP server.
6
+ *
7
+ * @module Validation/Middleware
8
+ */
9
+
10
+ // ━━ IMPORT MODULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
11
+ // » IMPORT LOCAL MODULES
12
+ import Validation from '#shared/outcome/Validation';
13
+ import MiddlewareError from '#shared/errors/MiddlewareError';
14
+
15
+ // ━━ MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
16
+ /**
17
+ * Validates a middleware function.
18
+ *
19
+ * A middleware must be a function that declares exactly two parameters:
20
+ * `context` and `next`.
21
+ *
22
+ * @param {unknown} middleware - Middleware function to validate.
23
+ * @returns {Validation} Validation result.
24
+ * @example
25
+ * ```js
26
+ * const validation = validateMiddleware((context, next) => {
27
+ * next();
28
+ * });
29
+ *
30
+ * console.log(validation.ok);
31
+ * // => true
32
+ * ```
33
+ *
34
+ * @example
35
+ * ```js
36
+ * const validation = validateMiddleware((context) => {
37
+ * return context;
38
+ * });
39
+ *
40
+ * console.log(validation.ok);
41
+ * // => false
42
+ * ```
43
+ */
44
+ export function validateMiddleware(middleware) {
45
+ if (typeof middleware !== 'function') {
46
+ return Validation.failure({
47
+ error: MiddlewareError,
48
+ code: 'INVALID_MIDDLEWARE_TYPE',
49
+ custom: true,
50
+ options: {
51
+ values: {
52
+ expected: 'function',
53
+ received: typeof middleware,
54
+ },
55
+ },
56
+ });
57
+ }
58
+
59
+ if (middleware.length !== 2) {
60
+ return Validation.failure({
61
+ error: MiddlewareError,
62
+ code: 'INVALID_MIDDLEWARE_ARITY',
63
+ custom: true,
64
+ options: {
65
+ values: {
66
+ expected: 2,
67
+ received: middleware.length,
68
+ },
69
+ },
70
+ });
71
+ }
72
+
73
+ return Validation.success();
74
+ }
75
+
76
+ /**
77
+ * Asserts that a middleware function is valid.
78
+ *
79
+ * @param {unknown} middleware - Middleware function to validate.
80
+ * @throws {MiddlewareError} If the middleware is not a function.
81
+ * @throws {MiddlewareError} If the middleware does not declare exactly two parameters.
82
+ * @example
83
+ * ```js
84
+ * assertMiddleware((context, next) => {
85
+ * next();
86
+ * });
87
+ * ```
88
+ */
89
+ export function assertMiddleware(middleware) {
90
+ const validation = validateMiddleware(middleware);
91
+
92
+ if (!validation.ok) {
93
+ throw validation.error;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Validates an array of middleware functions.
99
+ *
100
+ * @param {unknown} middlewares - Middleware functions to validate.
101
+ * @returns {Validation} Validation result.
102
+ * @example
103
+ * ```js
104
+ * const validation = validateMiddlewares([
105
+ * (context, next) => next(),
106
+ * (context, next) => next(),
107
+ * ]);
108
+ *
109
+ * console.log(validation.ok);
110
+ * // => true
111
+ * ```
112
+ */
113
+ export function validateMiddlewares(middlewares) {
114
+ if (!Array.isArray(middlewares)) {
115
+ return Validation.failure({
116
+ error: MiddlewareError,
117
+ code: 'INVALID_MIDDLEWARES_TYPE',
118
+ custom: true,
119
+ options: {
120
+ values: {
121
+ expected: 'array',
122
+ received: typeof middlewares,
123
+ },
124
+ },
125
+ });
126
+ }
127
+
128
+ for (const middleware of middlewares) {
129
+ const validation = validateMiddleware(middleware);
130
+
131
+ if (!validation.ok) {
132
+ return validation;
133
+ }
134
+ }
135
+
136
+ return Validation.success();
137
+ }
138
+
139
+ /**
140
+ * Asserts that all middleware functions are valid.
141
+ *
142
+ * @param {unknown} middlewares - Middleware functions to validate.
143
+ * @throws {MiddlewareError} If the middleware collection is not an array.
144
+ * @throws {MiddlewareError} If a middleware is not a function.
145
+ * @throws {MiddlewareError} If a middleware does not declare exactly two parameters.
146
+ * @example
147
+ * ```js
148
+ * assertMiddlewares([
149
+ * (context, next) => next(),
150
+ * (context, next) => next(),
151
+ * ]);
152
+ * ```
153
+ */
154
+ export function assertMiddlewares(middlewares) {
155
+ const validation = validateMiddlewares(middlewares);
156
+
157
+ if (!validation.ok) {
158
+ throw validation.error;
159
+ }
160
+ }
@@ -0,0 +1,256 @@
1
+ // @ts-check
2
+ /**
3
+ * @file Pathname pattern validation utilities.
4
+ *
5
+ * @description Validates pathname patterns used by the HTTP router, including
6
+ * parameter syntax, wildcard segments, length constraints, and duplicate
7
+ * parameter names.
8
+ *
9
+ * @module Validation/PathnamePattern
10
+ */
11
+
12
+ // ━━ IMPORT MODULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13
+ // » IMPORT LOCAL MODULES
14
+ import Validation from '#shared/outcome/Validation';
15
+ import PathnamePatternError from '#shared/errors/PathnamePatternError';
16
+
17
+ // ━━ CONSTANTS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18
+ /**
19
+ * Maximum number of characters allowed in a pathname pattern.
20
+ *
21
+ * @private
22
+ * @type {number}
23
+ */
24
+ const MAX_PATTERN_LENGTH = 512;
25
+
26
+ /**
27
+ * Regular expression source for a valid pathname parameter name.
28
+ *
29
+ * Parameter names must begin with a letter or underscore and may contain
30
+ * letters, digits, and underscores.
31
+ *
32
+ * @private
33
+ * @type {string}
34
+ */
35
+ const PARAM_NAME = String.raw`[A-Za-z_][A-Za-z0-9_]*`;
36
+
37
+ /**
38
+ * Regular expression source for a literal character allowed inside a pathname
39
+ * segment.
40
+ *
41
+ * @private
42
+ * @type {string}
43
+ */
44
+ const LITERAL_CHAR = String.raw`[A-Za-z0-9._~!$&'()+,;=@-]`;
45
+
46
+ /**
47
+ * Regular expression source for a parameter segment using colon or brace
48
+ * syntax.
49
+ *
50
+ * Supported forms are `:name` and `{name}`.
51
+ *
52
+ * @private
53
+ * @type {string}
54
+ */
55
+ const PARAM_SEGMENT = String.raw`(?::${PARAM_NAME}|\{${PARAM_NAME}\})`;
56
+
57
+ /**
58
+ * Regular expression source for a normal pathname segment.
59
+ *
60
+ * A normal segment may contain literal characters and parameter segments, but
61
+ * cannot represent `.` or `..` as complete path segments.
62
+ *
63
+ * @private
64
+ * @type {string}
65
+ */
66
+ const NORMAL_SEGMENT = String.raw`(?!\.{1,2}(?=/|$))(?:${LITERAL_CHAR}|${PARAM_SEGMENT})+`;
67
+
68
+ /**
69
+ * Regular expression source for a wildcard pathname segment.
70
+ *
71
+ * @private
72
+ * @type {string}
73
+ */
74
+ const WILDCARD_SEGMENT = String.raw`\*`;
75
+
76
+ /**
77
+ * Complete regular expression source for supported pathname patterns.
78
+ *
79
+ * The expression supports the global wildcard `*` and pathname patterns
80
+ * beginning with `/`, including parameter and trailing wildcard segments.
81
+ *
82
+ * @private
83
+ * @type {string}
84
+ */
85
+ const PATHNAME_PATTERN_SEGMENT = String.raw`^(?:\*|/(?:${NORMAL_SEGMENT}(?:/${NORMAL_SEGMENT})*)?(?:/${WILDCARD_SEGMENT})?)$`;
86
+
87
+ /**
88
+ * Regular expression used to validate pathname pattern syntax.
89
+ *
90
+ * @private
91
+ * @type {RegExp}
92
+ */
93
+ const PATHNAME_PATTERN_REGEXP = new RegExp(PATHNAME_PATTERN_SEGMENT);
94
+
95
+ /**
96
+ * Regular expression used to extract pathname parameter names.
97
+ *
98
+ * Both `:name` and `{name}` parameter syntaxes are supported.
99
+ *
100
+ * @private
101
+ * @type {RegExp}
102
+ */
103
+ const PARAMETER_PATTERN_REGEXP = /:([A-Za-z_][A-Za-z0-9_]*)|\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
104
+
105
+ // ━━ FUNCTIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
106
+ /**
107
+ * Validates that all parameter names in a pathname pattern are unique.
108
+ *
109
+ * Parameter names are compared by their literal names regardless of whether
110
+ * they use colon or brace syntax.
111
+ *
112
+ * @private
113
+ * @param {string} pathnamePattern - Pathname pattern to validate.
114
+ * @returns {Validation} Validation result.
115
+ * @example
116
+ * ```js
117
+ * const validation = validateUniqueParamNames('/users/:id/posts/{postId}');
118
+ *
119
+ * console.log(validation.ok);
120
+ * // => true
121
+ * ```
122
+ * @example
123
+ * ```js
124
+ * const validation = validateUniqueParamNames('/users/:id/posts/{id}');
125
+ *
126
+ * console.log(validation.ok);
127
+ * // => false
128
+ * ```
129
+ */
130
+ function validateUniqueParamNames(pathnamePattern) {
131
+ const names = new Set();
132
+
133
+ for (const match of pathnamePattern.matchAll(PARAMETER_PATTERN_REGEXP)) {
134
+ const name = match[1] ?? match[2];
135
+
136
+ if (names.has(name)) {
137
+ return Validation.failure({
138
+ error: PathnamePatternError,
139
+ code: 'DUPLICATE_PARAMETER_NAME',
140
+ custom: true,
141
+ options: {
142
+ values: { name },
143
+ details: { pathnamePattern },
144
+ },
145
+ });
146
+ }
147
+
148
+ names.add(name);
149
+ }
150
+
151
+ return Validation.success();
152
+ }
153
+
154
+ // ━━ MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
155
+ /**
156
+ * Validates a pathname pattern against the supported pathname syntax and
157
+ * semantic constraints.
158
+ *
159
+ * Validation is performed in the following order:
160
+ *
161
+ * 1. The pathname pattern must be a string.
162
+ * 2. The pathname pattern must not be empty.
163
+ * 3. The pathname pattern must not exceed the maximum allowed length.
164
+ * 4. The pathname pattern must match the supported pathname syntax.
165
+ * 5. Parameter names must be unique.
166
+ *
167
+ * @param {unknown} pathnamePattern - Pathname pattern to validate.
168
+ * @returns {Validation} Validation result.
169
+ * @example
170
+ * ```js
171
+ * const validation = validatePathnamePattern('/users/:id');
172
+ *
173
+ * console.log(validation.ok);
174
+ * // => true
175
+ * ```
176
+ *
177
+ * @example
178
+ * ```js
179
+ * const validation = validatePathnamePattern('/users/:id/:id');
180
+ *
181
+ * console.log(validation.ok);
182
+ * // => false
183
+ * ```
184
+ */
185
+ export function validatePathnamePattern(pathnamePattern) {
186
+ if (typeof pathnamePattern !== 'string') {
187
+ return Validation.failure({
188
+ error: PathnamePatternError,
189
+ code: 'INVALID_PATTERN_TYPE',
190
+ custom: true,
191
+ options: {
192
+ values: { expected: 'string', received: typeof pathnamePattern },
193
+ },
194
+ });
195
+ }
196
+
197
+ if (pathnamePattern.length === 0) {
198
+ return Validation.failure({
199
+ error: PathnamePatternError,
200
+ code: 'EMPTY_PATTERN',
201
+ custom: true,
202
+ });
203
+ }
204
+
205
+ if (pathnamePattern.length > MAX_PATTERN_LENGTH) {
206
+ return Validation.failure({
207
+ error: PathnamePatternError,
208
+ code: 'PATTERN_TOO_LONG',
209
+ custom: true,
210
+ options: {
211
+ values: { maxLength: MAX_PATTERN_LENGTH, actualLength: pathnamePattern.length },
212
+ },
213
+ });
214
+ }
215
+
216
+ if (!PATHNAME_PATTERN_REGEXP.test(pathnamePattern)) {
217
+ return Validation.failure({
218
+ error: PathnamePatternError,
219
+ code: 'INVALID_PATHNAME_PATTERN',
220
+ custom: true,
221
+ options: {
222
+ values: { pathnamePattern },
223
+ details: { constraint: 'Must be * or start with / and contain valid segments' },
224
+ },
225
+ });
226
+ }
227
+
228
+ return validateUniqueParamNames(pathnamePattern);
229
+ }
230
+
231
+ /**
232
+ * Asserts that a pathname pattern is valid.
233
+ *
234
+ * @param {unknown} pathnamePattern - Pathname pattern to validate.
235
+ * @throws {PathnamePatternError} If the pathname pattern is not a string.
236
+ * @throws {PathnamePatternError} If the pathname pattern is empty.
237
+ * @throws {PathnamePatternError} If the pathname pattern exceeds the maximum length.
238
+ * @throws {PathnamePatternError} If the pathname pattern syntax is invalid.
239
+ * @throws {PathnamePatternError} If the pathname pattern has duplicate parameter names.
240
+ * @example
241
+ * ```js
242
+ * assertPathnamePattern('/users/:id');
243
+ * ```
244
+ *
245
+ * @example
246
+ * ```js
247
+ * assertPathnamePattern('/users/:id/:id');
248
+ * // throws PathnamePatternError with code 'DUPLICATE_PARAMETER_NAME'
249
+ * ```
250
+ */
251
+ export function assertPathnamePattern(pathnamePattern) {
252
+ const validation = validatePathnamePattern(pathnamePattern);
253
+ if (!validation.ok) {
254
+ throw validation.error;
255
+ }
256
+ }
@@ -0,0 +1,94 @@
1
+ // @ts-check
2
+ /**
3
+ * @file NotFoundHandler validation utilities.
4
+ *
5
+ * @description Validates handlers used to resolve requests that do not match
6
+ * any registered route.
7
+ *
8
+ * @module Validation/Server/NotFoundHandler
9
+ */
10
+
11
+ // ━━ IMPORT MODULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12
+ // » IMPORT LOCAL MODULES
13
+ import Validation from '#shared/outcome/Validation';
14
+ import ServerError from '#shared/errors/ServerError';
15
+
16
+ // ━━ MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17
+ /**
18
+ * Validates a not-found handler.
19
+ *
20
+ * A not-found handler must be a function that declares exactly one parameter:
21
+ * `exchange`.
22
+ *
23
+ * @param {unknown} handler - Handler function to validate.
24
+ * @returns {Validation} Validation result.
25
+ * @example
26
+ * ```js
27
+ * const validation = validateNotFoundHandler((exchange) => {
28
+ * exchange.response.status(404).end();
29
+ * });
30
+ *
31
+ * console.log(validation.ok);
32
+ * // => true
33
+ * ```
34
+ *
35
+ * @example
36
+ * ```js
37
+ * const validation = validateNotFoundHandler(() => {});
38
+ *
39
+ * console.log(validation.ok);
40
+ * // => false
41
+ * ```
42
+ */
43
+ export function validateNotFoundHandler(handler) {
44
+ if (typeof handler !== 'function') {
45
+ return Validation.failure({
46
+ error: ServerError,
47
+ code: 'INVALID_NOT_FOUND_HANDLER_TYPE',
48
+ custom: true,
49
+ options: {
50
+ values: {
51
+ expected: 'function',
52
+ received: typeof handler,
53
+ },
54
+ },
55
+ });
56
+ }
57
+
58
+ if (handler.length !== 1) {
59
+ return Validation.failure({
60
+ error: ServerError,
61
+ code: 'INVALID_NOT_FOUND_HANDLER_ARITY',
62
+ custom: true,
63
+ options: {
64
+ values: {
65
+ expected: 1,
66
+ received: handler.length,
67
+ },
68
+ },
69
+ });
70
+ }
71
+
72
+ return Validation.success();
73
+ }
74
+
75
+ /**
76
+ * Asserts that a not-found handler is valid.
77
+ *
78
+ * @param {unknown} handler - Handler function to validate.
79
+ * @throws {ServerError} If the handler is not a function.
80
+ * @throws {ServerError} If the handler does not declare exactly one parameter.
81
+ * @example
82
+ * ```js
83
+ * assertNotFoundHandler((exchange) => {
84
+ * exchange.response.status(404).end();
85
+ * });
86
+ * ```
87
+ */
88
+ export function assertNotFoundHandler(handler) {
89
+ const validation = validateNotFoundHandler(handler);
90
+
91
+ if (!validation.ok) {
92
+ throw validation.error;
93
+ }
94
+ }