@nifrajs/cli 2.8.1 → 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/cli.js CHANGED
@@ -134,7 +134,7 @@ routes/ from the current directory. Run from your project root.
134
134
  Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with \`--port <n>\` (alias \`-p\`) or the
135
135
  \`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).`;
136
136
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
137
- const CLI_VERSION = "2.8.1";
137
+ const CLI_VERSION = "2.8.2";
138
138
  // A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
139
139
  const asAdapter = (v) => v;
140
140
  const asBunPlugins = (v) => v;
package/dist/mcp-http.js CHANGED
@@ -23,7 +23,7 @@ import { loadTypesCorpus } from "./types-search.js";
23
23
  export { examplesAppTool, examplesWidget } from "./mcp-app-tool.js";
24
24
  export { docsTools } from "./mcp-docs-tools.js";
25
25
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
26
- const VERSION = "2.8.1";
26
+ const VERSION = "2.8.2";
27
27
  const SERVER_INFO = { name: "nifra-docs", version: VERSION };
28
28
  // Derive the GET/health tool list from the tools actually served, so the line can never drift from them.
29
29
  const docsHealth = (tools) => `nifra docs MCP - POST JSON-RPC 2.0 here (methods: initialize, tools/list, tools/call). Tools: ${tools
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAA;AAWnF,sGAAsG;AACtG,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAGrF;AAWD;mFACmF;AACnF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAUpE;AAiCD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,mBAAoB,SAAQ,cAAc;IACzD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAED;oFACoF;AACpF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,cAAc,CAenF;AAWD;;oEAEoE;AACpE,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,mBAAmB,CAAC,CAoB9B;AAED,oGAAoG;AACpG,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,MAAM,CAS5E"}
1
+ {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAA;AAWnF,sGAAsG;AACtG,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAGrF;AAWD;mFACmF;AACnF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAUpE;AAiCD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,mBAAoB,SAAQ,cAAc;IACzD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAED;oFACoF;AACpF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,cAAc,CAenF;AA0CD;;oEAEoE;AACpE,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,mBAAmB,CAAC,CAsB9B;AAED,oGAAoG;AACpG,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,MAAM,CAS5E"}
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
 
@@ -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>
@@ -1759,7 +1769,7 @@ const app = server()
1759
1769
  .use(trimTrailingSlash()) // canonicalize URLs: 308 redirect (or rewrite), same-origin only, conservative methods
1760
1770
  .use(language({ supported: ["en", "fr"], defaultLanguage: "en" })) // Accept-Language → c.language + Content-Language
1761
1771
  .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.
1772
+ // poweredBy() is opt-in (Nifra emits no X-Powered-By by default). combine(a, b, c) bundles several into one plugin.
1763
1773
  ```
1764
1774
 
1765
1775
  ### Security middleware
@@ -2246,7 +2256,7 @@ server function:
2246
2256
 
2247
2257
  On its own that is an assertion. What makes it load-bearing is the other half:
2248
2258
  `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_.
2259
+ implies, so Nifra can compare what a route _says_ against what it _can do_.
2250
2260
 
2251
2261
  ```ts
2252
2262
  import { server } from "@nifrajs/core/server"
