@timber-js/app 0.2.0-alpha.185 → 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 (418) 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-Byy425ng.js.map → segment-classify-C539Pa2O.js.map} +1 -1
  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-_6zKFlch.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 +865 -306
  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 +38 -25
  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 +906 -321
  136. package/dist/index.js.map +1 -1
  137. package/dist/plugin-context.d.ts +31 -0
  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 +136 -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/walkers.d.ts +6 -0
  173. package/dist/routing/walkers.d.ts.map +1 -1
  174. package/dist/search-params/define.d.ts +0 -6
  175. package/dist/search-params/define.d.ts.map +1 -1
  176. package/dist/search-params/index.js +176 -2
  177. package/dist/search-params/index.js.map +1 -1
  178. package/dist/segment-params/define.d.ts +0 -6
  179. package/dist/segment-params/define.d.ts.map +1 -1
  180. package/dist/segment-params/index.js +69 -1
  181. package/dist/segment-params/index.js.map +1 -0
  182. package/dist/server/actions.d.ts +6 -3
  183. package/dist/server/actions.d.ts.map +1 -1
  184. package/dist/server/als-registry.d.ts +2 -1
  185. package/dist/server/als-registry.d.ts.map +1 -1
  186. package/dist/server/build-manifest.d.ts +13 -1
  187. package/dist/server/build-manifest.d.ts.map +1 -1
  188. package/dist/server/chain-url-parts.d.ts +82 -0
  189. package/dist/server/chain-url-parts.d.ts.map +1 -0
  190. package/dist/server/children-interception.d.ts +49 -0
  191. package/dist/server/children-interception.d.ts.map +1 -0
  192. package/dist/server/default-status-page.d.ts +45 -0
  193. package/dist/server/default-status-page.d.ts.map +1 -0
  194. package/dist/server/deny-renderer.d.ts +14 -3
  195. package/dist/server/deny-renderer.d.ts.map +1 -1
  196. package/dist/server/head-response.d.ts +35 -0
  197. package/dist/server/head-response.d.ts.map +1 -0
  198. package/dist/server/index.js +2 -2
  199. package/dist/server/internal.js +393 -212
  200. package/dist/server/internal.js.map +1 -1
  201. package/dist/server/param-coercion.d.ts +10 -1
  202. package/dist/server/param-coercion.d.ts.map +1 -1
  203. package/dist/server/pipeline-helpers.d.ts +0 -18
  204. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  205. package/dist/server/pipeline-interception.d.ts +59 -9
  206. package/dist/server/pipeline-interception.d.ts.map +1 -1
  207. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  208. package/dist/server/pipeline-phases.d.ts.map +1 -1
  209. package/dist/server/pipeline.d.ts +11 -0
  210. package/dist/server/pipeline.d.ts.map +1 -1
  211. package/dist/server/publish-params.d.ts +33 -0
  212. package/dist/server/publish-params.d.ts.map +1 -0
  213. package/dist/server/request-context.d.ts +48 -0
  214. package/dist/server/request-context.d.ts.map +1 -1
  215. package/dist/server/route-element-builder.d.ts +32 -2
  216. package/dist/server/route-element-builder.d.ts.map +1 -1
  217. package/dist/server/route-handler.d.ts.map +1 -1
  218. package/dist/server/route-matcher.d.ts +13 -0
  219. package/dist/server/route-matcher.d.ts.map +1 -1
  220. package/dist/server/rsc-cache-key-guard.d.ts +78 -0
  221. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
  222. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  223. package/dist/server/rsc-entry/error-renderer.d.ts +14 -2
  224. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  225. package/dist/server/rsc-entry/helpers.d.ts +2 -10
  226. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  227. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  228. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  229. package/dist/server/rsc-entry/rsc-payload.d.ts +3 -2
  230. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  231. package/dist/server/rsc-entry/rsc-stream.d.ts +3 -2
  232. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  233. package/dist/server/rsc-entry/ssr-renderer.d.ts +2 -0
  234. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  235. package/dist/server/skippable-prefix.d.ts +23 -0
  236. package/dist/server/skippable-prefix.d.ts.map +1 -0
  237. package/dist/server/slot-interception.d.ts +80 -0
  238. package/dist/server/slot-interception.d.ts.map +1 -0
  239. package/dist/server/slot-resolver.d.ts +70 -8
  240. package/dist/server/slot-resolver.d.ts.map +1 -1
  241. package/dist/server/ssr-bridge-types.d.ts +6 -0
  242. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  243. package/dist/server/ssr-entry.d.ts.map +1 -1
  244. package/dist/server/ssr-wrappers.d.ts +24 -13
  245. package/dist/server/ssr-wrappers.d.ts.map +1 -1
  246. package/dist/server/state-tree-diff.d.ts +6 -27
  247. package/dist/server/state-tree-diff.d.ts.map +1 -1
  248. package/dist/server/static-generator.d.ts +6 -0
  249. package/dist/server/static-generator.d.ts.map +1 -1
  250. package/dist/server/static-not-found.d.ts +52 -0
  251. package/dist/server/static-not-found.d.ts.map +1 -0
  252. package/dist/shared/als-slots.d.ts +41 -0
  253. package/dist/shared/als-slots.d.ts.map +1 -0
  254. package/dist/shared/base-path.d.ts +56 -0
  255. package/dist/shared/base-path.d.ts.map +1 -0
  256. package/dist/shared/param-value.d.ts +80 -0
  257. package/dist/shared/param-value.d.ts.map +1 -0
  258. package/dist/shared/payload-root.d.ts +108 -0
  259. package/dist/shared/payload-root.d.ts.map +1 -0
  260. package/dist/shared/rsc-cache-key.d.ts +163 -0
  261. package/dist/shared/rsc-cache-key.d.ts.map +1 -0
  262. package/dist/shared/rsc-error-envelope.d.ts +25 -0
  263. package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
  264. package/dist/shared/rsc-manifest.d.ts +28 -0
  265. package/dist/shared/rsc-manifest.d.ts.map +1 -0
  266. package/dist/shared/rsc-payload-path.d.ts +42 -0
  267. package/dist/shared/rsc-payload-path.d.ts.map +1 -0
  268. package/dist/shared/segment-info.d.ts +59 -0
  269. package/dist/shared/segment-info.d.ts.map +1 -0
  270. package/dist/shared/slot-params.d.ts +51 -0
  271. package/dist/shared/slot-params.d.ts.map +1 -0
  272. package/dist/shared/static-platform-files.d.ts +71 -0
  273. package/dist/shared/static-platform-files.d.ts.map +1 -0
  274. package/docs/api/31-api-client.mdx +4 -2
  275. package/docs/learn/02-pages-and-layouts.mdx +1 -1
  276. package/docs/learn/11-error-handling.mdx +2 -0
  277. package/docs/learn/12-client-navigation.mdx +2 -2
  278. package/docs/learn/14-deploying.mdx +23 -0
  279. package/docs/more/01-advanced-routing.mdx +172 -2
  280. package/docs/more/04-metadata-and-fonts.mdx +1 -1
  281. package/package.json +2 -2
  282. package/src/adapters/build-output-helper.ts +116 -3
  283. package/src/adapters/cloudflare.ts +86 -11
  284. package/src/adapters/fs-identity.ts +156 -0
  285. package/src/adapters/nitro.ts +6 -6
  286. package/src/adapters/shared.ts +196 -21
  287. package/src/adapters/types.ts +13 -1
  288. package/src/cache/fast-hash.ts +5 -0
  289. package/src/client/browser-entry/action-dispatch.ts +9 -4
  290. package/src/client/browser-entry/hydrate.ts +40 -29
  291. package/src/client/browser-entry/index.ts +15 -1
  292. package/src/client/browser-entry/post-hydration.ts +10 -9
  293. package/src/client/browser-entry/router-init.ts +104 -48
  294. package/src/client/browser-entry/rsc-stream.ts +11 -2
  295. package/src/client/error-boundary.tsx +20 -15
  296. package/src/client/history.ts +13 -6
  297. package/src/client/internal.ts +4 -3
  298. package/src/client/link.tsx +55 -8
  299. package/src/client/navigation-api.ts +3 -3
  300. package/src/client/navigation-commit.ts +174 -0
  301. package/src/client/navigation-context.ts +40 -67
  302. package/src/client/navigation-root.tsx +326 -101
  303. package/src/client/params-context.ts +192 -0
  304. package/src/client/router-effects.ts +231 -0
  305. package/src/client/router-lifecycle.ts +288 -0
  306. package/src/client/router-pipeline.ts +389 -0
  307. package/src/client/router-ref.ts +1 -1
  308. package/src/client/router-skew.ts +30 -0
  309. package/src/client/router-types.ts +230 -0
  310. package/src/client/router.ts +242 -707
  311. package/src/client/rsc-fetch.ts +169 -102
  312. package/src/client/segment-cache.ts +121 -44
  313. package/src/client/ssr-data.ts +6 -0
  314. package/src/client/stale-client.ts +191 -0
  315. package/src/client/state.ts +14 -1
  316. package/src/client/top-loader.tsx +2 -1
  317. package/src/client/unload-guard.ts +34 -2
  318. package/src/client/use-cookie.ts +9 -2
  319. package/src/client/use-router.ts +1 -1
  320. package/src/client/use-segment-params.ts +41 -18
  321. package/src/index.ts +5 -1
  322. package/src/plugin-context.ts +32 -0
  323. package/src/plugins/adapter-build.ts +4 -0
  324. package/src/plugins/build-manifest.ts +34 -13
  325. package/src/plugins/entries.ts +4 -0
  326. package/src/plugins/fonts.ts +11 -8
  327. package/src/plugins/request-dep/analysis.ts +447 -0
  328. package/src/plugins/request-dep/index.ts +376 -0
  329. package/src/plugins/request-dep/origin.ts +310 -0
  330. package/src/plugins/routing.ts +59 -2
  331. package/src/plugins/static-build.ts +315 -73
  332. package/src/routing/codegen-types.ts +18 -0
  333. package/src/routing/codegen.ts +50 -10
  334. package/src/routing/collision-probe.ts +438 -0
  335. package/src/routing/collision-spaces.ts +281 -0
  336. package/src/routing/interception.ts +896 -50
  337. package/src/routing/manifest-codegen.ts +5 -3
  338. package/src/routing/scanner.ts +114 -89
  339. package/src/routing/segment-classify.ts +57 -0
  340. package/src/routing/segment-keys.ts +146 -0
  341. package/src/routing/slot-placement.ts +97 -0
  342. package/src/routing/walkers.ts +8 -0
  343. package/src/search-params/define.ts +6 -29
  344. package/src/segment-params/define.ts +4 -27
  345. package/src/server/action-handler.ts +3 -1
  346. package/src/server/actions.ts +6 -3
  347. package/src/server/als-registry.ts +2 -1
  348. package/src/server/build-manifest.ts +14 -1
  349. package/src/server/chain-url-parts.ts +163 -0
  350. package/src/server/children-interception.ts +115 -0
  351. package/src/server/default-status-page.ts +127 -0
  352. package/src/server/deny-renderer.ts +81 -20
  353. package/src/server/head-response.ts +54 -0
  354. package/src/server/param-coercion.ts +48 -18
  355. package/src/server/pipeline-helpers.ts +0 -45
  356. package/src/server/pipeline-interception.ts +81 -32
  357. package/src/server/pipeline-outcome.ts +2 -4
  358. package/src/server/pipeline-phases.ts +68 -16
  359. package/src/server/pipeline.ts +57 -10
  360. package/src/server/prebuilt-runtime.ts +4 -4
  361. package/src/server/publish-params.ts +42 -0
  362. package/src/server/request-context.ts +97 -18
  363. package/src/server/route-element-builder.ts +89 -98
  364. package/src/server/route-handler.ts +8 -8
  365. package/src/server/route-matcher.ts +21 -9
  366. package/src/server/rsc-cache-key-guard.ts +170 -0
  367. package/src/server/rsc-entry/api-handler.ts +6 -8
  368. package/src/server/rsc-entry/error-renderer.ts +64 -25
  369. package/src/server/rsc-entry/helpers.ts +31 -16
  370. package/src/server/rsc-entry/index.ts +36 -28
  371. package/src/server/rsc-entry/render-route.ts +9 -4
  372. package/src/server/rsc-entry/revalidate-renderer.ts +1 -1
  373. package/src/server/rsc-entry/rsc-payload.ts +14 -25
  374. package/src/server/rsc-entry/rsc-stream.ts +4 -3
  375. package/src/server/rsc-entry/ssr-renderer.ts +13 -9
  376. package/src/server/skippable-prefix.ts +92 -0
  377. package/src/server/slot-interception.ts +168 -0
  378. package/src/server/slot-resolver.ts +253 -156
  379. package/src/server/ssr-bridge-types.ts +6 -0
  380. package/src/server/ssr-entry.ts +12 -4
  381. package/src/server/ssr-wrappers.tsx +33 -32
  382. package/src/server/state-tree-diff.ts +14 -55
  383. package/src/server/static-generator.ts +65 -27
  384. package/src/server/static-not-found.ts +134 -0
  385. package/src/shared/als-slots.ts +72 -0
  386. package/src/shared/base-path.ts +80 -0
  387. package/src/shared/param-value.ts +244 -0
  388. package/src/shared/payload-root.ts +151 -0
  389. package/src/shared/rsc-cache-key.ts +269 -0
  390. package/src/shared/rsc-error-envelope.ts +31 -0
  391. package/src/shared/rsc-manifest.ts +29 -0
  392. package/src/shared/rsc-payload-path.ts +45 -0
  393. package/src/shared/segment-info.ts +60 -0
  394. package/src/shared/slot-params.ts +111 -0
  395. package/src/shared/static-platform-files.ts +75 -0
  396. package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
  397. package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
  398. package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
  399. package/dist/_chunks/cli-schema-sync-BTWEKJXo.js +0 -668
  400. package/dist/_chunks/cli-schema-sync-BTWEKJXo.js.map +0 -1
  401. package/dist/_chunks/define-COtkxMRT.js +0 -191
  402. package/dist/_chunks/define-COtkxMRT.js.map +0 -1
  403. package/dist/_chunks/define-c4au4I9R.js +0 -82
  404. package/dist/_chunks/define-c4au4I9R.js.map +0 -1
  405. package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
  406. package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
  407. package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
  408. package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
  409. package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
  410. package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
  411. package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
  412. package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
  413. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
  414. package/dist/_chunks/walkers-_6zKFlch.js.map +0 -1
  415. package/dist/client/error-boundary.js.map +0 -1
  416. package/dist/plugins/request-dep.d.ts +0 -32
  417. package/dist/plugins/request-dep.d.ts.map +0 -1
  418. package/src/plugins/request-dep.ts +0 -499
