@nifrajs/cli 2.8.1 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -95,7 +95,7 @@ export default function Page(props: { data: LoaderData<typeof loader> }) {
95
95
 
96
96
  See the [benchmarks](/benchmarks) for how it performs.
97
97
 
98
- ## Nifra vs. other frameworks
98
+ ## Nifra vs. other frameworks - the honest comparison
99
99
 
100
100
  > How Nifra compares to the full-stack frameworks (Next.js, Nuxt, SvelteKit, Remix, TanStack Start) - and, as a standalone backend, to Hono and Elysia. Five UI frameworks, five runtimes, end-to-end types, and an AI-agent toolchain no competitor ships.
101
101
 
@@ -228,12 +228,22 @@ accurately:
228
228
  surface), `nifra_example` (snippets typechecked against the installed version - no
229
229
  hallucinated APIs), `nifra_scaffold` (URL → correct `routes/` file),
230
230
  `nifra_run` (verify via HTTP), and `nifra_check` (a drift gate that returns the
231
- fix).
231
+ fix). One MCP, two transports: the docs tools are also hosted at `mcp.nifra.dev`
232
+ (no checkout needed), while the project tools run only on your machine - your code never
233
+ leaves it.
232
234
 
233
235
  -
234
236
  `llms.txt` + `llms-full.txt` served at the site root, an `AGENTS.md`
235
237
  in every scaffold, and a docs corpus that can't drift from the code.
236
238
 
239
+ -
240
+ `nifra assure` + `nifra levels` - a route-assurance gate: a policy file
241
+ classifies every reflected route and CI fails naming exactly which evidence is missing
242
+ (authentication on writes, validation, declared effects). Spectral lints the OpenAPI
243
+ _document_ and Semgrep pattern-matches source text; neither sees the real route graph.
244
+ This is what makes agent-written routes safe to merge - an agent (or a human) cannot ship an
245
+ unauthenticated write past it.
246
+
237
247
  ### Where Nifra fits
238
248
 
239
249
  Reach for Nifra when you want:
@@ -750,7 +760,7 @@ export const addTodo = serverFn(
750
760
  )
751
761
  ```
752
762
 
753
- The second argument receives the validated input and the ordinary nifra `Context`:
763
+ The second argument receives the validated input and the ordinary Nifra `Context`:
754
764
  `c.env`, `c.clientIp`, `c.budget`, cookies and the capability
755
765
  guard are all there, because this is a route.
756
766
 
@@ -1019,7 +1029,7 @@ const isr = withISR(app, { store, revalidate: 60, now: () => Date.now() })
1019
1029
  Bun.serve({ fetch: (req) => isr(req) })
1020
1030
  ```
1021
1031
 
1022
- Set a route's freshness with `export const revalidate` (seconds) - nifra emits it as
1032
+ Set a route's freshness with `export const revalidate` (seconds) - Nifra emits it as
1023
1033
  the `x-nifra-isr-revalidate` header, which the wrapper reads to set that page's TTL.
1024
1034
 
1025
1035
  ```ts
@@ -1135,9 +1145,9 @@ route.
1135
1145
 
1136
1146
  ## Nifra - Hydration & pre-hydration forms
1137
1147
 
1138
- > SSR pages become interactive after hydration. nifra keeps that gap safe: progressive-enhancement forms and links work before it, and a JS-only form's broken native submit is guarded automatically.
1148
+ > SSR pages become interactive after hydration. Nifra keeps that gap safe: progressive-enhancement forms and links work before it, and a JS-only form's broken native submit is guarded automatically.
1139
1149
 
1140
- An SSR page is visible at once but interactive only after its island hydrates. nifra keeps
1150
+ An SSR page is visible at once but interactive only after its island hydrates. Nifra keeps
1141
1151
  that gap safe for you - you rarely have to think about it.
1142
1152
 
1143
1153
  ### Forms and links - nothing to do
@@ -1169,12 +1179,12 @@ async function onSubmit(e: FormEvent) {
1169
1179
  return <form onSubmit={onSubmit}>…</form>
1170
1180
  ```
1171
1181
 
1172
- nifra blocks that native submit until hydration commits, so the worst case is a no-op click -
1182
+ Nifra blocks that native submit until hydration commits, so the worst case is a no-op click -
1173
1183
  never a broken navigation. It never touches a `method="post"` form or a GET form
1174
1184
  with a real action. Opt a form out with `data-native`:
1175
1185
 
