@timber-js/app 0.2.0-alpha.21 → 0.2.0-alpha.211

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 (1393) hide show
  1. package/LICENSE +8 -0
  2. package/agent-skill.md +346 -0
  3. package/bin/timber.mjs +15 -1
  4. package/dist/_chunks/actions-Rjk4htmA.js +1199 -0
  5. package/dist/_chunks/actions-Rjk4htmA.js.map +1 -0
  6. package/dist/_chunks/als-registry-DaxkVjt5.js +45 -0
  7. package/dist/_chunks/als-registry-DaxkVjt5.js.map +1 -0
  8. package/dist/_chunks/als-slots-BEEIPKYm.js +26 -0
  9. package/dist/_chunks/als-slots-BEEIPKYm.js.map +1 -0
  10. package/dist/_chunks/base-path-DaQrzbez.js +80 -0
  11. package/dist/_chunks/base-path-DaQrzbez.js.map +1 -0
  12. package/dist/_chunks/build-manifest-DWppEdLB.js +86 -0
  13. package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
  14. package/dist/_chunks/build-output-helper-RV_BRcF1.js +367 -0
  15. package/dist/_chunks/build-output-helper-RV_BRcF1.js.map +1 -0
  16. package/dist/_chunks/cache-api-DGdYfNJn.js +1182 -0
  17. package/dist/_chunks/cache-api-DGdYfNJn.js.map +1 -0
  18. package/dist/_chunks/canonicalize-CgHoscYO.js +66 -0
  19. package/dist/_chunks/canonicalize-CgHoscYO.js.map +1 -0
  20. package/dist/_chunks/chains-DGX9zmg9.js +272 -0
  21. package/dist/_chunks/chains-DGX9zmg9.js.map +1 -0
  22. package/dist/_chunks/classify-QwG5rxKI.js +147 -0
  23. package/dist/_chunks/classify-QwG5rxKI.js.map +1 -0
  24. package/dist/_chunks/cli-check-ajNY3B2e.js +252 -0
  25. package/dist/_chunks/cli-check-ajNY3B2e.js.map +1 -0
  26. package/dist/_chunks/cli-context-By7k2xVo.js +70 -0
  27. package/dist/_chunks/cli-context-By7k2xVo.js.map +1 -0
  28. package/dist/_chunks/cli-schema-sync-D2eI8jEg.js +299 -0
  29. package/dist/_chunks/cli-schema-sync-D2eI8jEg.js.map +1 -0
  30. package/dist/_chunks/client-dep-entries-2HCF09no.js +66 -0
  31. package/dist/_chunks/client-dep-entries-2HCF09no.js.map +1 -0
  32. package/dist/_chunks/cloudflare-Byxl8FWP.js +1333 -0
  33. package/dist/_chunks/cloudflare-Byxl8FWP.js.map +1 -0
  34. package/dist/_chunks/codec-registry-oOUxugz3.js +21 -0
  35. package/dist/_chunks/codec-registry-oOUxugz3.js.map +1 -0
  36. package/dist/_chunks/codegen-Bps1sLKJ.js +432 -0
  37. package/dist/_chunks/codegen-Bps1sLKJ.js.map +1 -0
  38. package/dist/_chunks/convention-lint-DLmhGsRS.js +590 -0
  39. package/dist/_chunks/convention-lint-DLmhGsRS.js.map +1 -0
  40. package/dist/_chunks/dev-server-TFpEwm3H.js +2039 -0
  41. package/dist/_chunks/dev-server-TFpEwm3H.js.map +1 -0
  42. package/dist/_chunks/dist-DtrJyBbw.js +386 -0
  43. package/dist/_chunks/dist-DtrJyBbw.js.map +1 -0
  44. package/dist/_chunks/error-boundary-DsNScGRM.js +1024 -0
  45. package/dist/_chunks/error-boundary-DsNScGRM.js.map +1 -0
  46. package/dist/_chunks/fast-hash-C-tVYrLb.js +41 -0
  47. package/dist/_chunks/fast-hash-C-tVYrLb.js.map +1 -0
  48. package/dist/_chunks/format-BKclgVbk.js +14 -0
  49. package/dist/_chunks/format-BKclgVbk.js.map +1 -0
  50. package/dist/_chunks/fs-identity-BRKuL215.js +207 -0
  51. package/dist/_chunks/fs-identity-BRKuL215.js.map +1 -0
  52. package/dist/_chunks/graph-cache-CYiUS2aS.js +303 -0
  53. package/dist/_chunks/graph-cache-CYiUS2aS.js.map +1 -0
  54. package/dist/_chunks/href-validation-BIrxavIy.js +213 -0
  55. package/dist/_chunks/href-validation-BIrxavIy.js.map +1 -0
  56. package/dist/_chunks/json-lossy-check-CVuRs2hG.js +66 -0
  57. package/dist/_chunks/json-lossy-check-CVuRs2hG.js.map +1 -0
  58. package/dist/_chunks/live-graph-cNuWMYQI.js +151 -0
  59. package/dist/_chunks/live-graph-cNuWMYQI.js.map +1 -0
  60. package/dist/_chunks/logger-BP0LN6vP.js +662 -0
  61. package/dist/_chunks/logger-BP0LN6vP.js.map +1 -0
  62. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-DSDjM_hJ.js} +71 -27
  63. package/dist/_chunks/metadata-routes-DSDjM_hJ.js.map +1 -0
  64. package/dist/_chunks/navigation-root-BQfo1-kG.js +126 -0
  65. package/dist/_chunks/navigation-root-BQfo1-kG.js.map +1 -0
  66. package/dist/_chunks/param-value-C8TNYchQ.js +66 -0
  67. package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
  68. package/dist/_chunks/plugin-context-BPBPc3lL.js +345 -0
  69. package/dist/_chunks/plugin-context-BPBPc3lL.js.map +1 -0
  70. package/dist/_chunks/poison-rules-DoEhbqaY.js +110 -0
  71. package/dist/_chunks/poison-rules-DoEhbqaY.js.map +1 -0
  72. package/dist/_chunks/poison-scan-CfQ3unZR.js +99 -0
  73. package/dist/_chunks/poison-scan-CfQ3unZR.js.map +1 -0
  74. package/dist/_chunks/purge-store-Byr8XOjU.js +14 -0
  75. package/dist/_chunks/purge-store-Byr8XOjU.js.map +1 -0
  76. package/dist/_chunks/render-timeout-DOf_BBcO.js +46 -0
  77. package/dist/_chunks/render-timeout-DOf_BBcO.js.map +1 -0
  78. package/dist/_chunks/resolve-schema-CBR6Lm4i.js +50 -0
  79. package/dist/_chunks/resolve-schema-CBR6Lm4i.js.map +1 -0
  80. package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  81. package/dist/_chunks/rsc-cache-key-ClUiXQnK.js +243 -0
  82. package/dist/_chunks/rsc-cache-key-ClUiXQnK.js.map +1 -0
  83. package/dist/_chunks/rsc-media-type-DDc7duTD.js +61 -0
  84. package/dist/_chunks/rsc-media-type-DDc7duTD.js.map +1 -0
  85. package/dist/_chunks/rsc-payload-path-dffcs4jp.js +54 -0
  86. package/dist/_chunks/rsc-payload-path-dffcs4jp.js.map +1 -0
  87. package/dist/_chunks/scan-BoP17W3w.js +140 -0
  88. package/dist/_chunks/scan-BoP17W3w.js.map +1 -0
  89. package/dist/_chunks/scanner-DmqdxzbW.js +2056 -0
  90. package/dist/_chunks/scanner-DmqdxzbW.js.map +1 -0
  91. package/dist/_chunks/schema-bridge-C83xa9lT.js +199 -0
  92. package/dist/_chunks/schema-bridge-C83xa9lT.js.map +1 -0
  93. package/dist/_chunks/segment-classify-C539Pa2O.js +197 -0
  94. package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -0
  95. package/dist/_chunks/segment-context-xtPUGfdq.js +137 -0
  96. package/dist/_chunks/segment-context-xtPUGfdq.js.map +1 -0
  97. package/dist/_chunks/segment-keys-D5hu1hz4.js +62 -0
  98. package/dist/_chunks/segment-keys-D5hu1hz4.js.map +1 -0
  99. package/dist/_chunks/singleflight-BeVMMKi0.js +55 -0
  100. package/dist/_chunks/singleflight-BeVMMKi0.js.map +1 -0
  101. package/dist/_chunks/sizeof-BM1409x2.js +36 -0
  102. package/dist/_chunks/sizeof-BM1409x2.js.map +1 -0
  103. package/dist/_chunks/ssr-data-nA3I70_n.js +49 -0
  104. package/dist/_chunks/ssr-data-nA3I70_n.js.map +1 -0
  105. package/dist/_chunks/state-BhNGPsdi.js +50 -0
  106. package/dist/_chunks/state-BhNGPsdi.js.map +1 -0
  107. package/dist/_chunks/status-page-marker-BRX9Ib-d.js +564 -0
  108. package/dist/_chunks/status-page-marker-BRX9Ib-d.js.map +1 -0
  109. package/dist/_chunks/tree-match-6yihorhk.js +169 -0
  110. package/dist/_chunks/tree-match-6yihorhk.js.map +1 -0
  111. package/dist/_chunks/use-query-states-I3JMng6J.js +226 -0
  112. package/dist/_chunks/use-query-states-I3JMng6J.js.map +1 -0
  113. package/dist/_chunks/walkers-Czu2jXFq.js +144 -0
  114. package/dist/_chunks/walkers-Czu2jXFq.js.map +1 -0
  115. package/dist/_chunks/write-file-atomic-D1qWqHb4.js +32 -0
  116. package/dist/_chunks/write-file-atomic-D1qWqHb4.js.map +1 -0
  117. package/dist/adapters/build-output-helper.d.ts +72 -0
  118. package/dist/adapters/build-output-helper.d.ts.map +1 -0
  119. package/dist/adapters/canonicalize-module.d.ts +8 -0
  120. package/dist/adapters/canonicalize-module.d.ts.map +1 -0
  121. package/dist/adapters/cloudflare-dev.d.ts +109 -0
  122. package/dist/adapters/cloudflare-dev.d.ts.map +1 -0
  123. package/dist/adapters/cloudflare-dev.js +74 -0
  124. package/dist/adapters/cloudflare-dev.js.map +1 -0
  125. package/dist/adapters/cloudflare-kv-cache.d.ts +65 -0
  126. package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -0
  127. package/dist/adapters/cloudflare-kv-cache.js +123 -0
  128. package/dist/adapters/cloudflare-kv-cache.js.map +1 -0
  129. package/dist/adapters/cloudflare.d.ts +224 -24
  130. package/dist/adapters/cloudflare.d.ts.map +1 -1
  131. package/dist/adapters/cloudflare.js +2 -188
  132. package/dist/adapters/compress-module.d.ts +25 -4
  133. package/dist/adapters/compress-module.d.ts.map +1 -1
  134. package/dist/adapters/fs-identity.d.ts +63 -0
  135. package/dist/adapters/fs-identity.d.ts.map +1 -0
  136. package/dist/adapters/nitro-presets.d.ts +31 -0
  137. package/dist/adapters/nitro-presets.d.ts.map +1 -0
  138. package/dist/adapters/nitro-preview.d.ts +64 -0
  139. package/dist/adapters/nitro-preview.d.ts.map +1 -0
  140. package/dist/adapters/nitro.d.ts +54 -37
  141. package/dist/adapters/nitro.d.ts.map +1 -1
  142. package/dist/adapters/nitro.js +631 -359
  143. package/dist/adapters/nitro.js.map +1 -1
  144. package/dist/adapters/shared.d.ts +67 -0
  145. package/dist/adapters/shared.d.ts.map +1 -0
  146. package/dist/adapters/types.d.ts +122 -16
  147. package/dist/adapters/types.d.ts.map +1 -1
  148. package/dist/adapters/types.js +0 -0
  149. package/dist/analyze/chains.d.ts +124 -0
  150. package/dist/analyze/chains.d.ts.map +1 -0
  151. package/dist/analyze/classify.d.ts +153 -0
  152. package/dist/analyze/classify.d.ts.map +1 -0
  153. package/dist/analyze/crawl-entry.d.ts +17 -0
  154. package/dist/analyze/crawl-entry.d.ts.map +1 -0
  155. package/dist/analyze/crawl-entry.js +314 -0
  156. package/dist/analyze/crawl-entry.js.map +1 -0
  157. package/dist/analyze/crawl.d.ts +134 -0
  158. package/dist/analyze/crawl.d.ts.map +1 -0
  159. package/dist/analyze/graph-cache.d.ts +104 -0
  160. package/dist/analyze/graph-cache.d.ts.map +1 -0
  161. package/dist/analyze/graph-command.d.ts +43 -0
  162. package/dist/analyze/graph-command.d.ts.map +1 -0
  163. package/dist/analyze/graph-command.js +472 -0
  164. package/dist/analyze/graph-command.js.map +1 -0
  165. package/dist/analyze/live-graph.d.ts +81 -0
  166. package/dist/analyze/live-graph.d.ts.map +1 -0
  167. package/dist/analyze/poison-rules.d.ts +72 -0
  168. package/dist/analyze/poison-rules.d.ts.map +1 -0
  169. package/dist/analyze/poison-scan.d.ts +48 -0
  170. package/dist/analyze/poison-scan.d.ts.map +1 -0
  171. package/dist/analyze/scan.d.ts +73 -0
  172. package/dist/analyze/scan.d.ts.map +1 -0
  173. package/dist/cache/cache-api.d.ts +48 -0
  174. package/dist/cache/cache-api.d.ts.map +1 -0
  175. package/dist/cache/fast-hash.d.ts +5 -0
  176. package/dist/cache/fast-hash.d.ts.map +1 -1
  177. package/dist/cache/handler-store.d.ts +33 -0
  178. package/dist/cache/handler-store.d.ts.map +1 -0
  179. package/dist/cache/index.d.ts +32 -9
  180. package/dist/cache/index.d.ts.map +1 -1
  181. package/dist/cache/index.js +3 -326
  182. package/dist/cache/invalidation-epoch.d.ts +43 -0
  183. package/dist/cache/invalidation-epoch.d.ts.map +1 -0
  184. package/dist/cache/json-lossy-check.d.ts +11 -0
  185. package/dist/cache/json-lossy-check.d.ts.map +1 -0
  186. package/dist/cache/redis-handler.d.ts +141 -9
  187. package/dist/cache/redis-handler.d.ts.map +1 -1
  188. package/dist/cache/singleflight.d.ts +21 -2
  189. package/dist/cache/singleflight.d.ts.map +1 -1
  190. package/dist/cache/sizeof.d.ts +22 -0
  191. package/dist/cache/sizeof.d.ts.map +1 -0
  192. package/dist/cache/stable-stringify.d.ts +9 -0
  193. package/dist/cache/stable-stringify.d.ts.map +1 -1
  194. package/dist/cache/store.d.ts +53 -0
  195. package/dist/cache/store.d.ts.map +1 -0
  196. package/dist/cache/stores/cloudflare-kv.d.ts +38 -0
  197. package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -0
  198. package/dist/cache/stores/cloudflare-kv.js +29 -0
  199. package/dist/cache/stores/cloudflare-kv.js.map +1 -0
  200. package/dist/cache/stores/memory.d.ts +42 -0
  201. package/dist/cache/stores/memory.d.ts.map +1 -0
  202. package/dist/cache/stores/memory.js +90 -0
  203. package/dist/cache/stores/memory.js.map +1 -0
  204. package/dist/cache/stores/redis.d.ts +69 -0
  205. package/dist/cache/stores/redis.d.ts.map +1 -0
  206. package/dist/cache/stores/redis.js +35 -0
  207. package/dist/cache/stores/redis.js.map +1 -0
  208. package/dist/cache/stores/vercel.d.ts +36 -0
  209. package/dist/cache/stores/vercel.d.ts.map +1 -0
  210. package/dist/cache/stores/vercel.js +33 -0
  211. package/dist/cache/stores/vercel.js.map +1 -0
  212. package/dist/cache/tag-aware-handler.d.ts +52 -0
  213. package/dist/cache/tag-aware-handler.d.ts.map +1 -0
  214. package/dist/cache/timber-cache.d.ts +21 -11
  215. package/dist/cache/timber-cache.d.ts.map +1 -1
  216. package/dist/cdn/cloudflare-purge.d.ts +23 -0
  217. package/dist/cdn/cloudflare-purge.d.ts.map +1 -0
  218. package/dist/cdn/cloudflare-purge.js +30 -0
  219. package/dist/cdn/cloudflare-purge.js.map +1 -0
  220. package/dist/cdn/fastly-purge.d.ts +25 -0
  221. package/dist/cdn/fastly-purge.d.ts.map +1 -0
  222. package/dist/cdn/fastly-purge.js +33 -0
  223. package/dist/cdn/fastly-purge.js.map +1 -0
  224. package/dist/cdn/headers.d.ts +97 -0
  225. package/dist/cdn/headers.d.ts.map +1 -0
  226. package/dist/cdn/index.d.ts +5 -0
  227. package/dist/cdn/index.d.ts.map +1 -0
  228. package/dist/cdn/index.js +94 -0
  229. package/dist/cdn/index.js.map +1 -0
  230. package/dist/cdn/purge-store.d.ts +23 -0
  231. package/dist/cdn/purge-store.d.ts.map +1 -0
  232. package/dist/cdn/workers-cache-purge.d.ts +20 -0
  233. package/dist/cdn/workers-cache-purge.d.ts.map +1 -0
  234. package/dist/cdn/workers-cache-purge.js +35 -0
  235. package/dist/cdn/workers-cache-purge.js.map +1 -0
  236. package/dist/cli-check.d.ts +133 -0
  237. package/dist/cli-check.d.ts.map +1 -0
  238. package/dist/cli-context.d.ts +54 -0
  239. package/dist/cli-context.d.ts.map +1 -0
  240. package/dist/cli-schema-sync.d.ts +59 -0
  241. package/dist/cli-schema-sync.d.ts.map +1 -0
  242. package/dist/cli.d.ts +85 -13
  243. package/dist/cli.d.ts.map +1 -1
  244. package/dist/cli.js +224 -63
  245. package/dist/cli.js.map +1 -1
  246. package/dist/client/browser-dev.d.ts +42 -2
  247. package/dist/client/browser-dev.d.ts.map +1 -1
  248. package/dist/client/browser-entry/action-dispatch.d.ts +19 -0
  249. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -0
  250. package/dist/client/browser-entry/action-queue.d.ts +63 -0
  251. package/dist/client/browser-entry/action-queue.d.ts.map +1 -0
  252. package/dist/client/browser-entry/hmr.d.ts +21 -0
  253. package/dist/client/browser-entry/hmr.d.ts.map +1 -0
  254. package/dist/client/browser-entry/hydrate.d.ts +53 -0
  255. package/dist/client/browser-entry/hydrate.d.ts.map +1 -0
  256. package/dist/client/browser-entry/index.d.ts +30 -0
  257. package/dist/client/browser-entry/index.d.ts.map +1 -0
  258. package/dist/client/browser-entry/post-hydration.d.ts +26 -0
  259. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -0
  260. package/dist/client/browser-entry/router-init.d.ts +41 -0
  261. package/dist/client/browser-entry/router-init.d.ts.map +1 -0
  262. package/dist/client/browser-entry/rsc-stream.d.ts +32 -0
  263. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -0
  264. package/dist/client/browser-entry/scroll.d.ts +28 -0
  265. package/dist/client/browser-entry/scroll.d.ts.map +1 -0
  266. package/dist/client/deny-last-resort.d.ts +29 -0
  267. package/dist/client/deny-last-resort.d.ts.map +1 -0
  268. package/dist/client/error-boundary.d.ts +58 -9
  269. package/dist/client/error-boundary.d.ts.map +1 -1
  270. package/dist/client/error-boundary.js +2 -126
  271. package/dist/client/error-reconstituter.d.ts +55 -0
  272. package/dist/client/error-reconstituter.d.ts.map +1 -0
  273. package/dist/client/form.d.ts +16 -27
  274. package/dist/client/form.d.ts.map +1 -1
  275. package/dist/client/global-context.d.ts +15 -0
  276. package/dist/client/global-context.d.ts.map +1 -0
  277. package/dist/client/history.d.ts +50 -9
  278. package/dist/client/history.d.ts.map +1 -1
  279. package/dist/client/index.d.ts +20 -33
  280. package/dist/client/index.d.ts.map +1 -1
  281. package/dist/client/index.js +330 -1150
  282. package/dist/client/index.js.map +1 -1
  283. package/dist/client/internal.d.ts +20 -0
  284. package/dist/client/internal.d.ts.map +1 -0
  285. package/dist/client/internal.js +792 -0
  286. package/dist/client/internal.js.map +1 -0
  287. package/dist/client/link.d.ts +85 -60
  288. package/dist/client/link.d.ts.map +1 -1
  289. package/dist/client/location-search.d.ts +12 -0
  290. package/dist/client/location-search.d.ts.map +1 -0
  291. package/dist/client/navigation-api.d.ts +133 -0
  292. package/dist/client/navigation-api.d.ts.map +1 -0
  293. package/dist/client/navigation-commit.d.ts +111 -0
  294. package/dist/client/navigation-commit.d.ts.map +1 -0
  295. package/dist/client/navigation-context.d.ts +54 -36
  296. package/dist/client/navigation-context.d.ts.map +1 -1
  297. package/dist/client/navigation-root.d.ts +106 -0
  298. package/dist/client/navigation-root.d.ts.map +1 -0
  299. package/dist/client/navigation-transition.d.ts +192 -0
  300. package/dist/client/navigation-transition.d.ts.map +1 -0
  301. package/dist/client/nuqs-adapter.d.ts +1 -1
  302. package/dist/client/nuqs-adapter.d.ts.map +1 -1
  303. package/dist/client/params-context.d.ts +81 -0
  304. package/dist/client/params-context.d.ts.map +1 -0
  305. package/dist/client/react-root.d.ts +45 -0
  306. package/dist/client/react-root.d.ts.map +1 -0
  307. package/dist/client/router-effects.d.ts +137 -0
  308. package/dist/client/router-effects.d.ts.map +1 -0
  309. package/dist/client/router-lifecycle.d.ts +98 -0
  310. package/dist/client/router-lifecycle.d.ts.map +1 -0
  311. package/dist/client/router-pipeline.d.ts +84 -0
  312. package/dist/client/router-pipeline.d.ts.map +1 -0
  313. package/dist/client/router-ref.d.ts +2 -2
  314. package/dist/client/router-ref.d.ts.map +1 -1
  315. package/dist/client/router-skew.d.ts +21 -0
  316. package/dist/client/router-skew.d.ts.map +1 -0
  317. package/dist/client/router-types.d.ts +334 -0
  318. package/dist/client/router-types.d.ts.map +1 -0
  319. package/dist/client/router-url.d.ts +24 -0
  320. package/dist/client/router-url.d.ts.map +1 -0
  321. package/dist/client/router.d.ts +1 -97
  322. package/dist/client/router.d.ts.map +1 -1
  323. package/dist/client/rsc-fetch.d.ts +110 -28
  324. package/dist/client/rsc-fetch.d.ts.map +1 -1
  325. package/dist/client/segment-cache.d.ts +186 -25
  326. package/dist/client/segment-cache.d.ts.map +1 -1
  327. package/dist/client/segment-context.d.ts.map +1 -1
  328. package/dist/client/segment-outlet.d.ts +68 -0
  329. package/dist/client/segment-outlet.d.ts.map +1 -0
  330. package/dist/client/segment-update-context.d.ts +15 -0
  331. package/dist/client/segment-update-context.d.ts.map +1 -0
  332. package/dist/client/shallow-url.d.ts +10 -0
  333. package/dist/client/shallow-url.d.ts.map +1 -0
  334. package/dist/client/slot-content-cache-context.d.ts +35 -0
  335. package/dist/client/slot-content-cache-context.d.ts.map +1 -0
  336. package/dist/client/slot-context.d.ts +31 -0
  337. package/dist/client/slot-context.d.ts.map +1 -0
  338. package/dist/client/slot-outlet.d.ts +16 -0
  339. package/dist/client/slot-outlet.d.ts.map +1 -0
  340. package/dist/client/slot-provider.d.ts +25 -0
  341. package/dist/client/slot-provider.d.ts.map +1 -0
  342. package/dist/client/ssr-data.d.ts +26 -50
  343. package/dist/client/ssr-data.d.ts.map +1 -1
  344. package/dist/client/stale-client.d.ts +69 -0
  345. package/dist/client/stale-client.d.ts.map +1 -0
  346. package/dist/client/state.d.ts +21 -13
  347. package/dist/client/state.d.ts.map +1 -1
  348. package/dist/client/status-page-marker.d.ts +25 -0
  349. package/dist/client/status-page-marker.d.ts.map +1 -0
  350. package/dist/client/top-loader.d.ts +7 -6
  351. package/dist/client/top-loader.d.ts.map +1 -1
  352. package/dist/client/unload-guard.d.ts +5 -0
  353. package/dist/client/unload-guard.d.ts.map +1 -1
  354. package/dist/client/use-cookie.d.ts +12 -0
  355. package/dist/client/use-cookie.d.ts.map +1 -1
  356. package/dist/client/use-link-status.d.ts +5 -5
  357. package/dist/client/use-link-status.d.ts.map +1 -1
  358. package/dist/client/use-pathname.d.ts +13 -11
  359. package/dist/client/use-pathname.d.ts.map +1 -1
  360. package/dist/client/use-pending-navigation.d.ts +23 -0
  361. package/dist/client/use-pending-navigation.d.ts.map +1 -0
  362. package/dist/client/use-query-states.d.ts +10 -4
  363. package/dist/client/use-query-states.d.ts.map +1 -1
  364. package/dist/client/use-router.d.ts +15 -8
  365. package/dist/client/use-router.d.ts.map +1 -1
  366. package/dist/client/use-search-params.d.ts +15 -8
  367. package/dist/client/use-search-params.d.ts.map +1 -1
  368. package/dist/client/use-segment-params.d.ts +55 -0
  369. package/dist/client/use-segment-params.d.ts.map +1 -0
  370. package/dist/codec.d.ts +69 -0
  371. package/dist/codec.d.ts.map +1 -0
  372. package/dist/codec.js +76 -0
  373. package/dist/codec.js.map +1 -0
  374. package/dist/config-types.d.ts +323 -0
  375. package/dist/config-types.d.ts.map +1 -0
  376. package/dist/config-validation.d.ts +68 -0
  377. package/dist/config-validation.d.ts.map +1 -0
  378. package/dist/cookies/define-cookie.d.ts +58 -25
  379. package/dist/cookies/define-cookie.d.ts.map +1 -1
  380. package/dist/cookies/index.d.ts +3 -2
  381. package/dist/cookies/index.d.ts.map +1 -1
  382. package/dist/cookies/index.js +256 -36
  383. package/dist/cookies/index.js.map +1 -1
  384. package/dist/cookies/json-cookie.d.ts +64 -0
  385. package/dist/cookies/json-cookie.d.ts.map +1 -0
  386. package/dist/cookies/validation.d.ts +34 -0
  387. package/dist/cookies/validation.d.ts.map +1 -0
  388. package/dist/dev-tools/404-page.d.ts +50 -0
  389. package/dist/dev-tools/404-page.d.ts.map +1 -0
  390. package/dist/dev-tools/browser-logs.d.ts +40 -0
  391. package/dist/dev-tools/browser-logs.d.ts.map +1 -0
  392. package/dist/dev-tools/dev-page-shell.d.ts +95 -0
  393. package/dist/dev-tools/dev-page-shell.d.ts.map +1 -0
  394. package/dist/dev-tools/error-page.d.ts +32 -0
  395. package/dist/dev-tools/error-page.d.ts.map +1 -0
  396. package/dist/dev-tools/holding-server.d.ts +46 -0
  397. package/dist/dev-tools/holding-server.d.ts.map +1 -0
  398. package/dist/dev-tools/hydration-overlay.d.ts +11 -0
  399. package/dist/dev-tools/hydration-overlay.d.ts.map +1 -0
  400. package/dist/dev-tools/index.d.ts +31 -0
  401. package/dist/dev-tools/index.d.ts.map +1 -0
  402. package/dist/dev-tools/instrumentation.d.ts +74 -0
  403. package/dist/dev-tools/instrumentation.d.ts.map +1 -0
  404. package/dist/{server/dev-logger.d.ts → dev-tools/logger.d.ts} +1 -1
  405. package/dist/dev-tools/logger.d.ts.map +1 -0
  406. package/dist/{plugins/dev-logs.d.ts → dev-tools/logs.d.ts} +17 -3
  407. package/dist/dev-tools/logs.d.ts.map +1 -0
  408. package/dist/{plugins/dev-error-overlay.d.ts → dev-tools/overlay.d.ts} +15 -25
  409. package/dist/dev-tools/overlay.d.ts.map +1 -0
  410. package/dist/dev-tools/stack-classifier.d.ts +34 -0
  411. package/dist/dev-tools/stack-classifier.d.ts.map +1 -0
  412. package/dist/dev-tools/terminal.d.ts +28 -0
  413. package/dist/dev-tools/terminal.d.ts.map +1 -0
  414. package/dist/fonts/ast-walk.d.ts +68 -0
  415. package/dist/fonts/ast-walk.d.ts.map +1 -0
  416. package/dist/fonts/ast.d.ts +112 -8
  417. package/dist/fonts/ast.d.ts.map +1 -1
  418. package/dist/fonts/bundle.d.ts +50 -0
  419. package/dist/fonts/bundle.d.ts.map +1 -0
  420. package/dist/fonts/css.d.ts +1 -1
  421. package/dist/fonts/dev-middleware.d.ts +22 -0
  422. package/dist/fonts/dev-middleware.d.ts.map +1 -0
  423. package/dist/fonts/fallbacks.d.ts +0 -7
  424. package/dist/fonts/fallbacks.d.ts.map +1 -1
  425. package/dist/fonts/google.d.ts +5 -3
  426. package/dist/fonts/google.d.ts.map +1 -1
  427. package/dist/fonts/local.d.ts +5 -5
  428. package/dist/fonts/local.d.ts.map +1 -1
  429. package/dist/fonts/paths.d.ts +43 -0
  430. package/dist/fonts/paths.d.ts.map +1 -0
  431. package/dist/fonts/pipeline.d.ts +177 -0
  432. package/dist/fonts/pipeline.d.ts.map +1 -0
  433. package/dist/fonts/transform.d.ts +82 -0
  434. package/dist/fonts/transform.d.ts.map +1 -0
  435. package/dist/fonts/types.d.ts +47 -1
  436. package/dist/fonts/types.d.ts.map +1 -1
  437. package/dist/fonts/virtual-modules.d.ts +77 -0
  438. package/dist/fonts/virtual-modules.d.ts.map +1 -0
  439. package/dist/index.d.ts +35 -181
  440. package/dist/index.d.ts.map +1 -1
  441. package/dist/index.js +8871 -14233
  442. package/dist/index.js.map +1 -1
  443. package/dist/params/codec-registry.d.ts +26 -0
  444. package/dist/params/codec-registry.d.ts.map +1 -0
  445. package/dist/params/define-schema.d.ts +81 -0
  446. package/dist/params/define-schema.d.ts.map +1 -0
  447. package/dist/params/index.d.ts +5 -0
  448. package/dist/params/index.d.ts.map +1 -0
  449. package/dist/params/index.js +34 -0
  450. package/dist/params/index.js.map +1 -0
  451. package/dist/params/resolve-schema.d.ts +34 -0
  452. package/dist/params/resolve-schema.d.ts.map +1 -0
  453. package/dist/plugin-context.d.ts +262 -0
  454. package/dist/plugin-context.d.ts.map +1 -0
  455. package/dist/plugins/build-manifest.d.ts +7 -3
  456. package/dist/plugins/build-manifest.d.ts.map +1 -1
  457. package/dist/plugins/build-report.d.ts +61 -11
  458. package/dist/plugins/build-report.d.ts.map +1 -1
  459. package/dist/plugins/cache.d.ts +64 -0
  460. package/dist/plugins/cache.d.ts.map +1 -0
  461. package/dist/plugins/callsite-ast.d.ts +117 -0
  462. package/dist/plugins/callsite-ast.d.ts.map +1 -0
  463. package/dist/plugins/callsite-jsx.d.ts +35 -0
  464. package/dist/plugins/callsite-jsx.d.ts.map +1 -0
  465. package/dist/plugins/chunks.d.ts +11 -4
  466. package/dist/plugins/chunks.d.ts.map +1 -1
  467. package/dist/plugins/client-chunks.d.ts +40 -0
  468. package/dist/plugins/client-chunks.d.ts.map +1 -0
  469. package/dist/plugins/client-dep-entries.d.ts +44 -0
  470. package/dist/plugins/client-dep-entries.d.ts.map +1 -0
  471. package/dist/plugins/cloudflare-wasm.d.ts +21 -0
  472. package/dist/plugins/cloudflare-wasm.d.ts.map +1 -0
  473. package/dist/plugins/content.d.ts +1 -1
  474. package/dist/plugins/content.d.ts.map +1 -1
  475. package/dist/plugins/dev-discovery.d.ts +123 -0
  476. package/dist/plugins/dev-discovery.d.ts.map +1 -0
  477. package/dist/plugins/dev-server.d.ts +24 -1
  478. package/dist/plugins/dev-server.d.ts.map +1 -1
  479. package/dist/plugins/entries.d.ts +34 -4
  480. package/dist/plugins/entries.d.ts.map +1 -1
  481. package/dist/plugins/fonts.d.ts +35 -74
  482. package/dist/plugins/fonts.d.ts.map +1 -1
  483. package/dist/plugins/graph-endpoint.d.ts +112 -0
  484. package/dist/plugins/graph-endpoint.d.ts.map +1 -0
  485. package/dist/plugins/mdx.d.ts +18 -5
  486. package/dist/plugins/mdx.d.ts.map +1 -1
  487. package/dist/plugins/post-build.d.ts +110 -0
  488. package/dist/plugins/post-build.d.ts.map +1 -0
  489. package/dist/plugins/prebuilt-capture.d.ts +67 -0
  490. package/dist/plugins/prebuilt-capture.d.ts.map +1 -0
  491. package/dist/plugins/prebuilt-options-analysis.d.ts +40 -0
  492. package/dist/plugins/prebuilt-options-analysis.d.ts.map +1 -0
  493. package/dist/plugins/prebuilt.d.ts +98 -0
  494. package/dist/plugins/prebuilt.d.ts.map +1 -0
  495. package/dist/plugins/prerender-sugar.d.ts +39 -0
  496. package/dist/plugins/prerender-sugar.d.ts.map +1 -0
  497. package/dist/plugins/react-resolve.d.ts +49 -0
  498. package/dist/plugins/react-resolve.d.ts.map +1 -0
  499. package/dist/plugins/request-dep/analysis.d.ts +106 -0
  500. package/dist/plugins/request-dep/analysis.d.ts.map +1 -0
  501. package/dist/plugins/request-dep/index.d.ts +75 -0
  502. package/dist/plugins/request-dep/index.d.ts.map +1 -0
  503. package/dist/plugins/request-dep/origin.d.ts +61 -0
  504. package/dist/plugins/request-dep/origin.d.ts.map +1 -0
  505. package/dist/plugins/routing.d.ts +1 -1
  506. package/dist/plugins/routing.d.ts.map +1 -1
  507. package/dist/plugins/rsc-assets-manifest.d.ts +33 -0
  508. package/dist/plugins/rsc-assets-manifest.d.ts.map +1 -0
  509. package/dist/plugins/server-bundle.d.ts.map +1 -1
  510. package/dist/plugins/shims.d.ts +29 -5
  511. package/dist/plugins/shims.d.ts.map +1 -1
  512. package/dist/plugins/static-build.d.ts +246 -26
  513. package/dist/plugins/static-build.d.ts.map +1 -1
  514. package/dist/routing/codegen-shared.d.ts +26 -0
  515. package/dist/routing/codegen-shared.d.ts.map +1 -0
  516. package/dist/routing/codegen-types.d.ts +61 -0
  517. package/dist/routing/codegen-types.d.ts.map +1 -0
  518. package/dist/routing/codegen-write.d.ts +42 -0
  519. package/dist/routing/codegen-write.d.ts.map +1 -0
  520. package/dist/routing/codegen.d.ts +8 -3
  521. package/dist/routing/codegen.d.ts.map +1 -1
  522. package/dist/routing/collision-probe.d.ts +104 -0
  523. package/dist/routing/collision-probe.d.ts.map +1 -0
  524. package/dist/routing/collision-spaces.d.ts +26 -0
  525. package/dist/routing/collision-spaces.d.ts.map +1 -0
  526. package/dist/routing/convention-lint.d.ts +55 -0
  527. package/dist/routing/convention-lint.d.ts.map +1 -0
  528. package/dist/routing/export-detect.d.ts +91 -0
  529. package/dist/routing/export-detect.d.ts.map +1 -0
  530. package/dist/routing/file-cache.d.ts +8 -0
  531. package/dist/routing/file-cache.d.ts.map +1 -0
  532. package/dist/routing/index.d.ts +13 -7
  533. package/dist/routing/index.d.ts.map +1 -1
  534. package/dist/routing/index.js +5 -2
  535. package/dist/routing/interception-overlap.d.ts +35 -0
  536. package/dist/routing/interception-overlap.d.ts.map +1 -0
  537. package/dist/routing/interception.d.ts +234 -10
  538. package/dist/routing/interception.d.ts.map +1 -1
  539. package/dist/routing/link-codegen.d.ts +105 -0
  540. package/dist/routing/link-codegen.d.ts.map +1 -0
  541. package/dist/routing/manifest-codegen.d.ts +26 -0
  542. package/dist/routing/manifest-codegen.d.ts.map +1 -0
  543. package/dist/routing/scanner.d.ts +14 -10
  544. package/dist/routing/scanner.d.ts.map +1 -1
  545. package/dist/routing/schema-validation.d.ts +35 -0
  546. package/dist/routing/schema-validation.d.ts.map +1 -0
  547. package/dist/routing/segment-classify.d.ts +114 -0
  548. package/dist/routing/segment-classify.d.ts.map +1 -0
  549. package/dist/routing/segment-codegen.d.ts +38 -0
  550. package/dist/routing/segment-codegen.d.ts.map +1 -0
  551. package/dist/routing/segment-keys.d.ts +118 -0
  552. package/dist/routing/segment-keys.d.ts.map +1 -0
  553. package/dist/routing/slot-placement.d.ts +48 -0
  554. package/dist/routing/slot-placement.d.ts.map +1 -0
  555. package/dist/routing/types.d.ts +81 -26
  556. package/dist/routing/types.d.ts.map +1 -1
  557. package/dist/routing/walkers.d.ts +57 -0
  558. package/dist/routing/walkers.d.ts.map +1 -0
  559. package/dist/rsc-runtime/rsc.d.ts +1 -1
  560. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  561. package/dist/rsc-runtime/ssr.d.ts +14 -0
  562. package/dist/rsc-runtime/ssr.d.ts.map +1 -1
  563. package/dist/rsc-runtime/transforms.d.ts +13 -0
  564. package/dist/rsc-runtime/transforms.d.ts.map +1 -0
  565. package/dist/schema-bridge.d.ts +145 -0
  566. package/dist/schema-bridge.d.ts.map +1 -0
  567. package/dist/search-params/define.d.ts +262 -0
  568. package/dist/search-params/define.d.ts.map +1 -0
  569. package/dist/search-params/index.d.ts +3 -7
  570. package/dist/search-params/index.d.ts.map +1 -1
  571. package/dist/search-params/index.js +179 -360
  572. package/dist/search-params/index.js.map +1 -1
  573. package/dist/search-params/parse-total.d.ts +81 -0
  574. package/dist/search-params/parse-total.d.ts.map +1 -0
  575. package/dist/search-params/serialize-equal.d.ts +9 -0
  576. package/dist/search-params/serialize-equal.d.ts.map +1 -0
  577. package/dist/search-params/wrappers.d.ts +76 -0
  578. package/dist/search-params/wrappers.d.ts.map +1 -0
  579. package/dist/server/access-gate.d.ts +98 -10
  580. package/dist/server/access-gate.d.ts.map +1 -1
  581. package/dist/server/action-client.d.ts +44 -9
  582. package/dist/server/action-client.d.ts.map +1 -1
  583. package/dist/server/action-encryption.d.ts +76 -0
  584. package/dist/server/action-encryption.d.ts.map +1 -0
  585. package/dist/server/action-handler.d.ts +40 -5
  586. package/dist/server/action-handler.d.ts.map +1 -1
  587. package/dist/server/actions.d.ts +78 -40
  588. package/dist/server/actions.d.ts.map +1 -1
  589. package/dist/server/als-registry.d.ts +166 -16
  590. package/dist/server/als-registry.d.ts.map +1 -1
  591. package/dist/server/body-limits.d.ts.map +1 -1
  592. package/dist/server/build-manifest.d.ts +25 -30
  593. package/dist/server/build-manifest.d.ts.map +1 -1
  594. package/dist/server/canonicalize.d.ts +10 -0
  595. package/dist/server/canonicalize.d.ts.map +1 -1
  596. package/dist/server/chain-url-parts.d.ts +82 -0
  597. package/dist/server/chain-url-parts.d.ts.map +1 -0
  598. package/dist/server/children-interception.d.ts +49 -0
  599. package/dist/server/children-interception.d.ts.map +1 -0
  600. package/dist/server/coalesce-tick.d.ts +40 -0
  601. package/dist/server/coalesce-tick.d.ts.map +1 -0
  602. package/dist/server/compress.d.ts.map +1 -1
  603. package/dist/server/cookie-context.d.ts +149 -0
  604. package/dist/server/cookie-context.d.ts.map +1 -0
  605. package/dist/server/cookie-parsing.d.ts +65 -0
  606. package/dist/server/cookie-parsing.d.ts.map +1 -0
  607. package/dist/server/csp-nonce.d.ts +36 -0
  608. package/dist/server/csp-nonce.d.ts.map +1 -0
  609. package/dist/server/csrf.d.ts +58 -15
  610. package/dist/server/csrf.d.ts.map +1 -1
  611. package/dist/server/debug.d.ts +0 -1
  612. package/dist/server/debug.d.ts.map +1 -1
  613. package/dist/server/default-logger.d.ts +23 -0
  614. package/dist/server/default-logger.d.ts.map +1 -0
  615. package/dist/server/default-status-page.d.ts +45 -0
  616. package/dist/server/default-status-page.d.ts.map +1 -0
  617. package/dist/server/deny-boundary.d.ts +257 -0
  618. package/dist/server/deny-boundary.d.ts.map +1 -0
  619. package/dist/server/deny-renderer.d.ts +21 -15
  620. package/dist/server/deny-renderer.d.ts.map +1 -1
  621. package/dist/server/early-hints-sender.d.ts.map +1 -1
  622. package/dist/server/early-hints.d.ts +14 -12
  623. package/dist/server/early-hints.d.ts.map +1 -1
  624. package/dist/server/error-boundary-wrapper.d.ts +102 -10
  625. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  626. package/dist/server/error-response-headers.d.ts +3 -0
  627. package/dist/server/error-response-headers.d.ts.map +1 -0
  628. package/dist/server/fallback-error.d.ts +19 -7
  629. package/dist/server/fallback-error.d.ts.map +1 -1
  630. package/dist/server/flight-injection-state.d.ts +66 -0
  631. package/dist/server/flight-injection-state.d.ts.map +1 -0
  632. package/dist/server/flight-scripts.d.ts +45 -0
  633. package/dist/server/flight-scripts.d.ts.map +1 -0
  634. package/dist/server/form-data.d.ts +41 -0
  635. package/dist/server/form-data.d.ts.map +1 -1
  636. package/dist/server/form-flash.d.ts +2 -2
  637. package/dist/server/head-response.d.ts +35 -0
  638. package/dist/server/head-response.d.ts.map +1 -0
  639. package/dist/server/html-injector-core.d.ts +257 -0
  640. package/dist/server/html-injector-core.d.ts.map +1 -0
  641. package/dist/server/html-injectors.d.ts +84 -42
  642. package/dist/server/html-injectors.d.ts.map +1 -1
  643. package/dist/server/index.d.ts +17 -54
  644. package/dist/server/index.d.ts.map +1 -1
  645. package/dist/server/index.js +195 -2922
  646. package/dist/server/index.js.map +1 -1
  647. package/dist/server/instrumentation.d.ts +11 -4
  648. package/dist/server/instrumentation.d.ts.map +1 -1
  649. package/dist/server/internal.d.ts +47 -0
  650. package/dist/server/internal.d.ts.map +1 -0
  651. package/dist/server/internal.js +4645 -0
  652. package/dist/server/internal.js.map +1 -0
  653. package/dist/server/json-status-response.d.ts +34 -0
  654. package/dist/server/json-status-response.d.ts.map +1 -0
  655. package/dist/server/logger.d.ts +78 -7
  656. package/dist/server/logger.d.ts.map +1 -1
  657. package/dist/server/metadata-collector.d.ts +73 -0
  658. package/dist/server/metadata-collector.d.ts.map +1 -0
  659. package/dist/server/metadata-platform.d.ts +2 -2
  660. package/dist/server/metadata-render.d.ts +2 -2
  661. package/dist/server/metadata-render.d.ts.map +1 -1
  662. package/dist/server/metadata-routes.d.ts +71 -11
  663. package/dist/server/metadata-routes.d.ts.map +1 -1
  664. package/dist/server/metadata-social.d.ts +2 -2
  665. package/dist/server/metadata.d.ts +10 -2
  666. package/dist/server/metadata.d.ts.map +1 -1
  667. package/dist/server/middleware-runner.d.ts +20 -5
  668. package/dist/server/middleware-runner.d.ts.map +1 -1
  669. package/dist/server/node-stream-transforms.d.ts +105 -0
  670. package/dist/server/node-stream-transforms.d.ts.map +1 -0
  671. package/dist/server/nuqs-ssr-provider.d.ts +7 -3
  672. package/dist/server/nuqs-ssr-provider.d.ts.map +1 -1
  673. package/dist/server/param-coercion.d.ts +83 -0
  674. package/dist/server/param-coercion.d.ts.map +1 -0
  675. package/dist/server/pipeline-helpers.d.ts +193 -0
  676. package/dist/server/pipeline-helpers.d.ts.map +1 -0
  677. package/dist/server/pipeline-interception.d.ts +70 -9
  678. package/dist/server/pipeline-interception.d.ts.map +1 -1
  679. package/dist/server/pipeline-metadata.d.ts +7 -1
  680. package/dist/server/pipeline-metadata.d.ts.map +1 -1
  681. package/dist/server/pipeline-outcome.d.ts +59 -0
  682. package/dist/server/pipeline-outcome.d.ts.map +1 -0
  683. package/dist/server/pipeline-phases.d.ts +70 -0
  684. package/dist/server/pipeline-phases.d.ts.map +1 -0
  685. package/dist/server/pipeline.d.ts +147 -23
  686. package/dist/server/pipeline.d.ts.map +1 -1
  687. package/dist/server/port-resolution.d.ts +31 -0
  688. package/dist/server/port-resolution.d.ts.map +1 -0
  689. package/dist/server/prebuilt/cache-key.d.ts +63 -0
  690. package/dist/server/prebuilt/cache-key.d.ts.map +1 -0
  691. package/dist/server/prebuilt/capture-render.d.ts +25 -0
  692. package/dist/server/prebuilt/capture-render.d.ts.map +1 -0
  693. package/dist/server/prebuilt/capture-routes.d.ts +37 -0
  694. package/dist/server/prebuilt/capture-routes.d.ts.map +1 -0
  695. package/dist/server/prebuilt/capture-state.d.ts +101 -0
  696. package/dist/server/prebuilt/capture-state.d.ts.map +1 -0
  697. package/dist/server/prebuilt/key-discipline.d.ts +52 -0
  698. package/dist/server/prebuilt/key-discipline.d.ts.map +1 -0
  699. package/dist/server/prebuilt/lifetime.d.ts +38 -0
  700. package/dist/server/prebuilt/lifetime.d.ts.map +1 -0
  701. package/dist/server/prebuilt/manifest-tag-index.d.ts +27 -0
  702. package/dist/server/prebuilt/manifest-tag-index.d.ts.map +1 -0
  703. package/dist/server/prebuilt/overlay.d.ts +100 -0
  704. package/dist/server/prebuilt/overlay.d.ts.map +1 -0
  705. package/dist/server/prebuilt/payload-source.d.ts +79 -0
  706. package/dist/server/prebuilt/payload-source.d.ts.map +1 -0
  707. package/dist/server/prebuilt/slots.d.ts +106 -0
  708. package/dist/server/prebuilt/slots.d.ts.map +1 -0
  709. package/dist/server/prebuilt/synthetic-store.d.ts +67 -0
  710. package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -0
  711. package/dist/server/prebuilt-builder.d.ts +121 -0
  712. package/dist/server/prebuilt-builder.d.ts.map +1 -0
  713. package/dist/server/prebuilt-runtime.d.ts +111 -0
  714. package/dist/server/prebuilt-runtime.d.ts.map +1 -0
  715. package/dist/server/primitives.d.ts +101 -90
  716. package/dist/server/primitives.d.ts.map +1 -1
  717. package/dist/server/publish-params.d.ts +33 -0
  718. package/dist/server/publish-params.d.ts.map +1 -0
  719. package/dist/server/react-cache-bridge.d.ts +59 -0
  720. package/dist/server/react-cache-bridge.d.ts.map +1 -0
  721. package/dist/server/react-cache-scope.d.ts +49 -0
  722. package/dist/server/react-cache-scope.d.ts.map +1 -0
  723. package/dist/server/render-timeout.d.ts +66 -0
  724. package/dist/server/render-timeout.d.ts.map +1 -0
  725. package/dist/server/render-utils.d.ts +35 -0
  726. package/dist/server/render-utils.d.ts.map +1 -0
  727. package/dist/server/request-context.d.ts +128 -103
  728. package/dist/server/request-context.d.ts.map +1 -1
  729. package/dist/server/request-dep.d.ts +15 -0
  730. package/dist/server/request-dep.d.ts.map +1 -0
  731. package/dist/server/response-cache-policy.d.ts +3 -0
  732. package/dist/server/response-cache-policy.d.ts.map +1 -0
  733. package/dist/server/route-element-builder.d.ts +57 -30
  734. package/dist/server/route-element-builder.d.ts.map +1 -1
  735. package/dist/server/route-element-helpers.d.ts +12 -0
  736. package/dist/server/route-element-helpers.d.ts.map +1 -0
  737. package/dist/server/route-handler.d.ts +1 -1
  738. package/dist/server/route-handler.d.ts.map +1 -1
  739. package/dist/server/route-matcher.d.ts +55 -38
  740. package/dist/server/route-matcher.d.ts.map +1 -1
  741. package/dist/server/route-module-loader.d.ts +37 -0
  742. package/dist/server/route-module-loader.d.ts.map +1 -0
  743. package/dist/server/rsc-cache-key-guard.d.ts +78 -0
  744. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
  745. package/dist/server/rsc-entry/action-dispatcher.d.ts +67 -0
  746. package/dist/server/rsc-entry/action-dispatcher.d.ts.map +1 -0
  747. package/dist/server/rsc-entry/action-middleware-runner.d.ts +66 -0
  748. package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -0
  749. package/dist/server/rsc-entry/api-handler.d.ts +19 -3
  750. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  751. package/dist/server/rsc-entry/deny-fallback.d.ts +55 -0
  752. package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -0
  753. package/dist/server/rsc-entry/error-renderer.d.ts +35 -21
  754. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  755. package/dist/server/rsc-entry/helpers.d.ts +14 -59
  756. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  757. package/dist/server/rsc-entry/index.d.ts +30 -6
  758. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  759. package/dist/server/rsc-entry/render-route.d.ts +50 -0
  760. package/dist/server/rsc-entry/render-route.d.ts.map +1 -0
  761. package/dist/server/rsc-entry/render-signals.d.ts +65 -0
  762. package/dist/server/rsc-entry/render-signals.d.ts.map +1 -0
  763. package/dist/server/rsc-entry/revalidate-renderer.d.ts +98 -0
  764. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -0
  765. package/dist/server/rsc-entry/rsc-payload.d.ts +30 -6
  766. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  767. package/dist/server/rsc-entry/rsc-stream.d.ts +12 -30
  768. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  769. package/dist/server/rsc-entry/ssr-bridge.d.ts +6 -2
  770. package/dist/server/rsc-entry/ssr-bridge.d.ts.map +1 -1
  771. package/dist/server/rsc-entry/ssr-renderer.d.ts +44 -7
  772. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  773. package/dist/server/rsc-error-envelope.d.ts +11 -0
  774. package/dist/server/rsc-error-envelope.d.ts.map +1 -0
  775. package/dist/server/safe-load.d.ts +39 -0
  776. package/dist/server/safe-load.d.ts.map +1 -0
  777. package/dist/server/sensitive-fields.d.ts +74 -0
  778. package/dist/server/sensitive-fields.d.ts.map +1 -0
  779. package/dist/server/signal-identity.d.ts +43 -0
  780. package/dist/server/signal-identity.d.ts.map +1 -0
  781. package/dist/server/sitemap-generator.d.ts +130 -0
  782. package/dist/server/sitemap-generator.d.ts.map +1 -0
  783. package/dist/server/sitemap-handler.d.ts +22 -0
  784. package/dist/server/sitemap-handler.d.ts.map +1 -0
  785. package/dist/server/skippable-prefix.d.ts +29 -0
  786. package/dist/server/skippable-prefix.d.ts.map +1 -0
  787. package/dist/server/slot-interception.d.ts +80 -0
  788. package/dist/server/slot-interception.d.ts.map +1 -0
  789. package/dist/server/slot-resolver.d.ts +138 -7
  790. package/dist/server/slot-resolver.d.ts.map +1 -1
  791. package/dist/server/slot-subtree-contain.d.ts +54 -0
  792. package/dist/server/slot-subtree-contain.d.ts.map +1 -0
  793. package/dist/server/ssr-bridge-types.d.ts +198 -0
  794. package/dist/server/ssr-bridge-types.d.ts.map +1 -0
  795. package/dist/server/ssr-entry.d.ts +8 -47
  796. package/dist/server/ssr-entry.d.ts.map +1 -1
  797. package/dist/server/ssr-render.d.ts +60 -27
  798. package/dist/server/ssr-render.d.ts.map +1 -1
  799. package/dist/server/ssr-wrappers.d.ts +93 -0
  800. package/dist/server/ssr-wrappers.d.ts.map +1 -0
  801. package/dist/server/state-tree-diff.d.ts +44 -9
  802. package/dist/server/state-tree-diff.d.ts.map +1 -1
  803. package/dist/server/static-generator.d.ts +112 -0
  804. package/dist/server/static-generator.d.ts.map +1 -0
  805. package/dist/server/static-not-found.d.ts +52 -0
  806. package/dist/server/static-not-found.d.ts.map +1 -0
  807. package/dist/server/status-code-resolver.d.ts +26 -15
  808. package/dist/server/status-code-resolver.d.ts.map +1 -1
  809. package/dist/server/stream-utils.d.ts +59 -0
  810. package/dist/server/stream-utils.d.ts.map +1 -0
  811. package/dist/server/tracing.d.ts +57 -26
  812. package/dist/server/tracing.d.ts.map +1 -1
  813. package/dist/server/tree-match.d.ts +59 -0
  814. package/dist/server/tree-match.d.ts.map +1 -0
  815. package/dist/server/types.d.ts +89 -8
  816. package/dist/server/types.d.ts.map +1 -1
  817. package/dist/server/utils/element-type.d.ts +50 -0
  818. package/dist/server/utils/element-type.d.ts.map +1 -0
  819. package/dist/server/utils/escape-html.d.ts +14 -0
  820. package/dist/server/utils/escape-html.d.ts.map +1 -0
  821. package/dist/server/version-skew.d.ts +61 -0
  822. package/dist/server/version-skew.d.ts.map +1 -0
  823. package/dist/server/workers-cache-bridge.d.ts +26 -0
  824. package/dist/server/workers-cache-bridge.d.ts.map +1 -0
  825. package/dist/shared/als-slots.d.ts +42 -0
  826. package/dist/shared/als-slots.d.ts.map +1 -0
  827. package/dist/shared/base-path.d.ts +56 -0
  828. package/dist/shared/base-path.d.ts.map +1 -0
  829. package/dist/shared/csrf-reject.d.ts +16 -0
  830. package/dist/shared/csrf-reject.d.ts.map +1 -0
  831. package/dist/shared/error-digest.d.ts +28 -0
  832. package/dist/shared/error-digest.d.ts.map +1 -0
  833. package/dist/shared/href-validation.d.ts +49 -0
  834. package/dist/shared/href-validation.d.ts.map +1 -0
  835. package/dist/shared/merge-search-params.d.ts +22 -0
  836. package/dist/shared/merge-search-params.d.ts.map +1 -0
  837. package/dist/shared/param-value.d.ts +104 -0
  838. package/dist/shared/param-value.d.ts.map +1 -0
  839. package/dist/shared/payload-root.d.ts +110 -0
  840. package/dist/shared/payload-root.d.ts.map +1 -0
  841. package/dist/shared/redirect-type.d.ts +17 -0
  842. package/dist/shared/redirect-type.d.ts.map +1 -0
  843. package/dist/shared/rsc-cache-key.d.ts +163 -0
  844. package/dist/shared/rsc-cache-key.d.ts.map +1 -0
  845. package/dist/shared/rsc-error-envelope.d.ts +16 -0
  846. package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
  847. package/dist/shared/rsc-manifest.d.ts +28 -0
  848. package/dist/shared/rsc-manifest.d.ts.map +1 -0
  849. package/dist/shared/rsc-media-type.d.ts +52 -0
  850. package/dist/shared/rsc-media-type.d.ts.map +1 -0
  851. package/dist/shared/rsc-payload-path.d.ts +48 -0
  852. package/dist/shared/rsc-payload-path.d.ts.map +1 -0
  853. package/dist/shared/segment-info.d.ts +66 -0
  854. package/dist/shared/segment-info.d.ts.map +1 -0
  855. package/dist/shared/slot-params.d.ts +52 -0
  856. package/dist/shared/slot-params.d.ts.map +1 -0
  857. package/dist/shared/static-platform-files.d.ts +71 -0
  858. package/dist/shared/static-platform-files.d.ts.map +1 -0
  859. package/dist/shared/status-reason-phrase.d.ts +26 -0
  860. package/dist/shared/status-reason-phrase.d.ts.map +1 -0
  861. package/dist/shims/font-google.d.ts +1 -1
  862. package/dist/shims/font-google.d.ts.map +1 -1
  863. package/dist/shims/font-google.js +42 -0
  864. package/dist/shims/font-google.js.map +1 -0
  865. package/dist/shims/font-local.d.ts +26 -0
  866. package/dist/shims/font-local.d.ts.map +1 -0
  867. package/dist/shims/font-local.js +20 -0
  868. package/dist/shims/font-local.js.map +1 -0
  869. package/dist/shims/headers.d.ts +53 -1
  870. package/dist/shims/headers.d.ts.map +1 -1
  871. package/dist/shims/image.d.ts +120 -120
  872. package/dist/shims/image.d.ts.map +1 -1
  873. package/dist/shims/navigation-client.d.ts +25 -12
  874. package/dist/shims/navigation-client.d.ts.map +1 -1
  875. package/dist/shims/navigation-rsc.d.ts +21 -0
  876. package/dist/shims/navigation-rsc.d.ts.map +1 -0
  877. package/dist/shims/navigation.d.ts +10 -5
  878. package/dist/shims/navigation.d.ts.map +1 -1
  879. package/dist/utils/state-machine.d.ts +80 -0
  880. package/dist/utils/state-machine.d.ts.map +1 -0
  881. package/dist/utils/write-file-atomic.d.ts +17 -0
  882. package/dist/utils/write-file-atomic.d.ts.map +1 -0
  883. package/docs/api/30-api-server.mdx +239 -0
  884. package/docs/api/31-api-client.mdx +170 -0
  885. package/docs/api/32-api-cache.mdx +339 -0
  886. package/docs/api/33-api-search-params.mdx +176 -0
  887. package/docs/api/34-api-config.mdx +359 -0
  888. package/docs/api/35-api-typescript.mdx +91 -0
  889. package/docs/api/36-cli.mdx +202 -0
  890. package/docs/learn/00-introduction.mdx +109 -0
  891. package/docs/learn/01-quick-start.mdx +94 -0
  892. package/docs/learn/02-pages-and-layouts.mdx +147 -0
  893. package/docs/learn/03-fetching-data.mdx +227 -0
  894. package/docs/learn/04-access-control.mdx +115 -0
  895. package/docs/learn/05-the-flush-point.mdx +175 -0
  896. package/docs/learn/06-typed-params.mdx +275 -0
  897. package/docs/learn/07-typed-routes.mdx +108 -0
  898. package/docs/learn/08-forms-and-actions.mdx +146 -0
  899. package/docs/learn/09-streaming.mdx +140 -0
  900. package/docs/learn/10-caching.mdx +270 -0
  901. package/docs/learn/11-middleware.mdx +164 -0
  902. package/docs/learn/12-error-handling.mdx +192 -0
  903. package/docs/learn/13-deploying.mdx +287 -0
  904. package/llms.txt +55 -0
  905. package/package.json +120 -29
  906. package/src/adapters/build-output-helper.ts +353 -0
  907. package/src/adapters/canonicalize-module.ts +29 -0
  908. package/src/adapters/cloudflare-dev.ts +183 -0
  909. package/src/adapters/cloudflare-kv-cache.ts +207 -0
  910. package/src/adapters/cloudflare.ts +809 -150
  911. package/src/adapters/compress-module.ts +110 -86
  912. package/src/adapters/fs-identity.ts +155 -0
  913. package/src/adapters/nitro-presets.ts +124 -0
  914. package/src/adapters/nitro-preview.ts +476 -0
  915. package/src/adapters/nitro.ts +317 -457
  916. package/src/adapters/raw-imports.d.ts +5 -0
  917. package/src/adapters/shared.ts +283 -0
  918. package/src/adapters/types.ts +127 -17
  919. package/src/adapters/wrangler.d.ts +7 -0
  920. package/src/analyze/chains.ts +411 -0
  921. package/src/analyze/classify.ts +259 -0
  922. package/src/analyze/crawl-entry.ts +68 -0
  923. package/src/analyze/crawl.ts +500 -0
  924. package/src/analyze/graph-cache.ts +331 -0
  925. package/src/analyze/graph-command.ts +762 -0
  926. package/src/analyze/live-graph.ts +232 -0
  927. package/src/analyze/poison-rules.ts +165 -0
  928. package/src/analyze/poison-scan.ts +159 -0
  929. package/src/analyze/scan.ts +149 -0
  930. package/src/cache/cache-api.ts +107 -0
  931. package/src/cache/fast-hash.ts +5 -0
  932. package/src/cache/handler-store.ts +76 -0
  933. package/src/cache/index.ts +115 -22
  934. package/src/cache/invalidation-epoch.ts +93 -0
  935. package/src/cache/json-lossy-check.ts +69 -0
  936. package/src/cache/redis-handler.ts +198 -20
  937. package/src/cache/singleflight.ts +72 -6
  938. package/src/cache/sizeof.ts +35 -0
  939. package/src/cache/stable-stringify.ts +65 -2
  940. package/src/cache/store.ts +71 -0
  941. package/src/cache/stores/cloudflare-kv.ts +52 -0
  942. package/src/cache/stores/memory.ts +125 -0
  943. package/src/cache/stores/redis.ts +97 -0
  944. package/src/cache/stores/vercel.ts +59 -0
  945. package/src/cache/tag-aware-handler.ts +254 -0
  946. package/src/cache/timber-cache.ts +165 -40
  947. package/src/cdn/cloudflare-purge.ts +50 -0
  948. package/src/cdn/fastly-purge.ts +63 -0
  949. package/src/cdn/headers.ts +153 -0
  950. package/src/cdn/index.ts +23 -0
  951. package/src/cdn/purge-store.ts +34 -0
  952. package/src/cdn/workers-cache-purge.ts +34 -0
  953. package/src/cli-check.ts +417 -0
  954. package/src/cli-context.ts +119 -0
  955. package/src/cli-schema-sync.ts +401 -0
  956. package/src/cli.ts +330 -78
  957. package/src/client/browser-dev.ts +192 -5
  958. package/src/client/browser-entry/action-dispatch.ts +237 -0
  959. package/src/client/browser-entry/action-queue.ts +152 -0
  960. package/src/client/browser-entry/hmr.ts +70 -0
  961. package/src/client/browser-entry/hydrate.ts +143 -0
  962. package/src/client/browser-entry/index.ts +120 -0
  963. package/src/client/browser-entry/post-hydration.ts +99 -0
  964. package/src/client/browser-entry/router-init.ts +406 -0
  965. package/src/client/browser-entry/rsc-stream.ts +145 -0
  966. package/src/client/browser-entry/scroll.ts +50 -0
  967. package/src/client/deny-last-resort.tsx +54 -0
  968. package/src/client/error-boundary.tsx +279 -61
  969. package/src/client/error-reconstituter.tsx +84 -0
  970. package/src/client/form.tsx +36 -32
  971. package/src/client/global-context.ts +31 -0
  972. package/src/client/history.ts +74 -11
  973. package/src/client/index.ts +89 -53
  974. package/src/client/internal.ts +54 -0
  975. package/src/client/link.tsx +468 -151
  976. package/src/client/location-search.ts +15 -0
  977. package/src/client/navigation-api.ts +380 -0
  978. package/src/client/navigation-commit.ts +318 -0
  979. package/src/client/navigation-context.ts +67 -134
  980. package/src/client/navigation-root.tsx +236 -0
  981. package/src/client/navigation-transition.ts +337 -0
  982. package/src/client/nuqs-adapter.tsx +21 -9
  983. package/src/client/params-context.ts +187 -0
  984. package/src/client/react-root.ts +82 -0
  985. package/src/client/router-effects.ts +235 -0
  986. package/src/client/router-lifecycle.ts +354 -0
  987. package/src/client/router-pipeline.ts +458 -0
  988. package/src/client/router-ref.ts +3 -3
  989. package/src/client/router-skew.ts +30 -0
  990. package/src/client/router-types.ts +370 -0
  991. package/src/client/router-url.ts +32 -0
  992. package/src/client/router.ts +491 -424
  993. package/src/client/rsc-fetch.ts +510 -109
  994. package/src/client/segment-cache.ts +362 -41
  995. package/src/client/segment-context.ts +7 -4
  996. package/src/client/segment-outlet.tsx +96 -0
  997. package/src/client/segment-update-context.ts +23 -0
  998. package/src/client/shallow-url.ts +22 -0
  999. package/src/client/slot-content-cache-context.ts +43 -0
  1000. package/src/client/slot-context.ts +50 -0
  1001. package/src/client/slot-outlet.tsx +22 -0
  1002. package/src/client/slot-provider.tsx +32 -0
  1003. package/src/client/ssr-data.ts +27 -67
  1004. package/src/client/stale-client.ts +191 -0
  1005. package/src/client/state.ts +31 -24
  1006. package/src/client/status-page-marker.tsx +32 -0
  1007. package/src/client/top-loader.tsx +13 -10
  1008. package/src/client/unload-guard.ts +35 -3
  1009. package/src/client/use-cookie.ts +70 -18
  1010. package/src/client/use-link-status.ts +7 -7
  1011. package/src/client/use-pathname.ts +22 -31
  1012. package/src/client/use-pending-navigation.ts +40 -0
  1013. package/src/client/use-query-states.ts +206 -36
  1014. package/src/client/use-router.ts +31 -9
  1015. package/src/client/use-search-params.ts +38 -49
  1016. package/src/client/use-segment-params.ts +69 -0
  1017. package/src/client/use-selected-layout-segment.ts +2 -2
  1018. package/src/codec.ts +149 -0
  1019. package/src/config-types.ts +323 -0
  1020. package/src/config-validation.ts +464 -0
  1021. package/src/cookies/define-cookie.ts +222 -53
  1022. package/src/cookies/index.ts +13 -2
  1023. package/src/cookies/json-cookie.ts +105 -0
  1024. package/src/cookies/validation.ts +192 -0
  1025. package/src/dev-tools/404-page.ts +286 -0
  1026. package/src/dev-tools/browser-logs.ts +55 -0
  1027. package/src/dev-tools/dev-page-shell.ts +278 -0
  1028. package/src/dev-tools/error-page.ts +331 -0
  1029. package/src/dev-tools/holding-server.ts +185 -0
  1030. package/src/dev-tools/hydration-overlay.ts +158 -0
  1031. package/src/dev-tools/index.ts +79 -0
  1032. package/src/dev-tools/instrumentation.ts +261 -0
  1033. package/src/{server/dev-logger.ts → dev-tools/logger.ts} +33 -51
  1034. package/src/{plugins/dev-logs.ts → dev-tools/logs.ts} +122 -26
  1035. package/src/{plugins/dev-error-overlay.ts → dev-tools/overlay.ts} +25 -80
  1036. package/src/dev-tools/stack-classifier.ts +75 -0
  1037. package/src/dev-tools/terminal.ts +182 -0
  1038. package/src/fonts/ast-walk.ts +235 -0
  1039. package/src/fonts/ast.ts +479 -128
  1040. package/src/fonts/bundle.ts +138 -0
  1041. package/src/fonts/css.ts +1 -1
  1042. package/src/fonts/dev-middleware.ts +85 -0
  1043. package/src/fonts/fallbacks.ts +0 -23
  1044. package/src/fonts/google.ts +101 -19
  1045. package/src/fonts/local.ts +12 -11
  1046. package/src/fonts/paths.ts +58 -0
  1047. package/src/fonts/pipeline.ts +348 -0
  1048. package/src/fonts/transform.ts +311 -0
  1049. package/src/fonts/types.ts +52 -1
  1050. package/src/fonts/virtual-modules.ts +179 -0
  1051. package/src/index.ts +513 -380
  1052. package/src/params/codec-registry.ts +34 -0
  1053. package/src/params/define-schema.ts +115 -0
  1054. package/src/params/index.ts +17 -0
  1055. package/src/params/resolve-schema.ts +84 -0
  1056. package/src/plugin-context.ts +502 -0
  1057. package/src/plugins/build-manifest.ts +204 -36
  1058. package/src/plugins/build-report.ts +299 -87
  1059. package/src/plugins/cache.ts +243 -0
  1060. package/src/plugins/callsite-ast.ts +472 -0
  1061. package/src/plugins/callsite-jsx.ts +243 -0
  1062. package/src/plugins/chunks.ts +74 -5
  1063. package/src/plugins/client-chunks.ts +185 -0
  1064. package/src/plugins/client-dep-entries.ts +78 -0
  1065. package/src/plugins/cloudflare-wasm.ts +141 -0
  1066. package/src/plugins/content.ts +31 -2
  1067. package/src/plugins/dev-discovery.ts +218 -0
  1068. package/src/plugins/dev-server.ts +360 -97
  1069. package/src/plugins/entries.ts +149 -16
  1070. package/src/plugins/fonts.ts +260 -657
  1071. package/src/plugins/graph-endpoint.ts +532 -0
  1072. package/src/plugins/mdx.ts +108 -35
  1073. package/src/plugins/post-build.ts +327 -0
  1074. package/src/plugins/prebuilt-capture.ts +213 -0
  1075. package/src/plugins/prebuilt-options-analysis.ts +175 -0
  1076. package/src/plugins/prebuilt.ts +670 -0
  1077. package/src/plugins/prerender-sugar.ts +316 -0
  1078. package/src/plugins/react-resolve.ts +149 -0
  1079. package/src/plugins/request-dep/analysis.ts +447 -0
  1080. package/src/plugins/request-dep/index.ts +376 -0
  1081. package/src/plugins/request-dep/origin.ts +310 -0
  1082. package/src/plugins/routing.ts +273 -277
  1083. package/src/plugins/rsc-assets-manifest.ts +251 -0
  1084. package/src/plugins/server-bundle.ts +99 -14
  1085. package/src/plugins/shims.ts +260 -97
  1086. package/src/plugins/static-build.ts +681 -71
  1087. package/src/react-canary.d.ts +2 -0
  1088. package/src/routing/codegen-shared.ts +39 -0
  1089. package/src/routing/codegen-types.ts +62 -0
  1090. package/src/routing/codegen-write.ts +163 -0
  1091. package/src/routing/codegen.ts +349 -232
  1092. package/src/routing/collision-probe.ts +438 -0
  1093. package/src/routing/collision-spaces.ts +281 -0
  1094. package/src/routing/convention-lint.ts +651 -0
  1095. package/src/routing/export-detect.ts +536 -0
  1096. package/src/routing/file-cache.ts +23 -0
  1097. package/src/routing/index.ts +16 -7
  1098. package/src/routing/interception-overlap.ts +141 -0
  1099. package/src/routing/interception.ts +1044 -67
  1100. package/src/routing/link-codegen.ts +280 -0
  1101. package/src/routing/manifest-codegen.ts +232 -0
  1102. package/src/routing/scanner.ts +417 -223
  1103. package/src/routing/schema-validation.ts +155 -0
  1104. package/src/routing/segment-classify.ts +280 -0
  1105. package/src/routing/segment-codegen.ts +99 -0
  1106. package/src/routing/segment-keys.ts +186 -0
  1107. package/src/routing/slot-placement.ts +97 -0
  1108. package/src/routing/types.ts +82 -26
  1109. package/src/routing/walkers.ts +98 -0
  1110. package/src/rsc-runtime/rsc.ts +9 -0
  1111. package/src/rsc-runtime/ssr.ts +51 -0
  1112. package/src/rsc-runtime/transforms.ts +13 -0
  1113. package/src/rsc-runtime/vendor-types.d.ts +27 -0
  1114. package/src/schema-bridge.ts +382 -0
  1115. package/src/search-params/define.ts +677 -0
  1116. package/src/search-params/index.ts +14 -20
  1117. package/src/search-params/parse-total.ts +91 -0
  1118. package/src/search-params/serialize-equal.ts +14 -0
  1119. package/src/search-params/wrappers.ts +150 -0
  1120. package/src/server/access-gate.tsx +195 -56
  1121. package/src/server/action-client.ts +126 -33
  1122. package/src/server/action-encryption.ts +144 -0
  1123. package/src/server/action-handler.ts +457 -111
  1124. package/src/server/actions.ts +172 -85
  1125. package/src/server/als-registry.ts +224 -25
  1126. package/src/server/body-limits.ts +10 -2
  1127. package/src/server/build-manifest.ts +33 -57
  1128. package/src/server/canonicalize.ts +6 -3
  1129. package/src/server/chain-url-parts.ts +163 -0
  1130. package/src/server/children-interception.ts +116 -0
  1131. package/src/server/coalesce-tick.ts +75 -0
  1132. package/src/server/compress.ts +43 -8
  1133. package/src/server/cookie-context.ts +454 -0
  1134. package/src/server/cookie-parsing.ts +163 -0
  1135. package/src/server/csp-nonce.ts +58 -0
  1136. package/src/server/csrf.ts +228 -35
  1137. package/src/server/debug.ts +0 -1
  1138. package/src/server/default-logger.ts +100 -0
  1139. package/src/server/default-status-page.ts +88 -0
  1140. package/src/server/deny-boundary.ts +527 -0
  1141. package/src/server/deny-renderer.ts +201 -179
  1142. package/src/server/early-hints-sender.ts +4 -3
  1143. package/src/server/early-hints.ts +39 -29
  1144. package/src/server/error-boundary-wrapper.ts +160 -44
  1145. package/src/server/error-response-headers.ts +18 -0
  1146. package/src/server/fallback-error.ts +88 -104
  1147. package/src/server/flight-injection-state.ts +113 -0
  1148. package/src/server/flight-scripts.ts +67 -0
  1149. package/src/server/form-data.ts +124 -12
  1150. package/src/server/form-flash.ts +3 -3
  1151. package/src/server/head-response.ts +54 -0
  1152. package/src/server/html-injector-core.ts +512 -0
  1153. package/src/server/html-injectors.ts +270 -255
  1154. package/src/server/index.ts +36 -192
  1155. package/src/server/instrumentation.ts +11 -4
  1156. package/src/server/internal.ts +168 -0
  1157. package/src/server/json-status-response.ts +105 -0
  1158. package/src/server/logger.ts +126 -38
  1159. package/src/server/metadata-collector.ts +177 -0
  1160. package/src/server/metadata-platform.ts +2 -2
  1161. package/src/server/metadata-render.ts +30 -8
  1162. package/src/server/metadata-routes.ts +220 -24
  1163. package/src/server/metadata-social.ts +2 -2
  1164. package/src/server/metadata.ts +23 -2
  1165. package/src/server/middleware-runner.ts +32 -5
  1166. package/src/server/node-stream-transforms.ts +323 -0
  1167. package/src/server/nuqs-ssr-provider.tsx +8 -7
  1168. package/src/server/param-coercion.ts +201 -0
  1169. package/src/server/pipeline-helpers.ts +485 -0
  1170. package/src/server/pipeline-interception.ts +241 -32
  1171. package/src/server/pipeline-metadata.ts +15 -1
  1172. package/src/server/pipeline-outcome.ts +290 -0
  1173. package/src/server/pipeline-phases.ts +496 -0
  1174. package/src/server/pipeline.ts +385 -427
  1175. package/src/server/port-resolution.ts +53 -0
  1176. package/src/server/prebuilt/cache-key.ts +299 -0
  1177. package/src/server/prebuilt/capture-render.ts +118 -0
  1178. package/src/server/prebuilt/capture-routes.ts +104 -0
  1179. package/src/server/prebuilt/capture-state.ts +196 -0
  1180. package/src/server/prebuilt/key-discipline.ts +122 -0
  1181. package/src/server/prebuilt/lifetime.ts +151 -0
  1182. package/src/server/prebuilt/manifest-tag-index.ts +46 -0
  1183. package/src/server/prebuilt/overlay.ts +256 -0
  1184. package/src/server/prebuilt/payload-source.ts +202 -0
  1185. package/src/server/prebuilt/slots.ts +202 -0
  1186. package/src/server/prebuilt/synthetic-store.ts +125 -0
  1187. package/src/server/prebuilt-builder.ts +514 -0
  1188. package/src/server/prebuilt-runtime.ts +813 -0
  1189. package/src/server/primitives.ts +153 -171
  1190. package/src/server/publish-params.ts +42 -0
  1191. package/src/server/react-cache-bridge.ts +124 -0
  1192. package/src/server/react-cache-scope.ts +104 -0
  1193. package/src/server/render-timeout.ts +128 -0
  1194. package/src/server/render-utils.ts +45 -0
  1195. package/src/server/request-context.ts +269 -332
  1196. package/src/server/request-dep.ts +37 -0
  1197. package/src/server/response-cache-policy.ts +45 -0
  1198. package/src/server/route-element-builder.ts +514 -355
  1199. package/src/server/route-element-helpers.ts +37 -0
  1200. package/src/server/route-handler.ts +67 -27
  1201. package/src/server/route-matcher.ts +238 -267
  1202. package/src/server/route-module-loader.ts +161 -0
  1203. package/src/server/rsc-cache-key-guard.ts +122 -0
  1204. package/src/server/rsc-entry/action-dispatcher.ts +142 -0
  1205. package/src/server/rsc-entry/action-middleware-runner.ts +130 -0
  1206. package/src/server/rsc-entry/api-handler.ts +49 -81
  1207. package/src/server/rsc-entry/deny-fallback.ts +151 -0
  1208. package/src/server/rsc-entry/error-renderer.ts +301 -113
  1209. package/src/server/rsc-entry/helpers.ts +90 -115
  1210. package/src/server/rsc-entry/index.ts +370 -350
  1211. package/src/server/rsc-entry/render-route.ts +229 -0
  1212. package/src/server/rsc-entry/render-signals.ts +108 -0
  1213. package/src/server/rsc-entry/revalidate-renderer.ts +349 -0
  1214. package/src/server/rsc-entry/rsc-payload.ts +192 -72
  1215. package/src/server/rsc-entry/rsc-stream.ts +128 -131
  1216. package/src/server/rsc-entry/ssr-bridge.ts +19 -6
  1217. package/src/server/rsc-entry/ssr-renderer.ts +300 -137
  1218. package/src/server/rsc-error-envelope.ts +18 -0
  1219. package/src/server/rsc-prop-warnings.ts +1 -1
  1220. package/src/server/safe-load.ts +53 -0
  1221. package/src/server/sensitive-fields.ts +230 -0
  1222. package/src/server/server-only-guard-noop.js +3 -0
  1223. package/src/server/server-timing.ts +1 -1
  1224. package/src/server/signal-identity.ts +112 -0
  1225. package/src/server/sitemap-generator.ts +337 -0
  1226. package/src/server/sitemap-handler.ts +157 -0
  1227. package/src/server/skippable-prefix.ts +172 -0
  1228. package/src/server/slot-interception.ts +168 -0
  1229. package/src/server/slot-resolver.ts +707 -300
  1230. package/src/server/slot-subtree-contain.ts +273 -0
  1231. package/src/server/ssr-bridge-types.ts +201 -0
  1232. package/src/server/ssr-entry.ts +225 -104
  1233. package/src/server/ssr-render.ts +354 -91
  1234. package/src/server/ssr-wrappers.tsx +162 -0
  1235. package/src/server/state-tree-diff.ts +85 -30
  1236. package/src/server/static-generator.ts +680 -0
  1237. package/src/server/static-not-found.ts +134 -0
  1238. package/src/server/status-code-resolver.ts +107 -148
  1239. package/src/server/stream-utils.ts +343 -0
  1240. package/src/server/tracing.ts +163 -68
  1241. package/src/server/tree-match.ts +494 -0
  1242. package/src/server/types.ts +96 -11
  1243. package/src/server/utils/element-type.ts +88 -0
  1244. package/src/server/utils/escape-html.ts +20 -0
  1245. package/src/server/version-skew.ts +104 -0
  1246. package/src/server/waituntil-bridge.ts +1 -1
  1247. package/src/server/workers-cache-bridge.ts +33 -0
  1248. package/src/shared/als-slots.ts +74 -0
  1249. package/src/shared/base-path.ts +80 -0
  1250. package/src/shared/csrf-reject.ts +15 -0
  1251. package/src/shared/error-digest.ts +46 -0
  1252. package/src/shared/href-validation.ts +159 -0
  1253. package/src/shared/merge-search-params.ts +55 -0
  1254. package/src/shared/param-value.ts +281 -0
  1255. package/src/shared/payload-root.ts +153 -0
  1256. package/src/shared/redirect-type.ts +17 -0
  1257. package/src/shared/rsc-cache-key.ts +269 -0
  1258. package/src/shared/rsc-error-envelope.ts +16 -0
  1259. package/src/shared/rsc-manifest.ts +29 -0
  1260. package/src/shared/rsc-media-type.ts +59 -0
  1261. package/src/shared/rsc-payload-path.ts +52 -0
  1262. package/src/shared/segment-info.ts +67 -0
  1263. package/src/shared/slot-params.ts +109 -0
  1264. package/src/shared/static-platform-files.ts +75 -0
  1265. package/src/shared/status-reason-phrase.ts +61 -0
  1266. package/src/shims/font-google.ts +1 -1
  1267. package/src/shims/font-local.ts +34 -0
  1268. package/src/shims/headers.ts +122 -1
  1269. package/src/shims/navigation-client.ts +36 -32
  1270. package/src/shims/navigation-rsc.ts +47 -0
  1271. package/src/shims/navigation.ts +10 -7
  1272. package/src/utils/state-machine.ts +111 -0
  1273. package/src/utils/write-file-atomic.ts +32 -0
  1274. package/dist/_chunks/als-registry-B7DbZ2hS.js +0 -41
  1275. package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
  1276. package/dist/_chunks/debug-B3Gypr3D.js +0 -108
  1277. package/dist/_chunks/debug-B3Gypr3D.js.map +0 -1
  1278. package/dist/_chunks/format-RyoGQL74.js +0 -164
  1279. package/dist/_chunks/format-RyoGQL74.js.map +0 -1
  1280. package/dist/_chunks/interception-BOoWmLUA.js +0 -676
  1281. package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
  1282. package/dist/_chunks/metadata-routes-Cjmvi3rQ.js.map +0 -1
  1283. package/dist/_chunks/request-context-BQUC8PHn.js +0 -330
  1284. package/dist/_chunks/request-context-BQUC8PHn.js.map +0 -1
  1285. package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
  1286. package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
  1287. package/dist/_chunks/tracing-CemImE6h.js +0 -187
  1288. package/dist/_chunks/tracing-CemImE6h.js.map +0 -1
  1289. package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
  1290. package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
  1291. package/dist/_chunks/use-query-states-D5KaffOK.js +0 -109
  1292. package/dist/_chunks/use-query-states-D5KaffOK.js.map +0 -1
  1293. package/dist/adapters/cloudflare.js.map +0 -1
  1294. package/dist/cache/index.js.map +0 -1
  1295. package/dist/cache/register-cached-function.d.ts +0 -17
  1296. package/dist/cache/register-cached-function.d.ts.map +0 -1
  1297. package/dist/client/browser-entry.d.ts +0 -21
  1298. package/dist/client/browser-entry.d.ts.map +0 -1
  1299. package/dist/client/error-boundary.js.map +0 -1
  1300. package/dist/client/head.d.ts +0 -16
  1301. package/dist/client/head.d.ts.map +0 -1
  1302. package/dist/client/link-status-provider.d.ts +0 -11
  1303. package/dist/client/link-status-provider.d.ts.map +0 -1
  1304. package/dist/client/segment-merger.d.ts +0 -119
  1305. package/dist/client/segment-merger.d.ts.map +0 -1
  1306. package/dist/client/stale-reload.d.ts +0 -44
  1307. package/dist/client/stale-reload.d.ts.map +0 -1
  1308. package/dist/client/transition-root.d.ts +0 -74
  1309. package/dist/client/transition-root.d.ts.map +0 -1
  1310. package/dist/client/types.d.ts +0 -14
  1311. package/dist/client/types.d.ts.map +0 -1
  1312. package/dist/client/use-navigation-pending.d.ts +0 -26
  1313. package/dist/client/use-navigation-pending.d.ts.map +0 -1
  1314. package/dist/client/use-params.d.ts +0 -92
  1315. package/dist/client/use-params.d.ts.map +0 -1
  1316. package/dist/content/index.d.ts +0 -11
  1317. package/dist/content/index.d.ts.map +0 -1
  1318. package/dist/content/index.js +0 -2
  1319. package/dist/plugins/adapter-build.d.ts +0 -18
  1320. package/dist/plugins/adapter-build.d.ts.map +0 -1
  1321. package/dist/plugins/cache-transform.d.ts +0 -36
  1322. package/dist/plugins/cache-transform.d.ts.map +0 -1
  1323. package/dist/plugins/dev-error-overlay.d.ts.map +0 -1
  1324. package/dist/plugins/dev-logs.d.ts.map +0 -1
  1325. package/dist/plugins/dynamic-transform.d.ts +0 -72
  1326. package/dist/plugins/dynamic-transform.d.ts.map +0 -1
  1327. package/dist/plugins/server-action-exports.d.ts +0 -26
  1328. package/dist/plugins/server-action-exports.d.ts.map +0 -1
  1329. package/dist/routing/status-file-lint.d.ts +0 -33
  1330. package/dist/routing/status-file-lint.d.ts.map +0 -1
  1331. package/dist/search-params/analyze.d.ts +0 -54
  1332. package/dist/search-params/analyze.d.ts.map +0 -1
  1333. package/dist/search-params/builtin-codecs.d.ts +0 -105
  1334. package/dist/search-params/builtin-codecs.d.ts.map +0 -1
  1335. package/dist/search-params/codecs.d.ts +0 -53
  1336. package/dist/search-params/codecs.d.ts.map +0 -1
  1337. package/dist/search-params/create.d.ts +0 -106
  1338. package/dist/search-params/create.d.ts.map +0 -1
  1339. package/dist/search-params/registry.d.ts +0 -20
  1340. package/dist/search-params/registry.d.ts.map +0 -1
  1341. package/dist/server/asset-headers.d.ts +0 -42
  1342. package/dist/server/asset-headers.d.ts.map +0 -1
  1343. package/dist/server/client-module-map.d.ts +0 -47
  1344. package/dist/server/client-module-map.d.ts.map +0 -1
  1345. package/dist/server/dev-fetch-instrumentation.d.ts +0 -22
  1346. package/dist/server/dev-fetch-instrumentation.d.ts.map +0 -1
  1347. package/dist/server/dev-logger.d.ts.map +0 -1
  1348. package/dist/server/dev-span-processor.d.ts +0 -29
  1349. package/dist/server/dev-span-processor.d.ts.map +0 -1
  1350. package/dist/server/dev-warnings.d.ts +0 -129
  1351. package/dist/server/dev-warnings.d.ts.map +0 -1
  1352. package/dist/server/flush.d.ts +0 -74
  1353. package/dist/server/flush.d.ts.map +0 -1
  1354. package/dist/server/manifest-status-resolver.d.ts +0 -58
  1355. package/dist/server/manifest-status-resolver.d.ts.map +0 -1
  1356. package/dist/server/prerender.d.ts +0 -77
  1357. package/dist/server/prerender.d.ts.map +0 -1
  1358. package/dist/server/tree-builder.d.ts +0 -126
  1359. package/dist/server/tree-builder.d.ts.map +0 -1
  1360. package/dist/utils/directive-parser.d.ts +0 -70
  1361. package/dist/utils/directive-parser.d.ts.map +0 -1
  1362. package/src/cache/register-cached-function.ts +0 -103
  1363. package/src/client/browser-entry.ts +0 -678
  1364. package/src/client/head.ts +0 -61
  1365. package/src/client/link-status-provider.tsx +0 -30
  1366. package/src/client/segment-merger.ts +0 -339
  1367. package/src/client/stale-reload.ts +0 -87
  1368. package/src/client/transition-root.tsx +0 -166
  1369. package/src/client/types.ts +0 -21
  1370. package/src/client/use-navigation-pending.ts +0 -38
  1371. package/src/client/use-params.ts +0 -141
  1372. package/src/content/index.ts +0 -13
  1373. package/src/plugins/adapter-build.ts +0 -118
  1374. package/src/plugins/cache-transform.ts +0 -199
  1375. package/src/plugins/dynamic-transform.ts +0 -161
  1376. package/src/plugins/server-action-exports.ts +0 -220
  1377. package/src/routing/status-file-lint.ts +0 -112
  1378. package/src/search-params/analyze.ts +0 -192
  1379. package/src/search-params/builtin-codecs.ts +0 -228
  1380. package/src/search-params/codecs.ts +0 -153
  1381. package/src/search-params/create.ts +0 -321
  1382. package/src/search-params/registry.ts +0 -31
  1383. package/src/server/asset-headers.ts +0 -81
  1384. package/src/server/client-module-map.ts +0 -58
  1385. package/src/server/dev-fetch-instrumentation.ts +0 -96
  1386. package/src/server/dev-span-processor.ts +0 -78
  1387. package/src/server/dev-warnings.ts +0 -283
  1388. package/src/server/flush.ts +0 -182
  1389. package/src/server/manifest-status-resolver.ts +0 -215
  1390. package/src/server/prerender.ts +0 -139
  1391. package/src/server/tree-builder.ts +0 -382
  1392. package/src/shims/server-only-noop.js +0 -6
  1393. package/src/utils/directive-parser.ts +0 -529
@@ -0,0 +1,4645 @@
1
+ import { r as RenderTimeoutError, t as DEFAULT_RENDER_TIMEOUT_MS } from "../_chunks/render-timeout-DOf_BBcO.js";
2
+ import { n as isRscContentType, r as isRscRequest } from "../_chunks/rsc-media-type-DDc7duTD.js";
3
+ import { n as classifyUrlSegment, r as effectiveUrlSegment } from "../_chunks/segment-classify-C539Pa2O.js";
4
+ import { t as computeSegmentTreePaths } from "../_chunks/segment-keys-D5hu1hz4.js";
5
+ import { i as getMetadataRouteServePath, n as classifyMetadataRoute, r as getMetadataRouteAutoLink, t as METADATA_ROUTE_CONVENTIONS } from "../_chunks/metadata-routes-DSDjM_hJ.js";
6
+ import { r as canonicalize } from "../_chunks/canonicalize-CgHoscYO.js";
7
+ import { a as requestContextAls, r as prebuiltCaptureAls, s as timingAls, t as earlyHintsSenderAls } from "../_chunks/als-registry-DaxkVjt5.js";
8
+ import { C as isRedirectSignal, F as withSpan, M as replaceTraceId, N as runWithTraceId, O as generateTraceId, P as setSpanAttribute, S as isDenySignal, _ as setLogger, a as logMiddlewareError, c as logRenderError, d as logRouteError, f as logRouteSetupError, g as logWaitUntilUnsupported, h as logWaitUntilRejected, j as getTraceId, k as getOtelTraceId, l as logRequestCompleted, m as logSwrRefetchFailed, n as logCacheMiss, o as logMiddlewareShortCircuit, p as logSlowRequest, r as logCsrfRejected, s as logProxyError, t as getLogger, u as logRequestReceived, v as swallow, w as isDebug, x as isControlFlowSignal } from "../_chunks/logger-BP0LN6vP.js";
9
+ import { i as collectRouteModulepreloads, n as collectRouteCss, r as collectRouteFonts } from "../_chunks/build-manifest-DWppEdLB.js";
10
+ import { i as isRscCacheKeyShape, n as RSC_KEY_PARAM, s as rscCacheKey, t as RSC_KEY_HEADERS } from "../_chunks/rsc-cache-key-ClUiXQnK.js";
11
+ import { C as setMutableCookieContext, S as setMatchedSegmentPath, T as runOutsideReactCacheScope, b as markResponseFlushed, f as getCookie, g as getHeader, h as applyRequestHeaderOverlay, i as coerce, m as getSetCookieHeaders, o as DenySignal, s as RedirectSignal, t as executeAction, v as getSearchParams, w as setSegmentParams, x as runWithRequestContext, y as getSegmentParams } from "../_chunks/actions-Rjk4htmA.js";
12
+ import { a as getManifestEntry, b as wasInvalidatedSince, c as lookupPrebuiltPayload, d as checkVersionSkew, i as storeOverlayEntry, l as setPrebuiltPayloadSource, n as lookupOverlay, o as hasPrebuiltPayloadSource, r as overlayKey, s as isParamIndependent, u as applyReloadHeaders, v as currentInvalidationEpoch, x as getWaitUntil, y as lastInvalidationEpochFor } from "../_chunks/cache-api-DGdYfNJn.js";
13
+ import { n as createSingleflight } from "../_chunks/singleflight-BeVMMKi0.js";
14
+ import { n as toBracketKey } from "../_chunks/resolve-schema-CBR6Lm4i.js";
15
+ import { t as normalizeParamValue } from "../_chunks/param-value-C8TNYchQ.js";
16
+ import { l as TIMBER_ERROR_HEADER, t as StatusPageMarker } from "../_chunks/status-page-marker-BRX9Ib-d.js";
17
+ import { createHash } from "node:crypto";
18
+ import { readFile } from "node:fs/promises";
19
+ import React, { createElement, useContext } from "react";
20
+ //#region src/server/render-utils.ts
21
+ /**
22
+ * Check if an error is an abort error (connection closed by client).
23
+ *
24
+ * When the browser aborts a request (page refresh, navigation away),
25
+ * the AbortSignal fires and React/streams throw an AbortError. This
26
+ * is not an application error — suppress it from error boundaries and logs.
27
+ */
28
+ function isAbortError(error) {
29
+ if (error instanceof DOMException && error.name === "AbortError") return true;
30
+ if (error instanceof Error && error.name === "AbortError") return true;
31
+ return false;
32
+ }
33
+ //#endregion
34
+ //#region src/server/server-timing.ts
35
+ /**
36
+ * Server-Timing header — dev-mode timing breakdowns for Chrome DevTools.
37
+ *
38
+ * Collects timing entries per request using ALS. Each pipeline phase
39
+ * (proxy, middleware, render, SSR, access, fetch) records an entry.
40
+ * Before response flush, entries are formatted into a Server-Timing header.
41
+ *
42
+ * Only active in dev mode — zero overhead in production.
43
+ *
44
+ * See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing
45
+ * Task: LOCAL-290
46
+ */
47
+ /**
48
+ * Run a callback with a per-request timing collector.
49
+ * Must be called at the top of the request pipeline (wraps the full request).
50
+ */
51
+ function runWithTimingCollector(fn) {
52
+ return timingAls.run({ entries: [] }, fn);
53
+ }
54
+ /**
55
+ * Run a function and automatically record its duration as a timing entry.
56
+ * Returns the function's result. No-ops the recording if outside a collector.
57
+ */
58
+ async function withTiming(name, desc, fn) {
59
+ const store = timingAls.getStore();
60
+ if (!store) return fn();
61
+ const start = performance.now();
62
+ try {
63
+ return await fn();
64
+ } finally {
65
+ const dur = Math.round(performance.now() - start);
66
+ store.entries.push({
67
+ name,
68
+ dur,
69
+ desc
70
+ });
71
+ }
72
+ }
73
+ /**
74
+ * Get the Server-Timing header value for the current request.
75
+ * Returns null if no entries exist or outside a collector.
76
+ *
77
+ * Format: `name;dur=123;desc="description", name2;dur=456`
78
+ * See RFC 6797 / Server-Timing spec for format details.
79
+ */
80
+ function getServerTimingHeader() {
81
+ const store = timingAls.getStore();
82
+ if (!store || store.entries.length === 0) return null;
83
+ const nameCounts = /* @__PURE__ */ new Map();
84
+ const parts = store.entries.map((entry) => {
85
+ const count = nameCounts.get(entry.name) ?? 0;
86
+ nameCounts.set(entry.name, count + 1);
87
+ const uniqueName = count > 0 ? `${entry.name}-${count}` : entry.name;
88
+ return {
89
+ ...entry,
90
+ name: uniqueName
91
+ };
92
+ }).map((entry) => {
93
+ let part = `${entry.name};dur=${entry.dur}`;
94
+ if (entry.desc) {
95
+ const safeDesc = entry.desc.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
96
+ part += `;desc="${safeDesc}"`;
97
+ }
98
+ return part;
99
+ });
100
+ const MAX_HEADER_SIZE = 4096;
101
+ let result = "";
102
+ for (let i = 0; i < parts.length; i++) {
103
+ const candidate = result ? `${result}, ${parts[i]}` : parts[i];
104
+ if (candidate.length > MAX_HEADER_SIZE) break;
105
+ result = candidate;
106
+ }
107
+ return result || null;
108
+ }
109
+ //#endregion
110
+ //#region src/server/stream-utils.ts
111
+ /**
112
+ * Stream utilities for the RSC rendering pipeline.
113
+ *
114
+ * teeWithErrorPropagation — wraps native ReadableStream.tee() with:
115
+ * 1. Cross-branch error propagation (error on one branch cancels the other)
116
+ * 2. Buffer divergence monitoring (warns + cancels when threshold exceeded)
117
+ *
118
+ * This prevents two classes of bugs:
119
+ * - Silent stalls when one tee branch errors and the other hangs indefinitely
120
+ * - Unbounded memory growth when one consumer (SSR) is much slower than the other
121
+ *
122
+ * Design doc: 02-rendering-pipeline.md §"RSC → SSR → Client Hydration"
123
+ */
124
+ /**
125
+ * Wrap a response body with stream-lifecycle timing instrumentation.
126
+ *
127
+ * Logs firstChunk, lastChunk, close, maxGap (largest inter-chunk gap),
128
+ * chunk count, and byte count on stream termination (clean close OR
129
+ * cancellation/disconnect). Gated behind `serverTiming: 'detailed'`.
130
+ *
131
+ * Uses a manual reader pump to guarantee logging on all termination
132
+ * paths — TransformStream.flush() is not called on cancellation.
133
+ */
134
+ function wrapResponseWithStreamTiming(body, startTime) {
135
+ let firstChunkMs = 0;
136
+ let lastChunkMs = 0;
137
+ let maxGapMs = 0;
138
+ let prevChunkTime = 0;
139
+ let chunks = 0;
140
+ let bytes = 0;
141
+ let cancelled = false;
142
+ function emitLog() {
143
+ const closeMs = Math.round(performance.now() - startTime);
144
+ getLogger().debug("stream-lifecycle", {
145
+ firstChunkMs,
146
+ lastChunkMs,
147
+ closeMs,
148
+ maxGapMs,
149
+ chunks,
150
+ bytes,
151
+ ...cancelled ? { cancelled: true } : {}
152
+ });
153
+ }
154
+ const reader = body.getReader();
155
+ return new ReadableStream({
156
+ async pull(controller) {
157
+ try {
158
+ const { done, value } = await reader.read();
159
+ if (done) {
160
+ emitLog();
161
+ controller.close();
162
+ return;
163
+ }
164
+ const now = performance.now();
165
+ const relativeMs = Math.round(now - startTime);
166
+ if (chunks === 0) firstChunkMs = relativeMs;
167
+ else {
168
+ const gap = Math.round(now - prevChunkTime);
169
+ if (gap > maxGapMs) maxGapMs = gap;
170
+ }
171
+ prevChunkTime = now;
172
+ lastChunkMs = relativeMs;
173
+ chunks++;
174
+ bytes += value.byteLength;
175
+ controller.enqueue(value);
176
+ } catch (err) {
177
+ emitLog();
178
+ controller.error(err);
179
+ }
180
+ },
181
+ cancel(reason) {
182
+ cancelled = true;
183
+ emitLog();
184
+ reader.cancel(reason).catch(() => {});
185
+ }
186
+ });
187
+ }
188
+ /**
189
+ * Read a byte stream to completion under one overall deadline and return
190
+ * the concatenated bytes. Every read is raced against the time remaining,
191
+ * and the reader is cancelled on every exit, so a stalled upstream never
192
+ * outlives the deadline. See design/02-rendering-pipeline.md §"Always
193
+ * Timeout-Guard Stream Reads".
194
+ *
195
+ * @param label - Prefix for the timeout error message. Both ways the deadline
196
+ * can pass (between reads, or while a read is pending) reject with the same
197
+ * `${label} did not complete within ${timeoutMs}ms`.
198
+ */
199
+ async function readAllBytesWithDeadline(stream, timeoutMs, label) {
200
+ const reader = stream.getReader();
201
+ const chunks = [];
202
+ let totalLength = 0;
203
+ const deadline = Date.now() + timeoutMs;
204
+ const deadlineError = () => /* @__PURE__ */ new Error(`${label} did not complete within ${timeoutMs}ms`);
205
+ try {
206
+ for (;;) {
207
+ const remaining = deadline - Date.now();
208
+ if (remaining <= 0) throw deadlineError();
209
+ let timer;
210
+ const timeout = new Promise((_, reject) => {
211
+ timer = setTimeout(() => reject(deadlineError()), remaining);
212
+ });
213
+ let result;
214
+ try {
215
+ result = await Promise.race([reader.read(), timeout]);
216
+ } finally {
217
+ clearTimeout(timer);
218
+ }
219
+ if (result.done) break;
220
+ chunks.push(result.value);
221
+ totalLength += result.value.byteLength;
222
+ }
223
+ } finally {
224
+ reader.cancel().catch(() => {});
225
+ }
226
+ const bytes = new Uint8Array(totalLength);
227
+ let offset = 0;
228
+ for (const chunk of chunks) {
229
+ bytes.set(chunk, offset);
230
+ offset += chunk.byteLength;
231
+ }
232
+ return bytes;
233
+ }
234
+ //#endregion
235
+ //#region src/server/instrumentation.ts
236
+ /**
237
+ * Instrumentation — loads and runs the user's instrumentation.ts file.
238
+ *
239
+ * instrumentation.ts is a file convention at the project root that exports:
240
+ * - register() — called once at server startup, before the first request
241
+ * - onRequestError() — called for every unhandled server error
242
+ * - logger — any object with info/warn/error/debug methods
243
+ *
244
+ * See design/17-logging.md §"instrumentation.ts — The Entry Point"
245
+ */
246
+ var _initialized = false;
247
+ var _onRequestError = null;
248
+ /**
249
+ * Load and initialize the user's instrumentation.ts module.
250
+ *
251
+ * - Awaits register() before returning (server blocks on this).
252
+ * - Picks up the logger export and wires it into the framework logger.
253
+ * - Stores onRequestError for later invocation.
254
+ *
255
+ * @param loader - Function that dynamically imports the user's instrumentation module.
256
+ * Returns null if no instrumentation.ts exists.
257
+ */
258
+ async function loadInstrumentation(loader) {
259
+ if (_initialized) return;
260
+ _initialized = true;
261
+ let mod;
262
+ try {
263
+ mod = await loader();
264
+ } catch (error) {
265
+ console.error("[timber] Failed to load instrumentation.ts:", error);
266
+ return;
267
+ }
268
+ if (!mod) return;
269
+ if (mod.logger && typeof mod.logger.info === "function") setLogger(mod.logger);
270
+ if (typeof mod.onRequestError === "function") _onRequestError = mod.onRequestError;
271
+ if (typeof mod.register === "function") try {
272
+ await mod.register();
273
+ } catch (error) {
274
+ console.error("[timber] instrumentation.ts register() threw:", error);
275
+ throw error;
276
+ }
277
+ }
278
+ /**
279
+ * Call the user's onRequestError hook. Catches and logs any errors thrown
280
+ * by the hook itself — it must not affect the response.
281
+ */
282
+ async function callOnRequestError(error, request, context) {
283
+ if (!_onRequestError) return;
284
+ try {
285
+ await _onRequestError(error, request, context);
286
+ } catch (hookError) {
287
+ console.error("[timber] onRequestError hook threw:", hookError);
288
+ }
289
+ }
290
+ /**
291
+ * Check if onRequestError is registered.
292
+ */
293
+ function hasOnRequestError() {
294
+ return _onRequestError !== null;
295
+ }
296
+ //#endregion
297
+ //#region src/server/safe-load.ts
298
+ /**
299
+ * Custom error class for module load failures.
300
+ *
301
+ * Preserves the original error as `cause` while providing a
302
+ * human-readable message with the file path.
303
+ */
304
+ var ModuleLoadError = class extends Error {
305
+ /** The file path that failed to load. */
306
+ filePath;
307
+ constructor(filePath, cause) {
308
+ const originalMessage = cause instanceof Error ? cause.message : String(cause);
309
+ super(`[timber] Failed to load module ${filePath}\n ${originalMessage}`, { cause });
310
+ this.name = "ModuleLoadError";
311
+ this.filePath = filePath;
312
+ }
313
+ };
314
+ /**
315
+ * Load a route manifest module with enriched error context.
316
+ *
317
+ * On success: returns the module object (same as `loader.load()`).
318
+ * On failure: throws `ModuleLoadError` with file path and original cause.
319
+ *
320
+ * Error rendering paths that need fallthrough use `loadFallbackModule()`.
321
+ */
322
+ async function loadModule(loader) {
323
+ try {
324
+ return await loader.load();
325
+ } catch (error) {
326
+ throw new ModuleLoadError(loader.filePath, error);
327
+ }
328
+ }
329
+ //#endregion
330
+ //#region src/server/route-matcher.ts
331
+ /**
332
+ * Whether a matched chain renders as an API route: its leaf has a
333
+ * `route.ts` and no `page.tsx`. The same test `renderRoute` dispatches on,
334
+ * so every answer about a route's format — early hints, deny format, error
335
+ * format, error reporting — agrees with what actually ran.
336
+ */
337
+ function isApiRouteChain(segments) {
338
+ const leaf = segments[segments.length - 1];
339
+ return !!leaf?.route && !leaf.page;
340
+ }
341
+ /**
342
+ * Whether a matched chain is a route.ts handler that opted out of the CSRF
343
+ * check with `export const csrf = false`. Keyed on the same leaf
344
+ * `isApiRouteChain` dispatches on, so the exemption can only ever apply to
345
+ * the route.ts that will actually run — never to a page, and so never to a
346
+ * server action. See design/08-forms-and-actions.md §"Per-route exemption".
347
+ */
348
+ function isCsrfExemptRoute(segments) {
349
+ return isApiRouteChain(segments) && segments[segments.length - 1]?.csrfExempt === true;
350
+ }
351
+ var SERVE_PATH_TO_CONVENTION = /* @__PURE__ */ new Map();
352
+ for (const [baseName, convention] of Object.entries(METADATA_ROUTE_CONVENTIONS)) if (convention.serveExtension) for (const ext of [...convention.staticExtensions, convention.serveExtension]) SERVE_PATH_TO_CONVENTION.set(`${convention.servePath}.${ext}`, baseName);
353
+ else SERVE_PATH_TO_CONVENTION.set(convention.servePath, baseName);
354
+ //#endregion
355
+ //#region src/shared/csrf-reject.ts
356
+ /**
357
+ * Marker header on CSRF rejections.
358
+ *
359
+ * A CSRF rejection is a 403, the same status as an access-control denial.
360
+ * The server stamps this header on every CSRF 403 so callers — the browser
361
+ * action runtime, a same-origin `fetch` in user code, a curl session — can
362
+ * tell the two apart without parsing the body. A cross-origin `fetch` cannot
363
+ * read it: the rejection returns before proxy.ts, so no CORS headers are set
364
+ * and the browser reports a network error instead. Shared so the server writer and the client
365
+ * reader cannot drift.
366
+ *
367
+ * See design/08-forms-and-actions.md §"CSRF Protection" (TIM-1459).
368
+ */
369
+ var CSRF_REJECT_HEADER = "X-Timber-Reject";
370
+ var CSRF_REJECT_VALUE = "csrf";
371
+ //#endregion
372
+ //#region src/server/csrf.ts
373
+ /**
374
+ * CSRF protection — Fetch Metadata first, Origin as the fallback.
375
+ *
376
+ * CSRF needs a victim's browser to attach cookies, and browsers mark the
377
+ * requests they send: `Sec-Fetch-Site` on HTTPS (and localhost) pages in
378
+ * every current browser, `Origin` on cross-site POSTs everywhere else. So
379
+ * the check trusts those two headers in that order, and lets a request
380
+ * carrying neither through — a webhook, curl, or a server-to-server
381
+ * `fetch`. The residual risk is browsers old enough to send neither on a
382
+ * form POST (Firefox < 70, IE11); SameSite cookies are the defense there.
383
+ * See design/08-forms-and-actions.md §"CSRF Protection".
384
+ *
385
+ * Prior art: Go's `net/http.CrossOriginProtection` (Go 1.25) and the OWASP
386
+ * CSRF cheat sheet (Fetch Metadata as a primary defense).
387
+ *
388
+ * The per-route exemption (`export const csrf = false` in route.ts) is not
389
+ * decided here: the caller owns route matching and applies it to a
390
+ * rejection. Disable globally with csrf: false (not recommended).
391
+ *
392
+ * See design/08-forms-and-actions.md §"CSRF Protection"
393
+ * See design/13-security.md §"Security Testing Checklist" #6
394
+ */
395
+ /** HTTP methods that are considered safe (no mutation). */
396
+ var SAFE_METHODS = /* @__PURE__ */ new Set([
397
+ "GET",
398
+ "HEAD",
399
+ "OPTIONS"
400
+ ]);
401
+ /**
402
+ * `Sec-Fetch-Site` values that mean the browser itself vouches for the
403
+ * request: `same-origin`, or `none` (user-initiated — typed URL, bookmark).
404
+ * Every other value, including ones no browser sends, is not same-origin.
405
+ * The header is forbidden to page scripts, so a page cannot forge it.
406
+ */
407
+ var TRUSTED_FETCH_SITES = /* @__PURE__ */ new Set(["same-origin", "none"]);
408
+ /**
409
+ * Derive the request's scheme from trusted sources.
410
+ *
411
+ * Priority:
412
+ * 1. X-Forwarded-Proto (set by reverse proxies / load balancers)
413
+ * 2. Request URL protocol
414
+ *
415
+ * Falls back to 'https' (fail closed) if neither is available.
416
+ */
417
+ function deriveRequestScheme(req) {
418
+ const forwarded = req.headers.get("X-Forwarded-Proto");
419
+ if (forwarded) {
420
+ const proto = forwarded.split(",")[0].trim().toLowerCase();
421
+ if (proto === "http" || proto === "https") return proto;
422
+ }
423
+ try {
424
+ return new URL(req.url).protocol.replace(":", "");
425
+ } catch {
426
+ return "https";
427
+ }
428
+ }
429
+ /**
430
+ * Decide whether an unsafe-method request may proceed.
431
+ *
432
+ * In order:
433
+ * 1. `Sec-Fetch-Site` is `same-origin` or `none` → allow.
434
+ * 2. `Sec-Fetch-Site` has any other value → allow only if `Origin` is in
435
+ * `allowedOrigins`.
436
+ * 3. No `Sec-Fetch-Site`, `Origin` present (a plain-HTTP page, or an
437
+ * older browser) → `Origin` must be in `allowedOrigins` or equal the
438
+ * origin derived from the scheme and `Host`.
439
+ * 4. Neither header → allow: not a browser, so no ambient cookies to abuse.
440
+ *
441
+ * Safe methods (GET, HEAD, OPTIONS) and `csrf: false` always pass.
442
+ */
443
+ function validateCsrf(req, config) {
444
+ if (SAFE_METHODS.has(req.method)) return { ok: true };
445
+ if (config.csrf === false) return { ok: true };
446
+ const fetchSite = req.headers.get("Sec-Fetch-Site");
447
+ const origin = req.headers.get("Origin");
448
+ const rejection = (reason, expected) => ({
449
+ ok: false,
450
+ status: 403,
451
+ reason,
452
+ origin,
453
+ fetchSite,
454
+ expected
455
+ });
456
+ if (fetchSite !== null) {
457
+ if (TRUSTED_FETCH_SITES.has(fetchSite)) return { ok: true };
458
+ if (origin !== null && config.allowedOrigins?.includes(origin)) return { ok: true };
459
+ return rejection("cross-origin", config.allowedOrigins ?? []);
460
+ }
461
+ if (origin === null) return { ok: true };
462
+ const allowed = config.allowedOrigins ?? [];
463
+ if (allowed.includes(origin)) return { ok: true };
464
+ const host = req.headers.get("Host");
465
+ if (!host) return rejection("missing-host", allowed);
466
+ let originOrigin;
467
+ try {
468
+ originOrigin = new URL(origin).origin;
469
+ } catch {
470
+ return rejection("invalid-origin", allowed);
471
+ }
472
+ let expectedOrigin;
473
+ try {
474
+ expectedOrigin = new URL(`${deriveRequestScheme(req)}://${host}`).origin;
475
+ } catch {
476
+ return rejection("invalid-host", allowed);
477
+ }
478
+ return originOrigin === expectedOrigin ? { ok: true } : rejection("origin-mismatch", [expectedOrigin, ...allowed]);
479
+ }
480
+ /**
481
+ * Longest request-derived value echoed into the server log. Origin, Host,
482
+ * Sec-Fetch-Site, and the path are attacker-controlled; a request can carry
483
+ * kilobytes in each, and every rejection writes a log line. Real values are
484
+ * far shorter.
485
+ */
486
+ var MAX_LOGGED_VALUE = 200;
487
+ function clip(value) {
488
+ return value.length > MAX_LOGGED_VALUE ? `${value.slice(0, MAX_LOGGED_VALUE)}…` : value;
489
+ }
490
+ /**
491
+ * Clip and quote a request-derived value for the log. JSON quoting escapes
492
+ * quotes and control characters, so a crafted header cannot close the quote
493
+ * and forge `key=value` fields or a second line.
494
+ */
495
+ function quote(value) {
496
+ return JSON.stringify(clip(value));
497
+ }
498
+ var PROXY_HINT = "If the app runs behind a proxy that rewrites Host or terminates TLS, forward the original Host and X-Forwarded-Proto, or list the public origin in allowedOrigins in timber.config.ts.";
499
+ var EXEMPTION_HINT = "If this route.ts must accept cross-site browser requests (OIDC form_post, SAML, 3-D Secure returns), add `export const csrf = false` to it.";
500
+ /**
501
+ * Browsers send `Origin: null` from opaque contexts: sandboxed iframes, a POST
502
+ * redirected across origins, file: pages. Every such context shares the one
503
+ * value — an attacker's `<iframe sandbox>` included — so config validation
504
+ * refuses it in `allowedOrigins` and the hint points at the exemption instead.
505
+ */
506
+ var OPAQUE_ORIGIN_DETAIL = `origin "null" is an opaque origin (sandboxed iframe, a POST redirected across origins, or a file: page). allowedOrigins cannot admit it: every opaque context sends "null", including an attacker's sandboxed iframe. ${EXEMPTION_HINT}`;
507
+ /** One sentence per rejection path: what was observed, and what to change. */
508
+ function describeRejection(r, host) {
509
+ const opaque = r.origin === "null";
510
+ const origin = quote(r.origin ?? "");
511
+ const expected = r.expected.map(quote).join(", ");
512
+ switch (r.reason) {
513
+ case "cross-origin": {
514
+ if (opaque) return OPAQUE_ORIGIN_DETAIL;
515
+ const from = r.origin === null ? "with no Origin header" : `from origin ${origin}`;
516
+ const allowed = r.expected.length > 0 ? ` and is not in allowedOrigins [${expected}]` : "";
517
+ return `Sec-Fetch-Site ${quote(r.fetchSite ?? "")} request ${from}: the browser reports it is not same-origin${allowed}. If this origin is legitimate, add it to allowedOrigins in timber.config.ts. ${EXEMPTION_HINT}`;
518
+ }
519
+ case "missing-host": return `origin ${origin} could not be checked: request has no Host header. ${PROXY_HINT}`;
520
+ case "invalid-host": return `origin ${origin} could not be checked: Host header ${quote(host ?? "")} is not a valid host. ${PROXY_HINT}`;
521
+ case "invalid-origin":
522
+ if (opaque) return OPAQUE_ORIGIN_DETAIL;
523
+ return `Origin header ${origin} is not a valid origin.`;
524
+ case "origin-mismatch": {
525
+ const [own, ...listed] = r.expected.map(quote);
526
+ return `origin ${origin} does not match the request origin ${own}${listed.length > 0 ? ` or allowedOrigins [${listed.join(", ")}]` : ""}. ${PROXY_HINT}`;
527
+ }
528
+ }
529
+ }
530
+ /**
531
+ * Turn a failed check into the response the client sees, and log why.
532
+ *
533
+ * The response is a 403 marked as a CSRF rejection (header + plain-text
534
+ * body) so it cannot be mistaken for an access-control 403. It never
535
+ * carries the observed or expected origins — `allowedOrigins` is server
536
+ * configuration, and a Host-derived origin can be an internal hostname
537
+ * behind a proxy. The server log gets the full diagnosis.
538
+ */
539
+ function csrfRejectionResponse(req, rejection) {
540
+ logCsrfRejected(describeRejection(rejection, req.headers.get("Host")), {
541
+ method: req.method,
542
+ path: clip(new URL(req.url).pathname),
543
+ reason: rejection.reason
544
+ });
545
+ return new Response("Forbidden: CSRF origin check failed\n", {
546
+ status: rejection.status,
547
+ headers: {
548
+ "Content-Type": "text/plain; charset=utf-8",
549
+ [CSRF_REJECT_HEADER]: CSRF_REJECT_VALUE
550
+ }
551
+ });
552
+ }
553
+ //#endregion
554
+ //#region src/server/pipeline-helpers.ts
555
+ /**
556
+ * The pipeline-boundary CSRF gate: the 403 to answer with, or null to go on.
557
+ *
558
+ * Runs on every request after canonicalization and before proxy.ts, so it
559
+ * covers route.ts handlers and server actions alike, and no user code has
560
+ * run when it decides. The per-route exemption is looked up only for a
561
+ * request the check would reject — the common accepted request never pays
562
+ * for a match. The lookup uses the pipeline's own matcher on the canonical
563
+ * path, the same match `handleRequest` dispatches on, and reads a flag the
564
+ * scanner took from route.ts source, so deciding it loads no module.
565
+ *
566
+ * A matcher that throws leaves the request unexempted: fail closed.
567
+ *
568
+ * See design/08-forms-and-actions.md §"CSRF Protection".
569
+ */
570
+ function csrfGate(req, canonicalPath, csrf, matchRoute) {
571
+ const result = validateCsrf(req, csrf);
572
+ if (result.ok) return null;
573
+ let exempt = false;
574
+ try {
575
+ const match = matchRoute(canonicalPath);
576
+ exempt = match !== null && isCsrfExemptRoute(match.segments);
577
+ } catch (err) {
578
+ swallow(err, "csrf gate: route match for exemption lookup threw");
579
+ }
580
+ return exempt ? null : csrfRejectionResponse(req, result);
581
+ }
582
+ /**
583
+ * Build a proxy resolver closure from the declared source. Called exactly
584
+ * once at `createPipeline` setup time, so the hot path sees only the branch
585
+ * that corresponds to this pipeline's configured variant.
586
+ *
587
+ * Returns `null` when the app has no proxy.ts — the hot path short-circuits
588
+ * around `runProxyPhase` entirely in that case.
589
+ *
590
+ * Accepts the sugar form (a bare `ProxyExport` — function or function array)
591
+ * and normalises it to the static variant. Functions and arrays are
592
+ * structurally distinct from the tagged `{ kind: 'lazy', loader }` object,
593
+ * so discrimination is unambiguous.
594
+ */
595
+ function makeProxyResolver(proxy) {
596
+ if (proxy === void 0) return null;
597
+ if (typeof proxy === "function" || Array.isArray(proxy)) {
598
+ const exp = proxy;
599
+ return () => exp;
600
+ }
601
+ if (proxy.kind === "static") {
602
+ const exp = proxy.export;
603
+ return () => exp;
604
+ }
605
+ const loader = proxy.loader;
606
+ return async () => (await loader()).default;
607
+ }
608
+ /**
609
+ * Apply all Set-Cookie headers from the cookie jar to a Headers object.
610
+ * Each cookie gets its own Set-Cookie header per RFC 6265 §4.1.
611
+ */
612
+ function applyCookieJar(headers) {
613
+ for (const value of getSetCookieHeaders()) headers.append("Set-Cookie", value);
614
+ }
615
+ /**
616
+ * Bring a bag the jar was already applied to up to date with the jar.
617
+ *
618
+ * `applied` is what `getSetCookieHeaders()` returned when the jar was
619
+ * applied to `headers`. Values the jar no longer holds (a later write to the
620
+ * same name replaced them) are removed, and values it gained are appended.
621
+ * Every cookie then appears once, with its last write (design/29-cookies.md
622
+ * §"Last-Write-Wins for Same Name"). `Set-Cookie` values that did not come
623
+ * from the jar, such as a middleware appending to `ctx.headers` directly,
624
+ * are left alone.
625
+ */
626
+ function syncCookieJar(headers, applied) {
627
+ const current = getSetCookieHeaders();
628
+ const stale = applied.filter((value) => !current.includes(value));
629
+ const added = current.filter((value) => !applied.includes(value));
630
+ if (stale.length === 0 && added.length === 0) return;
631
+ const kept = headers.getSetCookie();
632
+ for (const value of stale) {
633
+ const index = kept.indexOf(value);
634
+ if (index !== -1) kept.splice(index, 1);
635
+ }
636
+ headers.delete("Set-Cookie");
637
+ for (const value of [...kept, ...added]) headers.append("Set-Cookie", value);
638
+ }
639
+ /**
640
+ * Merge framework-managed response headers onto a terminal response without
641
+ * overwriting headers the terminal response already set itself.
642
+ */
643
+ function mergeMissingHeaders(target, source) {
644
+ const existingKeys = new Set([...target.keys()].map((key) => key.toLowerCase()));
645
+ for (const [key, value] of source.entries()) if (!existingKeys.has(key.toLowerCase())) target.append(key, value);
646
+ }
647
+ /**
648
+ * Clone a Response into a fresh one whose header bag is guaranteed mutable.
649
+ *
650
+ * `Response.redirect()` and some platform-level passthrough responses (notably
651
+ * on Cloudflare Workers) return objects with frozen header bags. Calling
652
+ * `.set()` or `.append()` on them throws `TypeError: immutable`, which the
653
+ * pipeline can hit when it appends Set-Cookie or Server-Timing entries.
654
+ *
655
+ * The pipeline calls this at the producer sites where user-controlled
656
+ * responses enter the framework — `outcomeToResponse` for all phase outcomes,
657
+ * and `handleRequest` for metadata-route and auto-sitemap user handlers — so
658
+ * downstream code can write headers without runtime feature-detection.
659
+ *
660
+ * The clone is unconditional. This is a deliberate trade: we avoid a
661
+ * try/catch + thrown `TypeError` on every request (the previous probe-based
662
+ * approach paid that cost on the hot path) and accept one cheap Response
663
+ * rewrap at the framework boundary instead.
664
+ */
665
+ function cloneWithMutableHeaders(response) {
666
+ return new Response(response.body, {
667
+ status: response.status,
668
+ statusText: response.statusText,
669
+ headers: new Headers(response.headers)
670
+ });
671
+ }
672
+ /**
673
+ * A fresh response header bag for a page the pipeline renders, seeded with
674
+ * the default dynamic-page cache policy.
675
+ *
676
+ * Without a Cache-Control header, CDNs (particularly Cloudflare) may attempt
677
+ * to buffer/process the response differently, causing intermittent
678
+ * multi-second delays. This matches Next.js's default behavior. Every bag a
679
+ * rendered page can be written onto starts here — a matched route, the
680
+ * no-match 404, and a deny page for a proxy-phase signal — so none of them
681
+ * leaves without a cache policy. Middleware and route handlers may replace it.
682
+ */
683
+ function dynamicResponseHeaders() {
684
+ return new Headers({ "Cache-Control": "private, no-cache, no-store, max-age=0, must-revalidate" });
685
+ }
686
+ /**
687
+ * Build a redirect Response from a RedirectSignal.
688
+ *
689
+ * For a client navigation (an RSC payload request for a page), returns 204 +
690
+ * X-Timber-Redirect so the client router can perform a soft SPA redirect. A
691
+ * raw 302 would be turned into an opaque redirect by
692
+ * fetch({redirect:'manual'}), crashing createFromFetch. See
693
+ * design/19-client-navigation.md. The caller decides what counts as a
694
+ * navigation: a route.ts match answers with a real 3xx whatever it accepts.
695
+ */
696
+ function buildRedirectResponse(signal, headers, navigation) {
697
+ if (navigation) {
698
+ headers.set("X-Timber-Redirect", signal.location);
699
+ return new Response(null, {
700
+ status: 204,
701
+ headers
702
+ });
703
+ }
704
+ headers.set("Location", signal.location);
705
+ return new Response(null, {
706
+ status: signal.status,
707
+ headers
708
+ });
709
+ }
710
+ /**
711
+ * Fire the user's onRequestError hook with request context.
712
+ *
713
+ * The one place a pipeline phase becomes the `phase` / `routeType` the hook
714
+ * reports, so every caller agrees. A render-phase error on a route.ts match
715
+ * is phase `'route'`, because the handler is what ran. Middleware reports
716
+ * as `'handler'`, its public name.
717
+ */
718
+ async function fireOnRequestError(error, req, source, segments) {
719
+ if (!isReportableError(error) || req.signal?.aborted) return;
720
+ if (isRepeatedLoadFailure(error)) return;
721
+ await callHook(error, req, source, segments);
722
+ }
723
+ /**
724
+ * Module evaluations whose failure was already reported. A module that fails
725
+ * to evaluate rethrows the same error on every later import, and
726
+ * `loadModule()` wraps each one in a new `ModuleLoadError` with that error
727
+ * as its `cause`. One broken module is one bug until it changes, however many
728
+ * loads trip over it in a request or across requests: the page's boundaries,
729
+ * then the fallback page, then the next request. A WeakSet, so a module that
730
+ * is re-evaluated (an HMR edit) gets a new cause and is reported again.
731
+ */
732
+ var reportedLoadFailures = /* @__PURE__ */ new WeakSet();
733
+ /**
734
+ * The same for a module that threw a primitive (`throw 'broken'`), which a
735
+ * WeakSet cannot hold. Keyed on the file and the value instead, so a module
736
+ * re-broken after an edit with the very same value is not reported again.
737
+ * It grows with the number of broken modules, not with requests.
738
+ */
739
+ var reportedPrimitiveLoadFailures = /* @__PURE__ */ new Set();
740
+ function isRepeatedLoadFailure(error) {
741
+ if (!(error instanceof ModuleLoadError)) return false;
742
+ const cause = error.cause;
743
+ if (typeof cause === "object" && cause !== null) {
744
+ if (reportedLoadFailures.has(cause)) return true;
745
+ reportedLoadFailures.add(cause);
746
+ return false;
747
+ }
748
+ const key = `${error.filePath}\0${typeof cause}\0${String(cause)}`;
749
+ if (reportedPrimitiveLoadFailures.has(key)) return true;
750
+ reportedPrimitiveLoadFailures.add(key);
751
+ return false;
752
+ }
753
+ /**
754
+ * Call the hook with request context, with no filtering. Only
755
+ * `fireOnRequestError()` and background reporting call it: background work
756
+ * runs after the response, so its request having aborted says nothing about
757
+ * the failure.
758
+ */
759
+ async function callHook(error, req, source, segments) {
760
+ if (!hasOnRequestError()) return;
761
+ const url = new URL(req.url);
762
+ const headersObj = {};
763
+ req.headers.forEach((v, k) => {
764
+ headersObj[k] = v;
765
+ });
766
+ const apiRoute = source === "route" || segments !== void 0 && isApiRouteChain(segments);
767
+ await callOnRequestError(error, {
768
+ method: req.method,
769
+ path: url.pathname,
770
+ headers: headersObj
771
+ }, {
772
+ phase: reportedPhase(source, apiRoute),
773
+ routePath: url.pathname,
774
+ routeType: source === "action" ? "action" : apiRoute ? "route" : "page",
775
+ traceId: getTraceId()
776
+ });
777
+ }
778
+ /**
779
+ * Whether an error is a failure `onRequestError` should hear about.
780
+ *
781
+ * Control-flow signals are outcomes the code chose — a `deny()` picks the
782
+ * page's status and data — and an abort is the client leaving. Callers answering
783
+ * a live request also skip everything once `req.signal` has aborted: a
784
+ * driver that throws "connection closed" after a disconnect is not a bug.
785
+ */
786
+ function isReportableError(error) {
787
+ return !isControlFlowSignal(error) && !isAbortError(error);
788
+ }
789
+ /**
790
+ * Fire the hook without making the caller wait, for callers that cannot:
791
+ * Flight's `onError` is synchronous, and background work has no caller.
792
+ * The promise is registered with the request's `waitUntil` so platforms
793
+ * that stop work when the response ends (Workers, Lambda) let the hook
794
+ * finish. The hook's own failures are caught in `callOnRequestError()`.
795
+ */
796
+ function fireInBackground(report) {
797
+ getWaitUntil()?.(report.catch((hookError) => swallow(hookError, "onRequestError could not be called")));
798
+ }
799
+ /**
800
+ * Run a `cache.component` stale-while-revalidate re-render as a singleflight
801
+ * body, and log and report its failure once for the flight. The stale
802
+ * payload already answered, so nothing else sees the failure.
803
+ *
804
+ * - **Once per flight.** A `.catch` on the flight's promise would run once
805
+ * per request that joined it, so the report happens inside the body. A
806
+ * body still running when the singleflight times out is reported then,
807
+ * with the timeout error (the signal's reason), and its later rejection
808
+ * is dropped.
809
+ * - **Against the request whose stale read started it**, read from the
810
+ * request context the work runs in. With no request (the build-time
811
+ * capture store) it is only logged.
812
+ * - **As phase `'render'`, route type `'page`.** A `cache.component` is a
813
+ * server component, so the read that started this was a page render (a
814
+ * revalidated page tree included). The request's own abort does not
815
+ * suppress the report: the work runs after the response either way.
816
+ */
817
+ async function runBackgroundFlight(signal, work, log) {
818
+ const failed = (error) => {
819
+ log(error);
820
+ const request = requestContextAls.getStore()?.request;
821
+ if (request && isReportableError(error)) fireInBackground(callHook(error, request, "render"));
822
+ };
823
+ const onTimeout = () => failed(signal.reason);
824
+ signal.addEventListener("abort", onTimeout, { once: true });
825
+ try {
826
+ await work();
827
+ } catch (error) {
828
+ if (!signal.aborted) failed(error);
829
+ } finally {
830
+ signal.removeEventListener("abort", onTimeout);
831
+ }
832
+ }
833
+ function reportedPhase(source, apiRoute) {
834
+ switch (source) {
835
+ case "middleware": return "handler";
836
+ case "render": return apiRoute ? "route" : "render";
837
+ default: return source;
838
+ }
839
+ }
840
+ //#endregion
841
+ //#region src/server/proxy.ts
842
+ /**
843
+ * Run the proxy pipeline.
844
+ *
845
+ * @param proxyExport - The default export from proxy.ts (function or array)
846
+ * @param req - The incoming request
847
+ * @param next - The continuation that proceeds to route matching and rendering
848
+ * @returns The final response
849
+ */
850
+ async function runProxy(proxyExport, req, next) {
851
+ const fns = Array.isArray(proxyExport) ? proxyExport : [proxyExport];
852
+ let i = fns.length;
853
+ let composed = next;
854
+ while (i--) {
855
+ const fn = fns[i];
856
+ const downstream = composed;
857
+ composed = () => Promise.resolve(fn(req, downstream));
858
+ }
859
+ return composed();
860
+ }
861
+ //#endregion
862
+ //#region src/server/middleware-runner.ts
863
+ /**
864
+ * Run a route's middleware function.
865
+ *
866
+ * @param middlewareFn - The default export from the route's middleware.ts
867
+ * @param ctx - The middleware context (req, params, headers, requestHeaders, searchParams)
868
+ * @returns A Response if middleware short-circuited, or undefined to continue
869
+ */
870
+ async function runMiddleware(middlewareFn, ctx) {
871
+ const result = await middlewareFn(ctx);
872
+ if (result instanceof Response) return result;
873
+ }
874
+ /**
875
+ * Run all middleware functions in the segment chain, root to leaf.
876
+ *
877
+ * Execution is top-down: root middleware runs first, leaf middleware runs last.
878
+ * All middleware share the same MiddlewareContext — a parent that sets
879
+ * ctx.requestHeaders makes it visible to child middleware and downstream components.
880
+ *
881
+ * Short-circuits on the first middleware that returns a Response.
882
+ * Remaining middleware in the chain do not execute.
883
+ *
884
+ * @param chain - Middleware functions ordered root-to-leaf
885
+ * @param ctx - Shared middleware context
886
+ * @returns A Response if any middleware short-circuited, or undefined to continue
887
+ */
888
+ async function runMiddlewareChain(chain, ctx) {
889
+ for (const fn of chain) {
890
+ const result = await fn(ctx);
891
+ if (result instanceof Response) return result;
892
+ }
893
+ }
894
+ //#endregion
895
+ //#region src/server/csp-nonce.ts
896
+ /**
897
+ * CSP nonce — per-request, generated on demand.
898
+ *
899
+ * timber emits several inline `<script>` tags per HTML response — React's
900
+ * bootstrap script and Fizz runtime, the Flight init/chunk scripts, the
901
+ * segment-tree script, and the post-flush noindex script. Under a strict
902
+ * `Content-Security-Policy` (`script-src 'nonce-…' 'strict-dynamic'`) every
903
+ * one of them needs the request's nonce or the browser refuses to run it.
904
+ *
905
+ * The app opts in by reading `ctx.nonce` in `middleware.ts` and putting it
906
+ * in the CSP header it sets through `ctx.headers`. The first read generates
907
+ * the value and stores it on the request context; the render pipeline reads
908
+ * it back with `getCspNonce()` and stamps it on every inline script. A
909
+ * request whose middleware never reads `ctx.nonce` has no nonce, and its
910
+ * markup is byte-identical to a render without this feature. timber does
911
+ * not parse the CSP header — the app owns the policy, timber owns the
912
+ * markup, and `ctx.nonce` is the one value both are built from.
913
+ *
914
+ * `proxy.ts` cannot participate: it only sees the finished response from
915
+ * `next()`, after the markup exists.
916
+ *
917
+ * See design/13-security.md §"Content Security Policy" (TIM-1414).
918
+ */
919
+ /**
920
+ * Generate a CSP nonce: 128 random bits, base64. Matches CSP's
921
+ * `base64-value` grammar, so it is valid in the header verbatim and safe
922
+ * inside a double-quoted HTML attribute without escaping.
923
+ */
924
+ function generateNonce() {
925
+ const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
926
+ let binary = "";
927
+ for (const b of bytes) binary += String.fromCharCode(b);
928
+ return btoa(binary);
929
+ }
930
+ /**
931
+ * The request's CSP nonce, creating it on first call. Backs the
932
+ * `ctx.nonce` getter on `MiddlewareContext`. Outside a request context
933
+ * (tests, build-time rendering) a fresh value is returned but not
934
+ * retained, since there is no request for it to belong to.
935
+ */
936
+ function requestCspNonce() {
937
+ const store = requestContextAls.getStore();
938
+ if (!store) return generateNonce();
939
+ store.cspNonce ??= generateNonce();
940
+ return store.cspNonce;
941
+ }
942
+ //#endregion
943
+ //#region src/server/param-coercion.ts
944
+ /**
945
+ * Thrown when an app/schema.ts segment-param codec's parse() fails.
946
+ * The pipeline catches this and responds with 404.
947
+ */
948
+ var ParamCoercionError = class extends Error {
949
+ constructor(message) {
950
+ super(message);
951
+ this.name = "ParamCoercionError";
952
+ }
953
+ };
954
+ /** Global schema codecs, set once at startup from virtual:timber-schema. */
955
+ var _globalCodecs = null;
956
+ /**
957
+ * Coerce the matched route's raw segment params and return the result.
958
+ *
959
+ * Reads `match.rawSegmentParams` and never writes to `match`: the caller
960
+ * decides where the coerced params go. Reading the raw record makes the call
961
+ * repeatable — coercing a match twice runs every codec on the URL value both
962
+ * times, never on its own output.
963
+ *
964
+ * Codecs come from `app/schema.ts` and nowhere else (TIM-1342 completed
965
+ * design/41-global-params.md migration step 9). A route with no schema, or
966
+ * a param the schema does not declare, keeps its raw string — which is the
967
+ * honest type for an uninterpreted URL segment.
968
+ *
969
+ * Throws ParamCoercionError if any codec fails (→ 404).
970
+ *
971
+ * This runs BEFORE middleware, so ctx.segmentParams is already typed.
972
+ * See design/07-routing.md §"Where Coercion Runs"
973
+ * See design/41-global-params.md §"Pipeline Integration"
974
+ *
975
+ * Stays `async` with nothing to await: it is injected as a
976
+ * `CoerceSegmentParamsFn` into the revalidation renderer, which awaits
977
+ * it. Making it synchronous would
978
+ * change that contract and remove a microtask boundary the pipeline's
979
+ * ordering has always had, for no gain.
980
+ */
981
+ async function coerceSegmentParams(match) {
982
+ const globalCodecs = _globalCodecs;
983
+ const mergeTarget = Object.create(null);
984
+ for (const key of Object.keys(match.rawSegmentParams)) if (key !== "__proto__") mergeTarget[key] = match.rawSegmentParams[key];
985
+ if (globalCodecs) for (const node of match.segments) {
986
+ const segment = effectiveUrlSegment(node);
987
+ if (!segment.paramName) continue;
988
+ const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
989
+ const codec = globalCodecs[bracketKey];
990
+ if (!codec) continue;
991
+ const key = segment.paramName;
992
+ if (!(key in mergeTarget)) continue;
993
+ let parsed;
994
+ try {
995
+ parsed = codec.parse(mergeTarget[key]);
996
+ } catch (err) {
997
+ const message = err instanceof Error ? err.message : String(err);
998
+ if (isDebug()) console.warn(`[timber] Global schema codec rejected value for param "${key}"\n Error: ${message}\n Raw value: ${JSON.stringify(mergeTarget[key])}\n Hint: check the codec in app/schema.ts for key '${bracketKey}'.`);
999
+ throw new ParamCoercionError(message);
1000
+ }
1001
+ mergeTarget[key] = normalizeParamValue(parsed, key);
1002
+ }
1003
+ return mergeTarget;
1004
+ }
1005
+ //#endregion
1006
+ //#region src/server/pipeline-metadata.ts
1007
+ /**
1008
+ * Metadata route helpers for the request pipeline.
1009
+ *
1010
+ * Handles serving static metadata files and serializing sitemap responses.
1011
+ * Extracted from pipeline.ts to keep files under 500 lines.
1012
+ *
1013
+ * See design/16-metadata.md §"Metadata Routes"
1014
+ */
1015
+ /**
1016
+ * Content types that are text-based and should include charset=utf-8.
1017
+ * Binary formats (images) should not include charset.
1018
+ */
1019
+ var TEXT_CONTENT_TYPES = /* @__PURE__ */ new Set([
1020
+ "application/xml",
1021
+ "text/plain",
1022
+ "application/json",
1023
+ "application/manifest+json",
1024
+ "image/svg+xml"
1025
+ ]);
1026
+ /**
1027
+ * Serve a static metadata file by reading it from disk.
1028
+ *
1029
+ * Static metadata route files (.xml, .txt, .json, .png, .ico, .svg, etc.)
1030
+ * are served as-is with the appropriate Content-Type header.
1031
+ * Text files include charset=utf-8; binary files do not.
1032
+ *
1033
+ * See design/16-metadata.md §"Metadata Routes"
1034
+ */
1035
+ async function serveStaticMetadataFile(metaMatch) {
1036
+ const { contentType, file } = metaMatch;
1037
+ const isText = TEXT_CONTENT_TYPES.has(contentType);
1038
+ const body = await readFile(file.filePath);
1039
+ const headers = {
1040
+ "Content-Type": isText ? `${contentType}; charset=utf-8` : contentType,
1041
+ "Content-Length": String(body.byteLength),
1042
+ "Cache-Control": "public, max-age=14400, must-revalidate"
1043
+ };
1044
+ return new Response(body, {
1045
+ status: 200,
1046
+ headers
1047
+ });
1048
+ }
1049
+ /**
1050
+ * Serialize a sitemap array to XML.
1051
+ * Follows the sitemap.org protocol: https://www.sitemaps.org/protocol.html
1052
+ */
1053
+ function serializeSitemap(entries) {
1054
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${entries.map((e) => {
1055
+ let xml = ` <url>\n <loc>${escapeXml(e.url)}</loc>`;
1056
+ if (e.lastModified) {
1057
+ const date = e.lastModified instanceof Date ? e.lastModified.toISOString() : e.lastModified;
1058
+ xml += `\n <lastmod>${escapeXml(date)}</lastmod>`;
1059
+ }
1060
+ if (e.changeFrequency) xml += `\n <changefreq>${escapeXml(e.changeFrequency)}</changefreq>`;
1061
+ if (e.priority !== void 0) xml += `\n <priority>${e.priority}</priority>`;
1062
+ xml += "\n </url>";
1063
+ return xml;
1064
+ }).join("\n")}\n</urlset>`;
1065
+ }
1066
+ /** Escape special XML characters. */
1067
+ function escapeXml(str) {
1068
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1069
+ }
1070
+ //#endregion
1071
+ //#region src/server/state-tree-diff.ts
1072
+ /**
1073
+ * State Tree Diffing — Server-side parsing and diffing of X-Timber-State-Tree.
1074
+ *
1075
+ * The client sends X-Timber-State-Tree on navigation requests, listing
1076
+ * the sync segments it has cached. The server diffs this against the
1077
+ * target route's segments to skip re-rendering unchanged sync layouts.
1078
+ *
1079
+ * This is a performance optimization only — NOT a security boundary.
1080
+ * All access.ts files run regardless of the state tree content.
1081
+ * A fabricated state tree can only cause extra rendering work or stale
1082
+ * layouts — never auth bypass.
1083
+ *
1084
+ * See design/19-client-navigation.md §"X-Timber-State-Tree Header"
1085
+ * See design/13-security.md §"State tree manipulation"
1086
+ */
1087
+ /**
1088
+ * Parse the X-Timber-State-Tree header from a request.
1089
+ *
1090
+ * Returns a Set of segment paths the client has cached (with an
1091
+ * additional `slots` property for cached slot paths), or null if
1092
+ * the header is missing, malformed, or empty. Parsing happens before
1093
+ * renderToReadableStream — not inside the React render pass.
1094
+ *
1095
+ * @returns ClientStateTree with segments and slots, or null if no valid state tree
1096
+ */
1097
+ function parseClientStateTree(req) {
1098
+ const header = req.headers.get("X-Timber-State-Tree");
1099
+ if (!header) return null;
1100
+ try {
1101
+ const parsed = JSON.parse(header);
1102
+ const hasSegments = Array.isArray(parsed.segments) && parsed.segments.length > 0;
1103
+ const hasSlots = Array.isArray(parsed.slots) && parsed.slots.length > 0;
1104
+ const hasTreePaths = Array.isArray(parsed.treePaths) && parsed.treePaths.length > 0;
1105
+ if (!hasSegments && !hasSlots && !hasTreePaths) return null;
1106
+ const result = new Set(hasSegments ? parsed.segments : []);
1107
+ result.slots = hasSlots ? new Set(parsed.slots) : null;
1108
+ result.treePaths = hasTreePaths ? new Set(parsed.treePaths) : null;
1109
+ return result;
1110
+ } catch (err) {
1111
+ swallow(err, "malformed X-Timber-State-Tree header");
1112
+ return null;
1113
+ }
1114
+ }
1115
+ //#endregion
1116
+ //#region src/server/pipeline-interception.ts
1117
+ /**
1118
+ * Interception route matching for the request pipeline.
1119
+ *
1120
+ * Matches target URLs against interception rewrites to support the
1121
+ * modal route pattern (soft navigation intercepts).
1122
+ *
1123
+ * Extracted from pipeline.ts to keep files under 500 lines.
1124
+ *
1125
+ * See design/07-routing.md §"Intercepting Routes"
1126
+ */
1127
+ /**
1128
+ * Check if an intercepting route applies for this soft navigation.
1129
+ *
1130
+ * Two conditions must hold:
1131
+ *
1132
+ * 1. The **target** pathname matches the rewrite's intercepted pattern.
1133
+ * 2. The **source** route — the page the user is navigating FROM, per the
1134
+ * validated X-Timber-URL header — renders the segment that owns the slot
1135
+ * holding the intercepting page (`interceptingScope`). Only then does that
1136
+ * slot exist in the rendered tree, so only then can the intercepting page
1137
+ * render.
1138
+ *
1139
+ * Condition 2 replaced a URL-prefix check (TIM-1279). A prefix cannot
1140
+ * distinguish a route-group-scoped interception from a genuinely root-level
1141
+ * one — route groups are invisible in `urlPath`, so both collapse to "/" and
1142
+ * the guard degenerates to always-true. Tree paths carry group identity.
1143
+ * The scope check also subsumes the old segment-boundary rule: a source route
1144
+ * under /feed-private has no "/feed" in its chain.
1145
+ *
1146
+ * A third condition lives with the caller, which owns route matching, and it
1147
+ * applies to the `'slot'` shape only: the route produced by re-matching
1148
+ * `interceptingPrefix` must pass through the scope too. Conditions 1–2 are
1149
+ * about the route the user is leaving; the slot has to exist in the tree that
1150
+ * actually renders. The `'children'` shape resolves its owner from the scope
1151
+ * directly rather than from a pathname, so it satisfies that by construction.
1152
+ *
1153
+ * `resolveSourceScopes` is a thunk so its inputs are only resolved when some
1154
+ * rewrite's pattern actually matches the target — the common case is no match
1155
+ * at all. It returns the two client-supplied answers to "what is on screen?"
1156
+ * as **separate sets, most authoritative first**: the tree paths the client
1157
+ * says it has mounted, then those of the route the departing URL matches. It
1158
+ * returns null only when neither yields anything, which is not a soft
1159
+ * navigation this can reason about.
1160
+ *
1161
+ * They are two sets rather than one union because ranking needs a chain
1162
+ * (TIM-1318). Each channel describes a single rendered chain, so within one the
1163
+ * scopes are strictly nested and the deepest is the innermost owner. Unioned
1164
+ * they are not: once a modal is open the address bar holds the *intercepted*
1165
+ * URL, so the second channel describes the canonical branch — possibly an
1166
+ * exclusive route group the client never mounted — and a deeper scope from
1167
+ * there would outrank the owner actually on screen. Trying the mounted channel
1168
+ * to exhaustion first keeps the fallback the union was there to provide while
1169
+ * ranking only within one chain.
1170
+ *
1171
+ * Returns the winning build-time rewrite, or null. Only build-time values ever
1172
+ * reach the renderer: the client-supplied header selects a rewrite but never
1173
+ * becomes a rendered route or pathname.
1174
+ *
1175
+ * The **first** matching rewrite wins, and the caller does not fall back to the
1176
+ * next one when the winner fails to resolve. That is deliberate and pre-dates
1177
+ * the two shapes: a rewrite that matched the target and the scope described the
1178
+ * author's intent, and quietly rendering some other author's interception
1179
+ * instead would be harder to explain than rendering the canonical route.
1180
+ *
1181
+ * "First" is meaningful because the list arrives **deepest scope first** —
1182
+ * `collectInterceptionRewrites` sorts it at build time, so the deepest owner a
1183
+ * single chain passes through wins and nothing here has to score (TIM-1318).
1184
+ * Tree-walk order used to decide it, which meant renaming a route group flipped
1185
+ * which modal opened. The precondition is a property of the manifest this reads
1186
+ * rather than something it can re-derive, so it is asserted at its source:
1187
+ * `tests/interception.test.ts` §"rewrite order is decided by scope depth".
1188
+ */
1189
+ function findInterceptionMatch(targetPathname, resolveSourceScopes, rewrites) {
1190
+ const candidates = rewrites.filter((rewrite) => pathnameMatchesPattern(targetPathname, rewrite.interceptedPattern));
1191
+ if (candidates.length === 0) return null;
1192
+ const scopes = resolveSourceScopes();
1193
+ if (!scopes) return null;
1194
+ for (const scope of scopes) for (const rewrite of candidates) if (scope.has(rewrite.interceptingScope)) return rewrite;
1195
+ return null;
1196
+ }
1197
+ /**
1198
+ * Check if a pathname matches a URL pattern with dynamic segments.
1199
+ *
1200
+ * Supports [param] (single segment) and [...param] (one or more segments).
1201
+ * Static segments must match exactly.
1202
+ */
1203
+ function pathnameMatchesPattern(pathname, pattern) {
1204
+ const pathParts = pathname === "/" ? [] : pathname.slice(1).split("/");
1205
+ const patternParts = pattern === "/" ? [] : pattern.slice(1).split("/");
1206
+ let pi = 0;
1207
+ for (let i = 0; i < patternParts.length; i++) {
1208
+ const seg = classifyUrlSegment(patternParts[i]);
1209
+ switch (seg.kind) {
1210
+ case "catch-all": return pi < pathParts.length;
1211
+ case "optional-catch-all": return true;
1212
+ case "dynamic": {
1213
+ if (pi >= pathParts.length) return false;
1214
+ const prefix = seg.prefix ?? "";
1215
+ const suffix = seg.suffix ?? "";
1216
+ const pathPart = pathParts[pi];
1217
+ if (prefix || suffix) {
1218
+ if (pathPart.length <= prefix.length + suffix.length || prefix && !pathPart.startsWith(prefix) || suffix && !pathPart.endsWith(suffix)) return false;
1219
+ }
1220
+ pi++;
1221
+ continue;
1222
+ }
1223
+ case "static":
1224
+ if (pi >= pathParts.length || pathParts[pi] !== seg.value) return false;
1225
+ pi++;
1226
+ continue;
1227
+ }
1228
+ }
1229
+ return pi === pathParts.length;
1230
+ }
1231
+ /**
1232
+ * Resolve which route renders a soft navigation that an intercepting route
1233
+ * may claim. Returns the canonical match unchanged when no rewrite applies.
1234
+ *
1235
+ * Runs during route setup, so anything it throws is a `'setup'` outcome.
1236
+ */
1237
+ function resolveInterceptedRoute(config, req, canonicalPathname, canonicalMatch) {
1238
+ const stripTrailingSlash = config.stripTrailingSlash ?? true;
1239
+ let match = canonicalMatch;
1240
+ let interception;
1241
+ if (isRscRequest(req) && config.interceptionRewrites?.length) {
1242
+ const rawSourceUrl = req.headers.get("X-Timber-URL");
1243
+ const validatedSourceUrl = rawSourceUrl ? validateInterceptionHeader(rawSourceUrl, stripTrailingSlash) : null;
1244
+ if (validatedSourceUrl) {
1245
+ const resolveSourceScopes = () => {
1246
+ const mounted = new Set(parseClientStateTree(req)?.treePaths ?? []);
1247
+ const sourceRoute = config.matchRoute(validatedSourceUrl);
1248
+ const scopes = [mounted, new Set(sourceRoute ? computeSegmentTreePaths(sourceRoute.segments) : [])].filter((scope) => scope.size > 0);
1249
+ return scopes.length > 0 ? scopes : null;
1250
+ };
1251
+ const rewrite = findInterceptionMatch(canonicalPathname, resolveSourceScopes, config.interceptionRewrites);
1252
+ if (rewrite?.mode === "children") {
1253
+ const interceptedMatch = config.matchInterceptedRoute?.(canonicalPathname, rewrite.interceptingScope);
1254
+ if (interceptedMatch) match = interceptedMatch;
1255
+ } else if (rewrite) {
1256
+ const sourceMatch = config.matchRoute(rewrite.interceptingPrefix);
1257
+ if (sourceMatch !== null && computeSegmentTreePaths(sourceMatch.segments).includes(rewrite.interceptingScope)) {
1258
+ match = sourceMatch;
1259
+ interception = { targetPathname: canonicalPathname };
1260
+ }
1261
+ }
1262
+ }
1263
+ }
1264
+ return {
1265
+ match,
1266
+ interception
1267
+ };
1268
+ }
1269
+ /**
1270
+ * Validate and canonicalize the X-Timber-URL header value.
1271
+ *
1272
+ * Returns the canonical pathname if valid, or null if rejected:
1273
+ * - Must start with '/' (relative pathname, no scheme/authority)
1274
+ * - Must not contain control characters
1275
+ * - Must pass canonicalization (no encoded separators, null bytes, etc.)
1276
+ */
1277
+ function validateInterceptionHeader(raw, stripTrailingSlash) {
1278
+ if (!raw.startsWith("/")) return null;
1279
+ if (raw.startsWith("//")) return null;
1280
+ for (let i = 0; i < raw.length; i++) {
1281
+ const code = raw.charCodeAt(i);
1282
+ if (code <= 31 || code === 127) return null;
1283
+ }
1284
+ const result = canonicalize(raw, stripTrailingSlash);
1285
+ if (!result.ok) return null;
1286
+ return result.pathname;
1287
+ }
1288
+ //#endregion
1289
+ //#region src/server/response-cache-policy.ts
1290
+ /**
1291
+ * Legacy shared-cache control fields that predate RFC 9213 and so do not
1292
+ * follow the `<target>-Cache-Control` naming pattern — the one part of this
1293
+ * rule that cannot be matched by shape. `X-Accel-Expires` is nginx
1294
+ * `proxy_cache`, which gives it precedence over `Cache-Control`.
1295
+ *
1296
+ * A closed historical set, unlike the vendor `*-Cache-Control` names, which
1297
+ * keep being minted and are handled by pattern below.
1298
+ */
1299
+ var LEGACY_CDN_CACHE_HEADERS = [
1300
+ "Surrogate-Control",
1301
+ "Edge-Control",
1302
+ "X-Accel-Expires"
1303
+ ];
1304
+ /**
1305
+ * Whether a response header overrides `Cache-Control` at a shared cache.
1306
+ *
1307
+ * Matched by *shape*, not by vendor name. RFC 9213 defines targeted fields
1308
+ * as `<target>-Cache-Control`, and every CDN spells its own: `CDN-`,
1309
+ * `Cloudflare-CDN-`, `Netlify-CDN-`, `Vercel-CDN-`, and whichever ships
1310
+ * next. A vendor list here is a denylist with an unbounded tail — it was
1311
+ * extended twice during review, each time by one name, which is the signal
1312
+ * that enumeration is the wrong shape. The suffix rule is total.
1313
+ *
1314
+ * `Cache-Control` itself does not match (no `-` prefix) and is replaced
1315
+ * separately.
1316
+ */
1317
+ function isSharedCacheOverride(name) {
1318
+ const lowered = name.toLowerCase();
1319
+ return lowered.endsWith("-cache-control") || LEGACY_CDN_CACHE_HEADERS.some((legacy) => legacy.toLowerCase() === lowered);
1320
+ }
1321
+ /** Disable caching, including CDN overrides that take precedence over Cache-Control. */
1322
+ function preventResponseCaching(headers) {
1323
+ const overrides = [];
1324
+ for (const name of headers.keys()) if (isSharedCacheOverride(name)) overrides.push(name);
1325
+ for (const name of overrides) headers.delete(name);
1326
+ headers.set("Cache-Control", "private, no-store");
1327
+ }
1328
+ //#endregion
1329
+ //#region src/server/error-response-headers.ts
1330
+ /** Preserve request-wide metadata when replacing a failed response body. */
1331
+ function errorResponseHeaders(responseHeaders) {
1332
+ const headers = new Headers(responseHeaders);
1333
+ headers.delete("Content-Length");
1334
+ headers.delete("Content-Encoding");
1335
+ headers.delete("Transfer-Encoding");
1336
+ preventResponseCaching(headers);
1337
+ headers.delete("ETag");
1338
+ headers.delete("Last-Modified");
1339
+ headers.delete("Expires");
1340
+ headers.delete("Age");
1341
+ return headers;
1342
+ }
1343
+ //#endregion
1344
+ //#region src/server/rsc-error-envelope.ts
1345
+ /**
1346
+ * Build the hard-navigate envelope for a status code.
1347
+ *
1348
+ * Preserve middleware's request-wide headers and cookies, but discard the
1349
+ * abandoned body's framing, validators, and cache policy. Every producer,
1350
+ * including errors returned normally during render, gets this cleanup here.
1351
+ * The input header bag is left intact. The body deliberately carries no message —
1352
+ * only the status already on the wire (design/13-security.md principle 4).
1353
+ */
1354
+ function rscErrorEnvelope(status, responseHeaders) {
1355
+ const headers = errorResponseHeaders(responseHeaders);
1356
+ headers.set(TIMBER_ERROR_HEADER, "1");
1357
+ headers.set("content-type", "application/json; charset=utf-8");
1358
+ return new Response(JSON.stringify({
1359
+ error: true,
1360
+ status
1361
+ }), {
1362
+ status,
1363
+ headers
1364
+ });
1365
+ }
1366
+ //#endregion
1367
+ //#region src/server/pipeline-outcome.ts
1368
+ /**
1369
+ * Pipeline outcome translator — converts a `PhaseOutcome` (the value
1370
+ * each phase function returns) into a final `Response`.
1371
+ *
1372
+ * Lifted out of `pipeline-phases.ts` (TIM-853) so the per-phase try /
1373
+ * catch logic and the terminal Response-building logic each live in
1374
+ * their own file. The phases produce values; this module is the single
1375
+ * source of truth for how those values become wire responses.
1376
+ *
1377
+ * See design/07-routing.md §"Request Lifecycle".
1378
+ */
1379
+ function rscErrorResponse(isRsc, status, headers) {
1380
+ if (isRsc) return rscErrorEnvelope(status, headers);
1381
+ return new Response(null, {
1382
+ status,
1383
+ headers: errorResponseHeaders(headers)
1384
+ });
1385
+ }
1386
+ /**
1387
+ * Resolve the header bag a signal/error outcome is written onto, with the
1388
+ * cookie jar applied exactly once.
1389
+ *
1390
+ * Ownership rule (design/49 §P0b): the phase that owns `responseHeaders`
1391
+ * applies the jar to it once; the translator applies it only for outcomes
1392
+ * that arrive without it. Render-phase outcomes already carry the jar —
1393
+ * `runRenderPhase()` applies it on entry, with or without a middleware
1394
+ * chain — so re-applying here emitted every middleware `Set-Cookie` twice
1395
+ * (TIM-1491).
1396
+ *
1397
+ * The guard is "not render", not "only middleware": proxy-phase signals
1398
+ * reach the translator with no `responseHeaders` at all, and for a
1399
+ * `proxy.ts` that awaits `next()` and then throws, this is the only place
1400
+ * the cookies downstream middleware set can reach the wire
1401
+ * (design/29-cookies.md §"proxy.ts Cookie Access"). Middleware-phase
1402
+ * signals throw before that phase's own application, so they need it too,
1403
+ * and so do setup-phase outcomes, which come before any phase applied it.
1404
+ *
1405
+ * `fresh` creates the bag for an outcome that arrives without one.
1406
+ */
1407
+ function outcomeHeaders(outcome, ctx, fresh = () => new Headers()) {
1408
+ const headers = ctx.responseHeaders ?? fresh();
1409
+ if (outcome.phase !== "render") applyCookieJar(headers);
1410
+ return headers;
1411
+ }
1412
+ /** Whether the outcome is for a route.ts match, which answers in its own format. */
1413
+ function isApiRouteMatch(ctx) {
1414
+ return ctx.match !== void 0 && isApiRouteChain(ctx.match.segments);
1415
+ }
1416
+ /**
1417
+ * Report an error outcome once: log it under its phase, fire
1418
+ * `onRequestError`, and hand it to the dev overlay. An error from a
1419
+ * route.ts handler — or anything else in its render phase — logs as a route
1420
+ * error and reports as phase `'route'`.
1421
+ */
1422
+ async function reportError(config, error, phase, ctx) {
1423
+ const { method, path } = ctx;
1424
+ switch (phase) {
1425
+ case "proxy":
1426
+ logProxyError({ error });
1427
+ break;
1428
+ case "setup":
1429
+ logRouteSetupError({
1430
+ method,
1431
+ path,
1432
+ error
1433
+ });
1434
+ break;
1435
+ case "middleware":
1436
+ logMiddlewareError({
1437
+ method,
1438
+ path,
1439
+ error
1440
+ });
1441
+ break;
1442
+ case "render": if (isApiRouteMatch(ctx)) logRouteError({
1443
+ method,
1444
+ path,
1445
+ error
1446
+ });
1447
+ else logRenderError({
1448
+ method,
1449
+ path,
1450
+ error
1451
+ });
1452
+ }
1453
+ await fireOnRequestError(error, ctx.req, phase, ctx.match?.segments);
1454
+ if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, phase);
1455
+ }
1456
+ /** The outcome for a value a phase threw: a signal, or an error. */
1457
+ function thrownOutcome(phase, thrown) {
1458
+ if (isRedirectSignal(thrown)) return {
1459
+ kind: "redirect",
1460
+ phase,
1461
+ signal: thrown
1462
+ };
1463
+ if (isDenySignal(thrown)) return {
1464
+ kind: "deny",
1465
+ phase,
1466
+ signal: thrown
1467
+ };
1468
+ return {
1469
+ kind: "error",
1470
+ phase,
1471
+ error: thrown
1472
+ };
1473
+ }
1474
+ /**
1475
+ * Terminal outcome handler — converts a `PhaseOutcome` into a final
1476
+ * `Response`, applying cookies, building redirects, rendering deny pages
1477
+ * and fallback error pages, and firing instrumentation hooks.
1478
+ *
1479
+ * This is the single source of truth for how phase outputs become wire
1480
+ * responses; the per-phase try/catch blocks now produce values, not
1481
+ * Responses, so the conversion logic lives in exactly one place.
1482
+ */
1483
+ async function outcomeToResponse(config, outcome, ctx) {
1484
+ switch (outcome.kind) {
1485
+ case "response": {
1486
+ const finalResponse = cloneWithMutableHeaders(outcome.response);
1487
+ if (outcome.phase === "proxy") return finalResponse;
1488
+ if (outcome.phase === "middleware" && ctx.responseHeaders) {
1489
+ applyCookieJar(finalResponse.headers);
1490
+ mergeMissingHeaders(finalResponse.headers, ctx.responseHeaders);
1491
+ logMiddlewareShortCircuit({
1492
+ method: ctx.method,
1493
+ path: ctx.path,
1494
+ status: finalResponse.status
1495
+ });
1496
+ }
1497
+ if (outcome.phase === "render") markResponseFlushed();
1498
+ return finalResponse;
1499
+ }
1500
+ case "redirect": {
1501
+ const headers = outcomeHeaders(outcome, ctx);
1502
+ const navigation = isRscRequest(ctx.req) && !isApiRouteMatch(ctx);
1503
+ return buildRedirectResponse(outcome.signal, headers, navigation);
1504
+ }
1505
+ case "deny": {
1506
+ const headers = outcomeHeaders(outcome, ctx, dynamicResponseHeaders);
1507
+ if (config.renderDenyFallback) try {
1508
+ return cloneWithMutableHeaders(await config.renderDenyFallback(outcome.signal, ctx.req, headers, ctx.match));
1509
+ } catch (denyRenderError) {
1510
+ logRenderError({
1511
+ method: ctx.method,
1512
+ path: ctx.path,
1513
+ error: denyRenderError
1514
+ });
1515
+ await fireOnRequestError(denyRenderError, ctx.req, "render", ctx.match?.segments);
1516
+ if (config.onPipelineError && denyRenderError instanceof Error) config.onPipelineError(denyRenderError, "render");
1517
+ if (isRscRequest(ctx.req)) return rscErrorEnvelope(outcome.signal.status, headers);
1518
+ }
1519
+ if (isDebug()) console.warn(`[timber] DenySignal(${outcome.signal.status}) from ${outcome.phase} phase — no renderDenyFallback configured, returning bare ${outcome.signal.status} response\n Request: ${ctx.method} ${ctx.path}\n Add a status-code file (e.g. 404.tsx) or error.tsx to render a custom deny page.`);
1520
+ return new Response(null, {
1521
+ status: outcome.signal.status,
1522
+ headers
1523
+ });
1524
+ }
1525
+ case "error": {
1526
+ const isRsc = isRscRequest(ctx.req);
1527
+ const headers = outcomeHeaders(outcome, ctx);
1528
+ await reportError(config, outcome.error, outcome.phase, ctx);
1529
+ if (outcome.phase === "proxy" || outcome.phase === "middleware") return rscErrorResponse(isRsc, 500, headers);
1530
+ if (isRsc && !isApiRouteMatch(ctx)) return rscErrorResponse(true, 500, headers);
1531
+ if (config.renderFallbackError) try {
1532
+ return cloneWithMutableHeaders(await config.renderFallbackError(outcome.error, ctx.req, errorResponseHeaders(headers), ctx.match));
1533
+ } catch (fallbackRenderError) {
1534
+ logRenderError({
1535
+ method: ctx.method,
1536
+ path: ctx.path,
1537
+ error: fallbackRenderError
1538
+ });
1539
+ await fireOnRequestError(fallbackRenderError, ctx.req, "render", ctx.match?.segments);
1540
+ if (config.onPipelineError && fallbackRenderError instanceof Error) config.onPipelineError(fallbackRenderError, "render");
1541
+ }
1542
+ return rscErrorResponse(false, 500, headers);
1543
+ }
1544
+ }
1545
+ }
1546
+ //#endregion
1547
+ //#region src/server/pipeline-phases.ts
1548
+ /**
1549
+ * Pipeline phase functions — module-level free functions that take their
1550
+ * dependencies as explicit parameters. Each phase returns a `PhaseOutcome`
1551
+ * (a discriminated union over response / redirect / deny / error) defined
1552
+ * in `pipeline-outcome.ts`. The terminal `outcomeToResponse` (also in
1553
+ * `pipeline-outcome.ts`) translates outcomes into Responses.
1554
+ *
1555
+ * Lifted out of `createPipeline` so each phase can be unit-tested in
1556
+ * isolation. The lift is mechanical — these functions used to be closures
1557
+ * over `config`; they now take `config` as an explicit parameter.
1558
+ *
1559
+ * See design/07-routing.md §"Request Lifecycle", design/02-rendering-pipeline.md §"Request Flow".
1560
+ */
1561
+ /**
1562
+ * Run the proxy.ts phase. Calls user proxy code with a `next()` continuation.
1563
+ *
1564
+ * When `next` is provided, it replaces the default `handleRequest` as the
1565
+ * continuation — this lets the pipeline inject action dispatch between proxy
1566
+ * and route matching (TIM-1213). The proxy resolver was picked at pipeline
1567
+ * construction time so the hot path sees no per-request branching on the
1568
+ * `ProxyConfig` discriminant.
1569
+ */
1570
+ async function runProxyPhase(config, getProxy, req, method, path, next) {
1571
+ const detailed = config.serverTiming === "detailed";
1572
+ try {
1573
+ const proxyExport = await getProxy();
1574
+ const continuation = next ?? (() => handleRequest(config, req, method, path, true));
1575
+ const proxyFn = () => runProxy(proxyExport, req, continuation);
1576
+ return {
1577
+ kind: "response",
1578
+ phase: "proxy",
1579
+ response: await withSpan("timber.proxy", {}, () => detailed ? withTiming("proxy", "proxy.ts", proxyFn) : proxyFn())
1580
+ };
1581
+ } catch (error) {
1582
+ return thrownOutcome("proxy", error);
1583
+ }
1584
+ }
1585
+ /**
1586
+ * Run the middleware chain phase. If the chain short-circuits with a Response,
1587
+ * returns it as a 'response' outcome. Otherwise applies the request header
1588
+ * overlay and falls through to the render phase.
1589
+ */
1590
+ async function runMiddlewarePhase(config, req, match, responseHeaders, renderContext) {
1591
+ const detailed = config.serverTiming === "detailed";
1592
+ const requestHeaderOverlay = new Headers();
1593
+ const ctx = {
1594
+ req,
1595
+ requestHeaders: requestHeaderOverlay,
1596
+ headers: responseHeaders,
1597
+ segmentParams: match.segmentParams,
1598
+ get nonce() {
1599
+ return requestCspNonce();
1600
+ },
1601
+ earlyHints: (hints) => {
1602
+ for (const hint of hints) {
1603
+ let value;
1604
+ if (hint.as !== void 0) value = `<${hint.href}>; as=${hint.as}; rel=${hint.rel}`;
1605
+ else value = `<${hint.href}>; rel=${hint.rel}`;
1606
+ if (hint.crossOrigin !== void 0) value += `; crossorigin=${hint.crossOrigin}`;
1607
+ if (hint.fetchPriority !== void 0) value += `; fetchpriority=${hint.fetchPriority}`;
1608
+ responseHeaders.append("Link", value);
1609
+ }
1610
+ }
1611
+ };
1612
+ try {
1613
+ const chainFn = () => runMiddlewareChain(match.middlewareChain, ctx);
1614
+ const middlewareResponse = await (async () => {
1615
+ setMutableCookieContext(true);
1616
+ try {
1617
+ return await withSpan("timber.middleware", {}, () => detailed ? withTiming("mw", "middleware.ts", chainFn) : chainFn());
1618
+ } finally {
1619
+ setMutableCookieContext(false);
1620
+ }
1621
+ })();
1622
+ if (middlewareResponse) return {
1623
+ kind: "response",
1624
+ phase: "middleware",
1625
+ response: middlewareResponse
1626
+ };
1627
+ applyRequestHeaderOverlay(requestHeaderOverlay);
1628
+ return runRenderPhase(config, req, match, responseHeaders, renderContext);
1629
+ } catch (error) {
1630
+ return thrownOutcome("middleware", error);
1631
+ }
1632
+ }
1633
+ /**
1634
+ * Run the render phase. Wraps the configured renderer in a span and a
1635
+ * timing scope, and translates thrown signals into outcome variants.
1636
+ *
1637
+ * The render phase owns `responseHeaders` from here on, so it is the one
1638
+ * place the cookie jar is applied to that bag (design/29-cookies.md
1639
+ * §"Applied Exactly Once"). Doing it here rather than in the middleware
1640
+ * phase means every render-phase outcome — a 200, or a deny / redirect /
1641
+ * error thrown from render — carries the jar whether or not a middleware
1642
+ * chain ran, and `outcomeToResponse` can skip render-phase outcomes
1643
+ * unconditionally (TIM-1491). Application precedes `config.render` so
1644
+ * middleware cookie writes are visible to route-handler header merging,
1645
+ * while handler `Set-Cookie` values still come after them and take
1646
+ * precedence.
1647
+ */
1648
+ async function runRenderPhase(config, req, match, responseHeaders, { canonicalPathname, interception }) {
1649
+ const detailed = config.serverTiming === "detailed";
1650
+ applyCookieJar(responseHeaders);
1651
+ try {
1652
+ const renderFn = () => config.render(req, match, responseHeaders, interception);
1653
+ return {
1654
+ kind: "response",
1655
+ phase: "render",
1656
+ response: await withSpan("timber.render", { "http.route": canonicalPathname }, () => detailed ? withTiming("render", "RSC + SSR render", renderFn) : renderFn())
1657
+ };
1658
+ } catch (error) {
1659
+ return thrownOutcome("render", error);
1660
+ }
1661
+ }
1662
+ /**
1663
+ * Process a single request from canonicalization through phase dispatch.
1664
+ *
1665
+ * Stages: canonicalize → metadata routes → auto-sitemap → version skew →
1666
+ * route match → interception → early hints → param coercion → middleware →
1667
+ * render → outcome translation. Pre-routing short-circuits return Responses
1668
+ * directly; post-match dispatch goes through `outcomeToResponse`.
1669
+ *
1670
+ * Used both as the top-level entry (when no proxy.ts is configured) and as
1671
+ * the `next()` continuation passed to `runProxy()`.
1672
+ *
1673
+ * @param pathIsCanonical When true, `path` has already been canonicalized by
1674
+ * `createPipeline` — skip re-canonicalization to prevent double-decode.
1675
+ * When false (default), runs canonicalize as a safety net for direct callers.
1676
+ */
1677
+ async function handleRequest(config, req, method, path, pathIsCanonical) {
1678
+ const stripTrailingSlash = config.stripTrailingSlash ?? true;
1679
+ let canonicalPathname;
1680
+ if (pathIsCanonical) canonicalPathname = path;
1681
+ else {
1682
+ const result = canonicalize(path, stripTrailingSlash);
1683
+ if (!result.ok) {
1684
+ if (isDebug()) console.warn(`[timber] URL canonicalization rejected ${method} ${path} — responding with ${result.status}\n This usually means the URL contains encoded separators (%2f, %5c),\n null bytes (%00), path traversal (..), or malformed percent-encoding.`);
1685
+ return new Response(null, { status: result.status });
1686
+ }
1687
+ canonicalPathname = result.pathname;
1688
+ }
1689
+ if (config.matchMetadataRoute) {
1690
+ const metaMatch = config.matchMetadataRoute(canonicalPathname);
1691
+ if (metaMatch) try {
1692
+ if (metaMatch.isStatic) return await serveStaticMetadataFile(metaMatch);
1693
+ setSegmentParams(metaMatch.segmentParams);
1694
+ const mod = await loadModule(metaMatch.file);
1695
+ if (typeof mod.default !== "function") {
1696
+ if (isDebug()) console.warn(`[timber] Metadata route ${metaMatch.file} does not export a default function — responding with 500\n Metadata routes must export a default function that returns the metadata content.`);
1697
+ return new Response("Metadata route must export a default function", { status: 500 });
1698
+ }
1699
+ const handlerResult = await mod.default();
1700
+ if (handlerResult instanceof Response) return cloneWithMutableHeaders(handlerResult);
1701
+ const contentType = metaMatch.contentType;
1702
+ let body;
1703
+ if (typeof handlerResult === "string") body = handlerResult;
1704
+ else if (contentType === "application/xml") body = serializeSitemap(handlerResult);
1705
+ else if (contentType === "application/manifest+json") body = JSON.stringify(handlerResult, null, 2);
1706
+ else body = String(handlerResult);
1707
+ return new Response(body, {
1708
+ status: 200,
1709
+ headers: {
1710
+ "Content-Type": `${contentType}; charset=utf-8`,
1711
+ "Cache-Control": "public, max-age=14400, must-revalidate"
1712
+ }
1713
+ });
1714
+ } catch (error) {
1715
+ if (isRedirectSignal(error)) return new Response(null, {
1716
+ status: error.status,
1717
+ headers: { Location: error.location }
1718
+ });
1719
+ if (isDenySignal(error)) return new Response(null, { status: error.status });
1720
+ logRenderError({
1721
+ method,
1722
+ path,
1723
+ error
1724
+ });
1725
+ await fireOnRequestError(error, req, "route");
1726
+ if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, "metadata-route");
1727
+ return new Response(null, { status: 500 });
1728
+ }
1729
+ }
1730
+ if (config.autoSitemapHandler) try {
1731
+ const sitemapResponse = await config.autoSitemapHandler(canonicalPathname);
1732
+ if (sitemapResponse) return cloneWithMutableHeaders(sitemapResponse);
1733
+ } catch (error) {
1734
+ logRenderError({
1735
+ method,
1736
+ path,
1737
+ error
1738
+ });
1739
+ await fireOnRequestError(error, req, "route");
1740
+ if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, "auto-sitemap");
1741
+ return new Response(null, { status: 500 });
1742
+ }
1743
+ const isRsc = isRscRequest(req);
1744
+ if (isRsc) {
1745
+ if (!checkVersionSkew(req).ok) {
1746
+ const reloadHeaders = new Headers();
1747
+ applyReloadHeaders(reloadHeaders);
1748
+ return new Response(null, {
1749
+ status: 204,
1750
+ headers: reloadHeaders
1751
+ });
1752
+ }
1753
+ }
1754
+ const responseHeaders = dynamicResponseHeaders();
1755
+ const renderNotFound = async (headers) => {
1756
+ if (!config.renderNoMatch) return new Response(null, { status: 404 });
1757
+ try {
1758
+ return cloneWithMutableHeaders(await config.renderNoMatch(req, headers));
1759
+ } catch (error) {
1760
+ return outcomeToResponse(config, thrownOutcome("render", error), {
1761
+ req,
1762
+ method,
1763
+ path,
1764
+ responseHeaders: headers
1765
+ });
1766
+ }
1767
+ };
1768
+ let match = null;
1769
+ let interception;
1770
+ try {
1771
+ match = config.matchRoute(canonicalPathname);
1772
+ ({match, interception} = resolveInterceptedRoute(config, req, canonicalPathname, match));
1773
+ if (!match) {
1774
+ if (isDebug()) console.warn(`[timber] No route matched for pathname: ${canonicalPathname}\n Input path: ${path}\n Method: ${method}`);
1775
+ return await renderNotFound(responseHeaders);
1776
+ }
1777
+ if (config.earlyHints && !isRsc && !isApiRouteChain(match.segments)) try {
1778
+ await config.earlyHints(match, req, responseHeaders);
1779
+ } catch (err) {
1780
+ swallow(err, "early hints hook threw");
1781
+ }
1782
+ try {
1783
+ match = {
1784
+ ...match,
1785
+ segmentParams: await coerceSegmentParams(match)
1786
+ };
1787
+ } catch (error) {
1788
+ if (error instanceof ParamCoercionError) {
1789
+ if (isDebug()) {
1790
+ const segmentChain = match.segments.map((s) => s.segmentName || "/").join(" → ");
1791
+ console.warn(`[timber] Param coercion failed for ${method} ${canonicalPathname} — responding with 404\n Matched segments: ${segmentChain}\n Error: ${error.message}\n This usually means a codec in app/schema.ts rejected the URL value.\n Check the codec for the bracket key of the segment named above.`);
1792
+ }
1793
+ if (isApiRouteChain(match.segments)) return new Response(null, { status: 404 });
1794
+ return await renderNotFound(responseHeaders);
1795
+ }
1796
+ throw error;
1797
+ }
1798
+ setSegmentParams(match.segmentParams);
1799
+ const segmentPath = match.segments.map((s) => s.segmentName).filter(Boolean).join("/") || "/";
1800
+ setMatchedSegmentPath(segmentPath.startsWith("/") ? segmentPath : `/${segmentPath}`);
1801
+ } catch (error) {
1802
+ return outcomeToResponse(config, thrownOutcome("setup", error), {
1803
+ req,
1804
+ method,
1805
+ path,
1806
+ responseHeaders,
1807
+ match: match ?? void 0
1808
+ });
1809
+ }
1810
+ return outcomeToResponse(config, match.middlewareChain.length > 0 ? await runMiddlewarePhase(config, req, match, responseHeaders, {
1811
+ canonicalPathname,
1812
+ interception
1813
+ }) : await runRenderPhase(config, req, match, responseHeaders, {
1814
+ canonicalPathname,
1815
+ interception
1816
+ }), {
1817
+ req,
1818
+ method,
1819
+ path,
1820
+ responseHeaders,
1821
+ match
1822
+ });
1823
+ }
1824
+ //#endregion
1825
+ //#region src/server/rsc-cache-key-guard.ts
1826
+ /**
1827
+ * Origin-side binding of the `_rsc` cache key to the request that produced
1828
+ * the response (TIM-1268).
1829
+ *
1830
+ * The key is deterministic, and deliberately so — that is what makes RSC
1831
+ * payloads cacheable. But deterministic also means *predictable*: anyone can
1832
+ * compute the key another client will request. Without this guard, a caller
1833
+ * could request `/about?_rsc=<victim's key>` while sending its own
1834
+ * `X-Timber-State-Tree`, and a shared cache that keys by URL — the exact
1835
+ * environment this feature targets — would store that variant under the
1836
+ * victim's key. Every subsequent legitimate navigation would then be served
1837
+ * a payload rendered for someone else's segment tree or deployment.
1838
+ *
1839
+ * The guard is a *storability* rule, not a rejection: if the claimed key
1840
+ * does not match the key derived from the received headers, the response
1841
+ * loses its shared-cache directives. Mismatched requests are still served
1842
+ * normally, they just cannot be stored for anyone else.
1843
+ *
1844
+ * Why not Next.js's 307 self-correction: a redirect is cacheable in its own
1845
+ * right, costs a round trip on every legitimate miss, and would make the
1846
+ * server's behaviour depend on a parameter it otherwise never reads. This
1847
+ * rule closes the same hole by making the poisoned entry unstorable at the
1848
+ * source, in the response the developer already has to opt into sharing.
1849
+ *
1850
+ * See design/19-client-navigation.md §"The `_rsc` cache key".
1851
+ */
1852
+ /**
1853
+ * Whether the request's `_rsc` parameter matches the key derived from its
1854
+ * own headers.
1855
+ *
1856
+ * Requests without the parameter are not claiming a key at all, so they are
1857
+ * left alone — the bare document URL is the developer's declared territory
1858
+ * (see design/32-response-caching.md, TIM-1264). Neither is a parameter
1859
+ * whose value could not be one of our digests: `_rsc` is not reserved, and
1860
+ * an application that already uses it for its own search state
1861
+ * (`/search?_rsc=foo`) must not have its cache policy silently revoked by a
1862
+ * guard that mistook that for a forgery.
1863
+ */
1864
+ async function hasValidRscCacheKey(req) {
1865
+ if (!req.url.includes("?")) return true;
1866
+ let values;
1867
+ try {
1868
+ values = new URL(req.url).searchParams.getAll(RSC_KEY_PARAM);
1869
+ } catch {
1870
+ return false;
1871
+ }
1872
+ const claims = values.filter(isRscCacheKeyShape);
1873
+ if (claims.length === 0) return true;
1874
+ const expected = await rscCacheKey((name) => req.headers.get(name));
1875
+ if (expected === null) return false;
1876
+ return claims.every((claim) => claim === expected);
1877
+ }
1878
+ /**
1879
+ * Ensure a Flight payload can never be interpreted as a document.
1880
+ *
1881
+ * Applied here, at the pipeline's single exit, rather than where each
1882
+ * payload response is built. Six places build one — the success path, three
1883
+ * in `deny-renderer.ts`, the action paths — and hardening them one at a time
1884
+ * is how you end up hardening five of six. The response is Flight if it says
1885
+ * it is; that is the only test that covers every producer, including ones
1886
+ * added later.
1887
+ *
1888
+ * Matters because the payload URL is observable and, on a shared-cache hit,
1889
+ * the origin is never consulted — so a top-level navigation can be handed
1890
+ * these bytes with no request-time check available. See design/13-security.md
1891
+ * #36c.
1892
+ */
1893
+ function hardenRscPayloadResponse(response) {
1894
+ if (!isRscContentType(response.headers.get("content-type"))) return;
1895
+ response.headers.set("X-Content-Type-Options", "nosniff");
1896
+ }
1897
+ /**
1898
+ * Make a response unstorable by any shared cache when its request claimed an
1899
+ * `_rsc` key that does not match its own headers.
1900
+ *
1901
+ * The end state is unconditional and therefore statable: **a mismatched
1902
+ * response is never stored by any cache.** Every targeted cache-control
1903
+ * field is removed and `Cache-Control` is replaced outright, rather than
1904
+ * downgrading only when a shared directive is spotted. Conditional variants
1905
+ * of this were tried and were wrong twice over — they missed the CDN-
1906
+ * specific headers, and leaving `Cache-Control` alone when it looked
1907
+ * harmless lets a CDN apply heuristic caching to a response that has none.
1908
+ *
1909
+ * The cost is that a mismatched request loses developer-chosen *browser*
1910
+ * caching too. That is a request no timber client ever makes.
1911
+ *
1912
+ * Purge tags (`Surrogate-Key`, `Cache-Tag`) are deliberately left alone:
1913
+ * they address stored entries, and this response is not stored.
1914
+ *
1915
+ * Mutates `response.headers` in place; the pipeline guarantees mutable
1916
+ * headers at this point (see TIM-866).
1917
+ */
1918
+ async function enforceRscCacheKey(req, response) {
1919
+ if (await hasValidRscCacheKey(req)) return;
1920
+ preventResponseCaching(response.headers);
1921
+ }
1922
+ //#endregion
1923
+ //#region src/server/head-response.ts
1924
+ /**
1925
+ * HEAD body stripping — RFC 9110 §9.3.2.
1926
+ *
1927
+ * "The HEAD method is identical to GET except that the server MUST NOT send
1928
+ * content in the response." The headers, however, must be the ones the
1929
+ * corresponding GET would have produced — which is why timber renders the
1930
+ * route in full and discards the body here rather than short-circuiting the
1931
+ * render. Content-Type, Content-Length, Set-Cookie, cache directives and
1932
+ * everything else ride through untouched.
1933
+ *
1934
+ * Node's `ServerResponse` happens to discard writes for HEAD, so the wire is
1935
+ * usually correct even without this. That is a property of one adapter, not of
1936
+ * the `Response` object: static generation, the Cloudflare and Nitro adapters,
1937
+ * tests, and anything calling the pipeline handler as a function all observe
1938
+ * the `Response` directly and saw the full body before TIM-1324.
1939
+ *
1940
+ * The pipeline applies this at its single exit so every response shape is
1941
+ * covered — page render, API route, metadata route, deny page, error page,
1942
+ * proxy short-circuit. The unit-level producer that is also reachable as a
1943
+ * standalone function (`handleRouteRequest`) calls it too, so it honours HEAD
1944
+ * on its own; it shares this implementation rather than repeating it, and
1945
+ * applying it twice is a no-op.
1946
+ */
1947
+ /**
1948
+ * Return a bodyless copy of `response` when `method` is HEAD.
1949
+ *
1950
+ * A no-op for every other method and for responses that already carry no body
1951
+ * (204, 304, redirects), so callers do not need to pre-check either.
1952
+ *
1953
+ * The discarded stream is cancelled: an SSR render that reached this point has
1954
+ * a live producer attached, and dropping the reference without cancelling
1955
+ * leaks it for the lifetime of the render (design/13-security.md A4).
1956
+ */
1957
+ function stripHeadBody(response, method) {
1958
+ if (method.toUpperCase() !== "HEAD") return response;
1959
+ const body = response.body;
1960
+ if (body === null) return response;
1961
+ try {
1962
+ body.cancel().catch(() => {});
1963
+ } catch {}
1964
+ return new Response(null, {
1965
+ status: response.status,
1966
+ statusText: response.statusText,
1967
+ headers: response.headers
1968
+ });
1969
+ }
1970
+ //#endregion
1971
+ //#region src/server/pipeline.ts
1972
+ /**
1973
+ * Create the request handler from a pipeline configuration.
1974
+ *
1975
+ * Returns a function that processes an incoming Request through all pipeline
1976
+ * stages and produces a Response. This is the top-level entry point for the
1977
+ * server. The body is intentionally small — phase logic lives in
1978
+ * `pipeline-phases.ts`. This function only owns the per-request setup that
1979
+ * has to wrap the entire dispatch: trace ID, request context ALS, span
1980
+ * scope, Server-Timing header emission, and the active-request counter.
1981
+ */
1982
+ function createPipeline(config) {
1983
+ const proxyResolver = makeProxyResolver(config.proxy);
1984
+ const slowRequestMs = config.slowRequestMs ?? 3e3;
1985
+ const serverTiming = config.serverTiming ?? "total";
1986
+ let activeRequests = 0;
1987
+ const pipelineFn = async (req, cookies) => {
1988
+ const url = new URL(req.url);
1989
+ const method = req.method;
1990
+ const path = url.pathname;
1991
+ const startTime = performance.now();
1992
+ const traceIdValue = generateTraceId();
1993
+ return runWithTraceId(traceIdValue, async () => {
1994
+ const inRequestContext = async () => {
1995
+ const runRequest = async () => {
1996
+ logRequestReceived({
1997
+ method,
1998
+ path
1999
+ });
2000
+ activeRequests++;
2001
+ let concurrency = activeRequests;
2002
+ let response;
2003
+ try {
2004
+ response = await withSpan("http.server.request", {
2005
+ "http.request.method": method,
2006
+ "url.path": path
2007
+ }, async () => {
2008
+ const otelTraceId = await getOtelTraceId();
2009
+ if (otelTraceId) replaceTraceId(otelTraceId);
2010
+ const stripTrailingSlash = config.stripTrailingSlash ?? true;
2011
+ const canonResult = canonicalize(url.pathname, stripTrailingSlash);
2012
+ if (!canonResult.ok) {
2013
+ if (isDebug()) console.warn(`[timber] URL canonicalization rejected ${method} ${url.pathname} — responding with ${canonResult.status}\n This usually means the URL contains encoded separators (%2f, %5c),\n null bytes (%00), path traversal (..), or malformed percent-encoding.`);
2014
+ await setSpanAttribute("http.response.status_code", canonResult.status);
2015
+ return new Response(null, { status: canonResult.status });
2016
+ }
2017
+ const canonicalPath = canonResult.pathname;
2018
+ let canonicalReq = req;
2019
+ if (url.pathname !== canonicalPath) {
2020
+ const canonicalUrl = new URL(req.url);
2021
+ canonicalUrl.pathname = canonicalPath;
2022
+ canonicalReq = new Request(canonicalUrl.toString(), req);
2023
+ }
2024
+ const csrfRejection = csrfGate(canonicalReq, canonicalPath, config.csrf ?? {}, config.matchRoute);
2025
+ if (csrfRejection) {
2026
+ await setSpanAttribute("http.response.status_code", csrfRejection.status);
2027
+ return csrfRejection;
2028
+ }
2029
+ const innerHandler = async () => {
2030
+ if (config.dispatchAction) {
2031
+ const actionResult = await config.dispatchAction(canonicalReq, canonicalPath, pipelineFn);
2032
+ if (actionResult) return actionResult;
2033
+ }
2034
+ return handleRequest(config, canonicalReq, method, canonicalPath, true);
2035
+ };
2036
+ let result;
2037
+ if (proxyResolver) result = await outcomeToResponse(config, await runProxyPhase(config, proxyResolver, canonicalReq, method, canonicalPath, innerHandler), {
2038
+ req: canonicalReq,
2039
+ method,
2040
+ path: canonicalPath
2041
+ });
2042
+ else result = await innerHandler();
2043
+ await setSpanAttribute("http.response.status_code", result.status);
2044
+ const varyTokens = [...RSC_KEY_HEADERS];
2045
+ const existingVary = result.headers.get("Vary");
2046
+ const existingTokens = existingVary ? existingVary.toLowerCase().split(",").map((t) => t.trim()) : [];
2047
+ const newTokens = varyTokens.filter((t) => !existingTokens.includes(t.toLowerCase()));
2048
+ if (newTokens.length > 0) result.headers.set("Vary", existingVary ? `${existingVary}, ${newTokens.join(", ")}` : newTokens.join(", "));
2049
+ await enforceRscCacheKey(canonicalReq, result);
2050
+ hardenRscPayloadResponse(result);
2051
+ result = stripHeadBody(result, method);
2052
+ if (serverTiming === "detailed") {
2053
+ const timingHeader = getServerTimingHeader();
2054
+ if (timingHeader) result.headers.set("Server-Timing", timingHeader);
2055
+ } else if (serverTiming === "total") {
2056
+ const totalMs = Math.round(performance.now() - startTime);
2057
+ result.headers.set("Server-Timing", `total;dur=${totalMs}`);
2058
+ }
2059
+ if (serverTiming === "detailed" && result.body) result = new Response(wrapResponseWithStreamTiming(result.body, startTime), {
2060
+ status: result.status,
2061
+ statusText: result.statusText,
2062
+ headers: result.headers
2063
+ });
2064
+ return result;
2065
+ });
2066
+ } finally {
2067
+ concurrency = activeRequests;
2068
+ activeRequests--;
2069
+ }
2070
+ const durationMs = Math.round(performance.now() - startTime);
2071
+ const status = response.status;
2072
+ logRequestCompleted({
2073
+ method,
2074
+ path,
2075
+ status,
2076
+ durationMs,
2077
+ concurrency
2078
+ });
2079
+ if (slowRequestMs > 0 && durationMs > slowRequestMs) logSlowRequest({
2080
+ method,
2081
+ path,
2082
+ durationMs,
2083
+ threshold: slowRequestMs,
2084
+ concurrency
2085
+ });
2086
+ return response;
2087
+ };
2088
+ return serverTiming === "detailed" ? runWithTimingCollector(runRequest) : runRequest();
2089
+ };
2090
+ return runWithRequestContext(req, inRequestContext, { cookies });
2091
+ });
2092
+ };
2093
+ return pipelineFn;
2094
+ }
2095
+ //#endregion
2096
+ //#region src/server/early-hints.ts
2097
+ /**
2098
+ * 103 Early Hints utilities.
2099
+ *
2100
+ * Early Hints are sent before the final response to let the browser
2101
+ * start fetching critical resources (CSS, fonts, JS) while the server
2102
+ * is still rendering.
2103
+ *
2104
+ * The framework collects hints from two sources:
2105
+ * 1. Build manifest — CSS, fonts, and JS chunks known at route-match time
2106
+ * 2. ctx.earlyHints() — explicit hints added by middleware or route handlers
2107
+ *
2108
+ * Both are emitted as Link headers. Cloudflare CDN automatically converts
2109
+ * Link headers into 103 Early Hints responses.
2110
+ *
2111
+ * Design docs: 02-rendering-pipeline.md §"Early Hints (103)"
2112
+ */
2113
+ /**
2114
+ * Format a single EarlyHint as a Link header value.
2115
+ *
2116
+ * Attribute order: `as` before `rel` to match Cloudflare CDN's cached
2117
+ * Early Hints format. Cloudflare caches Link headers from 200 responses
2118
+ * and re-emits them as 103 Early Hints on subsequent requests. If our
2119
+ * attribute order differs from Cloudflare's cached copy, the browser
2120
+ * sees two preload headers for the same URL (different attribute order)
2121
+ * and warns "Preload was ignored." Matching the order ensures the
2122
+ * browser deduplicates them correctly.
2123
+ *
2124
+ * Examples:
2125
+ * `</styles/root.css>; as=style; rel=preload`
2126
+ * `</fonts/inter.woff2>; as=font; rel=preload; crossorigin=anonymous`
2127
+ * `</_timber/client.js>; rel=modulepreload`
2128
+ * `<https://fonts.googleapis.com>; rel=preconnect`
2129
+ */
2130
+ function formatLinkHeader(hint) {
2131
+ if (hint.as !== void 0) {
2132
+ let value = `<${hint.href}>; as=${hint.as}; rel=${hint.rel}`;
2133
+ if (hint.crossOrigin !== void 0) value += `; crossorigin=${hint.crossOrigin}`;
2134
+ if (hint.fetchPriority !== void 0) value += `; fetchpriority=${hint.fetchPriority}`;
2135
+ return value;
2136
+ }
2137
+ let value = `<${hint.href}>; rel=${hint.rel}`;
2138
+ if (hint.crossOrigin !== void 0) value += `; crossorigin=${hint.crossOrigin}`;
2139
+ if (hint.fetchPriority !== void 0) value += `; fetchpriority=${hint.fetchPriority}`;
2140
+ return value;
2141
+ }
2142
+ /**
2143
+ * Collect all Link header strings for a matched route's segment chain.
2144
+ *
2145
+ * Walks the build manifest to emit hints for:
2146
+ * - CSS stylesheets (as=style; rel=preload)
2147
+ * - Font assets (as=font; rel=preload; crossorigin)
2148
+ * - JS modulepreload hints (rel=modulepreload) — unless skipJs is set
2149
+ *
2150
+ * Returns formatted Link header strings, deduplicated by URL, root → leaf order.
2151
+ * Returns an empty array in dev mode (manifest is empty).
2152
+ */
2153
+ function collectEarlyHintHeaders(segments, manifest, options) {
2154
+ const result = [];
2155
+ const seenUrls = /* @__PURE__ */ new Set();
2156
+ const add = (url, header) => {
2157
+ if (!seenUrls.has(url)) {
2158
+ seenUrls.add(url);
2159
+ result.push(header);
2160
+ }
2161
+ };
2162
+ for (const url of collectRouteCss(segments, manifest)) add(url, formatLinkHeader({
2163
+ href: url,
2164
+ rel: "preload",
2165
+ as: "style"
2166
+ }));
2167
+ for (const font of collectRouteFonts(segments, manifest)) add(font.href, formatLinkHeader({
2168
+ href: font.href,
2169
+ rel: "preload",
2170
+ as: "font",
2171
+ crossOrigin: "anonymous"
2172
+ }));
2173
+ if (!options?.skipJs) for (const url of collectRouteModulepreloads(segments, manifest)) add(url, formatLinkHeader({
2174
+ href: url,
2175
+ rel: "modulepreload"
2176
+ }));
2177
+ return result;
2178
+ }
2179
+ //#endregion
2180
+ //#region src/server/early-hints-sender.ts
2181
+ /**
2182
+ * Per-request 103 Early Hints sender — ALS bridge for platform adapters.
2183
+ *
2184
+ * The pipeline collects Link headers for CSS, fonts, and JS chunks at
2185
+ * route-match time. On platforms that support it (Node.js v18.11+, Bun),
2186
+ * the adapter can send these as a 103 Early Hints interim response before
2187
+ * the final response is ready.
2188
+ *
2189
+ * This module provides an ALS-based bridge: the generated entry point
2190
+ * (e.g., the Nitro entry) wraps the handler with `runWithEarlyHintsSender`,
2191
+ * binding a per-request sender function. The pipeline calls
2192
+ * `sendEarlyHints103()` to fire the 103 if a sender is available.
2193
+ *
2194
+ * On platforms where 103 is handled at the CDN level (e.g., Cloudflare
2195
+ * converts Link headers into 103 automatically), no sender is installed
2196
+ * and `sendEarlyHints103()` is a no-op.
2197
+ *
2198
+ * Design doc: 02-rendering-pipeline.md §"Early Hints (103)"
2199
+ */
2200
+ /**
2201
+ * Run a function with a per-request early hints sender installed.
2202
+ *
2203
+ * Called by generated entry points (e.g., Nitro node-server/bun) to
2204
+ * bind the platform's writeEarlyHints capability for the request duration.
2205
+ */
2206
+ function runWithEarlyHintsSender(sender, fn) {
2207
+ return earlyHintsSenderAls.run(sender, fn);
2208
+ }
2209
+ /**
2210
+ * Send collected Link headers as a 103 Early Hints response.
2211
+ *
2212
+ * No-op if no sender is installed for the current request (e.g., on
2213
+ * Cloudflare where the CDN handles 103 automatically, or in dev mode).
2214
+ *
2215
+ * Non-fatal: errors from the sender are caught and silently ignored.
2216
+ */
2217
+ function sendEarlyHints103(links) {
2218
+ if (!links.length) return;
2219
+ const sender = earlyHintsSenderAls.getStore();
2220
+ if (!sender) return;
2221
+ try {
2222
+ sender(links);
2223
+ } catch (err) {
2224
+ swallow(err, "early hints 103 send failed");
2225
+ }
2226
+ }
2227
+ //#endregion
2228
+ //#region src/client/error-reconstituter.tsx
2229
+ /**
2230
+ * Reconstitutes a SerializableError into a real Error instance before
2231
+ * passing to the user's error component.
2232
+ *
2233
+ * TSX error pages are 'use client' components that receive { error: Error, digest, reset }.
2234
+ * Error objects are not RSC-serializable (React Flight throws "Only plain objects
2235
+ * can be passed to Client Components"). This wrapper receives the error as a plain
2236
+ * SerializableError object, reconstitutes a real Error instance, and passes it
2237
+ * to the user's error component — ensuring error instanceof Error works correctly.
2238
+ *
2239
+ * See design/spike-TIM-565-unify-error-pages.md §"Edge Case B"
2240
+ * See design/10-error-handling.md §"RSC → SSR for Error Pages via SerializableError"
2241
+ */
2242
+ /**
2243
+ * Reconstitute a SerializableError into a real Error instance and render
2244
+ * the user's error component with the proper props.
2245
+ */
2246
+ function ErrorReconstituter({ error: serialized, digest, reset, component, status, data }) {
2247
+ const error = Object.assign(new Error(serialized.message), {
2248
+ name: serialized.name,
2249
+ stack: serialized.stack ?? `${serialized.name}: ${serialized.message}`
2250
+ });
2251
+ return createElement(component, {
2252
+ error,
2253
+ digest,
2254
+ reset,
2255
+ ...status != null ? {
2256
+ status,
2257
+ data
2258
+ } : {}
2259
+ });
2260
+ }
2261
+ //#endregion
2262
+ //#region src/server/deny-boundary.ts
2263
+ /**
2264
+ * Deny boundary subsystem — the in-tree DenySignal flow.
2265
+ *
2266
+ * Three things live together here because they form a single flow:
2267
+ *
2268
+ * 1. **Chain construction** (`buildDenyPageChain`) — walks the matched
2269
+ * segment chain at element-tree build time and produces a list of
2270
+ * `DenyPageEntry` records ordered by specificity (specific status →
2271
+ * category catch-all → `error.tsx`).
2272
+ *
2273
+ * 2. **Runtime matching and placement** (`matchDenyEntry`, `placeDenyPage`,
2274
+ * `handleCaughtDeny`) — picks the first chain entry whose status filter
2275
+ * matches the thrown DenySignal, then decides whether the catch site may
2276
+ * render it where it stands. A chain spans the catcher outward to root,
2277
+ * so a match may belong to an ancestor segment; rendering it in place
2278
+ * would wrap it in every layout in between. Ancestor-owned matches are
2279
+ * re-thrown with `ownerKey` stamped on the signal so the error boundary
2280
+ * at the owning segment renders it. See TIM-1356.
2281
+ *
2282
+ * 3. **The page boundary itself** (`PageDenyBoundary`) — the async server
2283
+ * component that wraps a server-component page, calls it, and catches
2284
+ * `DenySignal` so the deny page renders in-tree (no throw reaches
2285
+ * React Flight, single render pass).
2286
+ *
2287
+ * Plus the ALS helpers (`setDenyStatus` / `getDenyStatus`) the boundary
2288
+ * uses to thread the matched status code back to the pipeline so the
2289
+ * HTTP status reflects the deny.
2290
+ *
2291
+ * Folded into one module from the former `deny-page-resolver.ts` and
2292
+ * `page-deny-boundary.tsx` (TIM-853) — the names were misleading and the
2293
+ * three pieces only made sense together.
2294
+ *
2295
+ * See design/04-authorization.md, design/10-error-handling.md, TIM-666.
2296
+ */
2297
+ /**
2298
+ * Find the first deny page in the chain whose status filter matches.
2299
+ * Returns null when nothing in the chain accepts this status.
2300
+ *
2301
+ * For 4xx statuses, the chain's stored order is correct: every status file
2302
+ * across all segments (inner→outer) before any error.tsx — so a root
2303
+ * `404.tsx` beats a leaf `error.tsx`. This matches `resolve4xx`.
2304
+ *
2305
+ * For 5xx statuses, the match walks per-segment (inner→outer): at each
2306
+ * segment, status files (`503.tsx` → `5xx.tsx`) then `error.tsx`, before
2307
+ * moving to the next segment outward. A leaf `error.tsx` therefore beats a
2308
+ * root `5xx.tsx` — the intentional inverse of the 4xx chain, matching
2309
+ * `resolve5xx`. See TIM-1360.
2310
+ */
2311
+ function matchDenyEntry(chain, status) {
2312
+ return matchDenyEntries(chain, status)[0] ?? null;
2313
+ }
2314
+ /**
2315
+ * Return ALL deny page entries whose status filter matches, in traversal
2316
+ * order (best match first). `matchDenyEntry` returns only the first;
2317
+ * late addressing (TIM-1450) needs the full list so the client can find
2318
+ * the first *reachable* owner via its ancestry context.
2319
+ */
2320
+ function matchDenyEntries(chain, status) {
2321
+ if (status >= 500) return matchDenyEntries5xx(chain, status);
2322
+ const matches = [];
2323
+ for (const entry of chain) if (entry.status === status) matches.push(entry);
2324
+ else if (entry.status === 400 && status >= 400 && status <= 499) matches.push(entry);
2325
+ else if (entry.status === null) matches.push(entry);
2326
+ return matches;
2327
+ }
2328
+ /**
2329
+ * 5xx per-segment walk: at each segment (inner→outer), check status files
2330
+ * then error.tsx before moving outward. Mirrors `resolve5xx`.
2331
+ */
2332
+ function matchDenyEntries5xx(chain, status) {
2333
+ const segmentIndices = [];
2334
+ for (const entry of chain) if (!segmentIndices.includes(entry.segmentIndex)) segmentIndices.push(entry.segmentIndex);
2335
+ segmentIndices.sort((a, b) => b - a);
2336
+ const matches = [];
2337
+ for (const seg of segmentIndices) {
2338
+ for (const entry of chain) {
2339
+ if (entry.segmentIndex !== seg || entry.kind === "error") continue;
2340
+ if (entry.status === status) matches.push(entry);
2341
+ else if (entry.status === 500 && status >= 500 && status <= 599) matches.push(entry);
2342
+ }
2343
+ for (const entry of chain) {
2344
+ if (entry.segmentIndex !== seg || entry.kind !== "error") continue;
2345
+ matches.push(entry);
2346
+ }
2347
+ }
2348
+ return matches;
2349
+ }
2350
+ /**
2351
+ * Decide where the deny page for `status` renders, from the perspective of
2352
+ * the catch site at `selfKey`.
2353
+ *
2354
+ * The match itself is unchanged — first entry in fallback order wins, so
2355
+ * every path picks the same file. What this adds is the position: a chain
2356
+ * built at segment i spans i outward to root, so a matched entry is owned
2357
+ * either by the catcher or by one of its ancestors.
2358
+ *
2359
+ * - Owned by the catcher → `render`. The catcher already sits at the
2360
+ * owning segment's position.
2361
+ * - Owned by an ancestor → `hoist`. Rendering here would wrap the deny page
2362
+ * in every layout between the owner and the catcher. The caller re-throws
2363
+ * with `ownerKey` stamped on the signal so the boundary at that segment
2364
+ * renders it instead — inside the owner's layout, outside the rest.
2365
+ *
2366
+ * Returns null when no entry matches; the caller re-throws unannotated and
2367
+ * the re-render fallback resolves it.
2368
+ *
2369
+ * See design/04-authorization.md §"Where a Deny Page Renders", TIM-1356.
2370
+ */
2371
+ function placeDenyPage(chain, status, data, selfKey) {
2372
+ const entry = matchDenyEntry(chain, status);
2373
+ if (!entry) return null;
2374
+ if (entry.ownerKey !== selfKey) return {
2375
+ kind: "hoist",
2376
+ ownerKey: entry.ownerKey
2377
+ };
2378
+ return {
2379
+ kind: "render",
2380
+ element: renderDenyEntry(entry, status, data)
2381
+ };
2382
+ }
2383
+ /**
2384
+ * Build the element for a matched deny chain entry with the props contract
2385
+ * the file's convention documents:
2386
+ *
2387
+ * - Status-code files: { status, data }
2388
+ * - error.tsx (TSX): { error, digest, reset } via ErrorReconstituter, plus
2389
+ * { status, data } for dual-shape implementations. Without
2390
+ * the Error prop, any error.tsx written per the docs (reading
2391
+ * error.message) crashes — turning a clean deny(404) into a 500. TIM-1081.
2392
+ * - error.mdx: plain { status } — server component, static content.
2393
+ */
2394
+ function renderDenyEntry(entry, status, data) {
2395
+ const h = createElement;
2396
+ return h(StatusPageMarker, null, renderDenyPage(entry, status, data, h));
2397
+ }
2398
+ function renderDenyPage(entry, status, data, h) {
2399
+ if (entry.kind !== "error") return h(entry.component, {
2400
+ status,
2401
+ data
2402
+ });
2403
+ return h(ErrorReconstituter, {
2404
+ error: {
2405
+ message: `Access denied with status ${status}`,
2406
+ name: "DenySignal"
2407
+ },
2408
+ digest: null,
2409
+ reset: void 0,
2410
+ component: entry.component,
2411
+ status,
2412
+ data
2413
+ });
2414
+ }
2415
+ /**
2416
+ * The whole of what an in-tree catch site does with a `DenySignal`, shared
2417
+ * by all three of them (`PageDenyBoundary`, `TracedLayout`, `AccessGate`).
2418
+ *
2419
+ * Returns the deny page element when this catch site owns the matched file.
2420
+ * Otherwise it throws — annotated with `ownerKey` when an ancestor owns the
2421
+ * file, bare when nothing matched at all.
2422
+ *
2423
+ * `setDenyStatus` runs on the hoist path too, *before* the throw, for one
2424
+ * case. Normally Flight's `onError` captures the rethrown signal within the
2425
+ * renderer's promotion cap, and the pipeline renders the deny page with
2426
+ * `deny.status` — the recorded status goes unread. When the signal lands
2427
+ * after the cap but within the detection window that follows, the renderer
2428
+ * discards it (never promoted) and streams the document it already has;
2429
+ * `getDenyStatus()`, read once as the renderer returns, gives that document
2430
+ * the deny's 4xx instead of a 200. A deny later than that commits 200. Pinned by
2431
+ * `tests/late-hoisted-deny-status.test.ts`. The RSC payload path never
2432
+ * reads it — a Flight payload response is always 200 and the row itself
2433
+ * carries the deny (TIM-1495).
2434
+ *
2435
+ * Either branch leaves part of the matched chain unmounted — hoisting sends
2436
+ * the render to a boundary that discards everything below it, and a layout
2437
+ * that denies returns the deny page instead of its own child outlet. What the
2438
+ * client then learns about the tree is decided by the boundary that renders
2439
+ * the page, at the commit that puts it on screen (`navigation-commit.ts`).
2440
+ */
2441
+ function handleCaughtDeny(chain, error, selfKey) {
2442
+ const placement = chain ? placeDenyPage(chain, error.status, error.data, selfKey) : null;
2443
+ if (placement?.kind === "render") {
2444
+ setDenyStatus(error.status);
2445
+ return placement.element;
2446
+ }
2447
+ if (placement?.kind === "hoist") {
2448
+ setDenyStatus(error.status);
2449
+ error.owners = [placement.ownerKey];
2450
+ }
2451
+ throw error;
2452
+ }
2453
+ /**
2454
+ * Set the deny status in the request context ALS.
2455
+ * Called from AccessGate / PageDenyBoundary when a DenySignal is caught.
2456
+ * The document path reads this after render to set the HTTP status code.
2457
+ */
2458
+ function setDenyStatus(status) {
2459
+ const store = requestContextAls.getStore();
2460
+ if (store) store.denyStatus = status;
2461
+ }
2462
+ /**
2463
+ * Record the first denied segment index in the request context ALS.
2464
+ * Called from AccessGate when a DenySignal is caught during render.
2465
+ * Only the first (shallowest) denial is recorded — deeper denials are
2466
+ * unreachable because the gate short-circuits the subtree.
2467
+ * See TIM-1363.
2468
+ */
2469
+ function setFirstDeniedSegmentIndex(index) {
2470
+ const store = requestContextAls.getStore();
2471
+ if (store && store._firstDeniedSegmentIndex === void 0) store._firstDeniedSegmentIndex = index;
2472
+ }
2473
+ //#endregion
2474
+ //#region src/server/access-gate.tsx
2475
+ /**
2476
+ * Framework-injected access gate for segments.
2477
+ *
2478
+ * Calls accessFn during render — inside `renderToReadableStream`, sharing
2479
+ * the same `React.cache` scope as layouts and pages. A `requireUser()`
2480
+ * call in access.ts populates the cache; the same call in any layout is
2481
+ * a free hit. Access executes exactly once per request. See TIM-1363.
2482
+ *
2483
+ * access.ts is a pure gate — return values are discarded. The layout below
2484
+ * gets the same data by calling the same cached functions (React.cache dedup).
2485
+ *
2486
+ * On denial the gate renders the matching deny page only when this segment
2487
+ * owns it. Because the gate is wrapped OUTSIDE its own layout, that render
2488
+ * omits the denying segment's layout — deliberate, and the one place the
2489
+ * in-tree path diverges from `renderDenyPage`'s inclusive slice: a layout
2490
+ * whose access.ts denied must never run and serialize protected data into
2491
+ * the Flight stream (TIM-1074). An ancestor-owned page is hoisted instead,
2492
+ * because the gate also sits inside every layout above it and rendering
2493
+ * here would wrap the deny page in all of them (TIM-1356).
2494
+ *
2495
+ * For skipped segments (skippedSegment=true), deny pages are NOT rendered
2496
+ * in-tree — the client's cached layout chrome would wrap them incorrectly.
2497
+ * Instead, the deny is recorded in ALS and rethrown so the response path
2498
+ * can return rscErrorEnvelope for a client hard-navigate.
2499
+ */
2500
+ function AccessGate(props) {
2501
+ const { accessFn, segmentName, segmentKey, segmentIndex, denyPages, skippedSegment, children } = props;
2502
+ return accessGateFallback(accessFn, segmentName, segmentKey, segmentIndex, skippedSegment ? void 0 : denyPages, skippedSegment, children);
2503
+ }
2504
+ /**
2505
+ * Calls accessFn during render with OTEL instrumentation.
2506
+ * Records the first denied segment index in ALS for post-render consumers.
2507
+ */
2508
+ async function accessGateFallback(accessFn, segmentName, segmentKey, segmentIndex, denyPages, skippedSegment, children) {
2509
+ try {
2510
+ await withSpan("timber.access", { "timber.segment": segmentName ?? "unknown" }, async () => {
2511
+ try {
2512
+ await accessFn();
2513
+ await setSpanAttribute("timber.result", "pass");
2514
+ } catch (error) {
2515
+ if (isDenySignal(error)) {
2516
+ await setSpanAttribute("timber.result", "deny");
2517
+ await setSpanAttribute("timber.deny_status", error.status);
2518
+ if (error.sourceFile) await setSpanAttribute("timber.deny_file", error.sourceFile);
2519
+ } else if (isRedirectSignal(error)) await setSpanAttribute("timber.result", "redirect");
2520
+ throw error;
2521
+ }
2522
+ });
2523
+ } catch (error) {
2524
+ if (isDenySignal(error)) {
2525
+ if (segmentIndex !== void 0) setFirstDeniedSegmentIndex(segmentIndex);
2526
+ if (skippedSegment) {
2527
+ error._hardNavigate = true;
2528
+ throw error;
2529
+ }
2530
+ if (denyPages) return handleCaughtDeny(denyPages, error, segmentKey);
2531
+ }
2532
+ throw error;
2533
+ }
2534
+ return children;
2535
+ }
2536
+ /**
2537
+ * Framework-injected access gate for parallel slots.
2538
+ *
2539
+ * On denial, graceful degradation: denied.tsx → default.tsx → null.
2540
+ * The HTTP status code is unaffected — slot denial is a UI concern, not
2541
+ * a protocol concern. The parent layout and sibling slots still render.
2542
+ *
2543
+ * DeniedComponent is passed instead of a pre-built element so that
2544
+ * DenySignal.data can be forwarded as the data prop
2545
+ * and the slot name can be passed as the slot prop. See TIM-488.
2546
+ *
2547
+ * redirect() in slot access.ts is a dev-mode error — redirecting from a
2548
+ * slot doesn't make architectural sense.
2549
+ */
2550
+ async function SlotAccessGate(props) {
2551
+ const { accessFn, DeniedComponent, slotName, createElement, defaultFallback, children } = props;
2552
+ try {
2553
+ await accessFn();
2554
+ } catch (error) {
2555
+ if (isDenySignal(error)) return buildDeniedFallback(DeniedComponent, slotName, error.data, createElement) ?? defaultFallback ?? null;
2556
+ if (isRedirectSignal(error)) {
2557
+ if (isDebug()) console.error("[timber] redirect() is not allowed in slot access.ts. Slots use deny() for graceful degradation — denied.tsx → default.tsx → null. If you need to redirect, move the logic to the parent segment's access.ts.");
2558
+ return buildDeniedFallback(DeniedComponent, slotName, void 0, createElement) ?? defaultFallback ?? null;
2559
+ }
2560
+ if (isDebug()) console.warn("[timber] Unhandled error in slot access.ts. Use deny() for access control, not unhandled throws.", error);
2561
+ throw error;
2562
+ }
2563
+ return children;
2564
+ }
2565
+ /**
2566
+ * Build the denied fallback element dynamically with DenySignal data.
2567
+ * Returns null if no DeniedComponent is available.
2568
+ */
2569
+ function buildDeniedFallback(DeniedComponent, slotName, data, createElement) {
2570
+ if (!DeniedComponent) return null;
2571
+ return createElement(DeniedComponent, {
2572
+ slot: slotName,
2573
+ data
2574
+ });
2575
+ }
2576
+ //#endregion
2577
+ //#region src/server/status-code-resolver.ts
2578
+ /**
2579
+ * Look up `{statusStr}` then `{categoryKey}` (e.g. "4xx" / "5xx") in a
2580
+ * status-file group on a single segment. Shared by all three fallback
2581
+ * chains — the only structural difference between component 4xx,
2582
+ * component 5xx, and JSON resolution is *which* group is searched and
2583
+ * how the per-segment loop is layered around it.
2584
+ */
2585
+ function lookupInGroup(group, statusStr, categoryKey, segmentIndex, status) {
2586
+ if (!group) return null;
2587
+ const exact = group[statusStr];
2588
+ if (exact) return {
2589
+ file: exact,
2590
+ status,
2591
+ kind: "exact",
2592
+ segmentIndex
2593
+ };
2594
+ const category = group[categoryKey];
2595
+ if (category) return {
2596
+ file: category,
2597
+ status,
2598
+ kind: "category",
2599
+ segmentIndex
2600
+ };
2601
+ return null;
2602
+ }
2603
+ /**
2604
+ * Resolve the status-code file to render for a given HTTP status code.
2605
+ *
2606
+ * Walks the segment chain from leaf to root following the fallback chain
2607
+ * defined in design/10-error-handling.md. Returns null if no file is found
2608
+ * (caller should render the framework default).
2609
+ *
2610
+ * @param status - The HTTP status code (4xx or 5xx).
2611
+ * @param segments - The matched segment chain from root (index 0) to leaf (last).
2612
+ * @param format - The response format family ('component' or 'json'). Defaults to 'component'.
2613
+ * @param maxIndex - Optional upper bound on the segment index to search.
2614
+ * When set, the resolver only considers segments `0..maxIndex` (inclusive),
2615
+ * skipping any segments deeper in the tree. Used by the deny renderers to
2616
+ * prevent resolving a status file below a denied segment — matching
2617
+ * `buildDenyPageChain`'s start-at-i semantics. The error renderers omit
2618
+ * this parameter, so the full chain is searched — a 500 from a leaf page
2619
+ * should use the leaf's own `5xx.tsx`. See TIM-1358.
2620
+ */
2621
+ function resolveStatusFile(status, segments, format = "component", maxIndex) {
2622
+ if (status < 400 || status > 599) return null;
2623
+ if (format === "json") return resolveJson(status, segments, maxIndex);
2624
+ if (status <= 499) return resolve4xx(status, segments, maxIndex);
2625
+ return resolve5xx(status, segments, maxIndex);
2626
+ }
2627
+ /**
2628
+ * 4xx component fallback chain — two separate full passes leaf→root.
2629
+ *
2630
+ * The passes must be separate (not interleaved per-segment) so that a
2631
+ * root-level `404.tsx` beats a leaf-level `error.tsx`. The 5xx chain
2632
+ * inverts this and is per-segment: a leaf's `error.tsx` beats a root's
2633
+ * `5xx.tsx`. This asymmetry is the only reason these two functions exist
2634
+ * separately.
2635
+ *
2636
+ * Pass 1 — {status}.tsx → 4xx.tsx (statusFiles)
2637
+ * Pass 2 — error.tsx (error)
2638
+ */
2639
+ function resolve4xx(status, segments, maxIndex) {
2640
+ const statusStr = String(status);
2641
+ const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
2642
+ for (let i = start; i >= 0; i--) {
2643
+ const r = lookupInGroup(segments[i].statusFiles, statusStr, "4xx", i, status);
2644
+ if (r) return r;
2645
+ }
2646
+ for (let i = start; i >= 0; i--) {
2647
+ const errorFile = segments[i].error;
2648
+ if (errorFile) return {
2649
+ file: errorFile,
2650
+ status,
2651
+ kind: "error",
2652
+ segmentIndex: i
2653
+ };
2654
+ }
2655
+ return null;
2656
+ }
2657
+ /**
2658
+ * 5xx component fallback chain — single pass, per-segment leaf→root.
2659
+ *
2660
+ * At each segment: {status}.tsx → 5xx.tsx → error.tsx. A leaf's
2661
+ * `error.tsx` therefore beats a root's `5xx.tsx`, which is the
2662
+ * intentional inverse of the 4xx chain.
2663
+ */
2664
+ function resolve5xx(status, segments, maxIndex) {
2665
+ const statusStr = String(status);
2666
+ const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
2667
+ for (let i = start; i >= 0; i--) {
2668
+ const segment = segments[i];
2669
+ const r = lookupInGroup(segment.statusFiles, statusStr, "5xx", i, status);
2670
+ if (r) return r;
2671
+ if (segment.error) return {
2672
+ file: segment.error,
2673
+ status,
2674
+ kind: "error",
2675
+ segmentIndex: i
2676
+ };
2677
+ }
2678
+ return null;
2679
+ }
2680
+ /**
2681
+ * JSON fallback chain (for both 4xx and 5xx) — single pass leaf→root.
2682
+ *
2683
+ * At each segment: {status}.json → {category}.json. No legacy compat,
2684
+ * no error.tsx — the JSON chain terminates at the category catch-all
2685
+ * and the caller falls back to a bare-JSON framework default.
2686
+ */
2687
+ function resolveJson(status, segments, maxIndex) {
2688
+ const statusStr = String(status);
2689
+ const categoryKey = status >= 500 ? "5xx" : "4xx";
2690
+ const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
2691
+ for (let i = start; i >= 0; i--) {
2692
+ const r = lookupInGroup(segments[i].jsonStatusFiles, statusStr, categoryKey, i, status);
2693
+ if (r) return r;
2694
+ }
2695
+ return null;
2696
+ }
2697
+ /**
2698
+ * Resolve the denial file for a parallel route slot.
2699
+ *
2700
+ * Slot denial is graceful degradation — no HTTP status on the wire.
2701
+ * Fallback chain: denied.tsx → default.tsx → null.
2702
+ *
2703
+ * @param slotNode - The segment node for the slot (segmentType === 'slot').
2704
+ */
2705
+ function resolveSlotDenied(slotNode) {
2706
+ const slotName = slotNode.segmentName.replace(/^@/, "");
2707
+ if (slotNode.denied) return {
2708
+ file: slotNode.denied,
2709
+ slotName,
2710
+ kind: "denied"
2711
+ };
2712
+ if (slotNode.default) return {
2713
+ file: slotNode.default,
2714
+ slotName,
2715
+ kind: "default"
2716
+ };
2717
+ return null;
2718
+ }
2719
+ //#endregion
2720
+ //#region src/server/body-limits.ts
2721
+ var KB = 1024;
2722
+ var MB = 1024 * KB;
2723
+ var GB = 1024 * MB;
2724
+ var DEFAULT_LIMITS = {
2725
+ actionBodySize: 1 * MB,
2726
+ uploadBodySize: 10 * MB,
2727
+ maxFields: 100
2728
+ };
2729
+ var SIZE_PATTERN = /^(\d+(?:\.\d+)?)\s*(kb|mb|gb)?$/i;
2730
+ /** Parse a human-readable size string ("1mb", "512kb", "1024") into bytes. */
2731
+ function parseBodySize(size) {
2732
+ const match = SIZE_PATTERN.exec(size.trim());
2733
+ if (!match) throw new Error(`Invalid body size format: "${size}". Expected format like "1mb", "512kb", or "1024".`);
2734
+ const value = Number.parseFloat(match[1]);
2735
+ const unit = (match[2] ?? "").toLowerCase();
2736
+ switch (unit) {
2737
+ case "kb": return Math.floor(value * KB);
2738
+ case "mb": return Math.floor(value * MB);
2739
+ case "gb": return Math.floor(value * GB);
2740
+ case "": return Math.floor(value);
2741
+ default: throw new Error(`Unknown size unit: "${unit}"`);
2742
+ }
2743
+ }
2744
+ /** Strict integer pattern: one or more digits, nothing else. */
2745
+ var STRICT_INTEGER_RE = /^\d+$/;
2746
+ /** Check whether a request body exceeds the configured size limit (stateless, no ALS). */
2747
+ function enforceBodyLimits(req, kind, config) {
2748
+ const contentLength = req.headers.get("Content-Length");
2749
+ if (!contentLength) return {
2750
+ ok: false,
2751
+ status: 411
2752
+ };
2753
+ const trimmed = contentLength.trim();
2754
+ if (!STRICT_INTEGER_RE.test(trimmed)) return {
2755
+ ok: false,
2756
+ status: 411
2757
+ };
2758
+ return Number.parseInt(trimmed, 10) <= resolveLimit(kind, config) ? { ok: true } : {
2759
+ ok: false,
2760
+ status: 413
2761
+ };
2762
+ }
2763
+ /**
2764
+ * Resolve the byte limit for a given body kind, using config overrides or defaults.
2765
+ */
2766
+ function resolveLimit(kind, config) {
2767
+ const userLimits = config.limits;
2768
+ if (kind === "action") return userLimits?.actionBodySize ? parseBodySize(userLimits.actionBodySize) : DEFAULT_LIMITS.actionBodySize;
2769
+ return userLimits?.uploadBodySize ? parseBodySize(userLimits.uploadBodySize) : DEFAULT_LIMITS.uploadBodySize;
2770
+ }
2771
+ //#endregion
2772
+ //#region src/server/metadata-social.ts
2773
+ /**
2774
+ * Render Open Graph metadata into head element descriptors.
2775
+ *
2776
+ * Handles og:title, og:description, og:image (with dimensions/alt),
2777
+ * og:video, og:audio, og:article:author, and other OG properties.
2778
+ */
2779
+ function renderOpenGraph(og, elements) {
2780
+ const simpleProps = [
2781
+ ["og:title", og.title],
2782
+ ["og:description", og.description],
2783
+ ["og:url", og.url],
2784
+ ["og:site_name", og.siteName],
2785
+ ["og:locale", og.locale],
2786
+ ["og:type", og.type],
2787
+ ["og:article:published_time", og.publishedTime],
2788
+ ["og:article:modified_time", og.modifiedTime]
2789
+ ];
2790
+ for (const [property, content] of simpleProps) if (content) elements.push({
2791
+ tag: "meta",
2792
+ attrs: {
2793
+ property,
2794
+ content
2795
+ }
2796
+ });
2797
+ if (og.images) {
2798
+ if (typeof og.images === "string") elements.push({
2799
+ tag: "meta",
2800
+ attrs: {
2801
+ property: "og:image",
2802
+ content: og.images
2803
+ }
2804
+ });
2805
+ else {
2806
+ const imgList = Array.isArray(og.images) ? og.images : [og.images];
2807
+ for (const img of imgList) {
2808
+ elements.push({
2809
+ tag: "meta",
2810
+ attrs: {
2811
+ property: "og:image",
2812
+ content: img.url
2813
+ }
2814
+ });
2815
+ if (img.width) elements.push({
2816
+ tag: "meta",
2817
+ attrs: {
2818
+ property: "og:image:width",
2819
+ content: String(img.width)
2820
+ }
2821
+ });
2822
+ if (img.height) elements.push({
2823
+ tag: "meta",
2824
+ attrs: {
2825
+ property: "og:image:height",
2826
+ content: String(img.height)
2827
+ }
2828
+ });
2829
+ if (img.alt) elements.push({
2830
+ tag: "meta",
2831
+ attrs: {
2832
+ property: "og:image:alt",
2833
+ content: img.alt
2834
+ }
2835
+ });
2836
+ }
2837
+ }
2838
+ }
2839
+ if (og.videos) for (const video of og.videos) elements.push({
2840
+ tag: "meta",
2841
+ attrs: {
2842
+ property: "og:video",
2843
+ content: video.url
2844
+ }
2845
+ });
2846
+ if (og.audio) for (const audio of og.audio) elements.push({
2847
+ tag: "meta",
2848
+ attrs: {
2849
+ property: "og:audio",
2850
+ content: audio.url
2851
+ }
2852
+ });
2853
+ if (og.authors) for (const author of og.authors) elements.push({
2854
+ tag: "meta",
2855
+ attrs: {
2856
+ property: "og:article:author",
2857
+ content: author
2858
+ }
2859
+ });
2860
+ }
2861
+ /**
2862
+ * Render Twitter Card metadata into head element descriptors.
2863
+ *
2864
+ * Handles twitter:card, twitter:site, twitter:title, twitter:image,
2865
+ * twitter:player, and twitter:app (per-platform name/id/url).
2866
+ */
2867
+ function renderTwitter(tw, elements) {
2868
+ const simpleProps = [
2869
+ ["twitter:card", tw.card],
2870
+ ["twitter:site", tw.site],
2871
+ ["twitter:site:id", tw.siteId],
2872
+ ["twitter:title", tw.title],
2873
+ ["twitter:description", tw.description],
2874
+ ["twitter:creator", tw.creator],
2875
+ ["twitter:creator:id", tw.creatorId]
2876
+ ];
2877
+ for (const [name, content] of simpleProps) if (content) elements.push({
2878
+ tag: "meta",
2879
+ attrs: {
2880
+ name,
2881
+ content
2882
+ }
2883
+ });
2884
+ if (tw.images) {
2885
+ if (typeof tw.images === "string") elements.push({
2886
+ tag: "meta",
2887
+ attrs: {
2888
+ name: "twitter:image",
2889
+ content: tw.images
2890
+ }
2891
+ });
2892
+ else {
2893
+ const imgList = Array.isArray(tw.images) ? tw.images : [tw.images];
2894
+ for (const img of imgList) {
2895
+ const url = typeof img === "string" ? img : img.url;
2896
+ elements.push({
2897
+ tag: "meta",
2898
+ attrs: {
2899
+ name: "twitter:image",
2900
+ content: url
2901
+ }
2902
+ });
2903
+ }
2904
+ }
2905
+ }
2906
+ if (tw.players) for (const player of tw.players) {
2907
+ elements.push({
2908
+ tag: "meta",
2909
+ attrs: {
2910
+ name: "twitter:player",
2911
+ content: player.playerUrl
2912
+ }
2913
+ });
2914
+ if (player.width) elements.push({
2915
+ tag: "meta",
2916
+ attrs: {
2917
+ name: "twitter:player:width",
2918
+ content: String(player.width)
2919
+ }
2920
+ });
2921
+ if (player.height) elements.push({
2922
+ tag: "meta",
2923
+ attrs: {
2924
+ name: "twitter:player:height",
2925
+ content: String(player.height)
2926
+ }
2927
+ });
2928
+ if (player.streamUrl) elements.push({
2929
+ tag: "meta",
2930
+ attrs: {
2931
+ name: "twitter:player:stream",
2932
+ content: player.streamUrl
2933
+ }
2934
+ });
2935
+ }
2936
+ if (tw.app) {
2937
+ const platforms = [
2938
+ ["iPhone", "iphone"],
2939
+ ["iPad", "ipad"],
2940
+ ["googlePlay", "googleplay"]
2941
+ ];
2942
+ if (tw.app.name) {
2943
+ for (const [key, tag] of platforms) if (tw.app.id?.[key]) elements.push({
2944
+ tag: "meta",
2945
+ attrs: {
2946
+ name: `twitter:app:name:${tag}`,
2947
+ content: tw.app.name
2948
+ }
2949
+ });
2950
+ }
2951
+ for (const [key, tag] of platforms) {
2952
+ const id = tw.app.id?.[key];
2953
+ if (id) elements.push({
2954
+ tag: "meta",
2955
+ attrs: {
2956
+ name: `twitter:app:id:${tag}`,
2957
+ content: id
2958
+ }
2959
+ });
2960
+ }
2961
+ for (const [key, tag] of platforms) {
2962
+ const url = tw.app.url?.[key];
2963
+ if (url) elements.push({
2964
+ tag: "meta",
2965
+ attrs: {
2966
+ name: `twitter:app:url:${tag}`,
2967
+ content: url
2968
+ }
2969
+ });
2970
+ }
2971
+ }
2972
+ }
2973
+ //#endregion
2974
+ //#region src/server/metadata-platform.ts
2975
+ /**
2976
+ * Render icon link elements (favicon, shortcut, apple-touch-icon, custom).
2977
+ */
2978
+ function renderIcons(icons, elements) {
2979
+ if (icons.icon) {
2980
+ if (typeof icons.icon === "string") elements.push({
2981
+ tag: "link",
2982
+ attrs: {
2983
+ rel: "icon",
2984
+ href: icons.icon
2985
+ }
2986
+ });
2987
+ else if (Array.isArray(icons.icon)) for (const icon of icons.icon) {
2988
+ const attrs = {
2989
+ rel: "icon",
2990
+ href: icon.url
2991
+ };
2992
+ if (icon.sizes) attrs.sizes = icon.sizes;
2993
+ if (icon.type) attrs.type = icon.type;
2994
+ elements.push({
2995
+ tag: "link",
2996
+ attrs
2997
+ });
2998
+ }
2999
+ }
3000
+ if (icons.shortcut) {
3001
+ const urls = Array.isArray(icons.shortcut) ? icons.shortcut : [icons.shortcut];
3002
+ for (const url of urls) elements.push({
3003
+ tag: "link",
3004
+ attrs: {
3005
+ rel: "shortcut icon",
3006
+ href: url
3007
+ }
3008
+ });
3009
+ }
3010
+ if (icons.apple) {
3011
+ if (typeof icons.apple === "string") elements.push({
3012
+ tag: "link",
3013
+ attrs: {
3014
+ rel: "apple-touch-icon",
3015
+ href: icons.apple
3016
+ }
3017
+ });
3018
+ else if (Array.isArray(icons.apple)) for (const icon of icons.apple) {
3019
+ const attrs = {
3020
+ rel: "apple-touch-icon",
3021
+ href: icon.url
3022
+ };
3023
+ if (icon.sizes) attrs.sizes = icon.sizes;
3024
+ elements.push({
3025
+ tag: "link",
3026
+ attrs
3027
+ });
3028
+ }
3029
+ }
3030
+ if (icons.other) for (const icon of icons.other) {
3031
+ const attrs = {
3032
+ rel: icon.rel,
3033
+ href: icon.url
3034
+ };
3035
+ if (icon.sizes) attrs.sizes = icon.sizes;
3036
+ if (icon.type) attrs.type = icon.type;
3037
+ elements.push({
3038
+ tag: "link",
3039
+ attrs
3040
+ });
3041
+ }
3042
+ }
3043
+ /**
3044
+ * Render alternate link elements (canonical, hreflang, media, types).
3045
+ */
3046
+ function renderAlternates(alternates, elements) {
3047
+ if (alternates.canonical) elements.push({
3048
+ tag: "link",
3049
+ attrs: {
3050
+ rel: "canonical",
3051
+ href: alternates.canonical
3052
+ }
3053
+ });
3054
+ if (alternates.languages) for (const [lang, href] of Object.entries(alternates.languages)) elements.push({
3055
+ tag: "link",
3056
+ attrs: {
3057
+ rel: "alternate",
3058
+ hreflang: lang,
3059
+ href
3060
+ }
3061
+ });
3062
+ if (alternates.media) for (const [media, href] of Object.entries(alternates.media)) elements.push({
3063
+ tag: "link",
3064
+ attrs: {
3065
+ rel: "alternate",
3066
+ media,
3067
+ href
3068
+ }
3069
+ });
3070
+ if (alternates.types) for (const [type, href] of Object.entries(alternates.types)) elements.push({
3071
+ tag: "link",
3072
+ attrs: {
3073
+ rel: "alternate",
3074
+ type,
3075
+ href
3076
+ }
3077
+ });
3078
+ }
3079
+ /**
3080
+ * Render site verification meta tags (Google, Yahoo, Yandex, custom).
3081
+ */
3082
+ function renderVerification(verification, elements) {
3083
+ const verificationProps = [
3084
+ ["google-site-verification", verification.google],
3085
+ ["y_key", verification.yahoo],
3086
+ ["yandex-verification", verification.yandex]
3087
+ ];
3088
+ for (const [name, content] of verificationProps) if (content) elements.push({
3089
+ tag: "meta",
3090
+ attrs: {
3091
+ name,
3092
+ content
3093
+ }
3094
+ });
3095
+ if (verification.other) for (const [name, value] of Object.entries(verification.other)) {
3096
+ const content = Array.isArray(value) ? value.join(", ") : value;
3097
+ elements.push({
3098
+ tag: "meta",
3099
+ attrs: {
3100
+ name,
3101
+ content
3102
+ }
3103
+ });
3104
+ }
3105
+ }
3106
+ /**
3107
+ * Render Apple Web App meta tags and startup image links.
3108
+ */
3109
+ function renderAppleWebApp(appleWebApp, elements) {
3110
+ if (appleWebApp.capable) elements.push({
3111
+ tag: "meta",
3112
+ attrs: {
3113
+ name: "apple-mobile-web-app-capable",
3114
+ content: "yes"
3115
+ }
3116
+ });
3117
+ if (appleWebApp.title) elements.push({
3118
+ tag: "meta",
3119
+ attrs: {
3120
+ name: "apple-mobile-web-app-title",
3121
+ content: appleWebApp.title
3122
+ }
3123
+ });
3124
+ if (appleWebApp.statusBarStyle) elements.push({
3125
+ tag: "meta",
3126
+ attrs: {
3127
+ name: "apple-mobile-web-app-status-bar-style",
3128
+ content: appleWebApp.statusBarStyle
3129
+ }
3130
+ });
3131
+ if (appleWebApp.startupImage) {
3132
+ const images = Array.isArray(appleWebApp.startupImage) ? appleWebApp.startupImage : [{ url: appleWebApp.startupImage }];
3133
+ for (const img of images) {
3134
+ const attrs = {
3135
+ rel: "apple-touch-startup-image",
3136
+ href: typeof img === "string" ? img : img.url
3137
+ };
3138
+ if (typeof img === "object" && img.media) attrs.media = img.media;
3139
+ elements.push({
3140
+ tag: "link",
3141
+ attrs
3142
+ });
3143
+ }
3144
+ }
3145
+ }
3146
+ /**
3147
+ * Render App Links (al:*) meta tags for deep linking across platforms.
3148
+ */
3149
+ function renderAppLinks(appLinks, elements) {
3150
+ const platformEntries = [
3151
+ ["ios", appLinks.ios],
3152
+ ["android", appLinks.android],
3153
+ ["windows", appLinks.windows],
3154
+ ["windows_phone", appLinks.windowsPhone],
3155
+ ["windows_universal", appLinks.windowsUniversal]
3156
+ ];
3157
+ for (const [platform, entries] of platformEntries) {
3158
+ if (!entries) continue;
3159
+ for (const entry of entries) for (const [key, value] of Object.entries(entry)) if (value !== void 0 && value !== null) elements.push({
3160
+ tag: "meta",
3161
+ attrs: {
3162
+ property: `al:${platform}:${key}`,
3163
+ content: String(value)
3164
+ }
3165
+ });
3166
+ }
3167
+ if (appLinks.web) {
3168
+ if (appLinks.web.url) elements.push({
3169
+ tag: "meta",
3170
+ attrs: {
3171
+ property: "al:web:url",
3172
+ content: appLinks.web.url
3173
+ }
3174
+ });
3175
+ if (appLinks.web.shouldFallback !== void 0) elements.push({
3176
+ tag: "meta",
3177
+ attrs: {
3178
+ property: "al:web:should_fallback",
3179
+ content: appLinks.web.shouldFallback ? "true" : "false"
3180
+ }
3181
+ });
3182
+ }
3183
+ }
3184
+ /**
3185
+ * Render Apple iTunes smart banner meta tag.
3186
+ */
3187
+ function renderItunes(itunes, elements) {
3188
+ const parts = [`app-id=${itunes.appId}`];
3189
+ if (itunes.affiliateData) parts.push(`affiliate-data=${itunes.affiliateData}`);
3190
+ if (itunes.appArgument) parts.push(`app-argument=${itunes.appArgument}`);
3191
+ elements.push({
3192
+ tag: "meta",
3193
+ attrs: {
3194
+ name: "apple-itunes-app",
3195
+ content: parts.join(", ")
3196
+ }
3197
+ });
3198
+ }
3199
+ //#endregion
3200
+ //#region src/server/metadata-render.ts
3201
+ /**
3202
+ * Convert resolved metadata into an array of head element descriptors.
3203
+ *
3204
+ * Each descriptor has a `tag` ('title', 'meta', 'link') and either
3205
+ * `content` (for <title>) or `attrs` (for <meta>/<link>).
3206
+ *
3207
+ * The framework's MetadataResolver component consumes these descriptors
3208
+ * and renders them into the <head>.
3209
+ */
3210
+ function renderMetadataToElements(metadata) {
3211
+ const elements = [];
3212
+ if (typeof metadata.title === "string") elements.push({
3213
+ tag: "title",
3214
+ content: metadata.title
3215
+ });
3216
+ const simpleMetaProps = [
3217
+ ["description", metadata.description],
3218
+ ["generator", metadata.generator],
3219
+ ["application-name", metadata.applicationName],
3220
+ ["referrer", metadata.referrer],
3221
+ ["category", metadata.category],
3222
+ ["creator", metadata.creator],
3223
+ ["publisher", metadata.publisher]
3224
+ ];
3225
+ for (const [name, content] of simpleMetaProps) if (content) elements.push({
3226
+ tag: "meta",
3227
+ attrs: {
3228
+ name,
3229
+ content
3230
+ }
3231
+ });
3232
+ if (metadata.keywords) {
3233
+ const content = Array.isArray(metadata.keywords) ? metadata.keywords.join(", ") : metadata.keywords;
3234
+ elements.push({
3235
+ tag: "meta",
3236
+ attrs: {
3237
+ name: "keywords",
3238
+ content
3239
+ }
3240
+ });
3241
+ }
3242
+ if (metadata.robots) {
3243
+ const content = typeof metadata.robots === "string" ? metadata.robots : renderRobotsObject(metadata.robots);
3244
+ elements.push({
3245
+ tag: "meta",
3246
+ attrs: {
3247
+ name: "robots",
3248
+ content
3249
+ }
3250
+ });
3251
+ if (typeof metadata.robots === "object" && metadata.robots.googleBot) {
3252
+ const gbContent = typeof metadata.robots.googleBot === "string" ? metadata.robots.googleBot : renderRobotsObject(metadata.robots.googleBot);
3253
+ elements.push({
3254
+ tag: "meta",
3255
+ attrs: {
3256
+ name: "googlebot",
3257
+ content: gbContent
3258
+ }
3259
+ });
3260
+ }
3261
+ }
3262
+ if (metadata.openGraph) {
3263
+ const og = { ...metadata.openGraph };
3264
+ if (og.title === void 0 && typeof metadata.title === "string") og.title = metadata.title;
3265
+ if (og.description === void 0 && metadata.description) og.description = metadata.description;
3266
+ renderOpenGraph(og, elements);
3267
+ }
3268
+ if (metadata.twitter) {
3269
+ const tw = { ...metadata.twitter };
3270
+ if (tw.title === void 0) tw.title = metadata.openGraph?.title !== void 0 ? metadata.openGraph.title : typeof metadata.title === "string" ? metadata.title : void 0;
3271
+ if (tw.description === void 0) tw.description = metadata.openGraph?.description !== void 0 ? metadata.openGraph.description : metadata.description;
3272
+ renderTwitter(tw, elements);
3273
+ }
3274
+ if (metadata.icons) renderIcons(metadata.icons, elements);
3275
+ if (metadata.manifest) elements.push({
3276
+ tag: "link",
3277
+ attrs: {
3278
+ rel: "manifest",
3279
+ href: metadata.manifest
3280
+ }
3281
+ });
3282
+ if (metadata.alternates) renderAlternates(metadata.alternates, elements);
3283
+ if (metadata.verification) renderVerification(metadata.verification, elements);
3284
+ if (metadata.formatDetection) {
3285
+ const parts = [];
3286
+ if (metadata.formatDetection.telephone === false) parts.push("telephone=no");
3287
+ if (metadata.formatDetection.email === false) parts.push("email=no");
3288
+ if (metadata.formatDetection.address === false) parts.push("address=no");
3289
+ if (parts.length > 0) elements.push({
3290
+ tag: "meta",
3291
+ attrs: {
3292
+ name: "format-detection",
3293
+ content: parts.join(", ")
3294
+ }
3295
+ });
3296
+ }
3297
+ if (metadata.authors) {
3298
+ const authorList = Array.isArray(metadata.authors) ? metadata.authors : [metadata.authors];
3299
+ for (const author of authorList) {
3300
+ if (author.name) elements.push({
3301
+ tag: "meta",
3302
+ attrs: {
3303
+ name: "author",
3304
+ content: author.name
3305
+ }
3306
+ });
3307
+ if (author.url) elements.push({
3308
+ tag: "link",
3309
+ attrs: {
3310
+ rel: "author",
3311
+ href: author.url
3312
+ }
3313
+ });
3314
+ }
3315
+ }
3316
+ if (metadata.appleWebApp) renderAppleWebApp(metadata.appleWebApp, elements);
3317
+ if (metadata.appLinks) renderAppLinks(metadata.appLinks, elements);
3318
+ if (metadata.itunes) renderItunes(metadata.itunes, elements);
3319
+ if (metadata.other) for (const [name, value] of Object.entries(metadata.other)) {
3320
+ const content = Array.isArray(value) ? value.join(", ") : value;
3321
+ elements.push({
3322
+ tag: "meta",
3323
+ attrs: {
3324
+ name,
3325
+ content
3326
+ }
3327
+ });
3328
+ }
3329
+ return elements;
3330
+ }
3331
+ function renderRobotsObject(robots) {
3332
+ const parts = [];
3333
+ if (robots.index === true) parts.push("index");
3334
+ if (robots.index === false) parts.push("noindex");
3335
+ if (robots.follow === true) parts.push("follow");
3336
+ if (robots.follow === false) parts.push("nofollow");
3337
+ return parts.join(", ");
3338
+ }
3339
+ //#endregion
3340
+ //#region src/server/metadata.ts
3341
+ /**
3342
+ * Resolve a title value with an optional template.
3343
+ *
3344
+ * - string → apply template if present
3345
+ * - { absolute: '...' } → use as-is, skip template
3346
+ * - { default: '...' } → use as fallback (no template applied)
3347
+ * - undefined → undefined
3348
+ */
3349
+ function resolveTitle(title, template) {
3350
+ if (title === void 0 || title === null) return;
3351
+ if (typeof title === "string") return template ? template.replace("%s", title) : title;
3352
+ if (title.absolute !== void 0) return title.absolute;
3353
+ if (title.default !== void 0) return title.default;
3354
+ }
3355
+ /**
3356
+ * Resolve metadata from a segment chain.
3357
+ *
3358
+ * Processes entries from root layout to page (in segment order).
3359
+ * The merge algorithm:
3360
+ * 1. Shallow-merge all keys except title (later wins)
3361
+ * 2. Track the most recent title template
3362
+ * 3. Resolve the final title using the template
3363
+ *
3364
+ * In error state, the page entry is dropped and noindex is injected.
3365
+ *
3366
+ * See design/16-metadata.md §"Merge Algorithm"
3367
+ */
3368
+ function resolveMetadata(entries, options = {}) {
3369
+ const { errorState = false } = options;
3370
+ const merged = {};
3371
+ let titleTemplate;
3372
+ let lastDefault;
3373
+ let rawTitle;
3374
+ for (const { metadata, isPage } of entries) {
3375
+ if (errorState && isPage) continue;
3376
+ if (metadata.title !== void 0 && typeof metadata.title === "object") {
3377
+ if (metadata.title.template !== void 0) titleTemplate = metadata.title.template;
3378
+ if (metadata.title.default !== void 0) lastDefault = metadata.title.default;
3379
+ }
3380
+ for (const key of Object.keys(metadata)) {
3381
+ if (key === "title") continue;
3382
+ merged[key] = metadata[key];
3383
+ }
3384
+ if (metadata.title !== void 0) rawTitle = metadata.title;
3385
+ }
3386
+ if (errorState) {
3387
+ rawTitle = lastDefault !== void 0 ? { default: lastDefault } : rawTitle;
3388
+ titleTemplate = void 0;
3389
+ }
3390
+ const resolvedTitle = resolveTitle(rawTitle, titleTemplate);
3391
+ if (resolvedTitle !== void 0) merged.title = resolvedTitle;
3392
+ if (errorState) merged.robots = "noindex";
3393
+ return merged;
3394
+ }
3395
+ /**
3396
+ * Check if a string is an absolute URL.
3397
+ */
3398
+ function isAbsoluteUrl(url) {
3399
+ return url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//");
3400
+ }
3401
+ /**
3402
+ * Resolve a relative URL against a base URL.
3403
+ */
3404
+ function resolveUrl(url, base) {
3405
+ if (isAbsoluteUrl(url)) return url;
3406
+ return new URL(url, base).toString();
3407
+ }
3408
+ /**
3409
+ * Resolve relative URLs in metadata fields against metadataBase.
3410
+ *
3411
+ * Returns a new metadata object with URLs resolved. Absolute URLs are not modified.
3412
+ * If metadataBase is not set, returns the metadata unchanged.
3413
+ */
3414
+ function resolveMetadataUrls(metadata) {
3415
+ const base = metadata.metadataBase;
3416
+ if (!base) return metadata;
3417
+ const result = { ...metadata };
3418
+ if (result.openGraph) {
3419
+ result.openGraph = { ...result.openGraph };
3420
+ if (typeof result.openGraph.images === "string") result.openGraph.images = resolveUrl(result.openGraph.images, base);
3421
+ else if (Array.isArray(result.openGraph.images)) result.openGraph.images = result.openGraph.images.map((img) => ({
3422
+ ...img,
3423
+ url: resolveUrl(img.url, base)
3424
+ }));
3425
+ else if (result.openGraph.images) result.openGraph.images = {
3426
+ ...result.openGraph.images,
3427
+ url: resolveUrl(result.openGraph.images.url, base)
3428
+ };
3429
+ if (result.openGraph.url && !isAbsoluteUrl(result.openGraph.url)) result.openGraph.url = resolveUrl(result.openGraph.url, base);
3430
+ }
3431
+ if (result.twitter) {
3432
+ result.twitter = { ...result.twitter };
3433
+ if (typeof result.twitter.images === "string") result.twitter.images = resolveUrl(result.twitter.images, base);
3434
+ else if (Array.isArray(result.twitter.images)) {
3435
+ const resolved = result.twitter.images.map((img) => typeof img === "string" ? resolveUrl(img, base) : {
3436
+ ...img,
3437
+ url: resolveUrl(img.url, base)
3438
+ });
3439
+ const allStrings = resolved.every((r) => typeof r === "string");
3440
+ result.twitter.images = allStrings ? resolved : resolved;
3441
+ } else if (result.twitter.images) result.twitter.images = {
3442
+ ...result.twitter.images,
3443
+ url: resolveUrl(result.twitter.images.url, base)
3444
+ };
3445
+ }
3446
+ if (result.alternates) {
3447
+ result.alternates = { ...result.alternates };
3448
+ if (result.alternates.canonical && !isAbsoluteUrl(result.alternates.canonical)) result.alternates.canonical = resolveUrl(result.alternates.canonical, base);
3449
+ if (result.alternates.languages) {
3450
+ const langs = {};
3451
+ for (const [lang, url] of Object.entries(result.alternates.languages)) langs[lang] = isAbsoluteUrl(url) ? url : resolveUrl(url, base);
3452
+ result.alternates.languages = langs;
3453
+ }
3454
+ }
3455
+ if (result.icons) {
3456
+ result.icons = { ...result.icons };
3457
+ if (typeof result.icons.icon === "string") result.icons.icon = resolveUrl(result.icons.icon, base);
3458
+ else if (Array.isArray(result.icons.icon)) result.icons.icon = result.icons.icon.map((i) => ({
3459
+ ...i,
3460
+ url: resolveUrl(i.url, base)
3461
+ }));
3462
+ if (typeof result.icons.apple === "string") result.icons.apple = resolveUrl(result.icons.apple, base);
3463
+ else if (Array.isArray(result.icons.apple)) result.icons.apple = result.icons.apple.map((i) => ({
3464
+ ...i,
3465
+ url: resolveUrl(i.url, base)
3466
+ }));
3467
+ }
3468
+ return result;
3469
+ }
3470
+ //#endregion
3471
+ //#region src/server/route-handler.ts
3472
+ /** All recognized HTTP method export names. */
3473
+ var HTTP_METHODS = [
3474
+ "GET",
3475
+ "POST",
3476
+ "PUT",
3477
+ "PATCH",
3478
+ "DELETE",
3479
+ "HEAD",
3480
+ "OPTIONS"
3481
+ ];
3482
+ /**
3483
+ * Resolve the full list of allowed methods for a route module.
3484
+ *
3485
+ * Includes:
3486
+ * - All explicitly exported methods
3487
+ * - HEAD (implicit when GET is exported)
3488
+ * - OPTIONS (always implicit)
3489
+ */
3490
+ function resolveAllowedMethods(mod) {
3491
+ const methods = [];
3492
+ for (const method of HTTP_METHODS) {
3493
+ if (method === "HEAD" || method === "OPTIONS") continue;
3494
+ if (mod[method]) methods.push(method);
3495
+ }
3496
+ if (mod.GET && !mod.HEAD) methods.push("HEAD");
3497
+ else if (mod.HEAD) methods.push("HEAD");
3498
+ if (!mod.OPTIONS) methods.push("OPTIONS");
3499
+ else methods.push("OPTIONS");
3500
+ return methods;
3501
+ }
3502
+ /**
3503
+ * Handle an incoming request against a route.ts module.
3504
+ *
3505
+ * Dispatches to the named method handler, auto-generates 405/OPTIONS,
3506
+ * and merges response headers from ctx.headers.
3507
+ */
3508
+ async function handleRouteRequest(mod, ctx) {
3509
+ const method = ctx.req.method.toUpperCase();
3510
+ const allowHeader = resolveAllowedMethods(mod).join(", ");
3511
+ if (method === "OPTIONS") {
3512
+ if (mod.OPTIONS) return runHandler(mod.OPTIONS, ctx);
3513
+ return mergeResponseHeaders(new Response(null, {
3514
+ status: 204,
3515
+ headers: { Allow: allowHeader }
3516
+ }), ctx.headers);
3517
+ }
3518
+ if (method === "HEAD") {
3519
+ if (mod.HEAD) return stripHeadBody(await runHandler(mod.HEAD, ctx), method);
3520
+ if (mod.GET) return stripHeadBody(await runHandler(mod.GET, ctx), method);
3521
+ }
3522
+ const handler = mod[method];
3523
+ if (!handler) return mergeResponseHeaders(new Response(null, {
3524
+ status: 405,
3525
+ headers: { Allow: allowHeader }
3526
+ }), ctx.headers);
3527
+ return runHandler(handler, ctx);
3528
+ }
3529
+ /**
3530
+ * Run a handler with cookie writes enabled and merge ctx.headers into its
3531
+ * response.
3532
+ *
3533
+ * Cookies (design/29-cookies.md §"Where Cookies Can Be Set"): the render
3534
+ * phase applied the jar to `ctx.headers` before the handler ran, so on the
3535
+ * way out only what the handler changed is synced onto that bag. It happens
3536
+ * whether the handler returns or throws, so a cookie set before `redirect()`
3537
+ * rides the redirect, and the pipeline's 500 for a throw carries the same
3538
+ * bag.
3539
+ *
3540
+ * Nothing is caught. A signal goes to the pipeline, whose deny fallback
3541
+ * answers a route.ts match in JSON and whose redirect outcome sends the 3xx
3542
+ * (TIM-1533). Any other error goes to the pipeline, which logs it,
3543
+ * reports it to `onRequestError` as phase `'route'`, and answers it in the
3544
+ * same format as every other route.ts error (TIM-1537).
3545
+ */
3546
+ async function runHandler(handler, ctx) {
3547
+ const applied = getSetCookieHeaders();
3548
+ setMutableCookieContext(true);
3549
+ let res;
3550
+ try {
3551
+ res = await handler(ctx);
3552
+ } finally {
3553
+ setMutableCookieContext(false);
3554
+ syncCookieJar(ctx.headers, applied);
3555
+ }
3556
+ return mergeResponseHeaders(res, ctx.headers);
3557
+ }
3558
+ /**
3559
+ * Merge response headers from ctx.headers into the handler's response.
3560
+ * ctx.headers (set by middleware or the handler) are applied to the final response.
3561
+ * Handler-set headers take precedence over ctx.headers.
3562
+ */
3563
+ function mergeResponseHeaders(res, ctxHeaders) {
3564
+ let hasCtxHeaders = false;
3565
+ ctxHeaders.forEach(() => {
3566
+ hasCtxHeaders = true;
3567
+ });
3568
+ if (!hasCtxHeaders) return res;
3569
+ const merged = new Headers();
3570
+ ctxHeaders.forEach((value, key) => {
3571
+ if (key.toLowerCase() === "set-cookie") merged.append(key, value);
3572
+ else merged.set(key, value);
3573
+ });
3574
+ const resCookies = res.headers.getSetCookie();
3575
+ for (const cookie of resCookies) merged.append("Set-Cookie", cookie);
3576
+ res.headers.forEach((value, key) => {
3577
+ if (key.toLowerCase() !== "set-cookie") merged.set(key, value);
3578
+ });
3579
+ return new Response(res.body, {
3580
+ status: res.status,
3581
+ statusText: res.statusText,
3582
+ headers: merged
3583
+ });
3584
+ }
3585
+ //#endregion
3586
+ //#region src/server/prebuilt/cache-key.ts
3587
+ /**
3588
+ * Component cache-key hashing for prebuilt (cache.component) entries.
3589
+ *
3590
+ * Key: `sha256(canonicalJson({ props, params })).slice(0, 16)` — see
3591
+ * design/44-render-at-build-time.md §Cache Key. Both the request-time
3592
+ * lookup (prebuilt-runtime.ts) and the post-build capture pass (TIM-1119)
3593
+ * MUST use this exact function, or build artifacts become unaddressable.
3594
+ *
3595
+ * THE IDENTITY CONTRACT (design/44 §Cache Key): cache identity is **JSON
3596
+ * value semantics** — two inputs are the same entry iff they are the same
3597
+ * JSON value. This is an ALLOWLIST, not a denylist: a value participates
3598
+ * in caching iff it is plain JSON data — null, boolean, finite number,
3599
+ * string, dense array, or a plain `Object.prototype` object with only
3600
+ * enumerable string-keyed data properties — plus `undefined` and `-0`,
3601
+ * which hash as distinct sentinels because flight preserves them. Every
3602
+ * value outside that domain (functions, symbols, bigints, accessors,
3603
+ * hidden or symbol-keyed properties, non-`Object.prototype` prototypes
3604
+ * including null, aliased references, sparse arrays, cycles, non-finite
3605
+ * numbers) is UNKEYABLE by definition: `computeComponentCacheKey` returns
3606
+ * null and the caller renders live — slower, never wrong.
3607
+ *
3608
+ * The line between rejected and declared-out-of-domain: values are
3609
+ * REJECTED when the JSON projection itself would be ambiguous or lossy
3610
+ * (hidden/symbol/non-enumerable properties, accessors, exotic
3611
+ * prototypes). Observable state that leaves the projection intact — key
3612
+ * order, reference identity, descriptor metadata (writable/configurable,
3613
+ * frozen/sealed/extensible), proxy behavior — is DECLARED outside
3614
+ * cache.component's supported input domain rather than rejected:
3615
+ * key-order insensitivity is required for determinism, and rejecting
3616
+ * frozen objects would unkey ubiquitous legitimate inputs
3617
+ * (Object.freeze'd config props) to defend against components that
3618
+ * render differently based on property attributes.
3619
+ *
3620
+ * Two hard rules for this walk itself: it must be TOTAL (never throw
3621
+ * anything but UnkeyableValueError) and SIDE-EFFECT-FREE (descriptor
3622
+ * reads only; never invoke user code such as getters).
3623
+ *
3624
+ * Object keys sort recursively so `{a, b}` and `{b, a}` hash identically
3625
+ * (design/13-security.md checklist #14 — cache key determinism; key order
3626
+ * is deliberately NOT part of identity).
3627
+ */
3628
+ var UnkeyableValueError = class extends Error {};
3629
+ function canonicalize$1(value, seen) {
3630
+ if (value === null) return "null";
3631
+ switch (typeof value) {
3632
+ case "string": return JSON.stringify(value);
3633
+ case "boolean": return value ? "true" : "false";
3634
+ case "number":
3635
+ if (!Number.isFinite(value)) throw new UnkeyableValueError("non-finite number");
3636
+ return Object.is(value, -0) ? "-0" : String(value);
3637
+ case "object": break;
3638
+ case "undefined": return "undefined";
3639
+ default: throw new UnkeyableValueError(`unsupported type: ${typeof value}`);
3640
+ }
3641
+ const obj = value;
3642
+ if (seen.has(obj)) throw new UnkeyableValueError("repeated object reference (cycle or alias)");
3643
+ seen.add(obj);
3644
+ if (Object.getOwnPropertySymbols(obj).length > 0) throw new UnkeyableValueError("symbol-keyed property");
3645
+ {
3646
+ if (Array.isArray(obj)) {
3647
+ if (Object.getOwnPropertyNames(obj).length !== obj.length + 1) throw new UnkeyableValueError("non-index array property");
3648
+ const parts = [];
3649
+ for (let i = 0; i < obj.length; i++) {
3650
+ const desc = Object.getOwnPropertyDescriptor(obj, i);
3651
+ if (desc === void 0) throw new UnkeyableValueError("sparse array hole");
3652
+ if (desc.get !== void 0 || desc.set !== void 0) throw new UnkeyableValueError("accessor property");
3653
+ if (!desc.enumerable) throw new UnkeyableValueError("non-enumerable array element");
3654
+ parts.push(canonicalize$1(desc.value, seen));
3655
+ }
3656
+ return "[" + parts.join(",") + "]";
3657
+ }
3658
+ if (Object.getPrototypeOf(obj) !== Object.prototype) throw new UnkeyableValueError("non-plain object");
3659
+ const keys = Object.keys(obj).sort();
3660
+ if (keys.length !== Object.getOwnPropertyNames(obj).length) throw new UnkeyableValueError("non-enumerable property");
3661
+ const parts = [];
3662
+ for (const key of keys) {
3663
+ const desc = Object.getOwnPropertyDescriptor(obj, key);
3664
+ if (desc.get !== void 0 || desc.set !== void 0) throw new UnkeyableValueError("accessor property");
3665
+ parts.push(JSON.stringify(key) + ":" + canonicalize$1(desc.value, seen));
3666
+ }
3667
+ return "{" + parts.join(",") + "}";
3668
+ }
3669
+ }
3670
+ /**
3671
+ * Canonicalize a single value, or null if it is unkeyable. The ONE
3672
+ * definition of "faithfully comparable" — every prebuilt code path that
3673
+ * needs value equality (cache keys, slot-param divergence) goes through
3674
+ * this, so the unkeyable rules (accessors, hidden props, aliases,
3675
+ * bigints, …) live in exactly one place.
3676
+ */
3677
+ function tryCanonicalize(value) {
3678
+ try {
3679
+ return canonicalize$1(value, /* @__PURE__ */ new Set());
3680
+ } catch (error) {
3681
+ return toUnkeyable(error);
3682
+ }
3683
+ }
3684
+ /**
3685
+ * Totality by construction: ANY throw from the walk means the input is
3686
+ * outside the keyable domain. The walk touches only user-controlled
3687
+ * values through reflection ops — and a Proxy trap can throw arbitrary
3688
+ * errors from any of them (codex review on PR #835) — so converting only
3689
+ * UnkeyableValueError would let hostile/exotic inputs abort a render
3690
+ * that plain live rendering would have served fine. Unexpected error
3691
+ * types are logged for visibility (they would indicate either a proxy
3692
+ * trap or a canonicalizer bug) but still degrade to unkeyable.
3693
+ */
3694
+ function toUnkeyable(error) {
3695
+ if (!(error instanceof UnkeyableValueError)) console.error("[timber] cache key canonicalization threw — treating input as unkeyable:", error);
3696
+ return null;
3697
+ }
3698
+ /**
3699
+ * Split a props record into data props (participate in the cache key) and
3700
+ * slot values (React-element holes, excluded from the key) per the
3701
+ * component's declared `slots` option (TIM-1173).
3702
+ *
3703
+ * Every DECLARED slot lands in `slotValues` — present or not — because the
3704
+ * capture render always injects a SlotOutlet placeholder for each declared
3705
+ * slot: the cached shell is identical whether the caller passed the slot
3706
+ * or omitted it, so slot presence must not vary the key. `dataProps` is
3707
+ * everything else and is what gets hashed.
3708
+ *
3709
+ * Returns null when the props OBJECT itself is outside the identity
3710
+ * contract at the top level — accessors, symbol-keyed or non-enumerable
3711
+ * properties, or a non-`Object.prototype` prototype. Splitting such an
3712
+ * object would normalize away observable state (and reading an accessor
3713
+ * would invoke user code, which this walk must never do) BEFORE
3714
+ * `computeComponentCacheKey` gets the chance to reject it — a cached
3715
+ * shell could then be served for inputs a live render distinguishes.
3716
+ * Null means unkeyable: the caller renders live with the original props.
3717
+ * The walk is descriptor-reads only; getters are never invoked. Nested
3718
+ * data-prop values still face the full allowlist when hashed.
3719
+ */
3720
+ /** Create an own enumerable data property — safe for keys like `__proto__`. */
3721
+ function defineOwn(target, key, value) {
3722
+ Object.defineProperty(target, key, {
3723
+ value,
3724
+ enumerable: true,
3725
+ writable: true,
3726
+ configurable: true
3727
+ });
3728
+ }
3729
+ function splitSlotProps(props, slots) {
3730
+ try {
3731
+ if (Object.getPrototypeOf(props) !== Object.prototype) return null;
3732
+ if (Object.getOwnPropertySymbols(props).length > 0) return null;
3733
+ const slotSet = new Set(slots);
3734
+ const dataProps = {};
3735
+ const slotValues = {};
3736
+ for (const key of Object.getOwnPropertyNames(props)) {
3737
+ const desc = Object.getOwnPropertyDescriptor(props, key);
3738
+ if (desc === void 0 || desc.get !== void 0 || desc.set !== void 0 || !desc.enumerable) return null;
3739
+ defineOwn(slotSet.has(key) ? slotValues : dataProps, key, desc.value);
3740
+ }
3741
+ for (const slot of slotSet) if (!Object.hasOwn(slotValues, slot)) defineOwn(slotValues, slot, void 0);
3742
+ return {
3743
+ dataProps,
3744
+ slotValues
3745
+ };
3746
+ } catch (error) {
3747
+ return toUnkeyable(error);
3748
+ }
3749
+ }
3750
+ /**
3751
+ * Compute the cache-entry hash for a component render:
3752
+ * `sha256({props, params}).slice(0, 16)` over canonical JSON.
3753
+ *
3754
+ * Returns null when props (or params) contain unkeyable values — the
3755
+ * caller must fall back to dynamic rendering.
3756
+ */
3757
+ function computeComponentCacheKey(props, params) {
3758
+ let canonical;
3759
+ try {
3760
+ const seen = /* @__PURE__ */ new Set();
3761
+ canonical = "{\"params\":" + canonicalize$1({ ...params }, seen) + ",\"props\":" + canonicalize$1(props, seen) + "}";
3762
+ } catch (error) {
3763
+ return toUnkeyable(error);
3764
+ }
3765
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
3766
+ }
3767
+ //#endregion
3768
+ //#region src/client/slot-context.ts
3769
+ /**
3770
+ * SlotContext — delivers live slot content to SlotOutlet holes inside
3771
+ * cached component shells (TIM-1173).
3772
+ *
3773
+ * A `cache.component(Fn, { slots: [...] })` shell is captured with a
3774
+ * stable <SlotOutlet slot={name} /> client reference in place of each
3775
+ * declared slot prop. At request time the revived shell is wrapped in a
3776
+ * SlotsProvider carrying the live slot values; each outlet reads its
3777
+ * value from this context by name.
3778
+ *
3779
+ * Providers MERGE per-key with their parent: an inner SlotsProvider
3780
+ * inherits the outer's values and overrides only the keys it declares.
3781
+ * Two sibling instances each have their own provider and resolve
3782
+ * independently. Layouts use a reserved slot name (LAYOUT_CHILDREN_SLOT)
3783
+ * that cannot collide with user-declared slot names (TIM-1191).
3784
+ *
3785
+ * Generalizes the original layout children pattern (TIM-1181) from a
3786
+ * single implicit `children` hole on layouts to explicitly declared,
3787
+ * named slot props on any cached component. Layouts use this same
3788
+ * context with the reserved LAYOUT_CHILDREN_SLOT key (TIM-1191).
3789
+ *
3790
+ * SINGLETON GUARANTEE: globalThis + Symbol.for — the RSC client bundler
3791
+ * can duplicate this module across chunks; globalThis guarantees a single
3792
+ * context instance.
3793
+ *
3794
+ * See design/45-cache-lifetimes.md §Slot Components.
3795
+ */
3796
+ var CTX_KEY = Symbol.for("__timber_slot_ctx");
3797
+ function getOrCreateContext() {
3798
+ const existing = globalThis[CTX_KEY];
3799
+ if (existing !== void 0) return existing;
3800
+ if (typeof React.createContext === "function") {
3801
+ const ctx = React.createContext(null);
3802
+ globalThis[CTX_KEY] = ctx;
3803
+ return ctx;
3804
+ }
3805
+ }
3806
+ var SlotContext = getOrCreateContext();
3807
+ //#endregion
3808
+ //#region src/client/slot-outlet.tsx
3809
+ /**
3810
+ * SlotOutlet — the hole in a cached component shell (TIM-1173).
3811
+ *
3812
+ * At capture time the prebuilt runtime renders the wrapped component with
3813
+ * <SlotOutlet slot={name} /> in place of each declared slot prop. Being a
3814
+ * client component, it serializes into the flight payload as a stable,
3815
+ * deterministic client reference — the same bytes regardless of what live
3816
+ * content will fill the hole. At request time the revived shell sits under
3817
+ * a SlotsProvider and each outlet reads its live value from SlotContext.
3818
+ *
3819
+ * See design/45-cache-lifetimes.md §Slot Components.
3820
+ */
3821
+ function SlotOutlet({ slot }) {
3822
+ const values = useContext(SlotContext);
3823
+ return values ? values[slot] ?? null : null;
3824
+ }
3825
+ //#endregion
3826
+ //#region src/client/slot-provider.tsx
3827
+ /**
3828
+ * SlotsProvider — wraps a revived cached shell to supply live slot content
3829
+ * to the SlotOutlet holes inside it (TIM-1173).
3830
+ *
3831
+ * Values MERGE with the nearest parent provider so that nesting a
3832
+ * cache.component SlotsProvider inside a layout SlotsProvider preserves
3833
+ * the layout's `children` slot while adding the component's own slots.
3834
+ * Inner values override outer ones with the same name (spread order).
3835
+ *
3836
+ * Tree structure per cached-component instance:
3837
+ * SlotsProvider(values={children: <Live />})
3838
+ * └── revived shell
3839
+ * └── SlotOutlet(slot="children") → reads context → <Live />
3840
+ *
3841
+ * See design/45-cache-lifetimes.md §Slot Components.
3842
+ */
3843
+ function SlotsProvider({ values, children }) {
3844
+ const parent = useContext(SlotContext);
3845
+ const merged = parent ? {
3846
+ ...parent,
3847
+ ...values
3848
+ } : values;
3849
+ return createElement(SlotContext.Provider, { value: merged }, children);
3850
+ }
3851
+ //#endregion
3852
+ //#region src/server/prebuilt/lifetime.ts
3853
+ /**
3854
+ * Lifetime declarations of a `cache.component` — `prerender`, `ttl`,
3855
+ * `tags` (design/45-cache-lifetimes.md) — and the nesting clamp between
3856
+ * two of them (§Nesting Semantics, TIM-1136).
3857
+ *
3858
+ * The clamp rule lives here once, for both places a capture can embed one
3859
+ * wrapped component in another: the post-build capture pass
3860
+ * (server/prebuilt-builder.ts) and request-time re-renders
3861
+ * (`captureComponentPayload()` in server/prebuilt-runtime.ts, TIM-1545).
3862
+ */
3863
+ /** Whether the component has a runtime lifetime (ttl or tags). */
3864
+ function hasRuntimeLifetime(options) {
3865
+ return options.ttl !== void 0 || options.tags != null;
3866
+ }
3867
+ /**
3868
+ * Resolve tags from options (may be static array or function of props).
3869
+ * Function-form tags receive DATA props only — slot props are excluded so
3870
+ * the tags fn never sees SlotOutlet placeholders (capture paths pass
3871
+ * placeholder-substituted props) and tags stay deterministic per key.
3872
+ */
3873
+ function resolveTags(options, props) {
3874
+ if (!options.tags) return [];
3875
+ if (Array.isArray(options.tags)) return options.tags;
3876
+ const slots = options.slots;
3877
+ const fnProps = slots && slots.length > 0 ? splitSlotProps(props, slots)?.dataProps ?? props : props;
3878
+ return options.tags(fnProps);
3879
+ }
3880
+ /**
3881
+ * Why an embedded copy of `inner` goes stale inside `outer`, or null when
3882
+ * the outer's refreshes cover every refresh channel the inner declares:
3883
+ *
3884
+ * - `shorterTtl`: the inner's ttl is shorter than the outer's effective
3885
+ * lifetime (its ttl, or deploy-long without one).
3886
+ * - `missingTags`: tags the inner resolved to that the outer lacks —
3887
+ * `revalidateTag` on them refreshes the inner's own entries, never the
3888
+ * copy. `null` inner tags (a tags function threw) cannot be proven a
3889
+ * subset, so they count as missing.
3890
+ *
3891
+ * Declaring the inner's tags on the outer is the documented fix, and it
3892
+ * silences the tag half by construction.
3893
+ */
3894
+ function clampReasons(outer, inner) {
3895
+ const outerTtl = outer.options.ttl ?? Infinity;
3896
+ const shorterTtl = inner.options.ttl !== void 0 && inner.options.ttl < outerTtl;
3897
+ const missingTags = inner.tags === null ? null : inner.tags.filter((t) => !outer.tags.includes(t));
3898
+ if (!shorterTtl && missingTags !== null && missingTags.length === 0) return null;
3899
+ return {
3900
+ shorterTtl,
3901
+ missingTags
3902
+ };
3903
+ }
3904
+ /** Render a lifetime declaration for the clamp warning. */
3905
+ function describeLifetime(options, tags) {
3906
+ const parts = [];
3907
+ if (options.prerender) parts.push("prerender");
3908
+ if (options.ttl !== void 0) parts.push(`ttl: ${options.ttl}`);
3909
+ if (tags === null) parts.push("tags: fn");
3910
+ else if (tags.length > 0) parts.push(`tags: ${JSON.stringify(tags)}`);
3911
+ return parts.join(", ") || "no lifetime";
3912
+ }
3913
+ /** When the outer entry — and so every copy embedded in it — refreshes. */
3914
+ function describeRefresh(outer) {
3915
+ const parts = [];
3916
+ if (outer.options.ttl !== void 0) parts.push(`every ${outer.options.ttl}s`);
3917
+ if (outer.tags.length > 0) parts.push(`on revalidateTag of its own tags`);
3918
+ parts.push("on redeploy");
3919
+ return parts.join(", or ");
3920
+ }
3921
+ /**
3922
+ * Warn for each wrapped component embedded in `outer`'s captured payload
3923
+ * whose refresh channels the outer cannot honor. Once per (outer, inner)
3924
+ * pair for the lifetime of `warnedPairs` — the builder passes one per
3925
+ * build, the runtime one per process, since re-renders repeat. The key is
3926
+ * the pair alone, not the reason: resolved tags vary with props, and a key
3927
+ * that included them would grow with the data. So the first clamp found
3928
+ * for a pair is the one reported. Returns the number of warnings emitted.
3929
+ */
3930
+ function warnNestingClamps(outer, nested, warnedPairs, log) {
3931
+ let count = 0;
3932
+ for (const inner of nested) {
3933
+ const reasons = clampReasons(outer, inner);
3934
+ if (reasons === null) continue;
3935
+ const pairKey = `${outer.id}\0${inner.innerId}`;
3936
+ if (warnedPairs.has(pairKey)) continue;
3937
+ warnedPairs.add(pairKey);
3938
+ count++;
3939
+ const outerLifetime = describeLifetime(outer.options, outer.tags);
3940
+ const innerLifetime = describeLifetime(inner.options, inner.tags);
3941
+ const consequences = [];
3942
+ if (reasons.missingTags === null) consequences.push(`${inner.innerId}'s tags could not be resolved during the capture (its tags function threw or needed request context), so revalidating them may NOT update the embedded copy.`);
3943
+ else if (reasons.missingTags.length > 0) {
3944
+ const calls = reasons.missingTags.map((t) => `revalidateTag(${JSON.stringify(t)})`);
3945
+ consequences.push(`${calls.join(", ")} will NOT update the embedded copy; tag ${outer.id} itself if you need that.`);
3946
+ }
3947
+ if (reasons.shorterTtl) consequences.push(`${inner.innerId}'s ttl: ${inner.options.ttl} does not apply to the embedded copy.`);
3948
+ log(`[timber] ${inner.innerId} (${innerLifetime}) is embedded in ${outer.options.prerender ? "prerendered" : "cached"} ${outer.id} — the embedded copy refreshes only when ${outer.id} does (${describeRefresh(outer)}). ${consequences.join(" ")} Chain: ${outer.id} (${outerLifetime}) → ${inner.innerId} (${innerLifetime})`);
3949
+ }
3950
+ return count;
3951
+ }
3952
+ /**
3953
+ * Is this value the framework-injected layout children slot outlet
3954
+ * (`<SlotOutlet slot={LAYOUT_CHILDREN_SLOT} />`)? Layout components
3955
+ * receive this as their `children` prop — a stable client reference
3956
+ * whose cache key is stripped so layout `cache.component` hits work
3957
+ * (design/45 §Layout Propagation).
3958
+ *
3959
+ * Uses a framework-reserved slot name (LAYOUT_CHILDREN_SLOT) that
3960
+ * cannot collide with user-declared slot names, preventing context
3961
+ * shadowing when a cached component with its own SlotsProvider is
3962
+ * nested inside a layout. See TIM-1191.
3963
+ *
3964
+ * Total: the probes run against a user-controlled value, and a Proxy
3965
+ * trap can throw — that must classify as "not the outlet" (the
3966
+ * canonicalizer then rejects the value as unkeyable → live render), not
3967
+ * abort the request (codex P2 on PR #890).
3968
+ */
3969
+ function isChildrenSlotOutletElement(value) {
3970
+ try {
3971
+ if (value == null || typeof value !== "object") return false;
3972
+ if (!("type" in value)) return false;
3973
+ const el = value;
3974
+ return el.type === SlotOutlet && el.props?.slot === "\0timber:children";
3975
+ } catch {
3976
+ return false;
3977
+ }
3978
+ }
3979
+ /**
3980
+ * `props` without the framework-injected children outlet — the key props
3981
+ * of a render. The outlet is the same client reference on every request,
3982
+ * so it never varies the cache key; user-provided children stay (and
3983
+ * leave the props unkeyable). See design/45 §Layout Propagation, TIM-1181.
3984
+ */
3985
+ function withoutChildrenOutlet(props) {
3986
+ if (!isChildrenSlotOutletElement(props.children)) return props;
3987
+ const { children: _children, ...rest } = props;
3988
+ return rest;
3989
+ }
3990
+ /**
3991
+ * Dense array of non-empty strings — no holes, no non-string entries.
3992
+ * Total: the reads run against a user-controlled value at module load,
3993
+ * and a throwing Proxy trap must classify as invalid, not crash startup.
3994
+ */
3995
+ function isDenseStringArray(value) {
3996
+ try {
3997
+ if (!Array.isArray(value)) return false;
3998
+ for (let i = 0; i < value.length; i++) {
3999
+ if (!Object.hasOwn(value, i)) return false;
4000
+ const entry = value[i];
4001
+ if (typeof entry !== "string" || entry.length === 0) return false;
4002
+ }
4003
+ return true;
4004
+ } catch {
4005
+ return false;
4006
+ }
4007
+ }
4008
+ /**
4009
+ * Validate the `slots` option. Both the production and dev wrappers gate
4010
+ * on this — slot substitution is active in dev exactly when it would be
4011
+ * active in production, so the tree shape a component sees never
4012
+ * diverges between the two.
4013
+ */
4014
+ function resolveSlotNames(id, options) {
4015
+ const slots = options.slots;
4016
+ if (slots === void 0) return { kind: "none" };
4017
+ if (!isDenseStringArray(slots)) {
4018
+ console.warn(`[timber] cache.component "${id}": invalid slots declaration — expected an array of non-empty prop-name strings. Slot caching is disabled.`);
4019
+ return { kind: "disabled" };
4020
+ }
4021
+ if (slots.length === 0) return { kind: "none" };
4022
+ if (!hasRuntimeLifetime(options)) {
4023
+ console.warn(`[timber] cache.component "${id}": slots require a runtime lifetime (ttl and/or tags). Slot caching is disabled.`);
4024
+ return { kind: "disabled" };
4025
+ }
4026
+ return {
4027
+ kind: "active",
4028
+ slots
4029
+ };
4030
+ }
4031
+ /**
4032
+ * Split the live props into the cacheable shell render (data props plus a
4033
+ * deterministic SlotOutlet placeholder per declared slot — identical
4034
+ * whether or not the caller passed the slot) and the per-instance live
4035
+ * slot values.
4036
+ *
4037
+ * Null when the props object itself is unkeyable at the top level
4038
+ * (accessors, hidden properties, exotic prototype — see splitSlotProps):
4039
+ * the caller must render live with the original props.
4040
+ */
4041
+ function prepareSlotRender(props, slots) {
4042
+ const split = splitSlotProps(props ?? {}, slots);
4043
+ if (split === null) return null;
4044
+ const { dataProps, slotValues } = split;
4045
+ const shellProps = { ...dataProps };
4046
+ for (const slot of slots) Object.defineProperty(shellProps, slot, {
4047
+ value: createElement(SlotOutlet, { slot }),
4048
+ enumerable: true,
4049
+ writable: true,
4050
+ configurable: true
4051
+ });
4052
+ return {
4053
+ shellProps,
4054
+ dataProps,
4055
+ slotValues
4056
+ };
4057
+ }
4058
+ /**
4059
+ * Wrap a rendered shell (revived payload or live element) in a
4060
+ * per-instance SlotsProvider feeding the live values to its outlets.
4061
+ */
4062
+ function wrapInSlotsProvider(shell, slotValues) {
4063
+ return createElement(SlotsProvider, {
4064
+ values: slotValues,
4065
+ children: shell
4066
+ });
4067
+ }
4068
+ //#endregion
4069
+ //#region src/server/prebuilt/key-discipline.ts
4070
+ var REQUEST_CONTEXT_FIELDS = {
4071
+ headers: "headers()",
4072
+ parsedCookies: "cookies()",
4073
+ searchParams: "searchParams"
4074
+ };
4075
+ /** Rate-limit interval: one diagnostic per component per 60 seconds. */
4076
+ var RATE_LIMIT_MS = 6e4;
4077
+ /** Last emission timestamp per component ID. */
4078
+ var lastEmitted = /* @__PURE__ */ new Map();
4079
+ var strict = false;
4080
+ /**
4081
+ * Whether the proxy should be created for this component on this render.
4082
+ * Returns true when strict mode is on (always check) or enough time has
4083
+ * elapsed since the last diagnostic for this component.
4084
+ */
4085
+ function shouldCheckKeyDiscipline(id) {
4086
+ if (strict) return true;
4087
+ const last = lastEmitted.get(id);
4088
+ if (last === void 0) return true;
4089
+ return Date.now() - last >= RATE_LIMIT_MS;
4090
+ }
4091
+ /**
4092
+ * Wrap a store with a proxy that records per-request field access.
4093
+ * The proxy forwards all reads/writes transparently — it only observes.
4094
+ */
4095
+ function createKeyDisciplineProxy(store) {
4096
+ const accessedApis = /* @__PURE__ */ new Set();
4097
+ return {
4098
+ proxy: new Proxy(store, { get(target, prop, receiver) {
4099
+ const api = REQUEST_CONTEXT_FIELDS[prop];
4100
+ if (api) accessedApis.add(api);
4101
+ return Reflect.get(target, prop, receiver);
4102
+ } }),
4103
+ accessedApis
4104
+ };
4105
+ }
4106
+ /**
4107
+ * Emit the key-discipline diagnostic or throw in strict mode.
4108
+ *
4109
+ * In strict mode: throws an error so the tainted payload is never cached.
4110
+ * In default mode: emits a structured error-level diagnostic through the
4111
+ * logger (same path as render errors), rate-limited per component.
4112
+ */
4113
+ function emitKeyDisciplineWarning(id, apis) {
4114
+ const apiList = Array.from(apis).join(", ");
4115
+ if (strict) throw new Error(`[timber] cache.component key-discipline violation: "${id}" reads ${apiList} during a cache capture render. The cache key does not include request-derived values — caching this output would serve one user's data to another. Either pass request-derived values as props (included in the cache key) or remove the ${apiList} call. To downgrade this to a warning, set cache.strictKeyDiscipline: false in timber.config.ts.`);
4116
+ const now = Date.now();
4117
+ const last = lastEmitted.get(id);
4118
+ if (last !== void 0 && now - last < RATE_LIMIT_MS) return;
4119
+ lastEmitted.set(id, now);
4120
+ getLogger().error("cache.component cross-user data leak risk", {
4121
+ component: id,
4122
+ accessedApis: Array.from(apis),
4123
+ message: `cache.component "${id}" reads ${apiList} but the cache key is computed from props and segment params only. If the output varies by ${apiList}, different users may see each other's cached content. Either pass request-derived values as props (included in the cache key) or remove the ${apiList} call.`
4124
+ });
4125
+ }
4126
+ //#endregion
4127
+ //#region src/server/prebuilt/capture-state.ts
4128
+ /**
4129
+ * Capture state — the channel between a payload capture and the
4130
+ * `__prebuilt` wrappers that render inside it (server/prebuilt-runtime.ts).
4131
+ *
4132
+ * Two callers capture a wrapped component's flight payload:
4133
+ *
4134
+ * - **Build time**: the post-build capture pass (server/prebuilt-builder.ts)
4135
+ * renders each registry entry in turn.
4136
+ * - **Request time**: `captureComponentPayload()` in prebuilt-runtime.ts
4137
+ * renders a component in isolation for an inline re-render (full miss,
4138
+ * stale entry, tombstone) or an SWR background re-render. These run
4139
+ * concurrently with other requests' renders — and with each other.
4140
+ *
4141
+ * During a capture, the component's subtree may render OTHER wrapped
4142
+ * components. Those inner wrappers render live and their output is baked
4143
+ * flat into the outer entry — the nesting clamp of
4144
+ * design/45-cache-lifetimes.md §Nesting Semantics (TIM-1136). The wrapper
4145
+ * reports every render that happens inside a capture:
4146
+ *
4147
+ * - **Cycle**: the inner render IS the entry being captured — same
4148
+ * component id AND same props key (TIM-1547). Left alone this recurses
4149
+ * until the render times out; instead the wrapper throws
4150
+ * `PrebuiltCaptureCycleError`, failing that entry with an error naming
4151
+ * the cycle (build continues per doc 44 skip-and-report policy; at
4152
+ * request time the re-render fails and is not cached, and the wrapper
4153
+ * falls back as for any failed re-render). A component that renders
4154
+ * itself with different props (a comment thread, a tree) is legitimate
4155
+ * recursion with a base case and bakes flat like any other nesting.
4156
+ * - **Nesting**: any other wrapper render is recorded per inner component
4157
+ * with the tags it resolved to. Both capture callers run the clamp rule
4158
+ * over those records when the entry completes (prebuilt/lifetime.ts).
4159
+ *
4160
+ * Identity is the capture ROOT only. A wrapper cannot see the wrappers
4161
+ * between it and the root: Flight renders a component's children after
4162
+ * the wrapper has returned, in Flight's own async context, not in a frame
4163
+ * the wrapper could enter. So a same-key cycle that closes below the root
4164
+ * (`A(p1) → A(p2) → A(p2) → …`) is not detected here; it is also an
4165
+ * infinite recursion without caching, and the capture's render timeout
4166
+ * fails it.
4167
+ *
4168
+ * The props key is the props half of the cache key (data props only,
4169
+ * framework children outlet stripped — the same inputs the cache key
4170
+ * hashes). The params half is left out: every render inside one capture
4171
+ * reads the same segment params, so props decide identity. Unkeyable props
4172
+ * (live React elements, …) cannot be proven identical and are never a
4173
+ * cycle.
4174
+ *
4175
+ * Each capture's state lives in an AsyncLocalStorage store entered by
4176
+ * `CaptureScope.run()`, so it is visible only to renders started inside
4177
+ * that capture — including work Flight resumes after an await, which runs
4178
+ * in the context the awaited promise was created in. A concurrent request
4179
+ * rendering the same component is not a cycle, and a concurrent request
4180
+ * rendering a different cached component still revives it from cache
4181
+ * instead of baking it live (TIM-1536). Outside any capture
4182
+ * `noteWrapperRender` is a single store lookup.
4183
+ */
4184
+ /** Thrown by the wrapper when a capture renders its own entry. */
4185
+ var PrebuiltCaptureCycleError = class extends Error {
4186
+ constructor(componentId) {
4187
+ super(`[timber] cache.component "${componentId}" renders itself with the same props (directly or through other components) — capturing its payload would recurse forever. Remove the cycle or stop caching this component with cache.component.`);
4188
+ this.name = "PrebuiltCaptureCycleError";
4189
+ }
4190
+ };
4191
+ /**
4192
+ * The props half of a render's cache key, or null when unkeyable. Same
4193
+ * inputs `tryRevivePayload` hashes: data props only for slot components,
4194
+ * the framework-injected children outlet stripped.
4195
+ */
4196
+ function propsKey(options, props) {
4197
+ let keyProps = props;
4198
+ if (options.slots && options.slots.length > 0) {
4199
+ const split = splitSlotProps(props, options.slots);
4200
+ if (split === null) return null;
4201
+ keyProps = split.dataProps;
4202
+ }
4203
+ return computeComponentCacheKey(withoutChildrenOutlet(keyProps), {});
4204
+ }
4205
+ /**
4206
+ * Open a capture of component `outerId` rendered with `props`. Its state
4207
+ * ends with the scope object.
4208
+ */
4209
+ function createCaptureScope(outerId, options, props) {
4210
+ const store = {
4211
+ outerId,
4212
+ outerPropsKey: propsKey(options, props),
4213
+ nested: /* @__PURE__ */ new Map()
4214
+ };
4215
+ return {
4216
+ run: (fn) => prebuiltCaptureAls.run(store, fn),
4217
+ nestedRenders: () => Array.from(store.nested, ([innerId, record]) => ({
4218
+ innerId,
4219
+ options: record.options,
4220
+ tags: record.tags && Array.from(record.tags)
4221
+ }))
4222
+ };
4223
+ }
4224
+ /** Whether the current render is inside a capture. */
4225
+ function isCaptureActive() {
4226
+ return prebuiltCaptureAls.getStore() !== void 0;
4227
+ }
4228
+ /**
4229
+ * Called by the `__prebuilt` wrapper on every render. No-op outside a
4230
+ * capture. Throws `PrebuiltCaptureCycleError` when the render is the
4231
+ * entry the enclosing capture is producing.
4232
+ */
4233
+ function noteWrapperRender(innerId, options, props) {
4234
+ const capture = prebuiltCaptureAls.getStore();
4235
+ if (capture === void 0) return;
4236
+ if (innerId === capture.outerId && capture.outerPropsKey !== null) {
4237
+ if (propsKey(options, props) === capture.outerPropsKey) throw new PrebuiltCaptureCycleError(innerId);
4238
+ }
4239
+ let record = capture.nested.get(innerId);
4240
+ if (record === void 0) {
4241
+ record = {
4242
+ options,
4243
+ tags: /* @__PURE__ */ new Set()
4244
+ };
4245
+ capture.nested.set(innerId, record);
4246
+ }
4247
+ if (record.tags !== null && options.tags) {
4248
+ const tags = record.tags;
4249
+ try {
4250
+ for (const tag of requestContextAls.exit(() => resolveTags(options, props))) tags.add(tag);
4251
+ } catch {
4252
+ record.tags = null;
4253
+ }
4254
+ }
4255
+ }
4256
+ //#endregion
4257
+ //#region src/server/prebuilt-runtime.ts
4258
+ /**
4259
+ * Prebuilt component runtime — registration target for the timber-prebuilt
4260
+ * transform (plugins/prebuilt.ts) and the request-time half of
4261
+ * cache.component (TIM-1140).
4262
+ *
4263
+ * `cache.component(fn, opts)` call sites are rewritten at build time to
4264
+ * `__prebuilt(stableId, fn, opts)` (production) or `__prebuiltDev(name, fn)`
4265
+ * (dev). Both are exported via #server-internal — they are framework
4266
+ * plumbing, never called by user code directly.
4267
+ *
4268
+ * Cache hits are served by FLIGHT REVIVAL: the cached `.flight` payload is
4269
+ * decoded back into React elements inside the RSC environment with
4270
+ * `createFromReadableStream` (the flight client running in the flight
4271
+ * server — plugin-rsc wires the server-decode manifest so client
4272
+ * references revive as re-serializable reference proxies). The revived
4273
+ * element is returned from the wrapper and the outer render re-serializes
4274
+ * it like any other subtree. This is the same mechanism as Next.js
4275
+ * `"use cache"` (next/src/server/use-cache/use-cache-wrapper.ts).
4276
+ *
4277
+ * What a hit skips: all user work — data fetching, MDX compilation, the
4278
+ * component function itself never runs. What it re-pays: React
4279
+ * re-serializes the already-resolved tree (cost proportional to payload
4280
+ * size, not to data latency). The alternative — splicing raw payload bytes
4281
+ * into the outer stream — avoids re-serialization but requires byte-level
4282
+ * coupling to the flight wire format (row framing, ID rebasing, marker
4283
+ * detection); it was implemented and reviewed on PR #835 and deliberately
4284
+ * replaced by revival. See design/44-render-at-build-time.md
4285
+ * §Request-Time Injection.
4286
+ *
4287
+ * Misses render the original component live via the wrapper's own closure,
4288
+ * inside the React pass — segment params from ALS, deny()/redirect()
4289
+ * signals, Suspense, and error boundaries behave exactly as an unwrapped
4290
+ * component. `fallbackOnMissingSegmentParams: false` throws instead.
4291
+ */
4292
+ /** Default timeout for component re-renders (overridden by renderTimeoutMs). */
4293
+ var DEFAULT_COMPONENT_TIMEOUT_MS = DEFAULT_RENDER_TIMEOUT_MS;
4294
+ /** Singleflight for component re-renders (inline and background). */
4295
+ var componentSingleflight = createSingleflight({ timeoutMs: DEFAULT_COMPONENT_TIMEOUT_MS });
4296
+ var componentTimeoutMs = DEFAULT_COMPONENT_TIMEOUT_MS;
4297
+ var warnedNoWaitUntil = false;
4298
+ /**
4299
+ * (outer, inner) pairs already warned about by request-time captures.
4300
+ * Process-wide: re-renders of a hot component repeat, and the warning is
4301
+ * about the code, not the request. Bounded by the number of wrapped
4302
+ * components in the app.
4303
+ */
4304
+ var warnedNestingPairs = /* @__PURE__ */ new Set();
4305
+ /**
4306
+ * Server-side component registry, keyed by stable component ID. Populated at
4307
+ * module load time by `__prebuilt` calls; consumed by the post-build capture
4308
+ * pass (TIM-1119). Never serialized — component functions are not
4309
+ * Flight-serializable.
4310
+ */
4311
+ var registry = /* @__PURE__ */ new Map();
4312
+ /**
4313
+ * Resolve the params the cache key may faithfully hash, or null if this
4314
+ * render is unkeyable.
4315
+ *
4316
+ * A wrapper rendered inside a parallel slot reads params merged with that
4317
+ * slot's overrides (`getSegmentParams`: `{...segmentParams, ...slotParams}`)
4318
+ * — but the cache key hashes only the route-level params, and the wrapper
4319
+ * cannot know which slot it renders under. The route params are usable
4320
+ * only when every slot's merged view canonicalizes identically to them;
4321
+ * otherwise two renders with the same key could see different effective
4322
+ * inputs and a hit could serve the WRONG cached subtree — so the render
4323
+ * goes live: slower, never wrong. (Build-time capture doesn't model slot
4324
+ * params either, so a divergent request could never have a faithful
4325
+ * entry. Codex review on PR #835; revisit with segment-path-scoped keys
4326
+ * if slot-scoped prebuilt is ever needed.)
4327
+ *
4328
+ * Comparison happens through `tryCanonicalize` — the same single
4329
+ * definition of value equality the cache key uses — so anything it
4330
+ * cannot represent faithfully (bigint-coerced codec params, accessors,
4331
+ * aliases, …) is unkeyable here too, and this guard can never throw.
4332
+ */
4333
+ function resolveKeyParams(store) {
4334
+ const main = store.segmentParams ?? {};
4335
+ const slots = store.slotParams;
4336
+ if (!slots || Object.keys(slots).length === 0) return main;
4337
+ const mainCanonical = tryCanonicalize({ ...main });
4338
+ if (mainCanonical === null) return null;
4339
+ for (const slotParams of Object.values(slots)) {
4340
+ const merged = tryCanonicalize({
4341
+ ...main,
4342
+ ...slotParams
4343
+ });
4344
+ if (merged === null || merged !== mainCanonical) return null;
4345
+ }
4346
+ return main;
4347
+ }
4348
+ /**
4349
+ * Mirror the production cache-key decision for a slot render, with no
4350
+ * cache IO — used by the dev wrapper so slot substitution happens in dev
4351
+ * exactly when production would serve the cache path rather than the
4352
+ * live fallback (dev/prod parity, codex P2 on PR #890): requires an ALS
4353
+ * store, keyable params (slot-param divergence unkeys), and keyable data
4354
+ * props (after the children slot outlet strip).
4355
+ */
4356
+ function isSlotRenderKeyable(dataProps) {
4357
+ const store = requestContextAls.getStore();
4358
+ if (!store) return false;
4359
+ const keyParams = resolveKeyParams(store);
4360
+ if (keyParams === null) return false;
4361
+ return computeComponentCacheKey(withoutChildrenOutlet(dataProps), keyParams) !== null;
4362
+ }
4363
+ /** Wrap payload bytes as a single-chunk ReadableStream for the decoder. */
4364
+ function bytesToStream(bytes) {
4365
+ return new ReadableStream({ start(controller) {
4366
+ controller.enqueue(bytes);
4367
+ controller.close();
4368
+ } });
4369
+ }
4370
+ /**
4371
+ * Serve a slot component from the cache: split live slot values out of the
4372
+ * props, look up / capture the shell (data props + a deterministic
4373
+ * SlotOutlet placeholder per slot), and wrap the revived shell in a
4374
+ * per-instance SlotsProvider that feeds the live values to the outlets.
4375
+ *
4376
+ * Null on any miss — the caller renders live with the original props.
4377
+ */
4378
+ async function tryReviveSlotShell(id, options, slots, props) {
4379
+ const render = prepareSlotRender(props, slots);
4380
+ if (render === null) return null;
4381
+ const { shellProps, dataProps, slotValues } = render;
4382
+ const revived = await tryRevivePayload(id, options, shellProps, dataProps);
4383
+ if (revived === null) return null;
4384
+ return { element: wrapInSlotsProvider(revived.element, slotValues) };
4385
+ }
4386
+ var rscFns = null;
4387
+ function requireRscFunctions() {
4388
+ if (!rscFns) throw new Error("[timber] RSC functions not wired — setRscFunctions() must be called at boot");
4389
+ return rscFns;
4390
+ }
4391
+ /**
4392
+ * Revive flight payload bytes into a React element.
4393
+ * Returns null on any failure (corrupt payload, missing decoder).
4394
+ */
4395
+ async function revivePayload(id, payload, outcome) {
4396
+ try {
4397
+ const { createFromReadableStream } = requireRscFunctions();
4398
+ return { element: await createFromReadableStream(bytesToStream(payload)) };
4399
+ } catch (error) {
4400
+ console.error(`[timber] failed to revive prebuilt payload for "${id}" — ${outcome}:`, error);
4401
+ return null;
4402
+ }
4403
+ }
4404
+ /** Ceiling on component re-render capture (design/13 A2). Uses configured renderTimeoutMs. */
4405
+ function getCaptureTimeoutMs() {
4406
+ return componentTimeoutMs;
4407
+ }
4408
+ /**
4409
+ * Render a component in isolation and capture its flight payload bytes.
4410
+ * Used for inline re-renders (tombstone/miss) and SWR background re-renders.
4411
+ *
4412
+ * The stream read is timeout-guarded and the reader is cancelled on
4413
+ * expiry (design/13 A2, A3).
4414
+ */
4415
+ async function captureComponentPayload(id, component, props, options) {
4416
+ const { renderToReadableStream } = requireRscFunctions();
4417
+ const element = createElement(component, props);
4418
+ const capture = createCaptureScope(id, options, props);
4419
+ let renderError;
4420
+ const store = requestContextAls.getStore();
4421
+ const discipline = store && shouldCheckKeyDiscipline(id) ? createKeyDisciplineProxy(store) : null;
4422
+ const doCapture = () => capture.run(async () => {
4423
+ const bytes = await readAllBytesWithDeadline(runOutsideReactCacheScope(() => renderToReadableStream(element, { onError(error) {
4424
+ renderError ??= { error };
4425
+ return "";
4426
+ } })), getCaptureTimeoutMs(), "[timber] component re-render capture");
4427
+ if (renderError) throw renderError.error;
4428
+ return bytes;
4429
+ });
4430
+ const bytes = discipline ? await requestContextAls.run(discipline.proxy, doCapture) : await doCapture();
4431
+ if (discipline && discipline.accessedApis.size > 0) emitKeyDisciplineWarning(id, discipline.accessedApis);
4432
+ return {
4433
+ bytes,
4434
+ nested: capture.nestedRenders()
4435
+ };
4436
+ }
4437
+ /**
4438
+ * Store a captured entry in the overlay, then say which embedded
4439
+ * components it clamps (design/45 §Nesting Semantics, TIM-1545). The
4440
+ * warning follows the store: a capture that is discarded (aborted,
4441
+ * invalidated mid-render, failed store) holds no clamped copy, and must
4442
+ * not spend the pair's one warning.
4443
+ */
4444
+ async function storeCapturedEntry(id, cacheKey, captured, options, tags, hasSeed) {
4445
+ await storeOverlayEntry(id, cacheKey, captured.bytes, {
4446
+ ttl: options.ttl,
4447
+ tags,
4448
+ hasSeed
4449
+ });
4450
+ warnNestingClamps({
4451
+ id,
4452
+ options,
4453
+ tags
4454
+ }, captured.nested, warnedNestingPairs, (m) => console.warn(m));
4455
+ }
4456
+ /**
4457
+ * Inline re-render: capture flight bytes, store in overlay, revive for
4458
+ * the current request. Singleflight-coalesced by overlay key.
4459
+ */
4460
+ async function inlineRerender(id, component, props, cacheKey, options, hasSeed, outcome) {
4461
+ const tags = resolveTags(options, props);
4462
+ const oKey = overlayKey(id, cacheKey);
4463
+ const sfKey = `prebuilt:${id}:${cacheKey}\0${lastInvalidationEpochFor(oKey, tags)}`;
4464
+ try {
4465
+ return await revivePayload(id, await componentSingleflight.do(sfKey, async (signal) => {
4466
+ const startEpoch = currentInvalidationEpoch();
4467
+ const captured = await captureComponentPayload(id, component, props, options);
4468
+ if (!signal.aborted && hasRuntimeLifetime(options) && !wasInvalidatedSince(startEpoch, oKey, tags)) await storeCapturedEntry(id, cacheKey, captured, options, tags, hasSeed);
4469
+ return captured.bytes;
4470
+ }), outcome);
4471
+ } catch (error) {
4472
+ console.error(`[timber] cache.component re-render failed for "${id}" — ${outcome}:`, error);
4473
+ return null;
4474
+ }
4475
+ }
4476
+ /**
4477
+ * Snapshot the request context values needed for a background re-render.
4478
+ * Creates a FRESH store with copied values — the original ALS store is
4479
+ * not referenced after capture. The `Request` itself is shared, not
4480
+ * copied: the re-render reports its failure against it.
4481
+ */
4482
+ function snapshotRequestContext(store) {
4483
+ return {
4484
+ request: store.request,
4485
+ headers: new Headers(store.headers),
4486
+ originalHeaders: new Headers(store.originalHeaders),
4487
+ cookieHeader: store.cookieHeader,
4488
+ parsedCookies: store.parsedCookies ? new Map(store.parsedCookies) : void 0,
4489
+ searchParams: new URLSearchParams(store.searchParams),
4490
+ searchString: store.searchString,
4491
+ segmentParams: store.segmentParams ? { ...store.segmentParams } : void 0,
4492
+ cookieJar: /* @__PURE__ */ new Map(),
4493
+ flushed: false,
4494
+ mutableContext: false
4495
+ };
4496
+ }
4497
+ /**
4498
+ * Kick off a background re-render via waitUntil. The re-render runs inside
4499
+ * a fresh ALS scope with snapshotted request context.
4500
+ */
4501
+ function backgroundRerender(id, component, props, cacheKey, options, hasSeed, snapshot) {
4502
+ const waitUntil = getWaitUntil();
4503
+ const tags = resolveTags(options, props);
4504
+ const oKey = overlayKey(id, cacheKey);
4505
+ const startEpoch = currentInvalidationEpoch();
4506
+ const sfKey = `prebuilt-bg:${id}:${cacheKey}\0${lastInvalidationEpochFor(oKey, tags)}`;
4507
+ const work = requestContextAls.run(snapshot, () => componentSingleflight.do(sfKey, (signal) => runBackgroundFlight(signal, async () => {
4508
+ const captured = await captureComponentPayload(id, component, props, options);
4509
+ if (!signal.aborted && !wasInvalidatedSince(startEpoch, oKey, tags)) await storeCapturedEntry(id, cacheKey, captured, options, tags, hasSeed);
4510
+ }, (error) => console.error(`[timber] cache.component background re-render failed for "${id}":`, error))).catch(() => {}));
4511
+ if (waitUntil) waitUntil(work);
4512
+ else if (!warnedNoWaitUntil) {
4513
+ warnedNoWaitUntil = true;
4514
+ console.warn("[timber] cache.component SWR background re-render has no waitUntil — the re-render may be killed when the handler returns on serverless platforms. Ensure the adapter wires runWithWaitUntil().");
4515
+ }
4516
+ }
4517
+ /**
4518
+ * Try to serve this render from the prebuilt cache: overlay → artifact →
4519
+ * live render. Handles ISR (SWR, tombstones, re-renders) per design/45.
4520
+ *
4521
+ * Null on any miss — the caller renders live.
4522
+ */
4523
+ async function tryRevivePayload(id, options, props, keyPropsOverride) {
4524
+ const hasSource = hasPrebuiltPayloadSource();
4525
+ const hasRuntime = hasRuntimeLifetime(options);
4526
+ if (!hasSource && !hasRuntime) return null;
4527
+ const store = requestContextAls.getStore();
4528
+ if (!store) return null;
4529
+ const keyParams = (options.prerender ? await isParamIndependent(id) : false) ? {} : resolveKeyParams(store);
4530
+ if (keyParams === null) return null;
4531
+ const cacheKey = computeComponentCacheKey(withoutChildrenOutlet(keyPropsOverride ?? props ?? {}), keyParams);
4532
+ if (cacheKey === null) return null;
4533
+ const outcome = options.fallbackOnMissingSegmentParams === false ? "failing the render (fallbackOnMissingSegmentParams: false)" : "rendering dynamically";
4534
+ const component = registry.get(id)?.component;
4535
+ if (hasRuntime) {
4536
+ const overlayResult = await lookupOverlay(id, cacheKey);
4537
+ if (overlayResult) {
4538
+ const overlayHasSeed = overlayResult.record?.s ?? false;
4539
+ if (overlayResult.kind === "tombstone") {
4540
+ if (component) return await inlineRerender(id, component, props, cacheKey, options, true, outcome);
4541
+ return null;
4542
+ }
4543
+ if (overlayResult.kind === "fresh" && overlayResult.payload) return await revivePayload(id, overlayResult.payload, outcome);
4544
+ if (overlayResult.kind === "stale" && overlayResult.payload) {
4545
+ if (options.staleWhileRevalidate && component) {
4546
+ backgroundRerender(id, component, props, cacheKey, options, overlayHasSeed, snapshotRequestContext(store));
4547
+ return await revivePayload(id, overlayResult.payload, outcome);
4548
+ }
4549
+ if (component) return await inlineRerender(id, component, props, cacheKey, options, overlayHasSeed, outcome);
4550
+ return null;
4551
+ }
4552
+ }
4553
+ }
4554
+ if (hasSource) {
4555
+ const artifactPayload = await lookupPrebuiltPayload(id, cacheKey);
4556
+ if (artifactPayload !== null) {
4557
+ if (options.ttl !== void 0) {
4558
+ const buildTs = (await getManifestEntry(id, cacheKey))?.buildTimestamp ?? 0;
4559
+ if ((Date.now() - buildTs) / 1e3 > options.ttl) {
4560
+ if (options.staleWhileRevalidate && component) {
4561
+ backgroundRerender(id, component, props, cacheKey, options, true, snapshotRequestContext(store));
4562
+ return await revivePayload(id, artifactPayload, outcome);
4563
+ }
4564
+ if (component) return await inlineRerender(id, component, props, cacheKey, options, true, outcome);
4565
+ return null;
4566
+ }
4567
+ }
4568
+ return await revivePayload(id, artifactPayload, outcome);
4569
+ }
4570
+ if (options.prerender) console.warn(`[timber] cache.component cache miss: "${id}" with params ${JSON.stringify(store.segmentParams ?? {})} — ${outcome}`);
4571
+ }
4572
+ if (hasRuntime && component && options.fallbackOnMissingSegmentParams !== false) return await inlineRerender(id, component, props, cacheKey, options, false, outcome);
4573
+ return null;
4574
+ }
4575
+ /**
4576
+ * Production wrapper injected by the timber-prebuilt transform. Registers
4577
+ * the component under its stable ID and returns a wrapper component that
4578
+ * either revives the cached payload (hit) or renders the original
4579
+ * component live (miss).
4580
+ */
4581
+ function __prebuilt(id, component, options = {}) {
4582
+ const resolution = resolveSlotNames(id, options);
4583
+ const slots = resolution.kind === "active" ? resolution.slots : null;
4584
+ if (resolution.kind === "active" && options.prerender) {
4585
+ console.warn(`[timber] cache.component "${id}": slots are runtime-tier only — prerender is ignored (the build cannot enumerate slot content). Use ttl/tags for the cache lifetime.`);
4586
+ options = {
4587
+ ...options,
4588
+ prerender: void 0
4589
+ };
4590
+ }
4591
+ if (resolution.kind === "disabled") options = {
4592
+ ...options,
4593
+ slots: void 0
4594
+ };
4595
+ registry.set(id, {
4596
+ id,
4597
+ component,
4598
+ options
4599
+ });
4600
+ const wrapper = async (props) => {
4601
+ noteWrapperRender(id, options, props);
4602
+ if (isCaptureActive()) return createElement(component, props);
4603
+ const revived = slots ? await tryReviveSlotShell(id, options, slots, props) : await tryRevivePayload(id, options, props);
4604
+ if (revived !== null) return revived.element;
4605
+ if (options.fallbackOnMissingSegmentParams === false) throw new Error(`[timber] cache.component "${id}": no prebuilt payload for the current params and fallbackOnMissingSegmentParams is false`);
4606
+ return createElement(component, props);
4607
+ };
4608
+ Object.defineProperty(wrapper, "name", { value: `Prebuilt(${id})` });
4609
+ return wrapper;
4610
+ }
4611
+ /** Look up a registration by stable component ID. */
4612
+ function getPrebuiltRegistration(id) {
4613
+ return registry.get(id);
4614
+ }
4615
+ /** All registrations, for the post-build capture pass (TIM-1119). */
4616
+ function getPrebuiltRegistry() {
4617
+ return registry;
4618
+ }
4619
+ /**
4620
+ * Dev-mode wrapper injected by the timber-prebuilt transform: a no-op that
4621
+ * renders the component dynamically on every request, logging once per
4622
+ * component on first render so the production behavior difference is visible.
4623
+ * See design/44 §Dev Mode.
4624
+ */
4625
+ function __prebuiltDev(name, component, options = {}) {
4626
+ const resolution = resolveSlotNames(name, options);
4627
+ const slots = resolution.kind === "active" ? resolution.slots : null;
4628
+ let logged = false;
4629
+ const wrapper = (props) => {
4630
+ if (!logged) {
4631
+ logged = true;
4632
+ console.log(`[timber] cache.component: "${name}" — rendering dynamically (dev mode)`);
4633
+ }
4634
+ if (slots) {
4635
+ const render = prepareSlotRender(props, slots);
4636
+ if (render !== null && isSlotRenderKeyable(render.dataProps)) return wrapInSlotsProvider(createElement(component, render.shellProps), render.slotValues);
4637
+ }
4638
+ return createElement(component, props);
4639
+ };
4640
+ return wrapper;
4641
+ }
4642
+ //#endregion
4643
+ export { AccessGate, DEFAULT_LIMITS, DenySignal, METADATA_ROUTE_CONVENTIONS, RedirectSignal, RenderTimeoutError, SlotAccessGate, __prebuilt, __prebuiltDev, callOnRequestError, canonicalize, classifyMetadataRoute, coerce, collectEarlyHintHeaders, computeComponentCacheKey, createPipeline, enforceBodyLimits, executeAction, formatLinkHeader, generateTraceId, getCookie, getHeader, getLogger, getMetadataRouteAutoLink, getMetadataRouteServePath, getPrebuiltRegistration, getPrebuiltRegistry, getSearchParams, getSegmentParams, getSetCookieHeaders, handleRouteRequest, hasOnRequestError, hasPrebuiltPayloadSource, loadInstrumentation, logCacheMiss, logMiddlewareError, logMiddlewareShortCircuit, logProxyError, logRenderError, logRequestCompleted, logRequestReceived, logSlowRequest, logSwrRefetchFailed, logWaitUntilRejected, logWaitUntilUnsupported, markResponseFlushed, parseBodySize, renderMetadataToElements, replaceTraceId, resolveAllowedMethods, resolveMetadata, resolveMetadataUrls, resolveSlotDenied, resolveStatusFile, resolveTitle, runMiddleware, runProxy, runWithEarlyHintsSender, runWithRequestContext, runWithTraceId, sendEarlyHints103, setLogger, setMutableCookieContext, setPrebuiltPayloadSource, setSegmentParams };
4644
+
4645
+ //# sourceMappingURL=internal.js.map