@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/check.d.ts +3 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +17 -7
- package/dist/check.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -1
- package/dist/doctor.js.map +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 +3 -3
- package/docs/llms-full.txt +191 -39
- package/docs/types.json +2 -2
- package/package.json +9 -9
- package/src/check.ts +18 -7
- package/src/cli.ts +1 -1
- package/src/doctor.ts +19 -6
- package/src/mcp-http.ts +1 -1
- package/src/scaffold.ts +34 -1
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
|
}
|
package/dist/scaffold.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"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"}
|
package/docs/examples.json
CHANGED
|
@@ -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,
|
|
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;
|
|
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 (
|
|
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",
|
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
|
|
|
@@ -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
|
|
197
|
-
|
|
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
|
|
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) -
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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> {/*
|
|
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
|
|
1400
|
+
<!-- routes/index.vue - a Nifra route authored as a Vue Single-File Component -->
|
|
1389
1401
|
<script lang="ts">
|
|
1390
|
-
// The plain <script> carries
|
|
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"]) //
|
|
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 (
|
|
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
|
|
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
|
|
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 -
|
|
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;
|
|
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 -
|
|
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
|
|
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
|
-
(
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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 +
|
|
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), //
|
|
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;
|
|
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
|
|
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,
|
|
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
|
|
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/*`, `
|
|
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 `
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 ~
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
45
|
-
"@nifrajs/core": "2.8.
|
|
46
|
-
"@nifrajs/mcp": "2.8.
|
|
47
|
-
"@nifrajs/runner": "2.8.
|
|
48
|
-
"@nifrajs/schema": "2.8.
|
|
49
|
-
"@nifrajs/testing": "2.8.
|
|
50
|
-
"@nifrajs/web": "2.8.
|
|
51
|
-
"create-nifra": "2.8.
|
|
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",
|