@nifrajs/web 4.0.2 → 5.1.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 (426) hide show
  1. package/README.md +19 -0
  2. package/dist/build-vite.d.ts +19 -0
  3. package/dist/build-vite.d.ts.map +1 -1
  4. package/dist/build-vite.js +31 -10
  5. package/dist/build-vite.js.map +1 -1
  6. package/dist/build.d.ts +8 -10
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +90 -31
  9. package/dist/build.js.map +1 -1
  10. package/dist/cdn.d.ts +8 -0
  11. package/dist/cdn.d.ts.map +1 -1
  12. package/dist/cdn.js +14 -15
  13. package/dist/cdn.js.map +1 -1
  14. package/dist/client.d.ts +6 -6
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +46 -18
  17. package/dist/client.js.map +1 -1
  18. package/dist/deferred.d.ts +2 -2
  19. package/dist/deferred.d.ts.map +1 -1
  20. package/dist/deferred.js +28 -7
  21. package/dist/deferred.js.map +1 -1
  22. package/dist/dev-feed.d.ts.map +1 -1
  23. package/dist/dev-feed.js +24 -5
  24. package/dist/dev-feed.js.map +1 -1
  25. package/dist/dev-port.d.ts +3 -2
  26. package/dist/dev-port.d.ts.map +1 -1
  27. package/dist/dev-port.js +2 -6
  28. package/dist/dev-port.js.map +1 -1
  29. package/dist/dev-session.d.ts.map +1 -1
  30. package/dist/dev-session.js +4 -14
  31. package/dist/dev-session.js.map +1 -1
  32. package/dist/dev.d.ts +7 -17
  33. package/dist/dev.d.ts.map +1 -1
  34. package/dist/dev.js +13 -1
  35. package/dist/dev.js.map +1 -1
  36. package/dist/draft.d.ts +8 -5
  37. package/dist/draft.d.ts.map +1 -1
  38. package/dist/draft.js +30 -13
  39. package/dist/draft.js.map +1 -1
  40. package/dist/fonts-google.d.ts.map +1 -1
  41. package/dist/fonts-google.js +7 -0
  42. package/dist/fonts-google.js.map +1 -1
  43. package/dist/index.d.ts +5 -4
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +4 -3
  46. package/dist/index.js.map +1 -1
  47. package/dist/internal/codegen.d.ts +1 -0
  48. package/dist/internal/codegen.d.ts.map +1 -1
  49. package/dist/internal/codegen.js +56 -29
  50. package/dist/internal/codegen.js.map +1 -1
  51. package/dist/internal/disposable-dir.d.ts +8 -0
  52. package/dist/internal/disposable-dir.d.ts.map +1 -0
  53. package/dist/internal/disposable-dir.js +25 -0
  54. package/dist/internal/disposable-dir.js.map +1 -0
  55. package/dist/internal/entry-chunks.d.ts +9 -0
  56. package/dist/internal/entry-chunks.d.ts.map +1 -0
  57. package/dist/internal/entry-chunks.js +15 -0
  58. package/dist/internal/entry-chunks.js.map +1 -0
  59. package/dist/internal/evidence-traversal.d.ts +12 -0
  60. package/dist/internal/evidence-traversal.d.ts.map +1 -0
  61. package/dist/internal/evidence-traversal.js +48 -0
  62. package/dist/internal/evidence-traversal.js.map +1 -0
  63. package/dist/internal/head-merge.d.ts +6 -0
  64. package/dist/internal/head-merge.d.ts.map +1 -1
  65. package/dist/internal/head-merge.js +9 -0
  66. package/dist/internal/head-merge.js.map +1 -1
  67. package/dist/internal/js-string.d.ts +2 -0
  68. package/dist/internal/js-string.d.ts.map +1 -1
  69. package/dist/internal/js-string.js +5 -1
  70. package/dist/internal/js-string.js.map +1 -1
  71. package/dist/internal/mount-shadow.js +1 -1
  72. package/dist/internal/mount-shadow.js.map +1 -1
  73. package/dist/internal/output-guard.d.ts +7 -0
  74. package/dist/internal/output-guard.d.ts.map +1 -1
  75. package/dist/internal/output-guard.js +80 -8
  76. package/dist/internal/output-guard.js.map +1 -1
  77. package/dist/internal/package-entries.d.ts +34 -0
  78. package/dist/internal/package-entries.d.ts.map +1 -0
  79. package/dist/internal/package-entries.js +324 -0
  80. package/dist/internal/package-entries.js.map +1 -0
  81. package/dist/internal/page-execution.d.ts +6 -0
  82. package/dist/internal/page-execution.d.ts.map +1 -1
  83. package/dist/internal/page-execution.js +437 -114
  84. package/dist/internal/page-execution.js.map +1 -1
  85. package/dist/internal/parity.d.ts.map +1 -1
  86. package/dist/internal/parity.js +15 -2
  87. package/dist/internal/parity.js.map +1 -1
  88. package/dist/internal/private-env.d.ts +8 -0
  89. package/dist/internal/private-env.d.ts.map +1 -1
  90. package/dist/internal/private-env.js +25 -10
  91. package/dist/internal/private-env.js.map +1 -1
  92. package/dist/internal/progressive-cookies.d.ts +14 -0
  93. package/dist/internal/progressive-cookies.d.ts.map +1 -0
  94. package/dist/internal/progressive-cookies.js +43 -0
  95. package/dist/internal/progressive-cookies.js.map +1 -0
  96. package/dist/internal/public-env.d.ts +2 -0
  97. package/dist/internal/public-env.d.ts.map +1 -0
  98. package/dist/internal/public-env.js +13 -0
  99. package/dist/internal/public-env.js.map +1 -0
  100. package/dist/internal/render-document.d.ts +5 -44
  101. package/dist/internal/render-document.d.ts.map +1 -1
  102. package/dist/internal/render-document.js +51 -63
  103. package/dist/internal/render-document.js.map +1 -1
  104. package/dist/internal/response-controls.d.ts +2 -0
  105. package/dist/internal/response-controls.d.ts.map +1 -1
  106. package/dist/internal/response-controls.js +26 -16
  107. package/dist/internal/response-controls.js.map +1 -1
  108. package/dist/internal/revive-dates.d.ts +16 -0
  109. package/dist/internal/revive-dates.d.ts.map +1 -0
  110. package/dist/internal/revive-dates.js +32 -0
  111. package/dist/internal/revive-dates.js.map +1 -0
  112. package/dist/internal/route-path.d.ts +14 -0
  113. package/dist/internal/route-path.d.ts.map +1 -0
  114. package/dist/internal/route-path.js +19 -0
  115. package/dist/internal/route-path.js.map +1 -0
  116. package/dist/internal/secret-scan.d.ts.map +1 -1
  117. package/dist/internal/secret-scan.js +190 -20
  118. package/dist/internal/secret-scan.js.map +1 -1
  119. package/dist/internal/server-boundary.d.ts.map +1 -1
  120. package/dist/internal/server-boundary.js +33 -5
  121. package/dist/internal/server-boundary.js.map +1 -1
  122. package/dist/internal/web-app.d.ts +25 -18
  123. package/dist/internal/web-app.d.ts.map +1 -1
  124. package/dist/internal/web-app.js +81 -24
  125. package/dist/internal/web-app.js.map +1 -1
  126. package/dist/internal/wire-date.d.ts +3 -0
  127. package/dist/internal/wire-date.d.ts.map +1 -0
  128. package/dist/internal/wire-date.js +14 -0
  129. package/dist/internal/wire-date.js.map +1 -0
  130. package/dist/internal/without-query.d.ts +4 -0
  131. package/dist/internal/without-query.d.ts.map +1 -0
  132. package/dist/internal/without-query.js +9 -0
  133. package/dist/internal/without-query.js.map +1 -0
  134. package/dist/internal/zone-graph.d.ts +4 -0
  135. package/dist/internal/zone-graph.d.ts.map +1 -1
  136. package/dist/internal/zone-graph.js +71 -20
  137. package/dist/internal/zone-graph.js.map +1 -1
  138. package/dist/isr.d.ts +59 -3
  139. package/dist/isr.d.ts.map +1 -1
  140. package/dist/isr.js +311 -41
  141. package/dist/isr.js.map +1 -1
  142. package/dist/llms-txt.d.ts +2 -0
  143. package/dist/llms-txt.d.ts.map +1 -1
  144. package/dist/llms-txt.js +29 -3
  145. package/dist/llms-txt.js.map +1 -1
  146. package/dist/manifest.d.ts +37 -4
  147. package/dist/manifest.d.ts.map +1 -1
  148. package/dist/manifest.js +4 -0
  149. package/dist/manifest.js.map +1 -1
  150. package/dist/open-resolved.d.ts +18 -0
  151. package/dist/open-resolved.d.ts.map +1 -0
  152. package/dist/open-resolved.js +62 -0
  153. package/dist/open-resolved.js.map +1 -0
  154. package/dist/plugins/kit.d.ts +1 -0
  155. package/dist/plugins/kit.d.ts.map +1 -1
  156. package/dist/plugins/kit.js +26 -11
  157. package/dist/plugins/kit.js.map +1 -1
  158. package/dist/plugins/vite-leak-guard.d.ts +9 -1
  159. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  160. package/dist/plugins/vite-leak-guard.js +84 -7
  161. package/dist/plugins/vite-leak-guard.js.map +1 -1
  162. package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
  163. package/dist/plugins/vite-zone-guard.js +17 -21
  164. package/dist/plugins/vite-zone-guard.js.map +1 -1
  165. package/dist/plugins/zone-guard.d.ts.map +1 -1
  166. package/dist/plugins/zone-guard.js +44 -3
  167. package/dist/plugins/zone-guard.js.map +1 -1
  168. package/dist/prerender.d.ts +5 -1
  169. package/dist/prerender.d.ts.map +1 -1
  170. package/dist/prerender.js +41 -19
  171. package/dist/prerender.js.map +1 -1
  172. package/dist/public-dir.d.ts +8 -1
  173. package/dist/public-dir.d.ts.map +1 -1
  174. package/dist/public-dir.js +17 -6
  175. package/dist/public-dir.js.map +1 -1
  176. package/dist/query.d.ts +13 -4
  177. package/dist/query.d.ts.map +1 -1
  178. package/dist/query.js +89 -26
  179. package/dist/query.js.map +1 -1
  180. package/dist/render-seam.d.ts +25 -2
  181. package/dist/render-seam.d.ts.map +1 -1
  182. package/dist/render-seam.js +1 -0
  183. package/dist/render-seam.js.map +1 -1
  184. package/dist/route-types.d.ts.map +1 -1
  185. package/dist/route-types.js +20 -9
  186. package/dist/route-types.js.map +1 -1
  187. package/dist/router.d.ts +9 -2
  188. package/dist/router.d.ts.map +1 -1
  189. package/dist/router.js +47 -50
  190. package/dist/router.js.map +1 -1
  191. package/dist/seo.d.ts +49 -0
  192. package/dist/seo.d.ts.map +1 -0
  193. package/dist/seo.js +48 -0
  194. package/dist/seo.js.map +1 -0
  195. package/dist/server-components/build-info.d.ts +3 -0
  196. package/dist/server-components/build-info.d.ts.map +1 -0
  197. package/dist/server-components/build-info.js +7 -0
  198. package/dist/server-components/build-info.js.map +1 -0
  199. package/dist/server-components/build-target.d.ts +12 -0
  200. package/dist/server-components/build-target.d.ts.map +1 -0
  201. package/dist/server-components/build-target.js +153 -0
  202. package/dist/server-components/build-target.js.map +1 -0
  203. package/dist/server-components/build.d.ts +24 -0
  204. package/dist/server-components/build.d.ts.map +1 -0
  205. package/dist/server-components/build.js +366 -0
  206. package/dist/server-components/build.js.map +1 -0
  207. package/dist/server-components/bundle-proof.d.ts +3 -0
  208. package/dist/server-components/bundle-proof.d.ts.map +1 -0
  209. package/dist/server-components/bundle-proof.js +23 -0
  210. package/dist/server-components/bundle-proof.js.map +1 -0
  211. package/dist/server-components/client.d.ts +46 -0
  212. package/dist/server-components/client.d.ts.map +1 -0
  213. package/dist/server-components/client.js +594 -0
  214. package/dist/server-components/client.js.map +1 -0
  215. package/dist/server-components/definition.d.ts +11 -0
  216. package/dist/server-components/definition.d.ts.map +1 -0
  217. package/dist/server-components/definition.js +94 -0
  218. package/dist/server-components/definition.js.map +1 -0
  219. package/dist/server-components/dev-http.d.ts +5 -0
  220. package/dist/server-components/dev-http.d.ts.map +1 -0
  221. package/dist/server-components/dev-http.js +71 -0
  222. package/dist/server-components/dev-http.js.map +1 -0
  223. package/dist/server-components/error.d.ts +9 -0
  224. package/dist/server-components/error.d.ts.map +1 -0
  225. package/dist/server-components/error.js +24 -0
  226. package/dist/server-components/error.js.map +1 -0
  227. package/dist/server-components/frames.d.ts +7 -0
  228. package/dist/server-components/frames.d.ts.map +1 -0
  229. package/dist/server-components/frames.js +169 -0
  230. package/dist/server-components/frames.js.map +1 -0
  231. package/dist/server-components/html-policy.d.ts +6 -0
  232. package/dist/server-components/html-policy.d.ts.map +1 -0
  233. package/dist/server-components/html-policy.js +75 -0
  234. package/dist/server-components/html-policy.js.map +1 -0
  235. package/dist/server-components/html.d.ts +4 -0
  236. package/dist/server-components/html.d.ts.map +1 -0
  237. package/dist/server-components/html.js +67 -0
  238. package/dist/server-components/html.js.map +1 -0
  239. package/dist/server-components/islands.d.ts +44 -0
  240. package/dist/server-components/islands.d.ts.map +1 -0
  241. package/dist/server-components/islands.js +42 -0
  242. package/dist/server-components/islands.js.map +1 -0
  243. package/dist/server-components/native.d.ts +4 -0
  244. package/dist/server-components/native.d.ts.map +1 -0
  245. package/dist/server-components/native.js +81 -0
  246. package/dist/server-components/native.js.map +1 -0
  247. package/dist/server-components/portal.d.ts +6 -0
  248. package/dist/server-components/portal.d.ts.map +1 -0
  249. package/dist/server-components/portal.js +35 -0
  250. package/dist/server-components/portal.js.map +1 -0
  251. package/dist/server-components/props.d.ts +3 -0
  252. package/dist/server-components/props.d.ts.map +1 -0
  253. package/dist/server-components/props.js +14 -0
  254. package/dist/server-components/props.js.map +1 -0
  255. package/dist/server-components/protocol.d.ts +44 -0
  256. package/dist/server-components/protocol.d.ts.map +1 -0
  257. package/dist/server-components/protocol.js +139 -0
  258. package/dist/server-components/protocol.js.map +1 -0
  259. package/dist/server-components/range.d.ts +24 -0
  260. package/dist/server-components/range.d.ts.map +1 -0
  261. package/dist/server-components/range.js +263 -0
  262. package/dist/server-components/range.js.map +1 -0
  263. package/dist/server-components/registry.d.ts +6 -0
  264. package/dist/server-components/registry.d.ts.map +1 -0
  265. package/dist/server-components/registry.js +30 -0
  266. package/dist/server-components/registry.js.map +1 -0
  267. package/dist/server-components/response.d.ts +4 -0
  268. package/dist/server-components/response.d.ts.map +1 -0
  269. package/dist/server-components/response.js +32 -0
  270. package/dist/server-components/response.js.map +1 -0
  271. package/dist/server-components/router-integration.d.ts +9 -0
  272. package/dist/server-components/router-integration.d.ts.map +1 -0
  273. package/dist/server-components/router-integration.js +29 -0
  274. package/dist/server-components/router-integration.js.map +1 -0
  275. package/dist/server-components/runtime.d.ts +14 -0
  276. package/dist/server-components/runtime.d.ts.map +1 -0
  277. package/dist/server-components/runtime.js +276 -0
  278. package/dist/server-components/runtime.js.map +1 -0
  279. package/dist/server-components/scheduler.d.ts +14 -0
  280. package/dist/server-components/scheduler.d.ts.map +1 -0
  281. package/dist/server-components/scheduler.js +74 -0
  282. package/dist/server-components/scheduler.js.map +1 -0
  283. package/dist/server-components/schema.d.ts +4 -0
  284. package/dist/server-components/schema.d.ts.map +1 -0
  285. package/dist/server-components/schema.js +77 -0
  286. package/dist/server-components/schema.js.map +1 -0
  287. package/dist/server-components/server-entry.d.ts +12 -0
  288. package/dist/server-components/server-entry.d.ts.map +1 -0
  289. package/dist/server-components/server-entry.js +90 -0
  290. package/dist/server-components/server-entry.js.map +1 -0
  291. package/dist/server-components/source.d.ts +15 -0
  292. package/dist/server-components/source.d.ts.map +1 -0
  293. package/dist/server-components/source.js +269 -0
  294. package/dist/server-components/source.js.map +1 -0
  295. package/dist/server-components/state.d.ts +4 -0
  296. package/dist/server-components/state.d.ts.map +1 -0
  297. package/dist/server-components/state.js +123 -0
  298. package/dist/server-components/state.js.map +1 -0
  299. package/dist/server-components/stream.d.ts +6 -0
  300. package/dist/server-components/stream.d.ts.map +1 -0
  301. package/dist/server-components/stream.js +70 -0
  302. package/dist/server-components/stream.js.map +1 -0
  303. package/dist/server-components/trusted-html.d.ts +3 -0
  304. package/dist/server-components/trusted-html.d.ts.map +1 -0
  305. package/dist/server-components/trusted-html.js +14 -0
  306. package/dist/server-components/trusted-html.js.map +1 -0
  307. package/dist/server-components/types.d.ts +107 -0
  308. package/dist/server-components/types.d.ts.map +1 -0
  309. package/dist/server-components/types.js +2 -0
  310. package/dist/server-components/types.js.map +1 -0
  311. package/dist/server-components/typescript.d.ts +11 -0
  312. package/dist/server-components/typescript.d.ts.map +1 -0
  313. package/dist/server-components/typescript.js +28 -0
  314. package/dist/server-components/typescript.js.map +1 -0
  315. package/dist/server-components/value.d.ts +5 -0
  316. package/dist/server-components/value.d.ts.map +1 -0
  317. package/dist/server-components/value.js +77 -0
  318. package/dist/server-components/value.js.map +1 -0
  319. package/dist/server-components/vite.d.ts +12 -0
  320. package/dist/server-components/vite.d.ts.map +1 -0
  321. package/dist/server-components/vite.js +197 -0
  322. package/dist/server-components/vite.js.map +1 -0
  323. package/dist/server-components.d.ts +5 -0
  324. package/dist/server-components.d.ts.map +1 -0
  325. package/dist/server-components.js +4 -0
  326. package/dist/server-components.js.map +1 -0
  327. package/dist/service-worker.d.ts +8 -1
  328. package/dist/service-worker.d.ts.map +1 -1
  329. package/dist/service-worker.js +20 -6
  330. package/dist/service-worker.js.map +1 -1
  331. package/dist/vite.d.ts +12 -0
  332. package/dist/vite.d.ts.map +1 -1
  333. package/dist/vite.js +7 -1
  334. package/dist/vite.js.map +1 -1
  335. package/dist/zones.d.ts +64 -2
  336. package/dist/zones.d.ts.map +1 -1
  337. package/dist/zones.js +331 -28
  338. package/dist/zones.js.map +1 -1
  339. package/package.json +102 -4
  340. package/src/build-vite.ts +31 -9
  341. package/src/build.ts +109 -31
  342. package/src/cdn.ts +18 -12
  343. package/src/client.ts +48 -17
  344. package/src/deferred.ts +27 -9
  345. package/src/dev-feed.ts +24 -5
  346. package/src/dev-port.ts +4 -10
  347. package/src/dev-session.ts +4 -13
  348. package/src/dev.ts +29 -1
  349. package/src/draft.ts +35 -14
  350. package/src/fonts-google.ts +10 -0
  351. package/src/index.ts +5 -6
  352. package/src/internal/codegen.ts +60 -29
  353. package/src/internal/disposable-dir.ts +24 -0
  354. package/src/internal/entry-chunks.ts +18 -0
  355. package/src/internal/evidence-traversal.ts +57 -0
  356. package/src/internal/head-merge.ts +16 -0
  357. package/src/internal/js-string.ts +6 -1
  358. package/src/internal/mount-shadow.ts +1 -1
  359. package/src/internal/output-guard.ts +88 -7
  360. package/src/internal/package-entries.ts +331 -0
  361. package/src/internal/page-execution.ts +573 -114
  362. package/src/internal/parity.ts +15 -2
  363. package/src/internal/private-env.ts +27 -13
  364. package/src/internal/progressive-cookies.ts +60 -0
  365. package/src/internal/public-env.ts +14 -0
  366. package/src/internal/render-document.ts +66 -89
  367. package/src/internal/response-controls.ts +31 -14
  368. package/src/internal/revive-dates.ts +35 -0
  369. package/src/internal/route-path.ts +24 -0
  370. package/src/internal/secret-scan.ts +190 -18
  371. package/src/internal/server-boundary.ts +29 -5
  372. package/src/internal/web-app.ts +126 -42
  373. package/src/internal/wire-date.ts +16 -0
  374. package/src/internal/without-query.ts +8 -0
  375. package/src/internal/zone-graph.ts +78 -25
  376. package/src/isr.ts +364 -45
  377. package/src/llms-txt.ts +32 -3
  378. package/src/manifest.ts +38 -4
  379. package/src/open-resolved.ts +64 -0
  380. package/src/plugins/kit.ts +31 -11
  381. package/src/plugins/vite-leak-guard.ts +104 -8
  382. package/src/plugins/vite-zone-guard.ts +21 -19
  383. package/src/plugins/zone-guard.ts +41 -2
  384. package/src/prerender.ts +39 -18
  385. package/src/public-dir.ts +27 -8
  386. package/src/query.ts +107 -26
  387. package/src/render-seam.ts +30 -2
  388. package/src/route-types.ts +34 -7
  389. package/src/router.ts +66 -59
  390. package/src/seo.ts +68 -0
  391. package/src/server-components/build-info.ts +11 -0
  392. package/src/server-components/build-target.ts +194 -0
  393. package/src/server-components/build.ts +419 -0
  394. package/src/server-components/bundle-proof.ts +33 -0
  395. package/src/server-components/client.ts +663 -0
  396. package/src/server-components/definition.ts +125 -0
  397. package/src/server-components/dev-http.ts +75 -0
  398. package/src/server-components/error.ts +24 -0
  399. package/src/server-components/frames.ts +181 -0
  400. package/src/server-components/html-policy.ts +94 -0
  401. package/src/server-components/html.ts +81 -0
  402. package/src/server-components/islands.ts +111 -0
  403. package/src/server-components/native.ts +101 -0
  404. package/src/server-components/portal.ts +42 -0
  405. package/src/server-components/props.ts +19 -0
  406. package/src/server-components/protocol.ts +167 -0
  407. package/src/server-components/range.ts +313 -0
  408. package/src/server-components/registry.ts +34 -0
  409. package/src/server-components/response.ts +44 -0
  410. package/src/server-components/router-integration.ts +40 -0
  411. package/src/server-components/runtime.ts +376 -0
  412. package/src/server-components/scheduler.ts +84 -0
  413. package/src/server-components/schema.ts +69 -0
  414. package/src/server-components/server-entry.ts +98 -0
  415. package/src/server-components/source.ts +324 -0
  416. package/src/server-components/state.ts +143 -0
  417. package/src/server-components/stream.ts +71 -0
  418. package/src/server-components/trusted-html.ts +21 -0
  419. package/src/server-components/types.ts +138 -0
  420. package/src/server-components/typescript.ts +39 -0
  421. package/src/server-components/value.ts +82 -0
  422. package/src/server-components/vite.ts +242 -0
  423. package/src/server-components.ts +32 -0
  424. package/src/service-worker.ts +24 -7
  425. package/src/vite.ts +8 -1
  426. package/src/zones.ts +418 -32
