@jellyfungus/hono-rate-limiter 0.1.0 → 0.3.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 +239 -44
- package/dist/index.cjs +68 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +124 -5
- package/dist/index.d.ts +124 -5
- package/dist/index.js +67 -4
- 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
|
+
- **Standard Rate Limit Headers** - Support for both IETF standard (`RateLimit`) and legacy (`X-RateLimit-*`) headers
|
|
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,77 @@ 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
|
|
64
|
+
|
|
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: "legacy", // Header format (see Response Headers section)
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
```
|
|
52
75
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
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` | `'legacy' \| 'draft-6' \| 'draft-7' \| 'standard' \| false` | `'legacy'` | Header format (see below) |
|
|
87
|
+
| `identifier` | `string` | `'default'` | Policy name for IETF headers |
|
|
88
|
+
| `quotaUnit` | `'requests' \| 'content-bytes' \| 'concurrent-requests'` | `'requests'` | Quota unit for IETF standard headers |
|
|
89
|
+
| `skip` | `Function` | - | Skip rate limiting conditionally |
|
|
90
|
+
| `skipSuccessfulRequests` | `boolean` | `false` | Don't count 2xx responses |
|
|
91
|
+
| `skipFailedRequests` | `boolean` | `false` | Don't count 4xx/5xx responses |
|
|
92
|
+
| `onRateLimited` | `Function` | - | Callback when rate limited |
|
|
66
93
|
|
|
67
94
|
## Stores
|
|
68
95
|
|
|
69
96
|
### Memory Store (Default)
|
|
70
97
|
|
|
71
|
-
|
|
72
|
-
import { rateLimiter } from "@jellyfungus/hono-rate-limiter";
|
|
98
|
+
Perfect for single-instance deployments:
|
|
73
99
|
|
|
74
|
-
|
|
100
|
+
```ts
|
|
101
|
+
app.use(rateLimiter()); // Uses MemoryStore automatically
|
|
75
102
|
```
|
|
76
103
|
|
|
77
104
|
### Redis Store
|
|
78
105
|
|
|
106
|
+
For distributed deployments. Uses atomic Lua scripts for race-condition-free operations:
|
|
107
|
+
|
|
79
108
|
```ts
|
|
80
109
|
import { rateLimiter } from "@jellyfungus/hono-rate-limiter";
|
|
81
110
|
import { RedisStore } from "@jellyfungus/hono-rate-limiter/store/redis";
|
|
@@ -92,12 +121,16 @@ app.use(
|
|
|
92
121
|
|
|
93
122
|
### Cloudflare KV Store
|
|
94
123
|
|
|
124
|
+
For Cloudflare Workers with KV:
|
|
125
|
+
|
|
95
126
|
```ts
|
|
96
127
|
import { rateLimiter } from "@jellyfungus/hono-rate-limiter";
|
|
97
128
|
import { CloudflareKVStore } from "@jellyfungus/hono-rate-limiter/store/cloudflare-kv";
|
|
98
129
|
|
|
99
130
|
type Bindings = { RATE_LIMIT_KV: KVNamespace };
|
|
100
131
|
|
|
132
|
+
const app = new Hono<{ Bindings: Bindings }>();
|
|
133
|
+
|
|
101
134
|
app.use("*", async (c, next) => {
|
|
102
135
|
const limiter = rateLimiter({
|
|
103
136
|
store: new CloudflareKVStore({ namespace: c.env.RATE_LIMIT_KV }),
|
|
@@ -106,15 +139,65 @@ app.use("*", async (c, next) => {
|
|
|
106
139
|
});
|
|
107
140
|
```
|
|
108
141
|
|
|
142
|
+
### Cloudflare Rate Limiting Binding
|
|
143
|
+
|
|
144
|
+
For enterprise-grade, globally distributed rate limiting using Cloudflare's native Rate Limiting:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { cloudflareRateLimiter } from "@jellyfungus/hono-rate-limiter";
|
|
148
|
+
|
|
149
|
+
type Bindings = { RATE_LIMITER: RateLimitBinding };
|
|
150
|
+
|
|
151
|
+
const app = new Hono<{ Bindings: Bindings }>();
|
|
152
|
+
|
|
153
|
+
app.use(
|
|
154
|
+
cloudflareRateLimiter({
|
|
155
|
+
binding: (c) => c.env.RATE_LIMITER,
|
|
156
|
+
keyGenerator: (c) => c.req.header("cf-connecting-ip") ?? "unknown",
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## WebSocket Rate Limiting
|
|
162
|
+
|
|
163
|
+
Rate limit WebSocket message frequency:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { Hono } from "hono";
|
|
167
|
+
import { createBunWebSocket } from "hono/bun";
|
|
168
|
+
import { webSocketLimiter } from "@jellyfungus/hono-rate-limiter/websocket";
|
|
169
|
+
|
|
170
|
+
const { upgradeWebSocket, websocket } = createBunWebSocket();
|
|
171
|
+
|
|
172
|
+
const wsLimiter = webSocketLimiter({
|
|
173
|
+
limit: 100,
|
|
174
|
+
windowMs: 60_000,
|
|
175
|
+
keyGenerator: (c) => c.req.header("cf-connecting-ip") ?? "unknown",
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
app.get(
|
|
179
|
+
"/ws",
|
|
180
|
+
upgradeWebSocket(
|
|
181
|
+
wsLimiter((c) => ({
|
|
182
|
+
onMessage(event, ws) {
|
|
183
|
+
ws.send("Hello!");
|
|
184
|
+
},
|
|
185
|
+
})),
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
```
|
|
189
|
+
|
|
109
190
|
## Dynamic Limits
|
|
110
191
|
|
|
192
|
+
Different limits for different users:
|
|
193
|
+
|
|
111
194
|
```ts
|
|
112
195
|
app.use(
|
|
113
196
|
rateLimiter({
|
|
114
197
|
limit: async (c) => {
|
|
115
198
|
const user = c.get("user");
|
|
116
|
-
if (user?.tier === "
|
|
117
|
-
if (user?.tier === "pro") return
|
|
199
|
+
if (user?.tier === "enterprise") return 10000;
|
|
200
|
+
if (user?.tier === "pro") return 1000;
|
|
118
201
|
return 100;
|
|
119
202
|
},
|
|
120
203
|
}),
|
|
@@ -123,11 +206,12 @@ app.use(
|
|
|
123
206
|
|
|
124
207
|
## Custom Key Generator
|
|
125
208
|
|
|
209
|
+
Rate limit by API key, user ID, or any identifier:
|
|
210
|
+
|
|
126
211
|
```ts
|
|
127
212
|
app.use(
|
|
128
213
|
rateLimiter({
|
|
129
214
|
keyGenerator: (c) => {
|
|
130
|
-
// Rate limit by API key instead of IP
|
|
131
215
|
return c.req.header("x-api-key") ?? "anonymous";
|
|
132
216
|
},
|
|
133
217
|
}),
|
|
@@ -136,12 +220,13 @@ app.use(
|
|
|
136
220
|
|
|
137
221
|
## Skip Certain Requests
|
|
138
222
|
|
|
223
|
+
Bypass rate limiting for specific routes:
|
|
224
|
+
|
|
139
225
|
```ts
|
|
140
226
|
app.use(
|
|
141
227
|
rateLimiter({
|
|
142
228
|
skip: (c) => {
|
|
143
|
-
|
|
144
|
-
return c.req.path === "/health";
|
|
229
|
+
return c.req.path === "/health" || c.req.path === "/metrics";
|
|
145
230
|
},
|
|
146
231
|
}),
|
|
147
232
|
);
|
|
@@ -149,19 +234,38 @@ app.use(
|
|
|
149
234
|
|
|
150
235
|
## Access Rate Limit Info
|
|
151
236
|
|
|
237
|
+
Get rate limit information in your handlers:
|
|
238
|
+
|
|
152
239
|
```ts
|
|
153
|
-
app.get("/status", (c) => {
|
|
240
|
+
app.get("/api/status", (c) => {
|
|
154
241
|
const info = c.get("rateLimit");
|
|
155
242
|
return c.json({
|
|
156
243
|
limit: info?.limit,
|
|
157
244
|
remaining: info?.remaining,
|
|
158
|
-
|
|
245
|
+
resetAt: info?.reset,
|
|
159
246
|
});
|
|
160
247
|
});
|
|
161
248
|
```
|
|
162
249
|
|
|
250
|
+
## Manual Store Control
|
|
251
|
+
|
|
252
|
+
Access the store directly for manual operations:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
app.post("/api/admin/reset-limit/:userId", async (c) => {
|
|
256
|
+
const store = c.get("rateLimitStore");
|
|
257
|
+
const userId = c.req.param("userId");
|
|
258
|
+
|
|
259
|
+
await store?.resetKey(userId);
|
|
260
|
+
|
|
261
|
+
return c.json({ success: true });
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
163
265
|
## Custom Handler
|
|
164
266
|
|
|
267
|
+
Customize the rate limit exceeded response:
|
|
268
|
+
|
|
165
269
|
```ts
|
|
166
270
|
app.use(
|
|
167
271
|
rateLimiter({
|
|
@@ -178,6 +282,97 @@ app.use(
|
|
|
178
282
|
);
|
|
179
283
|
```
|
|
180
284
|
|
|
285
|
+
## Response Headers
|
|
286
|
+
|
|
287
|
+
The middleware supports multiple header formats. Choose based on your needs:
|
|
288
|
+
|
|
289
|
+
### `"legacy"` (default)
|
|
290
|
+
|
|
291
|
+
The widely-used `X-RateLimit-*` headers. Used by GitHub, Twitter, and most APIs:
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
X-RateLimit-Limit: 60
|
|
295
|
+
X-RateLimit-Remaining: 45
|
|
296
|
+
X-RateLimit-Reset: 1640000000
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
- **Reset** is a Unix timestamp (seconds since epoch)
|
|
300
|
+
- Best for broad client compatibility
|
|
301
|
+
|
|
302
|
+
### `"draft-6"`
|
|
303
|
+
|
|
304
|
+
IETF draft-06 format with individual `RateLimit-*` headers:
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
RateLimit-Policy: 60;w=60
|
|
308
|
+
RateLimit-Limit: 60
|
|
309
|
+
RateLimit-Remaining: 45
|
|
310
|
+
RateLimit-Reset: 30
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
- **Reset** is seconds until the window resets (not a timestamp)
|
|
314
|
+
- Compatible with express-rate-limit's `draft-6` option
|
|
315
|
+
|
|
316
|
+
### `"draft-7"`
|
|
317
|
+
|
|
318
|
+
IETF draft-07 format with a combined `RateLimit` header:
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
RateLimit-Policy: 60;w=60
|
|
322
|
+
RateLimit: limit=60, remaining=45, reset=30
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
- Uses comma-separated values in a single header
|
|
326
|
+
- Compatible with express-rate-limit's `draft-7` option
|
|
327
|
+
|
|
328
|
+
### `"standard"` (IETF draft-08+)
|
|
329
|
+
|
|
330
|
+
Current IETF `draft-ietf-httpapi-ratelimit-headers` specification using structured field values ([RFC 9651](https://datatracker.ietf.org/doc/rfc9651/)):
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
RateLimit-Policy: "default";q=60;w=60
|
|
334
|
+
RateLimit: "default";r=45;t=30
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Parameters:
|
|
338
|
+
|
|
339
|
+
| Header | Param | Meaning |
|
|
340
|
+
| ---------------- | ----- | -------------------------- |
|
|
341
|
+
| RateLimit-Policy | `q` | quota (max requests) |
|
|
342
|
+
| RateLimit-Policy | `w` | window (seconds) |
|
|
343
|
+
| RateLimit-Policy | `qu` | quota unit (optional) |
|
|
344
|
+
| RateLimit | `r` | remaining requests |
|
|
345
|
+
| RateLimit | `t` | time until reset (seconds) |
|
|
346
|
+
|
|
347
|
+
Use the `identifier` option to customize the policy name:
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
rateLimiter({
|
|
351
|
+
headers: "standard",
|
|
352
|
+
identifier: "api-v1",
|
|
353
|
+
// Headers: RateLimit-Policy: "api-v1";q=60;w=60
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Use the `quotaUnit` option for non-request-based limits:
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
rateLimiter({
|
|
361
|
+
headers: "standard",
|
|
362
|
+
identifier: "bandwidth",
|
|
363
|
+
quotaUnit: "content-bytes",
|
|
364
|
+
// Headers: RateLimit-Policy: "bandwidth";q=1000000;w=60;qu="content-bytes"
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Disabled
|
|
369
|
+
|
|
370
|
+
Use `headers: false` to disable all rate limit headers.
|
|
371
|
+
|
|
372
|
+
### Retry-After Header
|
|
373
|
+
|
|
374
|
+
When a request is rate limited (429 response), the `Retry-After` header is always included with the number of seconds until the client can retry.
|
|
375
|
+
|
|
181
376
|
## License
|
|
182
377
|
|
|
183
378
|
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,18 +82,40 @@ var MemoryStore = class {
|
|
|
78
82
|
}
|
|
79
83
|
};
|
|
80
84
|
var defaultStore;
|
|
81
|
-
function setHeaders(c, info, format) {
|
|
85
|
+
function setHeaders(c, info, format, windowMs, identifier, quotaUnit) {
|
|
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));
|
|
86
91
|
switch (format) {
|
|
92
|
+
case "standard":
|
|
93
|
+
{
|
|
94
|
+
let policy = `"${identifier}";q=${info.limit};w=${windowSeconds}`;
|
|
95
|
+
if (quotaUnit !== "requests") {
|
|
96
|
+
policy += `;qu="${quotaUnit}"`;
|
|
97
|
+
}
|
|
98
|
+
c.header("RateLimit-Policy", policy);
|
|
99
|
+
c.header(
|
|
100
|
+
"RateLimit",
|
|
101
|
+
`"${identifier}";r=${info.remaining};t=${resetSeconds}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
87
105
|
case "draft-7":
|
|
106
|
+
c.header("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
107
|
+
c.header(
|
|
108
|
+
"RateLimit",
|
|
109
|
+
`limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
|
|
110
|
+
);
|
|
111
|
+
break;
|
|
112
|
+
case "draft-6":
|
|
113
|
+
c.header("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
|
|
88
114
|
c.header("RateLimit-Limit", String(info.limit));
|
|
89
115
|
c.header("RateLimit-Remaining", String(info.remaining));
|
|
90
116
|
c.header("RateLimit-Reset", String(resetSeconds));
|
|
91
117
|
break;
|
|
92
|
-
case "
|
|
118
|
+
case "legacy":
|
|
93
119
|
default:
|
|
94
120
|
c.header("X-RateLimit-Limit", String(info.limit));
|
|
95
121
|
c.header("X-RateLimit-Remaining", String(info.remaining));
|
|
@@ -165,7 +191,9 @@ var rateLimiter = (options) => {
|
|
|
165
191
|
store: void 0,
|
|
166
192
|
keyGenerator: getClientIP,
|
|
167
193
|
handler: void 0,
|
|
168
|
-
headers: "
|
|
194
|
+
headers: "legacy",
|
|
195
|
+
identifier: "default",
|
|
196
|
+
quotaUnit: "requests",
|
|
169
197
|
skip: void 0,
|
|
170
198
|
skipSuccessfulRequests: false,
|
|
171
199
|
skipFailedRequests: false,
|
|
@@ -189,7 +217,18 @@ var rateLimiter = (options) => {
|
|
|
189
217
|
const limit = typeof opts.limit === "function" ? await opts.limit(c) : opts.limit;
|
|
190
218
|
const { allowed, info } = opts.algorithm === "sliding-window" ? await checkSlidingWindow(store, key, limit, opts.windowMs) : await checkFixedWindow(store, key, limit, opts.windowMs);
|
|
191
219
|
c.set("rateLimit", info);
|
|
192
|
-
|
|
220
|
+
c.set("rateLimitStore", {
|
|
221
|
+
getKey: store.get?.bind(store) ?? (() => void 0),
|
|
222
|
+
resetKey: store.resetKey.bind(store)
|
|
223
|
+
});
|
|
224
|
+
setHeaders(
|
|
225
|
+
c,
|
|
226
|
+
info,
|
|
227
|
+
opts.headers,
|
|
228
|
+
opts.windowMs,
|
|
229
|
+
opts.identifier,
|
|
230
|
+
opts.quotaUnit
|
|
231
|
+
);
|
|
193
232
|
if (!allowed) {
|
|
194
233
|
if (opts.onRateLimited) {
|
|
195
234
|
await opts.onRateLimited(c, info);
|
|
@@ -211,9 +250,34 @@ var rateLimiter = (options) => {
|
|
|
211
250
|
}
|
|
212
251
|
};
|
|
213
252
|
};
|
|
253
|
+
var cloudflareRateLimiter = (options) => {
|
|
254
|
+
const { binding, keyGenerator, handler, skip } = options;
|
|
255
|
+
return async function cloudflareRateLimiter2(c, next) {
|
|
256
|
+
if (skip) {
|
|
257
|
+
const shouldSkip = await skip(c);
|
|
258
|
+
if (shouldSkip) {
|
|
259
|
+
return next();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const rateLimitBinding = typeof binding === "function" ? binding(c) : binding;
|
|
263
|
+
const key = await keyGenerator(c);
|
|
264
|
+
const { success } = await rateLimitBinding.limit({ key });
|
|
265
|
+
if (!success) {
|
|
266
|
+
if (handler) {
|
|
267
|
+
return handler(c);
|
|
268
|
+
}
|
|
269
|
+
return new Response("Rate limit exceeded", {
|
|
270
|
+
status: 429,
|
|
271
|
+
headers: { "Content-Type": "text/plain" }
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return next();
|
|
275
|
+
};
|
|
276
|
+
};
|
|
214
277
|
// Annotate the CommonJS export names for ESM import in node:
|
|
215
278
|
0 && (module.exports = {
|
|
216
279
|
MemoryStore,
|
|
280
|
+
cloudflareRateLimiter,
|
|
217
281
|
getClientIP,
|
|
218
282
|
rateLimiter
|
|
219
283
|
});
|
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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiLO,IAAM,cAAN,MAA4C;AAAA,EACzC,UAAU,oBAAI,IAAyB;AAAA,EACvC,WAAW;AAAA,EACX;AAAA,EAER,KAAK,UAAwB;AAC3B,SAAK,WAAW;AAGhB,SAAK,eAAe,YAAY,MAAM;AACpC,YAAM,MAAM,KAAK,IAAI;AACrB,iBAAW,CAAC,KAAK,KAAK,KAAK,KAAK,SAAS;AACvC,YAAI,MAAM,SAAS,KAAK;AACtB,eAAK,QAAQ,OAAO,GAAG;AAAA,QACzB;AAAA,MACF;AAAA,IACF,GAAG,GAAM;AAGT,QAAI,OAAO,KAAK,aAAa,UAAU,YAAY;AACjD,WAAK,aAAa,MAAM;AAAA,IAC1B;AAAA,EACF;AAAA,EAEA,UAAU,KAA0B;AAClC,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,WAAW,KAAK,QAAQ,IAAI,GAAG;AAErC,QAAI,CAAC,YAAY,SAAS,SAAS,KAAK;AAEtC,YAAM,QAAQ,MAAM,KAAK;AACzB,WAAK,QAAQ,IAAI,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;AACzC,aAAO,EAAE,OAAO,GAAG,MAAM;AAAA,IAC3B;AAGA,aAAS;AACT,WAAO,EAAE,OAAO,SAAS,OAAO,OAAO,SAAS,MAAM;AAAA,EACxD;AAAA,EAEA,IAAI,KAAsC;AACxC,UAAM,QAAQ,KAAK,QAAQ,IAAI,GAAG;AAClC,QAAI,CAAC,SAAS,MAAM,SAAS,KAAK,IAAI,GAAG;AACvC,aAAO;AAAA,IACT;AACA,WAAO,EAAE,OAAO,MAAM,OAAO,OAAO,MAAM,MAAM;AAAA,EAClD;AAAA,EAEA,UAAU,KAAmB;AAC3B,UAAM,QAAQ,KAAK,QAAQ,IAAI,GAAG;AAClC,QAAI,SAAS,MAAM,QAAQ,GAAG;AAC5B,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,SAAS,KAAmB;AAC1B,SAAK,QAAQ,OAAO,GAAG;AAAA,EACzB;AAAA,EAEA,WAAiB;AACf,QAAI,KAAK,cAAc;AACrB,oBAAc,KAAK,YAAY;AAAA,IACjC;AACA,SAAK,QAAQ,MAAM;AAAA,EACrB;AACF;AAGA,IAAI;AAMJ,SAAS,WACP,GACA,MACA,QACM;AACN,MAAI,WAAW,OAAO;AACpB;AAAA,EACF;AAEA,QAAM,eAAe,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,QAAQ,KAAK,IAAI,KAAK,GAAI,CAAC;AAE5E,UAAQ,QAAQ;AAAA,IACd,KAAK;AACH,QAAE,OAAO,mBAAmB,OAAO,KAAK,KAAK,CAAC;AAC9C,QAAE,OAAO,uBAAuB,OAAO,KAAK,SAAS,CAAC;AACtD,QAAE,OAAO,mBAAmB,OAAO,YAAY,CAAC;AAChD;AAAA,IAEF,KAAK;AAAA,IACL;AACE,QAAE,OAAO,qBAAqB,OAAO,KAAK,KAAK,CAAC;AAChD,QAAE,OAAO,yBAAyB,OAAO,KAAK,SAAS,CAAC;AACxD,QAAE,OAAO,qBAAqB,OAAO,KAAK,KAAK,KAAK,QAAQ,GAAI,CAAC,CAAC;AAClE;AAAA,EACJ;AACF;AAMA,SAAS,YAAY,GAAoB;AAEvC,QAAM,OAAO,EAAE,IAAI,OAAO,kBAAkB;AAC5C,MAAI,MAAM;AACR,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,EAAE,IAAI,OAAO,WAAW;AACxC,MAAI,SAAS;AACX,WAAO;AAAA,EACT;AAGA,QAAM,MAAM,EAAE,IAAI,OAAO,iBAAiB;AAC1C,MAAI,KAAK;AACP,WAAO,IAAI,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK;AAAA,EAChC;AAEA,SAAO;AACT;AAMA,SAAS,sBAAsB,MAA+B;AAC5D,QAAM,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,QAAQ,KAAK,IAAI,KAAK,GAAI,CAAC;AAE1E,SAAO,IAAI,SAAS,uBAAuB;AAAA,IACzC,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,eAAe,OAAO,UAAU;AAAA,IAClC;AAAA,EACF,CAAC;AACH;AAMA,eAAe,mBACb,OACA,KACA,OACA,UACoD;AACpD,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,qBAAqB,KAAK,MAAM,MAAM,QAAQ,IAAI;AACxD,QAAM,sBAAsB,qBAAqB;AAEjD,QAAM,cAAc,GAAG,GAAG,IAAI,mBAAmB;AACjD,QAAM,aAAa,GAAG,GAAG,IAAI,kBAAkB;AAG/C,QAAM,UAAU,MAAM,MAAM,UAAU,UAAU;AAGhD,MAAI,gBAAgB;AACpB,MAAI,MAAM,KAAK;AACb,UAAM,OAAO,MAAM,MAAM,IAAI,WAAW;AACxC,oBAAgB,MAAM,SAAS;AAAA,EACjC;AAGA,QAAM,YAAY,MAAM;AACxB,QAAM,UAAU,WAAW,aAAa;AACxC,QAAM,iBAAiB,KAAK,MAAM,gBAAgB,MAAM,IAAI,QAAQ;AAEpE,QAAM,YAAY,KAAK,IAAI,GAAG,QAAQ,cAAc;AACpD,QAAM,UAAU,kBAAkB;AAClC,QAAM,QAAQ,qBAAqB;AAEnC,SAAO;AAAA,IACL;AAAA,IACA,MAAM,EAAE,OAAO,WAAW,MAAM;AAAA,EAClC;AACF;AAMA,eAAe,iBACb,OACA,KACA,OACA,UACoD;AACpD,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,cAAc,KAAK,MAAM,MAAM,QAAQ,IAAI;AACjD,QAAM,YAAY,GAAG,GAAG,IAAI,WAAW;AAEvC,QAAM,EAAE,OAAO,MAAM,IAAI,MAAM,MAAM,UAAU,SAAS;AAExD,QAAM,YAAY,KAAK,IAAI,GAAG,QAAQ,KAAK;AAC3C,QAAM,UAAU,SAAS;AAEzB,SAAO;AAAA,IACL;AAAA,IACA,MAAM,EAAE,OAAO,WAAW,MAAM;AAAA,EAClC;AACF;AA6BO,IAAM,cAAc,CACzB,YACyB;AAEzB,QAAM,OAAO;AAAA,IACX,OAAO;AAAA,IACP,UAAU;AAAA,IACV,WAAW;AAAA,IACX,OAAO;AAAA,IACP,cAAc;AAAA,IACd,SAAS;AAAA,IAGT,SAAS;AAAA,IACT,MAAM;AAAA,IAGN,wBAAwB;AAAA,IACxB,oBAAoB;AAAA,IACpB,eAAe;AAAA,IAGf,GAAG;AAAA,EACL;AAGA,QAAM,QAAQ,KAAK,UAAU,iBAAiB,IAAI,YAAY;AAG9D,MAAI,cAAc;AAElB,SAAO,eAAeA,aAAY,GAAG,MAAM;AAEzC,QAAI,CAAC,eAAe,MAAM,MAAM;AAC9B,YAAM,MAAM,KAAK,KAAK,QAAQ;AAC9B,oBAAc;AAAA,IAChB;AAGA,QAAI,KAAK,MAAM;AACb,YAAM,aAAa,MAAM,KAAK,KAAK,CAAC;AACpC,UAAI,YAAY;AACd,eAAO,KAAK;AAAA,MACd;AAAA,IACF;AAGA,UAAM,MAAM,MAAM,KAAK,aAAa,CAAC;AAGrC,UAAM,QACJ,OAAO,KAAK,UAAU,aAAa,MAAM,KAAK,MAAM,CAAC,IAAI,KAAK;AAGhE,UAAM,EAAE,SAAS,KAAK,IACpB,KAAK,cAAc,mBACf,MAAM,mBAAmB,OAAO,KAAK,OAAO,KAAK,QAAQ,IACzD,MAAM,iBAAiB,OAAO,KAAK,OAAO,KAAK,QAAQ;AAG7D,MAAE,IAAI,aAAa,IAAI;AAGvB,eAAW,GAAG,MAAM,KAAK,OAAO;AAGhC,QAAI,CAAC,SAAS;AAEZ,UAAI,KAAK,eAAe;AACtB,cAAM,KAAK,cAAc,GAAG,IAAI;AAAA,MAClC;AAGA,UAAI,KAAK,SAAS;AAChB,eAAO,KAAK,QAAQ,GAAG,IAAI;AAAA,MAC7B;AACA,aAAO,sBAAsB,IAAI;AAAA,IACnC;AAGA,UAAM,KAAK;AAGX,QAAI,KAAK,0BAA0B,KAAK,oBAAoB;AAC1D,YAAM,SAAS,EAAE,IAAI;AACrB,YAAM,kBACH,KAAK,0BAA0B,UAAU,OAAO,SAAS,OACzD,KAAK,sBAAsB,UAAU;AAExC,UAAI,mBAAmB,MAAM,WAAW;AACtC,cAAM,cACJ,KAAK,MAAM,KAAK,IAAI,IAAI,KAAK,QAAQ,IAAI,KAAK;AAChD,cAAM,YAAY,GAAG,GAAG,IAAI,WAAW;AACvC,cAAM,MAAM,UAAU,SAAS;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AACF;","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 * Quota unit for IETF standard headers.\n * @see https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/\n */\nexport type QuotaUnit = \"requests\" | \"content-bytes\" | \"concurrent-requests\";\n\n/**\n * Header format options.\n *\n * ## \"legacy\" (default)\n * Common X-RateLimit-* headers used by GitHub, Twitter, and most APIs:\n * - `X-RateLimit-Limit`: max requests in window\n * - `X-RateLimit-Remaining`: remaining requests\n * - `X-RateLimit-Reset`: Unix timestamp (seconds) when window resets\n *\n * ## \"draft-6\"\n * IETF draft-06 format with individual RateLimit-* headers:\n * - `RateLimit-Policy`: policy description (e.g., `100;w=60`)\n * - `RateLimit-Limit`: max requests\n * - `RateLimit-Remaining`: remaining requests\n * - `RateLimit-Reset`: seconds until reset\n *\n * ## \"draft-7\"\n * IETF draft-07 format with combined RateLimit header:\n * - `RateLimit-Policy`: policy description\n * - `RateLimit`: combined (e.g., `limit=100, remaining=50, reset=30`)\n *\n * ## \"standard\"\n * Current IETF draft-08+ format with structured field values (RFC 9651):\n * - `RateLimit-Policy`: `\"name\";q=100;w=60`\n * - `RateLimit`: `\"name\";r=50;t=30`\n *\n * ## false\n * Disable all rate limit headers.\n *\n * @see https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/\n */\nexport type HeadersFormat =\n | \"legacy\" // X-RateLimit-* headers (GitHub/Twitter style)\n | \"draft-6\" // IETF draft-06: individual RateLimit-* headers\n | \"draft-7\" // IETF draft-07: combined RateLimit header\n | \"standard\" // IETF draft-08+: structured field format (current)\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 *\n * - \"legacy\": X-RateLimit-* headers (GitHub/Twitter style, default)\n * - \"draft-6\": IETF draft-06 individual headers\n * - \"draft-7\": IETF draft-07 combined header\n * - \"standard\": IETF draft-08+ structured fields (current spec)\n * - false: Disable headers\n *\n * @default 'legacy'\n */\n headers?: HeadersFormat;\n\n /**\n * Policy identifier for IETF headers (draft-6+).\n * Used in RateLimit and RateLimit-Policy headers.\n * @default 'default'\n */\n identifier?: string;\n\n /**\n * Quota unit for IETF standard headers.\n * Only included in \"standard\" format when not \"requests\".\n * @default 'requests'\n */\n quotaUnit?: QuotaUnit;\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\n/**\n * Set rate limit response headers based on the configured format.\n *\n * @see https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/\n */\nfunction setHeaders(\n c: Context,\n info: RateLimitInfo,\n format: HeadersFormat,\n windowMs: number,\n identifier: string,\n quotaUnit: QuotaUnit,\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 switch (format) {\n case \"standard\":\n // IETF draft-08+ (current): Structured field values per RFC 9651\n // RateLimit-Policy: describes the quota policy\n // Format: \"name\";q=<quota>;w=<window>[;qu=\"<unit>\"]\n {\n let policy = `\"${identifier}\";q=${info.limit};w=${windowSeconds}`;\n if (quotaUnit !== \"requests\") {\n policy += `;qu=\"${quotaUnit}\"`;\n }\n c.header(\"RateLimit-Policy\", policy);\n // RateLimit: describes current service limits\n // Format: \"name\";r=<remaining>;t=<reset>\n c.header(\n \"RateLimit\",\n `\"${identifier}\";r=${info.remaining};t=${resetSeconds}`,\n );\n }\n break;\n\n case \"draft-7\":\n // IETF draft-07: Combined RateLimit header with comma-separated values\n c.header(\"RateLimit-Policy\", `${info.limit};w=${windowSeconds}`);\n c.header(\n \"RateLimit\",\n `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`,\n );\n break;\n\n case \"draft-6\":\n // IETF draft-06: Individual RateLimit-* headers\n c.header(\"RateLimit-Policy\", `${info.limit};w=${windowSeconds}`);\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 \"legacy\":\n default:\n // Common X-RateLimit-* headers (GitHub, Twitter, most APIs)\n // Uses Unix timestamp for reset (seconds since epoch)\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: \"legacy\" as HeadersFormat,\n identifier: \"default\",\n quotaUnit: \"requests\" as QuotaUnit,\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(\n c,\n info,\n opts.headers,\n opts.windowMs,\n opts.identifier,\n opts.quotaUnit,\n );\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"]}
|