hono-cloudflare-rate-limit 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +109 -2
- package/dist/index.d.mts +27 -0
- package/dist/index.mjs +14 -0
- package/package.json +67 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 binochoi
|
|
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
CHANGED
|
@@ -1,3 +1,110 @@
|
|
|
1
|
-
#
|
|
1
|
+
# hono-cloudflare-rate-limit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/hono-cloudflare-rate-limit)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
A tiny, env-agnostic **[Hono](https://hono.dev) rate limit middleware for Cloudflare Workers**, built on the native [Workers Rate Limiting binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) (`ratelimits` in `wrangler.jsonc` / `wrangler.toml`). Throttle API requests per IP, per user, or per API key and return `429 Too Many Requests` when the limit is exceeded — useful for protecting login, signup, password reset, and OTP endpoints from brute-force attacks and abuse at the edge.
|
|
7
|
+
|
|
8
|
+
The middleware only calls the binding, throws on overflow, and exposes an on/off hook. **You decide which binding to use, how to build the counter key, and when to skip limiting** — it never reads a fixed `env` shape, so it works in any Hono app on Cloudflare Workers.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install hono-cloudflare-rate-limit
|
|
14
|
+
# pnpm add hono-cloudflare-rate-limit
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`hono` and `@cloudflare/workers-types` are peer dependencies.
|
|
18
|
+
|
|
19
|
+
## Runtime requirement
|
|
20
|
+
|
|
21
|
+
The `limit()` binding is a **Cloudflare Workers (workerd) only API**. It does not exist on Node.js, Bun, Deno, or other regular servers, so this middleware won't work there.
|
|
22
|
+
|
|
23
|
+
To rate limit an origin server through Cloudflare instead:
|
|
24
|
+
|
|
25
|
+
- Use **WAF Rate Limiting Rules** at the edge, or put a **thin Worker** in front of the origin.
|
|
26
|
+
- Lock the origin down to **Cloudflare IP ranges + Authenticated Origin Pulls (mTLS)**. Only then can you trust `CF-Connecting-IP` — requests that bypass the edge can omit or spoof that header.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
Declare a rate limit binding in `wrangler.jsonc`:
|
|
31
|
+
|
|
32
|
+
```jsonc
|
|
33
|
+
{
|
|
34
|
+
"ratelimits": [
|
|
35
|
+
{
|
|
36
|
+
"name": "RL",
|
|
37
|
+
"namespace_id": "1001",
|
|
38
|
+
"simple": { "limit": 100, "period": 60 }
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then apply the middleware:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { Hono } from "hono";
|
|
48
|
+
import type { RateLimit } from "@cloudflare/workers-types";
|
|
49
|
+
import { rateLimit, cfConnectingIp } from "hono-cloudflare-rate-limit";
|
|
50
|
+
|
|
51
|
+
type AppEnv = {
|
|
52
|
+
Bindings: { RL: RateLimit; APP_URL: string };
|
|
53
|
+
Variables: {};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const app = new Hono<AppEnv>();
|
|
57
|
+
|
|
58
|
+
const guard = rateLimit<AppEnv>({
|
|
59
|
+
// Return false to skip limiting, e.g. in local development.
|
|
60
|
+
enabled: (c) => !c.env.APP_URL.startsWith("http://localhost"),
|
|
61
|
+
limiter: (c) => c.env.RL,
|
|
62
|
+
key: (c) => {
|
|
63
|
+
const ip = cfConnectingIp(c);
|
|
64
|
+
if (!ip) throw new Error("unknown client");
|
|
65
|
+
return `api:${ip}`;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
app.use("/api/*", guard);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
By default an exceeded limit throws `HTTPException(429, "too many requests")`. Pass `onExceeded` to return a custom response.
|
|
73
|
+
|
|
74
|
+
## Use cases
|
|
75
|
+
|
|
76
|
+
- **Per-IP API limit** — key by `api:${ip}` on `/api/*`.
|
|
77
|
+
- **Stricter limits on sensitive routes** — bind a separate, tighter limiter to login, signup, password reset, or OTP endpoints to slow down brute-force and spam.
|
|
78
|
+
- **Per-user or per-API-key quotas** — key by `user:${userId}` or `key:${apiKey}` for plan-based limits.
|
|
79
|
+
- **Disable in development** — return `false` from `enabled` for localhost.
|
|
80
|
+
- **Custom 429 response** — use `onExceeded` to send a JSON error body or a `Retry-After` header:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
rateLimit<AppEnv>({
|
|
84
|
+
limiter: (c) => c.env.RL,
|
|
85
|
+
key: (c) => `login:${cfConnectingIp(c) ?? "unknown"}`,
|
|
86
|
+
onExceeded: (c) => {
|
|
87
|
+
c.header("Retry-After", "60");
|
|
88
|
+
return c.json({ error: "rate_limited" }, 429);
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## API
|
|
94
|
+
|
|
95
|
+
### `rateLimit(options): MiddlewareHandler`
|
|
96
|
+
|
|
97
|
+
| Option | Type | Description |
|
|
98
|
+
| ------------ | --------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
99
|
+
| `limiter` | `(c) => RateLimit` | Picks the rate limit binding from `env`. |
|
|
100
|
+
| `key` | `(c) => string` | Builds the counter key. If it throws (e.g. client can't be identified), the error propagates as-is. |
|
|
101
|
+
| `enabled?` | `(c) => boolean` | Return `false` to skip limiting for this request. Defaults to always on. |
|
|
102
|
+
| `onExceeded?` | `(c) => Response \| Promise<Response>` | Response to send when the limit is exceeded. Defaults to throwing `HTTPException(429)`. |
|
|
103
|
+
|
|
104
|
+
### `cfConnectingIp(c): string | null`
|
|
105
|
+
|
|
106
|
+
Returns the `CF-Connecting-IP` header set by the Cloudflare edge. It is `null` for requests that didn't pass through the edge (local dev, service bindings, etc.).
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
[MIT](./LICENSE)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Context, MiddlewareHandler } from "hono";
|
|
2
|
+
import { RateLimit } from "@cloudflare/workers-types";
|
|
3
|
+
export type RateLimitOptions<E extends {
|
|
4
|
+
Bindings: object;
|
|
5
|
+
Variables: object;
|
|
6
|
+
} = {
|
|
7
|
+
Bindings: object;
|
|
8
|
+
Variables: object;
|
|
9
|
+
}> = {
|
|
10
|
+
/** env 에서 쓸 RateLimit 바인딩을 고른다. */
|
|
11
|
+
limiter: (c: Context<E>) => RateLimit;
|
|
12
|
+
/** 카운터 키를 파생한다. 키를 만들 수 없는 상황(예: 클라이언트 식별 불가)의
|
|
13
|
+
* 처리는 소비자 책임 — 여기서 throw 하면 미들웨어가 그대로 전파한다. */
|
|
14
|
+
key: (c: Context<E>) => string;
|
|
15
|
+
/** false 를 반환하면 이 요청은 리밋을 건너뛴다(예: 개발환경). 생략하면 항상 적용. */
|
|
16
|
+
enabled?: (c: Context<E>) => boolean;
|
|
17
|
+
/** 초과 시 동작. 생략하면 HTTPException(429, "too many requests") 를 던진다. */
|
|
18
|
+
onExceeded?: (c: Context<E>) => Response | Promise<Response>;
|
|
19
|
+
};
|
|
20
|
+
/** RateLimit 바인딩으로 요청을 제한하는 Hono 미들웨어를 만든다. */
|
|
21
|
+
export declare const rateLimit: <E extends {
|
|
22
|
+
Bindings: object;
|
|
23
|
+
Variables: object;
|
|
24
|
+
}>(opts: RateLimitOptions<E>) => MiddlewareHandler<E>;
|
|
25
|
+
/** Cloudflare 엣지가 찍는 표준 클라이언트 IP 헤더. 엣지를 거치지 않은 요청
|
|
26
|
+
* (로컬 dev, service binding 등)에는 없으므로 null 이 반환될 수 있다. */
|
|
27
|
+
export declare const cfConnectingIp: (c: Context) => string | null;
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { HTTPException } from "hono/http-exception";
|
|
2
|
+
const rateLimit = (opts) => {
|
|
3
|
+
return async (c, next) => {
|
|
4
|
+
if (opts.enabled && !opts.enabled(c)) return next();
|
|
5
|
+
const { success } = await opts.limiter(c).limit({ key: opts.key(c) });
|
|
6
|
+
if (!success) {
|
|
7
|
+
if (opts.onExceeded) return opts.onExceeded(c);
|
|
8
|
+
throw new HTTPException(429, { message: "too many requests" });
|
|
9
|
+
}
|
|
10
|
+
return next();
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
const cfConnectingIp = (c) => c.req.header("CF-Connecting-IP") ?? null;
|
|
14
|
+
export { cfConnectingIp, rateLimit };
|
package/package.json
CHANGED
|
@@ -1,6 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hono-cloudflare-rate-limit",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Hono rate limit middleware for Cloudflare Workers using the native Rate Limiting binding. Throttle requests per IP, user, or API key and return 429 Too Many Requests.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "binochoi",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/binochoi/hono-cloudflare-rate-limit.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/binochoi/hono-cloudflare-rate-limit#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/binochoi/hono-cloudflare-rate-limit/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"hono",
|
|
18
|
+
"hono-middleware",
|
|
19
|
+
"middleware",
|
|
20
|
+
"rate-limit",
|
|
21
|
+
"rate-limiter",
|
|
22
|
+
"ratelimit",
|
|
23
|
+
"rate-limiting",
|
|
24
|
+
"throttle",
|
|
25
|
+
"throttling",
|
|
26
|
+
"cloudflare",
|
|
27
|
+
"cloudflare-workers",
|
|
28
|
+
"workers",
|
|
29
|
+
"workerd",
|
|
30
|
+
"wrangler",
|
|
31
|
+
"ratelimits",
|
|
32
|
+
"cloudflare-rate-limiting",
|
|
33
|
+
"429",
|
|
34
|
+
"too-many-requests",
|
|
35
|
+
"brute-force",
|
|
36
|
+
"abuse-protection",
|
|
37
|
+
"api-security",
|
|
38
|
+
"edge",
|
|
39
|
+
"serverless",
|
|
40
|
+
"typescript"
|
|
41
|
+
],
|
|
42
|
+
"exports": {
|
|
43
|
+
".": {
|
|
44
|
+
"types": "./dist/index.d.mts",
|
|
45
|
+
"import": "./dist/index.mjs"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"dist"
|
|
50
|
+
],
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"hono": "^4.9.0",
|
|
53
|
+
"@cloudflare/workers-types": "^4.20250101.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@cloudflare/workers-types": "^4.20250101.0",
|
|
57
|
+
"@types/node": "^22.10.0",
|
|
58
|
+
"hono": "^4.9.0",
|
|
59
|
+
"obuild": "^0.4.38",
|
|
60
|
+
"typescript": "^7.0.2",
|
|
61
|
+
"vitest": "^4.1.10"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build:dist": "obuild",
|
|
65
|
+
"release": "pnpm build:dist && pnpm publish --ignore-scripts",
|
|
66
|
+
"clean": "rm -rf dist node_modules .turbo",
|
|
67
|
+
"lint": "oxlint ./src && tsc --noEmit",
|
|
68
|
+
"test": "vitest run"
|
|
69
|
+
}
|
|
6
70
|
}
|