@jellyfungus/hono-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/README.md +183 -0
- package/dist/index.cjs +220 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +168 -0
- package/dist/index.d.ts +168 -0
- package/dist/index.js +193 -0
- package/dist/index.js.map +1 -0
- package/dist/store/cloudflare-kv.cjs +89 -0
- package/dist/store/cloudflare-kv.cjs.map +1 -0
- package/dist/store/cloudflare-kv.d.cts +67 -0
- package/dist/store/cloudflare-kv.d.ts +67 -0
- package/dist/store/cloudflare-kv.js +64 -0
- package/dist/store/cloudflare-kv.js.map +1 -0
- package/dist/store/redis.cjs +92 -0
- package/dist/store/redis.cjs.map +1 -0
- package/dist/store/redis.d.cts +59 -0
- package/dist/store/redis.d.ts +59 -0
- package/dist/store/redis.js +67 -0
- package/dist/store/redis.js.map +1 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# @jellyfungus/hono-rate-limiter
|
|
2
|
+
|
|
3
|
+
Rate limiting middleware for [Hono](https://hono.dev) web framework.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@jellyfungus/hono-rate-limiter)
|
|
6
|
+
[](https://github.com/rokasta12/hono-rate-limiter/actions/workflows/ci.yml)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Sliding Window Algorithm** - Accurate rate limiting with Cloudflare's approach
|
|
11
|
+
- **Fixed Window Algorithm** - Simple alternative
|
|
12
|
+
- **Multiple Stores** - Memory (default), Redis, Cloudflare KV
|
|
13
|
+
- **IETF Headers** - RateLimit headers (draft-6, draft-7)
|
|
14
|
+
- **Dynamic Limits** - Per-user or per-route limits
|
|
15
|
+
- **TypeScript** - Full type support
|
|
16
|
+
- **Zero Dependencies** - Only requires Hono as peer dependency
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @jellyfungus/hono-rate-limiter
|
|
22
|
+
# or
|
|
23
|
+
bun add @jellyfungus/hono-rate-limiter
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Basic Usage
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { Hono } from "hono";
|
|
30
|
+
import { rateLimiter } from "@jellyfungus/hono-rate-limiter";
|
|
31
|
+
|
|
32
|
+
const app = new Hono();
|
|
33
|
+
|
|
34
|
+
// 60 requests per minute (default)
|
|
35
|
+
app.use(rateLimiter());
|
|
36
|
+
|
|
37
|
+
// Custom configuration
|
|
38
|
+
app.use(
|
|
39
|
+
"/api/*",
|
|
40
|
+
rateLimiter({
|
|
41
|
+
limit: 100,
|
|
42
|
+
windowMs: 60 * 1000, // 1 minute
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
app.get("/", (c) => c.text("Hello!"));
|
|
47
|
+
|
|
48
|
+
export default app;
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Options
|
|
52
|
+
|
|
53
|
+
| Option | Type | Default | Description |
|
|
54
|
+
| ------------------------ | ------------------------------------ | ------------------ | ----------------------------- |
|
|
55
|
+
| `limit` | `number \| Function` | `60` | Max requests per window |
|
|
56
|
+
| `windowMs` | `number` | `60000` | Window duration in ms |
|
|
57
|
+
| `algorithm` | `'sliding-window' \| 'fixed-window'` | `'sliding-window'` | Rate limiting algorithm |
|
|
58
|
+
| `store` | `RateLimitStore` | `MemoryStore` | Storage backend |
|
|
59
|
+
| `keyGenerator` | `Function` | IP address | Generate client key |
|
|
60
|
+
| `handler` | `Function` | 429 response | Custom rate limit handler |
|
|
61
|
+
| `headers` | `'draft-6' \| 'draft-7' \| false` | `'draft-6'` | Header format |
|
|
62
|
+
| `skip` | `Function` | - | Skip rate limiting |
|
|
63
|
+
| `skipSuccessfulRequests` | `boolean` | `false` | Don't count 2xx responses |
|
|
64
|
+
| `skipFailedRequests` | `boolean` | `false` | Don't count 4xx/5xx responses |
|
|
65
|
+
| `onRateLimited` | `Function` | - | Callback when rate limited |
|
|
66
|
+
|
|
67
|
+
## Stores
|
|
68
|
+
|
|
69
|
+
### Memory Store (Default)
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { rateLimiter } from "@jellyfungus/hono-rate-limiter";
|
|
73
|
+
|
|
74
|
+
app.use(rateLimiter()); // Uses MemoryStore by default
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Redis Store
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { rateLimiter } from "@jellyfungus/hono-rate-limiter";
|
|
81
|
+
import { RedisStore } from "@jellyfungus/hono-rate-limiter/store/redis";
|
|
82
|
+
import Redis from "ioredis";
|
|
83
|
+
|
|
84
|
+
const redis = new Redis(process.env.REDIS_URL);
|
|
85
|
+
|
|
86
|
+
app.use(
|
|
87
|
+
rateLimiter({
|
|
88
|
+
store: new RedisStore({ client: redis }),
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Cloudflare KV Store
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { rateLimiter } from "@jellyfungus/hono-rate-limiter";
|
|
97
|
+
import { CloudflareKVStore } from "@jellyfungus/hono-rate-limiter/store/cloudflare-kv";
|
|
98
|
+
|
|
99
|
+
type Bindings = { RATE_LIMIT_KV: KVNamespace };
|
|
100
|
+
|
|
101
|
+
app.use("*", async (c, next) => {
|
|
102
|
+
const limiter = rateLimiter({
|
|
103
|
+
store: new CloudflareKVStore({ namespace: c.env.RATE_LIMIT_KV }),
|
|
104
|
+
});
|
|
105
|
+
return limiter(c, next);
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Dynamic Limits
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
app.use(
|
|
113
|
+
rateLimiter({
|
|
114
|
+
limit: async (c) => {
|
|
115
|
+
const user = c.get("user");
|
|
116
|
+
if (user?.tier === "premium") return 1000;
|
|
117
|
+
if (user?.tier === "pro") return 500;
|
|
118
|
+
return 100;
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Custom Key Generator
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
app.use(
|
|
128
|
+
rateLimiter({
|
|
129
|
+
keyGenerator: (c) => {
|
|
130
|
+
// Rate limit by API key instead of IP
|
|
131
|
+
return c.req.header("x-api-key") ?? "anonymous";
|
|
132
|
+
},
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Skip Certain Requests
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
app.use(
|
|
141
|
+
rateLimiter({
|
|
142
|
+
skip: (c) => {
|
|
143
|
+
// Don't rate limit health checks
|
|
144
|
+
return c.req.path === "/health";
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Access Rate Limit Info
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
app.get("/status", (c) => {
|
|
154
|
+
const info = c.get("rateLimit");
|
|
155
|
+
return c.json({
|
|
156
|
+
limit: info?.limit,
|
|
157
|
+
remaining: info?.remaining,
|
|
158
|
+
reset: info?.reset,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Custom Handler
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
app.use(
|
|
167
|
+
rateLimiter({
|
|
168
|
+
handler: (c, info) => {
|
|
169
|
+
return c.json(
|
|
170
|
+
{
|
|
171
|
+
error: "Rate limit exceeded",
|
|
172
|
+
retryAfter: Math.ceil((info.reset - Date.now()) / 1000),
|
|
173
|
+
},
|
|
174
|
+
429,
|
|
175
|
+
);
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
MemoryStore: () => MemoryStore,
|
|
24
|
+
getClientIP: () => getClientIP,
|
|
25
|
+
rateLimiter: () => rateLimiter
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
var MemoryStore = class {
|
|
29
|
+
entries = /* @__PURE__ */ new Map();
|
|
30
|
+
windowMs = 6e4;
|
|
31
|
+
cleanupTimer;
|
|
32
|
+
init(windowMs) {
|
|
33
|
+
this.windowMs = windowMs;
|
|
34
|
+
this.cleanupTimer = setInterval(() => {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
for (const [key, entry] of this.entries) {
|
|
37
|
+
if (entry.reset <= now) {
|
|
38
|
+
this.entries.delete(key);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}, 6e4);
|
|
42
|
+
if (typeof this.cleanupTimer.unref === "function") {
|
|
43
|
+
this.cleanupTimer.unref();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
increment(key) {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const existing = this.entries.get(key);
|
|
49
|
+
if (!existing || existing.reset <= now) {
|
|
50
|
+
const reset = now + this.windowMs;
|
|
51
|
+
this.entries.set(key, { count: 1, reset });
|
|
52
|
+
return { count: 1, reset };
|
|
53
|
+
}
|
|
54
|
+
existing.count++;
|
|
55
|
+
return { count: existing.count, reset: existing.reset };
|
|
56
|
+
}
|
|
57
|
+
get(key) {
|
|
58
|
+
const entry = this.entries.get(key);
|
|
59
|
+
if (!entry || entry.reset <= Date.now()) {
|
|
60
|
+
return void 0;
|
|
61
|
+
}
|
|
62
|
+
return { count: entry.count, reset: entry.reset };
|
|
63
|
+
}
|
|
64
|
+
decrement(key) {
|
|
65
|
+
const entry = this.entries.get(key);
|
|
66
|
+
if (entry && entry.count > 0) {
|
|
67
|
+
entry.count--;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
resetKey(key) {
|
|
71
|
+
this.entries.delete(key);
|
|
72
|
+
}
|
|
73
|
+
shutdown() {
|
|
74
|
+
if (this.cleanupTimer) {
|
|
75
|
+
clearInterval(this.cleanupTimer);
|
|
76
|
+
}
|
|
77
|
+
this.entries.clear();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var defaultStore;
|
|
81
|
+
function setHeaders(c, info, format) {
|
|
82
|
+
if (format === false) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const resetSeconds = Math.max(0, Math.ceil((info.reset - Date.now()) / 1e3));
|
|
86
|
+
switch (format) {
|
|
87
|
+
case "draft-7":
|
|
88
|
+
c.header("RateLimit-Limit", String(info.limit));
|
|
89
|
+
c.header("RateLimit-Remaining", String(info.remaining));
|
|
90
|
+
c.header("RateLimit-Reset", String(resetSeconds));
|
|
91
|
+
break;
|
|
92
|
+
case "draft-6":
|
|
93
|
+
default:
|
|
94
|
+
c.header("X-RateLimit-Limit", String(info.limit));
|
|
95
|
+
c.header("X-RateLimit-Remaining", String(info.remaining));
|
|
96
|
+
c.header("X-RateLimit-Reset", String(Math.ceil(info.reset / 1e3)));
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function getClientIP(c) {
|
|
101
|
+
const cfIP = c.req.header("cf-connecting-ip");
|
|
102
|
+
if (cfIP) {
|
|
103
|
+
return cfIP;
|
|
104
|
+
}
|
|
105
|
+
const xRealIP = c.req.header("x-real-ip");
|
|
106
|
+
if (xRealIP) {
|
|
107
|
+
return xRealIP;
|
|
108
|
+
}
|
|
109
|
+
const xff = c.req.header("x-forwarded-for");
|
|
110
|
+
if (xff) {
|
|
111
|
+
return xff.split(",")[0].trim();
|
|
112
|
+
}
|
|
113
|
+
return "unknown";
|
|
114
|
+
}
|
|
115
|
+
function createDefaultResponse(info) {
|
|
116
|
+
const retryAfter = Math.max(0, Math.ceil((info.reset - Date.now()) / 1e3));
|
|
117
|
+
return new Response("Rate limit exceeded", {
|
|
118
|
+
status: 429,
|
|
119
|
+
headers: {
|
|
120
|
+
"Content-Type": "text/plain",
|
|
121
|
+
"Retry-After": String(retryAfter)
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async function checkSlidingWindow(store, key, limit, windowMs) {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
const currentWindowStart = Math.floor(now / windowMs) * windowMs;
|
|
128
|
+
const previousWindowStart = currentWindowStart - windowMs;
|
|
129
|
+
const previousKey = `${key}:${previousWindowStart}`;
|
|
130
|
+
const currentKey = `${key}:${currentWindowStart}`;
|
|
131
|
+
const current = await store.increment(currentKey);
|
|
132
|
+
let previousCount = 0;
|
|
133
|
+
if (store.get) {
|
|
134
|
+
const prev = await store.get(previousKey);
|
|
135
|
+
previousCount = prev?.count ?? 0;
|
|
136
|
+
}
|
|
137
|
+
const elapsedMs = now - currentWindowStart;
|
|
138
|
+
const weight = (windowMs - elapsedMs) / windowMs;
|
|
139
|
+
const estimatedCount = Math.floor(previousCount * weight) + current.count;
|
|
140
|
+
const remaining = Math.max(0, limit - estimatedCount);
|
|
141
|
+
const allowed = estimatedCount <= limit;
|
|
142
|
+
const reset = currentWindowStart + windowMs;
|
|
143
|
+
return {
|
|
144
|
+
allowed,
|
|
145
|
+
info: { limit, remaining, reset }
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function checkFixedWindow(store, key, limit, windowMs) {
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const windowStart = Math.floor(now / windowMs) * windowMs;
|
|
151
|
+
const windowKey = `${key}:${windowStart}`;
|
|
152
|
+
const { count, reset } = await store.increment(windowKey);
|
|
153
|
+
const remaining = Math.max(0, limit - count);
|
|
154
|
+
const allowed = count <= limit;
|
|
155
|
+
return {
|
|
156
|
+
allowed,
|
|
157
|
+
info: { limit, remaining, reset }
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
var rateLimiter = (options) => {
|
|
161
|
+
const opts = {
|
|
162
|
+
limit: 60,
|
|
163
|
+
windowMs: 6e4,
|
|
164
|
+
algorithm: "sliding-window",
|
|
165
|
+
store: void 0,
|
|
166
|
+
keyGenerator: getClientIP,
|
|
167
|
+
handler: void 0,
|
|
168
|
+
headers: "draft-6",
|
|
169
|
+
skip: void 0,
|
|
170
|
+
skipSuccessfulRequests: false,
|
|
171
|
+
skipFailedRequests: false,
|
|
172
|
+
onRateLimited: void 0,
|
|
173
|
+
...options
|
|
174
|
+
};
|
|
175
|
+
const store = opts.store ?? (defaultStore ??= new MemoryStore());
|
|
176
|
+
let initialized = false;
|
|
177
|
+
return async function rateLimiter2(c, next) {
|
|
178
|
+
if (!initialized && store.init) {
|
|
179
|
+
await store.init(opts.windowMs);
|
|
180
|
+
initialized = true;
|
|
181
|
+
}
|
|
182
|
+
if (opts.skip) {
|
|
183
|
+
const shouldSkip = await opts.skip(c);
|
|
184
|
+
if (shouldSkip) {
|
|
185
|
+
return next();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const key = await opts.keyGenerator(c);
|
|
189
|
+
const limit = typeof opts.limit === "function" ? await opts.limit(c) : opts.limit;
|
|
190
|
+
const { allowed, info } = opts.algorithm === "sliding-window" ? await checkSlidingWindow(store, key, limit, opts.windowMs) : await checkFixedWindow(store, key, limit, opts.windowMs);
|
|
191
|
+
c.set("rateLimit", info);
|
|
192
|
+
setHeaders(c, info, opts.headers);
|
|
193
|
+
if (!allowed) {
|
|
194
|
+
if (opts.onRateLimited) {
|
|
195
|
+
await opts.onRateLimited(c, info);
|
|
196
|
+
}
|
|
197
|
+
if (opts.handler) {
|
|
198
|
+
return opts.handler(c, info);
|
|
199
|
+
}
|
|
200
|
+
return createDefaultResponse(info);
|
|
201
|
+
}
|
|
202
|
+
await next();
|
|
203
|
+
if (opts.skipSuccessfulRequests || opts.skipFailedRequests) {
|
|
204
|
+
const status = c.res.status;
|
|
205
|
+
const shouldDecrement = opts.skipSuccessfulRequests && status >= 200 && status < 300 || opts.skipFailedRequests && status >= 400;
|
|
206
|
+
if (shouldDecrement && store.decrement) {
|
|
207
|
+
const windowStart = Math.floor(Date.now() / opts.windowMs) * opts.windowMs;
|
|
208
|
+
const windowKey = `${key}:${windowStart}`;
|
|
209
|
+
await store.decrement(windowKey);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
215
|
+
0 && (module.exports = {
|
|
216
|
+
MemoryStore,
|
|
217
|
+
getClientIP,
|
|
218
|
+
rateLimiter
|
|
219
|
+
});
|
|
220
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @module\n * Rate Limit Middleware for Hono.\n */\n\nimport type { Context, Env, MiddlewareHandler } from \"hono\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Rate limit information for a single request\n */\nexport type RateLimitInfo = {\n /** Maximum requests allowed in window */\n limit: number;\n /** Remaining requests in current window */\n remaining: number;\n /** Unix timestamp (ms) when window resets */\n reset: number;\n};\n\n/**\n * Result from store increment operation\n */\nexport type StoreResult = {\n /** Current request count in window */\n count: number;\n /** When the window resets (Unix timestamp ms) */\n reset: number;\n};\n\n/**\n * Header format versions\n */\nexport type HeadersFormat =\n | \"draft-6\" // X-RateLimit-* headers\n | \"draft-7\" // RateLimit-* without structured fields\n | false; // Disable headers\n\n/**\n * Rate limit algorithm\n */\nexport type Algorithm = \"fixed-window\" | \"sliding-window\";\n\n/**\n * Store interface for rate limit state\n */\nexport type RateLimitStore = {\n /**\n * Initialize store. Called once before first use.\n */\n init?: (windowMs: number) => void | Promise<void>;\n\n /**\n * Increment counter for key and return current state.\n */\n increment: (key: string) => StoreResult | Promise<StoreResult>;\n\n /**\n * Decrement counter for key.\n */\n decrement?: (key: string) => void | Promise<void>;\n\n /**\n * Reset a specific key.\n */\n resetKey: (key: string) => void | Promise<void>;\n\n /**\n * Get current state for key.\n */\n get?: (\n key: string,\n ) => StoreResult | Promise<StoreResult | undefined> | undefined;\n\n /**\n * Graceful shutdown.\n */\n shutdown?: () => void | Promise<void>;\n};\n\n/**\n * Options for rate limit middleware\n */\nexport type RateLimitOptions<E extends Env = Env> = {\n /**\n * Maximum requests allowed in the time window.\n * @default 60\n */\n limit?: number | ((c: Context<E>) => number | Promise<number>);\n\n /**\n * Time window in milliseconds.\n * @default 60000 (1 minute)\n */\n windowMs?: number;\n\n /**\n * Rate limiting algorithm.\n * @default 'sliding-window'\n */\n algorithm?: Algorithm;\n\n /**\n * Storage backend for rate limit state.\n * @default MemoryStore\n */\n store?: RateLimitStore;\n\n /**\n * Generate unique key for each client.\n * @default IP address from headers\n */\n keyGenerator?: (c: Context<E>) => string | Promise<string>;\n\n /**\n * Handler called when rate limit is exceeded.\n */\n handler?: (\n c: Context<E>,\n info: RateLimitInfo,\n ) => Response | Promise<Response>;\n\n /**\n * HTTP header format to use.\n * @default 'draft-6'\n */\n headers?: HeadersFormat;\n\n /**\n * Skip rate limiting for certain requests.\n */\n skip?: (c: Context<E>) => boolean | Promise<boolean>;\n\n /**\n * Don't count successful (2xx) requests against limit.\n * @default false\n */\n skipSuccessfulRequests?: boolean;\n\n /**\n * Don't count failed (4xx, 5xx) requests against limit.\n * @default false\n */\n skipFailedRequests?: boolean;\n\n /**\n * Callback when a request is rate limited.\n */\n onRateLimited?: (c: Context<E>, info: RateLimitInfo) => void | Promise<void>;\n};\n\n// ============================================================================\n// Context Variable Type Extension\n// ============================================================================\n\ndeclare module \"hono\" {\n interface ContextVariableMap {\n rateLimit?: RateLimitInfo;\n }\n}\n\n// ============================================================================\n// Memory Store\n// ============================================================================\n\ntype MemoryEntry = {\n count: number;\n reset: number;\n};\n\n/**\n * In-memory store for rate limiting.\n * Suitable for single-instance deployments.\n */\nexport class MemoryStore implements RateLimitStore {\n private entries = new Map<string, MemoryEntry>();\n private windowMs = 60_000;\n private cleanupTimer?: ReturnType<typeof setInterval>;\n\n init(windowMs: number): void {\n this.windowMs = windowMs;\n\n // Cleanup expired entries every minute\n this.cleanupTimer = setInterval(() => {\n const now = Date.now();\n for (const [key, entry] of this.entries) {\n if (entry.reset <= now) {\n this.entries.delete(key);\n }\n }\n }, 60_000);\n\n // Don't keep process alive for cleanup\n if (typeof this.cleanupTimer.unref === \"function\") {\n this.cleanupTimer.unref();\n }\n }\n\n increment(key: string): StoreResult {\n const now = Date.now();\n const existing = this.entries.get(key);\n\n if (!existing || existing.reset <= now) {\n // New window\n const reset = now + this.windowMs;\n this.entries.set(key, { count: 1, reset });\n return { count: 1, reset };\n }\n\n // Increment existing\n existing.count++;\n return { count: existing.count, reset: existing.reset };\n }\n\n get(key: string): StoreResult | undefined {\n const entry = this.entries.get(key);\n if (!entry || entry.reset <= Date.now()) {\n return undefined;\n }\n return { count: entry.count, reset: entry.reset };\n }\n\n decrement(key: string): void {\n const entry = this.entries.get(key);\n if (entry && entry.count > 0) {\n entry.count--;\n }\n }\n\n resetKey(key: string): void {\n this.entries.delete(key);\n }\n\n shutdown(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n }\n this.entries.clear();\n }\n}\n\n// Singleton default store\nlet defaultStore: MemoryStore | undefined;\n\n// ============================================================================\n// Header Generation\n// ============================================================================\n\nfunction setHeaders(\n c: Context,\n info: RateLimitInfo,\n format: HeadersFormat,\n): void {\n if (format === false) {\n return;\n }\n\n const resetSeconds = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n switch (format) {\n case \"draft-7\":\n c.header(\"RateLimit-Limit\", String(info.limit));\n c.header(\"RateLimit-Remaining\", String(info.remaining));\n c.header(\"RateLimit-Reset\", String(resetSeconds));\n break;\n\n case \"draft-6\":\n default:\n c.header(\"X-RateLimit-Limit\", String(info.limit));\n c.header(\"X-RateLimit-Remaining\", String(info.remaining));\n c.header(\"X-RateLimit-Reset\", String(Math.ceil(info.reset / 1000)));\n break;\n }\n}\n\n// ============================================================================\n// Default Key Generator\n// ============================================================================\n\nfunction getClientIP(c: Context): string {\n // Platform-specific headers (most reliable)\n const cfIP = c.req.header(\"cf-connecting-ip\");\n if (cfIP) {\n return cfIP;\n }\n\n const xRealIP = c.req.header(\"x-real-ip\");\n if (xRealIP) {\n return xRealIP;\n }\n\n // X-Forwarded-For - take first IP\n const xff = c.req.header(\"x-forwarded-for\");\n if (xff) {\n return xff.split(\",\")[0].trim();\n }\n\n return \"unknown\";\n}\n\n// ============================================================================\n// Default Handler\n// ============================================================================\n\nfunction createDefaultResponse(info: RateLimitInfo): Response {\n const retryAfter = Math.max(0, Math.ceil((info.reset - Date.now()) / 1000));\n\n return new Response(\"Rate limit exceeded\", {\n status: 429,\n headers: {\n \"Content-Type\": \"text/plain\",\n \"Retry-After\": String(retryAfter),\n },\n });\n}\n\n// ============================================================================\n// Sliding Window Algorithm\n// ============================================================================\n\nasync function checkSlidingWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const currentWindowStart = Math.floor(now / windowMs) * windowMs;\n const previousWindowStart = currentWindowStart - windowMs;\n\n const previousKey = `${key}:${previousWindowStart}`;\n const currentKey = `${key}:${currentWindowStart}`;\n\n // Increment current window\n const current = await store.increment(currentKey);\n\n // Get previous window (may not exist)\n let previousCount = 0;\n if (store.get) {\n const prev = await store.get(previousKey);\n previousCount = prev?.count ?? 0;\n }\n\n // Cloudflare's weighted formula\n const elapsedMs = now - currentWindowStart;\n const weight = (windowMs - elapsedMs) / windowMs;\n const estimatedCount = Math.floor(previousCount * weight) + current.count;\n\n const remaining = Math.max(0, limit - estimatedCount);\n const allowed = estimatedCount <= limit;\n const reset = currentWindowStart + windowMs;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Fixed Window Algorithm\n// ============================================================================\n\nasync function checkFixedWindow(\n store: RateLimitStore,\n key: string,\n limit: number,\n windowMs: number,\n): Promise<{ allowed: boolean; info: RateLimitInfo }> {\n const now = Date.now();\n const windowStart = Math.floor(now / windowMs) * windowMs;\n const windowKey = `${key}:${windowStart}`;\n\n const { count, reset } = await store.increment(windowKey);\n\n const remaining = Math.max(0, limit - count);\n const allowed = count <= limit;\n\n return {\n allowed,\n info: { limit, remaining, reset },\n };\n}\n\n// ============================================================================\n// Main Middleware\n// ============================================================================\n\n/**\n * Rate Limit Middleware for Hono.\n *\n * @param {RateLimitOptions} [options] - Configuration options\n * @returns {MiddlewareHandler} Middleware handler\n *\n * @example\n * ```ts\n * import { Hono } from 'hono'\n * import { rateLimiter } from 'hono-rate-limit'\n *\n * const app = new Hono()\n *\n * // Basic usage - 60 requests per minute\n * app.use(rateLimiter())\n *\n * // Custom configuration\n * app.use('/api/*', rateLimiter({\n * limit: 100,\n * windowMs: 60 * 1000,\n * }))\n * ```\n */\nexport const rateLimiter = <E extends Env = Env>(\n options?: RateLimitOptions<E>,\n): MiddlewareHandler<E> => {\n // Merge with defaults\n const opts = {\n limit: 60 as number | ((c: Context<E>) => number | Promise<number>),\n windowMs: 60_000,\n algorithm: \"sliding-window\" as Algorithm,\n store: undefined as RateLimitStore | undefined,\n keyGenerator: getClientIP as (c: Context<E>) => string | Promise<string>,\n handler: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => Response | Promise<Response>)\n | undefined,\n headers: \"draft-6\" as HeadersFormat,\n skip: undefined as\n | ((c: Context<E>) => boolean | Promise<boolean>)\n | undefined,\n skipSuccessfulRequests: false,\n skipFailedRequests: false,\n onRateLimited: undefined as\n | ((c: Context<E>, info: RateLimitInfo) => void | Promise<void>)\n | undefined,\n ...options,\n };\n\n // Use default store if none provided\n const store = opts.store ?? (defaultStore ??= new MemoryStore());\n\n // Track initialization\n let initialized = false;\n\n return async function rateLimiter(c, next) {\n // Initialize store on first request\n if (!initialized && store.init) {\n await store.init(opts.windowMs);\n initialized = true;\n }\n\n // Check if should skip\n if (opts.skip) {\n const shouldSkip = await opts.skip(c);\n if (shouldSkip) {\n return next();\n }\n }\n\n // Generate key\n const key = await opts.keyGenerator(c);\n\n // Get limit (may be dynamic)\n const limit =\n typeof opts.limit === \"function\" ? await opts.limit(c) : opts.limit;\n\n // Check rate limit\n const { allowed, info } =\n opts.algorithm === \"sliding-window\"\n ? await checkSlidingWindow(store, key, limit, opts.windowMs)\n : await checkFixedWindow(store, key, limit, opts.windowMs);\n\n // Set context variable for downstream middleware\n c.set(\"rateLimit\", info);\n\n // Set headers\n setHeaders(c, info, opts.headers);\n\n // Handle rate limited\n if (!allowed) {\n // Fire callback\n if (opts.onRateLimited) {\n await opts.onRateLimited(c, info);\n }\n\n // Custom handler or default\n if (opts.handler) {\n return opts.handler(c, info);\n }\n return createDefaultResponse(info);\n }\n\n // Continue\n await next();\n\n // Handle skip options after response\n if (opts.skipSuccessfulRequests || opts.skipFailedRequests) {\n const status = c.res.status;\n const shouldDecrement =\n (opts.skipSuccessfulRequests && status >= 200 && status < 300) ||\n (opts.skipFailedRequests && status >= 400);\n\n if (shouldDecrement && store.decrement) {\n const windowStart =\n Math.floor(Date.now() / opts.windowMs) * opts.windowMs;\n const windowKey = `${key}:${windowStart}`;\n await store.decrement(windowKey);\n }\n }\n };\n};\n\n// ============================================================================\n// Exports\n// ============================================================================\n\nexport { getClientIP };\n"],"mappings":"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","names":["rateLimiter"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Env, Context, MiddlewareHandler } from 'hono';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module
|
|
5
|
+
* Rate Limit Middleware for Hono.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Rate limit information for a single request
|
|
10
|
+
*/
|
|
11
|
+
type RateLimitInfo = {
|
|
12
|
+
/** Maximum requests allowed in window */
|
|
13
|
+
limit: number;
|
|
14
|
+
/** Remaining requests in current window */
|
|
15
|
+
remaining: number;
|
|
16
|
+
/** Unix timestamp (ms) when window resets */
|
|
17
|
+
reset: number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Result from store increment operation
|
|
21
|
+
*/
|
|
22
|
+
type StoreResult = {
|
|
23
|
+
/** Current request count in window */
|
|
24
|
+
count: number;
|
|
25
|
+
/** When the window resets (Unix timestamp ms) */
|
|
26
|
+
reset: number;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Header format versions
|
|
30
|
+
*/
|
|
31
|
+
type HeadersFormat = "draft-6" | "draft-7" | false;
|
|
32
|
+
/**
|
|
33
|
+
* Rate limit algorithm
|
|
34
|
+
*/
|
|
35
|
+
type Algorithm = "fixed-window" | "sliding-window";
|
|
36
|
+
/**
|
|
37
|
+
* Store interface for rate limit state
|
|
38
|
+
*/
|
|
39
|
+
type RateLimitStore = {
|
|
40
|
+
/**
|
|
41
|
+
* Initialize store. Called once before first use.
|
|
42
|
+
*/
|
|
43
|
+
init?: (windowMs: number) => void | Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Increment counter for key and return current state.
|
|
46
|
+
*/
|
|
47
|
+
increment: (key: string) => StoreResult | Promise<StoreResult>;
|
|
48
|
+
/**
|
|
49
|
+
* Decrement counter for key.
|
|
50
|
+
*/
|
|
51
|
+
decrement?: (key: string) => void | Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Reset a specific key.
|
|
54
|
+
*/
|
|
55
|
+
resetKey: (key: string) => void | Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Get current state for key.
|
|
58
|
+
*/
|
|
59
|
+
get?: (key: string) => StoreResult | Promise<StoreResult | undefined> | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Graceful shutdown.
|
|
62
|
+
*/
|
|
63
|
+
shutdown?: () => void | Promise<void>;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Options for rate limit middleware
|
|
67
|
+
*/
|
|
68
|
+
type RateLimitOptions<E extends Env = Env> = {
|
|
69
|
+
/**
|
|
70
|
+
* Maximum requests allowed in the time window.
|
|
71
|
+
* @default 60
|
|
72
|
+
*/
|
|
73
|
+
limit?: number | ((c: Context<E>) => number | Promise<number>);
|
|
74
|
+
/**
|
|
75
|
+
* Time window in milliseconds.
|
|
76
|
+
* @default 60000 (1 minute)
|
|
77
|
+
*/
|
|
78
|
+
windowMs?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Rate limiting algorithm.
|
|
81
|
+
* @default 'sliding-window'
|
|
82
|
+
*/
|
|
83
|
+
algorithm?: Algorithm;
|
|
84
|
+
/**
|
|
85
|
+
* Storage backend for rate limit state.
|
|
86
|
+
* @default MemoryStore
|
|
87
|
+
*/
|
|
88
|
+
store?: RateLimitStore;
|
|
89
|
+
/**
|
|
90
|
+
* Generate unique key for each client.
|
|
91
|
+
* @default IP address from headers
|
|
92
|
+
*/
|
|
93
|
+
keyGenerator?: (c: Context<E>) => string | Promise<string>;
|
|
94
|
+
/**
|
|
95
|
+
* Handler called when rate limit is exceeded.
|
|
96
|
+
*/
|
|
97
|
+
handler?: (c: Context<E>, info: RateLimitInfo) => Response | Promise<Response>;
|
|
98
|
+
/**
|
|
99
|
+
* HTTP header format to use.
|
|
100
|
+
* @default 'draft-6'
|
|
101
|
+
*/
|
|
102
|
+
headers?: HeadersFormat;
|
|
103
|
+
/**
|
|
104
|
+
* Skip rate limiting for certain requests.
|
|
105
|
+
*/
|
|
106
|
+
skip?: (c: Context<E>) => boolean | Promise<boolean>;
|
|
107
|
+
/**
|
|
108
|
+
* Don't count successful (2xx) requests against limit.
|
|
109
|
+
* @default false
|
|
110
|
+
*/
|
|
111
|
+
skipSuccessfulRequests?: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Don't count failed (4xx, 5xx) requests against limit.
|
|
114
|
+
* @default false
|
|
115
|
+
*/
|
|
116
|
+
skipFailedRequests?: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Callback when a request is rate limited.
|
|
119
|
+
*/
|
|
120
|
+
onRateLimited?: (c: Context<E>, info: RateLimitInfo) => void | Promise<void>;
|
|
121
|
+
};
|
|
122
|
+
declare module "hono" {
|
|
123
|
+
interface ContextVariableMap {
|
|
124
|
+
rateLimit?: RateLimitInfo;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* In-memory store for rate limiting.
|
|
129
|
+
* Suitable for single-instance deployments.
|
|
130
|
+
*/
|
|
131
|
+
declare class MemoryStore implements RateLimitStore {
|
|
132
|
+
private entries;
|
|
133
|
+
private windowMs;
|
|
134
|
+
private cleanupTimer?;
|
|
135
|
+
init(windowMs: number): void;
|
|
136
|
+
increment(key: string): StoreResult;
|
|
137
|
+
get(key: string): StoreResult | undefined;
|
|
138
|
+
decrement(key: string): void;
|
|
139
|
+
resetKey(key: string): void;
|
|
140
|
+
shutdown(): void;
|
|
141
|
+
}
|
|
142
|
+
declare function getClientIP(c: Context): string;
|
|
143
|
+
/**
|
|
144
|
+
* Rate Limit Middleware for Hono.
|
|
145
|
+
*
|
|
146
|
+
* @param {RateLimitOptions} [options] - Configuration options
|
|
147
|
+
* @returns {MiddlewareHandler} Middleware handler
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* import { Hono } from 'hono'
|
|
152
|
+
* import { rateLimiter } from 'hono-rate-limit'
|
|
153
|
+
*
|
|
154
|
+
* const app = new Hono()
|
|
155
|
+
*
|
|
156
|
+
* // Basic usage - 60 requests per minute
|
|
157
|
+
* app.use(rateLimiter())
|
|
158
|
+
*
|
|
159
|
+
* // Custom configuration
|
|
160
|
+
* app.use('/api/*', rateLimiter({
|
|
161
|
+
* limit: 100,
|
|
162
|
+
* windowMs: 60 * 1000,
|
|
163
|
+
* }))
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
declare const rateLimiter: <E extends Env = Env>(options?: RateLimitOptions<E>) => MiddlewareHandler<E>;
|
|
167
|
+
|
|
168
|
+
export { type Algorithm, type HeadersFormat, MemoryStore, type RateLimitInfo, type RateLimitOptions, type RateLimitStore, type StoreResult, getClientIP, rateLimiter };
|