@arcis/node 1.0.0 → 1.2.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.
Files changed (54) hide show
  1. package/README.md +156 -222
  2. package/dist/core/index.d.mts +4 -4
  3. package/dist/core/index.d.ts +4 -4
  4. package/dist/core/index.js +13 -2
  5. package/dist/core/index.js.map +1 -1
  6. package/dist/core/index.mjs +13 -2
  7. package/dist/core/index.mjs.map +1 -1
  8. package/dist/index-A-m-pPeW.d.mts +340 -0
  9. package/dist/index-CgK94hY_.d.mts +532 -0
  10. package/dist/index-Co5kPRZz.d.ts +340 -0
  11. package/dist/index-D_bdJcF0.d.ts +532 -0
  12. package/dist/index.d.mts +144 -108
  13. package/dist/index.d.ts +144 -108
  14. package/dist/index.js +1541 -211
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +1515 -212
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/logging/index.d.mts +1 -1
  19. package/dist/logging/index.d.ts +1 -1
  20. package/dist/logging/index.js +12 -1
  21. package/dist/logging/index.js.map +1 -1
  22. package/dist/logging/index.mjs +12 -1
  23. package/dist/logging/index.mjs.map +1 -1
  24. package/dist/middleware/index.d.mts +2 -2
  25. package/dist/middleware/index.d.ts +2 -2
  26. package/dist/middleware/index.js +524 -4
  27. package/dist/middleware/index.js.map +1 -1
  28. package/dist/middleware/index.mjs +517 -5
  29. package/dist/middleware/index.mjs.map +1 -1
  30. package/dist/{headers-DBQedhrb.d.mts → pii-CXcHMlnX.d.mts} +156 -2
  31. package/dist/{headers-BJq2OA0i.d.ts → pii-DhNpl7M3.d.ts} +156 -2
  32. package/dist/sanitizers/index.d.mts +2 -2
  33. package/dist/sanitizers/index.d.ts +2 -2
  34. package/dist/sanitizers/index.js +331 -3
  35. package/dist/sanitizers/index.js.map +1 -1
  36. package/dist/sanitizers/index.mjs +321 -4
  37. package/dist/sanitizers/index.mjs.map +1 -1
  38. package/dist/stores/index.d.mts +1 -1
  39. package/dist/stores/index.d.ts +1 -1
  40. package/dist/stores/index.js.map +1 -1
  41. package/dist/stores/index.mjs.map +1 -1
  42. package/dist/{types-BOdL3ZWo.d.mts → types-CsOFHoD9.d.mts} +6 -1
  43. package/dist/{types-BOdL3ZWo.d.ts → types-CsOFHoD9.d.ts} +6 -1
  44. package/dist/validation/index.d.mts +2 -2
  45. package/dist/validation/index.d.ts +2 -2
  46. package/dist/validation/index.js +504 -2
  47. package/dist/validation/index.js.map +1 -1
  48. package/dist/validation/index.mjs +498 -3
  49. package/dist/validation/index.mjs.map +1 -1
  50. package/package.json +114 -109
  51. package/dist/index-BgHPM7LC.d.ts +0 -129
  52. package/dist/index-BpT7flAQ.d.ts +0 -255
  53. package/dist/index-JaFOUKyK.d.mts +0 -255
  54. package/dist/index-nAgXexwD.d.mts +0 -129
@@ -2,6 +2,8 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var crypto = require('crypto');
6
+
5
7
  // src/core/constants.ts
