@jellyfungus/hono-rate-limiter 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -53,6 +53,10 @@ type RateLimitStore = {
53
53
  * Reset a specific key.
54
54
  */
55
55
  resetKey: (key: string) => void | Promise<void>;
56
+ /**
57
+ * Reset all keys.
58
+ */
59
+ resetAll?: () => void | Promise<void>;
56
60
  /**
57
61
  * Get current state for key.
58
62
  */
@@ -62,6 +66,15 @@ type RateLimitStore = {
62
66
  */
63
67
  shutdown?: () => void | Promise<void>;
64
68
  };
69
+ /**
70
+ * Store access interface exposed in context
71
+ */
72
+ type RateLimitStoreAccess = {
73
+ /** Get rate limit info for a key */
74
+ getKey: (key: string) => StoreResult | Promise<StoreResult | undefined> | undefined;
75
+ /** Reset rate limit for a key */
76
+ resetKey: (key: string) => void | Promise<void>;
77
+ };
65
78
  /**
66
79
  * Options for rate limit middleware
67
80
  */
@@ -119,9 +132,41 @@ type RateLimitOptions<E extends Env = Env> = {
119
132
  */
120
133
  onRateLimited?: (c: Context<E>, info: RateLimitInfo) => void | Promise<void>;
121
134
  };
135
+ /**
136
+ * Cloudflare Rate Limiting binding interface
137
+ */
138
+ type RateLimitBinding = {
139
+ limit: (options: {
140
+ key: string;
141
+ }) => Promise<{
142
+ success: boolean;
143
+ }>;
144
+ };
145
+ /**
146
+ * Options for Cloudflare Rate Limiting binding
147
+ */
148
+ type CloudflareRateLimitOptions<E extends Env = Env> = {
149
+ /**
150
+ * Cloudflare Rate Limiting binding from env
151
+ */
152
+ binding: RateLimitBinding | ((c: Context<E>) => RateLimitBinding);
153
+ /**
154
+ * Generate unique key for each client.
155
+ */
156
+ keyGenerator: (c: Context<E>) => string | Promise<string>;
157
+ /**
158
+ * Handler called when rate limit is exceeded.
159
+ */
160
+ handler?: (c: Context<E>) => Response | Promise<Response>;
161
+ /**
162
+ * Skip rate limiting for certain requests.
163
+ */
164
+ skip?: (c: Context<E>) => boolean | Promise<boolean>;
165
+ };
122
166
  declare module "hono" {
123
167
  interface ContextVariableMap {
124
168
  rateLimit?: RateLimitInfo;
169
+ rateLimitStore?: RateLimitStoreAccess;
125
170
  }
126
171
  }
127
172
  /**
@@ -137,6 +182,7 @@ declare class MemoryStore implements RateLimitStore {
137
182
  get(key: string): StoreResult | undefined;
138
183
  decrement(key: string): void;
139
184
  resetKey(key: string): void;
185
+ resetAll(): void;
140
186
  shutdown(): void;
141
187
  }
142
188
  declare function getClientIP(c: Context): string;
@@ -149,7 +195,7 @@ declare function getClientIP(c: Context): string;
149
195
  * @example
150
196
  * ```ts
151
197
  * import { Hono } from 'hono'
152
- * import { rateLimiter } from 'hono-rate-limit'
198
+ * import { rateLimiter } from '@jellyfungus/hono-rate-limiter'
153
199
  *
154
200
  * const app = new Hono()
155
201
  *
@@ -164,5 +210,26 @@ declare function getClientIP(c: Context): string;
164
210
  * ```
165
211
  */
166
212
  declare const rateLimiter: <E extends Env = Env>(options?: RateLimitOptions<E>) => MiddlewareHandler<E>;
213
+ /**
214
+ * Rate limiter using Cloudflare's built-in Rate Limiting binding.
215
+ *
216
+ * This uses Cloudflare's globally distributed rate limiting infrastructure,
217
+ * which is ideal for high-traffic applications.
218
+ *
219
+ * @example
220
+ * ```ts
221
+ * import { cloudflareRateLimiter } from '@jellyfungus/hono-rate-limiter'
222
+ *
223
+ * type Bindings = { RATE_LIMITER: RateLimitBinding }
224
+ *
225
+ * const app = new Hono<{ Bindings: Bindings }>()
226
+ *
227
+ * app.use(cloudflareRateLimiter({
228
+ * binding: (c) => c.env.RATE_LIMITER,
229
+ * keyGenerator: (c) => c.req.header('cf-connecting-ip') ?? 'unknown',
230
+ * }))
231
+ * ```
232
+ */
233
+ declare const cloudflareRateLimiter: <E extends Env = Env>(options: CloudflareRateLimitOptions<E>) => MiddlewareHandler<E>;
167
234
 
168
- export { type Algorithm, type HeadersFormat, MemoryStore, type RateLimitInfo, type RateLimitOptions, type RateLimitStore, type StoreResult, getClientIP, rateLimiter };
235
+ export { type Algorithm, type CloudflareRateLimitOptions, type HeadersFormat, MemoryStore, type RateLimitBinding, type RateLimitInfo, type RateLimitOptions, type RateLimitStore, type RateLimitStoreAccess, type StoreResult, cloudflareRateLimiter, getClientIP, rateLimiter };
package/dist/index.js CHANGED
@@ -44,6 +44,9 @@ var MemoryStore = class {
44
44
  resetKey(key) {
45
45
  this.entries.delete(key);
46
46
  }
47
+ resetAll() {
48
+ this.entries.clear();
49
+ }
47
50
  shutdown() {
48
51
  if (this.cleanupTimer) {
49
52
  clearInterval(this.cleanupTimer);
@@ -52,16 +55,19 @@ var MemoryStore = class {
52
55
  }
53
56
  };
54
57
  var defaultStore;
55
- function setHeaders(c, info, format) {
58
+ function setHeaders(c, info, format, windowMs) {
56
59
  if (format === false) {
57
60
  return;
58
61
  }
62
+ const windowSeconds = Math.ceil(windowMs / 1e3);
59
63
  const resetSeconds = Math.max(0, Math.ceil((info.reset - Date.now()) / 1e3));
64
+ c.header("RateLimit-Policy", `${info.limit};w=${windowSeconds}`);
60
65
  switch (format) {
61
66
  case "draft-7":
62
- c.header("RateLimit-Limit", String(info.limit));
63
- c.header("RateLimit-Remaining", String(info.remaining));
64
- c.header("RateLimit-Reset", String(resetSeconds));
67
+ c.header(
68
+ "RateLimit",
69
+ `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`
70
+ );
65
71
  break;
66
72
  case "draft-6":
67
73
  default:
@@ -163,7 +169,11 @@ var rateLimiter = (options) => {
163
169
  const limit = typeof opts.limit === "function" ? await opts.limit(c) : opts.limit;
164
170
  const { allowed, info } = opts.algorithm === "sliding-window" ? await checkSlidingWindow(store, key, limit, opts.windowMs) : await checkFixedWindow(store, key, limit, opts.windowMs);
165
171
  c.set("rateLimit", info);
166
- setHeaders(c, info, opts.headers);
172
+ c.set("rateLimitStore", {
173
+ getKey: store.get?.bind(store) ?? (() => void 0),
174
+ resetKey: store.resetKey.bind(store)
175
+ });
176
+ setHeaders(c, info, opts.headers, opts.windowMs);
167
177
  if (!allowed) {
168
178
  if (opts.onRateLimited) {
169
179
  await opts.onRateLimited(c, info);
@@ -185,8 +195,33 @@ var rateLimiter = (options) => {
185
195
  }
186
196
  };
187
197
  };
198
+ var cloudflareRateLimiter = (options) => {
199
+ const { binding, keyGenerator, handler, skip } = options;
200
+ return async function cloudflareRateLimiter2(c, next) {
201
+ if (skip) {
202
+ const shouldSkip = await skip(c);
203
+ if (shouldSkip) {
204
+ return next();
205
+ }
206
+ }
207
+ const rateLimitBinding = typeof binding === "function" ? binding(c) : binding;
208
+ const key = await keyGenerator(c);
209
+ const { success } = await rateLimitBinding.limit({ key });
210
+ if (!success) {
211
+ if (handler) {
212
+ return handler(c);
213
+ }
214
+ return new Response("Rate limit exceeded", {
215
+ status: 429,
216
+ headers: { "Content-Type": "text/plain" }
217
+ });
218
+ }
219
+ return next();
220
+ };
221
+ };
188
222
  export {
189
223
  MemoryStore,
224
+ cloudflareRateLimiter,
190
225
  getClientIP,
191
226
  rateLimiter
192
227
  };
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 * Get current state for key.\n */\n get?: (\n key: string,\n ) => StoreResult | Promise<StoreResult | undefined> | undefined;\n\n /**\n * Graceful shutdown.\n */\n shutdown?: () => void | Promise<void>;\n};\n\n/**\n * Options for rate limit middleware\n */\nexport type RateLimitOptions<E extends Env = Env> = {\n /**\n * Maximum requests allowed in the time window.\n * @default 60\n */\n limit?: number | ((c: Context<E>) => number | Promise<number>);\n\n /**\n * Time window in milliseconds.\n * @default 60000 (1 minute)\n */\n windowMs?: number;\n\n /**\n * Rate limiting algorithm.\n * @default 'sliding-window'\n */\n algorithm?: Algorithm;\n\n /**\n * Storage backend for rate limit state.\n * @default MemoryStore\n */\n store?: RateLimitStore;\n\n /**\n * Generate unique key for each client.\n * @default IP address from headers\n */\n keyGenerator?: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n */\n handler?: (\n c: Context<E>,\n info: RateLimitInfo,\n ) => Response | Promise<Response>;\n\n /**\n * HTTP header format to use.\n * @default 'draft-6'\n */\n headers?: HeadersFormat;\n\n /**\n * Skip rate limiting for certain requests.\n */\n skip?: (c: Context<E>) => boolean | Promise<boolean>;\n\n /**\n * Don't count successful (2xx) requests against limit.\n * @default false\n */\n skipSuccessfulRequests?: boolean;\n\n /**\n * Don't count failed (4xx, 5xx) requests against limit.\n * @default false\n */\n skipFailedRequests?: boolean;\n\n /**\n * Callback when a request is rate limited.\n */\n onRateLimited?: (c: Context<E>, info: RateLimitInfo) => void | Promise<void>;\n};\n\n// ============================================================================\n// Context Variable Type Extension\n// ============================================================================\n\ndeclare module \"hono\" {\n interface ContextVariableMap {\n rateLimit?: RateLimitInfo;\n }\n}\n\n// ============================================================================\n// Memory Store\n// ============================================================================\n\ntype MemoryEntry = {\n count: number;\n reset: number;\n};\n\n/**\n * In-memory store for rate limiting.\n * Suitable for single-instance deployments.\n */\nexport class MemoryStore implements RateLimitStore {\n private entries = new Map<string, MemoryEntry>();\n private windowMs = 60_000;\n private cleanupTimer?: ReturnType<typeof setInterval>;\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n\n // Cleanup expired entries every minute\n this.cleanupTimer = setInterval(() => {\n const now = Date.now();\n for (const [key, entry] of this.entries) {\n if (entry.reset <= now) {\n this.entries.delete(key);\n }\n }\n }, 60_000);\n\n // Don't keep process alive for cleanup\n if (typeof this.cleanupTimer.unref === \"function\") {\n this.cleanupTimer.unref();\n }\n }\n\n increment(key: string): StoreResult {\n const now = Date.now();\n const existing = this.entries.get(key);\n\n if (!existing || existing.reset <= now) {\n // New window\n const reset = now + this.windowMs;\n this.entries.set(key, { count: 1, reset });\n return { count: 1, reset };\n }\n\n // Increment existing\n existing.count++;\n return { count: existing.count, reset: existing.reset };\n }\n\n get(key: string): StoreResult | undefined {\n const entry = this.entries.get(key);\n if (!entry || entry.reset <= Date.now()) {\n return undefined;\n }\n return { count: entry.count, reset: entry.reset };\n }\n\n decrement(key: string): void {\n const entry = this.entries.get(key);\n if (entry && entry.count > 0) {\n entry.count--;\n }\n }\n\n resetKey(key: string): void {\n this.entries.delete(key);\n }\n\n shutdown(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n }\n this.entries.clear();\n }\n}\n\n// Singleton default store\nlet defaultStore: MemoryStore | undefined;\n\n// ============================================================================\n// Header Generation\n// ============================================================================\n\nfunction setHeaders(\n c: Context,\n info: RateLimitInfo,\n format: HeadersFormat,\n): void {\n if (format === false) {\n return;\n }\n\n const resetSeconds = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n switch (format) {\n case \"draft-7\":\n c.header(\"RateLimit-Limit\", String(info.limit));\n c.header(\"RateLimit-Remaining\", String(info.remaining));\n c.header(\"RateLimit-Reset\", String(resetSeconds));\n break;\n\n case \"draft-6\":\n default:\n c.header(\"X-RateLimit-Limit\", String(info.limit));\n c.header(\"X-RateLimit-Remaining\", String(info.remaining));\n c.header(\"X-RateLimit-Reset\", String(Math.ceil(info.reset / 1000)));\n break;\n }\n}\n\n// ============================================================================\n// Default Key Generator\n// ============================================================================\n\nfunction getClientIP(c: Context): string {\n // Platform-specific headers (most reliable)\n const cfIP = c.req.header(\"cf-connecting-ip\");\n if (cfIP) {\n return cfIP;\n }\n\n const xRealIP = c.req.header(\"x-real-ip\");\n if (xRealIP) {\n return xRealIP;\n }\n\n // X-Forwarded-For - take first IP\n const xff = c.req.header(\"x-forwarded-for\");\n if (xff) {\n return xff.split(\",\")[0].trim();\n }\n\n return \"unknown\";\n}\n\n// ============================================================================\n// Default Handler\n// ============================================================================\n\nfunction createDefaultResponse(info: RateLimitInfo): Response {\n const retryAfter = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n return new Response(\"Rate limit exceeded\", {\n status: 429,\n headers: {\n \"Content-Type\": \"text/plain\",\n \"Retry-After\": String(retryAfter),\n },\n });\n}\n\n// ============================================================================\n// Sliding Window Algorithm\n// ============================================================================\n\nasync function checkSlidingWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const currentWindowStart = Math.floor(now / windowMs) * windowMs;\n const previousWindowStart = currentWindowStart - windowMs;\n\n const previousKey = `${key}:${previousWindowStart}`;\n const currentKey = `${key}:${currentWindowStart}`;\n\n // Increment current window\n const current = await store.increment(currentKey);\n\n // Get previous window (may not exist)\n let previousCount = 0;\n if (store.get) {\n const prev = await store.get(previousKey);\n previousCount = prev?.count ?? 0;\n }\n\n // Cloudflare's weighted formula\n const elapsedMs = now - currentWindowStart;\n const weight = (windowMs - elapsedMs) / windowMs;\n const estimatedCount = Math.floor(previousCount * weight) + current.count;\n\n const remaining = Math.max(0, limit - estimatedCount);\n const allowed = estimatedCount <= limit;\n const reset = currentWindowStart + windowMs;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Fixed Window Algorithm\n// ============================================================================\n\nasync function checkFixedWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const windowStart = Math.floor(now / windowMs) * windowMs;\n const windowKey = `${key}:${windowStart}`;\n\n const { count, reset } = await store.increment(windowKey);\n\n const remaining = Math.max(0, limit - count);\n const allowed = count <= limit;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Main Middleware\n// ============================================================================\n\n/**\n * Rate Limit Middleware for Hono.\n *\n * @param {RateLimitOptions} [options] - Configuration options\n * @returns {MiddlewareHandler} Middleware handler\n *\n * @example\n * ```ts\n * import { Hono } from 'hono'\n * import { rateLimiter } from 'hono-rate-limit'\n *\n * const app = new Hono()\n *\n * // Basic usage - 60 requests per minute\n * app.use(rateLimiter())\n *\n * // Custom configuration\n * app.use('/api/*', rateLimiter({\n * limit: 100,\n * windowMs: 60 * 1000,\n * }))\n * ```\n */\nexport const rateLimiter = <E extends Env = Env>(\n options?: RateLimitOptions<E>,\n): MiddlewareHandler<E> => {\n // Merge with defaults\n const opts = {\n limit: 60 as number | ((c: Context<E>) => number | Promise<number>),\n windowMs: 60_000,\n algorithm: \"sliding-window\" as Algorithm,\n store: undefined as RateLimitStore | undefined,\n keyGenerator: getClientIP as (c: Context<E>) => string | Promise<string>,\n handler: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => Response | Promise<Response>)\n | undefined,\n headers: \"draft-6\" as HeadersFormat,\n skip: undefined as\n | ((c: Context<E>) => boolean | Promise<boolean>)\n | undefined,\n skipSuccessfulRequests: false,\n skipFailedRequests: false,\n onRateLimited: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => void | Promise<void>)\n | undefined,\n ...options,\n };\n\n // Use default store if none provided\n const store = opts.store ?? (defaultStore ??= new MemoryStore());\n\n // Track initialization\n let initialized = false;\n\n return async function rateLimiter(c, next) {\n // Initialize store on first request\n if (!initialized && store.init) {\n await store.init(opts.windowMs);\n initialized = true;\n }\n\n // Check if should skip\n if (opts.skip) {\n const shouldSkip = await opts.skip(c);\n if (shouldSkip) {\n return next();\n }\n }\n\n // Generate key\n const key = await opts.keyGenerator(c);\n\n // Get limit (may be dynamic)\n const limit =\n typeof opts.limit === \"function\" ? await opts.limit(c) : opts.limit;\n\n // Check rate limit\n const { allowed, info } =\n opts.algorithm === \"sliding-window\"\n ? await checkSlidingWindow(store, key, limit, opts.windowMs)\n : await checkFixedWindow(store, key, limit, opts.windowMs);\n\n // Set context variable for downstream middleware\n c.set(\"rateLimit\", info);\n\n // Set headers\n setHeaders(c, info, opts.headers);\n\n // Handle rate limited\n if (!allowed) {\n // Fire callback\n if (opts.onRateLimited) {\n await opts.onRateLimited(c, info);\n }\n\n // Custom handler or default\n if (opts.handler) {\n return opts.handler(c, info);\n }\n return createDefaultResponse(info);\n }\n\n // Continue\n await next();\n\n // Handle skip options after response\n if (opts.skipSuccessfulRequests || opts.skipFailedRequests) {\n const status = c.res.status;\n const shouldDecrement =\n (opts.skipSuccessfulRequests && status >= 200 && status < 300) ||\n (opts.skipFailedRequests && status >= 400);\n\n if (shouldDecrement && store.decrement) {\n const windowStart =\n Math.floor(Date.now() / opts.windowMs) * opts.windowMs;\n const windowKey = `${key}:${windowStart}`;\n await store.decrement(windowKey);\n }\n }\n };\n};\n\n// ============================================================================\n// Exports\n// ============================================================================\n\nexport { getClientIP };\n"],"mappings":"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","names":["rateLimiter"]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @module\n * Rate Limit Middleware for Hono.\n */\n\nimport type { Context, Env, MiddlewareHandler } from \"hono\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Rate limit information for a single request\n */\nexport type RateLimitInfo = {\n /** Maximum requests allowed in window */\n limit: number;\n /** Remaining requests in current window */\n remaining: number;\n /** Unix timestamp (ms) when window resets */\n reset: number;\n};\n\n/**\n * Result from store increment operation\n */\nexport type StoreResult = {\n /** Current request count in window */\n count: number;\n /** When the window resets (Unix timestamp ms) */\n reset: number;\n};\n\n/**\n * Header format versions\n */\nexport type HeadersFormat =\n | \"draft-6\" // X-RateLimit-* headers\n | \"draft-7\" // RateLimit-* without structured fields\n | false; // Disable headers\n\n/**\n * Rate limit algorithm\n */\nexport type Algorithm = \"fixed-window\" | \"sliding-window\";\n\n/**\n * Store interface for rate limit state\n */\nexport type RateLimitStore = {\n /**\n * Initialize store. Called once before first use.\n */\n init?: (windowMs: number) => void | Promise<void>;\n\n /**\n * Increment counter for key and return current state.\n */\n increment: (key: string) => StoreResult | Promise<StoreResult>;\n\n /**\n * Decrement counter for key.\n */\n decrement?: (key: string) => void | Promise<void>;\n\n /**\n * Reset a specific key.\n */\n resetKey: (key: string) => void | Promise<void>;\n\n /**\n * Reset all keys.\n */\n resetAll?: () => void | Promise<void>;\n\n /**\n * Get current state for key.\n */\n get?: (\n key: string,\n ) => StoreResult | Promise<StoreResult | undefined> | undefined;\n\n /**\n * Graceful shutdown.\n */\n shutdown?: () => void | Promise<void>;\n};\n\n/**\n * Store access interface exposed in context\n */\nexport type RateLimitStoreAccess = {\n /** Get rate limit info for a key */\n getKey: (\n key: string,\n ) => StoreResult | Promise<StoreResult | undefined> | undefined;\n /** Reset rate limit for a key */\n resetKey: (key: string) => void | Promise<void>;\n};\n\n/**\n * Options for rate limit middleware\n */\nexport type RateLimitOptions<E extends Env = Env> = {\n /**\n * Maximum requests allowed in the time window.\n * @default 60\n */\n limit?: number | ((c: Context<E>) => number | Promise<number>);\n\n /**\n * Time window in milliseconds.\n * @default 60000 (1 minute)\n */\n windowMs?: number;\n\n /**\n * Rate limiting algorithm.\n * @default 'sliding-window'\n */\n algorithm?: Algorithm;\n\n /**\n * Storage backend for rate limit state.\n * @default MemoryStore\n */\n store?: RateLimitStore;\n\n /**\n * Generate unique key for each client.\n * @default IP address from headers\n */\n keyGenerator?: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n */\n handler?: (\n c: Context<E>,\n info: RateLimitInfo,\n ) => Response | Promise<Response>;\n\n /**\n * HTTP header format to use.\n * @default 'draft-6'\n */\n headers?: HeadersFormat;\n\n /**\n * Skip rate limiting for certain requests.\n */\n skip?: (c: Context<E>) => boolean | Promise<boolean>;\n\n /**\n * Don't count successful (2xx) requests against limit.\n * @default false\n */\n skipSuccessfulRequests?: boolean;\n\n /**\n * Don't count failed (4xx, 5xx) requests against limit.\n * @default false\n */\n skipFailedRequests?: boolean;\n\n /**\n * Callback when a request is rate limited.\n */\n onRateLimited?: (c: Context<E>, info: RateLimitInfo) => void | Promise<void>;\n};\n\n/**\n * Cloudflare Rate Limiting binding interface\n */\nexport type RateLimitBinding = {\n limit: (options: { key: string }) => Promise<{ success: boolean }>;\n};\n\n/**\n * Options for Cloudflare Rate Limiting binding\n */\nexport type CloudflareRateLimitOptions<E extends Env = Env> = {\n /**\n * Cloudflare Rate Limiting binding from env\n */\n binding: RateLimitBinding | ((c: Context<E>) => RateLimitBinding);\n\n /**\n * Generate unique key for each client.\n */\n keyGenerator: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n */\n handler?: (c: Context<E>) => Response | Promise<Response>;\n\n /**\n * Skip rate limiting for certain requests.\n */\n skip?: (c: Context<E>) => boolean | Promise<boolean>;\n};\n\n// ============================================================================\n// Context Variable Type Extension\n// ============================================================================\n\ndeclare module \"hono\" {\n interface ContextVariableMap {\n rateLimit?: RateLimitInfo;\n rateLimitStore?: RateLimitStoreAccess;\n }\n}\n\n// ============================================================================\n// Memory Store\n// ============================================================================\n\ntype MemoryEntry = {\n count: number;\n reset: number;\n};\n\n/**\n * In-memory store for rate limiting.\n * Suitable for single-instance deployments.\n */\nexport class MemoryStore implements RateLimitStore {\n private entries = new Map<string, MemoryEntry>();\n private windowMs = 60_000;\n private cleanupTimer?: ReturnType<typeof setInterval>;\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n\n // Cleanup expired entries every minute\n this.cleanupTimer = setInterval(() => {\n const now = Date.now();\n for (const [key, entry] of this.entries) {\n if (entry.reset <= now) {\n this.entries.delete(key);\n }\n }\n }, 60_000);\n\n // Don't keep process alive for cleanup\n if (typeof this.cleanupTimer.unref === \"function\") {\n this.cleanupTimer.unref();\n }\n }\n\n increment(key: string): StoreResult {\n const now = Date.now();\n const existing = this.entries.get(key);\n\n if (!existing || existing.reset <= now) {\n // New window\n const reset = now + this.windowMs;\n this.entries.set(key, { count: 1, reset });\n return { count: 1, reset };\n }\n\n // Increment existing\n existing.count++;\n return { count: existing.count, reset: existing.reset };\n }\n\n get(key: string): StoreResult | undefined {\n const entry = this.entries.get(key);\n if (!entry || entry.reset <= Date.now()) {\n return undefined;\n }\n return { count: entry.count, reset: entry.reset };\n }\n\n decrement(key: string): void {\n const entry = this.entries.get(key);\n if (entry && entry.count > 0) {\n entry.count--;\n }\n }\n\n resetKey(key: string): void {\n this.entries.delete(key);\n }\n\n resetAll(): void {\n this.entries.clear();\n }\n\n shutdown(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n }\n this.entries.clear();\n }\n}\n\n// Singleton default store\nlet defaultStore: MemoryStore | undefined;\n\n// ============================================================================\n// Header Generation\n// ============================================================================\n\nfunction setHeaders(\n c: Context,\n info: RateLimitInfo,\n format: HeadersFormat,\n windowMs: number,\n): void {\n if (format === false) {\n return;\n }\n\n const windowSeconds = Math.ceil(windowMs / 1000);\n const resetSeconds = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n // RateLimit-Policy header (IETF compliant)\n c.header(\"RateLimit-Policy\", `${info.limit};w=${windowSeconds}`);\n\n switch (format) {\n case \"draft-7\":\n c.header(\n \"RateLimit\",\n `limit=${info.limit}, remaining=${info.remaining}, reset=${resetSeconds}`,\n );\n break;\n\n case \"draft-6\":\n default:\n c.header(\"X-RateLimit-Limit\", String(info.limit));\n c.header(\"X-RateLimit-Remaining\", String(info.remaining));\n c.header(\"X-RateLimit-Reset\", String(Math.ceil(info.reset / 1000)));\n break;\n }\n}\n\n// ============================================================================\n// Default Key Generator\n// ============================================================================\n\nfunction getClientIP(c: Context): string {\n // Platform-specific headers (most reliable)\n const cfIP = c.req.header(\"cf-connecting-ip\");\n if (cfIP) {\n return cfIP;\n }\n\n const xRealIP = c.req.header(\"x-real-ip\");\n if (xRealIP) {\n return xRealIP;\n }\n\n // X-Forwarded-For - take first IP\n const xff = c.req.header(\"x-forwarded-for\");\n if (xff) {\n return xff.split(\",\")[0].trim();\n }\n\n return \"unknown\";\n}\n\n// ============================================================================\n// Default Handler\n// ============================================================================\n\nfunction createDefaultResponse(info: RateLimitInfo): Response {\n const retryAfter = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n return new Response(\"Rate limit exceeded\", {\n status: 429,\n headers: {\n \"Content-Type\": \"text/plain\",\n \"Retry-After\": String(retryAfter),\n },\n });\n}\n\n// ============================================================================\n// Sliding Window Algorithm\n// ============================================================================\n\nasync function checkSlidingWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const currentWindowStart = Math.floor(now / windowMs) * windowMs;\n const previousWindowStart = currentWindowStart - windowMs;\n\n const previousKey = `${key}:${previousWindowStart}`;\n const currentKey = `${key}:${currentWindowStart}`;\n\n // Increment current window\n const current = await store.increment(currentKey);\n\n // Get previous window (may not exist)\n let previousCount = 0;\n if (store.get) {\n const prev = await store.get(previousKey);\n previousCount = prev?.count ?? 0;\n }\n\n // Cloudflare's weighted formula\n const elapsedMs = now - currentWindowStart;\n const weight = (windowMs - elapsedMs) / windowMs;\n const estimatedCount = Math.floor(previousCount * weight) + current.count;\n\n const remaining = Math.max(0, limit - estimatedCount);\n const allowed = estimatedCount <= limit;\n const reset = currentWindowStart + windowMs;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Fixed Window Algorithm\n// ============================================================================\n\nasync function checkFixedWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const windowStart = Math.floor(now / windowMs) * windowMs;\n const windowKey = `${key}:${windowStart}`;\n\n const { count, reset } = await store.increment(windowKey);\n\n const remaining = Math.max(0, limit - count);\n const allowed = count <= limit;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Main Middleware\n// ============================================================================\n\n/**\n * Rate Limit Middleware for Hono.\n *\n * @param {RateLimitOptions} [options] - Configuration options\n * @returns {MiddlewareHandler} Middleware handler\n *\n * @example\n * ```ts\n * import { Hono } from 'hono'\n * import { rateLimiter } from '@jellyfungus/hono-rate-limiter'\n *\n * const app = new Hono()\n *\n * // Basic usage - 60 requests per minute\n * app.use(rateLimiter())\n *\n * // Custom configuration\n * app.use('/api/*', rateLimiter({\n * limit: 100,\n * windowMs: 60 * 1000,\n * }))\n * ```\n */\nexport const rateLimiter = <E extends Env = Env>(\n options?: RateLimitOptions<E>,\n): MiddlewareHandler<E> => {\n // Merge with defaults\n const opts = {\n limit: 60 as number | ((c: Context<E>) => number | Promise<number>),\n windowMs: 60_000,\n algorithm: \"sliding-window\" as Algorithm,\n store: undefined as RateLimitStore | undefined,\n keyGenerator: getClientIP as (c: Context<E>) => string | Promise<string>,\n handler: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => Response | Promise<Response>)\n | undefined,\n headers: \"draft-6\" as HeadersFormat,\n skip: undefined as\n | ((c: Context<E>) => boolean | Promise<boolean>)\n | undefined,\n skipSuccessfulRequests: false,\n skipFailedRequests: false,\n onRateLimited: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => void | Promise<void>)\n | undefined,\n ...options,\n };\n\n // Use default store if none provided\n const store = opts.store ?? (defaultStore ??= new MemoryStore());\n\n // Track initialization\n let initialized = false;\n\n return async function rateLimiter(c, next) {\n // Initialize store on first request\n if (!initialized && store.init) {\n await store.init(opts.windowMs);\n initialized = true;\n }\n\n // Check if should skip\n if (opts.skip) {\n const shouldSkip = await opts.skip(c);\n if (shouldSkip) {\n return next();\n }\n }\n\n // Generate key\n const key = await opts.keyGenerator(c);\n\n // Get limit (may be dynamic)\n const limit =\n typeof opts.limit === \"function\" ? await opts.limit(c) : opts.limit;\n\n // Check rate limit\n const { allowed, info } =\n opts.algorithm === \"sliding-window\"\n ? await checkSlidingWindow(store, key, limit, opts.windowMs)\n : await checkFixedWindow(store, key, limit, opts.windowMs);\n\n // Set context variable for downstream middleware\n c.set(\"rateLimit\", info);\n\n // Expose store access in context\n c.set(\"rateLimitStore\", {\n getKey: store.get?.bind(store) ?? (() => undefined),\n resetKey: store.resetKey.bind(store),\n });\n\n // Set headers\n setHeaders(c, info, opts.headers, opts.windowMs);\n\n // Handle rate limited\n if (!allowed) {\n // Fire callback\n if (opts.onRateLimited) {\n await opts.onRateLimited(c, info);\n }\n\n // Custom handler or default\n if (opts.handler) {\n return opts.handler(c, info);\n }\n return createDefaultResponse(info);\n }\n\n // Continue\n await next();\n\n // Handle skip options after response\n if (opts.skipSuccessfulRequests || opts.skipFailedRequests) {\n const status = c.res.status;\n const shouldDecrement =\n (opts.skipSuccessfulRequests && status >= 200 && status < 300) ||\n (opts.skipFailedRequests && status >= 400);\n\n if (shouldDecrement && store.decrement) {\n const windowStart =\n Math.floor(Date.now() / opts.windowMs) * opts.windowMs;\n const windowKey = `${key}:${windowStart}`;\n await store.decrement(windowKey);\n }\n }\n };\n};\n\n// ============================================================================\n// Cloudflare Rate Limiting Binding Middleware\n// ============================================================================\n\n/**\n * Rate limiter using Cloudflare's built-in Rate Limiting binding.\n *\n * This uses Cloudflare's globally distributed rate limiting infrastructure,\n * which is ideal for high-traffic applications.\n *\n * @example\n * ```ts\n * import { cloudflareRateLimiter } from '@jellyfungus/hono-rate-limiter'\n *\n * type Bindings = { RATE_LIMITER: RateLimitBinding }\n *\n * const app = new Hono<{ Bindings: Bindings }>()\n *\n * app.use(cloudflareRateLimiter({\n * binding: (c) => c.env.RATE_LIMITER,\n * keyGenerator: (c) => c.req.header('cf-connecting-ip') ?? 'unknown',\n * }))\n * ```\n */\nexport const cloudflareRateLimiter = <E extends Env = Env>(\n options: CloudflareRateLimitOptions<E>,\n): MiddlewareHandler<E> => {\n const { binding, keyGenerator, handler, skip } = options;\n\n return async function cloudflareRateLimiter(c, next) {\n // Check if should skip\n if (skip) {\n const shouldSkip = await skip(c);\n if (shouldSkip) {\n return next();\n }\n }\n\n // Get binding (may be dynamic)\n const rateLimitBinding =\n typeof binding === \"function\" ? binding(c) : binding;\n\n // Generate key\n const key = await keyGenerator(c);\n\n // Check rate limit\n const { success } = await rateLimitBinding.limit({ key });\n\n if (!success) {\n if (handler) {\n return handler(c);\n }\n return new Response(\"Rate limit exceeded\", {\n status: 429,\n headers: { \"Content-Type\": \"text/plain\" },\n });\n }\n\n return next();\n };\n};\n\n// ============================================================================\n// Exports\n// ============================================================================\n\nexport { getClientIP };\n"],"mappings":"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","names":["rateLimiter","cloudflareRateLimiter"]}
@@ -81,6 +81,13 @@ var CloudflareKVStore = class {
81
81
  async resetKey(key) {
82
82
  await this.namespace.delete(`${this.prefix}${key}`);
83
83
  }
84
+ /**
85
+ * Reset all keys. Note: This is a no-op for Cloudflare KV as listing
86
+ * all keys with a prefix requires pagination and is expensive.
87
+ * Use resetKey() for individual keys instead.
88
+ */
89
+ resetAll() {
90
+ }
84
91
  };
85
92
  // Annotate the CommonJS export names for ESM import in node:
86
93
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/store/cloudflare-kv.ts"],"sourcesContent":["/**\n * Cloudflare KV store for rate limiting.\n *\n * Note: KV is eventually consistent (~60s propagation).\n * For strict rate limiting, consider Durable Objects.\n */\n\nimport type { RateLimitStore, StoreResult } from \"../index\";\n\n/**\n * Cloudflare KV Namespace interface\n */\nexport type KVNamespace = {\n get: <T = string>(\n key: string,\n options?: { type: \"json\" },\n ) => Promise<T | null>;\n put: (\n key: string,\n value: string,\n options?: { expirationTtl?: number },\n ) => Promise<void>;\n delete: (key: string) => Promise<void>;\n};\n\n/**\n * Options for Cloudflare KV store\n */\nexport type CloudflareKVStoreOptions = {\n /**\n * KV Namespace binding\n */\n namespace: KVNamespace;\n\n /**\n * Key prefix for rate limit entries\n * @default 'rl:'\n */\n prefix?: string;\n};\n\ntype KVEntry = {\n count: number;\n reset: number;\n};\n\n/**\n * Cloudflare KV store for rate limiting in Workers.\n *\n * @example\n * ```ts\n * import { rateLimiter } from 'hono-rate-limit'\n * import { CloudflareKVStore } from 'hono-rate-limit/store/cloudflare-kv'\n *\n * type Bindings = { RATE_LIMIT_KV: KVNamespace }\n *\n * app.use('*', async (c, next) => {\n * const limiter = rateLimiter({\n * store: new CloudflareKVStore({ namespace: c.env.RATE_LIMIT_KV }),\n * })\n * return limiter(c, next)\n * })\n * ```\n */\nexport class CloudflareKVStore implements RateLimitStore {\n private namespace: KVNamespace;\n private prefix: string;\n private windowMs = 60_000;\n\n constructor(options: CloudflareKVStoreOptions) {\n this.namespace = options.namespace;\n this.prefix = options.prefix ?? \"rl:\";\n }\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n }\n\n async increment(key: string): Promise<StoreResult> {\n const fullKey = `${this.prefix}${key}`;\n const now = Date.now();\n\n // KV minimum TTL is 60 seconds\n const ttlSeconds = Math.max(60, Math.ceil(this.windowMs / 1000));\n\n const existing = await this.namespace.get<KVEntry>(fullKey, {\n type: \"json\",\n });\n\n let count: number;\n let reset: number;\n\n if (!existing || existing.reset <= now) {\n // New window\n count = 1;\n reset = now + this.windowMs;\n } else {\n // Increment\n count = existing.count + 1;\n reset = existing.reset;\n }\n\n await this.namespace.put(fullKey, JSON.stringify({ count, reset }), {\n expirationTtl: ttlSeconds,\n });\n\n return { count, reset };\n }\n\n async get(key: string): Promise<StoreResult | undefined> {\n const fullKey = `${this.prefix}${key}`;\n const data = await this.namespace.get<KVEntry>(fullKey, { type: \"json\" });\n\n if (!data || data.reset <= Date.now()) {\n return undefined;\n }\n\n return { count: data.count, reset: data.reset };\n }\n\n async decrement(key: string): Promise<void> {\n const fullKey = `${this.prefix}${key}`;\n const now = Date.now();\n\n const existing = await this.namespace.get<KVEntry>(fullKey, {\n type: \"json\",\n });\n\n if (existing && existing.count > 0 && existing.reset > now) {\n const ttlSeconds = Math.max(60, Math.ceil((existing.reset - now) / 1000));\n await this.namespace.put(\n fullKey,\n JSON.stringify({ count: existing.count - 1, reset: existing.reset }),\n { expirationTtl: ttlSeconds },\n );\n }\n }\n\n async resetKey(key: string): Promise<void> {\n await this.namespace.delete(`${this.prefix}${key}`);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAgEO,IAAM,oBAAN,MAAkD;AAAA,EAC/C;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EAEnB,YAAY,SAAmC;AAC7C,SAAK,YAAY,QAAQ;AACzB,SAAK,SAAS,QAAQ,UAAU;AAAA,EAClC;AAAA,EAEA,KAAK,UAAwB;AAC3B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,UAAU,KAAmC;AACjD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,MAAM,KAAK,IAAI;AAGrB,UAAM,aAAa,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,WAAW,GAAI,CAAC;AAE/D,UAAM,WAAW,MAAM,KAAK,UAAU,IAAa,SAAS;AAAA,MAC1D,MAAM;AAAA,IACR,CAAC;AAED,QAAI;AACJ,QAAI;AAEJ,QAAI,CAAC,YAAY,SAAS,SAAS,KAAK;AAEtC,cAAQ;AACR,cAAQ,MAAM,KAAK;AAAA,IACrB,OAAO;AAEL,cAAQ,SAAS,QAAQ;AACzB,cAAQ,SAAS;AAAA,IACnB;AAEA,UAAM,KAAK,UAAU,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,MAAM,CAAC,GAAG;AAAA,MAClE,eAAe;AAAA,IACjB,CAAC;AAED,WAAO,EAAE,OAAO,MAAM;AAAA,EACxB;AAAA,EAEA,MAAM,IAAI,KAA+C;AACvD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,OAAO,MAAM,KAAK,UAAU,IAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAExE,QAAI,CAAC,QAAQ,KAAK,SAAS,KAAK,IAAI,GAAG;AACrC,aAAO;AAAA,IACT;AAEA,WAAO,EAAE,OAAO,KAAK,OAAO,OAAO,KAAK,MAAM;AAAA,EAChD;AAAA,EAEA,MAAM,UAAU,KAA4B;AAC1C,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,MAAM,KAAK,IAAI;AAErB,UAAM,WAAW,MAAM,KAAK,UAAU,IAAa,SAAS;AAAA,MAC1D,MAAM;AAAA,IACR,CAAC;AAED,QAAI,YAAY,SAAS,QAAQ,KAAK,SAAS,QAAQ,KAAK;AAC1D,YAAM,aAAa,KAAK,IAAI,IAAI,KAAK,MAAM,SAAS,QAAQ,OAAO,GAAI,CAAC;AACxE,YAAM,KAAK,UAAU;AAAA,QACnB;AAAA,QACA,KAAK,UAAU,EAAE,OAAO,SAAS,QAAQ,GAAG,OAAO,SAAS,MAAM,CAAC;AAAA,QACnE,EAAE,eAAe,WAAW;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,KAA4B;AACzC,UAAM,KAAK,UAAU,OAAO,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE;AAAA,EACpD;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/store/cloudflare-kv.ts"],"sourcesContent":["/**\n * Cloudflare KV store for rate limiting.\n *\n * Note: KV is eventually consistent (~60s propagation).\n * For strict rate limiting, consider Durable Objects.\n */\n\nimport type { RateLimitStore, StoreResult } from \"../index\";\n\n/**\n * Cloudflare KV Namespace interface\n */\nexport type KVNamespace = {\n get: <T = string>(\n key: string,\n options?: { type: \"json\" },\n ) => Promise<T | null>;\n put: (\n key: string,\n value: string,\n options?: { expirationTtl?: number },\n ) => Promise<void>;\n delete: (key: string) => Promise<void>;\n};\n\n/**\n * Options for Cloudflare KV store\n */\nexport type CloudflareKVStoreOptions = {\n /**\n * KV Namespace binding\n */\n namespace: KVNamespace;\n\n /**\n * Key prefix for rate limit entries\n * @default 'rl:'\n */\n prefix?: string;\n};\n\ntype KVEntry = {\n count: number;\n reset: number;\n};\n\n/**\n * Cloudflare KV store for rate limiting in Workers.\n *\n * @example\n * ```ts\n * import { rateLimiter } from 'hono-rate-limit'\n * import { CloudflareKVStore } from 'hono-rate-limit/store/cloudflare-kv'\n *\n * type Bindings = { RATE_LIMIT_KV: KVNamespace }\n *\n * app.use('*', async (c, next) => {\n * const limiter = rateLimiter({\n * store: new CloudflareKVStore({ namespace: c.env.RATE_LIMIT_KV }),\n * })\n * return limiter(c, next)\n * })\n * ```\n */\nexport class CloudflareKVStore implements RateLimitStore {\n private namespace: KVNamespace;\n private prefix: string;\n private windowMs = 60_000;\n\n constructor(options: CloudflareKVStoreOptions) {\n this.namespace = options.namespace;\n this.prefix = options.prefix ?? \"rl:\";\n }\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n }\n\n async increment(key: string): Promise<StoreResult> {\n const fullKey = `${this.prefix}${key}`;\n const now = Date.now();\n\n // KV minimum TTL is 60 seconds\n const ttlSeconds = Math.max(60, Math.ceil(this.windowMs / 1000));\n\n const existing = await this.namespace.get<KVEntry>(fullKey, {\n type: \"json\",\n });\n\n let count: number;\n let reset: number;\n\n if (!existing || existing.reset <= now) {\n // New window\n count = 1;\n reset = now + this.windowMs;\n } else {\n // Increment\n count = existing.count + 1;\n reset = existing.reset;\n }\n\n await this.namespace.put(fullKey, JSON.stringify({ count, reset }), {\n expirationTtl: ttlSeconds,\n });\n\n return { count, reset };\n }\n\n async get(key: string): Promise<StoreResult | undefined> {\n const fullKey = `${this.prefix}${key}`;\n const data = await this.namespace.get<KVEntry>(fullKey, { type: \"json\" });\n\n if (!data || data.reset <= Date.now()) {\n return undefined;\n }\n\n return { count: data.count, reset: data.reset };\n }\n\n async decrement(key: string): Promise<void> {\n const fullKey = `${this.prefix}${key}`;\n const now = Date.now();\n\n const existing = await this.namespace.get<KVEntry>(fullKey, {\n type: \"json\",\n });\n\n if (existing && existing.count > 0 && existing.reset > now) {\n const ttlSeconds = Math.max(60, Math.ceil((existing.reset - now) / 1000));\n await this.namespace.put(\n fullKey,\n JSON.stringify({ count: existing.count - 1, reset: existing.reset }),\n { expirationTtl: ttlSeconds },\n );\n }\n }\n\n async resetKey(key: string): Promise<void> {\n await this.namespace.delete(`${this.prefix}${key}`);\n }\n\n /**\n * Reset all keys. Note: This is a no-op for Cloudflare KV as listing\n * all keys with a prefix requires pagination and is expensive.\n * Use resetKey() for individual keys instead.\n */\n resetAll(): void {\n // No-op: Listing all keys with prefix is expensive in KV\n // Keys will expire naturally based on TTL\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAgEO,IAAM,oBAAN,MAAkD;AAAA,EAC/C;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EAEnB,YAAY,SAAmC;AAC7C,SAAK,YAAY,QAAQ;AACzB,SAAK,SAAS,QAAQ,UAAU;AAAA,EAClC;AAAA,EAEA,KAAK,UAAwB;AAC3B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,UAAU,KAAmC;AACjD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,MAAM,KAAK,IAAI;AAGrB,UAAM,aAAa,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,WAAW,GAAI,CAAC;AAE/D,UAAM,WAAW,MAAM,KAAK,UAAU,IAAa,SAAS;AAAA,MAC1D,MAAM;AAAA,IACR,CAAC;AAED,QAAI;AACJ,QAAI;AAEJ,QAAI,CAAC,YAAY,SAAS,SAAS,KAAK;AAEtC,cAAQ;AACR,cAAQ,MAAM,KAAK;AAAA,IACrB,OAAO;AAEL,cAAQ,SAAS,QAAQ;AACzB,cAAQ,SAAS;AAAA,IACnB;AAEA,UAAM,KAAK,UAAU,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,MAAM,CAAC,GAAG;AAAA,MAClE,eAAe;AAAA,IACjB,CAAC;AAED,WAAO,EAAE,OAAO,MAAM;AAAA,EACxB;AAAA,EAEA,MAAM,IAAI,KAA+C;AACvD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,OAAO,MAAM,KAAK,UAAU,IAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAExE,QAAI,CAAC,QAAQ,KAAK,SAAS,KAAK,IAAI,GAAG;AACrC,aAAO;AAAA,IACT;AAEA,WAAO,EAAE,OAAO,KAAK,OAAO,OAAO,KAAK,MAAM;AAAA,EAChD;AAAA,EAEA,MAAM,UAAU,KAA4B;AAC1C,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,MAAM,KAAK,IAAI;AAErB,UAAM,WAAW,MAAM,KAAK,UAAU,IAAa,SAAS;AAAA,MAC1D,MAAM;AAAA,IACR,CAAC;AAED,QAAI,YAAY,SAAS,QAAQ,KAAK,SAAS,QAAQ,KAAK;AAC1D,YAAM,aAAa,KAAK,IAAI,IAAI,KAAK,MAAM,SAAS,QAAQ,OAAO,GAAI,CAAC;AACxE,YAAM,KAAK,UAAU;AAAA,QACnB;AAAA,QACA,KAAK,UAAU,EAAE,OAAO,SAAS,QAAQ,GAAG,OAAO,SAAS,MAAM,CAAC;AAAA,QACnE,EAAE,eAAe,WAAW;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,KAA4B;AACzC,UAAM,KAAK,UAAU,OAAO,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE;AAAA,EACpD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAiB;AAAA,EAGjB;AACF;","names":[]}
@@ -62,6 +62,12 @@ declare class CloudflareKVStore implements RateLimitStore {
62
62
  get(key: string): Promise<StoreResult | undefined>;
63
63
  decrement(key: string): Promise<void>;
64
64
  resetKey(key: string): Promise<void>;
65
+ /**
66
+ * Reset all keys. Note: This is a no-op for Cloudflare KV as listing
67
+ * all keys with a prefix requires pagination and is expensive.
68
+ * Use resetKey() for individual keys instead.
69
+ */
70
+ resetAll(): void;
65
71
  }
66
72
 
67
73
  export { CloudflareKVStore, type CloudflareKVStoreOptions, type KVNamespace };
@@ -62,6 +62,12 @@ declare class CloudflareKVStore implements RateLimitStore {
62
62
  get(key: string): Promise<StoreResult | undefined>;
63
63
  decrement(key: string): Promise<void>;
64
64
  resetKey(key: string): Promise<void>;
65
+ /**
66
+ * Reset all keys. Note: This is a no-op for Cloudflare KV as listing
67
+ * all keys with a prefix requires pagination and is expensive.
68
+ * Use resetKey() for individual keys instead.
69
+ */
70
+ resetAll(): void;
65
71
  }
66
72
 
67
73
  export { CloudflareKVStore, type CloudflareKVStoreOptions, type KVNamespace };
@@ -57,6 +57,13 @@ var CloudflareKVStore = class {
57
57
  async resetKey(key) {
58
58
  await this.namespace.delete(`${this.prefix}${key}`);
59
59
  }
60
+ /**
61
+ * Reset all keys. Note: This is a no-op for Cloudflare KV as listing
62
+ * all keys with a prefix requires pagination and is expensive.
63
+ * Use resetKey() for individual keys instead.
64
+ */
65
+ resetAll() {
66
+ }
60
67
  };
61
68
  export {
62
69
  CloudflareKVStore
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/store/cloudflare-kv.ts"],"sourcesContent":["/**\n * Cloudflare KV store for rate limiting.\n *\n * Note: KV is eventually consistent (~60s propagation).\n * For strict rate limiting, consider Durable Objects.\n */\n\nimport type { RateLimitStore, StoreResult } from \"../index\";\n\n/**\n * Cloudflare KV Namespace interface\n */\nexport type KVNamespace = {\n get: <T = string>(\n key: string,\n options?: { type: \"json\" },\n ) => Promise<T | null>;\n put: (\n key: string,\n value: string,\n options?: { expirationTtl?: number },\n ) => Promise<void>;\n delete: (key: string) => Promise<void>;\n};\n\n/**\n * Options for Cloudflare KV store\n */\nexport type CloudflareKVStoreOptions = {\n /**\n * KV Namespace binding\n */\n namespace: KVNamespace;\n\n /**\n * Key prefix for rate limit entries\n * @default 'rl:'\n */\n prefix?: string;\n};\n\ntype KVEntry = {\n count: number;\n reset: number;\n};\n\n/**\n * Cloudflare KV store for rate limiting in Workers.\n *\n * @example\n * ```ts\n * import { rateLimiter } from 'hono-rate-limit'\n * import { CloudflareKVStore } from 'hono-rate-limit/store/cloudflare-kv'\n *\n * type Bindings = { RATE_LIMIT_KV: KVNamespace }\n *\n * app.use('*', async (c, next) => {\n * const limiter = rateLimiter({\n * store: new CloudflareKVStore({ namespace: c.env.RATE_LIMIT_KV }),\n * })\n * return limiter(c, next)\n * })\n * ```\n */\nexport class CloudflareKVStore implements RateLimitStore {\n private namespace: KVNamespace;\n private prefix: string;\n private windowMs = 60_000;\n\n constructor(options: CloudflareKVStoreOptions) {\n this.namespace = options.namespace;\n this.prefix = options.prefix ?? \"rl:\";\n }\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n }\n\n async increment(key: string): Promise<StoreResult> {\n const fullKey = `${this.prefix}${key}`;\n const now = Date.now();\n\n // KV minimum TTL is 60 seconds\n const ttlSeconds = Math.max(60, Math.ceil(this.windowMs / 1000));\n\n const existing = await this.namespace.get<KVEntry>(fullKey, {\n type: \"json\",\n });\n\n let count: number;\n let reset: number;\n\n if (!existing || existing.reset <= now) {\n // New window\n count = 1;\n reset = now + this.windowMs;\n } else {\n // Increment\n count = existing.count + 1;\n reset = existing.reset;\n }\n\n await this.namespace.put(fullKey, JSON.stringify({ count, reset }), {\n expirationTtl: ttlSeconds,\n });\n\n return { count, reset };\n }\n\n async get(key: string): Promise<StoreResult | undefined> {\n const fullKey = `${this.prefix}${key}`;\n const data = await this.namespace.get<KVEntry>(fullKey, { type: \"json\" });\n\n if (!data || data.reset <= Date.now()) {\n return undefined;\n }\n\n return { count: data.count, reset: data.reset };\n }\n\n async decrement(key: string): Promise<void> {\n const fullKey = `${this.prefix}${key}`;\n const now = Date.now();\n\n const existing = await this.namespace.get<KVEntry>(fullKey, {\n type: \"json\",\n });\n\n if (existing && existing.count > 0 && existing.reset > now) {\n const ttlSeconds = Math.max(60, Math.ceil((existing.reset - now) / 1000));\n await this.namespace.put(\n fullKey,\n JSON.stringify({ count: existing.count - 1, reset: existing.reset }),\n { expirationTtl: ttlSeconds },\n );\n }\n }\n\n async resetKey(key: string): Promise<void> {\n await this.namespace.delete(`${this.prefix}${key}`);\n }\n}\n"],"mappings":";AAgEO,IAAM,oBAAN,MAAkD;AAAA,EAC/C;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EAEnB,YAAY,SAAmC;AAC7C,SAAK,YAAY,QAAQ;AACzB,SAAK,SAAS,QAAQ,UAAU;AAAA,EAClC;AAAA,EAEA,KAAK,UAAwB;AAC3B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,UAAU,KAAmC;AACjD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,MAAM,KAAK,IAAI;AAGrB,UAAM,aAAa,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,WAAW,GAAI,CAAC;AAE/D,UAAM,WAAW,MAAM,KAAK,UAAU,IAAa,SAAS;AAAA,MAC1D,MAAM;AAAA,IACR,CAAC;AAED,QAAI;AACJ,QAAI;AAEJ,QAAI,CAAC,YAAY,SAAS,SAAS,KAAK;AAEtC,cAAQ;AACR,cAAQ,MAAM,KAAK;AAAA,IACrB,OAAO;AAEL,cAAQ,SAAS,QAAQ;AACzB,cAAQ,SAAS;AAAA,IACnB;AAEA,UAAM,KAAK,UAAU,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,MAAM,CAAC,GAAG;AAAA,MAClE,eAAe;AAAA,IACjB,CAAC;AAED,WAAO,EAAE,OAAO,MAAM;AAAA,EACxB;AAAA,EAEA,MAAM,IAAI,KAA+C;AACvD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,OAAO,MAAM,KAAK,UAAU,IAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAExE,QAAI,CAAC,QAAQ,KAAK,SAAS,KAAK,IAAI,GAAG;AACrC,aAAO;AAAA,IACT;AAEA,WAAO,EAAE,OAAO,KAAK,OAAO,OAAO,KAAK,MAAM;AAAA,EAChD;AAAA,EAEA,MAAM,UAAU,KAA4B;AAC1C,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,MAAM,KAAK,IAAI;AAErB,UAAM,WAAW,MAAM,KAAK,UAAU,IAAa,SAAS;AAAA,MAC1D,MAAM;AAAA,IACR,CAAC;AAED,QAAI,YAAY,SAAS,QAAQ,KAAK,SAAS,QAAQ,KAAK;AAC1D,YAAM,aAAa,KAAK,IAAI,IAAI,KAAK,MAAM,SAAS,QAAQ,OAAO,GAAI,CAAC;AACxE,YAAM,KAAK,UAAU;AAAA,QACnB;AAAA,QACA,KAAK,UAAU,EAAE,OAAO,SAAS,QAAQ,GAAG,OAAO,SAAS,MAAM,CAAC;AAAA,QACnE,EAAE,eAAe,WAAW;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,KAA4B;AACzC,UAAM,KAAK,UAAU,OAAO,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE;AAAA,EACpD;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/store/cloudflare-kv.ts"],"sourcesContent":["/**\n * Cloudflare KV store for rate limiting.\n *\n * Note: KV is eventually consistent (~60s propagation).\n * For strict rate limiting, consider Durable Objects.\n */\n\nimport type { RateLimitStore, StoreResult } from \"../index\";\n\n/**\n * Cloudflare KV Namespace interface\n */\nexport type KVNamespace = {\n get: <T = string>(\n key: string,\n options?: { type: \"json\" },\n ) => Promise<T | null>;\n put: (\n key: string,\n value: string,\n options?: { expirationTtl?: number },\n ) => Promise<void>;\n delete: (key: string) => Promise<void>;\n};\n\n/**\n * Options for Cloudflare KV store\n */\nexport type CloudflareKVStoreOptions = {\n /**\n * KV Namespace binding\n */\n namespace: KVNamespace;\n\n /**\n * Key prefix for rate limit entries\n * @default 'rl:'\n */\n prefix?: string;\n};\n\ntype KVEntry = {\n count: number;\n reset: number;\n};\n\n/**\n * Cloudflare KV store for rate limiting in Workers.\n *\n * @example\n * ```ts\n * import { rateLimiter } from 'hono-rate-limit'\n * import { CloudflareKVStore } from 'hono-rate-limit/store/cloudflare-kv'\n *\n * type Bindings = { RATE_LIMIT_KV: KVNamespace }\n *\n * app.use('*', async (c, next) => {\n * const limiter = rateLimiter({\n * store: new CloudflareKVStore({ namespace: c.env.RATE_LIMIT_KV }),\n * })\n * return limiter(c, next)\n * })\n * ```\n */\nexport class CloudflareKVStore implements RateLimitStore {\n private namespace: KVNamespace;\n private prefix: string;\n private windowMs = 60_000;\n\n constructor(options: CloudflareKVStoreOptions) {\n this.namespace = options.namespace;\n this.prefix = options.prefix ?? \"rl:\";\n }\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n }\n\n async increment(key: string): Promise<StoreResult> {\n const fullKey = `${this.prefix}${key}`;\n const now = Date.now();\n\n // KV minimum TTL is 60 seconds\n const ttlSeconds = Math.max(60, Math.ceil(this.windowMs / 1000));\n\n const existing = await this.namespace.get<KVEntry>(fullKey, {\n type: \"json\",\n });\n\n let count: number;\n let reset: number;\n\n if (!existing || existing.reset <= now) {\n // New window\n count = 1;\n reset = now + this.windowMs;\n } else {\n // Increment\n count = existing.count + 1;\n reset = existing.reset;\n }\n\n await this.namespace.put(fullKey, JSON.stringify({ count, reset }), {\n expirationTtl: ttlSeconds,\n });\n\n return { count, reset };\n }\n\n async get(key: string): Promise<StoreResult | undefined> {\n const fullKey = `${this.prefix}${key}`;\n const data = await this.namespace.get<KVEntry>(fullKey, { type: \"json\" });\n\n if (!data || data.reset <= Date.now()) {\n return undefined;\n }\n\n return { count: data.count, reset: data.reset };\n }\n\n async decrement(key: string): Promise<void> {\n const fullKey = `${this.prefix}${key}`;\n const now = Date.now();\n\n const existing = await this.namespace.get<KVEntry>(fullKey, {\n type: \"json\",\n });\n\n if (existing && existing.count > 0 && existing.reset > now) {\n const ttlSeconds = Math.max(60, Math.ceil((existing.reset - now) / 1000));\n await this.namespace.put(\n fullKey,\n JSON.stringify({ count: existing.count - 1, reset: existing.reset }),\n { expirationTtl: ttlSeconds },\n );\n }\n }\n\n async resetKey(key: string): Promise<void> {\n await this.namespace.delete(`${this.prefix}${key}`);\n }\n\n /**\n * Reset all keys. Note: This is a no-op for Cloudflare KV as listing\n * all keys with a prefix requires pagination and is expensive.\n * Use resetKey() for individual keys instead.\n */\n resetAll(): void {\n // No-op: Listing all keys with prefix is expensive in KV\n // Keys will expire naturally based on TTL\n }\n}\n"],"mappings":";AAgEO,IAAM,oBAAN,MAAkD;AAAA,EAC/C;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EAEnB,YAAY,SAAmC;AAC7C,SAAK,YAAY,QAAQ;AACzB,SAAK,SAAS,QAAQ,UAAU;AAAA,EAClC;AAAA,EAEA,KAAK,UAAwB;AAC3B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,UAAU,KAAmC;AACjD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,MAAM,KAAK,IAAI;AAGrB,UAAM,aAAa,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,WAAW,GAAI,CAAC;AAE/D,UAAM,WAAW,MAAM,KAAK,UAAU,IAAa,SAAS;AAAA,MAC1D,MAAM;AAAA,IACR,CAAC;AAED,QAAI;AACJ,QAAI;AAEJ,QAAI,CAAC,YAAY,SAAS,SAAS,KAAK;AAEtC,cAAQ;AACR,cAAQ,MAAM,KAAK;AAAA,IACrB,OAAO;AAEL,cAAQ,SAAS,QAAQ;AACzB,cAAQ,SAAS;AAAA,IACnB;AAEA,UAAM,KAAK,UAAU,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,MAAM,CAAC,GAAG;AAAA,MAClE,eAAe;AAAA,IACjB,CAAC;AAED,WAAO,EAAE,OAAO,MAAM;AAAA,EACxB;AAAA,EAEA,MAAM,IAAI,KAA+C;AACvD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,OAAO,MAAM,KAAK,UAAU,IAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAExE,QAAI,CAAC,QAAQ,KAAK,SAAS,KAAK,IAAI,GAAG;AACrC,aAAO;AAAA,IACT;AAEA,WAAO,EAAE,OAAO,KAAK,OAAO,OAAO,KAAK,MAAM;AAAA,EAChD;AAAA,EAEA,MAAM,UAAU,KAA4B;AAC1C,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,MAAM,KAAK,IAAI;AAErB,UAAM,WAAW,MAAM,KAAK,UAAU,IAAa,SAAS;AAAA,MAC1D,MAAM;AAAA,IACR,CAAC;AAED,QAAI,YAAY,SAAS,QAAQ,KAAK,SAAS,QAAQ,KAAK;AAC1D,YAAM,aAAa,KAAK,IAAI,IAAI,KAAK,MAAM,SAAS,QAAQ,OAAO,GAAI,CAAC;AACxE,YAAM,KAAK,UAAU;AAAA,QACnB;AAAA,QACA,KAAK,UAAU,EAAE,OAAO,SAAS,QAAQ,GAAG,OAAO,SAAS,MAAM,CAAC;AAAA,QACnE,EAAE,eAAe,WAAW;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,KAA4B;AACzC,UAAM,KAAK,UAAU,OAAO,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE;AAAA,EACpD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAiB;AAAA,EAGjB;AACF;","names":[]}
@@ -84,6 +84,13 @@ var RedisStore = class {
84
84
  async resetKey(key) {
85
85
  await this.client.del(`${this.prefix}${key}`);
86
86
  }
87
+ /**
88
+ * Reset all keys. Note: This is a no-op for Redis as scanning
89
+ * all keys with a prefix is expensive and not recommended.
90
+ * Use resetKey() for individual keys instead.
91
+ */
92
+ resetAll() {
93
+ }
87
94
  };
88
95
  // Annotate the CommonJS export names for ESM import in node:
89
96
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/store/redis.ts"],"sourcesContent":["/**\n * Redis store for rate limiting.\n * Compatible with ioredis, @upstash/redis, and similar clients.\n */\n\nimport type { RateLimitStore, StoreResult } from \"../index\";\n\n/**\n * Redis client interface\n */\nexport type RedisClient = {\n eval: (\n script: string,\n keys: string[],\n args: (string | number)[],\n ) => Promise<unknown> | unknown;\n get: (key: string) => Promise<string | null> | string | null;\n del: (...keys: string[]) => Promise<number> | number;\n};\n\n/**\n * Options for Redis store\n */\nexport type RedisStoreOptions = {\n /**\n * Redis client instance\n */\n client: RedisClient;\n\n /**\n * Key prefix for rate limit entries\n * @default 'rl:'\n */\n prefix?: string;\n};\n\n// Lua script for atomic increment with expiry\nconst INCR_SCRIPT = `\nlocal key = KEYS[1]\nlocal window = tonumber(ARGV[1])\nlocal now = tonumber(ARGV[2])\n\nlocal count = redis.call('INCR', key)\nif count == 1 then\n redis.call('PEXPIRE', key, window)\nend\n\nlocal ttl = redis.call('PTTL', key)\nlocal reset = now + ttl\n\nreturn {count, reset}\n`;\n\n/**\n * Redis store for distributed rate limiting.\n *\n * @example\n * ```ts\n * import { rateLimiter } from 'hono-rate-limit'\n * import { RedisStore } from 'hono-rate-limit/store/redis'\n * import Redis from 'ioredis'\n *\n * const redis = new Redis(process.env.REDIS_URL)\n *\n * app.use(rateLimiter({\n * store: new RedisStore({ client: redis }),\n * }))\n * ```\n */\nexport class RedisStore implements RateLimitStore {\n private client: RedisClient;\n private prefix: string;\n private windowMs = 60_000;\n\n constructor(options: RedisStoreOptions) {\n this.client = options.client;\n this.prefix = options.prefix ?? \"rl:\";\n }\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n }\n\n async increment(key: string): Promise<StoreResult> {\n const fullKey = `${this.prefix}${key}`;\n const now = Date.now();\n\n const result = (await this.client.eval(\n INCR_SCRIPT,\n [fullKey],\n [this.windowMs, now],\n )) as [number, number];\n\n return {\n count: result[0],\n reset: result[1],\n };\n }\n\n async get(key: string): Promise<StoreResult | undefined> {\n const fullKey = `${this.prefix}${key}`;\n const value = await this.client.get(fullKey);\n\n if (!value) {\n return undefined;\n }\n\n return {\n count: parseInt(value, 10),\n reset: Date.now() + this.windowMs,\n };\n }\n\n async decrement(key: string): Promise<void> {\n const fullKey = `${this.prefix}${key}`;\n await this.client.eval(\n 'local count = redis.call(\"GET\", KEYS[1]); if count and tonumber(count) > 0 then redis.call(\"DECR\", KEYS[1]) end',\n [fullKey],\n [],\n );\n }\n\n async resetKey(key: string): Promise<void> {\n await this.client.del(`${this.prefix}${key}`);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAqCA,IAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgCb,IAAM,aAAN,MAA2C;AAAA,EACxC;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EAEnB,YAAY,SAA4B;AACtC,SAAK,SAAS,QAAQ;AACtB,SAAK,SAAS,QAAQ,UAAU;AAAA,EAClC;AAAA,EAEA,KAAK,UAAwB;AAC3B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,UAAU,KAAmC;AACjD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,MAAM,KAAK,IAAI;AAErB,UAAM,SAAU,MAAM,KAAK,OAAO;AAAA,MAChC;AAAA,MACA,CAAC,OAAO;AAAA,MACR,CAAC,KAAK,UAAU,GAAG;AAAA,IACrB;AAEA,WAAO;AAAA,MACL,OAAO,OAAO,CAAC;AAAA,MACf,OAAO,OAAO,CAAC;AAAA,IACjB;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,KAA+C;AACvD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,QAAQ,MAAM,KAAK,OAAO,IAAI,OAAO;AAE3C,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL,OAAO,SAAS,OAAO,EAAE;AAAA,MACzB,OAAO,KAAK,IAAI,IAAI,KAAK;AAAA,IAC3B;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,KAA4B;AAC1C,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,KAAK,OAAO;AAAA,MAChB;AAAA,MACA,CAAC,OAAO;AAAA,MACR,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,KAA4B;AACzC,UAAM,KAAK,OAAO,IAAI,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE;AAAA,EAC9C;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/store/redis.ts"],"sourcesContent":["/**\n * Redis store for rate limiting.\n * Compatible with ioredis, @upstash/redis, and similar clients.\n */\n\nimport type { RateLimitStore, StoreResult } from \"../index\";\n\n/**\n * Redis client interface\n */\nexport type RedisClient = {\n eval: (\n script: string,\n keys: string[],\n args: (string | number)[],\n ) => Promise<unknown> | unknown;\n get: (key: string) => Promise<string | null> | string | null;\n del: (...keys: string[]) => Promise<number> | number;\n};\n\n/**\n * Options for Redis store\n */\nexport type RedisStoreOptions = {\n /**\n * Redis client instance\n */\n client: RedisClient;\n\n /**\n * Key prefix for rate limit entries\n * @default 'rl:'\n */\n prefix?: string;\n};\n\n// Lua script for atomic increment with expiry\nconst INCR_SCRIPT = `\nlocal key = KEYS[1]\nlocal window = tonumber(ARGV[1])\nlocal now = tonumber(ARGV[2])\n\nlocal count = redis.call('INCR', key)\nif count == 1 then\n redis.call('PEXPIRE', key, window)\nend\n\nlocal ttl = redis.call('PTTL', key)\nlocal reset = now + ttl\n\nreturn {count, reset}\n`;\n\n/**\n * Redis store for distributed rate limiting.\n *\n * @example\n * ```ts\n * import { rateLimiter } from 'hono-rate-limit'\n * import { RedisStore } from 'hono-rate-limit/store/redis'\n * import Redis from 'ioredis'\n *\n * const redis = new Redis(process.env.REDIS_URL)\n *\n * app.use(rateLimiter({\n * store: new RedisStore({ client: redis }),\n * }))\n * ```\n */\nexport class RedisStore implements RateLimitStore {\n private client: RedisClient;\n private prefix: string;\n private windowMs = 60_000;\n\n constructor(options: RedisStoreOptions) {\n this.client = options.client;\n this.prefix = options.prefix ?? \"rl:\";\n }\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n }\n\n async increment(key: string): Promise<StoreResult> {\n const fullKey = `${this.prefix}${key}`;\n const now = Date.now();\n\n const result = (await this.client.eval(\n INCR_SCRIPT,\n [fullKey],\n [this.windowMs, now],\n )) as [number, number];\n\n return {\n count: result[0],\n reset: result[1],\n };\n }\n\n async get(key: string): Promise<StoreResult | undefined> {\n const fullKey = `${this.prefix}${key}`;\n const value = await this.client.get(fullKey);\n\n if (!value) {\n return undefined;\n }\n\n return {\n count: parseInt(value, 10),\n reset: Date.now() + this.windowMs,\n };\n }\n\n async decrement(key: string): Promise<void> {\n const fullKey = `${this.prefix}${key}`;\n await this.client.eval(\n 'local count = redis.call(\"GET\", KEYS[1]); if count and tonumber(count) > 0 then redis.call(\"DECR\", KEYS[1]) end',\n [fullKey],\n [],\n );\n }\n\n async resetKey(key: string): Promise<void> {\n await this.client.del(`${this.prefix}${key}`);\n }\n\n /**\n * Reset all keys. Note: This is a no-op for Redis as scanning\n * all keys with a prefix is expensive and not recommended.\n * Use resetKey() for individual keys instead.\n */\n resetAll(): void {\n // No-op: Scanning all keys with prefix is expensive in Redis\n // Keys will expire naturally based on TTL\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAqCA,IAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgCb,IAAM,aAAN,MAA2C;AAAA,EACxC;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EAEnB,YAAY,SAA4B;AACtC,SAAK,SAAS,QAAQ;AACtB,SAAK,SAAS,QAAQ,UAAU;AAAA,EAClC;AAAA,EAEA,KAAK,UAAwB;AAC3B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,UAAU,KAAmC;AACjD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,MAAM,KAAK,IAAI;AAErB,UAAM,SAAU,MAAM,KAAK,OAAO;AAAA,MAChC;AAAA,MACA,CAAC,OAAO;AAAA,MACR,CAAC,KAAK,UAAU,GAAG;AAAA,IACrB;AAEA,WAAO;AAAA,MACL,OAAO,OAAO,CAAC;AAAA,MACf,OAAO,OAAO,CAAC;AAAA,IACjB;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,KAA+C;AACvD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,QAAQ,MAAM,KAAK,OAAO,IAAI,OAAO;AAE3C,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL,OAAO,SAAS,OAAO,EAAE;AAAA,MACzB,OAAO,KAAK,IAAI,IAAI,KAAK;AAAA,IAC3B;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,KAA4B;AAC1C,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,KAAK,OAAO;AAAA,MAChB;AAAA,MACA,CAAC,OAAO;AAAA,MACR,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,KAA4B;AACzC,UAAM,KAAK,OAAO,IAAI,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAiB;AAAA,EAGjB;AACF;","names":[]}
@@ -54,6 +54,12 @@ declare class RedisStore implements RateLimitStore {
54
54
  get(key: string): Promise<StoreResult | undefined>;
55
55
  decrement(key: string): Promise<void>;
56
56
  resetKey(key: string): Promise<void>;
57
+ /**
58
+ * Reset all keys. Note: This is a no-op for Redis as scanning
59
+ * all keys with a prefix is expensive and not recommended.
60
+ * Use resetKey() for individual keys instead.
61
+ */
62
+ resetAll(): void;
57
63
  }
58
64
 
59
65
  export { type RedisClient, RedisStore, type RedisStoreOptions };
@@ -54,6 +54,12 @@ declare class RedisStore implements RateLimitStore {
54
54
  get(key: string): Promise<StoreResult | undefined>;
55
55
  decrement(key: string): Promise<void>;
56
56
  resetKey(key: string): Promise<void>;
57
+ /**
58
+ * Reset all keys. Note: This is a no-op for Redis as scanning
59
+ * all keys with a prefix is expensive and not recommended.
60
+ * Use resetKey() for individual keys instead.
61
+ */
62
+ resetAll(): void;
57
63
  }
58
64
 
59
65
  export { type RedisClient, RedisStore, type RedisStoreOptions };
@@ -60,6 +60,13 @@ var RedisStore = class {
60
60
  async resetKey(key) {
61
61
  await this.client.del(`${this.prefix}${key}`);
62
62
  }
63
+ /**
64
+ * Reset all keys. Note: This is a no-op for Redis as scanning
65
+ * all keys with a prefix is expensive and not recommended.
66
+ * Use resetKey() for individual keys instead.
67
+ */
68
+ resetAll() {
69
+ }
63
70
  };
64
71
  export {
65
72
  RedisStore
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/store/redis.ts"],"sourcesContent":["/**\n * Redis store for rate limiting.\n * Compatible with ioredis, @upstash/redis, and similar clients.\n */\n\nimport type { RateLimitStore, StoreResult } from \"../index\";\n\n/**\n * Redis client interface\n */\nexport type RedisClient = {\n eval: (\n script: string,\n keys: string[],\n args: (string | number)[],\n ) => Promise<unknown> | unknown;\n get: (key: string) => Promise<string | null> | string | null;\n del: (...keys: string[]) => Promise<number> | number;\n};\n\n/**\n * Options for Redis store\n */\nexport type RedisStoreOptions = {\n /**\n * Redis client instance\n */\n client: RedisClient;\n\n /**\n * Key prefix for rate limit entries\n * @default 'rl:'\n */\n prefix?: string;\n};\n\n// Lua script for atomic increment with expiry\nconst INCR_SCRIPT = `\nlocal key = KEYS[1]\nlocal window = tonumber(ARGV[1])\nlocal now = tonumber(ARGV[2])\n\nlocal count = redis.call('INCR', key)\nif count == 1 then\n redis.call('PEXPIRE', key, window)\nend\n\nlocal ttl = redis.call('PTTL', key)\nlocal reset = now + ttl\n\nreturn {count, reset}\n`;\n\n/**\n * Redis store for distributed rate limiting.\n *\n * @example\n * ```ts\n * import { rateLimiter } from 'hono-rate-limit'\n * import { RedisStore } from 'hono-rate-limit/store/redis'\n * import Redis from 'ioredis'\n *\n * const redis = new Redis(process.env.REDIS_URL)\n *\n * app.use(rateLimiter({\n * store: new RedisStore({ client: redis }),\n * }))\n * ```\n */\nexport class RedisStore implements RateLimitStore {\n private client: RedisClient;\n private prefix: string;\n private windowMs = 60_000;\n\n constructor(options: RedisStoreOptions) {\n this.client = options.client;\n this.prefix = options.prefix ?? \"rl:\";\n }\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n }\n\n async increment(key: string): Promise<StoreResult> {\n const fullKey = `${this.prefix}${key}`;\n const now = Date.now();\n\n const result = (await this.client.eval(\n INCR_SCRIPT,\n [fullKey],\n [this.windowMs, now],\n )) as [number, number];\n\n return {\n count: result[0],\n reset: result[1],\n };\n }\n\n async get(key: string): Promise<StoreResult | undefined> {\n const fullKey = `${this.prefix}${key}`;\n const value = await this.client.get(fullKey);\n\n if (!value) {\n return undefined;\n }\n\n return {\n count: parseInt(value, 10),\n reset: Date.now() + this.windowMs,\n };\n }\n\n async decrement(key: string): Promise<void> {\n const fullKey = `${this.prefix}${key}`;\n await this.client.eval(\n 'local count = redis.call(\"GET\", KEYS[1]); if count and tonumber(count) > 0 then redis.call(\"DECR\", KEYS[1]) end',\n [fullKey],\n [],\n );\n }\n\n async resetKey(key: string): Promise<void> {\n await this.client.del(`${this.prefix}${key}`);\n }\n}\n"],"mappings":";AAqCA,IAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgCb,IAAM,aAAN,MAA2C;AAAA,EACxC;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EAEnB,YAAY,SAA4B;AACtC,SAAK,SAAS,QAAQ;AACtB,SAAK,SAAS,QAAQ,UAAU;AAAA,EAClC;AAAA,EAEA,KAAK,UAAwB;AAC3B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,UAAU,KAAmC;AACjD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,MAAM,KAAK,IAAI;AAErB,UAAM,SAAU,MAAM,KAAK,OAAO;AAAA,MAChC;AAAA,MACA,CAAC,OAAO;AAAA,MACR,CAAC,KAAK,UAAU,GAAG;AAAA,IACrB;AAEA,WAAO;AAAA,MACL,OAAO,OAAO,CAAC;AAAA,MACf,OAAO,OAAO,CAAC;AAAA,IACjB;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,KAA+C;AACvD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,QAAQ,MAAM,KAAK,OAAO,IAAI,OAAO;AAE3C,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL,OAAO,SAAS,OAAO,EAAE;AAAA,MACzB,OAAO,KAAK,IAAI,IAAI,KAAK;AAAA,IAC3B;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,KAA4B;AAC1C,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,KAAK,OAAO;AAAA,MAChB;AAAA,MACA,CAAC,OAAO;AAAA,MACR,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,KAA4B;AACzC,UAAM,KAAK,OAAO,IAAI,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE;AAAA,EAC9C;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/store/redis.ts"],"sourcesContent":["/**\n * Redis store for rate limiting.\n * Compatible with ioredis, @upstash/redis, and similar clients.\n */\n\nimport type { RateLimitStore, StoreResult } from \"../index\";\n\n/**\n * Redis client interface\n */\nexport type RedisClient = {\n eval: (\n script: string,\n keys: string[],\n args: (string | number)[],\n ) => Promise<unknown> | unknown;\n get: (key: string) => Promise<string | null> | string | null;\n del: (...keys: string[]) => Promise<number> | number;\n};\n\n/**\n * Options for Redis store\n */\nexport type RedisStoreOptions = {\n /**\n * Redis client instance\n */\n client: RedisClient;\n\n /**\n * Key prefix for rate limit entries\n * @default 'rl:'\n */\n prefix?: string;\n};\n\n// Lua script for atomic increment with expiry\nconst INCR_SCRIPT = `\nlocal key = KEYS[1]\nlocal window = tonumber(ARGV[1])\nlocal now = tonumber(ARGV[2])\n\nlocal count = redis.call('INCR', key)\nif count == 1 then\n redis.call('PEXPIRE', key, window)\nend\n\nlocal ttl = redis.call('PTTL', key)\nlocal reset = now + ttl\n\nreturn {count, reset}\n`;\n\n/**\n * Redis store for distributed rate limiting.\n *\n * @example\n * ```ts\n * import { rateLimiter } from 'hono-rate-limit'\n * import { RedisStore } from 'hono-rate-limit/store/redis'\n * import Redis from 'ioredis'\n *\n * const redis = new Redis(process.env.REDIS_URL)\n *\n * app.use(rateLimiter({\n * store: new RedisStore({ client: redis }),\n * }))\n * ```\n */\nexport class RedisStore implements RateLimitStore {\n private client: RedisClient;\n private prefix: string;\n private windowMs = 60_000;\n\n constructor(options: RedisStoreOptions) {\n this.client = options.client;\n this.prefix = options.prefix ?? \"rl:\";\n }\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n }\n\n async increment(key: string): Promise<StoreResult> {\n const fullKey = `${this.prefix}${key}`;\n const now = Date.now();\n\n const result = (await this.client.eval(\n INCR_SCRIPT,\n [fullKey],\n [this.windowMs, now],\n )) as [number, number];\n\n return {\n count: result[0],\n reset: result[1],\n };\n }\n\n async get(key: string): Promise<StoreResult | undefined> {\n const fullKey = `${this.prefix}${key}`;\n const value = await this.client.get(fullKey);\n\n if (!value) {\n return undefined;\n }\n\n return {\n count: parseInt(value, 10),\n reset: Date.now() + this.windowMs,\n };\n }\n\n async decrement(key: string): Promise<void> {\n const fullKey = `${this.prefix}${key}`;\n await this.client.eval(\n 'local count = redis.call(\"GET\", KEYS[1]); if count and tonumber(count) > 0 then redis.call(\"DECR\", KEYS[1]) end',\n [fullKey],\n [],\n );\n }\n\n async resetKey(key: string): Promise<void> {\n await this.client.del(`${this.prefix}${key}`);\n }\n\n /**\n * Reset all keys. Note: This is a no-op for Redis as scanning\n * all keys with a prefix is expensive and not recommended.\n * Use resetKey() for individual keys instead.\n */\n resetAll(): void {\n // No-op: Scanning all keys with prefix is expensive in Redis\n // Keys will expire naturally based on TTL\n }\n}\n"],"mappings":";AAqCA,IAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgCb,IAAM,aAAN,MAA2C;AAAA,EACxC;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EAEnB,YAAY,SAA4B;AACtC,SAAK,SAAS,QAAQ;AACtB,SAAK,SAAS,QAAQ,UAAU;AAAA,EAClC;AAAA,EAEA,KAAK,UAAwB;AAC3B,SAAK,WAAW;AAAA,EAClB;AAAA,EAEA,MAAM,UAAU,KAAmC;AACjD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,MAAM,KAAK,IAAI;AAErB,UAAM,SAAU,MAAM,KAAK,OAAO;AAAA,MAChC;AAAA,MACA,CAAC,OAAO;AAAA,MACR,CAAC,KAAK,UAAU,GAAG;AAAA,IACrB;AAEA,WAAO;AAAA,MACL,OAAO,OAAO,CAAC;AAAA,MACf,OAAO,OAAO,CAAC;AAAA,IACjB;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,KAA+C;AACvD,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,QAAQ,MAAM,KAAK,OAAO,IAAI,OAAO;AAE3C,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL,OAAO,SAAS,OAAO,EAAE;AAAA,MACzB,OAAO,KAAK,IAAI,IAAI,KAAK;AAAA,IAC3B;AAAA,EACF;AAAA,EAEA,MAAM,UAAU,KAA4B;AAC1C,UAAM,UAAU,GAAG,KAAK,MAAM,GAAG,GAAG;AACpC,UAAM,KAAK,OAAO;AAAA,MAChB;AAAA,MACA,CAAC,OAAO;AAAA,MACR,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,KAA4B;AACzC,UAAM,KAAK,OAAO,IAAI,GAAG,KAAK,MAAM,GAAG,GAAG,EAAE;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAiB;AAAA,EAGjB;AACF;","names":[]}