package/dist/isr.js CHANGED
@@ -6,6 +6,8 @@
6
6
  *
7
7
  * This module is the store primitive; the SWR wrapper (`withISR`) builds on it next.
8
8
  */
9
+ import { isAuthenticatedRequest, writableResponse } from "@nifrajs/core/server";
10
+ import { readBoundedBytes, TransportCodecError } from "@nifrajs/core/transport-codec";
9
11
  import { isDraftEnabled } from "./draft.js";
10
12
  import { assertTokenSecret, timingSafeEqual } from "./internal/timing-safe-equal.js";
11
13
  const ISR_TAG_PATTERN = /^[A-Za-z][A-Za-z0-9._:/-]{0,127}$/;
@@ -81,6 +83,10 @@ export class MemoryCacheStore {
81
83
  cache = new Map();
82
84
  max;
83
85
  tagEpoch = new Map();
86
+ // Purge order: `fence` reads it; each purge advances it and records where, per key and per tag.
87
+ order = 0;
88
+ purgedKeys = new Map();
89
+ purgedTags = new Map();
84
90
  constructor(options = {}) {
85
91
  const isProd = typeof process !== "undefined" && process.env?.NODE_ENV === "production";
86
92
  if (options.allowInProduction !== true && isProd) {
@@ -113,8 +119,13 @@ export class MemoryCacheStore {
113
119
  }
114
120
  return Promise.resolve(value);
115
121
  }
122
+ fence() {
123
+ return this.order;
124
+ }
116
125
  set(key, value) {
117
126
  const tags = normalizeTags(value.tags);
127
+ if (value.fence !== undefined && this.purgedAfter(key, tags, value.fence))
128
+ return Promise.resolve();
118
129
  const tagEpochs = {};
119
130
  for (const tag of tags)
120
131
  tagEpochs[tag] = this.tagEpoch.get(tag) ?? "0";
@@ -131,12 +142,14 @@ export class MemoryCacheStore {
131
142
  }
132
143
  delete(key) {
133
144
  this.cache.delete(key);
145
+ this.markPurged(this.purgedKeys, key);
134
146
  this.pruneTagEpochs();
135
147
  return Promise.resolve();
136
148
  }
137
149
  invalidateTag(tag) {
138
150
  normalizeTag(tag);
139
151
  this.tagEpoch.set(tag, crypto.randomUUID());
152
+ this.markPurged(this.purgedTags, tag);
140
153
  // This store is bounded and local, so eagerly remove matching rows. It keeps invalidation
141
154
  // immediate and lets us discard epoch keys that no retained entry references anymore.
142
155
  for (const [key, row] of this.cache) {
@@ -146,6 +159,22 @@ export class MemoryCacheStore {
146
159
  this.pruneTagEpochs();
147
160
  return Promise.resolve();
148
161
  }
162
+ purgedAfter(key, tags, fence) {
163
+ if ((this.purgedKeys.get(key) ?? 0) > fence)
164
+ return true;
165
+ return tags.some((tag) => (this.purgedTags.get(tag) ?? 0) > fence);
166
+ }
167
+ markPurged(marks, name) {
168
+ marks.delete(name);
169
+ marks.set(name, ++this.order);
170
+ // Bounded like the entries: a mark matters only to a render still in flight.
171
+ while (marks.size > this.max) {
172
+ const oldest = marks.keys().next().value;
173
+ if (oldest === undefined)
174
+ break;
175
+ marks.delete(oldest);
176
+ }
177
+ }
149
178
  pruneTagEpochs() {
150
179
  if (this.tagEpoch.size === 0)
151
180
  return;
@@ -169,12 +198,21 @@ const isCachedResponse = (value) => {
169
198
  if (typeof value !== "object" || value === null)
170
199
  return false;
171
200
  const v = value;
201
+ // Everything a replay hands to `new Response` must be legal there, or a corrupt record would throw
202
+ // on every hit instead of reading as a miss.
172
203
  if (typeof v.body !== "string" ||
173
204
  typeof v.status !== "number" ||
205
+ !Number.isInteger(v.status) ||
206
+ v.status < 200 ||
207
+ v.status > 599 ||
174
208
  typeof v.storedAt !== "number" ||
209
+ !Number.isFinite(v.storedAt) ||
175
210
  typeof v.revalidate !== "number" ||
211
+ !Number.isFinite(v.revalidate) ||
212
+ v.revalidate < 0 ||
176
213
  typeof v.headers !== "object" ||
177
- v.headers === null) {
214
+ v.headers === null ||
215
+ Array.isArray(v.headers)) {
178
216
  return false;
179
217
  }
180
218
  if (v.tags !== undefined) {
@@ -187,13 +225,55 @@ const isCachedResponse = (value) => {
187
225
  return false;
188
226
  }
189
227
  }
190
- for (const headerValue of Object.values(v.headers)) {
228
+ const headers = {};
229
+ for (const [name, headerValue] of Object.entries(v.headers)) {
191
230
  if (typeof headerValue !== "string")
192
231
  return false;
232
+ headers[name] = headerValue;
233
+ }
234
+ try {
235
+ new Headers(headers);
236
+ }
237
+ catch {
238
+ return false;
193
239
  }
194
240
  return true;
195
241
  };
196
242
  const KV_TAG_EPOCH_PREFIX = "__nifra_isr_tag_v1:";
243
+ const KV_PURGED_AT = "__nifraPurgedAt";
244
+ /** When a KV page key was purged, from the marker `delete` leaves; `-Infinity` for a page or nothing. */
245
+ function kvPurgedAt(raw) {
246
+ if (raw === null || !raw.includes(KV_PURGED_AT))
247
+ return Number.NEGATIVE_INFINITY;
248
+ try {
249
+ const parsed = JSON.parse(raw);
250
+ if (typeof parsed === "object" && parsed !== null && KV_PURGED_AT in parsed) {
251
+ const at = parsed[KV_PURGED_AT];
252
+ if (typeof at === "number")
253
+ return at;
254
+ }
255
+ }
256
+ catch {
257
+ // Not a marker.
258
+ }
259
+ return Number.NEGATIVE_INFINITY;
260
+ }
261
+ /** When a tag epoch was written (`<time>:<uuid>`); an epoch from before epochs carried a time is older
262
+ * than any render. */
263
+ function kvEpochTime(epoch) {
264
+ const at = epoch.indexOf(":");
265
+ const time = at === -1 ? Number.NaN : Number(epoch.slice(0, at));
266
+ return Number.isFinite(time) ? time : Number.NEGATIVE_INFINITY;
267
+ }
268
+ let lastKvTime = 0;
269
+ /** Wall-clock ms, strictly increasing per process so a purge orders after a render it follows. Not
270
+ * `timeOrigin + performance.now()`: that clock stops while the host sleeps, so a purge from a process
271
+ * that slept would order before renders other instances began earlier. */
272
+ const kvClock = () => {
273
+ const now = Date.now();
274
+ lastKvTime = now > lastKvTime ? now : lastKvTime + 0.001;
275
+ return lastKvTime;
276
+ };
197
277
  /**
198
278
  * A {@link CacheStore} backed by a **Cloudflare Workers KV** namespace (or any {@link KVNamespaceLike}
199
279
  * binding) - the production-grade shared/durable store ISR wants: cached pages and on-demand purges
@@ -256,29 +336,42 @@ export class KVCacheStore {
256
336
  const { __nifraTagEpochs: _ignored, ...value } = parsed;
257
337
  return value;
258
338
  }
339
+ /** Wall-clock time, so purges on any instance order against it. KV is eventually consistent, so the
340
+ * fence holds only for purges this location has already seen. */
341
+ fence() {
342
+ return kvClock();
343
+ }
259
344
  async set(key, value) {
260
- const tags = normalizeTags(value.tags);
345
+ const { fence, ...entry } = value;
346
+ if (fence !== undefined && kvPurgedAt(await this.kv.get(key)) > fence)
347
+ return;
348
+ const tags = normalizeTags(entry.tags);
261
349
  if (tags.length === 0) {
262
- await this.kv.put(key, JSON.stringify(value), this.putOptions);
350
+ await this.kv.put(key, JSON.stringify(entry), this.putOptions);
263
351
  return;
264
352
  }
265
353
  const epochs = Object.fromEntries(await Promise.all(tags.map(async (tag) => [tag, (await this.kv.get(this.tagKey(tag))) ?? "0"])));
354
+ if (fence !== undefined && Object.values(epochs).some((epoch) => kvEpochTime(epoch) > fence)) {
355
+ return;
356
+ }
266
357
  const stored = {
267
- ...value,
358
+ ...entry,
268
359
  tags,
269
360
  __nifraTagEpochs: epochs,
270
361
  };
271
362
  await this.kv.put(key, JSON.stringify(stored), this.putOptions);
272
363
  }
364
+ /** Leaves a marker rather than removing the key, so `set` can refuse a render that began before
365
+ * the purge. The marker reads as a miss. */
273
366
  async delete(key) {
274
- await this.kv.delete(key);
367
+ await this.kv.put(key, JSON.stringify({ [KV_PURGED_AT]: kvClock() }), this.putOptions);
275
368
  }
276
369
  async invalidateTag(tag) {
277
370
  normalizeTag(tag);
278
371
  // Epoch invalidation avoids KV key enumeration and remains bounded for a tag with millions of
279
372
  // pages. Epoch keys intentionally have no expiration: an expired epoch would resurrect entries
280
- // written before the invalidation when their page key still exists.
281
- await this.kv.put(this.tagKey(tag), crypto.randomUUID());
373
+ // written before the invalidation when their page key still exists. The time orders it for `set`.
374
+ await this.kv.put(this.tagKey(tag), `${kvClock()}:${crypto.randomUUID()}`);
282
375
  }
283
376
  tagKey(tag) {
284
377
  return `${KV_TAG_EPOCH_PREFIX}${encodeURIComponent(tag)}`;
@@ -310,18 +403,30 @@ export function withoutCacheChannel(res) {
310
403
  if (!res.headers.has(ISR_REVALIDATE_HEADER) && !res.headers.has(ISR_REVALIDATE_TAGS_HEADER)) {
311
404
  return res;
312
405
  }
406
+ const target = writableResponse(res);
407
+ target.headers.delete(ISR_REVALIDATE_HEADER);
408
+ target.headers.delete(ISR_REVALIDATE_TAGS_HEADER);
409
+ return target;
410
+ }
411
+ const STORE_FAILED = Symbol("nifra.isr.store-failed");
412
+ const STORE_REPORT_INTERVAL_MS = 60_000;
413
+ const STORE_REPORT_DETAIL_CHARS = 200;
414
+ /** The error's own words with the cache key cut out, since a store may name the key it failed on. */
415
+ function storeErrorDetail(error, key) {
416
+ const text = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
417
+ let path;
313
418
  try {
314
- res.headers.delete(ISR_REVALIDATE_HEADER);
315
- res.headers.delete(ISR_REVALIDATE_TAGS_HEADER);
316
- return res;
419
+ path = new URL(key).pathname;
317
420
  }
318
421
  catch {
319
- // A `fetch()` response has immutable headers: rebuild it around the same body instead.
320
- const headers = new Headers(res.headers);
321
- headers.delete(ISR_REVALIDATE_HEADER);
322
- headers.delete(ISR_REVALIDATE_TAGS_HEADER);
323
- return new Response(res.body, { status: res.status, statusText: res.statusText, headers });
422
+ path = undefined;
324
423
  }
424
+ let redacted = text.split(key).join("<key>");
425
+ if (path !== undefined && path.length > 1)
426
+ redacted = redacted.split(path).join("<key>");
427
+ return redacted.length > STORE_REPORT_DETAIL_CHARS
428
+ ? `${redacted.slice(0, STORE_REPORT_DETAIL_CHARS)}...`
429
+ : redacted;
325
430
  }
326
431
  /** `createWebApp`'s marker for how its documents meet a CSP (see `DOCUMENT_POLICY` in ./csp.ts). Read
327
432
  * through the global symbol registry so a cache wrapper never imports the CSP module. */
@@ -376,7 +481,18 @@ const cacheControlHas = (headers, names) => {
376
481
  return names.some((name) => directives.includes(name));
377
482
  };
378
483
  const hasSetCookie = (headers) => headers.has("set-cookie") || (headers.getSetCookie?.().length ?? 0) > 0;
379
- const requestCarriesPrivateState = (req) => req.headers.has("authorization") || req.headers.has("cookie");
484
+ /** Whether every cookie in a `Cookie` header is named in `names`. */
485
+ const cookieNamesWithin = (header, names) => {
486
+ for (const pair of header.split(";")) {
487
+ const name = pair.split("=", 1)[0]?.trim() ?? "";
488
+ if (name !== "" && !names.has(name))
489
+ return false;
490
+ }
491
+ return true;
492
+ };
493
+ // An authentication stage may read any header, so a request it accepted is private whatever carried
494
+ // the credential. The mark is set on the request the app was handed, which is why this runs after it.
495
+ const requestCarriesPrivateState = (req) => req.headers.has("authorization") || req.headers.has("cookie") || isAuthenticatedRequest(req);
380
496
  const responseIsExplicitlyPublic = (res) => cacheControlHas(res.headers, ["public"]);
381
497
  // `x-nifra-data` alone is not a variation the URL key misses: a navigation data request never reads
382
498
  // or writes the store, so every stored entry is the document.
@@ -394,7 +510,8 @@ const responseDeclaresVary = (res) => {
394
510
  /**
395
511
  * Whether `res` may be stored by a shared cache and served to anyone requesting `req`'s URL: a
396
512
  * full-document `GET` 200 `text/html` with no Set-Cookie, no `private`/`no-store`, no `Vary` beyond
397
- * the data header, and, for a request carrying a cookie or Authorization, an explicit `public`.
513
+ * the data header, and, for a request carrying a cookie or Authorization or accepted by an
514
+ * authentication stage, an explicit `public`.
398
515
  * The one rule both `withISR` and `@nifrajs/web/cdn` cache by.
399
516
  */
400
517
  export const isCacheablePage = (req, res) => {
@@ -471,7 +588,21 @@ function sanitizeCachedHeaders(headers) {
471
588
  }
472
589
  return out;
473
590
  }
474
- const isInvalidationResponse = (res) => res.status === 404 || res.status === 410 || (res.status >= 300 && res.status < 400);
591
+ /**
592
+ * Whether a regeneration's answer withdraws the stored page: it is gone or moved, or, rendered for a
593
+ * request with no cookie or Authorization, refused (401, 403) or declared `private`/`no-store`. A
594
+ * signed-in visitor's own 403 or private copy says nothing about the shared page. A 5xx or 429 keeps
595
+ * serving the stale copy.
596
+ */
597
+ const isInvalidationResponse = (req, res) => {
598
+ if (res.status === 404 || res.status === 410 || (res.status >= 300 && res.status < 400))
599
+ return true;
600
+ if (requestCarriesPrivateState(req))
601
+ return false;
602
+ return (res.status === 401 ||
603
+ res.status === 403 ||
604
+ (res.status >= 200 && res.status < 300 && cacheControlHas(res.headers, ["private", "no-store"])));
605
+ };
475
606
  /**
476
607
  * Wrap a nifra app with **Incremental Static Regeneration**: a cacheable page is served from
477
608
  * {@link CacheStore} when fresh, served **stale while a fresh copy regenerates in the background**
@@ -486,8 +617,77 @@ const isInvalidationResponse = (res) => res.status === 404 || res.status === 410
486
617
  export function withISR(app, options) {
487
618
  const { store, now } = options;
488
619
  assertRevalidate(options.revalidate, "revalidate");
620
+ const storeTimeoutMs = options.storeTimeoutMs;
621
+ if (storeTimeoutMs !== undefined && !(Number.isFinite(storeTimeoutMs) && storeTimeoutMs > 0)) {
622
+ throw new RangeError("[nifra/web] ISR storeTimeoutMs must be a positive finite number");
623
+ }
624
+ const lastReport = new Map();
625
+ const reportStoreError = (op, key, error) => {
626
+ try {
627
+ if (options.onStoreError !== undefined) {
628
+ options.onStoreError(error, { op });
629
+ return;
630
+ }
631
+ const last = lastReport.get(op);
632
+ const at = now();
633
+ if (last !== undefined && at - last < STORE_REPORT_INTERVAL_MS)
634
+ return;
635
+ lastReport.set(op, at);
636
+ console.error(`[nifra/web] ISR store ${op} failed; serving without the cache (${storeErrorDetail(error, key)})`);
637
+ }
638
+ catch {
639
+ // A failing reporter must not turn a served page into an error.
640
+ }
641
+ };
642
+ // Runs one store call; a throw, a rejection or a timeout is reported and answered with STORE_FAILED.
643
+ const attempt = async (op, key, call) => {
644
+ let timer;
645
+ try {
646
+ const pending = call();
647
+ if (storeTimeoutMs === undefined)
648
+ return await pending;
649
+ const timeout = new Promise((_, reject) => {
650
+ timer = setTimeout(() => {
651
+ const error = new Error(`ISR store ${op} exceeded storeTimeoutMs (${storeTimeoutMs} ms)`);
652
+ error.name = "TimeoutError";
653
+ reject(error);
654
+ }, storeTimeoutMs);
655
+ });
656
+ return await Promise.race([pending, timeout]);
657
+ }
658
+ catch (error) {
659
+ reportStoreError(op, key, error);
660
+ return STORE_FAILED;
661
+ }
662
+ finally {
663
+ if (timer !== undefined)
664
+ clearTimeout(timer);
665
+ }
666
+ };
667
+ // A store without purge order, or one whose fence failed, stores the render unfenced.
668
+ const fenceFor = async (key) => {
669
+ const { fence } = store;
670
+ if (fence === undefined)
671
+ return undefined;
672
+ const taken = await attempt("fence", key, async () => fence.call(store));
673
+ return taken === STORE_FAILED ? undefined : taken;
674
+ };
489
675
  const keyOf = options.key ?? requestKeyOf(urlKeyOf(options.query));
490
676
  const draftSecret = options.draftSecret;
677
+ const anonymousCookies = new Set(options.anonymousCookies ?? []);
678
+ // A request whose cookies are all anonymous is judged for storage as if it carried none.
679
+ const judged = (req) => {
680
+ if (anonymousCookies.size === 0 ||
681
+ req.headers.has("authorization") ||
682
+ isAuthenticatedRequest(req))
683
+ return req;
684
+ const cookie = req.headers.get("cookie");
685
+ if (cookie === null || !cookieNamesWithin(cookie, anonymousCookies))
686
+ return req;
687
+ const headers = new Headers(req.headers);
688
+ headers.delete("cookie");
689
+ return new Request(req.url, { method: req.method, headers });
690
+ };
491
691
  const regenerating = new Set();
492
692
  openCacheChannel(app);
493
693
  // An outer cache layer (`withCdn`) opens this wrapper's own channel: cached responses then carry how
@@ -531,10 +731,12 @@ export function withISR(app, options) {
531
731
  const safeSeconds = Number.isFinite(seconds) && seconds >= 0 ? seconds : options.revalidate;
532
732
  return safeSeconds * 1000;
533
733
  };
534
- const render = async (req, platform, key) => {
734
+ const render = async (req, platform, key, remember, stored) => {
735
+ const fence = await fenceFor(key);
535
736
  const res = await app.fetch(req, platform);
536
- if (!isCacheablePage(req, res)) {
537
- rememberUncacheable(key, res);
737
+ if (!isCacheablePage(judged(req), res)) {
738
+ if (remember)
739
+ rememberUncacheable(key, res);
538
740
  return withoutCacheChannel(res);
539
741
  }
540
742
  uncacheable.delete(key);
@@ -547,14 +749,44 @@ export function withISR(app, options) {
547
749
  storedAt: now(),
548
750
  revalidate: ttlMs(res),
549
751
  ...(tags.length === 0 ? {} : { tags }),
752
+ fence,
550
753
  };
551
- await store.set(key, entry);
552
- const fresh = new Response(body, {
553
- status: res.status,
754
+ // A failed write still answers with the page, and still hands it to concurrent misses: whether it
755
+ // may be shared was decided above, not by the store.
756
+ await attempt("set", key, () => store.set(key, entry));
757
+ stored?.(entry);
758
+ return fresh(entry);
759
+ };
760
+ const fresh = (entry) => {
761
+ const res = new Response(entry.body, {
762
+ status: entry.status,
554
763
  headers: { ...entry.headers, [ISR_STATUS_HEADER]: "miss" },
555
764
  });
556
- advertise(fresh.headers, entry.revalidate, tags);
557
- return fresh;
765
+ advertise(res.headers, entry.revalidate, entry.tags ?? []);
766
+ return res;
767
+ };
768
+ // Cold misses in flight per key. Concurrent misses wait for the first one's stored entry instead
769
+ // of each rendering the page; a render that is not stored (private, failed) sends each waiter to
770
+ // render for itself, so a waiter never receives a page that was not safe to share.
771
+ const coldRenders = new Map();
772
+ const renderCold = async (req, platform, key) => {
773
+ const pending = coldRenders.get(key);
774
+ if (pending !== undefined) {
775
+ const entry = await pending;
776
+ return entry === undefined ? render(req, platform, key, true) : fresh(entry);
777
+ }
778
+ const shared = Promise.withResolvers();
779
+ coldRenders.set(key, shared.promise);
780
+ let entry;
781
+ try {
782
+ return await render(req, platform, key, true, (stored) => {
783
+ entry = stored;
784
+ });
785
+ }
786
+ finally {
787
+ coldRenders.delete(key);
788
+ shared.resolve(entry);
789
+ }
558
790
  };
559
791
  // Background regeneration (single-flight per key) - a failed regen keeps the stale entry; the next
560
792
  // stale hit retries. Never throws (it runs detached / under waitUntil).
@@ -563,23 +795,27 @@ export function withISR(app, options) {
563
795
  return;
564
796
  regenerating.add(key);
565
797
  try {
798
+ const fence = await fenceFor(key);
566
799
  const res = await app.fetch(req, platform);
567
- if (isCacheablePage(req, res)) {
800
+ if (isCacheablePage(judged(req), res)) {
568
801
  const tags = tagsOf(res);
569
- await store.set(key, {
802
+ const entry = {
570
803
  body: await res.text(),
571
804
  status: res.status,
572
805
  headers: headersOf(res),
573
806
  storedAt: now(),
574
807
  revalidate: ttlMs(res),
575
808
  ...(tags.length === 0 ? {} : { tags }),
576
- });
809
+ fence,
810
+ };
811
+ await attempt("set", key, () => store.set(key, entry));
577
812
  }
578
- else if (isInvalidationResponse(res)) {
579
- // A successful deletion or redirect is authoritative. Keeping the old entry would make a
580
- // withdrawn page stay publicly visible forever because every stale request would retry the same
581
- // non-cacheable response while continuing to serve the old body.
582
- await store.delete(key);
813
+ else if (isInvalidationResponse(judged(req), res)) {
814
+ // A withdrawal is authoritative. Keeping the old entry would make a withdrawn page stay
815
+ // publicly visible forever, because every stale request would retry the same non-cacheable
816
+ // response while continuing to serve the old body.
817
+ await res.body?.cancel();
818
+ await attempt("delete", key, () => store.delete(key));
583
819
  }
584
820
  else {
585
821
  await res.body?.cancel();
@@ -596,7 +832,14 @@ export function withISR(app, options) {
596
832
  // A data-mode soft-nav GET bypasses the cache entirely: entries are full HTML documents keyed
597
833
  // by URL, and serving one to a loader-data fetch would hand the client HTML where it expects
598
834
  // the loader payload. (The write path already refuses to cache data-mode responses.)
599
- const key = req.method === "GET" && req.headers.get("x-nifra-data") === null ? keyOf(req) : null;
835
+ const privateRoute = Reflect.get(app, Symbol.for("nifra.web.private-route"));
836
+ const ownsPrivate = typeof privateRoute === "function" && privateRoute(req) === true;
837
+ const key = req.method === "GET" &&
838
+ req.headers.get("x-nifra-data") === null &&
839
+ !req.headers.has("x-nifra-component") &&
840
+ !ownsPrivate
841
+ ? keyOf(req)
842
+ : null;
600
843
  if (key === null)
601
844
  return withoutCacheChannel(await app.fetch(req, platform));
602
845
  // Draft/preview: an editor (valid signed cookie) always renders fresh and is never cached, so
@@ -604,13 +847,17 @@ export function withISR(app, options) {
604
847
  if (draftSecret !== undefined && (await isDraftEnabled(req, draftSecret))) {
605
848
  return withoutCacheChannel(await app.fetch(req, platform));
606
849
  }
850
+ // A cookie or Authorization request may be personalized: under the default key it never reads
851
+ // the store, and its private render never marks the URL uncacheable for everyone else.
852
+ const personal = options.key === undefined && requestCarriesPrivateState(judged(req));
607
853
  const until = uncacheable.get(key);
608
854
  if (until !== undefined) {
609
855
  if (now() < until)
610
- return render(req, platform, key);
856
+ return render(req, platform, key, !personal);
611
857
  uncacheable.delete(key);
612
858
  }
613
- const hit = await store.get(key);
859
+ const read = personal ? undefined : await attempt("get", key, () => store.get(key));
860
+ const hit = read === STORE_FAILED ? undefined : read;
614
861
  if (hit !== undefined) {
615
862
  if (now() - hit.storedAt < hit.revalidate)
616
863
  return served(hit, "hit");
@@ -622,7 +869,7 @@ export function withISR(app, options) {
622
869
  void task.catch(() => { });
623
870
  return served(hit, "stale");
624
871
  }
625
- return render(req, platform, key);
872
+ return personal ? render(req, platform, key, false) : renderCold(req, platform, key);
626
873
  };
627
874
  Object.defineProperty(handler, CACHE_CHANNEL, {
628
875
  value: () => {
@@ -635,6 +882,8 @@ const jsonError = (status, error) => Response.json({ ok: false, error }, { statu
635
882
  /** Most paths and tags one revalidation request may name, so a leaked token cannot fan out. */
636
883
  export const MAX_REVALIDATE_PATHS = 100;
637
884
  export const MAX_REVALIDATE_TAGS = MAX_ISR_TAGS;
885
+ // Room for a full batch of long paths; a purge body past it is refused before it is buffered.
886
+ const MAX_REVALIDATE_BODY_BYTES = 1024 * 1024;
638
887
  const stringList = (value) => {
639
888
  if (value === undefined)
640
889
  return undefined;
@@ -648,7 +897,7 @@ const stringList = (value) => {
648
897
  * `?path=/blog/x`, `?tag=products`, a JSON `{ "path": "/blog/x" }` / `{ "tag": "products" }` body, or a
649
898
  * batch `{ "paths": [...], "tags": [...] }` of at most 100 paths and 32 tags.
650
899
  * The token is checked in **constant time** (wrong/missing → `401`); malformed targets → `400`;
651
- * non-POST → `405`. A store without tag support returns `501` for tag requests. Mount it on a nifra route, e.g.
900
+ * a JSON body over 1 MiB → `413`; non-POST → `405`. A store without tag support returns `501` for tag requests. Mount it on a nifra route, e.g.
652
901
  * `app.post("/__nifra/revalidate", (c) => handler(c.req))`.
653
902
  */
654
903
  export function revalidateEndpoint(options) {
@@ -671,7 +920,17 @@ export function revalidateEndpoint(options) {
671
920
  let tag = url.searchParams.get("tag");
672
921
  let batch;
673
922
  if (path === null && tag === null) {
674
- const body = await req.json().catch(() => null);
923
+ let body = null;
924
+ try {
925
+ const bytes = await readBoundedBytes(new Response(req.body, { headers: req.headers }), {
926
+ maxBytes: MAX_REVALIDATE_BODY_BYTES,
927
+ });
928
+ body = JSON.parse(new TextDecoder().decode(bytes));
929
+ }
930
+ catch (error) {
931
+ if (error instanceof TransportCodecError)
932
+ return jsonError(413, "body_too_large");
933
+ }
675
934
  if (typeof body === "object" && body !== null) {
676
935
  const candidate = body;
677
936
  if (candidate.paths !== undefined || candidate.tags !== undefined) {
@@ -723,6 +982,17 @@ export function revalidateEndpoint(options) {
723
982
  if (tags.length > 0 && invalidateTag === undefined) {
724
983
  return jsonError(501, "tag_invalidation_not_supported");
725
984
  }
985
+ if (tags.length > 0 && options.dataCaches !== undefined) {
986
+ try {
987
+ for (const cache of options.dataCaches) {
988
+ for (const candidate of tags)
989
+ await cache.invalidateTag(candidate);
990
+ }
991
+ }
992
+ catch {
993
+ return Response.json({ ok: false, error: "data_cache_invalidation_failed" }, { status: 500 });
994
+ }
995
+ }
726
996
  for (const key of keys)
727
997
  await options.store.delete(key);
728
998
  for (const candidate of tags)