@arcis/node 1.4.2 → 1.4.4
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 +11 -3
- package/dist/cli/arcis.d.ts +23 -0
- package/dist/cli/arcis.d.ts.map +1 -0
- package/dist/cli/arcis.js +312 -0
- package/dist/cli/arcis.js.map +1 -0
- package/dist/cli/arcis.mjs +309 -0
- package/dist/cli/arcis.mjs.map +1 -0
- package/dist/core/constants.d.ts +2 -2
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/index.js +4 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +4 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/types.d.ts +17 -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 +658 -161
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +655 -162
- 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 +833 -12
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +832 -13
- 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/index.d.ts +2 -1
- package/dist/sanitizers/index.d.ts.map +1 -1
- package/dist/sanitizers/index.js +238 -152
- package/dist/sanitizers/index.js.map +1 -1
- package/dist/sanitizers/index.mjs +238 -153
- package/dist/sanitizers/index.mjs.map +1 -1
- package/dist/sanitizers/pii.d.ts.map +1 -1
- package/dist/sanitizers/sanitize.d.ts +13 -0
- 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.map +1 -1
- package/dist/stores/index.mjs.map +1 -1
- package/dist/telemetry/client.d.ts +63 -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 +71 -0
- package/dist/telemetry/types.d.ts.map +1 -0
- package/dist/validation/index.js +3 -0
- package/dist/validation/index.js.map +1 -1
- package/dist/validation/index.mjs +3 -0
- package/dist/validation/index.mjs.map +1 -1
- package/package.json +10 -1
|
@@ -40,11 +40,47 @@ var HEADERS = {
|
|
|
40
40
|
/** Default Cache-Control value for security */
|
|
41
41
|
CACHE_CONTROL: "no-store, no-cache, must-revalidate, proxy-revalidate"
|
|
42
42
|
};
|
|
43
|
+
var XSS_PATTERNS = [
|
|
44
|
+
/** Script tags (ReDoS-safe version) */
|
|
45
|
+
/<script[^>]*>[\s\S]*?<\/script>/gi,
|
|
46
|
+
/** javascript: protocol (allow optional spaces before colon) */
|
|
47
|
+
/javascript\s*:/gi,
|
|
48
|
+
/** vbscript: protocol */
|
|
49
|
+
/vbscript\s*:/gi,
|
|
50
|
+
/** Event handlers (onclick, onerror, etc.) — any separator before attribute */
|
|
51
|
+
/(?:[\s/])on\w+\s*=/gi,
|
|
52
|
+
/** iframe tags */
|
|
53
|
+
/<iframe/gi,
|
|
54
|
+
/** object tags */
|
|
55
|
+
/<object/gi,
|
|
56
|
+
/** embed tags */
|
|
57
|
+
/<embed/gi,
|
|
58
|
+
/** data: URIs (only dangerous ones, avoid false positives) */
|
|
59
|
+
/(?:^|[\s"'=])data:/gi,
|
|
60
|
+
/** URL-encoded script tags */
|
|
61
|
+
/%3Cscript/gi,
|
|
62
|
+
/** SVG with onload */
|
|
63
|
+
/<svg[^>]*onload/gi,
|
|
64
|
+
/** form tags — phishing/credential harvesting via action= redirection */
|
|
65
|
+
/<form[\s>]/gi,
|
|
66
|
+
/** meta tags — http-equiv refresh redirects or CSP bypass */
|
|
67
|
+
/<meta[\s>]/gi,
|
|
68
|
+
/** base href hijacking — redirects all relative URLs to attacker domain */
|
|
69
|
+
/<base[\s>]/gi,
|
|
70
|
+
/** link tag injection — stylesheet or preload CSRF attacks */
|
|
71
|
+
/<link[\s>]/gi,
|
|
72
|
+
/** style tag — CSS expression() / behavior: / IE-era attacks. Mirrors
|
|
73
|
+
* Python's xss-style-tag from packages/core/patterns.json. */
|
|
74
|
+
/<style[\s>]/gi
|
|
75
|
+
];
|
|
43
76
|
var XSS_REMOVE_PATTERNS = [
|
|
44
77
|
/** Full script blocks (content + tags) */
|
|
45
78
|
/<script[^>]*>[\s\S]*?<\/script>/gi,
|
|
46
79
|
/** Standalone/unclosed script tags */
|
|
47
80
|
/<script[^>]*>/gi,
|
|
81
|
+
/** style — CSS expression() and behavior: attacks (IE-era but still relevant) */
|
|
82
|
+
/<style[^>]*>[\s\S]*?<\/style>/gi,
|
|
83
|
+
/<style[^>]*/gi,
|
|
48
84
|
/** iframe — full block and partial/unclosed */
|
|
49
85
|
/<iframe[^>]*>[\s\S]*?<\/iframe>/gi,
|
|
50
86
|
/<iframe[^>]*/gi,
|
|
@@ -357,13 +393,13 @@ function createRateLimiter(options = {}) {
|
|
|
357
393
|
statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
|
|
358
394
|
keyGenerator = (req) => {
|
|
359
395
|
const ip = req.ip ?? req.socket?.remoteAddress;
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return
|
|
396
|
+
if (ip) return ip;
|
|
397
|
+
const ua = req.headers["user-agent"] ?? "";
|
|
398
|
+
const lang = req.headers["accept-language"] ?? "";
|
|
399
|
+
const fp = `${ua}|${lang}`;
|
|
400
|
+
let hash = 0;
|
|
401
|
+
for (let i = 0; i < fp.length; i++) hash = (hash << 5) - hash + fp.charCodeAt(i) | 0;
|
|
402
|
+
return `unknown:${hash.toString(36)}`;
|
|
367
403
|
},
|
|
368
404
|
skip,
|
|
369
405
|
store: externalStore
|
|
@@ -555,6 +591,80 @@ var SecurityThreatError = class extends ArcisError {
|
|
|
555
591
|
}
|
|
556
592
|
};
|
|
557
593
|
|
|
594
|
+
// src/middleware/telemetry.ts
|
|
595
|
+
var THREAT_TO_VECTOR = {
|
|
596
|
+
xss: "xss",
|
|
597
|
+
sql_injection: "sql",
|
|
598
|
+
nosql_injection: "nosql",
|
|
599
|
+
path_traversal: "path",
|
|
600
|
+
command_injection: "command",
|
|
601
|
+
prototype_pollution: "prototype",
|
|
602
|
+
header_injection: "header",
|
|
603
|
+
ssti: "ssti",
|
|
604
|
+
xxe: "xxe"
|
|
605
|
+
};
|
|
606
|
+
function createTelemetryEmitter(client) {
|
|
607
|
+
return (req, res, next) => {
|
|
608
|
+
const start = performance.now();
|
|
609
|
+
res.on("finish", () => {
|
|
610
|
+
try {
|
|
611
|
+
const event = buildEvent(req, res.statusCode, performance.now() - start);
|
|
612
|
+
client.record(event);
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
next();
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function tapSanitizerThreats(handler) {
|
|
620
|
+
return (req, res, next) => {
|
|
621
|
+
handler(req, res, (err) => {
|
|
622
|
+
if (err instanceof SecurityThreatError) {
|
|
623
|
+
const vector = THREAT_TO_VECTOR[err.threatType] ?? err.threatType;
|
|
624
|
+
req.__arcis = {
|
|
625
|
+
vector,
|
|
626
|
+
rule: `${vector}/match`,
|
|
627
|
+
severity: "high",
|
|
628
|
+
matchedPattern: err.pattern,
|
|
629
|
+
reason: err.message,
|
|
630
|
+
decision: "deny"
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
next(err);
|
|
634
|
+
});
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function buildEvent(req, status, latencyMs) {
|
|
638
|
+
const marker = req.__arcis;
|
|
639
|
+
const decision = marker?.decision ?? inferDecision(status);
|
|
640
|
+
return {
|
|
641
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
642
|
+
ip: extractIp(req),
|
|
643
|
+
method: (req.method ?? "GET").toUpperCase(),
|
|
644
|
+
path: req.path ?? req.url ?? "/",
|
|
645
|
+
decision,
|
|
646
|
+
vector: marker?.vector ?? (status === 429 ? "rate-limit" : void 0),
|
|
647
|
+
rule: marker?.rule ?? (status === 429 ? "rate-limit/exceeded" : void 0),
|
|
648
|
+
severity: marker?.severity ?? (status === 429 ? "medium" : void 0),
|
|
649
|
+
userAgent: typeof req.headers?.["user-agent"] === "string" ? req.headers["user-agent"] : "",
|
|
650
|
+
reason: marker?.reason,
|
|
651
|
+
status,
|
|
652
|
+
matchedPattern: marker?.matchedPattern,
|
|
653
|
+
latencyMs: Math.max(0, latencyMs)
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function inferDecision(status) {
|
|
657
|
+
if (status === 429) return "deny";
|
|
658
|
+
if (status === 400) return "deny";
|
|
659
|
+
if (status === 403) return "deny";
|
|
660
|
+
return "allow";
|
|
661
|
+
}
|
|
662
|
+
function extractIp(req) {
|
|
663
|
+
if (typeof req.ip === "string" && req.ip.length > 0) return req.ip;
|
|
664
|
+
const remote = req.socket?.remoteAddress;
|
|
665
|
+
return typeof remote === "string" ? remote : "0.0.0.0";
|
|
666
|
+
}
|
|
667
|
+
|
|
558
668
|
// src/sanitizers/utils.ts
|
|
559
669
|
function encodeHtmlEntities(str) {
|
|
560
670
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -600,6 +710,20 @@ function sanitizeXss(input, collectThreats = false, htmlEncode = false) {
|
|
|
600
710
|
}
|
|
601
711
|
return value;
|
|
602
712
|
}
|
|
713
|
+
function detectXss(input) {
|
|
714
|
+
if (typeof input !== "string") return false;
|
|
715
|
+
if (/\s+on\w+\s*=/i.test(input)) return true;
|
|
716
|
+
if (/javascript\s*:/i.test(input)) return true;
|
|
717
|
+
if (/vbscript\s*:/i.test(input)) return true;
|
|
718
|
+
if (/data\s*:\s*text\/html/i.test(input)) return true;
|
|
719
|
+
for (const pattern of XSS_PATTERNS) {
|
|
720
|
+
pattern.lastIndex = 0;
|
|
721
|
+
if (pattern.test(input)) {
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
603
727
|
|
|
604
728
|
// src/sanitizers/sql.ts
|
|
605
729
|
function sanitizeSql(input, collectThreats = false) {
|
|
@@ -683,6 +807,17 @@ function sanitizePath(input, collectThreats = false) {
|
|
|
683
807
|
}
|
|
684
808
|
return value;
|
|
685
809
|
}
|
|
810
|
+
function detectPathTraversal(input) {
|
|
811
|
+
if (typeof input !== "string") return false;
|
|
812
|
+
const normalized = input.normalize("NFKC");
|
|
813
|
+
for (const pattern of PATH_PATTERNS) {
|
|
814
|
+
pattern.lastIndex = 0;
|
|
815
|
+
if (pattern.test(normalized)) {
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
686
821
|
|
|
687
822
|
// src/sanitizers/command.ts
|
|
688
823
|
function sanitizeCommand(input, collectThreats = false) {
|
|
@@ -728,6 +863,60 @@ function detectCommandInjection(input) {
|
|
|
728
863
|
return false;
|
|
729
864
|
}
|
|
730
865
|
|
|
866
|
+
// src/sanitizers/ssti.ts
|
|
867
|
+
var SSTI_DETECT_PATTERNS = [
|
|
868
|
+
/** Jinja2 / Twig / Nunjucks: {{ ... }} */
|
|
869
|
+
/\{\{.*?\}\}/g,
|
|
870
|
+
/** Freemarker / Thymeleaf / Spring EL: ${ ... } */
|
|
871
|
+
/\$\{.*?\}/g,
|
|
872
|
+
/** ERB / EJS: <%= ... %> or <% ... %> */
|
|
873
|
+
/<%[=\-]?.*?%>/gs,
|
|
874
|
+
/** Pug / Jade / Slim: #{ ... } */
|
|
875
|
+
/#\{.*?\}/g,
|
|
876
|
+
/** Python dunder sandbox escape */
|
|
877
|
+
/__(?:class|mro|subclasses|globals|builtins|import)__/gi,
|
|
878
|
+
/** Jinja2 config leak: {{config.X}} or {{config['X']}} */
|
|
879
|
+
/\{\{\s*config[.\[]/gi,
|
|
880
|
+
/** Jinja2 built-in objects */
|
|
881
|
+
/\{\{\s*(?:self|request|lipsum|cycler|joiner|namespace|range)\b/gi
|
|
882
|
+
];
|
|
883
|
+
function detectSsti(input) {
|
|
884
|
+
if (typeof input !== "string") return false;
|
|
885
|
+
for (const pattern of SSTI_DETECT_PATTERNS) {
|
|
886
|
+
pattern.lastIndex = 0;
|
|
887
|
+
if (pattern.test(input)) {
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/sanitizers/xxe.ts
|
|
895
|
+
var XXE_DETECT_PATTERNS = [
|
|
896
|
+
/** DOCTYPE declaration */
|
|
897
|
+
/<!DOCTYPE\b/gi,
|
|
898
|
+
/** ENTITY declaration */
|
|
899
|
+
/<!ENTITY\b/gi,
|
|
900
|
+
/** SYSTEM keyword with URI */
|
|
901
|
+
/\bSYSTEM\s+["']/gi,
|
|
902
|
+
/** PUBLIC keyword with URI */
|
|
903
|
+
/\bPUBLIC\s+["']/gi,
|
|
904
|
+
/** Parameter entity reference (%entity;) */
|
|
905
|
+
/%\s*\w+\s*;/g,
|
|
906
|
+
/** CDATA section (often used to smuggle payloads) */
|
|
907
|
+
/<!\[CDATA\[/gi
|
|
908
|
+
];
|
|
909
|
+
function detectXxe(input) {
|
|
910
|
+
if (typeof input !== "string") return false;
|
|
911
|
+
for (const pattern of XXE_DETECT_PATTERNS) {
|
|
912
|
+
pattern.lastIndex = 0;
|
|
913
|
+
if (pattern.test(input)) {
|
|
914
|
+
return true;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
|
|
731
920
|
// src/sanitizers/sanitize.ts
|
|
732
921
|
function sanitizeString(value, options = {}) {
|
|
733
922
|
if (typeof value !== "string") return value;
|
|
@@ -797,9 +986,73 @@ function sanitizeObjectDepth(obj, options, depth) {
|
|
|
797
986
|
}
|
|
798
987
|
return result;
|
|
799
988
|
}
|
|
989
|
+
function scanThreats(data, depth = 0) {
|
|
990
|
+
if (depth > INPUT.MAX_RECURSION_DEPTH) return null;
|
|
991
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
992
|
+
for (const key of Object.keys(data)) {
|
|
993
|
+
const lower = key.toLowerCase();
|
|
994
|
+
if (DANGEROUS_PROTO_KEYS.has(lower)) {
|
|
995
|
+
return { vector: "prototype", rule: "prototype/match", matchedPattern: key };
|
|
996
|
+
}
|
|
997
|
+
if (NOSQL_DANGEROUS_KEYS.has(key)) {
|
|
998
|
+
return { vector: "nosql", rule: "nosql/match", matchedPattern: key };
|
|
999
|
+
}
|
|
1000
|
+
const inner = scanThreats(data[key], depth + 1);
|
|
1001
|
+
if (inner) return inner;
|
|
1002
|
+
}
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
if (Array.isArray(data)) {
|
|
1006
|
+
for (const item of data) {
|
|
1007
|
+
const inner = scanThreats(item, depth + 1);
|
|
1008
|
+
if (inner) return inner;
|
|
1009
|
+
}
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
if (typeof data !== "string") return null;
|
|
1013
|
+
const sample = data.slice(0, 80);
|
|
1014
|
+
if (detectXss(data)) {
|
|
1015
|
+
return { vector: "xss", rule: "xss/match", matchedPattern: sample };
|
|
1016
|
+
}
|
|
1017
|
+
if (detectSsti(data)) {
|
|
1018
|
+
return { vector: "ssti", rule: "ssti/match", matchedPattern: sample };
|
|
1019
|
+
}
|
|
1020
|
+
if (detectXxe(data)) {
|
|
1021
|
+
return { vector: "xxe", rule: "xxe/match", matchedPattern: sample };
|
|
1022
|
+
}
|
|
1023
|
+
if (detectSql(data)) {
|
|
1024
|
+
return { vector: "sql", rule: "sql/match", matchedPattern: sample };
|
|
1025
|
+
}
|
|
1026
|
+
if (detectPathTraversal(data)) {
|
|
1027
|
+
return { vector: "path", rule: "path/match", matchedPattern: sample };
|
|
1028
|
+
}
|
|
1029
|
+
if (detectCommandInjection(data)) {
|
|
1030
|
+
return { vector: "command", rule: "command/match", matchedPattern: sample };
|
|
1031
|
+
}
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
800
1034
|
function createSanitizer(options = {}) {
|
|
801
|
-
return (req,
|
|
1035
|
+
return (req, res, next) => {
|
|
802
1036
|
try {
|
|
1037
|
+
if (options.block) {
|
|
1038
|
+
const hit = scanThreats(req.body) || scanThreats(req.query) || scanThreats(req.params) || scanThreats(req.path);
|
|
1039
|
+
if (hit) {
|
|
1040
|
+
req.__arcis = {
|
|
1041
|
+
vector: hit.vector,
|
|
1042
|
+
rule: hit.rule,
|
|
1043
|
+
severity: "high",
|
|
1044
|
+
matchedPattern: hit.matchedPattern,
|
|
1045
|
+
reason: `${hit.vector} pattern detected in request`,
|
|
1046
|
+
decision: "deny"
|
|
1047
|
+
};
|
|
1048
|
+
res.status(403).json({
|
|
1049
|
+
error: "Request blocked for security reasons",
|
|
1050
|
+
code: "SECURITY_THREAT",
|
|
1051
|
+
vector: hit.vector
|
|
1052
|
+
});
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
803
1056
|
if (req.body && typeof req.body === "object") {
|
|
804
1057
|
req.body = sanitizeObject(req.body, options);
|
|
805
1058
|
}
|
|
@@ -989,6 +1242,238 @@ function validateField(field, value, rules) {
|
|
|
989
1242
|
// Audio/Video
|
|
990
1243
|
"audio/mpeg": [Buffer.from([255, 251]), Buffer.from([255, 243]), Buffer.from([73, 68, 51])]});
|
|
991
1244
|
|
|
1245
|
+
// src/validation/email.ts
|
|
1246
|
+
var MAX_EMAIL_LENGTH = 254;
|
|
1247
|
+
var MAX_LOCAL_LENGTH = 64;
|
|
1248
|
+
var MAX_DOMAIN_LENGTH = 255;
|
|
1249
|
+
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,}$/;
|
|
1250
|
+
var FREE_PROVIDERS = /* @__PURE__ */ new Set([
|
|
1251
|
+
"gmail.com",
|
|
1252
|
+
"yahoo.com",
|
|
1253
|
+
"hotmail.com",
|
|
1254
|
+
"outlook.com",
|
|
1255
|
+
"aol.com",
|
|
1256
|
+
"protonmail.com",
|
|
1257
|
+
"proton.me",
|
|
1258
|
+
"icloud.com",
|
|
1259
|
+
"mail.com",
|
|
1260
|
+
"zoho.com",
|
|
1261
|
+
"yandex.com",
|
|
1262
|
+
"gmx.com",
|
|
1263
|
+
"gmx.net",
|
|
1264
|
+
"live.com",
|
|
1265
|
+
"msn.com",
|
|
1266
|
+
"me.com",
|
|
1267
|
+
"mac.com",
|
|
1268
|
+
"fastmail.com",
|
|
1269
|
+
"tutanota.com",
|
|
1270
|
+
"hey.com"
|
|
1271
|
+
]);
|
|
1272
|
+
var DISPOSABLE_DOMAINS = /* @__PURE__ */ new Set([
|
|
1273
|
+
// Popular disposable services
|
|
1274
|
+
"guerrillamail.com",
|
|
1275
|
+
"guerrillamail.net",
|
|
1276
|
+
"guerrillamail.org",
|
|
1277
|
+
"tempmail.com",
|
|
1278
|
+
"temp-mail.org",
|
|
1279
|
+
"temp-mail.io",
|
|
1280
|
+
"throwaway.email",
|
|
1281
|
+
"throwaway.com",
|
|
1282
|
+
"mailinator.com",
|
|
1283
|
+
"mailinator.net",
|
|
1284
|
+
"yopmail.com",
|
|
1285
|
+
"yopmail.fr",
|
|
1286
|
+
"yopmail.net",
|
|
1287
|
+
"sharklasers.com",
|
|
1288
|
+
"grr.la",
|
|
1289
|
+
"guerrillamail.info",
|
|
1290
|
+
"guerrillamail.biz",
|
|
1291
|
+
"guerrillamail.de",
|
|
1292
|
+
"trashmail.com",
|
|
1293
|
+
"trashmail.me",
|
|
1294
|
+
"trashmail.net",
|
|
1295
|
+
"dispostable.com",
|
|
1296
|
+
"maildrop.cc",
|
|
1297
|
+
"mailnesia.com",
|
|
1298
|
+
"tempail.com",
|
|
1299
|
+
"mohmal.com",
|
|
1300
|
+
"getnada.com",
|
|
1301
|
+
"emailondeck.com",
|
|
1302
|
+
"discard.email",
|
|
1303
|
+
"fakeinbox.com",
|
|
1304
|
+
"mailcatch.com",
|
|
1305
|
+
"mintemail.com",
|
|
1306
|
+
"tempr.email",
|
|
1307
|
+
"tempinbox.com",
|
|
1308
|
+
"burnermail.io",
|
|
1309
|
+
"mailsac.com",
|
|
1310
|
+
"harakirimail.com",
|
|
1311
|
+
"tempmailo.com",
|
|
1312
|
+
"emailfake.com",
|
|
1313
|
+
"crazymailing.com",
|
|
1314
|
+
"armyspy.com",
|
|
1315
|
+
"dayrep.com",
|
|
1316
|
+
"einrot.com",
|
|
1317
|
+
"fleckens.hu",
|
|
1318
|
+
"gustr.com",
|
|
1319
|
+
"jourrapide.com",
|
|
1320
|
+
"rhyta.com",
|
|
1321
|
+
"superrito.com",
|
|
1322
|
+
"teleworm.us",
|
|
1323
|
+
"10minutemail.com",
|
|
1324
|
+
"10minutemail.net",
|
|
1325
|
+
"minutemail.com",
|
|
1326
|
+
"tempsky.com",
|
|
1327
|
+
"spamgourmet.com",
|
|
1328
|
+
"mytrashmail.com",
|
|
1329
|
+
"mailexpire.com",
|
|
1330
|
+
"safetymail.info",
|
|
1331
|
+
"filzmail.com",
|
|
1332
|
+
"trashymail.com",
|
|
1333
|
+
"sharkmail.com",
|
|
1334
|
+
"jetable.org",
|
|
1335
|
+
"nospam.ze.tc",
|
|
1336
|
+
"trash-me.com",
|
|
1337
|
+
"dodgit.com",
|
|
1338
|
+
"mailmoat.com",
|
|
1339
|
+
"spamfree24.org",
|
|
1340
|
+
"incognitomail.org",
|
|
1341
|
+
"tempomail.fr",
|
|
1342
|
+
"ephemail.net",
|
|
1343
|
+
"hidemail.de",
|
|
1344
|
+
"spaml.de",
|
|
1345
|
+
"uggsrock.com",
|
|
1346
|
+
"binkmail.com",
|
|
1347
|
+
"suremail.info",
|
|
1348
|
+
"bugmenot.com"
|
|
1349
|
+
]);
|
|
1350
|
+
var DOMAIN_TYPOS = {
|
|
1351
|
+
"gmial.com": "gmail.com",
|
|
1352
|
+
"gmaill.com": "gmail.com",
|
|
1353
|
+
"gmai.com": "gmail.com",
|
|
1354
|
+
"gamil.com": "gmail.com",
|
|
1355
|
+
"gnail.com": "gmail.com",
|
|
1356
|
+
"gmal.com": "gmail.com",
|
|
1357
|
+
"gmil.com": "gmail.com",
|
|
1358
|
+
"gmail.co": "gmail.com",
|
|
1359
|
+
"gmail.cm": "gmail.com",
|
|
1360
|
+
"gmail.om": "gmail.com",
|
|
1361
|
+
"gmail.con": "gmail.com",
|
|
1362
|
+
"gmail.cim": "gmail.com",
|
|
1363
|
+
"gmail.comm": "gmail.com",
|
|
1364
|
+
"yahooo.com": "yahoo.com",
|
|
1365
|
+
"yaho.com": "yahoo.com",
|
|
1366
|
+
"yahoo.co": "yahoo.com",
|
|
1367
|
+
"yahoo.cm": "yahoo.com",
|
|
1368
|
+
"yahoo.con": "yahoo.com",
|
|
1369
|
+
"yahho.com": "yahoo.com",
|
|
1370
|
+
"hotmial.com": "hotmail.com",
|
|
1371
|
+
"hotmal.com": "hotmail.com",
|
|
1372
|
+
"hotmai.com": "hotmail.com",
|
|
1373
|
+
"hotmil.com": "hotmail.com",
|
|
1374
|
+
"hotmail.co": "hotmail.com",
|
|
1375
|
+
"hotmail.cm": "hotmail.com",
|
|
1376
|
+
"hotmail.con": "hotmail.com",
|
|
1377
|
+
"outlok.com": "outlook.com",
|
|
1378
|
+
"outloo.com": "outlook.com",
|
|
1379
|
+
"outlook.co": "outlook.com",
|
|
1380
|
+
"outlook.cm": "outlook.com",
|
|
1381
|
+
"protonmal.com": "protonmail.com",
|
|
1382
|
+
"protonmail.co": "protonmail.com",
|
|
1383
|
+
"icloud.co": "icloud.com",
|
|
1384
|
+
"icloud.cm": "icloud.com",
|
|
1385
|
+
"icoud.com": "icloud.com"
|
|
1386
|
+
};
|
|
1387
|
+
function invalidResult(reason, email) {
|
|
1388
|
+
return {
|
|
1389
|
+
valid: false,
|
|
1390
|
+
reason,
|
|
1391
|
+
suggestion: null,
|
|
1392
|
+
isFree: false,
|
|
1393
|
+
isDisposable: false,
|
|
1394
|
+
normalized: email
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
function validateEmail(email, options = {}) {
|
|
1398
|
+
const {
|
|
1399
|
+
checkDisposable = true,
|
|
1400
|
+
suggestTypoFix = true,
|
|
1401
|
+
blockedDomains = [],
|
|
1402
|
+
allowedDomains = []
|
|
1403
|
+
} = options;
|
|
1404
|
+
const normalized = email.trim().toLowerCase();
|
|
1405
|
+
if (!normalized || normalized.length > MAX_EMAIL_LENGTH) {
|
|
1406
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1407
|
+
}
|
|
1408
|
+
const atIndex = normalized.lastIndexOf("@");
|
|
1409
|
+
if (atIndex === -1) {
|
|
1410
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1411
|
+
}
|
|
1412
|
+
const localPart = normalized.slice(0, atIndex);
|
|
1413
|
+
const domain = normalized.slice(atIndex + 1);
|
|
1414
|
+
if (localPart.length === 0 || localPart.length > MAX_LOCAL_LENGTH) {
|
|
1415
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1416
|
+
}
|
|
1417
|
+
if (domain.length === 0 || domain.length > MAX_DOMAIN_LENGTH) {
|
|
1418
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1419
|
+
}
|
|
1420
|
+
if (localPart.includes("..")) {
|
|
1421
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1422
|
+
}
|
|
1423
|
+
if (localPart.startsWith(".") || localPart.endsWith(".")) {
|
|
1424
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1425
|
+
}
|
|
1426
|
+
if (!EMAIL_SYNTAX.test(normalized)) {
|
|
1427
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1428
|
+
}
|
|
1429
|
+
const allowedSet = new Set(allowedDomains.map((d) => d.toLowerCase()));
|
|
1430
|
+
if (allowedSet.has(domain)) {
|
|
1431
|
+
return {
|
|
1432
|
+
valid: true,
|
|
1433
|
+
reason: "valid",
|
|
1434
|
+
suggestion: null,
|
|
1435
|
+
isFree: FREE_PROVIDERS.has(domain),
|
|
1436
|
+
isDisposable: false,
|
|
1437
|
+
normalized
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
const blockedSet = new Set(blockedDomains.map((d) => d.toLowerCase()));
|
|
1441
|
+
if (blockedSet.has(domain)) {
|
|
1442
|
+
return invalidResult("blocked", normalized);
|
|
1443
|
+
}
|
|
1444
|
+
const isDisposable = DISPOSABLE_DOMAINS.has(domain);
|
|
1445
|
+
if (checkDisposable && isDisposable) {
|
|
1446
|
+
return {
|
|
1447
|
+
valid: false,
|
|
1448
|
+
reason: "disposable",
|
|
1449
|
+
suggestion: null,
|
|
1450
|
+
isFree: false,
|
|
1451
|
+
isDisposable: true,
|
|
1452
|
+
normalized
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
const isFree = FREE_PROVIDERS.has(domain);
|
|
1456
|
+
if (suggestTypoFix && DOMAIN_TYPOS[domain]) {
|
|
1457
|
+
const corrected = `${localPart}@${DOMAIN_TYPOS[domain]}`;
|
|
1458
|
+
return {
|
|
1459
|
+
valid: true,
|
|
1460
|
+
reason: "typo",
|
|
1461
|
+
suggestion: corrected,
|
|
1462
|
+
isFree: FREE_PROVIDERS.has(DOMAIN_TYPOS[domain]),
|
|
1463
|
+
isDisposable: false,
|
|
1464
|
+
normalized
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
return {
|
|
1468
|
+
valid: true,
|
|
1469
|
+
reason: "valid",
|
|
1470
|
+
suggestion: null,
|
|
1471
|
+
isFree,
|
|
1472
|
+
isDisposable,
|
|
1473
|
+
normalized
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
|
|
992
1477
|
// src/logging/redactor.ts
|
|
993
1478
|
var LOG_LEVELS = {
|
|
994
1479
|
debug: 0,
|
|
@@ -1061,10 +1546,213 @@ function redactString(str, maxLength, patterns) {
|
|
|
1061
1546
|
return safe;
|
|
1062
1547
|
}
|
|
1063
1548
|
|
|
1549
|
+
// src/telemetry/client.ts
|
|
1550
|
+
var DEFAULT_BATCH_SIZE = 50;
|
|
1551
|
+
var MAX_BATCH_SIZE = 500;
|
|
1552
|
+
var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
|
|
1553
|
+
var MIN_FLUSH_INTERVAL_MS = 500;
|
|
1554
|
+
var FLUSH_TIMEOUT_MS = 1e4;
|
|
1555
|
+
var DEFAULT_MAX_QUEUE_SIZE = 1e4;
|
|
1556
|
+
var TelemetryClient = class {
|
|
1557
|
+
constructor(options) {
|
|
1558
|
+
this.queue = [];
|
|
1559
|
+
this.flushing = false;
|
|
1560
|
+
this.closed = false;
|
|
1561
|
+
// Counts events dropped since the last successful flush. Resets to 0
|
|
1562
|
+
// each flush so onQueueOverflow callbacks see "drops in this window"
|
|
1563
|
+
// rather than a monotonic lifetime counter.
|
|
1564
|
+
this.droppedSinceLastFlush = 0;
|
|
1565
|
+
if (!options.endpoint || typeof options.endpoint !== "string") {
|
|
1566
|
+
throw new TypeError("TelemetryClient: `endpoint` is required");
|
|
1567
|
+
}
|
|
1568
|
+
this.endpoint = options.endpoint;
|
|
1569
|
+
this.apiKey = options.apiKey;
|
|
1570
|
+
this.workspaceId = options.workspaceId;
|
|
1571
|
+
this.batchSize = clamp(
|
|
1572
|
+
options.batchSize ?? DEFAULT_BATCH_SIZE,
|
|
1573
|
+
1,
|
|
1574
|
+
MAX_BATCH_SIZE
|
|
1575
|
+
);
|
|
1576
|
+
this.flushIntervalMs = Math.max(
|
|
1577
|
+
options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
1578
|
+
MIN_FLUSH_INTERVAL_MS
|
|
1579
|
+
);
|
|
1580
|
+
this.maxQueueSize = Math.max(
|
|
1581
|
+
options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
|
|
1582
|
+
this.batchSize
|
|
1583
|
+
);
|
|
1584
|
+
this.onError = options.onError ?? (() => {
|
|
1585
|
+
});
|
|
1586
|
+
this.onQueueOverflow = options.onQueueOverflow ?? (() => {
|
|
1587
|
+
});
|
|
1588
|
+
this.startTimer();
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Enqueue an event. Fast, synchronous, cannot throw.
|
|
1592
|
+
* Triggers a flush if the queue has reached `batchSize`.
|
|
1593
|
+
*/
|
|
1594
|
+
record(event) {
|
|
1595
|
+
if (this.closed) return;
|
|
1596
|
+
this.queue.push(event);
|
|
1597
|
+
if (this.queue.length > this.maxQueueSize) {
|
|
1598
|
+
const drop = this.queue.length - this.maxQueueSize;
|
|
1599
|
+
this.queue.splice(0, drop);
|
|
1600
|
+
this.droppedSinceLastFlush += drop;
|
|
1601
|
+
try {
|
|
1602
|
+
this.onQueueOverflow(this.droppedSinceLastFlush);
|
|
1603
|
+
} catch {
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
if (this.queue.length >= this.batchSize) {
|
|
1607
|
+
void this.flush();
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Manually flush the queue. Pulls up to `batchSize` events into a batch and
|
|
1612
|
+
* POSTs them. Returns a resolved promise on success OR on handled failure.
|
|
1613
|
+
* Never throws.
|
|
1614
|
+
*/
|
|
1615
|
+
async flush() {
|
|
1616
|
+
if (this.flushing) return;
|
|
1617
|
+
if (this.queue.length === 0) return;
|
|
1618
|
+
this.flushing = true;
|
|
1619
|
+
try {
|
|
1620
|
+
const batch = this.queue.splice(0, this.batchSize);
|
|
1621
|
+
await this.send(batch);
|
|
1622
|
+
this.droppedSinceLastFlush = 0;
|
|
1623
|
+
} catch (err) {
|
|
1624
|
+
this.safeNotify(err);
|
|
1625
|
+
} finally {
|
|
1626
|
+
this.flushing = false;
|
|
1627
|
+
}
|
|
1628
|
+
if (!this.closed && this.queue.length > 0) {
|
|
1629
|
+
void this.flush();
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Shut down: stop the interval timer and attempt one final flush.
|
|
1634
|
+
* Safe to call multiple times.
|
|
1635
|
+
*/
|
|
1636
|
+
async close() {
|
|
1637
|
+
if (this.closed) return;
|
|
1638
|
+
this.closed = true;
|
|
1639
|
+
if (this.timer !== void 0) {
|
|
1640
|
+
clearInterval(this.timer);
|
|
1641
|
+
this.timer = void 0;
|
|
1642
|
+
}
|
|
1643
|
+
if (this.signalHandler !== void 0) {
|
|
1644
|
+
process.off("SIGTERM", this.signalHandler);
|
|
1645
|
+
process.off("SIGINT", this.signalHandler);
|
|
1646
|
+
this.signalHandler = void 0;
|
|
1647
|
+
}
|
|
1648
|
+
try {
|
|
1649
|
+
await this.flush();
|
|
1650
|
+
} catch {
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Register `SIGTERM` / `SIGINT` handlers that call `close()` to drain
|
|
1655
|
+
* the queue on graceful shutdown. Opt-in — libraries should not silently
|
|
1656
|
+
* attach global signal handlers. Safe to call multiple times.
|
|
1657
|
+
*/
|
|
1658
|
+
installShutdownHooks() {
|
|
1659
|
+
if (this.signalHandler !== void 0 || this.closed) return;
|
|
1660
|
+
const handler = () => {
|
|
1661
|
+
void this.close();
|
|
1662
|
+
};
|
|
1663
|
+
this.signalHandler = handler;
|
|
1664
|
+
process.once("SIGTERM", handler);
|
|
1665
|
+
process.once("SIGINT", handler);
|
|
1666
|
+
}
|
|
1667
|
+
/** Count of events currently waiting to be sent. Useful for tests. */
|
|
1668
|
+
get pendingCount() {
|
|
1669
|
+
return this.queue.length;
|
|
1670
|
+
}
|
|
1671
|
+
// ── internals ─────────────────────────────────────────────────────────
|
|
1672
|
+
async send(batch) {
|
|
1673
|
+
const headers = {
|
|
1674
|
+
"content-type": "application/json"
|
|
1675
|
+
};
|
|
1676
|
+
if (this.apiKey) headers["authorization"] = `Bearer ${this.apiKey}`;
|
|
1677
|
+
if (this.workspaceId) headers["x-workspace-id"] = this.workspaceId;
|
|
1678
|
+
const controller = new AbortController();
|
|
1679
|
+
const abortTimer = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
|
|
1680
|
+
try {
|
|
1681
|
+
const res = await fetch(this.endpoint, {
|
|
1682
|
+
method: "POST",
|
|
1683
|
+
headers,
|
|
1684
|
+
body: JSON.stringify({ events: batch }),
|
|
1685
|
+
signal: controller.signal
|
|
1686
|
+
});
|
|
1687
|
+
if (!res.ok) {
|
|
1688
|
+
const text = await safeReadBody(res);
|
|
1689
|
+
throw new TelemetryHttpError(res.status, text);
|
|
1690
|
+
}
|
|
1691
|
+
} finally {
|
|
1692
|
+
clearTimeout(abortTimer);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
startTimer() {
|
|
1696
|
+
this.timer = setInterval(() => {
|
|
1697
|
+
void this.flush();
|
|
1698
|
+
}, this.flushIntervalMs);
|
|
1699
|
+
this.timer.unref?.();
|
|
1700
|
+
}
|
|
1701
|
+
safeNotify(err) {
|
|
1702
|
+
try {
|
|
1703
|
+
this.onError(err instanceof Error ? err : new Error(String(err)));
|
|
1704
|
+
} catch {
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
var TelemetryHttpError = class extends Error {
|
|
1709
|
+
constructor(status, responseBody) {
|
|
1710
|
+
super(`Telemetry ingest returned HTTP ${status}`);
|
|
1711
|
+
this.status = status;
|
|
1712
|
+
this.responseBody = responseBody;
|
|
1713
|
+
this.name = "TelemetryHttpError";
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
function clamp(value, min, max) {
|
|
1717
|
+
if (!Number.isFinite(value)) return min;
|
|
1718
|
+
return Math.max(min, Math.min(max, Math.trunc(value)));
|
|
1719
|
+
}
|
|
1720
|
+
async function safeReadBody(res) {
|
|
1721
|
+
try {
|
|
1722
|
+
const text = await res.text();
|
|
1723
|
+
return text.slice(0, 500);
|
|
1724
|
+
} catch {
|
|
1725
|
+
return "";
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1064
1729
|
// src/middleware/main.ts
|
|
1730
|
+
function buildTelemetryFromEnv() {
|
|
1731
|
+
const env = typeof process !== "undefined" ? process.env : void 0;
|
|
1732
|
+
const endpoint = env?.ARCIS_ENDPOINT;
|
|
1733
|
+
if (!endpoint) return void 0;
|
|
1734
|
+
const opts = { endpoint };
|
|
1735
|
+
if (env?.ARCIS_WORKSPACE_ID) opts.workspaceId = env.ARCIS_WORKSPACE_ID;
|
|
1736
|
+
if (env?.ARCIS_KEY) opts.apiKey = env.ARCIS_KEY;
|
|
1737
|
+
const batch = env?.ARCIS_BATCH_SIZE ? parseInt(env.ARCIS_BATCH_SIZE, 10) : NaN;
|
|
1738
|
+
if (!Number.isNaN(batch)) opts.batchSize = batch;
|
|
1739
|
+
const flush = env?.ARCIS_FLUSH_INTERVAL_MS ? parseInt(env.ARCIS_FLUSH_INTERVAL_MS, 10) : NaN;
|
|
1740
|
+
if (!Number.isNaN(flush)) opts.flushIntervalMs = flush;
|
|
1741
|
+
return opts;
|
|
1742
|
+
}
|
|
1065
1743
|
function arcis(options = {}) {
|
|
1066
1744
|
const middlewares = [];
|
|
1067
1745
|
const cleanupFns = [];
|
|
1746
|
+
let telemetryClient;
|
|
1747
|
+
const telemetryOpts = options.telemetry?.endpoint ? options.telemetry : buildTelemetryFromEnv();
|
|
1748
|
+
if (telemetryOpts) {
|
|
1749
|
+
const client = new TelemetryClient(telemetryOpts);
|
|
1750
|
+
telemetryClient = client;
|
|
1751
|
+
middlewares.push(createTelemetryEmitter(client));
|
|
1752
|
+
cleanupFns.push(() => {
|
|
1753
|
+
void client.close();
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1068
1756
|
if (options.headers !== false) {
|
|
1069
1757
|
const headerOpts = typeof options.headers === "object" ? options.headers : {};
|
|
1070
1758
|
middlewares.push(createHeaders(headerOpts));
|
|
@@ -1076,8 +1764,12 @@ function arcis(options = {}) {
|
|
|
1076
1764
|
cleanupFns.push(() => rateLimiter.close());
|
|
1077
1765
|
}
|
|
1078
1766
|
if (options.sanitize !== false) {
|
|
1079
|
-
const sanitizeOpts = typeof options.sanitize === "object" ? options.sanitize : {};
|
|
1080
|
-
|
|
1767
|
+
const sanitizeOpts = typeof options.sanitize === "object" ? { ...options.sanitize } : {};
|
|
1768
|
+
if (options.block && sanitizeOpts.block === void 0) {
|
|
1769
|
+
sanitizeOpts.block = true;
|
|
1770
|
+
}
|
|
1771
|
+
const sanitizer = createSanitizer(sanitizeOpts);
|
|
1772
|
+
middlewares.push(telemetryClient ? tapSanitizerThreats(sanitizer) : sanitizer);
|
|
1081
1773
|
}
|
|
1082
1774
|
const result = middlewares;
|
|
1083
1775
|
result.close = () => {
|
|
@@ -1390,6 +2082,16 @@ function secureCookieDefaults(options = {}) {
|
|
|
1390
2082
|
sameSite: options.sameSite ?? "Lax",
|
|
1391
2083
|
path: options.path
|
|
1392
2084
|
};
|
|
2085
|
+
if (resolved.sameSite === "None" && resolved.secure === false) {
|
|
2086
|
+
throw new Error(
|
|
2087
|
+
"[arcis] secureCookieDefaults: sameSite=None requires secure=true (modern browsers reject the cookie otherwise)"
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
if (resolved.httpOnly === false && resolved.secure === false && isProduction) {
|
|
2091
|
+
console.warn(
|
|
2092
|
+
"[arcis] secureCookieDefaults: httpOnly and secure are both disabled in production \u2014 cookies will be readable by JS and sent over HTTP"
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
1393
2095
|
return (_req, res, next) => {
|
|
1394
2096
|
const originalSetHeader = res.setHeader.bind(res);
|
|
1395
2097
|
res.setHeader = function patchedSetHeader(name, value) {
|
|
@@ -1522,7 +2224,16 @@ function detectBehavioralSignals(req) {
|
|
|
1522
2224
|
}
|
|
1523
2225
|
function detectBot(req) {
|
|
1524
2226
|
const rawUa = req.headers["user-agent"] ?? "";
|
|
1525
|
-
|
|
2227
|
+
if (rawUa.length > 2048) {
|
|
2228
|
+
return {
|
|
2229
|
+
isBot: true,
|
|
2230
|
+
category: "UNKNOWN",
|
|
2231
|
+
name: null,
|
|
2232
|
+
confidence: 0.9,
|
|
2233
|
+
signals: detectBehavioralSignals(req)
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
const ua = rawUa;
|
|
1526
2237
|
const signals = detectBehavioralSignals(req);
|
|
1527
2238
|
if (!ua) {
|
|
1528
2239
|
return {
|
|
@@ -1583,6 +2294,13 @@ function botProtection(options = {}) {
|
|
|
1583
2294
|
return next();
|
|
1584
2295
|
}
|
|
1585
2296
|
if (denySet.has(result.category)) {
|
|
2297
|
+
req.__arcis = {
|
|
2298
|
+
vector: "bot",
|
|
2299
|
+
rule: `bot/${result.category.toLowerCase()}`,
|
|
2300
|
+
severity: "medium",
|
|
2301
|
+
reason: result.name ? `Bot detected: ${result.name}` : "Bot detected",
|
|
2302
|
+
decision: "deny"
|
|
2303
|
+
};
|
|
1586
2304
|
if (onDetected) {
|
|
1587
2305
|
return onDetected(req, res, result);
|
|
1588
2306
|
}
|
|
@@ -1590,6 +2308,13 @@ function botProtection(options = {}) {
|
|
|
1590
2308
|
return;
|
|
1591
2309
|
}
|
|
1592
2310
|
if (defaultAction === "deny") {
|
|
2311
|
+
req.__arcis = {
|
|
2312
|
+
vector: "bot",
|
|
2313
|
+
rule: "bot/uncategorized",
|
|
2314
|
+
severity: "medium",
|
|
2315
|
+
reason: "Uncategorized bot under defaultAction=deny",
|
|
2316
|
+
decision: "deny"
|
|
2317
|
+
};
|
|
1593
2318
|
if (onDetected) {
|
|
1594
2319
|
return onDetected(req, res, result);
|
|
1595
2320
|
}
|
|
@@ -1643,7 +2368,14 @@ function csrfProtection(options = {}) {
|
|
|
1643
2368
|
sameSite: options.cookie?.sameSite ?? "Lax",
|
|
1644
2369
|
domain: options.cookie?.domain
|
|
1645
2370
|
};
|
|
1646
|
-
const defaultOnError = (
|
|
2371
|
+
const defaultOnError = (req, res, _next) => {
|
|
2372
|
+
req.__arcis = {
|
|
2373
|
+
vector: "csrf",
|
|
2374
|
+
rule: "csrf/token-mismatch",
|
|
2375
|
+
severity: "high",
|
|
2376
|
+
reason: "CSRF token missing or invalid",
|
|
2377
|
+
decision: "deny"
|
|
2378
|
+
};
|
|
1647
2379
|
res.status(403).json({
|
|
1648
2380
|
error: "CSRF token validation failed",
|
|
1649
2381
|
message: "Invalid or missing CSRF token. Include the token from the cookie in the X-CSRF-Token header."
|
|
@@ -1686,6 +2418,10 @@ function csrfProtection(options = {}) {
|
|
|
1686
2418
|
if (!validateCsrfToken(cookieToken, requestToken)) {
|
|
1687
2419
|
return onError(req, res, next);
|
|
1688
2420
|
}
|
|
2421
|
+
if (options.rotateOnUse) {
|
|
2422
|
+
const freshToken = generateCsrfToken(tokenLength);
|
|
2423
|
+
setCsrfCookie(res, cookieName, freshToken, cookieOpts);
|
|
2424
|
+
}
|
|
1689
2425
|
next();
|
|
1690
2426
|
};
|
|
1691
2427
|
}
|
|
@@ -1720,6 +2456,89 @@ function escapeRegex(str) {
|
|
|
1720
2456
|
}
|
|
1721
2457
|
var createCsrf = csrfProtection;
|
|
1722
2458
|
|
|
1723
|
-
|
|
2459
|
+
// src/middleware/signup-protection.ts
|
|
2460
|
+
function checkSignup(req, options = {}) {
|
|
2461
|
+
const {
|
|
2462
|
+
emailField = "email",
|
|
2463
|
+
checkEmail = true,
|
|
2464
|
+
blockDisposable = true,
|
|
2465
|
+
checkBot = true,
|
|
2466
|
+
allowedBotCategories = [],
|
|
2467
|
+
allowedEmailDomains = [],
|
|
2468
|
+
blockedEmailDomains = []
|
|
2469
|
+
} = options;
|
|
2470
|
+
if (checkBot) {
|
|
2471
|
+
const bot = detectBot(req);
|
|
2472
|
+
if (bot.isBot && !allowedBotCategories.includes(bot.category)) {
|
|
2473
|
+
return {
|
|
2474
|
+
allowed: false,
|
|
2475
|
+
reason: "bot",
|
|
2476
|
+
details: { category: bot.category, name: bot.name, confidence: bot.confidence }
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
if (checkEmail) {
|
|
2481
|
+
const email = req.body?.[emailField];
|
|
2482
|
+
if (typeof email !== "string" || email.length === 0) {
|
|
2483
|
+
return { allowed: false, reason: "missing_email" };
|
|
2484
|
+
}
|
|
2485
|
+
const result = validateEmail(email, {
|
|
2486
|
+
checkDisposable: blockDisposable,
|
|
2487
|
+
allowedDomains: allowedEmailDomains,
|
|
2488
|
+
blockedDomains: blockedEmailDomains
|
|
2489
|
+
});
|
|
2490
|
+
if (!result.valid) {
|
|
2491
|
+
const reason = result.reason === "disposable" ? "disposable_email" : "invalid_email";
|
|
2492
|
+
return { allowed: false, reason, details: { emailReason: result.reason } };
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
return { allowed: true, reason: "ok" };
|
|
2496
|
+
}
|
|
2497
|
+
function signupProtection(options = {}) {
|
|
2498
|
+
const rateLimitCfg = options.rateLimit;
|
|
2499
|
+
const limiter = rateLimitCfg === false ? null : createRateLimiter({
|
|
2500
|
+
max: rateLimitCfg?.max ?? 5,
|
|
2501
|
+
windowMs: rateLimitCfg?.windowMs ?? 6e4,
|
|
2502
|
+
message: "Too many signup attempts"
|
|
2503
|
+
});
|
|
2504
|
+
const handler = (req, res, next) => {
|
|
2505
|
+
const result = checkSignup(req, options);
|
|
2506
|
+
if (!result.allowed) {
|
|
2507
|
+
options.onBlocked?.(req, result);
|
|
2508
|
+
const status = result.reason === "bot" ? 403 : 400;
|
|
2509
|
+
res.status(status).json({ error: "signup_blocked", reason: result.reason });
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
if (limiter) {
|
|
2513
|
+
let rateLimited = false;
|
|
2514
|
+
const rateLimitNext = (err) => {
|
|
2515
|
+
if (err) return next(err);
|
|
2516
|
+
if (!rateLimited) next();
|
|
2517
|
+
};
|
|
2518
|
+
const patchedRes = new Proxy(res, {
|
|
2519
|
+
get(target, prop) {
|
|
2520
|
+
if (prop === "status") {
|
|
2521
|
+
return (code) => {
|
|
2522
|
+
if (code === 429) {
|
|
2523
|
+
rateLimited = true;
|
|
2524
|
+
options.onBlocked?.(req, { allowed: false, reason: "rate_limited" });
|
|
2525
|
+
}
|
|
2526
|
+
return target.status.call(target, code);
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
return Reflect.get(target, prop);
|
|
2530
|
+
}
|
|
2531
|
+
});
|
|
2532
|
+
limiter(req, patchedRes, rateLimitNext);
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
next();
|
|
2536
|
+
};
|
|
2537
|
+
const middleware = handler;
|
|
2538
|
+
middleware.close = () => limiter?.close();
|
|
2539
|
+
return middleware;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
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 };
|
|
1724
2543
|
//# sourceMappingURL=index.mjs.map
|
|
1725
2544
|
//# sourceMappingURL=index.mjs.map
|