@timber-js/app 0.2.0-alpha.185 → 0.2.0-alpha.187

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 (570) hide show
  1. package/dist/_chunks/{actions-O_LsyCE4.js → actions-35jnMdeJ.js} +4 -3
  2. package/dist/_chunks/{actions-O_LsyCE4.js.map → actions-35jnMdeJ.js.map} +1 -1
  3. package/dist/_chunks/als-registry-C6kcfprT.js +41 -0
  4. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -0
  5. package/dist/_chunks/als-slots-BEEIPKYm.js +26 -0
  6. package/dist/_chunks/als-slots-BEEIPKYm.js.map +1 -0
  7. package/dist/_chunks/base-path-DaQrzbez.js +80 -0
  8. package/dist/_chunks/base-path-DaQrzbez.js.map +1 -0
  9. package/dist/_chunks/build-manifest-DWppEdLB.js +86 -0
  10. package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
  11. package/dist/_chunks/build-output-helper-C3DrfzZR.js +369 -0
  12. package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +1 -0
  13. package/dist/_chunks/{cache-api-B-lhk9p4.js → cache-api-DjNrIWRR.js} +8 -14
  14. package/dist/_chunks/cache-api-DjNrIWRR.js.map +1 -0
  15. package/dist/_chunks/cli-check-CpmN7Nh-.js +256 -0
  16. package/dist/_chunks/cli-check-CpmN7Nh-.js.map +1 -0
  17. package/dist/_chunks/cli-schema-sync-CGMp_Psg.js +298 -0
  18. package/dist/_chunks/cli-schema-sync-CGMp_Psg.js.map +1 -0
  19. package/dist/_chunks/{cloudflare-CnT5Lr7U.js → cloudflare-CGP6BZKO.js} +92 -14
  20. package/dist/_chunks/{cloudflare-CnT5Lr7U.js.map → cloudflare-CGP6BZKO.js.map} +1 -1
  21. package/dist/_chunks/convention-lint-kXsgc_-7.js +784 -0
  22. package/dist/_chunks/convention-lint-kXsgc_-7.js.map +1 -0
  23. package/dist/_chunks/error-boundary-D-ODYX41.js +353 -0
  24. package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -0
  25. package/dist/_chunks/{fast-hash-D6hIVt1Y.js → fast-hash-C-tVYrLb.js} +6 -1
  26. package/dist/_chunks/fast-hash-C-tVYrLb.js.map +1 -0
  27. package/dist/_chunks/file-cache-DmX7OqZP.js +454 -0
  28. package/dist/_chunks/file-cache-DmX7OqZP.js.map +1 -0
  29. package/dist/_chunks/fs-identity-D7vb6GdI.js +208 -0
  30. package/dist/_chunks/fs-identity-D7vb6GdI.js.map +1 -0
  31. package/dist/_chunks/{logger-AWfuX-KJ.js → logger-D8xJZXIN.js} +34 -51
  32. package/dist/_chunks/logger-D8xJZXIN.js.map +1 -0
  33. package/dist/_chunks/mdx-file-C005ay-P.js +25 -0
  34. package/dist/_chunks/mdx-file-C005ay-P.js.map +1 -0
  35. package/dist/_chunks/{plugin-context---kTF5v8.js → plugin-context-rCinWLiE.js} +8 -2
  36. package/dist/_chunks/plugin-context-rCinWLiE.js.map +1 -0
  37. package/dist/_chunks/purge-store-Byr8XOjU.js +14 -0
  38. package/dist/_chunks/purge-store-Byr8XOjU.js.map +1 -0
  39. package/dist/_chunks/{resolve-schema-Dz3fcFUo.js → resolve-schema-5ma5pp1b.js} +2 -2
  40. package/dist/_chunks/{resolve-schema-Dz3fcFUo.js.map → resolve-schema-5ma5pp1b.js.map} +1 -1
  41. package/dist/_chunks/{router-ref-CtmF-aPv.js → router-ref-DuYuV_0Q.js} +2 -2
  42. package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -0
  43. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js +243 -0
  44. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js.map +1 -0
  45. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +40 -0
  46. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +1 -0
  47. package/dist/_chunks/rsc-media-type-DRqE_lD_.js +46 -0
  48. package/dist/_chunks/rsc-media-type-DRqE_lD_.js.map +1 -0
  49. package/dist/_chunks/rsc-payload-path-B_LBodc2.js +48 -0
  50. package/dist/_chunks/rsc-payload-path-B_LBodc2.js.map +1 -0
  51. package/dist/_chunks/scanner-C8b0Gcw3.js +1564 -0
  52. package/dist/_chunks/scanner-C8b0Gcw3.js.map +1 -0
  53. package/dist/_chunks/schema-bridge-Cc2Gngu1.js +199 -0
  54. package/dist/_chunks/schema-bridge-Cc2Gngu1.js.map +1 -0
  55. package/dist/_chunks/{segment-classify-Byy425ng.js → segment-classify-C539Pa2O.js} +23 -2
  56. package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -0
  57. package/dist/_chunks/segment-context-CjOlyB8Y.js +97 -0
  58. package/dist/_chunks/segment-context-CjOlyB8Y.js.map +1 -0
  59. package/dist/_chunks/{canonicalize-DQHyFClh.js → segment-keys-BawYuNFO.js} +61 -2
  60. package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -0
  61. package/dist/_chunks/slot-params-BCTmZkQB.js +76 -0
  62. package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -0
  63. package/dist/_chunks/{ssr-data-BOWsq18U.js → ssr-data-14MXm7Pj.js} +17 -2
  64. package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -0
  65. package/dist/_chunks/{use-query-states-DFvWd-EA.js → use-query-states-BbU5Ge1V.js} +74 -26
  66. package/dist/_chunks/use-query-states-BbU5Ge1V.js.map +1 -0
  67. package/dist/_chunks/use-segment-params-C4r4BD9T.js +398 -0
  68. package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -0
  69. package/dist/_chunks/walkers-RzN6AFjr.js +141 -0
  70. package/dist/_chunks/walkers-RzN6AFjr.js.map +1 -0
  71. package/dist/adapters/build-output-helper.d.ts +37 -0
  72. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  73. package/dist/adapters/cloudflare-dev.js +1 -1
  74. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  75. package/dist/adapters/cloudflare.d.ts.map +1 -1
  76. package/dist/adapters/cloudflare.js +1 -1
  77. package/dist/adapters/compress-module.d.ts +12 -0
  78. package/dist/adapters/compress-module.d.ts.map +1 -1
  79. package/dist/adapters/fs-identity.d.ts +64 -0
  80. package/dist/adapters/fs-identity.d.ts.map +1 -0
  81. package/dist/adapters/nitro.d.ts.map +1 -1
  82. package/dist/adapters/nitro.js +56 -5
  83. package/dist/adapters/nitro.js.map +1 -1
  84. package/dist/adapters/shared.d.ts +57 -1
  85. package/dist/adapters/shared.d.ts.map +1 -1
  86. package/dist/adapters/types.d.ts +13 -1
  87. package/dist/adapters/types.d.ts.map +1 -1
  88. package/dist/cache/fast-hash.d.ts +5 -0
  89. package/dist/cache/fast-hash.d.ts.map +1 -1
  90. package/dist/cache/index.js +1 -1
  91. package/dist/cdn/cloudflare-purge.js +30 -0
  92. package/dist/cdn/cloudflare-purge.js.map +1 -0
  93. package/dist/cdn/fastly-purge.js +33 -0
  94. package/dist/cdn/fastly-purge.js.map +1 -0
  95. package/dist/cdn/index.js +94 -0
  96. package/dist/cdn/index.js.map +1 -0
  97. package/dist/cdn/workers-cache-purge.js +35 -0
  98. package/dist/cdn/workers-cache-purge.js.map +1 -0
  99. package/dist/cli-check.d.ts +153 -0
  100. package/dist/cli-check.d.ts.map +1 -0
  101. package/dist/cli.d.ts +34 -8
  102. package/dist/cli.d.ts.map +1 -1
  103. package/dist/cli.js +46 -21
  104. package/dist/cli.js.map +1 -1
  105. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  106. package/dist/client/browser-entry/hydrate.d.ts +20 -9
  107. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  108. package/dist/client/browser-entry/index.d.ts +1 -1
  109. package/dist/client/browser-entry/index.d.ts.map +1 -1
  110. package/dist/client/browser-entry/post-hydration.d.ts +4 -1
  111. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  112. package/dist/client/browser-entry/router-init.d.ts +3 -0
  113. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  114. package/dist/client/browser-entry/rsc-stream.d.ts +8 -0
  115. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
  116. package/dist/client/error-boundary.d.ts +7 -0
  117. package/dist/client/error-boundary.d.ts.map +1 -1
  118. package/dist/client/error-boundary.js +1 -162
  119. package/dist/client/history.d.ts +13 -6
  120. package/dist/client/history.d.ts.map +1 -1
  121. package/dist/client/index.d.ts +1 -0
  122. package/dist/client/index.d.ts.map +1 -1
  123. package/dist/client/index.js +42 -41
  124. package/dist/client/index.js.map +1 -1
  125. package/dist/client/internal.d.ts +4 -3
  126. package/dist/client/internal.d.ts.map +1 -1
  127. package/dist/client/internal.js +877 -311
  128. package/dist/client/internal.js.map +1 -1
  129. package/dist/client/link.d.ts +1 -7
  130. package/dist/client/link.d.ts.map +1 -1
  131. package/dist/client/navigation-api.d.ts +2 -2
  132. package/dist/client/navigation-commit.d.ts +87 -0
  133. package/dist/client/navigation-commit.d.ts.map +1 -0
  134. package/dist/client/navigation-context.d.ts +37 -20
  135. package/dist/client/navigation-context.d.ts.map +1 -1
  136. package/dist/client/navigation-root.d.ts +63 -23
  137. package/dist/client/navigation-root.d.ts.map +1 -1
  138. package/dist/client/params-context.d.ts +81 -0
  139. package/dist/client/params-context.d.ts.map +1 -0
  140. package/dist/client/router-effects.d.ts +129 -0
  141. package/dist/client/router-effects.d.ts.map +1 -0
  142. package/dist/client/router-lifecycle.d.ts +55 -0
  143. package/dist/client/router-lifecycle.d.ts.map +1 -0
  144. package/dist/client/router-pipeline.d.ts +84 -0
  145. package/dist/client/router-pipeline.d.ts.map +1 -0
  146. package/dist/client/router-ref.d.ts +1 -1
  147. package/dist/client/router-ref.d.ts.map +1 -1
  148. package/dist/client/router-skew.d.ts +21 -0
  149. package/dist/client/router-skew.d.ts.map +1 -0
  150. package/dist/client/router-types.d.ts +207 -0
  151. package/dist/client/router-types.d.ts.map +1 -0
  152. package/dist/client/router.d.ts +1 -182
  153. package/dist/client/router.d.ts.map +1 -1
  154. package/dist/client/rsc-fetch.d.ts +47 -26
  155. package/dist/client/rsc-fetch.d.ts.map +1 -1
  156. package/dist/client/segment-cache.d.ts +87 -34
  157. package/dist/client/segment-cache.d.ts.map +1 -1
  158. package/dist/client/ssr-data.d.ts +6 -0
  159. package/dist/client/ssr-data.d.ts.map +1 -1
  160. package/dist/client/stale-client.d.ts +69 -0
  161. package/dist/client/stale-client.d.ts.map +1 -0
  162. package/dist/client/state.d.ts +10 -1
  163. package/dist/client/state.d.ts.map +1 -1
  164. package/dist/client/top-loader.d.ts +2 -1
  165. package/dist/client/top-loader.d.ts.map +1 -1
  166. package/dist/client/unload-guard.d.ts +5 -0
  167. package/dist/client/unload-guard.d.ts.map +1 -1
  168. package/dist/client/use-cookie.d.ts.map +1 -1
  169. package/dist/client/use-query-states.d.ts +9 -3
  170. package/dist/client/use-query-states.d.ts.map +1 -1
  171. package/dist/client/use-router.d.ts +1 -1
  172. package/dist/client/use-segment-params.d.ts +18 -8
  173. package/dist/client/use-segment-params.d.ts.map +1 -1
  174. package/dist/codec.js +1 -1
  175. package/dist/cookies/define-cookie.d.ts.map +1 -1
  176. package/dist/cookies/index.js +5 -5
  177. package/dist/cookies/index.js.map +1 -1
  178. package/dist/index.d.ts +4 -1
  179. package/dist/index.d.ts.map +1 -1
  180. package/dist/index.js +1009 -759
  181. package/dist/index.js.map +1 -1
  182. package/dist/params/index.js +1 -1
  183. package/dist/plugin-context.d.ts +46 -0
  184. package/dist/plugin-context.d.ts.map +1 -1
  185. package/dist/plugins/adapter-build.d.ts.map +1 -1
  186. package/dist/plugins/build-manifest.d.ts +1 -1
  187. package/dist/plugins/build-manifest.d.ts.map +1 -1
  188. package/dist/plugins/entries.d.ts.map +1 -1
  189. package/dist/plugins/fonts.d.ts.map +1 -1
  190. package/dist/plugins/request-dep/analysis.d.ts +106 -0
  191. package/dist/plugins/request-dep/analysis.d.ts.map +1 -0
  192. package/dist/plugins/request-dep/index.d.ts +75 -0
  193. package/dist/plugins/request-dep/index.d.ts.map +1 -0
  194. package/dist/plugins/request-dep/origin.d.ts +61 -0
  195. package/dist/plugins/request-dep/origin.d.ts.map +1 -0
  196. package/dist/plugins/routing.d.ts.map +1 -1
  197. package/dist/plugins/shims.d.ts.map +1 -1
  198. package/dist/plugins/static-build.d.ts +160 -8
  199. package/dist/plugins/static-build.d.ts.map +1 -1
  200. package/dist/routing/codegen-shared.d.ts +3 -44
  201. package/dist/routing/codegen-shared.d.ts.map +1 -1
  202. package/dist/routing/codegen-types.d.ts +28 -31
  203. package/dist/routing/codegen-types.d.ts.map +1 -1
  204. package/dist/routing/codegen-write.d.ts +51 -0
  205. package/dist/routing/codegen-write.d.ts.map +1 -0
  206. package/dist/routing/codegen.d.ts.map +1 -1
  207. package/dist/routing/collision-probe.d.ts +104 -0
  208. package/dist/routing/collision-probe.d.ts.map +1 -0
  209. package/dist/routing/collision-spaces.d.ts +26 -0
  210. package/dist/routing/collision-spaces.d.ts.map +1 -0
  211. package/dist/routing/convention-lint.d.ts +18 -4
  212. package/dist/routing/convention-lint.d.ts.map +1 -1
  213. package/dist/routing/export-detect.d.ts +16 -0
  214. package/dist/routing/export-detect.d.ts.map +1 -1
  215. package/dist/routing/index.js +4 -3
  216. package/dist/routing/interception.d.ts +235 -6
  217. package/dist/routing/interception.d.ts.map +1 -1
  218. package/dist/routing/link-codegen.d.ts +19 -4
  219. package/dist/routing/link-codegen.d.ts.map +1 -1
  220. package/dist/routing/manifest-codegen.d.ts +1 -7
  221. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  222. package/dist/routing/scanner.d.ts.map +1 -1
  223. package/dist/routing/segment-classify.d.ts +33 -0
  224. package/dist/routing/segment-classify.d.ts.map +1 -1
  225. package/dist/routing/segment-keys.d.ts +95 -0
  226. package/dist/routing/segment-keys.d.ts.map +1 -0
  227. package/dist/routing/slot-placement.d.ts +48 -0
  228. package/dist/routing/slot-placement.d.ts.map +1 -0
  229. package/dist/routing/types.d.ts +0 -6
  230. package/dist/routing/types.d.ts.map +1 -1
  231. package/dist/routing/walkers.d.ts +6 -0
  232. package/dist/routing/walkers.d.ts.map +1 -1
  233. package/dist/schema-bridge.d.ts +60 -9
  234. package/dist/schema-bridge.d.ts.map +1 -1
  235. package/dist/search-params/define.d.ts +62 -14
  236. package/dist/search-params/define.d.ts.map +1 -1
  237. package/dist/search-params/index.d.ts +0 -1
  238. package/dist/search-params/index.d.ts.map +1 -1
  239. package/dist/search-params/index.js +221 -10
  240. package/dist/search-params/index.js.map +1 -1
  241. package/dist/search-params/parse-total.d.ts +70 -0
  242. package/dist/search-params/parse-total.d.ts.map +1 -0
  243. package/dist/search-params/wrappers.d.ts +26 -3
  244. package/dist/search-params/wrappers.d.ts.map +1 -1
  245. package/dist/server/access-gate.d.ts +19 -8
  246. package/dist/server/access-gate.d.ts.map +1 -1
  247. package/dist/server/action-handler.d.ts.map +1 -1
  248. package/dist/server/actions.d.ts +6 -3
  249. package/dist/server/actions.d.ts.map +1 -1
  250. package/dist/server/als-registry.d.ts +18 -1
  251. package/dist/server/als-registry.d.ts.map +1 -1
  252. package/dist/server/build-manifest.d.ts +13 -1
  253. package/dist/server/build-manifest.d.ts.map +1 -1
  254. package/dist/server/chain-url-parts.d.ts +82 -0
  255. package/dist/server/chain-url-parts.d.ts.map +1 -0
  256. package/dist/server/children-interception.d.ts +49 -0
  257. package/dist/server/children-interception.d.ts.map +1 -0
  258. package/dist/server/compress.d.ts.map +1 -1
  259. package/dist/server/default-status-page.d.ts +45 -0
  260. package/dist/server/default-status-page.d.ts.map +1 -0
  261. package/dist/server/deny-boundary.d.ts +148 -15
  262. package/dist/server/deny-boundary.d.ts.map +1 -1
  263. package/dist/server/deny-renderer.d.ts +15 -4
  264. package/dist/server/deny-renderer.d.ts.map +1 -1
  265. package/dist/server/error-boundary-wrapper.d.ts +85 -15
  266. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  267. package/dist/server/head-response.d.ts +35 -0
  268. package/dist/server/head-response.d.ts.map +1 -0
  269. package/dist/server/index.d.ts +0 -1
  270. package/dist/server/index.d.ts.map +1 -1
  271. package/dist/server/index.js +3 -2
  272. package/dist/server/index.js.map +1 -1
  273. package/dist/server/internal.d.ts +3 -1
  274. package/dist/server/internal.d.ts.map +1 -1
  275. package/dist/server/internal.js +712 -418
  276. package/dist/server/internal.js.map +1 -1
  277. package/dist/server/metadata-collector.d.ts +52 -0
  278. package/dist/server/metadata-collector.d.ts.map +1 -0
  279. package/dist/server/param-coercion.d.ts +22 -6
  280. package/dist/server/param-coercion.d.ts.map +1 -1
  281. package/dist/server/pipeline-helpers.d.ts +0 -18
  282. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  283. package/dist/server/pipeline-interception.d.ts +59 -9
  284. package/dist/server/pipeline-interception.d.ts.map +1 -1
  285. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  286. package/dist/server/pipeline-phases.d.ts.map +1 -1
  287. package/dist/server/pipeline.d.ts +11 -0
  288. package/dist/server/pipeline.d.ts.map +1 -1
  289. package/dist/server/primitives.d.ts +23 -0
  290. package/dist/server/primitives.d.ts.map +1 -1
  291. package/dist/server/publish-params.d.ts +33 -0
  292. package/dist/server/publish-params.d.ts.map +1 -0
  293. package/dist/server/request-context.d.ts +48 -0
  294. package/dist/server/request-context.d.ts.map +1 -1
  295. package/dist/server/route-element-builder.d.ts +22 -3
  296. package/dist/server/route-element-builder.d.ts.map +1 -1
  297. package/dist/server/route-handler.d.ts.map +1 -1
  298. package/dist/server/route-matcher.d.ts +13 -0
  299. package/dist/server/route-matcher.d.ts.map +1 -1
  300. package/dist/server/rsc-cache-key-guard.d.ts +78 -0
  301. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
  302. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  303. package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
  304. package/dist/server/rsc-entry/error-renderer.d.ts +15 -3
  305. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  306. package/dist/server/rsc-entry/helpers.d.ts +2 -17
  307. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  308. package/dist/server/rsc-entry/index.d.ts +0 -1
  309. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  310. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  311. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
  312. package/dist/server/rsc-entry/rsc-payload.d.ts +1 -2
  313. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  314. package/dist/server/rsc-entry/rsc-stream.d.ts +15 -2
  315. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  316. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  317. package/dist/server/skippable-prefix.d.ts +23 -0
  318. package/dist/server/skippable-prefix.d.ts.map +1 -0
  319. package/dist/server/slot-interception.d.ts +80 -0
  320. package/dist/server/slot-interception.d.ts.map +1 -0
  321. package/dist/server/slot-resolver.d.ts +70 -8
  322. package/dist/server/slot-resolver.d.ts.map +1 -1
  323. package/dist/server/ssr-bridge-types.d.ts +17 -0
  324. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  325. package/dist/server/ssr-entry.d.ts +0 -1
  326. package/dist/server/ssr-entry.d.ts.map +1 -1
  327. package/dist/server/ssr-wrappers.d.ts +24 -13
  328. package/dist/server/ssr-wrappers.d.ts.map +1 -1
  329. package/dist/server/state-tree-diff.d.ts +6 -27
  330. package/dist/server/state-tree-diff.d.ts.map +1 -1
  331. package/dist/server/static-generator.d.ts +6 -0
  332. package/dist/server/static-generator.d.ts.map +1 -1
  333. package/dist/server/static-not-found.d.ts +52 -0
  334. package/dist/server/static-not-found.d.ts.map +1 -0
  335. package/dist/server/status-code-resolver.d.ts +8 -1
  336. package/dist/server/status-code-resolver.d.ts.map +1 -1
  337. package/dist/server/tree-builder.d.ts +28 -36
  338. package/dist/server/tree-builder.d.ts.map +1 -1
  339. package/dist/server/types.d.ts +12 -7
  340. package/dist/server/types.d.ts.map +1 -1
  341. package/dist/server/utils/element-type.d.ts +40 -0
  342. package/dist/server/utils/element-type.d.ts.map +1 -0
  343. package/dist/shared/als-slots.d.ts +41 -0
  344. package/dist/shared/als-slots.d.ts.map +1 -0
  345. package/dist/shared/base-path.d.ts +56 -0
  346. package/dist/shared/base-path.d.ts.map +1 -0
  347. package/dist/shared/param-value.d.ts +80 -0
  348. package/dist/shared/param-value.d.ts.map +1 -0
  349. package/dist/shared/payload-root.d.ts +108 -0
  350. package/dist/shared/payload-root.d.ts.map +1 -0
  351. package/dist/shared/rsc-cache-key.d.ts +163 -0
  352. package/dist/shared/rsc-cache-key.d.ts.map +1 -0
  353. package/dist/shared/rsc-error-envelope.d.ts +25 -0
  354. package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
  355. package/dist/shared/rsc-manifest.d.ts +28 -0
  356. package/dist/shared/rsc-manifest.d.ts.map +1 -0
  357. package/dist/shared/rsc-media-type.d.ts +40 -0
  358. package/dist/shared/rsc-media-type.d.ts.map +1 -0
  359. package/dist/shared/rsc-payload-path.d.ts +42 -0
  360. package/dist/shared/rsc-payload-path.d.ts.map +1 -0
  361. package/dist/shared/segment-info.d.ts +59 -0
  362. package/dist/shared/segment-info.d.ts.map +1 -0
  363. package/dist/shared/slot-params.d.ts +51 -0
  364. package/dist/shared/slot-params.d.ts.map +1 -0
  365. package/dist/shared/static-platform-files.d.ts +71 -0
  366. package/dist/shared/static-platform-files.d.ts.map +1 -0
  367. package/docs/api/30-api-server.mdx +1 -1
  368. package/docs/api/31-api-client.mdx +4 -2
  369. package/docs/api/33-api-search-params.mdx +38 -16
  370. package/docs/api/35-api-typescript.mdx +3 -3
  371. package/docs/api/36-cli.mdx +34 -7
  372. package/docs/learn/00-introduction.mdx +1 -1
  373. package/docs/learn/02-pages-and-layouts.mdx +2 -2
  374. package/docs/learn/05-typed-params.mdx +8 -8
  375. package/docs/learn/07-typed-routes.mdx +12 -7
  376. package/docs/learn/11-error-handling.mdx +18 -0
  377. package/docs/learn/12-client-navigation.mdx +2 -2
  378. package/docs/learn/14-deploying.mdx +23 -0
  379. package/docs/more/01-advanced-routing.mdx +173 -3
  380. package/docs/more/03-coming-from-nextjs.mdx +2 -2
  381. package/docs/more/04-metadata-and-fonts.mdx +1 -1
  382. package/docs/more/50-ai-agent-instructions.mdx +6 -4
  383. package/package.json +9 -6
  384. package/src/adapters/build-output-helper.ts +116 -3
  385. package/src/adapters/cloudflare.ts +90 -12
  386. package/src/adapters/compress-module.ts +79 -1
  387. package/src/adapters/fs-identity.ts +156 -0
  388. package/src/adapters/nitro.ts +6 -6
  389. package/src/adapters/shared.ts +196 -21
  390. package/src/adapters/types.ts +13 -1
  391. package/src/cache/fast-hash.ts +5 -0
  392. package/src/cli-check.ts +458 -0
  393. package/src/cli.ts +59 -24
  394. package/src/client/browser-entry/action-dispatch.ts +11 -5
  395. package/src/client/browser-entry/hydrate.ts +40 -29
  396. package/src/client/browser-entry/index.ts +15 -6
  397. package/src/client/browser-entry/post-hydration.ts +10 -9
  398. package/src/client/browser-entry/router-init.ts +104 -48
  399. package/src/client/browser-entry/rsc-stream.ts +11 -2
  400. package/src/client/error-boundary.tsx +85 -16
  401. package/src/client/history.ts +13 -6
  402. package/src/client/index.ts +14 -3
  403. package/src/client/internal.ts +4 -3
  404. package/src/client/link.tsx +120 -72
  405. package/src/client/navigation-api.ts +3 -3
  406. package/src/client/navigation-commit.ts +197 -0
  407. package/src/client/navigation-context.ts +40 -67
  408. package/src/client/navigation-root.tsx +326 -101
  409. package/src/client/params-context.ts +192 -0
  410. package/src/client/router-effects.ts +231 -0
  411. package/src/client/router-lifecycle.ts +288 -0
  412. package/src/client/router-pipeline.ts +393 -0
  413. package/src/client/router-ref.ts +1 -1
  414. package/src/client/router-skew.ts +30 -0
  415. package/src/client/router-types.ts +230 -0
  416. package/src/client/router.ts +243 -707
  417. package/src/client/rsc-fetch.ts +183 -106
  418. package/src/client/segment-cache.ts +129 -44
  419. package/src/client/ssr-data.ts +6 -0
  420. package/src/client/stale-client.ts +191 -0
  421. package/src/client/state.ts +14 -1
  422. package/src/client/top-loader.tsx +2 -1
  423. package/src/client/unload-guard.ts +34 -2
  424. package/src/client/use-cookie.ts +9 -2
  425. package/src/client/use-query-states.ts +102 -39
  426. package/src/client/use-router.ts +1 -1
  427. package/src/client/use-segment-params.ts +41 -18
  428. package/src/cookies/define-cookie.ts +6 -1
  429. package/src/index.ts +25 -4
  430. package/src/plugin-context.ts +58 -0
  431. package/src/plugins/adapter-build.ts +4 -0
  432. package/src/plugins/build-manifest.ts +34 -13
  433. package/src/plugins/entries.ts +4 -0
  434. package/src/plugins/fonts.ts +11 -8
  435. package/src/plugins/request-dep/analysis.ts +447 -0
  436. package/src/plugins/request-dep/index.ts +376 -0
  437. package/src/plugins/request-dep/origin.ts +310 -0
  438. package/src/plugins/routing.ts +97 -102
  439. package/src/plugins/shims.ts +0 -1
  440. package/src/plugins/static-build.ts +391 -95
  441. package/src/routing/codegen-shared.ts +3 -79
  442. package/src/routing/codegen-types.ts +28 -31
  443. package/src/routing/codegen-write.ts +139 -0
  444. package/src/routing/codegen.ts +86 -172
  445. package/src/routing/collision-probe.ts +438 -0
  446. package/src/routing/collision-spaces.ts +281 -0
  447. package/src/routing/convention-lint.ts +139 -40
  448. package/src/routing/export-detect.ts +151 -7
  449. package/src/routing/interception.ts +896 -50
  450. package/src/routing/link-codegen.ts +32 -65
  451. package/src/routing/manifest-codegen.ts +6 -62
  452. package/src/routing/scanner.ts +117 -92
  453. package/src/routing/segment-classify.ts +57 -0
  454. package/src/routing/segment-keys.ts +146 -0
  455. package/src/routing/slot-placement.ts +97 -0
  456. package/src/routing/types.ts +0 -6
  457. package/src/routing/walkers.ts +8 -0
  458. package/src/schema-bridge.ts +180 -58
  459. package/src/search-params/define.ts +108 -66
  460. package/src/search-params/index.ts +0 -1
  461. package/src/search-params/parse-total.ts +78 -0
  462. package/src/search-params/wrappers.ts +60 -11
  463. package/src/server/access-gate.tsx +60 -40
  464. package/src/server/action-handler.ts +4 -5
  465. package/src/server/actions.ts +6 -3
  466. package/src/server/als-registry.ts +18 -1
  467. package/src/server/build-manifest.ts +14 -1
  468. package/src/server/chain-url-parts.ts +163 -0
  469. package/src/server/children-interception.ts +115 -0
  470. package/src/server/compress.ts +9 -1
  471. package/src/server/default-status-page.ts +127 -0
  472. package/src/server/deny-boundary.ts +269 -41
  473. package/src/server/deny-renderer.ts +111 -39
  474. package/src/server/error-boundary-wrapper.ts +166 -79
  475. package/src/server/head-response.ts +54 -0
  476. package/src/server/index.ts +1 -3
  477. package/src/server/internal.ts +2 -2
  478. package/src/server/metadata-collector.ts +115 -0
  479. package/src/server/param-coercion.ts +51 -69
  480. package/src/server/pipeline-helpers.ts +2 -47
  481. package/src/server/pipeline-interception.ts +81 -32
  482. package/src/server/pipeline-outcome.ts +4 -5
  483. package/src/server/pipeline-phases.ts +77 -25
  484. package/src/server/pipeline.ts +57 -10
  485. package/src/server/prebuilt-runtime.ts +4 -4
  486. package/src/server/primitives.ts +25 -0
  487. package/src/server/publish-params.ts +42 -0
  488. package/src/server/request-context.ts +97 -18
  489. package/src/server/route-element-builder.ts +241 -253
  490. package/src/server/route-handler.ts +8 -8
  491. package/src/server/route-matcher.ts +21 -9
  492. package/src/server/rsc-cache-key-guard.ts +164 -0
  493. package/src/server/rsc-entry/api-handler.ts +6 -8
  494. package/src/server/rsc-entry/deny-fallback.ts +3 -2
  495. package/src/server/rsc-entry/error-renderer.ts +94 -35
  496. package/src/server/rsc-entry/helpers.ts +33 -28
  497. package/src/server/rsc-entry/index.ts +36 -33
  498. package/src/server/rsc-entry/render-route.ts +18 -10
  499. package/src/server/rsc-entry/revalidate-renderer.ts +3 -2
  500. package/src/server/rsc-entry/rsc-payload.ts +113 -40
  501. package/src/server/rsc-entry/rsc-stream.ts +29 -5
  502. package/src/server/rsc-entry/ssr-renderer.ts +38 -22
  503. package/src/server/skippable-prefix.ts +92 -0
  504. package/src/server/slot-interception.ts +168 -0
  505. package/src/server/slot-resolver.ts +263 -158
  506. package/src/server/ssr-bridge-types.ts +18 -0
  507. package/src/server/ssr-entry.ts +20 -10
  508. package/src/server/ssr-wrappers.tsx +33 -32
  509. package/src/server/state-tree-diff.ts +14 -55
  510. package/src/server/static-generator.ts +68 -29
  511. package/src/server/static-not-found.ts +134 -0
  512. package/src/server/status-code-resolver.ts +28 -11
  513. package/src/server/tree-builder.ts +35 -218
  514. package/src/server/types.ts +12 -7
  515. package/src/server/utils/element-type.ts +72 -0
  516. package/src/shared/als-slots.ts +72 -0
  517. package/src/shared/base-path.ts +80 -0
  518. package/src/shared/param-value.ts +244 -0
  519. package/src/shared/payload-root.ts +151 -0
  520. package/src/shared/rsc-cache-key.ts +269 -0
  521. package/src/shared/rsc-error-envelope.ts +31 -0
  522. package/src/shared/rsc-manifest.ts +29 -0
  523. package/src/shared/rsc-media-type.ts +43 -0
  524. package/src/shared/rsc-payload-path.ts +45 -0
  525. package/src/shared/segment-info.ts +60 -0
  526. package/src/shared/slot-params.ts +111 -0
  527. package/src/shared/static-platform-files.ts +75 -0
  528. package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
  529. package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
  530. package/dist/_chunks/cache-api-B-lhk9p4.js.map +0 -1
  531. package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
  532. package/dist/_chunks/cli-schema-sync-BTWEKJXo.js +0 -668
  533. package/dist/_chunks/cli-schema-sync-BTWEKJXo.js.map +0 -1
  534. package/dist/_chunks/define-COtkxMRT.js +0 -191
  535. package/dist/_chunks/define-COtkxMRT.js.map +0 -1
  536. package/dist/_chunks/define-c4au4I9R.js +0 -82
  537. package/dist/_chunks/define-c4au4I9R.js.map +0 -1
  538. package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
  539. package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
  540. package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
  541. package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
  542. package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
  543. package/dist/_chunks/registry-DbJPKoBp.js +0 -20
  544. package/dist/_chunks/registry-DbJPKoBp.js.map +0 -1
  545. package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
  546. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js +0 -119
  547. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js.map +0 -1
  548. package/dist/_chunks/segment-classify-Byy425ng.js.map +0 -1
  549. package/dist/_chunks/segment-context-ZDnXDkbz.js +0 -34
  550. package/dist/_chunks/segment-context-ZDnXDkbz.js.map +0 -1
  551. package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
  552. package/dist/_chunks/use-query-states-DFvWd-EA.js.map +0 -1
  553. package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
  554. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
  555. package/dist/_chunks/walkers-_6zKFlch.js +0 -1002
  556. package/dist/_chunks/walkers-_6zKFlch.js.map +0 -1
  557. package/dist/client/error-boundary.js.map +0 -1
  558. package/dist/plugins/request-dep.d.ts +0 -32
  559. package/dist/plugins/request-dep.d.ts.map +0 -1
  560. package/dist/search-params/registry.d.ts +0 -20
  561. package/dist/search-params/registry.d.ts.map +0 -1
  562. package/dist/segment-params/define.d.ts +0 -89
  563. package/dist/segment-params/define.d.ts.map +0 -1
  564. package/dist/segment-params/index.d.ts +0 -3
  565. package/dist/segment-params/index.d.ts.map +0 -1
  566. package/dist/segment-params/index.js +0 -2
  567. package/src/plugins/request-dep.ts +0 -499
  568. package/src/search-params/registry.ts +0 -31
  569. package/src/segment-params/define.ts +0 -249
  570. package/src/segment-params/index.ts +0 -9
