@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/dist/isr.d.ts CHANGED
@@ -28,8 +28,29 @@ export interface CacheStore {
28
28
  /** Drop every cached response carrying `tag`, when the backend supports tag invalidation. */
29
29
  readonly invalidateTag?: (tag: string) => Promise<void>;
30
30
  }
31
+ export declare function normalizeTags(tags: readonly string[] | undefined): readonly string[];
32
+ /** The route tags a response carries on {@link ISR_REVALIDATE_TAGS_HEADER}; malformed means none. */
33
+ export declare function tagsFromHeader(value: string | null): readonly string[];
31
34
  /** Validate and serialize route-declared ISR tags for the internal response header. */
32
35
  export declare function serializeISRTags(tags: readonly string[]): string | undefined;
36
+ /** What a `revalidateTags` function is given: the URL and route params only, both already public. */
37
+ export interface RevalidateTagsInput {
38
+ readonly params: Readonly<Record<string, string>>;
39
+ readonly url: URL;
40
+ }
41
+ /**
42
+ * A route's `revalidateTags`: a fixed list, or one computed per request from its params and URL. The
43
+ * function is declared as a method so a route may annotate its own params (`{ params: { id: string } }`).
44
+ */
45
+ export type RevalidateTags = readonly string[] | {
46
+ tags(input: RevalidateTagsInput): readonly string[];
47
+ }["tags"];
48
+ /**
49
+ * The tags `declared` gives this request. A fixed list is returned as declared (it is validated when
50
+ * the header is written). A function's result keeps its valid, distinct tags, at most 32; anything
51
+ * else is dropped with one warning per route, naming the route but never the tag.
52
+ */
53
+ export declare function routeTags(declared: RevalidateTags, input: RevalidateTagsInput, routeId: string): readonly string[];
33
54
  export interface MemoryCacheStoreOptions {
34
55
  /** Allow the in-memory store in production. Off by default - per-instance caching means revalidation
35
56
  * won't propagate across instances and each instance caches separately. */
@@ -132,6 +153,13 @@ export declare const ISR_REVALIDATE_TAGS_HEADER = "x-nifra-isr-tags";
132
153
  * refetch): this one is an integer TTL the *wrapper* reads, so the two channels never alias.
133
154
  */
134
155
  export declare const ISR_REVALIDATE_HEADER = "x-nifra-isr-revalidate";
156
+ /** A `createWebApp` app answers this with a function that turns its ISR headers on. A wrapper that
157
+ * reads them calls it, so an app nothing caches never sends a route's freshness or tags. */
158
+ export declare const CACHE_CHANNEL: unique symbol;
159
+ /** Ask `app` to emit its ISR headers; an app without the channel emits whatever it emits. */
160
+ export declare function openCacheChannel(app: object): void;
161
+ /** Drop the ISR headers from a response that leaves the cache layer. */
162
+ export declare function withoutCacheChannel(res: Response): Response;
135
163
  export interface ISROptions {
136
164
  readonly store: CacheStore;
137
165
  /** Default freshness window (**seconds**) for a cached page; older ⇒ stale (served, regenerated
@@ -140,15 +168,35 @@ export interface ISROptions {
140
168
  readonly revalidate: number;
141
169
  /** Monotonic clock (ms) - injected for testability; production passes `() => Date.now()`. */
142
170
  readonly now: () => number;
143
- /** Cache key for a request. Default: `origin + pathname + search` so host-routed apps do not share
144
- * entries across tenants. Return `null` to bypass the cache for this request (it goes straight to the
145
- * app, uncached). */
171
+ /** Cache key for a request. Default: `origin + pathname`, plus the query parameters `query` admits,
172
+ * so host-routed apps do not share entries across tenants. Return `null` to bypass the cache for
173
+ * this request (it goes straight to the app, uncached). Replaces `query` entirely. */
146
174
  readonly key?: (req: Request) => string | null;
175
+ /**
176
+ * How the query string reaches the default key. Default `"bypass"`: a request carrying any query
177
+ * parameter skips the cache - rendered fresh, never stored - so `?a=1`, `?a=2`, ... cannot each
178
+ * store a full page. A list caches by those parameters only, in any order (`?b=2&a=1` and
179
+ * `?a=1&b=2` share an entry); a request carrying any other parameter still bypasses, because a
180
+ * loader that reads it would otherwise be served another request's page. `"all"` keys on the whole
181
+ * query string, so every distinct query is a new entry.
182
+ */
183
+ readonly query?: ISRQuery;
147
184
  /** Draft/preview secret (the same one given to `createWebApp({ draftSecret })` + `enableDraft`). When
148
185
  * set, a request carrying a valid signed draft cookie **bypasses the cache** - editors always render
149
186
  * fresh, and a draft render is never written to the store (it can't poison the public cache). */
150
187
  readonly draftSecret?: string;
151
188
  }
