@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
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { f as arcis, g as arcisFunction, h as botProtection, i as createCors, j as createErrorHandler, k as createHeaders, l as createRateLimiter, m as createSecureCookies, n as createSlidingWindowLimiter, o as createTokenBucketLimiter, g as default, p as detectBot, q as enforceSecureCookie, r as errorHandler, s as rateLimit, t as safeCors, u as secureCookieDefaults, v as securityHeaders } from '../index-CCcPuTBo.mjs';
|
|
2
2
|
import '../types-BOdL3ZWo.mjs';
|
|
3
3
|
import 'express';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { f as arcis, g as arcisFunction, h as botProtection, i as createCors, j as createErrorHandler, k as createHeaders, l as createRateLimiter, m as createSecureCookies, n as createSlidingWindowLimiter, o as createTokenBucketLimiter, g as default, p as detectBot, q as enforceSecureCookie, r as errorHandler, s as rateLimit, t as safeCors, u as secureCookieDefaults, v as securityHeaders } from '../index-BvcFpoR3.js';
|
|
2
2
|
import '../types-BOdL3ZWo.js';
|
|
3
3
|
import 'express';
|
package/dist/middleware/index.js
CHANGED
|
@@ -1025,6 +1025,187 @@ arcisWithMethods.logger = createSafeLogger;
|
|
|
1025
1025
|
arcisWithMethods.errorHandler = createErrorHandler;
|
|
1026
1026
|
var main_default = arcisWithMethods;
|
|
1027
1027
|
|
|
1028
|
+
// src/utils/duration.ts
|
|
1029
|
+
var MAX_DURATION_MS = 4294967295;
|
|
1030
|
+
var DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/i;
|
|
1031
|
+
var UNIT_TO_MS = {
|
|
1032
|
+
ms: 1,
|
|
1033
|
+
s: 1e3,
|
|
1034
|
+
m: 6e4,
|
|
1035
|
+
h: 36e5,
|
|
1036
|
+
d: 864e5
|
|
1037
|
+
};
|
|
1038
|
+
function parseDuration(value) {
|
|
1039
|
+
if (typeof value === "number") {
|
|
1040
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1041
|
+
throw new Error(`Invalid duration: ${value}. Must be a non-negative finite number.`);
|
|
1042
|
+
}
|
|
1043
|
+
return Math.min(Math.floor(value), MAX_DURATION_MS);
|
|
1044
|
+
}
|
|
1045
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
1046
|
+
throw new Error(`Invalid duration: "${value}". Expected a duration string (e.g. "5m", "2h") or number.`);
|
|
1047
|
+
}
|
|
1048
|
+
const match = value.trim().match(DURATION_REGEX);
|
|
1049
|
+
if (!match) {
|
|
1050
|
+
throw new Error(
|
|
1051
|
+
`Invalid duration: "${value}". Expected format: <number><unit> where unit is ms, s, m, h, or d.`
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
const amount = parseFloat(match[1]);
|
|
1055
|
+
const unit = match[2].toLowerCase();
|
|
1056
|
+
const ms = Math.floor(amount * UNIT_TO_MS[unit]);
|
|
1057
|
+
if (ms < 0 || ms > MAX_DURATION_MS) {
|
|
1058
|
+
throw new Error(`Duration "${value}" exceeds maximum allowed (${MAX_DURATION_MS}ms / ~49.7 days).`);
|
|
1059
|
+
}
|
|
1060
|
+
return ms;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// src/middleware/rate-limit-sliding.ts
|
|
1064
|
+
function createSlidingWindowLimiter(options = {}) {
|
|
1065
|
+
const {
|
|
1066
|
+
max = RATE_LIMIT.DEFAULT_MAX_REQUESTS,
|
|
1067
|
+
window: windowOpt = RATE_LIMIT.DEFAULT_WINDOW_MS,
|
|
1068
|
+
message = RATE_LIMIT.DEFAULT_MESSAGE,
|
|
1069
|
+
statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
|
|
1070
|
+
keyGenerator = (req) => req.ip ?? req.socket?.remoteAddress ?? "unknown",
|
|
1071
|
+
skip
|
|
1072
|
+
} = options;
|
|
1073
|
+
const windowMs = parseDuration(windowOpt);
|
|
1074
|
+
const currentWindows = /* @__PURE__ */ Object.create(null);
|
|
1075
|
+
const previousWindows = /* @__PURE__ */ Object.create(null);
|
|
1076
|
+
const cleanupInterval = setInterval(() => {
|
|
1077
|
+
const now = Date.now();
|
|
1078
|
+
const cutoff = now - windowMs * 2;
|
|
1079
|
+
for (const key of Object.keys(previousWindows)) {
|
|
1080
|
+
if (previousWindows[key].startTime < cutoff) {
|
|
1081
|
+
delete previousWindows[key];
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
for (const key of Object.keys(currentWindows)) {
|
|
1085
|
+
if (currentWindows[key].startTime < cutoff) {
|
|
1086
|
+
delete currentWindows[key];
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}, windowMs);
|
|
1090
|
+
if (typeof cleanupInterval.unref === "function") {
|
|
1091
|
+
cleanupInterval.unref();
|
|
1092
|
+
}
|
|
1093
|
+
const handler = (req, res, next) => {
|
|
1094
|
+
try {
|
|
1095
|
+
if (skip?.(req)) return next();
|
|
1096
|
+
const key = keyGenerator(req);
|
|
1097
|
+
const now = Date.now();
|
|
1098
|
+
const windowStart = Math.floor(now / windowMs) * windowMs;
|
|
1099
|
+
if (!currentWindows[key] || currentWindows[key].startTime < windowStart) {
|
|
1100
|
+
if (currentWindows[key]) {
|
|
1101
|
+
previousWindows[key] = currentWindows[key];
|
|
1102
|
+
}
|
|
1103
|
+
currentWindows[key] = { count: 0, startTime: windowStart };
|
|
1104
|
+
}
|
|
1105
|
+
const elapsed = now - windowStart;
|
|
1106
|
+
const weight = Math.max(0, (windowMs - elapsed) / windowMs);
|
|
1107
|
+
const prevCount = previousWindows[key]?.count ?? 0;
|
|
1108
|
+
const estimatedCount = prevCount * weight + currentWindows[key].count + 1;
|
|
1109
|
+
const remaining = Math.max(0, Math.floor(max - estimatedCount));
|
|
1110
|
+
const resetMs = windowStart + windowMs - now;
|
|
1111
|
+
const resetSeconds = Math.max(1, Math.ceil(resetMs / 1e3));
|
|
1112
|
+
res.setHeader("X-RateLimit-Limit", max.toString());
|
|
1113
|
+
res.setHeader("X-RateLimit-Remaining", remaining.toString());
|
|
1114
|
+
res.setHeader("X-RateLimit-Reset", resetSeconds.toString());
|
|
1115
|
+
res.setHeader("X-RateLimit-Policy", `${max};w=${Math.floor(windowMs / 1e3)}`);
|
|
1116
|
+
if (estimatedCount > max) {
|
|
1117
|
+
res.setHeader("Retry-After", resetSeconds.toString());
|
|
1118
|
+
res.status(statusCode).json({
|
|
1119
|
+
error: message,
|
|
1120
|
+
retryAfter: resetSeconds
|
|
1121
|
+
});
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
currentWindows[key].count++;
|
|
1125
|
+
next();
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
console.error("[arcis] Sliding window rate limiter error:", error);
|
|
1128
|
+
next();
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
const middleware = handler;
|
|
1132
|
+
middleware.close = () => {
|
|
1133
|
+
clearInterval(cleanupInterval);
|
|
1134
|
+
};
|
|
1135
|
+
return middleware;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// src/middleware/rate-limit-token.ts
|
|
1139
|
+
function createTokenBucketLimiter(options = {}) {
|
|
1140
|
+
const {
|
|
1141
|
+
capacity = 100,
|
|
1142
|
+
refillRate = 10,
|
|
1143
|
+
cost = 1,
|
|
1144
|
+
message = RATE_LIMIT.DEFAULT_MESSAGE,
|
|
1145
|
+
statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
|
|
1146
|
+
keyGenerator = (req) => req.ip ?? req.socket?.remoteAddress ?? "unknown",
|
|
1147
|
+
skip
|
|
1148
|
+
} = options;
|
|
1149
|
+
if (capacity < 1) throw new RangeError(`Token bucket capacity must be >= 1, got ${capacity}`);
|
|
1150
|
+
if (refillRate <= 0) throw new RangeError(`Token bucket refillRate must be > 0, got ${refillRate}`);
|
|
1151
|
+
if (cost < 1) throw new RangeError(`Token bucket cost must be >= 1, got ${cost}`);
|
|
1152
|
+
if (cost > capacity) throw new RangeError(`Token bucket cost (${cost}) must be <= capacity (${capacity}), otherwise all requests are permanently denied`);
|
|
1153
|
+
const buckets = /* @__PURE__ */ Object.create(null);
|
|
1154
|
+
const cleanupInterval = setInterval(() => {
|
|
1155
|
+
const now = Date.now();
|
|
1156
|
+
const staleThreshold = capacity / refillRate * 1e3 * 2;
|
|
1157
|
+
for (const key of Object.keys(buckets)) {
|
|
1158
|
+
if (now - buckets[key].lastRefill > staleThreshold) {
|
|
1159
|
+
delete buckets[key];
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}, 6e4);
|
|
1163
|
+
if (typeof cleanupInterval.unref === "function") {
|
|
1164
|
+
cleanupInterval.unref();
|
|
1165
|
+
}
|
|
1166
|
+
function refillBucket(bucket, now) {
|
|
1167
|
+
const elapsed = (now - bucket.lastRefill) / 1e3;
|
|
1168
|
+
const tokensToAdd = elapsed * refillRate;
|
|
1169
|
+
bucket.tokens = Math.min(capacity, bucket.tokens + tokensToAdd);
|
|
1170
|
+
bucket.lastRefill = now;
|
|
1171
|
+
}
|
|
1172
|
+
const handler = (req, res, next) => {
|
|
1173
|
+
try {
|
|
1174
|
+
if (skip?.(req)) return next();
|
|
1175
|
+
const key = keyGenerator(req);
|
|
1176
|
+
const now = Date.now();
|
|
1177
|
+
if (!buckets[key]) {
|
|
1178
|
+
buckets[key] = { tokens: capacity, lastRefill: now };
|
|
1179
|
+
}
|
|
1180
|
+
const bucket = buckets[key];
|
|
1181
|
+
refillBucket(bucket, now);
|
|
1182
|
+
const retryAfterSec = bucket.tokens < cost ? Math.ceil((cost - bucket.tokens) / refillRate) : 0;
|
|
1183
|
+
res.setHeader("X-RateLimit-Limit", capacity.toString());
|
|
1184
|
+
res.setHeader("X-RateLimit-Remaining", Math.floor(Math.max(0, bucket.tokens - cost)).toString());
|
|
1185
|
+
res.setHeader("X-RateLimit-Policy", `${capacity};w=${Math.floor(capacity / refillRate)};burst=${capacity}`);
|
|
1186
|
+
if (bucket.tokens < cost) {
|
|
1187
|
+
res.setHeader("Retry-After", retryAfterSec.toString());
|
|
1188
|
+
res.setHeader("X-RateLimit-Reset", retryAfterSec.toString());
|
|
1189
|
+
res.status(statusCode).json({
|
|
1190
|
+
error: message,
|
|
1191
|
+
retryAfter: retryAfterSec
|
|
1192
|
+
});
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
bucket.tokens -= cost;
|
|
1196
|
+
next();
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
console.error("[arcis] Token bucket rate limiter error:", error);
|
|
1199
|
+
next();
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
const middleware = handler;
|
|
1203
|
+
middleware.close = () => {
|
|
1204
|
+
clearInterval(cleanupInterval);
|
|
1205
|
+
};
|
|
1206
|
+
return middleware;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1028
1209
|
// src/middleware/cors.ts
|
|
1029
1210
|
var DEFAULT_METHODS = ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"];
|
|
1030
1211
|
var DEFAULT_HEADERS = ["Content-Type", "Authorization"];
|
|
@@ -1155,14 +1336,211 @@ function secureCookieDefaults(options = {}) {
|
|
|
1155
1336
|
}
|
|
1156
1337
|
var createSecureCookies = secureCookieDefaults;
|
|
1157
1338
|
|
|
1339
|
+
// src/middleware/bot-detection.ts
|
|
1340
|
+
var BOT_PATTERNS = [
|
|
1341
|
+
// --- SEARCH ENGINES (specific variants before generic) ---
|
|
1342
|
+
{ pattern: /Googlebot-Image/i, name: "Googlebot-Image", category: "SEARCH_ENGINE" },
|
|
1343
|
+
{ pattern: /Googlebot-Video/i, name: "Googlebot-Video", category: "SEARCH_ENGINE" },
|
|
1344
|
+
{ pattern: /Googlebot-News/i, name: "Googlebot-News", category: "SEARCH_ENGINE" },
|
|
1345
|
+
{ pattern: /Googlebot/i, name: "Googlebot", category: "SEARCH_ENGINE" },
|
|
1346
|
+
{ pattern: /AdsBot-Google/i, name: "AdsBot-Google", category: "SEARCH_ENGINE" },
|
|
1347
|
+
{ pattern: /Mediapartners-Google/i, name: "Mediapartners-Google", category: "SEARCH_ENGINE" },
|
|
1348
|
+
{ pattern: /Bingbot/i, name: "Bingbot", category: "SEARCH_ENGINE" },
|
|
1349
|
+
{ pattern: /msnbot/i, name: "msnbot", category: "SEARCH_ENGINE" },
|
|
1350
|
+
{ pattern: /Slurp/i, name: "Yahoo Slurp", category: "SEARCH_ENGINE" },
|
|
1351
|
+
{ pattern: /DuckDuckBot/i, name: "DuckDuckBot", category: "SEARCH_ENGINE" },
|
|
1352
|
+
{ pattern: /Baiduspider/i, name: "Baiduspider", category: "SEARCH_ENGINE" },
|
|
1353
|
+
{ pattern: /YandexBot/i, name: "YandexBot", category: "SEARCH_ENGINE" },
|
|
1354
|
+
{ pattern: /YandexImages/i, name: "YandexImages", category: "SEARCH_ENGINE" },
|
|
1355
|
+
{ pattern: /Sogou/i, name: "Sogou", category: "SEARCH_ENGINE" },
|
|
1356
|
+
{ pattern: /Exabot/i, name: "Exabot", category: "SEARCH_ENGINE" },
|
|
1357
|
+
{ pattern: /ia_archiver/i, name: "Alexa", category: "SEARCH_ENGINE" },
|
|
1358
|
+
{ pattern: /Applebot/i, name: "Applebot", category: "SEARCH_ENGINE" },
|
|
1359
|
+
{ pattern: /Qwantify/i, name: "Qwantify", category: "SEARCH_ENGINE" },
|
|
1360
|
+
{ pattern: /PetalBot/i, name: "PetalBot", category: "SEARCH_ENGINE" },
|
|
1361
|
+
{ pattern: /SeznamBot/i, name: "SeznamBot", category: "SEARCH_ENGINE" },
|
|
1362
|
+
// --- SOCIAL ---
|
|
1363
|
+
{ pattern: /Twitterbot/i, name: "Twitterbot", category: "SOCIAL" },
|
|
1364
|
+
{ pattern: /facebookexternalhit/i, name: "Facebook", category: "SOCIAL" },
|
|
1365
|
+
{ pattern: /Facebot/i, name: "Facebot", category: "SOCIAL" },
|
|
1366
|
+
{ pattern: /LinkedInBot/i, name: "LinkedInBot", category: "SOCIAL" },
|
|
1367
|
+
{ pattern: /Pinterest/i, name: "Pinterest", category: "SOCIAL" },
|
|
1368
|
+
{ pattern: /Slackbot/i, name: "Slackbot", category: "SOCIAL" },
|
|
1369
|
+
{ pattern: /TelegramBot/i, name: "TelegramBot", category: "SOCIAL" },
|
|
1370
|
+
{ pattern: /WhatsApp/i, name: "WhatsApp", category: "SOCIAL" },
|
|
1371
|
+
{ pattern: /Discordbot/i, name: "Discordbot", category: "SOCIAL" },
|
|
1372
|
+
{ pattern: /Redditbot/i, name: "Redditbot", category: "SOCIAL" },
|
|
1373
|
+
{ pattern: /Embedly/i, name: "Embedly", category: "SOCIAL" },
|
|
1374
|
+
{ pattern: /Quora Link Preview/i, name: "Quora", category: "SOCIAL" },
|
|
1375
|
+
{ pattern: /Mastodon/i, name: "Mastodon", category: "SOCIAL" },
|
|
1376
|
+
// --- MONITORING ---
|
|
1377
|
+
{ pattern: /UptimeRobot/i, name: "UptimeRobot", category: "MONITORING" },
|
|
1378
|
+
{ pattern: /Pingdom/i, name: "Pingdom", category: "MONITORING" },
|
|
1379
|
+
{ pattern: /Site24x7/i, name: "Site24x7", category: "MONITORING" },
|
|
1380
|
+
{ pattern: /StatusCake/i, name: "StatusCake", category: "MONITORING" },
|
|
1381
|
+
{ pattern: /Datadog/i, name: "Datadog", category: "MONITORING" },
|
|
1382
|
+
{ pattern: /NewRelicPinger/i, name: "New Relic", category: "MONITORING" },
|
|
1383
|
+
{ pattern: /Better Uptime Bot/i, name: "Better Uptime", category: "MONITORING" },
|
|
1384
|
+
{ pattern: /GTmetrix/i, name: "GTmetrix", category: "MONITORING" },
|
|
1385
|
+
{ pattern: /PageSpeed/i, name: "PageSpeed Insights", category: "MONITORING" },
|
|
1386
|
+
// --- AI CRAWLERS ---
|
|
1387
|
+
{ pattern: /GPTBot/i, name: "GPTBot", category: "AI_CRAWLER" },
|
|
1388
|
+
{ pattern: /ChatGPT-User/i, name: "ChatGPT-User", category: "AI_CRAWLER" },
|
|
1389
|
+
{ pattern: /Claude-Web/i, name: "Claude-Web", category: "AI_CRAWLER" },
|
|
1390
|
+
{ pattern: /ClaudeBot/i, name: "ClaudeBot", category: "AI_CRAWLER" },
|
|
1391
|
+
{ pattern: /anthropic-ai/i, name: "Anthropic", category: "AI_CRAWLER" },
|
|
1392
|
+
{ pattern: /Bytespider/i, name: "Bytespider", category: "AI_CRAWLER" },
|
|
1393
|
+
{ pattern: /CCBot/i, name: "CCBot", category: "AI_CRAWLER" },
|
|
1394
|
+
{ pattern: /cohere-ai/i, name: "Cohere", category: "AI_CRAWLER" },
|
|
1395
|
+
{ pattern: /PerplexityBot/i, name: "PerplexityBot", category: "AI_CRAWLER" },
|
|
1396
|
+
{ pattern: /YouBot/i, name: "YouBot", category: "AI_CRAWLER" },
|
|
1397
|
+
{ pattern: /Google-Extended/i, name: "Google-Extended", category: "AI_CRAWLER" },
|
|
1398
|
+
{ pattern: /Diffbot/i, name: "Diffbot", category: "AI_CRAWLER" },
|
|
1399
|
+
{ pattern: /Amazonbot/i, name: "Amazonbot", category: "AI_CRAWLER" },
|
|
1400
|
+
{ pattern: /meta-externalagent/i, name: "Meta AI", category: "AI_CRAWLER" },
|
|
1401
|
+
// --- AUTOMATED TOOLS (headless browsers, testing frameworks) ---
|
|
1402
|
+
{ pattern: /HeadlessChrome/i, name: "Headless Chrome", category: "AUTOMATED" },
|
|
1403
|
+
{ pattern: /PhantomJS/i, name: "PhantomJS", category: "AUTOMATED" },
|
|
1404
|
+
{ pattern: /Selenium/i, name: "Selenium", category: "AUTOMATED" },
|
|
1405
|
+
{ pattern: /Puppeteer/i, name: "Puppeteer", category: "AUTOMATED" },
|
|
1406
|
+
{ pattern: /Playwright/i, name: "Playwright", category: "AUTOMATED" },
|
|
1407
|
+
{ pattern: /Cypress/i, name: "Cypress", category: "AUTOMATED" },
|
|
1408
|
+
{ pattern: /webdriver/i, name: "WebDriver", category: "AUTOMATED" },
|
|
1409
|
+
{ pattern: /MSIE 6\.0/i, name: "Fake IE6", category: "AUTOMATED" },
|
|
1410
|
+
// --- SCRAPERS / CLI TOOLS ---
|
|
1411
|
+
{ pattern: /^curl\//i, name: "curl", category: "SCRAPER" },
|
|
1412
|
+
{ pattern: /^wget\//i, name: "wget", category: "SCRAPER" },
|
|
1413
|
+
{ pattern: /^python-requests\//i, name: "python-requests", category: "SCRAPER" },
|
|
1414
|
+
{ pattern: /^python-httpx\//i, name: "python-httpx", category: "SCRAPER" },
|
|
1415
|
+
{ pattern: /^Python-urllib/i, name: "Python-urllib", category: "SCRAPER" },
|
|
1416
|
+
{ pattern: /^aiohttp\//i, name: "aiohttp", category: "SCRAPER" },
|
|
1417
|
+
{ pattern: /^Go-http-client/i, name: "Go-http-client", category: "SCRAPER" },
|
|
1418
|
+
{ pattern: /^Java\//i, name: "Java HttpClient", category: "SCRAPER" },
|
|
1419
|
+
{ pattern: /^Apache-HttpClient/i, name: "Apache HttpClient", category: "SCRAPER" },
|
|
1420
|
+
{ pattern: /^okhttp\//i, name: "OkHttp", category: "SCRAPER" },
|
|
1421
|
+
{ pattern: /^node-fetch\//i, name: "node-fetch", category: "SCRAPER" },
|
|
1422
|
+
{ pattern: /^axios\//i, name: "axios", category: "SCRAPER" },
|
|
1423
|
+
{ pattern: /^got\//i, name: "got", category: "SCRAPER" },
|
|
1424
|
+
{ pattern: /^libwww-perl/i, name: "libwww-perl", category: "SCRAPER" },
|
|
1425
|
+
{ pattern: /^Ruby/i, name: "Ruby", category: "SCRAPER" },
|
|
1426
|
+
{ pattern: /^PHP\//i, name: "PHP", category: "SCRAPER" },
|
|
1427
|
+
{ pattern: /Scrapy/i, name: "Scrapy", category: "SCRAPER" },
|
|
1428
|
+
{ pattern: /^Postman/i, name: "Postman", category: "SCRAPER" },
|
|
1429
|
+
{ pattern: /^Insomnia/i, name: "Insomnia", category: "SCRAPER" },
|
|
1430
|
+
{ pattern: /^HTTPie\//i, name: "HTTPie", category: "SCRAPER" }
|
|
1431
|
+
];
|
|
1432
|
+
function detectBehavioralSignals(req) {
|
|
1433
|
+
const signals = [];
|
|
1434
|
+
const headers = req.headers;
|
|
1435
|
+
if (!headers["user-agent"]) {
|
|
1436
|
+
signals.push("missing_user_agent");
|
|
1437
|
+
}
|
|
1438
|
+
if (!headers["accept"]) {
|
|
1439
|
+
signals.push("missing_accept");
|
|
1440
|
+
}
|
|
1441
|
+
if (!headers["accept-language"]) {
|
|
1442
|
+
signals.push("missing_accept_language");
|
|
1443
|
+
}
|
|
1444
|
+
if (!headers["accept-encoding"]) {
|
|
1445
|
+
signals.push("missing_accept_encoding");
|
|
1446
|
+
}
|
|
1447
|
+
if (headers["connection"] === "close") {
|
|
1448
|
+
signals.push("connection_close");
|
|
1449
|
+
}
|
|
1450
|
+
return signals;
|
|
1451
|
+
}
|
|
1452
|
+
function detectBot(req) {
|
|
1453
|
+
const rawUa = req.headers["user-agent"] ?? "";
|
|
1454
|
+
const ua = rawUa.length > 2048 ? rawUa.slice(0, 2048) : rawUa;
|
|
1455
|
+
const signals = detectBehavioralSignals(req);
|
|
1456
|
+
if (!ua) {
|
|
1457
|
+
return {
|
|
1458
|
+
isBot: true,
|
|
1459
|
+
category: "UNKNOWN",
|
|
1460
|
+
name: null,
|
|
1461
|
+
confidence: 0.8,
|
|
1462
|
+
signals
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
for (const bot of BOT_PATTERNS) {
|
|
1466
|
+
if (bot.pattern.test(ua)) {
|
|
1467
|
+
return {
|
|
1468
|
+
isBot: true,
|
|
1469
|
+
category: bot.category,
|
|
1470
|
+
name: bot.name,
|
|
1471
|
+
confidence: 0.95,
|
|
1472
|
+
signals
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
const behaviorScore = signals.length;
|
|
1477
|
+
if (behaviorScore >= 3) {
|
|
1478
|
+
return {
|
|
1479
|
+
isBot: true,
|
|
1480
|
+
category: "UNKNOWN",
|
|
1481
|
+
name: null,
|
|
1482
|
+
confidence: Math.min(1, 0.6 + behaviorScore * 0.1),
|
|
1483
|
+
signals
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
return {
|
|
1487
|
+
isBot: false,
|
|
1488
|
+
category: "HUMAN",
|
|
1489
|
+
name: null,
|
|
1490
|
+
confidence: Math.max(0, 1 - behaviorScore * 0.15),
|
|
1491
|
+
signals
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
function botProtection(options = {}) {
|
|
1495
|
+
const {
|
|
1496
|
+
allow = ["SEARCH_ENGINE", "SOCIAL", "MONITORING"],
|
|
1497
|
+
deny = ["AUTOMATED"],
|
|
1498
|
+
defaultAction = "allow",
|
|
1499
|
+
statusCode = 403,
|
|
1500
|
+
message = "Access denied.",
|
|
1501
|
+
onDetected
|
|
1502
|
+
} = options;
|
|
1503
|
+
const allowSet = new Set(allow);
|
|
1504
|
+
const denySet = new Set(deny);
|
|
1505
|
+
return (req, res, next) => {
|
|
1506
|
+
const result = detectBot(req);
|
|
1507
|
+
req.botDetection = result;
|
|
1508
|
+
if (!result.isBot) {
|
|
1509
|
+
return next();
|
|
1510
|
+
}
|
|
1511
|
+
if (allowSet.has(result.category)) {
|
|
1512
|
+
return next();
|
|
1513
|
+
}
|
|
1514
|
+
if (denySet.has(result.category)) {
|
|
1515
|
+
if (onDetected) {
|
|
1516
|
+
return onDetected(req, res, result);
|
|
1517
|
+
}
|
|
1518
|
+
res.status(statusCode).json({ error: message });
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
if (defaultAction === "deny") {
|
|
1522
|
+
if (onDetected) {
|
|
1523
|
+
return onDetected(req, res, result);
|
|
1524
|
+
}
|
|
1525
|
+
res.status(statusCode).json({ error: message });
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
next();
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1158
1532
|
exports.arcis = arcis;
|
|
1159
1533
|
exports.arcisFunction = arcisWithMethods;
|
|
1534
|
+
exports.botProtection = botProtection;
|
|
1160
1535
|
exports.createCors = createCors;
|
|
1161
1536
|
exports.createErrorHandler = createErrorHandler;
|
|
1162
1537
|
exports.createHeaders = createHeaders;
|
|
1163
1538
|
exports.createRateLimiter = createRateLimiter;
|
|
1164
1539
|
exports.createSecureCookies = createSecureCookies;
|
|
1540
|
+
exports.createSlidingWindowLimiter = createSlidingWindowLimiter;
|
|
1541
|
+
exports.createTokenBucketLimiter = createTokenBucketLimiter;
|
|
1165
1542
|
exports.default = main_default;
|
|
1543
|
+
exports.detectBot = detectBot;
|
|
1166
1544
|
exports.enforceSecureCookie = enforceSecureCookie;
|
|
1167
1545
|
exports.errorHandler = errorHandler;
|
|
1168
1546
|
exports.rateLimit = rateLimit;
|