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