express-adaptive-rate-limiter 2.0.0 → 2.1.0
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 +284 -28
- package/dist/algorithms/baseAlgorithm.js +1 -1
- package/dist/algorithms/fixedWindow.js +1 -1
- package/dist/algorithms/leakyBucket.js +1 -1
- package/dist/algorithms/slidingWindowCounter.js +1 -1
- package/dist/algorithms/slidingWindowLog.js +1 -1
- package/dist/algorithms/tokenBucket.js +1 -1
- package/dist/core/coreMiddleware.js +1 -1
- package/dist/core/limiters/clusterLimiter.js +1 -16
- package/dist/core/limiters/redisLimiter.js +1 -1
- package/dist/index.js +1 -1
- package/dist/middleware/expressMiddleware.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 +36 -16
- 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,24 @@ 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.1.0
|
|
15
|
+
|
|
16
|
+
- **Dynamic Request-Based Limits**: Pass a function or async resolver `(req, res) => number | Promise<number>` as `limit` or `capacity`. Easily apply VIP, customer tier, or route-based quotas dynamically across all 5 rate-limiting algorithms.
|
|
17
|
+
- **Lifecycle Event Emitters**: Express and Core middlewares now inherit from Node.js `EventEmitter`. Listen to `'consumed'`, `'limitReached'`, `'blocked'`, `'reset'`, and `'error'` events for real-time telemetry, Prometheus metrics, and security auditing.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What's new in 2.0.1
|
|
22
|
+
|
|
23
|
+
- **Full Redis Cluster support**:
|
|
24
|
+
- **Cross-Slot (`CROSSSLOT`) Protection**: Safe atomic key deletion (`delMany`) via pipeline or per-slot execution preventing Redis Cluster `CROSSSLOT` failures.
|
|
25
|
+
- **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.
|
|
26
|
+
- **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.
|
|
27
|
+
- **Cluster Limiter Parity**: Full drop-in compatibility with `RateLimiterRedisCluster`, `ClusterLimiter`, and `RedisClusterLimiter`, inheriting `rejectIfRedisNotReady` and `parseKey` key unwrapping.
|
|
28
|
+
- **Direct Express Tier Integration**: Configure `rateLimit({ store: 'redis-cluster' })` or `rateLimit({ store: 'cluster' })` with automated cluster client creation and node discovery.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
14
32
|
## What's new in 2.0.0
|
|
15
33
|
|
|
16
34
|
**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 +194,7 @@ Both modern descriptive names and classic class aliases are exported for conveni
|
|
|
176
194
|
| `MemoryLimiter` | `RateLimiterMemory` | none | in-process Map with O(1) eviction |
|
|
177
195
|
| `RedisLimiter` | `RateLimiterRedis` | `ioredis` or `redis` | Lua script |
|
|
178
196
|
| `RateLimiterCluster`| `RateLimiterCluster` | `node:cluster` IPC | Master-Worker IPC messaging |
|
|
179
|
-
| `RedisClusterLimiter`| `ClusterLimiter`
|
|
197
|
+
| `RedisClusterLimiter`| `ClusterLimiter` / `RateLimiterRedisCluster` | ioredis Cluster / node-redis cluster | Lua script, hash-tagged `{slot}` |
|
|
180
198
|
| `ValkeyLimiter` | `RateLimiterValkey` | `iovalkey` or `@valkey/valkey-glide` | Lua script |
|
|
181
199
|
| `MongoLimiter` | `RateLimiterMongo` | `mongodb` | `findOneAndUpdate` with `$inc` |
|
|
182
200
|
| `MongooseLimiter` | `RateLimiterMongoose` | `mongoose` | `findOneAndUpdate` with `$inc` |
|
|
@@ -220,6 +238,162 @@ if (cluster.isPrimary || cluster.isMaster) {
|
|
|
220
238
|
}
|
|
221
239
|
```
|
|
222
240
|
|
|
241
|
+
### Redis Cluster Rate Limiting (`ClusterLimiter` & `store: 'redis-cluster'`)
|
|
242
|
+
|
|
243
|
+
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 (`{...}`)**.
|
|
244
|
+
|
|
245
|
+
`express-adaptive-rate-limiter` provides **first-class Redis Cluster support** across both the Express middleware and Core tiers:
|
|
246
|
+
- **Automatic Hash Tagging**: Wraps keys in `{keyPrefix:key}` by default so multi-key operations land on the exact same hash slot.
|
|
247
|
+
- **Cross-Slot Safe Cleanups**: Key deletions (`delMany`) use Redis pipelines or individual per-slot deletions, preventing `CROSSSLOT` errors.
|
|
248
|
+
- **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.
|
|
249
|
+
- **Client Flexibility**: Works with both `ioredis.Cluster` and `@redis/client` (`node-redis`) cluster mode.
|
|
250
|
+
|
|
251
|
+
#### 1. Express Middleware with Redis Cluster
|
|
252
|
+
|
|
253
|
+
##### Using `ioredis.Cluster`:
|
|
254
|
+
|
|
255
|
+
```javascript
|
|
256
|
+
import express from 'express';
|
|
257
|
+
import Redis from 'ioredis';
|
|
258
|
+
import { rateLimit } from 'express-adaptive-rate-limiter';
|
|
259
|
+
|
|
260
|
+
const app = express();
|
|
261
|
+
|
|
262
|
+
// Initialize ioredis Cluster client
|
|
263
|
+
const clusterClient = new Redis.Cluster(
|
|
264
|
+
[
|
|
265
|
+
{ host: '10.0.0.1', port: 6379 },
|
|
266
|
+
{ host: '10.0.0.2', port: 6379 },
|
|
267
|
+
{ host: '10.0.0.3', port: 6379 }
|
|
268
|
+
],
|
|
269
|
+
{
|
|
270
|
+
redisOptions: {
|
|
271
|
+
enableOfflineQueue: false // recommended: fail fast on network partitions
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// Configure Express rate limiter with Redis Cluster
|
|
277
|
+
const clusterLimiter = rateLimit({
|
|
278
|
+
store: 'redis-cluster', // or 'cluster'
|
|
279
|
+
client: clusterClient,
|
|
280
|
+
algorithm: 'sliding-window-counter', // any of the 5 algorithms
|
|
281
|
+
limit: 100, // 100 requests
|
|
282
|
+
windowMs: 60 * 1000, // per 1 minute
|
|
283
|
+
hashTag: true, // wraps storage keys in {prefix:key} (default: true)
|
|
284
|
+
failOpen: true // allow traffic if cluster is unreachable
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
app.use(clusterLimiter);
|
|
288
|
+
|
|
289
|
+
app.get('/api/resource', (req, res) => {
|
|
290
|
+
res.json({ message: 'Within cluster rate limit', rateLimit: req.rateLimit });
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
##### Using `node-redis` (`createCluster`):
|
|
295
|
+
|
|
296
|
+
```javascript
|
|
297
|
+
import express from 'express';
|
|
298
|
+
import { createCluster } from 'redis';
|
|
299
|
+
import { rateLimit } from 'express-adaptive-rate-limiter';
|
|
300
|
+
|
|
301
|
+
const app = express();
|
|
302
|
+
|
|
303
|
+
const cluster = createCluster({
|
|
304
|
+
rootNodes: [{ url: 'redis://10.0.0.1:6379' }, { url: 'redis://10.0.0.2:6379' }]
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await cluster.connect();
|
|
308
|
+
|
|
309
|
+
app.use(
|
|
310
|
+
rateLimit({
|
|
311
|
+
store: 'redis-cluster',
|
|
312
|
+
client: cluster,
|
|
313
|
+
limit: 60,
|
|
314
|
+
windowMs: 60000
|
|
315
|
+
})
|
|
316
|
+
);
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
##### Auto-Connecting Cluster (without manual client instantiation):
|
|
320
|
+
|
|
321
|
+
You can also pass cluster nodes directly to `rateLimit` or `RedisStore`:
|
|
322
|
+
|
|
323
|
+
```javascript
|
|
324
|
+
import { rateLimit } from 'express-adaptive-rate-limiter';
|
|
325
|
+
|
|
326
|
+
app.use(
|
|
327
|
+
rateLimit({
|
|
328
|
+
store: 'redis-cluster',
|
|
329
|
+
cluster: [
|
|
330
|
+
{ host: '10.0.0.1', port: 6379 },
|
|
331
|
+
{ host: '10.0.0.2', port: 6379 }
|
|
332
|
+
],
|
|
333
|
+
limit: 100,
|
|
334
|
+
windowMs: 60000
|
|
335
|
+
})
|
|
336
|
+
);
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
#### 2. Framework-Agnostic Core Tier (`ClusterLimiter` / `RateLimiterRedisCluster`)
|
|
342
|
+
|
|
343
|
+
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):
|
|
344
|
+
|
|
345
|
+
```javascript
|
|
346
|
+
import Redis from 'ioredis';
|
|
347
|
+
import { ClusterLimiter } from 'express-adaptive-rate-limiter';
|
|
348
|
+
|
|
349
|
+
const redisCluster = new Redis.Cluster([
|
|
350
|
+
{ host: '10.0.0.1', port: 6379 },
|
|
351
|
+
{ host: '10.0.0.2', port: 6379 }
|
|
352
|
+
]);
|
|
353
|
+
|
|
354
|
+
const limiter = new ClusterLimiter({
|
|
355
|
+
storeClient: redisCluster,
|
|
356
|
+
points: 10, // 10 points
|
|
357
|
+
duration: 1, // per 1 second
|
|
358
|
+
keyPrefix: 'cluster_api',
|
|
359
|
+
hashTag: true, // wraps keys in {cluster_api:key}
|
|
360
|
+
rejectIfRedisNotReady: true // reject immediately if cluster is not ready
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Consume points
|
|
364
|
+
try {
|
|
365
|
+
const res = await limiter.consume('user_42', 1);
|
|
366
|
+
console.log('Points remaining:', res.remainingPoints);
|
|
367
|
+
} catch (rejRes) {
|
|
368
|
+
if (rejRes instanceof Error) {
|
|
369
|
+
console.error('Cluster storage error:', rejRes);
|
|
370
|
+
} else {
|
|
371
|
+
console.warn(`Rate limit exceeded! Retry after ${rejRes.msBeforeNext}ms`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Penalty and reward
|
|
376
|
+
await limiter.penalty('user_42', 2);
|
|
377
|
+
await limiter.reward('user_42', 1);
|
|
378
|
+
|
|
379
|
+
// Block a client across the cluster
|
|
380
|
+
await limiter.block('suspicious_user', 3600); // blocked for 1 hour
|
|
381
|
+
|
|
382
|
+
// Read state without consuming
|
|
383
|
+
const state = await limiter.get('user_42');
|
|
384
|
+
console.log(state?.consumedPoints);
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
#### Redis Cluster Options Reference
|
|
388
|
+
|
|
389
|
+
| Option | Type | Default | Description |
|
|
390
|
+
| --- | --- | --- | --- |
|
|
391
|
+
| `storeClient` / `client` | `object` | `null` | Active `ioredis.Cluster` or `node-redis` cluster instance. |
|
|
392
|
+
| `cluster` / `clusterNodes` | `Array` | `null` | Array of cluster node definitions to automatically instantiate. |
|
|
393
|
+
| `hashTag` | `boolean` | `true` | Wraps keys in `{...}` hash tags so multi-key operations land in the same slot. |
|
|
394
|
+
| `rejectIfRedisNotReady` | `boolean` | `false` | Throws `RedisConnectionError` if cluster status is not ready. |
|
|
395
|
+
| `failOpen` | `boolean` | `false` | When true, errors from the store fail open instead of failing closed. |
|
|
396
|
+
|
|
223
397
|
### In-Memory State Dump & Restore (`dump()` & `restore()`)
|
|
224
398
|
|
|
225
399
|
Save in-memory state on shutdown for zero-downtime restarts:
|
|
@@ -310,52 +484,118 @@ All options below can be passed to `rateLimit(options)`:
|
|
|
310
484
|
|
|
311
485
|
Counts requests in a window that starts with the **first request of a key** and lasts `windowMs`. The window never slides, so a client can never consume two windows worth of quota in quick succession.
|
|
312
486
|
|
|
313
|
-
| Option | Type
|
|
314
|
-
| --------------- |
|
|
315
|
-
| `limit` / `max` | `number` | `60` | Maximum requests allowed within `windowMs
|
|
316
|
-
| `windowMs` | `number`
|
|
487
|
+
| Option | Type | Default | Description |
|
|
488
|
+
| --------------- | ------------------------------------------- | ------- | ------------------------------------------- |
|
|
489
|
+
| `limit` / `max` | `number \| ((req, res) => number \| Promise<number>)` | `60` | Maximum requests allowed within `windowMs` (static or dynamic). |
|
|
490
|
+
| `windowMs` | `number` | `60000` | Window duration in milliseconds. |
|
|
317
491
|
|
|
318
492
|
#### Sliding Window Log (`algorithm: 'sliding-window-log'`)
|
|
319
493
|
|
|
320
494
|
Maintains timestamped log of each request. Provides 100% precision rolling window without boundary reset spikes.
|
|
321
495
|
|
|
322
|
-
| Option | Type
|
|
323
|
-
| --------------- |
|
|
324
|
-
| `limit` / `max` | `number` | `60` | Maximum requests allowed in the rolling window. |
|
|
325
|
-
| `windowMs` | `number`
|
|
496
|
+
| Option | Type | Default | Description |
|
|
497
|
+
| --------------- | ------------------------------------------- | ------- | ----------------------------------------------- |
|
|
498
|
+
| `limit` / `max` | `number \| ((req, res) => number \| Promise<number>)` | `60` | Maximum requests allowed in the rolling window (static or dynamic). |
|
|
499
|
+
| `windowMs` | `number` | `60000` | Rolling window duration in milliseconds. |
|
|
326
500
|
|
|
327
501
|
#### Sliding Window Counter (`algorithm: 'sliding-window-counter'`)
|
|
328
502
|
|
|
329
503
|
Approximates rolling window by weighting previous window count with current elapsed time. Combines accuracy of sliding log with O(1) memory overhead of fixed window.
|
|
330
504
|
|
|
331
|
-
| Option | Type
|
|
332
|
-
| --------------- |
|
|
333
|
-
| `limit` / `max` | `number` | `60` | Maximum estimated requests allowed in the sliding window. |
|
|
334
|
-
| `windowMs` | `number`
|
|
505
|
+
| Option | Type | Default | Description |
|
|
506
|
+
| --------------- | ------------------------------------------- | ------- | --------------------------------------------------------- |
|
|
507
|
+
| `limit` / `max` | `number \| ((req, res) => number \| Promise<number>)` | `60` | Maximum estimated requests allowed in the sliding window (static or dynamic). |
|
|
508
|
+
| `windowMs` | `number` | `60000` | Window duration in milliseconds. |
|
|
335
509
|
|
|
336
510
|
#### Token Bucket (`algorithm: 'token-bucket'`)
|
|
337
511
|
|
|
338
512
|
Tokens refill continuously at `refillRate` per `refillInterval` up to `capacity`. Allows bursts up to `capacity`.
|
|
339
513
|
|
|
340
|
-
| Option | Type
|
|
341
|
-
| ---------------------------- |
|
|
342
|
-
| `capacity` / `limit` / `max` | `number` | `60` | Maximum burst capacity of the bucket.
|
|
343
|
-
| `refillRate` | `number`
|
|
344
|
-
| `refillInterval` | `number`
|
|
345
|
-
| `initialTokens` | `number`
|
|
514
|
+
| Option | Type | Default | Description |
|
|
515
|
+
| ---------------------------- | ------------------------------------------- | ---------- | ---------------------------------------------- |
|
|
516
|
+
| `capacity` / `limit` / `max` | `number \| ((req, res) => number \| Promise<number>)` | `60` | Maximum burst capacity of the bucket (static or dynamic). |
|
|
517
|
+
| `refillRate` | `number` | `1` | Number of tokens refilled every interval. |
|
|
518
|
+
| `refillInterval` | `number` | `1000` | Refill interval in milliseconds. |
|
|
519
|
+
| `initialTokens` | `number` | `capacity` | Starting token balance when key is first seen. |
|
|
346
520
|
|
|
347
521
|
#### Leaky Bucket (`algorithm: 'leaky-bucket'`)
|
|
348
522
|
|
|
349
523
|
Requests fill a bucket that leaks at a constant rate. Supports policing (drop excess immediately) and shaping (queue excess and process smoothly).
|
|
350
524
|
|
|
351
|
-
| Option | Type
|
|
352
|
-
| ---------------------------- |
|
|
353
|
-
| `capacity` / `limit` / `max` | `number`
|
|
354
|
-
| `leakRate` | `number`
|
|
355
|
-
| `leakInterval` | `number`
|
|
356
|
-
| `mode` | `'policing' \| 'shaping'`
|
|
357
|
-
| `maxQueueSize` | `number`
|
|
358
|
-
| `queueTimeoutMs` | `number`
|
|
525
|
+
| Option | Type | Default | Description |
|
|
526
|
+
| ---------------------------- | ------------------------------------------- | ------------ | ------------------------------------------------------------------------ |
|
|
527
|
+
| `capacity` / `limit` / `max` | `number \| ((req, res) => number \| Promise<number>)` | `60` | Bucket water capacity / burst ceiling (static or dynamic). |
|
|
528
|
+
| `leakRate` | `number` | `1` | Number of requests/units leaked every interval. |
|
|
529
|
+
| `leakInterval` | `number` | `1000` | Leak interval in milliseconds. |
|
|
530
|
+
| `mode` | `'policing' \| 'shaping'` | `'policing'` | `'policing'` rejects immediately when full; `'shaping'` queues requests. |
|
|
531
|
+
| `maxQueueSize` | `number` | `100` | Max queued requests in shaping mode before throwing `QueueFullError`. |
|
|
532
|
+
| `queueTimeoutMs` | `number` | `30000` | Max wait time (ms) in shaping queue before `QueueTimeoutError`. |
|
|
533
|
+
|
|
534
|
+
#### Dynamic Request-Based Limits (`limit` as a Function)
|
|
535
|
+
|
|
536
|
+
`limit` and `capacity` accept a synchronous or asynchronous function `(req, res) => number | Promise<number>`. This allows you to dynamically scale limits based on client tiers, API plans, authenticated roles, or time-of-day:
|
|
537
|
+
|
|
538
|
+
```javascript
|
|
539
|
+
import express from 'express';
|
|
540
|
+
import { rateLimit } from 'express-adaptive-rate-limiter';
|
|
541
|
+
|
|
542
|
+
const app = express();
|
|
543
|
+
|
|
544
|
+
const limiter = rateLimit({
|
|
545
|
+
algorithm: 'sliding-window-counter',
|
|
546
|
+
windowMs: 60 * 1000,
|
|
547
|
+
// Dynamic limit based on user subscription tier or auth headers
|
|
548
|
+
limit: async (req) => {
|
|
549
|
+
if (req.user?.tier === 'enterprise') return 1000;
|
|
550
|
+
if (req.user?.tier === 'pro') return 200;
|
|
551
|
+
return 30; // free tier
|
|
552
|
+
},
|
|
553
|
+
keyGenerator: (req) => req.user?.id || req.ip
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
app.use(limiter);
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
#### Middleware Lifecycle Events (`EventEmitter`)
|
|
560
|
+
|
|
561
|
+
Both `rateLimit()` and `createCoreMiddleware()` return Express middleware functions that inherit from Node.js `EventEmitter`. You can register listeners for real-time observability, Prometheus/Datadog metrics, audit logging, or security alerts without modifying route handlers:
|
|
562
|
+
|
|
563
|
+
```javascript
|
|
564
|
+
import { rateLimit } from 'express-adaptive-rate-limiter';
|
|
565
|
+
|
|
566
|
+
const limiter = rateLimit({
|
|
567
|
+
limit: 100,
|
|
568
|
+
windowMs: 60000
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Emitted when a request is within limit and allowed
|
|
572
|
+
limiter.on('consumed', (req, res, info) => {
|
|
573
|
+
console.log(`Allowed ${req.ip} - Remaining: ${info.remaining}/${info.limit}`);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Emitted when a client reaches or exceeds its quota
|
|
577
|
+
limiter.on('limitReached', (req, res, info) => {
|
|
578
|
+
console.warn(`Rate limit reached for ${req.ip}. Retry-After: ${info.retryAfter}s`);
|
|
579
|
+
// metrics.increment('rate_limit_exceeded_total', { path: req.path });
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Emitted whenever a request is blocked (HTTP 429)
|
|
583
|
+
limiter.on('blocked', (req, res, info) => {
|
|
584
|
+
// auditLogger.warn({ ip: req.ip, path: req.path, user: req.user });
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
// Emitted when an IP or key is manually reset via resetKey()
|
|
588
|
+
limiter.on('reset', (key) => {
|
|
589
|
+
console.log(`Rate limit quota reset for key: ${key}`);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Emitted if an error occurs in the underlying storage store
|
|
593
|
+
limiter.on('error', (err, req, res) => {
|
|
594
|
+
console.error(`Rate limiter store error:`, err);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
app.use(limiter);
|
|
598
|
+
```
|
|
359
599
|
|
|
360
600
|
---
|
|
361
601
|
|
|
@@ -885,6 +1125,22 @@ Key performance highlights:
|
|
|
885
1125
|
|
|
886
1126
|
---
|
|
887
1127
|
|
|
1128
|
+
## Issues & Support
|
|
1129
|
+
|
|
1130
|
+
If you encounter any bug, issue, or have any questions:
|
|
1131
|
+
- **npm Package Page**: You can report issues or reach out via the package page on npm:
|
|
1132
|
+
👉 **[npm: express-adaptive-rate-limiter](https://www.npmjs.com/package/express-adaptive-rate-limiter)**
|
|
1133
|
+
- **Developer Profile**: You can also reach out directly through the maintainer's profile:
|
|
1134
|
+
👉 **[GitHub Profile: @ravinddeveloper](https://github.com/ravinddeveloper)**
|
|
1135
|
+
|
|
1136
|
+
Our team actively investigates and resolves all reported issues. When reporting a bug or requesting assistance, please include:
|
|
1137
|
+
- Node.js version (`node -v`)
|
|
1138
|
+
- Package version (`2.0.1`)
|
|
1139
|
+
- Storage driver & version (e.g., `ioredis@5.4.2` or `redis@4.7.0`)
|
|
1140
|
+
- A minimal reproducible example or stack trace
|
|
1141
|
+
|
|
1142
|
+
---
|
|
1143
|
+
|
|
888
1144
|
## License
|
|
889
1145
|
|
|
890
|
-
MIT ©
|
|
1146
|
+
MIT © [RAVI N D](https://github.com/ravinddeveloper)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export class BaseAlgorithm{constructor(options,store){this.options=options;this.store=store;this.locks=new Map();}isRedis(){return this.store&&typeof this.store.runScript==='function';}normalizeResult(allowed,limit,remaining,consumed,retryAfter,resetTime){const resetMs=Number(resetTime);const resetDate=new Date(resetMs);return{allowed:Boolean(allowed),limit:Number(limit),remaining:Math.max(0,Number(remaining)),consumed:Math.max(0,Number(consumed)),retryAfter:Math.max(0,Number(retryAfter)),resetTime:resetDate};}async withLock(key,fn){const prev=this.locks.get(key)||Promise.resolve();let release;const current=new Promise(resolve=>{release=resolve;});this.locks.set(key,current);try{await prev;return await fn();}finally{release();if(this.locks.get(key)===current){this.locks.delete(key);}}}async consume(_key,_cost=1){throw new Error('Algorithm.consume() must be implemented');}async reset(_key){return this.store.delete(_key);}async getState(_key){throw new Error('Algorithm.getState() must be implemented');}async close(){this.locks.clear();if(this.store&&typeof this.store.close==='function'){await this.store.close();}}}export default BaseAlgorithm;
|
|
1
|
+
export class BaseAlgorithm{constructor(options,store){this.options=options;this.store=store;this.locks=new Map();}async resolveLimit(context={}){const rawLimit=this.options?.limit!==undefined?this.options.limit:this.options?.capacity!==undefined?this.options.capacity:this.options?.max;if(typeof rawLimit==='function'){const res=await Promise.resolve(rawLimit(context?.req,context?.res));return Number.isFinite(res)&&res>0?Number(res):60;}if(typeof this.limit==='number')return this.limit;if(typeof this.capacity==='number')return this.capacity;return typeof rawLimit==='number'?rawLimit:60;}isRedis(){return this.store&&typeof this.store.runScript==='function';}normalizeResult(allowed,limit,remaining,consumed,retryAfter,resetTime){const resetMs=Number(resetTime);const resetDate=new Date(resetMs);return{allowed:Boolean(allowed),limit:Number(limit),remaining:Math.max(0,Number(remaining)),consumed:Math.max(0,Number(consumed)),retryAfter:Math.max(0,Number(retryAfter)),resetTime:resetDate};}async withLock(key,fn){const prev=this.locks.get(key)||Promise.resolve();let release;const current=new Promise(resolve=>{release=resolve;});this.locks.set(key,current);try{await prev;return await fn();}finally{release();if(this.locks.get(key)===current){this.locks.delete(key);}}}async consume(_key,_cost=1){throw new Error('Algorithm.consume() must be implemented');}async reset(_key){return this.store.delete(_key);}async getState(_key){throw new Error('Algorithm.getState() must be implemented');}async close(){this.locks.clear();if(this.store&&typeof this.store.close==='function'){await this.store.close();}}}export default BaseAlgorithm;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BaseAlgorithm}from'./baseAlgorithm.js';export class FixedWindow extends BaseAlgorithm{constructor(options,store){super(options,store);this.limit=options.limit;this.windowMs=options.windowMs;}resolveWindow(state,now){if(state&&typeof state.resetTime==='number'&&state.resetTime>now){return{count:state.count||0,windowStart:state.windowStart,resetTime:state.resetTime};}return{count:0,windowStart:now,resetTime:now+this.windowMs};}async consume(key,cost=1){if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('fixed-window',[key],[
|
|
1
|
+
import{BaseAlgorithm}from'./baseAlgorithm.js';export class FixedWindow extends BaseAlgorithm{constructor(options,store){super(options,store);this.limit=options.limit;this.windowMs=options.windowMs;}resolveWindow(state,now){if(state&&typeof state.resetTime==='number'&&state.resetTime>now){return{count:state.count||0,windowStart:state.windowStart,resetTime:state.resetTime};}return{count:0,windowStart:now,resetTime:now+this.windowMs};}async consume(key,cost=1,context={}){const limit=await this.resolveLimit(context);if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('fixed-window',[key],[limit,this.windowMs,cost,nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}return this.withLock(key,async()=>{const now=Date.now();const state=await this.store.get(key);const window=this.resolveWindow(state,now);const ttl=Math.max(1,window.resetTime-now);if(window.count+cost<=limit){const nextCount=window.count+cost;await this.store.set(key,{count:nextCount,windowStart:window.windowStart,resetTime:window.resetTime},ttl);return this.normalizeResult(true,limit,limit-nextCount,nextCount,0,window.resetTime);}await this.store.set(key,{count:window.count,windowStart:window.windowStart,resetTime:window.resetTime},ttl);const retryAfter=Math.max(1,Math.ceil(ttl/1000));const remaining=Math.max(0,limit-window.count);return this.normalizeResult(false,limit,remaining,window.count,retryAfter,window.resetTime);});}async getState(key,context={}){const limit=await this.resolveLimit(context);if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('fixed-window',[key],[limit,this.windowMs,0,nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}const now=Date.now();const state=await this.store.get(key);const window=this.resolveWindow(state,now);const remaining=Math.max(0,limit-window.count);const retryAfter=window.count>=limit?Math.max(1,Math.ceil((window.resetTime-now)/1000)):0;return this.normalizeResult(window.count<limit,limit,remaining,window.count,retryAfter,window.resetTime);}}export default FixedWindow;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BaseAlgorithm}from'./baseAlgorithm.js';import{QueueFullError,QueueTimeoutError}from'../utils/errors.js';export class LeakyBucket extends BaseAlgorithm{constructor(options,store){super(options,store);this.capacity=options.capacity;this.leakRate=options.leakRate;this.leakInterval=options.leakInterval;this.mode=options.mode||'policing';this.maxQueueSize=options.maxQueueSize||100;this.queueTimeoutMs=options.queueTimeoutMs||30000;this.rate=this.leakRate/this.leakInterval;this.queues=new Map();}async _checkPolicing(key,cost){if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('leaky-bucket',[key],[
|
|
1
|
+
import{BaseAlgorithm}from'./baseAlgorithm.js';import{QueueFullError,QueueTimeoutError}from'../utils/errors.js';export class LeakyBucket extends BaseAlgorithm{constructor(options,store){super(options,store);this.capacity=options.capacity;this.leakRate=options.leakRate;this.leakInterval=options.leakInterval;this.mode=options.mode||'policing';this.maxQueueSize=options.maxQueueSize||100;this.queueTimeoutMs=options.queueTimeoutMs||30000;this.rate=this.leakRate/this.leakInterval;this.queues=new Map();}async _checkPolicing(key,cost,context={}){const capacity=await this.resolveLimit(context);if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('leaky-bucket',[key],[capacity,this.leakRate,this.leakInterval,cost,nowArg]);const normalized=this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);return{...normalized,waitMs:Number(res[6]||0)};}return this.withLock(key,async()=>{const now=Date.now();const state=await this.store.get(key);let water=0;let lastLeak=now;if(state&&typeof state.water==='number'){const elapsed=Math.max(0,now-state.lastLeak);water=Math.max(0,state.water-elapsed*this.rate);lastLeak=now;}const ttlMs=Math.max(1000,Math.ceil(capacity/this.rate*2.0));if(water+cost<=capacity){water+=cost;const consumed=Math.ceil(water);const remaining=Math.max(0,Math.floor(capacity-water));const resetTime=now+Math.ceil(water/this.rate);await this.store.set(key,{water,lastLeak},ttlMs);return{...this.normalizeResult(true,capacity,remaining,consumed,0,resetTime),waitMs:0};}const consumed=Math.ceil(water);const remaining=Math.max(0,Math.floor(capacity-water));const deficit=water+cost-capacity;const waitMs=Math.ceil(deficit/this.rate);const retryAfter=Math.max(1,Math.ceil(waitMs/1000));const resetTime=now+Math.ceil(water/this.rate);await this.store.set(key,{water,lastLeak},ttlMs);return{...this.normalizeResult(false,capacity,remaining,consumed,retryAfter,resetTime),waitMs};});}async consume(key,cost=1,extra={}){const policingResult=await this._checkPolicing(key,cost,extra);if(this.mode==='policing'||policingResult.allowed){return policingResult;}return this._enqueue(key,cost,policingResult,extra);}_enqueue(key,cost,immediateResult,extra={}){if(extra.req&&(extra.req.destroyed||extra.req.closed||extra.req.aborted)){return{...immediateResult,allowed:false,aborted:true};}let queue=this.queues.get(key);if(!queue){queue=[];this.queues.set(key,queue);}if(queue.length>=this.maxQueueSize){return{...immediateResult,allowed:false,error:new QueueFullError()};}const waitMs=immediateResult.waitMs;if(waitMs>this.queueTimeoutMs){return{...immediateResult,allowed:false,retryAfter:Math.max(1,Math.ceil(waitMs/1000)),error:new QueueTimeoutError()};}return new Promise(resolve=>{let timer=null;let cleanedUp=false;const item={cost,resolve,timer:null};const cleanup=()=>{if(cleanedUp)return;cleanedUp=true;if(timer){clearTimeout(timer);timer=null;}const currentQueue=this.queues.get(key);if(currentQueue){const idx=currentQueue.indexOf(item);if(idx!==-1){currentQueue.splice(idx,1);}if(currentQueue.length===0){this.queues.delete(key);}}};if(extra.req&&typeof extra.req.on==='function'){extra.req.once('close',()=>{if(!cleanedUp){cleanup();resolve({...immediateResult,allowed:false,aborted:true});}});}timer=setTimeout(async()=>{cleanup();try{const afterWaitResult=await this._checkPolicing(key,cost,extra);resolve(afterWaitResult);}catch{resolve({...immediateResult,allowed:false});}},waitMs);if(timer.unref){timer.unref();}item.timer=timer;queue.push(item);});}async getState(key,context={}){const capacity=await this.resolveLimit(context);if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('leaky-bucket',[key],[capacity,this.leakRate,this.leakInterval,0,nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}const now=Date.now();const state=await this.store.get(key);let water=0;if(state&&typeof state.water==='number'){const elapsed=Math.max(0,now-state.lastLeak);water=Math.max(0,state.water-elapsed*this.rate);}const consumed=Math.ceil(water);const remaining=Math.max(0,Math.floor(capacity-water));const resetTime=now+Math.ceil(water/this.rate);const retryAfter=water>=capacity?Math.max(1,Math.ceil(1/this.rate/1000)):0;return this.normalizeResult(water<capacity,capacity,remaining,consumed,retryAfter,resetTime);}async close(){for(const[,queue]of this.queues){for(const item of queue){if(item.timer){clearTimeout(item.timer);}item.resolve({allowed:false,limit:this.capacity,remaining:0,consumed:this.capacity,retryAfter:1,resetTime:new Date()});}}this.queues.clear();await super.close();}}export default LeakyBucket;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BaseAlgorithm}from'./baseAlgorithm.js';export class SlidingWindowCounter extends BaseAlgorithm{constructor(options,store){super(options,store);this.limit=options.limit;this.windowMs=options.windowMs;}async consume(key,cost=1){if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('sliding-window-counter',[key],[
|
|
1
|
+
import{BaseAlgorithm}from'./baseAlgorithm.js';export class SlidingWindowCounter extends BaseAlgorithm{constructor(options,store){super(options,store);this.limit=options.limit;this.windowMs=options.windowMs;}async consume(key,cost=1,context={}){const limit=await this.resolveLimit(context);if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('sliding-window-counter',[key],[limit,this.windowMs,cost,nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}return this.withLock(key,async()=>{const now=Date.now();const currWindowStart=Math.floor(now/this.windowMs)*this.windowMs;const resetTime=currWindowStart+this.windowMs;const state=await this.store.get(key);let prevCount=0;let currCount=0;if(state){if(state.windowStart===currWindowStart){prevCount=state.prevCount||0;currCount=state.currCount||0;}else if(currWindowStart===state.windowStart+this.windowMs){prevCount=state.currCount||0;currCount=0;}}const elapsed=now-currWindowStart;const weight=Math.max(0,1-elapsed/this.windowMs);const estimated=prevCount*weight+currCount;if(estimated+cost<=limit){currCount+=cost;const consumed=Math.round(estimated+cost);const remaining=Math.max(0,limit-consumed);await this.store.set(key,{windowStart:currWindowStart,prevCount,currCount},this.windowMs*2);return this.normalizeResult(true,limit,remaining,consumed,0,resetTime);}const consumed=Math.round(estimated);const remaining=Math.max(0,limit-consumed);let waitMs=resetTime-now;if(prevCount>0){const neededWeight=(limit-currCount-cost)/prevCount;if(neededWeight<1&&neededWeight>=0){const t=this.windowMs*(1-neededWeight);waitMs=Math.max(1,Math.ceil(t-elapsed));}}if(waitMs<=0||waitMs>this.windowMs){waitMs=Math.max(1,resetTime-now);}const retryAfter=Math.max(1,Math.ceil(waitMs/1000));await this.store.set(key,{windowStart:currWindowStart,prevCount,currCount},this.windowMs*2);return this.normalizeResult(false,limit,remaining,consumed,retryAfter,resetTime);});}async getState(key,context={}){const limit=await this.resolveLimit(context);if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('sliding-window-counter',[key],[limit,this.windowMs,0,nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}const now=Date.now();const currWindowStart=Math.floor(now/this.windowMs)*this.windowMs;const resetTime=currWindowStart+this.windowMs;const state=await this.store.get(key);let prevCount=0;let currCount=0;if(state){if(state.windowStart===currWindowStart){prevCount=state.prevCount||0;currCount=state.currCount||0;}else if(currWindowStart===state.windowStart+this.windowMs){prevCount=state.currCount||0;currCount=0;}}const elapsed=now-currWindowStart;const weight=Math.max(0,1-elapsed/this.windowMs);const estimated=prevCount*weight+currCount;const consumed=Math.round(estimated);const remaining=Math.max(0,limit-consumed);const retryAfter=estimated>=limit?Math.max(1,Math.ceil((resetTime-now)/1000)):0;return this.normalizeResult(estimated<limit,limit,remaining,consumed,retryAfter,resetTime);}}export default SlidingWindowCounter;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BaseAlgorithm}from'./baseAlgorithm.js';export class SlidingWindowLog extends BaseAlgorithm{constructor(options,store){super(options,store);this.limit=options.limit;this.windowMs=options.windowMs;}async consume(key,cost=1){if(this.isRedis()){const uid=`${Date.now()}:${Math.random().toString(36).slice(2,8)}`;const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('sliding-window-log',[key],[
|
|
1
|
+
import{BaseAlgorithm}from'./baseAlgorithm.js';export class SlidingWindowLog extends BaseAlgorithm{constructor(options,store){super(options,store);this.limit=options.limit;this.windowMs=options.windowMs;}async consume(key,cost=1,context={}){const limit=await this.resolveLimit(context);if(this.isRedis()){const uid=`${Date.now()}:${Math.random().toString(36).slice(2,8)}`;const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('sliding-window-log',[key],[limit,this.windowMs,cost,uid,nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}return this.withLock(key,async()=>{const now=Date.now();const windowStart=now-this.windowMs;const state=await this.store.get(key);let log=[];if(state&&Array.isArray(state.log)){log=state.log.filter(entry=>entry.time>windowStart);}let current=0;for(let i=0;i<log.length;i++){current+=log[i].cost;}if(current+cost<=limit){log.push({time:now,cost});const consumed=current+cost;const remaining=limit-consumed;const oldestTime=log[0].time;const resetTime=oldestTime+this.windowMs;await this.store.set(key,{log},this.windowMs);return this.normalizeResult(true,limit,remaining,consumed,0,resetTime);}const deficit=current+cost-limit;let accumulated=0;let expireTime=now;for(let i=0;i<log.length;i++){accumulated+=log[i].cost;if(accumulated>=deficit){expireTime=log[i].time;break;}}const resetTime=expireTime+this.windowMs;const waitMs=Math.max(1,resetTime-now);const retryAfter=Math.max(1,Math.ceil(waitMs/1000));const remaining=Math.max(0,limit-current);await this.store.set(key,{log},this.windowMs);return this.normalizeResult(false,limit,remaining,current,retryAfter,resetTime);});}async getState(key,context={}){const limit=await this.resolveLimit(context);if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('sliding-window-log',[key],[limit,this.windowMs,0,'inspect',nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}const now=Date.now();const windowStart=now-this.windowMs;const state=await this.store.get(key);let log=[];if(state&&Array.isArray(state.log)){log=state.log.filter(entry=>entry.time>windowStart);}let current=0;for(let i=0;i<log.length;i++){current+=log[i].cost;}const remaining=Math.max(0,limit-current);const oldestTime=log.length>0?log[0].time:now;const resetTime=oldestTime+this.windowMs;const retryAfter=current>=limit?Math.max(1,Math.ceil((resetTime-now)/1000)):0;return this.normalizeResult(current<limit,limit,remaining,current,retryAfter,resetTime);}}export default SlidingWindowLog;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{BaseAlgorithm}from'./baseAlgorithm.js';export class TokenBucket extends BaseAlgorithm{constructor(options,store){super(options,store);this.capacity=options.capacity;this.refillRate=options.refillRate;this.refillInterval=options.refillInterval;this.initialTokens=options.initialTokens!==undefined?options.initialTokens:this.capacity;this.rate=this.refillRate/this.refillInterval;}async consume(key,cost=1){if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('token-bucket',[key],[
|
|
1
|
+
import{BaseAlgorithm}from'./baseAlgorithm.js';export class TokenBucket extends BaseAlgorithm{constructor(options,store){super(options,store);this.capacity=options.capacity!==undefined?options.capacity:options.limit;this.refillRate=options.refillRate;this.refillInterval=options.refillInterval;this.initialTokens=options.initialTokens!==undefined?options.initialTokens:typeof this.capacity==='number'?this.capacity:undefined;this.rate=this.refillRate/this.refillInterval;}async consume(key,cost=1,context={}){const capacity=await this.resolveLimit(context);const initialTokens=this.initialTokens!==undefined?this.initialTokens:capacity;if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('token-bucket',[key],[capacity,this.refillRate,this.refillInterval,cost,initialTokens,nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}return this.withLock(key,async()=>{const now=Date.now();const state=await this.store.get(key);let tokens=initialTokens;let lastRefill=now;if(state&&typeof state.tokens==='number'){const elapsed=Math.max(0,now-state.lastRefill);tokens=Math.min(capacity,state.tokens+elapsed*this.rate);lastRefill=now;}const ttlMs=Math.max(1000,Math.ceil(capacity/this.rate*2.0));if(tokens>=cost){tokens-=cost;const consumed=capacity-Math.floor(tokens);const remaining=Math.floor(tokens);const resetTime=now+Math.ceil((capacity-tokens)/this.rate);await this.store.set(key,{tokens,lastRefill},ttlMs);return this.normalizeResult(true,capacity,remaining,consumed,0,resetTime);}const consumed=capacity-Math.floor(tokens);const remaining=Math.floor(tokens);const deficit=cost-tokens;const waitMs=Math.ceil(deficit/this.rate);const retryAfter=Math.max(1,Math.ceil(waitMs/1000));const resetTime=now+Math.ceil((capacity-tokens)/this.rate);await this.store.set(key,{tokens,lastRefill},ttlMs);return this.normalizeResult(false,capacity,remaining,consumed,retryAfter,resetTime);});}async getState(key,context={}){const capacity=await this.resolveLimit(context);const initialTokens=this.initialTokens!==undefined?this.initialTokens:capacity;if(this.isRedis()){const nowArg=typeof this.store.getNow==='function'?this.store.getNow():0;const res=await this.store.runScript('token-bucket',[key],[capacity,this.refillRate,this.refillInterval,0,initialTokens,nowArg]);return this.normalizeResult(res[0]===1,res[1],res[2],res[3],res[4],res[5]);}const now=Date.now();const state=await this.store.get(key);let tokens=initialTokens;if(state&&typeof state.tokens==='number'){const elapsed=Math.max(0,now-state.lastRefill);tokens=Math.min(capacity,state.tokens+elapsed*this.rate);}const consumed=capacity-Math.floor(tokens);const remaining=Math.floor(tokens);const resetTime=now+Math.ceil((capacity-tokens)/this.rate);const retryAfter=tokens<1?Math.max(1,Math.ceil((1-tokens)/this.rate/1000)):0;return this.normalizeResult(tokens>=1,capacity,remaining,consumed,retryAfter,resetTime);}}export default TokenBucket;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{defaultKeyGenerator,ipKeyGenerator,graphQLKeyGenerator}from'../utils/keyGenerator.js';import{evaluateSkipRule}from'../utils/skipUtils.js';import{RateLimiterRes}from'../core/rateLimiterRes.js';export function createCoreMiddleware(limiter,options={}){if(!limiter||typeof limiter.consume!=='function'){throw new TypeError('createCoreMiddleware requires a limiter with a consume() method');}let keyGen=defaultKeyGenerator;if(typeof options.keyGenerator==='function'){keyGen=options.keyGenerator;}else if(options.keyGenerator==='ip'){keyGen=ipKeyGenerator;}else if(options.keyGenerator==='graphql'){keyGen=graphQLKeyGenerator;}const rawSkip=options.skip!==undefined?options.skip:options.customSkip!==undefined?options.customSkip:options.customskip;const skipFn=typeof rawSkip==='function'?rawSkip:typeof rawSkip==='object'&&rawSkip!==null?(req,res)=>evaluateSkipRule(rawSkip,req,res,{generateKey:keyGen}):()=>false;const defaultHandler=(req,res,next,opts)=>{res.status(opts.statusCode);const msg=typeof opts.message==='function'?opts.message(req,res):opts.message;if(typeof msg==='object'){res.json(msg);}else{res.send(msg);}};const handler=options.handler||defaultHandler;const resolvedOptions={standardHeaders:options.standardHeaders!==undefined?options.standardHeaders:true,legacyHeaders:options.legacyHeaders!==undefined?options.legacyHeaders:false,...options,statusCode:options.statusCode!==undefined?options.statusCode:429,message:options.message!==undefined?options.message:'Too many requests, please try again later.'};const middleware=async(req,res,next)=>{try{const shouldSkip=await Promise.resolve(skipFn(req,res));if(shouldSkip){return next();}const key=await Promise.resolve(keyGen(req,res));let cost=1;if(typeof options.cost==='function'){cost=await Promise.resolve(options.cost(req,res));}else if(typeof options.cost==='number'){cost=options.cost;}if(!Number.isFinite(cost)||cost<=0){cost=1;}let res1;try{res1=await limiter.consume(key,cost,{req,res});}catch(err){if(err instanceof RateLimiterRes){res1=err;}else if(err&&typeof err==='object'&&Object.values(err).some(v=>v instanceof RateLimiterRes)){const resValues=Object.values(err).filter(v=>v instanceof RateLimiterRes);const maxMsBeforeNext=Math.max(...resValues.map(v=>v.msBeforeNext||0));const minRemaining=Math.min(...resValues.map(v=>v.remainingPoints||0));const maxConsumed=Math.max(...resValues.map(v=>v.consumedPoints||0));res1=new RateLimiterRes(minRemaining,maxMsBeforeNext,maxConsumed,false);res1._unionRejections=err;}else{throw err;}}if(res1&&!(res1 instanceof RateLimiterRes)&&typeof res1==='object'&&Object.values(res1).every(v=>v instanceof RateLimiterRes)){const resValues=Object.values(res1);const maxMsBeforeNext=Math.max(...resValues.map(v=>v.msBeforeNext||0));const minRemaining=Math.min(...resValues.map(v=>v.remainingPoints||0));const maxConsumed=Math.max(...resValues.map(v=>v.consumedPoints||0));res1=new RateLimiterRes(minRemaining,maxMsBeforeNext,maxConsumed,resValues.some(v=>v.isFirstInDuration));}const result=toMiddlewareResult(res1,limiter,cost);req.rateLimit=result;setCoreHeaders(res,result,{...resolvedOptions,limiter});if(result.allowed){return next();}return handler(req,res,next,resolvedOptions);}catch(err){if(options.failOpen){req.rateLimit={allowed:true,limit:limiter.points||60,remaining:1,consumed:0,retryAfter:0,resetTime:new Date(Date.now()+(limiter.msDuration||60000)),failedOpen:true};if(typeof options.onError==='function'){options.onError(err,req,res,next);}return next();}if(typeof options.onError==='function'){return options.onError(err,req,res,next);}return next(err);}};middleware.resetKey=async keyOrReq=>{const k=typeof keyOrReq==='object'&&keyOrReq!==null?keyGen(keyOrReq):keyOrReq;if(typeof limiter.delete==='function'){
|
|
1
|
+
import{EventEmitter}from'node:events';import{defaultKeyGenerator,ipKeyGenerator,graphQLKeyGenerator}from'../utils/keyGenerator.js';import{evaluateSkipRule}from'../utils/skipUtils.js';import{RateLimiterRes}from'../core/rateLimiterRes.js';export function createCoreMiddleware(limiter,options={}){if(!limiter||typeof limiter.consume!=='function'){throw new TypeError('createCoreMiddleware requires a limiter with a consume() method');}let keyGen=defaultKeyGenerator;if(typeof options.keyGenerator==='function'){keyGen=options.keyGenerator;}else if(options.keyGenerator==='ip'){keyGen=ipKeyGenerator;}else if(options.keyGenerator==='graphql'){keyGen=graphQLKeyGenerator;}const rawSkip=options.skip!==undefined?options.skip:options.customSkip!==undefined?options.customSkip:options.customskip;const skipFn=typeof rawSkip==='function'?rawSkip:typeof rawSkip==='object'&&rawSkip!==null?(req,res)=>evaluateSkipRule(rawSkip,req,res,{generateKey:keyGen}):()=>false;const defaultHandler=(req,res,next,opts)=>{res.status(opts.statusCode);const msg=typeof opts.message==='function'?opts.message(req,res):opts.message;if(typeof msg==='object'){res.json(msg);}else{res.send(msg);}};const handler=options.handler||defaultHandler;const resolvedOptions={standardHeaders:options.standardHeaders!==undefined?options.standardHeaders:true,legacyHeaders:options.legacyHeaders!==undefined?options.legacyHeaders:false,...options,statusCode:options.statusCode!==undefined?options.statusCode:429,message:options.message!==undefined?options.message:'Too many requests, please try again later.'};const middleware=async(req,res,next)=>{try{const shouldSkip=await Promise.resolve(skipFn(req,res));if(shouldSkip){return next();}const key=await Promise.resolve(keyGen(req,res));let cost=1;if(typeof options.cost==='function'){cost=await Promise.resolve(options.cost(req,res));}else if(typeof options.cost==='number'){cost=options.cost;}if(!Number.isFinite(cost)||cost<=0){cost=1;}let res1;try{res1=await limiter.consume(key,cost,{req,res});}catch(err){if(err instanceof RateLimiterRes){res1=err;}else if(err&&typeof err==='object'&&Object.values(err).some(v=>v instanceof RateLimiterRes)){const resValues=Object.values(err).filter(v=>v instanceof RateLimiterRes);const maxMsBeforeNext=Math.max(...resValues.map(v=>v.msBeforeNext||0));const minRemaining=Math.min(...resValues.map(v=>v.remainingPoints||0));const maxConsumed=Math.max(...resValues.map(v=>v.consumedPoints||0));res1=new RateLimiterRes(minRemaining,maxMsBeforeNext,maxConsumed,false);res1._unionRejections=err;}else{throw err;}}if(res1&&!(res1 instanceof RateLimiterRes)&&typeof res1==='object'&&Object.values(res1).every(v=>v instanceof RateLimiterRes)){const resValues=Object.values(res1);const maxMsBeforeNext=Math.max(...resValues.map(v=>v.msBeforeNext||0));const minRemaining=Math.min(...resValues.map(v=>v.remainingPoints||0));const maxConsumed=Math.max(...resValues.map(v=>v.consumedPoints||0));res1=new RateLimiterRes(minRemaining,maxMsBeforeNext,maxConsumed,resValues.some(v=>v.isFirstInDuration));}const result=toMiddlewareResult(res1,limiter,cost);req.rateLimit=result;setCoreHeaders(res,result,{...resolvedOptions,limiter});if(result.allowed){middleware.emit('consumed',req,res,result);return next();}middleware.emit('limitReached',req,res,result);middleware.emit('blocked',req,res,result);return handler(req,res,next,resolvedOptions);}catch(err){if(middleware.listenerCount('error')>0){middleware.emit('error',err,req,res);}if(options.failOpen){req.rateLimit={allowed:true,limit:limiter.points||60,remaining:1,consumed:0,retryAfter:0,resetTime:new Date(Date.now()+(limiter.msDuration||60000)),failedOpen:true};if(typeof options.onError==='function'){options.onError(err,req,res,next);}return next();}if(typeof options.onError==='function'){return options.onError(err,req,res,next);}return next(err);}};Object.setPrototypeOf(middleware,EventEmitter.prototype);EventEmitter.call(middleware);middleware.resetKey=async keyOrReq=>{const k=typeof keyOrReq==='object'&&keyOrReq!==null?keyGen(keyOrReq):keyOrReq;let res=false;if(typeof limiter.delete==='function'){res=await limiter.delete(k);}middleware.emit('reset',k);return res;};middleware.getState=async keyOrReq=>{const k=typeof keyOrReq==='object'&&keyOrReq!==null?keyGen(keyOrReq):keyOrReq;if(typeof limiter.get==='function'){return limiter.get(k);}return null;};middleware.limiter=limiter;return middleware;}function toMiddlewareResult(res,limiter,cost){const limit=typeof limiter.points==='number'?limiter.points:typeof res.consumedPoints==='number'&&typeof res.remainingPoints==='number'?res.consumedPoints+res.remainingPoints:60;const consumed=typeof res.consumedPoints==='number'?res.consumedPoints:cost;const msBeforeNext=typeof res.msBeforeNext==='number'?res.msBeforeNext:0;const rawReset=Number.isFinite(msBeforeNext)&&msBeforeNext>0?Date.now()+Math.min(msBeforeNext,31536000000):Date.now();const resetTime=new Date(rawReset);return{allowed:consumed<=limit,limit,remaining:Math.max(0,limit-consumed),consumed,retryAfter:msBeforeNext>0?Math.max(1,Math.ceil(msBeforeNext/1000)):0,resetTime:Number.isNaN(resetTime.getTime())?new Date(Date.now()+3600000):resetTime,cost};}function setCoreHeaders(res,result,options={}){if(!res||typeof res.setHeader!=='function'||res.headersSent){return;}const rawDiff=result.resetTime?(result.resetTime.getTime()-Date.now())/1000:0;const resetSeconds=Number.isFinite(rawDiff)?Math.max(0,Math.ceil(rawDiff)):0;if(options.standardHeaders){res.setHeader('RateLimit-Limit',String(result.limit));res.setHeader('RateLimit-Remaining',String(result.remaining));res.setHeader('RateLimit-Reset',String(resetSeconds));const policyWindowMs=options.windowMs||options.limiter&&(options.limiter.msDuration||(options.limiter.duration?options.limiter.duration*1000:null))||60000;if(options.standardHeaders!=='draft-6'){const windowSec=Math.ceil(policyWindowMs/1000);res.setHeader('RateLimit-Policy',`${result.limit};w=${windowSec}`);}}if(options.legacyHeaders){res.setHeader('X-RateLimit-Limit',String(result.limit));res.setHeader('X-RateLimit-Remaining',String(result.remaining));res.setHeader('X-RateLimit-Reset',String(resetSeconds));}if(!result.allowed){const retryAfter=Math.max(1,Number(result.retryAfter)||resetSeconds||1);res.setHeader('Retry-After',String(retryAfter));}}export default createCoreMiddleware;
|
|
@@ -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';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{defaultKeyGenerator,ipKeyGenerator,graphQLKeyGenerator}from'../utils/keyGenerator.js';import{setRateLimitHeaders}from'./headers.js';import{evaluateSkipRule}from'../utils/skipUtils.js';export function createMiddleware(algorithm,options){let keyGen=defaultKeyGenerator;if(typeof options.keyGenerator==='function'){keyGen=options.keyGenerator;}else if(options.keyGenerator==='ip'){keyGen=ipKeyGenerator;}else if(options.keyGenerator==='graphql'){keyGen=graphQLKeyGenerator;}else if(options.keyGenerator==='ip-endpoint'){keyGen=defaultKeyGenerator;}const rawSkip=options.skip!==undefined?options.skip:options.customSkip!==undefined?options.customSkip:options.customskip;const skipFn=typeof rawSkip==='function'?rawSkip:typeof rawSkip==='object'&&rawSkip!==null?(req,res)=>evaluateSkipRule(rawSkip,req,res,{generateKey:keyGen}):()=>false;const defaultHandler=(req,res,next,opts)=>{res.status(opts.statusCode);const msg=typeof opts.message==='function'?opts.message(req,res):opts.message;if(typeof msg==='object'){res.json(msg);}else{res.send(msg);}};const handler=options.handler||defaultHandler;const middleware=async(req,res,next)=>{try{const shouldSkip=await Promise.resolve(skipFn(req,res));if(shouldSkip){return next();}const key=await Promise.resolve(keyGen(req,res));let cost=1;if(typeof options.cost==='function'){cost=await Promise.resolve(options.cost(req,res));}else if(typeof options.cost==='number'){cost=options.cost;}if(!Number.isFinite(cost)||cost<=0){cost=1;}const result=await algorithm.consume(key,cost,{req,res});req.rateLimit=result;setRateLimitHeaders(res,result,options);if(result.allowed){return next();}return handler(req,res,next,options);}catch(err){if(options.failOpen){req.
|
|
1
|
+
import{EventEmitter}from'node:events';import{defaultKeyGenerator,ipKeyGenerator,graphQLKeyGenerator}from'../utils/keyGenerator.js';import{setRateLimitHeaders}from'./headers.js';import{evaluateSkipRule}from'../utils/skipUtils.js';export function createMiddleware(algorithm,options){let keyGen=defaultKeyGenerator;if(typeof options.keyGenerator==='function'){keyGen=options.keyGenerator;}else if(options.keyGenerator==='ip'){keyGen=ipKeyGenerator;}else if(options.keyGenerator==='graphql'){keyGen=graphQLKeyGenerator;}else if(options.keyGenerator==='ip-endpoint'){keyGen=defaultKeyGenerator;}const rawSkip=options.skip!==undefined?options.skip:options.customSkip!==undefined?options.customSkip:options.customskip;const skipFn=typeof rawSkip==='function'?rawSkip:typeof rawSkip==='object'&&rawSkip!==null?(req,res)=>evaluateSkipRule(rawSkip,req,res,{generateKey:keyGen}):()=>false;const defaultHandler=(req,res,next,opts)=>{res.status(opts.statusCode);const msg=typeof opts.message==='function'?opts.message(req,res):opts.message;if(typeof msg==='object'){res.json(msg);}else{res.send(msg);}};const handler=options.handler||defaultHandler;const middleware=async(req,res,next)=>{try{const shouldSkip=await Promise.resolve(skipFn(req,res));if(shouldSkip){return next();}const key=await Promise.resolve(keyGen(req,res));let cost=1;if(typeof options.cost==='function'){cost=await Promise.resolve(options.cost(req,res));}else if(typeof options.cost==='number'){cost=options.cost;}if(!Number.isFinite(cost)||cost<=0){cost=1;}const result=await algorithm.consume(key,cost,{req,res});req.rateLimit=result;setRateLimitHeaders(res,result,options);if(result.allowed){middleware.emit('consumed',req,res,result);return next();}middleware.emit('limitReached',req,res,result);middleware.emit('blocked',req,res,result);return handler(req,res,next,options);}catch(err){if(middleware.listenerCount('error')>0){middleware.emit('error',err,req,res);}if(options.failOpen){const resolvedLimit=typeof options.limit==='function'?await Promise.resolve(options.limit(req,res)).catch(()=>60):options.limit||options.capacity||0;req.rateLimit={allowed:true,limit:Number.isFinite(resolvedLimit)?Number(resolvedLimit):60,remaining:1,consumed:0,retryAfter:0,resetTime:new Date(Date.now()+60000),failedOpen:true};if(typeof options.onError==='function'){options.onError(err,req,res,next);}return next();}if(typeof options.onError==='function'){return options.onError(err,req,res,next);}return next(err);}};Object.setPrototypeOf(middleware,EventEmitter.prototype);EventEmitter.call(middleware);middleware.resetKey=async keyOrReq=>{const k=typeof keyOrReq==='object'&&keyOrReq!==null?keyGen(keyOrReq):keyOrReq;const res=await algorithm.reset(k);middleware.emit('reset',k);return res;};middleware.getState=keyOrReq=>{const k=typeof keyOrReq==='object'&&keyOrReq!==null?keyGen(keyOrReq):keyOrReq;return algorithm.getState(k);};middleware.close=()=>algorithm.close();middleware.algorithm=algorithm;middleware.store=algorithm.store;return middleware;}
|
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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import type { EventEmitter } from 'node:events';
|
|
2
3
|
|
|
3
4
|
export interface RateLimitInfo {
|
|
4
5
|
allowed: boolean;
|
|
@@ -25,6 +26,7 @@ export type KeyGenerator =
|
|
|
25
26
|
| ((req: Request, res: Response) => string | Promise<string>);
|
|
26
27
|
export type SkipFunction = (req: Request, res: Response) => boolean | Promise<boolean>;
|
|
27
28
|
export type CostFunction = (req: Request, res: Response) => number | Promise<number>;
|
|
29
|
+
export type DynamicLimit = number | ((req: Request, res: Response) => number | Promise<number>);
|
|
28
30
|
export type RateLimitHandler = (
|
|
29
31
|
req: Request,
|
|
30
32
|
res: Response,
|
|
@@ -63,7 +65,7 @@ export interface SkipRuleObject {
|
|
|
63
65
|
export type SkipOption = SkipFunction | SkipRuleObject | Array<SkipFunction | SkipRuleObject>;
|
|
64
66
|
|
|
65
67
|
export interface BaseOptions {
|
|
66
|
-
store?: 'memory' | 'redis' | BaseStore;
|
|
68
|
+
store?: 'memory' | 'redis' | 'redis-cluster' | 'cluster' | BaseStore;
|
|
67
69
|
keyPrefix?: string;
|
|
68
70
|
prefix?: string;
|
|
69
71
|
storePrefix?: string;
|
|
@@ -93,30 +95,30 @@ export interface BaseOptions {
|
|
|
93
95
|
|
|
94
96
|
export interface FixedWindowOptions extends BaseOptions {
|
|
95
97
|
algorithm?: 'fixed-window';
|
|
96
|
-
limit?:
|
|
97
|
-
max?:
|
|
98
|
+
limit?: DynamicLimit;
|
|
99
|
+
max?: DynamicLimit;
|
|
98
100
|
windowMs?: number;
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
export interface SlidingWindowLogOptions extends BaseOptions {
|
|
102
104
|
algorithm: 'sliding-window-log';
|
|
103
|
-
limit?:
|
|
104
|
-
max?:
|
|
105
|
+
limit?: DynamicLimit;
|
|
106
|
+
max?: DynamicLimit;
|
|
105
107
|
windowMs?: number;
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
export interface SlidingWindowCounterOptions extends BaseOptions {
|
|
109
111
|
algorithm: 'sliding-window-counter';
|
|
110
|
-
limit?:
|
|
111
|
-
max?:
|
|
112
|
+
limit?: DynamicLimit;
|
|
113
|
+
max?: DynamicLimit;
|
|
112
114
|
windowMs?: number;
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
export interface TokenBucketOptions extends BaseOptions {
|
|
116
118
|
algorithm: 'token-bucket';
|
|
117
|
-
capacity?:
|
|
118
|
-
limit?:
|
|
119
|
-
max?:
|
|
119
|
+
capacity?: DynamicLimit;
|
|
120
|
+
limit?: DynamicLimit;
|
|
121
|
+
max?: DynamicLimit;
|
|
120
122
|
refillRate?: number;
|
|
121
123
|
refillInterval?: number;
|
|
122
124
|
initialTokens?: number;
|
|
@@ -124,9 +126,9 @@ export interface TokenBucketOptions extends BaseOptions {
|
|
|
124
126
|
|
|
125
127
|
export interface LeakyBucketOptions extends BaseOptions {
|
|
126
128
|
algorithm: 'leaky-bucket';
|
|
127
|
-
capacity?:
|
|
128
|
-
limit?:
|
|
129
|
-
max?:
|
|
129
|
+
capacity?: DynamicLimit;
|
|
130
|
+
limit?: DynamicLimit;
|
|
131
|
+
max?: DynamicLimit;
|
|
130
132
|
leakRate?: number;
|
|
131
133
|
leakInterval?: number;
|
|
132
134
|
mode?: 'policing' | 'shaping';
|
|
@@ -141,7 +143,7 @@ export type RateLimitOptions =
|
|
|
141
143
|
| TokenBucketOptions
|
|
142
144
|
| LeakyBucketOptions;
|
|
143
145
|
|
|
144
|
-
export interface RateLimitMiddleware {
|
|
146
|
+
export interface RateLimitMiddleware extends EventEmitter {
|
|
145
147
|
(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
146
148
|
resetKey(key: string): Promise<boolean>;
|
|
147
149
|
getState(key: string): Promise<RateLimitInfo>;
|
|
@@ -166,6 +168,17 @@ export declare class MemoryStore extends BaseStore {
|
|
|
166
168
|
export declare class RedisStore extends BaseStore {
|
|
167
169
|
constructor(options?: {
|
|
168
170
|
client?: any;
|
|
171
|
+
redisClient?: any;
|
|
172
|
+
storeClient?: any;
|
|
173
|
+
clusterClient?: any;
|
|
174
|
+
redisCluster?: any;
|
|
175
|
+
cluster?: any;
|
|
176
|
+
clusters?: any;
|
|
177
|
+
clusterNodes?: any;
|
|
178
|
+
rootNodes?: any;
|
|
179
|
+
clusterOptions?: any;
|
|
180
|
+
isCluster?: boolean;
|
|
181
|
+
hashTag?: boolean;
|
|
169
182
|
sendCommand?: (...args: any[]) => Promise<any>;
|
|
170
183
|
url?: string;
|
|
171
184
|
keyPrefix?: string;
|
|
@@ -184,6 +197,7 @@ export declare class RedisStore extends BaseStore {
|
|
|
184
197
|
|
|
185
198
|
export declare class BaseAlgorithm {
|
|
186
199
|
constructor(options: any, store: BaseStore);
|
|
200
|
+
resolveLimit(context?: { req?: Request; res?: Response }): Promise<number>;
|
|
187
201
|
isRedis(): boolean;
|
|
188
202
|
normalizeResult(
|
|
189
203
|
allowed: boolean,
|
|
@@ -444,6 +458,10 @@ export interface RedisLimiterOptions extends RateLimiterOptions {
|
|
|
444
458
|
clientKind?: string;
|
|
445
459
|
/** Reject requests while the connection is not ready. */
|
|
446
460
|
rejectIfRedisNotReady?: boolean;
|
|
461
|
+
/** Wrap storage keys in a hash tag so they share a Redis Cluster slot. */
|
|
462
|
+
hashTag?: boolean;
|
|
463
|
+
/** Explicitly flag whether this client is connected to a Redis Cluster. */
|
|
464
|
+
isCluster?: boolean;
|
|
447
465
|
customIncrTtlLuaScript?: string;
|
|
448
466
|
customSetTtlLuaScript?: string;
|
|
449
467
|
}
|
|
@@ -461,12 +479,14 @@ export interface ClusterLimiterOptions extends RedisLimiterOptions {
|
|
|
461
479
|
hashTag?: boolean;
|
|
462
480
|
}
|
|
463
481
|
|
|
464
|
-
export declare class ClusterLimiter extends
|
|
482
|
+
export declare class ClusterLimiter extends RedisLimiter {
|
|
465
483
|
constructor(options: ClusterLimiterOptions);
|
|
466
484
|
}
|
|
467
485
|
|
|
468
486
|
export declare const RedisClusterLimiter: typeof ClusterLimiter;
|
|
469
487
|
export type RedisClusterLimiter = ClusterLimiter;
|
|
488
|
+
export declare const RateLimiterRedisCluster: typeof ClusterLimiter;
|
|
489
|
+
export type RateLimiterRedisCluster = ClusterLimiter;
|
|
470
490
|
|
|
471
491
|
export declare class RateLimiterClusterMaster {
|
|
472
492
|
constructor(targetCluster?: any);
|
|
@@ -699,7 +719,7 @@ export interface CoreMiddlewareOptions {
|
|
|
699
719
|
windowMs?: number;
|
|
700
720
|
}
|
|
701
721
|
|
|
702
|
-
export interface CoreMiddleware {
|
|
722
|
+
export interface CoreMiddleware extends EventEmitter {
|
|
703
723
|
(req: Request, res: Response, next: NextFunction): Promise<any>;
|
|
704
724
|
resetKey(keyOrReq: string | Request): Promise<boolean>;
|
|
705
725
|
getState(keyOrReq: string | Request): Promise<RateLimiterRes | null>;
|
|
@@ -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!=='function'&&(typeof options.limit!=='number'||options.limit<=0||!Number.isFinite(options.limit))){throw new ConfigurationError(`${options.algorithm} requires "limit" to be a positive number or a function`);}}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!=='function'&&(typeof options.capacity!=='number'||options.capacity<=0||!Number.isFinite(options.capacity))){throw new ConfigurationError('token-bucket requires "capacity" to be a positive number or a function');}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 if(typeof options.capacity==='number'){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!=='function'&&(typeof options.capacity!=='number'||options.capacity<=0||!Number.isFinite(options.capacity))){throw new ConfigurationError('leaky-bucket requires "capacity" to be a positive number or a function');}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.
|
|
3
|
+
"version": "2.1.0",
|
|
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",
|