express-adaptive-rate-limiter 2.0.0 → 2.0.1
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.
- package/README.md +185 -2
- package/dist/core/limiters/clusterLimiter.js +1 -16
- package/dist/core/limiters/redisLimiter.js +1 -1
- package/dist/index.js +1 -1
- package/dist/rateLimit.js +1 -1
- package/dist/rateLimitManager.js +1 -1
- package/dist/stores/redisStore.js +1 -1
- package/dist/types/index.d.ts +19 -2
- package/dist/utils/redisClient.js +1 -1
- package/dist/utils/validation.js +1 -1
- package/package.json +7 -9
package/README.md
CHANGED
|
@@ -11,6 +11,17 @@ Every backend in the core tier is atomic: the counter increment and the window c
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## What's new in 2.0.1
|
|
15
|
+
|
|
16
|
+
- **Full Redis Cluster support**:
|
|
17
|
+
- **Cross-Slot (`CROSSSLOT`) Protection**: Safe atomic key deletion (`delMany`) via pipeline or per-slot execution preventing Redis Cluster `CROSSSLOT` failures.
|
|
18
|
+
- **Multi-Master Cluster Scanning**: `scanKeys` and `store.clear()` query every cluster master node (`nodes('master')` in `ioredis`, `masters` in `node-redis`) to locate and clear keys across all shards without missing any nodes.
|
|
19
|
+
- **Automatic Hash Tagging**: `hashTag: true` automatically wraps keys in `{keyPrefix:key}` so all operations for a given rate limit target map to the exact same hash slot on the cluster.
|
|
20
|
+
- **Cluster Limiter Parity**: Full drop-in compatibility with `RateLimiterRedisCluster`, `ClusterLimiter`, and `RedisClusterLimiter`, inheriting `rejectIfRedisNotReady` and `parseKey` key unwrapping.
|
|
21
|
+
- **Direct Express Tier Integration**: Configure `rateLimit({ store: 'redis-cluster' })` or `rateLimit({ store: 'cluster' })` with automated cluster client creation and node discovery.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
14
25
|
## What's new in 2.0.0
|
|
15
26
|
|
|
16
27
|
**New: the framework-agnostic core tier.** 13 storage backends (Memory, Redis, Valkey, Redis Cluster, Mongo, Mongoose, DynamoDB, MySQL, Postgres, SQLite, Prisma, Memcached, etcd) with a unified points-based API, plus the `Queue`, `Union`, `Bursty`, `BlackAndWhite` and `Timeout` wrappers. See [Core Tier](#core-tier-framework-agnostic-limiters).
|
|
@@ -176,7 +187,7 @@ Both modern descriptive names and classic class aliases are exported for conveni
|
|
|
176
187
|
| `MemoryLimiter` | `RateLimiterMemory` | none | in-process Map with O(1) eviction |
|
|
177
188
|
| `RedisLimiter` | `RateLimiterRedis` | `ioredis` or `redis` | Lua script |
|
|
178
189
|
| `RateLimiterCluster`| `RateLimiterCluster` | `node:cluster` IPC | Master-Worker IPC messaging |
|
|
179
|
-
| `RedisClusterLimiter`| `ClusterLimiter`
|
|
190
|
+
| `RedisClusterLimiter`| `ClusterLimiter` / `RateLimiterRedisCluster` | ioredis Cluster / node-redis cluster | Lua script, hash-tagged `{slot}` |
|
|
180
191
|
| `ValkeyLimiter` | `RateLimiterValkey` | `iovalkey` or `@valkey/valkey-glide` | Lua script |
|
|
181
192
|
| `MongoLimiter` | `RateLimiterMongo` | `mongodb` | `findOneAndUpdate` with `$inc` |
|
|
182
193
|
| `MongooseLimiter` | `RateLimiterMongoose` | `mongoose` | `findOneAndUpdate` with `$inc` |
|
|
@@ -220,6 +231,162 @@ if (cluster.isPrimary || cluster.isMaster) {
|
|
|
220
231
|
}
|
|
221
232
|
```
|
|
222
233
|
|
|
234
|
+
### Redis Cluster Rate Limiting (`ClusterLimiter` & `store: 'redis-cluster'`)
|
|
235
|
+
|
|
236
|
+
Rate limiting across a distributed Redis Cluster requires careful handling of key routing. Redis Cluster partitions keys across 16,384 hash slots. Multi-key operations or atomic scripts touching related keys will fail with `CROSSSLOT Keys in request don't hash to the same slot` unless configured with **hash tags (`{...}`)**.
|
|
237
|
+
|
|
238
|
+
`express-adaptive-rate-limiter` provides **first-class Redis Cluster support** across both the Express middleware and Core tiers:
|
|
239
|
+
- **Automatic Hash Tagging**: Wraps keys in `{keyPrefix:key}` by default so multi-key operations land on the exact same hash slot.
|
|
240
|
+
- **Cross-Slot Safe Cleanups**: Key deletions (`delMany`) use Redis pipelines or individual per-slot deletions, preventing `CROSSSLOT` errors.
|
|
241
|
+
- **Cluster-Wide Master Scanning**: `store.clear()` queries all cluster master nodes (`cluster.nodes('master')` in `ioredis`, `cluster.masters` in `node-redis`) to ensure no dangling keys remain across shards.
|
|
242
|
+
- **Client Flexibility**: Works with both `ioredis.Cluster` and `@redis/client` (`node-redis`) cluster mode.
|
|
243
|
+
|
|
244
|
+
#### 1. Express Middleware with Redis Cluster
|
|
245
|
+
|
|
246
|
+
##### Using `ioredis.Cluster`:
|
|
247
|
+
|
|
248
|
+
```javascript
|
|
249
|
+
import express from 'express';
|
|
250
|
+
import Redis from 'ioredis';
|
|
251
|
+
import { rateLimit } from 'express-adaptive-rate-limiter';
|
|
252
|
+
|
|
253
|
+
const app = express();
|
|
254
|
+
|
|
255
|
+
// Initialize ioredis Cluster client
|
|
256
|
+
const clusterClient = new Redis.Cluster(
|
|
257
|
+
[
|
|
258
|
+
{ host: '10.0.0.1', port: 6379 },
|
|
259
|
+
{ host: '10.0.0.2', port: 6379 },
|
|
260
|
+
{ host: '10.0.0.3', port: 6379 }
|
|
261
|
+
],
|
|
262
|
+
{
|
|
263
|
+
redisOptions: {
|
|
264
|
+
enableOfflineQueue: false // recommended: fail fast on network partitions
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Configure Express rate limiter with Redis Cluster
|
|
270
|
+
const clusterLimiter = rateLimit({
|
|
271
|
+
store: 'redis-cluster', // or 'cluster'
|
|
272
|
+
client: clusterClient,
|
|
273
|
+
algorithm: 'sliding-window-counter', // any of the 5 algorithms
|
|
274
|
+
limit: 100, // 100 requests
|
|
275
|
+
windowMs: 60 * 1000, // per 1 minute
|
|
276
|
+
hashTag: true, // wraps storage keys in {prefix:key} (default: true)
|
|
277
|
+
failOpen: true // allow traffic if cluster is unreachable
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
app.use(clusterLimiter);
|
|
281
|
+
|
|
282
|
+
app.get('/api/resource', (req, res) => {
|
|
283
|
+
res.json({ message: 'Within cluster rate limit', rateLimit: req.rateLimit });
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
##### Using `node-redis` (`createCluster`):
|
|
288
|
+
|
|
289
|
+
```javascript
|
|
290
|
+
import express from 'express';
|
|
291
|
+
import { createCluster } from 'redis';
|
|
292
|
+
import { rateLimit } from 'express-adaptive-rate-limiter';
|
|
293
|
+
|
|
294
|
+
const app = express();
|
|
295
|
+
|
|
296
|
+
const cluster = createCluster({
|
|
297
|
+
rootNodes: [{ url: 'redis://10.0.0.1:6379' }, { url: 'redis://10.0.0.2:6379' }]
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await cluster.connect();
|
|
301
|
+
|
|
302
|
+
app.use(
|
|
303
|
+
rateLimit({
|
|
304
|
+
store: 'redis-cluster',
|
|
305
|
+
client: cluster,
|
|
306
|
+
limit: 60,
|
|
307
|
+
windowMs: 60000
|
|
308
|
+
})
|
|
309
|
+
);
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
##### Auto-Connecting Cluster (without manual client instantiation):
|
|
313
|
+
|
|
314
|
+
You can also pass cluster nodes directly to `rateLimit` or `RedisStore`:
|
|
315
|
+
|
|
316
|
+
```javascript
|
|
317
|
+
import { rateLimit } from 'express-adaptive-rate-limiter';
|
|
318
|
+
|
|
319
|
+
app.use(
|
|
320
|
+
rateLimit({
|
|
321
|
+
store: 'redis-cluster',
|
|
322
|
+
cluster: [
|
|
323
|
+
{ host: '10.0.0.1', port: 6379 },
|
|
324
|
+
{ host: '10.0.0.2', port: 6379 }
|
|
325
|
+
],
|
|
326
|
+
limit: 100,
|
|
327
|
+
windowMs: 60000
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
#### 2. Framework-Agnostic Core Tier (`ClusterLimiter` / `RateLimiterRedisCluster`)
|
|
335
|
+
|
|
336
|
+
For non-Express applications, microservices, GraphQL resolvers, or background queues, use `ClusterLimiter` (also exported as `RedisClusterLimiter` and `RateLimiterRedisCluster` for 100% `rate-limiter-flexible` drop-in parity):
|
|
337
|
+
|
|
338
|
+
```javascript
|
|
339
|
+
import Redis from 'ioredis';
|
|
340
|
+
import { ClusterLimiter } from 'express-adaptive-rate-limiter';
|
|
341
|
+
|
|
342
|
+
const redisCluster = new Redis.Cluster([
|
|
343
|
+
{ host: '10.0.0.1', port: 6379 },
|
|
344
|
+
{ host: '10.0.0.2', port: 6379 }
|
|
345
|
+
]);
|
|
346
|
+
|
|
347
|
+
const limiter = new ClusterLimiter({
|
|
348
|
+
storeClient: redisCluster,
|
|
349
|
+
points: 10, // 10 points
|
|
350
|
+
duration: 1, // per 1 second
|
|
351
|
+
keyPrefix: 'cluster_api',
|
|
352
|
+
hashTag: true, // wraps keys in {cluster_api:key}
|
|
353
|
+
rejectIfRedisNotReady: true // reject immediately if cluster is not ready
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Consume points
|
|
357
|
+
try {
|
|
358
|
+
const res = await limiter.consume('user_42', 1);
|
|
359
|
+
console.log('Points remaining:', res.remainingPoints);
|
|
360
|
+
} catch (rejRes) {
|
|
361
|
+
if (rejRes instanceof Error) {
|
|
362
|
+
console.error('Cluster storage error:', rejRes);
|
|
363
|
+
} else {
|
|
364
|
+
console.warn(`Rate limit exceeded! Retry after ${rejRes.msBeforeNext}ms`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Penalty and reward
|
|
369
|
+
await limiter.penalty('user_42', 2);
|
|
370
|
+
await limiter.reward('user_42', 1);
|
|
371
|
+
|
|
372
|
+
// Block a client across the cluster
|
|
373
|
+
await limiter.block('suspicious_user', 3600); // blocked for 1 hour
|
|
374
|
+
|
|
375
|
+
// Read state without consuming
|
|
376
|
+
const state = await limiter.get('user_42');
|
|
377
|
+
console.log(state?.consumedPoints);
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### Redis Cluster Options Reference
|
|
381
|
+
|
|
382
|
+
| Option | Type | Default | Description |
|
|
383
|
+
| --- | --- | --- | --- |
|
|
384
|
+
| `storeClient` / `client` | `object` | `null` | Active `ioredis.Cluster` or `node-redis` cluster instance. |
|
|
385
|
+
| `cluster` / `clusterNodes` | `Array` | `null` | Array of cluster node definitions to automatically instantiate. |
|
|
386
|
+
| `hashTag` | `boolean` | `true` | Wraps keys in `{...}` hash tags so multi-key operations land in the same slot. |
|
|
387
|
+
| `rejectIfRedisNotReady` | `boolean` | `false` | Throws `RedisConnectionError` if cluster status is not ready. |
|
|
388
|
+
| `failOpen` | `boolean` | `false` | When true, errors from the store fail open instead of failing closed. |
|
|
389
|
+
|
|
223
390
|
### In-Memory State Dump & Restore (`dump()` & `restore()`)
|
|
224
391
|
|
|
225
392
|
Save in-memory state on shutdown for zero-downtime restarts:
|
|
@@ -885,6 +1052,22 @@ Key performance highlights:
|
|
|
885
1052
|
|
|
886
1053
|
---
|
|
887
1054
|
|
|
1055
|
+
## Issues & Support
|
|
1056
|
+
|
|
1057
|
+
If you encounter any bug, issue, or have any questions:
|
|
1058
|
+
- **npm Package Page**: You can report issues or reach out via the package page on npm:
|
|
1059
|
+
👉 **[npm: express-adaptive-rate-limiter](https://www.npmjs.com/package/express-adaptive-rate-limiter)**
|
|
1060
|
+
- **Developer Profile**: You can also reach out directly through the maintainer's profile:
|
|
1061
|
+
👉 **[GitHub Profile: @ravinddeveloper](https://github.com/ravinddeveloper)**
|
|
1062
|
+
|
|
1063
|
+
Our team actively investigates and resolves all reported issues. When reporting a bug or requesting assistance, please include:
|
|
1064
|
+
- Node.js version (`node -v`)
|
|
1065
|
+
- Package version (`2.0.1`)
|
|
1066
|
+
- Storage driver & version (e.g., `ioredis@5.4.2` or `redis@4.7.0`)
|
|
1067
|
+
- A minimal reproducible example or stack trace
|
|
1068
|
+
|
|
1069
|
+
---
|
|
1070
|
+
|
|
888
1071
|
## License
|
|
889
1072
|
|
|
890
|
-
MIT ©
|
|
1073
|
+
MIT © [RAVI N D](https://github.com/ravinddeveloper)
|
|
@@ -1,16 +1 @@
|
|
|
1
|
-
import{
|
|
2
|
-
local ttl = redis.call('pttl', KEYS[1])
|
|
3
|
-
if ttl == -2 then
|
|
4
|
-
redis.call('set', KEYS[1], ARGV[1], 'PX', ARGV[2])
|
|
5
|
-
return {ARGV[1], ARGV[2]}
|
|
6
|
-
end
|
|
7
|
-
local consumed = redis.call('incrby', KEYS[1], ARGV[1])
|
|
8
|
-
if ttl == -1 then
|
|
9
|
-
redis.call('pexpire', KEYS[1], ARGV[2])
|
|
10
|
-
ttl = tonumber(ARGV[2])
|
|
11
|
-
end
|
|
12
|
-
return {consumed, ttl}
|
|
13
|
-
`;const SET_TTL_LUA=`
|
|
14
|
-
redis.call('set', KEYS[1], ARGV[1], 'PX', ARGV[2])
|
|
15
|
-
return {ARGV[1], ARGV[2]}
|
|
16
|
-
`;export class ClusterLimiter extends RateLimiterStore{constructor(opts={}){super(opts);const client=opts.storeClient||opts.client||opts.redis||null;if(!client){throw new Error('Redis cluster client is not configured. Pass it via the "storeClient" option.');}this.client=client;this.adapter=new RedisClientAdapter(client,{clientKind:opts.clientKind});this.hashTag=opts.hashTag!==undefined?Boolean(opts.hashTag):true;this._incrTtlScript=opts.customIncrTtlLuaScript||INCR_TTL_LUA;this._setTtlScript=opts.customSetTtlLuaScript||SET_TTL_LUA;}getKey(key){const prefixed=super.getKey(key);if(!this.hashTag)return prefixed;if(prefixed.includes('{'))return prefixed;return`{${prefixed}}`;}async _upsert(rlKey,points,msDuration,forceExpire=false){if(!Number.isInteger(points)){throw new Error('Consuming decimal number of points is not supported by this package');}const ttl=Math.max(1,Math.floor(msDuration));const script=forceExpire?this._setTtlScript:this._incrTtlScript;const raw=await this.adapter.evalScript(script,[rlKey],[points,ttl]);const consumed=Array.isArray(raw)?Number(raw[0]):Number(raw);const resTtlMs=Array.isArray(raw)?Number(raw[1]):ttl;return{consumedPoints:consumed,expireAt:msDuration>0?Date.now()+resTtlMs:null};}async _get(rlKey){const entry=await this.adapter.getWithTtl(rlKey);if(!entry)return null;const consumedPoints=Number(entry.value);if(!Number.isFinite(consumedPoints))return null;return{consumedPoints,expireAt:entry.ttlMs>0?Date.now()+entry.ttlMs:null};}async _delete(rlKey){return this.adapter.del(rlKey);}}export const RedisClusterLimiter=ClusterLimiter;export default ClusterLimiter;
|
|
1
|
+
import{RedisLimiter}from'./redisLimiter.js';import{RedisConnectionError}from'../../utils/errors.js';export class ClusterLimiter extends RedisLimiter{constructor(opts={}){const client=opts.storeClient||opts.client||opts.redis||null;if(!client){throw new RedisConnectionError('Redis cluster client is not configured. Pass it via the "storeClient" option.');}super({...opts,isCluster:opts.isCluster!==undefined?opts.isCluster:true,hashTag:opts.hashTag!==undefined?Boolean(opts.hashTag):true});}}export const RedisClusterLimiter=ClusterLimiter;export const RateLimiterRedisCluster=ClusterLimiter;export default ClusterLimiter;
|
|
@@ -13,4 +13,4 @@ return {consumed, ttl}
|
|
|
13
13
|
`;const SET_TTL_LUA=`
|
|
14
14
|
redis.call('set', KEYS[1], ARGV[1], 'PX', ARGV[2])
|
|
15
15
|
return {ARGV[1], ARGV[2]}
|
|
16
|
-
`;export class RedisLimiter extends RateLimiterStore{constructor(opts={}){super(opts);const client=opts.storeClient||opts.client||opts.redis||null;if(!client){throw new RedisConnectionError('Redis client is not configured. Pass it via the "storeClient" option.');}this.client=client;this.adapter=new RedisClientAdapter(client,{clientKind:opts.clientKind});this.rejectIfRedisNotReady=Boolean(opts.rejectIfRedisNotReady);this._incrTtlScript=opts.customIncrTtlLuaScript||INCR_TTL_LUA;this._setTtlScript=opts.customSetTtlLuaScript||SET_TTL_LUA;}_isRedisReady(rlKey,isReadonly){if(!this.rejectIfRedisNotReady){return true;}return this.adapter.isReady(rlKey,isReadonly);}async _upsert(rlKey,points,msDuration,forceExpire=false){if(!Number.isInteger(points)){throw new Error('Consuming decimal number of points is not supported by this package');}if(!this._isRedisReady(rlKey,false)){throw new RedisConnectionError('Redis connection is not ready');}const ttl=Math.max(1,Math.floor(msDuration));const script=forceExpire?this._setTtlScript:this._incrTtlScript;const raw=await this.adapter.evalScript(script,[rlKey],[points,ttl]);const consumed=Array.isArray(raw)?Number(raw[0]):Number(raw);const resTtlMs=Array.isArray(raw)?Number(raw[1]):ttl;return{consumedPoints:consumed,expireAt:msDuration>0?Date.now()+resTtlMs:null};}async _get(rlKey){if(!this._isRedisReady(rlKey,true)){throw new RedisConnectionError('Redis connection is not ready');}const entry=await this.adapter.getWithTtl(rlKey);if(!entry){return null;}const consumedPoints=Number(entry.value);if(!Number.isFinite(consumedPoints)){return null;}return{consumedPoints,expireAt:entry.ttlMs>0?Date.now()+entry.ttlMs:null};}async _delete(rlKey){return this.adapter.del(rlKey);}}export default RedisLimiter;
|
|
16
|
+
`;export class RedisLimiter extends RateLimiterStore{constructor(opts={}){super(opts);const client=opts.storeClient||opts.client||opts.redis||null;if(!client){throw new RedisConnectionError('Redis client is not configured. Pass it via the "storeClient" option.');}this.client=client;this.adapter=new RedisClientAdapter(client,{clientKind:opts.clientKind,isCluster:opts.isCluster});this.hashTag=Boolean(opts.hashTag);this.rejectIfRedisNotReady=Boolean(opts.rejectIfRedisNotReady);this._incrTtlScript=opts.customIncrTtlLuaScript||INCR_TTL_LUA;this._setTtlScript=opts.customSetTtlLuaScript||SET_TTL_LUA;}getKey(key){const prefixed=super.getKey(key);if(!this.hashTag)return prefixed;if(prefixed.includes('{'))return prefixed;return`{${prefixed}}`;}parseKey(rlKey){if(!rlKey)return rlKey;let key=rlKey;if(this.hashTag&&key.startsWith('{')&&key.endsWith('}')){key=key.slice(1,-1);}return super.parseKey(key);}_isRedisReady(rlKey,isReadonly){if(!this.rejectIfRedisNotReady){return true;}return this.adapter.isReady(rlKey,isReadonly);}async _upsert(rlKey,points,msDuration,forceExpire=false){if(!Number.isInteger(points)){throw new Error('Consuming decimal number of points is not supported by this package');}if(!this._isRedisReady(rlKey,false)){throw new RedisConnectionError('Redis connection is not ready');}const ttl=Math.max(1,Math.floor(msDuration));const script=forceExpire?this._setTtlScript:this._incrTtlScript;const raw=await this.adapter.evalScript(script,[rlKey],[points,ttl]);const consumed=Array.isArray(raw)?Number(raw[0]):Number(raw);const resTtlMs=Array.isArray(raw)?Number(raw[1]):ttl;return{consumedPoints:consumed,expireAt:msDuration>0?Date.now()+resTtlMs:null};}async _get(rlKey){if(!this._isRedisReady(rlKey,true)){throw new RedisConnectionError('Redis connection is not ready');}const entry=await this.adapter.getWithTtl(rlKey);if(!entry){return null;}const consumedPoints=Number(entry.value);if(!Number.isFinite(consumedPoints)){return null;}return{consumedPoints,expireAt:entry.ttlMs>0?Date.now()+entry.ttlMs:null};}async _delete(rlKey){return this.adapter.del(rlKey);}}export default RedisLimiter;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{rateLimit,createLimiter}from'./rateLimit.js';import{RateLimitManager,createRateLimiters}from'./rateLimitManager.js';import{BaseStore}from'./stores/baseStore.js';import{MemoryStore}from'./stores/memoryStore.js';import{RedisStore}from'./stores/redisStore.js';import{BaseAlgorithm}from'./algorithms/baseAlgorithm.js';import{FixedWindow}from'./algorithms/fixedWindow.js';import{SlidingWindowLog}from'./algorithms/slidingWindowLog.js';import{SlidingWindowCounter}from'./algorithms/slidingWindowCounter.js';import{TokenBucket}from'./algorithms/tokenBucket.js';import{LeakyBucket}from'./algorithms/leakyBucket.js';import*as errors from'./utils/errors.js';import{defaultKeyGenerator,ipKeyGenerator,getEndpoint,normalizeIp,graphQLKeyGenerator}from'./utils/keyGenerator.js';import{extractGraphQLRootFieldFast}from'./utils/graphQLParser.js';import{validateIpRangeV4,validateSecondaryIps,compileWhitelist,isIpInWhitelist,cleanIp,ipToInt}from'./utils/ipUtils.js';import{evaluateSkipRule,checkHeaderCriteria,checkOriginCriteria,checkIpCriteria,checkKeyCriteria}from'./utils/skipUtils.js';import{RateLimiter}from'./core/rateLimiter.js';import{RateLimiterRes}from'./core/rateLimiterRes.js';import{RateLimiterStore}from'./core/rateLimiterStore.js';import{BlockedKeys}from'./core/blockedKeys.js';import{createCoreMiddleware}from'./core/coreMiddleware.js';import{MemoryLimiter}from'./core/limiters/memoryLimiter.js';import{RedisLimiter}from'./core/limiters/redisLimiter.js';import{ValkeyLimiter}from'./core/limiters/valkeyLimiter.js';import{ClusterLimiter,RedisClusterLimiter}from'./core/limiters/clusterLimiter.js';import{RateLimiterCluster,RateLimiterClusterMaster,RateLimiterClusterMasterPM2}from'./core/limiters/nodeClusterLimiter.js';import{MongoLimiter}from'./core/limiters/mongoLimiter.js';import{MongooseLimiter}from'./core/limiters/mongooseLimiter.js';import{DynamoDbLimiter}from'./core/limiters/dynamoDbLimiter.js';import{MySqlLimiter}from'./core/limiters/mySqlLimiter.js';import{PostgresLimiter}from'./core/limiters/postgresLimiter.js';import{SqliteLimiter}from'./core/limiters/sqliteLimiter.js';import{PrismaLimiter}from'./core/limiters/prismaLimiter.js';import{MemcachedLimiter}from'./core/limiters/memcachedLimiter.js';import{EtcdLimiter}from'./core/limiters/etcdLimiter.js';import{RateLimiterQueue,RateLimiterQueueError}from'./core/wrappers/rateLimiterQueue.js';import{RateLimiterUnion}from'./core/wrappers/rateLimiterUnion.js';import{BurstyRateLimiter}from'./core/wrappers/burstyRateLimiter.js';import{BlackAndWhiteLimiter}from'./core/wrappers/blackAndWhiteLimiter.js';import{TimeoutLimiter}from'./core/wrappers/timeoutLimiter.js';import{RedisClientAdapter,detectRedisClientKind}from'./utils/redisClient.js';const RateLimiterMemory=MemoryLimiter;const RateLimiterRedis=RedisLimiter;const RateLimiterMongo=MongoLimiter;const RateLimiterMongoose=MongooseLimiter;const RateLimiterMySQL=MySqlLimiter;const RateLimiterPostgres=PostgresLimiter;const RateLimiterSQLite=SqliteLimiter;const RateLimiterPrisma=PrismaLimiter;const RateLimiterMemcache=MemcachedLimiter;const RateLimiterMemcached=MemcachedLimiter;const RateLimiterDynamo=DynamoDbLimiter;const RateLimiterDynamoDb=DynamoDbLimiter;const RateLimiterValkey=ValkeyLimiter;const RateLimiterEtcd=EtcdLimiter;const RateLimiterAbstract=RateLimiter;const RateLimiterStoreAbstract=RateLimiterStore;const RateLimiterBlackAndWhite=BlackAndWhiteLimiter;export{rateLimit,rateLimit as default,createLimiter,RateLimitManager,createRateLimiters,BaseStore,MemoryStore,RedisStore,BaseAlgorithm,FixedWindow,SlidingWindowLog,SlidingWindowCounter,TokenBucket,LeakyBucket,errors,defaultKeyGenerator,ipKeyGenerator,getEndpoint,normalizeIp,graphQLKeyGenerator,extractGraphQLRootFieldFast,validateIpRangeV4,validateSecondaryIps,compileWhitelist,isIpInWhitelist,cleanIp,ipToInt,evaluateSkipRule,checkHeaderCriteria,checkOriginCriteria,checkIpCriteria,checkKeyCriteria,RateLimiter,RateLimiterRes,RateLimiterStore,BlockedKeys,createCoreMiddleware,MemoryLimiter,RedisLimiter,ValkeyLimiter,ClusterLimiter,RedisClusterLimiter,RateLimiterCluster,RateLimiterClusterMaster,RateLimiterClusterMasterPM2,MongoLimiter,MongooseLimiter,DynamoDbLimiter,MySqlLimiter,PostgresLimiter,SqliteLimiter,PrismaLimiter,MemcachedLimiter,EtcdLimiter,RateLimiterMemory,RateLimiterRedis,RateLimiterMongo,RateLimiterMongoose,RateLimiterMySQL,RateLimiterPostgres,RateLimiterSQLite,RateLimiterPrisma,RateLimiterMemcache,RateLimiterMemcached,RateLimiterDynamo,RateLimiterDynamoDb,RateLimiterValkey,RateLimiterEtcd,RateLimiterAbstract,RateLimiterStoreAbstract,RateLimiterBlackAndWhite,RateLimiterQueue,RateLimiterQueueError,RateLimiterUnion,BurstyRateLimiter,BlackAndWhiteLimiter,TimeoutLimiter,RedisClientAdapter,detectRedisClientKind};export*from'./utils/errors.js';
|
|
1
|
+
import{rateLimit,createLimiter}from'./rateLimit.js';import{RateLimitManager,createRateLimiters}from'./rateLimitManager.js';import{BaseStore}from'./stores/baseStore.js';import{MemoryStore}from'./stores/memoryStore.js';import{RedisStore}from'./stores/redisStore.js';import{BaseAlgorithm}from'./algorithms/baseAlgorithm.js';import{FixedWindow}from'./algorithms/fixedWindow.js';import{SlidingWindowLog}from'./algorithms/slidingWindowLog.js';import{SlidingWindowCounter}from'./algorithms/slidingWindowCounter.js';import{TokenBucket}from'./algorithms/tokenBucket.js';import{LeakyBucket}from'./algorithms/leakyBucket.js';import*as errors from'./utils/errors.js';import{defaultKeyGenerator,ipKeyGenerator,getEndpoint,normalizeIp,graphQLKeyGenerator}from'./utils/keyGenerator.js';import{extractGraphQLRootFieldFast}from'./utils/graphQLParser.js';import{validateIpRangeV4,validateSecondaryIps,compileWhitelist,isIpInWhitelist,cleanIp,ipToInt}from'./utils/ipUtils.js';import{evaluateSkipRule,checkHeaderCriteria,checkOriginCriteria,checkIpCriteria,checkKeyCriteria}from'./utils/skipUtils.js';import{RateLimiter}from'./core/rateLimiter.js';import{RateLimiterRes}from'./core/rateLimiterRes.js';import{RateLimiterStore}from'./core/rateLimiterStore.js';import{BlockedKeys}from'./core/blockedKeys.js';import{createCoreMiddleware}from'./core/coreMiddleware.js';import{MemoryLimiter}from'./core/limiters/memoryLimiter.js';import{RedisLimiter}from'./core/limiters/redisLimiter.js';import{ValkeyLimiter}from'./core/limiters/valkeyLimiter.js';import{ClusterLimiter,RedisClusterLimiter,RateLimiterRedisCluster}from'./core/limiters/clusterLimiter.js';import{RateLimiterCluster,RateLimiterClusterMaster,RateLimiterClusterMasterPM2}from'./core/limiters/nodeClusterLimiter.js';import{MongoLimiter}from'./core/limiters/mongoLimiter.js';import{MongooseLimiter}from'./core/limiters/mongooseLimiter.js';import{DynamoDbLimiter}from'./core/limiters/dynamoDbLimiter.js';import{MySqlLimiter}from'./core/limiters/mySqlLimiter.js';import{PostgresLimiter}from'./core/limiters/postgresLimiter.js';import{SqliteLimiter}from'./core/limiters/sqliteLimiter.js';import{PrismaLimiter}from'./core/limiters/prismaLimiter.js';import{MemcachedLimiter}from'./core/limiters/memcachedLimiter.js';import{EtcdLimiter}from'./core/limiters/etcdLimiter.js';import{RateLimiterQueue,RateLimiterQueueError}from'./core/wrappers/rateLimiterQueue.js';import{RateLimiterUnion}from'./core/wrappers/rateLimiterUnion.js';import{BurstyRateLimiter}from'./core/wrappers/burstyRateLimiter.js';import{BlackAndWhiteLimiter}from'./core/wrappers/blackAndWhiteLimiter.js';import{TimeoutLimiter}from'./core/wrappers/timeoutLimiter.js';import{RedisClientAdapter,detectRedisClientKind}from'./utils/redisClient.js';const RateLimiterMemory=MemoryLimiter;const RateLimiterRedis=RedisLimiter;const RateLimiterMongo=MongoLimiter;const RateLimiterMongoose=MongooseLimiter;const RateLimiterMySQL=MySqlLimiter;const RateLimiterPostgres=PostgresLimiter;const RateLimiterSQLite=SqliteLimiter;const RateLimiterPrisma=PrismaLimiter;const RateLimiterMemcache=MemcachedLimiter;const RateLimiterMemcached=MemcachedLimiter;const RateLimiterDynamo=DynamoDbLimiter;const RateLimiterDynamoDb=DynamoDbLimiter;const RateLimiterValkey=ValkeyLimiter;const RateLimiterEtcd=EtcdLimiter;const RateLimiterAbstract=RateLimiter;const RateLimiterStoreAbstract=RateLimiterStore;const RateLimiterBlackAndWhite=BlackAndWhiteLimiter;export{rateLimit,rateLimit as default,createLimiter,RateLimitManager,createRateLimiters,BaseStore,MemoryStore,RedisStore,BaseAlgorithm,FixedWindow,SlidingWindowLog,SlidingWindowCounter,TokenBucket,LeakyBucket,errors,defaultKeyGenerator,ipKeyGenerator,getEndpoint,normalizeIp,graphQLKeyGenerator,extractGraphQLRootFieldFast,validateIpRangeV4,validateSecondaryIps,compileWhitelist,isIpInWhitelist,cleanIp,ipToInt,evaluateSkipRule,checkHeaderCriteria,checkOriginCriteria,checkIpCriteria,checkKeyCriteria,RateLimiter,RateLimiterRes,RateLimiterStore,BlockedKeys,createCoreMiddleware,MemoryLimiter,RedisLimiter,ValkeyLimiter,ClusterLimiter,RedisClusterLimiter,RateLimiterRedisCluster,RateLimiterCluster,RateLimiterClusterMaster,RateLimiterClusterMasterPM2,MongoLimiter,MongooseLimiter,DynamoDbLimiter,MySqlLimiter,PostgresLimiter,SqliteLimiter,PrismaLimiter,MemcachedLimiter,EtcdLimiter,RateLimiterMemory,RateLimiterRedis,RateLimiterMongo,RateLimiterMongoose,RateLimiterMySQL,RateLimiterPostgres,RateLimiterSQLite,RateLimiterPrisma,RateLimiterMemcache,RateLimiterMemcached,RateLimiterDynamo,RateLimiterDynamoDb,RateLimiterValkey,RateLimiterEtcd,RateLimiterAbstract,RateLimiterStoreAbstract,RateLimiterBlackAndWhite,RateLimiterQueue,RateLimiterQueueError,RateLimiterUnion,BurstyRateLimiter,BlackAndWhiteLimiter,TimeoutLimiter,RedisClientAdapter,detectRedisClientKind};export*from'./utils/errors.js';
|
package/dist/rateLimit.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{validateOptions}from'./utils/validation.js';import{MemoryStore}from'./stores/memoryStore.js';import{RedisStore}from'./stores/redisStore.js';import{FixedWindow}from'./algorithms/fixedWindow.js';import{SlidingWindowLog}from'./algorithms/slidingWindowLog.js';import{SlidingWindowCounter}from'./algorithms/slidingWindowCounter.js';import{TokenBucket}from'./algorithms/tokenBucket.js';import{LeakyBucket}from'./algorithms/leakyBucket.js';import{createMiddleware}from'./middleware/expressMiddleware.js';const ALGORITHM_MAP={'fixed-window':FixedWindow,'sliding-window-log':SlidingWindowLog,'sliding-window-counter':SlidingWindowCounter,'token-bucket':TokenBucket,'leaky-bucket':LeakyBucket};export function rateLimit(rawOptions={}){const options=validateOptions(rawOptions);let store;if(typeof options.store==='string'){
|
|
1
|
+
import{validateOptions}from'./utils/validation.js';import{MemoryStore}from'./stores/memoryStore.js';import{RedisStore}from'./stores/redisStore.js';import{FixedWindow}from'./algorithms/fixedWindow.js';import{SlidingWindowLog}from'./algorithms/slidingWindowLog.js';import{SlidingWindowCounter}from'./algorithms/slidingWindowCounter.js';import{TokenBucket}from'./algorithms/tokenBucket.js';import{LeakyBucket}from'./algorithms/leakyBucket.js';import{createMiddleware}from'./middleware/expressMiddleware.js';const ALGORITHM_MAP={'fixed-window':FixedWindow,'sliding-window-log':SlidingWindowLog,'sliding-window-counter':SlidingWindowCounter,'token-bucket':TokenBucket,'leaky-bucket':LeakyBucket};export function rateLimit(rawOptions={}){const options=validateOptions(rawOptions);let store;if(typeof options.store==='string'){const s=options.store.toLowerCase();if(s==='redis'||s==='redis-cluster'||s==='cluster'){const isClusterStore=s==='redis-cluster'||s==='cluster';store=new RedisStore({...options,isCluster:options.isCluster!==undefined?options.isCluster:isClusterStore,hashTag:options.hashTag!==undefined?options.hashTag:isClusterStore?true:undefined,redis:options.redis||{}});}else{store=new MemoryStore(options);}}else if(options.store){store=options.store;}else if(options.client||options.redisClient||options.clusterClient||options.redisCluster||options.cluster||options.clusters||options.clusterNodes){store=new RedisStore(options);}else{store=new MemoryStore(options);}const AlgorithmClass=ALGORITHM_MAP[options.algorithm];const algorithm=new AlgorithmClass(options,store);return createMiddleware(algorithm,options);}export const createLimiter=rateLimit;export default rateLimit;
|
package/dist/rateLimitManager.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{rateLimit}from'./rateLimit.js';import{RedisStore}from'./stores/redisStore.js';import{graphQLKeyGenerator}from'./utils/keyGenerator.js';import{validateIpRangeV4,validateSecondaryIps,compileWhitelist}from'./utils/ipUtils.js';import{evaluateSkipRule}from'./utils/skipUtils.js';export class RateLimitManager{constructor(options={}){this.redisClient=options.redisClient||options.client||null;this.isRedisEnabled=options.isRedisEnabled!==undefined?Boolean(options.isRedisEnabled):Boolean(this.redisClient);this.excludedPaths=options.excludedPaths||['/health','/metrics','/favicon.ico'];this.whitelistIps=options.whitelistIps||process.env['Platform.WHITELIST_IPS']||null;this.compiledWhitelist=this.whitelistIps?compileWhitelist(this.whitelistIps):null;this.customSkip=options.customSkip!==undefined?options.customSkip:options.customskip!==undefined?options.customskip:options.skip!==undefined?options.skip:null;this.trustProxy=options.trustProxy!==undefined?Boolean(options.trustProxy):undefined;this.defaultConfig={algorithm:options.algorithm||'sliding-window-counter',windowMs:options.windowMs||60000,standardHeaders:options.standardHeaders!==undefined?options.standardHeaders:true,legacyHeaders:options.legacyHeaders!==undefined?options.legacyHeaders:false,passOnStoreError:true};}getClientIp(req){return req.ip||req.socket&&req.socket.remoteAddress||'127.0.0.1';}generateKey(req){return graphQLKeyGenerator(req);}validateIpRangeV4(ipAddr){return validateIpRangeV4(ipAddr,this.compiledWhitelist??this.whitelistIps);}validateSecondaryIps(req){return validateSecondaryIps(req,this.compiledWhitelist??this.whitelistIps,{trustProxy:this.trustProxy});}async evaluateSkip(skipRule,req,res){return evaluateSkipRule(skipRule,req,res,{generateKey:r=>this.generateKey(r),getClientIp:r=>this.getClientIp(r),trustProxy:this.trustProxy});}async skip(req,res,customSkipParam){if(this.excludedPaths.includes(req.path)){return true;}if(this.validateSecondaryIps(req)){return true;}if(this.validateIpRangeV4(this.getClientIp(req))){return true;}const skipRule=customSkipParam!==undefined?customSkipParam:this.customSkip;if(skipRule){const skipped=await this.evaluateSkip(skipRule,req,res);if(skipped)return true;}return false;}createRateLimiter(options={}){const config={...this.defaultConfig,...options};const prefix=config._storePrefix||config.storePrefix||config.keyPrefix||'rl:global:';let store='memory';if(config.store){store=config.store;}else if(this.isRedisEnabled&&this.redisClient){store=new RedisStore({client:this.redisClient,keyPrefix:prefix,passOnStoreError:config.passOnStoreError!==undefined?config.passOnStoreError:true,failOpen:config.failOpen!==undefined?config.failOpen:true});}const customSkip=options.customSkip!==undefined?options.customSkip:options.customskip!==undefined?options.customskip:options.skip!==undefined?options.skip:this.customSkip;const limiterOptions={keyGenerator:req=>this.generateKey(req),skip:(req,res)=>this.skip(req,res,customSkip),store,handler:(req,res)=>{res.status(config.statusCode||429).json({error:config.errorType||'Rate limit exceeded',message:config.message||'Too many requests, please try again later.',limit:req.rateLimit?.limit,remaining:req.rateLimit?.remaining,resetAt:req.rateLimit?.resetTime?.toLocaleString()});},...config};delete limiterOptions._storePrefix;delete limiterOptions.storePrefix;return rateLimit(limiterOptions);}createGlobalLimiter(options={}){return this.createRateLimiter({algorithm:'sliding-window-counter',_storePrefix:'rl:global:',max:100,windowMs:60000,message:'Too many requests, please try again later.',...options});}createAuthLimiter(options={}){return this.createRateLimiter({algorithm:'token-bucket',_storePrefix:'rl:auth:',capacity:10,refillRate:1,refillInterval:30000,message:'Too many authentication attempts, please try again later.',...options});}static async getAllLimiters(options={}){const manager=new RateLimitManager(options);return{manager,globalRateLimiter:manager.createGlobalLimiter(options.global),authRateLimiter:manager.createAuthLimiter(options.auth)};}}export async function createRateLimiters(options={}){return RateLimitManager.getAllLimiters(options);}
|
|
1
|
+
import{rateLimit}from'./rateLimit.js';import{RedisStore}from'./stores/redisStore.js';import{graphQLKeyGenerator}from'./utils/keyGenerator.js';import{validateIpRangeV4,validateSecondaryIps,compileWhitelist}from'./utils/ipUtils.js';import{evaluateSkipRule}from'./utils/skipUtils.js';export class RateLimitManager{constructor(options={}){this.redisClient=options.redisClient||options.client||options.clusterClient||options.redisCluster||null;this.isRedisEnabled=options.isRedisEnabled!==undefined?Boolean(options.isRedisEnabled):Boolean(this.redisClient);this.excludedPaths=options.excludedPaths||['/health','/metrics','/favicon.ico'];this.whitelistIps=options.whitelistIps||process.env['Platform.WHITELIST_IPS']||null;this.compiledWhitelist=this.whitelistIps?compileWhitelist(this.whitelistIps):null;this.customSkip=options.customSkip!==undefined?options.customSkip:options.customskip!==undefined?options.customskip:options.skip!==undefined?options.skip:null;this.trustProxy=options.trustProxy!==undefined?Boolean(options.trustProxy):undefined;this.defaultConfig={algorithm:options.algorithm||'sliding-window-counter',windowMs:options.windowMs||60000,standardHeaders:options.standardHeaders!==undefined?options.standardHeaders:true,legacyHeaders:options.legacyHeaders!==undefined?options.legacyHeaders:false,passOnStoreError:true};}getClientIp(req){return req.ip||req.socket&&req.socket.remoteAddress||'127.0.0.1';}generateKey(req){return graphQLKeyGenerator(req);}validateIpRangeV4(ipAddr){return validateIpRangeV4(ipAddr,this.compiledWhitelist??this.whitelistIps);}validateSecondaryIps(req){return validateSecondaryIps(req,this.compiledWhitelist??this.whitelistIps,{trustProxy:this.trustProxy});}async evaluateSkip(skipRule,req,res){return evaluateSkipRule(skipRule,req,res,{generateKey:r=>this.generateKey(r),getClientIp:r=>this.getClientIp(r),trustProxy:this.trustProxy});}async skip(req,res,customSkipParam){if(this.excludedPaths.includes(req.path)){return true;}if(this.validateSecondaryIps(req)){return true;}if(this.validateIpRangeV4(this.getClientIp(req))){return true;}const skipRule=customSkipParam!==undefined?customSkipParam:this.customSkip;if(skipRule){const skipped=await this.evaluateSkip(skipRule,req,res);if(skipped)return true;}return false;}createRateLimiter(options={}){const config={...this.defaultConfig,...options};const prefix=config._storePrefix||config.storePrefix||config.keyPrefix||'rl:global:';let store='memory';if(config.store){store=config.store;}else if(this.isRedisEnabled&&this.redisClient){store=new RedisStore({client:this.redisClient,keyPrefix:prefix,isCluster:config.isCluster!==undefined?config.isCluster:undefined,hashTag:config.hashTag!==undefined?config.hashTag:undefined,passOnStoreError:config.passOnStoreError!==undefined?config.passOnStoreError:true,failOpen:config.failOpen!==undefined?config.failOpen:true});}const customSkip=options.customSkip!==undefined?options.customSkip:options.customskip!==undefined?options.customskip:options.skip!==undefined?options.skip:this.customSkip;const limiterOptions={keyGenerator:req=>this.generateKey(req),skip:(req,res)=>this.skip(req,res,customSkip),store,handler:(req,res)=>{res.status(config.statusCode||429).json({error:config.errorType||'Rate limit exceeded',message:config.message||'Too many requests, please try again later.',limit:req.rateLimit?.limit,remaining:req.rateLimit?.remaining,resetAt:req.rateLimit?.resetTime?.toLocaleString()});},...config};delete limiterOptions._storePrefix;delete limiterOptions.storePrefix;return rateLimit(limiterOptions);}createGlobalLimiter(options={}){return this.createRateLimiter({algorithm:'sliding-window-counter',_storePrefix:'rl:global:',max:100,windowMs:60000,message:'Too many requests, please try again later.',...options});}createAuthLimiter(options={}){return this.createRateLimiter({algorithm:'token-bucket',_storePrefix:'rl:auth:',capacity:10,refillRate:1,refillInterval:30000,message:'Too many authentication attempts, please try again later.',...options});}static async getAllLimiters(options={}){const manager=new RateLimitManager(options);return{manager,globalRateLimiter:manager.createGlobalLimiter(options.global),authRateLimiter:manager.createAuthLimiter(options.auth)};}}export async function createRateLimiters(options={}){return RateLimitManager.getAllLimiters(options);}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createRequire}from'module';import{BaseStore}from'./baseStore.js';import{RedisConnectionError}from'../utils/errors.js';import{RedisClientAdapter}from'../utils/redisClient.js';import{SCRIPTS,SCRIPT_SHAS}from'./luaScripts.js';const require=createRequire(import.meta.url);export class RedisStore extends BaseStore{constructor(options={}){super();this.keyPrefix=options.keyPrefix||options.prefix||options.storePrefix||options._storePrefix||'rl:';this.failOpen=Boolean(options.failOpen||options.passOnStoreError);this.scripts=SCRIPTS;this.scriptShas=SCRIPT_SHAS;this.isInternalClient=false;this.isConnected=false;this._initClient(options);this.isMock=Boolean(options.isMock||this.client?.data instanceof Map||this.client?.constructor?.name==='RedisMock'||this.client?.constructor?.name==='_RedisMock');this.useRedisTime=options.useRedisTime!==undefined?Boolean(options.useRedisTime):!this.isMock;}getNow(){return this.useRedisTime?0:Date.now();}_initClient(options){let client=options.client||(options.redis&&typeof options.redis.eval==='function'?options.redis:null)||(options.redis&&typeof options.redis.sendCommand==='function'?options.redis:null)||options.redis&&options.redis.client||null;if(!client&&typeof options.sendCommand==='function'){client={sendCommand:options.sendCommand};}if(!client){const redisOptions=typeof options.redis==='object'&&options.redis!==null?{...options.redis}:{};const url=options.url||redisOptions.url;let Redis=options.Redis||redisOptions.Redis;if(!Redis){try{const ioredis=require('ioredis');Redis=ioredis.default||ioredis;}catch{}}if(Redis&&typeof Redis==='function'){try{client=url?new Redis(url,redisOptions):new Redis(redisOptions);this.isInternalClient=true;}catch{client=null;}}if(!client){try{const redisPkg=require('redis');const createClient=redisPkg.createClient||redisPkg.default?.createClient;if(typeof createClient==='function'){const clientOpts=url?{url,...redisOptions}:redisOptions;client=createClient(clientOpts);if(typeof client.connect==='function'){client.connect().catch(err=>{this.isConnected=false;if(typeof options.onError==='function'){options.onError(new RedisConnectionError(err.message,err));}});}this.isInternalClient=true;}}catch{}}}this.client=client;this.adapter=new RedisClientAdapter(client);if(this.client&&typeof this.client.on==='function'){this.client.on('connect',()=>{this.isConnected=true;});this.client.on('ready',()=>{this.isConnected=true;});this.client.on('error',err=>{this.isConnected=false;if(typeof options.onError==='function'){options.onError(new RedisConnectionError(err.message,err));}});this.client.on('close',()=>{this.isConnected=false;});this.client.on('end',()=>{this.isConnected=false;});}if(this.client&&(this.client.status==='ready'||this.client.status==='connect'||this.client.isReady||this.client.isOpen)){this.isConnected=true;}}_prefixKey(key){
|
|
1
|
+
import{createRequire}from'module';import{BaseStore}from'./baseStore.js';import{RedisConnectionError}from'../utils/errors.js';import{RedisClientAdapter}from'../utils/redisClient.js';import{SCRIPTS,SCRIPT_SHAS}from'./luaScripts.js';const require=createRequire(import.meta.url);export class RedisStore extends BaseStore{constructor(options={}){super();this.keyPrefix=options.keyPrefix||options.prefix||options.storePrefix||options._storePrefix||'rl:';this.failOpen=Boolean(options.failOpen||options.passOnStoreError);this.scripts=SCRIPTS;this.scriptShas=SCRIPT_SHAS;this.isInternalClient=false;this.isConnected=false;this._initClient(options);this.isCluster=Boolean(options.isCluster||this.adapter?.isCluster||this.client?.isCluster||this.client?.constructor?.name==='Cluster'||this.client?.constructor?.name==='RedisCluster');this.hashTag=options.hashTag!==undefined?Boolean(options.hashTag):Boolean(this.isCluster);this.isMock=Boolean(options.isMock||this.client?.data instanceof Map||this.client?.constructor?.name==='RedisMock'||this.client?.constructor?.name==='_RedisMock');this.useRedisTime=options.useRedisTime!==undefined?Boolean(options.useRedisTime):!this.isMock;}getNow(){return this.useRedisTime?0:Date.now();}_initClient(options){let client=options.client||options.redisClient||options.storeClient||options.clusterClient||options.redisCluster||(options.redis&&typeof options.redis.eval==='function'?options.redis:null)||(options.redis&&typeof options.redis.sendCommand==='function'?options.redis:null)||options.redis&&options.redis.client||null;if(!client&&typeof options.sendCommand==='function'){client={sendCommand:options.sendCommand};}if(!client){const redisOptions=typeof options.redis==='object'&&options.redis!==null?{...options.redis}:{};const url=options.url||redisOptions.url;let Redis=options.Redis||redisOptions.Redis;if(!Redis){try{const ioredis=require('ioredis');Redis=ioredis.default||ioredis;}catch{}}const clusterNodes=options.cluster||options.clusters||options.clusterNodes||options.rootNodes||redisOptions.cluster||redisOptions.clusters||redisOptions.clusterNodes||redisOptions.rootNodes||null;if(clusterNodes){const clusterOptions=options.clusterOptions||redisOptions.clusterOptions||redisOptions;if(Redis&&typeof Redis.Cluster==='function'){try{client=new Redis.Cluster(clusterNodes,clusterOptions);this.isInternalClient=true;}catch{client=null;}}if(!client){try{const redisPkg=require('redis');const createCluster=redisPkg.createCluster||redisPkg.default?.createCluster;if(typeof createCluster==='function'){const rootNodes=Array.isArray(clusterNodes)?clusterNodes.map(n=>typeof n==='string'?{url:n}:n.url?n:{url:`redis://${n.host||'127.0.0.1'}:${n.port||6379}`}):clusterNodes;client=createCluster({rootNodes,...clusterOptions});if(typeof client.connect==='function'){client.connect().catch(err=>{this.isConnected=false;if(typeof options.onError==='function'){options.onError(new RedisConnectionError(err.message,err));}});}this.isInternalClient=true;}}catch{}}}if(!client&&Redis&&typeof Redis==='function'){try{client=url?new Redis(url,redisOptions):new Redis(redisOptions);this.isInternalClient=true;}catch{client=null;}}if(!client){try{const redisPkg=require('redis');const createClient=redisPkg.createClient||redisPkg.default?.createClient;if(typeof createClient==='function'){const clientOpts=url?{url,...redisOptions}:redisOptions;client=createClient(clientOpts);if(typeof client.connect==='function'){client.connect().catch(err=>{this.isConnected=false;if(typeof options.onError==='function'){options.onError(new RedisConnectionError(err.message,err));}});}this.isInternalClient=true;}}catch{}}}this.client=client;this.adapter=new RedisClientAdapter(client,{isCluster:options.isCluster!==undefined?options.isCluster:undefined});if(this.client&&typeof this.client.on==='function'){this.client.on('connect',()=>{this.isConnected=true;});this.client.on('ready',()=>{this.isConnected=true;});this.client.on('error',err=>{this.isConnected=false;if(typeof options.onError==='function'){options.onError(new RedisConnectionError(err.message,err));}});this.client.on('close',()=>{this.isConnected=false;});this.client.on('end',()=>{this.isConnected=false;});}if(this.client&&(this.client.status==='ready'||this.client.status==='connect'||this.client.isReady||this.client.isOpen)){this.isConnected=true;}}_prefixKey(key){const prefixed=`${this.keyPrefix}${key}`;if(!this.hashTag)return prefixed;if(prefixed.includes('{'))return prefixed;return`{${prefixed}}`;}async runScript(scriptName,keys=[],args=[]){const script=this.scripts[scriptName];if(!script){throw new Error(`Lua script "${scriptName}" not found`);}if(!this.client){throw new RedisConnectionError('Redis client is not configured or available. Please install "ioredis" or "redis", or provide an active client instance via the "client" option.');}const prefixedKeys=keys.map(k=>this._prefixKey(k));const stringArgs=args.map(a=>String(a));try{return await this.adapter.evalScript(script,prefixedKeys,stringArgs);}catch(err){throw new RedisConnectionError(`Redis script execution error: ${err.message}`,err);}}async get(key){try{const fullKey=this._prefixKey(key);let res;if(typeof this.client?.get==='function'){res=await this.client.get(fullKey);}else if(typeof this.client?.sendCommand==='function'){res=await this.adapter.sendCommand(['GET',fullKey]);}if(!res)return null;try{return JSON.parse(res);}catch{return res;}}catch(err){throw new RedisConnectionError(`Redis get error: ${err.message}`,err);}}async set(key,value,ttlMs){try{const fullKey=this._prefixKey(key);const stringValue=typeof value==='object'?JSON.stringify(value):String(value);await this.adapter.setWithTtl(fullKey,stringValue,ttlMs);return true;}catch(err){throw new RedisConnectionError(`Redis set error: ${err.message}`,err);}}async delete(key){try{const fullKey=this._prefixKey(key);return this.adapter.del(fullKey);}catch(err){throw new RedisConnectionError(`Redis del error: ${err.message}`,err);}}async clear(){try{const pattern=`${this.keyPrefix}*`;const keys=await this.adapter.scanKeys(pattern);if(keys&&keys.length>0){await this.adapter.delMany(keys);}}catch(err){throw new RedisConnectionError(`Redis clear error: ${err.message}`,err);}}async close(){if(this.isInternalClient&&this.client){if(typeof this.client.quit==='function'){await this.client.quit();}else if(typeof this.client.disconnect==='function'){this.client.disconnect();}}}}export default RedisStore;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -63,7 +63,7 @@ export interface SkipRuleObject {
|
|
|
63
63
|
export type SkipOption = SkipFunction | SkipRuleObject | Array<SkipFunction | SkipRuleObject>;
|
|
64
64
|
|
|
65
65
|
export interface BaseOptions {
|
|
66
|
-
store?: 'memory' | 'redis' | BaseStore;
|
|
66
|
+
store?: 'memory' | 'redis' | 'redis-cluster' | 'cluster' | BaseStore;
|
|
67
67
|
keyPrefix?: string;
|
|
68
68
|
prefix?: string;
|
|
69
69
|
storePrefix?: string;
|
|
@@ -166,6 +166,17 @@ export declare class MemoryStore extends BaseStore {
|
|
|
166
166
|
export declare class RedisStore extends BaseStore {
|
|
167
167
|
constructor(options?: {
|
|
168
168
|
client?: any;
|
|
169
|
+
redisClient?: any;
|
|
170
|
+
storeClient?: any;
|
|
171
|
+
clusterClient?: any;
|
|
172
|
+
redisCluster?: any;
|
|
173
|
+
cluster?: any;
|
|
174
|
+
clusters?: any;
|
|
175
|
+
clusterNodes?: any;
|
|
176
|
+
rootNodes?: any;
|
|
177
|
+
clusterOptions?: any;
|
|
178
|
+
isCluster?: boolean;
|
|
179
|
+
hashTag?: boolean;
|
|
169
180
|
sendCommand?: (...args: any[]) => Promise<any>;
|
|
170
181
|
url?: string;
|
|
171
182
|
keyPrefix?: string;
|
|
@@ -444,6 +455,10 @@ export interface RedisLimiterOptions extends RateLimiterOptions {
|
|
|
444
455
|
clientKind?: string;
|
|
445
456
|
/** Reject requests while the connection is not ready. */
|
|
446
457
|
rejectIfRedisNotReady?: boolean;
|
|
458
|
+
/** Wrap storage keys in a hash tag so they share a Redis Cluster slot. */
|
|
459
|
+
hashTag?: boolean;
|
|
460
|
+
/** Explicitly flag whether this client is connected to a Redis Cluster. */
|
|
461
|
+
isCluster?: boolean;
|
|
447
462
|
customIncrTtlLuaScript?: string;
|
|
448
463
|
customSetTtlLuaScript?: string;
|
|
449
464
|
}
|
|
@@ -461,12 +476,14 @@ export interface ClusterLimiterOptions extends RedisLimiterOptions {
|
|
|
461
476
|
hashTag?: boolean;
|
|
462
477
|
}
|
|
463
478
|
|
|
464
|
-
export declare class ClusterLimiter extends
|
|
479
|
+
export declare class ClusterLimiter extends RedisLimiter {
|
|
465
480
|
constructor(options: ClusterLimiterOptions);
|
|
466
481
|
}
|
|
467
482
|
|
|
468
483
|
export declare const RedisClusterLimiter: typeof ClusterLimiter;
|
|
469
484
|
export type RedisClusterLimiter = ClusterLimiter;
|
|
485
|
+
export declare const RateLimiterRedisCluster: typeof ClusterLimiter;
|
|
486
|
+
export type RateLimiterRedisCluster = ClusterLimiter;
|
|
470
487
|
|
|
471
488
|
export declare class RateLimiterClusterMaster {
|
|
472
489
|
constructor(targetCluster?: any);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import crypto from'crypto';export const REDIS_CLIENT_KIND={IOREDIS:'ioredis',NODE_REDIS_V3:'node-redis-v3',NODE_REDIS_V4:'node-redis-v4',UNKNOWN:'unknown'};export function detectRedisClientKind(client){if(!client||typeof client!=='object'){return REDIS_CLIENT_KIND.UNKNOWN;}if(typeof client.isReady==='boolean'){return REDIS_CLIENT_KIND.NODE_REDIS_V4;}if(typeof client.status==='string'){return REDIS_CLIENT_KIND.IOREDIS;}if(typeof client.send_command==='function'&&typeof client.sendCommand!=='function'){return REDIS_CLIENT_KIND.NODE_REDIS_V3;}if((typeof client.ready==='boolean'||typeof client.connected==='boolean')&&typeof client.status!=='string'&&typeof client.isReady!=='boolean'){return REDIS_CLIENT_KIND.NODE_REDIS_V3;}if(typeof client.isReady==='function'){return REDIS_CLIENT_KIND.NODE_REDIS_V3;}if(typeof client.sendCommand==='function'){return REDIS_CLIENT_KIND.NODE_REDIS_V4;}return REDIS_CLIENT_KIND.UNKNOWN;}function invokeClientMethod(client,method,args){return new Promise((resolve,reject)=>{let settled=false;const cb=(err,res)=>{if(settled)return;settled=true;if(err)reject(err);else resolve(res);};try{const res=client[method](...args,cb);if(res&&typeof res.then==='function'){settled=true;res.then(resolve,reject);}}catch(err){if(!settled){settled=true;reject(err);}}});}export class RedisClientAdapter{constructor(client,options={}){this.client=client;this.kind=options.clientKind||detectRedisClientKind(client);this._scriptFallback=new Map();}usesObjectEval(){return this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V4;}static scriptSha(script){return crypto.createHash('sha1').update(script).digest('hex');}async evalScript(script,keys=[],args=[]){const stringArgs=args.map(a=>String(a));const sha=RedisClientAdapter.scriptSha(script);const useObjectForm=this.usesObjectEval();if(!this._scriptFallback.get(script)){if(typeof this.client.evalsha==='function'){try{return await this._callEvalsha(sha,keys,stringArgs,useObjectForm);}catch(err){if(!isNoScriptError(err)){throw err;}this._scriptFallback.set(script,true);}}else{this._scriptFallback.set(script,true);}}const res=await this._callEval(script,keys,stringArgs,useObjectForm);this._scriptFallback.delete(script);return res;}async _callEvalsha(sha,keys,stringArgs,useObjectForm){if(useObjectForm){return this.client.evalsha(sha,{keys,arguments:stringArgs});}return invokeClientMethod(this.client,'evalsha',[sha,keys.length,...keys,...stringArgs]);}async _callEval(script,keys,stringArgs,useObjectForm){if(typeof this.client.eval==='function'){if(useObjectForm){return this.client.eval(script,{keys,arguments:stringArgs});}return invokeClientMethod(this.client,'eval',[script,keys.length,...keys,...stringArgs]);}if(typeof this.client.sendCommand==='function'){const command=['EVAL',script,String(keys.length),...keys,...stringArgs];return this._sendCommand(command);}if(typeof this.client.send_command==='function'){return invokeClientMethod(this.client,'send_command',['EVAL',[script,keys.length,...keys,...stringArgs]]);}throw new Error('Redis client does not support eval, evalsha, or sendCommand');}async sendCommand(command){return this._sendCommand(command);}async _sendCommand(command){if(this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V4){return this.client.sendCommand(command);}if(typeof this.client.sendCommand==='function'){return this.client.sendCommand(...command);}if(typeof this.client.send_command==='function'){const[cmd,...args]=command;return invokeClientMethod(this.client,'send_command',[cmd,args]);}throw new Error('Redis client does not support sendCommand or send_command');}async getWithTtl(key){let value;if(typeof this.client.get==='function'){value=await invokeClientMethod(this.client,'get',[key]);}else if(typeof this.client.sendCommand==='function'||typeof this.client.send_command==='function'){value=await this._sendCommand(['GET',key]);}else{throw new Error('Redis client does not support get or sendCommand');}if(value===null||value===undefined){return null;}const ttlMs=await this.pttl(key);return{value,ttlMs};}async pttl(key){if(typeof this.client.pttl==='function'){return invokeClientMethod(this.client,'pttl',[key]);}if(typeof this.client.pTTL==='function'){return invokeClientMethod(this.client,'pTTL',[key]);}if(typeof this.client.sendCommand==='function'||typeof this.client.send_command==='function'){return this._sendCommand(['PTTL',key]);}throw new Error('Redis client does not support pttl or sendCommand');}async del(key){let count;if(typeof this.client.del==='function'){count=await invokeClientMethod(this.client,'del',[key]);}else if(typeof this.client.sendCommand==='function'||typeof this.client.send_command==='function'){count=await this._sendCommand(['DEL',key]);}else{throw new Error('Redis client does not support del or sendCommand');}return count>0;}async delMany(keys){if(!keys||keys.length===0){return 0;}if(typeof this.client.del==='function'){return this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V4?this.client.del(keys):invokeClientMethod(this.client,'del',keys);}if(typeof this.client.sendCommand==='function'||typeof this.client.send_command==='function'){return this._sendCommand(['DEL',...keys]);}throw new Error('Redis client does not support del or sendCommand');}async setWithTtl(key,value,ttlMs){const hasTtl=typeof ttlMs==='number'&&ttlMs>0;if(this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V4){if(typeof this.client.set==='function'){return hasTtl?this.client.set(key,value,{PX:ttlMs}):this.client.set(key,value);}if(typeof this.client.sendCommand==='function'){const cmd=hasTtl?['SET',key,value,'PX',String(ttlMs)]:['SET',key,value];return this._sendCommand(cmd);}}if(this.kind===REDIS_CLIENT_KIND.IOREDIS||this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V3){if(typeof this.client.set==='function'){if(!hasTtl){return invokeClientMethod(this.client,'set',[key,value]);}return invokeClientMethod(this.client,'set',[key,value,'PX',ttlMs]);}}if(typeof this.client.sendCommand==='function'||typeof this.client.send_command==='function'){const cmd=hasTtl?['SET',key,value,'PX',String(ttlMs)]:['SET',key,value];return this._sendCommand(cmd);}if(typeof this.client.set==='function'){if(!hasTtl){return invokeClientMethod(this.client,'set',[key,value]);}try{const res=await this.client.set(key,value,{PX:ttlMs});if(res!==undefined)return res;}catch{}return invokeClientMethod(this.client,'set',[key,value,'PX',ttlMs]);}throw new Error('Redis client does not support set or sendCommand');}async scanKeys(pattern){if(typeof this.client.scan==='function'){const keys=[];let cursor='0';do{let reply;if(this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V4){reply=await this.client.scan(cursor,{MATCH:pattern,COUNT:100});}else{reply=await invokeClientMethod(this.client,'scan',[cursor,'MATCH',pattern,'COUNT',100]);}if(Array.isArray(reply)){cursor=String(reply[0]);if(Array.isArray(reply[1])){keys.push(...reply[1]);}}else if(reply&&typeof reply==='object'){cursor=String(reply.cursor);if(Array.isArray(reply.keys)){keys.push(...reply.keys);}}else{break;}}while(cursor!=='0');return keys;}if(typeof this.client.sendCommand==='function'||typeof this.client.send_command==='function'){const keys=[];let cursor='0';do{const reply=await this._sendCommand(['SCAN',cursor,'MATCH',pattern,'COUNT','100']);if(Array.isArray(reply)){cursor=String(reply[0]);if(Array.isArray(reply[1])){keys.push(...reply[1]);}}else if(reply&&typeof reply==='object'){cursor=String(reply.cursor);if(Array.isArray(reply.keys)){keys.push(...reply.keys);}}else{break;}}while(cursor!=='0');return keys;}return[];}isReady(rlKey,isReadonly){if(this.kind===REDIS_CLIENT_KIND.IOREDIS){if(this.client.status){return this.client.status==='ready';}return true;}if(this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V4){if(typeof this.client.isOpen==='boolean'&&this.client.isOpen!==true){return false;}if(this.client._slots&&typeof this.client._slots.getClient==='function'){try{const slotClient=this.client._slots.getClient(rlKey,isReadonly);return Boolean(slotClient&&slotClient.isReady===true);}catch{return false;}}return this.client.isReady!==false;}if(this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V3){if(typeof this.client.isReady==='function'){return this.client.isReady();}if(typeof this.client.ready==='boolean'){return this.client.ready;}if(typeof this.client.connected==='boolean'){return this.client.connected;}}return true;}}export function isNoScriptError(err){return Boolean(err&&typeof err.message==='string'&&err.message.includes('NOSCRIPT'));}export default RedisClientAdapter;
|
|
1
|
+
import crypto from'crypto';export const REDIS_CLIENT_KIND={IOREDIS:'ioredis',NODE_REDIS_V3:'node-redis-v3',NODE_REDIS_V4:'node-redis-v4',UNKNOWN:'unknown'};export function detectRedisClientKind(client){if(!client||typeof client!=='object'){return REDIS_CLIENT_KIND.UNKNOWN;}if(typeof client.isReady==='boolean'){return REDIS_CLIENT_KIND.NODE_REDIS_V4;}if(typeof client.status==='string'){return REDIS_CLIENT_KIND.IOREDIS;}if(typeof client.send_command==='function'&&typeof client.sendCommand!=='function'){return REDIS_CLIENT_KIND.NODE_REDIS_V3;}if((typeof client.ready==='boolean'||typeof client.connected==='boolean')&&typeof client.status!=='string'&&typeof client.isReady!=='boolean'){return REDIS_CLIENT_KIND.NODE_REDIS_V3;}if(typeof client.isReady==='function'){return REDIS_CLIENT_KIND.NODE_REDIS_V3;}if(typeof client.sendCommand==='function'){return REDIS_CLIENT_KIND.NODE_REDIS_V4;}return REDIS_CLIENT_KIND.UNKNOWN;}function invokeClientMethod(client,method,args){return new Promise((resolve,reject)=>{let settled=false;const cb=(err,res)=>{if(settled)return;settled=true;if(err)reject(err);else resolve(res);};try{const res=client[method](...args,cb);if(res&&typeof res.then==='function'){settled=true;res.then(resolve,reject);}}catch(err){if(!settled){settled=true;reject(err);}}});}export class RedisClientAdapter{constructor(client,options={}){this.client=client;this.kind=options.clientKind||detectRedisClientKind(client);this._isCluster=options.isCluster!==undefined?Boolean(options.isCluster):undefined;this._scriptFallback=new Map();}get isCluster(){if(this._isCluster!==undefined)return this._isCluster;const c=this.client;if(!c||typeof c!=='object')return false;if(c.isCluster===true)return true;if(typeof c.nodes==='function')return true;if(c.constructor&&(c.constructor.name==='Cluster'||c.constructor.name==='RedisCluster'))return true;if(Array.isArray(c.masters)||typeof c.getMasters==='function'||c._slots)return true;return false;}usesObjectEval(){return this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V4;}static scriptSha(script){return crypto.createHash('sha1').update(script).digest('hex');}async evalScript(script,keys=[],args=[]){const stringArgs=args.map(a=>String(a));const sha=RedisClientAdapter.scriptSha(script);const useObjectForm=this.usesObjectEval();if(!this._scriptFallback.get(script)){const hasEvalsha=typeof this.client.evalsha==='function'||typeof this.client.evalSha==='function';if(hasEvalsha){try{return await this._callEvalsha(sha,keys,stringArgs,useObjectForm);}catch(err){if(!isNoScriptError(err)){throw err;}this._scriptFallback.set(script,true);}}else{this._scriptFallback.set(script,true);}}const res=await this._callEval(script,keys,stringArgs,useObjectForm);this._scriptFallback.delete(script);return res;}async _callEvalsha(sha,keys,stringArgs,useObjectForm){if(useObjectForm){const fn=typeof this.client.evalSha==='function'?'evalSha':'evalsha';return this.client[fn](sha,{keys,arguments:stringArgs});}const fn=typeof this.client.evalsha==='function'?'evalsha':'evalSha';return invokeClientMethod(this.client,fn,[sha,keys.length,...keys,...stringArgs]);}async _callEval(script,keys,stringArgs,useObjectForm){if(typeof this.client.eval==='function'){if(useObjectForm){return this.client.eval(script,{keys,arguments:stringArgs});}return invokeClientMethod(this.client,'eval',[script,keys.length,...keys,...stringArgs]);}if(typeof this.client.sendCommand==='function'){const command=['EVAL',script,String(keys.length),...keys,...stringArgs];return this._sendCommand(command);}if(typeof this.client.send_command==='function'){return invokeClientMethod(this.client,'send_command',['EVAL',[script,keys.length,...keys,...stringArgs]]);}throw new Error('Redis client does not support eval, evalsha, or sendCommand');}async sendCommand(command){return this._sendCommand(command);}async _sendCommand(command){if(this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V4){return this.client.sendCommand(command);}if(typeof this.client.sendCommand==='function'){return this.client.sendCommand(...command);}if(typeof this.client.send_command==='function'){const[cmd,...args]=command;return invokeClientMethod(this.client,'send_command',[cmd,args]);}throw new Error('Redis client does not support sendCommand or send_command');}async getWithTtl(key){let value;if(typeof this.client.get==='function'){value=await invokeClientMethod(this.client,'get',[key]);}else if(typeof this.client.sendCommand==='function'||typeof this.client.send_command==='function'){value=await this._sendCommand(['GET',key]);}else{throw new Error('Redis client does not support get or sendCommand');}if(value===null||value===undefined){return null;}const ttlMs=await this.pttl(key);return{value,ttlMs};}async pttl(key){if(typeof this.client.pttl==='function'){return invokeClientMethod(this.client,'pttl',[key]);}if(typeof this.client.pTTL==='function'){return invokeClientMethod(this.client,'pTTL',[key]);}if(typeof this.client.sendCommand==='function'||typeof this.client.send_command==='function'){return this._sendCommand(['PTTL',key]);}throw new Error('Redis client does not support pttl or sendCommand');}async del(key){let count;if(typeof this.client.del==='function'){count=await invokeClientMethod(this.client,'del',[key]);}else if(typeof this.client.sendCommand==='function'||typeof this.client.send_command==='function'){count=await this._sendCommand(['DEL',key]);}else{throw new Error('Redis client does not support del or sendCommand');}return count>0;}async delMany(keys){if(!keys||keys.length===0){return 0;}if(this.isCluster){if(typeof this.client.pipeline==='function'){try{const pipe=this.client.pipeline();for(const k of keys){pipe.del(k);}const results=await pipe.exec();if(Array.isArray(results)){return results.reduce((acc,[,res])=>acc+(Number(res)||0),0);}}catch{}}const results=await Promise.all(keys.map(k=>this.del(k)));return results.filter(Boolean).length;}try{if(typeof this.client.del==='function'){const res=this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V4?await this.client.del(keys):await invokeClientMethod(this.client,'del',keys);return Number(res)||0;}if(typeof this.client.sendCommand==='function'||typeof this.client.send_command==='function'){const res=await this._sendCommand(['DEL',...keys]);return Number(res)||0;}}catch(err){if(err&&String(err.message).includes('CROSSSLOT')){const results=await Promise.all(keys.map(k=>this.del(k)));return results.filter(Boolean).length;}throw err;}throw new Error('Redis client does not support del or sendCommand');}async setWithTtl(key,value,ttlMs){const hasTtl=typeof ttlMs==='number'&&ttlMs>0;if(this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V4){if(typeof this.client.set==='function'){return hasTtl?this.client.set(key,value,{PX:ttlMs}):this.client.set(key,value);}if(typeof this.client.sendCommand==='function'){const cmd=hasTtl?['SET',key,value,'PX',String(ttlMs)]:['SET',key,value];return this._sendCommand(cmd);}}if(this.kind===REDIS_CLIENT_KIND.IOREDIS||this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V3){if(typeof this.client.set==='function'){if(!hasTtl){return invokeClientMethod(this.client,'set',[key,value]);}return invokeClientMethod(this.client,'set',[key,value,'PX',ttlMs]);}}if(typeof this.client.sendCommand==='function'||typeof this.client.send_command==='function'){const cmd=hasTtl?['SET',key,value,'PX',String(ttlMs)]:['SET',key,value];return this._sendCommand(cmd);}if(typeof this.client.set==='function'){if(!hasTtl){return invokeClientMethod(this.client,'set',[key,value]);}try{const res=await this.client.set(key,value,{PX:ttlMs});if(res!==undefined)return res;}catch{}return invokeClientMethod(this.client,'set',[key,value,'PX',ttlMs]);}throw new Error('Redis client does not support set or sendCommand');}async scanKeys(pattern){if(this.isCluster){const masters=this._getClusterMasterNodes();if(masters.length>0){const allKeys=new Set();for(const master of masters){const nodeKeys=await this._scanNodeKeys(master,pattern);for(const k of nodeKeys){allKeys.add(k);}}return Array.from(allKeys);}}return this._scanNodeKeys(this.client,pattern);}_getClusterMasterNodes(){if(!this.client)return[];if(typeof this.client.nodes==='function'){try{const masters=this.client.nodes('master');if(Array.isArray(masters)&&masters.length>0)return masters;}catch{}}if(this.client.masters){if(Array.isArray(this.client.masters))return this.client.masters;if(typeof this.client.masters.values==='function'){return Array.from(this.client.masters.values());}if(typeof this.client.masters[Symbol.iterator]==='function'){const list=[];for(const item of this.client.masters){list.push(Array.isArray(item)?item[1]:item);}return list;}}return[];}async _scanNodeKeys(client,pattern){if(!client)return[];if(typeof client.scan==='function'){const keys=[];let cursor='0';const isNodeRedisV4=this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V4||typeof client.isReady==='boolean';do{let reply;if(isNodeRedisV4){reply=await client.scan(cursor,{MATCH:pattern,COUNT:100});}else{reply=await invokeClientMethod(client,'scan',[cursor,'MATCH',pattern,'COUNT',100]);}if(Array.isArray(reply)){cursor=String(reply[0]);if(Array.isArray(reply[1])){keys.push(...reply[1]);}}else if(reply&&typeof reply==='object'){cursor=String(reply.cursor);if(Array.isArray(reply.keys)){keys.push(...reply.keys);}}else{break;}}while(cursor!=='0');return keys;}if(typeof client.sendCommand==='function'||typeof client.send_command==='function'){const keys=[];let cursor='0';do{const reply=await this._sendCommandOnClient(client,['SCAN',cursor,'MATCH',pattern,'COUNT','100']);if(Array.isArray(reply)){cursor=String(reply[0]);if(Array.isArray(reply[1])){keys.push(...reply[1]);}}else if(reply&&typeof reply==='object'){cursor=String(reply.cursor);if(Array.isArray(reply.keys)){keys.push(...reply.keys);}}else{break;}}while(cursor!=='0');return keys;}return[];}async _sendCommandOnClient(client,command){if(this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V4||typeof client.isReady==='boolean'){return client.sendCommand(command);}if(typeof client.sendCommand==='function'){return client.sendCommand(...command);}if(typeof client.send_command==='function'){const[cmd,...args]=command;return invokeClientMethod(client,'send_command',[cmd,args]);}throw new Error('Redis client does not support sendCommand or send_command');}isReady(rlKey,isReadonly){if(this.kind===REDIS_CLIENT_KIND.IOREDIS){if(this.client.status){return this.client.status==='ready';}return true;}if(this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V4){if(typeof this.client.isOpen==='boolean'&&this.client.isOpen!==true){return false;}if(this.client._slots&&typeof this.client._slots.getClient==='function'){try{const slotClient=this.client._slots.getClient(rlKey,isReadonly);return Boolean(slotClient&&slotClient.isReady===true);}catch{return false;}}return this.client.isReady!==false;}if(this.kind===REDIS_CLIENT_KIND.NODE_REDIS_V3){if(typeof this.client.isReady==='function'){return this.client.isReady();}if(typeof this.client.ready==='boolean'){return this.client.ready;}if(typeof this.client.connected==='boolean'){return this.client.connected;}}return true;}}export function isNoScriptError(err){return Boolean(err&&typeof err.message==='string'&&err.message.includes('NOSCRIPT'));}export default RedisClientAdapter;
|
package/dist/utils/validation.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ConfigurationError}from'./errors.js';export const VALID_ALGORITHMS=new Set(['fixed-window','sliding-window-log','sliding-window-counter','token-bucket','leaky-bucket']);export function validateOptions(rawOptions={}){const options={...rawOptions};options.algorithm=options.algorithm||'fixed-window';if(!VALID_ALGORITHMS.has(options.algorithm)){throw new ConfigurationError(`Invalid algorithm: "${options.algorithm}". Supported: ${Array.from(VALID_ALGORITHMS).join(', ')}`);}if(typeof options.store==='string'){
|
|
1
|
+
import{ConfigurationError}from'./errors.js';export const VALID_ALGORITHMS=new Set(['fixed-window','sliding-window-log','sliding-window-counter','token-bucket','leaky-bucket']);export function validateOptions(rawOptions={}){const options={...rawOptions};options.algorithm=options.algorithm||'fixed-window';if(!VALID_ALGORITHMS.has(options.algorithm)){throw new ConfigurationError(`Invalid algorithm: "${options.algorithm}". Supported: ${Array.from(VALID_ALGORITHMS).join(', ')}`);}if(typeof options.store==='string'){const s=options.store.toLowerCase();if(s!=='memory'&&s!=='redis'&&s!=='redis-cluster'&&s!=='cluster'){throw new ConfigurationError(`Invalid store: "${options.store}". Supported string stores: "memory", "redis", "redis-cluster", "cluster"`);}}else if(options.store&&typeof options.store!=='object'){throw new ConfigurationError('Store must be "memory", "redis", "redis-cluster", "cluster", or a custom store instance');}else if(!options.store){options.store='memory';}const rawKeyPrefix=options.keyPrefix!==undefined?options.keyPrefix:options.prefix!==undefined?options.prefix:options.storePrefix!==undefined?options.storePrefix:options._storePrefix;if(rawKeyPrefix!==undefined&&typeof rawKeyPrefix!=='string'){throw new ConfigurationError('keyPrefix must be a string');}options.keyPrefix=typeof rawKeyPrefix==='string'?rawKeyPrefix:'rl:';if(options.cost!==undefined){if(typeof options.cost!=='function'&&(typeof options.cost!=='number'||options.cost<=0||!Number.isFinite(options.cost))){throw new ConfigurationError('Cost must be a positive number or a function returning a positive number');}}else{options.cost=1;}if(options.keyGenerator!==undefined){if(typeof options.keyGenerator!=='function'&&options.keyGenerator!=='ip'&&options.keyGenerator!=='ip-endpoint'&&options.keyGenerator!=='graphql'){throw new ConfigurationError('keyGenerator must be a function or one of "ip", "ip-endpoint", "graphql"');}}if(options.skip===undefined){if(options.customSkip!==undefined){options.skip=options.customSkip;}else if(options.customskip!==undefined){options.skip=options.customskip;}}if(options.skip!==undefined&&typeof options.skip!=='function'&&(typeof options.skip!=='object'||options.skip===null)){throw new ConfigurationError('skip must be a function, object, or array');}if(options.handler!==undefined&&typeof options.handler!=='function'){throw new ConfigurationError('handler must be a function');}if(options.onError!==undefined&&typeof options.onError!=='function'){throw new ConfigurationError('onError must be a function');}options.statusCode=options.statusCode!==undefined?options.statusCode:429;if(!Number.isInteger(options.statusCode)||options.statusCode<100||options.statusCode>599){throw new ConfigurationError('statusCode must be a valid HTTP status code (100-599)');}if(options.standardHeaders!==undefined&&typeof options.standardHeaders!=='boolean'&&options.standardHeaders!=='draft-6'&&options.standardHeaders!=='draft-7'){throw new ConfigurationError('standardHeaders must be a boolean or "draft-6" or "draft-7"');}options.standardHeaders=options.standardHeaders!==undefined?options.standardHeaders:true;options.legacyHeaders=options.legacyHeaders!==undefined?Boolean(options.legacyHeaders):false;options.failOpen=Boolean(options.failOpen||options.passOnStoreError);options.message=options.message!==undefined?options.message:'Too many requests, please try again later.';if(options.maxEntries!==undefined){if(!Number.isInteger(options.maxEntries)||options.maxEntries<=0){throw new ConfigurationError('maxEntries must be a positive integer');}}if(options.cleanupIntervalMs!==undefined){if(typeof options.cleanupIntervalMs!=='number'||options.cleanupIntervalMs<=0||!Number.isFinite(options.cleanupIntervalMs)){throw new ConfigurationError('cleanupIntervalMs must be a positive number');}}if(options.useRedisTime!==undefined&&typeof options.useRedisTime!=='boolean'){throw new ConfigurationError('useRedisTime must be a boolean');}if(options.algorithm==='fixed-window'||options.algorithm==='sliding-window-log'||options.algorithm==='sliding-window-counter'){options.windowMs=options.windowMs!==undefined?options.windowMs:60000;if(typeof options.windowMs!=='number'||options.windowMs<=0||!Number.isFinite(options.windowMs)){throw new ConfigurationError(`${options.algorithm} requires "windowMs" to be a positive number`);}options.limit=options.limit!==undefined?options.limit:options.max!==undefined?options.max:60;if(typeof options.limit!=='number'||options.limit<=0||!Number.isFinite(options.limit)){throw new ConfigurationError(`${options.algorithm} requires "limit" to be a positive number`);}}if(options.algorithm==='token-bucket'){options.capacity=options.capacity!==undefined?options.capacity:options.limit!==undefined?options.limit:options.max!==undefined?options.max:60;if(typeof options.capacity!=='number'||options.capacity<=0||!Number.isFinite(options.capacity)){throw new ConfigurationError('token-bucket requires "capacity" to be a positive number');}options.refillRate=options.refillRate!==undefined?options.refillRate:1;if(typeof options.refillRate!=='number'||options.refillRate<=0||!Number.isFinite(options.refillRate)){throw new ConfigurationError('token-bucket requires "refillRate" to be a positive number');}options.refillInterval=options.refillInterval!==undefined?options.refillInterval:1000;if(typeof options.refillInterval!=='number'||options.refillInterval<=0||!Number.isFinite(options.refillInterval)){throw new ConfigurationError('token-bucket requires "refillInterval" to be a positive number in milliseconds');}if(options.initialTokens!==undefined){if(typeof options.initialTokens!=='number'||options.initialTokens<0||!Number.isFinite(options.initialTokens)){throw new ConfigurationError('token-bucket "initialTokens" must be a non-negative number');}}else{options.initialTokens=options.capacity;}}if(options.algorithm==='leaky-bucket'){options.capacity=options.capacity!==undefined?options.capacity:options.limit!==undefined?options.limit:options.max!==undefined?options.max:60;if(typeof options.capacity!=='number'||options.capacity<=0||!Number.isFinite(options.capacity)){throw new ConfigurationError('leaky-bucket requires "capacity" to be a positive number');}options.leakRate=options.leakRate!==undefined?options.leakRate:1;if(typeof options.leakRate!=='number'||options.leakRate<=0||!Number.isFinite(options.leakRate)){throw new ConfigurationError('leaky-bucket requires "leakRate" to be a positive number');}options.leakInterval=options.leakInterval!==undefined?options.leakInterval:1000;if(typeof options.leakInterval!=='number'||options.leakInterval<=0||!Number.isFinite(options.leakInterval)){throw new ConfigurationError('leaky-bucket requires "leakInterval" to be a positive number in milliseconds');}options.mode=options.mode||'policing';if(options.mode!=='policing'&&options.mode!=='shaping'){throw new ConfigurationError('leaky-bucket "mode" must be either "policing" or "shaping"');}if(options.mode==='shaping'){options.maxQueueSize=options.maxQueueSize!==undefined?options.maxQueueSize:100;if(!Number.isInteger(options.maxQueueSize)||options.maxQueueSize<=0){throw new ConfigurationError('leaky-bucket in shaping mode requires "maxQueueSize" to be a positive integer');}options.queueTimeoutMs=options.queueTimeoutMs!==undefined?options.queueTimeoutMs:30000;if(typeof options.queueTimeoutMs!=='number'||options.queueTimeoutMs<=0||!Number.isFinite(options.queueTimeoutMs)){throw new ConfigurationError('leaky-bucket in shaping mode requires "queueTimeoutMs" to be a positive number');}}}return options;}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-adaptive-rate-limiter",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Production-ready rate limiting for Node.js: five rate limiting algorithms plus a framework-agnostic points-based core with Memory, Redis, Mongo, Mongoose, MySQL, Postgres, SQLite, Prisma, Memcached, DynamoDB, Valkey, etcd and Redis Cluster backends.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,16 +60,15 @@
|
|
|
60
60
|
"middleware",
|
|
61
61
|
"graphql"
|
|
62
62
|
],
|
|
63
|
-
"author":
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"type": "git",
|
|
67
|
-
"url": "https://github.com/ravinddeveloper/express-adaptive-rate-limiter.git"
|
|
63
|
+
"author": {
|
|
64
|
+
"name": "RAVI N D",
|
|
65
|
+
"url": "https://github.com/ravinddeveloper"
|
|
68
66
|
},
|
|
67
|
+
"license": "MIT",
|
|
68
|
+
"homepage": "https://github.com/ravinddeveloper",
|
|
69
69
|
"bugs": {
|
|
70
|
-
"url": "https://github.com/ravinddeveloper
|
|
70
|
+
"url": "https://github.com/ravinddeveloper"
|
|
71
71
|
},
|
|
72
|
-
"homepage": "https://github.com/ravinddeveloper/express-adaptive-rate-limiter#readme",
|
|
73
72
|
"peerDependencies": {
|
|
74
73
|
"graphql": ">=16.0.0",
|
|
75
74
|
"ioredis": ">=4.0.0",
|
|
@@ -150,7 +149,6 @@
|
|
|
150
149
|
"graphql": "^16.10.0",
|
|
151
150
|
"ioredis": "^5.4.2",
|
|
152
151
|
"ioredis-mock": "^8.13.1",
|
|
153
|
-
"javascript-obfuscator": "^5.10.0",
|
|
154
152
|
"prettier": "^3.9.10",
|
|
155
153
|
"supertest": "^7.0.0",
|
|
156
154
|
"typescript": "^5.7.3",
|