@63klabs/cache-data 1.3.8 → 1.3.10

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,10 @@ class ClientRequest extends RequestInfo {
61
398
  #context = null;
62
399
  #authorizations = safeClone(ClientRequest.#unauthenticatedAuthorizations);
63
400
  #roles = [];
401
+
402
+ /* Validation system */
403
+ #validationMatchers = {};
404
+ #validationReason = { isValid: true, statusCode: 200, messages: [] };
64
405
 
65
406
  /* The request data */
66
407
  #props = {};
@@ -74,9 +415,36 @@ class ClientRequest extends RequestInfo {
74
415
  }
75
416
 
76
417
  /**
77
- * Initializes the request data based on the event. Also sets the
78
- * validity of the request so it may be checked by .isValid()
79
- * @param {object} event object from Lambda
418
+ * Initializes the request data based on the event and performs parameter validation.
419
+ *
420
+ * The constructor initializes ValidationMatchers for each parameter type (path, query, header, cookie)
421
+ * using the configured validation rules. Validation is performed immediately during construction,
422
+ * and the request validity can be checked using isValid().
423
+ *
424
+ * Validation uses a four-tier priority system:
425
+ * 1. Method-and-route match (BY_ROUTE with "METHOD:route") - Most specific
426
+ * 2. Route-only match (BY_ROUTE with "route")
427
+ * 3. Method-only match (BY_METHOD with "METHOD")
428
+ * 4. Global parameter name - Least specific
429
+ *
430
+ * @param {Object} event - Lambda API Gateway event object
431
+ * @param {Object} context - Lambda context object
432
+ * @example
433
+ * // Basic usage
434
+ * const clientRequest = new ClientRequest(event, context);
435
+ * if (clientRequest.isValid()) {
436
+ * // Process valid request
437
+ * }
438
+ *
439
+ * @example
440
+ * // With route-specific validation
441
+ * // If event.resource = "/product/{id}" and event.httpMethod = "GET"
442
+ * // ValidationMatcher will check:
443
+ * // 1. GET:product/{id} validation (if exists)
444
+ * // 2. product/{id} validation (if exists)
445
+ * // 3. GET method validation (if exists)
446
+ * // 4. Global 'id' parameter validation (if exists)
447
+ * const clientRequest = new ClientRequest(event, context);
80
448
  */
81
449
  constructor(event, context) {
82
450
  super(event);
@@ -100,8 +468,13 @@ class ClientRequest extends RequestInfo {
100
468
  queryStringParameters: {},
101
469
  headerParameters: {},
102
470
  cookieParameters: {},
471
+ bodyParameters: {},
103
472
  bodyPayload: this.#event?.body || null, // from body
104
473
  client: {
474
+ ip: this.getClientIp(),
475
+ userAgent: this.getClientUserAgent(),
476
+ origin: this.getClientOrigin(),
477
+ referrer: this.getClientReferrer(),
105
478
  isAuthenticated: this.isAuthenticated(),
106
479
  isGuest: this.isGuest(),
107
480
  authorizations: this.getAuthorizations(),
@@ -111,18 +484,176 @@ class ClientRequest extends RequestInfo {
111
484
  calcMsToDeadline: this.calcMsToDeadline
112
485
  };
113
486
 
487
+ // >! Initialize ValidationMatchers for each parameter type
488
+ // >! This enables route-specific and method-specific validations
489
+ const httpMethod = this.#event.httpMethod || '';
490
+ const resourcePath = resource || '';
491
+ const paramValidations = ClientRequest.getParameterValidations();
492
+
493
+ // >! Support both queryParameters and queryStringParameters for backwards compatibility
494
+ const queryValidations = paramValidations?.queryStringParameters || paramValidations?.queryParameters;
495
+
496
+ this.#validationMatchers = {
497
+ pathParameters: new ValidationMatcher(paramValidations?.pathParameters, httpMethod, resourcePath),
498
+ queryStringParameters: new ValidationMatcher(queryValidations, httpMethod, resourcePath),
499
+ headerParameters: new ValidationMatcher(paramValidations?.headerParameters, httpMethod, resourcePath),
500
+ cookieParameters: new ValidationMatcher(paramValidations?.cookieParameters, httpMethod, resourcePath),
501
+ bodyParameters: new ValidationMatcher(paramValidations?.bodyParameters, httpMethod, resourcePath)
502
+ };
503
+
114
504
  this.#validate();
115
505
 
116
506
  };
117
507
 
118
508
  /**
119
- * This is used to initialize the ClientRequest class for all requests.
120
- * Add ClientRequest.init(options) to the Config.init process or at the
121
- * top of the main index.js file outside of the handler.
122
- * @param {object} options - Configuration options with validations property containing referrers and parameters
123
- * @param {Array<string>} options.referrers - Array of allowed referrers
124
- * @param {object} options.parameters - Object containing parameter validation functions
509
+ * Initialize the ClientRequest class with validation configuration.
510
+ *
511
+ * This method configures the validation system for all ClientRequest instances.
512
+ * Call this once during application initialization, before creating any ClientRequest instances.
513
+ *
514
+ * Validation Configuration Structure:
515
+ * - referrers: Array of allowed referrer domains (use ['*'] to allow all)
516
+ * - parameters: Object containing validation rules for each parameter type
517
+ * - pathParameters, queryStringParameters, headerParameters, cookieParameters, bodyParameters
518
+ * - Each parameter type can have:
519
+ * - Global validations: paramName: (value) => boolean
520
+ * - BY_ROUTE: Array of route-specific validation rules
521
+ * - BY_METHOD: Array of method-specific validation rules
522
+ *
523
+ * Parameter Specification Syntax:
524
+ * - Path parameters: "route/{param}" - Validates 'param' path parameter
525
+ * - Query parameters: "route?param" - Validates 'param' query parameter
526
+ * - Multiple parameters: "route?param1,param2" - Validates both parameters together
527
+ * - Method-and-route: "METHOD:route" - Applies only to specific HTTP method and route
528
+ *
529
+ * @param {Object} options - Configuration options
530
+ * @param {Array<string>} [options.referrers] - Array of allowed referrers (use ['*'] for all)
531
+ * @param {Object} [options.parameters] - Parameter validation configuration
532
+ * @param {Object} [options.parameters.pathParameters] - Path parameter validations
533
+ * @param {Object} [options.parameters.queryStringParameters] - Query parameter validations
534
+ * @param {Object} [options.parameters.headerParameters] - Header parameter validations
535
+ * @param {Object} [options.parameters.cookieParameters] - Cookie parameter validations
536
+ * @param {Object} [options.parameters.bodyParameters] - Body parameter validations
537
+ * @param {Array<{route: string, validate: Function}>} [options.parameters.*.BY_ROUTE] - Route-specific validations
538
+ * @param {Array<{method: string, validate: Function}>} [options.parameters.*.BY_METHOD] - Method-specific validations
539
+ * @param {boolean} [options.parameters.excludeParamsWithNoValidationMatch=true] - Exclude parameters without validation rules
125
540
  * @throws {Error} If options is not an object
541
+ *
542
+ * @example
543
+ * // Global validations only (backwards compatible)
544
+ * ClientRequest.init({
545
+ * referrers: ['example.com'],
546
+ * parameters: {
547
+ * pathParameters: {
548
+ * id: (value) => /^[a-zA-Z0-9-]+$/.test(value)
549
+ * },
550
+ * queryStringParameters: {
551
+ * limit: (value) => !isNaN(value) && value > 0 && value <= 100
552
+ * }
553
+ * }
554
+ * });
555
+ *
556
+ * @example
557
+ * // Route-specific validations (Priority 2)
558
+ * ClientRequest.init({
559
+ * parameters: {
560
+ * pathParameters: {
561
+ * id: (value) => typeof value === 'string', // Global fallback
562
+ * BY_ROUTE: [
563
+ * {
564
+ * route: "product/{id}",
565
+ * validate: (value) => /^P-[0-9]+$/.test(value)
566
+ * },
567
+ * {
568
+ * route: "employee/{id}",
569
+ * validate: (value) => /^E-[0-9]+$/.test(value)
570
+ * }
571
+ * ]
572
+ * }
573
+ * }
574
+ * });
575
+ *
576
+ * @example
577
+ * // Method-specific validations (Priority 3)
578
+ * ClientRequest.init({
579
+ * parameters: {
580
+ * pathParameters: {
581
+ * BY_METHOD: [
582
+ * {
583
+ * method: "POST",
584
+ * validate: (value) => value.length <= 50
585
+ * },
586
+ * {
587
+ * method: "GET",
588
+ * validate: (value) => value.length > 0
589
+ * }
590
+ * ]
591
+ * }
592
+ * }
593
+ * });
594
+ *
595
+ * @example
596
+ * // Method-and-route validations (Priority 1 - highest)
597
+ * ClientRequest.init({
598
+ * parameters: {
599
+ * pathParameters: {
600
+ * BY_ROUTE: [
601
+ * {
602
+ * route: "POST:game/join/{id}",
603
+ * validate: (value) => /^[0-9]{6}$/.test(value)
604
+ * },
605
+ * {
606
+ * route: "GET:game/join/{id}",
607
+ * validate: (value) => /^[0-9]+$/.test(value)
608
+ * }
609
+ * ]
610
+ * }
611
+ * }
612
+ * });
613
+ *
614
+ * @example
615
+ * // Multi-parameter validation
616
+ * ClientRequest.init({
617
+ * parameters: {
618
+ * queryStringParameters: {
619
+ * BY_ROUTE: [
620
+ * {
621
+ * route: "search?query,limit", // Specify multiple parameters
622
+ * validate: ({query, limit}) => {
623
+ * // Validation function receives object with all parameters
624
+ * return query.length > 0 && limit >= 1 && limit <= 100;
625
+ * }
626
+ * }
627
+ * ]
628
+ * }
629
+ * }
630
+ * });
631
+ *
632
+ * @example
633
+ * // Mixed priority levels
634
+ * ClientRequest.init({
635
+ * parameters: {
636
+ * pathParameters: {
637
+ * id: (value) => typeof value === 'string', // Priority 4: Global
638
+ * BY_METHOD: [
639
+ * {
640
+ * method: "POST", // Priority 3: Method-only
641
+ * validate: (value) => value.length <= 50
642
+ * }
643
+ * ],
644
+ * BY_ROUTE: [
645
+ * {
646
+ * route: "product/{id}", // Priority 2: Route-only
647
+ * validate: (value) => /^P-[0-9]+$/.test(value)
648
+ * },
649
+ * {
650
+ * route: "POST:product/{id}", // Priority 1: Method-and-route
651
+ * validate: (value) => /^P-[0-9]{4}$/.test(value)
652
+ * }
653
+ * ]
654
+ * }
655
+ * }
656
+ * });
126
657
  */
127
658
  static init(options) {
128
659
  if (typeof options === 'object') {
@@ -172,7 +703,7 @@ class ClientRequest extends RequestInfo {
172
703
  * Parameter validations
173
704
  * @returns {{
174
705
  * pathParameters?: object,
175
- * queryParameters?: object,
706
+ * queryStringParameters?: object,
176
707
  * headerParameters?: object,
177
708
  * cookieParameters?: object,
178
709
  * bodyParameters?: object
@@ -181,6 +712,47 @@ class ClientRequest extends RequestInfo {
181
712
  static getParameterValidations() {
182
713
  return ClientRequest.#validations.parameters;
183
714
  };
715
+ /**
716
+ * Convert HTTP header key from kebab-case to camelCase.
717
+ *
718
+ * This utility method helps developers determine the correct key names for header validation rules.
719
+ * HTTP headers use kebab-case (e.g., 'content-type'), but ClientRequest converts them to camelCase
720
+ * (e.g., 'contentType') during validation for JavaScript property naming conventions.
721
+ *
722
+ * The conversion algorithm:
723
+ * 1. Convert entire string to lowercase
724
+ * 2. Replace each hyphen followed by a letter with the uppercase letter
725
+ * 3. Remove all hyphens
726
+ *
727
+ * @param {string} headerKey - HTTP header key in kebab-case (e.g., 'Content-Type', 'x-custom-header')
728
+ * @returns {string} Header key in camelCase (e.g., 'contentType', 'xCustomHeader')
729
+ * @example
730
+ * // Common HTTP headers
731
+ * ClientRequest.convertHeaderKeyToCamelCase('content-type'); // 'contentType'
732
+ * ClientRequest.convertHeaderKeyToCamelCase('Content-Type'); // 'contentType'
733
+ * ClientRequest.convertHeaderKeyToCamelCase('x-api-key'); // 'xApiKey'
734
+ *
735
+ * @example
736
+ * // Multiple hyphens
737
+ * ClientRequest.convertHeaderKeyToCamelCase('x-custom-header-name'); // 'xCustomHeaderName'
738
+ *
739
+ * @example
740
+ * // Use in validation configuration
741
+ * const headerKey = ClientRequest.convertHeaderKeyToCamelCase('X-Custom-Header');
742
+ * // Now use 'xCustomHeader' in validation rules
743
+ */
744
+ static convertHeaderKeyToCamelCase(headerKey) {
745
+ if (!headerKey || typeof headerKey !== 'string') {
746
+ return '';
747
+ }
748
+
749
+ // >! Convert to lowercase and replace -([a-z]) with uppercase letter
750
+ // >! Then remove any remaining hyphens (e.g., from consecutive hyphens or trailing hyphens)
751
+ // >! This prevents injection via special characters in header names
752
+ return headerKey.toLowerCase()
753
+ .replace(/-([a-z])/g, (match, letter) => letter.toUpperCase())
754
+ .replace(/-/g, '');
755
+ }
184
756
 
185
757
  /**
186
758
  * Used in the constructor to set validity of the request
@@ -188,80 +760,440 @@ class ClientRequest extends RequestInfo {
188
760
  */
189
761
  #validate() {
190
762
 
191
- let valid = false;
763
+ const reasons = [];
764
+ let statusCode = 200;
765
+
766
+ // Referrer check
767
+ const referrerValid = this.isAuthorizedReferrer();
768
+ if (!referrerValid) {
769
+ reasons.push("Forbidden");
770
+ statusCode = this.#upgradeStatusCode(statusCode, 403);
771
+ }
192
772
 
193
- // add your additional validations here
194
- valid = this.isAuthorizedReferrer() && this.#hasValidPathParameters() && this.#hasValidQueryStringParameters() && this.#hasValidHeaderParameters() && this.#hasValidCookieParameters();
773
+ // Authentication check
774
+ const authFailed = this.hasNoAuthorization();
775
+ if (authFailed) {
776
+ reasons.push("Unauthorized");
777
+ statusCode = this.#upgradeStatusCode(statusCode, 401);
778
+ }
779
+
780
+ // Parameter checks - collect invalid parameter names from each
781
+ const pathResult = this.#hasValidPathParameters();
782
+ const queryResult = this.#hasValidQueryStringParameters();
783
+ const headerResult = this.#hasValidHeaderParameters();
784
+ const cookieResult = this.#hasValidCookieParameters();
785
+ const bodyResult = this.#hasValidBodyParameters();
786
+
787
+ // Collect invalid parameter messages
788
+ const paramResults = [pathResult, queryResult, headerResult, cookieResult, bodyResult];
789
+ for (const result of paramResults) {
790
+ if (result.invalidParams) {
791
+ for (const paramName of result.invalidParams) {
792
+ reasons.push(`Invalid parameter: ${paramName}`);
793
+ statusCode = this.#upgradeStatusCode(statusCode, 400);
794
+ }
795
+ }
796
+ }
797
+
798
+ // Handle invalid JSON body
799
+ if (bodyResult.invalidBody) {
800
+ reasons.push("Invalid request body");
801
+ statusCode = this.#upgradeStatusCode(statusCode, 400);
802
+ }
195
803
 
196
- // set the variable
804
+ // Compute combined valid boolean from all check results
805
+ const valid = referrerValid && !authFailed
806
+ && pathResult.isValid && queryResult.isValid && headerResult.isValid
807
+ && cookieResult.isValid && bodyResult.isValid;
808
+
809
+ // Preserve backwards compatibility
197
810
  super._isValid = valid;
198
811
 
812
+ // Populate validation reason
813
+ this.#validationReason = {
814
+ isValid: valid,
815
+ statusCode: valid ? 200 : statusCode,
816
+ messages: valid ? [] : reasons
817
+ };
818
+
199
819
  };
200
820
 
201
- #hasValidParameters(paramValidations, clientParameters) {
821
+ /**
822
+ * Returns the higher-priority HTTP status code between two candidates.
823
+ * Priority order: 401 > 403 > 400 > 200.
824
+ *
825
+ * @private
826
+ * @param {number} current - The current status code
827
+ * @param {number} candidate - The candidate status code to compare
828
+ * @returns {number} The status code with higher priority
829
+ */
830
+ #upgradeStatusCode(current, candidate) {
831
+ const priority = { 401: 3, 403: 2, 400: 1, 200: 0 };
832
+ return (priority[candidate] || 0) > (priority[current] || 0) ? candidate : current;
833
+ };
834
+
835
+ /**
836
+ * Returns a structured validation result object describing why the request
837
+ * passed or failed validation. The object includes the validation status,
838
+ * an appropriate HTTP status code, and descriptive messages identifying
839
+ * each failure.
840
+ *
841
+ * A new object is returned on each call to prevent external mutation of
842
+ * internal state.
843
+ *
844
+ * @returns {{ isValid: boolean, statusCode: number, messages: Array<string> }}
845
+ * A new object on each call containing:
846
+ * - isValid: whether the request passed all validation checks
847
+ * - statusCode: the appropriate HTTP status code (200, 400, 401, or 403)
848
+ * - messages: array of descriptive failure messages (empty when valid)
849
+ * @example
850
+ * // Valid request
851
+ * const reason = clientRequest.getValidationReason();
852
+ * // { isValid: true, statusCode: 200, messages: [] }
853
+ *
854
+ * @example
855
+ * // Invalid request with bad parameters
856
+ * const reason = clientRequest.getValidationReason();
857
+ * // { isValid: false, statusCode: 400, messages: ["Invalid parameter: limit"] }
858
+ */
859
+ getValidationReason() {
860
+ return {
861
+ isValid: this.#validationReason.isValid,
862
+ statusCode: this.#validationReason.statusCode,
863
+ messages: [...this.#validationReason.messages]
864
+ };
865
+ }
866
+
867
+ /**
868
+ * Validate parameters using ValidationMatcher and ValidationExecutor.
869
+ *
870
+ * This method implements the core parameter validation logic:
871
+ * 1. Uses ValidationMatcher to find the best matching validation rule (4-tier priority)
872
+ * 2. Uses ValidationExecutor to execute validation with appropriate interface (single or multi-parameter)
873
+ * 3. Extracts ALL validated parameters specified in the matching rule and returns them
874
+ * 4. Respects excludeParamsWithNoValidationMatch flag (default: true)
875
+ *
876
+ * When a validation rule matches and validation passes, ALL parameters specified in rule.params
877
+ * are extracted from clientParameters and included in the returned params object. This ensures
878
+ * that multi-parameter validation rules (e.g., validating query?param1,param2 together) correctly
879
+ * extract all validated parameters, not just the one that triggered the rule match.
880
+ *
881
+ * For single-parameter validation with multi-placeholder routes (e.g., users/{userId}/posts/{id}):
882
+ * - ValidationMatcher returns validateParam field indicating which parameter to validate
883
+ * - ValidationExecutor validates only that parameter with single-parameter interface
884
+ * - This method extracts ALL parameters from rule.params array (e.g., both userId and id)
885
+ *
886
+ * @private
887
+ * @param {Object} paramValidations - Parameter validation configuration (may include BY_ROUTE, BY_METHOD, and global validations)
888
+ * @param {Object} clientParameters - Parameters from the request (path, query, header, or cookie parameters)
889
+ * @param {ValidationMatcher} validationMatcher - ValidationMatcher instance for finding validation rules
890
+ * @returns {{isValid: boolean, params: Object}} Object with validation result and extracted parameters
891
+ * @example
892
+ * // Internal use - validates path parameters
893
+ * const { isValid, params } = #hasValidParameters(
894
+ * paramValidations.pathParameters,
895
+ * event.pathParameters,
896
+ * validationMatcher
897
+ * );
898
+ */
899
+ #hasValidParameters(paramValidations, clientParameters, validationMatcher) {
202
900
 
203
901
  let rValue = {
204
902
  isValid: true,
205
- params: {}
903
+ params: {},
904
+ invalidParams: []
206
905
  }
207
906
 
208
- if (clientParameters && paramValidations) {
907
+ if (clientParameters) {
908
+ // >! Check excludeParamsWithNoValidationMatch flag (default: true)
909
+ const excludeUnmatched = ClientRequest.#validations.parameters?.excludeParamsWithNoValidationMatch !== false;
910
+
911
+ // >! When no validation rules exist for this parameter type,
912
+ // >! pass through all parameters if excludeUnmatched is false
913
+ if (!paramValidations) {
914
+ if (!excludeUnmatched) {
915
+ rValue.params = { ...clientParameters };
916
+ }
917
+ return rValue;
918
+ }
919
+
920
+ // >! Track which parameters have been validated to avoid duplicate validation
921
+ const validatedParams = new Set();
922
+
923
+ // >! Create normalized parameter map for validation execution
924
+ // >! Include ALL parameter types so multi-parameter validations can access them
925
+ const normalizedParams = {};
926
+
927
+ // >! Add path parameters (if available)
928
+ if (this.#event?.pathParameters) {
929
+ for (const [key, value] of Object.entries(this.#event.pathParameters)) {
930
+ const normalizedKey = key.replace(/^\/|\/$/g, '');
931
+ normalizedParams[normalizedKey] = value;
932
+ }
933
+ }
934
+
935
+ // >! Add query parameters (if available, lowercased)
936
+ if (this.#event?.queryStringParameters) {
937
+ for (const [key, value] of Object.entries(this.#event.queryStringParameters)) {
938
+ const normalizedKey = key.toLowerCase().replace(/^\/|\/$/g, '');
939
+ normalizedParams[normalizedKey] = value;
940
+ }
941
+ }
942
+
943
+ // >! Add header parameters (if available, camelCased)
944
+ if (this.#event?.headers) {
945
+ for (const [key, value] of Object.entries(this.#event.headers)) {
946
+ const camelCaseKey = key.toLowerCase().replace(/-([a-z])/g, (g) => g[1].toUpperCase());
947
+ normalizedParams[camelCaseKey] = value;
948
+ }
949
+ }
950
+
951
+ // >! Add cookie parameters (if available)
952
+ if (this.#event?.cookie) {
953
+ for (const [key, value] of Object.entries(this.#event.cookie)) {
954
+ const normalizedKey = key.replace(/^\/|\/$/g, '');
955
+ normalizedParams[normalizedKey] = value;
956
+ }
957
+ }
958
+
959
+ // >! Add client parameters being validated to normalizedParams
960
+ // >! This ensures validation functions can access the parameters they're validating
961
+ for (const [key, value] of Object.entries(clientParameters)) {
962
+ const normalizedKey = key.replace(/^\/|\/$/g, '');
963
+ normalizedParams[normalizedKey] = value;
964
+ }
965
+
966
+ // >! Collect valid params separately so we can clear them if any fail
967
+ const collectedParams = {};
968
+
209
969
  // Use a for...of loop instead of forEach for better control flow
210
970
  for (const [key, value] of Object.entries(clientParameters)) {
971
+ // >! Preserve existing parameter key normalization
211
972
  const paramKey = key.replace(/^\/|\/$/g, '');
212
973
  const paramValue = value;
213
974
 
214
- if (paramKey in paramValidations) {
215
- const validationFunc = paramValidations[paramKey];
216
- if (typeof validationFunc === 'function' && validationFunc(paramValue)) {
217
- rValue.params[paramKey] = paramValue;
975
+ // >! Skip parameters that have already been validated
976
+ if (validatedParams.has(paramKey)) {
977
+ continue;
978
+ }
979
+
980
+ // >! Use ValidationMatcher to find the best matching validation rule
981
+ const rule = validationMatcher.findValidationRule(paramKey);
982
+
983
+ if (rule) {
984
+ // >! Use ValidationExecutor to execute validation with appropriate interface
985
+ // Pass normalized parameters so validation functions can access them by normalized names
986
+ const isValid = ValidationExecutor.execute(rule.validate, rule.params, normalizedParams);
987
+
988
+ if (isValid) {
989
+ // >! Extract ALL parameters specified in rule.params when validation passes
990
+ // >! This fixes the bug where only the current paramKey was added
991
+ for (const ruleParamName of rule.params) {
992
+ // >! Find the parameter value in clientParameters
993
+ // >! Use normalized key matching to handle case differences
994
+ const normalizedRuleParam = ruleParamName.replace(/^\/|\/$/g, '');
995
+
996
+ // >! Search for matching parameter in clientParameters
997
+ for (const [clientKey, clientValue] of Object.entries(clientParameters)) {
998
+ const normalizedClientKey = clientKey.replace(/^\/|\/$/g, '');
999
+
1000
+ if (normalizedClientKey === normalizedRuleParam) {
1001
+ collectedParams[clientKey] = clientValue;
1002
+ // >! Mark this parameter as validated to avoid duplicate validation
1003
+ validatedParams.add(normalizedClientKey);
1004
+ break;
1005
+ }
1006
+ }
1007
+ }
218
1008
  } else {
1009
+ // >! Maintain existing logging for invalid parameters
219
1010
  DebugAndLog.warn(`Invalid parameter: ${paramKey} = ${paramValue}`);
220
1011
  rValue.isValid = false;
221
- rValue.params = {};
222
- return rValue; // exit right away
1012
+ rValue.invalidParams.push(paramKey);
223
1013
  }
1014
+ } else if (!excludeUnmatched) {
1015
+ // No validation rule found, but excludeUnmatched is false
1016
+ // Include parameter without validation
1017
+ collectedParams[paramKey] = paramValue;
224
1018
  }
1019
+ // If excludeUnmatched is true and no rule found, skip parameter (existing behavior)
225
1020
  }
1021
+
1022
+ // >! If any parameter failed, clear params (preserves existing behavior)
1023
+ rValue.params = rValue.isValid ? collectedParams : {};
226
1024
  }
227
1025
  return rValue;
228
1026
  }
229
1027
 
1028
+ /**
1029
+ * Validate path parameters from the request.
1030
+ *
1031
+ * Uses ValidationMatcher to find matching validation rules based on route pattern and HTTP method.
1032
+ * Extracts validated path parameters and stores them in this.#props.pathParameters.
1033
+ *
1034
+ * @private
1035
+ * @returns {boolean} True if all path parameters are valid, false otherwise
1036
+ * @example
1037
+ * // Internal use during request validation
1038
+ * const isValid = #hasValidPathParameters();
1039
+ */
230
1040
  #hasValidPathParameters() {
231
- const { isValid, params } = this.#hasValidParameters(ClientRequest.getParameterValidations()?.pathParameters, this.#event?.pathParameters);
1041
+ const { isValid, params, invalidParams } = this.#hasValidParameters(
1042
+ ClientRequest.getParameterValidations()?.pathParameters,
1043
+ this.#event?.pathParameters,
1044
+ this.#validationMatchers.pathParameters
1045
+ );
232
1046
  this.#props.pathParameters = params;
233
- return isValid;
1047
+ return { isValid, invalidParams };
234
1048
  }
235
1049
 
1050
+ /**
1051
+ * Validate query string parameters from the request.
1052
+ *
1053
+ * Normalizes query parameter keys to lowercase before validation.
1054
+ * Uses ValidationMatcher to find matching validation rules based on route pattern and HTTP method.
1055
+ * Extracts validated query parameters and stores them in this.#props.queryStringParameters.
1056
+ * Supports both queryStringParameters and queryParameters for backwards compatibility.
1057
+ *
1058
+ * @private
1059
+ * @returns {boolean} True if all query string parameters are valid, false otherwise
1060
+ * @example
1061
+ * // Internal use during request validation
1062
+ * const isValid = #hasValidQueryStringParameters();
1063
+ */
236
1064
  #hasValidQueryStringParameters() {
237
1065
  // lowercase all the this.#event.queryStringParameters keys
238
1066
  const qs = {};
239
1067
  for (const key in this.#event.queryStringParameters) {
240
1068
  qs[key.toLowerCase()] = this.#event.queryStringParameters[key];
241
1069
  }
242
- const { isValid, params } = this.#hasValidParameters(ClientRequest.getParameterValidations()?.queryStringParameters, qs);
1070
+
1071
+ // >! Support both queryParameters and queryStringParameters for backwards compatibility
1072
+ const paramValidations = ClientRequest.getParameterValidations();
1073
+ const queryValidations = paramValidations?.queryStringParameters || paramValidations?.queryParameters;
1074
+
1075
+ const { isValid, params, invalidParams } = this.#hasValidParameters(
1076
+ queryValidations,
1077
+ qs,
1078
+ this.#validationMatchers.queryStringParameters
1079
+ );
243
1080
  this.#props.queryStringParameters = params;
244
- return isValid;
1081
+ return { isValid, invalidParams };
245
1082
  }
246
1083
 
247
- #hasValidHeaderParameters() {
248
- // camel case all the this.#event.headers keys and remove hyphens
249
- const headers = {};
250
- for (const key in this.#event.headers) {
251
- const camelCaseKey = key.toLowerCase().replace(/-([a-z])/g, (g) => g[1].toUpperCase());
252
- headers[camelCaseKey] = this.#event.headers[key];
1084
+ /**
1085
+ * Validate header parameters from the request.
1086
+ *
1087
+ * Normalizes header keys to camelCase (e.g., 'content-type' becomes 'contentType') before validation.
1088
+ * Uses ValidationMatcher to find matching validation rules based on route pattern and HTTP method.
1089
+ * Extracts validated header parameters and stores them in this.#props.headerParameters.
1090
+ *
1091
+ * @private
1092
+ * @returns {boolean} True if all header parameters are valid, false otherwise
1093
+ * @example
1094
+ * // Internal use during request validation
1095
+ * const isValid = #hasValidHeaderParameters();
1096
+ */
1097
+ /**
1098
+ * Validate header parameters from the request.
1099
+ *
1100
+ * HTTP headers are automatically converted from kebab-case to camelCase for JavaScript
1101
+ * property naming conventions. The conversion algorithm:
1102
+ * 1. Convert entire header key to lowercase
1103
+ * 2. Replace each hyphen followed by a letter with the uppercase letter
1104
+ * 3. Remove all hyphens
1105
+ *
1106
+ * This allows validation rules to use JavaScript-friendly property names while
1107
+ * maintaining compatibility with standard HTTP header naming conventions.
1108
+ *
1109
+ * @private
1110
+ * @returns {boolean} True if all header parameters are valid, false otherwise
1111
+ * @example
1112
+ * // HTTP header conversion examples:
1113
+ * // 'Content-Type' → 'contentType'
1114
+ * // 'content-type' → 'contentType'
1115
+ * // 'X-API-Key' → 'xApiKey'
1116
+ * // 'x-custom-header' → 'xCustomHeader'
1117
+ * // 'authorization' → 'authorization' (no hyphens, unchanged)
1118
+ *
1119
+ * // Internal use during request validation
1120
+ * const isValid = this.#hasValidHeaderParameters();
1121
+ */
1122
+ #hasValidHeaderParameters() {
1123
+ // camel case all the this.#event.headers keys and remove hyphens
1124
+ const headers = {};
1125
+ for (const key in this.#event.headers) {
1126
+ const camelCaseKey = key.toLowerCase().replace(/-([a-z])/g, (g) => g[1].toUpperCase());
1127
+ headers[camelCaseKey] = this.#event.headers[key];
1128
+ }
1129
+ const { isValid, params, invalidParams } = this.#hasValidParameters(
1130
+ ClientRequest.getParameterValidations()?.headerParameters,
1131
+ headers,
1132
+ this.#validationMatchers.headerParameters
1133
+ );
1134
+ this.#props.headerParameters = params;
1135
+ return { isValid, invalidParams };
253
1136
  }
254
- const { isValid, params } = this.#hasValidParameters(ClientRequest.getParameterValidations()?.headerParameters, headers);
255
- this.#props.headerParameters = params;
256
- return isValid;
257
- }
258
1137
 
259
1138
  #hasValidCookieParameters() {
260
- const { isValid, params } = this.#hasValidParameters(ClientRequest.getParameterValidations()?.cookiearameters, this.#event?.cookie); // TODO
1139
+ const { isValid, params, invalidParams } = this.#hasValidParameters(
1140
+ ClientRequest.getParameterValidations()?.cookieParameters,
1141
+ this.#event?.cookie,
1142
+ this.#validationMatchers.cookieParameters
1143
+ );
261
1144
  this.#props.cookieParameters = params;
262
- return isValid;
1145
+ return { isValid, invalidParams };
263
1146
  }
264
1147
 
1148
+ /**
1149
+ * Validate body parameters from the request.
1150
+ *
1151
+ * Parses JSON body content before validation. Handles both API Gateway v1 and v2 formats.
1152
+ * Uses ValidationMatcher to find matching validation rules based on route pattern and HTTP method.
1153
+ * Extracts validated body parameters and stores them in this.#props.bodyParameters.
1154
+ *
1155
+ * Null, undefined, or empty string bodies are treated as empty objects for validation.
1156
+ * If the body contains invalid JSON, the error is logged and validation fails.
1157
+ *
1158
+ * @private
1159
+ * @returns {boolean} True if all body parameters are valid, false otherwise
1160
+ * @example
1161
+ * // Internal use during request validation
1162
+ * const isValid = #hasValidBodyParameters();
1163
+ */
1164
+ #hasValidBodyParameters() {
1165
+ // >! Parse body content before validation
1166
+ let bodyObject = {};
1167
+
1168
+ // >! Handle null, undefined, and empty string body cases
1169
+ if (this.#event.body && this.#event.body !== '') {
1170
+ try {
1171
+ // >! Parse JSON with error handling
1172
+ bodyObject = JSON.parse(this.#event.body);
1173
+ } catch (error) {
1174
+ // >! Log JSON parsing errors
1175
+ DebugAndLog.error(
1176
+ `Failed to parse request body as JSON: ${error?.message || 'Unknown error'}`,
1177
+ error?.stack
1178
+ );
1179
+ this.#props.bodyParameters = {};
1180
+ return { isValid: false, invalidParams: [], invalidBody: true };
1181
+ }
1182
+ }
1183
+
1184
+ // >! Use existing validation framework with body validation matcher
1185
+ const { isValid, params, invalidParams } = this.#hasValidParameters(
1186
+ ClientRequest.getParameterValidations()?.bodyParameters,
1187
+ bodyObject,
1188
+ this.#validationMatchers.bodyParameters
1189
+ );
1190
+
1191
+ // >! Store validated parameters
1192
+ this.#props.bodyParameters = params;
1193
+ return { isValid, invalidParams };
1194
+ }
1195
+
1196
+
265
1197
 
266
1198
  // Utility function for getPathArray and getResourceArray
267
1199
  // Returns array slice based on n parameter
@@ -389,6 +1321,15 @@ class ClientRequest extends RequestInfo {
389
1321
  return this.#props.cookieParameters;
390
1322
  };
391
1323
 
1324
+ /**
1325
+ * Returns the body parameters received in the request.
1326
+ * Body parameters are validated in the applications validation functions.
1327
+ * @returns {object} body parameters
1328
+ */
1329
+ getBodyParameters() {
1330
+ return this.#props.bodyParameters || {};
1331
+ };
1332
+
392
1333
  #authenticate() {
393
1334
  // add your authentication logic here
394
1335
  this.authenticated = false; // anonymous
@@ -425,9 +1366,11 @@ class ClientRequest extends RequestInfo {
425
1366
  isAuthorizedReferrer() {
426
1367
  /* Check the array of valid referrers */
427
1368
  /* Check if the array includes a wildcard (*) OR if one of the whitelisted referrers matches the end of the referrer */
428
- if (ClientRequest.requiresValidReferrer()) {
1369
+ if (!ClientRequest.requiresValidReferrer()) {
1370
+ // Wildcard (*) is in the list, allow all referrers
429
1371
  return true;
430
1372
  } else {
1373
+ // Check if referrer matches one of the whitelisted referrers
431
1374
  for (let i = 0; i < ClientRequest.#validations.referrers.length; i++) {
432
1375
  if (this.getClientReferer().endsWith(ClientRequest.#validations.referrers[i])) {
433
1376
  return true;
@@ -458,7 +1401,7 @@ class ClientRequest extends RequestInfo {
458
1401
  * Get the _processed_ request properties. These are the properties that
459
1402
  * the ClientRequest object took from the event sent to Lambda, validated,
460
1403
  * supplemented, and makes available to controllers.
461
- * @returns {{ method: string, path: string, pathArray: string[], resource: string, resourceArray[], pathParameters: {}, queryStringParameters: {}, headerParameters: {}, cookieParameters: {}, bodyPayload: string, client: {isAuthenticated: boolean, isGuest: boolean, authorizations: string[], roles: string[]}, deadline: number, calcMsToDeadline: number}
1404
+ * @returns {{ method: string, path: string, pathArray: string[], resource: string, resourceArray[], pathParameters: {}, queryStringParameters: {}, headerParameters: {}, cookieParameters: {}, bodyParameters: {}, bodyPayload: string, client: {ip: string, userAgent: string, origin: string, referrer: string, isAuthenticated: boolean, isGuest: boolean, authorizations: string[], roles: string[]}, deadline: number, calcMsToDeadline: number}
462
1405
  */
463
1406
  getProps() {
464
1407
  return this.#props;