@arcis/node 1.3.0 → 1.4.2

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 (139) hide show
  1. package/README.md +1 -1
  2. package/dist/core/{index.d.mts → constants.d.ts} +21 -70
  3. package/dist/core/constants.d.ts.map +1 -0
  4. package/dist/core/errors.d.ts +53 -0
  5. package/dist/core/errors.d.ts.map +1 -0
  6. package/dist/core/index.d.ts +6 -168
  7. package/dist/core/index.d.ts.map +1 -0
  8. package/dist/core/index.js +11 -3
  9. package/dist/core/index.js.map +1 -1
  10. package/dist/core/index.mjs +11 -3
  11. package/dist/core/index.mjs.map +1 -1
  12. package/dist/{types-BOkx5YJc.d.mts → core/types.d.ts} +27 -30
  13. package/dist/core/types.d.ts.map +1 -0
  14. package/dist/index.d.ts +71 -166
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +182 -48
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +182 -50
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/logging/index.d.ts +4 -36
  21. package/dist/logging/index.d.ts.map +1 -0
  22. package/dist/logging/index.js.map +1 -1
  23. package/dist/logging/index.mjs.map +1 -1
  24. package/dist/logging/{index.d.mts → redactor.d.ts} +5 -9
  25. package/dist/logging/redactor.d.ts.map +1 -0
  26. package/dist/middleware/bot-detection.d.ts +86 -0
  27. package/dist/middleware/bot-detection.d.ts.map +1 -0
  28. package/dist/middleware/cookies.d.ts +48 -0
  29. package/dist/middleware/cookies.d.ts.map +1 -0
  30. package/dist/middleware/cors.d.ts +65 -0
  31. package/dist/middleware/cors.d.ts.map +1 -0
  32. package/dist/middleware/csrf.d.ts +109 -0
  33. package/dist/middleware/csrf.d.ts.map +1 -0
  34. package/dist/middleware/error-handler.d.ts +43 -0
  35. package/dist/middleware/error-handler.d.ts.map +1 -0
  36. package/dist/middleware/headers.d.ts +29 -0
  37. package/dist/middleware/headers.d.ts.map +1 -0
  38. package/dist/middleware/hpp.d.ts +56 -0
  39. package/dist/middleware/hpp.d.ts.map +1 -0
  40. package/dist/middleware/index.d.ts +16 -3
  41. package/dist/middleware/index.d.ts.map +1 -0
  42. package/dist/middleware/index.js +68 -31
  43. package/dist/middleware/index.js.map +1 -1
  44. package/dist/middleware/index.mjs +69 -32
  45. package/dist/middleware/index.mjs.map +1 -1
  46. package/dist/middleware/main.d.ts +40 -0
  47. package/dist/middleware/main.d.ts.map +1 -0
  48. package/dist/middleware/rate-limit-sliding.d.ts +46 -0
  49. package/dist/middleware/rate-limit-sliding.d.ts.map +1 -0
  50. package/dist/middleware/rate-limit-token.d.ts +51 -0
  51. package/dist/middleware/rate-limit-token.d.ts.map +1 -0
  52. package/dist/middleware/rate-limit.d.ts +34 -0
  53. package/dist/middleware/rate-limit.d.ts.map +1 -0
  54. package/dist/sanitizers/command.d.ts +28 -0
  55. package/dist/sanitizers/command.d.ts.map +1 -0
  56. package/dist/sanitizers/encode.d.ts +46 -0
  57. package/dist/sanitizers/encode.d.ts.map +1 -0
  58. package/dist/sanitizers/headers.d.ts +46 -0
  59. package/dist/sanitizers/headers.d.ts.map +1 -0
  60. package/dist/sanitizers/index.d.ts +18 -22
  61. package/dist/sanitizers/index.d.ts.map +1 -0
  62. package/dist/sanitizers/index.js +90 -32
  63. package/dist/sanitizers/index.js.map +1 -1
  64. package/dist/sanitizers/index.mjs +88 -33
  65. package/dist/sanitizers/index.mjs.map +1 -1
  66. package/dist/sanitizers/jsonp.d.ts +34 -0
  67. package/dist/sanitizers/jsonp.d.ts.map +1 -0
  68. package/dist/sanitizers/ldap.d.ts +42 -0
  69. package/dist/sanitizers/ldap.d.ts.map +1 -0
  70. package/dist/sanitizers/nosql.d.ts +31 -0
  71. package/dist/sanitizers/nosql.d.ts.map +1 -0
  72. package/dist/sanitizers/path.d.ts +28 -0
  73. package/dist/sanitizers/path.d.ts.map +1 -0
  74. package/dist/sanitizers/pii.d.ts +80 -0
  75. package/dist/sanitizers/pii.d.ts.map +1 -0
  76. package/dist/sanitizers/prototype.d.ts +34 -0
  77. package/dist/sanitizers/prototype.d.ts.map +1 -0
  78. package/dist/sanitizers/sanitize.d.ts +51 -0
  79. package/dist/sanitizers/sanitize.d.ts.map +1 -0
  80. package/dist/sanitizers/sql.d.ts +28 -0
  81. package/dist/sanitizers/sql.d.ts.map +1 -0
  82. package/dist/sanitizers/ssti.d.ts +20 -0
  83. package/dist/sanitizers/ssti.d.ts.map +1 -0
  84. package/dist/sanitizers/utils.d.ts +19 -0
  85. package/dist/sanitizers/utils.d.ts.map +1 -0
  86. package/dist/sanitizers/xss.d.ts +35 -0
  87. package/dist/sanitizers/xss.d.ts.map +1 -0
  88. package/dist/sanitizers/xxe.d.ts +20 -0
  89. package/dist/sanitizers/xxe.d.ts.map +1 -0
  90. package/dist/stores/index.d.ts +6 -104
  91. package/dist/stores/index.d.ts.map +1 -0
  92. package/dist/stores/index.js +21 -1
  93. package/dist/stores/index.js.map +1 -1
  94. package/dist/stores/index.mjs +21 -1
  95. package/dist/stores/index.mjs.map +1 -1
  96. package/dist/stores/memory.d.ts +29 -0
  97. package/dist/stores/memory.d.ts.map +1 -0
  98. package/dist/stores/{index.d.mts → redis.d.ts} +6 -45
  99. package/dist/stores/redis.d.ts.map +1 -0
  100. package/dist/utils/duration.d.ts +34 -0
  101. package/dist/utils/duration.d.ts.map +1 -0
  102. package/dist/utils/fingerprint.d.ts +64 -0
  103. package/dist/utils/fingerprint.d.ts.map +1 -0
  104. package/dist/utils/index.d.ts +10 -0
  105. package/dist/utils/index.d.ts.map +1 -0
  106. package/dist/utils/index.js +188 -0
  107. package/dist/utils/index.js.map +1 -0
  108. package/dist/utils/index.mjs +182 -0
  109. package/dist/utils/index.mjs.map +1 -0
  110. package/dist/utils/ip.d.ts +70 -0
  111. package/dist/utils/ip.d.ts.map +1 -0
  112. package/dist/validation/email.d.ts +82 -0
  113. package/dist/validation/email.d.ts.map +1 -0
  114. package/dist/validation/file.d.ts +90 -0
  115. package/dist/validation/file.d.ts.map +1 -0
  116. package/dist/validation/index.d.ts +10 -3
  117. package/dist/validation/index.d.ts.map +1 -0
  118. package/dist/validation/index.js +38 -21
  119. package/dist/validation/index.js.map +1 -1
  120. package/dist/validation/index.mjs +38 -21
  121. package/dist/validation/index.mjs.map +1 -1
  122. package/dist/validation/redirect.d.ts +64 -0
  123. package/dist/validation/redirect.d.ts.map +1 -0
  124. package/dist/validation/schema.d.ts +36 -0
  125. package/dist/validation/schema.d.ts.map +1 -0
  126. package/dist/validation/url.d.ts +65 -0
  127. package/dist/validation/url.d.ts.map +1 -0
  128. package/package.json +8 -6
  129. package/dist/encode-CrQCGlBq.d.mts +0 -484
  130. package/dist/encode-jl9sOwmA.d.ts +0 -484
  131. package/dist/index-BAhgn9V2.d.ts +0 -532
  132. package/dist/index-BGNKspqH.d.ts +0 -340
  133. package/dist/index-Cd02z-0j.d.mts +0 -340
  134. package/dist/index-DgJtWMSj.d.mts +0 -532
  135. package/dist/index.d.mts +0 -175
  136. package/dist/middleware/index.d.mts +0 -3
  137. package/dist/sanitizers/index.d.mts +0 -24
  138. package/dist/types-BOkx5YJc.d.ts +0 -279
  139. package/dist/validation/index.d.mts +0 -3
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { promises } from 'dns';
2
- import { randomBytes, createHash } from 'crypto';
2
+ import { randomBytes, timingSafeEqual, createHash } from 'crypto';
3
3
 
