@nifrajs/cli 2.8.1 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/cli",
3
- "version": "2.8.1",
3
+ "version": "2.9.0",
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.9.0",
45
+ "@nifrajs/core": "2.9.0",
46
+ "@nifrajs/mcp": "2.9.0",
47
+ "@nifrajs/runner": "2.9.0",
48
+ "@nifrajs/schema": "2.9.0",
49
+ "@nifrajs/testing": "2.9.0",
50
+ "@nifrajs/web": "2.9.0",
51
+ "create-nifra": "2.9.0"
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.9.0"
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.9.0"
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) {