@riocrypto/common-server 1.0.2277 → 1.0.2278

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.
@@ -72,18 +72,12 @@ function refreshCustomIpRulesCache(CustomRateLimit, filePathForLog) {
72
72
  });
73
73
  }
74
74
  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
- ;
75
+ 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
76
  if (!mongooseConnection) {
84
77
  throw new Error("[DynamicRateLimiter] Mongoose connection is required.");
85
78
  }
86
79
  const CustomRateLimit = (0, custom_rate_limit_1.buildCustomRateLimit)(mongooseConnection);
80
+ // Initial cache load (regex compilation happens here too)
87
81
  refreshCustomIpRulesCache(CustomRateLimit, "initialization");
88
82
  return (req, res, next) => __awaiter(this, void 0, void 0, function* () {
89
83
  try {
@@ -92,49 +86,63 @@ function createDynamicRateLimiter(options) {
92
86
  (customIpRulesCache.size === 0 && lastCacheRefreshTime === 0)) {
93
87
  yield refreshCustomIpRulesCache(CustomRateLimit, req.path);
94
88
  }
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;
89
+ let clientIp = req.ip;
90
+ let normalizedIp;
100
91
  if (clientIp) {
101
- customRule = customIpRulesCache.get(clientIp);
92
+ // Normalize IPv4-mapped IPv6 addresses (::ffff:x.x.x.x)
93
+ if (clientIp.startsWith("::ffff:")) {
94
+ normalizedIp = clientIp.substring(7);
95
+ }
96
+ else {
97
+ normalizedIp = clientIp;
98
+ }
102
99
  }
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}`;
100
+ let currentLimit = defaultMax;
101
+ let currentWindowMs = defaultWindowMs;
102
+ let ruleIdentifier = "default"; // Start with default identifier
103
+ let customRuleDoc;
104
+ let matchingRule = undefined;
105
+ if (normalizedIp) {
106
+ customRuleDoc = customIpRulesCache.get(normalizedIp);
107
+ }
108
+ if (customRuleDoc &&
109
+ customRuleDoc.routes &&
110
+ customRuleDoc.routes.length > 0) {
111
+ // Find the first specific rule that matches the request path and method
112
+ matchingRule = customRuleDoc.routes.find((rule) => {
113
+ const methodMatch = rule.action === "*" || rule.action === req.method.toUpperCase();
114
+ if (!methodMatch)
115
+ return false;
116
+ const regex = regexCache.get(rule.path);
117
+ if (!regex)
118
+ return false; // Should have been compiled on refresh
119
+ return regex.test(req.path);
120
+ });
121
+ if (matchingRule) {
122
+ // Apply limit and window from the *specific matching rule*
123
+ currentLimit = matchingRule.limit;
124
+ currentWindowMs = matchingRule.windowMs;
125
+ // Create a more specific identifier including the matched rule path/action
126
+ ruleIdentifier = `custom_ip:${normalizedIp}_rule:${matchingRule.action}:${matchingRule.path}`;
127
127
  }
128
+ // If no specific rule in the array matches, we just fall through
129
+ // and the defaults (currentLimit = defaultMax etc.) will be used.
128
130
  }
129
- // Get/create limiter instance using currentLimit, currentWindowMs, ruleIdentifier
131
+ // If customRuleDoc is undefined or routes array is empty, defaults apply.
132
+ // Generate cache key for the limiter instance. This will be unique for:
133
+ // - Default limit (if no rule found/matched)
134
+ // - Each specific matching rule (due to unique ruleIdentifier)
130
135
  const limiterCacheKey = `${currentLimit}-${currentWindowMs}-${ruleIdentifier}`;
131
136
  let selectedLimiter = limiterInstanceCache.get(limiterCacheKey);
132
137
  if (!selectedLimiter) {
138
+ // Create a new limiter instance with the determined limit/window
133
139
  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));
140
+ let ipForKey = request.ip;
141
+ if (ipForKey === null || ipForKey === void 0 ? void 0 : ipForKey.startsWith("::ffff:")) {
142
+ ipForKey = ipForKey.substring(7);
143
+ }
144
+ return ipForKey || "unknown_ip_for_rate_limit";
145
+ }, message: message, standardHeaders: standardHeaders, legacyHeaders: legacyHeaders, skip: skip, requestPropertyName: requestPropertyName }, otherRateLimitOptions));
138
146
  limiterInstanceCache.set(limiterCacheKey, selectedLimiter);
139
147
  }
140
148
  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.2278",
4
4
  "description": "",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",