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