@nifrajs/cli 2.8.0 → 2.8.2

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/scaffold.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * for vue/svelte/vanilla we return the correct PATH + the route-module contract and point at
9
9
  * `nifra_example` for the body, rather than hand-writing an SFC we can't typecheck here.
10
10
  */
11
- import { mkdir, writeFile } from "node:fs/promises";
11
+ import { lstat, mkdir, realpath, writeFile } from "node:fs/promises";
12
12
  import { dirname, resolve, sep } from "node:path";
13
13
  const EXT = {
14
14
  react: "tsx",
@@ -100,6 +100,37 @@ function resolveInsideCwd(cwd, relativeFile) {
100
100
  }
101
101
  return target;
102
102
  }
103
+ /** Lexical containment is not enough when an existing route directory is a symlink. Check every
104
+ * existing ancestor and its real path before scaffolding so an agent cannot redirect a write outside
105
+ * the selected project. The target itself is created with `wx`, so an existing target symlink is also
106
+ * never followed/overwritten. */
107
+ async function assertNoSymlinkedAncestors(root, target) {
108
+ const rootPath = resolve(root);
109
+ const realRoot = await realpath(rootPath);
110
+ let current = dirname(target);
111
+ while (current !== rootPath) {
112
+ if (!current.startsWith(`${rootPath}${sep}`)) {
113
+ throw new Error(`refusing to write outside project root: ${target}`);
114
+ }
115
+ try {
116
+ if ((await lstat(current)).isSymbolicLink()) {
117
+ throw new Error(`refusing to write through symlinked directory: ${current}`);
118
+ }
119
+ const actual = await realpath(current);
120
+ if (actual !== realRoot && !actual.startsWith(`${realRoot}${sep}`)) {
121
+ throw new Error(`refusing to write outside project root through: ${current}`);
122
+ }
123
+ }
124
+ catch (err) {
125
+ if (err && typeof err === "object" && err.code === "ENOENT") {
126
+ current = dirname(current);
127
+ continue;
128
+ }
129
+ throw err;
130
+ }
131
+ current = dirname(current);
132
+ }
133
+ }
103
134
  /** Write a scaffolded route stub when the framework has a verified ready-to-write body. The write is
104
135
  * intentionally conservative: it refuses non-JSX stubs (where we only return contract guidance) and
105
136
  * uses `wx`, so an agent cannot overwrite user work by accident. */
@@ -113,7 +144,9 @@ export async function writeScaffoldRoute(cwd, urlPath, framework) {
113
144
  };
114
145
  }
115
146
  const target = resolveInsideCwd(cwd, result.file);
147
+ await assertNoSymlinkedAncestors(cwd, target);
116
148
  await mkdir(dirname(target), { recursive: true });
149
+ await assertNoSymlinkedAncestors(cwd, target);
117
150
  try {
118
151
  await writeFile(target, result.content, { flag: "wx" });
119
152
  }
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"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"}
@@ -105,7 +105,7 @@
105
105
  "name": "DUAL_API",
106
106
  "topic": "Nifra - Framework contract",
107
107
  "slug": "contract",
108
- "code": "// TWO ways to handle a request - pick by who is calling.\nimport type { LoaderContext } from \"@nifrajs/web\"\n\n// (1) A page's own data + mutations → route exports. A PUBLIC POST is a route ACTION\n// (routes/contact.tsx) - the browser POSTs the route's own URL, nifra runs `action`:\nexport async function action(ctx: LoaderContext) {\n const form = await ctx.request.formData() // the browser POSTed this route\n return { ok: true, body: String(form.get(\"body\")) } // → props.actionData on re-render\n}\n\n// (2) ctx.api is the IN-PROCESS backend client (the inProcessClient(server) the app was given).\n// It is LOADERS-ONLY: the SSR backend registers GET handlers, so a .post() to ctx.api 405s.\nexport async function loader(ctx: LoaderContext) {\n // ✅ a GET in-process - no HTTP hop. (A .post() to ctx.api would 405; write an `action` instead.)\n return { id: ctx.params.id }\n}\n\n// Rule of thumb: data INTO a page → loader + ctx.api (GET). A mutation FROM the browser\n// (a form/fetch POST) → an `action` export on the route. Don't `inProcessClient(api).post()`."
108
+ "code": "// TWO ways to handle a request - pick by who is calling.\nimport type { LoaderContext } from \"@nifrajs/web\"\n\n// (1) A page's own data + mutations → route exports. A PUBLIC POST is a route ACTION\n// (routes/contact.tsx) - the browser POSTs the route's own URL, Nifra runs `action`:\nexport async function action(ctx: LoaderContext) {\n const form = await ctx.request.formData() // the browser POSTed this route\n return { ok: true, body: String(form.get(\"body\")) } // → props.actionData on re-render\n}\n\n// (2) ctx.api is the IN-PROCESS backend client (the inProcessClient(server) the app was given).\n// It is LOADERS-ONLY: the SSR backend registers GET handlers, so a .post() to ctx.api 405s.\nexport async function loader(ctx: LoaderContext) {\n // ✅ a GET in-process - no HTTP hop. (A .post() to ctx.api would 405; write an `action` instead.)\n return { id: ctx.params.id }\n}\n\n// Rule of thumb: data INTO a page → loader + ctx.api (GET). A mutation FROM the browser\n// (a form/fetch POST) → an `action` export on the route. Don't `inProcessClient(api).post()`."
109
109
  },