@@ -19,6 +19,9 @@ import { createElement, Fragment } from 'react';
19
19
  import { renderToReadableStream } from '../rsc-runtime/rsc.js';
20
20
 
21
21
  import { DenySignal } from './primitives.js';
22
+ import { appVisibleSearch } from '../shared/rsc-cache-key.js';
23
+ import { getSlotParamsForClient } from './request-context.js';
24
+ import { withPublishedParams } from './publish-params.js';
22
25
  import { renderDenyEntry, type DenyPageEntry } from './deny-boundary.js';
23
26
  import { getCookiesForSsr } from './cookie-context.js';
24
27
  import { logRenderError } from './logger.js';
@@ -28,6 +31,8 @@ import { isDebug } from './debug.js';
28
31
  import { resolveMetadata, renderMetadataToElements } from './metadata.js';
29
32
  import { headElementsToReact } from './metadata.js';
30
33
  import { resolveStatusFile } from './status-code-resolver.js';
34
+ import { defaultStatusPageResponse } from './default-status-page.js';
35
+ import { rscErrorEnvelope } from '../shared/rsc-error-envelope.js';
31
36
  import type { ManifestSegmentNode } from './route-matcher.js';
32
37
  import type { RouteMatch } from './pipeline.js';
33
38
  import type { NavContext } from './ssr-bridge-types.js';
