first commit
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
# `middleware/rate-limit` — Hono middleware wrapping the rate-limit service
|
||||
|
||||
## Purpose
|
||||
|
||||
Constructs a Hono `MiddlewareHandler` that consults a
|
||||
`RateLimitService` on every request, emits the configured
|
||||
`X-RateLimit-*` / `RateLimit-*` headers on every response (allowed AND
|
||||
denied), and on denial writes an RFC 7807 `application/problem+json`
|
||||
body with the 429 status. The middleware is the canonical mount point
|
||||
operators wire into their app — `app.use("*", rateLimit({ service }))`
|
||||
covers every route; selective mounts let consumers apply different
|
||||
policies per path prefix.
|
||||
|
||||
The middleware also publishes the resolved `RateLimitDecision` to the
|
||||
Hono context (`c.set("rateLimit", decision)`) so downstream handlers
|
||||
can inspect the matched route, the remaining budget, or the resolved
|
||||
tenant id without re-running the check.
|
||||
|
||||
## Features
|
||||
|
||||
- Per-request IP and tenant resolution via injectable resolvers
|
||||
(defaults: `X-Forwarded-For` / `X-Real-IP` for IP,
|
||||
`X-Tenant-ID` header for tenant).
|
||||
- Header bag merged onto the outgoing response after `next()` so
|
||||
downstream handlers cannot accidentally strip them.
|
||||
- RFC 7807 Problem+JSON body on 429.
|
||||
- `onDenied` hook fires before the response is written — useful for
|
||||
custom audit logging.
|
||||
- Decision attached to the Hono context so handlers can act on it.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { Hono } from "hono";
|
||||
import { rateLimit } from "@suaveplan/api-plugin-rate-limit";
|
||||
|
||||
const app = new Hono();
|
||||
app.use("*", rateLimit({ service }));
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { Hono } from "hono";
|
||||
import {
|
||||
rateLimit,
|
||||
type RateLimitDecision,
|
||||
} from "@suaveplan/api-plugin-rate-limit";
|
||||
|
||||
const app = new Hono();
|
||||
app.use(
|
||||
"/api/*",
|
||||
rateLimit({
|
||||
service,
|
||||
headerStyle: "both",
|
||||
emitRetryAfter: true,
|
||||
resolveIp: (c) => c.req.header("CF-Connecting-IP") ?? "unknown",
|
||||
resolveTenantId: (c) => c.get("organisation")?.id,
|
||||
onDenied: (decision: RateLimitDecision) => {
|
||||
auditLog.warn("rate-limit denied", {
|
||||
routeId: decision.matchedRouteId,
|
||||
key: decision.key,
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
app.get("/api/users", (c) => {
|
||||
const decision = c.get("rateLimit");
|
||||
return c.json({ remaining: decision.remaining });
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface RateLimitMiddlewareOptions` — `{ service, headerStyle?,
|
||||
emitRetryAfter?, resolveTenantId?, resolveIp?, onDenied? }`.
|
||||
- `function rateLimit(options: RateLimitMiddlewareOptions): MiddlewareHandler`
|
||||
— returns the Hono middleware. Defaults pulled from
|
||||
`service.config` when not overridden.
|
||||
- `function rateLimitExceeded(decision: RateLimitDecision): RateLimitPluginError`
|
||||
— construct a typed `LIMIT_EXCEEDED` error for callers that want to
|
||||
throw rather than return a Response.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The middleware returns the 429 response inline rather than throwing
|
||||
the typed error — operators who installed an error-handler
|
||||
translating `RateLimitPluginError` into Problem+JSON would get a
|
||||
double translation otherwise. Callers wiring `service.check`
|
||||
manually can use `rateLimitExceeded(decision)` to surface the same
|
||||
error through their handler chain.
|
||||
- Header bag is merged onto `c.res.headers` both before
|
||||
the denial response and after `next()` returns — Hono sometimes
|
||||
swaps the `res` between phases, so both writes are required.
|
||||
- The default IP resolver respects `X-Forwarded-For` (first hop) and
|
||||
falls back to `X-Real-IP`. Operators behind a trusted proxy should
|
||||
override this with a header their proxy guarantees (e.g.
|
||||
`CF-Connecting-IP` on Cloudflare).
|
||||
- The default tenant resolver reads `X-Tenant-ID` — consumers using
|
||||
JWT-derived tenant ids should override `resolveTenantId` to read
|
||||
from `c.get("user")`.
|
||||
Reference in New Issue
Block a user