@joint-ops/hitlimit-bun 1.0.6 → 1.1.1
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 +36 -31
- package/dist/elysia.d.ts +11 -0
- package/dist/elysia.d.ts.map +1 -1
- package/dist/elysia.js +150 -88
- package/dist/hono.d.ts +6 -0
- package/dist/hono.d.ts.map +1 -0
- package/dist/hono.js +364 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +167 -83
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +1 -0
- package/dist/stores/sqlite.d.ts.map +1 -1
- package/dist/stores/sqlite.js +1 -0
- package/package.json +23 -12
package/README.md
CHANGED
|
@@ -6,36 +6,36 @@
|
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
7
|
[](https://bun.sh)
|
|
8
8
|
|
|
9
|
-
> The fastest rate limiter for Bun -
|
|
9
|
+
> The fastest rate limiter for Bun - 7M+ ops/sec with memory-first design | Elysia, Hono & Bun.serve
|
|
10
10
|
|
|
11
|
-
**hitlimit-bun** is a
|
|
11
|
+
**hitlimit-bun** is a blazing-fast, Bun-native rate limiting library for Bun.serve, Elysia, and Hono applications. **Memory-first by default** with 7.29M ops/sec performance (14.6x faster than SQLite). Optional persistence with native bun:sqlite or Redis when you need it.
|
|
12
12
|
|
|
13
13
|
**[Documentation](https://hitlimit.jointops.dev/docs/bun)** | **[GitHub](https://github.com/JointOps/hitlimit-monorepo)** | **[npm](https://www.npmjs.com/package/@joint-ops/hitlimit-bun)**
|
|
14
14
|
|
|
15
15
|
## ⚡ Why hitlimit-bun?
|
|
16
16
|
|
|
17
|
-
**
|
|
17
|
+
**Memory-first for maximum performance.** 14.6x faster than SQLite.
|
|
18
18
|
|
|
19
19
|
```
|
|
20
20
|
┌─────────────────────────────────────────────────────────────────┐
|
|
21
21
|
│ │
|
|
22
|
-
│
|
|
23
|
-
│
|
|
22
|
+
│ Memory (v1.1+) ██████████████████████████████ 7.29M ops/s │
|
|
23
|
+
│ SQLite (v1.0) █░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 500K ops/s │
|
|
24
24
|
│ │
|
|
25
|
-
│
|
|
25
|
+
│ 14.6x performance improvement with memory default │
|
|
26
26
|
│ │
|
|
27
27
|
└─────────────────────────────────────────────────────────────────┘
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
- **🚀 Memory-First** - 7.29M ops/sec by default (v1.1.0+), 14.6x faster than SQLite
|
|
30
31
|
- **Bun Native** - Built specifically for Bun's runtime, not a Node.js port
|
|
31
|
-
- **6.1M ops/sec** - Memory store (multi-IP scenarios)
|
|
32
|
-
- **386K ops/sec** - With bun:sqlite (multi-IP scenarios)
|
|
33
32
|
- **Zero Config** - Works out of the box with sensible defaults
|
|
34
|
-
- **
|
|
33
|
+
- **Framework Support** - First-class Elysia and Hono integration
|
|
34
|
+
- **Optional Persistence** - SQLite (500K ops/sec) or Redis (6.6K ops/sec) when needed
|
|
35
35
|
- **TypeScript First** - Full type safety and IntelliSense support
|
|
36
36
|
- **Auto-Ban** - Automatically ban repeat offenders after threshold violations
|
|
37
37
|
- **Shared Limits** - Group rate limits via groupId for teams/tenants
|
|
38
|
-
- **Tiny Footprint** - ~
|
|
38
|
+
- **Tiny Footprint** - ~18KB core bundle, zero runtime dependencies
|
|
39
39
|
|
|
40
40
|
## Installation
|
|
41
41
|
|
|
@@ -67,6 +67,20 @@ new Elysia()
|
|
|
67
67
|
.listen(3000)
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
### Hono Rate Limiting
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { Hono } from 'hono'
|
|
74
|
+
import { hitlimit } from '@joint-ops/hitlimit-bun/hono'
|
|
75
|
+
|
|
76
|
+
const app = new Hono()
|
|
77
|
+
|
|
78
|
+
app.use(hitlimit({ limit: 100, window: '1m' }))
|
|
79
|
+
app.get('/', (c) => c.text('Hello Bun!'))
|
|
80
|
+
|
|
81
|
+
Bun.serve({ port: 3000, fetch: app.fetch })
|
|
82
|
+
```
|
|
83
|
+
|
|
70
84
|
### Using createHitLimit
|
|
71
85
|
|
|
72
86
|
```typescript
|
|
@@ -235,7 +249,7 @@ hitlimit({
|
|
|
235
249
|
retryAfter: true // Retry-After header on 429
|
|
236
250
|
},
|
|
237
251
|
|
|
238
|
-
// Store (default:
|
|
252
|
+
// Store (default: memory)
|
|
239
253
|
store: sqliteStore({ path: './ratelimit.db' }),
|
|
240
254
|
|
|
241
255
|
// Skip rate limiting
|
|
@@ -260,39 +274,30 @@ hitlimit({
|
|
|
260
274
|
|
|
261
275
|
## Storage Backends
|
|
262
276
|
|
|
263
|
-
###
|
|
277
|
+
### Memory Store (Default)
|
|
264
278
|
|
|
265
|
-
|
|
279
|
+
Fastest option, used by default. No persistence.
|
|
266
280
|
|
|
267
281
|
```typescript
|
|
268
282
|
import { hitlimit } from '@joint-ops/hitlimit-bun'
|
|
269
283
|
|
|
270
|
-
// Default - uses
|
|
284
|
+
// Default - uses memory store (no config needed)
|
|
271
285
|
Bun.serve({
|
|
272
286
|
fetch: hitlimit({}, handler)
|
|
273
287
|
})
|
|
274
|
-
|
|
275
|
-
// Custom path for persistence
|
|
276
|
-
import { sqliteStore } from '@joint-ops/hitlimit-bun'
|
|
277
|
-
|
|
278
|
-
Bun.serve({
|
|
279
|
-
fetch: hitlimit({
|
|
280
|
-
store: sqliteStore({ path: './ratelimit.db' })
|
|
281
|
-
}, handler)
|
|
282
|
-
})
|
|
283
288
|
```
|
|
284
289
|
|
|
285
|
-
###
|
|
290
|
+
### SQLite Store
|
|
286
291
|
|
|
287
|
-
|
|
292
|
+
Uses Bun's native bun:sqlite for persistent rate limiting.
|
|
288
293
|
|
|
289
294
|
```typescript
|
|
290
295
|
import { hitlimit } from '@joint-ops/hitlimit-bun'
|
|
291
|
-
import {
|
|
296
|
+
import { sqliteStore } from '@joint-ops/hitlimit-bun'
|
|
292
297
|
|
|
293
298
|
Bun.serve({
|
|
294
299
|
fetch: hitlimit({
|
|
295
|
-
store:
|
|
300
|
+
store: sqliteStore({ path: './ratelimit.db' })
|
|
296
301
|
}, handler)
|
|
297
302
|
})
|
|
298
303
|
```
|
|
@@ -351,9 +356,9 @@ hitlimit-bun is optimized for Bun's runtime with native performance:
|
|
|
351
356
|
|
|
352
357
|
| Store | Operations/sec | vs Node.js |
|
|
353
358
|
|-------|----------------|------------|
|
|
354
|
-
| **Memory** | 7,
|
|
355
|
-
| **bun:sqlite** |
|
|
356
|
-
| **Redis** | 6,
|
|
359
|
+
| **Memory** | 7,290,000+ | +52% faster |
|
|
360
|
+
| **bun:sqlite** | 500,000+ | ~same |
|
|
361
|
+
| **Redis** | 6,600+ | ~same |
|
|
357
362
|
|
|
358
363
|
### HTTP Throughput
|
|
359
364
|
|
|
@@ -423,7 +428,7 @@ new Elysia()
|
|
|
423
428
|
Node.js rate limiters like express-rate-limit use better-sqlite3 which relies on N-API bindings. In Bun, this adds overhead and loses the performance benefits of Bun's native runtime.
|
|
424
429
|
|
|
425
430
|
**hitlimit-bun** is built specifically for Bun:
|
|
426
|
-
- Uses native `bun:sqlite` (
|
|
431
|
+
- Uses native `bun:sqlite` (no N-API overhead)
|
|
427
432
|
- No FFI overhead or Node.js polyfills
|
|
428
433
|
- First-class Elysia framework support
|
|
429
434
|
- Optimized for Bun.serve's request handling
|
package/dist/elysia.d.ts
CHANGED
|
@@ -3,6 +3,17 @@ import type { HitLimitOptions } from '@joint-ops/hitlimit-types';
|
|
|
3
3
|
export interface ElysiaHitLimitOptions extends HitLimitOptions<{
|
|
4
4
|
request: Request;
|
|
5
5
|
}> {
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated Use `store: sqliteStore({ path })` instead.
|
|
8
|
+
*
|
|
9
|
+
* Starting with v1.1.0, the default store is Memory for 15.7x better performance.
|
|
10
|
+
* If you need SQLite persistence:
|
|
11
|
+
*
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { sqliteStore } from '@joint-ops/hitlimit-bun/stores/sqlite'
|
|
14
|
+
* hitlimit({ store: sqliteStore({ path: './db.sqlite' }) })
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
6
17
|
sqlitePath?: string;
|
|
7
18
|
name?: string;
|
|
8
19
|
}
|
package/dist/elysia.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"elysia.d.ts","sourceRoot":"","sources":["../src/elysia.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,KAAK,EAAE,eAAe,
|
|
1
|
+
{"version":3,"file":"elysia.d.ts","sourceRoot":"","sources":["../src/elysia.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,KAAK,EAAE,eAAe,EAAgE,MAAM,2BAA2B,CAAA;AAK9H,MAAM,WAAW,qBAAsB,SAAQ,eAAe,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;IAClF;;;;;;;;;;OAUG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAkBD,wBAAgB,QAAQ,CAAC,OAAO,GAAE,qBAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyG3D"}
|
package/dist/elysia.js
CHANGED
|
@@ -1,99 +1,103 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/stores/
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
getStmt;
|
|
9
|
-
resetStmt;
|
|
10
|
-
isBannedStmt;
|
|
11
|
-
banStmt;
|
|
12
|
-
recordViolationStmt;
|
|
13
|
-
getViolationStmt;
|
|
14
|
-
resetBanStmt;
|
|
15
|
-
resetViolationStmt;
|
|
16
|
-
cleanupTimer;
|
|
17
|
-
constructor(options = {}) {
|
|
18
|
-
this.db = new Database(options.path ?? ":memory:");
|
|
19
|
-
this.db.exec(`
|
|
20
|
-
CREATE TABLE IF NOT EXISTS hitlimit (
|
|
21
|
-
key TEXT PRIMARY KEY,
|
|
22
|
-
count INTEGER NOT NULL,
|
|
23
|
-
reset_at INTEGER NOT NULL
|
|
24
|
-
)
|
|
25
|
-
`);
|
|
26
|
-
this.db.exec(`
|
|
27
|
-
CREATE TABLE IF NOT EXISTS hitlimit_bans (
|
|
28
|
-
key TEXT PRIMARY KEY,
|
|
29
|
-
expires_at INTEGER NOT NULL
|
|
30
|
-
)
|
|
31
|
-
`);
|
|
32
|
-
this.db.exec(`
|
|
33
|
-
CREATE TABLE IF NOT EXISTS hitlimit_violations (
|
|
34
|
-
key TEXT PRIMARY KEY,
|
|
35
|
-
count INTEGER NOT NULL DEFAULT 1,
|
|
36
|
-
reset_at INTEGER NOT NULL
|
|
37
|
-
)
|
|
38
|
-
`);
|
|
39
|
-
this.hitStmt = this.db.prepare(`
|
|
40
|
-
INSERT INTO hitlimit (key, count, reset_at) VALUES (?1, 1, ?2)
|
|
41
|
-
ON CONFLICT(key) DO UPDATE SET
|
|
42
|
-
count = CASE WHEN reset_at <= ?3 THEN 1 ELSE count + 1 END,
|
|
43
|
-
reset_at = CASE WHEN reset_at <= ?3 THEN ?2 ELSE reset_at END
|
|
44
|
-
`);
|
|
45
|
-
this.getStmt = this.db.prepare("SELECT count, reset_at FROM hitlimit WHERE key = ?");
|
|
46
|
-
this.resetStmt = this.db.prepare("DELETE FROM hitlimit WHERE key = ?");
|
|
47
|
-
this.isBannedStmt = this.db.prepare("SELECT 1 FROM hitlimit_bans WHERE key = ?1 AND expires_at > ?2");
|
|
48
|
-
this.banStmt = this.db.prepare("INSERT OR REPLACE INTO hitlimit_bans (key, expires_at) VALUES (?1, ?2)");
|
|
49
|
-
this.recordViolationStmt = this.db.prepare(`
|
|
50
|
-
INSERT INTO hitlimit_violations (key, count, reset_at) VALUES (?1, 1, ?2)
|
|
51
|
-
ON CONFLICT(key) DO UPDATE SET
|
|
52
|
-
count = CASE WHEN reset_at <= ?3 THEN 1 ELSE count + 1 END,
|
|
53
|
-
reset_at = CASE WHEN reset_at <= ?3 THEN ?2 ELSE reset_at END
|
|
54
|
-
`);
|
|
55
|
-
this.getViolationStmt = this.db.prepare("SELECT count FROM hitlimit_violations WHERE key = ?");
|
|
56
|
-
this.resetBanStmt = this.db.prepare("DELETE FROM hitlimit_bans WHERE key = ?");
|
|
57
|
-
this.resetViolationStmt = this.db.prepare("DELETE FROM hitlimit_violations WHERE key = ?");
|
|
58
|
-
this.cleanupTimer = setInterval(() => {
|
|
59
|
-
const now = Date.now();
|
|
60
|
-
this.db.prepare("DELETE FROM hitlimit WHERE reset_at <= ?").run(now);
|
|
61
|
-
this.db.prepare("DELETE FROM hitlimit_bans WHERE expires_at <= ?").run(now);
|
|
62
|
-
this.db.prepare("DELETE FROM hitlimit_violations WHERE reset_at <= ?").run(now);
|
|
63
|
-
}, 60000);
|
|
64
|
-
}
|
|
2
|
+
// src/stores/memory.ts
|
|
3
|
+
class MemoryStore {
|
|
4
|
+
isSync = true;
|
|
5
|
+
hits = new Map;
|
|
6
|
+
bans = new Map;
|
|
7
|
+
violations = new Map;
|
|
65
8
|
hit(key, windowMs, _limit) {
|
|
9
|
+
const entry = this.hits.get(key);
|
|
10
|
+
if (entry !== undefined) {
|
|
11
|
+
entry.count++;
|
|
12
|
+
return { count: entry.count, resetAt: entry.resetAt };
|
|
13
|
+
}
|
|
66
14
|
const now = Date.now();
|
|
67
15
|
const resetAt = now + windowMs;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
16
|
+
const timeoutId = setTimeout(() => {
|
|
17
|
+
this.hits.delete(key);
|
|
18
|
+
}, windowMs);
|
|
19
|
+
if (typeof timeoutId.unref === "function") {
|
|
20
|
+
timeoutId.unref();
|
|
21
|
+
}
|
|
22
|
+
this.hits.set(key, { count: 1, resetAt, timeoutId });
|
|
23
|
+
return { count: 1, resetAt };
|
|
71
24
|
}
|
|
72
25
|
isBanned(key) {
|
|
73
|
-
|
|
26
|
+
const ban = this.bans.get(key);
|
|
27
|
+
if (!ban)
|
|
28
|
+
return false;
|
|
29
|
+
if (Date.now() >= ban.expiresAt) {
|
|
30
|
+
clearTimeout(ban.timeoutId);
|
|
31
|
+
this.bans.delete(key);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
74
35
|
}
|
|
75
36
|
ban(key, durationMs) {
|
|
76
|
-
this.
|
|
37
|
+
const existing = this.bans.get(key);
|
|
38
|
+
if (existing)
|
|
39
|
+
clearTimeout(existing.timeoutId);
|
|
40
|
+
const expiresAt = Date.now() + durationMs;
|
|
41
|
+
const timeoutId = setTimeout(() => {
|
|
42
|
+
this.bans.delete(key);
|
|
43
|
+
}, durationMs);
|
|
44
|
+
if (typeof timeoutId.unref === "function") {
|
|
45
|
+
timeoutId.unref();
|
|
46
|
+
}
|
|
47
|
+
this.bans.set(key, { expiresAt, timeoutId });
|
|
77
48
|
}
|
|
78
49
|
recordViolation(key, windowMs) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
50
|
+
const entry = this.violations.get(key);
|
|
51
|
+
if (entry && Date.now() < entry.resetAt) {
|
|
52
|
+
entry.count++;
|
|
53
|
+
return entry.count;
|
|
54
|
+
}
|
|
55
|
+
if (entry)
|
|
56
|
+
clearTimeout(entry.timeoutId);
|
|
57
|
+
const resetAt = Date.now() + windowMs;
|
|
58
|
+
const timeoutId = setTimeout(() => {
|
|
59
|
+
this.violations.delete(key);
|
|
60
|
+
}, windowMs);
|
|
61
|
+
if (typeof timeoutId.unref === "function") {
|
|
62
|
+
timeoutId.unref();
|
|
63
|
+
}
|
|
64
|
+
this.violations.set(key, { count: 1, resetAt, timeoutId });
|
|
65
|
+
return 1;
|
|
84
66
|
}
|
|
85
67
|
reset(key) {
|
|
86
|
-
this.
|
|
87
|
-
|
|
88
|
-
|
|
68
|
+
const entry = this.hits.get(key);
|
|
69
|
+
if (entry) {
|
|
70
|
+
clearTimeout(entry.timeoutId);
|
|
71
|
+
this.hits.delete(key);
|
|
72
|
+
}
|
|
73
|
+
const ban = this.bans.get(key);
|
|
74
|
+
if (ban) {
|
|
75
|
+
clearTimeout(ban.timeoutId);
|
|
76
|
+
this.bans.delete(key);
|
|
77
|
+
}
|
|
78
|
+
const violation = this.violations.get(key);
|
|
79
|
+
if (violation) {
|
|
80
|
+
clearTimeout(violation.timeoutId);
|
|
81
|
+
this.violations.delete(key);
|
|
82
|
+
}
|
|
89
83
|
}
|
|
90
84
|
shutdown() {
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
for (const [, entry] of this.hits) {
|
|
86
|
+
clearTimeout(entry.timeoutId);
|
|
87
|
+
}
|
|
88
|
+
this.hits.clear();
|
|
89
|
+
for (const [, entry] of this.bans) {
|
|
90
|
+
clearTimeout(entry.timeoutId);
|
|
91
|
+
}
|
|
92
|
+
this.bans.clear();
|
|
93
|
+
for (const [, entry] of this.violations) {
|
|
94
|
+
clearTimeout(entry.timeoutId);
|
|
95
|
+
}
|
|
96
|
+
this.violations.clear();
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
|
-
function
|
|
96
|
-
return new
|
|
99
|
+
function memoryStore() {
|
|
100
|
+
return new MemoryStore;
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
// src/elysia.ts
|
|
@@ -277,10 +281,69 @@ var instanceCounter = 0;
|
|
|
277
281
|
function getDefaultKey(_ctx) {
|
|
278
282
|
return "unknown";
|
|
279
283
|
}
|
|
284
|
+
function buildResponseBody(response, info) {
|
|
285
|
+
if (typeof response === "function") {
|
|
286
|
+
return response(info);
|
|
287
|
+
}
|
|
288
|
+
return { ...response, limit: info.limit, remaining: info.remaining, resetIn: info.resetIn };
|
|
289
|
+
}
|
|
280
290
|
function hitlimit(options = {}) {
|
|
281
291
|
const pluginName = options.name ?? `hitlimit-${instanceCounter++}`;
|
|
282
|
-
|
|
292
|
+
if (options.sqlitePath && !options.store) {
|
|
293
|
+
console.warn("[hitlimit-bun] DEPRECATION WARNING: " + "sqlitePath is deprecated and will be ignored. " + "Use store: sqliteStore({ path }) instead. " + "See migration guide: https://hitlimit.jointops.dev/docs/migration/v1.1.0");
|
|
294
|
+
}
|
|
295
|
+
const store = options.store ?? memoryStore();
|
|
283
296
|
const config = resolveConfig(options, store, getDefaultKey);
|
|
297
|
+
const hasSkip = !!config.skip;
|
|
298
|
+
const hasTiers = !!(config.tier && config.tiers);
|
|
299
|
+
const hasBan = !!config.ban;
|
|
300
|
+
const hasGroup = !!config.group;
|
|
301
|
+
const standardHeaders = config.headers.standard;
|
|
302
|
+
const legacyHeaders = config.headers.legacy;
|
|
303
|
+
const retryAfterHeader = config.headers.retryAfter;
|
|
304
|
+
const limit = config.limit;
|
|
305
|
+
const windowMs = config.windowMs;
|
|
306
|
+
const responseConfig = config.response;
|
|
307
|
+
const isSyncStore = store.isSync === true;
|
|
308
|
+
const isSyncKey = !options.key;
|
|
309
|
+
if (!hasSkip && !hasTiers && !hasBan && !hasGroup && isSyncStore && isSyncKey) {
|
|
310
|
+
return new Elysia({ name: pluginName }).onBeforeHandle({ as: "scoped" }, ({ set }) => {
|
|
311
|
+
const key = "unknown";
|
|
312
|
+
const result = store.hit(key, windowMs, limit);
|
|
313
|
+
const allowed = result.count <= limit;
|
|
314
|
+
const remaining = Math.max(0, limit - result.count);
|
|
315
|
+
const resetIn = Math.ceil((result.resetAt - Date.now()) / 1000);
|
|
316
|
+
if (standardHeaders) {
|
|
317
|
+
set.headers["RateLimit-Limit"] = String(limit);
|
|
318
|
+
set.headers["RateLimit-Remaining"] = String(remaining);
|
|
319
|
+
set.headers["RateLimit-Reset"] = String(resetIn);
|
|
320
|
+
}
|
|
321
|
+
if (legacyHeaders) {
|
|
322
|
+
set.headers["X-RateLimit-Limit"] = String(limit);
|
|
323
|
+
set.headers["X-RateLimit-Remaining"] = String(remaining);
|
|
324
|
+
set.headers["X-RateLimit-Reset"] = String(Math.ceil(result.resetAt / 1000));
|
|
325
|
+
}
|
|
326
|
+
if (!allowed) {
|
|
327
|
+
if (retryAfterHeader) {
|
|
328
|
+
set.headers["Retry-After"] = String(resetIn);
|
|
329
|
+
}
|
|
330
|
+
const body = buildResponseBody(responseConfig, {
|
|
331
|
+
limit,
|
|
332
|
+
remaining: 0,
|
|
333
|
+
resetIn,
|
|
334
|
+
resetAt: result.resetAt,
|
|
335
|
+
key
|
|
336
|
+
});
|
|
337
|
+
set.status = 429;
|
|
338
|
+
const headers = { "Content-Type": "application/json" };
|
|
339
|
+
for (const [k, v] of Object.entries(set.headers)) {
|
|
340
|
+
if (v != null)
|
|
341
|
+
headers[k] = String(v);
|
|
342
|
+
}
|
|
343
|
+
return new Response(JSON.stringify(body), { status: 429, headers });
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
284
347
|
return new Elysia({ name: pluginName }).onBeforeHandle({ as: "scoped" }, async ({ request, set }) => {
|
|
285
348
|
const ctx = { request };
|
|
286
349
|
if (config.skip) {
|
|
@@ -296,13 +359,12 @@ function hitlimit(options = {}) {
|
|
|
296
359
|
});
|
|
297
360
|
if (!result.allowed) {
|
|
298
361
|
set.status = 429;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
});
|
|
362
|
+
const headers = { "Content-Type": "application/json" };
|
|
363
|
+
for (const [k, v] of Object.entries(result.headers)) {
|
|
364
|
+
if (v != null)
|
|
365
|
+
headers[k] = String(v);
|
|
366
|
+
}
|
|
367
|
+
return new Response(JSON.stringify(result.body), { status: 429, headers });
|
|
306
368
|
}
|
|
307
369
|
} catch (error) {
|
|
308
370
|
const action = await config.onStoreError(error, ctx);
|
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,EAAgE,MAAM,2BAA2B,CAAA;AAqB9H,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAe,CAAC,OAAO,CAAM;;kBAkF9D"}
|
package/dist/hono.js
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/stores/memory.ts
|
|
3
|
+
class MemoryStore {
|
|
4
|
+
isSync = true;
|
|
5
|
+
hits = new Map;
|
|
6
|
+
bans = new Map;
|
|
7
|
+
violations = new Map;
|
|
8
|
+
hit(key, windowMs, _limit) {
|
|
9
|
+
const entry = this.hits.get(key);
|
|
10
|
+
if (entry !== undefined) {
|
|
11
|
+
entry.count++;
|
|
12
|
+
return { count: entry.count, resetAt: entry.resetAt };
|
|
13
|
+
}
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
const resetAt = now + windowMs;
|
|
16
|
+
const timeoutId = setTimeout(() => {
|
|
17
|
+
this.hits.delete(key);
|
|
18
|
+
}, windowMs);
|
|
19
|
+
if (typeof timeoutId.unref === "function") {
|
|
20
|
+
timeoutId.unref();
|
|
21
|
+
}
|
|
22
|
+
this.hits.set(key, { count: 1, resetAt, timeoutId });
|
|
23
|
+
return { count: 1, resetAt };
|
|
24
|
+
}
|
|
25
|
+
isBanned(key) {
|
|
26
|
+
const ban = this.bans.get(key);
|
|
27
|
+
if (!ban)
|
|
28
|
+
return false;
|
|
29
|
+
if (Date.now() >= ban.expiresAt) {
|
|
30
|
+
clearTimeout(ban.timeoutId);
|
|
31
|
+
this.bans.delete(key);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
ban(key, durationMs) {
|
|
37
|
+
const existing = this.bans.get(key);
|
|
38
|
+
if (existing)
|
|
39
|
+
clearTimeout(existing.timeoutId);
|
|
40
|
+
const expiresAt = Date.now() + durationMs;
|
|
41
|
+
const timeoutId = setTimeout(() => {
|
|
42
|
+
this.bans.delete(key);
|
|
43
|
+
}, durationMs);
|
|
44
|
+
if (typeof timeoutId.unref === "function") {
|
|
45
|
+
timeoutId.unref();
|
|
46
|
+
}
|
|
47
|
+
this.bans.set(key, { expiresAt, timeoutId });
|
|
48
|
+
}
|
|
49
|
+
recordViolation(key, windowMs) {
|
|
50
|
+
const entry = this.violations.get(key);
|
|
51
|
+
if (entry && Date.now() < entry.resetAt) {
|
|
52
|
+
entry.count++;
|
|
53
|
+
return entry.count;
|
|
54
|
+
}
|
|
55
|
+
if (entry)
|
|
56
|
+
clearTimeout(entry.timeoutId);
|
|
57
|
+
const resetAt = Date.now() + windowMs;
|
|
58
|
+
const timeoutId = setTimeout(() => {
|
|
59
|
+
this.violations.delete(key);
|
|
60
|
+
}, windowMs);
|
|
61
|
+
if (typeof timeoutId.unref === "function") {
|
|
62
|
+
timeoutId.unref();
|
|
63
|
+
}
|
|
64
|
+
this.violations.set(key, { count: 1, resetAt, timeoutId });
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
reset(key) {
|
|
68
|
+
const entry = this.hits.get(key);
|
|
69
|
+
if (entry) {
|
|
70
|
+
clearTimeout(entry.timeoutId);
|
|
71
|
+
this.hits.delete(key);
|
|
72
|
+
}
|
|
73
|
+
const ban = this.bans.get(key);
|
|
74
|
+
if (ban) {
|
|
75
|
+
clearTimeout(ban.timeoutId);
|
|
76
|
+
this.bans.delete(key);
|
|
77
|
+
}
|
|
78
|
+
const violation = this.violations.get(key);
|
|
79
|
+
if (violation) {
|
|
80
|
+
clearTimeout(violation.timeoutId);
|
|
81
|
+
this.violations.delete(key);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
shutdown() {
|
|
85
|
+
for (const [, entry] of this.hits) {
|
|
86
|
+
clearTimeout(entry.timeoutId);
|
|
87
|
+
}
|
|
88
|
+
this.hits.clear();
|
|
89
|
+
for (const [, entry] of this.bans) {
|
|
90
|
+
clearTimeout(entry.timeoutId);
|
|
91
|
+
}
|
|
92
|
+
this.bans.clear();
|
|
93
|
+
for (const [, entry] of this.violations) {
|
|
94
|
+
clearTimeout(entry.timeoutId);
|
|
95
|
+
}
|
|
96
|
+
this.violations.clear();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function memoryStore() {
|
|
100
|
+
return new MemoryStore;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/hono.ts
|
|
104
|
+
import { createMiddleware } from "hono/factory";
|
|
105
|
+
|
|
106
|
+
// src/core/utils.ts
|
|
107
|
+
var UNITS = {
|
|
108
|
+
s: 1000,
|
|
109
|
+
m: 60 * 1000,
|
|
110
|
+
h: 60 * 60 * 1000,
|
|
111
|
+
d: 24 * 60 * 60 * 1000
|
|
112
|
+
};
|
|
113
|
+
function parseWindow(window) {
|
|
114
|
+
if (typeof window === "number")
|
|
115
|
+
return window;
|
|
116
|
+
const match = window.match(/^(\d+)(s|m|h|d)$/);
|
|
117
|
+
if (!match)
|
|
118
|
+
throw new Error(`Invalid window format: ${window}`);
|
|
119
|
+
return parseInt(match[1]) * UNITS[match[2]];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/core/config.ts
|
|
123
|
+
function resolveConfig(options, defaultStore, defaultKey) {
|
|
124
|
+
return {
|
|
125
|
+
limit: options.limit ?? 100,
|
|
126
|
+
windowMs: parseWindow(options.window ?? "1m"),
|
|
127
|
+
key: options.key ?? defaultKey,
|
|
128
|
+
tiers: options.tiers,
|
|
129
|
+
tier: options.tier,
|
|
130
|
+
response: options.response ?? { hitlimit: true, message: "Whoa there! Rate limit exceeded." },
|
|
131
|
+
headers: {
|
|
132
|
+
standard: options.headers?.standard ?? true,
|
|
133
|
+
legacy: options.headers?.legacy ?? true,
|
|
134
|
+
retryAfter: options.headers?.retryAfter ?? true
|
|
135
|
+
},
|
|
136
|
+
store: options.store ?? defaultStore,
|
|
137
|
+
onStoreError: options.onStoreError ?? (() => "allow"),
|
|
138
|
+
skip: options.skip,
|
|
139
|
+
ban: options.ban ? { threshold: options.ban.threshold, durationMs: parseWindow(options.ban.duration) } : null,
|
|
140
|
+
group: options.group ?? null
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/core/headers.ts
|
|
145
|
+
function buildHeaders(info, config, allowed) {
|
|
146
|
+
const headers = {};
|
|
147
|
+
if (config.standard) {
|
|
148
|
+
headers["RateLimit-Limit"] = String(info.limit);
|
|
149
|
+
headers["RateLimit-Remaining"] = String(info.remaining);
|
|
150
|
+
headers["RateLimit-Reset"] = String(Math.ceil(info.resetAt / 1000));
|
|
151
|
+
}
|
|
152
|
+
if (config.legacy) {
|
|
153
|
+
headers["X-RateLimit-Limit"] = String(info.limit);
|
|
154
|
+
headers["X-RateLimit-Remaining"] = String(info.remaining);
|
|
155
|
+
headers["X-RateLimit-Reset"] = String(Math.ceil(info.resetAt / 1000));
|
|
156
|
+
}
|
|
157
|
+
if (!allowed && config.retryAfter) {
|
|
158
|
+
headers["Retry-After"] = String(info.resetIn);
|
|
159
|
+
}
|
|
160
|
+
if (info.banned) {
|
|
161
|
+
headers["X-RateLimit-Ban"] = "true";
|
|
162
|
+
if (info.banExpiresAt) {
|
|
163
|
+
headers["X-RateLimit-Ban-Expires"] = String(Math.ceil(info.banExpiresAt / 1000));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return headers;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/core/response.ts
|
|
170
|
+
function buildBody(response, info) {
|
|
171
|
+
if (typeof response === "function") {
|
|
172
|
+
return response(info);
|
|
173
|
+
}
|
|
174
|
+
const body = {
|
|
175
|
+
...response,
|
|
176
|
+
limit: info.limit,
|
|
177
|
+
remaining: info.remaining,
|
|
178
|
+
resetIn: info.resetIn
|
|
179
|
+
};
|
|
180
|
+
if (info.banned) {
|
|
181
|
+
body.banned = true;
|
|
182
|
+
body.banExpiresAt = info.banExpiresAt;
|
|
183
|
+
body.message = "You have been temporarily banned due to repeated rate limit violations";
|
|
184
|
+
}
|
|
185
|
+
return body;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/core/limiter.ts
|
|
189
|
+
async function resolveKey(config, req) {
|
|
190
|
+
let key = await config.key(req);
|
|
191
|
+
let groupId;
|
|
192
|
+
if (config.group) {
|
|
193
|
+
groupId = typeof config.group === "function" ? await config.group(req) : config.group;
|
|
194
|
+
key = `group:${groupId}:${key}`;
|
|
195
|
+
}
|
|
196
|
+
return { key, groupId };
|
|
197
|
+
}
|
|
198
|
+
function resolveTier(config, tierName) {
|
|
199
|
+
if (tierName && config.tiers) {
|
|
200
|
+
const tierConfig = config.tiers[tierName];
|
|
201
|
+
if (tierConfig) {
|
|
202
|
+
return {
|
|
203
|
+
limit: tierConfig.limit,
|
|
204
|
+
windowMs: tierConfig.window ? parseWindow(tierConfig.window) : config.windowMs
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { limit: config.limit, windowMs: config.windowMs };
|
|
209
|
+
}
|
|
210
|
+
async function checkLimit(config, req) {
|
|
211
|
+
const { key, groupId } = await resolveKey(config, req);
|
|
212
|
+
let tierName;
|
|
213
|
+
if (config.tier && config.tiers) {
|
|
214
|
+
tierName = await config.tier(req);
|
|
215
|
+
}
|
|
216
|
+
const { limit, windowMs } = resolveTier(config, tierName);
|
|
217
|
+
if (config.ban && config.store.isBanned) {
|
|
218
|
+
const banned = await config.store.isBanned(key);
|
|
219
|
+
if (banned) {
|
|
220
|
+
const banResetIn = Math.ceil(config.ban.durationMs / 1000);
|
|
221
|
+
const info2 = {
|
|
222
|
+
limit,
|
|
223
|
+
remaining: 0,
|
|
224
|
+
resetIn: banResetIn,
|
|
225
|
+
resetAt: Date.now() + config.ban.durationMs,
|
|
226
|
+
key,
|
|
227
|
+
tier: tierName,
|
|
228
|
+
banned: true,
|
|
229
|
+
banExpiresAt: Date.now() + config.ban.durationMs,
|
|
230
|
+
group: groupId
|
|
231
|
+
};
|
|
232
|
+
return {
|
|
233
|
+
allowed: false,
|
|
234
|
+
info: info2,
|
|
235
|
+
headers: buildHeaders(info2, config.headers, false),
|
|
236
|
+
body: buildBody(config.response, info2)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (limit === Infinity) {
|
|
241
|
+
return {
|
|
242
|
+
allowed: true,
|
|
243
|
+
info: { limit, remaining: Infinity, resetIn: 0, resetAt: 0, key, tier: tierName, group: groupId },
|
|
244
|
+
headers: {},
|
|
245
|
+
body: {}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const result = await config.store.hit(key, windowMs, limit);
|
|
249
|
+
const now = Date.now();
|
|
250
|
+
const resetIn = Math.max(0, Math.ceil((result.resetAt - now) / 1000));
|
|
251
|
+
const remaining = Math.max(0, limit - result.count);
|
|
252
|
+
const allowed = result.count <= limit;
|
|
253
|
+
const info = {
|
|
254
|
+
limit,
|
|
255
|
+
remaining,
|
|
256
|
+
resetIn,
|
|
257
|
+
resetAt: result.resetAt,
|
|
258
|
+
key,
|
|
259
|
+
tier: tierName,
|
|
260
|
+
group: groupId
|
|
261
|
+
};
|
|
262
|
+
if (!allowed && config.ban && config.store.recordViolation) {
|
|
263
|
+
const violations = await config.store.recordViolation(key, config.ban.durationMs);
|
|
264
|
+
info.violations = violations;
|
|
265
|
+
if (violations >= config.ban.threshold && config.store.ban) {
|
|
266
|
+
await config.store.ban(key, config.ban.durationMs);
|
|
267
|
+
info.banned = true;
|
|
268
|
+
info.banExpiresAt = now + config.ban.durationMs;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
allowed,
|
|
273
|
+
info,
|
|
274
|
+
headers: buildHeaders(info, config.headers, allowed),
|
|
275
|
+
body: allowed ? {} : buildBody(config.response, info)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/hono.ts
|
|
280
|
+
function getDefaultKey(c) {
|
|
281
|
+
return c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || c.req.header("x-real-ip") || "unknown";
|
|
282
|
+
}
|
|
283
|
+
function buildResponseBody(response, info) {
|
|
284
|
+
if (typeof response === "function") {
|
|
285
|
+
return response(info);
|
|
286
|
+
}
|
|
287
|
+
return { ...response, limit: info.limit, remaining: info.remaining, resetIn: info.resetIn };
|
|
288
|
+
}
|
|
289
|
+
function hitlimit(options = {}) {
|
|
290
|
+
const store = options.store ?? memoryStore();
|
|
291
|
+
const config = resolveConfig(options, store, getDefaultKey);
|
|
292
|
+
const hasSkip = !!config.skip;
|
|
293
|
+
const hasTiers = !!(config.tier && config.tiers);
|
|
294
|
+
const hasBan = !!config.ban;
|
|
295
|
+
const hasGroup = !!config.group;
|
|
296
|
+
const standardHeaders = config.headers.standard;
|
|
297
|
+
const legacyHeaders = config.headers.legacy;
|
|
298
|
+
const retryAfterHeader = config.headers.retryAfter;
|
|
299
|
+
const limit = config.limit;
|
|
300
|
+
const windowMs = config.windowMs;
|
|
301
|
+
const responseConfig = config.response;
|
|
302
|
+
const isSyncStore = store.isSync === true;
|
|
303
|
+
const isSyncKey = !options.key;
|
|
304
|
+
if (!hasSkip && !hasTiers && !hasBan && !hasGroup && isSyncStore && isSyncKey) {
|
|
305
|
+
return createMiddleware(async (c, next) => {
|
|
306
|
+
const key = c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || c.req.header("x-real-ip") || "unknown";
|
|
307
|
+
const result = store.hit(key, windowMs, limit);
|
|
308
|
+
const allowed = result.count <= limit;
|
|
309
|
+
const remaining = Math.max(0, limit - result.count);
|
|
310
|
+
const resetIn = Math.ceil((result.resetAt - Date.now()) / 1000);
|
|
311
|
+
if (standardHeaders) {
|
|
312
|
+
c.header("RateLimit-Limit", String(limit));
|
|
313
|
+
c.header("RateLimit-Remaining", String(remaining));
|
|
314
|
+
c.header("RateLimit-Reset", String(resetIn));
|
|
315
|
+
}
|
|
316
|
+
if (legacyHeaders) {
|
|
317
|
+
c.header("X-RateLimit-Limit", String(limit));
|
|
318
|
+
c.header("X-RateLimit-Remaining", String(remaining));
|
|
319
|
+
c.header("X-RateLimit-Reset", String(Math.ceil(result.resetAt / 1000)));
|
|
320
|
+
}
|
|
321
|
+
if (!allowed) {
|
|
322
|
+
if (retryAfterHeader) {
|
|
323
|
+
c.header("Retry-After", String(resetIn));
|
|
324
|
+
}
|
|
325
|
+
const body = buildResponseBody(responseConfig, {
|
|
326
|
+
limit,
|
|
327
|
+
remaining: 0,
|
|
328
|
+
resetIn,
|
|
329
|
+
resetAt: result.resetAt,
|
|
330
|
+
key
|
|
331
|
+
});
|
|
332
|
+
return c.json(body, 429);
|
|
333
|
+
}
|
|
334
|
+
await next();
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return createMiddleware(async (c, next) => {
|
|
338
|
+
if (config.skip) {
|
|
339
|
+
const shouldSkip = await config.skip(c);
|
|
340
|
+
if (shouldSkip) {
|
|
341
|
+
await next();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
const result = await checkLimit(config, c);
|
|
347
|
+
for (const [key, value] of Object.entries(result.headers)) {
|
|
348
|
+
c.header(key, value);
|
|
349
|
+
}
|
|
350
|
+
if (!result.allowed) {
|
|
351
|
+
return c.json(result.body, 429);
|
|
352
|
+
}
|
|
353
|
+
} catch (error) {
|
|
354
|
+
const action = await config.onStoreError(error, c);
|
|
355
|
+
if (action === "deny") {
|
|
356
|
+
return c.json({ hitlimit: true, message: "Rate limit error" }, 429);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
await next();
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
export {
|
|
363
|
+
hitlimit
|
|
364
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -2,9 +2,20 @@ import type { HitLimitOptions } from '@joint-ops/hitlimit-types';
|
|
|
2
2
|
import { checkLimit } from './core/limiter.js';
|
|
3
3
|
export type { HitLimitOptions, HitLimitInfo, HitLimitResult, HitLimitStore, StoreResult, TierConfig, HeadersConfig, ResolvedConfig, KeyGenerator, TierResolver, SkipFunction, StoreErrorHandler, ResponseFormatter, ResponseConfig, BanConfig, GroupIdResolver } from '@joint-ops/hitlimit-types';
|
|
4
4
|
export { DEFAULT_LIMIT, DEFAULT_WINDOW, DEFAULT_WINDOW_MS, DEFAULT_MESSAGE } from '@joint-ops/hitlimit-types';
|
|
5
|
-
export {
|
|
5
|
+
export { memoryStore } from './stores/memory.js';
|
|
6
6
|
export { checkLimit };
|
|
7
7
|
export interface BunHitLimitOptions extends HitLimitOptions<Request> {
|
|
8
|
+
/**
|
|
9
|
+
* @deprecated Use `store: sqliteStore({ path })` instead.
|
|
10
|
+
*
|
|
11
|
+
* Starting with v1.1.0, the default store is Memory for 15.7x better performance.
|
|
12
|
+
* If you need SQLite persistence:
|
|
13
|
+
*
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { sqliteStore } from '@joint-ops/hitlimit-bun/stores/sqlite'
|
|
16
|
+
* hitlimit({ store: sqliteStore({ path: './db.sqlite' }) }, handler)
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
8
19
|
sqlitePath?: string;
|
|
9
20
|
}
|
|
10
21
|
type BunServer = {
|
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,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAEhE,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;AAErB,MAAM,WAAW,kBAAmB,SAAQ,eAAe,CAAC,OAAO,CAAC;IAClE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,KAAK,SAAS,GAAG;IAAE,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CAAE,CAAA;AAExE,KAAK,YAAY,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAMrF,wBAAgB,QAAQ,CACtB,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,YAAY,GACpB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAEhE,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;AAErB,MAAM,WAAW,kBAAmB,SAAQ,eAAe,CAAC,OAAO,CAAC;IAClE;;;;;;;;;;OAUG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,KAAK,SAAS,GAAG;IAAE,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CAAE,CAAA;AAExE,KAAK,YAAY,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAMrF,wBAAgB,QAAQ,CACtB,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,YAAY,GACpB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsPnE;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,IAAI,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAA;IAClF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CACzC;AAED,wBAAgB,cAAc,CAAC,OAAO,GAAE,kBAAuB,GAAG,UAAU,CA0D3E"}
|
package/dist/index.js
CHANGED
|
@@ -1,99 +1,103 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/stores/
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
getStmt;
|
|
9
|
-
resetStmt;
|
|
10
|
-
isBannedStmt;
|
|
11
|
-
banStmt;
|
|
12
|
-
recordViolationStmt;
|
|
13
|
-
getViolationStmt;
|
|
14
|
-
resetBanStmt;
|
|
15
|
-
resetViolationStmt;
|
|
16
|
-
cleanupTimer;
|
|
17
|
-
constructor(options = {}) {
|
|
18
|
-
this.db = new Database(options.path ?? ":memory:");
|
|
19
|
-
this.db.exec(`
|
|
20
|
-
CREATE TABLE IF NOT EXISTS hitlimit (
|
|
21
|
-
key TEXT PRIMARY KEY,
|
|
22
|
-
count INTEGER NOT NULL,
|
|
23
|
-
reset_at INTEGER NOT NULL
|
|
24
|
-
)
|
|
25
|
-
`);
|
|
26
|
-
this.db.exec(`
|
|
27
|
-
CREATE TABLE IF NOT EXISTS hitlimit_bans (
|
|
28
|
-
key TEXT PRIMARY KEY,
|
|
29
|
-
expires_at INTEGER NOT NULL
|
|
30
|
-
)
|
|
31
|
-
`);
|
|
32
|
-
this.db.exec(`
|
|
33
|
-
CREATE TABLE IF NOT EXISTS hitlimit_violations (
|
|
34
|
-
key TEXT PRIMARY KEY,
|
|
35
|
-
count INTEGER NOT NULL DEFAULT 1,
|
|
36
|
-
reset_at INTEGER NOT NULL
|
|
37
|
-
)
|
|
38
|
-
`);
|
|
39
|
-
this.hitStmt = this.db.prepare(`
|
|
40
|
-
INSERT INTO hitlimit (key, count, reset_at) VALUES (?1, 1, ?2)
|
|
41
|
-
ON CONFLICT(key) DO UPDATE SET
|
|
42
|
-
count = CASE WHEN reset_at <= ?3 THEN 1 ELSE count + 1 END,
|
|
43
|
-
reset_at = CASE WHEN reset_at <= ?3 THEN ?2 ELSE reset_at END
|
|
44
|
-
`);
|
|
45
|
-
this.getStmt = this.db.prepare("SELECT count, reset_at FROM hitlimit WHERE key = ?");
|
|
46
|
-
this.resetStmt = this.db.prepare("DELETE FROM hitlimit WHERE key = ?");
|
|
47
|
-
this.isBannedStmt = this.db.prepare("SELECT 1 FROM hitlimit_bans WHERE key = ?1 AND expires_at > ?2");
|
|
48
|
-
this.banStmt = this.db.prepare("INSERT OR REPLACE INTO hitlimit_bans (key, expires_at) VALUES (?1, ?2)");
|
|
49
|
-
this.recordViolationStmt = this.db.prepare(`
|
|
50
|
-
INSERT INTO hitlimit_violations (key, count, reset_at) VALUES (?1, 1, ?2)
|
|
51
|
-
ON CONFLICT(key) DO UPDATE SET
|
|
52
|
-
count = CASE WHEN reset_at <= ?3 THEN 1 ELSE count + 1 END,
|
|
53
|
-
reset_at = CASE WHEN reset_at <= ?3 THEN ?2 ELSE reset_at END
|
|
54
|
-
`);
|
|
55
|
-
this.getViolationStmt = this.db.prepare("SELECT count FROM hitlimit_violations WHERE key = ?");
|
|
56
|
-
this.resetBanStmt = this.db.prepare("DELETE FROM hitlimit_bans WHERE key = ?");
|
|
57
|
-
this.resetViolationStmt = this.db.prepare("DELETE FROM hitlimit_violations WHERE key = ?");
|
|
58
|
-
this.cleanupTimer = setInterval(() => {
|
|
59
|
-
const now = Date.now();
|
|
60
|
-
this.db.prepare("DELETE FROM hitlimit WHERE reset_at <= ?").run(now);
|
|
61
|
-
this.db.prepare("DELETE FROM hitlimit_bans WHERE expires_at <= ?").run(now);
|
|
62
|
-
this.db.prepare("DELETE FROM hitlimit_violations WHERE reset_at <= ?").run(now);
|
|
63
|
-
}, 60000);
|
|
64
|
-
}
|
|
2
|
+
// src/stores/memory.ts
|
|
3
|
+
class MemoryStore {
|
|
4
|
+
isSync = true;
|
|
5
|
+
hits = new Map;
|
|
6
|
+
bans = new Map;
|
|
7
|
+
violations = new Map;
|
|
65
8
|
hit(key, windowMs, _limit) {
|
|
9
|
+
const entry = this.hits.get(key);
|
|
10
|
+
if (entry !== undefined) {
|
|
11
|
+
entry.count++;
|
|
12
|
+
return { count: entry.count, resetAt: entry.resetAt };
|
|
13
|
+
}
|
|
66
14
|
const now = Date.now();
|
|
67
15
|
const resetAt = now + windowMs;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
16
|
+
const timeoutId = setTimeout(() => {
|
|
17
|
+
this.hits.delete(key);
|
|
18
|
+
}, windowMs);
|
|
19
|
+
if (typeof timeoutId.unref === "function") {
|
|
20
|
+
timeoutId.unref();
|
|
21
|
+
}
|
|
22
|
+
this.hits.set(key, { count: 1, resetAt, timeoutId });
|
|
23
|
+
return { count: 1, resetAt };
|
|
71
24
|
}
|
|
72
25
|
isBanned(key) {
|
|
73
|
-
|
|
26
|
+
const ban = this.bans.get(key);
|
|
27
|
+
if (!ban)
|
|
28
|
+
return false;
|
|
29
|
+
if (Date.now() >= ban.expiresAt) {
|
|
30
|
+
clearTimeout(ban.timeoutId);
|
|
31
|
+
this.bans.delete(key);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
74
35
|
}
|
|
75
36
|
ban(key, durationMs) {
|
|
76
|
-
this.
|
|
37
|
+
const existing = this.bans.get(key);
|
|
38
|
+
if (existing)
|
|
39
|
+
clearTimeout(existing.timeoutId);
|
|
40
|
+
const expiresAt = Date.now() + durationMs;
|
|
41
|
+
const timeoutId = setTimeout(() => {
|
|
42
|
+
this.bans.delete(key);
|
|
43
|
+
}, durationMs);
|
|
44
|
+
if (typeof timeoutId.unref === "function") {
|
|
45
|
+
timeoutId.unref();
|
|
46
|
+
}
|
|
47
|
+
this.bans.set(key, { expiresAt, timeoutId });
|
|
77
48
|
}
|
|
78
49
|
recordViolation(key, windowMs) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
50
|
+
const entry = this.violations.get(key);
|
|
51
|
+
if (entry && Date.now() < entry.resetAt) {
|
|
52
|
+
entry.count++;
|
|
53
|
+
return entry.count;
|
|
54
|
+
}
|
|
55
|
+
if (entry)
|
|
56
|
+
clearTimeout(entry.timeoutId);
|
|
57
|
+
const resetAt = Date.now() + windowMs;
|
|
58
|
+
const timeoutId = setTimeout(() => {
|
|
59
|
+
this.violations.delete(key);
|
|
60
|
+
}, windowMs);
|
|
61
|
+
if (typeof timeoutId.unref === "function") {
|
|
62
|
+
timeoutId.unref();
|
|
63
|
+
}
|
|
64
|
+
this.violations.set(key, { count: 1, resetAt, timeoutId });
|
|
65
|
+
return 1;
|
|
84
66
|
}
|
|
85
67
|
reset(key) {
|
|
86
|
-
this.
|
|
87
|
-
|
|
88
|
-
|
|
68
|
+
const entry = this.hits.get(key);
|
|
69
|
+
if (entry) {
|
|
70
|
+
clearTimeout(entry.timeoutId);
|
|
71
|
+
this.hits.delete(key);
|
|
72
|
+
}
|
|
73
|
+
const ban = this.bans.get(key);
|
|
74
|
+
if (ban) {
|
|
75
|
+
clearTimeout(ban.timeoutId);
|
|
76
|
+
this.bans.delete(key);
|
|
77
|
+
}
|
|
78
|
+
const violation = this.violations.get(key);
|
|
79
|
+
if (violation) {
|
|
80
|
+
clearTimeout(violation.timeoutId);
|
|
81
|
+
this.violations.delete(key);
|
|
82
|
+
}
|
|
89
83
|
}
|
|
90
84
|
shutdown() {
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
for (const [, entry] of this.hits) {
|
|
86
|
+
clearTimeout(entry.timeoutId);
|
|
87
|
+
}
|
|
88
|
+
this.hits.clear();
|
|
89
|
+
for (const [, entry] of this.bans) {
|
|
90
|
+
clearTimeout(entry.timeoutId);
|
|
91
|
+
}
|
|
92
|
+
this.bans.clear();
|
|
93
|
+
for (const [, entry] of this.violations) {
|
|
94
|
+
clearTimeout(entry.timeoutId);
|
|
95
|
+
}
|
|
96
|
+
this.violations.clear();
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
|
-
function
|
|
96
|
-
return new
|
|
99
|
+
function memoryStore() {
|
|
100
|
+
return new MemoryStore;
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
// src/core/utils.ts
|
|
@@ -283,7 +287,10 @@ function hitlimit(options, handler) {
|
|
|
283
287
|
const defaultKey = (req) => {
|
|
284
288
|
return activeServer?.requestIP(req)?.address || "unknown";
|
|
285
289
|
};
|
|
286
|
-
|
|
290
|
+
if (options.sqlitePath && !options.store) {
|
|
291
|
+
console.warn("[hitlimit-bun] DEPRECATION WARNING: " + "sqlitePath is deprecated and will be ignored. " + "Use store: sqliteStore({ path }) instead. " + "See migration guide: https://hitlimit.jointops.dev/docs/migration/v1.1.0");
|
|
292
|
+
}
|
|
293
|
+
const store = options.store ?? memoryStore();
|
|
287
294
|
const config = resolveConfig(options, store, options.key ?? defaultKey);
|
|
288
295
|
const hasSkip = !!config.skip;
|
|
289
296
|
const hasTiers = !!(config.tier && config.tiers);
|
|
@@ -297,6 +304,80 @@ function hitlimit(options, handler) {
|
|
|
297
304
|
const response = config.response;
|
|
298
305
|
const customKey = options.key;
|
|
299
306
|
const blockedBody = JSON.stringify(response);
|
|
307
|
+
const isSyncStore = store.isSync === true;
|
|
308
|
+
const isSyncKey = !customKey;
|
|
309
|
+
if (!hasSkip && !hasTiers && !hasBan && !hasGroup && isSyncStore && isSyncKey) {
|
|
310
|
+
return (req, server) => {
|
|
311
|
+
const ip = server.requestIP(req)?.address || "unknown";
|
|
312
|
+
const result = store.hit(ip, windowMs, limit);
|
|
313
|
+
const allowed = result.count <= limit;
|
|
314
|
+
if (!allowed) {
|
|
315
|
+
const headers = { "Content-Type": "application/json" };
|
|
316
|
+
const resetIn = Math.ceil((result.resetAt - Date.now()) / 1000);
|
|
317
|
+
if (standardHeaders) {
|
|
318
|
+
headers["RateLimit-Limit"] = String(limit);
|
|
319
|
+
headers["RateLimit-Remaining"] = "0";
|
|
320
|
+
headers["RateLimit-Reset"] = String(resetIn);
|
|
321
|
+
}
|
|
322
|
+
if (legacyHeaders) {
|
|
323
|
+
headers["X-RateLimit-Limit"] = String(limit);
|
|
324
|
+
headers["X-RateLimit-Remaining"] = "0";
|
|
325
|
+
headers["X-RateLimit-Reset"] = String(Math.ceil(result.resetAt / 1000));
|
|
326
|
+
}
|
|
327
|
+
if (retryAfterHeader) {
|
|
328
|
+
headers["Retry-After"] = String(resetIn);
|
|
329
|
+
}
|
|
330
|
+
return new Response(blockedBody, { status: 429, headers });
|
|
331
|
+
}
|
|
332
|
+
const res = handler(req, server);
|
|
333
|
+
if (res instanceof Promise) {
|
|
334
|
+
return res.then((response2) => {
|
|
335
|
+
if (standardHeaders || legacyHeaders) {
|
|
336
|
+
const resetIn = Math.ceil((result.resetAt - Date.now()) / 1000);
|
|
337
|
+
const remaining = Math.max(0, limit - result.count);
|
|
338
|
+
const newHeaders = new Headers(response2.headers);
|
|
339
|
+
if (standardHeaders) {
|
|
340
|
+
newHeaders.set("RateLimit-Limit", String(limit));
|
|
341
|
+
newHeaders.set("RateLimit-Remaining", String(remaining));
|
|
342
|
+
newHeaders.set("RateLimit-Reset", String(resetIn));
|
|
343
|
+
}
|
|
344
|
+
if (legacyHeaders) {
|
|
345
|
+
newHeaders.set("X-RateLimit-Limit", String(limit));
|
|
346
|
+
newHeaders.set("X-RateLimit-Remaining", String(remaining));
|
|
347
|
+
newHeaders.set("X-RateLimit-Reset", String(Math.ceil(result.resetAt / 1000)));
|
|
348
|
+
}
|
|
349
|
+
return new Response(response2.body, {
|
|
350
|
+
status: response2.status,
|
|
351
|
+
statusText: response2.statusText,
|
|
352
|
+
headers: newHeaders
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return response2;
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
if (standardHeaders || legacyHeaders) {
|
|
359
|
+
const resetIn = Math.ceil((result.resetAt - Date.now()) / 1000);
|
|
360
|
+
const remaining = Math.max(0, limit - result.count);
|
|
361
|
+
const newHeaders = new Headers(res.headers);
|
|
362
|
+
if (standardHeaders) {
|
|
363
|
+
newHeaders.set("RateLimit-Limit", String(limit));
|
|
364
|
+
newHeaders.set("RateLimit-Remaining", String(remaining));
|
|
365
|
+
newHeaders.set("RateLimit-Reset", String(resetIn));
|
|
366
|
+
}
|
|
367
|
+
if (legacyHeaders) {
|
|
368
|
+
newHeaders.set("X-RateLimit-Limit", String(limit));
|
|
369
|
+
newHeaders.set("X-RateLimit-Remaining", String(remaining));
|
|
370
|
+
newHeaders.set("X-RateLimit-Reset", String(Math.ceil(result.resetAt / 1000)));
|
|
371
|
+
}
|
|
372
|
+
return new Response(res.body, {
|
|
373
|
+
status: res.status,
|
|
374
|
+
statusText: res.statusText,
|
|
375
|
+
headers: newHeaders
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return res;
|
|
379
|
+
};
|
|
380
|
+
}
|
|
300
381
|
if (!hasSkip && !hasTiers && !hasBan && !hasGroup) {
|
|
301
382
|
return async (req, server) => {
|
|
302
383
|
try {
|
|
@@ -402,7 +483,10 @@ function createHitLimit(options = {}) {
|
|
|
402
483
|
const defaultKey = (req) => {
|
|
403
484
|
return activeServer?.requestIP(req)?.address || "unknown";
|
|
404
485
|
};
|
|
405
|
-
|
|
486
|
+
if (options.sqlitePath && !options.store) {
|
|
487
|
+
console.warn("[hitlimit-bun] DEPRECATION WARNING: " + "sqlitePath is deprecated and will be ignored. " + "Use store: sqliteStore({ path }) instead. " + "See migration guide: https://hitlimit.jointops.dev/docs/migration/v1.1.0");
|
|
488
|
+
}
|
|
489
|
+
const store = options.store ?? memoryStore();
|
|
406
490
|
const config = resolveConfig(options, store, options.key ?? defaultKey);
|
|
407
491
|
return {
|
|
408
492
|
async check(req, server) {
|
|
@@ -439,7 +523,7 @@ function createHitLimit(options = {}) {
|
|
|
439
523
|
};
|
|
440
524
|
}
|
|
441
525
|
export {
|
|
442
|
-
|
|
526
|
+
memoryStore,
|
|
443
527
|
hitlimit,
|
|
444
528
|
createHitLimit,
|
|
445
529
|
checkLimit,
|
|
@@ -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;AAiJ3E,wBAAgB,WAAW,IAAI,aAAa,CAE3C"}
|
package/dist/stores/memory.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/stores/sqlite.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAA;AAE3E,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":"AACA,OAAO,KAAK,EAAE,aAAa,EAAe,MAAM,2BAA2B,CAAA;AAE3E,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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joint-ops/hitlimit-bun",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Ultra-fast Bun-native rate limiting - Memory-first with 6M+ ops/sec for Bun.serve, Elysia & Hono",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Shayan M Hussain",
|
|
7
7
|
"email": "shayanhussain48@gmail.com",
|
|
@@ -19,10 +19,16 @@
|
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
21
|
"rate-limit",
|
|
22
|
-
"rate-limiter",
|
|
23
|
-
"rate-limiting",
|
|
24
|
-
"ratelimit",
|
|
25
22
|
"bun",
|
|
23
|
+
"rate-limiter",
|
|
24
|
+
"memory",
|
|
25
|
+
"fast",
|
|
26
|
+
"elysia",
|
|
27
|
+
"bun.serve",
|
|
28
|
+
"middleware",
|
|
29
|
+
"sqlite",
|
|
30
|
+
"redis",
|
|
31
|
+
"hono",
|
|
26
32
|
"bun-rate-limit",
|
|
27
33
|
"bun-sqlite",
|
|
28
34
|
"bun-middleware",
|
|
@@ -30,15 +36,12 @@
|
|
|
30
36
|
"bun-server",
|
|
31
37
|
"bun-native",
|
|
32
38
|
"bun-http",
|
|
33
|
-
"elysia",
|
|
34
39
|
"elysia-plugin",
|
|
35
40
|
"elysia-rate-limit",
|
|
36
41
|
"elysia-middleware",
|
|
37
42
|
"elysia-throttle",
|
|
38
|
-
"hono",
|
|
39
43
|
"hono-rate-limit",
|
|
40
44
|
"hono-bun",
|
|
41
|
-
"middleware",
|
|
42
45
|
"throttle",
|
|
43
46
|
"throttling",
|
|
44
47
|
"api",
|
|
@@ -50,7 +53,6 @@
|
|
|
50
53
|
"brute-force",
|
|
51
54
|
"brute-force-protection",
|
|
52
55
|
"login-protection",
|
|
53
|
-
"redis",
|
|
54
56
|
"redis-rate-limit",
|
|
55
57
|
"typescript",
|
|
56
58
|
"high-performance",
|
|
@@ -64,7 +66,6 @@
|
|
|
64
66
|
"http-rate-limit",
|
|
65
67
|
"bun-api",
|
|
66
68
|
"bun-framework",
|
|
67
|
-
"fast",
|
|
68
69
|
"lightweight",
|
|
69
70
|
"zero-dependency",
|
|
70
71
|
"esm"
|
|
@@ -84,6 +85,11 @@
|
|
|
84
85
|
"bun": "./dist/elysia.js",
|
|
85
86
|
"import": "./dist/elysia.js"
|
|
86
87
|
},
|
|
88
|
+
"./hono": {
|
|
89
|
+
"types": "./dist/hono.d.ts",
|
|
90
|
+
"bun": "./dist/hono.js",
|
|
91
|
+
"import": "./dist/hono.js"
|
|
92
|
+
},
|
|
87
93
|
"./stores/memory": {
|
|
88
94
|
"types": "./dist/stores/memory.d.ts",
|
|
89
95
|
"bun": "./dist/stores/memory.js",
|
|
@@ -105,22 +111,26 @@
|
|
|
105
111
|
],
|
|
106
112
|
"sideEffects": false,
|
|
107
113
|
"scripts": {
|
|
108
|
-
"build": "bun build ./src/index.ts ./src/elysia.ts ./src/stores/memory.ts ./src/stores/redis.ts ./src/stores/sqlite.ts --outdir=./dist --target=bun --external=elysia --external=ioredis --external=@sinclair/typebox && tsc --emitDeclarationOnly",
|
|
114
|
+
"build": "bun build ./src/index.ts ./src/elysia.ts ./src/hono.ts ./src/stores/memory.ts ./src/stores/redis.ts ./src/stores/sqlite.ts --outdir=./dist --target=bun --external=elysia --external=hono --external=ioredis --external=@sinclair/typebox && tsc --emitDeclarationOnly",
|
|
109
115
|
"clean": "rm -rf dist",
|
|
110
116
|
"test": "bun test",
|
|
111
117
|
"test:watch": "bun test --watch"
|
|
112
118
|
},
|
|
113
119
|
"dependencies": {
|
|
114
|
-
"@joint-ops/hitlimit-types": "1.
|
|
120
|
+
"@joint-ops/hitlimit-types": "1.1.1"
|
|
115
121
|
},
|
|
116
122
|
"peerDependencies": {
|
|
117
123
|
"elysia": ">=1.0.0",
|
|
124
|
+
"hono": ">=4.0.0",
|
|
118
125
|
"ioredis": ">=5.0.0"
|
|
119
126
|
},
|
|
120
127
|
"peerDependenciesMeta": {
|
|
121
128
|
"elysia": {
|
|
122
129
|
"optional": true
|
|
123
130
|
},
|
|
131
|
+
"hono": {
|
|
132
|
+
"optional": true
|
|
133
|
+
},
|
|
124
134
|
"ioredis": {
|
|
125
135
|
"optional": true
|
|
126
136
|
}
|
|
@@ -129,6 +139,7 @@
|
|
|
129
139
|
"@sinclair/typebox": "^0.34.48",
|
|
130
140
|
"@types/bun": "latest",
|
|
131
141
|
"elysia": "^1.0.0",
|
|
142
|
+
"hono": "^4.11.9",
|
|
132
143
|
"ioredis": "^5.3.0",
|
|
133
144
|
"typescript": "^5.3.0"
|
|
134
145
|
}
|