halt-rate 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +911 -0
- package/dist/adapters/express.d.mts +21 -0
- package/dist/adapters/express.d.ts +21 -0
- package/dist/adapters/express.js +71 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/express.mjs +68 -0
- package/dist/adapters/express.mjs.map +1 -0
- package/dist/adapters/next.d.mts +21 -0
- package/dist/adapters/next.d.ts +21 -0
- package/dist/adapters/next.js +627 -0
- package/dist/adapters/next.js.map +1 -0
- package/dist/adapters/next.mjs +623 -0
- package/dist/adapters/next.mjs.map +1 -0
- package/dist/index.d.mts +83 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +782 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +773 -0
- package/dist/index.mjs.map +1 -0
- package/dist/limiter-qGH_X_KH.d.mts +128 -0
- package/dist/limiter-qGH_X_KH.d.ts +128 -0
- package/package.json +78 -0
package/README.md
ADDED
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
# Halt TypeScript SDK
|
|
2
|
+
|
|
3
|
+
**Drop-in middleware that enforces consistent rate limits per IP/user/api-key with safe defaults, Redis-backed accuracy, and clean headers.**
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
🚀 **Four Rate Limiting Algorithms**
|
|
11
|
+
- Token Bucket (burst-friendly, recommended)
|
|
12
|
+
- Fixed Window (simple, fast)
|
|
13
|
+
- Sliding Window (accurate, memory-intensive)
|
|
14
|
+
- Leaky Bucket (traffic shaping, constant rate)
|
|
15
|
+
|
|
16
|
+
💾 **Multiple Storage Backends**
|
|
17
|
+
- In-Memory (development, single-threaded)
|
|
18
|
+
- Redis (production, distributed) - Coming soon
|
|
19
|
+
- PostgreSQL (ACID, relational)
|
|
20
|
+
- MongoDB (document store, TTL indexes)
|
|
21
|
+
- DynamoDB (AWS serverless, auto-scaling)
|
|
22
|
+
- Memcached (distributed cache, fast)
|
|
23
|
+
|
|
24
|
+
🎯 **SaaS-Ready Features**
|
|
25
|
+
- Plan-based rate limiting (FREE, STARTER, PRO, BUSINESS, ENTERPRISE)
|
|
26
|
+
- Quota management (hourly, daily, monthly, yearly)
|
|
27
|
+
- Penalty system (abuse detection, progressive penalties)
|
|
28
|
+
- Telemetry hooks (logging, metrics, observability)
|
|
29
|
+
|
|
30
|
+
🔧 **Framework Support**
|
|
31
|
+
- Express
|
|
32
|
+
- Next.js (App Router & Pages Router)
|
|
33
|
+
- Next.js Middleware
|
|
34
|
+
|
|
35
|
+
✨ **Smart Features**
|
|
36
|
+
- Automatic health check exemptions
|
|
37
|
+
- Private IP exemptions
|
|
38
|
+
- Custom exemption lists
|
|
39
|
+
- Weighted endpoints (cost-based limiting)
|
|
40
|
+
- Per-request algorithm override
|
|
41
|
+
- Standard rate limit headers (RateLimit-*, Retry-After)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install halt
|
|
49
|
+
# or
|
|
50
|
+
yarn add halt
|
|
51
|
+
# or
|
|
52
|
+
pnpm add halt
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Optional Dependencies
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# PostgreSQL support
|
|
59
|
+
npm install pg
|
|
60
|
+
|
|
61
|
+
# MongoDB support
|
|
62
|
+
npm install mongodb
|
|
63
|
+
|
|
64
|
+
# DynamoDB support
|
|
65
|
+
npm install @aws-sdk/client-dynamodb @aws-sdk/util-dynamodb
|
|
66
|
+
|
|
67
|
+
# Memcached support
|
|
68
|
+
npm install memcached
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Storage Backends
|
|
74
|
+
|
|
75
|
+
### In-Memory (Development)
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { InMemoryStore } from 'halt';
|
|
79
|
+
|
|
80
|
+
const store = new InMemoryStore();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### PostgreSQL
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { PostgresStore } from 'halt/stores/postgres';
|
|
87
|
+
|
|
88
|
+
const store = new PostgresStore({
|
|
89
|
+
host: 'localhost',
|
|
90
|
+
port: 5432,
|
|
91
|
+
database: 'mydb',
|
|
92
|
+
user: 'user',
|
|
93
|
+
password: 'password',
|
|
94
|
+
tableName: 'rate_limits', // optional
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### MongoDB
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { MongoDBStore } from 'halt/stores/mongodb';
|
|
102
|
+
|
|
103
|
+
const store = new MongoDBStore({
|
|
104
|
+
connectionString: 'mongodb://localhost:27017',
|
|
105
|
+
database: 'halt',
|
|
106
|
+
collection: 'rate_limits',
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### DynamoDB
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { DynamoDBStore } from 'halt/stores/dynamodb';
|
|
114
|
+
|
|
115
|
+
const store = new DynamoDBStore({
|
|
116
|
+
tableName: 'rate_limits',
|
|
117
|
+
region: 'us-east-1',
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Memcached
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import { MemcachedStore } from 'halt/stores/memcached';
|
|
125
|
+
|
|
126
|
+
const store = new MemcachedStore({
|
|
127
|
+
servers: 'localhost:11211',
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## SaaS Features
|
|
134
|
+
|
|
135
|
+
### Plan-Based Rate Limiting
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { getPlanPolicy, PLAN_FREE, PLAN_PRO, PLAN_ENTERPRISE } from 'halt';
|
|
139
|
+
|
|
140
|
+
// Use plan-based presets
|
|
141
|
+
const freePolicy = PLAN_FREE; // 100 req/hour
|
|
142
|
+
const proPolicy = PLAN_PRO; // 2000 req/hour
|
|
143
|
+
const enterprisePolicy = PLAN_ENTERPRISE; // 20000 req/hour
|
|
144
|
+
|
|
145
|
+
// Get policy by plan name
|
|
146
|
+
const policy = getPlanPolicy('pro');
|
|
147
|
+
|
|
148
|
+
// Dynamic policy resolution
|
|
149
|
+
function getUserPolicy(user: User) {
|
|
150
|
+
return getPlanPolicy(user.plan);
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Quota Management
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { QuotaManager, Quota, QuotaPeriod } from 'halt/core/quota';
|
|
158
|
+
|
|
159
|
+
const quotaManager = new QuotaManager(store);
|
|
160
|
+
|
|
161
|
+
const monthlyQuota: Quota = {
|
|
162
|
+
name: 'api_calls',
|
|
163
|
+
limit: 100000,
|
|
164
|
+
period: QuotaPeriod.MONTHLY,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Check quota
|
|
168
|
+
const { allowed, quota: currentQuota } = await quotaManager.checkQuota(
|
|
169
|
+
'user_123',
|
|
170
|
+
monthlyQuota
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (allowed) {
|
|
174
|
+
// Consume quota
|
|
175
|
+
await quotaManager.consumeQuota('user_123', monthlyQuota, 1);
|
|
176
|
+
} else {
|
|
177
|
+
console.log(`Quota exceeded. Resets at: ${currentQuota.resetAt}`);
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Penalty System
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { PenaltyManager, PENALTY_MODERATE } from 'halt/core/penalty';
|
|
185
|
+
|
|
186
|
+
const penaltyManager = new PenaltyManager(store, PENALTY_MODERATE);
|
|
187
|
+
|
|
188
|
+
// Record violation
|
|
189
|
+
const penalty = await penaltyManager.recordViolation('user_123', 1.0);
|
|
190
|
+
|
|
191
|
+
// Check penalty status
|
|
192
|
+
if (penaltyManager.isActive(penalty)) {
|
|
193
|
+
console.log(`User penalized until: ${penalty.penaltyUntil}`);
|
|
194
|
+
console.log(`Abuse score: ${penalty.abuseScore}`);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Telemetry & Observability
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import { LoggingTelemetry, MetricsTelemetry, CompositeTelemetry } from 'halt/core/telemetry';
|
|
202
|
+
|
|
203
|
+
// Logging telemetry
|
|
204
|
+
const telemetry = new LoggingTelemetry(console);
|
|
205
|
+
|
|
206
|
+
// Metrics telemetry (with your metrics client)
|
|
207
|
+
class CustomMetrics {
|
|
208
|
+
increment(metric: string, tags?: any) { /* ... */ }
|
|
209
|
+
gauge(metric: string, value: number, tags?: any) { /* ... */ }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const metricsTelemetry = new MetricsTelemetry(new CustomMetrics());
|
|
213
|
+
|
|
214
|
+
// Combine multiple telemetry hooks
|
|
215
|
+
const compositeTelemetry = new CompositeTelemetry([
|
|
216
|
+
new LoggingTelemetry(console),
|
|
217
|
+
metricsTelemetry,
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
// Use with limiter
|
|
221
|
+
const limiter = new RateLimiter({
|
|
222
|
+
store,
|
|
223
|
+
policy,
|
|
224
|
+
telemetry: compositeTelemetry,
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Quick Start
|
|
231
|
+
|
|
232
|
+
### Express
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import express from 'express';
|
|
236
|
+
import { RateLimiter, InMemoryStore, presets } from 'halt';
|
|
237
|
+
import { haltMiddleware } from 'halt/express';
|
|
238
|
+
|
|
239
|
+
const app = express();
|
|
240
|
+
|
|
241
|
+
const limiter = new RateLimiter({
|
|
242
|
+
store: new InMemoryStore(),
|
|
243
|
+
policy: presets.PUBLIC_API, // 100 req/min
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
app.use(haltMiddleware({ limiter }));
|
|
247
|
+
|
|
248
|
+
app.get('/', (req, res) => {
|
|
249
|
+
res.json({ message: 'Hello World' });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
app.listen(3000);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Next.js App Router
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// app/api/data/route.ts
|
|
259
|
+
import { withHalt } from 'halt/next';
|
|
260
|
+
import { InMemoryStore, presets } from 'halt';
|
|
261
|
+
|
|
262
|
+
const store = new InMemoryStore();
|
|
263
|
+
|
|
264
|
+
async function handler(req: Request) {
|
|
265
|
+
return Response.json({ message: 'Hello World' });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export const GET = withHalt(handler, {
|
|
269
|
+
store,
|
|
270
|
+
policy: presets.PUBLIC_API,
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Next.js Middleware
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// middleware.ts
|
|
278
|
+
import { haltMiddleware } from 'halt/next';
|
|
279
|
+
import { InMemoryStore, presets } from 'halt';
|
|
280
|
+
|
|
281
|
+
export default haltMiddleware({
|
|
282
|
+
store: new InMemoryStore(),
|
|
283
|
+
policy: presets.PUBLIC_API,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
export const config = {
|
|
287
|
+
matcher: '/api/:path*',
|
|
288
|
+
};
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Preset Policies
|
|
294
|
+
|
|
295
|
+
Halt comes with battle-tested presets:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import { presets } from 'halt';
|
|
299
|
+
|
|
300
|
+
// Public API - moderate limits
|
|
301
|
+
presets.PUBLIC_API
|
|
302
|
+
// 100 requests/minute, burst: 120
|
|
303
|
+
|
|
304
|
+
// Authentication endpoints - strict
|
|
305
|
+
presets.AUTH_ENDPOINTS
|
|
306
|
+
// 5 requests/minute, burst: 10, 5min cooldown
|
|
307
|
+
|
|
308
|
+
// Expensive operations - very strict
|
|
309
|
+
presets.EXPENSIVE_OPS
|
|
310
|
+
// 10 requests/hour, burst: 15, cost: 10
|
|
311
|
+
|
|
312
|
+
// Strict API - for sensitive ops
|
|
313
|
+
presets.STRICT_API
|
|
314
|
+
// 20 requests/minute, burst: 25
|
|
315
|
+
|
|
316
|
+
// Generous API - for internal services
|
|
317
|
+
presets.GENEROUS_API
|
|
318
|
+
// 1000 requests/minute, burst: 1200
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Custom Policies
|
|
324
|
+
|
|
325
|
+
### Basic Custom Policy
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
import { Policy, KeyStrategy, Algorithm } from 'halt';
|
|
329
|
+
|
|
330
|
+
const customPolicy: Policy = {
|
|
331
|
+
name: 'custom',
|
|
332
|
+
limit: 50,
|
|
333
|
+
window: 60, // 1 minute
|
|
334
|
+
burst: 60,
|
|
335
|
+
algorithm: Algorithm.TOKEN_BUCKET,
|
|
336
|
+
keyStrategy: KeyStrategy.IP,
|
|
337
|
+
};
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Advanced Examples
|
|
341
|
+
|
|
342
|
+
#### Rate Limit by User
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
const userPolicy: Policy = {
|
|
346
|
+
name: 'per_user',
|
|
347
|
+
limit: 100,
|
|
348
|
+
window: 3600, // 1 hour
|
|
349
|
+
keyStrategy: KeyStrategy.USER,
|
|
350
|
+
};
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
#### Rate Limit by API Key
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
const apiPolicy: Policy = {
|
|
357
|
+
name: 'per_api_key',
|
|
358
|
+
limit: 1000,
|
|
359
|
+
window: 60,
|
|
360
|
+
keyStrategy: KeyStrategy.API_KEY,
|
|
361
|
+
};
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
#### Composite Keys (User + IP)
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
const compositePolicy: Policy = {
|
|
368
|
+
name: 'user_and_ip',
|
|
369
|
+
limit: 50,
|
|
370
|
+
window: 60,
|
|
371
|
+
keyStrategy: KeyStrategy.COMPOSITE,
|
|
372
|
+
};
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
#### Weighted Endpoints
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
const expensivePolicy: Policy = {
|
|
379
|
+
name: 'llm_endpoint',
|
|
380
|
+
limit: 100,
|
|
381
|
+
window: 3600,
|
|
382
|
+
cost: 10, // Each request costs 10 tokens
|
|
383
|
+
algorithm: Algorithm.TOKEN_BUCKET,
|
|
384
|
+
};
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Algorithms
|
|
390
|
+
|
|
391
|
+
### Token Bucket (Recommended)
|
|
392
|
+
|
|
393
|
+
Best for most use cases. Handles bursts naturally while maintaining average rate.
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
import { Policy, Algorithm } from 'halt';
|
|
397
|
+
|
|
398
|
+
const policy: Policy = {
|
|
399
|
+
name: 'token_bucket',
|
|
400
|
+
limit: 100, // 100 tokens per window
|
|
401
|
+
window: 60, // 1 minute
|
|
402
|
+
burst: 120, // Allow bursts up to 120
|
|
403
|
+
algorithm: Algorithm.TOKEN_BUCKET,
|
|
404
|
+
};
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Pros:**
|
|
408
|
+
- ✅ Handles burst traffic naturally
|
|
409
|
+
- ✅ Smooth rate limiting
|
|
410
|
+
- ✅ Low memory usage
|
|
411
|
+
|
|
412
|
+
**Cons:**
|
|
413
|
+
- ❌ Slightly more complex than fixed window
|
|
414
|
+
|
|
415
|
+
### Fixed Window
|
|
416
|
+
|
|
417
|
+
Simple and fast. Good for strict limits.
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
const policy: Policy = {
|
|
421
|
+
name: 'fixed_window',
|
|
422
|
+
limit: 100,
|
|
423
|
+
window: 60,
|
|
424
|
+
algorithm: Algorithm.FIXED_WINDOW,
|
|
425
|
+
};
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**Pros:**
|
|
429
|
+
- ✅ Very simple
|
|
430
|
+
- ✅ Low memory usage
|
|
431
|
+
- ✅ Fast
|
|
432
|
+
|
|
433
|
+
**Cons:**
|
|
434
|
+
- ❌ Can allow 2x limit at window boundaries
|
|
435
|
+
- ❌ No burst handling
|
|
436
|
+
|
|
437
|
+
### Sliding Window
|
|
438
|
+
|
|
439
|
+
Most accurate but uses more memory.
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
const policy: Policy = {
|
|
443
|
+
name: 'sliding_window',
|
|
444
|
+
limit: 100,
|
|
445
|
+
window: 60,
|
|
446
|
+
algorithm: Algorithm.SLIDING_WINDOW,
|
|
447
|
+
};
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Pros:**
|
|
451
|
+
- ✅ Most accurate
|
|
452
|
+
- ✅ No boundary issues
|
|
453
|
+
|
|
454
|
+
**Cons:**
|
|
455
|
+
- ❌ Higher memory usage
|
|
456
|
+
- ❌ Slightly slower
|
|
457
|
+
|
|
458
|
+
### Leaky Bucket
|
|
459
|
+
|
|
460
|
+
Traffic shaping with constant processing rate.
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
const policy: Policy = {
|
|
464
|
+
name: 'leaky_bucket',
|
|
465
|
+
limit: 100,
|
|
466
|
+
window: 60,
|
|
467
|
+
burst: 120,
|
|
468
|
+
algorithm: Algorithm.LEAKY_BUCKET,
|
|
469
|
+
};
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
**Pros:**
|
|
473
|
+
- ✅ Smooth traffic shaping
|
|
474
|
+
- ✅ Predictable behavior
|
|
475
|
+
|
|
476
|
+
**Cons:**
|
|
477
|
+
- ❌ May delay legitimate bursts
|
|
478
|
+
|
|
479
|
+
**Use case:** Strict QoS requirements, traffic shaping
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Key Strategies
|
|
484
|
+
|
|
485
|
+
### IP-based (Default)
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
import { Policy, KeyStrategy, RateLimiter } from 'halt';
|
|
489
|
+
|
|
490
|
+
const policy: Policy = {
|
|
491
|
+
name: 'per_ip',
|
|
492
|
+
limit: 100,
|
|
493
|
+
window: 60,
|
|
494
|
+
keyStrategy: KeyStrategy.IP,
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// With trusted proxies (for X-Forwarded-For)
|
|
498
|
+
const limiter = new RateLimiter({
|
|
499
|
+
store,
|
|
500
|
+
policy,
|
|
501
|
+
trustedProxies: ['10.0.0.0/8', '172.16.0.0/12'],
|
|
502
|
+
});
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### User-based
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
const policy: Policy = {
|
|
509
|
+
name: 'per_user',
|
|
510
|
+
limit: 1000,
|
|
511
|
+
window: 3600,
|
|
512
|
+
keyStrategy: KeyStrategy.USER,
|
|
513
|
+
};
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Extracts user ID from:
|
|
517
|
+
- `request.user.id`
|
|
518
|
+
- `request.userId`
|
|
519
|
+
|
|
520
|
+
### API Key-based
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
const policy: Policy = {
|
|
524
|
+
name: 'per_api_key',
|
|
525
|
+
limit: 5000,
|
|
526
|
+
window: 3600,
|
|
527
|
+
keyStrategy: KeyStrategy.API_KEY,
|
|
528
|
+
};
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Extracts API key from headers:
|
|
532
|
+
- `X-API-Key`
|
|
533
|
+
- `Authorization` (including Bearer tokens)
|
|
534
|
+
|
|
535
|
+
### Custom Key Extraction
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
function extractOrgId(request: any): string | null {
|
|
539
|
+
return request.headers['x-organization-id'] || null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const policy: Policy = {
|
|
543
|
+
name: 'per_org',
|
|
544
|
+
limit: 10000,
|
|
545
|
+
window: 3600,
|
|
546
|
+
keyStrategy: KeyStrategy.CUSTOM,
|
|
547
|
+
keyExtractor: extractOrgId,
|
|
548
|
+
};
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
## Exemptions
|
|
554
|
+
|
|
555
|
+
### Automatic Exemptions
|
|
556
|
+
|
|
557
|
+
Halt automatically exempts:
|
|
558
|
+
|
|
559
|
+
**Health Checks:**
|
|
560
|
+
- `/health`
|
|
561
|
+
- `/ping`
|
|
562
|
+
- `/ready`
|
|
563
|
+
- `/healthz`
|
|
564
|
+
- `/livez`
|
|
565
|
+
|
|
566
|
+
**Private IPs:**
|
|
567
|
+
- `127.0.0.1` (localhost)
|
|
568
|
+
- `10.0.0.0/8`
|
|
569
|
+
- `172.16.0.0/12`
|
|
570
|
+
- `192.168.0.0/16`
|
|
571
|
+
|
|
572
|
+
### Custom Exemptions
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
const policy: Policy = {
|
|
576
|
+
name: 'custom',
|
|
577
|
+
limit: 100,
|
|
578
|
+
window: 60,
|
|
579
|
+
exemptions: [
|
|
580
|
+
'/admin', // Path exemption
|
|
581
|
+
'/internal', // Another path
|
|
582
|
+
'192.168.1.100', // IP exemption
|
|
583
|
+
],
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// Disable private IP exemptions
|
|
587
|
+
const limiter = new RateLimiter({
|
|
588
|
+
store,
|
|
589
|
+
policy,
|
|
590
|
+
exemptPrivateIps: false,
|
|
591
|
+
});
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## Per-Route Rate Limiting
|
|
597
|
+
|
|
598
|
+
### Express - Route-Specific
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
import { createLimiter } from 'halt/express';
|
|
602
|
+
|
|
603
|
+
const publicLimiter = new RateLimiter({ store, policy: presets.PUBLIC_API });
|
|
604
|
+
const authLimiter = new RateLimiter({ store, policy: presets.AUTH_ENDPOINTS });
|
|
605
|
+
|
|
606
|
+
app.get('/api/data', createLimiter(publicLimiter), (req, res) => {
|
|
607
|
+
res.json({ data: '...' });
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
app.post('/auth/login', createLimiter(authLimiter), (req, res) => {
|
|
611
|
+
res.json({ token: '...' });
|
|
612
|
+
});
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Next.js - Multiple Policies
|
|
616
|
+
|
|
617
|
+
```typescript
|
|
618
|
+
// app/api/data/route.ts
|
|
619
|
+
import { withPolicy } from 'halt/next';
|
|
620
|
+
import { InMemoryStore, presets } from 'halt';
|
|
621
|
+
|
|
622
|
+
const store = new InMemoryStore();
|
|
623
|
+
|
|
624
|
+
async function handler(req: Request) {
|
|
625
|
+
return Response.json({ data: '...' });
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export const GET = withPolicy(handler, presets.PUBLIC_API, store);
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
// app/api/auth/login/route.ts
|
|
633
|
+
import { withPolicy } from 'halt/next';
|
|
634
|
+
import { InMemoryStore, presets } from 'halt';
|
|
635
|
+
|
|
636
|
+
const store = new InMemoryStore();
|
|
637
|
+
|
|
638
|
+
async function handler(req: Request) {
|
|
639
|
+
return Response.json({ token: '...' });
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export const POST = withPolicy(handler, presets.AUTH_ENDPOINTS, store);
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## Response Headers
|
|
648
|
+
|
|
649
|
+
All responses include standard rate limit headers:
|
|
650
|
+
|
|
651
|
+
```http
|
|
652
|
+
HTTP/1.1 200 OK
|
|
653
|
+
RateLimit-Limit: 100
|
|
654
|
+
RateLimit-Remaining: 95
|
|
655
|
+
RateLimit-Reset: 1708024800
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
When rate limited (429):
|
|
659
|
+
|
|
660
|
+
```http
|
|
661
|
+
HTTP/1.1 429 Too Many Requests
|
|
662
|
+
RateLimit-Limit: 100
|
|
663
|
+
RateLimit-Remaining: 0
|
|
664
|
+
RateLimit-Reset: 1708024860
|
|
665
|
+
Retry-After: 42
|
|
666
|
+
|
|
667
|
+
{
|
|
668
|
+
"error": "rate_limit_exceeded",
|
|
669
|
+
"message": "Too many requests. Please try again later.",
|
|
670
|
+
"retryAfter": 42
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
## Advanced Usage
|
|
677
|
+
|
|
678
|
+
### Dynamic Cost per Request
|
|
679
|
+
|
|
680
|
+
```typescript
|
|
681
|
+
// Next.js API Route
|
|
682
|
+
import { RateLimiter, InMemoryStore, presets } from 'halt';
|
|
683
|
+
|
|
684
|
+
const limiter = new RateLimiter({
|
|
685
|
+
store: new InMemoryStore(),
|
|
686
|
+
policy: presets.EXPENSIVE_OPS,
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
export async function POST(req: Request) {
|
|
690
|
+
const body = await req.json();
|
|
691
|
+
const promptLength = body.prompt?.length || 0;
|
|
692
|
+
|
|
693
|
+
// Calculate cost based on request
|
|
694
|
+
const cost = Math.max(1, Math.floor(promptLength / 100));
|
|
695
|
+
|
|
696
|
+
// Check with custom cost
|
|
697
|
+
const decision = limiter.check(req, cost);
|
|
698
|
+
|
|
699
|
+
if (!decision.allowed) {
|
|
700
|
+
return Response.json(
|
|
701
|
+
{
|
|
702
|
+
error: 'rate_limit_exceeded',
|
|
703
|
+
message: 'Too many requests',
|
|
704
|
+
retryAfter: decision.retryAfter,
|
|
705
|
+
},
|
|
706
|
+
{ status: 429 }
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return Response.json({ response: '...' });
|
|
711
|
+
}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### Multiple Policies (Express)
|
|
715
|
+
|
|
716
|
+
```typescript
|
|
717
|
+
import express from 'express';
|
|
718
|
+
import { RateLimiter, InMemoryStore, presets } from 'halt';
|
|
719
|
+
import { haltMiddleware, createLimiter } from 'halt/express';
|
|
720
|
+
|
|
721
|
+
const app = express();
|
|
722
|
+
|
|
723
|
+
// Global rate limit
|
|
724
|
+
const globalLimiter = new RateLimiter({
|
|
725
|
+
store: new InMemoryStore(),
|
|
726
|
+
policy: presets.GENEROUS_API,
|
|
727
|
+
});
|
|
728
|
+
app.use(haltMiddleware({ limiter: globalLimiter }));
|
|
729
|
+
|
|
730
|
+
// Endpoint-specific limits
|
|
731
|
+
const authLimiter = new RateLimiter({
|
|
732
|
+
store: new InMemoryStore(),
|
|
733
|
+
policy: presets.AUTH_ENDPOINTS,
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
app.post('/auth/login', createLimiter(authLimiter), (req, res) => {
|
|
737
|
+
// This endpoint has BOTH global AND auth limits
|
|
738
|
+
res.json({ token: '...' });
|
|
739
|
+
});
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
### Custom Blocked Response
|
|
743
|
+
|
|
744
|
+
```typescript
|
|
745
|
+
import { haltMiddleware } from 'halt/express';
|
|
746
|
+
|
|
747
|
+
app.use(haltMiddleware({
|
|
748
|
+
limiter,
|
|
749
|
+
onBlocked: (req, res) => {
|
|
750
|
+
res.status(429).json({
|
|
751
|
+
error: 'RATE_LIMIT_EXCEEDED',
|
|
752
|
+
message: 'Slow down! Try again later.',
|
|
753
|
+
timestamp: Date.now(),
|
|
754
|
+
});
|
|
755
|
+
},
|
|
756
|
+
}));
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
## Testing
|
|
762
|
+
|
|
763
|
+
```typescript
|
|
764
|
+
import { describe, it, expect } from 'vitest';
|
|
765
|
+
import { RateLimiter, InMemoryStore, Policy, Algorithm } from 'halt';
|
|
766
|
+
|
|
767
|
+
describe('Rate Limiting', () => {
|
|
768
|
+
it('should block after limit exceeded', () => {
|
|
769
|
+
const policy: Policy = {
|
|
770
|
+
name: 'test',
|
|
771
|
+
limit: 5,
|
|
772
|
+
window: 60,
|
|
773
|
+
algorithm: Algorithm.TOKEN_BUCKET,
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
const limiter = new RateLimiter({
|
|
777
|
+
store: new InMemoryStore(),
|
|
778
|
+
policy,
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
// Mock request
|
|
782
|
+
const request = {
|
|
783
|
+
socket: { remoteAddress: '127.0.0.1' },
|
|
784
|
+
headers: {},
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
// First 5 requests should succeed
|
|
788
|
+
for (let i = 0; i < 5; i++) {
|
|
789
|
+
const decision = limiter.check(request);
|
|
790
|
+
expect(decision.allowed).toBe(true);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// 6th request should be blocked
|
|
794
|
+
const decision = limiter.check(request);
|
|
795
|
+
expect(decision.allowed).toBe(false);
|
|
796
|
+
expect(decision.retryAfter).toBeGreaterThan(0);
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
---
|
|
802
|
+
|
|
803
|
+
## Troubleshooting
|
|
804
|
+
|
|
805
|
+
### Rate limits not working?
|
|
806
|
+
|
|
807
|
+
1. **Check if request is exempted:**
|
|
808
|
+
- Health check paths are auto-exempted
|
|
809
|
+
- Private IPs are auto-exempted (disable with `exemptPrivateIps: false`)
|
|
810
|
+
|
|
811
|
+
2. **Verify key extraction:**
|
|
812
|
+
```typescript
|
|
813
|
+
// Debug key extraction
|
|
814
|
+
const key = (limiter as any).extractKey(request);
|
|
815
|
+
console.log('Rate limit key:', key);
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
3. **Check storage:**
|
|
819
|
+
- InMemoryStore doesn't persist across restarts
|
|
820
|
+
- Each process has its own memory store
|
|
821
|
+
|
|
822
|
+
### Headers not appearing?
|
|
823
|
+
|
|
824
|
+
Make sure middleware is added correctly and responses are going through the middleware chain.
|
|
825
|
+
|
|
826
|
+
### Different limits for same IP?
|
|
827
|
+
|
|
828
|
+
You might be using different policy names. Each policy maintains separate counters:
|
|
829
|
+
|
|
830
|
+
```typescript
|
|
831
|
+
// These are SEPARATE limits
|
|
832
|
+
const policy1: Policy = { name: 'api_v1', limit: 100, window: 60 };
|
|
833
|
+
const policy2: Policy = { name: 'api_v2', limit: 100, window: 60 };
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
---
|
|
837
|
+
|
|
838
|
+
## Performance
|
|
839
|
+
|
|
840
|
+
| Algorithm | Throughput | Memory | Accuracy |
|
|
841
|
+
|-----------|-----------|--------|----------|
|
|
842
|
+
| Token Bucket | ~100k req/s | Low | High |
|
|
843
|
+
| Fixed Window | ~120k req/s | Very Low | Medium |
|
|
844
|
+
| Sliding Window | ~80k req/s | Medium | Very High |
|
|
845
|
+
| Leaky Bucket | ~90k req/s | Low | High |
|
|
846
|
+
|
|
847
|
+
*Benchmarks on M1 Mac, in-memory storage*
|
|
848
|
+
|
|
849
|
+
All algorithms use O(1) memory per key (except Sliding Window which uses O(precision) per key).
|
|
850
|
+
|
|
851
|
+
---
|
|
852
|
+
|
|
853
|
+
## TypeScript Support
|
|
854
|
+
|
|
855
|
+
Halt is written in TypeScript and provides full type safety:
|
|
856
|
+
|
|
857
|
+
```typescript
|
|
858
|
+
import type { Policy, Decision, RateLimiterOptions } from 'halt';
|
|
859
|
+
|
|
860
|
+
const policy: Policy = {
|
|
861
|
+
name: 'typed',
|
|
862
|
+
limit: 100,
|
|
863
|
+
window: 60,
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
const decision: Decision = limiter.check(request);
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
---
|
|
870
|
+
|
|
871
|
+
## License
|
|
872
|
+
|
|
873
|
+
MIT
|
|
874
|
+
|
|
875
|
+
---
|
|
876
|
+
|
|
877
|
+
## Contributing
|
|
878
|
+
|
|
879
|
+
Contributions welcome! Please open an issue or PR on GitHub.
|
|
880
|
+
|
|
881
|
+
---
|
|
882
|
+
|
|
883
|
+
## Roadmap
|
|
884
|
+
|
|
885
|
+
### v0.3 (Current)
|
|
886
|
+
- ✅ Token Bucket algorithm
|
|
887
|
+
- ✅ Fixed Window algorithm
|
|
888
|
+
- ✅ Sliding Window algorithm
|
|
889
|
+
- ✅ Leaky Bucket algorithm
|
|
890
|
+
- ✅ In-memory storage
|
|
891
|
+
- ✅ PostgreSQL storage
|
|
892
|
+
- ✅ MongoDB storage
|
|
893
|
+
- ✅ DynamoDB storage
|
|
894
|
+
- ✅ Memcached storage
|
|
895
|
+
- ✅ Quota system
|
|
896
|
+
- ✅ Penalty system
|
|
897
|
+
- ✅ Telemetry hooks
|
|
898
|
+
- ✅ Plan-based presets
|
|
899
|
+
- ⏳ Redis storage
|
|
900
|
+
|
|
901
|
+
### v0.4 (Next)
|
|
902
|
+
- OpenTelemetry integration
|
|
903
|
+
- Distributed global limits
|
|
904
|
+
- Idempotent response mode
|
|
905
|
+
- Enhanced metrics and dashboards
|
|
906
|
+
|
|
907
|
+
### v1.0 (Future)
|
|
908
|
+
- Adaptive limits
|
|
909
|
+
- Advanced abuse detection
|
|
910
|
+
- Multi-region support
|
|
911
|
+
- GraphQL support
|