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