@arcis/node 1.4.2 → 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/dist/core/constants.d.ts +1 -1
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- 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 +405 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +402 -21
- 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 +609 -9
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +608 -10
- 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.js +26 -8
- package/dist/sanitizers/index.js.map +1 -1
- package/dist/sanitizers/index.mjs +26 -8
- package/dist/sanitizers/index.mjs.map +1 -1
- package/dist/sanitizers/pii.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 +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 +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 +7 -1
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,
|
|
@@ -361,13 +364,13 @@ function createRateLimiter(options = {}) {
|
|
|
361
364
|
statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
|
|
362
365
|
keyGenerator = (req) => {
|
|
363
366
|
const ip = req.ip ?? req.socket?.remoteAddress;
|
|
364
|
-
if (
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
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)}`;
|
|
371
374
|
},
|
|
372
375
|
skip,
|
|
373
376
|
store: externalStore
|
|
@@ -559,6 +562,80 @@ var SecurityThreatError = class extends ArcisError {
|
|
|
559
562
|
}
|
|
560
563
|
};
|
|
561
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
|
+
|
|
562
639
|
// src/sanitizers/utils.ts
|
|
563
640
|
function encodeHtmlEntities(str) {
|
|
564
641
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -993,6 +1070,238 @@ function validateField(field, value, rules) {
|
|
|
993
1070
|
// Audio/Video
|
|
994
1071
|
"audio/mpeg": [Buffer.from([255, 251]), Buffer.from([255, 243]), Buffer.from([73, 68, 51])]});
|
|
995
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
|
+
|
|
996
1305
|
// src/logging/redactor.ts
|
|
997
1306
|
var LOG_LEVELS = {
|
|
998
1307
|
debug: 0,
|
|
@@ -1065,10 +1374,192 @@ function redactString(str, maxLength, patterns) {
|
|
|
1065
1374
|
return safe;
|
|
1066
1375
|
}
|
|
1067
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
|
+
|
|
1068
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
|
+
}
|
|
1069
1550
|
function arcis(options = {}) {
|
|
1070
1551
|
const middlewares = [];
|
|
1071
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
|
+
}
|
|
1072
1563
|
if (options.headers !== false) {
|
|
1073
1564
|
const headerOpts = typeof options.headers === "object" ? options.headers : {};
|
|
1074
1565
|
middlewares.push(createHeaders(headerOpts));
|
|
@@ -1081,7 +1572,8 @@ function arcis(options = {}) {
|
|
|
1081
1572
|
}
|
|
1082
1573
|
if (options.sanitize !== false) {
|
|
1083
1574
|
const sanitizeOpts = typeof options.sanitize === "object" ? options.sanitize : {};
|
|
1084
|
-
|
|
1575
|
+
const sanitizer = createSanitizer(sanitizeOpts);
|
|
1576
|
+
middlewares.push(telemetryClient ? tapSanitizerThreats(sanitizer) : sanitizer);
|
|
1085
1577
|
}
|
|
1086
1578
|
const result = middlewares;
|
|
1087
1579
|
result.close = () => {
|
|
@@ -1394,6 +1886,16 @@ function secureCookieDefaults(options = {}) {
|
|
|
1394
1886
|
sameSite: options.sameSite ?? "Lax",
|
|
1395
1887
|
path: options.path
|
|
1396
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
|
+
}
|
|
1397
1899
|
return (_req, res, next) => {
|
|
1398
1900
|
const originalSetHeader = res.setHeader.bind(res);
|
|
1399
1901
|
res.setHeader = function patchedSetHeader(name, value) {
|
|
@@ -1526,7 +2028,16 @@ function detectBehavioralSignals(req) {
|
|
|
1526
2028
|
}
|
|
1527
2029
|
function detectBot(req) {
|
|
1528
2030
|
const rawUa = req.headers["user-agent"] ?? "";
|
|
1529
|
-
|
|
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;
|
|
1530
2041
|
const signals = detectBehavioralSignals(req);
|
|
1531
2042
|
if (!ua) {
|
|
1532
2043
|
return {
|
|
@@ -1690,6 +2201,10 @@ function csrfProtection(options = {}) {
|
|
|
1690
2201
|
if (!validateCsrfToken(cookieToken, requestToken)) {
|
|
1691
2202
|
return onError(req, res, next);
|
|
1692
2203
|
}
|
|
2204
|
+
if (options.rotateOnUse) {
|
|
2205
|
+
const freshToken = generateCsrfToken(tokenLength);
|
|
2206
|
+
setCsrfCookie(res, cookieName, freshToken, cookieOpts);
|
|
2207
|
+
}
|
|
1693
2208
|
next();
|
|
1694
2209
|
};
|
|
1695
2210
|
}
|
|
@@ -1724,9 +2239,93 @@ function escapeRegex(str) {
|
|
|
1724
2239
|
}
|
|
1725
2240
|
var createCsrf = csrfProtection;
|
|
1726
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
|
+
|
|
1727
2325
|
exports.arcis = arcis;
|
|
1728
2326
|
exports.arcisFunction = arcisWithMethods;
|
|
1729
2327
|
exports.botProtection = botProtection;
|
|
2328
|
+
exports.checkSignup = checkSignup;
|
|
1730
2329
|
exports.createCors = createCors;
|
|
1731
2330
|
exports.createCsrf = createCsrf;
|
|
1732
2331
|
exports.createErrorHandler = createErrorHandler;
|
|
@@ -1745,6 +2344,7 @@ exports.rateLimit = rateLimit;
|
|
|
1745
2344
|
exports.safeCors = safeCors;
|
|
1746
2345
|
exports.secureCookieDefaults = secureCookieDefaults;
|
|
1747
2346
|
exports.securityHeaders = securityHeaders;
|
|
2347
|
+
exports.signupProtection = signupProtection;
|
|
1748
2348
|
exports.validateCsrfToken = validateCsrfToken;
|
|
1749
2349
|
//# sourceMappingURL=index.js.map
|
|
1750
2350
|
//# sourceMappingURL=index.js.map
|