@timber-js/app 0.2.0-alpha.207 → 0.2.0-alpha.209

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 (526) hide show
  1. package/dist/_chunks/{primitives-DLAnvsrk.js → actions-BerlqoXA.js} +640 -410
  2. package/dist/_chunks/actions-BerlqoXA.js.map +1 -0
  3. package/dist/_chunks/{als-registry-C6kcfprT.js → als-registry-DaxkVjt5.js} +6 -2
  4. package/dist/_chunks/als-registry-DaxkVjt5.js.map +1 -0
  5. package/dist/_chunks/{build-manifest-DTmSGLRz.js → build-manifest-DWppEdLB.js} +2 -51
  6. package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
  7. package/dist/_chunks/{build-output-helper-9dqCv1pZ.js → build-output-helper-RV_BRcF1.js} +50 -53
  8. package/dist/_chunks/build-output-helper-RV_BRcF1.js.map +1 -0
  9. package/dist/_chunks/{cache-api-CUD9Ezaq.js → cache-api-CR23J_NC.js} +64 -32
  10. package/dist/_chunks/cache-api-CR23J_NC.js.map +1 -0
  11. package/dist/_chunks/{chains-B4bgOl2g.js → chains-CpFg56UB.js} +2 -2
  12. package/dist/_chunks/{chains-B4bgOl2g.js.map → chains-CpFg56UB.js.map} +1 -1
  13. package/dist/_chunks/{cli-check-G8o30ldg.js → cli-check-C6Ev6wBO.js} +11 -52
  14. package/dist/_chunks/cli-check-C6Ev6wBO.js.map +1 -0
  15. package/dist/_chunks/cli-context-By7k2xVo.js +70 -0
  16. package/dist/_chunks/cli-context-By7k2xVo.js.map +1 -0
  17. package/dist/_chunks/{cli-schema-sync--LZKs8Zn.js → cli-schema-sync-CbT2AUUI.js} +3 -3
  18. package/dist/_chunks/{cli-schema-sync--LZKs8Zn.js.map → cli-schema-sync-CbT2AUUI.js.map} +1 -1
  19. package/dist/_chunks/{cloudflare-BFb__LYG.js → cloudflare-Byxl8FWP.js} +96 -53
  20. package/dist/_chunks/{cloudflare-BFb__LYG.js.map → cloudflare-Byxl8FWP.js.map} +1 -1
  21. package/dist/_chunks/{convention-lint-5KzoMg_A.js → convention-lint-jKTwKwPe.js} +21 -6
  22. package/dist/_chunks/convention-lint-jKTwKwPe.js.map +1 -0
  23. package/dist/_chunks/{dev-server-BzBxgYAV.js → dev-server-C4WZdB7L.js} +15 -167
  24. package/dist/_chunks/dev-server-C4WZdB7L.js.map +1 -0
  25. package/dist/_chunks/{error-boundary-BndF-3Td.js → error-boundary-tA7kVfs4.js} +84 -104
  26. package/dist/_chunks/error-boundary-tA7kVfs4.js.map +1 -0
  27. package/dist/_chunks/{fs-identity-D7vb6GdI.js → fs-identity-BRKuL215.js} +3 -4
  28. package/dist/_chunks/fs-identity-BRKuL215.js.map +1 -0
  29. package/dist/_chunks/{graph-cache-CP4GEmf9.js → graph-cache-CYiUS2aS.js} +4 -5
  30. package/dist/_chunks/graph-cache-CYiUS2aS.js.map +1 -0
  31. package/dist/_chunks/{json-lossy-check-ClNvBM_3.js → json-lossy-check-ip0Qi0MT.js} +6 -3
  32. package/dist/_chunks/json-lossy-check-ip0Qi0MT.js.map +1 -0
  33. package/dist/_chunks/{live-graph-Bm5WTvcr.js → live-graph-Dv-JJCZw.js} +3 -3
  34. package/dist/_chunks/{live-graph-Bm5WTvcr.js.map → live-graph-Dv-JJCZw.js.map} +1 -1
  35. package/dist/_chunks/{logger-k2DQ4EUf.js → logger-DiDt5ppH.js} +192 -51
  36. package/dist/_chunks/logger-DiDt5ppH.js.map +1 -0
  37. package/dist/_chunks/{navigation-root-mHSK9psY.js → navigation-root-BQfo1-kG.js} +2 -2
  38. package/dist/_chunks/{navigation-root-mHSK9psY.js.map → navigation-root-BQfo1-kG.js.map} +1 -1
  39. package/dist/_chunks/{plugin-context-8idfR3UZ.js → plugin-context-BPBPc3lL.js} +28 -4
  40. package/dist/_chunks/plugin-context-BPBPc3lL.js.map +1 -0
  41. package/dist/_chunks/{poison-scan-DG5eI2_z.js → poison-scan-CpeT6_OJ.js} +2 -2
  42. package/dist/_chunks/{poison-scan-DG5eI2_z.js.map → poison-scan-CpeT6_OJ.js.map} +1 -1
  43. package/dist/_chunks/render-timeout-DOf_BBcO.js +46 -0
  44. package/dist/_chunks/render-timeout-DOf_BBcO.js.map +1 -0
  45. package/dist/_chunks/{rsc-payload-path-B_LBodc2.js → rsc-payload-path-dffcs4jp.js} +9 -3
  46. package/dist/_chunks/{rsc-payload-path-B_LBodc2.js.map → rsc-payload-path-dffcs4jp.js.map} +1 -1
  47. package/dist/_chunks/{scanner-BQ5sPIJ7.js → scanner-Bw0oq1HB.js} +2 -2
  48. package/dist/_chunks/{scanner-BQ5sPIJ7.js.map → scanner-Bw0oq1HB.js.map} +1 -1
  49. package/dist/_chunks/{navigation-context-D0TU0Jog.js → segment-context-xtPUGfdq.js} +36 -34
  50. package/dist/_chunks/segment-context-xtPUGfdq.js.map +1 -0
  51. package/dist/_chunks/{singleflight-2lUWfcAk.js → singleflight-BeVMMKi0.js} +4 -3
  52. package/dist/_chunks/{singleflight-2lUWfcAk.js.map → singleflight-BeVMMKi0.js.map} +1 -1
  53. package/dist/_chunks/{sizeof-lXffQ1Vr.js → sizeof-UwzwB1uM.js} +3 -2
  54. package/dist/_chunks/sizeof-UwzwB1uM.js.map +1 -0
  55. package/dist/_chunks/ssr-data-nA3I70_n.js +49 -0
  56. package/dist/_chunks/ssr-data-nA3I70_n.js.map +1 -0
  57. package/dist/_chunks/{state-FippDgxN.js → state-BhNGPsdi.js} +6 -8
  58. package/dist/_chunks/state-BhNGPsdi.js.map +1 -0
  59. package/dist/_chunks/{status-page-marker-DwQBrLBz.js → status-page-marker-gaihi0KZ.js} +140 -28
  60. package/dist/_chunks/status-page-marker-gaihi0KZ.js.map +1 -0
  61. package/dist/_chunks/{walkers-CN2N_bA-.js → walkers-BXExhzzk.js} +2 -2
  62. package/dist/_chunks/{walkers-CN2N_bA-.js.map → walkers-BXExhzzk.js.map} +1 -1
  63. package/dist/_chunks/write-file-atomic-D1qWqHb4.js +32 -0
  64. package/dist/_chunks/write-file-atomic-D1qWqHb4.js.map +1 -0
  65. package/dist/adapters/build-output-helper.d.ts +7 -7
  66. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  67. package/dist/adapters/canonicalize-module.d.ts +8 -0
  68. package/dist/adapters/canonicalize-module.d.ts.map +1 -0
  69. package/dist/adapters/cloudflare-dev.js +1 -1
  70. package/dist/adapters/cloudflare-kv-cache.js +2 -2
  71. package/dist/adapters/cloudflare.d.ts +30 -8
  72. package/dist/adapters/cloudflare.d.ts.map +1 -1
  73. package/dist/adapters/cloudflare.js +2 -2
  74. package/dist/adapters/compress-module.d.ts +8 -0
  75. package/dist/adapters/compress-module.d.ts.map +1 -1
  76. package/dist/adapters/fs-identity.d.ts +2 -3
  77. package/dist/adapters/fs-identity.d.ts.map +1 -1
  78. package/dist/adapters/nitro-presets.d.ts +0 -6
  79. package/dist/adapters/nitro-presets.d.ts.map +1 -1
  80. package/dist/adapters/nitro-preview.d.ts +43 -11
  81. package/dist/adapters/nitro-preview.d.ts.map +1 -1
  82. package/dist/adapters/nitro.d.ts +1 -13
  83. package/dist/adapters/nitro.d.ts.map +1 -1
  84. package/dist/adapters/nitro.js +291 -176
  85. package/dist/adapters/nitro.js.map +1 -1
  86. package/dist/adapters/shared.d.ts +7 -2
  87. package/dist/adapters/shared.d.ts.map +1 -1
  88. package/dist/adapters/types.d.ts +105 -21
  89. package/dist/adapters/types.d.ts.map +1 -1
  90. package/dist/adapters/types.js +0 -0
  91. package/dist/analyze/crawl-entry.js +3 -3
  92. package/dist/analyze/graph-cache.d.ts.map +1 -1
  93. package/dist/analyze/graph-command.js +4 -4
  94. package/dist/cache/index.js +2 -2
  95. package/dist/cache/json-lossy-check.d.ts.map +1 -1
  96. package/dist/cache/singleflight.d.ts.map +1 -1
  97. package/dist/cache/sizeof.d.ts.map +1 -1
  98. package/dist/cache/stores/memory.js +1 -1
  99. package/dist/cache/tag-aware-handler.d.ts.map +1 -1
  100. package/dist/cdn/workers-cache-purge.js +1 -1
  101. package/dist/cli-check.d.ts +7 -37
  102. package/dist/cli-check.d.ts.map +1 -1
  103. package/dist/cli-context.d.ts +54 -0
  104. package/dist/cli-context.d.ts.map +1 -0
  105. package/dist/cli.d.ts +9 -9
  106. package/dist/cli.d.ts.map +1 -1
  107. package/dist/cli.js +20 -24
  108. package/dist/cli.js.map +1 -1
  109. package/dist/client/browser-dev.d.ts +1 -1
  110. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  111. package/dist/client/browser-entry/hydrate.d.ts +19 -29
  112. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  113. package/dist/client/browser-entry/index.d.ts +7 -7
  114. package/dist/client/browser-entry/post-hydration.d.ts +6 -9
  115. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  116. package/dist/client/browser-entry/router-init.d.ts +6 -11
  117. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  118. package/dist/client/error-boundary.d.ts +4 -3
  119. package/dist/client/error-boundary.d.ts.map +1 -1
  120. package/dist/client/error-boundary.js +1 -1
  121. package/dist/client/error-reconstituter.d.ts +3 -9
  122. package/dist/client/error-reconstituter.d.ts.map +1 -1
  123. package/dist/client/history.d.ts +20 -11
  124. package/dist/client/history.d.ts.map +1 -1
  125. package/dist/client/index.d.ts +0 -1
  126. package/dist/client/index.d.ts.map +1 -1
  127. package/dist/client/index.js +5 -6
  128. package/dist/client/index.js.map +1 -1
  129. package/dist/client/internal.d.ts +3 -3
  130. package/dist/client/internal.d.ts.map +1 -1
  131. package/dist/client/internal.js +95 -108
  132. package/dist/client/internal.js.map +1 -1
  133. package/dist/client/navigation-commit.d.ts +20 -7
  134. package/dist/client/navigation-commit.d.ts.map +1 -1
  135. package/dist/client/navigation-context.d.ts +3 -5
  136. package/dist/client/navigation-context.d.ts.map +1 -1
  137. package/dist/client/react-root.d.ts +3 -2
  138. package/dist/client/react-root.d.ts.map +1 -1
  139. package/dist/client/router-pipeline.d.ts +3 -8
  140. package/dist/client/router-pipeline.d.ts.map +1 -1
  141. package/dist/client/router-types.d.ts +45 -39
  142. package/dist/client/router-types.d.ts.map +1 -1
  143. package/dist/client/router-url.d.ts +24 -0
  144. package/dist/client/router-url.d.ts.map +1 -0
  145. package/dist/client/router.d.ts.map +1 -1
  146. package/dist/client/rsc-fetch.d.ts +8 -4
  147. package/dist/client/rsc-fetch.d.ts.map +1 -1
  148. package/dist/client/segment-cache.d.ts +44 -4
  149. package/dist/client/segment-cache.d.ts.map +1 -1
  150. package/dist/client/ssr-data.d.ts +18 -64
  151. package/dist/client/ssr-data.d.ts.map +1 -1
  152. package/dist/client/state.d.ts +2 -5
  153. package/dist/client/state.d.ts.map +1 -1
  154. package/dist/client/use-pathname.d.ts +1 -1
  155. package/dist/config-types.d.ts +2 -1
  156. package/dist/config-types.d.ts.map +1 -1
  157. package/dist/config-validation.d.ts.map +1 -1
  158. package/dist/cookies/index.js +1 -1
  159. package/dist/dev-tools/dev-page-shell.d.ts +17 -0
  160. package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
  161. package/dist/dev-tools/index.d.ts +1 -1
  162. package/dist/dev-tools/index.d.ts.map +1 -1
  163. package/dist/dev-tools/instrumentation.d.ts +17 -2
  164. package/dist/dev-tools/instrumentation.d.ts.map +1 -1
  165. package/dist/dev-tools/overlay.d.ts +6 -27
  166. package/dist/dev-tools/overlay.d.ts.map +1 -1
  167. package/dist/index.d.ts.map +1 -1
  168. package/dist/index.js +632 -373
  169. package/dist/index.js.map +1 -1
  170. package/dist/plugin-context.d.ts +26 -40
  171. package/dist/plugin-context.d.ts.map +1 -1
  172. package/dist/plugins/build-manifest.d.ts.map +1 -1
  173. package/dist/plugins/build-report.d.ts +15 -2
  174. package/dist/plugins/build-report.d.ts.map +1 -1
  175. package/dist/plugins/dev-server.d.ts.map +1 -1
  176. package/dist/plugins/entries.d.ts.map +1 -1
  177. package/dist/plugins/fonts.d.ts +19 -1
  178. package/dist/plugins/fonts.d.ts.map +1 -1
  179. package/dist/plugins/post-build.d.ts +110 -0
  180. package/dist/plugins/post-build.d.ts.map +1 -0
  181. package/dist/plugins/prebuilt-capture.d.ts +11 -4
  182. package/dist/plugins/prebuilt-capture.d.ts.map +1 -1
  183. package/dist/plugins/prebuilt.d.ts +17 -1
  184. package/dist/plugins/prebuilt.d.ts.map +1 -1
  185. package/dist/plugins/prerender-sugar.d.ts.map +1 -1
  186. package/dist/plugins/react-resolve.d.ts +49 -0
  187. package/dist/plugins/react-resolve.d.ts.map +1 -0
  188. package/dist/plugins/rsc-assets-manifest.d.ts +33 -0
  189. package/dist/plugins/rsc-assets-manifest.d.ts.map +1 -0
  190. package/dist/plugins/shims.d.ts.map +1 -1
  191. package/dist/plugins/static-build.d.ts +32 -9
  192. package/dist/plugins/static-build.d.ts.map +1 -1
  193. package/dist/routing/codegen-write.d.ts +0 -9
  194. package/dist/routing/codegen-write.d.ts.map +1 -1
  195. package/dist/routing/index.js +2 -2
  196. package/dist/routing/segment-codegen.d.ts.map +1 -1
  197. package/dist/server/access-gate.d.ts.map +1 -1
  198. package/dist/server/action-handler.d.ts +5 -4
  199. package/dist/server/action-handler.d.ts.map +1 -1
  200. package/dist/server/actions.d.ts +46 -7
  201. package/dist/server/actions.d.ts.map +1 -1
  202. package/dist/server/als-registry.d.ts +58 -5
  203. package/dist/server/als-registry.d.ts.map +1 -1
  204. package/dist/server/children-interception.d.ts.map +1 -1
  205. package/dist/server/cookie-context.d.ts +2 -23
  206. package/dist/server/cookie-context.d.ts.map +1 -1
  207. package/dist/server/csp-nonce.d.ts +0 -9
  208. package/dist/server/csp-nonce.d.ts.map +1 -1
  209. package/dist/server/csrf.d.ts +27 -4
  210. package/dist/server/csrf.d.ts.map +1 -1
  211. package/dist/server/debug.d.ts +0 -1
  212. package/dist/server/debug.d.ts.map +1 -1
  213. package/dist/server/default-logger.d.ts +2 -1
  214. package/dist/server/default-logger.d.ts.map +1 -1
  215. package/dist/server/deny-boundary.d.ts +12 -6
  216. package/dist/server/deny-boundary.d.ts.map +1 -1
  217. package/dist/server/deny-renderer.d.ts +2 -2
  218. package/dist/server/deny-renderer.d.ts.map +1 -1
  219. package/dist/server/error-boundary-wrapper.d.ts +7 -3
  220. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  221. package/dist/server/fallback-error.d.ts +9 -2
  222. package/dist/server/fallback-error.d.ts.map +1 -1
  223. package/dist/server/head-response.d.ts +4 -4
  224. package/dist/server/html-injectors.d.ts.map +1 -1
  225. package/dist/server/index.d.ts +0 -1
  226. package/dist/server/index.d.ts.map +1 -1
  227. package/dist/server/index.js +3 -4
  228. package/dist/server/index.js.map +1 -1
  229. package/dist/server/instrumentation.d.ts +10 -3
  230. package/dist/server/instrumentation.d.ts.map +1 -1
  231. package/dist/server/internal.d.ts +1 -4
  232. package/dist/server/internal.d.ts.map +1 -1
  233. package/dist/server/internal.js +961 -581
  234. package/dist/server/internal.js.map +1 -1
  235. package/dist/server/json-status-response.d.ts +34 -0
  236. package/dist/server/json-status-response.d.ts.map +1 -0
  237. package/dist/server/logger.d.ts +26 -0
  238. package/dist/server/logger.d.ts.map +1 -1
  239. package/dist/server/param-coercion.d.ts +11 -6
  240. package/dist/server/param-coercion.d.ts.map +1 -1
  241. package/dist/server/pipeline-helpers.d.ts +95 -7
  242. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  243. package/dist/server/pipeline-interception.d.ts +11 -0
  244. package/dist/server/pipeline-interception.d.ts.map +1 -1
  245. package/dist/server/pipeline-outcome.d.ts +11 -1
  246. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  247. package/dist/server/pipeline-phases.d.ts +2 -2
  248. package/dist/server/pipeline-phases.d.ts.map +1 -1
  249. package/dist/server/pipeline.d.ts +23 -35
  250. package/dist/server/pipeline.d.ts.map +1 -1
  251. package/dist/server/prebuilt/capture-render.d.ts +25 -0
  252. package/dist/server/prebuilt/capture-render.d.ts.map +1 -0
  253. package/dist/server/prebuilt/capture-routes.d.ts +37 -0
  254. package/dist/server/prebuilt/capture-routes.d.ts.map +1 -0
  255. package/dist/server/prebuilt/capture-state.d.ts +79 -35
  256. package/dist/server/prebuilt/capture-state.d.ts.map +1 -1
  257. package/dist/server/prebuilt/lifetime.d.ts +38 -0
  258. package/dist/server/prebuilt/lifetime.d.ts.map +1 -0
  259. package/dist/server/prebuilt/slots.d.ts +7 -0
  260. package/dist/server/prebuilt/slots.d.ts.map +1 -1
  261. package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -1
  262. package/dist/server/prebuilt-builder.d.ts.map +1 -1
  263. package/dist/server/prebuilt-runtime.d.ts.map +1 -1
  264. package/dist/server/primitives.d.ts +6 -81
  265. package/dist/server/primitives.d.ts.map +1 -1
  266. package/dist/server/react-cache-bridge.d.ts +59 -0
  267. package/dist/server/react-cache-bridge.d.ts.map +1 -0
  268. package/dist/server/react-cache-scope.d.ts +49 -0
  269. package/dist/server/react-cache-scope.d.ts.map +1 -0
  270. package/dist/server/render-timeout.d.ts +15 -0
  271. package/dist/server/render-timeout.d.ts.map +1 -1
  272. package/dist/server/render-utils.d.ts +9 -0
  273. package/dist/server/render-utils.d.ts.map +1 -1
  274. package/dist/server/request-context.d.ts +26 -5
  275. package/dist/server/request-context.d.ts.map +1 -1
  276. package/dist/server/route-element-builder.d.ts.map +1 -1
  277. package/dist/server/route-handler.d.ts.map +1 -1
  278. package/dist/server/route-matcher.d.ts +7 -0
  279. package/dist/server/route-matcher.d.ts.map +1 -1
  280. package/dist/server/rsc-entry/action-middleware-runner.d.ts +22 -22
  281. package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
  282. package/dist/server/rsc-entry/api-handler.d.ts +7 -0
  283. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  284. package/dist/server/rsc-entry/deny-fallback.d.ts +32 -6
  285. package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
  286. package/dist/server/rsc-entry/error-renderer.d.ts +11 -18
  287. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  288. package/dist/server/rsc-entry/helpers.d.ts +0 -1
  289. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  290. package/dist/server/rsc-entry/index.d.ts +5 -15
  291. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  292. package/dist/server/rsc-entry/render-route.d.ts +5 -6
  293. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  294. package/dist/server/rsc-entry/render-signals.d.ts +65 -0
  295. package/dist/server/rsc-entry/render-signals.d.ts.map +1 -0
  296. package/dist/server/rsc-entry/revalidate-renderer.d.ts +6 -16
  297. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
  298. package/dist/server/rsc-entry/rsc-payload.d.ts +7 -3
  299. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  300. package/dist/server/rsc-entry/rsc-stream.d.ts +8 -53
  301. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  302. package/dist/server/rsc-entry/ssr-bridge.d.ts +2 -2
  303. package/dist/server/rsc-entry/ssr-bridge.d.ts.map +1 -1
  304. package/dist/server/rsc-entry/ssr-renderer.d.ts +9 -4
  305. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  306. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +1 -1
  307. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -1
  308. package/dist/server/signal-identity.d.ts +43 -0
  309. package/dist/server/signal-identity.d.ts.map +1 -0
  310. package/dist/server/slot-resolver.d.ts +4 -3
  311. package/dist/server/slot-resolver.d.ts.map +1 -1
  312. package/dist/server/slot-subtree-contain.d.ts.map +1 -1
  313. package/dist/server/ssr-bridge-types.d.ts +91 -53
  314. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  315. package/dist/server/ssr-entry.d.ts +8 -5
  316. package/dist/server/ssr-entry.d.ts.map +1 -1
  317. package/dist/server/ssr-render.d.ts +26 -40
  318. package/dist/server/ssr-render.d.ts.map +1 -1
  319. package/dist/server/ssr-wrappers.d.ts +1 -1
  320. package/dist/server/static-generator.d.ts.map +1 -1
  321. package/dist/server/stream-utils.d.ts +12 -0
  322. package/dist/server/stream-utils.d.ts.map +1 -1
  323. package/dist/server/tracing.d.ts +12 -16
  324. package/dist/server/tracing.d.ts.map +1 -1
  325. package/dist/server/types.d.ts +1 -1
  326. package/dist/server/version-skew.d.ts +3 -3
  327. package/dist/shared/csrf-reject.d.ts +16 -0
  328. package/dist/shared/csrf-reject.d.ts.map +1 -0
  329. package/dist/shared/error-digest.d.ts +28 -0
  330. package/dist/shared/error-digest.d.ts.map +1 -0
  331. package/dist/shared/payload-root.d.ts +11 -10
  332. package/dist/shared/payload-root.d.ts.map +1 -1
  333. package/dist/shared/rsc-payload-path.d.ts +6 -0
  334. package/dist/shared/rsc-payload-path.d.ts.map +1 -1
  335. package/dist/utils/write-file-atomic.d.ts +17 -0
  336. package/dist/utils/write-file-atomic.d.ts.map +1 -0
  337. package/docs/api/30-api-server.mdx +17 -14
  338. package/docs/api/32-api-cache.mdx +51 -23
  339. package/docs/api/34-api-config.mdx +34 -6
  340. package/docs/api/36-cli.mdx +11 -1
  341. package/docs/learn/03-fetching-data.mdx +17 -1
  342. package/docs/learn/04-access-control.mdx +1 -1
  343. package/docs/learn/10-caching.mdx +29 -11
  344. package/docs/learn/11-middleware.mdx +8 -2
  345. package/docs/learn/12-error-handling.mdx +6 -1
  346. package/package.json +7 -3
  347. package/src/adapters/build-output-helper.ts +55 -53
  348. package/src/adapters/canonicalize-module.ts +29 -0
  349. package/src/adapters/cloudflare.ts +105 -63
  350. package/src/adapters/compress-module.ts +16 -5
  351. package/src/adapters/fs-identity.ts +2 -3
  352. package/src/adapters/nitro-presets.ts +0 -15
  353. package/src/adapters/nitro-preview.ts +272 -136
  354. package/src/adapters/nitro.ts +119 -82
  355. package/src/adapters/shared.ts +23 -14
  356. package/src/adapters/types.ts +110 -21
  357. package/src/analyze/graph-cache.ts +3 -4
  358. package/src/cache/json-lossy-check.ts +3 -9
  359. package/src/cache/singleflight.ts +5 -2
  360. package/src/cache/sizeof.ts +3 -1
  361. package/src/cache/tag-aware-handler.ts +43 -26
  362. package/src/cli-check.ts +8 -98
  363. package/src/cli-context.ts +119 -0
  364. package/src/cli.ts +19 -28
  365. package/src/client/browser-dev.ts +1 -1
  366. package/src/client/browser-entry/action-dispatch.ts +26 -4
  367. package/src/client/browser-entry/hydrate.ts +53 -58
  368. package/src/client/browser-entry/index.ts +16 -27
  369. package/src/client/browser-entry/post-hydration.ts +6 -40
  370. package/src/client/browser-entry/router-init.ts +66 -84
  371. package/src/client/error-boundary.tsx +9 -41
  372. package/src/client/error-reconstituter.tsx +10 -5
  373. package/src/client/history.ts +35 -21
  374. package/src/client/index.ts +0 -1
  375. package/src/client/internal.ts +2 -7
  376. package/src/client/navigation-commit.ts +41 -18
  377. package/src/client/navigation-context.ts +3 -39
  378. package/src/client/react-root.ts +3 -2
  379. package/src/client/router-pipeline.ts +96 -132
  380. package/src/client/router-types.ts +46 -44
  381. package/src/client/router-url.ts +32 -0
  382. package/src/client/router.ts +62 -21
  383. package/src/client/rsc-fetch.ts +117 -155
  384. package/src/client/segment-cache.ts +110 -11
  385. package/src/client/ssr-data.ts +19 -80
  386. package/src/client/state.ts +2 -9
  387. package/src/client/use-pathname.ts +1 -1
  388. package/src/config-types.ts +2 -1
  389. package/src/config-validation.ts +2 -1
  390. package/src/dev-tools/dev-page-shell.ts +18 -0
  391. package/src/dev-tools/index.ts +0 -2
  392. package/src/dev-tools/instrumentation.ts +50 -2
  393. package/src/dev-tools/overlay.ts +8 -238
  394. package/src/index.ts +26 -14
  395. package/src/plugin-context.ts +39 -40
  396. package/src/plugins/build-manifest.ts +4 -11
  397. package/src/plugins/build-report.ts +70 -70
  398. package/src/plugins/dev-server.ts +24 -44
  399. package/src/plugins/entries.ts +2 -1
  400. package/src/plugins/fonts.ts +35 -7
  401. package/src/plugins/post-build.ts +327 -0
  402. package/src/plugins/prebuilt-capture.ts +28 -32
  403. package/src/plugins/prebuilt.ts +54 -39
  404. package/src/plugins/prerender-sugar.ts +0 -4
  405. package/src/plugins/react-resolve.ts +149 -0
  406. package/src/plugins/{adapter-build.ts → rsc-assets-manifest.ts} +20 -109
  407. package/src/plugins/shims.ts +1 -0
  408. package/src/plugins/static-build.ts +42 -74
  409. package/src/routing/codegen-write.ts +25 -3
  410. package/src/routing/segment-codegen.ts +3 -2
  411. package/src/rsc-runtime/vendor-types.d.ts +8 -0
  412. package/src/server/access-gate.tsx +2 -1
  413. package/src/server/action-client.ts +1 -1
  414. package/src/server/action-handler.ts +253 -110
  415. package/src/server/actions.ts +73 -25
  416. package/src/server/als-registry.ts +73 -5
  417. package/src/server/chain-url-parts.ts +1 -1
  418. package/src/server/children-interception.ts +2 -1
  419. package/src/server/cookie-context.ts +18 -48
  420. package/src/server/csp-nonce.ts +0 -12
  421. package/src/server/csrf.ts +140 -8
  422. package/src/server/debug.ts +0 -1
  423. package/src/server/default-logger.ts +2 -1
  424. package/src/server/deny-boundary.ts +25 -13
  425. package/src/server/deny-renderer.ts +30 -97
  426. package/src/server/error-boundary-wrapper.ts +7 -3
  427. package/src/server/fallback-error.ts +26 -11
  428. package/src/server/flight-scripts.ts +1 -1
  429. package/src/server/head-response.ts +4 -4
  430. package/src/server/html-injector-core.ts +2 -2
  431. package/src/server/html-injectors.ts +2 -4
  432. package/src/server/index.ts +0 -4
  433. package/src/server/instrumentation.ts +10 -3
  434. package/src/server/internal.ts +1 -6
  435. package/src/server/json-status-response.ts +105 -0
  436. package/src/server/logger.ts +39 -3
  437. package/src/server/param-coercion.ts +15 -12
  438. package/src/server/pipeline-helpers.ts +257 -12
  439. package/src/server/pipeline-interception.ts +133 -0
  440. package/src/server/pipeline-outcome.ts +103 -31
  441. package/src/server/pipeline-phases.ts +127 -237
  442. package/src/server/pipeline.ts +204 -207
  443. package/src/server/prebuilt/capture-render.ts +118 -0
  444. package/src/server/prebuilt/capture-routes.ts +104 -0
  445. package/src/server/prebuilt/capture-state.ts +155 -48
  446. package/src/server/prebuilt/lifetime.ts +151 -0
  447. package/src/server/prebuilt/slots.ts +14 -2
  448. package/src/server/prebuilt/synthetic-store.ts +3 -0
  449. package/src/server/prebuilt-builder.ts +30 -320
  450. package/src/server/prebuilt-runtime.ts +125 -129
  451. package/src/server/primitives.ts +8 -168
  452. package/src/server/react-cache-bridge.ts +124 -0
  453. package/src/server/react-cache-scope.ts +104 -0
  454. package/src/server/render-timeout.ts +20 -0
  455. package/src/server/render-utils.ts +11 -1
  456. package/src/server/request-context.ts +36 -9
  457. package/src/server/route-element-builder.ts +11 -6
  458. package/src/server/route-handler.ts +36 -23
  459. package/src/server/route-matcher.ts +15 -1
  460. package/src/server/rsc-entry/action-middleware-runner.ts +79 -123
  461. package/src/server/rsc-entry/api-handler.ts +27 -103
  462. package/src/server/rsc-entry/deny-fallback.ts +93 -26
  463. package/src/server/rsc-entry/error-renderer.ts +39 -91
  464. package/src/server/rsc-entry/helpers.ts +2 -4
  465. package/src/server/rsc-entry/index.ts +64 -58
  466. package/src/server/rsc-entry/render-route.ts +15 -80
  467. package/src/server/rsc-entry/render-signals.ts +108 -0
  468. package/src/server/rsc-entry/revalidate-renderer.ts +56 -57
  469. package/src/server/rsc-entry/rsc-payload.ts +28 -61
  470. package/src/server/rsc-entry/rsc-stream.ts +113 -213
  471. package/src/server/rsc-entry/ssr-bridge.ts +2 -2
  472. package/src/server/rsc-entry/ssr-renderer.ts +182 -263
  473. package/src/server/rsc-entry/wrap-action-dispatch.ts +12 -14
  474. package/src/server/signal-identity.ts +112 -0
  475. package/src/server/slot-resolver.ts +18 -18
  476. package/src/server/slot-subtree-contain.ts +2 -1
  477. package/src/server/ssr-bridge-types.ts +92 -56
  478. package/src/server/ssr-entry.ts +87 -111
  479. package/src/server/ssr-render.ts +55 -64
  480. package/src/server/ssr-wrappers.tsx +2 -2
  481. package/src/server/static-generator.ts +7 -42
  482. package/src/server/static-not-found.ts +3 -3
  483. package/src/server/stream-utils.ts +51 -0
  484. package/src/server/tracing.ts +58 -75
  485. package/src/server/types.ts +1 -1
  486. package/src/server/version-skew.ts +5 -5
  487. package/src/shared/csrf-reject.ts +15 -0
  488. package/src/shared/error-digest.ts +46 -0
  489. package/src/shared/payload-root.ts +11 -10
  490. package/src/shared/rsc-payload-path.ts +8 -1
  491. package/src/utils/write-file-atomic.ts +32 -0
  492. package/dist/_chunks/actions-jNrXdzgs.js +0 -346
  493. package/dist/_chunks/actions-jNrXdzgs.js.map +0 -1
  494. package/dist/_chunks/als-registry-C6kcfprT.js.map +0 -1
  495. package/dist/_chunks/build-manifest-DTmSGLRz.js.map +0 -1
  496. package/dist/_chunks/build-output-helper-9dqCv1pZ.js.map +0 -1
  497. package/dist/_chunks/cache-api-CUD9Ezaq.js.map +0 -1
  498. package/dist/_chunks/cli-check-G8o30ldg.js.map +0 -1
  499. package/dist/_chunks/convention-lint-5KzoMg_A.js.map +0 -1
  500. package/dist/_chunks/dev-server-BzBxgYAV.js.map +0 -1
  501. package/dist/_chunks/error-boundary-BndF-3Td.js.map +0 -1
  502. package/dist/_chunks/fs-identity-D7vb6GdI.js.map +0 -1
  503. package/dist/_chunks/graph-cache-CP4GEmf9.js.map +0 -1
  504. package/dist/_chunks/json-lossy-check-ClNvBM_3.js.map +0 -1
  505. package/dist/_chunks/logger-k2DQ4EUf.js.map +0 -1
  506. package/dist/_chunks/navigation-context-D0TU0Jog.js.map +0 -1
  507. package/dist/_chunks/plugin-context-8idfR3UZ.js.map +0 -1
  508. package/dist/_chunks/primitives-DLAnvsrk.js.map +0 -1
  509. package/dist/_chunks/segment-context-D9_89u34.js +0 -34
  510. package/dist/_chunks/segment-context-D9_89u34.js.map +0 -1
  511. package/dist/_chunks/sizeof-lXffQ1Vr.js.map +0 -1
  512. package/dist/_chunks/ssr-data-D6T6Y3ef.js +0 -60
  513. package/dist/_chunks/ssr-data-D6T6Y3ef.js.map +0 -1
  514. package/dist/_chunks/state-FippDgxN.js.map +0 -1
  515. package/dist/_chunks/status-page-marker-DwQBrLBz.js.map +0 -1
  516. package/dist/client/types.d.ts +0 -14
  517. package/dist/client/types.d.ts.map +0 -1
  518. package/dist/plugins/adapter-build.d.ts +0 -38
  519. package/dist/plugins/adapter-build.d.ts.map +0 -1
  520. package/dist/server/asset-headers.d.ts +0 -32
  521. package/dist/server/asset-headers.d.ts.map +0 -1
  522. package/dist/server/dev-source-map.d.ts +0 -22
  523. package/dist/server/dev-source-map.d.ts.map +0 -1
  524. package/src/client/types.ts +0 -21
  525. package/src/server/asset-headers.ts +0 -59
  526. package/src/server/dev-source-map.ts +0 -31
