@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.js CHANGED
@@ -70,7 +70,15 @@ var XSS_PATTERNS = [
70
70
  /** URL-encoded script tags */
71
71
  /%3Cscript/gi,
72
72
  /** SVG with onload */
73
- /<svg[^>]*onload/gi
73
+ /<svg[^>]*onload/gi,
74
+ /** form tags — phishing/credential harvesting via action= redirection */
75
+ /<form[\s>]/gi,
76
+ /** meta tags — http-equiv refresh redirects or CSP bypass */
77
+ /<meta[\s>]/gi,
78
+ /** base href hijacking — redirects all relative URLs to attacker domain */
79
+ /<base[\s>]/gi,
80
+ /** link tag injection — stylesheet or preload CSRF attacks */
81
+ /<link[\s>]/gi
74
82
  ];
75
83
  var XSS_REMOVE_PATTERNS = [
76
84
  /** Full script blocks (content + tags) */
@@ -97,7 +105,15 @@ var XSS_REMOVE_PATTERNS = [
97
105
  /javascript\s*:/gi,
98
106
  /vbscript\s*:/gi,
99
107
  /** data: URIs with HTML/script content */
100
- /data\s*:\s*text\/html[^>\s]*/gi
108
+ /data\s*:\s*text\/html[^>\s]*/gi,
109
+ /** form tag injection — phishing via action= redirection */
110
+ /<form[\s>][^>]*/gi,
111
+ /** meta tag injection — http-equiv refresh or CSP bypass */
112
+ /<meta[\s>][^>]*/gi,
113
+ /** base href hijacking */
114
+ /<base[\s>][^>]*/gi,
115
+ /** link tag injection — stylesheet or preload attacks */
116
+ /<link[\s>][^>]*/gi
101
117
  ];
102
118
  var SQL_PATTERNS = [
103
119
  /** SQL keywords */
@@ -161,8 +177,8 @@ var COMMAND_PATTERNS = [
161
177
  /[;&|`]/g,
162
178
  /** Command substitution: $( ... ) — matched as a pair to reduce false positives */
163
179
  /\$\(/g,
164
- /** URL-encoded newline/carriage-return injection (%0a, %0d) */
165
- /%0[ad]/gi
180
+ /** URL-encoded control characters (%00-%0F): null, tab, vtab, formfeed, LF, CR */
181
+ /%0[0-9a-f]/gi
166
182
  ];
167
183
  var DANGEROUS_PROTO_KEYS = /* @__PURE__ */ new Set([
168
184
  "__proto__",
@@ -453,7 +469,24 @@ function createRateLimiter(options = {}) {
453
469
  }
454
470
  next();
455
471
  } catch (error) {
456
- console.error("[arcis] Rate limiter error:", error);
472
+ console.error("[arcis] Rate limiter store error, using in-memory fallback:", error);
473
+ try {
474
+ const key = keyGenerator(req);
475
+ const now = Date.now();
476
+ if (!inMemoryStore[key] || inMemoryStore[key].resetTime < now) {
477
+ inMemoryStore[key] = { count: 1, resetTime: now + windowMs };
478
+ } else {
479
+ inMemoryStore[key].count++;
480
+ }
481
+ const count = inMemoryStore[key].count;
482
+ if (count > max) {
483
+ const resetSeconds = Math.ceil((inMemoryStore[key].resetTime - now) / 1e3);
484
+ res.setHeader("Retry-After", resetSeconds.toString());
485
+ res.status(statusCode).json({ error: message, retryAfter: resetSeconds });
486
+ return;
487
+ }
488
+ } catch {
489
+ }
457
490
  next();
458
491
  }
459
492
  };
@@ -697,26 +730,31 @@ function sanitizePath(input, collectThreats = false) {
697
730
  const threats = [];
698
731
  let value = input;
699
732
  let wasSanitized = false;
700
- for (const pattern of PATH_PATTERNS) {
701
- pattern.lastIndex = 0;
702
- if (pattern.test(value)) {
733
+ value = value.normalize("NFKC");
734
+ let prev;
735
+ do {
736
+ prev = value;
737
+ for (const pattern of PATH_PATTERNS) {
703
738
  pattern.lastIndex = 0;
704
- if (collectThreats) {
705
- const matches = value.match(pattern);
706
- if (matches) {
707
- for (const match of matches) {
708
- threats.push({
709
- type: "path_traversal",
710
- pattern: pattern.source,
711
- original: match
712
- });
739
+ if (pattern.test(value)) {
740
+ pattern.lastIndex = 0;
741
+ if (collectThreats) {
742
+ const matches = value.match(pattern);
743
+ if (matches) {
744
+ for (const match of matches) {
745
+ threats.push({
746
+ type: "path_traversal",
747
+ pattern: pattern.source,
748
+ original: match
749
+ });
750
+ }
713
751
  }
714
752
  }
753
+ value = value.replace(pattern, "");
754
+ wasSanitized = true;
715
755
  }
716
- value = value.replace(pattern, "");
717
- wasSanitized = true;
718
756
  }
719
- }
757
+ } while (value !== prev);
720
758
  if (collectThreats) {
721
759
  return { value, wasSanitized, threats };
722
760
  }
@@ -724,9 +762,10 @@ function sanitizePath(input, collectThreats = false) {
724
762
  }
725
763
  function detectPathTraversal(input) {
726
764
  if (typeof input !== "string") return false;
765
+ const normalized = input.normalize("NFKC");
727
766
  for (const pattern of PATH_PATTERNS) {
728
767
  pattern.lastIndex = 0;
729
- if (pattern.test(input)) {
768
+ if (pattern.test(normalized)) {
730
769
  return true;
731
770
  }
732
771
  }
@@ -784,7 +823,7 @@ function sanitizeString(value, options = {}) {
784
823
  if (value.length > maxSize) {
785
824
  throw new InputTooLargeError(maxSize, value.length);
786
825
  }
787
- const reject = options.mode !== "sanitize";
826
+ const reject = options.mode === "reject";
788
827
  let result = value;
789
828
  if (options.sql !== false) {
790
829
  if (reject) {
@@ -933,10 +972,22 @@ var SSTI_DETECT_PATTERNS = [
933
972
  /\{\{\s*(?:self|request|lipsum|cycler|joiner|namespace|range)\b/gi
934
973
  ];
935
974
  var SSTI_REMOVE_PATTERNS = [
975
+ /** Jinja2 / Twig: {{ ... }} — always strip (not valid in any JS context) */
936
976
  /\{\{.*?\}\}/g,
937
- /\$\{.*?\}/g,
977
+ /**
978
+ * Freemarker / Spring EL: ${...} — only strip when the expression contains
979
+ * operators (?!*+-/), method calls (), or known-dangerous prefixes.
980
+ * Bare ${name} and ${user.name} are left intact (JS template literal syntax).
981
+ */
982
+ /\$\{[^}]*[?!()*+\-/][^}]*\}/g,
983
+ /** ERB / EJS: <%= ... %> */
938
984
  /<%[=\-]?.*?%>/gs,
939
- /#\{.*?\}/g,
985
+ /**
986
+ * Pug / Jade: #{...} — same narrowing as ${ above.
987
+ * #{name} output expressions are left intact.
988
+ */
989
+ /#\{[^}]*[?!()*+\-/][^}]*\}/g,
990
+ /** Python dunder sandbox escape — always strip */
940
991
  /__(?:class|mro|subclasses|globals|builtins|import)__/gi
941
992
  ];
942
993
  function sanitizeSsti(input, collectThreats = false) {
@@ -1300,16 +1351,18 @@ function encodeForAttribute(value) {
1300
1351
  function encodeForJs(value) {
1301
1352
  if (!value) return "";
1302
1353
  let result = "";
1303
- for (let i = 0; i < value.length; i++) {
1304
- const ch = value.charCodeAt(i);
1305
- if (ch >= 48 && ch <= 57 || // 0-9
1306
- ch >= 65 && ch <= 90 || // A-Z
1307
- ch >= 97 && ch <= 122) {
1308
- result += value[i];
1309
- } else if (ch < 256) {
1310
- result += `\\x${ch.toString(16).toUpperCase().padStart(2, "0")}`;
1354
+ for (const char of value) {
1355
+ const cp = char.codePointAt(0);
1356
+ if (cp >= 48 && cp <= 57 || // 0-9
1357
+ cp >= 65 && cp <= 90 || // A-Z
1358
+ cp >= 97 && cp <= 122) {
1359
+ result += char;
1360
+ } else if (cp < 256) {
1361
+ result += `\\x${cp.toString(16).toUpperCase().padStart(2, "0")}`;
1362
+ } else if (cp <= 65535) {
1363
+ result += `\\u${cp.toString(16).toUpperCase().padStart(4, "0")}`;
1311
1364
  } else {
1312
- result += `\\u${ch.toString(16).toUpperCase().padStart(4, "0")}`;
1365
+ result += `\\u{${cp.toString(16).toUpperCase()}}`;
1313
1366
  }
1314
1367
  }
1315
1368
  return result;
@@ -1762,8 +1815,12 @@ function checkPrivateIp(hostname) {
1762
1815
  if (hostname === "metadata.google.internal" || hostname === "metadata.internal" || hostname === "metadata.azure.internal") {
1763
1816
  return "cloud metadata endpoint";
1764
1817
  }
1765
- const ipv6 = hostname.replace(/^\[|\]$/g, "");
1766
- if (ipv6 === "::1" || ipv6 === "::" || ipv6.startsWith("fc") || ipv6.startsWith("fd") || ipv6.startsWith("fe80")) {
1818
+ let ipv6 = hostname.replace(/^\[|\]$/g, "");
1819
+ const zoneIdx = ipv6.indexOf("%");
1820
+ if (zoneIdx !== -1) {
1821
+ ipv6 = ipv6.slice(0, zoneIdx);
1822
+ }
1823
+ 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)) {
1767
1824
  return "private IPv6 address";
1768
1825
  }
1769
1826
  const mappedDotted = ipv6.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
@@ -2822,11 +2879,9 @@ function generateCsrfToken(length = 32) {
2822
2879
  function validateCsrfToken(cookieToken, requestToken) {
2823
2880
  if (!cookieToken || !requestToken) return false;
2824
2881
  if (cookieToken.length !== requestToken.length) return false;
2825
- let result = 0;
2826
- for (let i = 0; i < cookieToken.length; i++) {
2827
- result |= cookieToken.charCodeAt(i) ^ requestToken.charCodeAt(i);
2828
- }
2829
- return result === 0;
2882
+ const a = Buffer.from(cookieToken);
2883
+ const b = Buffer.from(requestToken);
2884
+ return crypto.timingSafeEqual(a, b);
2830
2885
  }
2831
2886
  function getRequestToken(req, headerName, fieldName) {
2832
2887
  const headerToken = req.headers[headerName.toLowerCase()];
@@ -2835,19 +2890,17 @@ function getRequestToken(req, headerName, fieldName) {
2835
2890
  const bodyToken = req.body[fieldName];
2836
2891
  if (typeof bodyToken === "string" && bodyToken) return bodyToken;
2837
2892
  }
2838
- if (req.query && fieldName in req.query) {
2839
- const queryToken = req.query[fieldName];
2840
- if (typeof queryToken === "string" && queryToken) return queryToken;
2841
- }
2842
2893
  return void 0;
2843
2894
  }
2844
2895
  function csrfProtection(options = {}) {
2845
- const cookieName = options.cookieName ?? DEFAULTS.cookieName;
2896
+ const baseCookieName = options.cookieName ?? DEFAULTS.cookieName;
2897
+ const cookieName = options.useHostPrefix ? `__Host-${baseCookieName}` : baseCookieName;
2846
2898
  const headerName = options.headerName ?? DEFAULTS.headerName;
2847
2899
  const fieldName = options.fieldName ?? DEFAULTS.fieldName;
2848
2900
  const tokenLength = options.tokenLength ?? DEFAULTS.tokenLength;
2849
2901
  const protectedMethods = options.protectedMethods ?? [...DEFAULTS.protectedMethods];
2850
2902
  const excludePaths = options.excludePaths ?? [];
2903
+ const skipCsrf = options.skipCsrf;
2851
2904
  const isProduction = process.env.NODE_ENV === "production";
2852
2905
  const cookieOpts = {
2853
2906
  path: options.cookie?.path ?? "/",
@@ -2867,6 +2920,9 @@ function csrfProtection(options = {}) {
2867
2920
  const protectedSet = new Set(protectedMethods.map((m) => m.toUpperCase()));
2868
2921
  return (req, res, next) => {
2869
2922
  const method = req.method.toUpperCase();
2923
+ if (skipCsrf && skipCsrf(req)) {
2924
+ return next();
2925
+ }
2870
2926
  const requestPath = req.path || req.url;
2871
2927
  if (excludePaths.some((p) => requestPath === p || requestPath.startsWith(p + "/"))) {
2872
2928
  return next();
@@ -2916,13 +2972,69 @@ function setCsrfCookie(res, name, token, opts) {
2916
2972
  if (opts.secure) parts.push("Secure");
2917
2973
  parts.push(`SameSite=${opts.sameSite}`);
2918
2974
  if (opts.domain) parts.push(`Domain=${opts.domain}`);
2919
- res.setHeader("Set-Cookie", parts.join("; "));
2975
+ const newCookie = parts.join("; ");
2976
+ const existing = res.getHeader("Set-Cookie");
2977
+ if (existing === void 0) {
2978
+ res.setHeader("Set-Cookie", newCookie);
2979
+ } else if (Array.isArray(existing)) {
2980
+ res.setHeader("Set-Cookie", [...existing, newCookie]);
2981
+ } else {
2982
+ res.setHeader("Set-Cookie", [existing, newCookie]);
2983
+ }
2920
2984
  }
2921
2985
  function escapeRegex(str) {
2922
2986
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2923
2987
  }
2924
2988
  var createCsrf = csrfProtection;
2925
2989
 
2990
+ // src/middleware/hpp.ts
2991
+ function hpp(options = {}) {
2992
+ const whitelist = new Set(options.whitelist ?? []);
2993
+ const checkQuery = options.checkQuery ?? true;
2994
+ const checkBody = options.checkBody ?? true;
2995
+ return (req, _res, next) => {
2996
+ if (checkQuery && req.query && typeof req.query === "object") {
2997
+ const polluted = {};
2998
+ const clean = {};
2999
+ for (const [key, value] of Object.entries(req.query)) {
3000
+ if (Array.isArray(value)) {
3001
+ const strings = value.filter((v) => typeof v === "string");
3002
+ if (whitelist.has(key)) {
3003
+ clean[key] = strings;
3004
+ } else {
3005
+ polluted[key] = strings;
3006
+ clean[key] = strings[strings.length - 1] ?? "";
3007
+ }
3008
+ } else {
3009
+ clean[key] = value;
3010
+ }
3011
+ }
3012
+ req.queryPolluted = polluted;
3013
+ Object.defineProperty(req, "query", { value: clean, writable: true, configurable: true });
3014
+ }
3015
+ if (checkBody && req.body && typeof req.body === "object" && !Array.isArray(req.body)) {
3016
+ const polluted = {};
3017
+ const clean = {};
3018
+ for (const [key, value] of Object.entries(req.body)) {
3019
+ if (Array.isArray(value)) {
3020
+ if (whitelist.has(key)) {
3021
+ clean[key] = value;
3022
+ } else {
3023
+ polluted[key] = value;
3024
+ clean[key] = value[value.length - 1];
3025
+ }
3026
+ } else {
3027
+ clean[key] = value;
3028
+ }
3029
+ }
3030
+ req.bodyPolluted = polluted;
3031
+ Object.defineProperty(req, "body", { value: clean, writable: true, configurable: true });
3032
+ }
3033
+ next();
3034
+ };
3035
+ }
3036
+ var createHpp = hpp;
3037
+
2926
3038
  // src/utils/ip.ts
2927
3039
  var PLATFORM_HEADERS = {
2928
3040
  cloudflare: "cf-connecting-ip",
@@ -3043,7 +3155,7 @@ function fingerprint(req, options = {}) {
3043
3155
  components.push(`enc:${getHeader2(req, "accept-encoding")}`);
3044
3156
  }
3045
3157
  for (const c of custom) {
3046
- if (c != null) components.push(`custom:${c}`);
3158
+ if (c !== null && c !== void 0) components.push(`custom:${c}`);
3047
3159
  }
3048
3160
  components.sort();
3049
3161
  const hash = crypto.createHash("sha256");
@@ -3052,8 +3164,9 @@ function fingerprint(req, options = {}) {
3052
3164
  }
3053
3165
 
3054
3166
  // src/stores/memory.ts
3167
+ var DEFAULT_MAX_SIZE2 = 1e4;
3055
3168
  var MemoryStore = class {
3056
- constructor(windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS) {
3169
+ constructor(windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS, maxSize = DEFAULT_MAX_SIZE2) {
3057
3170
  this.store = /* @__PURE__ */ new Map();
3058
3171
  this.cleanupInterval = null;
3059
3172
  if (!Number.isFinite(windowMs) || windowMs < RATE_LIMIT.MIN_WINDOW_MS) {
@@ -3061,7 +3174,11 @@ var MemoryStore = class {
3061
3174
  `MemoryStore: windowMs must be a finite number >= ${RATE_LIMIT.MIN_WINDOW_MS} (got ${windowMs})`
3062
3175
  );
3063
3176
  }
3177
+ if (!Number.isFinite(maxSize) || maxSize < 1) {
3178
+ throw new RangeError(`MemoryStore: maxSize must be >= 1 (got ${maxSize})`);
3179
+ }
3064
3180
  this.windowMs = windowMs;
3181
+ this.maxSize = maxSize;
3065
3182
  this.startCleanup();
3066
3183
  }
3067
3184
  /**
@@ -3093,18 +3210,33 @@ var MemoryStore = class {
3093
3210
  return entry;
3094
3211
  }
3095
3212
  async set(key, entry) {
3213
+ if (!this.store.has(key) && this.store.size >= this.maxSize) {
3214
+ this.evictExpired();
3215
+ if (this.store.size >= this.maxSize) return;
3216
+ }
3096
3217
  this.store.set(key, entry);
3097
3218
  }
3098
3219
  async increment(key) {
3099
3220
  const now = Date.now();
3100
3221
  const entry = this.store.get(key);
3101
3222
  if (!entry || entry.resetTime < now) {
3223
+ if (this.store.size >= this.maxSize) {
3224
+ this.evictExpired();
3225
+ if (this.store.size >= this.maxSize) return 1;
3226
+ }
3102
3227
  this.store.set(key, { count: 1, resetTime: now + this.windowMs });
3103
3228
  return 1;
3104
3229
  }
3105
3230
  entry.count++;
3106
3231
  return entry.count;
3107
3232
  }
3233
+ /** Eagerly remove expired entries to reclaim capacity. */
3234
+ evictExpired() {
3235
+ const now = Date.now();
3236
+ for (const [key, entry] of this.store.entries()) {
3237
+ if (entry.resetTime < now) this.store.delete(key);
3238
+ }
3239
+ }
3108
3240
  async decrement(key) {
3109
3241
  const entry = this.store.get(key);
3110
3242
  if (entry && entry.count > 0) {
@@ -3208,6 +3340,7 @@ exports.createCors = createCors;
3208
3340
  exports.createCsrf = createCsrf;
3209
3341
  exports.createErrorHandler = createErrorHandler;
3210
3342
  exports.createHeaders = createHeaders;
3343
+ exports.createHpp = createHpp;
3211
3344
  exports.createRateLimiter = createRateLimiter;
3212
3345
  exports.createRedactor = createRedactor;
3213
3346
  exports.createRedisStore = createRedisStore;
@@ -3242,6 +3375,7 @@ exports.errorHandler = errorHandler;
3242
3375
  exports.fingerprint = fingerprint;
3243
3376
  exports.formatDuration = formatDuration;
3244
3377
  exports.generateCsrfToken = generateCsrfToken;
3378
+ exports.hpp = hpp;
3245
3379
  exports.isDangerousExtension = isDangerousExtension;
3246
3380
  exports.isDangerousNoSqlKey = isDangerousNoSqlKey;
3247
3381
  exports.isDangerousProtoKey = isDangerousProtoKey;