@63klabs/cache-data 1.3.9 → 1.3.11
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/CHANGELOG.md +54 -7
- package/CONTRIBUTING.md +56 -3
- package/README.md +5 -5
- package/package.json +14 -25
- package/src/lib/dao-cache.js +25 -34
- package/src/lib/dao-endpoint.js +68 -38
- package/src/lib/tools/{APIRequest.class.js → ApiRequest.class.js} +164 -51
- package/src/lib/tools/CachedParametersSecrets.classes.js +10 -10
- package/src/lib/tools/ClientRequest.class.js +183 -26
- package/src/lib/tools/Connections.classes.js +5 -5
- package/src/lib/tools/RequestInfo.class.js +38 -3
- package/src/lib/tools/Response.class.js +25 -0
- package/src/lib/tools/generic.response.html.js +8 -113
- package/src/lib/tools/generic.response.js +73 -0
- package/src/lib/tools/generic.response.json.js +5 -135
- package/src/lib/tools/generic.response.rss.js +10 -114
- package/src/lib/tools/generic.response.text.js +5 -115
- package/src/lib/tools/generic.response.xml.js +10 -114
- package/src/lib/tools/index.js +19 -8
- package/src/lib/utils/ValidationExecutor.class.js +8 -4
- package/src/lib/utils/ValidationMatcher.class.js +13 -1
- package/AGENTS.md +0 -1107
|
@@ -401,6 +401,7 @@ class ClientRequest extends RequestInfo {
|
|
|
401
401
|
|
|
402
402
|
/* Validation system */
|
|
403
403
|
#validationMatchers = {};
|
|
404
|
+
#validationReason = { isValid: true, statusCode: 200, messages: [] };
|
|
404
405
|
|
|
405
406
|
/* The request data */
|
|
406
407
|
#props = {};
|
|
@@ -470,6 +471,10 @@ class ClientRequest extends RequestInfo {
|
|
|
470
471
|
bodyParameters: {},
|
|
471
472
|
bodyPayload: this.#event?.body || null, // from body
|
|
472
473
|
client: {
|
|
474
|
+
ip: this.getClientIp(),
|
|
475
|
+
userAgent: this.getClientUserAgent(),
|
|
476
|
+
origin: this.getClientOrigin(),
|
|
477
|
+
referrer: this.getClientReferrer(),
|
|
473
478
|
isAuthenticated: this.isAuthenticated(),
|
|
474
479
|
isGuest: this.isGuest(),
|
|
475
480
|
authorizations: this.getAuthorizations(),
|
|
@@ -698,7 +703,7 @@ class ClientRequest extends RequestInfo {
|
|
|
698
703
|
* Parameter validations
|
|
699
704
|
* @returns {{
|
|
700
705
|
* pathParameters?: object,
|
|
701
|
-
*
|
|
706
|
+
* queryStringParameters?: object,
|
|
702
707
|
* headerParameters?: object,
|
|
703
708
|
* cookieParameters?: object,
|
|
704
709
|
* bodyParameters?: object
|
|
@@ -755,25 +760,129 @@ class ClientRequest extends RequestInfo {
|
|
|
755
760
|
*/
|
|
756
761
|
#validate() {
|
|
757
762
|
|
|
758
|
-
|
|
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
|
+
}
|
|
772
|
+
|
|
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
|
+
}
|
|
759
797
|
|
|
760
|
-
//
|
|
761
|
-
|
|
798
|
+
// Handle invalid JSON body
|
|
799
|
+
if (bodyResult.invalidBody) {
|
|
800
|
+
reasons.push("Invalid request body");
|
|
801
|
+
statusCode = this.#upgradeStatusCode(statusCode, 400);
|
|
802
|
+
}
|
|
762
803
|
|
|
763
|
-
//
|
|
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
|
|
764
810
|
super._isValid = valid;
|
|
765
811
|
|
|
812
|
+
// Populate validation reason
|
|
813
|
+
this.#validationReason = {
|
|
814
|
+
isValid: valid,
|
|
815
|
+
statusCode: valid ? 200 : statusCode,
|
|
816
|
+
messages: valid ? [] : reasons
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
};
|
|
820
|
+
|
|
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;
|
|
766
833
|
};
|
|
767
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
|
+
|
|
768
867
|
/**
|
|
769
868
|
* Validate parameters using ValidationMatcher and ValidationExecutor.
|
|
770
869
|
*
|
|
771
870
|
* This method implements the core parameter validation logic:
|
|
772
871
|
* 1. Uses ValidationMatcher to find the best matching validation rule (4-tier priority)
|
|
773
872
|
* 2. Uses ValidationExecutor to execute validation with appropriate interface (single or multi-parameter)
|
|
774
|
-
* 3. Extracts validated parameters and returns them
|
|
873
|
+
* 3. Extracts ALL validated parameters specified in the matching rule and returns them
|
|
775
874
|
* 4. Respects excludeParamsWithNoValidationMatch flag (default: true)
|
|
776
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
|
+
*
|
|
777
886
|
* @private
|
|
778
887
|
* @param {Object} paramValidations - Parameter validation configuration (may include BY_ROUTE, BY_METHOD, and global validations)
|
|
779
888
|
* @param {Object} clientParameters - Parameters from the request (path, query, header, or cookie parameters)
|
|
@@ -791,12 +900,25 @@ class ClientRequest extends RequestInfo {
|
|
|
791
900
|
|
|
792
901
|
let rValue = {
|
|
793
902
|
isValid: true,
|
|
794
|
-
params: {}
|
|
903
|
+
params: {},
|
|
904
|
+
invalidParams: []
|
|
795
905
|
}
|
|
796
906
|
|
|
797
|
-
if (clientParameters
|
|
907
|
+
if (clientParameters) {
|
|
798
908
|
// >! Check excludeParamsWithNoValidationMatch flag (default: true)
|
|
799
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();
|
|
800
922
|
|
|
801
923
|
// >! Create normalized parameter map for validation execution
|
|
802
924
|
// >! Include ALL parameter types so multi-parameter validations can access them
|
|
@@ -841,12 +963,20 @@ class ClientRequest extends RequestInfo {
|
|
|
841
963
|
normalizedParams[normalizedKey] = value;
|
|
842
964
|
}
|
|
843
965
|
|
|
966
|
+
// >! Collect valid params separately so we can clear them if any fail
|
|
967
|
+
const collectedParams = {};
|
|
968
|
+
|
|
844
969
|
// Use a for...of loop instead of forEach for better control flow
|
|
845
970
|
for (const [key, value] of Object.entries(clientParameters)) {
|
|
846
971
|
// >! Preserve existing parameter key normalization
|
|
847
972
|
const paramKey = key.replace(/^\/|\/$/g, '');
|
|
848
973
|
const paramValue = value;
|
|
849
974
|
|
|
975
|
+
// >! Skip parameters that have already been validated
|
|
976
|
+
if (validatedParams.has(paramKey)) {
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
|
|
850
980
|
// >! Use ValidationMatcher to find the best matching validation rule
|
|
851
981
|
const rule = validationMatcher.findValidationRule(paramKey);
|
|
852
982
|
|
|
@@ -856,22 +986,41 @@ class ClientRequest extends RequestInfo {
|
|
|
856
986
|
const isValid = ValidationExecutor.execute(rule.validate, rule.params, normalizedParams);
|
|
857
987
|
|
|
858
988
|
if (isValid) {
|
|
859
|
-
|
|
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
|
+
}
|
|
860
1008
|
} else {
|
|
861
1009
|
// >! Maintain existing logging for invalid parameters
|
|
862
1010
|
DebugAndLog.warn(`Invalid parameter: ${paramKey} = ${paramValue}`);
|
|
863
1011
|
rValue.isValid = false;
|
|
864
|
-
rValue.
|
|
865
|
-
// >! Ensure early exit on validation failure
|
|
866
|
-
return rValue;
|
|
1012
|
+
rValue.invalidParams.push(paramKey);
|
|
867
1013
|
}
|
|
868
1014
|
} else if (!excludeUnmatched) {
|
|
869
1015
|
// No validation rule found, but excludeUnmatched is false
|
|
870
1016
|
// Include parameter without validation
|
|
871
|
-
|
|
1017
|
+
collectedParams[paramKey] = paramValue;
|
|
872
1018
|
}
|
|
873
1019
|
// If excludeUnmatched is true and no rule found, skip parameter (existing behavior)
|
|
874
1020
|
}
|
|
1021
|
+
|
|
1022
|
+
// >! If any parameter failed, clear params (preserves existing behavior)
|
|
1023
|
+
rValue.params = rValue.isValid ? collectedParams : {};
|
|
875
1024
|
}
|
|
876
1025
|
return rValue;
|
|
877
1026
|
}
|
|
@@ -889,13 +1038,13 @@ class ClientRequest extends RequestInfo {
|
|
|
889
1038
|
* const isValid = #hasValidPathParameters();
|
|
890
1039
|
*/
|
|
891
1040
|
#hasValidPathParameters() {
|
|
892
|
-
const { isValid, params } = this.#hasValidParameters(
|
|
1041
|
+
const { isValid, params, invalidParams } = this.#hasValidParameters(
|
|
893
1042
|
ClientRequest.getParameterValidations()?.pathParameters,
|
|
894
1043
|
this.#event?.pathParameters,
|
|
895
1044
|
this.#validationMatchers.pathParameters
|
|
896
1045
|
);
|
|
897
1046
|
this.#props.pathParameters = params;
|
|
898
|
-
return isValid;
|
|
1047
|
+
return { isValid, invalidParams };
|
|
899
1048
|
}
|
|
900
1049
|
|
|
901
1050
|
/**
|
|
@@ -923,13 +1072,13 @@ class ClientRequest extends RequestInfo {
|
|
|
923
1072
|
const paramValidations = ClientRequest.getParameterValidations();
|
|
924
1073
|
const queryValidations = paramValidations?.queryStringParameters || paramValidations?.queryParameters;
|
|
925
1074
|
|
|
926
|
-
const { isValid, params } = this.#hasValidParameters(
|
|
1075
|
+
const { isValid, params, invalidParams } = this.#hasValidParameters(
|
|
927
1076
|
queryValidations,
|
|
928
1077
|
qs,
|
|
929
1078
|
this.#validationMatchers.queryStringParameters
|
|
930
1079
|
);
|
|
931
1080
|
this.#props.queryStringParameters = params;
|
|
932
|
-
return isValid;
|
|
1081
|
+
return { isValid, invalidParams };
|
|
933
1082
|
}
|
|
934
1083
|
|
|
935
1084
|
/**
|
|
@@ -977,23 +1126,23 @@ class ClientRequest extends RequestInfo {
|
|
|
977
1126
|
const camelCaseKey = key.toLowerCase().replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
978
1127
|
headers[camelCaseKey] = this.#event.headers[key];
|
|
979
1128
|
}
|
|
980
|
-
const { isValid, params } = this.#hasValidParameters(
|
|
1129
|
+
const { isValid, params, invalidParams } = this.#hasValidParameters(
|
|
981
1130
|
ClientRequest.getParameterValidations()?.headerParameters,
|
|
982
1131
|
headers,
|
|
983
1132
|
this.#validationMatchers.headerParameters
|
|
984
1133
|
);
|
|
985
1134
|
this.#props.headerParameters = params;
|
|
986
|
-
return isValid;
|
|
1135
|
+
return { isValid, invalidParams };
|
|
987
1136
|
}
|
|
988
1137
|
|
|
989
1138
|
#hasValidCookieParameters() {
|
|
990
|
-
const { isValid, params } = this.#hasValidParameters(
|
|
1139
|
+
const { isValid, params, invalidParams } = this.#hasValidParameters(
|
|
991
1140
|
ClientRequest.getParameterValidations()?.cookieParameters,
|
|
992
1141
|
this.#event?.cookie,
|
|
993
1142
|
this.#validationMatchers.cookieParameters
|
|
994
1143
|
);
|
|
995
1144
|
this.#props.cookieParameters = params;
|
|
996
|
-
return isValid;
|
|
1145
|
+
return { isValid, invalidParams };
|
|
997
1146
|
}
|
|
998
1147
|
|
|
999
1148
|
/**
|
|
@@ -1028,12 +1177,12 @@ class ClientRequest extends RequestInfo {
|
|
|
1028
1177
|
error?.stack
|
|
1029
1178
|
);
|
|
1030
1179
|
this.#props.bodyParameters = {};
|
|
1031
|
-
return false;
|
|
1180
|
+
return { isValid: false, invalidParams: [], invalidBody: true };
|
|
1032
1181
|
}
|
|
1033
1182
|
}
|
|
1034
1183
|
|
|
1035
1184
|
// >! Use existing validation framework with body validation matcher
|
|
1036
|
-
const { isValid, params } = this.#hasValidParameters(
|
|
1185
|
+
const { isValid, params, invalidParams } = this.#hasValidParameters(
|
|
1037
1186
|
ClientRequest.getParameterValidations()?.bodyParameters,
|
|
1038
1187
|
bodyObject,
|
|
1039
1188
|
this.#validationMatchers.bodyParameters
|
|
@@ -1041,7 +1190,7 @@ class ClientRequest extends RequestInfo {
|
|
|
1041
1190
|
|
|
1042
1191
|
// >! Store validated parameters
|
|
1043
1192
|
this.#props.bodyParameters = params;
|
|
1044
|
-
return isValid;
|
|
1193
|
+
return { isValid, invalidParams };
|
|
1045
1194
|
}
|
|
1046
1195
|
|
|
1047
1196
|
|
|
@@ -1251,8 +1400,16 @@ class ClientRequest extends RequestInfo {
|
|
|
1251
1400
|
/**
|
|
1252
1401
|
* Get the _processed_ request properties. These are the properties that
|
|
1253
1402
|
* the ClientRequest object took from the event sent to Lambda, validated,
|
|
1254
|
-
* supplemented, and makes available to controllers.
|
|
1255
|
-
*
|
|
1403
|
+
* supplemented, and makes available to controllers.
|
|
1404
|
+
*
|
|
1405
|
+
* Note When accessed behind CloudFront:
|
|
1406
|
+
* To ensure the user agent is passed along to API Gateway, use the AWS
|
|
1407
|
+
* managed Origin Request Policy named AllViewerExceptHostHeader. This
|
|
1408
|
+
* forwards the User-Agent along with most other viewer headers while
|
|
1409
|
+
* maintaining the correct Host header required for API Gateway to route
|
|
1410
|
+
* the request properly.
|
|
1411
|
+
*
|
|
1412
|
+
* @returns {{ method: string, path: string, pathArray: string[], resource: string, resourceArray: string[], 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: function}
|
|
1256
1413
|
*/
|
|
1257
1414
|
getProps() {
|
|
1258
1415
|
return this.#props;
|
|
@@ -32,7 +32,7 @@ const { safeClone } = require('./utils');
|
|
|
32
32
|
*
|
|
33
33
|
* // Use with endpoint requests
|
|
34
34
|
* const dbConn = connections.get('database');
|
|
35
|
-
* const result = await endpoint.
|
|
35
|
+
* const result = await endpoint.send(dbConn);
|
|
36
36
|
*/
|
|
37
37
|
class Connections {
|
|
38
38
|
|
|
@@ -132,7 +132,7 @@ class Connections {
|
|
|
132
132
|
* The Connection object provides the base for requests but does not carry
|
|
133
133
|
* the request. myConnection.get() will return an object (associative array)
|
|
134
134
|
* that can then be used to generate and submit a request to a DAO class or
|
|
135
|
-
*
|
|
135
|
+
* ApiRequest object.
|
|
136
136
|
* You can store and manage multiple connections using the Connections object.
|
|
137
137
|
* @example
|
|
138
138
|
* // Create a simple connection
|
|
@@ -145,7 +145,7 @@ class Connections {
|
|
|
145
145
|
*
|
|
146
146
|
* // Get connection object for use with endpoint
|
|
147
147
|
* const connObj = apiConnection.get();
|
|
148
|
-
* const users = await endpoint.
|
|
148
|
+
* const users = await endpoint.send(connObj);
|
|
149
149
|
*
|
|
150
150
|
* @example
|
|
151
151
|
* // Create connection with authentication
|
|
@@ -581,7 +581,7 @@ class ConnectionAuthentication {
|
|
|
581
581
|
* request.addParameter('page', 1);
|
|
582
582
|
*
|
|
583
583
|
* // Make the request
|
|
584
|
-
* const result = await endpoint.
|
|
584
|
+
* const result = await endpoint.send(request);
|
|
585
585
|
*
|
|
586
586
|
* @example
|
|
587
587
|
* // Build a request dynamically in a DAO
|
|
@@ -597,7 +597,7 @@ class ConnectionAuthentication {
|
|
|
597
597
|
* 'Content-Type': 'application/json'
|
|
598
598
|
* });
|
|
599
599
|
*
|
|
600
|
-
* return await endpoint.
|
|
600
|
+
* return await endpoint.send(request);
|
|
601
601
|
* }
|
|
602
602
|
* }
|
|
603
603
|
*/
|
|
@@ -130,6 +130,12 @@ class RequestInfo {
|
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
132
|
* User Agent of client request
|
|
133
|
+
* Note When accessed behind CloudFront:
|
|
134
|
+
* To ensure the user agent is passed along to API Gateway, use the AWS
|
|
135
|
+
* managed Origin Request Policy named AllViewerExceptHostHeader. This
|
|
136
|
+
* forwards the User-Agent along with most other viewer headers while
|
|
137
|
+
* maintaining the correct Host header required for API Gateway to route
|
|
138
|
+
* the request properly.
|
|
133
139
|
* @returns {string} The user agent string supplied by the client request
|
|
134
140
|
*/
|
|
135
141
|
getClientUserAgent() {
|
|
@@ -259,9 +265,25 @@ class RequestInfo {
|
|
|
259
265
|
|
|
260
266
|
|
|
261
267
|
/**
|
|
262
|
-
* Obtain lambda event request details for logging
|
|
263
|
-
*
|
|
264
|
-
*
|
|
268
|
+
* Obtain lambda event request details for logging.
|
|
269
|
+
* Parses the API Gateway event to extract client information including IP,
|
|
270
|
+
* user agent, origin, referrer, headers, and query parameters.
|
|
271
|
+
*
|
|
272
|
+
* For `client.ip`, the `x-forwarded-for` header is preferred over
|
|
273
|
+
* `identity.sourceIp`. When `x-forwarded-for` is present and non-empty,
|
|
274
|
+
* the first IP from the comma-separated list is used (trimmed). This
|
|
275
|
+
* ensures the original client IP is returned when behind a proxy such
|
|
276
|
+
* as CloudFront. Falls back to `identity.sourceIp` when the header is
|
|
277
|
+
* absent or empty.
|
|
278
|
+
*
|
|
279
|
+
* For `client.userAgent`, the `user-agent` header is preferred over
|
|
280
|
+
* `identity.userAgent`. When the header is present and non-empty, its
|
|
281
|
+
* value is used directly. This ensures the original client user agent
|
|
282
|
+
* is returned instead of a proxy identifier (e.g. "Amazon CloudFront").
|
|
283
|
+
* Falls back to `identity.userAgent` when the header is absent or empty.
|
|
284
|
+
*
|
|
285
|
+
* @param {object} event - API Gateway Lambda event object
|
|
286
|
+
* @returns {{ip: string|null, userAgent: string|null, origin: string|null, referrer: string|null, ifModifiedSince: string|null, ifNoneMatch: string|null, accept: string|null, headers: object, parameters: object, body: string|null}} Client request information
|
|
265
287
|
*/
|
|
266
288
|
_clientRequestInfo (event) {
|
|
267
289
|
|
|
@@ -292,11 +314,24 @@ class RequestInfo {
|
|
|
292
314
|
client.ip = identity.sourceIp;
|
|
293
315
|
}
|
|
294
316
|
|
|
317
|
+
// if x-forwarded-for header is present, prefer it over identity.sourceIp (first IP in the list is the original client)
|
|
318
|
+
if ( "x-forwarded-for" in headers && headers["x-forwarded-for"] !== null && headers["x-forwarded-for"] !== "" ) {
|
|
319
|
+
let firstIp = headers["x-forwarded-for"].split(",")[0].trim();
|
|
320
|
+
if ( firstIp !== "" ) {
|
|
321
|
+
client.ip = firstIp;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
295
325
|
// if there is a user-agent header, set it
|
|
296
326
|
if ( "userAgent" in identity && identity.userAgent !== null ) {
|
|
297
327
|
client.userAgent = identity.userAgent;
|
|
298
328
|
}
|
|
299
329
|
|
|
330
|
+
// if user-agent header is present, prefer it over identity.userAgent (original client user agent)
|
|
331
|
+
if ( "user-agent" in headers && headers["user-agent"] !== null && headers["user-agent"] !== "" ) {
|
|
332
|
+
client.userAgent = headers["user-agent"];
|
|
333
|
+
}
|
|
334
|
+
|
|
300
335
|
// if there is an origin header, set it
|
|
301
336
|
if ( headers?.origin) {
|
|
302
337
|
client.origin = headers.origin;
|
|
@@ -525,6 +525,31 @@ class Response {
|
|
|
525
525
|
}
|
|
526
526
|
};
|
|
527
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Sets a message or messages property on the JSON response body.
|
|
530
|
+
* If the body is not a JSON object, this method is a no-op.
|
|
531
|
+
* Does not alter the status code or headers.
|
|
532
|
+
*
|
|
533
|
+
* @param {string|Array<string>} message - A single message string or array of message strings
|
|
534
|
+
* @returns {void}
|
|
535
|
+
* @example
|
|
536
|
+
* // Single message
|
|
537
|
+
* response.setMessage("Invalid parameter: limit");
|
|
538
|
+
* // body becomes: { ...existingBody, message: "Invalid parameter: limit" }
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* // Multiple messages
|
|
542
|
+
* response.setMessage(["Invalid parameter: limit", "Invalid parameter: offset"]);
|
|
543
|
+
* // body becomes: { ...existingBody, messages: ["Invalid parameter: limit", "Invalid parameter: offset"] }
|
|
544
|
+
*/
|
|
545
|
+
setMessage = (message) => {
|
|
546
|
+
if (Array.isArray(message)) {
|
|
547
|
+
this.addToJsonBody({ messages: message });
|
|
548
|
+
} else {
|
|
549
|
+
this.addToJsonBody({ message: message });
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
|
|
528
553
|
/**
|
|
529
554
|
* Converts the response to a plain object.
|
|
530
555
|
*
|
|
@@ -1,121 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
const { createGenericResponseModule } = require("./generic.response");
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
"Content-Type": contentType
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
html = (title, body) => {
|
|
3
|
+
const html = (title, body) => {
|
|
8
4
|
return `<html><head><title>${title}</title></head><body>${body}</body></html>`;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
response200 = {
|
|
12
|
-
statusCode: 200,
|
|
13
|
-
headers: headers,
|
|
14
|
-
body: html("200 OK", "<p>Success</p>")
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
response400 = {
|
|
18
|
-
statusCode: 400,
|
|
19
|
-
headers: headers,
|
|
20
|
-
body: html("400 Bad Request", "<p>Bad Request</p>")
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
response401 = {
|
|
24
|
-
statusCode: 401,
|
|
25
|
-
headers: headers,
|
|
26
|
-
body: html("401 Unauthorized", "<p>Unauthorized</p>")
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
response403 = {
|
|
30
|
-
statusCode: 403,
|
|
31
|
-
headers: headers,
|
|
32
|
-
body: html("403 Forbidden", "<p>Forbidden</p>")
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
response404 = {
|
|
36
|
-
statusCode: 404,
|
|
37
|
-
headers: headers,
|
|
38
|
-
body: html("404 Not Found", "<p>Not Found</p>")
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
response405 = {
|
|
42
|
-
statusCode: 405,
|
|
43
|
-
headers: headers,
|
|
44
|
-
body: html("405 Method Not Allowed", "<p>Method Not Allowed</p>")
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
response408 = {
|
|
48
|
-
statusCode: 408,
|
|
49
|
-
headers: headers,
|
|
50
|
-
body: html("408 Request Timeout", "<p>Request Timeout</p>")
|
|
51
5
|
};
|
|
52
6
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
body: html("418 I'm a teapot", "<p>I'm a teapot</p>")
|
|
7
|
+
const HTML_TITLE_MAP = {
|
|
8
|
+
200: "OK",
|
|
9
|
+
500: "Error"
|
|
57
10
|
};
|
|
58
11
|
|
|
59
|
-
|
|
60
|
-
statusCode: 427,
|
|
61
|
-
headers: headers,
|
|
62
|
-
body: html("427 Too Many Requests", "<p>Too Many Requests</p>")
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
response500 = {
|
|
66
|
-
statusCode: 500,
|
|
67
|
-
headers: headers,
|
|
68
|
-
body: html("500 Error", "<p>Internal Server Error</p>")
|
|
69
|
-
};
|
|
12
|
+
const htmlBodyFormatter = (statusCode, message) => html(statusCode + " " + (HTML_TITLE_MAP[statusCode] || message), "<p>" + message + "</p>");
|
|
70
13
|
|
|
71
|
-
|
|
72
|
-
*
|
|
73
|
-
* @param {number|string} statusCode
|
|
74
|
-
* @returns {{statusCode: number, headers: object, body: Array|Object|string}}
|
|
75
|
-
*/
|
|
76
|
-
response = function (statusCode) {
|
|
77
|
-
// convert to int
|
|
78
|
-
statusCode = parseInt(statusCode, 10);
|
|
79
|
-
|
|
80
|
-
switch (statusCode) {
|
|
81
|
-
case 200:
|
|
82
|
-
return this.response200;
|
|
83
|
-
case 400:
|
|
84
|
-
return this.response400;
|
|
85
|
-
case 401:
|
|
86
|
-
return this.response401;
|
|
87
|
-
case 403:
|
|
88
|
-
return this.response403;
|
|
89
|
-
case 404:
|
|
90
|
-
return this.response404;
|
|
91
|
-
case 405:
|
|
92
|
-
return this.response405;
|
|
93
|
-
case 408:
|
|
94
|
-
return this.response408;
|
|
95
|
-
case 418:
|
|
96
|
-
return this.response418;
|
|
97
|
-
case 427:
|
|
98
|
-
return this.response427;
|
|
99
|
-
case 500:
|
|
100
|
-
return this.response500;
|
|
101
|
-
default:
|
|
102
|
-
return this.response500;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
14
|
+
const mod = createGenericResponseModule("text/html; charset=utf-8", htmlBodyFormatter);
|
|
105
15
|
|
|
106
|
-
module.exports = {
|
|
107
|
-
contentType,
|
|
108
|
-
headers,
|
|
109
|
-
html,
|
|
110
|
-
response200,
|
|
111
|
-
response400,
|
|
112
|
-
response401,
|
|
113
|
-
response403,
|
|
114
|
-
response404,
|
|
115
|
-
response405,
|
|
116
|
-
response408,
|
|
117
|
-
response418,
|
|
118
|
-
response427,
|
|
119
|
-
response500,
|
|
120
|
-
response
|
|
121
|
-
}
|
|
16
|
+
module.exports = { ...mod, html };
|