@geekmidas/rate-limit 0.2.0 → 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # @geekmidas/rate-limit
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [`ff7b115`](https://github.com/geekmidas/toolbox/commit/ff7b11599f60f84ac6cdc73714c853ecf786b2e8) Thanks [@geekmidas](https://github.com/geekmidas)! - Version 1 Stable release
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [[`ff7b115`](https://github.com/geekmidas/toolbox/commit/ff7b11599f60f84ac6cdc73714c853ecf786b2e8)]:
12
+ - @geekmidas/cache@1.0.0
13
+ - @geekmidas/logger@1.0.0
14
+ - @geekmidas/services@1.0.0
@@ -1 +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"}
1
+ {"version":3,"file":"index.cjs","names":["message?: string","retryAfter?: number","defaultKeyGenerator: RateLimitKeyGenerator","config: RateLimitConfig","ctx: RateLimitContext<TServices, TLogger, TSession>","info: RateLimitInfo","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\tpublic readonly statusCode = 429;\n\tpublic readonly retryAfter?: number;\n\n\tconstructor(message?: string, retryAfter?: number) {\n\t\tsuper(message || 'Too many requests, please try again later.');\n\t\tthis.name = 'TooManyRequestsError';\n\t\tthis.retryAfter = retryAfter;\n\t}\n}\n\n/**\n * Rate limit configuration for an endpoint\n */\nexport interface RateLimitConfig {\n\t/**\n\t * Maximum number of requests allowed in the window\n\t */\n\tlimit: number;\n\n\t/**\n\t * Time window in milliseconds\n\t */\n\twindowMs: number;\n\n\t/**\n\t * Cache instance to store rate limit data\n\t */\n\tcache: Cache;\n\n\t/**\n\t * Key generator function to identify clients\n\t * Defaults to using IP address\n\t */\n\tkeyGenerator?: RateLimitKeyGenerator;\n\n\t/**\n\t * Skip rate limiting for certain requests\n\t */\n\tskip?: RateLimitSkipFn;\n\n\t/**\n\t * Optional message to return when rate limit is exceeded\n\t */\n\tmessage?: string;\n\n\t/**\n\t * Optional custom handler when rate limit is exceeded\n\t */\n\thandler?: RateLimitExceededHandler;\n\n\t/**\n\t * Whether to include rate limit headers in response\n\t * @default true\n\t */\n\tstandardHeaders?: boolean;\n\n\t/**\n\t * Whether to include legacy rate limit headers\n\t * @default false\n\t */\n\tlegacyHeaders?: boolean;\n}\n\n/**\n * Context for rate limiting decisions\n */\nexport interface RateLimitContext<\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tTSession = unknown,\n> {\n\theader: (key: string) => string | undefined;\n\tservices: ServiceRecord<TServices>;\n\tlogger: TLogger;\n\tsession: TSession;\n\tpath: string;\n\tmethod: string;\n}\n\n/**\n * Function to generate a unique key for rate limiting\n */\nexport type RateLimitKeyGenerator<\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tTSession = unknown,\n> = (\n\tctx: 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\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tTSession = unknown,\n> = (\n\tctx: RateLimitContext<TServices, TLogger, TSession>,\n) => boolean | Promise<boolean>;\n\n/**\n * Handler for when rate limit is exceeded\n */\nexport type RateLimitExceededHandler<\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tTSession = unknown,\n> = (\n\tctx: RateLimitContext<TServices, TLogger, TSession>,\n\tinfo: RateLimitInfo,\n) => void | Promise<void>;\n\n/**\n * Information about current rate limit status\n */\nexport interface RateLimitInfo {\n\t/**\n\t * Current request count in the window\n\t */\n\tcount: number;\n\n\t/**\n\t * Maximum allowed requests\n\t */\n\tlimit: number;\n\n\t/**\n\t * Remaining requests allowed\n\t */\n\tremaining: number;\n\n\t/**\n\t * Time when the window resets (Unix timestamp)\n\t */\n\tresetTime: number;\n\n\t/**\n\t * Time until reset in milliseconds\n\t */\n\tretryAfter: number;\n}\n\n/**\n * Headers to be set on responses\n */\nexport interface RateLimitHeaders {\n\t'X-RateLimit-Limit'?: string;\n\t'X-RateLimit-Remaining'?: string;\n\t'X-RateLimit-Reset'?: string;\n\t'Retry-After'?: string;\n\t'X-RateLimit-Retry-After'?: string;\n\t'X-RateLimit-Reset-After'?: string;\n}\n\n/**\n * Data stored in cache for rate limiting\n */\nexport interface RateLimitData {\n\tcount: number;\n\tresetTime: number;\n}\n\n/**\n * Default key generator using IP address\n */\nexport const defaultKeyGenerator: RateLimitKeyGenerator = (ctx) => {\n\t// Try various headers for IP address\n\tconst ip =\n\t\tctx.header('x-forwarded-for')?.split(',')[0]?.trim() ||\n\t\tctx.header('x-real-ip') ||\n\t\tctx.header('x-client-ip') ||\n\t\tctx.header('cf-connecting-ip') ||\n\t\t'unknown';\n\n\treturn `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\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tTSession = unknown,\n>(\n\tconfig: RateLimitConfig,\n\tctx: RateLimitContext<TServices, TLogger, TSession>,\n): Promise<RateLimitInfo> {\n\t// Check if we should skip rate limiting\n\tif (config.skip && (await config.skip(ctx))) {\n\t\treturn {\n\t\t\tcount: 0,\n\t\t\tlimit: config.limit,\n\t\t\tremaining: config.limit,\n\t\t\tresetTime: Date.now() + config.windowMs,\n\t\t\tretryAfter: config.windowMs,\n\t\t};\n\t}\n\n\t// Generate key for this request\n\tconst keyGenerator = config.keyGenerator || defaultKeyGenerator;\n\tconst key = await keyGenerator(ctx);\n\n\t// Get current data from cache\n\tconst now = Date.now();\n\tlet data = await config.cache.get<RateLimitData>(key);\n\n\t// If no data or window expired, create new entry\n\tif (!data || data.resetTime <= now) {\n\t\tconst resetTime = now + config.windowMs;\n\t\tdata = { count: 1, resetTime };\n\n\t\t// Store with TTL matching the window\n\t\tconst ttlSeconds = Math.ceil(config.windowMs / 1000);\n\t\tawait config.cache.set(key, data, ttlSeconds);\n\t} else {\n\t\t// Increment count\n\t\tdata.count++;\n\n\t\t// Calculate remaining TTL\n\t\tconst remainingMs = data.resetTime - now;\n\t\tconst ttlSeconds = Math.ceil(remainingMs / 1000);\n\t\tawait config.cache.set(key, data, ttlSeconds);\n\t}\n\n\t// Calculate rate limit info\n\tconst info: RateLimitInfo = {\n\t\tcount: data.count,\n\t\tlimit: config.limit,\n\t\tremaining: Math.max(0, config.limit - data.count),\n\t\tresetTime: data.resetTime,\n\t\tretryAfter: data.resetTime - now,\n\t};\n\n\t// Check if limit exceeded\n\tif (data.count > config.limit) {\n\t\t// Call custom handler if provided\n\t\tif (config.handler) {\n\t\t\tawait config.handler(ctx, info);\n\t\t}\n\n\t\t// Throw rate limit error\n\t\tconst retryAfterSeconds = Math.ceil(info.retryAfter / 1000);\n\t\tthrow new TooManyRequestsError(\n\t\t\tconfig.message || 'Too many requests, please try again later.',\n\t\t\tretryAfterSeconds,\n\t\t);\n\t}\n\n\treturn info;\n}\n\n/**\n * Generate rate limit headers\n */\nexport function getRateLimitHeaders(\n\tinfo: RateLimitInfo,\n\tconfig: RateLimitConfig,\n): RateLimitHeaders {\n\tconst headers: RateLimitHeaders = {};\n\n\tif (config.standardHeaders !== false) {\n\t\theaders['X-RateLimit-Limit'] = info.limit.toString();\n\t\theaders['X-RateLimit-Remaining'] = info.remaining.toString();\n\t\theaders['X-RateLimit-Reset'] = new Date(info.resetTime).toISOString();\n\t}\n\n\tif (config.legacyHeaders) {\n\t\theaders['X-RateLimit-Retry-After'] = info.retryAfter.toString();\n\t\theaders['X-RateLimit-Reset-After'] = Math.ceil(\n\t\t\tinfo.retryAfter / 1000,\n\t\t).toString();\n\t}\n\n\t// Always set Retry-After when limit is exceeded\n\tif (info.remaining === 0) {\n\t\theaders['Retry-After'] = Math.ceil(info.retryAfter / 1000).toString();\n\t}\n\n\treturn headers;\n}\n"],"mappings":";;;;;AAOA,IAAa,uBAAb,cAA0C,MAAM;CAC/C,AAAgB,aAAa;CAC7B,AAAgB;CAEhB,YAAYA,SAAkBC,YAAqB;AAClD,QAAM,WAAW,6CAA6C;AAC9D,OAAK,OAAO;AACZ,OAAK,aAAa;CAClB;AACD;;;;AA8JD,MAAaC,sBAA6C,CAAC,QAAQ;CAElE,MAAM,KACL,IAAI,OAAO,kBAAkB,EAAE,MAAM,IAAI,CAAC,IAAI,MAAM,IACpD,IAAI,OAAO,YAAY,IACvB,IAAI,OAAO,cAAc,IACzB,IAAI,OAAO,mBAAmB,IAC9B;AAED,SAAQ,aAAa,IAAI,OAAO,GAAG,IAAI,KAAK,GAAG,GAAG;AAClD;;;;AAKD,eAAsB,eAKrBC,QACAC,KACyB;AAEzB,KAAI,OAAO,QAAS,MAAM,OAAO,KAAK,IAAI,CACzC,QAAO;EACN,OAAO;EACP,OAAO,OAAO;EACd,WAAW,OAAO;EAClB,WAAW,KAAK,KAAK,GAAG,OAAO;EAC/B,YAAY,OAAO;CACnB;CAIF,MAAM,eAAe,OAAO,gBAAgB;CAC5C,MAAM,MAAM,MAAM,aAAa,IAAI;CAGnC,MAAM,MAAM,KAAK,KAAK;CACtB,IAAI,OAAO,MAAM,OAAO,MAAM,IAAmB,IAAI;AAGrD,MAAK,QAAQ,KAAK,aAAa,KAAK;EACnC,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;CAC7C,OAAM;AAEN,OAAK;EAGL,MAAM,cAAc,KAAK,YAAY;EACrC,MAAM,aAAa,KAAK,KAAK,cAAc,IAAK;AAChD,QAAM,OAAO,MAAM,IAAI,KAAK,MAAM,WAAW;CAC7C;CAGD,MAAMC,OAAsB;EAC3B,OAAO,KAAK;EACZ,OAAO,OAAO;EACd,WAAW,KAAK,IAAI,GAAG,OAAO,QAAQ,KAAK,MAAM;EACjD,WAAW,KAAK;EAChB,YAAY,KAAK,YAAY;CAC7B;AAGD,KAAI,KAAK,QAAQ,OAAO,OAAO;AAE9B,MAAI,OAAO,QACV,OAAM,OAAO,QAAQ,KAAK,KAAK;EAIhC,MAAM,oBAAoB,KAAK,KAAK,KAAK,aAAa,IAAK;AAC3D,QAAM,IAAI,qBACT,OAAO,WAAW,8CAClB;CAED;AAED,QAAO;AACP;;;;AAKD,SAAgB,oBACfA,MACAF,QACmB;CACnB,MAAMG,UAA4B,CAAE;AAEpC,KAAI,OAAO,oBAAoB,OAAO;AACrC,UAAQ,uBAAuB,KAAK,MAAM,UAAU;AACpD,UAAQ,2BAA2B,KAAK,UAAU,UAAU;AAC5D,UAAQ,uBAAuB,IAAI,KAAK,KAAK,WAAW,aAAa;CACrE;AAED,KAAI,OAAO,eAAe;AACzB,UAAQ,6BAA6B,KAAK,WAAW,UAAU;AAC/D,UAAQ,6BAA6B,KAAK,KACzC,KAAK,aAAa,IAClB,CAAC,UAAU;CACZ;AAGD,KAAI,KAAK,cAAc,EACtB,SAAQ,iBAAiB,KAAK,KAAK,KAAK,aAAa,IAAK,CAAC,UAAU;AAGtE,QAAO;AACP"}
package/dist/index.d.cts CHANGED
@@ -15,7 +15,7 @@ declare class TooManyRequestsError extends Error {
15
15
  /**
16
16
  * Rate limit configuration for an endpoint
17
17
  */
18
- interface RateLimitConfig<T = RateLimitData> {
18
+ interface RateLimitConfig {
19
19
  /**
20
20
  * Maximum number of requests allowed in the window
21
21
  */
@@ -27,7 +27,7 @@ interface RateLimitConfig<T = RateLimitData> {
27
27
  /**
28
28
  * Cache instance to store rate limit data
29
29
  */
30
- cache: Cache<T>;
30
+ cache: Cache;
31
31
  /**
32
32
  * Key generator function to identify clients
33
33
  * Defaults to using IP address
@@ -129,11 +129,12 @@ declare const defaultKeyGenerator: RateLimitKeyGenerator;
129
129
  /**
130
130
  * Check rate limit and throw error if exceeded
131
131
  */
132
- declare function checkRateLimit<TServices extends Service[] = [], TLogger extends Logger = Logger, TSession = unknown>(config: RateLimitConfig<RateLimitData>, ctx: RateLimitContext<TServices, TLogger, TSession>): Promise<RateLimitInfo>;
132
+ declare function checkRateLimit<TServices extends Service[] = [], TLogger extends Logger = Logger, TSession = unknown>(config: RateLimitConfig, ctx: RateLimitContext<TServices, TLogger, TSession>): Promise<RateLimitInfo>;
133
133
  /**
134
134
  * Generate rate limit headers
135
135
  */
136
136
  declare function getRateLimitHeaders(info: RateLimitInfo, config: RateLimitConfig): RateLimitHeaders;
137
+ //# sourceMappingURL=index.d.ts.map
137
138
  //#endregion
138
139
  export { RateLimitConfig, RateLimitContext, RateLimitData, RateLimitExceededHandler, RateLimitHeaders, RateLimitInfo, RateLimitKeyGenerator, RateLimitSkipFn, TooManyRequestsError, checkRateLimit, defaultKeyGenerator, getRateLimitHeaders };
139
140
  //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;AAOA;AAciB,cAdJ,oBAAA,SAA6B,KAAK,CAcf;EAAA,SAAA,UAAA,GAAA,GAAA;EAAA,SAcxB,UAAA,CAAA,EAAA,MAAA;EAAK,WAMG,CAAA,OAAA,CAAA,EAAA,MAAA,EAAA,UAAA,CAAA,EAAA,MAAA;;;AAemB;AAkBnC;AAAiC,UArDhB,eAAA,CAqDgB;EAAA;;;EAED,KAIP,EAAA,MAAA;EAAS;;;EAEhB,QAAA,EAAA,MAAA;EAQN;;;EACc,KACT,EAzDT,KAyDS;EAAM;;;;EAG4B,YAA7C,CAAA,EAtDU,qBAsDV;EAAgB;AACD;AAKrB;EAA2B,IAAA,CAAA,EAvDnB,eAuDmB;EAAA;;;EAEK,OAGT,CAAA,EAAA,MAAA;EAAS;;;EAAV,OACP,CAAA,EAnDJ,wBAmDI;EAAO;AAKtB;;;EAC0B,eACT,CAAA,EAAA,OAAA;EAAM;;;;EAG4B,aAA7C,CAAA,EAAA,OAAA;;;AAEa;AAKnB;AA8BiB,UAhFA,gBAgFgB,CAAA,kBA/Ed,OA+Ec,EAAA,GAAA,EAAA,EAAA,gBA9EhB,MA8EgB,GA9EP,MA8EO,EAAA,WAAA,OAAA,CAAA,CAAA;EAYhB,MAAA,EAAA,CAAA,GAAA,EAAA,MAAa,EAAA,GAAA,MAAA,GAAA,SAAA;EAQjB,QAAA,EA9FF,aAwGV,CAxGwB,SA8FS,CAAA;EAeZ,MAAA,EA5Gb,OA4Ga;EAAc,OAAA,EA3G1B,QA2G0B;EAAA,IACjB,EAAA,MAAA;EAAO,MACT,EAAA,MAAA;;;;;AAI0B,KAzG/B,qBAyG+B,CAAA,kBAxGxB,OAwGwB,EAAA,GAAA,EAAA,EAAA,gBAvG1B,MAuG0B,GAvGjB,MAuGiB,EAAA,WAAA,OAAA,CAAA,GAAA,CAAA,GAAA,EApGrC,gBAoGqC,CApGpB,SAoGoB,EApGT,OAoGS,EApGA,QAoGA,CAAA,EAAA,GAAA,MAAA,GAnG7B,OAmG6B,CAAA,MAAA,CAAA;;;;AACjC,KA/FE,eA+FF,CAAA,kBA9FS,OA8FT,EAAA,GAAA,EAAA,EAAA,gBA7FO,MA6FP,GA7FgB,MA6FhB,EAAA,WAAA,OAAA,CAAA,GAAA,CAAA,GAAA,EA1FJ,gBA0FI,CA1Fa,SA0Fb,EA1FwB,OA0FxB,EA1FiC,QA0FjC,CAAA,EAAA,GAAA,OAAA,GAzFK,OAyFL,CAAA,OAAA,CAAA;AAoEV;;;AAES,KA1JG,wBA0JH,CAAA,kBAzJU,OAyJV,EAAA,GAAA,EAAA,EAAA,gBAxJQ,MAwJR,GAxJiB,MAwJjB,EAAA,WAAA,OAAA,CAAA,GAAA,CAAA,GAAA,EArJH,gBAqJG,CArJc,SAqJd,EArJyB,OAqJzB,EArJkC,QAqJlC,CAAA,EAAA,IAAA,EApJF,aAoJE,EAAA,GAAA,IAAA,GAnJG,OAmJH,CAAA,IAAA,CAAA;;AACU;;UA/IF,aAAA;;;;;;;;;;;;;;;;;;;;;;;;;UA8BA,gBAAA;;;;;;;;;;;UAYA,aAAA;;;;;;;cAQJ,qBAAqB;;;;iBAeZ,iCACH,gCACF,SAAS,oCAGjB,sBACH,iBAAiB,WAAW,SAAS,YACxC,QAAQ;;;;iBAoEK,mBAAA,OACT,uBACE,kBACN"}
package/dist/index.d.mts CHANGED
@@ -15,7 +15,7 @@ declare class TooManyRequestsError extends Error {
15
15
  /**
16
16
  * Rate limit configuration for an endpoint
17
17
  */
18
- interface RateLimitConfig<T = RateLimitData> {
18
+ interface RateLimitConfig {
19
19
  /**
20
20
  * Maximum number of requests allowed in the window
21
21
  */
@@ -27,7 +27,7 @@ interface RateLimitConfig<T = RateLimitData> {
27
27
  /**
28
28
  * Cache instance to store rate limit data
29
29
  */
30
- cache: Cache<T>;
30
+ cache: Cache;
31
31
  /**
32
32
  * Key generator function to identify clients
33
33
  * Defaults to using IP address
@@ -129,11 +129,12 @@ declare const defaultKeyGenerator: RateLimitKeyGenerator;
129
129
  /**
130
130
  * Check rate limit and throw error if exceeded
131
131
  */
132
- declare function checkRateLimit<TServices extends Service[] = [], TLogger extends Logger = Logger, TSession = unknown>(config: RateLimitConfig<RateLimitData>, ctx: RateLimitContext<TServices, TLogger, TSession>): Promise<RateLimitInfo>;
132
+ declare function checkRateLimit<TServices extends Service[] = [], TLogger extends Logger = Logger, TSession = unknown>(config: RateLimitConfig, ctx: RateLimitContext<TServices, TLogger, TSession>): Promise<RateLimitInfo>;
133
133
  /**
134
134
  * Generate rate limit headers
135
135
  */
136
136
  declare function getRateLimitHeaders(info: RateLimitInfo, config: RateLimitConfig): RateLimitHeaders;
137
+ //# sourceMappingURL=index.d.ts.map
137
138
  //#endregion
138
139
  export { RateLimitConfig, RateLimitContext, RateLimitData, RateLimitExceededHandler, RateLimitHeaders, RateLimitInfo, RateLimitKeyGenerator, RateLimitSkipFn, TooManyRequestsError, checkRateLimit, defaultKeyGenerator, getRateLimitHeaders };
139
140
  //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;AAOA;AAciB,cAdJ,oBAAA,SAA6B,KAAK,CAcf;EAAA,SAAA,UAAA,GAAA,GAAA;EAAA,SAcxB,UAAA,CAAA,EAAA,MAAA;EAAK,WAMG,CAAA,OAAA,CAAA,EAAA,MAAA,EAAA,UAAA,CAAA,EAAA,MAAA;;;AAemB;AAkBnC;AAAiC,UArDhB,eAAA,CAqDgB;EAAA;;;EAED,KAIP,EAAA,MAAA;EAAS;;;EAEhB,QAAA,EAAA,MAAA;EAQN;;;EACc,KACT,EAzDT,KAyDS;EAAM;;;;EAG4B,YAA7C,CAAA,EAtDU,qBAsDV;EAAgB;AACD;AAKrB;EAA2B,IAAA,CAAA,EAvDnB,eAuDmB;EAAA;;;EAEK,OAGT,CAAA,EAAA,MAAA;EAAS;;;EAAV,OACP,CAAA,EAnDJ,wBAmDI;EAAO;AAKtB;;;EAC0B,eACT,CAAA,EAAA,OAAA;EAAM;;;;EAG4B,aAA7C,CAAA,EAAA,OAAA;;;AAEa;AAKnB;AA8BiB,UAhFA,gBAgFgB,CAAA,kBA/Ed,OA+Ec,EAAA,GAAA,EAAA,EAAA,gBA9EhB,MA8EgB,GA9EP,MA8EO,EAAA,WAAA,OAAA,CAAA,CAAA;EAYhB,MAAA,EAAA,CAAA,GAAA,EAAA,MAAa,EAAA,GAAA,MAAA,GAAA,SAAA;EAQjB,QAAA,EA9FF,aAwGV,CAxGwB,SA8FS,CAAA;EAeZ,MAAA,EA5Gb,OA4Ga;EAAc,OAAA,EA3G1B,QA2G0B;EAAA,IACjB,EAAA,MAAA;EAAO,MACT,EAAA,MAAA;;;;;AAI0B,KAzG/B,qBAyG+B,CAAA,kBAxGxB,OAwGwB,EAAA,GAAA,EAAA,EAAA,gBAvG1B,MAuG0B,GAvGjB,MAuGiB,EAAA,WAAA,OAAA,CAAA,GAAA,CAAA,GAAA,EApGrC,gBAoGqC,CApGpB,SAoGoB,EApGT,OAoGS,EApGA,QAoGA,CAAA,EAAA,GAAA,MAAA,GAnG7B,OAmG6B,CAAA,MAAA,CAAA;;;;AACjC,KA/FE,eA+FF,CAAA,kBA9FS,OA8FT,EAAA,GAAA,EAAA,EAAA,gBA7FO,MA6FP,GA7FgB,MA6FhB,EAAA,WAAA,OAAA,CAAA,GAAA,CAAA,GAAA,EA1FJ,gBA0FI,CA1Fa,SA0Fb,EA1FwB,OA0FxB,EA1FiC,QA0FjC,CAAA,EAAA,GAAA,OAAA,GAzFK,OAyFL,CAAA,OAAA,CAAA;AAoEV;;;AAES,KA1JG,wBA0JH,CAAA,kBAzJU,OAyJV,EAAA,GAAA,EAAA,EAAA,gBAxJQ,MAwJR,GAxJiB,MAwJjB,EAAA,WAAA,OAAA,CAAA,GAAA,CAAA,GAAA,EArJH,gBAqJG,CArJc,SAqJd,EArJyB,OAqJzB,EArJkC,QAqJlC,CAAA,EAAA,IAAA,EApJF,aAoJE,EAAA,GAAA,IAAA,GAnJG,OAmJH,CAAA,IAAA,CAAA;;AACU;;UA/IF,aAAA;;;;;;;;;;;;;;;;;;;;;;;;;UA8BA,gBAAA;;;;;;;;;;;UAYA,aAAA;;;;;;;cAQJ,qBAAqB;;;;iBAeZ,iCACH,gCACF,SAAS,oCAGjB,sBACH,iBAAiB,WAAW,SAAS,YACxC,QAAQ;;;;iBAoEK,mBAAA,OACT,uBACE,kBACN"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","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"}
1
+ {"version":3,"file":"index.mjs","names":["message?: string","retryAfter?: number","defaultKeyGenerator: RateLimitKeyGenerator","config: RateLimitConfig","ctx: RateLimitContext<TServices, TLogger, TSession>","info: RateLimitInfo","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\tpublic readonly statusCode = 429;\n\tpublic readonly retryAfter?: number;\n\n\tconstructor(message?: string, retryAfter?: number) {\n\t\tsuper(message || 'Too many requests, please try again later.');\n\t\tthis.name = 'TooManyRequestsError';\n\t\tthis.retryAfter = retryAfter;\n\t}\n}\n\n/**\n * Rate limit configuration for an endpoint\n */\nexport interface RateLimitConfig {\n\t/**\n\t * Maximum number of requests allowed in the window\n\t */\n\tlimit: number;\n\n\t/**\n\t * Time window in milliseconds\n\t */\n\twindowMs: number;\n\n\t/**\n\t * Cache instance to store rate limit data\n\t */\n\tcache: Cache;\n\n\t/**\n\t * Key generator function to identify clients\n\t * Defaults to using IP address\n\t */\n\tkeyGenerator?: RateLimitKeyGenerator;\n\n\t/**\n\t * Skip rate limiting for certain requests\n\t */\n\tskip?: RateLimitSkipFn;\n\n\t/**\n\t * Optional message to return when rate limit is exceeded\n\t */\n\tmessage?: string;\n\n\t/**\n\t * Optional custom handler when rate limit is exceeded\n\t */\n\thandler?: RateLimitExceededHandler;\n\n\t/**\n\t * Whether to include rate limit headers in response\n\t * @default true\n\t */\n\tstandardHeaders?: boolean;\n\n\t/**\n\t * Whether to include legacy rate limit headers\n\t * @default false\n\t */\n\tlegacyHeaders?: boolean;\n}\n\n/**\n * Context for rate limiting decisions\n */\nexport interface RateLimitContext<\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tTSession = unknown,\n> {\n\theader: (key: string) => string | undefined;\n\tservices: ServiceRecord<TServices>;\n\tlogger: TLogger;\n\tsession: TSession;\n\tpath: string;\n\tmethod: string;\n}\n\n/**\n * Function to generate a unique key for rate limiting\n */\nexport type RateLimitKeyGenerator<\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tTSession = unknown,\n> = (\n\tctx: 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\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tTSession = unknown,\n> = (\n\tctx: RateLimitContext<TServices, TLogger, TSession>,\n) => boolean | Promise<boolean>;\n\n/**\n * Handler for when rate limit is exceeded\n */\nexport type RateLimitExceededHandler<\n\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tTSession = unknown,\n> = (\n\tctx: RateLimitContext<TServices, TLogger, TSession>,\n\tinfo: RateLimitInfo,\n) => void | Promise<void>;\n\n/**\n * Information about current rate limit status\n */\nexport interface RateLimitInfo {\n\t/**\n\t * Current request count in the window\n\t */\n\tcount: number;\n\n\t/**\n\t * Maximum allowed requests\n\t */\n\tlimit: number;\n\n\t/**\n\t * Remaining requests allowed\n\t */\n\tremaining: number;\n\n\t/**\n\t * Time when the window resets (Unix timestamp)\n\t */\n\tresetTime: number;\n\n\t/**\n\t * Time until reset in milliseconds\n\t */\n\tretryAfter: number;\n}\n\n/**\n * Headers to be set on responses\n */\nexport interface RateLimitHeaders {\n\t'X-RateLimit-Limit'?: string;\n\t'X-RateLimit-Remaining'?: string;\n\t'X-RateLimit-Reset'?: string;\n\t'Retry-After'?: string;\n\t'X-RateLimit-Retry-After'?: string;\n\t'X-RateLimit-Reset-After'?: string;\n}\n\n/**\n * Data stored in cache for rate limiting\n */\nexport interface RateLimitData {\n\tcount: number;\n\tresetTime: number;\n}\n\n/**\n * Default key generator using IP address\n */\nexport const defaultKeyGenerator: RateLimitKeyGenerator = (ctx) => {\n\t// Try various headers for IP address\n\tconst ip =\n\t\tctx.header('x-forwarded-for')?.split(',')[0]?.trim() ||\n\t\tctx.header('x-real-ip') ||\n\t\tctx.header('x-client-ip') ||\n\t\tctx.header('cf-connecting-ip') ||\n\t\t'unknown';\n\n\treturn `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\tTServices extends Service[] = [],\n\tTLogger extends Logger = Logger,\n\tTSession = unknown,\n>(\n\tconfig: RateLimitConfig,\n\tctx: RateLimitContext<TServices, TLogger, TSession>,\n): Promise<RateLimitInfo> {\n\t// Check if we should skip rate limiting\n\tif (config.skip && (await config.skip(ctx))) {\n\t\treturn {\n\t\t\tcount: 0,\n\t\t\tlimit: config.limit,\n\t\t\tremaining: config.limit,\n\t\t\tresetTime: Date.now() + config.windowMs,\n\t\t\tretryAfter: config.windowMs,\n\t\t};\n\t}\n\n\t// Generate key for this request\n\tconst keyGenerator = config.keyGenerator || defaultKeyGenerator;\n\tconst key = await keyGenerator(ctx);\n\n\t// Get current data from cache\n\tconst now = Date.now();\n\tlet data = await config.cache.get<RateLimitData>(key);\n\n\t// If no data or window expired, create new entry\n\tif (!data || data.resetTime <= now) {\n\t\tconst resetTime = now + config.windowMs;\n\t\tdata = { count: 1, resetTime };\n\n\t\t// Store with TTL matching the window\n\t\tconst ttlSeconds = Math.ceil(config.windowMs / 1000);\n\t\tawait config.cache.set(key, data, ttlSeconds);\n\t} else {\n\t\t// Increment count\n\t\tdata.count++;\n\n\t\t// Calculate remaining TTL\n\t\tconst remainingMs = data.resetTime - now;\n\t\tconst ttlSeconds = Math.ceil(remainingMs / 1000);\n\t\tawait config.cache.set(key, data, ttlSeconds);\n\t}\n\n\t// Calculate rate limit info\n\tconst info: RateLimitInfo = {\n\t\tcount: data.count,\n\t\tlimit: config.limit,\n\t\tremaining: Math.max(0, config.limit - data.count),\n\t\tresetTime: data.resetTime,\n\t\tretryAfter: data.resetTime - now,\n\t};\n\n\t// Check if limit exceeded\n\tif (data.count > config.limit) {\n\t\t// Call custom handler if provided\n\t\tif (config.handler) {\n\t\t\tawait config.handler(ctx, info);\n\t\t}\n\n\t\t// Throw rate limit error\n\t\tconst retryAfterSeconds = Math.ceil(info.retryAfter / 1000);\n\t\tthrow new TooManyRequestsError(\n\t\t\tconfig.message || 'Too many requests, please try again later.',\n\t\t\tretryAfterSeconds,\n\t\t);\n\t}\n\n\treturn info;\n}\n\n/**\n * Generate rate limit headers\n */\nexport function getRateLimitHeaders(\n\tinfo: RateLimitInfo,\n\tconfig: RateLimitConfig,\n): RateLimitHeaders {\n\tconst headers: RateLimitHeaders = {};\n\n\tif (config.standardHeaders !== false) {\n\t\theaders['X-RateLimit-Limit'] = info.limit.toString();\n\t\theaders['X-RateLimit-Remaining'] = info.remaining.toString();\n\t\theaders['X-RateLimit-Reset'] = new Date(info.resetTime).toISOString();\n\t}\n\n\tif (config.legacyHeaders) {\n\t\theaders['X-RateLimit-Retry-After'] = info.retryAfter.toString();\n\t\theaders['X-RateLimit-Reset-After'] = Math.ceil(\n\t\t\tinfo.retryAfter / 1000,\n\t\t).toString();\n\t}\n\n\t// Always set Retry-After when limit is exceeded\n\tif (info.remaining === 0) {\n\t\theaders['Retry-After'] = Math.ceil(info.retryAfter / 1000).toString();\n\t}\n\n\treturn headers;\n}\n"],"mappings":";;;;AAOA,IAAa,uBAAb,cAA0C,MAAM;CAC/C,AAAgB,aAAa;CAC7B,AAAgB;CAEhB,YAAYA,SAAkBC,YAAqB;AAClD,QAAM,WAAW,6CAA6C;AAC9D,OAAK,OAAO;AACZ,OAAK,aAAa;CAClB;AACD;;;;AA8JD,MAAaC,sBAA6C,CAAC,QAAQ;CAElE,MAAM,KACL,IAAI,OAAO,kBAAkB,EAAE,MAAM,IAAI,CAAC,IAAI,MAAM,IACpD,IAAI,OAAO,YAAY,IACvB,IAAI,OAAO,cAAc,IACzB,IAAI,OAAO,mBAAmB,IAC9B;AAED,SAAQ,aAAa,IAAI,OAAO,GAAG,IAAI,KAAK,GAAG,GAAG;AAClD;;;;AAKD,eAAsB,eAKrBC,QACAC,KACyB;AAEzB,KAAI,OAAO,QAAS,MAAM,OAAO,KAAK,IAAI,CACzC,QAAO;EACN,OAAO;EACP,OAAO,OAAO;EACd,WAAW,OAAO;EAClB,WAAW,KAAK,KAAK,GAAG,OAAO;EAC/B,YAAY,OAAO;CACnB;CAIF,MAAM,eAAe,OAAO,gBAAgB;CAC5C,MAAM,MAAM,MAAM,aAAa,IAAI;CAGnC,MAAM,MAAM,KAAK,KAAK;CACtB,IAAI,OAAO,MAAM,OAAO,MAAM,IAAmB,IAAI;AAGrD,MAAK,QAAQ,KAAK,aAAa,KAAK;EACnC,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;CAC7C,OAAM;AAEN,OAAK;EAGL,MAAM,cAAc,KAAK,YAAY;EACrC,MAAM,aAAa,KAAK,KAAK,cAAc,IAAK;AAChD,QAAM,OAAO,MAAM,IAAI,KAAK,MAAM,WAAW;CAC7C;CAGD,MAAMC,OAAsB;EAC3B,OAAO,KAAK;EACZ,OAAO,OAAO;EACd,WAAW,KAAK,IAAI,GAAG,OAAO,QAAQ,KAAK,MAAM;EACjD,WAAW,KAAK;EAChB,YAAY,KAAK,YAAY;CAC7B;AAGD,KAAI,KAAK,QAAQ,OAAO,OAAO;AAE9B,MAAI,OAAO,QACV,OAAM,OAAO,QAAQ,KAAK,KAAK;EAIhC,MAAM,oBAAoB,KAAK,KAAK,KAAK,aAAa,IAAK;AAC3D,QAAM,IAAI,qBACT,OAAO,WAAW,8CAClB;CAED;AAED,QAAO;AACP;;;;AAKD,SAAgB,oBACfA,MACAF,QACmB;CACnB,MAAMG,UAA4B,CAAE;AAEpC,KAAI,OAAO,oBAAoB,OAAO;AACrC,UAAQ,uBAAuB,KAAK,MAAM,UAAU;AACpD,UAAQ,2BAA2B,KAAK,UAAU,UAAU;AAC5D,UAAQ,uBAAuB,IAAI,KAAK,KAAK,WAAW,aAAa;CACrE;AAED,KAAI,OAAO,eAAe;AACzB,UAAQ,6BAA6B,KAAK,WAAW,UAAU;AAC/D,UAAQ,6BAA6B,KAAK,KACzC,KAAK,aAAa,IAClB,CAAC,UAAU;CACZ;AAGD,KAAI,KAAK,cAAc,EACtB,SAAQ,iBAAiB,KAAK,KAAK,KAAK,aAAa,IAAK,CAAC,UAAU;AAGtE,QAAO;AACP"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/rate-limit",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -20,9 +20,9 @@
20
20
  },
21
21
  "dependencies": {},
22
22
  "peerDependencies": {
23
- "@geekmidas/cache": "^0.1.0",
24
- "@geekmidas/logger": "^0.3.0",
25
- "@geekmidas/services": "^0.1.0"
23
+ "@geekmidas/cache": "^1.0.0",
24
+ "@geekmidas/logger": "^1.0.0",
25
+ "@geekmidas/services": "^1.0.0"
26
26
  },
27
27
  "peerDependenciesMeta": {
28
28
  "@geekmidas/cache": {
@@ -36,9 +36,9 @@
36
36
  }
37
37
  },
38
38
  "devDependencies": {
39
- "@geekmidas/cache": "^0.1.0",
40
- "@geekmidas/logger": "^0.3.0",
41
- "@geekmidas/services": "^0.1.0"
39
+ "@geekmidas/cache": "^1.0.0",
40
+ "@geekmidas/logger": "^1.0.0",
41
+ "@geekmidas/services": "^1.0.0"
42
42
  },
43
43
  "scripts": {
44
44
  "ts": "tsc --noEmit --skipLibCheck src/**/*.ts"
@@ -1,110 +1,109 @@
1
1
  import { InMemoryCache } from '@geekmidas/cache/memory';
2
2
  import { bench, describe } from 'vitest';
3
3
  import {
4
- type RateLimitConfig,
5
- type RateLimitContext,
6
- type RateLimitData,
7
- checkRateLimit,
4
+ checkRateLimit,
5
+ type RateLimitConfig,
6
+ type RateLimitContext,
8
7
  } from '../index';
9
8
 
10
9
  // Helper to create a mock context
11
10
  function createContext(ip: string): RateLimitContext {
12
- return {
13
- header: (key: string) => (key === 'x-forwarded-for' ? ip : undefined),
14
- services: {},
15
- logger: {
16
- info: () => {},
17
- error: () => {},
18
- debug: () => {},
19
- warn: () => {},
20
- } as any,
21
- session: {},
22
- path: '/test',
23
- method: 'GET',
24
- };
11
+ return {
12
+ header: (key: string) => (key === 'x-forwarded-for' ? ip : undefined),
13
+ services: {},
14
+ logger: {
15
+ info: () => {},
16
+ error: () => {},
17
+ debug: () => {},
18
+ warn: () => {},
19
+ } as any,
20
+ session: {},
21
+ path: '/test',
22
+ method: 'GET',
23
+ };
25
24
  }
26
25
 
27
26
  describe('Rate Limiting', () => {
28
- const cache = new InMemoryCache<RateLimitData>();
29
- const config: RateLimitConfig<RateLimitData> = {
30
- limit: 100,
31
- windowMs: 60000,
32
- cache,
33
- };
27
+ const cache = new InMemoryCache();
28
+ const config: RateLimitConfig = {
29
+ limit: 100,
30
+ windowMs: 60000,
31
+ cache,
32
+ };
34
33
 
35
- bench('checkRateLimit - single IP', async () => {
36
- const ctx = createContext('127.0.0.1');
37
- await checkRateLimit(config, ctx);
38
- });
34
+ bench('checkRateLimit - single IP', async () => {
35
+ const ctx = createContext('127.0.0.1');
36
+ await checkRateLimit(config, ctx);
37
+ });
39
38
 
40
- bench('checkRateLimit - varying IPs', async () => {
41
- const ip = `192.168.1.${Math.floor(Math.random() * 255)}`;
42
- const ctx = createContext(ip);
43
- await checkRateLimit(config, ctx);
44
- });
39
+ bench('checkRateLimit - varying IPs', async () => {
40
+ const ip = `192.168.1.${Math.floor(Math.random() * 255)}`;
41
+ const ctx = createContext(ip);
42
+ await checkRateLimit(config, ctx);
43
+ });
45
44
  });
46
45
 
47
46
  describe('Rate Limiting - High Volume', () => {
48
- bench('100 requests same IP', async () => {
49
- const cache = new InMemoryCache<RateLimitData>();
50
- const config: RateLimitConfig<RateLimitData> = {
51
- limit: 1000,
52
- windowMs: 60000,
53
- cache,
54
- };
55
- const ctx = createContext('10.0.0.1');
47
+ bench('100 requests same IP', async () => {
48
+ const cache = new InMemoryCache();
49
+ const config: RateLimitConfig = {
50
+ limit: 1000,
51
+ windowMs: 60000,
52
+ cache,
53
+ };
54
+ const ctx = createContext('10.0.0.1');
56
55
 
57
- for (let i = 0; i < 100; i++) {
58
- await checkRateLimit(config, ctx);
59
- }
60
- });
56
+ for (let i = 0; i < 100; i++) {
57
+ await checkRateLimit(config, ctx);
58
+ }
59
+ });
61
60
 
62
- bench('100 requests different IPs', async () => {
63
- const cache = new InMemoryCache<RateLimitData>();
64
- const config: RateLimitConfig<RateLimitData> = {
65
- limit: 100,
66
- windowMs: 60000,
67
- cache,
68
- };
61
+ bench('100 requests different IPs', async () => {
62
+ const cache = new InMemoryCache();
63
+ const config: RateLimitConfig = {
64
+ limit: 100,
65
+ windowMs: 60000,
66
+ cache,
67
+ };
69
68
 
70
- for (let i = 0; i < 100; i++) {
71
- const ctx = createContext(`10.0.0.${i}`);
72
- await checkRateLimit(config, ctx);
73
- }
74
- });
69
+ for (let i = 0; i < 100; i++) {
70
+ const ctx = createContext(`10.0.0.${i}`);
71
+ await checkRateLimit(config, ctx);
72
+ }
73
+ });
75
74
  });
76
75
 
77
76
  describe('Rate Limiting - Window Sizes', () => {
78
- bench('1 second window', async () => {
79
- const cache = new InMemoryCache<RateLimitData>();
80
- const config: RateLimitConfig<RateLimitData> = {
81
- limit: 10,
82
- windowMs: 1000,
83
- cache,
84
- };
85
- const ctx = createContext('127.0.0.1');
86
- await checkRateLimit(config, ctx);
87
- });
77
+ bench('1 second window', async () => {
78
+ const cache = new InMemoryCache();
79
+ const config: RateLimitConfig = {
80
+ limit: 10,
81
+ windowMs: 1000,
82
+ cache,
83
+ };
84
+ const ctx = createContext('127.0.0.1');
85
+ await checkRateLimit(config, ctx);
86
+ });
88
87
 
89
- bench('1 minute window', async () => {
90
- const cache = new InMemoryCache<RateLimitData>();
91
- const config: RateLimitConfig<RateLimitData> = {
92
- limit: 100,
93
- windowMs: 60000,
94
- cache,
95
- };
96
- const ctx = createContext('127.0.0.1');
97
- await checkRateLimit(config, ctx);
98
- });
88
+ bench('1 minute window', async () => {
89
+ const cache = new InMemoryCache();
90
+ const config: RateLimitConfig = {
91
+ limit: 100,
92
+ windowMs: 60000,
93
+ cache,
94
+ };
95
+ const ctx = createContext('127.0.0.1');
96
+ await checkRateLimit(config, ctx);
97
+ });
99
98
 
100
- bench('1 hour window', async () => {
101
- const cache = new InMemoryCache<RateLimitData>();
102
- const config: RateLimitConfig<RateLimitData> = {
103
- limit: 1000,
104
- windowMs: 3600000,
105
- cache,
106
- };
107
- const ctx = createContext('127.0.0.1');
108
- await checkRateLimit(config, ctx);
109
- });
99
+ bench('1 hour window', async () => {
100
+ const cache = new InMemoryCache();
101
+ const config: RateLimitConfig = {
102
+ limit: 1000,
103
+ windowMs: 3600000,
104
+ cache,
105
+ };
106
+ const ctx = createContext('127.0.0.1');
107
+ await checkRateLimit(config, ctx);
108
+ });
110
109
  });