@jellyfungus/hono-rate-limiter 0.2.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
@@ -13,7 +13,7 @@ Production-ready rate limiting middleware for [Hono](https://hono.dev) web frame
13
13
  - **Multiple Stores** - Memory (default), Redis (with atomic Lua scripts), Cloudflare KV
14
14
  - **Cloudflare Rate Limiting Binding** - Native support for Cloudflare's globally distributed rate limiter
15
15
  - **WebSocket Support** - Rate limit WebSocket connections
16
- - **IETF Compliant Headers** - Full support for `RateLimit-*` headers (draft-6, draft-7)
16
+ - **Standard Rate Limit Headers** - Support for both IETF standard (`RateLimit`) and legacy (`X-RateLimit-*`) headers
17
17
  - **TypeScript First** - Complete type safety
18
18
 
19
19
  ## Installation
@@ -68,26 +68,28 @@ app.use(
68
68
  limit: 100, // Max requests per window (default: 60)
69
69
  windowMs: 60 * 1000, // Window duration in ms (default: 60000)
70
70
  algorithm: "sliding-window", // or "fixed-window" (default: sliding-window)
71
- headers: "draft-6", // or "draft-7" or false (default: draft-6)
71
+ headers: "legacy", // Header format (see Response Headers section)
72
72
  }),
73
73
  );
74
74
  ```
75
75
 
76
76
  ### All Options
77
77
 
78
- | Option | Type | Default | Description |
79
- | ------------------------ | ------------------------------------ | ------------------ | -------------------------------- |
80
- | `limit` | `number \| Function` | `60` | Max requests per window |
81
- | `windowMs` | `number` | `60000` | Window duration in ms |
82
- | `algorithm` | `'sliding-window' \| 'fixed-window'` | `'sliding-window'` | Rate limiting algorithm |
83
- | `store` | `RateLimitStore` | `MemoryStore` | Storage backend |
84
- | `keyGenerator` | `Function` | IP detection | Generate unique client key |
85
- | `handler` | `Function` | 429 response | Custom rate limit response |
86
- | `headers` | `'draft-6' \| 'draft-7' \| false` | `'draft-6'` | IETF header format |
87
- | `skip` | `Function` | - | Skip rate limiting conditionally |
88
- | `skipSuccessfulRequests` | `boolean` | `false` | Don't count 2xx responses |
89
- | `skipFailedRequests` | `boolean` | `false` | Don't count 4xx/5xx responses |
90
- | `onRateLimited` | `Function` | - | Callback when rate limited |
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 |
91
93
 
92
94
  ## Stores
93
95
 
@@ -282,26 +284,95 @@ app.use(
282
284
 
283
285
  ## Response Headers
284
286
 
285
- The middleware sets standard rate limit headers:
287
+ The middleware supports multiple header formats. Choose based on your needs:
286
288
 
287
- **draft-6 (default):**
289
+ ### `"legacy"` (default)
290
+
291
+ The widely-used `X-RateLimit-*` headers. Used by GitHub, Twitter, and most APIs:
288
292
 
289
293
  ```
290
294
  X-RateLimit-Limit: 60
291
295
  X-RateLimit-Remaining: 45
292
296
  X-RateLimit-Reset: 1640000000
293
- RateLimit-Policy: 60;w=60
294
297
  ```
295
298
 
296
- **draft-7:**
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:
297
305
 
298
306
  ```
307
+ RateLimit-Policy: 60;w=60
299
308
  RateLimit-Limit: 60
300
309
  RateLimit-Remaining: 45
301
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
+ ```
302
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
303
335
  ```
304
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
+
305
376
  ## License
306
377
 
307
378
  MIT
package/dist/index.cjs CHANGED
@@ -82,21 +82,40 @@ var MemoryStore = class {
82
82
  }
83
83
  };
84
84
  var defaultStore;
