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