@timber-js/app 0.2.0-alpha.17 → 0.2.0-alpha.170

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 (1026) hide show
  1. package/LICENSE +8 -0
  2. package/agent-skill.md +344 -0
  3. package/dist/_chunks/actions-O_LsyCE4.js +317 -0
  4. package/dist/_chunks/actions-O_LsyCE4.js.map +1 -0
  5. package/dist/_chunks/build-output-helper-DF5uScqS.js +158 -0
  6. package/dist/_chunks/build-output-helper-DF5uScqS.js.map +1 -0
  7. package/dist/_chunks/cache-api-B-lhk9p4.js +1202 -0
  8. package/dist/_chunks/cache-api-B-lhk9p4.js.map +1 -0
  9. package/dist/_chunks/canonicalize-Du3o_ptW.js +247 -0
  10. package/dist/_chunks/canonicalize-Du3o_ptW.js.map +1 -0
  11. package/dist/_chunks/cli-schema-sync-B5FDplGI.js +688 -0
  12. package/dist/_chunks/cli-schema-sync-B5FDplGI.js.map +1 -0
  13. package/dist/_chunks/cloudflare-AHoWYTYr.js +1188 -0
  14. package/dist/_chunks/cloudflare-AHoWYTYr.js.map +1 -0
  15. package/dist/_chunks/codec-registry-oOUxugz3.js +21 -0
  16. package/dist/_chunks/codec-registry-oOUxugz3.js.map +1 -0
  17. package/dist/_chunks/define-COtkxMRT.js +191 -0
  18. package/dist/_chunks/define-COtkxMRT.js.map +1 -0
  19. package/dist/_chunks/define-c4au4I9R.js +82 -0
  20. package/dist/_chunks/define-c4au4I9R.js.map +1 -0
  21. package/dist/_chunks/dist-BA3u1z3W.js +423 -0
  22. package/dist/_chunks/dist-BA3u1z3W.js.map +1 -0
  23. package/dist/_chunks/fast-hash-D6hIVt1Y.js +36 -0
  24. package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +1 -0
  25. package/dist/_chunks/format-CfwjgPz9.js +14 -0
  26. package/dist/_chunks/format-CfwjgPz9.js.map +1 -0
  27. package/dist/_chunks/href-validation-CMc5JRls.js +141 -0
  28. package/dist/_chunks/href-validation-CMc5JRls.js.map +1 -0
  29. package/dist/_chunks/json-lossy-check-ClNvBM_3.js +63 -0
  30. package/dist/_chunks/json-lossy-check-ClNvBM_3.js.map +1 -0
  31. package/dist/_chunks/logger-AWfuX-KJ.js +1468 -0
  32. package/dist/_chunks/logger-AWfuX-KJ.js.map +1 -0
  33. package/dist/_chunks/navigation-root-bUDZLw74.js +124 -0
  34. package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -0
  35. package/dist/_chunks/plugin-context---kTF5v8.js +315 -0
  36. package/dist/_chunks/plugin-context---kTF5v8.js.map +1 -0
  37. package/dist/_chunks/registry-DbJPKoBp.js +20 -0
  38. package/dist/_chunks/registry-DbJPKoBp.js.map +1 -0
  39. package/dist/_chunks/resolve-schema-Dz3fcFUo.js +50 -0
  40. package/dist/_chunks/resolve-schema-Dz3fcFUo.js.map +1 -0
  41. package/dist/_chunks/router-ref-CtmF-aPv.js +28 -0
  42. package/dist/_chunks/router-ref-CtmF-aPv.js.map +1 -0
  43. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js +119 -0
  44. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js.map +1 -0
  45. package/dist/_chunks/segment-classify-Byy425ng.js +176 -0
  46. package/dist/_chunks/segment-classify-Byy425ng.js.map +1 -0
  47. package/dist/_chunks/segment-context-ZDnXDkbz.js +34 -0
  48. package/dist/_chunks/segment-context-ZDnXDkbz.js.map +1 -0
  49. package/dist/_chunks/sizeof-lXffQ1Vr.js +35 -0
  50. package/dist/_chunks/sizeof-lXffQ1Vr.js.map +1 -0
  51. package/dist/_chunks/{ssr-data-MjmprTmO.js → ssr-data-BOWsq18U.js} +1 -1
  52. package/dist/_chunks/ssr-data-BOWsq18U.js.map +1 -0
  53. package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-DFvWd-EA.js} +71 -26
  54. package/dist/_chunks/use-query-states-DFvWd-EA.js.map +1 -0
  55. package/dist/_chunks/use-segment-params-D1CzlgBo.js +151 -0
  56. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +1 -0
  57. package/dist/_chunks/walkers-DCoE-LJf.js +1002 -0
  58. package/dist/_chunks/walkers-DCoE-LJf.js.map +1 -0
  59. package/dist/adapters/build-output-helper.d.ts +29 -0
  60. package/dist/adapters/build-output-helper.d.ts.map +1 -0
  61. package/dist/adapters/cloudflare-dev.d.ts +109 -0
  62. package/dist/adapters/cloudflare-dev.d.ts.map +1 -0
  63. package/dist/adapters/cloudflare-dev.js +73 -0
  64. package/dist/adapters/cloudflare-dev.js.map +1 -0
  65. package/dist/adapters/cloudflare-kv-cache.d.ts +65 -0
  66. package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -0
  67. package/dist/adapters/cloudflare-kv-cache.js +124 -0
  68. package/dist/adapters/cloudflare-kv-cache.js.map +1 -0
  69. package/dist/adapters/cloudflare.d.ts +189 -21
  70. package/dist/adapters/cloudflare.d.ts.map +1 -1
  71. package/dist/adapters/cloudflare.js +2 -188
  72. package/dist/adapters/compress-module.d.ts +5 -4
  73. package/dist/adapters/compress-module.d.ts.map +1 -1
  74. package/dist/adapters/nitro.d.ts +41 -3
  75. package/dist/adapters/nitro.d.ts.map +1 -1
  76. package/dist/adapters/nitro.js +224 -172
  77. package/dist/adapters/nitro.js.map +1 -1
  78. package/dist/adapters/shared.d.ts +6 -0
  79. package/dist/adapters/shared.d.ts.map +1 -0
  80. package/dist/adapters/types.d.ts +2 -6
  81. package/dist/adapters/types.d.ts.map +1 -1
  82. package/dist/cache/cache-api.d.ts +48 -0
  83. package/dist/cache/cache-api.d.ts.map +1 -0
  84. package/dist/cache/handler-store.d.ts +33 -0
  85. package/dist/cache/handler-store.d.ts.map +1 -0
  86. package/dist/cache/index.d.ts +31 -8
  87. package/dist/cache/index.d.ts.map +1 -1
  88. package/dist/cache/index.js +3 -326
  89. package/dist/cache/invalidation-epoch.d.ts +43 -0
  90. package/dist/cache/invalidation-epoch.d.ts.map +1 -0
  91. package/dist/cache/json-lossy-check.d.ts +11 -0
  92. package/dist/cache/json-lossy-check.d.ts.map +1 -0
  93. package/dist/cache/redis-handler.d.ts +140 -8
  94. package/dist/cache/redis-handler.d.ts.map +1 -1
  95. package/dist/cache/singleflight.d.ts +19 -2
  96. package/dist/cache/singleflight.d.ts.map +1 -1
  97. package/dist/cache/sizeof.d.ts +22 -0
  98. package/dist/cache/sizeof.d.ts.map +1 -0
  99. package/dist/cache/stable-stringify.d.ts +9 -0
  100. package/dist/cache/stable-stringify.d.ts.map +1 -1
  101. package/dist/cache/store.d.ts +53 -0
  102. package/dist/cache/store.d.ts.map +1 -0
  103. package/dist/cache/stores/cloudflare-kv.d.ts +38 -0
  104. package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -0
  105. package/dist/cache/stores/cloudflare-kv.js +29 -0
  106. package/dist/cache/stores/cloudflare-kv.js.map +1 -0
  107. package/dist/cache/stores/memory.d.ts +42 -0
  108. package/dist/cache/stores/memory.d.ts.map +1 -0
  109. package/dist/cache/stores/memory.js +90 -0
  110. package/dist/cache/stores/memory.js.map +1 -0
  111. package/dist/cache/stores/redis.d.ts +69 -0
  112. package/dist/cache/stores/redis.d.ts.map +1 -0
  113. package/dist/cache/stores/redis.js +35 -0
  114. package/dist/cache/stores/redis.js.map +1 -0
  115. package/dist/cache/stores/vercel.d.ts +36 -0
  116. package/dist/cache/stores/vercel.d.ts.map +1 -0
  117. package/dist/cache/stores/vercel.js +33 -0
  118. package/dist/cache/stores/vercel.js.map +1 -0
  119. package/dist/cache/tag-aware-handler.d.ts +52 -0
  120. package/dist/cache/tag-aware-handler.d.ts.map +1 -0
  121. package/dist/cache/timber-cache.d.ts +20 -10
  122. package/dist/cache/timber-cache.d.ts.map +1 -1
  123. package/dist/cdn/cloudflare-purge.d.ts +23 -0
  124. package/dist/cdn/cloudflare-purge.d.ts.map +1 -0
  125. package/dist/cdn/fastly-purge.d.ts +25 -0
  126. package/dist/cdn/fastly-purge.d.ts.map +1 -0
  127. package/dist/cdn/headers.d.ts +97 -0
  128. package/dist/cdn/headers.d.ts.map +1 -0
  129. package/dist/cdn/index.d.ts +5 -0
  130. package/dist/cdn/index.d.ts.map +1 -0
  131. package/dist/cdn/purge-store.d.ts +23 -0
  132. package/dist/cdn/purge-store.d.ts.map +1 -0
  133. package/dist/cdn/workers-cache-purge.d.ts +16 -0
  134. package/dist/cdn/workers-cache-purge.d.ts.map +1 -0
  135. package/dist/cli-schema-sync.d.ts +59 -0
  136. package/dist/cli-schema-sync.d.ts.map +1 -0
  137. package/dist/cli.d.ts +34 -2
  138. package/dist/cli.d.ts.map +1 -1
  139. package/dist/cli.js +109 -34
  140. package/dist/cli.js.map +1 -1
  141. package/dist/client/browser-dev.d.ts +37 -1
  142. package/dist/client/browser-dev.d.ts.map +1 -1
  143. package/dist/client/browser-entry/action-dispatch.d.ts +17 -0
  144. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -0
  145. package/dist/client/browser-entry/hmr.d.ts +21 -0
  146. package/dist/client/browser-entry/hmr.d.ts.map +1 -0
  147. package/dist/client/browser-entry/hydrate.d.ts +46 -0
  148. package/dist/client/browser-entry/hydrate.d.ts.map +1 -0
  149. package/dist/client/browser-entry/index.d.ts +29 -0
  150. package/dist/client/browser-entry/index.d.ts.map +1 -0
  151. package/dist/client/browser-entry/post-hydration.d.ts +26 -0
  152. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -0
  153. package/dist/client/browser-entry/router-init.d.ts +27 -0
  154. package/dist/client/browser-entry/router-init.d.ts.map +1 -0
  155. package/dist/client/browser-entry/rsc-stream.d.ts +24 -0
  156. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -0
  157. package/dist/client/browser-entry/scroll.d.ts +28 -0
  158. package/dist/client/browser-entry/scroll.d.ts.map +1 -0
  159. package/dist/client/child-segment-context.d.ts +22 -0
  160. package/dist/client/child-segment-context.d.ts.map +1 -0
  161. package/dist/client/child-segment-outlet.d.ts +18 -0
  162. package/dist/client/child-segment-outlet.d.ts.map +1 -0
  163. package/dist/client/child-segment-provider.d.ts +21 -0
  164. package/dist/client/child-segment-provider.d.ts.map +1 -0
  165. package/dist/client/error-boundary.d.ts +12 -5
  166. package/dist/client/error-boundary.d.ts.map +1 -1
  167. package/dist/client/error-boundary.js +47 -10
  168. package/dist/client/error-boundary.js.map +1 -1
  169. package/dist/client/error-reconstituter.d.ts +61 -0
  170. package/dist/client/error-reconstituter.d.ts.map +1 -0
  171. package/dist/client/form.d.ts +16 -27
  172. package/dist/client/form.d.ts.map +1 -1
  173. package/dist/client/history.d.ts +21 -6
  174. package/dist/client/history.d.ts.map +1 -1
  175. package/dist/client/index.d.ts +10 -24
  176. package/dist/client/index.d.ts.map +1 -1
  177. package/dist/client/index.js +185 -1134
  178. package/dist/client/index.js.map +1 -1
  179. package/dist/client/internal.d.ts +19 -0
  180. package/dist/client/internal.d.ts.map +1 -0
  181. package/dist/client/internal.js +849 -0
  182. package/dist/client/internal.js.map +1 -0
  183. package/dist/client/link.d.ts +69 -60
  184. package/dist/client/link.d.ts.map +1 -1
  185. package/dist/client/navigation-api.d.ts +120 -0
  186. package/dist/client/navigation-api.d.ts.map +1 -0
  187. package/dist/client/navigation-context.d.ts +3 -2
  188. package/dist/client/navigation-context.d.ts.map +1 -1
  189. package/dist/client/navigation-root.d.ts +119 -0
  190. package/dist/client/navigation-root.d.ts.map +1 -0
  191. package/dist/client/nuqs-adapter.d.ts +1 -1
  192. package/dist/client/nuqs-adapter.d.ts.map +1 -1
  193. package/dist/client/router-ref.d.ts +1 -1
  194. package/dist/client/router.d.ts +93 -14
  195. package/dist/client/router.d.ts.map +1 -1
  196. package/dist/client/rsc-fetch.d.ts +47 -14
  197. package/dist/client/rsc-fetch.d.ts.map +1 -1
  198. package/dist/client/segment-cache.d.ts +4 -4
  199. package/dist/client/segment-cache.d.ts.map +1 -1
  200. package/dist/client/segment-outlet.d.ts +49 -0
  201. package/dist/client/segment-outlet.d.ts.map +1 -0
  202. package/dist/client/segment-update-context.d.ts +21 -0
  203. package/dist/client/segment-update-context.d.ts.map +1 -0
  204. package/dist/client/shallow-url.d.ts +10 -0
  205. package/dist/client/shallow-url.d.ts.map +1 -0
  206. package/dist/client/slot-context.d.ts +29 -0
  207. package/dist/client/slot-context.d.ts.map +1 -0
  208. package/dist/client/slot-outlet.d.ts +16 -0
  209. package/dist/client/slot-outlet.d.ts.map +1 -0
  210. package/dist/client/slot-provider.d.ts +20 -0
  211. package/dist/client/slot-provider.d.ts.map +1 -0
  212. package/dist/client/ssr-data.d.ts +13 -4
  213. package/dist/client/ssr-data.d.ts.map +1 -1
  214. package/dist/client/state.d.ts +1 -1
  215. package/dist/client/top-loader.d.ts +5 -5
  216. package/dist/client/top-loader.d.ts.map +1 -1
  217. package/dist/client/use-cookie.d.ts +12 -0
  218. package/dist/client/use-cookie.d.ts.map +1 -1
  219. package/dist/client/use-link-status.d.ts +5 -5
  220. package/dist/client/use-link-status.d.ts.map +1 -1
  221. package/dist/client/use-pathname.d.ts.map +1 -1
  222. package/dist/client/use-pending-navigation.d.ts +23 -0
  223. package/dist/client/use-pending-navigation.d.ts.map +1 -0
  224. package/dist/client/use-query-states.d.ts +1 -1
  225. package/dist/client/use-query-states.d.ts.map +1 -1
  226. package/dist/client/use-router.d.ts +1 -1
  227. package/dist/client/use-router.d.ts.map +1 -1
  228. package/dist/client/use-search-params.d.ts +12 -5
  229. package/dist/client/use-search-params.d.ts.map +1 -1
  230. package/dist/client/{use-params.d.ts → use-segment-params.d.ts} +8 -6
  231. package/dist/client/use-segment-params.d.ts.map +1 -0
  232. package/dist/codec.d.ts +69 -0
  233. package/dist/codec.d.ts.map +1 -0
  234. package/dist/codec.js +76 -0
  235. package/dist/codec.js.map +1 -0
  236. package/dist/config-types.d.ts +284 -0
  237. package/dist/config-types.d.ts.map +1 -0
  238. package/dist/config-validation.d.ts +51 -0
  239. package/dist/config-validation.d.ts.map +1 -0
  240. package/dist/content/index.d.ts +1 -10
  241. package/dist/content/index.d.ts.map +1 -1
  242. package/dist/content/index.js +0 -2
  243. package/dist/cookies/define-cookie.d.ts +58 -25
  244. package/dist/cookies/define-cookie.d.ts.map +1 -1
  245. package/dist/cookies/index.d.ts +2 -1
  246. package/dist/cookies/index.d.ts.map +1 -1
  247. package/dist/cookies/index.js +257 -36
  248. package/dist/cookies/index.js.map +1 -1
  249. package/dist/cookies/json-cookie.d.ts +64 -0
  250. package/dist/cookies/json-cookie.d.ts.map +1 -0
  251. package/dist/cookies/validation.d.ts +34 -0
  252. package/dist/cookies/validation.d.ts.map +1 -0
  253. package/dist/dev-tools/404-page.d.ts +50 -0
  254. package/dist/dev-tools/404-page.d.ts.map +1 -0
  255. package/dist/dev-tools/browser-logs.d.ts +40 -0
  256. package/dist/dev-tools/browser-logs.d.ts.map +1 -0
  257. package/dist/dev-tools/dev-page-shell.d.ts +78 -0
  258. package/dist/dev-tools/dev-page-shell.d.ts.map +1 -0
  259. package/dist/dev-tools/error-page.d.ts +32 -0
  260. package/dist/dev-tools/error-page.d.ts.map +1 -0
  261. package/dist/dev-tools/holding-server.d.ts +54 -0
  262. package/dist/dev-tools/holding-server.d.ts.map +1 -0
  263. package/dist/dev-tools/hydration-overlay.d.ts +11 -0
  264. package/dist/dev-tools/hydration-overlay.d.ts.map +1 -0
  265. package/dist/dev-tools/index.d.ts +31 -0
  266. package/dist/dev-tools/index.d.ts.map +1 -0
  267. package/dist/dev-tools/instrumentation.d.ts +59 -0
  268. package/dist/dev-tools/instrumentation.d.ts.map +1 -0
  269. package/dist/{server/dev-logger.d.ts → dev-tools/logger.d.ts} +1 -1
  270. package/dist/dev-tools/logger.d.ts.map +1 -0
  271. package/dist/{plugins/dev-logs.d.ts → dev-tools/logs.d.ts} +17 -3
  272. package/dist/dev-tools/logs.d.ts.map +1 -0
  273. package/dist/dev-tools/overlay.d.ts +91 -0
  274. package/dist/dev-tools/overlay.d.ts.map +1 -0
  275. package/dist/dev-tools/stack-classifier.d.ts +34 -0
  276. package/dist/dev-tools/stack-classifier.d.ts.map +1 -0
  277. package/dist/dev-tools/terminal.d.ts +28 -0
  278. package/dist/dev-tools/terminal.d.ts.map +1 -0
  279. package/dist/fonts/ast-walk.d.ts +68 -0
  280. package/dist/fonts/ast-walk.d.ts.map +1 -0
  281. package/dist/fonts/ast.d.ts +110 -6
  282. package/dist/fonts/ast.d.ts.map +1 -1
  283. package/dist/fonts/bundle.d.ts +50 -0
  284. package/dist/fonts/bundle.d.ts.map +1 -0
  285. package/dist/fonts/dev-middleware.d.ts +22 -0
  286. package/dist/fonts/dev-middleware.d.ts.map +1 -0
  287. package/dist/fonts/fallbacks.d.ts +0 -7
  288. package/dist/fonts/fallbacks.d.ts.map +1 -1
  289. package/dist/fonts/google.d.ts +4 -2
  290. package/dist/fonts/google.d.ts.map +1 -1
  291. package/dist/fonts/local.d.ts +4 -4
  292. package/dist/fonts/local.d.ts.map +1 -1
  293. package/dist/fonts/paths.d.ts +43 -0
  294. package/dist/fonts/paths.d.ts.map +1 -0
  295. package/dist/fonts/pipeline.d.ts +177 -0
  296. package/dist/fonts/pipeline.d.ts.map +1 -0
  297. package/dist/fonts/transform.d.ts +82 -0
  298. package/dist/fonts/transform.d.ts.map +1 -0
  299. package/dist/fonts/types.d.ts +47 -1
  300. package/dist/fonts/types.d.ts.map +1 -1
  301. package/dist/fonts/virtual-modules.d.ts +77 -0
  302. package/dist/fonts/virtual-modules.d.ts.map +1 -0
  303. package/dist/index.d.ts +46 -180
  304. package/dist/index.d.ts.map +1 -1
  305. package/dist/index.js +8506 -13599
  306. package/dist/index.js.map +1 -1
  307. package/dist/params/codec-registry.d.ts +26 -0
  308. package/dist/params/codec-registry.d.ts.map +1 -0
  309. package/dist/params/define-schema.d.ts +81 -0
  310. package/dist/params/define-schema.d.ts.map +1 -0
  311. package/dist/params/index.d.ts +5 -0
  312. package/dist/params/index.d.ts.map +1 -0
  313. package/dist/params/index.js +34 -0
  314. package/dist/params/index.js.map +1 -0
  315. package/dist/params/resolve-schema.d.ts +34 -0
  316. package/dist/params/resolve-schema.d.ts.map +1 -0
  317. package/dist/plugin-context.d.ts +217 -0
  318. package/dist/plugin-context.d.ts.map +1 -0
  319. package/dist/plugins/adapter-build.d.ts +21 -1
  320. package/dist/plugins/adapter-build.d.ts.map +1 -1
  321. package/dist/plugins/build-manifest.d.ts +7 -3
  322. package/dist/plugins/build-manifest.d.ts.map +1 -1
  323. package/dist/plugins/build-report.d.ts +43 -7
  324. package/dist/plugins/build-report.d.ts.map +1 -1
  325. package/dist/plugins/cache.d.ts +64 -0
  326. package/dist/plugins/cache.d.ts.map +1 -0
  327. package/dist/plugins/callsite-ast.d.ts +117 -0
  328. package/dist/plugins/callsite-ast.d.ts.map +1 -0
  329. package/dist/plugins/callsite-jsx.d.ts +35 -0
  330. package/dist/plugins/callsite-jsx.d.ts.map +1 -0
  331. package/dist/plugins/chunks.d.ts +11 -4
  332. package/dist/plugins/chunks.d.ts.map +1 -1
  333. package/dist/plugins/client-chunks.d.ts +40 -0
  334. package/dist/plugins/client-chunks.d.ts.map +1 -0
  335. package/dist/plugins/cloudflare-wasm.d.ts +21 -0
  336. package/dist/plugins/cloudflare-wasm.d.ts.map +1 -0
  337. package/dist/plugins/content.d.ts +1 -1
  338. package/dist/plugins/content.d.ts.map +1 -1
  339. package/dist/plugins/dev-server.d.ts +17 -1
  340. package/dist/plugins/dev-server.d.ts.map +1 -1
  341. package/dist/plugins/entries.d.ts +28 -4
  342. package/dist/plugins/entries.d.ts.map +1 -1
  343. package/dist/plugins/fonts.d.ts +16 -73
  344. package/dist/plugins/fonts.d.ts.map +1 -1
  345. package/dist/plugins/mdx.d.ts +1 -1
  346. package/dist/plugins/mdx.d.ts.map +1 -1
  347. package/dist/plugins/prebuilt-capture.d.ts +60 -0
  348. package/dist/plugins/prebuilt-capture.d.ts.map +1 -0
  349. package/dist/plugins/prebuilt-options-analysis.d.ts +40 -0
  350. package/dist/plugins/prebuilt-options-analysis.d.ts.map +1 -0
  351. package/dist/plugins/prebuilt.d.ts +82 -0
  352. package/dist/plugins/prebuilt.d.ts.map +1 -0
  353. package/dist/plugins/prerender-sugar.d.ts +42 -0
  354. package/dist/plugins/prerender-sugar.d.ts.map +1 -0
  355. package/dist/plugins/routing.d.ts +1 -1
  356. package/dist/plugins/routing.d.ts.map +1 -1
  357. package/dist/plugins/server-bundle.d.ts.map +1 -1
  358. package/dist/plugins/shims.d.ts +29 -5
  359. package/dist/plugins/shims.d.ts.map +1 -1
  360. package/dist/plugins/static-build.d.ts +5 -26
  361. package/dist/plugins/static-build.d.ts.map +1 -1
  362. package/dist/routing/codegen-shared.d.ts +67 -0
  363. package/dist/routing/codegen-shared.d.ts.map +1 -0
  364. package/dist/routing/codegen-types.d.ts +64 -0
  365. package/dist/routing/codegen-types.d.ts.map +1 -0
  366. package/dist/routing/codegen.d.ts +7 -2
  367. package/dist/routing/codegen.d.ts.map +1 -1
  368. package/dist/routing/convention-lint.d.ts +41 -0
  369. package/dist/routing/convention-lint.d.ts.map +1 -0
  370. package/dist/routing/export-detect.d.ts +56 -0
  371. package/dist/routing/export-detect.d.ts.map +1 -0
  372. package/dist/routing/file-cache.d.ts +8 -0
  373. package/dist/routing/file-cache.d.ts.map +1 -0
  374. package/dist/routing/index.d.ts +7 -1
  375. package/dist/routing/index.d.ts.map +1 -1
  376. package/dist/routing/index.js +4 -2
  377. package/dist/routing/interception.d.ts +0 -5
  378. package/dist/routing/interception.d.ts.map +1 -1
  379. package/dist/routing/link-codegen.d.ts +90 -0
  380. package/dist/routing/link-codegen.d.ts.map +1 -0
  381. package/dist/routing/manifest-codegen.d.ts +32 -0
  382. package/dist/routing/manifest-codegen.d.ts.map +1 -0
  383. package/dist/routing/scanner.d.ts +1 -10
  384. package/dist/routing/scanner.d.ts.map +1 -1
  385. package/dist/routing/schema-validation.d.ts +35 -0
  386. package/dist/routing/schema-validation.d.ts.map +1 -0
  387. package/dist/routing/segment-classify.d.ts +81 -0
  388. package/dist/routing/segment-classify.d.ts.map +1 -0
  389. package/dist/routing/segment-codegen.d.ts +38 -0
  390. package/dist/routing/segment-codegen.d.ts.map +1 -0
  391. package/dist/routing/types.d.ts +81 -25
  392. package/dist/routing/types.d.ts.map +1 -1
  393. package/dist/routing/walkers.d.ts +51 -0
  394. package/dist/routing/walkers.d.ts.map +1 -0
  395. package/dist/rsc-runtime/rsc.d.ts +1 -1
  396. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  397. package/dist/rsc-runtime/ssr.d.ts +12 -0
  398. package/dist/rsc-runtime/ssr.d.ts.map +1 -1
  399. package/dist/schema-bridge.d.ts +91 -0
  400. package/dist/schema-bridge.d.ts.map +1 -0
  401. package/dist/search-params/define.d.ts +188 -0
  402. package/dist/search-params/define.d.ts.map +1 -0
  403. package/dist/search-params/index.d.ts +4 -7
  404. package/dist/search-params/index.d.ts.map +1 -1
  405. package/dist/search-params/index.js +34 -441
  406. package/dist/search-params/index.js.map +1 -1
  407. package/dist/search-params/registry.d.ts +2 -2
  408. package/dist/search-params/registry.d.ts.map +1 -1
  409. package/dist/search-params/wrappers.d.ts +53 -0
  410. package/dist/search-params/wrappers.d.ts.map +1 -0
  411. package/dist/segment-params/define.d.ts +89 -0
  412. package/dist/segment-params/define.d.ts.map +1 -0
  413. package/dist/segment-params/index.d.ts +3 -0
  414. package/dist/segment-params/index.d.ts.map +1 -0
  415. package/dist/segment-params/index.js +2 -0
  416. package/dist/server/access-gate.d.ts +8 -3
  417. package/dist/server/access-gate.d.ts.map +1 -1
  418. package/dist/server/action-client.d.ts +42 -7
  419. package/dist/server/action-client.d.ts.map +1 -1
  420. package/dist/server/action-encryption.d.ts +76 -0
  421. package/dist/server/action-encryption.d.ts.map +1 -0
  422. package/dist/server/action-handler.d.ts +35 -1
  423. package/dist/server/action-handler.d.ts.map +1 -1
  424. package/dist/server/actions.d.ts +6 -30
  425. package/dist/server/actions.d.ts.map +1 -1
  426. package/dist/server/als-registry.d.ts +93 -11
  427. package/dist/server/als-registry.d.ts.map +1 -1
  428. package/dist/server/asset-headers.d.ts +8 -18
  429. package/dist/server/asset-headers.d.ts.map +1 -1
  430. package/dist/server/body-limits.d.ts.map +1 -1
  431. package/dist/server/build-manifest.d.ts +6 -29
  432. package/dist/server/build-manifest.d.ts.map +1 -1
  433. package/dist/server/canonicalize.d.ts +10 -0
  434. package/dist/server/canonicalize.d.ts.map +1 -1
  435. package/dist/server/coalesce-tick.d.ts +40 -0
  436. package/dist/server/coalesce-tick.d.ts.map +1 -0
  437. package/dist/server/cookie-context.d.ts +170 -0
  438. package/dist/server/cookie-context.d.ts.map +1 -0
  439. package/dist/server/cookie-parsing.d.ts +65 -0
  440. package/dist/server/cookie-parsing.d.ts.map +1 -0
  441. package/dist/server/csrf.d.ts +3 -2
  442. package/dist/server/csrf.d.ts.map +1 -1
  443. package/dist/server/default-logger.d.ts +22 -0
  444. package/dist/server/default-logger.d.ts.map +1 -0
  445. package/dist/server/deny-boundary.d.ts +117 -0
  446. package/dist/server/deny-boundary.d.ts.map +1 -0
  447. package/dist/server/deny-renderer.d.ts +2 -2
  448. package/dist/server/deny-renderer.d.ts.map +1 -1
  449. package/dist/server/dev-source-map.d.ts +22 -0
  450. package/dist/server/dev-source-map.d.ts.map +1 -0
  451. package/dist/server/early-hints-sender.d.ts.map +1 -1
  452. package/dist/server/early-hints.d.ts +13 -11
  453. package/dist/server/early-hints.d.ts.map +1 -1
  454. package/dist/server/error-boundary-wrapper.d.ts +7 -1
  455. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  456. package/dist/server/fallback-error.d.ts +12 -7
  457. package/dist/server/fallback-error.d.ts.map +1 -1
  458. package/dist/server/flight-injection-state.d.ts +66 -0
  459. package/dist/server/flight-injection-state.d.ts.map +1 -0
  460. package/dist/server/flight-scripts.d.ts +42 -0
  461. package/dist/server/flight-scripts.d.ts.map +1 -0
  462. package/dist/server/form-data.d.ts +41 -0
  463. package/dist/server/form-data.d.ts.map +1 -1
  464. package/dist/server/html-injector-core.d.ts +242 -0
  465. package/dist/server/html-injector-core.d.ts.map +1 -0
  466. package/dist/server/html-injectors.d.ts +82 -41
  467. package/dist/server/html-injectors.d.ts.map +1 -1
  468. package/dist/server/index.d.ts +11 -47
  469. package/dist/server/index.d.ts.map +1 -1
  470. package/dist/server/index.js +182 -2906
  471. package/dist/server/index.js.map +1 -1
  472. package/dist/server/internal.d.ts +49 -0
  473. package/dist/server/internal.d.ts.map +1 -0
  474. package/dist/server/internal.js +3945 -0
  475. package/dist/server/internal.js.map +1 -0
  476. package/dist/server/logger.d.ts +39 -7
  477. package/dist/server/logger.d.ts.map +1 -1
  478. package/dist/server/metadata-routes.d.ts +47 -11
  479. package/dist/server/metadata-routes.d.ts.map +1 -1
  480. package/dist/server/middleware-runner.d.ts +19 -4
  481. package/dist/server/middleware-runner.d.ts.map +1 -1
  482. package/dist/server/node-stream-transforms.d.ts +119 -0
  483. package/dist/server/node-stream-transforms.d.ts.map +1 -0
  484. package/dist/server/param-coercion.d.ts +54 -0
  485. package/dist/server/param-coercion.d.ts.map +1 -0
  486. package/dist/server/pipeline-helpers.d.ts +95 -0
  487. package/dist/server/pipeline-helpers.d.ts.map +1 -0
  488. package/dist/server/pipeline-interception.d.ts +1 -1
  489. package/dist/server/pipeline-interception.d.ts.map +1 -1
  490. package/dist/server/pipeline-metadata.d.ts +6 -0
  491. package/dist/server/pipeline-metadata.d.ts.map +1 -1
  492. package/dist/server/pipeline-outcome.d.ts +49 -0
  493. package/dist/server/pipeline-outcome.d.ts.map +1 -0
  494. package/dist/server/pipeline-phases.d.ts +59 -0
  495. package/dist/server/pipeline-phases.d.ts.map +1 -0
  496. package/dist/server/pipeline.d.ts +140 -19
  497. package/dist/server/pipeline.d.ts.map +1 -1
  498. package/dist/server/port-resolution.d.ts +117 -0
  499. package/dist/server/port-resolution.d.ts.map +1 -0
  500. package/dist/server/prebuilt/cache-key.d.ts +62 -0
  501. package/dist/server/prebuilt/cache-key.d.ts.map +1 -0
  502. package/dist/server/prebuilt/capture-state.d.ts +57 -0
  503. package/dist/server/prebuilt/capture-state.d.ts.map +1 -0
  504. package/dist/server/prebuilt/key-discipline.d.ts +23 -0
  505. package/dist/server/prebuilt/key-discipline.d.ts.map +1 -0
  506. package/dist/server/prebuilt/manifest-tag-index.d.ts +27 -0
  507. package/dist/server/prebuilt/manifest-tag-index.d.ts.map +1 -0
  508. package/dist/server/prebuilt/overlay.d.ts +100 -0
  509. package/dist/server/prebuilt/overlay.d.ts.map +1 -0
  510. package/dist/server/prebuilt/payload-source.d.ts +78 -0
  511. package/dist/server/prebuilt/payload-source.d.ts.map +1 -0
  512. package/dist/server/prebuilt/slots.d.ts +74 -0
  513. package/dist/server/prebuilt/slots.d.ts.map +1 -0
  514. package/dist/server/prebuilt/synthetic-store.d.ts +66 -0
  515. package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -0
  516. package/dist/server/prebuilt-builder.d.ts +120 -0
  517. package/dist/server/prebuilt-builder.d.ts.map +1 -0
  518. package/dist/server/prebuilt-runtime.d.ts +111 -0
  519. package/dist/server/prebuilt-runtime.d.ts.map +1 -0
  520. package/dist/server/primitives.d.ts +88 -43
  521. package/dist/server/primitives.d.ts.map +1 -1
  522. package/dist/server/render-timeout.d.ts +51 -0
  523. package/dist/server/render-timeout.d.ts.map +1 -0
  524. package/dist/server/render-utils.d.ts +25 -0
  525. package/dist/server/render-utils.d.ts.map +1 -0
  526. package/dist/server/request-context.d.ts +62 -107
  527. package/dist/server/request-context.d.ts.map +1 -1
  528. package/dist/server/route-element-builder.d.ts +55 -25
  529. package/dist/server/route-element-builder.d.ts.map +1 -1
  530. package/dist/server/route-handler.d.ts.map +1 -1
  531. package/dist/server/route-matcher.d.ts +25 -36
  532. package/dist/server/route-matcher.d.ts.map +1 -1
  533. package/dist/server/rsc-entry/action-middleware-runner.d.ts +66 -0
  534. package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -0
  535. package/dist/server/rsc-entry/api-handler.d.ts +12 -3
  536. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  537. package/dist/server/rsc-entry/deny-fallback.d.ts +29 -0
  538. package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -0
  539. package/dist/server/rsc-entry/error-renderer.d.ts +26 -13
  540. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  541. package/dist/server/rsc-entry/helpers.d.ts +51 -28
  542. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  543. package/dist/server/rsc-entry/index.d.ts +28 -6
  544. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  545. package/dist/server/rsc-entry/render-route.d.ts +51 -0
  546. package/dist/server/rsc-entry/render-route.d.ts.map +1 -0
  547. package/dist/server/rsc-entry/revalidate-renderer.d.ts +108 -0
  548. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -0
  549. package/dist/server/rsc-entry/rsc-payload.d.ts +5 -6
  550. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  551. package/dist/server/rsc-entry/rsc-stream.d.ts +25 -1
  552. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  553. package/dist/server/rsc-entry/ssr-bridge.d.ts +5 -1
  554. package/dist/server/rsc-entry/ssr-bridge.d.ts.map +1 -1
  555. package/dist/server/rsc-entry/ssr-renderer.d.ts +34 -4
  556. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  557. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +89 -0
  558. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -0
  559. package/dist/server/safe-load.d.ts +46 -0
  560. package/dist/server/safe-load.d.ts.map +1 -0
  561. package/dist/server/sensitive-fields.d.ts +74 -0
  562. package/dist/server/sensitive-fields.d.ts.map +1 -0
  563. package/dist/server/sitemap-generator.d.ts +129 -0
  564. package/dist/server/sitemap-generator.d.ts.map +1 -0
  565. package/dist/server/sitemap-handler.d.ts +22 -0
  566. package/dist/server/sitemap-handler.d.ts.map +1 -0
  567. package/dist/server/slot-resolver.d.ts +7 -5
  568. package/dist/server/slot-resolver.d.ts.map +1 -1
  569. package/dist/server/ssr-bridge-types.d.ts +121 -0
  570. package/dist/server/ssr-bridge-types.d.ts.map +1 -0
  571. package/dist/server/ssr-entry.d.ts +2 -43
  572. package/dist/server/ssr-entry.d.ts.map +1 -1
  573. package/dist/server/ssr-render.d.ts +64 -21
  574. package/dist/server/ssr-render.d.ts.map +1 -1
  575. package/dist/server/ssr-wrappers.d.ts +50 -0
  576. package/dist/server/ssr-wrappers.d.ts.map +1 -0
  577. package/dist/server/state-tree-diff.d.ts +36 -6
  578. package/dist/server/state-tree-diff.d.ts.map +1 -1
  579. package/dist/server/status-code-resolver.d.ts +22 -11
  580. package/dist/server/status-code-resolver.d.ts.map +1 -1
  581. package/dist/server/stream-utils.d.ts +47 -0
  582. package/dist/server/stream-utils.d.ts.map +1 -0
  583. package/dist/server/tracing.d.ts +44 -9
  584. package/dist/server/tracing.d.ts.map +1 -1
  585. package/dist/server/tree-builder.d.ts +62 -30
  586. package/dist/server/tree-builder.d.ts.map +1 -1
  587. package/dist/server/tree-match.d.ts +49 -0
  588. package/dist/server/tree-match.d.ts.map +1 -0
  589. package/dist/server/types.d.ts +71 -6
  590. package/dist/server/types.d.ts.map +1 -1
  591. package/dist/server/utils/escape-html.d.ts +14 -0
  592. package/dist/server/utils/escape-html.d.ts.map +1 -0
  593. package/dist/server/utils/mdx-file.d.ts +15 -0
  594. package/dist/server/utils/mdx-file.d.ts.map +1 -0
  595. package/dist/server/version-skew.d.ts +61 -0
  596. package/dist/server/version-skew.d.ts.map +1 -0
  597. package/dist/server/workers-cache-bridge.d.ts +26 -0
  598. package/dist/server/workers-cache-bridge.d.ts.map +1 -0
  599. package/dist/shared/href-validation.d.ts +49 -0
  600. package/dist/shared/href-validation.d.ts.map +1 -0
  601. package/dist/shared/merge-search-params.d.ts +22 -0
  602. package/dist/shared/merge-search-params.d.ts.map +1 -0
  603. package/dist/shared/redirect-type.d.ts +17 -0
  604. package/dist/shared/redirect-type.d.ts.map +1 -0
  605. package/dist/shims/font-google.d.ts +1 -1
  606. package/dist/shims/font-google.d.ts.map +1 -1
  607. package/dist/shims/font-google.js +42 -0
  608. package/dist/shims/font-google.js.map +1 -0
  609. package/dist/shims/font-local.d.ts +26 -0
  610. package/dist/shims/font-local.d.ts.map +1 -0
  611. package/dist/shims/font-local.js +20 -0
  612. package/dist/shims/font-local.js.map +1 -0
  613. package/dist/shims/headers.d.ts +53 -1
  614. package/dist/shims/headers.d.ts.map +1 -1
  615. package/dist/shims/image.d.ts +120 -120
  616. package/dist/shims/image.d.ts.map +1 -1
  617. package/dist/shims/navigation-client.d.ts +35 -11
  618. package/dist/shims/navigation-client.d.ts.map +1 -1
  619. package/dist/shims/navigation.d.ts +11 -5
  620. package/dist/shims/navigation.d.ts.map +1 -1
  621. package/dist/utils/state-machine.d.ts +80 -0
  622. package/dist/utils/state-machine.d.ts.map +1 -0
  623. package/docs/api/30-api-server.mdx +233 -0
  624. package/docs/api/31-api-client.mdx +161 -0
  625. package/docs/api/32-api-cache.mdx +311 -0
  626. package/docs/api/33-api-search-params.mdx +154 -0
  627. package/docs/api/34-api-config.mdx +325 -0
  628. package/docs/api/35-api-typescript.mdx +170 -0
  629. package/docs/api/36-cli.mdx +143 -0
  630. package/docs/learn/00-introduction.mdx +109 -0
  631. package/docs/learn/01-quick-start.mdx +94 -0
  632. package/docs/learn/02-pages-and-layouts.mdx +147 -0
  633. package/docs/learn/03-fetching-data.mdx +211 -0
  634. package/docs/learn/03b-access-control.mdx +130 -0
  635. package/docs/learn/04-loading-states.mdx +67 -0
  636. package/docs/learn/04b-the-flush-point.mdx +115 -0
  637. package/docs/learn/05-typed-params.mdx +275 -0
  638. package/docs/learn/06-forms-and-actions.mdx +146 -0
  639. package/docs/learn/07-typed-routes.mdx +127 -0
  640. package/docs/learn/08-streaming.mdx +146 -0
  641. package/docs/learn/09-caching.mdx +252 -0
  642. package/docs/learn/10-middleware.mdx +157 -0
  643. package/docs/learn/11-error-handling.mdx +156 -0
  644. package/docs/learn/12-client-navigation.mdx +176 -0
  645. package/docs/learn/13-configuration.mdx +162 -0
  646. package/docs/learn/14-deploying.mdx +264 -0
  647. package/docs/more/01-advanced-routing.mdx +173 -0
  648. package/docs/more/02-advanced-forms.mdx +137 -0
  649. package/docs/more/03-coming-from-nextjs.mdx +186 -0
  650. package/docs/more/04-metadata-and-fonts.mdx +193 -0
  651. package/docs/more/04b-mdx.mdx +209 -0
  652. package/docs/more/05-content-collections.mdx +90 -0
  653. package/docs/more/06-instrumentation.mdx +214 -0
  654. package/docs/more/07-security.mdx +129 -0
  655. package/docs/more/08-developer-experience.mdx +134 -0
  656. package/docs/more/40-why-timber.mdx +50 -0
  657. package/docs/more/41-timber-vs-nextjs.mdx +81 -0
  658. package/docs/more/42-timber-vs-others.mdx +68 -0
  659. package/docs/more/50-ai-agent-instructions.mdx +169 -0
  660. package/llms.txt +55 -0
  661. package/package.json +105 -26
  662. package/src/adapters/build-output-helper.ts +199 -0
  663. package/src/adapters/cloudflare-dev.ts +177 -0
  664. package/src/adapters/cloudflare-kv-cache.ts +204 -0
  665. package/src/adapters/cloudflare.ts +620 -129
  666. package/src/adapters/compress-module.ts +27 -92
  667. package/src/adapters/nitro.ts +266 -122
  668. package/src/adapters/raw-imports.d.ts +5 -0
  669. package/src/adapters/shared.ts +68 -0
  670. package/src/adapters/types.ts +2 -7
  671. package/src/adapters/wrangler.d.ts +7 -0
  672. package/src/cache/cache-api.ts +107 -0
  673. package/src/cache/handler-store.ts +76 -0
  674. package/src/cache/index.ts +114 -21
  675. package/src/cache/invalidation-epoch.ts +93 -0
  676. package/src/cache/json-lossy-check.ts +75 -0
  677. package/src/cache/redis-handler.ts +197 -19
  678. package/src/cache/singleflight.ts +64 -6
  679. package/src/cache/sizeof.ts +33 -0
  680. package/src/cache/stable-stringify.ts +65 -2
  681. package/src/cache/store.ts +71 -0
  682. package/src/cache/stores/cloudflare-kv.ts +52 -0
  683. package/src/cache/stores/memory.ts +125 -0
  684. package/src/cache/stores/redis.ts +97 -0
  685. package/src/cache/stores/vercel.ts +59 -0
  686. package/src/cache/tag-aware-handler.ts +237 -0
  687. package/src/cache/timber-cache.ts +161 -36
  688. package/src/cdn/cloudflare-purge.ts +50 -0
  689. package/src/cdn/fastly-purge.ts +63 -0
  690. package/src/cdn/headers.ts +153 -0
  691. package/src/cdn/index.ts +17 -0
  692. package/src/cdn/purge-store.ts +34 -0
  693. package/src/cdn/workers-cache-purge.ts +30 -0
  694. package/src/cli-schema-sync.ts +401 -0
  695. package/src/cli.ts +157 -34
  696. package/src/client/browser-dev.ts +156 -1
  697. package/src/client/browser-entry/action-dispatch.ts +124 -0
  698. package/src/client/browser-entry/hmr.ts +70 -0
  699. package/src/client/browser-entry/hydrate.ts +187 -0
  700. package/src/client/browser-entry/index.ts +113 -0
  701. package/src/client/browser-entry/post-hydration.ts +110 -0
  702. package/src/client/browser-entry/router-init.ts +289 -0
  703. package/src/client/browser-entry/rsc-stream.ts +157 -0
  704. package/src/client/browser-entry/scroll.ts +50 -0
  705. package/src/client/child-segment-context.ts +40 -0
  706. package/src/client/child-segment-outlet.tsx +25 -0
  707. package/src/client/child-segment-provider.tsx +27 -0
  708. package/src/client/error-boundary.tsx +127 -22
  709. package/src/client/error-reconstituter.tsx +79 -0
  710. package/src/client/form.tsx +31 -32
  711. package/src/client/history.ts +21 -6
  712. package/src/client/index.ts +67 -45
  713. package/src/client/internal.ts +58 -0
  714. package/src/client/link.tsx +349 -150
  715. package/src/client/navigation-api.ts +341 -0
  716. package/src/client/navigation-context.ts +15 -7
  717. package/src/client/navigation-root.tsx +355 -0
  718. package/src/client/nuqs-adapter.tsx +16 -3
  719. package/src/client/router-ref.ts +1 -1
  720. package/src/client/router.ts +538 -206
  721. package/src/client/rsc-fetch.ts +214 -33
  722. package/src/client/segment-cache.ts +11 -12
  723. package/src/client/segment-outlet.tsx +77 -0
  724. package/src/client/segment-update-context.ts +42 -0
  725. package/src/client/shallow-url.ts +22 -0
  726. package/src/client/slot-context.ts +48 -0
  727. package/src/client/slot-outlet.tsx +22 -0
  728. package/src/client/slot-provider.tsx +25 -0
  729. package/src/client/ssr-data.ts +13 -5
  730. package/src/client/state.ts +2 -2
  731. package/src/client/top-loader.tsx +11 -9
  732. package/src/client/use-cookie.ts +60 -15
  733. package/src/client/use-link-status.ts +7 -7
  734. package/src/client/use-pathname.ts +1 -0
  735. package/src/client/use-pending-navigation.ts +40 -0
  736. package/src/client/use-query-states.ts +108 -10
  737. package/src/client/use-router.ts +4 -1
  738. package/src/client/use-search-params.ts +31 -44
  739. package/src/client/{use-params.ts → use-segment-params.ts} +9 -6
  740. package/src/codec.ts +149 -0
  741. package/src/config-types.ts +282 -0
  742. package/src/config-validation.ts +330 -0
  743. package/src/content/index.ts +5 -13
  744. package/src/cookies/define-cookie.ts +217 -53
  745. package/src/cookies/index.ts +12 -1
  746. package/src/cookies/json-cookie.ts +105 -0
  747. package/src/cookies/validation.ts +192 -0
  748. package/src/dev-tools/404-page.ts +286 -0
  749. package/src/dev-tools/browser-logs.ts +55 -0
  750. package/src/dev-tools/dev-page-shell.ts +260 -0
  751. package/src/dev-tools/error-page.ts +331 -0
  752. package/src/dev-tools/holding-server.ts +187 -0
  753. package/src/dev-tools/hydration-overlay.ts +158 -0
  754. package/src/dev-tools/index.ts +83 -0
  755. package/src/dev-tools/instrumentation.ts +213 -0
  756. package/src/{server/dev-logger.ts → dev-tools/logger.ts} +33 -51
  757. package/src/{plugins/dev-logs.ts → dev-tools/logs.ts} +122 -26
  758. package/src/dev-tools/overlay.ts +469 -0
  759. package/src/dev-tools/stack-classifier.ts +75 -0
  760. package/src/dev-tools/terminal.ts +182 -0
  761. package/src/fonts/ast-walk.ts +235 -0
  762. package/src/fonts/ast.ts +477 -126
  763. package/src/fonts/bundle.ts +138 -0
  764. package/src/fonts/dev-middleware.ts +85 -0
  765. package/src/fonts/fallbacks.ts +0 -23
  766. package/src/fonts/google.ts +63 -8
  767. package/src/fonts/local.ts +9 -8
  768. package/src/fonts/paths.ts +58 -0
  769. package/src/fonts/pipeline.ts +348 -0
  770. package/src/fonts/transform.ts +311 -0
  771. package/src/fonts/types.ts +52 -1
  772. package/src/fonts/virtual-modules.ts +179 -0
  773. package/src/index.ts +444 -352
  774. package/src/params/codec-registry.ts +34 -0
  775. package/src/params/define-schema.ts +115 -0
  776. package/src/params/index.ts +17 -0
  777. package/src/params/resolve-schema.ts +84 -0
  778. package/src/plugin-context.ts +432 -0
  779. package/src/plugins/adapter-build.ts +220 -7
  780. package/src/plugins/build-manifest.ts +168 -23
  781. package/src/plugins/build-report.ts +253 -35
  782. package/src/plugins/cache.ts +243 -0
  783. package/src/plugins/callsite-ast.ts +472 -0
  784. package/src/plugins/callsite-jsx.ts +243 -0
  785. package/src/plugins/chunks.ts +74 -5
  786. package/src/plugins/client-chunks.ts +185 -0
  787. package/src/plugins/cloudflare-wasm.ts +141 -0
  788. package/src/plugins/content.ts +1 -1
  789. package/src/plugins/dev-server.ts +284 -94
  790. package/src/plugins/entries.ts +134 -16
  791. package/src/plugins/fonts.ts +225 -660
  792. package/src/plugins/mdx.ts +47 -10
  793. package/src/plugins/prebuilt-capture.ts +217 -0
  794. package/src/plugins/prebuilt-options-analysis.ts +175 -0
  795. package/src/plugins/prebuilt.ts +655 -0
  796. package/src/plugins/prerender-sugar.ts +328 -0
  797. package/src/plugins/routing.ts +237 -241
  798. package/src/plugins/server-bundle.ts +89 -12
  799. package/src/plugins/shims.ts +241 -64
  800. package/src/plugins/static-build.ts +15 -90
  801. package/src/routing/codegen-shared.ts +115 -0
  802. package/src/routing/codegen-types.ts +65 -0
  803. package/src/routing/codegen.ts +372 -178
  804. package/src/routing/convention-lint.ts +394 -0
  805. package/src/routing/export-detect.ts +304 -0
  806. package/src/routing/file-cache.ts +23 -0
  807. package/src/routing/index.ts +10 -1
  808. package/src/routing/interception.ts +3 -22
  809. package/src/routing/link-codegen.ts +313 -0
  810. package/src/routing/manifest-codegen.ts +287 -0
  811. package/src/routing/scanner.ts +338 -189
  812. package/src/routing/schema-validation.ts +155 -0
  813. package/src/routing/segment-classify.ts +223 -0
  814. package/src/routing/segment-codegen.ts +98 -0
  815. package/src/routing/types.ts +82 -25
  816. package/src/routing/walkers.ts +90 -0
  817. package/src/rsc-runtime/rsc.ts +9 -0
  818. package/src/rsc-runtime/ssr.ts +50 -0
  819. package/src/rsc-runtime/vendor-types.d.ts +7 -0
  820. package/src/schema-bridge.ts +253 -0
  821. package/src/search-params/define.ts +576 -0
  822. package/src/search-params/index.ts +14 -20
  823. package/src/search-params/registry.ts +2 -2
  824. package/src/search-params/wrappers.ts +101 -0
  825. package/src/segment-params/define.ts +249 -0
  826. package/src/segment-params/index.ts +9 -0
  827. package/src/server/access-gate.tsx +84 -34
  828. package/src/server/action-client.ts +120 -29
  829. package/src/server/action-encryption.ts +144 -0
  830. package/src/server/action-handler.ts +123 -33
  831. package/src/server/actions.ts +48 -54
  832. package/src/server/als-registry.ts +137 -20
  833. package/src/server/asset-headers.ts +15 -37
  834. package/src/server/body-limits.ts +10 -2
  835. package/src/server/build-manifest.ts +13 -56
  836. package/src/server/canonicalize.ts +6 -3
  837. package/src/server/coalesce-tick.ts +75 -0
  838. package/src/server/compress.ts +34 -7
  839. package/src/server/cookie-context.ts +484 -0
  840. package/src/server/cookie-parsing.ts +163 -0
  841. package/src/server/csrf.ts +39 -6
  842. package/src/server/default-logger.ts +99 -0
  843. package/src/server/deny-boundary.ts +314 -0
  844. package/src/server/deny-renderer.ts +85 -77
  845. package/src/server/dev-source-map.ts +31 -0
  846. package/src/server/early-hints-sender.ts +3 -2
  847. package/src/server/early-hints.ts +37 -27
  848. package/src/server/error-boundary-wrapper.ts +61 -22
  849. package/src/server/fallback-error.ts +79 -102
  850. package/src/server/flight-injection-state.ts +113 -0
  851. package/src/server/flight-scripts.ts +62 -0
  852. package/src/server/form-data.ts +124 -12
  853. package/src/server/html-injector-core.ts +495 -0
  854. package/src/server/html-injectors.ts +271 -254
  855. package/src/server/index.ts +31 -185
  856. package/src/server/internal.ts +181 -0
  857. package/src/server/logger.ts +72 -36
  858. package/src/server/metadata-routes.ts +110 -24
  859. package/src/server/middleware-runner.ts +31 -4
  860. package/src/server/node-stream-transforms.ts +336 -0
  861. package/src/server/param-coercion.ts +207 -0
  862. package/src/server/pipeline-helpers.ts +204 -0
  863. package/src/server/pipeline-interception.ts +45 -18
  864. package/src/server/pipeline-metadata.ts +14 -0
  865. package/src/server/pipeline-outcome.ts +197 -0
  866. package/src/server/pipeline-phases.ts +545 -0
  867. package/src/server/pipeline.ts +274 -382
  868. package/src/server/port-resolution.ts +215 -0
  869. package/src/server/prebuilt/cache-key.ts +298 -0
  870. package/src/server/prebuilt/capture-state.ts +89 -0
  871. package/src/server/prebuilt/key-discipline.ts +53 -0
  872. package/src/server/prebuilt/manifest-tag-index.ts +46 -0
  873. package/src/server/prebuilt/overlay.ts +256 -0
  874. package/src/server/prebuilt/payload-source.ts +201 -0
  875. package/src/server/prebuilt/slots.ts +167 -0
  876. package/src/server/prebuilt/synthetic-store.ts +121 -0
  877. package/src/server/prebuilt-builder.ts +808 -0
  878. package/src/server/prebuilt-runtime.ts +815 -0
  879. package/src/server/primitives.ts +160 -83
  880. package/src/server/render-timeout.ts +108 -0
  881. package/src/server/render-utils.ts +31 -0
  882. package/src/server/request-context.ts +168 -337
  883. package/src/server/route-element-builder.ts +661 -278
  884. package/src/server/route-handler.ts +31 -4
  885. package/src/server/route-matcher.ts +158 -277
  886. package/src/server/rsc-entry/action-middleware-runner.ts +167 -0
  887. package/src/server/rsc-entry/api-handler.ts +69 -32
  888. package/src/server/rsc-entry/deny-fallback.ts +92 -0
  889. package/src/server/rsc-entry/error-renderer.ts +306 -89
  890. package/src/server/rsc-entry/helpers.ts +165 -79
  891. package/src/server/rsc-entry/index.ts +332 -341
  892. package/src/server/rsc-entry/render-route.ts +292 -0
  893. package/src/server/rsc-entry/revalidate-renderer.ts +349 -0
  894. package/src/server/rsc-entry/rsc-payload.ts +95 -39
  895. package/src/server/rsc-entry/rsc-stream.ts +93 -13
  896. package/src/server/rsc-entry/ssr-bridge.ts +18 -5
  897. package/src/server/rsc-entry/ssr-renderer.ts +273 -43
  898. package/src/server/rsc-entry/wrap-action-dispatch.ts +186 -0
  899. package/src/server/safe-load.ts +60 -0
  900. package/src/server/sensitive-fields.ts +230 -0
  901. package/src/server/server-only-guard-noop.js +3 -0
  902. package/src/server/sitemap-generator.ts +345 -0
  903. package/src/server/sitemap-handler.ts +157 -0
  904. package/src/server/slot-resolver.ts +264 -257
  905. package/src/server/ssr-bridge-types.ts +124 -0
  906. package/src/server/ssr-entry.ts +206 -78
  907. package/src/server/ssr-render.ts +345 -82
  908. package/src/server/ssr-wrappers.tsx +141 -0
  909. package/src/server/state-tree-diff.ts +76 -24
  910. package/src/server/status-code-resolver.ts +115 -130
  911. package/src/server/stream-utils.ts +288 -0
  912. package/src/server/tracing.ts +126 -14
  913. package/src/server/tree-builder.ts +204 -90
  914. package/src/server/tree-match.ts +262 -0
  915. package/src/server/types.ts +78 -9
  916. package/src/server/utils/escape-html.ts +20 -0
  917. package/src/server/utils/mdx-file.ts +22 -0
  918. package/src/server/version-skew.ts +104 -0
  919. package/src/server/workers-cache-bridge.ts +33 -0
  920. package/src/shared/href-validation.ts +159 -0
  921. package/src/shared/merge-search-params.ts +55 -0
  922. package/src/shared/redirect-type.ts +17 -0
  923. package/src/shims/font-google.ts +1 -1
  924. package/src/shims/font-local.ts +34 -0
  925. package/src/shims/headers.ts +122 -1
  926. package/src/shims/navigation-client.ts +51 -29
  927. package/src/shims/navigation.ts +14 -7
  928. package/src/utils/state-machine.ts +111 -0
  929. package/dist/_chunks/als-registry-B7DbZ2hS.js +0 -41
  930. package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
  931. package/dist/_chunks/debug-B3Gypr3D.js +0 -108
  932. package/dist/_chunks/debug-B3Gypr3D.js.map +0 -1
  933. package/dist/_chunks/format-RyoGQL74.js +0 -164
  934. package/dist/_chunks/format-RyoGQL74.js.map +0 -1
  935. package/dist/_chunks/interception-BOoWmLUA.js +0 -676
  936. package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
  937. package/dist/_chunks/metadata-routes-Cjmvi3rQ.js +0 -153
  938. package/dist/_chunks/metadata-routes-Cjmvi3rQ.js.map +0 -1
  939. package/dist/_chunks/request-context-BQUC8PHn.js +0 -330
  940. package/dist/_chunks/request-context-BQUC8PHn.js.map +0 -1
  941. package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
  942. package/dist/_chunks/tracing-CemImE6h.js +0 -187
  943. package/dist/_chunks/tracing-CemImE6h.js.map +0 -1
  944. package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
  945. package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
  946. package/dist/_chunks/use-query-states-D5KaffOK.js.map +0 -1
  947. package/dist/adapters/cloudflare.js.map +0 -1
  948. package/dist/cache/index.js.map +0 -1
  949. package/dist/cache/register-cached-function.d.ts +0 -17
  950. package/dist/cache/register-cached-function.d.ts.map +0 -1
  951. package/dist/client/browser-entry.d.ts +0 -21
  952. package/dist/client/browser-entry.d.ts.map +0 -1
  953. package/dist/client/head.d.ts +0 -16
  954. package/dist/client/head.d.ts.map +0 -1
  955. package/dist/client/link-status-provider.d.ts +0 -11
  956. package/dist/client/link-status-provider.d.ts.map +0 -1
  957. package/dist/client/segment-merger.d.ts +0 -119
  958. package/dist/client/segment-merger.d.ts.map +0 -1
  959. package/dist/client/stale-reload.d.ts +0 -44
  960. package/dist/client/stale-reload.d.ts.map +0 -1
  961. package/dist/client/transition-root.d.ts +0 -74
  962. package/dist/client/transition-root.d.ts.map +0 -1
  963. package/dist/client/use-navigation-pending.d.ts +0 -26
  964. package/dist/client/use-navigation-pending.d.ts.map +0 -1
  965. package/dist/client/use-params.d.ts.map +0 -1
  966. package/dist/plugins/cache-transform.d.ts +0 -36
  967. package/dist/plugins/cache-transform.d.ts.map +0 -1
  968. package/dist/plugins/dev-error-overlay.d.ts +0 -60
  969. package/dist/plugins/dev-error-overlay.d.ts.map +0 -1
  970. package/dist/plugins/dev-logs.d.ts.map +0 -1
  971. package/dist/plugins/dynamic-transform.d.ts +0 -72
  972. package/dist/plugins/dynamic-transform.d.ts.map +0 -1
  973. package/dist/plugins/server-action-exports.d.ts +0 -26
  974. package/dist/plugins/server-action-exports.d.ts.map +0 -1
  975. package/dist/routing/status-file-lint.d.ts +0 -33
  976. package/dist/routing/status-file-lint.d.ts.map +0 -1
  977. package/dist/search-params/analyze.d.ts +0 -54
  978. package/dist/search-params/analyze.d.ts.map +0 -1
  979. package/dist/search-params/builtin-codecs.d.ts +0 -105
  980. package/dist/search-params/builtin-codecs.d.ts.map +0 -1
  981. package/dist/search-params/codecs.d.ts +0 -53
  982. package/dist/search-params/codecs.d.ts.map +0 -1
  983. package/dist/search-params/create.d.ts +0 -106
  984. package/dist/search-params/create.d.ts.map +0 -1
  985. package/dist/server/client-module-map.d.ts +0 -47
  986. package/dist/server/client-module-map.d.ts.map +0 -1
  987. package/dist/server/dev-fetch-instrumentation.d.ts +0 -22
  988. package/dist/server/dev-fetch-instrumentation.d.ts.map +0 -1
  989. package/dist/server/dev-logger.d.ts.map +0 -1
  990. package/dist/server/dev-span-processor.d.ts +0 -29
  991. package/dist/server/dev-span-processor.d.ts.map +0 -1
  992. package/dist/server/dev-warnings.d.ts +0 -129
  993. package/dist/server/dev-warnings.d.ts.map +0 -1
  994. package/dist/server/flush.d.ts +0 -74
  995. package/dist/server/flush.d.ts.map +0 -1
  996. package/dist/server/manifest-status-resolver.d.ts +0 -58
  997. package/dist/server/manifest-status-resolver.d.ts.map +0 -1
  998. package/dist/server/prerender.d.ts +0 -77
  999. package/dist/server/prerender.d.ts.map +0 -1
  1000. package/dist/utils/directive-parser.d.ts +0 -70
  1001. package/dist/utils/directive-parser.d.ts.map +0 -1
  1002. package/src/cache/register-cached-function.ts +0 -103
  1003. package/src/client/browser-entry.ts +0 -678
  1004. package/src/client/head.ts +0 -61
  1005. package/src/client/link-status-provider.tsx +0 -30
  1006. package/src/client/segment-merger.ts +0 -339
  1007. package/src/client/stale-reload.ts +0 -87
  1008. package/src/client/transition-root.tsx +0 -166
  1009. package/src/client/use-navigation-pending.ts +0 -38
  1010. package/src/plugins/cache-transform.ts +0 -199
  1011. package/src/plugins/dev-error-overlay.ts +0 -230
  1012. package/src/plugins/dynamic-transform.ts +0 -161
  1013. package/src/plugins/server-action-exports.ts +0 -220
  1014. package/src/routing/status-file-lint.ts +0 -112
  1015. package/src/search-params/analyze.ts +0 -192
  1016. package/src/search-params/builtin-codecs.ts +0 -228
  1017. package/src/search-params/codecs.ts +0 -153
  1018. package/src/search-params/create.ts +0 -321
  1019. package/src/server/client-module-map.ts +0 -58
  1020. package/src/server/dev-fetch-instrumentation.ts +0 -96
  1021. package/src/server/dev-span-processor.ts +0 -78
  1022. package/src/server/dev-warnings.ts +0 -283
  1023. package/src/server/flush.ts +0 -182
  1024. package/src/server/manifest-status-resolver.ts +0 -215
  1025. package/src/server/prerender.ts +0 -139
  1026. package/src/utils/directive-parser.ts +0 -529
