@timber-js/app 0.2.0-alpha.5 → 0.2.0-alpha.50

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 (333) hide show
  1. package/LICENSE +8 -0
  2. package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
  3. package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
  4. package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
  5. package/dist/_chunks/{debug-gwlJkDuf.js → debug-ECi_61pb.js} +2 -2
  6. package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
  7. package/dist/_chunks/define-TK8C1M3x.js +279 -0
  8. package/dist/_chunks/define-TK8C1M3x.js.map +1 -0
  9. package/dist/_chunks/define-cookie-k9btcEfI.js +93 -0
  10. package/dist/_chunks/define-cookie-k9btcEfI.js.map +1 -0
  11. package/dist/_chunks/error-boundary-B9vT_YK_.js +211 -0
  12. package/dist/_chunks/error-boundary-B9vT_YK_.js.map +1 -0
  13. package/dist/_chunks/{format-DviM89f0.js → format-cX7wzEp2.js} +2 -2
  14. package/dist/_chunks/{format-DviM89f0.js.map → format-cX7wzEp2.js.map} +1 -1
  15. package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
  16. package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
  17. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
  18. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  19. package/dist/_chunks/{request-context-DIkVh_jG.js → request-context-0h-6Voad.js} +95 -69
  20. package/dist/_chunks/request-context-0h-6Voad.js.map +1 -0
  21. package/dist/_chunks/segment-context-DBn-nrMN.js +69 -0
  22. package/dist/_chunks/segment-context-DBn-nrMN.js.map +1 -0
  23. package/dist/_chunks/stale-reload-4L-_skC7.js +47 -0
  24. package/dist/_chunks/stale-reload-4L-_skC7.js.map +1 -0
  25. package/dist/_chunks/{tracing-Cwn7697K.js → tracing-JI4cYUdz.js} +17 -3
  26. package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-JI4cYUdz.js.map} +1 -1
  27. package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-wEXY2JQB.js} +1 -1
  28. package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-wEXY2JQB.js.map} +1 -1
  29. package/dist/_chunks/wrappers-C9XPg7-U.js +63 -0
  30. package/dist/_chunks/wrappers-C9XPg7-U.js.map +1 -0
  31. package/dist/adapters/compress-module.d.ts.map +1 -1
  32. package/dist/adapters/nitro.d.ts +17 -1
  33. package/dist/adapters/nitro.d.ts.map +1 -1
  34. package/dist/adapters/nitro.js +56 -13
  35. package/dist/adapters/nitro.js.map +1 -1
  36. package/dist/cache/fast-hash.d.ts +22 -0
  37. package/dist/cache/fast-hash.d.ts.map +1 -0
  38. package/dist/cache/index.d.ts +5 -2
  39. package/dist/cache/index.d.ts.map +1 -1
  40. package/dist/cache/index.js +90 -20
  41. package/dist/cache/index.js.map +1 -1
  42. package/dist/cache/register-cached-function.d.ts.map +1 -1
  43. package/dist/cache/singleflight.d.ts +18 -1
  44. package/dist/cache/singleflight.d.ts.map +1 -1
  45. package/dist/cache/timber-cache.d.ts +1 -1
  46. package/dist/cache/timber-cache.d.ts.map +1 -1
  47. package/dist/client/error-boundary.d.ts +10 -1
  48. package/dist/client/error-boundary.d.ts.map +1 -1
  49. package/dist/client/error-boundary.js +1 -125
  50. package/dist/client/index.d.ts +3 -2
  51. package/dist/client/index.d.ts.map +1 -1
  52. package/dist/client/index.js +213 -93
  53. package/dist/client/index.js.map +1 -1
  54. package/dist/client/link.d.ts +22 -8
  55. package/dist/client/link.d.ts.map +1 -1
  56. package/dist/client/navigation-context.d.ts +2 -2
  57. package/dist/client/router.d.ts +25 -3
  58. package/dist/client/router.d.ts.map +1 -1
  59. package/dist/client/rsc-fetch.d.ts +23 -2
  60. package/dist/client/rsc-fetch.d.ts.map +1 -1
  61. package/dist/client/segment-cache.d.ts +1 -1
  62. package/dist/client/segment-cache.d.ts.map +1 -1
  63. package/dist/client/segment-context.d.ts +1 -1
  64. package/dist/client/segment-context.d.ts.map +1 -1
  65. package/dist/client/segment-merger.d.ts.map +1 -1
  66. package/dist/client/stale-reload.d.ts +15 -0
  67. package/dist/client/stale-reload.d.ts.map +1 -1
  68. package/dist/client/top-loader.d.ts +1 -1
  69. package/dist/client/top-loader.d.ts.map +1 -1
  70. package/dist/client/transition-root.d.ts +1 -1
  71. package/dist/client/transition-root.d.ts.map +1 -1
  72. package/dist/client/use-params.d.ts +2 -2
  73. package/dist/client/use-params.d.ts.map +1 -1
  74. package/dist/client/use-query-states.d.ts +1 -1
  75. package/dist/codec.d.ts +21 -0
  76. package/dist/codec.d.ts.map +1 -0
  77. package/dist/cookies/define-cookie.d.ts +33 -12
  78. package/dist/cookies/define-cookie.d.ts.map +1 -1
  79. package/dist/cookies/index.js +1 -83
  80. package/dist/fonts/css.d.ts +1 -0
  81. package/dist/fonts/css.d.ts.map +1 -1
  82. package/dist/index.d.ts +112 -35
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +467 -246
  85. package/dist/index.js.map +1 -1
  86. package/dist/params/define.d.ts +76 -0
  87. package/dist/params/define.d.ts.map +1 -0
  88. package/dist/params/index.d.ts +8 -0
  89. package/dist/params/index.d.ts.map +1 -0
  90. package/dist/params/index.js +105 -0
  91. package/dist/params/index.js.map +1 -0
  92. package/dist/plugins/adapter-build.d.ts.map +1 -1
  93. package/dist/plugins/build-manifest.d.ts.map +1 -1
  94. package/dist/plugins/client-chunks.d.ts +32 -0
  95. package/dist/plugins/client-chunks.d.ts.map +1 -0
  96. package/dist/plugins/dev-error-overlay.d.ts +26 -1
  97. package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
  98. package/dist/plugins/entries.d.ts.map +1 -1
  99. package/dist/plugins/fonts.d.ts +7 -0
  100. package/dist/plugins/fonts.d.ts.map +1 -1
  101. package/dist/plugins/routing.d.ts.map +1 -1
  102. package/dist/plugins/server-bundle.d.ts.map +1 -1
  103. package/dist/plugins/static-build.d.ts.map +1 -1
  104. package/dist/routing/codegen.d.ts +2 -2
  105. package/dist/routing/codegen.d.ts.map +1 -1
  106. package/dist/routing/index.js +1 -1
  107. package/dist/routing/scanner.d.ts.map +1 -1
  108. package/dist/routing/status-file-lint.d.ts +2 -1
  109. package/dist/routing/status-file-lint.d.ts.map +1 -1
  110. package/dist/routing/types.d.ts +6 -4
  111. package/dist/routing/types.d.ts.map +1 -1
  112. package/dist/rsc-runtime/rsc.d.ts +1 -1
  113. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  114. package/dist/rsc-runtime/ssr.d.ts +12 -0
  115. package/dist/rsc-runtime/ssr.d.ts.map +1 -1
  116. package/dist/search-params/codecs.d.ts +1 -1
  117. package/dist/search-params/define.d.ts +159 -0
  118. package/dist/search-params/define.d.ts.map +1 -0
  119. package/dist/search-params/index.d.ts +4 -5
  120. package/dist/search-params/index.d.ts.map +1 -1
  121. package/dist/search-params/index.js +4 -474
  122. package/dist/search-params/registry.d.ts +1 -1
  123. package/dist/search-params/wrappers.d.ts +53 -0
  124. package/dist/search-params/wrappers.d.ts.map +1 -0
  125. package/dist/server/access-gate.d.ts +4 -0
  126. package/dist/server/access-gate.d.ts.map +1 -1
  127. package/dist/server/action-client.d.ts.map +1 -1
  128. package/dist/server/action-encryption.d.ts +76 -0
  129. package/dist/server/action-encryption.d.ts.map +1 -0
  130. package/dist/server/action-handler.d.ts.map +1 -1
  131. package/dist/server/als-registry.d.ts +18 -4
  132. package/dist/server/als-registry.d.ts.map +1 -1
  133. package/dist/server/build-manifest.d.ts +2 -2
  134. package/dist/server/debug.d.ts +1 -1
  135. package/dist/server/default-logger.d.ts +22 -0
  136. package/dist/server/default-logger.d.ts.map +1 -0
  137. package/dist/server/deny-renderer.d.ts.map +1 -1
  138. package/dist/server/early-hints.d.ts +13 -5
  139. package/dist/server/early-hints.d.ts.map +1 -1
  140. package/dist/server/error-boundary-wrapper.d.ts +4 -0
  141. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  142. package/dist/server/flight-injection-state.d.ts +66 -0
  143. package/dist/server/flight-injection-state.d.ts.map +1 -0
  144. package/dist/server/flight-scripts.d.ts +39 -0
  145. package/dist/server/flight-scripts.d.ts.map +1 -0
  146. package/dist/server/flush.d.ts.map +1 -1
  147. package/dist/server/form-data.d.ts +29 -0
  148. package/dist/server/form-data.d.ts.map +1 -1
  149. package/dist/server/html-injectors.d.ts +51 -11
  150. package/dist/server/html-injectors.d.ts.map +1 -1
  151. package/dist/server/index.d.ts +4 -2
  152. package/dist/server/index.d.ts.map +1 -1
  153. package/dist/server/index.js +1974 -1648
  154. package/dist/server/index.js.map +1 -1
  155. package/dist/server/logger.d.ts +24 -7
  156. package/dist/server/logger.d.ts.map +1 -1
  157. package/dist/server/node-stream-transforms.d.ts +113 -0
  158. package/dist/server/node-stream-transforms.d.ts.map +1 -0
  159. package/dist/server/pipeline.d.ts +7 -4
  160. package/dist/server/pipeline.d.ts.map +1 -1
  161. package/dist/server/primitives.d.ts +30 -3
  162. package/dist/server/primitives.d.ts.map +1 -1
  163. package/dist/server/render-timeout.d.ts +51 -0
  164. package/dist/server/render-timeout.d.ts.map +1 -0
  165. package/dist/server/request-context.d.ts +65 -38
  166. package/dist/server/request-context.d.ts.map +1 -1
  167. package/dist/server/route-element-builder.d.ts +7 -0
  168. package/dist/server/route-element-builder.d.ts.map +1 -1
  169. package/dist/server/route-handler.d.ts.map +1 -1
  170. package/dist/server/route-matcher.d.ts +2 -2
  171. package/dist/server/route-matcher.d.ts.map +1 -1
  172. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  173. package/dist/server/rsc-entry/helpers.d.ts +46 -3
  174. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  175. package/dist/server/rsc-entry/index.d.ts +6 -1
  176. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  177. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  178. package/dist/server/rsc-entry/rsc-stream.d.ts +9 -0
  179. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  180. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  181. package/dist/server/slot-resolver.d.ts +1 -1
  182. package/dist/server/slot-resolver.d.ts.map +1 -1
  183. package/dist/server/ssr-entry.d.ts +22 -0
  184. package/dist/server/ssr-entry.d.ts.map +1 -1
  185. package/dist/server/ssr-render.d.ts +39 -21
  186. package/dist/server/ssr-render.d.ts.map +1 -1
  187. package/dist/server/ssr-wrappers.d.ts +50 -0
  188. package/dist/server/ssr-wrappers.d.ts.map +1 -0
  189. package/dist/server/tracing.d.ts +10 -0
  190. package/dist/server/tracing.d.ts.map +1 -1
  191. package/dist/server/tree-builder.d.ts +19 -12
  192. package/dist/server/tree-builder.d.ts.map +1 -1
  193. package/dist/server/types.d.ts +1 -3
  194. package/dist/server/types.d.ts.map +1 -1
  195. package/dist/server/version-skew.d.ts +61 -0
  196. package/dist/server/version-skew.d.ts.map +1 -0
  197. package/dist/server/waituntil-bridge.d.ts.map +1 -1
  198. package/dist/shared/merge-search-params.d.ts +22 -0
  199. package/dist/shared/merge-search-params.d.ts.map +1 -0
  200. package/dist/shims/navigation-client.d.ts +1 -1
  201. package/dist/shims/navigation-client.d.ts.map +1 -1
  202. package/dist/shims/navigation.d.ts +1 -1
  203. package/dist/shims/navigation.d.ts.map +1 -1
  204. package/dist/utils/state-machine.d.ts +80 -0
  205. package/dist/utils/state-machine.d.ts.map +1 -0
  206. package/package.json +17 -14
  207. package/src/adapters/compress-module.ts +24 -4
  208. package/src/adapters/nitro.ts +58 -9
  209. package/src/cache/fast-hash.ts +34 -0
  210. package/src/cache/index.ts +5 -2
  211. package/src/cache/register-cached-function.ts +7 -3
  212. package/src/cache/singleflight.ts +62 -4
  213. package/src/cache/timber-cache.ts +40 -29
  214. package/src/cli.ts +0 -0
  215. package/src/client/browser-entry.ts +133 -93
  216. package/src/client/error-boundary.tsx +18 -1
  217. package/src/client/index.ts +10 -1
  218. package/src/client/link.tsx +78 -19
  219. package/src/client/navigation-context.ts +2 -2
  220. package/src/client/router.ts +105 -60
  221. package/src/client/rsc-fetch.ts +63 -2
  222. package/src/client/segment-cache.ts +1 -1
  223. package/src/client/segment-context.ts +6 -1
  224. package/src/client/segment-merger.ts +2 -8
  225. package/src/client/stale-reload.ts +32 -6
  226. package/src/client/top-loader.tsx +10 -9
  227. package/src/client/transition-root.tsx +7 -1
  228. package/src/client/use-params.ts +3 -3
  229. package/src/client/use-query-states.ts +1 -1
  230. package/src/codec.ts +21 -0
  231. package/src/cookies/define-cookie.ts +69 -18
  232. package/src/fonts/css.ts +2 -1
  233. package/src/index.ts +280 -85
  234. package/src/params/define.ts +260 -0
  235. package/src/params/index.ts +28 -0
  236. package/src/plugins/adapter-build.ts +6 -0
  237. package/src/plugins/build-manifest.ts +11 -0
  238. package/src/plugins/client-chunks.ts +65 -0
  239. package/src/plugins/dev-error-overlay.ts +70 -1
  240. package/src/plugins/dev-server.ts +38 -4
  241. package/src/plugins/entries.ts +5 -7
  242. package/src/plugins/fonts.ts +93 -42
  243. package/src/plugins/routing.ts +40 -14
  244. package/src/plugins/server-bundle.ts +32 -1
  245. package/src/plugins/shims.ts +1 -1
  246. package/src/plugins/static-build.ts +8 -4
  247. package/src/routing/codegen.ts +109 -88
  248. package/src/routing/scanner.ts +55 -6
  249. package/src/routing/status-file-lint.ts +2 -1
  250. package/src/routing/types.ts +7 -4
  251. package/src/rsc-runtime/rsc.ts +2 -0
  252. package/src/rsc-runtime/ssr.ts +50 -0
  253. package/src/rsc-runtime/vendor-types.d.ts +7 -0
  254. package/src/search-params/codecs.ts +1 -1
  255. package/src/search-params/define.ts +518 -0
  256. package/src/search-params/index.ts +12 -18
  257. package/src/search-params/registry.ts +1 -1
  258. package/src/search-params/wrappers.ts +85 -0
  259. package/src/server/access-gate.tsx +40 -9
  260. package/src/server/action-client.ts +7 -1
  261. package/src/server/action-encryption.ts +144 -0
  262. package/src/server/action-handler.ts +19 -2
  263. package/src/server/als-registry.ts +18 -4
  264. package/src/server/build-manifest.ts +4 -4
  265. package/src/server/compress.ts +25 -7
  266. package/src/server/debug.ts +1 -1
  267. package/src/server/default-logger.ts +98 -0
  268. package/src/server/deny-renderer.ts +2 -1
  269. package/src/server/early-hints.ts +36 -15
  270. package/src/server/error-boundary-wrapper.ts +57 -14
  271. package/src/server/flight-injection-state.ts +113 -0
  272. package/src/server/flight-scripts.ts +59 -0
  273. package/src/server/flush.ts +2 -1
  274. package/src/server/form-data.ts +76 -0
  275. package/src/server/html-injectors.ts +261 -117
  276. package/src/server/index.ts +9 -4
  277. package/src/server/logger.ts +38 -35
  278. package/src/server/node-stream-transforms.ts +504 -0
  279. package/src/server/pipeline.ts +131 -39
  280. package/src/server/primitives.ts +47 -5
  281. package/src/server/render-timeout.ts +108 -0
  282. package/src/server/request-context.ts +119 -119
  283. package/src/server/route-element-builder.ts +106 -114
  284. package/src/server/route-handler.ts +2 -1
  285. package/src/server/route-matcher.ts +2 -2
  286. package/src/server/rsc-entry/error-renderer.ts +5 -3
  287. package/src/server/rsc-entry/helpers.ts +122 -3
  288. package/src/server/rsc-entry/index.ts +108 -43
  289. package/src/server/rsc-entry/rsc-payload.ts +52 -12
  290. package/src/server/rsc-entry/rsc-stream.ts +49 -12
  291. package/src/server/rsc-entry/ssr-renderer.ts +40 -13
  292. package/src/server/slot-resolver.ts +222 -217
  293. package/src/server/ssr-entry.ts +209 -30
  294. package/src/server/ssr-render.ts +289 -67
  295. package/src/server/ssr-wrappers.tsx +139 -0
  296. package/src/server/tracing.ts +23 -0
  297. package/src/server/tree-builder.ts +91 -57
  298. package/src/server/types.ts +1 -3
  299. package/src/server/version-skew.ts +104 -0
  300. package/src/server/waituntil-bridge.ts +4 -1
  301. package/src/shared/merge-search-params.ts +48 -0
  302. package/src/shims/navigation-client.ts +1 -1
  303. package/src/shims/navigation.ts +1 -1
  304. package/src/utils/state-machine.ts +111 -0
  305. package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
  306. package/dist/_chunks/debug-gwlJkDuf.js.map +0 -1
  307. package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
  308. package/dist/_chunks/request-context-DIkVh_jG.js.map +0 -1
  309. package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
  310. package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
  311. package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
  312. package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
  313. package/dist/client/error-boundary.js.map +0 -1
  314. package/dist/cookies/index.js.map +0 -1
  315. package/dist/plugins/dynamic-transform.d.ts +0 -72
  316. package/dist/plugins/dynamic-transform.d.ts.map +0 -1
  317. package/dist/search-params/analyze.d.ts +0 -54
  318. package/dist/search-params/analyze.d.ts.map +0 -1
  319. package/dist/search-params/builtin-codecs.d.ts +0 -105
  320. package/dist/search-params/builtin-codecs.d.ts.map +0 -1
  321. package/dist/search-params/create.d.ts +0 -106
  322. package/dist/search-params/create.d.ts.map +0 -1
  323. package/dist/search-params/index.js.map +0 -1
  324. package/dist/server/prerender.d.ts +0 -77
  325. package/dist/server/prerender.d.ts.map +0 -1
  326. package/dist/server/response-cache.d.ts +0 -53
  327. package/dist/server/response-cache.d.ts.map +0 -1
  328. package/src/plugins/dynamic-transform.ts +0 -161
  329. package/src/search-params/analyze.ts +0 -192
  330. package/src/search-params/builtin-codecs.ts +0 -228
  331. package/src/search-params/create.ts +0 -321
  332. package/src/server/prerender.ts +0 -139
  333. package/src/server/response-cache.ts +0 -277