@@ -88,12 +93,24 @@ function buildDenyElement(
88
93
  return renderDenyEntry(entry, deny.status, deny.data);
89
94
  }
90
95
 
96
+ /**
97
+ * Supplies a replacement for the framework default page at the end of the
98
+ * chain. Returning `null` keeps the framework default.
99
+ *
100
+ * The dev server uses this to substitute its route-listing 404 page. It is a
101
+ * parameter rather than a check on the produced Response because "the app
102
+ * authored no status page" is a fact about the *chain*, and inspecting the
103
+ * result for it (e.g. an empty body) stops being true the moment the chain
104
+ * grows a real terminal — which is exactly what TIM-1258 did.
105
+ */
106
+ export type DefaultPageRenderer = (status: number, responseHeaders: Headers) => Response | null;
107
+
91
108
  /**
92
109
  * Render a status-code error page for a DenySignal.
93
110
  *
94
111
  * Resolves the appropriate status-code file from the segment chain based
95
- * on format preference. Returns an HTML Response (component), JSON Response,
96
- * or bare fallback Response.
112
+ * on format preference. Returns an HTML Response (component), a JSON Response,
113
+ * or the framework default status page.
97
114
  */
98
115
  export async function renderDenyPage(
99
116
  deny: DenySignal,
@@ -104,7 +121,8 @@ export async function renderDenyPage(
104
121
  responseHeaders: Headers,
105
122
  clientBootstrap: ClientBootstrapConfig,
106
123
  createDebugChannelSink: DebugChannelFactory,
107
- callSsr: CallSsrFn
124
+ callSsr: CallSsrFn,
125
+ defaultPage?: DefaultPageRenderer
108
126
  ): Promise<Response> {
109
127
  // API routes (route.ts) → JSON only, never render components
110
128
  if (isApiRoute(segments)) {
@@ -116,11 +134,14 @@ export async function renderDenyPage(
116
134
  // Page routes → component chain first, JSON fallback only if no component found.
117
135
  const resolution = resolveStatusFile(deny.status, segments, 'component');
118
136
 
119
- // No component status file — try JSON chain before bare fallback
137
+ // No component status file — try JSON chain, then the framework default page.
120
138
  if (!resolution) {
121
139
  const jsonResponse = await renderDenyPageJson(deny, segments, responseHeaders);
122
140
  if (jsonResponse) return jsonResponse;
123
- return new Response(null, { status: deny.status, headers: responseHeaders });
141
+ return (
142
+ defaultPage?.(deny.status, responseHeaders) ??
143
+ defaultStatusPageResponse(deny.status, responseHeaders)
144
+ );
124
145
  }
125
146
 
126
147
  // Dev warning: JSON status file exists but is shadowed by the component chain.
@@ -137,10 +158,20 @@ export async function renderDenyPage(
137
158
  }
138
159
  }
139
160
 
140
- // Load the status-code page component
141
- const mod = await loadModule(resolution.file);
142
- if (!mod.default) {
143
- return new Response(null, { status: deny.status, headers: responseHeaders });
161
+ // Load the status-code page component. A file that resolved but exports no
162
+ // default — or whose module fails to import or evaluate — is not a usable
163
+ // component match: continue down the documented chain (JSON, then framework
164
+ // default) rather than returning an empty body or letting the loader's
165
+ // rejection escape request handling. Mirrors buildDenyPageChain and the
166
+ // error-file resolver, which both swallow the load failure.
167
+ const mod = await loadModule(resolution.file).catch(() => null);
168
+ if (!mod?.default) {
169
+ const jsonResponse = await renderDenyPageJson(deny, segments, responseHeaders);
170
+ if (jsonResponse) return jsonResponse;
171
+ return (
172
+ defaultPage?.(deny.status, responseHeaders) ??
173
+ defaultStatusPageResponse(deny.status, responseHeaders)
174
+ );
144
175
  }
145
176
 
146
177
  const ErrorPageComponent = mod.default as (...args: unknown[]) => unknown;
@@ -176,7 +207,7 @@ export async function renderDenyPage(
176
207
  }
177
208
 
178
209
  // Render the error page to a fresh RSC Flight stream.
179
- const rscStream = renderToReadableStream(element, {
210
+ const rscStream = renderToReadableStream(withPublishedParams(element), {
180
211
  signal: req.signal,
181
212
  onError(error: unknown) {
182
213
  logRenderError({ method: req.method, path: new URL(req.url).pathname, error });
@@ -186,10 +217,13 @@ export async function renderDenyPage(
186
217
 
187
218
  const [ssrStream, inlineStream] = teeWithErrorPropagation(rscStream);
188
219
 
220
+ const slotParams = getSlotParamsForClient();
221
+
189
222
  const navContext: NavContext = {
190
223
  pathname: new URL(req.url).pathname,
191
224
  segmentParams: match.segmentParams,
192
- searchParams: Object.fromEntries(new URL(req.url).searchParams),
225
+ searchParams: Object.fromEntries(appVisibleSearch(new URL(req.url)).params),
226
+ slotParams,
193
227
  statusCode: deny.status,
194
228
  responseHeaders,
195
229
  headHtml: flightInitScript(),
@@ -199,7 +233,32 @@ export async function renderDenyPage(
199
233
  cookies: getCookiesForSsr(),
200
234
  };
201
235
 
202
- return callSsr(ssrStream, navContext);
236
+ try {
237
+ // `await` inside the try on purpose — see the same note in
238
+ // rsc-entry/error-renderer.ts. A status page that crashes during SSR
239
+ // ends in the framework default page, not a blank body: the pipeline's
240
+ // own deny fallback catches this and can only produce an empty response,
241
+ // and a crashing 404.tsx is exactly when the reader has nothing else to
242
+ // go on (TIM-1258).
243
+ return await callSsr(ssrStream, navContext);
244
+ } catch (renderError) {
245
+ logRenderError({ method: req.method, path: new URL(req.url).pathname, error: renderError });
246
+ return defaultStatusPageResponse(deny.status, responseHeaders);
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Terminal fallback for a client-navigation deny with no status-code file.
252
+ *
253
+ * The HTML path answers this case with the framework default page, but a
254
+ * client-navigation response cannot carry HTML — the Flight decoder would
255
+ * choke on it. Emit the hard-navigate envelope instead: the client abandons
256
+ * SPA navigation and the resulting document request renders that same default
257
+ * page. Before TIM-1258 this returned an empty Flight body, which the decoder
258
+ * hit as a malformed stream.
259
+ */
260
+ function denyHardNavigateResponse(status: number, responseHeaders: Headers): Response {
261
+ return rscErrorEnvelope(status, responseHeaders);
203
262
  }
204
263
 
205
264
  /**
@@ -219,14 +278,12 @@ export async function renderDenyPageAsRsc(
219
278
  const resolution = resolveStatusFile(deny.status, segments, 'component');
220
279
 
221
280
  if (!resolution) {
222
- responseHeaders.set('content-type', `${RSC_CONTENT_TYPE}; charset=utf-8`);
223
- return new Response(null, { status: deny.status, headers: responseHeaders });
281
+ return denyHardNavigateResponse(deny.status, responseHeaders);
224
282
  }
225
283
 
226
- const mod = await loadModule(resolution.file);
227
- if (!mod.default) {
228
- responseHeaders.set('content-type', `${RSC_CONTENT_TYPE}; charset=utf-8`);
229
- return new Response(null, { status: deny.status, headers: responseHeaders });
284
+ const mod = await loadModule(resolution.file).catch(() => null);
285
+ if (!mod?.default) {
286
+ return denyHardNavigateResponse(deny.status, responseHeaders);
230
287
  }
231
288
 
232
289
  const ErrorPageComponent = mod.default as (...args: unknown[]) => unknown;
@@ -247,7 +304,7 @@ export async function renderDenyPageAsRsc(
247
304
  element = h(component, null, element);
248
305
  }
249
306
 
250
- const rscStream = renderToReadableStream(element, {
307
+ const rscStream = renderToReadableStream(withPublishedParams(element), {
251
308
  signal: req.signal,
252
309
  onError(error: unknown) {
253
310
  console.error('[timber] Error page RSC render error:', error);
@@ -286,7 +343,11 @@ async function renderDenyPageJson(
286
343
  // JSON status files are loaded as modules that export the JSON content.
287
344
  // The manifest's load() imports the .json file, which Vite handles as a
288
345
  // default export of the parsed JSON object.
289
- const mod = await loadModule(resolution.file);
346
+ // A .json status file that fails to load is not a usable match — return
347
+ // null so the caller continues to the framework default rather than
348
+ // rejecting out of the chain.
349
+ const mod = await loadModule(resolution.file).catch(() => null);
350
+ if (!mod) return null;
290
351
  const jsonContent = mod.default ?? mod;
291
352
 
292
353
  responseHeaders.set('content-type', 'application/json; charset=utf-8');
@@ -0,0 +1,54 @@
1
+ /**
2
+ * HEAD body stripping — RFC 9110 §9.3.2.
3
+ *
4
+ * "The HEAD method is identical to GET except that the server MUST NOT send
5
+ * content in the response." The headers, however, must be the ones the
6
+ * corresponding GET would have produced — which is why timber renders the
7
+ * route in full and discards the body here rather than short-circuiting the
8
+ * render. Content-Type, Content-Length, Set-Cookie, cache directives and
9
+ * everything else ride through untouched.
10
+ *
11
+ * Node's `ServerResponse` happens to discard writes for HEAD, so the wire is
12
+ * usually correct even without this. That is a property of one adapter, not of
13
+ * the `Response` object: static generation, the Cloudflare and Nitro adapters,
14
+ * tests, and anything calling the pipeline handler as a function all observe
15
+ * the `Response` directly and saw the full body before TIM-1324.
16
+ *
17
+ * The pipeline applies this at its single exit so every response shape is
18
+ * covered — page render, API route, metadata route, deny page, error page,
19
+ * proxy short-circuit. The two unit-level producers that are also reachable as
20
+ * standalone functions (`handleRouteRequest`, `handleApiRoute`) call it too, so
21
+ * each honours HEAD on its own; they share this implementation rather than
22
+ * repeating it, and applying it twice is a no-op.
23
+ */
24
+
25
+ /**
26
+ * Return a bodyless copy of `response` when `method` is HEAD.
27
+ *
28
+ * A no-op for every other method and for responses that already carry no body
29
+ * (204, 304, redirects), so callers do not need to pre-check either.
30
+ *
31
+ * The discarded stream is cancelled: an SSR render that reached this point has
32
+ * a live producer attached, and dropping the reference without cancelling
33
+ * leaks it for the lifetime of the render (design/13-security.md A4).
34
+ */
35
+ export function stripHeadBody(response: Response, method: string): Response {
36
+ if (method.toUpperCase() !== 'HEAD') return response;
37
+ const body = response.body;
38
+ if (body === null) return response;
39
+
40
+ // `cancel()` rejects if the stream is already errored and throws
41
+ // synchronously if it is locked — neither is actionable here, the body is
42
+ // being discarded either way.
43
+ try {
44
+ body.cancel().catch(() => {});
45
+ } catch {
46
+ /* stream already locked or disturbed */
47
+ }
48
+
49
+ return new Response(null, {
50
+ status: response.status,
51
+ statusText: response.statusText,
52
+ headers: response.headers,
53
+ });
54
+ }
@@ -13,9 +13,11 @@
13
13
  */
14
14
 
15
15
  import type { Codec } from '../codec.js';
16
+ import type { SegmentType } from '../routing/types.js';
17
+ import { effectiveUrlSegment } from '../routing/segment-classify.js';
16
18
  import { toBracketKey } from '../params/resolve-schema.js';
17
19
  import type { RouteMatch } from './pipeline.js';
18
- import { sanitizeParamValue } from './pipeline-helpers.js';
20
+ import { normalizeParamValue } from '../shared/param-value.js';
19
21
  import { loadModule } from './safe-load.js';
20
22
  import { ParamCoercionError } from './route-element-builder.js';
21
23
  import { isDebug } from './debug.js';
@@ -44,6 +46,12 @@ export function setGlobalSchemaCodecs(codecs: Record<string, Codec<unknown>> | n
44
46
  * exists, the raw value is parsed through it. If no codec exists or no
45
47
  * global codecs are registered, the raw value passes through unchanged.
46
48
  *
49
+ * Segments are read through `effectiveUrlSegment` because an intercepting
50
+ * node's own `segmentType` is `'intercepting'` and its `paramName` is unset
51
+ * — the param it binds lives in the intercepted name (`(.)[id]` → `[id]`).
52
+ * The slot resolver matches through the same helper, so the key a param is
53
+ * coerced under is always the key it was matched under (TIM-1281).
54
+ *
47
55
  * Unlike main route coercion, this does NOT throw ParamCoercionError on
48
56
  * failure — slots degrade gracefully. A failed coercion logs a warning
49
57
  * in dev mode and keeps the raw value.
@@ -51,7 +59,12 @@ export function setGlobalSchemaCodecs(codecs: Record<string, Codec<unknown>> | n
51
59
  * @internal — framework use only
52
60
  */
53
61
  export function coerceSlotParams(
54
- slotChain: Array<{ segmentType: string; paramName?: string }>,
62
+ slotChain: Array<{
63
+ segmentName: string;
64
+ segmentType: SegmentType;
65
+ paramName?: string;
66
+ interceptedSegmentName?: string;
67
+ }>,
55
68
  rawParams: Record<string, string | string[]>
56
69
  ): Record<string, string | string[]> {
57
70
  const globalCodecs = _globalCodecs;
@@ -62,7 +75,8 @@ export function coerceSlotParams(
62
75
  result[key] = rawParams[key];
63
76
  }
64
77
 
65
- for (const segment of slotChain) {
78
+ for (const node of slotChain) {
79
+ const segment = effectiveUrlSegment(node);
66
80
  if (!segment.paramName) continue;
67
81
  const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
68
82
  const codec = globalCodecs[bracketKey];
@@ -71,10 +85,9 @@ export function coerceSlotParams(
71
85
  const key = segment.paramName;
72
86
  if (!(key in result)) continue;
73
87
 
88
+ let parsed: unknown;
74
89
  try {
75
- result[key] = sanitizeParamValue(codec.parse(result[key] as string | string[])) as
76
- | string
77
- | string[];
90
+ parsed = codec.parse(result[key] as string | string[]);
78
91
  } catch (err) {
79
92
  // Slot param coercion failures are non-fatal — keep the raw value.
80
93
  // The main route already validated the URL; the slot just interprets
@@ -88,7 +101,9 @@ export function coerceSlotParams(
88
101
  ` Keeping raw value.`
89
102
  );
90
103
  }
104
+ continue;
91
105
  }
106
+ result[key] = normalizeParamValue(parsed, key) as string | string[];
92
107
  }
93
108
 
94
109
  return result as Record<string, string | string[]>;
@@ -125,7 +140,17 @@ export async function coerceSegmentParams(match: RouteMatch): Promise<void> {
125
140
  // by bracket name (e.g. '[id]', '[...slug]') to avoid collisions between
126
141
  // distinct segment types that share a param name.
127
142
  if (globalCodecs) {
128
- for (const segment of match.segments) {
143
+ for (const node of match.segments) {
144
+ // Read through `effectiveUrlSegment` for the same reason
145
+ // `coerceSlotParams` does: an intercepting node's own `segmentType` is
146
+ // 'intercepting' and its `paramName` is unset — the param it binds lives
147
+ // in the intercepted name (`(.)[id]` → `[id]`). Until TIM-1280 an
148
+ // intercepting node could only appear in a *slot* chain, so only that
149
+ // path needed it; a children-path interception puts one in the main
150
+ // chain, and reading the node directly would silently skip coercion for
151
+ // exactly the param the intercepting directory declares — so the swapped
152
+ // route and the canonical one would disagree about the param's type.
153
+ const segment = effectiveUrlSegment(node);
129
154
  if (!segment.paramName) continue;
130
155
  const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
131
156
  const codec = globalCodecs[bracketKey];
@@ -133,8 +158,9 @@ export async function coerceSegmentParams(match: RouteMatch): Promise<void> {
133
158
 
134
159
  const key = segment.paramName;
135
160
  if (!(key in mergeTarget)) continue;
161
+ let parsed: unknown;
136
162
  try {
137
- mergeTarget[key] = sanitizeParamValue(codec.parse(mergeTarget[key] as string | string[]));
163
+ parsed = codec.parse(mergeTarget[key] as string | string[]);
138
164
  } catch (err) {
139
165
  const message = err instanceof Error ? err.message : String(err);
140
166
  if (isDebug()) {
@@ -147,6 +173,10 @@ export async function coerceSegmentParams(match: RouteMatch): Promise<void> {
147
173
  }
148
174
  throw new ParamCoercionError(message);
149
175
  }
176
+ // Outside the catch: a codec that rejects a URL value is a 404, a codec
177
+ // that returns an unrepresentable one is a bug in the app. Normalizing
178
+ // inside would report the second as the first.
179
+ mergeTarget[key] = normalizeParamValue(parsed, key);
150
180
  }
151
181
  return;
152
182
  }
@@ -177,17 +207,9 @@ export async function coerceSegmentParams(match: RouteMatch): Promise<void> {
177
207
 
178
208
  if (!segmentParamsDef || typeof segmentParamsDef.parse !== 'function') continue;
179
209
 
210
+ let coerced: Record<string, unknown>;
180
211
  try {
181
- const coerced = segmentParamsDef.parse(match.segmentParams);
182
-
183
- // Deep-sanitize codec output: every nested plain object becomes
184
- // null-prototype with dangerous keys stripped at every depth.
185
- // See TIM-873, design/13-security.md
186
- for (const key of Object.keys(coerced as Record<string, unknown>)) {
187
- if (key !== '__proto__') {
188
- mergeTarget[key] = sanitizeParamValue((coerced as Record<string, unknown>)[key]);
189
- }
190
- }
212
+ coerced = segmentParamsDef.parse(match.segmentParams) as Record<string, unknown>;
191
213
  } catch (err) {
192
214
  const message = err instanceof Error ? err.message : String(err);
193
215
  if (isDebug()) {
@@ -203,5 +225,13 @@ export async function coerceSegmentParams(match: RouteMatch): Promise<void> {
203
225
  }
204
226
  throw new ParamCoercionError(message);
205
227
  }
228
+
229
+ // Normalize outside the catch, for the same reason as above. `__proto__`
230
+ // is skipped here as well as inside `normalizeParamValue`, because this
231
+ // loop assigns into `mergeTarget` directly.
232
+ for (const key of Object.keys(coerced)) {
233
+ if (key === '__proto__') continue;
234
+ mergeTarget[key] = normalizeParamValue(coerced[key], key);
235
+ }
206
236
  }
207
237
  }
@@ -18,51 +18,6 @@ import type { ProxyConfig } from './pipeline.js';
18
18
 
19
19
  // ─── Prototype-Pollution-Safe Sanitizer ────────────────────────────────────
20
20
 
21
- /**
22
- * Only __proto__ needs stripping — it has a language-level setter that
23
- * changes the prototype chain of spread copies. constructor and prototype
24
- * are harmless own properties on null-prototype objects.
25
- */
26
- const DANGEROUS_KEYS = new Set(['__proto__']);
27
-
28
- /**
29
- * Deep-walk a value returned by a segment param codec, producing a
30
- * sanitized copy where every plain object is null-prototype and
31
- * dangerous keys (__proto__, constructor, prototype) are stripped at
32
- * every depth.
33
- *
34
- * Non-plain objects (Date, Map, class instances, etc.) are returned
35
- * as-is — they cannot be poisoned by `{...x}` spread and may carry
36
- * author-intended prototype methods.
37
- *
38
- * Arrays are walked element-wise.
39
- *
40
- * Performance: URL params are bounded by URL length (~8 KB). Realistic
41
- * trees are <1 KB. The recursive walk is sub-microsecond.
42
- *
43
- * See TIM-655, TIM-855, TIM-873, design/13-security.md
44
- */
45
- export function sanitizeParamValue(value: unknown): unknown {
46
- if (value === null || typeof value !== 'object') return value;
47
-
48
- if (Array.isArray(value)) {
49
- return value.map(sanitizeParamValue);
50
- }
51
-
52
- // Only walk plain objects — anything with a custom prototype (Date, Map,
53
- // class instances) is left untouched.
54
- const proto = Object.getPrototypeOf(value);
55
- if (proto !== Object.prototype && proto !== null) return value;
56
-
57
- const out: Record<string, unknown> = Object.create(null);
58
- for (const key of Object.keys(value as Record<string, unknown>)) {
59
- if (!DANGEROUS_KEYS.has(key)) {
60
- out[key] = sanitizeParamValue((value as Record<string, unknown>)[key]);
61
- }
62
- }
63
- return out;
64
- }
65
-
66
21
  // ─── Proxy Resolver ────────────────────────────────────────────────────────
67
22
 
68
23
  /**
@@ -10,47 +10,96 @@
10
10
  */
11
11
 
12
12
  import { classifyUrlSegment } from '../routing/segment-classify.js';
13
-
14
- /** Result of a successful interception match. */
15
- export interface InterceptionMatchResult {
16
- /** The pathname to re-match (the source/intercepting route's parent). */
17
- sourcePathname: string;
18
- }
19
-
20
- /**
21
- * Check if a pathname starts with a prefix on a segment boundary.
22
- *
23
- * Prevents /feed from matching /feed-private — the prefix must be
24
- * followed by '/' or be an exact match.
25
- */
26
- function hasSegmentPrefix(pathname: string, prefix: string): boolean {
27
- if (prefix === '/') return true;
28
- if (!pathname.startsWith(prefix)) return false;
29
- return pathname.length === prefix.length || pathname[prefix.length] === '/';
30
- }
13
+ import type { InterceptionRewrite } from '../routing/interception.js';
31
14
 
32
15
  /**
33
16
  * Check if an intercepting route applies for this soft navigation.
34
17
  *
35
- * Matches the target pathname against interception rewrites, constrained
36
- * by the source URL (X-Timber-URL header — where the user navigates FROM).
18
+ * Two conditions must hold:
19
+ *
20
+ * 1. The **target** pathname matches the rewrite's intercepted pattern.
21
+ * 2. The **source** route — the page the user is navigating FROM, per the
22
+ * validated X-Timber-URL header — renders the segment that owns the slot
23
+ * holding the intercepting page (`interceptingScope`). Only then does that
24
+ * slot exist in the rendered tree, so only then can the intercepting page
25
+ * render.
37
26
  *
38
- * Returns the source pathname to re-match if interception applies, or null.
27
+ * Condition 2 replaced a URL-prefix check (TIM-1279). A prefix cannot
28
+ * distinguish a route-group-scoped interception from a genuinely root-level
29
+ * one — route groups are invisible in `urlPath`, so both collapse to "/" and
30
+ * the guard degenerates to always-true. Tree paths carry group identity.
31
+ * The scope check also subsumes the old segment-boundary rule: a source route
32
+ * under /feed-private has no "/feed" in its chain.
33
+ *
34
+ * A third condition lives with the caller, which owns route matching, and it
35
+ * applies to the `'slot'` shape only: the route produced by re-matching
36
+ * `interceptingPrefix` must pass through the scope too. Conditions 1–2 are
37
+ * about the route the user is leaving; the slot has to exist in the tree that
38
+ * actually renders. The `'children'` shape resolves its owner from the scope
39
+ * directly rather than from a pathname, so it satisfies that by construction.
40
+ *
41
+ * `resolveSourceScopes` is a thunk so its inputs are only resolved when some
42
+ * rewrite's pattern actually matches the target — the common case is no match
43
+ * at all. It returns the two client-supplied answers to "what is on screen?"
44
+ * as **separate sets, most authoritative first**: the tree paths the client
45
+ * says it has mounted, then those of the route the departing URL matches. It
46
+ * returns null only when neither yields anything, which is not a soft
47
+ * navigation this can reason about.
48
+ *
49
+ * They are two sets rather than one union because ranking needs a chain
50
+ * (TIM-1318). Each channel describes a single rendered chain, so within one the
51
+ * scopes are strictly nested and the deepest is the innermost owner. Unioned
52
+ * they are not: once a modal is open the address bar holds the *intercepted*
53
+ * URL, so the second channel describes the canonical branch — possibly an
54
+ * exclusive route group the client never mounted — and a deeper scope from
55
+ * there would outrank the owner actually on screen. Trying the mounted channel
56
+ * to exhaustion first keeps the fallback the union was there to provide while
57
+ * ranking only within one chain.
58
+ *
59
+ * Returns the winning build-time rewrite, or null. Only build-time values ever
60
+ * reach the renderer: the client-supplied header selects a rewrite but never
61
+ * becomes a rendered route or pathname.
62
+ *
63
+ * The **first** matching rewrite wins, and the caller does not fall back to the
64
+ * next one when the winner fails to resolve. That is deliberate and pre-dates
65
+ * the two shapes: a rewrite that matched the target and the scope described the
66
+ * author's intent, and quietly rendering some other author's interception
67
+ * instead would be harder to explain than rendering the canonical route.
68
+ *
69
+ * "First" is meaningful because the list arrives **deepest scope first** —
70
+ * `collectInterceptionRewrites` sorts it at build time, so the deepest owner a
71
+ * single chain passes through wins and nothing here has to score (TIM-1318).
72
+ * Tree-walk order used to decide it, which meant renaming a route group flipped
73
+ * which modal opened. The precondition is a property of the manifest this reads
74
+ * rather than something it can re-derive, so it is asserted at its source:
75
+ * `tests/interception.test.ts` §"rewrite order is decided by scope depth".
39
76
  */
40
77
  export function findInterceptionMatch(
41
78
  targetPathname: string,
42
- sourceUrl: string,
43
- rewrites: import('../routing/interception.js').InterceptionRewrite[]
44
- ): InterceptionMatchResult | null {
45
- for (const rewrite of rewrites) {
46
- // Check if the source URL starts with the intercepting prefix,
47
- // enforcing segment boundary to prevent /feed matching /feed-private.
48
- if (!hasSegmentPrefix(sourceUrl, rewrite.interceptingPrefix)) continue;
79
+ resolveSourceScopes: () => ReadonlySet<string>[] | null,
80
+ rewrites: InterceptionRewrite[]
81
+ ): InterceptionRewrite | null {
82
+ // Dynamic segments in the pattern match any single URL segment. Collected up
83
+ // front rather than inside the channel loop so a pattern is matched once
84
+ // however many channels are tried — and the common case, no rewrite matching
85
+ // at all, costs exactly the same scan it always did and resolves no scope.
86
+ const candidates = rewrites.filter((rewrite) =>
87
+ pathnameMatchesPattern(targetPathname, rewrite.interceptedPattern)
88
+ );
89
+ if (candidates.length === 0) return null;
90
+
91
+ // Nothing at all is known about the source tree — no scope can contain
92
+ // anything, so no rewrite can apply.
93
+ const scopes = resolveSourceScopes();
94
+ if (!scopes) return null;
49
95
 
50
- // Check if the target URL matches the intercepted pattern.
51
- // Dynamic segments in the pattern match any single URL segment.
52
- if (pathnameMatchesPattern(targetPathname, rewrite.interceptedPattern)) {
53
- return { sourcePathname: rewrite.interceptingPrefix };
96
+ // `candidates` is deepest scope first (`collectInterceptionRewrites`), so
97
+ // within one channel the first hit is the innermost owner that channel
98
+ // reports. Channels are tried in order, so a scope the client says it has
99
+ // mounted always beats one only the address bar implies.
100
+ for (const scope of scopes) {
101
+ for (const rewrite of candidates) {
102
+ if (scope.has(rewrite.interceptingScope)) return rewrite;
54
103
  }
55
104
  }
56
105
  return null;
@@ -26,16 +26,14 @@ import {
26
26
  import { markResponseFlushed } from './request-context.js';
27
27
  import { RedirectSignal, DenySignal } from './primitives.js';
28
28
  import { isDebug } from './debug.js';
29
+ import { rscErrorEnvelope } from '../shared/rsc-error-envelope.js';
29
30
  import type { PipelineConfig, RouteMatch } from './pipeline.js';
30
31
 
31
32
  // ─── Helpers ───────────────────────────────────────────────────────────────
32
33
 
33
34
  function rscErrorResponse(isRsc: boolean, status: number, headers?: Headers): Response {
34
35
  if (!isRsc) return new Response(null, { status });
35
- const h = headers ?? new Headers();
36
- h.set('X-Timber-Error', '1');
37
- h.set('content-type', 'application/json; charset=utf-8');
38
- return new Response(JSON.stringify({ error: true, status }), { status, headers: h });
36
+ return rscErrorEnvelope(status, headers);
39
37
  }
40
38
 
41
39
  // ─── Phase Outcome ─────────────────────────────────────────────────────────