@@ -0,0 +1,3945 @@
1
+ import { a as classifyMetadataRoute, i as METADATA_ROUTE_CONVENTIONS, o as getMetadataRouteAutoLink, r as canonicalize, s as getMetadataRouteServePath } from "../_chunks/canonicalize-Du3o_ptW.js";
2
+ import { n as classifyUrlSegment } from "../_chunks/segment-classify-Byy425ng.js";
3
+ import { $ as timingAls, A as setMatchedSegmentPath, C as applyRequestHeaderOverlay, D as getSegmentParams, E as getSearchParams, F as getSetCookieHeaders, G as replaceTraceId, H as getOtelTraceId, I as getWaitUntil, J as withSpan, K as runWithTraceId, L as isDebug, M as setSegmentParams, N as getCookie, O as markResponseFlushed, V as generateTraceId, W as getTraceId, Y as earlyHintsSenderAls, Z as requestContextAls, _ as RedirectSignal, a as logProxyError, c as logRequestReceived, d as logSwrRefetchFailed, f as logWaitUntilRejected, g as DenySignal, h as swallow, i as logMiddlewareShortCircuit, j as setMutableCookieContext, k as runWithRequestContext, l as logRouteError, m as setLogger, n as logCacheMiss, o as logRenderError, p as logWaitUntilUnsupported, q as setSpanAttribute, r as logMiddlewareError, s as logRequestCompleted, t as getLogger, u as logSlowRequest, v as RenderError, w as getHeader } from "../_chunks/logger-AWfuX-KJ.js";
4
+ import { i as coerce, t as executeAction } from "../_chunks/actions-O_LsyCE4.js";
5
+ import { a as getManifestEntry, b as wasInvalidatedSince, c as lookupPrebuiltPayload, d as checkVersionSkew, i as storeOverlayEntry, l as setPrebuiltPayloadSource, n as lookupOverlay, o as hasPrebuiltPayloadSource, r as overlayKey, s as isParamIndependent, u as applyReloadHeaders, v as currentInvalidationEpoch, x as createSingleflight, y as lastInvalidationEpochFor } from "../_chunks/cache-api-B-lhk9p4.js";
6
+ import "../client/error-boundary.js";
7
+ import { n as toBracketKey } from "../_chunks/resolve-schema-Dz3fcFUo.js";
8
+ import "../_chunks/segment-context-ZDnXDkbz.js";
9
+ import { readFile } from "node:fs/promises";
10
+ import { createHash } from "node:crypto";
11
+ import React, { createElement, useContext } from "react";
12
+ //#region src/server/server-timing.ts
13
+ /**
14
+ * Server-Timing header — dev-mode timing breakdowns for Chrome DevTools.
15
+ *
16
+ * Collects timing entries per request using ALS. Each pipeline phase
17
+ * (proxy, middleware, render, SSR, access, fetch) records an entry.
18
+ * Before response flush, entries are formatted into a Server-Timing header.
19
+ *
20
+ * Only active in dev mode — zero overhead in production.
21
+ *
22
+ * See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing
23
+ * Task: LOCAL-290
24
+ */
25
+ /**
26
+ * Run a callback with a per-request timing collector.
27
+ * Must be called at the top of the request pipeline (wraps the full request).
28
+ */
29
+ function runWithTimingCollector(fn) {
30
+ return timingAls.run({ entries: [] }, fn);
31
+ }
32
+ /**
33
+ * Run a function and automatically record its duration as a timing entry.
34
+ * Returns the function's result. No-ops the recording if outside a collector.
35
+ */
36
+ async function withTiming(name, desc, fn) {
37
+ const store = timingAls.getStore();
38
+ if (!store) return fn();
39
+ const start = performance.now();
40
+ try {
41
+ return await fn();
42
+ } finally {
43
+ const dur = Math.round(performance.now() - start);
44
+ store.entries.push({
45
+ name,
46
+ dur,
47
+ desc
48
+ });
49
+ }
50
+ }
51
+ /**
52
+ * Get the Server-Timing header value for the current request.
53
+ * Returns null if no entries exist or outside a collector.
54
+ *
55
+ * Format: `name;dur=123;desc="description", name2;dur=456`
56
+ * See RFC 6797 / Server-Timing spec for format details.
57
+ */
58
+ function getServerTimingHeader() {
59
+ const store = timingAls.getStore();
60
+ if (!store || store.entries.length === 0) return null;
61
+ const nameCounts = /* @__PURE__ */ new Map();
62
+ const parts = store.entries.map((entry) => {
63
+ const count = nameCounts.get(entry.name) ?? 0;
64
+ nameCounts.set(entry.name, count + 1);
65
+ const uniqueName = count > 0 ? `${entry.name}-${count}` : entry.name;
66
+ return {
67
+ ...entry,
68
+ name: uniqueName
69
+ };
70
+ }).map((entry) => {
71
+ let part = `${entry.name};dur=${entry.dur}`;
72
+ if (entry.desc) {
73
+ const safeDesc = entry.desc.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
74
+ part += `;desc="${safeDesc}"`;
75
+ }
76
+ return part;
77
+ });
78
+ const MAX_HEADER_SIZE = 4096;
79
+ let result = "";
80
+ for (let i = 0; i < parts.length; i++) {
81
+ const candidate = result ? `${result}, ${parts[i]}` : parts[i];
82
+ if (candidate.length > MAX_HEADER_SIZE) break;
83
+ result = candidate;
84
+ }
85
+ return result || null;
86
+ }
87
+ //#endregion
88
+ //#region src/server/stream-utils.ts
89
+ /**
90
+ * Wrap a response body with stream-lifecycle timing instrumentation.
91
+ *
92
+ * Logs firstChunk, lastChunk, close, maxGap (largest inter-chunk gap),
93
+ * chunk count, and byte count on stream termination (clean close OR
94
+ * cancellation/disconnect). Gated behind `serverTiming: 'detailed'`.
95
+ *
96
+ * Uses a manual reader pump to guarantee logging on all termination
97
+ * paths — TransformStream.flush() is not called on cancellation.
98
+ */
99
+ function wrapResponseWithStreamTiming(body, startTime) {
100
+ let firstChunkMs = 0;
101
+ let lastChunkMs = 0;
102
+ let maxGapMs = 0;
103
+ let prevChunkTime = 0;
104
+ let chunks = 0;
105
+ let bytes = 0;
106
+ let cancelled = false;
107
+ function emitLog() {
108
+ if (process.env.NODE_ENV !== "production") {
109
+ const closeMs = Math.round(performance.now() - startTime);
110
+ console.debug(`[timber] stream-lifecycle: firstChunk=${firstChunkMs}ms lastChunk=${lastChunkMs}ms close=${closeMs}ms maxGap=${maxGapMs}ms chunks=${chunks} bytes=${bytes}` + (cancelled ? " cancelled=true" : ""));
111
+ }
112
+ }
113
+ const reader = body.getReader();
114
+ return new ReadableStream({
115
+ async pull(controller) {
116
+ try {
117
+ const { done, value } = await reader.read();
118
+ if (done) {
119
+ emitLog();
120
+ controller.close();
121
+ return;
122
+ }
123
+ const now = performance.now();
124
+ const relativeMs = Math.round(now - startTime);
125
+ if (chunks === 0) firstChunkMs = relativeMs;
126
+ else {
127
+ const gap = Math.round(now - prevChunkTime);
128
+ if (gap > maxGapMs) maxGapMs = gap;
129
+ }
130
+ prevChunkTime = now;
131
+ lastChunkMs = relativeMs;
132
+ chunks++;
133
+ bytes += value.byteLength;
134
+ controller.enqueue(value);
135
+ } catch (err) {
136
+ emitLog();
137
+ controller.error(err);
138
+ }
139
+ },
140
+ cancel(reason) {
141
+ cancelled = true;
142
+ emitLog();
143
+ reader.cancel(reason).catch(() => {});
144
+ }
145
+ });
146
+ }
147
+ //#endregion
148
+ //#region src/server/instrumentation.ts
149
+ /**
150
+ * Instrumentation — loads and runs the user's instrumentation.ts file.
151
+ *
152
+ * instrumentation.ts is a file convention at the project root that exports:
153
+ * - register() — called once at server startup, before the first request
154
+ * - onRequestError() — called for every unhandled server error
155
+ * - logger — any object with info/warn/error/debug methods
156
+ *
157
+ * See design/17-logging.md §"instrumentation.ts — The Entry Point"
158
+ */
159
+ var _initialized = false;
160
+ var _onRequestError = null;
161
+ /**
162
+ * Load and initialize the user's instrumentation.ts module.
163
+ *
164
+ * - Awaits register() before returning (server blocks on this).
165
+ * - Picks up the logger export and wires it into the framework logger.
166
+ * - Stores onRequestError for later invocation.
167
+ *
168
+ * @param loader - Function that dynamically imports the user's instrumentation module.
169
+ * Returns null if no instrumentation.ts exists.
170
+ */
171
+ async function loadInstrumentation(loader) {
172
+ if (_initialized) return;
173
+ _initialized = true;
174
+ let mod;
175
+ try {
176
+ mod = await loader();
177
+ } catch (error) {
178
+ console.error("[timber] Failed to load instrumentation.ts:", error);
179
+ return;
180
+ }
181
+ if (!mod) return;
182
+ if (mod.logger && typeof mod.logger.info === "function") setLogger(mod.logger);
183
+ if (typeof mod.onRequestError === "function") _onRequestError = mod.onRequestError;
184
+ if (typeof mod.register === "function") try {
185
+ await mod.register();
186
+ } catch (error) {
187
+ console.error("[timber] instrumentation.ts register() threw:", error);
188
+ throw error;
189
+ }
190
+ }
191
+ /**
192
+ * Call the user's onRequestError hook. Catches and logs any errors thrown
193
+ * by the hook itself — it must not affect the response.
194
+ */
195
+ async function callOnRequestError(error, request, context) {
196
+ if (!_onRequestError) return;
197
+ try {
198
+ await _onRequestError(error, request, context);
199
+ } catch (hookError) {
200
+ console.error("[timber] onRequestError hook threw:", hookError);
201
+ }
202
+ }
203
+ /**
204
+ * Check if onRequestError is registered.
205
+ */
206
+ function hasOnRequestError() {
207
+ return _onRequestError !== null;
208
+ }
209
+ //#endregion
210
+ //#region src/server/pipeline-helpers.ts
211
+ /**
212
+ * Only __proto__ needs stripping — it has a language-level setter that
213
+ * changes the prototype chain of spread copies. constructor and prototype
214
+ * are harmless own properties on null-prototype objects.
215
+ */
216
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__"]);
217
+ /**
218
+ * Deep-walk a value returned by a segment param codec, producing a
219
+ * sanitized copy where every plain object is null-prototype and
220
+ * dangerous keys (__proto__, constructor, prototype) are stripped at
221
+ * every depth.
222
+ *
223
+ * Non-plain objects (Date, Map, class instances, etc.) are returned
224
+ * as-is — they cannot be poisoned by `{...x}` spread and may carry
225
+ * author-intended prototype methods.
226
+ *
227
+ * Arrays are walked element-wise.
228
+ *
229
+ * Performance: URL params are bounded by URL length (~8 KB). Realistic
230
+ * trees are <1 KB. The recursive walk is sub-microsecond.
231
+ *
232
+ * See TIM-655, TIM-855, TIM-873, design/13-security.md
233
+ */
234
+ function sanitizeParamValue(value) {
235
+ if (value === null || typeof value !== "object") return value;
236
+ if (Array.isArray(value)) return value.map(sanitizeParamValue);
237
+ const proto = Object.getPrototypeOf(value);
238
+ if (proto !== Object.prototype && proto !== null) return value;
239
+ const out = Object.create(null);
240
+ for (const key of Object.keys(value)) if (!DANGEROUS_KEYS.has(key)) out[key] = sanitizeParamValue(value[key]);
241
+ return out;
242
+ }
243
+ /**
244
+ * Build a proxy resolver closure from the declared source. Called exactly
245
+ * once at `createPipeline` setup time, so the hot path sees only the branch
246
+ * that corresponds to this pipeline's configured variant.
247
+ *
248
+ * Returns `null` when the app has no proxy.ts — the hot path short-circuits
249
+ * around `runProxyPhase` entirely in that case.
250
+ *
251
+ * Accepts the sugar form (a bare `ProxyExport` — function or function array)
252
+ * and normalises it to the static variant. Functions and arrays are
253
+ * structurally distinct from the tagged `{ kind: 'lazy', loader }` object,
254
+ * so discrimination is unambiguous.
255
+ */
256
+ function makeProxyResolver(proxy) {
257
+ if (proxy === void 0) return null;
258
+ if (typeof proxy === "function" || Array.isArray(proxy)) {
259
+ const exp = proxy;
260
+ return () => exp;
261
+ }
262
+ if (proxy.kind === "static") {
263
+ const exp = proxy.export;
264
+ return () => exp;
265
+ }
266
+ const loader = proxy.loader;
267
+ return async () => (await loader()).default;
268
+ }
269
+ /**
270
+ * Apply all Set-Cookie headers from the cookie jar to a Headers object.
271
+ * Each cookie gets its own Set-Cookie header per RFC 6265 §4.1.
272
+ */
273
+ function applyCookieJar(headers) {
274
+ for (const value of getSetCookieHeaders()) headers.append("Set-Cookie", value);
275
+ }
276
+ /**
277
+ * Merge framework-managed response headers onto a terminal response without
278
+ * overwriting headers the terminal response already set itself.
279
+ */
280
+ function mergeMissingHeaders(target, source) {
281
+ const existingKeys = new Set([...target.keys()].map((key) => key.toLowerCase()));
282
+ for (const [key, value] of source.entries()) if (!existingKeys.has(key.toLowerCase())) target.append(key, value);
283
+ }
284
+ /**
285
+ * Clone a Response into a fresh one whose header bag is guaranteed mutable.
286
+ *
287
+ * `Response.redirect()` and some platform-level passthrough responses (notably
288
+ * on Cloudflare Workers) return objects with frozen header bags. Calling
289
+ * `.set()` or `.append()` on them throws `TypeError: immutable`, which the
290
+ * pipeline can hit when it appends Set-Cookie or Server-Timing entries.
291
+ *
292
+ * The pipeline calls this at the producer sites where user-controlled
293
+ * responses enter the framework — `outcomeToResponse` for all phase outcomes,
294
+ * and `handleRequest` for metadata-route and auto-sitemap user handlers — so
295
+ * downstream code can write headers without runtime feature-detection.
296
+ *
297
+ * The clone is unconditional. This is a deliberate trade: we avoid a
298
+ * try/catch + thrown `TypeError` on every request (the previous probe-based
299
+ * approach paid that cost on the hot path) and accept one cheap Response
300
+ * rewrap at the framework boundary instead.
301
+ */
302
+ function cloneWithMutableHeaders(response) {
303
+ return new Response(response.body, {
304
+ status: response.status,
305
+ statusText: response.statusText,
306
+ headers: new Headers(response.headers)
307
+ });
308
+ }
309
+ /**
310
+ * Build a redirect Response from a RedirectSignal.
311
+ *
312
+ * For RSC payload requests (client navigation), returns 204 + X-Timber-Redirect
313
+ * so the client router can perform a soft SPA redirect. A raw 302 would be
314
+ * turned into an opaque redirect by fetch({redirect:'manual'}), crashing
315
+ * createFromFetch. See design/19-client-navigation.md.
316
+ */
317
+ function buildRedirectResponse(signal, req, headers) {
318
+ if ((req.headers.get("Accept") ?? "").includes("text/x-component")) {
319
+ headers.set("X-Timber-Redirect", signal.location);
320
+ return new Response(null, {
321
+ status: 204,
322
+ headers
323
+ });
324
+ }
325
+ headers.set("Location", signal.location);
326
+ return new Response(null, {
327
+ status: signal.status,
328
+ headers
329
+ });
330
+ }
331
+ /**
332
+ * Fire the user's onRequestError hook with request context.
333
+ * Extracts request info from the Request object and calls the instrumentation hook.
334
+ */
335
+ async function fireOnRequestError(error, req, phase) {
336
+ const url = new URL(req.url);
337
+ const headersObj = {};
338
+ req.headers.forEach((v, k) => {
339
+ headersObj[k] = v;
340
+ });
341
+ await callOnRequestError(error, {
342
+ method: req.method,
343
+ path: url.pathname,
344
+ headers: headersObj
345
+ }, {
346
+ phase,
347
+ routePath: url.pathname,
348
+ routeType: "page",
349
+ traceId: getTraceId()
350
+ });
351
+ }
352
+ //#endregion
353
+ //#region src/server/proxy.ts
354
+ /**
355
+ * Run the proxy pipeline.
356
+ *
357
+ * @param proxyExport - The default export from proxy.ts (function or array)
358
+ * @param req - The incoming request
359
+ * @param next - The continuation that proceeds to route matching and rendering
360
+ * @returns The final response
361
+ */
362
+ async function runProxy(proxyExport, req, next) {
363
+ const fns = Array.isArray(proxyExport) ? proxyExport : [proxyExport];
364
+ let i = fns.length;
365
+ let composed = next;
366
+ while (i--) {
367
+ const fn = fns[i];
368
+ const downstream = composed;
369
+ composed = () => Promise.resolve(fn(req, downstream));
370
+ }
371
+ return composed();
372
+ }
373
+ //#endregion
374
+ //#region src/server/middleware-runner.ts
375
+ /**
376
+ * Run a route's middleware function.
377
+ *
378
+ * @param middlewareFn - The default export from the route's middleware.ts
379
+ * @param ctx - The middleware context (req, params, headers, requestHeaders, searchParams)
380
+ * @returns A Response if middleware short-circuited, or undefined to continue
381
+ */
382
+ async function runMiddleware(middlewareFn, ctx) {
383
+ const result = await middlewareFn(ctx);
384
+ if (result instanceof Response) return result;
385
+ }
386
+ /**
387
+ * Run all middleware functions in the segment chain, root to leaf.
388
+ *
389
+ * Execution is top-down: root middleware runs first, leaf middleware runs last.
390
+ * All middleware share the same MiddlewareContext — a parent that sets
391
+ * ctx.requestHeaders makes it visible to child middleware and downstream components.
392
+ *
393
+ * Short-circuits on the first middleware that returns a Response.
394
+ * Remaining middleware in the chain do not execute.
395
+ *
396
+ * @param chain - Middleware functions ordered root-to-leaf
397
+ * @param ctx - Shared middleware context
398
+ * @returns A Response if any middleware short-circuited, or undefined to continue
399
+ */
400
+ async function runMiddlewareChain(chain, ctx) {
401
+ for (const fn of chain) {
402
+ const result = await fn(ctx);
403
+ if (result instanceof Response) return result;
404
+ }
405
+ }
406
+ //#endregion
407
+ //#region src/server/metadata-social.ts
408
+ /**
409
+ * Render Open Graph metadata into head element descriptors.
410
+ *
411
+ * Handles og:title, og:description, og:image (with dimensions/alt),
412
+ * og:video, og:audio, og:article:author, and other OG properties.
413
+ */
414
+ function renderOpenGraph(og, elements) {
415
+ const simpleProps = [
416
+ ["og:title", og.title],
417
+ ["og:description", og.description],
418
+ ["og:url", og.url],
419
+ ["og:site_name", og.siteName],
420
+ ["og:locale", og.locale],
421
+ ["og:type", og.type],
422
+ ["og:article:published_time", og.publishedTime],
423
+ ["og:article:modified_time", og.modifiedTime]
424
+ ];
425
+ for (const [property, content] of simpleProps) if (content) elements.push({
426
+ tag: "meta",
427
+ attrs: {
428
+ property,
429
+ content
430
+ }
431
+ });
432
+ if (og.images) if (typeof og.images === "string") elements.push({
433
+ tag: "meta",
434
+ attrs: {
435
+ property: "og:image",
436
+ content: og.images
437
+ }
438
+ });
439
+ else {
440
+ const imgList = Array.isArray(og.images) ? og.images : [og.images];
441
+ for (const img of imgList) {
442
+ elements.push({
443
+ tag: "meta",
444
+ attrs: {
445
+ property: "og:image",
446
+ content: img.url
447
+ }
448
+ });
449
+ if (img.width) elements.push({
450
+ tag: "meta",
451
+ attrs: {
452
+ property: "og:image:width",
453
+ content: String(img.width)
454
+ }
455
+ });
456
+ if (img.height) elements.push({
457
+ tag: "meta",
458
+ attrs: {
459
+ property: "og:image:height",
460
+ content: String(img.height)
461
+ }
462
+ });
463
+ if (img.alt) elements.push({
464
+ tag: "meta",
465
+ attrs: {
466
+ property: "og:image:alt",
467
+ content: img.alt
468
+ }
469
+ });
470
+ }
471
+ }
472
+ if (og.videos) for (const video of og.videos) elements.push({
473
+ tag: "meta",
474
+ attrs: {
475
+ property: "og:video",
476
+ content: video.url
477
+ }
478
+ });
479
+ if (og.audio) for (const audio of og.audio) elements.push({
480
+ tag: "meta",
481
+ attrs: {
482
+ property: "og:audio",
483
+ content: audio.url
484
+ }
485
+ });
486
+ if (og.authors) for (const author of og.authors) elements.push({
487
+ tag: "meta",
488
+ attrs: {
489
+ property: "og:article:author",
490
+ content: author
491
+ }
492
+ });
493
+ }
494
+ /**
495
+ * Render Twitter Card metadata into head element descriptors.
496
+ *
497
+ * Handles twitter:card, twitter:site, twitter:title, twitter:image,
498
+ * twitter:player, and twitter:app (per-platform name/id/url).
499
+ */
500
+ function renderTwitter(tw, elements) {
501
+ const simpleProps = [
502
+ ["twitter:card", tw.card],
503
+ ["twitter:site", tw.site],
504
+ ["twitter:site:id", tw.siteId],
505
+ ["twitter:title", tw.title],
506
+ ["twitter:description", tw.description],
507
+ ["twitter:creator", tw.creator],
508
+ ["twitter:creator:id", tw.creatorId]
509
+ ];
510
+ for (const [name, content] of simpleProps) if (content) elements.push({
511
+ tag: "meta",
512
+ attrs: {
513
+ name,
514
+ content
515
+ }
516
+ });
517
+ if (tw.images) if (typeof tw.images === "string") elements.push({
518
+ tag: "meta",
519
+ attrs: {
520
+ name: "twitter:image",
521
+ content: tw.images
522
+ }
523
+ });
524
+ else {
525
+ const imgList = Array.isArray(tw.images) ? tw.images : [tw.images];
526
+ for (const img of imgList) {
527
+ const url = typeof img === "string" ? img : img.url;
528
+ elements.push({
529
+ tag: "meta",
530
+ attrs: {
531
+ name: "twitter:image",
532
+ content: url
533
+ }
534
+ });
535
+ }
536
+ }
537
+ if (tw.players) for (const player of tw.players) {
538
+ elements.push({
539
+ tag: "meta",
540
+ attrs: {
541
+ name: "twitter:player",
542
+ content: player.playerUrl
543
+ }
544
+ });
545
+ if (player.width) elements.push({
546
+ tag: "meta",
547
+ attrs: {
548
+ name: "twitter:player:width",
549
+ content: String(player.width)
550
+ }
551
+ });
552
+ if (player.height) elements.push({
553
+ tag: "meta",
554
+ attrs: {
555
+ name: "twitter:player:height",
556
+ content: String(player.height)
557
+ }
558
+ });
559
+ if (player.streamUrl) elements.push({
560
+ tag: "meta",
561
+ attrs: {
562
+ name: "twitter:player:stream",
563
+ content: player.streamUrl
564
+ }
565
+ });
566
+ }
567
+ if (tw.app) {
568
+ const platforms = [
569
+ ["iPhone", "iphone"],
570
+ ["iPad", "ipad"],
571
+ ["googlePlay", "googleplay"]
572
+ ];
573
+ if (tw.app.name) {
574
+ for (const [key, tag] of platforms) if (tw.app.id?.[key]) elements.push({
575
+ tag: "meta",
576
+ attrs: {
577
+ name: `twitter:app:name:${tag}`,
578
+ content: tw.app.name
579
+ }
580
+ });
581
+ }
582
+ for (const [key, tag] of platforms) {
583
+ const id = tw.app.id?.[key];
584
+ if (id) elements.push({
585
+ tag: "meta",
586
+ attrs: {
587
+ name: `twitter:app:id:${tag}`,
588
+ content: id
589
+ }
590
+ });
591
+ }
592
+ for (const [key, tag] of platforms) {
593
+ const url = tw.app.url?.[key];
594
+ if (url) elements.push({
595
+ tag: "meta",
596
+ attrs: {
597
+ name: `twitter:app:url:${tag}`,
598
+ content: url
599
+ }
600
+ });
601
+ }
602
+ }
603
+ }
604
+ //#endregion
605
+ //#region src/server/metadata-platform.ts
606
+ /**
607
+ * Render icon link elements (favicon, shortcut, apple-touch-icon, custom).
608
+ */
609
+ function renderIcons(icons, elements) {
610
+ if (icons.icon) {
611
+ if (typeof icons.icon === "string") elements.push({
612
+ tag: "link",
613
+ attrs: {
614
+ rel: "icon",
615
+ href: icons.icon
616
+ }
617
+ });
618
+ else if (Array.isArray(icons.icon)) for (const icon of icons.icon) {
619
+ const attrs = {
620
+ rel: "icon",
621
+ href: icon.url
622
+ };
623
+ if (icon.sizes) attrs.sizes = icon.sizes;
624
+ if (icon.type) attrs.type = icon.type;
625
+ elements.push({
626
+ tag: "link",
627
+ attrs
628
+ });
629
+ }
630
+ }
631
+ if (icons.shortcut) {
632
+ const urls = Array.isArray(icons.shortcut) ? icons.shortcut : [icons.shortcut];
633
+ for (const url of urls) elements.push({
634
+ tag: "link",
635
+ attrs: {
636
+ rel: "shortcut icon",
637
+ href: url
638
+ }
639
+ });
640
+ }
641
+ if (icons.apple) {
642
+ if (typeof icons.apple === "string") elements.push({
643
+ tag: "link",
644
+ attrs: {
645
+ rel: "apple-touch-icon",
646
+ href: icons.apple
647
+ }
648
+ });
649
+ else if (Array.isArray(icons.apple)) for (const icon of icons.apple) {
650
+ const attrs = {
651
+ rel: "apple-touch-icon",
652
+ href: icon.url
653
+ };
654
+ if (icon.sizes) attrs.sizes = icon.sizes;
655
+ elements.push({
656
+ tag: "link",
657
+ attrs
658
+ });
659
+ }
660
+ }
661
+ if (icons.other) for (const icon of icons.other) {
662
+ const attrs = {
663
+ rel: icon.rel,
664
+ href: icon.url
665
+ };
666
+ if (icon.sizes) attrs.sizes = icon.sizes;
667
+ if (icon.type) attrs.type = icon.type;
668
+ elements.push({
669
+ tag: "link",
670
+ attrs
671
+ });
672
+ }
673
+ }
674
+ /**
675
+ * Render alternate link elements (canonical, hreflang, media, types).
676
+ */
677
+ function renderAlternates(alternates, elements) {
678
+ if (alternates.canonical) elements.push({
679
+ tag: "link",
680
+ attrs: {
681
+ rel: "canonical",
682
+ href: alternates.canonical
683
+ }
684
+ });
685
+ if (alternates.languages) for (const [lang, href] of Object.entries(alternates.languages)) elements.push({
686
+ tag: "link",
687
+ attrs: {
688
+ rel: "alternate",
689
+ hreflang: lang,
690
+ href
691
+ }
692
+ });
693
+ if (alternates.media) for (const [media, href] of Object.entries(alternates.media)) elements.push({
694
+ tag: "link",
695
+ attrs: {
696
+ rel: "alternate",
697
+ media,
698
+ href
699
+ }
700
+ });
701
+ if (alternates.types) for (const [type, href] of Object.entries(alternates.types)) elements.push({
702
+ tag: "link",
703
+ attrs: {
704
+ rel: "alternate",
705
+ type,
706
+ href
707
+ }
708
+ });
709
+ }
710
+ /**
711
+ * Render site verification meta tags (Google, Yahoo, Yandex, custom).
712
+ */
713
+ function renderVerification(verification, elements) {
714
+ const verificationProps = [
715
+ ["google-site-verification", verification.google],
716
+ ["y_key", verification.yahoo],
717
+ ["yandex-verification", verification.yandex]
718
+ ];
719
+ for (const [name, content] of verificationProps) if (content) elements.push({
720
+ tag: "meta",
721
+ attrs: {
722
+ name,
723
+ content
724
+ }
725
+ });
726
+ if (verification.other) for (const [name, value] of Object.entries(verification.other)) {
727
+ const content = Array.isArray(value) ? value.join(", ") : value;
728
+ elements.push({
729
+ tag: "meta",
730
+ attrs: {
731
+ name,
732
+ content
733
+ }
734
+ });
735
+ }
736
+ }
737
+ /**
738
+ * Render Apple Web App meta tags and startup image links.
739
+ */
740
+ function renderAppleWebApp(appleWebApp, elements) {
741
+ if (appleWebApp.capable) elements.push({
742
+ tag: "meta",
743
+ attrs: {
744
+ name: "apple-mobile-web-app-capable",
745
+ content: "yes"
746
+ }
747
+ });
748
+ if (appleWebApp.title) elements.push({
749
+ tag: "meta",
750
+ attrs: {
751
+ name: "apple-mobile-web-app-title",
752
+ content: appleWebApp.title
753
+ }
754
+ });
755
+ if (appleWebApp.statusBarStyle) elements.push({
756
+ tag: "meta",
757
+ attrs: {
758
+ name: "apple-mobile-web-app-status-bar-style",
759
+ content: appleWebApp.statusBarStyle
760
+ }
761
+ });
762
+ if (appleWebApp.startupImage) {
763
+ const images = Array.isArray(appleWebApp.startupImage) ? appleWebApp.startupImage : [{ url: appleWebApp.startupImage }];
764
+ for (const img of images) {
765
+ const attrs = {
766
+ rel: "apple-touch-startup-image",
767
+ href: typeof img === "string" ? img : img.url
768
+ };
769
+ if (typeof img === "object" && img.media) attrs.media = img.media;
770
+ elements.push({
771
+ tag: "link",
772
+ attrs
773
+ });
774
+ }
775
+ }
776
+ }
777
+ /**
778
+ * Render App Links (al:*) meta tags for deep linking across platforms.
779
+ */
780
+ function renderAppLinks(appLinks, elements) {
781
+ const platformEntries = [
782
+ ["ios", appLinks.ios],
783
+ ["android", appLinks.android],
784
+ ["windows", appLinks.windows],
785
+ ["windows_phone", appLinks.windowsPhone],
786
+ ["windows_universal", appLinks.windowsUniversal]
787
+ ];
788
+ for (const [platform, entries] of platformEntries) {
789
+ if (!entries) continue;
790
+ for (const entry of entries) for (const [key, value] of Object.entries(entry)) if (value !== void 0 && value !== null) elements.push({
791
+ tag: "meta",
792
+ attrs: {
793
+ property: `al:${platform}:${key}`,
794
+ content: String(value)
795
+ }
796
+ });
797
+ }
798
+ if (appLinks.web) {
799
+ if (appLinks.web.url) elements.push({
800
+ tag: "meta",
801
+ attrs: {
802
+ property: "al:web:url",
803
+ content: appLinks.web.url
804
+ }
805
+ });
806
+ if (appLinks.web.shouldFallback !== void 0) elements.push({
807
+ tag: "meta",
808
+ attrs: {
809
+ property: "al:web:should_fallback",
810
+ content: appLinks.web.shouldFallback ? "true" : "false"
811
+ }
812
+ });
813
+ }
814
+ }
815
+ /**
816
+ * Render Apple iTunes smart banner meta tag.
817
+ */
818
+ function renderItunes(itunes, elements) {
819
+ const parts = [`app-id=${itunes.appId}`];
820
+ if (itunes.affiliateData) parts.push(`affiliate-data=${itunes.affiliateData}`);
821
+ if (itunes.appArgument) parts.push(`app-argument=${itunes.appArgument}`);
822
+ elements.push({
823
+ tag: "meta",
824
+ attrs: {
825
+ name: "apple-itunes-app",
826
+ content: parts.join(", ")
827
+ }
828
+ });
829
+ }
830
+ //#endregion
831
+ //#region src/server/metadata-render.ts
832
+ /**
833
+ * Convert resolved metadata into an array of head element descriptors.
834
+ *
835
+ * Each descriptor has a `tag` ('title', 'meta', 'link') and either
836
+ * `content` (for <title>) or `attrs` (for <meta>/<link>).
837
+ *
838
+ * The framework's MetadataResolver component consumes these descriptors
839
+ * and renders them into the <head>.
840
+ */
841
+ function renderMetadataToElements(metadata) {
842
+ const elements = [];
843
+ if (typeof metadata.title === "string") elements.push({
844
+ tag: "title",
845
+ content: metadata.title
846
+ });
847
+ const simpleMetaProps = [
848
+ ["description", metadata.description],
849
+ ["generator", metadata.generator],
850
+ ["application-name", metadata.applicationName],
851
+ ["referrer", metadata.referrer],
852
+ ["category", metadata.category],
853
+ ["creator", metadata.creator],
854
+ ["publisher", metadata.publisher]
855
+ ];
856
+ for (const [name, content] of simpleMetaProps) if (content) elements.push({
857
+ tag: "meta",
858
+ attrs: {
859
+ name,
860
+ content
861
+ }
862
+ });
863
+ if (metadata.keywords) {
864
+ const content = Array.isArray(metadata.keywords) ? metadata.keywords.join(", ") : metadata.keywords;
865
+ elements.push({
866
+ tag: "meta",
867
+ attrs: {
868
+ name: "keywords",
869
+ content
870
+ }
871
+ });
872
+ }
873
+ if (metadata.robots) {
874
+ const content = typeof metadata.robots === "string" ? metadata.robots : renderRobotsObject(metadata.robots);
875
+ elements.push({
876
+ tag: "meta",
877
+ attrs: {
878
+ name: "robots",
879
+ content
880
+ }
881
+ });
882
+ if (typeof metadata.robots === "object" && metadata.robots.googleBot) {
883
+ const gbContent = typeof metadata.robots.googleBot === "string" ? metadata.robots.googleBot : renderRobotsObject(metadata.robots.googleBot);
884
+ elements.push({
885
+ tag: "meta",
886
+ attrs: {
887
+ name: "googlebot",
888
+ content: gbContent
889
+ }
890
+ });
891
+ }
892
+ }
893
+ if (metadata.openGraph) renderOpenGraph(metadata.openGraph, elements);
894
+ if (metadata.twitter) renderTwitter(metadata.twitter, elements);
895
+ if (metadata.icons) renderIcons(metadata.icons, elements);
896
+ if (metadata.manifest) elements.push({
897
+ tag: "link",
898
+ attrs: {
899
+ rel: "manifest",
900
+ href: metadata.manifest
901
+ }
902
+ });
903
+ if (metadata.alternates) renderAlternates(metadata.alternates, elements);
904
+ if (metadata.verification) renderVerification(metadata.verification, elements);
905
+ if (metadata.formatDetection) {
906
+ const parts = [];
907
+ if (metadata.formatDetection.telephone === false) parts.push("telephone=no");
908
+ if (metadata.formatDetection.email === false) parts.push("email=no");
909
+ if (metadata.formatDetection.address === false) parts.push("address=no");
910
+ if (parts.length > 0) elements.push({
911
+ tag: "meta",
912
+ attrs: {
913
+ name: "format-detection",
914
+ content: parts.join(", ")
915
+ }
916
+ });
917
+ }
918
+ if (metadata.authors) {
919
+ const authorList = Array.isArray(metadata.authors) ? metadata.authors : [metadata.authors];
920
+ for (const author of authorList) {
921
+ if (author.name) elements.push({
922
+ tag: "meta",
923
+ attrs: {
924
+ name: "author",
925
+ content: author.name
926
+ }
927
+ });
928
+ if (author.url) elements.push({
929
+ tag: "link",
930
+ attrs: {
931
+ rel: "author",
932
+ href: author.url
933
+ }
934
+ });
935
+ }
936
+ }
937
+ if (metadata.appleWebApp) renderAppleWebApp(metadata.appleWebApp, elements);
938
+ if (metadata.appLinks) renderAppLinks(metadata.appLinks, elements);
939
+ if (metadata.itunes) renderItunes(metadata.itunes, elements);
940
+ if (metadata.other) for (const [name, value] of Object.entries(metadata.other)) {
941
+ const content = Array.isArray(value) ? value.join(", ") : value;
942
+ elements.push({
943
+ tag: "meta",
944
+ attrs: {
945
+ name,
946
+ content
947
+ }
948
+ });
949
+ }
950
+ return elements;
951
+ }
952
+ function renderRobotsObject(robots) {
953
+ const parts = [];
954
+ if (robots.index === true) parts.push("index");
955
+ if (robots.index === false) parts.push("noindex");
956
+ if (robots.follow === true) parts.push("follow");
957
+ if (robots.follow === false) parts.push("nofollow");
958
+ return parts.join(", ");
959
+ }
960
+ //#endregion
961
+ //#region src/server/metadata.ts
962
+ /**
963
+ * Resolve a title value with an optional template.
964
+ *
965
+ * - string → apply template if present
966
+ * - { absolute: '...' } → use as-is, skip template
967
+ * - { default: '...' } → use as fallback (no template applied)
968
+ * - undefined → undefined
969
+ */
970
+ function resolveTitle(title, template) {
971
+ if (title === void 0 || title === null) return;
972
+ if (typeof title === "string") return template ? template.replace("%s", title) : title;
973
+ if (title.absolute !== void 0) return title.absolute;
974
+ if (title.default !== void 0) return title.default;
975
+ }
976
+ /**
977
+ * Resolve metadata from a segment chain.
978
+ *
979
+ * Processes entries from root layout to page (in segment order).
980
+ * The merge algorithm:
981
+ * 1. Shallow-merge all keys except title (later wins)
982
+ * 2. Track the most recent title template
983
+ * 3. Resolve the final title using the template
984
+ *
985
+ * In error state, the page entry is dropped and noindex is injected.
986
+ *
987
+ * See design/16-metadata.md §"Merge Algorithm"
988
+ */
989
+ function resolveMetadata(entries, options = {}) {
990
+ const { errorState = false } = options;
991
+ const merged = {};
992
+ let titleTemplate;
993
+ let lastDefault;
994
+ let rawTitle;
995
+ for (const { metadata, isPage } of entries) {
996
+ if (errorState && isPage) continue;
997
+ if (metadata.title !== void 0 && typeof metadata.title === "object") {
998
+ if (metadata.title.template !== void 0) titleTemplate = metadata.title.template;
999
+ if (metadata.title.default !== void 0) lastDefault = metadata.title.default;
1000
+ }
1001
+ for (const key of Object.keys(metadata)) {
1002
+ if (key === "title") continue;
1003
+ merged[key] = metadata[key];
1004
+ }
1005
+ if (metadata.title !== void 0) rawTitle = metadata.title;
1006
+ }
1007
+ if (errorState) {
1008
+ rawTitle = lastDefault !== void 0 ? { default: lastDefault } : rawTitle;
1009
+ titleTemplate = void 0;
1010
+ }
1011
+ const resolvedTitle = resolveTitle(rawTitle, titleTemplate);
1012
+ if (resolvedTitle !== void 0) merged.title = resolvedTitle;
1013
+ if (errorState) merged.robots = "noindex";
1014
+ return merged;
1015
+ }
1016
+ /**
1017
+ * Check if a string is an absolute URL.
1018
+ */
1019
+ function isAbsoluteUrl(url) {
1020
+ return url.startsWith("http://") || url.startsWith("https://") || url.startsWith("//");
1021
+ }
1022
+ /**
1023
+ * Resolve a relative URL against a base URL.
1024
+ */
1025
+ function resolveUrl(url, base) {
1026
+ if (isAbsoluteUrl(url)) return url;
1027
+ return new URL(url, base).toString();
1028
+ }
1029
+ /**
1030
+ * Resolve relative URLs in metadata fields against metadataBase.
1031
+ *
1032
+ * Returns a new metadata object with URLs resolved. Absolute URLs are not modified.
1033
+ * If metadataBase is not set, returns the metadata unchanged.
1034
+ */
1035
+ function resolveMetadataUrls(metadata) {
1036
+ const base = metadata.metadataBase;
1037
+ if (!base) return metadata;
1038
+ const result = { ...metadata };
1039
+ if (result.openGraph) {
1040
+ result.openGraph = { ...result.openGraph };
1041
+ if (typeof result.openGraph.images === "string") result.openGraph.images = resolveUrl(result.openGraph.images, base);
1042
+ else if (Array.isArray(result.openGraph.images)) result.openGraph.images = result.openGraph.images.map((img) => ({
1043
+ ...img,
1044
+ url: resolveUrl(img.url, base)
1045
+ }));
1046
+ else if (result.openGraph.images) result.openGraph.images = {
1047
+ ...result.openGraph.images,
1048
+ url: resolveUrl(result.openGraph.images.url, base)
1049
+ };
1050
+ if (result.openGraph.url && !isAbsoluteUrl(result.openGraph.url)) result.openGraph.url = resolveUrl(result.openGraph.url, base);
1051
+ }
1052
+ if (result.twitter) {
1053
+ result.twitter = { ...result.twitter };
1054
+ if (typeof result.twitter.images === "string") result.twitter.images = resolveUrl(result.twitter.images, base);
1055
+ else if (Array.isArray(result.twitter.images)) {
1056
+ const resolved = result.twitter.images.map((img) => typeof img === "string" ? resolveUrl(img, base) : {
1057
+ ...img,
1058
+ url: resolveUrl(img.url, base)
1059
+ });
1060
+ const allStrings = resolved.every((r) => typeof r === "string");
1061
+ result.twitter.images = allStrings ? resolved : resolved;
1062
+ } else if (result.twitter.images) result.twitter.images = {
1063
+ ...result.twitter.images,
1064
+ url: resolveUrl(result.twitter.images.url, base)
1065
+ };
1066
+ }
1067
+ if (result.alternates) {
1068
+ result.alternates = { ...result.alternates };
1069
+ if (result.alternates.canonical && !isAbsoluteUrl(result.alternates.canonical)) result.alternates.canonical = resolveUrl(result.alternates.canonical, base);
1070
+ if (result.alternates.languages) {
1071
+ const langs = {};
1072
+ for (const [lang, url] of Object.entries(result.alternates.languages)) langs[lang] = isAbsoluteUrl(url) ? url : resolveUrl(url, base);
1073
+ result.alternates.languages = langs;
1074
+ }
1075
+ }
1076
+ if (result.icons) {
1077
+ result.icons = { ...result.icons };
1078
+ if (typeof result.icons.icon === "string") result.icons.icon = resolveUrl(result.icons.icon, base);
1079
+ else if (Array.isArray(result.icons.icon)) result.icons.icon = result.icons.icon.map((i) => ({
1080
+ ...i,
1081
+ url: resolveUrl(i.url, base)
1082
+ }));
1083
+ if (typeof result.icons.apple === "string") result.icons.apple = resolveUrl(result.icons.apple, base);
1084
+ else if (Array.isArray(result.icons.apple)) result.icons.apple = result.icons.apple.map((i) => ({
1085
+ ...i,
1086
+ url: resolveUrl(i.url, base)
1087
+ }));
1088
+ }
1089
+ return result;
1090
+ }
1091
+ //#endregion
1092
+ //#region src/client/error-reconstituter.tsx
1093
+ /**
1094
+ * Reconstitutes a SerializableError into a real Error instance before
1095
+ * passing to the user's error component.
1096
+ *
1097
+ * TSX error pages are 'use client' components that receive { error: Error, digest, reset }.
1098
+ * Error objects are not RSC-serializable (React Flight throws "Only plain objects
1099
+ * can be passed to Client Components"). This wrapper receives the error as a plain
1100
+ * SerializableError object, reconstitutes a real Error instance, and passes it
1101
+ * to the user's error component — ensuring error instanceof Error works correctly.
1102
+ *
1103
+ * See design/spike-TIM-565-unify-error-pages.md §"Edge Case B"
1104
+ * See design/10-error-handling.md §"RSC → SSR for Error Pages via SerializableError"
1105
+ */
1106
+ /**
1107
+ * Reconstitute a SerializableError into a real Error instance and render
1108
+ * the user's error component with the proper props.
1109
+ */
1110
+ function ErrorReconstituter({ error: serialized, digest, reset, component, status, dangerouslyPassData }) {
1111
+ return createElement(component, {
1112
+ error: Object.assign(new Error(serialized.message), {
1113
+ name: serialized.name,
1114
+ ...serialized.stack != null ? { stack: serialized.stack } : {}
1115
+ }),
1116
+ digest,
1117
+ reset,
1118
+ ...status != null ? {
1119
+ status,
1120
+ dangerouslyPassData
1121
+ } : {}
1122
+ });
1123
+ }
1124
+ //#endregion
1125
+ //#region src/server/safe-load.ts
1126
+ /**
1127
+ * Custom error class for module load failures.
1128
+ *
1129
+ * Preserves the original error as `cause` while providing a
1130
+ * human-readable message with the file path.
1131
+ */
1132
+ var ModuleLoadError = class extends Error {
1133
+ /** The file path that failed to load. */
1134
+ filePath;
1135
+ constructor(filePath, cause) {
1136
+ const originalMessage = cause instanceof Error ? cause.message : String(cause);
1137
+ super(`[timber] Failed to load module ${filePath}\n ${originalMessage}`, { cause });
1138
+ this.name = "ModuleLoadError";
1139
+ this.filePath = filePath;
1140
+ }
1141
+ };
1142
+ /**
1143
+ * Load a route manifest module with enriched error context.
1144
+ *
1145
+ * On success: returns the module object (same as `loader.load()`).
1146
+ * On failure: throws `ModuleLoadError` with file path and original cause.
1147
+ *
1148
+ * For error rendering paths that need fallthrough instead of throwing,
1149
+ * callers should catch at the call site:
1150
+ *
1151
+ * ```ts
1152
+ * // Throwing (default) — route-element-builder, api-handler, etc.
1153
+ * const mod = await loadModule(segment.page);
1154
+ *
1155
+ * // Fallthrough — error-renderer, error-boundary-wrapper
1156
+ * const mod = await loadModule(segment.error).catch(() => null);
1157
+ * ```
1158
+ */
1159
+ async function loadModule(loader) {
1160
+ try {
1161
+ return await loader.load();
1162
+ } catch (error) {
1163
+ throw new ModuleLoadError(loader.filePath, error);
1164
+ }
1165
+ }
1166
+ /** Reverse index: status code → legacy file name. Built once at module load. */
1167
+ var STATUS_TO_LEGACY_FILE = Object.fromEntries(Object.entries({
1168
+ "not-found": 404,
1169
+ "forbidden": 403,
1170
+ "unauthorized": 401
1171
+ }).map(([name, status]) => [status, name]));
1172
+ /**
1173
+ * Look up `{statusStr}` then `{categoryKey}` (e.g. "4xx" / "5xx") in a
1174
+ * status-file group on a single segment. Shared by all three fallback
1175
+ * chains — the only structural difference between component 4xx,
1176
+ * component 5xx, and JSON resolution is *which* group is searched and
1177
+ * how the per-segment loop is layered around it.
1178
+ */
1179
+ function lookupInGroup(group, statusStr, categoryKey, segmentIndex, status) {
1180
+ if (!group) return null;
1181
+ const exact = group[statusStr];
1182
+ if (exact) return {
1183
+ file: exact,
1184
+ status,
1185
+ kind: "exact",
1186
+ segmentIndex
1187
+ };
1188
+ const category = group[categoryKey];
1189
+ if (category) return {
1190
+ file: category,
1191
+ status,
1192
+ kind: "category",
1193
+ segmentIndex
1194
+ };
1195
+ return null;
1196
+ }
1197
+ /**
1198
+ * Look up the legacy convention file (`not-found.tsx` / `forbidden.tsx` /
1199
+ * `unauthorized.tsx`) for `status` on a single segment. Returns null if
1200
+ * `status` has no legacy mapping or the file isn't present.
1201
+ */
1202
+ function lookupLegacy(group, status, segmentIndex) {
1203
+ if (!group) return null;
1204
+ const name = STATUS_TO_LEGACY_FILE[status];
1205
+ if (!name) return null;
1206
+ const file = group[name];
1207
+ return file ? {
1208
+ file,
1209
+ status,
1210
+ kind: "legacy",
1211
+ segmentIndex
1212
+ } : null;
1213
+ }
1214
+ /**
1215
+ * Resolve the status-code file to render for a given HTTP status code.
1216
+ *
1217
+ * Walks the segment chain from leaf to root following the fallback chain
1218
+ * defined in design/10-error-handling.md. Returns null if no file is found
1219
+ * (caller should render the framework default).
1220
+ *
1221
+ * @param status - The HTTP status code (4xx or 5xx).
1222
+ * @param segments - The matched segment chain from root (index 0) to leaf (last).
1223
+ * @param format - The response format family ('component' or 'json'). Defaults to 'component'.
1224
+ */
1225
+ function resolveStatusFile(status, segments, format = "component") {
1226
+ if (status < 400 || status > 599) return null;
1227
+ if (format === "json") return resolveJson(status, segments);
1228
+ if (status <= 499) return resolve4xx(status, segments);
1229
+ return resolve5xx(status, segments);
1230
+ }
1231
+ /**
1232
+ * 4xx component fallback chain — three separate full passes leaf→root.
1233
+ *
1234
+ * The passes must be separate (not interleaved per-segment) so that a
1235
+ * root-level `404.tsx` beats a leaf-level `error.tsx`. The 5xx chain
1236
+ * inverts this and is per-segment: a leaf's `error.tsx` beats a root's
1237
+ * `5xx.tsx`. This asymmetry is the only reason these two functions exist
1238
+ * separately.
1239
+ *
1240
+ * Pass 1 — {status}.tsx → 4xx.tsx (statusFiles)
1241
+ * Pass 2 — not-found / forbidden / unauthorized (legacyStatusFiles)
1242
+ * Pass 3 — error.tsx (error)
1243
+ */
1244
+ function resolve4xx(status, segments) {
1245
+ const statusStr = String(status);
1246
+ for (let i = segments.length - 1; i >= 0; i--) {
1247
+ const r = lookupInGroup(segments[i].statusFiles, statusStr, "4xx", i, status);
1248
+ if (r) return r;
1249
+ }
1250
+ for (let i = segments.length - 1; i >= 0; i--) {
1251
+ const r = lookupLegacy(segments[i].legacyStatusFiles, status, i);
1252
+ if (r) return r;
1253
+ }
1254
+ for (let i = segments.length - 1; i >= 0; i--) {
1255
+ const errorFile = segments[i].error;
1256
+ if (errorFile) return {
1257
+ file: errorFile,
1258
+ status,
1259
+ kind: "error",
1260
+ segmentIndex: i
1261
+ };
1262
+ }
1263
+ return null;
1264
+ }
1265
+ /**
1266
+ * 5xx component fallback chain — single pass, per-segment leaf→root.
1267
+ *
1268
+ * At each segment: {status}.tsx → 5xx.tsx → error.tsx. A leaf's
1269
+ * `error.tsx` therefore beats a root's `5xx.tsx`, which is the
1270
+ * intentional inverse of the 4xx chain.
1271
+ */
1272
+ function resolve5xx(status, segments) {
1273
+ const statusStr = String(status);
1274
+ for (let i = segments.length - 1; i >= 0; i--) {
1275
+ const segment = segments[i];
1276
+ const r = lookupInGroup(segment.statusFiles, statusStr, "5xx", i, status);
1277
+ if (r) return r;
1278
+ if (segment.error) return {
1279
+ file: segment.error,
1280
+ status,
1281
+ kind: "error",
1282
+ segmentIndex: i
1283
+ };
1284
+ }
1285
+ return null;
1286
+ }
1287
+ /**
1288
+ * JSON fallback chain (for both 4xx and 5xx) — single pass leaf→root.
1289
+ *
1290
+ * At each segment: {status}.json → {category}.json. No legacy compat,
1291
+ * no error.tsx — the JSON chain terminates at the category catch-all
1292
+ * and the caller falls back to a bare-JSON framework default.
1293
+ */
1294
+ function resolveJson(status, segments) {
1295
+ const statusStr = String(status);
1296
+ const categoryKey = status >= 500 ? "5xx" : "4xx";
1297
+ for (let i = segments.length - 1; i >= 0; i--) {
1298
+ const r = lookupInGroup(segments[i].jsonStatusFiles, statusStr, categoryKey, i, status);
1299
+ if (r) return r;
1300
+ }
1301
+ return null;
1302
+ }
1303
+ /**
1304
+ * Resolve the denial file for a parallel route slot.
1305
+ *
1306
+ * Slot denial is graceful degradation — no HTTP status on the wire.
1307
+ * Fallback chain: denied.tsx → default.tsx → null.
1308
+ *
1309
+ * @param slotNode - The segment node for the slot (segmentType === 'slot').
1310
+ */
1311
+ function resolveSlotDenied(slotNode) {
1312
+ const slotName = slotNode.segmentName.replace(/^@/, "");
1313
+ if (slotNode.denied) return {
1314
+ file: slotNode.denied,
1315
+ slotName,
1316
+ kind: "denied"
1317
+ };
1318
+ if (slotNode.default) return {
1319
+ file: slotNode.default,
1320
+ slotName,
1321
+ kind: "default"
1322
+ };
1323
+ return null;
1324
+ }
1325
+ //#endregion
1326
+ //#region src/server/deny-boundary.ts
1327
+ /**
1328
+ * Deny boundary subsystem — the in-tree DenySignal flow.
1329
+ *
1330
+ * Three things live together here because they form a single flow:
1331
+ *
1332
+ * 1. **Chain construction** (`buildDenyPageChain`) — walks the matched
1333
+ * segment chain at element-tree build time and produces a list of
1334
+ * `DenyPageEntry` records ordered by specificity (specific status →
1335
+ * category catch-all → `error.tsx`).
1336
+ *
1337
+ * 2. **Runtime matching** (`renderMatchingDenyPage`) — picks the first
1338
+ * chain entry whose status filter matches the thrown DenySignal and
1339
+ * returns a React element for the matching component. Used by
1340
+ * `AccessGate` and `PageDenyBoundary` when they catch a deny.
1341
+ *
1342
+ * 3. **The page boundary itself** (`PageDenyBoundary`) — the async server
1343
+ * component that wraps a server-component page, calls it, and catches
1344
+ * `DenySignal` so the deny page renders in-tree (no throw reaches
1345
+ * React Flight, single render pass).
1346
+ *
1347
+ * Plus the ALS helpers (`setDenyStatus` / `getDenyStatus`) the boundary
1348
+ * uses to thread the matched status code back to the pipeline so the
1349
+ * HTTP status reflects the deny.
1350
+ *
1351
+ * Folded into one module from the former `deny-page-resolver.ts` and
1352
+ * `page-deny-boundary.tsx` (TIM-853) — the names were misleading and the
1353
+ * three pieces only made sense together.
1354
+ *
1355
+ * See design/04-authorization.md, design/10-error-handling.md, TIM-666.
1356
+ */
1357
+ /**
1358
+ * Find the first deny page in the chain that matches the given status code.
1359
+ * Returns a React element for the matching component, or null if no match.
1360
+ */
1361
+ function renderMatchingDenyPage(chain, status, data) {
1362
+ for (const entry of chain) {
1363
+ if (entry.status === status) return renderDenyEntry(entry, status, data);
1364
+ if (entry.status === 400 && status >= 400 && status <= 499) return renderDenyEntry(entry, status, data);
1365
+ if (entry.status === 500 && status >= 500 && status <= 599) return renderDenyEntry(entry, status, data);
1366
+ if (entry.status === null) return renderDenyEntry(entry, status, data);
1367
+ }
1368
+ return null;
1369
+ }
1370
+ /**
1371
+ * Build the element for a matched deny chain entry with the props contract
1372
+ * the file's convention documents:
1373
+ *
1374
+ * - Status-code and legacy files: { status, dangerouslyPassData }
1375
+ * - error.tsx (TSX): { error, digest, reset } via ErrorReconstituter, plus
1376
+ * { status, dangerouslyPassData } for dual-shape implementations. Without
1377
+ * the Error prop, any error.tsx written per the docs (reading
1378
+ * error.message) crashes — turning a clean deny(404) into a 500. TIM-1081.
1379
+ * - error.mdx: plain { status } — server component, static content.
1380
+ */
1381
+ function renderDenyEntry(entry, status, data) {
1382
+ const h = createElement;
1383
+ if (entry.kind !== "error") return h(entry.component, {
1384
+ status,
1385
+ dangerouslyPassData: data
1386
+ });
1387
+ if (entry.isMdx) return h(entry.component, { status });
1388
+ return h(ErrorReconstituter, {
1389
+ error: {
1390
+ message: `Access denied with status ${status}`,
1391
+ name: "DenySignal"
1392
+ },
1393
+ digest: null,
1394
+ reset: void 0,
1395
+ component: entry.component,
1396
+ status,
1397
+ dangerouslyPassData: data
1398
+ });
1399
+ }
1400
+ /**
1401
+ * Set the deny status in the request context ALS.
1402
+ * Called from AccessGate / PageDenyBoundary when a DenySignal is caught.
1403
+ * The pipeline reads this after render to set the HTTP status code.
1404
+ */
1405
+ function setDenyStatus(status) {
1406
+ const store = requestContextAls.getStore();
1407
+ if (store) {
1408
+ store.denyStatus = status;
1409
+ store.onDenyStatus?.();
1410
+ }
1411
+ }
1412
+ //#endregion
1413
+ //#region src/server/access-gate.tsx
1414
+ /**
1415
+ * AccessGate and SlotAccessGate — framework-injected async server components.
1416
+ *
1417
+ * AccessGate wraps each segment's layout in the element tree. It calls the
1418
+ * segment's access.ts before the layout renders. If access.ts calls deny()
1419
+ * or redirect(), the signal propagates as a render-phase throw — caught by
1420
+ * the flush controller to produce the correct HTTP status code.
1421
+ *
1422
+ * SlotAccessGate wraps parallel slot content. On denial, it renders the
1423
+ * graceful degradation chain: denied.tsx → default.tsx → null. Slot denial
1424
+ * does not affect the HTTP status code.
1425
+ *
1426
+ * See design/04-authorization.md and design/02-rendering-pipeline.md §"AccessGate"
1427
+ */
1428
+ /**
1429
+ * Framework-injected access gate for segments.
1430
+ *
1431
+ * When a pre-computed `verdict` prop is provided (from the pre-render pass
1432
+ * in route-element-builder.ts), AccessGate replays it synchronously — no
1433
+ * async, no re-execution of access.ts, immune to Suspense timing. The OTEL
1434
+ * span was already emitted during the pre-render pass.
1435
+ *
1436
+ * When no verdict is provided (backward compat with tree-builder.ts),
1437
+ * AccessGate calls accessFn directly with OTEL instrumentation.
1438
+ *
1439
+ * access.ts is a pure gate — return values are discarded. The layout below
1440
+ * gets the same data by calling the same cached functions (React.cache dedup).
1441
+ */
1442
+ function AccessGate(props) {
1443
+ const { accessFn, segmentName, verdict, denyPages, children } = props;
1444
+ if (verdict !== void 0) {
1445
+ if (verdict === "pass") return children;
1446
+ if (verdict instanceof DenySignal && denyPages) {
1447
+ const denyElement = renderMatchingDenyPage(denyPages, verdict.status, verdict.data);
1448
+ if (denyElement) {
1449
+ setDenyStatus(verdict.status);
1450
+ return denyElement;
1451
+ }
1452
+ }
1453
+ throw verdict;
1454
+ }
1455
+ return accessGateFallback(accessFn, segmentName, denyPages, children);
1456
+ }
1457
+ /**
1458
+ * Async fallback for AccessGate when no pre-computed verdict is available.
1459
+ * Calls accessFn with OTEL instrumentation.
1460
+ */
1461
+ async function accessGateFallback(accessFn, segmentName, denyPages, children) {
1462
+ try {
1463
+ await withSpan("timber.access", { "timber.segment": segmentName ?? "unknown" }, async () => {
1464
+ try {
1465
+ await accessFn();
1466
+ await setSpanAttribute("timber.result", "pass");
1467
+ } catch (error) {
1468
+ if (error instanceof DenySignal) {
1469
+ await setSpanAttribute("timber.result", "deny");
1470
+ await setSpanAttribute("timber.deny_status", error.status);
1471
+ if (error.sourceFile) await setSpanAttribute("timber.deny_file", error.sourceFile);
1472
+ } else if (error instanceof RedirectSignal) await setSpanAttribute("timber.result", "redirect");
1473
+ throw error;
1474
+ }
1475
+ });
1476
+ } catch (error) {
1477
+ if (error instanceof DenySignal && denyPages) {
1478
+ const denyElement = renderMatchingDenyPage(denyPages, error.status, error.data);
1479
+ if (denyElement) {
1480
+ setDenyStatus(error.status);
1481
+ return denyElement;
1482
+ }
1483
+ }
1484
+ throw error;
1485
+ }
1486
+ return children;
1487
+ }
1488
+ /**
1489
+ * Framework-injected access gate for parallel slots.
1490
+ *
1491
+ * On denial, graceful degradation: denied.tsx → default.tsx → null.
1492
+ * The HTTP status code is unaffected — slot denial is a UI concern, not
1493
+ * a protocol concern. The parent layout and sibling slots still render.
1494
+ *
1495
+ * DeniedComponent is passed instead of a pre-built element so that
1496
+ * DenySignal.data can be forwarded as the dangerouslyPassData prop
1497
+ * and the slot name can be passed as the slot prop. See TIM-488.
1498
+ *
1499
+ * redirect() in slot access.ts is a dev-mode error — redirecting from a
1500
+ * slot doesn't make architectural sense.
1501
+ */
1502
+ async function SlotAccessGate(props) {
1503
+ const { accessFn, DeniedComponent, slotName, createElement, defaultFallback, children } = props;
1504
+ try {
1505
+ await accessFn();
1506
+ } catch (error) {
1507
+ if (error instanceof DenySignal) return buildDeniedFallback(DeniedComponent, slotName, error.data, createElement) ?? defaultFallback ?? null;
1508
+ if (error instanceof RedirectSignal) {
1509
+ 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.");
1510
+ return buildDeniedFallback(DeniedComponent, slotName, void 0, createElement) ?? defaultFallback ?? null;
1511
+ }
1512
+ if (isDebug()) console.warn("[timber] Unhandled error in slot access.ts. Use deny() for access control, not unhandled throws.", error);
1513
+ throw error;
1514
+ }
1515
+ return children;
1516
+ }
1517
+ /**
1518
+ * Build the denied fallback element dynamically with DenySignal data.
1519
+ * Returns null if no DeniedComponent is available.
1520
+ */
1521
+ function buildDeniedFallback(DeniedComponent, slotName, data, createElement) {
1522
+ if (!DeniedComponent) return null;
1523
+ return createElement(DeniedComponent, {
1524
+ slot: slotName,
1525
+ dangerouslyPassData: data
1526
+ });
1527
+ }
1528
+ //#endregion
1529
+ //#region src/server/param-coercion.ts
1530
+ /** Global schema codecs, set once at startup from virtual:timber-schema. */
1531
+ var _globalCodecs = null;
1532
+ /**
1533
+ * Run segment param coercion on the matched route's segments.
1534
+ *
1535
+ * When `globalCodecs` is provided (from app/schema.ts), uses the global
1536
+ * codec map keyed by bare param name. Otherwise falls back to loading
1537
+ * per-segment params.ts modules.
1538
+ *
1539
+ * Throws ParamCoercionError if any codec fails (→ 404).
1540
+ *
1541
+ * This runs BEFORE middleware, so ctx.segmentParams is already typed.
1542
+ * See design/07-routing.md §"Where Coercion Runs"
1543
+ * See design/41-global-params.md §"Pipeline Integration"
1544
+ */
1545
+ async function coerceSegmentParams(match) {
1546
+ const globalCodecs = _globalCodecs;
1547
+ const mergeTarget = Object.create(null);
1548
+ for (const key of Object.keys(match.segmentParams)) if (key !== "__proto__") mergeTarget[key] = match.segmentParams[key];
1549
+ match.segmentParams = mergeTarget;
1550
+ if (globalCodecs) {
1551
+ for (const segment of match.segments) {
1552
+ if (!segment.paramName) continue;
1553
+ const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
1554
+ const codec = globalCodecs[bracketKey];
1555
+ if (!codec) continue;
1556
+ const key = segment.paramName;
1557
+ if (!(key in mergeTarget)) continue;
1558
+ try {
1559
+ mergeTarget[key] = sanitizeParamValue(codec.parse(mergeTarget[key]));
1560
+ } catch (err) {
1561
+ const message = err instanceof Error ? err.message : String(err);
1562
+ 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}'.`);
1563
+ throw new ParamCoercionError(message);
1564
+ }
1565
+ }
1566
+ return;
1567
+ }
1568
+ for (const segment of match.segments) {
1569
+ if (!segment.params) continue;
1570
+ let mod;
1571
+ try {
1572
+ mod = await loadModule(segment.params);
1573
+ } catch (err) {
1574
+ const message = `Failed to load params module for segment "${segment.segmentName}": ${err instanceof Error ? err.message : String(err)}`;
1575
+ if (isDebug()) console.warn(`[timber] Param coercion error: ${message}\n Segment: ${segment.segmentName}\n Params file: ${segment.params}`);
1576
+ throw new ParamCoercionError(message);
1577
+ }
1578
+ const segmentParamsDef = mod.segmentParams;
1579
+ if (!segmentParamsDef || typeof segmentParamsDef.parse !== "function") continue;
1580
+ try {
1581
+ const coerced = segmentParamsDef.parse(match.segmentParams);
1582
+ for (const key of Object.keys(coerced)) if (key !== "__proto__") mergeTarget[key] = sanitizeParamValue(coerced[key]);
1583
+ } catch (err) {
1584
+ const message = err instanceof Error ? err.message : String(err);
1585
+ if (isDebug()) {
1586
+ const rawKeys = Object.keys(match.segmentParams).join(", ");
1587
+ console.warn(`[timber] Param codec rejected values for segment "${segment.segmentName}"\n Error: ${message}\n Available raw params: { ${rawKeys} }\n Params file: ${segment.params}\n Hint: this usually means a codec threw for the raw URL value.\n Check that the regex/schema accepts the actual path segment string.`);
1588
+ }
1589
+ throw new ParamCoercionError(message);
1590
+ }
1591
+ }
1592
+ }
1593
+ //#endregion
1594
+ //#region src/client/segment-update-context.ts
1595
+ /**
1596
+ * SegmentUpdateContext — React context for partial navigation updates.
1597
+ *
1598
+ * During partial navigation (server skips unchanged sync layouts), the
1599
+ * router builds a Map of segment path → ReactNode updates and passes it
1600
+ * as the context value. Mounted SegmentOutlet components read from this
1601
+ * context to decide whether to render the update or their cached content.
1602
+ *
1603
+ * SINGLETON GUARANTEE: Uses globalThis + Symbol.for — same pattern as
1604
+ * NavigationContext. The RSC client bundler can duplicate this module
1605
+ * across chunks (browser-entry graph + client-reference graph). With
1606
+ * ESM output, each chunk gets its own module scope — a bare createContext
1607
+ * at module level would create separate instances per chunk. globalThis
1608
+ * guarantees a single instance regardless of duplication.
1609
+ *
1610
+ * See design/19-client-navigation.md §"Singleton Guarantee via globalThis"
1611
+ */
1612
+ var EMPTY_SEGMENT_UPDATES = /* @__PURE__ */ new Map();
1613
+ var CTX_KEY$2 = Symbol.for("__timber_segment_update_ctx");
1614
+ function getOrCreateContext$2() {
1615
+ const existing = globalThis[CTX_KEY$2];
1616
+ if (existing !== void 0) return existing;
1617
+ if (typeof React.createContext === "function") {
1618
+ const ctx = React.createContext(EMPTY_SEGMENT_UPDATES);
1619
+ globalThis[CTX_KEY$2] = ctx;
1620
+ return ctx;
1621
+ }
1622
+ }
1623
+ getOrCreateContext$2();
1624
+ //#endregion
1625
+ //#region src/client/segment-outlet.tsx
1626
+ /**
1627
+ * SegmentOutlet — client component boundary at each layout segment.
1628
+ *
1629
+ * Each layout in the segment tree is wrapped with a SegmentOutlet that:
1630
+ * 1. Knows its segment path (prop from the server)
1631
+ * 2. Reads from SegmentUpdateContext for partial navigation updates
1632
+ * 3. Caches its rendered content in a ref across navigations
1633
+ *
1634
+ * On full navigation: receives new children via props, caches and renders them.
1635
+ * On partial navigation (this segment skipped): the context map has no entry
1636
+ * for this path, so the outlet returns cached content — preserving layout state.
1637
+ * On partial navigation (this segment updated): the context map has content
1638
+ * for this path, so the outlet renders the update.
1639
+ *
1640
+ * Uses React context instead of useSyncExternalStore to stay compatible
1641
+ * with concurrent rendering (transitions). All outlets re-render when the
1642
+ * context value changes, but each bails out quickly if its segment has
1643
+ * no update — same approach as Next.js LayoutRouter.
1644
+ *
1645
+ * Security: performance optimization only. The server always runs all
1646
+ * access.ts files regardless of segment skipping.
1647
+ * See design/13-security.md §"State tree manipulation".
1648
+ */
1649
+ //#endregion
1650
+ //#region src/client/child-segment-context.ts
1651
+ /**
1652
+ * ChildSegmentContext — delivers child segment content to ChildSegmentOutlet.
1653
+ *
1654
+ * Each layout receives a stable <ChildSegmentOutlet /> client reference as
1655
+ * its `children` prop instead of varying server content. The actual child
1656
+ * content (page or inner layout) is provided via this context from a
1657
+ * ChildSegmentProvider placed ABOVE the layout in the element tree.
1658
+ *
1659
+ * This structural separation is what makes layout cache.component hits
1660
+ * possible: the cached layout payload contains a deterministic client
1661
+ * reference as children, not the varying inner content that changes per
1662
+ * route. Same pattern as Next.js LayoutRouter.
1663
+ *
1664
+ * SINGLETON GUARANTEE: Uses globalThis + Symbol.for — same pattern as
1665
+ * SegmentUpdateContext. The RSC client bundler can duplicate this module
1666
+ * across chunks; globalThis guarantees a single context instance.
1667
+ *
1668
+ * See design/45-cache-lifetimes.md §Layout Propagation.
1669
+ */
1670
+ var CTX_KEY$1 = Symbol.for("__timber_child_segment_ctx");
1671
+ function getOrCreateContext$1() {
1672
+ const existing = globalThis[CTX_KEY$1];
1673
+ if (existing !== void 0) return existing;
1674
+ if (typeof React.createContext === "function") {
1675
+ const ctx = React.createContext(null);
1676
+ globalThis[CTX_KEY$1] = ctx;
1677
+ return ctx;
1678
+ }
1679
+ }
1680
+ var ChildSegmentContext = getOrCreateContext$1();
1681
+ //#endregion
1682
+ //#region src/client/child-segment-outlet.tsx
1683
+ /**
1684
+ * ChildSegmentOutlet — renders child segment content from context.
1685
+ *
1686
+ * Passed as `children` to every layout component, replacing the previous
1687
+ * pattern of passing rendered server content directly. The layout receives
1688
+ * a stable client reference (this component), and the actual child content
1689
+ * is delivered via ChildSegmentContext from a provider above the layout.
1690
+ *
1691
+ * This makes layout cache.component hits possible: the cached layout
1692
+ * payload contains this deterministic client reference, not varying
1693
+ * server content. On cache hit, the revived payload contains the layout
1694
+ * shell + this outlet; the provider above supplies the current request's
1695
+ * child content.
1696
+ *
1697
+ * See design/45-cache-lifetimes.md §Layout Propagation.
1698
+ */
1699
+ function ChildSegmentOutlet() {
1700
+ return useContext(ChildSegmentContext);
1701
+ }
1702
+ //#endregion
1703
+ //#region src/client/child-segment-provider.tsx
1704
+ /**
1705
+ * ChildSegmentProvider — wraps a layout to provide child segment content.
1706
+ *
1707
+ * Placed above the layout in the element tree so that ChildSegmentOutlet
1708
+ * (inside the layout's `children`) can read the actual child content.
1709
+ *
1710
+ * Tree structure:
1711
+ * ChildSegmentProvider(childContent=innerElement)
1712
+ * └── Layout(children=<ChildSegmentOutlet />)
1713
+ * └── ChildSegmentOutlet → reads context → innerElement
1714
+ *
1715
+ * See design/45-cache-lifetimes.md §Layout Propagation.
1716
+ */
1717
+ //#endregion
1718
+ //#region src/server/route-element-builder.ts
1719
+ /**
1720
+ * Thrown when a defineSegmentParams codec's parse() fails.
1721
+ * The pipeline catches this and responds with 404.
1722
+ */
1723
+ var ParamCoercionError = class extends Error {
1724
+ constructor(message) {
1725
+ super(message);
1726
+ this.name = "ParamCoercionError";
1727
+ }
1728
+ };
1729
+ //#endregion
1730
+ //#region src/server/pipeline-metadata.ts
1731
+ /**
1732
+ * Metadata route helpers for the request pipeline.
1733
+ *
1734
+ * Handles serving static metadata files and serializing sitemap responses.
1735
+ * Extracted from pipeline.ts to keep files under 500 lines.
1736
+ *
1737
+ * See design/16-metadata.md §"Metadata Routes"
1738
+ */
1739
+ /**
1740
+ * Content types that are text-based and should include charset=utf-8.
1741
+ * Binary formats (images) should not include charset.
1742
+ */
1743
+ var TEXT_CONTENT_TYPES = /* @__PURE__ */ new Set([
1744
+ "application/xml",
1745
+ "text/plain",
1746
+ "application/json",
1747
+ "application/manifest+json",
1748
+ "image/svg+xml"
1749
+ ]);
1750
+ /**
1751
+ * Serve a static metadata file by reading it from disk.
1752
+ *
1753
+ * Static metadata route files (.xml, .txt, .json, .png, .ico, .svg, etc.)
1754
+ * are served as-is with the appropriate Content-Type header.
1755
+ * Text files include charset=utf-8; binary files do not.
1756
+ *
1757
+ * See design/16-metadata.md §"Metadata Routes"
1758
+ */
1759
+ async function serveStaticMetadataFile(metaMatch) {
1760
+ const { contentType, file } = metaMatch;
1761
+ const isText = TEXT_CONTENT_TYPES.has(contentType);
1762
+ const body = await readFile(file.filePath);
1763
+ const headers = {
1764
+ "Content-Type": isText ? `${contentType}; charset=utf-8` : contentType,
1765
+ "Content-Length": String(body.byteLength),
1766
+ "Cache-Control": "public, max-age=14400, must-revalidate"
1767
+ };
1768
+ return new Response(body, {
1769
+ status: 200,
1770
+ headers
1771
+ });
1772
+ }
1773
+ /**
1774
+ * Serialize a sitemap array to XML.
1775
+ * Follows the sitemap.org protocol: https://www.sitemaps.org/protocol.html
1776
+ */
1777
+ function serializeSitemap(entries) {
1778
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${entries.map((e) => {
1779
+ let xml = ` <url>\n <loc>${escapeXml(e.url)}</loc>`;
1780
+ if (e.lastModified) {
1781
+ const date = e.lastModified instanceof Date ? e.lastModified.toISOString() : e.lastModified;
1782
+ xml += `\n <lastmod>${escapeXml(date)}</lastmod>`;
1783
+ }
1784
+ if (e.changeFrequency) xml += `\n <changefreq>${escapeXml(e.changeFrequency)}</changefreq>`;
1785
+ if (e.priority !== void 0) xml += `\n <priority>${e.priority}</priority>`;
1786
+ xml += "\n </url>";
1787
+ return xml;
1788
+ }).join("\n")}\n</urlset>`;
1789
+ }
1790
+ /** Escape special XML characters. */
1791
+ function escapeXml(str) {
1792
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1793
+ }
1794
+ //#endregion
1795
+ //#region src/server/pipeline-interception.ts
1796
+ /**
1797
+ * Interception route matching for the request pipeline.
1798
+ *
1799
+ * Matches target URLs against interception rewrites to support the
1800
+ * modal route pattern (soft navigation intercepts).
1801
+ *
1802
+ * Extracted from pipeline.ts to keep files under 500 lines.
1803
+ *
1804
+ * See design/07-routing.md §"Intercepting Routes"
1805
+ */
1806
+ /**
1807
+ * Check if a pathname starts with a prefix on a segment boundary.
1808
+ *
1809
+ * Prevents /feed from matching /feed-private — the prefix must be
1810
+ * followed by '/' or be an exact match.
1811
+ */
1812
+ function hasSegmentPrefix(pathname, prefix) {
1813
+ if (prefix === "/") return true;
1814
+ if (!pathname.startsWith(prefix)) return false;
1815
+ return pathname.length === prefix.length || pathname[prefix.length] === "/";
1816
+ }
1817
+ /**
1818
+ * Check if an intercepting route applies for this soft navigation.
1819
+ *
1820
+ * Matches the target pathname against interception rewrites, constrained
1821
+ * by the source URL (X-Timber-URL header — where the user navigates FROM).
1822
+ *
1823
+ * Returns the source pathname to re-match if interception applies, or null.
1824
+ */
1825
+ function findInterceptionMatch(targetPathname, sourceUrl, rewrites) {
1826
+ for (const rewrite of rewrites) {
1827
+ if (!hasSegmentPrefix(sourceUrl, rewrite.interceptingPrefix)) continue;
1828
+ if (pathnameMatchesPattern(targetPathname, rewrite.interceptedPattern)) return { sourcePathname: rewrite.interceptingPrefix };
1829
+ }
1830
+ return null;
1831
+ }
1832
+ /**
1833
+ * Check if a pathname matches a URL pattern with dynamic segments.
1834
+ *
1835
+ * Supports [param] (single segment) and [...param] (one or more segments).
1836
+ * Static segments must match exactly.
1837
+ */
1838
+ function pathnameMatchesPattern(pathname, pattern) {
1839
+ const pathParts = pathname === "/" ? [] : pathname.slice(1).split("/");
1840
+ const patternParts = pattern === "/" ? [] : pattern.slice(1).split("/");
1841
+ let pi = 0;
1842
+ for (let i = 0; i < patternParts.length; i++) {
1843
+ const seg = classifyUrlSegment(patternParts[i]);
1844
+ switch (seg.kind) {
1845
+ case "catch-all": return pi < pathParts.length;
1846
+ case "optional-catch-all": return true;
1847
+ case "dynamic": {
1848
+ if (pi >= pathParts.length) return false;
1849
+ const prefix = seg.prefix ?? "";
1850
+ const suffix = seg.suffix ?? "";
1851
+ const pathPart = pathParts[pi];
1852
+ if (prefix || suffix) {
1853
+ if (pathPart.length <= prefix.length + suffix.length || prefix && !pathPart.startsWith(prefix) || suffix && !pathPart.endsWith(suffix)) return false;
1854
+ }
1855
+ pi++;
1856
+ continue;
1857
+ }
1858
+ case "static":
1859
+ if (pi >= pathParts.length || pathParts[pi] !== seg.value) return false;
1860
+ pi++;
1861
+ continue;
1862
+ }
1863
+ }
1864
+ return pi === pathParts.length;
1865
+ }
1866
+ //#endregion
1867
+ //#region src/server/pipeline-outcome.ts
1868
+ /**
1869
+ * Pipeline outcome translator — converts a `PhaseOutcome` (the value
1870
+ * each phase function returns) into a final `Response`.
1871
+ *
1872
+ * Lifted out of `pipeline-phases.ts` (TIM-853) so the per-phase try /
1873
+ * catch logic and the terminal Response-building logic each live in
1874
+ * their own file. The phases produce values; this module is the single
1875
+ * source of truth for how those values become wire responses.
1876
+ *
1877
+ * See design/07-routing.md §"Request Lifecycle".
1878
+ */
1879
+ function rscErrorResponse(isRsc, status, headers) {
1880
+ if (!isRsc) return new Response(null, { status });
1881
+ const h = headers ?? new Headers();
1882
+ h.set("X-Timber-Error", "1");
1883
+ h.set("content-type", "application/json; charset=utf-8");
1884
+ return new Response(JSON.stringify({
1885
+ error: true,
1886
+ status
1887
+ }), {
1888
+ status,
1889
+ headers: h
1890
+ });
1891
+ }
1892
+ /**
1893
+ * Terminal outcome handler — converts a `PhaseOutcome` into a final
1894
+ * `Response`, applying cookies, building redirects, rendering deny pages
1895
+ * and fallback error pages, and firing instrumentation hooks.
1896
+ *
1897
+ * This is the single source of truth for how phase outputs become wire
1898
+ * responses; the per-phase try/catch blocks now produce values, not
1899
+ * Responses, so the conversion logic lives in exactly one place.
1900
+ */
1901
+ async function outcomeToResponse(config, outcome, ctx) {
1902
+ switch (outcome.kind) {
1903
+ case "response": {
1904
+ const finalResponse = cloneWithMutableHeaders(outcome.response);
1905
+ if (outcome.phase === "proxy") return finalResponse;
1906
+ if (outcome.phase === "middleware" && ctx.responseHeaders) {
1907
+ applyCookieJar(finalResponse.headers);
1908
+ mergeMissingHeaders(finalResponse.headers, ctx.responseHeaders);
1909
+ logMiddlewareShortCircuit({
1910
+ method: ctx.method,
1911
+ path: ctx.path,
1912
+ status: finalResponse.status
1913
+ });
1914
+ }
1915
+ if (outcome.phase === "render") markResponseFlushed();
1916
+ return finalResponse;
1917
+ }
1918
+ case "redirect": {
1919
+ const headers = ctx.responseHeaders ?? new Headers();
1920
+ applyCookieJar(headers);
1921
+ return buildRedirectResponse(outcome.signal, ctx.req, headers);
1922
+ }
1923
+ case "deny": {
1924
+ const headers = ctx.responseHeaders ?? new Headers();
1925
+ applyCookieJar(headers);
1926
+ if (config.renderDenyFallback) try {
1927
+ return cloneWithMutableHeaders(await config.renderDenyFallback(outcome.signal, ctx.req, headers, ctx.match));
1928
+ } catch (denyRenderError) {
1929
+ logRenderError({
1930
+ method: ctx.method,
1931
+ path: ctx.path,
1932
+ error: denyRenderError
1933
+ });
1934
+ await fireOnRequestError(denyRenderError, ctx.req, "render");
1935
+ if (config.onPipelineError && denyRenderError instanceof Error) config.onPipelineError(denyRenderError, "render");
1936
+ }
1937
+ if (isDebug()) console.warn(`[timber] DenySignal(${outcome.signal.status}) from ${outcome.phase} phase — no renderDenyFallback configured, returning bare ${outcome.signal.status} response\n Request: ${ctx.method} ${ctx.path}\n Add a not-found.tsx or error.tsx to render a custom deny page.`);
1938
+ return new Response(null, {
1939
+ status: outcome.signal.status,
1940
+ headers
1941
+ });
1942
+ }
1943
+ case "error": {
1944
+ const isRsc = (ctx.req.headers.get("Accept") ?? "").includes("text/x-component");
1945
+ if (outcome.phase === "proxy") {
1946
+ logProxyError({ error: outcome.error });
1947
+ await fireOnRequestError(outcome.error, ctx.req, "proxy");
1948
+ if (config.onPipelineError && outcome.error instanceof Error) config.onPipelineError(outcome.error, "proxy");
1949
+ return rscErrorResponse(isRsc, 500);
1950
+ }
1951
+ if (outcome.phase === "middleware") {
1952
+ logMiddlewareError({
1953
+ method: ctx.method,
1954
+ path: ctx.path,
1955
+ error: outcome.error
1956
+ });
1957
+ await fireOnRequestError(outcome.error, ctx.req, "handler");
1958
+ if (config.onPipelineError && outcome.error instanceof Error) config.onPipelineError(outcome.error, "middleware");
1959
+ return rscErrorResponse(isRsc, 500);
1960
+ }
1961
+ const headers = ctx.responseHeaders ?? new Headers();
1962
+ applyCookieJar(headers);
1963
+ logRenderError({
1964
+ method: ctx.method,
1965
+ path: ctx.path,
1966
+ error: outcome.error
1967
+ });
1968
+ await fireOnRequestError(outcome.error, ctx.req, "render");
1969
+ if (config.onPipelineError && outcome.error instanceof Error) config.onPipelineError(outcome.error, "render");
1970
+ if (isRsc) return rscErrorResponse(true, 500, headers);
1971
+ if (config.renderFallbackError) try {
1972
+ return cloneWithMutableHeaders(await config.renderFallbackError(outcome.error, ctx.req, headers));
1973
+ } catch (fallbackRenderError) {
1974
+ logRenderError({
1975
+ method: ctx.method,
1976
+ path: ctx.path,
1977
+ error: fallbackRenderError
1978
+ });
1979
+ await fireOnRequestError(fallbackRenderError, ctx.req, "render");
1980
+ if (config.onPipelineError && fallbackRenderError instanceof Error) config.onPipelineError(fallbackRenderError, "render");
1981
+ }
1982
+ return new Response(null, { status: 500 });
1983
+ }
1984
+ }
1985
+ }
1986
+ //#endregion
1987
+ //#region src/server/pipeline-phases.ts
1988
+ /**
1989
+ * Pipeline phase functions — module-level free functions that take their
1990
+ * dependencies as explicit parameters. Each phase returns a `PhaseOutcome`
1991
+ * (a discriminated union over response / redirect / deny / error) defined
1992
+ * in `pipeline-outcome.ts`. The terminal `outcomeToResponse` (also in
1993
+ * `pipeline-outcome.ts`) translates outcomes into Responses.
1994
+ *
1995
+ * Lifted out of `createPipeline` so each phase can be unit-tested in
1996
+ * isolation. The lift is mechanical — these functions used to be closures
1997
+ * over `config`; they now take `config` as an explicit parameter.
1998
+ *
1999
+ * See design/07-routing.md §"Request Lifecycle", design/02-rendering-pipeline.md §"Request Flow".
2000
+ */
2001
+ /**
2002
+ * Validate and canonicalize the X-Timber-URL header value.
2003
+ *
2004
+ * Returns the canonical pathname if valid, or null if rejected:
2005
+ * - Must start with '/' (relative pathname, no scheme/authority)
2006
+ * - Must not contain control characters
2007
+ * - Must pass canonicalization (no encoded separators, null bytes, etc.)
2008
+ */
2009
+ function validateInterceptionHeader(raw, stripTrailingSlash) {
2010
+ if (!raw.startsWith("/")) return null;
2011
+ if (raw.startsWith("//")) return null;
2012
+ for (let i = 0; i < raw.length; i++) {
2013
+ const code = raw.charCodeAt(i);
2014
+ if (code <= 31 || code === 127) return null;
2015
+ }
2016
+ const result = canonicalize(raw, stripTrailingSlash);
2017
+ if (!result.ok) return null;
2018
+ return result.pathname;
2019
+ }
2020
+ /**
2021
+ * Run the proxy.ts phase. Calls user proxy code with a `next()` continuation.
2022
+ *
2023
+ * When `next` is provided, it replaces the default `handleRequest` as the
2024
+ * continuation — this lets the pipeline inject action dispatch between proxy
2025
+ * and route matching (TIM-1213). The proxy resolver was picked at pipeline
2026
+ * construction time so the hot path sees no per-request branching on the
2027
+ * `ProxyConfig` discriminant.
2028
+ */
2029
+ async function runProxyPhase(config, getProxy, req, method, path, next) {
2030
+ const detailed = config.serverTiming === "detailed";
2031
+ try {
2032
+ const proxyExport = await getProxy();
2033
+ const continuation = next ?? (() => handleRequest(config, req, method, path, true));
2034
+ const proxyFn = () => runProxy(proxyExport, req, continuation);
2035
+ return {
2036
+ kind: "response",
2037
+ phase: "proxy",
2038
+ response: await withSpan("timber.proxy", {}, () => detailed ? withTiming("proxy", "proxy.ts", proxyFn) : proxyFn())
2039
+ };
2040
+ } catch (error) {
2041
+ if (error instanceof RedirectSignal) return {
2042
+ kind: "redirect",
2043
+ phase: "proxy",
2044
+ signal: error
2045
+ };
2046
+ if (error instanceof DenySignal) return {
2047
+ kind: "deny",
2048
+ phase: "proxy",
2049
+ signal: error
2050
+ };
2051
+ return {
2052
+ kind: "error",
2053
+ phase: "proxy",
2054
+ error
2055
+ };
2056
+ }
2057
+ }
2058
+ /**
2059
+ * Run the middleware chain phase. If the chain short-circuits with a Response,
2060
+ * returns it as a 'response' outcome. Otherwise applies the request header
2061
+ * overlay and falls through to the render phase.
2062
+ */
2063
+ async function runMiddlewarePhase(config, req, match, responseHeaders, requestHeaderOverlay, renderContext) {
2064
+ const detailed = config.serverTiming === "detailed";
2065
+ const ctx = {
2066
+ req,
2067
+ requestHeaders: requestHeaderOverlay,
2068
+ headers: responseHeaders,
2069
+ segmentParams: match.segmentParams,
2070
+ earlyHints: (hints) => {
2071
+ for (const hint of hints) {
2072
+ let value;
2073
+ if (hint.as !== void 0) value = `<${hint.href}>; as=${hint.as}; rel=${hint.rel}`;
2074
+ else value = `<${hint.href}>; rel=${hint.rel}`;
2075
+ if (hint.crossOrigin !== void 0) value += `; crossorigin=${hint.crossOrigin}`;
2076
+ if (hint.fetchPriority !== void 0) value += `; fetchpriority=${hint.fetchPriority}`;
2077
+ responseHeaders.append("Link", value);
2078
+ }
2079
+ }
2080
+ };
2081
+ try {
2082
+ const chainFn = () => runMiddlewareChain(match.middlewareChain, ctx);
2083
+ const middlewareResponse = await (async () => {
2084
+ setMutableCookieContext(true);
2085
+ try {
2086
+ return await withSpan("timber.middleware", {}, () => detailed ? withTiming("mw", "middleware.ts", chainFn) : chainFn());
2087
+ } finally {
2088
+ setMutableCookieContext(false);
2089
+ }
2090
+ })();
2091
+ if (middlewareResponse) return {
2092
+ kind: "response",
2093
+ phase: "middleware",
2094
+ response: middlewareResponse
2095
+ };
2096
+ applyRequestHeaderOverlay(requestHeaderOverlay);
2097
+ applyCookieJar(responseHeaders);
2098
+ return runRenderPhase(config, req, match, responseHeaders, requestHeaderOverlay, renderContext);
2099
+ } catch (error) {
2100
+ if (error instanceof RedirectSignal) return {
2101
+ kind: "redirect",
2102
+ phase: "middleware",
2103
+ signal: error
2104
+ };
2105
+ if (error instanceof DenySignal) return {
2106
+ kind: "deny",
2107
+ phase: "middleware",
2108
+ signal: error
2109
+ };
2110
+ return {
2111
+ kind: "error",
2112
+ phase: "middleware",
2113
+ error
2114
+ };
2115
+ }
2116
+ }
2117
+ /**
2118
+ * Run the render phase. Wraps the configured renderer in a span and a
2119
+ * timing scope, and translates thrown signals into outcome variants.
2120
+ */
2121
+ async function runRenderPhase(config, req, match, responseHeaders, requestHeaderOverlay, { canonicalPathname, interception }) {
2122
+ const detailed = config.serverTiming === "detailed";
2123
+ try {
2124
+ const renderFn = () => config.render(req, match, responseHeaders, requestHeaderOverlay, interception);
2125
+ return {
2126
+ kind: "response",
2127
+ phase: "render",
2128
+ response: await withSpan("timber.render", { "http.route": canonicalPathname }, () => detailed ? withTiming("render", "RSC + SSR render", renderFn) : renderFn())
2129
+ };
2130
+ } catch (error) {
2131
+ if (error instanceof DenySignal) return {
2132
+ kind: "deny",
2133
+ phase: "render",
2134
+ signal: error
2135
+ };
2136
+ if (error instanceof RedirectSignal) return {
2137
+ kind: "redirect",
2138
+ phase: "render",
2139
+ signal: error
2140
+ };
2141
+ return {
2142
+ kind: "error",
2143
+ phase: "render",
2144
+ error
2145
+ };
2146
+ }
2147
+ }
2148
+ /**
2149
+ * Process a single request from canonicalization through phase dispatch.
2150
+ *
2151
+ * Stages: canonicalize → metadata routes → auto-sitemap → version skew →
2152
+ * route match → interception → early hints → param coercion → middleware →
2153
+ * render → outcome translation. Pre-routing short-circuits return Responses
2154
+ * directly; post-match dispatch goes through `outcomeToResponse`.
2155
+ *
2156
+ * Used both as the top-level entry (when no proxy.ts is configured) and as
2157
+ * the `next()` continuation passed to `runProxy()`.
2158
+ *
2159
+ * @param pathIsCanonical When true, `path` has already been canonicalized by
2160
+ * `createPipeline` — skip re-canonicalization to prevent double-decode.
2161
+ * When false (default), runs canonicalize as a safety net for direct callers.
2162
+ */
2163
+ async function handleRequest(config, req, method, path, pathIsCanonical) {
2164
+ const stripTrailingSlash = config.stripTrailingSlash ?? true;
2165
+ let canonicalPathname;
2166
+ if (pathIsCanonical) canonicalPathname = path;
2167
+ else {
2168
+ const result = canonicalize(path, stripTrailingSlash);
2169
+ if (!result.ok) {
2170
+ 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.`);
2171
+ return new Response(null, { status: result.status });
2172
+ }
2173
+ canonicalPathname = result.pathname;
2174
+ }
2175
+ if (config.matchMetadataRoute) {
2176
+ const metaMatch = config.matchMetadataRoute(canonicalPathname);
2177
+ if (metaMatch) try {
2178
+ if (metaMatch.isStatic) return await serveStaticMetadataFile(metaMatch);
2179
+ setSegmentParams(metaMatch.segmentParams);
2180
+ const mod = await loadModule(metaMatch.file);
2181
+ if (typeof mod.default !== "function") {
2182
+ 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.`);
2183
+ return new Response("Metadata route must export a default function", { status: 500 });
2184
+ }
2185
+ const handlerResult = await mod.default();
2186
+ if (handlerResult instanceof Response) {
2187
+ if (method === "HEAD") {
2188
+ const headHeaders = new Headers(handlerResult.headers);
2189
+ if (handlerResult.ok && !headHeaders.has("Cache-Control")) headHeaders.set("Cache-Control", "public, max-age=14400, must-revalidate");
2190
+ return new Response(null, {
2191
+ status: handlerResult.status,
2192
+ statusText: handlerResult.statusText,
2193
+ headers: headHeaders
2194
+ });
2195
+ }
2196
+ const res = cloneWithMutableHeaders(handlerResult);
2197
+ if (res.ok && !res.headers.has("Cache-Control")) res.headers.set("Cache-Control", "public, max-age=14400, must-revalidate");
2198
+ return res;
2199
+ }
2200
+ const contentType = metaMatch.contentType;
2201
+ let body;
2202
+ if (typeof handlerResult === "string") body = handlerResult;
2203
+ else if (contentType === "application/xml") body = serializeSitemap(handlerResult);
2204
+ else if (contentType === "application/manifest+json") body = JSON.stringify(handlerResult, null, 2);
2205
+ else body = String(handlerResult);
2206
+ return new Response(body, {
2207
+ status: 200,
2208
+ headers: {
2209
+ "Content-Type": `${contentType}; charset=utf-8`,
2210
+ "Cache-Control": "public, max-age=14400, must-revalidate"
2211
+ }
2212
+ });
2213
+ } catch (error) {
2214
+ if (error instanceof RedirectSignal) return new Response(null, {
2215
+ status: error.status,
2216
+ headers: { Location: error.location }
2217
+ });
2218
+ if (error instanceof DenySignal) return new Response(null, { status: error.status });
2219
+ logRenderError({
2220
+ method,
2221
+ path,
2222
+ error
2223
+ });
2224
+ if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, "metadata-route");
2225
+ return new Response(null, { status: 500 });
2226
+ }
2227
+ }
2228
+ if (config.autoSitemapHandler) try {
2229
+ const sitemapResponse = await config.autoSitemapHandler(canonicalPathname);
2230
+ if (sitemapResponse) return cloneWithMutableHeaders(sitemapResponse);
2231
+ } catch (error) {
2232
+ logRenderError({
2233
+ method,
2234
+ path,
2235
+ error
2236
+ });
2237
+ if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, "auto-sitemap");
2238
+ return new Response(null, { status: 500 });
2239
+ }
2240
+ const isRscRequest = (req.headers.get("Accept") ?? "").includes("text/x-component");
2241
+ if (isRscRequest) {
2242
+ if (!checkVersionSkew(req).ok) {
2243
+ const reloadHeaders = new Headers();
2244
+ applyReloadHeaders(reloadHeaders);
2245
+ return new Response(null, {
2246
+ status: 204,
2247
+ headers: reloadHeaders
2248
+ });
2249
+ }
2250
+ }
2251
+ let match = config.matchRoute(canonicalPathname);
2252
+ let interception;
2253
+ if (isRscRequest && config.interceptionRewrites?.length) {
2254
+ const rawSourceUrl = req.headers.get("X-Timber-URL");
2255
+ const validatedSourceUrl = rawSourceUrl ? validateInterceptionHeader(rawSourceUrl, stripTrailingSlash) : null;
2256
+ if (validatedSourceUrl) {
2257
+ const intercepted = findInterceptionMatch(canonicalPathname, validatedSourceUrl, config.interceptionRewrites);
2258
+ if (intercepted) {
2259
+ const sourceMatch = config.matchRoute(intercepted.sourcePathname);
2260
+ if (sourceMatch) {
2261
+ match = sourceMatch;
2262
+ interception = { targetPathname: canonicalPathname };
2263
+ }
2264
+ }
2265
+ }
2266
+ }
2267
+ if (!match) {
2268
+ if (isDebug()) console.warn(`[timber] No route matched for pathname: ${canonicalPathname}\n Input path: ${path}\n Method: ${method}`);
2269
+ if (config.renderNoMatch) {
2270
+ const responseHeaders = new Headers();
2271
+ return cloneWithMutableHeaders(await config.renderNoMatch(req, responseHeaders));
2272
+ }
2273
+ return new Response(null, { status: 404 });
2274
+ }
2275
+ const responseHeaders = new Headers();
2276
+ const requestHeaderOverlay = new Headers();
2277
+ responseHeaders.set("Cache-Control", "private, no-cache, no-store, max-age=0, must-revalidate");
2278
+ const leafSegment = match.segments[match.segments.length - 1];
2279
+ const isApiRoute = leafSegment?.route && !leafSegment?.page;
2280
+ if (config.earlyHints && !isRscRequest && !isApiRoute) try {
2281
+ await config.earlyHints(match, req, responseHeaders);
2282
+ } catch (err) {
2283
+ swallow(err, "early hints hook threw");
2284
+ }
2285
+ match.rawSegmentParams = { ...match.segmentParams };
2286
+ try {
2287
+ await coerceSegmentParams(match);
2288
+ } catch (error) {
2289
+ if (error instanceof ParamCoercionError) {
2290
+ if (isDebug()) {
2291
+ const segmentChain = match.segments.map((s) => s.segmentName || "/").join(" → ");
2292
+ 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 params.ts codec rejected the URL params.\n Check that all fields in defineSegmentParams() are optional for params\n that don't appear at every route depth (e.g. year, month, day).`);
2293
+ }
2294
+ const leafSegment = match.segments[match.segments.length - 1];
2295
+ if (leafSegment.route && !leafSegment.page) return new Response(null, { status: 404 });
2296
+ if (config.renderNoMatch) return cloneWithMutableHeaders(await config.renderNoMatch(req, responseHeaders));
2297
+ return new Response(null, { status: 404 });
2298
+ }
2299
+ throw error;
2300
+ }
2301
+ setSegmentParams(match.segmentParams);
2302
+ const segmentPath = match.segments.map((s) => s.segmentName).filter(Boolean).join("/") || "/";
2303
+ setMatchedSegmentPath(segmentPath.startsWith("/") ? segmentPath : `/${segmentPath}`);
2304
+ return outcomeToResponse(config, match.middlewareChain.length > 0 ? await runMiddlewarePhase(config, req, match, responseHeaders, requestHeaderOverlay, {
2305
+ canonicalPathname,
2306
+ interception
2307
+ }) : await runRenderPhase(config, req, match, responseHeaders, requestHeaderOverlay, {
2308
+ canonicalPathname,
2309
+ interception
2310
+ }), {
2311
+ req,
2312
+ method,
2313
+ path,
2314
+ responseHeaders,
2315
+ match
2316
+ });
2317
+ }
2318
+ //#endregion
2319
+ //#region src/server/pipeline.ts
2320
+ /**
2321
+ * Create the request handler from a pipeline configuration.
2322
+ *
2323
+ * Returns a function that processes an incoming Request through all pipeline
2324
+ * stages and produces a Response. This is the top-level entry point for the
2325
+ * server. The body is intentionally small — phase logic lives in
2326
+ * `pipeline-phases.ts`. This function only owns the per-request setup that
2327
+ * has to wrap the entire dispatch: trace ID, request context ALS, span
2328
+ * scope, Server-Timing header emission, and the active-request counter.
2329
+ */
2330
+ function createPipeline(config) {
2331
+ const proxyResolver = makeProxyResolver(config.proxy);
2332
+ const slowRequestMs = config.slowRequestMs ?? 3e3;
2333
+ const serverTiming = config.serverTiming ?? "total";
2334
+ let activeRequests = 0;
2335
+ const pipelineFn = async (req) => {
2336
+ const url = new URL(req.url);
2337
+ const method = req.method;
2338
+ const path = url.pathname;
2339
+ const startTime = performance.now();
2340
+ activeRequests++;
2341
+ return runWithTraceId(generateTraceId(), async () => {
2342
+ return runWithRequestContext(req, async () => {
2343
+ const runRequest = async () => {
2344
+ logRequestReceived({
2345
+ method,
2346
+ path
2347
+ });
2348
+ const response = await withSpan("http.server.request", {
2349
+ "http.request.method": method,
2350
+ "url.path": path
2351
+ }, async () => {
2352
+ const otelIds = await getOtelTraceId();
2353
+ if (otelIds) replaceTraceId(otelIds.traceId, otelIds.spanId);
2354
+ const stripTrailingSlash = config.stripTrailingSlash ?? true;
2355
+ const canonResult = canonicalize(url.pathname, stripTrailingSlash);
2356
+ if (!canonResult.ok) {
2357
+ 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.`);
2358
+ return new Response(null, { status: canonResult.status });
2359
+ }
2360
+ const canonicalPath = canonResult.pathname;
2361
+ let canonicalReq = req;
2362
+ if (url.pathname !== canonicalPath) {
2363
+ const canonicalUrl = new URL(req.url);
2364
+ canonicalUrl.pathname = canonicalPath;
2365
+ canonicalReq = new Request(canonicalUrl.toString(), req);
2366
+ }
2367
+ const innerHandler = async () => {
2368
+ if (config.dispatchAction) {
2369
+ const actionResult = await config.dispatchAction(canonicalReq, canonicalPath, pipelineFn);
2370
+ if (actionResult) return actionResult;
2371
+ }
2372
+ return handleRequest(config, canonicalReq, method, canonicalPath, true);
2373
+ };
2374
+ let result;
2375
+ if (proxyResolver) result = await outcomeToResponse(config, await runProxyPhase(config, proxyResolver, canonicalReq, method, canonicalPath, innerHandler), {
2376
+ req: canonicalReq,
2377
+ method,
2378
+ path: canonicalPath
2379
+ });
2380
+ else result = await innerHandler();
2381
+ await setSpanAttribute("http.response.status_code", result.status);
2382
+ const varyTokens = ["Accept"];
2383
+ if (config.interceptionRewrites?.length) varyTokens.push("X-Timber-URL");
2384
+ const existingVary = result.headers.get("Vary");
2385
+ const existingTokens = existingVary ? existingVary.toLowerCase().split(",").map((t) => t.trim()) : [];
2386
+ const newTokens = varyTokens.filter((t) => !existingTokens.includes(t.toLowerCase()));
2387
+ if (newTokens.length > 0) result.headers.set("Vary", existingVary ? `${existingVary}, ${newTokens.join(", ")}` : newTokens.join(", "));
2388
+ if (serverTiming === "detailed") {
2389
+ const timingHeader = getServerTimingHeader();
2390
+ if (timingHeader) result.headers.set("Server-Timing", timingHeader);
2391
+ } else if (serverTiming === "total") {
2392
+ const totalMs = Math.round(performance.now() - startTime);
2393
+ result.headers.set("Server-Timing", `total;dur=${totalMs}`);
2394
+ }
2395
+ if (serverTiming === "detailed" && result.body) result = new Response(wrapResponseWithStreamTiming(result.body, startTime), {
2396
+ status: result.status,
2397
+ statusText: result.statusText,
2398
+ headers: result.headers
2399
+ });
2400
+ return result;
2401
+ });
2402
+ const durationMs = Math.round(performance.now() - startTime);
2403
+ const status = response.status;
2404
+ const concurrency = activeRequests;
2405
+ activeRequests--;
2406
+ logRequestCompleted({
2407
+ method,
2408
+ path,
2409
+ status,
2410
+ durationMs,
2411
+ concurrency
2412
+ });
2413
+ if (slowRequestMs > 0 && durationMs > slowRequestMs) logSlowRequest({
2414
+ method,
2415
+ path,
2416
+ durationMs,
2417
+ threshold: slowRequestMs,
2418
+ concurrency
2419
+ });
2420
+ return response;
2421
+ };
2422
+ return serverTiming === "detailed" ? runWithTimingCollector(runRequest) : runRequest();
2423
+ });
2424
+ });
2425
+ };
2426
+ return pipelineFn;
2427
+ }
2428
+ //#endregion
2429
+ //#region src/server/build-manifest.ts
2430
+ /**
2431
+ * Collect all CSS files needed for a matched route's segment chain.
2432
+ *
2433
+ * Walks segments root → leaf, collecting CSS for each layout and page.
2434
+ * Deduplicates while preserving order (root layout CSS first).
2435
+ */
2436
+ function collectRouteCss(segments, manifest) {
2437
+ const seen = /* @__PURE__ */ new Set();
2438
+ const result = [];
2439
+ for (const segment of segments) for (const file of [segment.layout, segment.page]) {
2440
+ if (!file) continue;
2441
+ const cssFiles = manifest.css[file.filePath];
2442
+ if (!cssFiles) continue;
2443
+ for (const url of cssFiles) if (!seen.has(url)) {
2444
+ seen.add(url);
2445
+ result.push(url);
2446
+ }
2447
+ }
2448
+ return result;
2449
+ }
2450
+ /**
2451
+ * Collect all font entries needed for a matched route's segment chain.
2452
+ *
2453
+ * Walks segments root → leaf, collecting fonts for each layout and page.
2454
+ * Deduplicates by href while preserving order.
2455
+ */
2456
+ function collectRouteFonts(segments, manifest) {
2457
+ const seen = /* @__PURE__ */ new Set();
2458
+ const result = [];
2459
+ for (const segment of segments) for (const file of [segment.layout, segment.page]) {
2460
+ if (!file) continue;
2461
+ const fonts = manifest.fonts[file.filePath];
2462
+ if (!fonts) continue;
2463
+ for (const entry of fonts) if (!seen.has(entry.href)) {
2464
+ seen.add(entry.href);
2465
+ result.push(entry);
2466
+ }
2467
+ }
2468
+ return result;
2469
+ }
2470
+ /**
2471
+ * Collect modulepreload URLs for a matched route's segment chain.
2472
+ *
2473
+ * Walks segments root → leaf, collecting transitive JS dependencies
2474
+ * for each layout and page. Deduplicates across segments.
2475
+ */
2476
+ function collectRouteModulepreloads(segments, manifest) {
2477
+ const seen = /* @__PURE__ */ new Set();
2478
+ const result = [];
2479
+ for (const segment of segments) for (const file of [segment.layout, segment.page]) {
2480
+ if (!file) continue;
2481
+ const preloads = manifest.modulepreload[file.filePath];
2482
+ if (!preloads) continue;
2483
+ for (const url of preloads) if (!seen.has(url)) {
2484
+ seen.add(url);
2485
+ result.push(url);
2486
+ }
2487
+ }
2488
+ return result;
2489
+ }
2490
+ //#endregion
2491
+ //#region src/server/early-hints.ts
2492
+ /**
2493
+ * 103 Early Hints utilities.
2494
+ *
2495
+ * Early Hints are sent before the final response to let the browser
2496
+ * start fetching critical resources (CSS, fonts, JS) while the server
2497
+ * is still rendering.
2498
+ *
2499
+ * The framework collects hints from two sources:
2500
+ * 1. Build manifest — CSS, fonts, and JS chunks known at route-match time
2501
+ * 2. ctx.earlyHints() — explicit hints added by middleware or route handlers
2502
+ *
2503
+ * Both are emitted as Link headers. Cloudflare CDN automatically converts
2504
+ * Link headers into 103 Early Hints responses.
2505
+ *
2506
+ * Design docs: 02-rendering-pipeline.md §"Early Hints (103)"
2507
+ */
2508
+ /**
2509
+ * Format a single EarlyHint as a Link header value.
2510
+ *
2511
+ * Attribute order: `as` before `rel` to match Cloudflare CDN's cached
2512
+ * Early Hints format. Cloudflare caches Link headers from 200 responses
2513
+ * and re-emits them as 103 Early Hints on subsequent requests. If our
2514
+ * attribute order differs from Cloudflare's cached copy, the browser
2515
+ * sees two preload headers for the same URL (different attribute order)
2516
+ * and warns "Preload was ignored." Matching the order ensures the
2517
+ * browser deduplicates them correctly.
2518
+ *
2519
+ * Examples:
2520
+ * `</styles/root.css>; as=style; rel=preload`
2521
+ * `</fonts/inter.woff2>; as=font; rel=preload; crossorigin=anonymous`
2522
+ * `</_timber/client.js>; rel=modulepreload`
2523
+ * `<https://fonts.googleapis.com>; rel=preconnect`
2524
+ */
2525
+ function formatLinkHeader(hint) {
2526
+ if (hint.as !== void 0) {
2527
+ let value = `<${hint.href}>; as=${hint.as}; rel=${hint.rel}`;
2528
+ if (hint.crossOrigin !== void 0) value += `; crossorigin=${hint.crossOrigin}`;
2529
+ if (hint.fetchPriority !== void 0) value += `; fetchpriority=${hint.fetchPriority}`;
2530
+ return value;
2531
+ }
2532
+ let value = `<${hint.href}>; rel=${hint.rel}`;
2533
+ if (hint.crossOrigin !== void 0) value += `; crossorigin=${hint.crossOrigin}`;
2534
+ if (hint.fetchPriority !== void 0) value += `; fetchpriority=${hint.fetchPriority}`;
2535
+ return value;
2536
+ }
2537
+ /**
2538
+ * Collect all Link header strings for a matched route's segment chain.
2539
+ *
2540
+ * Walks the build manifest to emit hints for:
2541
+ * - CSS stylesheets (as=style; rel=preload)
2542
+ * - Font assets (as=font; rel=preload; crossorigin)
2543
+ * - JS modulepreload hints (rel=modulepreload) — unless skipJs is set
2544
+ *
2545
+ * Returns formatted Link header strings, deduplicated by URL, root → leaf order.
2546
+ * Returns an empty array in dev mode (manifest is empty).
2547
+ */
2548
+ function collectEarlyHintHeaders(segments, manifest, options) {
2549
+ const result = [];
2550
+ const seenUrls = /* @__PURE__ */ new Set();
2551
+ const add = (url, header) => {
2552
+ if (!seenUrls.has(url)) {
2553
+ seenUrls.add(url);
2554
+ result.push(header);
2555
+ }
2556
+ };
2557
+ for (const url of collectRouteCss(segments, manifest)) add(url, formatLinkHeader({
2558
+ href: url,
2559
+ rel: "preload",
2560
+ as: "style"
2561
+ }));
2562
+ for (const font of collectRouteFonts(segments, manifest)) add(font.href, formatLinkHeader({
2563
+ href: font.href,
2564
+ rel: "preload",
2565
+ as: "font",
2566
+ crossOrigin: "anonymous"
2567
+ }));
2568
+ if (!options?.skipJs) for (const url of collectRouteModulepreloads(segments, manifest)) add(url, formatLinkHeader({
2569
+ href: url,
2570
+ rel: "modulepreload"
2571
+ }));
2572
+ return result;
2573
+ }
2574
+ //#endregion
2575
+ //#region src/server/early-hints-sender.ts
2576
+ /**
2577
+ * Per-request 103 Early Hints sender — ALS bridge for platform adapters.
2578
+ *
2579
+ * The pipeline collects Link headers for CSS, fonts, and JS chunks at
2580
+ * route-match time. On platforms that support it (Node.js v18.11+, Bun),
2581
+ * the adapter can send these as a 103 Early Hints interim response before
2582
+ * the final response is ready.
2583
+ *
2584
+ * This module provides an ALS-based bridge: the generated entry point
2585
+ * (e.g., the Nitro entry) wraps the handler with `runWithEarlyHintsSender`,
2586
+ * binding a per-request sender function. The pipeline calls
2587
+ * `sendEarlyHints103()` to fire the 103 if a sender is available.
2588
+ *
2589
+ * On platforms where 103 is handled at the CDN level (e.g., Cloudflare
2590
+ * converts Link headers into 103 automatically), no sender is installed
2591
+ * and `sendEarlyHints103()` is a no-op.
2592
+ *
2593
+ * Design doc: 02-rendering-pipeline.md §"Early Hints (103)"
2594
+ */
2595
+ /**
2596
+ * Run a function with a per-request early hints sender installed.
2597
+ *
2598
+ * Called by generated entry points (e.g., Nitro node-server/bun) to
2599
+ * bind the platform's writeEarlyHints capability for the request duration.
2600
+ */
2601
+ function runWithEarlyHintsSender(sender, fn) {
2602
+ return earlyHintsSenderAls.run(sender, fn);
2603
+ }
2604
+ /**
2605
+ * Send collected Link headers as a 103 Early Hints response.
2606
+ *
2607
+ * No-op if no sender is installed for the current request (e.g., on
2608
+ * Cloudflare where the CDN handles 103 automatically, or in dev mode).
2609
+ *
2610
+ * Non-fatal: errors from the sender are caught and silently ignored.
2611
+ */
2612
+ function sendEarlyHints103(links) {
2613
+ if (!links.length) return;
2614
+ const sender = earlyHintsSenderAls.getStore();
2615
+ if (!sender) return;
2616
+ try {
2617
+ sender(links);
2618
+ } catch (err) {
2619
+ swallow(err, "early hints 103 send failed");
2620
+ }
2621
+ }
2622
+ //#endregion
2623
+ //#region src/server/tree-builder.ts
2624
+ var REACT_COMPONENT_TYPE_MARKERS = /* @__PURE__ */ new Set([
2625
+ Symbol.for("react.forward_ref"),
2626
+ Symbol.for("react.memo"),
2627
+ Symbol.for("react.lazy"),
2628
+ Symbol.for("react.provider"),
2629
+ Symbol.for("react.context"),
2630
+ Symbol.for("react.suspense"),
2631
+ Symbol.for("react.suspense_list"),
2632
+ Symbol.for("react.client.reference")
2633
+ ]);
2634
+ /**
2635
+ * Validate that a loaded module's `default` export is something React
2636
+ * accepts as the first argument to `createElement` — i.e. a valid component
2637
+ * type. React doesn't export `isValidElementType` (only `isValidElement`,
2638
+ * which checks for *elements*, not *component types*), so this mirrors
2639
+ * React's internal check:
2640
+ *
2641
+ * - functions → function or class components
2642
+ * - objects with a `$$typeof` matching one of React's known component
2643
+ * markers → exotic components (`memo`, `forwardRef`, `lazy`, context,
2644
+ * suspense, client references via `@vitejs/plugin-rsc`)
2645
+ *
2646
+ * Strings (HTML tag names) are valid for `createElement` but never appear
2647
+ * as a route module's default export, so they're not recognized here.
2648
+ *
2649
+ * Anything else (numbers, plain config objects, JSON, etc.) is rejected so
2650
+ * the boundary wrapper is skipped rather than crashing inside React.
2651
+ */
2652
+ function isValidElementType(value) {
2653
+ if (typeof value === "function") return true;
2654
+ if (typeof value !== "object" || value === null) return false;
2655
+ const marker = value.$$typeof;
2656
+ return typeof marker === "symbol" && REACT_COMPONENT_TYPE_MARKERS.has(marker);
2657
+ }
2658
+ /**
2659
+ * Build the unified element tree from a matched segment chain.
2660
+ *
2661
+ * Construction is bottom-up:
2662
+ * 1. Start with the page component (leaf segment)
2663
+ * 2. Wrap in status-code error boundaries (fallback chain)
2664
+ * 3. Wrap in AccessGate (if segment has access.ts)
2665
+ * 4. Pass as children to the segment's layout
2666
+ * 5. Repeat up the segment chain to root
2667
+ *
2668
+ * Parallel slots are resolved at each layout level and composed as named props.
2669
+ */
2670
+ async function buildElementTree(config) {
2671
+ const { segments, loadModule, createElement, errorBoundaryComponent } = config;
2672
+ if (segments.length === 0) throw new Error("[timber] buildElementTree: empty segment chain");
2673
+ const leaf = segments[segments.length - 1];
2674
+ if (leaf.route && !leaf.page) return {
2675
+ tree: null,
2676
+ isApiRoute: true
2677
+ };
2678
+ const PageComponent = (leaf.page ? await loadModule(leaf.page) : null)?.default;
2679
+ if (!PageComponent) throw new Error(`[timber] No page component found for route at ${leaf.urlPath}. Each route must have a page.tsx or route.ts.`);
2680
+ let element = createElement(PageComponent, {});
2681
+ for (let i = segments.length - 1; i >= 0; i--) {
2682
+ const segment = segments[i];
2683
+ element = await wrapWithErrorBoundaries(segment, element, loadModule, createElement, errorBoundaryComponent);
2684
+ if (segment.access) {
2685
+ const accessFn = (await loadModule(segment.access)).default;
2686
+ element = createElement("timber:access-gate", {
2687
+ accessFn,
2688
+ segmentName: segment.segmentName,
2689
+ children: element
2690
+ });
2691
+ }
2692
+ if (segment.layout) {
2693
+ const LayoutComponent = (await loadModule(segment.layout)).default;
2694
+ if (LayoutComponent) {
2695
+ const slotProps = {};
2696
+ const slotNames = Object.keys(segment.slots);
2697
+ if (slotNames.length > 0) for (const slotName of slotNames) {
2698
+ const slotNode = segment.slots[slotName];
2699
+ slotProps[slotName] = await buildSlotElement(slotNode, loadModule, createElement, errorBoundaryComponent);
2700
+ }
2701
+ element = createElement(LayoutComponent, {
2702
+ ...slotProps,
2703
+ children: element
2704
+ });
2705
+ }
2706
+ }
2707
+ }
2708
+ return {
2709
+ tree: element,
2710
+ isApiRoute: false
2711
+ };
2712
+ }
2713
+ /**
2714
+ * Build the element tree for a parallel slot.
2715
+ *
2716
+ * Slots have their own access.ts (SlotAccessGate) and error boundaries.
2717
+ * On access denial: denied.tsx → default.tsx → null (graceful degradation).
2718
+ */
2719
+ async function buildSlotElement(slotNode, loadModule, createElement, errorBoundaryComponent) {
2720
+ const PageComponent = (slotNode.page ? await loadModule(slotNode.page) : null)?.default;
2721
+ const DefaultComponent = (slotNode.default ? await loadModule(slotNode.default) : null)?.default;
2722
+ if (!PageComponent) return DefaultComponent ? createElement(DefaultComponent, {}) : null;
2723
+ let element = createElement(PageComponent, {});
2724
+ element = await wrapWithErrorBoundaries(slotNode, element, loadModule, createElement, errorBoundaryComponent);
2725
+ if (slotNode.access) {
2726
+ const accessFn = (await loadModule(slotNode.access)).default;
2727
+ const DeniedComponent = (slotNode.denied ? await loadModule(slotNode.denied) : null)?.default ?? null;
2728
+ const defaultFallback = DefaultComponent ? createElement(DefaultComponent, {}) : null;
2729
+ element = createElement("timber:slot-access-gate", {
2730
+ accessFn,
2731
+ DeniedComponent,
2732
+ slotName: slotNode.segmentName.replace(/^@/, ""),
2733
+ createElement,
2734
+ defaultFallback,
2735
+ children: element
2736
+ });
2737
+ }
2738
+ return element;
2739
+ }
2740
+ /** MDX/markdown extensions — these are server components that cannot be passed as function props. */
2741
+ var MDX_EXTENSIONS = /* @__PURE__ */ new Set(["mdx", "md"]);
2742
+ /**
2743
+ * Check if a route file is an MDX/markdown file based on its extension.
2744
+ * MDX components are server components by default and cannot cross the
2745
+ * RSC→client boundary as function props. They must be pre-rendered as
2746
+ * elements and passed as fallbackElement instead of fallbackComponent.
2747
+ */
2748
+ function isMdxFile(file) {
2749
+ return MDX_EXTENSIONS.has(file.extension);
2750
+ }
2751
+ /**
2752
+ * Wrap an element with error boundaries from a segment's status-code files.
2753
+ *
2754
+ * Wrapping order (innermost to outermost):
2755
+ * 1. Specific status files (503.tsx, 429.tsx, etc.)
2756
+ * 2. Category catch-alls (4xx.tsx, 5xx.tsx)
2757
+ * 3. error.tsx (general error boundary)
2758
+ *
2759
+ * This creates the fallback chain described in design/10-error-handling.md.
2760
+ *
2761
+ * MDX status files are server components and cannot be passed as function
2762
+ * props to TimberErrorBoundary (a 'use client' component). Instead, they
2763
+ * are pre-rendered as elements and passed as fallbackElement. The error
2764
+ * boundary renders the element directly when an error is caught.
2765
+ * See TIM-503.
2766
+ */
2767
+ async function wrapWithErrorBoundaries(segment, element, loadModule, createElement, errorBoundaryComponent) {
2768
+ if (segment.statusFiles) {
2769
+ for (const [key, file] of Object.entries(segment.statusFiles)) if (key !== "4xx" && key !== "5xx") {
2770
+ const status = parseInt(key, 10);
2771
+ if (!isNaN(status)) {
2772
+ const mod = await loadModule(file);
2773
+ const Component = isValidElementType(mod.default) ? mod.default : null;
2774
+ if (Component) element = createElement(errorBoundaryComponent, isMdxFile(file) ? {
2775
+ fallbackElement: createElement(Component, { status }),
2776
+ status,
2777
+ children: element
2778
+ } : {
2779
+ fallbackComponent: Component,
2780
+ status,
2781
+ children: element
2782
+ });
2783
+ }
2784
+ }
2785
+ for (const [key, file] of Object.entries(segment.statusFiles)) if (key === "4xx" || key === "5xx") {
2786
+ const mod = await loadModule(file);
2787
+ const Component = isValidElementType(mod.default) ? mod.default : null;
2788
+ if (Component) {
2789
+ const categoryStatus = key === "4xx" ? 400 : 500;
2790
+ element = createElement(errorBoundaryComponent, isMdxFile(file) ? {
2791
+ fallbackElement: createElement(Component, {}),
2792
+ status: categoryStatus,
2793
+ children: element
2794
+ } : {
2795
+ fallbackComponent: Component,
2796
+ status: categoryStatus,
2797
+ children: element
2798
+ });
2799
+ }
2800
+ }
2801
+ }
2802
+ if (segment.error) {
2803
+ const errorModule = await loadModule(segment.error);
2804
+ const ErrorComponent = isValidElementType(errorModule.default) ? errorModule.default : null;
2805
+ if (ErrorComponent) element = createElement(errorBoundaryComponent, isMdxFile(segment.error) ? {
2806
+ fallbackElement: createElement(ErrorComponent, {}),
2807
+ children: element
2808
+ } : {
2809
+ fallbackComponent: ErrorComponent,
2810
+ children: element
2811
+ });
2812
+ }
2813
+ return element;
2814
+ }
2815
+ //#endregion
2816
+ //#region src/server/csrf.ts
2817
+ /** HTTP methods that are considered safe (no mutation). */
2818
+ var SAFE_METHODS = /* @__PURE__ */ new Set([
2819
+ "GET",
2820
+ "HEAD",
2821
+ "OPTIONS"
2822
+ ]);
2823
+ /**
2824
+ * Derive the request's scheme from trusted sources.
2825
+ *
2826
+ * Priority:
2827
+ * 1. X-Forwarded-Proto (set by reverse proxies / load balancers)
2828
+ * 2. Request URL protocol
2829
+ *
2830
+ * Falls back to 'https' (fail closed) if neither is available.
2831
+ */
2832
+ function deriveRequestScheme(req) {
2833
+ const forwarded = req.headers.get("X-Forwarded-Proto");
2834
+ if (forwarded) {
2835
+ const proto = forwarded.split(",")[0].trim().toLowerCase();
2836
+ if (proto === "http" || proto === "https") return proto;
2837
+ }
2838
+ try {
2839
+ return new URL(req.url).protocol.replace(":", "");
2840
+ } catch {
2841
+ return "https";
2842
+ }
2843
+ }
2844
+ /**
2845
+ * Validate the Origin header against the request's full origin
2846
+ * (scheme + host + port).
2847
+ *
2848
+ * For mutation methods (POST, PUT, PATCH, DELETE):
2849
+ * - If `csrf: false`, skip validation.
2850
+ * - If `allowedOrigins` is set, Origin must match one exactly (no wildcards).
2851
+ * - Otherwise, Origin must match the derived request origin.
2852
+ *
2853
+ * Safe methods (GET, HEAD, OPTIONS) always pass.
2854
+ */
2855
+ function validateCsrf(req, config) {
2856
+ if (SAFE_METHODS.has(req.method)) return { ok: true };
2857
+ if (config.csrf === false) return { ok: true };
2858
+ const origin = req.headers.get("Origin");
2859
+ if (!origin) return {
2860
+ ok: false,
2861
+ status: 403
2862
+ };
2863
+ if (config.allowedOrigins) return config.allowedOrigins.includes(origin) ? { ok: true } : {
2864
+ ok: false,
2865
+ status: 403
2866
+ };
2867
+ const host = req.headers.get("Host");
2868
+ if (!host) return {
2869
+ ok: false,
2870
+ status: 403
2871
+ };
2872
+ let originOrigin;
2873
+ try {
2874
+ originOrigin = new URL(origin).origin;
2875
+ } catch {
2876
+ return {
2877
+ ok: false,
2878
+ status: 403
2879
+ };
2880
+ }
2881
+ const scheme = deriveRequestScheme(req);
2882
+ let expectedOrigin;
2883
+ try {
2884
+ expectedOrigin = new URL(`${scheme}://${host}`).origin;
2885
+ } catch {
2886
+ return {
2887
+ ok: false,
2888
+ status: 403
2889
+ };
2890
+ }
2891
+ return originOrigin === expectedOrigin ? { ok: true } : {
2892
+ ok: false,
2893
+ status: 403
2894
+ };
2895
+ }
2896
+ //#endregion
2897
+ //#region src/server/body-limits.ts
2898
+ var KB = 1024;
2899
+ var MB = 1024 * KB;
2900
+ var GB = 1024 * MB;
2901
+ var DEFAULT_LIMITS = {
2902
+ actionBodySize: 1 * MB,
2903
+ uploadBodySize: 10 * MB,
2904
+ maxFields: 100
2905
+ };
2906
+ var SIZE_PATTERN = /^(\d+(?:\.\d+)?)\s*(kb|mb|gb)?$/i;
2907
+ /** Parse a human-readable size string ("1mb", "512kb", "1024") into bytes. */
2908
+ function parseBodySize(size) {
2909
+ const match = SIZE_PATTERN.exec(size.trim());
2910
+ if (!match) throw new Error(`Invalid body size format: "${size}". Expected format like "1mb", "512kb", or "1024".`);
2911
+ const value = Number.parseFloat(match[1]);
2912
+ const unit = (match[2] ?? "").toLowerCase();
2913
+ switch (unit) {
2914
+ case "kb": return Math.floor(value * KB);
2915
+ case "mb": return Math.floor(value * MB);
2916
+ case "gb": return Math.floor(value * GB);
2917
+ case "": return Math.floor(value);
2918
+ default: throw new Error(`Unknown size unit: "${unit}"`);
2919
+ }
2920
+ }
2921
+ /** Strict integer pattern: one or more digits, nothing else. */
2922
+ var STRICT_INTEGER_RE = /^\d+$/;
2923
+ /** Check whether a request body exceeds the configured size limit (stateless, no ALS). */
2924
+ function enforceBodyLimits(req, kind, config) {
2925
+ const contentLength = req.headers.get("Content-Length");
2926
+ if (!contentLength) return {
2927
+ ok: false,
2928
+ status: 411
2929
+ };
2930
+ const trimmed = contentLength.trim();
2931
+ if (!STRICT_INTEGER_RE.test(trimmed)) return {
2932
+ ok: false,
2933
+ status: 411
2934
+ };
2935
+ return Number.parseInt(trimmed, 10) <= resolveLimit(kind, config) ? { ok: true } : {
2936
+ ok: false,
2937
+ status: 413
2938
+ };
2939
+ }
2940
+ /**
2941
+ * Resolve the byte limit for a given body kind, using config overrides or defaults.
2942
+ */
2943
+ function resolveLimit(kind, config) {
2944
+ const userLimits = config.limits;
2945
+ if (kind === "action") return userLimits?.actionBodySize ? parseBodySize(userLimits.actionBodySize) : DEFAULT_LIMITS.actionBodySize;
2946
+ return userLimits?.uploadBodySize ? parseBodySize(userLimits.uploadBodySize) : DEFAULT_LIMITS.uploadBodySize;
2947
+ }
2948
+ //#endregion
2949
+ //#region src/server/route-handler.ts
2950
+ /** All recognized HTTP method export names. */
2951
+ var HTTP_METHODS = [
2952
+ "GET",
2953
+ "POST",
2954
+ "PUT",
2955
+ "PATCH",
2956
+ "DELETE",
2957
+ "HEAD",
2958
+ "OPTIONS"
2959
+ ];
2960
+ /**
2961
+ * Resolve the full list of allowed methods for a route module.
2962
+ *
2963
+ * Includes:
2964
+ * - All explicitly exported methods
2965
+ * - HEAD (implicit when GET is exported)
2966
+ * - OPTIONS (always implicit)
2967
+ */
2968
+ function resolveAllowedMethods(mod) {
2969
+ const methods = [];
2970
+ for (const method of HTTP_METHODS) {
2971
+ if (method === "HEAD" || method === "OPTIONS") continue;
2972
+ if (mod[method]) methods.push(method);
2973
+ }
2974
+ if (mod.GET && !mod.HEAD) methods.push("HEAD");
2975
+ else if (mod.HEAD) methods.push("HEAD");
2976
+ if (!mod.OPTIONS) methods.push("OPTIONS");
2977
+ else methods.push("OPTIONS");
2978
+ return methods;
2979
+ }
2980
+ /**
2981
+ * Handle an incoming request against a route.ts module.
2982
+ *
2983
+ * Dispatches to the named method handler, auto-generates 405/OPTIONS,
2984
+ * and merges response headers from ctx.headers.
2985
+ */
2986
+ async function handleRouteRequest(mod, ctx) {
2987
+ const method = ctx.req.method.toUpperCase();
2988
+ const allowHeader = resolveAllowedMethods(mod).join(", ");
2989
+ if (method === "OPTIONS") {
2990
+ if (mod.OPTIONS) return runHandler(mod.OPTIONS, ctx);
2991
+ return new Response(null, {
2992
+ status: 204,
2993
+ headers: { Allow: allowHeader }
2994
+ });
2995
+ }
2996
+ if (method === "HEAD") {
2997
+ if (mod.HEAD) return runHandler(mod.HEAD, ctx);
2998
+ if (mod.GET) {
2999
+ const res = await runHandler(mod.GET, ctx);
3000
+ return new Response(null, {
3001
+ status: res.status,
3002
+ headers: res.headers
3003
+ });
3004
+ }
3005
+ }
3006
+ const handler = mod[method];
3007
+ if (!handler) return new Response(null, {
3008
+ status: 405,
3009
+ headers: { Allow: allowHeader }
3010
+ });
3011
+ return runHandler(handler, ctx);
3012
+ }
3013
+ /**
3014
+ * Run a handler, merge ctx.headers into the response, and catch errors.
3015
+ */
3016
+ async function runHandler(handler, ctx) {
3017
+ try {
3018
+ return mergeResponseHeaders(await handler(ctx), ctx.headers);
3019
+ } catch (error) {
3020
+ if (error instanceof DenySignal || error instanceof RedirectSignal) throw error;
3021
+ logRouteError({
3022
+ method: ctx.req.method,
3023
+ path: new URL(ctx.req.url).pathname,
3024
+ error
3025
+ });
3026
+ return new Response(null, { status: 500 });
3027
+ }
3028
+ }
3029
+ /**
3030
+ * Merge response headers from ctx.headers into the handler's response.
3031
+ * ctx.headers (set by middleware or the handler) are applied to the final response.
3032
+ * Handler-set headers take precedence over ctx.headers.
3033
+ */
3034
+ function mergeResponseHeaders(res, ctxHeaders) {
3035
+ let hasCtxHeaders = false;
3036
+ ctxHeaders.forEach(() => {
3037
+ hasCtxHeaders = true;
3038
+ });
3039
+ if (!hasCtxHeaders) return res;
3040
+ const merged = new Headers();
3041
+ ctxHeaders.forEach((value, key) => {
3042
+ if (key.toLowerCase() === "set-cookie") merged.append(key, value);
3043
+ else merged.set(key, value);
3044
+ });
3045
+ const resCookies = res.headers.getSetCookie();
3046
+ for (const cookie of resCookies) merged.append("Set-Cookie", cookie);
3047
+ res.headers.forEach((value, key) => {
3048
+ if (key.toLowerCase() !== "set-cookie") merged.set(key, value);
3049
+ });
3050
+ return new Response(res.body, {
3051
+ status: res.status,
3052
+ statusText: res.statusText,
3053
+ headers: merged
3054
+ });
3055
+ }
3056
+ //#endregion
3057
+ //#region src/server/render-timeout.ts
3058
+ /**
3059
+ * Render timeout utilities for SSR streaming pipeline.
3060
+ *
3061
+ * Provides a RenderTimeoutError class and a helper to create
3062
+ * timeout-guarded AbortSignals. Used to defend against hung RSC
3063
+ * streams and infinite SSR renders.
3064
+ *
3065
+ * Design doc: 02-rendering-pipeline.md §"Streaming Constraints"
3066
+ */
3067
+ /**
3068
+ * Error thrown when an SSR render or RSC stream read exceeds the
3069
+ * configured timeout. Callers can check `instanceof RenderTimeoutError`
3070
+ * to distinguish timeout from other errors and return a 504 or close
3071
+ * the connection cleanly.
3072
+ */
3073
+ var RenderTimeoutError = class extends Error {
3074
+ timeoutMs;
3075
+ constructor(timeoutMs, context) {
3076
+ const message = context ? `Render timeout after ${timeoutMs}ms: ${context}` : `Render timeout after ${timeoutMs}ms`;
3077
+ super(message);
3078
+ this.name = "RenderTimeoutError";
3079
+ this.timeoutMs = timeoutMs;
3080
+ }
3081
+ };
3082
+ //#endregion
3083
+ //#region src/server/prebuilt/cache-key.ts
3084
+ /**
3085
+ * Component cache-key hashing for prebuilt (cache.component) entries.
3086
+ *
3087
+ * Key: `sha256(canonicalJson({ props, params })).slice(0, 16)` — see
3088
+ * design/44-render-at-build-time.md §Cache Key. Both the request-time
3089
+ * lookup (prebuilt-runtime.ts) and the post-build capture pass (TIM-1119)
3090
+ * MUST use this exact function, or build artifacts become unaddressable.
3091
+ *
3092
+ * THE IDENTITY CONTRACT (design/44 §Cache Key): cache identity is **JSON
3093
+ * value semantics** — two inputs are the same entry iff they are the same
3094
+ * JSON value. This is an ALLOWLIST, not a denylist: a value participates
3095
+ * in caching iff it is plain JSON data — null, boolean, finite number,
3096
+ * string, dense array, or a plain `Object.prototype` object with only
3097
+ * enumerable string-keyed data properties — plus `undefined` and `-0`,
3098
+ * which hash as distinct sentinels because flight preserves them. Every
3099
+ * value outside that domain (functions, symbols, bigints, accessors,
3100
+ * hidden or symbol-keyed properties, non-`Object.prototype` prototypes
3101
+ * including null, aliased references, sparse arrays, cycles, non-finite
3102
+ * numbers) is UNKEYABLE by definition: `computeComponentCacheKey` returns
3103
+ * null and the caller renders live — slower, never wrong.
3104
+ *
3105
+ * The line between rejected and declared-out-of-domain: values are
3106
+ * REJECTED when the JSON projection itself would be ambiguous or lossy
3107
+ * (hidden/symbol/non-enumerable properties, accessors, exotic
3108
+ * prototypes). Observable state that leaves the projection intact — key
3109
+ * order, reference identity, descriptor metadata (writable/configurable,
3110
+ * frozen/sealed/extensible), proxy behavior — is DECLARED outside
3111
+ * cache.component's supported input domain rather than rejected:
3112
+ * key-order insensitivity is required for determinism, and rejecting
3113
+ * frozen objects would unkey ubiquitous legitimate inputs
3114
+ * (Object.freeze'd config props) to defend against components that
3115
+ * render differently based on property attributes.
3116
+ *
3117
+ * Two hard rules for this walk itself: it must be TOTAL (never throw
3118
+ * anything but UnkeyableValueError) and SIDE-EFFECT-FREE (descriptor
3119
+ * reads only; never invoke user code such as getters).
3120
+ *
3121
+ * Object keys sort recursively so `{a, b}` and `{b, a}` hash identically
3122
+ * (design/13-security.md checklist #14 — cache key determinism; key order
3123
+ * is deliberately NOT part of identity).
3124
+ */
3125
+ var UnkeyableValueError = class extends Error {};
3126
+ function canonicalize$1(value, seen) {
3127
+ if (value === null) return "null";
3128
+ switch (typeof value) {
3129
+ case "string": return JSON.stringify(value);
3130
+ case "boolean": return value ? "true" : "false";
3131
+ case "number":
3132
+ if (!Number.isFinite(value)) throw new UnkeyableValueError("non-finite number");
3133
+ return Object.is(value, -0) ? "-0" : String(value);
3134
+ case "object": break;
3135
+ case "undefined": return "undefined";
3136
+ default: throw new UnkeyableValueError(`unsupported type: ${typeof value}`);
3137
+ }
3138
+ const obj = value;
3139
+ if (seen.has(obj)) throw new UnkeyableValueError("repeated object reference (cycle or alias)");
3140
+ seen.add(obj);
3141
+ if (Object.getOwnPropertySymbols(obj).length > 0) throw new UnkeyableValueError("symbol-keyed property");
3142
+ {
3143
+ if (Array.isArray(obj)) {
3144
+ if (Object.getOwnPropertyNames(obj).length !== obj.length + 1) throw new UnkeyableValueError("non-index array property");
3145
+ const parts = [];
3146
+ for (let i = 0; i < obj.length; i++) {
3147
+ const desc = Object.getOwnPropertyDescriptor(obj, i);
3148
+ if (desc === void 0) throw new UnkeyableValueError("sparse array hole");
3149
+ if (desc.get !== void 0 || desc.set !== void 0) throw new UnkeyableValueError("accessor property");
3150
+ if (!desc.enumerable) throw new UnkeyableValueError("non-enumerable array element");
3151
+ parts.push(canonicalize$1(desc.value, seen));
3152
+ }
3153
+ return "[" + parts.join(",") + "]";
3154
+ }
3155
+ if (Object.getPrototypeOf(obj) !== Object.prototype) throw new UnkeyableValueError("non-plain object");
3156
+ const keys = Object.keys(obj).sort();
3157
+ if (keys.length !== Object.getOwnPropertyNames(obj).length) throw new UnkeyableValueError("non-enumerable property");
3158
+ const parts = [];
3159
+ for (const key of keys) {
3160
+ const desc = Object.getOwnPropertyDescriptor(obj, key);
3161
+ if (desc.get !== void 0 || desc.set !== void 0) throw new UnkeyableValueError("accessor property");
3162
+ parts.push(JSON.stringify(key) + ":" + canonicalize$1(desc.value, seen));
3163
+ }
3164
+ return "{" + parts.join(",") + "}";
3165
+ }
3166
+ }
3167
+ /**
3168
+ * Canonicalize a single value, or null if it is unkeyable. The ONE
3169
+ * definition of "faithfully comparable" — every prebuilt code path that
3170
+ * needs value equality (cache keys, slot-param divergence) goes through
3171
+ * this, so the unkeyable rules (accessors, hidden props, aliases,
3172
+ * bigints, …) live in exactly one place.
3173
+ */
3174
+ function tryCanonicalize(value) {
3175
+ try {
3176
+ return canonicalize$1(value, /* @__PURE__ */ new Set());
3177
+ } catch (error) {
3178
+ return toUnkeyable(error);
3179
+ }
3180
+ }
3181
+ /**
3182
+ * Totality by construction: ANY throw from the walk means the input is
3183
+ * outside the keyable domain. The walk touches only user-controlled
3184
+ * values through reflection ops — and a Proxy trap can throw arbitrary
3185
+ * errors from any of them (codex review on PR #835) — so converting only
3186
+ * UnkeyableValueError would let hostile/exotic inputs abort a render
3187
+ * that plain live rendering would have served fine. Unexpected error
3188
+ * types are logged for visibility (they would indicate either a proxy
3189
+ * trap or a canonicalizer bug) but still degrade to unkeyable.
3190
+ */
3191
+ function toUnkeyable(error) {
3192
+ if (!(error instanceof UnkeyableValueError)) console.error("[timber] cache key canonicalization threw — treating input as unkeyable:", error);
3193
+ return null;
3194
+ }
3195
+ /**
3196
+ * Split a props record into data props (participate in the cache key) and
3197
+ * slot values (React-element holes, excluded from the key) per the
3198
+ * component's declared `slots` option (TIM-1173).
3199
+ *
3200
+ * Every DECLARED slot lands in `slotValues` — present or not — because the
3201
+ * capture render always injects a SlotOutlet placeholder for each declared
3202
+ * slot: the cached shell is identical whether the caller passed the slot
3203
+ * or omitted it, so slot presence must not vary the key. `dataProps` is
3204
+ * everything else and is what gets hashed.
3205
+ *
3206
+ * Returns null when the props OBJECT itself is outside the identity
3207
+ * contract at the top level — accessors, symbol-keyed or non-enumerable
3208
+ * properties, or a non-`Object.prototype` prototype. Splitting such an
3209
+ * object would normalize away observable state (and reading an accessor
3210
+ * would invoke user code, which this walk must never do) BEFORE
3211
+ * `computeComponentCacheKey` gets the chance to reject it — a cached
3212
+ * shell could then be served for inputs a live render distinguishes.
3213
+ * Null means unkeyable: the caller renders live with the original props.
3214
+ * The walk is descriptor-reads only; getters are never invoked. Nested
3215
+ * data-prop values still face the full allowlist when hashed.
3216
+ */
3217
+ /** Create an own enumerable data property — safe for keys like `__proto__`. */
3218
+ function defineOwn(target, key, value) {
3219
+ Object.defineProperty(target, key, {
3220
+ value,
3221
+ enumerable: true,
3222
+ writable: true,
3223
+ configurable: true
3224
+ });
3225
+ }
3226
+ function splitSlotProps(props, slots) {
3227
+ try {
3228
+ if (Object.getPrototypeOf(props) !== Object.prototype) return null;
3229
+ if (Object.getOwnPropertySymbols(props).length > 0) return null;
3230
+ const slotSet = new Set(slots);
3231
+ const dataProps = {};
3232
+ const slotValues = {};
3233
+ for (const key of Object.getOwnPropertyNames(props)) {
3234
+ const desc = Object.getOwnPropertyDescriptor(props, key);
3235
+ if (desc === void 0 || desc.get !== void 0 || desc.set !== void 0 || !desc.enumerable) return null;
3236
+ defineOwn(slotSet.has(key) ? slotValues : dataProps, key, desc.value);
3237
+ }
3238
+ for (const slot of slotSet) if (!Object.hasOwn(slotValues, slot)) defineOwn(slotValues, slot, void 0);
3239
+ return {
3240
+ dataProps,
3241
+ slotValues
3242
+ };
3243
+ } catch (error) {
3244
+ return toUnkeyable(error);
3245
+ }
3246
+ }
3247
+ /**
3248
+ * Compute the cache-entry hash for a component render:
3249
+ * `sha256({props, params}).slice(0, 16)` over canonical JSON.
3250
+ *
3251
+ * Returns null when props (or params) contain unkeyable values — the
3252
+ * caller must fall back to dynamic rendering.
3253
+ */
3254
+ function computeComponentCacheKey(props, params) {
3255
+ let canonical;
3256
+ try {
3257
+ const seen = /* @__PURE__ */ new Set();
3258
+ canonical = "{\"params\":" + canonicalize$1({ ...params }, seen) + ",\"props\":" + canonicalize$1(props, seen) + "}";
3259
+ } catch (error) {
3260
+ return toUnkeyable(error);
3261
+ }
3262
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
3263
+ }
3264
+ //#endregion
3265
+ //#region src/client/slot-context.ts
3266
+ /**
3267
+ * SlotContext — delivers live slot content to SlotOutlet holes inside
3268
+ * cached component shells (TIM-1173).
3269
+ *
3270
+ * A `cache.component(Fn, { slots: [...] })` shell is captured with a
3271
+ * stable <SlotOutlet slot={name} /> client reference in place of each
3272
+ * declared slot prop. At request time the revived shell is wrapped in a
3273
+ * SlotsProvider carrying the live slot values; each outlet reads its
3274
+ * value from this context by name.
3275
+ *
3276
+ * The value is a per-instance record — the NEAREST provider wins, so two
3277
+ * instances of the same cached component on one page each resolve their
3278
+ * own children, and a slot component nested inside another slot
3279
+ * component's children reads its own provider, not the outer one.
3280
+ *
3281
+ * Generalizes the ChildSegmentContext pattern (TIM-1181) from a single
3282
+ * implicit `children` hole on layouts to explicitly declared, named slot
3283
+ * props on any cached component.
3284
+ *
3285
+ * SINGLETON GUARANTEE: globalThis + Symbol.for — the RSC client bundler
3286
+ * can duplicate this module across chunks; globalThis guarantees a single
3287
+ * context instance. Same pattern as ChildSegmentContext.
3288
+ *
3289
+ * See design/45-cache-lifetimes.md §Slot Components.
3290
+ */
3291
+ var CTX_KEY = Symbol.for("__timber_slot_ctx");
3292
+ function getOrCreateContext() {
3293
+ const existing = globalThis[CTX_KEY];
3294
+ if (existing !== void 0) return existing;
3295
+ if (typeof React.createContext === "function") {
3296
+ const ctx = React.createContext(null);
3297
+ globalThis[CTX_KEY] = ctx;
3298
+ return ctx;
3299
+ }
3300
+ }
3301
+ var SlotContext = getOrCreateContext();
3302
+ //#endregion
3303
+ //#region src/client/slot-outlet.tsx
3304
+ /**
3305
+ * SlotOutlet — the hole in a cached component shell (TIM-1173).
3306
+ *
3307
+ * At capture time the prebuilt runtime renders the wrapped component with
3308
+ * <SlotOutlet slot={name} /> in place of each declared slot prop. Being a
3309
+ * client component, it serializes into the flight payload as a stable,
3310
+ * deterministic client reference — the same bytes regardless of what live
3311
+ * content will fill the hole. At request time the revived shell sits under
3312
+ * a SlotsProvider and each outlet reads its live value from SlotContext.
3313
+ *
3314
+ * See design/45-cache-lifetimes.md §Slot Components.
3315
+ */
3316
+ function SlotOutlet({ slot }) {
3317
+ const values = useContext(SlotContext);
3318
+ return values ? values[slot] ?? null : null;
3319
+ }
3320
+ //#endregion
3321
+ //#region src/client/slot-provider.tsx
3322
+ /**
3323
+ * SlotsProvider — wraps a revived cached shell to supply live slot content
3324
+ * to the SlotOutlet holes inside it (TIM-1173).
3325
+ *
3326
+ * Tree structure per cached-component instance:
3327
+ * SlotsProvider(values={children: <Live />})
3328
+ * └── revived shell
3329
+ * └── SlotOutlet(slot="children") → reads context → <Live />
3330
+ *
3331
+ * See design/45-cache-lifetimes.md §Slot Components.
3332
+ */
3333
+ function SlotsProvider({ values, children }) {
3334
+ return createElement(SlotContext.Provider, { value: values }, children);
3335
+ }
3336
+ //#endregion
3337
+ //#region src/server/prebuilt/slots.ts
3338
+ /**
3339
+ * Slot-component helpers for cache.component (TIM-1173).
3340
+ *
3341
+ * A `slots: ['children', ...]` declaration caches the component's output
3342
+ * as a shell with a stable <SlotOutlet slot={name} /> client-reference
3343
+ * hole per declared slot prop; live slot content composes in per instance
3344
+ * at request time via SlotsProvider/SlotContext. This generalizes the
3345
+ * TIM-1181 layout mechanism (ChildSegmentOutlet) to explicitly declared,
3346
+ * named slots on any cached component.
3347
+ *
3348
+ * Both the production wrapper (cache path) and the dev wrapper (no cache,
3349
+ * same tree shape for dev/prod parity) build their render from these
3350
+ * helpers. See design/45-cache-lifetimes.md §Slot Components.
3351
+ */
3352
+ /**
3353
+ * Is this value the framework-injected `<ChildSegmentOutlet />` element?
3354
+ * Total: the `in` probe and `.type` read run against a user-controlled
3355
+ * value, and a Proxy trap can throw — that must classify as "not the
3356
+ * outlet" (the canonicalizer then rejects the value as unkeyable → live
3357
+ * render), not abort the request (codex P2 on PR #890).
3358
+ */
3359
+ function isChildSegmentOutletElement(value) {
3360
+ try {
3361
+ return value != null && typeof value === "object" && "type" in value && value.type === ChildSegmentOutlet;
3362
+ } catch {
3363
+ return false;
3364
+ }
3365
+ }
3366
+ /**
3367
+ * Dense array of non-empty strings — no holes, no non-string entries.
3368
+ * Total: the reads run against a user-controlled value at module load,
3369
+ * and a throwing Proxy trap must classify as invalid, not crash startup.
3370
+ */
3371
+ function isDenseStringArray(value) {
3372
+ try {
3373
+ if (!Array.isArray(value)) return false;
3374
+ for (let i = 0; i < value.length; i++) {
3375
+ if (!Object.hasOwn(value, i)) return false;
3376
+ const entry = value[i];
3377
+ if (typeof entry !== "string" || entry.length === 0) return false;
3378
+ }
3379
+ return true;
3380
+ } catch {
3381
+ return false;
3382
+ }
3383
+ }
3384
+ /**
3385
+ * Validate the `slots` option. Both the production and dev wrappers gate
3386
+ * on this — slot substitution is active in dev exactly when it would be
3387
+ * active in production, so the tree shape a component sees never
3388
+ * diverges between the two.
3389
+ */
3390
+ function resolveSlotNames(id, options) {
3391
+ const slots = options.slots;
3392
+ if (slots === void 0) return { kind: "none" };
3393
+ if (!isDenseStringArray(slots)) {
3394
+ console.warn(`[timber] cache.component "${id}": invalid slots declaration — expected an array of non-empty prop-name strings. Slot caching is disabled.`);
3395
+ return { kind: "disabled" };
3396
+ }
3397
+ if (slots.length === 0) return { kind: "none" };
3398
+ if (!(options.ttl !== void 0 || options.tags != null)) {
3399
+ console.warn(`[timber] cache.component "${id}": slots require a runtime lifetime (ttl and/or tags). Slot caching is disabled.`);
3400
+ return { kind: "disabled" };
3401
+ }
3402
+ return {
3403
+ kind: "active",
3404
+ slots
3405
+ };
3406
+ }
3407
+ /**
3408
+ * Split the live props into the cacheable shell render (data props plus a
3409
+ * deterministic SlotOutlet placeholder per declared slot — identical
3410
+ * whether or not the caller passed the slot) and the per-instance live
3411
+ * slot values.
3412
+ *
3413
+ * Null when the props object itself is unkeyable at the top level
3414
+ * (accessors, hidden properties, exotic prototype — see splitSlotProps):
3415
+ * the caller must render live with the original props.
3416
+ */
3417
+ function prepareSlotRender(props, slots) {
3418
+ const split = splitSlotProps(props ?? {}, slots);
3419
+ if (split === null) return null;
3420
+ const { dataProps, slotValues } = split;
3421
+ const shellProps = { ...dataProps };
3422
+ for (const slot of slots) Object.defineProperty(shellProps, slot, {
3423
+ value: createElement(SlotOutlet, { slot }),
3424
+ enumerable: true,
3425
+ writable: true,
3426
+ configurable: true
3427
+ });
3428
+ return {
3429
+ shellProps,
3430
+ dataProps,
3431
+ slotValues
3432
+ };
3433
+ }
3434
+ /**
3435
+ * Wrap a rendered shell (revived payload or live element) in a
3436
+ * per-instance SlotsProvider feeding the live values to its outlets.
3437
+ */
3438
+ function wrapInSlotsProvider(shell, slotValues) {
3439
+ return createElement(SlotsProvider, {
3440
+ values: slotValues,
3441
+ children: shell
3442
+ });
3443
+ }
3444
+ //#endregion
3445
+ //#region src/server/prebuilt/key-discipline.ts
3446
+ /** Components already warned — exported so tests can reset between cases. */
3447
+ var keyDisciplineWarned = /* @__PURE__ */ new Set();
3448
+ var REQUEST_CONTEXT_FIELDS = {
3449
+ headers: "headers()",
3450
+ parsedCookies: "cookies()",
3451
+ searchParams: "searchParams"
3452
+ };
3453
+ /**
3454
+ * Wrap a store with a proxy that records per-request field access.
3455
+ * The proxy forwards all reads/writes transparently — it only observes.
3456
+ */
3457
+ function createKeyDisciplineProxy(store) {
3458
+ const accessedApis = /* @__PURE__ */ new Set();
3459
+ return {
3460
+ proxy: new Proxy(store, { get(target, prop, receiver) {
3461
+ const api = REQUEST_CONTEXT_FIELDS[prop];
3462
+ if (api) accessedApis.add(api);
3463
+ return Reflect.get(target, prop, receiver);
3464
+ } }),
3465
+ accessedApis
3466
+ };
3467
+ }
3468
+ function emitKeyDisciplineWarning(id, apis) {
3469
+ if (keyDisciplineWarned.has(id)) return;
3470
+ keyDisciplineWarned.add(id);
3471
+ const apiList = Array.from(apis).join(", ");
3472
+ console.warn(`[timber] cache.component key-discipline warning: "${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.`);
3473
+ }
3474
+ //#endregion
3475
+ //#region src/server/prebuilt/capture-state.ts
3476
+ /** Thrown by the wrapper when a capture renders its own component. */
3477
+ var PrebuiltCaptureCycleError = class extends Error {
3478
+ constructor(componentId) {
3479
+ super(`[timber] cache.component "${componentId}" renders itself (directly or through other components) — prerendering it would recurse forever. Remove the cycle or drop { prerender: true } on this component.`);
3480
+ this.name = "PrebuiltCaptureCycleError";
3481
+ }
3482
+ };
3483
+ var active = null;
3484
+ /**
3485
+ * Mark a capture as active. The builder calls this immediately before
3486
+ * rendering an entry and MUST pair it with `endCapture()` in a finally.
3487
+ */
3488
+ function beginCapture(outerId) {
3489
+ active = {
3490
+ outerId,
3491
+ nested: /* @__PURE__ */ new Map()
3492
+ };
3493
+ }
3494
+ /**
3495
+ * End the active capture and return the wrapped components that rendered
3496
+ * inside it (first options object wins per inner ID).
3497
+ */
3498
+ function endCapture() {
3499
+ const nested = active?.nested;
3500
+ active = null;
3501
+ if (!nested) return [];
3502
+ return Array.from(nested, ([innerId, options]) => ({
3503
+ innerId,
3504
+ options
3505
+ }));
3506
+ }
3507
+ /** Whether a build-time capture is currently rendering. */
3508
+ function isCaptureActive() {
3509
+ return active !== null;
3510
+ }
3511
+ /**
3512
+ * Called by the `__prebuilt` wrapper on every render. No-op unless a
3513
+ * capture is active. Throws `PrebuiltCaptureCycleError` when the wrapper
3514
+ * belongs to the component currently being captured.
3515
+ */
3516
+ function noteWrapperRender(innerId, options) {
3517
+ if (active === null) return;
3518
+ if (innerId === active.outerId) throw new PrebuiltCaptureCycleError(innerId);
3519
+ if (!active.nested.has(innerId)) active.nested.set(innerId, options);
3520
+ }
3521
+ //#endregion
3522
+ //#region src/server/prebuilt-runtime.ts
3523
+ /**
3524
+ * Prebuilt component runtime — registration target for the timber-prebuilt
3525
+ * transform (plugins/prebuilt.ts) and the request-time half of
3526
+ * cache.component (TIM-1140).
3527
+ *
3528
+ * `cache.component(fn, opts)` call sites are rewritten at build time to
3529
+ * `__prebuilt(stableId, fn, opts)` (production) or `__prebuiltDev(name, fn)`
3530
+ * (dev). Both are exported via #server-internal — they are framework
3531
+ * plumbing, never called by user code directly.
3532
+ *
3533
+ * Cache hits are served by FLIGHT REVIVAL: the cached `.flight` payload is
3534
+ * decoded back into React elements inside the RSC environment with
3535
+ * `createFromReadableStream` (the flight client running in the flight
3536
+ * server — plugin-rsc wires the server-decode manifest so client
3537
+ * references revive as re-serializable reference proxies). The revived
3538
+ * element is returned from the wrapper and the outer render re-serializes
3539
+ * it like any other subtree. This is the same mechanism as Next.js
3540
+ * `"use cache"` (next/src/server/use-cache/use-cache-wrapper.ts).
3541
+ *
3542
+ * What a hit skips: all user work — data fetching, MDX compilation, the
3543
+ * component function itself never runs. What it re-pays: React
3544
+ * re-serializes the already-resolved tree (cost proportional to payload
3545
+ * size, not to data latency). The alternative — splicing raw payload bytes
3546
+ * into the outer stream — avoids re-serialization but requires byte-level
3547
+ * coupling to the flight wire format (row framing, ID rebasing, marker
3548
+ * detection); it was implemented and reviewed on PR #835 and deliberately
3549
+ * replaced by revival. See design/44-render-at-build-time.md
3550
+ * §Request-Time Injection.
3551
+ *
3552
+ * Misses render the original component live via the wrapper's own closure,
3553
+ * inside the React pass — segment params from ALS, deny()/redirect()
3554
+ * signals, Suspense, and error boundaries behave exactly as an unwrapped
3555
+ * component. `fallbackOnMissingSegmentParams: false` throws instead.
3556
+ */
3557
+ /** Default timeout for component re-renders (overridden by renderTimeoutMs). */
3558
+ var DEFAULT_COMPONENT_TIMEOUT_MS = 3e4;
3559
+ /** Singleflight for component re-renders (inline and background). */
3560
+ var componentSingleflight = createSingleflight({ timeoutMs: DEFAULT_COMPONENT_TIMEOUT_MS });
3561
+ var componentTimeoutMs = DEFAULT_COMPONENT_TIMEOUT_MS;
3562
+ var warnedNoWaitUntil = false;
3563
+ /**
3564
+ * Server-side component registry, keyed by stable component ID. Populated at
3565
+ * module load time by `__prebuilt` calls; consumed by the post-build capture
3566
+ * pass (TIM-1119). Never serialized — component functions are not
3567
+ * Flight-serializable.
3568
+ */
3569
+ var registry = /* @__PURE__ */ new Map();
3570
+ /**
3571
+ * Resolve the params the cache key may faithfully hash, or null if this
3572
+ * render is unkeyable.
3573
+ *
3574
+ * A wrapper rendered inside a parallel slot reads params merged with that
3575
+ * slot's overrides (`getSegmentParams`: `{...segmentParams, ...slotParams}`)
3576
+ * — but the cache key hashes only the route-level params, and the wrapper
3577
+ * cannot know which slot it renders under. The route params are usable
3578
+ * only when every slot's merged view canonicalizes identically to them;
3579
+ * otherwise two renders with the same key could see different effective
3580
+ * inputs and a hit could serve the WRONG cached subtree — so the render
3581
+ * goes live: slower, never wrong. (Build-time capture doesn't model slot
3582
+ * params either, so a divergent request could never have a faithful
3583
+ * entry. Codex review on PR #835; revisit with segment-path-scoped keys
3584
+ * if slot-scoped prebuilt is ever needed.)
3585
+ *
3586
+ * Comparison happens through `tryCanonicalize` — the same single
3587
+ * definition of value equality the cache key uses — so anything it
3588
+ * cannot represent faithfully (bigint-coerced codec params, accessors,
3589
+ * aliases, …) is unkeyable here too, and this guard can never throw.
3590
+ */
3591
+ function resolveKeyParams(store) {
3592
+ const main = store.segmentParams ?? {};
3593
+ const map = store.slotParamsMap;
3594
+ if (!map || map.size === 0) return main;
3595
+ const mainCanonical = tryCanonicalize({ ...main });
3596
+ if (mainCanonical === null) return null;
3597
+ for (const slotParams of map.values()) {
3598
+ const merged = tryCanonicalize({
3599
+ ...main,
3600
+ ...slotParams
3601
+ });
3602
+ if (merged === null || merged !== mainCanonical) return null;
3603
+ }
3604
+ return main;
3605
+ }
3606
+ /**
3607
+ * Mirror the production cache-key decision for a slot render, with no
3608
+ * cache IO — used by the dev wrapper so slot substitution happens in dev
3609
+ * exactly when production would serve the cache path rather than the
3610
+ * live fallback (dev/prod parity, codex P2 on PR #890): requires an ALS
3611
+ * store, keyable params (slot-param divergence unkeys), and keyable data
3612
+ * props (after the ChildSegmentOutlet strip).
3613
+ */
3614
+ function isSlotRenderKeyable(dataProps) {
3615
+ const store = requestContextAls.getStore();
3616
+ if (!store) return false;
3617
+ const keyParams = resolveKeyParams(store);
3618
+ if (keyParams === null) return false;
3619
+ let keyProps = dataProps;
3620
+ if (isChildSegmentOutletElement(keyProps.children)) {
3621
+ const { children: _children, ...rest } = keyProps;
3622
+ keyProps = rest;
3623
+ }
3624
+ return computeComponentCacheKey(keyProps, keyParams) !== null;
3625
+ }
3626
+ /** Wrap payload bytes as a single-chunk ReadableStream for the decoder. */
3627
+ function bytesToStream(bytes) {
3628
+ return new ReadableStream({ start(controller) {
3629
+ controller.enqueue(bytes);
3630
+ controller.close();
3631
+ } });
3632
+ }
3633
+ /** Whether the component has a runtime lifetime (ttl or tags). */
3634
+ function hasRuntimeLifetime(options) {
3635
+ return options.ttl !== void 0 || options.tags !== void 0 && options.tags !== null;
3636
+ }
3637
+ /**
3638
+ * Resolve tags from options (may be static array or function of props).
3639
+ * Function-form tags receive DATA props only — slot props are excluded so
3640
+ * the tags fn never sees SlotOutlet placeholders (capture paths pass
3641
+ * placeholder-substituted props) and tags stay deterministic per key.
3642
+ */
3643
+ function resolveTags(options, props) {
3644
+ if (!options.tags) return [];
3645
+ if (Array.isArray(options.tags)) return options.tags;
3646
+ const slots = options.slots;
3647
+ const fnProps = slots && slots.length > 0 ? splitSlotProps(props, slots)?.dataProps ?? props : props;
3648
+ return options.tags(fnProps);
3649
+ }
3650
+ /**
3651
+ * Serve a slot component from the cache: split live slot values out of the
3652
+ * props, look up / capture the shell (data props + a deterministic
3653
+ * SlotOutlet placeholder per slot), and wrap the revived shell in a
3654
+ * per-instance SlotsProvider that feeds the live values to the outlets.
3655
+ *
3656
+ * Null on any miss — the caller renders live with the original props.
3657
+ */
3658
+ async function tryReviveSlotShell(id, options, slots, props) {
3659
+ const render = prepareSlotRender(props, slots);
3660
+ if (render === null) return null;
3661
+ const { shellProps, dataProps, slotValues } = render;
3662
+ const revived = await tryRevivePayload(id, options, shellProps, dataProps);
3663
+ if (revived === null) return null;
3664
+ return { element: wrapInSlotsProvider(revived.element, slotValues) };
3665
+ }
3666
+ var rscFns = null;
3667
+ function requireRscFunctions() {
3668
+ if (!rscFns) throw new Error("[timber] RSC functions not wired — setRscFunctions() must be called at boot");
3669
+ return rscFns;
3670
+ }
3671
+ /**
3672
+ * Revive flight payload bytes into a React element.
3673
+ * Returns null on any failure (corrupt payload, missing decoder).
3674
+ */
3675
+ async function revivePayload(id, payload, outcome) {
3676
+ try {
3677
+ const { createFromReadableStream } = requireRscFunctions();
3678
+ return { element: await createFromReadableStream(bytesToStream(payload)) };
3679
+ } catch (error) {
3680
+ console.error(`[timber] failed to revive prebuilt payload for "${id}" — ${outcome}:`, error);
3681
+ return null;
3682
+ }
3683
+ }
3684
+ /** Ceiling on component re-render capture (design/13 A2). Uses configured renderTimeoutMs. */
3685
+ function getCaptureTimeoutMs() {
3686
+ return componentTimeoutMs;
3687
+ }
3688
+ /**
3689
+ * Render a component in isolation and capture its flight payload bytes.
3690
+ * Used for inline re-renders (tombstone/miss) and SWR background re-renders.
3691
+ *
3692
+ * The stream read is timeout-guarded and the reader is cancelled on
3693
+ * expiry (design/13 A2, A3).
3694
+ */
3695
+ async function captureComponentPayload(id, component, props) {
3696
+ const { renderToReadableStream } = requireRscFunctions();
3697
+ const element = createElement(component, props);
3698
+ let renderErrored = false;
3699
+ const store = requestContextAls.getStore();
3700
+ const discipline = store && !keyDisciplineWarned.has(id) ? createKeyDisciplineProxy(store) : null;
3701
+ const doCapture = async () => {
3702
+ beginCapture(id);
3703
+ try {
3704
+ const reader = renderToReadableStream(element, { onError() {
3705
+ renderErrored = true;
3706
+ return "";
3707
+ } }).getReader();
3708
+ const chunks = [];
3709
+ let totalLength = 0;
3710
+ const deadline = Date.now() + getCaptureTimeoutMs();
3711
+ try {
3712
+ for (;;) {
3713
+ const remaining = deadline - Date.now();
3714
+ if (remaining <= 0) throw new Error(`[timber] component re-render capture exceeded ${getCaptureTimeoutMs()}ms`);
3715
+ let timer;
3716
+ const { done, value } = await Promise.race([reader.read(), new Promise((_, reject) => {
3717
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`[timber] component re-render stream read timed out`)), remaining);
3718
+ })]);
3719
+ clearTimeout(timer);
3720
+ if (done) break;
3721
+ chunks.push(value);
3722
+ totalLength += value.byteLength;
3723
+ }
3724
+ } finally {
3725
+ reader.cancel().catch(() => {});
3726
+ }
3727
+ if (renderErrored) throw new Error("[timber] component re-render produced error rows — not caching");
3728
+ const result = new Uint8Array(totalLength);
3729
+ let offset = 0;
3730
+ for (const chunk of chunks) {
3731
+ result.set(chunk, offset);
3732
+ offset += chunk.byteLength;
3733
+ }
3734
+ return result;
3735
+ } finally {
3736
+ endCapture();
3737
+ }
3738
+ };
3739
+ const bytes = discipline ? await requestContextAls.run(discipline.proxy, doCapture) : await doCapture();
3740
+ if (discipline && discipline.accessedApis.size > 0) emitKeyDisciplineWarning(id, discipline.accessedApis);
3741
+ return bytes;
3742
+ }
3743
+ /**
3744
+ * Inline re-render: capture flight bytes, store in overlay, revive for
3745
+ * the current request. Singleflight-coalesced by overlay key.
3746
+ */
3747
+ async function inlineRerender(id, component, props, cacheKey, options, hasSeed, outcome) {
3748
+ const tags = resolveTags(options, props);
3749
+ const oKey = overlayKey(id, cacheKey);
3750
+ const sfKey = `prebuilt:${id}:${cacheKey}\0${lastInvalidationEpochFor(oKey, tags)}`;
3751
+ try {
3752
+ return await revivePayload(id, await componentSingleflight.do(sfKey, async (signal) => {
3753
+ const startEpoch = currentInvalidationEpoch();
3754
+ const bytes = await captureComponentPayload(id, component, props);
3755
+ if (!signal.aborted && hasRuntimeLifetime(options) && !wasInvalidatedSince(startEpoch, oKey, tags)) await storeOverlayEntry(id, cacheKey, bytes, {
3756
+ ttl: options.ttl,
3757
+ tags,
3758
+ hasSeed
3759
+ });
3760
+ return bytes;
3761
+ }), outcome);
3762
+ } catch (error) {
3763
+ console.error(`[timber] cache.component re-render failed for "${id}" — ${outcome}:`, error);
3764
+ return null;
3765
+ }
3766
+ }
3767
+ /**
3768
+ * Snapshot the request context values needed for a background re-render.
3769
+ * Creates a FRESH store with copied values — the original ALS store is
3770
+ * not referenced after capture.
3771
+ */
3772
+ function snapshotRequestContext(store) {
3773
+ return {
3774
+ headers: new Headers(store.headers),
3775
+ originalHeaders: new Headers(store.originalHeaders),
3776
+ cookieHeader: store.cookieHeader,
3777
+ parsedCookies: store.parsedCookies ? new Map(store.parsedCookies) : void 0,
3778
+ searchParams: new URLSearchParams(store.searchParams),
3779
+ searchString: store.searchString,
3780
+ segmentParams: store.segmentParams ? { ...store.segmentParams } : void 0,
3781
+ cookieJar: /* @__PURE__ */ new Map(),
3782
+ flushed: false,
3783
+ mutableContext: false
3784
+ };
3785
+ }
3786
+ /**
3787
+ * Kick off a background re-render via waitUntil. The re-render runs inside
3788
+ * a fresh ALS scope with snapshotted request context.
3789
+ */
3790
+ function backgroundRerender(id, component, props, cacheKey, options, hasSeed, snapshot) {
3791
+ const waitUntil = getWaitUntil();
3792
+ const tags = resolveTags(options, props);
3793
+ const oKey = overlayKey(id, cacheKey);
3794
+ const startEpoch = currentInvalidationEpoch();
3795
+ const sfKey = `prebuilt-bg:${id}:${cacheKey}\0${lastInvalidationEpochFor(oKey, tags)}`;
3796
+ const work = requestContextAls.run(snapshot, () => componentSingleflight.do(sfKey, async (signal) => {
3797
+ const bytes = await captureComponentPayload(id, component, props);
3798
+ if (!signal.aborted && !wasInvalidatedSince(startEpoch, oKey, tags)) await storeOverlayEntry(id, cacheKey, bytes, {
3799
+ ttl: options.ttl,
3800
+ tags,
3801
+ hasSeed
3802
+ });
3803
+ }).catch((error) => {
3804
+ console.error(`[timber] cache.component background re-render failed for "${id}":`, error);
3805
+ }));
3806
+ if (waitUntil) waitUntil(work);
3807
+ else if (!warnedNoWaitUntil) {
3808
+ warnedNoWaitUntil = true;
3809
+ 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().");
3810
+ }
3811
+ }
3812
+ /**
3813
+ * Try to serve this render from the prebuilt cache: overlay → artifact →
3814
+ * live render. Handles ISR (SWR, tombstones, re-renders) per design/45.
3815
+ *
3816
+ * Null on any miss — the caller renders live.
3817
+ */
3818
+ async function tryRevivePayload(id, options, props, keyPropsOverride) {
3819
+ const hasSource = hasPrebuiltPayloadSource();
3820
+ const hasRuntime = hasRuntimeLifetime(options);
3821
+ if (!hasSource && !hasRuntime) return null;
3822
+ const store = requestContextAls.getStore();
3823
+ if (!store) return null;
3824
+ const keyParams = (options.prerender ? await isParamIndependent(id) : false) ? {} : resolveKeyParams(store);
3825
+ if (keyParams === null) return null;
3826
+ let keyProps = keyPropsOverride ?? props ?? {};
3827
+ if (isChildSegmentOutletElement(keyProps.children)) {
3828
+ const { children: _children, ...rest } = keyProps;
3829
+ keyProps = rest;
3830
+ }
3831
+ const cacheKey = computeComponentCacheKey(keyProps, keyParams);
3832
+ if (cacheKey === null) return null;
3833
+ const outcome = options.fallbackOnMissingSegmentParams === false ? "failing the render (fallbackOnMissingSegmentParams: false)" : "rendering dynamically";
3834
+ const component = registry.get(id)?.component;
3835
+ if (hasRuntime) {
3836
+ const overlayResult = await lookupOverlay(id, cacheKey);
3837
+ if (overlayResult) {
3838
+ const overlayHasSeed = overlayResult.record?.s ?? false;
3839
+ if (overlayResult.kind === "tombstone") {
3840
+ if (component) return await inlineRerender(id, component, props, cacheKey, options, true, outcome);
3841
+ return null;
3842
+ }
3843
+ if (overlayResult.kind === "fresh" && overlayResult.payload) return await revivePayload(id, overlayResult.payload, outcome);
3844
+ if (overlayResult.kind === "stale" && overlayResult.payload) {
3845
+ if (options.staleWhileRevalidate && component) {
3846
+ backgroundRerender(id, component, props, cacheKey, options, overlayHasSeed, snapshotRequestContext(store));
3847
+ return await revivePayload(id, overlayResult.payload, outcome);
3848
+ }
3849
+ if (component) return await inlineRerender(id, component, props, cacheKey, options, overlayHasSeed, outcome);
3850
+ return null;
3851
+ }
3852
+ }
3853
+ }
3854
+ if (hasSource) {
3855
+ const artifactPayload = await lookupPrebuiltPayload(id, cacheKey);
3856
+ if (artifactPayload !== null) {
3857
+ if (options.ttl !== void 0) {
3858
+ const buildTs = (await getManifestEntry(id, cacheKey))?.buildTimestamp ?? 0;
3859
+ if ((Date.now() - buildTs) / 1e3 > options.ttl) {
3860
+ if (options.staleWhileRevalidate && component) {
3861
+ backgroundRerender(id, component, props, cacheKey, options, true, snapshotRequestContext(store));
3862
+ return await revivePayload(id, artifactPayload, outcome);
3863
+ }
3864
+ if (component) return await inlineRerender(id, component, props, cacheKey, options, true, outcome);
3865
+ return null;
3866
+ }
3867
+ }
3868
+ return await revivePayload(id, artifactPayload, outcome);
3869
+ }
3870
+ if (options.prerender) console.warn(`[timber] cache.component cache miss: "${id}" with params ${JSON.stringify(store.segmentParams ?? {})} — ${outcome}`);
3871
+ }
3872
+ if (hasRuntime && component && options.fallbackOnMissingSegmentParams !== false) return await inlineRerender(id, component, props, cacheKey, options, false, outcome);
3873
+ return null;
3874
+ }
3875
+ /**
3876
+ * Production wrapper injected by the timber-prebuilt transform. Registers
3877
+ * the component under its stable ID and returns a wrapper component that
3878
+ * either revives the cached payload (hit) or renders the original
3879
+ * component live (miss).
3880
+ */
3881
+ function __prebuilt(id, component, options = {}) {
3882
+ const resolution = resolveSlotNames(id, options);
3883
+ const slots = resolution.kind === "active" ? resolution.slots : null;
3884
+ if (resolution.kind === "active" && options.prerender) {
3885
+ 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.`);
3886
+ options = {
3887
+ ...options,
3888
+ prerender: void 0
3889
+ };
3890
+ }
3891
+ if (resolution.kind === "disabled") options = {
3892
+ ...options,
3893
+ slots: void 0
3894
+ };
3895
+ registry.set(id, {
3896
+ id,
3897
+ component,
3898
+ options
3899
+ });
3900
+ const wrapper = async (props) => {
3901
+ noteWrapperRender(id, options);
3902
+ if (isCaptureActive()) return createElement(component, props);
3903
+ const revived = slots ? await tryReviveSlotShell(id, options, slots, props) : await tryRevivePayload(id, options, props);
3904
+ if (revived !== null) return revived.element;
3905
+ if (options.fallbackOnMissingSegmentParams === false) throw new Error(`[timber] cache.component "${id}": no prebuilt payload for the current params and fallbackOnMissingSegmentParams is false`);
3906
+ return createElement(component, props);
3907
+ };
3908
+ Object.defineProperty(wrapper, "name", { value: `Prebuilt(${id})` });
3909
+ return wrapper;
3910
+ }
3911
+ /** Look up a registration by stable component ID. */
3912
+ function getPrebuiltRegistration(id) {
3913
+ return registry.get(id);
3914
+ }
3915
+ /** All registrations, for the post-build capture pass (TIM-1119). */
3916
+ function getPrebuiltRegistry() {
3917
+ return registry;
3918
+ }
3919
+ /**
3920
+ * Dev-mode wrapper injected by the timber-prebuilt transform: a no-op that
3921
+ * renders the component dynamically on every request, logging once per
3922
+ * component on first render so the production behavior difference is visible.
3923
+ * See design/44 §Dev Mode.
3924
+ */
3925
+ function __prebuiltDev(name, component, options = {}) {
3926
+ const resolution = resolveSlotNames(name, options);
3927
+ const slots = resolution.kind === "active" ? resolution.slots : null;
3928
+ let logged = false;
3929
+ const wrapper = (props) => {
3930
+ if (!logged) {
3931
+ logged = true;
3932
+ console.log(`[timber] cache.component: "${name}" — rendering dynamically (dev mode)`);
3933
+ }
3934
+ if (slots) {
3935
+ const render = prepareSlotRender(props, slots);
3936
+ if (render !== null && isSlotRenderKeyable(render.dataProps)) return wrapInSlotsProvider(createElement(component, render.shellProps), render.slotValues);
3937
+ }
3938
+ return createElement(component, props);
3939
+ };
3940
+ return wrapper;
3941
+ }
3942
+ //#endregion
3943
+ export { AccessGate, DEFAULT_LIMITS, DenySignal, METADATA_ROUTE_CONVENTIONS, RedirectSignal, RenderError, RenderTimeoutError, SlotAccessGate, __prebuilt, __prebuiltDev, buildElementTree, 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, validateCsrf };
3944
+
3945
+ //# sourceMappingURL=internal.js.map