@arcis/node 1.4.0 → 1.4.3
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 +1 -1
- package/dist/core/constants.d.ts +2 -2
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/index.js +11 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +11 -3
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +527 -63
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +525 -65
- package/dist/index.mjs.map +1 -1
- package/dist/logging/index.js.map +1 -1
- package/dist/logging/index.mjs.map +1 -1
- package/dist/middleware/bot-detection.d.ts.map +1 -1
- package/dist/middleware/cookies.d.ts.map +1 -1
- package/dist/middleware/csrf.d.ts +10 -0
- package/dist/middleware/csrf.d.ts.map +1 -1
- package/dist/middleware/hpp.d.ts.map +1 -1
- package/dist/middleware/index.d.ts +2 -0
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +671 -39
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +671 -41
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/middleware/main.d.ts.map +1 -1
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/middleware/signup-protection.d.ts +65 -0
- package/dist/middleware/signup-protection.d.ts.map +1 -0
- package/dist/middleware/telemetry.d.ts +36 -0
- package/dist/middleware/telemetry.d.ts.map +1 -0
- package/dist/sanitizers/encode.d.ts.map +1 -1
- package/dist/sanitizers/index.d.ts +1 -0
- package/dist/sanitizers/index.d.ts.map +1 -1
- package/dist/sanitizers/index.js +113 -37
- package/dist/sanitizers/index.js.map +1 -1
- package/dist/sanitizers/index.mjs +111 -38
- package/dist/sanitizers/index.mjs.map +1 -1
- package/dist/sanitizers/ldap.d.ts +42 -0
- package/dist/sanitizers/ldap.d.ts.map +1 -0
- package/dist/sanitizers/path.d.ts.map +1 -1
- package/dist/sanitizers/pii.d.ts.map +1 -1
- package/dist/sanitizers/sanitize.d.ts.map +1 -1
- package/dist/sanitizers/ssti.d.ts.map +1 -1
- package/dist/sanitizers/xxe.d.ts.map +1 -1
- package/dist/stores/index.js +21 -1
- package/dist/stores/index.js.map +1 -1
- package/dist/stores/index.mjs +21 -1
- package/dist/stores/index.mjs.map +1 -1
- package/dist/stores/memory.d.ts +4 -10
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/telemetry/client.d.ts +60 -0
- package/dist/telemetry/client.d.ts.map +1 -0
- package/dist/telemetry/index.d.ts +3 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/types.d.ts +59 -0
- package/dist/telemetry/types.d.ts.map +1 -0
- package/dist/validation/index.js +41 -21
- package/dist/validation/index.js.map +1 -1
- package/dist/validation/index.mjs +41 -21
- package/dist/validation/index.mjs.map +1 -1
- package/package.json +8 -2
package/dist/index.js
CHANGED
|
@@ -70,13 +70,24 @@ var XSS_PATTERNS = [
|
|
|
70
70
|
/** URL-encoded script tags */
|
|
71
71
|
/%3Cscript/gi,
|
|
72
72
|
/** SVG with onload */
|
|
73
|
-
/<svg[^>]*onload/gi
|
|
73
|
+
/<svg[^>]*onload/gi,
|
|
74
|
+
/** form tags — phishing/credential harvesting via action= redirection */
|
|
75
|
+
/<form[\s>]/gi,
|
|
76
|
+
/** meta tags — http-equiv refresh redirects or CSP bypass */
|
|
77
|
+
/<meta[\s>]/gi,
|
|
78
|
+
/** base href hijacking — redirects all relative URLs to attacker domain */
|
|
79
|
+
/<base[\s>]/gi,
|
|
80
|
+
/** link tag injection — stylesheet or preload CSRF attacks */
|
|
81
|
+
/<link[\s>]/gi
|
|
74
82
|
];
|
|
75
83
|
var XSS_REMOVE_PATTERNS = [
|
|
76
84
|
/** Full script blocks (content + tags) */
|
|
77
85
|
/<script[^>]*>[\s\S]*?<\/script>/gi,
|
|
78
86
|
/** Standalone/unclosed script tags */
|
|
79
87
|
/<script[^>]*>/gi,
|
|
88
|
+
/** style — CSS expression() and behavior: attacks (IE-era but still relevant) */
|
|
89
|
+
/<style[^>]*>[\s\S]*?<\/style>/gi,
|
|
90
|
+
/<style[^>]*/gi,
|
|
80
91
|
/** iframe — full block and partial/unclosed */
|
|
81
92
|
/<iframe[^>]*>[\s\S]*?<\/iframe>/gi,
|
|
82
93
|
/<iframe[^>]*/gi,
|
|
@@ -97,7 +108,15 @@ var XSS_REMOVE_PATTERNS = [
|
|
|
97
108
|
/javascript\s*:/gi,
|
|
98
109
|
/vbscript\s*:/gi,
|
|
99
110
|
/** data: URIs with HTML/script content */
|
|
100
|
-
/data\s*:\s*text\/html[^>\s]*/gi
|
|
111
|
+
/data\s*:\s*text\/html[^>\s]*/gi,
|
|
112
|
+
/** form tag injection — phishing via action= redirection */
|
|
113
|
+
/<form[\s>][^>]*/gi,
|
|
114
|
+
/** meta tag injection — http-equiv refresh or CSP bypass */
|
|
115
|
+
/<meta[\s>][^>]*/gi,
|
|
116
|
+
/** base href hijacking */
|
|
117
|
+
/<base[\s>][^>]*/gi,
|
|
118
|
+
/** link tag injection — stylesheet or preload attacks */
|
|
119
|
+
/<link[\s>][^>]*/gi
|
|
101
120
|
];
|
|
102
121
|
var SQL_PATTERNS = [
|
|
103
122
|
/** SQL keywords */
|
|
@@ -161,8 +180,8 @@ var COMMAND_PATTERNS = [
|
|
|
161
180
|
/[;&|`]/g,
|
|
162
181
|
/** Command substitution: $( ... ) — matched as a pair to reduce false positives */
|
|
163
182
|
/\$\(/g,
|
|
164
|
-
/** URL-encoded
|
|
165
|
-
/%0[
|
|
183
|
+
/** URL-encoded control characters (%00-%0F): null, tab, vtab, formfeed, LF, CR */
|
|
184
|
+
/%0[0-9a-f]/gi
|
|
166
185
|
];
|
|
167
186
|
var DANGEROUS_PROTO_KEYS = /* @__PURE__ */ new Set([
|
|
168
187
|
"__proto__",
|
|
@@ -384,13 +403,13 @@ function createRateLimiter(options = {}) {
|
|
|
384
403
|
statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
|
|
385
404
|
keyGenerator = (req) => {
|
|
386
405
|
const ip = req.ip ?? req.socket?.remoteAddress;
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
return
|
|
406
|
+
if (ip) return ip;
|
|
407
|
+
const ua = req.headers["user-agent"] ?? "";
|
|
408
|
+
const lang = req.headers["accept-language"] ?? "";
|
|
409
|
+
const fp = `${ua}|${lang}`;
|
|
410
|
+
let hash = 0;
|
|
411
|
+
for (let i = 0; i < fp.length; i++) hash = (hash << 5) - hash + fp.charCodeAt(i) | 0;
|
|
412
|
+
return `unknown:${hash.toString(36)}`;
|
|
394
413
|
},
|
|
395
414
|
skip,
|
|
396
415
|
store: externalStore
|
|
@@ -453,7 +472,24 @@ function createRateLimiter(options = {}) {
|
|
|
453
472
|
}
|
|
454
473
|
next();
|
|
455
474
|
} catch (error) {
|
|
456
|
-
console.error("[arcis] Rate limiter error:", error);
|
|
475
|
+
console.error("[arcis] Rate limiter store error, using in-memory fallback:", error);
|
|
476
|
+
try {
|
|
477
|
+
const key = keyGenerator(req);
|
|
478
|
+
const now = Date.now();
|
|
479
|
+
if (!inMemoryStore[key] || inMemoryStore[key].resetTime < now) {
|
|
480
|
+
inMemoryStore[key] = { count: 1, resetTime: now + windowMs };
|
|
481
|
+
} else {
|
|
482
|
+
inMemoryStore[key].count++;
|
|
483
|
+
}
|
|
484
|
+
const count = inMemoryStore[key].count;
|
|
485
|
+
if (count > max) {
|
|
486
|
+
const resetSeconds = Math.ceil((inMemoryStore[key].resetTime - now) / 1e3);
|
|
487
|
+
res.setHeader("Retry-After", resetSeconds.toString());
|
|
488
|
+
res.status(statusCode).json({ error: message, retryAfter: resetSeconds });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
} catch {
|
|
492
|
+
}
|
|
457
493
|
next();
|
|
458
494
|
}
|
|
459
495
|
};
|
|
@@ -585,6 +621,80 @@ var SanitizationError = class extends ArcisError {
|
|
|
585
621
|
}
|
|
586
622
|
};
|
|
587
623
|
|
|
624
|
+
// src/middleware/telemetry.ts
|
|
625
|
+
var THREAT_TO_VECTOR = {
|
|
626
|
+
xss: "xss",
|
|
627
|
+
sql_injection: "sql",
|
|
628
|
+
nosql_injection: "nosql",
|
|
629
|
+
path_traversal: "path",
|
|
630
|
+
command_injection: "command",
|
|
631
|
+
prototype_pollution: "prototype",
|
|
632
|
+
header_injection: "header",
|
|
633
|
+
ssti: "ssti",
|
|
634
|
+
xxe: "xxe"
|
|
635
|
+
};
|
|
636
|
+
function createTelemetryEmitter(client) {
|
|
637
|
+
return (req, res, next) => {
|
|
638
|
+
const start = performance.now();
|
|
639
|
+
res.on("finish", () => {
|
|
640
|
+
try {
|
|
641
|
+
const event = buildEvent(req, res.statusCode, performance.now() - start);
|
|
642
|
+
client.record(event);
|
|
643
|
+
} catch {
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
next();
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function tapSanitizerThreats(handler) {
|
|
650
|
+
return (req, res, next) => {
|
|
651
|
+
handler(req, res, (err) => {
|
|
652
|
+
if (err instanceof SecurityThreatError) {
|
|
653
|
+
const vector = THREAT_TO_VECTOR[err.threatType] ?? err.threatType;
|
|
654
|
+
req.__arcis = {
|
|
655
|
+
vector,
|
|
656
|
+
rule: `${vector}/match`,
|
|
657
|
+
severity: "high",
|
|
658
|
+
matchedPattern: err.pattern,
|
|
659
|
+
reason: err.message,
|
|
660
|
+
decision: "deny"
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
next(err);
|
|
664
|
+
});
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function buildEvent(req, status, latencyMs) {
|
|
668
|
+
const marker = req.__arcis;
|
|
669
|
+
const decision = marker?.decision ?? inferDecision(status);
|
|
670
|
+
return {
|
|
671
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
672
|
+
ip: extractIp(req),
|
|
673
|
+
method: (req.method ?? "GET").toUpperCase(),
|
|
674
|
+
path: req.path ?? req.url ?? "/",
|
|
675
|
+
decision,
|
|
676
|
+
vector: marker?.vector ?? (status === 429 ? "rate-limit" : void 0),
|
|
677
|
+
rule: marker?.rule ?? (status === 429 ? "rate-limit/exceeded" : void 0),
|
|
678
|
+
severity: marker?.severity ?? (status === 429 ? "medium" : void 0),
|
|
679
|
+
userAgent: typeof req.headers?.["user-agent"] === "string" ? req.headers["user-agent"] : "",
|
|
680
|
+
reason: marker?.reason,
|
|
681
|
+
status,
|
|
682
|
+
matchedPattern: marker?.matchedPattern,
|
|
683
|
+
latencyMs: Math.max(0, latencyMs)
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
function inferDecision(status) {
|
|
687
|
+
if (status === 429) return "deny";
|
|
688
|
+
if (status === 400) return "deny";
|
|
689
|
+
if (status === 403) return "deny";
|
|
690
|
+
return "allow";
|
|
691
|
+
}
|
|
692
|
+
function extractIp(req) {
|
|
693
|
+
if (typeof req.ip === "string" && req.ip.length > 0) return req.ip;
|
|
694
|
+
const remote = req.socket?.remoteAddress;
|
|
695
|
+
return typeof remote === "string" ? remote : "0.0.0.0";
|
|
696
|
+
}
|
|
697
|
+
|
|
588
698
|
// src/sanitizers/utils.ts
|
|
589
699
|
function encodeHtmlEntities(str) {
|
|
590
700
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -697,26 +807,31 @@ function sanitizePath(input, collectThreats = false) {
|
|
|
697
807
|
const threats = [];
|
|
698
808
|
let value = input;
|
|
699
809
|
let wasSanitized = false;
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
810
|
+
value = value.normalize("NFKC");
|
|
811
|
+
let prev;
|
|
812
|
+
do {
|
|
813
|
+
prev = value;
|
|
814
|
+
for (const pattern of PATH_PATTERNS) {
|
|
703
815
|
pattern.lastIndex = 0;
|
|
704
|
-
if (
|
|
705
|
-
|
|
706
|
-
if (
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
816
|
+
if (pattern.test(value)) {
|
|
817
|
+
pattern.lastIndex = 0;
|
|
818
|
+
if (collectThreats) {
|
|
819
|
+
const matches = value.match(pattern);
|
|
820
|
+
if (matches) {
|
|
821
|
+
for (const match of matches) {
|
|
822
|
+
threats.push({
|
|
823
|
+
type: "path_traversal",
|
|
824
|
+
pattern: pattern.source,
|
|
825
|
+
original: match
|
|
826
|
+
});
|
|
827
|
+
}
|
|
713
828
|
}
|
|
714
829
|
}
|
|
830
|
+
value = value.replace(pattern, "");
|
|
831
|
+
wasSanitized = true;
|
|
715
832
|
}
|
|
716
|
-
value = value.replace(pattern, "");
|
|
717
|
-
wasSanitized = true;
|
|
718
833
|
}
|
|
719
|
-
}
|
|
834
|
+
} while (value !== prev);
|
|
720
835
|
if (collectThreats) {
|
|
721
836
|
return { value, wasSanitized, threats };
|
|
722
837
|
}
|
|
@@ -724,9 +839,10 @@ function sanitizePath(input, collectThreats = false) {
|
|
|
724
839
|
}
|
|
725
840
|
function detectPathTraversal(input) {
|
|
726
841
|
if (typeof input !== "string") return false;
|
|
842
|
+
const normalized = input.normalize("NFKC");
|
|
727
843
|
for (const pattern of PATH_PATTERNS) {
|
|
728
844
|
pattern.lastIndex = 0;
|
|
729
|
-
if (pattern.test(
|
|
845
|
+
if (pattern.test(normalized)) {
|
|
730
846
|
return true;
|
|
731
847
|
}
|
|
732
848
|
}
|
|
@@ -784,7 +900,7 @@ function sanitizeString(value, options = {}) {
|
|
|
784
900
|
if (value.length > maxSize) {
|
|
785
901
|
throw new InputTooLargeError(maxSize, value.length);
|
|
786
902
|
}
|
|
787
|
-
const reject = options.mode
|
|
903
|
+
const reject = options.mode === "reject";
|
|
788
904
|
let result = value;
|
|
789
905
|
if (options.sql !== false) {
|
|
790
906
|
if (reject) {
|
|
@@ -933,10 +1049,24 @@ var SSTI_DETECT_PATTERNS = [
|
|
|
933
1049
|
/\{\{\s*(?:self|request|lipsum|cycler|joiner|namespace|range)\b/gi
|
|
934
1050
|
];
|
|
935
1051
|
var SSTI_REMOVE_PATTERNS = [
|
|
1052
|
+
/** Jinja2 / Twig: {{ ... }} — always strip (not valid in any JS context) */
|
|
936
1053
|
/\{\{.*?\}\}/g,
|
|
937
|
-
|
|
1054
|
+
/**
|
|
1055
|
+
* Freemarker / Spring EL: ${...} — strip when expression contains operators,
|
|
1056
|
+
* method calls, or Python dunder patterns (sandbox escape).
|
|
1057
|
+
* Bare ${name} and ${user.name} are left intact (JS template literal syntax).
|
|
1058
|
+
*/
|
|
1059
|
+
/\$\{[^}]*__\w+__[^}]*\}/g,
|
|
1060
|
+
/\$\{[^}]*[?!()*+\-/][^}]*\}/g,
|
|
1061
|
+
/** ERB / EJS: <%= ... %> */
|
|
938
1062
|
/<%[=\-]?.*?%>/gs,
|
|
939
|
-
|
|
1063
|
+
/**
|
|
1064
|
+
* Pug / Jade: #{...} — same narrowing as ${ above, plus dunder detection.
|
|
1065
|
+
* #{name} output expressions are left intact.
|
|
1066
|
+
*/
|
|
1067
|
+
/#\{[^}]*__\w+__[^}]*\}/g,
|
|
1068
|
+
/#\{[^}]*[?!()*+\-/][^}]*\}/g,
|
|
1069
|
+
/** Python dunder sandbox escape — always strip */
|
|
940
1070
|
/__(?:class|mro|subclasses|globals|builtins|import)__/gi
|
|
941
1071
|
];
|
|
942
1072
|
function sanitizeSsti(input, collectThreats = false) {
|
|
@@ -983,6 +1113,8 @@ function detectSsti(input) {
|
|
|
983
1113
|
}
|
|
984
1114
|
|
|
985
1115
|
// src/sanitizers/xxe.ts
|
|
1116
|
+
var MAX_XXE_INPUT_BYTES = 1e6;
|
|
1117
|
+
var MAX_ENTITY_REFERENCES = 64;
|
|
986
1118
|
var XXE_DETECT_PATTERNS = [
|
|
987
1119
|
/** DOCTYPE declaration */
|
|
988
1120
|
/<!DOCTYPE\b/gi,
|
|
@@ -1012,6 +1144,19 @@ function sanitizeXxe(input, collectThreats = false) {
|
|
|
1012
1144
|
const threats = [];
|
|
1013
1145
|
let value = input;
|
|
1014
1146
|
let wasSanitized = false;
|
|
1147
|
+
if (value.length > MAX_XXE_INPUT_BYTES) {
|
|
1148
|
+
if (collectThreats) {
|
|
1149
|
+
threats.push({ type: "xxe", pattern: "oversize_input", original: `length=${value.length}` });
|
|
1150
|
+
}
|
|
1151
|
+
return collectThreats ? { value: "", wasSanitized: true, threats } : "";
|
|
1152
|
+
}
|
|
1153
|
+
const entityRefs = value.match(/&\w+;/g);
|
|
1154
|
+
if (entityRefs && entityRefs.length > MAX_ENTITY_REFERENCES) {
|
|
1155
|
+
if (collectThreats) {
|
|
1156
|
+
threats.push({ type: "xxe", pattern: "entity_expansion", original: `count=${entityRefs.length}` });
|
|
1157
|
+
}
|
|
1158
|
+
return collectThreats ? { value: "", wasSanitized: true, threats } : "";
|
|
1159
|
+
}
|
|
1015
1160
|
for (const pattern of XXE_REMOVE_PATTERNS) {
|
|
1016
1161
|
pattern.lastIndex = 0;
|
|
1017
1162
|
if (pattern.test(value)) {
|
|
@@ -1049,12 +1194,10 @@ function detectXxe(input) {
|
|
|
1049
1194
|
}
|
|
1050
1195
|
|
|
1051
1196
|
// src/sanitizers/jsonp.ts
|
|
1052
|
-
var SAFE_CALLBACK_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$.
|
|
1197
|
+
var SAFE_CALLBACK_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/;
|
|
1053
1198
|
var DANGEROUS_CALLBACK_PATTERNS = [
|
|
1054
|
-
|
|
1199
|
+
/\.\./
|
|
1055
1200
|
// prototype chain traversal
|
|
1056
|
-
/\[\s*\]/
|
|
1057
|
-
// empty bracket access
|
|
1058
1201
|
];
|
|
1059
1202
|
function sanitizeJsonpCallback(callback, maxLength = 128) {
|
|
1060
1203
|
if (typeof callback !== "string" || callback.length === 0) {
|
|
@@ -1139,7 +1282,7 @@ function detectHeaderInjection(input) {
|
|
|
1139
1282
|
|
|
1140
1283
|
// src/sanitizers/pii.ts
|
|
1141
1284
|
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;
|
|
1142
|
-
var PHONE_RE = /(?:\+?1[-.\s]?)?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g;
|
|
1285
|
+
var PHONE_RE = /(?<!\d)(?:\+?1[-.\s]?)?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}(?!\d)/g;
|
|
1143
1286
|
var CREDIT_CARD_RE = /\b(?:\d[ -]*?){13,19}\b/g;
|
|
1144
1287
|
var SSN_RE = /\b\d{3}[-\s]\d{2}[-\s]\d{4}\b/g;
|
|
1145
1288
|
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;
|
|
@@ -1300,16 +1443,18 @@ function encodeForAttribute(value) {
|
|
|
1300
1443
|
function encodeForJs(value) {
|
|
1301
1444
|
if (!value) return "";
|
|
1302
1445
|
let result = "";
|
|
1303
|
-
for (
|
|
1304
|
-
const
|
|
1305
|
-
if (
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
result +=
|
|
1309
|
-
} else if (
|
|
1310
|
-
result += `\\x${
|
|
1446
|
+
for (const char of value) {
|
|
1447
|
+
const cp = char.codePointAt(0);
|
|
1448
|
+
if (cp >= 48 && cp <= 57 || // 0-9
|
|
1449
|
+
cp >= 65 && cp <= 90 || // A-Z
|
|
1450
|
+
cp >= 97 && cp <= 122) {
|
|
1451
|
+
result += char;
|
|
1452
|
+
} else if (cp < 256) {
|
|
1453
|
+
result += `\\x${cp.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
1454
|
+
} else if (cp <= 65535) {
|
|
1455
|
+
result += `\\u${cp.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
1311
1456
|
} else {
|
|
1312
|
-
result += `\\u${
|
|
1457
|
+
result += `\\u{${cp.toString(16).toUpperCase()}}`;
|
|
1313
1458
|
}
|
|
1314
1459
|
}
|
|
1315
1460
|
return result;
|
|
@@ -1762,8 +1907,12 @@ function checkPrivateIp(hostname) {
|
|
|
1762
1907
|
if (hostname === "metadata.google.internal" || hostname === "metadata.internal" || hostname === "metadata.azure.internal") {
|
|
1763
1908
|
return "cloud metadata endpoint";
|
|
1764
1909
|
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1910
|
+
let ipv6 = hostname.replace(/^\[|\]$/g, "");
|
|
1911
|
+
const zoneIdx = ipv6.indexOf("%");
|
|
1912
|
+
if (zoneIdx !== -1) {
|
|
1913
|
+
ipv6 = ipv6.slice(0, zoneIdx);
|
|
1914
|
+
}
|
|
1915
|
+
if (ipv6 === "::1" || ipv6 === "::" || /^fc[0-9a-f]{2}:/i.test(ipv6) || /^fd[0-9a-f]{2}:/i.test(ipv6) || /^fe80:/i.test(ipv6) || /^ff[0-9a-f]{2}:/i.test(ipv6)) {
|
|
1767
1916
|
return "private IPv6 address";
|
|
1768
1917
|
}
|
|
1769
1918
|
const mappedDotted = ipv6.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
|
|
@@ -2257,10 +2406,192 @@ function createRedactor(sensitiveKeys = []) {
|
|
|
2257
2406
|
}
|
|
2258
2407
|
var safeLog = createSafeLogger;
|
|
2259
2408
|
|
|
2409
|
+
// src/telemetry/client.ts
|
|
2410
|
+
var DEFAULT_BATCH_SIZE = 50;
|
|
2411
|
+
var MAX_BATCH_SIZE = 500;
|
|
2412
|
+
var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
|
|
2413
|
+
var MIN_FLUSH_INTERVAL_MS = 500;
|
|
2414
|
+
var FLUSH_TIMEOUT_MS = 1e4;
|
|
2415
|
+
var TelemetryClient = class {
|
|
2416
|
+
constructor(options) {
|
|
2417
|
+
this.queue = [];
|
|
2418
|
+
this.flushing = false;
|
|
2419
|
+
this.closed = false;
|
|
2420
|
+
if (!options.endpoint || typeof options.endpoint !== "string") {
|
|
2421
|
+
throw new TypeError("TelemetryClient: `endpoint` is required");
|
|
2422
|
+
}
|
|
2423
|
+
this.endpoint = options.endpoint;
|
|
2424
|
+
this.apiKey = options.apiKey;
|
|
2425
|
+
this.workspaceId = options.workspaceId;
|
|
2426
|
+
this.batchSize = clamp(
|
|
2427
|
+
options.batchSize ?? DEFAULT_BATCH_SIZE,
|
|
2428
|
+
1,
|
|
2429
|
+
MAX_BATCH_SIZE
|
|
2430
|
+
);
|
|
2431
|
+
this.flushIntervalMs = Math.max(
|
|
2432
|
+
options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
2433
|
+
MIN_FLUSH_INTERVAL_MS
|
|
2434
|
+
);
|
|
2435
|
+
this.onError = options.onError ?? (() => {
|
|
2436
|
+
});
|
|
2437
|
+
this.startTimer();
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* Enqueue an event. Fast, synchronous, cannot throw.
|
|
2441
|
+
* Triggers a flush if the queue has reached `batchSize`.
|
|
2442
|
+
*/
|
|
2443
|
+
record(event) {
|
|
2444
|
+
if (this.closed) return;
|
|
2445
|
+
this.queue.push(event);
|
|
2446
|
+
if (this.queue.length >= this.batchSize) {
|
|
2447
|
+
void this.flush();
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
2451
|
+
* Manually flush the queue. Pulls up to `batchSize` events into a batch and
|
|
2452
|
+
* POSTs them. Returns a resolved promise on success OR on handled failure.
|
|
2453
|
+
* Never throws.
|
|
2454
|
+
*/
|
|
2455
|
+
async flush() {
|
|
2456
|
+
if (this.flushing) return;
|
|
2457
|
+
if (this.queue.length === 0) return;
|
|
2458
|
+
this.flushing = true;
|
|
2459
|
+
try {
|
|
2460
|
+
const batch = this.queue.splice(0, this.batchSize);
|
|
2461
|
+
await this.send(batch);
|
|
2462
|
+
} catch (err) {
|
|
2463
|
+
this.safeNotify(err);
|
|
2464
|
+
} finally {
|
|
2465
|
+
this.flushing = false;
|
|
2466
|
+
}
|
|
2467
|
+
if (!this.closed && this.queue.length > 0) {
|
|
2468
|
+
void this.flush();
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
/**
|
|
2472
|
+
* Shut down: stop the interval timer and attempt one final flush.
|
|
2473
|
+
* Safe to call multiple times.
|
|
2474
|
+
*/
|
|
2475
|
+
async close() {
|
|
2476
|
+
if (this.closed) return;
|
|
2477
|
+
this.closed = true;
|
|
2478
|
+
if (this.timer !== void 0) {
|
|
2479
|
+
clearInterval(this.timer);
|
|
2480
|
+
this.timer = void 0;
|
|
2481
|
+
}
|
|
2482
|
+
if (this.signalHandler !== void 0) {
|
|
2483
|
+
process.off("SIGTERM", this.signalHandler);
|
|
2484
|
+
process.off("SIGINT", this.signalHandler);
|
|
2485
|
+
this.signalHandler = void 0;
|
|
2486
|
+
}
|
|
2487
|
+
try {
|
|
2488
|
+
await this.flush();
|
|
2489
|
+
} catch {
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
/**
|
|
2493
|
+
* Register `SIGTERM` / `SIGINT` handlers that call `close()` to drain
|
|
2494
|
+
* the queue on graceful shutdown. Opt-in — libraries should not silently
|
|
2495
|
+
* attach global signal handlers. Safe to call multiple times.
|
|
2496
|
+
*/
|
|
2497
|
+
installShutdownHooks() {
|
|
2498
|
+
if (this.signalHandler !== void 0 || this.closed) return;
|
|
2499
|
+
const handler = () => {
|
|
2500
|
+
void this.close();
|
|
2501
|
+
};
|
|
2502
|
+
this.signalHandler = handler;
|
|
2503
|
+
process.once("SIGTERM", handler);
|
|
2504
|
+
process.once("SIGINT", handler);
|
|
2505
|
+
}
|
|
2506
|
+
/** Count of events currently waiting to be sent. Useful for tests. */
|
|
2507
|
+
get pendingCount() {
|
|
2508
|
+
return this.queue.length;
|
|
2509
|
+
}
|
|
2510
|
+
// ── internals ─────────────────────────────────────────────────────────
|
|
2511
|
+
async send(batch) {
|
|
2512
|
+
const headers = {
|
|
2513
|
+
"content-type": "application/json"
|
|
2514
|
+
};
|
|
2515
|
+
if (this.apiKey) headers["authorization"] = `Bearer ${this.apiKey}`;
|
|
2516
|
+
if (this.workspaceId) headers["x-workspace-id"] = this.workspaceId;
|
|
2517
|
+
const controller = new AbortController();
|
|
2518
|
+
const abortTimer = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
|
|
2519
|
+
try {
|
|
2520
|
+
const res = await fetch(this.endpoint, {
|
|
2521
|
+
method: "POST",
|
|
2522
|
+
headers,
|
|
2523
|
+
body: JSON.stringify({ events: batch }),
|
|
2524
|
+
signal: controller.signal
|
|
2525
|
+
});
|
|
2526
|
+
if (!res.ok) {
|
|
2527
|
+
const text = await safeReadBody(res);
|
|
2528
|
+
throw new TelemetryHttpError(res.status, text);
|
|
2529
|
+
}
|
|
2530
|
+
} finally {
|
|
2531
|
+
clearTimeout(abortTimer);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
startTimer() {
|
|
2535
|
+
this.timer = setInterval(() => {
|
|
2536
|
+
void this.flush();
|
|
2537
|
+
}, this.flushIntervalMs);
|
|
2538
|
+
this.timer.unref?.();
|
|
2539
|
+
}
|
|
2540
|
+
safeNotify(err) {
|
|
2541
|
+
try {
|
|
2542
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
2543
|
+
} catch {
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
};
|
|
2547
|
+
var TelemetryHttpError = class extends Error {
|
|
2548
|
+
constructor(status, responseBody) {
|
|
2549
|
+
super(`Telemetry ingest returned HTTP ${status}`);
|
|
2550
|
+
this.status = status;
|
|
2551
|
+
this.responseBody = responseBody;
|
|
2552
|
+
this.name = "TelemetryHttpError";
|
|
2553
|
+
}
|
|
2554
|
+
};
|
|
2555
|
+
function clamp(value, min, max) {
|
|
2556
|
+
if (!Number.isFinite(value)) return min;
|
|
2557
|
+
return Math.max(min, Math.min(max, Math.trunc(value)));
|
|
2558
|
+
}
|
|
2559
|
+
async function safeReadBody(res) {
|
|
2560
|
+
try {
|
|
2561
|
+
const text = await res.text();
|
|
2562
|
+
return text.slice(0, 500);
|
|
2563
|
+
} catch {
|
|
2564
|
+
return "";
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2260
2568
|
// src/middleware/main.ts
|
|
2569
|
+
function buildTelemetryFromEnv() {
|
|
2570
|
+
const env = typeof process !== "undefined" ? process.env : void 0;
|
|
2571
|
+
const endpoint = env?.ARCIS_ENDPOINT;
|
|
2572
|
+
if (!endpoint) return void 0;
|
|
2573
|
+
const opts = { endpoint };
|
|
2574
|
+
if (env?.ARCIS_WORKSPACE_ID) opts.workspaceId = env.ARCIS_WORKSPACE_ID;
|
|
2575
|
+
if (env?.ARCIS_KEY) opts.apiKey = env.ARCIS_KEY;
|
|
2576
|
+
const batch = env?.ARCIS_BATCH_SIZE ? parseInt(env.ARCIS_BATCH_SIZE, 10) : NaN;
|
|
2577
|
+
if (!Number.isNaN(batch)) opts.batchSize = batch;
|
|
2578
|
+
const flush = env?.ARCIS_FLUSH_INTERVAL_MS ? parseInt(env.ARCIS_FLUSH_INTERVAL_MS, 10) : NaN;
|
|
2579
|
+
if (!Number.isNaN(flush)) opts.flushIntervalMs = flush;
|
|
2580
|
+
return opts;
|
|
2581
|
+
}
|
|
2261
2582
|
function arcis(options = {}) {
|
|
2262
2583
|
const middlewares = [];
|
|
2263
2584
|
const cleanupFns = [];
|
|
2585
|
+
let telemetryClient;
|
|
2586
|
+
const telemetryOpts = options.telemetry?.endpoint ? options.telemetry : buildTelemetryFromEnv();
|
|
2587
|
+
if (telemetryOpts) {
|
|
2588
|
+
const client = new TelemetryClient(telemetryOpts);
|
|
2589
|
+
telemetryClient = client;
|
|
2590
|
+
middlewares.push(createTelemetryEmitter(client));
|
|
2591
|
+
cleanupFns.push(() => {
|
|
2592
|
+
void client.close();
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2264
2595
|
if (options.headers !== false) {
|
|
2265
2596
|
const headerOpts = typeof options.headers === "object" ? options.headers : {};
|
|
2266
2597
|
middlewares.push(createHeaders(headerOpts));
|
|
@@ -2273,7 +2604,8 @@ function arcis(options = {}) {
|
|
|
2273
2604
|
}
|
|
2274
2605
|
if (options.sanitize !== false) {
|
|
2275
2606
|
const sanitizeOpts = typeof options.sanitize === "object" ? options.sanitize : {};
|
|
2276
|
-
|
|
2607
|
+
const sanitizer = createSanitizer(sanitizeOpts);
|
|
2608
|
+
middlewares.push(telemetryClient ? tapSanitizerThreats(sanitizer) : sanitizer);
|
|
2277
2609
|
}
|
|
2278
2610
|
const result = middlewares;
|
|
2279
2611
|
result.close = () => {
|
|
@@ -2600,6 +2932,16 @@ function secureCookieDefaults(options = {}) {
|
|
|
2600
2932
|
sameSite: options.sameSite ?? "Lax",
|
|
2601
2933
|
path: options.path
|
|
2602
2934
|
};
|
|
2935
|
+
if (resolved.sameSite === "None" && resolved.secure === false) {
|
|
2936
|
+
throw new Error(
|
|
2937
|
+
"[arcis] secureCookieDefaults: sameSite=None requires secure=true (modern browsers reject the cookie otherwise)"
|
|
2938
|
+
);
|
|
2939
|
+
}
|
|
2940
|
+
if (resolved.httpOnly === false && resolved.secure === false && isProduction) {
|
|
2941
|
+
console.warn(
|
|
2942
|
+
"[arcis] secureCookieDefaults: httpOnly and secure are both disabled in production \u2014 cookies will be readable by JS and sent over HTTP"
|
|
2943
|
+
);
|
|
2944
|
+
}
|
|
2603
2945
|
return (_req, res, next) => {
|
|
2604
2946
|
const originalSetHeader = res.setHeader.bind(res);
|
|
2605
2947
|
res.setHeader = function patchedSetHeader(name, value) {
|
|
@@ -2732,7 +3074,16 @@ function detectBehavioralSignals(req) {
|
|
|
2732
3074
|
}
|
|
2733
3075
|
function detectBot(req) {
|
|
2734
3076
|
const rawUa = req.headers["user-agent"] ?? "";
|
|
2735
|
-
|
|
3077
|
+
if (rawUa.length > 2048) {
|
|
3078
|
+
return {
|
|
3079
|
+
isBot: true,
|
|
3080
|
+
category: "UNKNOWN",
|
|
3081
|
+
name: null,
|
|
3082
|
+
confidence: 0.9,
|
|
3083
|
+
signals: detectBehavioralSignals(req)
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
const ua = rawUa;
|
|
2736
3087
|
const signals = detectBehavioralSignals(req);
|
|
2737
3088
|
if (!ua) {
|
|
2738
3089
|
return {
|
|
@@ -2822,11 +3173,9 @@ function generateCsrfToken(length = 32) {
|
|
|
2822
3173
|
function validateCsrfToken(cookieToken, requestToken) {
|
|
2823
3174
|
if (!cookieToken || !requestToken) return false;
|
|
2824
3175
|
if (cookieToken.length !== requestToken.length) return false;
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
}
|
|
2829
|
-
return result === 0;
|
|
3176
|
+
const a = Buffer.from(cookieToken);
|
|
3177
|
+
const b = Buffer.from(requestToken);
|
|
3178
|
+
return crypto.timingSafeEqual(a, b);
|
|
2830
3179
|
}
|
|
2831
3180
|
function getRequestToken(req, headerName, fieldName) {
|
|
2832
3181
|
const headerToken = req.headers[headerName.toLowerCase()];
|
|
@@ -2835,10 +3184,6 @@ function getRequestToken(req, headerName, fieldName) {
|
|
|
2835
3184
|
const bodyToken = req.body[fieldName];
|
|
2836
3185
|
if (typeof bodyToken === "string" && bodyToken) return bodyToken;
|
|
2837
3186
|
}
|
|
2838
|
-
if (req.query && fieldName in req.query) {
|
|
2839
|
-
const queryToken = req.query[fieldName];
|
|
2840
|
-
if (typeof queryToken === "string" && queryToken) return queryToken;
|
|
2841
|
-
}
|
|
2842
3187
|
return void 0;
|
|
2843
3188
|
}
|
|
2844
3189
|
function csrfProtection(options = {}) {
|
|
@@ -2902,6 +3247,10 @@ function csrfProtection(options = {}) {
|
|
|
2902
3247
|
if (!validateCsrfToken(cookieToken, requestToken)) {
|
|
2903
3248
|
return onError(req, res, next);
|
|
2904
3249
|
}
|
|
3250
|
+
if (options.rotateOnUse) {
|
|
3251
|
+
const freshToken = generateCsrfToken(tokenLength);
|
|
3252
|
+
setCsrfCookie(res, cookieName, freshToken, cookieOpts);
|
|
3253
|
+
}
|
|
2905
3254
|
next();
|
|
2906
3255
|
};
|
|
2907
3256
|
}
|
|
@@ -2921,7 +3270,15 @@ function setCsrfCookie(res, name, token, opts) {
|
|
|
2921
3270
|
if (opts.secure) parts.push("Secure");
|
|
2922
3271
|
parts.push(`SameSite=${opts.sameSite}`);
|
|
2923
3272
|
if (opts.domain) parts.push(`Domain=${opts.domain}`);
|
|
2924
|
-
|
|
3273
|
+
const newCookie = parts.join("; ");
|
|
3274
|
+
const existing = res.getHeader("Set-Cookie");
|
|
3275
|
+
if (existing === void 0) {
|
|
3276
|
+
res.setHeader("Set-Cookie", newCookie);
|
|
3277
|
+
} else if (Array.isArray(existing)) {
|
|
3278
|
+
res.setHeader("Set-Cookie", [...existing, newCookie]);
|
|
3279
|
+
} else {
|
|
3280
|
+
res.setHeader("Set-Cookie", [existing, newCookie]);
|
|
3281
|
+
}
|
|
2925
3282
|
}
|
|
2926
3283
|
function escapeRegex(str) {
|
|
2927
3284
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -2930,7 +3287,7 @@ var createCsrf = csrfProtection;
|
|
|
2930
3287
|
|
|
2931
3288
|
// src/middleware/hpp.ts
|
|
2932
3289
|
function hpp(options = {}) {
|
|
2933
|
-
const whitelist = new Set(options.whitelist ?? []);
|
|
3290
|
+
const whitelist = new Set((options.whitelist ?? []).map((k) => k.toLowerCase()));
|
|
2934
3291
|
const checkQuery = options.checkQuery ?? true;
|
|
2935
3292
|
const checkBody = options.checkBody ?? true;
|
|
2936
3293
|
return (req, _res, next) => {
|
|
@@ -2940,7 +3297,7 @@ function hpp(options = {}) {
|
|
|
2940
3297
|
for (const [key, value] of Object.entries(req.query)) {
|
|
2941
3298
|
if (Array.isArray(value)) {
|
|
2942
3299
|
const strings = value.filter((v) => typeof v === "string");
|
|
2943
|
-
if (whitelist.has(key)) {
|
|
3300
|
+
if (whitelist.has(key.toLowerCase())) {
|
|
2944
3301
|
clean[key] = strings;
|
|
2945
3302
|
} else {
|
|
2946
3303
|
polluted[key] = strings;
|
|
@@ -2958,7 +3315,7 @@ function hpp(options = {}) {
|
|
|
2958
3315
|
const clean = {};
|
|
2959
3316
|
for (const [key, value] of Object.entries(req.body)) {
|
|
2960
3317
|
if (Array.isArray(value)) {
|
|
2961
|
-
if (whitelist.has(key)) {
|
|
3318
|
+
if (whitelist.has(key.toLowerCase())) {
|
|
2962
3319
|
clean[key] = value;
|
|
2963
3320
|
} else {
|
|
2964
3321
|
polluted[key] = value;
|
|
@@ -2976,6 +3333,89 @@ function hpp(options = {}) {
|
|
|
2976
3333
|
}
|
|
2977
3334
|
var createHpp = hpp;
|
|
2978
3335
|
|
|
3336
|
+
// src/middleware/signup-protection.ts
|
|
3337
|
+
function checkSignup(req, options = {}) {
|
|
3338
|
+
const {
|
|
3339
|
+
emailField = "email",
|
|
3340
|
+
checkEmail = true,
|
|
3341
|
+
blockDisposable = true,
|
|
3342
|
+
checkBot = true,
|
|
3343
|
+
allowedBotCategories = [],
|
|
3344
|
+
allowedEmailDomains = [],
|
|
3345
|
+
blockedEmailDomains = []
|
|
3346
|
+
} = options;
|
|
3347
|
+
if (checkBot) {
|
|
3348
|
+
const bot = detectBot(req);
|
|
3349
|
+
if (bot.isBot && !allowedBotCategories.includes(bot.category)) {
|
|
3350
|
+
return {
|
|
3351
|
+
allowed: false,
|
|
3352
|
+
reason: "bot",
|
|
3353
|
+
details: { category: bot.category, name: bot.name, confidence: bot.confidence }
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
if (checkEmail) {
|
|
3358
|
+
const email = req.body?.[emailField];
|
|
3359
|
+
if (typeof email !== "string" || email.length === 0) {
|
|
3360
|
+
return { allowed: false, reason: "missing_email" };
|
|
3361
|
+
}
|
|
3362
|
+
const result = validateEmail(email, {
|
|
3363
|
+
checkDisposable: blockDisposable,
|
|
3364
|
+
allowedDomains: allowedEmailDomains,
|
|
3365
|
+
blockedDomains: blockedEmailDomains
|
|
3366
|
+
});
|
|
3367
|
+
if (!result.valid) {
|
|
3368
|
+
const reason = result.reason === "disposable" ? "disposable_email" : "invalid_email";
|
|
3369
|
+
return { allowed: false, reason, details: { emailReason: result.reason } };
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
return { allowed: true, reason: "ok" };
|
|
3373
|
+
}
|
|
3374
|
+
function signupProtection(options = {}) {
|
|
3375
|
+
const rateLimitCfg = options.rateLimit;
|
|
3376
|
+
const limiter = rateLimitCfg === false ? null : createRateLimiter({
|
|
3377
|
+
max: rateLimitCfg?.max ?? 5,
|
|
3378
|
+
windowMs: rateLimitCfg?.windowMs ?? 6e4,
|
|
3379
|
+
message: "Too many signup attempts"
|
|
3380
|
+
});
|
|
3381
|
+
const handler = (req, res, next) => {
|
|
3382
|
+
const result = checkSignup(req, options);
|
|
3383
|
+
if (!result.allowed) {
|
|
3384
|
+
options.onBlocked?.(req, result);
|
|
3385
|
+
const status = result.reason === "bot" ? 403 : 400;
|
|
3386
|
+
res.status(status).json({ error: "signup_blocked", reason: result.reason });
|
|
3387
|
+
return;
|
|
3388
|
+
}
|
|
3389
|
+
if (limiter) {
|
|
3390
|
+
let rateLimited = false;
|
|
3391
|
+
const rateLimitNext = (err) => {
|
|
3392
|
+
if (err) return next(err);
|
|
3393
|
+
if (!rateLimited) next();
|
|
3394
|
+
};
|
|
3395
|
+
const patchedRes = new Proxy(res, {
|
|
3396
|
+
get(target, prop) {
|
|
3397
|
+
if (prop === "status") {
|
|
3398
|
+
return (code) => {
|
|
3399
|
+
if (code === 429) {
|
|
3400
|
+
rateLimited = true;
|
|
3401
|
+
options.onBlocked?.(req, { allowed: false, reason: "rate_limited" });
|
|
3402
|
+
}
|
|
3403
|
+
return target.status.call(target, code);
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
return Reflect.get(target, prop);
|
|
3407
|
+
}
|
|
3408
|
+
});
|
|
3409
|
+
limiter(req, patchedRes, rateLimitNext);
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
next();
|
|
3413
|
+
};
|
|
3414
|
+
const middleware = handler;
|
|
3415
|
+
middleware.close = () => limiter?.close();
|
|
3416
|
+
return middleware;
|
|
3417
|
+
}
|
|
3418
|
+
|
|
2979
3419
|
// src/utils/ip.ts
|
|
2980
3420
|
var PLATFORM_HEADERS = {
|
|
2981
3421
|
cloudflare: "cf-connecting-ip",
|
|
@@ -3105,8 +3545,9 @@ function fingerprint(req, options = {}) {
|
|
|
3105
3545
|
}
|
|
3106
3546
|
|
|
3107
3547
|
// src/stores/memory.ts
|
|
3548
|
+
var DEFAULT_MAX_SIZE2 = 1e4;
|
|
3108
3549
|
var MemoryStore = class {
|
|
3109
|
-
constructor(windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS) {
|
|
3550
|
+
constructor(windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS, maxSize = DEFAULT_MAX_SIZE2) {
|
|
3110
3551
|
this.store = /* @__PURE__ */ new Map();
|
|
3111
3552
|
this.cleanupInterval = null;
|
|
3112
3553
|
if (!Number.isFinite(windowMs) || windowMs < RATE_LIMIT.MIN_WINDOW_MS) {
|
|
@@ -3114,7 +3555,11 @@ var MemoryStore = class {
|
|
|
3114
3555
|
`MemoryStore: windowMs must be a finite number >= ${RATE_LIMIT.MIN_WINDOW_MS} (got ${windowMs})`
|
|
3115
3556
|
);
|
|
3116
3557
|
}
|
|
3558
|
+
if (!Number.isFinite(maxSize) || maxSize < 1) {
|
|
3559
|
+
throw new RangeError(`MemoryStore: maxSize must be >= 1 (got ${maxSize})`);
|
|
3560
|
+
}
|
|
3117
3561
|
this.windowMs = windowMs;
|
|
3562
|
+
this.maxSize = maxSize;
|
|
3118
3563
|
this.startCleanup();
|
|
3119
3564
|
}
|
|
3120
3565
|
/**
|
|
@@ -3146,18 +3591,33 @@ var MemoryStore = class {
|
|
|
3146
3591
|
return entry;
|
|
3147
3592
|
}
|
|
3148
3593
|
async set(key, entry) {
|
|
3594
|
+
if (!this.store.has(key) && this.store.size >= this.maxSize) {
|
|
3595
|
+
this.evictExpired();
|
|
3596
|
+
if (this.store.size >= this.maxSize) return;
|
|
3597
|
+
}
|
|
3149
3598
|
this.store.set(key, entry);
|
|
3150
3599
|
}
|
|
3151
3600
|
async increment(key) {
|
|
3152
3601
|
const now = Date.now();
|
|
3153
3602
|
const entry = this.store.get(key);
|
|
3154
3603
|
if (!entry || entry.resetTime < now) {
|
|
3604
|
+
if (this.store.size >= this.maxSize) {
|
|
3605
|
+
this.evictExpired();
|
|
3606
|
+
if (this.store.size >= this.maxSize) return 1;
|
|
3607
|
+
}
|
|
3155
3608
|
this.store.set(key, { count: 1, resetTime: now + this.windowMs });
|
|
3156
3609
|
return 1;
|
|
3157
3610
|
}
|
|
3158
3611
|
entry.count++;
|
|
3159
3612
|
return entry.count;
|
|
3160
3613
|
}
|
|
3614
|
+
/** Eagerly remove expired entries to reclaim capacity. */
|
|
3615
|
+
evictExpired() {
|
|
3616
|
+
const now = Date.now();
|
|
3617
|
+
for (const [key, entry] of this.store.entries()) {
|
|
3618
|
+
if (entry.resetTime < now) this.store.delete(key);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3161
3621
|
async decrement(key) {
|
|
3162
3622
|
const entry = this.store.get(key);
|
|
3163
3623
|
if (entry && entry.count > 0) {
|
|
@@ -3253,10 +3713,13 @@ exports.RateLimitError = RateLimitError;
|
|
|
3253
3713
|
exports.RedisStore = RedisStore;
|
|
3254
3714
|
exports.SanitizationError = SanitizationError;
|
|
3255
3715
|
exports.SecurityThreatError = SecurityThreatError;
|
|
3716
|
+
exports.TelemetryClient = TelemetryClient;
|
|
3717
|
+
exports.TelemetryHttpError = TelemetryHttpError;
|
|
3256
3718
|
exports.VALIDATION = VALIDATION;
|
|
3257
3719
|
exports.arcis = arcis;
|
|
3258
3720
|
exports.arcisFunction = arcisWithMethods;
|
|
3259
3721
|
exports.botProtection = botProtection;
|
|
3722
|
+
exports.checkSignup = checkSignup;
|
|
3260
3723
|
exports.createCors = createCors;
|
|
3261
3724
|
exports.createCsrf = createCsrf;
|
|
3262
3725
|
exports.createErrorHandler = createErrorHandler;
|
|
@@ -3326,6 +3789,7 @@ exports.scanObjectPii = scanObjectPii;
|
|
|
3326
3789
|
exports.scanPii = scanPii;
|
|
3327
3790
|
exports.secureCookieDefaults = secureCookieDefaults;
|
|
3328
3791
|
exports.securityHeaders = securityHeaders;
|
|
3792
|
+
exports.signupProtection = signupProtection;
|
|
3329
3793
|
exports.validate = validate;
|
|
3330
3794
|
exports.validateCsrfToken = validateCsrfToken;
|
|
3331
3795
|
exports.validateEmail = validateEmail;
|