@63klabs/cache-data 1.3.8 → 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 +96 -1
- package/CHANGELOG.md +63 -0
- package/CONTRIBUTING.md +2 -2
- package/README.md +17 -31
- package/package.json +5 -4
- package/src/lib/dao-cache.js +30 -13
- 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 +826 -32
- package/src/lib/tools/index.js +96 -34
- 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 = {};
|
|
@@ -74,9 +414,36 @@ class ClientRequest extends RequestInfo {
|
|
|
74
414
|
}
|
|
75
415
|
|
|
76
416
|
/**
|
|
77
|
-
* Initializes the request data based on the event
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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);
|
|
80
447
|
*/
|
|
81
448
|
constructor(event, context) {
|
|
82
449
|
super(event);
|
|
@@ -100,6 +467,7 @@ class ClientRequest extends RequestInfo {
|
|
|
100
467
|
queryStringParameters: {},
|
|
101
468
|
headerParameters: {},
|
|
102
469
|
cookieParameters: {},
|
|
470
|
+
bodyParameters: {},
|
|
103
471
|
bodyPayload: this.#event?.body || null, // from body
|
|
104
472
|
client: {
|
|
105
473
|
isAuthenticated: this.isAuthenticated(),
|
|
@@ -111,18 +479,176 @@ class ClientRequest extends RequestInfo {
|
|
|
111
479
|
calcMsToDeadline: this.calcMsToDeadline
|
|
112
480
|
};
|
|
113
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
|
+
|
|
114
499
|
this.#validate();
|
|
115
500
|
|
|
116
501
|
};
|
|
117
502
|
|
|
118
503
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
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
|
|
125
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
|
+
* });
|
|
126
652
|
*/
|
|
127
653
|
static init(options) {
|
|
128
654
|
if (typeof options === 'object') {
|
|
@@ -181,6 +707,47 @@ class ClientRequest extends RequestInfo {
|
|
|
181
707
|
static getParameterValidations() {
|
|
182
708
|
return ClientRequest.#validations.parameters;
|
|
183
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
|
+
}
|
|
184
751
|
|
|
185
752
|
/**
|
|
186
753
|
* Used in the constructor to set validity of the request
|
|
@@ -191,14 +758,36 @@ class ClientRequest extends RequestInfo {
|
|
|
191
758
|
let valid = false;
|
|
192
759
|
|
|
193
760
|
// add your additional validations here
|
|
194
|
-
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();
|
|
195
762
|
|
|
196
763
|
// set the variable
|
|
197
764
|
super._isValid = valid;
|
|
198
765
|
|
|
199
766
|
};
|
|
200
767
|
|
|
201
|
-
|
|
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) {
|
|
202
791
|
|
|
203
792
|
let rValue = {
|
|
204
793
|
isValid: true,
|
|
@@ -206,62 +795,256 @@ class ClientRequest extends RequestInfo {
|
|
|
206
795
|
}
|
|
207
796
|
|
|
208
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
|
+
|
|
209
844
|
// Use a for...of loop instead of forEach for better control flow
|
|
210
845
|
for (const [key, value] of Object.entries(clientParameters)) {
|
|
846
|
+
// >! Preserve existing parameter key normalization
|
|
211
847
|
const paramKey = key.replace(/^\/|\/$/g, '');
|
|
212
848
|
const paramValue = value;
|
|
213
849
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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) {
|
|
217
859
|
rValue.params[paramKey] = paramValue;
|
|
218
860
|
} else {
|
|
861
|
+
// >! Maintain existing logging for invalid parameters
|
|
219
862
|
DebugAndLog.warn(`Invalid parameter: ${paramKey} = ${paramValue}`);
|
|
220
863
|
rValue.isValid = false;
|
|
221
864
|
rValue.params = {};
|
|
222
|
-
|
|
865
|
+
// >! Ensure early exit on validation failure
|
|
866
|
+
return rValue;
|
|
223
867
|
}
|
|
868
|
+
} else if (!excludeUnmatched) {
|
|
869
|
+
// No validation rule found, but excludeUnmatched is false
|
|
870
|
+
// Include parameter without validation
|
|
871
|
+
rValue.params[paramKey] = paramValue;
|
|
224
872
|
}
|
|
873
|
+
// If excludeUnmatched is true and no rule found, skip parameter (existing behavior)
|
|
225
874
|
}
|
|
226
875
|
}
|
|
227
876
|
return rValue;
|
|
228
877
|
}
|
|
229
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
|
+
*/
|
|
230
891
|
#hasValidPathParameters() {
|
|
231
|
-
const { isValid, params } = this.#hasValidParameters(
|
|
892
|
+
const { isValid, params } = this.#hasValidParameters(
|
|
893
|
+
ClientRequest.getParameterValidations()?.pathParameters,
|
|
894
|
+
this.#event?.pathParameters,
|
|
895
|
+
this.#validationMatchers.pathParameters
|
|
896
|
+
);
|
|
232
897
|
this.#props.pathParameters = params;
|
|
233
898
|
return isValid;
|
|
234
899
|
}
|
|
235
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
|
+
*/
|
|
236
915
|
#hasValidQueryStringParameters() {
|
|
237
916
|
// lowercase all the this.#event.queryStringParameters keys
|
|
238
917
|
const qs = {};
|
|
239
918
|
for (const key in this.#event.queryStringParameters) {
|
|
240
919
|
qs[key.toLowerCase()] = this.#event.queryStringParameters[key];
|
|
241
920
|
}
|
|
242
|
-
|
|
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
|
+
);
|
|
243
931
|
this.#props.queryStringParameters = params;
|
|
244
932
|
return isValid;
|
|
245
933
|
}
|
|
246
934
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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;
|
|
253
987
|
}
|
|
254
|
-
const { isValid, params } = this.#hasValidParameters(ClientRequest.getParameterValidations()?.headerParameters, headers);
|
|
255
|
-
this.#props.headerParameters = params;
|
|
256
|
-
return isValid;
|
|
257
|
-
}
|
|
258
988
|
|
|
259
989
|
#hasValidCookieParameters() {
|
|
260
|
-
const { isValid, params } = this.#hasValidParameters(
|
|
990
|
+
const { isValid, params } = this.#hasValidParameters(
|
|
991
|
+
ClientRequest.getParameterValidations()?.cookieParameters,
|
|
992
|
+
this.#event?.cookie,
|
|
993
|
+
this.#validationMatchers.cookieParameters
|
|
994
|
+
);
|
|
261
995
|
this.#props.cookieParameters = params;
|
|
262
996
|
return isValid;
|
|
263
997
|
}
|
|
264
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
|
+
|
|
265
1048
|
|
|
266
1049
|
// Utility function for getPathArray and getResourceArray
|
|
267
1050
|
// Returns array slice based on n parameter
|
|
@@ -389,6 +1172,15 @@ class ClientRequest extends RequestInfo {
|
|
|
389
1172
|
return this.#props.cookieParameters;
|
|
390
1173
|
};
|
|
391
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
|
+
|
|
392
1184
|
#authenticate() {
|
|
393
1185
|
// add your authentication logic here
|
|
394
1186
|
this.authenticated = false; // anonymous
|
|
@@ -425,9 +1217,11 @@ class ClientRequest extends RequestInfo {
|
|
|
425
1217
|
isAuthorizedReferrer() {
|
|
426
1218
|
/* Check the array of valid referrers */
|
|
427
1219
|
/* Check if the array includes a wildcard (*) OR if one of the whitelisted referrers matches the end of the referrer */
|
|
428
|
-
if (ClientRequest.requiresValidReferrer()) {
|
|
1220
|
+
if (!ClientRequest.requiresValidReferrer()) {
|
|
1221
|
+
// Wildcard (*) is in the list, allow all referrers
|
|
429
1222
|
return true;
|
|
430
1223
|
} else {
|
|
1224
|
+
// Check if referrer matches one of the whitelisted referrers
|
|
431
1225
|
for (let i = 0; i < ClientRequest.#validations.referrers.length; i++) {
|
|
432
1226
|
if (this.getClientReferer().endsWith(ClientRequest.#validations.referrers[i])) {
|
|
433
1227
|
return true;
|