@@ -10,10 +10,9 @@
10
10
  * See design/29-cookies.md for cookie mutation semantics.
11
11
  */
12
12
 
13
- import { createHmac, timingSafeEqual } from 'node:crypto';
14
- import type { Routes } from '#/index.js';
15
13
  import { requestContextAls, type RequestContextStore, type CookieEntry } from './als-registry.js';
16
14
  import { isDebug } from './debug.js';
15
+ import { _setRawSearchParamsFn } from '#/search-params/define.js';
17
16
 
18
17
  // Re-export the ALS for framework-internal consumers that need direct access.
19
18
  export { requestContextAls };
@@ -22,30 +21,6 @@ export { requestContextAls };
22
21
  // the ALS context persists for the entire request lifecycle including
23
22
  // async stream consumption by React's renderToReadableStream.
24
23
 
25
- // ─── Cookie Signing Secrets ──────────────────────────────────────────────
26
-
27
- /**
28
- * Module-level cookie signing secrets. Index 0 is the newest (used for signing).
29
- * All entries are tried for verification (key rotation support).
30
- *
31
- * Set by the framework at startup via `setCookieSecrets()`.
32
- * See design/29-cookies.md §"Signed Cookies"
33
- */
34
- let _cookieSecrets: string[] = [];
35
-
36
- /**
37
- * Configure the cookie signing secrets.
38
- *
39
- * Called by the framework during server initialization with values from
40
- * `cookies.secret` or `cookies.secrets` in timber.config.ts.
41
- *
42
- * The first secret (index 0) is used for signing new cookies.
43
- * All secrets are tried for verification (supports key rotation).
44
- */
45
- export function setCookieSecrets(secrets: string[]): void {
46
- _cookieSecrets = secrets.filter(Boolean);
47
- }
48
-
49
24
  // ─── Public API ───────────────────────────────────────────────────────────
