@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 CHANGED
@@ -6,36 +6,36 @@
6
6
  [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
7
7
  [![Bun](https://img.shields.io/badge/Bun-Native-black.svg)](https://bun.sh)
8
8
 
9
- > The fastest rate limiter for Bun - 6M+ ops/sec with memory-first design | Elysia, Hono & Bun.serve
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 6.10M ops/sec performance (15.7x faster than SQLite). Optional persistence with native bun:sqlite or Redis when you need it.
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.** 15.7x faster than SQLite in high-traffic scenarios.
17
+ **Memory-first for maximum performance.** ~15x faster than SQLite.
18
18
 
19
19
  ```
20
20
  ┌─────────────────────────────────────────────────────────────────┐
21
21
  │ │
22
- │ Memory (v1.1+) ██████████████████████████████ 6.10M ops/s │
23
- │ SQLite (v1.0) █░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 386K ops/s │
22
+ │ Memory (v1.1+) ██████████████████████████████ 5.62M ops/s │
23
+ │ SQLite (v1.0) █░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 383K ops/s │
24
24
  │ │
25
- 15.7x performance improvement with memory default
25
+ ~15x performance improvement with memory default (10K IPs)
26
26
  │ │
27
27
  └─────────────────────────────────────────────────────────────────┘
28
28
  ```
29
29
 
30
- - **🚀 Memory-First** - 6.10M ops/sec by default (v1.1.0+), 15.7x faster than SQLite
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 (386K ops/sec) or Redis (6.9K ops/sec) when needed
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** - ~23KB total, zero runtime dependencies
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: bun:sqlite)
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
- ### SQLite Store (Default)
277
+ ### Memory Store (Default)
278
278
 
279
- Uses Bun's native bun:sqlite for maximum performance. Default store.
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 bun:sqlite with in-memory database
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
- ### Memory Store
290
+ ### SQLite Store
300
291
 
301
- For simple use cases without persistence.
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 { memoryStore } from '@joint-ops/hitlimit-bun/stores/memory'
296
+ import { sqliteStore } from '@joint-ops/hitlimit-bun'
306
297
 
307
298
  Bun.serve({
308
299
  fetch: hitlimit({
309
- store: memoryStore()
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,210,000+ | +130% faster |
369
- | **bun:sqlite** | 520,000+ | **+10% faster** 🔥 |
370
- | **Redis** | 6,900+ | +3% faster |
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` (2.7x faster than better-sqlite3)
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
@@ -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,EAAE,MAAM,2BAA2B,CAAA;AAKhE,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;AAQD,wBAAgB,QAAQ,CAAC,OAAO,GAAE,qBAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuD3D"}
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
- entry.count++;
11
- return { count: entry.count, resetAt: entry.resetAt };
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
- const timeoutId = setTimeout(() => {
16
- this.hits.delete(key);
17
- }, windowMs);
18
- if (typeof timeoutId.unref === "function") {
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
- const timeoutId = setTimeout(() => {
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
- const timeoutId = setTimeout(() => {
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
- const entry = this.hits.get(key);
68
- if (entry) {
69
- clearTimeout(entry.timeoutId);
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
- for (const [, entry] of this.hits) {
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
- return new Response(JSON.stringify(result.body), {
306
- status: 429,
307
- headers: {
308
- "Content-Type": "application/json",
309
- ...result.headers
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);
@@ -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,EAAE,MAAM,2BAA2B,CAAA;AAWhE,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAe,CAAC,OAAO,CAAM;;kBAgC9D"}
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
- entry.count++;
11
- return { count: entry.count, resetAt: entry.resetAt };
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
- const timeoutId = setTimeout(() => {
16
- this.hits.delete(key);
17
- }, windowMs);
18
- if (typeof timeoutId.unref === "function") {
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
- const timeoutId = setTimeout(() => {
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
- const timeoutId = setTimeout(() => {
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
- const entry = this.hits.get(key);
68
- if (entry) {
69
- clearTimeout(entry.timeoutId);
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
- for (const [, entry] of this.hits) {
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);
@@ -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,CA2JnE;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"}
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
- entry.count++;
11
- return { count: entry.count, resetAt: entry.resetAt };
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
- const timeoutId = setTimeout(() => {
16
- this.hits.delete(key);
17
- }, windowMs);
18
- if (typeof timeoutId.unref === "function") {
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
- const timeoutId = setTimeout(() => {
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
- const timeoutId = setTimeout(() => {
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
- const entry = this.hits.get(key);
68
- if (entry) {
69
- clearTimeout(entry.timeoutId);
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
- for (const [, entry] of this.hits) {
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;AAgJ3E,wBAAgB,WAAW,IAAI,aAAa,CAE3C"}
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"}
@@ -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
- entry.count++;
11
- return { count: entry.count, resetAt: entry.resetAt };
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
- const timeoutId = setTimeout(() => {
16
- this.hits.delete(key);
17
- }, windowMs);
18
- if (typeof timeoutId.unref === "function") {
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
- const timeoutId = setTimeout(() => {
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
- const timeoutId = setTimeout(() => {
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
- const entry = this.hits.get(key);
68
- if (entry) {
69
- clearTimeout(entry.timeoutId);
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
- for (const [, entry] of this.hits) {
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;AA+GD,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,aAAa,CAEvE"}
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"}
@@ -3,6 +3,7 @@
3
3
  import { Database } from "bun:sqlite";
4
4
 
5
5
  class BunSqliteStore {
6
+ isSync = true;
6
7
  db;
7
8
  hitStmt;
8
9
  getStmt;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joint-ops/hitlimit-bun",
3
- "version": "1.1.0",
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.0"
120
+ "@joint-ops/hitlimit-types": "1.1.2"
121
121
  },
122
122
  "peerDependencies": {
123
123
  "elysia": ">=1.0.0",