@timber-js/app 0.2.0-alpha.186 → 0.2.0-alpha.188

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 (331) hide show
  1. package/dist/_chunks/{actions-C-Rw9vPc.js → actions-35jnMdeJ.js} +4 -3
  2. package/dist/_chunks/{actions-C-Rw9vPc.js.map → actions-35jnMdeJ.js.map} +1 -1
  3. package/dist/_chunks/als-registry-C6kcfprT.js +41 -0
  4. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -0
  5. package/dist/_chunks/{als-slots-mFweg276.js → als-slots-BEEIPKYm.js} +3 -4
  6. package/dist/_chunks/{als-slots-mFweg276.js.map → als-slots-BEEIPKYm.js.map} +1 -1
  7. package/dist/_chunks/{cache-api-Cd0VZ_Pd.js → cache-api-DjNrIWRR.js} +7 -13
  8. package/dist/_chunks/cache-api-DjNrIWRR.js.map +1 -0
  9. package/dist/_chunks/cli-check-CpmN7Nh-.js +256 -0
  10. package/dist/_chunks/cli-check-CpmN7Nh-.js.map +1 -0
  11. package/dist/_chunks/cli-schema-sync-CGMp_Psg.js +298 -0
  12. package/dist/_chunks/cli-schema-sync-CGMp_Psg.js.map +1 -0
  13. package/dist/_chunks/{cloudflare-Cs0uZXea.js → cloudflare-CGP6BZKO.js} +4 -3
  14. package/dist/_chunks/{cloudflare-Cs0uZXea.js.map → cloudflare-CGP6BZKO.js.map} +1 -1
  15. package/dist/_chunks/convention-lint-kXsgc_-7.js +784 -0
  16. package/dist/_chunks/convention-lint-kXsgc_-7.js.map +1 -0
  17. package/dist/_chunks/{error-boundary-DpYRI_I1.js → error-boundary-D-ODYX41.js} +25 -2
  18. package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -0
  19. package/dist/_chunks/file-cache-DmX7OqZP.js +454 -0
  20. package/dist/_chunks/file-cache-DmX7OqZP.js.map +1 -0
  21. package/dist/_chunks/{logger-N7e5auP0.js → logger-D8xJZXIN.js} +27 -40
  22. package/dist/_chunks/logger-D8xJZXIN.js.map +1 -0
  23. package/dist/_chunks/mdx-file-C005ay-P.js +25 -0
  24. package/dist/_chunks/mdx-file-C005ay-P.js.map +1 -0
  25. package/dist/_chunks/{plugin-context-DEGLSJs3.js → plugin-context-rCinWLiE.js} +7 -2
  26. package/dist/_chunks/{plugin-context-DEGLSJs3.js.map → plugin-context-rCinWLiE.js.map} +1 -1
  27. package/dist/_chunks/purge-store-Byr8XOjU.js +14 -0
  28. package/dist/_chunks/purge-store-Byr8XOjU.js.map +1 -0
  29. package/dist/_chunks/{resolve-schema-Dz3fcFUo.js → resolve-schema-5ma5pp1b.js} +2 -2
  30. package/dist/_chunks/{resolve-schema-Dz3fcFUo.js.map → resolve-schema-5ma5pp1b.js.map} +1 -1
  31. package/dist/_chunks/rsc-media-type-DRqE_lD_.js +46 -0
  32. package/dist/_chunks/rsc-media-type-DRqE_lD_.js.map +1 -0
  33. package/dist/_chunks/{cli-schema-sync-CKgHC2MB.js → scanner-C8b0Gcw3.js} +5 -298
  34. package/dist/_chunks/scanner-C8b0Gcw3.js.map +1 -0
  35. package/dist/_chunks/schema-bridge-Cc2Gngu1.js +199 -0
  36. package/dist/_chunks/schema-bridge-Cc2Gngu1.js.map +1 -0
  37. package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -1
  38. package/dist/_chunks/{param-value-C8TNYchQ.js → segment-context-CjOlyB8Y.js} +33 -2
  39. package/dist/_chunks/segment-context-CjOlyB8Y.js.map +1 -0
  40. package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -1
  41. package/dist/_chunks/{use-query-states-DFvWd-EA.js → use-query-states-BbU5Ge1V.js} +74 -26
  42. package/dist/_chunks/use-query-states-BbU5Ge1V.js.map +1 -0
  43. package/dist/_chunks/{navigation-root-B29qg0_T.js → use-segment-params-C4r4BD9T.js} +129 -5
  44. package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -0
  45. package/dist/_chunks/walkers-RzN6AFjr.js +141 -0
  46. package/dist/_chunks/walkers-RzN6AFjr.js.map +1 -0
  47. package/dist/adapters/cloudflare-dev.js +1 -1
  48. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  49. package/dist/adapters/cloudflare.d.ts.map +1 -1
  50. package/dist/adapters/cloudflare.js +1 -1
  51. package/dist/adapters/compress-module.d.ts +12 -0
  52. package/dist/adapters/compress-module.d.ts.map +1 -1
  53. package/dist/adapters/nitro.js +54 -2
  54. package/dist/adapters/nitro.js.map +1 -1
  55. package/dist/cache/index.js +1 -1
  56. package/dist/cdn/cloudflare-purge.js +30 -0
  57. package/dist/cdn/cloudflare-purge.js.map +1 -0
  58. package/dist/cdn/fastly-purge.js +33 -0
  59. package/dist/cdn/fastly-purge.js.map +1 -0
  60. package/dist/cdn/index.js +94 -0
  61. package/dist/cdn/index.js.map +1 -0
  62. package/dist/cdn/workers-cache-purge.js +35 -0
  63. package/dist/cdn/workers-cache-purge.js.map +1 -0
  64. package/dist/cli-check.d.ts +153 -0
  65. package/dist/cli-check.d.ts.map +1 -0
  66. package/dist/cli.d.ts +34 -8
  67. package/dist/cli.d.ts.map +1 -1
  68. package/dist/cli.js +46 -21
  69. package/dist/cli.js.map +1 -1
  70. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  71. package/dist/client/browser-entry/index.d.ts +1 -1
  72. package/dist/client/browser-entry/index.d.ts.map +1 -1
  73. package/dist/client/error-boundary.d.ts +6 -0
  74. package/dist/client/error-boundary.d.ts.map +1 -1
  75. package/dist/client/error-boundary.js +1 -1
  76. package/dist/client/index.d.ts +1 -0
  77. package/dist/client/index.d.ts.map +1 -1
  78. package/dist/client/index.js +27 -33
  79. package/dist/client/index.js.map +1 -1
  80. package/dist/client/internal.js +33 -17
  81. package/dist/client/internal.js.map +1 -1
  82. package/dist/client/link.d.ts +1 -7
  83. package/dist/client/link.d.ts.map +1 -1
  84. package/dist/client/navigation-commit.d.ts +16 -0
  85. package/dist/client/navigation-commit.d.ts.map +1 -1
  86. package/dist/client/router-pipeline.d.ts.map +1 -1
  87. package/dist/client/router.d.ts.map +1 -1
  88. package/dist/client/rsc-fetch.d.ts +9 -1
  89. package/dist/client/rsc-fetch.d.ts.map +1 -1
  90. package/dist/client/segment-cache.d.ts +14 -0
  91. package/dist/client/segment-cache.d.ts.map +1 -1
  92. package/dist/client/use-query-states.d.ts +9 -3
  93. package/dist/client/use-query-states.d.ts.map +1 -1
  94. package/dist/codec.js +1 -1
  95. package/dist/cookies/define-cookie.d.ts.map +1 -1
  96. package/dist/cookies/index.js +2 -2
  97. package/dist/cookies/index.js.map +1 -1
  98. package/dist/index.d.ts +4 -1
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +142 -477
  101. package/dist/index.js.map +1 -1
  102. package/dist/params/index.js +1 -1
  103. package/dist/plugin-context.d.ts +15 -0
  104. package/dist/plugin-context.d.ts.map +1 -1
  105. package/dist/plugins/routing.d.ts +0 -9
  106. package/dist/plugins/routing.d.ts.map +1 -1
  107. package/dist/plugins/shims.d.ts.map +1 -1
  108. package/dist/plugins/static-build.d.ts +24 -0
  109. package/dist/plugins/static-build.d.ts.map +1 -1
  110. package/dist/routing/codegen-shared.d.ts +3 -44
  111. package/dist/routing/codegen-shared.d.ts.map +1 -1
  112. package/dist/routing/codegen-types.d.ts +10 -31
  113. package/dist/routing/codegen-types.d.ts.map +1 -1
  114. package/dist/routing/codegen-write.d.ts +51 -0
  115. package/dist/routing/codegen-write.d.ts.map +1 -0
  116. package/dist/routing/codegen.d.ts.map +1 -1
  117. package/dist/routing/convention-lint.d.ts +18 -4
  118. package/dist/routing/convention-lint.d.ts.map +1 -1
  119. package/dist/routing/export-detect.d.ts +16 -0
  120. package/dist/routing/export-detect.d.ts.map +1 -1
  121. package/dist/routing/index.js +3 -2
  122. package/dist/routing/link-codegen.d.ts +19 -4
  123. package/dist/routing/link-codegen.d.ts.map +1 -1
  124. package/dist/routing/manifest-codegen.d.ts +1 -7
  125. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  126. package/dist/routing/segment-keys.d.ts +22 -0
  127. package/dist/routing/segment-keys.d.ts.map +1 -1
  128. package/dist/routing/types.d.ts +0 -6
  129. package/dist/routing/types.d.ts.map +1 -1
  130. package/dist/schema-bridge.d.ts +60 -9
  131. package/dist/schema-bridge.d.ts.map +1 -1
  132. package/dist/search-params/define.d.ts +62 -8
  133. package/dist/search-params/define.d.ts.map +1 -1
  134. package/dist/search-params/index.d.ts +0 -1
  135. package/dist/search-params/index.d.ts.map +1 -1
  136. package/dist/search-params/index.js +66 -29
  137. package/dist/search-params/index.js.map +1 -1
  138. package/dist/search-params/parse-total.d.ts +70 -0
  139. package/dist/search-params/parse-total.d.ts.map +1 -0
  140. package/dist/search-params/wrappers.d.ts +26 -3
  141. package/dist/search-params/wrappers.d.ts.map +1 -1
  142. package/dist/server/access-gate.d.ts +19 -8
  143. package/dist/server/access-gate.d.ts.map +1 -1
  144. package/dist/server/action-handler.d.ts.map +1 -1
  145. package/dist/server/als-registry.d.ts +16 -0
  146. package/dist/server/als-registry.d.ts.map +1 -1
  147. package/dist/server/chain-url-parts.d.ts +2 -3
  148. package/dist/server/chain-url-parts.d.ts.map +1 -1
  149. package/dist/server/compress.d.ts.map +1 -1
  150. package/dist/server/deny-boundary.d.ts +148 -15
  151. package/dist/server/deny-boundary.d.ts.map +1 -1
  152. package/dist/server/deny-renderer.d.ts +2 -2
  153. package/dist/server/deny-renderer.d.ts.map +1 -1
  154. package/dist/server/error-boundary-wrapper.d.ts +85 -15
  155. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  156. package/dist/server/index.d.ts +0 -1
  157. package/dist/server/index.d.ts.map +1 -1
  158. package/dist/server/index.js +3 -2
  159. package/dist/server/index.js.map +1 -1
  160. package/dist/server/internal.d.ts +3 -1
  161. package/dist/server/internal.d.ts.map +1 -1
  162. package/dist/server/internal.js +344 -237
  163. package/dist/server/internal.js.map +1 -1
  164. package/dist/server/metadata-collector.d.ts +52 -0
  165. package/dist/server/metadata-collector.d.ts.map +1 -0
  166. package/dist/server/param-coercion.d.ts +12 -5
  167. package/dist/server/param-coercion.d.ts.map +1 -1
  168. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  169. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  170. package/dist/server/pipeline-phases.d.ts.map +1 -1
  171. package/dist/server/primitives.d.ts +23 -0
  172. package/dist/server/primitives.d.ts.map +1 -1
  173. package/dist/server/route-element-builder.d.ts +1 -12
  174. package/dist/server/route-element-builder.d.ts.map +1 -1
  175. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
  176. package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
  177. package/dist/server/rsc-entry/error-renderer.d.ts +1 -1
  178. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  179. package/dist/server/rsc-entry/helpers.d.ts +0 -7
  180. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  181. package/dist/server/rsc-entry/index.d.ts +0 -1
  182. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  183. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  184. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
  185. package/dist/server/rsc-entry/rsc-payload.d.ts +1 -3
  186. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  187. package/dist/server/rsc-entry/rsc-stream.d.ts +12 -0
  188. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  189. package/dist/server/rsc-entry/ssr-renderer.d.ts +0 -2
  190. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  191. package/dist/server/skippable-prefix.d.ts +3 -0
  192. package/dist/server/skippable-prefix.d.ts.map +1 -1
  193. package/dist/server/slot-resolver.d.ts +18 -19
  194. package/dist/server/slot-resolver.d.ts.map +1 -1
  195. package/dist/server/ssr-bridge-types.d.ts +11 -0
  196. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  197. package/dist/server/ssr-entry.d.ts +0 -1
  198. package/dist/server/ssr-entry.d.ts.map +1 -1
  199. package/dist/server/state-tree-diff.d.ts +9 -16
  200. package/dist/server/state-tree-diff.d.ts.map +1 -1
  201. package/dist/server/static-generator.d.ts.map +1 -1
  202. package/dist/server/status-code-resolver.d.ts +8 -1
  203. package/dist/server/status-code-resolver.d.ts.map +1 -1
  204. package/dist/server/tree-builder.d.ts +28 -43
  205. package/dist/server/tree-builder.d.ts.map +1 -1
  206. package/dist/server/types.d.ts +12 -7
  207. package/dist/server/types.d.ts.map +1 -1
  208. package/dist/server/utils/element-type.d.ts +40 -0
  209. package/dist/server/utils/element-type.d.ts.map +1 -0
  210. package/dist/shared/rsc-media-type.d.ts +40 -0
  211. package/dist/shared/rsc-media-type.d.ts.map +1 -0
  212. package/dist/shared/segment-info.d.ts +7 -0
  213. package/dist/shared/segment-info.d.ts.map +1 -1
  214. package/docs/api/30-api-server.mdx +1 -1
  215. package/docs/api/33-api-search-params.mdx +38 -16
  216. package/docs/api/35-api-typescript.mdx +3 -3
  217. package/docs/api/36-cli.mdx +34 -7
  218. package/docs/learn/00-introduction.mdx +1 -1
  219. package/docs/learn/02-pages-and-layouts.mdx +1 -1
  220. package/docs/learn/05-typed-params.mdx +8 -8
  221. package/docs/learn/07-typed-routes.mdx +12 -7
  222. package/docs/learn/11-error-handling.mdx +16 -0
  223. package/docs/more/01-advanced-routing.mdx +1 -1
  224. package/docs/more/03-coming-from-nextjs.mdx +2 -2
  225. package/docs/more/50-ai-agent-instructions.mdx +6 -4
  226. package/package.json +8 -5
  227. package/src/adapters/cloudflare.ts +4 -1
  228. package/src/adapters/compress-module.ts +79 -1
  229. package/src/cli-check.ts +458 -0
  230. package/src/cli.ts +59 -24
  231. package/src/client/browser-entry/action-dispatch.ts +2 -1
  232. package/src/client/browser-entry/index.ts +0 -5
  233. package/src/client/error-boundary.tsx +65 -1
  234. package/src/client/index.ts +14 -3
  235. package/src/client/link.tsx +65 -64
  236. package/src/client/navigation-commit.ts +27 -4
  237. package/src/client/params-context.ts +4 -4
  238. package/src/client/router-pipeline.ts +4 -0
  239. package/src/client/router.ts +1 -0
  240. package/src/client/rsc-fetch.ts +14 -4
  241. package/src/client/segment-cache.ts +26 -5
  242. package/src/client/use-query-states.ts +102 -39
  243. package/src/cookies/define-cookie.ts +6 -1
  244. package/src/index.ts +20 -3
  245. package/src/plugin-context.ts +26 -0
  246. package/src/plugins/routing.ts +84 -146
  247. package/src/plugins/shims.ts +0 -1
  248. package/src/plugins/static-build.ts +78 -24
  249. package/src/routing/codegen-shared.ts +3 -79
  250. package/src/routing/codegen-types.ts +10 -31
  251. package/src/routing/codegen-write.ts +139 -0
  252. package/src/routing/codegen.ts +56 -182
  253. package/src/routing/convention-lint.ts +139 -40
  254. package/src/routing/export-detect.ts +151 -7
  255. package/src/routing/link-codegen.ts +32 -65
  256. package/src/routing/manifest-codegen.ts +1 -59
  257. package/src/routing/scanner.ts +3 -3
  258. package/src/routing/segment-keys.ts +37 -0
  259. package/src/routing/types.ts +0 -6
  260. package/src/schema-bridge.ts +180 -58
  261. package/src/search-params/define.ts +102 -37
  262. package/src/search-params/index.ts +0 -1
  263. package/src/search-params/parse-total.ts +78 -0
  264. package/src/search-params/wrappers.ts +60 -11
  265. package/src/server/access-gate.tsx +61 -68
  266. package/src/server/action-handler.ts +1 -4
  267. package/src/server/als-registry.ts +16 -0
  268. package/src/server/chain-url-parts.ts +2 -3
  269. package/src/server/compress.ts +9 -1
  270. package/src/server/deny-boundary.ts +269 -41
  271. package/src/server/deny-renderer.ts +32 -21
  272. package/src/server/error-boundary-wrapper.ts +166 -79
  273. package/src/server/index.ts +1 -3
  274. package/src/server/internal.ts +2 -2
  275. package/src/server/metadata-collector.ts +115 -0
  276. package/src/server/param-coercion.ts +13 -61
  277. package/src/server/pipeline-helpers.ts +2 -2
  278. package/src/server/pipeline-outcome.ts +2 -1
  279. package/src/server/pipeline-phases.ts +9 -9
  280. package/src/server/primitives.ts +25 -0
  281. package/src/server/route-element-builder.ts +169 -194
  282. package/src/server/rsc-cache-key-guard.ts +2 -8
  283. package/src/server/rsc-entry/deny-fallback.ts +3 -2
  284. package/src/server/rsc-entry/error-renderer.ts +31 -11
  285. package/src/server/rsc-entry/helpers.ts +3 -12
  286. package/src/server/rsc-entry/index.ts +0 -5
  287. package/src/server/rsc-entry/render-route.ts +14 -11
  288. package/src/server/rsc-entry/revalidate-renderer.ts +2 -1
  289. package/src/server/rsc-entry/rsc-payload.ts +104 -20
  290. package/src/server/rsc-entry/rsc-stream.ts +25 -2
  291. package/src/server/rsc-entry/ssr-renderer.ts +28 -16
  292. package/src/server/skippable-prefix.ts +21 -39
  293. package/src/server/slot-resolver.ts +69 -189
  294. package/src/server/ssr-bridge-types.ts +12 -0
  295. package/src/server/ssr-entry.ts +8 -6
  296. package/src/server/state-tree-diff.ts +11 -62
  297. package/src/server/static-generator.ts +3 -2
  298. package/src/server/status-code-resolver.ts +28 -11
  299. package/src/server/tree-builder.ts +35 -228
  300. package/src/server/types.ts +12 -7
  301. package/src/server/utils/element-type.ts +72 -0
  302. package/src/shared/rsc-media-type.ts +43 -0
  303. package/src/shared/segment-info.ts +7 -0
  304. package/dist/_chunks/cache-api-Cd0VZ_Pd.js.map +0 -1
  305. package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +0 -1
  306. package/dist/_chunks/error-boundary-DpYRI_I1.js.map +0 -1
  307. package/dist/_chunks/logger-N7e5auP0.js.map +0 -1
  308. package/dist/_chunks/navigation-root-B29qg0_T.js.map +0 -1
  309. package/dist/_chunks/param-value-C8TNYchQ.js.map +0 -1
  310. package/dist/_chunks/registry-DbJPKoBp.js +0 -20
  311. package/dist/_chunks/registry-DbJPKoBp.js.map +0 -1
  312. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js +0 -119
  313. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js.map +0 -1
  314. package/dist/_chunks/segment-context-ZDnXDkbz.js +0 -34
  315. package/dist/_chunks/segment-context-ZDnXDkbz.js.map +0 -1
  316. package/dist/_chunks/use-query-states-DFvWd-EA.js.map +0 -1
  317. package/dist/_chunks/use-segment-params-ClyUNq4d.js +0 -128
  318. package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +0 -1
  319. package/dist/_chunks/walkers-BhhwI9TD.js +0 -936
  320. package/dist/_chunks/walkers-BhhwI9TD.js.map +0 -1
  321. package/dist/search-params/registry.d.ts +0 -20
  322. package/dist/search-params/registry.d.ts.map +0 -1
  323. package/dist/segment-params/define.d.ts +0 -83
  324. package/dist/segment-params/define.d.ts.map +0 -1
  325. package/dist/segment-params/index.d.ts +0 -3
  326. package/dist/segment-params/index.d.ts.map +0 -1
  327. package/dist/segment-params/index.js +0 -70
  328. package/dist/segment-params/index.js.map +0 -1
  329. package/src/search-params/registry.ts +0 -31
  330. package/src/segment-params/define.ts +0 -226
  331. package/src/segment-params/index.ts +0 -9
