@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 +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 +95 -41
- package/docs/types.json +2 -2
- package/package.json +9 -9
- package/src/cli.ts +1 -1
- package/src/mcp-http.ts +1 -1
- package/src/scaffold.ts +34 -1
package/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.
|
|
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.
|
|
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
|
package/dist/scaffold.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
}
|
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
|
|
|
@@ -228,12 +228,22 @@ accurately:
|
|
|
228
228
|
surface), `nifra_example` (snippets typechecked against the installed version - no
|
|
229
229
|
hallucinated APIs), `nifra_scaffold` (URL → correct `routes/` file),
|
|
230
230
|
`nifra_run` (verify via HTTP), and `nifra_check` (a drift gate that returns the
|
|
231
|
-
fix).
|
|
231
|
+
fix). One MCP, two transports: the docs tools are also hosted at `mcp.nifra.dev`
|
|
232
|
+
(no checkout needed), while the project tools run only on your machine - your code never
|
|
233
|
+
leaves it.
|
|
232
234
|
|
|
233
235
|
-
|
|
234
236
|
`llms.txt` + `llms-full.txt` served at the site root, an `AGENTS.md`
|
|
235
237
|
in every scaffold, and a docs corpus that can't drift from the code.
|
|
236
238
|
|
|
239
|
+
-
|
|
240
|
+
`nifra assure` + `nifra levels` - a route-assurance gate: a policy file
|
|
241
|
+
classifies every reflected route and CI fails naming exactly which evidence is missing
|
|
242
|
+
(authentication on writes, validation, declared effects). Spectral lints the OpenAPI
|
|
243
|
+
_document_ and Semgrep pattern-matches source text; neither sees the real route graph.
|
|
244
|
+
This is what makes agent-written routes safe to merge - an agent (or a human) cannot ship an
|
|
245
|
+
unauthenticated write past it.
|
|
246
|
+
|
|
237
247
|
### Where Nifra fits
|
|
238
248
|
|
|
239
249
|
Reach for Nifra when you want:
|
|
@@ -750,7 +760,7 @@ export const addTodo = serverFn(
|
|
|
750
760
|
)
|
|
751
761
|
```
|
|
752
762
|
|
|
753
|
-
The second argument receives the validated input and the ordinary
|
|
763
|
+
The second argument receives the validated input and the ordinary Nifra `Context`:
|
|
754
764
|
`c.env`, `c.clientIp`, `c.budget`, cookies and the capability
|
|
755
765
|
guard are all there, because this is a route.
|
|
756
766
|
|
|
@@ -1019,7 +1029,7 @@ const isr = withISR(app, { store, revalidate: 60, now: () => Date.now() })
|
|
|
1019
1029
|
Bun.serve({ fetch: (req) => isr(req) })
|
|
1020
1030
|
```
|
|
1021
1031
|
|
|
1022
|
-
Set a route's freshness with `export const revalidate` (seconds) -
|
|
1032
|
+
Set a route's freshness with `export const revalidate` (seconds) - Nifra emits it as
|
|
1023
1033
|
the `x-nifra-isr-revalidate` header, which the wrapper reads to set that page's TTL.
|
|
1024
1034
|
|
|
1025
1035
|
```ts
|
|
@@ -1135,9 +1145,9 @@ route.
|
|
|
1135
1145
|
|
|
1136
1146
|
## Nifra - Hydration & pre-hydration forms
|
|
1137
1147
|
|
|
1138
|
-
> SSR pages become interactive after hydration.
|
|
1148
|
+
> SSR pages become interactive after hydration. Nifra keeps that gap safe: progressive-enhancement forms and links work before it, and a JS-only form's broken native submit is guarded automatically.
|
|
1139
1149
|
|
|
1140
|
-
An SSR page is visible at once but interactive only after its island hydrates.
|
|
1150
|
+
An SSR page is visible at once but interactive only after its island hydrates. Nifra keeps
|
|
1141
1151
|
that gap safe for you - you rarely have to think about it.
|
|
1142
1152
|
|
|
1143
1153
|
### Forms and links - nothing to do
|
|
@@ -1169,12 +1179,12 @@ async function onSubmit(e: FormEvent) {
|
|
|
1169
1179
|
return <form onSubmit={onSubmit}>…</form>
|
|
1170
1180
|
```
|
|
1171
1181
|
|
|
1172
|
-
|
|
1182
|
+
Nifra blocks that native submit until hydration commits, so the worst case is a no-op click -
|
|
1173
1183
|
never a broken navigation. It never touches a `method="post"` form or a GET form
|
|
1174
1184
|
with a real action. Opt a form out with `data-native`:
|
|
1175
1185
|
|
|
1176
1186
|
```ts
|
|
1177
|
-
<form data-native>…</form> {/*
|
|
1187
|
+
<form data-native>…</form> {/* Nifra won't guard it - native submit is intended */}
|
|
1178
1188
|
```
|
|
1179
1189
|
|
|
1180
1190
|
### Gate other JS on the signal
|
|
@@ -1387,9 +1397,9 @@ but the compiled frameworks use their native single-file format - **Svelte**
|
|
|
1387
1397
|
package's Bun plugin (`@nifrajs/web-vue/plugin`, `@nifrajs/web-svelte/plugin`).
|
|
1388
1398
|
|
|
1389
1399
|
```ts
|
|
1390
|
-
<!-- routes/index.vue - a
|
|
1400
|
+
<!-- routes/index.vue - a Nifra route authored as a Vue Single-File Component -->
|
|
1391
1401
|
<script lang="ts">
|
|
1392
|
-
// The plain <script> carries
|
|
1402
|
+
// The plain <script> carries Nifra's route convention (server-only named exports):
|
|
1393
1403
|
export const meta = { title: "Home" }
|
|
1394
1404
|
export async function loader({ api }) {
|
|
1395
1405
|
const res = await api.count.get()
|
|
@@ -1398,7 +1408,7 @@ export async function loader({ api }) {
|
|
|
1398
1408
|
</script>
|
|
1399
1409
|
|
|
1400
1410
|
<script setup lang="ts">
|
|
1401
|
-
defineProps(["data"]) //
|
|
1411
|
+
defineProps(["data"]) // Nifra passes the loader data in as `data`
|
|
1402
1412
|
</script>
|
|
1403
1413
|
|
|
1404
1414
|
<template>
|
|
@@ -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 (
|
|
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
|
|
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
|
|
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 -
|
|
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;
|
|
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 -
|
|
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
|
|
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
|
-
(
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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 +
|
|
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), //
|
|
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;
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5704
|
-
rule that still applies 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,
|
|
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/*`, `
|
|
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 `
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 ~
|
|
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
|
|
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.
|
|
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",
|
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.
|
|
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.
|
|
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) {
|