@nifrajs/cli 4.0.2 → 5.0.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/README.md +3 -0
- package/dist/agent-files.d.ts +107 -0
- package/dist/agent-files.d.ts.map +1 -0
- package/dist/agent-files.js +162 -0
- package/dist/agent-files.js.map +1 -0
- package/dist/app-files.d.ts +6 -0
- package/dist/app-files.d.ts.map +1 -1
- package/dist/app-files.js +27 -2
- package/dist/app-files.js.map +1 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +7 -2
- package/dist/assure-hydration.js.map +1 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +13 -3
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +20 -2
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +54 -7
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +75 -22
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +1475 -310
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +2 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +192 -65
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +112 -5
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +18 -0
- package/dist/client-module.d.ts.map +1 -0
- package/dist/client-module.js +49 -0
- package/dist/client-module.js.map +1 -0
- package/dist/command-catalog.d.ts +16 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +133 -29
- package/dist/command-catalog.js.map +1 -1
- package/dist/component-check.d.ts +13 -0
- package/dist/component-check.d.ts.map +1 -0
- package/dist/component-check.js +59 -0
- package/dist/component-check.js.map +1 -0
- package/dist/contract-proof.d.ts +1 -1
- package/dist/contract-proof.d.ts.map +1 -1
- package/dist/contract-proof.js +2 -24
- package/dist/contract-proof.js.map +1 -1
- package/dist/contracts.d.ts +19 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +50 -3
- package/dist/contracts.js.map +1 -1
- package/dist/db-tool.d.ts.map +1 -1
- package/dist/db-tool.js +4 -3
- package/dist/db-tool.js.map +1 -1
- package/dist/dev-bun-config.d.ts +5 -1
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +44 -11
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/diagnostics.d.ts +3 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/diff-tool.d.ts +9 -8
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +28 -24
- package/dist/diff-tool.js.map +1 -1
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +23 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +6 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +6 -0
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +21 -2
- package/dist/fix-recipes.js.map +1 -1
- package/dist/hand-built-manifest.d.ts +27 -0
- package/dist/hand-built-manifest.d.ts.map +1 -0
- package/dist/hand-built-manifest.js +124 -0
- package/dist/hand-built-manifest.js.map +1 -0
- package/dist/hooks-report.d.ts +23 -0
- package/dist/hooks-report.d.ts.map +1 -0
- package/dist/hooks-report.js +104 -0
- package/dist/hooks-report.js.map +1 -0
- package/dist/init-agents.d.ts +2 -2
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +17 -11
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/nifra-project.d.ts +41 -0
- package/dist/internal/nifra-project.d.ts.map +1 -0
- package/dist/internal/nifra-project.js +183 -0
- package/dist/internal/nifra-project.js.map +1 -0
- package/dist/internal/project-specifiers.d.ts +16 -0
- package/dist/internal/project-specifiers.d.ts.map +1 -0
- package/dist/internal/project-specifiers.js +396 -0
- package/dist/internal/project-specifiers.js.map +1 -0
- package/dist/internal/script-kind.d.ts.map +1 -1
- package/dist/internal/script-kind.js +2 -1
- package/dist/internal/script-kind.js.map +1 -1
- package/dist/internal/source-facts.d.ts +2 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +11 -0
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +1 -1
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +5 -3
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +6 -6
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +55 -24
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +93 -15
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +31 -8
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +37 -7
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts +5 -5
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +2 -2
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-exec.d.ts +12 -4
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +50 -13
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +17 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +40 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts.map +1 -1
- package/dist/mcp-isolate.js +3 -2
- package/dist/mcp-isolate.js.map +1 -1
- package/dist/mcp-project-child.js +1 -1
- package/dist/mcp-project-child.js.map +1 -1
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +50 -10
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-ws.d.ts.map +1 -1
- package/dist/mcp-ws.js +3 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +8 -2
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +7 -0
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +778 -64
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +94 -0
- package/dist/output-schemas.d.ts.map +1 -0
- package/dist/output-schemas.js +864 -0
- package/dist/output-schemas.js.map +1 -0
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +3 -2
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +15 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +1 -0
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/5.0.0.d.ts +11 -0
- package/dist/recipes/5.0.0.d.ts.map +1 -0
- package/dist/recipes/5.0.0.js +34 -0
- package/dist/recipes/5.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts +7 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +12 -3
- package/dist/replay.js.map +1 -1
- package/dist/review.d.ts +3 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +38 -7
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +10 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +10 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/comment-markers.d.ts +16 -7
- package/dist/rules/comment-markers.d.ts.map +1 -1
- package/dist/rules/comment-markers.js +58 -19
- package/dist/rules/comment-markers.js.map +1 -1
- package/dist/rules/contract-visibility.d.ts +3 -0
- package/dist/rules/contract-visibility.d.ts.map +1 -0
- package/dist/rules/contract-visibility.js +337 -0
- package/dist/rules/contract-visibility.js.map +1 -0
- package/dist/rules/data-guard.d.ts +7 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +319 -37
- package/dist/rules/data-guard.js.map +1 -1
- package/dist/rules/layout.d.ts +4 -0
- package/dist/rules/layout.d.ts.map +1 -0
- package/dist/rules/layout.js +19 -0
- package/dist/rules/layout.js.map +1 -0
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +158 -43
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +24 -0
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +221 -3
- package/dist/rules/pages.js.map +1 -1
- package/dist/rules/routes.d.ts +6 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +111 -16
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.js +2 -2
- package/dist/rules/secrets.js.map +1 -1
- package/dist/rules/security.d.ts +12 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +281 -68
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/zones.d.ts +13 -0
- package/dist/rules/zones.d.ts.map +1 -1
- package/dist/rules/zones.js +192 -40
- package/dist/rules/zones.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +6 -4
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +14 -5
- package/dist/sdk.js.map +1 -1
- package/dist/stylex-migrate.d.ts.map +1 -1
- package/dist/stylex-migrate.js +163 -7
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +12 -3
- package/dist/test-tool.js.map +1 -1
- package/dist/typecheck-coverage.d.ts +20 -0
- package/dist/typecheck-coverage.d.ts.map +1 -0
- package/dist/typecheck-coverage.js +129 -0
- package/dist/typecheck-coverage.js.map +1 -0
- package/dist/upgrade.d.ts +69 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +420 -77
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +11 -0
- package/dist/verification-plan.js.map +1 -1
- package/dist/version-drift.d.ts +22 -0
- package/dist/version-drift.d.ts.map +1 -0
- package/dist/version-drift.js +42 -0
- package/dist/version-drift.js.map +1 -0
- package/dist/web-app-options.d.ts +1 -1
- package/dist/web-app-options.d.ts.map +1 -1
- package/dist/web-app-options.js +1 -1
- package/dist/web-app-options.js.map +1 -1
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +20 -8
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +54 -6
- package/docs/llms-full.txt +1539 -138
- package/docs/types.json +1456 -206
- package/package.json +11 -12
- package/src/agent-files.ts +203 -0
- package/src/app-files.ts +34 -4
- package/src/assure-hydration.ts +15 -2
- package/src/capabilities-tool.ts +15 -3
- package/src/check-diagnostics.ts +73 -9
- package/src/check-scan.ts +1773 -381
- package/src/check.ts +237 -76
- package/src/cli.ts +141 -5
- package/src/client-module.ts +61 -0
- package/src/command-catalog.ts +166 -37
- package/src/component-check.ts +79 -0
- package/src/contract-proof.ts +3 -25
- package/src/contracts.ts +60 -3
- package/src/db-tool.ts +5 -2
- package/src/dev-bun-config.ts +51 -10
- package/src/diagnostics.ts +3 -0
- package/src/diff-tool.ts +33 -26
- package/src/doctor.ts +43 -21
- package/src/env-file.ts +7 -0
- package/src/fix-recipes.ts +25 -2
- package/src/hand-built-manifest.ts +144 -0
- package/src/hooks-report.ts +135 -0
- package/src/init-agents.ts +17 -11
- package/src/internal/nifra-project.ts +191 -0
- package/src/internal/project-specifiers.ts +424 -0
- package/src/internal/script-kind.ts +2 -1
- package/src/internal/source-facts.ts +25 -0
- package/src/internal/typescript-import.ts +10 -3
- package/src/introspect.ts +65 -27
- package/src/levels-tool.ts +117 -20
- package/src/load.ts +78 -14
- package/src/mcp-context.ts +7 -7
- package/src/mcp-exec.ts +60 -17
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +41 -0
- package/src/mcp-isolate.ts +9 -2
- package/src/mcp-project-child.ts +1 -1
- package/src/mcp-root.ts +58 -8
- package/src/mcp-ws.ts +4 -2
- package/src/mcp.ts +8 -3
- package/src/migrate-layout.ts +885 -63
- package/src/output-schemas.ts +1084 -0
- package/src/port.ts +3 -2
- package/src/project-facts.ts +17 -3
- package/src/recipes/5.0.0.ts +35 -0
- package/src/recipes/index.ts +10 -0
- package/src/replay.ts +14 -3
- package/src/review.ts +39 -10
- package/src/rules/codes.ts +10 -0
- package/src/rules/comment-markers.ts +57 -21
- package/src/rules/contract-visibility.ts +383 -0
- package/src/rules/data-guard.ts +351 -42
- package/src/rules/layout.ts +22 -0
- package/src/rules/legacy.ts +184 -51
- package/src/rules/pages.ts +238 -3
- package/src/rules/routes.ts +124 -16
- package/src/rules/secrets.ts +2 -2
- package/src/rules/security.ts +352 -71
- package/src/rules/zones.ts +205 -38
- package/src/scaffold.ts +6 -4
- package/src/sdk.ts +14 -5
- package/src/stylex-migrate.ts +134 -7
- package/src/test-tool.ts +18 -3
- package/src/typecheck-coverage.ts +134 -0
- package/src/upgrade.ts +487 -71
- package/src/verification-plan.ts +24 -0
- package/src/version-drift.ts +67 -0
- package/src/web-app-options.ts +2 -2
- package/src/work-graph.ts +17 -12
package/src/port.ts
CHANGED
|
@@ -310,9 +310,10 @@ export function scanFileForFeatures(file: string, content: string, state: ScanSt
|
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
// Mirror nifra check/doctor's ignore set: deps, build output, generated/per-runtime dist dirs, VCS.
|
|
313
|
-
// PLUS *.config.* and
|
|
313
|
+
// PLUS *.config.* and build scripts (build.ts, build-<target>.ts, build.<x>.ts) - `port` scans
|
|
314
|
+
// request/app source, not tooling. A source file that merely starts with "build" is still source.
|
|
314
315
|
const IGNORED =
|
|
315
|
-
/(^|\/)(node_modules|dist(-[a-z0-9]+)?|build|\.nifra|\.git|\.wrangler|coverage)\/|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)[^/]*\.config\.[cm]?[jt]sx?$|(^|\/)build[^/]
|
|
316
|
+
/(^|\/)(node_modules|dist(-[a-z0-9]+)?|build|\.nifra|\.git|\.wrangler|coverage)\/|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)[^/]*\.config\.[cm]?[jt]sx?$|(^|\/)build(?:[-.][^/]*)?\.[cm]?[jt]sx?$/
|
|
316
317
|
|
|
317
318
|
/** Walk the project's source (same surface as `nifra check`, minus config + build scripts). */
|
|
318
319
|
async function walkPortSource(
|
package/src/project-facts.ts
CHANGED
|
@@ -7,8 +7,11 @@ import type {
|
|
|
7
7
|
import type {
|
|
8
8
|
ManifestDriftFinding,
|
|
9
9
|
SourceFinding,
|
|
10
|
+
SqlFinding,
|
|
10
11
|
StaticRouteFinding,
|
|
11
12
|
TransitiveServerImportFinding,
|
|
13
|
+
TypedClientFinding,
|
|
14
|
+
UnresolvedRoutePathFinding,
|
|
12
15
|
} from "./check-scan.ts"
|
|
13
16
|
import type { DoctorResult } from "./doctor.ts"
|
|
14
17
|
import type { TypeScriptSession } from "./internal/typescript-import.ts"
|
|
@@ -16,13 +19,15 @@ import type { PipelineReport } from "./pipeline-report.ts"
|
|
|
16
19
|
import type { RulePack, SourceIndex } from "./rules/index.ts"
|
|
17
20
|
|
|
18
21
|
export interface ProjectSourceFindings {
|
|
19
|
-
readonly fetches: readonly
|
|
22
|
+
readonly fetches: readonly TypedClientFinding[]
|
|
20
23
|
/** Hand-rolled `new EventSource("/…")` / `new WebSocket("/…")` to the app's own streaming surface. */
|
|
21
|
-
readonly streams: readonly
|
|
24
|
+
readonly streams: readonly TypedClientFinding[]
|
|
22
25
|
readonly untypedClients: readonly SourceFinding[]
|
|
23
26
|
readonly removedImports: readonly SourceFinding[]
|
|
24
27
|
readonly responseRoutes: readonly SourceFinding[]
|
|
25
|
-
readonly interpolatedSql: readonly
|
|
28
|
+
readonly interpolatedSql: readonly SqlFinding[]
|
|
29
|
+
/** Route registrations whose path is neither a literal nor a constant the scan resolves. */
|
|
30
|
+
readonly routePaths: readonly UnresolvedRoutePathFinding[]
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
export interface ProjectPackageFacts {
|
|
@@ -41,6 +46,10 @@ export interface ContractCheckFacts {
|
|
|
41
46
|
readonly present: boolean
|
|
42
47
|
readonly vacuous: boolean
|
|
43
48
|
readonly diagnostics: readonly { readonly route?: string; readonly message: string }[]
|
|
49
|
+
/** `METHOD /path` of every route `backend/app.ts` serves; set only when the lock check loaded it. */
|
|
50
|
+
readonly served?: readonly string[]
|
|
51
|
+
/** `METHOD /path` of each authenticated route that validates its input before its auth hook runs. */
|
|
52
|
+
readonly inputBeforeAuth?: readonly string[]
|
|
44
53
|
readonly error?: string
|
|
45
54
|
}
|
|
46
55
|
|
|
@@ -50,6 +59,10 @@ export interface ProjectCheckFacts {
|
|
|
50
59
|
readonly sqlCompilerAvailable: boolean
|
|
51
60
|
/** Set when a project TypeScript install is present but outside the scanner adapter range. */
|
|
52
61
|
readonly unsupportedTypeScriptVersion?: string
|
|
62
|
+
/** Why component modules could not be checked: the project's TypeScript has no compiler API. */
|
|
63
|
+
readonly componentCheckUnsupported?:
|
|
64
|
+
| { readonly version: string; readonly message: string }
|
|
65
|
+
| undefined
|
|
53
66
|
/** Shared syntax/checker session for built-in rules; omitted from serialized check output. */
|
|
54
67
|
readonly typescriptSession?: TypeScriptSession
|
|
55
68
|
readonly checkConfigError?: string
|
|
@@ -87,6 +100,7 @@ export function freezeProjectFacts(seed: ProjectFactsSeed): ProjectFacts {
|
|
|
87
100
|
removedImports: freezeFindings(seed.sourceFindings.removedImports),
|
|
88
101
|
responseRoutes: freezeFindings(seed.sourceFindings.responseRoutes),
|
|
89
102
|
interpolatedSql: freezeFindings(seed.sourceFindings.interpolatedSql),
|
|
103
|
+
routePaths: freezeFindings(seed.sourceFindings.routePaths),
|
|
90
104
|
})
|
|
91
105
|
const policies = Object.freeze({
|
|
92
106
|
...seed.policies,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { UpgradeRecipe } from "./index.ts"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Upgrade recipe for Nifra 5.0.
|
|
5
|
+
*
|
|
6
|
+
* The runner pins the fixed group and lists every retired `@nifra-gate-reviewed` comment. It never
|
|
7
|
+
* writes the `nifra-expect` comment that replaces one: a reason it made up would silence a finding
|
|
8
|
+
* nobody reviewed. Every changed default below has a `nifra check` finding or a one-line way to keep
|
|
9
|
+
* the 4.x behavior, so the notes name both.
|
|
10
|
+
*/
|
|
11
|
+
export const recipe: UpgradeRecipe = {
|
|
12
|
+
version: "5.0.0",
|
|
13
|
+
pins: [
|
|
14
|
+
{ match: "@nifrajs/", to: "5.0.0" },
|
|
15
|
+
{ match: "create-nifra", to: "5.0.0" },
|
|
16
|
+
{ match: "nifra", to: "5.0.0" },
|
|
17
|
+
],
|
|
18
|
+
importMoves: [],
|
|
19
|
+
retiredMarkers: [
|
|
20
|
+
{
|
|
21
|
+
marker: "@nifra-gate-reviewed",
|
|
22
|
+
instead: "// nifra-expect <code>: <reason>, with the code nifra check reports there",
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
notes: [
|
|
26
|
+
"`@nifra-gate-reviewed` silences nothing. After review, silence one finding with `// nifra-expect <code>: <reason>` on its line or anywhere in the comment block above it. The upgrade lists every old comment and the line it covered; it never writes a reason. Every security finding and every error needs the reason, so `// nifra-expect route-overlap` (NF-C024, NF-C025) without one no longer silences the overlap.",
|
|
27
|
+
"`nifra check` reports a secret comparison (NF-S002) as advisory only in browser code inside a nifra web app: a directory with `nifra.config.ts` or `backend/framework.ts`, or whose nearest package.json depends on `@nifrajs/web` or a `@nifrajs/web-*` adapter. The same comparison in `routes/` or `frontend/` of an API-only project, or of a package without a web dependency, fails the check.",
|
|
28
|
+
"A WebSocket handshake runs the route chain an HTTP `GET` registered at the same point runs: `authenticate()`, auth middleware, `derive` and `beforeHandle` added before `app.ws()` guard the socket and can refuse the handshake with the response they send an HTTP request. Register a socket before the guard to keep it public. `nifra routes --hooks` lists what each socket runs.",
|
|
29
|
+
'`createWebApp` serves the backend over HTTP only at an `apiPrefix` the app sets; without one it is loader-only (`ctx.api`) and a browser call to `/api` reaches page routing. `nifra check` reports a `backend/framework.ts` that sets none (NF-C035); run `nifra fix --code NF-C035` to add `export const apiPrefix = "/api"` and keep the 4.x mount, or export `""` to keep the backend loader-only. A hand-written `createWebApp` call passes `apiPrefix: "/api"` itself.',
|
|
30
|
+
"Root status pages (`_404.tsx`, `_<status>.tsx`) and `_error.tsx` render inside the layouts above them with their loader data, and a page rendered with layout data is `private, no-store`. A root status page that imports the root layout to wrap itself now renders it twice: `nifra check` reports it (NF-C034); remove the wrap. `export const inLayouts = false` renders a page the 4.x way and runs no layout loader for it.",
|
|
31
|
+
"A Response returned to more than one request is no longer shared: a body-less one is copied for each request before cookies and headers are written, and one whose body was already sent answers `500`. Build a Response per request, or return data and let the handler serialize it.",
|
|
32
|
+
"`invariants.executor` in `nifra.assurance.ts` is optional now that `invariants.setup` can provision the target, so code that reads the config's type handles a missing `executor`.",
|
|
33
|
+
"A layout's `meta` receives its own loader data as `data`, `null` when the layout has no loader; the page's data moved to `pageData`. A layout `meta` written against the page's data, such as a title template, reads `pageData` now: `({ pageData }) => ({ title: pageData.title })`. `nifra check` reports a layout with no loader whose `meta` reads `data` (NF-C040). A page's own `meta` is unchanged.",
|
|
34
|
+
],
|
|
35
|
+
}
|
package/src/recipes/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { recipe as v1_8_0 } from "./1.8.0.ts"
|
|
|
8
8
|
import { recipe as v2_0_0 } from "./2.0.0.ts"
|
|
9
9
|
import { recipe as v3_0_0 } from "./3.0.0.ts"
|
|
10
10
|
import { recipe as v4_0_0 } from "./4.0.0.ts"
|
|
11
|
+
import { recipe as v5_0_0 } from "./5.0.0.ts"
|
|
11
12
|
|
|
12
13
|
/** A pin rule: every dependency whose name starts with `match` is set to version `to`. */
|
|
13
14
|
export interface PinRule {
|
|
@@ -30,11 +31,19 @@ export interface ImportMove {
|
|
|
30
31
|
readonly to: string
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/** A source comment a release stopped honoring. The runner lists each one; it never rewrites it. */
|
|
35
|
+
export interface RetiredMarker {
|
|
36
|
+
readonly marker: string
|
|
37
|
+
/** What to write instead, printed beside each comment. */
|
|
38
|
+
readonly instead: string
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
export interface UpgradeRecipe {
|
|
34
42
|
readonly version: string
|
|
35
43
|
readonly pins: readonly PinRule[]
|
|
36
44
|
readonly dependencyMoves?: readonly DependencyMove[]
|
|
37
45
|
readonly importMoves: readonly ImportMove[]
|
|
46
|
+
readonly retiredMarkers?: readonly RetiredMarker[]
|
|
38
47
|
/** Human notes printed after the plan - e.g. structural changes the runner deliberately can't do. */
|
|
39
48
|
readonly notes?: readonly string[]
|
|
40
49
|
}
|
|
@@ -44,6 +53,7 @@ const RECIPES: Record<string, UpgradeRecipe> = {
|
|
|
44
53
|
[v2_0_0.version]: v2_0_0,
|
|
45
54
|
[v3_0_0.version]: v3_0_0,
|
|
46
55
|
[v4_0_0.version]: v4_0_0,
|
|
56
|
+
[v5_0_0.version]: v5_0_0,
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
export function getRecipe(version: string): UpgradeRecipe | undefined {
|
package/src/replay.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { realpath } from "node:fs/promises"
|
|
2
|
+
import { isAbsolute, relative, resolve } from "node:path"
|
|
2
3
|
import { type CompatibleReplayFile, parseCompatibleReplayFile } from "@nifrajs/core/replay"
|
|
3
4
|
import { runHydrationAssurance } from "./assure-hydration.ts"
|
|
4
5
|
import { checkContractsLock } from "./contracts.ts"
|
|
@@ -12,10 +13,20 @@ export interface ReplayResult {
|
|
|
12
13
|
readonly replays?: readonly string[]
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
const within = (root: string, path: string): boolean => {
|
|
17
|
+
const rel = relative(root, path)
|
|
18
|
+
return !rel.startsWith("..") && !isAbsolute(rel)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const canonical = (path: string): Promise<string> => realpath(path).catch(() => path)
|
|
22
|
+
|
|
15
23
|
/** Validate and dispatch one token-only replay while keeping the file inside the project root. */
|
|
16
24
|
export async function runReplay(cwd: string, file: string): Promise<ReplayResult> {
|
|
17
|
-
const
|
|
18
|
-
|
|
25
|
+
const root = resolve(cwd)
|
|
26
|
+
// Compared as real paths: the MCP host hands over an already-resolved file, which never starts
|
|
27
|
+
// with a root reached through a symlink, and a replay file that is itself a link must not lead out.
|
|
28
|
+
const path = await canonical(resolve(root, file))
|
|
29
|
+
if (!within(await canonical(root), path) && !within(root, path)) {
|
|
19
30
|
throw new Error("replay file must stay inside the project root")
|
|
20
31
|
}
|
|
21
32
|
const replay = parseCompatibleReplayFile(JSON.parse(await Bun.file(path).text()))
|
package/src/review.ts
CHANGED
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
import type { CheckResult } from "./check.ts"
|
|
17
17
|
import type { CommandCtx, CommandSpec } from "./command-catalog.ts"
|
|
18
18
|
import type { Diagnostic } from "./diagnostics.ts"
|
|
19
|
-
import { toSarifLog } from "./diagnostics.ts"
|
|
19
|
+
import { diagnosticCompatibilityOf, toSarifLog } from "./diagnostics.ts"
|
|
20
20
|
import { RULE_CODES } from "./rules/codes.ts"
|
|
21
21
|
import { LEGACY_RULE_CODES } from "./rules/legacy.ts"
|
|
22
22
|
|
|
@@ -110,7 +110,8 @@ const CHECK_SOURCES: Readonly<Record<ReviewCheckId, ReviewEvidenceSource>> = {
|
|
|
110
110
|
|
|
111
111
|
const ALLOWED_FIXES = new Set<ReviewFixRecipeId>(["manifest.sync", "workspace-dist.rebuild"])
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
/** The review check each built-in code reports under; a code missing here would read as configuration. */
|
|
114
|
+
export const CHECK_CODES: Readonly<Record<string, ReviewCheckId>> = {
|
|
114
115
|
"NF-C001": "typecheck",
|
|
115
116
|
"NF-C002": "typed-client",
|
|
116
117
|
"NF-C003": "typed-client",
|
|
@@ -134,18 +135,41 @@ const CHECK_CODES: Readonly<Record<string, ReviewCheckId>> = {
|
|
|
134
135
|
"NF-C021": "route-boundary",
|
|
135
136
|
"NF-C022": "route-boundary",
|
|
136
137
|
"NF-C023": "route-boundary",
|
|
138
|
+
"NF-C024": "route-boundary",
|
|
139
|
+
"NF-C025": "route-boundary",
|
|
140
|
+
"NF-C026": "route-boundary",
|
|
141
|
+
"NF-C027": "route-boundary",
|
|
142
|
+
"NF-C028": "server-boundary",
|
|
143
|
+
"NF-C029": "security",
|
|
144
|
+
"NF-C030": "security",
|
|
145
|
+
"NF-C031": "security",
|
|
146
|
+
"NF-C032": "security",
|
|
147
|
+
"NF-C033": "security",
|
|
148
|
+
"NF-C034": "route-boundary",
|
|
149
|
+
"NF-C035": "route-boundary",
|
|
150
|
+
"NF-C036": "route-boundary",
|
|
151
|
+
"NF-C037": "configuration",
|
|
152
|
+
"NF-C038": "configuration",
|
|
153
|
+
"NF-C039": "contract-witness",
|
|
154
|
+
"NF-C040": "route-boundary",
|
|
137
155
|
"NF-D001": "dependency",
|
|
156
|
+
"NF-D002": "dependency",
|
|
138
157
|
"NF-A001": "route-assurance",
|
|
139
158
|
"NF-H001": "hydration",
|
|
140
159
|
"NF-H002": "hydration",
|
|
141
160
|
"NF-H003": "hydration",
|
|
142
161
|
"NF-H004": "hydration",
|
|
143
162
|
"NF-K001": "contract-witness",
|
|
163
|
+
"NF-S001": "security",
|
|
164
|
+
"NF-S002": "security",
|
|
165
|
+
"NF-S003": "security",
|
|
166
|
+
"NF-S004": "security",
|
|
167
|
+
"NF-S005": "security",
|
|
168
|
+
"NF-S006": "security",
|
|
169
|
+
"NF-S007": "security",
|
|
170
|
+
"NF-S008": "security",
|
|
144
171
|
}
|
|
145
172
|
|
|
146
|
-
for (const code of ["NF-S001", "NF-S002", "NF-S003", "NF-S004", "NF-S005", "NF-S006", "NF-S007"])
|
|
147
|
-
(CHECK_CODES as Record<string, ReviewCheckId>)[code] = "security"
|
|
148
|
-
|
|
149
173
|
const REVIEW_INPUT_SCHEMA = {
|
|
150
174
|
type: "object",
|
|
151
175
|
properties: {
|
|
@@ -613,17 +637,22 @@ async function composeCollectedReport(
|
|
|
613
637
|
}
|
|
614
638
|
|
|
615
639
|
const changedPaths = new Set(scope.changedPaths)
|
|
640
|
+
// A server-only chain is in scope when any file on it changed: a new import in a shared module
|
|
641
|
+
// reaches the browser through a route the diff never touched.
|
|
642
|
+
const inScope = (candidate: CandidateFinding): boolean =>
|
|
643
|
+
candidate.location === undefined ||
|
|
644
|
+
changedPaths.has(candidate.location.path) ||
|
|
645
|
+
(diagnosticCompatibilityOf(candidate.diagnostic)?.modules ?? []).some((module) => {
|
|
646
|
+
const path = normalizedLocation(cwd, module, undefined)?.path
|
|
647
|
+
return path !== undefined && changedPaths.has(path)
|
|
648
|
+
})
|
|
616
649
|
const retainedByCheck = new Map<ReviewCheckId, CandidateFinding[]>()
|
|
617
650
|
let totalOutOfScope = 0
|
|
618
651
|
let typeErrorOutOfScope = false
|
|
619
652
|
for (const id of CHECK_IDS) {
|
|
620
653
|
const retained: CandidateFinding[] = []
|
|
621
654
|
for (const candidate of candidatesByCheck.get(id) ?? []) {
|
|
622
|
-
if (
|
|
623
|
-
scope.kind === "diff" &&
|
|
624
|
-
candidate.location !== undefined &&
|
|
625
|
-
!changedPaths.has(candidate.location.path)
|
|
626
|
-
) {
|
|
655
|
+
if (scope.kind === "diff" && !inScope(candidate)) {
|
|
627
656
|
totalOutOfScope += 1
|
|
628
657
|
if (id === "typecheck" && candidate.severity === "error") typeErrorOutOfScope = true
|
|
629
658
|
} else retained.push(candidate)
|
package/src/rules/codes.ts
CHANGED
|
@@ -29,11 +29,20 @@ export const RULE_CODES = Object.freeze({
|
|
|
29
29
|
"NF-C030": "Route data without an output schema",
|
|
30
30
|
"NF-C031": "Sensitive field in an output schema",
|
|
31
31
|
"NF-C032": "Credential in browser code",
|
|
32
|
+
"NF-C033": "Generated output schema out of date",
|
|
33
|
+
"NF-C034": "Root status page wraps the root layout itself",
|
|
34
|
+
"NF-C035": "Backend mounted at the implicit /api",
|
|
35
|
+
"NF-C036": "Route path the static scan cannot read",
|
|
36
|
+
"NF-C037": "API-only declaration over a web app",
|
|
37
|
+
"NF-C038": "Retired root layout file",
|
|
38
|
+
"NF-C039": "Implemented contract operation the backend does not serve",
|
|
39
|
+
"NF-C040": "Layout meta reads data its layout never loads",
|
|
32
40
|
"NF-C020": "Island enhancer cleanup check",
|
|
33
41
|
"NF-C021": "nano binding cleanup check",
|
|
34
42
|
"NF-C022": "nano bindList key check",
|
|
35
43
|
"NF-C023": "nano computed deps check",
|
|
36
44
|
"NF-D001": "Doctor check",
|
|
45
|
+
"NF-D002": "Mixed nifra versions",
|
|
37
46
|
"NF-A001": "Assurance bundle check",
|
|
38
47
|
"NF-H001": "Hydration mismatch",
|
|
39
48
|
"NF-H002": "Duplicate framework runtime",
|
|
@@ -47,6 +56,7 @@ export const RULE_CODES = Object.freeze({
|
|
|
47
56
|
"NF-S005": "External redirect opt-out",
|
|
48
57
|
"NF-S006": "Security escape hatch enabled",
|
|
49
58
|
"NF-S007": "Secure cookie without a __Host-/__Secure- prefix",
|
|
59
|
+
"NF-S008": "Authenticated route validates input before authentication",
|
|
50
60
|
} as const)
|
|
51
61
|
|
|
52
62
|
/** Codes that no longer fire but remain reserved forever. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Marker recognition for in-source escape hatches (
|
|
2
|
+
* Marker recognition for in-source escape hatches (`nifra-expect <code>: <reason>`).
|
|
3
3
|
*
|
|
4
4
|
* A marker counts when it appears on the flagged line itself (trailing comment) or ANYWHERE in the
|
|
5
5
|
* contiguous comment block directly above it - which is where a human naturally writes the
|
|
@@ -11,12 +11,27 @@
|
|
|
11
11
|
// open/body/close lines, and the JSX comment form.
|
|
12
12
|
const COMMENT_LINE = /^\s*(?:\/\/|\/\*|\*\/|\*|\{\/\*)/
|
|
13
13
|
|
|
14
|
+
/** The trailing comment on a code line: from its first `//` or `/*` outside a string literal to the
|
|
15
|
+
* end, else `""`. A marker in a string the code passes is data, not a comment. */
|
|
16
|
+
function trailingComment(text: string): string {
|
|
17
|
+
let quote: string | undefined
|
|
18
|
+
for (let i = 0; i < text.length; i++) {
|
|
19
|
+
const char = text[i]
|
|
20
|
+
if (quote !== undefined) {
|
|
21
|
+
if (char === "\\") i++
|
|
22
|
+
else if (char === quote) quote = undefined
|
|
23
|
+
} else if (char === '"' || char === "'" || char === "`") quote = char
|
|
24
|
+
else if (char === "/" && (text[i + 1] === "/" || text[i + 1] === "*")) return text.slice(i)
|
|
25
|
+
}
|
|
26
|
+
return ""
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
export function commentBlockHasMarker(
|
|
15
30
|
lines: readonly string[],
|
|
16
31
|
line: number,
|
|
17
32
|
marker: string,
|
|
18
33
|
): boolean {
|
|
19
|
-
if ((lines[line - 1] ?? "").includes(marker)) return true
|
|
34
|
+
if (trailingComment(lines[line - 1] ?? "").includes(marker)) return true
|
|
20
35
|
for (let i = line - 2; i >= 0; i--) {
|
|
21
36
|
const text = lines[i] ?? ""
|
|
22
37
|
if (!COMMENT_LINE.test(text)) return false
|
|
@@ -26,37 +41,58 @@ export function commentBlockHasMarker(
|
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
/**
|
|
29
|
-
* The
|
|
30
|
-
*
|
|
31
|
-
* comment-block-above region as {@link commentBlockHasMarker}, so a hatch that demands a written reason
|
|
32
|
-
* can require this to be non-empty before it silences anything - an empty `<marker>:` never suppresses,
|
|
33
|
-
* which is how "reason mandatory" is enforced without a second diagnostic.
|
|
44
|
+
* The 1-based line a marker comment on `line` applies to: `line` itself when code precedes the comment
|
|
45
|
+
* there, else the first line after its comment block, or `undefined` when a blank line ends the block.
|
|
34
46
|
*/
|
|
35
|
-
export function
|
|
47
|
+
export function markerTarget(lines: readonly string[], line: number): number | undefined {
|
|
48
|
+
if (!COMMENT_LINE.test(lines[line - 1] ?? "")) return line
|
|
49
|
+
for (let i = line; i < lines.length; i++) {
|
|
50
|
+
const text = lines[i] ?? ""
|
|
51
|
+
if (COMMENT_LINE.test(text)) continue
|
|
52
|
+
return text.trim() === "" ? undefined : i + 1
|
|
53
|
+
}
|
|
54
|
+
return undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** What a finding adds when a `nifra-expect` comment names it but leaves out the reason it needs. */
|
|
58
|
+
export const MISSING_EXPECTATION_REASON =
|
|
59
|
+
"the nifra-expect comment here gives no reason after its colon, so it does not silence this"
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A `nifra-expect <name>: <reason>` comment for the finding on `line`, where `<name>` is one of
|
|
63
|
+
* `names`: the finding's NF code, or a slug it also answers to. It counts in the same trailing-comment /
|
|
64
|
+
* comment-block-above region as {@link commentBlockHasMarker}. `undefined` when no such comment is
|
|
65
|
+
* there; `reason` is `undefined` when none follows the name's colon. A hatch that silences an error
|
|
66
|
+
* requires the reason, so an empty `nifra-expect NF-C006:` never suppresses one.
|
|
67
|
+
*/
|
|
68
|
+
export function commentBlockExpectation(
|
|
36
69
|
lines: readonly string[],
|
|
37
70
|
line: number,
|
|
38
|
-
|
|
39
|
-
): string | undefined {
|
|
71
|
+
names: readonly string[],
|
|
72
|
+
): { readonly reason: string | undefined } | undefined {
|
|
73
|
+
const escaped = names.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
74
|
+
const pattern = new RegExp(`nifra-expect\\s+(?:${escaped.join("|")})(?![\\w-])(.*)`)
|
|
75
|
+
let found = false
|
|
40
76
|
const reasonOn = (text: string): string | undefined => {
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
77
|
+
const match = pattern.exec(text)
|
|
78
|
+
if (match === null) return undefined
|
|
79
|
+
found = true
|
|
80
|
+
const rest = (match[1] ?? "").trimStart()
|
|
81
|
+
if (!rest.startsWith(":")) return undefined
|
|
46
82
|
// A block comment's `*/` closer is not part of the reason.
|
|
47
83
|
const reason = rest
|
|
48
|
-
.slice(
|
|
84
|
+
.slice(1)
|
|
49
85
|
.replace(/\*\/\s*$/, "")
|
|
50
86
|
.trim()
|
|
51
87
|
return reason.length > 0 ? reason : undefined
|
|
52
88
|
}
|
|
53
|
-
const onLine = reasonOn(lines[line - 1] ?? "")
|
|
54
|
-
if (onLine !== undefined) return onLine
|
|
89
|
+
const onLine = reasonOn(trailingComment(lines[line - 1] ?? ""))
|
|
90
|
+
if (onLine !== undefined) return { reason: onLine }
|
|
55
91
|
for (let i = line - 2; i >= 0; i--) {
|
|
56
92
|
const text = lines[i] ?? ""
|
|
57
|
-
if (!COMMENT_LINE.test(text))
|
|
93
|
+
if (!COMMENT_LINE.test(text)) break
|
|
58
94
|
const reason = reasonOn(text)
|
|
59
|
-
if (reason !== undefined) return reason
|
|
95
|
+
if (reason !== undefined) return { reason }
|
|
60
96
|
}
|
|
61
|
-
return undefined
|
|
97
|
+
return found ? { reason: undefined } : undefined
|
|
62
98
|
}
|