@@ -1,14 +1,16 @@
1
+ import { n as isRscRequest } from "../_chunks/rsc-media-type-DRqE_lD_.js";
1
2
  import { a as canonicalize, c as getMetadataRouteAutoLink, l as getMetadataRouteServePath, o as METADATA_ROUTE_CONVENTIONS, s as classifyMetadataRoute, t as computeSegmentTreePaths } from "../_chunks/segment-keys-BawYuNFO.js";
2
3
  import { n as classifyUrlSegment, r as effectiveUrlSegment } from "../_chunks/segment-classify-C539Pa2O.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-N7e5auP0.js";
4
+ import { a as timingAls, r as requestContextAls, t as earlyHintsSenderAls } from "../_chunks/als-registry-C6kcfprT.js";
5
+ import { 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, _ 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-D8xJZXIN.js";
4
6
  import { i as isRscCacheKeyShape, n as RSC_KEY_PARAM, s as rscCacheKey, t as RSC_KEY_HEADERS } from "../_chunks/rsc-cache-key-DD0fl_-s.js";
7
+ import { t as isMdxFilePath } from "../_chunks/mdx-file-C005ay-P.js";
5
8
  import { i as collectRouteModulepreloads, n as collectRouteCss, r as collectRouteFonts } from "../_chunks/build-manifest-DWppEdLB.js";
6
- import { i as coerce, t as executeAction } from "../_chunks/actions-C-Rw9vPc.js";
7
- 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-Cd0VZ_Pd.js";
8
- import "../_chunks/error-boundary-DpYRI_I1.js";
9
- import { n as toBracketKey } from "../_chunks/resolve-schema-Dz3fcFUo.js";
10
- import { t as normalizeParamValue } from "../_chunks/param-value-C8TNYchQ.js";
11
- import "../_chunks/segment-context-ZDnXDkbz.js";
9
+ import { i as coerce, t as executeAction } from "../_chunks/actions-35jnMdeJ.js";
10
+ 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-DjNrIWRR.js";
11
+ import "../_chunks/error-boundary-D-ODYX41.js";
12
+ import { n as toBracketKey } from "../_chunks/resolve-schema-5ma5pp1b.js";
13
+ import { r as normalizeParamValue } from "../_chunks/segment-context-CjOlyB8Y.js";
12
14
  import { n as rscErrorEnvelope } from "../_chunks/rsc-error-envelope-tT5PJs4q.js";
13
15
  import { createHash } from "node:crypto";
14
16
  import { readFile } from "node:fs/promises";
@@ -287,7 +289,7 @@ function cloneWithMutableHeaders(response) {
287
289
  * createFromFetch. See design/19-client-navigation.md.
288
290
  */
289
291
  function buildRedirectResponse(signal, req, headers) {
290
- if ((req.headers.get("Accept") ?? "").includes("text/x-component")) {
292
+ if (isRscRequest(req)) {
291
293
  headers.set("X-Timber-Redirect", signal.location);
292
294
  return new Response(null, {
293
295
  status: 204,
@@ -1136,6 +1138,42 @@ async function loadModule(loader) {
1136
1138
  }
1137
1139
  }
1138
1140
  //#endregion
1141
+ //#region src/server/utils/element-type.ts
1142
+ var REACT_COMPONENT_TYPE_MARKERS = /* @__PURE__ */ new Set([
1143
+ Symbol.for("react.forward_ref"),
1144
+ Symbol.for("react.memo"),
1145
+ Symbol.for("react.lazy"),
1146
+ Symbol.for("react.provider"),
1147
+ Symbol.for("react.context"),
1148
+ Symbol.for("react.suspense"),
1149
+ Symbol.for("react.suspense_list"),
1150
+ Symbol.for("react.client.reference")
1151
+ ]);
1152
+ /**
1153
+ * Validate that a loaded module's `default` export is something React
1154
+ * accepts as the first argument to `createElement` — i.e. a valid component
1155
+ * type. React doesn't export `isValidElementType` (only `isValidElement`,
1156
+ * which checks for *elements*, not *component types*), so this mirrors
1157
+ * React's internal check:
1158
+ *
1159
+ * - functions → function or class components
1160
+ * - objects with a `$$typeof` matching one of React's known component
1161
+ * markers → exotic components (`memo`, `forwardRef`, `lazy`, context,
1162
+ * suspense, client references via `@vitejs/plugin-rsc`)
1163
+ *
1164
+ * Strings (HTML tag names) are valid for `createElement` but never appear
1165
+ * as a route module's default export, so they're not recognized here.
1166
+ *
1167
+ * Anything else (numbers, plain config objects, JSON, etc.) is rejected so
1168
+ * the boundary wrapper is skipped rather than crashing inside React.
1169
+ */
1170
+ function isValidElementType(value) {
1171
+ if (typeof value === "function") return true;
1172
+ if (typeof value !== "object" || value === null) return false;
1173
+ const marker = value.$$typeof;
1174
+ return typeof marker === "symbol" && REACT_COMPONENT_TYPE_MARKERS.has(marker);
1175
+ }
1176
+ //#endregion
1139
1177
  //#region src/server/deny-boundary.ts
1140
1178
  /**
1141
1179
  * Deny boundary subsystem — the in-tree DenySignal flow.
@@ -1147,10 +1185,14 @@ async function loadModule(loader) {
1147
1185
  * `DenyPageEntry` records ordered by specificity (specific status →
1148
1186
  * category catch-all → `error.tsx`).
1149
1187
  *
1150
- * 2. **Runtime matching** (`renderMatchingDenyPage`) — picks the first
1151
- * chain entry whose status filter matches the thrown DenySignal and
1152
- * returns a React element for the matching component. Used by
1153
- * `AccessGate` and `PageDenyBoundary` when they catch a deny.
1188
+ * 2. **Runtime matching and placement** (`matchDenyEntry`, `placeDenyPage`,
1189
+ * `handleCaughtDeny`) — picks the first chain entry whose status filter
1190
+ * matches the thrown DenySignal, then decides whether the catch site may
1191
+ * render it where it stands. A chain spans the catcher outward to root,
1192
+ * so a match may belong to an ancestor segment; rendering it in place
1193
+ * would wrap it in every layout in between. Ancestor-owned matches are
1194
+ * re-thrown with `ownerKey` stamped on the signal so the error boundary
1195
+ * at the owning segment renders it. See TIM-1356.
1154
1196
  *
1155
1197
  * 3. **The page boundary itself** (`PageDenyBoundary`) — the async server
1156
1198
  * component that wraps a server-component page, calls it, and catches
@@ -1168,19 +1210,86 @@ async function loadModule(loader) {
1168
1210
  * See design/04-authorization.md, design/10-error-handling.md, TIM-666.
1169
1211
  */
1170
1212
  /**
1171
- * Find the first deny page in the chain that matches the given status code.
1172
- * Returns a React element for the matching component, or null if no match.
1213
+ * Find the first deny page in the chain whose status filter matches.
1214
+ * Returns null when nothing in the chain accepts this status.
1215
+ *
1216
+ * For 4xx statuses, the chain's stored order is correct: every status file
1217
+ * across all segments (inner→outer) before any error.tsx — so a root
1218
+ * `404.tsx` beats a leaf `error.tsx`. This matches `resolve4xx`.
1219
+ *
1220
+ * For 5xx statuses, the match walks per-segment (inner→outer): at each
1221
+ * segment, status files (`503.tsx` → `5xx.tsx`) then `error.tsx`, before
1222
+ * moving to the next segment outward. A leaf `error.tsx` therefore beats a
1223
+ * root `5xx.tsx` — the intentional inverse of the 4xx chain, matching
1224
+ * `resolve5xx`. See TIM-1360.
1173
1225
  */
1174
- function renderMatchingDenyPage(chain, status, data) {
1226
+ function matchDenyEntry(chain, status) {
1227
+ if (status >= 500) return matchDenyEntry5xx(chain, status);
1175
1228
  for (const entry of chain) {
1176
- if (entry.status === status) return renderDenyEntry(entry, status, data);
1177
- if (entry.status === 400 && status >= 400 && status <= 499) return renderDenyEntry(entry, status, data);
1178
- if (entry.status === 500 && status >= 500 && status <= 599) return renderDenyEntry(entry, status, data);
1179
- if (entry.status === null) return renderDenyEntry(entry, status, data);
1229
+ if (entry.status === status) return entry;
1230
+ if (entry.status === 400 && status >= 400 && status <= 499) return entry;
1231
+ if (entry.status === null) return entry;
1232
+ }
1233
+ return null;
1234
+ }
1235
+ /**
1236
+ * 5xx per-segment walk: at each segment (inner→outer), check status files
1237
+ * then error.tsx before moving outward. Mirrors `resolve5xx`.
1238
+ */
1239
+ function matchDenyEntry5xx(chain, status) {
1240
+ const segmentIndices = [];
1241
+ for (const entry of chain) if (!segmentIndices.includes(entry.segmentIndex)) segmentIndices.push(entry.segmentIndex);
1242
+ segmentIndices.sort((a, b) => b - a);
1243
+ for (const seg of segmentIndices) {
1244
+ for (const entry of chain) {
1245
+ if (entry.segmentIndex !== seg || entry.kind === "error") continue;
1246
+ if (entry.status === status) return entry;
1247
+ if (entry.status === 500 && status >= 500 && status <= 599) return entry;
1248
+ }
1249
+ for (const entry of chain) {
1250
+ if (entry.segmentIndex !== seg || entry.kind !== "error") continue;
1251
+ return entry;
1252
+ }
1180
1253
  }
1181
1254
  return null;
1182
1255
  }
1183
1256
  /**
1257
+ * Decide where the deny page for `status` renders, from the perspective of
1258
+ * the catch site at `selfKey`.
1259
+ *
1260
+ * The match itself is unchanged — first entry in fallback order wins, so
1261
+ * every path picks the same file. What this adds is the position: a chain
1262
+ * built at segment i spans i outward to root, so a matched entry is owned
1263
+ * either by the catcher or by one of its ancestors.
1264
+ *
1265
+ * - Owned by the catcher → `render`. The catcher already sits at the
1266
+ * owning segment's position.
1267
+ * - Owned by an ancestor → `hoist`. Rendering here would wrap the deny page
1268
+ * in every layout between the owner and the catcher. The caller re-throws
1269
+ * with `ownerKey` stamped on the signal so the boundary at that segment
1270
+ * renders it instead — inside the owner's layout, outside the rest.
1271
+ *
1272
+ * Returns null when no entry matches; the caller re-throws unannotated and
1273
+ * the re-render fallback resolves it.
1274
+ *
1275
+ * See design/04-authorization.md §"Where a Deny Page Renders", TIM-1356.
1276
+ */
1277
+ function placeDenyPage(chain, status, data, selfKey) {
1278
+ const entry = matchDenyEntry(chain, status);
1279
+ if (!entry) return null;
1280
+ if (entry.ownerKey !== selfKey) {
1281
+ if (entry.isMdx) return null;
1282
+ return {
1283
+ kind: "hoist",
1284
+ ownerKey: entry.ownerKey
1285
+ };
1286
+ }
1287
+ return {
1288
+ kind: "render",
1289
+ element: renderDenyEntry(entry, status, data)
1290
+ };
1291
+ }
1292
+ /**
1184
1293
  * Build the element for a matched deny chain entry with the props contract
1185
1294
  * the file's convention documents:
1186
1295
  *
@@ -1211,6 +1320,37 @@ function renderDenyEntry(entry, status, data) {
1211
1320
  });
1212
1321
  }
1213
1322
  /**
1323
+ * The whole of what an in-tree catch site does with a `DenySignal`, shared
1324
+ * by all three of them (`PageDenyBoundary`, `TracedLayout`, `AccessGate`).
1325
+ *
1326
+ * Returns the deny page element when this catch site owns the matched file.
1327
+ * Otherwise it throws — annotated with `ownerKey` when an ancestor owns the
1328
+ * file, bare when nothing matched at all.
1329
+ *
1330
+ * `setDenyStatus` runs on the hoist path too, and must run *before* the
1331
+ * throw: on the RSC payload path no boundary executes server-side, so the
1332
+ * ALS status is the only thing standing between the response and a 200
1333
+ * (`rsc-payload.ts` reads it after the stream settles).
1334
+ *
1335
+ * Either branch leaves part of the matched chain unmounted — hoisting sends
1336
+ * the render to a boundary that discards everything below it, and a layout
1337
+ * that denies returns the deny page instead of its own child outlet. What the
1338
+ * response then says about the tree is decided from the deny status alone in
1339
+ * `rsc-payload.ts`, not tracked here; see the note there for why.
1340
+ */
1341
+ function handleCaughtDeny(chain, error, selfKey) {
1342
+ const placement = chain ? placeDenyPage(chain, error.status, error.data, selfKey) : null;
1343
+ if (placement?.kind === "render") {
1344
+ setDenyStatus(error.status);
1345
+ return placement.element;
1346
+ }
1347
+ if (placement?.kind === "hoist") {
1348
+ setDenyStatus(error.status);
1349
+ error.ownerKey = placement.ownerKey;
1350
+ }
1351
+ throw error;
1352
+ }
1353
+ /**
1214
1354
  * Set the deny status in the request context ALS.
1215
1355
  * Called from AccessGate / PageDenyBoundary when a DenySignal is caught.
1216
1356
  * The pipeline reads this after render to set the HTTP status code.
@@ -1222,6 +1362,17 @@ function setDenyStatus(status) {
1222
1362
  store.onDenyStatus?.();
1223
1363
  }
1224
1364
  }
1365
+ /**
1366
+ * Record the first denied segment index in the request context ALS.
1367
+ * Called from AccessGate when a DenySignal is caught during render.
1368
+ * Only the first (shallowest) denial is recorded — deeper denials are
1369
+ * unreachable because the gate short-circuits the subtree.
1370
+ * See TIM-1363.
1371
+ */
1372
+ function setFirstDeniedSegmentIndex(index) {
1373
+ const store = requestContextAls.getStore();
1374
+ if (store && store._firstDeniedSegmentIndex === void 0) store._firstDeniedSegmentIndex = index;
1375
+ }
1225
1376
  //#endregion
1226
1377
  //#region src/server/access-gate.tsx
1227
1378
  /**
@@ -1241,37 +1392,37 @@ function setDenyStatus(status) {
1241
1392
  /**
1242
1393
  * Framework-injected access gate for segments.
1243
1394
  *
1244
- * When a pre-computed `verdict` prop is provided (from the pre-render pass
1245
- * in route-element-builder.ts), AccessGate replays it synchronously — no
1246
- * async, no re-execution of access.ts, immune to Suspense timing. The OTEL
1247
- * span was already emitted during the pre-render pass.
1248
- *
1249
- * When no verdict is provided (backward compat with tree-builder.ts),
1250
- * AccessGate calls accessFn directly with OTEL instrumentation.
1395
+ * Calls accessFn during render — inside `renderToReadableStream`, sharing
1396
+ * the same `React.cache` scope as layouts and pages. A `requireUser()`
1397
+ * call in access.ts populates the cache; the same call in any layout is
1398
+ * a free hit. Access executes exactly once per request. See TIM-1363.
1251
1399
  *
1252
1400
  * access.ts is a pure gate — return values are discarded. The layout below
1253
1401
  * gets the same data by calling the same cached functions (React.cache dedup).
1402
+ *
1403
+ * On denial the gate renders the matching deny page only when this segment
1404
+ * owns it. Because the gate is wrapped OUTSIDE its own layout, that render
1405
+ * omits the denying segment's layout — deliberate, and the one place the
1406
+ * in-tree path diverges from `renderDenyPage`'s inclusive slice: a layout
1407
+ * whose access.ts denied must never run and serialize protected data into
1408
+ * the Flight stream (TIM-1074). An ancestor-owned page is hoisted instead,
1409
+ * because the gate also sits inside every layout above it and rendering
1410
+ * here would wrap the deny page in all of them (TIM-1356).
1411
+ *
1412
+ * For skipped segments (skippedSegment=true), deny pages are NOT rendered
1413
+ * in-tree — the client's cached layout chrome would wrap them incorrectly.
1414
+ * Instead, the deny is recorded in ALS and rethrown so the response path
1415
+ * can return rscErrorEnvelope for a client hard-navigate.
1254
1416
  */
1255
1417
  function AccessGate(props) {
1256
- const { accessFn, segmentName, verdict, denyPages, children } = props;
1257
- if (verdict !== void 0) {
1258
- if (verdict === "pass") return children;
1259
- if (verdict instanceof DenySignal && denyPages) {
1260
- const denyElement = renderMatchingDenyPage(denyPages, verdict.status, verdict.data);
1261
- if (denyElement) {
1262
- setDenyStatus(verdict.status);
1263
- return denyElement;
1264
- }
1265
- }
1266
- throw verdict;
1267
- }
1268
- return accessGateFallback(accessFn, segmentName, denyPages, children);
1418
+ const { accessFn, segmentName, segmentKey, segmentIndex, denyPages, skippedSegment, onSettled, children } = props;
1419
+ return accessGateFallback(accessFn, segmentName, segmentKey, segmentIndex, skippedSegment ? void 0 : denyPages, skippedSegment, onSettled, children);
1269
1420
  }
1270
1421
  /**
1271
- * Async fallback for AccessGate when no pre-computed verdict is available.
1272
- * Calls accessFn with OTEL instrumentation.
1422
+ * Calls accessFn during render with OTEL instrumentation.
1423
+ * Records the first denied segment index in ALS for post-render consumers.
1273
1424
  */
1274
- async function accessGateFallback(accessFn, segmentName, denyPages, children) {
1425
+ async function accessGateFallback(accessFn, segmentName, segmentKey, segmentIndex, denyPages, skippedSegment, onSettled, children) {
1275
1426
  try {
1276
1427
  await withSpan("timber.access", { "timber.segment": segmentName ?? "unknown" }, async () => {
1277
1428
  try {
@@ -1287,14 +1438,17 @@ async function accessGateFallback(accessFn, segmentName, denyPages, children) {
1287
1438
  }
1288
1439
  });
1289
1440
  } catch (error) {
1290
- if (error instanceof DenySignal && denyPages) {
1291
- const denyElement = renderMatchingDenyPage(denyPages, error.status, error.data);
1292
- if (denyElement) {
1293
- setDenyStatus(error.status);
1294
- return denyElement;
1441
+ if (error instanceof DenySignal) {
1442
+ if (segmentIndex !== void 0) setFirstDeniedSegmentIndex(segmentIndex);
1443
+ if (skippedSegment) {
1444
+ error._hardNavigate = true;
1445
+ throw error;
1295
1446
  }
1447
+ if (denyPages) return handleCaughtDeny(denyPages, error, segmentKey);
1296
1448
  }
1297
1449
  throw error;
1450
+ } finally {
1451
+ onSettled?.();
1298
1452
  }
1299
1453
  return children;
1300
1454
  }
@@ -1313,13 +1467,7 @@ async function accessGateFallback(accessFn, segmentName, denyPages, children) {
1313
1467
  * slot doesn't make architectural sense.
1314
1468
  */
1315
1469
  async function SlotAccessGate(props) {
1316
- const { accessFn, DeniedComponent, slotName, createElement, defaultFallback, children, verdict } = props;
1317
- if (verdict !== void 0) {
1318
- if (verdict === "pass") return children;
1319
- if (verdict instanceof DenySignal) return buildDeniedFallback(DeniedComponent, slotName, verdict.data, createElement) ?? defaultFallback ?? null;
1320
- 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.");
1321
- return buildDeniedFallback(DeniedComponent, slotName, void 0, createElement) ?? defaultFallback ?? null;
1322
- }
1470
+ const { accessFn, DeniedComponent, slotName, createElement, defaultFallback, children } = props;
1323
1471
  try {
1324
1472
  await accessFn();
1325
1473
  } catch (error) {
@@ -1716,75 +1864,131 @@ function wrapInSlotsProvider(shell, slotValues) {
1716
1864
  });
1717
1865
  }
1718
1866
  //#endregion
1867
+ //#region src/server/error-boundary-wrapper.ts
1868
+ /**
1869
+ * Load a status/error file's component, or `null` if the boundary should be skipped.
1870
+ *
1871
+ * Two ways to get `null`, both deliberate:
1872
+ * - the module fails to load (syntax error in the error page) — building an
1873
+ * error boundary must not itself throw, so the boundary is skipped (TIM-584);
1874
+ * - the default export is not a valid React component type, which would
1875
+ * otherwise crash inside `createElement`.
1876
+ */
1877
+ async function loadBoundaryComponent(file, loadModule) {
1878
+ let mod;
1879
+ try {
1880
+ mod = await loadModule(file);
1881
+ } catch {
1882
+ return null;
1883
+ }
1884
+ return isValidElementType(mod?.default) ? mod.default : null;
1885
+ }
1886
+ /**
1887
+ * Wrap `element` in one error boundary rendering `Component` on failure.
1888
+ *
1889
+ * MDX status files are server components and cannot be passed as function
1890
+ * props to `TimberErrorBoundary` (a 'use client' component). They are
1891
+ * pre-rendered as elements and passed as `fallbackElement` instead. See TIM-503.
1892
+ *
1893
+ * @param status boundary status filter — omitted entirely for `error.tsx`
1894
+ * @param fallbackProps props handed to an MDX fallback when it is pre-rendered
1895
+ */
1896
+ function wrapInBoundary(element, Component, file, status, fallbackProps, config) {
1897
+ const props = isMdxFilePath(file.filePath) ? {
1898
+ fallbackElement: config.createElement(Component, fallbackProps),
1899
+ children: element
1900
+ } : {
1901
+ fallbackComponent: Component,
1902
+ children: element
1903
+ };
1904
+ if (status !== void 0) props.status = status;
1905
+ if (config.segmentKey !== void 0) props.segmentKey = config.segmentKey;
1906
+ return config.createElement(config.errorBoundaryComponent, props);
1907
+ }
1908
+ /**
1909
+ * Wrap an element in the error boundaries declared by a route segment.
1910
+ *
1911
+ * Wrapping is applied inside-out — the last wrap produces the outermost
1912
+ * boundary — so the processing order below is innermost to outermost:
1913
+ *
1914
+ * 1. Specific status files (e.g. `404.tsx`, `503.tsx`) — highest priority at runtime
1915
+ * 2. Category catch-alls (`4xx.tsx`, `5xx.tsx`)
1916
+ * 3. `error.tsx` — catches anything not matched by a status file
1917
+ *
1918
+ * The order is load-bearing: it decides which boundary catches a given throw.
1919
+ *
1920
+ * See design/10-error-handling.md §"Format-Aware Fallback Chains".
1921
+ */
1922
+ async function wrapSegmentWithErrorBoundaries(segment, element, config) {
1923
+ const { loadModule, skipDenyBoundaries = false } = config;
1924
+ if (segment.statusFiles) {
1925
+ for (const [key, file] of Object.entries(segment.statusFiles)) {
1926
+ if (key === "4xx" || key === "5xx") continue;
1927
+ const status = parseInt(key, 10);
1928
+ if (isNaN(status)) continue;
1929
+ if (skipDenyBoundaries && status >= 400 && status <= 499) continue;
1930
+ const Component = await loadBoundaryComponent(file, loadModule);
1931
+ if (Component) element = wrapInBoundary(element, Component, file, status, { status }, config);
1932
+ }
1933
+ for (const [key, file] of Object.entries(segment.statusFiles)) {
1934
+ if (key !== "4xx" && key !== "5xx") continue;
1935
+ if (skipDenyBoundaries && key === "4xx") continue;
1936
+ const Component = await loadBoundaryComponent(file, loadModule);
1937
+ if (Component) element = wrapInBoundary(element, Component, file, key === "4xx" ? 400 : 500, {}, config);
1938
+ }
1939
+ }
1940
+ if (segment.error) {
1941
+ const Component = await loadBoundaryComponent(segment.error, loadModule);
1942
+ if (Component) element = wrapInBoundary(element, Component, segment.error, void 0, {}, config);
1943
+ }
1944
+ return element;
1945
+ }
1946
+ //#endregion
1719
1947
  //#region src/server/param-coercion.ts
1720
1948
  /** Global schema codecs, set once at startup from virtual:timber-schema. */
1721
1949
  var _globalCodecs = null;
1722
1950
  /**
1723
1951
  * Run segment param coercion on the matched route's segments.
1724
1952
  *
1725
- * When `globalCodecs` is provided (from app/schema.ts), uses the global
1726
- * codec map keyed by bare param name. Otherwise falls back to loading
1727
- * per-segment params.ts modules.
1953
+ * Codecs come from `app/schema.ts` and nowhere else (TIM-1342 completed
1954
+ * design/41-global-params.md migration step 9). A route with no schema, or
1955
+ * a param the schema does not declare, keeps its raw string — which is the
1956
+ * honest type for an uninterpreted URL segment.
1728
1957
  *
1729
1958
  * Throws ParamCoercionError if any codec fails (→ 404).
1730
1959
  *
1731
1960
  * This runs BEFORE middleware, so ctx.segmentParams is already typed.
1732
1961
  * See design/07-routing.md §"Where Coercion Runs"
1733
1962
  * See design/41-global-params.md §"Pipeline Integration"
1963
+ *
1964
+ * Stays `async` with nothing to await: it is injected as a
1965
+ * `CoerceSegmentParamsFn` into the revalidation renderer and the action
1966
+ * middleware runner, both of which await it. Narrowing the return type to
1967
+ * `void` would break those contracts and remove a microtask boundary the
1968
+ * pipeline's ordering has always had, for no gain.
1734
1969
  */
1735
1970
  async function coerceSegmentParams(match) {
1736
1971
  const globalCodecs = _globalCodecs;
1737
1972
  const mergeTarget = Object.create(null);
1738
1973
  for (const key of Object.keys(match.segmentParams)) if (key !== "__proto__") mergeTarget[key] = match.segmentParams[key];
1739
1974
  match.segmentParams = mergeTarget;
1740
- if (globalCodecs) {
1741
- for (const node of match.segments) {
1742
- const segment = effectiveUrlSegment(node);
1743
- if (!segment.paramName) continue;
1744
- const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
1745
- const codec = globalCodecs[bracketKey];
1746
- if (!codec) continue;
1747
- const key = segment.paramName;
1748
- if (!(key in mergeTarget)) continue;
1749
- let parsed;
1750
- try {
1751
- parsed = codec.parse(mergeTarget[key]);
1752
- } catch (err) {
1753
- const message = err instanceof Error ? err.message : String(err);
1754
- 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}'.`);
1755
- throw new ParamCoercionError(message);
1756
- }
1757
- mergeTarget[key] = normalizeParamValue(parsed, key);
1758
- }
1759
- return;
1760
- }
1761
- for (const segment of match.segments) {
1762
- if (!segment.params) continue;
1763
- let mod;
1764
- try {
1765
- mod = await loadModule(segment.params);
1766
- } catch (err) {
1767
- const message = `Failed to load params module for segment "${segment.segmentName}": ${err instanceof Error ? err.message : String(err)}`;
1768
- if (isDebug()) console.warn(`[timber] Param coercion error: ${message}\n Segment: ${segment.segmentName}\n Params file: ${segment.params}`);
1769
- throw new ParamCoercionError(message);
1770
- }
1771
- const segmentParamsDef = mod.segmentParams;
1772
- if (!segmentParamsDef || typeof segmentParamsDef.parse !== "function") continue;
1773
- let coerced;
1975
+ if (globalCodecs) for (const node of match.segments) {
1976
+ const segment = effectiveUrlSegment(node);
1977
+ if (!segment.paramName) continue;
1978
+ const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
1979
+ const codec = globalCodecs[bracketKey];
1980
+ if (!codec) continue;
1981
+ const key = segment.paramName;
1982
+ if (!(key in mergeTarget)) continue;
1983
+ let parsed;
1774
1984
  try {
1775
- coerced = segmentParamsDef.parse(match.segmentParams);
1985
+ parsed = codec.parse(mergeTarget[key]);
1776
1986
  } catch (err) {
1777
1987
  const message = err instanceof Error ? err.message : String(err);
1778
- if (isDebug()) {
1779
- const rawKeys = Object.keys(match.segmentParams).join(", ");
1780
- 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.`);
1781
- }
1988
+ 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}'.`);
1782
1989
  throw new ParamCoercionError(message);
1783
1990
  }
1784
- for (const key of Object.keys(coerced)) {
1785
- if (key === "__proto__") continue;
1786
- mergeTarget[key] = normalizeParamValue(coerced[key], key);
1787
- }
1991
+ mergeTarget[key] = normalizeParamValue(parsed, key);
1788
1992
  }
1789
1993
  }
1790
1994
  //#endregion
@@ -2007,7 +2211,7 @@ function parseClientStateTree(req) {
2007
2211
  //#endregion
2008
2212
  //#region src/server/route-element-builder.ts
2009
2213
  /**
2010
- * Thrown when a defineSegmentParams codec's parse() fails.
2214
+ * Thrown when an app/schema.ts segment-param codec's parse() fails.
2011
2215
  * The pipeline catches this and responds with 404.
2012
2216
  */
2013
2217
  var ParamCoercionError = class extends Error {
@@ -2150,7 +2354,7 @@ async function outcomeToResponse(config, outcome, ctx) {
2150
2354
  });
2151
2355
  }
2152
2356
  case "error": {
2153
- const isRsc = (ctx.req.headers.get("Accept") ?? "").includes("text/x-component");
2357
+ const isRsc = isRscRequest(ctx.req);
2154
2358
  if (outcome.phase === "proxy") {
2155
2359
  logProxyError({ error: outcome.error });
2156
2360
  await fireOnRequestError(outcome.error, ctx.req, "proxy");
@@ -2437,8 +2641,8 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
2437
2641
  if (config.onPipelineError && error instanceof Error) config.onPipelineError(error, "auto-sitemap");
2438
2642
  return new Response(null, { status: 500 });
2439
2643
  }
2440
- const isRscRequest = (req.headers.get("Accept") ?? "").includes("text/x-component");
2441
- if (isRscRequest) {
2644
+ const isRsc = isRscRequest(req);
2645
+ if (isRsc) {
2442
2646
  if (!checkVersionSkew(req).ok) {
2443
2647
  const reloadHeaders = new Headers();
2444
2648
  applyReloadHeaders(reloadHeaders);
@@ -2450,7 +2654,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
2450
2654
  }
2451
2655
  let match = config.matchRoute(canonicalPathname);
2452
2656
  let interception;
2453
- if (isRscRequest && config.interceptionRewrites?.length) {
2657
+ if (isRsc && config.interceptionRewrites?.length) {
2454
2658
  const rawSourceUrl = req.headers.get("X-Timber-URL");
2455
2659
  const validatedSourceUrl = rawSourceUrl ? validateInterceptionHeader(rawSourceUrl, stripTrailingSlash) : null;
2456
2660
  if (validatedSourceUrl) {
@@ -2486,7 +2690,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
2486
2690
  responseHeaders.set("Cache-Control", "private, no-cache, no-store, max-age=0, must-revalidate");
2487
2691
  const leafSegment = match.segments[match.segments.length - 1];
2488
2692
  const isApiRoute = leafSegment?.route && !leafSegment?.page;
2489
- if (config.earlyHints && !isRscRequest && !isApiRoute) try {
2693
+ if (config.earlyHints && !isRsc && !isApiRoute) try {
2490
2694
  await config.earlyHints(match, req, responseHeaders);
2491
2695
  } catch (err) {
2492
2696
  swallow(err, "early hints hook threw");
@@ -2498,7 +2702,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
2498
2702
  if (error instanceof ParamCoercionError) {
2499
2703
  if (isDebug()) {
2500
2704
  const segmentChain = match.segments.map((s) => s.segmentName || "/").join(" → ");
2501
- 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).`);
2705
+ console.warn(`[timber] Param coercion failed for ${method} ${canonicalPathname} — responding with 404\n Matched segments: ${segmentChain}\n Error: ${error.message}\n This usually means a codec in app/schema.ts rejected the URL value.\n Check the codec for the bracket key of the segment named above.`);
2502
2706
  }
2503
2707
  const leafSegment = match.segments[match.segments.length - 1];
2504
2708
  if (leafSegment.route && !leafSegment.page) return new Response(null, { status: 404 });
@@ -2610,12 +2814,6 @@ async function hasValidRscCacheKey(req) {
2610
2814
  return claims.every((claim) => claim === expected);
2611
2815
  }
2612
2816
  /**
2613
- * Media type of a Flight payload. Also spelled in `rsc-entry/helpers.ts`,
2614
- * `deny-renderer.ts`, `action-handler.ts`, and `client/rsc-fetch.ts` — that
2615
- * duplication predates this module and is tracked separately.
2616
- */
2617
- var RSC_MEDIA_TYPE = "text/x-component";
2618
- /**
2619
2817
  * Ensure a Flight payload can never be interpreted as a document.
2620
2818
  *
2621
2819
  * Applied here, at the pipeline's single exit, rather than where each
@@ -2633,7 +2831,7 @@ var RSC_MEDIA_TYPE = "text/x-component";
2633
2831
  function hardenRscPayloadResponse(response) {
2634
2832
  const contentType = response.headers.get("content-type");
2635
2833
  if (!contentType) return;
2636
- if (contentType.split(";")[0].trim().toLowerCase() !== RSC_MEDIA_TYPE) return;
2834
+ if (contentType.split(";")[0].trim().toLowerCase() !== "text/x-component") return;
2637
2835
  response.headers.set("X-Content-Type-Options", "nosniff");
2638
2836
  }
2639
2837
  /**
@@ -2958,40 +3156,6 @@ function sendEarlyHints103(links) {
2958
3156
  }
2959
3157
  //#endregion
2960
3158
  //#region src/server/tree-builder.ts
2961
- var REACT_COMPONENT_TYPE_MARKERS = /* @__PURE__ */ new Set([
2962
- Symbol.for("react.forward_ref"),
2963
- Symbol.for("react.memo"),
2964
- Symbol.for("react.lazy"),
2965
- Symbol.for("react.provider"),
2966
- Symbol.for("react.context"),
2967
- Symbol.for("react.suspense"),
2968
- Symbol.for("react.suspense_list"),
2969
- Symbol.for("react.client.reference")
2970
- ]);
2971
- /**
2972
- * Validate that a loaded module's `default` export is something React
2973
- * accepts as the first argument to `createElement` — i.e. a valid component
2974
- * type. React doesn't export `isValidElementType` (only `isValidElement`,
2975
- * which checks for *elements*, not *component types*), so this mirrors
2976
- * React's internal check:
2977
- *
2978
- * - functions → function or class components
2979
- * - objects with a `$$typeof` matching one of React's known component
2980
- * markers → exotic components (`memo`, `forwardRef`, `lazy`, context,
2981
- * suspense, client references via `@vitejs/plugin-rsc`)
2982
- *
2983
- * Strings (HTML tag names) are valid for `createElement` but never appear
2984
- * as a route module's default export, so they're not recognized here.
2985
- *
2986
- * Anything else (numbers, plain config objects, JSON, etc.) is rejected so
2987
- * the boundary wrapper is skipped rather than crashing inside React.
2988
- */
2989
- function isValidElementType(value) {
2990
- if (typeof value === "function") return true;
2991
- if (typeof value !== "object" || value === null) return false;
2992
- const marker = value.$$typeof;
2993
- return typeof marker === "symbol" && REACT_COMPONENT_TYPE_MARKERS.has(marker);
2994
- }
2995
3159
  /**
2996
3160
  * Build the unified element tree from a matched segment chain.
2997
3161
  *
@@ -3017,7 +3181,11 @@ async function buildElementTree(config) {
3017
3181
  let element = createElement(PageComponent, {});
3018
3182
  for (let i = segments.length - 1; i >= 0; i--) {
3019
3183
  const segment = segments[i];
3020
- element = await wrapWithErrorBoundaries(segment, element, loadModule, createElement, errorBoundaryComponent);
3184
+ element = await wrapSegmentWithErrorBoundaries(segment, element, {
3185
+ loadModule,
3186
+ createElement,
3187
+ errorBoundaryComponent
3188
+ });
3021
3189
  if (segment.access) {
3022
3190
  const accessFn = (await loadModule(segment.access)).default;
3023
3191
  element = createElement("timber:access-gate", {
@@ -3058,7 +3226,11 @@ async function buildSlotElement(slotNode, loadModule, createElement, errorBounda
3058
3226
  const DefaultComponent = (slotNode.default ? await loadModule(slotNode.default) : null)?.default;
3059
3227
  if (!PageComponent) return DefaultComponent ? createElement(DefaultComponent, {}) : null;
3060
3228
  let element = createElement(PageComponent, {});
3061
- element = await wrapWithErrorBoundaries(slotNode, element, loadModule, createElement, errorBoundaryComponent);
3229
+ element = await wrapSegmentWithErrorBoundaries(slotNode, element, {
3230
+ loadModule,
3231
+ createElement,
3232
+ errorBoundaryComponent
3233
+ });
3062
3234
  if (slotNode.access) {
3063
3235
  const accessFn = (await loadModule(slotNode.access)).default;
3064
3236
  const DeniedComponent = (slotNode.denied ? await loadModule(slotNode.denied) : null)?.default ?? null;
@@ -3074,81 +3246,6 @@ async function buildSlotElement(slotNode, loadModule, createElement, errorBounda
3074
3246
  }
3075
3247
  return element;
3076
3248
  }
3077
- /** MDX/markdown extensions — these are server components that cannot be passed as function props. */
3078
- var MDX_EXTENSIONS = /* @__PURE__ */ new Set(["mdx", "md"]);
3079
- /**
3080
- * Check if a route file is an MDX/markdown file based on its extension.
3081
- * MDX components are server components by default and cannot cross the
3082
- * RSC→client boundary as function props. They must be pre-rendered as
3083
- * elements and passed as fallbackElement instead of fallbackComponent.
3084
- */
3085
- function isMdxFile(file) {
3086
- return MDX_EXTENSIONS.has(file.extension);
3087
- }
3088
- /**
3089
- * Wrap an element with error boundaries from a segment's status-code files.
3090
- *
3091
- * Wrapping order (innermost to outermost):
3092
- * 1. Specific status files (503.tsx, 429.tsx, etc.)
3093
- * 2. Category catch-alls (4xx.tsx, 5xx.tsx)
3094
- * 3. error.tsx (general error boundary)
3095
- *
3096
- * This creates the fallback chain described in design/10-error-handling.md.
3097
- *
3098
- * MDX status files are server components and cannot be passed as function
3099
- * props to TimberErrorBoundary (a 'use client' component). Instead, they
3100
- * are pre-rendered as elements and passed as fallbackElement. The error
3101
- * boundary renders the element directly when an error is caught.
3102
- * See TIM-503.
3103
- */
3104
- async function wrapWithErrorBoundaries(segment, element, loadModule, createElement, errorBoundaryComponent) {
3105
- if (segment.statusFiles) {
3106
- for (const [key, file] of Object.entries(segment.statusFiles)) if (key !== "4xx" && key !== "5xx") {
3107
- const status = parseInt(key, 10);
3108
- if (!isNaN(status)) {
3109
- const mod = await loadModule(file);
3110
- const Component = isValidElementType(mod.default) ? mod.default : null;
3111
- if (Component) element = createElement(errorBoundaryComponent, isMdxFile(file) ? {
3112
- fallbackElement: createElement(Component, { status }),
3113
- status,
3114
- children: element
3115
- } : {
3116
- fallbackComponent: Component,
3117
- status,
3118
- children: element
3119
- });
3120
- }
3121
- }
3122
- for (const [key, file] of Object.entries(segment.statusFiles)) if (key === "4xx" || key === "5xx") {
3123
- const mod = await loadModule(file);
3124
- const Component = isValidElementType(mod.default) ? mod.default : null;
3125
- if (Component) {
3126
- const categoryStatus = key === "4xx" ? 400 : 500;
3127
- element = createElement(errorBoundaryComponent, isMdxFile(file) ? {
3128
- fallbackElement: createElement(Component, {}),
3129
- status: categoryStatus,
3130
- children: element
3131
- } : {
3132
- fallbackComponent: Component,
3133
- status: categoryStatus,
3134
- children: element
3135
- });
3136
- }
3137
- }
3138
- }
3139
- if (segment.error) {
3140
- const errorModule = await loadModule(segment.error);
3141
- const ErrorComponent = isValidElementType(errorModule.default) ? errorModule.default : null;
3142
- if (ErrorComponent) element = createElement(errorBoundaryComponent, isMdxFile(segment.error) ? {
3143
- fallbackElement: createElement(ErrorComponent, {}),
3144
- children: element
3145
- } : {
3146
- fallbackComponent: ErrorComponent,
3147
- children: element
3148
- });
3149
- }
3150
- return element;
3151
- }
3152
3249
  //#endregion
3153
3250
  //#region src/server/status-code-resolver.ts
3154
3251
  /**
@@ -3186,12 +3283,19 @@ function lookupInGroup(group, statusStr, categoryKey, segmentIndex, status) {
3186
3283
  * @param status - The HTTP status code (4xx or 5xx).
3187
3284
  * @param segments - The matched segment chain from root (index 0) to leaf (last).
3188
3285
  * @param format - The response format family ('component' or 'json'). Defaults to 'component'.
3189
- */
3190
- function resolveStatusFile(status, segments, format = "component") {
3286
+ * @param maxIndex - Optional upper bound on the segment index to search.
3287
+ * When set, the resolver only considers segments `0..maxIndex` (inclusive),
3288
+ * skipping any segments deeper in the tree. Used by the deny renderers to
3289
+ * prevent resolving a status file below a denied segment — matching
3290
+ * `buildDenyPageChain`'s start-at-i semantics. The error renderers omit
3291
+ * this parameter, so the full chain is searched — a 500 from a leaf page
3292
+ * should use the leaf's own `5xx.tsx`. See TIM-1358.
3293
+ */
3294
+ function resolveStatusFile(status, segments, format = "component", maxIndex) {
3191
3295
  if (status < 400 || status > 599) return null;
3192
- if (format === "json") return resolveJson(status, segments);
3193
- if (status <= 499) return resolve4xx(status, segments);
3194
- return resolve5xx(status, segments);
3296
+ if (format === "json") return resolveJson(status, segments, maxIndex);
3297
+ if (status <= 499) return resolve4xx(status, segments, maxIndex);
3298
+ return resolve5xx(status, segments, maxIndex);
3195
3299
  }
3196
3300
  /**
3197
3301
  * 4xx component fallback chain — two separate full passes leaf→root.
@@ -3205,13 +3309,14 @@ function resolveStatusFile(status, segments, format = "component") {
3205
3309
  * Pass 1 — {status}.tsx → 4xx.tsx (statusFiles)
3206
3310
  * Pass 2 — error.tsx (error)
3207
3311
  */
3208
- function resolve4xx(status, segments) {
3312
+ function resolve4xx(status, segments, maxIndex) {
3209
3313
  const statusStr = String(status);
3210
- for (let i = segments.length - 1; i >= 0; i--) {
3314
+ const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
3315
+ for (let i = start; i >= 0; i--) {
3211
3316
  const r = lookupInGroup(segments[i].statusFiles, statusStr, "4xx", i, status);
3212
3317
  if (r) return r;
3213
3318
  }
3214
- for (let i = segments.length - 1; i >= 0; i--) {
3319
+ for (let i = start; i >= 0; i--) {
3215
3320
  const errorFile = segments[i].error;
3216
3321
  if (errorFile) return {
3217
3322
  file: errorFile,
@@ -3229,9 +3334,10 @@ function resolve4xx(status, segments) {
3229
3334
  * `error.tsx` therefore beats a root's `5xx.tsx`, which is the
3230
3335
  * intentional inverse of the 4xx chain.
3231
3336
  */
3232
- function resolve5xx(status, segments) {
3337
+ function resolve5xx(status, segments, maxIndex) {
3233
3338
  const statusStr = String(status);
3234
- for (let i = segments.length - 1; i >= 0; i--) {
3339
+ const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
3340
+ for (let i = start; i >= 0; i--) {
3235
3341
  const segment = segments[i];
3236
3342
  const r = lookupInGroup(segment.statusFiles, statusStr, "5xx", i, status);
3237
3343
  if (r) return r;
@@ -3251,10 +3357,11 @@ function resolve5xx(status, segments) {
3251
3357
  * no error.tsx — the JSON chain terminates at the category catch-all
3252
3358
  * and the caller falls back to a bare-JSON framework default.
3253
3359
  */
3254
- function resolveJson(status, segments) {
3360
+ function resolveJson(status, segments, maxIndex) {
3255
3361
  const statusStr = String(status);
3256
3362
  const categoryKey = status >= 500 ? "5xx" : "4xx";
3257
- for (let i = segments.length - 1; i >= 0; i--) {
3363
+ const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
3364
+ for (let i = start; i >= 0; i--) {
3258
3365
  const r = lookupInGroup(segments[i].jsonStatusFiles, statusStr, categoryKey, i, status);
3259
3366
  if (r) return r;
3260
3367
  }