@ooneex/rate-limit 0.0.18 → 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/README.md CHANGED
@@ -1 +1,73 @@
1
1
  # @ooneex/rate-limit
2
+
3
+ API rate limiting middleware with configurable throttling strategies, sliding window counters, and per-client request quota enforcement.
4
+
5
+ ![Bun](https://img.shields.io/badge/Bun-Compatible-orange?style=flat-square&logo=bun)
6
+ ![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue?style=flat-square&logo=typescript)
7
+ ![MIT License](https://img.shields.io/badge/License-MIT-yellow?style=flat-square)
8
+
9
+ ## Features
10
+
11
+ ✅ **Redis-Backed** - Uses Bun.RedisClient for fast, distributed rate limiting across instances
12
+
13
+ ✅ **Configurable Windows** - Set custom time windows and request limits per key
14
+
15
+ ✅ **Counter-Based** - Atomic increment counters with automatic TTL expiry per window
16
+
17
+ ✅ **Rate Limit Result** - Returns detailed results including remaining quota, total limit, and reset time
18
+
19
+ ✅ **Key-Based Limiting** - Rate limit by any key (IP address, user ID, API key, etc.)
20
+
21
+ ✅ **Reset Support** - Programmatically reset rate limit counters for specific keys
22
+
23
+ ✅ **Count Inspection** - Query the current request count for any key
24
+
25
+ ✅ **Auto-Reconnect** - Configurable Redis connection with auto-reconnect, retries, and pipelining
26
+
27
+ ✅ **Dependency Injection** - Injectable via @ooneex/container for seamless DI integration
28
+
29
+ ✅ **Environment Config** - Automatic Redis URL loading from RATE_LIMIT_REDIS_URL environment variable
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ bun add @ooneex/rate-limit
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Basic Rate Limiting
40
+
41
+ ```typescript
42
+ import { RedisRateLimiter } from '@ooneex/rate-limit';
43
+
44
+ const limiter = new RedisRateLimiter({
45
+ connectionString: 'redis://localhost:6379',
46
+ });
47
+
48
+ // Check if a client has exceeded their limit (100 requests per 60 seconds)
49
+ const result = await limiter.check('client-ip:192.168.1.1', 100, 60);
50
+
51
+ console.log(result.limited); // false if under limit
52
+ console.log(result.remaining); // remaining requests in window
53
+ console.log(result.total); // total allowed requests
54
+ console.log(result.resetAt); // Date when the window resets
55
+ ```
56
+
57
+ ### Reset and Inspect
58
+
59
+ ```typescript
60
+ // Get current request count for a key
61
+ const count = await limiter.getCount('client-ip:192.168.1.1');
62
+
63
+ // Reset the rate limit for a key
64
+ const wasReset = await limiter.reset('client-ip:192.168.1.1');
65
+ ```
66
+
67
+ ## License
68
+
69
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
70
+
71
+ ---
72
+
73
+ Made with love by the Ooneex team
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // @bun
2
- import{Exception as b}from"@ooneex/exception";import{HttpStatus as p}from"@ooneex/http-status";class t extends b{constructor(n,e={}){super(n,{status:p.Code.TooManyRequests,data:e});this.name="RateLimitException"}}class u{client;constructor(n={}){let e=n.connectionString||Bun.env.RATE_LIMIT_REDIS_URL;if(!e)throw new t("Redis connection string is required. Please provide a connection string either through the constructor options or set the RATE_LIMIT_REDIS_URL environment variable.");let{connectionString:o,...r}=n,m={...{connectionTimeout:1e4,idleTimeout:30000,autoReconnect:!0,maxRetries:3,enableOfflineQueue:!0,enableAutoPipelining:!0},...r};this.client=new Bun.RedisClient(e,m)}async connect(){if(!this.client.connected)await this.client.connect()}getKey(n){return`ratelimit:${n}`}async check(n,e,o){try{await this.connect();let r=this.getKey(n),s=await this.client.incr(r);if(s===1)await this.client.expire(r,o);let m=await this.client.ttl(r),a=new Date(Date.now()+m*1000);return{limited:s>e,remaining:Math.max(0,e-s),total:e,resetAt:a}}catch(r){throw new t(`Failed to check rate limit for key "${n}": ${r}`)}}async reset(n){try{await this.connect();let e=this.getKey(n);return await this.client.del(e)>0}catch(e){throw new t(`Failed to reset rate limit for key "${n}": ${e}`)}}async getCount(n){try{await this.connect();let e=this.getKey(n),o=await this.client.get(e);if(o===null)return 0;return Number.parseInt(o,10)}catch(e){throw new t(`Failed to get count for key "${n}": ${e}`)}}}export{u as RedisRateLimiter,t as RateLimitException};
2
+ var a=function(o,e,n,r){var t=arguments.length,s=t<3?e:r===null?r=Object.getOwnPropertyDescriptor(e,n):r,b;if(typeof Reflect==="object"&&typeof Reflect.decorate==="function")s=Reflect.decorate(o,e,n,r);else for(var p=o.length-1;p>=0;p--)if(b=o[p])s=(t<3?b(s):t>3?b(e,n,s):b(e,n))||s;return t>3&&s&&Object.defineProperty(e,n,s),s};var g=(o,e)=>{if(typeof Reflect==="object"&&typeof Reflect.metadata==="function")return Reflect.metadata(o,e)};import{Exception as x}from"@ooneex/exception";import{HttpStatus as l}from"@ooneex/http-status";class u extends x{constructor(o,e={}){super(o,{status:l.Code.TooManyRequests,data:e});this.name="RateLimitException"}}import{injectable as f}from"@ooneex/container";class m{client;constructor(o={}){let e=o.connectionString||Bun.env.RATE_LIMIT_REDIS_URL;if(!e)throw new u("Redis connection string is required. Please provide a connection string either through the constructor options or set the RATE_LIMIT_REDIS_URL environment variable.");let{connectionString:n,...r}=o,s={...{connectionTimeout:1e4,idleTimeout:30000,autoReconnect:!0,maxRetries:3,enableOfflineQueue:!0,enableAutoPipelining:!0},...r};this.client=new Bun.RedisClient(e,s)}async connect(){if(!this.client.connected)await this.client.connect()}getKey(o){return`ratelimit:${o}`}async check(o,e,n){try{await this.connect();let r=this.getKey(o),t=await this.client.incr(r);if(t===1)await this.client.expire(r,n);let s=await this.client.ttl(r),b=new Date(Date.now()+s*1000);return{limited:t>e,remaining:Math.max(0,e-t),total:e,resetAt:b}}catch(r){throw new u(`Failed to check rate limit for key "${o}": ${r}`)}}async reset(o){try{await this.connect();let e=this.getKey(o);return await this.client.del(e)>0}catch(e){throw new u(`Failed to reset rate limit for key "${o}": ${e}`)}}async getCount(o){try{await this.connect();let e=this.getKey(o),n=await this.client.get(e);if(n===null)return 0;return Number.parseInt(n,10)}catch(e){throw new u(`Failed to get count for key "${o}": ${e}`)}}}m=a([f(),g("design:paramtypes",[typeof RedisRateLimiterOptionsType==="undefined"?Object:RedisRateLimiterOptionsType])],m);export{m as RedisRateLimiter,u as RateLimitException};
3
3
 
4
- //# debugId=DD9E5A2DC9D13F5664756E2164756E21
4
+ //# debugId=B4AF9472051F496D64756E2164756E21
package/dist/index.js.map CHANGED
@@ -3,9 +3,9 @@
3
3
  "sources": ["src/RateLimitException.ts", "src/RedisRateLimiter.ts"],
4
4
  "sourcesContent": [
5
5
  "import { Exception } from \"@ooneex/exception\";\nimport { HttpStatus } from \"@ooneex/http-status\";\n\nexport class RateLimitException extends Exception {\n constructor(message: string, data: Record<string, unknown> = {}) {\n super(message, {\n status: HttpStatus.Code.TooManyRequests,\n data,\n });\n this.name = \"RateLimitException\";\n }\n}\n",
6
- "import { RateLimitException } from \"./RateLimitException\";\nimport type { IRateLimiter, RateLimitResultType, RedisRateLimiterOptionsType } from \"./types\";\n\nexport class RedisRateLimiter implements IRateLimiter {\n private client: Bun.RedisClient;\n\n constructor(options: RedisRateLimiterOptionsType = {}) {\n const connectionString = options.connectionString || Bun.env.RATE_LIMIT_REDIS_URL;\n\n if (!connectionString) {\n throw new RateLimitException(\n \"Redis connection string is required. Please provide a connection string either through the constructor options or set the RATE_LIMIT_REDIS_URL environment variable.\",\n );\n }\n\n const { connectionString: _, ...userOptions } = options;\n\n const defaultOptions = {\n connectionTimeout: 10_000,\n idleTimeout: 30_000,\n autoReconnect: true,\n maxRetries: 3,\n enableOfflineQueue: true,\n enableAutoPipelining: true,\n };\n\n const clientOptions = { ...defaultOptions, ...userOptions };\n\n this.client = new Bun.RedisClient(connectionString, clientOptions);\n }\n\n private async connect(): Promise<void> {\n if (!this.client.connected) {\n await this.client.connect();\n }\n }\n\n private getKey(key: string): string {\n return `ratelimit:${key}`;\n }\n\n public async check(key: string, limit: number, windowSeconds: number): Promise<RateLimitResultType> {\n try {\n await this.connect();\n\n const rateLimitKey = this.getKey(key);\n\n // Increment counter\n const count = await this.client.incr(rateLimitKey);\n\n // Set expiry if this is the first request in window\n if (count === 1) {\n await this.client.expire(rateLimitKey, windowSeconds);\n }\n\n // Get TTL for reset time calculation\n const ttl = await this.client.ttl(rateLimitKey);\n const resetAt = new Date(Date.now() + ttl * 1000);\n\n return {\n limited: count > limit,\n remaining: Math.max(0, limit - count),\n total: limit,\n resetAt,\n };\n } catch (error) {\n throw new RateLimitException(`Failed to check rate limit for key \"${key}\": ${error}`);\n }\n }\n\n public async reset(key: string): Promise<boolean> {\n try {\n await this.connect();\n\n const rateLimitKey = this.getKey(key);\n const result = await this.client.del(rateLimitKey);\n\n return result > 0;\n } catch (error) {\n throw new RateLimitException(`Failed to reset rate limit for key \"${key}\": ${error}`);\n }\n }\n\n public async getCount(key: string): Promise<number> {\n try {\n await this.connect();\n\n const rateLimitKey = this.getKey(key);\n const value = await this.client.get(rateLimitKey);\n\n if (value === null) {\n return 0;\n }\n\n return Number.parseInt(value, 10);\n } catch (error) {\n throw new RateLimitException(`Failed to get count for key \"${key}\": ${error}`);\n }\n }\n}\n"
6
+ "import { injectable } from \"@ooneex/container\";\nimport { RateLimitException } from \"./RateLimitException\";\nimport type { IRateLimiter, RateLimitResultType, RedisRateLimiterOptionsType } from \"./types\";\n\n@injectable()\nexport class RedisRateLimiter implements IRateLimiter {\n private client: Bun.RedisClient;\n\n constructor(options: RedisRateLimiterOptionsType = {}) {\n const connectionString = options.connectionString || Bun.env.RATE_LIMIT_REDIS_URL;\n\n if (!connectionString) {\n throw new RateLimitException(\n \"Redis connection string is required. Please provide a connection string either through the constructor options or set the RATE_LIMIT_REDIS_URL environment variable.\",\n );\n }\n\n const { connectionString: _, ...userOptions } = options;\n\n const defaultOptions = {\n connectionTimeout: 10_000,\n idleTimeout: 30_000,\n autoReconnect: true,\n maxRetries: 3,\n enableOfflineQueue: true,\n enableAutoPipelining: true,\n };\n\n const clientOptions = { ...defaultOptions, ...userOptions };\n\n this.client = new Bun.RedisClient(connectionString, clientOptions);\n }\n\n private async connect(): Promise<void> {\n if (!this.client.connected) {\n await this.client.connect();\n }\n }\n\n private getKey(key: string): string {\n return `ratelimit:${key}`;\n }\n\n public async check(key: string, limit: number, windowSeconds: number): Promise<RateLimitResultType> {\n try {\n await this.connect();\n\n const rateLimitKey = this.getKey(key);\n\n // Increment counter\n const count = await this.client.incr(rateLimitKey);\n\n // Set expiry if this is the first request in window\n if (count === 1) {\n await this.client.expire(rateLimitKey, windowSeconds);\n }\n\n // Get TTL for reset time calculation\n const ttl = await this.client.ttl(rateLimitKey);\n const resetAt = new Date(Date.now() + ttl * 1000);\n\n return {\n limited: count > limit,\n remaining: Math.max(0, limit - count),\n total: limit,\n resetAt,\n };\n } catch (error) {\n throw new RateLimitException(`Failed to check rate limit for key \"${key}\": ${error}`);\n }\n }\n\n public async reset(key: string): Promise<boolean> {\n try {\n await this.connect();\n\n const rateLimitKey = this.getKey(key);\n const result = await this.client.del(rateLimitKey);\n\n return result > 0;\n } catch (error) {\n throw new RateLimitException(`Failed to reset rate limit for key \"${key}\": ${error}`);\n }\n }\n\n public async getCount(key: string): Promise<number> {\n try {\n await this.connect();\n\n const rateLimitKey = this.getKey(key);\n const value = await this.client.get(rateLimitKey);\n\n if (value === null) {\n return 0;\n }\n\n return Number.parseInt(value, 10);\n } catch (error) {\n throw new RateLimitException(`Failed to get count for key \"${key}\": ${error}`);\n }\n }\n}\n"
7
7
  ],
8
- "mappings": ";AAAA,oBAAS,0BACT,qBAAS,4BAEF,MAAM,UAA2B,CAAU,CAChD,WAAW,CAAC,EAAiB,EAAgC,CAAC,EAAG,CAC/D,MAAM,EAAS,CACb,OAAQ,EAAW,KAAK,gBACxB,MACF,CAAC,EACD,KAAK,KAAO,qBAEhB,CCRO,MAAM,CAAyC,CAC5C,OAER,WAAW,CAAC,EAAuC,CAAC,EAAG,CACrD,IAAM,EAAmB,EAAQ,kBAAoB,IAAI,IAAI,qBAE7D,GAAI,CAAC,EACH,MAAM,IAAI,EACR,sKACF,EAGF,IAAQ,iBAAkB,KAAM,GAAgB,EAW1C,EAAgB,IATC,CACrB,kBAAmB,IACnB,YAAa,MACb,cAAe,GACf,WAAY,EACZ,mBAAoB,GACpB,qBAAsB,EACxB,KAE8C,CAAY,EAE1D,KAAK,OAAS,IAAI,IAAI,YAAY,EAAkB,CAAa,OAGrD,QAAO,EAAkB,CACrC,GAAI,CAAC,KAAK,OAAO,UACf,MAAM,KAAK,OAAO,QAAQ,EAItB,MAAM,CAAC,EAAqB,CAClC,MAAO,aAAa,SAGT,MAAK,CAAC,EAAa,EAAe,EAAqD,CAClG,GAAI,CACF,MAAM,KAAK,QAAQ,EAEnB,IAAM,EAAe,KAAK,OAAO,CAAG,EAG9B,EAAQ,MAAM,KAAK,OAAO,KAAK,CAAY,EAGjD,GAAI,IAAU,EACZ,MAAM,KAAK,OAAO,OAAO,EAAc,CAAa,EAItD,IAAM,EAAM,MAAM,KAAK,OAAO,IAAI,CAAY,EACxC,EAAU,IAAI,KAAK,KAAK,IAAI,EAAI,EAAM,IAAI,EAEhD,MAAO,CACL,QAAS,EAAQ,EACjB,UAAW,KAAK,IAAI,EAAG,EAAQ,CAAK,EACpC,MAAO,EACP,SACF,EACA,MAAO,EAAO,CACd,MAAM,IAAI,EAAmB,uCAAuC,OAAS,GAAO,QAI3E,MAAK,CAAC,EAA+B,CAChD,GAAI,CACF,MAAM,KAAK,QAAQ,EAEnB,IAAM,EAAe,KAAK,OAAO,CAAG,EAGpC,OAFe,MAAM,KAAK,OAAO,IAAI,CAAY,EAEjC,EAChB,MAAO,EAAO,CACd,MAAM,IAAI,EAAmB,uCAAuC,OAAS,GAAO,QAI3E,SAAQ,CAAC,EAA8B,CAClD,GAAI,CACF,MAAM,KAAK,QAAQ,EAEnB,IAAM,EAAe,KAAK,OAAO,CAAG,EAC9B,EAAQ,MAAM,KAAK,OAAO,IAAI,CAAY,EAEhD,GAAI,IAAU,KACZ,MAAO,GAGT,OAAO,OAAO,SAAS,EAAO,EAAE,EAChC,MAAO,EAAO,CACd,MAAM,IAAI,EAAmB,gCAAgC,OAAS,GAAO,GAGnF",
9
- "debugId": "DD9E5A2DC9D13F5664756E2164756E21",
8
+ "mappings": ";ybAAA,oBAAS,0BACT,qBAAS,4BAEF,MAAM,UAA2B,CAAU,CAChD,WAAW,CAAC,EAAiB,EAAgC,CAAC,EAAG,CAC/D,MAAM,EAAS,CACb,OAAQ,EAAW,KAAK,gBACxB,MACF,CAAC,EACD,KAAK,KAAO,qBAEhB,CCXA,qBAAS,0BAKF,MAAM,CAAyC,CAC5C,OAER,WAAW,CAAC,EAAuC,CAAC,EAAG,CACrD,IAAM,EAAmB,EAAQ,kBAAoB,IAAI,IAAI,qBAE7D,GAAI,CAAC,EACH,MAAM,IAAI,EACR,sKACF,EAGF,IAAQ,iBAAkB,KAAM,GAAgB,EAW1C,EAAgB,IATC,CACrB,kBAAmB,IACnB,YAAa,MACb,cAAe,GACf,WAAY,EACZ,mBAAoB,GACpB,qBAAsB,EACxB,KAE8C,CAAY,EAE1D,KAAK,OAAS,IAAI,IAAI,YAAY,EAAkB,CAAa,OAGrD,QAAO,EAAkB,CACrC,GAAI,CAAC,KAAK,OAAO,UACf,MAAM,KAAK,OAAO,QAAQ,EAItB,MAAM,CAAC,EAAqB,CAClC,MAAO,aAAa,SAGT,MAAK,CAAC,EAAa,EAAe,EAAqD,CAClG,GAAI,CACF,MAAM,KAAK,QAAQ,EAEnB,IAAM,EAAe,KAAK,OAAO,CAAG,EAG9B,EAAQ,MAAM,KAAK,OAAO,KAAK,CAAY,EAGjD,GAAI,IAAU,EACZ,MAAM,KAAK,OAAO,OAAO,EAAc,CAAa,EAItD,IAAM,EAAM,MAAM,KAAK,OAAO,IAAI,CAAY,EACxC,EAAU,IAAI,KAAK,KAAK,IAAI,EAAI,EAAM,IAAI,EAEhD,MAAO,CACL,QAAS,EAAQ,EACjB,UAAW,KAAK,IAAI,EAAG,EAAQ,CAAK,EACpC,MAAO,EACP,SACF,EACA,MAAO,EAAO,CACd,MAAM,IAAI,EAAmB,uCAAuC,OAAS,GAAO,QAI3E,MAAK,CAAC,EAA+B,CAChD,GAAI,CACF,MAAM,KAAK,QAAQ,EAEnB,IAAM,EAAe,KAAK,OAAO,CAAG,EAGpC,OAFe,MAAM,KAAK,OAAO,IAAI,CAAY,EAEjC,EAChB,MAAO,EAAO,CACd,MAAM,IAAI,EAAmB,uCAAuC,OAAS,GAAO,QAI3E,SAAQ,CAAC,EAA8B,CAClD,GAAI,CACF,MAAM,KAAK,QAAQ,EAEnB,IAAM,EAAe,KAAK,OAAO,CAAG,EAC9B,EAAQ,MAAM,KAAK,OAAO,IAAI,CAAY,EAEhD,GAAI,IAAU,KACZ,MAAO,GAGT,OAAO,OAAO,SAAS,EAAO,EAAE,EAChC,MAAO,EAAO,CACd,MAAM,IAAI,EAAmB,gCAAgC,OAAS,GAAO,GAGnF,CAhGa,EAAN,GADN,EAAW,EACL,8GAAM",
9
+ "debugId": "B4AF9472051F496D64756E2164756E21",
10
10
  "names": []
11
11
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ooneex/rate-limit",
3
- "description": "Rate limiting middleware for API request throttling and abuse prevention",
4
- "version": "0.0.18",
3
+ "description": "API rate limiting middleware with configurable throttling strategies, sliding window counters, and per-client request quota enforcement",
4
+ "version": "1.0.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -28,12 +28,13 @@
28
28
  "npm:publish": "bun publish --tolerate-republish --access public"
29
29
  },
30
30
  "peerDependencies": {
31
- "@ooneex/exception": "0.0.17",
32
- "@ooneex/http-status": "0.0.17"
31
+ "@ooneex/exception": "0.0.18",
32
+ "@ooneex/http-status": "0.0.18"
33
33
  },
34
34
  "dependencies": {
35
- "@ooneex/exception": "0.0.17",
36
- "@ooneex/http-status": "0.0.17"
35
+ "@ooneex/exception": "0.0.18",
36
+ "@ooneex/container": "0.0.19",
37
+ "@ooneex/http-status": "0.0.18"
37
38
  },
38
39
  "keywords": [
39
40
  "api",