@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.
Files changed (404) hide show
  1. package/README.md +42 -3
  2. package/dist/app-files.d.ts +11 -0
  3. package/dist/app-files.d.ts.map +1 -0
  4. package/dist/app-files.js +34 -0
  5. package/dist/app-files.js.map +1 -0
  6. package/dist/assure-hydration.d.ts +7 -1
  7. package/dist/assure-hydration.d.ts.map +1 -1
  8. package/dist/assure-hydration.js +128 -43
  9. package/dist/assure-hydration.js.map +1 -1
  10. package/dist/assure.d.ts.map +1 -1
  11. package/dist/assure.js +16 -2
  12. package/dist/assure.js.map +1 -1
  13. package/dist/capabilities-tool.d.ts +5 -1
  14. package/dist/capabilities-tool.d.ts.map +1 -1
  15. package/dist/capabilities-tool.js +32 -10
  16. package/dist/capabilities-tool.js.map +1 -1
  17. package/dist/cdn-check.d.ts +41 -0
  18. package/dist/cdn-check.d.ts.map +1 -0
  19. package/dist/cdn-check.js +207 -0
  20. package/dist/cdn-check.js.map +1 -0
  21. package/dist/check-diagnostics.d.ts +4 -0
  22. package/dist/check-diagnostics.d.ts.map +1 -1
  23. package/dist/check-diagnostics.js +13 -0
  24. package/dist/check-diagnostics.js.map +1 -1
  25. package/dist/check-scan.d.ts +49 -7
  26. package/dist/check-scan.d.ts.map +1 -1
  27. package/dist/check-scan.js +291 -62
  28. package/dist/check-scan.js.map +1 -1
  29. package/dist/check.d.ts.map +1 -1
  30. package/dist/check.js +99 -63
  31. package/dist/check.js.map +1 -1
  32. package/dist/cli.d.ts +42 -16
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +262 -82
  35. package/dist/cli.js.map +1 -1
  36. package/dist/command-catalog.d.ts +62 -5
  37. package/dist/command-catalog.d.ts.map +1 -1
  38. package/dist/command-catalog.js +275 -35
  39. package/dist/command-catalog.js.map +1 -1
  40. package/dist/config-target.d.ts +8 -0
  41. package/dist/config-target.d.ts.map +1 -0
  42. package/dist/config-target.js +45 -0
  43. package/dist/config-target.js.map +1 -0
  44. package/dist/contracts.d.ts.map +1 -1
  45. package/dist/contracts.js +52 -13
  46. package/dist/contracts.js.map +1 -1
  47. package/dist/db-audit.d.ts +37 -0
  48. package/dist/db-audit.d.ts.map +1 -0
  49. package/dist/db-audit.js +99 -0
  50. package/dist/db-audit.js.map +1 -0
  51. package/dist/db-child.d.ts +64 -0
  52. package/dist/db-child.d.ts.map +1 -0
  53. package/dist/db-child.js +213 -0
  54. package/dist/db-child.js.map +1 -0
  55. package/dist/db-config.d.ts +60 -0
  56. package/dist/db-config.d.ts.map +1 -0
  57. package/dist/db-config.js +176 -0
  58. package/dist/db-config.js.map +1 -0
  59. package/dist/db-tool.d.ts +84 -0
  60. package/dist/db-tool.d.ts.map +1 -0
  61. package/dist/db-tool.js +537 -0
  62. package/dist/db-tool.js.map +1 -0
  63. package/dist/dev-bun-config.d.ts +20 -13
  64. package/dist/dev-bun-config.d.ts.map +1 -1
  65. package/dist/dev-bun-config.js +98 -41
  66. package/dist/dev-bun-config.js.map +1 -1
  67. package/dist/dev-feed-tool.d.ts +106 -0
  68. package/dist/dev-feed-tool.d.ts.map +1 -0
  69. package/dist/dev-feed-tool.js +713 -0
  70. package/dist/dev-feed-tool.js.map +1 -0
  71. package/dist/dev-server-client.d.ts +67 -0
  72. package/dist/dev-server-client.d.ts.map +1 -0
  73. package/dist/dev-server-client.js +195 -0
  74. package/dist/dev-server-client.js.map +1 -0
  75. package/dist/diff-tool.d.ts +3 -3
  76. package/dist/diff-tool.d.ts.map +1 -1
  77. package/dist/diff-tool.js +6 -5
  78. package/dist/diff-tool.js.map +1 -1
  79. package/dist/docs-search.d.ts.map +1 -1
  80. package/dist/docs-search.js +12 -4
  81. package/dist/docs-search.js.map +1 -1
  82. package/dist/doctor.d.ts +17 -0
  83. package/dist/doctor.d.ts.map +1 -1
  84. package/dist/doctor.js +76 -15
  85. package/dist/doctor.js.map +1 -1
  86. package/dist/env-file.d.ts +15 -0
  87. package/dist/env-file.d.ts.map +1 -1
  88. package/dist/env-file.js +73 -1
  89. package/dist/env-file.js.map +1 -1
  90. package/dist/fix-recipes.d.ts.map +1 -1
  91. package/dist/fix-recipes.js +48 -22
  92. package/dist/fix-recipes.js.map +1 -1
  93. package/dist/frontend-guidance.js +5 -5
  94. package/dist/frontend-guidance.js.map +1 -1
  95. package/dist/i18n-check.d.ts +16 -0
  96. package/dist/i18n-check.d.ts.map +1 -0
  97. package/dist/i18n-check.js +154 -0
  98. package/dist/i18n-check.js.map +1 -0
  99. package/dist/init-agents.d.ts +49 -13
  100. package/dist/init-agents.d.ts.map +1 -1
  101. package/dist/init-agents.js +239 -34
  102. package/dist/init-agents.js.map +1 -1
  103. package/dist/internal/code-unit-order.d.ts +4 -0
  104. package/dist/internal/code-unit-order.d.ts.map +1 -0
  105. package/dist/internal/code-unit-order.js +6 -0
  106. package/dist/internal/code-unit-order.js.map +1 -0
  107. package/dist/internal/script-kind.d.ts +7 -0
  108. package/dist/internal/script-kind.d.ts.map +1 -0
  109. package/dist/internal/script-kind.js +12 -0
  110. package/dist/internal/script-kind.js.map +1 -0
  111. package/dist/internal/source-facts.d.ts +5 -0
  112. package/dist/internal/source-facts.d.ts.map +1 -1
  113. package/dist/internal/source-facts.js +61 -5
  114. package/dist/internal/source-facts.js.map +1 -1
  115. package/dist/internal/typescript-import.d.ts +49 -24
  116. package/dist/internal/typescript-import.d.ts.map +1 -1
  117. package/dist/internal/typescript-import.js +507 -67
  118. package/dist/internal/typescript-import.js.map +1 -1
  119. package/dist/introspect.d.ts +5 -1
  120. package/dist/introspect.d.ts.map +1 -1
  121. package/dist/introspect.js +92 -27
  122. package/dist/introspect.js.map +1 -1
  123. package/dist/learn.js +3 -3
  124. package/dist/learn.js.map +1 -1
  125. package/dist/levels-tool.d.ts +10 -0
  126. package/dist/levels-tool.d.ts.map +1 -1
  127. package/dist/levels-tool.js +33 -3
  128. package/dist/levels-tool.js.map +1 -1
  129. package/dist/load.d.ts +67 -17
  130. package/dist/load.d.ts.map +1 -1
  131. package/dist/load.js +78 -18
  132. package/dist/load.js.map +1 -1
  133. package/dist/manifest-tool.d.ts.map +1 -1
  134. package/dist/manifest-tool.js +3 -2
  135. package/dist/manifest-tool.js.map +1 -1
  136. package/dist/mcp-app-tool.d.ts.map +1 -1
  137. package/dist/mcp-app-tool.js +2 -0
  138. package/dist/mcp-app-tool.js.map +1 -1
  139. package/dist/mcp-context.js +1 -1
  140. package/dist/mcp-context.js.map +1 -1
  141. package/dist/mcp-delegate.d.ts +57 -0
  142. package/dist/mcp-delegate.d.ts.map +1 -0
  143. package/dist/mcp-delegate.js +130 -0
  144. package/dist/mcp-delegate.js.map +1 -0
  145. package/dist/mcp-docs-tools.d.ts.map +1 -1
  146. package/dist/mcp-docs-tools.js +23 -5
  147. package/dist/mcp-docs-tools.js.map +1 -1
  148. package/dist/mcp-exec.d.ts +2 -0
  149. package/dist/mcp-exec.d.ts.map +1 -1
  150. package/dist/mcp-exec.js +132 -108
  151. package/dist/mcp-exec.js.map +1 -1
  152. package/dist/mcp-http.d.ts.map +1 -1
  153. package/dist/mcp-http.js +17 -5
  154. package/dist/mcp-http.js.map +1 -1
  155. package/dist/mcp-io.d.ts +14 -0
  156. package/dist/mcp-io.d.ts.map +1 -1
  157. package/dist/mcp-io.js +45 -0
  158. package/dist/mcp-io.js.map +1 -1
  159. package/dist/mcp-isolate.d.ts +48 -0
  160. package/dist/mcp-isolate.d.ts.map +1 -0
  161. package/dist/mcp-isolate.js +222 -0
  162. package/dist/mcp-isolate.js.map +1 -0
  163. package/dist/mcp-project-child.d.ts +74 -0
  164. package/dist/mcp-project-child.d.ts.map +1 -0
  165. package/dist/mcp-project-child.js +130 -0
  166. package/dist/mcp-project-child.js.map +1 -0
  167. package/dist/mcp-render.js +2 -2
  168. package/dist/mcp-render.js.map +1 -1
  169. package/dist/mcp-root.d.ts +37 -9
  170. package/dist/mcp-root.d.ts.map +1 -1
  171. package/dist/mcp-root.js +110 -23
  172. package/dist/mcp-root.js.map +1 -1
  173. package/dist/mcp-run.d.ts +18 -1
  174. package/dist/mcp-run.d.ts.map +1 -1
  175. package/dist/mcp-run.js +82 -9
  176. package/dist/mcp-run.js.map +1 -1
  177. package/dist/mcp-ws.js +4 -1
  178. package/dist/mcp-ws.js.map +1 -1
  179. package/dist/mcp.d.ts +16 -3
  180. package/dist/mcp.d.ts.map +1 -1
  181. package/dist/mcp.js +81 -53
  182. package/dist/mcp.js.map +1 -1
  183. package/dist/migrate-layout.d.ts +44 -0
  184. package/dist/migrate-layout.d.ts.map +1 -0
  185. package/dist/migrate-layout.js +1155 -0
  186. package/dist/migrate-layout.js.map +1 -0
  187. package/dist/openapi-tool.d.ts +2 -2
  188. package/dist/openapi-tool.d.ts.map +1 -1
  189. package/dist/openapi-tool.js.map +1 -1
  190. package/dist/openapi-types.d.ts +2 -1
  191. package/dist/openapi-types.d.ts.map +1 -1
  192. package/dist/openapi-types.js +240 -12
  193. package/dist/openapi-types.js.map +1 -1
  194. package/dist/pipeline-report.d.ts +3 -2
  195. package/dist/pipeline-report.d.ts.map +1 -1
  196. package/dist/pipeline-report.js +19 -16
  197. package/dist/pipeline-report.js.map +1 -1
  198. package/dist/port.d.ts +5 -4
  199. package/dist/port.d.ts.map +1 -1
  200. package/dist/port.js +46 -29
  201. package/dist/port.js.map +1 -1
  202. package/dist/project-facts.d.ts +5 -0
  203. package/dist/project-facts.d.ts.map +1 -1
  204. package/dist/project-facts.js.map +1 -1
  205. package/dist/recipes/4.0.0.d.ts +9 -0
  206. package/dist/recipes/4.0.0.d.ts.map +1 -0
  207. package/dist/recipes/4.0.0.js +36 -0
  208. package/dist/recipes/4.0.0.js.map +1 -0
  209. package/dist/recipes/index.d.ts.map +1 -1
  210. package/dist/recipes/index.js +2 -0
  211. package/dist/recipes/index.js.map +1 -1
  212. package/dist/release-verification.d.ts +11 -1
  213. package/dist/release-verification.d.ts.map +1 -1
  214. package/dist/release-verification.js +47 -4
  215. package/dist/release-verification.js.map +1 -1
  216. package/dist/review.d.ts.map +1 -1
  217. package/dist/review.js +12 -8
  218. package/dist/review.js.map +1 -1
  219. package/dist/route-types.d.ts +28 -0
  220. package/dist/route-types.d.ts.map +1 -0
  221. package/dist/route-types.js +106 -0
  222. package/dist/route-types.js.map +1 -0
  223. package/dist/rules/codes.d.ts +9 -0
  224. package/dist/rules/codes.d.ts.map +1 -1
  225. package/dist/rules/codes.js +9 -0
  226. package/dist/rules/codes.js.map +1 -1
  227. package/dist/rules/data-guard.d.ts +12 -0
  228. package/dist/rules/data-guard.d.ts.map +1 -0
  229. package/dist/rules/data-guard.js +213 -0
  230. package/dist/rules/data-guard.js.map +1 -0
  231. package/dist/rules/index.d.ts +3 -0
  232. package/dist/rules/index.d.ts.map +1 -1
  233. package/dist/rules/index.js +17 -5
  234. package/dist/rules/index.js.map +1 -1
  235. package/dist/rules/islands.d.ts.map +1 -1
  236. package/dist/rules/islands.js +7 -4
  237. package/dist/rules/islands.js.map +1 -1
  238. package/dist/rules/legacy.d.ts.map +1 -1
  239. package/dist/rules/legacy.js +71 -38
  240. package/dist/rules/legacy.js.map +1 -1
  241. package/dist/rules/nano.d.ts.map +1 -1
  242. package/dist/rules/nano.js +15 -6
  243. package/dist/rules/nano.js.map +1 -1
  244. package/dist/rules/pages.d.ts +15 -0
  245. package/dist/rules/pages.d.ts.map +1 -0
  246. package/dist/rules/pages.js +88 -0
  247. package/dist/rules/pages.js.map +1 -0
  248. package/dist/rules/routes.d.ts +11 -0
  249. package/dist/rules/routes.d.ts.map +1 -1
  250. package/dist/rules/routes.js +180 -2
  251. package/dist/rules/routes.js.map +1 -1
  252. package/dist/rules/secrets.d.ts +10 -0
  253. package/dist/rules/secrets.d.ts.map +1 -0
  254. package/dist/rules/secrets.js +169 -0
  255. package/dist/rules/secrets.js.map +1 -0
  256. package/dist/rules/security.d.ts +7 -0
  257. package/dist/rules/security.d.ts.map +1 -1
  258. package/dist/rules/security.js +171 -65
  259. package/dist/rules/security.js.map +1 -1
  260. package/dist/rules/typescript.d.ts +16 -0
  261. package/dist/rules/typescript.d.ts.map +1 -0
  262. package/dist/rules/typescript.js +59 -0
  263. package/dist/rules/typescript.js.map +1 -0
  264. package/dist/rules/zones.d.ts +17 -0
  265. package/dist/rules/zones.d.ts.map +1 -0
  266. package/dist/rules/zones.js +174 -0
  267. package/dist/rules/zones.js.map +1 -0
  268. package/dist/scaffold.d.ts +25 -12
  269. package/dist/scaffold.d.ts.map +1 -1
  270. package/dist/scaffold.js +132 -87
  271. package/dist/scaffold.js.map +1 -1
  272. package/dist/sdk.d.ts +17 -3
  273. package/dist/sdk.d.ts.map +1 -1
  274. package/dist/sdk.js +941 -153
  275. package/dist/sdk.js.map +1 -1
  276. package/dist/search-terms.d.ts +2 -0
  277. package/dist/search-terms.d.ts.map +1 -1
  278. package/dist/search-terms.js +23 -8
  279. package/dist/search-terms.js.map +1 -1
  280. package/dist/smoke.d.ts +79 -0
  281. package/dist/smoke.d.ts.map +1 -0
  282. package/dist/smoke.js +494 -0
  283. package/dist/smoke.js.map +1 -0
  284. package/dist/sync-manifest.d.ts +10 -2
  285. package/dist/sync-manifest.d.ts.map +1 -1
  286. package/dist/sync-manifest.js +44 -19
  287. package/dist/sync-manifest.js.map +1 -1
  288. package/dist/sync-routes.d.ts +4 -3
  289. package/dist/sync-routes.d.ts.map +1 -1
  290. package/dist/sync-routes.js +4 -3
  291. package/dist/sync-routes.js.map +1 -1
  292. package/dist/test-tool.d.ts.map +1 -1
  293. package/dist/test-tool.js +30 -11
  294. package/dist/test-tool.js.map +1 -1
  295. package/dist/types-search.d.ts.map +1 -1
  296. package/dist/types-search.js +11 -5
  297. package/dist/types-search.js.map +1 -1
  298. package/dist/upgrade.d.ts +36 -6
  299. package/dist/upgrade.d.ts.map +1 -1
  300. package/dist/upgrade.js +167 -29
  301. package/dist/upgrade.js.map +1 -1
  302. package/dist/verification-plan.d.ts.map +1 -1
  303. package/dist/verification-plan.js +4 -0
  304. package/dist/verification-plan.js.map +1 -1
  305. package/dist/verification.d.ts.map +1 -1
  306. package/dist/verification.js +45 -12
  307. package/dist/verification.js.map +1 -1
  308. package/dist/web-app-options.d.ts +19 -0
  309. package/dist/web-app-options.d.ts.map +1 -0
  310. package/dist/web-app-options.js +49 -0
  311. package/dist/web-app-options.js.map +1 -0
  312. package/dist/work-graph.d.ts.map +1 -1
  313. package/dist/work-graph.js +29 -13
  314. package/dist/work-graph.js.map +1 -1
  315. package/docs/examples.json +185 -29
  316. package/docs/llms-full.txt +3807 -580
  317. package/docs/types.json +14229 -10939
  318. package/package.json +19 -12
  319. package/src/app-files.ts +40 -0
  320. package/src/assure-hydration.ts +139 -46
  321. package/src/assure.ts +17 -2
  322. package/src/capabilities-tool.ts +35 -9
  323. package/src/cdn-check.ts +268 -0
  324. package/src/check-diagnostics.ts +17 -0
  325. package/src/check-scan.ts +358 -63
  326. package/src/check.ts +97 -62
  327. package/src/cli.ts +296 -87
  328. package/src/command-catalog.ts +389 -48
  329. package/src/config-target.ts +47 -0
  330. package/src/contracts.ts +54 -12
  331. package/src/db-audit.ts +128 -0
  332. package/src/db-child.ts +285 -0
  333. package/src/db-config.ts +224 -0
  334. package/src/db-tool.ts +690 -0
  335. package/src/dev-bun-config.ts +116 -40
  336. package/src/dev-feed-tool.ts +912 -0
  337. package/src/dev-server-client.ts +267 -0
  338. package/src/diff-tool.ts +6 -5
  339. package/src/docs-search.ts +10 -3
  340. package/src/doctor.ts +98 -15
  341. package/src/env-file.ts +78 -1
  342. package/src/fix-recipes.ts +53 -32
  343. package/src/frontend-guidance.ts +5 -5
  344. package/src/i18n-check.ts +179 -0
  345. package/src/init-agents.ts +301 -38
  346. package/src/internal/code-unit-order.ts +5 -0
  347. package/src/internal/script-kind.ts +11 -0
  348. package/src/internal/source-facts.ts +96 -22
  349. package/src/internal/typescript-import.ts +663 -68
  350. package/src/introspect.ts +90 -27
  351. package/src/learn.ts +3 -3
  352. package/src/levels-tool.ts +47 -2
  353. package/src/load.ts +140 -33
  354. package/src/manifest-tool.ts +3 -2
  355. package/src/mcp-app-tool.ts +2 -0
  356. package/src/mcp-context.ts +1 -1
  357. package/src/mcp-delegate.ts +162 -0
  358. package/src/mcp-docs-tools.ts +23 -5
  359. package/src/mcp-exec.ts +160 -132
  360. package/src/mcp-http.ts +19 -5
  361. package/src/mcp-io.ts +57 -0
  362. package/src/mcp-isolate.ts +287 -0
  363. package/src/mcp-project-child.ts +182 -0
  364. package/src/mcp-render.ts +2 -2
  365. package/src/mcp-root.ts +117 -22
  366. package/src/mcp-run.ts +104 -10
  367. package/src/mcp-ws.ts +4 -1
  368. package/src/mcp.ts +106 -64
  369. package/src/migrate-layout.ts +1338 -0
  370. package/src/openapi-tool.ts +7 -3
  371. package/src/openapi-types.ts +315 -13
  372. package/src/pipeline-report.ts +22 -18
  373. package/src/port.ts +53 -30
  374. package/src/project-facts.ts +5 -0
  375. package/src/recipes/4.0.0.ts +37 -0
  376. package/src/recipes/index.ts +2 -0
  377. package/src/release-verification.ts +74 -5
  378. package/src/review.ts +13 -8
  379. package/src/route-types.ts +133 -0
  380. package/src/rules/codes.ts +9 -0
  381. package/src/rules/data-guard.ts +230 -0
  382. package/src/rules/index.ts +18 -5
  383. package/src/rules/islands.ts +12 -4
  384. package/src/rules/legacy.ts +71 -39
  385. package/src/rules/nano.ts +18 -6
  386. package/src/rules/pages.ts +99 -0
  387. package/src/rules/routes.ts +181 -2
  388. package/src/rules/secrets.ts +174 -0
  389. package/src/rules/security.ts +198 -70
  390. package/src/rules/typescript.ts +77 -0
  391. package/src/rules/zones.ts +195 -0
  392. package/src/scaffold.ts +147 -95
  393. package/src/sdk.ts +1215 -158
  394. package/src/search-terms.ts +24 -7
  395. package/src/smoke.ts +607 -0
  396. package/src/sync-manifest.ts +50 -19
  397. package/src/sync-routes.ts +4 -3
  398. package/src/test-tool.ts +32 -11
  399. package/src/types-search.ts +11 -4
  400. package/src/upgrade.ts +205 -25
  401. package/src/verification-plan.ts +13 -0
  402. package/src/verification.ts +50 -12
  403. package/src/web-app-options.ts +57 -0
  404. package/src/work-graph.ts +27 -13
@@ -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
+ }
@@ -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
  }