@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
@@ -34,7 +34,10 @@ import {
34
34
  } from './deny-boundary.js';
35
35
  import type { DenyPageEntry } from './deny-boundary.js';
36
36
  import { LAYOUT_CHILDREN_SLOT } from './prebuilt/slots.js';
37
- import { resolveSlotProps, extractUrlParts, type SlotSkipEntry } from './slot-resolver.js';
37
+ import { resolveSlotProps, publishSlotSegmentParams, type SlotSkipEntry } from './slot-resolver.js';
38
+ import { consumedUrlParts } from './chain-url-parts.js';
39
+ import { withPublishedParams } from './publish-params.js';
40
+ import type { RscPayloadRoot } from '../shared/payload-root.js';
38
41
  import { SegmentProvider } from '../client/segment-context.js';
39
42
  import { SegmentOutlet } from '../client/segment-outlet.js';
40
43
  import { SlotOutlet } from '../client/slot-outlet.js';
@@ -42,93 +45,16 @@ import { SlotsProvider } from '../client/slot-provider.js';
42
45
 
43
46
  import { wrapSegmentWithErrorBoundaries } from './error-boundary-wrapper.js';
44
47
  import type { InterceptionContext } from './pipeline.js';
48
+ import { shouldSkipSegment, type ClientStateTree } from './state-tree-diff.js';
49
+ import { truncateSkippableForChangedSlots } from './skippable-prefix.js';
45
50
  import {
46
- shouldSkipSegment,
47
- shouldSkipSlot,
48
51
  computeSegmentKeys,
52
+ computeSegmentTreePaths,
49
53
  computeSlotKey,
50
- type ClientStateTree,
51
- } from './state-tree-diff.js';
54
+ } from '../routing/segment-keys.js';
52
55
  import { headElementsToReact } from './metadata.js';
53
56
  import { loadModule } from './safe-load.js';
54
57
 
55
- /**
56
- * Truncate the skippable prefix when a skippable layout has slots whose
57
- * content changed between the departing and destination URLs (TIM-1232).
58
- *
59
- * Without this, changed slots at skipped layouts are never sent to the
60
- * client — the `continue` in the main loop skips slot resolution entirely.
61
- * Removing the segment from the skippable set forces it to render with
62
- * its slots. The prefix must stay contiguous, so all segments after the
63
- * first changed-slot segment are also removed.
64
- */
65
- function truncateSkippableForChangedSlots(
66
- skippableSet: Set<number>,
67
- segments: ManifestSegmentNode[],
68
- segmentKeys: string[],
69
- match: RouteMatch,
70
- clientStateTree: ClientStateTree,
71
- req: Request
72
- ): void {
73
- const departingUrl = req.headers.get('X-Timber-URL');
74
- let depParsed: URL | null = null;
75
- if (departingUrl) {
76
- try {
77
- depParsed = new URL(departingUrl, 'http://localhost');
78
- } catch {
79
- skippableSet.clear();
80
- return;
81
- }
82
- }
83
- const destPathname = new URL(req.url, 'http://localhost').pathname;
84
- const depPathname = depParsed?.pathname ?? null;
85
- const split = (p: string) => (p === '/' ? [] : p.slice(1).split('/'));
86
- const destAll = split(destPathname);
87
- const depAll = depPathname ? split(depPathname) : null;
88
- const clientSlots = clientStateTree.slots ?? null;
89
- const rawParams = match.rawSegmentParams ?? match.segmentParams ?? {};
90
-
91
- for (let i = 0; i < segments.length; i++) {
92
- if (!skippableSet.has(i)) continue;
93
- const segment = segments[i];
94
- const slotEntries = Object.entries(segment.slots ?? {});
95
- if (slotEntries.length === 0) continue;
96
-
97
- const segIdx = match.segments.indexOf(segment);
98
- const parents =
99
- segIdx >= 0 ? match.segments.slice(0, segIdx + 1).filter((s) => s.segmentName !== '') : [];
100
- const sliceAt = extractUrlParts(parents, rawParams).length;
101
- const destParts = destAll.slice(sliceAt);
102
- const depParts = depAll ? depAll.slice(sliceAt) : null;
103
- let parentChanged = false;
104
- if (depAll) {
105
- const dp = depAll.slice(0, sliceAt),
106
- dd = destAll.slice(0, sliceAt);
107
- parentChanged = dp.length !== dd.length || dp.some((p, j) => p !== dd[j]);
108
- }
109
-
110
- for (const [slotName, slotNode] of slotEntries) {
111
- const m = slotNode as ManifestSegmentNode;
112
- const k = computeSlotKey(segmentKeys[i], `@${slotName}`);
113
- if (
114
- m.children.some((c) => c.segmentType === 'intercepting') ||
115
- parentChanged ||
116
- depParts === null ||
117
- !shouldSkipSlot({
118
- slotKey: k,
119
- clientSlots,
120
- slotNode: m,
121
- departingUrlParts: depParts,
122
- destinationUrlParts: destParts,
123
- })
124
- ) {
125
- for (let j = i; j < segments.length; j++) skippableSet.delete(j);
126
- return;
127
- }
128
- }
129
- }
130
- }
131
-
132
58
  /**
133
59
  * Replace the outermost SegmentOutlet in a partial payload with its
134
60
  * SegmentProvider child. Walks through AccessGate wrappers (which sit
@@ -175,6 +101,28 @@ const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference');
175
101
  * function. Client components must go through createElement only — they are
176
102
  * serialized as references in the RSC Flight stream, not executed on the server.
177
103
  */
