@riocrypto/common-server 1.0.2277 → 1.0.2279

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.
@@ -25,8 +25,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.createDynamicRateLimiter = void 0;
27
27
  const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
28
- // Removed path-to-regexp import
29
- // Adjust the import path according to your project structure
30
28
  const custom_rate_limit_1 = require("../models/custom-rate-limit");
31
29
  // Cache for rate limiter instances
32
30
  const limiterInstanceCache = new Map();
@@ -72,18 +70,12 @@ function refreshCustomIpRulesCache(CustomRateLimit, filePathForLog) {
72
70
  });
73
71
  }
74
72
  function createDynamicRateLimiter(options) {
75
- const { windowMs: defaultWindowMs, // Rename for clarity
76
- max: defaultMax, // Rename for clarity
77
- mongooseConnection, cacheTtlMs = DEFAULT_CACHE_TTL_MS,
78
- // Capture other express-rate-limit options passed in
79
- message, standardHeaders, legacyHeaders, skip, requestPropertyName } = options,
80
- // Add any other valid RateLimitOptions you might pass
81
- otherRateLimitOptions = __rest(options, ["windowMs", "max", "mongooseConnection", "cacheTtlMs", "message", "standardHeaders", "legacyHeaders", "skip", "requestPropertyName"]) // Use a specific variable if you know exactly what else you pass
82
- ;
73
+ const { windowMs: defaultWindowMs, max: defaultMax, mongooseConnection, cacheTtlMs = DEFAULT_CACHE_TTL_MS, message, standardHeaders, legacyHeaders, skip, requestPropertyName } = options, otherRateLimitOptions = __rest(options, ["windowMs", "max", "mongooseConnection", "cacheTtlMs", "message", "standardHeaders", "legacyHeaders", "skip", "requestPropertyName"]);
83
74
  if (!mongooseConnection) {
84
75
  throw new Error("[DynamicRateLimiter] Mongoose connection is required.");
85
76
  }
86
77
  const CustomRateLimit = (0, custom_rate_limit_1.buildCustomRateLimit)(mongooseConnection);
78
+ // Initial cache load (regex compilation happens here too)
87
79
  refreshCustomIpRulesCache(CustomRateLimit, "initialization");
88
80
  return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
89
81
  try {
@@ -92,49 +84,78 @@ function createDynamicRateLimiter(options) {
92
84
  (customIpRulesCache.size === 0 && lastCacheRefreshTime === 0)) {
93
85
  yield refreshCustomIpRulesCache(CustomRateLimit, req.path);
94
86
  }
95
- const clientIp = req.ip;
96
- let currentLimit = defaultMax; // Use destructured default
97
- let currentWindowMs = defaultWindowMs; // Use destructured default
98
- let ruleIdentifier = "default";
99
- let customRule;
100
- if (clientIp) {
101
- customRule = customIpRulesCache.get(clientIp);
87
+ // Determine the client's IP address, prioritizing Cloudflare/proxy headers
88
+ let rawClientIp;
89
+ const cfConnectingIp = req.headers["cf-connecting-ip"];
90
+ const xForwardedForHeader = req.headers["x-forwarded-for"];
91
+ if (cfConnectingIp) {
92
+ rawClientIp = cfConnectingIp;
102
93
  }
103
- if (customRule) {
104
- // Check if routes array exists and is not empty
105
- const appliesToCurrentRoute = !customRule.routes || // Should not happen if default is []
106
- customRule.routes.length === 0 || // Empty array means apply custom limit to ALL requests for this IP
107
- customRule.routes.some((rule) => {
108
- // Check HTTP Method first (faster check)
109
- const methodMatch = rule.action === "*" || rule.action === req.method.toUpperCase();
110
- if (!methodMatch) {
111
- return false;
112
- }
113
- // Get the compiled regex from cache
114
- const regex = regexCache.get(rule.path);
115
- if (!regex) {
116
- // Regex failed compilation during refresh, skip this rule path
117
- // Error was already logged during refreshCustomIpRulesCache
118
- return false;
119
- }
120
- // Test the path against the regex
121
- return regex.test(req.path);
122
- });
123
- if (appliesToCurrentRoute) {
124
- currentLimit = customRule.limit;
125
- currentWindowMs = customRule.windowMs;
126
- ruleIdentifier = `custom_ip:${clientIp}`;
94
+ else if (xForwardedForHeader) {
95
+ // X-Forwarded-For can be a comma-separated list (client, proxy1, proxy2)
96
+ // The first IP is the original client IP
97
+ rawClientIp = xForwardedForHeader.split(",")[0].trim();
98
+ }
99
+ else {
100
+ rawClientIp = req.ip; // Fallback to Express's req.ip
101
+ }
102
+ let normalizedIp;
103
+ if (rawClientIp) {
104
+ // Normalize IPv4-mapped IPv6 addresses (e.g., ::ffff:192.0.2.1 -> 192.0.2.1)
105
+ // Also handles direct IPv4 and standard IPv6 addresses
106
+ if (rawClientIp.startsWith("::ffff:")) {
107
+ normalizedIp = rawClientIp.substring(7);
108
+ }
109
+ else {
110
+ normalizedIp = rawClientIp;
127
111
  }
128
112
  }
129
- // Get/create limiter instance using currentLimit, currentWindowMs, ruleIdentifier
113
+ let currentLimit = defaultMax;
114
+ let currentWindowMs = defaultWindowMs;
115
+ let ruleIdentifier = "default"; // Start with default identifier
116
+ let customRuleDoc;
117
+ let matchingRule = undefined;
118
+ if (normalizedIp) {
119
+ customRuleDoc = customIpRulesCache.get(normalizedIp);
120
+ }
121
+ if (customRuleDoc &&
122
+ customRuleDoc.routes &&
123
+ customRuleDoc.routes.length > 0) {
124
+ // Find the first specific rule that matches the request path and method
125
+ matchingRule = customRuleDoc.routes.find((rule) => {
126
+ const methodMatch = rule.action === "*" || rule.action === req.method.toUpperCase();
127
+ if (!methodMatch)
128
+ return false;
129
+ const regex = regexCache.get(rule.path);
130
+ if (!regex)
131
+ return false; // Should have been compiled on refresh
132
+ return regex.test(req.path);
133
+ });
134
+ if (matchingRule) {
135
+ // Apply limit and window from the *specific matching rule*
136
+ currentLimit = matchingRule.limit;
137
+ currentWindowMs = matchingRule.windowMs;
138
+ // Create a more specific identifier including the matched rule path/action
139
+ ruleIdentifier = `custom_ip:${normalizedIp}_rule:${matchingRule.action}:${matchingRule.path}`;
140
+ }
141
+ // If no specific rule in the array matches, we just fall through
142
+ // and the defaults (currentLimit = defaultMax etc.) will be used.
143
+ }
144
+ // If customRuleDoc is undefined or routes array is empty, defaults apply.
145
+ // Generate cache key for the limiter instance. This will be unique for:
146
+ // - Default limit (if no rule found/matched)
147
+ // - Each specific matching rule (due to unique ruleIdentifier)
130
148
  const limiterCacheKey = `${currentLimit}-${currentWindowMs}-${ruleIdentifier}`;
131
149
  let selectedLimiter = limiterInstanceCache.get(limiterCacheKey);
132
150
  if (!selectedLimiter) {
151
+ // Create a new limiter instance with the determined limit/window
133
152
  selectedLimiter = (0, express_rate_limit_1.default)(Object.assign({ windowMs: currentWindowMs, max: currentLimit, keyGenerator: (request) => {
134
- return request.ip || "unknown_ip_for_rate_limit";
135
- },
136
- // Pass through other specified options
137
- message: message, standardHeaders: standardHeaders, legacyHeaders: legacyHeaders, skip: skip, requestPropertyName: requestPropertyName }, otherRateLimitOptions));
153
+ let ipForKey = request.ip;
154
+ if (ipForKey === null || ipForKey === void 0 ? void 0 : ipForKey.startsWith("::ffff:")) {
155
+ ipForKey = ipForKey.substring(7);
156
+ }
157
+ return ipForKey || "unknown_ip_for_rate_limit";
158
+ }, message: message, standardHeaders: standardHeaders, legacyHeaders: legacyHeaders, skip: skip, requestPropertyName: requestPropertyName }, otherRateLimitOptions));
138
159
  limiterInstanceCache.set(limiterCacheKey, selectedLimiter);
139
160
  }
140
161
  return selectedLimiter(req, res, next);
@@ -2,19 +2,17 @@ import mongoose from "mongoose";
2
2
  interface RouteRule {
3
3
  path: string;
4
4
  action: string;
5
+ limit: number;
6
+ windowMs: number;
5
7
  }
6
8
  interface CustomRateLimitAttrs {
7
- createdAt?: Date;
9
+ createdAt: Date;
8
10
  ipAddress: string;
9
- limit: number;
10
- windowMs: number;
11
11
  routes: RouteRule[];
12
12
  }
13
13
  interface CustomRateLimitDoc extends mongoose.Document {
14
14
  createdAt: Date;
15
15
  ipAddress: string;
16
- limit: number;
17
- windowMs: number;
18
16
  routes: RouteRule[];
19
17
  }
20
18
  interface CustomRateLimitModel extends mongoose.Model<CustomRateLimitDoc> {
@@ -2,11 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildCustomRateLimit = void 0;
4
4
  const buildCustomRateLimit = (mongoose) => {
5
- // if model is already defined, return it
6
5
  if (mongoose.models.CustomRateLimit) {
7
6
  return mongoose.model("CustomRateLimit");
8
7
  }
9
- // Define the schema for the nested RouteRule object
8
+ // Updated: Define the schema for the nested RouteRule object including limits
10
9
  const RouteRuleSchema = new mongoose.Schema({
11
10
  path: {
12
11
  type: String,
@@ -15,32 +14,36 @@ const buildCustomRateLimit = (mongoose) => {
15
14
  action: {
16
15
  type: String,
17
16
  required: [true, "Action (HTTP method) is required for a route rule"],
18
- uppercase: true, // Store methods consistently (e.g., 'GET')
19
- // Optional: Add enum validation if you want to restrict methods
20
- // enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', '*'] // Example enum
17
+ uppercase: true,
18
+ // enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', '*']
21
19
  },
22
- }, { _id: false }); // Don't create separate IDs for subdocuments
20
+ limit: {
21
+ type: Number,
22
+ required: [true, "Limit is required for a specific route rule"],
23
+ min: [0, "Limit cannot be negative"], // Allow 0 for blocking?
24
+ },
25
+ windowMs: {
26
+ type: Number,
27
+ required: [true, "windowMs is required for a specific route rule"],
28
+ min: [1000, "windowMs must be at least 1000 (1 second)"],
29
+ },
30
+ }, { _id: false });
23
31
  const CustomRateLimitSchema = new mongoose.Schema({
24
32
  createdAt: {
25
33
  type: mongoose.Schema.Types.Date,
26
- default: Date.now, // Add default value for createdAt
34
+ default: Date.now,
27
35
  },
28
36
  ipAddress: {
29
37
  type: String,
30
38
  required: true,
39
+ // Add index for faster IP lookups
40
+ index: true,
31
41
  },
32
- limit: {
33
- type: Number,
34
- required: true,
35
- },
36
- windowMs: {
37
- type: Number,
38
- required: true,
39
- },
42
+ // Removed limit and windowMs from top level schema
40
43
  routes: {
41
44
  type: [RouteRuleSchema],
42
45
  required: true,
43
- // Default to empty array, meaning applies to all routes/methods if not specified
46
+ // Default to empty array. If empty, default limiter settings apply.
44
47
  default: [],
45
48
  },
46
49
  }, {
@@ -54,7 +57,6 @@ const buildCustomRateLimit = (mongoose) => {
54
57
  });
55
58
  CustomRateLimitSchema.statics.build = (attrs) => {
56
59
  var _a;
57
- // Ensure default createdAt is handled if not provided
58
60
  return new CustomRateLimit(Object.assign(Object.assign({}, attrs), { createdAt: (_a = attrs.createdAt) !== null && _a !== void 0 ? _a : new Date() }));
59
61
  };
60
62
  const CustomRateLimit = mongoose.model("CustomRateLimit", CustomRateLimitSchema);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riocrypto/common-server",
3
- "version": "1.0.2277",
3
+ "version": "1.0.2279",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",