@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.
- package/dist/cli.js +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +34 -1
- package/dist/scaffold.js.map +1 -1
- package/docs/examples.json +9 -3
- package/docs/llms-full.txt +219 -45
- package/docs/types.json +177 -15
- package/package.json +9 -9
- package/src/cli.ts +1 -1
- package/src/mcp-http.ts +1 -1
- package/src/scaffold.ts +34 -1
package/docs/llms-full.txt
CHANGED
|
@@ -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
|
|
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) -
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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> {/*
|
|
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
|
|
1400
|
+
<!-- routes/index.vue - a Nifra route authored as a Vue Single-File Component -->
|
|
1391
1401
|
<script lang="ts">
|
|
1392
|
-
// The plain <script> carries
|
|
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"]) //
|
|
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 (
|
|
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
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
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’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
|
|
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
|
|
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 -
|
|
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;
|
|
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 -
|
|
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
|
|
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
|
-
(
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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 +
|
|
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), //
|
|
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;
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5704
|
-
rule that still applies is
|
|
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/*`, `
|
|
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 `
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 ~
|
|
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
|
|
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
|
|