1176
1186
  ```ts
1177
- <form data-native>…</form> {/* nifra won't guard it - native submit is intended */}
1187
+ <form data-native>…</form> {/* Nifra won't guard it - native submit is intended */}
1178
1188
  ```
1179
1189
 
1180
1190
  ### Gate other JS on the signal
@@ -1387,9 +1397,9 @@ but the compiled frameworks use their native single-file format - **Svelte**
1387
1397
  package's Bun plugin (`@nifrajs/web-vue/plugin`, `@nifrajs/web-svelte/plugin`).
1388
1398
 
1389
1399
  ```ts
1390
- <!-- routes/index.vue - a nifra route authored as a Vue Single-File Component -->
1400
+ <!-- routes/index.vue - a Nifra route authored as a Vue Single-File Component -->
1391
1401
  <script lang="ts">
1392
- // The plain <script> carries nifra's route convention (server-only named exports):
1402
+ // The plain <script> carries Nifra's route convention (server-only named exports):
1393
1403
  export const meta = { title: "Home" }
1394
1404
  export async function loader({ api }) {
1395
1405
  const res = await api.count.get()
@@ -1398,7 +1408,7 @@ export async function loader({ api }) {
1398
1408
  </script>
1399
1409
 
1400
1410
  <script setup lang="ts">
1401
- defineProps(["data"]) // nifra passes the loader data in as `data`
1411
+ defineProps(["data"]) // Nifra passes the loader data in as `data`
1402
1412
  </script>
1403
1413
 
1404
1414
  <template>
@@ -1568,6 +1578,102 @@ const app = server()
1568
1578
  .use(rateLimit({ store: new MemoryStore(), max: 100, windowMs: 60_000 }))
1569
1579
  ```
1570
1580
 
1581
+ ### Header middleware: one portable hook, fast everywhere
1582
+
1583
+ Most response middleware only reads or writes **headers** - security headers, CORS
1584
+ reflection, cache directives, negotiation. For that shape, prefer
1585
+ `onResponseHeaders` over `onResponse`: it receives a mutable
1586
+ case-insensitive header view, a minimal request view (`{ method, url, header(name) }`),
1587
+ and the status. One implementation runs on every runtime - on Bun/Deno inside the normal
1588
+ response walk against the response's own `Headers` (in place, no clone), and on
1589
+ Node against the outcome's plain header record on the **direct socket writer**, without
1590
+ ever materializing Web `Request`/`Response` objects:
1591
+
1592
+ ```ts
1593
+ import type { Middleware } from "@nifrajs/core/server"
1594
+
1595
+ // ONE implementation, fast on every runtime: on Bun/Deno it mutates the
1596
+ // response's own Headers in place; on Node it writes the outcome record on the
1597
+ // direct socket writer - no Web Request/Response is ever built for it.
1598
+ export function serverName(value: string): Middleware {
1599
+ return {
1600
+ onResponseHeaders(headers, req, status) {
1601
+ if (status < 500) headers.set("server", value)
1602
+ },
1603
+ }
1604
+ }
1605
+ ```
1606
+
1607
+ Why this matters on Node: a full `onResponse(res: Response)` hook needs real Web
1608
+ objects, and building them costs several microseconds per request - registering even one
1609
+ drops a realistic Node app from ~95% to ~70% of a raw `node:http` server's
1610
+ throughput (it was worse before the lazy bridge below). `onResponseHeaders` never
1611
+ pays that: the built-ins that use it (`cors`, static `cacheControl`,
1612
+ `language`) keep the direct writer at full speed.
1613
+
1614
+ ### Fixed headers: declare them, don't hook them
1615
+
1616
+ Some response headers have no per-request decision behind them at all - a frame policy, a
1617
+ referrer policy, an HSTS directive. Declaring those with
1618
+ `app.responseHeaders(record)` registers **no hook**: the values are folded into
1619
+ response construction (one prebuilt init for JSON renders, one record merge when the route
1620
+ set its own headers), so the app keeps the lanes a hook would close - Bun's fused native
1621
+ routes, and the Node direct writer for a full `onResponse`. They still apply to
1622
+ every response a hook would cover: success, error, 404/405, timeout, short-circuit.
1623
+
1624
+ ```ts
1625
+ // doc-check: skip - fragment: `app` is your application's server instance.
1626
+ // No hook is registered, so a bare route still takes Bun's fused native lane.
1627
+ app.responseHeaders({
1628
+ "x-frame-options": "DENY",
1629
+ "referrer-policy": "no-referrer",
1630
+ "strict-transport-security": "max-age=31536000; includeSubDomains",
1631
+ })
1632
+ ```
1633
+
1634
+ Declared headers are **defaults**: a value the request itself produced -
1635
+ `c.set.headers`, or a response hook - wins, whatever casing it used. Names are
1636
+ lowercased once at wire-up, and a name the render owns (`content-type`,
1637
+ `content-length`, `transfer-encoding`, `set-cookie`), an
1638
+ invalid name, or a non-string value throws immediately. Declare them before your response
1639
+ hooks: a declaration made after one cannot precede it, so it degrades to an ordinary
1640
+ `onResponseHeaders` hook to keep registration order honest.
1641
+
1642
+ `securityHeaders()` and the default `poweredBy()` ship this way -
1643
+ **measured +11% on a bare Bun GET** against the same headers written by a hook, byte-identical
1644
+ on the wire (pinned by parity suites on Bun, Node, and Deno). A route whose headers depend on
1645
+ the request keeps `onResponseHeaders`: that is why `cors` (origin
1646
+ reflection) and conditional `cacheControl` are still hooks.
1647
+
1648
+ -
1649
+ **Body middleware gets its own portable tier: `onResponseBody`.** It
1650
+ receives the FINAL framework-serialized bytes (plus the same header view and status) and
1651
+ may return replacement bytes - on every runtime the bytes are already resident before any
1652
+ Web `Response` exists, so nothing is drained. A body-hashing middleware
1653
+ benchmarks at ~92% of a raw `node:http` server this way - Fastify-class - vs
1654
+ ~50% through the full `onResponse` contract. Handler-returned raw
1655
+ `Response`s (proxied fetch, SSE, streamed SSR) are skipped by contract.
1656
+
1657
+ -
1658
+ **The full `onResponse(res: Response)` stays for what only it can do:**
1659
+ wrapping streams, capturing complete responses (caching, idempotency replay), and
1660
+ intercepting raw `Response`s uniformly. On Node such a response is bridged
1661
+ through a lazy spec-shaped Response that materializes headers and body machinery only when
1662
+ the hook actually touches them - powerful, and priced accordingly.
1663
+
1664
+ -
1665
+ **Advanced - native twins:** a middleware that needs per-request state or a native
1666
+ preflight short-circuit can pair its Web hooks with
1667
+ `onNodeRequest`/`onNodeResponse` twins (that is how
1668
+ `rateLimit` and `logger` stay native: the same context object reaches
1669
+ both twins, so a `WeakMap` keyed on it carries state across the request). Twins
1670
+ are all-or-nothing per app on Node and must stay semantically identical to their Web
1671
+ hooks; `onResponseHeaders` needs none of this.
1672
+
1673
+ -
1674
+ `timing` stays Web-only for now (its `c.timing` metric channel
1675
+ crosses the handler lifecycle).
1676
+
1571
1677
  ### Official plugins
