@nifrajs/web 3.5.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 (390) 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 +18 -6
  10. package/dist/build-plan.d.ts.map +1 -1
  11. package/dist/build-plan.js +82 -34
  12. package/dist/build-plan.js.map +1 -1
  13. package/dist/build-vite.d.ts +4 -0
  14. package/dist/build-vite.d.ts.map +1 -1
  15. package/dist/build-vite.js +68 -13
  16. package/dist/build-vite.js.map +1 -1
  17. package/dist/build.d.ts +57 -23
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/build.js +356 -85
  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 +3 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +111 -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/dev-client.d.ts +76 -0
  40. package/dist/dev-client.d.ts.map +1 -0
  41. package/dist/dev-client.js +280 -0
  42. package/dist/dev-client.js.map +1 -0
  43. package/dist/dev-diagnostics.d.ts +26 -2
  44. package/dist/dev-diagnostics.d.ts.map +1 -1
  45. package/dist/dev-diagnostics.js +38 -7
  46. package/dist/dev-diagnostics.js.map +1 -1
  47. package/dist/dev-error.d.ts +4 -13
  48. package/dist/dev-error.d.ts.map +1 -1
  49. package/dist/dev-error.js +46 -4
  50. package/dist/dev-error.js.map +1 -1
  51. package/dist/dev-feed.d.ts +298 -0
  52. package/dist/dev-feed.d.ts.map +1 -0
  53. package/dist/dev-feed.js +797 -0
  54. package/dist/dev-feed.js.map +1 -0
  55. package/dist/dev-indicator.d.ts +38 -0
  56. package/dist/dev-indicator.d.ts.map +1 -0
  57. package/dist/dev-indicator.js +93 -0
  58. package/dist/dev-indicator.js.map +1 -0
  59. package/dist/dev-session.d.ts +77 -0
  60. package/dist/dev-session.d.ts.map +1 -0
  61. package/dist/dev-session.js +580 -0
  62. package/dist/dev-session.js.map +1 -0
  63. package/dist/dev-sourcemap.d.ts +27 -0
  64. package/dist/dev-sourcemap.d.ts.map +1 -0
  65. package/dist/dev-sourcemap.js +235 -0
  66. package/dist/dev-sourcemap.js.map +1 -0
  67. package/dist/dev-ssr-graph.d.ts.map +1 -1
  68. package/dist/dev-ssr-graph.js +13 -5
  69. package/dist/dev-ssr-graph.js.map +1 -1
  70. package/dist/dev.d.ts +15 -2
  71. package/dist/dev.d.ts.map +1 -1
  72. package/dist/dev.js +69 -44
  73. package/dist/dev.js.map +1 -1
  74. package/dist/diagnostic-catalog.d.ts +35 -0
  75. package/dist/diagnostic-catalog.d.ts.map +1 -0
  76. package/dist/diagnostic-catalog.js +203 -0
  77. package/dist/diagnostic-catalog.js.map +1 -0
  78. package/dist/diagnostic-prompt.d.ts +49 -0
  79. package/dist/diagnostic-prompt.d.ts.map +1 -0
  80. package/dist/diagnostic-prompt.js +303 -0
  81. package/dist/diagnostic-prompt.js.map +1 -0
  82. package/dist/diagnostic.d.ts +17 -23
  83. package/dist/diagnostic.d.ts.map +1 -1
  84. package/dist/diagnostic.js +31 -39
  85. package/dist/diagnostic.js.map +1 -1
  86. package/dist/draft.d.ts.map +1 -1
  87. package/dist/draft.js +4 -1
  88. package/dist/draft.js.map +1 -1
  89. package/dist/fn.d.ts +5 -0
  90. package/dist/fn.d.ts.map +1 -1
  91. package/dist/fn.js +13 -3
  92. package/dist/fn.js.map +1 -1
  93. package/dist/fs.d.ts.map +1 -1
  94. package/dist/fs.js +4 -2
  95. package/dist/fs.js.map +1 -1
  96. package/dist/index.d.ts +13 -15
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +11 -12
  99. package/dist/index.js.map +1 -1
  100. package/dist/internal/cdn/cloudflare.d.ts +48 -0
  101. package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
  102. package/dist/internal/cdn/cloudflare.js +94 -0
  103. package/dist/internal/cdn/cloudflare.js.map +1 -0
  104. package/dist/internal/cdn/core.d.ts +91 -0
  105. package/dist/internal/cdn/core.d.ts.map +1 -0
  106. package/dist/internal/cdn/core.js +220 -0
  107. package/dist/internal/cdn/core.js.map +1 -0
  108. package/dist/internal/cdn/fastly.d.ts +17 -0
  109. package/dist/internal/cdn/fastly.d.ts.map +1 -0
  110. package/dist/internal/cdn/fastly.js +43 -0
  111. package/dist/internal/cdn/fastly.js.map +1 -0
  112. package/dist/internal/cdn/vercel.d.ts +31 -0
  113. package/dist/internal/cdn/vercel.d.ts.map +1 -0
  114. package/dist/internal/cdn/vercel.js +56 -0
  115. package/dist/internal/cdn/vercel.js.map +1 -0
  116. package/dist/internal/code-unit-order.d.ts +4 -0
  117. package/dist/internal/code-unit-order.d.ts.map +1 -0
  118. package/dist/internal/code-unit-order.js +6 -0
  119. package/dist/internal/code-unit-order.js.map +1 -0
  120. package/dist/internal/codegen.d.ts.map +1 -1
  121. package/dist/internal/codegen.js +141 -25
  122. package/dist/internal/codegen.js.map +1 -1
  123. package/dist/internal/dev-csp.d.ts +24 -0
  124. package/dist/internal/dev-csp.d.ts.map +1 -0
  125. package/dist/internal/dev-csp.js +160 -0
  126. package/dist/internal/dev-csp.js.map +1 -0
  127. package/dist/internal/dev-reserved.d.ts +25 -0
  128. package/dist/internal/dev-reserved.d.ts.map +1 -0
  129. package/dist/internal/dev-reserved.js +39 -0
  130. package/dist/internal/dev-reserved.js.map +1 -0
  131. package/dist/internal/html-spans.d.ts +10 -0
  132. package/dist/internal/html-spans.d.ts.map +1 -0
  133. package/dist/internal/html-spans.js +46 -0
  134. package/dist/internal/html-spans.js.map +1 -0
  135. package/dist/internal/loading-runtime.d.ts +44 -0
  136. package/dist/internal/loading-runtime.d.ts.map +1 -0
  137. package/dist/internal/loading-runtime.js +206 -0
  138. package/dist/internal/loading-runtime.js.map +1 -0
  139. package/dist/internal/matches-runtime.d.ts +8 -0
  140. package/dist/internal/matches-runtime.d.ts.map +1 -0
  141. package/dist/internal/matches-runtime.js +61 -0
  142. package/dist/internal/matches-runtime.js.map +1 -0
  143. package/dist/internal/mount-shadow.d.ts +36 -0
  144. package/dist/internal/mount-shadow.d.ts.map +1 -0
  145. package/dist/internal/mount-shadow.js +65 -0
  146. package/dist/internal/mount-shadow.js.map +1 -0
  147. package/dist/internal/node-builtins.d.ts +3 -0
  148. package/dist/internal/node-builtins.d.ts.map +1 -0
  149. package/dist/internal/node-builtins.js +79 -0
  150. package/dist/internal/node-builtins.js.map +1 -0
  151. package/dist/internal/output-guard.d.ts +65 -0
  152. package/dist/internal/output-guard.d.ts.map +1 -0
  153. package/dist/internal/output-guard.js +630 -0
  154. package/dist/internal/output-guard.js.map +1 -0
  155. package/dist/internal/page-execution.d.ts +20 -8
  156. package/dist/internal/page-execution.d.ts.map +1 -1
  157. package/dist/internal/page-execution.js +571 -105
  158. package/dist/internal/page-execution.js.map +1 -1
  159. package/dist/internal/parity.d.ts +21 -0
  160. package/dist/internal/parity.d.ts.map +1 -1
  161. package/dist/internal/parity.js +104 -22
  162. package/dist/internal/parity.js.map +1 -1
  163. package/dist/internal/private-env.d.ts +12 -0
  164. package/dist/internal/private-env.d.ts.map +1 -0
  165. package/dist/internal/private-env.js +335 -0
  166. package/dist/internal/private-env.js.map +1 -0
  167. package/dist/internal/public-url.d.ts +3 -0
  168. package/dist/internal/public-url.d.ts.map +1 -0
  169. package/dist/internal/public-url.js +18 -0
  170. package/dist/internal/public-url.js.map +1 -0
  171. package/dist/internal/render-document.d.ts +25 -19
  172. package/dist/internal/render-document.d.ts.map +1 -1
  173. package/dist/internal/render-document.js +77 -61
  174. package/dist/internal/render-document.js.map +1 -1
  175. package/dist/internal/response-controls.d.ts +75 -0
  176. package/dist/internal/response-controls.d.ts.map +1 -0
  177. package/dist/internal/response-controls.js +256 -0
  178. package/dist/internal/response-controls.js.map +1 -0
  179. package/dist/internal/runtime-contract.d.ts +11 -16
  180. package/dist/internal/runtime-contract.d.ts.map +1 -1
  181. package/dist/internal/runtime-contract.js.map +1 -1
  182. package/dist/internal/secret-scan.d.ts +104 -0
  183. package/dist/internal/secret-scan.d.ts.map +1 -0
  184. package/dist/internal/secret-scan.js +485 -0
  185. package/dist/internal/secret-scan.js.map +1 -0
  186. package/dist/internal/server-boundary.d.ts +6 -29
  187. package/dist/internal/server-boundary.d.ts.map +1 -1
  188. package/dist/internal/server-boundary.js +6 -66
  189. package/dist/internal/server-boundary.js.map +1 -1
  190. package/dist/internal/source-map.d.ts +47 -0
  191. package/dist/internal/source-map.d.ts.map +1 -0
  192. package/dist/internal/source-map.js +225 -0
  193. package/dist/internal/source-map.js.map +1 -0
  194. package/dist/internal/target-compat.d.ts +27 -0
  195. package/dist/internal/target-compat.d.ts.map +1 -0
  196. package/dist/internal/target-compat.js +77 -0
  197. package/dist/internal/target-compat.js.map +1 -0
  198. package/dist/internal/timing-safe-equal.d.ts +6 -0
  199. package/dist/internal/timing-safe-equal.d.ts.map +1 -1
  200. package/dist/internal/timing-safe-equal.js +10 -0
  201. package/dist/internal/timing-safe-equal.js.map +1 -1
  202. package/dist/internal/vitals-runtime.d.ts +18 -0
  203. package/dist/internal/vitals-runtime.d.ts.map +1 -0
  204. package/dist/internal/vitals-runtime.js +38 -0
  205. package/dist/internal/vitals-runtime.js.map +1 -0
  206. package/dist/internal/web-app.d.ts +38 -9
  207. package/dist/internal/web-app.d.ts.map +1 -1
  208. package/dist/internal/web-app.js +199 -59
  209. package/dist/internal/web-app.js.map +1 -1
  210. package/dist/internal/zone-graph.d.ts +89 -0
  211. package/dist/internal/zone-graph.d.ts.map +1 -0
  212. package/dist/internal/zone-graph.js +492 -0
  213. package/dist/internal/zone-graph.js.map +1 -0
  214. package/dist/isr.d.ts +83 -6
  215. package/dist/isr.d.ts.map +1 -1
  216. package/dist/isr.js +263 -36
  217. package/dist/isr.js.map +1 -1
  218. package/dist/llms-txt.d.ts.map +1 -1
  219. package/dist/llms-txt.js +31 -6
  220. package/dist/llms-txt.js.map +1 -1
  221. package/dist/manifest.d.ts +220 -13
  222. package/dist/manifest.d.ts.map +1 -1
  223. package/dist/manifest.js +386 -9
  224. package/dist/manifest.js.map +1 -1
  225. package/dist/module-graph.d.ts +26 -5
  226. package/dist/module-graph.d.ts.map +1 -1
  227. package/dist/module-graph.js +17 -7
  228. package/dist/module-graph.js.map +1 -1
  229. package/dist/navigation.d.ts +17 -3
  230. package/dist/navigation.d.ts.map +1 -1
  231. package/dist/navigation.js +11 -5
  232. package/dist/navigation.js.map +1 -1
  233. package/dist/nonce.d.ts +47 -0
  234. package/dist/nonce.d.ts.map +1 -0
  235. package/dist/nonce.js +63 -0
  236. package/dist/nonce.js.map +1 -0
  237. package/dist/plugins/css-modules.d.ts.map +1 -1
  238. package/dist/plugins/css-modules.js +4 -3
  239. package/dist/plugins/css-modules.js.map +1 -1
  240. package/dist/plugins/kit.d.ts +9 -0
  241. package/dist/plugins/kit.d.ts.map +1 -1
  242. package/dist/plugins/kit.js +18 -0
  243. package/dist/plugins/kit.js.map +1 -1
  244. package/dist/plugins/svg.d.ts +22 -3
  245. package/dist/plugins/svg.d.ts.map +1 -1
  246. package/dist/plugins/svg.js +112 -1
  247. package/dist/plugins/svg.js.map +1 -1
  248. package/dist/plugins/vite-leak-guard.d.ts +107 -3
  249. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  250. package/dist/plugins/vite-leak-guard.js +378 -17
  251. package/dist/plugins/vite-leak-guard.js.map +1 -1
  252. package/dist/plugins/vite-zone-guard.d.ts +61 -0
  253. package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
  254. package/dist/plugins/vite-zone-guard.js +208 -0
  255. package/dist/plugins/vite-zone-guard.js.map +1 -0
  256. package/dist/plugins/zone-guard.d.ts +27 -0
  257. package/dist/plugins/zone-guard.d.ts.map +1 -0
  258. package/dist/plugins/zone-guard.js +72 -0
  259. package/dist/plugins/zone-guard.js.map +1 -0
  260. package/dist/prerender.d.ts +10 -0
  261. package/dist/prerender.d.ts.map +1 -1
  262. package/dist/prerender.js +35 -3
  263. package/dist/prerender.js.map +1 -1
  264. package/dist/public-dir.d.ts +9 -0
  265. package/dist/public-dir.d.ts.map +1 -1
  266. package/dist/public-dir.js +67 -9
  267. package/dist/public-dir.js.map +1 -1
  268. package/dist/pwa-manifest.d.ts +68 -0
  269. package/dist/pwa-manifest.d.ts.map +1 -0
  270. package/dist/pwa-manifest.js +178 -0
  271. package/dist/pwa-manifest.js.map +1 -0
  272. package/dist/query.js +4 -4
  273. package/dist/query.js.map +1 -1
  274. package/dist/render-seam.d.ts +42 -2
  275. package/dist/render-seam.d.ts.map +1 -1
  276. package/dist/render-seam.js +6 -0
  277. package/dist/render-seam.js.map +1 -1
  278. package/dist/route-manifest.d.ts +3 -0
  279. package/dist/route-manifest.d.ts.map +1 -1
  280. package/dist/route-manifest.js +26 -3
  281. package/dist/route-manifest.js.map +1 -1
  282. package/dist/route-types.d.ts +24 -0
  283. package/dist/route-types.d.ts.map +1 -0
  284. package/dist/route-types.js +195 -0
  285. package/dist/route-types.js.map +1 -0
  286. package/dist/router.d.ts +28 -6
  287. package/dist/router.d.ts.map +1 -1
  288. package/dist/router.js +129 -40
  289. package/dist/router.js.map +1 -1
  290. package/dist/vitals.d.ts +33 -0
  291. package/dist/vitals.d.ts.map +1 -0
  292. package/dist/vitals.js +16 -0
  293. package/dist/vitals.js.map +1 -0
  294. package/dist/vite.d.ts +16 -1
  295. package/dist/vite.d.ts.map +1 -1
  296. package/dist/vite.js +187 -36
  297. package/dist/vite.js.map +1 -1
  298. package/dist/zones.d.ts +70 -0
  299. package/dist/zones.d.ts.map +1 -0
  300. package/dist/zones.js +365 -0
  301. package/dist/zones.js.map +1 -0
  302. package/package.json +67 -14
  303. package/src/backend-only.ts +16 -0
  304. package/src/boundary.ts +18 -5
  305. package/src/build-plan.ts +90 -35
  306. package/src/build-vite.ts +77 -11
  307. package/src/build.ts +466 -96
  308. package/src/bun-dev-entry.ts +1 -1
  309. package/src/cdn.ts +216 -0
  310. package/src/client.ts +111 -18
  311. package/src/conformance.ts +16 -3
  312. package/src/csp.ts +194 -0
  313. package/src/dev-client.ts +349 -0
  314. package/src/dev-diagnostics.ts +60 -7
  315. package/src/dev-error.ts +49 -5
  316. package/src/dev-feed.ts +1140 -0
  317. package/src/dev-indicator.ts +113 -0
  318. package/src/dev-session.ts +729 -0
  319. package/src/dev-sourcemap.ts +281 -0
  320. package/src/dev-ssr-graph.ts +16 -5
  321. package/src/dev.ts +86 -43
  322. package/src/diagnostic-catalog.ts +243 -0
  323. package/src/diagnostic-prompt.ts +355 -0
  324. package/src/diagnostic.ts +63 -66
  325. package/src/draft.ts +4 -1
  326. package/src/fn.ts +19 -3
  327. package/src/fs.ts +13 -2
  328. package/src/index.ts +42 -10
  329. package/src/internal/cdn/cloudflare.ts +146 -0
  330. package/src/internal/cdn/core.ts +316 -0
  331. package/src/internal/cdn/fastly.ts +62 -0
  332. package/src/internal/cdn/vercel.ts +95 -0
  333. package/src/internal/code-unit-order.ts +5 -0
  334. package/src/internal/codegen.ts +161 -24
  335. package/src/internal/dev-csp.ts +176 -0
  336. package/src/internal/dev-reserved.ts +52 -0
  337. package/src/internal/html-spans.ts +50 -0
  338. package/src/internal/loading-runtime.ts +244 -0
  339. package/src/internal/matches-runtime.ts +62 -0
  340. package/src/internal/mount-shadow.ts +88 -0
  341. package/src/internal/node-builtins.ts +79 -0
  342. package/src/internal/output-guard.ts +701 -0
  343. package/src/internal/page-execution.ts +795 -185
  344. package/src/internal/parity.ts +150 -36
  345. package/src/internal/private-env.ts +334 -0
  346. package/src/internal/public-url.ts +22 -0
  347. package/src/internal/render-document.ts +114 -67
  348. package/src/internal/response-controls.ts +284 -0
  349. package/src/internal/runtime-contract.ts +10 -15
  350. package/src/internal/secret-scan.ts +623 -0
  351. package/src/internal/server-boundary.ts +6 -69
  352. package/src/internal/source-map.ts +256 -0
  353. package/src/internal/target-compat.ts +92 -0
  354. package/src/internal/timing-safe-equal.ts +13 -0
  355. package/src/internal/vitals-runtime.ts +53 -0
  356. package/src/internal/web-app.ts +262 -84
  357. package/src/internal/zone-graph.ts +593 -0
  358. package/src/isr.ts +325 -37
  359. package/src/llms-txt.ts +28 -4
  360. package/src/manifest.ts +670 -16
  361. package/src/module-graph.ts +53 -9
  362. package/src/navigation.ts +29 -6
  363. package/src/nonce.ts +104 -0
  364. package/src/plugins/css-modules.ts +5 -3
  365. package/src/plugins/kit.ts +24 -0
  366. package/src/plugins/svg.ts +135 -4
  367. package/src/plugins/vite-leak-guard.ts +529 -16
  368. package/src/plugins/vite-zone-guard.ts +263 -0
  369. package/src/plugins/zone-guard.ts +110 -0
  370. package/src/prerender.ts +49 -3
  371. package/src/public-dir.ts +69 -8
  372. package/src/pwa-manifest.ts +226 -0
  373. package/src/query.ts +4 -4
  374. package/src/render-seam.ts +46 -1
  375. package/src/route-manifest.ts +15 -3
  376. package/src/route-types.ts +264 -0
  377. package/src/router.ts +163 -63
  378. package/src/vitals.ts +44 -0
  379. package/src/vite.ts +207 -36
  380. package/src/zones.ts +430 -0
  381. package/dist/plugins/vite-server-only.d.ts +0 -49
  382. package/dist/plugins/vite-server-only.d.ts.map +0 -1
  383. package/dist/plugins/vite-server-only.js +0 -57
  384. package/dist/plugins/vite-server-only.js.map +0 -1
  385. package/dist/server-only.d.ts +0 -21
  386. package/dist/server-only.d.ts.map +0 -1
  387. package/dist/server-only.js +0 -25
  388. package/dist/server-only.js.map +0 -1
  389. package/src/plugins/vite-server-only.ts +0 -75
  390. 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,14 @@ 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"
