@geekmidas/rate-limit 0.1.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 ADDED
@@ -0,0 +1,504 @@
1
+ # @geekmidas/rate-limit
2
+
3
+ Flexible rate limiting library with support for multiple cache backends, customizable key generation, and full TypeScript support.
4
+
5
+ ## Features
6
+
7
+ - **Multiple Cache Backends**: Works with any cache implementation (Redis, In-Memory, etc.)
8
+ - **Flexible Key Generation**: Customize how clients are identified
9
+ - **Skip Logic**: Conditionally skip rate limiting for certain requests
10
+ - **Standard Headers**: Automatic rate limit header generation
11
+ - **Type-Safe**: Full TypeScript support with generic types
12
+ - **Customizable**: Configure limits, windows, messages, and handlers
13
+ - **IP Detection**: Automatic client IP detection from various headers
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pnpm add @geekmidas/rate-limit
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### Basic Usage
24
+
25
+ ```typescript
26
+ import { checkRateLimit } from '@geekmidas/rate-limit';
27
+ import { InMemoryCache } from '@geekmidas/cache/memory';
28
+
29
+ const config = {
30
+ limit: 10, // 10 requests
31
+ windowMs: 60000, // per 1 minute
32
+ cache: new InMemoryCache(), // storage backend
33
+ };
34
+
35
+ // In your handler
36
+ try {
37
+ const info = await checkRateLimit(config, {
38
+ header: (key) => request.headers.get(key),
39
+ services: {},
40
+ logger,
41
+ session: null,
42
+ path: '/api/users',
43
+ method: 'GET'
44
+ });
45
+
46
+ // Request allowed
47
+ console.log(`${info.remaining} requests remaining`);
48
+ } catch (error) {
49
+ if (error instanceof TooManyRequestsError) {
50
+ // Rate limit exceeded
51
+ console.log(`Try again in ${error.retryAfter} seconds`);
52
+ }
53
+ }
54
+ ```
55
+
56
+ ### With Endpoints
57
+
58
+ Rate limiting is built into the constructs package:
59
+
60
+ ```typescript
61
+ import { e } from '@geekmidas/constructs/endpoints';
62
+ import { InMemoryCache } from '@geekmidas/cache/memory';
63
+ import { z } from 'zod';
64
+
65
+ export const sendMessage = e
66
+ .post('/api/messages')
67
+ .rateLimit({
68
+ limit: 10, // 10 requests
69
+ windowMs: 60000, // per minute
70
+ cache: new InMemoryCache(),
71
+ message: 'Too many messages sent. Please try again later.'
72
+ })
73
+ .body(z.object({
74
+ content: z.string()
75
+ }))
76
+ .handle(async ({ body }) => {
77
+ // Rate limited to 10 requests per minute
78
+ return { success: true };
79
+ });
80
+ ```
81
+
82
+ ## Configuration
83
+
84
+ ### RateLimitConfig
85
+
86
+ ```typescript
87
+ interface RateLimitConfig {
88
+ limit: number; // Max requests in window
89
+ windowMs: number; // Time window in milliseconds
90
+ cache: Cache; // Cache backend
91
+ keyGenerator?: RateLimitKeyGenerator; // Custom key generation
92
+ skip?: RateLimitSkipFn; // Skip certain requests
93
+ message?: string; // Custom error message
94
+ handler?: RateLimitExceededHandler; // Custom exceeded handler
95
+ standardHeaders?: boolean; // Include standard headers (default: true)
96
+ legacyHeaders?: boolean; // Include legacy headers (default: false)
97
+ }
98
+ ```
99
+
100
+ ## Cache Backends
101
+
102
+ ### In-Memory Cache
103
+
104
+ Best for development and single-instance deployments:
105
+
106
+ ```typescript
107
+ import { InMemoryCache } from '@geekmidas/cache/memory';
108
+
109
+ const config = {
110
+ limit: 100,
111
+ windowMs: 60000,
112
+ cache: new InMemoryCache()
113
+ };
114
+ ```
115
+
116
+ ### Redis (Upstash)
117
+
118
+ Best for production and distributed systems:
119
+
120
+ ```typescript
121
+ import { UpstashCache } from '@geekmidas/cache/upstash';
122
+
123
+ const config = {
124
+ limit: 100,
125
+ windowMs: 60000,
126
+ cache: new UpstashCache({
127
+ url: process.env.UPSTASH_REDIS_URL,
128
+ token: process.env.UPSTASH_REDIS_TOKEN
129
+ })
130
+ };
131
+ ```
132
+
133
+ ## Custom Key Generation
134
+
135
+ ### By User ID
136
+
137
+ ```typescript
138
+ const config = {
139
+ limit: 100,
140
+ windowMs: 60000,
141
+ cache: new InMemoryCache(),
142
+ keyGenerator: (ctx) => {
143
+ const userId = ctx.session?.userId || 'anonymous';
144
+ return `rate-limit:${ctx.method}:${ctx.path}:${userId}`;
145
+ }
146
+ };
147
+ ```
148
+
149
+ ### By API Key
150
+
151
+ ```typescript
152
+ const config = {
153
+ limit: 1000,
154
+ windowMs: 3600000, // 1 hour
155
+ cache: new InMemoryCache(),
156
+ keyGenerator: (ctx) => {
157
+ const apiKey = ctx.header('x-api-key') || 'unknown';
158
+ return `rate-limit:${ctx.path}:${apiKey}`;
159
+ }
160
+ };
161
+ ```
162
+
163
+ ### By IP and User Agent
164
+
165
+ ```typescript
166
+ const config = {
167
+ limit: 50,
168
+ windowMs: 60000,
169
+ cache: new InMemoryCache(),
170
+ keyGenerator: (ctx) => {
171
+ const ip = ctx.header('x-forwarded-for')?.split(',')[0] || 'unknown';
172
+ const userAgent = ctx.header('user-agent') || 'unknown';
173
+ return `rate-limit:${ip}:${userAgent}`;
174
+ }
175
+ };
176
+ ```
177
+
178
+ ## Skip Logic
179
+
180
+ ### Skip Authenticated Users
181
+
182
+ ```typescript
183
+ const config = {
184
+ limit: 10,
185
+ windowMs: 60000,
186
+ cache: new InMemoryCache(),
187
+ skip: (ctx) => {
188
+ // Skip rate limiting for authenticated users
189
+ return !!ctx.session?.userId;
190
+ }
191
+ };
192
+ ```
193
+
194
+ ### Skip Admin Routes
195
+
196
+ ```typescript
197
+ const config = {
198
+ limit: 100,
199
+ windowMs: 60000,
200
+ cache: new InMemoryCache(),
201
+ skip: (ctx) => {
202
+ // Skip rate limiting for admin routes
203
+ return ctx.path.startsWith('/admin/');
204
+ }
205
+ };
206
+ ```
207
+
208
+ ### Skip Internal IPs
209
+
210
+ ```typescript
211
+ const config = {
212
+ limit: 10,
213
+ windowMs: 60000,
214
+ cache: new InMemoryCache(),
215
+ skip: (ctx) => {
216
+ const ip = ctx.header('x-forwarded-for')?.split(',')[0];
217
+ // Skip rate limiting for internal IPs
218
+ return ip?.startsWith('10.') || ip?.startsWith('192.168.');
219
+ }
220
+ };
221
+ ```
222
+
223
+ ## Custom Handlers
224
+
225
+ ### Log Rate Limit Violations
226
+
227
+ ```typescript
228
+ const config = {
229
+ limit: 100,
230
+ windowMs: 60000,
231
+ cache: new InMemoryCache(),
232
+ handler: async (ctx, info) => {
233
+ ctx.logger.warn({
234
+ path: ctx.path,
235
+ method: ctx.method,
236
+ count: info.count,
237
+ limit: info.limit,
238
+ ip: ctx.header('x-forwarded-for')
239
+ }, 'Rate limit exceeded');
240
+ }
241
+ };
242
+ ```
243
+
244
+ ### Alert on Abuse
245
+
246
+ ```typescript
247
+ const config = {
248
+ limit: 100,
249
+ windowMs: 60000,
250
+ cache: new InMemoryCache(),
251
+ handler: async (ctx, info) => {
252
+ // Alert if significantly over limit
253
+ if (info.count > info.limit * 2) {
254
+ await alertService.send({
255
+ type: 'rate_limit_abuse',
256
+ ip: ctx.header('x-forwarded-for'),
257
+ count: info.count,
258
+ path: ctx.path
259
+ });
260
+ }
261
+ }
262
+ };
263
+ ```
264
+
265
+ ## Response Headers
266
+
267
+ The library automatically sets standard rate limit headers:
268
+
269
+ ```
270
+ X-RateLimit-Limit: 100
271
+ X-RateLimit-Remaining: 95
272
+ X-RateLimit-Reset: 2024-01-15T12:34:56.789Z
273
+ Retry-After: 45
274
+ ```
275
+
276
+ ### Disable Headers
277
+
278
+ ```typescript
279
+ const config = {
280
+ limit: 100,
281
+ windowMs: 60000,
282
+ cache: new InMemoryCache(),
283
+ standardHeaders: false // Disable standard headers
284
+ };
285
+ ```
286
+
287
+ ### Enable Legacy Headers
288
+
289
+ ```typescript
290
+ const config = {
291
+ limit: 100,
292
+ windowMs: 60000,
293
+ cache: new InMemoryCache(),
294
+ legacyHeaders: true // Enable legacy headers
295
+ };
296
+ ```
297
+
298
+ ## Rate Limit Info
299
+
300
+ The `checkRateLimit` function returns information about the current rate limit status:
301
+
302
+ ```typescript
303
+ interface RateLimitInfo {
304
+ count: number; // Current request count
305
+ limit: number; // Max allowed requests
306
+ remaining: number; // Remaining requests
307
+ resetTime: number; // Unix timestamp when window resets
308
+ retryAfter: number; // Milliseconds until reset
309
+ }
310
+
311
+ const info = await checkRateLimit(config, ctx);
312
+ console.log(`You have ${info.remaining} requests remaining`);
313
+ console.log(`Rate limit resets at ${new Date(info.resetTime)}`);
314
+ ```
315
+
316
+ ## Error Handling
317
+
318
+ ### TooManyRequestsError
319
+
320
+ Thrown when rate limit is exceeded:
321
+
322
+ ```typescript
323
+ import { TooManyRequestsError } from '@geekmidas/rate-limit';
324
+
325
+ try {
326
+ await checkRateLimit(config, ctx);
327
+ } catch (error) {
328
+ if (error instanceof TooManyRequestsError) {
329
+ console.log(`Status: ${error.statusCode}`); // 429
330
+ console.log(`Message: ${error.message}`);
331
+ console.log(`Retry after: ${error.retryAfter} seconds`);
332
+
333
+ // Send appropriate response
334
+ return new Response(error.message, {
335
+ status: error.statusCode,
336
+ headers: {
337
+ 'Retry-After': error.retryAfter?.toString() || '60'
338
+ }
339
+ });
340
+ }
341
+ }
342
+ ```
343
+
344
+ ## Common Patterns
345
+
346
+ ### Different Limits for Different Endpoints
347
+
348
+ ```typescript
349
+ // Strict limit for auth endpoints
350
+ export const login = e
351
+ .post('/auth/login')
352
+ .rateLimit({
353
+ limit: 5,
354
+ windowMs: 300000, // 5 minutes
355
+ cache: new InMemoryCache(),
356
+ message: 'Too many login attempts. Please try again later.'
357
+ })
358
+ .handle(async () => {
359
+ // Login logic
360
+ });
361
+
362
+ // Generous limit for read endpoints
363
+ export const getUsers = e
364
+ .get('/users')
365
+ .rateLimit({
366
+ limit: 1000,
367
+ windowMs: 60000, // 1 minute
368
+ cache: new InMemoryCache()
369
+ })
370
+ .handle(async () => {
371
+ // Get users logic
372
+ });
373
+ ```
374
+
375
+ ### Tiered Rate Limits
376
+
377
+ ```typescript
378
+ const config = {
379
+ limit: 100,
380
+ windowMs: 60000,
381
+ cache: new InMemoryCache(),
382
+ keyGenerator: (ctx) => {
383
+ // Different limits based on user tier
384
+ const tier = ctx.session?.tier || 'free';
385
+ const limit = tier === 'premium' ? 1000 : 100;
386
+
387
+ return `rate-limit:${tier}:${ctx.path}:${ctx.session?.userId}`;
388
+ }
389
+ };
390
+ ```
391
+
392
+ ### Per-API-Key Limits
393
+
394
+ ```typescript
395
+ const config = {
396
+ limit: 10000,
397
+ windowMs: 3600000, // 1 hour
398
+ cache: new UpstashCache({ /* config */ }),
399
+ keyGenerator: (ctx) => {
400
+ const apiKey = ctx.header('x-api-key');
401
+ if (!apiKey) {
402
+ throw new Error('API key required');
403
+ }
404
+ return `rate-limit:api-key:${apiKey}`;
405
+ },
406
+ message: 'API rate limit exceeded. Upgrade your plan for higher limits.'
407
+ };
408
+ ```
409
+
410
+ ## Testing
411
+
412
+ Mock the cache for testing:
413
+
414
+ ```typescript
415
+ import { checkRateLimit, type RateLimitData } from '@geekmidas/rate-limit';
416
+ import { vi } from 'vitest';
417
+
418
+ const mockCache = {
419
+ get: vi.fn(),
420
+ set: vi.fn(),
421
+ delete: vi.fn()
422
+ };
423
+
424
+ const config = {
425
+ limit: 10,
426
+ windowMs: 60000,
427
+ cache: mockCache as any
428
+ };
429
+
430
+ // Test rate limit not exceeded
431
+ mockCache.get.mockResolvedValue({ count: 5, resetTime: Date.now() + 30000 });
432
+
433
+ const info = await checkRateLimit(config, ctx);
434
+ expect(info.remaining).toBe(5);
435
+
436
+ // Test rate limit exceeded
437
+ mockCache.get.mockResolvedValue({ count: 11, resetTime: Date.now() + 30000 });
438
+
439
+ await expect(checkRateLimit(config, ctx)).rejects.toThrow(TooManyRequestsError);
440
+ ```
441
+
442
+ ## Best Practices
443
+
444
+ ### 1. Use Redis in Production
445
+
446
+ ```typescript
447
+ // ✅ Use Redis for distributed systems
448
+ import { UpstashCache } from '@geekmidas/cache/upstash';
449
+
450
+ const cache = new UpstashCache({ /* config */ });
451
+
452
+ // ❌ Don't use in-memory cache in production
453
+ const cache = new InMemoryCache(); // Only for dev/testing
454
+ ```
455
+
456
+ ### 2. Set Appropriate Limits
457
+
458
+ ```typescript
459
+ // ✅ Different limits for different operations
460
+ const authConfig = { limit: 5, windowMs: 300000 }; // 5 per 5 min
461
+ const readConfig = { limit: 1000, windowMs: 60000 }; // 1000 per min
462
+
463
+ // ❌ Don't use same limit for everything
464
+ const config = { limit: 100, windowMs: 60000 }; // Too generic
465
+ ```
466
+
467
+ ### 3. Include Helpful Error Messages
468
+
469
+ ```typescript
470
+ // ✅ Clear, actionable messages
471
+ message: 'Rate limit exceeded. You can make 100 requests per minute. Please try again in 45 seconds.'
472
+
473
+ // ❌ Generic messages
474
+ message: 'Too many requests'
475
+ ```
476
+
477
+ ### 4. Log Rate Limit Events
478
+
479
+ ```typescript
480
+ const config = {
481
+ limit: 100,
482
+ windowMs: 60000,
483
+ cache,
484
+ handler: async (ctx, info) => {
485
+ ctx.logger.warn({
486
+ event: 'rate_limit_exceeded',
487
+ path: ctx.path,
488
+ count: info.count,
489
+ limit: info.limit
490
+ }, 'Rate limit exceeded');
491
+ }
492
+ };
493
+ ```
494
+
495
+ ## Related Packages
496
+
497
+ - [@geekmidas/cache](../cache) - Cache implementations for rate limiting
498
+ - [@geekmidas/constructs](../constructs) - Built-in rate limiting for endpoints
499
+ - [@geekmidas/errors](../errors) - HTTP error classes
500
+ - [@geekmidas/logger](../logger) - Structured logging
501
+
502
+ ## License
503
+
504
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,88 @@
1
+
2
+ //#region src/index.ts
3
+ /**
4
+ * Error thrown when rate limit is exceeded
5
+ */
6
+ var TooManyRequestsError = class extends Error {
7
+ statusCode = 429;
8
+ retryAfter;
9
+ constructor(message, retryAfter) {
10
+ super(message || "Too many requests, please try again later.");
11
+ this.name = "TooManyRequestsError";
12
+ this.retryAfter = retryAfter;
13
+ }
14
+ };
15
+ /**
16
+ * Default key generator using IP address
17
+ */
18
+ const defaultKeyGenerator = (ctx) => {
19
+ const ip = ctx.header("x-forwarded-for")?.split(",")[0]?.trim() || ctx.header("x-real-ip") || ctx.header("x-client-ip") || ctx.header("cf-connecting-ip") || "unknown";
20
+ return `rate-limit:${ctx.method}:${ctx.path}:${ip}`;
21
+ };
22
+ /**
23
+ * Check rate limit and throw error if exceeded
24
+ */
25
+ async function checkRateLimit(config, ctx) {
26
+ if (config.skip && await config.skip(ctx)) return {
27
+ count: 0,
28
+ limit: config.limit,
29
+ remaining: config.limit,
30
+ resetTime: Date.now() + config.windowMs,
31
+ retryAfter: config.windowMs
32
+ };
33
+ const keyGenerator = config.keyGenerator || defaultKeyGenerator;
34
+ const key = await keyGenerator(ctx);
35
+ const now = Date.now();
36
+ let data = await config.cache.get(key);
37
+ if (!data || data.resetTime <= now) {
38
+ const resetTime = now + config.windowMs;
39
+ data = {
40
+ count: 1,
41
+ resetTime
42
+ };
43
+ const ttlSeconds = Math.ceil(config.windowMs / 1e3);
44
+ await config.cache.set(key, data, ttlSeconds);
45
+ } else {
46
+ data.count++;
47
+ const remainingMs = data.resetTime - now;
48
+ const ttlSeconds = Math.ceil(remainingMs / 1e3);
49
+ await config.cache.set(key, data, ttlSeconds);
50
+ }
51
+ const info = {
52
+ count: data.count,
53
+ limit: config.limit,
54
+ remaining: Math.max(0, config.limit - data.count),
55
+ resetTime: data.resetTime,
56
+ retryAfter: data.resetTime - now
57
+ };
58
+ if (data.count > config.limit) {
59
+ if (config.handler) await config.handler(ctx, info);
60
+ const retryAfterSeconds = Math.ceil(info.retryAfter / 1e3);
61
+ throw new TooManyRequestsError(config.message || "Too many requests, please try again later.", retryAfterSeconds);
62
+ }
63
+ return info;
64
+ }
65
+ /**
66
+ * Generate rate limit headers
67
+ */
68
+ function getRateLimitHeaders(info, config) {
69
+ const headers = {};
70
+ if (config.standardHeaders !== false) {
71
+ headers["X-RateLimit-Limit"] = info.limit.toString();
72
+ headers["X-RateLimit-Remaining"] = info.remaining.toString();
73
+ headers["X-RateLimit-Reset"] = new Date(info.resetTime).toISOString();
74
+ }
75
+ if (config.legacyHeaders) {
76
+ headers["X-RateLimit-Retry-After"] = info.retryAfter.toString();
77
+ headers["X-RateLimit-Reset-After"] = Math.ceil(info.retryAfter / 1e3).toString();
78
+ }
79
+ if (info.remaining === 0) headers["Retry-After"] = Math.ceil(info.retryAfter / 1e3).toString();
80
+ return headers;
81
+ }
82
+
83
+ //#endregion
84
+ exports.TooManyRequestsError = TooManyRequestsError;
85
+ exports.checkRateLimit = checkRateLimit;
86
+ exports.defaultKeyGenerator = defaultKeyGenerator;
87
+ exports.getRateLimitHeaders = getRateLimitHeaders;
88
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["message?: string","retryAfter?: number","defaultKeyGenerator: RateLimitKeyGenerator","config: RateLimitConfig<RateLimitData>","ctx: RateLimitContext<TServices, TLogger, TSession>","info: RateLimitInfo","config: RateLimitConfig","headers: RateLimitHeaders"],"sources":["../src/index.ts"],"sourcesContent":["import type { Cache } from '@geekmidas/cache';\nimport type { Logger } from '@geekmidas/logger';\nimport type { Service, ServiceRecord } from '@geekmidas/services';\n\n/**\n * Error thrown when rate limit is exceeded\n */\nexport class TooManyRequestsError extends Error {\n public readonly statusCode = 429;\n public readonly retryAfter?: number;\n\n constructor(message?: string, retryAfter?: number) {\n super(message || 'Too many requests, please try again later.');\n this.name = 'TooManyRequestsError';\n this.retryAfter = retryAfter;\n }\n}\n\n/**\n * Rate limit configuration for an endpoint\n */\nexport interface RateLimitConfig<T = RateLimitData> {\n /**\n * Maximum number of requests allowed in the window\n */\n limit: number;\n\n /**\n * Time window in milliseconds\n */\n windowMs: number;\n\n /**\n * Cache instance to store rate limit data\n */\n cache: Cache<T>;\n\n /**\n * Key generator function to identify clients\n * Defaults to using IP address\n */\n keyGenerator?: RateLimitKeyGenerator;\n\n /**\n * Skip rate limiting for certain requests\n */\n skip?: RateLimitSkipFn;\n\n /**\n * Optional message to return when rate limit is exceeded\n */\n message?: string;\n\n /**\n * Optional custom handler when rate limit is exceeded\n */\n handler?: RateLimitExceededHandler;\n\n /**\n * Whether to include rate limit headers in response\n * @default true\n */\n standardHeaders?: boolean;\n\n /**\n * Whether to include legacy rate limit headers\n * @default false\n */\n legacyHeaders?: boolean;\n}\n\n/**\n * Context for rate limiting decisions\n */\nexport interface RateLimitContext<\n TServices extends Service[] = [],\n TLogger extends Logger = Logger,\n TSession = unknown,\n> {\n header: (key: string) => string | undefined;\n services: ServiceRecord<TServices>;\n logger: TLogger;\n session: TSession;\n path: string;\n method: string;\n}\n\n/**\n * Function to generate a unique key for rate limiting\n */\nexport type RateLimitKeyGenerator<\n TServices extends Service[] = [],\n TLogger extends Logger = Logger,\n TSession = unknown,\n> = (\n ctx: RateLimitContext<TServices, TLogger, TSession>,\n) => string | Promise<string>;\n\n/**\n * Function to determine if rate limiting should be skipped\n */\nexport type RateLimitSkipFn<\n TServices extends Service[] = [],\n TLogger extends Logger = Logger,\n TSession = unknown,\n> = (\n ctx: RateLimitContext<TServices, TLogger, TSession>,\n) => boolean | Promise<boolean>;\n\n/**\n * Handler for when rate limit is exceeded\n */\nexport type RateLimitExceededHandler<\n TServices extends Service[] = [],\n TLogger extends Logger = Logger,\n TSession = unknown,\n> = (\n ctx: RateLimitContext<TServices, TLogger, TSession>,\n info: RateLimitInfo,\n) => void | Promise<void>;\n\n/**\n * Information about current rate limit status\n */\nexport interface RateLimitInfo {\n /**\n * Current request count in the window\n */\n count: number;\n\n /**\n * Maximum allowed requests\n */\n limit: number;\n\n /**\n * Remaining requests allowed\n */\n remaining: number;\n\n /**\n * Time when the window resets (Unix timestamp)\n */\n resetTime: number;\n\n /**\n * Time until reset in milliseconds\n */\n retryAfter: number;\n}\n\n/**\n * Headers to be set on responses\n */\nexport interface RateLimitHeaders {\n 'X-RateLimit-Limit'?: string;\n 'X-RateLimit-Remaining'?: string;\n 'X-RateLimit-Reset'?: string;\n 'Retry-After'?: string;\n 'X-RateLimit-Retry-After'?: string;\n 'X-RateLimit-Reset-After'?: string;\n}\n\n/**\n * Data stored in cache for rate limiting\n */\nexport interface RateLimitData {\n count: number;\n resetTime: number;\n}\n\n/**\n * Default key generator using IP address\n */\nexport const defaultKeyGenerator: RateLimitKeyGenerator = (ctx) => {\n // Try various headers for IP address\n const ip =\n ctx.header('x-forwarded-for')?.split(',')[0]?.trim() ||\n ctx.header('x-real-ip') ||\n ctx.header('x-client-ip') ||\n ctx.header('cf-connecting-ip') ||\n 'unknown';\n\n return `rate-limit:${ctx.method}:${ctx.path}:${ip}`;\n};\n\n/**\n * Check rate limit and throw error if exceeded\n */\nexport async function checkRateLimit<\n TServices extends Service[] = [],\n TLogger extends Logger = Logger,\n TSession = unknown,\n>(\n config: RateLimitConfig<RateLimitData>,\n ctx: RateLimitContext<TServices, TLogger, TSession>,\n): Promise<RateLimitInfo> {\n // Check if we should skip rate limiting\n if (config.skip && (await config.skip(ctx))) {\n return {\n count: 0,\n limit: config.limit,\n remaining: config.limit,\n resetTime: Date.now() + config.windowMs,\n retryAfter: config.windowMs,\n };\n }\n\n // Generate key for this request\n const keyGenerator = config.keyGenerator || defaultKeyGenerator;\n const key = await keyGenerator(ctx);\n\n // Get current data from cache\n const now = Date.now();\n let data = await config.cache.get(key);\n\n // If no data or window expired, create new entry\n if (!data || data.resetTime <= now) {\n const resetTime = now + config.windowMs;\n data = { count: 1, resetTime };\n\n // Store with TTL matching the window\n const ttlSeconds = Math.ceil(config.windowMs / 1000);\n await config.cache.set(key, data, ttlSeconds);\n } else {\n // Increment count\n data.count++;\n\n // Calculate remaining TTL\n const remainingMs = data.resetTime - now;\n const ttlSeconds = Math.ceil(remainingMs / 1000);\n await config.cache.set(key, data, ttlSeconds);\n }\n\n // Calculate rate limit info\n const info: RateLimitInfo = {\n count: data.count,\n limit: config.limit,\n remaining: Math.max(0, config.limit - data.count),\n resetTime: data.resetTime,\n retryAfter: data.resetTime - now,\n };\n\n // Check if limit exceeded\n if (data.count > config.limit) {\n // Call custom handler if provided\n if (config.handler) {\n await config.handler(ctx, info);\n }\n\n // Throw rate limit error\n const retryAfterSeconds = Math.ceil(info.retryAfter / 1000);\n throw new TooManyRequestsError(\n config.message || 'Too many requests, please try again later.',\n retryAfterSeconds,\n );\n }\n\n return info;\n}\n\n/**\n * Generate rate limit headers\n */\nexport function getRateLimitHeaders(\n info: RateLimitInfo,\n config: RateLimitConfig,\n): RateLimitHeaders {\n const headers: RateLimitHeaders = {};\n\n if (config.standardHeaders !== false) {\n headers['X-RateLimit-Limit'] = info.limit.toString();\n headers['X-RateLimit-Remaining'] = info.remaining.toString();\n headers['X-RateLimit-Reset'] = new Date(info.resetTime).toISOString();\n }\n\n if (config.legacyHeaders) {\n headers['X-RateLimit-Retry-After'] = info.retryAfter.toString();\n headers['X-RateLimit-Reset-After'] = Math.ceil(\n info.retryAfter / 1000,\n ).toString();\n }\n\n // Always set Retry-After when limit is exceeded\n if (info.remaining === 0) {\n headers['Retry-After'] = Math.ceil(info.retryAfter / 1000).toString();\n }\n\n return headers;\n}\n"],"mappings":";;;;;AAOA,IAAa,uBAAb,cAA0C,MAAM;CAC9C,AAAgB,aAAa;CAC7B,AAAgB;CAEhB,YAAYA,SAAkBC,YAAqB;AACjD,QAAM,WAAW,6CAA6C;AAC9D,OAAK,OAAO;AACZ,OAAK,aAAa;CACnB;AACF;;;;AA8JD,MAAaC,sBAA6C,CAAC,QAAQ;CAEjE,MAAM,KACJ,IAAI,OAAO,kBAAkB,EAAE,MAAM,IAAI,CAAC,IAAI,MAAM,IACpD,IAAI,OAAO,YAAY,IACvB,IAAI,OAAO,cAAc,IACzB,IAAI,OAAO,mBAAmB,IAC9B;AAEF,SAAQ,aAAa,IAAI,OAAO,GAAG,IAAI,KAAK,GAAG,GAAG;AACnD;;;;AAKD,eAAsB,eAKpBC,QACAC,KACwB;AAExB,KAAI,OAAO,QAAS,MAAM,OAAO,KAAK,IAAI,CACxC,QAAO;EACL,OAAO;EACP,OAAO,OAAO;EACd,WAAW,OAAO;EAClB,WAAW,KAAK,KAAK,GAAG,OAAO;EAC/B,YAAY,OAAO;CACpB;CAIH,MAAM,eAAe,OAAO,gBAAgB;CAC5C,MAAM,MAAM,MAAM,aAAa,IAAI;CAGnC,MAAM,MAAM,KAAK,KAAK;CACtB,IAAI,OAAO,MAAM,OAAO,MAAM,IAAI,IAAI;AAGtC,MAAK,QAAQ,KAAK,aAAa,KAAK;EAClC,MAAM,YAAY,MAAM,OAAO;AAC/B,SAAO;GAAE,OAAO;GAAG;EAAW;EAG9B,MAAM,aAAa,KAAK,KAAK,OAAO,WAAW,IAAK;AACpD,QAAM,OAAO,MAAM,IAAI,KAAK,MAAM,WAAW;CAC9C,OAAM;AAEL,OAAK;EAGL,MAAM,cAAc,KAAK,YAAY;EACrC,MAAM,aAAa,KAAK,KAAK,cAAc,IAAK;AAChD,QAAM,OAAO,MAAM,IAAI,KAAK,MAAM,WAAW;CAC9C;CAGD,MAAMC,OAAsB;EAC1B,OAAO,KAAK;EACZ,OAAO,OAAO;EACd,WAAW,KAAK,IAAI,GAAG,OAAO,QAAQ,KAAK,MAAM;EACjD,WAAW,KAAK;EAChB,YAAY,KAAK,YAAY;CAC9B;AAGD,KAAI,KAAK,QAAQ,OAAO,OAAO;AAE7B,MAAI,OAAO,QACT,OAAM,OAAO,QAAQ,KAAK,KAAK;EAIjC,MAAM,oBAAoB,KAAK,KAAK,KAAK,aAAa,IAAK;AAC3D,QAAM,IAAI,qBACR,OAAO,WAAW,8CAClB;CAEH;AAED,QAAO;AACR;;;;AAKD,SAAgB,oBACdA,MACAC,QACkB;CAClB,MAAMC,UAA4B,CAAE;AAEpC,KAAI,OAAO,oBAAoB,OAAO;AACpC,UAAQ,uBAAuB,KAAK,MAAM,UAAU;AACpD,UAAQ,2BAA2B,KAAK,UAAU,UAAU;AAC5D,UAAQ,uBAAuB,IAAI,KAAK,KAAK,WAAW,aAAa;CACtE;AAED,KAAI,OAAO,eAAe;AACxB,UAAQ,6BAA6B,KAAK,WAAW,UAAU;AAC/D,UAAQ,6BAA6B,KAAK,KACxC,KAAK,aAAa,IACnB,CAAC,UAAU;CACb;AAGD,KAAI,KAAK,cAAc,EACrB,SAAQ,iBAAiB,KAAK,KAAK,KAAK,aAAa,IAAK,CAAC,UAAU;AAGvE,QAAO;AACR"}