85
- function setHeaders(c, info, format, windowMs) {
85
+ function setHeaders(c, info, format, windowMs, identifier, quotaUnit) {
86
86
  if (format === false) {
87
87
  return;
88
88
  }
89
89
  const windowSeconds = Math.ceil(windowMs / 1e3);
90
90
  const resetSeconds = Math.max(0, Math.ceil((info.reset - Date.now()) / 1e3));
91
- c.header("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
92
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;
93
105
  case "draft-7":
106
+ c.header("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
94
107
  c.header(
95
108
  "RateLimit",
96
109
  `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
97
110
  );
98
111
  break;
99
112
  case "draft-6":
113
+ c.header("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
114
+ c.header("RateLimit-Limit", String(info.limit));
115
+ c.header("RateLimit-Remaining", String(info.remaining));
116
+ c.header("RateLimit-Reset", String(resetSeconds));
117
+ break;
118
+ case "legacy":
100
119
  default:
101
120
  c.header("X-RateLimit-Limit", String(info.limit));
102
121
  c.header("X-RateLimit-Remaining", String(info.remaining));
@@ -172,7 +191,9 @@ var rateLimiter = (options) => {
172
191
  store: void 0,
173
192
  keyGenerator: getClientIP,
174
193
  handler: void 0,
175
- headers: "draft-6",
194
+ headers: "legacy",
195
+ identifier: "default",
196
+ quotaUnit: "requests",
176
197
  skip: void 0,
177
198
  skipSuccessfulRequests: false,
178
199
  skipFailedRequests: false,
@@ -200,7 +221,14 @@ var rateLimiter = (options) => {
200
221
  getKey: store.get?.bind(store) ?? (() => void 0),
201
222
  resetKey: store.resetKey.bind(store)
202
223
  });
203
- setHeaders(c, info, opts.headers, opts.windowMs);
224
+ setHeaders(
225
+ c,
226
+ info,
227
+ opts.headers,
228
+ opts.windowMs,
229
+ opts.identifier,
230
+ opts.quotaUnit
231
+ );
204
232
  if (!allowed) {
205
233
  if (opts.onRateLimited) {
206
234
  await opts.onRateLimited(c, info);
@@ -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 * Reset all keys.\n */\n resetAll?: () => void | Promise<void>;\n\n /**\n * Get current state for key.\n */\n get?: (\n key: string,\n ) => StoreResult | Promise<StoreResult | undefined> | undefined;\n\n /**\n * Graceful shutdown.\n */\n shutdown?: () => void | Promise<void>;\n};\n\n/**\n * Store access interface exposed in context\n */\nexport type RateLimitStoreAccess = {\n /** Get rate limit info for a key */\n getKey: (\n key: string,\n ) => StoreResult | Promise<StoreResult | undefined> | undefined;\n /** Reset rate limit for a key */\n resetKey: (key: string) => void | Promise<void>;\n};\n\n/**\n * Options for rate limit middleware\n */\nexport type RateLimitOptions<E extends Env = Env> = {\n /**\n * Maximum requests allowed in the time window.\n * @default 60\n */\n limit?: number | ((c: Context<E>) => number | Promise<number>);\n\n /**\n * Time window in milliseconds.\n * @default 60000 (1 minute)\n */\n windowMs?: number;\n\n /**\n * Rate limiting algorithm.\n * @default 'sliding-window'\n */\n algorithm?: Algorithm;\n\n /**\n * Storage backend for rate limit state.\n * @default MemoryStore\n */\n store?: RateLimitStore;\n\n /**\n * Generate unique key for each client.\n * @default IP address from headers\n */\n keyGenerator?: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n */\n handler?: (\n c: Context<E>,\n info: RateLimitInfo,\n ) => Response | Promise<Response>;\n\n /**\n * HTTP header format to use.\n * @default 'draft-6'\n */\n headers?: HeadersFormat;\n\n /**\n * Skip rate limiting for certain requests.\n */\n skip?: (c: Context<E>) => boolean | Promise<boolean>;\n\n /**\n * Don't count successful (2xx) requests against limit.\n * @default false\n */\n skipSuccessfulRequests?: boolean;\n\n /**\n * Don't count failed (4xx, 5xx) requests against limit.\n * @default false\n */\n skipFailedRequests?: boolean;\n\n /**\n * Callback when a request is rate limited.\n */\n onRateLimited?: (c: Context<E>, info: RateLimitInfo) => void | Promise<void>;\n};\n\n/**\n * Cloudflare Rate Limiting binding interface\n */\nexport type RateLimitBinding = {\n limit: (options: { key: string }) => Promise<{ success: boolean }>;\n};\n\n/**\n * Options for Cloudflare Rate Limiting binding\n */\nexport type CloudflareRateLimitOptions<E extends Env = Env> = {\n /**\n * Cloudflare Rate Limiting binding from env\n */\n binding: RateLimitBinding | ((c: Context<E>) => RateLimitBinding);\n\n /**\n * Generate unique key for each client.\n */\n keyGenerator: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n */\n handler?: (c: Context<E>) => Response | Promise<Response>;\n\n /**\n * Skip rate limiting for certain requests.\n */\n skip?: (c: Context<E>) => boolean | Promise<boolean>;\n};\n\n// ============================================================================\n// Context Variable Type Extension\n// ============================================================================\n\ndeclare module \"hono\" {\n interface ContextVariableMap {\n rateLimit?: RateLimitInfo;\n rateLimitStore?: RateLimitStoreAccess;\n }\n}\n\n// ============================================================================\n// Memory Store\n// ============================================================================\n\ntype MemoryEntry = {\n count: number;\n reset: number;\n};\n\n/**\n * In-memory store for rate limiting.\n * Suitable for single-instance deployments.\n */\nexport class MemoryStore implements RateLimitStore {\n private entries = new Map<string, MemoryEntry>();\n private windowMs = 60_000;\n private cleanupTimer?: ReturnType<typeof setInterval>;\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n\n // Cleanup expired entries every minute\n this.cleanupTimer = setInterval(() => {\n const now = Date.now();\n for (const [key, entry] of this.entries) {\n if (entry.reset <= now) {\n this.entries.delete(key);\n }\n }\n }, 60_000);\n\n // Don't keep process alive for cleanup\n if (typeof this.cleanupTimer.unref === \"function\") {\n this.cleanupTimer.unref();\n }\n }\n\n increment(key: string): StoreResult {\n const now = Date.now();\n const existing = this.entries.get(key);\n\n if (!existing || existing.reset <= now) {\n // New window\n const reset = now + this.windowMs;\n this.entries.set(key, { count: 1, reset });\n return { count: 1, reset };\n }\n\n // Increment existing\n existing.count++;\n return { count: existing.count, reset: existing.reset };\n }\n\n get(key: string): StoreResult | undefined {\n const entry = this.entries.get(key);\n if (!entry || entry.reset <= Date.now()) {\n return undefined;\n }\n return { count: entry.count, reset: entry.reset };\n }\n\n decrement(key: string): void {\n const entry = this.entries.get(key);\n if (entry && entry.count > 0) {\n entry.count--;\n }\n }\n\n resetKey(key: string): void {\n this.entries.delete(key);\n }\n\n resetAll(): void {\n this.entries.clear();\n }\n\n shutdown(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n }\n this.entries.clear();\n }\n}\n\n// Singleton default store\nlet defaultStore: MemoryStore | undefined;\n\n// ============================================================================\n// Header Generation\n// ============================================================================\n\nfunction setHeaders(\n c: Context,\n info: RateLimitInfo,\n format: HeadersFormat,\n windowMs: number,\n): void {\n if (format === false) {\n return;\n }\n\n const windowSeconds = Math.ceil(windowMs / 1000);\n const resetSeconds = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n // RateLimit-Policy header (IETF compliant)\n c.header(\"RateLimit-Policy\", `${info.limit};w=${windowSeconds}`);\n\n switch (format) {\n case \"draft-7\":\n c.header(\n \"RateLimit\",\n `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`,\n );\n break;\n\n case \"draft-6\":\n default:\n c.header(\"X-RateLimit-Limit\", String(info.limit));\n c.header(\"X-RateLimit-Remaining\", String(info.remaining));\n c.header(\"X-RateLimit-Reset\", String(Math.ceil(info.reset / 1000)));\n break;\n }\n}\n\n// ============================================================================\n// Default Key Generator\n// ============================================================================\n\nfunction getClientIP(c: Context): string {\n // Platform-specific headers (most reliable)\n const cfIP = c.req.header(\"cf-connecting-ip\");\n if (cfIP) {\n return cfIP;\n }\n\n const xRealIP = c.req.header(\"x-real-ip\");\n if (xRealIP) {\n return xRealIP;\n }\n\n // X-Forwarded-For - take first IP\n const xff = c.req.header(\"x-forwarded-for\");\n if (xff) {\n return xff.split(\",\")[0].trim();\n }\n\n return \"unknown\";\n}\n\n// ============================================================================\n// Default Handler\n// ============================================================================\n\nfunction createDefaultResponse(info: RateLimitInfo): Response {\n const retryAfter = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n return new Response(\"Rate limit exceeded\", {\n status: 429,\n headers: {\n \"Content-Type\": \"text/plain\",\n \"Retry-After\": String(retryAfter),\n },\n });\n}\n\n// ============================================================================\n// Sliding Window Algorithm\n// ============================================================================\n\nasync function checkSlidingWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const currentWindowStart = Math.floor(now / windowMs) * windowMs;\n const previousWindowStart = currentWindowStart - windowMs;\n\n const previousKey = `${key}:${previousWindowStart}`;\n const currentKey = `${key}:${currentWindowStart}`;\n\n // Increment current window\n const current = await store.increment(currentKey);\n\n // Get previous window (may not exist)\n let previousCount = 0;\n if (store.get) {\n const prev = await store.get(previousKey);\n previousCount = prev?.count ?? 0;\n }\n\n // Cloudflare's weighted formula\n const elapsedMs = now - currentWindowStart;\n const weight = (windowMs - elapsedMs) / windowMs;\n const estimatedCount = Math.floor(previousCount * weight) + current.count;\n\n const remaining = Math.max(0, limit - estimatedCount);\n const allowed = estimatedCount <= limit;\n const reset = currentWindowStart + windowMs;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Fixed Window Algorithm\n// ============================================================================\n\nasync function checkFixedWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const windowStart = Math.floor(now / windowMs) * windowMs;\n const windowKey = `${key}:${windowStart}`;\n\n const { count, reset } = await store.increment(windowKey);\n\n const remaining = Math.max(0, limit - count);\n const allowed = count <= limit;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Main Middleware\n// ============================================================================\n\n/**\n * Rate Limit Middleware for Hono.\n *\n * @param {RateLimitOptions} [options] - Configuration options\n * @returns {MiddlewareHandler} Middleware handler\n *\n * @example\n * ```ts\n * import { Hono } from 'hono'\n * import { rateLimiter } from '@jellyfungus/hono-rate-limiter'\n *\n * const app = new Hono()\n *\n * // Basic usage - 60 requests per minute\n * app.use(rateLimiter())\n *\n * // Custom configuration\n * app.use('/api/*', rateLimiter({\n * limit: 100,\n * windowMs: 60 * 1000,\n * }))\n * ```\n */\nexport const rateLimiter = <E extends Env = Env>(\n options?: RateLimitOptions<E>,\n): MiddlewareHandler<E> => {\n // Merge with defaults\n const opts = {\n limit: 60 as number | ((c: Context<E>) => number | Promise<number>),\n windowMs: 60_000,\n algorithm: \"sliding-window\" as Algorithm,\n store: undefined as RateLimitStore | undefined,\n keyGenerator: getClientIP as (c: Context<E>) => string | Promise<string>,\n handler: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => Response | Promise<Response>)\n | undefined,\n headers: \"draft-6\" as HeadersFormat,\n skip: undefined as\n | ((c: Context<E>) => boolean | Promise<boolean>)\n | undefined,\n skipSuccessfulRequests: false,\n skipFailedRequests: false,\n onRateLimited: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => void | Promise<void>)\n | undefined,\n ...options,\n };\n\n // Use default store if none provided\n const store = opts.store ?? (defaultStore ??= new MemoryStore());\n\n // Track initialization\n let initialized = false;\n\n return async function rateLimiter(c, next) {\n // Initialize store on first request\n if (!initialized && store.init) {\n await store.init(opts.windowMs);\n initialized = true;\n }\n\n // Check if should skip\n if (opts.skip) {\n const shouldSkip = await opts.skip(c);\n if (shouldSkip) {\n return next();\n }\n }\n\n // Generate key\n const key = await opts.keyGenerator(c);\n\n // Get limit (may be dynamic)\n const limit =\n typeof opts.limit === \"function\" ? await opts.limit(c) : opts.limit;\n\n // Check rate limit\n const { allowed, info } =\n opts.algorithm === \"sliding-window\"\n ? await checkSlidingWindow(store, key, limit, opts.windowMs)\n : await checkFixedWindow(store, key, limit, opts.windowMs);\n\n // Set context variable for downstream middleware\n c.set(\"rateLimit\", info);\n\n // Expose store access in context\n c.set(\"rateLimitStore\", {\n getKey: store.get?.bind(store) ?? (() => undefined),\n resetKey: store.resetKey.bind(store),\n });\n\n // Set headers\n setHeaders(c, info, opts.headers, opts.windowMs);\n\n // Handle rate limited\n if (!allowed) {\n // Fire callback\n if (opts.onRateLimited) {\n await opts.onRateLimited(c, info);\n }\n\n // Custom handler or default\n if (opts.handler) {\n return opts.handler(c, info);\n }\n return createDefaultResponse(info);\n }\n\n // Continue\n await next();\n\n // Handle skip options after response\n if (opts.skipSuccessfulRequests || opts.skipFailedRequests) {\n const status = c.res.status;\n const shouldDecrement =\n (opts.skipSuccessfulRequests && status >= 200 && status < 300) ||\n (opts.skipFailedRequests && status >= 400);\n\n if (shouldDecrement && store.decrement) {\n const windowStart =\n Math.floor(Date.now() / opts.windowMs) * opts.windowMs;\n const windowKey = `${key}:${windowStart}`;\n await store.decrement(windowKey);\n }\n }\n };\n};\n\n// ============================================================================\n// Cloudflare Rate Limiting Binding Middleware\n// ============================================================================\n\n/**\n * Rate limiter using Cloudflare's built-in Rate Limiting binding.\n *\n * This uses Cloudflare's globally distributed rate limiting infrastructure,\n * which is ideal for high-traffic applications.\n *\n * @example\n * ```ts\n * import { cloudflareRateLimiter } from '@jellyfungus/hono-rate-limiter'\n *\n * type Bindings = { RATE_LIMITER: RateLimitBinding }\n *\n * const app = new Hono<{ Bindings: Bindings }>()\n *\n * app.use(cloudflareRateLimiter({\n * binding: (c) => c.env.RATE_LIMITER,\n * keyGenerator: (c) => c.req.header('cf-connecting-ip') ?? 'unknown',\n * }))\n * ```\n */\nexport const cloudflareRateLimiter = <E extends Env = Env>(\n options: CloudflareRateLimitOptions<E>,\n): MiddlewareHandler<E> => {\n const { binding, keyGenerator, handler, skip } = options;\n\n return async function cloudflareRateLimiter(c, next) {\n // Check if should skip\n if (skip) {\n const shouldSkip = await skip(c);\n if (shouldSkip) {\n return next();\n }\n }\n\n // Get binding (may be dynamic)\n const rateLimitBinding =\n typeof binding === \"function\" ? binding(c) : binding;\n\n // Generate key\n const key = await keyGenerator(c);\n\n // Check rate limit\n const { success } = await rateLimitBinding.limit({ key });\n\n if (!success) {\n if (handler) {\n return handler(c);\n }\n return new Response(\"Rate limit exceeded\", {\n status: 429,\n headers: { \"Content-Type\": \"text/plain\" },\n });\n }\n\n return next();\n };\n};\n\n// ============================================================================\n// Exports\n// ============================================================================\n\nexport { getClientIP };\n"],"mappings":"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","names":["rateLimiter","cloudflareRateLimiter"]}
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"]}
package/dist/index.d.cts CHANGED
@@ -26,9 +26,42 @@ type StoreResult = {
26
26
  reset: number;
27
27
  };
28
28
  /**
29
- * Header format versions
29
+ * Quota unit for IETF standard headers.
30
+ * @see https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/
30
31
  */
31
- type HeadersFormat = "draft-6" | "draft-7" | false;
32
+ type QuotaUnit = "requests" | "content-bytes" | "concurrent-requests";
33
+ /**
34
+ * Header format options.
35
+ *
36
+ * ## "legacy" (default)
37
+ * Common X-RateLimit-* headers used by GitHub, Twitter, and most APIs:
38
+ * - `X-RateLimit-Limit`: max requests in window
39
+ * - `X-RateLimit-Remaining`: remaining requests
40
+ * - `X-RateLimit-Reset`: Unix timestamp (seconds) when window resets
41
+ *
42
+ * ## "draft-6"
43
+ * IETF draft-06 format with individual RateLimit-* headers:
44
+ * - `RateLimit-Policy`: policy description (e.g., `100;w=60`)
45
+ * - `RateLimit-Limit`: max requests
46
+ * - `RateLimit-Remaining`: remaining requests
47
+ * - `RateLimit-Reset`: seconds until reset
48
+ *
49
+ * ## "draft-7"
50
+ * IETF draft-07 format with combined RateLimit header:
51
+ * - `RateLimit-Policy`: policy description
52
+ * - `RateLimit`: combined (e.g., `limit=100, remaining=50, reset=30`)
53
+ *
54
+ * ## "standard"
55
+ * Current IETF draft-08+ format with structured field values (RFC 9651):
56
+ * - `RateLimit-Policy`: `"name";q=100;w=60`
57
+ * - `RateLimit`: `"name";r=50;t=30`
58
+ *
59
+ * ## false
60
+ * Disable all rate limit headers.
61
+ *
62
+ * @see https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/
63
+ */
64
+ type HeadersFormat = "legacy" | "draft-6" | "draft-7" | "standard" | false;
32
65
  /**
33
66
  * Rate limit algorithm
34
67
  */
@@ -110,9 +143,28 @@ type RateLimitOptions<E extends Env = Env> = {
110
143
  handler?: (c: Context<E>, info: RateLimitInfo) => Response | Promise<Response>;
111
144
  /**
112
145
  * HTTP header format to use.
113
- * @default 'draft-6'
146
+ *
147
+ * - "legacy": X-RateLimit-* headers (GitHub/Twitter style, default)
148
+ * - "draft-6": IETF draft-06 individual headers
149
+ * - "draft-7": IETF draft-07 combined header
150
+ * - "standard": IETF draft-08+ structured fields (current spec)
151
+ * - false: Disable headers
152
+ *
153
+ * @default 'legacy'
114
154
  */
115
155
  headers?: HeadersFormat;
156
+ /**
157
+ * Policy identifier for IETF headers (draft-6+).
158
+ * Used in RateLimit and RateLimit-Policy headers.
159
+ * @default 'default'
160
+ */
161
+ identifier?: string;
162
+ /**
163
+ * Quota unit for IETF standard headers.
164
+ * Only included in "standard" format when not "requests".
165
+ * @default 'requests'
166
+ */
167
+ quotaUnit?: QuotaUnit;
116
168
  /**
117
169
  * Skip rate limiting for certain requests.
118
170
  */
@@ -232,4 +284,4 @@ declare const rateLimiter: <E extends Env = Env>(options?: RateLimitOptions<E>)
232
284
  */
233
285
  declare const cloudflareRateLimiter: <E extends Env = Env>(options: CloudflareRateLimitOptions<E>) => MiddlewareHandler<E>;
234
286
 
235
- export { type Algorithm, type CloudflareRateLimitOptions, type HeadersFormat, MemoryStore, type RateLimitBinding, type RateLimitInfo, type RateLimitOptions, type RateLimitStore, type RateLimitStoreAccess, type StoreResult, cloudflareRateLimiter, getClientIP, rateLimiter };
287
+ export { type Algorithm, type CloudflareRateLimitOptions, type HeadersFormat, MemoryStore, type QuotaUnit, type RateLimitBinding, type RateLimitInfo, type RateLimitOptions, type RateLimitStore, type RateLimitStoreAccess, type StoreResult, cloudflareRateLimiter, getClientIP, rateLimiter };
package/dist/index.d.ts CHANGED
@@ -26,9 +26,42 @@ type StoreResult = {
26
26
  reset: number;
27
27
  };
28
28
  /**
29
- * Header format versions
29
+ * Quota unit for IETF standard headers.
30
+ * @see https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/
30
31
  */
31
- type HeadersFormat = "draft-6" | "draft-7" | false;
32
+ type QuotaUnit = "requests" | "content-bytes" | "concurrent-requests";
33
+ /**
34
+ * Header format options.
35
+ *
36
+ * ## "legacy" (default)
37
+ * Common X-RateLimit-* headers used by GitHub, Twitter, and most APIs:
38
+ * - `X-RateLimit-Limit`: max requests in window
39
+ * - `X-RateLimit-Remaining`: remaining requests
40
+ * - `X-RateLimit-Reset`: Unix timestamp (seconds) when window resets
41
+ *
42
+ * ## "draft-6"
43
+ * IETF draft-06 format with individual RateLimit-* headers:
44
+ * - `RateLimit-Policy`: policy description (e.g., `100;w=60`)
45
+ * - `RateLimit-Limit`: max requests
46
+ * - `RateLimit-Remaining`: remaining requests
47
+ * - `RateLimit-Reset`: seconds until reset
48
+ *
49
+ * ## "draft-7"
50
+ * IETF draft-07 format with combined RateLimit header:
51
+ * - `RateLimit-Policy`: policy description
52
+ * - `RateLimit`: combined (e.g., `limit=100, remaining=50, reset=30`)
53
+ *
54
+ * ## "standard"
55
+ * Current IETF draft-08+ format with structured field values (RFC 9651):
56
+ * - `RateLimit-Policy`: `"name";q=100;w=60`
57
+ * - `RateLimit`: `"name";r=50;t=30`
58
+ *
59
+ * ## false
60
+ * Disable all rate limit headers.
61
+ *
62
+ * @see https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/
63
+ */
64
+ type HeadersFormat = "legacy" | "draft-6" | "draft-7" | "standard" | false;
32
65
  /**
33
66
  * Rate limit algorithm
34
67
  */
@@ -110,9 +143,28 @@ type RateLimitOptions<E extends Env = Env> = {
110
143
  handler?: (c: Context<E>, info: RateLimitInfo) => Response | Promise<Response>;
111
144
  /**
112
145
  * HTTP header format to use.
113
- * @default 'draft-6'
146
+ *
147
+ * - "legacy": X-RateLimit-* headers (GitHub/Twitter style, default)
148
+ * - "draft-6": IETF draft-06 individual headers
149
+ * - "draft-7": IETF draft-07 combined header
150
+ * - "standard": IETF draft-08+ structured fields (current spec)
151
+ * - false: Disable headers
152
+ *
153
+ * @default 'legacy'
114
154
  */
115
155
  headers?: HeadersFormat;
156
+ /**
157
+ * Policy identifier for IETF headers (draft-6+).
158
+ * Used in RateLimit and RateLimit-Policy headers.
159
+ * @default 'default'
160
+ */
161
+ identifier?: string;
162
+ /**
163
+ * Quota unit for IETF standard headers.
164
+ * Only included in "standard" format when not "requests".
165
+ * @default 'requests'
166
+ */
167
+ quotaUnit?: QuotaUnit;
116
168
  /**
117
169
  * Skip rate limiting for certain requests.
118
170
  */
@@ -232,4 +284,4 @@ declare const rateLimiter: <E extends Env = Env>(options?: RateLimitOptions<E>)
232
284
  */
233
285
  declare const cloudflareRateLimiter: <E extends Env = Env>(options: CloudflareRateLimitOptions<E>) => MiddlewareHandler<E>;
234
286
 
235
- export { type Algorithm, type CloudflareRateLimitOptions, type HeadersFormat, MemoryStore, type RateLimitBinding, type RateLimitInfo, type RateLimitOptions, type RateLimitStore, type RateLimitStoreAccess, type StoreResult, cloudflareRateLimiter, getClientIP, rateLimiter };
287
+ export { type Algorithm, type CloudflareRateLimitOptions, type HeadersFormat, MemoryStore, type QuotaUnit, type RateLimitBinding, type RateLimitInfo, type RateLimitOptions, type RateLimitStore, type RateLimitStoreAccess, type StoreResult, cloudflareRateLimiter, getClientIP, rateLimiter };
package/dist/index.js CHANGED
@@ -55,21 +55,40 @@ var MemoryStore = class {
55
55
  }
56
56
  };
57
57
  var defaultStore;
58
- function setHeaders(c, info, format, windowMs) {
58
+ function setHeaders(c, info, format, windowMs, identifier, quotaUnit) {
59
59
  if (format === false) {
60
60
  return;
61
61
  }
62
62
  const windowSeconds = Math.ceil(windowMs / 1e3);
63
63
  const resetSeconds = Math.max(0, Math.ceil((info.reset - Date.now()) / 1e3));
64
- c.header("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
65
64
  switch (format) {
65
+ case "standard":
66
+ {
67
+ let policy = `"${identifier}";q=${info.limit};w=${windowSeconds}`;
68
+ if (quotaUnit !== "requests") {
69
+ policy += `;qu="${quotaUnit}"`;
70
+ }
71
+ c.header("RateLimit-Policy", policy);
72
+ c.header(
73
+ "RateLimit",
74
+ `"${identifier}";r=${info.remaining};t=${resetSeconds}`
75
+ );
76
+ }
77
+ break;
66
78
  case "draft-7":
79
+ c.header("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
67
80
  c.header(
68
81
  "RateLimit",
69
82
  `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
70
83
  );
71
84
  break;
72
85
  case "draft-6":
86
+ c.header("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
87
+ c.header("RateLimit-Limit", String(info.limit));
88
+ c.header("RateLimit-Remaining", String(info.remaining));
89
+ c.header("RateLimit-Reset", String(resetSeconds));
90
+ break;
91
+ case "legacy":
73
92
  default:
74
93
  c.header("X-RateLimit-Limit", String(info.limit));
75
94
  c.header("X-RateLimit-Remaining", String(info.remaining));
@@ -145,7 +164,9 @@ var rateLimiter = (options) => {
145
164
  store: void 0,
146
165
  keyGenerator: getClientIP,
147
166
  handler: void 0,
148
- headers: "draft-6",
167
+ headers: "legacy",
168
+ identifier: "default",
169
+ quotaUnit: "requests",
149
170
  skip: void 0,
150
171
  skipSuccessfulRequests: false,
151
172
  skipFailedRequests: false,
@@ -173,7 +194,14 @@ var rateLimiter = (options) => {
173
194
  getKey: store.get?.bind(store) ?? (() => void 0),
174
195
  resetKey: store.resetKey.bind(store)
175
196
  });
176
- setHeaders(c, info, opts.headers, opts.windowMs);
197
+ setHeaders(
198
+ c,
199
+ info,
200
+ opts.headers,
201
+ opts.windowMs,
202
+ opts.identifier,
203
+ opts.quotaUnit
204
+ );
177
205
  if (!allowed) {
178
206
  if (opts.onRateLimited) {
179
207
  await opts.onRateLimited(c, info);
package/dist/index.js.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 * Reset all keys.\n */\n resetAll?: () => void | Promise<void>;\n\n /**\n * Get current state for key.\n */\n get?: (\n key: string,\n ) => StoreResult | Promise<StoreResult | undefined> | undefined;\n\n /**\n * Graceful shutdown.\n */\n shutdown?: () => void | Promise<void>;\n};\n\n/**\n * Store access interface exposed in context\n */\nexport type RateLimitStoreAccess = {\n /** Get rate limit info for a key */\n getKey: (\n key: string,\n ) => StoreResult | Promise<StoreResult | undefined> | undefined;\n /** Reset rate limit for a key */\n resetKey: (key: string) => void | Promise<void>;\n};\n\n/**\n * Options for rate limit middleware\n */\nexport type RateLimitOptions<E extends Env = Env> = {\n /**\n * Maximum requests allowed in the time window.\n * @default 60\n */\n limit?: number | ((c: Context<E>) => number | Promise<number>);\n\n /**\n * Time window in milliseconds.\n * @default 60000 (1 minute)\n */\n windowMs?: number;\n\n /**\n * Rate limiting algorithm.\n * @default 'sliding-window'\n */\n algorithm?: Algorithm;\n\n /**\n * Storage backend for rate limit state.\n * @default MemoryStore\n */\n store?: RateLimitStore;\n\n /**\n * Generate unique key for each client.\n * @default IP address from headers\n */\n keyGenerator?: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n */\n handler?: (\n c: Context<E>,\n info: RateLimitInfo,\n ) => Response | Promise<Response>;\n\n /**\n * HTTP header format to use.\n * @default 'draft-6'\n */\n headers?: HeadersFormat;\n\n /**\n * Skip rate limiting for certain requests.\n */\n skip?: (c: Context<E>) => boolean | Promise<boolean>;\n\n /**\n * Don't count successful (2xx) requests against limit.\n * @default false\n */\n skipSuccessfulRequests?: boolean;\n\n /**\n * Don't count failed (4xx, 5xx) requests against limit.\n * @default false\n */\n skipFailedRequests?: boolean;\n\n /**\n * Callback when a request is rate limited.\n */\n onRateLimited?: (c: Context<E>, info: RateLimitInfo) => void | Promise<void>;\n};\n\n/**\n * Cloudflare Rate Limiting binding interface\n */\nexport type RateLimitBinding = {\n limit: (options: { key: string }) => Promise<{ success: boolean }>;\n};\n\n/**\n * Options for Cloudflare Rate Limiting binding\n */\nexport type CloudflareRateLimitOptions<E extends Env = Env> = {\n /**\n * Cloudflare Rate Limiting binding from env\n */\n binding: RateLimitBinding | ((c: Context<E>) => RateLimitBinding);\n\n /**\n * Generate unique key for each client.\n */\n keyGenerator: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n */\n handler?: (c: Context<E>) => Response | Promise<Response>;\n\n /**\n * Skip rate limiting for certain requests.\n */\n skip?: (c: Context<E>) => boolean | Promise<boolean>;\n};\n\n// ============================================================================\n// Context Variable Type Extension\n// ============================================================================\n\ndeclare module \"hono\" {\n interface ContextVariableMap {\n rateLimit?: RateLimitInfo;\n rateLimitStore?: RateLimitStoreAccess;\n }\n}\n\n// ============================================================================\n// Memory Store\n// ============================================================================\n\ntype MemoryEntry = {\n count: number;\n reset: number;\n};\n\n/**\n * In-memory store for rate limiting.\n * Suitable for single-instance deployments.\n */\nexport class MemoryStore implements RateLimitStore {\n private entries = new Map<string, MemoryEntry>();\n private windowMs = 60_000;\n private cleanupTimer?: ReturnType<typeof setInterval>;\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n\n // Cleanup expired entries every minute\n this.cleanupTimer = setInterval(() => {\n const now = Date.now();\n for (const [key, entry] of this.entries) {\n if (entry.reset <= now) {\n this.entries.delete(key);\n }\n }\n }, 60_000);\n\n // Don't keep process alive for cleanup\n if (typeof this.cleanupTimer.unref === \"function\") {\n this.cleanupTimer.unref();\n }\n }\n\n increment(key: string): StoreResult {\n const now = Date.now();\n const existing = this.entries.get(key);\n\n if (!existing || existing.reset <= now) {\n // New window\n const reset = now + this.windowMs;\n this.entries.set(key, { count: 1, reset });\n return { count: 1, reset };\n }\n\n // Increment existing\n existing.count++;\n return { count: existing.count, reset: existing.reset };\n }\n\n get(key: string): StoreResult | undefined {\n const entry = this.entries.get(key);\n if (!entry || entry.reset <= Date.now()) {\n return undefined;\n }\n return { count: entry.count, reset: entry.reset };\n }\n\n decrement(key: string): void {\n const entry = this.entries.get(key);\n if (entry && entry.count > 0) {\n entry.count--;\n }\n }\n\n resetKey(key: string): void {\n this.entries.delete(key);\n }\n\n resetAll(): void {\n this.entries.clear();\n }\n\n shutdown(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n }\n this.entries.clear();\n }\n}\n\n// Singleton default store\nlet defaultStore: MemoryStore | undefined;\n\n// ============================================================================\n// Header Generation\n// ============================================================================\n\nfunction setHeaders(\n c: Context,\n info: RateLimitInfo,\n format: HeadersFormat,\n windowMs: number,\n): void {\n if (format === false) {\n return;\n }\n\n const windowSeconds = Math.ceil(windowMs / 1000);\n const resetSeconds = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n // RateLimit-Policy header (IETF compliant)\n c.header(\"RateLimit-Policy\", `${info.limit};w=${windowSeconds}`);\n\n switch (format) {\n case \"draft-7\":\n c.header(\n \"RateLimit\",\n `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`,\n );\n break;\n\n case \"draft-6\":\n default:\n c.header(\"X-RateLimit-Limit\", String(info.limit));\n c.header(\"X-RateLimit-Remaining\", String(info.remaining));\n c.header(\"X-RateLimit-Reset\", String(Math.ceil(info.reset / 1000)));\n break;\n }\n}\n\n// ============================================================================\n// Default Key Generator\n// ============================================================================\n\nfunction getClientIP(c: Context): string {\n // Platform-specific headers (most reliable)\n const cfIP = c.req.header(\"cf-connecting-ip\");\n if (cfIP) {\n return cfIP;\n }\n\n const xRealIP = c.req.header(\"x-real-ip\");\n if (xRealIP) {\n return xRealIP;\n }\n\n // X-Forwarded-For - take first IP\n const xff = c.req.header(\"x-forwarded-for\");\n if (xff) {\n return xff.split(\",\")[0].trim();\n }\n\n return \"unknown\";\n}\n\n// ============================================================================\n// Default Handler\n// ============================================================================\n\nfunction createDefaultResponse(info: RateLimitInfo): Response {\n const retryAfter = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n return new Response(\"Rate limit exceeded\", {\n status: 429,\n headers: {\n \"Content-Type\": \"text/plain\",\n \"Retry-After\": String(retryAfter),\n },\n });\n}\n\n// ============================================================================\n// Sliding Window Algorithm\n// ============================================================================\n\nasync function checkSlidingWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const currentWindowStart = Math.floor(now / windowMs) * windowMs;\n const previousWindowStart = currentWindowStart - windowMs;\n\n const previousKey = `${key}:${previousWindowStart}`;\n const currentKey = `${key}:${currentWindowStart}`;\n\n // Increment current window\n const current = await store.increment(currentKey);\n\n // Get previous window (may not exist)\n let previousCount = 0;\n if (store.get) {\n const prev = await store.get(previousKey);\n previousCount = prev?.count ?? 0;\n }\n\n // Cloudflare's weighted formula\n const elapsedMs = now - currentWindowStart;\n const weight = (windowMs - elapsedMs) / windowMs;\n const estimatedCount = Math.floor(previousCount * weight) + current.count;\n\n const remaining = Math.max(0, limit - estimatedCount);\n const allowed = estimatedCount <= limit;\n const reset = currentWindowStart + windowMs;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Fixed Window Algorithm\n// ============================================================================\n\nasync function checkFixedWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const windowStart = Math.floor(now / windowMs) * windowMs;\n const windowKey = `${key}:${windowStart}`;\n\n const { count, reset } = await store.increment(windowKey);\n\n const remaining = Math.max(0, limit - count);\n const allowed = count <= limit;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Main Middleware\n// ============================================================================\n\n/**\n * Rate Limit Middleware for Hono.\n *\n * @param {RateLimitOptions} [options] - Configuration options\n * @returns {MiddlewareHandler} Middleware handler\n *\n * @example\n * ```ts\n * import { Hono } from 'hono'\n * import { rateLimiter } from '@jellyfungus/hono-rate-limiter'\n *\n * const app = new Hono()\n *\n * // Basic usage - 60 requests per minute\n * app.use(rateLimiter())\n *\n * // Custom configuration\n * app.use('/api/*', rateLimiter({\n * limit: 100,\n * windowMs: 60 * 1000,\n * }))\n * ```\n */\nexport const rateLimiter = <E extends Env = Env>(\n options?: RateLimitOptions<E>,\n): MiddlewareHandler<E> => {\n // Merge with defaults\n const opts = {\n limit: 60 as number | ((c: Context<E>) => number | Promise<number>),\n windowMs: 60_000,\n algorithm: \"sliding-window\" as Algorithm,\n store: undefined as RateLimitStore | undefined,\n keyGenerator: getClientIP as (c: Context<E>) => string | Promise<string>,\n handler: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => Response | Promise<Response>)\n | undefined,\n headers: \"draft-6\" as HeadersFormat,\n skip: undefined as\n | ((c: Context<E>) => boolean | Promise<boolean>)\n | undefined,\n skipSuccessfulRequests: false,\n skipFailedRequests: false,\n onRateLimited: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => void | Promise<void>)\n | undefined,\n ...options,\n };\n\n // Use default store if none provided\n const store = opts.store ?? (defaultStore ??= new MemoryStore());\n\n // Track initialization\n let initialized = false;\n\n return async function rateLimiter(c, next) {\n // Initialize store on first request\n if (!initialized && store.init) {\n await store.init(opts.windowMs);\n initialized = true;\n }\n\n // Check if should skip\n if (opts.skip) {\n const shouldSkip = await opts.skip(c);\n if (shouldSkip) {\n return next();\n }\n }\n\n // Generate key\n const key = await opts.keyGenerator(c);\n\n // Get limit (may be dynamic)\n const limit =\n typeof opts.limit === \"function\" ? await opts.limit(c) : opts.limit;\n\n // Check rate limit\n const { allowed, info } =\n opts.algorithm === \"sliding-window\"\n ? await checkSlidingWindow(store, key, limit, opts.windowMs)\n : await checkFixedWindow(store, key, limit, opts.windowMs);\n\n // Set context variable for downstream middleware\n c.set(\"rateLimit\", info);\n\n // Expose store access in context\n c.set(\"rateLimitStore\", {\n getKey: store.get?.bind(store) ?? (() => undefined),\n resetKey: store.resetKey.bind(store),\n });\n\n // Set headers\n setHeaders(c, info, opts.headers, opts.windowMs);\n\n // Handle rate limited\n if (!allowed) {\n // Fire callback\n if (opts.onRateLimited) {\n await opts.onRateLimited(c, info);\n }\n\n // Custom handler or default\n if (opts.handler) {\n return opts.handler(c, info);\n }\n return createDefaultResponse(info);\n }\n\n // Continue\n await next();\n\n // Handle skip options after response\n if (opts.skipSuccessfulRequests || opts.skipFailedRequests) {\n const status = c.res.status;\n const shouldDecrement =\n (opts.skipSuccessfulRequests && status >= 200 && status < 300) ||\n (opts.skipFailedRequests && status >= 400);\n\n if (shouldDecrement && store.decrement) {\n const windowStart =\n Math.floor(Date.now() / opts.windowMs) * opts.windowMs;\n const windowKey = `${key}:${windowStart}`;\n await store.decrement(windowKey);\n }\n }\n };\n};\n\n// ============================================================================\n// Cloudflare Rate Limiting Binding Middleware\n// ============================================================================\n\n/**\n * Rate limiter using Cloudflare's built-in Rate Limiting binding.\n *\n * This uses Cloudflare's globally distributed rate limiting infrastructure,\n * which is ideal for high-traffic applications.\n *\n * @example\n * ```ts\n * import { cloudflareRateLimiter } from '@jellyfungus/hono-rate-limiter'\n *\n * type Bindings = { RATE_LIMITER: RateLimitBinding }\n *\n * const app = new Hono<{ Bindings: Bindings }>()\n *\n * app.use(cloudflareRateLimiter({\n * binding: (c) => c.env.RATE_LIMITER,\n * keyGenerator: (c) => c.req.header('cf-connecting-ip') ?? 'unknown',\n * }))\n * ```\n */\nexport const cloudflareRateLimiter = <E extends Env = Env>(\n options: CloudflareRateLimitOptions<E>,\n): MiddlewareHandler<E> => {\n const { binding, keyGenerator, handler, skip } = options;\n\n return async function cloudflareRateLimiter(c, next) {\n // Check if should skip\n if (skip) {\n const shouldSkip = await skip(c);\n if (shouldSkip) {\n return next();\n }\n }\n\n // Get binding (may be dynamic)\n const rateLimitBinding =\n typeof binding === \"function\" ? binding(c) : binding;\n\n // Generate key\n const key = await keyGenerator(c);\n\n // Check rate limit\n const { success } = await rateLimitBinding.limit({ key });\n\n if (!success) {\n if (handler) {\n return handler(c);\n }\n return new Response(\"Rate limit exceeded\", {\n status: 429,\n headers: { \"Content-Type\": \"text/plain\" },\n });\n }\n\n return next();\n };\n};\n\n// ============================================================================\n// Exports\n// ============================================================================\n\nexport { getClientIP };\n"],"mappings":"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","names":["rateLimiter","cloudflareRateLimiter"]}
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"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/websocket.ts","../src/index.ts"],"sourcesContent":["/**\n * @module\n * WebSocket Rate Limiting for Hono.\n */\n\nimport type { Context, Env } from \"hono\";\nimport type { WSContext, WSEvents } from \"hono/ws\";\nimport {\n MemoryStore,\n type RateLimitStore,\n type RateLimitInfo,\n type StoreResult,\n type Algorithm,\n} from \"./index\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Options for WebSocket rate limiting\n */\nexport type WSRateLimitOptions<E extends Env = Env> = {\n /**\n * Maximum messages allowed in the time window.\n * @default 100\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 * Required for WebSocket rate limiting.\n */\n keyGenerator: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n * Default: closes connection with code 1008.\n */\n handler?: (ws: WSContext, info: RateLimitInfo) => void;\n\n /**\n * Skip rate limiting for certain messages.\n */\n skip?: (event: MessageEvent, ws: WSContext) => boolean | Promise<boolean>;\n};\n\n// ============================================================================\n// Sliding Window Algorithm (adapted for WS)\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 = `ws:${key}:${previousWindowStart}`;\n const currentKey = `ws:${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 (adapted for WS)\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 = `ws:${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// WebSocket Rate Limiter\n// ============================================================================\n\n// Singleton default store for WebSocket\nlet wsDefaultStore: MemoryStore | undefined;\n\n/**\n * WebSocket rate limiting middleware for Hono.\n *\n * Wraps your WebSocket event handlers to add rate limiting on messages.\n *\n * @example\n * ```ts\n * import { Hono } from 'hono'\n * import { createBunWebSocket } from 'hono/bun'\n * import { webSocketLimiter } from '@jellyfungus/hono-rate-limiter/websocket'\n *\n * const { upgradeWebSocket, websocket } = createBunWebSocket()\n *\n * const app = new Hono()\n *\n * const wsLimiter = webSocketLimiter({\n * limit: 100,\n * windowMs: 60_000,\n * keyGenerator: (c) => c.req.header('cf-connecting-ip') ?? 'unknown',\n * })\n *\n * app.get('/ws', upgradeWebSocket(wsLimiter((c) => ({\n * onMessage(event, ws) {\n * ws.send('Hello!')\n * },\n * }))))\n *\n * export default { port: 3000, fetch: app.fetch, websocket }\n * ```\n */\nexport function webSocketLimiter<E extends Env = Env>(\n options: WSRateLimitOptions<E>,\n): (\n createEvents: (c: Context<E>) => WSEvents | Promise<WSEvents>,\n) => (c: Context<E>) => Promise<WSEvents> {\n const {\n limit = 100,\n windowMs = 60_000,\n algorithm = \"sliding-window\",\n store = (wsDefaultStore ??= new MemoryStore()),\n keyGenerator,\n handler = (ws, info) => {\n const retryAfter = Math.ceil((info.reset - Date.now()) / 1000);\n ws.close(1008, `Rate limit exceeded. Retry after ${retryAfter}s`);\n },\n skip,\n } = options;\n\n // Track initialization\n let initialized = false;\n\n return (createEvents: (c: Context<E>) => WSEvents | Promise<WSEvents>) => {\n return async (c: Context<E>): Promise<WSEvents> => {\n // Initialize store on first use\n if (!initialized && store.init) {\n await store.init(windowMs);\n initialized = true;\n }\n\n // Get the key for this connection\n const key = await keyGenerator(c);\n\n // Get the limit (may be dynamic)\n const currentLimit = typeof limit === \"function\" ? await limit(c) : limit;\n\n // Get the original events\n const events = await createEvents(c);\n\n return {\n ...events,\n\n onMessage: async (event, ws) => {\n // Check if should skip\n if (skip) {\n const shouldSkip = await skip(event, ws);\n if (shouldSkip) {\n await events.onMessage?.(event, ws);\n return;\n }\n }\n\n // Check rate limit\n const { allowed, info } =\n algorithm === \"sliding-window\"\n ? await checkSlidingWindow(store, key, currentLimit, windowMs)\n : await checkFixedWindow(store, key, currentLimit, windowMs);\n\n if (!allowed) {\n handler(ws, info);\n return;\n }\n\n // Call original handler\n await events.onMessage?.(event, ws);\n },\n\n onOpen: async (event, ws) => {\n await events.onOpen?.(event, ws);\n },\n\n onClose: async (event, ws) => {\n await events.onClose?.(event, ws);\n },\n\n onError: async (event, ws) => {\n await events.onError?.(event, ws);\n },\n };\n };\n };\n}\n","/**\n * @module\n * Rate Limit Middleware for Hono.\n */\n\nimport type { Context, Env, MiddlewareHandler } from \"hono\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Rate limit information for a single request\n */\nexport type RateLimitInfo = {\n /** Maximum requests allowed in window */\n limit: number;\n /** Remaining requests in current window */\n remaining: number;\n /** Unix timestamp (ms) when window resets */\n reset: number;\n};\n\n/**\n * Result from store increment operation\n */\nexport type StoreResult = {\n /** Current request count in window */\n count: number;\n /** When the window resets (Unix timestamp ms) */\n reset: number;\n};\n\n/**\n * Header format versions\n */\nexport type HeadersFormat =\n | \"draft-6\" // X-RateLimit-* headers\n | \"draft-7\" // RateLimit-* without structured fields\n | false; // Disable headers\n\n/**\n * Rate limit algorithm\n */\nexport type Algorithm = \"fixed-window\" | \"sliding-window\";\n\n/**\n * Store interface for rate limit state\n */\nexport type RateLimitStore = {\n /**\n * Initialize store. Called once before first use.\n */\n init?: (windowMs: number) => void | Promise<void>;\n\n /**\n * Increment counter for key and return current state.\n */\n increment: (key: string) => StoreResult | Promise<StoreResult>;\n\n /**\n * Decrement counter for key.\n */\n decrement?: (key: string) => void | Promise<void>;\n\n /**\n * Reset a specific key.\n */\n resetKey: (key: string) => void | Promise<void>;\n\n /**\n * Reset all keys.\n */\n resetAll?: () => void | Promise<void>;\n\n /**\n * Get current state for key.\n */\n get?: (\n key: string,\n ) => StoreResult | Promise<StoreResult | undefined> | undefined;\n\n /**\n * Graceful shutdown.\n */\n shutdown?: () => void | Promise<void>;\n};\n\n/**\n * Store access interface exposed in context\n */\nexport type RateLimitStoreAccess = {\n /** Get rate limit info for a key */\n getKey: (\n key: string,\n ) => StoreResult | Promise<StoreResult | undefined> | undefined;\n /** Reset rate limit for a key */\n resetKey: (key: string) => void | Promise<void>;\n};\n\n/**\n * Options for rate limit middleware\n */\nexport type RateLimitOptions<E extends Env = Env> = {\n /**\n * Maximum requests allowed in the time window.\n * @default 60\n */\n limit?: number | ((c: Context<E>) => number | Promise<number>);\n\n /**\n * Time window in milliseconds.\n * @default 60000 (1 minute)\n */\n windowMs?: number;\n\n /**\n * Rate limiting algorithm.\n * @default 'sliding-window'\n */\n algorithm?: Algorithm;\n\n /**\n * Storage backend for rate limit state.\n * @default MemoryStore\n */\n store?: RateLimitStore;\n\n /**\n * Generate unique key for each client.\n * @default IP address from headers\n */\n keyGenerator?: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n */\n handler?: (\n c: Context<E>,\n info: RateLimitInfo,\n ) => Response | Promise<Response>;\n\n /**\n * HTTP header format to use.\n * @default 'draft-6'\n */\n headers?: HeadersFormat;\n\n /**\n * Skip rate limiting for certain requests.\n */\n skip?: (c: Context<E>) => boolean | Promise<boolean>;\n\n /**\n * Don't count successful (2xx) requests against limit.\n * @default false\n */\n skipSuccessfulRequests?: boolean;\n\n /**\n * Don't count failed (4xx, 5xx) requests against limit.\n * @default false\n */\n skipFailedRequests?: boolean;\n\n /**\n * Callback when a request is rate limited.\n */\n onRateLimited?: (c: Context<E>, info: RateLimitInfo) => void | Promise<void>;\n};\n\n/**\n * Cloudflare Rate Limiting binding interface\n */\nexport type RateLimitBinding = {\n limit: (options: { key: string }) => Promise<{ success: boolean }>;\n};\n\n/**\n * Options for Cloudflare Rate Limiting binding\n */\nexport type CloudflareRateLimitOptions<E extends Env = Env> = {\n /**\n * Cloudflare Rate Limiting binding from env\n */\n binding: RateLimitBinding | ((c: Context<E>) => RateLimitBinding);\n\n /**\n * Generate unique key for each client.\n */\n keyGenerator: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n */\n handler?: (c: Context<E>) => Response | Promise<Response>;\n\n /**\n * Skip rate limiting for certain requests.\n */\n skip?: (c: Context<E>) => boolean | Promise<boolean>;\n};\n\n// ============================================================================\n// Context Variable Type Extension\n// ============================================================================\n\ndeclare module \"hono\" {\n interface ContextVariableMap {\n rateLimit?: RateLimitInfo;\n rateLimitStore?: RateLimitStoreAccess;\n }\n}\n\n// ============================================================================\n// Memory Store\n// ============================================================================\n\ntype MemoryEntry = {\n count: number;\n reset: number;\n};\n\n/**\n * In-memory store for rate limiting.\n * Suitable for single-instance deployments.\n */\nexport class MemoryStore implements RateLimitStore {\n private entries = new Map<string, MemoryEntry>();\n private windowMs = 60_000;\n private cleanupTimer?: ReturnType<typeof setInterval>;\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n\n // Cleanup expired entries every minute\n this.cleanupTimer = setInterval(() => {\n const now = Date.now();\n for (const [key, entry] of this.entries) {\n if (entry.reset <= now) {\n this.entries.delete(key);\n }\n }\n }, 60_000);\n\n // Don't keep process alive for cleanup\n if (typeof this.cleanupTimer.unref === \"function\") {\n this.cleanupTimer.unref();\n }\n }\n\n increment(key: string): StoreResult {\n const now = Date.now();\n const existing = this.entries.get(key);\n\n if (!existing || existing.reset <= now) {\n // New window\n const reset = now + this.windowMs;\n this.entries.set(key, { count: 1, reset });\n return { count: 1, reset };\n }\n\n // Increment existing\n existing.count++;\n return { count: existing.count, reset: existing.reset };\n }\n\n get(key: string): StoreResult | undefined {\n const entry = this.entries.get(key);\n if (!entry || entry.reset <= Date.now()) {\n return undefined;\n }\n return { count: entry.count, reset: entry.reset };\n }\n\n decrement(key: string): void {\n const entry = this.entries.get(key);\n if (entry && entry.count > 0) {\n entry.count--;\n }\n }\n\n resetKey(key: string): void {\n this.entries.delete(key);\n }\n\n resetAll(): void {\n this.entries.clear();\n }\n\n shutdown(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n }\n this.entries.clear();\n }\n}\n\n// Singleton default store\nlet defaultStore: MemoryStore | undefined;\n\n// ============================================================================\n// Header Generation\n// ============================================================================\n\nfunction setHeaders(\n c: Context,\n info: RateLimitInfo,\n format: HeadersFormat,\n windowMs: number,\n): void {\n if (format === false) {\n return;\n }\n\n const windowSeconds = Math.ceil(windowMs / 1000);\n const resetSeconds = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n // RateLimit-Policy header (IETF compliant)\n c.header(\"RateLimit-Policy\", `${info.limit};w=${windowSeconds}`);\n\n switch (format) {\n case \"draft-7\":\n c.header(\n \"RateLimit\",\n `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`,\n );\n break;\n\n case \"draft-6\":\n default:\n c.header(\"X-RateLimit-Limit\", String(info.limit));\n c.header(\"X-RateLimit-Remaining\", String(info.remaining));\n c.header(\"X-RateLimit-Reset\", String(Math.ceil(info.reset / 1000)));\n break;\n }\n}\n\n// ============================================================================\n// Default Key Generator\n// ============================================================================\n\nfunction getClientIP(c: Context): string {\n // Platform-specific headers (most reliable)\n const cfIP = c.req.header(\"cf-connecting-ip\");\n if (cfIP) {\n return cfIP;\n }\n\n const xRealIP = c.req.header(\"x-real-ip\");\n if (xRealIP) {\n return xRealIP;\n }\n\n // X-Forwarded-For - take first IP\n const xff = c.req.header(\"x-forwarded-for\");\n if (xff) {\n return xff.split(\",\")[0].trim();\n }\n\n return \"unknown\";\n}\n\n// ============================================================================\n// Default Handler\n// ============================================================================\n\nfunction createDefaultResponse(info: RateLimitInfo): Response {\n const retryAfter = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n return new Response(\"Rate limit exceeded\", {\n status: 429,\n headers: {\n \"Content-Type\": \"text/plain\",\n \"Retry-After\": String(retryAfter),\n },\n });\n}\n\n// ============================================================================\n// Sliding Window Algorithm\n// ============================================================================\n\nasync function checkSlidingWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const currentWindowStart = Math.floor(now / windowMs) * windowMs;\n const previousWindowStart = currentWindowStart - windowMs;\n\n const previousKey = `${key}:${previousWindowStart}`;\n const currentKey = `${key}:${currentWindowStart}`;\n\n // Increment current window\n const current = await store.increment(currentKey);\n\n // Get previous window (may not exist)\n let previousCount = 0;\n if (store.get) {\n const prev = await store.get(previousKey);\n previousCount = prev?.count ?? 0;\n }\n\n // Cloudflare's weighted formula\n const elapsedMs = now - currentWindowStart;\n const weight = (windowMs - elapsedMs) / windowMs;\n const estimatedCount = Math.floor(previousCount * weight) + current.count;\n\n const remaining = Math.max(0, limit - estimatedCount);\n const allowed = estimatedCount <= limit;\n const reset = currentWindowStart + windowMs;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Fixed Window Algorithm\n// ============================================================================\n\nasync function checkFixedWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const windowStart = Math.floor(now / windowMs) * windowMs;\n const windowKey = `${key}:${windowStart}`;\n\n const { count, reset } = await store.increment(windowKey);\n\n const remaining = Math.max(0, limit - count);\n const allowed = count <= limit;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Main Middleware\n// ============================================================================\n\n/**\n * Rate Limit Middleware for Hono.\n *\n * @param {RateLimitOptions} [options] - Configuration options\n * @returns {MiddlewareHandler} Middleware handler\n *\n * @example\n * ```ts\n * import { Hono } from 'hono'\n * import { rateLimiter } from '@jellyfungus/hono-rate-limiter'\n *\n * const app = new Hono()\n *\n * // Basic usage - 60 requests per minute\n * app.use(rateLimiter())\n *\n * // Custom configuration\n * app.use('/api/*', rateLimiter({\n * limit: 100,\n * windowMs: 60 * 1000,\n * }))\n * ```\n */\nexport const rateLimiter = <E extends Env = Env>(\n options?: RateLimitOptions<E>,\n): MiddlewareHandler<E> => {\n // Merge with defaults\n const opts = {\n limit: 60 as number | ((c: Context<E>) => number | Promise<number>),\n windowMs: 60_000,\n algorithm: \"sliding-window\" as Algorithm,\n store: undefined as RateLimitStore | undefined,\n keyGenerator: getClientIP as (c: Context<E>) => string | Promise<string>,\n handler: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => Response | Promise<Response>)\n | undefined,\n headers: \"draft-6\" as HeadersFormat,\n skip: undefined as\n | ((c: Context<E>) => boolean | Promise<boolean>)\n | undefined,\n skipSuccessfulRequests: false,\n skipFailedRequests: false,\n onRateLimited: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => void | Promise<void>)\n | undefined,\n ...options,\n };\n\n // Use default store if none provided\n const store = opts.store ?? (defaultStore ??= new MemoryStore());\n\n // Track initialization\n let initialized = false;\n\n return async function rateLimiter(c, next) {\n // Initialize store on first request\n if (!initialized && store.init) {\n await store.init(opts.windowMs);\n initialized = true;\n }\n\n // Check if should skip\n if (opts.skip) {\n const shouldSkip = await opts.skip(c);\n if (shouldSkip) {\n return next();\n }\n }\n\n // Generate key\n const key = await opts.keyGenerator(c);\n\n // Get limit (may be dynamic)\n const limit =\n typeof opts.limit === \"function\" ? await opts.limit(c) : opts.limit;\n\n // Check rate limit\n const { allowed, info } =\n opts.algorithm === \"sliding-window\"\n ? await checkSlidingWindow(store, key, limit, opts.windowMs)\n : await checkFixedWindow(store, key, limit, opts.windowMs);\n\n // Set context variable for downstream middleware\n c.set(\"rateLimit\", info);\n\n // Expose store access in context\n c.set(\"rateLimitStore\", {\n getKey: store.get?.bind(store) ?? (() => undefined),\n resetKey: store.resetKey.bind(store),\n });\n\n // Set headers\n setHeaders(c, info, opts.headers, opts.windowMs);\n\n // Handle rate limited\n if (!allowed) {\n // Fire callback\n if (opts.onRateLimited) {\n await opts.onRateLimited(c, info);\n }\n\n // Custom handler or default\n if (opts.handler) {\n return opts.handler(c, info);\n }\n return createDefaultResponse(info);\n }\n\n // Continue\n await next();\n\n // Handle skip options after response\n if (opts.skipSuccessfulRequests || opts.skipFailedRequests) {\n const status = c.res.status;\n const shouldDecrement =\n (opts.skipSuccessfulRequests && status >= 200 && status < 300) ||\n (opts.skipFailedRequests && status >= 400);\n\n if (shouldDecrement && store.decrement) {\n const windowStart =\n Math.floor(Date.now() / opts.windowMs) * opts.windowMs;\n const windowKey = `${key}:${windowStart}`;\n await store.decrement(windowKey);\n }\n }\n };\n};\n\n// ============================================================================\n// Cloudflare Rate Limiting Binding Middleware\n// ============================================================================\n\n/**\n * Rate limiter using Cloudflare's built-in Rate Limiting binding.\n *\n * This uses Cloudflare's globally distributed rate limiting infrastructure,\n * which is ideal for high-traffic applications.\n *\n * @example\n * ```ts\n * import { cloudflareRateLimiter } from '@jellyfungus/hono-rate-limiter'\n *\n * type Bindings = { RATE_LIMITER: RateLimitBinding }\n *\n * const app = new Hono<{ Bindings: Bindings }>()\n *\n * app.use(cloudflareRateLimiter({\n * binding: (c) => c.env.RATE_LIMITER,\n * keyGenerator: (c) => c.req.header('cf-connecting-ip') ?? 'unknown',\n * }))\n * ```\n */\nexport const cloudflareRateLimiter = <E extends Env = Env>(\n options: CloudflareRateLimitOptions<E>,\n): MiddlewareHandler<E> => {\n const { binding, keyGenerator, handler, skip } = options;\n\n return async function cloudflareRateLimiter(c, next) {\n // Check if should skip\n if (skip) {\n const shouldSkip = await skip(c);\n if (shouldSkip) {\n return next();\n }\n }\n\n // Get binding (may be dynamic)\n const rateLimitBinding =\n typeof binding === \"function\" ? binding(c) : binding;\n\n // Generate key\n const key = await keyGenerator(c);\n\n // Check rate limit\n const { success } = await rateLimitBinding.limit({ key });\n\n if (!success) {\n if (handler) {\n return handler(c);\n }\n return new Response(\"Rate limit exceeded\", {\n status: 429,\n headers: { \"Content-Type\": \"text/plain\" },\n });\n }\n\n return next();\n };\n};\n\n// ============================================================================\n// Exports\n// ============================================================================\n\nexport { getClientIP };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACmOO,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,SAAK,QAAQ,MAAM;AAAA,EACrB;AAAA,EAEA,WAAiB;AACf,QAAI,KAAK,cAAc;AACrB,oBAAc,KAAK,YAAY;AAAA,IACjC;AACA,SAAK,QAAQ,MAAM;AAAA,EACrB;AACF;;;ADnOA,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,MAAM,GAAG,IAAI,mBAAmB;AACpD,QAAM,aAAa,MAAM,GAAG,IAAI,kBAAkB;AAGlD,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,MAAM,GAAG,IAAI,WAAW;AAE1C,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;AAOA,IAAI;AAgCG,SAAS,iBACd,SAGwC;AACxC,QAAM;AAAA,IACJ,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,YAAY;AAAA,IACZ,QAAS,mBAAmB,IAAI,YAAY;AAAA,IAC5C;AAAA,IACA,UAAU,CAAC,IAAI,SAAS;AACtB,YAAM,aAAa,KAAK,MAAM,KAAK,QAAQ,KAAK,IAAI,KAAK,GAAI;AAC7D,SAAG,MAAM,MAAM,oCAAoC,UAAU,GAAG;AAAA,IAClE;AAAA,IACA;AAAA,EACF,IAAI;AAGJ,MAAI,cAAc;AAElB,SAAO,CAAC,iBAAkE;AACxE,WAAO,OAAO,MAAqC;AAEjD,UAAI,CAAC,eAAe,MAAM,MAAM;AAC9B,cAAM,MAAM,KAAK,QAAQ;AACzB,sBAAc;AAAA,MAChB;AAGA,YAAM,MAAM,MAAM,aAAa,CAAC;AAGhC,YAAM,eAAe,OAAO,UAAU,aAAa,MAAM,MAAM,CAAC,IAAI;AAGpE,YAAM,SAAS,MAAM,aAAa,CAAC;AAEnC,aAAO;AAAA,QACL,GAAG;AAAA,QAEH,WAAW,OAAO,OAAO,OAAO;AAE9B,cAAI,MAAM;AACR,kBAAM,aAAa,MAAM,KAAK,OAAO,EAAE;AACvC,gBAAI,YAAY;AACd,oBAAM,OAAO,YAAY,OAAO,EAAE;AAClC;AAAA,YACF;AAAA,UACF;AAGA,gBAAM,EAAE,SAAS,KAAK,IACpB,cAAc,mBACV,MAAM,mBAAmB,OAAO,KAAK,cAAc,QAAQ,IAC3D,MAAM,iBAAiB,OAAO,KAAK,cAAc,QAAQ;AAE/D,cAAI,CAAC,SAAS;AACZ,oBAAQ,IAAI,IAAI;AAChB;AAAA,UACF;AAGA,gBAAM,OAAO,YAAY,OAAO,EAAE;AAAA,QACpC;AAAA,QAEA,QAAQ,OAAO,OAAO,OAAO;AAC3B,gBAAM,OAAO,SAAS,OAAO,EAAE;AAAA,QACjC;AAAA,QAEA,SAAS,OAAO,OAAO,OAAO;AAC5B,gBAAM,OAAO,UAAU,OAAO,EAAE;AAAA,QAClC;AAAA,QAEA,SAAS,OAAO,OAAO,OAAO;AAC5B,gBAAM,OAAO,UAAU,OAAO,EAAE;AAAA,QAClC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/websocket.ts","../src/index.ts"],"sourcesContent":["/**\n * @module\n * WebSocket Rate Limiting for Hono.\n */\n\nimport type { Context, Env } from \"hono\";\nimport type { WSContext, WSEvents } from \"hono/ws\";\nimport {\n MemoryStore,\n type RateLimitStore,\n type RateLimitInfo,\n type StoreResult,\n type Algorithm,\n} from \"./index\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Options for WebSocket rate limiting\n */\nexport type WSRateLimitOptions<E extends Env = Env> = {\n /**\n * Maximum messages allowed in the time window.\n * @default 100\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 * Required for WebSocket rate limiting.\n */\n keyGenerator: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n * Default: closes connection with code 1008.\n */\n handler?: (ws: WSContext, info: RateLimitInfo) => void;\n\n /**\n * Skip rate limiting for certain messages.\n */\n skip?: (event: MessageEvent, ws: WSContext) => boolean | Promise<boolean>;\n};\n\n// ============================================================================\n// Sliding Window Algorithm (adapted for WS)\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 = `ws:${key}:${previousWindowStart}`;\n const currentKey = `ws:${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 (adapted for WS)\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 = `ws:${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// WebSocket Rate Limiter\n// ============================================================================\n\n// Singleton default store for WebSocket\nlet wsDefaultStore: MemoryStore | undefined;\n\n/**\n * WebSocket rate limiting middleware for Hono.\n *\n * Wraps your WebSocket event handlers to add rate limiting on messages.\n *\n * @example\n * ```ts\n * import { Hono } from 'hono'\n * import { createBunWebSocket } from 'hono/bun'\n * import { webSocketLimiter } from '@jellyfungus/hono-rate-limiter/websocket'\n *\n * const { upgradeWebSocket, websocket } = createBunWebSocket()\n *\n * const app = new Hono()\n *\n * const wsLimiter = webSocketLimiter({\n * limit: 100,\n * windowMs: 60_000,\n * keyGenerator: (c) => c.req.header('cf-connecting-ip') ?? 'unknown',\n * })\n *\n * app.get('/ws', upgradeWebSocket(wsLimiter((c) => ({\n * onMessage(event, ws) {\n * ws.send('Hello!')\n * },\n * }))))\n *\n * export default { port: 3000, fetch: app.fetch, websocket }\n * ```\n */\nexport function webSocketLimiter<E extends Env = Env>(\n options: WSRateLimitOptions<E>,\n): (\n createEvents: (c: Context<E>) => WSEvents | Promise<WSEvents>,\n) => (c: Context<E>) => Promise<WSEvents> {\n const {\n limit = 100,\n windowMs = 60_000,\n algorithm = \"sliding-window\",\n store = (wsDefaultStore ??= new MemoryStore()),\n keyGenerator,\n handler = (ws, info) => {\n const retryAfter = Math.ceil((info.reset - Date.now()) / 1000);\n ws.close(1008, `Rate limit exceeded. Retry after ${retryAfter}s`);\n },\n skip,\n } = options;\n\n // Track initialization\n let initialized = false;\n\n return (createEvents: (c: Context<E>) => WSEvents | Promise<WSEvents>) => {\n return async (c: Context<E>): Promise<WSEvents> => {\n // Initialize store on first use\n if (!initialized && store.init) {\n await store.init(windowMs);\n initialized = true;\n }\n\n // Get the key for this connection\n const key = await keyGenerator(c);\n\n // Get the limit (may be dynamic)\n const currentLimit = typeof limit === \"function\" ? await limit(c) : limit;\n\n // Get the original events\n const events = await createEvents(c);\n\n return {\n ...events,\n\n onMessage: async (event, ws) => {\n // Check if should skip\n if (skip) {\n const shouldSkip = await skip(event, ws);\n if (shouldSkip) {\n await events.onMessage?.(event, ws);\n return;\n }\n }\n\n // Check rate limit\n const { allowed, info } =\n algorithm === \"sliding-window\"\n ? await checkSlidingWindow(store, key, currentLimit, windowMs)\n : await checkFixedWindow(store, key, currentLimit, windowMs);\n\n if (!allowed) {\n handler(ws, info);\n return;\n }\n\n // Call original handler\n await events.onMessage?.(event, ws);\n },\n\n onOpen: async (event, ws) => {\n await events.onOpen?.(event, ws);\n },\n\n onClose: async (event, ws) => {\n await events.onClose?.(event, ws);\n },\n\n onError: async (event, ws) => {\n await events.onError?.(event, ws);\n },\n };\n };\n };\n}\n","/**\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":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/websocket.ts"],"sourcesContent":["/**\n * @module\n * Rate Limit Middleware for Hono.\n */\n\nimport type { Context, Env, MiddlewareHandler } from \"hono\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Rate limit information for a single request\n */\nexport type RateLimitInfo = {\n /** Maximum requests allowed in window */\n limit: number;\n /** Remaining requests in current window */\n remaining: number;\n /** Unix timestamp (ms) when window resets */\n reset: number;\n};\n\n/**\n * Result from store increment operation\n */\nexport type StoreResult = {\n /** Current request count in window */\n count: number;\n /** When the window resets (Unix timestamp ms) */\n reset: number;\n};\n\n/**\n * Header format versions\n */\nexport type HeadersFormat =\n | \"draft-6\" // X-RateLimit-* headers\n | \"draft-7\" // RateLimit-* without structured fields\n | false; // Disable headers\n\n/**\n * Rate limit algorithm\n */\nexport type Algorithm = \"fixed-window\" | \"sliding-window\";\n\n/**\n * Store interface for rate limit state\n */\nexport type RateLimitStore = {\n /**\n * Initialize store. Called once before first use.\n */\n init?: (windowMs: number) => void | Promise<void>;\n\n /**\n * Increment counter for key and return current state.\n */\n increment: (key: string) => StoreResult | Promise<StoreResult>;\n\n /**\n * Decrement counter for key.\n */\n decrement?: (key: string) => void | Promise<void>;\n\n /**\n * Reset a specific key.\n */\n resetKey: (key: string) => void | Promise<void>;\n\n /**\n * Reset all keys.\n */\n resetAll?: () => void | Promise<void>;\n\n /**\n * Get current state for key.\n */\n get?: (\n key: string,\n ) => StoreResult | Promise<StoreResult | undefined> | undefined;\n\n /**\n * Graceful shutdown.\n */\n shutdown?: () => void | Promise<void>;\n};\n\n/**\n * Store access interface exposed in context\n */\nexport type RateLimitStoreAccess = {\n /** Get rate limit info for a key */\n getKey: (\n key: string,\n ) => StoreResult | Promise<StoreResult | undefined> | undefined;\n /** Reset rate limit for a key */\n resetKey: (key: string) => void | Promise<void>;\n};\n\n/**\n * Options for rate limit middleware\n */\nexport type RateLimitOptions<E extends Env = Env> = {\n /**\n * Maximum requests allowed in the time window.\n * @default 60\n */\n limit?: number | ((c: Context<E>) => number | Promise<number>);\n\n /**\n * Time window in milliseconds.\n * @default 60000 (1 minute)\n */\n windowMs?: number;\n\n /**\n * Rate limiting algorithm.\n * @default 'sliding-window'\n */\n algorithm?: Algorithm;\n\n /**\n * Storage backend for rate limit state.\n * @default MemoryStore\n */\n store?: RateLimitStore;\n\n /**\n * Generate unique key for each client.\n * @default IP address from headers\n */\n keyGenerator?: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n */\n handler?: (\n c: Context<E>,\n info: RateLimitInfo,\n ) => Response | Promise<Response>;\n\n /**\n * HTTP header format to use.\n * @default 'draft-6'\n */\n headers?: HeadersFormat;\n\n /**\n * Skip rate limiting for certain requests.\n */\n skip?: (c: Context<E>) => boolean | Promise<boolean>;\n\n /**\n * Don't count successful (2xx) requests against limit.\n * @default false\n */\n skipSuccessfulRequests?: boolean;\n\n /**\n * Don't count failed (4xx, 5xx) requests against limit.\n * @default false\n */\n skipFailedRequests?: boolean;\n\n /**\n * Callback when a request is rate limited.\n */\n onRateLimited?: (c: Context<E>, info: RateLimitInfo) => void | Promise<void>;\n};\n\n/**\n * Cloudflare Rate Limiting binding interface\n */\nexport type RateLimitBinding = {\n limit: (options: { key: string }) => Promise<{ success: boolean }>;\n};\n\n/**\n * Options for Cloudflare Rate Limiting binding\n */\nexport type CloudflareRateLimitOptions<E extends Env = Env> = {\n /**\n * Cloudflare Rate Limiting binding from env\n */\n binding: RateLimitBinding | ((c: Context<E>) => RateLimitBinding);\n\n /**\n * Generate unique key for each client.\n */\n keyGenerator: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n */\n handler?: (c: Context<E>) => Response | Promise<Response>;\n\n /**\n * Skip rate limiting for certain requests.\n */\n skip?: (c: Context<E>) => boolean | Promise<boolean>;\n};\n\n// ============================================================================\n// Context Variable Type Extension\n// ============================================================================\n\ndeclare module \"hono\" {\n interface ContextVariableMap {\n rateLimit?: RateLimitInfo;\n rateLimitStore?: RateLimitStoreAccess;\n }\n}\n\n// ============================================================================\n// Memory Store\n// ============================================================================\n\ntype MemoryEntry = {\n count: number;\n reset: number;\n};\n\n/**\n * In-memory store for rate limiting.\n * Suitable for single-instance deployments.\n */\nexport class MemoryStore implements RateLimitStore {\n private entries = new Map<string, MemoryEntry>();\n private windowMs = 60_000;\n private cleanupTimer?: ReturnType<typeof setInterval>;\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n\n // Cleanup expired entries every minute\n this.cleanupTimer = setInterval(() => {\n const now = Date.now();\n for (const [key, entry] of this.entries) {\n if (entry.reset <= now) {\n this.entries.delete(key);\n }\n }\n }, 60_000);\n\n // Don't keep process alive for cleanup\n if (typeof this.cleanupTimer.unref === \"function\") {\n this.cleanupTimer.unref();\n }\n }\n\n increment(key: string): StoreResult {\n const now = Date.now();\n const existing = this.entries.get(key);\n\n if (!existing || existing.reset <= now) {\n // New window\n const reset = now + this.windowMs;\n this.entries.set(key, { count: 1, reset });\n return { count: 1, reset };\n }\n\n // Increment existing\n existing.count++;\n return { count: existing.count, reset: existing.reset };\n }\n\n get(key: string): StoreResult | undefined {\n const entry = this.entries.get(key);\n if (!entry || entry.reset <= Date.now()) {\n return undefined;\n }\n return { count: entry.count, reset: entry.reset };\n }\n\n decrement(key: string): void {\n const entry = this.entries.get(key);\n if (entry && entry.count > 0) {\n entry.count--;\n }\n }\n\n resetKey(key: string): void {\n this.entries.delete(key);\n }\n\n resetAll(): void {\n this.entries.clear();\n }\n\n shutdown(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n }\n this.entries.clear();\n }\n}\n\n// Singleton default store\nlet defaultStore: MemoryStore | undefined;\n\n// ============================================================================\n// Header Generation\n// ============================================================================\n\nfunction setHeaders(\n c: Context,\n info: RateLimitInfo,\n format: HeadersFormat,\n windowMs: number,\n): void {\n if (format === false) {\n return;\n }\n\n const windowSeconds = Math.ceil(windowMs / 1000);\n const resetSeconds = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n // RateLimit-Policy header (IETF compliant)\n c.header(\"RateLimit-Policy\", `${info.limit};w=${windowSeconds}`);\n\n switch (format) {\n case \"draft-7\":\n c.header(\n \"RateLimit\",\n `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`,\n );\n break;\n\n case \"draft-6\":\n default:\n c.header(\"X-RateLimit-Limit\", String(info.limit));\n c.header(\"X-RateLimit-Remaining\", String(info.remaining));\n c.header(\"X-RateLimit-Reset\", String(Math.ceil(info.reset / 1000)));\n break;\n }\n}\n\n// ============================================================================\n// Default Key Generator\n// ============================================================================\n\nfunction getClientIP(c: Context): string {\n // Platform-specific headers (most reliable)\n const cfIP = c.req.header(\"cf-connecting-ip\");\n if (cfIP) {\n return cfIP;\n }\n\n const xRealIP = c.req.header(\"x-real-ip\");\n if (xRealIP) {\n return xRealIP;\n }\n\n // X-Forwarded-For - take first IP\n const xff = c.req.header(\"x-forwarded-for\");\n if (xff) {\n return xff.split(\",\")[0].trim();\n }\n\n return \"unknown\";\n}\n\n// ============================================================================\n// Default Handler\n// ============================================================================\n\nfunction createDefaultResponse(info: RateLimitInfo): Response {\n const retryAfter = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n return new Response(\"Rate limit exceeded\", {\n status: 429,\n headers: {\n \"Content-Type\": \"text/plain\",\n \"Retry-After\": String(retryAfter),\n },\n });\n}\n\n// ============================================================================\n// Sliding Window Algorithm\n// ============================================================================\n\nasync function checkSlidingWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const currentWindowStart = Math.floor(now / windowMs) * windowMs;\n const previousWindowStart = currentWindowStart - windowMs;\n\n const previousKey = `${key}:${previousWindowStart}`;\n const currentKey = `${key}:${currentWindowStart}`;\n\n // Increment current window\n const current = await store.increment(currentKey);\n\n // Get previous window (may not exist)\n let previousCount = 0;\n if (store.get) {\n const prev = await store.get(previousKey);\n previousCount = prev?.count ?? 0;\n }\n\n // Cloudflare's weighted formula\n const elapsedMs = now - currentWindowStart;\n const weight = (windowMs - elapsedMs) / windowMs;\n const estimatedCount = Math.floor(previousCount * weight) + current.count;\n\n const remaining = Math.max(0, limit - estimatedCount);\n const allowed = estimatedCount <= limit;\n const reset = currentWindowStart + windowMs;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Fixed Window Algorithm\n// ============================================================================\n\nasync function checkFixedWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const windowStart = Math.floor(now / windowMs) * windowMs;\n const windowKey = `${key}:${windowStart}`;\n\n const { count, reset } = await store.increment(windowKey);\n\n const remaining = Math.max(0, limit - count);\n const allowed = count <= limit;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Main Middleware\n// ============================================================================\n\n/**\n * Rate Limit Middleware for Hono.\n *\n * @param {RateLimitOptions} [options] - Configuration options\n * @returns {MiddlewareHandler} Middleware handler\n *\n * @example\n * ```ts\n * import { Hono } from 'hono'\n * import { rateLimiter } from '@jellyfungus/hono-rate-limiter'\n *\n * const app = new Hono()\n *\n * // Basic usage - 60 requests per minute\n * app.use(rateLimiter())\n *\n * // Custom configuration\n * app.use('/api/*', rateLimiter({\n * limit: 100,\n * windowMs: 60 * 1000,\n * }))\n * ```\n */\nexport const rateLimiter = <E extends Env = Env>(\n options?: RateLimitOptions<E>,\n): MiddlewareHandler<E> => {\n // Merge with defaults\n const opts = {\n limit: 60 as number | ((c: Context<E>) => number | Promise<number>),\n windowMs: 60_000,\n algorithm: \"sliding-window\" as Algorithm,\n store: undefined as RateLimitStore | undefined,\n keyGenerator: getClientIP as (c: Context<E>) => string | Promise<string>,\n handler: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => Response | Promise<Response>)\n | undefined,\n headers: \"draft-6\" as HeadersFormat,\n skip: undefined as\n | ((c: Context<E>) => boolean | Promise<boolean>)\n | undefined,\n skipSuccessfulRequests: false,\n skipFailedRequests: false,\n onRateLimited: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => void | Promise<void>)\n | undefined,\n ...options,\n };\n\n // Use default store if none provided\n const store = opts.store ?? (defaultStore ??= new MemoryStore());\n\n // Track initialization\n let initialized = false;\n\n return async function rateLimiter(c, next) {\n // Initialize store on first request\n if (!initialized && store.init) {\n await store.init(opts.windowMs);\n initialized = true;\n }\n\n // Check if should skip\n if (opts.skip) {\n const shouldSkip = await opts.skip(c);\n if (shouldSkip) {\n return next();\n }\n }\n\n // Generate key\n const key = await opts.keyGenerator(c);\n\n // Get limit (may be dynamic)\n const limit =\n typeof opts.limit === \"function\" ? await opts.limit(c) : opts.limit;\n\n // Check rate limit\n const { allowed, info } =\n opts.algorithm === \"sliding-window\"\n ? await checkSlidingWindow(store, key, limit, opts.windowMs)\n : await checkFixedWindow(store, key, limit, opts.windowMs);\n\n // Set context variable for downstream middleware\n c.set(\"rateLimit\", info);\n\n // Expose store access in context\n c.set(\"rateLimitStore\", {\n getKey: store.get?.bind(store) ?? (() => undefined),\n resetKey: store.resetKey.bind(store),\n });\n\n // Set headers\n setHeaders(c, info, opts.headers, opts.windowMs);\n\n // Handle rate limited\n if (!allowed) {\n // Fire callback\n if (opts.onRateLimited) {\n await opts.onRateLimited(c, info);\n }\n\n // Custom handler or default\n if (opts.handler) {\n return opts.handler(c, info);\n }\n return createDefaultResponse(info);\n }\n\n // Continue\n await next();\n\n // Handle skip options after response\n if (opts.skipSuccessfulRequests || opts.skipFailedRequests) {\n const status = c.res.status;\n const shouldDecrement =\n (opts.skipSuccessfulRequests && status >= 200 && status < 300) ||\n (opts.skipFailedRequests && status >= 400);\n\n if (shouldDecrement && store.decrement) {\n const windowStart =\n Math.floor(Date.now() / opts.windowMs) * opts.windowMs;\n const windowKey = `${key}:${windowStart}`;\n await store.decrement(windowKey);\n }\n }\n };\n};\n\n// ============================================================================\n// Cloudflare Rate Limiting Binding Middleware\n// ============================================================================\n\n/**\n * Rate limiter using Cloudflare's built-in Rate Limiting binding.\n *\n * This uses Cloudflare's globally distributed rate limiting infrastructure,\n * which is ideal for high-traffic applications.\n *\n * @example\n * ```ts\n * import { cloudflareRateLimiter } from '@jellyfungus/hono-rate-limiter'\n *\n * type Bindings = { RATE_LIMITER: RateLimitBinding }\n *\n * const app = new Hono<{ Bindings: Bindings }>()\n *\n * app.use(cloudflareRateLimiter({\n * binding: (c) => c.env.RATE_LIMITER,\n * keyGenerator: (c) => c.req.header('cf-connecting-ip') ?? 'unknown',\n * }))\n * ```\n */\nexport const cloudflareRateLimiter = <E extends Env = Env>(\n options: CloudflareRateLimitOptions<E>,\n): MiddlewareHandler<E> => {\n const { binding, keyGenerator, handler, skip } = options;\n\n return async function cloudflareRateLimiter(c, next) {\n // Check if should skip\n if (skip) {\n const shouldSkip = await skip(c);\n if (shouldSkip) {\n return next();\n }\n }\n\n // Get binding (may be dynamic)\n const rateLimitBinding =\n typeof binding === \"function\" ? binding(c) : binding;\n\n // Generate key\n const key = await keyGenerator(c);\n\n // Check rate limit\n const { success } = await rateLimitBinding.limit({ key });\n\n if (!success) {\n if (handler) {\n return handler(c);\n }\n return new Response(\"Rate limit exceeded\", {\n status: 429,\n headers: { \"Content-Type\": \"text/plain\" },\n });\n }\n\n return next();\n };\n};\n\n// ============================================================================\n// Exports\n// ============================================================================\n\nexport { getClientIP };\n","/**\n * @module\n * WebSocket Rate Limiting for Hono.\n */\n\nimport type { Context, Env } from \"hono\";\nimport type { WSContext, WSEvents } from \"hono/ws\";\nimport {\n MemoryStore,\n type RateLimitStore,\n type RateLimitInfo,\n type StoreResult,\n type Algorithm,\n} from \"./index\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Options for WebSocket rate limiting\n */\nexport type WSRateLimitOptions<E extends Env = Env> = {\n /**\n * Maximum messages allowed in the time window.\n * @default 100\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 * Required for WebSocket rate limiting.\n */\n keyGenerator: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n * Default: closes connection with code 1008.\n */\n handler?: (ws: WSContext, info: RateLimitInfo) => void;\n\n /**\n * Skip rate limiting for certain messages.\n */\n skip?: (event: MessageEvent, ws: WSContext) => boolean | Promise<boolean>;\n};\n\n// ============================================================================\n// Sliding Window Algorithm (adapted for WS)\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 = `ws:${key}:${previousWindowStart}`;\n const currentKey = `ws:${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 (adapted for WS)\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 = `ws:${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// WebSocket Rate Limiter\n// ============================================================================\n\n// Singleton default store for WebSocket\nlet wsDefaultStore: MemoryStore | undefined;\n\n/**\n * WebSocket rate limiting middleware for Hono.\n *\n * Wraps your WebSocket event handlers to add rate limiting on messages.\n *\n * @example\n * ```ts\n * import { Hono } from 'hono'\n * import { createBunWebSocket } from 'hono/bun'\n * import { webSocketLimiter } from '@jellyfungus/hono-rate-limiter/websocket'\n *\n * const { upgradeWebSocket, websocket } = createBunWebSocket()\n *\n * const app = new Hono()\n *\n * const wsLimiter = webSocketLimiter({\n * limit: 100,\n * windowMs: 60_000,\n * keyGenerator: (c) => c.req.header('cf-connecting-ip') ?? 'unknown',\n * })\n *\n * app.get('/ws', upgradeWebSocket(wsLimiter((c) => ({\n * onMessage(event, ws) {\n * ws.send('Hello!')\n * },\n * }))))\n *\n * export default { port: 3000, fetch: app.fetch, websocket }\n * ```\n */\nexport function webSocketLimiter<E extends Env = Env>(\n options: WSRateLimitOptions<E>,\n): (\n createEvents: (c: Context<E>) => WSEvents | Promise<WSEvents>,\n) => (c: Context<E>) => Promise<WSEvents> {\n const {\n limit = 100,\n windowMs = 60_000,\n algorithm = \"sliding-window\",\n store = (wsDefaultStore ??= new MemoryStore()),\n keyGenerator,\n handler = (ws, info) => {\n const retryAfter = Math.ceil((info.reset - Date.now()) / 1000);\n ws.close(1008, `Rate limit exceeded. Retry after ${retryAfter}s`);\n },\n skip,\n } = options;\n\n // Track initialization\n let initialized = false;\n\n return (createEvents: (c: Context<E>) => WSEvents | Promise<WSEvents>) => {\n return async (c: Context<E>): Promise<WSEvents> => {\n // Initialize store on first use\n if (!initialized && store.init) {\n await store.init(windowMs);\n initialized = true;\n }\n\n // Get the key for this connection\n const key = await keyGenerator(c);\n\n // Get the limit (may be dynamic)\n const currentLimit = typeof limit === \"function\" ? await limit(c) : limit;\n\n // Get the original events\n const events = await createEvents(c);\n\n return {\n ...events,\n\n onMessage: async (event, ws) => {\n // Check if should skip\n if (skip) {\n const shouldSkip = await skip(event, ws);\n if (shouldSkip) {\n await events.onMessage?.(event, ws);\n return;\n }\n }\n\n // Check rate limit\n const { allowed, info } =\n algorithm === \"sliding-window\"\n ? await checkSlidingWindow(store, key, currentLimit, windowMs)\n : await checkFixedWindow(store, key, currentLimit, windowMs);\n\n if (!allowed) {\n handler(ws, info);\n return;\n }\n\n // Call original handler\n await events.onMessage?.(event, ws);\n },\n\n onOpen: async (event, ws) => {\n await events.onOpen?.(event, ws);\n },\n\n onClose: async (event, ws) => {\n await events.onClose?.(event, ws);\n },\n\n onError: async (event, ws) => {\n await events.onError?.(event, ws);\n },\n };\n };\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/websocket.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","/**\n * @module\n * WebSocket Rate Limiting for Hono.\n */\n\nimport type { Context, Env } from \"hono\";\nimport type { WSContext, WSEvents } from \"hono/ws\";\nimport {\n MemoryStore,\n type RateLimitStore,\n type RateLimitInfo,\n type StoreResult,\n type Algorithm,\n} from \"./index\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Options for WebSocket rate limiting\n */\nexport type WSRateLimitOptions<E extends Env = Env> = {\n /**\n * Maximum messages allowed in the time window.\n * @default 100\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 * Required for WebSocket rate limiting.\n */\n keyGenerator: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n * Default: closes connection with code 1008.\n */\n handler?: (ws: WSContext, info: RateLimitInfo) => void;\n\n /**\n * Skip rate limiting for certain messages.\n */\n skip?: (event: MessageEvent, ws: WSContext) => boolean | Promise<boolean>;\n};\n\n// ============================================================================\n// Sliding Window Algorithm (adapted for WS)\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 = `ws:${key}:${previousWindowStart}`;\n const currentKey = `ws:${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 (adapted for WS)\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 = `ws:${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// WebSocket Rate Limiter\n// ============================================================================\n\n// Singleton default store for WebSocket\nlet wsDefaultStore: MemoryStore | undefined;\n\n/**\n * WebSocket rate limiting middleware for Hono.\n *\n * Wraps your WebSocket event handlers to add rate limiting on messages.\n *\n * @example\n * ```ts\n * import { Hono } from 'hono'\n * import { createBunWebSocket } from 'hono/bun'\n * import { webSocketLimiter } from '@jellyfungus/hono-rate-limiter/websocket'\n *\n * const { upgradeWebSocket, websocket } = createBunWebSocket()\n *\n * const app = new Hono()\n *\n * const wsLimiter = webSocketLimiter({\n * limit: 100,\n * windowMs: 60_000,\n * keyGenerator: (c) => c.req.header('cf-connecting-ip') ?? 'unknown',\n * })\n *\n * app.get('/ws', upgradeWebSocket(wsLimiter((c) => ({\n * onMessage(event, ws) {\n * ws.send('Hello!')\n * },\n * }))))\n *\n * export default { port: 3000, fetch: app.fetch, websocket }\n * ```\n */\nexport function webSocketLimiter<E extends Env = Env>(\n options: WSRateLimitOptions<E>,\n): (\n createEvents: (c: Context<E>) => WSEvents | Promise<WSEvents>,\n) => (c: Context<E>) => Promise<WSEvents> {\n const {\n limit = 100,\n windowMs = 60_000,\n algorithm = \"sliding-window\",\n store = (wsDefaultStore ??= new MemoryStore()),\n keyGenerator,\n handler = (ws, info) => {\n const retryAfter = Math.ceil((info.reset - Date.now()) / 1000);\n ws.close(1008, `Rate limit exceeded. Retry after ${retryAfter}s`);\n },\n skip,\n } = options;\n\n // Track initialization\n let initialized = false;\n\n return (createEvents: (c: Context<E>) => WSEvents | Promise<WSEvents>) => {\n return async (c: Context<E>): Promise<WSEvents> => {\n // Initialize store on first use\n if (!initialized && store.init) {\n await store.init(windowMs);\n initialized = true;\n }\n\n // Get the key for this connection\n const key = await keyGenerator(c);\n\n // Get the limit (may be dynamic)\n const currentLimit = typeof limit === \"function\" ? await limit(c) : limit;\n\n // Get the original events\n const events = await createEvents(c);\n\n return {\n ...events,\n\n onMessage: async (event, ws) => {\n // Check if should skip\n if (skip) {\n const shouldSkip = await skip(event, ws);\n if (shouldSkip) {\n await events.onMessage?.(event, ws);\n return;\n }\n }\n\n // Check rate limit\n const { allowed, info } =\n algorithm === \"sliding-window\"\n ? await checkSlidingWindow(store, key, currentLimit, windowMs)\n : await checkFixedWindow(store, key, currentLimit, windowMs);\n\n if (!allowed) {\n handler(ws, info);\n return;\n }\n\n // Call original handler\n await events.onMessage?.(event, ws);\n },\n\n onOpen: async (event, ws) => {\n await events.onOpen?.(event, ws);\n },\n\n onClose: async (event, ws) => {\n await events.onClose?.(event, ws);\n },\n\n onError: async (event, ws) => {\n await events.onError?.(event, ws);\n },\n };\n };\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jellyfungus/hono-rate-limiter",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Production-ready rate limiting middleware for Hono web framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",