@nifrajs/middleware
v3.1.0
Published
Composable middleware for nifra - auth, CSRF, JWT/JWKS, IP restriction, CORS, body limits, response cache, timing, and operational helpers.
Maintainers
Readme
@nifrajs/middleware
Composable, dependency-light middleware for nifra - CORS, security headers, body
limits, auth, CSRF, JWT/JWKS, IP restriction, response caching, timing, and ops helpers - applied with
app.use().
bun add @nifrajs/middlewareimport { 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-closedContent-Lengthgate before routing. Lengthless bodies are rejected with411by default; use route-levelc.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 provideclientIp, 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 ifcredentials: trueis paired withorigin: "*"(the browser rejects it).securityHeaders(options)-X-Content-Type-Options,X-Frame-Options,Referrer-Policyby 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 pluggableRateLimitStore. Configure a trustedkey, trusted single-IPheader, ortrustedProxies; a missing key source fails closed instead of silently sharing one bucket. The bundledMemoryStorerefuses 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, andCache-Control/Set-Cookiesafety defaults.MemoryResponseCacheis dev/single-instance only unless explicitly allowed in production.timing()-Server-Timingplus typedc.timing.metric/mark/measurecontrols.problemDetails()- opt-in RFC 9457application/problem+jsonresponses for framework errors. The default{ ok: false, error }envelope remains unchanged unless installed; validation issues are preserved, andincludeInstanceincludes 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-styleAcceptmatching 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 sendPOST. 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-Languagenegotiation with typedc.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 Responses are
skipped by contract); full-response capture and stream wrapping keep the onResponse contract.
timing stays Web-only for now. See the plugins guide for the
contracts.
Request timeouts are configured at the core server boundary so they can abort c.signal and race the
whole lifecycle:
const app = server({ requestTimeoutMs: 5_000 })@nifrajs/core is a peer dependency. ESM-only. MIT.
For AI agents
Start with 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 is the copy-paste quick reference, and
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.
