@arcis/node 1.0.0 → 1.1.0
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 +7 -18
- package/dist/{index-BpT7flAQ.d.ts → index-BvcFpoR3.d.ts} +184 -1
- package/dist/{index-JaFOUKyK.d.mts → index-CCcPuTBo.d.mts} +184 -1
- package/dist/index-CslcoZUN.d.mts +340 -0
- package/dist/index-iCOw8Fcg.d.ts +340 -0
- package/dist/index.d.mts +142 -106
- package/dist/index.d.ts +142 -106
- package/dist/index.js +896 -114
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +885 -115
- package/dist/index.mjs.map +1 -1
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.d.ts +1 -1
- package/dist/middleware/index.js +378 -0
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +375 -1
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/validation/index.d.mts +1 -1
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +400 -0
- package/dist/validation/index.js.map +1 -1
- package/dist/validation/index.mjs +394 -1
- package/dist/validation/index.mjs.map +1 -1
- package/package.json +6 -1
- package/dist/index-BgHPM7LC.d.ts +0 -129
- package/dist/index-nAgXexwD.d.mts +0 -129
package/dist/index.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
+
var dns = require('dns');
|
|
6
|
+
var crypto = require('crypto');
|
|
7
|
+
|
|
5
8
|
// src/core/constants.ts
|
|
6
9
|
var INPUT = {
|
|
7
10
|
/** Default maximum input size (1MB) */
|
|
@@ -1270,6 +1273,397 @@ function isDangerousExtension(filename) {
|
|
|
1270
1273
|
return ext !== "" && DANGEROUS_EXTENSIONS.has(ext);
|
|
1271
1274
|
}
|
|
1272
1275
|
|
|
1276
|
+
// src/validation/url.ts
|
|
1277
|
+
function validateUrl(url, options = {}) {
|
|
1278
|
+
const {
|
|
1279
|
+
allowedProtocols = ["http:", "https:"],
|
|
1280
|
+
blockedHosts = [],
|
|
1281
|
+
allowedHosts = [],
|
|
1282
|
+
allowLocalhost = false,
|
|
1283
|
+
allowPrivate = false
|
|
1284
|
+
} = options;
|
|
1285
|
+
if (typeof url !== "string" || url.trim() === "") {
|
|
1286
|
+
return { safe: false, reason: "invalid URL: empty or not a string" };
|
|
1287
|
+
}
|
|
1288
|
+
let parsed;
|
|
1289
|
+
try {
|
|
1290
|
+
parsed = new URL(url);
|
|
1291
|
+
} catch {
|
|
1292
|
+
return { safe: false, reason: "invalid URL: failed to parse" };
|
|
1293
|
+
}
|
|
1294
|
+
if (!allowedProtocols.includes(parsed.protocol)) {
|
|
1295
|
+
return { safe: false, reason: `disallowed protocol: ${parsed.protocol}` };
|
|
1296
|
+
}
|
|
1297
|
+
if (parsed.username || parsed.password) {
|
|
1298
|
+
return { safe: false, reason: "URL contains credentials" };
|
|
1299
|
+
}
|
|
1300
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1301
|
+
if (allowedHosts.some((h) => hostname === h.toLowerCase())) {
|
|
1302
|
+
return { safe: true };
|
|
1303
|
+
}
|
|
1304
|
+
if (blockedHosts.some((h) => hostname === h.toLowerCase())) {
|
|
1305
|
+
return { safe: false, reason: `blocked host: ${hostname}` };
|
|
1306
|
+
}
|
|
1307
|
+
if (!allowLocalhost) {
|
|
1308
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1" || hostname === "0.0.0.0" || hostname.endsWith(".localhost")) {
|
|
1309
|
+
return { safe: false, reason: "loopback address" };
|
|
1310
|
+
}
|
|
1311
|
+
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
|
|
1312
|
+
return { safe: false, reason: "loopback address" };
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
if (!allowPrivate) {
|
|
1316
|
+
const privateCheck = checkPrivateIp(hostname);
|
|
1317
|
+
if (privateCheck) {
|
|
1318
|
+
return { safe: false, reason: privateCheck };
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return { safe: true };
|
|
1322
|
+
}
|
|
1323
|
+
function isUrlSafe(url, options = {}) {
|
|
1324
|
+
return validateUrl(url, options).safe;
|
|
1325
|
+
}
|
|
1326
|
+
function checkPrivateIp(hostname) {
|
|
1327
|
+
if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
|
|
1328
|
+
return "private address (10.0.0.0/8)";
|
|
1329
|
+
}
|
|
1330
|
+
const match172 = hostname.match(/^172\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/);
|
|
1331
|
+
if (match172) {
|
|
1332
|
+
const second = parseInt(match172[1], 10);
|
|
1333
|
+
if (second >= 16 && second <= 31) {
|
|
1334
|
+
return "private address (172.16.0.0/12)";
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
|
|
1338
|
+
return "private address (192.168.0.0/16)";
|
|
1339
|
+
}
|
|
1340
|
+
if (/^169\.254\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
|
|
1341
|
+
return "link-local address (169.254.0.0/16)";
|
|
1342
|
+
}
|
|
1343
|
+
if (/^0\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
|
|
1344
|
+
return "current network address (0.0.0.0/8)";
|
|
1345
|
+
}
|
|
1346
|
+
if (hostname === "metadata.google.internal" || hostname === "metadata.internal") {
|
|
1347
|
+
return "cloud metadata endpoint";
|
|
1348
|
+
}
|
|
1349
|
+
const ipv6 = hostname.replace(/^\[|\]$/g, "");
|
|
1350
|
+
if (ipv6 === "::1" || ipv6 === "::" || ipv6.startsWith("fc") || ipv6.startsWith("fd") || ipv6.startsWith("fe80")) {
|
|
1351
|
+
return "private IPv6 address";
|
|
1352
|
+
}
|
|
1353
|
+
return null;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// src/validation/redirect.ts
|
|
1357
|
+
var DANGEROUS_PROTOCOLS = /^(javascript|data|vbscript|blob):/i;
|
|
1358
|
+
var CONTROL_CHARS = /[\t\n\r]/g;
|
|
1359
|
+
function validateRedirect(url, options = {}) {
|
|
1360
|
+
const {
|
|
1361
|
+
allowedHosts = [],
|
|
1362
|
+
allowProtocolRelative = false,
|
|
1363
|
+
allowedProtocols = ["http:", "https:"]
|
|
1364
|
+
} = options;
|
|
1365
|
+
if (typeof url !== "string" || url.trim() === "") {
|
|
1366
|
+
return { safe: false, reason: "invalid redirect: empty or not a string" };
|
|
1367
|
+
}
|
|
1368
|
+
const cleaned = url.replace(CONTROL_CHARS, "");
|
|
1369
|
+
if (DANGEROUS_PROTOCOLS.test(cleaned)) {
|
|
1370
|
+
const proto = cleaned.match(DANGEROUS_PROTOCOLS);
|
|
1371
|
+
return { safe: false, reason: `dangerous protocol: ${proto[0]}` };
|
|
1372
|
+
}
|
|
1373
|
+
if (cleaned.startsWith("\\")) {
|
|
1374
|
+
return { safe: false, reason: "backslash-prefixed URL (browser treats as protocol-relative)" };
|
|
1375
|
+
}
|
|
1376
|
+
if (cleaned.startsWith("//")) {
|
|
1377
|
+
if (!allowProtocolRelative) {
|
|
1378
|
+
const host2 = extractHost(cleaned);
|
|
1379
|
+
if (host2 && allowedHosts.some((h) => host2 === h.toLowerCase())) {
|
|
1380
|
+
return { safe: true };
|
|
1381
|
+
}
|
|
1382
|
+
return { safe: false, reason: "protocol-relative URL not in allowed hosts" };
|
|
1383
|
+
}
|
|
1384
|
+
const host = extractHost(cleaned);
|
|
1385
|
+
if (host && allowedHosts.length > 0 && !allowedHosts.some((h) => host === h.toLowerCase())) {
|
|
1386
|
+
return { safe: false, reason: "protocol-relative URL not in allowed hosts" };
|
|
1387
|
+
}
|
|
1388
|
+
return { safe: true };
|
|
1389
|
+
}
|
|
1390
|
+
let parsed;
|
|
1391
|
+
try {
|
|
1392
|
+
parsed = new URL(cleaned);
|
|
1393
|
+
} catch {
|
|
1394
|
+
return { safe: true };
|
|
1395
|
+
}
|
|
1396
|
+
if (!allowedProtocols.includes(parsed.protocol)) {
|
|
1397
|
+
return { safe: false, reason: `disallowed protocol: ${parsed.protocol}` };
|
|
1398
|
+
}
|
|
1399
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1400
|
+
if (allowedHosts.length === 0) {
|
|
1401
|
+
return { safe: false, reason: "absolute URL not in allowed hosts" };
|
|
1402
|
+
}
|
|
1403
|
+
if (!allowedHosts.some((h) => hostname === h.toLowerCase())) {
|
|
1404
|
+
return { safe: false, reason: `host not allowed: ${hostname}` };
|
|
1405
|
+
}
|
|
1406
|
+
return { safe: true };
|
|
1407
|
+
}
|
|
1408
|
+
function isRedirectSafe(url, options = {}) {
|
|
1409
|
+
return validateRedirect(url, options).safe;
|
|
1410
|
+
}
|
|
1411
|
+
function extractHost(url) {
|
|
1412
|
+
const match = url.match(/^\/\/([^/:?#]+)/);
|
|
1413
|
+
return match ? match[1].toLowerCase() : null;
|
|
1414
|
+
}
|
|
1415
|
+
var MAX_EMAIL_LENGTH = 254;
|
|
1416
|
+
var MAX_LOCAL_LENGTH = 64;
|
|
1417
|
+
var MAX_DOMAIN_LENGTH = 255;
|
|
1418
|
+
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,}$/;
|
|
1419
|
+
var FREE_PROVIDERS = /* @__PURE__ */ new Set([
|
|
1420
|
+
"gmail.com",
|
|
1421
|
+
"yahoo.com",
|
|
1422
|
+
"hotmail.com",
|
|
1423
|
+
"outlook.com",
|
|
1424
|
+
"aol.com",
|
|
1425
|
+
"protonmail.com",
|
|
1426
|
+
"proton.me",
|
|
1427
|
+
"icloud.com",
|
|
1428
|
+
"mail.com",
|
|
1429
|
+
"zoho.com",
|
|
1430
|
+
"yandex.com",
|
|
1431
|
+
"gmx.com",
|
|
1432
|
+
"gmx.net",
|
|
1433
|
+
"live.com",
|
|
1434
|
+
"msn.com",
|
|
1435
|
+
"me.com",
|
|
1436
|
+
"mac.com",
|
|
1437
|
+
"fastmail.com",
|
|
1438
|
+
"tutanota.com",
|
|
1439
|
+
"hey.com"
|
|
1440
|
+
]);
|
|
1441
|
+
var DISPOSABLE_DOMAINS = /* @__PURE__ */ new Set([
|
|
1442
|
+
// Popular disposable services
|
|
1443
|
+
"guerrillamail.com",
|
|
1444
|
+
"guerrillamail.net",
|
|
1445
|
+
"guerrillamail.org",
|
|
1446
|
+
"tempmail.com",
|
|
1447
|
+
"temp-mail.org",
|
|
1448
|
+
"temp-mail.io",
|
|
1449
|
+
"throwaway.email",
|
|
1450
|
+
"throwaway.com",
|
|
1451
|
+
"mailinator.com",
|
|
1452
|
+
"mailinator.net",
|
|
1453
|
+
"yopmail.com",
|
|
1454
|
+
"yopmail.fr",
|
|
1455
|
+
"yopmail.net",
|
|
1456
|
+
"sharklasers.com",
|
|
1457
|
+
"grr.la",
|
|
1458
|
+
"guerrillamail.info",
|
|
1459
|
+
"guerrillamail.biz",
|
|
1460
|
+
"guerrillamail.de",
|
|
1461
|
+
"trashmail.com",
|
|
1462
|
+
"trashmail.me",
|
|
1463
|
+
"trashmail.net",
|
|
1464
|
+
"dispostable.com",
|
|
1465
|
+
"maildrop.cc",
|
|
1466
|
+
"mailnesia.com",
|
|
1467
|
+
"tempail.com",
|
|
1468
|
+
"mohmal.com",
|
|
1469
|
+
"getnada.com",
|
|
1470
|
+
"emailondeck.com",
|
|
1471
|
+
"discard.email",
|
|
1472
|
+
"fakeinbox.com",
|
|
1473
|
+
"mailcatch.com",
|
|
1474
|
+
"mintemail.com",
|
|
1475
|
+
"tempr.email",
|
|
1476
|
+
"tempinbox.com",
|
|
1477
|
+
"burnermail.io",
|
|
1478
|
+
"mailsac.com",
|
|
1479
|
+
"harakirimail.com",
|
|
1480
|
+
"tempmailo.com",
|
|
1481
|
+
"emailfake.com",
|
|
1482
|
+
"crazymailing.com",
|
|
1483
|
+
"armyspy.com",
|
|
1484
|
+
"dayrep.com",
|
|
1485
|
+
"einrot.com",
|
|
1486
|
+
"fleckens.hu",
|
|
1487
|
+
"gustr.com",
|
|
1488
|
+
"jourrapide.com",
|
|
1489
|
+
"rhyta.com",
|
|
1490
|
+
"superrito.com",
|
|
1491
|
+
"teleworm.us",
|
|
1492
|
+
"10minutemail.com",
|
|
1493
|
+
"10minutemail.net",
|
|
1494
|
+
"minutemail.com",
|
|
1495
|
+
"tempsky.com",
|
|
1496
|
+
"spamgourmet.com",
|
|
1497
|
+
"mytrashmail.com",
|
|
1498
|
+
"mailexpire.com",
|
|
1499
|
+
"safetymail.info",
|
|
1500
|
+
"filzmail.com",
|
|
1501
|
+
"trashymail.com",
|
|
1502
|
+
"sharkmail.com",
|
|
1503
|
+
"jetable.org",
|
|
1504
|
+
"nospam.ze.tc",
|
|
1505
|
+
"trash-me.com",
|
|
1506
|
+
"dodgit.com",
|
|
1507
|
+
"mailmoat.com",
|
|
1508
|
+
"spamfree24.org",
|
|
1509
|
+
"incognitomail.org",
|
|
1510
|
+
"tempomail.fr",
|
|
1511
|
+
"ephemail.net",
|
|
1512
|
+
"hidemail.de",
|
|
1513
|
+
"spaml.de",
|
|
1514
|
+
"uggsrock.com",
|
|
1515
|
+
"binkmail.com",
|
|
1516
|
+
"suremail.info",
|
|
1517
|
+
"bugmenot.com"
|
|
1518
|
+
]);
|
|
1519
|
+
var DOMAIN_TYPOS = {
|
|
1520
|
+
"gmial.com": "gmail.com",
|
|
1521
|
+
"gmaill.com": "gmail.com",
|
|
1522
|
+
"gmai.com": "gmail.com",
|
|
1523
|
+
"gamil.com": "gmail.com",
|
|
1524
|
+
"gnail.com": "gmail.com",
|
|
1525
|
+
"gmal.com": "gmail.com",
|
|
1526
|
+
"gmil.com": "gmail.com",
|
|
1527
|
+
"gmail.co": "gmail.com",
|
|
1528
|
+
"gmail.cm": "gmail.com",
|
|
1529
|
+
"gmail.om": "gmail.com",
|
|
1530
|
+
"gmail.con": "gmail.com",
|
|
1531
|
+
"gmail.cim": "gmail.com",
|
|
1532
|
+
"gmail.comm": "gmail.com",
|
|
1533
|
+
"yahooo.com": "yahoo.com",
|
|
1534
|
+
"yaho.com": "yahoo.com",
|
|
1535
|
+
"yahoo.co": "yahoo.com",
|
|
1536
|
+
"yahoo.cm": "yahoo.com",
|
|
1537
|
+
"yahoo.con": "yahoo.com",
|
|
1538
|
+
"yahho.com": "yahoo.com",
|
|
1539
|
+
"hotmial.com": "hotmail.com",
|
|
1540
|
+
"hotmal.com": "hotmail.com",
|
|
1541
|
+
"hotmai.com": "hotmail.com",
|
|
1542
|
+
"hotmil.com": "hotmail.com",
|
|
1543
|
+
"hotmail.co": "hotmail.com",
|
|
1544
|
+
"hotmail.cm": "hotmail.com",
|
|
1545
|
+
"hotmail.con": "hotmail.com",
|
|
1546
|
+
"outlok.com": "outlook.com",
|
|
1547
|
+
"outloo.com": "outlook.com",
|
|
1548
|
+
"outlook.co": "outlook.com",
|
|
1549
|
+
"outlook.cm": "outlook.com",
|
|
1550
|
+
"protonmal.com": "protonmail.com",
|
|
1551
|
+
"protonmail.co": "protonmail.com",
|
|
1552
|
+
"icloud.co": "icloud.com",
|
|
1553
|
+
"icloud.cm": "icloud.com",
|
|
1554
|
+
"icoud.com": "icloud.com"
|
|
1555
|
+
};
|
|
1556
|
+
function invalidResult(reason, email) {
|
|
1557
|
+
return {
|
|
1558
|
+
valid: false,
|
|
1559
|
+
reason,
|
|
1560
|
+
suggestion: null,
|
|
1561
|
+
isFree: false,
|
|
1562
|
+
isDisposable: false,
|
|
1563
|
+
normalized: email
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function validateEmail(email, options = {}) {
|
|
1567
|
+
const {
|
|
1568
|
+
checkDisposable = true,
|
|
1569
|
+
suggestTypoFix = true,
|
|
1570
|
+
blockedDomains = [],
|
|
1571
|
+
allowedDomains = []
|
|
1572
|
+
} = options;
|
|
1573
|
+
const normalized = email.trim().toLowerCase();
|
|
1574
|
+
if (!normalized || normalized.length > MAX_EMAIL_LENGTH) {
|
|
1575
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1576
|
+
}
|
|
1577
|
+
const atIndex = normalized.lastIndexOf("@");
|
|
1578
|
+
if (atIndex === -1) {
|
|
1579
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1580
|
+
}
|
|
1581
|
+
const localPart = normalized.slice(0, atIndex);
|
|
1582
|
+
const domain = normalized.slice(atIndex + 1);
|
|
1583
|
+
if (localPart.length === 0 || localPart.length > MAX_LOCAL_LENGTH) {
|
|
1584
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1585
|
+
}
|
|
1586
|
+
if (domain.length === 0 || domain.length > MAX_DOMAIN_LENGTH) {
|
|
1587
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1588
|
+
}
|
|
1589
|
+
if (localPart.includes("..")) {
|
|
1590
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1591
|
+
}
|
|
1592
|
+
if (localPart.startsWith(".") || localPart.endsWith(".")) {
|
|
1593
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1594
|
+
}
|
|
1595
|
+
if (!EMAIL_SYNTAX.test(normalized)) {
|
|
1596
|
+
return invalidResult("invalid_syntax", normalized);
|
|
1597
|
+
}
|
|
1598
|
+
const allowedSet = new Set(allowedDomains.map((d) => d.toLowerCase()));
|
|
1599
|
+
if (allowedSet.has(domain)) {
|
|
1600
|
+
return {
|
|
1601
|
+
valid: true,
|
|
1602
|
+
reason: "valid",
|
|
1603
|
+
suggestion: null,
|
|
1604
|
+
isFree: FREE_PROVIDERS.has(domain),
|
|
1605
|
+
isDisposable: false,
|
|
1606
|
+
normalized
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
const blockedSet = new Set(blockedDomains.map((d) => d.toLowerCase()));
|
|
1610
|
+
if (blockedSet.has(domain)) {
|
|
1611
|
+
return invalidResult("blocked", normalized);
|
|
1612
|
+
}
|
|
1613
|
+
const isDisposable = DISPOSABLE_DOMAINS.has(domain);
|
|
1614
|
+
if (checkDisposable && isDisposable) {
|
|
1615
|
+
return {
|
|
1616
|
+
valid: false,
|
|
1617
|
+
reason: "disposable",
|
|
1618
|
+
suggestion: null,
|
|
1619
|
+
isFree: false,
|
|
1620
|
+
isDisposable: true,
|
|
1621
|
+
normalized
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
const isFree = FREE_PROVIDERS.has(domain);
|
|
1625
|
+
if (suggestTypoFix && DOMAIN_TYPOS[domain]) {
|
|
1626
|
+
const corrected = `${localPart}@${DOMAIN_TYPOS[domain]}`;
|
|
1627
|
+
return {
|
|
1628
|
+
valid: true,
|
|
1629
|
+
reason: "typo",
|
|
1630
|
+
suggestion: corrected,
|
|
1631
|
+
isFree: FREE_PROVIDERS.has(DOMAIN_TYPOS[domain]),
|
|
1632
|
+
isDisposable: false,
|
|
1633
|
+
normalized
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
return {
|
|
1637
|
+
valid: true,
|
|
1638
|
+
reason: "valid",
|
|
1639
|
+
suggestion: null,
|
|
1640
|
+
isFree,
|
|
1641
|
+
isDisposable,
|
|
1642
|
+
normalized
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
async function verifyEmailMx(email) {
|
|
1646
|
+
if (!isValidEmailSyntax(email)) return false;
|
|
1647
|
+
const atIndex = email.lastIndexOf("@");
|
|
1648
|
+
const domain = email.slice(atIndex + 1).trim().toLowerCase();
|
|
1649
|
+
if (!domain) return false;
|
|
1650
|
+
try {
|
|
1651
|
+
const records = await dns.promises.resolveMx(domain);
|
|
1652
|
+
return records.length > 0;
|
|
1653
|
+
} catch {
|
|
1654
|
+
return false;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
function isValidEmailSyntax(email) {
|
|
1658
|
+
const normalized = email.trim().toLowerCase();
|
|
1659
|
+
if (!normalized || normalized.length > MAX_EMAIL_LENGTH) return false;
|
|
1660
|
+
const atIndex = normalized.lastIndexOf("@");
|
|
1661
|
+
if (atIndex === -1) return false;
|
|
1662
|
+
const localPart = normalized.slice(0, atIndex);
|
|
1663
|
+
if (localPart.includes("..") || localPart.startsWith(".") || localPart.endsWith(".")) return false;
|
|
1664
|
+
return EMAIL_SYNTAX.test(normalized);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1273
1667
|
// src/logging/redactor.ts
|
|
1274
1668
|
function createSafeLogger(options = {}) {
|
|
1275
1669
|
const {
|
|
@@ -1391,6 +1785,201 @@ arcisWithMethods.logger = createSafeLogger;
|
|
|
1391
1785
|
arcisWithMethods.errorHandler = createErrorHandler;
|
|
1392
1786
|
var main_default = arcisWithMethods;
|
|
1393
1787
|
|
|
1788
|
+
// src/utils/duration.ts
|
|
1789
|
+
var MAX_DURATION_MS = 4294967295;
|
|
1790
|
+
var DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/i;
|
|
1791
|
+
var UNIT_TO_MS = {
|
|
1792
|
+
ms: 1,
|
|
1793
|
+
s: 1e3,
|
|
1794
|
+
m: 6e4,
|
|
1795
|
+
h: 36e5,
|
|
1796
|
+
d: 864e5
|
|
1797
|
+
};
|
|
1798
|
+
function parseDuration(value) {
|
|
1799
|
+
if (typeof value === "number") {
|
|
1800
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1801
|
+
throw new Error(`Invalid duration: ${value}. Must be a non-negative finite number.`);
|
|
1802
|
+
}
|
|
1803
|
+
return Math.min(Math.floor(value), MAX_DURATION_MS);
|
|
1804
|
+
}
|
|
1805
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
1806
|
+
throw new Error(`Invalid duration: "${value}". Expected a duration string (e.g. "5m", "2h") or number.`);
|
|
1807
|
+
}
|
|
1808
|
+
const match = value.trim().match(DURATION_REGEX);
|
|
1809
|
+
if (!match) {
|
|
1810
|
+
throw new Error(
|
|
1811
|
+
`Invalid duration: "${value}". Expected format: <number><unit> where unit is ms, s, m, h, or d.`
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
const amount = parseFloat(match[1]);
|
|
1815
|
+
const unit = match[2].toLowerCase();
|
|
1816
|
+
const ms = Math.floor(amount * UNIT_TO_MS[unit]);
|
|
1817
|
+
if (ms < 0 || ms > MAX_DURATION_MS) {
|
|
1818
|
+
throw new Error(`Duration "${value}" exceeds maximum allowed (${MAX_DURATION_MS}ms / ~49.7 days).`);
|
|
1819
|
+
}
|
|
1820
|
+
return ms;
|
|
1821
|
+
}
|
|
1822
|
+
function formatDuration(ms) {
|
|
1823
|
+
if (!Number.isFinite(ms) || ms < 0) return "0ms";
|
|
1824
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
1825
|
+
const days = Math.floor(ms / 864e5);
|
|
1826
|
+
const hours = Math.floor(ms % 864e5 / 36e5);
|
|
1827
|
+
const minutes = Math.floor(ms % 36e5 / 6e4);
|
|
1828
|
+
const seconds = Math.floor(ms % 6e4 / 1e3);
|
|
1829
|
+
const parts = [];
|
|
1830
|
+
if (days > 0) parts.push(`${days}d`);
|
|
1831
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
1832
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
1833
|
+
if (seconds > 0) parts.push(`${seconds}s`);
|
|
1834
|
+
return parts.join(" ") || "0ms";
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// src/middleware/rate-limit-sliding.ts
|
|
1838
|
+
function createSlidingWindowLimiter(options = {}) {
|
|
1839
|
+
const {
|
|
1840
|
+
max = RATE_LIMIT.DEFAULT_MAX_REQUESTS,
|
|
1841
|
+
window: windowOpt = RATE_LIMIT.DEFAULT_WINDOW_MS,
|
|
1842
|
+
message = RATE_LIMIT.DEFAULT_MESSAGE,
|
|
1843
|
+
statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
|
|
1844
|
+
keyGenerator = (req) => req.ip ?? req.socket?.remoteAddress ?? "unknown",
|
|
1845
|
+
skip
|
|
1846
|
+
} = options;
|
|
1847
|
+
const windowMs = parseDuration(windowOpt);
|
|
1848
|
+
const currentWindows = /* @__PURE__ */ Object.create(null);
|
|
1849
|
+
const previousWindows = /* @__PURE__ */ Object.create(null);
|
|
1850
|
+
const cleanupInterval = setInterval(() => {
|
|
1851
|
+
const now = Date.now();
|
|
1852
|
+
const cutoff = now - windowMs * 2;
|
|
1853
|
+
for (const key of Object.keys(previousWindows)) {
|
|
1854
|
+
if (previousWindows[key].startTime < cutoff) {
|
|
1855
|
+
delete previousWindows[key];
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
for (const key of Object.keys(currentWindows)) {
|
|
1859
|
+
if (currentWindows[key].startTime < cutoff) {
|
|
1860
|
+
delete currentWindows[key];
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}, windowMs);
|
|
1864
|
+
if (typeof cleanupInterval.unref === "function") {
|
|
1865
|
+
cleanupInterval.unref();
|
|
1866
|
+
}
|
|
1867
|
+
const handler = (req, res, next) => {
|
|
1868
|
+
try {
|
|
1869
|
+
if (skip?.(req)) return next();
|
|
1870
|
+
const key = keyGenerator(req);
|
|
1871
|
+
const now = Date.now();
|
|
1872
|
+
const windowStart = Math.floor(now / windowMs) * windowMs;
|
|
1873
|
+
if (!currentWindows[key] || currentWindows[key].startTime < windowStart) {
|
|
1874
|
+
if (currentWindows[key]) {
|
|
1875
|
+
previousWindows[key] = currentWindows[key];
|
|
1876
|
+
}
|
|
1877
|
+
currentWindows[key] = { count: 0, startTime: windowStart };
|
|
1878
|
+
}
|
|
1879
|
+
const elapsed = now - windowStart;
|
|
1880
|
+
const weight = Math.max(0, (windowMs - elapsed) / windowMs);
|
|
1881
|
+
const prevCount = previousWindows[key]?.count ?? 0;
|
|
1882
|
+
const estimatedCount = prevCount * weight + currentWindows[key].count + 1;
|
|
1883
|
+
const remaining = Math.max(0, Math.floor(max - estimatedCount));
|
|
1884
|
+
const resetMs = windowStart + windowMs - now;
|
|
1885
|
+
const resetSeconds = Math.max(1, Math.ceil(resetMs / 1e3));
|
|
1886
|
+
res.setHeader("X-RateLimit-Limit", max.toString());
|
|
1887
|
+
res.setHeader("X-RateLimit-Remaining", remaining.toString());
|
|
1888
|
+
res.setHeader("X-RateLimit-Reset", resetSeconds.toString());
|
|
1889
|
+
res.setHeader("X-RateLimit-Policy", `${max};w=${Math.floor(windowMs / 1e3)}`);
|
|
1890
|
+
if (estimatedCount > max) {
|
|
1891
|
+
res.setHeader("Retry-After", resetSeconds.toString());
|
|
1892
|
+
res.status(statusCode).json({
|
|
1893
|
+
error: message,
|
|
1894
|
+
retryAfter: resetSeconds
|
|
1895
|
+
});
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
currentWindows[key].count++;
|
|
1899
|
+
next();
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
console.error("[arcis] Sliding window rate limiter error:", error);
|
|
1902
|
+
next();
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
const middleware = handler;
|
|
1906
|
+
middleware.close = () => {
|
|
1907
|
+
clearInterval(cleanupInterval);
|
|
1908
|
+
};
|
|
1909
|
+
return middleware;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// src/middleware/rate-limit-token.ts
|
|
1913
|
+
function createTokenBucketLimiter(options = {}) {
|
|
1914
|
+
const {
|
|
1915
|
+
capacity = 100,
|
|
1916
|
+
refillRate = 10,
|
|
1917
|
+
cost = 1,
|
|
1918
|
+
message = RATE_LIMIT.DEFAULT_MESSAGE,
|
|
1919
|
+
statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
|
|
1920
|
+
keyGenerator = (req) => req.ip ?? req.socket?.remoteAddress ?? "unknown",
|
|
1921
|
+
skip
|
|
1922
|
+
} = options;
|
|
1923
|
+
if (capacity < 1) throw new RangeError(`Token bucket capacity must be >= 1, got ${capacity}`);
|
|
1924
|
+
if (refillRate <= 0) throw new RangeError(`Token bucket refillRate must be > 0, got ${refillRate}`);
|
|
1925
|
+
if (cost < 1) throw new RangeError(`Token bucket cost must be >= 1, got ${cost}`);
|
|
1926
|
+
if (cost > capacity) throw new RangeError(`Token bucket cost (${cost}) must be <= capacity (${capacity}), otherwise all requests are permanently denied`);
|
|
1927
|
+
const buckets = /* @__PURE__ */ Object.create(null);
|
|
1928
|
+
const cleanupInterval = setInterval(() => {
|
|
1929
|
+
const now = Date.now();
|
|
1930
|
+
const staleThreshold = capacity / refillRate * 1e3 * 2;
|
|
1931
|
+
for (const key of Object.keys(buckets)) {
|
|
1932
|
+
if (now - buckets[key].lastRefill > staleThreshold) {
|
|
1933
|
+
delete buckets[key];
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
}, 6e4);
|
|
1937
|
+
if (typeof cleanupInterval.unref === "function") {
|
|
1938
|
+
cleanupInterval.unref();
|
|
1939
|
+
}
|
|
1940
|
+
function refillBucket(bucket, now) {
|
|
1941
|
+
const elapsed = (now - bucket.lastRefill) / 1e3;
|
|
1942
|
+
const tokensToAdd = elapsed * refillRate;
|
|
1943
|
+
bucket.tokens = Math.min(capacity, bucket.tokens + tokensToAdd);
|
|
1944
|
+
bucket.lastRefill = now;
|
|
1945
|
+
}
|
|
1946
|
+
const handler = (req, res, next) => {
|
|
1947
|
+
try {
|
|
1948
|
+
if (skip?.(req)) return next();
|
|
1949
|
+
const key = keyGenerator(req);
|
|
1950
|
+
const now = Date.now();
|
|
1951
|
+
if (!buckets[key]) {
|
|
1952
|
+
buckets[key] = { tokens: capacity, lastRefill: now };
|
|
1953
|
+
}
|
|
1954
|
+
const bucket = buckets[key];
|
|
1955
|
+
refillBucket(bucket, now);
|
|
1956
|
+
const retryAfterSec = bucket.tokens < cost ? Math.ceil((cost - bucket.tokens) / refillRate) : 0;
|
|
1957
|
+
res.setHeader("X-RateLimit-Limit", capacity.toString());
|
|
1958
|
+
res.setHeader("X-RateLimit-Remaining", Math.floor(Math.max(0, bucket.tokens - cost)).toString());
|
|
1959
|
+
res.setHeader("X-RateLimit-Policy", `${capacity};w=${Math.floor(capacity / refillRate)};burst=${capacity}`);
|
|
1960
|
+
if (bucket.tokens < cost) {
|
|
1961
|
+
res.setHeader("Retry-After", retryAfterSec.toString());
|
|
1962
|
+
res.setHeader("X-RateLimit-Reset", retryAfterSec.toString());
|
|
1963
|
+
res.status(statusCode).json({
|
|
1964
|
+
error: message,
|
|
1965
|
+
retryAfter: retryAfterSec
|
|
1966
|
+
});
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
bucket.tokens -= cost;
|
|
1970
|
+
next();
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
console.error("[arcis] Token bucket rate limiter error:", error);
|
|
1973
|
+
next();
|
|
1974
|
+
}
|
|
1975
|
+
};
|
|
1976
|
+
const middleware = handler;
|
|
1977
|
+
middleware.close = () => {
|
|
1978
|
+
clearInterval(cleanupInterval);
|
|
1979
|
+
};
|
|
1980
|
+
return middleware;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1394
1983
|
// src/middleware/cors.ts
|
|
1395
1984
|
var DEFAULT_METHODS = ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"];
|
|
1396
1985
|
var DEFAULT_HEADERS = ["Content-Type", "Authorization"];
|
|
@@ -1521,144 +2110,325 @@ function secureCookieDefaults(options = {}) {
|
|
|
1521
2110
|
}
|
|
1522
2111
|
var createSecureCookies = secureCookieDefaults;
|
|
1523
2112
|
|
|
1524
|
-
// src/
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
2113
|
+
// src/middleware/bot-detection.ts
|
|
2114
|
+
var BOT_PATTERNS = [
|
|
2115
|
+
// --- SEARCH ENGINES (specific variants before generic) ---
|
|
2116
|
+
{ pattern: /Googlebot-Image/i, name: "Googlebot-Image", category: "SEARCH_ENGINE" },
|
|
2117
|
+
{ pattern: /Googlebot-Video/i, name: "Googlebot-Video", category: "SEARCH_ENGINE" },
|
|
2118
|
+
{ pattern: /Googlebot-News/i, name: "Googlebot-News", category: "SEARCH_ENGINE" },
|
|
2119
|
+
{ pattern: /Googlebot/i, name: "Googlebot", category: "SEARCH_ENGINE" },
|
|
2120
|
+
{ pattern: /AdsBot-Google/i, name: "AdsBot-Google", category: "SEARCH_ENGINE" },
|
|
2121
|
+
{ pattern: /Mediapartners-Google/i, name: "Mediapartners-Google", category: "SEARCH_ENGINE" },
|
|
2122
|
+
{ pattern: /Bingbot/i, name: "Bingbot", category: "SEARCH_ENGINE" },
|
|
2123
|
+
{ pattern: /msnbot/i, name: "msnbot", category: "SEARCH_ENGINE" },
|
|
2124
|
+
{ pattern: /Slurp/i, name: "Yahoo Slurp", category: "SEARCH_ENGINE" },
|
|
2125
|
+
{ pattern: /DuckDuckBot/i, name: "DuckDuckBot", category: "SEARCH_ENGINE" },
|
|
2126
|
+
{ pattern: /Baiduspider/i, name: "Baiduspider", category: "SEARCH_ENGINE" },
|
|
2127
|
+
{ pattern: /YandexBot/i, name: "YandexBot", category: "SEARCH_ENGINE" },
|
|
2128
|
+
{ pattern: /YandexImages/i, name: "YandexImages", category: "SEARCH_ENGINE" },
|
|
2129
|
+
{ pattern: /Sogou/i, name: "Sogou", category: "SEARCH_ENGINE" },
|
|
2130
|
+
{ pattern: /Exabot/i, name: "Exabot", category: "SEARCH_ENGINE" },
|
|
2131
|
+
{ pattern: /ia_archiver/i, name: "Alexa", category: "SEARCH_ENGINE" },
|
|
2132
|
+
{ pattern: /Applebot/i, name: "Applebot", category: "SEARCH_ENGINE" },
|
|
2133
|
+
{ pattern: /Qwantify/i, name: "Qwantify", category: "SEARCH_ENGINE" },
|
|
2134
|
+
{ pattern: /PetalBot/i, name: "PetalBot", category: "SEARCH_ENGINE" },
|
|
2135
|
+
{ pattern: /SeznamBot/i, name: "SeznamBot", category: "SEARCH_ENGINE" },
|
|
2136
|
+
// --- SOCIAL ---
|
|
2137
|
+
{ pattern: /Twitterbot/i, name: "Twitterbot", category: "SOCIAL" },
|
|
2138
|
+
{ pattern: /facebookexternalhit/i, name: "Facebook", category: "SOCIAL" },
|
|
2139
|
+
{ pattern: /Facebot/i, name: "Facebot", category: "SOCIAL" },
|
|
2140
|
+
{ pattern: /LinkedInBot/i, name: "LinkedInBot", category: "SOCIAL" },
|
|
2141
|
+
{ pattern: /Pinterest/i, name: "Pinterest", category: "SOCIAL" },
|
|
2142
|
+
{ pattern: /Slackbot/i, name: "Slackbot", category: "SOCIAL" },
|
|
2143
|
+
{ pattern: /TelegramBot/i, name: "TelegramBot", category: "SOCIAL" },
|
|
2144
|
+
{ pattern: /WhatsApp/i, name: "WhatsApp", category: "SOCIAL" },
|
|
2145
|
+
{ pattern: /Discordbot/i, name: "Discordbot", category: "SOCIAL" },
|
|
2146
|
+
{ pattern: /Redditbot/i, name: "Redditbot", category: "SOCIAL" },
|
|
2147
|
+
{ pattern: /Embedly/i, name: "Embedly", category: "SOCIAL" },
|
|
2148
|
+
{ pattern: /Quora Link Preview/i, name: "Quora", category: "SOCIAL" },
|
|
2149
|
+
{ pattern: /Mastodon/i, name: "Mastodon", category: "SOCIAL" },
|
|
2150
|
+
// --- MONITORING ---
|
|
2151
|
+
{ pattern: /UptimeRobot/i, name: "UptimeRobot", category: "MONITORING" },
|
|
2152
|
+
{ pattern: /Pingdom/i, name: "Pingdom", category: "MONITORING" },
|
|
2153
|
+
{ pattern: /Site24x7/i, name: "Site24x7", category: "MONITORING" },
|
|
2154
|
+
{ pattern: /StatusCake/i, name: "StatusCake", category: "MONITORING" },
|
|
2155
|
+
{ pattern: /Datadog/i, name: "Datadog", category: "MONITORING" },
|
|
2156
|
+
{ pattern: /NewRelicPinger/i, name: "New Relic", category: "MONITORING" },
|
|
2157
|
+
{ pattern: /Better Uptime Bot/i, name: "Better Uptime", category: "MONITORING" },
|
|
2158
|
+
{ pattern: /GTmetrix/i, name: "GTmetrix", category: "MONITORING" },
|
|
2159
|
+
{ pattern: /PageSpeed/i, name: "PageSpeed Insights", category: "MONITORING" },
|
|
2160
|
+
// --- AI CRAWLERS ---
|
|
2161
|
+
{ pattern: /GPTBot/i, name: "GPTBot", category: "AI_CRAWLER" },
|
|
2162
|
+
{ pattern: /ChatGPT-User/i, name: "ChatGPT-User", category: "AI_CRAWLER" },
|
|
2163
|
+
{ pattern: /Claude-Web/i, name: "Claude-Web", category: "AI_CRAWLER" },
|
|
2164
|
+
{ pattern: /ClaudeBot/i, name: "ClaudeBot", category: "AI_CRAWLER" },
|
|
2165
|
+
{ pattern: /anthropic-ai/i, name: "Anthropic", category: "AI_CRAWLER" },
|
|
2166
|
+
{ pattern: /Bytespider/i, name: "Bytespider", category: "AI_CRAWLER" },
|
|
2167
|
+
{ pattern: /CCBot/i, name: "CCBot", category: "AI_CRAWLER" },
|
|
2168
|
+
{ pattern: /cohere-ai/i, name: "Cohere", category: "AI_CRAWLER" },
|
|
2169
|
+
{ pattern: /PerplexityBot/i, name: "PerplexityBot", category: "AI_CRAWLER" },
|
|
2170
|
+
{ pattern: /YouBot/i, name: "YouBot", category: "AI_CRAWLER" },
|
|
2171
|
+
{ pattern: /Google-Extended/i, name: "Google-Extended", category: "AI_CRAWLER" },
|
|
2172
|
+
{ pattern: /Diffbot/i, name: "Diffbot", category: "AI_CRAWLER" },
|
|
2173
|
+
{ pattern: /Amazonbot/i, name: "Amazonbot", category: "AI_CRAWLER" },
|
|
2174
|
+
{ pattern: /meta-externalagent/i, name: "Meta AI", category: "AI_CRAWLER" },
|
|
2175
|
+
// --- AUTOMATED TOOLS (headless browsers, testing frameworks) ---
|
|
2176
|
+
{ pattern: /HeadlessChrome/i, name: "Headless Chrome", category: "AUTOMATED" },
|
|
2177
|
+
{ pattern: /PhantomJS/i, name: "PhantomJS", category: "AUTOMATED" },
|
|
2178
|
+
{ pattern: /Selenium/i, name: "Selenium", category: "AUTOMATED" },
|
|
2179
|
+
{ pattern: /Puppeteer/i, name: "Puppeteer", category: "AUTOMATED" },
|
|
2180
|
+
{ pattern: /Playwright/i, name: "Playwright", category: "AUTOMATED" },
|
|
2181
|
+
{ pattern: /Cypress/i, name: "Cypress", category: "AUTOMATED" },
|
|
2182
|
+
{ pattern: /webdriver/i, name: "WebDriver", category: "AUTOMATED" },
|
|
2183
|
+
{ pattern: /MSIE 6\.0/i, name: "Fake IE6", category: "AUTOMATED" },
|
|
2184
|
+
// --- SCRAPERS / CLI TOOLS ---
|
|
2185
|
+
{ pattern: /^curl\//i, name: "curl", category: "SCRAPER" },
|
|
2186
|
+
{ pattern: /^wget\//i, name: "wget", category: "SCRAPER" },
|
|
2187
|
+
{ pattern: /^python-requests\//i, name: "python-requests", category: "SCRAPER" },
|
|
2188
|
+
{ pattern: /^python-httpx\//i, name: "python-httpx", category: "SCRAPER" },
|
|
2189
|
+
{ pattern: /^Python-urllib/i, name: "Python-urllib", category: "SCRAPER" },
|
|
2190
|
+
{ pattern: /^aiohttp\//i, name: "aiohttp", category: "SCRAPER" },
|
|
2191
|
+
{ pattern: /^Go-http-client/i, name: "Go-http-client", category: "SCRAPER" },
|
|
2192
|
+
{ pattern: /^Java\//i, name: "Java HttpClient", category: "SCRAPER" },
|
|
2193
|
+
{ pattern: /^Apache-HttpClient/i, name: "Apache HttpClient", category: "SCRAPER" },
|
|
2194
|
+
{ pattern: /^okhttp\//i, name: "OkHttp", category: "SCRAPER" },
|
|
2195
|
+
{ pattern: /^node-fetch\//i, name: "node-fetch", category: "SCRAPER" },
|
|
2196
|
+
{ pattern: /^axios\//i, name: "axios", category: "SCRAPER" },
|
|
2197
|
+
{ pattern: /^got\//i, name: "got", category: "SCRAPER" },
|
|
2198
|
+
{ pattern: /^libwww-perl/i, name: "libwww-perl", category: "SCRAPER" },
|
|
2199
|
+
{ pattern: /^Ruby/i, name: "Ruby", category: "SCRAPER" },
|
|
2200
|
+
{ pattern: /^PHP\//i, name: "PHP", category: "SCRAPER" },
|
|
2201
|
+
{ pattern: /Scrapy/i, name: "Scrapy", category: "SCRAPER" },
|
|
2202
|
+
{ pattern: /^Postman/i, name: "Postman", category: "SCRAPER" },
|
|
2203
|
+
{ pattern: /^Insomnia/i, name: "Insomnia", category: "SCRAPER" },
|
|
2204
|
+
{ pattern: /^HTTPie\//i, name: "HTTPie", category: "SCRAPER" }
|
|
2205
|
+
];
|
|
2206
|
+
function detectBehavioralSignals(req) {
|
|
2207
|
+
const signals = [];
|
|
2208
|
+
const headers = req.headers;
|
|
2209
|
+
if (!headers["user-agent"]) {
|
|
2210
|
+
signals.push("missing_user_agent");
|
|
1535
2211
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
parsed = new URL(url);
|
|
1539
|
-
} catch {
|
|
1540
|
-
return { safe: false, reason: "invalid URL: failed to parse" };
|
|
2212
|
+
if (!headers["accept"]) {
|
|
2213
|
+
signals.push("missing_accept");
|
|
1541
2214
|
}
|
|
1542
|
-
if (!
|
|
1543
|
-
|
|
2215
|
+
if (!headers["accept-language"]) {
|
|
2216
|
+
signals.push("missing_accept_language");
|
|
1544
2217
|
}
|
|
1545
|
-
if (
|
|
1546
|
-
|
|
2218
|
+
if (!headers["accept-encoding"]) {
|
|
2219
|
+
signals.push("missing_accept_encoding");
|
|
1547
2220
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
return { safe: true };
|
|
2221
|
+
if (headers["connection"] === "close") {
|
|
2222
|
+
signals.push("connection_close");
|
|
1551
2223
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
2224
|
+
return signals;
|
|
2225
|
+
}
|
|
2226
|
+
function detectBot(req) {
|
|
2227
|
+
const rawUa = req.headers["user-agent"] ?? "";
|
|
2228
|
+
const ua = rawUa.length > 2048 ? rawUa.slice(0, 2048) : rawUa;
|
|
2229
|
+
const signals = detectBehavioralSignals(req);
|
|
2230
|
+
if (!ua) {
|
|
2231
|
+
return {
|
|
2232
|
+
isBot: true,
|
|
2233
|
+
category: "UNKNOWN",
|
|
2234
|
+
name: null,
|
|
2235
|
+
confidence: 0.8,
|
|
2236
|
+
signals
|
|
2237
|
+
};
|
|
1554
2238
|
}
|
|
1555
|
-
|
|
1556
|
-
if (
|
|
1557
|
-
return {
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
2239
|
+
for (const bot of BOT_PATTERNS) {
|
|
2240
|
+
if (bot.pattern.test(ua)) {
|
|
2241
|
+
return {
|
|
2242
|
+
isBot: true,
|
|
2243
|
+
category: bot.category,
|
|
2244
|
+
name: bot.name,
|
|
2245
|
+
confidence: 0.95,
|
|
2246
|
+
signals
|
|
2247
|
+
};
|
|
1561
2248
|
}
|
|
1562
2249
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
2250
|
+
const behaviorScore = signals.length;
|
|
2251
|
+
if (behaviorScore >= 3) {
|
|
2252
|
+
return {
|
|
2253
|
+
isBot: true,
|
|
2254
|
+
category: "UNKNOWN",
|
|
2255
|
+
name: null,
|
|
2256
|
+
confidence: Math.min(1, 0.6 + behaviorScore * 0.1),
|
|
2257
|
+
signals
|
|
2258
|
+
};
|
|
1568
2259
|
}
|
|
1569
|
-
return {
|
|
2260
|
+
return {
|
|
2261
|
+
isBot: false,
|
|
2262
|
+
category: "HUMAN",
|
|
2263
|
+
name: null,
|
|
2264
|
+
confidence: Math.max(0, 1 - behaviorScore * 0.15),
|
|
2265
|
+
signals
|
|
2266
|
+
};
|
|
1570
2267
|
}
|
|
1571
|
-
function
|
|
1572
|
-
|
|
2268
|
+
function botProtection(options = {}) {
|
|
2269
|
+
const {
|
|
2270
|
+
allow = ["SEARCH_ENGINE", "SOCIAL", "MONITORING"],
|
|
2271
|
+
deny = ["AUTOMATED"],
|
|
2272
|
+
defaultAction = "allow",
|
|
2273
|
+
statusCode = 403,
|
|
2274
|
+
message = "Access denied.",
|
|
2275
|
+
onDetected
|
|
2276
|
+
} = options;
|
|
2277
|
+
const allowSet = new Set(allow);
|
|
2278
|
+
const denySet = new Set(deny);
|
|
2279
|
+
return (req, res, next) => {
|
|
2280
|
+
const result = detectBot(req);
|
|
2281
|
+
req.botDetection = result;
|
|
2282
|
+
if (!result.isBot) {
|
|
2283
|
+
return next();
|
|
2284
|
+
}
|
|
2285
|
+
if (allowSet.has(result.category)) {
|
|
2286
|
+
return next();
|
|
2287
|
+
}
|
|
2288
|
+
if (denySet.has(result.category)) {
|
|
2289
|
+
if (onDetected) {
|
|
2290
|
+
return onDetected(req, res, result);
|
|
2291
|
+
}
|
|
2292
|
+
res.status(statusCode).json({ error: message });
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
if (defaultAction === "deny") {
|
|
2296
|
+
if (onDetected) {
|
|
2297
|
+
return onDetected(req, res, result);
|
|
2298
|
+
}
|
|
2299
|
+
res.status(statusCode).json({ error: message });
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
next();
|
|
2303
|
+
};
|
|
1573
2304
|
}
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
2305
|
+
|
|
2306
|
+
// src/utils/ip.ts
|
|
2307
|
+
var PLATFORM_HEADERS = {
|
|
2308
|
+
cloudflare: "cf-connecting-ip",
|
|
2309
|
+
vercel: "x-real-ip",
|
|
2310
|
+
flyio: "fly-client-ip",
|
|
2311
|
+
render: "x-render-client-ip",
|
|
2312
|
+
firebase: "x-appengine-user-ip",
|
|
2313
|
+
"aws-alb": "x-forwarded-for"
|
|
2314
|
+
};
|
|
2315
|
+
function detectPlatform() {
|
|
2316
|
+
const env = typeof process !== "undefined" ? process.env : {};
|
|
2317
|
+
if (env.CF_PAGES || env.CF_WORKERS) return "cloudflare";
|
|
2318
|
+
if (env.VERCEL) return "vercel";
|
|
2319
|
+
if (env.FLY_APP_NAME) return "flyio";
|
|
2320
|
+
if (env.RENDER) return "render";
|
|
2321
|
+
if (env.FIREBASE_CONFIG || env.GCLOUD_PROJECT) return "firebase";
|
|
2322
|
+
if (env.AWS_EXECUTION_ENV || env.AWS_LAMBDA_FUNCTION_NAME) return "aws-alb";
|
|
2323
|
+
return "generic";
|
|
2324
|
+
}
|
|
2325
|
+
var _cachedPlatform = null;
|
|
2326
|
+
function getCachedPlatform() {
|
|
2327
|
+
if (_cachedPlatform === null) {
|
|
2328
|
+
_cachedPlatform = detectPlatform();
|
|
1577
2329
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
2330
|
+
return _cachedPlatform;
|
|
2331
|
+
}
|
|
2332
|
+
var MAX_IP_LENGTH = 45;
|
|
2333
|
+
function sanitizeIp(ip) {
|
|
2334
|
+
const trimmed = ip.trim();
|
|
2335
|
+
if (trimmed.length > MAX_IP_LENGTH) return trimmed.slice(0, MAX_IP_LENGTH);
|
|
2336
|
+
return trimmed;
|
|
2337
|
+
}
|
|
2338
|
+
function getHeader(req, name) {
|
|
2339
|
+
const val = req.headers[name];
|
|
2340
|
+
if (Array.isArray(val)) return val[0];
|
|
2341
|
+
return val;
|
|
2342
|
+
}
|
|
2343
|
+
function parseForwardedFor(header, trustedProxyCount) {
|
|
2344
|
+
const ips = header.split(",").map((ip) => ip.trim()).filter(Boolean);
|
|
2345
|
+
if (ips.length === 0) return void 0;
|
|
2346
|
+
const clientIndex = Math.max(0, ips.length - trustedProxyCount);
|
|
2347
|
+
return ips[clientIndex] || void 0;
|
|
2348
|
+
}
|
|
2349
|
+
function detectClientIp(req, options = {}) {
|
|
2350
|
+
const { platform = "auto", trustedProxyCount = 1 } = options;
|
|
2351
|
+
const r = req;
|
|
2352
|
+
const resolvedPlatform = platform === "auto" ? getCachedPlatform() : platform;
|
|
2353
|
+
if (resolvedPlatform !== "generic" && resolvedPlatform in PLATFORM_HEADERS) {
|
|
2354
|
+
const headerName = PLATFORM_HEADERS[resolvedPlatform];
|
|
2355
|
+
if (headerName) {
|
|
2356
|
+
if (resolvedPlatform === "aws-alb") {
|
|
2357
|
+
const xff2 = getHeader(r, "x-forwarded-for");
|
|
2358
|
+
if (xff2) {
|
|
2359
|
+
const ip = parseForwardedFor(xff2, trustedProxyCount);
|
|
2360
|
+
if (ip) return sanitizeIp(ip);
|
|
2361
|
+
}
|
|
2362
|
+
} else {
|
|
2363
|
+
const ip = getHeader(r, headerName);
|
|
2364
|
+
if (ip) return sanitizeIp(ip);
|
|
2365
|
+
}
|
|
1583
2366
|
}
|
|
1584
2367
|
}
|
|
1585
|
-
if (
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
}
|
|
1591
|
-
if (/^0\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
|
|
1592
|
-
return "current network address (0.0.0.0/8)";
|
|
2368
|
+
if (r.ip) return sanitizeIp(r.ip);
|
|
2369
|
+
const xff = getHeader(r, "x-forwarded-for");
|
|
2370
|
+
if (xff) {
|
|
2371
|
+
const ip = parseForwardedFor(xff, trustedProxyCount);
|
|
2372
|
+
if (ip) return sanitizeIp(ip);
|
|
1593
2373
|
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
return "private IPv6 address";
|
|
1600
|
-
}
|
|
1601
|
-
return null;
|
|
2374
|
+
const realIp = getHeader(r, "x-real-ip");
|
|
2375
|
+
if (realIp) return sanitizeIp(realIp);
|
|
2376
|
+
const socketIp = r.socket?.remoteAddress ?? r.connection?.remoteAddress;
|
|
2377
|
+
if (socketIp) return sanitizeIp(socketIp);
|
|
2378
|
+
return "unknown";
|
|
1602
2379
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
2380
|
+
function isPrivateIp(ip) {
|
|
2381
|
+
const normalized = ip.startsWith("::ffff:") ? ip.slice(7) : ip;
|
|
2382
|
+
if (/^127\./.test(normalized)) return true;
|
|
2383
|
+
if (/^10\./.test(normalized)) return true;
|
|
2384
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(normalized)) return true;
|
|
2385
|
+
if (/^192\.168\./.test(normalized)) return true;
|
|
2386
|
+
if (/^169\.254\./.test(normalized)) return true;
|
|
2387
|
+
if (/^0\./.test(normalized)) return true;
|
|
2388
|
+
if (ip === "::1") return true;
|
|
2389
|
+
if (/^fe80:/i.test(ip)) return true;
|
|
2390
|
+
if (/^fc00:/i.test(ip)) return true;
|
|
2391
|
+
if (/^fd/i.test(ip)) return true;
|
|
2392
|
+
return false;
|
|
2393
|
+
}
|
|
2394
|
+
function getHeader2(req, name) {
|
|
2395
|
+
const val = req.headers[name];
|
|
2396
|
+
if (Array.isArray(val)) return val[0] ?? "";
|
|
2397
|
+
return val ?? "";
|
|
2398
|
+
}
|
|
2399
|
+
function fingerprint(req, options = {}) {
|
|
1608
2400
|
const {
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
2401
|
+
ip = true,
|
|
2402
|
+
userAgent = true,
|
|
2403
|
+
accept = true,
|
|
2404
|
+
acceptLanguage = true,
|
|
2405
|
+
acceptEncoding = true,
|
|
2406
|
+
custom = [],
|
|
2407
|
+
ipOptions
|
|
1612
2408
|
} = options;
|
|
1613
|
-
|
|
1614
|
-
|
|
2409
|
+
const components = [];
|
|
2410
|
+
if (ip) {
|
|
2411
|
+
components.push(`ip:${detectClientIp(req, ipOptions)}`);
|
|
1615
2412
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
const proto = cleaned.match(DANGEROUS_PROTOCOLS);
|
|
1619
|
-
return { safe: false, reason: `dangerous protocol: ${proto[0]}` };
|
|
1620
|
-
}
|
|
1621
|
-
if (cleaned.startsWith("\\")) {
|
|
1622
|
-
return { safe: false, reason: "backslash-prefixed URL (browser treats as protocol-relative)" };
|
|
1623
|
-
}
|
|
1624
|
-
if (cleaned.startsWith("//")) {
|
|
1625
|
-
if (!allowProtocolRelative) {
|
|
1626
|
-
const host2 = extractHost(cleaned);
|
|
1627
|
-
if (host2 && allowedHosts.some((h) => host2 === h.toLowerCase())) {
|
|
1628
|
-
return { safe: true };
|
|
1629
|
-
}
|
|
1630
|
-
return { safe: false, reason: "protocol-relative URL not in allowed hosts" };
|
|
1631
|
-
}
|
|
1632
|
-
const host = extractHost(cleaned);
|
|
1633
|
-
if (host && allowedHosts.length > 0 && !allowedHosts.some((h) => host === h.toLowerCase())) {
|
|
1634
|
-
return { safe: false, reason: "protocol-relative URL not in allowed hosts" };
|
|
1635
|
-
}
|
|
1636
|
-
return { safe: true };
|
|
2413
|
+
if (userAgent) {
|
|
2414
|
+
components.push(`ua:${getHeader2(req, "user-agent")}`);
|
|
1637
2415
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
parsed = new URL(cleaned);
|
|
1641
|
-
} catch {
|
|
1642
|
-
return { safe: true };
|
|
2416
|
+
if (accept) {
|
|
2417
|
+
components.push(`accept:${getHeader2(req, "accept")}`);
|
|
1643
2418
|
}
|
|
1644
|
-
if (
|
|
1645
|
-
|
|
2419
|
+
if (acceptLanguage) {
|
|
2420
|
+
components.push(`lang:${getHeader2(req, "accept-language")}`);
|
|
1646
2421
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
return { safe: false, reason: "absolute URL not in allowed hosts" };
|
|
2422
|
+
if (acceptEncoding) {
|
|
2423
|
+
components.push(`enc:${getHeader2(req, "accept-encoding")}`);
|
|
1650
2424
|
}
|
|
1651
|
-
|
|
1652
|
-
|
|
2425
|
+
for (const c of custom) {
|
|
2426
|
+
if (c != null) components.push(`custom:${c}`);
|
|
1653
2427
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
return
|
|
1658
|
-
}
|
|
1659
|
-
function extractHost(url) {
|
|
1660
|
-
const match = url.match(/^\/\/([^/:?#]+)/);
|
|
1661
|
-
return match ? match[1].toLowerCase() : null;
|
|
2428
|
+
components.sort();
|
|
2429
|
+
const hash = crypto.createHash("sha256");
|
|
2430
|
+
hash.update(components.join("|"));
|
|
2431
|
+
return hash.digest("hex");
|
|
1662
2432
|
}
|
|
1663
2433
|
|
|
1664
2434
|
// src/stores/memory.ts
|
|
@@ -1813,6 +2583,7 @@ exports.SecurityThreatError = SecurityThreatError;
|
|
|
1813
2583
|
exports.VALIDATION = VALIDATION;
|
|
1814
2584
|
exports.arcis = arcis;
|
|
1815
2585
|
exports.arcisFunction = arcisWithMethods;
|
|
2586
|
+
exports.botProtection = botProtection;
|
|
1816
2587
|
exports.createCors = createCors;
|
|
1817
2588
|
exports.createErrorHandler = createErrorHandler;
|
|
1818
2589
|
exports.createHeaders = createHeaders;
|
|
@@ -1822,8 +2593,12 @@ exports.createRedisStore = createRedisStore;
|
|
|
1822
2593
|
exports.createSafeLogger = createSafeLogger;
|
|
1823
2594
|
exports.createSanitizer = createSanitizer;
|
|
1824
2595
|
exports.createSecureCookies = createSecureCookies;
|
|
2596
|
+
exports.createSlidingWindowLimiter = createSlidingWindowLimiter;
|
|
2597
|
+
exports.createTokenBucketLimiter = createTokenBucketLimiter;
|
|
1825
2598
|
exports.createValidator = createValidator;
|
|
1826
2599
|
exports.default = main_default;
|
|
2600
|
+
exports.detectBot = detectBot;
|
|
2601
|
+
exports.detectClientIp = detectClientIp;
|
|
1827
2602
|
exports.detectCommandInjection = detectCommandInjection;
|
|
1828
2603
|
exports.detectHeaderInjection = detectHeaderInjection;
|
|
1829
2604
|
exports.detectNoSqlInjection = detectNoSqlInjection;
|
|
@@ -1833,11 +2608,16 @@ exports.detectSql = detectSql;
|
|
|
1833
2608
|
exports.detectXss = detectXss;
|
|
1834
2609
|
exports.enforceSecureCookie = enforceSecureCookie;
|
|
1835
2610
|
exports.errorHandler = errorHandler;
|
|
2611
|
+
exports.fingerprint = fingerprint;
|
|
2612
|
+
exports.formatDuration = formatDuration;
|
|
1836
2613
|
exports.isDangerousExtension = isDangerousExtension;
|
|
1837
2614
|
exports.isDangerousNoSqlKey = isDangerousNoSqlKey;
|
|
1838
2615
|
exports.isDangerousProtoKey = isDangerousProtoKey;
|
|
2616
|
+
exports.isPrivateIp = isPrivateIp;
|
|
1839
2617
|
exports.isRedirectSafe = isRedirectSafe;
|
|
1840
2618
|
exports.isUrlSafe = isUrlSafe;
|
|
2619
|
+
exports.isValidEmailSyntax = isValidEmailSyntax;
|
|
2620
|
+
exports.parseDuration = parseDuration;
|
|
1841
2621
|
exports.rateLimit = rateLimit;
|
|
1842
2622
|
exports.safeCors = safeCors;
|
|
1843
2623
|
exports.safeLog = safeLog;
|
|
@@ -1853,8 +2633,10 @@ exports.sanitizeXss = sanitizeXss;
|
|
|
1853
2633
|
exports.secureCookieDefaults = secureCookieDefaults;
|
|
1854
2634
|
exports.securityHeaders = securityHeaders;
|
|
1855
2635
|
exports.validate = validate;
|
|
2636
|
+
exports.validateEmail = validateEmail;
|
|
1856
2637
|
exports.validateFile = validateFile;
|
|
1857
2638
|
exports.validateRedirect = validateRedirect;
|
|
1858
2639
|
exports.validateUrl = validateUrl;
|
|
2640
|
+
exports.verifyEmailMx = verifyEmailMx;
|
|
1859
2641
|
//# sourceMappingURL=index.js.map
|
|
1860
2642
|
//# sourceMappingURL=index.js.map
|