limitly 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/algorithms/factory.d.ts +3 -0
- package/dist/algorithms/factory.d.ts.map +1 -0
- package/dist/algorithms/factory.js +18 -0
- package/dist/algorithms/factory.js.map +1 -0
- package/dist/algorithms/sliding-window.d.ts +10 -0
- package/dist/algorithms/sliding-window.d.ts.map +1 -0
- package/dist/algorithms/sliding-window.js +35 -0
- package/dist/algorithms/sliding-window.js.map +1 -0
- package/dist/algorithms/strategy.d.ts +2 -0
- package/dist/algorithms/strategy.d.ts.map +1 -0
- package/dist/algorithms/strategy.js +3 -0
- package/dist/algorithms/strategy.js.map +1 -0
- package/dist/algorithms/token-bucket.d.ts +10 -0
- package/dist/algorithms/token-bucket.d.ts.map +1 -0
- package/dist/algorithms/token-bucket.js +32 -0
- package/dist/algorithms/token-bucket.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/limiter.d.ts +19 -0
- package/dist/limiter.d.ts.map +1 -0
- package/dist/limiter.js +54 -0
- package/dist/limiter.js.map +1 -0
- package/dist/middleware/express.d.ts +5 -0
- package/dist/middleware/express.d.ts.map +1 -0
- package/dist/middleware/express.js +40 -0
- package/dist/middleware/express.js.map +1 -0
- package/dist/middleware/fastify.d.ts +9 -0
- package/dist/middleware/fastify.d.ts.map +1 -0
- package/dist/middleware/fastify.js +53 -0
- package/dist/middleware/fastify.js.map +1 -0
- package/dist/scripts/sliding.lua +38 -0
- package/dist/scripts/token.lua +41 -0
- package/dist/types/index.d.ts +49 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/headers.d.ts +6 -0
- package/dist/utils/headers.d.ts.map +1 -0
- package/dist/utils/headers.js +23 -0
- package/dist/utils/headers.js.map +1 -0
- package/dist/utils/redis.d.ts +4 -0
- package/dist/utils/redis.d.ts.map +1 -0
- package/dist/utils/redis.js +63 -0
- package/dist/utils/redis.js.map +1 -0
- package/dist/utils/scripts.d.ts +11 -0
- package/dist/utils/scripts.d.ts.map +1 -0
- package/dist/utils/scripts.js +66 -0
- package/dist/utils/scripts.js.map +1 -0
- package/package.json +93 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Arpan Das
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# redislimit
|
|
2
|
+
|
|
3
|
+
Distributed, Redis-powered rate limiting for Express and Fastify.
|
|
4
|
+
|
|
5
|
+
> Express-rate-limit, but distributed, Redis-powered, and production ready.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install redislimit ioredis
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### Express
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import express from "express";
|
|
19
|
+
import Redis from "ioredis";
|
|
20
|
+
import { createLimiter } from "redislimit";
|
|
21
|
+
|
|
22
|
+
const app = express();
|
|
23
|
+
const redis = new Redis();
|
|
24
|
+
const limiter = createLimiter({ redis });
|
|
25
|
+
|
|
26
|
+
app.use(
|
|
27
|
+
limiter.middleware({
|
|
28
|
+
algorithm: "sliding-window",
|
|
29
|
+
limit: 100,
|
|
30
|
+
window: 60,
|
|
31
|
+
key: (req) => req.ip,
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Fastify
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import Fastify from "fastify";
|
|
40
|
+
import Redis from "ioredis";
|
|
41
|
+
import { createLimiter } from "redislimit";
|
|
42
|
+
|
|
43
|
+
const fastify = Fastify();
|
|
44
|
+
const limiter = createLimiter({ redis: new Redis() });
|
|
45
|
+
|
|
46
|
+
await fastify.register(limiter.fastifyPlugin, {
|
|
47
|
+
algorithm: "token-bucket",
|
|
48
|
+
capacity: 50,
|
|
49
|
+
refillRate: 10,
|
|
50
|
+
key: (req) => req.ip,
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Algorithms
|
|
55
|
+
|
|
56
|
+
### Sliding Window
|
|
57
|
+
|
|
58
|
+
Uses Redis Sorted Sets for precise rate limiting over a rolling time window.
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
limiter.middleware({
|
|
62
|
+
algorithm: "sliding-window",
|
|
63
|
+
limit: 100,
|
|
64
|
+
window: 60, // seconds
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Token Bucket
|
|
69
|
+
|
|
70
|
+
Supports burst traffic with configurable refill rate.
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
limiter.middleware({
|
|
74
|
+
algorithm: "token-bucket",
|
|
75
|
+
capacity: 100,
|
|
76
|
+
refillRate: 10, // tokens per second
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Redis Connection
|
|
81
|
+
|
|
82
|
+
Supports multiple connection modes:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// Existing Redis instance
|
|
86
|
+
createLimiter({ redis: new Redis() });
|
|
87
|
+
|
|
88
|
+
// Connection URL
|
|
89
|
+
createLimiter({ redis: "redis://localhost:6379" });
|
|
90
|
+
|
|
91
|
+
// Options object
|
|
92
|
+
createLimiter({ redis: { host: "localhost", port: 6379 } });
|
|
93
|
+
|
|
94
|
+
// Cluster
|
|
95
|
+
createLimiter({
|
|
96
|
+
redis: {
|
|
97
|
+
nodes: [{ host: "127.0.0.1", port: 7000 }],
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Key Extraction
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// IP-based
|
|
106
|
+
key: (req) => req.ip
|
|
107
|
+
|
|
108
|
+
// API Key
|
|
109
|
+
key: (req) => req.headers["x-api-key"]
|
|
110
|
+
|
|
111
|
+
// User ID
|
|
112
|
+
key: (req) => req.user.id
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Response Headers
|
|
116
|
+
|
|
117
|
+
Standard rate limit headers are set by default:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
X-RateLimit-Limit: 100
|
|
121
|
+
X-RateLimit-Remaining: 45
|
|
122
|
+
X-RateLimit-Reset: 1710000000
|
|
123
|
+
Retry-After: 15
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Disable with `headers: false`.
|
|
127
|
+
|
|
128
|
+
## Custom Limit Response
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
limiter.middleware({
|
|
132
|
+
algorithm: "sliding-window",
|
|
133
|
+
limit: 100,
|
|
134
|
+
window: 60,
|
|
135
|
+
onLimitReached(req, res) {
|
|
136
|
+
res.status(429).json({ code: "RATE_LIMITED" });
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Fail Open / Closed
|
|
142
|
+
|
|
143
|
+
When Redis is unavailable:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
createLimiter({
|
|
147
|
+
redis: new Redis(),
|
|
148
|
+
failOpen: true, // allow traffic (default)
|
|
149
|
+
// failOpen: false, // block with 503
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Extensibility
|
|
154
|
+
|
|
155
|
+
Implement custom algorithms with the `RateLimitStrategy` interface:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
interface RateLimitStrategy {
|
|
159
|
+
consume(key: string): Promise<RateLimitResult>;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Performance
|
|
164
|
+
|
|
165
|
+
- Atomic operations via Lua scripts (`EVALSHA`)
|
|
166
|
+
- Automatic key cleanup with `EXPIRE`
|
|
167
|
+
- P95 < 5ms per check (excluding network latency)
|
|
168
|
+
- 20,000+ checks/sec on a single Redis node
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/algorithms/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,WAAW,EACZ,MAAM,UAAU,CAAC;AAIlB,wBAAgB,cAAc,CAC5B,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,eAAe,EACvB,SAAS,CAAC,EAAE,MAAM,GACjB,iBAAiB,CAanB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createStrategy = createStrategy;
|
|
4
|
+
const sliding_window_1 = require("./sliding-window");
|
|
5
|
+
const token_bucket_1 = require("./token-bucket");
|
|
6
|
+
function createStrategy(redis, config, keyPrefix) {
|
|
7
|
+
switch (config.algorithm) {
|
|
8
|
+
case "sliding-window":
|
|
9
|
+
return new sliding_window_1.SlidingWindowStrategy(redis, config, keyPrefix);
|
|
10
|
+
case "token-bucket":
|
|
11
|
+
return new token_bucket_1.TokenBucketStrategy(redis, config, keyPrefix);
|
|
12
|
+
default: {
|
|
13
|
+
const exhaustive = config;
|
|
14
|
+
throw new Error(`Unknown algorithm: ${exhaustive.algorithm}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/algorithms/factory.ts"],"names":[],"mappings":";;AAQA,wCAiBC;AApBD,qDAAyD;AACzD,iDAAqD;AAErD,SAAgB,cAAc,CAC5B,KAAkB,EAClB,MAAuB,EACvB,SAAkB;IAElB,QAAQ,MAAM,CAAC,SAAS,EAAE,CAAC;QACzB,KAAK,gBAAgB;YACnB,OAAO,IAAI,sCAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;QAC7D,KAAK,cAAc;YACjB,OAAO,IAAI,kCAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;QAC3D,OAAO,CAAC,CAAC,CAAC;YACR,MAAM,UAAU,GAAU,MAAM,CAAC;YACjC,MAAM,IAAI,KAAK,CACb,sBAAuB,UAA8B,CAAC,SAAS,EAAE,CAClE,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RateLimitResult, RateLimitStrategy, RedisClient, SlidingWindowConfig } from "../types";
|
|
2
|
+
export declare class SlidingWindowStrategy implements RateLimitStrategy {
|
|
3
|
+
private readonly redis;
|
|
4
|
+
private readonly limit;
|
|
5
|
+
private readonly window;
|
|
6
|
+
private readonly keyPrefix;
|
|
7
|
+
constructor(redis: RedisClient, config: SlidingWindowConfig, keyPrefix?: string);
|
|
8
|
+
consume(key: string): Promise<RateLimitResult>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=sliding-window.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sliding-window.d.ts","sourceRoot":"","sources":["../../src/algorithms/sliding-window.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,mBAAmB,EACpB,MAAM,UAAU,CAAC;AAIlB,qBAAa,qBAAsB,YAAW,iBAAiB;IAC7D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAGjC,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,mBAAmB,EAC3B,SAAS,SAAe;IAQpB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;CAsBrD"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SlidingWindowStrategy = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const redis_1 = require("../utils/redis");
|
|
6
|
+
const scripts_1 = require("../utils/scripts");
|
|
7
|
+
class SlidingWindowStrategy {
|
|
8
|
+
constructor(redis, config, keyPrefix = "redislimit") {
|
|
9
|
+
this.redis = redis;
|
|
10
|
+
this.limit = config.limit;
|
|
11
|
+
this.window = config.window;
|
|
12
|
+
this.keyPrefix = keyPrefix;
|
|
13
|
+
}
|
|
14
|
+
async consume(key) {
|
|
15
|
+
const redisKey = (0, redis_1.buildKey)(this.keyPrefix, `sw:${key}`);
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
const requestId = (0, crypto_1.randomUUID)();
|
|
18
|
+
const result = await (0, scripts_1.evalScript)(this.redis, "sliding", [redisKey], [
|
|
19
|
+
this.limit,
|
|
20
|
+
this.window,
|
|
21
|
+
now,
|
|
22
|
+
requestId,
|
|
23
|
+
]);
|
|
24
|
+
const parsed = (0, scripts_1.parseScriptResult)(result);
|
|
25
|
+
return {
|
|
26
|
+
allowed: parsed.allowed,
|
|
27
|
+
limit: parsed.limit,
|
|
28
|
+
remaining: parsed.remaining,
|
|
29
|
+
reset: parsed.reset,
|
|
30
|
+
retryAfter: parsed.retryAfter || undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.SlidingWindowStrategy = SlidingWindowStrategy;
|
|
35
|
+
//# sourceMappingURL=sliding-window.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sliding-window.js","sourceRoot":"","sources":["../../src/algorithms/sliding-window.ts"],"names":[],"mappings":";;;AAAA,mCAAoC;AAOpC,0CAA0C;AAC1C,8CAAiE;AAEjE,MAAa,qBAAqB;IAMhC,YACE,KAAkB,EAClB,MAA2B,EAC3B,SAAS,GAAG,YAAY;QAExB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;QAC1B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,GAAW;QACvB,MAAM,QAAQ,GAAG,IAAA,gBAAQ,EAAC,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,EAAE,CAAC,CAAC;QACvD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,SAAS,GAAG,IAAA,mBAAU,GAAE,CAAC;QAE/B,MAAM,MAAM,GAAG,MAAM,IAAA,oBAAU,EAAC,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,CAAC,QAAQ,CAAC,EAAE;YACjE,IAAI,CAAC,KAAK;YACV,IAAI,CAAC,MAAM;YACX,GAAG;YACH,SAAS;SACV,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,IAAA,2BAAiB,EAAC,MAAM,CAAC,CAAC;QAEzC,OAAO;YACL,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,SAAS;SAC3C,CAAC;IACJ,CAAC;CACF;AAvCD,sDAuCC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strategy.d.ts","sourceRoot":"","sources":["../../src/algorithms/strategy.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strategy.js","sourceRoot":"","sources":["../../src/algorithms/strategy.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RateLimitResult, RateLimitStrategy, RedisClient, TokenBucketConfig } from "../types";
|
|
2
|
+
export declare class TokenBucketStrategy implements RateLimitStrategy {
|
|
3
|
+
private readonly redis;
|
|
4
|
+
private readonly capacity;
|
|
5
|
+
private readonly refillRate;
|
|
6
|
+
private readonly keyPrefix;
|
|
7
|
+
constructor(redis: RedisClient, config: TokenBucketConfig, keyPrefix?: string);
|
|
8
|
+
consume(key: string): Promise<RateLimitResult>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=token-bucket.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-bucket.d.ts","sourceRoot":"","sources":["../../src/algorithms/token-bucket.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAIlB,qBAAa,mBAAoB,YAAW,iBAAiB;IAC3D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAGjC,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,iBAAiB,EACzB,SAAS,SAAe;IAQpB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;CAoBrD"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TokenBucketStrategy = void 0;
|
|
4
|
+
const redis_1 = require("../utils/redis");
|
|
5
|
+
const scripts_1 = require("../utils/scripts");
|
|
6
|
+
class TokenBucketStrategy {
|
|
7
|
+
constructor(redis, config, keyPrefix = "redislimit") {
|
|
8
|
+
this.redis = redis;
|
|
9
|
+
this.capacity = config.capacity;
|
|
10
|
+
this.refillRate = config.refillRate;
|
|
11
|
+
this.keyPrefix = keyPrefix;
|
|
12
|
+
}
|
|
13
|
+
async consume(key) {
|
|
14
|
+
const redisKey = (0, redis_1.buildKey)(this.keyPrefix, `tb:${key}`);
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const result = await (0, scripts_1.evalScript)(this.redis, "token", [redisKey], [
|
|
17
|
+
this.capacity,
|
|
18
|
+
this.refillRate,
|
|
19
|
+
now,
|
|
20
|
+
]);
|
|
21
|
+
const parsed = (0, scripts_1.parseScriptResult)(result);
|
|
22
|
+
return {
|
|
23
|
+
allowed: parsed.allowed,
|
|
24
|
+
limit: parsed.limit,
|
|
25
|
+
remaining: parsed.remaining,
|
|
26
|
+
reset: parsed.reset,
|
|
27
|
+
retryAfter: parsed.retryAfter || undefined,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.TokenBucketStrategy = TokenBucketStrategy;
|
|
32
|
+
//# sourceMappingURL=token-bucket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-bucket.js","sourceRoot":"","sources":["../../src/algorithms/token-bucket.ts"],"names":[],"mappings":";;;AAMA,0CAA0C;AAC1C,8CAAiE;AAEjE,MAAa,mBAAmB;IAM9B,YACE,KAAkB,EAClB,MAAyB,EACzB,SAAS,GAAG,YAAY;QAExB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;QAChC,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;QACpC,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,GAAW;QACvB,MAAM,QAAQ,GAAG,IAAA,gBAAQ,EAAC,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,EAAE,CAAC,CAAC;QACvD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAEvB,MAAM,MAAM,GAAG,MAAM,IAAA,oBAAU,EAAC,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE;YAC/D,IAAI,CAAC,QAAQ;YACb,IAAI,CAAC,UAAU;YACf,GAAG;SACJ,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,IAAA,2BAAiB,EAAC,MAAM,CAAC,CAAC;QAEzC,OAAO;YACL,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,SAAS;SAC3C,CAAC;IACJ,CAAC;CACF;AArCD,kDAqCC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { RedisLimit, createLimiter } from "./limiter";
|
|
2
|
+
export { createExpressMiddleware } from "./middleware/express";
|
|
3
|
+
export { createFastifyPlugin, redisLimitPlugin } from "./middleware/fastify";
|
|
4
|
+
export { SlidingWindowStrategy } from "./algorithms/sliding-window";
|
|
5
|
+
export { TokenBucketStrategy } from "./algorithms/token-bucket";
|
|
6
|
+
export type { RateLimitStrategy } from "./algorithms/strategy";
|
|
7
|
+
export type { AlgorithmConfig, BaseMiddlewareOptions, MiddlewareOptions, RateLimitHeaders, RateLimitResult, RedisClient, RedisConfig, RedisLimitOptions, SlidingWindowConfig, TokenBucketConfig, } from "./types";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,YAAY,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,YAAY,EACV,eAAe,EACf,qBAAqB,EACrB,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TokenBucketStrategy = exports.SlidingWindowStrategy = exports.redisLimitPlugin = exports.createFastifyPlugin = exports.createExpressMiddleware = exports.createLimiter = exports.RedisLimit = void 0;
|
|
4
|
+
var limiter_1 = require("./limiter");
|
|
5
|
+
Object.defineProperty(exports, "RedisLimit", { enumerable: true, get: function () { return limiter_1.RedisLimit; } });
|
|
6
|
+
Object.defineProperty(exports, "createLimiter", { enumerable: true, get: function () { return limiter_1.createLimiter; } });
|
|
7
|
+
var express_1 = require("./middleware/express");
|
|
8
|
+
Object.defineProperty(exports, "createExpressMiddleware", { enumerable: true, get: function () { return express_1.createExpressMiddleware; } });
|
|
9
|
+
var fastify_1 = require("./middleware/fastify");
|
|
10
|
+
Object.defineProperty(exports, "createFastifyPlugin", { enumerable: true, get: function () { return fastify_1.createFastifyPlugin; } });
|
|
11
|
+
Object.defineProperty(exports, "redisLimitPlugin", { enumerable: true, get: function () { return fastify_1.redisLimitPlugin; } });
|
|
12
|
+
var sliding_window_1 = require("./algorithms/sliding-window");
|
|
13
|
+
Object.defineProperty(exports, "SlidingWindowStrategy", { enumerable: true, get: function () { return sliding_window_1.SlidingWindowStrategy; } });
|
|
14
|
+
var token_bucket_1 = require("./algorithms/token-bucket");
|
|
15
|
+
Object.defineProperty(exports, "TokenBucketStrategy", { enumerable: true, get: function () { return token_bucket_1.TokenBucketStrategy; } });
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,qCAAsD;AAA7C,qGAAA,UAAU,OAAA;AAAE,wGAAA,aAAa,OAAA;AAClC,gDAA+D;AAAtD,kHAAA,uBAAuB,OAAA;AAChC,gDAA6E;AAApE,8GAAA,mBAAmB,OAAA;AAAE,2GAAA,gBAAgB,OAAA;AAC9C,8DAAoE;AAA3D,uHAAA,qBAAqB,OAAA;AAC9B,0DAAgE;AAAvD,mHAAA,mBAAmB,OAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
2
|
+
import type { AlgorithmConfig, MiddlewareOptions, RateLimitResult, RateLimitStrategy, RedisClient, RedisLimitOptions } from "./types";
|
|
3
|
+
export declare class RedisLimit {
|
|
4
|
+
private readonly redis;
|
|
5
|
+
private readonly failOpen;
|
|
6
|
+
private readonly keyPrefix;
|
|
7
|
+
constructor(options: RedisLimitOptions);
|
|
8
|
+
getRedis(): RedisClient;
|
|
9
|
+
createStrategy(config: AlgorithmConfig): RateLimitStrategy;
|
|
10
|
+
check(key: string, config: AlgorithmConfig, options?: {
|
|
11
|
+
failOpen?: boolean;
|
|
12
|
+
}): Promise<RateLimitResult>;
|
|
13
|
+
middleware(options: MiddlewareOptions): (req: import("express").Request, res: import("express").Response, next: import("express").NextFunction) => Promise<void>;
|
|
14
|
+
get fastifyPlugin(): FastifyPluginAsync<MiddlewareOptions>;
|
|
15
|
+
private createFailOpenResult;
|
|
16
|
+
}
|
|
17
|
+
export declare function createLimiter(options: RedisLimitOptions): RedisLimit;
|
|
18
|
+
export type { MiddlewareOptions };
|
|
19
|
+
//# sourceMappingURL=limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limiter.d.ts","sourceRoot":"","sources":["../src/limiter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAGjB,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,OAAO,EAAE,iBAAiB;IAMtC,QAAQ,IAAI,WAAW;IAIvB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,iBAAiB;IAIpD,KAAK,CACT,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,eAAe,EACvB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAC/B,OAAO,CAAC,eAAe,CAAC;IAc3B,UAAU,CAAC,OAAO,EAAE,iBAAiB;IAIrC,IAAI,aAAa,IAAI,kBAAkB,CAAC,iBAAiB,CAAC,CAEzD;IAED,OAAO,CAAC,oBAAoB;CAW7B;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAEpE;AAED,YAAY,EAAE,iBAAiB,EAAE,CAAC"}
|
package/dist/limiter.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RedisLimit = void 0;
|
|
4
|
+
exports.createLimiter = createLimiter;
|
|
5
|
+
const factory_1 = require("./algorithms/factory");
|
|
6
|
+
const express_1 = require("./middleware/express");
|
|
7
|
+
const fastify_1 = require("./middleware/fastify");
|
|
8
|
+
const redis_1 = require("./utils/redis");
|
|
9
|
+
class RedisLimit {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.redis = (0, redis_1.createRedisClient)(options.redis);
|
|
12
|
+
this.failOpen = options.failOpen ?? true;
|
|
13
|
+
this.keyPrefix = options.keyPrefix ?? "redislimit";
|
|
14
|
+
}
|
|
15
|
+
getRedis() {
|
|
16
|
+
return this.redis;
|
|
17
|
+
}
|
|
18
|
+
createStrategy(config) {
|
|
19
|
+
return (0, factory_1.createStrategy)(this.redis, config, this.keyPrefix);
|
|
20
|
+
}
|
|
21
|
+
async check(key, config, options) {
|
|
22
|
+
const strategy = this.createStrategy(config);
|
|
23
|
+
const shouldFailOpen = options?.failOpen ?? this.failOpen;
|
|
24
|
+
try {
|
|
25
|
+
return await strategy.consume(key);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (shouldFailOpen) {
|
|
29
|
+
return this.createFailOpenResult(config);
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
middleware(options) {
|
|
35
|
+
return (0, express_1.createExpressMiddleware)(this)(options);
|
|
36
|
+
}
|
|
37
|
+
get fastifyPlugin() {
|
|
38
|
+
return (0, fastify_1.createFastifyPlugin)(this);
|
|
39
|
+
}
|
|
40
|
+
createFailOpenResult(config) {
|
|
41
|
+
const limit = config.algorithm === "sliding-window" ? config.limit : config.capacity;
|
|
42
|
+
return {
|
|
43
|
+
allowed: true,
|
|
44
|
+
limit,
|
|
45
|
+
remaining: limit,
|
|
46
|
+
reset: Math.ceil(Date.now() / 1000) + 60,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.RedisLimit = RedisLimit;
|
|
51
|
+
function createLimiter(options) {
|
|
52
|
+
return new RedisLimit(options);
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limiter.js","sourceRoot":"","sources":["../src/limiter.ts"],"names":[],"mappings":";;;AAwEA,sCAEC;AA1ED,kDAAsD;AACtD,kDAA+D;AAC/D,kDAA2D;AAU3D,yCAAkD;AAElD,MAAa,UAAU;IAKrB,YAAY,OAA0B;QACpC,IAAI,CAAC,KAAK,GAAG,IAAA,yBAAiB,EAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC9C,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC;QACzC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,YAAY,CAAC;IACrD,CAAC;IAED,QAAQ;QACN,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,cAAc,CAAC,MAAuB;QACpC,OAAO,IAAA,wBAAc,EAAC,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAC5D,CAAC;IAED,KAAK,CAAC,KAAK,CACT,GAAW,EACX,MAAuB,EACvB,OAAgC;QAEhC,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QAC7C,MAAM,cAAc,GAAG,OAAO,EAAE,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC;QAE1D,IAAI,CAAC;YACH,OAAO,MAAM,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,cAAc,EAAE,CAAC;gBACnB,OAAO,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC;YAC3C,CAAC;YACD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,UAAU,CAAC,OAA0B;QACnC,OAAO,IAAA,iCAAuB,EAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;IAChD,CAAC;IAED,IAAI,aAAa;QACf,OAAO,IAAA,6BAAmB,EAAC,IAAI,CAAC,CAAC;IACnC,CAAC;IAEO,oBAAoB,CAAC,MAAuB;QAClD,MAAM,KAAK,GACT,MAAM,CAAC,SAAS,KAAK,gBAAgB,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC;QAEzE,OAAO;YACL,OAAO,EAAE,IAAI;YACb,KAAK;YACL,SAAS,EAAE,KAAK;YAChB,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE;SACzC,CAAC;IACJ,CAAC;CACF;AAxDD,gCAwDC;AAED,SAAgB,aAAa,CAAC,OAA0B;IACtD,OAAO,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;AACjC,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
import type { RedisLimit } from "../limiter";
|
|
3
|
+
import type { MiddlewareOptions } from "../types";
|
|
4
|
+
export declare function createExpressMiddleware(limiter: RedisLimit): (options: MiddlewareOptions) => (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
5
|
+
//# sourceMappingURL=express.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/middleware/express.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAKlD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,UAAU,IAC9B,SAAS,iBAAiB,MASjD,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,MAAM,YAAY,KACjB,OAAO,CAAC,IAAI,CAAC,CA+BnB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createExpressMiddleware = createExpressMiddleware;
|
|
4
|
+
const headers_1 = require("../utils/headers");
|
|
5
|
+
const DEFAULT_KEY = (req) => req.ip ?? "unknown";
|
|
6
|
+
function createExpressMiddleware(limiter) {
|
|
7
|
+
return function middleware(options) {
|
|
8
|
+
const strategy = limiter.createStrategy(options);
|
|
9
|
+
const keyExtractor = (options.key ?? DEFAULT_KEY);
|
|
10
|
+
const sendHeaders = options.headers !== false;
|
|
11
|
+
const failOpen = options.failOpen ?? true;
|
|
12
|
+
return async (req, res, next) => {
|
|
13
|
+
const key = keyExtractor(req) ?? "unknown";
|
|
14
|
+
let result;
|
|
15
|
+
try {
|
|
16
|
+
result = await strategy.consume(key);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
if (failOpen) {
|
|
20
|
+
return next();
|
|
21
|
+
}
|
|
22
|
+
res.status(503).json({ error: "Service Unavailable" });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (sendHeaders) {
|
|
26
|
+
(0, headers_1.setHeaders)(res, (0, headers_1.buildRateLimitHeaders)(result));
|
|
27
|
+
}
|
|
28
|
+
if (result.allowed) {
|
|
29
|
+
next();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (options.onLimitReached) {
|
|
33
|
+
await options.onLimitReached(req, res);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
res.status(429).json({ error: "Too Many Requests" });
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=express.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/middleware/express.ts"],"names":[],"mappings":";;AAOA,0DA4CC;AAhDD,8CAAqE;AAErE,MAAM,WAAW,GAAG,CAAC,GAAY,EAAU,EAAE,CAAC,GAAG,CAAC,EAAE,IAAI,SAAS,CAAC;AAElE,SAAgB,uBAAuB,CAAC,OAAmB;IACzD,OAAO,SAAS,UAAU,CAAC,OAA0B;QACnD,MAAM,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;QACjD,MAAM,YAAY,GAAG,CAAC,OAAO,CAAC,GAAG,IAAI,WAAW,CAEzB,CAAC;QACxB,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,KAAK,KAAK,CAAC;QAC9C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC;QAE1C,OAAO,KAAK,EACV,GAAY,EACZ,GAAa,EACb,IAAkB,EACH,EAAE;YACjB,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC;YAE3C,IAAI,MAAM,CAAC;YACX,IAAI,CAAC;gBACH,MAAM,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACvC,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,QAAQ,EAAE,CAAC;oBACb,OAAO,IAAI,EAAE,CAAC;gBAChB,CAAC;gBACD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAC;gBACvD,OAAO;YACT,CAAC;YAED,IAAI,WAAW,EAAE,CAAC;gBAChB,IAAA,oBAAU,EAAC,GAAG,EAAE,IAAA,+BAAqB,EAAC,MAAM,CAAC,CAAC,CAAC;YACjD,CAAC;YAED,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBACnB,IAAI,EAAE,CAAC;gBACP,OAAO;YACT,CAAC;YAED,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;gBAC3B,MAAM,OAAO,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;gBACvC,OAAO;YACT,CAAC;YAED,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,CAAC;QACvD,CAAC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
2
|
+
import type { RedisLimit } from "../limiter";
|
|
3
|
+
import type { MiddlewareOptions } from "../types";
|
|
4
|
+
export type FastifyRateLimitOptions = MiddlewareOptions & {
|
|
5
|
+
limiter: RedisLimit;
|
|
6
|
+
};
|
|
7
|
+
export declare function createFastifyPlugin(limiter: RedisLimit): FastifyPluginAsync<MiddlewareOptions>;
|
|
8
|
+
export declare const redisLimitPlugin: FastifyPluginAsync<FastifyRateLimitOptions>;
|
|
9
|
+
//# sourceMappingURL=fastify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fastify.d.ts","sourceRoot":"","sources":["../../src/middleware/fastify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAgC,MAAM,SAAS,CAAC;AAChF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAKlD,MAAM,MAAM,uBAAuB,GAAG,iBAAiB,GAAG;IACxD,OAAO,EAAE,UAAU,CAAC;CACrB,CAAC;AAaF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,UAAU,GAAG,kBAAkB,CAAC,iBAAiB,CAAC,CAuC9F;AAED,eAAO,MAAM,gBAAgB,EAAE,kBAAkB,CAAC,uBAAuB,CAOxE,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.redisLimitPlugin = void 0;
|
|
4
|
+
exports.createFastifyPlugin = createFastifyPlugin;
|
|
5
|
+
const headers_1 = require("../utils/headers");
|
|
6
|
+
const DEFAULT_KEY = (req) => req.ip;
|
|
7
|
+
function setFastifyHeaders(reply, headers) {
|
|
8
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
9
|
+
if (value !== undefined) {
|
|
10
|
+
reply.header(name, value);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function createFastifyPlugin(limiter) {
|
|
15
|
+
const plugin = async (fastify, options) => {
|
|
16
|
+
const strategy = limiter.createStrategy(options);
|
|
17
|
+
const keyExtractor = (options.key ?? DEFAULT_KEY);
|
|
18
|
+
const sendHeaders = options.headers !== false;
|
|
19
|
+
const failOpen = options.failOpen ?? true;
|
|
20
|
+
fastify.addHook("preHandler", async (request, reply) => {
|
|
21
|
+
const key = keyExtractor(request) ?? "unknown";
|
|
22
|
+
let result;
|
|
23
|
+
try {
|
|
24
|
+
result = await strategy.consume(key);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
if (failOpen) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
reply.status(503).send({ error: "Service Unavailable" });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (sendHeaders) {
|
|
34
|
+
setFastifyHeaders(reply, (0, headers_1.buildRateLimitHeaders)(result));
|
|
35
|
+
}
|
|
36
|
+
if (!result.allowed) {
|
|
37
|
+
if (options.onLimitReached) {
|
|
38
|
+
await options.onLimitReached(request, reply);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
reply.status(429).send({ error: "Too Many Requests" });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
return plugin;
|
|
46
|
+
}
|
|
47
|
+
const redisLimitPlugin = async (fastify, options) => {
|
|
48
|
+
const { limiter, ...rest } = options;
|
|
49
|
+
const middlewareOptions = rest;
|
|
50
|
+
await fastify.register(createFastifyPlugin(limiter), middlewareOptions);
|
|
51
|
+
};
|
|
52
|
+
exports.redisLimitPlugin = redisLimitPlugin;
|
|
53
|
+
//# sourceMappingURL=fastify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fastify.js","sourceRoot":"","sources":["../../src/middleware/fastify.ts"],"names":[],"mappings":";;;AAsBA,kDAuCC;AA1DD,8CAAyD;AAEzD,MAAM,WAAW,GAAG,CAAC,GAAmB,EAAU,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;AAM5D,SAAS,iBAAiB,CACxB,KAAmB,EACnB,OAAiD;IAEjD,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAgB,mBAAmB,CAAC,OAAmB;IACrD,MAAM,MAAM,GAA0C,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;QAC/E,MAAM,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;QACjD,MAAM,YAAY,GAAG,CAAC,OAAO,CAAC,GAAG,IAAI,WAAW,CAEzB,CAAC;QACxB,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,KAAK,KAAK,CAAC;QAC9C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,IAAI,CAAC;QAE1C,OAAO,CAAC,OAAO,CAAC,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;YACrD,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,SAAS,CAAC;YAE/C,IAAI,MAAM,CAAC;YACX,IAAI,CAAC;gBACH,MAAM,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACvC,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,QAAQ,EAAE,CAAC;oBACb,OAAO;gBACT,CAAC;gBACD,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAC;gBACzD,OAAO;YACT,CAAC;YAED,IAAI,WAAW,EAAE,CAAC;gBAChB,iBAAiB,CAAC,KAAK,EAAE,IAAA,+BAAqB,EAAC,MAAM,CAAC,CAAC,CAAC;YAC1D,CAAC;YAED,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;oBAC3B,MAAM,OAAO,CAAC,cAAc,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;oBAC7C,OAAO;gBACT,CAAC;gBAED,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,CAAC;YACzD,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,OAAO,MAAM,CAAC;AAChB,CAAC;AAEM,MAAM,gBAAgB,GAAgD,KAAK,EAChF,OAAO,EACP,OAAO,EACP,EAAE;IACF,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IACrC,MAAM,iBAAiB,GAAG,IAAyB,CAAC;IACpD,MAAM,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC,OAAO,CAAC,EAAE,iBAAiB,CAAC,CAAC;AAC1E,CAAC,CAAC;AAPW,QAAA,gBAAgB,oBAO3B"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
-- Sliding window rate limiter using sorted sets
|
|
2
|
+
-- KEYS[1] = rate limit key
|
|
3
|
+
-- ARGV[1] = limit
|
|
4
|
+
-- ARGV[2] = window (seconds)
|
|
5
|
+
-- ARGV[3] = current timestamp (milliseconds)
|
|
6
|
+
-- ARGV[4] = unique request id
|
|
7
|
+
|
|
8
|
+
local key = KEYS[1]
|
|
9
|
+
local limit = tonumber(ARGV[1])
|
|
10
|
+
local window = tonumber(ARGV[2])
|
|
11
|
+
local now = tonumber(ARGV[3])
|
|
12
|
+
local request_id = ARGV[4]
|
|
13
|
+
|
|
14
|
+
local window_start = now - (window * 1000)
|
|
15
|
+
|
|
16
|
+
-- Remove entries outside the sliding window
|
|
17
|
+
redis.call('ZREMRANGEBYSCORE', key, '-inf', window_start)
|
|
18
|
+
|
|
19
|
+
local current_count = redis.call('ZCARD', key)
|
|
20
|
+
|
|
21
|
+
if current_count < limit then
|
|
22
|
+
redis.call('ZADD', key, now, request_id)
|
|
23
|
+
redis.call('PEXPIRE', key, window * 1000)
|
|
24
|
+
current_count = current_count + 1
|
|
25
|
+
local remaining = limit - current_count
|
|
26
|
+
local reset = math.ceil((now + (window * 1000)) / 1000)
|
|
27
|
+
return {1, limit, remaining, reset, 0}
|
|
28
|
+
else
|
|
29
|
+
local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
|
|
30
|
+
local reset
|
|
31
|
+
if #oldest > 0 then
|
|
32
|
+
reset = math.ceil((tonumber(oldest[2]) + (window * 1000)) / 1000)
|
|
33
|
+
else
|
|
34
|
+
reset = math.ceil((now + (window * 1000)) / 1000)
|
|
35
|
+
end
|
|
36
|
+
local retry_after = math.max(1, reset - math.ceil(now / 1000))
|
|
37
|
+
return {0, limit, 0, reset, retry_after}
|
|
38
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
-- Token bucket rate limiter
|
|
2
|
+
-- KEYS[1] = rate limit key
|
|
3
|
+
-- ARGV[1] = capacity
|
|
4
|
+
-- ARGV[2] = refill rate (tokens per second)
|
|
5
|
+
-- ARGV[3] = current timestamp (milliseconds)
|
|
6
|
+
|
|
7
|
+
local key = KEYS[1]
|
|
8
|
+
local capacity = tonumber(ARGV[1])
|
|
9
|
+
local refill_rate = tonumber(ARGV[2])
|
|
10
|
+
local now = tonumber(ARGV[3])
|
|
11
|
+
|
|
12
|
+
local data = redis.call('HMGET', key, 'tokens', 'last_refill')
|
|
13
|
+
local tokens = tonumber(data[1])
|
|
14
|
+
local last_refill = tonumber(data[2])
|
|
15
|
+
|
|
16
|
+
if tokens == nil then
|
|
17
|
+
tokens = capacity
|
|
18
|
+
last_refill = now
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
local elapsed = (now - last_refill) / 1000
|
|
22
|
+
local refill_amount = elapsed * refill_rate
|
|
23
|
+
tokens = math.min(capacity, tokens + refill_amount)
|
|
24
|
+
last_refill = now
|
|
25
|
+
|
|
26
|
+
if tokens >= 1 then
|
|
27
|
+
tokens = tokens - 1
|
|
28
|
+
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', last_refill)
|
|
29
|
+
local ttl = math.ceil(capacity / refill_rate) + 1
|
|
30
|
+
redis.call('EXPIRE', key, ttl)
|
|
31
|
+
local remaining = math.floor(tokens)
|
|
32
|
+
local reset = math.ceil(now / 1000) + math.ceil((capacity - tokens) / refill_rate)
|
|
33
|
+
return {1, capacity, remaining, reset, 0}
|
|
34
|
+
else
|
|
35
|
+
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', last_refill)
|
|
36
|
+
local ttl = math.ceil(capacity / refill_rate) + 1
|
|
37
|
+
redis.call('EXPIRE', key, ttl)
|
|
38
|
+
local retry_after = math.max(1, math.ceil((1 - tokens) / refill_rate))
|
|
39
|
+
local reset = math.ceil(now / 1000) + retry_after
|
|
40
|
+
return {0, capacity, 0, reset, retry_after}
|
|
41
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Cluster, Redis, RedisOptions } from "ioredis";
|
|
2
|
+
export type RedisClient = Redis | Cluster;
|
|
3
|
+
export type RedisConfig = RedisClient | string | RedisOptions | {
|
|
4
|
+
nodes: {
|
|
5
|
+
host: string;
|
|
6
|
+
port: number;
|
|
7
|
+
}[];
|
|
8
|
+
options?: RedisOptions;
|
|
9
|
+
};
|
|
10
|
+
export interface RedisLimitOptions {
|
|
11
|
+
redis: RedisConfig;
|
|
12
|
+
failOpen?: boolean;
|
|
13
|
+
keyPrefix?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface RateLimitResult {
|
|
16
|
+
allowed: boolean;
|
|
17
|
+
limit: number;
|
|
18
|
+
remaining: number;
|
|
19
|
+
reset: number;
|
|
20
|
+
retryAfter?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface RateLimitStrategy {
|
|
23
|
+
consume(key: string): Promise<RateLimitResult>;
|
|
24
|
+
}
|
|
25
|
+
export interface SlidingWindowConfig {
|
|
26
|
+
algorithm: "sliding-window";
|
|
27
|
+
limit: number;
|
|
28
|
+
window: number;
|
|
29
|
+
}
|
|
30
|
+
export interface TokenBucketConfig {
|
|
31
|
+
algorithm: "token-bucket";
|
|
32
|
+
capacity: number;
|
|
33
|
+
refillRate: number;
|
|
34
|
+
}
|
|
35
|
+
export type AlgorithmConfig = SlidingWindowConfig | TokenBucketConfig;
|
|
36
|
+
export interface BaseMiddlewareOptions {
|
|
37
|
+
key?: (req: unknown) => string | undefined;
|
|
38
|
+
headers?: boolean;
|
|
39
|
+
onLimitReached?: (req: unknown, res: unknown) => void | Promise<void>;
|
|
40
|
+
failOpen?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export type MiddlewareOptions = BaseMiddlewareOptions & AlgorithmConfig;
|
|
43
|
+
export interface RateLimitHeaders {
|
|
44
|
+
"X-RateLimit-Limit": string;
|
|
45
|
+
"X-RateLimit-Remaining": string;
|
|
46
|
+
"X-RateLimit-Reset": string;
|
|
47
|
+
"Retry-After"?: string;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5D,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,OAAO,CAAC;AAE1C,MAAM,MAAM,WAAW,GACnB,WAAW,GACX,MAAM,GACN,YAAY,GACZ;IAAE,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,YAAY,CAAA;CAAE,CAAC;AAExE,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,gBAAgB,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,eAAe,GAAG,mBAAmB,GAAG,iBAAiB,CAAC;AAEtE,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;IAC3C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,GAAG,eAAe,CAAC;AAExE,MAAM,WAAW,gBAAgB;IAC/B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,uBAAuB,EAAE,MAAM,CAAC;IAChC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RateLimitHeaders, RateLimitResult } from "../types";
|
|
2
|
+
export declare function buildRateLimitHeaders(result: RateLimitResult): RateLimitHeaders;
|
|
3
|
+
export declare function setHeaders(res: {
|
|
4
|
+
setHeader: (name: string, value: string) => void;
|
|
5
|
+
}, headers: RateLimitHeaders): void;
|
|
6
|
+
//# sourceMappingURL=headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/utils/headers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAElE,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,eAAe,GACtB,gBAAgB,CAYlB;AAED,wBAAgB,UAAU,CACxB,GAAG,EAAE;IAAE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EACzD,OAAO,EAAE,gBAAgB,GACxB,IAAI,CAMN"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildRateLimitHeaders = buildRateLimitHeaders;
|
|
4
|
+
exports.setHeaders = setHeaders;
|
|
5
|
+
function buildRateLimitHeaders(result) {
|
|
6
|
+
const headers = {
|
|
7
|
+
"X-RateLimit-Limit": String(result.limit),
|
|
8
|
+
"X-RateLimit-Remaining": String(result.remaining),
|
|
9
|
+
"X-RateLimit-Reset": String(result.reset),
|
|
10
|
+
};
|
|
11
|
+
if (!result.allowed && result.retryAfter !== undefined) {
|
|
12
|
+
headers["Retry-After"] = String(result.retryAfter);
|
|
13
|
+
}
|
|
14
|
+
return headers;
|
|
15
|
+
}
|
|
16
|
+
function setHeaders(res, headers) {
|
|
17
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
18
|
+
if (value !== undefined) {
|
|
19
|
+
res.setHeader(name, value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=headers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../../src/utils/headers.ts"],"names":[],"mappings":";;AAEA,sDAcC;AAED,gCASC;AAzBD,SAAgB,qBAAqB,CACnC,MAAuB;IAEvB,MAAM,OAAO,GAAqB;QAChC,mBAAmB,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC;QACzC,uBAAuB,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC;QACjD,mBAAmB,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC;KAC1C,CAAC;IAEF,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACvD,OAAO,CAAC,aAAa,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAgB,UAAU,CACxB,GAAyD,EACzD,OAAyB;IAEzB,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../src/utils/redis.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,WAAW,CAclE;AAiBD,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE5D"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createRedisClient = createRedisClient;
|
|
37
|
+
exports.buildKey = buildKey;
|
|
38
|
+
const ioredis_1 = __importStar(require("ioredis"));
|
|
39
|
+
function createRedisClient(config) {
|
|
40
|
+
if (isRedisClient(config)) {
|
|
41
|
+
return config;
|
|
42
|
+
}
|
|
43
|
+
if (typeof config === "string") {
|
|
44
|
+
return new ioredis_1.default(config);
|
|
45
|
+
}
|
|
46
|
+
if (isClusterConfig(config)) {
|
|
47
|
+
return new ioredis_1.Cluster(config.nodes, config.options);
|
|
48
|
+
}
|
|
49
|
+
return new ioredis_1.default(config);
|
|
50
|
+
}
|
|
51
|
+
function isRedisClient(config) {
|
|
52
|
+
return config instanceof ioredis_1.default || config instanceof ioredis_1.Cluster;
|
|
53
|
+
}
|
|
54
|
+
function isClusterConfig(config) {
|
|
55
|
+
return (typeof config === "object" &&
|
|
56
|
+
config !== null &&
|
|
57
|
+
"nodes" in config &&
|
|
58
|
+
Array.isArray(config.nodes));
|
|
59
|
+
}
|
|
60
|
+
function buildKey(prefix, key) {
|
|
61
|
+
return `${prefix}:${key}`;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=redis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../../src/utils/redis.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,8CAcC;AAiBD,4BAEC;AApCD,mDAA4D;AAG5D,SAAgB,iBAAiB,CAAC,MAAmB;IACnD,IAAI,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1B,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,IAAI,iBAAK,CAAC,MAAM,CAAC,CAAC;IAC3B,CAAC;IAED,IAAI,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5B,OAAO,IAAI,iBAAO,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IACnD,CAAC;IAED,OAAO,IAAI,iBAAK,CAAC,MAAM,CAAC,CAAC;AAC3B,CAAC;AAED,SAAS,aAAa,CAAC,MAAmB;IACxC,OAAO,MAAM,YAAY,iBAAK,IAAI,MAAM,YAAY,iBAAO,CAAC;AAC9D,CAAC;AAED,SAAS,eAAe,CACtB,MAAmB;IAEnB,OAAO,CACL,OAAO,MAAM,KAAK,QAAQ;QAC1B,MAAM,KAAK,IAAI;QACf,OAAO,IAAI,MAAM;QACjB,KAAK,CAAC,OAAO,CAAE,MAA6B,CAAC,KAAK,CAAC,CACpD,CAAC;AACJ,CAAC;AAED,SAAgB,QAAQ,CAAC,MAAc,EAAE,GAAW;IAClD,OAAO,GAAG,MAAM,IAAI,GAAG,EAAE,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RedisClient } from "../types";
|
|
2
|
+
export declare function loadScriptSha(redis: RedisClient, name: string): Promise<string>;
|
|
3
|
+
export declare function evalScript(redis: RedisClient, name: string, keys: string[], args: (string | number)[]): Promise<(string | number)[]>;
|
|
4
|
+
export declare function parseScriptResult(result: (string | number)[]): {
|
|
5
|
+
allowed: boolean;
|
|
6
|
+
limit: number;
|
|
7
|
+
remaining: number;
|
|
8
|
+
reset: number;
|
|
9
|
+
retryAfter: number;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=scripts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scripts.d.ts","sourceRoot":"","sources":["../../src/utils/scripts.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAiB5C,wBAAsB,aAAa,CACjC,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CAWjB;AAED,wBAAsB,UAAU,CAC9B,KAAK,EAAE,WAAW,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GACxB,OAAO,CAAC,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,CA0B9B;AAeD,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAC1B;IACD,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,CAQA"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadScriptSha = loadScriptSha;
|
|
4
|
+
exports.evalScript = evalScript;
|
|
5
|
+
exports.parseScriptResult = parseScriptResult;
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
const scriptCache = new Map();
|
|
9
|
+
const shaCache = new Map();
|
|
10
|
+
function loadScript(name) {
|
|
11
|
+
const cached = scriptCache.get(name);
|
|
12
|
+
if (cached) {
|
|
13
|
+
return cached;
|
|
14
|
+
}
|
|
15
|
+
const scriptPath = (0, path_1.join)(__dirname, "..", "scripts", `${name}.lua`);
|
|
16
|
+
const script = (0, fs_1.readFileSync)(scriptPath, "utf-8");
|
|
17
|
+
scriptCache.set(name, script);
|
|
18
|
+
return script;
|
|
19
|
+
}
|
|
20
|
+
async function loadScriptSha(redis, name) {
|
|
21
|
+
const cacheKey = getCacheKey(redis, name);
|
|
22
|
+
const cached = shaCache.get(cacheKey);
|
|
23
|
+
if (cached) {
|
|
24
|
+
return cached;
|
|
25
|
+
}
|
|
26
|
+
const script = loadScript(name);
|
|
27
|
+
const sha = (await redis.script("LOAD", script));
|
|
28
|
+
shaCache.set(cacheKey, sha);
|
|
29
|
+
return sha;
|
|
30
|
+
}
|
|
31
|
+
async function evalScript(redis, name, keys, args) {
|
|
32
|
+
const sha = await loadScriptSha(redis, name);
|
|
33
|
+
try {
|
|
34
|
+
const result = await redis.evalsha(sha, keys.length, ...keys, ...args.map(String));
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (isNoScriptError(error)) {
|
|
39
|
+
const script = loadScript(name);
|
|
40
|
+
const result = await redis.eval(script, keys.length, ...keys, ...args.map(String));
|
|
41
|
+
const newSha = (await redis.script("LOAD", script));
|
|
42
|
+
shaCache.set(getCacheKey(redis, name), newSha);
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function getCacheKey(redis, name) {
|
|
49
|
+
const status = redis.status ?? "unknown";
|
|
50
|
+
return `${status}:${name}`;
|
|
51
|
+
}
|
|
52
|
+
function isNoScriptError(error) {
|
|
53
|
+
return (error instanceof Error &&
|
|
54
|
+
(error.message.includes("NOSCRIPT") ||
|
|
55
|
+
error.message.includes("No matching script")));
|
|
56
|
+
}
|
|
57
|
+
function parseScriptResult(result) {
|
|
58
|
+
return {
|
|
59
|
+
allowed: Number(result[0]) === 1,
|
|
60
|
+
limit: Number(result[1]),
|
|
61
|
+
remaining: Number(result[2]),
|
|
62
|
+
reset: Number(result[3]),
|
|
63
|
+
retryAfter: Number(result[4]),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=scripts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scripts.js","sourceRoot":"","sources":["../../src/utils/scripts.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "limitly",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Express-rate-limit, but distributed, Redis-powered, and production ready.",
|
|
5
|
+
"author": "Arpan Das <dasarpan471@gmail.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://github.com/adasarpan404/limitly#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/adasarpan404/limitly.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/adasarpan404/limitly/issues"
|
|
14
|
+
},
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"types": "dist/index.d.ts",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"require": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./express": {
|
|
25
|
+
"types": "./dist/middleware/express.d.ts",
|
|
26
|
+
"import": "./dist/middleware/express.js",
|
|
27
|
+
"require": "./dist/middleware/express.js"
|
|
28
|
+
},
|
|
29
|
+
"./fastify": {
|
|
30
|
+
"types": "./dist/middleware/fastify.d.ts",
|
|
31
|
+
"import": "./dist/middleware/fastify.js",
|
|
32
|
+
"require": "./dist/middleware/fastify.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"LICENSE",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc && rm -rf dist/scripts && cp -r src/scripts dist/scripts",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:watch": "vitest",
|
|
44
|
+
"prepack": "npm run build",
|
|
45
|
+
"prepublishOnly": "npm test",
|
|
46
|
+
"pack:check": "npm pack --dry-run",
|
|
47
|
+
"version:patch": "npm version patch",
|
|
48
|
+
"version:minor": "npm version minor",
|
|
49
|
+
"version:major": "npm version major"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"limitly",
|
|
53
|
+
"rate-limit",
|
|
54
|
+
"rate-limiting",
|
|
55
|
+
"redis",
|
|
56
|
+
"express",
|
|
57
|
+
"fastify",
|
|
58
|
+
"sliding-window",
|
|
59
|
+
"token-bucket",
|
|
60
|
+
"distributed",
|
|
61
|
+
"middleware",
|
|
62
|
+
"ioredis"
|
|
63
|
+
],
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public",
|
|
66
|
+
"registry": "https://registry.npmjs.org/"
|
|
67
|
+
},
|
|
68
|
+
"peerDependencies": {
|
|
69
|
+
"express": ">=4.0.0",
|
|
70
|
+
"fastify": ">=4.0.0",
|
|
71
|
+
"ioredis": ">=5.0.0"
|
|
72
|
+
},
|
|
73
|
+
"peerDependenciesMeta": {
|
|
74
|
+
"express": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"fastify": {
|
|
78
|
+
"optional": true
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"devDependencies": {
|
|
82
|
+
"@types/express": "^4.17.21",
|
|
83
|
+
"@types/node": "^20.14.0",
|
|
84
|
+
"express": "^4.19.2",
|
|
85
|
+
"fastify": "^4.28.0",
|
|
86
|
+
"ioredis": "^5.4.1",
|
|
87
|
+
"typescript": "^5.5.0",
|
|
88
|
+
"vitest": "^1.6.0"
|
|
89
|
+
},
|
|
90
|
+
"engines": {
|
|
91
|
+
"node": ">=18.0.0"
|
|
92
|
+
}
|
|
93
|
+
}
|