@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
@@ -0,0 +1,244 @@
1
+ /**
2
+ * The canonical form of a segment param value.
3
+ *
4
+ * `getSegmentParams()` on the server and `useSegmentParams()` on the client
5
+ * must return the same value — same prototype, same own properties. This
6
+ * module is how that is guaranteed, and the mechanism matters:
7
+ *
8
+ * **Both sides are images of one normalizer.** A codec's output is deep-cloned
9
+ * into a closed canonical domain at coercion time, and *that clone* is what the
10
+ * server returns and what goes on the wire. Server and client agree by
11
+ * construction, because neither holds the codec's original.
12
+ *
13
+ * That replaces predicting what React Flight would do to an arbitrary value.
14
+ * Prediction is what the previous design attempted and it cannot be finished:
15
+ * the question "will Flight's decode output be observably identical to this
16
+ * input?" ranges over every way a JS object can carry state that is not in its
17
+ * serialization — own keys, enumerability, symbols, holes, accessors,
18
+ * subclassing, exotic objects — and a `Proxy` defeats it outright, since a
19
+ * lying `getPrototypeOf` trap makes any static check unsound. Twelve review
20
+ * findings on PR #992 were each one more case of that set, and the set does
21
+ * not close.
22
+ *
23
+ * Normalizing closes it. The only remaining question is whether Flight
24
+ * round-trips the normalizer's *output range* — a finite domain we defined,
25
+ * covered by `tests/e2e/segment-params.test.ts` against the real serializer.
26
+ *
27
+ * ## The domain
28
+ *
29
+ * - `undefined`, `null`, `string`, `number`, `boolean`, `bigint`
30
+ * - null-prototype objects, own enumerable string keys only
31
+ * - dense arrays
32
+ * - `Date`, `Map`, `Set` — rebuilt, never the codec's instance
33
+ *
34
+ * ...and it is a **tree**: no value appears twice, and nothing is cyclic.
35
+ * That is not a simplification, it is the last prediction being removed.
36
+ * Flight encodes a `Date` by value at each occurrence but tracks plain objects
37
+ * for reference dedup, so preserving aliasing would carry it across for some
38
+ * types and drop it for others — and knowing which is exactly the kind of
39
+ * guess this design exists to stop making (codex, PR #992). A tree has no
40
+ * sharing to preserve or lose, so what Flight does about it is unobservable.
41
+ *
42
+ * Anything else throws, naming the param and what it was. Note what is *not*
43
+ * a rejection: a `Date` subclass normalizes to a `Date`, a decorated array to
44
+ * a plain one, a sparse array to a dense one, a getter to the value it
45
+ * returned. Those used to be twelve separate checks. They are not divergences
46
+ * any more, because the normalization happens once and both sides see its
47
+ * result — so they need no checks at all.
48
+ *
49
+ * Own properties that Flight would drop (non-enumerable, symbol-keyed) are
50
+ * dropped here instead, symmetrically. Prototype pollution is handled by the
51
+ * same pass: `__proto__` is never copied, and every object is
52
+ * `Object.create(null)` (design/13-security.md #36b/#36c, TIM-873).
53
+ *
54
+ * ## The prototype flip
55
+ *
56
+ * Flight refuses to serialize a null-prototype object, so the canonical form
57
+ * cannot go on the wire as-is. `toPlainRecord` and `toNullProtoRecord` flip
58
+ * the prototype on the way out and back. They are exact inverses and both
59
+ * total, because they only ever see canonical values — there is nothing to
60
+ * validate and no shape to special-case.
61
+ *
62
+ * See design/41-global-params.md §"Transport".
63
+ */
64
+
65
+ /** Types the canonical form rebuilds rather than rejects. */
66
+ const REBUILT = ['Date', 'Map', 'Set'] as const;
67
+
68
+ /** The domain, for error messages. Derived, never a second hand-written list. */
69
+ const DOMAIN = `a string, number, boolean, bigint, null, undefined, a plain object, an array, or ${REBUILT.join('/')}`;
70
+
71
+ function reject(path: string, was: string, hint: string): never {
72
+ throw new Error(
73
+ `[timber] Segment param "${path || '(root)'}" was coerced to ${was}, which cannot ` +
74
+ `cross to the client. A param value may be ${DOMAIN}.\n` +
75
+ ` ${hint}\n` +
76
+ ` See design/41-global-params.md §"Transport".`
77
+ );
78
+ }
79
+
80
+ function describe(value: object): string {
81
+ return (value as { constructor?: { name?: string } }).constructor?.name ?? 'an object';
82
+ }
83
+
84
+ /**
85
+ * Deep-clone a codec's output into the canonical form, or throw.
86
+ *
87
+ * `seen` accumulates every object the walk has entered, and a second encounter
88
+ * is rejected. That is one rule for two things — a shared reference and a cycle
89
+ * are both "this object again" — and it is what keeps the result a tree.
90
+ */
91
+ export function normalizeParamValue(value: unknown, path = ''): unknown {
92
+ return normalize(value, path, new Set());
93
+ }
94
+
95
+ function normalize(value: unknown, path: string, seen: Set<object>): unknown {
96
+ if (value === null) return null;
97
+
98
+ const kind = typeof value;
99
+ if (kind === 'string' || kind === 'number' || kind === 'boolean' || kind === 'bigint') {
100
+ return value;
101
+ }
102
+ if (kind === 'undefined') return undefined;
103
+ if (kind === 'function') {
104
+ reject(path, 'a function', 'Return the data it would produce, not the function.');
105
+ }
106
+ if (kind === 'symbol') {
107
+ reject(path, 'a symbol', 'Return a string instead — a symbol has no wire representation.');
108
+ }
109
+
110
+ const object = value as object;
111
+ if (seen.has(object)) {
112
+ throw new Error(
113
+ `[timber] Segment param "${path || '(root)'}" is a value that already appears ` +
114
+ `elsewhere in the same param — a shared reference or a cycle. A param value is a ` +
115
+ `tree: React Flight carries a repeated \`Date\` by value and a repeated object by ` +
116
+ `reference, so sharing would survive for some types and not others.\n` +
117
+ ` Return separate values, or move the shared part outside the params.\n` +
118
+ ` See design/41-global-params.md §"Transport".`
119
+ );
120
+ }
121
+ seen.add(object);
122
+
123
+ if (Array.isArray(object)) {
124
+ // Read by index, so holes become `undefined` — on both sides, which is
125
+ // the point. A subclass and any own properties are left behind for the
126
+ // same reason: the clone is a plain, dense array either way.
127
+ const out: unknown[] = [];
128
+ for (let index = 0; index < object.length; index++) {
129
+ out.push(normalize((object as unknown[])[index], `${path}[${index}]`, seen));
130
+ }
131
+ return out;
132
+ }
133
+
134
+ if (object instanceof Date) {
135
+ // A fresh Date, so a subclass or an attached property cannot travel half
136
+ // way and be dropped by the wire.
137
+ return new Date(object.getTime());
138
+ }
139
+
140
+ if (object instanceof Map) {
141
+ const out = new Map<unknown, unknown>();
142
+ for (const [key, entry] of object) {
143
+ out.set(
144
+ normalize(key, `${path}<key>`, seen),
145
+ normalize(entry, `${path}.get(${keyLabel(key)})`, seen)
146
+ );
147
+ }
148
+ return out;
149
+ }
150
+
151
+ if (object instanceof Set) {
152
+ const out = new Set<unknown>();
153
+ let index = 0;
154
+ for (const entry of object) out.add(normalize(entry, `${path}[${index++}]`, seen));
155
+ return out;
156
+ }
157
+
158
+ // Everything else is judged by its prototype. A `Proxy` can lie here, and
159
+ // that is fine: whatever it answers, the clone below reads through it once
160
+ // and both sides see the same snapshot.
161
+ const prototype = Object.getPrototypeOf(object);
162
+ if (prototype !== null && prototype !== Object.prototype) {
163
+ reject(
164
+ path,
165
+ `a ${describe(object)} instance`,
166
+ 'Return a plain object with the fields you need.'
167
+ );
168
+ }
169
+
170
+ // Own *enumerable string* keys only — the rest is what Flight would drop,
171
+ // so dropping it here keeps the two sides identical. `__proto__` is skipped
172
+ // rather than copied: it has a language-level setter that would change the
173
+ // prototype chain of the copy (TIM-655, TIM-855, TIM-873).
174
+ const out: Record<string, unknown> = Object.create(null);
175
+ for (const key of Object.keys(object)) {
176
+ if (key === '__proto__') continue;
177
+ out[key] = normalize(
178
+ (object as Record<string, unknown>)[key],
179
+ path ? `${path}.${key}` : key,
180
+ seen
181
+ );
182
+ }
183
+ return out;
184
+ }
185
+
186
+ function keyLabel(key: unknown): string {
187
+ return typeof key === 'string' ? JSON.stringify(key) : String(key);
188
+ }
189
+
190
+ // ─── Prototype flip ──────────────────────────────────────────────
191
+
192
+ /**
193
+ * Canonical form → wire form. React Flight rejects a null prototype
194
+ * ("Classes or null prototypes are not supported"), so objects cross as plain
195
+ * ones and are restored on arrival.
196
+ */
197
+ export function toPlainRecord<T>(value: T): T {
198
+ return reproto(value, {}) as T;
199
+ }
200
+
201
+ /** Wire form → canonical form. The exact inverse of `toPlainRecord`. */
202
+ export function toNullProtoRecord<T>(value: T): T {
203
+ return reproto(value, null) as T;
204
+ }
205
+
206
+ /**
207
+ * Rebuild a canonical value with objects on `proto`.
208
+ *
209
+ * Total by construction: its input is always canonical, so the cases are the
210
+ * domain and nothing else. No validation, no shape it might not have seen, and
211
+ * no cycle to guard — a canonical value is a tree. That is what moving the
212
+ * checking to `normalizeParamValue` bought.
213
+ */
214
+ function reproto(value: unknown, proto: object | null): unknown {
215
+ if (value === null || typeof value !== 'object') return value;
216
+
217
+ const object = value as object;
218
+
219
+ // A Date carries no nested values, so it crosses as itself.
220
+ if (object instanceof Date) return object;
221
+
222
+ if (Array.isArray(object)) {
223
+ return (object as unknown[]).map((item) => reproto(item, proto));
224
+ }
225
+
226
+ if (object instanceof Map) {
227
+ const out = new Map<unknown, unknown>();
228
+ for (const [key, entry] of object) out.set(reproto(key, proto), reproto(entry, proto));
229
+ return out;
230
+ }
231
+
232
+ if (object instanceof Set) {
233
+ const out = new Set<unknown>();
234
+ for (const entry of object) out.add(reproto(entry, proto));
235
+ return out;
236
+ }
237
+
238
+ const out: Record<string, unknown> = proto === null ? Object.create(null) : {};
239
+ for (const key of Object.keys(object)) {
240
+ if (key === '__proto__') continue;
241
+ out[key] = reproto((object as Record<string, unknown>)[key], proto);
242
+ }
243
+ return out;
244
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * The shape of an RSC payload's root row.
3
+ *
4
+ * Every tree the server sends the browser is serialized as
5
+ * `{ tree, params, slotParams }` — the route's element tree, plus the coerced
6
+ * params it rendered with, as *siblings* rather than as a wrapper around the
7
+ * tree. See design/41-global-params.md §"Transport".
8
+ *
9
+ * Two reasons it is a sibling field rather than a provider inside the payload
10
+ * (TIM-1297, which is TIM-1294's mechanism corrected):
11
+ *
12
+ * 1. **A provider inside the payload cannot serve a partial navigation.** The
13
+ * client keeps the previous tree and splices the new payload in at the
14
+ * first updated `SegmentOutlet`, so the payload's own provider lands
15
+ * *below* the retained region — and the retained region's root is the
16
+ * *departing* route's provider, which therefore wins for every reader in a
17
+ * skipped layout. Wrapping something above it does not help: context
18
+ * resolves to the nearest provider, and the departing one is nearer.
19
+ * Params belong to the *navigation*, so the provider has to be owned by the
20
+ * client, above the merge point, and there must be exactly one.
21
+ *
22
+ * 2. **Element props are not readable at the instant the root resolves.**
23
+ * Flight outlines a React element's props and fills the record in after the
24
+ * root row resolves, so a reader that unblocks on the root sees `{}` — and
25
+ * since the object is filled *in place*, its identity never changes and any
26
+ * memo keyed on it caches the empty snapshot permanently. A plain model
27
+ * field has no such window: the root chunk stays blocked until everything
28
+ * it references has resolved.
29
+ *
30
+ * This module is the single declaration of that shape. The server builds roots
31
+ * through `server/publish-params.ts` and the client reads them through the
32
+ * helpers here — producer and consumer do not each describe the payload.
33
+ */
34
+
35
+ import type { SlotParamsRecord } from './slot-params.js';
36
+
37
+ /** The params published beside a tree. */
38
+ export interface PublishedParams {
39
+ /** The main route's coerced params. */
40
+ params: Record<string, string | string[]>;
41
+ /** Per-slot params, or null when the route rendered no slots with params. */
42
+ slotParams: SlotParamsRecord | null;
43
+ }
44
+
45
+ /**
46
+ * Params ready to publish: settled, or a promise the root provider suspends on.
47
+ *
48
+ * Both forms exist on purpose. The hydration path cannot await — `hydrateRoot`
49
+ * has to be called before the inline Flight stream has necessarily produced
50
+ * the root row — so it hands over the promise and suspends exactly where React
51
+ * was already going to suspend on the tree. Every other path settles it inside
52
+ * the navigation transition first, which is what keeps a decode failure inside
53
+ * the app's own error boundaries instead of above them (TIM-1297).
54
+ */
55
+ export type ParamsSource = PublishedParams | Promise<PublishedParams>;
56
+
57
+ /** The root row of an RSC payload. */
58
+ export interface RscPayloadRoot extends PublishedParams {
59
+ /** The route's element tree. */
60
+ tree: unknown;
61
+ }
62
+
63
+ /**
64
+ * What a reader gets when the value it was handed is not a payload root.
65
+ *
66
+ * In the browser this is unreachable for a real response: all four producers
67
+ * of a route payload go through `withPublishedParams`, and a client talking to
68
+ * a different build is answered with 204 at Stage 1c
69
+ * (`server/pipeline-phases.ts`) before any payload exists. It is reachable in
70
+ * the router's test/fallback path, where `decodeRsc` is absent and the
71
+ * "payload" is the raw response text.
72
+ */
73
+ export const NO_PUBLISHED_PARAMS: PublishedParams = { params: {}, slotParams: null };
74
+
75
+ /** True when `value` carries published params — a root, or a read of one. */
76
+ function hasPublishedParams(value: unknown): value is PublishedParams {
77
+ return (
78
+ typeof value === 'object' &&
79
+ value !== null &&
80
+ 'params' in value &&
81
+ typeof (value as PublishedParams).params === 'object' &&
82
+ (value as PublishedParams).params !== null
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Read the params published beside a tree.
88
+ *
89
+ * Accepts a payload root *or* a `PublishedParams` already split off one, so
90
+ * the client can pre-resolve on the navigation path and still hand the same
91
+ * value to the same provider. Never throws.
92
+ */
93
+ export function readPublishedParams(source: unknown): PublishedParams {
94
+ if (!hasPublishedParams(source)) return NO_PUBLISHED_PARAMS;
95
+ return { params: source.params, slotParams: source.slotParams ?? null };
96
+ }
97
+
98
+ /**
99
+ * Read the renderable tree out of a payload root.
100
+ *
101
+ * Returns the value unchanged when it is not a root — the router's fallback
102
+ * path stores raw response text under the same name, and a test asserting on
103
+ * that text should see the text.
104
+ */
105
+ export function readPayloadTree(root: unknown): unknown {
106
+ if (typeof root === 'object' && root !== null && 'tree' in root && hasPublishedParams(root)) {
107
+ return (root as RscPayloadRoot).tree;
108
+ }
109
+ return root;
110
+ }
111
+
112
+ /** A decoded payload, split into the two things the client does with it. */
113
+ export interface SplitPayload {
114
+ /** The renderable tree — an element, or a thenable React suspends on. */
115
+ tree: unknown;
116
+ /**
117
+ * The params published beside it. Settled when the root already is, so the
118
+ * hydration path can render without an extra await.
119
+ *
120
+ * **Never rejects.** A decode failure has to surface where React renders
121
+ * `tree`, so the error boundary *around the tree* catches it. Rejecting here
122
+ * too would take down the provider above every boundary in the app, turning
123
+ * a contained failure into a blank page (TIM-1297).
124
+ */
125
+ params: ParamsSource;
126
+ }
127
+
128
+ /**
129
+ * Split a decoded payload root into its tree and its params.
130
+ *
131
+ * Accepts a settled root or a thenable of one, and preserves which it was: a
132
+ * settled root splits synchronously, so no code path gains a suspend point it
133
+ * did not already have.
134
+ */
135
+ export function splitPayloadRoot(root: unknown): SplitPayload {
136
+ if (
137
+ typeof root !== 'object' ||
138
+ root === null ||
139
+ typeof (root as PromiseLike<unknown>).then !== 'function'
140
+ ) {
141
+ return { tree: readPayloadTree(root), params: readPublishedParams(root) };
142
+ }
143
+ const settled = root as PromiseLike<unknown>;
144
+ const tree = Promise.resolve(settled).then(readPayloadTree);
145
+ // The rejection still reaches React through its own read of `tree`; this
146
+ // only marks it handled for the case where nothing renders it (a partial
147
+ // navigation whose segment update finds no matching outlet).
148
+ tree.catch(() => {});
149
+ const params = Promise.resolve(settled).then(readPublishedParams, () => NO_PUBLISHED_PARAMS);
150
+ return { tree, params };
151
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * The `_rsc` payload cache key — derived identically on both sides.
3
+ *
4
+ * The client puts this in the payload URL so the document and its Flight
5
+ * payload occupy different cache keys (TIM-1268). The server recomputes it
6
+ * from the received headers so a request cannot claim one client's key while
7
+ * sending another client's headers — see `server/rsc-cache-key-guard.ts`.
8
+ * Both sides must agree exactly, which is why the derivation lives here and
9
+ * not in either one.
10
+ *
11
+ * Isomorphic: no server or client imports. The browser bundle audit
12
+ * (`tests/client-bundle-audit.test.ts`) enforces the client half of that.
13
+ *
14
+ * See design/19-client-navigation.md §"The `_rsc` cache key".
15
+ */
16
+
17
+ /**
18
+ * Digest length in hex characters. 128 bits: the origin guard compares a
19
+ * caller-supplied key against a recomputed one, so the relevant bound is a
20
+ * birthday attack (an attacker who can influence a victim's `X-Timber-URL`
21
+ * gets to search both sides), not second preimage. 64 bits would fall to
22
+ * ~2^32 work; 128 puts it at 2^64.
23
+ */
24
+ const KEY_HEX_LENGTH = 32;
25
+
26
+ /**
27
+ * Marks a value as a framework-issued key, and versions the derivation.
28
+ *
29
+ * Without it, an application value that happened to be 32 lowercase hex — an
30
+ * MD5, a token — is indistinguishable from a forged claim: both are "the
31
+ * right shape, the wrong digest", and no information in the value separates
32
+ * them. So the exemption cannot key on shape alone.
33
+ *
34
+ * The prefix resolves that because it is anchored to the address the
35
+ * victim's own client uses. A payload lives at `?_rsc=1.<digest>`, so
36
+ * anything aimed at that cache entry must carry the prefix and therefore
37
+ * gets validated; anything without it cannot be addressing a payload URL at
38
+ * all, and is exempt as application data. An attacker cannot both hit the
39
+ * victim's cache key and dodge the check.
40
+ *
41
+ * The version component also gives the derivation a migration path: the
42
+ * client and the origin must agree byte for byte, so a future change to the
43
+ * canonical form can be rolled out by bumping this instead of a flag day.
44
+ */
45
+ const KEY_PREFIX = '1.';
46
+
47
+ /**
48
+ * The request headers that vary an RSC payload response, in canonical
49
+ * casing — this list is also emitted verbatim as the response's `Vary`
50
+ * tokens (`server/pipeline.ts`), since "what varies the response" is one
51
+ * question with one answer, not two lists that happen to agree.
52
+ *
53
+ * This list is the contract, and it is enforced rather than trusted:
54
+ * `tests/rsc-cache-key.test.ts` asserts that the header names
55
+ * `buildRscHeaders()` can emit are exactly this set. A header added to the
56
+ * request without being added here fails that test, because a varying input
57
+ * outside the key is precisely the cache-poisoning hazard the key exists to
58
+ * close.
59
+ *
60
+ * **No credentials, session identifiers, or user-supplied content may be
61
+ * added.** Every value here is hashed into a URL, and URLs are logged by
62
+ * proxies, CDNs, and origin access logs. `fnv1aHash` is a cache-key hash,
63
+ * not a KDF — a low-entropy secret would be recoverable by brute force.
64
+ * Authentication travels on cookies, which never enter the key.
65
+ */
66
+ export const RSC_KEY_HEADERS = [
67
+ 'Accept',
68
+ 'X-Timber-State-Tree',
69
+ 'X-Timber-URL',
70
+ 'X-Timber-Deployment-Id',
71
+ ] as const;
72
+
73
+ /**
74
+ * Whether a value could be a key this module produced.
75
+ *
76
+ * `_rsc` is not a reserved parameter name — an application may already use
77
+ * it for its own search state, including for a value that happens to look
78
+ * like a digest. Anything without the `KEY_PREFIX` is therefore application
79
+ * data, not a claim on a payload cache key, and the origin guard leaves it
80
+ * alone rather than penalizing the response.
81
+ *
82
+ * Derived from `KEY_PREFIX` and `KEY_HEX_LENGTH` rather than spelled again,
83
+ * so the shape cannot drift from the digest that has to satisfy it.
84
+ *
85
+ * Ignoring a malformed value costs no protection: clients only ever send
86
+ * well-formed keys, so a URL bearing a malformed one is an address no
87
+ * legitimate request will ever ask for, and nothing can be poisoned at an
88
+ * address nobody reads.
89
+ */
90
+ export function isRscCacheKeyShape(value: string): boolean {
91
+ if (!value.startsWith(KEY_PREFIX)) return false;
92
+ const digest = value.slice(KEY_PREFIX.length);
93
+ return digest.length === KEY_HEX_LENGTH && /^[0-9a-f]+$/.test(digest);
94
+ }
95
+
96
+ /**
97
+ * The query parameter carrying the payload cache key.
98
+ *
99
+ * Spelled here rather than at each of the four places that read or write it
100
+ * — the client that appends it, the origin guard that validates it, and the
101
+ * strip below — because it is a wire contract between two independent
102
+ * implementations, exactly like the digest itself.
103
+ */
104
+ export const RSC_KEY_PARAM = '_rsc';
105
+
106
+ // ─── Keeping the key out of application state ────────────────────────────
107
+
108
+ /**
109
+ * Decode one raw `name` or `value` the way `URLSearchParams` does:
110
+ * `application/x-www-form-urlencoded`, so `+` is a space.
111
+ *
112
+ * Malformed percent-escapes throw in `decodeURIComponent`. `URLSearchParams`
113
+ * leaves those bytes as-is rather than throwing, and so do we — a pair we
114
+ * cannot decode is by definition not the framework's, so it is kept.
115
+ */
116
+ function formDecode(raw: string): string {
117
+ try {
118
+ return decodeURIComponent(raw.replace(/\+/g, ' '));
119
+ } catch {
120
+ return raw;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Remove framework-issued `_rsc` values from a raw query string.
126
+ *
127
+ * Only values matching `isRscCacheKeyShape` are removed. `_rsc` is not a
128
+ * reserved name (see that function), so `/search?_rsc=foo` is application
129
+ * state and survives — the same exemption the origin guard applies, derived
130
+ * from the same predicate so the two cannot disagree about what "ours"
131
+ * means.
132
+ *
133
+ * Operates on the raw string rather than round-tripping through
134
+ * `URLSearchParams`, so every surviving pair keeps its original bytes.
135
+ * Re-serializing would rewrite `?b` as `?b=` and `?a=%7E` as `?a=~` on RSC
136
+ * navigations only, which is the same class of inconsistency this function
137
+ * exists to remove.
138
+ *
139
+ * Matching decodes the name, because `?%5Frsc=<key>` is a `_rsc` parameter
140
+ * as far as `URLSearchParams` is concerned — the same reasoning as the
141
+ * guard's fast path.
142
+ *
143
+ * @param search - `url.search`, with or without the leading `?`.
144
+ * @returns The query string including a leading `?`, or `''` if empty.
145
+ */
146
+ export function stripRscCacheKey(search: string): string {
147
+ const query = search.startsWith('?') ? search.slice(1) : search;
148
+ if (query === '') return '';
149
+ const kept = query.split('&').filter((pair) => {
150
+ const eq = pair.indexOf('=');
151
+ if (formDecode(eq === -1 ? pair : pair.slice(0, eq)) !== RSC_KEY_PARAM) return true;
152
+ return !isRscCacheKeyShape(eq === -1 ? '' : formDecode(pair.slice(eq + 1)));
153
+ });
154
+ const result = kept.join('&');
155
+ return result === '' ? '' : `?${result}`;
156
+ }
157
+
158
+ /** A request's search state as application code should see it. */
159
+ export interface AppVisibleSearch {
160
+ /** Parsed params, with the framework's cache key removed. */
161
+ params: URLSearchParams;
162
+ /** The matching raw query string — `''` or `?…`. */
163
+ search: string;
164
+ }
165
+
166
+ /**
167
+ * The search state application code sees, for any request URL.
168
+ *
169
+ * The `_rsc` key rides on the payload URL of every RSC navigation and on no
170
+ * other request, so reading search params straight off `req.url` makes the
171
+ * same page observe different search state depending on whether it was
172
+ * reached by a full load or a client navigation — and lets the cache key
173
+ * leak into whatever the page builds from it: a canonical URL, a pagination
174
+ * link, an analytics payload (TIM-1272).
175
+ *
176
+ * Every application-facing derivation of search state goes through here.
177
+ * `params` and `search` are returned together, from one strip, because they
178
+ * are two views of one answer and were previously two expressions that had
179
+ * to agree.
180
+ *
181
+ * The raw request URL is deliberately untouched: `server/rsc-cache-key-guard.ts`
182
+ * still reads the parameter from `req.url` to bind the key to the request.
183
+ */
184
+ export function appVisibleSearch(url: URL): AppVisibleSearch {
185
+ const search = stripRscCacheKey(url.search);
186
+ return { params: new URLSearchParams(search), search };
187
+ }
188
+
189
+ /**
190
+ * Reads a header value by name. Must be case-insensitive — `Headers.get()`
191
+ * already is, and `recordLookup` below makes a plain record so.
192
+ */
193
+ export type HeaderLookup = (name: string) => string | null | undefined;
194
+
195
+ /**
196
+ * Compute the `_rsc` cache key for a request.
197
+ *
198
+ * Absent headers are omitted rather than sent as empty, so "header missing"
199
+ * and "header present but empty" produce different keys.
200
+ *
201
+ * Values are length-prefixed rather than delimiter-separated. Any delimiter
202
+ * can be forged by a value that contains it: with a `\0` separator, an
203
+ * `accept` of `a\nx-timber-url\0b` serializes identically to an `accept` of
204
+ * `a` alongside an `x-timber-url` of `b`, which would let one header claim
205
+ * another's key. HTTP forbids control characters in header values and both
206
+ * `fetch()` and the server runtime reject them, so this is defense in depth
207
+ * — but a length prefix is unambiguous for *any* content, which is a
208
+ * property worth having in a function whose whole job is to be agreed on by
209
+ * two independent implementations.
210
+ *
211
+ * SHA-256, truncated to 128 bits. A fast non-cryptographic hash is the
212
+ * obvious choice for a cache key and is the wrong one here: the origin
213
+ * *compares* this value against a recomputed one, which makes it a check.
214
+ * FNV-1a — used for timber's other cache keys — is a multiply-xor over a
215
+ * fixed field, so an attacker controlling a few bytes of any input (their
216
+ * own `X-Timber-URL`, say) can solve for a chosen digest rather than search
217
+ * for one, and forge a key belonging to somebody else.
218
+ *
219
+ * Returns `null` when no digest is available — `crypto.subtle` is absent in
220
+ * non-secure browsing contexts (plain `http://` on a LAN address). Callers
221
+ * must treat that as "no key", never as "key matches": the client omits the
222
+ * parameter and the server refuses to share-cache. That returns those
223
+ * clients to the pre-TIM-1268 posture, where `Vary` alone separates HTML
224
+ * from Flight — acceptable, because a non-secure origin has no shared-CDN
225
+ * caching story to protect in the first place.
226
+ */
227
+ export async function rscCacheKey(lookup: HeaderLookup): Promise<string | null> {
228
+ const subtle = globalThis.crypto?.subtle;
229
+ if (!subtle) return null;
230
+ let canonical = '';
231
+ for (const name of RSC_KEY_HEADERS) {
232
+ const value = lookup(name);
233
+ if (value == null) continue;
234
+ // Lowercased in the canonical form so the two implementations cannot
235
+ // disagree over the casing of the constant they share.
236
+ canonical += `${name.toLowerCase()}:${value.length}:${value}`;
237
+ }
238
+ const digest = await subtle.digest('SHA-256', new TextEncoder().encode(canonical));
239
+ let hex = '';
240
+ for (const byte of new Uint8Array(digest, 0, KEY_HEX_LENGTH / 2)) {
241
+ hex += byte.toString(16).padStart(2, '0');
242
+ }
243
+ return KEY_PREFIX + hex;
244
+ }
245
+
246
+ /**
247
+ * An unverifiable stand-in key, for the one case where the digest cannot be
248
+ * computed: `crypto.subtle` is absent in non-secure browsing contexts.
249
+ * `crypto.getRandomValues` is not gated that way, so it is available exactly
250
+ * where the digest is not.
251
+ *
252
+ * Carries `KEY_PREFIX` like any other key, so the origin reads it as a claim
253
+ * it cannot verify — and refuses to share-cache the response — rather than
254
+ * exempting it as application data. Lives here so the prefix has one home.
255
+ */
256
+ export function randomRscCacheKey(): string {
257
+ const bytes = new Uint8Array(KEY_HEX_LENGTH / 2);
258
+ globalThis.crypto.getRandomValues(bytes);
259
+ return KEY_PREFIX + [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
260
+ }
261
+
262
+ /** Build a case-insensitive lookup over a plain header record. */
263
+ export function recordLookup(headers: Record<string, string>): HeaderLookup {
264
+ const lowered: Record<string, string> = {};
265
+ for (const name of Object.keys(headers)) {
266
+ lowered[name.toLowerCase()] = headers[name];
267
+ }
268
+ return (name) => lowered[name.toLowerCase()];
269
+ }