@@ -1,22 +1,36 @@
1
+ import { r as RenderTimeoutError, t as DEFAULT_RENDER_TIMEOUT_MS } from "../_chunks/render-timeout-DOf_BBcO.js";
1
2
  import { n as isRscContentType, r as isRscRequest } from "../_chunks/rsc-media-type-DDc7duTD.js";
2
- import { a as timingAls, r as requestContextAls, t as earlyHintsSenderAls } from "../_chunks/als-registry-C6kcfprT.js";
3
- import { D as getWaitUntil, S as getCookie, T as isDebug, _ as markResponseFlushed, b as setMutableCookieContext, f as applyRequestHeaderOverlay, g as getSegmentParams, h as getSearchParams, n as RedirectSignal, o as isDenySignal, p as getHeader, r as RenderError, s as isRedirectSignal, t as DenySignal, v as runWithRequestContext, w as getSetCookieHeaders, x as setSegmentParams, y as setMatchedSegmentPath } from "../_chunks/primitives-DLAnvsrk.js";
4
- import { i as isRscCacheKeyShape, n as RSC_KEY_PARAM, s as rscCacheKey, t as RSC_KEY_HEADERS } from "../_chunks/rsc-cache-key-ClUiXQnK.js";
5
3
  import { i as getMetadataRouteServePath, n as classifyMetadataRoute, r as getMetadataRouteAutoLink, t as METADATA_ROUTE_CONVENTIONS } from "../_chunks/metadata-routes-DSDjM_hJ.js";
