halt-rate 0.1.1 → 0.3.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 +28 -885
- package/dist/adapters/express.d.mts +1 -1
- package/dist/adapters/express.d.ts +1 -1
- package/dist/adapters/next.d.mts +1 -1
- package/dist/adapters/next.d.ts +1 -1
- package/dist/adapters/next.js +60 -6
- package/dist/adapters/next.js.map +1 -1
- package/dist/adapters/next.mjs +60 -6
- package/dist/adapters/next.mjs.map +1 -1
- package/dist/index.d.mts +176 -3
- package/dist/index.d.ts +176 -3
- package/dist/index.js +867 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +850 -7
- package/dist/index.mjs.map +1 -1
- package/dist/limiter-DqVVE0Kl.d.mts +310 -0
- package/dist/limiter-DqVVE0Kl.d.ts +310 -0
- package/package.json +17 -4
- package/dist/limiter-qGH_X_KH.d.mts +0 -128
- package/dist/limiter-qGH_X_KH.d.ts +0 -128
package/README.md
CHANGED
|
@@ -1,911 +1,54 @@
|
|
|
1
|
-
# Halt TypeScript SDK
|
|
1
|
+
# Halt — TypeScript SDK
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**SaaS-aware, cross-language rate limiting.** Per-user / per-API-key / per-plan limits, quotas, weighted endpoints, abuse controls, atomic Redis accuracy, and built-in observability — with a matching [Python package](https://pypi.org/project/halt-rate/).
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/halt-rate)
|
|
5
6
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
-
[](https://www.typescriptlang.org/)
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
```
|
|
8
|
+
📖 **Full documentation: [halt.afroawi.com](https://halt.afroawi.com)**
|
|
54
9
|
|
|
55
|
-
|
|
10
|
+
## Install
|
|
56
11
|
|
|
57
12
|
```bash
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
13
|
+
npm install halt-rate
|
|
14
|
+
# optional: production store + metrics
|
|
15
|
+
npm install ioredis @opentelemetry/api
|
|
656
16
|
```
|
|
657
17
|
|
|
658
|
-
|
|
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
|
|
18
|
+
## Quick start
|
|
679
19
|
|
|
680
20
|
```typescript
|
|
681
|
-
|
|
682
|
-
import { RateLimiter, InMemoryStore, presets } from 'halt';
|
|
21
|
+
import { RateLimiter, InMemoryStore, presets } from 'halt-rate';
|
|
683
22
|
|
|
684
23
|
const limiter = new RateLimiter({
|
|
685
|
-
store: new InMemoryStore(),
|
|
686
|
-
policy: presets.
|
|
24
|
+
store: new InMemoryStore(), // use RedisStore in production
|
|
25
|
+
policy: presets.PUBLIC_API, // 100 req/min per IP
|
|
687
26
|
});
|
|
688
27
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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: '...' });
|
|
28
|
+
const decision = await limiter.check(req);
|
|
29
|
+
if (!decision.allowed) {
|
|
30
|
+
// respond 429, Retry-After: decision.retryAfter
|
|
711
31
|
}
|
|
712
32
|
```
|
|
713
33
|
|
|
714
|
-
|
|
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
|
-
```
|
|
34
|
+
Production Redis, Express/Next adapters, plan-based limits, quotas, penalties, and
|
|
35
|
+
observability are covered in the docs → **[halt.afroawi.com/docs](https://halt.afroawi.com/docs)**.
|
|
835
36
|
|
|
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
|
|
37
|
+
## Features
|
|
854
38
|
|
|
855
|
-
|
|
39
|
+
- Algorithms: token bucket, fixed/sliding window, leaky bucket
|
|
40
|
+
- Keys: IP, user, API key, composite, or custom
|
|
41
|
+
- Atomic **Redis** store (Lua, cluster-safe, fail-open) + in-memory dev store
|
|
42
|
+
- SaaS: per-plan limits, quotas, weighted endpoints, abuse penalties
|
|
43
|
+
- Observability: `StatsCollector` + OpenTelemetry metrics
|
|
44
|
+
- Adapters: Express, Next.js
|
|
856
45
|
|
|
857
|
-
|
|
858
|
-
import type { Policy, Decision, RateLimiterOptions } from 'halt';
|
|
46
|
+
## Links
|
|
859
47
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
window: 60,
|
|
864
|
-
};
|
|
865
|
-
|
|
866
|
-
const decision: Decision = limiter.check(request);
|
|
867
|
-
```
|
|
868
|
-
|
|
869
|
-
---
|
|
48
|
+
- Docs: https://halt.afroawi.com
|
|
49
|
+
- npm: https://www.npmjs.com/package/halt-rate
|
|
50
|
+
- Source & issues: https://github.com/surafel-kindu/halt
|
|
870
51
|
|
|
871
52
|
## License
|
|
872
53
|
|
|
873
54
|
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
|