1572
1678
 
1573
1679
  `@nifrajs/middleware` seeds a few `definePlugin` plugins to build on:
@@ -1759,7 +1865,7 @@ const app = server()
1759
1865
  .use(trimTrailingSlash()) // canonicalize URLs: 308 redirect (or rewrite), same-origin only, conservative methods
1760
1866
  .use(language({ supported: ["en", "fr"], defaultLanguage: "en" })) // Accept-Language → c.language + Content-Language
1761
1867
  .use(timing()) // Server-Timing header + typed c.timing marks/measures
1762
- // poweredBy() is opt-in (nifra emits no X-Powered-By by default). combine(a, b, c) bundles several into one plugin.
1868
+ // poweredBy() is opt-in (Nifra emits no X-Powered-By by default). combine(a, b, c) bundles several into one plugin.
1763
1869
  ```
1764
1870
 
1765
1871
  ### Security middleware
@@ -1842,9 +1948,12 @@ Valibot) yields a cleaned value, so the extra fields are dropped. A _strict_ one
1842
1948
  response becomes a 500 and the detail goes to the logger, never to the caller. Both are what you
1843
1949
  already declared about extra fields.
1844
1950
 
1845
- Routes with a `response` schema leave the fused and native fast paths while this is
1846
- on, the same trade an idempotent route makes: the check needs the handler&rsquo;s value before it
1847
- becomes bytes.
1951
+ The check itself is essentially free: with a compiled validator it measures in the
1952
+ ~100ns-per-response range, and a realistic middleware-carrying route benchmarks within noise
1953
+ of the same route with no contract at all, on Bun and Node alike. What a contracted route
1954
+ does give up is the bare-route fused lane - the check needs the handler&rsquo;s value before
1955
+ it becomes bytes - and a route with any middleware, derive, or lifecycle hook has already
1956
+ left that lane. If a route looks like production, the contract costs nothing: declare it.
1848
1957
 
1849
1958
  ### Bounded request bodies
1850
1959
 
@@ -2246,7 +2355,7 @@ server function:
2246
2355
 
2247
2356
  On its own that is an assertion. What makes it load-bearing is the other half:
2248
2357
  `provenance.imports` maps a module specifier to the capabilities that reaching it
2249
- implies, so nifra can compare what a route _says_ against what it _can do_.
2358
+ implies, so Nifra can compare what a route _says_ against what it _can do_.
2250
2359
 
2251
2360
  ```ts