6
4
  import { r as canonicalize } from "../_chunks/canonicalize-CgHoscYO.js";
7
5
  import { n as classifyUrlSegment, r as effectiveUrlSegment } from "../_chunks/segment-classify-C539Pa2O.js";
8
6
  import { t as computeSegmentTreePaths } from "../_chunks/segment-keys-D5hu1hz4.js";
9
- import { C as runWithTraceId, S as replaceTraceId, T as withSpan, a as logProxyError, c as logRequestReceived, d as logSwrRefetchFailed, f as logWaitUntilRejected, h as swallow, i as logMiddlewareShortCircuit, l as logRouteError, m as setLogger, n as logCacheMiss, o as logRenderError, p as logWaitUntilUnsupported, r as logMiddlewareError, s as logRequestCompleted, t as getLogger, u as logSlowRequest, v as generateTraceId, w as setSpanAttribute, x as getTraceId, y as getOtelTraceId } from "../_chunks/logger-k2DQ4EUf.js";
10
- import { a as requestCspNonce, i as collectRouteModulepreloads, n as collectRouteCss, r as collectRouteFonts } from "../_chunks/build-manifest-DTmSGLRz.js";
11
- import { i as coerce, t as executeAction } from "../_chunks/actions-jNrXdzgs.js";
12
- import { a as getManifestEntry, b as wasInvalidatedSince, c as lookupPrebuiltPayload, d as checkVersionSkew, i as storeOverlayEntry, l as setPrebuiltPayloadSource, n as lookupOverlay, o as hasPrebuiltPayloadSource, r as overlayKey, s as isParamIndependent, u as applyReloadHeaders, v as currentInvalidationEpoch, y as lastInvalidationEpochFor } from "../_chunks/cache-api-CUD9Ezaq.js";
13
- import { n as createSingleflight } from "../_chunks/singleflight-2lUWfcAk.js";
7
+ import { a as requestContextAls, r as prebuiltCaptureAls, s as timingAls, t as earlyHintsSenderAls } from "../_chunks/als-registry-DaxkVjt5.js";
8
+ import { A as getTraceId, C as isDebug, D as generateTraceId, M as runWithTraceId, N as setSpanAttribute, O as getOtelTraceId, P as withSpan, S as isRedirectSignal, _ as swallow, a as logMiddlewareShortCircuit, b as isControlFlowSignal, c as logRequestCompleted, d as logRouteSetupError, f as logSlowRequest, g as setLogger, h as logWaitUntilUnsupported, i as logMiddlewareError, j as replaceTraceId, l as logRequestReceived, m as logWaitUntilRejected, n as logCacheMiss, o as logProxyError, p as logSwrRefetchFailed, s as logRenderError, t as getLogger, u as logRouteError, x as isDenySignal } from "../_chunks/logger-DiDt5ppH.js";
9
+ import { i as collectRouteModulepreloads, n as collectRouteCss, r as collectRouteFonts } from "../_chunks/build-manifest-DWppEdLB.js";
10
+ import { i as isRscCacheKeyShape, n as RSC_KEY_PARAM, s as rscCacheKey, t as RSC_KEY_HEADERS } from "../_chunks/rsc-cache-key-ClUiXQnK.js";
11
+ import { C as setMutableCookieContext, S as setMatchedSegmentPath, T as runOutsideReactCacheScope, b as markResponseFlushed, f as getCookie, g as getHeader, h as applyRequestHeaderOverlay, i as coerce, m as getSetCookieHeaders, o as DenySignal, s as RedirectSignal, t as executeAction, v as getSearchParams, w as setSegmentParams, x as runWithRequestContext, y as getSegmentParams } from "../_chunks/actions-BerlqoXA.js";
12
+ import { a as getManifestEntry, b as wasInvalidatedSince, c as lookupPrebuiltPayload, d as checkVersionSkew, i as storeOverlayEntry, l as setPrebuiltPayloadSource, n as lookupOverlay, o as hasPrebuiltPayloadSource, r as overlayKey, s as isParamIndependent, u as applyReloadHeaders, v as currentInvalidationEpoch, x as getWaitUntil, y as lastInvalidationEpochFor } from "../_chunks/cache-api-CR23J_NC.js";
13
+ import { n as createSingleflight } from "../_chunks/singleflight-BeVMMKi0.js";
14
14
  import { n as toBracketKey } from "../_chunks/resolve-schema-CBR6Lm4i.js";
15
15
  import { t as normalizeParamValue } from "../_chunks/param-value-C8TNYchQ.js";
16
- import { s as TIMBER_ERROR_HEADER, t as StatusPageMarker } from "../_chunks/status-page-marker-DwQBrLBz.js";
16
+ import { l as TIMBER_ERROR_HEADER, t as StatusPageMarker } from "../_chunks/status-page-marker-gaihi0KZ.js";
17
17
  import { createHash } from "node:crypto";
18
18
  import { readFile } from "node:fs/promises";
19
19
  import React, { createElement, useContext } from "react";
20
+ //#region src/server/render-utils.ts
21
+ /**
22
+ * Check if an error is an abort error (connection closed by client).
23
+ *
24
+ * When the browser aborts a request (page refresh, navigation away),
25
+ * the AbortSignal fires and React/streams throw an AbortError. This
26
+ * is not an application error — suppress it from error boundaries and logs.
27
+ */
28
+ function isAbortError(error) {
29
+ if (error instanceof DOMException && error.name === "AbortError") return true;
30
+ if (error instanceof Error && error.name === "AbortError") return true;
31
+ return false;
32
+ }
33
+ //#endregion
20
34
  //#region src/server/server-timing.ts
21
35
  /**
22
36
  * Server-Timing header — dev-mode timing breakdowns for Chrome DevTools.
@@ -171,6 +185,52 @@ function wrapResponseWithStreamTiming(body, startTime) {
171
185
  }
172
186
  });
173
187
  }
188
+ /**
189
+ * Read a byte stream to completion under one overall deadline and return
190
+ * the concatenated bytes. Every read is raced against the time remaining,
191
+ * and the reader is cancelled on every exit, so a stalled upstream never
192
+ * outlives the deadline. See design/02-rendering-pipeline.md §"Always
193
+ * Timeout-Guard Stream Reads".
194
+ *
195
+ * @param label - Prefix for the timeout error message. Both ways the deadline
196
+ * can pass (between reads, or while a read is pending) reject with the same
197
+ * `${label} did not complete within ${timeoutMs}ms`.
198
+ */
199
+ async function readAllBytesWithDeadline(stream, timeoutMs, label) {
200
+ const reader = stream.getReader();
201
+ const chunks = [];
202
+ let totalLength = 0;
203
+ const deadline = Date.now() + timeoutMs;
204
+ const deadlineError = () => /* @__PURE__ */ new Error(`${label} did not complete within ${timeoutMs}ms`);
205
+ try {
206
+ for (;;) {
207
+ const remaining = deadline - Date.now();
208
+ if (remaining <= 0) throw deadlineError();
209
+ let timer;
210
+ const timeout = new Promise((_, reject) => {
211
+ timer = setTimeout(() => reject(deadlineError()), remaining);
212
+ });
213
+ let result;
214
+ try {
215
+ result = await Promise.race([reader.read(), timeout]);
216
+ } finally {
217
+ clearTimeout(timer);
218
+ }
219
+ if (result.done) break;
220
+ chunks.push(result.value);
221
+ totalLength += result.value.byteLength;
222
+ }
223
+ } finally {
224
+ reader.cancel().catch(() => {});
225
+ }
226
+ const bytes = new Uint8Array(totalLength);
227
+ let offset = 0;
228
+ for (const chunk of chunks) {
229
+ bytes.set(chunk, offset);
230
+ offset += chunk.byteLength;
231
+ }
232
+ return bytes;
233
+ }
174
234
  //#endregion
175
235
  //#region src/server/instrumentation.ts
176
236
  /**
@@ -234,6 +294,63 @@ function hasOnRequestError() {
234
294
  return _onRequestError !== null;
235
295
  }
236
296
  //#endregion
297
+ //#region src/server/safe-load.ts
298
+ /**
299
+ * Custom error class for module load failures.
300
+ *
301
+ * Preserves the original error as `cause` while providing a
302
+ * human-readable message with the file path.
303
+ */
304
+ var ModuleLoadError = class extends Error {
305
+ /** The file path that failed to load. */
306
+ filePath;
307
+ constructor(filePath, cause) {
308
+ const originalMessage = cause instanceof Error ? cause.message : String(cause);
309
+ super(`[timber] Failed to load module ${filePath}\n ${originalMessage}`, { cause });
310
+ this.name = "ModuleLoadError";
311
+ this.filePath = filePath;
312
+ }
313
+ };
314
+ /**
315
+ * Load a route manifest module with enriched error context.
316
+ *
317
+ * On success: returns the module object (same as `loader.load()`).
318
+ * On failure: throws `ModuleLoadError` with file path and original cause.
319
+ *
320
+ * For error rendering paths that need fallthrough instead of throwing,
321
+ * callers should catch at the call site:
322
+ *
323
+ * ```ts
324
+ * // Throwing (default) — route-element-builder, api-handler, etc.
325
+ * const mod = await loadModule(segment.page);
326
+ *
327
+ * // Fallthrough — error-renderer, error-boundary-wrapper
328
+ * const mod = await loadModule(segment.error).catch(() => null);
329
+ * ```
330
+ */
331
+ async function loadModule(loader) {
332
+ try {
333
+ return await loader.load();
334
+ } catch (error) {
335
+ throw new ModuleLoadError(loader.filePath, error);
336
+ }
337
+ }
338
+ //#endregion
339
+ //#region src/server/route-matcher.ts
340
+ /**
341
+ * Whether a matched chain renders as an API route: its leaf has a
342
+ * `route.ts` and no `page.tsx`. The same test `renderRoute` dispatches on,
343
+ * so every answer about a route's format — early hints, deny format, error
344
+ * format, error reporting — agrees with what actually ran.
345
+ */
346
+ function isApiRouteChain(segments) {
347
+ const leaf = segments[segments.length - 1];
348
+ return !!leaf?.route && !leaf.page;
349
+ }
350
+ var SERVE_PATH_TO_CONVENTION = /* @__PURE__ */ new Map();
351
+ for (const [baseName, convention] of Object.entries(METADATA_ROUTE_CONVENTIONS)) if (convention.serveExtension) for (const ext of [...convention.staticExtensions, convention.serveExtension]) SERVE_PATH_TO_CONVENTION.set(`${convention.servePath}.${ext}`, baseName);
352
+ else SERVE_PATH_TO_CONVENTION.set(convention.servePath, baseName);
353
+ //#endregion
237
354
  //#region src/server/pipeline-helpers.ts
238
355
  /**
239
356
  * Build a proxy resolver closure from the declared source. Called exactly
@@ -269,6 +386,30 @@ function applyCookieJar(headers) {
269
386
  for (const value of getSetCookieHeaders()) headers.append("Set-Cookie", value);
270
387
  }
271
388
  /**
389
+ * Bring a bag the jar was already applied to up to date with the jar.
390
+ *
391
+ * `applied` is what `getSetCookieHeaders()` returned when the jar was
392
+ * applied to `headers`. Values the jar no longer holds (a later write to the
393
+ * same name replaced them) are removed, and values it gained are appended.
394
+ * Every cookie then appears once, with its last write (design/29-cookies.md
395
+ * §"Last-Write-Wins for Same Name"). `Set-Cookie` values that did not come
396
+ * from the jar, such as a middleware appending to `ctx.headers` directly,
397
+ * are left alone.
398
+ */
399
+ function syncCookieJar(headers, applied) {
400
+ const current = getSetCookieHeaders();
401
+ const stale = applied.filter((value) => !current.includes(value));
402
+ const added = current.filter((value) => !applied.includes(value));
403
+ if (stale.length === 0 && added.length === 0) return;
404
+ const kept = headers.getSetCookie();
405
+ for (const value of stale) {
406
+ const index = kept.indexOf(value);
407
+ if (index !== -1) kept.splice(index, 1);
408
+ }
409
+ headers.delete("Set-Cookie");
410
+ for (const value of [...kept, ...added]) headers.append("Set-Cookie", value);
411
+ }
412
+ /**
272
413
  * Merge framework-managed response headers onto a terminal response without
273
414
  * overwriting headers the terminal response already set itself.
274
415
  */
@@ -302,15 +443,31 @@ function cloneWithMutableHeaders(response) {
302
443
  });
303
444
  }
304
445
  /**
446
+ * A fresh response header bag for a page the pipeline renders, seeded with
447
+ * the default dynamic-page cache policy.
448
+ *
449
+ * Without a Cache-Control header, CDNs (particularly Cloudflare) may attempt
450
+ * to buffer/process the response differently, causing intermittent
451
+ * multi-second delays. This matches Next.js's default behavior. Every bag a
452
+ * rendered page can be written onto starts here — a matched route, the
453
+ * no-match 404, and a deny page for a proxy-phase signal — so none of them
454
+ * leaves without a cache policy. Middleware and route handlers may replace it.
455
+ */
456
+ function dynamicResponseHeaders() {
457
+ return new Headers({ "Cache-Control": "private, no-cache, no-store, max-age=0, must-revalidate" });
458
+ }
459
+ /**
305
460
  * Build a redirect Response from a RedirectSignal.
306
461
  *
307
- * For RSC payload requests (client navigation), returns 204 + X-Timber-Redirect
308
- * so the client router can perform a soft SPA redirect. A raw 302 would be
309
- * turned into an opaque redirect by fetch({redirect:'manual'}), crashing
310
- * createFromFetch. See design/19-client-navigation.md.
462
+ * For a client navigation (an RSC payload request for a page), returns 204 +
463
+ * X-Timber-Redirect so the client router can perform a soft SPA redirect. A
464
+ * raw 302 would be turned into an opaque redirect by
465
+ * fetch({redirect:'manual'}), crashing createFromFetch. See
466
+ * design/19-client-navigation.md. The caller decides what counts as a
467
+ * navigation: a route.ts match answers with a real 3xx whatever it accepts.
311
468
  */
