@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.
@@ -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
- * Can be used to create a custom ClientRequest object
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. Also sets the
79
- * validity of the request so it may be checked by .isValid()
80
- * @param {object} event object from Lambda
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
- * This is used to initialize the ClientRequest class for all requests.
121
- * Add ClientRequest.init(options) to the Config.init process or at the
122
- * top of the main index.js file outside of the handler.
123
- * @param {object} options - Configuration options with validations property containing referrers and parameters
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
- #hasValidParameters(paramValidations, clientParameters) {
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
- if (paramKey in paramValidations) {
182
- const validationFunc = paramValidations[paramKey];
183
- if (typeof validationFunc === 'function' && validationFunc(paramValue)) {
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
- return rValue; // exit right away
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(ClientRequest.getParameterValidations()?.pathParameters, this.#event?.pathParameters);
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
- const { isValid, params } = this.#hasValidParameters(ClientRequest.getParameterValidations()?.queryStringParameters, qs);
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
- #hasValidHeaderParameters() {
215
- // camel case all the this.#event.headers keys and remove hyphens
216
- const headers = {};
217
- for (const key in this.#event.headers) {
218
- const camelCaseKey = key.toLowerCase().replace(/-([a-z])/g, (g) => g[1].toUpperCase());
219
- headers[camelCaseKey] = this.#event.headers[key];
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(ClientRequest.getParameterValidations()?.cookiearameters, this.#event?.cookie); // TODO
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;