189
+ /** Which query parameters an ISR key carries - see {@link ISROptions.query}. */
190
+ export type ISRQuery = "bypass" | "all" | readonly string[];
191
+ /** The default key for a URL under a `query` policy, or `null` when the URL must bypass the cache. */
192
+ export declare const urlKeyOf: (query?: ISRQuery) => ((url: URL) => string | null);
193
+ /**
194
+ * Whether `res` may be stored by a shared cache and served to anyone requesting `req`'s URL: a
195
+ * full-document `GET` 200 `text/html` with no Set-Cookie, no `private`/`no-store`, no `Vary` beyond
196
+ * the data header, and, for a request carrying a cookie or Authorization, an explicit `public`.
197
+ * The one rule both `withISR` and `@nifrajs/web/cdn` cache by.
198
+ */
199
+ export declare const isCacheablePage: (req: Request, res: Response) => boolean;
152
200
  /**
153
201
  * Wrap a nifra app with **Incremental Static Regeneration**: a cacheable page is served from
154
202
  * {@link CacheStore} when fresh, served **stale while a fresh copy regenerates in the background**
@@ -161,6 +209,24 @@ export interface ISROptions {
161
209
  * GET 200s are cached; everything else (assets, data-mode GETs, redirects, errors) passes through.
162
210
  */
163
211
  export declare function withISR(app: ISRApp, options: ISROptions): (req: Request, platform?: ISRPlatform) => Promise<Response>;
