@nifrajs/cli 3.5.0 → 4.0.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/README.md +42 -3
- package/dist/app-files.d.ts +11 -0
- package/dist/app-files.d.ts.map +1 -0
- package/dist/app-files.js +34 -0
- package/dist/app-files.js.map +1 -0
- package/dist/assure-hydration.d.ts +7 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +128 -43
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +16 -2
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +5 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +32 -10
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/cdn-check.d.ts +41 -0
- package/dist/cdn-check.d.ts.map +1 -0
- package/dist/cdn-check.js +207 -0
- package/dist/cdn-check.js.map +1 -0
- package/dist/check-diagnostics.d.ts +4 -0
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +13 -0
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +49 -7
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +291 -62
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +99 -63
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +42 -16
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +262 -82
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +62 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +275 -35
- package/dist/command-catalog.js.map +1 -1
- package/dist/config-target.d.ts +8 -0
- package/dist/config-target.d.ts.map +1 -0
- package/dist/config-target.js +45 -0
- package/dist/config-target.js.map +1 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +52 -13
- package/dist/contracts.js.map +1 -1
- package/dist/db-audit.d.ts +37 -0
- package/dist/db-audit.d.ts.map +1 -0
- package/dist/db-audit.js +99 -0
- package/dist/db-audit.js.map +1 -0
- package/dist/db-child.d.ts +64 -0
- package/dist/db-child.d.ts.map +1 -0
- package/dist/db-child.js +213 -0
- package/dist/db-child.js.map +1 -0
- package/dist/db-config.d.ts +60 -0
- package/dist/db-config.d.ts.map +1 -0
- package/dist/db-config.js +176 -0
- package/dist/db-config.js.map +1 -0
- package/dist/db-tool.d.ts +84 -0
- package/dist/db-tool.d.ts.map +1 -0
- package/dist/db-tool.js +537 -0
- package/dist/db-tool.js.map +1 -0
- package/dist/dev-bun-config.d.ts +20 -13
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +98 -41
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +106 -0
- package/dist/dev-feed-tool.d.ts.map +1 -0
- package/dist/dev-feed-tool.js +713 -0
- package/dist/dev-feed-tool.js.map +1 -0
- package/dist/dev-server-client.d.ts +67 -0
- package/dist/dev-server-client.d.ts.map +1 -0
- package/dist/dev-server-client.js +195 -0
- package/dist/dev-server-client.js.map +1 -0
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +6 -5
- package/dist/diff-tool.js.map +1 -1
- package/dist/docs-search.d.ts.map +1 -1
- package/dist/docs-search.js +12 -4
- package/dist/docs-search.js.map +1 -1
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +76 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +15 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +73 -1
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +48 -22
- package/dist/fix-recipes.js.map +1 -1
- package/dist/frontend-guidance.js +5 -5
- package/dist/frontend-guidance.js.map +1 -1
- package/dist/i18n-check.d.ts +16 -0
- package/dist/i18n-check.d.ts.map +1 -0
- package/dist/i18n-check.js +154 -0
- package/dist/i18n-check.js.map +1 -0
- package/dist/init-agents.d.ts +49 -13
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +239 -34
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/script-kind.d.ts +7 -0
- package/dist/internal/script-kind.d.ts.map +1 -0
- package/dist/internal/script-kind.js +12 -0
- package/dist/internal/script-kind.js.map +1 -0
- package/dist/internal/source-facts.d.ts +5 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +61 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +49 -24
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +507 -67
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +5 -1
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +92 -27
- package/dist/introspect.js.map +1 -1
- package/dist/learn.js +3 -3
- package/dist/learn.js.map +1 -1
- package/dist/levels-tool.d.ts +10 -0
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -3
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +67 -17
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +78 -18
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +3 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-app-tool.d.ts.map +1 -1
- package/dist/mcp-app-tool.js +2 -0
- package/dist/mcp-app-tool.js.map +1 -1
- package/dist/mcp-context.js +1 -1
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +57 -0
- package/dist/mcp-delegate.d.ts.map +1 -0
- package/dist/mcp-delegate.js +130 -0
- package/dist/mcp-delegate.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +23 -5
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-exec.d.ts +2 -0
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +132 -108
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +17 -5
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-io.d.ts +14 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +45 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts +48 -0
- package/dist/mcp-isolate.d.ts.map +1 -0
- package/dist/mcp-isolate.js +222 -0
- package/dist/mcp-isolate.js.map +1 -0
- package/dist/mcp-project-child.d.ts +74 -0
- package/dist/mcp-project-child.d.ts.map +1 -0
- package/dist/mcp-project-child.js +130 -0
- package/dist/mcp-project-child.js.map +1 -0
- package/dist/mcp-render.js +2 -2
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp-root.d.ts +37 -9
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +110 -23
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-run.d.ts +18 -1
- package/dist/mcp-run.d.ts.map +1 -1
- package/dist/mcp-run.js +82 -9
- package/dist/mcp-run.js.map +1 -1
- package/dist/mcp-ws.js +4 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts +16 -3
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +81 -53
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +44 -0
- package/dist/migrate-layout.d.ts.map +1 -0
- package/dist/migrate-layout.js +1155 -0
- package/dist/migrate-layout.js.map +1 -0
- package/dist/openapi-tool.d.ts +2 -2
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js.map +1 -1
- package/dist/openapi-types.d.ts +2 -1
- package/dist/openapi-types.d.ts.map +1 -1
- package/dist/openapi-types.js +240 -12
- package/dist/openapi-types.js.map +1 -1
- package/dist/pipeline-report.d.ts +3 -2
- package/dist/pipeline-report.d.ts.map +1 -1
- package/dist/pipeline-report.js +19 -16
- package/dist/pipeline-report.js.map +1 -1
- package/dist/port.d.ts +5 -4
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +46 -29
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +5 -0
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/4.0.0.d.ts +9 -0
- package/dist/recipes/4.0.0.d.ts.map +1 -0
- package/dist/recipes/4.0.0.js +36 -0
- package/dist/recipes/4.0.0.js.map +1 -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/release-verification.d.ts +11 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +47 -4
- package/dist/release-verification.js.map +1 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +12 -8
- package/dist/review.js.map +1 -1
- package/dist/route-types.d.ts +28 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +106 -0
- package/dist/route-types.js.map +1 -0
- package/dist/rules/codes.d.ts +9 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +9 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/data-guard.d.ts +12 -0
- package/dist/rules/data-guard.d.ts.map +1 -0
- package/dist/rules/data-guard.js +213 -0
- package/dist/rules/data-guard.js.map +1 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +17 -5
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/islands.d.ts.map +1 -1
- package/dist/rules/islands.js +7 -4
- package/dist/rules/islands.js.map +1 -1
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +71 -38
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/nano.d.ts.map +1 -1
- package/dist/rules/nano.js +15 -6
- package/dist/rules/nano.js.map +1 -1
- package/dist/rules/pages.d.ts +15 -0
- package/dist/rules/pages.d.ts.map +1 -0
- package/dist/rules/pages.js +88 -0
- package/dist/rules/pages.js.map +1 -0
- package/dist/rules/routes.d.ts +11 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +180 -2
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.d.ts +10 -0
- package/dist/rules/secrets.d.ts.map +1 -0
- package/dist/rules/secrets.js +169 -0
- package/dist/rules/secrets.js.map +1 -0
- package/dist/rules/security.d.ts +7 -0
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +171 -65
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/typescript.d.ts +16 -0
- package/dist/rules/typescript.d.ts.map +1 -0
- package/dist/rules/typescript.js +59 -0
- package/dist/rules/typescript.js.map +1 -0
- package/dist/rules/zones.d.ts +17 -0
- package/dist/rules/zones.d.ts.map +1 -0
- package/dist/rules/zones.js +174 -0
- package/dist/rules/zones.js.map +1 -0
- package/dist/scaffold.d.ts +25 -12
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +132 -87
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts +17 -3
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +941 -153
- package/dist/sdk.js.map +1 -1
- package/dist/search-terms.d.ts +2 -0
- package/dist/search-terms.d.ts.map +1 -1
- package/dist/search-terms.js +23 -8
- package/dist/search-terms.js.map +1 -1
- package/dist/smoke.d.ts +79 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +494 -0
- package/dist/smoke.js.map +1 -0
- package/dist/sync-manifest.d.ts +10 -2
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +44 -19
- package/dist/sync-manifest.js.map +1 -1
- package/dist/sync-routes.d.ts +4 -3
- package/dist/sync-routes.d.ts.map +1 -1
- package/dist/sync-routes.js +4 -3
- package/dist/sync-routes.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +30 -11
- package/dist/test-tool.js.map +1 -1
- package/dist/types-search.d.ts.map +1 -1
- package/dist/types-search.js +11 -5
- package/dist/types-search.js.map +1 -1
- package/dist/upgrade.d.ts +36 -6
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +167 -29
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +4 -0
- package/dist/verification-plan.js.map +1 -1
- package/dist/verification.d.ts.map +1 -1
- package/dist/verification.js +45 -12
- package/dist/verification.js.map +1 -1
- package/dist/web-app-options.d.ts +19 -0
- package/dist/web-app-options.d.ts.map +1 -0
- package/dist/web-app-options.js +49 -0
- package/dist/web-app-options.js.map +1 -0
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +29 -13
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +185 -29
- package/docs/llms-full.txt +3807 -580
- package/docs/types.json +14229 -10939
- package/package.json +19 -12
- package/src/app-files.ts +40 -0
- package/src/assure-hydration.ts +139 -46
- package/src/assure.ts +17 -2
- package/src/capabilities-tool.ts +35 -9
- package/src/cdn-check.ts +268 -0
- package/src/check-diagnostics.ts +17 -0
- package/src/check-scan.ts +358 -63
- package/src/check.ts +97 -62
- package/src/cli.ts +296 -87
- package/src/command-catalog.ts +389 -48
- package/src/config-target.ts +47 -0
- package/src/contracts.ts +54 -12
- package/src/db-audit.ts +128 -0
- package/src/db-child.ts +285 -0
- package/src/db-config.ts +224 -0
- package/src/db-tool.ts +690 -0
- package/src/dev-bun-config.ts +116 -40
- package/src/dev-feed-tool.ts +912 -0
- package/src/dev-server-client.ts +267 -0
- package/src/diff-tool.ts +6 -5
- package/src/docs-search.ts +10 -3
- package/src/doctor.ts +98 -15
- package/src/env-file.ts +78 -1
- package/src/fix-recipes.ts +53 -32
- package/src/frontend-guidance.ts +5 -5
- package/src/i18n-check.ts +179 -0
- package/src/init-agents.ts +301 -38
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/script-kind.ts +11 -0
- package/src/internal/source-facts.ts +96 -22
- package/src/internal/typescript-import.ts +663 -68
- package/src/introspect.ts +90 -27
- package/src/learn.ts +3 -3
- package/src/levels-tool.ts +47 -2
- package/src/load.ts +140 -33
- package/src/manifest-tool.ts +3 -2
- package/src/mcp-app-tool.ts +2 -0
- package/src/mcp-context.ts +1 -1
- package/src/mcp-delegate.ts +162 -0
- package/src/mcp-docs-tools.ts +23 -5
- package/src/mcp-exec.ts +160 -132
- package/src/mcp-http.ts +19 -5
- package/src/mcp-io.ts +57 -0
- package/src/mcp-isolate.ts +287 -0
- package/src/mcp-project-child.ts +182 -0
- package/src/mcp-render.ts +2 -2
- package/src/mcp-root.ts +117 -22
- package/src/mcp-run.ts +104 -10
- package/src/mcp-ws.ts +4 -1
- package/src/mcp.ts +106 -64
- package/src/migrate-layout.ts +1338 -0
- package/src/openapi-tool.ts +7 -3
- package/src/openapi-types.ts +315 -13
- package/src/pipeline-report.ts +22 -18
- package/src/port.ts +53 -30
- package/src/project-facts.ts +5 -0
- package/src/recipes/4.0.0.ts +37 -0
- package/src/recipes/index.ts +2 -0
- package/src/release-verification.ts +74 -5
- package/src/review.ts +13 -8
- package/src/route-types.ts +133 -0
- package/src/rules/codes.ts +9 -0
- package/src/rules/data-guard.ts +230 -0
- package/src/rules/index.ts +18 -5
- package/src/rules/islands.ts +12 -4
- package/src/rules/legacy.ts +71 -39
- package/src/rules/nano.ts +18 -6
- package/src/rules/pages.ts +99 -0
- package/src/rules/routes.ts +181 -2
- package/src/rules/secrets.ts +174 -0
- package/src/rules/security.ts +198 -70
- package/src/rules/typescript.ts +77 -0
- package/src/rules/zones.ts +195 -0
- package/src/scaffold.ts +147 -95
- package/src/sdk.ts +1215 -158
- package/src/search-terms.ts +24 -7
- package/src/smoke.ts +607 -0
- package/src/sync-manifest.ts +50 -19
- package/src/sync-routes.ts +4 -3
- package/src/test-tool.ts +32 -11
- package/src/types-search.ts +11 -4
- package/src/upgrade.ts +205 -25
- package/src/verification-plan.ts +13 -0
- package/src/verification.ts +50 -12
- package/src/web-app-options.ts +57 -0
- package/src/work-graph.ts +27 -13
package/src/cdn-check.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nifra cdn-check <url>` - is a deployed page actually served by the CDN the way `withCdn` intends?
|
|
3
|
+
*
|
|
4
|
+
* Read-only: two GETs of the page and one shaped like a soft navigation (`x-nifra-data: 1`). It names
|
|
5
|
+
* the CDN from its status header, and fails when nifra's internal headers reach the visitor or when
|
|
6
|
+
* the CDN answers a navigation data request with the cached document (Cloudflare's zone cache ignores
|
|
7
|
+
* `Vary`, so it needs a rule that bypasses the cache for that header). CLI only: an agent-facing tool
|
|
8
|
+
* that fetches arbitrary URLs from this machine could reach its private network.
|
|
9
|
+
*/
|
|
10
|
+
import type { CommandSpec } from "./command-catalog.ts"
|
|
11
|
+
|
|
12
|
+
export interface CdnCheckInput {
|
|
13
|
+
readonly url: string
|
|
14
|
+
readonly json?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type CdnName = "cloudflare" | "vercel" | "fastly" | "unknown"
|
|
18
|
+
|
|
19
|
+
export interface CdnCheckRequest {
|
|
20
|
+
readonly kind: "first" | "second" | "data"
|
|
21
|
+
readonly status: number
|
|
22
|
+
readonly cache?: string | undefined
|
|
23
|
+
readonly age?: string | undefined
|
|
24
|
+
readonly contentType?: string | undefined
|
|
25
|
+
readonly cacheControl?: string | undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CdnCheckFinding {
|
|
29
|
+
readonly level: "fail" | "warn" | "info"
|
|
30
|
+
readonly code: string
|
|
31
|
+
readonly message: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CdnCheckOutput {
|
|
35
|
+
readonly url: string
|
|
36
|
+
readonly cdn: CdnName
|
|
37
|
+
readonly requests: readonly CdnCheckRequest[]
|
|
38
|
+
readonly findings: readonly CdnCheckFinding[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const TIMEOUT_MS = 10_000
|
|
42
|
+
const INTERNAL = ["x-nifra-isr-revalidate", "x-nifra-isr-tags"]
|
|
43
|
+
const CDN_ONLY = [
|
|
44
|
+
"cache-tag",
|
|
45
|
+
"cloudflare-cdn-cache-control",
|
|
46
|
+
"vercel-cache-tag",
|
|
47
|
+
"vercel-cdn-cache-control",
|
|
48
|
+
"surrogate-key",
|
|
49
|
+
"surrogate-control",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
const field = (from: unknown, name: string): unknown =>
|
|
53
|
+
typeof from === "object" && from !== null ? Reflect.get(from, name) : undefined
|
|
54
|
+
|
|
55
|
+
function isCdnCheckOutput(value: unknown): value is CdnCheckOutput {
|
|
56
|
+
return (
|
|
57
|
+
typeof field(value, "url") === "string" &&
|
|
58
|
+
typeof field(value, "cdn") === "string" &&
|
|
59
|
+
Array.isArray(field(value, "requests")) &&
|
|
60
|
+
Array.isArray(field(value, "findings"))
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseInput(value: unknown): CdnCheckInput {
|
|
65
|
+
const url = field(value, "url")
|
|
66
|
+
if (typeof url !== "string" || url === "") throw new TypeError("cdn-check needs a page URL")
|
|
67
|
+
let parsed: URL
|
|
68
|
+
try {
|
|
69
|
+
parsed = new URL(url)
|
|
70
|
+
} catch {
|
|
71
|
+
throw new TypeError(`cdn-check: ${JSON.stringify(url)} is not a URL`)
|
|
72
|
+
}
|
|
73
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
74
|
+
throw new TypeError("cdn-check: the URL must be http or https")
|
|
75
|
+
}
|
|
76
|
+
if (parsed.username !== "" || parsed.password !== "") {
|
|
77
|
+
throw new TypeError("cdn-check: leave credentials out of the URL")
|
|
78
|
+
}
|
|
79
|
+
return field(value, "json") === true ? { url: parsed.href, json: true } : { url: parsed.href }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The CDN a response came through, from the status header each one sets. */
|
|
83
|
+
export function detectCdn(headers: Headers): CdnName {
|
|
84
|
+
if (headers.has("cf-cache-status") || headers.has("cf-ray")) return "cloudflare"
|
|
85
|
+
if (headers.has("x-vercel-cache") || headers.has("x-vercel-id")) return "vercel"
|
|
86
|
+
if (/\bcache-[a-z0-9]+/i.test(headers.get("x-served-by") ?? "")) return "fastly"
|
|
87
|
+
return "unknown"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function cacheStatus(cdn: CdnName, headers: Headers): string | undefined {
|
|
91
|
+
const value =
|
|
92
|
+
cdn === "cloudflare"
|
|
93
|
+
? headers.get("cf-cache-status")
|
|
94
|
+
: cdn === "vercel"
|
|
95
|
+
? headers.get("x-vercel-cache")
|
|
96
|
+
: headers.get("x-cache")
|
|
97
|
+
return value ?? undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const isHit = (status: string | undefined): boolean =>
|
|
101
|
+
status !== undefined && /\b(HIT|STALE|UPDATING|REVALIDATED)\b/i.test(status)
|
|
102
|
+
|
|
103
|
+
async function get(
|
|
104
|
+
url: string,
|
|
105
|
+
extra: Record<string, string>,
|
|
106
|
+
signal: AbortSignal | undefined,
|
|
107
|
+
): Promise<Response> {
|
|
108
|
+
const timeout = AbortSignal.timeout(TIMEOUT_MS)
|
|
109
|
+
return fetch(url, {
|
|
110
|
+
headers: { "user-agent": "nifra-cdn-check", ...extra },
|
|
111
|
+
redirect: "manual",
|
|
112
|
+
signal: signal === undefined ? timeout : AbortSignal.any([signal, timeout]),
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function runCdnCheck(
|
|
117
|
+
input: CdnCheckInput,
|
|
118
|
+
ctx: { readonly signal?: AbortSignal } = {},
|
|
119
|
+
): Promise<CdnCheckOutput> {
|
|
120
|
+
const findings: CdnCheckFinding[] = []
|
|
121
|
+
const requests: CdnCheckRequest[] = []
|
|
122
|
+
const record = async (extra: Record<string, string> = {}) => {
|
|
123
|
+
const res = await get(input.url, extra, ctx.signal)
|
|
124
|
+
await res.body?.cancel()
|
|
125
|
+
return res
|
|
126
|
+
}
|
|
127
|
+
const first = await record()
|
|
128
|
+
const cdn = detectCdn(first.headers)
|
|
129
|
+
const summarize = (kind: CdnCheckRequest["kind"], res: Response): CdnCheckRequest => ({
|
|
130
|
+
kind,
|
|
131
|
+
status: res.status,
|
|
132
|
+
cache: cacheStatus(cdn, res.headers),
|
|
133
|
+
age: res.headers.get("age") ?? undefined,
|
|
134
|
+
contentType: res.headers.get("content-type") ?? undefined,
|
|
135
|
+
cacheControl: res.headers.get("cache-control") ?? undefined,
|
|
136
|
+
})
|
|
137
|
+
requests.push(summarize("first", first))
|
|
138
|
+
if (first.status >= 300 && first.status < 400) {
|
|
139
|
+
findings.push({
|
|
140
|
+
level: "info",
|
|
141
|
+
code: "redirect",
|
|
142
|
+
message: `the URL redirects (${first.status}) to ${first.headers.get("location") ?? "?"}; check the page it lands on`,
|
|
143
|
+
})
|
|
144
|
+
return { url: input.url, cdn, requests, findings }
|
|
145
|
+
}
|
|
146
|
+
const second = await record()
|
|
147
|
+
requests.push(summarize("second", second))
|
|
148
|
+
const data = await record({ "x-nifra-data": "1" })
|
|
149
|
+
requests.push(summarize("data", data))
|
|
150
|
+
|
|
151
|
+
for (const res of [first, second, data]) {
|
|
152
|
+
const leaked = INTERNAL.filter((name) => res.headers.has(name))
|
|
153
|
+
if (leaked.length > 0) {
|
|
154
|
+
findings.push({
|
|
155
|
+
level: "fail",
|
|
156
|
+
code: "internal-headers",
|
|
157
|
+
message: `${leaked.join(", ")} reached the visitor: the app's cache channel is open without withISR or withCdn removing it`,
|
|
158
|
+
})
|
|
159
|
+
break
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const cdnOnly = CDN_ONLY.filter((name) => second.headers.has(name))
|
|
163
|
+
if (cdnOnly.length > 0) {
|
|
164
|
+
findings.push({
|
|
165
|
+
level: "warn",
|
|
166
|
+
code: "cdn-headers-visible",
|
|
167
|
+
message: `${cdnOnly.join(", ")} reached the visitor: the response did not pass through the CDN these headers are for${cdn === "unknown" ? "" : `, or ${cdn} is not consuming them`}`,
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
if (cdn === "unknown") {
|
|
171
|
+
findings.push({
|
|
172
|
+
level: "warn",
|
|
173
|
+
code: "no-cdn",
|
|
174
|
+
message:
|
|
175
|
+
"no CDN status header (cf-cache-status, x-vercel-cache, x-served-by): the page does not seem to pass through Cloudflare, Vercel or Fastly",
|
|
176
|
+
})
|
|
177
|
+
} else if (!isHit(summarize("second", second).cache)) {
|
|
178
|
+
findings.push({
|
|
179
|
+
level: "warn",
|
|
180
|
+
code: "not-cached",
|
|
181
|
+
message:
|
|
182
|
+
cdn === "cloudflare"
|
|
183
|
+
? `the second request was not served from cache (${second.headers.get("cf-cache-status") ?? "no status"}). Cloudflare caches HTML only under a Cache Rule that makes it eligible for cache`
|
|
184
|
+
: `the second request was not served from cache (${summarize("second", second).cache ?? "no status"}); a personalized or no-store page is expected to stay uncached`,
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
const browser = second.headers.get("cache-control") ?? ""
|
|
188
|
+
const maxAge = /(?:^|,)\s*max-age=(\d+)/i.exec(browser)?.[1]
|
|
189
|
+
if (
|
|
190
|
+
(second.headers.get("content-type") ?? "").includes("text/html") &&
|
|
191
|
+
maxAge !== undefined &&
|
|
192
|
+
Number(maxAge) > 0
|
|
193
|
+
) {
|
|
194
|
+
findings.push({
|
|
195
|
+
level: "warn",
|
|
196
|
+
code: "browser-holds-html",
|
|
197
|
+
message: `browsers may keep this page for ${maxAge}s (cache-control: ${browser}); a CDN purge cannot reach them`,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
const dataType = data.headers.get("content-type") ?? ""
|
|
201
|
+
if (dataType.includes("text/html") || isHit(cacheStatus(cdn, data.headers))) {
|
|
202
|
+
findings.push({
|
|
203
|
+
level: "fail",
|
|
204
|
+
code: "data-served-from-cache",
|
|
205
|
+
message:
|
|
206
|
+
cdn === "cloudflare"
|
|
207
|
+
? "a soft navigation (x-nifra-data: 1) got the cached document. The zone cache ignores Vary: add a Cache Rule that bypasses the cache when the request has an x-nifra-data header"
|
|
208
|
+
: "a soft navigation (x-nifra-data: 1) got the cached document instead of the page data; the CDN is not honoring Vary: x-nifra-data",
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
return { url: input.url, cdn, requests, findings }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function renderCdnCheck(out: CdnCheckOutput): readonly string[] {
|
|
215
|
+
const lines = [`cdn-check ${out.url} (cdn: ${out.cdn})`]
|
|
216
|
+
for (const r of out.requests) {
|
|
217
|
+
lines.push(
|
|
218
|
+
` ${r.kind.padEnd(6)} ${r.status} cache=${r.cache ?? "-"} age=${r.age ?? "-"} ${r.contentType ?? ""}`,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
if (out.findings.length === 0) lines.push("ok - nothing to fix")
|
|
222
|
+
for (const f of out.findings) lines.push(`${f.level.toUpperCase()} ${f.code}: ${f.message}`)
|
|
223
|
+
return lines
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export const cdnCheckSpec: CommandSpec<CdnCheckInput, CdnCheckOutput> = {
|
|
227
|
+
name: "cdn-check",
|
|
228
|
+
summary:
|
|
229
|
+
"Check a deployed page behind a CDN: is it served from cache, do internal or CDN-only headers reach the visitor, and does a soft navigation get page data rather than the cached document.",
|
|
230
|
+
input: {
|
|
231
|
+
jsonSchema: {
|
|
232
|
+
type: "object",
|
|
233
|
+
properties: {
|
|
234
|
+
url: { type: "string", description: "The deployed page to check (http or https)." },
|
|
235
|
+
json: { type: "boolean" },
|
|
236
|
+
},
|
|
237
|
+
required: ["url"],
|
|
238
|
+
},
|
|
239
|
+
parse: parseInput,
|
|
240
|
+
},
|
|
241
|
+
output: {
|
|
242
|
+
version: 1,
|
|
243
|
+
jsonSchema: {
|
|
244
|
+
type: "object",
|
|
245
|
+
properties: {
|
|
246
|
+
url: { type: "string" },
|
|
247
|
+
cdn: { type: "string", enum: ["cloudflare", "vercel", "fastly", "unknown"] },
|
|
248
|
+
requests: { type: "array" },
|
|
249
|
+
findings: { type: "array" },
|
|
250
|
+
},
|
|
251
|
+
required: ["url", "cdn", "requests", "findings"],
|
|
252
|
+
},
|
|
253
|
+
parse: (value) => {
|
|
254
|
+
if (isCdnCheckOutput(value)) return value
|
|
255
|
+
throw new TypeError("cdn-check output must carry url, cdn, requests and findings")
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
transports: ["cli"],
|
|
259
|
+
stability: "experimental",
|
|
260
|
+
argv: {
|
|
261
|
+
positionals: ["url"],
|
|
262
|
+
flags: [{ name: "json", field: "json", type: "boolean" }],
|
|
263
|
+
},
|
|
264
|
+
run: (input, ctx) => runCdnCheck(input, ctx),
|
|
265
|
+
render: renderCdnCheck,
|
|
266
|
+
exitCode: (out) => (out.findings.some((f) => f.level === "fail") ? 1 : 0),
|
|
267
|
+
json: (out) => out,
|
|
268
|
+
}
|
package/src/check-diagnostics.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { existsSync } from "node:fs"
|
|
9
9
|
import { join } from "node:path"
|
|
10
10
|
import type { AssuranceConfig, AssuranceReport } from "@nifrajs/core/assurance"
|
|
11
|
+
import type { CapabilityGap } from "@nifrajs/core/capabilities"
|
|
11
12
|
import type { ProjectEvidenceSnapshot } from "@nifrajs/core/evidence"
|
|
12
13
|
import type { CapabilityProjectReport } from "./capabilities-tool.ts"
|
|
13
14
|
import {
|
|
@@ -19,12 +20,16 @@ import {
|
|
|
19
20
|
import type { DuplicateInstallFinding } from "./doctor.ts"
|
|
20
21
|
import type { PipelineReport } from "./pipeline-report.ts"
|
|
21
22
|
import { freezeProjectFacts, type ProjectFactsSeed } from "./project-facts.ts"
|
|
23
|
+
import { dataGuardRules } from "./rules/data-guard.ts"
|
|
22
24
|
import { parseRulePacks, runRuleRegistry } from "./rules/index.ts"
|
|
23
25
|
import { islandRules } from "./rules/islands.ts"
|
|
24
26
|
import { LEGACY_RULE_CODES, LEGACY_RULE_ORDER, legacyRules } from "./rules/legacy.ts"
|
|
25
27
|
import { nanoRules } from "./rules/nano.ts"
|
|
28
|
+
import { pageRules } from "./rules/pages.ts"
|
|
26
29
|
import { routeRules } from "./rules/routes.ts"
|
|
30
|
+
import { secretRules } from "./rules/secrets.ts"
|
|
27
31
|
import { securityRules } from "./rules/security.ts"
|
|
32
|
+
import { zoneRules } from "./rules/zones.ts"
|
|
28
33
|
|
|
29
34
|
/** A single machine-readable check failure - the unit an agent (or CI) acts on. */
|
|
30
35
|
export interface CheckDiagnostic {
|
|
@@ -84,6 +89,9 @@ export interface CheckResult {
|
|
|
84
89
|
* better-auth). Echoed here so `--json` / the MCP tool / the report can show what the typed-client scan
|
|
85
90
|
* deliberately skipped - a suppressed prefix stays auditable instead of silently hiding real drift. */
|
|
86
91
|
readonly externalMounts?: readonly string[]
|
|
92
|
+
/** Mounts declared `opaque` - outside capability assurance on purpose, each with its stated reason.
|
|
93
|
+
* Listed on every run so the report states its own boundary; never a failure. */
|
|
94
|
+
readonly knownGaps?: readonly CapabilityGap[]
|
|
87
95
|
/** Active per-rule overrides from `nifra.check.json` `rules`, echoed verbatim so a retagged or
|
|
88
96
|
* suppressed finding stays auditable in `--json`, the MCP tool, and the human report - config can
|
|
89
97
|
* lower (or raise) the gate, but never invisibly. */
|
|
@@ -419,12 +427,19 @@ export async function collectCheckDiagnostics(
|
|
|
419
427
|
root: cwd,
|
|
420
428
|
sources: projectFacts.source,
|
|
421
429
|
project: projectFacts,
|
|
430
|
+
...(projectFacts.check.typescriptSession === undefined
|
|
431
|
+
? {}
|
|
432
|
+
: { typescriptSession: projectFacts.check.typescriptSession }),
|
|
422
433
|
}
|
|
423
434
|
|
|
424
435
|
const builtInRules = [
|
|
425
436
|
...legacyRules,
|
|
426
437
|
...securityRules,
|
|
427
438
|
...routeRules,
|
|
439
|
+
...pageRules,
|
|
440
|
+
...zoneRules,
|
|
441
|
+
...dataGuardRules,
|
|
442
|
+
...secretRules,
|
|
428
443
|
...islandRules,
|
|
429
444
|
...nanoRules,
|
|
430
445
|
]
|
|
@@ -568,6 +583,7 @@ export async function collectCheckDiagnostics(
|
|
|
568
583
|
shown.length < total ? shown.length + nonLegacyStructuredCount : finalStructured.length
|
|
569
584
|
|
|
570
585
|
const doctor = projectFacts.packages.doctor
|
|
586
|
+
const knownGaps = projectFacts.policies.capability?.report.gaps ?? []
|
|
571
587
|
const result: CheckResult = {
|
|
572
588
|
ok: !finalDiagnostics.some((value) => value.severity === "error"),
|
|
573
589
|
typecheck: projectFacts.check.typecheck.ran
|
|
@@ -593,6 +609,7 @@ export async function collectCheckDiagnostics(
|
|
|
593
609
|
...(checkConfig.externalMounts.length === 0
|
|
594
610
|
? {}
|
|
595
611
|
: { externalMounts: checkConfig.externalMounts }),
|
|
612
|
+
...(knownGaps.length === 0 ? {} : { knownGaps }),
|
|
596
613
|
...(Object.keys(checkConfig.rules).length === 0 ? {} : { ruleOverrides: checkConfig.rules }),
|
|
597
614
|
...(shown.length < total ? { truncated: { shown: shown.length, total } } : {}),
|
|
598
615
|
}
|