@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/middleware/index.js
CHANGED
|
@@ -49,6 +49,9 @@ var XSS_REMOVE_PATTERNS = [
|
|
|
49
49
|
/<script[^>]*>[\s\S]*?<\/script>/gi,
|
|
50
50
|
/** Standalone/unclosed script tags */
|
|
51
51
|
/<script[^>]*>/gi,
|
|
52
|
+
/** style — CSS expression() and behavior: attacks (IE-era but still relevant) */
|
|
53
|
+
/<style[^>]*>[\s\S]*?<\/style>/gi,
|
|
54
|
+
/<style[^>]*/gi,
|
|
52
55
|
/** iframe — full block and partial/unclosed */
|
|
53
56
|
/<iframe[^>]*>[\s\S]*?<\/iframe>/gi,
|
|
54
57
|
/<iframe[^>]*/gi,
|
|
@@ -69,7 +72,15 @@ var XSS_REMOVE_PATTERNS = [
|
|
|
69
72
|
/javascript\s*:/gi,
|
|
70
73
|
/vbscript\s*:/gi,
|
|
71
74
|
/** data: URIs with HTML/script content */
|
|
72
|
-
/data\s*:\s*text\/html[^>\s]*/gi
|
|
75
|
+
/data\s*:\s*text\/html[^>\s]*/gi,
|
|
76
|
+
/** form tag injection — phishing via action= redirection */
|
|
77
|
+
/<form[\s>][^>]*/gi,
|
|
78
|
+
/** meta tag injection — http-equiv refresh or CSP bypass */
|
|
79
|
+
/<meta[\s>][^>]*/gi,
|
|
80
|
+
/** base href hijacking */
|
|
81
|
+
/<base[\s>][^>]*/gi,
|
|
82
|
+
/** link tag injection — stylesheet or preload attacks */
|
|
83
|
+
/<link[\s>][^>]*/gi
|
|
73
84
|
];
|
|
74
85
|
var SQL_PATTERNS = [
|
|
75
86
|
/** SQL keywords */
|
|
@@ -133,8 +144,8 @@ var COMMAND_PATTERNS = [
|
|
|
133
144
|
/[;&|`]/g,
|
|
134
145
|
/** Command substitution: $( ... ) — matched as a pair to reduce false positives */
|
|
135
146
|
/\$\(/g,
|
|
136
|
-
/** URL-encoded
|
|
137
|
-
/%0[
|
|
147
|
+
/** URL-encoded control characters (%00-%0F): null, tab, vtab, formfeed, LF, CR */
|
|
148
|
+
/%0[0-9a-f]/gi
|
|
138
149
|
];
|
|
139
150
|
var DANGEROUS_PROTO_KEYS = /* @__PURE__ */ new Set([
|
|
140
151
|
"__proto__",
|
|
@@ -353,13 +364,13 @@ function createRateLimiter(options = {}) {
|
|
|
353
364
|
statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
|
|
354
365
|
keyGenerator = (req) => {
|
|
355
366
|
const ip = req.ip ?? req.socket?.remoteAddress;
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
return
|
|
367
|
+
if (ip) return ip;
|
|
368
|
+
const ua = req.headers["user-agent"] ?? "";
|
|
369
|
+
const lang = req.headers["accept-language"] ?? "";
|
|
370
|
+
const fp = `${ua}|${lang}`;
|
|
371
|
+
let hash = 0;
|
|
372
|
+
for (let i = 0; i < fp.length; i++) hash = (hash << 5) - hash + fp.charCodeAt(i) | 0;
|
|
373
|
+
return `unknown:${hash.toString(36)}`;
|
|
363
374
|
},
|
|
364
375
|
skip,
|
|
365
376
|
store: externalStore
|
|
@@ -422,7 +433,24 @@ function createRateLimiter(options = {}) {
|
|
|
422
433
|
}
|
|
423
434
|
next();
|
|
424
435
|
} catch (error) {
|
|
425
|
-
console.error("[arcis] Rate limiter error:", error);
|
|
436
|
+
console.error("[arcis] Rate limiter store error, using in-memory fallback:", error);
|
|
437
|
+
try {
|
|
438
|
+
const key = keyGenerator(req);
|
|
439
|
+
const now = Date.now();
|
|
440
|
+
if (!inMemoryStore[key] || inMemoryStore[key].resetTime < now) {
|
|
441
|
+
inMemoryStore[key] = { count: 1, resetTime: now + windowMs };
|
|
442
|
+
} else {
|
|
443
|
+
inMemoryStore[key].count++;
|
|
444
|
+
}
|
|
445
|
+
const count = inMemoryStore[key].count;
|
|
446
|
+
if (count > max) {
|
|
447
|
+
const resetSeconds = Math.ceil((inMemoryStore[key].resetTime - now) / 1e3);
|
|
448
|
+
res.setHeader("Retry-After", resetSeconds.toString());
|
|
449
|
+
res.status(statusCode).json({ error: message, retryAfter: resetSeconds });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
426
454
|
next();
|
|
427
455
|
}
|
|
428
456
|
};
|
|
@@ -534,6 +562,80 @@ var SecurityThreatError = class extends ArcisError {
|
|
|
534
562
|
}
|
|
535
563
|
};
|
|
536
564
|
|
|
565
|
+
// src/middleware/telemetry.ts
|
|
566
|
+
var THREAT_TO_VECTOR = {
|
|
567
|
+
xss: "xss",
|
|
568
|
+
sql_injection: "sql",
|
|
569
|
+
nosql_injection: "nosql",
|
|
570
|
+
path_traversal: "path",
|
|
571
|
+
command_injection: "command",
|
|
572
|
+
prototype_pollution: "prototype",
|
|
573
|
+
header_injection: "header",
|
|
574
|
+
ssti: "ssti",
|
|
575
|
+
xxe: "xxe"
|
|
576
|
+
};
|
|
577
|
+
function createTelemetryEmitter(client) {
|
|
578
|
+
return (req, res, next) => {
|
|
579
|
+
const start = performance.now();
|
|
580
|
+
res.on("finish", () => {
|
|
581
|
+
try {
|
|
582
|
+
const event = buildEvent(req, res.statusCode, performance.now() - start);
|
|
583
|
+
client.record(event);
|
|
584
|
+
} catch {
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
next();
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function tapSanitizerThreats(handler) {
|
|
591
|
+
return (req, res, next) => {
|
|
592
|
+
handler(req, res, (err) => {
|
|
593
|
+
if (err instanceof SecurityThreatError) {
|
|
594
|
+
const vector = THREAT_TO_VECTOR[err.threatType] ?? err.threatType;
|
|
595
|
+
req.__arcis = {
|
|
596
|
+
vector,
|
|
597
|
+
rule: `${vector}/match`,
|
|
598
|
+
severity: "high",
|
|
599
|
+
matchedPattern: err.pattern,
|
|
600
|
+
reason: err.message,
|
|
601
|
+
decision: "deny"
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
next(err);
|
|
605
|
+
});
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function buildEvent(req, status, latencyMs) {
|
|
609
|
+
const marker = req.__arcis;
|
|
610
|
+
const decision = marker?.decision ?? inferDecision(status);
|
|
611
|
+
return {
|
|
612
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
613
|
+
ip: extractIp(req),
|
|
614
|
+
method: (req.method ?? "GET").toUpperCase(),
|
|
615
|
+
path: req.path ?? req.url ?? "/",
|
|
616
|
+
decision,
|
|
617
|
+
vector: marker?.vector ?? (status === 429 ? "rate-limit" : void 0),
|
|
618
|
+
rule: marker?.rule ?? (status === 429 ? "rate-limit/exceeded" : void 0),
|
|
619
|
+
severity: marker?.severity ?? (status === 429 ? "medium" : void 0),
|
|
620
|
+
userAgent: typeof req.headers?.["user-agent"] === "string" ? req.headers["user-agent"] : "",
|
|
621
|
+
reason: marker?.reason,
|
|
622
|
+
status,
|
|
623
|
+
matchedPattern: marker?.matchedPattern,
|
|
624
|
+
latencyMs: Math.max(0, latencyMs)
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function inferDecision(status) {
|
|
628
|
+
if (status === 429) return "deny";
|
|
629
|
+
if (status === 400) return "deny";
|
|
630
|
+
if (status === 403) return "deny";
|
|
631
|
+
return "allow";
|
|
632
|
+
}
|
|
633
|
+
function extractIp(req) {
|
|
634
|
+
if (typeof req.ip === "string" && req.ip.length > 0) return req.ip;
|
|
635
|
+
const remote = req.socket?.remoteAddress;
|
|
636
|
+
return typeof remote === "string" ? remote : "0.0.0.0";
|
|
637
|
+
}
|
|
638
|
+
|
|
537
639
|
// src/sanitizers/utils.ts
|
|
538
640
|
function encodeHtmlEntities(str) {
|
|
539
641
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -632,26 +734,31 @@ function sanitizePath(input, collectThreats = false) {
|
|
|
632
734
|
const threats = [];
|
|
633
735
|
let value = input;
|
|
634
736
|
let wasSanitized = false;
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
737
|
+
value = value.normalize("NFKC");
|
|
738
|
+
let prev;
|
|
739
|
+
do {
|
|
740
|
+
prev = value;
|
|
741
|
+
for (const pattern of PATH_PATTERNS) {
|
|
638
742
|
pattern.lastIndex = 0;
|
|
639
|
-
if (
|
|
640
|
-
|
|
641
|
-
if (
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
743
|
+
if (pattern.test(value)) {
|
|
744
|
+
pattern.lastIndex = 0;
|
|
745
|
+
if (collectThreats) {
|
|
746
|
+
const matches = value.match(pattern);
|
|
747
|
+
if (matches) {
|
|
748
|
+
for (const match of matches) {
|
|
749
|
+
threats.push({
|
|
750
|
+
type: "path_traversal",
|
|
751
|
+
pattern: pattern.source,
|
|
752
|
+
original: match
|
|
753
|
+
});
|
|
754
|
+
}
|
|
648
755
|
}
|
|
649
756
|
}
|
|
757
|
+
value = value.replace(pattern, "");
|
|
758
|
+
wasSanitized = true;
|
|
650
759
|
}
|
|
651
|
-
value = value.replace(pattern, "");
|
|
652
|
-
wasSanitized = true;
|
|
653
760
|
}
|
|
654
|
-
}
|
|
761
|
+
} while (value !== prev);
|
|
655
762
|
if (collectThreats) {
|
|
656
763
|
return { value, wasSanitized, threats };
|
|
657
764
|
}
|
|
@@ -709,7 +816,7 @@ function sanitizeString(value, options = {}) {
|
|
|
709
816
|
if (value.length > maxSize) {
|
|
710
817
|
throw new InputTooLargeError(maxSize, value.length);
|
|
711
818
|
}
|
|
712
|
-
const reject = options.mode
|
|
819
|
+
const reject = options.mode === "reject";
|
|
713
820
|
let result = value;
|
|
714
821
|
if (options.sql !== false) {
|
|
715
822
|
if (reject) {
|
|
@@ -963,6 +1070,238 @@ function validateField(field, value, rules) {
|
|
|
963
1070
|
// Audio/Video
|
|
964
1071
|
"audio/mpeg": [Buffer.from([255, 251]), Buffer.from([255, 243]), Buffer.from([73, 68, 51])]});
|
|
965
1072
|
|
|
1073
|
+
// src/validation/email.ts
|
|
1074
|
+
var MAX_EMAIL_LENGTH = 254;
|
|
1075
|
+
var MAX_LOCAL_LENGTH = 64;
|
|
1076
|
+
var MAX_DOMAIN_LENGTH = 255;
|
|
1077
|
+
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,}$/;
|
|
1078
|
+
var FREE_PROVIDERS = /* @__PURE__ */ new Set([
|
|
1079
|
+
"gmail.com",
|
|
1080
|
+
"yahoo.com",
|
|
1081
|
+
"hotmail.com",
|
|
1082
|
+
"outlook.com",
|
|
1083
|
+
"aol.com",
|
|
1084
|
+
"protonmail.com",
|
|
1085
|
+
"proton.me",
|
|
1086
|
+
"icloud.com",
|
|
1087
|
+
"mail.com",
|
|
1088
|
+
"zoho.com",
|
|
1089
|
+
"yandex.com",
|
|
1090
|
+
"gmx.com",
|
|
1091
|
+
"gmx.net",
|
|
1092
|
+
"live.com",
|
|
1093
|
+
"msn.com",
|
|
1094
|
+
"me.com",
|
|
1095
|
+
"mac.com",
|
|
1096
|
+
"fastmail.com",
|
|
1097
|
+
"tutanota.com",
|
|
1098
|
+
"hey.com"
|
|
1099
|
+
]);
|
|
1100
|
+
var DISPOSABLE_DOMAINS = /* @__PURE__ */ new Set([
|
|
1101
|
+
// Popular disposable services
|
|
1102
|
+
"guerrillamail.com",
|
|
1103
|
+
"guerrillamail.net",
|
|
1104
|
+
"guerrillamail.org",
|
|
1105
|
+
"tempmail.com",
|
|
1106
|
+
"temp-mail.org",
|
|
1107
|
+
"temp-mail.io",
|
|
1108
|
+
"throwaway.email",
|
|
1109
|
+
"throwaway.com",
|
|
1110
|
+
"mailinator.com",
|
|
1111
|
+
"mailinator.net",
|
|
1112
|
+
"yopmail.com",
|
|
1113
|
+
"yopmail.fr",
|
|
1114
|
+
"yopmail.net",
|
|
1115
|
+
"sharklasers.com",
|
|
1116
|
+
"grr.la",
|
|
1117
|
+
"guerrillamail.info",
|
|
1118
|
+
"guerrillamail.biz",
|
|
1119
|
+
"guerrillamail.de",
|
|
1120
|
+
"trashmail.com",
|
|
1121
|
+
"trashmail.me",
|
|
1122
|
+
"trashmail.net",
|
|
1123
|
+
"dispostable.com",
|
|
1124
|
+
"maildrop.cc",
|
|
1125
|
+
"mailnesia.com",
|
|
1126
|
+
"tempail.com",
|
|
1127
|
+
"mohmal.com",
|
|
1128
|
+
"getnada.com",
|
|
1129
|
+
"emailondeck.com",
|
|
1130
|
+
"discard.email",
|
|
1131
|
+
"fakeinbox.com",
|
|
1132
|
+
"mailcatch.com",
|
|
1133
|
+
"mintemail.com",
|
|
1134
|
+
"tempr.email",
|
|
1135
|
+
"tempinbox.com",
|
|
1136
|
+
"burnermail.io",
|
|
1137
|
+
"mailsac.com",
|
|
1138
|
+
"harakirimail.com",
|
|
1139
|
+
"tempmailo.com",
|
|
1140
|
+
"emailfake.com",
|
|
1141
|
+
"crazymailing.com",
|
|
1142
|
+
"armyspy.com",
|
|
1143
|
+
"dayrep.com",
|
|
1144
|
+
"einrot.com",
|
|
1145
|
+
"fleckens.hu",
|
|
1146
|
+
"gustr.com",
|
|
1147
|
+
"jourrapide.com",
|
|
1148
|
+
"rhyta.com",
|
|
1149
|
+
"superrito.com",
|
|
1150
|
+
"teleworm.us",
|
|
1151
|
+
"10minutemail.com",
|
|
1152
|
+
"10minutemail.net",
|
|
1153
|
+
"minutemail.com",
|
|
1154
|
+
"tempsky.com",
|
|
1155
|
+
"spamgourmet.com",
|
|
1156
|
+
"mytrashmail.com",
|
|
1157
|
+
"mailexpire.com",
|
|
1158
|
+
"safetymail.info",
|
|
1159
|
+
"filzmail.com",
|
|
1160
|
+
"trashymail.com",
|
|
1161
|
+
"sharkmail.com",
|
|
1162
|
+
"jetable.org",
|
|
1163
|
+
"nospam.ze.tc",
|
|
1164
|
+
"trash-me.com",
|
|
1165
|
+
"dodgit.com",
|
|
1166
|
+
"mailmoat.com",
|
|
1167
|
+
"spamfree24.org",
|
|
1168
|
+
"incognitomail.org",
|
|
1169
|
+
"tempomail.fr",
|
|
1170
|
+
"ephemail.net",
|
|
1171
|
+
"hidemail.de",
|
|
1172
|
+
"spaml.de",
|
|
1173
|
+
"uggsrock.com",
|
|
1174
|
+
"binkmail.com",
|
|
1175
|
+
"suremail.info",
|
|
1176
|
+
"bugmenot.com"
|
|
1177
|
+
]);
|
|
1178
|
+
var DOMAIN_TYPOS = {
|
|
1179
|
+
"gmial.com": "gmail.com",
|
|
1180
|
+
"gmaill.com": "gmail.com",
|
|
1181
|
+
"gmai.com": "gmail.com",
|
|
1182
|
+
"gamil.com": "gmail.com",
|
|
1183
|
+
"gnail.com": "gmail.com",
|
|
1184
|
+
"gmal.com": "gmail.com",
|
|
1185
|
+
"gmil.com": "gmail.com",
|
|
1186
|
+
"gmail.co": "gmail.com",
|
|
1187
|
+
"gmail.cm": "gmail.com",
|
|
1188
|
+
"gmail.om": "gmail.com",
|
|
1189
|
+
"gmail.con": "gmail.com",
|
|
1190
|
+
"gmail.cim": "gmail.com",
|
|
1191
|
+
"gmail.comm": "gmail.com",
|
|
1192
|
+
"yahooo.com": "yahoo.com",
|
|
1193
|
+
"yaho.com": "yahoo.com",
|
|
1194
|
+
"yahoo.co": "yahoo.com",
|
|
1195
|
+
"yahoo.cm": "yahoo.com",
|
|
1196
|
+
"yahoo.con": "yahoo.com",
|
|
1197
|
+
"yahho.com": "yahoo.com",
|
|
1198
|
+
"hotmial.com": "hotmail.com",
|
|
1199
|
+
"hotmal.com": "hotmail.com",
|
|
1200
|
+
"hotmai.com": "hotmail.com",
|
|
1201
|
+
"hotmil.com": "hotmail.com",
|
|
1202
|
+
"hotmail.co": "hotmail.com",
|
|
1203
|
+
"hotmail.cm": "hotmail.com",
|
|
1204
|
+
"hotmail.con": "hotmail.com",
|
|
1205
|
+
"outlok.com": "outlook.com",
|
|
1206
|
+
"outloo.com": "outlook.com",
|
|
1207
|
+
"outlook.co": "outlook.com",
|
|
1208
|
+
"outlook.cm": "outlook.com",
|
|
1209
|
+
"protonmal.com": "protonmail.com",
|
|
1210
|
+
"protonmail.co": "protonmail.com",
|
|
1211
|
+
"icloud.co": "icloud.com",
|
|
1212
|
+
"icloud.cm": "icloud.com",
|
|
1213
|
+
"icoud.com": "icloud.com"
|
|
1214
|
+
};
|
|
1215
|
+
function invalidResult(reason, email) {
|
|
1216
|
+
return {
|
|
1217
|
+
valid: false,
|
|
1218
|
+
reason,
|
|
1219
|
+
suggestion: null,
|
|
1220
|
+
isFree: false,
|
|
1221
|
+
isDisposable: false,
|
|
1222
|
+
normalized: email
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
function validateEmail(email, options = {}) {
|
|
1226
|
+
const {
|
|
1227
|
+
checkDisposable = true,
|
|
1228
|
+
suggestTypoFix = true,
|
|
1229
|
+
blockedDomains = [],
|
|
1230
|
+
allowedDomains = []
|
|
1231
|
+
} = options;
|
|
1232
|
+
const normalized = email.trim().toLowerCase();
|
|
1233
|
+
if (!normalized || normalized.length > MAX_EMAIL_LENGTH) {
|
|
1234
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1235
|
+
}
|
|
1236
|
+
const atIndex = normalized.lastIndexOf("@");
|
|
1237
|
+
if (atIndex === -1) {
|
|
1238
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1239
|
+
}
|
|
1240
|
+
const localPart = normalized.slice(0, atIndex);
|
|
1241
|
+
const domain = normalized.slice(atIndex + 1);
|
|
1242
|
+
if (localPart.length === 0 || localPart.length > MAX_LOCAL_LENGTH) {
|
|
1243
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1244
|
+
}
|
|
1245
|
+
if (domain.length === 0 || domain.length > MAX_DOMAIN_LENGTH) {
|
|
1246
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1247
|
+
}
|
|
1248
|
+
if (localPart.includes("..")) {
|
|
1249
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1250
|
+
}
|
|
1251
|
+
if (localPart.startsWith(".") || localPart.endsWith(".")) {
|
|
1252
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1253
|
+
}
|
|
1254
|
+
if (!EMAIL_SYNTAX.test(normalized)) {
|
|
1255
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1256
|
+
}
|
|
1257
|
+
const allowedSet = new Set(allowedDomains.map((d) => d.toLowerCase()));
|
|
1258
|
+
if (allowedSet.has(domain)) {
|
|
1259
|
+
return {
|
|
1260
|
+
valid: true,
|
|
1261
|
+
reason: "valid",
|
|
1262
|
+
suggestion: null,
|
|
1263
|
+
isFree: FREE_PROVIDERS.has(domain),
|
|
1264
|
+
isDisposable: false,
|
|
1265
|
+
normalized
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
const blockedSet = new Set(blockedDomains.map((d) => d.toLowerCase()));
|
|
1269
|
+
if (blockedSet.has(domain)) {
|
|
1270
|
+
return invalidResult("blocked", normalized);
|
|
1271
|
+
}
|
|
1272
|
+
const isDisposable = DISPOSABLE_DOMAINS.has(domain);
|
|
1273
|
+
if (checkDisposable && isDisposable) {
|
|
1274
|
+
return {
|
|
1275
|
+
valid: false,
|
|
1276
|
+
reason: "disposable",
|
|
1277
|
+
suggestion: null,
|
|
1278
|
+
isFree: false,
|
|
1279
|
+
isDisposable: true,
|
|
1280
|
+
normalized
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
const isFree = FREE_PROVIDERS.has(domain);
|
|
1284
|
+
if (suggestTypoFix && DOMAIN_TYPOS[domain]) {
|
|
1285
|
+
const corrected = `${localPart}@${DOMAIN_TYPOS[domain]}`;
|
|
1286
|
+
return {
|
|
1287
|
+
valid: true,
|
|
1288
|
+
reason: "typo",
|
|
1289
|
+
suggestion: corrected,
|
|
1290
|
+
isFree: FREE_PROVIDERS.has(DOMAIN_TYPOS[domain]),
|
|
1291
|
+
isDisposable: false,
|
|
1292
|
+
normalized
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
return {
|
|
1296
|
+
valid: true,
|
|
1297
|
+
reason: "valid",
|
|
1298
|
+
suggestion: null,
|
|
1299
|
+
isFree,
|
|
1300
|
+
isDisposable,
|
|
1301
|
+
normalized
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
966
1305
|
// src/logging/redactor.ts
|
|
967
1306
|
var LOG_LEVELS = {
|
|
968
1307
|
debug: 0,
|
|
@@ -1035,10 +1374,192 @@ function redactString(str, maxLength, patterns) {
|
|
|
1035
1374
|
return safe;
|
|
1036
1375
|
}
|
|
1037
1376
|
|
|
1377
|
+
// src/telemetry/client.ts
|
|
1378
|
+
var DEFAULT_BATCH_SIZE = 50;
|
|
1379
|
+
var MAX_BATCH_SIZE = 500;
|
|
1380
|
+
var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
|
|
1381
|
+
var MIN_FLUSH_INTERVAL_MS = 500;
|
|
1382
|
+
var FLUSH_TIMEOUT_MS = 1e4;
|
|
1383
|
+
var TelemetryClient = class {
|
|
1384
|
+
constructor(options) {
|
|
1385
|
+
this.queue = [];
|
|
1386
|
+
this.flushing = false;
|
|
1387
|
+
this.closed = false;
|
|
1388
|
+
if (!options.endpoint || typeof options.endpoint !== "string") {
|
|
1389
|
+
throw new TypeError("TelemetryClient: `endpoint` is required");
|
|
1390
|
+
}
|
|
1391
|
+
this.endpoint = options.endpoint;
|
|
1392
|
+
this.apiKey = options.apiKey;
|
|
1393
|
+
this.workspaceId = options.workspaceId;
|
|
1394
|
+
this.batchSize = clamp(
|
|
1395
|
+
options.batchSize ?? DEFAULT_BATCH_SIZE,
|
|
1396
|
+
1,
|
|
1397
|
+
MAX_BATCH_SIZE
|
|
1398
|
+
);
|
|
1399
|
+
this.flushIntervalMs = Math.max(
|
|
1400
|
+
options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
1401
|
+
MIN_FLUSH_INTERVAL_MS
|
|
1402
|
+
);
|
|
1403
|
+
this.onError = options.onError ?? (() => {
|
|
1404
|
+
});
|
|
1405
|
+
this.startTimer();
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Enqueue an event. Fast, synchronous, cannot throw.
|
|
1409
|
+
* Triggers a flush if the queue has reached `batchSize`.
|
|
1410
|
+
*/
|
|
1411
|
+
record(event) {
|
|
1412
|
+
if (this.closed) return;
|
|
1413
|
+
this.queue.push(event);
|
|
1414
|
+
if (this.queue.length >= this.batchSize) {
|
|
1415
|
+
void this.flush();
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Manually flush the queue. Pulls up to `batchSize` events into a batch and
|
|
1420
|
+
* POSTs them. Returns a resolved promise on success OR on handled failure.
|
|
1421
|
+
* Never throws.
|
|
1422
|
+
*/
|
|
1423
|
+
async flush() {
|
|
1424
|
+
if (this.flushing) return;
|
|
1425
|
+
if (this.queue.length === 0) return;
|
|
1426
|
+
this.flushing = true;
|
|
1427
|
+
try {
|
|
1428
|
+
const batch = this.queue.splice(0, this.batchSize);
|
|
1429
|
+
await this.send(batch);
|
|
1430
|
+
} catch (err) {
|
|
1431
|
+
this.safeNotify(err);
|
|
1432
|
+
} finally {
|
|
1433
|
+
this.flushing = false;
|
|
1434
|
+
}
|
|
1435
|
+
if (!this.closed && this.queue.length > 0) {
|
|
1436
|
+
void this.flush();
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Shut down: stop the interval timer and attempt one final flush.
|
|
1441
|
+
* Safe to call multiple times.
|
|
1442
|
+
*/
|
|
1443
|
+
async close() {
|
|
1444
|
+
if (this.closed) return;
|
|
1445
|
+
this.closed = true;
|
|
1446
|
+
if (this.timer !== void 0) {
|
|
1447
|
+
clearInterval(this.timer);
|
|
1448
|
+
this.timer = void 0;
|
|
1449
|
+
}
|
|
1450
|
+
if (this.signalHandler !== void 0) {
|
|
1451
|
+
process.off("SIGTERM", this.signalHandler);
|
|
1452
|
+
process.off("SIGINT", this.signalHandler);
|
|
1453
|
+
this.signalHandler = void 0;
|
|
1454
|
+
}
|
|
1455
|
+
try {
|
|
1456
|
+
await this.flush();
|
|
1457
|
+
} catch {
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Register `SIGTERM` / `SIGINT` handlers that call `close()` to drain
|
|
1462
|
+
* the queue on graceful shutdown. Opt-in — libraries should not silently
|
|
1463
|
+
* attach global signal handlers. Safe to call multiple times.
|
|
1464
|
+
*/
|
|
1465
|
+
installShutdownHooks() {
|
|
1466
|
+
if (this.signalHandler !== void 0 || this.closed) return;
|
|
1467
|
+
const handler = () => {
|
|
1468
|
+
void this.close();
|
|
1469
|
+
};
|
|
1470
|
+
this.signalHandler = handler;
|
|
1471
|
+
process.once("SIGTERM", handler);
|
|
1472
|
+
process.once("SIGINT", handler);
|
|
1473
|
+
}
|
|
1474
|
+
/** Count of events currently waiting to be sent. Useful for tests. */
|
|
1475
|
+
get pendingCount() {
|
|
1476
|
+
return this.queue.length;
|
|
1477
|
+
}
|
|
1478
|
+
// ── internals ─────────────────────────────────────────────────────────
|
|
1479
|
+
async send(batch) {
|
|
1480
|
+
const headers = {
|
|
1481
|
+
"content-type": "application/json"
|
|
1482
|
+
};
|
|
1483
|
+
if (this.apiKey) headers["authorization"] = `Bearer ${this.apiKey}`;
|
|
1484
|
+
if (this.workspaceId) headers["x-workspace-id"] = this.workspaceId;
|
|
1485
|
+
const controller = new AbortController();
|
|
1486
|
+
const abortTimer = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
|
|
1487
|
+
try {
|
|
1488
|
+
const res = await fetch(this.endpoint, {
|
|
1489
|
+
method: "POST",
|
|
1490
|
+
headers,
|
|
1491
|
+
body: JSON.stringify({ events: batch }),
|
|
1492
|
+
signal: controller.signal
|
|
1493
|
+
});
|
|
1494
|
+
if (!res.ok) {
|
|
1495
|
+
const text = await safeReadBody(res);
|
|
1496
|
+
throw new TelemetryHttpError(res.status, text);
|
|
1497
|
+
}
|
|
1498
|
+
} finally {
|
|
1499
|
+
clearTimeout(abortTimer);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
startTimer() {
|
|
1503
|
+
this.timer = setInterval(() => {
|
|
1504
|
+
void this.flush();
|
|
1505
|
+
}, this.flushIntervalMs);
|
|
1506
|
+
this.timer.unref?.();
|
|
1507
|
+
}
|
|
1508
|
+
safeNotify(err) {
|
|
1509
|
+
try {
|
|
1510
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
1511
|
+
} catch {
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
var TelemetryHttpError = class extends Error {
|
|
1516
|
+
constructor(status, responseBody) {
|
|
1517
|
+
super(`Telemetry ingest returned HTTP ${status}`);
|
|
1518
|
+
this.status = status;
|
|
1519
|
+
this.responseBody = responseBody;
|
|
1520
|
+
this.name = "TelemetryHttpError";
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
function clamp(value, min, max) {
|
|
1524
|
+
if (!Number.isFinite(value)) return min;
|
|
1525
|
+
return Math.max(min, Math.min(max, Math.trunc(value)));
|
|
1526
|
+
}
|
|
1527
|
+
async function safeReadBody(res) {
|
|
1528
|
+
try {
|
|
1529
|
+
const text = await res.text();
|
|
1530
|
+
return text.slice(0, 500);
|
|
1531
|
+
} catch {
|
|
1532
|
+
return "";
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1038
1536
|
// src/middleware/main.ts
|
|
1537
|
+
function buildTelemetryFromEnv() {
|
|
1538
|
+
const env = typeof process !== "undefined" ? process.env : void 0;
|
|
1539
|
+
const endpoint = env?.ARCIS_ENDPOINT;
|
|
1540
|
+
if (!endpoint) return void 0;
|
|
1541
|
+
const opts = { endpoint };
|
|
1542
|
+
if (env?.ARCIS_WORKSPACE_ID) opts.workspaceId = env.ARCIS_WORKSPACE_ID;
|
|
1543
|
+
if (env?.ARCIS_KEY) opts.apiKey = env.ARCIS_KEY;
|
|
1544
|
+
const batch = env?.ARCIS_BATCH_SIZE ? parseInt(env.ARCIS_BATCH_SIZE, 10) : NaN;
|
|
1545
|
+
if (!Number.isNaN(batch)) opts.batchSize = batch;
|
|
1546
|
+
const flush = env?.ARCIS_FLUSH_INTERVAL_MS ? parseInt(env.ARCIS_FLUSH_INTERVAL_MS, 10) : NaN;
|
|
1547
|
+
if (!Number.isNaN(flush)) opts.flushIntervalMs = flush;
|
|
1548
|
+
return opts;
|
|
1549
|
+
}
|
|
1039
1550
|
function arcis(options = {}) {
|
|
1040
1551
|
const middlewares = [];
|
|
1041
1552
|
const cleanupFns = [];
|
|
1553
|
+
let telemetryClient;
|
|
1554
|
+
const telemetryOpts = options.telemetry?.endpoint ? options.telemetry : buildTelemetryFromEnv();
|
|
1555
|
+
if (telemetryOpts) {
|
|
1556
|
+
const client = new TelemetryClient(telemetryOpts);
|
|
1557
|
+
telemetryClient = client;
|
|
1558
|
+
middlewares.push(createTelemetryEmitter(client));
|
|
1559
|
+
cleanupFns.push(() => {
|
|
1560
|
+
void client.close();
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1042
1563
|
if (options.headers !== false) {
|
|
1043
1564
|
const headerOpts = typeof options.headers === "object" ? options.headers : {};
|
|
1044
1565
|
middlewares.push(createHeaders(headerOpts));
|
|
@@ -1051,7 +1572,8 @@ function arcis(options = {}) {
|
|
|
1051
1572
|
}
|
|
1052
1573
|
if (options.sanitize !== false) {
|
|
1053
1574
|
const sanitizeOpts = typeof options.sanitize === "object" ? options.sanitize : {};
|
|
1054
|
-
|
|
1575
|
+
const sanitizer = createSanitizer(sanitizeOpts);
|
|
1576
|
+
middlewares.push(telemetryClient ? tapSanitizerThreats(sanitizer) : sanitizer);
|
|
1055
1577
|
}
|
|
1056
1578
|
const result = middlewares;
|
|
1057
1579
|
result.close = () => {
|
|
@@ -1364,6 +1886,16 @@ function secureCookieDefaults(options = {}) {
|
|
|
1364
1886
|
sameSite: options.sameSite ?? "Lax",
|
|
1365
1887
|
path: options.path
|
|
1366
1888
|
};
|
|
1889
|
+
if (resolved.sameSite === "None" && resolved.secure === false) {
|
|
1890
|
+
throw new Error(
|
|
1891
|
+
"[arcis] secureCookieDefaults: sameSite=None requires secure=true (modern browsers reject the cookie otherwise)"
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
if (resolved.httpOnly === false && resolved.secure === false && isProduction) {
|
|
1895
|
+
console.warn(
|
|
1896
|
+
"[arcis] secureCookieDefaults: httpOnly and secure are both disabled in production \u2014 cookies will be readable by JS and sent over HTTP"
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1367
1899
|
return (_req, res, next) => {
|
|
1368
1900
|
const originalSetHeader = res.setHeader.bind(res);
|
|
1369
1901
|
res.setHeader = function patchedSetHeader(name, value) {
|
|
@@ -1496,7 +2028,16 @@ function detectBehavioralSignals(req) {
|
|
|
1496
2028
|
}
|
|
1497
2029
|
function detectBot(req) {
|
|
1498
2030
|
const rawUa = req.headers["user-agent"] ?? "";
|
|
1499
|
-
|
|
2031
|
+
if (rawUa.length > 2048) {
|
|
2032
|
+
return {
|
|
2033
|
+
isBot: true,
|
|
2034
|
+
category: "UNKNOWN",
|
|
2035
|
+
name: null,
|
|
2036
|
+
confidence: 0.9,
|
|
2037
|
+
signals: detectBehavioralSignals(req)
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
const ua = rawUa;
|
|
1500
2041
|
const signals = detectBehavioralSignals(req);
|
|
1501
2042
|
if (!ua) {
|
|
1502
2043
|
return {
|
|
@@ -1586,11 +2127,9 @@ function generateCsrfToken(length = 32) {
|
|
|
1586
2127
|
function validateCsrfToken(cookieToken, requestToken) {
|
|
1587
2128
|
if (!cookieToken || !requestToken) return false;
|
|
1588
2129
|
if (cookieToken.length !== requestToken.length) return false;
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
}
|
|
1593
|
-
return result === 0;
|
|
2130
|
+
const a = Buffer.from(cookieToken);
|
|
2131
|
+
const b = Buffer.from(requestToken);
|
|
2132
|
+
return crypto.timingSafeEqual(a, b);
|
|
1594
2133
|
}
|
|
1595
2134
|
function getRequestToken(req, headerName, fieldName) {
|
|
1596
2135
|
const headerToken = req.headers[headerName.toLowerCase()];
|
|
@@ -1599,10 +2138,6 @@ function getRequestToken(req, headerName, fieldName) {
|
|
|
1599
2138
|
const bodyToken = req.body[fieldName];
|
|
1600
2139
|
if (typeof bodyToken === "string" && bodyToken) return bodyToken;
|
|
1601
2140
|
}
|
|
1602
|
-
if (req.query && fieldName in req.query) {
|
|
1603
|
-
const queryToken = req.query[fieldName];
|
|
1604
|
-
if (typeof queryToken === "string" && queryToken) return queryToken;
|
|
1605
|
-
}
|
|
1606
2141
|
return void 0;
|
|
1607
2142
|
}
|
|
1608
2143
|
function csrfProtection(options = {}) {
|
|
@@ -1666,6 +2201,10 @@ function csrfProtection(options = {}) {
|
|
|
1666
2201
|
if (!validateCsrfToken(cookieToken, requestToken)) {
|
|
1667
2202
|
return onError(req, res, next);
|
|
1668
2203
|
}
|
|
2204
|
+
if (options.rotateOnUse) {
|
|
2205
|
+
const freshToken = generateCsrfToken(tokenLength);
|
|
2206
|
+
setCsrfCookie(res, cookieName, freshToken, cookieOpts);
|
|
2207
|
+
}
|
|
1669
2208
|
next();
|
|
1670
2209
|
};
|
|
1671
2210
|
}
|
|
@@ -1685,16 +2224,108 @@ function setCsrfCookie(res, name, token, opts) {
|
|
|
1685
2224
|
if (opts.secure) parts.push("Secure");
|
|
1686
2225
|
parts.push(`SameSite=${opts.sameSite}`);
|
|
1687
2226
|
if (opts.domain) parts.push(`Domain=${opts.domain}`);
|
|
1688
|
-
|
|
2227
|
+
const newCookie = parts.join("; ");
|
|
2228
|
+
const existing = res.getHeader("Set-Cookie");
|
|
2229
|
+
if (existing === void 0) {
|
|
2230
|
+
res.setHeader("Set-Cookie", newCookie);
|
|
2231
|
+
} else if (Array.isArray(existing)) {
|
|
2232
|
+
res.setHeader("Set-Cookie", [...existing, newCookie]);
|
|
2233
|
+
} else {
|
|
2234
|
+
res.setHeader("Set-Cookie", [existing, newCookie]);
|
|
2235
|
+
}
|
|
1689
2236
|
}
|
|
1690
2237
|
function escapeRegex(str) {
|
|
1691
2238
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1692
2239
|
}
|
|
1693
2240
|
var createCsrf = csrfProtection;
|
|
1694
2241
|
|
|
2242
|
+
// src/middleware/signup-protection.ts
|
|
2243
|
+
function checkSignup(req, options = {}) {
|
|
2244
|
+
const {
|
|
2245
|
+
emailField = "email",
|
|
2246
|
+
checkEmail = true,
|
|
2247
|
+
blockDisposable = true,
|
|
2248
|
+
checkBot = true,
|
|
2249
|
+
allowedBotCategories = [],
|
|
2250
|
+
allowedEmailDomains = [],
|
|
2251
|
+
blockedEmailDomains = []
|
|
2252
|
+
} = options;
|
|
2253
|
+
if (checkBot) {
|
|
2254
|
+
const bot = detectBot(req);
|
|
2255
|
+
if (bot.isBot && !allowedBotCategories.includes(bot.category)) {
|
|
2256
|
+
return {
|
|
2257
|
+
allowed: false,
|
|
2258
|
+
reason: "bot",
|
|
2259
|
+
details: { category: bot.category, name: bot.name, confidence: bot.confidence }
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
if (checkEmail) {
|
|
2264
|
+
const email = req.body?.[emailField];
|
|
2265
|
+
if (typeof email !== "string" || email.length === 0) {
|
|
2266
|
+
return { allowed: false, reason: "missing_email" };
|
|
2267
|
+
}
|
|
2268
|
+
const result = validateEmail(email, {
|
|
2269
|
+
checkDisposable: blockDisposable,
|
|
2270
|
+
allowedDomains: allowedEmailDomains,
|
|
2271
|
+
blockedDomains: blockedEmailDomains
|
|
2272
|
+
});
|
|
2273
|
+
if (!result.valid) {
|
|
2274
|
+
const reason = result.reason === "disposable" ? "disposable_email" : "invalid_email";
|
|
2275
|
+
return { allowed: false, reason, details: { emailReason: result.reason } };
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
return { allowed: true, reason: "ok" };
|
|
2279
|
+
}
|
|
2280
|
+
function signupProtection(options = {}) {
|
|
2281
|
+
const rateLimitCfg = options.rateLimit;
|
|
2282
|
+
const limiter = rateLimitCfg === false ? null : createRateLimiter({
|
|
2283
|
+
max: rateLimitCfg?.max ?? 5,
|
|
2284
|
+
windowMs: rateLimitCfg?.windowMs ?? 6e4,
|
|
2285
|
+
message: "Too many signup attempts"
|
|
2286
|
+
});
|
|
2287
|
+
const handler = (req, res, next) => {
|
|
2288
|
+
const result = checkSignup(req, options);
|
|
2289
|
+
if (!result.allowed) {
|
|
2290
|
+
options.onBlocked?.(req, result);
|
|
2291
|
+
const status = result.reason === "bot" ? 403 : 400;
|
|
2292
|
+
res.status(status).json({ error: "signup_blocked", reason: result.reason });
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
if (limiter) {
|
|
2296
|
+
let rateLimited = false;
|
|
2297
|
+
const rateLimitNext = (err) => {
|
|
2298
|
+
if (err) return next(err);
|
|
2299
|
+
if (!rateLimited) next();
|
|
2300
|
+
};
|
|
2301
|
+
const patchedRes = new Proxy(res, {
|
|
2302
|
+
get(target, prop) {
|
|
2303
|
+
if (prop === "status") {
|
|
2304
|
+
return (code) => {
|
|
2305
|
+
if (code === 429) {
|
|
2306
|
+
rateLimited = true;
|
|
2307
|
+
options.onBlocked?.(req, { allowed: false, reason: "rate_limited" });
|
|
2308
|
+
}
|
|
2309
|
+
return target.status.call(target, code);
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
return Reflect.get(target, prop);
|
|
2313
|
+
}
|
|
2314
|
+
});
|
|
2315
|
+
limiter(req, patchedRes, rateLimitNext);
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
next();
|
|
2319
|
+
};
|
|
2320
|
+
const middleware = handler;
|
|
2321
|
+
middleware.close = () => limiter?.close();
|
|
2322
|
+
return middleware;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
1695
2325
|
exports.arcis = arcis;
|
|
1696
2326
|
exports.arcisFunction = arcisWithMethods;
|
|
1697
2327
|
exports.botProtection = botProtection;
|
|
2328
|
+
exports.checkSignup = checkSignup;
|
|
1698
2329
|
exports.createCors = createCors;
|
|
1699
2330
|
exports.createCsrf = createCsrf;
|
|
1700
2331
|
exports.createErrorHandler = createErrorHandler;
|
|
@@ -1713,6 +2344,7 @@ exports.rateLimit = rateLimit;
|
|
|
1713
2344
|
exports.safeCors = safeCors;
|
|
1714
2345
|
exports.secureCookieDefaults = secureCookieDefaults;
|
|
1715
2346
|
exports.securityHeaders = securityHeaders;
|
|
2347
|
+
exports.signupProtection = signupProtection;
|
|
1716
2348
|
exports.validateCsrfToken = validateCsrfToken;
|
|
1717
2349
|
//# sourceMappingURL=index.js.map
|
|
1718
2350
|
//# sourceMappingURL=index.js.map
|