104
+ /**
105
+ * The `segments` array handed to a layout's `SegmentProvider` — the value
106
+ * `useSelectedLayoutSegment(s)` reads.
107
+ *
108
+ * Only its **length** matters: both hooks take `segments.length` as a depth and
109
+ * index the live pathname with it. So this is really "how many URL parts had
110
+ * been consumed by the time this layout rendered", which is
111
+ * `consumedUrlParts` — the same derivation slot matching and the slot-skip
112
+ * slicing use, so a layout and the slots beside it cannot disagree about how
113
+ * deep they are. The root consumes nothing → `['']`, length 1.
114
+ *
115
+ * @internal — exported for tests; production has exactly one caller.
116
+ */
117
+ export function computeLayoutSegmentPath(
118
+ segments: ManifestSegmentNode[],
119
+ index: number,
120
+ match: RouteMatch
121
+ ): string[] {
122
+ const parts = consumedUrlParts(segments, index, match);
123
+ return parts.length === 0 ? [''] : ['', ...parts];
124
+ }
125
+
178
126
  export function isClientReference(component: unknown): boolean {
179
127
  return (
180
128
  component != null &&
@@ -217,8 +165,12 @@ export interface LayoutComponentEntry {
217
165
 
218
166
  /** Result of building a route element tree. */
219
167
  export interface RouteElementResult {
220
- /** The React element tree (page wrapped in layouts, access gates, error boundaries). */
221
- element: React.ReactElement;
168
+ /**
169
+ * The payload root: the React element tree (page wrapped in layouts, access
170
+ * gates, error boundaries) plus the params it rendered with, as siblings.
171
+ * See shared/payload-root.ts for why params are not a wrapper around it.
172
+ */
173
+ payload: RscPayloadRoot;
222
174
  /** Layout components loaded along the segment chain. */
223
175
  layoutComponents: LayoutComponentEntry[];
224
176
  /** Segments from the route match. */
@@ -247,6 +199,17 @@ export interface RouteElementResult {
247
199
  * enabling the client to populate its segment cache with slot entries.
248
200
  */
249
201
  slotSkipInfo: SlotSkipEntry[];
202
+ /**
203
+ * Index of the first segment whose `access.ts` denied, or `Infinity`.
204
+ *
205
+ * AccessGate sits OUTSIDE the layout, so a denial means this segment and
206
+ * everything below it never rendered — no layout, no slots. It is the
207
+ * boundary of the tree that actually exists, which is why
208
+ * `collectMetadataRouteHeadElements` already filters on it and why
209
+ * `buildSegmentInfo` must too: the layouts were still *loaded*, so a list
210
+ * built from `layoutComponents` describes more than what mounted.
211
+ */
212
+ firstDeniedIndex: number;
250
213
  }
251
214
 
252
215
  // ─── Module Processing Helpers ─────────────────────────────────────────────
@@ -554,6 +517,10 @@ export async function buildRouteElement(
554
517
  // the same keys are used for skip decisions, SegmentOutlet props, and
555
518
  // the X-Timber-Segments header (via buildSegmentInfo).
556
519
  const segmentKeys = computeSegmentKeys(segments);
520
+ // The directory path of each segment — the key `setSlotParams` and codegen
521
+ // both use for a slot beneath it, and the identity interception scoping
522
+ // compares against. One derivation, so a slot cannot be keyed two ways.
523
+ const segmentTreePaths = computeSegmentTreePaths(segments);
557
524
 
558
525
  // Pre-compute the contiguous skippable prefix (top-down).
559
526
  //
@@ -645,8 +612,27 @@ export async function buildRouteElement(
645
612
 
646
613
  // Slot caching: the departing URL (X-Timber-URL header) and the
647
614
  // destination URL (request URL) are compared to determine which
648
- // slots' content has changed. Full URLs (with search) are passed
649
- // so slot pages reading getSearchParams() are correctly re-rendered.
615
+ // slots' content has changed.
616
+ //
617
+ // Pathnames only. The client sends `X-Timber-URL` as a pathname
618
+ // (`client/router.ts` takes `.pathname` on both branches) and
619
+ // `slot-resolver.ts` reads `.pathname` from both sides, so search
620
+ // params take no part in the skip decision.
621
+ //
622
+ // Nothing here can tell whether a slot's content depends on the query.
623
+ // What is supposed to make that safe lives elsewhere: a slot whose server
624
+ // render varies with the query must reach it through a request accessor
625
+ // (page components are built with no props), which the request-dep analysis
626
+ // is meant to catch — and that analysis is incomplete for accessors reached
627
+ // through indirection, so this is currently a real staleness hole. See
628
+ // TIM-1291 and design/19-client-navigation.md. A slot whose *client* subtree
629
+ // varies with the query reads useSearchParams(), which re-renders from
630
+ // NavigationContext whether or not the slot was skipped; that half is
631
+ // unconditional.
632
+ //
633
+ // Do not add search to the comparison by sending the full departing URL:
634
+ // the header is hashed into the public `_rsc` cache key (TIM-1268),
635
+ // and a digest does not anonymize a guessable query string.
650
636
  const departingUrl = req.headers.get('X-Timber-URL');
651
637
  const destinationUrl = req.url;
652
638
  const slotSkipInfo: SlotSkipEntry[] = [];
@@ -661,6 +647,14 @@ export async function buildRouteElement(
661
647
  // Record for X-Timber-Skipped-Segments header (outermost first, so prepend).
662
648
  skippedSegments.unshift(segmentKeys[i]);
663
649
 
650
+ // A skipped layout never reaches resolveSlotProps below, but its slots
651
+ // stay mounted on the client — so their params still have to be
652
+ // published, or useSegmentParams(slotPath) falls back to the main
653
+ // route's record after any navigation that skipped their parent
654
+ // (TIM-1285). Params only: nothing renders and no access chain runs,
655
+ // because an unchanged slot is what made this layout skippable.
656
+ publishSlotSegmentParams(segment, match, destinationUrl, segmentTreePaths[i]);
657
+
664
658
  // SECURITY: Even though the layout is skipped, AccessGate MUST still
665
659
  // wrap the element tree. access.ts runs on every navigation regardless
666
660
  // of cached layouts or state tree content.
@@ -705,13 +699,7 @@ export async function buildRouteElement(
705
699
  // so slot props are never consumed. Resolving them is wasted work
706
700
  // and could trigger side effects in slot access.ts functions.
707
701
  const segmentId = segmentKeys[i];
708
- const parentTreePath =
709
- '/' +
710
- segments
711
- .slice(0, i + 1)
712
- .map((s) => s.segmentName)
713
- .filter(Boolean)
714
- .join('/');
702
+ const parentTreePath = segmentTreePaths[i];
715
703
  const slotProps =
716
704
  i >= firstDeniedIndex
717
705
  ? {}
@@ -728,10 +716,7 @@ export async function buildRouteElement(
728
716
  slotSkipInfo,
729
717
  });
730
718
 
731
- // '/'.split('/') → ['', ''] (length 2), but root should be [''] (length 1).
732
- // The extra empty string makes useSelectedLayoutSegment skip the first URL segment.
733
- const rawPath = segment.urlPath.split('/');
734
- const segmentPath = rawPath.length === 2 && rawPath[1] === '' ? [''] : rawPath;
719
+ const segmentPath = computeLayoutSegmentPath(segments, i, match);
735
720
  const parallelRouteKeys = Object.keys(segment.slots ?? {});
736
721
 
737
722
  // Build the layout element.
@@ -854,13 +839,19 @@ export async function buildRouteElement(
854
839
  shellSettled = Promise.race([Promise.all(shellPromises).then(() => {}), shortCircuit]);
855
840
  }
856
841
 
842
+ // Publish the request's params beside the finished tree, as the payload's
843
+ // root row. Last, because the partial-navigation rewrite above rebuilds the
844
+ // tree and this takes it as it finally stands.
845
+ const payload = withPublishedParams(element);
846
+
857
847
  return {
858
- element,
848
+ payload,
859
849
  layoutComponents,
860
850
  segments,
861
851
  deferSuspenseFor,
862
852
  skippedSegments,
863
853
  shellSettled,
864
854
  slotSkipInfo,
855
+ firstDeniedIndex,
865
856
  };
866
857
  }
@@ -11,6 +11,7 @@
11
11
  import type { RouteContext } from './types.js';
12
12
  import { logRouteError } from './logger.js';
13
13
  import { DenySignal, RedirectSignal } from './primitives.js';
14
+ import { stripHeadBody } from './head-response.js';
14
15
 
15
16
  // ─── Types ───────────────────────────────────────────────────────────────
16
17
 
@@ -89,18 +90,17 @@ export async function handleRouteRequest(mod: RouteModule, ctx: RouteContext): P
89
90
  });
90
91
  }
91
92
 
92
- // HEAD fallback — run GET, strip body
93
+ // HEAD fallback — dispatch to GET when the module exports no HEAD, so the
94
+ // headers are the ones a GET would produce. The body is stripped by the
95
+ // shared helper (also applied at the pipeline exit, where it is a no-op the
96
+ // second time) rather than open-coded here — one implementation means the
97
+ // discarded stream is cancelled on every path. See TIM-1324.
93
98
  if (method === 'HEAD') {
94
99
  if (mod.HEAD) {
95
- return runHandler(mod.HEAD, ctx);
100
+ return stripHeadBody(await runHandler(mod.HEAD, ctx), method);
96
101
  }
97
102
  if (mod.GET) {
98
- const res = await runHandler(mod.GET, ctx);
99
- // Return headers + status but no body
100
- return new Response(null, {
101
- status: res.status,
102
- headers: res.headers,
103
- });
103
+ return stripHeadBody(await runHandler(mod.GET, ctx), method);
104
104
  }
105
105
  }
106
106
 
@@ -83,10 +83,27 @@ function matchPathname(root: ManifestSegmentNode, pathname: string): RouteMatch
83
83
  const result = matchUrlParts(root, parts);
84
84
  if (!result) return null;
85
85
 
86
- // Collect middleware from all segments in the chain, root to leaf.
87
- // Each middleware is lazy-loaded via dynamic import on first invocation.
86
+ return {
87
+ segments: result.chain,
88
+ segmentParams: result.params,
89
+ middlewareChain: collectMiddlewareChain(result.chain),
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Collect middleware from a matched segment chain, root to leaf.
95
+ *
96
+ * Each entry is lazy-loaded via dynamic import on first invocation.
97
+ *
98
+ * Shared with the children-path interception resolver
99
+ * (`children-interception.ts`), which assembles a chain from the tree rather
100
+ * than from a pathname but produces the same kind of `RouteMatch` — every
101
+ * `middleware.ts` above an intercepted page must run exactly as it does for
102
+ * the canonical route.
103
+ */
104
+ export function collectMiddlewareChain(chain: ManifestSegmentNode[]): MiddlewareFn[] {
88
105
  const middlewareChain: MiddlewareFn[] = [];
89
- for (const segment of result.chain) {
106
+ for (const segment of chain) {
90
107
  if (segment.middleware) {
91
108
  const loader = segment.middleware.load;
92
109
  middlewareChain.push(async (ctx) => {
@@ -97,12 +114,7 @@ function matchPathname(root: ManifestSegmentNode, pathname: string): RouteMatch
97
114
  });
98
115
  }
99
116
  }
100
-
101
- return {
102
- segments: result.chain,
103
- segmentParams: result.params,
104
- middlewareChain,
105
- };
117
+ return middlewareChain;
106
118
  }
107
119
 
108
120
  // ─── Metadata Route Matcher ─────────────────────────────────────────────
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Origin-side binding of the `_rsc` cache key to the request that produced
3
+ * the response (TIM-1268).
4
+ *
5
+ * The key is deterministic, and deliberately so — that is what makes RSC
6
+ * payloads cacheable. But deterministic also means *predictable*: anyone can
7
+ * compute the key another client will request. Without this guard, a caller
8
+ * could request `/about?_rsc=<victim's key>` while sending its own
9
+ * `X-Timber-State-Tree`, and a shared cache that keys by URL — the exact
10
+ * environment this feature targets — would store that variant under the
11
+ * victim's key. Every subsequent legitimate navigation would then be served
12
+ * a payload rendered for someone else's segment tree or deployment.
13
+ *
14
+ * The guard is a *storability* rule, not a rejection: if the claimed key
15
+ * does not match the key derived from the received headers, the response
16
+ * loses its shared-cache directives. Mismatched requests are still served
17
+ * normally, they just cannot be stored for anyone else.
18
+ *
19
+ * Why not Next.js's 307 self-correction: a redirect is cacheable in its own
20
+ * right, costs a round trip on every legitimate miss, and would make the
21
+ * server's behaviour depend on a parameter it otherwise never reads. This
22
+ * rule closes the same hole by making the poisoned entry unstorable at the
23
+ * source, in the response the developer already has to opt into sharing.
24
+ *
25
+ * See design/19-client-navigation.md §"The `_rsc` cache key".
26
+ */
27
+
28
+ import { rscCacheKey, isRscCacheKeyShape, RSC_KEY_PARAM } from '../shared/rsc-cache-key.js';
29
+
30
+ /**
31
+ * Legacy shared-cache control fields that predate RFC 9213 and so do not
32
+ * follow the `<target>-Cache-Control` naming pattern — the one part of this
33
+ * rule that cannot be matched by shape. `X-Accel-Expires` is nginx
34
+ * `proxy_cache`, which gives it precedence over `Cache-Control`.
35
+ *
36
+ * A closed historical set, unlike the vendor `*-Cache-Control` names, which
37
+ * keep being minted and are handled by pattern below.
38
+ */
39
+ const LEGACY_CDN_CACHE_HEADERS = ['Surrogate-Control', 'Edge-Control', 'X-Accel-Expires'];
40
+
41
+ /**
42
+ * Whether a response header overrides `Cache-Control` at a shared cache.
43
+ *
44
+ * Matched by *shape*, not by vendor name. RFC 9213 defines targeted fields
45
+ * as `<target>-Cache-Control`, and every CDN spells its own: `CDN-`,
46
+ * `Cloudflare-CDN-`, `Netlify-CDN-`, `Vercel-CDN-`, and whichever ships
47
+ * next. A vendor list here is a denylist with an unbounded tail — it was
48
+ * extended twice during review, each time by one name, which is the signal
49
+ * that enumeration is the wrong shape. The suffix rule is total.
50
+ *
51
+ * `Cache-Control` itself does not match (no `-` prefix) and is replaced
52
+ * separately.
53
+ */
54
+ function isSharedCacheOverride(name: string): boolean {
55
+ const lowered = name.toLowerCase();
56
+ return (
57
+ lowered.endsWith('-cache-control') ||
58
+ LEGACY_CDN_CACHE_HEADERS.some((legacy) => legacy.toLowerCase() === lowered)
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Whether the request's `_rsc` parameter matches the key derived from its
64
+ * own headers.
65
+ *
66
+ * Requests without the parameter are not claiming a key at all, so they are
67
+ * left alone — the bare document URL is the developer's declared territory
68
+ * (see design/32-response-caching.md, TIM-1264). Neither is a parameter
69
+ * whose value could not be one of our digests: `_rsc` is not reserved, and
70
+ * an application that already uses it for its own search state
71
+ * (`/search?_rsc=foo`) must not have its cache policy silently revoked by a
72
+ * guard that mistook that for a forgery.
73
+ */
74
+ export async function hasValidRscCacheKey(req: Request): Promise<boolean> {
75
+ // Fast path: this runs on every request, and the overwhelming majority
76
+ // (document requests) have no query string at all. Keyed on `?` and not
77
+ // on the literal `_rsc=`, because a substring test is not a parse: a
78
+ // request for `?%5Frsc=<key>` carries no literal `_rsc=`, yet
79
+ // `URLSearchParams` decodes it to one — and a cache that canonicalizes
80
+ // percent-encoded unreserved characters in its key while forwarding the
81
+ // original URL would store the response under the decoded, claimed key.
82
+ // The only sound fast path is one that cannot answer "no key" wrongly.
83
+ if (!req.url.includes('?')) return true;
84
+ let values: string[];
85
+ try {
86
+ values = new URL(req.url).searchParams.getAll(RSC_KEY_PARAM);
87
+ } catch {
88
+ // Unparseable URL — nothing to validate, and nothing safe to assume.
89
+ return false;
90
+ }
91
+ // Every value, not the first. An application that owns `_rsc` for its own
92
+ // search state makes the client emit `?_rsc=foo&_rsc=<key>` — reading only
93
+ // the first would see `foo`, exempt it as application data, and never
94
+ // check the real key sitting beside it.
95
+ const claims = values.filter(isRscCacheKeyShape);
96
+ if (claims.length === 0) return true;
97
+ const expected = await rscCacheKey((name) => req.headers.get(name));
98
+ // No digest available means the claim is unverifiable, which is not the
99
+ // same as verified. Every server runtime timber targets has
100
+ // `crypto.subtle`, so this is a guard against a future one that does not.
101
+ if (expected === null) return false;
102
+ // All of them, because at most one can be legitimate and a cache keys on
103
+ // the whole query string — a second, mismatched key would ride along
104
+ // inside an address that otherwise looks validated.
105
+ return claims.every((claim) => claim === expected);
106
+ }
107
+
108
+ /**
109
+ * Media type of a Flight payload. Also spelled in `rsc-entry/helpers.ts`,
110
+ * `deny-renderer.ts`, `action-handler.ts`, and `client/rsc-fetch.ts` — that
111
+ * duplication predates this module and is tracked separately.
112
+ */
113
+ const RSC_MEDIA_TYPE = 'text/x-component';
114
+
115
+ /**
116
+ * Ensure a Flight payload can never be interpreted as a document.
117
+ *
118
+ * Applied here, at the pipeline's single exit, rather than where each
119
+ * payload response is built. Six places build one — the success path, three
120
+ * in `deny-renderer.ts`, the action paths — and hardening them one at a time
121
+ * is how you end up hardening five of six. The response is Flight if it says
122
+ * it is; that is the only test that covers every producer, including ones
123
+ * added later.
124
+ *
125
+ * Matters because the payload URL is observable and, on a shared-cache hit,
126
+ * the origin is never consulted — so a top-level navigation can be handed
127
+ * these bytes with no request-time check available. See design/13-security.md
128
+ * #36c.
129
+ */
130
+ export function hardenRscPayloadResponse(response: Response): void {
131
+ const contentType = response.headers.get('content-type');
132
+ if (!contentType) return;
133
+ if (contentType.split(';')[0].trim().toLowerCase() !== RSC_MEDIA_TYPE) return;
134
+ response.headers.set('X-Content-Type-Options', 'nosniff');
135
+ }
136
+
137
+ /**
138
+ * Make a response unstorable by any shared cache when its request claimed an
139
+ * `_rsc` key that does not match its own headers.
140
+ *
141
+ * The end state is unconditional and therefore statable: **a mismatched
142
+ * response is never stored by any cache.** Every targeted cache-control
143
+ * field is removed and `Cache-Control` is replaced outright, rather than
144
+ * downgrading only when a shared directive is spotted. Conditional variants
145
+ * of this were tried and were wrong twice over — they missed the CDN-
146
+ * specific headers, and leaving `Cache-Control` alone when it looked
147
+ * harmless lets a CDN apply heuristic caching to a response that has none.
148
+ *
149
+ * The cost is that a mismatched request loses developer-chosen *browser*
150
+ * caching too. That is a request no timber client ever makes.
151
+ *
152
+ * Purge tags (`Surrogate-Key`, `Cache-Tag`) are deliberately left alone:
153
+ * they address stored entries, and this response is not stored.
154
+ *
155
+ * Mutates `response.headers` in place; the pipeline guarantees mutable
156
+ * headers at this point (see TIM-866).
157
+ */
158
+ export async function enforceRscCacheKey(req: Request, response: Response): Promise<void> {
159
+ if (await hasValidRscCacheKey(req)) return;
160
+ // Collected before deleting — mutating Headers while iterating its live
161
+ // key iterator can skip entries.
162
+ const overrides: string[] = [];
163
+ for (const name of response.headers.keys()) {
164
+ if (isSharedCacheOverride(name)) overrides.push(name);
165
+ }
166
+ for (const name of overrides) {
167
+ response.headers.delete(name);
168
+ }
169
+ response.headers.set('Cache-Control', 'private, no-store');
170
+ }
@@ -11,6 +11,7 @@ import type { ManifestSegmentNode } from '../route-matcher.js';
11
11
  import { loadModule } from '../safe-load.js';
12
12
  import { DenySignal, RedirectSignal } from '../primitives.js';
13
13
  import { handleRouteRequest } from '../route-handler.js';
14
+ import { stripHeadBody } from '../head-response.js';
14
15
  import type { RouteModule } from '../route-handler.js';
15
16
  import type { RouteContext } from '../types.js';
16
17
 
@@ -89,9 +90,10 @@ export async function handleApiRoute(
89
90
  * method handler into the proper API response: DenySignal → JSON status-file
90
91
  * chain via `renderApiDeny`, RedirectSignal → bare 3xx with Location header.
91
92
  *
92
- * For HEAD requests, strip the deny body to preserve HEAD semantics — the
93
- * normal HEAD path in `handleRouteRequest` strips bodies after the handler
94
- * returns, but signals bypass that codepath.
93
+ * For HEAD requests, strip the deny body to preserve HEAD semantics — signals
94
+ * bypass the HEAD path in `handleRouteRequest`, so this exit needs its own
95
+ * call. Shares `stripHeadBody` with that path and with the pipeline exit; the
96
+ * pipeline applying it again downstream is a no-op (TIM-1324).
95
97
  *
96
98
  * Returns `null` for non-signal errors so the caller knows to rethrow.
97
99
  */
@@ -102,11 +104,7 @@ async function translateApiSignal(
102
104
  responseHeaders: Headers
103
105
  ): Promise<Response | null> {
104
106
  if (error instanceof DenySignal) {
105
- const res = await renderApiDeny(error, segments, responseHeaders);
106
- if (req.method.toUpperCase() === 'HEAD') {
107
- return new Response(null, { status: res.status, headers: res.headers });
108
- }
109
- return res;
107
+ return stripHeadBody(await renderApiDeny(error, segments, responseHeaders), req.method);
110
108
  }
111
109
  if (error instanceof RedirectSignal) {
112
110
  responseHeaders.set('Location', error.location);