@@ -1,11 +1,17 @@
1
- import { a as classifyMetadataRoute, i as METADATA_ROUTE_CONVENTIONS, o as getMetadataRouteAutoLink, r as canonicalize, s as getMetadataRouteServePath } from "../_chunks/canonicalize-DQHyFClh.js";
2
- import { n as classifyUrlSegment } from "../_chunks/segment-classify-Byy425ng.js";
3
- import { $ as timingAls, A as setMatchedSegmentPath, C as applyRequestHeaderOverlay, D as getSegmentParams, E as getSearchParams, F as getSetCookieHeaders, G as replaceTraceId, H as getOtelTraceId, I as getWaitUntil, J as withSpan, K as runWithTraceId, L as isDebug, M as setSegmentParams, N as getCookie, O as markResponseFlushed, V as generateTraceId, W as getTraceId, Y as earlyHintsSenderAls, Z as requestContextAls, _ as RedirectSignal, a as logProxyError, c as logRequestReceived, d as logSwrRefetchFailed, f as logWaitUntilRejected, g as DenySignal, h as swallow, i as logMiddlewareShortCircuit, j as setMutableCookieContext, k as runWithRequestContext, l as logRouteError, m as setLogger, n as logCacheMiss, o as logRenderError, p as logWaitUntilUnsupported, q as setSpanAttribute, r as logMiddlewareError, s as logRequestCompleted, t as getLogger, u as logSlowRequest, v as RenderError, w as getHeader } from "../_chunks/logger-AWfuX-KJ.js";
4
- import { i as coerce, t as executeAction } from "../_chunks/actions-O_LsyCE4.js";
5
- 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 createSingleflight, y as lastInvalidationEpochFor } from "../_chunks/cache-api-B-lhk9p4.js";
6
- import "../client/error-boundary.js";
7
- import { n as toBracketKey } from "../_chunks/resolve-schema-Dz3fcFUo.js";
8
- import "../_chunks/segment-context-ZDnXDkbz.js";
1
+ import { n as isRscRequest } from "../_chunks/rsc-media-type-DRqE_lD_.js";
2
+ import { a as canonicalize, c as getMetadataRouteAutoLink, l as getMetadataRouteServePath, o as METADATA_ROUTE_CONVENTIONS, s as classifyMetadataRoute, t as computeSegmentTreePaths } from "../_chunks/segment-keys-BawYuNFO.js";
3
+ import { n as classifyUrlSegment, r as effectiveUrlSegment } from "../_chunks/segment-classify-C539Pa2O.js";
4
+ import { a as timingAls, r as requestContextAls, t as earlyHintsSenderAls } from "../_chunks/als-registry-C6kcfprT.js";
5
+ import { A as setMatchedSegmentPath, C as applyRequestHeaderOverlay, D as getSegmentParams, E as getSearchParams, F as getSetCookieHeaders, G as replaceTraceId, H as getOtelTraceId, I as getWaitUntil, J as withSpan, K as runWithTraceId, L as isDebug, M as setSegmentParams, N as getCookie, O as markResponseFlushed, V as generateTraceId, W as getTraceId, _ as RedirectSignal, a as logProxyError, c as logRequestReceived, d as logSwrRefetchFailed, f as logWaitUntilRejected, g as DenySignal, h as swallow, i as logMiddlewareShortCircuit, j as setMutableCookieContext, k as runWithRequestContext, l as logRouteError, m as setLogger, n as logCacheMiss, o as logRenderError, p as logWaitUntilUnsupported, q as setSpanAttribute, r as logMiddlewareError, s as logRequestCompleted, t as getLogger, u as logSlowRequest, v as RenderError, w as getHeader } from "../_chunks/logger-D8xJZXIN.js";
6
+ import { i as isRscCacheKeyShape, n as RSC_KEY_PARAM, s as rscCacheKey, t as RSC_KEY_HEADERS } from "../_chunks/rsc-cache-key-DD0fl_-s.js";
7
+ import { t as isMdxFilePath } from "../_chunks/mdx-file-C005ay-P.js";
8
+ import { i as collectRouteModulepreloads, n as collectRouteCss, r as collectRouteFonts } from "../_chunks/build-manifest-DWppEdLB.js";
9
+ import { i as coerce, t as executeAction } from "../_chunks/actions-35jnMdeJ.js";
10
+ 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 createSingleflight, y as lastInvalidationEpochFor } from "../_chunks/cache-api-DjNrIWRR.js";
11
+ import "../_chunks/error-boundary-D-ODYX41.js";
12
+ import { n as toBracketKey } from "../_chunks/resolve-schema-5ma5pp1b.js";
13
+ import { r as normalizeParamValue } from "../_chunks/segment-context-CjOlyB8Y.js";
14
+ import { n as rscErrorEnvelope } from "../_chunks/rsc-error-envelope-tT5PJs4q.js";
9
15
  import { createHash } from "node:crypto";