4
4
  // src/core/constants.ts
5
5
  var INPUT = {
@@ -66,7 +66,15 @@ var XSS_PATTERNS = [
66
66
  /** URL-encoded script tags */
67
67
  /%3Cscript/gi,
68
68
  /** SVG with onload */
69
- /<svg[^>]*onload/gi
69
+ /<svg[^>]*onload/gi,
70
+ /** form tags — phishing/credential harvesting via action= redirection */
71
+ /<form[\s>]/gi,
72
+ /** meta tags — http-equiv refresh redirects or CSP bypass */
73
+ /<meta[\s>]/gi,
74
+ /** base href hijacking — redirects all relative URLs to attacker domain */
75
+ /<base[\s>]/gi,
76
+ /** link tag injection — stylesheet or preload CSRF attacks */
77
+ /<link[\s>]/gi
70
78
  ];
71
79
  var XSS_REMOVE_PATTERNS = [
72
80
  /** Full script blocks (content + tags) */
@@ -93,7 +101,15 @@ var XSS_REMOVE_PATTERNS = [
93
101
  /javascript\s*:/gi,
94
102
  /vbscript\s*:/gi,
95
103
  /** data: URIs with HTML/script content */
96
- /data\s*:\s*text\/html[^>\s]*/gi
104
+ /data\s*:\s*text\/html[^>\s]*/gi,
105
+ /** form tag injection — phishing via action= redirection */
106
+ /<form[\s>][^>]*/gi,
107
+ /** meta tag injection — http-equiv refresh or CSP bypass */
108
+ /<meta[\s>][^>]*/gi,
109
+ /** base href hijacking */
110
+ /<base[\s>][^>]*/gi,
111
+ /** link tag injection — stylesheet or preload attacks */
112
+ /<link[\s>][^>]*/gi
97
113
  ];
98
114
  var SQL_PATTERNS = [
99
115
  /** SQL keywords */
@@ -157,8 +173,8 @@ var COMMAND_PATTERNS = [
157
173
  /[;&|`]/g,
158
174
  /** Command substitution: $( ... ) — matched as a pair to reduce false positives */
159
175
  /\$\(/g,
160
- /** URL-encoded newline/carriage-return injection (%0a, %0d) */
161
- /%0[ad]/gi
176
+ /** URL-encoded control characters (%00-%0F): null, tab, vtab, formfeed, LF, CR */
177
+ /%0[0-9a-f]/gi
162
178
  ];
163
179
  var DANGEROUS_PROTO_KEYS = /* @__PURE__ */ new Set([
164
180
  "__proto__",
@@ -449,7 +465,24 @@ function createRateLimiter(options = {}) {
449
465
  }
450
466
  next();
451
467
  } catch (error) {
452
- console.error("[arcis] Rate limiter error:", error);
468
+ console.error("[arcis] Rate limiter store error, using in-memory fallback:", error);
469
+ try {
470
+ const key = keyGenerator(req);
471
+ const now = Date.now();
472
+ if (!inMemoryStore[key] || inMemoryStore[key].resetTime < now) {
473
+ inMemoryStore[key] = { count: 1, resetTime: now + windowMs };
474
+ } else {
475
+ inMemoryStore[key].count++;
476
+ }
477
+ const count = inMemoryStore[key].count;
478
+ if (count > max) {
479
+ const resetSeconds = Math.ceil((inMemoryStore[key].resetTime - now) / 1e3);
480
+ res.setHeader("Retry-After", resetSeconds.toString());
481
+ res.status(statusCode).json({ error: message, retryAfter: resetSeconds });
482
+ return;
483
+ }
484
+ } catch {
485
+ }
453
486
  next();
454
487
  }
455
488
  };
@@ -693,26 +726,31 @@ function sanitizePath(input, collectThreats = false) {
693
726
  const threats = [];
694
727
  let value = input;
695
728
  let wasSanitized = false;
696
- for (const pattern of PATH_PATTERNS) {
697
- pattern.lastIndex = 0;
698
- if (pattern.test(value)) {
729
+ value = value.normalize("NFKC");
730
+ let prev;
731
+ do {
732
+ prev = value;
733
+ for (const pattern of PATH_PATTERNS) {
699
734
  pattern.lastIndex = 0;
700
- if (collectThreats) {
701
- const matches = value.match(pattern);
702
- if (matches) {
703
- for (const match of matches) {
704
- threats.push({
705
- type: "path_traversal",
706
- pattern: pattern.source,
707
- original: match
708
- });
735
+ if (pattern.test(value)) {
736
+ pattern.lastIndex = 0;
737
+ if (collectThreats) {
738
+ const matches = value.match(pattern);
739
+ if (matches) {
740
+ for (const match of matches) {
741
+ threats.push({
742
+ type: "path_traversal",
743
+ pattern: pattern.source,
744
+ original: match
745
+ });
746
+ }
709
747
  }
710
748
  }
749
+ value = value.replace(pattern, "");
750
+ wasSanitized = true;
711
751
  }
712
- value = value.replace(pattern, "");
713
- wasSanitized = true;
714
752
  }
715
- }
753
+ } while (value !== prev);
716
754
  if (collectThreats) {
717
755
  return { value, wasSanitized, threats };
718
756
  }
@@ -720,9 +758,10 @@ function sanitizePath(input, collectThreats = false) {
720
758
  }
721
759
  function detectPathTraversal(input) {
722
760
  if (typeof input !== "string") return false;
761
+ const normalized = input.normalize("NFKC");
723
762
  for (const pattern of PATH_PATTERNS) {
724
763
  pattern.lastIndex = 0;
725
- if (pattern.test(input)) {
764
+ if (pattern.test(normalized)) {
726
765
  return true;
727
766
  }
728
767
  }
@@ -780,7 +819,7 @@ function sanitizeString(value, options = {}) {
780
819
  if (value.length > maxSize) {
781
820
  throw new InputTooLargeError(maxSize, value.length);
782
821
  }
783
- const reject = options.mode !== "sanitize";
822
+ const reject = options.mode === "reject";
784
823
  let result = value;
785
824
  if (options.sql !== false) {
786
825
  if (reject) {
@@ -929,10 +968,22 @@ var SSTI_DETECT_PATTERNS = [
929
968
  /\{\{\s*(?:self|request|lipsum|cycler|joiner|namespace|range)\b/gi
930
969
  ];
931
970
  var SSTI_REMOVE_PATTERNS = [
971
+ /** Jinja2 / Twig: {{ ... }} — always strip (not valid in any JS context) */
932
972
  /\{\{.*?\}\}/g,
933
- /\$\{.*?\}/g,
973
+ /**
974
+ * Freemarker / Spring EL: ${...} — only strip when the expression contains
975
+ * operators (?!*+-/), method calls (), or known-dangerous prefixes.
976
+ * Bare ${name} and ${user.name} are left intact (JS template literal syntax).
977
+ */
978
+ /\$\{[^}]*[?!()*+\-/][^}]*\}/g,
979
+ /** ERB / EJS: <%= ... %> */
934
980
  /<%[=\-]?.*?%>/gs,
935
- /#\{.*?\}/g,
981
+ /**
982
+ * Pug / Jade: #{...} — same narrowing as ${ above.
983
+ * #{name} output expressions are left intact.
984
+ */
985
+ /#\{[^}]*[?!()*+\-/][^}]*\}/g,
986
+ /** Python dunder sandbox escape — always strip */
936
987
  /__(?:class|mro|subclasses|globals|builtins|import)__/gi
937
988
  ];
938
989
  function sanitizeSsti(input, collectThreats = false) {
@@ -1296,16 +1347,18 @@ function encodeForAttribute(value) {
1296
1347
  function encodeForJs(value) {
1297
1348
  if (!value) return "";
1298
1349
  let result = "";
1299
- for (let i = 0; i < value.length; i++) {
1300
- const ch = value.charCodeAt(i);
1301
- if (ch >= 48 && ch <= 57 || // 0-9
1302
- ch >= 65 && ch <= 90 || // A-Z
1303
- ch >= 97 && ch <= 122) {
1304
- result += value[i];
1305
- } else if (ch < 256) {
1306
- result += `\\x${ch.toString(16).toUpperCase().padStart(2, "0")}`;
1350
+ for (const char of value) {
1351
+ const cp = char.codePointAt(0);
1352
+ if (cp >= 48 && cp <= 57 || // 0-9
1353
+ cp >= 65 && cp <= 90 || // A-Z
1354
+ cp >= 97 && cp <= 122) {
1355
+ result += char;
1356
+ } else if (cp < 256) {
1357
+ result += `\\x${cp.toString(16).toUpperCase().padStart(2, "0")}`;
1358
+ } else if (cp <= 65535) {
1359
+ result += `\\u${cp.toString(16).toUpperCase().padStart(4, "0")}`;
1307
1360
  } else {
1308
- result += `\\u${ch.toString(16).toUpperCase().padStart(4, "0")}`;
1361
+ result += `\\u{${cp.toString(16).toUpperCase()}}`;
1309
1362
  }
1310
1363
  }
1311
1364
  return result;
@@ -1758,8 +1811,12 @@ function checkPrivateIp(hostname) {
1758
1811
  if (hostname === "metadata.google.internal" || hostname === "metadata.internal" || hostname === "metadata.azure.internal") {
1759
1812
  return "cloud metadata endpoint";
1760
1813
  }
1761
- const ipv6 = hostname.replace(/^\[|\]$/g, "");
1762
- if (ipv6 === "::1" || ipv6 === "::" || ipv6.startsWith("fc") || ipv6.startsWith("fd") || ipv6.startsWith("fe80")) {
1814
+ let ipv6 = hostname.replace(/^\[|\]$/g, "");
1815
+ const zoneIdx = ipv6.indexOf("%");
1816
+ if (zoneIdx !== -1) {
1817
+ ipv6 = ipv6.slice(0, zoneIdx);
1818
+ }
1819
+ if (ipv6 === "::1" || ipv6 === "::" || /^fc[0-9a-f]{2}:/i.test(ipv6) || /^fd[0-9a-f]{2}:/i.test(ipv6) || /^fe80:/i.test(ipv6) || /^ff[0-9a-f]{2}:/i.test(ipv6)) {
1763
1820
  return "private IPv6 address";
1764
1821
  }
1765
1822
  const mappedDotted = ipv6.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
@@ -2818,11 +2875,9 @@ function generateCsrfToken(length = 32) {
2818
2875
  function validateCsrfToken(cookieToken, requestToken) {
2819
2876
  if (!cookieToken || !requestToken) return false;
2820
2877
  if (cookieToken.length !== requestToken.length) return false;
2821
- let result = 0;
2822
- for (let i = 0; i < cookieToken.length; i++) {
2823
- result |= cookieToken.charCodeAt(i) ^ requestToken.charCodeAt(i);
2824
- }
2825
- return result === 0;
2878
+ const a = Buffer.from(cookieToken);
2879
+ const b = Buffer.from(requestToken);
2880
+ return timingSafeEqual(a, b);
2826
2881
  }
2827
2882
  function getRequestToken(req, headerName, fieldName) {
2828
2883
  const headerToken = req.headers[headerName.toLowerCase()];
@@ -2831,19 +2886,17 @@ function getRequestToken(req, headerName, fieldName) {
2831
2886
  const bodyToken = req.body[fieldName];
2832
2887
  if (typeof bodyToken === "string" && bodyToken) return bodyToken;
2833
2888
  }
2834
- if (req.query && fieldName in req.query) {
2835
- const queryToken = req.query[fieldName];
2836
- if (typeof queryToken === "string" && queryToken) return queryToken;
2837
- }
2838
2889
  return void 0;
2839
2890
  }
2840
2891
  function csrfProtection(options = {}) {
2841
- const cookieName = options.cookieName ?? DEFAULTS.cookieName;
2892
+ const baseCookieName = options.cookieName ?? DEFAULTS.cookieName;
2893
+ const cookieName = options.useHostPrefix ? `__Host-${baseCookieName}` : baseCookieName;
2842
2894
  const headerName = options.headerName ?? DEFAULTS.headerName;
2843
2895
  const fieldName = options.fieldName ?? DEFAULTS.fieldName;
2844
2896
  const tokenLength = options.tokenLength ?? DEFAULTS.tokenLength;
2845
2897
  const protectedMethods = options.protectedMethods ?? [...DEFAULTS.protectedMethods];
2846
2898
  const excludePaths = options.excludePaths ?? [];
2899
+ const skipCsrf = options.skipCsrf;
2847
2900
  const isProduction = process.env.NODE_ENV === "production";
2848
2901
  const cookieOpts = {
2849
2902
  path: options.cookie?.path ?? "/",
@@ -2863,6 +2916,9 @@ function csrfProtection(options = {}) {
2863
2916
  const protectedSet = new Set(protectedMethods.map((m) => m.toUpperCase()));
2864
2917
  return (req, res, next) => {
2865
2918
  const method = req.method.toUpperCase();
2919
+ if (skipCsrf && skipCsrf(req)) {
2920
+ return next();
2921
+ }
2866
2922
  const requestPath = req.path || req.url;
2867
2923
  if (excludePaths.some((p) => requestPath === p || requestPath.startsWith(p + "/"))) {
2868
2924
  return next();
@@ -2912,13 +2968,69 @@ function setCsrfCookie(res, name, token, opts) {
2912
2968
  if (opts.secure) parts.push("Secure");
2913
2969
  parts.push(`SameSite=${opts.sameSite}`);
2914
2970
  if (opts.domain) parts.push(`Domain=${opts.domain}`);
2915
- res.setHeader("Set-Cookie", parts.join("; "));
2971
+ const newCookie = parts.join("; ");
2972
+ const existing = res.getHeader("Set-Cookie");
2973
+ if (existing === void 0) {
2974
+ res.setHeader("Set-Cookie", newCookie);
2975
+ } else if (Array.isArray(existing)) {
2976
+ res.setHeader("Set-Cookie", [...existing, newCookie]);
2977
+ } else {
2978
+ res.setHeader("Set-Cookie", [existing, newCookie]);
2979
+ }
2916
2980
  }
2917
2981
  function escapeRegex(str) {
2918
2982
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2919
2983
  }
2920
2984
  var createCsrf = csrfProtection;
2921
2985
 
2986
+ // src/middleware/hpp.ts
2987
+ function hpp(options = {}) {
2988
+ const whitelist = new Set(options.whitelist ?? []);
2989
+ const checkQuery = options.checkQuery ?? true;
2990
+ const checkBody = options.checkBody ?? true;
2991
+ return (req, _res, next) => {
2992
+ if (checkQuery && req.query && typeof req.query === "object") {
2993
+ const polluted = {};
2994
+ const clean = {};
2995
+ for (const [key, value] of Object.entries(req.query)) {
2996
+ if (Array.isArray(value)) {
2997
+ const strings = value.filter((v) => typeof v === "string");
2998
+ if (whitelist.has(key)) {
2999
+ clean[key] = strings;
3000
+ } else {
3001
+ polluted[key] = strings;
3002
+ clean[key] = strings[strings.length - 1] ?? "";
3003
+ }
3004
+ } else {
3005
+ clean[key] = value;
3006
+ }
3007
+ }
3008
+ req.queryPolluted = polluted;
3009
+ Object.defineProperty(req, "query", { value: clean, writable: true, configurable: true });
3010
+ }
3011
+ if (checkBody && req.body && typeof req.body === "object" && !Array.isArray(req.body)) {
3012
+ const polluted = {};
3013
+ const clean = {};
3014
+ for (const [key, value] of Object.entries(req.body)) {
3015
+ if (Array.isArray(value)) {
3016
+ if (whitelist.has(key)) {
3017
+ clean[key] = value;
3018
+ } else {
3019
+ polluted[key] = value;
3020
+ clean[key] = value[value.length - 1];
3021
+ }
3022
+ } else {
3023
+ clean[key] = value;
3024
+ }
3025
+ }
3026
+ req.bodyPolluted = polluted;
3027
+ Object.defineProperty(req, "body", { value: clean, writable: true, configurable: true });
3028
+ }
3029
+ next();
3030
+ };
3031
+ }
3032
+ var createHpp = hpp;
3033
+
2922
3034
  // src/utils/ip.ts
2923
3035
  var PLATFORM_HEADERS = {
2924
3036
  cloudflare: "cf-connecting-ip",
@@ -3039,7 +3151,7 @@ function fingerprint(req, options = {}) {
3039
3151
  components.push(`enc:${getHeader2(req, "accept-encoding")}`);
3040
3152
  }
3041
3153
  for (const c of custom) {
3042
- if (c != null) components.push(`custom:${c}`);
3154
+ if (c !== null && c !== void 0) components.push(`custom:${c}`);
3043
3155
  }
3044
3156
  components.sort();
3045
3157
  const hash = createHash("sha256");
@@ -3048,8 +3160,9 @@ function fingerprint(req, options = {}) {
3048
3160
  }
3049
3161
 
3050
3162
  // src/stores/memory.ts
3163
+ var DEFAULT_MAX_SIZE2 = 1e4;
3051
3164
  var MemoryStore = class {
3052
- constructor(windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS) {
3165
+ constructor(windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS, maxSize = DEFAULT_MAX_SIZE2) {
3053
3166
  this.store = /* @__PURE__ */ new Map();
3054
3167
  this.cleanupInterval = null;
3055
3168
  if (!Number.isFinite(windowMs) || windowMs < RATE_LIMIT.MIN_WINDOW_MS) {
@@ -3057,7 +3170,11 @@ var MemoryStore = class {
3057
3170
  `MemoryStore: windowMs must be a finite number >= ${RATE_LIMIT.MIN_WINDOW_MS} (got ${windowMs})`
3058
3171
  );
3059
3172
  }
3173
+ if (!Number.isFinite(maxSize) || maxSize < 1) {
3174
+ throw new RangeError(`MemoryStore: maxSize must be >= 1 (got ${maxSize})`);
3175
+ }
3060
3176
  this.windowMs = windowMs;
3177
+ this.maxSize = maxSize;
3061
3178
  this.startCleanup();
3062
3179
  }
3063
3180
  /**
@@ -3089,18 +3206,33 @@ var MemoryStore = class {
3089
3206
  return entry;
3090
3207
  }
3091
3208
  async set(key, entry) {
3209
+ if (!this.store.has(key) && this.store.size >= this.maxSize) {
3210
+ this.evictExpired();
3211
+ if (this.store.size >= this.maxSize) return;
3212
+ }
3092
3213
  this.store.set(key, entry);
3093
3214
  }
3094
3215
  async increment(key) {
3095
3216
  const now = Date.now();
3096
3217
  const entry = this.store.get(key);
3097
3218
  if (!entry || entry.resetTime < now) {
3219
+ if (this.store.size >= this.maxSize) {
3220
+ this.evictExpired();
3221
+ if (this.store.size >= this.maxSize) return 1;
3222
+ }
3098
3223
  this.store.set(key, { count: 1, resetTime: now + this.windowMs });
3099
3224
  return 1;
3100
3225
  }
3101
3226
  entry.count++;
3102
3227
  return entry.count;
3103
3228
  }
3229
+ /** Eagerly remove expired entries to reclaim capacity. */
3230
+ evictExpired() {
3231
+ const now = Date.now();
3232
+ for (const [key, entry] of this.store.entries()) {
3233
+ if (entry.resetTime < now) this.store.delete(key);
3234
+ }
3235
+ }
3104
3236
  async decrement(key) {
3105
3237
  const entry = this.store.get(key);
3106
3238
  if (entry && entry.count > 0) {
@@ -3182,6 +3314,6 @@ function createRedisStore(options) {
3182
3314
  return new RedisStore(options);
3183
3315
  }
3184
3316
 
3185
- 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, createCsrf, createErrorHandler, createHeaders, createRateLimiter, createRedactor, createRedisStore, createSafeLogger, createSanitizer, createSecureCookies, createSlidingWindowLimiter, createTokenBucketLimiter, createValidator, csrfProtection, main_default as default, detectBot, detectClientIp, detectCommandInjection, detectHeaderInjection, detectJsonpInjection, detectNoSqlInjection, detectPathTraversal, detectPii, detectPrototypePollution, detectSql, detectSsti, detectXss, detectXxe, encodeForAttribute, encodeForCss, encodeForHtml, encodeForJs, encodeForUrl, enforceSecureCookie, errorHandler, fingerprint, formatDuration, generateCsrfToken, isDangerousExtension, isDangerousNoSqlKey, isDangerousProtoKey, isPrivateIp, isRedirectSafe, isUrlSafe, isValidEmailSyntax, parseDuration, rateLimit, redactObjectPii, redactPii, safeCors, safeLog, sanitizeCommand, sanitizeFilename, sanitizeHeaderValue, sanitizeHeaders, sanitizeJsonpCallback, sanitizeObject, sanitizePath, sanitizeSql, sanitizeSsti, sanitizeString, sanitizeXss, sanitizeXxe, scanObjectPii, scanPii, secureCookieDefaults, securityHeaders, validate, validateCsrfToken, validateEmail, validateFile, validateRedirect, validateUrl, verifyEmailMx };
3317
+ 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, createCsrf, createErrorHandler, createHeaders, createHpp, createRateLimiter, createRedactor, createRedisStore, createSafeLogger, createSanitizer, createSecureCookies, createSlidingWindowLimiter, createTokenBucketLimiter, createValidator, csrfProtection, main_default as default, detectBot, detectClientIp, detectCommandInjection, detectHeaderInjection, detectJsonpInjection, detectNoSqlInjection, detectPathTraversal, detectPii, detectPrototypePollution, detectSql, detectSsti, detectXss, detectXxe, encodeForAttribute, encodeForCss, encodeForHtml, encodeForJs, encodeForUrl, enforceSecureCookie, errorHandler, fingerprint, formatDuration, generateCsrfToken, hpp, isDangerousExtension, isDangerousNoSqlKey, isDangerousProtoKey, isPrivateIp, isRedirectSafe, isUrlSafe, isValidEmailSyntax, parseDuration, rateLimit, redactObjectPii, redactPii, safeCors, safeLog, sanitizeCommand, sanitizeFilename, sanitizeHeaderValue, sanitizeHeaders, sanitizeJsonpCallback, sanitizeObject, sanitizePath, sanitizeSql, sanitizeSsti, sanitizeString, sanitizeXss, sanitizeXxe, scanObjectPii, scanPii, secureCookieDefaults, securityHeaders, validate, validateCsrfToken, validateEmail, validateFile, validateRedirect, validateUrl, verifyEmailMx };
3186
3318
  //# sourceMappingURL=index.mjs.map
3187
3319
  //# sourceMappingURL=index.mjs.map