@@ -2493,7 +2503,7 @@ yourself. Nifra owns the _session_; you bring (or mount) the _who_.
2493
2503
  `@nifrajs/better-auth` bridges [Better Auth](https://better-auth.com) into
2494
2504
  nifra: `betterAuth(auth)` mounts its handler at `/api/auth/*` (GET + POST),
2495
2505
  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
2506
+ your Nifra server. Read the session with `getSession(auth, request)` (typed
2497
2507
  `{ user, session } | null`) or guard a route with
2498
2508
  `requireSession(auth, request, options?)` (returns it, or throws a 401/redirect
2499
2509
  `Response`). It's declared **structurally** - no hard dependency on Better Auth, so
@@ -2574,7 +2584,7 @@ createWebApp({ /* … */ api: sessions }) // inject the manager
2574
2584
  ### Guard a route
2575
2585
 
2576
2586
  `requireSession` / `requireUser` throw a `Response` (a 302 to
2577
- `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
2578
2588
  as-is, so the guard short-circuits the loader.
2579
2589
 
2580
2590
  ```ts
@@ -2585,7 +2595,7 @@ import { requireUser } from "@nifrajs/auth" // browser-safe; OK to import in a
2585
2595
  export async function loader({ request, api }) {
2586
2596
  const sessions = api // the manager injected via createWebApp's `api`
2587
2597
  const session = await sessions.read(request)
2588
- // 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.
2589
2599
  const userId = requireUser(session, "userId", { redirectTo: "/login" })
2590
2600
  return { userId }
2591
2601
  }
@@ -2715,11 +2725,11 @@ const imgix: ImageLoader = ({ src, width, quality }) =>
2715
2725
  `https://my.imgix.net${src}?w=${width}&auto=format${quality ? `&q=${quality}` : ""}`
2716
2726
  ```
2717
2727
 
2718
- ### Self-hosting - nifra's own resize endpoint
2728
+ ### Self-hosting - Nifra's own resize endpoint
2719
2729
 
2720
2730
  No CDN? `@nifrajs/image/server`'s `createImageHandler` is a self-hosted resize
2721
2731
  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
2732
+ Pair it with `selfHostedLoader` and Nifra resizes your own images - no third party in the
2723
2733
  path.
2724
2734
 
2725
2735
  ```ts
@@ -2775,7 +2785,7 @@ decodes/resizes/encodes. Three official backends, all from `@nifrajs/image/backe
2775
2785
 
2776
2786
  -
2777
2787
  `sharpImageBackend(sharp)` - libvips for Node. You pass your `sharp` import
2778
- (nifra keeps zero dependency on it, and you pin the version).
2788
+ (Nifra keeps zero dependency on it, and you pin the version).
2779
2789
 
2780
2790
  -
2781
2791
  `wasmImageBackend(codecs)` - pure-WASM decode/resize/encode you wire up (jSquash is
@@ -2789,7 +2799,7 @@ built in (it never decodes just to read dimensions).
2789
2799
  import { createImageHandler } from "@nifrajs/image/server"
2790
2800
  import { sharpImageBackend, wasmImageBackend } from "@nifrajs/image/backends"
2791
2801
 
2792
- // 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):
2793
2803
  import sharp from "sharp"
2794
2804
  createImageHandler({ backend: sharpImageBackend(sharp), root: "./public" })
2795
2805
 
@@ -3057,7 +3067,7 @@ export default toFetchHandler(app, {
3057
3067
 
3058
3068
  `app.ws(path, handler)` registers a WebSocket route. It mirrors
3059
3069
  `.get()`/`.post()` - chainable, with per-connection state typed
3060
- 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
3061
3071
  Workers.
3062
3072
 
3063
3073
  The WebSocket runtime ships as an opt-in plugin, so apps that never use it don’t bundle it.
@@ -3143,7 +3153,7 @@ escape hatch to the native socket
3143
3153
  Inbound frames arrive raw (`string | Uint8Array`) by default. Add a
3144
3154
  `messageSchema` - any
3145
3155
  [Standard Schema](https://standardschema.dev) (`t`, zod, valibot) - and
3146
- 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
3147
3157
  value; anything that fails goes to `onInvalidMessage` instead (so a malformed
3148
3158
  frame can never reach your handler).
3149
3159
 
@@ -3180,7 +3190,7 @@ Bun, Deno, and Node are long-lived processes, so this works directly on a single
3180
3190
  Across a load balancer, `app.publish` only reaches sockets on the same instance -
3181
3191
  bridge an external fan-out (Redis pub/sub, NATS, a queue) to broadcast across all of them. On
3182
3192
  **Cloudflare Workers**, a stateless isolate can’t broadcast across connections,
3183
- so nifra ships a Durable Object hub: `createWebSocketHub(app)` holds the
3193
+ so Nifra ships a Durable Object hub: `createWebSocketHub(app)` holds the
3184
3194
  connections, and `toFetchHandler(app, { webSocketHub })` routes upgrades to it
3185
3195
  - then `ws.subscribe` / `app.publish` behave exactly as on Bun.
3186
3196
 
@@ -3436,7 +3446,7 @@ Deploy: `wrangler pages deploy dist` (Pages) or `wrangler deploy`
3436
3446
  `server.ts` to hand-write. (`create-nifra` scaffolds the conventions.)
3437
3447
 
3438
3448
  ```ts
3439
- 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
3440
3450
  nifra build # full Bun deploy → dist/server.js + content-hashed dist/assets/ (default target: bun)
3441
3451
  nifra start # run dist/server.js on Bun
3442
3452
  nifra build --target cf-pages # also: node | deno | vercel | static; add --report for chunk sizes
@@ -3512,6 +3522,50 @@ Nifra ships an MCP server. It is not a documentation lookup bolted onto a framew
3512
3522
  can read the project, learn the real API, run actual requests against the backend it just
3513
3523
  edited, and finish with a report of what the change proved.
3514
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
+
3515
3569
  ### Setup
3516
3570
 
3517
3571
  ```ts
@@ -3738,7 +3792,7 @@ const server = await createViteDevServer({
3738
3792
  routesDir,
3739
3793
  clientModule: "@nifrajs/web-react/client",
3740
3794
  plugins: [react()], // Vue: @vitejs/plugin-vue, Svelte: …, etc.
3741
- 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
3742
3796
  createApp: (clientEntry, importQuery) =>
3743
3797
  createWebApp({
3744
3798
  adapter: reactAdapter,
@@ -3908,13 +3962,13 @@ so `node:` builtins and `server-only` modules fail the build either way.
3908
3962
 
3909
3963
  ```ts
3910
3964
  // 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
3965
+ // Only reach for this when an app needs a Vite-only transform with no Bun equivalent; Nifra's default
3912
3966
  // production bundler stays Bun (buildClient), which is faster and Bun-native.
3913
3967
  import { viteLeakGuard } from "@nifrajs/web/plugins/vite-leak-guard"
3914
3968
 
3915
3969
  export default {
3916
3970
  build: {
3917
- // 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
3918
3972
  // reaching the browser fails the build, with the identical error message. A second production
3919
3973
  // pipeline must not ship without them.
3920
3974
  rollupOptions: { plugins: [viteLeakGuard()] },
@@ -4843,7 +4897,7 @@ call.
4843
4897
  import type { LoaderContext } from "@nifrajs/web"
4844
4898
 
4845
4899
  // (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`:
4900
+ // (routes/contact.tsx) - the browser POSTs the route's own URL, Nifra runs `action`:
4847
4901
  export async function action(ctx: LoaderContext) {
4848
4902
  const form = await ctx.request.formData() // the browser POSTed this route
4849
4903
  return { ok: true, body: String(form.get("body")) } // → props.actionData on re-render
@@ -5619,7 +5673,7 @@ await Bun.write("app/fonts.preloads.json", JSON.stringify(inter.preloads))
5619
5673
  Then wire the two outputs into your app:
5620
5674
 
5621
5675
  ```ts
5622
- // app entry - bundled + content-hashed by nifra's CSS pipeline
5676
+ // app entry - bundled + content-hashed by Nifra's CSS pipeline
5623
5677
  import "./fonts.css"
5624
5678
 
5625
5679
  // root layout - preload the primary file (one less render-blocking round trip)
@@ -5690,9 +5744,9 @@ traversal.
5690
5744
 
5691
5745
  ## Nifra - Integrations (Stripe, and any npm library)
5692
5746
 
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.
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.
5694
5748
 
5695
- The most important thing on this page: **you rarely need it**. A nifra handler
5749
+ The most important thing on this page: **you rarely need it**. A Nifra handler
5696
5750
  is plain TypeScript running on Bun, Node, Deno, or the edge - the npm ecosystem works
5697
5751
  as-is. Stripe, OpenAI, Resend, Drizzle, Prisma, AWS SDKs: import them and call them, the
5698
5752
  way their own documentation shows.
@@ -5700,8 +5754,8 @@ way their own documentation shows.
5700
5754
  ### When you need nothing at all
5701
5755
 
5702
5756
  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,
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,
5705
5759
  a webhook payload, a third-party response you act on) goes through a schema first.
5706
5760
 
5707
5761
  ### The Stripe recipe
@@ -5798,7 +5852,7 @@ and import edits; this guide covers the structural changes it deliberately canno
5798
5852
 
5799
5853
  ```
5800
5854
 
5801
- The command updates existing `@nifrajs/*`, `nifra`, and
5855
+ The command updates existing `@nifrajs/*`, `Nifra`, and
5802
5856
  `create-nifra` dependency ranges to `2.0.0` while preserving
5803
5857
  caret/tilde/exact style. It also replaces the removed `@nifrajs/budget` dependency
5804
5858
  with `@nifrajs/core` and moves its imports to
@@ -5821,7 +5875,7 @@ Only applications calling `app.resolveNode()` directly need to install it.
5821
5875
 
5822
5876
  ### 3. Use the lean package entry points
5823
5877
 
5824
- 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
5825
5879
  surface. Prefer `@nifrajs/core/server` for `server()`, and import
5826
5880
  contracts, assurance, capabilities, budgets, manifests, reflection, SSE, WebSockets, and
5827
5881
  other optional systems from their documented subpaths.
@@ -5975,7 +6029,7 @@ onto the route. The client then infers all of this for free.
5975
6029
  app.get("/users/:id", (req, res) => res.json({ id: req.params.id }))
5976
6030
  app.post("/users", express.json(), (req, res) => res.status(201).json(create(req.body)))
5977
6031
 
5978
- // 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
5979
6033
  server()
5980
6034
  .get("/users/:id", (c) => ({ id: c.params.id }))
5981
6035
  .post("/users", { body: t.object({ name: t.string() }) }, (c) => create(c.body))
@@ -5991,7 +6045,7 @@ adds the typed client, schema validation, multi-runtime deploy, and (if you want
5991
6045
  // Hono
5992
6046
  app.get("/users/:id", (c) => c.json({ id: c.req.param("id") }))
5993
6047
 
5994
- // 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
5995
6049
  server().get("/users/:id", (c) => ({ id: c.params.id }))
5996
6050
  ```
5997
6051
 
@@ -6073,7 +6127,7 @@ export default async function Page({ params }) {
6073
6127
  return <h1>{user.name}</h1>
6074
6128
  }
6075
6129
 
6076
- // nifra - routes/users/[id].tsx
6130
+ // Nifra - routes/users/[id].tsx
6077
6131
  export async function loader({ params, api }: LoaderArgs<typeof backend>) {
6078
6132
  const res = await api.users({ id: params.id }).get() // typed, in-process during SSR
6079
6133
  return { user: res.data }
@@ -6094,7 +6148,7 @@ example:
6094
6148
  // SvelteKit - +page.server.ts + +page.svelte
6095
6149
  export async function load({ params }) { return { post: await getPost(params.slug) } }
6096
6150
 
6097
- // 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)
6098
6152
  export async function loader({ params }) { return { post: await getPost(params.slug) } }
6099
6153
  ```
6100
6154
 
@@ -6105,7 +6159,7 @@ export async function loader({ params }) { return { post: await getPost(params.s
6105
6159
  app.
6106
6160
 
6107
6161
  -
6108
- **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
6109
6163
  dynamic pages, with a fraction of the client JS. See [benchmarks](/benchmarks).
6110
6164
 
6111
6165
  -
@@ -6483,7 +6537,7 @@ export default defineAssuranceConfig({
6483
6537
  ```
6484
6538
 
6485
6539
  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
6540
+ input, so Nifra will not guess which app to point them at; a level that fuzzed your live
6487
6541
  server by default would be a footgun, and a skipped route is reported rather than passed over
6488
6542
  in silence.
6489
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.1\";",
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.1\";",
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.1",
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.1",
45
- "@nifrajs/core": "2.8.1",
46
- "@nifrajs/mcp": "2.8.1",
47
- "@nifrajs/runner": "2.8.1",
48
- "@nifrajs/schema": "2.8.1",
49
- "@nifrajs/testing": "2.8.1",
50
- "@nifrajs/web": "2.8.1",
51
- "create-nifra": "2.8.1"
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",
package/src/cli.ts CHANGED
@@ -148,7 +148,7 @@ Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with
148
148
  \`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).`
149
149
 
150
150
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
151
- const CLI_VERSION = "2.8.1"
151
+ const CLI_VERSION = "2.8.2"
152
152
 
153
153
  // A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
154
154
  const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
package/src/mcp-http.ts CHANGED
@@ -29,7 +29,7 @@ export { docsTools } from "./mcp-docs-tools.ts"
29
29
  export type { TypeEntry } from "./types-search.ts"
30
30
 
31
31
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
32
- const VERSION = "2.8.1"
32
+ const VERSION = "2.8.2"
33
33
  const SERVER_INFO = { name: "nifra-docs", version: VERSION }
34
34
  // Derive the GET/health tool list from the tools actually served, so the line can never drift from them.
35
35
  const docsHealth = (tools: McpTool[]): string =>
package/src/scaffold.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * `nifra_example` for the body, rather than hand-writing an SFC we can't typecheck here.
10
10
  */
11
11
 
12
- import { mkdir, writeFile } from "node:fs/promises"
12
+ import { lstat, mkdir, realpath, writeFile } from "node:fs/promises"
13
13
  import { dirname, resolve, sep } from "node:path"
14
14
 
15
15
  export type Framework = "react" | "preact" | "solid" | "vue" | "svelte" | "vanilla"
@@ -122,6 +122,37 @@ function resolveInsideCwd(cwd: string, relativeFile: string): string {
122
122
  return target
123
123
  }
124
124
 
125
+ /** Lexical containment is not enough when an existing route directory is a symlink. Check every
126
+ * existing ancestor and its real path before scaffolding so an agent cannot redirect a write outside
127
+ * the selected project. The target itself is created with `wx`, so an existing target symlink is also
128
+ * never followed/overwritten. */
129
+ async function assertNoSymlinkedAncestors(root: string, target: string): Promise<void> {
130
+ const rootPath = resolve(root)
131
+ const realRoot = await realpath(rootPath)
132
+ let current = dirname(target)
133
+ while (current !== rootPath) {
134
+ if (!current.startsWith(`${rootPath}${sep}`)) {
135
+ throw new Error(`refusing to write outside project root: ${target}`)
136
+ }
137
+ try {
138
+ if ((await lstat(current)).isSymbolicLink()) {
139
+ throw new Error(`refusing to write through symlinked directory: ${current}`)
140
+ }
141
+ const actual = await realpath(current)
142
+ if (actual !== realRoot && !actual.startsWith(`${realRoot}${sep}`)) {
143
+ throw new Error(`refusing to write outside project root through: ${current}`)
144
+ }
145
+ } catch (err) {
146
+ if (err && typeof err === "object" && (err as { code?: string }).code === "ENOENT") {
147
+ current = dirname(current)
148
+ continue
149
+ }
150
+ throw err
151
+ }
152
+ current = dirname(current)
153
+ }
154
+ }
155
+
125
156
  /** Write a scaffolded route stub when the framework has a verified ready-to-write body. The write is
126
157
  * intentionally conservative: it refuses non-JSX stubs (where we only return contract guidance) and
127
158
  * uses `wx`, so an agent cannot overwrite user work by accident. */
@@ -139,7 +170,9 @@ export async function writeScaffoldRoute(
139
170
  }
140
171
  }
141
172
  const target = resolveInsideCwd(cwd, result.file)
173
+ await assertNoSymlinkedAncestors(cwd, target)
142
174
  await mkdir(dirname(target), { recursive: true })
175
+ await assertNoSymlinkedAncestors(cwd, target)
143
176
  try {
144
177
  await writeFile(target, result.content, { flag: "wx" })
145
178
  } catch (err) {