110
110
  {
111
111
  "name": "INLINE_SCRIPT",
@@ -183,7 +183,7 @@
183
183
  "name": "VITE_PROD",
184
184
  "topic": "Nifra - Dev & HMR",
185
185
  "slug": "dev",
186
- "code": "// vite.config.ts - a Vite/Rollup PRODUCTION client build (the escape hatch, not the default).\n// Only reach for this when an app needs a Vite-only transform with no Bun equivalent; nifra's default\n// production bundler stays Bun (buildClient), which is faster and Bun-native.\nimport { viteLeakGuard } from \"@nifrajs/web/plugins/vite-leak-guard\"\n\nexport default {\n build: {\n // The SAME two client-leak guards nifra's Bun build runs - server-only code or a node: builtin\n // reaching the browser fails the build, with the identical error message. A second production\n // pipeline must not ship without them.\n rollupOptions: { plugins: [viteLeakGuard()] },\n },\n}"
186
+ "code": "// vite.config.ts - a Vite/Rollup PRODUCTION client build (the escape hatch, not the default).\n// Only reach for this when an app needs a Vite-only transform with no Bun equivalent; Nifra's default\n// production bundler stays Bun (buildClient), which is faster and Bun-native.\nimport { viteLeakGuard } from \"@nifrajs/web/plugins/vite-leak-guard\"\n\nexport default {\n build: {\n // The SAME two client-leak guards Nifra's Bun build runs - server-only code or a node: builtin\n // reaching the browser fails the build, with the identical error message. A second production\n // pipeline must not ship without them.\n rollupOptions: { plugins: [viteLeakGuard()] },\n },\n}"
187
187
  },
188
188
  {
189
189
  "name": "LAB",
@@ -297,7 +297,7 @@
297
297
  "name": "SHAPING",
298
298
  "topic": "Nifra - Plugins & middleware",
299
299
  "slug": "plugins",
300
- "code": "import { server } from \"@nifrajs/core/server\"\nimport { methodOverride, trimTrailingSlash, language, timing, poweredBy, combine } from \"@nifrajs/middleware\"\n\nconst app = server()\n .use(methodOverride()) // POST + X-HTTP-Method-Override → PUT/PATCH/DELETE (a real pre-routing request rewrite)\n .use(trimTrailingSlash()) // canonicalize URLs: 308 redirect (or rewrite), same-origin only, conservative methods\n .use(language({ supported: [\"en\", \"fr\"], defaultLanguage: \"en\" })) // Accept-Language → c.language + Content-Language\n .use(timing()) // Server-Timing header + typed c.timing marks/measures\n// poweredBy() is opt-in (nifra emits no X-Powered-By by default). combine(a, b, c) bundles several into one plugin."
300
+ "code": "import { server } from \"@nifrajs/core/server\"\nimport { methodOverride, trimTrailingSlash, language, timing, poweredBy, combine } from \"@nifrajs/middleware\"\n\nconst app = server()\n .use(methodOverride()) // POST + X-HTTP-Method-Override → PUT/PATCH/DELETE (a real pre-routing request rewrite)\n .use(trimTrailingSlash()) // canonicalize URLs: 308 redirect (or rewrite), same-origin only, conservative methods\n .use(language({ supported: [\"en\", \"fr\"], defaultLanguage: \"en\" })) // Accept-Language → c.language + Content-Language\n .use(timing()) // Server-Timing header + typed c.timing marks/measures\n// poweredBy() is opt-in (Nifra emits no X-Powered-By by default). combine(a, b, c) bundles several into one plugin."
301
301
  },
302
302
  {
303
303
  "name": "BUILD",
@@ -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
 
@@ -193,8 +193,10 @@ regex), but a router is **~1% of a real request** - the time goes to middleware,
193
193
  context, and serialization. In the current matrix (median of 5 full runs) Nifra
194
194
  **tops the framework field on Bun** - level with Elysia on `GET /users/:id` at
195
195
  101% of the raw-runtime ceiling, 105% of Elysia on the validated `POST` - and
196
- **leads every framework on Deno** on both workloads. On **Node** it runs ahead of Elysia,
197
- Hono, and Express and within ~2% of Fastify. In the realistic shape (security headers + CORS +
196
+ **leads every framework on Deno** on both workloads. On **Node** it
197
+ **leads the framework field too** - ahead of Fastify by ~12% on the validated
198
+ `POST` (96% of the raw-Node ceiling) and level-to-ahead on GET, with Elysia, Hono,
199
+ and Express behind. In the realistic shape (security headers + CORS +
198
200
  bearer auth + cookies + validated query/body + a ~2.4 KB JSON response, measured with
199
201
  `oha`) Nifra runs at **103% of Elysia on GET and 108% on POST**. Treat benchmark
200
202
  rows as same-run evidence, not a permanent law of nature.
@@ -226,12 +228,22 @@ accurately:
226
228
  surface), `nifra_example` (snippets typechecked against the installed version - no
227
229
  hallucinated APIs), `nifra_scaffold` (URL → correct `routes/` file),
