@joint-ops/hitlimit-bun 1.1.0 → 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 +21 -30
- package/dist/elysia.d.ts.map +1 -1
- package/dist/elysia.js +63 -7
- package/dist/hono.d.ts.map +1 -1
- package/dist/hono.js +52 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +75 -0
- 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 +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 - 7M+ ops/sec with memory-first design | 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 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
|
-
**Memory-first for maximum performance.**
|
|
17
|
+
**Memory-first for maximum performance.** 14.6x faster than SQLite.
|
|
18
18
|
|
|
19
19
|
```
|
|
20
20
|
┌─────────────────────────────────────────────────────────────────┐
|
|
21
21
|
│ │
|
|
22
|
-
│ Memory (v1.1+) ██████████████████████████████
|
|
23
|
-
│ SQLite (v1.0) █░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
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** -
|
|
30
|
+
- **🚀 Memory-First** - 7.29M ops/sec by default (v1.1.0+), 14.6x 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 (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
|
|
|
@@ -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** | 7,
|
|
369
|
-
| **bun:sqlite** |
|
|
370
|
-
| **Redis** | 6,
|
|
359
|
+
| **Memory** | 7,290,000+ | +52% faster |
|
|
360
|
+
| **bun:sqlite** | 500,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,6 +1,7 @@
|
|
|
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;
|
|
@@ -280,6 +281,12 @@ var instanceCounter = 0;
|
|
|
280
281
|
function getDefaultKey(_ctx) {
|
|
281
282
|
return "unknown";
|
|
282
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
|
+
}
|
|
283
290
|
function hitlimit(options = {}) {
|
|
284
291
|
const pluginName = options.name ?? `hitlimit-${instanceCounter++}`;
|
|
285
292
|
if (options.sqlitePath && !options.store) {
|
|
@@ -287,6 +294,56 @@ function hitlimit(options = {}) {
|
|
|
287
294
|
}
|
|
288
295
|
const store = options.store ?? memoryStore();
|
|
289
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
|
+
}
|
|
290
347
|
return new Elysia({ name: pluginName }).onBeforeHandle({ as: "scoped" }, async ({ request, set }) => {
|
|
291
348
|
const ctx = { request };
|
|
292
349
|
if (config.skip) {
|
|
@@ -302,13 +359,12 @@ function hitlimit(options = {}) {
|
|
|
302
359
|
});
|
|
303
360
|
if (!result.allowed) {
|
|
304
361
|
set.status = 429;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
});
|
|
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 });
|
|
312
368
|
}
|
|
313
369
|
} catch (error) {
|
|
314
370
|
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,6 +1,7 @@
|
|
|
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;
|
|
@@ -279,9 +280,60 @@ async function checkLimit(config, req) {
|
|
|
279
280
|
function getDefaultKey(c) {
|
|
280
281
|
return c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || c.req.header("x-real-ip") || "unknown";
|
|
281
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
|
+
}
|
|
282
289
|
function hitlimit(options = {}) {
|
|
283
290
|
const store = options.store ?? memoryStore();
|
|
284
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
|
+
}
|
|
285
337
|
return createMiddleware(async (c, next) => {
|
|
286
338
|
if (config.skip) {
|
|
287
339
|
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,6 +1,7 @@
|
|
|
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;
|
|
@@ -303,6 +304,80 @@ function hitlimit(options, handler) {
|
|
|
303
304
|
const response = config.response;
|
|
304
305
|
const customKey = options.key;
|
|
305
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
|
+
}
|
|
306
381
|
if (!hasSkip && !hasTiers && !hasBan && !hasGroup) {
|
|
307
382
|
return async (req, server) => {
|
|
308
383
|
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;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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joint-ops/hitlimit-bun",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
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.1"
|
|
121
121
|
},
|
|
122
122
|
"peerDependencies": {
|
|
123
123
|
"elysia": ">=1.0.0",
|