6
8
  var INPUT = {
7
9
  /** Default maximum input size (1MB) */
@@ -89,7 +91,11 @@ var SQL_PATTERNS = [
89
91
  /** Time-based blind: SLEEP() */
90
92
  /\bSLEEP\s*\(\s*\d+\s*\)/gi,
91
93
  /** Time-based blind: BENCHMARK() */
92
- /\bBENCHMARK\s*\(/gi
94
+ /\bBENCHMARK\s*\(/gi,
95
+ /** Time-based blind: PostgreSQL pg_sleep() */
96
+ /\bpg_sleep\s*\(/gi,
97
+ /** Time-based blind: MSSQL WAITFOR DELAY */
98
+ /\bWAITFOR\s+DELAY\b/gi
93
99
  ];
94
100
  var PATH_PATTERNS = [
95
101
  /** Unix path traversal */
@@ -107,6 +113,10 @@ var PATH_PATTERNS = [
107
113
  /\.%2e[\\/]/gi,
108
114
  /** Fully URL-encoded: %2e%2e%2f */
109
115
  /%2e%2e%2f/gi,
116
+ /** Double URL-encoded forward slash: %252f */
117
+ /%252f/gi,
118
+ /** Dotdotslash bypass: ....// or ....\\ */
119
+ /\.{2,}[/\\]{2,}/g,
110
120
  /** Null byte injection in paths */
111
121
  /\0/g
112
122
  ];
@@ -122,7 +132,9 @@ var COMMAND_PATTERNS = [
122
132
  */
123
133
  /[;&|`]/g,
124
134
  /** Command substitution: $( ... ) — matched as a pair to reduce false positives */
125
- /\$\(/g
135
+ /\$\(/g,
136
+ /** URL-encoded newline/carriage-return injection (%0a, %0d) */
137
+ /%0[ad]/gi
126
138
  ];
127
139
  var DANGEROUS_PROTO_KEYS = /* @__PURE__ */ new Set([
128
140
  "__proto__",
@@ -156,6 +168,7 @@ var NOSQL_DANGEROUS_KEYS = /* @__PURE__ */ new Set([
156
168
  "$expr",
157
169
  "$mod",
158
170
  "$text",
171
+ "$jsonSchema",
159
172
  // Array
160
173
  "$elemMatch",
161
174
  "$all",
@@ -709,7 +722,8 @@ function sanitizeObject(obj, options = {}) {
709
722
  if (typeof obj === "string") return sanitizeString(obj, options);
710
723
  if (typeof obj !== "object") return obj;
711
724
  if (Array.isArray(obj)) return obj.map((item) => sanitizeObject(item, options));
712
- return sanitizeObjectDepth(obj, options, 0);
725
+ const result = sanitizeObjectDepth(obj, options, 0);
726
+ return options.freeze ? Object.freeze(result) : result;
713
727
  }
714
728
  function sanitizeObjectDepth(obj, options, depth) {
715
729
  if (depth >= INPUT.MAX_RECURSION_DEPTH) return obj;
@@ -930,12 +944,21 @@ function validateField(field, value, rules) {
930
944
  "audio/mpeg": [Buffer.from([255, 251]), Buffer.from([255, 243]), Buffer.from([73, 68, 51])]});
931
945
 
932
946
  // src/logging/redactor.ts
947
+ var LOG_LEVELS = {
948
+ debug: 0,
949
+ info: 1,
950
+ warn: 2,
951
+ error: 3,
952
+ silent: 4
953
+ };
933
954
  function createSafeLogger(options = {}) {
934
955
  const {
935
956
  redactKeys = [],
936
957
  maxLength = REDACTION.DEFAULT_MAX_LENGTH,
937
- redactPatterns = []
958
+ redactPatterns = [],
959
+ level: minLevel = "debug"
938
960
  } = options;
961
+ const minLevelNum = LOG_LEVELS[minLevel] ?? 0;
939
962
  const allRedactKeys = /* @__PURE__ */ new Set([
940
963
  ...Array.from(REDACTION.SENSITIVE_KEYS),
941
964
  ...redactKeys.map((k) => k.toLowerCase())
@@ -961,6 +984,8 @@ function createSafeLogger(options = {}) {
961
984
  return result;
962
985
  }
963
986
  function log(level, message, data) {
987
+ const levelNum = LOG_LEVELS[level] ?? 0;
988
+ if (levelNum < minLevelNum) return;
964
989
  const entry = {
965
990
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
966
991
  level,
@@ -1025,6 +1050,187 @@ arcisWithMethods.logger = createSafeLogger;
1025
1050
  arcisWithMethods.errorHandler = createErrorHandler;
1026
1051
  var main_default = arcisWithMethods;
1027
1052
 
1053
+ // src/utils/duration.ts
1054
+ var MAX_DURATION_MS = 4294967295;
1055
+ var DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/i;
1056
+ var UNIT_TO_MS = {
1057
+ ms: 1,
1058
+ s: 1e3,
1059
+ m: 6e4,
1060
+ h: 36e5,
1061
+ d: 864e5
1062
+ };
1063
+ function parseDuration(value) {
1064
+ if (typeof value === "number") {
1065
+ if (!Number.isFinite(value) || value < 0) {
1066
+ throw new Error(`Invalid duration: ${value}. Must be a non-negative finite number.`);
1067
+ }
1068
+ return Math.min(Math.floor(value), MAX_DURATION_MS);
1069
+ }
1070
+ if (typeof value !== "string" || value.trim() === "") {
1071
+ throw new Error(`Invalid duration: "${value}". Expected a duration string (e.g. "5m", "2h") or number.`);
1072
+ }
1073
+ const match = value.trim().match(DURATION_REGEX);
1074
+ if (!match) {
1075
+ throw new Error(
1076
+ `Invalid duration: "${value}". Expected format: <number><unit> where unit is ms, s, m, h, or d.`
1077
+ );
1078
+ }
1079
+ const amount = parseFloat(match[1]);
1080
+ const unit = match[2].toLowerCase();
1081
+ const ms = Math.floor(amount * UNIT_TO_MS[unit]);
1082
+ if (ms < 0 || ms > MAX_DURATION_MS) {
1083
+ throw new Error(`Duration "${value}" exceeds maximum allowed (${MAX_DURATION_MS}ms / ~49.7 days).`);
1084
+ }
1085
+ return ms;
1086
+ }
1087
+
1088
+ // src/middleware/rate-limit-sliding.ts
1089
+ function createSlidingWindowLimiter(options = {}) {
1090
+ const {
1091
+ max = RATE_LIMIT.DEFAULT_MAX_REQUESTS,
1092
+ window: windowOpt = RATE_LIMIT.DEFAULT_WINDOW_MS,
1093
+ message = RATE_LIMIT.DEFAULT_MESSAGE,
1094
+ statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
1095
+ keyGenerator = (req) => req.ip ?? req.socket?.remoteAddress ?? "unknown",
1096
+ skip
1097
+ } = options;
1098
+ const windowMs = parseDuration(windowOpt);
1099
+ const currentWindows = /* @__PURE__ */ Object.create(null);
1100
+ const previousWindows = /* @__PURE__ */ Object.create(null);
1101
+ const cleanupInterval = setInterval(() => {
1102
+ const now = Date.now();
1103
+ const cutoff = now - windowMs * 2;
1104
+ for (const key of Object.keys(previousWindows)) {
1105
+ if (previousWindows[key].startTime < cutoff) {
1106
+ delete previousWindows[key];
1107
+ }
1108
+ }
1109
+ for (const key of Object.keys(currentWindows)) {
1110
+ if (currentWindows[key].startTime < cutoff) {
1111
+ delete currentWindows[key];
1112
+ }
1113
+ }
1114
+ }, windowMs);
1115
+ if (typeof cleanupInterval.unref === "function") {
1116
+ cleanupInterval.unref();
1117
+ }
1118
+ const handler = (req, res, next) => {
1119
+ try {
1120
+ if (skip?.(req)) return next();
1121
+ const key = keyGenerator(req);
1122
+ const now = Date.now();
1123
+ const windowStart = Math.floor(now / windowMs) * windowMs;
1124
+ if (!currentWindows[key] || currentWindows[key].startTime < windowStart) {
1125
+ if (currentWindows[key]) {
1126
+ previousWindows[key] = currentWindows[key];
1127
+ }
1128
+ currentWindows[key] = { count: 0, startTime: windowStart };
1129
+ }
1130
+ const elapsed = now - windowStart;
1131
+ const weight = Math.max(0, (windowMs - elapsed) / windowMs);
1132
+ const prevCount = previousWindows[key]?.count ?? 0;
1133
+ const estimatedCount = prevCount * weight + currentWindows[key].count + 1;
1134
+ const remaining = Math.max(0, Math.floor(max - estimatedCount));
1135
+ const resetMs = windowStart + windowMs - now;
1136
+ const resetSeconds = Math.max(1, Math.ceil(resetMs / 1e3));
1137
+ res.setHeader("X-RateLimit-Limit", max.toString());
1138
+ res.setHeader("X-RateLimit-Remaining", remaining.toString());
1139
+ res.setHeader("X-RateLimit-Reset", resetSeconds.toString());
1140
+ res.setHeader("X-RateLimit-Policy", `${max};w=${Math.floor(windowMs / 1e3)}`);
1141
+ if (estimatedCount > max) {
1142
+ res.setHeader("Retry-After", resetSeconds.toString());
1143
+ res.status(statusCode).json({
1144
+ error: message,
1145
+ retryAfter: resetSeconds
1146
+ });
1147
+ return;
1148
+ }
1149
+ currentWindows[key].count++;
1150
+ next();
1151
+ } catch (error) {
1152
+ console.error("[arcis] Sliding window rate limiter error:", error);
1153
+ next();
1154
+ }
1155
+ };
1156
+ const middleware = handler;
1157
+ middleware.close = () => {
1158
+ clearInterval(cleanupInterval);
1159
+ };
1160
+ return middleware;
1161
+ }
1162
+
1163
+ // src/middleware/rate-limit-token.ts
1164
+ function createTokenBucketLimiter(options = {}) {
1165
+ const {
1166
+ capacity = 100,
1167
+ refillRate = 10,
1168
+ cost = 1,
1169
+ message = RATE_LIMIT.DEFAULT_MESSAGE,
1170
+ statusCode = RATE_LIMIT.DEFAULT_STATUS_CODE,
1171
+ keyGenerator = (req) => req.ip ?? req.socket?.remoteAddress ?? "unknown",
1172
+ skip
1173
+ } = options;
1174
+ if (capacity < 1) throw new RangeError(`Token bucket capacity must be >= 1, got ${capacity}`);
1175
+ if (refillRate <= 0) throw new RangeError(`Token bucket refillRate must be > 0, got ${refillRate}`);
1176
+ if (cost < 1) throw new RangeError(`Token bucket cost must be >= 1, got ${cost}`);
1177
+ if (cost > capacity) throw new RangeError(`Token bucket cost (${cost}) must be <= capacity (${capacity}), otherwise all requests are permanently denied`);
1178
+ const buckets = /* @__PURE__ */ Object.create(null);
1179
+ const cleanupInterval = setInterval(() => {
1180
+ const now = Date.now();
1181
+ const staleThreshold = capacity / refillRate * 1e3 * 2;
1182
+ for (const key of Object.keys(buckets)) {
1183
+ if (now - buckets[key].lastRefill > staleThreshold) {
1184
+ delete buckets[key];
1185
+ }
1186
+ }
1187
+ }, 6e4);
1188
+ if (typeof cleanupInterval.unref === "function") {
1189
+ cleanupInterval.unref();
1190
+ }
1191
+ function refillBucket(bucket, now) {
1192
+ const elapsed = (now - bucket.lastRefill) / 1e3;
1193
+ const tokensToAdd = elapsed * refillRate;
1194
+ bucket.tokens = Math.min(capacity, bucket.tokens + tokensToAdd);
1195
+ bucket.lastRefill = now;
1196
+ }
1197
+ const handler = (req, res, next) => {
1198
+ try {
1199
+ if (skip?.(req)) return next();
1200
+ const key = keyGenerator(req);
1201
+ const now = Date.now();
1202
+ if (!buckets[key]) {
1203
+ buckets[key] = { tokens: capacity, lastRefill: now };
1204
+ }
1205
+ const bucket = buckets[key];
1206
+ refillBucket(bucket, now);
1207
+ const retryAfterSec = bucket.tokens < cost ? Math.ceil((cost - bucket.tokens) / refillRate) : 0;
1208
+ res.setHeader("X-RateLimit-Limit", capacity.toString());
1209
+ res.setHeader("X-RateLimit-Remaining", Math.floor(Math.max(0, bucket.tokens - cost)).toString());
1210
+ res.setHeader("X-RateLimit-Policy", `${capacity};w=${Math.floor(capacity / refillRate)};burst=${capacity}`);
1211
+ if (bucket.tokens < cost) {
1212
+ res.setHeader("Retry-After", retryAfterSec.toString());
1213
+ res.setHeader("X-RateLimit-Reset", retryAfterSec.toString());
1214
+ res.status(statusCode).json({
1215
+ error: message,
1216
+ retryAfter: retryAfterSec
1217
+ });
1218
+ return;
1219
+ }
1220
+ bucket.tokens -= cost;
1221
+ next();
1222
+ } catch (error) {
1223
+ console.error("[arcis] Token bucket rate limiter error:", error);
1224
+ next();
1225
+ }
1226
+ };
1227
+ const middleware = handler;
1228
+ middleware.close = () => {
1229
+ clearInterval(cleanupInterval);
1230
+ };
1231
+ return middleware;
1232
+ }
1233
+
1028
1234
  // src/middleware/cors.ts
1029
1235
  var DEFAULT_METHODS = ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"];
1030
1236
  var DEFAULT_HEADERS = ["Content-Type", "Authorization"];
@@ -1155,19 +1361,333 @@ function secureCookieDefaults(options = {}) {
1155
1361
  }
1156
1362
  var createSecureCookies = secureCookieDefaults;
1157
1363
 
1364
+ // src/middleware/bot-detection.ts
1365
+ var BOT_PATTERNS = [
1366
+ // --- SEARCH ENGINES (specific variants before generic) ---
1367
+ { pattern: /Googlebot-Image/i, name: "Googlebot-Image", category: "SEARCH_ENGINE" },
1368
+ { pattern: /Googlebot-Video/i, name: "Googlebot-Video", category: "SEARCH_ENGINE" },
1369
+ { pattern: /Googlebot-News/i, name: "Googlebot-News", category: "SEARCH_ENGINE" },
1370
+ { pattern: /Googlebot/i, name: "Googlebot", category: "SEARCH_ENGINE" },
1371
+ { pattern: /AdsBot-Google/i, name: "AdsBot-Google", category: "SEARCH_ENGINE" },
1372
+ { pattern: /Mediapartners-Google/i, name: "Mediapartners-Google", category: "SEARCH_ENGINE" },
1373
+ { pattern: /Bingbot/i, name: "Bingbot", category: "SEARCH_ENGINE" },
1374
+ { pattern: /msnbot/i, name: "msnbot", category: "SEARCH_ENGINE" },
1375
+ { pattern: /Slurp/i, name: "Yahoo Slurp", category: "SEARCH_ENGINE" },
1376
+ { pattern: /DuckDuckBot/i, name: "DuckDuckBot", category: "SEARCH_ENGINE" },
1377
+ { pattern: /Baiduspider/i, name: "Baiduspider", category: "SEARCH_ENGINE" },
1378
+ { pattern: /YandexBot/i, name: "YandexBot", category: "SEARCH_ENGINE" },
1379
+ { pattern: /YandexImages/i, name: "YandexImages", category: "SEARCH_ENGINE" },
1380
+ { pattern: /Sogou/i, name: "Sogou", category: "SEARCH_ENGINE" },
1381
+ { pattern: /Exabot/i, name: "Exabot", category: "SEARCH_ENGINE" },
1382
+ { pattern: /ia_archiver/i, name: "Alexa", category: "SEARCH_ENGINE" },
1383
+ { pattern: /Applebot/i, name: "Applebot", category: "SEARCH_ENGINE" },
1384
+ { pattern: /Qwantify/i, name: "Qwantify", category: "SEARCH_ENGINE" },
1385
+ { pattern: /PetalBot/i, name: "PetalBot", category: "SEARCH_ENGINE" },
1386
+ { pattern: /SeznamBot/i, name: "SeznamBot", category: "SEARCH_ENGINE" },
1387
+ // --- SOCIAL ---
1388
+ { pattern: /Twitterbot/i, name: "Twitterbot", category: "SOCIAL" },
1389
+ { pattern: /facebookexternalhit/i, name: "Facebook", category: "SOCIAL" },
1390
+ { pattern: /Facebot/i, name: "Facebot", category: "SOCIAL" },
1391
+ { pattern: /LinkedInBot/i, name: "LinkedInBot", category: "SOCIAL" },
1392
+ { pattern: /Pinterest/i, name: "Pinterest", category: "SOCIAL" },
1393
+ { pattern: /Slackbot/i, name: "Slackbot", category: "SOCIAL" },
1394
+ { pattern: /TelegramBot/i, name: "TelegramBot", category: "SOCIAL" },
1395
+ { pattern: /WhatsApp/i, name: "WhatsApp", category: "SOCIAL" },
1396
+ { pattern: /Discordbot/i, name: "Discordbot", category: "SOCIAL" },
1397
+ { pattern: /Redditbot/i, name: "Redditbot", category: "SOCIAL" },
1398
+ { pattern: /Embedly/i, name: "Embedly", category: "SOCIAL" },
1399
+ { pattern: /Quora Link Preview/i, name: "Quora", category: "SOCIAL" },
1400
+ { pattern: /Mastodon/i, name: "Mastodon", category: "SOCIAL" },
1401
+ // --- MONITORING ---
1402
+ { pattern: /UptimeRobot/i, name: "UptimeRobot", category: "MONITORING" },
1403
+ { pattern: /Pingdom/i, name: "Pingdom", category: "MONITORING" },
1404
+ { pattern: /Site24x7/i, name: "Site24x7", category: "MONITORING" },
1405
+ { pattern: /StatusCake/i, name: "StatusCake", category: "MONITORING" },
1406
+ { pattern: /Datadog/i, name: "Datadog", category: "MONITORING" },
1407
+ { pattern: /NewRelicPinger/i, name: "New Relic", category: "MONITORING" },
1408
+ { pattern: /Better Uptime Bot/i, name: "Better Uptime", category: "MONITORING" },
1409
+ { pattern: /GTmetrix/i, name: "GTmetrix", category: "MONITORING" },
1410
+ { pattern: /PageSpeed/i, name: "PageSpeed Insights", category: "MONITORING" },
1411
+ // --- AI CRAWLERS ---
1412
+ { pattern: /GPTBot/i, name: "GPTBot", category: "AI_CRAWLER" },
1413
+ { pattern: /ChatGPT-User/i, name: "ChatGPT-User", category: "AI_CRAWLER" },
1414
+ { pattern: /Claude-Web/i, name: "Claude-Web", category: "AI_CRAWLER" },
1415
+ { pattern: /ClaudeBot/i, name: "ClaudeBot", category: "AI_CRAWLER" },
1416
+ { pattern: /anthropic-ai/i, name: "Anthropic", category: "AI_CRAWLER" },
1417
+ { pattern: /Bytespider/i, name: "Bytespider", category: "AI_CRAWLER" },
1418
+ { pattern: /CCBot/i, name: "CCBot", category: "AI_CRAWLER" },
1419
+ { pattern: /cohere-ai/i, name: "Cohere", category: "AI_CRAWLER" },
1420
+ { pattern: /PerplexityBot/i, name: "PerplexityBot", category: "AI_CRAWLER" },
1421
+ { pattern: /YouBot/i, name: "YouBot", category: "AI_CRAWLER" },
1422
+ { pattern: /Google-Extended/i, name: "Google-Extended", category: "AI_CRAWLER" },
1423
+ { pattern: /Diffbot/i, name: "Diffbot", category: "AI_CRAWLER" },
1424
+ { pattern: /Amazonbot/i, name: "Amazonbot", category: "AI_CRAWLER" },
1425
+ { pattern: /meta-externalagent/i, name: "Meta AI", category: "AI_CRAWLER" },
1426
+ // --- AUTOMATED TOOLS (headless browsers, testing frameworks) ---
1427
+ { pattern: /HeadlessChrome/i, name: "Headless Chrome", category: "AUTOMATED" },
1428
+ { pattern: /PhantomJS/i, name: "PhantomJS", category: "AUTOMATED" },
1429
+ { pattern: /Selenium/i, name: "Selenium", category: "AUTOMATED" },
1430
+ { pattern: /Puppeteer/i, name: "Puppeteer", category: "AUTOMATED" },
1431
+ { pattern: /Playwright/i, name: "Playwright", category: "AUTOMATED" },
1432
+ { pattern: /Cypress/i, name: "Cypress", category: "AUTOMATED" },
1433
+ { pattern: /webdriver/i, name: "WebDriver", category: "AUTOMATED" },
1434
+ { pattern: /MSIE 6\.0/i, name: "Fake IE6", category: "AUTOMATED" },
1435
+ // --- SCRAPERS / CLI TOOLS ---
1436
+ { pattern: /^curl\//i, name: "curl", category: "SCRAPER" },
1437
+ { pattern: /^wget\//i, name: "wget", category: "SCRAPER" },
1438
+ { pattern: /^python-requests\//i, name: "python-requests", category: "SCRAPER" },
1439
+ { pattern: /^python-httpx\//i, name: "python-httpx", category: "SCRAPER" },
1440
+ { pattern: /^Python-urllib/i, name: "Python-urllib", category: "SCRAPER" },
1441
+ { pattern: /^aiohttp\//i, name: "aiohttp", category: "SCRAPER" },
1442
+ { pattern: /^Go-http-client/i, name: "Go-http-client", category: "SCRAPER" },
1443
+ { pattern: /^Java\//i, name: "Java HttpClient", category: "SCRAPER" },
1444
+ { pattern: /^Apache-HttpClient/i, name: "Apache HttpClient", category: "SCRAPER" },
1445
+ { pattern: /^okhttp\//i, name: "OkHttp", category: "SCRAPER" },
1446
+ { pattern: /^node-fetch\//i, name: "node-fetch", category: "SCRAPER" },
1447
+ { pattern: /^axios\//i, name: "axios", category: "SCRAPER" },
1448
+ { pattern: /^got\//i, name: "got", category: "SCRAPER" },
1449
+ { pattern: /^libwww-perl/i, name: "libwww-perl", category: "SCRAPER" },
1450
+ { pattern: /^Ruby/i, name: "Ruby", category: "SCRAPER" },
1451
+ { pattern: /^PHP\//i, name: "PHP", category: "SCRAPER" },
1452
+ { pattern: /Scrapy/i, name: "Scrapy", category: "SCRAPER" },
1453
+ { pattern: /^Postman/i, name: "Postman", category: "SCRAPER" },
1454
+ { pattern: /^Insomnia/i, name: "Insomnia", category: "SCRAPER" },
1455
+ { pattern: /^HTTPie\//i, name: "HTTPie", category: "SCRAPER" }
1456
+ ];
1457
+ function detectBehavioralSignals(req) {
1458
+ const signals = [];
1459
+ const headers = req.headers;
1460
+ if (!headers["user-agent"]) {
1461
+ signals.push("missing_user_agent");
1462
+ }
1463
+ if (!headers["accept"]) {
1464
+ signals.push("missing_accept");
1465
+ }
1466
+ if (!headers["accept-language"]) {
1467
+ signals.push("missing_accept_language");
1468
+ }
1469
+ if (!headers["accept-encoding"]) {
1470
+ signals.push("missing_accept_encoding");
1471
+ }
1472
+ if (headers["connection"] === "close") {
1473
+ signals.push("connection_close");
1474
+ }
1475
+ return signals;
1476
+ }
1477
+ function detectBot(req) {
1478
+ const rawUa = req.headers["user-agent"] ?? "";
1479
+ const ua = rawUa.length > 2048 ? rawUa.slice(0, 2048) : rawUa;
1480
+ const signals = detectBehavioralSignals(req);
1481
+ if (!ua) {
1482
+ return {
1483
+ isBot: true,
1484
+ category: "UNKNOWN",
1485
+ name: null,
1486
+ confidence: 0.8,
1487
+ signals
1488
+ };
1489
+ }
1490
+ for (const bot of BOT_PATTERNS) {
1491
+ if (bot.pattern.test(ua)) {
1492
+ return {
1493
+ isBot: true,
1494
+ category: bot.category,
1495
+ name: bot.name,
1496
+ confidence: 0.95,
1497
+ signals
1498
+ };
1499
+ }
1500
+ }
1501
+ const behaviorScore = signals.length;
1502
+ if (behaviorScore >= 3) {
1503
+ return {
1504
+ isBot: true,
1505
+ category: "UNKNOWN",
1506
+ name: null,
1507
+ confidence: Math.min(1, 0.6 + behaviorScore * 0.1),
1508
+ signals
1509
+ };
1510
+ }
1511
+ return {
1512
+ isBot: false,
1513
+ category: "HUMAN",
1514
+ name: null,
1515
+ confidence: Math.max(0, 1 - behaviorScore * 0.15),
1516
+ signals
1517
+ };
1518
+ }
1519
+ function botProtection(options = {}) {
1520
+ const {
1521
+ allow = ["SEARCH_ENGINE", "SOCIAL", "MONITORING"],
1522
+ deny = ["AUTOMATED"],
1523
+ defaultAction = "allow",
1524
+ statusCode = 403,
1525
+ message = "Access denied.",
1526
+ onDetected
1527
+ } = options;
1528
+ const allowSet = new Set(allow);
1529
+ const denySet = new Set(deny);
1530
+ return (req, res, next) => {
1531
+ const result = detectBot(req);
1532
+ req.botDetection = result;
1533
+ if (!result.isBot) {
1534
+ return next();
1535
+ }
1536
+ if (allowSet.has(result.category)) {
1537
+ return next();
1538
+ }
1539
+ if (denySet.has(result.category)) {
1540
+ if (onDetected) {
1541
+ return onDetected(req, res, result);
1542
+ }
1543
+ res.status(statusCode).json({ error: message });
1544
+ return;
1545
+ }
1546
+ if (defaultAction === "deny") {
1547
+ if (onDetected) {
1548
+ return onDetected(req, res, result);
1549
+ }
1550
+ res.status(statusCode).json({ error: message });
1551
+ return;
1552
+ }
1553
+ next();
1554
+ };
1555
+ }
1556
+ var DEFAULTS = {
1557
+ cookieName: "_csrf",
1558
+ headerName: "x-csrf-token",
1559
+ fieldName: "_csrf",
1560
+ tokenLength: 32,
1561
+ protectedMethods: ["POST", "PUT", "PATCH", "DELETE"]
1562
+ };
1563
+ function generateCsrfToken(length = 32) {
1564
+ return crypto.randomBytes(length).toString("hex");
1565
+ }
1566
+ function validateCsrfToken(cookieToken, requestToken) {
1567
+ if (!cookieToken || !requestToken) return false;
1568
+ if (cookieToken.length !== requestToken.length) return false;
1569
+ let result = 0;
1570
+ for (let i = 0; i < cookieToken.length; i++) {
1571
+ result |= cookieToken.charCodeAt(i) ^ requestToken.charCodeAt(i);
1572
+ }
1573
+ return result === 0;
1574
+ }
1575
+ function getRequestToken(req, headerName, fieldName) {
1576
+ const headerToken = req.headers[headerName.toLowerCase()];
1577
+ if (typeof headerToken === "string" && headerToken) return headerToken;
1578
+ if (req.body && typeof req.body === "object" && fieldName in req.body) {
1579
+ const bodyToken = req.body[fieldName];
1580
+ if (typeof bodyToken === "string" && bodyToken) return bodyToken;
1581
+ }
1582
+ if (req.query && fieldName in req.query) {
1583
+ const queryToken = req.query[fieldName];
1584
+ if (typeof queryToken === "string" && queryToken) return queryToken;
1585
+ }
1586
+ return void 0;
1587
+ }
1588
+ function csrfProtection(options = {}) {
1589
+ const cookieName = options.cookieName ?? DEFAULTS.cookieName;
1590
+ const headerName = options.headerName ?? DEFAULTS.headerName;
1591
+ const fieldName = options.fieldName ?? DEFAULTS.fieldName;
1592
+ const tokenLength = options.tokenLength ?? DEFAULTS.tokenLength;
1593
+ const protectedMethods = options.protectedMethods ?? [...DEFAULTS.protectedMethods];
1594
+ const excludePaths = options.excludePaths ?? [];
1595
+ const isProduction = process.env.NODE_ENV === "production";
1596
+ const cookieOpts = {
1597
+ path: options.cookie?.path ?? "/",
1598
+ httpOnly: options.cookie?.httpOnly ?? false,
1599
+ // Must be readable by client JS
1600
+ secure: options.cookie?.secure ?? isProduction,
1601
+ sameSite: options.cookie?.sameSite ?? "Lax",
1602
+ domain: options.cookie?.domain
1603
+ };
1604
+ const defaultOnError = (_req, res, _next) => {
1605
+ res.status(403).json({
1606
+ error: "CSRF token validation failed",
1607
+ message: "Invalid or missing CSRF token. Include the token from the cookie in the X-CSRF-Token header."
1608
+ });
1609
+ };
1610
+ const onError = options.onError ?? defaultOnError;
1611
+ const protectedSet = new Set(protectedMethods.map((m) => m.toUpperCase()));
1612
+ return (req, res, next) => {
1613
+ const method = req.method.toUpperCase();
1614
+ const requestPath = req.path || req.url;
1615
+ if (excludePaths.some((p) => requestPath === p || requestPath.startsWith(p + "/"))) {
1616
+ return next();
1617
+ }
1618
+ req.csrfToken = () => {
1619
+ const existing = getCookieValue(req, cookieName);
1620
+ if (existing) return existing;
1621
+ const token = generateCsrfToken(tokenLength);
1622
+ setCsrfCookie(res, cookieName, token, cookieOpts);
1623
+ return token;
1624
+ };
1625
+ if (!protectedSet.has(method)) {
1626
+ const existing = getCookieValue(req, cookieName);
1627
+ if (!existing) {
1628
+ const token = generateCsrfToken(tokenLength);
1629
+ setCsrfCookie(res, cookieName, token, cookieOpts);
1630
+ }
1631
+ return next();
1632
+ }
1633
+ const cookieToken = getCookieValue(req, cookieName);
1634
+ if (!cookieToken) {
1635
+ return onError(req, res, next);
1636
+ }
1637
+ const requestToken = getRequestToken(req, headerName, fieldName);
1638
+ if (!requestToken) {
1639
+ return onError(req, res, next);
1640
+ }
1641
+ if (!validateCsrfToken(cookieToken, requestToken)) {
1642
+ return onError(req, res, next);
1643
+ }
1644
+ next();
1645
+ };
1646
+ }
1647
+ function getCookieValue(req, name) {
1648
+ if (req.cookies && typeof req.cookies === "object" && name in req.cookies) {
1649
+ return req.cookies[name];
1650
+ }
1651
+ const cookieHeader = req.headers.cookie;
1652
+ if (!cookieHeader) return void 0;
1653
+ const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${escapeRegex(name)}=([^;]*)`));
1654
+ return match ? decodeURIComponent(match[1]) : void 0;
1655
+ }
1656
+ function setCsrfCookie(res, name, token, opts) {
1657
+ const parts = [`${name}=${token}`];
1658
+ parts.push(`Path=${opts.path}`);
1659
+ if (opts.httpOnly) parts.push("HttpOnly");
1660
+ if (opts.secure) parts.push("Secure");
1661
+ parts.push(`SameSite=${opts.sameSite}`);
1662
+ if (opts.domain) parts.push(`Domain=${opts.domain}`);
1663
+ res.setHeader("Set-Cookie", parts.join("; "));
1664
+ }
1665
+ function escapeRegex(str) {
1666
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1667
+ }
1668
+ var createCsrf = csrfProtection;
1669
+
1158
1670
  exports.arcis = arcis;
1159
1671
  exports.arcisFunction = arcisWithMethods;
1672
+ exports.botProtection = botProtection;
1160
1673
  exports.createCors = createCors;
1674
+ exports.createCsrf = createCsrf;
1161
1675
  exports.createErrorHandler = createErrorHandler;
1162
1676
  exports.createHeaders = createHeaders;
1163
1677
  exports.createRateLimiter = createRateLimiter;
1164
1678
  exports.createSecureCookies = createSecureCookies;
1679
+ exports.createSlidingWindowLimiter = createSlidingWindowLimiter;
1680
+ exports.createTokenBucketLimiter = createTokenBucketLimiter;
1681
+ exports.csrfProtection = csrfProtection;
1165
1682
  exports.default = main_default;
1683
+ exports.detectBot = detectBot;
1166
1684
  exports.enforceSecureCookie = enforceSecureCookie;
1167
1685
  exports.errorHandler = errorHandler;
1686
+ exports.generateCsrfToken = generateCsrfToken;
1168
1687
  exports.rateLimit = rateLimit;
1169
1688
  exports.safeCors = safeCors;
1170
1689
  exports.secureCookieDefaults = secureCookieDefaults;
1171
1690
  exports.securityHeaders = securityHeaders;
1691
+ exports.validateCsrfToken = validateCsrfToken;
1172
1692
  //# sourceMappingURL=index.js.map
1173
1693
  //# sourceMappingURL=index.js.map