@joint-ops/hitlimit 1.0.5 → 1.0.6
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 +94 -7
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +5 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/headers.d.ts.map +1 -1
- package/dist/core/headers.js +7 -0
- package/dist/core/headers.js.map +1 -1
- package/dist/core/limiter.d.ts.map +1 -1
- package/dist/core/limiter.js +90 -24
- package/dist/core/limiter.js.map +1 -1
- package/dist/core/response.d.ts.map +1 -1
- package/dist/core/response.js +7 -1
- package/dist/core/response.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -59
- package/dist/index.js.map +1 -1
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +63 -0
- package/dist/stores/memory.js.map +1 -1
- package/dist/stores/redis.d.ts.map +1 -1
- package/dist/stores/redis.js +20 -1
- package/dist/stores/redis.js.map +1 -1
- package/dist/stores/sqlite.d.ts.map +1 -1
- package/dist/stores/sqlite.js +49 -1
- package/dist/stores/sqlite.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
## Why hitlimit?
|
|
16
16
|
|
|
17
|
-
- **Blazing Fast** -
|
|
17
|
+
- **Blazing Fast** - 2,450,000+ ops/sec with memory store (multi-IP scenarios), ~7% HTTP overhead
|
|
18
18
|
- **Zero Config** - Works out of the box with sensible defaults
|
|
19
19
|
- **Tiny Footprint** - Only ~7KB core, zero runtime dependencies
|
|
20
20
|
- **Framework Agnostic** - Express, NestJS, Fastify, native HTTP
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
- **TypeScript First** - Full type safety and IntelliSense support
|
|
23
23
|
- **Flexible Keys** - Rate limit by IP, user ID, API key, or custom logic
|
|
24
24
|
- **Tiered Limits** - Different limits for free/pro/enterprise users
|
|
25
|
+
- **Auto-Ban** - Automatically ban repeat offenders after threshold violations
|
|
26
|
+
- **Shared Limits** - Group rate limits via groupId for teams/tenants
|
|
25
27
|
- **Standard Headers** - RFC-compliant RateLimit-* and X-RateLimit-* headers
|
|
26
28
|
|
|
27
29
|
## Performance
|
|
@@ -32,17 +34,17 @@ hitlimit is designed for speed. Here's how it performs:
|
|
|
32
34
|
|
|
33
35
|
| Store | Operations/sec | Avg Latency | Use Case |
|
|
34
36
|
|-------|----------------|-------------|----------|
|
|
35
|
-
| **Memory** | 2,
|
|
36
|
-
| **SQLite** |
|
|
37
|
+
| **Memory** | 2,450,000+ | 0.41μs | Single instance, no persistence (multi-IP scenarios) |
|
|
38
|
+
| **SQLite** | 390,000+ | 2.56μs | Single instance, persistence needed (multi-IP scenarios) |
|
|
37
39
|
| **Redis** | 6,500+ | 153μs | Multi-instance, distributed |
|
|
38
40
|
|
|
39
41
|
### vs Competitors
|
|
40
42
|
|
|
41
43
|
| Library | Memory 10K IPs (ops/s) | Bundle Size |
|
|
42
44
|
|---------|------------------------|-------------|
|
|
43
|
-
| **hitlimit** | **2,
|
|
44
|
-
| rate-limiter-flexible | 1,
|
|
45
|
-
| express-rate-limit | 1,
|
|
45
|
+
| **hitlimit** | **2,450,000** | **~7KB** |
|
|
46
|
+
| rate-limiter-flexible | 1,840,000 | ~155KB |
|
|
47
|
+
| express-rate-limit | 1,210,000 | ~66KB |
|
|
46
48
|
|
|
47
49
|
> **Note:** Benchmark results vary by hardware and environment. Run your own benchmarks to see results on your specific setup.
|
|
48
50
|
|
|
@@ -223,6 +225,37 @@ hitlimit({
|
|
|
223
225
|
})
|
|
224
226
|
```
|
|
225
227
|
|
|
228
|
+
### Auto-Ban Repeat Offenders
|
|
229
|
+
|
|
230
|
+
Automatically ban IPs that violate rate limits repeatedly.
|
|
231
|
+
|
|
232
|
+
```javascript
|
|
233
|
+
hitlimit({
|
|
234
|
+
limit: 100,
|
|
235
|
+
window: '1m',
|
|
236
|
+
ban: {
|
|
237
|
+
threshold: 5, // Ban after 5 violations
|
|
238
|
+
duration: '15m' // Ban for 15 minutes
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
When a client exceeds the rate limit 5 times, they'll be banned for 15 minutes. During the ban, all requests return 429 immediately.
|
|
244
|
+
|
|
245
|
+
### Shared Rate Limits (Group)
|
|
246
|
+
|
|
247
|
+
Share rate limits across multiple clients using a group identifier.
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
hitlimit({
|
|
251
|
+
limit: 10000,
|
|
252
|
+
window: '1h',
|
|
253
|
+
group: (req) => req.user.teamId // Share limit across team
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
All requests with the same team ID share the same rate limit counter. Perfect for team-based SaaS quotas.
|
|
258
|
+
|
|
226
259
|
### Skip Certain Requests
|
|
227
260
|
|
|
228
261
|
Whitelist health checks, internal routes, or admin users.
|
|
@@ -237,6 +270,39 @@ hitlimit({
|
|
|
237
270
|
})
|
|
238
271
|
```
|
|
239
272
|
|
|
273
|
+
### Auto-Ban Repeat Offenders
|
|
274
|
+
|
|
275
|
+
Automatically ban clients that repeatedly exceed rate limits.
|
|
276
|
+
|
|
277
|
+
```javascript
|
|
278
|
+
hitlimit({
|
|
279
|
+
limit: 10,
|
|
280
|
+
window: '1m',
|
|
281
|
+
ban: {
|
|
282
|
+
threshold: 5, // Ban after 5 violations
|
|
283
|
+
duration: '1h' // Ban lasts 1 hour
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Banned clients receive `X-RateLimit-Ban: true` header and `banned: true` in the response body.
|
|
289
|
+
|
|
290
|
+
### Grouped / Shared Limits
|
|
291
|
+
|
|
292
|
+
Rate limit by organization, API key, or any shared identifier.
|
|
293
|
+
|
|
294
|
+
```javascript
|
|
295
|
+
// Per-API-key rate limiting
|
|
296
|
+
hitlimit({
|
|
297
|
+
limit: 1000,
|
|
298
|
+
window: '1h',
|
|
299
|
+
group: (req) => req.headers['x-api-key'] || 'anonymous'
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
// Static group prefix
|
|
303
|
+
hitlimit({ group: 'api', limit: 100, window: '1m' })
|
|
304
|
+
```
|
|
305
|
+
|
|
240
306
|
## Configuration Options
|
|
241
307
|
|
|
242
308
|
```javascript
|
|
@@ -276,7 +342,16 @@ hitlimit({
|
|
|
276
342
|
skip: (req) => req.path === '/health',
|
|
277
343
|
|
|
278
344
|
// Error handling
|
|
279
|
-
onStoreError: (error, req) => 'allow' // or 'deny'
|
|
345
|
+
onStoreError: (error, req) => 'allow', // or 'deny'
|
|
346
|
+
|
|
347
|
+
// Ban repeat offenders
|
|
348
|
+
ban: {
|
|
349
|
+
threshold: 5, // violations before ban
|
|
350
|
+
duration: '1h' // ban duration
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
// Group/shared limits
|
|
354
|
+
group: (req) => req.headers['x-api-key'] || 'default'
|
|
280
355
|
})
|
|
281
356
|
```
|
|
282
357
|
|
|
@@ -404,6 +479,18 @@ import { hitlimit } from '@joint-ops/hitlimit'
|
|
|
404
479
|
app.use(hitlimit({ limit: 100, window: '1m' }))
|
|
405
480
|
```
|
|
406
481
|
|
|
482
|
+
### From @fastify/rate-limit
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
// Before (@fastify/rate-limit)
|
|
486
|
+
import rateLimit from '@fastify/rate-limit'
|
|
487
|
+
await app.register(rateLimit, { max: 100, timeWindow: '1 minute' })
|
|
488
|
+
|
|
489
|
+
// After (hitlimit) - tiered limits, SQLite, multi-framework
|
|
490
|
+
import { hitlimit } from '@joint-ops/hitlimit/fastify'
|
|
491
|
+
await app.register(hitlimit, { limit: 100, window: '1m' })
|
|
492
|
+
```
|
|
493
|
+
|
|
407
494
|
### From @nestjs/throttler
|
|
408
495
|
|
|
409
496
|
```typescript
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,YAAY,EACZ,cAAc,EACf,MAAM,2BAA2B,CAAA;AAGlC,wBAAgB,aAAa,CAAC,QAAQ,EACpC,OAAO,EAAE,eAAe,CAAC,QAAQ,CAAC,EAClC,YAAY,EAAE,aAAa,EAC3B,UAAU,EAAE,YAAY,CAAC,QAAQ,CAAC,GACjC,cAAc,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,YAAY,EACZ,cAAc,EACf,MAAM,2BAA2B,CAAA;AAGlC,wBAAgB,aAAa,CAAC,QAAQ,EACpC,OAAO,EAAE,eAAe,CAAC,QAAQ,CAAC,EAClC,YAAY,EAAE,aAAa,EAC3B,UAAU,EAAE,YAAY,CAAC,QAAQ,CAAC,GACjC,cAAc,CAAC,QAAQ,CAAC,CAqB1B"}
|
package/dist/core/config.js
CHANGED
|
@@ -14,7 +14,11 @@ export function resolveConfig(options, defaultStore, defaultKey) {
|
|
|
14
14
|
},
|
|
15
15
|
store: options.store ?? defaultStore,
|
|
16
16
|
onStoreError: options.onStoreError ?? (() => 'allow'),
|
|
17
|
-
skip: options.skip
|
|
17
|
+
skip: options.skip,
|
|
18
|
+
ban: options.ban
|
|
19
|
+
? { threshold: options.ban.threshold, durationMs: parseWindow(options.ban.duration) }
|
|
20
|
+
: null,
|
|
21
|
+
group: options.group ?? null
|
|
18
22
|
};
|
|
19
23
|
}
|
|
20
24
|
//# sourceMappingURL=config.js.map
|
package/dist/core/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAExC,MAAM,UAAU,aAAa,CAC3B,OAAkC,EAClC,YAA2B,EAC3B,UAAkC;IAElC,OAAO;QACL,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG;QAC3B,QAAQ,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC;QAC7C,GAAG,EAAE,OAAO,CAAC,GAAG,IAAI,UAAU;QAC9B,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,kCAAkC,EAAE;QAC7F,OAAO,EAAE;YACP,QAAQ,EAAE,OAAO,CAAC,OAAO,EAAE,QAAQ,IAAI,IAAI;YAC3C,MAAM,EAAE,OAAO,CAAC,OAAO,EAAE,MAAM,IAAI,IAAI;YACvC,UAAU,EAAE,OAAO,CAAC,OAAO,EAAE,UAAU,IAAI,IAAI;SAChD;QACD,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,YAAY;QACpC,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC;QACrD,IAAI,EAAE,OAAO,CAAC,IAAI;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAExC,MAAM,UAAU,aAAa,CAC3B,OAAkC,EAClC,YAA2B,EAC3B,UAAkC;IAElC,OAAO;QACL,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG;QAC3B,QAAQ,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC;QAC7C,GAAG,EAAE,OAAO,CAAC,GAAG,IAAI,UAAU;QAC9B,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,kCAAkC,EAAE;QAC7F,OAAO,EAAE;YACP,QAAQ,EAAE,OAAO,CAAC,OAAO,EAAE,QAAQ,IAAI,IAAI;YAC3C,MAAM,EAAE,OAAO,CAAC,OAAO,EAAE,MAAM,IAAI,IAAI;YACvC,UAAU,EAAE,OAAO,CAAC,OAAO,EAAE,UAAU,IAAI,IAAI;SAChD;QACD,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,YAAY;QACpC,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC;QACrD,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,GAAG,EAAE,OAAO,CAAC,GAAG;YACd,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE;YACrF,CAAC,CAAC,IAAI;QACR,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,IAAI;KAC7B,CAAA;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/core/headers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAE5E,wBAAgB,YAAY,CAC1B,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,QAAQ,CAAC,aAAa,CAAC,EAC/B,OAAO,EAAE,OAAO,GACf,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/core/headers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAE5E,wBAAgB,YAAY,CAC1B,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,QAAQ,CAAC,aAAa,CAAC,EAC/B,OAAO,EAAE,OAAO,GACf,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA4BxB"}
|
package/dist/core/headers.js
CHANGED
|
@@ -11,8 +11,15 @@ export function buildHeaders(info, config, allowed) {
|
|
|
11
11
|
headers['X-RateLimit-Reset'] = String(Math.ceil(info.resetAt / 1000));
|
|
12
12
|
}
|
|
13
13
|
if (!allowed && config.retryAfter) {
|
|
14
|
+
// When banned, Retry-After reflects ban duration, not window reset
|
|
14
15
|
headers['Retry-After'] = String(info.resetIn);
|
|
15
16
|
}
|
|
17
|
+
if (info.banned) {
|
|
18
|
+
headers['X-RateLimit-Ban'] = 'true';
|
|
19
|
+
if (info.banExpiresAt) {
|
|
20
|
+
headers['X-RateLimit-Ban-Expires'] = String(Math.ceil(info.banExpiresAt / 1000));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
16
23
|
return headers;
|
|
17
24
|
}
|
|
18
25
|
//# sourceMappingURL=headers.js.map
|
package/dist/core/headers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../../src/core/headers.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,YAAY,CAC1B,IAAkB,EAClB,MAA+B,EAC/B,OAAgB;IAEhB,MAAM,OAAO,GAA2B,EAAE,CAAA;IAE1C,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,OAAO,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC/C,OAAO,CAAC,qBAAqB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACvD,OAAO,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAA;IACrE,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,OAAO,CAAC,mBAAmB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACjD,OAAO,CAAC,uBAAuB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACzD,OAAO,CAAC,mBAAmB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAA;IACvE,CAAC;IAED,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QAClC,OAAO,CAAC,aAAa,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC/C,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC"}
|
|
1
|
+
{"version":3,"file":"headers.js","sourceRoot":"","sources":["../../src/core/headers.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,YAAY,CAC1B,IAAkB,EAClB,MAA+B,EAC/B,OAAgB;IAEhB,MAAM,OAAO,GAA2B,EAAE,CAAA;IAE1C,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,OAAO,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC/C,OAAO,CAAC,qBAAqB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACvD,OAAO,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAA;IACrE,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,OAAO,CAAC,mBAAmB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACjD,OAAO,CAAC,uBAAuB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACzD,OAAO,CAAC,mBAAmB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAA;IACvE,CAAC;IAED,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QAClC,mEAAmE;QACnE,OAAO,CAAC,aAAa,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC/C,CAAC;IAED,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,OAAO,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAA;QACnC,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,OAAO,CAAC,yBAAyB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,CAAA;QAClF,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"limiter.d.ts","sourceRoot":"","sources":["../../src/core/limiter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,cAAc,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAM7F,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB;
|
|
1
|
+
{"version":3,"file":"limiter.d.ts","sourceRoot":"","sources":["../../src/core/limiter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,cAAc,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAM7F,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB;AAsCD,wBAAsB,cAAc,CAAC,QAAQ,EAC3C,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,EAChC,GAAG,EAAE,QAAQ,GACZ,OAAO,CAAC,UAAU,CAAC,CA8CrB;AAGD,wBAAsB,UAAU,CAAC,QAAQ,EACvC,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,EAChC,GAAG,EAAE,QAAQ,GACZ,OAAO,CAAC,cAAc,CAAC,CA6EzB"}
|
package/dist/core/limiter.js
CHANGED
|
@@ -1,19 +1,50 @@
|
|
|
1
1
|
import { parseWindow } from './utils.js';
|
|
2
2
|
import { buildHeaders } from './headers.js';
|
|
3
3
|
import { buildBody } from './response.js';
|
|
4
|
+
// Resolve group-prefixed key
|
|
5
|
+
async function resolveKey(config, req) {
|
|
6
|
+
let key = await config.key(req);
|
|
7
|
+
let groupId;
|
|
8
|
+
if (config.group) {
|
|
9
|
+
groupId = typeof config.group === 'function'
|
|
10
|
+
? await config.group(req)
|
|
11
|
+
: config.group;
|
|
12
|
+
key = `group:${groupId}:${key}`;
|
|
13
|
+
}
|
|
14
|
+
return { key, groupId };
|
|
15
|
+
}
|
|
16
|
+
// Resolve tier-specific limit and window
|
|
17
|
+
function resolveTier(config, tierName) {
|
|
18
|
+
if (tierName && config.tiers) {
|
|
19
|
+
const tierConfig = config.tiers[tierName];
|
|
20
|
+
if (tierConfig) {
|
|
21
|
+
return {
|
|
22
|
+
limit: tierConfig.limit,
|
|
23
|
+
windowMs: tierConfig.window ? parseWindow(tierConfig.window) : config.windowMs
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { limit: config.limit, windowMs: config.windowMs };
|
|
28
|
+
}
|
|
4
29
|
// Optimized check for tiered limits - returns minimal object
|
|
5
30
|
export async function checkLimitFast(config, req) {
|
|
6
|
-
const key = await config
|
|
7
|
-
let
|
|
8
|
-
let windowMs = config.windowMs;
|
|
31
|
+
const { key } = await resolveKey(config, req);
|
|
32
|
+
let tierName;
|
|
9
33
|
if (config.tier && config.tiers) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
34
|
+
tierName = await config.tier(req);
|
|
35
|
+
}
|
|
36
|
+
const { limit, windowMs } = resolveTier(config, tierName);
|
|
37
|
+
// Check ban status
|
|
38
|
+
if (config.ban && config.store.isBanned) {
|
|
39
|
+
const banned = await config.store.isBanned(key);
|
|
40
|
+
if (banned) {
|
|
41
|
+
return {
|
|
42
|
+
allowed: false,
|
|
43
|
+
limit,
|
|
44
|
+
remaining: 0,
|
|
45
|
+
resetIn: Math.ceil(config.ban.durationMs / 1000),
|
|
46
|
+
resetAt: Date.now() + config.ban.durationMs
|
|
47
|
+
};
|
|
17
48
|
}
|
|
18
49
|
}
|
|
19
50
|
if (limit === Infinity) {
|
|
@@ -21,35 +52,59 @@ export async function checkLimitFast(config, req) {
|
|
|
21
52
|
}
|
|
22
53
|
const result = await config.store.hit(key, windowMs, limit);
|
|
23
54
|
const now = Date.now();
|
|
55
|
+
const allowed = result.count <= limit;
|
|
56
|
+
// Track violations for ban
|
|
57
|
+
if (!allowed && config.ban && config.store.recordViolation) {
|
|
58
|
+
const violations = await config.store.recordViolation(key, config.ban.durationMs);
|
|
59
|
+
if (violations >= config.ban.threshold && config.store.ban) {
|
|
60
|
+
await config.store.ban(key, config.ban.durationMs);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
24
63
|
return {
|
|
25
|
-
allowed
|
|
64
|
+
allowed,
|
|
26
65
|
limit,
|
|
27
66
|
remaining: Math.max(0, limit - result.count),
|
|
28
67
|
resetIn: Math.max(0, Math.ceil((result.resetAt - now) / 1000)),
|
|
29
68
|
resetAt: result.resetAt
|
|
30
69
|
};
|
|
31
70
|
}
|
|
32
|
-
//
|
|
71
|
+
// Full check with complete info object
|
|
33
72
|
export async function checkLimit(config, req) {
|
|
34
|
-
|
|
35
|
-
const key = await config.key(req);
|
|
36
|
-
let limit = config.limit;
|
|
37
|
-
let windowMs = config.windowMs;
|
|
73
|
+
const { key, groupId } = await resolveKey(config, req);
|
|
38
74
|
let tierName;
|
|
39
75
|
if (config.tier && config.tiers) {
|
|
40
76
|
tierName = await config.tier(req);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
}
|
|
78
|
+
const { limit, windowMs } = resolveTier(config, tierName);
|
|
79
|
+
// Check ban status BEFORE hitting store
|
|
80
|
+
if (config.ban && config.store.isBanned) {
|
|
81
|
+
const banned = await config.store.isBanned(key);
|
|
82
|
+
if (banned) {
|
|
83
|
+
const banResetIn = Math.ceil(config.ban.durationMs / 1000);
|
|
84
|
+
const info = {
|
|
85
|
+
limit,
|
|
86
|
+
remaining: 0,
|
|
87
|
+
resetIn: banResetIn,
|
|
88
|
+
resetAt: Date.now() + config.ban.durationMs,
|
|
89
|
+
key,
|
|
90
|
+
tier: tierName,
|
|
91
|
+
banned: true,
|
|
92
|
+
banExpiresAt: Date.now() + config.ban.durationMs,
|
|
93
|
+
group: groupId
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
allowed: false,
|
|
97
|
+
info,
|
|
98
|
+
headers: buildHeaders(info, config.headers, false),
|
|
99
|
+
body: buildBody(config.response, info)
|
|
100
|
+
};
|
|
47
101
|
}
|
|
48
102
|
}
|
|
103
|
+
// Infinity limit skips store entirely (no violations possible)
|
|
49
104
|
if (limit === Infinity) {
|
|
50
105
|
return {
|
|
51
106
|
allowed: true,
|
|
52
|
-
info: { limit, remaining: Infinity, resetIn: 0, resetAt: 0, key, tier: tierName },
|
|
107
|
+
info: { limit, remaining: Infinity, resetIn: 0, resetAt: 0, key, tier: tierName, group: groupId },
|
|
53
108
|
headers: {},
|
|
54
109
|
body: {}
|
|
55
110
|
};
|
|
@@ -65,8 +120,19 @@ export async function checkLimit(config, req) {
|
|
|
65
120
|
resetIn,
|
|
66
121
|
resetAt: result.resetAt,
|
|
67
122
|
key,
|
|
68
|
-
tier: tierName
|
|
123
|
+
tier: tierName,
|
|
124
|
+
group: groupId
|
|
69
125
|
};
|
|
126
|
+
// Track violations and check ban threshold
|
|
127
|
+
if (!allowed && config.ban && config.store.recordViolation) {
|
|
128
|
+
const violations = await config.store.recordViolation(key, config.ban.durationMs);
|
|
129
|
+
info.violations = violations;
|
|
130
|
+
if (violations >= config.ban.threshold && config.store.ban) {
|
|
131
|
+
await config.store.ban(key, config.ban.durationMs);
|
|
132
|
+
info.banned = true;
|
|
133
|
+
info.banExpiresAt = now + config.ban.durationMs;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
70
136
|
return {
|
|
71
137
|
allowed,
|
|
72
138
|
info,
|
package/dist/core/limiter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"limiter.js","sourceRoot":"","sources":["../../src/core/limiter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAWzC,
|
|
1
|
+
{"version":3,"file":"limiter.js","sourceRoot":"","sources":["../../src/core/limiter.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../src/core/response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAEhG,wBAAgB,SAAS,CACvB,QAAQ,EAAE,cAAc,GAAG,iBAAiB,EAC5C,IAAI,EAAE,YAAY,GACjB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../src/core/response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAEhG,wBAAgB,SAAS,CACvB,QAAQ,EAAE,cAAc,GAAG,iBAAiB,EAC5C,IAAI,EAAE,YAAY,GACjB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAmBrB"}
|
package/dist/core/response.js
CHANGED
|
@@ -2,11 +2,17 @@ export function buildBody(response, info) {
|
|
|
2
2
|
if (typeof response === 'function') {
|
|
3
3
|
return response(info);
|
|
4
4
|
}
|
|
5
|
-
|
|
5
|
+
const body = {
|
|
6
6
|
...response,
|
|
7
7
|
limit: info.limit,
|
|
8
8
|
remaining: info.remaining,
|
|
9
9
|
resetIn: info.resetIn
|
|
10
10
|
};
|
|
11
|
+
if (info.banned) {
|
|
12
|
+
body.banned = true;
|
|
13
|
+
body.banExpiresAt = info.banExpiresAt;
|
|
14
|
+
body.message = 'You have been temporarily banned due to repeated rate limit violations';
|
|
15
|
+
}
|
|
16
|
+
return body;
|
|
11
17
|
}
|
|
12
18
|
//# sourceMappingURL=response.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"response.js","sourceRoot":"","sources":["../../src/core/response.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,SAAS,CACvB,QAA4C,EAC5C,IAAkB;IAElB,IAAI,OAAO,QAAQ,KAAK,UAAU,EAAE,CAAC;QACnC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAA;IACvB,CAAC;IAED,
|
|
1
|
+
{"version":3,"file":"response.js","sourceRoot":"","sources":["../../src/core/response.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,SAAS,CACvB,QAA4C,EAC5C,IAAkB;IAElB,IAAI,OAAO,QAAQ,KAAK,UAAU,EAAE,CAAC;QACnC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAA;IACvB,CAAC;IAED,MAAM,IAAI,GAAwB;QAChC,GAAG,QAAQ;QACX,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAA;IAED,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QAClB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAA;QACrC,IAAI,CAAC,OAAO,GAAG,wEAAwE,CAAA;IACzF,CAAC;IAED,OAAO,IAAI,CAAA;AACb,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Request, Response, NextFunction } from 'express';
|
|
2
2
|
import type { HitLimitOptions } from '@joint-ops/hitlimit-types';
|
|
3
|
-
|
|
3
|
+
import { checkLimit } from './core/limiter.js';
|
|
4
|
+
export type { HitLimitOptions, HitLimitInfo, HitLimitResult, HitLimitStore, StoreResult, TierConfig, HeadersConfig, ResolvedConfig, KeyGenerator, TierResolver, SkipFunction, StoreErrorHandler, ResponseFormatter, ResponseConfig, BanConfig, GroupIdResolver } from '@joint-ops/hitlimit-types';
|
|
4
5
|
export { DEFAULT_LIMIT, DEFAULT_WINDOW, DEFAULT_WINDOW_MS, DEFAULT_MESSAGE } from '@joint-ops/hitlimit-types';
|
|
5
6
|
export { memoryStore } from './stores/memory.js';
|
|
6
|
-
export { checkLimit }
|
|
7
|
+
export { checkLimit };
|
|
7
8
|
export declare function hitlimit(options?: HitLimitOptions<Request>): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
8
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAmD,MAAM,2BAA2B,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAmD,MAAM,2BAA2B,CAAA;AAEjH,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAG9C,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,cAAc,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AACjS,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC7G,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,UAAU,EAAE,CAAA;AAiBrB,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAe,CAAC,OAAO,CAAM,SAkBxC,OAAO,OAAO,QAAQ,QAAQ,YAAY,mBA0EhE"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { resolveConfig } from './core/config.js';
|
|
2
|
+
import { checkLimit } from './core/limiter.js';
|
|
2
3
|
import { memoryStore } from './stores/memory.js';
|
|
3
4
|
export { DEFAULT_LIMIT, DEFAULT_WINDOW, DEFAULT_WINDOW_MS, DEFAULT_MESSAGE } from '@joint-ops/hitlimit-types';
|
|
4
5
|
export { memoryStore } from './stores/memory.js';
|
|
5
|
-
export { checkLimit }
|
|
6
|
+
export { checkLimit };
|
|
6
7
|
function getDefaultKey(req) {
|
|
7
8
|
return req.ip || req.socket?.remoteAddress || 'unknown';
|
|
8
9
|
}
|
|
@@ -19,14 +20,16 @@ export function hitlimit(options = {}) {
|
|
|
19
20
|
// Pre-compute flags
|
|
20
21
|
const hasSkip = !!config.skip;
|
|
21
22
|
const hasTiers = !!(config.tier && config.tiers);
|
|
23
|
+
const hasBan = !!config.ban;
|
|
24
|
+
const hasGroup = !!config.group;
|
|
22
25
|
const standardHeaders = config.headers.standard;
|
|
23
26
|
const legacyHeaders = config.headers.legacy;
|
|
24
27
|
const retryAfterHeader = config.headers.retryAfter;
|
|
25
28
|
const limit = config.limit;
|
|
26
29
|
const windowMs = config.windowMs;
|
|
27
30
|
const responseConfig = config.response;
|
|
28
|
-
// Fast path: no skip, no tiers (most common case
|
|
29
|
-
if (!hasSkip && !hasTiers) {
|
|
31
|
+
// Fast path: no skip, no tiers, no ban, no group (most common case)
|
|
32
|
+
if (!hasSkip && !hasTiers && !hasBan && !hasGroup) {
|
|
30
33
|
return async (req, res, next) => {
|
|
31
34
|
try {
|
|
32
35
|
const key = await config.key(req);
|
|
@@ -67,7 +70,7 @@ export function hitlimit(options = {}) {
|
|
|
67
70
|
}
|
|
68
71
|
};
|
|
69
72
|
}
|
|
70
|
-
// Full path: with skip
|
|
73
|
+
// Full path: with skip, tiers, ban, or group — delegates to core checkLimit
|
|
71
74
|
return async (req, res, next) => {
|
|
72
75
|
if (hasSkip) {
|
|
73
76
|
const shouldSkip = await config.skip(req);
|
|
@@ -76,45 +79,12 @@ export function hitlimit(options = {}) {
|
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
try {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let tierName;
|
|
83
|
-
if (hasTiers) {
|
|
84
|
-
tierName = await config.tier(req);
|
|
85
|
-
const tierConfig = config.tiers[tierName];
|
|
86
|
-
if (tierConfig) {
|
|
87
|
-
effectiveLimit = tierConfig.limit;
|
|
88
|
-
if (tierConfig.window) {
|
|
89
|
-
effectiveWindowMs = parseWindow(tierConfig.window);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (effectiveLimit === Infinity) {
|
|
94
|
-
return next();
|
|
95
|
-
}
|
|
96
|
-
const result = await config.store.hit(key, effectiveWindowMs, effectiveLimit);
|
|
97
|
-
const allowed = result.count <= effectiveLimit;
|
|
98
|
-
const remaining = Math.max(0, effectiveLimit - result.count);
|
|
99
|
-
const resetIn = Math.ceil((result.resetAt - Date.now()) / 1000);
|
|
100
|
-
if (standardHeaders) {
|
|
101
|
-
res.setHeader('RateLimit-Limit', effectiveLimit);
|
|
102
|
-
res.setHeader('RateLimit-Remaining', remaining);
|
|
103
|
-
res.setHeader('RateLimit-Reset', resetIn);
|
|
82
|
+
const result = await checkLimit(config, req);
|
|
83
|
+
for (const [key, value] of Object.entries(result.headers)) {
|
|
84
|
+
res.setHeader(key, value);
|
|
104
85
|
}
|
|
105
|
-
if (
|
|
106
|
-
res.
|
|
107
|
-
res.setHeader('X-RateLimit-Remaining', remaining);
|
|
108
|
-
res.setHeader('X-RateLimit-Reset', Math.ceil(result.resetAt / 1000));
|
|
109
|
-
}
|
|
110
|
-
if (!allowed) {
|
|
111
|
-
if (retryAfterHeader) {
|
|
112
|
-
res.setHeader('Retry-After', resetIn);
|
|
113
|
-
}
|
|
114
|
-
const body = buildResponseBody(responseConfig, {
|
|
115
|
-
limit: effectiveLimit, remaining: 0, resetIn, resetAt: result.resetAt, key, tier: tierName
|
|
116
|
-
});
|
|
117
|
-
res.status(429).json(body);
|
|
86
|
+
if (!result.allowed) {
|
|
87
|
+
res.status(429).json(result.body);
|
|
118
88
|
return;
|
|
119
89
|
}
|
|
120
90
|
next();
|
|
@@ -129,21 +99,4 @@ export function hitlimit(options = {}) {
|
|
|
129
99
|
}
|
|
130
100
|
};
|
|
131
101
|
}
|
|
132
|
-
function parseWindow(window) {
|
|
133
|
-
if (typeof window === 'number')
|
|
134
|
-
return window;
|
|
135
|
-
const match = window.match(/^(\d+)(ms|s|m|h|d)$/);
|
|
136
|
-
if (!match)
|
|
137
|
-
return 60000;
|
|
138
|
-
const value = parseInt(match[1], 10);
|
|
139
|
-
const unit = match[2];
|
|
140
|
-
switch (unit) {
|
|
141
|
-
case 'ms': return value;
|
|
142
|
-
case 's': return value * 1000;
|
|
143
|
-
case 'm': return value * 60 * 1000;
|
|
144
|
-
case 'h': return value * 60 * 60 * 1000;
|
|
145
|
-
case 'd': return value * 24 * 60 * 60 * 1000;
|
|
146
|
-
default: return 60000;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
102
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAGhD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC7G,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/stores/memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAA;
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/stores/memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAA;AAgJ3E,wBAAgB,WAAW,IAAI,aAAa,CAE3C"}
|
package/dist/stores/memory.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
class MemoryStore {
|
|
2
2
|
hits = new Map();
|
|
3
|
+
bans = new Map();
|
|
4
|
+
violations = new Map();
|
|
3
5
|
hit(key, windowMs, _limit) {
|
|
4
6
|
const entry = this.hits.get(key);
|
|
5
7
|
if (entry !== undefined) {
|
|
@@ -21,18 +23,79 @@ class MemoryStore {
|
|
|
21
23
|
this.hits.set(key, { count: 1, resetAt, timeoutId });
|
|
22
24
|
return { count: 1, resetAt };
|
|
23
25
|
}
|
|
26
|
+
isBanned(key) {
|
|
27
|
+
const ban = this.bans.get(key);
|
|
28
|
+
if (!ban)
|
|
29
|
+
return false;
|
|
30
|
+
if (Date.now() >= ban.expiresAt) {
|
|
31
|
+
clearTimeout(ban.timeoutId);
|
|
32
|
+
this.bans.delete(key);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
ban(key, durationMs) {
|
|
38
|
+
const existing = this.bans.get(key);
|
|
39
|
+
if (existing)
|
|
40
|
+
clearTimeout(existing.timeoutId);
|
|
41
|
+
const expiresAt = Date.now() + durationMs;
|
|
42
|
+
const timeoutId = setTimeout(() => {
|
|
43
|
+
this.bans.delete(key);
|
|
44
|
+
}, durationMs);
|
|
45
|
+
if (typeof timeoutId.unref === 'function') {
|
|
46
|
+
timeoutId.unref();
|
|
47
|
+
}
|
|
48
|
+
this.bans.set(key, { expiresAt, timeoutId });
|
|
49
|
+
}
|
|
50
|
+
recordViolation(key, windowMs) {
|
|
51
|
+
const entry = this.violations.get(key);
|
|
52
|
+
if (entry && Date.now() < entry.resetAt) {
|
|
53
|
+
entry.count++;
|
|
54
|
+
return entry.count;
|
|
55
|
+
}
|
|
56
|
+
// New or expired violation window
|
|
57
|
+
if (entry)
|
|
58
|
+
clearTimeout(entry.timeoutId);
|
|
59
|
+
const resetAt = Date.now() + windowMs;
|
|
60
|
+
const timeoutId = setTimeout(() => {
|
|
61
|
+
this.violations.delete(key);
|
|
62
|
+
}, windowMs);
|
|
63
|
+
if (typeof timeoutId.unref === 'function') {
|
|
64
|
+
timeoutId.unref();
|
|
65
|
+
}
|
|
66
|
+
this.violations.set(key, { count: 1, resetAt, timeoutId });
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
24
69
|
reset(key) {
|
|
25
70
|
const entry = this.hits.get(key);
|
|
26
71
|
if (entry) {
|
|
27
72
|
clearTimeout(entry.timeoutId);
|
|
28
73
|
this.hits.delete(key);
|
|
29
74
|
}
|
|
75
|
+
const ban = this.bans.get(key);
|
|
76
|
+
if (ban) {
|
|
77
|
+
clearTimeout(ban.timeoutId);
|
|
78
|
+
this.bans.delete(key);
|
|
79
|
+
}
|
|
80
|
+
const violation = this.violations.get(key);
|
|
81
|
+
if (violation) {
|
|
82
|
+
clearTimeout(violation.timeoutId);
|
|
83
|
+
this.violations.delete(key);
|
|
84
|
+
}
|
|
30
85
|
}
|
|
31
86
|
shutdown() {
|
|
32
87
|
for (const [, entry] of this.hits) {
|
|
33
88
|
clearTimeout(entry.timeoutId);
|
|
34
89
|
}
|
|
35
90
|
this.hits.clear();
|
|
91
|
+
for (const [, entry] of this.bans) {
|
|
92
|
+
clearTimeout(entry.timeoutId);
|
|
93
|
+
}
|
|
94
|
+
this.bans.clear();
|
|
95
|
+
for (const [, entry] of this.violations) {
|
|
96
|
+
clearTimeout(entry.timeoutId);
|
|
97
|
+
}
|
|
98
|
+
this.violations.clear();
|
|
36
99
|
}
|
|
37
100
|
}
|
|
38
101
|
export function memoryStore() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/stores/memory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/stores/memory.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../src/stores/redis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAA;AAG3E,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;
|
|
1
|
+
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../../src/stores/redis.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAA;AAG3E,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAqED,wBAAgB,UAAU,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,aAAa,CAErE"}
|
package/dist/stores/redis.js
CHANGED
|
@@ -2,9 +2,13 @@ import Redis from 'ioredis';
|
|
|
2
2
|
class RedisStore {
|
|
3
3
|
redis;
|
|
4
4
|
prefix;
|
|
5
|
+
banPrefix;
|
|
6
|
+
violationPrefix;
|
|
5
7
|
constructor(options = {}) {
|
|
6
8
|
this.redis = new Redis(options.url ?? 'redis://localhost:6379');
|
|
7
9
|
this.prefix = options.keyPrefix ?? 'hitlimit:';
|
|
10
|
+
this.banPrefix = (options.keyPrefix ?? 'hitlimit:') + 'ban:';
|
|
11
|
+
this.violationPrefix = (options.keyPrefix ?? 'hitlimit:') + 'violations:';
|
|
8
12
|
}
|
|
9
13
|
async hit(key, windowMs, _limit) {
|
|
10
14
|
const redisKey = this.prefix + key;
|
|
@@ -23,8 +27,23 @@ class RedisStore {
|
|
|
23
27
|
const resetAt = now + ttl;
|
|
24
28
|
return { count, resetAt };
|
|
25
29
|
}
|
|
30
|
+
async isBanned(key) {
|
|
31
|
+
const result = await this.redis.exists(this.banPrefix + key);
|
|
32
|
+
return result === 1;
|
|
33
|
+
}
|
|
34
|
+
async ban(key, durationMs) {
|
|
35
|
+
await this.redis.set(this.banPrefix + key, '1', 'PX', durationMs);
|
|
36
|
+
}
|
|
37
|
+
async recordViolation(key, windowMs) {
|
|
38
|
+
const redisKey = this.violationPrefix + key;
|
|
39
|
+
const count = await this.redis.incr(redisKey);
|
|
40
|
+
if (count === 1) {
|
|
41
|
+
await this.redis.pexpire(redisKey, windowMs);
|
|
42
|
+
}
|
|
43
|
+
return count;
|
|
44
|
+
}
|
|
26
45
|
async reset(key) {
|
|
27
|
-
await this.redis.del(this.prefix + key);
|
|
46
|
+
await this.redis.del(this.prefix + key, this.banPrefix + key, this.violationPrefix + key);
|
|
28
47
|
}
|
|
29
48
|
async shutdown() {
|
|
30
49
|
await this.redis.quit();
|
package/dist/stores/redis.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../../src/stores/redis.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,SAAS,CAAA;AAO3B,MAAM,UAAU;IACN,KAAK,CAAO;IACZ,MAAM,CAAQ;
|
|
1
|
+
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../../src/stores/redis.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/stores/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAA;AAG3E,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;
|
|
1
|
+
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/stores/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAA;AAG3E,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAgHD,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,aAAa,CAEvE"}
|
package/dist/stores/sqlite.js
CHANGED
|
@@ -4,6 +4,12 @@ class SqliteStore {
|
|
|
4
4
|
hitStmt;
|
|
5
5
|
getStmt;
|
|
6
6
|
resetStmt;
|
|
7
|
+
isBannedStmt;
|
|
8
|
+
banStmt;
|
|
9
|
+
recordViolationStmt;
|
|
10
|
+
getViolationStmt;
|
|
11
|
+
resetBanStmt;
|
|
12
|
+
resetViolationStmt;
|
|
7
13
|
cleanupTimer;
|
|
8
14
|
constructor(options = {}) {
|
|
9
15
|
this.db = new Database(options.path ?? ':memory:');
|
|
@@ -14,6 +20,19 @@ class SqliteStore {
|
|
|
14
20
|
count INTEGER NOT NULL,
|
|
15
21
|
reset_at INTEGER NOT NULL
|
|
16
22
|
)
|
|
23
|
+
`);
|
|
24
|
+
this.db.exec(`
|
|
25
|
+
CREATE TABLE IF NOT EXISTS hitlimit_bans (
|
|
26
|
+
key TEXT PRIMARY KEY,
|
|
27
|
+
expires_at INTEGER NOT NULL
|
|
28
|
+
)
|
|
29
|
+
`);
|
|
30
|
+
this.db.exec(`
|
|
31
|
+
CREATE TABLE IF NOT EXISTS hitlimit_violations (
|
|
32
|
+
key TEXT PRIMARY KEY,
|
|
33
|
+
count INTEGER NOT NULL DEFAULT 1,
|
|
34
|
+
reset_at INTEGER NOT NULL
|
|
35
|
+
)
|
|
17
36
|
`);
|
|
18
37
|
this.hitStmt = this.db.prepare(`
|
|
19
38
|
INSERT INTO hitlimit (key, count, reset_at) VALUES (?, 1, ?)
|
|
@@ -23,8 +42,22 @@ class SqliteStore {
|
|
|
23
42
|
`);
|
|
24
43
|
this.getStmt = this.db.prepare('SELECT count, reset_at FROM hitlimit WHERE key = ?');
|
|
25
44
|
this.resetStmt = this.db.prepare('DELETE FROM hitlimit WHERE key = ?');
|
|
45
|
+
this.isBannedStmt = this.db.prepare('SELECT 1 FROM hitlimit_bans WHERE key = ? AND expires_at > ?');
|
|
46
|
+
this.banStmt = this.db.prepare('INSERT OR REPLACE INTO hitlimit_bans (key, expires_at) VALUES (?, ?)');
|
|
47
|
+
this.recordViolationStmt = this.db.prepare(`
|
|
48
|
+
INSERT INTO hitlimit_violations (key, count, reset_at) VALUES (?, 1, ?)
|
|
49
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
50
|
+
count = CASE WHEN reset_at <= ? THEN 1 ELSE count + 1 END,
|
|
51
|
+
reset_at = CASE WHEN reset_at <= ? THEN excluded.reset_at ELSE reset_at END
|
|
52
|
+
`);
|
|
53
|
+
this.getViolationStmt = this.db.prepare('SELECT count FROM hitlimit_violations WHERE key = ?');
|
|
54
|
+
this.resetBanStmt = this.db.prepare('DELETE FROM hitlimit_bans WHERE key = ?');
|
|
55
|
+
this.resetViolationStmt = this.db.prepare('DELETE FROM hitlimit_violations WHERE key = ?');
|
|
26
56
|
this.cleanupTimer = setInterval(() => {
|
|
27
|
-
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
this.db.prepare('DELETE FROM hitlimit WHERE reset_at <= ?').run(now);
|
|
59
|
+
this.db.prepare('DELETE FROM hitlimit_bans WHERE expires_at <= ?').run(now);
|
|
60
|
+
this.db.prepare('DELETE FROM hitlimit_violations WHERE reset_at <= ?').run(now);
|
|
28
61
|
}, 60000);
|
|
29
62
|
}
|
|
30
63
|
hit(key, windowMs, _limit) {
|
|
@@ -34,8 +67,23 @@ class SqliteStore {
|
|
|
34
67
|
const row = this.getStmt.get(key);
|
|
35
68
|
return { count: row.count, resetAt: row.reset_at };
|
|
36
69
|
}
|
|
70
|
+
isBanned(key) {
|
|
71
|
+
return this.isBannedStmt.get(key, Date.now()) !== undefined;
|
|
72
|
+
}
|
|
73
|
+
ban(key, durationMs) {
|
|
74
|
+
this.banStmt.run(key, Date.now() + durationMs);
|
|
75
|
+
}
|
|
76
|
+
recordViolation(key, windowMs) {
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
const resetAt = now + windowMs;
|
|
79
|
+
this.recordViolationStmt.run(key, resetAt, now, now);
|
|
80
|
+
const row = this.getViolationStmt.get(key);
|
|
81
|
+
return row?.count ?? 1;
|
|
82
|
+
}
|
|
37
83
|
reset(key) {
|
|
38
84
|
this.resetStmt.run(key);
|
|
85
|
+
this.resetBanStmt.run(key);
|
|
86
|
+
this.resetViolationStmt.run(key);
|
|
39
87
|
}
|
|
40
88
|
shutdown() {
|
|
41
89
|
clearInterval(this.cleanupTimer);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/stores/sqlite.ts"],"names":[],"mappings":"AACA,OAAO,QAAQ,MAAM,gBAAgB,CAAA;AAMrC,MAAM,WAAW;IACP,EAAE,CAAmB;IACrB,OAAO,CAAoB;IAC3B,OAAO,CAAoB;IAC3B,SAAS,CAAoB;IAC7B,YAAY,CAAgC;IAEpD,YAAY,UAA8B,EAAE;QAC1C,IAAI,CAAC,EAAE,GAAG,IAAI,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,UAAU,CAAC,CAAA;QAClD,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAA;QAEpC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;;;;;;KAMZ,CAAC,CAAA;QAEF,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC;;;;;KAK9B,CAAC,CAAA;QAEF,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,oDAAoD,CAAC,CAAA;QACpF,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAA;QAEtE,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC,GAAG,EAAE;YACnC,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,0CAA0C,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/stores/sqlite.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joint-ops/hitlimit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Fast rate limiting middleware for Express, Fastify, NestJS & Node.js - API throttling, brute force protection, request limiting",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Shayan M Hussain",
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
"test:watch": "vitest"
|
|
130
130
|
},
|
|
131
131
|
"dependencies": {
|
|
132
|
-
"@joint-ops/hitlimit-types": "1.0.
|
|
132
|
+
"@joint-ops/hitlimit-types": "1.0.6"
|
|
133
133
|
},
|
|
134
134
|
"peerDependencies": {
|
|
135
135
|
"@nestjs/common": ">=8.0.0",
|