@arcis/node 1.0.0 → 1.2.0
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/README.md +156 -222
- package/dist/core/index.d.mts +4 -4
- package/dist/core/index.d.ts +4 -4
- package/dist/core/index.js +13 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +13 -2
- package/dist/core/index.mjs.map +1 -1
- package/dist/index-A-m-pPeW.d.mts +340 -0
- package/dist/index-CgK94hY_.d.mts +532 -0
- package/dist/index-Co5kPRZz.d.ts +340 -0
- package/dist/index-D_bdJcF0.d.ts +532 -0
- package/dist/index.d.mts +144 -108
- package/dist/index.d.ts +144 -108
- package/dist/index.js +1541 -211
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1515 -212
- package/dist/index.mjs.map +1 -1
- package/dist/logging/index.d.mts +1 -1
- package/dist/logging/index.d.ts +1 -1
- package/dist/logging/index.js +12 -1
- package/dist/logging/index.js.map +1 -1
- package/dist/logging/index.mjs +12 -1
- package/dist/logging/index.mjs.map +1 -1
- package/dist/middleware/index.d.mts +2 -2
- package/dist/middleware/index.d.ts +2 -2
- package/dist/middleware/index.js +524 -4
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +517 -5
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/{headers-DBQedhrb.d.mts → pii-CXcHMlnX.d.mts} +156 -2
- package/dist/{headers-BJq2OA0i.d.ts → pii-DhNpl7M3.d.ts} +156 -2
- package/dist/sanitizers/index.d.mts +2 -2
- package/dist/sanitizers/index.d.ts +2 -2
- package/dist/sanitizers/index.js +331 -3
- package/dist/sanitizers/index.js.map +1 -1
- package/dist/sanitizers/index.mjs +321 -4
- package/dist/sanitizers/index.mjs.map +1 -1
- package/dist/stores/index.d.mts +1 -1
- package/dist/stores/index.d.ts +1 -1
- package/dist/stores/index.js.map +1 -1
- package/dist/stores/index.mjs.map +1 -1
- package/dist/{types-BOdL3ZWo.d.mts → types-CsOFHoD9.d.mts} +6 -1
- package/dist/{types-BOdL3ZWo.d.ts → types-CsOFHoD9.d.ts} +6 -1
- package/dist/validation/index.d.mts +2 -2
- package/dist/validation/index.d.ts +2 -2
- package/dist/validation/index.js +504 -2
- package/dist/validation/index.js.map +1 -1
- package/dist/validation/index.mjs +498 -3
- package/dist/validation/index.mjs.map +1 -1
- package/package.json +114 -109
- package/dist/index-BgHPM7LC.d.ts +0 -129
- package/dist/index-BpT7flAQ.d.ts +0 -255
- package/dist/index-JaFOUKyK.d.mts +0 -255
- package/dist/index-nAgXexwD.d.mts +0 -129
package/dist/index.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
+
var dns = require('dns');
|
|
6
|
+
var crypto = require('crypto');
|
|
7
|
+
|
|
5
8
|
// src/core/constants.ts
|
|
6
9
|
var INPUT = {
|
|
7
10
|
/** Default maximum input size (1MB) */
|
|
@@ -116,7 +119,11 @@ var SQL_PATTERNS = [
|
|
|
116
119
|
/** Time-based blind: SLEEP() */
|
|
117
120
|
/\bSLEEP\s*\(\s*\d+\s*\)/gi,
|
|
118
121
|
/** Time-based blind: BENCHMARK() */
|
|
119
|
-
/\bBENCHMARK\s*\(/gi
|
|
122
|
+
/\bBENCHMARK\s*\(/gi,
|
|
123
|
+
/** Time-based blind: PostgreSQL pg_sleep() */
|
|
124
|
+
/\bpg_sleep\s*\(/gi,
|
|
125
|
+
/** Time-based blind: MSSQL WAITFOR DELAY */
|
|
126
|
+
/\bWAITFOR\s+DELAY\b/gi
|
|
120
127
|
];
|
|
121
128
|
var PATH_PATTERNS = [
|
|
122
129
|
/** Unix path traversal */
|
|
@@ -134,6 +141,10 @@ var PATH_PATTERNS = [
|
|
|
134
141
|
/\.%2e[\\/]/gi,
|
|
135
142
|
/** Fully URL-encoded: %2e%2e%2f */
|
|
136
143
|
/%2e%2e%2f/gi,
|
|
144
|
+
/** Double URL-encoded forward slash: %252f */
|
|
145
|
+
/%252f/gi,
|
|
146
|
+
/** Dotdotslash bypass: ....// or ....\\ */
|
|
147
|
+
/\.{2,}[/\\]{2,}/g,
|
|
137
148
|
/** Null byte injection in paths */
|
|
138
149
|
/\0/g
|
|
139
150
|
];
|
|
@@ -149,7 +160,9 @@ var COMMAND_PATTERNS = [
|
|
|
149
160
|
*/
|
|
150
161
|
/[;&|`]/g,
|
|
151
162
|
/** Command substitution: $( ... ) — matched as a pair to reduce false positives */
|
|
152
|
-
/\$\(/g
|
|
163
|
+
/\$\(/g,
|
|
164
|
+
/** URL-encoded newline/carriage-return injection (%0a, %0d) */
|
|
165
|
+
/%0[ad]/gi
|
|
153
166
|
];
|
|
154
167
|
var DANGEROUS_PROTO_KEYS = /* @__PURE__ */ new Set([
|
|
155
168
|
"__proto__",
|
|
@@ -183,6 +196,7 @@ var NOSQL_DANGEROUS_KEYS = /* @__PURE__ */ new Set([
|
|
|
183
196
|
"$expr",
|
|
184
197
|
"$mod",
|
|
185
198
|
"$text",
|
|
199
|
+
"$jsonSchema",
|
|
186
200
|
// Array
|
|
187
201
|
"$elemMatch",
|
|
188
202
|
"$all",
|
|
@@ -783,7 +797,8 @@ function sanitizeObject(obj, options = {}) {
|
|
|
783
797
|
if (typeof obj === "string") return sanitizeString(obj, options);
|
|
784
798
|
if (typeof obj !== "object") return obj;
|
|
785
799
|
if (Array.isArray(obj)) return obj.map((item) => sanitizeObject(item, options));
|
|
786
|
-
|
|
800
|
+
const result = sanitizeObjectDepth(obj, options, 0);
|
|
801
|
+
return options.freeze ? Object.freeze(result) : result;
|
|
787
802
|
}
|
|
788
803
|
function sanitizeObjectDepth(obj, options, depth) {
|
|
789
804
|
if (depth >= INPUT.MAX_RECURSION_DEPTH) return obj;
|
|
@@ -880,6 +895,179 @@ function detectPrototypePollution(obj, maxDepth = 10) {
|
|
|
880
895
|
return false;
|
|
881
896
|
}
|
|
882
897
|
|
|
898
|
+
// src/sanitizers/ssti.ts
|
|
899
|
+
var SSTI_DETECT_PATTERNS = [
|
|
900
|
+
/** Jinja2 / Twig / Nunjucks: {{ ... }} */
|
|
901
|
+
/\{\{.*?\}\}/g,
|
|
902
|
+
/** Freemarker / Thymeleaf / Spring EL: ${ ... } */
|
|
903
|
+
/\$\{.*?\}/g,
|
|
904
|
+
/** ERB / EJS: <%= ... %> or <% ... %> */
|
|
905
|
+
/<%[=\-]?.*?%>/gs,
|
|
906
|
+
/** Pug / Jade / Slim: #{ ... } */
|
|
907
|
+
/#\{.*?\}/g,
|
|
908
|
+
/** Python dunder sandbox escape */
|
|
909
|
+
/__(?:class|mro|subclasses|globals|builtins|import)__/gi,
|
|
910
|
+
/** Jinja2 config leak: {{config.X}} or {{config['X']}} */
|
|
911
|
+
/\{\{\s*config[.\[]/gi,
|
|
912
|
+
/** Jinja2 built-in objects */
|
|
913
|
+
/\{\{\s*(?:self|request|lipsum|cycler|joiner|namespace|range)\b/gi
|
|
914
|
+
];
|
|
915
|
+
var SSTI_REMOVE_PATTERNS = [
|
|
916
|
+
/\{\{.*?\}\}/g,
|
|
917
|
+
/\$\{.*?\}/g,
|
|
918
|
+
/<%[=\-]?.*?%>/gs,
|
|
919
|
+
/#\{.*?\}/g,
|
|
920
|
+
/__(?:class|mro|subclasses|globals|builtins|import)__/gi
|
|
921
|
+
];
|
|
922
|
+
function sanitizeSsti(input, collectThreats = false) {
|
|
923
|
+
if (typeof input !== "string") {
|
|
924
|
+
return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
|
|
925
|
+
}
|
|
926
|
+
const threats = [];
|
|
927
|
+
let value = input;
|
|
928
|
+
let wasSanitized = false;
|
|
929
|
+
for (const pattern of SSTI_REMOVE_PATTERNS) {
|
|
930
|
+
pattern.lastIndex = 0;
|
|
931
|
+
if (pattern.test(value)) {
|
|
932
|
+
pattern.lastIndex = 0;
|
|
933
|
+
if (collectThreats) {
|
|
934
|
+
const matches = value.match(pattern);
|
|
935
|
+
if (matches) {
|
|
936
|
+
for (const match of matches) {
|
|
937
|
+
threats.push({
|
|
938
|
+
type: "ssti",
|
|
939
|
+
pattern: pattern.source,
|
|
940
|
+
original: match
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
value = value.replace(pattern, "");
|
|
946
|
+
wasSanitized = true;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (collectThreats) {
|
|
950
|
+
return { value, wasSanitized, threats };
|
|
951
|
+
}
|
|
952
|
+
return value;
|
|
953
|
+
}
|
|
954
|
+
function detectSsti(input) {
|
|
955
|
+
if (typeof input !== "string") return false;
|
|
956
|
+
for (const pattern of SSTI_DETECT_PATTERNS) {
|
|
957
|
+
pattern.lastIndex = 0;
|
|
958
|
+
if (pattern.test(input)) {
|
|
959
|
+
return true;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/sanitizers/xxe.ts
|
|
966
|
+
var XXE_DETECT_PATTERNS = [
|
|
967
|
+
/** DOCTYPE declaration */
|
|
968
|
+
/<!DOCTYPE\b/gi,
|
|
969
|
+
/** ENTITY declaration */
|
|
970
|
+
/<!ENTITY\b/gi,
|
|
971
|
+
/** SYSTEM keyword with URI */
|
|
972
|
+
/\bSYSTEM\s+["']/gi,
|
|
973
|
+
/** PUBLIC keyword with URI */
|
|
974
|
+
/\bPUBLIC\s+["']/gi,
|
|
975
|
+
/** Parameter entity reference (%entity;) */
|
|
976
|
+
/%\s*\w+\s*;/g,
|
|
977
|
+
/** CDATA section (often used to smuggle payloads) */
|
|
978
|
+
/<!\[CDATA\[/gi
|
|
979
|
+
];
|
|
980
|
+
var XXE_REMOVE_PATTERNS = [
|
|
981
|
+
/** Full DOCTYPE block with optional internal subset: <!DOCTYPE ... [...]> */
|
|
982
|
+
/<!DOCTYPE\s[^[>]*(?:\[[^\]]*\]\s*)?>|<!DOCTYPE\s[^>]*>/gi,
|
|
983
|
+
/** Full ENTITY declaration: <!ENTITY ... > */
|
|
984
|
+
/<!ENTITY[^>]*>/gi,
|
|
985
|
+
/** CDATA sections: <![CDATA[ ... ]]> */
|
|
986
|
+
/<!\[CDATA\[[\s\S]*?\]\]>/gi
|
|
987
|
+
];
|
|
988
|
+
function sanitizeXxe(input, collectThreats = false) {
|
|
989
|
+
if (typeof input !== "string") {
|
|
990
|
+
return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
|
|
991
|
+
}
|
|
992
|
+
const threats = [];
|
|
993
|
+
let value = input;
|
|
994
|
+
let wasSanitized = false;
|
|
995
|
+
for (const pattern of XXE_REMOVE_PATTERNS) {
|
|
996
|
+
pattern.lastIndex = 0;
|
|
997
|
+
if (pattern.test(value)) {
|
|
998
|
+
pattern.lastIndex = 0;
|
|
999
|
+
if (collectThreats) {
|
|
1000
|
+
const matches = value.match(pattern);
|
|
1001
|
+
if (matches) {
|
|
1002
|
+
for (const match of matches) {
|
|
1003
|
+
threats.push({
|
|
1004
|
+
type: "xxe",
|
|
1005
|
+
pattern: pattern.source,
|
|
1006
|
+
original: match
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
value = value.replace(pattern, "");
|
|
1012
|
+
wasSanitized = true;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
if (collectThreats) {
|
|
1016
|
+
return { value, wasSanitized, threats };
|
|
1017
|
+
}
|
|
1018
|
+
return value;
|
|
1019
|
+
}
|
|
1020
|
+
function detectXxe(input) {
|
|
1021
|
+
if (typeof input !== "string") return false;
|
|
1022
|
+
for (const pattern of XXE_DETECT_PATTERNS) {
|
|
1023
|
+
pattern.lastIndex = 0;
|
|
1024
|
+
if (pattern.test(input)) {
|
|
1025
|
+
return true;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// src/sanitizers/jsonp.ts
|
|
1032
|
+
var SAFE_CALLBACK_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$.[\]]*$/;
|
|
1033
|
+
var DANGEROUS_CALLBACK_PATTERNS = [
|
|
1034
|
+
/\.\./,
|
|
1035
|
+
// prototype chain traversal
|
|
1036
|
+
/\[\s*\]/
|
|
1037
|
+
// empty bracket access
|
|
1038
|
+
];
|
|
1039
|
+
function sanitizeJsonpCallback(callback, maxLength = 128) {
|
|
1040
|
+
if (typeof callback !== "string" || callback.length === 0) {
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
if (callback.length > maxLength) {
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
if (!SAFE_CALLBACK_PATTERN.test(callback)) {
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
for (const pattern of DANGEROUS_CALLBACK_PATTERNS) {
|
|
1050
|
+
if (pattern.test(callback)) {
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return callback;
|
|
1055
|
+
}
|
|
1056
|
+
function detectJsonpInjection(callback) {
|
|
1057
|
+
if (typeof callback !== "string" || callback.length === 0) {
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
if (!SAFE_CALLBACK_PATTERN.test(callback)) {
|
|
1061
|
+
return true;
|
|
1062
|
+
}
|
|
1063
|
+
for (const pattern of DANGEROUS_CALLBACK_PATTERNS) {
|
|
1064
|
+
if (pattern.test(callback)) {
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return false;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
883
1071
|
// src/sanitizers/headers.ts
|
|
884
1072
|
var HEADER_INJECTION_PATTERN = /\r\n|\r|\n|\0/g;
|
|
885
1073
|
function sanitizeHeaderValue(input, collectThreats = false) {
|
|
@@ -929,6 +1117,138 @@ function detectHeaderInjection(input) {
|
|
|
929
1117
|
return HEADER_INJECTION_PATTERN.test(input);
|
|
930
1118
|
}
|
|
931
1119
|
|
|
1120
|
+
// src/sanitizers/pii.ts
|
|
1121
|
+
var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z]{2,})+/g;
|
|
1122
|
+
var PHONE_RE = /(?:\+?1[-.\s]?)?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g;
|
|
1123
|
+
var CREDIT_CARD_RE = /\b(?:\d[ -]*?){13,19}\b/g;
|
|
1124
|
+
var SSN_RE = /\b\d{3}[-\s]\d{2}[-\s]\d{4}\b/g;
|
|
1125
|
+
var IPV4_RE = /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g;
|
|
1126
|
+
var IPV6_RE = /\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b|\b(?:[0-9a-fA-F]{1,4}:){1,7}:|::(?:[0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4}\b/g;
|
|
1127
|
+
var PATTERN_MAP = {
|
|
1128
|
+
email: [EMAIL_RE],
|
|
1129
|
+
phone: [PHONE_RE],
|
|
1130
|
+
credit_card: [CREDIT_CARD_RE],
|
|
1131
|
+
ssn: [SSN_RE],
|
|
1132
|
+
ip_address: [IPV4_RE, IPV6_RE]
|
|
1133
|
+
};
|
|
1134
|
+
var ALL_TYPES = ["email", "phone", "credit_card", "ssn", "ip_address"];
|
|
1135
|
+
var TYPE_LABELS = {
|
|
1136
|
+
email: "[EMAIL]",
|
|
1137
|
+
phone: "[PHONE]",
|
|
1138
|
+
credit_card: "[CREDIT_CARD]",
|
|
1139
|
+
ssn: "[SSN]",
|
|
1140
|
+
ip_address: "[IP_ADDRESS]"
|
|
1141
|
+
};
|
|
1142
|
+
function luhnCheck(value) {
|
|
1143
|
+
const digits = value.replace(/[\s-]/g, "");
|
|
1144
|
+
if (!/^\d{13,19}$/.test(digits)) return false;
|
|
1145
|
+
let sum = 0;
|
|
1146
|
+
let alternate = false;
|
|
1147
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
1148
|
+
let n = parseInt(digits[i], 10);
|
|
1149
|
+
if (alternate) {
|
|
1150
|
+
n *= 2;
|
|
1151
|
+
if (n > 9) n -= 9;
|
|
1152
|
+
}
|
|
1153
|
+
sum += n;
|
|
1154
|
+
alternate = !alternate;
|
|
1155
|
+
}
|
|
1156
|
+
return sum % 10 === 0;
|
|
1157
|
+
}
|
|
1158
|
+
function scanPii(input, options = {}) {
|
|
1159
|
+
if (!input || typeof input !== "string") return [];
|
|
1160
|
+
const types = options.types ?? ALL_TYPES;
|
|
1161
|
+
const matches = [];
|
|
1162
|
+
for (const type of types) {
|
|
1163
|
+
const patterns = PATTERN_MAP[type];
|
|
1164
|
+
if (!patterns) continue;
|
|
1165
|
+
for (const pattern of patterns) {
|
|
1166
|
+
const re = new RegExp(pattern.source, pattern.flags);
|
|
1167
|
+
let match;
|
|
1168
|
+
while ((match = re.exec(input)) !== null) {
|
|
1169
|
+
const value = match[0];
|
|
1170
|
+
if (type === "credit_card" && !luhnCheck(value)) continue;
|
|
1171
|
+
if (type === "ssn") {
|
|
1172
|
+
const area = parseInt(value.substring(0, 3), 10);
|
|
1173
|
+
if (area === 0 || area === 666 || area >= 900) continue;
|
|
1174
|
+
}
|
|
1175
|
+
matches.push({
|
|
1176
|
+
type,
|
|
1177
|
+
value,
|
|
1178
|
+
start: match.index,
|
|
1179
|
+
end: match.index + value.length
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
matches.sort((a, b) => a.start - b.start);
|
|
1185
|
+
return matches;
|
|
1186
|
+
}
|
|
1187
|
+
function detectPii(input, options = {}) {
|
|
1188
|
+
return scanPii(input, options).length > 0;
|
|
1189
|
+
}
|
|
1190
|
+
function redactPii(input, options = {}) {
|
|
1191
|
+
if (!input || typeof input !== "string") return input;
|
|
1192
|
+
const matches = scanPii(input, options);
|
|
1193
|
+
if (matches.length === 0) return input;
|
|
1194
|
+
const replacement = options.replacement ?? "[REDACTED]";
|
|
1195
|
+
let result = input;
|
|
1196
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
1197
|
+
const m = matches[i];
|
|
1198
|
+
const label = options.typeLabels ? TYPE_LABELS[m.type] : replacement;
|
|
1199
|
+
result = result.substring(0, m.start) + label + result.substring(m.end);
|
|
1200
|
+
}
|
|
1201
|
+
return result;
|
|
1202
|
+
}
|
|
1203
|
+
function scanObjectPii(obj, options = {}, path = "") {
|
|
1204
|
+
const results = [];
|
|
1205
|
+
if (!obj || typeof obj !== "object") return results;
|
|
1206
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1207
|
+
const fieldPath = path ? `${path}.${key}` : key;
|
|
1208
|
+
if (typeof value === "string") {
|
|
1209
|
+
const matches = scanPii(value, options);
|
|
1210
|
+
for (const m of matches) {
|
|
1211
|
+
results.push({ ...m, field: fieldPath });
|
|
1212
|
+
}
|
|
1213
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
1214
|
+
results.push(...scanObjectPii(value, options, fieldPath));
|
|
1215
|
+
} else if (Array.isArray(value)) {
|
|
1216
|
+
for (let i = 0; i < value.length; i++) {
|
|
1217
|
+
const item = value[i];
|
|
1218
|
+
if (typeof item === "string") {
|
|
1219
|
+
const matches = scanPii(item, options);
|
|
1220
|
+
for (const m of matches) {
|
|
1221
|
+
results.push({ ...m, field: `${fieldPath}[${i}]` });
|
|
1222
|
+
}
|
|
1223
|
+
} else if (item && typeof item === "object") {
|
|
1224
|
+
results.push(...scanObjectPii(item, options, `${fieldPath}[${i}]`));
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
return results;
|
|
1230
|
+
}
|
|
1231
|
+
function redactObjectPii(obj, options = {}) {
|
|
1232
|
+
if (!obj || typeof obj !== "object") return obj;
|
|
1233
|
+
const result = {};
|
|
1234
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1235
|
+
if (typeof value === "string") {
|
|
1236
|
+
result[key] = redactPii(value, options);
|
|
1237
|
+
} else if (Array.isArray(value)) {
|
|
1238
|
+
result[key] = value.map((item) => {
|
|
1239
|
+
if (typeof item === "string") return redactPii(item, options);
|
|
1240
|
+
if (item && typeof item === "object") return redactObjectPii(item, options);
|
|
1241
|
+
return item;
|
|
1242
|
+
});
|
|
1243
|
+
} else if (value && typeof value === "object") {
|
|
1244
|
+
result[key] = redactObjectPii(value, options);
|
|
1245
|
+
} else {
|
|
1246
|
+
result[key] = value;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
return result;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
932
1252
|
// src/validation/schema.ts
|
|
933
1253
|
function validate(schema, source = "body") {
|
|
934
1254
|
return (req, res, next) => {
|
|
@@ -1270,112 +1590,606 @@ function isDangerousExtension(filename) {
|
|
|
1270
1590
|
return ext !== "" && DANGEROUS_EXTENSIONS.has(ext);
|
|
1271
1591
|
}
|
|
1272
1592
|
|
|
1273
|
-
// src/
|
|
1274
|
-
function
|
|
1593
|
+
// src/validation/url.ts
|
|
1594
|
+
function validateUrl(url, options = {}) {
|
|
1275
1595
|
const {
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1596
|
+
allowedProtocols = ["http:", "https:"],
|
|
1597
|
+
blockedHosts = [],
|
|
1598
|
+
allowedHosts = [],
|
|
1599
|
+
allowLocalhost = false,
|
|
1600
|
+
allowPrivate = false
|
|
1279
1601
|
} = options;
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
...redactKeys.map((k) => k.toLowerCase())
|
|
1283
|
-
]);
|
|
1284
|
-
function redact(obj, depth = 0) {
|
|
1285
|
-
if (depth > INPUT.MAX_RECURSION_DEPTH) return REDACTION.MAX_DEPTH;
|
|
1286
|
-
if (obj === null || obj === void 0) return obj;
|
|
1287
|
-
if (typeof obj === "string") {
|
|
1288
|
-
return redactString(obj, maxLength, redactPatterns);
|
|
1289
|
-
}
|
|
1290
|
-
if (typeof obj !== "object") return obj;
|
|
1291
|
-
if (Array.isArray(obj)) {
|
|
1292
|
-
return obj.map((item) => redact(item, depth + 1));
|
|
1293
|
-
}
|
|
1294
|
-
const result = {};
|
|
1295
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
1296
|
-
if (allRedactKeys.has(key.toLowerCase())) {
|
|
1297
|
-
result[key] = REDACTION.REPLACEMENT;
|
|
1298
|
-
} else {
|
|
1299
|
-
result[key] = redact(value, depth + 1);
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
return result;
|
|
1602
|
+
if (typeof url !== "string" || url.trim() === "") {
|
|
1603
|
+
return { safe: false, reason: "invalid URL: empty or not a string" };
|
|
1303
1604
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
};
|
|
1310
|
-
if (data !== void 0) {
|
|
1311
|
-
entry.data = redact(data);
|
|
1312
|
-
}
|
|
1313
|
-
console.log(JSON.stringify(entry));
|
|
1605
|
+
let parsed;
|
|
1606
|
+
try {
|
|
1607
|
+
parsed = new URL(url);
|
|
1608
|
+
} catch {
|
|
1609
|
+
return { safe: false, reason: "invalid URL: failed to parse" };
|
|
1314
1610
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
info: (msg, data) => log("info", msg, data),
|
|
1318
|
-
warn: (msg, data) => log("warn", msg, data),
|
|
1319
|
-
error: (msg, data) => log("error", msg, data),
|
|
1320
|
-
debug: (msg, data) => log("debug", msg, data)
|
|
1321
|
-
};
|
|
1322
|
-
}
|
|
1323
|
-
function redactString(str, maxLength, patterns) {
|
|
1324
|
-
let safe = str.replace(/[\r\n\t]/g, " ").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]/g, "");
|
|
1325
|
-
for (const pattern of patterns) {
|
|
1326
|
-
safe = safe.replace(pattern, REDACTION.REPLACEMENT);
|
|
1611
|
+
if (!allowedProtocols.includes(parsed.protocol)) {
|
|
1612
|
+
return { safe: false, reason: `disallowed protocol: ${parsed.protocol}` };
|
|
1327
1613
|
}
|
|
1328
|
-
if (
|
|
1329
|
-
|
|
1614
|
+
if (parsed.username || parsed.password) {
|
|
1615
|
+
return { safe: false, reason: "URL contains credentials" };
|
|
1330
1616
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
if (
|
|
1340
|
-
|
|
1341
|
-
if (typeof obj !== "object") return obj;
|
|
1342
|
-
if (Array.isArray(obj)) {
|
|
1343
|
-
return obj.map((item) => redact(item, depth + 1));
|
|
1617
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1618
|
+
if (allowedHosts.some((h) => hostname === h.toLowerCase())) {
|
|
1619
|
+
return { safe: true };
|
|
1620
|
+
}
|
|
1621
|
+
if (blockedHosts.some((h) => hostname === h.toLowerCase())) {
|
|
1622
|
+
return { safe: false, reason: `blocked host: ${hostname}` };
|
|
1623
|
+
}
|
|
1624
|
+
if (!allowLocalhost) {
|
|
1625
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1" || hostname === "0.0.0.0" || hostname.endsWith(".localhost")) {
|
|
1626
|
+
return { safe: false, reason: "loopback address" };
|
|
1344
1627
|
}
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
if (allKeys.has(key.toLowerCase())) {
|
|
1348
|
-
result[key] = REDACTION.REPLACEMENT;
|
|
1349
|
-
} else {
|
|
1350
|
-
result[key] = redact(value, depth + 1);
|
|
1351
|
-
}
|
|
1628
|
+
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
|
|
1629
|
+
return { safe: false, reason: "loopback address" };
|
|
1352
1630
|
}
|
|
1353
|
-
return result;
|
|
1354
1631
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
function arcis(options = {}) {
|
|
1361
|
-
const middlewares = [];
|
|
1362
|
-
const cleanupFns = [];
|
|
1363
|
-
if (options.headers !== false) {
|
|
1364
|
-
const headerOpts = typeof options.headers === "object" ? options.headers : {};
|
|
1365
|
-
middlewares.push(createHeaders(headerOpts));
|
|
1632
|
+
if (!allowLocalhost || !allowPrivate) {
|
|
1633
|
+
const decimalCheck = checkDecimalIp(hostname, allowLocalhost, allowPrivate);
|
|
1634
|
+
if (decimalCheck) {
|
|
1635
|
+
return { safe: false, reason: decimalCheck };
|
|
1636
|
+
}
|
|
1366
1637
|
}
|
|
1367
|
-
if (
|
|
1368
|
-
const
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1638
|
+
if (!allowLocalhost || !allowPrivate) {
|
|
1639
|
+
const octalCheck = checkOctalIp(hostname, allowLocalhost, allowPrivate);
|
|
1640
|
+
if (octalCheck) {
|
|
1641
|
+
return { safe: false, reason: octalCheck };
|
|
1642
|
+
}
|
|
1372
1643
|
}
|
|
1373
|
-
if (
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1644
|
+
if (!allowPrivate) {
|
|
1645
|
+
const privateCheck = checkPrivateIp(hostname);
|
|
1646
|
+
if (privateCheck) {
|
|
1647
|
+
return { safe: false, reason: privateCheck };
|
|
1648
|
+
}
|
|
1376
1649
|
}
|
|
1377
|
-
|
|
1378
|
-
|
|
1650
|
+
return { safe: true };
|
|
1651
|
+
}
|
|
1652
|
+
function isUrlSafe(url, options = {}) {
|
|
1653
|
+
return validateUrl(url, options).safe;
|
|
1654
|
+
}
|
|
1655
|
+
function checkPrivateIp(hostname) {
|
|
1656
|
+
if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
|
|
1657
|
+
return "private address (10.0.0.0/8)";
|
|
1658
|
+
}
|
|
1659
|
+
const match172 = hostname.match(/^172\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/);
|
|
1660
|
+
if (match172) {
|
|
1661
|
+
const second = parseInt(match172[1], 10);
|
|
1662
|
+
if (second >= 16 && second <= 31) {
|
|
1663
|
+
return "private address (172.16.0.0/12)";
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
|
|
1667
|
+
return "private address (192.168.0.0/16)";
|
|
1668
|
+
}
|
|
1669
|
+
if (/^169\.254\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
|
|
1670
|
+
return "link-local address (169.254.0.0/16)";
|
|
1671
|
+
}
|
|
1672
|
+
if (/^0\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
|
|
1673
|
+
return "current network address (0.0.0.0/8)";
|
|
1674
|
+
}
|
|
1675
|
+
if (hostname === "metadata.google.internal" || hostname === "metadata.internal" || hostname === "metadata.azure.internal") {
|
|
1676
|
+
return "cloud metadata endpoint";
|
|
1677
|
+
}
|
|
1678
|
+
const ipv6 = hostname.replace(/^\[|\]$/g, "");
|
|
1679
|
+
if (ipv6 === "::1" || ipv6 === "::" || ipv6.startsWith("fc") || ipv6.startsWith("fd") || ipv6.startsWith("fe80")) {
|
|
1680
|
+
return "private IPv6 address";
|
|
1681
|
+
}
|
|
1682
|
+
const mappedDotted = ipv6.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
|
|
1683
|
+
if (mappedDotted) {
|
|
1684
|
+
const mappedIp = mappedDotted[1];
|
|
1685
|
+
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(mappedIp)) {
|
|
1686
|
+
return "IPv6-mapped loopback address";
|
|
1687
|
+
}
|
|
1688
|
+
const mappedCheck = checkPrivateIp(mappedIp);
|
|
1689
|
+
if (mappedCheck) {
|
|
1690
|
+
return `IPv6-mapped ${mappedCheck}`;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
const mappedHex = ipv6.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
|
|
1694
|
+
if (mappedHex) {
|
|
1695
|
+
const hi = parseInt(mappedHex[1], 16);
|
|
1696
|
+
const lo = parseInt(mappedHex[2], 16);
|
|
1697
|
+
const a = hi >> 8 & 255;
|
|
1698
|
+
const b = hi & 255;
|
|
1699
|
+
const c = lo >> 8 & 255;
|
|
1700
|
+
const d = lo & 255;
|
|
1701
|
+
const dotted = `${a}.${b}.${c}.${d}`;
|
|
1702
|
+
if (a === 127) {
|
|
1703
|
+
return "IPv6-mapped loopback address";
|
|
1704
|
+
}
|
|
1705
|
+
const hexCheck = checkPrivateIp(dotted);
|
|
1706
|
+
if (hexCheck) {
|
|
1707
|
+
return `IPv6-mapped ${hexCheck}`;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
return null;
|
|
1711
|
+
}
|
|
1712
|
+
function checkDecimalIp(hostname, allowLocalhost, allowPrivate) {
|
|
1713
|
+
if (!/^\d+$/.test(hostname)) return null;
|
|
1714
|
+
const num = parseInt(hostname, 10);
|
|
1715
|
+
if (isNaN(num) || num < 0 || num > 4294967295) return null;
|
|
1716
|
+
const a = num >>> 24 & 255;
|
|
1717
|
+
const b = num >>> 16 & 255;
|
|
1718
|
+
const c = num >>> 8 & 255;
|
|
1719
|
+
const d = num & 255;
|
|
1720
|
+
const dotted = `${a}.${b}.${c}.${d}`;
|
|
1721
|
+
if (!allowLocalhost && a === 127) {
|
|
1722
|
+
return `loopback address (decimal IP: ${dotted})`;
|
|
1723
|
+
}
|
|
1724
|
+
if (!allowPrivate) {
|
|
1725
|
+
const privateCheck = checkPrivateIp(dotted);
|
|
1726
|
+
if (privateCheck) {
|
|
1727
|
+
return `${privateCheck} (decimal IP: ${dotted})`;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
return null;
|
|
1731
|
+
}
|
|
1732
|
+
function checkOctalIp(hostname, allowLocalhost, allowPrivate) {
|
|
1733
|
+
const parts = hostname.split(".");
|
|
1734
|
+
if (parts.length !== 4) return null;
|
|
1735
|
+
const hasAlternateNotation = parts.some((p) => /^0[0-7]+$/.test(p) || /^0x[0-9a-fA-F]+$/i.test(p));
|
|
1736
|
+
if (!hasAlternateNotation) return null;
|
|
1737
|
+
const octets = [];
|
|
1738
|
+
for (const part of parts) {
|
|
1739
|
+
let val;
|
|
1740
|
+
if (/^0x[0-9a-fA-F]+$/i.test(part)) {
|
|
1741
|
+
val = parseInt(part, 16);
|
|
1742
|
+
} else if (/^0[0-7]*$/.test(part)) {
|
|
1743
|
+
val = parseInt(part, 8);
|
|
1744
|
+
} else if (/^\d+$/.test(part)) {
|
|
1745
|
+
val = parseInt(part, 10);
|
|
1746
|
+
} else {
|
|
1747
|
+
return null;
|
|
1748
|
+
}
|
|
1749
|
+
if (val < 0 || val > 255) return null;
|
|
1750
|
+
octets.push(val);
|
|
1751
|
+
}
|
|
1752
|
+
const dotted = octets.join(".");
|
|
1753
|
+
if (!allowLocalhost && octets[0] === 127) {
|
|
1754
|
+
return `loopback address (octal IP: ${dotted})`;
|
|
1755
|
+
}
|
|
1756
|
+
if (!allowPrivate) {
|
|
1757
|
+
const privateCheck = checkPrivateIp(dotted);
|
|
1758
|
+
if (privateCheck) {
|
|
1759
|
+
return `${privateCheck} (octal IP: ${dotted})`;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return null;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// src/validation/redirect.ts
|
|
1766
|
+
var DANGEROUS_PROTOCOLS = /^(javascript|data|vbscript|blob):/i;
|
|
1767
|
+
var CONTROL_CHARS = /[\t\n\r]/g;
|
|
1768
|
+
function validateRedirect(url, options = {}) {
|
|
1769
|
+
const {
|
|
1770
|
+
allowedHosts = [],
|
|
1771
|
+
allowProtocolRelative = false,
|
|
1772
|
+
allowedProtocols = ["http:", "https:"]
|
|
1773
|
+
} = options;
|
|
1774
|
+
if (typeof url !== "string" || url.trim() === "") {
|
|
1775
|
+
return { safe: false, reason: "invalid redirect: empty or not a string" };
|
|
1776
|
+
}
|
|
1777
|
+
const cleaned = url.replace(CONTROL_CHARS, "");
|
|
1778
|
+
if (DANGEROUS_PROTOCOLS.test(cleaned)) {
|
|
1779
|
+
const proto = cleaned.match(DANGEROUS_PROTOCOLS);
|
|
1780
|
+
return { safe: false, reason: `dangerous protocol: ${proto[0]}` };
|
|
1781
|
+
}
|
|
1782
|
+
if (cleaned.startsWith("\\")) {
|
|
1783
|
+
return { safe: false, reason: "backslash-prefixed URL (browser treats as protocol-relative)" };
|
|
1784
|
+
}
|
|
1785
|
+
if (cleaned.startsWith("//")) {
|
|
1786
|
+
if (!allowProtocolRelative) {
|
|
1787
|
+
const host2 = extractHost(cleaned);
|
|
1788
|
+
if (host2 && allowedHosts.some((h) => host2 === h.toLowerCase())) {
|
|
1789
|
+
return { safe: true };
|
|
1790
|
+
}
|
|
1791
|
+
return { safe: false, reason: "protocol-relative URL not in allowed hosts" };
|
|
1792
|
+
}
|
|
1793
|
+
const host = extractHost(cleaned);
|
|
1794
|
+
if (host && allowedHosts.length > 0 && !allowedHosts.some((h) => host === h.toLowerCase())) {
|
|
1795
|
+
return { safe: false, reason: "protocol-relative URL not in allowed hosts" };
|
|
1796
|
+
}
|
|
1797
|
+
return { safe: true };
|
|
1798
|
+
}
|
|
1799
|
+
let parsed;
|
|
1800
|
+
try {
|
|
1801
|
+
parsed = new URL(cleaned);
|
|
1802
|
+
} catch {
|
|
1803
|
+
return { safe: true };
|
|
1804
|
+
}
|
|
1805
|
+
if (!allowedProtocols.includes(parsed.protocol)) {
|
|
1806
|
+
return { safe: false, reason: `disallowed protocol: ${parsed.protocol}` };
|
|
1807
|
+
}
|
|
1808
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1809
|
+
if (allowedHosts.length === 0) {
|
|
1810
|
+
return { safe: false, reason: "absolute URL not in allowed hosts" };
|
|
1811
|
+
}
|
|
1812
|
+
if (!allowedHosts.some((h) => hostname === h.toLowerCase())) {
|
|
1813
|
+
return { safe: false, reason: `host not allowed: ${hostname}` };
|
|
1814
|
+
}
|
|
1815
|
+
return { safe: true };
|
|
1816
|
+
}
|
|
1817
|
+
function isRedirectSafe(url, options = {}) {
|
|
1818
|
+
return validateRedirect(url, options).safe;
|
|
1819
|
+
}
|
|
1820
|
+
function extractHost(url) {
|
|
1821
|
+
const match = url.match(/^\/\/([^/:?#]+)/);
|
|
1822
|
+
return match ? match[1].toLowerCase() : null;
|
|
1823
|
+
}
|
|
1824
|
+
var MAX_EMAIL_LENGTH = 254;
|
|
1825
|
+
var MAX_LOCAL_LENGTH = 64;
|
|
1826
|
+
var MAX_DOMAIN_LENGTH = 255;
|
|
1827
|
+
var EMAIL_SYNTAX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/;
|
|
1828
|
+
var FREE_PROVIDERS = /* @__PURE__ */ new Set([
|
|
1829
|
+
"gmail.com",
|
|
1830
|
+
"yahoo.com",
|
|
1831
|
+
"hotmail.com",
|
|
1832
|
+
"outlook.com",
|
|
1833
|
+
"aol.com",
|
|
1834
|
+
"protonmail.com",
|
|
1835
|
+
"proton.me",
|
|
1836
|
+
"icloud.com",
|
|
1837
|
+
"mail.com",
|
|
1838
|
+
"zoho.com",
|
|
1839
|
+
"yandex.com",
|
|
1840
|
+
"gmx.com",
|
|
1841
|
+
"gmx.net",
|
|
1842
|
+
"live.com",
|
|
1843
|
+
"msn.com",
|
|
1844
|
+
"me.com",
|
|
1845
|
+
"mac.com",
|
|
1846
|
+
"fastmail.com",
|
|
1847
|
+
"tutanota.com",
|
|
1848
|
+
"hey.com"
|
|
1849
|
+
]);
|
|
1850
|
+
var DISPOSABLE_DOMAINS = /* @__PURE__ */ new Set([
|
|
1851
|
+
// Popular disposable services
|
|
1852
|
+
"guerrillamail.com",
|
|
1853
|
+
"guerrillamail.net",
|
|
1854
|
+
"guerrillamail.org",
|
|
1855
|
+
"tempmail.com",
|
|
1856
|
+
"temp-mail.org",
|
|
1857
|
+
"temp-mail.io",
|
|
1858
|
+
"throwaway.email",
|
|
1859
|
+
"throwaway.com",
|
|
1860
|
+
"mailinator.com",
|
|
1861
|
+
"mailinator.net",
|
|
1862
|
+
"yopmail.com",
|
|
1863
|
+
"yopmail.fr",
|
|
1864
|
+
"yopmail.net",
|
|
1865
|
+
"sharklasers.com",
|
|
1866
|
+
"grr.la",
|
|
1867
|
+
"guerrillamail.info",
|
|
1868
|
+
"guerrillamail.biz",
|
|
1869
|
+
"guerrillamail.de",
|
|
1870
|
+
"trashmail.com",
|
|
1871
|
+
"trashmail.me",
|
|
1872
|
+
"trashmail.net",
|
|
1873
|
+
"dispostable.com",
|
|
1874
|
+
"maildrop.cc",
|
|
1875
|
+
"mailnesia.com",
|
|
1876
|
+
"tempail.com",
|
|
1877
|
+
"mohmal.com",
|
|
1878
|
+
"getnada.com",
|
|
1879
|
+
"emailondeck.com",
|
|
1880
|
+
"discard.email",
|
|
1881
|
+
"fakeinbox.com",
|
|
1882
|
+
"mailcatch.com",
|
|
1883
|
+
"mintemail.com",
|
|
1884
|
+
"tempr.email",
|
|
1885
|
+
"tempinbox.com",
|
|
1886
|
+
"burnermail.io",
|
|
1887
|
+
"mailsac.com",
|
|
1888
|
+
"harakirimail.com",
|
|
1889
|
+
"tempmailo.com",
|
|
1890
|
+
"emailfake.com",
|
|
1891
|
+
"crazymailing.com",
|
|
1892
|
+
"armyspy.com",
|
|
1893
|
+
"dayrep.com",
|
|
1894
|
+
"einrot.com",
|
|
1895
|
+
"fleckens.hu",
|
|
1896
|
+
"gustr.com",
|
|
1897
|
+
"jourrapide.com",
|
|
1898
|
+
"rhyta.com",
|
|
1899
|
+
"superrito.com",
|
|
1900
|
+
"teleworm.us",
|
|
1901
|
+
"10minutemail.com",
|
|
1902
|
+
"10minutemail.net",
|
|
1903
|
+
"minutemail.com",
|
|
1904
|
+
"tempsky.com",
|
|
1905
|
+
"spamgourmet.com",
|
|
1906
|
+
"mytrashmail.com",
|
|
1907
|
+
"mailexpire.com",
|
|
1908
|
+
"safetymail.info",
|
|
1909
|
+
"filzmail.com",
|
|
1910
|
+
"trashymail.com",
|
|
1911
|
+
"sharkmail.com",
|
|
1912
|
+
"jetable.org",
|
|
1913
|
+
"nospam.ze.tc",
|
|
1914
|
+
"trash-me.com",
|
|
1915
|
+
"dodgit.com",
|
|
1916
|
+
"mailmoat.com",
|
|
1917
|
+
"spamfree24.org",
|
|
1918
|
+
"incognitomail.org",
|
|
1919
|
+
"tempomail.fr",
|
|
1920
|
+
"ephemail.net",
|
|
1921
|
+
"hidemail.de",
|
|
1922
|
+
"spaml.de",
|
|
1923
|
+
"uggsrock.com",
|
|
1924
|
+
"binkmail.com",
|
|
1925
|
+
"suremail.info",
|
|
1926
|
+
"bugmenot.com"
|
|
1927
|
+
]);
|
|
1928
|
+
var DOMAIN_TYPOS = {
|
|
1929
|
+
"gmial.com": "gmail.com",
|
|
1930
|
+
"gmaill.com": "gmail.com",
|
|
1931
|
+
"gmai.com": "gmail.com",
|
|
1932
|
+
"gamil.com": "gmail.com",
|
|
1933
|
+
"gnail.com": "gmail.com",
|
|
1934
|
+
"gmal.com": "gmail.com",
|
|
1935
|
+
"gmil.com": "gmail.com",
|
|
1936
|
+
"gmail.co": "gmail.com",
|
|
1937
|
+
"gmail.cm": "gmail.com",
|
|
1938
|
+
"gmail.om": "gmail.com",
|
|
1939
|
+
"gmail.con": "gmail.com",
|
|
1940
|
+
"gmail.cim": "gmail.com",
|
|
1941
|
+
"gmail.comm": "gmail.com",
|
|
1942
|
+
"yahooo.com": "yahoo.com",
|
|
1943
|
+
"yaho.com": "yahoo.com",
|
|
1944
|
+
"yahoo.co": "yahoo.com",
|
|
1945
|
+
"yahoo.cm": "yahoo.com",
|
|
1946
|
+
"yahoo.con": "yahoo.com",
|
|
1947
|
+
"yahho.com": "yahoo.com",
|
|
1948
|
+
"hotmial.com": "hotmail.com",
|
|
1949
|
+
"hotmal.com": "hotmail.com",
|
|
1950
|
+
"hotmai.com": "hotmail.com",
|
|
1951
|
+
"hotmil.com": "hotmail.com",
|
|
1952
|
+
"hotmail.co": "hotmail.com",
|
|
1953
|
+
"hotmail.cm": "hotmail.com",
|
|
1954
|
+
"hotmail.con": "hotmail.com",
|
|
1955
|
+
"outlok.com": "outlook.com",
|
|
1956
|
+
"outloo.com": "outlook.com",
|
|
1957
|
+
"outlook.co": "outlook.com",
|
|
1958
|
+
"outlook.cm": "outlook.com",
|
|
1959
|
+
"protonmal.com": "protonmail.com",
|
|
1960
|
+
"protonmail.co": "protonmail.com",
|
|
1961
|
+
"icloud.co": "icloud.com",
|
|
1962
|
+
"icloud.cm": "icloud.com",
|
|
1963
|
+
"icoud.com": "icloud.com"
|
|
1964
|
+
};
|
|
1965
|
+
function invalidResult(reason, email) {
|
|
1966
|
+
return {
|
|
1967
|
+
valid: false,
|
|
1968
|
+
reason,
|
|
1969
|
+
suggestion: null,
|
|
1970
|
+
isFree: false,
|
|
1971
|
+
isDisposable: false,
|
|
1972
|
+
normalized: email
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
function validateEmail(email, options = {}) {
|
|
1976
|
+
const {
|
|
1977
|
+
checkDisposable = true,
|
|
1978
|
+
suggestTypoFix = true,
|
|
1979
|
+
blockedDomains = [],
|
|
1980
|
+
allowedDomains = []
|
|
1981
|
+
} = options;
|
|
1982
|
+
const normalized = email.trim().toLowerCase();
|
|
1983
|
+
if (!normalized || normalized.length > MAX_EMAIL_LENGTH) {
|
|
1984
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1985
|
+
}
|
|
1986
|
+
const atIndex = normalized.lastIndexOf("@");
|
|
1987
|
+
if (atIndex === -1) {
|
|
1988
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1989
|
+
}
|
|
1990
|
+
const localPart = normalized.slice(0, atIndex);
|
|
1991
|
+
const domain = normalized.slice(atIndex + 1);
|
|
1992
|
+
if (localPart.length === 0 || localPart.length > MAX_LOCAL_LENGTH) {
|
|
1993
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1994
|
+
}
|
|
1995
|
+
if (domain.length === 0 || domain.length > MAX_DOMAIN_LENGTH) {
|
|
1996
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1997
|
+
}
|
|
1998
|
+
if (localPart.includes("..")) {
|
|
1999
|
+
return invalidResult("invalid_syntax", normalized);
|
|
2000
|
+
}
|
|
2001
|
+
if (localPart.startsWith(".") || localPart.endsWith(".")) {
|
|
2002
|
+
return invalidResult("invalid_syntax", normalized);
|
|
2003
|
+
}
|
|
2004
|
+
if (!EMAIL_SYNTAX.test(normalized)) {
|
|
2005
|
+
return invalidResult("invalid_syntax", normalized);
|
|
2006
|
+
}
|
|
2007
|
+
const allowedSet = new Set(allowedDomains.map((d) => d.toLowerCase()));
|
|
2008
|
+
if (allowedSet.has(domain)) {
|
|
2009
|
+
return {
|
|
2010
|
+
valid: true,
|
|
2011
|
+
reason: "valid",
|
|
2012
|
+
suggestion: null,
|
|
2013
|
+
isFree: FREE_PROVIDERS.has(domain),
|
|
2014
|
+
isDisposable: false,
|
|
2015
|
+
normalized
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
const blockedSet = new Set(blockedDomains.map((d) => d.toLowerCase()));
|
|
2019
|
+
if (blockedSet.has(domain)) {
|
|
2020
|
+
return invalidResult("blocked", normalized);
|
|
2021
|
+
}
|
|
2022
|
+
const isDisposable = DISPOSABLE_DOMAINS.has(domain);
|
|
2023
|
+
if (checkDisposable && isDisposable) {
|
|
2024
|
+
return {
|
|
2025
|
+
valid: false,
|
|
2026
|
+
reason: "disposable",
|
|
2027
|
+
suggestion: null,
|
|
2028
|
+
isFree: false,
|
|
2029
|
+
isDisposable: true,
|
|
2030
|
+
normalized
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
const isFree = FREE_PROVIDERS.has(domain);
|
|
2034
|
+
if (suggestTypoFix && DOMAIN_TYPOS[domain]) {
|
|
2035
|
+
const corrected = `${localPart}@${DOMAIN_TYPOS[domain]}`;
|
|
2036
|
+
return {
|
|
2037
|
+
valid: true,
|
|
2038
|
+
reason: "typo",
|
|
2039
|
+
suggestion: corrected,
|
|
2040
|
+
isFree: FREE_PROVIDERS.has(DOMAIN_TYPOS[domain]),
|
|
2041
|
+
isDisposable: false,
|
|
2042
|
+
normalized
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
return {
|
|
2046
|
+
valid: true,
|
|
2047
|
+
reason: "valid",
|
|
2048
|
+
suggestion: null,
|
|
2049
|
+
isFree,
|
|
2050
|
+
isDisposable,
|
|
2051
|
+
normalized
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
async function verifyEmailMx(email) {
|
|
2055
|
+
if (!isValidEmailSyntax(email)) return false;
|
|
2056
|
+
const atIndex = email.lastIndexOf("@");
|
|
2057
|
+
const domain = email.slice(atIndex + 1).trim().toLowerCase();
|
|
2058
|
+
if (!domain) return false;
|
|
2059
|
+
try {
|
|
2060
|
+
const records = await dns.promises.resolveMx(domain);
|
|
2061
|
+
return records.length > 0;
|
|
2062
|
+
} catch {
|
|
2063
|
+
return false;
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
function isValidEmailSyntax(email) {
|
|
2067
|
+
const normalized = email.trim().toLowerCase();
|
|
2068
|
+
if (!normalized || normalized.length > MAX_EMAIL_LENGTH) return false;
|
|
2069
|
+
const atIndex = normalized.lastIndexOf("@");
|
|
2070
|
+
if (atIndex === -1) return false;
|
|
2071
|
+
const localPart = normalized.slice(0, atIndex);
|
|
2072
|
+
if (localPart.includes("..") || localPart.startsWith(".") || localPart.endsWith(".")) return false;
|
|
2073
|
+
return EMAIL_SYNTAX.test(normalized);
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// src/logging/redactor.ts
|
|
2077
|
+
var LOG_LEVELS = {
|
|
2078
|
+
debug: 0,
|
|
2079
|
+
info: 1,
|
|
2080
|
+
warn: 2,
|
|
2081
|
+
error: 3,
|
|
2082
|
+
silent: 4
|
|
2083
|
+
};
|
|
2084
|
+
function createSafeLogger(options = {}) {
|
|
2085
|
+
const {
|
|
2086
|
+
redactKeys = [],
|
|
2087
|
+
maxLength = REDACTION.DEFAULT_MAX_LENGTH,
|
|
2088
|
+
redactPatterns = [],
|
|
2089
|
+
level: minLevel = "debug"
|
|
2090
|
+
} = options;
|
|
2091
|
+
const minLevelNum = LOG_LEVELS[minLevel] ?? 0;
|
|
2092
|
+
const allRedactKeys = /* @__PURE__ */ new Set([
|
|
2093
|
+
...Array.from(REDACTION.SENSITIVE_KEYS),
|
|
2094
|
+
...redactKeys.map((k) => k.toLowerCase())
|
|
2095
|
+
]);
|
|
2096
|
+
function redact(obj, depth = 0) {
|
|
2097
|
+
if (depth > INPUT.MAX_RECURSION_DEPTH) return REDACTION.MAX_DEPTH;
|
|
2098
|
+
if (obj === null || obj === void 0) return obj;
|
|
2099
|
+
if (typeof obj === "string") {
|
|
2100
|
+
return redactString(obj, maxLength, redactPatterns);
|
|
2101
|
+
}
|
|
2102
|
+
if (typeof obj !== "object") return obj;
|
|
2103
|
+
if (Array.isArray(obj)) {
|
|
2104
|
+
return obj.map((item) => redact(item, depth + 1));
|
|
2105
|
+
}
|
|
2106
|
+
const result = {};
|
|
2107
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2108
|
+
if (allRedactKeys.has(key.toLowerCase())) {
|
|
2109
|
+
result[key] = REDACTION.REPLACEMENT;
|
|
2110
|
+
} else {
|
|
2111
|
+
result[key] = redact(value, depth + 1);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
return result;
|
|
2115
|
+
}
|
|
2116
|
+
function log(level, message, data) {
|
|
2117
|
+
const levelNum = LOG_LEVELS[level] ?? 0;
|
|
2118
|
+
if (levelNum < minLevelNum) return;
|
|
2119
|
+
const entry = {
|
|
2120
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2121
|
+
level,
|
|
2122
|
+
message: redactString(message, maxLength, redactPatterns)
|
|
2123
|
+
};
|
|
2124
|
+
if (data !== void 0) {
|
|
2125
|
+
entry.data = redact(data);
|
|
2126
|
+
}
|
|
2127
|
+
console.log(JSON.stringify(entry));
|
|
2128
|
+
}
|
|
2129
|
+
return {
|
|
2130
|
+
log,
|
|
2131
|
+
info: (msg, data) => log("info", msg, data),
|
|
2132
|
+
warn: (msg, data) => log("warn", msg, data),
|
|
2133
|
+
error: (msg, data) => log("error", msg, data),
|
|
2134
|
+
debug: (msg, data) => log("debug", msg, data)
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
function redactString(str, maxLength, patterns) {
|
|
2138
|
+
let safe = str.replace(/[\r\n\t]/g, " ").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]/g, "");
|
|
2139
|
+
for (const pattern of patterns) {
|
|
2140
|
+
safe = safe.replace(pattern, REDACTION.REPLACEMENT);
|
|
2141
|
+
}
|
|
2142
|
+
if (safe.length > maxLength) {
|
|
2143
|
+
safe = safe.substring(0, maxLength) + `...${REDACTION.TRUNCATED}`;
|
|
2144
|
+
}
|
|
2145
|
+
return safe;
|
|
2146
|
+
}
|
|
2147
|
+
function createRedactor(sensitiveKeys = []) {
|
|
2148
|
+
const allKeys = /* @__PURE__ */ new Set([
|
|
2149
|
+
...Array.from(REDACTION.SENSITIVE_KEYS),
|
|
2150
|
+
...sensitiveKeys.map((k) => k.toLowerCase())
|
|
2151
|
+
]);
|
|
2152
|
+
function redact(obj, depth = 0) {
|
|
2153
|
+
if (depth > INPUT.MAX_RECURSION_DEPTH) return REDACTION.MAX_DEPTH;
|
|
2154
|
+
if (obj === null || obj === void 0) return obj;
|
|
2155
|
+
if (typeof obj !== "object") return obj;
|
|
2156
|
+
if (Array.isArray(obj)) {
|
|
2157
|
+
return obj.map((item) => redact(item, depth + 1));
|
|
2158
|
+
}
|
|
2159
|
+
const result = {};
|
|
2160
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2161
|
+
if (allKeys.has(key.toLowerCase())) {
|
|
2162
|
+
result[key] = REDACTION.REPLACEMENT;
|
|
2163
|
+
} else {
|
|
2164
|
+
result[key] = redact(value, depth + 1);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
return result;
|
|
2168
|
+
}
|
|
2169
|
+
return redact;
|
|
2170
|
+
}
|
|
2171
|
+
var safeLog = createSafeLogger;
|
|
2172
|
+
|
|
2173
|
+
// src/middleware/main.ts
|
|
2174
|
+
function arcis(options = {}) {
|
|
2175
|
+
const middlewares = [];
|
|
2176
|
+
const cleanupFns = [];
|
|
2177
|
+
if (options.headers !== false) {
|
|
2178
|
+
const headerOpts = typeof options.headers === "object" ? options.headers : {};
|
|
2179
|
+
middlewares.push(createHeaders(headerOpts));
|
|
2180
|
+
}
|
|
2181
|
+
if (options.rateLimit !== false) {
|
|
2182
|
+
const rateLimitOpts = typeof options.rateLimit === "object" ? options.rateLimit : {};
|
|
2183
|
+
const rateLimiter = createRateLimiter(rateLimitOpts);
|
|
2184
|
+
middlewares.push(rateLimiter);
|
|
2185
|
+
cleanupFns.push(() => rateLimiter.close());
|
|
2186
|
+
}
|
|
2187
|
+
if (options.sanitize !== false) {
|
|
2188
|
+
const sanitizeOpts = typeof options.sanitize === "object" ? options.sanitize : {};
|
|
2189
|
+
middlewares.push(createSanitizer(sanitizeOpts));
|
|
2190
|
+
}
|
|
2191
|
+
const result = middlewares;
|
|
2192
|
+
result.close = () => {
|
|
1379
2193
|
for (const fn of cleanupFns) {
|
|
1380
2194
|
fn();
|
|
1381
2195
|
}
|
|
@@ -1391,6 +2205,201 @@ arcisWithMethods.logger = createSafeLogger;
|
|
|
1391
2205
|
arcisWithMethods.errorHandler = createErrorHandler;
|
|
1392
2206
|
var main_default = arcisWithMethods;
|
|
1393
2207
|
|
|
2208
|
+
// src/utils/duration.ts
|
|
2209
|
+
var MAX_DURATION_MS = 4294967295;
|
|
2210
|
+
var DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/i;
|
|
2211
|
+
var UNIT_TO_MS = {
|
|
2212
|
+
ms: 1,
|
|
2213
|
+
s: 1e3,
|
|
2214
|
+
m: 6e4,
|
|
2215
|
+
h: 36e5,
|
|
2216
|
+
d: 864e5
|
|
2217
|
+
};
|
|
2218
|
+
function parseDuration(value) {
|
|
2219
|
+
if (typeof value === "number") {
|
|
2220
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
2221
|
+
throw new Error(`Invalid duration: ${value}. Must be a non-negative finite number.`);
|
|
2222
|
+
}
|
|
2223
|
+
return Math.min(Math.floor(value), MAX_DURATION_MS);
|
|
2224
|
+
}
|
|
2225
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2226
|
+
throw new Error(`Invalid duration: "${value}". Expected a duration string (e.g. "5m", "2h") or number.`);
|
|
2227
|
+
}
|
|
2228
|
+
const match = value.trim().match(DURATION_REGEX);
|
|
2229
|
+
if (!match) {
|
|
2230
|
+
throw new Error(
|
|
2231
|
+
`Invalid duration: "${value}". Expected format: <number><unit> where unit is ms, s, m, h, or d.`
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
const amount = parseFloat(match[1]);
|
|
2235
|
+
const unit = match[2].toLowerCase();
|
|
2236
|
+
const ms = Math.floor(amount * UNIT_TO_MS[unit]);
|
|
2237
|
+
if (ms < 0 || ms > MAX_DURATION_MS) {
|
|
2238
|
+
throw new Error(`Duration "${value}" exceeds maximum allowed (${MAX_DURATION_MS}ms / ~49.7 days).`);
|
|
2239
|
+
}
|
|
2240
|
+
return ms;
|
|
2241
|
+
}
|
|
2242
|
+
function formatDuration(ms) {
|
|
2243
|
+
if (!Number.isFinite(ms) || ms < 0) return "0ms";
|
|
2244
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
2245
|
+
const days = Math.floor(ms / 864e5);
|
|
2246
|
+
const hours = Math.floor(ms % 864e5 / 36e5);
|
|
2247
|
+
const minutes = Math.floor(ms % 36e5 / 6e4);
|
|
2248
|
+
const seconds = Math.floor(ms % 6e4 / 1e3);
|
|
2249
|
+
const parts = [];
|
|
2250
|
+
if (days > 0) parts.push(`${days}d`);
|
|
2251
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
2252
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
2253
|
+
if (seconds > 0) parts.push(`${seconds}s`);
|
|
2254
|
+
return parts.join(" ") || "0ms";
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
// src/middleware/rate-limit-sliding.ts
|
|
2258
|
+
function createSlidingWindowLimiter(options = {}) {
|
|
2259
|
+
const {
|
|
2260
|
+
max = RATE_LIMIT.DEFAULT_MAX_REQUESTS,
|
|
2261
|
+
window: windowOpt = RATE_LIMIT.DEFAULT_WINDOW_MS,
|
|
2262
|
+
message = RATE_LIMIT.DEFAULT_MESSAGE,
|
|
2263
|
+
statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
|
|
2264
|
+
keyGenerator = (req) => req.ip ?? req.socket?.remoteAddress ?? "unknown",
|
|
2265
|
+
skip
|
|
2266
|
+
} = options;
|
|
2267
|
+
const windowMs = parseDuration(windowOpt);
|
|
2268
|
+
const currentWindows = /* @__PURE__ */ Object.create(null);
|
|
2269
|
+
const previousWindows = /* @__PURE__ */ Object.create(null);
|
|
2270
|
+
const cleanupInterval = setInterval(() => {
|
|
2271
|
+
const now = Date.now();
|
|
2272
|
+
const cutoff = now - windowMs * 2;
|
|
2273
|
+
for (const key of Object.keys(previousWindows)) {
|
|
2274
|
+
if (previousWindows[key].startTime < cutoff) {
|
|
2275
|
+
delete previousWindows[key];
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
for (const key of Object.keys(currentWindows)) {
|
|
2279
|
+
if (currentWindows[key].startTime < cutoff) {
|
|
2280
|
+
delete currentWindows[key];
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}, windowMs);
|
|
2284
|
+
if (typeof cleanupInterval.unref === "function") {
|
|
2285
|
+
cleanupInterval.unref();
|
|
2286
|
+
}
|
|
2287
|
+
const handler = (req, res, next) => {
|
|
2288
|
+
try {
|
|
2289
|
+
if (skip?.(req)) return next();
|
|
2290
|
+
const key = keyGenerator(req);
|
|
2291
|
+
const now = Date.now();
|
|
2292
|
+
const windowStart = Math.floor(now / windowMs) * windowMs;
|
|
2293
|
+
if (!currentWindows[key] || currentWindows[key].startTime < windowStart) {
|
|
2294
|
+
if (currentWindows[key]) {
|
|
2295
|
+
previousWindows[key] = currentWindows[key];
|
|
2296
|
+
}
|
|
2297
|
+
currentWindows[key] = { count: 0, startTime: windowStart };
|
|
2298
|
+
}
|
|
2299
|
+
const elapsed = now - windowStart;
|
|
2300
|
+
const weight = Math.max(0, (windowMs - elapsed) / windowMs);
|
|
2301
|
+
const prevCount = previousWindows[key]?.count ?? 0;
|
|
2302
|
+
const estimatedCount = prevCount * weight + currentWindows[key].count + 1;
|
|
2303
|
+
const remaining = Math.max(0, Math.floor(max - estimatedCount));
|
|
2304
|
+
const resetMs = windowStart + windowMs - now;
|
|
2305
|
+
const resetSeconds = Math.max(1, Math.ceil(resetMs / 1e3));
|
|
2306
|
+
res.setHeader("X-RateLimit-Limit", max.toString());
|
|
2307
|
+
res.setHeader("X-RateLimit-Remaining", remaining.toString());
|
|
2308
|
+
res.setHeader("X-RateLimit-Reset", resetSeconds.toString());
|
|
2309
|
+
res.setHeader("X-RateLimit-Policy", `${max};w=${Math.floor(windowMs / 1e3)}`);
|
|
2310
|
+
if (estimatedCount > max) {
|
|
2311
|
+
res.setHeader("Retry-After", resetSeconds.toString());
|
|
2312
|
+
res.status(statusCode).json({
|
|
2313
|
+
error: message,
|
|
2314
|
+
retryAfter: resetSeconds
|
|
2315
|
+
});
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
currentWindows[key].count++;
|
|
2319
|
+
next();
|
|
2320
|
+
} catch (error) {
|
|
2321
|
+
console.error("[arcis] Sliding window rate limiter error:", error);
|
|
2322
|
+
next();
|
|
2323
|
+
}
|
|
2324
|
+
};
|
|
2325
|
+
const middleware = handler;
|
|
2326
|
+
middleware.close = () => {
|
|
2327
|
+
clearInterval(cleanupInterval);
|
|
2328
|
+
};
|
|
2329
|
+
return middleware;
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// src/middleware/rate-limit-token.ts
|
|
2333
|
+
function createTokenBucketLimiter(options = {}) {
|
|
2334
|
+
const {
|
|
2335
|
+
capacity = 100,
|
|
2336
|
+
refillRate = 10,
|
|
2337
|
+
cost = 1,
|
|
2338
|
+
message = RATE_LIMIT.DEFAULT_MESSAGE,
|
|
2339
|
+
statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
|
|
2340
|
+
keyGenerator = (req) => req.ip ?? req.socket?.remoteAddress ?? "unknown",
|
|
2341
|
+
skip
|
|
2342
|
+
} = options;
|
|
2343
|
+
if (capacity < 1) throw new RangeError(`Token bucket capacity must be >= 1, got ${capacity}`);
|
|
2344
|
+
if (refillRate <= 0) throw new RangeError(`Token bucket refillRate must be > 0, got ${refillRate}`);
|
|
2345
|
+
if (cost < 1) throw new RangeError(`Token bucket cost must be >= 1, got ${cost}`);
|
|
2346
|
+
if (cost > capacity) throw new RangeError(`Token bucket cost (${cost}) must be <= capacity (${capacity}), otherwise all requests are permanently denied`);
|
|
2347
|
+
const buckets = /* @__PURE__ */ Object.create(null);
|
|
2348
|
+
const cleanupInterval = setInterval(() => {
|
|
2349
|
+
const now = Date.now();
|
|
2350
|
+
const staleThreshold = capacity / refillRate * 1e3 * 2;
|
|
2351
|
+
for (const key of Object.keys(buckets)) {
|
|
2352
|
+
if (now - buckets[key].lastRefill > staleThreshold) {
|
|
2353
|
+
delete buckets[key];
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}, 6e4);
|
|
2357
|
+
if (typeof cleanupInterval.unref === "function") {
|
|
2358
|
+
cleanupInterval.unref();
|
|
2359
|
+
}
|
|
2360
|
+
function refillBucket(bucket, now) {
|
|
2361
|
+
const elapsed = (now - bucket.lastRefill) / 1e3;
|
|
2362
|
+
const tokensToAdd = elapsed * refillRate;
|
|
2363
|
+
bucket.tokens = Math.min(capacity, bucket.tokens + tokensToAdd);
|
|
2364
|
+
bucket.lastRefill = now;
|
|
2365
|
+
}
|
|
2366
|
+
const handler = (req, res, next) => {
|
|
2367
|
+
try {
|
|
2368
|
+
if (skip?.(req)) return next();
|
|
2369
|
+
const key = keyGenerator(req);
|
|
2370
|
+
const now = Date.now();
|
|
2371
|
+
if (!buckets[key]) {
|
|
2372
|
+
buckets[key] = { tokens: capacity, lastRefill: now };
|
|
2373
|
+
}
|
|
2374
|
+
const bucket = buckets[key];
|
|
2375
|
+
refillBucket(bucket, now);
|
|
2376
|
+
const retryAfterSec = bucket.tokens < cost ? Math.ceil((cost - bucket.tokens) / refillRate) : 0;
|
|
2377
|
+
res.setHeader("X-RateLimit-Limit", capacity.toString());
|
|
2378
|
+
res.setHeader("X-RateLimit-Remaining", Math.floor(Math.max(0, bucket.tokens - cost)).toString());
|
|
2379
|
+
res.setHeader("X-RateLimit-Policy", `${capacity};w=${Math.floor(capacity / refillRate)};burst=${capacity}`);
|
|
2380
|
+
if (bucket.tokens < cost) {
|
|
2381
|
+
res.setHeader("Retry-After", retryAfterSec.toString());
|
|
2382
|
+
res.setHeader("X-RateLimit-Reset", retryAfterSec.toString());
|
|
2383
|
+
res.status(statusCode).json({
|
|
2384
|
+
error: message,
|
|
2385
|
+
retryAfter: retryAfterSec
|
|
2386
|
+
});
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
bucket.tokens -= cost;
|
|
2390
|
+
next();
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
console.error("[arcis] Token bucket rate limiter error:", error);
|
|
2393
|
+
next();
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
const middleware = handler;
|
|
2397
|
+
middleware.close = () => {
|
|
2398
|
+
clearInterval(cleanupInterval);
|
|
2399
|
+
};
|
|
2400
|
+
return middleware;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
1394
2403
|
// src/middleware/cors.ts
|
|
1395
2404
|
var DEFAULT_METHODS = ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"];
|
|
1396
2405
|
var DEFAULT_HEADERS = ["Content-Type", "Authorization"];
|
|
@@ -1521,144 +2530,438 @@ function secureCookieDefaults(options = {}) {
|
|
|
1521
2530
|
}
|
|
1522
2531
|
var createSecureCookies = secureCookieDefaults;
|
|
1523
2532
|
|
|
1524
|
-
// src/
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
2533
|
+
// src/middleware/bot-detection.ts
|
|
2534
|
+
var BOT_PATTERNS = [
|
|
2535
|
+
// --- SEARCH ENGINES (specific variants before generic) ---
|
|
2536
|
+
{ pattern: /Googlebot-Image/i, name: "Googlebot-Image", category: "SEARCH_ENGINE" },
|
|
2537
|
+
{ pattern: /Googlebot-Video/i, name: "Googlebot-Video", category: "SEARCH_ENGINE" },
|
|
2538
|
+
{ pattern: /Googlebot-News/i, name: "Googlebot-News", category: "SEARCH_ENGINE" },
|
|
2539
|
+
{ pattern: /Googlebot/i, name: "Googlebot", category: "SEARCH_ENGINE" },
|
|
2540
|
+
{ pattern: /AdsBot-Google/i, name: "AdsBot-Google", category: "SEARCH_ENGINE" },
|
|
2541
|
+
{ pattern: /Mediapartners-Google/i, name: "Mediapartners-Google", category: "SEARCH_ENGINE" },
|
|
2542
|
+
{ pattern: /Bingbot/i, name: "Bingbot", category: "SEARCH_ENGINE" },
|
|
2543
|
+
{ pattern: /msnbot/i, name: "msnbot", category: "SEARCH_ENGINE" },
|
|
2544
|
+
{ pattern: /Slurp/i, name: "Yahoo Slurp", category: "SEARCH_ENGINE" },
|
|
2545
|
+
{ pattern: /DuckDuckBot/i, name: "DuckDuckBot", category: "SEARCH_ENGINE" },
|
|
2546
|
+
{ pattern: /Baiduspider/i, name: "Baiduspider", category: "SEARCH_ENGINE" },
|
|
2547
|
+
{ pattern: /YandexBot/i, name: "YandexBot", category: "SEARCH_ENGINE" },
|
|
2548
|
+
{ pattern: /YandexImages/i, name: "YandexImages", category: "SEARCH_ENGINE" },
|
|
2549
|
+
{ pattern: /Sogou/i, name: "Sogou", category: "SEARCH_ENGINE" },
|
|
2550
|
+
{ pattern: /Exabot/i, name: "Exabot", category: "SEARCH_ENGINE" },
|
|
2551
|
+
{ pattern: /ia_archiver/i, name: "Alexa", category: "SEARCH_ENGINE" },
|
|
2552
|
+
{ pattern: /Applebot/i, name: "Applebot", category: "SEARCH_ENGINE" },
|
|
2553
|
+
{ pattern: /Qwantify/i, name: "Qwantify", category: "SEARCH_ENGINE" },
|
|
2554
|
+
{ pattern: /PetalBot/i, name: "PetalBot", category: "SEARCH_ENGINE" },
|
|
2555
|
+
{ pattern: /SeznamBot/i, name: "SeznamBot", category: "SEARCH_ENGINE" },
|
|
2556
|
+
// --- SOCIAL ---
|
|
2557
|
+
{ pattern: /Twitterbot/i, name: "Twitterbot", category: "SOCIAL" },
|
|
2558
|
+
{ pattern: /facebookexternalhit/i, name: "Facebook", category: "SOCIAL" },
|
|
2559
|
+
{ pattern: /Facebot/i, name: "Facebot", category: "SOCIAL" },
|
|
2560
|
+
{ pattern: /LinkedInBot/i, name: "LinkedInBot", category: "SOCIAL" },
|
|
2561
|
+
{ pattern: /Pinterest/i, name: "Pinterest", category: "SOCIAL" },
|
|
2562
|
+
{ pattern: /Slackbot/i, name: "Slackbot", category: "SOCIAL" },
|
|
2563
|
+
{ pattern: /TelegramBot/i, name: "TelegramBot", category: "SOCIAL" },
|
|
2564
|
+
{ pattern: /WhatsApp/i, name: "WhatsApp", category: "SOCIAL" },
|
|
2565
|
+
{ pattern: /Discordbot/i, name: "Discordbot", category: "SOCIAL" },
|
|
2566
|
+
{ pattern: /Redditbot/i, name: "Redditbot", category: "SOCIAL" },
|
|
2567
|
+
{ pattern: /Embedly/i, name: "Embedly", category: "SOCIAL" },
|
|
2568
|
+
{ pattern: /Quora Link Preview/i, name: "Quora", category: "SOCIAL" },
|
|
2569
|
+
{ pattern: /Mastodon/i, name: "Mastodon", category: "SOCIAL" },
|
|
2570
|
+
// --- MONITORING ---
|
|
2571
|
+
{ pattern: /UptimeRobot/i, name: "UptimeRobot", category: "MONITORING" },
|
|
2572
|
+
{ pattern: /Pingdom/i, name: "Pingdom", category: "MONITORING" },
|
|
2573
|
+
{ pattern: /Site24x7/i, name: "Site24x7", category: "MONITORING" },
|
|
2574
|
+
{ pattern: /StatusCake/i, name: "StatusCake", category: "MONITORING" },
|
|
2575
|
+
{ pattern: /Datadog/i, name: "Datadog", category: "MONITORING" },
|
|
2576
|
+
{ pattern: /NewRelicPinger/i, name: "New Relic", category: "MONITORING" },
|
|
2577
|
+
{ pattern: /Better Uptime Bot/i, name: "Better Uptime", category: "MONITORING" },
|
|
2578
|
+
{ pattern: /GTmetrix/i, name: "GTmetrix", category: "MONITORING" },
|
|
2579
|
+
{ pattern: /PageSpeed/i, name: "PageSpeed Insights", category: "MONITORING" },
|
|
2580
|
+
// --- AI CRAWLERS ---
|
|
2581
|
+
{ pattern: /GPTBot/i, name: "GPTBot", category: "AI_CRAWLER" },
|
|
2582
|
+
{ pattern: /ChatGPT-User/i, name: "ChatGPT-User", category: "AI_CRAWLER" },
|
|
2583
|
+
{ pattern: /Claude-Web/i, name: "Claude-Web", category: "AI_CRAWLER" },
|
|
2584
|
+
{ pattern: /ClaudeBot/i, name: "ClaudeBot", category: "AI_CRAWLER" },
|
|
2585
|
+
{ pattern: /anthropic-ai/i, name: "Anthropic", category: "AI_CRAWLER" },
|
|
2586
|
+
{ pattern: /Bytespider/i, name: "Bytespider", category: "AI_CRAWLER" },
|
|
2587
|
+
{ pattern: /CCBot/i, name: "CCBot", category: "AI_CRAWLER" },
|
|
2588
|
+
{ pattern: /cohere-ai/i, name: "Cohere", category: "AI_CRAWLER" },
|
|
2589
|
+
{ pattern: /PerplexityBot/i, name: "PerplexityBot", category: "AI_CRAWLER" },
|
|
2590
|
+
{ pattern: /YouBot/i, name: "YouBot", category: "AI_CRAWLER" },
|
|
2591
|
+
{ pattern: /Google-Extended/i, name: "Google-Extended", category: "AI_CRAWLER" },
|
|
2592
|
+
{ pattern: /Diffbot/i, name: "Diffbot", category: "AI_CRAWLER" },
|
|
2593
|
+
{ pattern: /Amazonbot/i, name: "Amazonbot", category: "AI_CRAWLER" },
|
|
2594
|
+
{ pattern: /meta-externalagent/i, name: "Meta AI", category: "AI_CRAWLER" },
|
|
2595
|
+
// --- AUTOMATED TOOLS (headless browsers, testing frameworks) ---
|
|
2596
|
+
{ pattern: /HeadlessChrome/i, name: "Headless Chrome", category: "AUTOMATED" },
|
|
2597
|
+
{ pattern: /PhantomJS/i, name: "PhantomJS", category: "AUTOMATED" },
|
|
2598
|
+
{ pattern: /Selenium/i, name: "Selenium", category: "AUTOMATED" },
|
|
2599
|
+
{ pattern: /Puppeteer/i, name: "Puppeteer", category: "AUTOMATED" },
|
|
2600
|
+
{ pattern: /Playwright/i, name: "Playwright", category: "AUTOMATED" },
|
|
2601
|
+
{ pattern: /Cypress/i, name: "Cypress", category: "AUTOMATED" },
|
|
2602
|
+
{ pattern: /webdriver/i, name: "WebDriver", category: "AUTOMATED" },
|
|
2603
|
+
{ pattern: /MSIE 6\.0/i, name: "Fake IE6", category: "AUTOMATED" },
|
|
2604
|
+
// --- SCRAPERS / CLI TOOLS ---
|
|
2605
|
+
{ pattern: /^curl\//i, name: "curl", category: "SCRAPER" },
|
|
2606
|
+
{ pattern: /^wget\//i, name: "wget", category: "SCRAPER" },
|
|
2607
|
+
{ pattern: /^python-requests\//i, name: "python-requests", category: "SCRAPER" },
|
|
2608
|
+
{ pattern: /^python-httpx\//i, name: "python-httpx", category: "SCRAPER" },
|
|
2609
|
+
{ pattern: /^Python-urllib/i, name: "Python-urllib", category: "SCRAPER" },
|
|
2610
|
+
{ pattern: /^aiohttp\//i, name: "aiohttp", category: "SCRAPER" },
|
|
2611
|
+
{ pattern: /^Go-http-client/i, name: "Go-http-client", category: "SCRAPER" },
|
|
2612
|
+
{ pattern: /^Java\//i, name: "Java HttpClient", category: "SCRAPER" },
|
|
2613
|
+
{ pattern: /^Apache-HttpClient/i, name: "Apache HttpClient", category: "SCRAPER" },
|
|
2614
|
+
{ pattern: /^okhttp\//i, name: "OkHttp", category: "SCRAPER" },
|
|
2615
|
+
{ pattern: /^node-fetch\//i, name: "node-fetch", category: "SCRAPER" },
|
|
2616
|
+
{ pattern: /^axios\//i, name: "axios", category: "SCRAPER" },
|
|
2617
|
+
{ pattern: /^got\//i, name: "got", category: "SCRAPER" },
|
|
2618
|
+
{ pattern: /^libwww-perl/i, name: "libwww-perl", category: "SCRAPER" },
|
|
2619
|
+
{ pattern: /^Ruby/i, name: "Ruby", category: "SCRAPER" },
|
|
2620
|
+
{ pattern: /^PHP\//i, name: "PHP", category: "SCRAPER" },
|
|
2621
|
+
{ pattern: /Scrapy/i, name: "Scrapy", category: "SCRAPER" },
|
|
2622
|
+
{ pattern: /^Postman/i, name: "Postman", category: "SCRAPER" },
|
|
2623
|
+
{ pattern: /^Insomnia/i, name: "Insomnia", category: "SCRAPER" },
|
|
2624
|
+
{ pattern: /^HTTPie\//i, name: "HTTPie", category: "SCRAPER" }
|
|
2625
|
+
];
|
|
2626
|
+
function detectBehavioralSignals(req) {
|
|
2627
|
+
const signals = [];
|
|
2628
|
+
const headers = req.headers;
|
|
2629
|
+
if (!headers["user-agent"]) {
|
|
2630
|
+
signals.push("missing_user_agent");
|
|
1535
2631
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
parsed = new URL(url);
|
|
1539
|
-
} catch {
|
|
1540
|
-
return { safe: false, reason: "invalid URL: failed to parse" };
|
|
2632
|
+
if (!headers["accept"]) {
|
|
2633
|
+
signals.push("missing_accept");
|
|
1541
2634
|
}
|
|
1542
|
-
if (!
|
|
1543
|
-
|
|
2635
|
+
if (!headers["accept-language"]) {
|
|
2636
|
+
signals.push("missing_accept_language");
|
|
1544
2637
|
}
|
|
1545
|
-
if (
|
|
1546
|
-
|
|
2638
|
+
if (!headers["accept-encoding"]) {
|
|
2639
|
+
signals.push("missing_accept_encoding");
|
|
1547
2640
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
return { safe: true };
|
|
2641
|
+
if (headers["connection"] === "close") {
|
|
2642
|
+
signals.push("connection_close");
|
|
1551
2643
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
2644
|
+
return signals;
|
|
2645
|
+
}
|
|
2646
|
+
function detectBot(req) {
|
|
2647
|
+
const rawUa = req.headers["user-agent"] ?? "";
|
|
2648
|
+
const ua = rawUa.length > 2048 ? rawUa.slice(0, 2048) : rawUa;
|
|
2649
|
+
const signals = detectBehavioralSignals(req);
|
|
2650
|
+
if (!ua) {
|
|
2651
|
+
return {
|
|
2652
|
+
isBot: true,
|
|
2653
|
+
category: "UNKNOWN",
|
|
2654
|
+
name: null,
|
|
2655
|
+
confidence: 0.8,
|
|
2656
|
+
signals
|
|
2657
|
+
};
|
|
1554
2658
|
}
|
|
1555
|
-
|
|
1556
|
-
if (
|
|
1557
|
-
return {
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
2659
|
+
for (const bot of BOT_PATTERNS) {
|
|
2660
|
+
if (bot.pattern.test(ua)) {
|
|
2661
|
+
return {
|
|
2662
|
+
isBot: true,
|
|
2663
|
+
category: bot.category,
|
|
2664
|
+
name: bot.name,
|
|
2665
|
+
confidence: 0.95,
|
|
2666
|
+
signals
|
|
2667
|
+
};
|
|
1561
2668
|
}
|
|
1562
2669
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
2670
|
+
const behaviorScore = signals.length;
|
|
2671
|
+
if (behaviorScore >= 3) {
|
|
2672
|
+
return {
|
|
2673
|
+
isBot: true,
|
|
2674
|
+
category: "UNKNOWN",
|
|
2675
|
+
name: null,
|
|
2676
|
+
confidence: Math.min(1, 0.6 + behaviorScore * 0.1),
|
|
2677
|
+
signals
|
|
2678
|
+
};
|
|
1568
2679
|
}
|
|
1569
|
-
return {
|
|
2680
|
+
return {
|
|
2681
|
+
isBot: false,
|
|
2682
|
+
category: "HUMAN",
|
|
2683
|
+
name: null,
|
|
2684
|
+
confidence: Math.max(0, 1 - behaviorScore * 0.15),
|
|
2685
|
+
signals
|
|
2686
|
+
};
|
|
1570
2687
|
}
|
|
1571
|
-
function
|
|
1572
|
-
|
|
2688
|
+
function botProtection(options = {}) {
|
|
2689
|
+
const {
|
|
2690
|
+
allow = ["SEARCH_ENGINE", "SOCIAL", "MONITORING"],
|
|
2691
|
+
deny = ["AUTOMATED"],
|
|
2692
|
+
defaultAction = "allow",
|
|
2693
|
+
statusCode = 403,
|
|
2694
|
+
message = "Access denied.",
|
|
2695
|
+
onDetected
|
|
2696
|
+
} = options;
|
|
2697
|
+
const allowSet = new Set(allow);
|
|
2698
|
+
const denySet = new Set(deny);
|
|
2699
|
+
return (req, res, next) => {
|
|
2700
|
+
const result = detectBot(req);
|
|
2701
|
+
req.botDetection = result;
|
|
2702
|
+
if (!result.isBot) {
|
|
2703
|
+
return next();
|
|
2704
|
+
}
|
|
2705
|
+
if (allowSet.has(result.category)) {
|
|
2706
|
+
return next();
|
|
2707
|
+
}
|
|
2708
|
+
if (denySet.has(result.category)) {
|
|
2709
|
+
if (onDetected) {
|
|
2710
|
+
return onDetected(req, res, result);
|
|
2711
|
+
}
|
|
2712
|
+
res.status(statusCode).json({ error: message });
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
if (defaultAction === "deny") {
|
|
2716
|
+
if (onDetected) {
|
|
2717
|
+
return onDetected(req, res, result);
|
|
2718
|
+
}
|
|
2719
|
+
res.status(statusCode).json({ error: message });
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
next();
|
|
2723
|
+
};
|
|
1573
2724
|
}
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
2725
|
+
var DEFAULTS = {
|
|
2726
|
+
cookieName: "_csrf",
|
|
2727
|
+
headerName: "x-csrf-token",
|
|
2728
|
+
fieldName: "_csrf",
|
|
2729
|
+
tokenLength: 32,
|
|
2730
|
+
protectedMethods: ["POST", "PUT", "PATCH", "DELETE"]
|
|
2731
|
+
};
|
|
2732
|
+
function generateCsrfToken(length = 32) {
|
|
2733
|
+
return crypto.randomBytes(length).toString("hex");
|
|
2734
|
+
}
|
|
2735
|
+
function validateCsrfToken(cookieToken, requestToken) {
|
|
2736
|
+
if (!cookieToken || !requestToken) return false;
|
|
2737
|
+
if (cookieToken.length !== requestToken.length) return false;
|
|
2738
|
+
let result = 0;
|
|
2739
|
+
for (let i = 0; i < cookieToken.length; i++) {
|
|
2740
|
+
result |= cookieToken.charCodeAt(i) ^ requestToken.charCodeAt(i);
|
|
2741
|
+
}
|
|
2742
|
+
return result === 0;
|
|
2743
|
+
}
|
|
2744
|
+
function getRequestToken(req, headerName, fieldName) {
|
|
2745
|
+
const headerToken = req.headers[headerName.toLowerCase()];
|
|
2746
|
+
if (typeof headerToken === "string" && headerToken) return headerToken;
|
|
2747
|
+
if (req.body && typeof req.body === "object" && fieldName in req.body) {
|
|
2748
|
+
const bodyToken = req.body[fieldName];
|
|
2749
|
+
if (typeof bodyToken === "string" && bodyToken) return bodyToken;
|
|
2750
|
+
}
|
|
2751
|
+
if (req.query && fieldName in req.query) {
|
|
2752
|
+
const queryToken = req.query[fieldName];
|
|
2753
|
+
if (typeof queryToken === "string" && queryToken) return queryToken;
|
|
2754
|
+
}
|
|
2755
|
+
return void 0;
|
|
2756
|
+
}
|
|
2757
|
+
function csrfProtection(options = {}) {
|
|
2758
|
+
const cookieName = options.cookieName ?? DEFAULTS.cookieName;
|
|
2759
|
+
const headerName = options.headerName ?? DEFAULTS.headerName;
|
|
2760
|
+
const fieldName = options.fieldName ?? DEFAULTS.fieldName;
|
|
2761
|
+
const tokenLength = options.tokenLength ?? DEFAULTS.tokenLength;
|
|
2762
|
+
const protectedMethods = options.protectedMethods ?? [...DEFAULTS.protectedMethods];
|
|
2763
|
+
const excludePaths = options.excludePaths ?? [];
|
|
2764
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
2765
|
+
const cookieOpts = {
|
|
2766
|
+
path: options.cookie?.path ?? "/",
|
|
2767
|
+
httpOnly: options.cookie?.httpOnly ?? false,
|
|
2768
|
+
// Must be readable by client JS
|
|
2769
|
+
secure: options.cookie?.secure ?? isProduction,
|
|
2770
|
+
sameSite: options.cookie?.sameSite ?? "Lax",
|
|
2771
|
+
domain: options.cookie?.domain
|
|
2772
|
+
};
|
|
2773
|
+
const defaultOnError = (_req, res, _next) => {
|
|
2774
|
+
res.status(403).json({
|
|
2775
|
+
error: "CSRF token validation failed",
|
|
2776
|
+
message: "Invalid or missing CSRF token. Include the token from the cookie in the X-CSRF-Token header."
|
|
2777
|
+
});
|
|
2778
|
+
};
|
|
2779
|
+
const onError = options.onError ?? defaultOnError;
|
|
2780
|
+
const protectedSet = new Set(protectedMethods.map((m) => m.toUpperCase()));
|
|
2781
|
+
return (req, res, next) => {
|
|
2782
|
+
const method = req.method.toUpperCase();
|
|
2783
|
+
const requestPath = req.path || req.url;
|
|
2784
|
+
if (excludePaths.some((p) => requestPath === p || requestPath.startsWith(p + "/"))) {
|
|
2785
|
+
return next();
|
|
1583
2786
|
}
|
|
2787
|
+
req.csrfToken = () => {
|
|
2788
|
+
const existing = getCookieValue(req, cookieName);
|
|
2789
|
+
if (existing) return existing;
|
|
2790
|
+
const token = generateCsrfToken(tokenLength);
|
|
2791
|
+
setCsrfCookie(res, cookieName, token, cookieOpts);
|
|
2792
|
+
return token;
|
|
2793
|
+
};
|
|
2794
|
+
if (!protectedSet.has(method)) {
|
|
2795
|
+
const existing = getCookieValue(req, cookieName);
|
|
2796
|
+
if (!existing) {
|
|
2797
|
+
const token = generateCsrfToken(tokenLength);
|
|
2798
|
+
setCsrfCookie(res, cookieName, token, cookieOpts);
|
|
2799
|
+
}
|
|
2800
|
+
return next();
|
|
2801
|
+
}
|
|
2802
|
+
const cookieToken = getCookieValue(req, cookieName);
|
|
2803
|
+
if (!cookieToken) {
|
|
2804
|
+
return onError(req, res, next);
|
|
2805
|
+
}
|
|
2806
|
+
const requestToken = getRequestToken(req, headerName, fieldName);
|
|
2807
|
+
if (!requestToken) {
|
|
2808
|
+
return onError(req, res, next);
|
|
2809
|
+
}
|
|
2810
|
+
if (!validateCsrfToken(cookieToken, requestToken)) {
|
|
2811
|
+
return onError(req, res, next);
|
|
2812
|
+
}
|
|
2813
|
+
next();
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
function getCookieValue(req, name) {
|
|
2817
|
+
if (req.cookies && typeof req.cookies === "object" && name in req.cookies) {
|
|
2818
|
+
return req.cookies[name];
|
|
2819
|
+
}
|
|
2820
|
+
const cookieHeader = req.headers.cookie;
|
|
2821
|
+
if (!cookieHeader) return void 0;
|
|
2822
|
+
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${escapeRegex(name)}=([^;]*)`));
|
|
2823
|
+
return match ? decodeURIComponent(match[1]) : void 0;
|
|
2824
|
+
}
|
|
2825
|
+
function setCsrfCookie(res, name, token, opts) {
|
|
2826
|
+
const parts = [`${name}=${token}`];
|
|
2827
|
+
parts.push(`Path=${opts.path}`);
|
|
2828
|
+
if (opts.httpOnly) parts.push("HttpOnly");
|
|
2829
|
+
if (opts.secure) parts.push("Secure");
|
|
2830
|
+
parts.push(`SameSite=${opts.sameSite}`);
|
|
2831
|
+
if (opts.domain) parts.push(`Domain=${opts.domain}`);
|
|
2832
|
+
res.setHeader("Set-Cookie", parts.join("; "));
|
|
2833
|
+
}
|
|
2834
|
+
function escapeRegex(str) {
|
|
2835
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2836
|
+
}
|
|
2837
|
+
var createCsrf = csrfProtection;
|
|
2838
|
+
|
|
2839
|
+
// src/utils/ip.ts
|
|
2840
|
+
var PLATFORM_HEADERS = {
|
|
2841
|
+
cloudflare: "cf-connecting-ip",
|
|
2842
|
+
vercel: "x-real-ip",
|
|
2843
|
+
flyio: "fly-client-ip",
|
|
2844
|
+
render: "x-render-client-ip",
|
|
2845
|
+
firebase: "x-appengine-user-ip",
|
|
2846
|
+
"aws-alb": "x-forwarded-for"
|
|
2847
|
+
};
|
|
2848
|
+
function detectPlatform() {
|
|
2849
|
+
const env = typeof process !== "undefined" ? process.env : {};
|
|
2850
|
+
if (env.CF_PAGES || env.CF_WORKERS) return "cloudflare";
|
|
2851
|
+
if (env.VERCEL) return "vercel";
|
|
2852
|
+
if (env.FLY_APP_NAME) return "flyio";
|
|
2853
|
+
if (env.RENDER) return "render";
|
|
2854
|
+
if (env.FIREBASE_CONFIG || env.GCLOUD_PROJECT) return "firebase";
|
|
2855
|
+
if (env.AWS_EXECUTION_ENV || env.AWS_LAMBDA_FUNCTION_NAME) return "aws-alb";
|
|
2856
|
+
return "generic";
|
|
2857
|
+
}
|
|
2858
|
+
var _cachedPlatform = null;
|
|
2859
|
+
function getCachedPlatform() {
|
|
2860
|
+
if (_cachedPlatform === null) {
|
|
2861
|
+
_cachedPlatform = detectPlatform();
|
|
1584
2862
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
2863
|
+
return _cachedPlatform;
|
|
2864
|
+
}
|
|
2865
|
+
var MAX_IP_LENGTH = 45;
|
|
2866
|
+
function sanitizeIp(ip) {
|
|
2867
|
+
const trimmed = ip.trim();
|
|
2868
|
+
if (trimmed.length > MAX_IP_LENGTH) return trimmed.slice(0, MAX_IP_LENGTH);
|
|
2869
|
+
return trimmed;
|
|
2870
|
+
}
|
|
2871
|
+
function getHeader(req, name) {
|
|
2872
|
+
const val = req.headers[name];
|
|
2873
|
+
if (Array.isArray(val)) return val[0];
|
|
2874
|
+
return val;
|
|
2875
|
+
}
|
|
2876
|
+
function parseForwardedFor(header, trustedProxyCount) {
|
|
2877
|
+
const ips = header.split(",").map((ip) => ip.trim()).filter(Boolean);
|
|
2878
|
+
if (ips.length === 0) return void 0;
|
|
2879
|
+
const clientIndex = Math.max(0, ips.length - trustedProxyCount);
|
|
2880
|
+
return ips[clientIndex] || void 0;
|
|
2881
|
+
}
|
|
2882
|
+
function detectClientIp(req, options = {}) {
|
|
2883
|
+
const { platform = "auto", trustedProxyCount = 1 } = options;
|
|
2884
|
+
const r = req;
|
|
2885
|
+
const resolvedPlatform = platform === "auto" ? getCachedPlatform() : platform;
|
|
2886
|
+
if (resolvedPlatform !== "generic" && resolvedPlatform in PLATFORM_HEADERS) {
|
|
2887
|
+
const headerName = PLATFORM_HEADERS[resolvedPlatform];
|
|
2888
|
+
if (headerName) {
|
|
2889
|
+
if (resolvedPlatform === "aws-alb") {
|
|
2890
|
+
const xff2 = getHeader(r, "x-forwarded-for");
|
|
2891
|
+
if (xff2) {
|
|
2892
|
+
const ip = parseForwardedFor(xff2, trustedProxyCount);
|
|
2893
|
+
if (ip) return sanitizeIp(ip);
|
|
2894
|
+
}
|
|
2895
|
+
} else {
|
|
2896
|
+
const ip = getHeader(r, headerName);
|
|
2897
|
+
if (ip) return sanitizeIp(ip);
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
1596
2900
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
2901
|
+
if (r.ip) return sanitizeIp(r.ip);
|
|
2902
|
+
const xff = getHeader(r, "x-forwarded-for");
|
|
2903
|
+
if (xff) {
|
|
2904
|
+
const ip = parseForwardedFor(xff, trustedProxyCount);
|
|
2905
|
+
if (ip) return sanitizeIp(ip);
|
|
1600
2906
|
}
|
|
1601
|
-
|
|
2907
|
+
const realIp = getHeader(r, "x-real-ip");
|
|
2908
|
+
if (realIp) return sanitizeIp(realIp);
|
|
2909
|
+
const socketIp = r.socket?.remoteAddress ?? r.connection?.remoteAddress;
|
|
2910
|
+
if (socketIp) return sanitizeIp(socketIp);
|
|
2911
|
+
return "unknown";
|
|
1602
2912
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
2913
|
+
function isPrivateIp(ip) {
|
|
2914
|
+
const normalized = ip.startsWith("::ffff:") ? ip.slice(7) : ip;
|
|
2915
|
+
if (/^127\./.test(normalized)) return true;
|
|
2916
|
+
if (/^10\./.test(normalized)) return true;
|
|
2917
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(normalized)) return true;
|
|
2918
|
+
if (/^192\.168\./.test(normalized)) return true;
|
|
2919
|
+
if (/^169\.254\./.test(normalized)) return true;
|
|
2920
|
+
if (/^0\./.test(normalized)) return true;
|
|
2921
|
+
if (ip === "::1") return true;
|
|
2922
|
+
if (/^fe80:/i.test(ip)) return true;
|
|
2923
|
+
if (/^fc00:/i.test(ip)) return true;
|
|
2924
|
+
if (/^fd/i.test(ip)) return true;
|
|
2925
|
+
return false;
|
|
2926
|
+
}
|
|
2927
|
+
function getHeader2(req, name) {
|
|
2928
|
+
const val = req.headers[name];
|
|
2929
|
+
if (Array.isArray(val)) return val[0] ?? "";
|
|
2930
|
+
return val ?? "";
|
|
2931
|
+
}
|
|
2932
|
+
function fingerprint(req, options = {}) {
|
|
1608
2933
|
const {
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
2934
|
+
ip = true,
|
|
2935
|
+
userAgent = true,
|
|
2936
|
+
accept = true,
|
|
2937
|
+
acceptLanguage = true,
|
|
2938
|
+
acceptEncoding = true,
|
|
2939
|
+
custom = [],
|
|
2940
|
+
ipOptions
|
|
1612
2941
|
} = options;
|
|
1613
|
-
|
|
1614
|
-
|
|
2942
|
+
const components = [];
|
|
2943
|
+
if (ip) {
|
|
2944
|
+
components.push(`ip:${detectClientIp(req, ipOptions)}`);
|
|
1615
2945
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
const proto = cleaned.match(DANGEROUS_PROTOCOLS);
|
|
1619
|
-
return { safe: false, reason: `dangerous protocol: ${proto[0]}` };
|
|
2946
|
+
if (userAgent) {
|
|
2947
|
+
components.push(`ua:${getHeader2(req, "user-agent")}`);
|
|
1620
2948
|
}
|
|
1621
|
-
if (
|
|
1622
|
-
|
|
2949
|
+
if (accept) {
|
|
2950
|
+
components.push(`accept:${getHeader2(req, "accept")}`);
|
|
1623
2951
|
}
|
|
1624
|
-
if (
|
|
1625
|
-
|
|
1626
|
-
const host2 = extractHost(cleaned);
|
|
1627
|
-
if (host2 && allowedHosts.some((h) => host2 === h.toLowerCase())) {
|
|
1628
|
-
return { safe: true };
|
|
1629
|
-
}
|
|
1630
|
-
return { safe: false, reason: "protocol-relative URL not in allowed hosts" };
|
|
1631
|
-
}
|
|
1632
|
-
const host = extractHost(cleaned);
|
|
1633
|
-
if (host && allowedHosts.length > 0 && !allowedHosts.some((h) => host === h.toLowerCase())) {
|
|
1634
|
-
return { safe: false, reason: "protocol-relative URL not in allowed hosts" };
|
|
1635
|
-
}
|
|
1636
|
-
return { safe: true };
|
|
2952
|
+
if (acceptLanguage) {
|
|
2953
|
+
components.push(`lang:${getHeader2(req, "accept-language")}`);
|
|
1637
2954
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
parsed = new URL(cleaned);
|
|
1641
|
-
} catch {
|
|
1642
|
-
return { safe: true };
|
|
2955
|
+
if (acceptEncoding) {
|
|
2956
|
+
components.push(`enc:${getHeader2(req, "accept-encoding")}`);
|
|
1643
2957
|
}
|
|
1644
|
-
|
|
1645
|
-
|
|
2958
|
+
for (const c of custom) {
|
|
2959
|
+
if (c != null) components.push(`custom:${c}`);
|
|
1646
2960
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
if (!allowedHosts.some((h) => hostname === h.toLowerCase())) {
|
|
1652
|
-
return { safe: false, reason: `host not allowed: ${hostname}` };
|
|
1653
|
-
}
|
|
1654
|
-
return { safe: true };
|
|
1655
|
-
}
|
|
1656
|
-
function isRedirectSafe(url, options = {}) {
|
|
1657
|
-
return validateRedirect(url, options).safe;
|
|
1658
|
-
}
|
|
1659
|
-
function extractHost(url) {
|
|
1660
|
-
const match = url.match(/^\/\/([^/:?#]+)/);
|
|
1661
|
-
return match ? match[1].toLowerCase() : null;
|
|
2961
|
+
components.sort();
|
|
2962
|
+
const hash = crypto.createHash("sha256");
|
|
2963
|
+
hash.update(components.join("|"));
|
|
2964
|
+
return hash.digest("hex");
|
|
1662
2965
|
}
|
|
1663
2966
|
|
|
1664
2967
|
// src/stores/memory.ts
|
|
@@ -1813,7 +3116,9 @@ exports.SecurityThreatError = SecurityThreatError;
|
|
|
1813
3116
|
exports.VALIDATION = VALIDATION;
|
|
1814
3117
|
exports.arcis = arcis;
|
|
1815
3118
|
exports.arcisFunction = arcisWithMethods;
|
|
3119
|
+
exports.botProtection = botProtection;
|
|
1816
3120
|
exports.createCors = createCors;
|
|
3121
|
+
exports.createCsrf = createCsrf;
|
|
1817
3122
|
exports.createErrorHandler = createErrorHandler;
|
|
1818
3123
|
exports.createHeaders = createHeaders;
|
|
1819
3124
|
exports.createRateLimiter = createRateLimiter;
|
|
@@ -1822,39 +3127,64 @@ exports.createRedisStore = createRedisStore;
|
|
|
1822
3127
|
exports.createSafeLogger = createSafeLogger;
|
|
1823
3128
|
exports.createSanitizer = createSanitizer;
|
|
1824
3129
|
exports.createSecureCookies = createSecureCookies;
|
|
3130
|
+
exports.createSlidingWindowLimiter = createSlidingWindowLimiter;
|
|
3131
|
+
exports.createTokenBucketLimiter = createTokenBucketLimiter;
|
|
1825
3132
|
exports.createValidator = createValidator;
|
|
3133
|
+
exports.csrfProtection = csrfProtection;
|
|
1826
3134
|
exports.default = main_default;
|
|
3135
|
+
exports.detectBot = detectBot;
|
|
3136
|
+
exports.detectClientIp = detectClientIp;
|
|
1827
3137
|
exports.detectCommandInjection = detectCommandInjection;
|
|
1828
3138
|
exports.detectHeaderInjection = detectHeaderInjection;
|
|
3139
|
+
exports.detectJsonpInjection = detectJsonpInjection;
|
|
1829
3140
|
exports.detectNoSqlInjection = detectNoSqlInjection;
|
|
1830
3141
|
exports.detectPathTraversal = detectPathTraversal;
|
|
3142
|
+
exports.detectPii = detectPii;
|
|
1831
3143
|
exports.detectPrototypePollution = detectPrototypePollution;
|
|
1832
3144
|
exports.detectSql = detectSql;
|
|
3145
|
+
exports.detectSsti = detectSsti;
|
|
1833
3146
|
exports.detectXss = detectXss;
|
|
3147
|
+
exports.detectXxe = detectXxe;
|
|
1834
3148
|
exports.enforceSecureCookie = enforceSecureCookie;
|
|
1835
3149
|
exports.errorHandler = errorHandler;
|
|
3150
|
+
exports.fingerprint = fingerprint;
|
|
3151
|
+
exports.formatDuration = formatDuration;
|
|
3152
|
+
exports.generateCsrfToken = generateCsrfToken;
|
|
1836
3153
|
exports.isDangerousExtension = isDangerousExtension;
|
|
1837
3154
|
exports.isDangerousNoSqlKey = isDangerousNoSqlKey;
|
|
1838
3155
|
exports.isDangerousProtoKey = isDangerousProtoKey;
|
|
3156
|
+
exports.isPrivateIp = isPrivateIp;
|
|
1839
3157
|
exports.isRedirectSafe = isRedirectSafe;
|
|
1840
3158
|
exports.isUrlSafe = isUrlSafe;
|
|
3159
|
+
exports.isValidEmailSyntax = isValidEmailSyntax;
|
|
3160
|
+
exports.parseDuration = parseDuration;
|
|
1841
3161
|
exports.rateLimit = rateLimit;
|
|
3162
|
+
exports.redactObjectPii = redactObjectPii;
|
|
3163
|
+
exports.redactPii = redactPii;
|
|
1842
3164
|
exports.safeCors = safeCors;
|
|
1843
3165
|
exports.safeLog = safeLog;
|
|
1844
3166
|
exports.sanitizeCommand = sanitizeCommand;
|
|
1845
3167
|
exports.sanitizeFilename = sanitizeFilename;
|
|
1846
3168
|
exports.sanitizeHeaderValue = sanitizeHeaderValue;
|
|
1847
3169
|
exports.sanitizeHeaders = sanitizeHeaders;
|
|
3170
|
+
exports.sanitizeJsonpCallback = sanitizeJsonpCallback;
|
|
1848
3171
|
exports.sanitizeObject = sanitizeObject;
|
|
1849
3172
|
exports.sanitizePath = sanitizePath;
|
|
1850
3173
|
exports.sanitizeSql = sanitizeSql;
|
|
3174
|
+
exports.sanitizeSsti = sanitizeSsti;
|
|
1851
3175
|
exports.sanitizeString = sanitizeString;
|
|
1852
3176
|
exports.sanitizeXss = sanitizeXss;
|
|
3177
|
+
exports.sanitizeXxe = sanitizeXxe;
|
|
3178
|
+
exports.scanObjectPii = scanObjectPii;
|
|
3179
|
+
exports.scanPii = scanPii;
|
|
1853
3180
|
exports.secureCookieDefaults = secureCookieDefaults;
|
|
1854
3181
|
exports.securityHeaders = securityHeaders;
|
|
1855
3182
|
exports.validate = validate;
|
|
3183
|
+
exports.validateCsrfToken = validateCsrfToken;
|
|
3184
|
+
exports.validateEmail = validateEmail;
|
|
1856
3185
|
exports.validateFile = validateFile;
|
|
1857
3186
|
exports.validateRedirect = validateRedirect;
|
|
1858
3187
|
exports.validateUrl = validateUrl;
|
|
3188
|
+
exports.verifyEmailMx = verifyEmailMx;
|
|
1859
3189
|
//# sourceMappingURL=index.js.map
|
|
1860
3190
|
//# sourceMappingURL=index.js.map
|