228
230
  `nifra_run` (verify via HTTP), and `nifra_check` (a drift gate that returns the
229
- 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.
230
234
 
231
235
  -
232
236
  `llms.txt` + `llms-full.txt` served at the site root, an `AGENTS.md`
233
237
  in every scaffold, and a docs corpus that can't drift from the code.
234
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
+
235
247
  ### Where Nifra fits
236
248
 
237
249
  Reach for Nifra when you want:
@@ -748,7 +760,7 @@ export const addTodo = serverFn(
748
760
  )
749
761
  ```
750
762
 
751
- 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`:
752
764
  `c.env`, `c.clientIp`, `c.budget`, cookies and the capability
753
765
  guard are all there, because this is a route.
754
766
 
@@ -1017,7 +1029,7 @@ const isr = withISR(app, { store, revalidate: 60, now: () => Date.now() })
1017
1029
  Bun.serve({ fetch: (req) => isr(req) })
1018
1030
  ```
1019
1031
 
1020
- 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
1021
1033
  the `x-nifra-isr-revalidate` header, which the wrapper reads to set that page's TTL.
1022
1034
 
1023
1035
  ```ts
@@ -1133,9 +1145,9 @@ route.
1133
1145
 
1134
1146
  ## Nifra - Hydration & pre-hydration forms
1135
1147
 
1136
- > 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.
1137
1149
 
1138
- 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
1139
1151
  that gap safe for you - you rarely have to think about it.
1140
1152
 
1141
1153
  ### Forms and links - nothing to do
@@ -1167,12 +1179,12 @@ async function onSubmit(e: FormEvent) {
1167
1179
  return <form onSubmit={onSubmit}>…</form>
1168
1180
  ```
1169
1181
 
1170
- 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 -
1171
1183
  never a broken navigation. It never touches a `method="post"` form or a GET form
1172
1184
  with a real action. Opt a form out with `data-native`:
1173
1185
 
1174
1186
  ```ts
1175
- <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 */}
1176
1188
  ```
1177
1189
 
1178
1190
  ### Gate other JS on the signal
@@ -1385,9 +1397,9 @@ but the compiled frameworks use their native single-file format - **Svelte**
1385
1397
  package's Bun plugin (`@nifrajs/web-vue/plugin`, `@nifrajs/web-svelte/plugin`).
1386
1398
 
1387
1399
  ```ts
1388
- <!-- 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 -->
1389
1401
  <script lang="ts">
1390
- // 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):
1391
1403
  export const meta = { title: "Home" }
1392
1404
  export async function loader({ api }) {
1393
1405
  const res = await api.count.get()
@@ -1396,7 +1408,7 @@ export async function loader({ api }) {
1396
1408
  </script>
1397
1409
 
1398
1410
  <script setup lang="ts">
1399
- defineProps(["data"]) // nifra passes the loader data in as `data`
1411
+ defineProps(["data"]) // Nifra passes the loader data in as `data`
1400
1412
  </script>
1401
1413
 
1402
1414
  <template>
@@ -1757,7 +1769,7 @@ const app = server()
1757
1769
  .use(trimTrailingSlash()) // canonicalize URLs: 308 redirect (or rewrite), same-origin only, conservative methods
1758
1770
  .use(language({ supported: ["en", "fr"], defaultLanguage: "en" })) // Accept-Language → c.language + Content-Language
1759
1771
  .use(timing()) // Server-Timing header + typed c.timing marks/measures
1760
- // poweredBy() is opt-in (nifra emits no X-Powered-By by default). combine(a, b, c) bundles several into one plugin.
1772
+ // poweredBy() is opt-in (Nifra emits no X-Powered-By by default). combine(a, b, c) bundles several into one plugin.
1761
1773
  ```
1762
1774
 
1763
1775
  ### Security middleware
@@ -2244,7 +2256,7 @@ server function:
2244
2256
 
2245
2257
  On its own that is an assertion. What makes it load-bearing is the other half:
2246
2258
  `provenance.imports` maps a module specifier to the capabilities that reaching it
2247
- implies, so nifra can compare what a route _says_ against what it _can do_.
2259
+ implies, so Nifra can compare what a route _says_ against what it _can do_.
2248
2260
 
2249
2261
  ```ts
2250
2262
  import { server } from "@nifrajs/core/server"
@@ -2491,7 +2503,7 @@ yourself. Nifra owns the _session_; you bring (or mount) the _who_.
2491
2503
  `@nifrajs/better-auth` bridges [Better Auth](https://better-auth.com) into
2492
2504
  nifra: `betterAuth(auth)` mounts its handler at `/api/auth/*` (GET + POST),
2493
2505
  so every endpoint - sign-in/up/out, OAuth callbacks, session, 2FA, magic links - is served by
2494
- your nifra server. Read the session with `getSession(auth, request)` (typed
2506
+ your Nifra server. Read the session with `getSession(auth, request)` (typed
2495
2507
  `{ user, session } | null`) or guard a route with
2496
2508
  `requireSession(auth, request, options?)` (returns it, or throws a 401/redirect
2497
2509
  `Response`). It's declared **structurally** - no hard dependency on Better Auth, so
@@ -2572,7 +2584,7 @@ createWebApp({ /* … */ api: sessions }) // inject the manager
2572
2584
  ### Guard a route
2573
2585
 
2574
2586
  `requireSession` / `requireUser` throw a `Response` (a 302 to
2575
- `redirectTo`, or a 401) when the session is missing - nifra returns a thrown Response
2587
+ `redirectTo`, or a 401) when the session is missing - Nifra returns a thrown Response
2576
2588
  as-is, so the guard short-circuits the loader.
2577
2589
 
2578
2590
  ```ts
@@ -2583,7 +2595,7 @@ import { requireUser } from "@nifrajs/auth" // browser-safe; OK to import in a
2583
2595
  export async function loader({ request, api }) {
2584
2596
  const sessions = api // the manager injected via createWebApp's `api`
2585
2597
  const session = await sessions.read(request)
2586
- // requireUser throws a 302 to /login when there's no session; nifra returns the thrown Response.
2598
+ // requireUser throws a 302 to /login when there's no session; Nifra returns the thrown Response.
2587
2599
  const userId = requireUser(session, "userId", { redirectTo: "/login" })
2588
2600
  return { userId }
2589
2601
  }
@@ -2713,11 +2725,11 @@ const imgix: ImageLoader = ({ src, width, quality }) =>
2713
2725
  `https://my.imgix.net${src}?w=${width}&auto=format${quality ? `&q=${quality}` : ""}`
2714
2726
  ```
2715
2727
 
2716
- ### Self-hosting - nifra's own resize endpoint
2728
+ ### Self-hosting - Nifra's own resize endpoint
2717
2729
 
2718
2730
  No CDN? `@nifrajs/image/server`'s `createImageHandler` is a self-hosted resize
2719
2731
  endpoint backed by `Bun.Image` (libjpeg-turbo / libspng / libwebp, decoded off-thread).
2720
- Pair it with `selfHostedLoader` and nifra resizes your own images - no third party in the
2732
+ Pair it with `selfHostedLoader` and Nifra resizes your own images - no third party in the
2721
2733
  path.
2722
2734
 
2723
2735
  ```ts
@@ -2773,7 +2785,7 @@ decodes/resizes/encodes. Three official backends, all from `@nifrajs/image/backe
2773
2785
 
2774
2786
  -
2775
2787
  `sharpImageBackend(sharp)` - libvips for Node. You pass your `sharp` import
2776
- (nifra keeps zero dependency on it, and you pin the version).
2788
+ (Nifra keeps zero dependency on it, and you pin the version).
2777
2789
 
2778
2790
  -
2779
2791
  `wasmImageBackend(codecs)` - pure-WASM decode/resize/encode you wire up (jSquash is
@@ -2787,7 +2799,7 @@ built in (it never decodes just to read dimensions).
2787
2799
  import { createImageHandler } from "@nifrajs/image/server"
2788
2800
  import { sharpImageBackend, wasmImageBackend } from "@nifrajs/image/backends"
2789
2801
 
2790
- // Node - libvips via sharp. Pass your own import (nifra never depends on it):
2802
+ // Node - libvips via sharp. Pass your own import (Nifra never depends on it):
2791
2803
  import sharp from "sharp"
2792
2804
  createImageHandler({ backend: sharpImageBackend(sharp), root: "./public" })
2793
2805
 
@@ -3055,7 +3067,7 @@ export default toFetchHandler(app, {
3055
3067
 
3056
3068
  `app.ws(path, handler)` registers a WebSocket route. It mirrors
3057
3069
  `.get()`/`.post()` - chainable, with per-connection state typed
3058
- through a generic - and runs on every runtime nifra serves: Bun, Deno, Node, and Cloudflare
3070
+ through a generic - and runs on every runtime Nifra serves: Bun, Deno, Node, and Cloudflare
3059
3071
  Workers.
3060
3072
 
3061
3073
  The WebSocket runtime ships as an opt-in plugin, so apps that never use it don’t bundle it.
@@ -3141,7 +3153,7 @@ escape hatch to the native socket
3141
3153
  Inbound frames arrive raw (`string | Uint8Array`) by default. Add a
3142
3154
  `messageSchema` - any
3143
3155
  [Standard Schema](https://standardschema.dev) (`t`, zod, valibot) - and
3144
- nifra parses each frame as JSON, validates it, and hands `message` the typed
3156
+ Nifra parses each frame as JSON, validates it, and hands `message` the typed
3145
3157
  value; anything that fails goes to `onInvalidMessage` instead (so a malformed
3146
3158
  frame can never reach your handler).
3147
3159
 
@@ -3178,7 +3190,7 @@ Bun, Deno, and Node are long-lived processes, so this works directly on a single
3178
3190
  Across a load balancer, `app.publish` only reaches sockets on the same instance -
3179
3191
  bridge an external fan-out (Redis pub/sub, NATS, a queue) to broadcast across all of them. On
3180
3192
  **Cloudflare Workers**, a stateless isolate can’t broadcast across connections,
3181
- so nifra ships a Durable Object hub: `createWebSocketHub(app)` holds the
3193
+ so Nifra ships a Durable Object hub: `createWebSocketHub(app)` holds the
3182
3194
  connections, and `toFetchHandler(app, { webSocketHub })` routes upgrades to it
3183
3195
  - then `ws.subscribe` / `app.publish` behave exactly as on Bun.
3184
3196
 
@@ -3434,7 +3446,7 @@ Deploy: `wrangler pages deploy dist` (Pages) or `wrangler deploy`
3434
3446
  `server.ts` to hand-write. (`create-nifra` scaffolds the conventions.)
3435
3447
 
3436
3448
  ```ts
3437
- nifra dev # true-HMR dev server (Vite middleware + nifra SSR) - http://localhost:4321
3449
+ nifra dev # true-HMR dev server (Vite middleware + Nifra SSR) - http://localhost:4321
3438
3450
  nifra build # full Bun deploy → dist/server.js + content-hashed dist/assets/ (default target: bun)
3439
3451
  nifra start # run dist/server.js on Bun
3440
3452
  nifra build --target cf-pages # also: node | deno | vercel | static; add --report for chunk sizes
@@ -3510,6 +3522,50 @@ Nifra ships an MCP server. It is not a documentation lookup bolted onto a framew
3510
3522
  can read the project, learn the real API, run actual requests against the backend it just
3511
3523
  edited, and finish with a report of what the change proved.
3512
3524
 
3525
+ ### One MCP, two ways to connect
3526
+
3527
+ There is one Nifra MCP. It reaches your agent over the two standard MCP transports, and which
3528
+ one you use is decided by a single question: **is the agent working inside a Nifra repo?**
3529
+
3530
+ Local - `nifra mcp`
3531
+
3532
+ Hosted - `mcp.nifra.dev`
3533
+
3534
+ Transport
3535
+ stdio - the agent spawns it as a process
3536
+ HTTP - add one URL, nothing to install
3537
+
3538
+ Runs
3539
+ on your machine, in your project
3540
+ on our infrastructure
3541
+
3542
+ Sees
3543
+ your routes, schemas, and files - locally only, nothing leaves the machine
3544
+ only Nifra's published docs corpus - never your code
3545
+
3546
+ Tools
3547
+
3548
+ everything: project tools (`nifra_context`, `nifra_run`,
3549
+ `nifra_assure`, …) **plus** the docs tools
3550
+
3551
+ docs tools only (`nifra_docs`, `nifra_example`,
3552
+ `nifra_types`, `nifra_learn`)
3553
+
3554
+ Use when
3555
+ building or editing a Nifra app
3556
+ learning Nifra, or the client can't spawn processes (Claude.ai, ChatGPT)
3557
+
3558
+ The project tools _must_ run where your code is - hosting them would mean uploading
3559
+ your source, which is exactly what this design refuses to do. And the local server does not
3560
+ proxy the hosted one for docs: the docs corpus ships inside the npm package, so the answers
3561
+ match **the Nifra version installed in your project**, work offline, and send nothing
3562
+ anywhere. This hosted-plus-local pairing is the same shape Supabase, Stripe, Sentry, and
3563
+ GitHub ship their MCP servers in, for the same reason: public knowledge can be hosted; tools
3564
+ that touch your own code and data run where that code lives.
3565
+
3566
+ So: inside a Nifra repo, register the local server (it includes the docs tools - you never
3567
+ need both). Anywhere else, add the URL.
3568
+
3513
3569
  ### Setup
3514
3570
 
3515
3571
  ```ts
@@ -3736,7 +3792,7 @@ const server = await createViteDevServer({
3736
3792
  routesDir,
3737
3793
  clientModule: "@nifrajs/web-react/client",
3738
3794
  plugins: [react()], // Vue: @vitejs/plugin-vue, Svelte: …, etc.
3739
- port: Number(Bun.env.PORT ?? 4321), // nifra's default; --port / PORT override it
3795
+ port: Number(Bun.env.PORT ?? 4321), // Nifra's default; --port / PORT override it
3740
3796
  createApp: (clientEntry, importQuery) =>
3741
3797
  createWebApp({
3742
3798
  adapter: reactAdapter,
@@ -3906,13 +3962,13 @@ so `node:` builtins and `server-only` modules fail the build either way.
3906
3962
 
3907
3963
  ```ts
3908
3964
  // vite.config.ts - a Vite/Rollup PRODUCTION client build (the escape hatch, not the default).
3909
- // Only reach for this when an app needs a Vite-only transform with no Bun equivalent; nifra's default
3965
+ // Only reach for this when an app needs a Vite-only transform with no Bun equivalent; Nifra's default
3910
3966
  // production bundler stays Bun (buildClient), which is faster and Bun-native.
3911
3967
  import { viteLeakGuard } from "@nifrajs/web/plugins/vite-leak-guard"
3912
3968
 
3913
3969
  export default {
3914
3970
  build: {
3915
- // The SAME two client-leak guards nifra's Bun build runs - server-only code or a node: builtin
3971
+ // The SAME two client-leak guards Nifra's Bun build runs - server-only code or a node: builtin
3916
3972
  // reaching the browser fails the build, with the identical error message. A second production
3917
3973
  // pipeline must not ship without them.
3918
3974
  rollupOptions: { plugins: [viteLeakGuard()] },
@@ -4841,7 +4897,7 @@ call.
4841
4897
  import type { LoaderContext } from "@nifrajs/web"
4842
4898
 
4843
4899
  // (1) A page's own data + mutations → route exports. A PUBLIC POST is a route ACTION
4844
- // (routes/contact.tsx) - the browser POSTs the route's own URL, nifra runs `action`:
4900
+ // (routes/contact.tsx) - the browser POSTs the route's own URL, Nifra runs `action`:
4845
4901
  export async function action(ctx: LoaderContext) {
4846
4902
  const form = await ctx.request.formData() // the browser POSTed this route
4847
4903
  return { ok: true, body: String(form.get("body")) } // → props.actionData on re-render
@@ -5617,7 +5673,7 @@ await Bun.write("app/fonts.preloads.json", JSON.stringify(inter.preloads))
5617
5673
  Then wire the two outputs into your app:
5618
5674
 
5619
5675
  ```ts
5620
- // app entry - bundled + content-hashed by nifra's CSS pipeline
5676
+ // app entry - bundled + content-hashed by Nifra's CSS pipeline
5621
5677
  import "./fonts.css"
5622
5678
 
5623
5679
  // root layout - preload the primary file (one less render-blocking round trip)
@@ -5686,6 +5742,102 @@ stylesheet can't redirect the download elsewhere - an SSRF gate), and caps each
5686
5742
  Filenames are derived only from validated tokens plus a content hash, so there's no path
5687
5743
  traversal.
5688
5744
 
5745
+ ## Nifra - Integrations (Stripe, and any npm library)
5746
+
5747
+ > 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.
5748
+
5749
+ The most important thing on this page: **you rarely need it**. A Nifra handler
5750
+ is plain TypeScript running on Bun, Node, Deno, or the edge - the npm ecosystem works
5751
+ as-is. Stripe, OpenAI, Resend, Drizzle, Prisma, AWS SDKs: import them and call them, the
5752
+ way their own documentation shows.
5753
+
5754
+ ### When you need nothing at all
5755
+
5756
+ If a library is called from your code - an SDK, a database client, a date library - there is
5757
+ no integration step. It does not know or care that Nifra is serving the request. The one
5758
+ rule that still applies is Nifra's own: anything crossing a trust boundary (a request body,
5759
+ a webhook payload, a third-party response you act on) goes through a schema first.
5760
+
5761
+ ### The Stripe recipe
5762
+
5763
+ Three parts, each already covered by something the framework ships.
5764
+
5765
+ #### 1. Calling the SDK
5766
+
5767
+ ```ts
5768
+ // doc-check: skip - fragment: `env` and your price ids are your application's.
5769
+ import { server } from "@nifrajs/core/server"
5770
+ import { t } from "@nifrajs/schema"
5771
+ import Stripe from "stripe"
5772
+
5773
+ // The Stripe SDK, exactly as its own docs show it. No adapter, no wrapper, no plugin.
5774
+ const stripe = new Stripe(env.STRIPE_SECRET_KEY)
5775
+
5776
+ export const billing = server().post(
5777
+ "/api/checkout",
5778
+ { body: t.object({ priceId: t.string({ minLength: 1 }) }) },
5779
+ async (c) => {
5780
+ const session = await stripe.checkout.sessions.create({
5781
+ mode: "subscription",
5782
+ line_items: [{ price: c.body.priceId, quantity: 1 }],
5783
+ success_url: "https://app.example.com/billing?done=1",
5784
+ cancel_url: "https://app.example.com/billing",
5785
+ })
5786
+ return { url: session.url }
5787
+ },
5788
+ )
5789
+ ```
5790
+
5791
+ Ordinary route, ordinary SDK call. The body schema is doing real work: a checkout endpoint
5792
+ is a public POST, and `c.body.priceId` arrives validated and typed.
5793
+
5794
+ #### 2. Receiving webhooks
5795
+
5796
+ ```ts
5797
+ // doc-check: skip - fragment: `app`, `env`, and `StripeEvent` are your application's.
5798
+ import { verifyWebhook } from "@nifrajs/core/webhook"
5799
+
5800
+ app.post("/webhooks/stripe", async (c) => {
5801
+ // Bounded raw-body read + constant-time HMAC check, BEFORE any parsing.
5802
+ const r = await verifyWebhook(c.req, env.STRIPE_WEBHOOK_SECRET, { provider: "stripe" })
5803
+ if (!r.ok) { c.set.status = 400; return { ok: false, error: r.reason } }
5804
+
5805
+ const event = StripeEvent.parse(JSON.parse(r.payload)) // validate at the trust boundary
5806
+ // …fulfil the event…
5807
+ return { ok: true }
5808
+ })
5809
+ ```
5810
+
5811
+ This is the part teams hand-roll incorrectly. `verifyWebhook` reads the raw body
5812
+ bounded (DoS guard), verifies the signature in constant time, checks the timestamp
5813
+ tolerance, and only then hands you the payload. Presets exist for Stripe and GitHub, and a
5814
+ generic header/encoding form covers any other provider - including key rotation via an
5815
+ array of secrets. Details in [security](/docs/security).
5816
+
5817
+ #### 3. Processing exactly once
5818
+
5819
+ ```ts
5820
+ // doc-check: skip - fragment: `app` and `fulfil` are your application's.
5821
+ import { idempotency, MemoryIdempotencyStore } from "@nifrajs/middleware"
5822
+
5823
+ // Stripe redelivers webhooks and clients retry checkouts. With the idempotency
5824
+ // middleware, a retried request with the same Idempotency-Key replays the FIRST
5825
+ // response - the side effect runs once. Use a shared store (Redis) in production.
5826
+ app.use(idempotency({ store: new MemoryIdempotencyStore() }))
5827
+ ```
5828
+
5829
+ Stripe redelivers; users double-click. The `idempotency` middleware replays the
5830
+ first response for a retried key instead of re-running the charge, and rejects a concurrent
5831
+ duplicate while the first is in flight.
5832
+
5833
+ ### When to write a plugin
5834
+
5835
+ Reach for [a plugin](/docs/plugins) only when a library should participate in
5836
+ the framework itself: contribute typed context to every handler (`c.stripe`, a
5837
+ session, a tenant), mount routes of its own, or hook the request lifecycle. That is
5838
+ `definePlugin` - a few lines, typed end-to-end. For everything else, an import
5839
+ at the top of the file is the whole integration.
5840
+
5689
5841
  ## Nifra 2.0 migration guide - upgrade from 1.x
5690
5842
 
5691
5843
  > Upgrade a Nifra 1.x application to 2.0: automated package and import moves, opt-in runtime plugins, client and web changes, external auth mounts, and release verification.
@@ -5700,7 +5852,7 @@ and import edits; this guide covers the structural changes it deliberately canno
5700
5852
 
5701
5853
  ```
5702
5854
 
5703
- The command updates existing `@nifrajs/*`, `nifra`, and
5855
+ The command updates existing `@nifrajs/*`, `Nifra`, and
5704
5856
  `create-nifra` dependency ranges to `2.0.0` while preserving
5705
5857
  caret/tilde/exact style. It also replaces the removed `@nifrajs/budget` dependency
5706
5858
  with `@nifrajs/core` and moves its imports to
@@ -5723,7 +5875,7 @@ Only applications calling `app.resolveNode()` directly need to install it.
5723
5875
 
5724
5876
  ### 3. Use the lean package entry points
5725
5877
 
5726
- The `@nifrajs/core` and `nifra` roots now expose the lean HTTP server
5878
+ The `@nifrajs/core` and `Nifra` roots now expose the lean HTTP server
5727
5879
  surface. Prefer `@nifrajs/core/server` for `server()`, and import
5728
5880
  contracts, assurance, capabilities, budgets, manifests, reflection, SSE, WebSockets, and
5729
5881
  other optional systems from their documented subpaths.
@@ -5877,7 +6029,7 @@ onto the route. The client then infers all of this for free.
5877
6029
  app.get("/users/:id", (req, res) => res.json({ id: req.params.id }))
5878
6030
  app.post("/users", express.json(), (req, res) => res.status(201).json(create(req.body)))
5879
6031
 
5880
- // nifra - return the value (or a Response); c is Web-standard; the body is parsed + validated
6032
+ // Nifra - return the value (or a Response); c is Web-standard; the body is parsed + validated
5881
6033
  server()
5882
6034
  .get("/users/:id", (c) => ({ id: c.params.id }))
5883
6035
  .post("/users", { body: t.object({ name: t.string() }) }, (c) => create(c.body))
@@ -5893,7 +6045,7 @@ adds the typed client, schema validation, multi-runtime deploy, and (if you want
5893
6045
  // Hono
5894
6046
  app.get("/users/:id", (c) => c.json({ id: c.req.param("id") }))
5895
6047
 
5896
- // nifra - nearly identical; params are a typed object, you return the value
6048
+ // Nifra - nearly identical; params are a typed object, you return the value
5897
6049
  server().get("/users/:id", (c) => ({ id: c.params.id }))
5898
6050
  ```
5899
6051
 
@@ -5975,7 +6127,7 @@ export default async function Page({ params }) {
5975
6127
  return <h1>{user.name}</h1>
5976
6128
  }
5977
6129
 
5978
- // nifra - routes/users/[id].tsx
6130
+ // Nifra - routes/users/[id].tsx
5979
6131
  export async function loader({ params, api }: LoaderArgs<typeof backend>) {
5980
6132
  const res = await api.users({ id: params.id }).get() // typed, in-process during SSR
5981
6133
  return { user: res.data }
@@ -5996,7 +6148,7 @@ example:
5996
6148
  // SvelteKit - +page.server.ts + +page.svelte
5997
6149
  export async function load({ params }) { return { post: await getPost(params.slug) } }
5998
6150
 
5999
- // nifra - routes/blog/[slug].svelte (loader is a module export, page is the .svelte)
6151
+ // Nifra - routes/blog/[slug].svelte (loader is a module export, page is the .svelte)
6000
6152
  export async function loader({ params }) { return { post: await getPost(params.slug) } }
6001
6153
  ```
6002
6154
 
@@ -6007,7 +6159,7 @@ export async function loader({ params }) { return { post: await getPost(params.s
6007
6159
  app.
6008
6160
 
6009
6161
  -
6010
- **Much faster SSR** - Nifra renders ~28× Next.js, ~7× Nuxt, ~4× SvelteKit/SolidStart on
6162
+ **Much faster SSR** - Nifra renders ~25× Next.js, ~7× Nuxt, ~4× SvelteKit/SolidStart on
6011
6163
  dynamic pages, with a fraction of the client JS. See [benchmarks](/benchmarks).
6012
6164
 
6013
6165
  -
@@ -6385,7 +6537,7 @@ export default defineAssuranceConfig({
6385
6537
  ```
6386
6538
 
6387
6539
  L4 requires an **explicit** executor. Invariant runs send deliberately hostile
6388
- input, so nifra will not guess which app to point them at; a level that fuzzed your live
6540
+ input, so Nifra will not guess which app to point them at; a level that fuzzed your live
6389
6541
  server by default would be a footgun, and a skipped route is reported rather than passed over
6390
6542
  in silence.
6391
6543
 
package/docs/types.json CHANGED
@@ -10821,14 +10821,14 @@
10821
10821
  "name": "VERSION",
10822
10822
  "kind": "const",
10823
10823
  "package": "@nifrajs/core",
10824
- "signature": "export declare const VERSION: \"2.8.0\";",
10824
+ "signature": "export declare const VERSION: \"2.8.2\";",
10825
10825
  "doc": "/**\n * Current package version. A hardcoded literal on purpose - core runs on the edge (no fs), so it can't\n * read its own package.json at runtime. `scripts/version.ts` rewrites it on every release bump and\n * `check:publish` asserts it equals `@nifrajs/core`'s package version.\n */"
10826
10826
  },
10827
10827
  {
10828
10828
  "name": "VERSION",
10829
10829
  "kind": "const",
10830
10830
  "package": "nifra",
10831
- "signature": "export declare const VERSION: \"2.8.0\";",
10831
+ "signature": "export declare const VERSION: \"2.8.2\";",
10832
10832
  "doc": "/**\n * Current package version. A hardcoded literal on purpose - core runs on the edge (no fs), so it can't\n * read its own package.json at runtime. `scripts/version.ts` rewrites it on every release bump and\n * `check:publish` asserts it equals `@nifrajs/core`'s package version.\n */"
10833
10833
  },
10834
10834
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/cli",
3
- "version": "2.8.0",
3
+ "version": "2.8.2",
4
4
  "description": "The nifra CLI - zero-config dev/build/start for a nifra app (`nifra dev`, `nifra build`, `nifra start`).",
5
5
  "keywords": [
6
6
  "bun",
@@ -41,14 +41,14 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@nifrajs/client": "2.8.0",
45
- "@nifrajs/core": "2.8.0",
46
- "@nifrajs/mcp": "2.8.0",
47
- "@nifrajs/runner": "2.8.0",
48
- "@nifrajs/schema": "2.8.0",
49
- "@nifrajs/testing": "2.8.0",
50
- "@nifrajs/web": "2.8.0",
51
- "create-nifra": "2.8.0"
44
+ "@nifrajs/client": "2.8.2",
45
+ "@nifrajs/core": "2.8.2",
46
+ "@nifrajs/mcp": "2.8.2",
47
+ "@nifrajs/runner": "2.8.2",
48
+ "@nifrajs/schema": "2.8.2",
49
+ "@nifrajs/testing": "2.8.2",
50
+ "@nifrajs/web": "2.8.2",
51
+ "create-nifra": "2.8.2"
52
52
  },
53
53
  "repository": {
54
54
  "type": "git",