@joint-ops/hitlimit 1.0.5 → 1.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 +113 -8
- 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/hono.d.ts +6 -0
- package/dist/hono.d.ts.map +1 -0
- package/dist/hono.js +44 -0
- package/dist/hono.js.map +1 -0
- 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 +13 -3
package/README.md
CHANGED
|
@@ -14,14 +14,16 @@
|
|
|
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
|
-
- **Framework Agnostic** - Express,
|
|
20
|
+
- **Framework Agnostic** - Express, Fastify, Hono, NestJS, native HTTP
|
|
21
21
|
- **Multiple Stores** - Memory, Redis, SQLite for distributed systems
|
|
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
|
|
|
@@ -122,6 +124,24 @@ app.get('/api', () => ({ status: 'ok' }))
|
|
|
122
124
|
await app.listen({ port: 3000 })
|
|
123
125
|
```
|
|
124
126
|
|
|
127
|
+
### Hono Rate Limiting
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { Hono } from 'hono'
|
|
131
|
+
import { serve } from '@hono/node-server'
|
|
132
|
+
import { hitlimit } from '@joint-ops/hitlimit/hono'
|
|
133
|
+
|
|
134
|
+
const app = new Hono()
|
|
135
|
+
|
|
136
|
+
app.use(hitlimit({
|
|
137
|
+
limit: 100,
|
|
138
|
+
window: '1m'
|
|
139
|
+
}))
|
|
140
|
+
|
|
141
|
+
app.get('/api', (c) => c.json({ status: 'ok' }))
|
|
142
|
+
serve({ fetch: app.fetch, port: 3000 })
|
|
143
|
+
```
|
|
144
|
+
|
|
125
145
|
### NestJS Rate Limiting
|
|
126
146
|
|
|
127
147
|
```typescript
|
|
@@ -223,6 +243,37 @@ hitlimit({
|
|
|
223
243
|
})
|
|
224
244
|
```
|
|
225
245
|
|
|
246
|
+
### Auto-Ban Repeat Offenders
|
|
247
|
+
|
|
248
|
+
Automatically ban IPs that violate rate limits repeatedly.
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
hitlimit({
|
|
252
|
+
limit: 100,
|
|
253
|
+
window: '1m',
|
|
254
|
+
ban: {
|
|
255
|
+
threshold: 5, // Ban after 5 violations
|
|
256
|
+
duration: '15m' // Ban for 15 minutes
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
When a client exceeds the rate limit 5 times, they'll be banned for 15 minutes. During the ban, all requests return 429 immediately.
|
|
262
|
+
|
|
263
|
+
### Shared Rate Limits (Group)
|
|
264
|
+
|
|
265
|
+
Share rate limits across multiple clients using a group identifier.
|
|
266
|
+
|
|
267
|
+
```javascript
|
|
268
|
+
hitlimit({
|
|
269
|
+
limit: 10000,
|
|
270
|
+
window: '1h',
|
|
271
|
+
group: (req) => req.user.teamId // Share limit across team
|
|
272
|
+
})
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
All requests with the same team ID share the same rate limit counter. Perfect for team-based SaaS quotas.
|
|
276
|
+
|
|
226
277
|
### Skip Certain Requests
|
|
227
278
|
|
|
228
279
|
Whitelist health checks, internal routes, or admin users.
|
|
@@ -237,6 +288,39 @@ hitlimit({
|
|
|
237
288
|
})
|
|
238
289
|
```
|
|
239
290
|
|
|
291
|
+
### Auto-Ban Repeat Offenders
|
|
292
|
+
|
|
293
|
+
Automatically ban clients that repeatedly exceed rate limits.
|
|
294
|
+
|
|
295
|
+
```javascript
|
|
296
|
+
hitlimit({
|
|
297
|
+
limit: 10,
|
|
298
|
+
window: '1m',
|
|
299
|
+
ban: {
|
|
300
|
+
threshold: 5, // Ban after 5 violations
|
|
301
|
+
duration: '1h' // Ban lasts 1 hour
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Banned clients receive `X-RateLimit-Ban: true` header and `banned: true` in the response body.
|
|
307
|
+
|
|
308
|
+
### Grouped / Shared Limits
|
|
309
|
+
|
|
310
|
+
Rate limit by organization, API key, or any shared identifier.
|
|
311
|
+
|
|
312
|
+
```javascript
|
|
313
|
+
// Per-API-key rate limiting
|
|
314
|
+
hitlimit({
|
|
315
|
+
limit: 1000,
|
|
316
|
+
window: '1h',
|
|
317
|
+
group: (req) => req.headers['x-api-key'] || 'anonymous'
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
// Static group prefix
|
|
321
|
+
hitlimit({ group: 'api', limit: 100, window: '1m' })
|
|
322
|
+
```
|
|
323
|
+
|
|
240
324
|
## Configuration Options
|
|
241
325
|
|
|
242
326
|
```javascript
|
|
@@ -276,7 +360,16 @@ hitlimit({
|
|
|
276
360
|
skip: (req) => req.path === '/health',
|
|
277
361
|
|
|
278
362
|
// Error handling
|
|
279
|
-
onStoreError: (error, req) => 'allow' // or 'deny'
|
|
363
|
+
onStoreError: (error, req) => 'allow', // or 'deny'
|
|
364
|
+
|
|
365
|
+
// Ban repeat offenders
|
|
366
|
+
ban: {
|
|
367
|
+
threshold: 5, // violations before ban
|
|
368
|
+
duration: '1h' // ban duration
|
|
369
|
+
},
|
|
370
|
+
|
|
371
|
+
// Group/shared limits
|
|
372
|
+
group: (req) => req.headers['x-api-key'] || 'default'
|
|
280
373
|
})
|
|
281
374
|
```
|
|
282
375
|
|
|
@@ -404,6 +497,18 @@ import { hitlimit } from '@joint-ops/hitlimit'
|
|
|
404
497
|
app.use(hitlimit({ limit: 100, window: '1m' }))
|
|
405
498
|
```
|
|
406
499
|
|
|
500
|
+
### From @fastify/rate-limit
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
// Before (@fastify/rate-limit)
|
|
504
|
+
import rateLimit from '@fastify/rate-limit'
|
|
505
|
+
await app.register(rateLimit, { max: 100, timeWindow: '1 minute' })
|
|
506
|
+
|
|
507
|
+
// After (hitlimit) - tiered limits, SQLite, multi-framework
|
|
508
|
+
import { hitlimit } from '@joint-ops/hitlimit/fastify'
|
|
509
|
+
await app.register(hitlimit, { limit: 100, window: '1m' })
|
|
510
|
+
```
|
|
511
|
+
|
|
407
512
|
### From @nestjs/throttler
|
|
408
513
|
|
|
409
514
|
```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/hono.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import type { HitLimitOptions } from '@joint-ops/hitlimit-types';
|
|
3
|
+
export declare function hitlimit(options?: HitLimitOptions<Context>): import("hono").MiddlewareHandler<any, string, {}, Response | (Response & import("hono").TypedResponse<{
|
|
4
|
+
[x: string]: any;
|
|
5
|
+
}, 429, "json">)>;
|
|
6
|
+
//# sourceMappingURL=hono.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../src/hono.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACnC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAahE,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAe,CAAC,OAAO,CAAM;;kBAmC9D"}
|
package/dist/hono.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createMiddleware } from 'hono/factory';
|
|
2
|
+
import { resolveConfig } from './core/config.js';
|
|
3
|
+
import { checkLimit } from './core/limiter.js';
|
|
4
|
+
import { memoryStore } from './stores/memory.js';
|
|
5
|
+
function getDefaultKey(c) {
|
|
6
|
+
// Hono doesn't expose raw IP — it's runtime-dependent
|
|
7
|
+
// Use standard proxy headers as fallback
|
|
8
|
+
return c.req.header('x-forwarded-for')?.split(',')[0]?.trim()
|
|
9
|
+
|| c.req.header('x-real-ip')
|
|
10
|
+
|| 'unknown';
|
|
11
|
+
}
|
|
12
|
+
export function hitlimit(options = {}) {
|
|
13
|
+
const store = options.store ?? memoryStore();
|
|
14
|
+
const config = resolveConfig(options, store, getDefaultKey);
|
|
15
|
+
return createMiddleware(async (c, next) => {
|
|
16
|
+
// Skip check
|
|
17
|
+
if (config.skip) {
|
|
18
|
+
const shouldSkip = await config.skip(c);
|
|
19
|
+
if (shouldSkip) {
|
|
20
|
+
await next();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const result = await checkLimit(config, c);
|
|
26
|
+
// Set all rate limit headers
|
|
27
|
+
for (const [key, value] of Object.entries(result.headers)) {
|
|
28
|
+
c.header(key, value);
|
|
29
|
+
}
|
|
30
|
+
// Block if not allowed
|
|
31
|
+
if (!result.allowed) {
|
|
32
|
+
return c.json(result.body, 429);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
const action = await config.onStoreError(error, c);
|
|
37
|
+
if (action === 'deny') {
|
|
38
|
+
return c.json({ hitlimit: true, message: 'Rate limit error' }, 429);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
await next();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=hono.js.map
|
package/dist/hono.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hono.js","sourceRoot":"","sources":["../src/hono.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAG/C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAEhD,SAAS,aAAa,CAAC,CAAU;IAC/B,sDAAsD;IACtD,yCAAyC;IACzC,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,iBAAiB,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE;WACxD,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC;WACzB,SAAS,CAAA;AAChB,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,UAAoC,EAAE;IAC7D,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,WAAW,EAAE,CAAA;IAC5C,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,CAAC,CAAA;IAE3D,OAAO,gBAAgB,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;QACxC,aAAa;QACb,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YAChB,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACvC,IAAI,UAAU,EAAE,CAAC;gBACf,MAAM,IAAI,EAAE,CAAA;gBACZ,OAAM;YACR,CAAC;QACH,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;YAE1C,6BAA6B;YAC7B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC1D,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;YACtB,CAAC;YAED,uBAAuB;YACvB,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;YACjC,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,KAAc,EAAE,CAAC,CAAC,CAAA;YAC3D,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;gBACtB,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,kBAAkB,EAAE,EAAE,GAAG,CAAC,CAAA;YACrE,CAAC;QACH,CAAC;QAED,MAAM,IAAI,EAAE,CAAA;IACd,CAAC,CAAC,CAAA;AACJ,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.1.0",
|
|
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",
|
|
@@ -93,6 +93,10 @@
|
|
|
93
93
|
"types": "./dist/fastify.d.ts",
|
|
94
94
|
"import": "./dist/fastify.js"
|
|
95
95
|
},
|
|
96
|
+
"./hono": {
|
|
97
|
+
"types": "./dist/hono.d.ts",
|
|
98
|
+
"import": "./dist/hono.js"
|
|
99
|
+
},
|
|
96
100
|
"./stores/memory": {
|
|
97
101
|
"types": "./dist/stores/memory.d.ts",
|
|
98
102
|
"import": "./dist/stores/memory.js"
|
|
@@ -129,14 +133,15 @@
|
|
|
129
133
|
"test:watch": "vitest"
|
|
130
134
|
},
|
|
131
135
|
"dependencies": {
|
|
132
|
-
"@joint-ops/hitlimit-types": "1.0
|
|
136
|
+
"@joint-ops/hitlimit-types": "1.1.0"
|
|
133
137
|
},
|
|
134
138
|
"peerDependencies": {
|
|
135
139
|
"@nestjs/common": ">=8.0.0",
|
|
136
140
|
"@nestjs/core": ">=8.0.0",
|
|
141
|
+
"better-sqlite3": ">=9.0.0",
|
|
137
142
|
"fastify": ">=4.0.0",
|
|
138
143
|
"fastify-plugin": ">=4.0.0",
|
|
139
|
-
"
|
|
144
|
+
"hono": ">=4.0.0",
|
|
140
145
|
"ioredis": ">=5.0.0",
|
|
141
146
|
"pino": ">=8.0.0",
|
|
142
147
|
"winston": ">=3.0.0"
|
|
@@ -154,6 +159,9 @@
|
|
|
154
159
|
"fastify-plugin": {
|
|
155
160
|
"optional": true
|
|
156
161
|
},
|
|
162
|
+
"hono": {
|
|
163
|
+
"optional": true
|
|
164
|
+
},
|
|
157
165
|
"better-sqlite3": {
|
|
158
166
|
"optional": true
|
|
159
167
|
},
|
|
@@ -168,6 +176,7 @@
|
|
|
168
176
|
}
|
|
169
177
|
},
|
|
170
178
|
"devDependencies": {
|
|
179
|
+
"@hono/node-server": "^1.19.9",
|
|
171
180
|
"@nestjs/common": "^10.0.0",
|
|
172
181
|
"@nestjs/core": "^10.0.0",
|
|
173
182
|
"@nestjs/platform-express": "^10.0.0",
|
|
@@ -180,6 +189,7 @@
|
|
|
180
189
|
"express": "^4.18.0",
|
|
181
190
|
"fastify": "^5.7.4",
|
|
182
191
|
"fastify-plugin": "^5.1.0",
|
|
192
|
+
"hono": "^4.11.9",
|
|
183
193
|
"ioredis": "^5.3.0",
|
|
184
194
|
"pino": "^10.3.0",
|
|
185
195
|
"reflect-metadata": "^0.2.0",
|