2252
2361
  import { server } from "@nifrajs/core/server"
@@ -2493,7 +2602,7 @@ yourself. Nifra owns the _session_; you bring (or mount) the _who_.
2493
2602
  `@nifrajs/better-auth` bridges [Better Auth](https://better-auth.com) into
2494
2603
  nifra: `betterAuth(auth)` mounts its handler at `/api/auth/*` (GET + POST),
2495
2604
  so every endpoint - sign-in/up/out, OAuth callbacks, session, 2FA, magic links - is served by
2496
- your nifra server. Read the session with `getSession(auth, request)` (typed
2605
+ your Nifra server. Read the session with `getSession(auth, request)` (typed
2497
2606
  `{ user, session } | null`) or guard a route with
2498
2607
  `requireSession(auth, request, options?)` (returns it, or throws a 401/redirect
2499
2608
  `Response`). It's declared **structurally** - no hard dependency on Better Auth, so
@@ -2574,7 +2683,7 @@ createWebApp({ /* … */ api: sessions }) // inject the manager
2574
2683
  ### Guard a route
2575
2684
 
2576
2685
  `requireSession` / `requireUser` throw a `Response` (a 302 to
2577
- `redirectTo`, or a 401) when the session is missing - nifra returns a thrown Response
2686
+ `redirectTo`, or a 401) when the session is missing - Nifra returns a thrown Response
2578
2687
  as-is, so the guard short-circuits the loader.
2579
2688
 
2580
2689
  ```ts
@@ -2585,7 +2694,7 @@ import { requireUser } from "@nifrajs/auth" // browser-safe; OK to import in a
2585
2694
  export async function loader({ request, api }) {
2586
2695
  const sessions = api // the manager injected via createWebApp's `api`
2587
2696
  const session = await sessions.read(request)
2588
- // requireUser throws a 302 to /login when there's no session; nifra returns the thrown Response.
2697
+ // requireUser throws a 302 to /login when there's no session; Nifra returns the thrown Response.
2589
2698
  const userId = requireUser(session, "userId", { redirectTo: "/login" })
2590
2699
  return { userId }
2591
2700
  }
@@ -2715,11 +2824,11 @@ const imgix: ImageLoader = ({ src, width, quality }) =>
2715
2824
  `https://my.imgix.net${src}?w=${width}&auto=format${quality ? `&q=${quality}` : ""}`
2716
2825
  ```
2717
2826
 
2718
- ### Self-hosting - nifra's own resize endpoint
2827
+ ### Self-hosting - Nifra's own resize endpoint
2719
2828
 
2720
2829
  No CDN? `@nifrajs/image/server`'s `createImageHandler` is a self-hosted resize
2721
2830
  endpoint backed by `Bun.Image` (libjpeg-turbo / libspng / libwebp, decoded off-thread).
2722
- Pair it with `selfHostedLoader` and nifra resizes your own images - no third party in the
2831
+ Pair it with `selfHostedLoader` and Nifra resizes your own images - no third party in the
2723
2832
  path.
2724
2833
 
2725
2834
  ```ts
@@ -2775,7 +2884,7 @@ decodes/resizes/encodes. Three official backends, all from `@nifrajs/image/backe
2775
2884
 
2776
2885
  -
2777
2886
  `sharpImageBackend(sharp)` - libvips for Node. You pass your `sharp` import
2778
- (nifra keeps zero dependency on it, and you pin the version).
2887
+ (Nifra keeps zero dependency on it, and you pin the version).
2779
2888
 
2780
2889
  -
2781
2890
  `wasmImageBackend(codecs)` - pure-WASM decode/resize/encode you wire up (jSquash is
@@ -2789,7 +2898,7 @@ built in (it never decodes just to read dimensions).
2789
2898
  import { createImageHandler } from "@nifrajs/image/server"
2790
2899
  import { sharpImageBackend, wasmImageBackend } from "@nifrajs/image/backends"
2791
2900
 
2792
- // Node - libvips via sharp. Pass your own import (nifra never depends on it):
2901
+ // Node - libvips via sharp. Pass your own import (Nifra never depends on it):
2793
2902
  import sharp from "sharp"
2794
2903
  createImageHandler({ backend: sharpImageBackend(sharp), root: "./public" })
2795
2904
 
@@ -3057,7 +3166,7 @@ export default toFetchHandler(app, {
3057
3166
 
3058
3167
  `app.ws(path, handler)` registers a WebSocket route. It mirrors
3059
3168
  `.get()`/`.post()` - chainable, with per-connection state typed
3060
- through a generic - and runs on every runtime nifra serves: Bun, Deno, Node, and Cloudflare
3169
+ through a generic - and runs on every runtime Nifra serves: Bun, Deno, Node, and Cloudflare
3061
3170
  Workers.
3062
3171
 
3063
3172
  The WebSocket runtime ships as an opt-in plugin, so apps that never use it don’t bundle it.
@@ -3143,7 +3252,7 @@ escape hatch to the native socket
3143
3252
  Inbound frames arrive raw (`string | Uint8Array`) by default. Add a
3144
3253
  `messageSchema` - any
3145
3254
  [Standard Schema](https://standardschema.dev) (`t`, zod, valibot) - and
3146
- nifra parses each frame as JSON, validates it, and hands `message` the typed
3255
+ Nifra parses each frame as JSON, validates it, and hands `message` the typed
3147
3256
  value; anything that fails goes to `onInvalidMessage` instead (so a malformed
3148
3257
  frame can never reach your handler).
3149
3258
 
@@ -3180,7 +3289,7 @@ Bun, Deno, and Node are long-lived processes, so this works directly on a single
3180
3289
  Across a load balancer, `app.publish` only reaches sockets on the same instance -
3181
3290
  bridge an external fan-out (Redis pub/sub, NATS, a queue) to broadcast across all of them. On
3182
3291
  **Cloudflare Workers**, a stateless isolate can’t broadcast across connections,
3183
- so nifra ships a Durable Object hub: `createWebSocketHub(app)` holds the
3292
+ so Nifra ships a Durable Object hub: `createWebSocketHub(app)` holds the
3184
3293
  connections, and `toFetchHandler(app, { webSocketHub })` routes upgrades to it
3185
3294
  - then `ws.subscribe` / `app.publish` behave exactly as on Bun.
3186
3295
 
@@ -3436,7 +3545,7 @@ Deploy: `wrangler pages deploy dist` (Pages) or `wrangler deploy`
3436
3545
  `server.ts` to hand-write. (`create-nifra` scaffolds the conventions.)
3437
3546
 
3438
3547
  ```ts
3439
- nifra dev # true-HMR dev server (Vite middleware + nifra SSR) - http://localhost:4321
3548
+ nifra dev # true-HMR dev server (Vite middleware + Nifra SSR) - http://localhost:4321
3440
3549
  nifra build # full Bun deploy → dist/server.js + content-hashed dist/assets/ (default target: bun)
3441
3550
  nifra start # run dist/server.js on Bun
3442
3551
  nifra build --target cf-pages # also: node | deno | vercel | static; add --report for chunk sizes
@@ -3512,6 +3621,50 @@ Nifra ships an MCP server. It is not a documentation lookup bolted onto a framew
3512
3621
  can read the project, learn the real API, run actual requests against the backend it just
3513
3622
  edited, and finish with a report of what the change proved.
3514
3623
 
3624
+ ### One MCP, two ways to connect
3625
+
3626
+ There is one Nifra MCP. It reaches your agent over the two standard MCP transports, and which
3627
+ one you use is decided by a single question: **is the agent working inside a Nifra repo?**
3628
+
3629
+ Local - `nifra mcp`
3630
+
3631
+ Hosted - `mcp.nifra.dev`
3632
+
3633
+ Transport
3634
+ stdio - the agent spawns it as a process
3635
+ HTTP - add one URL, nothing to install
3636
+
3637
+ Runs
3638
+ on your machine, in your project
3639
+ on our infrastructure
3640
+
3641
+ Sees
3642
+ your routes, schemas, and files - locally only, nothing leaves the machine
3643
+ only Nifra's published docs corpus - never your code
3644
+
3645
+ Tools
3646
+
3647
+ everything: project tools (`nifra_context`, `nifra_run`,
3648
+ `nifra_assure`, …) **plus** the docs tools
3649
+
3650
+ docs tools only (`nifra_docs`, `nifra_example`,
3651
+ `nifra_types`, `nifra_learn`)
3652
+
3653
+ Use when
3654
+ building or editing a Nifra app
3655
+ learning Nifra, or the client can't spawn processes (Claude.ai, ChatGPT)
3656
+
3657
+ The project tools _must_ run where your code is - hosting them would mean uploading
3658
+ your source, which is exactly what this design refuses to do. And the local server does not
3659
+ proxy the hosted one for docs: the docs corpus ships inside the npm package, so the answers
3660
+ match **the Nifra version installed in your project**, work offline, and send nothing
3661
+ anywhere. This hosted-plus-local pairing is the same shape Supabase, Stripe, Sentry, and
3662
+ GitHub ship their MCP servers in, for the same reason: public knowledge can be hosted; tools
3663
+ that touch your own code and data run where that code lives.
3664
+
3665
+ So: inside a Nifra repo, register the local server (it includes the docs tools - you never
3666
+ need both). Anywhere else, add the URL.
3667
+
3515
3668
  ### Setup
3516
3669
 
3517
3670
  ```ts
@@ -3738,7 +3891,7 @@ const server = await createViteDevServer({
3738
3891
  routesDir,
3739
3892
  clientModule: "@nifrajs/web-react/client",
3740
3893
  plugins: [react()], // Vue: @vitejs/plugin-vue, Svelte: …, etc.
3741
- port: Number(Bun.env.PORT ?? 4321), // nifra's default; --port / PORT override it
3894
+ port: Number(Bun.env.PORT ?? 4321), // Nifra's default; --port / PORT override it
3742
3895
  createApp: (clientEntry, importQuery) =>
3743
3896
  createWebApp({
3744
3897
  adapter: reactAdapter,
@@ -3908,13 +4061,13 @@ so `node:` builtins and `server-only` modules fail the build either way.
3908
4061
 
3909
4062
  ```ts
3910
4063
  // vite.config.ts - a Vite/Rollup PRODUCTION client build (the escape hatch, not the default).
3911
- // Only reach for this when an app needs a Vite-only transform with no Bun equivalent; nifra's default
4064
+ // Only reach for this when an app needs a Vite-only transform with no Bun equivalent; Nifra's default
3912
4065
  // production bundler stays Bun (buildClient), which is faster and Bun-native.
3913
4066
  import { viteLeakGuard } from "@nifrajs/web/plugins/vite-leak-guard"
3914
4067
 
3915
4068
  export default {
3916
4069
  build: {
3917
- // The SAME two client-leak guards nifra's Bun build runs - server-only code or a node: builtin
4070
+ // The SAME two client-leak guards Nifra's Bun build runs - server-only code or a node: builtin
3918
4071
  // reaching the browser fails the build, with the identical error message. A second production
3919
4072
  // pipeline must not ship without them.
3920
4073
  rollupOptions: { plugins: [viteLeakGuard()] },
@@ -4843,7 +4996,7 @@ call.
4843
4996
  import type { LoaderContext } from "@nifrajs/web"
4844
4997
 
4845
4998
  // (1) A page's own data + mutations → route exports. A PUBLIC POST is a route ACTION
4846
- // (routes/contact.tsx) - the browser POSTs the route's own URL, nifra runs `action`:
4999
+ // (routes/contact.tsx) - the browser POSTs the route's own URL, Nifra runs `action`:
4847
5000
  export async function action(ctx: LoaderContext) {
4848
5001
  const form = await ctx.request.formData() // the browser POSTed this route
4849
5002
  return { ok: true, body: String(form.get("body")) } // → props.actionData on re-render
@@ -5619,7 +5772,7 @@ await Bun.write("app/fonts.preloads.json", JSON.stringify(inter.preloads))
5619
5772
  Then wire the two outputs into your app:
5620
5773
 
5621
5774
  ```ts
5622
- // app entry - bundled + content-hashed by nifra's CSS pipeline
5775
+ // app entry - bundled + content-hashed by Nifra's CSS pipeline
5623
5776
  import "./fonts.css"
5624
5777
 
5625
5778
  // root layout - preload the primary file (one less render-blocking round trip)
@@ -5690,9 +5843,9 @@ traversal.
5690
5843
 
5691
5844
  ## Nifra - Integrations (Stripe, and any npm library)
5692
5845
 
5693
- > Third-party libraries work in nifra as-is - handlers are plain TypeScript. The Stripe recipe end-to-end: SDK calls in a route, constant-time webhook verification, and idempotent processing.
5846
+ > Third-party libraries work in Nifra as-is - handlers are plain TypeScript. The Stripe recipe end-to-end: SDK calls in a route, constant-time webhook verification, and idempotent processing.
5694
5847
 
5695
- The most important thing on this page: **you rarely need it**. A nifra handler
5848
+ The most important thing on this page: **you rarely need it**. A Nifra handler
5696
5849
  is plain TypeScript running on Bun, Node, Deno, or the edge - the npm ecosystem works
5697
5850
  as-is. Stripe, OpenAI, Resend, Drizzle, Prisma, AWS SDKs: import them and call them, the
5698
5851
  way their own documentation shows.
@@ -5700,8 +5853,8 @@ way their own documentation shows.
5700
5853
  ### When you need nothing at all
5701
5854
 
5702
5855
  If a library is called from your code - an SDK, a database client, a date library - there is
5703
- no integration step. It does not know or care that nifra is serving the request. The one
5704
- rule that still applies is nifra's own: anything crossing a trust boundary (a request body,
5856
+ no integration step. It does not know or care that Nifra is serving the request. The one
5857
+ rule that still applies is Nifra's own: anything crossing a trust boundary (a request body,
5705
5858
  a webhook payload, a third-party response you act on) goes through a schema first.
5706
5859
 
5707
5860
  ### The Stripe recipe
@@ -5798,7 +5951,7 @@ and import edits; this guide covers the structural changes it deliberately canno
5798
5951
 
5799
5952
  ```
5800
5953
 
5801
- The command updates existing `@nifrajs/*`, `nifra`, and
5954
+ The command updates existing `@nifrajs/*`, `Nifra`, and
5802
5955
  `create-nifra` dependency ranges to `2.0.0` while preserving
5803
5956
  caret/tilde/exact style. It also replaces the removed `@nifrajs/budget` dependency
5804
5957
  with `@nifrajs/core` and moves its imports to
@@ -5821,7 +5974,7 @@ Only applications calling `app.resolveNode()` directly need to install it.
5821
5974
 
5822
5975
  ### 3. Use the lean package entry points
5823
5976
 
5824
- The `@nifrajs/core` and `nifra` roots now expose the lean HTTP server
5977
+ The `@nifrajs/core` and `Nifra` roots now expose the lean HTTP server
5825
5978
  surface. Prefer `@nifrajs/core/server` for `server()`, and import
5826
5979
  contracts, assurance, capabilities, budgets, manifests, reflection, SSE, WebSockets, and
5827
5980
  other optional systems from their documented subpaths.
@@ -5975,7 +6128,7 @@ onto the route. The client then infers all of this for free.
5975
6128
  app.get("/users/:id", (req, res) => res.json({ id: req.params.id }))
5976
6129
  app.post("/users", express.json(), (req, res) => res.status(201).json(create(req.body)))
5977
6130
 
5978
- // nifra - return the value (or a Response); c is Web-standard; the body is parsed + validated
6131
+ // Nifra - return the value (or a Response); c is Web-standard; the body is parsed + validated
5979
6132
  server()
5980
6133
  .get("/users/:id", (c) => ({ id: c.params.id }))
5981
6134
  .post("/users", { body: t.object({ name: t.string() }) }, (c) => create(c.body))
@@ -5991,7 +6144,7 @@ adds the typed client, schema validation, multi-runtime deploy, and (if you want
5991
6144
  // Hono
5992
6145
  app.get("/users/:id", (c) => c.json({ id: c.req.param("id") }))
5993
6146
 
5994
- // nifra - nearly identical; params are a typed object, you return the value
6147
+ // Nifra - nearly identical; params are a typed object, you return the value
5995
6148
  server().get("/users/:id", (c) => ({ id: c.params.id }))
5996
6149
  ```
5997
6150
 
@@ -6073,7 +6226,7 @@ export default async function Page({ params }) {
6073
6226
  return <h1>{user.name}</h1>
6074
6227
  }
6075
6228
 
6076
- // nifra - routes/users/[id].tsx
6229
+ // Nifra - routes/users/[id].tsx
6077
6230
  export async function loader({ params, api }: LoaderArgs<typeof backend>) {
6078
6231
  const res = await api.users({ id: params.id }).get() // typed, in-process during SSR
6079
6232
  return { user: res.data }
@@ -6094,7 +6247,7 @@ example:
6094
6247
  // SvelteKit - +page.server.ts + +page.svelte
6095
6248
  export async function load({ params }) { return { post: await getPost(params.slug) } }
6096
6249
 
6097
- // nifra - routes/blog/[slug].svelte (loader is a module export, page is the .svelte)
6250
+ // Nifra - routes/blog/[slug].svelte (loader is a module export, page is the .svelte)
6098
6251
  export async function loader({ params }) { return { post: await getPost(params.slug) } }
6099
6252
  ```
6100
6253
 
@@ -6105,7 +6258,7 @@ export async function loader({ params }) { return { post: await getPost(params.s
6105
6258
  app.
6106
6259
 
6107
6260
  -
6108
- **Much faster SSR** - Nifra renders ~28× Next.js, ~7× Nuxt, ~4× SvelteKit/SolidStart on
6261
+ **Much faster SSR** - Nifra renders ~25× Next.js, ~7× Nuxt, ~4× SvelteKit/SolidStart on
6109
6262
  dynamic pages, with a fraction of the client JS. See [benchmarks](/benchmarks).
6110
6263
 
6111
6264
  -
@@ -6483,7 +6636,7 @@ export default defineAssuranceConfig({
6483
6636
  ```
6484
6637
 
6485
6638
  L4 requires an **explicit** executor. Invariant runs send deliberately hostile
6486
- input, so nifra will not guess which app to point them at; a level that fuzzed your live
6639
+ input, so Nifra will not guess which app to point them at; a level that fuzzed your live
6487
6640
  server by default would be a footgun, and a skipped route is reported rather than passed over
6488
6641
  in silence.
6489
6642
 
@@ -7602,7 +7755,8 @@ const app = server()
7602
7755
  404s included). Origin as `"*"` / exact / list / predicate. **Throws** if
7603
7756
  `credentials: true` is paired with `origin: "*"` (the browser rejects it).
7604
7757
  - **`securityHeaders(options)`** - `X-Content-Type-Options`, `X-Frame-Options`,
7605
- `Referrer-Policy` by default; opt-in HSTS and CSP.
7758
+ `Referrer-Policy` by default; opt-in HSTS and CSP. Every value is fixed, so they are **declared**
7759
+ (see below) rather than written by a hook - the app keeps its fused/native lanes.
7606
7760
  - **`rateLimit(options)`** - `429` + `Retry-After` + `RateLimit-*` headers, with a
7607
7761
  pluggable `RateLimitStore`. Configure a trusted `key`, trusted single-IP `header`, or
7608
7762
  `trustedProxies`; a missing key source fails closed instead of silently sharing one
@@ -7622,6 +7776,26 @@ const app = server()
7622
7776
  - **`requestId()` / `logger()` / `etag()` / `compression()` / `cacheControl()` /
7623
7777
  `idempotency()` / `healthcheck()` / `openapi()`** - additional operational middleware for APIs.
7624
7778
 
7779
+ ## Header middleware: declared headers and `onResponseHeaders`
7780
+
7781
+ A bundle whose headers are FIXED at construction should declare them (`responseHeaders`) rather than
7782
+ write them from a hook: declared headers register no hook at all, so they fold into response
7783
+ construction and the app keeps the fused/native lanes a response hook closes (measured +11% on a bare
7784
+ Bun GET, byte-identical on the wire). They are defaults - anything the request produced wins.
7785
+ `securityHeaders` and the default `poweredBy` ship this way.
7786
+
7787
+ A middleware whose response hook only reads/writes **headers** but needs the request should use the
7788
+ portable `onResponseHeaders` hook - one implementation, fast on every runtime: it mutates the
7789
+ response's own `Headers` on Bun/Deno and the outcome record on Node's direct socket writer, never
7790
+ materializing Web `Request`/`Response` objects. `cors` (origin reflection), static `cacheControl`,
7791
+ `language`, and `poweredBy({ respectExisting: false })` are built on it. Stateful middleware (`rateLimit` with the built-in key derivation,
7792
+ `logger`) pairs full native twins (`onNodeRequest`/`onNodeResponse`) instead. Body-transforming
7793
+ middleware has its own portable tier, `onResponseBody` - the hook receives the final
7794
+ framework-serialized bytes on every runtime with no stream drained (raw handler `Response`s are
7795
+ skipped by contract); full-response capture and stream wrapping keep the `onResponse` contract.
7796
+ `timing` stays Web-only for now. See the [plugins guide](https://nifra.dev/docs/plugins) for the
7797
+ contracts.
7798
+
7625
7799
  Request timeouts are configured at the core server boundary so they can abort `c.signal` and race the
7626
7800
  whole lifecycle:
7627
7801