312
- function buildRedirectResponse(signal, req, headers) {
313
- if (isRscRequest(req)) {
469
+ function buildRedirectResponse(signal, headers, navigation) {
470
+ if (navigation) {
314
471
  headers.set("X-Timber-Redirect", signal.location);
315
472
  return new Response(null, {
316
473
  status: 204,
@@ -325,25 +482,134 @@ function buildRedirectResponse(signal, req, headers) {
325
482
  }
326
483
  /**
327
484
  * Fire the user's onRequestError hook with request context.
328
- * Extracts request info from the Request object and calls the instrumentation hook.
485
+ *
486
+ * The one place a pipeline phase becomes the `phase` / `routeType` the hook
487
+ * reports, so every caller agrees. A render-phase error on a route.ts match
488
+ * is phase `'route'`, because the handler is what ran. Middleware reports
489
+ * as `'handler'`, its public name.
490
+ */
491
+ async function fireOnRequestError(error, req, source, segments) {
492
+ if (!isReportableError(error) || req.signal?.aborted) return;
493
+ if (isRepeatedLoadFailure(error)) return;
494
+ await callHook(error, req, source, segments);
495
+ }
496
+ /**
497
+ * Module evaluations whose failure was already reported. A module that fails
498
+ * to evaluate rethrows the same error on every later import, and
499
+ * `loadModule()` wraps each one in a new `ModuleLoadError` with that error
500
+ * as its `cause`. One broken module is one bug until it changes, however many
501
+ * loads trip over it in a request or across requests: the page's boundaries,
502
+ * then the fallback page, then the next request. A WeakSet, so a module that
503
+ * is re-evaluated (an HMR edit) gets a new cause and is reported again.
504
+ */
505
+ var reportedLoadFailures = /* @__PURE__ */ new WeakSet();
506
+ /**
507
+ * The same for a module that threw a primitive (`throw 'broken'`), which a
508
+ * WeakSet cannot hold. Keyed on the file and the value instead, so a module
509
+ * re-broken after an edit with the very same value is not reported again.
510
+ * It grows with the number of broken modules, not with requests.
511
+ */
512
+ var reportedPrimitiveLoadFailures = /* @__PURE__ */ new Set();
513
+ function isRepeatedLoadFailure(error) {
514
+ if (!(error instanceof ModuleLoadError)) return false;
515
+ const cause = error.cause;
516
+ if (typeof cause === "object" && cause !== null) {
517
+ if (reportedLoadFailures.has(cause)) return true;
518
+ reportedLoadFailures.add(cause);
519
+ return false;
520
+ }
521
+ const key = `${error.filePath}\0${typeof cause}\0${String(cause)}`;
522
+ if (reportedPrimitiveLoadFailures.has(key)) return true;
523
+ reportedPrimitiveLoadFailures.add(key);
524
+ return false;
525
+ }
526
+ /**
527
+ * Call the hook with request context, with no filtering. Only
528
+ * `fireOnRequestError()` and background reporting call it: background work
529
+ * runs after the response, so its request having aborted says nothing about
530
+ * the failure.
329
531
  */
330
- async function fireOnRequestError(error, req, phase) {
532
+ async function callHook(error, req, source, segments) {
533
+ if (!hasOnRequestError()) return;
331
534
  const url = new URL(req.url);
332
535
  const headersObj = {};
333
536
  req.headers.forEach((v, k) => {
334
537
  headersObj[k] = v;
335
538
  });
539
+ const apiRoute = source === "route" || segments !== void 0 && isApiRouteChain(segments);
336
540
  await callOnRequestError(error, {
337
541
  method: req.method,
338
542
  path: url.pathname,
339
543
  headers: headersObj
340
544
  }, {
341
- phase,
545
+ phase: reportedPhase(source, apiRoute),
342
546
  routePath: url.pathname,
343
- routeType: "page",
547
+ routeType: source === "action" ? "action" : apiRoute ? "route" : "page",
344
548
  traceId: getTraceId()
345
549
  });
346
550
  }
551
+ /**
552
+ * Whether an error is a failure `onRequestError` should hear about.
553
+ *
554
+ * Control-flow signals are outcomes the code chose — a `deny()` picks the
555
+ * page's status and data — and an abort is the client leaving. Callers answering
556
+ * a live request also skip everything once `req.signal` has aborted: a
557
+ * driver that throws "connection closed" after a disconnect is not a bug.
558
+ */
559
+ function isReportableError(error) {
560
+ return !isControlFlowSignal(error) && !isAbortError(error);
561
+ }
562
+ /**
563
+ * Fire the hook without making the caller wait, for callers that cannot:
564
+ * Flight's `onError` is synchronous, and background work has no caller.
565
+ * The promise is registered with the request's `waitUntil` so platforms
566
+ * that stop work when the response ends (Workers, Lambda) let the hook
567
+ * finish. The hook's own failures are caught in `callOnRequestError()`.
568
+ */
569
+ function fireInBackground(report) {
570
+ getWaitUntil()?.(report.catch((hookError) => swallow(hookError, "onRequestError could not be called")));
571
+ }
572
+ /**
573
+ * Run a `cache.component` stale-while-revalidate re-render as a singleflight
574
+ * body, and log and report its failure once for the flight. The stale
575
+ * payload already answered, so nothing else sees the failure.
576
+ *
577
+ * - **Once per flight.** A `.catch` on the flight's promise would run once
578
+ * per request that joined it, so the report happens inside the body. A
579
+ * body still running when the singleflight times out is reported then,
580
+ * with the timeout error (the signal's reason), and its later rejection
581
+ * is dropped.
582
+ * - **Against the request whose stale read started it**, read from the
583
+ * request context the work runs in. With no request (the build-time
584
+ * capture store) it is only logged.
585
+ * - **As phase `'render'`, route type `'page`.** A `cache.component` is a
586
+ * server component, so the read that started this was a page render (a
587
+ * revalidated page tree included). The request's own abort does not
588
+ * suppress the report: the work runs after the response either way.
589
+ */
590
+ async function runBackgroundFlight(signal, work, log) {
591
+ const failed = (error) => {
592
+ log(error);
593
+ const request = requestContextAls.getStore()?.request;
594
+ if (request && isReportableError(error)) fireInBackground(callHook(error, request, "render"));
595
+ };
596
+ const onTimeout = () => failed(signal.reason);
597
+ signal.addEventListener("abort", onTimeout, { once: true });
598
+ try {
599
+ await work();
600
+ } catch (error) {
601
+ if (!signal.aborted) failed(error);
602
+ } finally {
603
+ signal.removeEventListener("abort", onTimeout);
604
+ }
605
+ }
606
+ function reportedPhase(source, apiRoute) {
607
+ switch (source) {
608
+ case "middleware": return "handler";
609
+ case "render": return apiRoute ? "route" : "render";
610
+ default: return source;
611
+ }
612
+ }
347
613
  //#endregion
348
614
  //#region src/server/proxy.ts
349
615
  /**
@@ -399,6 +665,54 @@ async function runMiddlewareChain(chain, ctx) {
399
665
  }
400
666
  }
401
667
  //#endregion
668
+ //#region src/server/csp-nonce.ts
669
+ /**
670
+ * CSP nonce — per-request, generated on demand.
671
+ *
672
+ * timber emits several inline `<script>` tags per HTML response — React's
673
+ * bootstrap script and Fizz runtime, the Flight init/chunk scripts, the
674
+ * segment-tree script, and the post-flush noindex script. Under a strict
675
+ * `Content-Security-Policy` (`script-src 'nonce-…' 'strict-dynamic'`) every
676
+ * one of them needs the request's nonce or the browser refuses to run it.
677
+ *
678
+ * The app opts in by reading `ctx.nonce` in `middleware.ts` and putting it
679
+ * in the CSP header it sets through `ctx.headers`. The first read generates
680
+ * the value and stores it on the request context; the render pipeline reads
681
+ * it back with `getCspNonce()` and stamps it on every inline script. A
682
+ * request whose middleware never reads `ctx.nonce` has no nonce, and its
683
+ * markup is byte-identical to a render without this feature. timber does
684
+ * not parse the CSP header — the app owns the policy, timber owns the
685
+ * markup, and `ctx.nonce` is the one value both are built from.
686
+ *
687
+ * `proxy.ts` cannot participate: it only sees the finished response from
688
+ * `next()`, after the markup exists.
689
+ *
690
+ * See design/13-security.md §"Content Security Policy" (TIM-1414).
691
+ */
692
+ /**
693
+ * Generate a CSP nonce: 128 random bits, base64. Matches CSP's
694
+ * `base64-value` grammar, so it is valid in the header verbatim and safe
695
+ * inside a double-quoted HTML attribute without escaping.
696
+ */
697
+ function generateNonce() {
698
+ const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
699
+ let binary = "";
700
+ for (const b of bytes) binary += String.fromCharCode(b);
701
+ return btoa(binary);
702
+ }
703
+ /**
704
+ * The request's CSP nonce, creating it on first call. Backs the
705
+ * `ctx.nonce` getter on `MiddlewareContext`. Outside a request context
706
+ * (tests, build-time rendering) a fresh value is returned but not
707
+ * retained, since there is no request for it to belong to.
708
+ */
709
+ function requestCspNonce() {
710
+ const store = requestContextAls.getStore();
711
+ if (!store) return generateNonce();
712
+ store.cspNonce ??= generateNonce();
713
+ return store.cspNonce;
714
+ }
715
+ //#endregion
402
716
  //#region src/server/param-coercion.ts
403
717
  /**
404
718
  * Thrown when an app/schema.ts segment-param codec's parse() fails.
@@ -413,7 +727,12 @@ var ParamCoercionError = class extends Error {
413
727
  /** Global schema codecs, set once at startup from virtual:timber-schema. */
414
728
  var _globalCodecs = null;
415
729
  /**
416
- * Run segment param coercion on the matched route's segments.
730
+ * Coerce the matched route's raw segment params and return the result.
731
+ *
732
+ * Reads `match.rawSegmentParams` and never writes to `match`: the caller
733
+ * decides where the coerced params go. Reading the raw record makes the call
734
+ * repeatable — coercing a match twice runs every codec on the URL value both
735
+ * times, never on its own output.
417
736
  *
418
737
  * Codecs come from `app/schema.ts` and nowhere else (TIM-1342 completed
419
738
  * design/41-global-params.md migration step 9). A route with no schema, or
@@ -427,16 +746,15 @@ var _globalCodecs = null;
427
746
  * See design/41-global-params.md §"Pipeline Integration"
428
747
  *
429
748
  * Stays `async` with nothing to await: it is injected as a
430
- * `CoerceSegmentParamsFn` into the revalidation renderer and the action
431
- * middleware runner, both of which await it. Narrowing the return type to
432
- * `void` would break those contracts and remove a microtask boundary the
433
- * pipeline's ordering has always had, for no gain.
749
+ * `CoerceSegmentParamsFn` into the revalidation renderer, which awaits
750
+ * it. Making it synchronous would
751
+ * change that contract and remove a microtask boundary the pipeline's
752
+ * ordering has always had, for no gain.
434
753
  */
435
754
  async function coerceSegmentParams(match) {
436
755
  const globalCodecs = _globalCodecs;
437
756
  const mergeTarget = Object.create(null);
438
- for (const key of Object.keys(match.segmentParams)) if (key !== "__proto__") mergeTarget[key] = match.segmentParams[key];
439
- match.segmentParams = mergeTarget;
757
+ for (const key of Object.keys(match.rawSegmentParams)) if (key !== "__proto__") mergeTarget[key] = match.rawSegmentParams[key];
440
758
  if (globalCodecs) for (const node of match.segments) {
441
759
  const segment = effectiveUrlSegment(node);
442
760
  if (!segment.paramName) continue;
@@ -455,6 +773,7 @@ async function coerceSegmentParams(match) {
455
773
  }
456
774
  mergeTarget[key] = normalizeParamValue(parsed, key);
457
775
  }
776
+ return mergeTarget;
458
777
  }
459
778
  //#endregion
460
779
  //#region src/server/pipeline-metadata.ts
@@ -522,45 +841,48 @@ function escapeXml(str) {
522
841
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
523
842
  }
524
843
  //#endregion
525
- //#region src/server/safe-load.ts
844
+ //#region src/server/state-tree-diff.ts
526
845
  /**
527
- * Custom error class for module load failures.
846
+ * State Tree Diffing — Server-side parsing and diffing of X-Timber-State-Tree.
528
847
  *
529
- * Preserves the original error as `cause` while providing a
530
- * human-readable message with the file path.
531
- */
532
- var ModuleLoadError = class extends Error {
533
- /** The file path that failed to load. */
534
- filePath;
535
- constructor(filePath, cause) {
536
- const originalMessage = cause instanceof Error ? cause.message : String(cause);
537
- super(`[timber] Failed to load module ${filePath}\n ${originalMessage}`, { cause });
538
- this.name = "ModuleLoadError";
539
- this.filePath = filePath;
540
- }
541
- };
542
- /**
543
- * Load a route manifest module with enriched error context.
848
+ * The client sends X-Timber-State-Tree on navigation requests, listing
849
+ * the sync segments it has cached. The server diffs this against the
850
+ * target route's segments to skip re-rendering unchanged sync layouts.
544
851
  *
545
- * On success: returns the module object (same as `loader.load()`).
546
- * On failure: throws `ModuleLoadError` with file path and original cause.
852
+ * This is a performance optimization only — NOT a security boundary.
853
+ * All access.ts files run regardless of the state tree content.
854
+ * A fabricated state tree can only cause extra rendering work or stale
855
+ * layouts — never auth bypass.
547
856
  *
548
- * For error rendering paths that need fallthrough instead of throwing,
549
- * callers should catch at the call site:
857
+ * See design/19-client-navigation.md §"X-Timber-State-Tree Header"
858
+ * See design/13-security.md §"State tree manipulation"
859
+ */
860
+ /**
861
+ * Parse the X-Timber-State-Tree header from a request.
550
862
  *
551
- * ```ts
552
- * // Throwing (default) — route-element-builder, api-handler, etc.
553
- * const mod = await loadModule(segment.page);
863
+ * Returns a Set of segment paths the client has cached (with an
864
+ * additional `slots` property for cached slot paths), or null if
865
+ * the header is missing, malformed, or empty. Parsing happens before
866
+ * renderToReadableStream — not inside the React render pass.
554
867
  *
555
- * // Fallthrough — error-renderer, error-boundary-wrapper
556
- * const mod = await loadModule(segment.error).catch(() => null);
557
- * ```
868
+ * @returns ClientStateTree with segments and slots, or null if no valid state tree
558
869
  */
559
- async function loadModule(loader) {
870
+ function parseClientStateTree(req) {
871
+ const header = req.headers.get("X-Timber-State-Tree");
872
+ if (!header) return null;
560
873
  try {
561
- return await loader.load();
562
- } catch (error) {
563
- throw new ModuleLoadError(loader.filePath, error);
874
+ const parsed = JSON.parse(header);
875
+ const hasSegments = Array.isArray(parsed.segments) && parsed.segments.length > 0;
876
+ const hasSlots = Array.isArray(parsed.slots) && parsed.slots.length > 0;
877
+ const hasTreePaths = Array.isArray(parsed.treePaths) && parsed.treePaths.length > 0;
878
+ if (!hasSegments && !hasSlots && !hasTreePaths) return null;
879
+ const result = new Set(hasSegments ? parsed.segments : []);
880
+ result.slots = hasSlots ? new Set(parsed.slots) : null;
881
+ result.treePaths = hasTreePaths ? new Set(parsed.treePaths) : null;
882
+ return result;
883
+ } catch (err) {
884
+ swallow(err, "malformed X-Timber-State-Tree header");
885
+ return null;
564
886
  }
565
887
  }
566
888
  //#endregion
@@ -679,50 +1001,62 @@ function pathnameMatchesPattern(pathname, pattern) {
679
1001
  }
680
1002
  return pi === pathParts.length;
681
1003
  }
682
- //#endregion
683
- //#region src/server/state-tree-diff.ts
684
1004
  /**
685
- * State Tree Diffing — Server-side parsing and diffing of X-Timber-State-Tree.
1005
+ * Resolve which route renders a soft navigation that an intercepting route
1006
+ * may claim. Returns the canonical match unchanged when no rewrite applies.
686
1007
  *
687
- * The client sends X-Timber-State-Tree on navigation requests, listing
688
- * the sync segments it has cached. The server diffs this against the
689
- * target route's segments to skip re-rendering unchanged sync layouts.
690
- *
691
- * This is a performance optimization only — NOT a security boundary.
692
- * All access.ts files run regardless of the state tree content.
693
- * A fabricated state tree can only cause extra rendering work or stale
694
- * layouts — never auth bypass.
695
- *
696
- * See design/19-client-navigation.md §"X-Timber-State-Tree Header"
697
- * See design/13-security.md §"State tree manipulation"
1008
+ * Runs during route setup, so anything it throws is a `'setup'` outcome.
698
1009
  */
1010
+ function resolveInterceptedRoute(config, req, canonicalPathname, canonicalMatch) {
1011
+ const stripTrailingSlash = config.stripTrailingSlash ?? true;
1012
+ let match = canonicalMatch;
1013
+ let interception;
1014
+ if (isRscRequest(req) && config.interceptionRewrites?.length) {
1015
+ const rawSourceUrl = req.headers.get("X-Timber-URL");
1016
+ const validatedSourceUrl = rawSourceUrl ? validateInterceptionHeader(rawSourceUrl, stripTrailingSlash) : null;
1017
+ if (validatedSourceUrl) {
1018
+ const resolveSourceScopes = () => {
1019
+ const mounted = new Set(parseClientStateTree(req)?.treePaths ?? []);
1020
+ const sourceRoute = config.matchRoute(validatedSourceUrl);
1021
+ const scopes = [mounted, new Set(sourceRoute ? computeSegmentTreePaths(sourceRoute.segments) : [])].filter((scope) => scope.size > 0);
1022
+ return scopes.length > 0 ? scopes : null;
1023
+ };
1024
+ const rewrite = findInterceptionMatch(canonicalPathname, resolveSourceScopes, config.interceptionRewrites);
1025
+ if (rewrite?.mode === "children") {
1026
+ const interceptedMatch = config.matchInterceptedRoute?.(canonicalPathname, rewrite.interceptingScope);
1027
+ if (interceptedMatch) match = interceptedMatch;
1028
+ } else if (rewrite) {
1029
+ const sourceMatch = config.matchRoute(rewrite.interceptingPrefix);
1030
+ if (sourceMatch !== null && computeSegmentTreePaths(sourceMatch.segments).includes(rewrite.interceptingScope)) {
1031
+ match = sourceMatch;
1032
+ interception = { targetPathname: canonicalPathname };
1033
+ }
1034
+ }
1035
+ }
1036
+ }
1037
+ return {
1038
+ match,
1039
+ interception
1040
+ };
1041
+ }
699
1042
  /**
700
- * Parse the X-Timber-State-Tree header from a request.
701
- *
702
- * Returns a Set of segment paths the client has cached (with an
703
- * additional `slots` property for cached slot paths), or null if
704
- * the header is missing, malformed, or empty. Parsing happens before
705
- * renderToReadableStream — not inside the React render pass.
1043
+ * Validate and canonicalize the X-Timber-URL header value.
706
1044
  *
707
- * @returns ClientStateTree with segments and slots, or null if no valid state tree
1045
+ * Returns the canonical pathname if valid, or null if rejected:
1046
+ * - Must start with '/' (relative pathname, no scheme/authority)
1047
+ * - Must not contain control characters
1048
+ * - Must pass canonicalization (no encoded separators, null bytes, etc.)
708
1049
  */
709
- function parseClientStateTree(req) {
710
- const header = req.headers.get("X-Timber-State-Tree");
711
- if (!header) return null;
712
- try {
713
- const parsed = JSON.parse(header);
714
- const hasSegments = Array.isArray(parsed.segments) && parsed.segments.length > 0;
715
- const hasSlots = Array.isArray(parsed.slots) && parsed.slots.length > 0;
716
- const hasTreePaths = Array.isArray(parsed.treePaths) && parsed.treePaths.length > 0;
717
- if (!hasSegments && !hasSlots && !hasTreePaths) return null;
718
- const result = new Set(hasSegments ? parsed.segments : []);
719
- result.slots = hasSlots ? new Set(parsed.slots) : null;
720
- result.treePaths = hasTreePaths ? new Set(parsed.treePaths) : null;
721
- return result;
722
- } catch (err) {
723
- swallow(err, "malformed X-Timber-State-Tree header");
724
- return null;
1050
+ function validateInterceptionHeader(raw, stripTrailingSlash) {
1051
+ if (!raw.startsWith("/")) return null;
1052
+ if (raw.startsWith("//")) return null;
1053
+ for (let i = 0; i < raw.length; i++) {
1054
+ const code = raw.charCodeAt(i);
1055
+ if (code <= 31 || code === 127) return null;
725
1056
  }
1057
+ const result = canonicalize(raw, stripTrailingSlash);
1058
+ if (!result.ok) return null;
1059
+ return result.pathname;
726
1060
  }
727
1061
  //#endregion
728
1062
  //#region src/server/response-cache-policy.ts
@@ -838,13 +1172,78 @@ function rscErrorResponse(isRsc, status, headers) {
838
1172
  * `proxy.ts` that awaits `next()` and then throws, this is the only place
839
1173
  * the cookies downstream middleware set can reach the wire
840
1174
  * (design/29-cookies.md §"proxy.ts Cookie Access"). Middleware-phase
841
- * signals throw before that phase's own application, so they need it too.
1175
+ * signals throw before that phase's own application, so they need it too,
1176
+ * and so do setup-phase outcomes, which come before any phase applied it.
1177
+ *
1178
+ * `fresh` creates the bag for an outcome that arrives without one.
842
1179
  */
843
- function outcomeHeaders(outcome, ctx) {
844
- const headers = ctx.responseHeaders ?? new Headers();
1180
+ function outcomeHeaders(outcome, ctx, fresh = () => new Headers()) {
1181
+ const headers = ctx.responseHeaders ?? fresh();
845
1182
  if (outcome.phase !== "render") applyCookieJar(headers);
846
1183
  return headers;
847
1184
  }
1185
+ /** Whether the outcome is for a route.ts match, which answers in its own format. */
1186
+ function isApiRouteMatch(ctx) {
1187
+ return ctx.match !== void 0 && isApiRouteChain(ctx.match.segments);
1188
+ }
1189
+ /**
1190
+ * Report an error outcome once: log it under its phase, fire
1191
+ * `onRequestError`, and hand it to the dev overlay. An error from a
1192
+ * route.ts handler — or anything else in its render phase — logs as a route
1193
+ * error and reports as phase `'route'`.
1194
+ */
1195
+ async function reportError(config, error, phase, ctx) {
1196
+ const { method, path } = ctx;
1197
+ switch (phase) {
1198
+ case "proxy":
1199
+ logProxyError({ error });
1200
+ break;
1201
+ case "setup":
1202
+ logRouteSetupError({
1203
+ method,
1204
+ path,
1205
+ error
1206
+ });
1207
+ break;
1208
+ case "middleware":
1209
+ logMiddlewareError({
1210
+ method,
1211
+ path,
1212
+ error
1213
+ });
1214
+ break;
1215
+ case "render": if (isApiRouteMatch(ctx)) logRouteError({
1216
+ method,
1217
+ path,
1218
+ error
1219
+ });
1220
+ else logRenderError({
1221
+ method,
1222
+ path,
1223
+ error
1224
+ });
1225
+ }
1226
+ await fireOnRequestError(error, ctx.req, phase, ctx.match?.segments);
1227
+ if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, phase);
1228
+ }
1229
+ /** The outcome for a value a phase threw: a signal, or an error. */
1230
+ function thrownOutcome(phase, thrown) {
1231
+ if (isRedirectSignal(thrown)) return {
1232
+ kind: "redirect",
1233
+ phase,
1234
+ signal: thrown
1235
+ };
1236
+ if (isDenySignal(thrown)) return {
1237
+ kind: "deny",
1238
+ phase,
1239
+ signal: thrown
1240
+ };
1241
+ return {
1242
+ kind: "error",
1243
+ phase,
1244
+ error: thrown
1245
+ };
1246
+ }
848
1247
  /**
849
1248
  * Terminal outcome handler — converts a `PhaseOutcome` into a final
850
1249
  * `Response`, applying cookies, building redirects, rendering deny pages
@@ -873,10 +1272,11 @@ async function outcomeToResponse(config, outcome, ctx) {
873
1272
  }
874
1273
  case "redirect": {
875
1274
  const headers = outcomeHeaders(outcome, ctx);
876
- return buildRedirectResponse(outcome.signal, ctx.req, headers);
1275
+ const navigation = isRscRequest(ctx.req) && !isApiRouteMatch(ctx);
1276
+ return buildRedirectResponse(outcome.signal, headers, navigation);
877
1277
  }
878
1278
  case "deny": {
879
- const headers = outcomeHeaders(outcome, ctx);
1279
+ const headers = outcomeHeaders(outcome, ctx, dynamicResponseHeaders);
880
1280
  if (config.renderDenyFallback) try {
881
1281
  return cloneWithMutableHeaders(await config.renderDenyFallback(outcome.signal, ctx.req, headers, ctx.match));
882
1282
  } catch (denyRenderError) {
@@ -885,51 +1285,31 @@ async function outcomeToResponse(config, outcome, ctx) {
885
1285
  path: ctx.path,
886
1286
  error: denyRenderError
887
1287
  });
888
- await fireOnRequestError(denyRenderError, ctx.req, "render");
1288
+ await fireOnRequestError(denyRenderError, ctx.req, "render", ctx.match?.segments);
889
1289
  if (config.onPipelineError && denyRenderError instanceof Error) config.onPipelineError(denyRenderError, "render");
1290
+ if (isRscRequest(ctx.req)) return rscErrorEnvelope(outcome.signal.status, headers);
890
1291
  }
891
1292
  if (isDebug()) console.warn(`[timber] DenySignal(${outcome.signal.status}) from ${outcome.phase} phase — no renderDenyFallback configured, returning bare ${outcome.signal.status} response\n Request: ${ctx.method} ${ctx.path}\n Add a status-code file (e.g. 404.tsx) or error.tsx to render a custom deny page.`);
892
1293
  return new Response(null, {
893
1294
  status: outcome.signal.status,
894
1295
  headers
895
1296
  });
896
- }
897
- case "error": {
898
- const isRsc = isRscRequest(ctx.req);
899
- if (outcome.phase === "proxy") {
900
- logProxyError({ error: outcome.error });
901
- await fireOnRequestError(outcome.error, ctx.req, "proxy");
902
- if (config.onPipelineError && outcome.error instanceof Error) config.onPipelineError(outcome.error, "proxy");
903
- return rscErrorResponse(isRsc, 500, outcomeHeaders(outcome, ctx));
904
- }
905
- if (outcome.phase === "middleware") {
906
- logMiddlewareError({
907
- method: ctx.method,
908
- path: ctx.path,
909
- error: outcome.error
910
- });
911
- await fireOnRequestError(outcome.error, ctx.req, "handler");
912
- if (config.onPipelineError && outcome.error instanceof Error) config.onPipelineError(outcome.error, "middleware");
913
- return rscErrorResponse(isRsc, 500, outcomeHeaders(outcome, ctx));
914
- }
915
- const headers = outcomeHeaders(outcome, ctx);
916
- logRenderError({
917
- method: ctx.method,
918
- path: ctx.path,
919
- error: outcome.error
920
- });
921
- await fireOnRequestError(outcome.error, ctx.req, "render");
922
- if (config.onPipelineError && outcome.error instanceof Error) config.onPipelineError(outcome.error, "render");
923
- if (isRsc) return rscErrorResponse(true, 500, headers);
1297
+ }
1298
+ case "error": {
1299
+ const isRsc = isRscRequest(ctx.req);
1300
+ const headers = outcomeHeaders(outcome, ctx);
1301
+ await reportError(config, outcome.error, outcome.phase, ctx);
1302
+ if (outcome.phase === "proxy" || outcome.phase === "middleware") return rscErrorResponse(isRsc, 500, headers);
1303
+ if (isRsc && !isApiRouteMatch(ctx)) return rscErrorResponse(true, 500, headers);
924
1304
  if (config.renderFallbackError) try {
925
- return cloneWithMutableHeaders(await config.renderFallbackError(outcome.error, ctx.req, headers));
1305
+ return cloneWithMutableHeaders(await config.renderFallbackError(outcome.error, ctx.req, errorResponseHeaders(headers), ctx.match));
926
1306
  } catch (fallbackRenderError) {
927
1307
  logRenderError({
928
1308
  method: ctx.method,
929
1309
  path: ctx.path,
930
1310
  error: fallbackRenderError
931
1311
  });
932
- await fireOnRequestError(fallbackRenderError, ctx.req, "render");
1312
+ await fireOnRequestError(fallbackRenderError, ctx.req, "render", ctx.match?.segments);
933
1313
  if (config.onPipelineError && fallbackRenderError instanceof Error) config.onPipelineError(fallbackRenderError, "render");
934
1314
  }
935
1315
  return rscErrorResponse(false, 500, headers);
@@ -952,25 +1332,6 @@ async function outcomeToResponse(config, outcome, ctx) {
952
1332
  * See design/07-routing.md §"Request Lifecycle", design/02-rendering-pipeline.md §"Request Flow".
953
1333
  */
954
1334
  /**
955
- * Validate and canonicalize the X-Timber-URL header value.
956
- *
957
- * Returns the canonical pathname if valid, or null if rejected:
958
- * - Must start with '/' (relative pathname, no scheme/authority)
959
- * - Must not contain control characters
960
- * - Must pass canonicalization (no encoded separators, null bytes, etc.)
961
- */
962
- function validateInterceptionHeader(raw, stripTrailingSlash) {
963
- if (!raw.startsWith("/")) return null;
964
- if (raw.startsWith("//")) return null;
965
- for (let i = 0; i < raw.length; i++) {
966
- const code = raw.charCodeAt(i);
967
- if (code <= 31 || code === 127) return null;
968
- }
969
- const result = canonicalize(raw, stripTrailingSlash);
970
- if (!result.ok) return null;
971
- return result.pathname;
972
- }
973
- /**
974
1335
  * Run the proxy.ts phase. Calls user proxy code with a `next()` continuation.
975
1336
  *
976
1337
  * When `next` is provided, it replaces the default `handleRequest` as the
@@ -991,21 +1352,7 @@ async function runProxyPhase(config, getProxy, req, method, path, next) {
991
1352
  response: await withSpan("timber.proxy", {}, () => detailed ? withTiming("proxy", "proxy.ts", proxyFn) : proxyFn())
992
1353
  };
993
1354
  } catch (error) {
994
- if (isRedirectSignal(error)) return {
995
- kind: "redirect",
996
- phase: "proxy",
997
- signal: error
998
- };
999
- if (isDenySignal(error)) return {
1000
- kind: "deny",
1001
- phase: "proxy",
1002
- signal: error
1003
- };
1004
- return {
1005
- kind: "error",
1006
- phase: "proxy",
1007
- error
1008
- };
1355
+ return thrownOutcome("proxy", error);
1009
1356
  }
1010
1357
  }
1011
1358
  /**
@@ -1013,8 +1360,9 @@ async function runProxyPhase(config, getProxy, req, method, path, next) {
1013
1360
  * returns it as a 'response' outcome. Otherwise applies the request header
1014
1361
  * overlay and falls through to the render phase.
1015
1362
  */
1016
- async function runMiddlewarePhase(config, req, match, responseHeaders, requestHeaderOverlay, renderContext) {
1363
+ async function runMiddlewarePhase(config, req, match, responseHeaders, renderContext) {
1017
1364
  const detailed = config.serverTiming === "detailed";
1365
+ const requestHeaderOverlay = new Headers();
1018
1366
  const ctx = {
1019
1367
  req,
1020
1368
  requestHeaders: requestHeaderOverlay,
@@ -1050,23 +1398,9 @@ async function runMiddlewarePhase(config, req, match, responseHeaders, requestHe
1050
1398
  response: middlewareResponse
1051
1399
  };
1052
1400
  applyRequestHeaderOverlay(requestHeaderOverlay);
1053
- return runRenderPhase(config, req, match, responseHeaders, requestHeaderOverlay, renderContext);
1401
+ return runRenderPhase(config, req, match, responseHeaders, renderContext);
1054
1402
  } catch (error) {
1055
- if (isRedirectSignal(error)) return {
1056
- kind: "redirect",
1057
- phase: "middleware",
1058
- signal: error
1059
- };
1060
- if (isDenySignal(error)) return {
1061
- kind: "deny",
1062
- phase: "middleware",
1063
- signal: error
1064
- };
1065
- return {
1066
- kind: "error",
1067
- phase: "middleware",
1068
- error
1069
- };
1403
+ return thrownOutcome("middleware", error);
1070
1404
  }
1071
1405
  }
1072
1406
  /**
@@ -1084,32 +1418,18 @@ async function runMiddlewarePhase(config, req, match, responseHeaders, requestHe
1084
1418
  * while handler `Set-Cookie` values still come after them and take
1085
1419
  * precedence.
1086
1420
  */
1087
- async function runRenderPhase(config, req, match, responseHeaders, requestHeaderOverlay, { canonicalPathname, interception }) {
1421
+ async function runRenderPhase(config, req, match, responseHeaders, { canonicalPathname, interception }) {
1088
1422
  const detailed = config.serverTiming === "detailed";
1089
1423
  applyCookieJar(responseHeaders);
1090
1424
  try {
1091
- const renderFn = () => config.render(req, match, responseHeaders, requestHeaderOverlay, interception);
1425
+ const renderFn = () => config.render(req, match, responseHeaders, interception);
1092
1426
  return {
1093
1427
  kind: "response",
1094
1428
  phase: "render",
1095
1429
  response: await withSpan("timber.render", { "http.route": canonicalPathname }, () => detailed ? withTiming("render", "RSC + SSR render", renderFn) : renderFn())
1096
1430
  };
1097
1431
  } catch (error) {
1098
- if (isDenySignal(error)) return {
1099
- kind: "deny",
1100
- phase: "render",
1101
- signal: error
1102
- };
1103
- if (isRedirectSignal(error)) return {
1104
- kind: "redirect",
1105
- phase: "render",
1106
- signal: error
1107
- };
1108
- return {
1109
- kind: "error",
1110
- phase: "render",
1111
- error
1112
- };
1432
+ return thrownOutcome("render", error);
1113
1433
  }
1114
1434
  }
1115
1435
  /**
@@ -1175,6 +1495,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
1175
1495
  path,
1176
1496
  error
1177
1497
  });
1498
+ await fireOnRequestError(error, req, "route");
1178
1499
  if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, "metadata-route");
1179
1500
  return new Response(null, { status: 500 });
1180
1501
  }
@@ -1188,6 +1509,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
1188
1509
  path,
1189
1510
  error
1190
1511
  });
1512
+ await fireOnRequestError(error, req, "route");
1191
1513
  if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, "auto-sitemap");
1192
1514
  return new Response(null, { status: 500 });
1193
1515
  }
@@ -1202,72 +1524,66 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
1202
1524
  });
1203
1525
  }
1204
1526
  }
1205
- let match = config.matchRoute(canonicalPathname);
1527
+ const responseHeaders = dynamicResponseHeaders();
1528
+ const renderNotFound = async (headers) => {
1529
+ if (!config.renderNoMatch) return new Response(null, { status: 404 });
1530
+ try {
1531
+ return cloneWithMutableHeaders(await config.renderNoMatch(req, headers));
1532
+ } catch (error) {
1533
+ return outcomeToResponse(config, thrownOutcome("render", error), {
1534
+ req,
1535
+ method,
1536
+ path,
1537
+ responseHeaders: headers
1538
+ });
1539
+ }
1540
+ };
1541
+ let match = null;
1206
1542
  let interception;
1207
- if (isRsc && config.interceptionRewrites?.length) {
1208
- const rawSourceUrl = req.headers.get("X-Timber-URL");
1209
- const validatedSourceUrl = rawSourceUrl ? validateInterceptionHeader(rawSourceUrl, stripTrailingSlash) : null;
1210
- if (validatedSourceUrl) {
1211
- const resolveSourceScopes = () => {
1212
- const mounted = new Set(parseClientStateTree(req)?.treePaths ?? []);
1213
- const sourceRoute = config.matchRoute(validatedSourceUrl);
1214
- const scopes = [mounted, new Set(sourceRoute ? computeSegmentTreePaths(sourceRoute.segments) : [])].filter((scope) => scope.size > 0);
1215
- return scopes.length > 0 ? scopes : null;
1543
+ try {
1544
+ match = config.matchRoute(canonicalPathname);
1545
+ ({match, interception} = resolveInterceptedRoute(config, req, canonicalPathname, match));
1546
+ if (!match) {
1547
+ if (isDebug()) console.warn(`[timber] No route matched for pathname: ${canonicalPathname}\n Input path: ${path}\n Method: ${method}`);
1548
+ return await renderNotFound(responseHeaders);
1549
+ }
1550
+ if (config.earlyHints && !isRsc && !isApiRouteChain(match.segments)) try {
1551
+ await config.earlyHints(match, req, responseHeaders);
1552
+ } catch (err) {
1553
+ swallow(err, "early hints hook threw");
1554
+ }
1555
+ try {
1556
+ match = {
1557
+ ...match,
1558
+ segmentParams: await coerceSegmentParams(match)
1216
1559
  };
1217
- const rewrite = findInterceptionMatch(canonicalPathname, resolveSourceScopes, config.interceptionRewrites);
1218
- if (rewrite?.mode === "children") {
1219
- const interceptedMatch = config.matchInterceptedRoute?.(canonicalPathname, rewrite.interceptingScope);
1220
- if (interceptedMatch) match = interceptedMatch;
1221
- } else if (rewrite) {
1222
- const sourceMatch = config.matchRoute(rewrite.interceptingPrefix);
1223
- if (sourceMatch !== null && computeSegmentTreePaths(sourceMatch.segments).includes(rewrite.interceptingScope)) {
1224
- match = sourceMatch;
1225
- interception = { targetPathname: canonicalPathname };
1560
+ } catch (error) {
1561
+ if (error instanceof ParamCoercionError) {
1562
+ if (isDebug()) {
1563
+ const segmentChain = match.segments.map((s) => s.segmentName || "/").join(" → ");
1564
+ console.warn(`[timber] Param coercion failed for ${method} ${canonicalPathname} — responding with 404\n Matched segments: ${segmentChain}\n Error: ${error.message}\n This usually means a codec in app/schema.ts rejected the URL value.\n Check the codec for the bracket key of the segment named above.`);
1226
1565
  }
1566
+ if (isApiRouteChain(match.segments)) return new Response(null, { status: 404 });
1567
+ return await renderNotFound(responseHeaders);
1227
1568
  }
1569
+ throw error;
1228
1570
  }
1229
- }
1230
- if (!match) {
1231
- if (isDebug()) console.warn(`[timber] No route matched for pathname: ${canonicalPathname}\n Input path: ${path}\n Method: ${method}`);
1232
- if (config.renderNoMatch) {
1233
- const responseHeaders = new Headers();
1234
- return cloneWithMutableHeaders(await config.renderNoMatch(req, responseHeaders));
1235
- }
1236
- return new Response(null, { status: 404 });
1237
- }
1238
- const responseHeaders = new Headers();
1239
- const requestHeaderOverlay = new Headers();
1240
- responseHeaders.set("Cache-Control", "private, no-cache, no-store, max-age=0, must-revalidate");
1241
- const leafSegment = match.segments[match.segments.length - 1];
1242
- const isApiRoute = leafSegment?.route && !leafSegment?.page;
1243
- if (config.earlyHints && !isRsc && !isApiRoute) try {
1244
- await config.earlyHints(match, req, responseHeaders);
1245
- } catch (err) {
1246
- swallow(err, "early hints hook threw");
1247
- }
1248
- match.rawSegmentParams = { ...match.segmentParams };
1249
- try {
1250
- await coerceSegmentParams(match);
1571
+ setSegmentParams(match.segmentParams);
1572
+ const segmentPath = match.segments.map((s) => s.segmentName).filter(Boolean).join("/") || "/";
1573
+ setMatchedSegmentPath(segmentPath.startsWith("/") ? segmentPath : `/${segmentPath}`);
1251
1574
  } catch (error) {
1252
- if (error instanceof ParamCoercionError) {
1253
- if (isDebug()) {
1254
- const segmentChain = match.segments.map((s) => s.segmentName || "/").join(" → ");
1255
- console.warn(`[timber] Param coercion failed for ${method} ${canonicalPathname} — responding with 404\n Matched segments: ${segmentChain}\n Error: ${error.message}\n This usually means a codec in app/schema.ts rejected the URL value.\n Check the codec for the bracket key of the segment named above.`);
1256
- }
1257
- const leafSegment = match.segments[match.segments.length - 1];
1258
- if (leafSegment.route && !leafSegment.page) return new Response(null, { status: 404 });
1259
- if (config.renderNoMatch) return cloneWithMutableHeaders(await config.renderNoMatch(req, responseHeaders));
1260
- return new Response(null, { status: 404 });
1261
- }
1262
- throw error;
1575
+ return outcomeToResponse(config, thrownOutcome("setup", error), {
1576
+ req,
1577
+ method,
1578
+ path,
1579
+ responseHeaders,
1580
+ match: match ?? void 0
1581
+ });
1263
1582
  }
1264
- setSegmentParams(match.segmentParams);
1265
- const segmentPath = match.segments.map((s) => s.segmentName).filter(Boolean).join("/") || "/";
1266
- setMatchedSegmentPath(segmentPath.startsWith("/") ? segmentPath : `/${segmentPath}`);
1267
- return outcomeToResponse(config, match.middlewareChain.length > 0 ? await runMiddlewarePhase(config, req, match, responseHeaders, requestHeaderOverlay, {
1583
+ return outcomeToResponse(config, match.middlewareChain.length > 0 ? await runMiddlewarePhase(config, req, match, responseHeaders, {
1268
1584
  canonicalPathname,
1269
1585
  interception
1270
- }) : await runRenderPhase(config, req, match, responseHeaders, requestHeaderOverlay, {
1586
+ }) : await runRenderPhase(config, req, match, responseHeaders, {
1271
1587
  canonicalPathname,
1272
1588
  interception
1273
1589
  }), {
@@ -1396,10 +1712,10 @@ async function enforceRscCacheKey(req, response) {
1396
1712
  *
1397
1713
  * The pipeline applies this at its single exit so every response shape is
1398
1714
  * covered — page render, API route, metadata route, deny page, error page,
1399
- * proxy short-circuit. The two unit-level producers that are also reachable as
1400
- * standalone functions (`handleRouteRequest`, `handleApiRoute`) call it too, so
1401
- * each honours HEAD on its own; they share this implementation rather than
1402
- * repeating it, and applying it twice is a no-op.
1715
+ * proxy short-circuit. The unit-level producer that is also reachable as a
1716
+ * standalone function (`handleRouteRequest`) calls it too, so it honours HEAD
1717
+ * on its own; it shares this implementation rather than repeating it, and
1718
+ * applying it twice is a no-op.
1403
1719
  */
1404
1720
  /**
1405
1721
  * Return a bodyless copy of `response` when `method` is HEAD.
@@ -1441,80 +1757,85 @@ function createPipeline(config) {
1441
1757
  const slowRequestMs = config.slowRequestMs ?? 3e3;
1442
1758
  const serverTiming = config.serverTiming ?? "total";
1443
1759
  let activeRequests = 0;
1444
- const pipelineFn = async (req) => {
1760
+ const pipelineFn = async (req, cookies) => {
1445
1761
  const url = new URL(req.url);
1446
1762
  const method = req.method;
1447
1763
  const path = url.pathname;
1448
1764
  const startTime = performance.now();
1449
- activeRequests++;
1450
1765
  const traceIdValue = generateTraceId();
1451
1766
  return runWithTraceId(traceIdValue, async () => {
1452
- return runWithRequestContext(req, async () => {
1767
+ const inRequestContext = async () => {
1453
1768
  const runRequest = async () => {
1454
1769
  logRequestReceived({
1455
1770
  method,
1456
1771
  path
1457
1772
  });
1458
- const response = await withSpan("http.server.request", {
1459
- "http.request.method": method,
1460
- "url.path": path
1461
- }, async () => {
1462
- const otelIds = await getOtelTraceId();
1463
- if (otelIds) replaceTraceId(otelIds.traceId, otelIds.spanId);
1464
- const stripTrailingSlash = config.stripTrailingSlash ?? true;
1465
- const canonResult = canonicalize(url.pathname, stripTrailingSlash);
1466
- if (!canonResult.ok) {
1467
- if (isDebug()) console.warn(`[timber] URL canonicalization rejected ${method} ${url.pathname} — responding with ${canonResult.status}\n This usually means the URL contains encoded separators (%2f, %5c),\n null bytes (%00), path traversal (..), or malformed percent-encoding.`);
1468
- return new Response(null, { status: canonResult.status });
1469
- }
1470
- const canonicalPath = canonResult.pathname;
1471
- let canonicalReq = req;
1472
- if (url.pathname !== canonicalPath) {
1473
- const canonicalUrl = new URL(req.url);
1474
- canonicalUrl.pathname = canonicalPath;
1475
- canonicalReq = new Request(canonicalUrl.toString(), req);
1476
- }
1477
- const innerHandler = async () => {
1478
- if (config.dispatchAction) {
1479
- const actionResult = await config.dispatchAction(canonicalReq, canonicalPath, pipelineFn);
1480
- if (actionResult) return actionResult;
1773
+ activeRequests++;
1774
+ let concurrency = activeRequests;
1775
+ let response;
1776
+ try {
1777
+ response = await withSpan("http.server.request", {
1778
+ "http.request.method": method,
1779
+ "url.path": path
1780
+ }, async () => {
1781
+ const otelTraceId = await getOtelTraceId();
1782
+ if (otelTraceId) replaceTraceId(otelTraceId);
1783
+ const stripTrailingSlash = config.stripTrailingSlash ?? true;
1784
+ const canonResult = canonicalize(url.pathname, stripTrailingSlash);
1785
+ if (!canonResult.ok) {
1786
+ if (isDebug()) console.warn(`[timber] URL canonicalization rejected ${method} ${url.pathname} — responding with ${canonResult.status}\n This usually means the URL contains encoded separators (%2f, %5c),\n null bytes (%00), path traversal (..), or malformed percent-encoding.`);
1787
+ return new Response(null, { status: canonResult.status });
1481
1788
  }
1482
- return handleRequest(config, canonicalReq, method, canonicalPath, true);
1483
- };
1484
- let result;
1485
- if (proxyResolver) result = await outcomeToResponse(config, await runProxyPhase(config, proxyResolver, canonicalReq, method, canonicalPath, innerHandler), {
1486
- req: canonicalReq,
1487
- method,
1488
- path: canonicalPath
1489
- });
1490
- else result = await innerHandler();
1491
- await setSpanAttribute("http.response.status_code", result.status);
1492
- const varyTokens = [...RSC_KEY_HEADERS];
1493
- const existingVary = result.headers.get("Vary");
1494
- const existingTokens = existingVary ? existingVary.toLowerCase().split(",").map((t) => t.trim()) : [];
1495
- const newTokens = varyTokens.filter((t) => !existingTokens.includes(t.toLowerCase()));
1496
- if (newTokens.length > 0) result.headers.set("Vary", existingVary ? `${existingVary}, ${newTokens.join(", ")}` : newTokens.join(", "));
1497
- await enforceRscCacheKey(canonicalReq, result);
1498
- hardenRscPayloadResponse(result);
1499
- result = stripHeadBody(result, method);
1500
- if (serverTiming === "detailed") {
1501
- const timingHeader = getServerTimingHeader();
1502
- if (timingHeader) result.headers.set("Server-Timing", timingHeader);
1503
- } else if (serverTiming === "total") {
1504
- const totalMs = Math.round(performance.now() - startTime);
1505
- result.headers.set("Server-Timing", `total;dur=${totalMs}`);
1506
- }
1507
- if (serverTiming === "detailed" && result.body) result = new Response(wrapResponseWithStreamTiming(result.body, startTime), {
1508
- status: result.status,
1509
- statusText: result.statusText,
1510
- headers: result.headers
1789
+ const canonicalPath = canonResult.pathname;
1790
+ let canonicalReq = req;
1791
+ if (url.pathname !== canonicalPath) {
1792
+ const canonicalUrl = new URL(req.url);
1793
+ canonicalUrl.pathname = canonicalPath;
1794
+ canonicalReq = new Request(canonicalUrl.toString(), req);
1795
+ }
1796
+ const innerHandler = async () => {
1797
+ if (config.dispatchAction) {
1798
+ const actionResult = await config.dispatchAction(canonicalReq, canonicalPath, pipelineFn);
1799
+ if (actionResult) return actionResult;
1800
+ }
1801
+ return handleRequest(config, canonicalReq, method, canonicalPath, true);
1802
+ };
1803
+ let result;
1804
+ if (proxyResolver) result = await outcomeToResponse(config, await runProxyPhase(config, proxyResolver, canonicalReq, method, canonicalPath, innerHandler), {
1805
+ req: canonicalReq,
1806
+ method,
1807
+ path: canonicalPath
1808
+ });
1809
+ else result = await innerHandler();
1810
+ await setSpanAttribute("http.response.status_code", result.status);
1811
+ const varyTokens = [...RSC_KEY_HEADERS];
1812
+ const existingVary = result.headers.get("Vary");
1813
+ const existingTokens = existingVary ? existingVary.toLowerCase().split(",").map((t) => t.trim()) : [];
1814
+ const newTokens = varyTokens.filter((t) => !existingTokens.includes(t.toLowerCase()));
1815
+ if (newTokens.length > 0) result.headers.set("Vary", existingVary ? `${existingVary}, ${newTokens.join(", ")}` : newTokens.join(", "));
1816
+ await enforceRscCacheKey(canonicalReq, result);
1817
+ hardenRscPayloadResponse(result);
1818
+ result = stripHeadBody(result, method);
1819
+ if (serverTiming === "detailed") {
1820
+ const timingHeader = getServerTimingHeader();
1821
+ if (timingHeader) result.headers.set("Server-Timing", timingHeader);
1822
+ } else if (serverTiming === "total") {
1823
+ const totalMs = Math.round(performance.now() - startTime);
1824
+ result.headers.set("Server-Timing", `total;dur=${totalMs}`);
1825
+ }
1826
+ if (serverTiming === "detailed" && result.body) result = new Response(wrapResponseWithStreamTiming(result.body, startTime), {
1827
+ status: result.status,
1828
+ statusText: result.statusText,
1829
+ headers: result.headers
1830
+ });
1831
+ return result;
1511
1832
  });
1512
- return result;
1513
- });
1833
+ } finally {
1834
+ concurrency = activeRequests;
1835
+ activeRequests--;
1836
+ }
1514
1837
  const durationMs = Math.round(performance.now() - startTime);
1515
1838
  const status = response.status;
1516
- const concurrency = activeRequests;
1517
- activeRequests--;
1518
1839
  logRequestCompleted({
1519
1840
  method,
1520
1841
  path,
@@ -1532,7 +1853,8 @@ function createPipeline(config) {
1532
1853
  return response;
1533
1854
  };
1534
1855
  return serverTiming === "detailed" ? runWithTimingCollector(runRequest) : runRequest();
1535
- });
1856
+ };
1857
+ return runWithRequestContext(req, inRequestContext, { cookies });
1536
1858
  });
1537
1859
  };
1538
1860
  return pipelineFn;
@@ -1691,7 +2013,7 @@ function sendEarlyHints103(links) {
1691
2013
  function ErrorReconstituter({ error: serialized, digest, reset, component, status, data }) {
1692
2014
  const error = Object.assign(new Error(serialized.message), {
1693
2015
  name: serialized.name,
1694
- ...serialized.stack != null ? { stack: serialized.stack } : {}
2016
+ stack: serialized.stack ?? `${serialized.name}: ${serialized.message}`
1695
2017
  });
1696
2018
  return createElement(component, {
1697
2019
  error,
@@ -1865,11 +2187,17 @@ function renderDenyPage(entry, status, data, h) {
1865
2187
  * Otherwise it throws — annotated with `ownerKey` when an ancestor owns the
1866
2188
  * file, bare when nothing matched at all.
1867
2189
  *
1868
- * `setDenyStatus` runs on the hoist path too, and must run *before* the
1869
- * throw: on the document path the hoisted row fails the Fizz shell and the
1870
- * re-render fallback serves the page, and the ALS status is what the
1871
- * pipeline commits for it. The RSC payload path never reads it — a Flight
1872
- * payload response is always 200 and the row itself carries the deny (TIM-1495).
2190
+ * `setDenyStatus` runs on the hoist path too, *before* the throw, for one
2191
+ * case. Normally Flight's `onError` captures the rethrown signal within the
2192
+ * renderer's promotion cap, and the pipeline renders the deny page with
2193
+ * `deny.status` — the recorded status goes unread. When the signal lands
2194
+ * after the cap but within the detection window that follows, the renderer
2195
+ * discards it (never promoted) and streams the document it already has;
2196
+ * `getDenyStatus()`, read once as the renderer returns, gives that document
2197
+ * the deny's 4xx instead of a 200. A deny later than that commits 200. Pinned by
2198
+ * `tests/late-hoisted-deny-status.test.ts`. The RSC payload path never
2199
+ * reads it — a Flight payload response is always 200 and the row itself
2200
+ * carries the deny (TIM-1495).
1873
2201
  *
1874
2202
  * Either branch leaves part of the matched chain unmounted — hoisting sends
1875
2203
  * the render to a boundary that discards everything below it, and a layout
@@ -1912,20 +2240,6 @@ function setFirstDeniedSegmentIndex(index) {
1912
2240
  //#endregion
1913
2241
  //#region src/server/access-gate.tsx
1914
2242
  /**
1915
- * AccessGate and SlotAccessGate — framework-injected async server components.
1916
- *
1917
- * AccessGate wraps each segment's layout in the element tree. It calls the
1918
- * segment's access.ts before the layout renders. If access.ts calls deny()
1919
- * or redirect(), the signal propagates as a render-phase throw — caught by
1920
- * the flush controller to produce the correct HTTP status code.
1921
- *
1922
- * SlotAccessGate wraps parallel slot content. On denial, it renders the
1923
- * graceful degradation chain: denied.tsx → default.tsx → null. Slot denial
1924
- * does not affect the HTTP status code.
1925
- *
1926
- * See design/04-authorization.md and design/02-rendering-pipeline.md §"AccessGate"
1927
- */
1928
- /**
1929
2243
  * Framework-injected access gate for segments.
1930
2244
  *
1931
2245
  * Calls accessFn during render — inside `renderToReadableStream`, sharing
@@ -2170,87 +2484,6 @@ function resolveSlotDenied(slotNode) {
2170
2484
  return null;
2171
2485
  }
2172
2486
  //#endregion
2173
- //#region src/server/csrf.ts
2174
- /** HTTP methods that are considered safe (no mutation). */
2175
- var SAFE_METHODS = /* @__PURE__ */ new Set([
2176
- "GET",
2177
- "HEAD",
2178
- "OPTIONS"
2179
- ]);
2180
- /**
2181
- * Derive the request's scheme from trusted sources.
2182
- *
2183
- * Priority:
2184
- * 1. X-Forwarded-Proto (set by reverse proxies / load balancers)
2185
- * 2. Request URL protocol
2186
- *
2187
- * Falls back to 'https' (fail closed) if neither is available.
2188
- */
2189
- function deriveRequestScheme(req) {
2190
- const forwarded = req.headers.get("X-Forwarded-Proto");
2191
- if (forwarded) {
2192
- const proto = forwarded.split(",")[0].trim().toLowerCase();
2193
- if (proto === "http" || proto === "https") return proto;
2194
- }
2195
- try {
2196
- return new URL(req.url).protocol.replace(":", "");
2197
- } catch {
2198
- return "https";
2199
- }
2200
- }
2201
- /**
2202
- * Validate the Origin header against the request's full origin
2203
- * (scheme + host + port).
2204
- *
2205
- * For mutation methods (POST, PUT, PATCH, DELETE):
2206
- * - If `csrf: false`, skip validation.
2207
- * - If `allowedOrigins` is set, Origin must match one exactly (no wildcards).
2208
- * - Otherwise, Origin must match the derived request origin.
2209
- *
2210
- * Safe methods (GET, HEAD, OPTIONS) always pass.
2211
- */
2212
- function validateCsrf(req, config) {
2213
- if (SAFE_METHODS.has(req.method)) return { ok: true };
2214
- if (config.csrf === false) return { ok: true };
2215
- const origin = req.headers.get("Origin");
2216
- if (!origin) return {
2217
- ok: false,
2218
- status: 403
2219
- };
2220
- if (config.allowedOrigins) return config.allowedOrigins.includes(origin) ? { ok: true } : {
2221
- ok: false,
2222
- status: 403
2223
- };
2224
- const host = req.headers.get("Host");
2225
- if (!host) return {
2226
- ok: false,
2227
- status: 403
2228
- };
2229
- let originOrigin;
2230
- try {
2231
- originOrigin = new URL(origin).origin;
2232
- } catch {
2233
- return {
2234
- ok: false,
2235
- status: 403
2236
- };
2237
- }
2238
- const scheme = deriveRequestScheme(req);
2239
- let expectedOrigin;
2240
- try {
2241
- expectedOrigin = new URL(`${scheme}://${host}`).origin;
2242
- } catch {
2243
- return {
2244
- ok: false,
2245
- status: 403
2246
- };
2247
- }
2248
- return originOrigin === expectedOrigin ? { ok: true } : {
2249
- ok: false,
2250
- status: 403
2251
- };
2252
- }
2253
- //#endregion
2254
2487
  //#region src/server/body-limits.ts
2255
2488
  var KB = 1024;
2256
2489
  var MB = 1024 * KB;
@@ -3044,37 +3277,50 @@ async function handleRouteRequest(mod, ctx) {
3044
3277
  const allowHeader = resolveAllowedMethods(mod).join(", ");
3045
3278
  if (method === "OPTIONS") {
3046
3279
  if (mod.OPTIONS) return runHandler(mod.OPTIONS, ctx);
3047
- return new Response(null, {
3280
+ return mergeResponseHeaders(new Response(null, {
3048
3281
  status: 204,
3049
3282
  headers: { Allow: allowHeader }
3050
- });
3283
+ }), ctx.headers);
3051
3284
  }
3052
3285
  if (method === "HEAD") {
3053
3286
  if (mod.HEAD) return stripHeadBody(await runHandler(mod.HEAD, ctx), method);
3054
3287
  if (mod.GET) return stripHeadBody(await runHandler(mod.GET, ctx), method);
3055
3288
  }
3056
3289
  const handler = mod[method];
3057
- if (!handler) return new Response(null, {
3290
+ if (!handler) return mergeResponseHeaders(new Response(null, {
3058
3291
  status: 405,
3059
3292
  headers: { Allow: allowHeader }
3060
- });
3293
+ }), ctx.headers);
3061
3294
  return runHandler(handler, ctx);
3062
3295
  }
3063
3296
  /**
3064
- * Run a handler, merge ctx.headers into the response, and catch errors.
3297
+ * Run a handler with cookie writes enabled and merge ctx.headers into its
3298
+ * response.
3299
+ *
3300
+ * Cookies (design/29-cookies.md §"Where Cookies Can Be Set"): the render
3301
+ * phase applied the jar to `ctx.headers` before the handler ran, so on the
3302
+ * way out only what the handler changed is synced onto that bag. It happens
3303
+ * whether the handler returns or throws, so a cookie set before `redirect()`
3304
+ * rides the redirect, and the pipeline's 500 for a throw carries the same
3305
+ * bag.
3306
+ *
3307
+ * Nothing is caught. A signal goes to the pipeline, whose deny fallback
3308
+ * answers a route.ts match in JSON and whose redirect outcome sends the 3xx
3309
+ * (TIM-1533). Any other error goes to the pipeline, which logs it,
3310
+ * reports it to `onRequestError` as phase `'route'`, and answers it in the
3311
+ * same format as every other route.ts error (TIM-1537).
3065
3312
  */
3066
3313
  async function runHandler(handler, ctx) {
3314
+ const applied = getSetCookieHeaders();
3315
+ setMutableCookieContext(true);
3316
+ let res;
3067
3317
  try {
3068
- return mergeResponseHeaders(await handler(ctx), ctx.headers);
3069
- } catch (error) {
3070
- if (isDenySignal(error) || isRedirectSignal(error)) throw error;
3071
- logRouteError({
3072
- method: ctx.req.method,
3073
- path: new URL(ctx.req.url).pathname,
3074
- error
3075
- });
3076
- return new Response(null, { status: 500 });
3318
+ res = await handler(ctx);
3319
+ } finally {
3320
+ setMutableCookieContext(false);
3321
+ syncCookieJar(ctx.headers, applied);
3077
3322
  }
3323
+ return mergeResponseHeaders(res, ctx.headers);
3078
3324
  }
3079
3325
  /**
3080
3326
  * Merge response headers from ctx.headers into the handler's response.
@@ -3104,32 +3350,6 @@ function mergeResponseHeaders(res, ctxHeaders) {
3104
3350
  });
3105
3351
  }
3106
3352
  //#endregion
3107
- //#region src/server/render-timeout.ts
3108
- /**
3109
- * Render timeout utilities for SSR streaming pipeline.
3110
- *
3111
- * Provides a RenderTimeoutError class and a helper to create
3112
- * timeout-guarded AbortSignals. Used to defend against hung RSC
3113
- * streams and infinite SSR renders.
3114
- *
3115
- * Design doc: 02-rendering-pipeline.md §"Streaming Constraints"
3116
- */
3117
- /**
3118
- * Error thrown when an SSR render or RSC stream read exceeds the
3119
- * configured timeout. Callers can check `instanceof RenderTimeoutError`
3120
- * to distinguish timeout from other errors and return a 504 or close
3121
- * the connection cleanly.
3122
- */
3123
- var RenderTimeoutError = class extends Error {
3124
- timeoutMs;
3125
- constructor(timeoutMs, context) {
3126
- const message = context ? `Render timeout after ${timeoutMs}ms: ${context}` : `Render timeout after ${timeoutMs}ms`;
3127
- super(message);
3128
- this.name = "RenderTimeoutError";
3129
- this.timeoutMs = timeoutMs;
3130
- }
3131
- };
3132
- //#endregion
3133
3353
  //#region src/server/prebuilt/cache-key.ts
3134
3354
  /**
3135
3355
  * Component cache-key hashing for prebuilt (cache.component) entries.
@@ -3395,6 +3615,107 @@ function SlotsProvider({ values, children }) {
3395
3615
  } : values;
3396
3616
  return createElement(SlotContext.Provider, { value: merged }, children);
3397
3617
  }
3618
+ //#endregion
3619
+ //#region src/server/prebuilt/lifetime.ts
3620
+ /**
3621
+ * Lifetime declarations of a `cache.component` — `prerender`, `ttl`,
3622
+ * `tags` (design/45-cache-lifetimes.md) — and the nesting clamp between
3623
+ * two of them (§Nesting Semantics, TIM-1136).
3624
+ *
3625
+ * The clamp rule lives here once, for both places a capture can embed one
3626
+ * wrapped component in another: the post-build capture pass
3627
+ * (server/prebuilt-builder.ts) and request-time re-renders
3628
+ * (`captureComponentPayload()` in server/prebuilt-runtime.ts, TIM-1545).
3629
+ */
3630
+ /** Whether the component has a runtime lifetime (ttl or tags). */
3631
+ function hasRuntimeLifetime(options) {
3632
+ return options.ttl !== void 0 || options.tags != null;
3633
+ }
3634
+ /**
3635
+ * Resolve tags from options (may be static array or function of props).
3636
+ * Function-form tags receive DATA props only — slot props are excluded so
3637
+ * the tags fn never sees SlotOutlet placeholders (capture paths pass
3638
+ * placeholder-substituted props) and tags stay deterministic per key.
3639
+ */
3640
+ function resolveTags(options, props) {
3641
+ if (!options.tags) return [];
3642
+ if (Array.isArray(options.tags)) return options.tags;
3643
+ const slots = options.slots;
3644
+ const fnProps = slots && slots.length > 0 ? splitSlotProps(props, slots)?.dataProps ?? props : props;
3645
+ return options.tags(fnProps);
3646
+ }
3647
+ /**
3648
+ * Why an embedded copy of `inner` goes stale inside `outer`, or null when
3649
+ * the outer's refreshes cover every refresh channel the inner declares:
3650
+ *
3651
+ * - `shorterTtl`: the inner's ttl is shorter than the outer's effective
3652
+ * lifetime (its ttl, or deploy-long without one).
3653
+ * - `missingTags`: tags the inner resolved to that the outer lacks —
3654
+ * `revalidateTag` on them refreshes the inner's own entries, never the
3655
+ * copy. `null` inner tags (a tags function threw) cannot be proven a
3656
+ * subset, so they count as missing.
3657
+ *
3658
+ * Declaring the inner's tags on the outer is the documented fix, and it
3659
+ * silences the tag half by construction.
3660
+ */
3661
+ function clampReasons(outer, inner) {
3662
+ const outerTtl = outer.options.ttl ?? Infinity;
3663
+ const shorterTtl = inner.options.ttl !== void 0 && inner.options.ttl < outerTtl;
3664
+ const missingTags = inner.tags === null ? null : inner.tags.filter((t) => !outer.tags.includes(t));
3665
+ if (!shorterTtl && missingTags !== null && missingTags.length === 0) return null;
3666
+ return {
3667
+ shorterTtl,
3668
+ missingTags
3669
+ };
3670
+ }
3671
+ /** Render a lifetime declaration for the clamp warning. */
3672
+ function describeLifetime(options, tags) {
3673
+ const parts = [];
3674
+ if (options.prerender) parts.push("prerender");
3675
+ if (options.ttl !== void 0) parts.push(`ttl: ${options.ttl}`);
3676
+ if (tags === null) parts.push("tags: fn");
3677
+ else if (tags.length > 0) parts.push(`tags: ${JSON.stringify(tags)}`);
3678
+ return parts.join(", ") || "no lifetime";
3679
+ }
3680
+ /** When the outer entry — and so every copy embedded in it — refreshes. */
3681
+ function describeRefresh(outer) {
3682
+ const parts = [];
3683
+ if (outer.options.ttl !== void 0) parts.push(`every ${outer.options.ttl}s`);
3684
+ if (outer.tags.length > 0) parts.push(`on revalidateTag of its own tags`);
3685
+ parts.push("on redeploy");
3686
+ return parts.join(", or ");
3687
+ }
3688
+ /**
3689
+ * Warn for each wrapped component embedded in `outer`'s captured payload
3690
+ * whose refresh channels the outer cannot honor. Once per (outer, inner)
3691
+ * pair for the lifetime of `warnedPairs` — the builder passes one per
3692
+ * build, the runtime one per process, since re-renders repeat. The key is
3693
+ * the pair alone, not the reason: resolved tags vary with props, and a key
3694
+ * that included them would grow with the data. So the first clamp found
3695
+ * for a pair is the one reported. Returns the number of warnings emitted.
3696
+ */
3697
+ function warnNestingClamps(outer, nested, warnedPairs, log) {
3698
+ let count = 0;
3699
+ for (const inner of nested) {
3700
+ const reasons = clampReasons(outer, inner);
3701
+ if (reasons === null) continue;
3702
+ const pairKey = `${outer.id}\0${inner.innerId}`;
3703
+ if (warnedPairs.has(pairKey)) continue;
3704
+ warnedPairs.add(pairKey);
3705
+ count++;
3706
+ const outerLifetime = describeLifetime(outer.options, outer.tags);
3707
+ const innerLifetime = describeLifetime(inner.options, inner.tags);
3708
+ const consequences = [];
3709
+ if (reasons.missingTags === null) consequences.push(`${inner.innerId}'s tags could not be resolved during the capture (its tags function threw or needed request context), so revalidating them may NOT update the embedded copy.`);
3710
+ else if (reasons.missingTags.length > 0) {
3711
+ const calls = reasons.missingTags.map((t) => `revalidateTag(${JSON.stringify(t)})`);
3712
+ consequences.push(`${calls.join(", ")} will NOT update the embedded copy; tag ${outer.id} itself if you need that.`);
3713
+ }
3714
+ if (reasons.shorterTtl) consequences.push(`${inner.innerId}'s ttl: ${inner.options.ttl} does not apply to the embedded copy.`);
3715
+ log(`[timber] ${inner.innerId} (${innerLifetime}) is embedded in ${outer.options.prerender ? "prerendered" : "cached"} ${outer.id} — the embedded copy refreshes only when ${outer.id} does (${describeRefresh(outer)}). ${consequences.join(" ")} Chain: ${outer.id} (${outerLifetime}) → ${inner.innerId} (${innerLifetime})`);
3716
+ }
3717
+ return count;
3718
+ }
3398
3719
  /**
3399
3720
  * Is this value the framework-injected layout children slot outlet
3400
3721
  * (`<SlotOutlet slot={LAYOUT_CHILDREN_SLOT} />`)? Layout components
@@ -3423,6 +3744,17 @@ function isChildrenSlotOutletElement(value) {
3423
3744
  }
3424
3745
  }
3425
3746
  /**
3747
+ * `props` without the framework-injected children outlet — the key props
3748
+ * of a render. The outlet is the same client reference on every request,
3749
+ * so it never varies the cache key; user-provided children stay (and
3750
+ * leave the props unkeyable). See design/45 §Layout Propagation, TIM-1181.
3751
+ */
3752
+ function withoutChildrenOutlet(props) {
3753
+ if (!isChildrenSlotOutletElement(props.children)) return props;
3754
+ const { children: _children, ...rest } = props;
3755
+ return rest;
3756
+ }
3757
+ /**
3426
3758
  * Dense array of non-empty strings — no holes, no non-string entries.
3427
3759
  * Total: the reads run against a user-controlled value at module load,
3428
3760
  * and a throwing Proxy trap must classify as invalid, not crash startup.
@@ -3454,7 +3786,7 @@ function resolveSlotNames(id, options) {
3454
3786
  return { kind: "disabled" };
3455
3787
  }
3456
3788
  if (slots.length === 0) return { kind: "none" };
3457
- if (!(options.ttl !== void 0 || options.tags != null)) {
3789
+ if (!hasRuntimeLifetime(options)) {
3458
3790
  console.warn(`[timber] cache.component "${id}": slots require a runtime lifetime (ttl and/or tags). Slot caching is disabled.`);
3459
3791
  return { kind: "disabled" };
3460
3792
  }
@@ -3560,50 +3892,133 @@ function emitKeyDisciplineWarning(id, apis) {
3560
3892
  }
3561
3893
  //#endregion
3562
3894
  //#region src/server/prebuilt/capture-state.ts
3563
- /** Thrown by the wrapper when a capture renders its own component. */
3895
+ /**
3896
+ * Capture state — the channel between a payload capture and the
3897
+ * `__prebuilt` wrappers that render inside it (server/prebuilt-runtime.ts).
3898
+ *
3899
+ * Two callers capture a wrapped component's flight payload:
3900
+ *
3901
+ * - **Build time**: the post-build capture pass (server/prebuilt-builder.ts)
3902
+ * renders each registry entry in turn.
3903
+ * - **Request time**: `captureComponentPayload()` in prebuilt-runtime.ts
3904
+ * renders a component in isolation for an inline re-render (full miss,
3905
+ * stale entry, tombstone) or an SWR background re-render. These run
3906
+ * concurrently with other requests' renders — and with each other.
3907
+ *
3908
+ * During a capture, the component's subtree may render OTHER wrapped
3909
+ * components. Those inner wrappers render live and their output is baked
3910
+ * flat into the outer entry — the nesting clamp of
3911
+ * design/45-cache-lifetimes.md §Nesting Semantics (TIM-1136). The wrapper
3912
+ * reports every render that happens inside a capture:
3913
+ *
3914
+ * - **Cycle**: the inner render IS the entry being captured — same
3915
+ * component id AND same props key (TIM-1547). Left alone this recurses
3916
+ * until the render times out; instead the wrapper throws
3917
+ * `PrebuiltCaptureCycleError`, failing that entry with an error naming
3918
+ * the cycle (build continues per doc 44 skip-and-report policy; at
3919
+ * request time the re-render fails and is not cached, and the wrapper
3920
+ * falls back as for any failed re-render). A component that renders
3921
+ * itself with different props (a comment thread, a tree) is legitimate
3922
+ * recursion with a base case and bakes flat like any other nesting.
3923
+ * - **Nesting**: any other wrapper render is recorded per inner component
3924
+ * with the tags it resolved to. Both capture callers run the clamp rule
3925
+ * over those records when the entry completes (prebuilt/lifetime.ts).
3926
+ *
3927
+ * Identity is the capture ROOT only. A wrapper cannot see the wrappers
3928
+ * between it and the root: Flight renders a component's children after
3929
+ * the wrapper has returned, in Flight's own async context, not in a frame
3930
+ * the wrapper could enter. So a same-key cycle that closes below the root
3931
+ * (`A(p1) → A(p2) → A(p2) → …`) is not detected here; it is also an
3932
+ * infinite recursion without caching, and the capture's render timeout
3933
+ * fails it.
3934
+ *
3935
+ * The props key is the props half of the cache key (data props only,
3936
+ * framework children outlet stripped — the same inputs the cache key
3937
+ * hashes). The params half is left out: every render inside one capture
3938
+ * reads the same segment params, so props decide identity. Unkeyable props
3939
+ * (live React elements, …) cannot be proven identical and are never a
3940
+ * cycle.
3941
+ *
3942
+ * Each capture's state lives in an AsyncLocalStorage store entered by
3943
+ * `CaptureScope.run()`, so it is visible only to renders started inside
3944
+ * that capture — including work Flight resumes after an await, which runs
3945
+ * in the context the awaited promise was created in. A concurrent request
3946
+ * rendering the same component is not a cycle, and a concurrent request
3947
+ * rendering a different cached component still revives it from cache
3948
+ * instead of baking it live (TIM-1536). Outside any capture
3949
+ * `noteWrapperRender` is a single store lookup.
3950
+ */
3951
+ /** Thrown by the wrapper when a capture renders its own entry. */
3564
3952
  var PrebuiltCaptureCycleError = class extends Error {
3565
3953
  constructor(componentId) {
3566
- super(`[timber] cache.component "${componentId}" renders itself (directly or through other components) — prerendering it would recurse forever. Remove the cycle or drop { prerender: true } on this component.`);
3954
+ super(`[timber] cache.component "${componentId}" renders itself with the same props (directly or through other components) — capturing its payload would recurse forever. Remove the cycle or stop caching this component with cache.component.`);
3567
3955
  this.name = "PrebuiltCaptureCycleError";
3568
3956
  }
3569
3957
  };
3570
- var active = null;
3571
3958
  /**
3572
- * Mark a capture as active. The builder calls this immediately before
3573
- * rendering an entry and MUST pair it with `endCapture()` in a finally.
3959
+ * The props half of a render's cache key, or null when unkeyable. Same
3960
+ * inputs `tryRevivePayload` hashes: data props only for slot components,
3961
+ * the framework-injected children outlet stripped.
3574
3962
  */
3575
- function beginCapture(outerId) {
3576
- active = {
3577
- outerId,
3578
- nested: /* @__PURE__ */ new Map()
3579
- };
3963
+ function propsKey(options, props) {
3964
+ let keyProps = props;
3965
+ if (options.slots && options.slots.length > 0) {
3966
+ const split = splitSlotProps(props, options.slots);
3967
+ if (split === null) return null;
3968
+ keyProps = split.dataProps;
3969
+ }
3970
+ return computeComponentCacheKey(withoutChildrenOutlet(keyProps), {});
3580
3971
  }
3581
3972
  /**
3582
- * End the active capture and return the wrapped components that rendered
3583
- * inside it (first options object wins per inner ID).
3973
+ * Open a capture of component `outerId` rendered with `props`. Its state
3974
+ * ends with the scope object.
3584
3975
  */
3585
- function endCapture() {
3586
- const nested = active?.nested;
3587
- active = null;
3588
- if (!nested) return [];
3589
- return Array.from(nested, ([innerId, options]) => ({
3590
- innerId,
3591
- options
3592
- }));
3976
+ function createCaptureScope(outerId, options, props) {
3977
+ const store = {
3978
+ outerId,
3979
+ outerPropsKey: propsKey(options, props),
3980
+ nested: /* @__PURE__ */ new Map()
3981
+ };
3982
+ return {
3983
+ run: (fn) => prebuiltCaptureAls.run(store, fn),
3984
+ nestedRenders: () => Array.from(store.nested, ([innerId, record]) => ({
3985
+ innerId,
3986
+ options: record.options,
3987
+ tags: record.tags && Array.from(record.tags)
3988
+ }))
3989
+ };
3593
3990
  }
3594
- /** Whether a build-time capture is currently rendering. */
3991
+ /** Whether the current render is inside a capture. */
3595
3992
  function isCaptureActive() {
3596
- return active !== null;
3993
+ return prebuiltCaptureAls.getStore() !== void 0;
3597
3994
  }
3598
3995
  /**
3599
- * Called by the `__prebuilt` wrapper on every render. No-op unless a
3600
- * capture is active. Throws `PrebuiltCaptureCycleError` when the wrapper
3601
- * belongs to the component currently being captured.
3996
+ * Called by the `__prebuilt` wrapper on every render. No-op outside a
3997
+ * capture. Throws `PrebuiltCaptureCycleError` when the render is the
3998
+ * entry the enclosing capture is producing.
3602
3999
  */
3603
- function noteWrapperRender(innerId, options) {
3604
- if (active === null) return;
3605
- if (innerId === active.outerId) throw new PrebuiltCaptureCycleError(innerId);
3606
- if (!active.nested.has(innerId)) active.nested.set(innerId, options);
4000
+ function noteWrapperRender(innerId, options, props) {
4001
+ const capture = prebuiltCaptureAls.getStore();
4002
+ if (capture === void 0) return;
4003
+ if (innerId === capture.outerId && capture.outerPropsKey !== null) {
4004
+ if (propsKey(options, props) === capture.outerPropsKey) throw new PrebuiltCaptureCycleError(innerId);
4005
+ }
4006
+ let record = capture.nested.get(innerId);
4007
+ if (record === void 0) {
4008
+ record = {
4009
+ options,
4010
+ tags: /* @__PURE__ */ new Set()
4011
+ };
4012
+ capture.nested.set(innerId, record);
4013
+ }
4014
+ if (record.tags !== null && options.tags) {
4015
+ const tags = record.tags;
4016
+ try {
4017
+ for (const tag of requestContextAls.exit(() => resolveTags(options, props))) tags.add(tag);
4018
+ } catch {
4019
+ record.tags = null;
4020
+ }
4021
+ }
3607
4022
  }
3608
4023
  //#endregion
3609
4024
  //#region src/server/prebuilt-runtime.ts
@@ -3642,12 +4057,19 @@ function noteWrapperRender(innerId, options) {
3642
4057
  * component. `fallbackOnMissingSegmentParams: false` throws instead.
3643
4058
  */
3644
4059
  /** Default timeout for component re-renders (overridden by renderTimeoutMs). */
3645
- var DEFAULT_COMPONENT_TIMEOUT_MS = 3e4;
4060
+ var DEFAULT_COMPONENT_TIMEOUT_MS = DEFAULT_RENDER_TIMEOUT_MS;
3646
4061
  /** Singleflight for component re-renders (inline and background). */
3647
4062
  var componentSingleflight = createSingleflight({ timeoutMs: DEFAULT_COMPONENT_TIMEOUT_MS });
3648
4063
  var componentTimeoutMs = DEFAULT_COMPONENT_TIMEOUT_MS;
3649
4064
  var warnedNoWaitUntil = false;
3650
4065
  /**
4066
+ * (outer, inner) pairs already warned about by request-time captures.
4067
+ * Process-wide: re-renders of a hot component repeat, and the warning is
4068
+ * about the code, not the request. Bounded by the number of wrapped
4069
+ * components in the app.
4070
+ */
4071
+ var warnedNestingPairs = /* @__PURE__ */ new Set();
4072
+ /**
3651
4073
  * Server-side component registry, keyed by stable component ID. Populated at
3652
4074
  * module load time by `__prebuilt` calls; consumed by the post-build capture
3653
4075
  * pass (TIM-1119). Never serialized — component functions are not
@@ -3703,12 +4125,7 @@ function isSlotRenderKeyable(dataProps) {
3703
4125
  if (!store) return false;
3704
4126
  const keyParams = resolveKeyParams(store);
3705
4127
  if (keyParams === null) return false;
3706
- let keyProps = dataProps;
3707
- if (isChildrenSlotOutletElement(keyProps.children)) {
3708
- const { children: _children, ...rest } = keyProps;
3709
- keyProps = rest;
3710
- }
3711
- return computeComponentCacheKey(keyProps, keyParams) !== null;
4128
+ return computeComponentCacheKey(withoutChildrenOutlet(dataProps), keyParams) !== null;
3712
4129
  }
3713
4130
  /** Wrap payload bytes as a single-chunk ReadableStream for the decoder. */
3714
4131
  function bytesToStream(bytes) {
@@ -3717,23 +4134,6 @@ function bytesToStream(bytes) {
3717
4134
  controller.close();
3718
4135
  } });
3719
4136
  }
3720
- /** Whether the component has a runtime lifetime (ttl or tags). */
3721
- function hasRuntimeLifetime(options) {
3722
- return options.ttl !== void 0 || options.tags !== void 0 && options.tags !== null;
3723
- }
3724
- /**
3725
- * Resolve tags from options (may be static array or function of props).
3726
- * Function-form tags receive DATA props only — slot props are excluded so
3727
- * the tags fn never sees SlotOutlet placeholders (capture paths pass
3728
- * placeholder-substituted props) and tags stay deterministic per key.
3729
- */
3730
- function resolveTags(options, props) {
3731
- if (!options.tags) return [];
3732
- if (Array.isArray(options.tags)) return options.tags;
3733
- const slots = options.slots;
3734
- const fnProps = slots && slots.length > 0 ? splitSlotProps(props, slots)?.dataProps ?? props : props;
3735
- return options.tags(fnProps);
3736
- }
3737
4137
  /**
3738
4138
  * Serve a slot component from the cache: split live slot values out of the
3739
4139
  * props, look up / capture the shell (data props + a deterministic
@@ -3779,53 +4179,46 @@ function getCaptureTimeoutMs() {
3779
4179
  * The stream read is timeout-guarded and the reader is cancelled on
3780
4180
  * expiry (design/13 A2, A3).
3781
4181
  */
3782
- async function captureComponentPayload(id, component, props) {
4182
+ async function captureComponentPayload(id, component, props, options) {
3783
4183
  const { renderToReadableStream } = requireRscFunctions();
3784
4184
  const element = createElement(component, props);
3785
- let renderErrored = false;
4185
+ const capture = createCaptureScope(id, options, props);
4186
+ let renderError;
3786
4187
  const store = requestContextAls.getStore();
3787
4188
  const discipline = store && shouldCheckKeyDiscipline(id) ? createKeyDisciplineProxy(store) : null;
3788
- const doCapture = async () => {
3789
- beginCapture(id);
3790
- try {
3791
- const reader = renderToReadableStream(element, { onError() {
3792
- renderErrored = true;
3793
- return "";
3794
- } }).getReader();
3795
- const chunks = [];
3796
- let totalLength = 0;
3797
- const deadline = Date.now() + getCaptureTimeoutMs();
3798
- try {
3799
- for (;;) {
3800
- const remaining = deadline - Date.now();
3801
- if (remaining <= 0) throw new Error(`[timber] component re-render capture exceeded ${getCaptureTimeoutMs()}ms`);
3802
- let timer;
3803
- const { done, value } = await Promise.race([reader.read(), new Promise((_, reject) => {
3804
- timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`[timber] component re-render stream read timed out`)), remaining);
3805
- })]);
3806
- clearTimeout(timer);
3807
- if (done) break;
3808
- chunks.push(value);
3809
- totalLength += value.byteLength;
3810
- }
3811
- } finally {
3812
- reader.cancel().catch(() => {});
3813
- }
3814
- if (renderErrored) throw new Error("[timber] component re-render produced error rows — not caching");
3815
- const result = new Uint8Array(totalLength);
3816
- let offset = 0;
3817
- for (const chunk of chunks) {
3818
- result.set(chunk, offset);
3819
- offset += chunk.byteLength;
3820
- }
3821
- return result;
3822
- } finally {
3823
- endCapture();
3824
- }
3825
- };
4189
+ const doCapture = () => capture.run(async () => {
4190
+ const bytes = await readAllBytesWithDeadline(runOutsideReactCacheScope(() => renderToReadableStream(element, { onError(error) {
4191
+ renderError ??= { error };
4192
+ return "";
4193
+ } })), getCaptureTimeoutMs(), "[timber] component re-render capture");
4194
+ if (renderError) throw renderError.error;
4195
+ return bytes;
4196
+ });
3826
4197
  const bytes = discipline ? await requestContextAls.run(discipline.proxy, doCapture) : await doCapture();
3827
4198
  if (discipline && discipline.accessedApis.size > 0) emitKeyDisciplineWarning(id, discipline.accessedApis);
3828
- return bytes;
4199
+ return {
4200
+ bytes,
4201
+ nested: capture.nestedRenders()
4202
+ };
4203
+ }
4204
+ /**
4205
+ * Store a captured entry in the overlay, then say which embedded
4206
+ * components it clamps (design/45 §Nesting Semantics, TIM-1545). The
4207
+ * warning follows the store: a capture that is discarded (aborted,
4208
+ * invalidated mid-render, failed store) holds no clamped copy, and must
4209
+ * not spend the pair's one warning.
4210
+ */
4211
+ async function storeCapturedEntry(id, cacheKey, captured, options, tags, hasSeed) {
4212
+ await storeOverlayEntry(id, cacheKey, captured.bytes, {
4213
+ ttl: options.ttl,
4214
+ tags,
4215
+ hasSeed
4216
+ });
4217
+ warnNestingClamps({
4218
+ id,
4219
+ options,
4220
+ tags
4221
+ }, captured.nested, warnedNestingPairs, (m) => console.warn(m));
3829
4222
  }
3830
4223
  /**
3831
4224
  * Inline re-render: capture flight bytes, store in overlay, revive for
@@ -3838,13 +4231,9 @@ async function inlineRerender(id, component, props, cacheKey, options, hasSeed,
3838
4231
  try {
3839
4232
  return await revivePayload(id, await componentSingleflight.do(sfKey, async (signal) => {
3840
4233
  const startEpoch = currentInvalidationEpoch();
3841
- const bytes = await captureComponentPayload(id, component, props);
3842
- if (!signal.aborted && hasRuntimeLifetime(options) && !wasInvalidatedSince(startEpoch, oKey, tags)) await storeOverlayEntry(id, cacheKey, bytes, {
3843
- ttl: options.ttl,
3844
- tags,
3845
- hasSeed
3846
- });
3847
- return bytes;
4234
+ const captured = await captureComponentPayload(id, component, props, options);
4235
+ if (!signal.aborted && hasRuntimeLifetime(options) && !wasInvalidatedSince(startEpoch, oKey, tags)) await storeCapturedEntry(id, cacheKey, captured, options, tags, hasSeed);
4236
+ return captured.bytes;
3848
4237
  }), outcome);
3849
4238
  } catch (error) {
3850
4239
  console.error(`[timber] cache.component re-render failed for "${id}" — ${outcome}:`, error);
@@ -3854,10 +4243,12 @@ async function inlineRerender(id, component, props, cacheKey, options, hasSeed,
3854
4243
  /**
3855
4244
  * Snapshot the request context values needed for a background re-render.
3856
4245
  * Creates a FRESH store with copied values — the original ALS store is
3857
- * not referenced after capture.
4246
+ * not referenced after capture. The `Request` itself is shared, not
4247
+ * copied: the re-render reports its failure against it.
3858
4248
  */
3859
4249
  function snapshotRequestContext(store) {
3860
4250
  return {
4251
+ request: store.request,
3861
4252
  headers: new Headers(store.headers),
3862
4253
  originalHeaders: new Headers(store.originalHeaders),
3863
4254
  cookieHeader: store.cookieHeader,
@@ -3880,16 +4271,10 @@ function backgroundRerender(id, component, props, cacheKey, options, hasSeed, sn
3880
4271
  const oKey = overlayKey(id, cacheKey);
3881
4272
  const startEpoch = currentInvalidationEpoch();
3882
4273
  const sfKey = `prebuilt-bg:${id}:${cacheKey}\0${lastInvalidationEpochFor(oKey, tags)}`;
3883
- const work = requestContextAls.run(snapshot, () => componentSingleflight.do(sfKey, async (signal) => {
3884
- const bytes = await captureComponentPayload(id, component, props);
3885
- if (!signal.aborted && !wasInvalidatedSince(startEpoch, oKey, tags)) await storeOverlayEntry(id, cacheKey, bytes, {
3886
- ttl: options.ttl,
3887
- tags,
3888
- hasSeed
3889
- });
3890
- }).catch((error) => {
3891
- console.error(`[timber] cache.component background re-render failed for "${id}":`, error);
3892
- }));
4274
+ const work = requestContextAls.run(snapshot, () => componentSingleflight.do(sfKey, (signal) => runBackgroundFlight(signal, async () => {
4275
+ const captured = await captureComponentPayload(id, component, props, options);
4276
+ if (!signal.aborted && !wasInvalidatedSince(startEpoch, oKey, tags)) await storeCapturedEntry(id, cacheKey, captured, options, tags, hasSeed);
4277
+ }, (error) => console.error(`[timber] cache.component background re-render failed for "${id}":`, error))).catch(() => {}));
3893
4278
  if (waitUntil) waitUntil(work);
3894
4279
  else if (!warnedNoWaitUntil) {
3895
4280
  warnedNoWaitUntil = true;
@@ -3910,12 +4295,7 @@ async function tryRevivePayload(id, options, props, keyPropsOverride) {
3910
4295
  if (!store) return null;
3911
4296
  const keyParams = (options.prerender ? await isParamIndependent(id) : false) ? {} : resolveKeyParams(store);
3912
4297
  if (keyParams === null) return null;
3913
- let keyProps = keyPropsOverride ?? props ?? {};
3914
- if (isChildrenSlotOutletElement(keyProps.children)) {
3915
- const { children: _children, ...rest } = keyProps;
3916
- keyProps = rest;
3917
- }
3918
- const cacheKey = computeComponentCacheKey(keyProps, keyParams);
4298
+ const cacheKey = computeComponentCacheKey(withoutChildrenOutlet(keyPropsOverride ?? props ?? {}), keyParams);
3919
4299
  if (cacheKey === null) return null;
3920
4300
  const outcome = options.fallbackOnMissingSegmentParams === false ? "failing the render (fallbackOnMissingSegmentParams: false)" : "rendering dynamically";
3921
4301
  const component = registry.get(id)?.component;
@@ -3985,7 +4365,7 @@ function __prebuilt(id, component, options = {}) {
3985
4365
  options
3986
4366
  });
3987
4367
  const wrapper = async (props) => {
3988
- noteWrapperRender(id, options);
4368
+ noteWrapperRender(id, options, props);
3989
4369
  if (isCaptureActive()) return createElement(component, props);
3990
4370
  const revived = slots ? await tryReviveSlotShell(id, options, slots, props) : await tryRevivePayload(id, options, props);
3991
4371
  if (revived !== null) return revived.element;
@@ -4027,6 +4407,6 @@ function __prebuiltDev(name, component, options = {}) {
4027
4407
  return wrapper;
4028
4408
  }
4029
4409
  //#endregion
4030
- export { AccessGate, DEFAULT_LIMITS, DenySignal, METADATA_ROUTE_CONVENTIONS, RedirectSignal, RenderError, RenderTimeoutError, SlotAccessGate, __prebuilt, __prebuiltDev, callOnRequestError, canonicalize, classifyMetadataRoute, coerce, collectEarlyHintHeaders, computeComponentCacheKey, createPipeline, enforceBodyLimits, executeAction, formatLinkHeader, generateTraceId, getCookie, getHeader, getLogger, getMetadataRouteAutoLink, getMetadataRouteServePath, getPrebuiltRegistration, getPrebuiltRegistry, getSearchParams, getSegmentParams, getSetCookieHeaders, handleRouteRequest, hasOnRequestError, hasPrebuiltPayloadSource, loadInstrumentation, logCacheMiss, logMiddlewareError, logMiddlewareShortCircuit, logProxyError, logRenderError, logRequestCompleted, logRequestReceived, logSlowRequest, logSwrRefetchFailed, logWaitUntilRejected, logWaitUntilUnsupported, markResponseFlushed, parseBodySize, renderMetadataToElements, replaceTraceId, resolveAllowedMethods, resolveMetadata, resolveMetadataUrls, resolveSlotDenied, resolveStatusFile, resolveTitle, runMiddleware, runProxy, runWithEarlyHintsSender, runWithRequestContext, runWithTraceId, sendEarlyHints103, setLogger, setMutableCookieContext, setPrebuiltPayloadSource, setSegmentParams, validateCsrf };
4410
+ export { AccessGate, DEFAULT_LIMITS, DenySignal, METADATA_ROUTE_CONVENTIONS, RedirectSignal, RenderTimeoutError, SlotAccessGate, __prebuilt, __prebuiltDev, callOnRequestError, canonicalize, classifyMetadataRoute, coerce, collectEarlyHintHeaders, computeComponentCacheKey, createPipeline, enforceBodyLimits, executeAction, formatLinkHeader, generateTraceId, getCookie, getHeader, getLogger, getMetadataRouteAutoLink, getMetadataRouteServePath, getPrebuiltRegistration, getPrebuiltRegistry, getSearchParams, getSegmentParams, getSetCookieHeaders, handleRouteRequest, hasOnRequestError, hasPrebuiltPayloadSource, loadInstrumentation, logCacheMiss, logMiddlewareError, logMiddlewareShortCircuit, logProxyError, logRenderError, logRequestCompleted, logRequestReceived, logSlowRequest, logSwrRefetchFailed, logWaitUntilRejected, logWaitUntilUnsupported, markResponseFlushed, parseBodySize, renderMetadataToElements, replaceTraceId, resolveAllowedMethods, resolveMetadata, resolveMetadataUrls, resolveSlotDenied, resolveStatusFile, resolveTitle, runMiddleware, runProxy, runWithEarlyHintsSender, runWithRequestContext, runWithTraceId, sendEarlyHints103, setLogger, setMutableCookieContext, setPrebuiltPayloadSource, setSegmentParams };
4031
4411
 
4032
4412
  //# sourceMappingURL=internal.js.map