@joint-ops/hitlimit-bun 1.1.0 → 1.1.2
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 +21 -30
- package/dist/elysia.d.ts.map +1 -1
- package/dist/elysia.js +105 -61
- package/dist/hono.d.ts.map +1 -1
- package/dist/hono.js +94 -54
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +117 -54
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +43 -54
- package/dist/stores/sqlite.d.ts.map +1 -1
- package/dist/stores/sqlite.js +1 -0
- package/package.json +2 -2
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 - 5M+ ops/sec under real-world load | Elysia, Hono & Bun.serve
|
|
10
10
|
|
|
11
|
-
**hitlimit-bun** is a blazing-fast, Bun-native rate limiting library for Bun.serve, Elysia, and Hono applications. **Memory-first by default** with
|
|
11
|
+
**hitlimit-bun** is a blazing-fast, Bun-native rate limiting library for Bun.serve, Elysia, and Hono applications. **Memory-first by default** with 5.62M ops/sec under real-world load (~15x 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
|
-
**Memory-first for maximum performance.**
|
|
17
|
+
**Memory-first for maximum performance.** ~15x faster than SQLite.
|
|
18
18
|
|
|
19
19
|
```
|
|
20
20
|
┌─────────────────────────────────────────────────────────────────┐
|
|
21
21
|
│ │
|
|
22
|
-
│ Memory (v1.1+) ██████████████████████████████
|
|
23
|
-
│ SQLite (v1.0) █░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
22
|
+
│ Memory (v1.1+) ██████████████████████████████ 5.62M ops/s │
|
|
23
|
+
│ SQLite (v1.0) █░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 383K ops/s │
|
|
24
24
|
│ │
|
|
25
|
-
│
|
|
25
|
+
│ ~15x performance improvement with memory default (10K IPs) │
|
|
26
26
|
│ │
|
|
27
27
|
└─────────────────────────────────────────────────────────────────┘
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
- **🚀 Memory-First** -
|
|
30
|
+
- **🚀 Memory-First** - 5.62M ops/sec under real-world load (v1.1+), ~15x faster than SQLite
|
|
31
31
|
- **Bun Native** - Built specifically for Bun's runtime, not a Node.js port
|
|
32
32
|
- **Zero Config** - Works out of the box with sensible defaults
|
|
33
33
|
- **Framework Support** - First-class Elysia and Hono integration
|
|
34
|
-
- **Optional Persistence** - SQLite (
|
|
34
|
+
- **Optional Persistence** - SQLite (383K 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
|
|
|
@@ -249,7 +249,7 @@ hitlimit({
|
|
|
249
249
|
retryAfter: true // Retry-After header on 429
|
|
250
250
|
},
|
|
251
251
|
|
|
252
|
-
// Store (default:
|
|
252
|
+
// Store (default: memory)
|
|
253
253
|
store: sqliteStore({ path: './ratelimit.db' }),
|
|
254
254
|
|
|
255
255
|
// Skip rate limiting
|
|
@@ -274,39 +274,30 @@ hitlimit({
|
|
|
274
274
|
|
|
275
275
|
## Storage Backends
|
|
276
276
|
|
|
277
|
-
###
|
|
277
|
+
### Memory Store (Default)
|
|
278
278
|
|
|
279
|
-
|
|
279
|
+
Fastest option, used by default. No persistence.
|
|
280
280
|
|
|
281
281
|
```typescript
|
|
282
282
|
import { hitlimit } from '@joint-ops/hitlimit-bun'
|
|
283
283
|
|
|
284
|
-
// Default - uses
|
|
284
|
+
// Default - uses memory store (no config needed)
|
|
285
285
|
Bun.serve({
|
|
286
286
|
fetch: hitlimit({}, handler)
|
|
287
287
|
})
|
|
288
|
-
|
|
289
|
-
// Custom path for persistence
|
|
290
|
-
import { sqliteStore } from '@joint-ops/hitlimit-bun'
|
|
291
|
-
|
|
292
|
-
Bun.serve({
|
|
293
|
-
fetch: hitlimit({
|
|
294
|
-
store: sqliteStore({ path: './ratelimit.db' })
|
|
295
|
-
}, handler)
|
|
296
|
-
})
|
|
297
288
|
```
|
|
298
289
|
|
|
299
|
-
###
|
|
290
|
+
### SQLite Store
|
|
300
291
|
|
|
301
|
-
|
|
292
|
+
Uses Bun's native bun:sqlite for persistent rate limiting.
|
|
302
293
|
|
|
303
294
|
```typescript
|
|
304
295
|
import { hitlimit } from '@joint-ops/hitlimit-bun'
|
|
305
|
-
import {
|
|
296
|
+
import { sqliteStore } from '@joint-ops/hitlimit-bun'
|
|
306
297
|
|
|
307
298
|
Bun.serve({
|
|
308
299
|
fetch: hitlimit({
|
|
309
|
-
store:
|
|
300
|
+
store: sqliteStore({ path: './ratelimit.db' })
|
|
310
301
|
}, handler)
|
|
311
302
|
})
|
|
312
303
|
```
|
|
@@ -365,9 +356,9 @@ hitlimit-bun is optimized for Bun's runtime with native performance:
|
|
|
365
356
|
|
|
366
357
|
| Store | Operations/sec | vs Node.js |
|
|
367
358
|
|-------|----------------|------------|
|
|
368
|
-
| **Memory** |
|
|
369
|
-
| **bun:sqlite** |
|
|
370
|
-
| **Redis** | 6,
|
|
359
|
+
| **Memory** | 5,620,000+ | +68% faster |
|
|
360
|
+
| **bun:sqlite** | 383,000+ | ~same |
|
|
361
|
+
| **Redis** | 6,600+ | ~same |
|
|
371
362
|
|
|
372
363
|
### HTTP Throughput
|
|
373
364
|
|
|
@@ -437,7 +428,7 @@ new Elysia()
|
|
|
437
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.
|
|
438
429
|
|
|
439
430
|
**hitlimit-bun** is built specifically for Bun:
|
|
440
|
-
- Uses native `bun:sqlite` (
|
|
431
|
+
- Uses native `bun:sqlite` (no N-API overhead)
|
|
441
432
|
- No FFI overhead or Node.js polyfills
|
|
442
433
|
- First-class Elysia framework support
|
|
443
434
|
- Optimized for Bun.serve's request handling
|
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,49 +1,66 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/stores/memory.ts
|
|
3
3
|
class MemoryStore {
|
|
4
|
+
isSync = true;
|
|
4
5
|
hits = new Map;
|
|
5
6
|
bans = new Map;
|
|
6
7
|
violations = new Map;
|
|
8
|
+
_result = { count: 0, resetAt: 0 };
|
|
9
|
+
sweepInterval;
|
|
10
|
+
constructor() {
|
|
11
|
+
this.sweepInterval = setInterval(() => {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
for (const [key, entry] of this.hits) {
|
|
14
|
+
if (now >= entry.resetAt)
|
|
15
|
+
this.hits.delete(key);
|
|
16
|
+
}
|
|
17
|
+
for (const [key, ban] of this.bans) {
|
|
18
|
+
if (now >= ban.expiresAt)
|
|
19
|
+
this.bans.delete(key);
|
|
20
|
+
}
|
|
21
|
+
for (const [key, violation] of this.violations) {
|
|
22
|
+
if (now >= violation.resetAt)
|
|
23
|
+
this.violations.delete(key);
|
|
24
|
+
}
|
|
25
|
+
}, 1e4);
|
|
26
|
+
if (typeof this.sweepInterval.unref === "function") {
|
|
27
|
+
this.sweepInterval.unref();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
7
30
|
hit(key, windowMs, _limit) {
|
|
8
31
|
const entry = this.hits.get(key);
|
|
9
32
|
if (entry !== undefined) {
|
|
10
|
-
|
|
11
|
-
|
|
33
|
+
const now2 = Date.now();
|
|
34
|
+
if (now2 >= entry.resetAt) {
|
|
35
|
+
entry.count = 1;
|
|
36
|
+
entry.resetAt = now2 + windowMs;
|
|
37
|
+
} else {
|
|
38
|
+
entry.count++;
|
|
39
|
+
}
|
|
40
|
+
this._result.count = entry.count;
|
|
41
|
+
this._result.resetAt = entry.resetAt;
|
|
42
|
+
return this._result;
|
|
12
43
|
}
|
|
13
44
|
const now = Date.now();
|
|
14
45
|
const resetAt = now + windowMs;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
timeoutId.unref();
|
|
20
|
-
}
|
|
21
|
-
this.hits.set(key, { count: 1, resetAt, timeoutId });
|
|
22
|
-
return { count: 1, resetAt };
|
|
46
|
+
this.hits.set(key, { count: 1, resetAt });
|
|
47
|
+
this._result.count = 1;
|
|
48
|
+
this._result.resetAt = resetAt;
|
|
49
|
+
return this._result;
|
|
23
50
|
}
|
|
24
51
|
isBanned(key) {
|
|
25
52
|
const ban = this.bans.get(key);
|
|
26
53
|
if (!ban)
|
|
27
54
|
return false;
|
|
28
55
|
if (Date.now() >= ban.expiresAt) {
|
|
29
|
-
clearTimeout(ban.timeoutId);
|
|
30
56
|
this.bans.delete(key);
|
|
31
57
|
return false;
|
|
32
58
|
}
|
|
33
59
|
return true;
|
|
34
60
|
}
|
|
35
61
|
ban(key, durationMs) {
|
|
36
|
-
const existing = this.bans.get(key);
|
|
37
|
-
if (existing)
|
|
38
|
-
clearTimeout(existing.timeoutId);
|
|
39
62
|
const expiresAt = Date.now() + durationMs;
|
|
40
|
-
|
|
41
|
-
this.bans.delete(key);
|
|
42
|
-
}, durationMs);
|
|
43
|
-
if (typeof timeoutId.unref === "function") {
|
|
44
|
-
timeoutId.unref();
|
|
45
|
-
}
|
|
46
|
-
this.bans.set(key, { expiresAt, timeoutId });
|
|
63
|
+
this.bans.set(key, { expiresAt });
|
|
47
64
|
}
|
|
48
65
|
recordViolation(key, windowMs) {
|
|
49
66
|
const entry = this.violations.get(key);
|
|
@@ -51,47 +68,19 @@ class MemoryStore {
|
|
|
51
68
|
entry.count++;
|
|
52
69
|
return entry.count;
|
|
53
70
|
}
|
|
54
|
-
if (entry)
|
|
55
|
-
clearTimeout(entry.timeoutId);
|
|
56
71
|
const resetAt = Date.now() + windowMs;
|
|
57
|
-
|
|
58
|
-
this.violations.delete(key);
|
|
59
|
-
}, windowMs);
|
|
60
|
-
if (typeof timeoutId.unref === "function") {
|
|
61
|
-
timeoutId.unref();
|
|
62
|
-
}
|
|
63
|
-
this.violations.set(key, { count: 1, resetAt, timeoutId });
|
|
72
|
+
this.violations.set(key, { count: 1, resetAt });
|
|
64
73
|
return 1;
|
|
65
74
|
}
|
|
66
75
|
reset(key) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.hits.delete(key);
|
|
71
|
-
}
|
|
72
|
-
const ban = this.bans.get(key);
|
|
73
|
-
if (ban) {
|
|
74
|
-
clearTimeout(ban.timeoutId);
|
|
75
|
-
this.bans.delete(key);
|
|
76
|
-
}
|
|
77
|
-
const violation = this.violations.get(key);
|
|
78
|
-
if (violation) {
|
|
79
|
-
clearTimeout(violation.timeoutId);
|
|
80
|
-
this.violations.delete(key);
|
|
81
|
-
}
|
|
76
|
+
this.hits.delete(key);
|
|
77
|
+
this.bans.delete(key);
|
|
78
|
+
this.violations.delete(key);
|
|
82
79
|
}
|
|
83
80
|
shutdown() {
|
|
84
|
-
|
|
85
|
-
clearTimeout(entry.timeoutId);
|
|
86
|
-
}
|
|
81
|
+
clearInterval(this.sweepInterval);
|
|
87
82
|
this.hits.clear();
|
|
88
|
-
for (const [, entry] of this.bans) {
|
|
89
|
-
clearTimeout(entry.timeoutId);
|
|
90
|
-
}
|
|
91
83
|
this.bans.clear();
|
|
92
|
-
for (const [, entry] of this.violations) {
|
|
93
|
-
clearTimeout(entry.timeoutId);
|
|
94
|
-
}
|
|
95
84
|
this.violations.clear();
|
|
96
85
|
}
|
|
97
86
|
}
|
|
@@ -280,6 +269,12 @@ var instanceCounter = 0;
|
|
|
280
269
|
function getDefaultKey(_ctx) {
|
|
281
270
|
return "unknown";
|
|
282
271
|
}
|
|
272
|
+
function buildResponseBody(response, info) {
|
|
273
|
+
if (typeof response === "function") {
|
|
274
|
+
return response(info);
|
|
275
|
+
}
|
|
276
|
+
return { ...response, limit: info.limit, remaining: info.remaining, resetIn: info.resetIn };
|
|
277
|
+
}
|
|
283
278
|
function hitlimit(options = {}) {
|
|
284
279
|
const pluginName = options.name ?? `hitlimit-${instanceCounter++}`;
|
|
285
280
|
if (options.sqlitePath && !options.store) {
|
|
@@ -287,6 +282,56 @@ function hitlimit(options = {}) {
|
|
|
287
282
|
}
|
|
288
283
|
const store = options.store ?? memoryStore();
|
|
289
284
|
const config = resolveConfig(options, store, getDefaultKey);
|
|
285
|
+
const hasSkip = !!config.skip;
|
|
286
|
+
const hasTiers = !!(config.tier && config.tiers);
|
|
287
|
+
const hasBan = !!config.ban;
|
|
288
|
+
const hasGroup = !!config.group;
|
|
289
|
+
const standardHeaders = config.headers.standard;
|
|
290
|
+
const legacyHeaders = config.headers.legacy;
|
|
291
|
+
const retryAfterHeader = config.headers.retryAfter;
|
|
292
|
+
const limit = config.limit;
|
|
293
|
+
const windowMs = config.windowMs;
|
|
294
|
+
const responseConfig = config.response;
|
|
295
|
+
const isSyncStore = store.isSync === true;
|
|
296
|
+
const isSyncKey = !options.key;
|
|
297
|
+
if (!hasSkip && !hasTiers && !hasBan && !hasGroup && isSyncStore && isSyncKey) {
|
|
298
|
+
return new Elysia({ name: pluginName }).onBeforeHandle({ as: "scoped" }, ({ set }) => {
|
|
299
|
+
const key = "unknown";
|
|
300
|
+
const result = store.hit(key, windowMs, limit);
|
|
301
|
+
const allowed = result.count <= limit;
|
|
302
|
+
const remaining = Math.max(0, limit - result.count);
|
|
303
|
+
const resetIn = Math.ceil((result.resetAt - Date.now()) / 1000);
|
|
304
|
+
if (standardHeaders) {
|
|
305
|
+
set.headers["RateLimit-Limit"] = String(limit);
|
|
306
|
+
set.headers["RateLimit-Remaining"] = String(remaining);
|
|
307
|
+
set.headers["RateLimit-Reset"] = String(resetIn);
|
|
308
|
+
}
|
|
309
|
+
if (legacyHeaders) {
|
|
310
|
+
set.headers["X-RateLimit-Limit"] = String(limit);
|
|
311
|
+
set.headers["X-RateLimit-Remaining"] = String(remaining);
|
|
312
|
+
set.headers["X-RateLimit-Reset"] = String(Math.ceil(result.resetAt / 1000));
|
|
313
|
+
}
|
|
314
|
+
if (!allowed) {
|
|
315
|
+
if (retryAfterHeader) {
|
|
316
|
+
set.headers["Retry-After"] = String(resetIn);
|
|
317
|
+
}
|
|
318
|
+
const body = buildResponseBody(responseConfig, {
|
|
319
|
+
limit,
|
|
320
|
+
remaining: 0,
|
|
321
|
+
resetIn,
|
|
322
|
+
resetAt: result.resetAt,
|
|
323
|
+
key
|
|
324
|
+
});
|
|
325
|
+
set.status = 429;
|
|
326
|
+
const headers = { "Content-Type": "application/json" };
|
|
327
|
+
for (const [k, v] of Object.entries(set.headers)) {
|
|
328
|
+
if (v != null)
|
|
329
|
+
headers[k] = String(v);
|
|
330
|
+
}
|
|
331
|
+
return new Response(JSON.stringify(body), { status: 429, headers });
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
290
335
|
return new Elysia({ name: pluginName }).onBeforeHandle({ as: "scoped" }, async ({ request, set }) => {
|
|
291
336
|
const ctx = { request };
|
|
292
337
|
if (config.skip) {
|
|
@@ -302,13 +347,12 @@ function hitlimit(options = {}) {
|
|
|
302
347
|
});
|
|
303
348
|
if (!result.allowed) {
|
|
304
349
|
set.status = 429;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
});
|
|
350
|
+
const headers = { "Content-Type": "application/json" };
|
|
351
|
+
for (const [k, v] of Object.entries(result.headers)) {
|
|
352
|
+
if (v != null)
|
|
353
|
+
headers[k] = String(v);
|
|
354
|
+
}
|
|
355
|
+
return new Response(JSON.stringify(result.body), { status: 429, headers });
|
|
312
356
|
}
|
|
313
357
|
} catch (error) {
|
|
314
358
|
const action = await config.onStoreError(error, ctx);
|
package/dist/hono.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
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
CHANGED
|
@@ -1,49 +1,66 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/stores/memory.ts
|
|
3
3
|
class MemoryStore {
|
|
4
|
+
isSync = true;
|
|
4
5
|
hits = new Map;
|
|
5
6
|
bans = new Map;
|
|
6
7
|
violations = new Map;
|
|
8
|
+
_result = { count: 0, resetAt: 0 };
|
|
9
|
+
sweepInterval;
|
|
10
|
+
constructor() {
|
|
11
|
+
this.sweepInterval = setInterval(() => {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
for (const [key, entry] of this.hits) {
|
|
14
|
+
if (now >= entry.resetAt)
|
|
15
|
+
this.hits.delete(key);
|
|
16
|
+
}
|
|
17
|
+
for (const [key, ban] of this.bans) {
|
|
18
|
+
if (now >= ban.expiresAt)
|
|
19
|
+
this.bans.delete(key);
|
|
20
|
+
}
|
|
21
|
+
for (const [key, violation] of this.violations) {
|
|
22
|
+
if (now >= violation.resetAt)
|
|
23
|
+
this.violations.delete(key);
|
|
24
|
+
}
|
|
25
|
+
}, 1e4);
|
|
26
|
+
if (typeof this.sweepInterval.unref === "function") {
|
|
27
|
+
this.sweepInterval.unref();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
7
30
|
hit(key, windowMs, _limit) {
|
|
8
31
|
const entry = this.hits.get(key);
|
|
9
32
|
if (entry !== undefined) {
|
|
10
|
-
|
|
11
|
-
|
|
33
|
+
const now2 = Date.now();
|
|
34
|
+
if (now2 >= entry.resetAt) {
|
|
35
|
+
entry.count = 1;
|
|
36
|
+
entry.resetAt = now2 + windowMs;
|
|
37
|
+
} else {
|
|
38
|
+
entry.count++;
|
|
39
|
+
}
|
|
40
|
+
this._result.count = entry.count;
|
|
41
|
+
this._result.resetAt = entry.resetAt;
|
|
42
|
+
return this._result;
|
|
12
43
|
}
|
|
13
44
|
const now = Date.now();
|
|
14
45
|
const resetAt = now + windowMs;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
timeoutId.unref();
|
|
20
|
-
}
|
|
21
|
-
this.hits.set(key, { count: 1, resetAt, timeoutId });
|
|
22
|
-
return { count: 1, resetAt };
|
|
46
|
+
this.hits.set(key, { count: 1, resetAt });
|
|
47
|
+
this._result.count = 1;
|
|
48
|
+
this._result.resetAt = resetAt;
|
|
49
|
+
return this._result;
|
|
23
50
|
}
|
|
24
51
|
isBanned(key) {
|
|
25
52
|
const ban = this.bans.get(key);
|
|
26
53
|
if (!ban)
|
|
27
54
|
return false;
|
|
28
55
|
if (Date.now() >= ban.expiresAt) {
|
|
29
|
-
clearTimeout(ban.timeoutId);
|
|
30
56
|
this.bans.delete(key);
|
|
31
57
|
return false;
|
|
32
58
|
}
|
|
33
59
|
return true;
|
|
34
60
|
}
|
|
35
61
|
ban(key, durationMs) {
|
|
36
|
-
const existing = this.bans.get(key);
|
|
37
|
-
if (existing)
|
|
38
|
-
clearTimeout(existing.timeoutId);
|
|
39
62
|
const expiresAt = Date.now() + durationMs;
|
|
40
|
-
|
|
41
|
-
this.bans.delete(key);
|
|
42
|
-
}, durationMs);
|
|
43
|
-
if (typeof timeoutId.unref === "function") {
|
|
44
|
-
timeoutId.unref();
|
|
45
|
-
}
|
|
46
|
-
this.bans.set(key, { expiresAt, timeoutId });
|
|
63
|
+
this.bans.set(key, { expiresAt });
|
|
47
64
|
}
|
|
48
65
|
recordViolation(key, windowMs) {
|
|
49
66
|
const entry = this.violations.get(key);
|
|
@@ -51,47 +68,19 @@ class MemoryStore {
|
|
|
51
68
|
entry.count++;
|
|
52
69
|
return entry.count;
|
|
53
70
|
}
|
|
54
|
-
if (entry)
|
|
55
|
-
clearTimeout(entry.timeoutId);
|
|
56
71
|
const resetAt = Date.now() + windowMs;
|
|
57
|
-
|
|
58
|
-
this.violations.delete(key);
|
|
59
|
-
}, windowMs);
|
|
60
|
-
if (typeof timeoutId.unref === "function") {
|
|
61
|
-
timeoutId.unref();
|
|
62
|
-
}
|
|
63
|
-
this.violations.set(key, { count: 1, resetAt, timeoutId });
|
|
72
|
+
this.violations.set(key, { count: 1, resetAt });
|
|
64
73
|
return 1;
|
|
65
74
|
}
|
|
66
75
|
reset(key) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.hits.delete(key);
|
|
71
|
-
}
|
|
72
|
-
const ban = this.bans.get(key);
|
|
73
|
-
if (ban) {
|
|
74
|
-
clearTimeout(ban.timeoutId);
|
|
75
|
-
this.bans.delete(key);
|
|
76
|
-
}
|
|
77
|
-
const violation = this.violations.get(key);
|
|
78
|
-
if (violation) {
|
|
79
|
-
clearTimeout(violation.timeoutId);
|
|
80
|
-
this.violations.delete(key);
|
|
81
|
-
}
|
|
76
|
+
this.hits.delete(key);
|
|
77
|
+
this.bans.delete(key);
|
|
78
|
+
this.violations.delete(key);
|
|
82
79
|
}
|
|
83
80
|
shutdown() {
|
|
84
|
-
|
|
85
|
-
clearTimeout(entry.timeoutId);
|
|
86
|
-
}
|
|
81
|
+
clearInterval(this.sweepInterval);
|
|
87
82
|
this.hits.clear();
|
|
88
|
-
for (const [, entry] of this.bans) {
|
|
89
|
-
clearTimeout(entry.timeoutId);
|
|
90
|
-
}
|
|
91
83
|
this.bans.clear();
|
|
92
|
-
for (const [, entry] of this.violations) {
|
|
93
|
-
clearTimeout(entry.timeoutId);
|
|
94
|
-
}
|
|
95
84
|
this.violations.clear();
|
|
96
85
|
}
|
|
97
86
|
}
|
|
@@ -279,9 +268,60 @@ async function checkLimit(config, req) {
|
|
|
279
268
|
function getDefaultKey(c) {
|
|
280
269
|
return c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || c.req.header("x-real-ip") || "unknown";
|
|
281
270
|
}
|
|
271
|
+
function buildResponseBody(response, info) {
|
|
272
|
+
if (typeof response === "function") {
|
|
273
|
+
return response(info);
|
|
274
|
+
}
|
|
275
|
+
return { ...response, limit: info.limit, remaining: info.remaining, resetIn: info.resetIn };
|
|
276
|
+
}
|
|
282
277
|
function hitlimit(options = {}) {
|
|
283
278
|
const store = options.store ?? memoryStore();
|
|
284
279
|
const config = resolveConfig(options, store, getDefaultKey);
|
|
280
|
+
const hasSkip = !!config.skip;
|
|
281
|
+
const hasTiers = !!(config.tier && config.tiers);
|
|
282
|
+
const hasBan = !!config.ban;
|
|
283
|
+
const hasGroup = !!config.group;
|
|
284
|
+
const standardHeaders = config.headers.standard;
|
|
285
|
+
const legacyHeaders = config.headers.legacy;
|
|
286
|
+
const retryAfterHeader = config.headers.retryAfter;
|
|
287
|
+
const limit = config.limit;
|
|
288
|
+
const windowMs = config.windowMs;
|
|
289
|
+
const responseConfig = config.response;
|
|
290
|
+
const isSyncStore = store.isSync === true;
|
|
291
|
+
const isSyncKey = !options.key;
|
|
292
|
+
if (!hasSkip && !hasTiers && !hasBan && !hasGroup && isSyncStore && isSyncKey) {
|
|
293
|
+
return createMiddleware(async (c, next) => {
|
|
294
|
+
const key = c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || c.req.header("x-real-ip") || "unknown";
|
|
295
|
+
const result = store.hit(key, windowMs, limit);
|
|
296
|
+
const allowed = result.count <= limit;
|
|
297
|
+
const remaining = Math.max(0, limit - result.count);
|
|
298
|
+
const resetIn = Math.ceil((result.resetAt - Date.now()) / 1000);
|
|
299
|
+
if (standardHeaders) {
|
|
300
|
+
c.header("RateLimit-Limit", String(limit));
|
|
301
|
+
c.header("RateLimit-Remaining", String(remaining));
|
|
302
|
+
c.header("RateLimit-Reset", String(resetIn));
|
|
303
|
+
}
|
|
304
|
+
if (legacyHeaders) {
|
|
305
|
+
c.header("X-RateLimit-Limit", String(limit));
|
|
306
|
+
c.header("X-RateLimit-Remaining", String(remaining));
|
|
307
|
+
c.header("X-RateLimit-Reset", String(Math.ceil(result.resetAt / 1000)));
|
|
308
|
+
}
|
|
309
|
+
if (!allowed) {
|
|
310
|
+
if (retryAfterHeader) {
|
|
311
|
+
c.header("Retry-After", String(resetIn));
|
|
312
|
+
}
|
|
313
|
+
const body = buildResponseBody(responseConfig, {
|
|
314
|
+
limit,
|
|
315
|
+
remaining: 0,
|
|
316
|
+
resetIn,
|
|
317
|
+
resetAt: result.resetAt,
|
|
318
|
+
key
|
|
319
|
+
});
|
|
320
|
+
return c.json(body, 429);
|
|
321
|
+
}
|
|
322
|
+
await next();
|
|
323
|
+
});
|
|
324
|
+
}
|
|
285
325
|
return createMiddleware(async (c, next) => {
|
|
286
326
|
if (config.skip) {
|
|
287
327
|
const shouldSkip = await config.skip(c);
|
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;;;;;;;;;;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,
|
|
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,49 +1,66 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/stores/memory.ts
|
|
3
3
|
class MemoryStore {
|
|
4
|
+
isSync = true;
|
|
4
5
|
hits = new Map;
|
|
5
6
|
bans = new Map;
|
|
6
7
|
violations = new Map;
|
|
8
|
+
_result = { count: 0, resetAt: 0 };
|
|
9
|
+
sweepInterval;
|
|
10
|
+
constructor() {
|
|
11
|
+
this.sweepInterval = setInterval(() => {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
for (const [key, entry] of this.hits) {
|
|
14
|
+
if (now >= entry.resetAt)
|
|
15
|
+
this.hits.delete(key);
|
|
16
|
+
}
|
|
17
|
+
for (const [key, ban] of this.bans) {
|
|
18
|
+
if (now >= ban.expiresAt)
|
|
19
|
+
this.bans.delete(key);
|
|
20
|
+
}
|
|
21
|
+
for (const [key, violation] of this.violations) {
|
|
22
|
+
if (now >= violation.resetAt)
|
|
23
|
+
this.violations.delete(key);
|
|
24
|
+
}
|
|
25
|
+
}, 1e4);
|
|
26
|
+
if (typeof this.sweepInterval.unref === "function") {
|
|
27
|
+
this.sweepInterval.unref();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
7
30
|
hit(key, windowMs, _limit) {
|
|
8
31
|
const entry = this.hits.get(key);
|
|
9
32
|
if (entry !== undefined) {
|
|
10
|
-
|
|
11
|
-
|
|
33
|
+
const now2 = Date.now();
|
|
34
|
+
if (now2 >= entry.resetAt) {
|
|
35
|
+
entry.count = 1;
|
|
36
|
+
entry.resetAt = now2 + windowMs;
|
|
37
|
+
} else {
|
|
38
|
+
entry.count++;
|
|
39
|
+
}
|
|
40
|
+
this._result.count = entry.count;
|
|
41
|
+
this._result.resetAt = entry.resetAt;
|
|
42
|
+
return this._result;
|
|
12
43
|
}
|
|
13
44
|
const now = Date.now();
|
|
14
45
|
const resetAt = now + windowMs;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
timeoutId.unref();
|
|
20
|
-
}
|
|
21
|
-
this.hits.set(key, { count: 1, resetAt, timeoutId });
|
|
22
|
-
return { count: 1, resetAt };
|
|
46
|
+
this.hits.set(key, { count: 1, resetAt });
|
|
47
|
+
this._result.count = 1;
|
|
48
|
+
this._result.resetAt = resetAt;
|
|
49
|
+
return this._result;
|
|
23
50
|
}
|
|
24
51
|
isBanned(key) {
|
|
25
52
|
const ban = this.bans.get(key);
|
|
26
53
|
if (!ban)
|
|
27
54
|
return false;
|
|
28
55
|
if (Date.now() >= ban.expiresAt) {
|
|
29
|
-
clearTimeout(ban.timeoutId);
|
|
30
56
|
this.bans.delete(key);
|
|
31
57
|
return false;
|
|
32
58
|
}
|
|
33
59
|
return true;
|
|
34
60
|
}
|
|
35
61
|
ban(key, durationMs) {
|
|
36
|
-
const existing = this.bans.get(key);
|
|
37
|
-
if (existing)
|
|
38
|
-
clearTimeout(existing.timeoutId);
|
|
39
62
|
const expiresAt = Date.now() + durationMs;
|
|
40
|
-
|
|
41
|
-
this.bans.delete(key);
|
|
42
|
-
}, durationMs);
|
|
43
|
-
if (typeof timeoutId.unref === "function") {
|
|
44
|
-
timeoutId.unref();
|
|
45
|
-
}
|
|
46
|
-
this.bans.set(key, { expiresAt, timeoutId });
|
|
63
|
+
this.bans.set(key, { expiresAt });
|
|
47
64
|
}
|
|
48
65
|
recordViolation(key, windowMs) {
|
|
49
66
|
const entry = this.violations.get(key);
|
|
@@ -51,47 +68,19 @@ class MemoryStore {
|
|
|
51
68
|
entry.count++;
|
|
52
69
|
return entry.count;
|
|
53
70
|
}
|
|
54
|
-
if (entry)
|
|
55
|
-
clearTimeout(entry.timeoutId);
|
|
56
71
|
const resetAt = Date.now() + windowMs;
|
|
57
|
-
|
|
58
|
-
this.violations.delete(key);
|
|
59
|
-
}, windowMs);
|
|
60
|
-
if (typeof timeoutId.unref === "function") {
|
|
61
|
-
timeoutId.unref();
|
|
62
|
-
}
|
|
63
|
-
this.violations.set(key, { count: 1, resetAt, timeoutId });
|
|
72
|
+
this.violations.set(key, { count: 1, resetAt });
|
|
64
73
|
return 1;
|
|
65
74
|
}
|
|
66
75
|
reset(key) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.hits.delete(key);
|
|
71
|
-
}
|
|
72
|
-
const ban = this.bans.get(key);
|
|
73
|
-
if (ban) {
|
|
74
|
-
clearTimeout(ban.timeoutId);
|
|
75
|
-
this.bans.delete(key);
|
|
76
|
-
}
|
|
77
|
-
const violation = this.violations.get(key);
|
|
78
|
-
if (violation) {
|
|
79
|
-
clearTimeout(violation.timeoutId);
|
|
80
|
-
this.violations.delete(key);
|
|
81
|
-
}
|
|
76
|
+
this.hits.delete(key);
|
|
77
|
+
this.bans.delete(key);
|
|
78
|
+
this.violations.delete(key);
|
|
82
79
|
}
|
|
83
80
|
shutdown() {
|
|
84
|
-
|
|
85
|
-
clearTimeout(entry.timeoutId);
|
|
86
|
-
}
|
|
81
|
+
clearInterval(this.sweepInterval);
|
|
87
82
|
this.hits.clear();
|
|
88
|
-
for (const [, entry] of this.bans) {
|
|
89
|
-
clearTimeout(entry.timeoutId);
|
|
90
|
-
}
|
|
91
83
|
this.bans.clear();
|
|
92
|
-
for (const [, entry] of this.violations) {
|
|
93
|
-
clearTimeout(entry.timeoutId);
|
|
94
|
-
}
|
|
95
84
|
this.violations.clear();
|
|
96
85
|
}
|
|
97
86
|
}
|
|
@@ -303,6 +292,80 @@ function hitlimit(options, handler) {
|
|
|
303
292
|
const response = config.response;
|
|
304
293
|
const customKey = options.key;
|
|
305
294
|
const blockedBody = JSON.stringify(response);
|
|
295
|
+
const isSyncStore = store.isSync === true;
|
|
296
|
+
const isSyncKey = !customKey;
|
|
297
|
+
if (!hasSkip && !hasTiers && !hasBan && !hasGroup && isSyncStore && isSyncKey) {
|
|
298
|
+
return (req, server) => {
|
|
299
|
+
const ip = server.requestIP(req)?.address || "unknown";
|
|
300
|
+
const result = store.hit(ip, windowMs, limit);
|
|
301
|
+
const allowed = result.count <= limit;
|
|
302
|
+
if (!allowed) {
|
|
303
|
+
const headers = { "Content-Type": "application/json" };
|
|
304
|
+
const resetIn = Math.ceil((result.resetAt - Date.now()) / 1000);
|
|
305
|
+
if (standardHeaders) {
|
|
306
|
+
headers["RateLimit-Limit"] = String(limit);
|
|
307
|
+
headers["RateLimit-Remaining"] = "0";
|
|
308
|
+
headers["RateLimit-Reset"] = String(resetIn);
|
|
309
|
+
}
|
|
310
|
+
if (legacyHeaders) {
|
|
311
|
+
headers["X-RateLimit-Limit"] = String(limit);
|
|
312
|
+
headers["X-RateLimit-Remaining"] = "0";
|
|
313
|
+
headers["X-RateLimit-Reset"] = String(Math.ceil(result.resetAt / 1000));
|
|
314
|
+
}
|
|
315
|
+
if (retryAfterHeader) {
|
|
316
|
+
headers["Retry-After"] = String(resetIn);
|
|
317
|
+
}
|
|
318
|
+
return new Response(blockedBody, { status: 429, headers });
|
|
319
|
+
}
|
|
320
|
+
const res = handler(req, server);
|
|
321
|
+
if (res instanceof Promise) {
|
|
322
|
+
return res.then((response2) => {
|
|
323
|
+
if (standardHeaders || legacyHeaders) {
|
|
324
|
+
const resetIn = Math.ceil((result.resetAt - Date.now()) / 1000);
|
|
325
|
+
const remaining = Math.max(0, limit - result.count);
|
|
326
|
+
const newHeaders = new Headers(response2.headers);
|
|
327
|
+
if (standardHeaders) {
|
|
328
|
+
newHeaders.set("RateLimit-Limit", String(limit));
|
|
329
|
+
newHeaders.set("RateLimit-Remaining", String(remaining));
|
|
330
|
+
newHeaders.set("RateLimit-Reset", String(resetIn));
|
|
331
|
+
}
|
|
332
|
+
if (legacyHeaders) {
|
|
333
|
+
newHeaders.set("X-RateLimit-Limit", String(limit));
|
|
334
|
+
newHeaders.set("X-RateLimit-Remaining", String(remaining));
|
|
335
|
+
newHeaders.set("X-RateLimit-Reset", String(Math.ceil(result.resetAt / 1000)));
|
|
336
|
+
}
|
|
337
|
+
return new Response(response2.body, {
|
|
338
|
+
status: response2.status,
|
|
339
|
+
statusText: response2.statusText,
|
|
340
|
+
headers: newHeaders
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return response2;
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (standardHeaders || legacyHeaders) {
|
|
347
|
+
const resetIn = Math.ceil((result.resetAt - Date.now()) / 1000);
|
|
348
|
+
const remaining = Math.max(0, limit - result.count);
|
|
349
|
+
const newHeaders = new Headers(res.headers);
|
|
350
|
+
if (standardHeaders) {
|
|
351
|
+
newHeaders.set("RateLimit-Limit", String(limit));
|
|
352
|
+
newHeaders.set("RateLimit-Remaining", String(remaining));
|
|
353
|
+
newHeaders.set("RateLimit-Reset", String(resetIn));
|
|
354
|
+
}
|
|
355
|
+
if (legacyHeaders) {
|
|
356
|
+
newHeaders.set("X-RateLimit-Limit", String(limit));
|
|
357
|
+
newHeaders.set("X-RateLimit-Remaining", String(remaining));
|
|
358
|
+
newHeaders.set("X-RateLimit-Reset", String(Math.ceil(result.resetAt / 1000)));
|
|
359
|
+
}
|
|
360
|
+
return new Response(res.body, {
|
|
361
|
+
status: res.status,
|
|
362
|
+
statusText: res.statusText,
|
|
363
|
+
headers: newHeaders
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return res;
|
|
367
|
+
};
|
|
368
|
+
}
|
|
306
369
|
if (!hasSkip && !hasTiers && !hasBan && !hasGroup) {
|
|
307
370
|
return async (req, server) => {
|
|
308
371
|
try {
|
|
@@ -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;AA2G3E,wBAAgB,WAAW,IAAI,aAAa,CAE3C"}
|
package/dist/stores/memory.js
CHANGED
|
@@ -1,49 +1,66 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/stores/memory.ts
|
|
3
3
|
class MemoryStore {
|
|
4
|
+
isSync = true;
|
|
4
5
|
hits = new Map;
|
|
5
6
|
bans = new Map;
|
|
6
7
|
violations = new Map;
|
|
8
|
+
_result = { count: 0, resetAt: 0 };
|
|
9
|
+
sweepInterval;
|
|
10
|
+
constructor() {
|
|
11
|
+
this.sweepInterval = setInterval(() => {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
for (const [key, entry] of this.hits) {
|
|
14
|
+
if (now >= entry.resetAt)
|
|
15
|
+
this.hits.delete(key);
|
|
16
|
+
}
|
|
17
|
+
for (const [key, ban] of this.bans) {
|
|
18
|
+
if (now >= ban.expiresAt)
|
|
19
|
+
this.bans.delete(key);
|
|
20
|
+
}
|
|
21
|
+
for (const [key, violation] of this.violations) {
|
|
22
|
+
if (now >= violation.resetAt)
|
|
23
|
+
this.violations.delete(key);
|
|
24
|
+
}
|
|
25
|
+
}, 1e4);
|
|
26
|
+
if (typeof this.sweepInterval.unref === "function") {
|
|
27
|
+
this.sweepInterval.unref();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
7
30
|
hit(key, windowMs, _limit) {
|
|
8
31
|
const entry = this.hits.get(key);
|
|
9
32
|
if (entry !== undefined) {
|
|
10
|
-
|
|
11
|
-
|
|
33
|
+
const now2 = Date.now();
|
|
34
|
+
if (now2 >= entry.resetAt) {
|
|
35
|
+
entry.count = 1;
|
|
36
|
+
entry.resetAt = now2 + windowMs;
|
|
37
|
+
} else {
|
|
38
|
+
entry.count++;
|
|
39
|
+
}
|
|
40
|
+
this._result.count = entry.count;
|
|
41
|
+
this._result.resetAt = entry.resetAt;
|
|
42
|
+
return this._result;
|
|
12
43
|
}
|
|
13
44
|
const now = Date.now();
|
|
14
45
|
const resetAt = now + windowMs;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
timeoutId.unref();
|
|
20
|
-
}
|
|
21
|
-
this.hits.set(key, { count: 1, resetAt, timeoutId });
|
|
22
|
-
return { count: 1, resetAt };
|
|
46
|
+
this.hits.set(key, { count: 1, resetAt });
|
|
47
|
+
this._result.count = 1;
|
|
48
|
+
this._result.resetAt = resetAt;
|
|
49
|
+
return this._result;
|
|
23
50
|
}
|
|
24
51
|
isBanned(key) {
|
|
25
52
|
const ban = this.bans.get(key);
|
|
26
53
|
if (!ban)
|
|
27
54
|
return false;
|
|
28
55
|
if (Date.now() >= ban.expiresAt) {
|
|
29
|
-
clearTimeout(ban.timeoutId);
|
|
30
56
|
this.bans.delete(key);
|
|
31
57
|
return false;
|
|
32
58
|
}
|
|
33
59
|
return true;
|
|
34
60
|
}
|
|
35
61
|
ban(key, durationMs) {
|
|
36
|
-
const existing = this.bans.get(key);
|
|
37
|
-
if (existing)
|
|
38
|
-
clearTimeout(existing.timeoutId);
|
|
39
62
|
const expiresAt = Date.now() + durationMs;
|
|
40
|
-
|
|
41
|
-
this.bans.delete(key);
|
|
42
|
-
}, durationMs);
|
|
43
|
-
if (typeof timeoutId.unref === "function") {
|
|
44
|
-
timeoutId.unref();
|
|
45
|
-
}
|
|
46
|
-
this.bans.set(key, { expiresAt, timeoutId });
|
|
63
|
+
this.bans.set(key, { expiresAt });
|
|
47
64
|
}
|
|
48
65
|
recordViolation(key, windowMs) {
|
|
49
66
|
const entry = this.violations.get(key);
|
|
@@ -51,47 +68,19 @@ class MemoryStore {
|
|
|
51
68
|
entry.count++;
|
|
52
69
|
return entry.count;
|
|
53
70
|
}
|
|
54
|
-
if (entry)
|
|
55
|
-
clearTimeout(entry.timeoutId);
|
|
56
71
|
const resetAt = Date.now() + windowMs;
|
|
57
|
-
|
|
58
|
-
this.violations.delete(key);
|
|
59
|
-
}, windowMs);
|
|
60
|
-
if (typeof timeoutId.unref === "function") {
|
|
61
|
-
timeoutId.unref();
|
|
62
|
-
}
|
|
63
|
-
this.violations.set(key, { count: 1, resetAt, timeoutId });
|
|
72
|
+
this.violations.set(key, { count: 1, resetAt });
|
|
64
73
|
return 1;
|
|
65
74
|
}
|
|
66
75
|
reset(key) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this.hits.delete(key);
|
|
71
|
-
}
|
|
72
|
-
const ban = this.bans.get(key);
|
|
73
|
-
if (ban) {
|
|
74
|
-
clearTimeout(ban.timeoutId);
|
|
75
|
-
this.bans.delete(key);
|
|
76
|
-
}
|
|
77
|
-
const violation = this.violations.get(key);
|
|
78
|
-
if (violation) {
|
|
79
|
-
clearTimeout(violation.timeoutId);
|
|
80
|
-
this.violations.delete(key);
|
|
81
|
-
}
|
|
76
|
+
this.hits.delete(key);
|
|
77
|
+
this.bans.delete(key);
|
|
78
|
+
this.violations.delete(key);
|
|
82
79
|
}
|
|
83
80
|
shutdown() {
|
|
84
|
-
|
|
85
|
-
clearTimeout(entry.timeoutId);
|
|
86
|
-
}
|
|
81
|
+
clearInterval(this.sweepInterval);
|
|
87
82
|
this.hits.clear();
|
|
88
|
-
for (const [, entry] of this.bans) {
|
|
89
|
-
clearTimeout(entry.timeoutId);
|
|
90
|
-
}
|
|
91
83
|
this.bans.clear();
|
|
92
|
-
for (const [, entry] of this.violations) {
|
|
93
|
-
clearTimeout(entry.timeoutId);
|
|
94
|
-
}
|
|
95
84
|
this.violations.clear();
|
|
96
85
|
}
|
|
97
86
|
}
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joint-ops/hitlimit-bun",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
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",
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"test:watch": "bun test --watch"
|
|
118
118
|
},
|
|
119
119
|
"dependencies": {
|
|
120
|
-
"@joint-ops/hitlimit-types": "1.1.
|
|
120
|
+
"@joint-ops/hitlimit-types": "1.1.2"
|
|
121
121
|
},
|
|
122
122
|
"peerDependencies": {
|
|
123
123
|
"elysia": ">=1.0.0",
|