@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.
- package/LICENSE +21 -0
- package/README.md +310 -0
- package/dist/adapters/express.d.ts +26 -0
- package/dist/adapters/express.d.ts.map +1 -0
- package/dist/adapters/express.js +51 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/fastify.d.ts +24 -0
- package/dist/adapters/fastify.d.ts.map +1 -0
- package/dist/adapters/fastify.js +40 -0
- package/dist/adapters/fastify.js.map +1 -0
- package/dist/adapters/fetch.d.ts +17 -0
- package/dist/adapters/fetch.d.ts.map +1 -0
- package/dist/adapters/fetch.js +53 -0
- package/dist/adapters/fetch.js.map +1 -0
- package/dist/adapters/hono.d.ts +25 -0
- package/dist/adapters/hono.d.ts.map +1 -0
- package/dist/adapters/hono.js +40 -0
- package/dist/adapters/hono.js.map +1 -0
- package/dist/adapters/nest.d.ts +32 -0
- package/dist/adapters/nest.d.ts.map +1 -0
- package/dist/adapters/nest.js +47 -0
- package/dist/adapters/nest.js.map +1 -0
- package/dist/adapters/next.d.ts +12 -0
- package/dist/adapters/next.d.ts.map +1 -0
- package/dist/adapters/next.js +11 -0
- package/dist/adapters/next.js.map +1 -0
- package/dist/core/cached-rate-limiter-proxy.d.ts +17 -0
- package/dist/core/cached-rate-limiter-proxy.d.ts.map +1 -0
- package/dist/core/cached-rate-limiter-proxy.js +47 -0
- package/dist/core/cached-rate-limiter-proxy.js.map +1 -0
- package/dist/core/create-rate-limiter.d.ts +11 -0
- package/dist/core/create-rate-limiter.d.ts.map +1 -0
- package/dist/core/create-rate-limiter.js +34 -0
- package/dist/core/create-rate-limiter.js.map +1 -0
- package/dist/core/rate-limiter-builder.d.ts +27 -0
- package/dist/core/rate-limiter-builder.d.ts.map +1 -0
- package/dist/core/rate-limiter-builder.js +73 -0
- package/dist/core/rate-limiter-builder.js.map +1 -0
- package/dist/core/rate-limiter.d.ts +23 -0
- package/dist/core/rate-limiter.d.ts.map +1 -0
- package/dist/core/rate-limiter.js +59 -0
- package/dist/core/rate-limiter.js.map +1 -0
- package/dist/core/strategy-factory.d.ts +18 -0
- package/dist/core/strategy-factory.d.ts.map +1 -0
- package/dist/core/strategy-factory.js +17 -0
- package/dist/core/strategy-factory.js.map +1 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/stores/memory-store.d.ts +9 -0
- package/dist/stores/memory-store.d.ts.map +1 -0
- package/dist/stores/memory-store.js +24 -0
- package/dist/stores/memory-store.js.map +1 -0
- package/dist/stores/rate-limit-store.d.ts +6 -0
- package/dist/stores/rate-limit-store.d.ts.map +1 -0
- package/dist/stores/rate-limit-store.js +2 -0
- package/dist/stores/rate-limit-store.js.map +1 -0
- package/dist/stores/redis-store.d.ts +26 -0
- package/dist/stores/redis-store.d.ts.map +1 -0
- package/dist/stores/redis-store.js +41 -0
- package/dist/stores/redis-store.js.map +1 -0
- package/dist/strategies/sliding-window-strategy.d.ts +31 -0
- package/dist/strategies/sliding-window-strategy.d.ts.map +1 -0
- package/dist/strategies/sliding-window-strategy.js +212 -0
- package/dist/strategies/sliding-window-strategy.js.map +1 -0
- package/dist/strategies/token-bucket-strategy.d.ts +30 -0
- package/dist/strategies/token-bucket-strategy.d.ts.map +1 -0
- package/dist/strategies/token-bucket-strategy.js +154 -0
- package/dist/strategies/token-bucket-strategy.js.map +1 -0
- package/dist/types.d.ts +48 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/headers.d.ts +4 -0
- package/dist/utils/headers.d.ts.map +1 -0
- package/dist/utils/headers.js +16 -0
- package/dist/utils/headers.js.map +1 -0
- package/dist/utils/http.d.ts +11 -0
- package/dist/utils/http.d.ts.map +1 -0
- package/dist/utils/http.js +41 -0
- package/dist/utils/http.js.map +1 -0
- package/dist/utils/math.d.ts +4 -0
- package/dist/utils/math.d.ts.map +1 -0
- package/dist/utils/math.js +21 -0
- package/dist/utils/math.js.map +1 -0
- 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"}
|