50
25
 
51
26
  /**
@@ -109,12 +84,6 @@ export function cookies(): RequestCookies {
109
84
  return map.size;
110
85
  },
111
86
 
112
- getSigned(name: string): string | undefined {
113
- const raw = map.get(name);
114
- if (!raw || _cookieSecrets.length === 0) return undefined;
115
- return verifySignedCookie(raw, _cookieSecrets);
116
- },
117
-
118
87
  set(name: string, value: string, options?: CookieOptions): void {
119
88
  assertMutable(store, 'set');
120
89
  if (store.flushed) {
@@ -127,21 +96,10 @@ export function cookies(): RequestCookies {
127
96
  }
128
97
  return;
129
98
  }
130
- let storedValue = value;
131
- if (options?.signed) {
132
- if (_cookieSecrets.length === 0) {
133
- throw new Error(
134
- `[timber] cookies().set('${name}', ..., { signed: true }) requires ` +
135
- `cookies.secret or cookies.secrets in timber.config.ts.`
136
- );
137
- }
138
- storedValue = signCookieValue(value, _cookieSecrets[0]);
139
- }
140
99
  const opts = { ...DEFAULT_COOKIE_OPTIONS, ...options };
141
- store.cookieJar.set(name, { name, value: storedValue, options: opts });
142
- // Read-your-own-writes: update the parsed cookies map with the signed value
143
- // so getSigned() can verify it in the same request
144
- map.set(name, storedValue);
100
+ store.cookieJar.set(name, { name, value, options: opts });
101
+ // Read-your-own-writes: update the parsed cookies map
102
+ map.set(name, value);
145
103
  },
146
104
 
147
105
  delete(name: string, options?: Pick<CookieOptions, 'path' | 'domain'>): void {
@@ -190,41 +148,107 @@ export function cookies(): RequestCookies {
190
148
  }
191
149
 
192
150
  /**
193
- * Returns a Promise resolving to the current request's search params.
151
+ * Returns a Promise resolving to the current request's raw URLSearchParams.
152
+ *
153
+ * For typed, parsed search params, import the definition from params.ts
154
+ * and call `.load()` or `.parse()`:
194
155
  *
195
- * In `page.tsx`, `middleware.ts`, and `access.ts` the framework pre-parses the
196
- * route's `search-params.ts` definition and the Promise resolves to the typed
197
- * object. In all other server component contexts it resolves to raw
198
- * `URLSearchParams`.
156
+ * ```ts
157
+ * import { searchParams } from './params'
158
+ * const parsed = await searchParams.load()
159
+ * ```
199
160
  *
200
- * Returned as a Promise to match the `params` prop convention and to allow
201
- * future partial pre-rendering support where param resolution may be deferred.
161
+ * Or explicitly:
162
+ *
163
+ * ```ts
164
+ * import { rawSearchParams } from '@timber-js/app/server'
165
+ * import { searchParams } from './params'
166
+ * const parsed = searchParams.parse(await rawSearchParams())
167
+ * ```
202
168
  *
203
169
  * Throws if called outside a request context.
204
170
  */
