@timber-js/app 0.2.0-alpha.184 → 0.2.0-alpha.186

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 (441) hide show
  1. package/dist/_chunks/{actions-O_LsyCE4.js → actions-C-Rw9vPc.js} +3 -3
  2. package/dist/_chunks/{actions-O_LsyCE4.js.map → actions-C-Rw9vPc.js.map} +1 -1
  3. package/dist/_chunks/als-slots-mFweg276.js +27 -0
  4. package/dist/_chunks/als-slots-mFweg276.js.map +1 -0
  5. package/dist/_chunks/base-path-DaQrzbez.js +80 -0
  6. package/dist/_chunks/base-path-DaQrzbez.js.map +1 -0
  7. package/dist/_chunks/build-manifest-DWppEdLB.js +86 -0
  8. package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
  9. package/dist/_chunks/build-output-helper-C3DrfzZR.js +369 -0
  10. package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +1 -0
  11. package/dist/_chunks/{cache-api-B-lhk9p4.js → cache-api-Cd0VZ_Pd.js} +3 -3
  12. package/dist/_chunks/{cache-api-B-lhk9p4.js.map → cache-api-Cd0VZ_Pd.js.map} +1 -1
  13. package/dist/_chunks/cli-schema-sync-CKgHC2MB.js +1857 -0
  14. package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +1 -0
  15. package/dist/_chunks/{cloudflare-CnT5Lr7U.js → cloudflare-Cs0uZXea.js} +89 -12
  16. package/dist/_chunks/{cloudflare-CnT5Lr7U.js.map → cloudflare-Cs0uZXea.js.map} +1 -1
  17. package/dist/_chunks/error-boundary-DpYRI_I1.js +330 -0
  18. package/dist/_chunks/error-boundary-DpYRI_I1.js.map +1 -0
  19. package/dist/_chunks/{fast-hash-D6hIVt1Y.js → fast-hash-C-tVYrLb.js} +6 -1
  20. package/dist/_chunks/fast-hash-C-tVYrLb.js.map +1 -0
  21. package/dist/_chunks/fs-identity-D7vb6GdI.js +208 -0
  22. package/dist/_chunks/fs-identity-D7vb6GdI.js.map +1 -0
  23. package/dist/_chunks/{logger-AWfuX-KJ.js → logger-N7e5auP0.js} +9 -13
  24. package/dist/_chunks/logger-N7e5auP0.js.map +1 -0
  25. package/dist/_chunks/navigation-root-B29qg0_T.js +274 -0
  26. package/dist/_chunks/navigation-root-B29qg0_T.js.map +1 -0
  27. package/dist/_chunks/param-value-C8TNYchQ.js +66 -0
  28. package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
  29. package/dist/_chunks/{plugin-context---kTF5v8.js → plugin-context-DEGLSJs3.js} +2 -1
  30. package/dist/_chunks/plugin-context-DEGLSJs3.js.map +1 -0
  31. package/dist/_chunks/{router-ref-CtmF-aPv.js → router-ref-DuYuV_0Q.js} +2 -2
  32. package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -0
  33. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js +243 -0
  34. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js.map +1 -0
  35. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +40 -0
  36. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +1 -0
  37. package/dist/_chunks/rsc-payload-path-B_LBodc2.js +48 -0
  38. package/dist/_chunks/rsc-payload-path-B_LBodc2.js.map +1 -0
  39. package/dist/_chunks/{segment-classify-Byy425ng.js → segment-classify-C539Pa2O.js} +23 -2
  40. package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -0
  41. package/dist/_chunks/{canonicalize-DQHyFClh.js → segment-keys-BawYuNFO.js} +61 -2
  42. package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -0
  43. package/dist/_chunks/slot-params-BCTmZkQB.js +76 -0
  44. package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -0
  45. package/dist/_chunks/{ssr-data-BOWsq18U.js → ssr-data-14MXm7Pj.js} +17 -2
  46. package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -0
  47. package/dist/_chunks/use-segment-params-ClyUNq4d.js +128 -0
  48. package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +1 -0
  49. package/dist/_chunks/{walkers-BtTlKviE.js → walkers-BhhwI9TD.js} +21 -87
  50. package/dist/_chunks/walkers-BhhwI9TD.js.map +1 -0
  51. package/dist/adapters/build-output-helper.d.ts +37 -0
  52. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  53. package/dist/adapters/cloudflare-dev.js +1 -1
  54. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  55. package/dist/adapters/cloudflare.d.ts.map +1 -1
  56. package/dist/adapters/cloudflare.js +1 -1
  57. package/dist/adapters/fs-identity.d.ts +64 -0
  58. package/dist/adapters/fs-identity.d.ts.map +1 -0
  59. package/dist/adapters/nitro.d.ts.map +1 -1
  60. package/dist/adapters/nitro.js +2 -3
  61. package/dist/adapters/nitro.js.map +1 -1
  62. package/dist/adapters/shared.d.ts +57 -1
  63. package/dist/adapters/shared.d.ts.map +1 -1
  64. package/dist/adapters/types.d.ts +13 -1
  65. package/dist/adapters/types.d.ts.map +1 -1
  66. package/dist/cache/fast-hash.d.ts +5 -0
  67. package/dist/cache/fast-hash.d.ts.map +1 -1
  68. package/dist/cache/index.js +1 -1
  69. package/dist/cli.js +2 -2
  70. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  71. package/dist/client/browser-entry/hydrate.d.ts +20 -9
  72. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  73. package/dist/client/browser-entry/post-hydration.d.ts +4 -1
  74. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  75. package/dist/client/browser-entry/router-init.d.ts +3 -0
  76. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  77. package/dist/client/browser-entry/rsc-stream.d.ts +8 -0
  78. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
  79. package/dist/client/error-boundary.d.ts +1 -0
  80. package/dist/client/error-boundary.d.ts.map +1 -1
  81. package/dist/client/error-boundary.js +1 -162
  82. package/dist/client/history.d.ts +13 -6
  83. package/dist/client/history.d.ts.map +1 -1
  84. package/dist/client/index.js +18 -11
  85. package/dist/client/index.js.map +1 -1
  86. package/dist/client/internal.d.ts +4 -3
  87. package/dist/client/internal.d.ts.map +1 -1
  88. package/dist/client/internal.js +872 -305
  89. package/dist/client/internal.js.map +1 -1
  90. package/dist/client/link.d.ts.map +1 -1
  91. package/dist/client/navigation-api.d.ts +2 -2
  92. package/dist/client/navigation-commit.d.ts +71 -0
  93. package/dist/client/navigation-commit.d.ts.map +1 -0
  94. package/dist/client/navigation-context.d.ts +37 -20
  95. package/dist/client/navigation-context.d.ts.map +1 -1
  96. package/dist/client/navigation-root.d.ts +63 -23
  97. package/dist/client/navigation-root.d.ts.map +1 -1
  98. package/dist/client/params-context.d.ts +81 -0
  99. package/dist/client/params-context.d.ts.map +1 -0
  100. package/dist/client/router-effects.d.ts +129 -0
  101. package/dist/client/router-effects.d.ts.map +1 -0
  102. package/dist/client/router-lifecycle.d.ts +55 -0
  103. package/dist/client/router-lifecycle.d.ts.map +1 -0
  104. package/dist/client/router-pipeline.d.ts +84 -0
  105. package/dist/client/router-pipeline.d.ts.map +1 -0
  106. package/dist/client/router-ref.d.ts +1 -1
  107. package/dist/client/router-ref.d.ts.map +1 -1
  108. package/dist/client/router-skew.d.ts +21 -0
  109. package/dist/client/router-skew.d.ts.map +1 -0
  110. package/dist/client/router-types.d.ts +207 -0
  111. package/dist/client/router-types.d.ts.map +1 -0
  112. package/dist/client/router.d.ts +1 -182
  113. package/dist/client/router.d.ts.map +1 -1
  114. package/dist/client/rsc-fetch.d.ts +39 -16
  115. package/dist/client/rsc-fetch.d.ts.map +1 -1
  116. package/dist/client/segment-cache.d.ts +79 -34
  117. package/dist/client/segment-cache.d.ts.map +1 -1
  118. package/dist/client/ssr-data.d.ts +6 -0
  119. package/dist/client/ssr-data.d.ts.map +1 -1
  120. package/dist/client/stale-client.d.ts +69 -0
  121. package/dist/client/stale-client.d.ts.map +1 -0
  122. package/dist/client/state.d.ts +10 -1
  123. package/dist/client/state.d.ts.map +1 -1
  124. package/dist/client/top-loader.d.ts +2 -1
  125. package/dist/client/top-loader.d.ts.map +1 -1
  126. package/dist/client/unload-guard.d.ts +5 -0
  127. package/dist/client/unload-guard.d.ts.map +1 -1
  128. package/dist/client/use-cookie.d.ts.map +1 -1
  129. package/dist/client/use-router.d.ts +1 -1
  130. package/dist/client/use-segment-params.d.ts +18 -8
  131. package/dist/client/use-segment-params.d.ts.map +1 -1
  132. package/dist/cookies/index.js +3 -3
  133. package/dist/cookies/index.js.map +1 -1
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +915 -320
  136. package/dist/index.js.map +1 -1
  137. package/dist/plugin-context.d.ts +33 -1
  138. package/dist/plugin-context.d.ts.map +1 -1
  139. package/dist/plugins/adapter-build.d.ts.map +1 -1
  140. package/dist/plugins/build-manifest.d.ts +1 -1
  141. package/dist/plugins/build-manifest.d.ts.map +1 -1
  142. package/dist/plugins/entries.d.ts.map +1 -1
  143. package/dist/plugins/fonts.d.ts.map +1 -1
  144. package/dist/plugins/request-dep/analysis.d.ts +106 -0
  145. package/dist/plugins/request-dep/analysis.d.ts.map +1 -0
  146. package/dist/plugins/request-dep/index.d.ts +75 -0
  147. package/dist/plugins/request-dep/index.d.ts.map +1 -0
  148. package/dist/plugins/request-dep/origin.d.ts +61 -0
  149. package/dist/plugins/request-dep/origin.d.ts.map +1 -0
  150. package/dist/plugins/routing.d.ts +9 -0
  151. package/dist/plugins/routing.d.ts.map +1 -1
  152. package/dist/plugins/static-build.d.ts +149 -8
  153. package/dist/plugins/static-build.d.ts.map +1 -1
  154. package/dist/routing/codegen-types.d.ts +18 -0
  155. package/dist/routing/codegen-types.d.ts.map +1 -1
  156. package/dist/routing/codegen.d.ts.map +1 -1
  157. package/dist/routing/collision-probe.d.ts +104 -0
  158. package/dist/routing/collision-probe.d.ts.map +1 -0
  159. package/dist/routing/collision-spaces.d.ts +26 -0
  160. package/dist/routing/collision-spaces.d.ts.map +1 -0
  161. package/dist/routing/index.js +3 -3
  162. package/dist/routing/interception.d.ts +235 -6
  163. package/dist/routing/interception.d.ts.map +1 -1
  164. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  165. package/dist/routing/scanner.d.ts.map +1 -1
  166. package/dist/routing/segment-classify.d.ts +33 -0
  167. package/dist/routing/segment-classify.d.ts.map +1 -1
  168. package/dist/routing/segment-keys.d.ts +95 -0
  169. package/dist/routing/segment-keys.d.ts.map +1 -0
  170. package/dist/routing/slot-placement.d.ts +48 -0
  171. package/dist/routing/slot-placement.d.ts.map +1 -0
  172. package/dist/routing/types.d.ts +1 -3
  173. package/dist/routing/types.d.ts.map +1 -1
  174. package/dist/routing/walkers.d.ts +6 -0
  175. package/dist/routing/walkers.d.ts.map +1 -1
  176. package/dist/search-params/define.d.ts +0 -6
  177. package/dist/search-params/define.d.ts.map +1 -1
  178. package/dist/search-params/index.js +176 -2
  179. package/dist/search-params/index.js.map +1 -1
  180. package/dist/segment-params/define.d.ts +0 -6
  181. package/dist/segment-params/define.d.ts.map +1 -1
  182. package/dist/segment-params/index.js +69 -1
  183. package/dist/segment-params/index.js.map +1 -0
  184. package/dist/server/actions.d.ts +6 -3
  185. package/dist/server/actions.d.ts.map +1 -1
  186. package/dist/server/als-registry.d.ts +2 -1
  187. package/dist/server/als-registry.d.ts.map +1 -1
  188. package/dist/server/build-manifest.d.ts +13 -1
  189. package/dist/server/build-manifest.d.ts.map +1 -1
  190. package/dist/server/chain-url-parts.d.ts +82 -0
  191. package/dist/server/chain-url-parts.d.ts.map +1 -0
  192. package/dist/server/children-interception.d.ts +49 -0
  193. package/dist/server/children-interception.d.ts.map +1 -0
  194. package/dist/server/default-status-page.d.ts +45 -0
  195. package/dist/server/default-status-page.d.ts.map +1 -0
  196. package/dist/server/deny-boundary.d.ts +4 -7
  197. package/dist/server/deny-boundary.d.ts.map +1 -1
  198. package/dist/server/deny-renderer.d.ts +14 -3
  199. package/dist/server/deny-renderer.d.ts.map +1 -1
  200. package/dist/server/head-response.d.ts +35 -0
  201. package/dist/server/head-response.d.ts.map +1 -0
  202. package/dist/server/index.js +2 -2
  203. package/dist/server/internal.js +528 -373
  204. package/dist/server/internal.js.map +1 -1
  205. package/dist/server/param-coercion.d.ts +10 -1
  206. package/dist/server/param-coercion.d.ts.map +1 -1
  207. package/dist/server/pipeline-helpers.d.ts +0 -18
  208. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  209. package/dist/server/pipeline-interception.d.ts +59 -9
  210. package/dist/server/pipeline-interception.d.ts.map +1 -1
  211. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  212. package/dist/server/pipeline-phases.d.ts.map +1 -1
  213. package/dist/server/pipeline.d.ts +11 -0
  214. package/dist/server/pipeline.d.ts.map +1 -1
  215. package/dist/server/prebuilt-builder.d.ts.map +1 -1
  216. package/dist/server/primitives.d.ts +0 -12
  217. package/dist/server/primitives.d.ts.map +1 -1
  218. package/dist/server/publish-params.d.ts +33 -0
  219. package/dist/server/publish-params.d.ts.map +1 -0
  220. package/dist/server/request-context.d.ts +48 -0
  221. package/dist/server/request-context.d.ts.map +1 -1
  222. package/dist/server/route-element-builder.d.ts +32 -2
  223. package/dist/server/route-element-builder.d.ts.map +1 -1
  224. package/dist/server/route-handler.d.ts.map +1 -1
  225. package/dist/server/route-matcher.d.ts +13 -0
  226. package/dist/server/route-matcher.d.ts.map +1 -1
  227. package/dist/server/rsc-cache-key-guard.d.ts +78 -0
  228. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
  229. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  230. package/dist/server/rsc-entry/error-renderer.d.ts +14 -2
  231. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  232. package/dist/server/rsc-entry/helpers.d.ts +2 -10
  233. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  234. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  235. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  236. package/dist/server/rsc-entry/rsc-payload.d.ts +3 -2
  237. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  238. package/dist/server/rsc-entry/rsc-stream.d.ts +3 -2
  239. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  240. package/dist/server/rsc-entry/ssr-renderer.d.ts +2 -0
  241. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  242. package/dist/server/sitemap-generator.d.ts.map +1 -1
  243. package/dist/server/skippable-prefix.d.ts +23 -0
  244. package/dist/server/skippable-prefix.d.ts.map +1 -0
  245. package/dist/server/slot-interception.d.ts +80 -0
  246. package/dist/server/slot-interception.d.ts.map +1 -0
  247. package/dist/server/slot-resolver.d.ts +70 -8
  248. package/dist/server/slot-resolver.d.ts.map +1 -1
  249. package/dist/server/ssr-bridge-types.d.ts +6 -0
  250. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  251. package/dist/server/ssr-entry.d.ts.map +1 -1
  252. package/dist/server/ssr-wrappers.d.ts +24 -13
  253. package/dist/server/ssr-wrappers.d.ts.map +1 -1
  254. package/dist/server/state-tree-diff.d.ts +6 -27
  255. package/dist/server/state-tree-diff.d.ts.map +1 -1
  256. package/dist/server/static-generator.d.ts +6 -0
  257. package/dist/server/static-generator.d.ts.map +1 -1
  258. package/dist/server/static-not-found.d.ts +52 -0
  259. package/dist/server/static-not-found.d.ts.map +1 -0
  260. package/dist/server/status-code-resolver.d.ts +3 -10
  261. package/dist/server/status-code-resolver.d.ts.map +1 -1
  262. package/dist/shared/als-slots.d.ts +41 -0
  263. package/dist/shared/als-slots.d.ts.map +1 -0
  264. package/dist/shared/base-path.d.ts +56 -0
  265. package/dist/shared/base-path.d.ts.map +1 -0
  266. package/dist/shared/param-value.d.ts +80 -0
  267. package/dist/shared/param-value.d.ts.map +1 -0
  268. package/dist/shared/payload-root.d.ts +108 -0
  269. package/dist/shared/payload-root.d.ts.map +1 -0
  270. package/dist/shared/rsc-cache-key.d.ts +163 -0
  271. package/dist/shared/rsc-cache-key.d.ts.map +1 -0
  272. package/dist/shared/rsc-error-envelope.d.ts +25 -0
  273. package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
  274. package/dist/shared/rsc-manifest.d.ts +28 -0
  275. package/dist/shared/rsc-manifest.d.ts.map +1 -0
  276. package/dist/shared/rsc-payload-path.d.ts +42 -0
  277. package/dist/shared/rsc-payload-path.d.ts.map +1 -0
  278. package/dist/shared/segment-info.d.ts +59 -0
  279. package/dist/shared/segment-info.d.ts.map +1 -0
  280. package/dist/shared/slot-params.d.ts +51 -0
  281. package/dist/shared/slot-params.d.ts.map +1 -0
  282. package/dist/shared/static-platform-files.d.ts +71 -0
  283. package/dist/shared/static-platform-files.d.ts.map +1 -0
  284. package/dist/shims/navigation-client.d.ts +8 -19
  285. package/dist/shims/navigation-client.d.ts.map +1 -1
  286. package/dist/shims/navigation.d.ts +0 -1
  287. package/dist/shims/navigation.d.ts.map +1 -1
  288. package/docs/api/31-api-client.mdx +4 -2
  289. package/docs/learn/02-pages-and-layouts.mdx +1 -1
  290. package/docs/learn/11-error-handling.mdx +4 -3
  291. package/docs/learn/12-client-navigation.mdx +2 -2
  292. package/docs/learn/14-deploying.mdx +23 -0
  293. package/docs/more/01-advanced-routing.mdx +172 -2
  294. package/docs/more/04-metadata-and-fonts.mdx +1 -1
  295. package/package.json +2 -2
  296. package/src/adapters/build-output-helper.ts +116 -3
  297. package/src/adapters/cloudflare.ts +86 -11
  298. package/src/adapters/fs-identity.ts +156 -0
  299. package/src/adapters/nitro.ts +6 -6
  300. package/src/adapters/shared.ts +196 -21
  301. package/src/adapters/types.ts +13 -1
  302. package/src/cache/fast-hash.ts +5 -0
  303. package/src/client/browser-entry/action-dispatch.ts +9 -4
  304. package/src/client/browser-entry/hydrate.ts +40 -29
  305. package/src/client/browser-entry/index.ts +15 -1
  306. package/src/client/browser-entry/post-hydration.ts +10 -9
  307. package/src/client/browser-entry/router-init.ts +104 -48
  308. package/src/client/browser-entry/rsc-stream.ts +11 -2
  309. package/src/client/error-boundary.tsx +20 -15
  310. package/src/client/history.ts +13 -6
  311. package/src/client/internal.ts +4 -3
  312. package/src/client/link.tsx +55 -8
  313. package/src/client/navigation-api.ts +3 -3
  314. package/src/client/navigation-commit.ts +174 -0
  315. package/src/client/navigation-context.ts +40 -67
  316. package/src/client/navigation-root.tsx +326 -101
  317. package/src/client/params-context.ts +192 -0
  318. package/src/client/router-effects.ts +231 -0
  319. package/src/client/router-lifecycle.ts +288 -0
  320. package/src/client/router-pipeline.ts +389 -0
  321. package/src/client/router-ref.ts +1 -1
  322. package/src/client/router-skew.ts +30 -0
  323. package/src/client/router-types.ts +230 -0
  324. package/src/client/router.ts +242 -690
  325. package/src/client/rsc-fetch.ts +180 -122
  326. package/src/client/segment-cache.ts +121 -44
  327. package/src/client/ssr-data.ts +6 -0
  328. package/src/client/stale-client.ts +191 -0
  329. package/src/client/state.ts +14 -1
  330. package/src/client/top-loader.tsx +2 -1
  331. package/src/client/unload-guard.ts +34 -2
  332. package/src/client/use-cookie.ts +9 -2
  333. package/src/client/use-router.ts +1 -1
  334. package/src/client/use-segment-params.ts +41 -18
  335. package/src/index.ts +5 -1
  336. package/src/plugin-context.ts +34 -1
  337. package/src/plugins/adapter-build.ts +4 -0
  338. package/src/plugins/build-manifest.ts +34 -13
  339. package/src/plugins/entries.ts +4 -0
  340. package/src/plugins/fonts.ts +11 -8
  341. package/src/plugins/request-dep/analysis.ts +447 -0
  342. package/src/plugins/request-dep/index.ts +376 -0
  343. package/src/plugins/request-dep/origin.ts +310 -0
  344. package/src/plugins/routing.ts +60 -3
  345. package/src/plugins/static-build.ts +333 -63
  346. package/src/routing/codegen-types.ts +18 -0
  347. package/src/routing/codegen.ts +50 -10
  348. package/src/routing/collision-probe.ts +438 -0
  349. package/src/routing/collision-spaces.ts +281 -0
  350. package/src/routing/interception.ts +896 -50
  351. package/src/routing/manifest-codegen.ts +6 -9
  352. package/src/routing/scanner.ts +114 -113
  353. package/src/routing/segment-classify.ts +57 -0
  354. package/src/routing/segment-keys.ts +146 -0
  355. package/src/routing/slot-placement.ts +97 -0
  356. package/src/routing/types.ts +1 -3
  357. package/src/routing/walkers.ts +8 -0
  358. package/src/search-params/define.ts +6 -29
  359. package/src/segment-params/define.ts +4 -27
  360. package/src/server/action-handler.ts +3 -1
  361. package/src/server/actions.ts +6 -3
  362. package/src/server/als-registry.ts +2 -1
  363. package/src/server/build-manifest.ts +14 -1
  364. package/src/server/chain-url-parts.ts +163 -0
  365. package/src/server/children-interception.ts +115 -0
  366. package/src/server/default-status-page.ts +127 -0
  367. package/src/server/deny-boundary.ts +5 -31
  368. package/src/server/deny-renderer.ts +83 -22
  369. package/src/server/head-response.ts +54 -0
  370. package/src/server/param-coercion.ts +48 -18
  371. package/src/server/pipeline-helpers.ts +0 -45
  372. package/src/server/pipeline-interception.ts +81 -32
  373. package/src/server/pipeline-outcome.ts +3 -5
  374. package/src/server/pipeline-phases.ts +68 -16
  375. package/src/server/pipeline.ts +57 -10
  376. package/src/server/prebuilt-builder.ts +1 -7
  377. package/src/server/prebuilt-runtime.ts +4 -4
  378. package/src/server/primitives.ts +0 -18
  379. package/src/server/publish-params.ts +42 -0
  380. package/src/server/request-context.ts +97 -18
  381. package/src/server/route-element-builder.ts +89 -98
  382. package/src/server/route-handler.ts +8 -8
  383. package/src/server/route-matcher.ts +21 -9
  384. package/src/server/rsc-cache-key-guard.ts +170 -0
  385. package/src/server/rsc-entry/api-handler.ts +6 -8
  386. package/src/server/rsc-entry/error-renderer.ts +64 -25
  387. package/src/server/rsc-entry/helpers.ts +31 -16
  388. package/src/server/rsc-entry/index.ts +36 -28
  389. package/src/server/rsc-entry/render-route.ts +12 -7
  390. package/src/server/rsc-entry/revalidate-renderer.ts +1 -1
  391. package/src/server/rsc-entry/rsc-payload.ts +14 -25
  392. package/src/server/rsc-entry/rsc-stream.ts +4 -3
  393. package/src/server/rsc-entry/ssr-renderer.ts +13 -9
  394. package/src/server/sitemap-generator.ts +1 -7
  395. package/src/server/skippable-prefix.ts +92 -0
  396. package/src/server/slot-interception.ts +168 -0
  397. package/src/server/slot-resolver.ts +253 -156
  398. package/src/server/ssr-bridge-types.ts +6 -0
  399. package/src/server/ssr-entry.ts +12 -4
  400. package/src/server/ssr-wrappers.tsx +33 -32
  401. package/src/server/state-tree-diff.ts +14 -55
  402. package/src/server/static-generator.ts +67 -45
  403. package/src/server/static-not-found.ts +134 -0
  404. package/src/server/status-code-resolver.ts +4 -47
  405. package/src/shared/als-slots.ts +72 -0
  406. package/src/shared/base-path.ts +80 -0
  407. package/src/shared/param-value.ts +244 -0
  408. package/src/shared/payload-root.ts +151 -0
  409. package/src/shared/rsc-cache-key.ts +269 -0
  410. package/src/shared/rsc-error-envelope.ts +31 -0
  411. package/src/shared/rsc-manifest.ts +29 -0
  412. package/src/shared/rsc-payload-path.ts +45 -0
  413. package/src/shared/segment-info.ts +60 -0
  414. package/src/shared/slot-params.ts +111 -0
  415. package/src/shared/static-platform-files.ts +75 -0
  416. package/src/shims/navigation-client.ts +8 -26
  417. package/src/shims/navigation.ts +0 -4
  418. package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
  419. package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
  420. package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
  421. package/dist/_chunks/cli-schema-sync-D3naS3eH.js +0 -688
  422. package/dist/_chunks/cli-schema-sync-D3naS3eH.js.map +0 -1
  423. package/dist/_chunks/define-COtkxMRT.js +0 -191
  424. package/dist/_chunks/define-COtkxMRT.js.map +0 -1
  425. package/dist/_chunks/define-c4au4I9R.js +0 -82
  426. package/dist/_chunks/define-c4au4I9R.js.map +0 -1
  427. package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
  428. package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
  429. package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
  430. package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
  431. package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
  432. package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
  433. package/dist/_chunks/segment-classify-Byy425ng.js.map +0 -1
  434. package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
  435. package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
  436. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
  437. package/dist/_chunks/walkers-BtTlKviE.js.map +0 -1
  438. package/dist/client/error-boundary.js.map +0 -1
  439. package/dist/plugins/request-dep.d.ts +0 -32
  440. package/dist/plugins/request-dep.d.ts.map +0 -1
  441. package/src/plugins/request-dep.ts +0 -499
