@jellyfungus/hono-rate-limiter 0.1.0 → 0.2.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 +168 -44
- package/dist/index.cjs +41 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -2
- package/dist/index.d.ts +69 -2
- package/dist/index.js +40 -5
- package/dist/index.js.map +1 -1
- package/dist/store/cloudflare-kv.cjs +7 -0
- package/dist/store/cloudflare-kv.cjs.map +1 -1
- package/dist/store/cloudflare-kv.d.cts +6 -0
- package/dist/store/cloudflare-kv.d.ts +6 -0
- package/dist/store/cloudflare-kv.js +7 -0
- package/dist/store/cloudflare-kv.js.map +1 -1
- package/dist/store/redis.cjs +7 -0
- package/dist/store/redis.cjs.map +1 -1
- package/dist/store/redis.d.cts +6 -0
- package/dist/store/redis.d.ts +6 -0
- package/dist/store/redis.js +7 -0
- package/dist/store/redis.js.map +1 -1
- package/dist/websocket.cjs +178 -0
- package/dist/websocket.cjs.map +1 -0
- package/dist/websocket.d.cts +81 -0
- package/dist/websocket.d.ts +81 -0
- package/dist/websocket.js +151 -0
- package/dist/websocket.js.map +1 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
# @jellyfungus/hono-rate-limiter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Production-ready rate limiting middleware for [Hono](https://hono.dev) web framework.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@jellyfungus/hono-rate-limiter)
|
|
6
6
|
[](https://github.com/rokasta12/hono-rate-limiter/actions/workflows/ci.yml)
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Highlights
|
|
9
9
|
|
|
10
|
-
- **Sliding Window Algorithm** -
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
10
|
+
- **Sliding Window Algorithm** - Same approach used by Cloudflare, AWS, and major CDNs. Prevents burst attacks at window boundaries.
|
|
11
|
+
- **Zero Dependencies** - Only Hono as a peer dependency. No bloat.
|
|
12
|
+
- **Works Out of the Box** - Sensible defaults with built-in IP detection. Just add `rateLimiter()` and you're done.
|
|
13
|
+
- **Multiple Stores** - Memory (default), Redis (with atomic Lua scripts), Cloudflare KV
|
|
14
|
+
- **Cloudflare Rate Limiting Binding** - Native support for Cloudflare's globally distributed rate limiter
|
|
15
|
+
- **WebSocket Support** - Rate limit WebSocket connections
|
|
16
|
+
- **IETF Compliant Headers** - Full support for `RateLimit-*` headers (draft-6, draft-7)
|
|
17
|
+
- **TypeScript First** - Complete type safety
|
|
17
18
|
|
|
18
19
|
## Installation
|
|
19
20
|
|
|
@@ -21,9 +22,11 @@ Rate limiting middleware for [Hono](https://hono.dev) web framework.
|
|
|
21
22
|
npm install @jellyfungus/hono-rate-limiter
|
|
22
23
|
# or
|
|
23
24
|
bun add @jellyfungus/hono-rate-limiter
|
|
25
|
+
# or
|
|
26
|
+
pnpm add @jellyfungus/hono-rate-limiter
|
|
24
27
|
```
|
|
25
28
|
|
|
26
|
-
##
|
|
29
|
+
## Quick Start
|
|
27
30
|
|
|
28
31
|
```ts
|
|
29
32
|
import { Hono } from "hono";
|
|
@@ -31,51 +34,75 @@ import { rateLimiter } from "@jellyfungus/hono-rate-limiter";
|
|
|
31
34
|
|
|
32
35
|
const app = new Hono();
|
|
33
36
|
|
|
34
|
-
// 60 requests per minute
|
|
37
|
+
// That's it! 60 requests per minute with sliding window
|
|
35
38
|
app.use(rateLimiter());
|
|
36
39
|
|
|
37
|
-
// Custom configuration
|
|
38
|
-
app.use(
|
|
39
|
-
"/api/*",
|
|
40
|
-
rateLimiter({
|
|
41
|
-
limit: 100,
|
|
42
|
-
windowMs: 60 * 1000, // 1 minute
|
|
43
|
-
}),
|
|
44
|
-
);
|
|
45
|
-
|
|
46
40
|
app.get("/", (c) => c.text("Hello!"));
|
|
47
41
|
|
|
48
42
|
export default app;
|
|
49
43
|
```
|
|
50
44
|
|
|
51
|
-
##
|
|
45
|
+
## Sliding Window Algorithm
|
|
46
|
+
|
|
47
|
+
Traditional fixed-window rate limiters have a critical flaw: users can burst 2x the limit at window boundaries.
|
|
48
|
+
|
|
49
|
+
Our sliding window algorithm (same approach used by Cloudflare) smooths traffic across boundaries:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
Fixed Window Problem:
|
|
53
|
+
|-------- Window 1 --------|-------- Window 2 --------|
|
|
54
|
+
[60 req][60 req]
|
|
55
|
+
└── 120 requests in seconds! ──┘
|
|
56
|
+
|
|
57
|
+
Sliding Window Solution:
|
|
58
|
+
|-------- Window 1 --------|-------- Window 2 --------|
|
|
59
|
+
Weighted calculation prevents bursts
|
|
60
|
+
└── Always respects the 60 req limit ──┘
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
52
64
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
```ts
|
|
66
|
+
app.use(
|
|
67
|
+
rateLimiter({
|
|
68
|
+
limit: 100, // Max requests per window (default: 60)
|
|
69
|
+
windowMs: 60 * 1000, // Window duration in ms (default: 60000)
|
|
70
|
+
algorithm: "sliding-window", // or "fixed-window" (default: sliding-window)
|
|
71
|
+
headers: "draft-6", // or "draft-7" or false (default: draft-6)
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### All Options
|
|
77
|
+
|
|
78
|
+
| Option | Type | Default | Description |
|
|
79
|
+
| ------------------------ | ------------------------------------ | ------------------ | -------------------------------- |
|
|
80
|
+
| `limit` | `number \| Function` | `60` | Max requests per window |
|
|
81
|
+
| `windowMs` | `number` | `60000` | Window duration in ms |
|
|
82
|
+
| `algorithm` | `'sliding-window' \| 'fixed-window'` | `'sliding-window'` | Rate limiting algorithm |
|
|
83
|
+
| `store` | `RateLimitStore` | `MemoryStore` | Storage backend |
|
|
84
|
+
| `keyGenerator` | `Function` | IP detection | Generate unique client key |
|
|
85
|
+
| `handler` | `Function` | 429 response | Custom rate limit response |
|
|
86
|
+
| `headers` | `'draft-6' \| 'draft-7' \| false` | `'draft-6'` | IETF header format |
|
|
87
|
+
| `skip` | `Function` | - | Skip rate limiting conditionally |
|
|
88
|
+
| `skipSuccessfulRequests` | `boolean` | `false` | Don't count 2xx responses |
|
|
89
|
+
| `skipFailedRequests` | `boolean` | `false` | Don't count 4xx/5xx responses |
|
|
90
|
+
| `onRateLimited` | `Function` | - | Callback when rate limited |
|
|
66
91
|
|
|
67
92
|
## Stores
|
|
68
93
|
|
|
69
94
|
### Memory Store (Default)
|
|
70
95
|
|
|
71
|
-
|
|
72
|
-
import { rateLimiter } from "@jellyfungus/hono-rate-limiter";
|
|
96
|
+
Perfect for single-instance deployments:
|
|
73
97
|
|
|
74
|
-
|
|
98
|
+
```ts
|
|
99
|
+
app.use(rateLimiter()); // Uses MemoryStore automatically
|
|
75
100
|
```
|
|
76
101
|
|
|
77
102
|
### Redis Store
|
|
78
103
|
|
|
104
|
+
For distributed deployments. Uses atomic Lua scripts for race-condition-free operations:
|
|
105
|
+
|
|
79
106
|
```ts
|
|
80
107
|
import { rateLimiter } from "@jellyfungus/hono-rate-limiter";
|
|
81
108
|
import { RedisStore } from "@jellyfungus/hono-rate-limiter/store/redis";
|
|
@@ -92,12 +119,16 @@ app.use(
|
|
|
92
119
|
|
|
93
120
|
### Cloudflare KV Store
|
|
94
121
|
|
|
122
|
+
For Cloudflare Workers with KV:
|
|
123
|
+
|
|
95
124
|
```ts
|
|
96
125
|
import { rateLimiter } from "@jellyfungus/hono-rate-limiter";
|
|
97
126
|
import { CloudflareKVStore } from "@jellyfungus/hono-rate-limiter/store/cloudflare-kv";
|
|
98
127
|
|
|
99
128
|
type Bindings = { RATE_LIMIT_KV: KVNamespace };
|
|
100
129
|
|
|
130
|
+
const app = new Hono<{ Bindings: Bindings }>();
|
|
131
|
+
|
|
101
132
|
app.use("*", async (c, next) => {
|
|
102
133
|
const limiter = rateLimiter({
|
|
103
134
|
store: new CloudflareKVStore({ namespace: c.env.RATE_LIMIT_KV }),
|
|
@@ -106,15 +137,65 @@ app.use("*", async (c, next) => {
|
|
|
106
137
|
});
|
|
107
138
|
```
|
|
108
139
|
|
|
140
|
+
### Cloudflare Rate Limiting Binding
|
|
141
|
+
|
|
142
|
+
For enterprise-grade, globally distributed rate limiting using Cloudflare's native Rate Limiting:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { cloudflareRateLimiter } from "@jellyfungus/hono-rate-limiter";
|
|
146
|
+
|
|
147
|
+
type Bindings = { RATE_LIMITER: RateLimitBinding };
|
|
148
|
+
|
|
149
|
+
const app = new Hono<{ Bindings: Bindings }>();
|
|
150
|
+
|
|
151
|
+
app.use(
|
|
152
|
+
cloudflareRateLimiter({
|
|
153
|
+
binding: (c) => c.env.RATE_LIMITER,
|
|
154
|
+
keyGenerator: (c) => c.req.header("cf-connecting-ip") ?? "unknown",
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## WebSocket Rate Limiting
|
|
160
|
+
|
|
161
|
+
Rate limit WebSocket message frequency:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { Hono } from "hono";
|
|
165
|
+
import { createBunWebSocket } from "hono/bun";
|
|
166
|
+
import { webSocketLimiter } from "@jellyfungus/hono-rate-limiter/websocket";
|
|
167
|
+
|
|
168
|
+
const { upgradeWebSocket, websocket } = createBunWebSocket();
|
|
169
|
+
|
|
170
|
+
const wsLimiter = webSocketLimiter({
|
|
171
|
+
limit: 100,
|
|
172
|
+
windowMs: 60_000,
|
|
173
|
+
keyGenerator: (c) => c.req.header("cf-connecting-ip") ?? "unknown",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
app.get(
|
|
177
|
+
"/ws",
|
|
178
|
+
upgradeWebSocket(
|
|
179
|
+
wsLimiter((c) => ({
|
|
180
|
+
onMessage(event, ws) {
|
|
181
|
+
ws.send("Hello!");
|
|
182
|
+
},
|
|
183
|
+
})),
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
```
|
|
187
|
+
|
|
109
188
|
## Dynamic Limits
|
|
110
189
|
|
|
190
|
+
Different limits for different users:
|
|
191
|
+
|
|
111
192
|
```ts
|
|
112
193
|
app.use(
|
|
113
194
|
rateLimiter({
|
|
114
195
|
limit: async (c) => {
|
|
115
196
|
const user = c.get("user");
|
|
116
|
-
if (user?.tier === "
|
|
117
|
-
if (user?.tier === "pro") return
|
|
197
|
+
if (user?.tier === "enterprise") return 10000;
|
|
198
|
+
if (user?.tier === "pro") return 1000;
|
|
118
199
|
return 100;
|
|
119
200
|
},
|
|
120
201
|
}),
|
|
@@ -123,11 +204,12 @@ app.use(
|
|
|
123
204
|
|
|
124
205
|
## Custom Key Generator
|
|
125
206
|
|
|
207
|
+
Rate limit by API key, user ID, or any identifier:
|
|
208
|
+
|
|
126
209
|
```ts
|
|
127
210
|
app.use(
|
|
128
211
|
rateLimiter({
|
|
129
212
|
keyGenerator: (c) => {
|
|
130
|
-
// Rate limit by API key instead of IP
|
|
131
213
|
return c.req.header("x-api-key") ?? "anonymous";
|
|
132
214
|
},
|
|
133
215
|
}),
|
|
@@ -136,12 +218,13 @@ app.use(
|
|
|
136
218
|
|
|
137
219
|
## Skip Certain Requests
|
|
138
220
|
|
|
221
|
+
Bypass rate limiting for specific routes:
|
|
222
|
+
|
|
139
223
|
```ts
|
|
140
224
|
app.use(
|
|
141
225
|
rateLimiter({
|
|
142
226
|
skip: (c) => {
|
|
143
|
-
|
|
144
|
-
return c.req.path === "/health";
|
|
227
|
+
return c.req.path === "/health" || c.req.path === "/metrics";
|
|
145
228
|
},
|
|
146
229
|
}),
|
|
147
230
|
);
|
|
@@ -149,19 +232,38 @@ app.use(
|
|
|
149
232
|
|
|
150
233
|
## Access Rate Limit Info
|
|
151
234
|
|
|
235
|
+
Get rate limit information in your handlers:
|
|
236
|
+
|
|
152
237
|
```ts
|
|
153
|
-
app.get("/status", (c) => {
|
|
238
|
+
app.get("/api/status", (c) => {
|
|
154
239
|
const info = c.get("rateLimit");
|
|
155
240
|
return c.json({
|
|
156
241
|
limit: info?.limit,
|
|
157
242
|
remaining: info?.remaining,
|
|
158
|
-
|
|
243
|
+
resetAt: info?.reset,
|
|
159
244
|
});
|
|
160
245
|
});
|
|
161
246
|
```
|
|
162
247
|
|
|
248
|
+
## Manual Store Control
|
|
249
|
+
|
|
250
|
+
Access the store directly for manual operations:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
app.post("/api/admin/reset-limit/:userId", async (c) => {
|
|
254
|
+
const store = c.get("rateLimitStore");
|
|
255
|
+
const userId = c.req.param("userId");
|
|
256
|
+
|
|
257
|
+
await store?.resetKey(userId);
|
|
258
|
+
|
|
259
|
+
return c.json({ success: true });
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
163
263
|
## Custom Handler
|
|
164
264
|
|
|
265
|
+
Customize the rate limit exceeded response:
|
|
266
|
+
|
|
165
267
|
```ts
|
|
166
268
|
app.use(
|
|
167
269
|
rateLimiter({
|
|
@@ -178,6 +280,28 @@ app.use(
|
|
|
178
280
|
);
|
|
179
281
|
```
|
|
180
282
|
|
|
283
|
+
## Response Headers
|
|
284
|
+
|
|
285
|
+
The middleware sets standard rate limit headers:
|
|
286
|
+
|
|
287
|
+
**draft-6 (default):**
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
X-RateLimit-Limit: 60
|
|
291
|
+
X-RateLimit-Remaining: 45
|
|
292
|
+
X-RateLimit-Reset: 1640000000
|
|
293
|
+
RateLimit-Policy: 60;w=60
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**draft-7:**
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
RateLimit-Limit: 60
|
|
300
|
+
RateLimit-Remaining: 45
|
|
301
|
+
RateLimit-Reset: 30
|
|
302
|
+
RateLimit-Policy: 60;w=60
|
|
303
|
+
```
|
|
304
|
+
|
|
181
305
|
## License
|
|
182
306
|
|
|
183
307
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
MemoryStore: () => MemoryStore,
|
|
24
|
+
cloudflareRateLimiter: () => cloudflareRateLimiter,
|
|
24
25
|
getClientIP: () => getClientIP,
|
|
25
26
|
rateLimiter: () => rateLimiter
|
|
26
27
|
});
|
|
@@ -70,6 +71,9 @@ var MemoryStore = class {
|
|
|
70
71
|
resetKey(key) {
|
|
71
72
|
this.entries.delete(key);
|
|
72
73
|
}
|
|
74
|
+
resetAll() {
|
|
75
|
+
this.entries.clear();
|
|
76
|
+
}
|
|
73
77
|
shutdown() {
|
|
74
78
|
if (this.cleanupTimer) {
|
|
75
79
|
clearInterval(this.cleanupTimer);
|
|
@@ -78,16 +82,19 @@ var MemoryStore = class {
|
|
|
78
82
|
}
|
|
79
83
|
};
|
|
80
84
|
var defaultStore;
|
|
81
|
-
function setHeaders(c, info, format) {
|
|
85
|
+
function setHeaders(c, info, format, windowMs) {
|
|
82
86
|
if (format === false) {
|
|
83
87
|
return;
|
|
84
88
|
}
|
|
89
|
+
const windowSeconds = Math.ceil(windowMs / 1e3);
|
|
85
90
|
const resetSeconds = Math.max(0, Math.ceil((info.reset - Date.now()) / 1e3));
|
|
91
|
+
c.header("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
86
92
|
switch (format) {
|
|
87
93
|
case "draft-7":
|
|
88
|
-
c.header(
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
c.header(
|
|
95
|
+
"RateLimit",
|
|
96
|
+
`limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
|
|
97
|
+
);
|
|
91
98
|
break;
|
|
92
99
|
case "draft-6":
|
|
93
100
|
default:
|
|
@@ -189,7 +196,11 @@ var rateLimiter = (options) => {
|
|
|
189
196
|
const limit = typeof opts.limit === "function" ? await opts.limit(c) : opts.limit;
|
|
190
197
|
const { allowed, info } = opts.algorithm === "sliding-window" ? await checkSlidingWindow(store, key, limit, opts.windowMs) : await checkFixedWindow(store, key, limit, opts.windowMs);
|
|
191
198
|
c.set("rateLimit", info);
|
|
192
|
-
|
|
199
|
+
c.set("rateLimitStore", {
|
|
200
|
+
getKey: store.get?.bind(store) ?? (() => void 0),
|
|
201
|
+
resetKey: store.resetKey.bind(store)
|
|
202
|
+
});
|
|
203
|
+
setHeaders(c, info, opts.headers, opts.windowMs);
|
|
193
204
|
if (!allowed) {
|
|
194
205
|
if (opts.onRateLimited) {
|
|
195
206
|
await opts.onRateLimited(c, info);
|
|
@@ -211,9 +222,34 @@ var rateLimiter = (options) => {
|
|
|
211
222
|
}
|
|
212
223
|
};
|
|
213
224
|
};
|
|
225
|
+
var cloudflareRateLimiter = (options) => {
|
|
226
|
+
const { binding, keyGenerator, handler, skip } = options;
|
|
227
|
+
return async function cloudflareRateLimiter2(c, next) {
|
|
228
|
+
if (skip) {
|
|
229
|
+
const shouldSkip = await skip(c);
|
|
230
|
+
if (shouldSkip) {
|
|
231
|
+
return next();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const rateLimitBinding = typeof binding === "function" ? binding(c) : binding;
|
|
235
|
+
const key = await keyGenerator(c);
|
|
236
|
+
const { success } = await rateLimitBinding.limit({ key });
|
|
237
|
+
if (!success) {
|
|
238
|
+
if (handler) {
|
|
239
|
+
return handler(c);
|
|
240
|
+
}
|
|
241
|
+
return new Response("Rate limit exceeded", {
|
|
242
|
+
status: 429,
|
|
243
|
+
headers: { "Content-Type": "text/plain" }
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return next();
|
|
247
|
+
};
|
|
248
|
+
};
|
|
214
249
|
// Annotate the CommonJS export names for ESM import in node:
|
|
215
250
|
0 && (module.exports = {
|
|
216
251
|
MemoryStore,
|
|
252
|
+
cloudflareRateLimiter,
|
|
217
253
|
getClientIP,
|
|
218
254
|
rateLimiter
|
|
219
255
|
});
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @module\n * Rate Limit Middleware for Hono.\n */\n\nimport type { Context, Env, MiddlewareHandler } from \"hono\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Rate limit information for a single request\n */\nexport type RateLimitInfo = {\n /** Maximum requests allowed in window */\n limit: number;\n /** Remaining requests in current window */\n remaining: number;\n /** Unix timestamp (ms) when window resets */\n reset: number;\n};\n\n/**\n * Result from store increment operation\n */\nexport type StoreResult = {\n /** Current request count in window */\n count: number;\n /** When the window resets (Unix timestamp ms) */\n reset: number;\n};\n\n/**\n * Header format versions\n */\nexport type HeadersFormat =\n | \"draft-6\" // X-RateLimit-* headers\n | \"draft-7\" // RateLimit-* without structured fields\n | false; // Disable headers\n\n/**\n * Rate limit algorithm\n */\nexport type Algorithm = \"fixed-window\" | \"sliding-window\";\n\n/**\n * Store interface for rate limit state\n */\nexport type RateLimitStore = {\n /**\n * Initialize store. Called once before first use.\n */\n init?: (windowMs: number) => void | Promise<void>;\n\n /**\n * Increment counter for key and return current state.\n */\n increment: (key: string) => StoreResult | Promise<StoreResult>;\n\n /**\n * Decrement counter for key.\n */\n decrement?: (key: string) => void | Promise<void>;\n\n /**\n * Reset a specific key.\n */\n resetKey: (key: string) => void | Promise<void>;\n\n /**\n * Get current state for key.\n */\n get?: (\n key: string,\n ) => StoreResult | Promise<StoreResult | undefined> | undefined;\n\n /**\n * Graceful shutdown.\n */\n shutdown?: () => void | Promise<void>;\n};\n\n/**\n * Options for rate limit middleware\n */\nexport type RateLimitOptions<E extends Env = Env> = {\n /**\n * Maximum requests allowed in the time window.\n * @default 60\n */\n limit?: number | ((c: Context<E>) => number | Promise<number>);\n\n /**\n * Time window in milliseconds.\n * @default 60000 (1 minute)\n */\n windowMs?: number;\n\n /**\n * Rate limiting algorithm.\n * @default 'sliding-window'\n */\n algorithm?: Algorithm;\n\n /**\n * Storage backend for rate limit state.\n * @default MemoryStore\n */\n store?: RateLimitStore;\n\n /**\n * Generate unique key for each client.\n * @default IP address from headers\n */\n keyGenerator?: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n */\n handler?: (\n c: Context<E>,\n info: RateLimitInfo,\n ) => Response | Promise<Response>;\n\n /**\n * HTTP header format to use.\n * @default 'draft-6'\n */\n headers?: HeadersFormat;\n\n /**\n * Skip rate limiting for certain requests.\n */\n skip?: (c: Context<E>) => boolean | Promise<boolean>;\n\n /**\n * Don't count successful (2xx) requests against limit.\n * @default false\n */\n skipSuccessfulRequests?: boolean;\n\n /**\n * Don't count failed (4xx, 5xx) requests against limit.\n * @default false\n */\n skipFailedRequests?: boolean;\n\n /**\n * Callback when a request is rate limited.\n */\n onRateLimited?: (c: Context<E>, info: RateLimitInfo) => void | Promise<void>;\n};\n\n// ============================================================================\n// Context Variable Type Extension\n// ============================================================================\n\ndeclare module \"hono\" {\n interface ContextVariableMap {\n rateLimit?: RateLimitInfo;\n }\n}\n\n// ============================================================================\n// Memory Store\n// ============================================================================\n\ntype MemoryEntry = {\n count: number;\n reset: number;\n};\n\n/**\n * In-memory store for rate limiting.\n * Suitable for single-instance deployments.\n */\nexport class MemoryStore implements RateLimitStore {\n private entries = new Map<string, MemoryEntry>();\n private windowMs = 60_000;\n private cleanupTimer?: ReturnType<typeof setInterval>;\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n\n // Cleanup expired entries every minute\n this.cleanupTimer = setInterval(() => {\n const now = Date.now();\n for (const [key, entry] of this.entries) {\n if (entry.reset <= now) {\n this.entries.delete(key);\n }\n }\n }, 60_000);\n\n // Don't keep process alive for cleanup\n if (typeof this.cleanupTimer.unref === \"function\") {\n this.cleanupTimer.unref();\n }\n }\n\n increment(key: string): StoreResult {\n const now = Date.now();\n const existing = this.entries.get(key);\n\n if (!existing || existing.reset <= now) {\n // New window\n const reset = now + this.windowMs;\n this.entries.set(key, { count: 1, reset });\n return { count: 1, reset };\n }\n\n // Increment existing\n existing.count++;\n return { count: existing.count, reset: existing.reset };\n }\n\n get(key: string): StoreResult | undefined {\n const entry = this.entries.get(key);\n if (!entry || entry.reset <= Date.now()) {\n return undefined;\n }\n return { count: entry.count, reset: entry.reset };\n }\n\n decrement(key: string): void {\n const entry = this.entries.get(key);\n if (entry && entry.count > 0) {\n entry.count--;\n }\n }\n\n resetKey(key: string): void {\n this.entries.delete(key);\n }\n\n shutdown(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n }\n this.entries.clear();\n }\n}\n\n// Singleton default store\nlet defaultStore: MemoryStore | undefined;\n\n// ============================================================================\n// Header Generation\n// ============================================================================\n\nfunction setHeaders(\n c: Context,\n info: RateLimitInfo,\n format: HeadersFormat,\n): void {\n if (format === false) {\n return;\n }\n\n const resetSeconds = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n switch (format) {\n case \"draft-7\":\n c.header(\"RateLimit-Limit\", String(info.limit));\n c.header(\"RateLimit-Remaining\", String(info.remaining));\n c.header(\"RateLimit-Reset\", String(resetSeconds));\n break;\n\n case \"draft-6\":\n default:\n c.header(\"X-RateLimit-Limit\", String(info.limit));\n c.header(\"X-RateLimit-Remaining\", String(info.remaining));\n c.header(\"X-RateLimit-Reset\", String(Math.ceil(info.reset / 1000)));\n break;\n }\n}\n\n// ============================================================================\n// Default Key Generator\n// ============================================================================\n\nfunction getClientIP(c: Context): string {\n // Platform-specific headers (most reliable)\n const cfIP = c.req.header(\"cf-connecting-ip\");\n if (cfIP) {\n return cfIP;\n }\n\n const xRealIP = c.req.header(\"x-real-ip\");\n if (xRealIP) {\n return xRealIP;\n }\n\n // X-Forwarded-For - take first IP\n const xff = c.req.header(\"x-forwarded-for\");\n if (xff) {\n return xff.split(\",\")[0].trim();\n }\n\n return \"unknown\";\n}\n\n// ============================================================================\n// Default Handler\n// ============================================================================\n\nfunction createDefaultResponse(info: RateLimitInfo): Response {\n const retryAfter = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n return new Response(\"Rate limit exceeded\", {\n status: 429,\n headers: {\n \"Content-Type\": \"text/plain\",\n \"Retry-After\": String(retryAfter),\n },\n });\n}\n\n// ============================================================================\n// Sliding Window Algorithm\n// ============================================================================\n\nasync function checkSlidingWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const currentWindowStart = Math.floor(now / windowMs) * windowMs;\n const previousWindowStart = currentWindowStart - windowMs;\n\n const previousKey = `${key}:${previousWindowStart}`;\n const currentKey = `${key}:${currentWindowStart}`;\n\n // Increment current window\n const current = await store.increment(currentKey);\n\n // Get previous window (may not exist)\n let previousCount = 0;\n if (store.get) {\n const prev = await store.get(previousKey);\n previousCount = prev?.count ?? 0;\n }\n\n // Cloudflare's weighted formula\n const elapsedMs = now - currentWindowStart;\n const weight = (windowMs - elapsedMs) / windowMs;\n const estimatedCount = Math.floor(previousCount * weight) + current.count;\n\n const remaining = Math.max(0, limit - estimatedCount);\n const allowed = estimatedCount <= limit;\n const reset = currentWindowStart + windowMs;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Fixed Window Algorithm\n// ============================================================================\n\nasync function checkFixedWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const windowStart = Math.floor(now / windowMs) * windowMs;\n const windowKey = `${key}:${windowStart}`;\n\n const { count, reset } = await store.increment(windowKey);\n\n const remaining = Math.max(0, limit - count);\n const allowed = count <= limit;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Main Middleware\n// ============================================================================\n\n/**\n * Rate Limit Middleware for Hono.\n *\n * @param {RateLimitOptions} [options] - Configuration options\n * @returns {MiddlewareHandler} Middleware handler\n *\n * @example\n * ```ts\n * import { Hono } from 'hono'\n * import { rateLimiter } from 'hono-rate-limit'\n *\n * const app = new Hono()\n *\n * // Basic usage - 60 requests per minute\n * app.use(rateLimiter())\n *\n * // Custom configuration\n * app.use('/api/*', rateLimiter({\n * limit: 100,\n * windowMs: 60 * 1000,\n * }))\n * ```\n */\nexport const rateLimiter = <E extends Env = Env>(\n options?: RateLimitOptions<E>,\n): MiddlewareHandler<E> => {\n // Merge with defaults\n const opts = {\n limit: 60 as number | ((c: Context<E>) => number | Promise<number>),\n windowMs: 60_000,\n algorithm: \"sliding-window\" as Algorithm,\n store: undefined as RateLimitStore | undefined,\n keyGenerator: getClientIP as (c: Context<E>) => string | Promise<string>,\n handler: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => Response | Promise<Response>)\n | undefined,\n headers: \"draft-6\" as HeadersFormat,\n skip: undefined as\n | ((c: Context<E>) => boolean | Promise<boolean>)\n | undefined,\n skipSuccessfulRequests: false,\n skipFailedRequests: false,\n onRateLimited: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => void | Promise<void>)\n | undefined,\n ...options,\n };\n\n // Use default store if none provided\n const store = opts.store ?? (defaultStore ??= new MemoryStore());\n\n // Track initialization\n let initialized = false;\n\n return async function rateLimiter(c, next) {\n // Initialize store on first request\n if (!initialized && store.init) {\n await store.init(opts.windowMs);\n initialized = true;\n }\n\n // Check if should skip\n if (opts.skip) {\n const shouldSkip = await opts.skip(c);\n if (shouldSkip) {\n return next();\n }\n }\n\n // Generate key\n const key = await opts.keyGenerator(c);\n\n // Get limit (may be dynamic)\n const limit =\n typeof opts.limit === \"function\" ? await opts.limit(c) : opts.limit;\n\n // Check rate limit\n const { allowed, info } =\n opts.algorithm === \"sliding-window\"\n ? await checkSlidingWindow(store, key, limit, opts.windowMs)\n : await checkFixedWindow(store, key, limit, opts.windowMs);\n\n // Set context variable for downstream middleware\n c.set(\"rateLimit\", info);\n\n // Set headers\n setHeaders(c, info, opts.headers);\n\n // Handle rate limited\n if (!allowed) {\n // Fire callback\n if (opts.onRateLimited) {\n await opts.onRateLimited(c, info);\n }\n\n // Custom handler or default\n if (opts.handler) {\n return opts.handler(c, info);\n }\n return createDefaultResponse(info);\n }\n\n // Continue\n await next();\n\n // Handle skip options after response\n if (opts.skipSuccessfulRequests || opts.skipFailedRequests) {\n const status = c.res.status;\n const shouldDecrement =\n (opts.skipSuccessfulRequests && status >= 200 && status < 300) ||\n (opts.skipFailedRequests && status >= 400);\n\n if (shouldDecrement && store.decrement) {\n const windowStart =\n Math.floor(Date.now() / opts.windowMs) * opts.windowMs;\n const windowKey = `${key}:${windowStart}`;\n await store.decrement(windowKey);\n }\n }\n };\n};\n\n// ============================================================================\n// Exports\n// ============================================================================\n\nexport { getClientIP };\n"],"mappings":"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","names":["rateLimiter"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @module\n * Rate Limit Middleware for Hono.\n */\n\nimport type { Context, Env, MiddlewareHandler } from \"hono\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Rate limit information for a single request\n */\nexport type RateLimitInfo = {\n /** Maximum requests allowed in window */\n limit: number;\n /** Remaining requests in current window */\n remaining: number;\n /** Unix timestamp (ms) when window resets */\n reset: number;\n};\n\n/**\n * Result from store increment operation\n */\nexport type StoreResult = {\n /** Current request count in window */\n count: number;\n /** When the window resets (Unix timestamp ms) */\n reset: number;\n};\n\n/**\n * Header format versions\n */\nexport type HeadersFormat =\n | \"draft-6\" // X-RateLimit-* headers\n | \"draft-7\" // RateLimit-* without structured fields\n | false; // Disable headers\n\n/**\n * Rate limit algorithm\n */\nexport type Algorithm = \"fixed-window\" | \"sliding-window\";\n\n/**\n * Store interface for rate limit state\n */\nexport type RateLimitStore = {\n /**\n * Initialize store. Called once before first use.\n */\n init?: (windowMs: number) => void | Promise<void>;\n\n /**\n * Increment counter for key and return current state.\n */\n increment: (key: string) => StoreResult | Promise<StoreResult>;\n\n /**\n * Decrement counter for key.\n */\n decrement?: (key: string) => void | Promise<void>;\n\n /**\n * Reset a specific key.\n */\n resetKey: (key: string) => void | Promise<void>;\n\n /**\n * Reset all keys.\n */\n resetAll?: () => void | Promise<void>;\n\n /**\n * Get current state for key.\n */\n get?: (\n key: string,\n ) => StoreResult | Promise<StoreResult | undefined> | undefined;\n\n /**\n * Graceful shutdown.\n */\n shutdown?: () => void | Promise<void>;\n};\n\n/**\n * Store access interface exposed in context\n */\nexport type RateLimitStoreAccess = {\n /** Get rate limit info for a key */\n getKey: (\n key: string,\n ) => StoreResult | Promise<StoreResult | undefined> | undefined;\n /** Reset rate limit for a key */\n resetKey: (key: string) => void | Promise<void>;\n};\n\n/**\n * Options for rate limit middleware\n */\nexport type RateLimitOptions<E extends Env = Env> = {\n /**\n * Maximum requests allowed in the time window.\n * @default 60\n */\n limit?: number | ((c: Context<E>) => number | Promise<number>);\n\n /**\n * Time window in milliseconds.\n * @default 60000 (1 minute)\n */\n windowMs?: number;\n\n /**\n * Rate limiting algorithm.\n * @default 'sliding-window'\n */\n algorithm?: Algorithm;\n\n /**\n * Storage backend for rate limit state.\n * @default MemoryStore\n */\n store?: RateLimitStore;\n\n /**\n * Generate unique key for each client.\n * @default IP address from headers\n */\n keyGenerator?: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n */\n handler?: (\n c: Context<E>,\n info: RateLimitInfo,\n ) => Response | Promise<Response>;\n\n /**\n * HTTP header format to use.\n * @default 'draft-6'\n */\n headers?: HeadersFormat;\n\n /**\n * Skip rate limiting for certain requests.\n */\n skip?: (c: Context<E>) => boolean | Promise<boolean>;\n\n /**\n * Don't count successful (2xx) requests against limit.\n * @default false\n */\n skipSuccessfulRequests?: boolean;\n\n /**\n * Don't count failed (4xx, 5xx) requests against limit.\n * @default false\n */\n skipFailedRequests?: boolean;\n\n /**\n * Callback when a request is rate limited.\n */\n onRateLimited?: (c: Context<E>, info: RateLimitInfo) => void | Promise<void>;\n};\n\n/**\n * Cloudflare Rate Limiting binding interface\n */\nexport type RateLimitBinding = {\n limit: (options: { key: string }) => Promise<{ success: boolean }>;\n};\n\n/**\n * Options for Cloudflare Rate Limiting binding\n */\nexport type CloudflareRateLimitOptions<E extends Env = Env> = {\n /**\n * Cloudflare Rate Limiting binding from env\n */\n binding: RateLimitBinding | ((c: Context<E>) => RateLimitBinding);\n\n /**\n * Generate unique key for each client.\n */\n keyGenerator: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n */\n handler?: (c: Context<E>) => Response | Promise<Response>;\n\n /**\n * Skip rate limiting for certain requests.\n */\n skip?: (c: Context<E>) => boolean | Promise<boolean>;\n};\n\n// ============================================================================\n// Context Variable Type Extension\n// ============================================================================\n\ndeclare module \"hono\" {\n interface ContextVariableMap {\n rateLimit?: RateLimitInfo;\n rateLimitStore?: RateLimitStoreAccess;\n }\n}\n\n// ============================================================================\n// Memory Store\n// ============================================================================\n\ntype MemoryEntry = {\n count: number;\n reset: number;\n};\n\n/**\n * In-memory store for rate limiting.\n * Suitable for single-instance deployments.\n */\nexport class MemoryStore implements RateLimitStore {\n private entries = new Map<string, MemoryEntry>();\n private windowMs = 60_000;\n private cleanupTimer?: ReturnType<typeof setInterval>;\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n\n // Cleanup expired entries every minute\n this.cleanupTimer = setInterval(() => {\n const now = Date.now();\n for (const [key, entry] of this.entries) {\n if (entry.reset <= now) {\n this.entries.delete(key);\n }\n }\n }, 60_000);\n\n // Don't keep process alive for cleanup\n if (typeof this.cleanupTimer.unref === \"function\") {\n this.cleanupTimer.unref();\n }\n }\n\n increment(key: string): StoreResult {\n const now = Date.now();\n const existing = this.entries.get(key);\n\n if (!existing || existing.reset <= now) {\n // New window\n const reset = now + this.windowMs;\n this.entries.set(key, { count: 1, reset });\n return { count: 1, reset };\n }\n\n // Increment existing\n existing.count++;\n return { count: existing.count, reset: existing.reset };\n }\n\n get(key: string): StoreResult | undefined {\n const entry = this.entries.get(key);\n if (!entry || entry.reset <= Date.now()) {\n return undefined;\n }\n return { count: entry.count, reset: entry.reset };\n }\n\n decrement(key: string): void {\n const entry = this.entries.get(key);\n if (entry && entry.count > 0) {\n entry.count--;\n }\n }\n\n resetKey(key: string): void {\n this.entries.delete(key);\n }\n\n resetAll(): void {\n this.entries.clear();\n }\n\n shutdown(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n }\n this.entries.clear();\n }\n}\n\n// Singleton default store\nlet defaultStore: MemoryStore | undefined;\n\n// ============================================================================\n// Header Generation\n// ============================================================================\n\nfunction setHeaders(\n c: Context,\n info: RateLimitInfo,\n format: HeadersFormat,\n windowMs: number,\n): void {\n if (format === false) {\n return;\n }\n\n const windowSeconds = Math.ceil(windowMs / 1000);\n const resetSeconds = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n // RateLimit-Policy header (IETF compliant)\n c.header(\"RateLimit-Policy\", `${info.limit};w=${windowSeconds}`);\n\n switch (format) {\n case \"draft-7\":\n c.header(\n \"RateLimit\",\n `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`,\n );\n break;\n\n case \"draft-6\":\n default:\n c.header(\"X-RateLimit-Limit\", String(info.limit));\n c.header(\"X-RateLimit-Remaining\", String(info.remaining));\n c.header(\"X-RateLimit-Reset\", String(Math.ceil(info.reset / 1000)));\n break;\n }\n}\n\n// ============================================================================\n// Default Key Generator\n// ============================================================================\n\nfunction getClientIP(c: Context): string {\n // Platform-specific headers (most reliable)\n const cfIP = c.req.header(\"cf-connecting-ip\");\n if (cfIP) {\n return cfIP;\n }\n\n const xRealIP = c.req.header(\"x-real-ip\");\n if (xRealIP) {\n return xRealIP;\n }\n\n // X-Forwarded-For - take first IP\n const xff = c.req.header(\"x-forwarded-for\");\n if (xff) {\n return xff.split(\",\")[0].trim();\n }\n\n return \"unknown\";\n}\n\n// ============================================================================\n// Default Handler\n// ============================================================================\n\nfunction createDefaultResponse(info: RateLimitInfo): Response {\n const retryAfter = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n return new Response(\"Rate limit exceeded\", {\n status: 429,\n headers: {\n \"Content-Type\": \"text/plain\",\n \"Retry-After\": String(retryAfter),\n },\n });\n}\n\n// ============================================================================\n// Sliding Window Algorithm\n// ============================================================================\n\nasync function checkSlidingWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const currentWindowStart = Math.floor(now / windowMs) * windowMs;\n const previousWindowStart = currentWindowStart - windowMs;\n\n const previousKey = `${key}:${previousWindowStart}`;\n const currentKey = `${key}:${currentWindowStart}`;\n\n // Increment current window\n const current = await store.increment(currentKey);\n\n // Get previous window (may not exist)\n let previousCount = 0;\n if (store.get) {\n const prev = await store.get(previousKey);\n previousCount = prev?.count ?? 0;\n }\n\n // Cloudflare's weighted formula\n const elapsedMs = now - currentWindowStart;\n const weight = (windowMs - elapsedMs) / windowMs;\n const estimatedCount = Math.floor(previousCount * weight) + current.count;\n\n const remaining = Math.max(0, limit - estimatedCount);\n const allowed = estimatedCount <= limit;\n const reset = currentWindowStart + windowMs;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Fixed Window Algorithm\n// ============================================================================\n\nasync function checkFixedWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const windowStart = Math.floor(now / windowMs) * windowMs;\n const windowKey = `${key}:${windowStart}`;\n\n const { count, reset } = await store.increment(windowKey);\n\n const remaining = Math.max(0, limit - count);\n const allowed = count <= limit;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Main Middleware\n// ============================================================================\n\n/**\n * Rate Limit Middleware for Hono.\n *\n * @param {RateLimitOptions} [options] - Configuration options\n * @returns {MiddlewareHandler} Middleware handler\n *\n * @example\n * ```ts\n * import { Hono } from 'hono'\n * import { rateLimiter } from '@jellyfungus/hono-rate-limiter'\n *\n * const app = new Hono()\n *\n * // Basic usage - 60 requests per minute\n * app.use(rateLimiter())\n *\n * // Custom configuration\n * app.use('/api/*', rateLimiter({\n * limit: 100,\n * windowMs: 60 * 1000,\n * }))\n * ```\n */\nexport const rateLimiter = <E extends Env = Env>(\n options?: RateLimitOptions<E>,\n): MiddlewareHandler<E> => {\n // Merge with defaults\n const opts = {\n limit: 60 as number | ((c: Context<E>) => number | Promise<number>),\n windowMs: 60_000,\n algorithm: \"sliding-window\" as Algorithm,\n store: undefined as RateLimitStore | undefined,\n keyGenerator: getClientIP as (c: Context<E>) => string | Promise<string>,\n handler: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => Response | Promise<Response>)\n | undefined,\n headers: \"draft-6\" as HeadersFormat,\n skip: undefined as\n | ((c: Context<E>) => boolean | Promise<boolean>)\n | undefined,\n skipSuccessfulRequests: false,\n skipFailedRequests: false,\n onRateLimited: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => void | Promise<void>)\n | undefined,\n ...options,\n };\n\n // Use default store if none provided\n const store = opts.store ?? (defaultStore ??= new MemoryStore());\n\n // Track initialization\n let initialized = false;\n\n return async function rateLimiter(c, next) {\n // Initialize store on first request\n if (!initialized && store.init) {\n await store.init(opts.windowMs);\n initialized = true;\n }\n\n // Check if should skip\n if (opts.skip) {\n const shouldSkip = await opts.skip(c);\n if (shouldSkip) {\n return next();\n }\n }\n\n // Generate key\n const key = await opts.keyGenerator(c);\n\n // Get limit (may be dynamic)\n const limit =\n typeof opts.limit === \"function\" ? await opts.limit(c) : opts.limit;\n\n // Check rate limit\n const { allowed, info } =\n opts.algorithm === \"sliding-window\"\n ? await checkSlidingWindow(store, key, limit, opts.windowMs)\n : await checkFixedWindow(store, key, limit, opts.windowMs);\n\n // Set context variable for downstream middleware\n c.set(\"rateLimit\", info);\n\n // Expose store access in context\n c.set(\"rateLimitStore\", {\n getKey: store.get?.bind(store) ?? (() => undefined),\n resetKey: store.resetKey.bind(store),\n });\n\n // Set headers\n setHeaders(c, info, opts.headers, opts.windowMs);\n\n // Handle rate limited\n if (!allowed) {\n // Fire callback\n if (opts.onRateLimited) {\n await opts.onRateLimited(c, info);\n }\n\n // Custom handler or default\n if (opts.handler) {\n return opts.handler(c, info);\n }\n return createDefaultResponse(info);\n }\n\n // Continue\n await next();\n\n // Handle skip options after response\n if (opts.skipSuccessfulRequests || opts.skipFailedRequests) {\n const status = c.res.status;\n const shouldDecrement =\n (opts.skipSuccessfulRequests && status >= 200 && status < 300) ||\n (opts.skipFailedRequests && status >= 400);\n\n if (shouldDecrement && store.decrement) {\n const windowStart =\n Math.floor(Date.now() / opts.windowMs) * opts.windowMs;\n const windowKey = `${key}:${windowStart}`;\n await store.decrement(windowKey);\n }\n }\n };\n};\n\n// ============================================================================\n// Cloudflare Rate Limiting Binding Middleware\n// ============================================================================\n\n/**\n * Rate limiter using Cloudflare's built-in Rate Limiting binding.\n *\n * This uses Cloudflare's globally distributed rate limiting infrastructure,\n * which is ideal for high-traffic applications.\n *\n * @example\n * ```ts\n * import { cloudflareRateLimiter } from '@jellyfungus/hono-rate-limiter'\n *\n * type Bindings = { RATE_LIMITER: RateLimitBinding }\n *\n * const app = new Hono<{ Bindings: Bindings }>()\n *\n * app.use(cloudflareRateLimiter({\n * binding: (c) => c.env.RATE_LIMITER,\n * keyGenerator: (c) => c.req.header('cf-connecting-ip') ?? 'unknown',\n * }))\n * ```\n */\nexport const cloudflareRateLimiter = <E extends Env = Env>(\n options: CloudflareRateLimitOptions<E>,\n): MiddlewareHandler<E> => {\n const { binding, keyGenerator, handler, skip } = options;\n\n return async function cloudflareRateLimiter(c, next) {\n // Check if should skip\n if (skip) {\n const shouldSkip = await skip(c);\n if (shouldSkip) {\n return next();\n }\n }\n\n // Get binding (may be dynamic)\n const rateLimitBinding =\n typeof binding === \"function\" ? binding(c) : binding;\n\n // Generate key\n const key = await keyGenerator(c);\n\n // Check rate limit\n const { success } = await rateLimitBinding.limit({ key });\n\n if (!success) {\n if (handler) {\n return handler(c);\n }\n return new Response(\"Rate limit exceeded\", {\n status: 429,\n headers: { \"Content-Type\": \"text/plain\" },\n });\n }\n\n return next();\n };\n};\n\n// ============================================================================\n// Exports\n// ============================================================================\n\nexport { getClientIP };\n"],"mappings":"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","names":["rateLimiter","cloudflareRateLimiter"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -53,6 +53,10 @@ type RateLimitStore = {
|
|
|
53
53
|
* Reset a specific key.
|
|
54
54
|
*/
|
|
55
55
|
resetKey: (key: string) => void | Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Reset all keys.
|
|
58
|
+
*/
|
|
59
|
+
resetAll?: () => void | Promise<void>;
|
|
56
60
|
/**
|
|
57
61
|
* Get current state for key.
|
|
58
62
|
*/
|
|
@@ -62,6 +66,15 @@ type RateLimitStore = {
|
|
|
62
66
|
*/
|
|
63
67
|
shutdown?: () => void | Promise<void>;
|
|
64
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* Store access interface exposed in context
|
|
71
|
+
*/
|
|
72
|
+
type RateLimitStoreAccess = {
|
|
73
|
+
/** Get rate limit info for a key */
|
|
74
|
+
getKey: (key: string) => StoreResult | Promise<StoreResult | undefined> | undefined;
|
|
75
|
+
/** Reset rate limit for a key */
|
|
76
|
+
resetKey: (key: string) => void | Promise<void>;
|
|
77
|
+
};
|
|
65
78
|
/**
|
|
66
79
|
* Options for rate limit middleware
|
|
67
80
|
*/
|
|
@@ -119,9 +132,41 @@ type RateLimitOptions<E extends Env = Env> = {
|
|
|
119
132
|
*/
|
|
120
133
|
onRateLimited?: (c: Context<E>, info: RateLimitInfo) => void | Promise<void>;
|
|
121
134
|
};
|
|
135
|
+
/**
|
|
136
|
+
* Cloudflare Rate Limiting binding interface
|
|
137
|
+
*/
|
|
138
|
+
type RateLimitBinding = {
|
|
139
|
+
limit: (options: {
|
|
140
|
+
key: string;
|
|
141
|
+
}) => Promise<{
|
|
142
|
+
success: boolean;
|
|
143
|
+
}>;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Options for Cloudflare Rate Limiting binding
|
|
147
|
+
*/
|
|
148
|
+
type CloudflareRateLimitOptions<E extends Env = Env> = {
|
|
149
|
+
/**
|
|
150
|
+
* Cloudflare Rate Limiting binding from env
|
|
151
|
+
*/
|
|
152
|
+
binding: RateLimitBinding | ((c: Context<E>) => RateLimitBinding);
|
|
153
|
+
/**
|
|
154
|
+
* Generate unique key for each client.
|
|
155
|
+
*/
|
|
156
|
+
keyGenerator: (c: Context<E>) => string | Promise<string>;
|
|
157
|
+
/**
|
|
158
|
+
* Handler called when rate limit is exceeded.
|
|
159
|
+
*/
|
|
160
|
+
handler?: (c: Context<E>) => Response | Promise<Response>;
|
|
161
|
+
/**
|
|
162
|
+
* Skip rate limiting for certain requests.
|
|
163
|
+
*/
|
|
164
|
+
skip?: (c: Context<E>) => boolean | Promise<boolean>;
|
|
165
|
+
};
|
|
122
166
|
declare module "hono" {
|
|
123
167
|
interface ContextVariableMap {
|
|
124
168
|
rateLimit?: RateLimitInfo;
|
|
169
|
+
rateLimitStore?: RateLimitStoreAccess;
|
|
125
170
|
}
|
|
126
171
|
}
|
|
127
172
|
/**
|
|
@@ -137,6 +182,7 @@ declare class MemoryStore implements RateLimitStore {
|
|
|
137
182
|
get(key: string): StoreResult | undefined;
|
|
138
183
|
decrement(key: string): void;
|
|
139
184
|
resetKey(key: string): void;
|
|
185
|
+
resetAll(): void;
|
|
140
186
|
shutdown(): void;
|
|
141
187
|
}
|
|
142
188
|
declare function getClientIP(c: Context): string;
|
|
@@ -149,7 +195,7 @@ declare function getClientIP(c: Context): string;
|
|
|
149
195
|
* @example
|
|
150
196
|
* ```ts
|
|
151
197
|
* import { Hono } from 'hono'
|
|
152
|
-
* import { rateLimiter } from 'hono-rate-
|
|
198
|
+
* import { rateLimiter } from '@jellyfungus/hono-rate-limiter'
|
|
153
199
|
*
|
|
154
200
|
* const app = new Hono()
|
|
155
201
|
*
|
|
@@ -164,5 +210,26 @@ declare function getClientIP(c: Context): string;
|
|
|
164
210
|
* ```
|
|
165
211
|
*/
|
|
166
212
|
declare const rateLimiter: <E extends Env = Env>(options?: RateLimitOptions<E>) => MiddlewareHandler<E>;
|
|
213
|
+
/**
|
|
214
|
+
* Rate limiter using Cloudflare's built-in Rate Limiting binding.
|
|
215
|
+
*
|
|
216
|
+
* This uses Cloudflare's globally distributed rate limiting infrastructure,
|
|
217
|
+
* which is ideal for high-traffic applications.
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```ts
|
|
221
|
+
* import { cloudflareRateLimiter } from '@jellyfungus/hono-rate-limiter'
|
|
222
|
+
*
|
|
223
|
+
* type Bindings = { RATE_LIMITER: RateLimitBinding }
|
|
224
|
+
*
|
|
225
|
+
* const app = new Hono<{ Bindings: Bindings }>()
|
|
226
|
+
*
|
|
227
|
+
* app.use(cloudflareRateLimiter({
|
|
228
|
+
* binding: (c) => c.env.RATE_LIMITER,
|
|
229
|
+
* keyGenerator: (c) => c.req.header('cf-connecting-ip') ?? 'unknown',
|
|
230
|
+
* }))
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
declare const cloudflareRateLimiter: <E extends Env = Env>(options: CloudflareRateLimitOptions<E>) => MiddlewareHandler<E>;
|
|
167
234
|
|
|
168
|
-
export { type Algorithm, type HeadersFormat, MemoryStore, type RateLimitInfo, type RateLimitOptions, type RateLimitStore, type StoreResult, getClientIP, rateLimiter };
|
|
235
|
+
export { type Algorithm, type CloudflareRateLimitOptions, type HeadersFormat, MemoryStore, type RateLimitBinding, type RateLimitInfo, type RateLimitOptions, type RateLimitStore, type RateLimitStoreAccess, type StoreResult, cloudflareRateLimiter, getClientIP, rateLimiter };
|