205
- export function searchParams<R extends keyof Routes>(): Promise<Routes[R]['searchParams']>;
206
- export function searchParams(): Promise<URLSearchParams | Record<string, unknown>>;
207
- export function searchParams(): Promise<URLSearchParams | Record<string, unknown>> {
171
+ export function rawSearchParams(): Promise<URLSearchParams> {
208
172
  const store = requestContextAls.getStore();
209
173
  if (!store) {
210
174
  throw new Error(
211
- '[timber] searchParams() called outside of a request context. ' +
175
+ '[timber] rawSearchParams() called outside of a request context. ' +
212
176
  'It can only be used in middleware, access checks, server components, and server actions.'
213
177
  );
214
178
  }
215
179
  return store.searchParamsPromise;
216
180
  }
217
181
 
182
+ // Eagerly register rawSearchParams with the search-params module so
183
+ // searchParams.load() can call it synchronously without a dynamic import.
184
+ // Dynamic imports lose ALS context in React's RSC Flight renderer,
185
+ // breaking rawSearchParams() in parallel slot pages. See TIM-523.
186
+ _setRawSearchParamsFn(rawSearchParams);
187
+
218
188
  /**
219
- * Replace the search params Promise for the current request with one that
220
- * resolves to the typed parsed result from the route's search-params.ts.
221
- * Called by the framework before rendering the page not for app code.
189
+ * Returns a Promise resolving to the current request's coerced segment params.
190
+ *
191
+ * Segment params are set by the pipeline after route matching and param
192
+ * coercion (via params.ts codecs). When no params.ts exists, values are
193
+ * raw strings. When codecs are defined, values are already coerced
194
+ * (e.g., `id` is a `number` if `defineSegmentParams({ id: z.coerce.number() })`).
195
+ *
196
+ * This is the primary way page and layout components access route params:
197
+ *
198
+ * ```ts
199
+ * import { rawSegmentParams } from '@timber-js/app/server'
200
+ *
201
+ * export default async function Page() {
202
+ * const { slug } = await rawSegmentParams()
203
+ * // ...
204
+ * }
205
+ * ```
206
+ *
207
+ * Throws if called outside a request context.
222
208
  */
223
- export function setParsedSearchParams(parsed: Record<string, unknown>): void {
209
+ export function rawSegmentParams(): Promise<Record<string, string | string[]>> {
224
210
  const store = requestContextAls.getStore();
225
- if (store) {
226
- store.searchParamsPromise = Promise.resolve(parsed);
211
+ if (!store) {
212
+ throw new Error(
213
+ '[timber] rawSegmentParams() called outside of a request context. ' +
214
+ 'It can only be used in middleware, access checks, server components, and server actions.'
215
+ );
227
216
  }
217
+ if (!store.segmentParamsPromise) {
218
+ throw new Error(
219
+ '[timber] rawSegmentParams() called before route matching completed. ' +
220
+ 'Segment params are not available until after the route is matched.'
221
+ );
222
+ }
223
+ return store.segmentParamsPromise;
224
+ }
225
+
226
+ /**
227
+ * Set the segment params promise on the current request context.
228
+ * Called by the pipeline after route matching and param coercion.
229
+ *
230
+ * @internal — framework use only
231
+ */
232
+ export function setSegmentParams(params: Record<string, string | string[]>): void {
233
+ const store = requestContextAls.getStore();
234
+ if (!store) {
235
+ throw new Error('[timber] setSegmentParams() called outside of a request context.');
236
+ }
237
+ store.segmentParamsPromise = Promise.resolve(params);
238
+ }
239
+
240
+ /**
241
+ * Returns the raw search string from the current request URL (e.g. "?foo=bar").
242
+ * Synchronous — safe for use in `redirect()` which throws synchronously.
243
+ *
244
+ * Returns empty string if called outside a request context (non-throwing for
245
+ * use in redirect's optional preserveSearchParams path).
246
+ *
247
+ * @internal — used by redirect() for preserveSearchParams support.
248
+ */
249
+ export function getRequestSearchString(): string {
250
+ const store = requestContextAls.getStore();
251
+ return store?.searchString ?? '';
228
252
  }
229
253
 
230
254
  // ─── Types ────────────────────────────────────────────────────────────────
@@ -257,12 +281,6 @@ export interface CookieOptions {
257
281
  sameSite?: 'strict' | 'lax' | 'none';
258
282
  /** Partitioned (CHIPS) — isolate cookie per top-level site. Default: false. */
259
283
  partitioned?: boolean;
260
- /**
261
- * Sign the cookie value with HMAC-SHA256 for integrity verification.
262
- * Requires `cookies.secret` or `cookies.secrets` in timber.config.ts.
263
- * See design/29-cookies.md §"Signed Cookies".
264
- */
265
- signed?: boolean;
266
284
  }
267
285
 
268
286
  const DEFAULT_COOKIE_OPTIONS: CookieOptions = {
@@ -287,14 +305,6 @@ export interface RequestCookies {
287
305
  getAll(): Array<{ name: string; value: string }>;
288
306
  /** Number of cookies. */
289
307
  readonly size: number;
290
- /**
291
- * Get a signed cookie value, verifying its HMAC-SHA256 signature.
292
- * Returns undefined if the cookie is missing, the signature is invalid,
293
- * or no secrets are configured. Never throws.
294
- *
295
- * See design/29-cookies.md §"Signed Cookies"
296
- */
297
- getSigned(name: string): string | undefined;
298
308
  /** Set a cookie. Only available in mutable contexts (middleware, actions, route handlers). */
299
309
  set(name: string, value: string, options?: CookieOptions): void;
300
310
  /** Delete a cookie. Only available in mutable contexts. */
@@ -316,11 +326,13 @@ export interface RequestCookies {
316
326
  */
317
327
  export function runWithRequestContext<T>(req: Request, fn: () => T): T {
318
328
  const originalCopy = new Headers(req.headers);
329
+ const parsedUrl = new URL(req.url);
319
330
  const store: RequestContextStore = {
320
331
  headers: freezeHeaders(req.headers),
321
332
  originalHeaders: originalCopy,
322
333
  cookieHeader: req.headers.get('cookie') ?? '',
323
- searchParamsPromise: Promise.resolve(new URL(req.url).searchParams),
334
+ searchParamsPromise: Promise.resolve(parsedUrl.searchParams),
335
+ searchString: parsedUrl.search,
324
336
  cookieJar: new Map(),
325
337
  flushed: false,
326
338
  mutableContext: false,
@@ -354,6 +366,35 @@ export function markResponseFlushed(): void {
354
366
  }
355
367
  }
356
368
 
369
+ /**
370
+ * Build a Map of cookie name → value reflecting the current request's
371
+ * read-your-own-writes state. Includes incoming cookies plus any
372
+ * mutations from cookies().set() / cookies().delete() in the same request.
373
+ *
374
+ * Used by SSR renderers to populate NavContext.cookies so that
375
+ * useCookie()'s server snapshot matches the actual response state.
376
+ *
377
+ * See design/29-cookies.md §"Read-Your-Own-Writes"
378
+ * See design/triage/TIM-441-cookie-api-triage.md §4
379
+ */
380
+ export function getCookiesForSsr(): Map<string, string> {
381
+ const store = requestContextAls.getStore();
382
+ if (!store) {
383
+ throw new Error('[timber] getCookiesForSsr() called outside of a request context.');
384
+ }
385
+
386
+ // Trigger lazy parsing if not yet done
387
+ if (!store.parsedCookies) {
388
+ store.parsedCookies = parseCookieHeader(store.cookieHeader);
389
+ }
390
+
391
+ // The parsedCookies map already reflects read-your-own-writes:
392
+ // - cookies().set() updates the map via map.set(name, value)
393
+ // - cookies().delete() removes from the map via map.delete(name)
394
+ // Return a copy so callers can't mutate the internal map.
395
+ return new Map(store.parsedCookies);
396
+ }
397
+
357
398
  /**
358
399
  * Collect all Set-Cookie headers from the cookie jar.
359
400
  * Called by the framework at flush time to apply cookies to the response.
@@ -467,47 +508,6 @@ function parseCookieHeader(header: string): Map<string, string> {
467
508
  return map;
468
509
  }
469
510
 
470
- // ─── Cookie Signing ──────────────────────────────────────────────────────
471
-
472
- /**
473
- * Sign a cookie value with HMAC-SHA256.
474
- * Returns `value.hex_signature`.
475
- */
476
- function signCookieValue(value: string, secret: string): string {
477
- const signature = createHmac('sha256', secret).update(value).digest('hex');
478
- return `${value}.${signature}`;
479
- }
480
-
481
- /**
482
- * Verify a signed cookie value against an array of secrets.
483
- * Returns the original value if any secret produces a matching signature,
484
- * or undefined if none match. Uses timing-safe comparison.
485
- *
486
- * The signed format is `value.hex_signature` — split at the last `.`.
487
- */
488
- function verifySignedCookie(raw: string, secrets: string[]): string | undefined {
489
- const lastDot = raw.lastIndexOf('.');
490
- if (lastDot <= 0 || lastDot === raw.length - 1) return undefined;
491
-
492
- const value = raw.slice(0, lastDot);
493
- const signature = raw.slice(lastDot + 1);
494
-
495
- // Hex-encoded SHA-256 is always 64 chars
496
- if (signature.length !== 64) return undefined;
497
-
498
- const signatureBuffer = Buffer.from(signature, 'hex');
499
- // If the hex decode produced fewer bytes, the signature was not valid hex
500
- if (signatureBuffer.length !== 32) return undefined;
501
-
502
- for (const secret of secrets) {
503
- const expected = createHmac('sha256', secret).update(value).digest();
504
- if (timingSafeEqual(expected, signatureBuffer)) {
505
- return value;
506
- }
507
- }
508
- return undefined;
509
- }
510
-
511
511
  /** Serialize a CookieEntry into a Set-Cookie header value. */
512
512
  function serializeCookieEntry(entry: CookieEntry): string {
513
513
  const parts = [`${entry.name}=${entry.value}`];
@@ -28,12 +28,24 @@ import { DenySignal, RedirectSignal } from './primitives.js';
28
28
  import { AccessGate } from './access-gate.js';
29
29
  import { resolveSlotElement } from './slot-resolver.js';
30
30
  import { SegmentProvider } from '#/client/segment-context.js';
31
- import { setParsedSearchParams } from './request-context.js';
32
- import type { SearchParamsDefinition } from '#/search-params/create.js';
31
+
33
32
  import { wrapSegmentWithErrorBoundaries } from './error-boundary-wrapper.js';
34
33
  import type { InterceptionContext } from './pipeline.js';
35
34
  import { shouldSkipSegment } from './state-tree-diff.js';
36
35
 
36
+ // ─── Param Coercion Error ─────────────────────────────────────────────────
37
+
38
+ /**
39
+ * Thrown when a defineSegmentParams codec's parse() fails.
40
+ * The pipeline catches this and responds with 404.
41
+ */
42
+ export class ParamCoercionError extends Error {
43
+ constructor(message: string) {
44
+ super(message);
45
+ this.name = 'ParamCoercionError';
46
+ }
47
+ }
48
+
37
49
  // ─── Types ────────────────────────────────────────────────────────────────
38
50
 
39
51
  /** Head element for client-side metadata updates. */
@@ -84,6 +96,64 @@ export class RouteSignalWithContext extends Error {
84
96
  }
85
97
  }
86
98
 
99
+ // ─── Module Processing Helpers ─────────────────────────────────────────────
100
+
101
+ /**
102
+ * Reject the legacy `generateMetadata` export with a helpful migration message.
103
+ * Throws if the module exports `generateMetadata` instead of `metadata`.
104
+ */
105
+ function rejectLegacyGenerateMetadata(mod: Record<string, unknown>, filePath: string): void {
106
+ if ('generateMetadata' in mod) {
107
+ throw new Error(
108
+ `${filePath}: "generateMetadata" is not a valid export. ` +
109
+ `Export an async function named "metadata" instead.\n\n` +
110
+ ` // Before\n` +
111
+ ` export async function generateMetadata({ params }) { ... }\n\n` +
112
+ ` // After\n` +
113
+ ` export async function metadata() { ... }`
114
+ );
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Extract and resolve metadata from a module (layout or page).
120
+ * Handles both static metadata objects and async metadata functions.
121
+ * Returns the resolved Metadata, or null if none exported.
122
+ *
123
+ * Metadata functions no longer receive { params } — they access params
124
+ * via rawSegmentParams() from ALS, same as page/layout components.
125
+ */
126
+ async function extractMetadata(
127
+ mod: Record<string, unknown>,
128
+ segment: ManifestSegmentNode
129
+ ): Promise<Metadata | null> {
130
+ if (typeof mod.metadata === 'function') {
131
+ type MetadataFn = () => Promise<Metadata>;
132
+ return (
133
+ (await withSpan(
134
+ 'timber.metadata',
135
+ { 'timber.segment': segment.segmentName ?? segment.urlPath },
136
+ () => (mod.metadata as MetadataFn)()
137
+ )) ?? null
138
+ );
139
+ }
140
+ if (mod.metadata) {
141
+ return mod.metadata as Metadata;
142
+ }
143
+ return null;
144
+ }
145
+
146
+ /**
147
+ * Extract `deferSuspenseFor` from a module and return the maximum
148
+ * of the current value and the module's value.
149
+ */
150
+ function extractDeferSuspenseFor(mod: Record<string, unknown>, current: number): number {
151
+ if (typeof mod.deferSuspenseFor === 'number' && mod.deferSuspenseFor > current) {
152
+ return mod.deferSuspenseFor;
153
+ }
154
+ return current;
155
+ }
156
+
87
157
  // ─── Builder ──────────────────────────────────────────────────────────────
88
158
 
89
159
  /**
@@ -104,9 +174,6 @@ export async function buildRouteElement(
104
174
  ): Promise<RouteElementResult> {
105
175
  const segments = match.segments as unknown as ManifestSegmentNode[];
106
176
 
107
- // Params are passed as a Promise to match Next.js 15+ convention.
108
- const paramsPromise = Promise.resolve(match.params);
109
-
110
177
  // Load all modules along the segment chain
111
178
  const metadataEntries: Array<{ metadata: Metadata; isPage: boolean }> = [];
112
179
  const layoutComponents: LayoutComponentEntry[] = [];
@@ -126,87 +193,34 @@ export async function buildRouteElement(
126
193
  segment,
127
194
  });
128
195
  }
129
- // Reject legacy generateMetadata export — use `export async function metadata()` instead
130
- if ('generateMetadata' in mod) {
131
- const filePath = segment.layout.filePath ?? segment.urlPath;
132
- throw new Error(
133
- `${filePath}: "generateMetadata" is not a valid export. ` +
134
- `Export an async function named "metadata" instead.\n\n` +
135
- ` // Before\n` +
136
- ` export async function generateMetadata({ params }) { ... }\n\n` +
137
- ` // After\n` +
138
- ` export async function metadata({ params }) { ... }`
139
- );
140
- }
141
- // Unified metadata export: static object or async function
142
- if (typeof mod.metadata === 'function') {
143
- type MetadataFn = (props: Record<string, unknown>) => Promise<Metadata>;
144
- const generated = await withSpan(
145
- 'timber.metadata',
146
- { 'timber.segment': segment.segmentName ?? segment.urlPath },
147
- () => (mod.metadata as MetadataFn)({ params: paramsPromise })
148
- );
149
- if (generated) {
150
- metadataEntries.push({ metadata: generated, isPage: false });
151
- }
152
- } else if (mod.metadata) {
153
- metadataEntries.push({ metadata: mod.metadata as Metadata, isPage: false });
154
- }
155
- // deferSuspenseFor hold window — max across all segments
156
- if (typeof mod.deferSuspenseFor === 'number' && mod.deferSuspenseFor > deferSuspenseFor) {
157
- deferSuspenseFor = mod.deferSuspenseFor;
196
+
197
+ // Param coercion is handled in the pipeline (Stage 2c) before
198
+ // middleware and rendering. See coerceSegmentParams() in pipeline.ts.
199
+
200
+ rejectLegacyGenerateMetadata(mod, segment.layout.filePath ?? segment.urlPath);
201
+ const layoutMetadata = await extractMetadata(mod, segment);
202
+ if (layoutMetadata) {
203
+ metadataEntries.push({ metadata: layoutMetadata, isPage: false });
158
204
  }
205
+ deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
159
206
  }
160
207
 
161
208
  // Load page (leaf segment only)
162
209
  if (isLeaf && segment.page) {
163
- // Load and apply search-params.ts definition before rendering so
164
- // searchParams() from @timber-js/app/server returns parsed typed values.
165
- if (segment.searchParams) {
166
- const spMod = (await segment.searchParams.load()) as {
167
- default?: SearchParamsDefinition<Record<string, unknown>>;
168
- };
169
- if (spMod.default) {
170
- const rawSearchParams = new URL(req.url).searchParams;
171
- const parsed = spMod.default.parse(rawSearchParams);
172
- setParsedSearchParams(parsed);
173
- }
174
- }
175
-
176
210
  const mod = (await segment.page.load()) as Record<string, unknown>;
211
+
212
+ // Param coercion is handled in the pipeline (Stage 2c) before
213
+ // middleware and rendering. See coerceSegmentParams() in pipeline.ts.
214
+
177
215
  if (mod.default) {
178
216
  PageComponent = mod.default as (...args: unknown[]) => unknown;
179
217
  }
180
- // Reject legacy generateMetadata export — use `export async function metadata()` instead
181
- if ('generateMetadata' in mod) {
182
- const filePath = segment.page.filePath ?? segment.urlPath;
183
- throw new Error(
184
- `${filePath}: "generateMetadata" is not a valid export. ` +
185
- `Export an async function named "metadata" instead.\n\n` +
186
- ` // Before\n` +
187
- ` export async function generateMetadata({ params }) { ... }\n\n` +
188
- ` // After\n` +
189
- ` export async function metadata({ params }) { ... }`
190
- );
191
- }
192
- // Unified metadata export: static object or async function
193
- if (typeof mod.metadata === 'function') {
194
- type MetadataFn = (props: Record<string, unknown>) => Promise<Metadata>;
195
- const generated = await withSpan(
196
- 'timber.metadata',
197
- { 'timber.segment': segment.segmentName ?? segment.urlPath },
198
- () => (mod.metadata as MetadataFn)({ params: paramsPromise })
199
- );
200
- if (generated) {
201
- metadataEntries.push({ metadata: generated, isPage: true });
202
- }
203
- } else if (mod.metadata) {
204
- metadataEntries.push({ metadata: mod.metadata as Metadata, isPage: true });
205
- }
206
- // deferSuspenseFor hold window — max across all segments
207
- if (typeof mod.deferSuspenseFor === 'number' && mod.deferSuspenseFor > deferSuspenseFor) {
208
- deferSuspenseFor = mod.deferSuspenseFor;
218
+ rejectLegacyGenerateMetadata(mod, segment.page.filePath ?? segment.urlPath);
219
+ const pageMetadata = await extractMetadata(mod, segment);
220
+ if (pageMetadata) {
221
+ metadataEntries.push({ metadata: pageMetadata, isPage: true });
209
222
  }
223
+ deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
210
224
  }
211
225
  }
212
226
 
@@ -227,7 +241,7 @@ export async function buildRouteElement(
227
241
  if (segment.access) {
228
242
  const accessMod = (await segment.access.load()) as Record<string, unknown>;
229
243
  const accessFn = accessMod.default as
230
- | ((ctx: { params: Record<string, string | string[]>; searchParams: unknown }) => unknown)
244
+ | ((ctx: { params: Record<string, string | string[]> }) => unknown)
231
245
  | undefined;
232
246
  if (accessFn) {
233
247
  try {
@@ -236,7 +250,7 @@ export async function buildRouteElement(
236
250
  { 'timber.segment': segment.segmentName ?? 'unknown' },
237
251
  async () => {
238
252
  try {
239
- await accessFn({ params: match.params, searchParams: {} });
253
+ await accessFn({ params: match.params });
240
254
  await setSpanAttribute('timber.result', 'pass');
241
255
  accessVerdicts.set(si, 'pass');
242
256
  } catch (error) {
@@ -302,10 +316,7 @@ export async function buildRouteElement(
302
316
  );
303
317
  };
304
318
 
305
- let element = h(TracedPage, {
306
- params: paramsPromise,
307
- searchParams: {},
308
- });
319
+ let element = h(TracedPage, {});
309
320
 
310
321
  // Build a lookup of layout components by segment for O(1) access.
311
322
  const layoutBySegment = new Map(
@@ -352,12 +363,7 @@ export async function buildRouteElement(
352
363
  // same urlPath (e.g., /(marketing) and /(app) both have "/"),
353
364
  // which would cause the wrong cached layout to be reused
354
365
  const skip =
355
- shouldSkipSegment(
356
- segment.urlPath,
357
- layoutComponent,
358
- isLeaf,
359
- clientStateTree ?? null
360
- ) &&
366
+ shouldSkipSegment(segment.urlPath, layoutComponent, isLeaf, clientStateTree ?? null) &&
361
367
  hasRenderedLayoutBelow &&
362
368
  segment.segmentType !== 'group';
363
369
 
@@ -385,13 +391,11 @@ export async function buildRouteElement(
385
391
  if (segment.access) {
386
392
  const accessMod = (await segment.access.load()) as Record<string, unknown>;
387
393
  const accessFn = accessMod.default as
388
- | ((ctx: { params: Record<string, string | string[]>; searchParams: unknown }) => unknown)
394
+ | ((ctx: { params: Record<string, string | string[]> }) => unknown)
389
395
  | undefined;
390
396
  if (accessFn) {
391
397
  element = h(AccessGate, {
392
398
  accessFn,
393
- params: match.params,
394
- searchParams: {},
395
399
  segmentName: segment.segmentName,
396
400
  verdict: accessVerdicts.get(i),
397
401
  children: element,
@@ -408,7 +412,6 @@ export async function buildRouteElement(
408
412
  slotProps[slotName] = await resolveSlotElement(
409
413
  slotNode as ManifestSegmentNode,
410
414
  match,
411
- paramsPromise,
412
415
  h,
413
416
  interception
414
417
  );
@@ -417,39 +420,28 @@ export async function buildRouteElement(
417
420
  const segmentPath = segment.urlPath.split('/');
418
421
  const parallelRouteKeys = Object.keys(segment.slots ?? {});
419
422
 
420
- // Wrap the layout component in an OTEL span.
421
- // For route groups, urlPath is "/" (groups don't add URL segments), so
422
- // include the directory name to distinguish e.g. "layout /(pre-release)"
423
- // from the root "layout /".
424
- const segmentForSpan = segment;
425
- const layoutComponentForSpan = layoutComponent;
426
- const segmentLabel =
427
- segmentForSpan.segmentType === 'group'
428
- ? `${segmentForSpan.urlPath === '/' ? '' : segmentForSpan.urlPath}/${segmentForSpan.segmentName}`
429
- : segmentForSpan.urlPath;
430
- const TracedLayout = async (props: Record<string, unknown>) => {
431
- return withSpan('timber.layout', { 'timber.segment': segmentLabel }, () =>
432
- (layoutComponentForSpan as (props: Record<string, unknown>) => unknown)(props)
433
- );
434
- };
435
-
436
- // segmentId uniquely identifies this segment for client-side element
437
- // caching. For route groups, urlPath is shared with the parent (both "/"),
438
- // so we include the group name to distinguish them. Without this, the
439
- // segment merger's element cache would conflate root and group elements.
423
+ // For route groups, urlPath is shared with the parent (both "/"),
424
+ // so include the group name to distinguish them. Used for both OTEL
425
+ // span labels and client-side element caching (segmentId).
440
426
  const segmentId =
441
427
  segment.segmentType === 'group'
442
428
  ? `${segment.urlPath === '/' ? '' : segment.urlPath}/${segment.segmentName}`
443
429
  : segment.urlPath;
444
430
 
431
+ // Wrap the layout component in an OTEL span
432
+ const layoutComponentRef = layoutComponent;
433
+ const TracedLayout = async (props: Record<string, unknown>) => {
434
+ return withSpan('timber.layout', { 'timber.segment': segmentId }, () =>
435
+ (layoutComponentRef as (props: Record<string, unknown>) => unknown)(props)
436
+ );
437
+ };
438
+
445
439
  element = h(SegmentProvider, {
446
440
  segments: segmentPath,
447
441
  segmentId,
448
442
  parallelRouteKeys,
449
443
  children: h(TracedLayout, {
450
444
  ...slotProps,
451
- params: paramsPromise,
452
- searchParams: {},
453
445
  children: element,
454
446
  }),
455
447
  });
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import type { RouteContext } from './types.js';
12
+ import { logRouteError } from './logger.js';
12
13
 
13
14
  // ─── Types ───────────────────────────────────────────────────────────────
14
15
 
@@ -122,7 +123,7 @@ async function runHandler(handler: RouteHandler, ctx: RouteContext): Promise<Res
122
123
  const res = await handler(ctx);
123
124
  return mergeResponseHeaders(res, ctx.headers);
124
125
  } catch (error) {
125
- console.error('[timber] Uncaught error in route.ts handler:', error);
126
+ logRouteError({ method: ctx.req.method, path: new URL(ctx.req.url).pathname, error });
126
127
  return new Response(null, { status: 500 });
127
128
  }
128
129
  }