@@ -1,11 +1,15 @@
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";
1
+ 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";
2
+ import { n as classifyUrlSegment, r as effectiveUrlSegment } from "../_chunks/segment-classify-C539Pa2O.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-N7e5auP0.js";
4
+ 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";
5
+ import { i as collectRouteModulepreloads, n as collectRouteCss, r as collectRouteFonts } from "../_chunks/build-manifest-DWppEdLB.js";
6
+ import { i as coerce, t as executeAction } from "../_chunks/actions-C-Rw9vPc.js";
7
+ 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-Cd0VZ_Pd.js";
8
+ import "../_chunks/error-boundary-DpYRI_I1.js";
7
9
  import { n as toBracketKey } from "../_chunks/resolve-schema-Dz3fcFUo.js";
10
+ import { t as normalizeParamValue } from "../_chunks/param-value-C8TNYchQ.js";
8
11
  import "../_chunks/segment-context-ZDnXDkbz.js";
12
+ import { n as rscErrorEnvelope } from "../_chunks/rsc-error-envelope-tT5PJs4q.js";
9
13
  import { createHash } from "node:crypto";
10
14
  import { readFile } from "node:fs/promises";
11
15
  import React, { createElement, useContext } from "react";
@@ -209,38 +213,6 @@ function hasOnRequestError() {
209
213
  //#endregion
210
214
  //#region src/server/pipeline-helpers.ts
211
215
  /**
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
216
  * Build a proxy resolver closure from the declared source. Called exactly
245
217
  * once at `createPipeline` setup time, so the hot path sees only the branch
246
218
  * that corresponds to this pipeline's configured variant.
@@ -1163,165 +1135,6 @@ async function loadModule(loader) {
1163
1135
  throw new ModuleLoadError(loader.filePath, error);
1164
1136
  }
1165
1137
  }
1166
- /** Reverse index: status code → legacy file name. Built once at module load. */
1167
- var STATUS_TO_LEGACY_FILE = Object.fromEntries(Object.entries({
1168
- "not-found": 404,
1169
- "forbidden": 403,
1170
- "unauthorized": 401
1171
- }).map(([name, status]) => [status, name]));
1172
- /**
1173
- * Look up `{statusStr}` then `{categoryKey}` (e.g. "4xx" / "5xx") in a
1174
- * status-file group on a single segment. Shared by all three fallback
1175
- * chains — the only structural difference between component 4xx,
1176
- * component 5xx, and JSON resolution is *which* group is searched and
1177
- * how the per-segment loop is layered around it.
1178
- */
1179
- function lookupInGroup(group, statusStr, categoryKey, segmentIndex, status) {
1180
- if (!group) return null;
1181
- const exact = group[statusStr];
1182
- if (exact) return {
1183
- file: exact,
1184
- status,
1185
- kind: "exact",
1186
- segmentIndex
1187
- };
1188
- const category = group[categoryKey];
1189
- if (category) return {
1190
- file: category,
1191
- status,
1192
- kind: "category",
1193
- segmentIndex
1194
- };
1195
- return null;
1196
- }
1197
- /**
1198
- * Look up the legacy convention file (`not-found.tsx` / `forbidden.tsx` /
1199
- * `unauthorized.tsx`) for `status` on a single segment. Returns null if
1200
- * `status` has no legacy mapping or the file isn't present.
1201
- */
1202
- function lookupLegacy(group, status, segmentIndex) {
1203
- if (!group) return null;
1204
- const name = STATUS_TO_LEGACY_FILE[status];
1205
- if (!name) return null;
1206
- const file = group[name];
1207
- return file ? {
1208
- file,
1209
- status,
1210
- kind: "legacy",
1211
- segmentIndex
1212
- } : null;
1213
- }
1214
- /**
1215
- * Resolve the status-code file to render for a given HTTP status code.
1216
- *
1217
- * Walks the segment chain from leaf to root following the fallback chain
1218
- * defined in design/10-error-handling.md. Returns null if no file is found
1219
- * (caller should render the framework default).
1220
- *
1221
- * @param status - The HTTP status code (4xx or 5xx).
1222
- * @param segments - The matched segment chain from root (index 0) to leaf (last).
1223
- * @param format - The response format family ('component' or 'json'). Defaults to 'component'.
1224
- */
1225
- function resolveStatusFile(status, segments, format = "component") {
1226
- if (status < 400 || status > 599) return null;
1227
- if (format === "json") return resolveJson(status, segments);
1228
- if (status <= 499) return resolve4xx(status, segments);
1229
- return resolve5xx(status, segments);
1230
- }
1231
- /**
1232
- * 4xx component fallback chain — three separate full passes leaf→root.
1233
- *
1234
- * The passes must be separate (not interleaved per-segment) so that a
1235
- * root-level `404.tsx` beats a leaf-level `error.tsx`. The 5xx chain
1236
- * inverts this and is per-segment: a leaf's `error.tsx` beats a root's
1237
- * `5xx.tsx`. This asymmetry is the only reason these two functions exist
1238
- * separately.
1239
- *
1240
- * Pass 1 — {status}.tsx → 4xx.tsx (statusFiles)
1241
- * Pass 2 — not-found / forbidden / unauthorized (legacyStatusFiles)
1242
- * Pass 3 — error.tsx (error)
1243
- */
1244
- function resolve4xx(status, segments) {
1245
- const statusStr = String(status);
1246
- for (let i = segments.length - 1; i >= 0; i--) {
1247
- const r = lookupInGroup(segments[i].statusFiles, statusStr, "4xx", i, status);
1248
- if (r) return r;
1249
- }
1250
- for (let i = segments.length - 1; i >= 0; i--) {
1251
- const r = lookupLegacy(segments[i].legacyStatusFiles, status, i);
1252
- if (r) return r;
1253
- }
1254
- for (let i = segments.length - 1; i >= 0; i--) {
1255
- const errorFile = segments[i].error;
1256
- if (errorFile) return {
1257
- file: errorFile,
1258
- status,
1259
- kind: "error",
1260
- segmentIndex: i
1261
- };
1262
- }
1263
- return null;
1264
- }
1265
- /**
1266
- * 5xx component fallback chain — single pass, per-segment leaf→root.
1267
- *
1268
- * At each segment: {status}.tsx → 5xx.tsx → error.tsx. A leaf's
1269
- * `error.tsx` therefore beats a root's `5xx.tsx`, which is the
1270
- * intentional inverse of the 4xx chain.
1271
- */
1272
- function resolve5xx(status, segments) {
1273
- const statusStr = String(status);
1274
- for (let i = segments.length - 1; i >= 0; i--) {
1275
- const segment = segments[i];
1276
- const r = lookupInGroup(segment.statusFiles, statusStr, "5xx", i, status);
1277
- if (r) return r;
1278
- if (segment.error) return {
1279
- file: segment.error,
1280
- status,
1281
- kind: "error",
1282
- segmentIndex: i
1283
- };
1284
- }
1285
- return null;
1286
- }
1287
- /**
1288
- * JSON fallback chain (for both 4xx and 5xx) — single pass leaf→root.
1289
- *
1290
- * At each segment: {status}.json → {category}.json. No legacy compat,
1291
- * no error.tsx — the JSON chain terminates at the category catch-all
1292
- * and the caller falls back to a bare-JSON framework default.
1293
- */
1294
- function resolveJson(status, segments) {
1295
- const statusStr = String(status);
1296
- const categoryKey = status >= 500 ? "5xx" : "4xx";
1297
- for (let i = segments.length - 1; i >= 0; i--) {
1298
- const r = lookupInGroup(segments[i].jsonStatusFiles, statusStr, categoryKey, i, status);
1299
- if (r) return r;
1300
- }
1301
- return null;
1302
- }
1303
- /**
1304
- * Resolve the denial file for a parallel route slot.
1305
- *
1306
- * Slot denial is graceful degradation — no HTTP status on the wire.
1307
- * Fallback chain: denied.tsx → default.tsx → null.
1308
- *
1309
- * @param slotNode - The segment node for the slot (segmentType === 'slot').
1310
- */
1311
- function resolveSlotDenied(slotNode) {
1312
- const slotName = slotNode.segmentName.replace(/^@/, "");
1313
- if (slotNode.denied) return {
1314
- file: slotNode.denied,
1315
- slotName,
1316
- kind: "denied"
1317
- };
1318
- if (slotNode.default) return {
1319
- file: slotNode.default,
1320
- slotName,
1321
- kind: "default"
1322
- };
1323
- return null;
1324
- }
1325
1138
  //#endregion
1326
1139
  //#region src/server/deny-boundary.ts
1327
1140
  /**
@@ -1371,7 +1184,7 @@ function renderMatchingDenyPage(chain, status, data) {
1371
1184
  * Build the element for a matched deny chain entry with the props contract
1372
1185
  * the file's convention documents:
1373
1186
  *
1374
- * - Status-code and legacy files: { status, dangerouslyPassData }
1187
+ * - Status-code files: { status, dangerouslyPassData }
1375
1188
  * - error.tsx (TSX): { error, digest, reset } via ErrorReconstituter, plus
1376
1189
  * { status, dangerouslyPassData } for dual-shape implementations. Without
1377
1190
  * the Error prop, any error.tsx written per the docs (reading
@@ -1925,20 +1738,23 @@ async function coerceSegmentParams(match) {
1925
1738
  for (const key of Object.keys(match.segmentParams)) if (key !== "__proto__") mergeTarget[key] = match.segmentParams[key];
1926
1739
  match.segmentParams = mergeTarget;
1927
1740
  if (globalCodecs) {
1928
- for (const segment of match.segments) {
1741
+ for (const node of match.segments) {
1742
+ const segment = effectiveUrlSegment(node);
1929
1743
  if (!segment.paramName) continue;
1930
1744
  const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
1931
1745
  const codec = globalCodecs[bracketKey];
1932
1746
  if (!codec) continue;
1933
1747
  const key = segment.paramName;
1934
1748
  if (!(key in mergeTarget)) continue;
1749
+ let parsed;
1935
1750
  try {
1936
- mergeTarget[key] = sanitizeParamValue(codec.parse(mergeTarget[key]));
1751
+ parsed = codec.parse(mergeTarget[key]);
1937
1752
  } catch (err) {
1938
1753
  const message = err instanceof Error ? err.message : String(err);
1939
1754
  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}'.`);
1940
1755
  throw new ParamCoercionError(message);
1941
1756
  }
1757
+ mergeTarget[key] = normalizeParamValue(parsed, key);
1942
1758
  }
1943
1759
  return;
1944
1760
  }
@@ -1954,9 +1770,9 @@ async function coerceSegmentParams(match) {
1954
1770
  }
1955
1771
  const segmentParamsDef = mod.segmentParams;
1956
1772
  if (!segmentParamsDef || typeof segmentParamsDef.parse !== "function") continue;
1773
+ let coerced;
1957
1774
  try {
1958
- const coerced = segmentParamsDef.parse(match.segmentParams);
1959
- for (const key of Object.keys(coerced)) if (key !== "__proto__") mergeTarget[key] = sanitizeParamValue(coerced[key]);
1775
+ coerced = segmentParamsDef.parse(match.segmentParams);
1960
1776
  } catch (err) {
1961
1777
  const message = err instanceof Error ? err.message : String(err);
1962
1778
  if (isDebug()) {
@@ -1965,7 +1781,127 @@ async function coerceSegmentParams(match) {
1965
1781
  }
1966
1782
  throw new ParamCoercionError(message);
1967
1783
  }
1784
+ for (const key of Object.keys(coerced)) {
1785
+ if (key === "__proto__") continue;
1786
+ mergeTarget[key] = normalizeParamValue(coerced[key], key);
1787
+ }
1788
+ }
1789
+ }
1790
+ //#endregion
1791
+ //#region src/server/pipeline-interception.ts
1792
+ /**
1793
+ * Interception route matching for the request pipeline.
1794
+ *
1795
+ * Matches target URLs against interception rewrites to support the
1796
+ * modal route pattern (soft navigation intercepts).
1797
+ *
1798
+ * Extracted from pipeline.ts to keep files under 500 lines.
1799
+ *
1800
+ * See design/07-routing.md §"Intercepting Routes"
1801
+ */
1802
+ /**
1803
+ * Check if an intercepting route applies for this soft navigation.
1804
+ *
1805
+ * Two conditions must hold:
1806
+ *
1807
+ * 1. The **target** pathname matches the rewrite's intercepted pattern.
1808
+ * 2. The **source** route — the page the user is navigating FROM, per the
1809
+ * validated X-Timber-URL header — renders the segment that owns the slot
1810
+ * holding the intercepting page (`interceptingScope`). Only then does that
1811
+ * slot exist in the rendered tree, so only then can the intercepting page
1812
+ * render.
1813
+ *
1814
+ * Condition 2 replaced a URL-prefix check (TIM-1279). A prefix cannot
1815
+ * distinguish a route-group-scoped interception from a genuinely root-level
1816
+ * one — route groups are invisible in `urlPath`, so both collapse to "/" and
1817
+ * the guard degenerates to always-true. Tree paths carry group identity.
1818
+ * The scope check also subsumes the old segment-boundary rule: a source route
1819
+ * under /feed-private has no "/feed" in its chain.
1820
+ *
1821
+ * A third condition lives with the caller, which owns route matching, and it
1822
+ * applies to the `'slot'` shape only: the route produced by re-matching
1823
+ * `interceptingPrefix` must pass through the scope too. Conditions 1–2 are
1824
+ * about the route the user is leaving; the slot has to exist in the tree that
1825
+ * actually renders. The `'children'` shape resolves its owner from the scope
1826
+ * directly rather than from a pathname, so it satisfies that by construction.
1827
+ *
1828
+ * `resolveSourceScopes` is a thunk so its inputs are only resolved when some
1829
+ * rewrite's pattern actually matches the target — the common case is no match
1830
+ * at all. It returns the two client-supplied answers to "what is on screen?"
1831
+ * as **separate sets, most authoritative first**: the tree paths the client
1832
+ * says it has mounted, then those of the route the departing URL matches. It
1833
+ * returns null only when neither yields anything, which is not a soft
1834
+ * navigation this can reason about.
1835
+ *
1836
+ * They are two sets rather than one union because ranking needs a chain
1837
+ * (TIM-1318). Each channel describes a single rendered chain, so within one the
1838
+ * scopes are strictly nested and the deepest is the innermost owner. Unioned
1839
+ * they are not: once a modal is open the address bar holds the *intercepted*
1840
+ * URL, so the second channel describes the canonical branch — possibly an
1841
+ * exclusive route group the client never mounted — and a deeper scope from
1842
+ * there would outrank the owner actually on screen. Trying the mounted channel
1843
+ * to exhaustion first keeps the fallback the union was there to provide while
1844
+ * ranking only within one chain.
1845
+ *
1846
+ * Returns the winning build-time rewrite, or null. Only build-time values ever
1847
+ * reach the renderer: the client-supplied header selects a rewrite but never
1848
+ * becomes a rendered route or pathname.
1849
+ *
1850
+ * The **first** matching rewrite wins, and the caller does not fall back to the
1851
+ * next one when the winner fails to resolve. That is deliberate and pre-dates
1852
+ * the two shapes: a rewrite that matched the target and the scope described the
1853
+ * author's intent, and quietly rendering some other author's interception
1854
+ * instead would be harder to explain than rendering the canonical route.
1855
+ *
1856
+ * "First" is meaningful because the list arrives **deepest scope first** —
1857
+ * `collectInterceptionRewrites` sorts it at build time, so the deepest owner a
1858
+ * single chain passes through wins and nothing here has to score (TIM-1318).
1859
+ * Tree-walk order used to decide it, which meant renaming a route group flipped
1860
+ * which modal opened. The precondition is a property of the manifest this reads
1861
+ * rather than something it can re-derive, so it is asserted at its source:
1862
+ * `tests/interception.test.ts` §"rewrite order is decided by scope depth".
1863
+ */
1864
+ function findInterceptionMatch(targetPathname, resolveSourceScopes, rewrites) {
1865
+ const candidates = rewrites.filter((rewrite) => pathnameMatchesPattern(targetPathname, rewrite.interceptedPattern));
1866
+ if (candidates.length === 0) return null;
1867
+ const scopes = resolveSourceScopes();
1868
+ if (!scopes) return null;
1869
+ for (const scope of scopes) for (const rewrite of candidates) if (scope.has(rewrite.interceptingScope)) return rewrite;
1870
+ return null;
1871
+ }
1872
+ /**
1873
+ * Check if a pathname matches a URL pattern with dynamic segments.
1874
+ *
1875
+ * Supports [param] (single segment) and [...param] (one or more segments).
1876
+ * Static segments must match exactly.
1877
+ */
1878
+ function pathnameMatchesPattern(pathname, pattern) {
1879
+ const pathParts = pathname === "/" ? [] : pathname.slice(1).split("/");
1880
+ const patternParts = pattern === "/" ? [] : pattern.slice(1).split("/");
1881
+ let pi = 0;
1882
+ for (let i = 0; i < patternParts.length; i++) {
1883
+ const seg = classifyUrlSegment(patternParts[i]);
1884
+ switch (seg.kind) {
1885
+ case "catch-all": return pi < pathParts.length;
1886
+ case "optional-catch-all": return true;
1887
+ case "dynamic": {
1888
+ if (pi >= pathParts.length) return false;
1889
+ const prefix = seg.prefix ?? "";
1890
+ const suffix = seg.suffix ?? "";
1891
+ const pathPart = pathParts[pi];
1892
+ if (prefix || suffix) {
1893
+ if (pathPart.length <= prefix.length + suffix.length || prefix && !pathPart.startsWith(prefix) || suffix && !pathPart.endsWith(suffix)) return false;
1894
+ }
1895
+ pi++;
1896
+ continue;
1897
+ }
1898
+ case "static":
1899
+ if (pi >= pathParts.length || pathParts[pi] !== seg.value) return false;
1900
+ pi++;
1901
+ continue;
1902
+ }
1968
1903
  }
1904
+ return pi === pathParts.length;
1969
1905
  }
1970
1906
  //#endregion
1971
1907
  //#region src/client/segment-update-context.ts
@@ -2024,6 +1960,51 @@ getOrCreateContext();
2024
1960
  * See design/13-security.md §"State tree manipulation".
2025
1961
  */
2026
1962
  //#endregion
1963
+ //#region src/server/state-tree-diff.ts
1964
+ /**
1965
+ * State Tree Diffing — Server-side parsing and diffing of X-Timber-State-Tree.
1966
+ *
1967
+ * The client sends X-Timber-State-Tree on navigation requests, listing
1968
+ * the sync segments it has cached. The server diffs this against the
1969
+ * target route's segments to skip re-rendering unchanged sync layouts.
1970
+ *
1971
+ * This is a performance optimization only — NOT a security boundary.
1972
+ * All access.ts files run regardless of the state tree content.
1973
+ * A fabricated state tree can only cause extra rendering work or stale
1974
+ * layouts — never auth bypass.
1975
+ *
1976
+ * See design/19-client-navigation.md §"X-Timber-State-Tree Header"
1977
+ * See design/13-security.md §"State tree manipulation"
1978
+ */
1979
+ /**
1980
+ * Parse the X-Timber-State-Tree header from a request.
1981
+ *
1982
+ * Returns a Set of segment paths the client has cached (with an
1983
+ * additional `slots` property for cached slot paths), or null if
1984
+ * the header is missing, malformed, or empty. Parsing happens before
1985
+ * renderToReadableStream — not inside the React render pass.
1986
+ *
1987
+ * @returns ClientStateTree with segments and slots, or null if no valid state tree
1988
+ */
1989
+ function parseClientStateTree(req) {
1990
+ const header = req.headers.get("X-Timber-State-Tree");
1991
+ if (!header) return null;
1992
+ try {
1993
+ const parsed = JSON.parse(header);
1994
+ const hasSegments = Array.isArray(parsed.segments) && parsed.segments.length > 0;
1995
+ const hasSlots = Array.isArray(parsed.slots) && parsed.slots.length > 0;
1996
+ const hasTreePaths = Array.isArray(parsed.treePaths) && parsed.treePaths.length > 0;
1997
+ if (!hasSegments && !hasSlots && !hasTreePaths) return null;
1998
+ const result = new Set(hasSegments ? parsed.segments : []);
1999
+ result.slots = hasSlots ? new Set(parsed.slots) : null;
2000
+ result.treePaths = hasTreePaths ? new Set(parsed.treePaths) : null;
2001
+ return result;
2002
+ } catch (err) {
2003
+ swallow(err, "malformed X-Timber-State-Tree header");
2004
+ return null;
2005
+ }
2006
+ }
2007
+ //#endregion
2027
2008
  //#region src/server/route-element-builder.ts
2028
2009
  /**
2029
2010
  * Thrown when a defineSegmentParams codec's parse() fails.
@@ -2101,78 +2082,6 @@ function escapeXml(str) {
2101
2082
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
2102
2083
  }
2103
2084
  //#endregion
2104
- //#region src/server/pipeline-interception.ts
2105
- /**
2106
- * Interception route matching for the request pipeline.
2107
- *
2108
- * Matches target URLs against interception rewrites to support the
2109
- * modal route pattern (soft navigation intercepts).
2110
- *
2111
- * Extracted from pipeline.ts to keep files under 500 lines.
2112
- *
2113
- * See design/07-routing.md §"Intercepting Routes"
2114
- */
2115
- /**
2116
- * Check if a pathname starts with a prefix on a segment boundary.
2117
- *
2118
- * Prevents /feed from matching /feed-private — the prefix must be
2119
- * followed by '/' or be an exact match.
2120
- */
2121
- function hasSegmentPrefix(pathname, prefix) {
2122
- if (prefix === "/") return true;
2123
- if (!pathname.startsWith(prefix)) return false;
2124
- return pathname.length === prefix.length || pathname[prefix.length] === "/";
2125
- }
2126
- /**
2127
- * Check if an intercepting route applies for this soft navigation.
2128
- *
2129
- * Matches the target pathname against interception rewrites, constrained
2130
- * by the source URL (X-Timber-URL header — where the user navigates FROM).
2131
- *
2132
- * Returns the source pathname to re-match if interception applies, or null.
2133
- */
2134
- function findInterceptionMatch(targetPathname, sourceUrl, rewrites) {
2135
- for (const rewrite of rewrites) {
2136
- if (!hasSegmentPrefix(sourceUrl, rewrite.interceptingPrefix)) continue;
2137
- if (pathnameMatchesPattern(targetPathname, rewrite.interceptedPattern)) return { sourcePathname: rewrite.interceptingPrefix };
2138
- }
2139
- return null;
2140
- }
2141
- /**
2142
- * Check if a pathname matches a URL pattern with dynamic segments.
2143
- *
2144
- * Supports [param] (single segment) and [...param] (one or more segments).
2145
- * Static segments must match exactly.
2146
- */
2147
- function pathnameMatchesPattern(pathname, pattern) {
2148
- const pathParts = pathname === "/" ? [] : pathname.slice(1).split("/");
2149
- const patternParts = pattern === "/" ? [] : pattern.slice(1).split("/");
2150
- let pi = 0;
2151
- for (let i = 0; i < patternParts.length; i++) {
2152
- const seg = classifyUrlSegment(patternParts[i]);
2153
- switch (seg.kind) {
2154
- case "catch-all": return pi < pathParts.length;
2155
- case "optional-catch-all": return true;
2156
- case "dynamic": {
2157
- if (pi >= pathParts.length) return false;
2158
- const prefix = seg.prefix ?? "";
2159
- const suffix = seg.suffix ?? "";
2160
- const pathPart = pathParts[pi];
2161
- if (prefix || suffix) {
2162
- if (pathPart.length <= prefix.length + suffix.length || prefix && !pathPart.startsWith(prefix) || suffix && !pathPart.endsWith(suffix)) return false;
2163
- }
2164
- pi++;
2165
- continue;
2166
- }
2167
- case "static":
2168
- if (pi >= pathParts.length || pathParts[pi] !== seg.value) return false;
2169
- pi++;
2170
- continue;
2171
- }
2172
- }
2173
- return pi === pathParts.length;
2174
- }
2175
- //#endregion
2176
2085
  //#region src/server/pipeline-outcome.ts
2177
2086
  /**
2178
2087
  * Pipeline outcome translator — converts a `PhaseOutcome` (the value
@@ -2187,16 +2096,7 @@ function pathnameMatchesPattern(pathname, pattern) {
2187
2096
  */
2188
2097
  function rscErrorResponse(isRsc, status, headers) {
2189
2098
  if (!isRsc) return new Response(null, { status });
2190
- const h = headers ?? new Headers();
2191
- h.set("X-Timber-Error", "1");
2192
- h.set("content-type", "application/json; charset=utf-8");
2193
- return new Response(JSON.stringify({
2194
- error: true,
2195
- status
2196
- }), {
2197
- status,
2198
- headers: h
2199
- });
2099
+ return rscErrorEnvelope(status, headers);
2200
2100
  }
2201
2101
  /**
2202
2102
  * Terminal outcome handler — converts a `PhaseOutcome` into a final
@@ -2243,7 +2143,7 @@ async function outcomeToResponse(config, outcome, ctx) {
2243
2143
  await fireOnRequestError(denyRenderError, ctx.req, "render");
2244
2144
  if (config.onPipelineError && denyRenderError instanceof Error) config.onPipelineError(denyRenderError, "render");
2245
2145
  }
2246
- if (isDebug()) console.warn(`[timber] DenySignal(${outcome.signal.status}) from ${outcome.phase} phase — no renderDenyFallback configured, returning bare ${outcome.signal.status} response\n Request: ${ctx.method} ${ctx.path}\n Add a not-found.tsx or error.tsx to render a custom deny page.`);
2146
+ if (isDebug()) console.warn(`[timber] DenySignal(${outcome.signal.status}) from ${outcome.phase} phase — no renderDenyFallback configured, returning bare ${outcome.signal.status} response\n Request: ${ctx.method} ${ctx.path}\n Add a status-code file (e.g. 404.tsx) or error.tsx to render a custom deny page.`);
2247
2147
  return new Response(null, {
2248
2148
  status: outcome.signal.status,
2249
2149
  headers
@@ -2493,15 +2393,6 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
2493
2393
  }
2494
2394
  const handlerResult = await mod.default();
2495
2395
  if (handlerResult instanceof Response) {
2496
- if (method === "HEAD") {
2497
- const headHeaders = new Headers(handlerResult.headers);
2498
- if (handlerResult.ok && !headHeaders.has("Cache-Control")) headHeaders.set("Cache-Control", "public, max-age=14400, must-revalidate");
2499
- return new Response(null, {
2500
- status: handlerResult.status,
2501
- statusText: handlerResult.statusText,
2502
- headers: headHeaders
2503
- });
2504
- }
2505
2396
  const res = cloneWithMutableHeaders(handlerResult);
2506
2397
  if (res.ok && !res.headers.has("Cache-Control")) res.headers.set("Cache-Control", "public, max-age=14400, must-revalidate");
2507
2398
  return res;
@@ -2563,10 +2454,19 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
2563
2454
  const rawSourceUrl = req.headers.get("X-Timber-URL");
2564
2455
  const validatedSourceUrl = rawSourceUrl ? validateInterceptionHeader(rawSourceUrl, stripTrailingSlash) : null;
2565
2456
  if (validatedSourceUrl) {
2566
- const intercepted = findInterceptionMatch(canonicalPathname, validatedSourceUrl, config.interceptionRewrites);
2567
- if (intercepted) {
2568
- const sourceMatch = config.matchRoute(intercepted.sourcePathname);
2569
- if (sourceMatch) {
2457
+ const resolveSourceScopes = () => {
2458
+ const mounted = new Set(parseClientStateTree(req)?.treePaths ?? []);
2459
+ const sourceRoute = config.matchRoute(validatedSourceUrl);
2460
+ const scopes = [mounted, new Set(sourceRoute ? computeSegmentTreePaths(sourceRoute.segments) : [])].filter((scope) => scope.size > 0);
2461
+ return scopes.length > 0 ? scopes : null;
2462
+ };
2463
+ const rewrite = findInterceptionMatch(canonicalPathname, resolveSourceScopes, config.interceptionRewrites);
2464
+ if (rewrite?.mode === "children") {
2465
+ const interceptedMatch = config.matchInterceptedRoute?.(canonicalPathname, rewrite.interceptingScope);
2466
+ if (interceptedMatch) match = interceptedMatch;
2467
+ } else if (rewrite) {
2468
+ const sourceMatch = config.matchRoute(rewrite.interceptingPrefix);
2469
+ if (sourceMatch !== null && computeSegmentTreePaths(sourceMatch.segments).includes(rewrite.interceptingScope)) {
2570
2470
  match = sourceMatch;
2571
2471
  interception = { targetPathname: canonicalPathname };
2572
2472
  }
@@ -2625,6 +2525,194 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
2625
2525
  });
2626
2526
  }
2627
2527
  //#endregion
2528
+ //#region src/server/rsc-cache-key-guard.ts
2529
+ /**
2530
+ * Origin-side binding of the `_rsc` cache key to the request that produced
2531
+ * the response (TIM-1268).
2532
+ *
2533
+ * The key is deterministic, and deliberately so — that is what makes RSC
2534
+ * payloads cacheable. But deterministic also means *predictable*: anyone can
2535
+ * compute the key another client will request. Without this guard, a caller
2536
+ * could request `/about?_rsc=<victim's key>` while sending its own
2537
+ * `X-Timber-State-Tree`, and a shared cache that keys by URL — the exact
2538
+ * environment this feature targets — would store that variant under the
2539
+ * victim's key. Every subsequent legitimate navigation would then be served
2540
+ * a payload rendered for someone else's segment tree or deployment.
2541
+ *
2542
+ * The guard is a *storability* rule, not a rejection: if the claimed key
2543
+ * does not match the key derived from the received headers, the response
2544
+ * loses its shared-cache directives. Mismatched requests are still served
2545
+ * normally, they just cannot be stored for anyone else.
2546
+ *
2547
+ * Why not Next.js's 307 self-correction: a redirect is cacheable in its own
2548
+ * right, costs a round trip on every legitimate miss, and would make the
2549
+ * server's behaviour depend on a parameter it otherwise never reads. This
2550
+ * rule closes the same hole by making the poisoned entry unstorable at the
2551
+ * source, in the response the developer already has to opt into sharing.
2552
+ *
2553
+ * See design/19-client-navigation.md §"The `_rsc` cache key".
2554
+ */
2555
+ /**
2556
+ * Legacy shared-cache control fields that predate RFC 9213 and so do not
2557
+ * follow the `<target>-Cache-Control` naming pattern — the one part of this
2558
+ * rule that cannot be matched by shape. `X-Accel-Expires` is nginx
2559
+ * `proxy_cache`, which gives it precedence over `Cache-Control`.
2560
+ *
2561
+ * A closed historical set, unlike the vendor `*-Cache-Control` names, which
2562
+ * keep being minted and are handled by pattern below.
2563
+ */
2564
+ var LEGACY_CDN_CACHE_HEADERS = [
2565
+ "Surrogate-Control",
2566
+ "Edge-Control",
2567
+ "X-Accel-Expires"
2568
+ ];
2569
+ /**
2570
+ * Whether a response header overrides `Cache-Control` at a shared cache.
2571
+ *
2572
+ * Matched by *shape*, not by vendor name. RFC 9213 defines targeted fields
2573
+ * as `<target>-Cache-Control`, and every CDN spells its own: `CDN-`,
2574
+ * `Cloudflare-CDN-`, `Netlify-CDN-`, `Vercel-CDN-`, and whichever ships
2575
+ * next. A vendor list here is a denylist with an unbounded tail — it was
2576
+ * extended twice during review, each time by one name, which is the signal
2577
+ * that enumeration is the wrong shape. The suffix rule is total.
2578
+ *
2579
+ * `Cache-Control` itself does not match (no `-` prefix) and is replaced
2580
+ * separately.
2581
+ */
2582
+ function isSharedCacheOverride(name) {
2583
+ const lowered = name.toLowerCase();
2584
+ return lowered.endsWith("-cache-control") || LEGACY_CDN_CACHE_HEADERS.some((legacy) => legacy.toLowerCase() === lowered);
2585
+ }
2586
+ /**
2587
+ * Whether the request's `_rsc` parameter matches the key derived from its
2588
+ * own headers.
2589
+ *
2590
+ * Requests without the parameter are not claiming a key at all, so they are
2591
+ * left alone — the bare document URL is the developer's declared territory
2592
+ * (see design/32-response-caching.md, TIM-1264). Neither is a parameter
2593
+ * whose value could not be one of our digests: `_rsc` is not reserved, and
2594
+ * an application that already uses it for its own search state
2595
+ * (`/search?_rsc=foo`) must not have its cache policy silently revoked by a
2596
+ * guard that mistook that for a forgery.
2597
+ */
2598
+ async function hasValidRscCacheKey(req) {
2599
+ if (!req.url.includes("?")) return true;
2600
+ let values;
2601
+ try {
2602
+ values = new URL(req.url).searchParams.getAll(RSC_KEY_PARAM);
2603
+ } catch {
2604
+ return false;
2605
+ }
2606
+ const claims = values.filter(isRscCacheKeyShape);
2607
+ if (claims.length === 0) return true;
2608
+ const expected = await rscCacheKey((name) => req.headers.get(name));
2609
+ if (expected === null) return false;
2610
+ return claims.every((claim) => claim === expected);
2611
+ }
2612
+ /**
2613
+ * Media type of a Flight payload. Also spelled in `rsc-entry/helpers.ts`,
2614
+ * `deny-renderer.ts`, `action-handler.ts`, and `client/rsc-fetch.ts` — that
2615
+ * duplication predates this module and is tracked separately.
2616
+ */
2617
+ var RSC_MEDIA_TYPE = "text/x-component";
2618
+ /**
2619
+ * Ensure a Flight payload can never be interpreted as a document.
2620
+ *
2621
+ * Applied here, at the pipeline's single exit, rather than where each
2622
+ * payload response is built. Six places build one — the success path, three
2623
+ * in `deny-renderer.ts`, the action paths — and hardening them one at a time
2624
+ * is how you end up hardening five of six. The response is Flight if it says
2625
+ * it is; that is the only test that covers every producer, including ones
2626
+ * added later.
2627
+ *
2628
+ * Matters because the payload URL is observable and, on a shared-cache hit,
2629
+ * the origin is never consulted — so a top-level navigation can be handed
2630
+ * these bytes with no request-time check available. See design/13-security.md
2631
+ * #36c.
2632
+ */
2633
+ function hardenRscPayloadResponse(response) {
2634
+ const contentType = response.headers.get("content-type");
2635
+ if (!contentType) return;
2636
+ if (contentType.split(";")[0].trim().toLowerCase() !== RSC_MEDIA_TYPE) return;
2637
+ response.headers.set("X-Content-Type-Options", "nosniff");
2638
+ }
2639
+ /**
2640
+ * Make a response unstorable by any shared cache when its request claimed an
2641
+ * `_rsc` key that does not match its own headers.
2642
+ *
2643
+ * The end state is unconditional and therefore statable: **a mismatched
2644
+ * response is never stored by any cache.** Every targeted cache-control
2645
+ * field is removed and `Cache-Control` is replaced outright, rather than
2646
+ * downgrading only when a shared directive is spotted. Conditional variants
2647
+ * of this were tried and were wrong twice over — they missed the CDN-
2648
+ * specific headers, and leaving `Cache-Control` alone when it looked
2649
+ * harmless lets a CDN apply heuristic caching to a response that has none.
2650
+ *
2651
+ * The cost is that a mismatched request loses developer-chosen *browser*
2652
+ * caching too. That is a request no timber client ever makes.
2653
+ *
2654
+ * Purge tags (`Surrogate-Key`, `Cache-Tag`) are deliberately left alone:
2655
+ * they address stored entries, and this response is not stored.
2656
+ *
2657
+ * Mutates `response.headers` in place; the pipeline guarantees mutable
2658
+ * headers at this point (see TIM-866).
2659
+ */
2660
+ async function enforceRscCacheKey(req, response) {
2661
+ if (await hasValidRscCacheKey(req)) return;
2662
+ const overrides = [];
2663
+ for (const name of response.headers.keys()) if (isSharedCacheOverride(name)) overrides.push(name);
2664
+ for (const name of overrides) response.headers.delete(name);
2665
+ response.headers.set("Cache-Control", "private, no-store");
2666
+ }
2667
+ //#endregion
2668
+ //#region src/server/head-response.ts
2669
+ /**
2670
+ * HEAD body stripping — RFC 9110 §9.3.2.
2671
+ *
2672
+ * "The HEAD method is identical to GET except that the server MUST NOT send
2673
+ * content in the response." The headers, however, must be the ones the
2674
+ * corresponding GET would have produced — which is why timber renders the
2675
+ * route in full and discards the body here rather than short-circuiting the
2676
+ * render. Content-Type, Content-Length, Set-Cookie, cache directives and
2677
+ * everything else ride through untouched.
2678
+ *
2679
+ * Node's `ServerResponse` happens to discard writes for HEAD, so the wire is
2680
+ * usually correct even without this. That is a property of one adapter, not of
2681
+ * the `Response` object: static generation, the Cloudflare and Nitro adapters,
2682
+ * tests, and anything calling the pipeline handler as a function all observe
2683
+ * the `Response` directly and saw the full body before TIM-1324.
2684
+ *
2685
+ * The pipeline applies this at its single exit so every response shape is
2686
+ * covered — page render, API route, metadata route, deny page, error page,
2687
+ * proxy short-circuit. The two unit-level producers that are also reachable as
2688
+ * standalone functions (`handleRouteRequest`, `handleApiRoute`) call it too, so
2689
+ * each honours HEAD on its own; they share this implementation rather than
2690
+ * repeating it, and applying it twice is a no-op.
2691
+ */
2692
+ /**
2693
+ * Return a bodyless copy of `response` when `method` is HEAD.
2694
+ *
2695
+ * A no-op for every other method and for responses that already carry no body
2696
+ * (204, 304, redirects), so callers do not need to pre-check either.
2697
+ *
2698
+ * The discarded stream is cancelled: an SSR render that reached this point has
2699
+ * a live producer attached, and dropping the reference without cancelling
2700
+ * leaks it for the lifetime of the render (design/13-security.md A4).
2701
+ */
2702
+ function stripHeadBody(response, method) {
2703
+ if (method.toUpperCase() !== "HEAD") return response;
2704
+ const body = response.body;
2705
+ if (body === null) return response;
2706
+ try {
2707
+ body.cancel().catch(() => {});
2708
+ } catch {}
2709
+ return new Response(null, {
2710
+ status: response.status,
2711
+ statusText: response.statusText,
2712
+ headers: response.headers
2713
+ });
2714
+ }
2715
+ //#endregion
2628
2716
  //#region src/server/pipeline.ts
2629
2717
  /**
2630
2718
  * Create the request handler from a pipeline configuration.
@@ -2688,12 +2776,14 @@ function createPipeline(config) {
2688
2776
  });
2689
2777
  else result = await innerHandler();
2690
2778
  await setSpanAttribute("http.response.status_code", result.status);
2691
- const varyTokens = ["Accept"];
2692
- if (config.interceptionRewrites?.length) varyTokens.push("X-Timber-URL");
2779
+ const varyTokens = [...RSC_KEY_HEADERS];
2693
2780
  const existingVary = result.headers.get("Vary");
2694
2781
  const existingTokens = existingVary ? existingVary.toLowerCase().split(",").map((t) => t.trim()) : [];
2695
2782
  const newTokens = varyTokens.filter((t) => !existingTokens.includes(t.toLowerCase()));
2696
2783
  if (newTokens.length > 0) result.headers.set("Vary", existingVary ? `${existingVary}, ${newTokens.join(", ")}` : newTokens.join(", "));
2784
+ await enforceRscCacheKey(canonicalReq, result);
2785
+ hardenRscPayloadResponse(result);
2786
+ result = stripHeadBody(result, method);
2697
2787
  if (serverTiming === "detailed") {
2698
2788
  const timingHeader = getServerTimingHeader();
2699
2789
  if (timingHeader) result.headers.set("Server-Timing", timingHeader);
@@ -2735,68 +2825,6 @@ function createPipeline(config) {
2735
2825
  return pipelineFn;
2736
2826
  }
2737
2827
  //#endregion
2738
- //#region src/server/build-manifest.ts
2739
- /**
2740
- * Collect all CSS files needed for a matched route's segment chain.
2741
- *
2742
- * Walks segments root → leaf, collecting CSS for each layout and page.
2743
- * Deduplicates while preserving order (root layout CSS first).
2744
- */
2745
- function collectRouteCss(segments, manifest) {
2746
- const seen = /* @__PURE__ */ new Set();
2747
- const result = [];
2748
- for (const segment of segments) for (const file of [segment.layout, segment.page]) {
2749
- if (!file) continue;
2750
- const cssFiles = manifest.css[file.filePath];
2751
- if (!cssFiles) continue;
2752
- for (const url of cssFiles) if (!seen.has(url)) {
2753
- seen.add(url);
2754
- result.push(url);
2755
- }
2756
- }
2757
- return result;
2758
- }
2759
- /**
2760
- * Collect all font entries needed for a matched route's segment chain.
2761
- *
2762
- * Walks segments root → leaf, collecting fonts for each layout and page.
2763
- * Deduplicates by href while preserving order.
2764
- */
2765
- function collectRouteFonts(segments, manifest) {
2766
- const seen = /* @__PURE__ */ new Set();
2767
- const result = [];
2768
- for (const segment of segments) for (const file of [segment.layout, segment.page]) {
2769
- if (!file) continue;
2770
- const fonts = manifest.fonts[file.filePath];
2771
- if (!fonts) continue;
2772
- for (const entry of fonts) if (!seen.has(entry.href)) {
2773
- seen.add(entry.href);
2774
- result.push(entry);
2775
- }
2776
- }
2777
- return result;
2778
- }
2779
- /**
2780
- * Collect modulepreload URLs for a matched route's segment chain.
2781
- *
2782
- * Walks segments root → leaf, collecting transitive JS dependencies
2783
- * for each layout and page. Deduplicates across segments.
2784
- */
2785
- function collectRouteModulepreloads(segments, manifest) {
2786
- const seen = /* @__PURE__ */ new Set();
2787
- const result = [];
2788
- for (const segment of segments) for (const file of [segment.layout, segment.page]) {
2789
- if (!file) continue;
2790
- const preloads = manifest.modulepreload[file.filePath];
2791
- if (!preloads) continue;
2792
- for (const url of preloads) if (!seen.has(url)) {
2793
- seen.add(url);
2794
- result.push(url);
2795
- }
2796
- }
2797
- return result;
2798
- }
2799
- //#endregion
2800
2828
  //#region src/server/early-hints.ts
2801
2829
  /**
2802
2830
  * 103 Early Hints utilities.
@@ -3122,6 +3150,139 @@ async function wrapWithErrorBoundaries(segment, element, loadModule, createEleme
3122
3150
  return element;
3123
3151
  }
3124
3152
  //#endregion
3153
+ //#region src/server/status-code-resolver.ts
3154
+ /**
3155
+ * Look up `{statusStr}` then `{categoryKey}` (e.g. "4xx" / "5xx") in a
3156
+ * status-file group on a single segment. Shared by all three fallback
3157
+ * chains — the only structural difference between component 4xx,
3158
+ * component 5xx, and JSON resolution is *which* group is searched and
3159
+ * how the per-segment loop is layered around it.
3160
+ */
3161
+ function lookupInGroup(group, statusStr, categoryKey, segmentIndex, status) {
3162
+ if (!group) return null;
3163
+ const exact = group[statusStr];
3164
+ if (exact) return {
3165
+ file: exact,
3166
+ status,
3167
+ kind: "exact",
3168
+ segmentIndex
3169
+ };
3170
+ const category = group[categoryKey];
3171
+ if (category) return {
3172
+ file: category,
3173
+ status,
3174
+ kind: "category",
3175
+ segmentIndex
3176
+ };
3177
+ return null;
3178
+ }
3179
+ /**
3180
+ * Resolve the status-code file to render for a given HTTP status code.
3181
+ *
3182
+ * Walks the segment chain from leaf to root following the fallback chain
3183
+ * defined in design/10-error-handling.md. Returns null if no file is found
3184
+ * (caller should render the framework default).
3185
+ *
3186
+ * @param status - The HTTP status code (4xx or 5xx).
3187
+ * @param segments - The matched segment chain from root (index 0) to leaf (last).
3188
+ * @param format - The response format family ('component' or 'json'). Defaults to 'component'.
3189
+ */
3190
+ function resolveStatusFile(status, segments, format = "component") {
3191
+ if (status < 400 || status > 599) return null;
3192
+ if (format === "json") return resolveJson(status, segments);
3193
+ if (status <= 499) return resolve4xx(status, segments);
3194
+ return resolve5xx(status, segments);
3195
+ }
3196
+ /**
3197
+ * 4xx component fallback chain — two separate full passes leaf→root.
3198
+ *
3199
+ * The passes must be separate (not interleaved per-segment) so that a
3200
+ * root-level `404.tsx` beats a leaf-level `error.tsx`. The 5xx chain
3201
+ * inverts this and is per-segment: a leaf's `error.tsx` beats a root's
3202
+ * `5xx.tsx`. This asymmetry is the only reason these two functions exist
3203
+ * separately.
3204
+ *
3205
+ * Pass 1 — {status}.tsx → 4xx.tsx (statusFiles)
3206
+ * Pass 2 — error.tsx (error)
3207
+ */
3208
+ function resolve4xx(status, segments) {
3209
+ const statusStr = String(status);
3210
+ for (let i = segments.length - 1; i >= 0; i--) {
3211
+ const r = lookupInGroup(segments[i].statusFiles, statusStr, "4xx", i, status);
3212
+ if (r) return r;
3213
+ }
3214
+ for (let i = segments.length - 1; i >= 0; i--) {
3215
+ const errorFile = segments[i].error;
3216
+ if (errorFile) return {
3217
+ file: errorFile,
3218
+ status,
3219
+ kind: "error",
3220
+ segmentIndex: i
3221
+ };
3222
+ }
3223
+ return null;
3224
+ }
3225
+ /**
3226
+ * 5xx component fallback chain — single pass, per-segment leaf→root.
3227
+ *
3228
+ * At each segment: {status}.tsx → 5xx.tsx → error.tsx. A leaf's
3229
+ * `error.tsx` therefore beats a root's `5xx.tsx`, which is the
3230
+ * intentional inverse of the 4xx chain.
3231
+ */
3232
+ function resolve5xx(status, segments) {
3233
+ const statusStr = String(status);
3234
+ for (let i = segments.length - 1; i >= 0; i--) {
3235
+ const segment = segments[i];
3236
+ const r = lookupInGroup(segment.statusFiles, statusStr, "5xx", i, status);
3237
+ if (r) return r;
3238
+ if (segment.error) return {
3239
+ file: segment.error,
3240
+ status,
3241
+ kind: "error",
3242
+ segmentIndex: i
3243
+ };
3244
+ }
3245
+ return null;
3246
+ }
3247
+ /**
3248
+ * JSON fallback chain (for both 4xx and 5xx) — single pass leaf→root.
3249
+ *
3250
+ * At each segment: {status}.json → {category}.json. No legacy compat,
3251
+ * no error.tsx — the JSON chain terminates at the category catch-all
3252
+ * and the caller falls back to a bare-JSON framework default.
3253
+ */
3254
+ function resolveJson(status, segments) {
3255
+ const statusStr = String(status);
3256
+ const categoryKey = status >= 500 ? "5xx" : "4xx";
3257
+ for (let i = segments.length - 1; i >= 0; i--) {
3258
+ const r = lookupInGroup(segments[i].jsonStatusFiles, statusStr, categoryKey, i, status);
3259
+ if (r) return r;
3260
+ }
3261
+ return null;
3262
+ }
3263
+ /**
3264
+ * Resolve the denial file for a parallel route slot.
3265
+ *
3266
+ * Slot denial is graceful degradation — no HTTP status on the wire.
3267
+ * Fallback chain: denied.tsx → default.tsx → null.
3268
+ *
3269
+ * @param slotNode - The segment node for the slot (segmentType === 'slot').
3270
+ */
3271
+ function resolveSlotDenied(slotNode) {
3272
+ const slotName = slotNode.segmentName.replace(/^@/, "");
3273
+ if (slotNode.denied) return {
3274
+ file: slotNode.denied,
3275
+ slotName,
3276
+ kind: "denied"
3277
+ };
3278
+ if (slotNode.default) return {
3279
+ file: slotNode.default,
3280
+ slotName,
3281
+ kind: "default"
3282
+ };
3283
+ return null;
3284
+ }
3285
+ //#endregion
3125
3286
  //#region src/server/csrf.ts
3126
3287
  /** HTTP methods that are considered safe (no mutation). */
3127
3288
  var SAFE_METHODS = /* @__PURE__ */ new Set([
@@ -3303,14 +3464,8 @@ async function handleRouteRequest(mod, ctx) {
3303
3464
  });
3304
3465
  }
3305
3466
  if (method === "HEAD") {
3306
- if (mod.HEAD) return runHandler(mod.HEAD, ctx);
3307
- if (mod.GET) {
3308
- const res = await runHandler(mod.GET, ctx);
3309
- return new Response(null, {
3310
- status: res.status,
3311
- headers: res.headers
3312
- });
3313
- }
3467
+ if (mod.HEAD) return stripHeadBody(await runHandler(mod.HEAD, ctx), method);
3468
+ if (mod.GET) return stripHeadBody(await runHandler(mod.GET, ctx), method);
3314
3469
  }
3315
3470
  const handler = mod[method];
3316
3471
  if (!handler) return new Response(null, {
@@ -3537,11 +3692,11 @@ var registry = /* @__PURE__ */ new Map();
3537
3692
  */
3538
3693
  function resolveKeyParams(store) {
3539
3694
  const main = store.segmentParams ?? {};
3540
- const map = store.slotParamsMap;
3541
- if (!map || map.size === 0) return main;
3695
+ const slots = store.slotParams;
3696
+ if (!slots || Object.keys(slots).length === 0) return main;
3542
3697
  const mainCanonical = tryCanonicalize({ ...main });
3543
3698
  if (mainCanonical === null) return null;
3544
- for (const slotParams of map.values()) {
3699
+ for (const slotParams of Object.values(slots)) {
3545
3700
  const merged = tryCanonicalize({
3546
3701
  ...main,
3547
3702
  ...slotParams