@xyph3r/rate-limiter 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +310 -0
  3. package/dist/adapters/express.d.ts +26 -0
  4. package/dist/adapters/express.d.ts.map +1 -0
  5. package/dist/adapters/express.js +51 -0
  6. package/dist/adapters/express.js.map +1 -0
  7. package/dist/adapters/fastify.d.ts +24 -0
  8. package/dist/adapters/fastify.d.ts.map +1 -0
  9. package/dist/adapters/fastify.js +40 -0
  10. package/dist/adapters/fastify.js.map +1 -0
  11. package/dist/adapters/fetch.d.ts +17 -0
  12. package/dist/adapters/fetch.d.ts.map +1 -0
  13. package/dist/adapters/fetch.js +53 -0
  14. package/dist/adapters/fetch.js.map +1 -0
  15. package/dist/adapters/hono.d.ts +25 -0
  16. package/dist/adapters/hono.d.ts.map +1 -0
  17. package/dist/adapters/hono.js +40 -0
  18. package/dist/adapters/hono.js.map +1 -0
  19. package/dist/adapters/nest.d.ts +32 -0
  20. package/dist/adapters/nest.d.ts.map +1 -0
  21. package/dist/adapters/nest.js +47 -0
  22. package/dist/adapters/nest.js.map +1 -0
  23. package/dist/adapters/next.d.ts +12 -0
  24. package/dist/adapters/next.d.ts.map +1 -0
  25. package/dist/adapters/next.js +11 -0
  26. package/dist/adapters/next.js.map +1 -0
  27. package/dist/core/cached-rate-limiter-proxy.d.ts +17 -0
  28. package/dist/core/cached-rate-limiter-proxy.d.ts.map +1 -0
  29. package/dist/core/cached-rate-limiter-proxy.js +47 -0
  30. package/dist/core/cached-rate-limiter-proxy.js.map +1 -0
  31. package/dist/core/create-rate-limiter.d.ts +11 -0
  32. package/dist/core/create-rate-limiter.d.ts.map +1 -0
  33. package/dist/core/create-rate-limiter.js +34 -0
  34. package/dist/core/create-rate-limiter.js.map +1 -0
  35. package/dist/core/rate-limiter-builder.d.ts +27 -0
  36. package/dist/core/rate-limiter-builder.d.ts.map +1 -0
  37. package/dist/core/rate-limiter-builder.js +73 -0
  38. package/dist/core/rate-limiter-builder.js.map +1 -0
  39. package/dist/core/rate-limiter.d.ts +23 -0
  40. package/dist/core/rate-limiter.d.ts.map +1 -0
  41. package/dist/core/rate-limiter.js +59 -0
  42. package/dist/core/rate-limiter.js.map +1 -0
  43. package/dist/core/strategy-factory.d.ts +18 -0
  44. package/dist/core/strategy-factory.d.ts.map +1 -0
  45. package/dist/core/strategy-factory.js +17 -0
  46. package/dist/core/strategy-factory.js.map +1 -0
  47. package/dist/errors.d.ts +7 -0
  48. package/dist/errors.d.ts.map +1 -0
  49. package/dist/errors.js +13 -0
  50. package/dist/errors.js.map +1 -0
  51. package/dist/index.d.ts +19 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +17 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/stores/memory-store.d.ts +9 -0
  56. package/dist/stores/memory-store.d.ts.map +1 -0
  57. package/dist/stores/memory-store.js +24 -0
  58. package/dist/stores/memory-store.js.map +1 -0
  59. package/dist/stores/rate-limit-store.d.ts +6 -0
  60. package/dist/stores/rate-limit-store.d.ts.map +1 -0
  61. package/dist/stores/rate-limit-store.js +2 -0
  62. package/dist/stores/rate-limit-store.js.map +1 -0
  63. package/dist/stores/redis-store.d.ts +26 -0
  64. package/dist/stores/redis-store.d.ts.map +1 -0
  65. package/dist/stores/redis-store.js +41 -0
  66. package/dist/stores/redis-store.js.map +1 -0
  67. package/dist/strategies/sliding-window-strategy.d.ts +31 -0
  68. package/dist/strategies/sliding-window-strategy.d.ts.map +1 -0
  69. package/dist/strategies/sliding-window-strategy.js +212 -0
  70. package/dist/strategies/sliding-window-strategy.js.map +1 -0
  71. package/dist/strategies/token-bucket-strategy.d.ts +30 -0
  72. package/dist/strategies/token-bucket-strategy.d.ts.map +1 -0
  73. package/dist/strategies/token-bucket-strategy.js +154 -0
  74. package/dist/strategies/token-bucket-strategy.js.map +1 -0
  75. package/dist/types.d.ts +48 -0
  76. package/dist/types.d.ts.map +1 -0
  77. package/dist/types.js +2 -0
  78. package/dist/types.js.map +1 -0
  79. package/dist/utils/headers.d.ts +4 -0
  80. package/dist/utils/headers.d.ts.map +1 -0
  81. package/dist/utils/headers.js +16 -0
  82. package/dist/utils/headers.js.map +1 -0
  83. package/dist/utils/http.d.ts +11 -0
  84. package/dist/utils/http.d.ts.map +1 -0
  85. package/dist/utils/http.js +41 -0
  86. package/dist/utils/http.js.map +1 -0
  87. package/dist/utils/math.d.ts +4 -0
  88. package/dist/utils/math.d.ts.map +1 -0
  89. package/dist/utils/math.js +21 -0
  90. package/dist/utils/math.js.map +1 -0
  91. package/package.json +94 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xyph3r
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,310 @@
1
+ # `@xyph3r/rate-limiter`
2
+
3
+ Framework-agnostic rate limiting for Node.js and fetch-based runtimes with two clear goals:
4
+
5
+ - keep the core API small enough to use without ceremony
6
+ - keep the internals structured enough to stay maintainable when requirements change
7
+
8
+ The package is organized around a few deliberate design choices:
9
+
10
+ - `Strategy`: sliding window and token bucket are swappable algorithms
11
+ - `Builder`: fluent construction for readable setup
12
+ - `Decorator`: optional Express and Fastify adapters wrap the core
13
+ - `Decorator`: fetch/Hono/Next/Nest integrations stay outside the core
14
+ - `Proxy`: short-lived decision caching for hot paths
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @xyph3r/rate-limiter
20
+ ```
21
+
22
+ ## Quick start
23
+
24
+ ```ts
25
+ import { createExpressRateLimit, RateLimiterBuilder } from "@xyph3r/rate-limiter";
26
+
27
+ const limiter = new RateLimiterBuilder()
28
+ .forSlidingWindow({ limit: 100, windowMs: 60_000 })
29
+ .withMemoryStore()
30
+ .withKeyPrefix("api")
31
+ .build();
32
+
33
+ app.use(createExpressRateLimit(limiter));
34
+ ```
35
+
36
+ ## Bun
37
+
38
+ For Bun's native `fetch` handler, use the shared fetch adapter:
39
+
40
+ ```ts
41
+ import { createFetchRateLimit, RateLimiterBuilder } from "@xyph3r/rate-limiter";
42
+
43
+ const limiter = new RateLimiterBuilder()
44
+ .forSlidingWindow({ limit: 100, windowMs: 60_000 })
45
+ .withMemoryStore()
46
+ .build();
47
+
48
+ const withRateLimit = createFetchRateLimit(limiter);
49
+
50
+ Bun.serve({
51
+ fetch: withRateLimit(async () => {
52
+ return Response.json({ ok: true });
53
+ }),
54
+ });
55
+ ```
56
+
57
+ ## Core usage
58
+
59
+ ### Sliding window
60
+
61
+ ```ts
62
+ import { MemoryStore, RateLimiterBuilder } from "@xyph3r/rate-limiter";
63
+
64
+ const limiter = new RateLimiterBuilder()
65
+ .useStore(new MemoryStore())
66
+ .forSlidingWindow({
67
+ limit: 10,
68
+ windowMs: 1_000,
69
+ })
70
+ .build();
71
+
72
+ const decision = await limiter.check("user:42");
73
+
74
+ if (!decision.allowed) {
75
+ console.log(`Retry in ${decision.retryAfterMs}ms`);
76
+ }
77
+ ```
78
+
79
+ ### Token bucket
80
+
81
+ ```ts
82
+ import { createRateLimiter } from "@xyph3r/rate-limiter";
83
+
84
+ const limiter = createRateLimiter({
85
+ algorithm: "token-bucket",
86
+ capacity: 20,
87
+ refillRate: 5,
88
+ refillIntervalMs: 1_000,
89
+ keyPrefix: "jobs",
90
+ });
91
+ ```
92
+
93
+ ## Redis-backed usage
94
+
95
+ The package does not force a Redis client. Instead, it ships lightweight executors for the two common interfaces.
96
+
97
+ ### `node-redis`
98
+
99
+ ```ts
100
+ import { createClient } from "redis";
101
+ import {
102
+ createNodeRedisExecutor,
103
+ RedisStore,
104
+ RateLimiterBuilder,
105
+ } from "@xyph3r/rate-limiter";
106
+
107
+ const client = createClient();
108
+ await client.connect();
109
+
110
+ const store = new RedisStore(createNodeRedisExecutor(client));
111
+
112
+ const limiter = new RateLimiterBuilder()
113
+ .useStore(store)
114
+ .forSlidingWindow({ limit: 200, windowMs: 60_000 })
115
+ .build();
116
+ ```
117
+
118
+ ### `ioredis`
119
+
120
+ ```ts
121
+ import Redis from "ioredis";
122
+ import {
123
+ createIORedisExecutor,
124
+ RedisStore,
125
+ RateLimiterBuilder,
126
+ } from "@xyph3r/rate-limiter";
127
+
128
+ const client = new Redis(process.env.REDIS_URL!);
129
+ const store = new RedisStore(createIORedisExecutor(client));
130
+ ```
131
+
132
+ ## Simple factory
133
+
134
+ If you prefer a plain config object over the builder:
135
+
136
+ ```ts
137
+ import { createRateLimiter } from "@xyph3r/rate-limiter";
138
+
139
+ const limiter = createRateLimiter({
140
+ algorithm: "sliding-window",
141
+ limit: 50,
142
+ windowMs: 10_000,
143
+ keyPrefix: "public-api",
144
+ cacheMs: 25,
145
+ });
146
+ ```
147
+
148
+ ## Express adapter
149
+
150
+ ```ts
151
+ import express from "express";
152
+ import {
153
+ createExpressRateLimit,
154
+ RateLimiterBuilder,
155
+ } from "@xyph3r/rate-limiter";
156
+
157
+ const app = express();
158
+
159
+ const limiter = new RateLimiterBuilder()
160
+ .forTokenBucket({
161
+ capacity: 30,
162
+ refillRate: 10,
163
+ refillIntervalMs: 1_000,
164
+ })
165
+ .withMemoryStore()
166
+ .build();
167
+
168
+ app.use(
169
+ createExpressRateLimit(limiter, {
170
+ key: (req) => req.headers["x-api-key"] as string,
171
+ }),
172
+ );
173
+ ```
174
+
175
+ ## Hono adapter
176
+
177
+ ```ts
178
+ import { Hono } from "hono";
179
+ import {
180
+ createHonoRateLimit,
181
+ RateLimiterBuilder,
182
+ } from "@xyph3r/rate-limiter";
183
+
184
+ const app = new Hono();
185
+
186
+ const limiter = new RateLimiterBuilder()
187
+ .forSlidingWindow({ limit: 20, windowMs: 1_000 })
188
+ .withMemoryStore()
189
+ .build();
190
+
191
+ app.use("*", createHonoRateLimit(limiter));
192
+ ```
193
+
194
+ ## Fastify adapter
195
+
196
+ ```ts
197
+ import fastify from "fastify";
198
+ import {
199
+ createFastifyRateLimit,
200
+ RateLimiterBuilder,
201
+ } from "@xyph3r/rate-limiter";
202
+
203
+ const app = fastify();
204
+
205
+ const limiter = new RateLimiterBuilder()
206
+ .forSlidingWindow({ limit: 5, windowMs: 1_000 })
207
+ .withMemoryStore()
208
+ .build();
209
+
210
+ app.addHook("preHandler", createFastifyRateLimit(limiter));
211
+ ```
212
+
213
+ ## Next.js route handlers
214
+
215
+ For App Router route handlers, wrap the exported handler:
216
+
217
+ ```ts
218
+ import { createNextRateLimit, RateLimiterBuilder } from "@xyph3r/rate-limiter";
219
+
220
+ const limiter = new RateLimiterBuilder()
221
+ .forSlidingWindow({ limit: 30, windowMs: 60_000 })
222
+ .withMemoryStore()
223
+ .build();
224
+
225
+ const withRateLimit = createNextRateLimit(limiter);
226
+
227
+ export const GET = withRateLimit(async () => {
228
+ return Response.json({ ok: true });
229
+ });
230
+ ```
231
+
232
+ You can also use `key(request, context)` to derive limits from route params, session data, or tenant IDs.
233
+
234
+ ## NestJS guard
235
+
236
+ Nest is exposed as a guard-shaped decorator. Pass your own exception from `@nestjs/common` so the framework returns a proper `429`.
237
+
238
+ ```ts
239
+ import { TooManyRequestsException } from "@nestjs/common";
240
+ import {
241
+ createNestRateLimitGuard,
242
+ RateLimiterBuilder,
243
+ } from "@xyph3r/rate-limiter";
244
+
245
+ const limiter = new RateLimiterBuilder()
246
+ .forSlidingWindow({ limit: 10, windowMs: 1_000 })
247
+ .withMemoryStore()
248
+ .build();
249
+
250
+ export const RateLimitGuard = createNestRateLimitGuard(limiter, {
251
+ errorFactory: (decision) =>
252
+ new TooManyRequestsException({
253
+ error: "Too many requests",
254
+ retryAfterMs: decision.retryAfterMs,
255
+ }),
256
+ });
257
+ ```
258
+
259
+ ## Public API
260
+
261
+ ### `RateLimiterBuilder`
262
+
263
+ - `.forSlidingWindow({ limit, windowMs })`
264
+ - `.forTokenBucket({ capacity, refillRate, refillIntervalMs })`
265
+ - `.useStore(store)` / `.withMemoryStore()`
266
+ - `.withKeyPrefix(prefix)`
267
+ - `.withCache(ttlMs)`
268
+ - `.build()`
269
+
270
+ ### `RateLimiterLike`
271
+
272
+ - `check(key, { cost? })`
273
+ - `reset(key)`
274
+
275
+ ### Adapters
276
+
277
+ - `createFetchRateLimit()` for Bun or any fetch-native runtime
278
+ - `createHonoRateLimit()` for Hono middleware
279
+ - `createNextRateLimit()` for Next.js App Router handlers
280
+ - `createNestRateLimitGuard()` for NestJS HTTP guards
281
+ - `createExpressRateLimit()` and `createFastifyRateLimit()`
282
+
283
+ ### Decision fields
284
+
285
+ - `allowed`
286
+ - `limit`
287
+ - `used`
288
+ - `remaining`
289
+ - `retryAfterMs`
290
+ - `resetAt`
291
+ - `policy`
292
+
293
+ ## Notes
294
+
295
+ - The memory store is process-local. Use Redis for multi-instance deployments.
296
+ - Redis execution is atomic because each strategy ships its own Lua program.
297
+ - The sliding window implementation uses a weighted previous-window approximation rather than a timestamp log. That keeps storage compact and predictable.
298
+ - Bun, Hono, and Next.js all use the fetch-compatible adapter surface under the hood.
299
+
300
+ ## Publishing
301
+
302
+ The package is set up so `npm publish` builds `dist/` during `prepack` and runs the test suite during `prepublishOnly`.
303
+
304
+ Release flow:
305
+
306
+ 1. Install dev dependencies with `npm install`.
307
+ 2. Verify the package locally with `npm test`.
308
+ 3. Inspect the publish tarball with `npm pack --dry-run`.
309
+ 4. Log in with `npm login` if needed, then confirm the target account with `npm whoami`.
310
+ 5. Publish the public scoped package with `npm publish --access public --provenance`.
@@ -0,0 +1,26 @@
1
+ import { type HeaderCarrier } from "../utils/http.js";
2
+ import type { RateLimitDecision, RateLimiterLike } from "../types.js";
3
+ export interface ExpressLikeRequest extends HeaderCarrier {
4
+ }
5
+ export interface ExpressLikeResponse {
6
+ end(body?: string): unknown;
7
+ json?(body: unknown): unknown;
8
+ setHeader(name: string, value: string): void;
9
+ status(code: number): this;
10
+ }
11
+ export type ExpressLikeNext = (error?: unknown) => void;
12
+ export interface ExpressRateLimitOptions<TRequest extends ExpressLikeRequest = ExpressLikeRequest, TResponse extends ExpressLikeResponse = ExpressLikeResponse> {
13
+ cost?: (request: TRequest) => number | Promise<number>;
14
+ key?: (request: TRequest) => string | Promise<string>;
15
+ onRejected?: (request: TRequest, response: TResponse, decision: RateLimitDecision) => void | Promise<void>;
16
+ setHeaders?: boolean;
17
+ skip?: (request: TRequest) => boolean | Promise<boolean>;
18
+ }
19
+ /**
20
+ * Pattern: Decorator
21
+ * Problem: HTTP concerns should not leak into the core limiter.
22
+ * Solution: The adapter decorates the core limiter with request parsing and response shaping.
23
+ * Trade-off: One wrapper per framework; justified because the core stays framework-agnostic.
24
+ */
25
+ export declare function createExpressRateLimit<TRequest extends ExpressLikeRequest = ExpressLikeRequest, TResponse extends ExpressLikeResponse = ExpressLikeResponse>(limiter: RateLimiterLike, options?: ExpressRateLimitOptions<TRequest, TResponse>): (request: TRequest, response: TResponse, next: ExpressLikeNext) => Promise<void>;
26
+ //# sourceMappingURL=express.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/adapters/express.ts"],"names":[],"mappings":"AACA,OAAO,EAA4B,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEtE,MAAM,WAAW,kBAAmB,SAAQ,aAAa;CAAG;AAE5D,MAAM,WAAW,mBAAmB;IAClC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5B,IAAI,CAAC,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IAC9B,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;AAExD,MAAM,WAAW,uBAAuB,CACtC,QAAQ,SAAS,kBAAkB,GAAG,kBAAkB,EACxD,SAAS,SAAS,mBAAmB,GAAG,mBAAmB;IAE3D,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvD,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACtD,UAAU,CAAC,EAAE,CACX,OAAO,EAAE,QAAQ,EACjB,QAAQ,EAAE,SAAS,EACnB,QAAQ,EAAE,iBAAiB,KACxB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,SAAS,kBAAkB,GAAG,kBAAkB,EACxD,SAAS,SAAS,mBAAmB,GAAG,mBAAmB,EAE3D,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE,uBAAuB,CAAC,QAAQ,EAAE,SAAS,CAAM,GACzD,CACD,OAAO,EAAE,QAAQ,EACjB,QAAQ,EAAE,SAAS,EACnB,IAAI,EAAE,eAAe,KAClB,OAAO,CAAC,IAAI,CAAC,CAuCjB"}
@@ -0,0 +1,51 @@
1
+ import { applyHeaders } from "../utils/headers.js";
2
+ import { getDefaultKeyFromRequest } from "../utils/http.js";
3
+ /**
4
+ * Pattern: Decorator
5
+ * Problem: HTTP concerns should not leak into the core limiter.
6
+ * Solution: The adapter decorates the core limiter with request parsing and response shaping.
7
+ * Trade-off: One wrapper per framework; justified because the core stays framework-agnostic.
8
+ */
9
+ export function createExpressRateLimit(limiter, options = {}) {
10
+ return async (request, response, next) => {
11
+ try {
12
+ if ((await options.skip?.(request)) === true) {
13
+ next();
14
+ return;
15
+ }
16
+ const key = (await options.key?.(request)) ?? getDefaultKeyFromRequest(request);
17
+ const cost = (await options.cost?.(request)) ?? 1;
18
+ const decision = await limiter.check(key, { cost });
19
+ if (options.setHeaders !== false) {
20
+ applyHeaders((name, value) => response.setHeader(name, value), decision);
21
+ }
22
+ if (decision.allowed) {
23
+ next();
24
+ return;
25
+ }
26
+ if (options.onRejected) {
27
+ await options.onRejected(request, response, decision);
28
+ return;
29
+ }
30
+ response.status(429);
31
+ response.setHeader("content-type", "application/json; charset=utf-8");
32
+ if (typeof response.json === "function") {
33
+ response.json(defaultRateLimitBody(decision));
34
+ return;
35
+ }
36
+ response.end(JSON.stringify(defaultRateLimitBody(decision)));
37
+ }
38
+ catch (error) {
39
+ next(error);
40
+ }
41
+ };
42
+ }
43
+ function defaultRateLimitBody(decision) {
44
+ return {
45
+ error: "Too many requests",
46
+ limit: decision.limit,
47
+ remaining: decision.remaining,
48
+ retryAfterMs: decision.retryAfterMs,
49
+ };
50
+ }
51
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/adapters/express.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,wBAAwB,EAAsB,MAAM,kBAAkB,CAAC;AA6BhF;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAIpC,OAAwB,EACxB,UAAwD,EAAE;IAM1D,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;QACvC,IAAI,CAAC;YACH,IAAI,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;gBAC7C,IAAI,EAAE,CAAC;gBACP,OAAO;YACT,CAAC;YAED,MAAM,GAAG,GACP,CAAC,MAAM,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,IAAI,wBAAwB,CAAC,OAAO,CAAC,CAAC;YACtE,MAAM,IAAI,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC;YAClD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;YAEpD,IAAI,OAAO,CAAC,UAAU,KAAK,KAAK,EAAE,CAAC;gBACjC,YAAY,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC;YAC3E,CAAC;YAED,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;gBACrB,IAAI,EAAE,CAAC;gBACP,OAAO;YACT,CAAC;YAED,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;gBACvB,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;gBACtD,OAAO;YACT,CAAC;YAED,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACrB,QAAQ,CAAC,SAAS,CAAC,cAAc,EAAE,iCAAiC,CAAC,CAAC;YACtE,IAAI,OAAO,QAAQ,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;gBACxC,QAAQ,CAAC,IAAI,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAC9C,OAAO;YACT,CAAC;YAED,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC/D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,KAAK,CAAC,CAAC;QACd,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB,CAAC,QAA2B;IACvD,OAAO;QACL,KAAK,EAAE,mBAAmB;QAC1B,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,YAAY,EAAE,QAAQ,CAAC,YAAY;KACpC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { type HeaderCarrier } from "../utils/http.js";
2
+ import type { RateLimitDecision, RateLimiterLike } from "../types.js";
3
+ export interface FastifyLikeRequest extends HeaderCarrier {
4
+ }
5
+ export interface FastifyLikeReply {
6
+ code(statusCode: number): this;
7
+ header(name: string, value: string): this;
8
+ send(payload: unknown): unknown;
9
+ }
10
+ export interface FastifyRateLimitOptions<TRequest extends FastifyLikeRequest = FastifyLikeRequest, TReply extends FastifyLikeReply = FastifyLikeReply> {
11
+ cost?: (request: TRequest) => number | Promise<number>;
12
+ key?: (request: TRequest) => string | Promise<string>;
13
+ onRejected?: (request: TRequest, reply: TReply, decision: RateLimitDecision) => void | Promise<void>;
14
+ setHeaders?: boolean;
15
+ skip?: (request: TRequest) => boolean | Promise<boolean>;
16
+ }
17
+ /**
18
+ * Pattern: Decorator
19
+ * Problem: Fastify request handling is framework-specific, but rate-limit decisions are not.
20
+ * Solution: The adapter layers Fastify semantics over the shared limiter contract.
21
+ * Trade-off: Separate adapter module; justified because it keeps the core reusable elsewhere.
22
+ */
23
+ export declare function createFastifyRateLimit<TRequest extends FastifyLikeRequest = FastifyLikeRequest, TReply extends FastifyLikeReply = FastifyLikeReply>(limiter: RateLimiterLike, options?: FastifyRateLimitOptions<TRequest, TReply>): (request: TRequest, reply: TReply) => Promise<void>;
24
+ //# sourceMappingURL=fastify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fastify.d.ts","sourceRoot":"","sources":["../../src/adapters/fastify.ts"],"names":[],"mappings":"AACA,OAAO,EAA4B,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEtE,MAAM,WAAW,kBAAmB,SAAQ,aAAa;CAAG;AAE5D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,uBAAuB,CACtC,QAAQ,SAAS,kBAAkB,GAAG,kBAAkB,EACxD,MAAM,SAAS,gBAAgB,GAAG,gBAAgB;IAElD,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvD,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACtD,UAAU,CAAC,EAAE,CACX,OAAO,EAAE,QAAQ,EACjB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,iBAAiB,KACxB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,SAAS,kBAAkB,GAAG,kBAAkB,EACxD,MAAM,SAAS,gBAAgB,GAAG,gBAAgB,EAElD,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE,uBAAuB,CAAC,QAAQ,EAAE,MAAM,CAAM,GACtD,CAAC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CA2BrD"}
@@ -0,0 +1,40 @@
1
+ import { applyHeaders } from "../utils/headers.js";
2
+ import { getDefaultKeyFromRequest } from "../utils/http.js";
3
+ /**
4
+ * Pattern: Decorator
5
+ * Problem: Fastify request handling is framework-specific, but rate-limit decisions are not.
6
+ * Solution: The adapter layers Fastify semantics over the shared limiter contract.
7
+ * Trade-off: Separate adapter module; justified because it keeps the core reusable elsewhere.
8
+ */
9
+ export function createFastifyRateLimit(limiter, options = {}) {
10
+ return async (request, reply) => {
11
+ if ((await options.skip?.(request)) === true) {
12
+ return;
13
+ }
14
+ const key = (await options.key?.(request)) ?? getDefaultKeyFromRequest(request);
15
+ const cost = (await options.cost?.(request)) ?? 1;
16
+ const decision = await limiter.check(key, { cost });
17
+ if (options.setHeaders !== false) {
18
+ applyHeaders((name, value) => {
19
+ reply.header(name, value);
20
+ }, decision);
21
+ }
22
+ if (decision.allowed) {
23
+ return;
24
+ }
25
+ if (options.onRejected) {
26
+ await options.onRejected(request, reply, decision);
27
+ return;
28
+ }
29
+ reply.code(429).send(defaultRateLimitBody(decision));
30
+ };
31
+ }
32
+ function defaultRateLimitBody(decision) {
33
+ return {
34
+ error: "Too many requests",
35
+ limit: decision.limit,
36
+ remaining: decision.remaining,
37
+ retryAfterMs: decision.retryAfterMs,
38
+ };
39
+ }
40
+ //# sourceMappingURL=fastify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fastify.js","sourceRoot":"","sources":["../../src/adapters/fastify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,wBAAwB,EAAsB,MAAM,kBAAkB,CAAC;AA0BhF;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAIpC,OAAwB,EACxB,UAAqD,EAAE;IAEvD,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAC9B,IAAI,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAC7C,OAAO;QACT,CAAC;QAED,MAAM,GAAG,GAAG,CAAC,MAAM,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,IAAI,wBAAwB,CAAC,OAAO,CAAC,CAAC;QAChF,MAAM,IAAI,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC;QAClD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;QAEpD,IAAI,OAAO,CAAC,UAAU,KAAK,KAAK,EAAE,CAAC;YACjC,YAAY,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;gBAC3B,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAC5B,CAAC,EAAE,QAAQ,CAAC,CAAC;QACf,CAAC;QAED,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;YACrB,OAAO;QACT,CAAC;QAED,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;YACvB,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;YACnD,OAAO;QACT,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAAC;IACvD,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB,CAAC,QAA2B;IACvD,OAAO;QACL,KAAK,EAAE,mBAAmB;QAC1B,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,YAAY,EAAE,QAAQ,CAAC,YAAY;KACpC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { RateLimitDecision, RateLimiterLike } from "../types.js";
2
+ export type FetchLikeHandler<TContext = unknown> = (request: Request, context: TContext) => Response | Promise<Response>;
3
+ export interface FetchRateLimitOptions<TContext = unknown> {
4
+ cost?: (request: Request, context: TContext) => number | Promise<number>;
5
+ key?: (request: Request, context: TContext) => string | Promise<string>;
6
+ onRejected?: (request: Request, context: TContext, decision: RateLimitDecision) => Response | Promise<Response>;
7
+ setHeaders?: boolean;
8
+ skip?: (request: Request, context: TContext) => boolean | Promise<boolean>;
9
+ }
10
+ /**
11
+ * Pattern: Decorator
12
+ * Problem: Fetch-style runtimes need rate limiting without coupling the core to Bun or Next.js.
13
+ * Solution: The adapter decorates standard Request/Response handlers behind a shared contract.
14
+ * Trade-off: One more wrapper around the handler; justified because Bun and Next.js both speak fetch natively.
15
+ */
16
+ export declare function createFetchRateLimit<TContext = unknown>(limiter: RateLimiterLike, options?: FetchRateLimitOptions<TContext>): (handler: FetchLikeHandler<TContext>) => FetchLikeHandler<TContext>;
17
+ //# sourceMappingURL=fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/adapters/fetch.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEtE,MAAM,MAAM,gBAAgB,CAAC,QAAQ,GAAG,OAAO,IAAI,CACjD,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,QAAQ,KACd,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElC,MAAM,WAAW,qBAAqB,CAAC,QAAQ,GAAG,OAAO;IACvD,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzE,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxE,UAAU,CAAC,EAAE,CACX,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,QAAQ,EACjB,QAAQ,EAAE,iBAAiB,KACxB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAClC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,GAAG,OAAO,EACrD,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE,qBAAqB,CAAC,QAAQ,CAAM,GAC5C,CAAC,OAAO,EAAE,gBAAgB,CAAC,QAAQ,CAAC,KAAK,gBAAgB,CAAC,QAAQ,CAAC,CA8BrE"}
@@ -0,0 +1,53 @@
1
+ import { applyHeaders } from "../utils/headers.js";
2
+ import { getDefaultKeyFromFetchRequest } from "../utils/http.js";
3
+ /**
4
+ * Pattern: Decorator
5
+ * Problem: Fetch-style runtimes need rate limiting without coupling the core to Bun or Next.js.
6
+ * Solution: The adapter decorates standard Request/Response handlers behind a shared contract.
7
+ * Trade-off: One more wrapper around the handler; justified because Bun and Next.js both speak fetch natively.
8
+ */
9
+ export function createFetchRateLimit(limiter, options = {}) {
10
+ return (handler) => {
11
+ return async (request, context) => {
12
+ if ((await options.skip?.(request, context)) === true) {
13
+ return handler(request, context);
14
+ }
15
+ const key = (await options.key?.(request, context)) ??
16
+ getDefaultKeyFromFetchRequest(request);
17
+ const cost = (await options.cost?.(request, context)) ?? 1;
18
+ const decision = await limiter.check(key, { cost });
19
+ if (!decision.allowed) {
20
+ const rejected = (await options.onRejected?.(request, context, decision)) ??
21
+ defaultRateLimitResponse(decision);
22
+ return options.setHeaders === false
23
+ ? rejected
24
+ : withRateLimitHeaders(rejected, decision);
25
+ }
26
+ const response = await handler(request, context);
27
+ if (options.setHeaders === false) {
28
+ return response;
29
+ }
30
+ return withRateLimitHeaders(response, decision);
31
+ };
32
+ };
33
+ }
34
+ function defaultRateLimitResponse(decision) {
35
+ return Response.json({
36
+ error: "Too many requests",
37
+ limit: decision.limit,
38
+ remaining: decision.remaining,
39
+ retryAfterMs: decision.retryAfterMs,
40
+ }, { status: 429 });
41
+ }
42
+ function withRateLimitHeaders(response, decision) {
43
+ const headers = new Headers(response.headers);
44
+ applyHeaders((name, value) => {
45
+ headers.set(name, value);
46
+ }, decision);
47
+ return new Response(response.body, {
48
+ status: response.status,
49
+ statusText: response.statusText,
50
+ headers,
51
+ });
52
+ }
53
+ //# sourceMappingURL=fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/adapters/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC;AAoBjE;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAwB,EACxB,UAA2C,EAAE;IAE7C,OAAO,CAAC,OAAO,EAAE,EAAE;QACjB,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;YAChC,IAAI,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;gBACtD,OAAO,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACnC,CAAC;YAED,MAAM,GAAG,GACP,CAAC,MAAM,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBACvC,6BAA6B,CAAC,OAAO,CAAC,CAAC;YACzC,MAAM,IAAI,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC;YAC3D,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;YAEpD,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;gBACtB,MAAM,QAAQ,GACZ,CAAC,MAAM,OAAO,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;oBACxD,wBAAwB,CAAC,QAAQ,CAAC,CAAC;gBACrC,OAAO,OAAO,CAAC,UAAU,KAAK,KAAK;oBACjC,CAAC,CAAC,QAAQ;oBACV,CAAC,CAAC,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;YAC/C,CAAC;YAED,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACjD,IAAI,OAAO,CAAC,UAAU,KAAK,KAAK,EAAE,CAAC;gBACjC,OAAO,QAAQ,CAAC;YAClB,CAAC;YAED,OAAO,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAClD,CAAC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,wBAAwB,CAAC,QAA2B;IAC3D,OAAO,QAAQ,CAAC,IAAI,CAClB;QACE,KAAK,EAAE,mBAAmB;QAC1B,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,YAAY,EAAE,QAAQ,CAAC,YAAY;KACpC,EACD,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB,CAC3B,QAAkB,EAClB,QAA2B;IAE3B,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC9C,YAAY,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAC3B,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC3B,CAAC,EAAE,QAAQ,CAAC,CAAC;IAEb,OAAO,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE;QACjC,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,OAAO;KACR,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { RateLimitDecision, RateLimiterLike } from "../types.js";
2
+ export interface HonoLikeRequest {
3
+ raw: Request;
4
+ header(name: string): string | undefined;
5
+ }
6
+ export interface HonoLikeContext {
7
+ header(name: string, value: string): void;
8
+ json(body: unknown, status?: number): Response;
9
+ req: HonoLikeRequest;
10
+ }
11
+ export interface HonoRateLimitOptions<TContext extends HonoLikeContext = HonoLikeContext> {
12
+ cost?: (context: TContext) => number | Promise<number>;
13
+ key?: (context: TContext) => string | Promise<string>;
14
+ onRejected?: (context: TContext, decision: RateLimitDecision) => Response | Promise<Response>;
15
+ setHeaders?: boolean;
16
+ skip?: (context: TContext) => boolean | Promise<boolean>;
17
+ }
18
+ /**
19
+ * Pattern: Decorator
20
+ * Problem: Hono middleware needs framework-specific request and response hooks while the limiter stays framework-agnostic.
21
+ * Solution: The adapter decorates the core limiter with Hono's context API.
22
+ * Trade-off: Separate Hono wrapper; justified because Bun/Hono is a first-class target for this package.
23
+ */
24
+ export declare function createHonoRateLimit<TContext extends HonoLikeContext = HonoLikeContext>(limiter: RateLimiterLike, options?: HonoRateLimitOptions<TContext>): (context: TContext, next: () => Promise<void>) => Promise<void | Response>;
25
+ //# sourceMappingURL=hono.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../../src/adapters/hono.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEtE,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAC1C;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC/C,GAAG,EAAE,eAAe,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB,CAAC,QAAQ,SAAS,eAAe,GAAG,eAAe;IACtF,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvD,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACtD,UAAU,CAAC,EAAE,CACX,OAAO,EAAE,QAAQ,EACjB,QAAQ,EAAE,iBAAiB,KACxB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAClC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1D;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,SAAS,eAAe,GAAG,eAAe,EACpF,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE,oBAAoB,CAAC,QAAQ,CAAM,GAC3C,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAC,CA4B5E"}
@@ -0,0 +1,40 @@
1
+ import { applyHeaders } from "../utils/headers.js";
2
+ import { getDefaultKeyFromFetchRequest } from "../utils/http.js";
3
+ /**
4
+ * Pattern: Decorator
5
+ * Problem: Hono middleware needs framework-specific request and response hooks while the limiter stays framework-agnostic.
6
+ * Solution: The adapter decorates the core limiter with Hono's context API.
7
+ * Trade-off: Separate Hono wrapper; justified because Bun/Hono is a first-class target for this package.
8
+ */
9
+ export function createHonoRateLimit(limiter, options = {}) {
10
+ return async (context, next) => {
11
+ if ((await options.skip?.(context)) === true) {
12
+ await next();
13
+ return;
14
+ }
15
+ const key = (await options.key?.(context)) ??
16
+ getDefaultKeyFromFetchRequest(context.req.raw);
17
+ const cost = (await options.cost?.(context)) ?? 1;
18
+ const decision = await limiter.check(key, { cost });
19
+ if (options.setHeaders !== false) {
20
+ applyHeaders((name, value) => context.header(name, value), decision);
21
+ }
22
+ if (decision.allowed) {
23
+ await next();
24
+ return;
25
+ }
26
+ if (options.onRejected) {
27
+ return options.onRejected(context, decision);
28
+ }
29
+ return context.json(defaultRateLimitBody(decision), 429);
30
+ };
31
+ }
32
+ function defaultRateLimitBody(decision) {
33
+ return {
34
+ error: "Too many requests",
35
+ limit: decision.limit,
36
+ remaining: decision.remaining,
37
+ retryAfterMs: decision.retryAfterMs,
38
+ };
39
+ }
40
+ //# sourceMappingURL=hono.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hono.js","sourceRoot":"","sources":["../../src/adapters/hono.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC;AAyBjE;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAwB,EACxB,UAA0C,EAAE;IAE5C,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;QAC7B,IAAI,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAC7C,MAAM,IAAI,EAAE,CAAC;YACb,OAAO;QACT,CAAC;QAED,MAAM,GAAG,GACP,CAAC,MAAM,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC;YAC9B,6BAA6B,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACjD,MAAM,IAAI,GAAG,CAAC,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC;QAClD,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;QAEpD,IAAI,OAAO,CAAC,UAAU,KAAK,KAAK,EAAE,CAAC;YACjC,YAAY,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,QAAQ,CAAC,CAAC;QACvE,CAAC;QAED,IAAI,QAAQ,CAAC,OAAO,EAAE,CAAC;YACrB,MAAM,IAAI,EAAE,CAAC;YACb,OAAO;QACT,CAAC;QAED,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;YACvB,OAAO,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC/C,CAAC;QAED,OAAO,OAAO,CAAC,IAAI,CAAC,oBAAoB,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,CAAC;IAC3D,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB,CAAC,QAA2B;IACvD,OAAO;QACL,KAAK,EAAE,mBAAmB;QAC1B,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,YAAY,EAAE,QAAQ,CAAC,YAAY;KACpC,CAAC;AACJ,CAAC"}