@63klabs/cache-data 1.3.7 → 1.3.9
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/AGENTS.md +1107 -0
- package/CHANGELOG.md +121 -2
- package/CONTRIBUTING.md +4 -5
- package/README.md +50 -25
- package/eslint.config.js +53 -0
- package/package.json +26 -14
- package/src/lib/dao-cache.js +30 -14
- package/src/lib/tools/APIRequest.class.js +948 -91
- package/src/lib/tools/AWS.classes.js +58 -5
- package/src/lib/tools/CachedParametersSecrets.classes.js +1 -0
- package/src/lib/tools/ClientRequest.class.js +858 -31
- package/src/lib/tools/Connections.classes.js +40 -3
- package/src/lib/tools/Response.class.js +11 -0
- package/src/lib/tools/generic.response.html.js +33 -0
- package/src/lib/tools/generic.response.json.js +40 -1
- package/src/lib/tools/generic.response.rss.js +33 -0
- package/src/lib/tools/generic.response.text.js +34 -1
- package/src/lib/tools/generic.response.xml.js +39 -0
- package/src/lib/tools/index.js +211 -50
- package/src/lib/utils/ValidationExecutor.class.js +66 -0
- package/src/lib/utils/ValidationMatcher.class.js +405 -0
|
@@ -2,13 +2,62 @@ const RequestInfo = require('./RequestInfo.class');
|
|
|
2
2
|
const Timer = require('./Timer.class');
|
|
3
3
|
const DebugAndLog = require('./DebugAndLog.class');
|
|
4
4
|
const { safeClone } = require('./utils');
|
|
5
|
+
const ValidationMatcher = require('../utils/ValidationMatcher.class');
|
|
6
|
+
const ValidationExecutor = require('../utils/ValidationExecutor.class');
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
|
-
* Extends RequestInfo
|
|
9
|
-
*
|
|
10
|
+
* Extends RequestInfo to provide request validation, parameter extraction, and authentication.
|
|
11
|
+
*
|
|
12
|
+
* ClientRequest processes Lambda API Gateway events and validates parameters using a flexible
|
|
13
|
+
* validation system that supports global, route-specific, method-specific, and method-and-route
|
|
14
|
+
* specific validation rules with clear priority ordering.
|
|
15
|
+
*
|
|
16
|
+
* Validation Priority Order (highest to lowest):
|
|
17
|
+
* 1. Method-and-route match (BY_ROUTE with "METHOD:route")
|
|
18
|
+
* 2. Route-only match (BY_ROUTE with "route")
|
|
19
|
+
* 3. Method-only match (BY_METHOD with "METHOD")
|
|
20
|
+
* 4. Global parameter name
|
|
21
|
+
*
|
|
22
|
+
* Header Key Format Conversion:
|
|
23
|
+
*
|
|
24
|
+
* HTTP headers use kebab-case naming (e.g., 'content-type', 'x-api-key'), but JavaScript
|
|
25
|
+
* property naming conventions prefer camelCase. ClientRequest automatically converts header
|
|
26
|
+
* keys from kebab-case to camelCase during validation and parameter extraction to maintain
|
|
27
|
+
* consistency with JavaScript naming standards.
|
|
28
|
+
*
|
|
29
|
+
* This conversion is necessary because:
|
|
30
|
+
* - JavaScript object properties conventionally use camelCase
|
|
31
|
+
* - Accessing headers as object properties (e.g., headers.contentType) is more idiomatic
|
|
32
|
+
* - Validation rule keys should match JavaScript property naming conventions
|
|
33
|
+
*
|
|
34
|
+
* Conversion Algorithm:
|
|
35
|
+
* 1. Convert entire header name to lowercase
|
|
36
|
+
* 2. Replace each hyphen followed by a letter with the uppercase letter
|
|
37
|
+
* 3. Remove all hyphens
|
|
38
|
+
*
|
|
39
|
+
* Header Key Conversion Reference Table:
|
|
40
|
+
*
|
|
41
|
+
* | HTTP Header (kebab-case) | JavaScript Property (camelCase) |
|
|
42
|
+
* |---------------------------|----------------------------------|
|
|
43
|
+
* | content-type | contentType |
|
|
44
|
+
* | Content-Type | contentType |
|
|
45
|
+
* | authorization | authorization |
|
|
46
|
+
* | x-api-key | xApiKey |
|
|
47
|
+
* | X-API-Key | xApiKey |
|
|
48
|
+
* | x-custom-header | xCustomHeader |
|
|
49
|
+
* | if-modified-since | ifModifiedSince |
|
|
50
|
+
* | if-none-match | ifNoneMatch |
|
|
51
|
+
* | cache-control | cacheControl |
|
|
52
|
+
* | user-agent | userAgent |
|
|
53
|
+
* | accept-encoding | acceptEncoding |
|
|
54
|
+
*
|
|
55
|
+
* When configuring header parameter validation rules, use the camelCase property names
|
|
56
|
+
* shown in the right column. Use the static method convertHeaderKeyToCamelCase() to
|
|
57
|
+
* determine the correct property name for any HTTP header
|
|
58
|
+
*
|
|
10
59
|
* @example
|
|
11
|
-
* // Initialize ClientRequest with validations
|
|
60
|
+
* // Initialize ClientRequest with global validations (backwards compatible)
|
|
12
61
|
* ClientRequest.init({
|
|
13
62
|
* validations: {
|
|
14
63
|
* referrers: ['example.com', 'myapp.com'],
|
|
@@ -25,6 +74,294 @@ const { safeClone } = require('./utils');
|
|
|
25
74
|
* });
|
|
26
75
|
*
|
|
27
76
|
* @example
|
|
77
|
+
* // Initialize with route-specific validations
|
|
78
|
+
* ClientRequest.init({
|
|
79
|
+
* parameters: {
|
|
80
|
+
* pathParameters: {
|
|
81
|
+
* // Global validation (Priority 4) - applies to all routes
|
|
82
|
+
* id: (value) => typeof value === 'string' && value.length > 0,
|
|
83
|
+
*
|
|
84
|
+
* // Route-specific validations (Priority 2)
|
|
85
|
+
* BY_ROUTE: [
|
|
86
|
+
* {
|
|
87
|
+
* route: "product/{id}",
|
|
88
|
+
* validate: (value) => /^P-[0-9]+$/.test(value)
|
|
89
|
+
* },
|
|
90
|
+
* {
|
|
91
|
+
* route: "employee/{id}",
|
|
92
|
+
* validate: (value) => /^E-[0-9]+$/.test(value)
|
|
93
|
+
* }
|
|
94
|
+
* ]
|
|
95
|
+
* }
|
|
96
|
+
* }
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* // Initialize with method-specific validations
|
|
101
|
+
* ClientRequest.init({
|
|
102
|
+
* parameters: {
|
|
103
|
+
* pathParameters: {
|
|
104
|
+
* id: (value) => typeof value === 'string',
|
|
105
|
+
*
|
|
106
|
+
* // Method-specific validations (Priority 3)
|
|
107
|
+
* BY_METHOD: [
|
|
108
|
+
* {
|
|
109
|
+
* method: "POST",
|
|
110
|
+
* validate: (value) => value.length <= 50
|
|
111
|
+
* },
|
|
112
|
+
* {
|
|
113
|
+
* method: "GET",
|
|
114
|
+
* validate: (value) => value.length > 0
|
|
115
|
+
* }
|
|
116
|
+
* ]
|
|
117
|
+
* }
|
|
118
|
+
* }
|
|
119
|
+
* });
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* // Initialize with method-and-route validations (highest priority)
|
|
123
|
+
* ClientRequest.init({
|
|
124
|
+
* parameters: {
|
|
125
|
+
* pathParameters: {
|
|
126
|
+
* BY_ROUTE: [
|
|
127
|
+
* {
|
|
128
|
+
* route: "POST:game/join/{id}", // Priority 1: Method-and-route
|
|
129
|
+
* validate: (value) => /^[0-9]{6}$/.test(value)
|
|
130
|
+
* },
|
|
131
|
+
* {
|
|
132
|
+
* route: "GET:game/join/{id}", // Priority 1: Method-and-route
|
|
133
|
+
* validate: (value) => /^[0-9]+$/.test(value)
|
|
134
|
+
* }
|
|
135
|
+
* ]
|
|
136
|
+
* }
|
|
137
|
+
* }
|
|
138
|
+
* });
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* // Multi-parameter validation - validate multiple parameters together
|
|
142
|
+
* ClientRequest.init({
|
|
143
|
+
* parameters: {
|
|
144
|
+
* queryStringParameters: {
|
|
145
|
+
* BY_ROUTE: [
|
|
146
|
+
* {
|
|
147
|
+
* route: "search?query,limit", // Specify multiple parameters
|
|
148
|
+
* validate: ({query, limit}) => {
|
|
149
|
+
* // Validation function receives object with all specified parameters
|
|
150
|
+
* return query.length > 0 && limit >= 1 && limit <= 100;
|
|
151
|
+
* }
|
|
152
|
+
* }
|
|
153
|
+
* ]
|
|
154
|
+
* }
|
|
155
|
+
* }
|
|
156
|
+
* });
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* // Header parameter validation with camelCase property names
|
|
160
|
+
* // Note: HTTP headers are automatically converted from kebab-case to camelCase
|
|
161
|
+
* ClientRequest.init({
|
|
162
|
+
* parameters: {
|
|
163
|
+
* headerParameters: {
|
|
164
|
+
* // Use camelCase for header validation rules
|
|
165
|
+
* contentType: (value) => value === 'application/json',
|
|
166
|
+
* authorization: (value) => value.startsWith('Bearer '),
|
|
167
|
+
* xApiKey: (value) => /^[a-zA-Z0-9]{32}$/.test(value),
|
|
168
|
+
*
|
|
169
|
+
* // Use convertHeaderKeyToCamelCase() to determine correct property names
|
|
170
|
+
* // ClientRequest.convertHeaderKeyToCamelCase('x-custom-header') returns 'xCustomHeader'
|
|
171
|
+
* xCustomHeader: (value) => value.length > 0
|
|
172
|
+
* }
|
|
173
|
+
* }
|
|
174
|
+
* });
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* // Body parameter validation - basic single-field validation
|
|
178
|
+
* // Body content is automatically parsed from JSON before validation
|
|
179
|
+
* ClientRequest.init({
|
|
180
|
+
* parameters: {
|
|
181
|
+
* bodyParameters: {
|
|
182
|
+
* // Global validation for specific fields
|
|
183
|
+
* email: (value) => typeof value === 'string' && value.includes('@'),
|
|
184
|
+
* age: (value) => typeof value === 'number' && value >= 0 && value <= 150,
|
|
185
|
+
* username: (value) => /^[a-zA-Z0-9_-]{3,20}$/.test(value)
|
|
186
|
+
* }
|
|
187
|
+
* }
|
|
188
|
+
* });
|
|
189
|
+
*
|
|
190
|
+
* // In Lambda handler
|
|
191
|
+
* exports.handler = async (event, context) => {
|
|
192
|
+
* // event.body = '{"email":"user@example.com","age":25,"username":"john_doe"}'
|
|
193
|
+
* const clientRequest = new ClientRequest(event, context);
|
|
194
|
+
*
|
|
195
|
+
* if (!clientRequest.isValid()) {
|
|
196
|
+
* return { statusCode: 400, body: 'Invalid request body' };
|
|
197
|
+
* }
|
|
198
|
+
*
|
|
199
|
+
* // Access validated body parameters
|
|
200
|
+
* const bodyParams = clientRequest.getBodyParameters();
|
|
201
|
+
* console.log(bodyParams.email); // 'user@example.com'
|
|
202
|
+
* console.log(bodyParams.age); // 25
|
|
203
|
+
* console.log(bodyParams.username); // 'john_doe'
|
|
204
|
+
* };
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* // Body parameter validation - multi-field validation
|
|
208
|
+
* // Validate multiple fields together with complex business logic
|
|
209
|
+
* ClientRequest.init({
|
|
210
|
+
* parameters: {
|
|
211
|
+
* bodyParameters: {
|
|
212
|
+
* BY_ROUTE: [
|
|
213
|
+
* {
|
|
214
|
+
* route: 'POST:users',
|
|
215
|
+
* validate: ({email, password, confirmPassword}) => {
|
|
216
|
+
* // Multi-field validation: password confirmation
|
|
217
|
+
* return email && password &&
|
|
218
|
+
* password === confirmPassword &&
|
|
219
|
+
* password.length >= 8 &&
|
|
220
|
+
* email.includes('@');
|
|
221
|
+
* }
|
|
222
|
+
* },
|
|
223
|
+
* {
|
|
224
|
+
* route: 'PUT:users/{id}',
|
|
225
|
+
* validate: ({email, username}) => {
|
|
226
|
+
* // At least one field must be provided for update
|
|
227
|
+
* return email || username;
|
|
228
|
+
* }
|
|
229
|
+
* }
|
|
230
|
+
* ]
|
|
231
|
+
* }
|
|
232
|
+
* }
|
|
233
|
+
* });
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* // Body parameter validation - error handling for invalid JSON
|
|
237
|
+
* // ClientRequest handles JSON parsing errors gracefully
|
|
238
|
+
* exports.handler = async (event, context) => {
|
|
239
|
+
* // event.body = 'invalid json{' (malformed JSON)
|
|
240
|
+
* const clientRequest = new ClientRequest(event, context);
|
|
241
|
+
*
|
|
242
|
+
* if (!clientRequest.isValid()) {
|
|
243
|
+
* // Validation fails for invalid JSON
|
|
244
|
+
* return {
|
|
245
|
+
* statusCode: 400,
|
|
246
|
+
* body: JSON.stringify({ error: 'Invalid request body' })
|
|
247
|
+
* };
|
|
248
|
+
* }
|
|
249
|
+
*
|
|
250
|
+
* // This code only runs if body is valid JSON and passes validation
|
|
251
|
+
* const bodyParams = clientRequest.getBodyParameters();
|
|
252
|
+
* // Process valid body parameters...
|
|
253
|
+
* };
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* // Body parameter validation - complex nested objects and arrays
|
|
257
|
+
* ClientRequest.init({
|
|
258
|
+
* parameters: {
|
|
259
|
+
* bodyParameters: {
|
|
260
|
+
* // Validate nested object structure
|
|
261
|
+
* user: (value) => {
|
|
262
|
+
* return value &&
|
|
263
|
+
* typeof value === 'object' &&
|
|
264
|
+
* typeof value.name === 'string' &&
|
|
265
|
+
* typeof value.email === 'string' &&
|
|
266
|
+
* value.email.includes('@');
|
|
267
|
+
* },
|
|
268
|
+
*
|
|
269
|
+
* // Validate array of items
|
|
270
|
+
* items: (value) => {
|
|
271
|
+
* return Array.isArray(value) &&
|
|
272
|
+
* value.length > 0 &&
|
|
273
|
+
* value.every(item =>
|
|
274
|
+
* item.id &&
|
|
275
|
+
* typeof item.quantity === 'number' &&
|
|
276
|
+
* item.quantity > 0
|
|
277
|
+
* );
|
|
278
|
+
* },
|
|
279
|
+
*
|
|
280
|
+
* // Validate nested array of objects with specific structure
|
|
281
|
+
* addresses: (value) => {
|
|
282
|
+
* return Array.isArray(value) &&
|
|
283
|
+
* value.every(addr =>
|
|
284
|
+
* addr.street &&
|
|
285
|
+
* addr.city &&
|
|
286
|
+
* addr.zipCode &&
|
|
287
|
+
* /^\d{5}$/.test(addr.zipCode)
|
|
288
|
+
* );
|
|
289
|
+
* }
|
|
290
|
+
* }
|
|
291
|
+
* }
|
|
292
|
+
* });
|
|
293
|
+
*
|
|
294
|
+
* // Example request body:
|
|
295
|
+
* // {
|
|
296
|
+
* // "user": {
|
|
297
|
+
* // "name": "John Doe",
|
|
298
|
+
* // "email": "john@example.com"
|
|
299
|
+
* // },
|
|
300
|
+
* // "items": [
|
|
301
|
+
* // {"id": "item-1", "quantity": 2},
|
|
302
|
+
* // {"id": "item-2", "quantity": 1}
|
|
303
|
+
* // ],
|
|
304
|
+
* // "addresses": [
|
|
305
|
+
* // {"street": "123 Main St", "city": "Boston", "zipCode": "02101"}
|
|
306
|
+
* // ]
|
|
307
|
+
* // }
|
|
308
|
+
*
|
|
309
|
+
* exports.handler = async (event, context) => {
|
|
310
|
+
* const clientRequest = new ClientRequest(event, context);
|
|
311
|
+
*
|
|
312
|
+
* if (!clientRequest.isValid()) {
|
|
313
|
+
* return { statusCode: 400, body: 'Invalid request body structure' };
|
|
314
|
+
* }
|
|
315
|
+
*
|
|
316
|
+
* const bodyParams = clientRequest.getBodyParameters();
|
|
317
|
+
* console.log(bodyParams.user.name); // 'John Doe'
|
|
318
|
+
* console.log(bodyParams.items.length); // 2
|
|
319
|
+
* console.log(bodyParams.addresses[0].city); // 'Boston'
|
|
320
|
+
* };
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* // Body parameter validation - combining with other parameter types
|
|
324
|
+
* ClientRequest.init({
|
|
325
|
+
* parameters: {
|
|
326
|
+
* pathParameters: {
|
|
327
|
+
* id: (value) => /^[0-9]+$/.test(value)
|
|
328
|
+
* },
|
|
329
|
+
* bodyParameters: {
|
|
330
|
+
* BY_ROUTE: [
|
|
331
|
+
* {
|
|
332
|
+
* route: 'PUT:users/{id}',
|
|
333
|
+
* validate: ({email, username, bio}) => {
|
|
334
|
+
* // Validate update payload
|
|
335
|
+
* const hasAtLeastOneField = email || username || bio;
|
|
336
|
+
* const emailValid = !email || email.includes('@');
|
|
337
|
+
* const usernameValid = !username || /^[a-zA-Z0-9_-]{3,20}$/.test(username);
|
|
338
|
+
* const bioValid = !bio || bio.length <= 500;
|
|
339
|
+
*
|
|
340
|
+
* return hasAtLeastOneField && emailValid && usernameValid && bioValid;
|
|
341
|
+
* }
|
|
342
|
+
* }
|
|
343
|
+
* ]
|
|
344
|
+
* }
|
|
345
|
+
* }
|
|
346
|
+
* });
|
|
347
|
+
*
|
|
348
|
+
* exports.handler = async (event, context) => {
|
|
349
|
+
* const clientRequest = new ClientRequest(event, context);
|
|
350
|
+
*
|
|
351
|
+
* if (!clientRequest.isValid()) {
|
|
352
|
+
* return { statusCode: 400, body: 'Invalid request' };
|
|
353
|
+
* }
|
|
354
|
+
*
|
|
355
|
+
* // All parameter types are validated
|
|
356
|
+
* const userId = clientRequest.getPathParameters().id;
|
|
357
|
+
* const updates = clientRequest.getBodyParameters();
|
|
358
|
+
*
|
|
359
|
+
* // Update user with validated data
|
|
360
|
+
* await updateUser(userId, updates);
|
|
361
|
+
* return { statusCode: 200, body: 'User updated' };
|
|
362
|
+
* };
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
28
365
|
* // Use in Lambda handler
|
|
29
366
|
* exports.handler = async (event, context) => {
|
|
30
367
|
* const clientRequest = new ClientRequest(event, context);
|
|
@@ -61,6 +398,9 @@ class ClientRequest extends RequestInfo {
|
|
|
61
398
|
#context = null;
|
|
62
399
|
#authorizations = safeClone(ClientRequest.#unauthenticatedAuthorizations);
|
|
63
400
|
#roles = [];
|
|
401
|
+
|
|
402
|
+
/* Validation system */
|
|
403
|
+
#validationMatchers = {};
|
|
64
404
|
|
|
65
405
|
/* The request data */
|
|
66
406
|
#props = {};
|
|
@@ -73,11 +413,37 @@ class ClientRequest extends RequestInfo {
|
|
|
73
413
|
apiKey: null
|
|
74
414
|
}
|
|
75
415
|
|
|
76
|
-
|
|
77
416
|
/**
|
|
78
|
-
* Initializes the request data based on the event
|
|
79
|
-
*
|
|
80
|
-
*
|
|
417
|
+
* Initializes the request data based on the event and performs parameter validation.
|
|
418
|
+
*
|
|
419
|
+
* The constructor initializes ValidationMatchers for each parameter type (path, query, header, cookie)
|
|
420
|
+
* using the configured validation rules. Validation is performed immediately during construction,
|
|
421
|
+
* and the request validity can be checked using isValid().
|
|
422
|
+
*
|
|
423
|
+
* Validation uses a four-tier priority system:
|
|
424
|
+
* 1. Method-and-route match (BY_ROUTE with "METHOD:route") - Most specific
|
|
425
|
+
* 2. Route-only match (BY_ROUTE with "route")
|
|
426
|
+
* 3. Method-only match (BY_METHOD with "METHOD")
|
|
427
|
+
* 4. Global parameter name - Least specific
|
|
428
|
+
*
|
|
429
|
+
* @param {Object} event - Lambda API Gateway event object
|
|
430
|
+
* @param {Object} context - Lambda context object
|
|
431
|
+
* @example
|
|
432
|
+
* // Basic usage
|
|
433
|
+
* const clientRequest = new ClientRequest(event, context);
|
|
434
|
+
* if (clientRequest.isValid()) {
|
|
435
|
+
* // Process valid request
|
|
436
|
+
* }
|
|
437
|
+
*
|
|
438
|
+
* @example
|
|
439
|
+
* // With route-specific validation
|
|
440
|
+
* // If event.resource = "/product/{id}" and event.httpMethod = "GET"
|
|
441
|
+
* // ValidationMatcher will check:
|
|
442
|
+
* // 1. GET:product/{id} validation (if exists)
|
|
443
|
+
* // 2. product/{id} validation (if exists)
|
|
444
|
+
* // 3. GET method validation (if exists)
|
|
445
|
+
* // 4. Global 'id' parameter validation (if exists)
|
|
446
|
+
* const clientRequest = new ClientRequest(event, context);
|
|
81
447
|
*/
|
|
82
448
|
constructor(event, context) {
|
|
83
449
|
super(event);
|
|
@@ -101,6 +467,7 @@ class ClientRequest extends RequestInfo {
|
|
|
101
467
|
queryStringParameters: {},
|
|
102
468
|
headerParameters: {},
|
|
103
469
|
cookieParameters: {},
|
|
470
|
+
bodyParameters: {},
|
|
104
471
|
bodyPayload: this.#event?.body || null, // from body
|
|
105
472
|
client: {
|
|
106
473
|
isAuthenticated: this.isAuthenticated(),
|
|
@@ -112,19 +479,187 @@ class ClientRequest extends RequestInfo {
|
|
|
112
479
|
calcMsToDeadline: this.calcMsToDeadline
|
|
113
480
|
};
|
|
114
481
|
|
|
482
|
+
// >! Initialize ValidationMatchers for each parameter type
|
|
483
|
+
// >! This enables route-specific and method-specific validations
|
|
484
|
+
const httpMethod = this.#event.httpMethod || '';
|
|
485
|
+
const resourcePath = resource || '';
|
|
486
|
+
const paramValidations = ClientRequest.getParameterValidations();
|
|
487
|
+
|
|
488
|
+
// >! Support both queryParameters and queryStringParameters for backwards compatibility
|
|
489
|
+
const queryValidations = paramValidations?.queryStringParameters || paramValidations?.queryParameters;
|
|
490
|
+
|
|
491
|
+
this.#validationMatchers = {
|
|
492
|
+
pathParameters: new ValidationMatcher(paramValidations?.pathParameters, httpMethod, resourcePath),
|
|
493
|
+
queryStringParameters: new ValidationMatcher(queryValidations, httpMethod, resourcePath),
|
|
494
|
+
headerParameters: new ValidationMatcher(paramValidations?.headerParameters, httpMethod, resourcePath),
|
|
495
|
+
cookieParameters: new ValidationMatcher(paramValidations?.cookieParameters, httpMethod, resourcePath),
|
|
496
|
+
bodyParameters: new ValidationMatcher(paramValidations?.bodyParameters, httpMethod, resourcePath)
|
|
497
|
+
};
|
|
498
|
+
|
|
115
499
|
this.#validate();
|
|
116
500
|
|
|
117
501
|
};
|
|
118
502
|
|
|
119
503
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
504
|
+
* Initialize the ClientRequest class with validation configuration.
|
|
505
|
+
*
|
|
506
|
+
* This method configures the validation system for all ClientRequest instances.
|
|
507
|
+
* Call this once during application initialization, before creating any ClientRequest instances.
|
|
508
|
+
*
|
|
509
|
+
* Validation Configuration Structure:
|
|
510
|
+
* - referrers: Array of allowed referrer domains (use ['*'] to allow all)
|
|
511
|
+
* - parameters: Object containing validation rules for each parameter type
|
|
512
|
+
* - pathParameters, queryStringParameters, headerParameters, cookieParameters, bodyParameters
|
|
513
|
+
* - Each parameter type can have:
|
|
514
|
+
* - Global validations: paramName: (value) => boolean
|
|
515
|
+
* - BY_ROUTE: Array of route-specific validation rules
|
|
516
|
+
* - BY_METHOD: Array of method-specific validation rules
|
|
517
|
+
*
|
|
518
|
+
* Parameter Specification Syntax:
|
|
519
|
+
* - Path parameters: "route/{param}" - Validates 'param' path parameter
|
|
520
|
+
* - Query parameters: "route?param" - Validates 'param' query parameter
|
|
521
|
+
* - Multiple parameters: "route?param1,param2" - Validates both parameters together
|
|
522
|
+
* - Method-and-route: "METHOD:route" - Applies only to specific HTTP method and route
|
|
523
|
+
*
|
|
524
|
+
* @param {Object} options - Configuration options
|
|
525
|
+
* @param {Array<string>} [options.referrers] - Array of allowed referrers (use ['*'] for all)
|
|
526
|
+
* @param {Object} [options.parameters] - Parameter validation configuration
|
|
527
|
+
* @param {Object} [options.parameters.pathParameters] - Path parameter validations
|
|
528
|
+
* @param {Object} [options.parameters.queryStringParameters] - Query parameter validations
|
|
529
|
+
* @param {Object} [options.parameters.headerParameters] - Header parameter validations
|
|
530
|
+
* @param {Object} [options.parameters.cookieParameters] - Cookie parameter validations
|
|
531
|
+
* @param {Object} [options.parameters.bodyParameters] - Body parameter validations
|
|
532
|
+
* @param {Array<{route: string, validate: Function}>} [options.parameters.*.BY_ROUTE] - Route-specific validations
|
|
533
|
+
* @param {Array<{method: string, validate: Function}>} [options.parameters.*.BY_METHOD] - Method-specific validations
|
|
534
|
+
* @param {boolean} [options.parameters.excludeParamsWithNoValidationMatch=true] - Exclude parameters without validation rules
|
|
124
535
|
* @throws {Error} If options is not an object
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* // Global validations only (backwards compatible)
|
|
539
|
+
* ClientRequest.init({
|
|
540
|
+
* referrers: ['example.com'],
|
|
541
|
+
* parameters: {
|
|
542
|
+
* pathParameters: {
|
|
543
|
+
* id: (value) => /^[a-zA-Z0-9-]+$/.test(value)
|
|
544
|
+
* },
|
|
545
|
+
* queryStringParameters: {
|
|
546
|
+
* limit: (value) => !isNaN(value) && value > 0 && value <= 100
|
|
547
|
+
* }
|
|
548
|
+
* }
|
|
549
|
+
* });
|
|
550
|
+
*
|
|
551
|
+
* @example
|
|
552
|
+
* // Route-specific validations (Priority 2)
|
|
553
|
+
* ClientRequest.init({
|
|
554
|
+
* parameters: {
|
|
555
|
+
* pathParameters: {
|
|
556
|
+
* id: (value) => typeof value === 'string', // Global fallback
|
|
557
|
+
* BY_ROUTE: [
|
|
558
|
+
* {
|
|
559
|
+
* route: "product/{id}",
|
|
560
|
+
* validate: (value) => /^P-[0-9]+$/.test(value)
|
|
561
|
+
* },
|
|
562
|
+
* {
|
|
563
|
+
* route: "employee/{id}",
|
|
564
|
+
* validate: (value) => /^E-[0-9]+$/.test(value)
|
|
565
|
+
* }
|
|
566
|
+
* ]
|
|
567
|
+
* }
|
|
568
|
+
* }
|
|
569
|
+
* });
|
|
570
|
+
*
|
|
571
|
+
* @example
|
|
572
|
+
* // Method-specific validations (Priority 3)
|
|
573
|
+
* ClientRequest.init({
|
|
574
|
+
* parameters: {
|
|
575
|
+
* pathParameters: {
|
|
576
|
+
* BY_METHOD: [
|
|
577
|
+
* {
|
|
578
|
+
* method: "POST",
|
|
579
|
+
* validate: (value) => value.length <= 50
|
|
580
|
+
* },
|
|
581
|
+
* {
|
|
582
|
+
* method: "GET",
|
|
583
|
+
* validate: (value) => value.length > 0
|
|
584
|
+
* }
|
|
585
|
+
* ]
|
|
586
|
+
* }
|
|
587
|
+
* }
|
|
588
|
+
* });
|
|
589
|
+
*
|
|
590
|
+
* @example
|
|
591
|
+
* // Method-and-route validations (Priority 1 - highest)
|
|
592
|
+
* ClientRequest.init({
|
|
593
|
+
* parameters: {
|
|
594
|
+
* pathParameters: {
|
|
595
|
+
* BY_ROUTE: [
|
|
596
|
+
* {
|
|
597
|
+
* route: "POST:game/join/{id}",
|
|
598
|
+
* validate: (value) => /^[0-9]{6}$/.test(value)
|
|
599
|
+
* },
|
|
600
|
+
* {
|
|
601
|
+
* route: "GET:game/join/{id}",
|
|
602
|
+
* validate: (value) => /^[0-9]+$/.test(value)
|
|
603
|
+
* }
|
|
604
|
+
* ]
|
|
605
|
+
* }
|
|
606
|
+
* }
|
|
607
|
+
* });
|
|
608
|
+
*
|
|
609
|
+
* @example
|
|
610
|
+
* // Multi-parameter validation
|
|
611
|
+
* ClientRequest.init({
|
|
612
|
+
* parameters: {
|
|
613
|
+
* queryStringParameters: {
|
|
614
|
+
* BY_ROUTE: [
|
|
615
|
+
* {
|
|
616
|
+
* route: "search?query,limit", // Specify multiple parameters
|
|
617
|
+
* validate: ({query, limit}) => {
|
|
618
|
+
* // Validation function receives object with all parameters
|
|
619
|
+
* return query.length > 0 && limit >= 1 && limit <= 100;
|
|
620
|
+
* }
|
|
621
|
+
* }
|
|
622
|
+
* ]
|
|
623
|
+
* }
|
|
624
|
+
* }
|
|
625
|
+
* });
|
|
626
|
+
*
|
|
627
|
+
* @example
|
|
628
|
+
* // Mixed priority levels
|
|
629
|
+
* ClientRequest.init({
|
|
630
|
+
* parameters: {
|
|
631
|
+
* pathParameters: {
|
|
632
|
+
* id: (value) => typeof value === 'string', // Priority 4: Global
|
|
633
|
+
* BY_METHOD: [
|
|
634
|
+
* {
|
|
635
|
+
* method: "POST", // Priority 3: Method-only
|
|
636
|
+
* validate: (value) => value.length <= 50
|
|
637
|
+
* }
|
|
638
|
+
* ],
|
|
639
|
+
* BY_ROUTE: [
|
|
640
|
+
* {
|
|
641
|
+
* route: "product/{id}", // Priority 2: Route-only
|
|
642
|
+
* validate: (value) => /^P-[0-9]+$/.test(value)
|
|
643
|
+
* },
|
|
644
|
+
* {
|
|
645
|
+
* route: "POST:product/{id}", // Priority 1: Method-and-route
|
|
646
|
+
* validate: (value) => /^P-[0-9]{4}$/.test(value)
|
|
647
|
+
* }
|
|
648
|
+
* ]
|
|
649
|
+
* }
|
|
650
|
+
* }
|
|
651
|
+
* });
|
|
125
652
|
*/
|
|
126
653
|
static init(options) {
|
|
127
654
|
if (typeof options === 'object') {
|
|
655
|
+
if ('referrers' in options) {
|
|
656
|
+
ClientRequest.#validations.referrers = options.referrers;
|
|
657
|
+
}
|
|
658
|
+
if ('parameters' in options) {
|
|
659
|
+
ClientRequest.#validations.parameters = options.parameters;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Backwards compatibility - deprecated
|
|
128
663
|
if ('validations' in options) {
|
|
129
664
|
if ('referrers' in options.validations) {
|
|
130
665
|
ClientRequest.#validations.referrers = options.validations.referrers;
|
|
@@ -141,13 +676,78 @@ class ClientRequest extends RequestInfo {
|
|
|
141
676
|
|
|
142
677
|
};
|
|
143
678
|
|
|
679
|
+
/**
|
|
680
|
+
* Returns the current validation rules
|
|
681
|
+
* @returns {{referrerWhiteList<Array>}} validations
|
|
682
|
+
*/
|
|
683
|
+
static info() {
|
|
684
|
+
return {
|
|
685
|
+
referrerWhiteList: ClientRequest.getReferrerWhiteList(),
|
|
686
|
+
};
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Allowed referrers
|
|
691
|
+
* @returns {Array<string>} Allowed referrers
|
|
692
|
+
*/
|
|
144
693
|
static getReferrerWhiteList() {
|
|
145
694
|
return ClientRequest.#validations.referrers;
|
|
146
695
|
};
|
|
147
696
|
|
|
697
|
+
/**
|
|
698
|
+
* Parameter validations
|
|
699
|
+
* @returns {{
|
|
700
|
+
* pathParameters?: object,
|
|
701
|
+
* queryParameters?: object,
|
|
702
|
+
* headerParameters?: object,
|
|
703
|
+
* cookieParameters?: object,
|
|
704
|
+
* bodyParameters?: object
|
|
705
|
+
* }}
|
|
706
|
+
*/
|
|
148
707
|
static getParameterValidations() {
|
|
149
708
|
return ClientRequest.#validations.parameters;
|
|
150
709
|
};
|
|
710
|
+
/**
|
|
711
|
+
* Convert HTTP header key from kebab-case to camelCase.
|
|
712
|
+
*
|
|
713
|
+
* This utility method helps developers determine the correct key names for header validation rules.
|
|
714
|
+
* HTTP headers use kebab-case (e.g., 'content-type'), but ClientRequest converts them to camelCase
|
|
715
|
+
* (e.g., 'contentType') during validation for JavaScript property naming conventions.
|
|
716
|
+
*
|
|
717
|
+
* The conversion algorithm:
|
|
718
|
+
* 1. Convert entire string to lowercase
|
|
719
|
+
* 2. Replace each hyphen followed by a letter with the uppercase letter
|
|
720
|
+
* 3. Remove all hyphens
|
|
721
|
+
*
|
|
722
|
+
* @param {string} headerKey - HTTP header key in kebab-case (e.g., 'Content-Type', 'x-custom-header')
|
|
723
|
+
* @returns {string} Header key in camelCase (e.g., 'contentType', 'xCustomHeader')
|
|
724
|
+
* @example
|
|
725
|
+
* // Common HTTP headers
|
|
726
|
+
* ClientRequest.convertHeaderKeyToCamelCase('content-type'); // 'contentType'
|
|
727
|
+
* ClientRequest.convertHeaderKeyToCamelCase('Content-Type'); // 'contentType'
|
|
728
|
+
* ClientRequest.convertHeaderKeyToCamelCase('x-api-key'); // 'xApiKey'
|
|
729
|
+
*
|
|
730
|
+
* @example
|
|
731
|
+
* // Multiple hyphens
|
|
732
|
+
* ClientRequest.convertHeaderKeyToCamelCase('x-custom-header-name'); // 'xCustomHeaderName'
|
|
733
|
+
*
|
|
734
|
+
* @example
|
|
735
|
+
* // Use in validation configuration
|
|
736
|
+
* const headerKey = ClientRequest.convertHeaderKeyToCamelCase('X-Custom-Header');
|
|
737
|
+
* // Now use 'xCustomHeader' in validation rules
|
|
738
|
+
*/
|
|
739
|
+
static convertHeaderKeyToCamelCase(headerKey) {
|
|
740
|
+
if (!headerKey || typeof headerKey !== 'string') {
|
|
741
|
+
return '';
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// >! Convert to lowercase and replace -([a-z]) with uppercase letter
|
|
745
|
+
// >! Then remove any remaining hyphens (e.g., from consecutive hyphens or trailing hyphens)
|
|
746
|
+
// >! This prevents injection via special characters in header names
|
|
747
|
+
return headerKey.toLowerCase()
|
|
748
|
+
.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase())
|
|
749
|
+
.replace(/-/g, '');
|
|
750
|
+
}
|
|
151
751
|
|
|
152
752
|
/**
|
|
153
753
|
* Used in the constructor to set validity of the request
|
|
@@ -158,14 +758,36 @@ class ClientRequest extends RequestInfo {
|
|
|
158
758
|
let valid = false;
|
|
159
759
|
|
|
160
760
|
// add your additional validations here
|
|
161
|
-
valid = this.isAuthorizedReferrer() && this.#hasValidPathParameters() && this.#hasValidQueryStringParameters() && this.#hasValidHeaderParameters() && this.#hasValidCookieParameters();
|
|
761
|
+
valid = this.isAuthorizedReferrer() && this.#hasValidPathParameters() && this.#hasValidQueryStringParameters() && this.#hasValidHeaderParameters() && this.#hasValidCookieParameters() && this.#hasValidBodyParameters();
|
|
162
762
|
|
|
163
763
|
// set the variable
|
|
164
764
|
super._isValid = valid;
|
|
165
765
|
|
|
166
766
|
};
|
|
167
767
|
|
|
168
|
-
|
|
768
|
+
/**
|
|
769
|
+
* Validate parameters using ValidationMatcher and ValidationExecutor.
|
|
770
|
+
*
|
|
771
|
+
* This method implements the core parameter validation logic:
|
|
772
|
+
* 1. Uses ValidationMatcher to find the best matching validation rule (4-tier priority)
|
|
773
|
+
* 2. Uses ValidationExecutor to execute validation with appropriate interface (single or multi-parameter)
|
|
774
|
+
* 3. Extracts validated parameters and returns them
|
|
775
|
+
* 4. Respects excludeParamsWithNoValidationMatch flag (default: true)
|
|
776
|
+
*
|
|
777
|
+
* @private
|
|
778
|
+
* @param {Object} paramValidations - Parameter validation configuration (may include BY_ROUTE, BY_METHOD, and global validations)
|
|
779
|
+
* @param {Object} clientParameters - Parameters from the request (path, query, header, or cookie parameters)
|
|
780
|
+
* @param {ValidationMatcher} validationMatcher - ValidationMatcher instance for finding validation rules
|
|
781
|
+
* @returns {{isValid: boolean, params: Object}} Object with validation result and extracted parameters
|
|
782
|
+
* @example
|
|
783
|
+
* // Internal use - validates path parameters
|
|
784
|
+
* const { isValid, params } = #hasValidParameters(
|
|
785
|
+
* paramValidations.pathParameters,
|
|
786
|
+
* event.pathParameters,
|
|
787
|
+
* validationMatcher
|
|
788
|
+
* );
|
|
789
|
+
*/
|
|
790
|
+
#hasValidParameters(paramValidations, clientParameters, validationMatcher) {
|
|
169
791
|
|
|
170
792
|
let rValue = {
|
|
171
793
|
isValid: true,
|
|
@@ -173,62 +795,256 @@ class ClientRequest extends RequestInfo {
|
|
|
173
795
|
}
|
|
174
796
|
|
|
175
797
|
if (clientParameters && paramValidations) {
|
|
798
|
+
// >! Check excludeParamsWithNoValidationMatch flag (default: true)
|
|
799
|
+
const excludeUnmatched = ClientRequest.#validations.parameters?.excludeParamsWithNoValidationMatch !== false;
|
|
800
|
+
|
|
801
|
+
// >! Create normalized parameter map for validation execution
|
|
802
|
+
// >! Include ALL parameter types so multi-parameter validations can access them
|
|
803
|
+
const normalizedParams = {};
|
|
804
|
+
|
|
805
|
+
// >! Add path parameters (if available)
|
|
806
|
+
if (this.#event?.pathParameters) {
|
|
807
|
+
for (const [key, value] of Object.entries(this.#event.pathParameters)) {
|
|
808
|
+
const normalizedKey = key.replace(/^\/|\/$/g, '');
|
|
809
|
+
normalizedParams[normalizedKey] = value;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// >! Add query parameters (if available, lowercased)
|
|
814
|
+
if (this.#event?.queryStringParameters) {
|
|
815
|
+
for (const [key, value] of Object.entries(this.#event.queryStringParameters)) {
|
|
816
|
+
const normalizedKey = key.toLowerCase().replace(/^\/|\/$/g, '');
|
|
817
|
+
normalizedParams[normalizedKey] = value;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// >! Add header parameters (if available, camelCased)
|
|
822
|
+
if (this.#event?.headers) {
|
|
823
|
+
for (const [key, value] of Object.entries(this.#event.headers)) {
|
|
824
|
+
const camelCaseKey = key.toLowerCase().replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
825
|
+
normalizedParams[camelCaseKey] = value;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// >! Add cookie parameters (if available)
|
|
830
|
+
if (this.#event?.cookie) {
|
|
831
|
+
for (const [key, value] of Object.entries(this.#event.cookie)) {
|
|
832
|
+
const normalizedKey = key.replace(/^\/|\/$/g, '');
|
|
833
|
+
normalizedParams[normalizedKey] = value;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// >! Add client parameters being validated to normalizedParams
|
|
838
|
+
// >! This ensures validation functions can access the parameters they're validating
|
|
839
|
+
for (const [key, value] of Object.entries(clientParameters)) {
|
|
840
|
+
const normalizedKey = key.replace(/^\/|\/$/g, '');
|
|
841
|
+
normalizedParams[normalizedKey] = value;
|
|
842
|
+
}
|
|
843
|
+
|
|
176
844
|
// Use a for...of loop instead of forEach for better control flow
|
|
177
845
|
for (const [key, value] of Object.entries(clientParameters)) {
|
|
846
|
+
// >! Preserve existing parameter key normalization
|
|
178
847
|
const paramKey = key.replace(/^\/|\/$/g, '');
|
|
179
848
|
const paramValue = value;
|
|
180
849
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
850
|
+
// >! Use ValidationMatcher to find the best matching validation rule
|
|
851
|
+
const rule = validationMatcher.findValidationRule(paramKey);
|
|
852
|
+
|
|
853
|
+
if (rule) {
|
|
854
|
+
// >! Use ValidationExecutor to execute validation with appropriate interface
|
|
855
|
+
// Pass normalized parameters so validation functions can access them by normalized names
|
|
856
|
+
const isValid = ValidationExecutor.execute(rule.validate, rule.params, normalizedParams);
|
|
857
|
+
|
|
858
|
+
if (isValid) {
|
|
184
859
|
rValue.params[paramKey] = paramValue;
|
|
185
860
|
} else {
|
|
861
|
+
// >! Maintain existing logging for invalid parameters
|
|
186
862
|
DebugAndLog.warn(`Invalid parameter: ${paramKey} = ${paramValue}`);
|
|
187
863
|
rValue.isValid = false;
|
|
188
864
|
rValue.params = {};
|
|
189
|
-
|
|
865
|
+
// >! Ensure early exit on validation failure
|
|
866
|
+
return rValue;
|
|
190
867
|
}
|
|
868
|
+
} else if (!excludeUnmatched) {
|
|
869
|
+
// No validation rule found, but excludeUnmatched is false
|
|
870
|
+
// Include parameter without validation
|
|
871
|
+
rValue.params[paramKey] = paramValue;
|
|
191
872
|
}
|
|
873
|
+
// If excludeUnmatched is true and no rule found, skip parameter (existing behavior)
|
|
192
874
|
}
|
|
193
875
|
}
|
|
194
876
|
return rValue;
|
|
195
877
|
}
|
|
196
878
|
|
|
879
|
+
/**
|
|
880
|
+
* Validate path parameters from the request.
|
|
881
|
+
*
|
|
882
|
+
* Uses ValidationMatcher to find matching validation rules based on route pattern and HTTP method.
|
|
883
|
+
* Extracts validated path parameters and stores them in this.#props.pathParameters.
|
|
884
|
+
*
|
|
885
|
+
* @private
|
|
886
|
+
* @returns {boolean} True if all path parameters are valid, false otherwise
|
|
887
|
+
* @example
|
|
888
|
+
* // Internal use during request validation
|
|
889
|
+
* const isValid = #hasValidPathParameters();
|
|
890
|
+
*/
|
|
197
891
|
#hasValidPathParameters() {
|
|
198
|
-
const { isValid, params } = this.#hasValidParameters(
|
|
892
|
+
const { isValid, params } = this.#hasValidParameters(
|
|
893
|
+
ClientRequest.getParameterValidations()?.pathParameters,
|
|
894
|
+
this.#event?.pathParameters,
|
|
895
|
+
this.#validationMatchers.pathParameters
|
|
896
|
+
);
|
|
199
897
|
this.#props.pathParameters = params;
|
|
200
898
|
return isValid;
|
|
201
899
|
}
|
|
202
900
|
|
|
901
|
+
/**
|
|
902
|
+
* Validate query string parameters from the request.
|
|
903
|
+
*
|
|
904
|
+
* Normalizes query parameter keys to lowercase before validation.
|
|
905
|
+
* Uses ValidationMatcher to find matching validation rules based on route pattern and HTTP method.
|
|
906
|
+
* Extracts validated query parameters and stores them in this.#props.queryStringParameters.
|
|
907
|
+
* Supports both queryStringParameters and queryParameters for backwards compatibility.
|
|
908
|
+
*
|
|
909
|
+
* @private
|
|
910
|
+
* @returns {boolean} True if all query string parameters are valid, false otherwise
|
|
911
|
+
* @example
|
|
912
|
+
* // Internal use during request validation
|
|
913
|
+
* const isValid = #hasValidQueryStringParameters();
|
|
914
|
+
*/
|
|
203
915
|
#hasValidQueryStringParameters() {
|
|
204
916
|
// lowercase all the this.#event.queryStringParameters keys
|
|
205
917
|
const qs = {};
|
|
206
918
|
for (const key in this.#event.queryStringParameters) {
|
|
207
919
|
qs[key.toLowerCase()] = this.#event.queryStringParameters[key];
|
|
208
920
|
}
|
|
209
|
-
|
|
921
|
+
|
|
922
|
+
// >! Support both queryParameters and queryStringParameters for backwards compatibility
|
|
923
|
+
const paramValidations = ClientRequest.getParameterValidations();
|
|
924
|
+
const queryValidations = paramValidations?.queryStringParameters || paramValidations?.queryParameters;
|
|
925
|
+
|
|
926
|
+
const { isValid, params } = this.#hasValidParameters(
|
|
927
|
+
queryValidations,
|
|
928
|
+
qs,
|
|
929
|
+
this.#validationMatchers.queryStringParameters
|
|
930
|
+
);
|
|
210
931
|
this.#props.queryStringParameters = params;
|
|
211
932
|
return isValid;
|
|
212
933
|
}
|
|
213
934
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
935
|
+
/**
|
|
936
|
+
* Validate header parameters from the request.
|
|
937
|
+
*
|
|
938
|
+
* Normalizes header keys to camelCase (e.g., 'content-type' becomes 'contentType') before validation.
|
|
939
|
+
* Uses ValidationMatcher to find matching validation rules based on route pattern and HTTP method.
|
|
940
|
+
* Extracts validated header parameters and stores them in this.#props.headerParameters.
|
|
941
|
+
*
|
|
942
|
+
* @private
|
|
943
|
+
* @returns {boolean} True if all header parameters are valid, false otherwise
|
|
944
|
+
* @example
|
|
945
|
+
* // Internal use during request validation
|
|
946
|
+
* const isValid = #hasValidHeaderParameters();
|
|
947
|
+
*/
|
|
948
|
+
/**
|
|
949
|
+
* Validate header parameters from the request.
|
|
950
|
+
*
|
|
951
|
+
* HTTP headers are automatically converted from kebab-case to camelCase for JavaScript
|
|
952
|
+
* property naming conventions. The conversion algorithm:
|
|
953
|
+
* 1. Convert entire header key to lowercase
|
|
954
|
+
* 2. Replace each hyphen followed by a letter with the uppercase letter
|
|
955
|
+
* 3. Remove all hyphens
|
|
956
|
+
*
|
|
957
|
+
* This allows validation rules to use JavaScript-friendly property names while
|
|
958
|
+
* maintaining compatibility with standard HTTP header naming conventions.
|
|
959
|
+
*
|
|
960
|
+
* @private
|
|
961
|
+
* @returns {boolean} True if all header parameters are valid, false otherwise
|
|
962
|
+
* @example
|
|
963
|
+
* // HTTP header conversion examples:
|
|
964
|
+
* // 'Content-Type' → 'contentType'
|
|
965
|
+
* // 'content-type' → 'contentType'
|
|
966
|
+
* // 'X-API-Key' → 'xApiKey'
|
|
967
|
+
* // 'x-custom-header' → 'xCustomHeader'
|
|
968
|
+
* // 'authorization' → 'authorization' (no hyphens, unchanged)
|
|
969
|
+
*
|
|
970
|
+
* // Internal use during request validation
|
|
971
|
+
* const isValid = this.#hasValidHeaderParameters();
|
|
972
|
+
*/
|
|
973
|
+
#hasValidHeaderParameters() {
|
|
974
|
+
// camel case all the this.#event.headers keys and remove hyphens
|
|
975
|
+
const headers = {};
|
|
976
|
+
for (const key in this.#event.headers) {
|
|
977
|
+
const camelCaseKey = key.toLowerCase().replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
978
|
+
headers[camelCaseKey] = this.#event.headers[key];
|
|
979
|
+
}
|
|
980
|
+
const { isValid, params } = this.#hasValidParameters(
|
|
981
|
+
ClientRequest.getParameterValidations()?.headerParameters,
|
|
982
|
+
headers,
|
|
983
|
+
this.#validationMatchers.headerParameters
|
|
984
|
+
);
|
|
985
|
+
this.#props.headerParameters = params;
|
|
986
|
+
return isValid;
|
|
220
987
|
}
|
|
221
|
-
const { isValid, params } = this.#hasValidParameters(ClientRequest.getParameterValidations()?.headerParameters, headers);
|
|
222
|
-
this.#props.headerParameters = params;
|
|
223
|
-
return isValid;
|
|
224
|
-
}
|
|
225
988
|
|
|
226
989
|
#hasValidCookieParameters() {
|
|
227
|
-
const { isValid, params } = this.#hasValidParameters(
|
|
990
|
+
const { isValid, params } = this.#hasValidParameters(
|
|
991
|
+
ClientRequest.getParameterValidations()?.cookieParameters,
|
|
992
|
+
this.#event?.cookie,
|
|
993
|
+
this.#validationMatchers.cookieParameters
|
|
994
|
+
);
|
|
228
995
|
this.#props.cookieParameters = params;
|
|
229
996
|
return isValid;
|
|
230
997
|
}
|
|
231
998
|
|
|
999
|
+
/**
|
|
1000
|
+
* Validate body parameters from the request.
|
|
1001
|
+
*
|
|
1002
|
+
* Parses JSON body content before validation. Handles both API Gateway v1 and v2 formats.
|
|
1003
|
+
* Uses ValidationMatcher to find matching validation rules based on route pattern and HTTP method.
|
|
1004
|
+
* Extracts validated body parameters and stores them in this.#props.bodyParameters.
|
|
1005
|
+
*
|
|
1006
|
+
* Null, undefined, or empty string bodies are treated as empty objects for validation.
|
|
1007
|
+
* If the body contains invalid JSON, the error is logged and validation fails.
|
|
1008
|
+
*
|
|
1009
|
+
* @private
|
|
1010
|
+
* @returns {boolean} True if all body parameters are valid, false otherwise
|
|
1011
|
+
* @example
|
|
1012
|
+
* // Internal use during request validation
|
|
1013
|
+
* const isValid = #hasValidBodyParameters();
|
|
1014
|
+
*/
|
|
1015
|
+
#hasValidBodyParameters() {
|
|
1016
|
+
// >! Parse body content before validation
|
|
1017
|
+
let bodyObject = {};
|
|
1018
|
+
|
|
1019
|
+
// >! Handle null, undefined, and empty string body cases
|
|
1020
|
+
if (this.#event.body && this.#event.body !== '') {
|
|
1021
|
+
try {
|
|
1022
|
+
// >! Parse JSON with error handling
|
|
1023
|
+
bodyObject = JSON.parse(this.#event.body);
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
// >! Log JSON parsing errors
|
|
1026
|
+
DebugAndLog.error(
|
|
1027
|
+
`Failed to parse request body as JSON: ${error?.message || 'Unknown error'}`,
|
|
1028
|
+
error?.stack
|
|
1029
|
+
);
|
|
1030
|
+
this.#props.bodyParameters = {};
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// >! Use existing validation framework with body validation matcher
|
|
1036
|
+
const { isValid, params } = this.#hasValidParameters(
|
|
1037
|
+
ClientRequest.getParameterValidations()?.bodyParameters,
|
|
1038
|
+
bodyObject,
|
|
1039
|
+
this.#validationMatchers.bodyParameters
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
// >! Store validated parameters
|
|
1043
|
+
this.#props.bodyParameters = params;
|
|
1044
|
+
return isValid;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
|
|
232
1048
|
|
|
233
1049
|
// Utility function for getPathArray and getResourceArray
|
|
234
1050
|
// Returns array slice based on n parameter
|
|
@@ -356,6 +1172,15 @@ class ClientRequest extends RequestInfo {
|
|
|
356
1172
|
return this.#props.cookieParameters;
|
|
357
1173
|
};
|
|
358
1174
|
|
|
1175
|
+
/**
|
|
1176
|
+
* Returns the body parameters received in the request.
|
|
1177
|
+
* Body parameters are validated in the applications validation functions.
|
|
1178
|
+
* @returns {object} body parameters
|
|
1179
|
+
*/
|
|
1180
|
+
getBodyParameters() {
|
|
1181
|
+
return this.#props.bodyParameters || {};
|
|
1182
|
+
};
|
|
1183
|
+
|
|
359
1184
|
#authenticate() {
|
|
360
1185
|
// add your authentication logic here
|
|
361
1186
|
this.authenticated = false; // anonymous
|
|
@@ -392,9 +1217,11 @@ class ClientRequest extends RequestInfo {
|
|
|
392
1217
|
isAuthorizedReferrer() {
|
|
393
1218
|
/* Check the array of valid referrers */
|
|
394
1219
|
/* Check if the array includes a wildcard (*) OR if one of the whitelisted referrers matches the end of the referrer */
|
|
395
|
-
if (ClientRequest.requiresValidReferrer()) {
|
|
1220
|
+
if (!ClientRequest.requiresValidReferrer()) {
|
|
1221
|
+
// Wildcard (*) is in the list, allow all referrers
|
|
396
1222
|
return true;
|
|
397
1223
|
} else {
|
|
1224
|
+
// Check if referrer matches one of the whitelisted referrers
|
|
398
1225
|
for (let i = 0; i < ClientRequest.#validations.referrers.length; i++) {
|
|
399
1226
|
if (this.getClientReferer().endsWith(ClientRequest.#validations.referrers[i])) {
|
|
400
1227
|
return true;
|