44
52
  export {
45
53
  assertCssLoadingCompatible,
46
54
  type CssLoadingMode,
@@ -102,43 +110,46 @@ export {
102
110
  unsafeInlineScript,
103
111
  } from "./internal/render-document.ts"
104
112
  export {
113
+ type BackendOnly,
105
114
  DEFAULT_DEV_PORT,
106
115
  PRE_HYDRATION_GUARD,
107
- type ServerOnly,
108
116
  } from "./internal/runtime-contract.ts"
109
117
  /**
110
- * The two file-name conventions that decide what never reaches a browser: a `*.server` module is
111
- * EMPTIED in the client build, a `*.fn` module is REPLACED with client stubs.
112
- *
113
- * Exported because more than the bundlers need them. `nifra dev --bun` cannot transform (Bun's dev
114
- * bundler takes no plugins) so it must REFUSE instead, and a refusal driven by its own hand-written
115
- * glob drifts from the transform - which is how `.fn.mts` came to be stubbed by both build pipelines
116
- * and waved through by the guard that exists to stop it leaking. Anything deciding "is this module
117
- * 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.
118
122
  */
119
- export { SERVER_FN_MODULE, SERVER_ONLY_MODULE } from "./internal/server-boundary.ts"
123
+ export { SERVER_FN_MODULE } from "./internal/server-boundary.ts"
120
124
  export {
121
125
  type CreateWebAppOptions,
122
126
  createWebApp,
123
127
  type NonceResolver,
128
+ webProjectEvidence,
124
129
  } from "./internal/web-app.ts"
125
130
  // ISR (incremental static regeneration): a pluggable cache store + the `withISR` stale-while-revalidate
126
131
  // wrapper for rendered SSR responses.
127
132
  export {
128
133
  type CachedResponse,
129
134
  type CacheStore,
135
+ type CdnPurgeOutcome,
136
+ type CdnPurgeTarget,
130
137
  ISR_REVALIDATE_HEADER,
131
138
  ISR_REVALIDATE_TAGS_HEADER,
132
139
  ISR_STATUS_HEADER,
133
140
  type ISRApp,
134
141
  type ISROptions,
135
142
  type ISRPlatform,
143
+ type ISRQuery,
144
+ isCacheablePage,
136
145
  KVCacheStore,
137
146
  type KVCacheStoreOptions,
138
147
  type KVNamespaceLike,
139
148
  MemoryCacheStore,
140
149
  type MemoryCacheStoreOptions,
141
150
  type RevalidateEndpointOptions,
151
+ type RevalidateTags,
152
+ type RevalidateTagsInput,
142
153
  revalidateEndpoint,
143
154
  withISR,
144
155
  } from "./isr.ts"
@@ -162,14 +173,22 @@ export {
162
173
  type LinkDescriptor,
163
174
  type Loader,
164
175
  type LoaderContext,
176
+ type LoaderResponseControls,
177
+ type LoadingEntry,
165
178
  type Manifest,
166
179
  type Meta,
167
180
  type MetaArgs,
168
181
  type MetaDescriptor,
169
182
  type MetaInput,
183
+ type MiddlewareOutcome,
184
+ type NotFoundEntry,
185
+ type NotFoundScope,
170
186
  type RouteEntry,
187
+ type RouteMiddleware,
171
188
  type RouteModule,
172
189
  type ScriptDescriptor,
190
+ type ShouldRevalidate,
191
+ type ShouldRevalidateArgs,
173
192
  type StaticPath,
174
193
  type StaticPaths,
175
194
  type StaticRoutes,
@@ -190,12 +209,21 @@ export {
190
209
  type NavigateOptions,
191
210
  type NavigateSearchOf,
192
211
  type NavigateTargetInput,
212
+ type PrefetchMode,
193
213
  type RouteSearch,
194
214
  registerBlocker,
195
215
  resolveNavigate,
196
216
  setBlockerController,
197
217
  setBrowserNavigate,
198
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"
199
227
  // public/ - user-authored static files, served identically in dev and production by one handler.
200
228
  export {
201
229
  type PublicDirCache,
@@ -228,14 +256,18 @@ export {
228
256
  ACTION_GLOBAL,
229
257
  BOUNDARY_GLOBAL,
230
258
  DATA_GLOBAL,
259
+ HANDOVER_ID,
231
260
  LAYOUT_DATA_GLOBAL,
261
+ type MatchChain,
232
262
  type RenderAdapter,
233
263
  type RenderProps,
264
+ type RenderStreamOptions,
234
265
  ROOT_ATTRIBUTE,
235
266
  ROUTE_GLOBAL,
236
267
  type SsrModuleLoader,
237
268
  setSsrModuleLoader,
238
269
  ssrModuleLoader,
270
+ type UIMatch,
239
271
  } from "./render-seam.ts"
240
272
  // Agnostic client-side router core (pure + DOM-free) - consumed by per-adapter Router bindings.
241
273
  // `DATA_HEADER` marks a navigation's data-only GET; `createWebApp` answers it with loader JSON.
@@ -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
+ }