@nifrajs/web 3.3.0 → 4.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.
Files changed (407) hide show
  1. package/dist/backend-only.d.ts +15 -0
  2. package/dist/backend-only.d.ts.map +1 -0
  3. package/dist/backend-only.js +16 -0
  4. package/dist/backend-only.js.map +1 -0
  5. package/dist/boundary.d.ts +5 -0
  6. package/dist/boundary.d.ts.map +1 -1
  7. package/dist/boundary.js +11 -2
  8. package/dist/boundary.js.map +1 -1
  9. package/dist/build-plan.d.ts +35 -6
  10. package/dist/build-plan.d.ts.map +1 -1
  11. package/dist/build-plan.js +89 -35
  12. package/dist/build-plan.js.map +1 -1
  13. package/dist/build-vite.d.ts +18 -0
  14. package/dist/build-vite.d.ts.map +1 -1
  15. package/dist/build-vite.js +97 -17
  16. package/dist/build-vite.js.map +1 -1
  17. package/dist/build.d.ts +72 -23
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/build.js +411 -98
  20. package/dist/build.js.map +1 -1
  21. package/dist/bun-dev-entry.d.ts +1 -1
  22. package/dist/bun-dev-entry.js +1 -1
  23. package/dist/cdn.d.ts +74 -0
  24. package/dist/cdn.d.ts.map +1 -0
  25. package/dist/cdn.js +132 -0
  26. package/dist/cdn.js.map +1 -0
  27. package/dist/client.d.ts +21 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +226 -15
  30. package/dist/client.js.map +1 -1
  31. package/dist/conformance.d.ts +4 -3
  32. package/dist/conformance.d.ts.map +1 -1
  33. package/dist/conformance.js +12 -3
  34. package/dist/conformance.js.map +1 -1
  35. package/dist/csp.d.ts +93 -0
  36. package/dist/csp.d.ts.map +1 -0
  37. package/dist/csp.js +122 -0
  38. package/dist/csp.js.map +1 -0
  39. package/dist/css-contract.d.ts +21 -0
  40. package/dist/css-contract.d.ts.map +1 -0
  41. package/dist/css-contract.js +28 -0
  42. package/dist/css-contract.js.map +1 -0
  43. package/dist/dev-client.d.ts +76 -0
  44. package/dist/dev-client.d.ts.map +1 -0
  45. package/dist/dev-client.js +280 -0
  46. package/dist/dev-client.js.map +1 -0
  47. package/dist/dev-diagnostics.d.ts +26 -2
  48. package/dist/dev-diagnostics.d.ts.map +1 -1
  49. package/dist/dev-diagnostics.js +38 -7
  50. package/dist/dev-diagnostics.js.map +1 -1
  51. package/dist/dev-error.d.ts +4 -13
  52. package/dist/dev-error.d.ts.map +1 -1
  53. package/dist/dev-error.js +46 -4
  54. package/dist/dev-error.js.map +1 -1
  55. package/dist/dev-feed.d.ts +298 -0
  56. package/dist/dev-feed.d.ts.map +1 -0
  57. package/dist/dev-feed.js +797 -0
  58. package/dist/dev-feed.js.map +1 -0
  59. package/dist/dev-indicator.d.ts +38 -0
  60. package/dist/dev-indicator.d.ts.map +1 -0
  61. package/dist/dev-indicator.js +93 -0
  62. package/dist/dev-indicator.js.map +1 -0
  63. package/dist/dev-session.d.ts +77 -0
  64. package/dist/dev-session.d.ts.map +1 -0
  65. package/dist/dev-session.js +580 -0
  66. package/dist/dev-session.js.map +1 -0
  67. package/dist/dev-sourcemap.d.ts +27 -0
  68. package/dist/dev-sourcemap.d.ts.map +1 -0
  69. package/dist/dev-sourcemap.js +235 -0
  70. package/dist/dev-sourcemap.js.map +1 -0
  71. package/dist/dev-ssr-graph.d.ts.map +1 -1
  72. package/dist/dev-ssr-graph.js +13 -5
  73. package/dist/dev-ssr-graph.js.map +1 -1
  74. package/dist/dev.d.ts +15 -2
  75. package/dist/dev.d.ts.map +1 -1
  76. package/dist/dev.js +69 -44
  77. package/dist/dev.js.map +1 -1
  78. package/dist/diagnostic-catalog.d.ts +35 -0
  79. package/dist/diagnostic-catalog.d.ts.map +1 -0
  80. package/dist/diagnostic-catalog.js +203 -0
  81. package/dist/diagnostic-catalog.js.map +1 -0
  82. package/dist/diagnostic-prompt.d.ts +49 -0
  83. package/dist/diagnostic-prompt.d.ts.map +1 -0
  84. package/dist/diagnostic-prompt.js +303 -0
  85. package/dist/diagnostic-prompt.js.map +1 -0
  86. package/dist/diagnostic.d.ts +17 -23
  87. package/dist/diagnostic.d.ts.map +1 -1
  88. package/dist/diagnostic.js +31 -39
  89. package/dist/diagnostic.js.map +1 -1
  90. package/dist/draft.d.ts.map +1 -1
  91. package/dist/draft.js +4 -1
  92. package/dist/draft.js.map +1 -1
  93. package/dist/fn.d.ts +5 -0
  94. package/dist/fn.d.ts.map +1 -1
  95. package/dist/fn.js +13 -3
  96. package/dist/fn.js.map +1 -1
  97. package/dist/fs.d.ts.map +1 -1
  98. package/dist/fs.js +4 -2
  99. package/dist/fs.js.map +1 -1
  100. package/dist/index.d.ts +15 -15
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +13 -12
  103. package/dist/index.js.map +1 -1
  104. package/dist/internal/cdn/cloudflare.d.ts +48 -0
  105. package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
  106. package/dist/internal/cdn/cloudflare.js +94 -0
  107. package/dist/internal/cdn/cloudflare.js.map +1 -0
  108. package/dist/internal/cdn/core.d.ts +91 -0
  109. package/dist/internal/cdn/core.d.ts.map +1 -0
  110. package/dist/internal/cdn/core.js +220 -0
  111. package/dist/internal/cdn/core.js.map +1 -0
  112. package/dist/internal/cdn/fastly.d.ts +17 -0
  113. package/dist/internal/cdn/fastly.d.ts.map +1 -0
  114. package/dist/internal/cdn/fastly.js +43 -0
  115. package/dist/internal/cdn/fastly.js.map +1 -0
  116. package/dist/internal/cdn/vercel.d.ts +31 -0
  117. package/dist/internal/cdn/vercel.d.ts.map +1 -0
  118. package/dist/internal/cdn/vercel.js +56 -0
  119. package/dist/internal/cdn/vercel.js.map +1 -0
  120. package/dist/internal/code-unit-order.d.ts +4 -0
  121. package/dist/internal/code-unit-order.d.ts.map +1 -0
  122. package/dist/internal/code-unit-order.js +6 -0
  123. package/dist/internal/code-unit-order.js.map +1 -0
  124. package/dist/internal/codegen.d.ts +3 -0
  125. package/dist/internal/codegen.d.ts.map +1 -1
  126. package/dist/internal/codegen.js +151 -30
  127. package/dist/internal/codegen.js.map +1 -1
  128. package/dist/internal/dev-csp.d.ts +24 -0
  129. package/dist/internal/dev-csp.d.ts.map +1 -0
  130. package/dist/internal/dev-csp.js +160 -0
  131. package/dist/internal/dev-csp.js.map +1 -0
  132. package/dist/internal/dev-reserved.d.ts +25 -0
  133. package/dist/internal/dev-reserved.d.ts.map +1 -0
  134. package/dist/internal/dev-reserved.js +39 -0
  135. package/dist/internal/dev-reserved.js.map +1 -0
  136. package/dist/internal/html-spans.d.ts +10 -0
  137. package/dist/internal/html-spans.d.ts.map +1 -0
  138. package/dist/internal/html-spans.js +46 -0
  139. package/dist/internal/html-spans.js.map +1 -0
  140. package/dist/internal/loading-runtime.d.ts +44 -0
  141. package/dist/internal/loading-runtime.d.ts.map +1 -0
  142. package/dist/internal/loading-runtime.js +206 -0
  143. package/dist/internal/loading-runtime.js.map +1 -0
  144. package/dist/internal/matches-runtime.d.ts +8 -0
  145. package/dist/internal/matches-runtime.d.ts.map +1 -0
  146. package/dist/internal/matches-runtime.js +61 -0
  147. package/dist/internal/matches-runtime.js.map +1 -0
  148. package/dist/internal/mount-shadow.d.ts +36 -0
  149. package/dist/internal/mount-shadow.d.ts.map +1 -0
  150. package/dist/internal/mount-shadow.js +65 -0
  151. package/dist/internal/mount-shadow.js.map +1 -0
  152. package/dist/internal/node-builtins.d.ts +3 -0
  153. package/dist/internal/node-builtins.d.ts.map +1 -0
  154. package/dist/internal/node-builtins.js +79 -0
  155. package/dist/internal/node-builtins.js.map +1 -0
  156. package/dist/internal/output-guard.d.ts +65 -0
  157. package/dist/internal/output-guard.d.ts.map +1 -0
  158. package/dist/internal/output-guard.js +630 -0
  159. package/dist/internal/output-guard.js.map +1 -0
  160. package/dist/internal/page-execution.d.ts +69 -0
  161. package/dist/internal/page-execution.d.ts.map +1 -0
  162. package/dist/internal/page-execution.js +1021 -0
  163. package/dist/internal/page-execution.js.map +1 -0
  164. package/dist/internal/parity.d.ts +21 -0
  165. package/dist/internal/parity.d.ts.map +1 -1
  166. package/dist/internal/parity.js +104 -22
  167. package/dist/internal/parity.js.map +1 -1
  168. package/dist/internal/private-env.d.ts +12 -0
  169. package/dist/internal/private-env.d.ts.map +1 -0
  170. package/dist/internal/private-env.js +335 -0
  171. package/dist/internal/private-env.js.map +1 -0
  172. package/dist/internal/public-url.d.ts +3 -0
  173. package/dist/internal/public-url.d.ts.map +1 -0
  174. package/dist/internal/public-url.js +18 -0
  175. package/dist/internal/public-url.js.map +1 -0
  176. package/dist/internal/render-document.d.ts +37 -21
  177. package/dist/internal/render-document.d.ts.map +1 -1
  178. package/dist/internal/render-document.js +104 -65
  179. package/dist/internal/render-document.js.map +1 -1
  180. package/dist/internal/request-url.d.ts +3 -0
  181. package/dist/internal/request-url.d.ts.map +1 -0
  182. package/dist/internal/request-url.js +14 -0
  183. package/dist/internal/request-url.js.map +1 -0
  184. package/dist/internal/response-controls.d.ts +75 -0
  185. package/dist/internal/response-controls.d.ts.map +1 -0
  186. package/dist/internal/response-controls.js +256 -0
  187. package/dist/internal/response-controls.js.map +1 -0
  188. package/dist/internal/runtime-contract.d.ts +11 -16
  189. package/dist/internal/runtime-contract.d.ts.map +1 -1
  190. package/dist/internal/runtime-contract.js.map +1 -1
  191. package/dist/internal/secret-scan.d.ts +104 -0
  192. package/dist/internal/secret-scan.d.ts.map +1 -0
  193. package/dist/internal/secret-scan.js +485 -0
  194. package/dist/internal/secret-scan.js.map +1 -0
  195. package/dist/internal/server-boundary.d.ts +6 -29
  196. package/dist/internal/server-boundary.d.ts.map +1 -1
  197. package/dist/internal/server-boundary.js +6 -66
  198. package/dist/internal/server-boundary.js.map +1 -1
  199. package/dist/internal/source-map.d.ts +47 -0
  200. package/dist/internal/source-map.d.ts.map +1 -0
  201. package/dist/internal/source-map.js +225 -0
  202. package/dist/internal/source-map.js.map +1 -0
  203. package/dist/internal/target-compat.d.ts +27 -0
  204. package/dist/internal/target-compat.d.ts.map +1 -0
  205. package/dist/internal/target-compat.js +77 -0
  206. package/dist/internal/target-compat.js.map +1 -0
  207. package/dist/internal/timing-safe-equal.d.ts +6 -0
  208. package/dist/internal/timing-safe-equal.d.ts.map +1 -1
  209. package/dist/internal/timing-safe-equal.js +10 -0
  210. package/dist/internal/timing-safe-equal.js.map +1 -1
  211. package/dist/internal/vitals-runtime.d.ts +18 -0
  212. package/dist/internal/vitals-runtime.d.ts.map +1 -0
  213. package/dist/internal/vitals-runtime.js +38 -0
  214. package/dist/internal/vitals-runtime.js.map +1 -0
  215. package/dist/internal/web-app.d.ts +56 -8
  216. package/dist/internal/web-app.d.ts.map +1 -1
  217. package/dist/internal/web-app.js +215 -772
  218. package/dist/internal/web-app.js.map +1 -1
  219. package/dist/internal/zone-graph.d.ts +89 -0
  220. package/dist/internal/zone-graph.d.ts.map +1 -0
  221. package/dist/internal/zone-graph.js +492 -0
  222. package/dist/internal/zone-graph.js.map +1 -0
  223. package/dist/isr.d.ts +83 -6
  224. package/dist/isr.d.ts.map +1 -1
  225. package/dist/isr.js +301 -39
  226. package/dist/isr.js.map +1 -1
  227. package/dist/llms-txt.d.ts +3 -0
  228. package/dist/llms-txt.d.ts.map +1 -1
  229. package/dist/llms-txt.js +37 -9
  230. package/dist/llms-txt.js.map +1 -1
  231. package/dist/manifest.d.ts +221 -14
  232. package/dist/manifest.d.ts.map +1 -1
  233. package/dist/manifest.js +386 -9
  234. package/dist/manifest.js.map +1 -1
  235. package/dist/module-graph.d.ts +26 -5
  236. package/dist/module-graph.d.ts.map +1 -1
  237. package/dist/module-graph.js +17 -7
  238. package/dist/module-graph.js.map +1 -1
  239. package/dist/navigation.d.ts +17 -3
  240. package/dist/navigation.d.ts.map +1 -1
  241. package/dist/navigation.js +11 -5
  242. package/dist/navigation.js.map +1 -1
  243. package/dist/nonce.d.ts +47 -0
  244. package/dist/nonce.d.ts.map +1 -0
  245. package/dist/nonce.js +63 -0
  246. package/dist/nonce.js.map +1 -0
  247. package/dist/plugins/css-modules.d.ts.map +1 -1
  248. package/dist/plugins/css-modules.js +4 -3
  249. package/dist/plugins/css-modules.js.map +1 -1
  250. package/dist/plugins/kit.d.ts +9 -0
  251. package/dist/plugins/kit.d.ts.map +1 -1
  252. package/dist/plugins/kit.js +18 -0
  253. package/dist/plugins/kit.js.map +1 -1
  254. package/dist/plugins/svg.d.ts +22 -3
  255. package/dist/plugins/svg.d.ts.map +1 -1
  256. package/dist/plugins/svg.js +112 -1
  257. package/dist/plugins/svg.js.map +1 -1
  258. package/dist/plugins/vite-leak-guard.d.ts +107 -3
  259. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  260. package/dist/plugins/vite-leak-guard.js +378 -17
  261. package/dist/plugins/vite-leak-guard.js.map +1 -1
  262. package/dist/plugins/vite-zone-guard.d.ts +61 -0
  263. package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
  264. package/dist/plugins/vite-zone-guard.js +208 -0
  265. package/dist/plugins/vite-zone-guard.js.map +1 -0
  266. package/dist/plugins/zone-guard.d.ts +27 -0
  267. package/dist/plugins/zone-guard.d.ts.map +1 -0
  268. package/dist/plugins/zone-guard.js +72 -0
  269. package/dist/plugins/zone-guard.js.map +1 -0
  270. package/dist/prerender.d.ts +10 -0
  271. package/dist/prerender.d.ts.map +1 -1
  272. package/dist/prerender.js +35 -3
  273. package/dist/prerender.js.map +1 -1
  274. package/dist/public-dir.d.ts +9 -0
  275. package/dist/public-dir.d.ts.map +1 -1
  276. package/dist/public-dir.js +131 -48
  277. package/dist/public-dir.js.map +1 -1
  278. package/dist/pwa-manifest.d.ts +68 -0
  279. package/dist/pwa-manifest.d.ts.map +1 -0
  280. package/dist/pwa-manifest.js +178 -0
  281. package/dist/pwa-manifest.js.map +1 -0
  282. package/dist/query.js +4 -4
  283. package/dist/query.js.map +1 -1
  284. package/dist/render-seam.d.ts +42 -2
  285. package/dist/render-seam.d.ts.map +1 -1
  286. package/dist/render-seam.js +6 -0
  287. package/dist/render-seam.js.map +1 -1
  288. package/dist/route-manifest.d.ts +3 -0
  289. package/dist/route-manifest.d.ts.map +1 -1
  290. package/dist/route-manifest.js +26 -3
  291. package/dist/route-manifest.js.map +1 -1
  292. package/dist/route-types.d.ts +24 -0
  293. package/dist/route-types.d.ts.map +1 -0
  294. package/dist/route-types.js +195 -0
  295. package/dist/route-types.js.map +1 -0
  296. package/dist/router.d.ts +28 -6
  297. package/dist/router.d.ts.map +1 -1
  298. package/dist/router.js +129 -40
  299. package/dist/router.js.map +1 -1
  300. package/dist/trusted-html.d.ts +34 -0
  301. package/dist/trusted-html.d.ts.map +1 -0
  302. package/dist/trusted-html.js +35 -0
  303. package/dist/trusted-html.js.map +1 -0
  304. package/dist/vitals.d.ts +33 -0
  305. package/dist/vitals.d.ts.map +1 -0
  306. package/dist/vitals.js +16 -0
  307. package/dist/vitals.js.map +1 -0
  308. package/dist/vite.d.ts +16 -1
  309. package/dist/vite.d.ts.map +1 -1
  310. package/dist/vite.js +187 -36
  311. package/dist/vite.js.map +1 -1
  312. package/dist/zones.d.ts +70 -0
  313. package/dist/zones.d.ts.map +1 -0
  314. package/dist/zones.js +365 -0
  315. package/dist/zones.js.map +1 -0
  316. package/package.json +67 -14
  317. package/src/backend-only.ts +16 -0
  318. package/src/boundary.ts +18 -5
  319. package/src/build-plan.ts +113 -36
  320. package/src/build-vite.ts +124 -15
  321. package/src/build.ts +535 -102
  322. package/src/bun-dev-entry.ts +1 -1
  323. package/src/cdn.ts +216 -0
  324. package/src/client.ts +251 -18
  325. package/src/conformance.ts +16 -3
  326. package/src/csp.ts +194 -0
  327. package/src/css-contract.ts +44 -0
  328. package/src/dev-client.ts +349 -0
  329. package/src/dev-diagnostics.ts +60 -7
  330. package/src/dev-error.ts +49 -5
  331. package/src/dev-feed.ts +1140 -0
  332. package/src/dev-indicator.ts +113 -0
  333. package/src/dev-session.ts +729 -0
  334. package/src/dev-sourcemap.ts +281 -0
  335. package/src/dev-ssr-graph.ts +16 -5
  336. package/src/dev.ts +86 -43
  337. package/src/diagnostic-catalog.ts +243 -0
  338. package/src/diagnostic-prompt.ts +355 -0
  339. package/src/diagnostic.ts +63 -66
  340. package/src/draft.ts +4 -1
  341. package/src/fn.ts +19 -3
  342. package/src/fs.ts +13 -2
  343. package/src/index.ts +57 -10
  344. package/src/internal/cdn/cloudflare.ts +146 -0
  345. package/src/internal/cdn/core.ts +316 -0
  346. package/src/internal/cdn/fastly.ts +62 -0
  347. package/src/internal/cdn/vercel.ts +95 -0
  348. package/src/internal/code-unit-order.ts +5 -0
  349. package/src/internal/codegen.ts +180 -29
  350. package/src/internal/dev-csp.ts +176 -0
  351. package/src/internal/dev-reserved.ts +52 -0
  352. package/src/internal/html-spans.ts +50 -0
  353. package/src/internal/loading-runtime.ts +244 -0
  354. package/src/internal/matches-runtime.ts +62 -0
  355. package/src/internal/mount-shadow.ts +88 -0
  356. package/src/internal/node-builtins.ts +79 -0
  357. package/src/internal/output-guard.ts +701 -0
  358. package/src/internal/page-execution.ts +1371 -0
  359. package/src/internal/parity.ts +150 -36
  360. package/src/internal/private-env.ts +334 -0
  361. package/src/internal/public-url.ts +22 -0
  362. package/src/internal/render-document.ts +153 -73
  363. package/src/internal/request-url.ts +14 -0
  364. package/src/internal/response-controls.ts +284 -0
  365. package/src/internal/runtime-contract.ts +10 -15
  366. package/src/internal/secret-scan.ts +623 -0
  367. package/src/internal/server-boundary.ts +6 -69
  368. package/src/internal/source-map.ts +256 -0
  369. package/src/internal/target-compat.ts +92 -0
  370. package/src/internal/timing-safe-equal.ts +13 -0
  371. package/src/internal/vitals-runtime.ts +53 -0
  372. package/src/internal/web-app.ts +290 -879
  373. package/src/internal/zone-graph.ts +593 -0
  374. package/src/isr.ts +366 -39
  375. package/src/llms-txt.ts +44 -8
  376. package/src/manifest.ts +671 -17
  377. package/src/module-graph.ts +53 -9
  378. package/src/navigation.ts +29 -6
  379. package/src/nonce.ts +104 -0
  380. package/src/plugins/css-modules.ts +5 -3
  381. package/src/plugins/kit.ts +24 -0
  382. package/src/plugins/svg.ts +135 -4
  383. package/src/plugins/vite-leak-guard.ts +529 -16
  384. package/src/plugins/vite-zone-guard.ts +263 -0
  385. package/src/plugins/zone-guard.ts +110 -0
  386. package/src/prerender.ts +49 -3
  387. package/src/public-dir.ts +135 -49
  388. package/src/pwa-manifest.ts +226 -0
  389. package/src/query.ts +4 -4
  390. package/src/render-seam.ts +46 -1
  391. package/src/route-manifest.ts +15 -3
  392. package/src/route-types.ts +264 -0
  393. package/src/router.ts +163 -63
  394. package/src/trusted-html.ts +44 -0
  395. package/src/vitals.ts +44 -0
  396. package/src/vite.ts +207 -36
  397. package/src/zones.ts +430 -0
  398. package/dist/plugins/vite-server-only.d.ts +0 -49
  399. package/dist/plugins/vite-server-only.d.ts.map +0 -1
  400. package/dist/plugins/vite-server-only.js +0 -57
  401. package/dist/plugins/vite-server-only.js.map +0 -1
  402. package/dist/server-only.d.ts +0 -21
  403. package/dist/server-only.d.ts.map +0 -1
  404. package/dist/server-only.js +0 -25
  405. package/dist/server-only.js.map +0 -1
  406. package/src/plugins/vite-server-only.ts +0 -75
  407. package/src/server-only.ts +0 -25
