@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.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { promises } from 'dns';
|
|
2
|
-
import { randomBytes, createHash } from 'crypto';
|
|
2
|
+
import { randomBytes, timingSafeEqual, createHash } from 'crypto';
|
|
3
3
|
|
|
4
4
|
// src/core/constants.ts
|
|
5
5
|
var INPUT = {
|
|
@@ -66,13 +66,24 @@ var XSS_PATTERNS = [
|
|
|
66
66
|
/** URL-encoded script tags */
|
|
67
67
|
/%3Cscript/gi,
|
|
68
68
|
/** SVG with onload */
|
|
69
|
-
/<svg[^>]*onload/gi
|
|
69
|
+
/<svg[^>]*onload/gi,
|
|
70
|
+
/** form tags — phishing/credential harvesting via action= redirection */
|
|
71
|
+
/<form[\s>]/gi,
|
|
72
|
+
/** meta tags — http-equiv refresh redirects or CSP bypass */
|
|
73
|
+
/<meta[\s>]/gi,
|
|
74
|
+
/** base href hijacking — redirects all relative URLs to attacker domain */
|
|
75
|
+
/<base[\s>]/gi,
|
|
76
|
+
/** link tag injection — stylesheet or preload CSRF attacks */
|
|
77
|
+
/<link[\s>]/gi
|
|
70
78
|
];
|
|
71
79
|
var XSS_REMOVE_PATTERNS = [
|
|
72
80
|
/** Full script blocks (content + tags) */
|
|
73
81
|
/<script[^>]*>[\s\S]*?<\/script>/gi,
|
|
74
82
|
/** Standalone/unclosed script tags */
|
|
75
83
|
/<script[^>]*>/gi,
|
|
84
|
+
/** style — CSS expression() and behavior: attacks (IE-era but still relevant) */
|
|
85
|
+
/<style[^>]*>[\s\S]*?<\/style>/gi,
|
|
86
|
+
/<style[^>]*/gi,
|
|
76
87
|
/** iframe — full block and partial/unclosed */
|
|
77
88
|
/<iframe[^>]*>[\s\S]*?<\/iframe>/gi,
|
|
78
89
|
/<iframe[^>]*/gi,
|
|
@@ -93,7 +104,15 @@ var XSS_REMOVE_PATTERNS = [
|
|
|
93
104
|
/javascript\s*:/gi,
|
|
94
105
|
/vbscript\s*:/gi,
|
|
95
106
|
/** data: URIs with HTML/script content */
|
|
96
|
-
/data\s*:\s*text\/html[^>\s]*/gi
|
|
107
|
+
/data\s*:\s*text\/html[^>\s]*/gi,
|
|
108
|
+
/** form tag injection — phishing via action= redirection */
|
|
109
|
+
/<form[\s>][^>]*/gi,
|
|
110
|
+
/** meta tag injection — http-equiv refresh or CSP bypass */
|
|
111
|
+
/<meta[\s>][^>]*/gi,
|
|
112
|
+
/** base href hijacking */
|
|
113
|
+
/<base[\s>][^>]*/gi,
|
|
114
|
+
/** link tag injection — stylesheet or preload attacks */
|
|
115
|
+
/<link[\s>][^>]*/gi
|
|
97
116
|
];
|
|
98
117
|
var SQL_PATTERNS = [
|
|
99
118
|
/** SQL keywords */
|
|
@@ -157,8 +176,8 @@ var COMMAND_PATTERNS = [
|
|
|
157
176
|
/[;&|`]/g,
|
|
158
177
|
/** Command substitution: $( ... ) — matched as a pair to reduce false positives */
|
|
159
178
|
/\$\(/g,
|
|
160
|
-
/** URL-encoded
|
|
161
|
-
/%0[
|
|
179
|
+
/** URL-encoded control characters (%00-%0F): null, tab, vtab, formfeed, LF, CR */
|
|
180
|
+
/%0[0-9a-f]/gi
|
|
162
181
|
];
|
|
163
182
|
var DANGEROUS_PROTO_KEYS = /* @__PURE__ */ new Set([
|
|
164
183
|
"__proto__",
|
|
@@ -380,13 +399,13 @@ function createRateLimiter(options = {}) {
|
|
|
380
399
|
statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
|
|
381
400
|
keyGenerator = (req) => {
|
|
382
401
|
const ip = req.ip ?? req.socket?.remoteAddress;
|
|
383
|
-
if (
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
return
|
|
402
|
+
if (ip) return ip;
|
|
403
|
+
const ua = req.headers["user-agent"] ?? "";
|
|
404
|
+
const lang = req.headers["accept-language"] ?? "";
|
|
405
|
+
const fp = `${ua}|${lang}`;
|
|
406
|
+
let hash = 0;
|
|
407
|
+
for (let i = 0; i < fp.length; i++) hash = (hash << 5) - hash + fp.charCodeAt(i) | 0;
|
|
408
|
+
return `unknown:${hash.toString(36)}`;
|
|
390
409
|
},
|
|
391
410
|
skip,
|
|
392
411
|
store: externalStore
|
|
@@ -449,7 +468,24 @@ function createRateLimiter(options = {}) {
|
|
|
449
468
|
}
|
|
450
469
|
next();
|
|
451
470
|
} catch (error) {
|
|
452
|
-
console.error("[arcis] Rate limiter error:", error);
|
|
471
|
+
console.error("[arcis] Rate limiter store error, using in-memory fallback:", error);
|
|
472
|
+
try {
|
|
473
|
+
const key = keyGenerator(req);
|
|
474
|
+
const now = Date.now();
|
|
475
|
+
if (!inMemoryStore[key] || inMemoryStore[key].resetTime < now) {
|
|
476
|
+
inMemoryStore[key] = { count: 1, resetTime: now + windowMs };
|
|
477
|
+
} else {
|
|
478
|
+
inMemoryStore[key].count++;
|
|
479
|
+
}
|
|
480
|
+
const count = inMemoryStore[key].count;
|
|
481
|
+
if (count > max) {
|
|
482
|
+
const resetSeconds = Math.ceil((inMemoryStore[key].resetTime - now) / 1e3);
|
|
483
|
+
res.setHeader("Retry-After", resetSeconds.toString());
|
|
484
|
+
res.status(statusCode).json({ error: message, retryAfter: resetSeconds });
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
} catch {
|
|
488
|
+
}
|
|
453
489
|
next();
|
|
454
490
|
}
|
|
455
491
|
};
|
|
@@ -581,6 +617,80 @@ var SanitizationError = class extends ArcisError {
|
|
|
581
617
|
}
|
|
582
618
|
};
|
|
583
619
|
|
|
620
|
+
// src/middleware/telemetry.ts
|
|
621
|
+
var THREAT_TO_VECTOR = {
|
|
622
|
+
xss: "xss",
|
|
623
|
+
sql_injection: "sql",
|
|
624
|
+
nosql_injection: "nosql",
|
|
625
|
+
path_traversal: "path",
|
|
626
|
+
command_injection: "command",
|
|
627
|
+
prototype_pollution: "prototype",
|
|
628
|
+
header_injection: "header",
|
|
629
|
+
ssti: "ssti",
|
|
630
|
+
xxe: "xxe"
|
|
631
|
+
};
|
|
632
|
+
function createTelemetryEmitter(client) {
|
|
633
|
+
return (req, res, next) => {
|
|
634
|
+
const start = performance.now();
|
|
635
|
+
res.on("finish", () => {
|
|
636
|
+
try {
|
|
637
|
+
const event = buildEvent(req, res.statusCode, performance.now() - start);
|
|
638
|
+
client.record(event);
|
|
639
|
+
} catch {
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
next();
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
function tapSanitizerThreats(handler) {
|
|
646
|
+
return (req, res, next) => {
|
|
647
|
+
handler(req, res, (err) => {
|
|
648
|
+
if (err instanceof SecurityThreatError) {
|
|
649
|
+
const vector = THREAT_TO_VECTOR[err.threatType] ?? err.threatType;
|
|
650
|
+
req.__arcis = {
|
|
651
|
+
vector,
|
|
652
|
+
rule: `${vector}/match`,
|
|
653
|
+
severity: "high",
|
|
654
|
+
matchedPattern: err.pattern,
|
|
655
|
+
reason: err.message,
|
|
656
|
+
decision: "deny"
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
next(err);
|
|
660
|
+
});
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function buildEvent(req, status, latencyMs) {
|
|
664
|
+
const marker = req.__arcis;
|
|
665
|
+
const decision = marker?.decision ?? inferDecision(status);
|
|
666
|
+
return {
|
|
667
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
668
|
+
ip: extractIp(req),
|
|
669
|
+
method: (req.method ?? "GET").toUpperCase(),
|
|
670
|
+
path: req.path ?? req.url ?? "/",
|
|
671
|
+
decision,
|
|
672
|
+
vector: marker?.vector ?? (status === 429 ? "rate-limit" : void 0),
|
|
673
|
+
rule: marker?.rule ?? (status === 429 ? "rate-limit/exceeded" : void 0),
|
|
674
|
+
severity: marker?.severity ?? (status === 429 ? "medium" : void 0),
|
|
675
|
+
userAgent: typeof req.headers?.["user-agent"] === "string" ? req.headers["user-agent"] : "",
|
|
676
|
+
reason: marker?.reason,
|
|
677
|
+
status,
|
|
678
|
+
matchedPattern: marker?.matchedPattern,
|
|
679
|
+
latencyMs: Math.max(0, latencyMs)
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
function inferDecision(status) {
|
|
683
|
+
if (status === 429) return "deny";
|
|
684
|
+
if (status === 400) return "deny";
|
|
685
|
+
if (status === 403) return "deny";
|
|
686
|
+
return "allow";
|
|
687
|
+
}
|
|
688
|
+
function extractIp(req) {
|
|
689
|
+
if (typeof req.ip === "string" && req.ip.length > 0) return req.ip;
|
|
690
|
+
const remote = req.socket?.remoteAddress;
|
|
691
|
+
return typeof remote === "string" ? remote : "0.0.0.0";
|
|
692
|
+
}
|
|
693
|
+
|
|
584
694
|
// src/sanitizers/utils.ts
|
|
585
695
|
function encodeHtmlEntities(str) {
|
|
586
696
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -693,26 +803,31 @@ function sanitizePath(input, collectThreats = false) {
|
|
|
693
803
|
const threats = [];
|
|
694
804
|
let value = input;
|
|
695
805
|
let wasSanitized = false;
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
806
|
+
value = value.normalize("NFKC");
|
|
807
|
+
let prev;
|
|
808
|
+
do {
|
|
809
|
+
prev = value;
|
|
810
|
+
for (const pattern of PATH_PATTERNS) {
|
|
699
811
|
pattern.lastIndex = 0;
|
|
700
|
-
if (
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
812
|
+
if (pattern.test(value)) {
|
|
813
|
+
pattern.lastIndex = 0;
|
|
814
|
+
if (collectThreats) {
|
|
815
|
+
const matches = value.match(pattern);
|
|
816
|
+
if (matches) {
|
|
817
|
+
for (const match of matches) {
|
|
818
|
+
threats.push({
|
|
819
|
+
type: "path_traversal",
|
|
820
|
+
pattern: pattern.source,
|
|
821
|
+
original: match
|
|
822
|
+
});
|
|
823
|
+
}
|
|
709
824
|
}
|
|
710
825
|
}
|
|
826
|
+
value = value.replace(pattern, "");
|
|
827
|
+
wasSanitized = true;
|
|
711
828
|
}
|
|
712
|
-
value = value.replace(pattern, "");
|
|
713
|
-
wasSanitized = true;
|
|
714
829
|
}
|
|
715
|
-
}
|
|
830
|
+
} while (value !== prev);
|
|
716
831
|
if (collectThreats) {
|
|
717
832
|
return { value, wasSanitized, threats };
|
|
718
833
|
}
|
|
@@ -720,9 +835,10 @@ function sanitizePath(input, collectThreats = false) {
|
|
|
720
835
|
}
|
|
721
836
|
function detectPathTraversal(input) {
|
|
722
837
|
if (typeof input !== "string") return false;
|
|
838
|
+
const normalized = input.normalize("NFKC");
|
|
723
839
|
for (const pattern of PATH_PATTERNS) {
|
|
724
840
|
pattern.lastIndex = 0;
|
|
725
|
-
if (pattern.test(
|
|
841
|
+
if (pattern.test(normalized)) {
|
|
726
842
|
return true;
|
|
727
843
|
}
|
|
728
844
|
}
|
|
@@ -780,7 +896,7 @@ function sanitizeString(value, options = {}) {
|
|
|
780
896
|
if (value.length > maxSize) {
|
|
781
897
|
throw new InputTooLargeError(maxSize, value.length);
|
|
782
898
|
}
|
|
783
|
-
const reject = options.mode
|
|
899
|
+
const reject = options.mode === "reject";
|
|
784
900
|
let result = value;
|
|
785
901
|
if (options.sql !== false) {
|
|
786
902
|
if (reject) {
|
|
@@ -929,10 +1045,24 @@ var SSTI_DETECT_PATTERNS = [
|
|
|
929
1045
|
/\{\{\s*(?:self|request|lipsum|cycler|joiner|namespace|range)\b/gi
|
|
930
1046
|
];
|
|
931
1047
|
var SSTI_REMOVE_PATTERNS = [
|
|
1048
|
+
/** Jinja2 / Twig: {{ ... }} — always strip (not valid in any JS context) */
|
|
932
1049
|
/\{\{.*?\}\}/g,
|
|
933
|
-
|
|
1050
|
+
/**
|
|
1051
|
+
* Freemarker / Spring EL: ${...} — strip when expression contains operators,
|
|
1052
|
+
* method calls, or Python dunder patterns (sandbox escape).
|
|
1053
|
+
* Bare ${name} and ${user.name} are left intact (JS template literal syntax).
|
|
1054
|
+
*/
|
|
1055
|
+
/\$\{[^}]*__\w+__[^}]*\}/g,
|
|
1056
|
+
/\$\{[^}]*[?!()*+\-/][^}]*\}/g,
|
|
1057
|
+
/** ERB / EJS: <%= ... %> */
|
|
934
1058
|
/<%[=\-]?.*?%>/gs,
|
|
935
|
-
|
|
1059
|
+
/**
|
|
1060
|
+
* Pug / Jade: #{...} — same narrowing as ${ above, plus dunder detection.
|
|
1061
|
+
* #{name} output expressions are left intact.
|
|
1062
|
+
*/
|
|
1063
|
+
/#\{[^}]*__\w+__[^}]*\}/g,
|
|
1064
|
+
/#\{[^}]*[?!()*+\-/][^}]*\}/g,
|
|
1065
|
+
/** Python dunder sandbox escape — always strip */
|
|
936
1066
|
/__(?:class|mro|subclasses|globals|builtins|import)__/gi
|
|
937
1067
|
];
|
|
938
1068
|
function sanitizeSsti(input, collectThreats = false) {
|
|
@@ -979,6 +1109,8 @@ function detectSsti(input) {
|
|
|
979
1109
|
}
|
|
980
1110
|
|
|
981
1111
|
// src/sanitizers/xxe.ts
|
|
1112
|
+
var MAX_XXE_INPUT_BYTES = 1e6;
|
|
1113
|
+
var MAX_ENTITY_REFERENCES = 64;
|
|
982
1114
|
var XXE_DETECT_PATTERNS = [
|
|
983
1115
|
/** DOCTYPE declaration */
|
|
984
1116
|
/<!DOCTYPE\b/gi,
|
|
@@ -1008,6 +1140,19 @@ function sanitizeXxe(input, collectThreats = false) {
|
|
|
1008
1140
|
const threats = [];
|
|
1009
1141
|
let value = input;
|
|
1010
1142
|
let wasSanitized = false;
|
|
1143
|
+
if (value.length > MAX_XXE_INPUT_BYTES) {
|
|
1144
|
+
if (collectThreats) {
|
|
1145
|
+
threats.push({ type: "xxe", pattern: "oversize_input", original: `length=${value.length}` });
|
|
1146
|
+
}
|
|
1147
|
+
return collectThreats ? { value: "", wasSanitized: true, threats } : "";
|
|
1148
|
+
}
|
|
1149
|
+
const entityRefs = value.match(/&\w+;/g);
|
|
1150
|
+
if (entityRefs && entityRefs.length > MAX_ENTITY_REFERENCES) {
|
|
1151
|
+
if (collectThreats) {
|
|
1152
|
+
threats.push({ type: "xxe", pattern: "entity_expansion", original: `count=${entityRefs.length}` });
|
|
1153
|
+
}
|
|
1154
|
+
return collectThreats ? { value: "", wasSanitized: true, threats } : "";
|
|
1155
|
+
}
|
|
1011
1156
|
for (const pattern of XXE_REMOVE_PATTERNS) {
|
|
1012
1157
|
pattern.lastIndex = 0;
|
|
1013
1158
|
if (pattern.test(value)) {
|
|
@@ -1045,12 +1190,10 @@ function detectXxe(input) {
|
|
|
1045
1190
|
}
|
|
1046
1191
|
|
|
1047
1192
|
// src/sanitizers/jsonp.ts
|
|
1048
|
-
var SAFE_CALLBACK_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$.
|
|
1193
|
+
var SAFE_CALLBACK_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/;
|
|
1049
1194
|
var DANGEROUS_CALLBACK_PATTERNS = [
|
|
1050
|
-
|
|
1195
|
+
/\.\./
|
|
1051
1196
|
// prototype chain traversal
|
|
1052
|
-
/\[\s*\]/
|
|
1053
|
-
// empty bracket access
|
|
1054
1197
|
];
|
|
1055
1198
|
function sanitizeJsonpCallback(callback, maxLength = 128) {
|
|
1056
1199
|
if (typeof callback !== "string" || callback.length === 0) {
|
|
@@ -1135,7 +1278,7 @@ function detectHeaderInjection(input) {
|
|
|
1135
1278
|
|
|
1136
1279
|
// src/sanitizers/pii.ts
|
|
1137
1280
|
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;
|
|
1138
|
-
var PHONE_RE = /(?:\+?1[-.\s]?)?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g;
|
|
1281
|
+
var PHONE_RE = /(?<!\d)(?:\+?1[-.\s]?)?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}(?!\d)/g;
|
|
1139
1282
|
var CREDIT_CARD_RE = /\b(?:\d[ -]*?){13,19}\b/g;
|
|
1140
1283
|
var SSN_RE = /\b\d{3}[-\s]\d{2}[-\s]\d{4}\b/g;
|
|
1141
1284
|
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;
|
|
@@ -1296,16 +1439,18 @@ function encodeForAttribute(value) {
|
|
|
1296
1439
|
function encodeForJs(value) {
|
|
1297
1440
|
if (!value) return "";
|
|
1298
1441
|
let result = "";
|
|
1299
|
-
for (
|
|
1300
|
-
const
|
|
1301
|
-
if (
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
result +=
|
|
1305
|
-
} else if (
|
|
1306
|
-
result += `\\x${
|
|
1442
|
+
for (const char of value) {
|
|
1443
|
+
const cp = char.codePointAt(0);
|
|
1444
|
+
if (cp >= 48 && cp <= 57 || // 0-9
|
|
1445
|
+
cp >= 65 && cp <= 90 || // A-Z
|
|
1446
|
+
cp >= 97 && cp <= 122) {
|
|
1447
|
+
result += char;
|
|
1448
|
+
} else if (cp < 256) {
|
|
1449
|
+
result += `\\x${cp.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
1450
|
+
} else if (cp <= 65535) {
|
|
1451
|
+
result += `\\u${cp.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
1307
1452
|
} else {
|
|
1308
|
-
result += `\\u${
|
|
1453
|
+
result += `\\u{${cp.toString(16).toUpperCase()}}`;
|
|
1309
1454
|
}
|
|
1310
1455
|
}
|
|
1311
1456
|
return result;
|
|
@@ -1758,8 +1903,12 @@ function checkPrivateIp(hostname) {
|
|
|
1758
1903
|
if (hostname === "metadata.google.internal" || hostname === "metadata.internal" || hostname === "metadata.azure.internal") {
|
|
1759
1904
|
return "cloud metadata endpoint";
|
|
1760
1905
|
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1906
|
+
let ipv6 = hostname.replace(/^\[|\]$/g, "");
|
|
1907
|
+
const zoneIdx = ipv6.indexOf("%");
|
|
1908
|
+
if (zoneIdx !== -1) {
|
|
1909
|
+
ipv6 = ipv6.slice(0, zoneIdx);
|
|
1910
|
+
}
|
|
1911
|
+
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)) {
|
|
1763
1912
|
return "private IPv6 address";
|
|
1764
1913
|
}
|
|
1765
1914
|
const mappedDotted = ipv6.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
|
|
@@ -2253,10 +2402,192 @@ function createRedactor(sensitiveKeys = []) {
|
|
|
2253
2402
|
}
|
|
2254
2403
|
var safeLog = createSafeLogger;
|
|
2255
2404
|
|
|
2405
|
+
// src/telemetry/client.ts
|
|
2406
|
+
var DEFAULT_BATCH_SIZE = 50;
|
|
2407
|
+
var MAX_BATCH_SIZE = 500;
|
|
2408
|
+
var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
|
|
2409
|
+
var MIN_FLUSH_INTERVAL_MS = 500;
|
|
2410
|
+
var FLUSH_TIMEOUT_MS = 1e4;
|
|
2411
|
+
var TelemetryClient = class {
|
|
2412
|
+
constructor(options) {
|
|
2413
|
+
this.queue = [];
|
|
2414
|
+
this.flushing = false;
|
|
2415
|
+
this.closed = false;
|
|
2416
|
+
if (!options.endpoint || typeof options.endpoint !== "string") {
|
|
2417
|
+
throw new TypeError("TelemetryClient: `endpoint` is required");
|
|
2418
|
+
}
|
|
2419
|
+
this.endpoint = options.endpoint;
|
|
2420
|
+
this.apiKey = options.apiKey;
|
|
2421
|
+
this.workspaceId = options.workspaceId;
|
|
2422
|
+
this.batchSize = clamp(
|
|
2423
|
+
options.batchSize ?? DEFAULT_BATCH_SIZE,
|
|
2424
|
+
1,
|
|
2425
|
+
MAX_BATCH_SIZE
|
|
2426
|
+
);
|
|
2427
|
+
this.flushIntervalMs = Math.max(
|
|
2428
|
+
options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
2429
|
+
MIN_FLUSH_INTERVAL_MS
|
|
2430
|
+
);
|
|
2431
|
+
this.onError = options.onError ?? (() => {
|
|
2432
|
+
});
|
|
2433
|
+
this.startTimer();
|
|
2434
|
+
}
|
|
2435
|
+
/**
|
|
2436
|
+
* Enqueue an event. Fast, synchronous, cannot throw.
|
|
2437
|
+
* Triggers a flush if the queue has reached `batchSize`.
|
|
2438
|
+
*/
|
|
2439
|
+
record(event) {
|
|
2440
|
+
if (this.closed) return;
|
|
2441
|
+
this.queue.push(event);
|
|
2442
|
+
if (this.queue.length >= this.batchSize) {
|
|
2443
|
+
void this.flush();
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* Manually flush the queue. Pulls up to `batchSize` events into a batch and
|
|
2448
|
+
* POSTs them. Returns a resolved promise on success OR on handled failure.
|
|
2449
|
+
* Never throws.
|
|
2450
|
+
*/
|
|
2451
|
+
async flush() {
|
|
2452
|
+
if (this.flushing) return;
|
|
2453
|
+
if (this.queue.length === 0) return;
|
|
2454
|
+
this.flushing = true;
|
|
2455
|
+
try {
|
|
2456
|
+
const batch = this.queue.splice(0, this.batchSize);
|
|
2457
|
+
await this.send(batch);
|
|
2458
|
+
} catch (err) {
|
|
2459
|
+
this.safeNotify(err);
|
|
2460
|
+
} finally {
|
|
2461
|
+
this.flushing = false;
|
|
2462
|
+
}
|
|
2463
|
+
if (!this.closed && this.queue.length > 0) {
|
|
2464
|
+
void this.flush();
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* Shut down: stop the interval timer and attempt one final flush.
|
|
2469
|
+
* Safe to call multiple times.
|
|
2470
|
+
*/
|
|
2471
|
+
async close() {
|
|
2472
|
+
if (this.closed) return;
|
|
2473
|
+
this.closed = true;
|
|
2474
|
+
if (this.timer !== void 0) {
|
|
2475
|
+
clearInterval(this.timer);
|
|
2476
|
+
this.timer = void 0;
|
|
2477
|
+
}
|
|
2478
|
+
if (this.signalHandler !== void 0) {
|
|
2479
|
+
process.off("SIGTERM", this.signalHandler);
|
|
2480
|
+
process.off("SIGINT", this.signalHandler);
|
|
2481
|
+
this.signalHandler = void 0;
|
|
2482
|
+
}
|
|
2483
|
+
try {
|
|
2484
|
+
await this.flush();
|
|
2485
|
+
} catch {
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Register `SIGTERM` / `SIGINT` handlers that call `close()` to drain
|
|
2490
|
+
* the queue on graceful shutdown. Opt-in — libraries should not silently
|
|
2491
|
+
* attach global signal handlers. Safe to call multiple times.
|
|
2492
|
+
*/
|
|
2493
|
+
installShutdownHooks() {
|
|
2494
|
+
if (this.signalHandler !== void 0 || this.closed) return;
|
|
2495
|
+
const handler = () => {
|
|
2496
|
+
void this.close();
|
|
2497
|
+
};
|
|
2498
|
+
this.signalHandler = handler;
|
|
2499
|
+
process.once("SIGTERM", handler);
|
|
2500
|
+
process.once("SIGINT", handler);
|
|
2501
|
+
}
|
|
2502
|
+
/** Count of events currently waiting to be sent. Useful for tests. */
|
|
2503
|
+
get pendingCount() {
|
|
2504
|
+
return this.queue.length;
|
|
2505
|
+
}
|
|
2506
|
+
// ── internals ─────────────────────────────────────────────────────────
|
|
2507
|
+
async send(batch) {
|
|
2508
|
+
const headers = {
|
|
2509
|
+
"content-type": "application/json"
|
|
2510
|
+
};
|
|
2511
|
+
if (this.apiKey) headers["authorization"] = `Bearer ${this.apiKey}`;
|
|
2512
|
+
if (this.workspaceId) headers["x-workspace-id"] = this.workspaceId;
|
|
2513
|
+
const controller = new AbortController();
|
|
2514
|
+
const abortTimer = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
|
|
2515
|
+
try {
|
|
2516
|
+
const res = await fetch(this.endpoint, {
|
|
2517
|
+
method: "POST",
|
|
2518
|
+
headers,
|
|
2519
|
+
body: JSON.stringify({ events: batch }),
|
|
2520
|
+
signal: controller.signal
|
|
2521
|
+
});
|
|
2522
|
+
if (!res.ok) {
|
|
2523
|
+
const text = await safeReadBody(res);
|
|
2524
|
+
throw new TelemetryHttpError(res.status, text);
|
|
2525
|
+
}
|
|
2526
|
+
} finally {
|
|
2527
|
+
clearTimeout(abortTimer);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
startTimer() {
|
|
2531
|
+
this.timer = setInterval(() => {
|
|
2532
|
+
void this.flush();
|
|
2533
|
+
}, this.flushIntervalMs);
|
|
2534
|
+
this.timer.unref?.();
|
|
2535
|
+
}
|
|
2536
|
+
safeNotify(err) {
|
|
2537
|
+
try {
|
|
2538
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
2539
|
+
} catch {
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
};
|
|
2543
|
+
var TelemetryHttpError = class extends Error {
|
|
2544
|
+
constructor(status, responseBody) {
|
|
2545
|
+
super(`Telemetry ingest returned HTTP ${status}`);
|
|
2546
|
+
this.status = status;
|
|
2547
|
+
this.responseBody = responseBody;
|
|
2548
|
+
this.name = "TelemetryHttpError";
|
|
2549
|
+
}
|
|
2550
|
+
};
|
|
2551
|
+
function clamp(value, min, max) {
|
|
2552
|
+
if (!Number.isFinite(value)) return min;
|
|
2553
|
+
return Math.max(min, Math.min(max, Math.trunc(value)));
|
|
2554
|
+
}
|
|
2555
|
+
async function safeReadBody(res) {
|
|
2556
|
+
try {
|
|
2557
|
+
const text = await res.text();
|
|
2558
|
+
return text.slice(0, 500);
|
|
2559
|
+
} catch {
|
|
2560
|
+
return "";
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2256
2564
|
// src/middleware/main.ts
|
|
2565
|
+
function buildTelemetryFromEnv() {
|
|
2566
|
+
const env = typeof process !== "undefined" ? process.env : void 0;
|
|
2567
|
+
const endpoint = env?.ARCIS_ENDPOINT;
|
|
2568
|
+
if (!endpoint) return void 0;
|
|
2569
|
+
const opts = { endpoint };
|
|
2570
|
+
if (env?.ARCIS_WORKSPACE_ID) opts.workspaceId = env.ARCIS_WORKSPACE_ID;
|
|
2571
|
+
if (env?.ARCIS_KEY) opts.apiKey = env.ARCIS_KEY;
|
|
2572
|
+
const batch = env?.ARCIS_BATCH_SIZE ? parseInt(env.ARCIS_BATCH_SIZE, 10) : NaN;
|
|
2573
|
+
if (!Number.isNaN(batch)) opts.batchSize = batch;
|
|
2574
|
+
const flush = env?.ARCIS_FLUSH_INTERVAL_MS ? parseInt(env.ARCIS_FLUSH_INTERVAL_MS, 10) : NaN;
|
|
2575
|
+
if (!Number.isNaN(flush)) opts.flushIntervalMs = flush;
|
|
2576
|
+
return opts;
|
|
2577
|
+
}
|
|
2257
2578
|
function arcis(options = {}) {
|
|
2258
2579
|
const middlewares = [];
|
|
2259
2580
|
const cleanupFns = [];
|
|
2581
|
+
let telemetryClient;
|
|
2582
|
+
const telemetryOpts = options.telemetry?.endpoint ? options.telemetry : buildTelemetryFromEnv();
|
|
2583
|
+
if (telemetryOpts) {
|
|
2584
|
+
const client = new TelemetryClient(telemetryOpts);
|
|
2585
|
+
telemetryClient = client;
|
|
2586
|
+
middlewares.push(createTelemetryEmitter(client));
|
|
2587
|
+
cleanupFns.push(() => {
|
|
2588
|
+
void client.close();
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2260
2591
|
if (options.headers !== false) {
|
|
2261
2592
|
const headerOpts = typeof options.headers === "object" ? options.headers : {};
|
|
2262
2593
|
middlewares.push(createHeaders(headerOpts));
|
|
@@ -2269,7 +2600,8 @@ function arcis(options = {}) {
|
|
|
2269
2600
|
}
|
|
2270
2601
|
if (options.sanitize !== false) {
|
|
2271
2602
|
const sanitizeOpts = typeof options.sanitize === "object" ? options.sanitize : {};
|
|
2272
|
-
|
|
2603
|
+
const sanitizer = createSanitizer(sanitizeOpts);
|
|
2604
|
+
middlewares.push(telemetryClient ? tapSanitizerThreats(sanitizer) : sanitizer);
|
|
2273
2605
|
}
|
|
2274
2606
|
const result = middlewares;
|
|
2275
2607
|
result.close = () => {
|
|
@@ -2596,6 +2928,16 @@ function secureCookieDefaults(options = {}) {
|
|
|
2596
2928
|
sameSite: options.sameSite ?? "Lax",
|
|
2597
2929
|
path: options.path
|
|
2598
2930
|
};
|
|
2931
|
+
if (resolved.sameSite === "None" && resolved.secure === false) {
|
|
2932
|
+
throw new Error(
|
|
2933
|
+
"[arcis] secureCookieDefaults: sameSite=None requires secure=true (modern browsers reject the cookie otherwise)"
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
if (resolved.httpOnly === false && resolved.secure === false && isProduction) {
|
|
2937
|
+
console.warn(
|
|
2938
|
+
"[arcis] secureCookieDefaults: httpOnly and secure are both disabled in production \u2014 cookies will be readable by JS and sent over HTTP"
|
|
2939
|
+
);
|
|
2940
|
+
}
|
|
2599
2941
|
return (_req, res, next) => {
|
|
2600
2942
|
const originalSetHeader = res.setHeader.bind(res);
|
|
2601
2943
|
res.setHeader = function patchedSetHeader(name, value) {
|
|
@@ -2728,7 +3070,16 @@ function detectBehavioralSignals(req) {
|
|
|
2728
3070
|
}
|
|
2729
3071
|
function detectBot(req) {
|
|
2730
3072
|
const rawUa = req.headers["user-agent"] ?? "";
|
|
2731
|
-
|
|
3073
|
+
if (rawUa.length > 2048) {
|
|
3074
|
+
return {
|
|
3075
|
+
isBot: true,
|
|
3076
|
+
category: "UNKNOWN",
|
|
3077
|
+
name: null,
|
|
3078
|
+
confidence: 0.9,
|
|
3079
|
+
signals: detectBehavioralSignals(req)
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
const ua = rawUa;
|
|
2732
3083
|
const signals = detectBehavioralSignals(req);
|
|
2733
3084
|
if (!ua) {
|
|
2734
3085
|
return {
|
|
@@ -2818,11 +3169,9 @@ function generateCsrfToken(length = 32) {
|
|
|
2818
3169
|
function validateCsrfToken(cookieToken, requestToken) {
|
|
2819
3170
|
if (!cookieToken || !requestToken) return false;
|
|
2820
3171
|
if (cookieToken.length !== requestToken.length) return false;
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
}
|
|
2825
|
-
return result === 0;
|
|
3172
|
+
const a = Buffer.from(cookieToken);
|
|
3173
|
+
const b = Buffer.from(requestToken);
|
|
3174
|
+
return timingSafeEqual(a, b);
|
|
2826
3175
|
}
|
|
2827
3176
|
function getRequestToken(req, headerName, fieldName) {
|
|
2828
3177
|
const headerToken = req.headers[headerName.toLowerCase()];
|
|
@@ -2831,10 +3180,6 @@ function getRequestToken(req, headerName, fieldName) {
|
|
|
2831
3180
|
const bodyToken = req.body[fieldName];
|
|
2832
3181
|
if (typeof bodyToken === "string" && bodyToken) return bodyToken;
|
|
2833
3182
|
}
|
|
2834
|
-
if (req.query && fieldName in req.query) {
|
|
2835
|
-
const queryToken = req.query[fieldName];
|
|
2836
|
-
if (typeof queryToken === "string" && queryToken) return queryToken;
|
|
2837
|
-
}
|
|
2838
3183
|
return void 0;
|
|
2839
3184
|
}
|
|
2840
3185
|
function csrfProtection(options = {}) {
|
|
@@ -2898,6 +3243,10 @@ function csrfProtection(options = {}) {
|
|
|
2898
3243
|
if (!validateCsrfToken(cookieToken, requestToken)) {
|
|
2899
3244
|
return onError(req, res, next);
|
|
2900
3245
|
}
|
|
3246
|
+
if (options.rotateOnUse) {
|
|
3247
|
+
const freshToken = generateCsrfToken(tokenLength);
|
|
3248
|
+
setCsrfCookie(res, cookieName, freshToken, cookieOpts);
|
|
3249
|
+
}
|
|
2901
3250
|
next();
|
|
2902
3251
|
};
|
|
2903
3252
|
}
|
|
@@ -2917,7 +3266,15 @@ function setCsrfCookie(res, name, token, opts) {
|
|
|
2917
3266
|
if (opts.secure) parts.push("Secure");
|
|
2918
3267
|
parts.push(`SameSite=${opts.sameSite}`);
|
|
2919
3268
|
if (opts.domain) parts.push(`Domain=${opts.domain}`);
|
|
2920
|
-
|
|
3269
|
+
const newCookie = parts.join("; ");
|
|
3270
|
+
const existing = res.getHeader("Set-Cookie");
|
|
3271
|
+
if (existing === void 0) {
|
|
3272
|
+
res.setHeader("Set-Cookie", newCookie);
|
|
3273
|
+
} else if (Array.isArray(existing)) {
|
|
3274
|
+
res.setHeader("Set-Cookie", [...existing, newCookie]);
|
|
3275
|
+
} else {
|
|
3276
|
+
res.setHeader("Set-Cookie", [existing, newCookie]);
|
|
3277
|
+
}
|
|
2921
3278
|
}
|
|
2922
3279
|
function escapeRegex(str) {
|
|
2923
3280
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -2926,7 +3283,7 @@ var createCsrf = csrfProtection;
|
|
|
2926
3283
|
|
|
2927
3284
|
// src/middleware/hpp.ts
|
|
2928
3285
|
function hpp(options = {}) {
|
|
2929
|
-
const whitelist = new Set(options.whitelist ?? []);
|
|
3286
|
+
const whitelist = new Set((options.whitelist ?? []).map((k) => k.toLowerCase()));
|
|
2930
3287
|
const checkQuery = options.checkQuery ?? true;
|
|
2931
3288
|
const checkBody = options.checkBody ?? true;
|
|
2932
3289
|
return (req, _res, next) => {
|
|
@@ -2936,7 +3293,7 @@ function hpp(options = {}) {
|
|
|
2936
3293
|
for (const [key, value] of Object.entries(req.query)) {
|
|
2937
3294
|
if (Array.isArray(value)) {
|
|
2938
3295
|
const strings = value.filter((v) => typeof v === "string");
|
|
2939
|
-
if (whitelist.has(key)) {
|
|
3296
|
+
if (whitelist.has(key.toLowerCase())) {
|
|
2940
3297
|
clean[key] = strings;
|
|
2941
3298
|
} else {
|
|
2942
3299
|
polluted[key] = strings;
|
|
@@ -2954,7 +3311,7 @@ function hpp(options = {}) {
|
|
|
2954
3311
|
const clean = {};
|
|
2955
3312
|
for (const [key, value] of Object.entries(req.body)) {
|
|
2956
3313
|
if (Array.isArray(value)) {
|
|
2957
|
-
if (whitelist.has(key)) {
|
|
3314
|
+
if (whitelist.has(key.toLowerCase())) {
|
|
2958
3315
|
clean[key] = value;
|
|
2959
3316
|
} else {
|
|
2960
3317
|
polluted[key] = value;
|
|
@@ -2972,6 +3329,89 @@ function hpp(options = {}) {
|
|
|
2972
3329
|
}
|
|
2973
3330
|
var createHpp = hpp;
|
|
2974
3331
|
|
|
3332
|
+
// src/middleware/signup-protection.ts
|
|
3333
|
+
function checkSignup(req, options = {}) {
|
|
3334
|
+
const {
|
|
3335
|
+
emailField = "email",
|
|
3336
|
+
checkEmail = true,
|
|
3337
|
+
blockDisposable = true,
|
|
3338
|
+
checkBot = true,
|
|
3339
|
+
allowedBotCategories = [],
|
|
3340
|
+
allowedEmailDomains = [],
|
|
3341
|
+
blockedEmailDomains = []
|
|
3342
|
+
} = options;
|
|
3343
|
+
if (checkBot) {
|
|
3344
|
+
const bot = detectBot(req);
|
|
3345
|
+
if (bot.isBot && !allowedBotCategories.includes(bot.category)) {
|
|
3346
|
+
return {
|
|
3347
|
+
allowed: false,
|
|
3348
|
+
reason: "bot",
|
|
3349
|
+
details: { category: bot.category, name: bot.name, confidence: bot.confidence }
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
if (checkEmail) {
|
|
3354
|
+
const email = req.body?.[emailField];
|
|
3355
|
+
if (typeof email !== "string" || email.length === 0) {
|
|
3356
|
+
return { allowed: false, reason: "missing_email" };
|
|
3357
|
+
}
|
|
3358
|
+
const result = validateEmail(email, {
|
|
3359
|
+
checkDisposable: blockDisposable,
|
|
3360
|
+
allowedDomains: allowedEmailDomains,
|
|
3361
|
+
blockedDomains: blockedEmailDomains
|
|
3362
|
+
});
|
|
3363
|
+
if (!result.valid) {
|
|
3364
|
+
const reason = result.reason === "disposable" ? "disposable_email" : "invalid_email";
|
|
3365
|
+
return { allowed: false, reason, details: { emailReason: result.reason } };
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
return { allowed: true, reason: "ok" };
|
|
3369
|
+
}
|
|
3370
|
+
function signupProtection(options = {}) {
|
|
3371
|
+
const rateLimitCfg = options.rateLimit;
|
|
3372
|
+
const limiter = rateLimitCfg === false ? null : createRateLimiter({
|
|
3373
|
+
max: rateLimitCfg?.max ?? 5,
|
|
3374
|
+
windowMs: rateLimitCfg?.windowMs ?? 6e4,
|
|
3375
|
+
message: "Too many signup attempts"
|
|
3376
|
+
});
|
|
3377
|
+
const handler = (req, res, next) => {
|
|
3378
|
+
const result = checkSignup(req, options);
|
|
3379
|
+
if (!result.allowed) {
|
|
3380
|
+
options.onBlocked?.(req, result);
|
|
3381
|
+
const status = result.reason === "bot" ? 403 : 400;
|
|
3382
|
+
res.status(status).json({ error: "signup_blocked", reason: result.reason });
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
if (limiter) {
|
|
3386
|
+
let rateLimited = false;
|
|
3387
|
+
const rateLimitNext = (err) => {
|
|
3388
|
+
if (err) return next(err);
|
|
3389
|
+
if (!rateLimited) next();
|
|
3390
|
+
};
|
|
3391
|
+
const patchedRes = new Proxy(res, {
|
|
3392
|
+
get(target, prop) {
|
|
3393
|
+
if (prop === "status") {
|
|
3394
|
+
return (code) => {
|
|
3395
|
+
if (code === 429) {
|
|
3396
|
+
rateLimited = true;
|
|
3397
|
+
options.onBlocked?.(req, { allowed: false, reason: "rate_limited" });
|
|
3398
|
+
}
|
|
3399
|
+
return target.status.call(target, code);
|
|
3400
|
+
};
|
|
3401
|
+
}
|
|
3402
|
+
return Reflect.get(target, prop);
|
|
3403
|
+
}
|
|
3404
|
+
});
|
|
3405
|
+
limiter(req, patchedRes, rateLimitNext);
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
next();
|
|
3409
|
+
};
|
|
3410
|
+
const middleware = handler;
|
|
3411
|
+
middleware.close = () => limiter?.close();
|
|
3412
|
+
return middleware;
|
|
3413
|
+
}
|
|
3414
|
+
|
|
2975
3415
|
// src/utils/ip.ts
|
|
2976
3416
|
var PLATFORM_HEADERS = {
|
|
2977
3417
|
cloudflare: "cf-connecting-ip",
|
|
@@ -3101,8 +3541,9 @@ function fingerprint(req, options = {}) {
|
|
|
3101
3541
|
}
|
|
3102
3542
|
|
|
3103
3543
|
// src/stores/memory.ts
|
|
3544
|
+
var DEFAULT_MAX_SIZE2 = 1e4;
|
|
3104
3545
|
var MemoryStore = class {
|
|
3105
|
-
constructor(windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS) {
|
|
3546
|
+
constructor(windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS, maxSize = DEFAULT_MAX_SIZE2) {
|
|
3106
3547
|
this.store = /* @__PURE__ */ new Map();
|
|
3107
3548
|
this.cleanupInterval = null;
|
|
3108
3549
|
if (!Number.isFinite(windowMs) || windowMs < RATE_LIMIT.MIN_WINDOW_MS) {
|
|
@@ -3110,7 +3551,11 @@ var MemoryStore = class {
|
|
|
3110
3551
|
`MemoryStore: windowMs must be a finite number >= ${RATE_LIMIT.MIN_WINDOW_MS} (got ${windowMs})`
|
|
3111
3552
|
);
|
|
3112
3553
|
}
|
|
3554
|
+
if (!Number.isFinite(maxSize) || maxSize < 1) {
|
|
3555
|
+
throw new RangeError(`MemoryStore: maxSize must be >= 1 (got ${maxSize})`);
|
|
3556
|
+
}
|
|
3113
3557
|
this.windowMs = windowMs;
|
|
3558
|
+
this.maxSize = maxSize;
|
|
3114
3559
|
this.startCleanup();
|
|
3115
3560
|
}
|
|
3116
3561
|
/**
|
|
@@ -3142,18 +3587,33 @@ var MemoryStore = class {
|
|
|
3142
3587
|
return entry;
|
|
3143
3588
|
}
|
|
3144
3589
|
async set(key, entry) {
|
|
3590
|
+
if (!this.store.has(key) && this.store.size >= this.maxSize) {
|
|
3591
|
+
this.evictExpired();
|
|
3592
|
+
if (this.store.size >= this.maxSize) return;
|
|
3593
|
+
}
|
|
3145
3594
|
this.store.set(key, entry);
|
|
3146
3595
|
}
|
|
3147
3596
|
async increment(key) {
|
|
3148
3597
|
const now = Date.now();
|
|
3149
3598
|
const entry = this.store.get(key);
|
|
3150
3599
|
if (!entry || entry.resetTime < now) {
|
|
3600
|
+
if (this.store.size >= this.maxSize) {
|
|
3601
|
+
this.evictExpired();
|
|
3602
|
+
if (this.store.size >= this.maxSize) return 1;
|
|
3603
|
+
}
|
|
3151
3604
|
this.store.set(key, { count: 1, resetTime: now + this.windowMs });
|
|
3152
3605
|
return 1;
|
|
3153
3606
|
}
|
|
3154
3607
|
entry.count++;
|
|
3155
3608
|
return entry.count;
|
|
3156
3609
|
}
|
|
3610
|
+
/** Eagerly remove expired entries to reclaim capacity. */
|
|
3611
|
+
evictExpired() {
|
|
3612
|
+
const now = Date.now();
|
|
3613
|
+
for (const [key, entry] of this.store.entries()) {
|
|
3614
|
+
if (entry.resetTime < now) this.store.delete(key);
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3157
3617
|
async decrement(key) {
|
|
3158
3618
|
const entry = this.store.get(key);
|
|
3159
3619
|
if (entry && entry.count > 0) {
|
|
@@ -3235,6 +3695,6 @@ function createRedisStore(options) {
|
|
|
3235
3695
|
return new RedisStore(options);
|
|
3236
3696
|
}
|
|
3237
3697
|
|
|
3238
|
-
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, createHpp, 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, encodeForAttribute, encodeForCss, encodeForHtml, encodeForJs, encodeForUrl, enforceSecureCookie, errorHandler, fingerprint, formatDuration, generateCsrfToken, hpp, 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 };
|
|
3698
|
+
export { ArcisError, ValidationError as ArcisValidationError, BLOCKED, ERRORS, HEADERS, INPUT, InputTooLargeError, MemoryStore, RATE_LIMIT, REDACTION, RateLimitError, RedisStore, SanitizationError, SecurityThreatError, TelemetryClient, TelemetryHttpError, VALIDATION, arcis, arcisWithMethods as arcisFunction, botProtection, checkSignup, createCors, createCsrf, createErrorHandler, createHeaders, createHpp, 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, encodeForAttribute, encodeForCss, encodeForHtml, encodeForJs, encodeForUrl, enforceSecureCookie, errorHandler, fingerprint, formatDuration, generateCsrfToken, hpp, 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, signupProtection, validate, validateCsrfToken, validateEmail, validateFile, validateRedirect, validateUrl, verifyEmailMx };
|
|
3239
3699
|
//# sourceMappingURL=index.mjs.map
|
|
3240
3700
|
//# sourceMappingURL=index.mjs.map
|