10
16
  import { readFile } from "node:fs/promises";
11
17
  import React, { createElement, useContext } from "react";
@@ -209,38 +215,6 @@ function hasOnRequestError() {
209
215
  //#endregion
210
216
  //#region src/server/pipeline-helpers.ts
211
217
  /**
212
- * Only __proto__ needs stripping — it has a language-level setter that
213
- * changes the prototype chain of spread copies. constructor and prototype
214
- * are harmless own properties on null-prototype objects.
215
- */
216
- var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__"]);
217
- /**
218
- * Deep-walk a value returned by a segment param codec, producing a
219
- * sanitized copy where every plain object is null-prototype and
220
- * dangerous keys (__proto__, constructor, prototype) are stripped at
221
- * every depth.
222
- *
223
- * Non-plain objects (Date, Map, class instances, etc.) are returned
224
- * as-is — they cannot be poisoned by `{...x}` spread and may carry
225
- * author-intended prototype methods.
226
- *
227
- * Arrays are walked element-wise.
228
- *
229
- * Performance: URL params are bounded by URL length (~8 KB). Realistic
230
- * trees are <1 KB. The recursive walk is sub-microsecond.
231
- *
232
- * See TIM-655, TIM-855, TIM-873, design/13-security.md
233
- */
234
- function sanitizeParamValue(value) {
235
- if (value === null || typeof value !== "object") return value;
236
- if (Array.isArray(value)) return value.map(sanitizeParamValue);
237
- const proto = Object.getPrototypeOf(value);
238
- if (proto !== Object.prototype && proto !== null) return value;
239
- const out = Object.create(null);
240
- for (const key of Object.keys(value)) if (!DANGEROUS_KEYS.has(key)) out[key] = sanitizeParamValue(value[key]);
241
- return out;
242
- }
243
- /**
244
218
  * Build a proxy resolver closure from the declared source. Called exactly
245
219
  * once at `createPipeline` setup time, so the hot path sees only the branch
246
220
  * that corresponds to this pipeline's configured variant.
@@ -315,7 +289,7 @@ function cloneWithMutableHeaders(response) {
315
289
  * createFromFetch. See design/19-client-navigation.md.
316
290
  */
317
291
  function buildRedirectResponse(signal, req, headers) {
318
- if ((req.headers.get("Accept") ?? "").includes("text/x-component")) {
292
+ if (isRscRequest(req)) {
319
293
  headers.set("X-Timber-Redirect", signal.location);
320
294
  return new Response(null, {
321
295
  status: 204,
@@ -1164,6 +1138,42 @@ async function loadModule(loader) {
1164
1138
  }
1165
1139
  }
1166
1140
  //#endregion
1141
+ //#region src/server/utils/element-type.ts
1142
+ var REACT_COMPONENT_TYPE_MARKERS = /* @__PURE__ */ new Set([
1143
+ Symbol.for("react.forward_ref"),
1144
+ Symbol.for("react.memo"),
1145
+ Symbol.for("react.lazy"),
1146
+ Symbol.for("react.provider"),
1147
+ Symbol.for("react.context"),
1148
+ Symbol.for("react.suspense"),
1149
+ Symbol.for("react.suspense_list"),
1150
+ Symbol.for("react.client.reference")
1151
+ ]);
1152
+ /**
1153
+ * Validate that a loaded module's `default` export is something React
1154
+ * accepts as the first argument to `createElement` — i.e. a valid component
1155
+ * type. React doesn't export `isValidElementType` (only `isValidElement`,
1156
+ * which checks for *elements*, not *component types*), so this mirrors
1157
+ * React's internal check:
1158
+ *
1159
+ * - functions → function or class components
1160
+ * - objects with a `$$typeof` matching one of React's known component
1161
+ * markers → exotic components (`memo`, `forwardRef`, `lazy`, context,
1162
+ * suspense, client references via `@vitejs/plugin-rsc`)
1163
+ *
1164
+ * Strings (HTML tag names) are valid for `createElement` but never appear
1165
+ * as a route module's default export, so they're not recognized here.
1166
+ *
1167
+ * Anything else (numbers, plain config objects, JSON, etc.) is rejected so
1168
+ * the boundary wrapper is skipped rather than crashing inside React.
1169
+ */
1170
+ function isValidElementType(value) {
1171
+ if (typeof value === "function") return true;
1172
+ if (typeof value !== "object" || value === null) return false;
1173
+ const marker = value.$$typeof;
1174
+ return typeof marker === "symbol" && REACT_COMPONENT_TYPE_MARKERS.has(marker);
1175
+ }
1176
+ //#endregion
1167
1177
  //#region src/server/deny-boundary.ts
1168
1178
  /**
1169
1179
  * Deny boundary subsystem — the in-tree DenySignal flow.
@@ -1175,10 +1185,14 @@ async function loadModule(loader) {
1175
1185
  * `DenyPageEntry` records ordered by specificity (specific status →
1176
1186
  * category catch-all → `error.tsx`).
1177
1187
  *
1178
- * 2. **Runtime matching** (`renderMatchingDenyPage`) — picks the first
1179
- * chain entry whose status filter matches the thrown DenySignal and
1180
- * returns a React element for the matching component. Used by
1181
- * `AccessGate` and `PageDenyBoundary` when they catch a deny.
1188
+ * 2. **Runtime matching and placement** (`matchDenyEntry`, `placeDenyPage`,
1189
+ * `handleCaughtDeny`) — picks the first chain entry whose status filter
1190
+ * matches the thrown DenySignal, then decides whether the catch site may
1191
+ * render it where it stands. A chain spans the catcher outward to root,
1192
+ * so a match may belong to an ancestor segment; rendering it in place
1193
+ * would wrap it in every layout in between. Ancestor-owned matches are
1194
+ * re-thrown with `ownerKey` stamped on the signal so the error boundary
1195
+ * at the owning segment renders it. See TIM-1356.
1182
1196
  *
1183
1197
  * 3. **The page boundary itself** (`PageDenyBoundary`) — the async server
1184
1198
  * component that wraps a server-component page, calls it, and catches
@@ -1196,19 +1210,86 @@ async function loadModule(loader) {
1196
1210
  * See design/04-authorization.md, design/10-error-handling.md, TIM-666.
1197
1211
  */
1198
1212
  /**
1199
- * Find the first deny page in the chain that matches the given status code.
1200
- * Returns a React element for the matching component, or null if no match.
1213
+ * Find the first deny page in the chain whose status filter matches.
1214
+ * Returns null when nothing in the chain accepts this status.
1215
+ *
1216
+ * For 4xx statuses, the chain's stored order is correct: every status file
1217
+ * across all segments (inner→outer) before any error.tsx — so a root
1218
+ * `404.tsx` beats a leaf `error.tsx`. This matches `resolve4xx`.
1219
+ *
1220
+ * For 5xx statuses, the match walks per-segment (inner→outer): at each
1221
+ * segment, status files (`503.tsx` → `5xx.tsx`) then `error.tsx`, before
1222
+ * moving to the next segment outward. A leaf `error.tsx` therefore beats a
1223
+ * root `5xx.tsx` — the intentional inverse of the 4xx chain, matching
1224
+ * `resolve5xx`. See TIM-1360.
1201
1225
  */
1202
- function renderMatchingDenyPage(chain, status, data) {
1226
+ function matchDenyEntry(chain, status) {
1227
+ if (status >= 500) return matchDenyEntry5xx(chain, status);
1203
1228
  for (const entry of chain) {
1204
- if (entry.status === status) return renderDenyEntry(entry, status, data);
1205
- if (entry.status === 400 && status >= 400 && status <= 499) return renderDenyEntry(entry, status, data);
1206
- if (entry.status === 500 && status >= 500 && status <= 599) return renderDenyEntry(entry, status, data);
1207
- if (entry.status === null) return renderDenyEntry(entry, status, data);
1229
+ if (entry.status === status) return entry;
1230
+ if (entry.status === 400 && status >= 400 && status <= 499) return entry;
1231
+ if (entry.status === null) return entry;
1208
1232
  }
1209
1233
  return null;
1210
1234
  }
1211
1235
  /**
1236
+ * 5xx per-segment walk: at each segment (inner→outer), check status files
1237
+ * then error.tsx before moving outward. Mirrors `resolve5xx`.
1238
+ */
1239
+ function matchDenyEntry5xx(chain, status) {
1240
+ const segmentIndices = [];
1241
+ for (const entry of chain) if (!segmentIndices.includes(entry.segmentIndex)) segmentIndices.push(entry.segmentIndex);
1242
+ segmentIndices.sort((a, b) => b - a);
1243
+ for (const seg of segmentIndices) {
1244
+ for (const entry of chain) {
1245
+ if (entry.segmentIndex !== seg || entry.kind === "error") continue;
1246
+ if (entry.status === status) return entry;
1247
+ if (entry.status === 500 && status >= 500 && status <= 599) return entry;
1248
+ }
1249
+ for (const entry of chain) {
1250
+ if (entry.segmentIndex !== seg || entry.kind !== "error") continue;
1251
+ return entry;
1252
+ }
1253
+ }
1254
+ return null;
1255
+ }
1256
+ /**
1257
+ * Decide where the deny page for `status` renders, from the perspective of
1258
+ * the catch site at `selfKey`.
1259
+ *
1260
+ * The match itself is unchanged — first entry in fallback order wins, so
1261
+ * every path picks the same file. What this adds is the position: a chain
1262
+ * built at segment i spans i outward to root, so a matched entry is owned
1263
+ * either by the catcher or by one of its ancestors.
1264
+ *
1265
+ * - Owned by the catcher → `render`. The catcher already sits at the
1266
+ * owning segment's position.
1267
+ * - Owned by an ancestor → `hoist`. Rendering here would wrap the deny page
1268
+ * in every layout between the owner and the catcher. The caller re-throws
1269
+ * with `ownerKey` stamped on the signal so the boundary at that segment
1270
+ * renders it instead — inside the owner's layout, outside the rest.
1271
+ *
1272
+ * Returns null when no entry matches; the caller re-throws unannotated and
1273
+ * the re-render fallback resolves it.
1274
+ *
1275
+ * See design/04-authorization.md §"Where a Deny Page Renders", TIM-1356.
1276
+ */
1277
+ function placeDenyPage(chain, status, data, selfKey) {
1278
+ const entry = matchDenyEntry(chain, status);
1279
+ if (!entry) return null;
1280
+ if (entry.ownerKey !== selfKey) {
1281
+ if (entry.isMdx) return null;
1282
+ return {
1283
+ kind: "hoist",
1284
+ ownerKey: entry.ownerKey
1285
+ };
1286
+ }
1287
+ return {
1288
+ kind: "render",
1289
+ element: renderDenyEntry(entry, status, data)
1290
+ };
1291
+ }
1292
+ /**
1212
1293
  * Build the element for a matched deny chain entry with the props contract
1213
1294
  * the file's convention documents:
1214
1295
  *
@@ -1239,6 +1320,37 @@ function renderDenyEntry(entry, status, data) {
1239
1320
  });
1240
1321
  }
1241
1322
  /**
1323
+ * The whole of what an in-tree catch site does with a `DenySignal`, shared
1324
+ * by all three of them (`PageDenyBoundary`, `TracedLayout`, `AccessGate`).
1325
+ *
1326
+ * Returns the deny page element when this catch site owns the matched file.
1327
+ * Otherwise it throws — annotated with `ownerKey` when an ancestor owns the
1328
+ * file, bare when nothing matched at all.
1329
+ *
1330
+ * `setDenyStatus` runs on the hoist path too, and must run *before* the
1331
+ * throw: on the RSC payload path no boundary executes server-side, so the
1332
+ * ALS status is the only thing standing between the response and a 200
1333
+ * (`rsc-payload.ts` reads it after the stream settles).
1334
+ *
1335
+ * Either branch leaves part of the matched chain unmounted — hoisting sends
1336
+ * the render to a boundary that discards everything below it, and a layout
1337
+ * that denies returns the deny page instead of its own child outlet. What the
1338
+ * response then says about the tree is decided from the deny status alone in
1339
+ * `rsc-payload.ts`, not tracked here; see the note there for why.
1340
+ */
1341
+ function handleCaughtDeny(chain, error, selfKey) {
1342
+ const placement = chain ? placeDenyPage(chain, error.status, error.data, selfKey) : null;
1343
+ if (placement?.kind === "render") {
1344
+ setDenyStatus(error.status);
1345
+ return placement.element;
1346
+ }
1347
+ if (placement?.kind === "hoist") {
1348
+ setDenyStatus(error.status);
1349
+ error.ownerKey = placement.ownerKey;
1350
+ }
1351
+ throw error;
1352
+ }
1353
+ /**
1242
1354
  * Set the deny status in the request context ALS.
1243
1355
  * Called from AccessGate / PageDenyBoundary when a DenySignal is caught.
1244
1356
  * The pipeline reads this after render to set the HTTP status code.
@@ -1250,6 +1362,17 @@ function setDenyStatus(status) {
1250
1362
  store.onDenyStatus?.();
1251
1363
  }
1252
1364
  }
1365
+ /**
1366
+ * Record the first denied segment index in the request context ALS.
1367
+ * Called from AccessGate when a DenySignal is caught during render.
1368
+ * Only the first (shallowest) denial is recorded — deeper denials are
1369
+ * unreachable because the gate short-circuits the subtree.
1370
+ * See TIM-1363.
1371
+ */
1372
+ function setFirstDeniedSegmentIndex(index) {
1373
+ const store = requestContextAls.getStore();
1374
+ if (store && store._firstDeniedSegmentIndex === void 0) store._firstDeniedSegmentIndex = index;
1375
+ }
1253
1376
  //#endregion
1254
1377
  //#region src/server/access-gate.tsx
1255
1378
  /**
@@ -1269,37 +1392,37 @@ function setDenyStatus(status) {
1269
1392
  /**
1270
1393
  * Framework-injected access gate for segments.
1271
1394
  *
1272
- * When a pre-computed `verdict` prop is provided (from the pre-render pass
1273
- * in route-element-builder.ts), AccessGate replays it synchronously — no
1274
- * async, no re-execution of access.ts, immune to Suspense timing. The OTEL
1275
- * span was already emitted during the pre-render pass.
1276
- *
1277
- * When no verdict is provided (backward compat with tree-builder.ts),
1278
- * AccessGate calls accessFn directly with OTEL instrumentation.
1395
+ * Calls accessFn during render — inside `renderToReadableStream`, sharing
1396
+ * the same `React.cache` scope as layouts and pages. A `requireUser()`
1397
+ * call in access.ts populates the cache; the same call in any layout is
1398
+ * a free hit. Access executes exactly once per request. See TIM-1363.
1279
1399
  *
1280
1400
  * access.ts is a pure gate — return values are discarded. The layout below
1281
1401
  * gets the same data by calling the same cached functions (React.cache dedup).
1402
+ *
1403
+ * On denial the gate renders the matching deny page only when this segment
1404
+ * owns it. Because the gate is wrapped OUTSIDE its own layout, that render
1405
+ * omits the denying segment's layout — deliberate, and the one place the
1406
+ * in-tree path diverges from `renderDenyPage`'s inclusive slice: a layout
1407
+ * whose access.ts denied must never run and serialize protected data into
1408
+ * the Flight stream (TIM-1074). An ancestor-owned page is hoisted instead,
1409
+ * because the gate also sits inside every layout above it and rendering
1410
+ * here would wrap the deny page in all of them (TIM-1356).
1411
+ *
1412
+ * For skipped segments (skippedSegment=true), deny pages are NOT rendered
1413
+ * in-tree — the client's cached layout chrome would wrap them incorrectly.
1414
+ * Instead, the deny is recorded in ALS and rethrown so the response path
1415
+ * can return rscErrorEnvelope for a client hard-navigate.
1282
1416
  */
1283
1417
  function AccessGate(props) {
1284
- const { accessFn, segmentName, verdict, denyPages, children } = props;
1285
- if (verdict !== void 0) {
1286
- if (verdict === "pass") return children;
1287
- if (verdict instanceof DenySignal && denyPages) {
1288
- const denyElement = renderMatchingDenyPage(denyPages, verdict.status, verdict.data);
1289
- if (denyElement) {
1290
- setDenyStatus(verdict.status);
1291
- return denyElement;
1292
- }
1293
- }
1294
- throw verdict;
1295
- }
1296
- return accessGateFallback(accessFn, segmentName, denyPages, children);
1418
+ const { accessFn, segmentName, segmentKey, segmentIndex, denyPages, skippedSegment, onSettled, children } = props;
1419
+ return accessGateFallback(accessFn, segmentName, segmentKey, segmentIndex, skippedSegment ? void 0 : denyPages, skippedSegment, onSettled, children);
1297
1420
  }
1298
1421
  /**
1299
- * Async fallback for AccessGate when no pre-computed verdict is available.
1300
- * Calls accessFn with OTEL instrumentation.
1422
+ * Calls accessFn during render with OTEL instrumentation.
1423
+ * Records the first denied segment index in ALS for post-render consumers.
1301
1424
  */
1302
- async function accessGateFallback(accessFn, segmentName, denyPages, children) {
1425
+ async function accessGateFallback(accessFn, segmentName, segmentKey, segmentIndex, denyPages, skippedSegment, onSettled, children) {
1303
1426
  try {
1304
1427
  await withSpan("timber.access", { "timber.segment": segmentName ?? "unknown" }, async () => {
1305
1428
  try {
@@ -1315,14 +1438,17 @@ async function accessGateFallback(accessFn, segmentName, denyPages, children) {
1315
1438
  }
1316
1439
  });
1317
1440
  } catch (error) {
1318
- if (error instanceof DenySignal && denyPages) {
1319
- const denyElement = renderMatchingDenyPage(denyPages, error.status, error.data);
1320
- if (denyElement) {
1321
- setDenyStatus(error.status);
1322
- return denyElement;
1441
+ if (error instanceof DenySignal) {
1442
+ if (segmentIndex !== void 0) setFirstDeniedSegmentIndex(segmentIndex);
1443
+ if (skippedSegment) {
1444
+ error._hardNavigate = true;
1445
+ throw error;
1323
1446
  }
1447
+ if (denyPages) return handleCaughtDeny(denyPages, error, segmentKey);
1324
1448
  }
1325
1449
  throw error;
1450
+ } finally {
1451
+ onSettled?.();
1326
1452
  }
1327
1453
  return children;
1328
1454
  }
@@ -1744,69 +1870,248 @@ function wrapInSlotsProvider(shell, slotValues) {
1744
1870
  });
1745
1871
  }
1746
1872
  //#endregion
1873
+ //#region src/server/error-boundary-wrapper.ts
1874
+ /**
1875
+ * Load a status/error file's component, or `null` if the boundary should be skipped.
1876
+ *
1877
+ * Two ways to get `null`, both deliberate:
1878
+ * - the module fails to load (syntax error in the error page) — building an
1879
+ * error boundary must not itself throw, so the boundary is skipped (TIM-584);
1880
+ * - the default export is not a valid React component type, which would
1881
+ * otherwise crash inside `createElement`.
1882
+ */
1883
+ async function loadBoundaryComponent(file, loadModule) {
1884
+ let mod;
1885
+ try {
1886
+ mod = await loadModule(file);
1887
+ } catch {
1888
+ return null;
1889
+ }
1890
+ return isValidElementType(mod?.default) ? mod.default : null;
1891
+ }
1892
+ /**
1893
+ * Wrap `element` in one error boundary rendering `Component` on failure.
1894
+ *
1895
+ * MDX status files are server components and cannot be passed as function
1896
+ * props to `TimberErrorBoundary` (a 'use client' component). They are
1897
+ * pre-rendered as elements and passed as `fallbackElement` instead. See TIM-503.
1898
+ *
1899
+ * @param status boundary status filter — omitted entirely for `error.tsx`
1900
+ * @param fallbackProps props handed to an MDX fallback when it is pre-rendered
1901
+ */
1902
+ function wrapInBoundary(element, Component, file, status, fallbackProps, config) {
1903
+ const props = isMdxFilePath(file.filePath) ? {
1904
+ fallbackElement: config.createElement(Component, fallbackProps),
1905
+ children: element
1906
+ } : {
1907
+ fallbackComponent: Component,
1908
+ children: element
1909
+ };
1910
+ if (status !== void 0) props.status = status;
1911
+ if (config.segmentKey !== void 0) props.segmentKey = config.segmentKey;
1912
+ return config.createElement(config.errorBoundaryComponent, props);
1913
+ }
1914
+ /**
1915
+ * Wrap an element in the error boundaries declared by a route segment.
1916
+ *
1917
+ * Wrapping is applied inside-out — the last wrap produces the outermost
1918
+ * boundary — so the processing order below is innermost to outermost:
1919
+ *
1920
+ * 1. Specific status files (e.g. `404.tsx`, `503.tsx`) — highest priority at runtime
1921
+ * 2. Category catch-alls (`4xx.tsx`, `5xx.tsx`)
1922
+ * 3. `error.tsx` — catches anything not matched by a status file
1923
+ *
1924
+ * The order is load-bearing: it decides which boundary catches a given throw.
1925
+ *
1926
+ * See design/10-error-handling.md §"Format-Aware Fallback Chains".
1927
+ */
1928
+ async function wrapSegmentWithErrorBoundaries(segment, element, config) {
1929
+ const { loadModule, skipDenyBoundaries = false } = config;
1930
+ if (segment.statusFiles) {
1931
+ for (const [key, file] of Object.entries(segment.statusFiles)) {
1932
+ if (key === "4xx" || key === "5xx") continue;
1933
+ const status = parseInt(key, 10);
1934
+ if (isNaN(status)) continue;
1935
+ if (skipDenyBoundaries && status >= 400 && status <= 499) continue;
1936
+ const Component = await loadBoundaryComponent(file, loadModule);
1937
+ if (Component) element = wrapInBoundary(element, Component, file, status, { status }, config);
1938
+ }
1939
+ for (const [key, file] of Object.entries(segment.statusFiles)) {
1940
+ if (key !== "4xx" && key !== "5xx") continue;
1941
+ if (skipDenyBoundaries && key === "4xx") continue;
1942
+ const Component = await loadBoundaryComponent(file, loadModule);
1943
+ if (Component) element = wrapInBoundary(element, Component, file, key === "4xx" ? 400 : 500, {}, config);
1944
+ }
1945
+ }
1946
+ if (segment.error) {
1947
+ const Component = await loadBoundaryComponent(segment.error, loadModule);
1948
+ if (Component) element = wrapInBoundary(element, Component, segment.error, void 0, {}, config);
1949
+ }
1950
+ return element;
1951
+ }
1952
+ //#endregion
1747
1953
  //#region src/server/param-coercion.ts
1748
1954
  /** Global schema codecs, set once at startup from virtual:timber-schema. */
1749
1955
  var _globalCodecs = null;
1750
1956
  /**
1751
1957
  * Run segment param coercion on the matched route's segments.
1752
1958
  *
1753
- * When `globalCodecs` is provided (from app/schema.ts), uses the global
1754
- * codec map keyed by bare param name. Otherwise falls back to loading
1755
- * per-segment params.ts modules.
1959
+ * Codecs come from `app/schema.ts` and nowhere else (TIM-1342 completed
1960
+ * design/41-global-params.md migration step 9). A route with no schema, or
1961
+ * a param the schema does not declare, keeps its raw string — which is the
1962
+ * honest type for an uninterpreted URL segment.
1756
1963
  *
1757
1964
  * Throws ParamCoercionError if any codec fails (→ 404).
1758
1965
  *
1759
1966
  * This runs BEFORE middleware, so ctx.segmentParams is already typed.
1760
1967
  * See design/07-routing.md §"Where Coercion Runs"
1761
1968
  * See design/41-global-params.md §"Pipeline Integration"
1969
+ *
1970
+ * Stays `async` with nothing to await: it is injected as a
1971
+ * `CoerceSegmentParamsFn` into the revalidation renderer and the action
1972
+ * middleware runner, both of which await it. Narrowing the return type to
1973
+ * `void` would break those contracts and remove a microtask boundary the
1974
+ * pipeline's ordering has always had, for no gain.
1762
1975
  */
1763
1976
  async function coerceSegmentParams(match) {
1764
1977
  const globalCodecs = _globalCodecs;
1765
1978
  const mergeTarget = Object.create(null);
1766
1979
  for (const key of Object.keys(match.segmentParams)) if (key !== "__proto__") mergeTarget[key] = match.segmentParams[key];
1767
1980
  match.segmentParams = mergeTarget;
1768
- if (globalCodecs) {
1769
- for (const segment of match.segments) {
1770
- if (!segment.paramName) continue;
1771
- const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
1772
- const codec = globalCodecs[bracketKey];
1773
- if (!codec) continue;
1774
- const key = segment.paramName;
1775
- if (!(key in mergeTarget)) continue;
1776
- try {
1777
- mergeTarget[key] = sanitizeParamValue(codec.parse(mergeTarget[key]));
1778
- } catch (err) {
1779
- const message = err instanceof Error ? err.message : String(err);
1780
- if (isDebug()) console.warn(`[timber] Global schema codec rejected value for param "${key}"\n Error: ${message}\n Raw value: ${JSON.stringify(mergeTarget[key])}\n Hint: check the codec in app/schema.ts for key '${bracketKey}'.`);
1781
- throw new ParamCoercionError(message);
1782
- }
1783
- }
1784
- return;
1785
- }
1786
- for (const segment of match.segments) {
1787
- if (!segment.params) continue;
1788
- let mod;
1981
+ if (globalCodecs) for (const node of match.segments) {
1982
+ const segment = effectiveUrlSegment(node);
1983
+ if (!segment.paramName) continue;
1984
+ const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
1985
+ const codec = globalCodecs[bracketKey];
1986
+ if (!codec) continue;
1987
+ const key = segment.paramName;
1988
+ if (!(key in mergeTarget)) continue;
1989
+ let parsed;
1789
1990
  try {
1790
- mod = await loadModule(segment.params);
1991
+ parsed = codec.parse(mergeTarget[key]);
1791
1992
  } catch (err) {
1792
- const message = `Failed to load params module for segment "${segment.segmentName}": ${err instanceof Error ? err.message : String(err)}`;
1793
- if (isDebug()) console.warn(`[timber] Param coercion error: ${message}\n Segment: ${segment.segmentName}\n Params file: ${segment.params}`);
1993
+ const message = err instanceof Error ? err.message : String(err);
1994
+ if (isDebug()) console.warn(`[timber] Global schema codec rejected value for param "${key}"\n Error: ${message}\n Raw value: ${JSON.stringify(mergeTarget[key])}\n Hint: check the codec in app/schema.ts for key '${bracketKey}'.`);
1794
1995
  throw new ParamCoercionError(message);
1795
1996
  }
1796
- const segmentParamsDef = mod.segmentParams;
1797
- if (!segmentParamsDef || typeof segmentParamsDef.parse !== "function") continue;
1798
- try {
1799
- const coerced = segmentParamsDef.parse(match.segmentParams);
1800
- for (const key of Object.keys(coerced)) if (key !== "__proto__") mergeTarget[key] = sanitizeParamValue(coerced[key]);
1801
- } catch (err) {
1802
- const message = err instanceof Error ? err.message : String(err);
1803
- if (isDebug()) {
1804
- const rawKeys = Object.keys(match.segmentParams).join(", ");
1805
- console.warn(`[timber] Param codec rejected values for segment "${segment.segmentName}"\n Error: ${message}\n Available raw params: { ${rawKeys} }\n Params file: ${segment.params}\n Hint: this usually means a codec threw for the raw URL value.\n Check that the regex/schema accepts the actual path segment string.`);
1997
+ mergeTarget[key] = normalizeParamValue(parsed, key);
1998
+ }
1999
+ }
2000
+ //#endregion
2001
+ //#region src/server/pipeline-interception.ts
2002
+ /**
2003
+ * Interception route matching for the request pipeline.
2004
+ *
2005
+ * Matches target URLs against interception rewrites to support the
2006
+ * modal route pattern (soft navigation intercepts).
2007
+ *
2008
+ * Extracted from pipeline.ts to keep files under 500 lines.
2009
+ *
2010
+ * See design/07-routing.md §"Intercepting Routes"
2011
+ */
2012
+ /**
2013
+ * Check if an intercepting route applies for this soft navigation.
2014
+ *
2015
+ * Two conditions must hold:
2016
+ *
2017
+ * 1. The **target** pathname matches the rewrite's intercepted pattern.
2018
+ * 2. The **source** route — the page the user is navigating FROM, per the
2019
+ * validated X-Timber-URL header — renders the segment that owns the slot
2020
+ * holding the intercepting page (`interceptingScope`). Only then does that
2021
+ * slot exist in the rendered tree, so only then can the intercepting page
2022
+ * render.
2023
+ *
2024
+ * Condition 2 replaced a URL-prefix check (TIM-1279). A prefix cannot
2025
+ * distinguish a route-group-scoped interception from a genuinely root-level
2026
+ * one — route groups are invisible in `urlPath`, so both collapse to "/" and
2027
+ * the guard degenerates to always-true. Tree paths carry group identity.
2028
+ * The scope check also subsumes the old segment-boundary rule: a source route
2029
+ * under /feed-private has no "/feed" in its chain.
2030
+ *
2031
+ * A third condition lives with the caller, which owns route matching, and it
2032
+ * applies to the `'slot'` shape only: the route produced by re-matching
2033
+ * `interceptingPrefix` must pass through the scope too. Conditions 1–2 are
2034
+ * about the route the user is leaving; the slot has to exist in the tree that
2035
+ * actually renders. The `'children'` shape resolves its owner from the scope
2036
+ * directly rather than from a pathname, so it satisfies that by construction.
2037
+ *
2038
+ * `resolveSourceScopes` is a thunk so its inputs are only resolved when some
2039
+ * rewrite's pattern actually matches the target — the common case is no match
2040
+ * at all. It returns the two client-supplied answers to "what is on screen?"
2041
+ * as **separate sets, most authoritative first**: the tree paths the client
2042
+ * says it has mounted, then those of the route the departing URL matches. It
2043
+ * returns null only when neither yields anything, which is not a soft
2044
+ * navigation this can reason about.
2045
+ *
2046
+ * They are two sets rather than one union because ranking needs a chain
2047
+ * (TIM-1318). Each channel describes a single rendered chain, so within one the
2048
+ * scopes are strictly nested and the deepest is the innermost owner. Unioned
2049
+ * they are not: once a modal is open the address bar holds the *intercepted*
2050
+ * URL, so the second channel describes the canonical branch — possibly an
2051
+ * exclusive route group the client never mounted — and a deeper scope from
2052
+ * there would outrank the owner actually on screen. Trying the mounted channel
2053
+ * to exhaustion first keeps the fallback the union was there to provide while
2054
+ * ranking only within one chain.
2055
+ *
2056
+ * Returns the winning build-time rewrite, or null. Only build-time values ever
2057
+ * reach the renderer: the client-supplied header selects a rewrite but never
2058
+ * becomes a rendered route or pathname.
2059
+ *
2060
+ * The **first** matching rewrite wins, and the caller does not fall back to the
2061
+ * next one when the winner fails to resolve. That is deliberate and pre-dates
2062
+ * the two shapes: a rewrite that matched the target and the scope described the
2063
+ * author's intent, and quietly rendering some other author's interception
2064
+ * instead would be harder to explain than rendering the canonical route.
2065
+ *
2066
+ * "First" is meaningful because the list arrives **deepest scope first** —
2067
+ * `collectInterceptionRewrites` sorts it at build time, so the deepest owner a
2068
+ * single chain passes through wins and nothing here has to score (TIM-1318).
2069
+ * Tree-walk order used to decide it, which meant renaming a route group flipped
2070
+ * which modal opened. The precondition is a property of the manifest this reads
2071
+ * rather than something it can re-derive, so it is asserted at its source:
2072
+ * `tests/interception.test.ts` §"rewrite order is decided by scope depth".
2073
+ */
2074
+ function findInterceptionMatch(targetPathname, resolveSourceScopes, rewrites) {
2075
+ const candidates = rewrites.filter((rewrite) => pathnameMatchesPattern(targetPathname, rewrite.interceptedPattern));
2076
+ if (candidates.length === 0) return null;
2077
+ const scopes = resolveSourceScopes();
2078
+ if (!scopes) return null;
2079
+ for (const scope of scopes) for (const rewrite of candidates) if (scope.has(rewrite.interceptingScope)) return rewrite;
2080
+ return null;
2081
+ }
2082
+ /**
2083
+ * Check if a pathname matches a URL pattern with dynamic segments.
2084
+ *
2085
+ * Supports [param] (single segment) and [...param] (one or more segments).
2086
+ * Static segments must match exactly.
2087
+ */
2088
+ function pathnameMatchesPattern(pathname, pattern) {
2089
+ const pathParts = pathname === "/" ? [] : pathname.slice(1).split("/");
2090
+ const patternParts = pattern === "/" ? [] : pattern.slice(1).split("/");
2091
+ let pi = 0;
2092
+ for (let i = 0; i < patternParts.length; i++) {
2093
+ const seg = classifyUrlSegment(patternParts[i]);
2094
+ switch (seg.kind) {
2095
+ case "catch-all": return pi < pathParts.length;
2096
+ case "optional-catch-all": return true;
2097
+ case "dynamic": {
2098
+ if (pi >= pathParts.length) return false;
2099
+ const prefix = seg.prefix ?? "";
2100
+ const suffix = seg.suffix ?? "";
2101
+ const pathPart = pathParts[pi];
2102
+ if (prefix || suffix) {
2103
+ if (pathPart.length <= prefix.length + suffix.length || prefix && !pathPart.startsWith(prefix) || suffix && !pathPart.endsWith(suffix)) return false;
2104
+ }
2105
+ pi++;
2106
+ continue;
1806
2107
  }
1807
- throw new ParamCoercionError(message);
2108
+ case "static":
2109
+ if (pi >= pathParts.length || pathParts[pi] !== seg.value) return false;
2110
+ pi++;
2111
+ continue;
1808
2112
  }
1809
2113
  }
2114
+ return pi === pathParts.length;
1810
2115
  }
1811
2116
  //#endregion
1812
2117
  //#region src/client/segment-update-context.ts
@@ -1865,9 +2170,54 @@ getOrCreateContext();
1865
2170
  * See design/13-security.md §"State tree manipulation".
1866
2171
  */
1867
2172
  //#endregion
2173
+ //#region src/server/state-tree-diff.ts
2174
+ /**
2175
+ * State Tree Diffing — Server-side parsing and diffing of X-Timber-State-Tree.
2176
+ *
2177
+ * The client sends X-Timber-State-Tree on navigation requests, listing
2178
+ * the sync segments it has cached. The server diffs this against the
2179
+ * target route's segments to skip re-rendering unchanged sync layouts.
2180
+ *
2181
+ * This is a performance optimization only — NOT a security boundary.
2182
+ * All access.ts files run regardless of the state tree content.
2183
+ * A fabricated state tree can only cause extra rendering work or stale
2184
+ * layouts — never auth bypass.
2185
+ *
2186
+ * See design/19-client-navigation.md §"X-Timber-State-Tree Header"
2187
+ * See design/13-security.md §"State tree manipulation"
2188
+ */
2189
+ /**
2190
+ * Parse the X-Timber-State-Tree header from a request.
2191
+ *
2192
+ * Returns a Set of segment paths the client has cached (with an
2193
+ * additional `slots` property for cached slot paths), or null if
2194
+ * the header is missing, malformed, or empty. Parsing happens before
2195
+ * renderToReadableStream — not inside the React render pass.
2196
+ *
2197
+ * @returns ClientStateTree with segments and slots, or null if no valid state tree
2198
+ */
2199
+ function parseClientStateTree(req) {
2200
+ const header = req.headers.get("X-Timber-State-Tree");
2201
+ if (!header) return null;
2202
+ try {
2203
+ const parsed = JSON.parse(header);
2204
+ const hasSegments = Array.isArray(parsed.segments) && parsed.segments.length > 0;
2205
+ const hasSlots = Array.isArray(parsed.slots) && parsed.slots.length > 0;
2206
+ const hasTreePaths = Array.isArray(parsed.treePaths) && parsed.treePaths.length > 0;
2207
+ if (!hasSegments && !hasSlots && !hasTreePaths) return null;
2208
+ const result = new Set(hasSegments ? parsed.segments : []);
2209
+ result.slots = hasSlots ? new Set(parsed.slots) : null;
2210
+ result.treePaths = hasTreePaths ? new Set(parsed.treePaths) : null;
2211
+ return result;
2212
+ } catch (err) {
2213
+ swallow(err, "malformed X-Timber-State-Tree header");
2214
+ return null;
2215
+ }
2216
+ }
2217
+ //#endregion
1868
2218
  //#region src/server/route-element-builder.ts
1869
2219
  /**
1870
- * Thrown when a defineSegmentParams codec's parse() fails.
2220
+ * Thrown when an app/schema.ts segment-param codec's parse() fails.
1871
2221
  * The pipeline catches this and responds with 404.
1872
2222
  */
1873
2223
  var ParamCoercionError = class extends Error {
@@ -1942,78 +2292,6 @@ function escapeXml(str) {
1942
2292
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1943
2293
  }
1944
2294
  //#endregion
1945
- //#region src/server/pipeline-interception.ts
1946
- /**
1947
- * Interception route matching for the request pipeline.
1948
- *
1949
- * Matches target URLs against interception rewrites to support the
1950
- * modal route pattern (soft navigation intercepts).
1951
- *
1952
- * Extracted from pipeline.ts to keep files under 500 lines.
1953
- *
1954
- * See design/07-routing.md §"Intercepting Routes"
1955
- */
1956
- /**
1957
- * Check if a pathname starts with a prefix on a segment boundary.
1958
- *
1959
- * Prevents /feed from matching /feed-private — the prefix must be
1960
- * followed by '/' or be an exact match.
1961
- */
1962
- function hasSegmentPrefix(pathname, prefix) {
1963
- if (prefix === "/") return true;
1964
- if (!pathname.startsWith(prefix)) return false;
1965
- return pathname.length === prefix.length || pathname[prefix.length] === "/";
1966
- }
1967
- /**
1968
- * Check if an intercepting route applies for this soft navigation.
1969
- *
1970
- * Matches the target pathname against interception rewrites, constrained
1971
- * by the source URL (X-Timber-URL header — where the user navigates FROM).
1972
- *
1973
- * Returns the source pathname to re-match if interception applies, or null.
1974
- */
1975
- function findInterceptionMatch(targetPathname, sourceUrl, rewrites) {
1976
- for (const rewrite of rewrites) {
1977
- if (!hasSegmentPrefix(sourceUrl, rewrite.interceptingPrefix)) continue;
1978
- if (pathnameMatchesPattern(targetPathname, rewrite.interceptedPattern)) return { sourcePathname: rewrite.interceptingPrefix };
1979
- }
1980
- return null;
1981
- }
1982
- /**
1983
- * Check if a pathname matches a URL pattern with dynamic segments.
1984
- *
1985
- * Supports [param] (single segment) and [...param] (one or more segments).
1986
- * Static segments must match exactly.
1987
- */
1988
- function pathnameMatchesPattern(pathname, pattern) {
1989
- const pathParts = pathname === "/" ? [] : pathname.slice(1).split("/");
1990
- const patternParts = pattern === "/" ? [] : pattern.slice(1).split("/");
1991
- let pi = 0;
1992
- for (let i = 0; i < patternParts.length; i++) {
1993
- const seg = classifyUrlSegment(patternParts[i]);
1994
- switch (seg.kind) {
1995
- case "catch-all": return pi < pathParts.length;
1996
- case "optional-catch-all": return true;
1997
- case "dynamic": {
1998
- if (pi >= pathParts.length) return false;
1999
- const prefix = seg.prefix ?? "";
2000
- const suffix = seg.suffix ?? "";
2001
- const pathPart = pathParts[pi];
2002
- if (prefix || suffix) {
2003
- if (pathPart.length <= prefix.length + suffix.length || prefix && !pathPart.startsWith(prefix) || suffix && !pathPart.endsWith(suffix)) return false;
2004
- }
2005
- pi++;
2006
- continue;
2007
- }
2008
- case "static":
2009
- if (pi >= pathParts.length || pathParts[pi] !== seg.value) return false;
2010
- pi++;
2011
- continue;
2012
- }
2013
- }
2014
- return pi === pathParts.length;
2015
- }
2016
- //#endregion
2017
2295
  //#region src/server/pipeline-outcome.ts
2018
2296
  /**
2019
2297
  * Pipeline outcome translator — converts a `PhaseOutcome` (the value
@@ -2028,16 +2306,7 @@ function pathnameMatchesPattern(pathname, pattern) {
2028
2306
  */
2029
2307
  function rscErrorResponse(isRsc, status, headers) {
2030
2308
  if (!isRsc) return new Response(null, { status });
2031
- const h = headers ?? new Headers();
2032
- h.set("X-Timber-Error", "1");
2033
- h.set("content-type", "application/json; charset=utf-8");
2034
- return new Response(JSON.stringify({
2035
- error: true,
2036
- status
2037
- }), {
2038
- status,
2039
- headers: h
2040
- });
2309
+ return rscErrorEnvelope(status, headers);
2041
2310
  }
2042
2311
  /**
2043
2312
  * Terminal outcome handler — converts a `PhaseOutcome` into a final
@@ -2091,7 +2360,7 @@ async function outcomeToResponse(config, outcome, ctx) {
2091
2360
  });
2092
2361
  }
2093
2362
  case "error": {
2094
- const isRsc = (ctx.req.headers.get("Accept") ?? "").includes("text/x-component");
2363
+ const isRsc = isRscRequest(ctx.req);
2095
2364
  if (outcome.phase === "proxy") {
2096
2365
  logProxyError({ error: outcome.error });
2097
2366
  await fireOnRequestError(outcome.error, ctx.req, "proxy");
@@ -2334,15 +2603,6 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
2334
2603
  }
2335
2604
  const handlerResult = await mod.default();
2336
2605
  if (handlerResult instanceof Response) {
2337
- if (method === "HEAD") {
2338
- const headHeaders = new Headers(handlerResult.headers);
2339
- if (handlerResult.ok && !headHeaders.has("Cache-Control")) headHeaders.set("Cache-Control", "public, max-age=14400, must-revalidate");
2340
- return new Response(null, {
2341
- status: handlerResult.status,
2342
- statusText: handlerResult.statusText,
2343
- headers: headHeaders
2344
- });
2345
- }
2346
2606
  const res = cloneWithMutableHeaders(handlerResult);
2347
2607
  if (res.ok && !res.headers.has("Cache-Control")) res.headers.set("Cache-Control", "public, max-age=14400, must-revalidate");
2348
2608
  return res;
@@ -2387,8 +2647,8 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
2387
2647
  if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, "auto-sitemap");
2388
2648
  return new Response(null, { status: 500 });
2389
2649
  }
2390
- const isRscRequest = (req.headers.get("Accept") ?? "").includes("text/x-component");
2391
- if (isRscRequest) {
2650
+ const isRsc = isRscRequest(req);
2651
+ if (isRsc) {
2392
2652
  if (!checkVersionSkew(req).ok) {
2393
2653
  const reloadHeaders = new Headers();
2394
2654
  applyReloadHeaders(reloadHeaders);
@@ -2400,14 +2660,23 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
2400
2660
  }
2401
2661
  let match = config.matchRoute(canonicalPathname);
2402
2662
  let interception;
2403
- if (isRscRequest && config.interceptionRewrites?.length) {
2663
+ if (isRsc && config.interceptionRewrites?.length) {
2404
2664
  const rawSourceUrl = req.headers.get("X-Timber-URL");
2405
2665
  const validatedSourceUrl = rawSourceUrl ? validateInterceptionHeader(rawSourceUrl, stripTrailingSlash) : null;
2406
2666
  if (validatedSourceUrl) {
2407
- const intercepted = findInterceptionMatch(canonicalPathname, validatedSourceUrl, config.interceptionRewrites);
2408
- if (intercepted) {
2409
- const sourceMatch = config.matchRoute(intercepted.sourcePathname);
2410
- if (sourceMatch) {
2667
+ const resolveSourceScopes = () => {
2668
+ const mounted = new Set(parseClientStateTree(req)?.treePaths ?? []);
2669
+ const sourceRoute = config.matchRoute(validatedSourceUrl);
2670
+ const scopes = [mounted, new Set(sourceRoute ? computeSegmentTreePaths(sourceRoute.segments) : [])].filter((scope) => scope.size > 0);
2671
+ return scopes.length > 0 ? scopes : null;
2672
+ };
2673
+ const rewrite = findInterceptionMatch(canonicalPathname, resolveSourceScopes, config.interceptionRewrites);
2674
+ if (rewrite?.mode === "children") {
2675
+ const interceptedMatch = config.matchInterceptedRoute?.(canonicalPathname, rewrite.interceptingScope);
2676
+ if (interceptedMatch) match = interceptedMatch;
2677
+ } else if (rewrite) {
2678
+ const sourceMatch = config.matchRoute(rewrite.interceptingPrefix);
2679
+ if (sourceMatch !== null && computeSegmentTreePaths(sourceMatch.segments).includes(rewrite.interceptingScope)) {
2411
2680
  match = sourceMatch;
2412
2681
  interception = { targetPathname: canonicalPathname };
2413
2682
  }
@@ -2427,7 +2696,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
2427
2696
  responseHeaders.set("Cache-Control", "private, no-cache, no-store, max-age=0, must-revalidate");
2428
2697
  const leafSegment = match.segments[match.segments.length - 1];
2429
2698
  const isApiRoute = leafSegment?.route && !leafSegment?.page;
2430
- if (config.earlyHints && !isRscRequest && !isApiRoute) try {
2699
+ if (config.earlyHints && !isRsc && !isApiRoute) try {
2431
2700
  await config.earlyHints(match, req, responseHeaders);
2432
2701
  } catch (err) {
2433
2702
  swallow(err, "early hints hook threw");
@@ -2439,7 +2708,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
2439
2708
  if (error instanceof ParamCoercionError) {
2440
2709
  if (isDebug()) {
2441
2710
  const segmentChain = match.segments.map((s) => s.segmentName || "/").join(" → ");
2442
- 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 params.ts codec rejected the URL params.\n Check that all fields in defineSegmentParams() are optional for params\n that don't appear at every route depth (e.g. year, month, day).`);
2711
+ 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.`);
2443
2712
  }
2444
2713
  const leafSegment = match.segments[match.segments.length - 1];
2445
2714
  if (leafSegment.route && !leafSegment.page) return new Response(null, { status: 404 });
@@ -2466,6 +2735,188 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
2466
2735
  });
2467
2736
  }
2468
2737
  //#endregion
2738
+ //#region src/server/rsc-cache-key-guard.ts
2739
+ /**
2740
+ * Origin-side binding of the `_rsc` cache key to the request that produced
2741
+ * the response (TIM-1268).
2742
+ *
2743
+ * The key is deterministic, and deliberately so — that is what makes RSC
2744
+ * payloads cacheable. But deterministic also means *predictable*: anyone can
2745
+ * compute the key another client will request. Without this guard, a caller
2746
+ * could request `/about?_rsc=<victim's key>` while sending its own
2747
+ * `X-Timber-State-Tree`, and a shared cache that keys by URL — the exact
2748
+ * environment this feature targets — would store that variant under the
2749
+ * victim's key. Every subsequent legitimate navigation would then be served
2750
+ * a payload rendered for someone else's segment tree or deployment.
2751
+ *
2752
+ * The guard is a *storability* rule, not a rejection: if the claimed key
2753
+ * does not match the key derived from the received headers, the response
2754
+ * loses its shared-cache directives. Mismatched requests are still served
2755
+ * normally, they just cannot be stored for anyone else.
2756
+ *
2757
+ * Why not Next.js's 307 self-correction: a redirect is cacheable in its own
2758
+ * right, costs a round trip on every legitimate miss, and would make the
2759
+ * server's behaviour depend on a parameter it otherwise never reads. This
2760
+ * rule closes the same hole by making the poisoned entry unstorable at the
2761
+ * source, in the response the developer already has to opt into sharing.
2762
+ *
2763
+ * See design/19-client-navigation.md §"The `_rsc` cache key".
2764
+ */
2765
+ /**
2766
+ * Legacy shared-cache control fields that predate RFC 9213 and so do not
2767
+ * follow the `<target>-Cache-Control` naming pattern — the one part of this
2768
+ * rule that cannot be matched by shape. `X-Accel-Expires` is nginx
2769
+ * `proxy_cache`, which gives it precedence over `Cache-Control`.
2770
+ *
2771
+ * A closed historical set, unlike the vendor `*-Cache-Control` names, which
2772
+ * keep being minted and are handled by pattern below.
2773
+ */
2774
+ var LEGACY_CDN_CACHE_HEADERS = [
2775
+ "Surrogate-Control",
2776
+ "Edge-Control",
2777
+ "X-Accel-Expires"
2778
+ ];
2779
+ /**
2780
+ * Whether a response header overrides `Cache-Control` at a shared cache.
2781
+ *
2782
+ * Matched by *shape*, not by vendor name. RFC 9213 defines targeted fields
2783
+ * as `<target>-Cache-Control`, and every CDN spells its own: `CDN-`,
2784
+ * `Cloudflare-CDN-`, `Netlify-CDN-`, `Vercel-CDN-`, and whichever ships
2785
+ * next. A vendor list here is a denylist with an unbounded tail — it was
2786
+ * extended twice during review, each time by one name, which is the signal
2787
+ * that enumeration is the wrong shape. The suffix rule is total.
2788
+ *
2789
+ * `Cache-Control` itself does not match (no `-` prefix) and is replaced
2790
+ * separately.
2791
+ */
2792
+ function isSharedCacheOverride(name) {
2793
+ const lowered = name.toLowerCase();
2794
+ return lowered.endsWith("-cache-control") || LEGACY_CDN_CACHE_HEADERS.some((legacy) => legacy.toLowerCase() === lowered);
2795
+ }
2796
+ /**
2797
+ * Whether the request's `_rsc` parameter matches the key derived from its
2798
+ * own headers.
2799
+ *
2800
+ * Requests without the parameter are not claiming a key at all, so they are
2801
+ * left alone — the bare document URL is the developer's declared territory
2802
+ * (see design/32-response-caching.md, TIM-1264). Neither is a parameter
2803
+ * whose value could not be one of our digests: `_rsc` is not reserved, and
2804
+ * an application that already uses it for its own search state
2805
+ * (`/search?_rsc=foo`) must not have its cache policy silently revoked by a
2806
+ * guard that mistook that for a forgery.
2807
+ */
2808
+ async function hasValidRscCacheKey(req) {
2809
+ if (!req.url.includes("?")) return true;
2810
+ let values;
2811
+ try {
2812
+ values = new URL(req.url).searchParams.getAll(RSC_KEY_PARAM);
2813
+ } catch {
2814
+ return false;
2815
+ }
2816
+ const claims = values.filter(isRscCacheKeyShape);
2817
+ if (claims.length === 0) return true;
2818
+ const expected = await rscCacheKey((name) => req.headers.get(name));
2819
+ if (expected === null) return false;
2820
+ return claims.every((claim) => claim === expected);
2821
+ }
2822
+ /**
2823
+ * Ensure a Flight payload can never be interpreted as a document.
2824
+ *
2825
+ * Applied here, at the pipeline's single exit, rather than where each
2826
+ * payload response is built. Six places build one — the success path, three
2827
+ * in `deny-renderer.ts`, the action paths — and hardening them one at a time
2828
+ * is how you end up hardening five of six. The response is Flight if it says
2829
+ * it is; that is the only test that covers every producer, including ones
2830
+ * added later.
2831
+ *
2832
+ * Matters because the payload URL is observable and, on a shared-cache hit,
2833
+ * the origin is never consulted — so a top-level navigation can be handed
2834
+ * these bytes with no request-time check available. See design/13-security.md
2835
+ * #36c.
2836
+ */
2837
+ function hardenRscPayloadResponse(response) {
2838
+ const contentType = response.headers.get("content-type");
2839
+ if (!contentType) return;
2840
+ if (contentType.split(";")[0].trim().toLowerCase() !== "text/x-component") return;
2841
+ response.headers.set("X-Content-Type-Options", "nosniff");
2842
+ }
2843
+ /**
2844
+ * Make a response unstorable by any shared cache when its request claimed an
2845
+ * `_rsc` key that does not match its own headers.
2846
+ *
2847
+ * The end state is unconditional and therefore statable: **a mismatched
2848
+ * response is never stored by any cache.** Every targeted cache-control
2849
+ * field is removed and `Cache-Control` is replaced outright, rather than
2850
+ * downgrading only when a shared directive is spotted. Conditional variants
2851
+ * of this were tried and were wrong twice over — they missed the CDN-
2852
+ * specific headers, and leaving `Cache-Control` alone when it looked
2853
+ * harmless lets a CDN apply heuristic caching to a response that has none.
2854
+ *
2855
+ * The cost is that a mismatched request loses developer-chosen *browser*
2856
+ * caching too. That is a request no timber client ever makes.
2857
+ *
2858
+ * Purge tags (`Surrogate-Key`, `Cache-Tag`) are deliberately left alone:
2859
+ * they address stored entries, and this response is not stored.
2860
+ *
2861
+ * Mutates `response.headers` in place; the pipeline guarantees mutable
2862
+ * headers at this point (see TIM-866).
2863
+ */
2864
+ async function enforceRscCacheKey(req, response) {
2865
+ if (await hasValidRscCacheKey(req)) return;
2866
+ const overrides = [];
2867
+ for (const name of response.headers.keys()) if (isSharedCacheOverride(name)) overrides.push(name);
2868
+ for (const name of overrides) response.headers.delete(name);
2869
+ response.headers.set("Cache-Control", "private, no-store");
2870
+ }
2871
+ //#endregion
2872
+ //#region src/server/head-response.ts
2873
+ /**
2874
+ * HEAD body stripping — RFC 9110 §9.3.2.
2875
+ *
2876
+ * "The HEAD method is identical to GET except that the server MUST NOT send
2877
+ * content in the response." The headers, however, must be the ones the
2878
+ * corresponding GET would have produced — which is why timber renders the
2879
+ * route in full and discards the body here rather than short-circuiting the
2880
+ * render. Content-Type, Content-Length, Set-Cookie, cache directives and
2881
+ * everything else ride through untouched.
2882
+ *
2883
+ * Node's `ServerResponse` happens to discard writes for HEAD, so the wire is
2884
+ * usually correct even without this. That is a property of one adapter, not of
2885
+ * the `Response` object: static generation, the Cloudflare and Nitro adapters,
2886
+ * tests, and anything calling the pipeline handler as a function all observe
2887
+ * the `Response` directly and saw the full body before TIM-1324.
2888
+ *
2889
+ * The pipeline applies this at its single exit so every response shape is
2890
+ * covered — page render, API route, metadata route, deny page, error page,
2891
+ * proxy short-circuit. The two unit-level producers that are also reachable as
2892
+ * standalone functions (`handleRouteRequest`, `handleApiRoute`) call it too, so
2893
+ * each honours HEAD on its own; they share this implementation rather than
2894
+ * repeating it, and applying it twice is a no-op.
2895
+ */
2896
+ /**
2897
+ * Return a bodyless copy of `response` when `method` is HEAD.
2898
+ *
2899
+ * A no-op for every other method and for responses that already carry no body
2900
+ * (204, 304, redirects), so callers do not need to pre-check either.
2901
+ *
2902
+ * The discarded stream is cancelled: an SSR render that reached this point has
2903
+ * a live producer attached, and dropping the reference without cancelling
2904
+ * leaks it for the lifetime of the render (design/13-security.md A4).
2905
+ */
2906
+ function stripHeadBody(response, method) {
2907
+ if (method.toUpperCase() !== "HEAD") return response;
2908
+ const body = response.body;
2909
+ if (body === null) return response;
2910
+ try {
2911
+ body.cancel().catch(() => {});
2912
+ } catch {}
2913
+ return new Response(null, {
2914
+ status: response.status,
2915
+ statusText: response.statusText,
2916
+ headers: response.headers
2917
+ });
2918
+ }
2919
+ //#endregion
2469
2920
  //#region src/server/pipeline.ts
2470
2921
  /**
2471
2922
  * Create the request handler from a pipeline configuration.
@@ -2529,12 +2980,14 @@ function createPipeline(config) {
2529
2980
  });
2530
2981
  else result = await innerHandler();
2531
2982
  await setSpanAttribute("http.response.status_code", result.status);
2532
- const varyTokens = ["Accept"];
2533
- if (config.interceptionRewrites?.length) varyTokens.push("X-Timber-URL");
2983
+ const varyTokens = [...RSC_KEY_HEADERS];
2534
2984
  const existingVary = result.headers.get("Vary");
2535
2985
  const existingTokens = existingVary ? existingVary.toLowerCase().split(",").map((t) => t.trim()) : [];
2536
2986
  const newTokens = varyTokens.filter((t) => !existingTokens.includes(t.toLowerCase()));
2537
2987
  if (newTokens.length > 0) result.headers.set("Vary", existingVary ? `${existingVary}, ${newTokens.join(", ")}` : newTokens.join(", "));
2988
+ await enforceRscCacheKey(canonicalReq, result);
2989
+ hardenRscPayloadResponse(result);
2990
+ result = stripHeadBody(result, method);
2538
2991
  if (serverTiming === "detailed") {
2539
2992
  const timingHeader = getServerTimingHeader();
2540
2993
  if (timingHeader) result.headers.set("Server-Timing", timingHeader);
@@ -2576,68 +3029,6 @@ function createPipeline(config) {
2576
3029
  return pipelineFn;
2577
3030
  }
2578
3031
  //#endregion
2579
- //#region src/server/build-manifest.ts
2580
- /**
2581
- * Collect all CSS files needed for a matched route's segment chain.
2582
- *
2583
- * Walks segments root → leaf, collecting CSS for each layout and page.
2584
- * Deduplicates while preserving order (root layout CSS first).
2585
- */
2586
- function collectRouteCss(segments, manifest) {
2587
- const seen = /* @__PURE__ */ new Set();
2588
- const result = [];
2589
- for (const segment of segments) for (const file of [segment.layout, segment.page]) {
2590
- if (!file) continue;
2591
- const cssFiles = manifest.css[file.filePath];
2592
- if (!cssFiles) continue;
2593
- for (const url of cssFiles) if (!seen.has(url)) {
2594
- seen.add(url);
2595
- result.push(url);
2596
- }
2597
- }
2598
- return result;
2599
- }
2600
- /**
2601
- * Collect all font entries needed for a matched route's segment chain.
2602
- *
2603
- * Walks segments root → leaf, collecting fonts for each layout and page.
2604
- * Deduplicates by href while preserving order.
2605
- */
2606
- function collectRouteFonts(segments, manifest) {
2607
- const seen = /* @__PURE__ */ new Set();
2608
- const result = [];
2609
- for (const segment of segments) for (const file of [segment.layout, segment.page]) {
2610
- if (!file) continue;
2611
- const fonts = manifest.fonts[file.filePath];
2612
- if (!fonts) continue;
2613
- for (const entry of fonts) if (!seen.has(entry.href)) {
2614
- seen.add(entry.href);
2615
- result.push(entry);
2616
- }
2617
- }
2618
- return result;
2619
- }
2620
- /**
2621
- * Collect modulepreload URLs for a matched route's segment chain.
2622
- *
2623
- * Walks segments root → leaf, collecting transitive JS dependencies
2624
- * for each layout and page. Deduplicates across segments.
2625
- */
2626
- function collectRouteModulepreloads(segments, manifest) {
2627
- const seen = /* @__PURE__ */ new Set();
2628
- const result = [];
2629
- for (const segment of segments) for (const file of [segment.layout, segment.page]) {
2630
- if (!file) continue;
2631
- const preloads = manifest.modulepreload[file.filePath];
2632
- if (!preloads) continue;
2633
- for (const url of preloads) if (!seen.has(url)) {
2634
- seen.add(url);
2635
- result.push(url);
2636
- }
2637
- }
2638
- return result;
2639
- }
2640
- //#endregion
2641
3032
  //#region src/server/early-hints.ts
2642
3033
  /**
2643
3034
  * 103 Early Hints utilities.
@@ -2771,40 +3162,6 @@ function sendEarlyHints103(links) {
2771
3162
  }
2772
3163
  //#endregion
2773
3164
  //#region src/server/tree-builder.ts
2774
- var REACT_COMPONENT_TYPE_MARKERS = /* @__PURE__ */ new Set([
2775
- Symbol.for("react.forward_ref"),
2776
- Symbol.for("react.memo"),
2777
- Symbol.for("react.lazy"),
2778
- Symbol.for("react.provider"),
2779
- Symbol.for("react.context"),
2780
- Symbol.for("react.suspense"),
2781
- Symbol.for("react.suspense_list"),
2782
- Symbol.for("react.client.reference")
2783
- ]);
2784
- /**
2785
- * Validate that a loaded module's `default` export is something React
2786
- * accepts as the first argument to `createElement` — i.e. a valid component
2787
- * type. React doesn't export `isValidElementType` (only `isValidElement`,
2788
- * which checks for *elements*, not *component types*), so this mirrors
2789
- * React's internal check:
2790
- *
2791
- * - functions → function or class components
2792
- * - objects with a `$$typeof` matching one of React's known component
2793
- * markers → exotic components (`memo`, `forwardRef`, `lazy`, context,
2794
- * suspense, client references via `@vitejs/plugin-rsc`)
2795
- *
2796
- * Strings (HTML tag names) are valid for `createElement` but never appear
2797
- * as a route module's default export, so they're not recognized here.
2798
- *
2799
- * Anything else (numbers, plain config objects, JSON, etc.) is rejected so
2800
- * the boundary wrapper is skipped rather than crashing inside React.
2801
- */
2802
- function isValidElementType(value) {
2803
- if (typeof value === "function") return true;
2804
- if (typeof value !== "object" || value === null) return false;
2805
- const marker = value.$$typeof;
2806
- return typeof marker === "symbol" && REACT_COMPONENT_TYPE_MARKERS.has(marker);
2807
- }
2808
3165
  /**
2809
3166
  * Build the unified element tree from a matched segment chain.
2810
3167
  *
@@ -2830,7 +3187,11 @@ async function buildElementTree(config) {
2830
3187
  let element = createElement(PageComponent, {});
2831
3188
  for (let i = segments.length - 1; i >= 0; i--) {
2832
3189
  const segment = segments[i];
2833
- element = await wrapWithErrorBoundaries(segment, element, loadModule, createElement, errorBoundaryComponent);
3190
+ element = await wrapSegmentWithErrorBoundaries(segment, element, {
3191
+ loadModule,
3192
+ createElement,
3193
+ errorBoundaryComponent
3194
+ });
2834
3195
  if (segment.access) {
2835
3196
  const accessFn = (await loadModule(segment.access)).default;
2836
3197
  element = createElement("timber:access-gate", {
@@ -2871,7 +3232,11 @@ async function buildSlotElement(slotNode, loadModule, createElement, errorBounda
2871
3232
  const DefaultComponent = (slotNode.default ? await loadModule(slotNode.default) : null)?.default;
2872
3233
  if (!PageComponent) return DefaultComponent ? createElement(DefaultComponent, {}) : null;
2873
3234
  let element = createElement(PageComponent, {});
2874
- element = await wrapWithErrorBoundaries(slotNode, element, loadModule, createElement, errorBoundaryComponent);
3235
+ element = await wrapSegmentWithErrorBoundaries(slotNode, element, {
3236
+ loadModule,
3237
+ createElement,
3238
+ errorBoundaryComponent
3239
+ });
2875
3240
  if (slotNode.access) {
2876
3241
  const accessFn = (await loadModule(slotNode.access)).default;
2877
3242
  const DeniedComponent = (slotNode.denied ? await loadModule(slotNode.denied) : null)?.default ?? null;
@@ -2887,81 +3252,6 @@ async function buildSlotElement(slotNode, loadModule, createElement, errorBounda
2887
3252
  }
2888
3253
  return element;
2889
3254
  }
2890
- /** MDX/markdown extensions — these are server components that cannot be passed as function props. */
2891
- var MDX_EXTENSIONS = /* @__PURE__ */ new Set(["mdx", "md"]);
2892
- /**
2893
- * Check if a route file is an MDX/markdown file based on its extension.
2894
- * MDX components are server components by default and cannot cross the
2895
- * RSC→client boundary as function props. They must be pre-rendered as
2896
- * elements and passed as fallbackElement instead of fallbackComponent.
2897
- */
2898
- function isMdxFile(file) {
2899
- return MDX_EXTENSIONS.has(file.extension);
2900
- }
2901
- /**
2902
- * Wrap an element with error boundaries from a segment's status-code files.
2903
- *
2904
- * Wrapping order (innermost to outermost):
2905
- * 1. Specific status files (503.tsx, 429.tsx, etc.)
2906
- * 2. Category catch-alls (4xx.tsx, 5xx.tsx)
2907
- * 3. error.tsx (general error boundary)
2908
- *
2909
- * This creates the fallback chain described in design/10-error-handling.md.
2910
- *
2911
- * MDX status files are server components and cannot be passed as function
2912
- * props to TimberErrorBoundary (a 'use client' component). Instead, they
2913
- * are pre-rendered as elements and passed as fallbackElement. The error
2914
- * boundary renders the element directly when an error is caught.
2915
- * See TIM-503.
2916
- */
2917
- async function wrapWithErrorBoundaries(segment, element, loadModule, createElement, errorBoundaryComponent) {
2918
- if (segment.statusFiles) {
2919
- for (const [key, file] of Object.entries(segment.statusFiles)) if (key !== "4xx" && key !== "5xx") {
2920
- const status = parseInt(key, 10);
2921
- if (!isNaN(status)) {
2922
- const mod = await loadModule(file);
2923
- const Component = isValidElementType(mod.default) ? mod.default : null;
2924
- if (Component) element = createElement(errorBoundaryComponent, isMdxFile(file) ? {
2925
- fallbackElement: createElement(Component, { status }),
2926
- status,
2927
- children: element
2928
- } : {
2929
- fallbackComponent: Component,
2930
- status,
2931
- children: element
2932
- });
2933
- }
2934
- }
2935
- for (const [key, file] of Object.entries(segment.statusFiles)) if (key === "4xx" || key === "5xx") {
2936
- const mod = await loadModule(file);
2937
- const Component = isValidElementType(mod.default) ? mod.default : null;
2938
- if (Component) {
2939
- const categoryStatus = key === "4xx" ? 400 : 500;
2940
- element = createElement(errorBoundaryComponent, isMdxFile(file) ? {
2941
- fallbackElement: createElement(Component, {}),
2942
- status: categoryStatus,
2943
- children: element
2944
- } : {
2945
- fallbackComponent: Component,
2946
- status: categoryStatus,
2947
- children: element
2948
- });
2949
- }
2950
- }
2951
- }
2952
- if (segment.error) {
2953
- const errorModule = await loadModule(segment.error);
2954
- const ErrorComponent = isValidElementType(errorModule.default) ? errorModule.default : null;
2955
- if (ErrorComponent) element = createElement(errorBoundaryComponent, isMdxFile(segment.error) ? {
2956
- fallbackElement: createElement(ErrorComponent, {}),
2957
- children: element
2958
- } : {
2959
- fallbackComponent: ErrorComponent,
2960
- children: element
2961
- });
2962
- }
2963
- return element;
2964
- }
2965
3255
  //#endregion
2966
3256
  //#region src/server/status-code-resolver.ts
2967
3257
  /**
@@ -2999,12 +3289,19 @@ function lookupInGroup(group, statusStr, categoryKey, segmentIndex, status) {
2999
3289
  * @param status - The HTTP status code (4xx or 5xx).
3000
3290
  * @param segments - The matched segment chain from root (index 0) to leaf (last).
3001
3291
  * @param format - The response format family ('component' or 'json'). Defaults to 'component'.
3002
- */
3003
- function resolveStatusFile(status, segments, format = "component") {
3292
+ * @param maxIndex - Optional upper bound on the segment index to search.
3293
+ * When set, the resolver only considers segments `0..maxIndex` (inclusive),
3294
+ * skipping any segments deeper in the tree. Used by the deny renderers to
3295
+ * prevent resolving a status file below a denied segment — matching
3296
+ * `buildDenyPageChain`'s start-at-i semantics. The error renderers omit
3297
+ * this parameter, so the full chain is searched — a 500 from a leaf page
3298
+ * should use the leaf's own `5xx.tsx`. See TIM-1358.
3299
+ */
3300
+ function resolveStatusFile(status, segments, format = "component", maxIndex) {
3004
3301
  if (status < 400 || status > 599) return null;
3005
- if (format === "json") return resolveJson(status, segments);
3006
- if (status <= 499) return resolve4xx(status, segments);
3007
- return resolve5xx(status, segments);
3302
+ if (format === "json") return resolveJson(status, segments, maxIndex);
3303
+ if (status <= 499) return resolve4xx(status, segments, maxIndex);
3304
+ return resolve5xx(status, segments, maxIndex);
3008
3305
  }
3009
3306
  /**
3010
3307
  * 4xx component fallback chain — two separate full passes leaf→root.
@@ -3018,13 +3315,14 @@ function resolveStatusFile(status, segments, format = "component") {
3018
3315
  * Pass 1 — {status}.tsx → 4xx.tsx (statusFiles)
3019
3316
  * Pass 2 — error.tsx (error)
3020
3317
  */
3021
- function resolve4xx(status, segments) {
3318
+ function resolve4xx(status, segments, maxIndex) {
3022
3319
  const statusStr = String(status);
3023
- for (let i = segments.length - 1; i >= 0; i--) {
3320
+ const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
3321
+ for (let i = start; i >= 0; i--) {
3024
3322
  const r = lookupInGroup(segments[i].statusFiles, statusStr, "4xx", i, status);
3025
3323
  if (r) return r;
3026
3324
  }
3027
- for (let i = segments.length - 1; i >= 0; i--) {
3325
+ for (let i = start; i >= 0; i--) {
3028
3326
  const errorFile = segments[i].error;
3029
3327
  if (errorFile) return {
3030
3328
  file: errorFile,
@@ -3042,9 +3340,10 @@ function resolve4xx(status, segments) {
3042
3340
  * `error.tsx` therefore beats a root's `5xx.tsx`, which is the
3043
3341
  * intentional inverse of the 4xx chain.
3044
3342
  */
3045
- function resolve5xx(status, segments) {
3343
+ function resolve5xx(status, segments, maxIndex) {
3046
3344
  const statusStr = String(status);
3047
- for (let i = segments.length - 1; i >= 0; i--) {
3345
+ const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
3346
+ for (let i = start; i >= 0; i--) {
3048
3347
  const segment = segments[i];
3049
3348
  const r = lookupInGroup(segment.statusFiles, statusStr, "5xx", i, status);
3050
3349
  if (r) return r;
@@ -3064,10 +3363,11 @@ function resolve5xx(status, segments) {
3064
3363
  * no error.tsx — the JSON chain terminates at the category catch-all
3065
3364
  * and the caller falls back to a bare-JSON framework default.
3066
3365
  */
3067
- function resolveJson(status, segments) {
3366
+ function resolveJson(status, segments, maxIndex) {
3068
3367
  const statusStr = String(status);
3069
3368
  const categoryKey = status >= 500 ? "5xx" : "4xx";
3070
- for (let i = segments.length - 1; i >= 0; i--) {
3369
+ const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
3370
+ for (let i = start; i >= 0; i--) {
3071
3371
  const r = lookupInGroup(segments[i].jsonStatusFiles, statusStr, categoryKey, i, status);
3072
3372
  if (r) return r;
3073
3373
  }
@@ -3277,14 +3577,8 @@ async function handleRouteRequest(mod, ctx) {
3277
3577
  });
3278
3578
  }
3279
3579
  if (method === "HEAD") {
3280
- if (mod.HEAD) return runHandler(mod.HEAD, ctx);
3281
- if (mod.GET) {
3282
- const res = await runHandler(mod.GET, ctx);
3283
- return new Response(null, {
3284
- status: res.status,
3285
- headers: res.headers
3286
- });
3287
- }
3580
+ if (mod.HEAD) return stripHeadBody(await runHandler(mod.HEAD, ctx), method);
3581
+ if (mod.GET) return stripHeadBody(await runHandler(mod.GET, ctx), method);
3288
3582
  }
3289
3583
  const handler = mod[method];
3290
3584
  if (!handler) return new Response(null, {
@@ -3511,11 +3805,11 @@ var registry = /* @__PURE__ */ new Map();
3511
3805
  */
3512
3806
  function resolveKeyParams(store) {
3513
3807
  const main = store.segmentParams ?? {};
3514
- const map = store.slotParamsMap;
3515
- if (!map || map.size === 0) return main;
3808
+ const slots = store.slotParams;
3809
+ if (!slots || Object.keys(slots).length === 0) return main;
3516
3810
  const mainCanonical = tryCanonicalize({ ...main });
3517
3811
  if (mainCanonical === null) return null;
3518
- for (const slotParams of map.values()) {
3812
+ for (const slotParams of Object.values(slots)) {
3519
3813
  const merged = tryCanonicalize({
3520
3814
  ...main,
3521
3815
  ...slotParams