first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -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")`.