# @nifrajs/middleware

Composable, dependency-light middleware for [nifra](../../README.md) - CORS, security headers, body
limits, auth, CSRF, JWT/JWKS, IP restriction, response caching, timing, and ops helpers - applied with
`app.use()`.

```sh
bun add @nifrajs/middleware
```

```ts
import { server } from "@nifrajs/core/server"
import {
  bodyLimit,
  cors,
  MemoryStore,
  problemDetails,
  rateLimit,
  securityHeaders,
  timing,
} from "@nifrajs/middleware"

const app = server()
  .use(securityHeaders())
  .use(problemDetails())
  .use(cors({ origin: ["https://app.example.com"], credentials: true }))
  .use(bodyLimit({ maxBytes: 1_000_000 }))
  .use(rateLimit({
    store: new MemoryStore(),
    max: 100,
    windowMs: 60_000,
    key: (req) => req.headers.get("x-user-id") ?? "anonymous",
  }))
  .use(timing())
  .get("/", () => ({ ok: true }))
```

- **`bodyLimit({ maxBytes })`** - fail-closed `Content-Length` gate before routing. Lengthless
  bodies are rejected with `411` by default; use route-level `c.boundedBody()` / schema validation
  for intentionally streamed endpoints.
- **`basicAuth(options)`** - Basic Auth plugin with constant-time static credential comparison or
  a custom verifier.
- **`bearer(options)` / `apiKey(options)`** - token auth plugins with typed principals.
- **`csrf({ secret })`** - signed double-submit CSRF protection plus Origin/Referer checking.
- **`jwt(options)` + `verifyJwt()` / `tryVerifyJwt()` + `jwk()` / `jwks()`** - JWT auth
  with explicit algorithm allowlists, required expiration by default, issuer/audience checks,
  direct JWK, HTTPS JWKS, and an additive no-throw Result helper for manual verification.
- **`ipRestriction(options)`** - allow/deny IPv4/IPv6 exact and CIDR matches. Fails closed unless
  you provide `clientIp`, trusted proxy extraction, or a trusted single-IP header.
- **`cors(options)`** - preflight handling + headers on *every* response (errors and
  404s included). Origin as `"*"` / exact / list / predicate. **Throws** if
  `credentials: true` is paired with `origin: "*"` (the browser rejects it).
- **`securityHeaders(options)`** - `X-Content-Type-Options`, `X-Frame-Options`,
  `Referrer-Policy` by default; opt-in HSTS and CSP. Every value is fixed, so they are **declared**
  (see below) rather than written by a hook - the app keeps its fused/native lanes.
- **`rateLimit(options)`** - `429` + `Retry-After` + `RateLimit-*` headers, with a
  pluggable `RateLimitStore`. Configure a trusted `key`, trusted single-IP `header`, or
  `trustedProxies`; a missing key source fails closed instead of silently sharing one
  bucket. The bundled `MemoryStore` **refuses to run in production** (a per-instance
  limiter is unsafe across instances) - provide a shared store (Redis, etc.) there.
- **`cache({ store, ttlMs })` / `responseCache(...)`** - full response cache with a pluggable
  store, `Vary`-aware keys, `Age`, byte caps, and `Cache-Control` / `Set-Cookie` safety defaults.
  `MemoryResponseCache` is dev/single-instance only unless explicitly allowed in production.
- **`timing()`** - `Server-Timing` plus typed `c.timing.metric/mark/measure` controls.
- **`problemDetails()`** - opt-in RFC 9457 `application/problem+json` responses for framework
  errors. The default `{ ok: false, error }` envelope remains unchanged unless installed; validation
  issues are preserved, and `includeInstance` includes only the request pathname.
- **`rangeResponse()` / `parseByteRange()`** - bounded byte-range responses with 206/416,
  `If-Range`, multipart ranges, and conditional validators.
- **`conditionalResponse()`** - reusable ETag/Last-Modified handling that emits a bodyless 304.
- **`negotiateContentType()`** - RFC-style `Accept` matching with q-values, wildcards, and q=0.
- **`multipartResponse()`** - cancellable streaming multipart output without buffering all parts.
- **`prettyJson()`** - capped, JSON-only pretty printing for debugging and developer-facing APIs.
- **`methodOverride()`** - header/query method tunneling for clients that can only send `POST`.
  Header override is on by default; query override is opt-in.
- **`trimTrailingSlash()` / `appendTrailingSlash()`** - redirect or rewrite URL canonicalization.
- **`poweredBy()`** - opt-in product/framework header; Nifra emits no powered-by header by default.
- **`language()` / `pickLanguage()`** - `Accept-Language` negotiation with typed `c.language`.
- **`combine()` / `namedCombine()`** - reusable runtime bundles for middleware/plugins.
- **`requestId()` / `logger()` / `etag()` / `compression()` / `cacheControl()` /
  `idempotency()` / `healthcheck()` / `openapi()`** - additional operational middleware for APIs.

## Header middleware: declared headers and `onResponseHeaders`

A bundle whose headers are FIXED at construction should declare them (`responseHeaders`) rather than
write them from a hook: declared headers register no hook at all, so they fold into response
construction and the app keeps the fused/native lanes a response hook closes (measured +11% on a bare
Bun GET, byte-identical on the wire). They are defaults - anything the request produced wins.
`securityHeaders` and the default `poweredBy` ship this way.

A middleware whose response hook only reads/writes **headers** but needs the request should use the
portable `onResponseHeaders` hook - enable the observer surface with
`app.use(responseObserver())` from `@nifrajs/core/response-observer` before registering a custom
hook. It remains one implementation, fast on every runtime: it mutates the
response's own `Headers` on Bun/Deno and the outcome record on Node's direct socket writer, never
materializing Web `Request`/`Response` objects. `cors` (origin reflection), static `cacheControl`,
`language`, and `poweredBy({ respectExisting: false })` are built on it. Stateful middleware (`rateLimit` with the built-in key derivation,
`logger`) pairs full native twins (`onNodeRequest`/`onNodeResponse`) instead. Body-transforming
middleware has its own portable tier, `onResponseBody` - the hook receives the final
framework-serialized bytes on every runtime with no stream drained (raw handler `Response`s are
skipped by contract); full-response capture and stream wrapping keep the `onResponse` contract.
`timing` stays Web-only for now. See the [plugins guide](https://nifra.dev/docs/plugins) for the
contracts.

Request timeouts are configured at the core server boundary so they can abort `c.signal` and race the
whole lifecycle:

```ts
const app = server({ requestTimeoutMs: 5_000 })
```

`@nifrajs/core` is a peer dependency. ESM-only. MIT.

## For AI agents

Start with [`LLM.md`](./LLM.md) - this package's contract card (the exports you call + its footguns),
one cheap read instead of the whole corpus. For the wider framework: the repo's
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
done-gate, or `nifra mcp` to give the agent live project tools.