212
+ /** What a CDN purge came to: sent and accepted, queued behind a rate limit or retry, or refused. */
213
+ export interface CdnPurgeOutcome {
214
+ readonly cdn: "accepted" | "queued" | "failed";
215
+ /** Whether the same purge may succeed if sent again. */
216
+ readonly retryable: boolean;
217
+ /** A short reason when it did not succeed (never the provider's credentials). */
218
+ readonly error?: string;
219
+ }
220
+ /** A CDN a revalidation purges after the origin store; every `@nifrajs/web/cdn` provider is one. */
221
+ export interface CdnPurgeTarget {
222
+ purge(target: {
223
+ readonly tags?: readonly string[];
224
+ readonly paths?: readonly string[];
225
+ }, platform?: ISRPlatform): Promise<CdnPurgeOutcome>;
226
+ }
227
+ /** Most paths and tags one revalidation request may name, so a leaked token cannot fan out. */
228
+ export declare const MAX_REVALIDATE_PATHS = 100;
229
+ export declare const MAX_REVALIDATE_TAGS = 32;
164
230
  export interface RevalidateEndpointOptions {
165
231
  readonly store: CacheStore;
166
232
  /** Shared secret; the request's token must match it (constant-time). */
@@ -170,14 +236,25 @@ export interface RevalidateEndpointOptions {
170
236
  /** Map a to-purge path → its cache key - MUST match the `withISR` `key` fn. The default uses the
171
237
  * revalidation request's origin plus the purged path, matching `withISR`'s default host-aware key. */
172
238
  readonly key?: (path: string, req: Request) => string;
239
+ /** The `query` policy given to `withISR`, so a purged path's query string is keyed the same way.
240
+ * Default `"bypass"`. A path whose query `withISR` never caches is refused with `400`. Ignored
241
+ * when `key` is supplied. */
242
+ readonly query?: ISRQuery;
243
+ /**
244
+ * A CDN to purge after the origin store (a `@nifrajs/web/cdn` provider). The origin goes first so a
245
+ * CDN refetch cannot repopulate from a stale origin entry. The reply is `200` when the CDN accepted
246
+ * the purge, `202` when it is queued (rate limit or retry), and `502` when the CDN refused it.
247
+ */
248
+ readonly cdn?: CdnPurgeTarget;
173
249
  }
174
250
  /**
175
- * An **on-demand revalidation** (purge) endpoint - a `fetch` handler that drops a path's cached entry
251
+ * An **on-demand revalidation** (purge) endpoint - a `fetch` handler that drops cached pages by path
176
252
  * or invalidates every entry carrying a tag. `POST` with the secret in the token header and either
177
- * `?path=/blog/x`, `?tag=products`, or a JSON `{ "path": "/blog/x" }` / `{ "tag": "products" }` body.
253
+ * `?path=/blog/x`, `?tag=products`, a JSON `{ "path": "/blog/x" }` / `{ "tag": "products" }` body, or a
254
+ * batch `{ "paths": [...], "tags": [...] }` of at most 100 paths and 32 tags.
178
255
  * The token is checked in **constant time** (wrong/missing → `401`); malformed targets → `400`;
179
256
  * non-POST → `405`. A store without tag support returns `501` for tag requests. Mount it on a nifra route, e.g.
180
257
  * `app.post("/__nifra/revalidate", (c) => handler(c.req))`.
181
258
  */
182
- export declare function revalidateEndpoint(options: RevalidateEndpointOptions): (req: Request) => Promise<Response>;
259
+ export declare function revalidateEndpoint(options: RevalidateEndpointOptions): (req: Request, platform?: ISRPlatform) => Promise<Response>;
183
260
  //# sourceMappingURL=isr.d.ts.map
package/dist/isr.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"isr.d.ts","sourceRoot":"","sources":["../src/isr.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"isr.d.ts","sourceRoot":"","sources":["../src/isr.ts"],"names":[],"mappings":"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"}
package/dist/isr.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * This module is the store primitive; the SWR wrapper (`withISR`) builds on it next.
8
8
  */
9
9
  import { isDraftEnabled } from "./draft.js";
10
- import { timingSafeEqual } from "./internal/timing-safe-equal.js";
10
+ import { assertTokenSecret, timingSafeEqual } from "./internal/timing-safe-equal.js";
11
11
  const ISR_TAG_PATTERN = /^[A-Za-z][A-Za-z0-9._:/-]{0,127}$/;
12
12
  const MAX_ISR_TAGS = 32;
13
13
  function normalizeTag(tag) {
@@ -16,7 +16,7 @@ function normalizeTag(tag) {
16
16
  }
17
17
  return tag;
18
18
  }
19
- function normalizeTags(tags) {
19
+ export function normalizeTags(tags) {
20
20
  if (tags === undefined)
21
21
  return [];
22
22
  if (!Array.isArray(tags) || tags.length > MAX_ISR_TAGS) {
@@ -27,7 +27,8 @@ function normalizeTags(tags) {
27
27
  unique.add(normalizeTag(tag));
28
28
  return Object.freeze([...unique]);
29
29
  }
30
- function tagsFromHeader(value) {
30
+ /** The route tags a response carries on {@link ISR_REVALIDATE_TAGS_HEADER}; malformed means none. */
31
+ export function tagsFromHeader(value) {
31
32
  if (value === null || value.trim() === "")
32
33
  return [];
33
34
  try {
@@ -44,6 +45,32 @@ export function serializeISRTags(tags) {
44
45
  const normalized = normalizeTags(tags);
45
46
  return normalized.length === 0 ? undefined : normalized.join(",");
46
47
  }
48
+ const warnedTagRoutes = new Set();
49
+ /**
50
+ * The tags `declared` gives this request. A fixed list is returned as declared (it is validated when
51
+ * the header is written). A function's result keeps its valid, distinct tags, at most 32; anything
52
+ * else is dropped with one warning per route, naming the route but never the tag.
53
+ */
54
+ export function routeTags(declared, input, routeId) {
55
+ if (typeof declared !== "function")
56
+ return declared;
57
+ const result = declared(input);
58
+ const listed = Array.isArray(result) ? result : [];
59
+ const kept = new Set();
60
+ let dropped = 0;
61
+ for (const tag of listed) {
62
+ if (typeof tag === "string" && ISR_TAG_PATTERN.test(tag) && kept.size < MAX_ISR_TAGS) {
63
+ kept.add(tag);
64
+ }
65
+ else
66
+ dropped++;
67
+ }
68
+ if ((dropped > 0 || !Array.isArray(result)) && !warnedTagRoutes.has(routeId)) {
69
+ warnedTagRoutes.add(routeId);
70
+ console.warn(`[nifra/web] NIFRA_CDN_TAG_INVALID: revalidateTags of route "${routeId}" returned ${Array.isArray(result) ? `${dropped} tag(s) that are not a letter followed by up to 127 of A-Z a-z 0-9 . _ : / -, or past the first 32` : "something other than an array"}; those were dropped.`);
71
+ }
72
+ return [...kept];
73
+ }
47
74
  /**
48
75
  * In-process ISR cache. Refuses to run in production unless explicitly allowed (mirrors the
49
76
  * rate-limit `MemoryStore` - a per-instance cache is unsafe across instances). Bounded **LRU**: a
@@ -269,10 +296,68 @@ export const ISR_REVALIDATE_TAGS_HEADER = "x-nifra-isr-tags";
269
296
  * refetch): this one is an integer TTL the *wrapper* reads, so the two channels never alias.
270
297
  */
271
298
  export const ISR_REVALIDATE_HEADER = "x-nifra-isr-revalidate";
272
- const defaultKey = (req) => {
273
- const url = new URL(req.url);
274
- return url.origin + url.pathname + url.search;
299
+ /** A `createWebApp` app answers this with a function that turns its ISR headers on. A wrapper that
300
+ * reads them calls it, so an app nothing caches never sends a route's freshness or tags. */
301
+ export const CACHE_CHANNEL = Symbol.for("nifra.web.cacheChannel");
302
+ /** Ask `app` to emit its ISR headers; an app without the channel emits whatever it emits. */
303
+ export function openCacheChannel(app) {
304
+ const open = Reflect.get(app, CACHE_CHANNEL);
305
+ if (typeof open === "function")
306
+ open();
307
+ }
308
+ /** Drop the ISR headers from a response that leaves the cache layer. */
309
+ export function withoutCacheChannel(res) {
310
+ if (!res.headers.has(ISR_REVALIDATE_HEADER) && !res.headers.has(ISR_REVALIDATE_TAGS_HEADER)) {
311
+ return res;
312
+ }
313
+ try {
314
+ res.headers.delete(ISR_REVALIDATE_HEADER);
315
+ res.headers.delete(ISR_REVALIDATE_TAGS_HEADER);
316
+ return res;
317
+ }
318
+ 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 });
324
+ }
325
+ }
326
+ /** `createWebApp`'s marker for how its documents meet a CSP (see `DOCUMENT_POLICY` in ./csp.ts). Read
327
+ * through the global symbol registry so a cache wrapper never imports the CSP module. */
328
+ const DOCUMENT_POLICY = Symbol.for("nifra.web.documentPolicy");
329
+ /** Remembered `no-store` keys per `withISR` wrapper. */
330
+ const MAX_UNCACHEABLE_KEYS = 1024;
331
+ /** The default key for a URL under a `query` policy, or `null` when the URL must bypass the cache. */
332
+ export const urlKeyOf = (query = "bypass") => {
333
+ if (query === "all")
334
+ return (url) => url.origin + url.pathname + url.search;
335
+ if (query !== "bypass" && !Array.isArray(query)) {
336
+ throw new TypeError('[nifra/web] ISR query must be "bypass", "all", or a list of parameter names');
337
+ }
338
+ const allowed = new Set(query === "bypass" ? [] : query);
339
+ for (const name of allowed) {
340
+ if (typeof name !== "string" || name === "") {
341
+ throw new TypeError("[nifra/web] ISR query parameter names must be non-empty strings");
342
+ }
343
+ }
344
+ return (url) => {
345
+ if (url.search === "")
346
+ return url.origin + url.pathname;
347
+ if (allowed.size === 0)
348
+ return null;
349
+ const entries = [];
350
+ for (const entry of url.searchParams) {
351
+ if (!allowed.has(entry[0]))
352
+ return null;
353
+ entries.push(entry);
354
+ }
355
+ // Stable by name, so a repeated name keeps its value order (`getAll` can tell them apart).
356
+ entries.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
357
+ return `${url.origin}${url.pathname}?${new URLSearchParams(entries)}`;
358
+ };
275
359
  };
360
+ const requestKeyOf = (urlKey) => (req) => urlKey(new URL(req.url));
276
361
  function assertRevalidate(value, name) {
277
362
  if (!Number.isFinite(value) || value < 0) {
278
363
  throw new RangeError(`[nifra/web] ${name} must be a finite non-negative number`);
@@ -293,11 +378,26 @@ const cacheControlHas = (headers, names) => {
293
378
  const hasSetCookie = (headers) => headers.has("set-cookie") || (headers.getSetCookie?.().length ?? 0) > 0;
294
379
  const requestCarriesPrivateState = (req) => req.headers.has("authorization") || req.headers.has("cookie");
295
380
  const responseIsExplicitlyPublic = (res) => cacheControlHas(res.headers, ["public"]);
381
+ // `x-nifra-data` alone is not a variation the URL key misses: a navigation data request never reads
382
+ // or writes the store, so every stored entry is the document.
296
383
  const responseDeclaresVary = (res) => {
297
384
  const value = res.headers.get("vary");
298
- return value !== null && value.trim() !== "";
385
+ if (value === null)
386
+ return false;
387
+ for (const part of value.split(",")) {
388
+ const name = part.trim().toLowerCase();
389
+ if (name !== "" && name !== "x-nifra-data")
390
+ return true;
391
+ }
392
+ return false;
299
393
  };
300
- const isCacheablePage = (req, res) => {
394
+ /**
395
+ * Whether `res` may be stored by a shared cache and served to anyone requesting `req`'s URL: a
396
+ * 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`.
398
+ * The one rule both `withISR` and `@nifrajs/web/cdn` cache by.
399
+ */
400
+ export const isCacheablePage = (req, res) => {
301
401
  if (req.method !== "GET")
302
402
  return false;
303
403
  if (req.headers.get("x-nifra-data") !== null)
@@ -341,6 +441,7 @@ const CACHEABLE_RESPONSE_HEADERS = new Set([
341
441
  "vary",
342
442
  "x-content-type-options",
343
443
  "x-frame-options",
444
+ "x-robots-tag",
344
445
  ]);
345
446
  const isCacheableResponseHeader = (key) => {
346
447
  const normalized = key.toLowerCase();
@@ -385,9 +486,44 @@ const isInvalidationResponse = (res) => res.status === 404 || res.status === 410
385
486
  export function withISR(app, options) {
386
487
  const { store, now } = options;
387
488
  assertRevalidate(options.revalidate, "revalidate");
388
- const keyOf = options.key ?? defaultKey;
489
+ const keyOf = options.key ?? requestKeyOf(urlKeyOf(options.query));
389
490
  const draftSecret = options.draftSecret;
390
491
  const regenerating = new Set();
492
+ openCacheChannel(app);
493
+ // An outer cache layer (`withCdn`) opens this wrapper's own channel: cached responses then carry how
494
+ // much freshness they have left and their tags, so the CDN never holds a page longer than ISR would.
495
+ let channelOpen = false;
496
+ const advertise = (headers, remainingMs, tags) => {
497
+ if (!channelOpen)
498
+ return;
499
+ headers.set(ISR_REVALIDATE_HEADER, String(Math.max(0, Math.floor(remainingMs / 1000))));
500
+ const serialized = tags.length === 0 ? undefined : tags.join(",");
501
+ if (serialized !== undefined)
502
+ headers.set(ISR_REVALIDATE_TAGS_HEADER, serialized);
503
+ };
504
+ const served = (entry, status) => {
505
+ const res = responseFrom(entry, status);
506
+ advertise(res.headers, status === "stale" ? 0 : entry.revalidate - (now() - entry.storedAt), entry.tags ?? []);
507
+ return res;
508
+ };
509
+ if (app[DOCUMENT_POLICY] === "nonce") {
510
+ console.warn("[nifra/web] withISR wraps an app whose every document carries a CSP nonce. Such documents are " +
511
+ "`private, no-store`, so this cache will never store a page. Use " +
512
+ "`createWebApp({ csp: createCspPolicy(...) })` instead of `nonce` to cache pages under a CSP.");
513
+ }
514
+ // Keys whose last render declared itself `private` or `no-store`, with when that memo expires. A
515
+ // remembered key skips the store lookup (a network round trip on a shared store) and renders fresh;
516
+ // a render that turns out cacheable is stored and forgets the key. So the memo can only cost a cache
517
+ // hit, never serve the wrong page. Bounded, oldest first, so a URL flood cannot grow it.
518
+ const uncacheable = new Map();
519
+ const rememberUncacheable = (key, res) => {
520
+ if (options.revalidate === 0 || !cacheControlHas(res.headers, ["private", "no-store"]))
521
+ return;
522
+ if (uncacheable.size >= MAX_UNCACHEABLE_KEYS) {
523
+ uncacheable.delete(uncacheable.keys().next().value);
524
+ }
525
+ uncacheable.set(key, now() + options.revalidate * 1000);
526
+ };
391
527
  // Per-page TTL (ms): the app's `x-nifra-isr-revalidate` header (seconds) if present, else the default.
392
528
  const ttlMs = (res) => {
393
529
  const header = res.headers.get(ISR_REVALIDATE_HEADER);
@@ -397,8 +533,11 @@ export function withISR(app, options) {
397
533
  };
398
534
  const render = async (req, platform, key) => {
399
535
  const res = await app.fetch(req, platform);
400
- if (!isCacheablePage(req, res))
401
- return res;
536
+ if (!isCacheablePage(req, res)) {
537
+ rememberUncacheable(key, res);
538
+ return withoutCacheChannel(res);
539
+ }
540
+ uncacheable.delete(key);
402
541
  const body = await res.text();
403
542
  const tags = tagsOf(res);
404
543
  const entry = {
@@ -410,10 +549,12 @@ export function withISR(app, options) {
410
549
  ...(tags.length === 0 ? {} : { tags }),
411
550
  };
412
551
  await store.set(key, entry);
413
- return new Response(body, {
552
+ const fresh = new Response(body, {
414
553
  status: res.status,
415
554
  headers: { ...entry.headers, [ISR_STATUS_HEADER]: "miss" },
416
555
  });
556
+ advertise(fresh.headers, entry.revalidate, tags);
557
+ return fresh;
417
558
  };
418
559
  // Background regeneration (single-flight per key) - a failed regen keeps the stale entry; the next
419
560
  // stale hit retries. Never throws (it runs detached / under waitUntil).
@@ -451,46 +592,75 @@ export function withISR(app, options) {
451
592
  regenerating.delete(key);
452
593
  }
453
594
  };
454
- return async (req, platform) => {
595
+ const handler = async (req, platform) => {
455
596
  // A data-mode soft-nav GET bypasses the cache entirely: entries are full HTML documents keyed
456
597
  // by URL, and serving one to a loader-data fetch would hand the client HTML where it expects
457
598
  // the loader payload. (The write path already refuses to cache data-mode responses.)
458
599
  const key = req.method === "GET" && req.headers.get("x-nifra-data") === null ? keyOf(req) : null;
459
600
  if (key === null)
460
- return app.fetch(req, platform);
601
+ return withoutCacheChannel(await app.fetch(req, platform));
461
602
  // Draft/preview: an editor (valid signed cookie) always renders fresh and is never cached, so
462
603
  // unpublished content can't leak into the public cache (and the editor isn't served a stale page).
463
604
  if (draftSecret !== undefined && (await isDraftEnabled(req, draftSecret))) {
464
- return app.fetch(req, platform);
605
+ return withoutCacheChannel(await app.fetch(req, platform));
606
+ }
607
+ const until = uncacheable.get(key);
608
+ if (until !== undefined) {
609
+ if (now() < until)
610
+ return render(req, platform, key);
611
+ uncacheable.delete(key);
465
612
  }
466
613
  const hit = await store.get(key);
467
614
  if (hit !== undefined) {
468
615
  if (now() - hit.storedAt < hit.revalidate)
469
- return responseFrom(hit, "hit");
616
+ return served(hit, "hit");
470
617
  // Stale: serve it now, regenerate behind it (waitUntil keeps the edge worker alive for the regen).
471
618
  const task = regenerate(req, platform, key);
472
619
  if (typeof platform?.waitUntil === "function")
473
620
  platform.waitUntil(task);
474
621
  else
475
622
  void task.catch(() => { });
476
- return responseFrom(hit, "stale");
623
+ return served(hit, "stale");
477
624
  }
478
625
  return render(req, platform, key);
479
626
  };
627
+ Object.defineProperty(handler, CACHE_CHANNEL, {
628
+ value: () => {
629
+ channelOpen = true;
630
+ },
631
+ });
632
+ return handler;
480
633
  }
481
634
  const jsonError = (status, error) => Response.json({ ok: false, error }, { status });
635
+ /** Most paths and tags one revalidation request may name, so a leaked token cannot fan out. */
636
+ export const MAX_REVALIDATE_PATHS = 100;
637
+ export const MAX_REVALIDATE_TAGS = MAX_ISR_TAGS;
638
+ const stringList = (value) => {
639
+ if (value === undefined)
640
+ return undefined;
641
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string"))
642
+ return null;
643
+ return value;
644
+ };
482
645
  /**
483
- * An **on-demand revalidation** (purge) endpoint - a `fetch` handler that drops a path's cached entry
646
+ * An **on-demand revalidation** (purge) endpoint - a `fetch` handler that drops cached pages by path
484
647
  * or invalidates every entry carrying a tag. `POST` with the secret in the token header and either
485
- * `?path=/blog/x`, `?tag=products`, or a JSON `{ "path": "/blog/x" }` / `{ "tag": "products" }` body.
648
+ * `?path=/blog/x`, `?tag=products`, a JSON `{ "path": "/blog/x" }` / `{ "tag": "products" }` body, or a
649
+ * batch `{ "paths": [...], "tags": [...] }` of at most 100 paths and 32 tags.
486
650
  * The token is checked in **constant time** (wrong/missing → `401`); malformed targets → `400`;
487
651
  * non-POST → `405`. A store without tag support returns `501` for tag requests. Mount it on a nifra route, e.g.
488
652
  * `app.post("/__nifra/revalidate", (c) => handler(c.req))`.
489
653
  */
490
654
  export function revalidateEndpoint(options) {
655
+ // An empty secret matches a request that omits the header (`"" === ""`), so an unset env var passed
656
+ // through `?? ""` would open the purge to anyone. Refuse it at construction instead.
657
+ assertTokenSecret(options.secret, "revalidateEndpoint");
491
658
  const tokenHeader = options.tokenHeader ?? "x-nifra-revalidate-token";
492
- const keyOf = options.key ?? ((path, req) => new URL(req.url).origin + path);
493
- return async (req) => {
659
+ const urlKey = options.key === undefined ? urlKeyOf(options.query) : undefined;
660
+ // Joined as text, never resolved against the origin: `//host/x` must stay a path on this origin.
661
+ const keyOf = options.key ??
662
+ ((path, req) => urlKey?.(new URL(new URL(req.url).origin + path)) ?? null);
663
+ return async (req, platform) => {
494
664
  if (req.method !== "POST")
495
665
  return jsonError(405, "method_not_allowed");
496
666
  if (!timingSafeEqual(req.headers.get(tokenHeader) ?? "", options.secret)) {
@@ -499,33 +669,90 @@ export function revalidateEndpoint(options) {
499
669
  const url = new URL(req.url);
500
670
  let path = url.searchParams.get("path");
501
671
  let tag = url.searchParams.get("tag");
672
+ let batch;
502
673
  if (path === null && tag === null) {
503
674
  const body = await req.json().catch(() => null);
504
675
  if (typeof body === "object" && body !== null) {
505
676
  const candidate = body;
506
- path = typeof candidate.path === "string" ? candidate.path : null;
507
- tag = typeof candidate.tag === "string" ? candidate.tag : null;
677
+ if (candidate.paths !== undefined || candidate.tags !== undefined) {
678
+ const paths = stringList(candidate.paths);
679
+ const tags = stringList(candidate.tags);
680
+ if (paths === null || tags === null)
681
+ return jsonError(400, "invalid_batch");
682
+ if ((paths?.length ?? 0) > MAX_REVALIDATE_PATHS)
683
+ return jsonError(400, "too_many_paths");
684
+ if ((tags?.length ?? 0) > MAX_REVALIDATE_TAGS)
685
+ return jsonError(400, "too_many_tags");
686
+ batch = { paths: paths ?? [], tags: tags ?? [] };
687
+ if (batch.paths.length + batch.tags.length === 0)
688
+ return jsonError(400, "empty_batch");
689
+ }
690
+ else {
691
+ path = typeof candidate.path === "string" ? candidate.path : null;
692
+ tag = typeof candidate.tag === "string" ? candidate.tag : null;
693
+ }
508
694
  }
509
695
  }
510
- if (path !== null && tag !== null)
511
- return jsonError(400, "choose_path_or_tag");
512
- if (tag !== null) {
696
+ if (batch === undefined) {
697
+ if (path !== null && tag !== null)
698
+ return jsonError(400, "choose_path_or_tag");
699
+ if (path === null && tag === null)
700
+ return jsonError(400, "invalid_path");
701
+ batch = { paths: path === null ? [] : [path], tags: tag === null ? [] : [tag] };
702
+ }
703
+ const tags = [...new Set(batch.tags)];
704
+ for (const candidate of tags) {
513
705
  try {
514
- normalizeTag(tag);
706
+ normalizeTag(candidate);
515
707
  }
516
708
  catch {
517
709
  return jsonError(400, "invalid_tag");
518
710
  }
519
- if (options.store.invalidateTag === undefined) {
520
- return jsonError(501, "tag_invalidation_not_supported");
521
- }
522
- await options.store.invalidateTag(tag);
523
- return Response.json({ revalidatedTag: tag });
524
711
  }
525
- if (path === null || !path.startsWith("/"))
526
- return jsonError(400, "invalid_path");
527
- await options.store.delete(keyOf(path, req));
528
- return Response.json({ revalidated: path });
712
+ const paths = [...new Set(batch.paths)];
713
+ const keys = [];
714
+ for (const candidate of paths) {
715
+ if (!candidate.startsWith("/"))
716
+ return jsonError(400, "invalid_path");
717
+ const key = keyOf(candidate, req);
718
+ if (key === null)
719
+ return jsonError(400, "uncached_query");
720
+ keys.push(key);
721
+ }
722
+ const invalidateTag = options.store.invalidateTag;
723
+ if (tags.length > 0 && invalidateTag === undefined) {
724
+ return jsonError(501, "tag_invalidation_not_supported");
725
+ }
726
+ for (const key of keys)
727
+ await options.store.delete(key);
728
+ for (const candidate of tags)
729
+ await invalidateTag?.call(options.store, candidate);
730
+ const done = path !== null
731
+ ? { revalidated: path }
732
+ : tag !== null
733
+ ? { revalidatedTag: tag }
734
+ : { revalidated: paths, revalidatedTags: tags };
735
+ if (options.cdn === undefined)
736
+ return Response.json(done);
737
+ let outcome;
738
+ try {
739
+ outcome = await options.cdn.purge({ tags, paths }, platform);
740
+ }
741
+ catch {
742
+ outcome = { cdn: "failed", retryable: true, error: "cdn_purge_threw" };
743
+ }
744
+ if (outcome.cdn === "failed") {
745
+ const failure = {
746
+ ok: false,
747
+ error: "cdn_purge_failed",
748
+ origin: "done",
749
+ retryable: outcome.retryable,
750
+ };
751
+ if (outcome.error !== undefined)
752
+ failure.reason = outcome.error;
753
+ return Response.json(failure, { status: 502 });
754
+ }
755
+ return Response.json({ ...done, cdn: outcome.cdn }, { status: outcome.cdn === "queued" ? 202 : 200 });
529
756
  };
530
757
  }
531
758
  //# sourceMappingURL=isr.js.map