@snap-agent/middleware-ratelimit 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/LICENSE +22 -0
- package/README.md +158 -0
- package/dist/index.d.mts +175 -0
- package/dist/index.d.ts +175 -0
- package/dist/index.js +295 -0
- package/dist/index.mjs +268 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ViloTech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# @snap-agent/middleware-ratelimit
|
|
2
|
+
|
|
3
|
+
Rate limiting middleware for SnapAgent SDK. Per-user, per-agent, or global rate limiting to prevent abuse and control costs.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @snap-agent/middleware-ratelimit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createClient } from '@snap-agent/core';
|
|
15
|
+
import { RateLimiter } from '@snap-agent/middleware-ratelimit';
|
|
16
|
+
|
|
17
|
+
const rateLimiter = new RateLimiter({
|
|
18
|
+
maxRequests: 100,
|
|
19
|
+
windowMs: 60 * 1000, // 1 minute
|
|
20
|
+
keyBy: 'userId',
|
|
21
|
+
onLimit: 'reject',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const agent = await client.createAgent({
|
|
25
|
+
plugins: [rateLimiter],
|
|
26
|
+
// ...
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
| Option | Type | Default | Description |
|
|
33
|
+
|--------|------|---------|-------------|
|
|
34
|
+
| `maxRequests` | `number` | `100` | Max requests per window |
|
|
35
|
+
| `windowMs` | `number` | `60000` | Window size in ms |
|
|
36
|
+
| `keyBy` | `string` | `'userId'` | How to bucket limits |
|
|
37
|
+
| `onLimit` | `string` | `'reject'` | Action when limited |
|
|
38
|
+
| `limitMessage` | `string` | "Too many requests..." | Message when limited |
|
|
39
|
+
|
|
40
|
+
## Key By Options
|
|
41
|
+
|
|
42
|
+
- `userId` - Rate limit per user
|
|
43
|
+
- `threadId` - Rate limit per thread
|
|
44
|
+
- `agentId` - Rate limit per agent
|
|
45
|
+
- `ip` - Rate limit per IP
|
|
46
|
+
- `global` - Global rate limit
|
|
47
|
+
- Custom function for advanced use cases
|
|
48
|
+
|
|
49
|
+
## On Limit Actions
|
|
50
|
+
|
|
51
|
+
- `reject` - Return error message immediately
|
|
52
|
+
- `queue` - Queue request until rate limit resets
|
|
53
|
+
- `throttle` - Allow but add delay
|
|
54
|
+
|
|
55
|
+
## Examples
|
|
56
|
+
|
|
57
|
+
### Per-User Rate Limiting
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
new RateLimiter({
|
|
61
|
+
maxRequests: 50,
|
|
62
|
+
windowMs: 60000,
|
|
63
|
+
keyBy: 'userId',
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Custom Key Function
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
new RateLimiter({
|
|
71
|
+
maxRequests: 100,
|
|
72
|
+
windowMs: 60000,
|
|
73
|
+
keyBy: (context) => {
|
|
74
|
+
// Rate limit by organization
|
|
75
|
+
return `org:${context.metadata?.organizationId || 'unknown'}`;
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Skip Certain Users
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
new RateLimiter({
|
|
84
|
+
maxRequests: 100,
|
|
85
|
+
windowMs: 60000,
|
|
86
|
+
skip: (key, context) => {
|
|
87
|
+
// Skip rate limiting for admin users
|
|
88
|
+
return context.metadata?.role === 'admin';
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Queue Mode
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
new RateLimiter({
|
|
97
|
+
maxRequests: 10,
|
|
98
|
+
windowMs: 60000,
|
|
99
|
+
onLimit: 'queue',
|
|
100
|
+
queue: {
|
|
101
|
+
maxSize: 100,
|
|
102
|
+
timeout: 30000, // 30 second timeout
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Distributed Rate Limiting (Redis)
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import { Redis } from 'ioredis';
|
|
111
|
+
|
|
112
|
+
const redis = new Redis();
|
|
113
|
+
|
|
114
|
+
new RateLimiter({
|
|
115
|
+
maxRequests: 100,
|
|
116
|
+
windowMs: 60000,
|
|
117
|
+
storage: {
|
|
118
|
+
async get(key) {
|
|
119
|
+
const data = await redis.get(`ratelimit:${key}`);
|
|
120
|
+
return data ? JSON.parse(data) : null;
|
|
121
|
+
},
|
|
122
|
+
async set(key, entry, ttlMs) {
|
|
123
|
+
await redis.set(`ratelimit:${key}`, JSON.stringify(entry), 'PX', ttlMs);
|
|
124
|
+
},
|
|
125
|
+
async increment(key, windowMs) {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
const multi = redis.multi();
|
|
128
|
+
multi.incr(`ratelimit:${key}:count`);
|
|
129
|
+
multi.pexpire(`ratelimit:${key}:count`, windowMs);
|
|
130
|
+
const [[, count]] = await multi.exec();
|
|
131
|
+
return {
|
|
132
|
+
count: count as number,
|
|
133
|
+
resetAt: now + windowMs,
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Rate Limit Headers
|
|
141
|
+
|
|
142
|
+
When `includeHeaders: true` (default), rate limit info is added to response metadata:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// Available in response metadata
|
|
146
|
+
{
|
|
147
|
+
headers: {
|
|
148
|
+
'X-RateLimit-Limit': 100,
|
|
149
|
+
'X-RateLimit-Remaining': 95,
|
|
150
|
+
'X-RateLimit-Reset': '2024-01-15T12:01:00.000Z'
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT
|
|
158
|
+
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { MiddlewarePlugin } from '@snap-agent/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How to identify rate limit buckets
|
|
5
|
+
*/
|
|
6
|
+
type RateLimitKey = 'userId' | 'threadId' | 'agentId' | 'ip' | 'global';
|
|
7
|
+
/**
|
|
8
|
+
* Result of rate limit check
|
|
9
|
+
*/
|
|
10
|
+
interface RateLimitResult {
|
|
11
|
+
allowed: boolean;
|
|
12
|
+
limit: number;
|
|
13
|
+
remaining: number;
|
|
14
|
+
resetAt: Date;
|
|
15
|
+
retryAfter?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Rate limit entry
|
|
19
|
+
*/
|
|
20
|
+
interface RateLimitEntry {
|
|
21
|
+
count: number;
|
|
22
|
+
resetAt: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Configuration for rate limiter
|
|
26
|
+
*/
|
|
27
|
+
interface RateLimitConfig {
|
|
28
|
+
/**
|
|
29
|
+
* Maximum requests allowed in the window
|
|
30
|
+
* @default 100
|
|
31
|
+
*/
|
|
32
|
+
maxRequests: number;
|
|
33
|
+
/**
|
|
34
|
+
* Time window in milliseconds
|
|
35
|
+
* @default 60000 (1 minute)
|
|
36
|
+
*/
|
|
37
|
+
windowMs: number;
|
|
38
|
+
/**
|
|
39
|
+
* How to identify rate limit buckets
|
|
40
|
+
* @default 'userId'
|
|
41
|
+
*/
|
|
42
|
+
keyBy: RateLimitKey | ((context: {
|
|
43
|
+
agentId: string;
|
|
44
|
+
threadId?: string;
|
|
45
|
+
metadata?: any;
|
|
46
|
+
}) => string);
|
|
47
|
+
/**
|
|
48
|
+
* Action when rate limited
|
|
49
|
+
* @default 'reject'
|
|
50
|
+
*/
|
|
51
|
+
onLimit: 'reject' | 'queue' | 'throttle';
|
|
52
|
+
/**
|
|
53
|
+
* Message to return when rate limited
|
|
54
|
+
* @default "Too many requests. Please try again later."
|
|
55
|
+
*/
|
|
56
|
+
limitMessage?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Include rate limit info in response metadata
|
|
59
|
+
* @default true
|
|
60
|
+
*/
|
|
61
|
+
includeHeaders?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Skip rate limiting for certain keys (e.g., admin users)
|
|
64
|
+
*/
|
|
65
|
+
skip?: (key: string, context: {
|
|
66
|
+
agentId: string;
|
|
67
|
+
threadId?: string;
|
|
68
|
+
metadata?: any;
|
|
69
|
+
}) => boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Callback when rate limit is hit
|
|
72
|
+
*/
|
|
73
|
+
onRateLimited?: (key: string, result: RateLimitResult, context: {
|
|
74
|
+
agentId: string;
|
|
75
|
+
threadId?: string;
|
|
76
|
+
}) => void;
|
|
77
|
+
/**
|
|
78
|
+
* Custom storage adapter (for distributed rate limiting)
|
|
79
|
+
* Default uses in-memory storage
|
|
80
|
+
*/
|
|
81
|
+
storage?: {
|
|
82
|
+
get: (key: string) => Promise<RateLimitEntry | null>;
|
|
83
|
+
set: (key: string, entry: RateLimitEntry, ttlMs: number) => Promise<void>;
|
|
84
|
+
increment: (key: string, windowMs: number) => Promise<{
|
|
85
|
+
count: number;
|
|
86
|
+
resetAt: number;
|
|
87
|
+
}>;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Queue configuration (when onLimit is 'queue')
|
|
91
|
+
*/
|
|
92
|
+
queue?: {
|
|
93
|
+
maxSize: number;
|
|
94
|
+
timeout: number;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Rate Limiter Middleware
|
|
99
|
+
*
|
|
100
|
+
* Implements per-user, per-agent, or global rate limiting to prevent abuse
|
|
101
|
+
* and control costs.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* import { RateLimiter } from '@snap-agent/middleware-ratelimit';
|
|
106
|
+
*
|
|
107
|
+
* const rateLimiter = new RateLimiter({
|
|
108
|
+
* maxRequests: 100,
|
|
109
|
+
* windowMs: 60 * 1000, // 1 minute
|
|
110
|
+
* keyBy: 'userId',
|
|
111
|
+
* onLimit: 'reject',
|
|
112
|
+
* });
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
declare class RateLimiter implements MiddlewarePlugin {
|
|
116
|
+
name: string;
|
|
117
|
+
type: "middleware";
|
|
118
|
+
priority: number;
|
|
119
|
+
private config;
|
|
120
|
+
private limits;
|
|
121
|
+
private cleanupInterval;
|
|
122
|
+
private queues;
|
|
123
|
+
private processing;
|
|
124
|
+
constructor(config?: Partial<RateLimitConfig>);
|
|
125
|
+
beforeRequest(messages: any[], context: {
|
|
126
|
+
agentId: string;
|
|
127
|
+
threadId?: string;
|
|
128
|
+
metadata?: any;
|
|
129
|
+
}): Promise<{
|
|
130
|
+
messages: any[];
|
|
131
|
+
metadata?: any;
|
|
132
|
+
}>;
|
|
133
|
+
afterResponse(response: string, context: {
|
|
134
|
+
agentId: string;
|
|
135
|
+
threadId?: string;
|
|
136
|
+
metadata?: any;
|
|
137
|
+
}): Promise<{
|
|
138
|
+
response: string;
|
|
139
|
+
metadata?: any;
|
|
140
|
+
}>;
|
|
141
|
+
/**
|
|
142
|
+
* Get the rate limit key for a context
|
|
143
|
+
*/
|
|
144
|
+
private getKey;
|
|
145
|
+
/**
|
|
146
|
+
* Check and increment rate limit
|
|
147
|
+
*/
|
|
148
|
+
private checkLimit;
|
|
149
|
+
/**
|
|
150
|
+
* Add request to queue
|
|
151
|
+
*/
|
|
152
|
+
private enqueue;
|
|
153
|
+
/**
|
|
154
|
+
* Process queue for a key
|
|
155
|
+
*/
|
|
156
|
+
private processQueue;
|
|
157
|
+
/**
|
|
158
|
+
* Cleanup expired entries
|
|
159
|
+
*/
|
|
160
|
+
private cleanup;
|
|
161
|
+
/**
|
|
162
|
+
* Get current usage for a key
|
|
163
|
+
*/
|
|
164
|
+
getUsage(key: string): Promise<RateLimitResult>;
|
|
165
|
+
/**
|
|
166
|
+
* Reset rate limit for a key
|
|
167
|
+
*/
|
|
168
|
+
reset(key: string): void;
|
|
169
|
+
/**
|
|
170
|
+
* Destroy the rate limiter
|
|
171
|
+
*/
|
|
172
|
+
destroy(): void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { type RateLimitConfig, type RateLimitKey, type RateLimitResult, RateLimiter };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { MiddlewarePlugin } from '@snap-agent/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How to identify rate limit buckets
|
|
5
|
+
*/
|
|
6
|
+
type RateLimitKey = 'userId' | 'threadId' | 'agentId' | 'ip' | 'global';
|
|
7
|
+
/**
|
|
8
|
+
* Result of rate limit check
|
|
9
|
+
*/
|
|
10
|
+
interface RateLimitResult {
|
|
11
|
+
allowed: boolean;
|
|
12
|
+
limit: number;
|
|
13
|
+
remaining: number;
|
|
14
|
+
resetAt: Date;
|
|
15
|
+
retryAfter?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Rate limit entry
|
|
19
|
+
*/
|
|
20
|
+
interface RateLimitEntry {
|
|
21
|
+
count: number;
|
|
22
|
+
resetAt: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Configuration for rate limiter
|
|
26
|
+
*/
|
|
27
|
+
interface RateLimitConfig {
|
|
28
|
+
/**
|
|
29
|
+
* Maximum requests allowed in the window
|
|
30
|
+
* @default 100
|
|
31
|
+
*/
|
|
32
|
+
maxRequests: number;
|
|
33
|
+
/**
|
|
34
|
+
* Time window in milliseconds
|
|
35
|
+
* @default 60000 (1 minute)
|
|
36
|
+
*/
|
|
37
|
+
windowMs: number;
|
|
38
|
+
/**
|
|
39
|
+
* How to identify rate limit buckets
|
|
40
|
+
* @default 'userId'
|
|
41
|
+
*/
|
|
42
|
+
keyBy: RateLimitKey | ((context: {
|
|
43
|
+
agentId: string;
|
|
44
|
+
threadId?: string;
|
|
45
|
+
metadata?: any;
|
|
46
|
+
}) => string);
|
|
47
|
+
/**
|
|
48
|
+
* Action when rate limited
|
|
49
|
+
* @default 'reject'
|
|
50
|
+
*/
|
|
51
|
+
onLimit: 'reject' | 'queue' | 'throttle';
|
|
52
|
+
/**
|
|
53
|
+
* Message to return when rate limited
|
|
54
|
+
* @default "Too many requests. Please try again later."
|
|
55
|
+
*/
|
|
56
|
+
limitMessage?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Include rate limit info in response metadata
|
|
59
|
+
* @default true
|
|
60
|
+
*/
|
|
61
|
+
includeHeaders?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Skip rate limiting for certain keys (e.g., admin users)
|
|
64
|
+
*/
|
|
65
|
+
skip?: (key: string, context: {
|
|
66
|
+
agentId: string;
|
|
67
|
+
threadId?: string;
|
|
68
|
+
metadata?: any;
|
|
69
|
+
}) => boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Callback when rate limit is hit
|
|
72
|
+
*/
|
|
73
|
+
onRateLimited?: (key: string, result: RateLimitResult, context: {
|
|
74
|
+
agentId: string;
|
|
75
|
+
threadId?: string;
|
|
76
|
+
}) => void;
|
|
77
|
+
/**
|
|
78
|
+
* Custom storage adapter (for distributed rate limiting)
|
|
79
|
+
* Default uses in-memory storage
|
|
80
|
+
*/
|
|
81
|
+
storage?: {
|
|
82
|
+
get: (key: string) => Promise<RateLimitEntry | null>;
|
|
83
|
+
set: (key: string, entry: RateLimitEntry, ttlMs: number) => Promise<void>;
|
|
84
|
+
increment: (key: string, windowMs: number) => Promise<{
|
|
85
|
+
count: number;
|
|
86
|
+
resetAt: number;
|
|
87
|
+
}>;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Queue configuration (when onLimit is 'queue')
|
|
91
|
+
*/
|
|
92
|
+
queue?: {
|
|
93
|
+
maxSize: number;
|
|
94
|
+
timeout: number;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Rate Limiter Middleware
|
|
99
|
+
*
|
|
100
|
+
* Implements per-user, per-agent, or global rate limiting to prevent abuse
|
|
101
|
+
* and control costs.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* import { RateLimiter } from '@snap-agent/middleware-ratelimit';
|
|
106
|
+
*
|
|
107
|
+
* const rateLimiter = new RateLimiter({
|
|
108
|
+
* maxRequests: 100,
|
|
109
|
+
* windowMs: 60 * 1000, // 1 minute
|
|
110
|
+
* keyBy: 'userId',
|
|
111
|
+
* onLimit: 'reject',
|
|
112
|
+
* });
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
declare class RateLimiter implements MiddlewarePlugin {
|
|
116
|
+
name: string;
|
|
117
|
+
type: "middleware";
|
|
118
|
+
priority: number;
|
|
119
|
+
private config;
|
|
120
|
+
private limits;
|
|
121
|
+
private cleanupInterval;
|
|
122
|
+
private queues;
|
|
123
|
+
private processing;
|
|
124
|
+
constructor(config?: Partial<RateLimitConfig>);
|
|
125
|
+
beforeRequest(messages: any[], context: {
|
|
126
|
+
agentId: string;
|
|
127
|
+
threadId?: string;
|
|
128
|
+
metadata?: any;
|
|
129
|
+
}): Promise<{
|
|
130
|
+
messages: any[];
|
|
131
|
+
metadata?: any;
|
|
132
|
+
}>;
|
|
133
|
+
afterResponse(response: string, context: {
|
|
134
|
+
agentId: string;
|
|
135
|
+
threadId?: string;
|
|
136
|
+
metadata?: any;
|
|
137
|
+
}): Promise<{
|
|
138
|
+
response: string;
|
|
139
|
+
metadata?: any;
|
|
140
|
+
}>;
|
|
141
|
+
/**
|
|
142
|
+
* Get the rate limit key for a context
|
|
143
|
+
*/
|
|
144
|
+
private getKey;
|
|
145
|
+
/**
|
|
146
|
+
* Check and increment rate limit
|
|
147
|
+
*/
|
|
148
|
+
private checkLimit;
|
|
149
|
+
/**
|
|
150
|
+
* Add request to queue
|
|
151
|
+
*/
|
|
152
|
+
private enqueue;
|
|
153
|
+
/**
|
|
154
|
+
* Process queue for a key
|
|
155
|
+
*/
|
|
156
|
+
private processQueue;
|
|
157
|
+
/**
|
|
158
|
+
* Cleanup expired entries
|
|
159
|
+
*/
|
|
160
|
+
private cleanup;
|
|
161
|
+
/**
|
|
162
|
+
* Get current usage for a key
|
|
163
|
+
*/
|
|
164
|
+
getUsage(key: string): Promise<RateLimitResult>;
|
|
165
|
+
/**
|
|
166
|
+
* Reset rate limit for a key
|
|
167
|
+
*/
|
|
168
|
+
reset(key: string): void;
|
|
169
|
+
/**
|
|
170
|
+
* Destroy the rate limiter
|
|
171
|
+
*/
|
|
172
|
+
destroy(): void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { type RateLimitConfig, type RateLimitKey, type RateLimitResult, RateLimiter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
RateLimiter: () => RateLimiter
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/RateLimiter.ts
|
|
28
|
+
var RateLimiter = class {
|
|
29
|
+
constructor(config = {}) {
|
|
30
|
+
this.name = "rate-limiter";
|
|
31
|
+
this.type = "middleware";
|
|
32
|
+
this.priority = 5;
|
|
33
|
+
// In-memory storage
|
|
34
|
+
this.limits = /* @__PURE__ */ new Map();
|
|
35
|
+
this.cleanupInterval = null;
|
|
36
|
+
// Queue for 'queue' mode
|
|
37
|
+
this.queues = /* @__PURE__ */ new Map();
|
|
38
|
+
this.processing = /* @__PURE__ */ new Set();
|
|
39
|
+
this.config = {
|
|
40
|
+
maxRequests: config.maxRequests ?? 100,
|
|
41
|
+
windowMs: config.windowMs ?? 6e4,
|
|
42
|
+
keyBy: config.keyBy ?? "userId",
|
|
43
|
+
onLimit: config.onLimit ?? "reject",
|
|
44
|
+
limitMessage: config.limitMessage ?? "Too many requests. Please try again later.",
|
|
45
|
+
includeHeaders: config.includeHeaders !== false,
|
|
46
|
+
skip: config.skip,
|
|
47
|
+
onRateLimited: config.onRateLimited,
|
|
48
|
+
storage: config.storage,
|
|
49
|
+
queue: config.queue
|
|
50
|
+
};
|
|
51
|
+
if (!this.config.storage) {
|
|
52
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 6e4);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async beforeRequest(messages, context) {
|
|
56
|
+
const key = this.getKey(context);
|
|
57
|
+
if (this.config.skip?.(key, context)) {
|
|
58
|
+
return { messages, metadata: { rateLimit: { skipped: true } } };
|
|
59
|
+
}
|
|
60
|
+
const result = await this.checkLimit(key);
|
|
61
|
+
if (!result.allowed) {
|
|
62
|
+
this.config.onRateLimited?.(key, result, context);
|
|
63
|
+
if (this.config.onLimit === "reject") {
|
|
64
|
+
const limitedMessage = {
|
|
65
|
+
role: "system",
|
|
66
|
+
content: `[RATE_LIMITED] ${this.config.limitMessage}`
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
messages: [limitedMessage],
|
|
70
|
+
metadata: {
|
|
71
|
+
rateLimit: {
|
|
72
|
+
limited: true,
|
|
73
|
+
...result
|
|
74
|
+
},
|
|
75
|
+
_skipLLM: true
|
|
76
|
+
// Signal to skip LLM call
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (this.config.onLimit === "queue") {
|
|
81
|
+
await this.enqueue(key);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
messages,
|
|
86
|
+
metadata: {
|
|
87
|
+
rateLimit: {
|
|
88
|
+
...result,
|
|
89
|
+
key
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async afterResponse(response, context) {
|
|
95
|
+
if (context.metadata?._skipLLM) {
|
|
96
|
+
return {
|
|
97
|
+
response: this.config.limitMessage,
|
|
98
|
+
metadata: {
|
|
99
|
+
...context.metadata,
|
|
100
|
+
rateLimited: true
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const key = this.getKey(context);
|
|
105
|
+
this.processQueue(key);
|
|
106
|
+
if (this.config.includeHeaders && context.metadata?.rateLimit) {
|
|
107
|
+
return {
|
|
108
|
+
response,
|
|
109
|
+
metadata: {
|
|
110
|
+
...context.metadata,
|
|
111
|
+
headers: {
|
|
112
|
+
"X-RateLimit-Limit": context.metadata.rateLimit.limit,
|
|
113
|
+
"X-RateLimit-Remaining": context.metadata.rateLimit.remaining,
|
|
114
|
+
"X-RateLimit-Reset": context.metadata.rateLimit.resetAt?.toISOString()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return { response, metadata: context.metadata };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get the rate limit key for a context
|
|
123
|
+
*/
|
|
124
|
+
getKey(context) {
|
|
125
|
+
if (typeof this.config.keyBy === "function") {
|
|
126
|
+
return this.config.keyBy(context);
|
|
127
|
+
}
|
|
128
|
+
switch (this.config.keyBy) {
|
|
129
|
+
case "userId":
|
|
130
|
+
return `user:${context.metadata?.userId || "anonymous"}`;
|
|
131
|
+
case "threadId":
|
|
132
|
+
return `thread:${context.threadId || "unknown"}`;
|
|
133
|
+
case "agentId":
|
|
134
|
+
return `agent:${context.agentId}`;
|
|
135
|
+
case "ip":
|
|
136
|
+
return `ip:${context.metadata?.ip || "unknown"}`;
|
|
137
|
+
case "global":
|
|
138
|
+
return "global";
|
|
139
|
+
default:
|
|
140
|
+
return `user:${context.metadata?.userId || "anonymous"}`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Check and increment rate limit
|
|
145
|
+
*/
|
|
146
|
+
async checkLimit(key) {
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
if (this.config.storage) {
|
|
149
|
+
const { count, resetAt } = await this.config.storage.increment(key, this.config.windowMs);
|
|
150
|
+
const allowed2 = count <= this.config.maxRequests;
|
|
151
|
+
return {
|
|
152
|
+
allowed: allowed2,
|
|
153
|
+
limit: this.config.maxRequests,
|
|
154
|
+
remaining: Math.max(0, this.config.maxRequests - count),
|
|
155
|
+
resetAt: new Date(resetAt),
|
|
156
|
+
retryAfter: allowed2 ? void 0 : Math.ceil((resetAt - now) / 1e3)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
let entry = this.limits.get(key);
|
|
160
|
+
if (!entry || now > entry.resetAt) {
|
|
161
|
+
entry = {
|
|
162
|
+
count: 1,
|
|
163
|
+
resetAt: now + this.config.windowMs
|
|
164
|
+
};
|
|
165
|
+
this.limits.set(key, entry);
|
|
166
|
+
} else {
|
|
167
|
+
entry.count++;
|
|
168
|
+
}
|
|
169
|
+
const allowed = entry.count <= this.config.maxRequests;
|
|
170
|
+
return {
|
|
171
|
+
allowed,
|
|
172
|
+
limit: this.config.maxRequests,
|
|
173
|
+
remaining: Math.max(0, this.config.maxRequests - entry.count),
|
|
174
|
+
resetAt: new Date(entry.resetAt),
|
|
175
|
+
retryAfter: allowed ? void 0 : Math.ceil((entry.resetAt - now) / 1e3)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Add request to queue
|
|
180
|
+
*/
|
|
181
|
+
enqueue(key) {
|
|
182
|
+
const queue = this.queues.get(key) || [];
|
|
183
|
+
const maxSize = this.config.queue?.maxSize || 10;
|
|
184
|
+
const timeout = this.config.queue?.timeout || 3e4;
|
|
185
|
+
if (queue.length >= maxSize) {
|
|
186
|
+
return Promise.reject(new Error("Rate limit queue is full"));
|
|
187
|
+
}
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
queue.push({
|
|
190
|
+
resolve,
|
|
191
|
+
reject,
|
|
192
|
+
addedAt: Date.now()
|
|
193
|
+
});
|
|
194
|
+
this.queues.set(key, queue);
|
|
195
|
+
setTimeout(() => {
|
|
196
|
+
const idx = queue.findIndex((q) => q.resolve === resolve);
|
|
197
|
+
if (idx > -1) {
|
|
198
|
+
queue.splice(idx, 1);
|
|
199
|
+
reject(new Error("Queue timeout"));
|
|
200
|
+
}
|
|
201
|
+
}, timeout);
|
|
202
|
+
this.processQueue(key);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Process queue for a key
|
|
207
|
+
*/
|
|
208
|
+
processQueue(key) {
|
|
209
|
+
if (this.processing.has(key)) return;
|
|
210
|
+
const queue = this.queues.get(key);
|
|
211
|
+
if (!queue || queue.length === 0) return;
|
|
212
|
+
this.processing.add(key);
|
|
213
|
+
const entry = this.limits.get(key);
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
if (!entry || now > entry.resetAt || entry.count < this.config.maxRequests) {
|
|
216
|
+
const item = queue.shift();
|
|
217
|
+
if (item) {
|
|
218
|
+
item.resolve(void 0);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
this.processing.delete(key);
|
|
222
|
+
if (queue.length > 0) {
|
|
223
|
+
const waitTime = entry ? Math.max(0, entry.resetAt - now) : 0;
|
|
224
|
+
setTimeout(() => this.processQueue(key), Math.min(waitTime, 1e3));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Cleanup expired entries
|
|
229
|
+
*/
|
|
230
|
+
cleanup() {
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
for (const [key, entry] of this.limits.entries()) {
|
|
233
|
+
if (now > entry.resetAt) {
|
|
234
|
+
this.limits.delete(key);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Get current usage for a key
|
|
240
|
+
*/
|
|
241
|
+
async getUsage(key) {
|
|
242
|
+
if (this.config.storage) {
|
|
243
|
+
const entry2 = await this.config.storage.get(key);
|
|
244
|
+
if (!entry2) {
|
|
245
|
+
return {
|
|
246
|
+
allowed: true,
|
|
247
|
+
limit: this.config.maxRequests,
|
|
248
|
+
remaining: this.config.maxRequests,
|
|
249
|
+
resetAt: /* @__PURE__ */ new Date()
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
allowed: entry2.count < this.config.maxRequests,
|
|
254
|
+
limit: this.config.maxRequests,
|
|
255
|
+
remaining: Math.max(0, this.config.maxRequests - entry2.count),
|
|
256
|
+
resetAt: new Date(entry2.resetAt)
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const entry = this.limits.get(key);
|
|
260
|
+
if (!entry || Date.now() > entry.resetAt) {
|
|
261
|
+
return {
|
|
262
|
+
allowed: true,
|
|
263
|
+
limit: this.config.maxRequests,
|
|
264
|
+
remaining: this.config.maxRequests,
|
|
265
|
+
resetAt: /* @__PURE__ */ new Date()
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
allowed: entry.count < this.config.maxRequests,
|
|
270
|
+
limit: this.config.maxRequests,
|
|
271
|
+
remaining: Math.max(0, this.config.maxRequests - entry.count),
|
|
272
|
+
resetAt: new Date(entry.resetAt)
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Reset rate limit for a key
|
|
277
|
+
*/
|
|
278
|
+
reset(key) {
|
|
279
|
+
this.limits.delete(key);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Destroy the rate limiter
|
|
283
|
+
*/
|
|
284
|
+
destroy() {
|
|
285
|
+
if (this.cleanupInterval) {
|
|
286
|
+
clearInterval(this.cleanupInterval);
|
|
287
|
+
}
|
|
288
|
+
this.limits.clear();
|
|
289
|
+
this.queues.clear();
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
293
|
+
0 && (module.exports = {
|
|
294
|
+
RateLimiter
|
|
295
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// src/RateLimiter.ts
|
|
2
|
+
var RateLimiter = class {
|
|
3
|
+
constructor(config = {}) {
|
|
4
|
+
this.name = "rate-limiter";
|
|
5
|
+
this.type = "middleware";
|
|
6
|
+
this.priority = 5;
|
|
7
|
+
// In-memory storage
|
|
8
|
+
this.limits = /* @__PURE__ */ new Map();
|
|
9
|
+
this.cleanupInterval = null;
|
|
10
|
+
// Queue for 'queue' mode
|
|
11
|
+
this.queues = /* @__PURE__ */ new Map();
|
|
12
|
+
this.processing = /* @__PURE__ */ new Set();
|
|
13
|
+
this.config = {
|
|
14
|
+
maxRequests: config.maxRequests ?? 100,
|
|
15
|
+
windowMs: config.windowMs ?? 6e4,
|
|
16
|
+
keyBy: config.keyBy ?? "userId",
|
|
17
|
+
onLimit: config.onLimit ?? "reject",
|
|
18
|
+
limitMessage: config.limitMessage ?? "Too many requests. Please try again later.",
|
|
19
|
+
includeHeaders: config.includeHeaders !== false,
|
|
20
|
+
skip: config.skip,
|
|
21
|
+
onRateLimited: config.onRateLimited,
|
|
22
|
+
storage: config.storage,
|
|
23
|
+
queue: config.queue
|
|
24
|
+
};
|
|
25
|
+
if (!this.config.storage) {
|
|
26
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 6e4);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async beforeRequest(messages, context) {
|
|
30
|
+
const key = this.getKey(context);
|
|
31
|
+
if (this.config.skip?.(key, context)) {
|
|
32
|
+
return { messages, metadata: { rateLimit: { skipped: true } } };
|
|
33
|
+
}
|
|
34
|
+
const result = await this.checkLimit(key);
|
|
35
|
+
if (!result.allowed) {
|
|
36
|
+
this.config.onRateLimited?.(key, result, context);
|
|
37
|
+
if (this.config.onLimit === "reject") {
|
|
38
|
+
const limitedMessage = {
|
|
39
|
+
role: "system",
|
|
40
|
+
content: `[RATE_LIMITED] ${this.config.limitMessage}`
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
messages: [limitedMessage],
|
|
44
|
+
metadata: {
|
|
45
|
+
rateLimit: {
|
|
46
|
+
limited: true,
|
|
47
|
+
...result
|
|
48
|
+
},
|
|
49
|
+
_skipLLM: true
|
|
50
|
+
// Signal to skip LLM call
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (this.config.onLimit === "queue") {
|
|
55
|
+
await this.enqueue(key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
messages,
|
|
60
|
+
metadata: {
|
|
61
|
+
rateLimit: {
|
|
62
|
+
...result,
|
|
63
|
+
key
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async afterResponse(response, context) {
|
|
69
|
+
if (context.metadata?._skipLLM) {
|
|
70
|
+
return {
|
|
71
|
+
response: this.config.limitMessage,
|
|
72
|
+
metadata: {
|
|
73
|
+
...context.metadata,
|
|
74
|
+
rateLimited: true
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const key = this.getKey(context);
|
|
79
|
+
this.processQueue(key);
|
|
80
|
+
if (this.config.includeHeaders && context.metadata?.rateLimit) {
|
|
81
|
+
return {
|
|
82
|
+
response,
|
|
83
|
+
metadata: {
|
|
84
|
+
...context.metadata,
|
|
85
|
+
headers: {
|
|
86
|
+
"X-RateLimit-Limit": context.metadata.rateLimit.limit,
|
|
87
|
+
"X-RateLimit-Remaining": context.metadata.rateLimit.remaining,
|
|
88
|
+
"X-RateLimit-Reset": context.metadata.rateLimit.resetAt?.toISOString()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { response, metadata: context.metadata };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get the rate limit key for a context
|
|
97
|
+
*/
|
|
98
|
+
getKey(context) {
|
|
99
|
+
if (typeof this.config.keyBy === "function") {
|
|
100
|
+
return this.config.keyBy(context);
|
|
101
|
+
}
|
|
102
|
+
switch (this.config.keyBy) {
|
|
103
|
+
case "userId":
|
|
104
|
+
return `user:${context.metadata?.userId || "anonymous"}`;
|
|
105
|
+
case "threadId":
|
|
106
|
+
return `thread:${context.threadId || "unknown"}`;
|
|
107
|
+
case "agentId":
|
|
108
|
+
return `agent:${context.agentId}`;
|
|
109
|
+
case "ip":
|
|
110
|
+
return `ip:${context.metadata?.ip || "unknown"}`;
|
|
111
|
+
case "global":
|
|
112
|
+
return "global";
|
|
113
|
+
default:
|
|
114
|
+
return `user:${context.metadata?.userId || "anonymous"}`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Check and increment rate limit
|
|
119
|
+
*/
|
|
120
|
+
async checkLimit(key) {
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
if (this.config.storage) {
|
|
123
|
+
const { count, resetAt } = await this.config.storage.increment(key, this.config.windowMs);
|
|
124
|
+
const allowed2 = count <= this.config.maxRequests;
|
|
125
|
+
return {
|
|
126
|
+
allowed: allowed2,
|
|
127
|
+
limit: this.config.maxRequests,
|
|
128
|
+
remaining: Math.max(0, this.config.maxRequests - count),
|
|
129
|
+
resetAt: new Date(resetAt),
|
|
130
|
+
retryAfter: allowed2 ? void 0 : Math.ceil((resetAt - now) / 1e3)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
let entry = this.limits.get(key);
|
|
134
|
+
if (!entry || now > entry.resetAt) {
|
|
135
|
+
entry = {
|
|
136
|
+
count: 1,
|
|
137
|
+
resetAt: now + this.config.windowMs
|
|
138
|
+
};
|
|
139
|
+
this.limits.set(key, entry);
|
|
140
|
+
} else {
|
|
141
|
+
entry.count++;
|
|
142
|
+
}
|
|
143
|
+
const allowed = entry.count <= this.config.maxRequests;
|
|
144
|
+
return {
|
|
145
|
+
allowed,
|
|
146
|
+
limit: this.config.maxRequests,
|
|
147
|
+
remaining: Math.max(0, this.config.maxRequests - entry.count),
|
|
148
|
+
resetAt: new Date(entry.resetAt),
|
|
149
|
+
retryAfter: allowed ? void 0 : Math.ceil((entry.resetAt - now) / 1e3)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Add request to queue
|
|
154
|
+
*/
|
|
155
|
+
enqueue(key) {
|
|
156
|
+
const queue = this.queues.get(key) || [];
|
|
157
|
+
const maxSize = this.config.queue?.maxSize || 10;
|
|
158
|
+
const timeout = this.config.queue?.timeout || 3e4;
|
|
159
|
+
if (queue.length >= maxSize) {
|
|
160
|
+
return Promise.reject(new Error("Rate limit queue is full"));
|
|
161
|
+
}
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
queue.push({
|
|
164
|
+
resolve,
|
|
165
|
+
reject,
|
|
166
|
+
addedAt: Date.now()
|
|
167
|
+
});
|
|
168
|
+
this.queues.set(key, queue);
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
const idx = queue.findIndex((q) => q.resolve === resolve);
|
|
171
|
+
if (idx > -1) {
|
|
172
|
+
queue.splice(idx, 1);
|
|
173
|
+
reject(new Error("Queue timeout"));
|
|
174
|
+
}
|
|
175
|
+
}, timeout);
|
|
176
|
+
this.processQueue(key);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Process queue for a key
|
|
181
|
+
*/
|
|
182
|
+
processQueue(key) {
|
|
183
|
+
if (this.processing.has(key)) return;
|
|
184
|
+
const queue = this.queues.get(key);
|
|
185
|
+
if (!queue || queue.length === 0) return;
|
|
186
|
+
this.processing.add(key);
|
|
187
|
+
const entry = this.limits.get(key);
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
if (!entry || now > entry.resetAt || entry.count < this.config.maxRequests) {
|
|
190
|
+
const item = queue.shift();
|
|
191
|
+
if (item) {
|
|
192
|
+
item.resolve(void 0);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
this.processing.delete(key);
|
|
196
|
+
if (queue.length > 0) {
|
|
197
|
+
const waitTime = entry ? Math.max(0, entry.resetAt - now) : 0;
|
|
198
|
+
setTimeout(() => this.processQueue(key), Math.min(waitTime, 1e3));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Cleanup expired entries
|
|
203
|
+
*/
|
|
204
|
+
cleanup() {
|
|
205
|
+
const now = Date.now();
|
|
206
|
+
for (const [key, entry] of this.limits.entries()) {
|
|
207
|
+
if (now > entry.resetAt) {
|
|
208
|
+
this.limits.delete(key);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get current usage for a key
|
|
214
|
+
*/
|
|
215
|
+
async getUsage(key) {
|
|
216
|
+
if (this.config.storage) {
|
|
217
|
+
const entry2 = await this.config.storage.get(key);
|
|
218
|
+
if (!entry2) {
|
|
219
|
+
return {
|
|
220
|
+
allowed: true,
|
|
221
|
+
limit: this.config.maxRequests,
|
|
222
|
+
remaining: this.config.maxRequests,
|
|
223
|
+
resetAt: /* @__PURE__ */ new Date()
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
allowed: entry2.count < this.config.maxRequests,
|
|
228
|
+
limit: this.config.maxRequests,
|
|
229
|
+
remaining: Math.max(0, this.config.maxRequests - entry2.count),
|
|
230
|
+
resetAt: new Date(entry2.resetAt)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const entry = this.limits.get(key);
|
|
234
|
+
if (!entry || Date.now() > entry.resetAt) {
|
|
235
|
+
return {
|
|
236
|
+
allowed: true,
|
|
237
|
+
limit: this.config.maxRequests,
|
|
238
|
+
remaining: this.config.maxRequests,
|
|
239
|
+
resetAt: /* @__PURE__ */ new Date()
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
allowed: entry.count < this.config.maxRequests,
|
|
244
|
+
limit: this.config.maxRequests,
|
|
245
|
+
remaining: Math.max(0, this.config.maxRequests - entry.count),
|
|
246
|
+
resetAt: new Date(entry.resetAt)
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Reset rate limit for a key
|
|
251
|
+
*/
|
|
252
|
+
reset(key) {
|
|
253
|
+
this.limits.delete(key);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Destroy the rate limiter
|
|
257
|
+
*/
|
|
258
|
+
destroy() {
|
|
259
|
+
if (this.cleanupInterval) {
|
|
260
|
+
clearInterval(this.cleanupInterval);
|
|
261
|
+
}
|
|
262
|
+
this.limits.clear();
|
|
263
|
+
this.queues.clear();
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
export {
|
|
267
|
+
RateLimiter
|
|
268
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@snap-agent/middleware-ratelimit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Rate limiting middleware for SnapAgent SDK - Per-user rate limiting and abuse prevention.",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
17
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"snap-agent",
|
|
24
|
+
"ai",
|
|
25
|
+
"agents",
|
|
26
|
+
"llm",
|
|
27
|
+
"middleware",
|
|
28
|
+
"rate-limit",
|
|
29
|
+
"throttle",
|
|
30
|
+
"abuse-prevention",
|
|
31
|
+
"typescript",
|
|
32
|
+
"plugin"
|
|
33
|
+
],
|
|
34
|
+
"author": "ViloTech",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@snap-agent/core": "^0.1.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^24.0.0",
|
|
44
|
+
"tsup": "^8.0.0",
|
|
45
|
+
"typescript": "^5.8.0",
|
|
46
|
+
"vitest": "^3.2.4"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"dist",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
],
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/vilo-hq/snap-agent.git",
|
|
56
|
+
"directory": "middlewares/ratelimit"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://github.com/vilo-hq/snap-agent/tree/main/middlewares/ratelimit",
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/vilo-hq/snap-agent/issues"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|