@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 CHANGED
@@ -1,19 +1,20 @@
1
1
  # @jellyfungus/hono-rate-limiter
2
2
 
3
- Rate limiting middleware for [Hono](https://hono.dev) web framework.
3
+ Production-ready rate limiting middleware for [Hono](https://hono.dev) web framework.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@jellyfungus/hono-rate-limiter.svg)](https://www.npmjs.com/package/@jellyfungus/hono-rate-limiter)
6
6
  [![CI](https://github.com/rokasta12/hono-rate-limiter/actions/workflows/ci.yml/badge.svg)](https://github.com/rokasta12/hono-rate-limiter/actions/workflows/ci.yml)
7
7
 
8
- ## Features
8
+ ## Highlights
9
9
 
10
- - **Sliding Window Algorithm** - Accurate rate limiting with Cloudflare's approach
11
- - **Fixed Window Algorithm** - Simple alternative
12
- - **Multiple Stores** - Memory (default), Redis, Cloudflare KV
13
- - **IETF Headers** - RateLimit headers (draft-6, draft-7)
14
- - **Dynamic Limits** - Per-user or per-route limits
15
- - **TypeScript** - Full type support
16
- - **Zero Dependencies** - Only requires Hono as peer dependency
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
- ## Basic Usage
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 (default)
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
- ## Options
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
- | Option | Type | Default | Description |
54
- | ------------------------ | ------------------------------------ | ------------------ | ----------------------------- |
55
- | `limit` | `number \| Function` | `60` | Max requests per window |
56
- | `windowMs` | `number` | `60000` | Window duration in ms |
57
- | `algorithm` | `'sliding-window' \| 'fixed-window'` | `'sliding-window'` | Rate limiting algorithm |
58
- | `store` | `RateLimitStore` | `MemoryStore` | Storage backend |
59
- | `keyGenerator` | `Function` | IP address | Generate client key |
60
- | `handler` | `Function` | 429 response | Custom rate limit handler |
61
- | `headers` | `'draft-6' \| 'draft-7' \| false` | `'draft-6'` | Header format |
62
- | `skip` | `Function` | - | Skip rate limiting |
63
- | `skipSuccessfulRequests` | `boolean` | `false` | Don't count 2xx responses |
64
- | `skipFailedRequests` | `boolean` | `false` | Don't count 4xx/5xx responses |
65
- | `onRateLimited` | `Function` | - | Callback when rate limited |
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
- ```ts
72
- import { rateLimiter } from "@jellyfungus/hono-rate-limiter";
98
+ Perfect for single-instance deployments:
73
99
 
74
- app.use(rateLimiter()); // Uses MemoryStore by default
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 === "premium") return 1000;
117
- if (user?.tier === "pro") return 500;
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
- // Don't rate limit health checks
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
- reset: info?.reset,
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 "draft-6":
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: "draft-6",
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
- setHeaders(c, info, opts.headers);
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
  });
@@ -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 * 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"]}