package/src/diagnostic.ts CHANGED
@@ -14,6 +14,22 @@
14
14
  import { readFileSync, realpathSync } from "node:fs"
15
15
  import { isAbsolute, relative, resolve } from "node:path"
16
16
  import { fileURLToPath } from "node:url"
17
+ import { classify, type FixOption } from "./diagnostic-catalog.ts"
18
+
19
+ export {
20
+ classify,
21
+ DIAGNOSTIC_CATALOG,
22
+ type FixOption,
23
+ isHydrationMismatch,
24
+ } from "./diagnostic-catalog.ts"
25
+ export {
26
+ buildFixPrompt,
27
+ type FixPrompt,
28
+ type FixPromptContext,
29
+ type FixPromptSurface,
30
+ fixPrompts,
31
+ promptPath,
32
+ } from "./diagnostic-prompt.ts"
17
33
 
18
34
  /** Shared endpoint name used by both dev pipelines and the agent-facing MCP tools. */
19
35
  export const LAST_ERROR_PATH = "/__nifra/last-error"
@@ -40,7 +56,7 @@ export interface Codeframe {
40
56
 
41
57
  /** The structured failure. Serialisable as-is to JSON for the agent surfaces. */
42
58
  export interface Diagnostic {
43
- /** Stable, greppable identifier, e.g. `NIFRA_SERVER_ONLY_IN_CLIENT`. `NIFRA_UNHANDLED` when unrecognised. */
59
+ /** Stable, greppable identifier, e.g. `NIFRA_BACKEND_IN_CLIENT`. `NIFRA_UNHANDLED` when unrecognised. */
44
60
  readonly code: string
45
61
  readonly name: string
46
62
  readonly message: string
@@ -51,8 +67,10 @@ export interface Diagnostic {
51
67
  readonly cause?: string | undefined
52
68
  /** Plain-language "do this", when the failure is recognised. */
53
69
  readonly fix?: string | undefined
54
- /** Docs section anchor for the code, e.g. `errors#server-only-in-client`. */
70
+ /** Docs section anchor for the code, e.g. `errors#backend-in-client`. */
55
71
  readonly docsAnchor?: string | undefined
72
+ /** The distinct ways to fix it, when more than one is right; `fix` covers them all in one line. */
73
+ readonly fixOptions?: readonly FixOption[] | undefined
56
74
  }
57
75
 
58
76
  /** Reads a source file's text, or returns undefined if it cannot (missing, binary, permission). */
@@ -178,61 +196,6 @@ export function buildCodeframe(
178
196
  return { file, line, column, lines }
179
197
  }
180
198
 
181
- /** A recognised failure shape: a stable code plus the plain-language cause/fix/anchor to attach. */
182
- interface CatalogEntry {
183
- readonly code: string
184
- readonly match: (name: string, message: string) => boolean
185
- readonly cause: string
186
- readonly fix: string
187
- readonly docsAnchor: string
188
- }
189
-
190
- /**
191
- * The recognised-failure catalog. Seeded with the highest-signal nifra failures; extend it as new
192
- * classes of error earn a stable code. Order matters only in that the first match wins.
193
- */
194
- export const DIAGNOSTIC_CATALOG: readonly CatalogEntry[] = [
195
- {
196
- code: "NIFRA_SERVER_ONLY_IN_CLIENT",
197
- match: (_n, m) => m.includes("server-only module(s) in the client bundle"),
198
- cause:
199
- "A module marked server-only was reachable from a client entry, so it would ship to the browser.",
200
- fix: "Follow the import chain in the message and move the server-only use behind a loader/action or a `*.server.ts` boundary, so it never enters a client component.",
201
- docsAnchor: "errors#server-only-in-client",
202
- },
203
- {
204
- code: "NIFRA_NODE_BUILTIN_IN_CLIENT",
205
- match: (_n, m) => m.includes("Node built-in(s) in the client bundle"),
206
- cause: "A `node:` built-in was reached from a client entry; it has no browser implementation.",
207
- fix: "Move the code using the built-in to the server (loader/action or `*.server.ts`); the message lists the import chain that pulled it in.",
208
- docsAnchor: "errors#node-builtin-in-client",
209
- },
210
- {
211
- code: "NIFRA_SCHEMA_PARSE",
212
- match: (n, m) =>
213
- n === "SchemaError" ||
214
- /failed to (parse|validate)|invalid_type|expected .* received/i.test(m),
215
- cause: "Data crossing a boundary did not match its declared schema.",
216
- fix: "Check the value against the schema at the failing boundary (loader input, search params, or request body); parse-don't-cast means the shape must match exactly.",
217
- docsAnchor: "errors#schema-parse",
218
- },
219
- ]
220
-
221
- /** Classify an error name+message against the catalog; falls back to the generic unhandled code. */
222
- export function classify(
223
- name: string,
224
- message: string,
225
- ): {
226
- code: string
227
- cause?: string
228
- fix?: string
229
- docsAnchor?: string
230
- } {
231
- const hit = DIAGNOSTIC_CATALOG.find((e) => e.match(name, message))
232
- if (hit === undefined) return { code: "NIFRA_UNHANDLED" }
233
- return { code: hit.code, cause: hit.cause, fix: hit.fix, docsAnchor: hit.docsAnchor }
234
- }
235
-
236
199
  /** Split an Error's `stack` into its leading message block and its frame lines. */
237
200
  function messageAndStack(err: Error): { message: string; stack: string } {
238
201
  const stack = err.stack ?? `${err.name}: ${err.message}`
@@ -251,6 +214,37 @@ export interface BuildDiagnosticOptions {
251
214
  readonly root?: string
252
215
  /** Injectable source reader (tests pass a fake; production reads the filesystem). */
253
216
  readonly read?: SourceReader
217
+ /** Whether a file's source may appear in the codeframe, given the path the codeframe would read. The
218
+ * overlay is served to a browser, so a dev server passes the zone check here and backend source never
219
+ * renders in it. */
220
+ readonly showSource?: (file: string) => boolean
221
+ /** A frame's authored position when its file was compiled by a plugin whose map the runtime did not
222
+ * apply (Bun ignores a plugin's inline map), so a compiled line never lands in the codeframe. */
223
+ readonly remap?: (
224
+ file: string,
225
+ line: number,
226
+ column: number,
227
+ ) => { readonly line: number; readonly column: number } | undefined
228
+ }
229
+
230
+ function remapFrames(
231
+ frames: DiagnosticFrame[],
232
+ remap: BuildDiagnosticOptions["remap"],
233
+ ): DiagnosticFrame[] {
234
+ if (remap === undefined) return frames
235
+ return frames.map((frame) => {
236
+ if (frame.file === undefined || frame.line === undefined || frame.column === undefined) {
237
+ return frame
238
+ }
239
+ const authored = remap(frame.file, frame.line, frame.column)
240
+ if (authored === undefined) return frame
241
+ const at = frame.raw.lastIndexOf(`:${frame.line}:${frame.column}`)
242
+ const raw =
243
+ at === -1
244
+ ? frame.raw
245
+ : `${frame.raw.slice(0, at)}:${authored.line}:${authored.column}${frame.raw.slice(at + `:${frame.line}:${frame.column}`.length)}`
246
+ return { raw, file: frame.file, line: authored.line, column: authored.column }
247
+ })
254
248
  }
255
249
 
256
250
  /**
@@ -262,18 +256,20 @@ export function buildDiagnostic(err: unknown, options: BuildDiagnosticOptions =
262
256
  const error = err instanceof Error ? err : new Error(String(err))
263
257
  const root = options.root ?? safeCwd()
264
258
  const { message } = messageAndStack(error)
265
- const frames = parseFrames(error.stack ?? "")
259
+ const frames = remapFrames(parseFrames(error.stack ?? ""), options.remap)
266
260
  const top = topUserFrame(frames, root)
261
+ // The gate sees the path that will be read: a frame's text can come from a browser.
262
+ const source =
263
+ top?.file === undefined
264
+ ? undefined
265
+ : options.read === undefined
266
+ ? canonicalPath(top.file)
267
+ : top.file
267
268
  const codeframe =
268
- top?.file !== undefined && top.line !== undefined
269
- ? buildCodeframe(
270
- options.read === undefined ? canonicalPath(top.file) : top.file,
271
- top.line,
272
- top.column,
273
- options.read,
274
- )
269
+ source !== undefined && top?.line !== undefined && (options.showSource?.(source) ?? true)
270
+ ? buildCodeframe(source, top.line, top.column, options.read)
275
271
  : undefined
276
- const { code, cause, fix, docsAnchor } = classify(error.name || "Error", message)
272
+ const { code, cause, fix, docsAnchor, fixOptions } = classify(error.name || "Error", message)
277
273
  return {
278
274
  code,
279
275
  name: error.name || "Error",
@@ -284,6 +280,7 @@ export function buildDiagnostic(err: unknown, options: BuildDiagnosticOptions =
284
280
  cause,
285
281
  fix,
286
282
  docsAnchor,
283
+ fixOptions,
287
284
  }
288
285
  }
289
286
 
package/src/draft.ts CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  signValue,
22
22
  unsignValue,
23
23
  } from "@nifrajs/core/server"
24
- import { timingSafeEqual } from "./internal/timing-safe-equal.ts"
24
+ import { assertTokenSecret, timingSafeEqual } from "./internal/timing-safe-equal.ts"
25
25
 
26
26
  /** The cookie name nifra uses for draft/preview mode. */
27
27
  export const DRAFT_COOKIE = "__nifra_draft"
@@ -160,6 +160,9 @@ export interface PreviewEndpointOptions {
160
160
  export function previewEndpoint(
161
161
  options: PreviewEndpointOptions,
162
162
  ): (request: Request) => Promise<Response> {
163
+ // An empty token matches a link with no `?token=` at all, which would hand draft mode (and every
164
+ // unpublished page) to anyone. Refuse it at construction, where an unset env var surfaces at boot.
165
+ assertTokenSecret(options.secret, "previewEndpoint")
163
166
  const tokenParam = options.tokenParam ?? "token"
164
167
  const redirectParam = options.redirectParam ?? "to"
165
168
  const fallbackPath = options.fallbackPath ?? "/"
package/src/fn.ts CHANGED
@@ -53,6 +53,7 @@ import {
53
53
  type RouteSchema,
54
54
  type StandardSchemaV1,
55
55
  } from "@nifrajs/core/server"
56
+ import { guardChannel, outputGuard } from "./internal/output-guard.ts"
56
57
 
57
58
  /** The URL prefix every mounted function lives under. Namespaced per mount, then by export name. */
58
59
  export const SERVER_FN_PREFIX = "/_nifra/fn"
@@ -66,6 +67,11 @@ export interface ServerFnConfig<Input> {
66
67
  * the caller controls this value completely, so an unvalidated one is an open door.
67
68
  */
68
69
  readonly input?: StandardSchemaV1<unknown, Input>
70
+ /**
71
+ * Describes what the function returns to the browser. Required whenever it returns data: keys the
72
+ * schema does not declare are dropped, and a declared field of the wrong shape fails the call.
73
+ */
74
+ readonly output?: StandardSchemaV1
69
75
  /** Effect tokens, forwarded to the route so `nifra assure` and the effect ledger see them. */
70
76
  readonly capabilities?: readonly string[]
71
77
  }
@@ -172,6 +178,13 @@ export function serverFunctions(namespace: string, module: ServerFnModule): Iden
172
178
  )
173
179
  }
174
180
  const config = fn[SERVER_FN]
181
+ const label = `server function "${namespace}.${name}"`
182
+ const guarded = guardChannel(fn, {
183
+ label,
184
+ guard:
185
+ config.output === undefined ? undefined : outputGuard(config.output, label, "output"),
186
+ missing: "an output schema in its serverFn({ output }) config",
187
+ })
175
188
  const schema: RouteSchema = {
176
189
  ...(config.input !== undefined ? { body: config.input } : {}),
177
190
  ...(config.capabilities !== undefined ? { capabilities: config.capabilities } : {}),
@@ -182,17 +195,20 @@ export function serverFunctions(namespace: string, module: ServerFnModule): Iden
182
195
  // has happened yet, so rejecting here is a complete refusal.
183
196
  const request = c.req
184
197
  const contentType = request.headers.get("content-type") ?? ""
185
- if (!contentType.includes("application/json")) {
198
+ const semi = contentType.indexOf(";")
199
+ const mediaType = (semi === -1 ? contentType : contentType.slice(0, semi)).trim()
200
+ if (mediaType.toLowerCase() !== "application/json") {
186
201
  // A form cannot send this content type cross-origin without a preflight. Rejecting anything
187
202
  // else is what makes form-driven CSRF against a server function impossible rather than
188
- // merely unlikely.
203
+ // merely unlikely. Compare the media type, never search the header: `text/plain;
204
+ // x=application/json` contains the string but is a no-preflight `text/plain` request.
189
205
  return c.json({ ok: false, error: "unsupported_media_type" }, 415)
190
206
  }
191
207
  const origin = request.headers.get("origin")
192
208
  if (origin !== null && !sameOrigin(origin, request)) {
193
209
  return c.json({ ok: false, error: "forbidden_origin" }, 403)
194
210
  }
195
- return fn(config.input === undefined ? (undefined as never) : (c.body as never), c)
211
+ return guarded(config.input === undefined ? undefined : c.body, c)
196
212
  }) as (context: never) => unknown)
197
213
  }
198
214
  return app
package/src/fs.ts CHANGED
@@ -5,7 +5,13 @@
5
5
  */
6
6
  import { readdirSync } from "node:fs"
7
7
  import { join } from "node:path"
8
- import { buildManifest, type Manifest, type RouteModule } from "./manifest.ts"
8
+ import {
9
+ buildManifest,
10
+ isMiddlewareFile,
11
+ isRouteBackendFile,
12
+ type Manifest,
13
+ type RouteModule,
14
+ } from "./manifest.ts"
9
15
 
10
16
  /** Options for {@link discoverRoutes}. */
11
17
  export interface DiscoverRoutesOptions {
@@ -31,7 +37,12 @@ export interface DiscoverRoutesOptions {
31
37
  export function discoverRoutes(dir: string, options: DiscoverRoutesOptions = {}): Manifest {
32
38
  const files = (readdirSync(dir, { recursive: true }) as string[])
33
39
  .map((file) => file.replaceAll("\\", "/")) // normalize Windows separators
34
- .filter((file) => /\.(tsx|jsx|svelte|vue|mdx)$/.test(file))
40
+ .filter(
41
+ (file) =>
42
+ /\.(tsx|jsx|svelte|vue|mdx)$/.test(file) ||
43
+ isRouteBackendFile(file) ||
44
+ isMiddlewareFile(file),
45
+ )
35
46
  const query = options.importQuery ? `?${options.importQuery}` : ""
36
47
  const load = options.load
37
48
  return buildManifest(files, (file) =>
package/src/index.ts CHANGED
@@ -41,6 +41,20 @@ export {
41
41
  RenderAdapterConformanceError,
42
42
  type RenderAdapterConformanceFixture,
43
43
  } from "./conformance.ts"
44
+ // Hash-based CSP: a document carries a nonce only when it needs one, so a CSP page stays cacheable.
45
+ export {
46
+ type CreateCspPolicyOptions,
47
+ type CspHeaderContext,
48
+ type CspPolicy,
49
+ createCspPolicy,
50
+ nifraScriptHashes,
51
+ } from "./csp.ts"
52
+ export {
53
+ assertCssLoadingCompatible,
54
+ type CssLoadingMode,
55
+ DEFAULT_CSS_LOADING,
56
+ normalizeCssLoading,
57
+ } from "./css-contract.ts"
44
58
  // Deferred loader data (`defer()` + the `Deferred<T>` type) - consumed by the adapter's `<Await>`.
45
59
  export { type Deferred, defer } from "./deferred.ts"
46
60
  export {
@@ -96,42 +110,46 @@ export {
96
110
  unsafeInlineScript,
97
111
  } from "./internal/render-document.ts"
98
112
  export {
113
+ type BackendOnly,
99
114
  DEFAULT_DEV_PORT,
100
115
  PRE_HYDRATION_GUARD,
101
- type ServerOnly,
102
116
  } from "./internal/runtime-contract.ts"
103
117
  /**
104
- * The two file-name conventions that decide what never reaches a browser: a `*.server` module is
105
- * EMPTIED in the client build, a `*.fn` module is REPLACED with client stubs.
106
- *
107
- * Exported because more than the bundlers need them. `nifra dev --bun` cannot transform (Bun's dev
108
- * bundler takes no plugins) so it must REFUSE instead, and a refusal driven by its own hand-written
109
- * glob drifts from the transform - which is how `.fn.mts` came to be stubbed by both build pipelines
110
- * and waved through by the guard that exists to stop it leaking. Anything deciding "is this module
111
- * server-only" should import the matcher rather than re-encode it.
118
+ * The `*.fn` server-function convention: the browser build REPLACES such a module with client stubs.
119
+ * Exported so anything deciding "is this a server-function module" imports the matcher instead of
120
+ * re-encoding it - a hand-written glob drifts from the transform, which is how `.fn.mts` was once
121
+ * stubbed by both build pipelines and waved through by the guard meant to stop it leaking.
112
122
  */
113
- export { SERVER_FN_MODULE, SERVER_ONLY_MODULE } from "./internal/server-boundary.ts"
123
+ export { SERVER_FN_MODULE } from "./internal/server-boundary.ts"
114
124
  export {
115
125
  type CreateWebAppOptions,
116
126
  createWebApp,
127
+ type NonceResolver,
128
+ webProjectEvidence,
117
129
  } from "./internal/web-app.ts"
118
130
  // ISR (incremental static regeneration): a pluggable cache store + the `withISR` stale-while-revalidate
119
131
  // wrapper for rendered SSR responses.
120
132
  export {
121
133
  type CachedResponse,
122
134
  type CacheStore,
135
+ type CdnPurgeOutcome,
136
+ type CdnPurgeTarget,
123
137
  ISR_REVALIDATE_HEADER,
124
138
  ISR_REVALIDATE_TAGS_HEADER,
125
139
  ISR_STATUS_HEADER,
126
140
  type ISRApp,
127
141
  type ISROptions,
128
142
  type ISRPlatform,
143
+ type ISRQuery,
144
+ isCacheablePage,
129
145
  KVCacheStore,
130
146
  type KVCacheStoreOptions,
131
147
  type KVNamespaceLike,
132
148
  MemoryCacheStore,
133
149
  type MemoryCacheStoreOptions,
134
150
  type RevalidateEndpointOptions,
151
+ type RevalidateTags,
152
+ type RevalidateTagsInput,
135
153
  revalidateEndpoint,
136
154
  withISR,
137
155
  } from "./isr.ts"
@@ -155,14 +173,22 @@ export {
155
173
  type LinkDescriptor,
156
174
  type Loader,
157
175
  type LoaderContext,
176
+ type LoaderResponseControls,
177
+ type LoadingEntry,
158
178
  type Manifest,
159
179
  type Meta,
160
180
  type MetaArgs,
161
181
  type MetaDescriptor,
162
182
  type MetaInput,
183
+ type MiddlewareOutcome,
184
+ type NotFoundEntry,
185
+ type NotFoundScope,
163
186
  type RouteEntry,
187
+ type RouteMiddleware,
164
188
  type RouteModule,
165
189
  type ScriptDescriptor,
190
+ type ShouldRevalidate,
191
+ type ShouldRevalidateArgs,
166
192
  type StaticPath,
167
193
  type StaticPaths,
168
194
  type StaticRoutes,
@@ -183,12 +209,21 @@ export {
183
209
  type NavigateOptions,
184
210
  type NavigateSearchOf,
185
211
  type NavigateTargetInput,
212
+ type PrefetchMode,
186
213
  type RouteSearch,
187
214
  registerBlocker,
188
215
  resolveNavigate,
189
216
  setBlockerController,
190
217
  setBrowserNavigate,
191
218
  } from "./navigation.ts"
219
+ export {
220
+ type CreateNonceResolverOptions,
221
+ createNonceResolver,
222
+ type NonceContext,
223
+ type NonceGenerator,
224
+ type NonceHeader,
225
+ type NonceHeaderContext,
226
+ } from "./nonce.ts"
192
227
  // public/ - user-authored static files, served identically in dev and production by one handler.
193
228
  export {
194
229
  type PublicDirCache,
@@ -221,14 +256,18 @@ export {
221
256
  ACTION_GLOBAL,
222
257
  BOUNDARY_GLOBAL,
223
258
  DATA_GLOBAL,
259
+ HANDOVER_ID,
224
260
  LAYOUT_DATA_GLOBAL,
261
+ type MatchChain,
225
262
  type RenderAdapter,
226
263
  type RenderProps,
264
+ type RenderStreamOptions,
227
265
  ROOT_ATTRIBUTE,
228
266
  ROUTE_GLOBAL,
229
267
  type SsrModuleLoader,
230
268
  setSsrModuleLoader,
231
269
  ssrModuleLoader,
270
+ type UIMatch,
232
271
  } from "./render-seam.ts"
233
272
  // Agnostic client-side router core (pure + DOM-free) - consumed by per-adapter Router bindings.
234
273
  // `DATA_HEADER` marks a navigation's data-only GET; `createWebApp` answers it with loader JSON.
@@ -258,3 +297,11 @@ export {
258
297
  // ctx + `renderPage`) and a client adapter mount call this with the same URL + schema, so the two sides
259
298
  // produce the identical value by construction. An adapter's `useSearch` binding reads its result.
260
299
  export { type SearchOf, searchOf, searchOfChain, serializeSearch } from "./search.ts"
300
+ export {
301
+ type HtmlSanitizer,
302
+ type SanitizedHtml,
303
+ sanitizedHtml,
304
+ sanitizeHtml,
305
+ type TrustedHtml,
306
+ trustHtml,
307
+ } from "./trusted-html.ts"
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Cloudflare: a proxied zone in front of the origin, and Workers Cache in front of a Worker. Both read
3
+ * `Cache-Tag` and `Cloudflare-CDN-Cache-Control` and strip them before the browser; the zone purges
4
+ * through the API (at most 100 tags a call; the Free plan allows 5 calls a minute), Workers Cache
5
+ * through the Worker's own `cache.purge` (always Free-plan limits).
6
+ */
7
+ import {
8
+ attemptFromStatus,
9
+ type CdnCacheInput,
10
+ type CdnProvider,
11
+ cdnDirectives,
12
+ defineCdnProvider,
13
+ type PurgeQueueOptions,
14
+ } from "./core.ts"
15
+
16
+ const headersFor = (input: CdnCacheInput): Record<string, string> => ({
17
+ "cache-tag": input.tags.join(","),
18
+ "cloudflare-cdn-cache-control": cdnDirectives(input),
19
+ })
20
+ const NO_STORE = { "cloudflare-cdn-cache-control": "no-store" }
21
+
22
+ export interface CloudflareZoneOptions {
23
+ /** The zone id (32 hex characters, on the zone's Overview page). */
24
+ readonly zoneId: string
25
+ /** An API token scoped to Zone > Cache Purge for this zone. Keep it in an env binding. */
26
+ readonly apiToken: string
27
+ readonly queue?: PurgeQueueOptions
28
+ /** The fetch purge calls go through; tests pass their own. */
29
+ readonly fetch?: typeof fetch
30
+ }
31
+
32
+ /**
33
+ * A Cloudflare zone proxying the origin. HTML is only cached by a Cache Rule that makes it eligible,
34
+ * and the zone's cache ignores `Vary`: the rule must also bypass the cache when the request carries
35
+ * `x-nifra-data` (soft navigations fetch the page URL with that header). `nifra cdn-check` tests both.
36
+ */
37
+ export function cloudflareZone(options: CloudflareZoneOptions): CdnProvider {
38
+ if (!/^[a-f0-9]{32}$/.test(options.zoneId)) {
39
+ throw new TypeError(
40
+ "[nifra/web/cdn] cloudflareZone: zoneId must be the 32-character hex zone id",
41
+ )
42
+ }
43
+ if (typeof options.apiToken !== "string" || options.apiToken === "") {
44
+ throw new TypeError(
45
+ "[nifra/web/cdn] cloudflareZone: apiToken is empty (is the env binding set?)",
46
+ )
47
+ }
48
+ const send = options.fetch ?? fetch
49
+ const endpoint = `https://api.cloudflare.com/client/v4/zones/${options.zoneId}/purge_cache`
50
+ return defineCdnProvider(
51
+ {
52
+ name: "cloudflare",
53
+ tagsPerCall: 100,
54
+ maxResponseTags: 1000,
55
+ cacheHeaders: headersFor,
56
+ noStoreHeaders: () => NO_STORE,
57
+ async purgeTags(tags) {
58
+ const res = await send(endpoint, {
59
+ method: "POST",
60
+ headers: {
61
+ authorization: `Bearer ${options.apiToken}`,
62
+ "content-type": "application/json",
63
+ },
64
+ body: JSON.stringify({ tags }),
65
+ })
66
+ const attempt = attemptFromStatus(res, "cloudflare")
67
+ if (!res.ok) {
68
+ await res.body?.cancel()
69
+ return attempt
70
+ }
71
+ // A 200 can still carry `success: false`; only `true` means the purge was received.
72
+ const body: unknown = await res.json().catch(() => null)
73
+ const field = (from: unknown, name: string): unknown =>
74
+ typeof from === "object" && from !== null ? Reflect.get(from, name) : undefined
75
+ if (field(body, "success") === true) return attempt
76
+ const errors = field(body, "errors")
77
+ const code = field(Array.isArray(errors) ? errors[0] : undefined, "code")
78
+ return {
79
+ ok: false,
80
+ retryable: false,
81
+ reason: typeof code === "number" ? `cloudflare_error_${code}` : "cloudflare_unsuccessful",
82
+ }
83
+ },
84
+ },
85
+ options.queue,
86
+ )
87
+ }
88
+
89
+ /** The Worker's cache binding: `import { cache } from "cloudflare:workers"`, or `ctx.cache`. */
90
+ export interface WorkersCacheLike {
91
+ purge(options: { tags?: string[]; pathPrefixes?: string[] }): Promise<{
92
+ success: boolean
93
+ errors?: readonly { code?: number; message?: string }[]
94
+ }>
95
+ }
96
+
97
+ export interface CloudflareWorkersCacheOptions {
98
+ /** `import { cache } from "cloudflare:workers"` (needs `[cache] enabled = true` in wrangler). */
99
+ readonly cache: WorkersCacheLike
100
+ /**
101
+ * Whether the Worker serves different content per hostname. Workers Cache keys entries by path, not
102
+ * host, so such an app would serve one host's page to another: `true` is refused.
103
+ */
104
+ readonly hostRouted?: boolean
105
+ readonly queue?: PurgeQueueOptions
106
+ }
107
+
108
+ /** Workers Cache in front of the Worker serving the app. Purges are scoped to the calling entrypoint. */
109
+ export function cloudflareWorkersCache(options: CloudflareWorkersCacheOptions): CdnProvider {
110
+ if (options.hostRouted === true) {
111
+ throw new Error(
112
+ "[nifra/web/cdn] NIFRA_CDN_HOST_ROUTED: Workers Cache keys pages by path, not host, so a Worker that serves different content per hostname would serve one host's page to another. Put a Cloudflare zone (cloudflareZone) in front instead, or serve each host from its own Worker.",
113
+ )
114
+ }
115
+ if (typeof options.cache?.purge !== "function") {
116
+ throw new TypeError(
117
+ '[nifra/web/cdn] cloudflareWorkersCache: pass `cache` from `import { cache } from "cloudflare:workers"`',
118
+ )
119
+ }
120
+ return defineCdnProvider(
121
+ {
122
+ name: "cloudflare-workers-cache",
123
+ tagsPerCall: 100,
124
+ maxResponseTags: 1000,
125
+ cacheHeaders: headersFor,
126
+ noStoreHeaders: () => NO_STORE,
127
+ async purgeTags(tags) {
128
+ const result = await options.cache.purge({ tags: [...tags] })
129
+ if (result.success) return { ok: true }
130
+ const first = result.errors?.[0]
131
+ // A rate-limit refusal is the one worth retrying; anything else is configuration.
132
+ const limited = /rate/i.test(first?.message ?? "")
133
+ return {
134
+ ok: false,
135
+ retryable: limited,
136
+ rateLimited: limited,
137
+ reason:
138
+ typeof first?.code === "number"
139
+ ? `workers_cache_${first.code}`
140
+ : "workers_cache_refused",
141
+ }
142
+ },
143
+ },
144
+ options.queue,
145
+ )
146
+ }