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