@timber-js/app 0.2.0-alpha.4 → 0.2.0-alpha.41

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 (336) 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-ECi_61pb.js +108 -0
  6. package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
  7. package/dist/_chunks/define-cookie-BmKbSyp0.js +93 -0
  8. package/dist/_chunks/define-cookie-BmKbSyp0.js.map +1 -0
  9. package/dist/_chunks/error-boundary-BAN3751q.js +211 -0
  10. package/dist/_chunks/error-boundary-BAN3751q.js.map +1 -0
  11. package/dist/_chunks/{format-CwdaB0_2.js → format-cX7wzEp2.js} +2 -2
  12. package/dist/_chunks/{format-CwdaB0_2.js.map → format-cX7wzEp2.js.map} +1 -1
  13. package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
  14. package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
  15. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
  16. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  17. package/dist/_chunks/{request-context-CZJi4CuK.js → request-context-BxYIJM24.js} +93 -69
  18. package/dist/_chunks/request-context-BxYIJM24.js.map +1 -0
  19. package/dist/_chunks/segment-context-C6byCyZU.js +69 -0
  20. package/dist/_chunks/segment-context-C6byCyZU.js.map +1 -0
  21. package/dist/_chunks/stale-reload-C0ValzG7.js +47 -0
  22. package/dist/_chunks/stale-reload-C0ValzG7.js.map +1 -0
  23. package/dist/_chunks/{tracing-Cwn7697K.js → tracing-CuXiCP5p.js} +17 -3
  24. package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-CuXiCP5p.js.map} +1 -1
  25. package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-BvW0TKDn.js} +1 -1
  26. package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-BvW0TKDn.js.map} +1 -1
  27. package/dist/_chunks/wrappers-C6J0nNji.js +331 -0
  28. package/dist/_chunks/wrappers-C6J0nNji.js.map +1 -0
  29. package/dist/adapters/compress-module.d.ts.map +1 -1
  30. package/dist/adapters/nitro.d.ts +17 -1
  31. package/dist/adapters/nitro.d.ts.map +1 -1
  32. package/dist/adapters/nitro.js +56 -13
  33. package/dist/adapters/nitro.js.map +1 -1
  34. package/dist/cache/fast-hash.d.ts +22 -0
  35. package/dist/cache/fast-hash.d.ts.map +1 -0
  36. package/dist/cache/index.d.ts +5 -2
  37. package/dist/cache/index.d.ts.map +1 -1
  38. package/dist/cache/index.js +88 -18
  39. package/dist/cache/index.js.map +1 -1
  40. package/dist/cache/register-cached-function.d.ts.map +1 -1
  41. package/dist/cache/singleflight.d.ts +18 -1
  42. package/dist/cache/singleflight.d.ts.map +1 -1
  43. package/dist/cache/timber-cache.d.ts.map +1 -1
  44. package/dist/client/error-boundary.d.ts +10 -1
  45. package/dist/client/error-boundary.d.ts.map +1 -1
  46. package/dist/client/error-boundary.js +1 -125
  47. package/dist/client/index.d.ts +3 -2
  48. package/dist/client/index.d.ts.map +1 -1
  49. package/dist/client/index.js +213 -93
  50. package/dist/client/index.js.map +1 -1
  51. package/dist/client/link.d.ts +22 -8
  52. package/dist/client/link.d.ts.map +1 -1
  53. package/dist/client/navigation-context.d.ts +2 -2
  54. package/dist/client/router.d.ts +25 -3
  55. package/dist/client/router.d.ts.map +1 -1
  56. package/dist/client/rsc-fetch.d.ts +23 -2
  57. package/dist/client/rsc-fetch.d.ts.map +1 -1
  58. package/dist/client/segment-cache.d.ts +1 -1
  59. package/dist/client/segment-cache.d.ts.map +1 -1
  60. package/dist/client/segment-context.d.ts +1 -1
  61. package/dist/client/segment-context.d.ts.map +1 -1
  62. package/dist/client/segment-merger.d.ts.map +1 -1
  63. package/dist/client/stale-reload.d.ts +15 -0
  64. package/dist/client/stale-reload.d.ts.map +1 -1
  65. package/dist/client/top-loader.d.ts +1 -1
  66. package/dist/client/top-loader.d.ts.map +1 -1
  67. package/dist/client/transition-root.d.ts +1 -1
  68. package/dist/client/transition-root.d.ts.map +1 -1
  69. package/dist/client/use-params.d.ts +2 -2
  70. package/dist/client/use-params.d.ts.map +1 -1
  71. package/dist/client/use-query-states.d.ts +1 -1
  72. package/dist/codec.d.ts +21 -0
  73. package/dist/codec.d.ts.map +1 -0
  74. package/dist/cookies/define-cookie.d.ts +33 -12
  75. package/dist/cookies/define-cookie.d.ts.map +1 -1
  76. package/dist/cookies/index.js +1 -83
  77. package/dist/fonts/css.d.ts +1 -0
  78. package/dist/fonts/css.d.ts.map +1 -1
  79. package/dist/fonts/local.d.ts +4 -2
  80. package/dist/fonts/local.d.ts.map +1 -1
  81. package/dist/index.d.ts +112 -35
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +635 -233
  84. package/dist/index.js.map +1 -1
  85. package/dist/params/define.d.ts +76 -0
  86. package/dist/params/define.d.ts.map +1 -0
  87. package/dist/params/index.d.ts +8 -0
  88. package/dist/params/index.d.ts.map +1 -0
  89. package/dist/params/index.js +104 -0
  90. package/dist/params/index.js.map +1 -0
  91. package/dist/plugins/adapter-build.d.ts.map +1 -1
  92. package/dist/plugins/build-manifest.d.ts.map +1 -1
  93. package/dist/plugins/client-chunks.d.ts +32 -0
  94. package/dist/plugins/client-chunks.d.ts.map +1 -0
  95. package/dist/plugins/dev-error-overlay.d.ts +26 -1
  96. package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
  97. package/dist/plugins/entries.d.ts +7 -0
  98. package/dist/plugins/entries.d.ts.map +1 -1
  99. package/dist/plugins/fonts.d.ts +9 -1
  100. package/dist/plugins/fonts.d.ts.map +1 -1
  101. package/dist/plugins/mdx.d.ts +6 -0
  102. package/dist/plugins/mdx.d.ts.map +1 -1
  103. package/dist/plugins/routing.d.ts.map +1 -1
  104. package/dist/plugins/server-bundle.d.ts.map +1 -1
  105. package/dist/plugins/static-build.d.ts.map +1 -1
  106. package/dist/routing/codegen.d.ts +2 -2
  107. package/dist/routing/codegen.d.ts.map +1 -1
  108. package/dist/routing/index.js +1 -1
  109. package/dist/routing/scanner.d.ts.map +1 -1
  110. package/dist/routing/status-file-lint.d.ts +2 -1
  111. package/dist/routing/status-file-lint.d.ts.map +1 -1
  112. package/dist/routing/types.d.ts +6 -4
  113. package/dist/routing/types.d.ts.map +1 -1
  114. package/dist/rsc-runtime/rsc.d.ts +1 -1
  115. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  116. package/dist/rsc-runtime/ssr.d.ts +12 -0
  117. package/dist/rsc-runtime/ssr.d.ts.map +1 -1
  118. package/dist/search-params/codecs.d.ts +1 -1
  119. package/dist/search-params/define.d.ts +153 -0
  120. package/dist/search-params/define.d.ts.map +1 -0
  121. package/dist/search-params/index.d.ts +4 -5
  122. package/dist/search-params/index.d.ts.map +1 -1
  123. package/dist/search-params/index.js +3 -474
  124. package/dist/search-params/registry.d.ts +1 -1
  125. package/dist/search-params/wrappers.d.ts +53 -0
  126. package/dist/search-params/wrappers.d.ts.map +1 -0
  127. package/dist/server/access-gate.d.ts +4 -0
  128. package/dist/server/access-gate.d.ts.map +1 -1
  129. package/dist/server/action-client.d.ts.map +1 -1
  130. package/dist/server/action-encryption.d.ts +76 -0
  131. package/dist/server/action-encryption.d.ts.map +1 -0
  132. package/dist/server/action-handler.d.ts.map +1 -1
  133. package/dist/server/als-registry.d.ts +18 -4
  134. package/dist/server/als-registry.d.ts.map +1 -1
  135. package/dist/server/build-manifest.d.ts +2 -2
  136. package/dist/server/debug.d.ts +46 -15
  137. package/dist/server/debug.d.ts.map +1 -1
  138. package/dist/server/default-logger.d.ts +22 -0
  139. package/dist/server/default-logger.d.ts.map +1 -0
  140. package/dist/server/deny-renderer.d.ts.map +1 -1
  141. package/dist/server/early-hints.d.ts +13 -5
  142. package/dist/server/early-hints.d.ts.map +1 -1
  143. package/dist/server/error-boundary-wrapper.d.ts +4 -0
  144. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  145. package/dist/server/flight-injection-state.d.ts +78 -0
  146. package/dist/server/flight-injection-state.d.ts.map +1 -0
  147. package/dist/server/flight-scripts.d.ts +39 -0
  148. package/dist/server/flight-scripts.d.ts.map +1 -0
  149. package/dist/server/flush.d.ts.map +1 -1
  150. package/dist/server/form-data.d.ts +29 -0
  151. package/dist/server/form-data.d.ts.map +1 -1
  152. package/dist/server/html-injectors.d.ts +5 -11
  153. package/dist/server/html-injectors.d.ts.map +1 -1
  154. package/dist/server/index.d.ts +4 -2
  155. package/dist/server/index.d.ts.map +1 -1
  156. package/dist/server/index.js +1975 -1649
  157. package/dist/server/index.js.map +1 -1
  158. package/dist/server/logger.d.ts +24 -7
  159. package/dist/server/logger.d.ts.map +1 -1
  160. package/dist/server/node-stream-transforms.d.ts +77 -0
  161. package/dist/server/node-stream-transforms.d.ts.map +1 -0
  162. package/dist/server/pipeline.d.ts +7 -4
  163. package/dist/server/pipeline.d.ts.map +1 -1
  164. package/dist/server/primitives.d.ts +30 -3
  165. package/dist/server/primitives.d.ts.map +1 -1
  166. package/dist/server/render-timeout.d.ts +51 -0
  167. package/dist/server/render-timeout.d.ts.map +1 -0
  168. package/dist/server/request-context.d.ts +65 -38
  169. package/dist/server/request-context.d.ts.map +1 -1
  170. package/dist/server/route-element-builder.d.ts +7 -0
  171. package/dist/server/route-element-builder.d.ts.map +1 -1
  172. package/dist/server/route-handler.d.ts.map +1 -1
  173. package/dist/server/route-matcher.d.ts +2 -2
  174. package/dist/server/route-matcher.d.ts.map +1 -1
  175. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  176. package/dist/server/rsc-entry/helpers.d.ts +46 -3
  177. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  178. package/dist/server/rsc-entry/index.d.ts +6 -1
  179. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  180. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  181. package/dist/server/rsc-entry/rsc-stream.d.ts +9 -0
  182. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  183. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  184. package/dist/server/slot-resolver.d.ts +1 -1
  185. package/dist/server/slot-resolver.d.ts.map +1 -1
  186. package/dist/server/ssr-entry.d.ts +22 -0
  187. package/dist/server/ssr-entry.d.ts.map +1 -1
  188. package/dist/server/ssr-render.d.ts +39 -21
  189. package/dist/server/ssr-render.d.ts.map +1 -1
  190. package/dist/server/tracing.d.ts +10 -0
  191. package/dist/server/tracing.d.ts.map +1 -1
  192. package/dist/server/tree-builder.d.ts +19 -12
  193. package/dist/server/tree-builder.d.ts.map +1 -1
  194. package/dist/server/types.d.ts +1 -3
  195. package/dist/server/types.d.ts.map +1 -1
  196. package/dist/server/version-skew.d.ts +61 -0
  197. package/dist/server/version-skew.d.ts.map +1 -0
  198. package/dist/server/waituntil-bridge.d.ts.map +1 -1
  199. package/dist/shared/merge-search-params.d.ts +22 -0
  200. package/dist/shared/merge-search-params.d.ts.map +1 -0
  201. package/dist/shims/navigation-client.d.ts +1 -1
  202. package/dist/shims/navigation-client.d.ts.map +1 -1
  203. package/dist/shims/navigation.d.ts +1 -1
  204. package/dist/shims/navigation.d.ts.map +1 -1
  205. package/dist/utils/state-machine.d.ts +80 -0
  206. package/dist/utils/state-machine.d.ts.map +1 -0
  207. package/package.json +17 -14
  208. package/src/adapters/compress-module.ts +24 -4
  209. package/src/adapters/nitro.ts +58 -9
  210. package/src/cache/fast-hash.ts +34 -0
  211. package/src/cache/index.ts +5 -2
  212. package/src/cache/register-cached-function.ts +7 -3
  213. package/src/cache/singleflight.ts +62 -4
  214. package/src/cache/timber-cache.ts +34 -26
  215. package/src/cli.ts +0 -0
  216. package/src/client/browser-entry.ts +94 -90
  217. package/src/client/error-boundary.tsx +18 -1
  218. package/src/client/index.ts +10 -1
  219. package/src/client/link.tsx +78 -19
  220. package/src/client/navigation-context.ts +2 -2
  221. package/src/client/router.ts +105 -60
  222. package/src/client/rsc-fetch.ts +63 -2
  223. package/src/client/segment-cache.ts +1 -1
  224. package/src/client/segment-context.ts +6 -1
  225. package/src/client/segment-merger.ts +2 -8
  226. package/src/client/stale-reload.ts +32 -6
  227. package/src/client/top-loader.tsx +10 -9
  228. package/src/client/transition-root.tsx +7 -1
  229. package/src/client/use-params.ts +3 -3
  230. package/src/client/use-query-states.ts +1 -1
  231. package/src/codec.ts +21 -0
  232. package/src/cookies/define-cookie.ts +69 -18
  233. package/src/fonts/css.ts +2 -1
  234. package/src/fonts/local.ts +7 -3
  235. package/src/index.ts +280 -85
  236. package/src/params/define.ts +260 -0
  237. package/src/params/index.ts +28 -0
  238. package/src/plugins/adapter-build.ts +6 -0
  239. package/src/plugins/build-manifest.ts +11 -0
  240. package/src/plugins/client-chunks.ts +65 -0
  241. package/src/plugins/dev-error-overlay.ts +70 -1
  242. package/src/plugins/dev-server.ts +38 -4
  243. package/src/plugins/entries.ts +12 -11
  244. package/src/plugins/fonts.ts +171 -19
  245. package/src/plugins/mdx.ts +9 -5
  246. package/src/plugins/routing.ts +40 -14
  247. package/src/plugins/server-bundle.ts +32 -1
  248. package/src/plugins/shims.ts +1 -1
  249. package/src/plugins/static-build.ts +8 -4
  250. package/src/routing/codegen.ts +109 -88
  251. package/src/routing/scanner.ts +55 -6
  252. package/src/routing/status-file-lint.ts +2 -1
  253. package/src/routing/types.ts +7 -4
  254. package/src/rsc-runtime/rsc.ts +2 -0
  255. package/src/rsc-runtime/ssr.ts +50 -0
  256. package/src/rsc-runtime/vendor-types.d.ts +7 -0
  257. package/src/search-params/codecs.ts +1 -1
  258. package/src/search-params/define.ts +504 -0
  259. package/src/search-params/index.ts +12 -18
  260. package/src/search-params/registry.ts +1 -1
  261. package/src/search-params/wrappers.ts +85 -0
  262. package/src/server/access-gate.tsx +40 -9
  263. package/src/server/action-client.ts +14 -5
  264. package/src/server/action-encryption.ts +144 -0
  265. package/src/server/action-handler.ts +19 -2
  266. package/src/server/als-registry.ts +18 -4
  267. package/src/server/build-manifest.ts +4 -4
  268. package/src/server/compress.ts +25 -7
  269. package/src/server/debug.ts +55 -17
  270. package/src/server/default-logger.ts +98 -0
  271. package/src/server/deny-renderer.ts +2 -1
  272. package/src/server/early-hints.ts +36 -15
  273. package/src/server/error-boundary-wrapper.ts +57 -14
  274. package/src/server/flight-injection-state.ts +152 -0
  275. package/src/server/flight-scripts.ts +59 -0
  276. package/src/server/flush.ts +2 -1
  277. package/src/server/form-data.ts +76 -0
  278. package/src/server/html-injectors.ts +103 -66
  279. package/src/server/index.ts +9 -4
  280. package/src/server/logger.ts +38 -35
  281. package/src/server/node-stream-transforms.ts +381 -0
  282. package/src/server/pipeline.ts +131 -39
  283. package/src/server/primitives.ts +47 -5
  284. package/src/server/render-timeout.ts +108 -0
  285. package/src/server/request-context.ts +112 -119
  286. package/src/server/route-element-builder.ts +106 -114
  287. package/src/server/route-handler.ts +2 -1
  288. package/src/server/route-matcher.ts +2 -2
  289. package/src/server/rsc-entry/error-renderer.ts +5 -3
  290. package/src/server/rsc-entry/helpers.ts +122 -3
  291. package/src/server/rsc-entry/index.ts +125 -49
  292. package/src/server/rsc-entry/rsc-payload.ts +52 -12
  293. package/src/server/rsc-entry/rsc-stream.ts +33 -8
  294. package/src/server/rsc-entry/ssr-renderer.ts +40 -13
  295. package/src/server/slot-resolver.ts +199 -210
  296. package/src/server/ssr-entry.ts +168 -22
  297. package/src/server/ssr-render.ts +289 -67
  298. package/src/server/tracing.ts +23 -0
  299. package/src/server/tree-builder.ts +91 -57
  300. package/src/server/types.ts +1 -3
  301. package/src/server/version-skew.ts +104 -0
  302. package/src/server/waituntil-bridge.ts +4 -1
  303. package/src/shared/merge-search-params.ts +48 -0
  304. package/src/shims/navigation-client.ts +1 -1
  305. package/src/shims/navigation.ts +1 -1
  306. package/src/utils/state-machine.ts +111 -0
  307. package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
  308. package/dist/_chunks/debug-B4WUeqJ-.js +0 -75
  309. package/dist/_chunks/debug-B4WUeqJ-.js.map +0 -1
  310. package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
  311. package/dist/_chunks/request-context-CZJi4CuK.js.map +0 -1
  312. package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
  313. package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
  314. package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
  315. package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
  316. package/dist/client/error-boundary.js.map +0 -1
  317. package/dist/cookies/index.js.map +0 -1
  318. package/dist/plugins/dynamic-transform.d.ts +0 -72
  319. package/dist/plugins/dynamic-transform.d.ts.map +0 -1
  320. package/dist/search-params/analyze.d.ts +0 -54
  321. package/dist/search-params/analyze.d.ts.map +0 -1
  322. package/dist/search-params/builtin-codecs.d.ts +0 -105
  323. package/dist/search-params/builtin-codecs.d.ts.map +0 -1
  324. package/dist/search-params/create.d.ts +0 -106
  325. package/dist/search-params/create.d.ts.map +0 -1
  326. package/dist/search-params/index.js.map +0 -1
  327. package/dist/server/prerender.d.ts +0 -77
  328. package/dist/server/prerender.d.ts.map +0 -1
  329. package/dist/server/response-cache.d.ts +0 -53
  330. package/dist/server/response-cache.d.ts.map +0 -1
  331. package/src/plugins/dynamic-transform.ts +0 -161
  332. package/src/search-params/analyze.ts +0 -192
  333. package/src/search-params/builtin-codecs.ts +0 -228
  334. package/src/search-params/create.ts +0 -321
  335. package/src/server/prerender.ts +0 -139
  336. package/src/server/response-cache.ts +0 -277
@@ -6,6 +6,8 @@
6
6
  import type { JsonSerializable } from './types.js';
7
7
  import { getWaitUntil as _getWaitUntil } from './waituntil-bridge.js';
8
8
  import { isDebug } from './debug.js';
9
+ import { getRequestSearchString } from './request-context.js';
10
+ import { mergePreservedSearchParams } from '#/shared/merge-search-params.js';
9
11
 
10
12
  // ─── Dev-mode validation ────────────────────────────────────────────────────
11
13
 
@@ -209,14 +211,46 @@ export class RedirectSignal extends Error {
209
211
  /** Pattern matching absolute URLs: http(s):// or protocol-relative // */
210
212
  const ABSOLUTE_URL_RE = /^(?:[a-zA-Z][a-zA-Z\d+\-.]*:|\/\/)/;
211
213
 
214
+ /**
215
+ * Options for redirect() — alternative to passing a bare status code.
216
+ */
217
+ export interface RedirectOptions {
218
+ /** HTTP redirect status code (3xx). Defaults to 302. */
219
+ status?: number;
220
+ /**
221
+ * Preserve search params from the current request URL on the redirect target.
222
+ *
223
+ * - `true` — preserve ALL current search params (target params take precedence)
224
+ * - `string[]` — preserve only the named params (e.g. `['private', 'token']`)
225
+ *
226
+ * Target path's own query params always take precedence over preserved ones.
227
+ */
228
+ preserveSearchParams?: true | string[];
229
+ }
230
+
212
231
  /**
213
232
  * Redirect to a relative path. Rejects absolute and protocol-relative URLs.
214
233
  * Use `redirectExternal()` for external redirects with an allow-list.
215
234
  *
216
235
  * @param path - Relative path (e.g. '/login', 'settings', '/login?returnTo=/dash')
217
- * @param status - HTTP redirect status code (3xx). Defaults to 302.
236
+ * @param statusOrOptions - HTTP status code (3xx, default 302) or options object.
237
+ *
238
+ * @example
239
+ * // Simple redirect
240
+ * redirect('/login');
241
+ *
242
+ * // With status code
243
+ * redirect('/login', 301);
244
+ *
245
+ * // With preserved search params
246
+ * redirect(`/docs/${version}/${slug}`, { preserveSearchParams: ['foo'] });
218
247
  */
219
- export function redirect(path: string, status: number = 302): never {
248
+ export function redirect(path: string, statusOrOptions?: number | RedirectOptions): never {
249
+ const status =
250
+ typeof statusOrOptions === 'number' ? statusOrOptions : (statusOrOptions?.status ?? 302);
251
+ const preserveSearchParams =
252
+ typeof statusOrOptions === 'object' ? statusOrOptions.preserveSearchParams : undefined;
253
+
220
254
  if (status < 300 || status > 399) {
221
255
  throw new Error(`redirect() requires a 3xx status code, got ${status}.`);
222
256
  }
@@ -226,7 +260,14 @@ export function redirect(path: string, status: number = 302): never {
226
260
  'Use redirectExternal(url, allowList) for external redirects.'
227
261
  );
228
262
  }
229
- throw new RedirectSignal(path, status);
263
+
264
+ let resolvedPath = path;
265
+ if (preserveSearchParams) {
266
+ const currentSearch = getRequestSearchString();
267
+ resolvedPath = mergePreservedSearchParams(path, currentSearch, preserveSearchParams);
268
+ }
269
+
270
+ throw new RedirectSignal(resolvedPath, status);
230
271
  }
231
272
 
232
273
  /**
@@ -236,9 +277,10 @@ export function redirect(path: string, status: number = 302): never {
236
277
  * will replay POST requests to the new location. This matches Next.js behavior.
237
278
  *
238
279
  * @param path - Relative path (e.g. '/new-page', '/dashboard')
280
+ * @param options - Optional redirect options (e.g. preserveSearchParams).
239
281
  */
240
- export function permanentRedirect(path: string): never {
241
- redirect(path, 308);
282
+ export function permanentRedirect(path: string, options?: Omit<RedirectOptions, 'status'>): never {
283
+ redirect(path, { status: 308, ...options });
242
284
  }
243
285
 
244
286
  /**
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Render timeout utilities for SSR streaming pipeline.
3
+ *
4
+ * Provides a RenderTimeoutError class and a helper to create
5
+ * timeout-guarded AbortSignals. Used to defend against hung RSC
6
+ * streams and infinite SSR renders.
7
+ *
8
+ * Design doc: 02-rendering-pipeline.md §"Streaming Constraints"
9
+ */
10
+
11
+ /**
12
+ * Error thrown when an SSR render or RSC stream read exceeds the
13
+ * configured timeout. Callers can check `instanceof RenderTimeoutError`
14
+ * to distinguish timeout from other errors and return a 504 or close
15
+ * the connection cleanly.
16
+ */
17
+ export class RenderTimeoutError extends Error {
18
+ readonly timeoutMs: number;
19
+
20
+ constructor(timeoutMs: number, context?: string) {
21
+ const message = context
22
+ ? `Render timeout after ${timeoutMs}ms: ${context}`
23
+ : `Render timeout after ${timeoutMs}ms`;
24
+ super(message);
25
+ this.name = 'RenderTimeoutError';
26
+ this.timeoutMs = timeoutMs;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Result of createRenderTimeout — an AbortSignal that fires after
32
+ * the given duration, plus a cancel function to clear the timer
33
+ * when the render completes normally.
34
+ */
35
+ export interface RenderTimeout {
36
+ /** AbortSignal that aborts after timeoutMs. */
37
+ signal: AbortSignal;
38
+ /** Cancel the timeout timer. Call this when the render completes. */
39
+ cancel: () => void;
40
+ }
41
+
42
+ /**
43
+ * Create a render timeout that aborts after the given duration.
44
+ *
45
+ * Returns an AbortSignal and a cancel function. The signal fires
46
+ * with a RenderTimeoutError as the abort reason after `timeoutMs`.
47
+ * Call `cancel()` when the render completes to prevent the timeout
48
+ * from firing.
49
+ *
50
+ * If an existing `parentSignal` is provided, the returned signal
51
+ * aborts when either the parent signal or the timeout fires —
52
+ * whichever comes first.
53
+ */
54
+ export function createRenderTimeout(timeoutMs: number, parentSignal?: AbortSignal): RenderTimeout {
55
+ const controller = new AbortController();
56
+ const reason = new RenderTimeoutError(timeoutMs, 'RSC stream read timed out');
57
+
58
+ const timer = setTimeout(() => {
59
+ controller.abort(reason);
60
+ }, timeoutMs);
61
+
62
+ // If there's a parent signal (e.g. request abort), chain it
63
+ if (parentSignal) {
64
+ if (parentSignal.aborted) {
65
+ clearTimeout(timer);
66
+ controller.abort(parentSignal.reason);
67
+ } else {
68
+ parentSignal.addEventListener(
69
+ 'abort',
70
+ () => {
71
+ clearTimeout(timer);
72
+ controller.abort(parentSignal.reason);
73
+ },
74
+ { once: true }
75
+ );
76
+ }
77
+ }
78
+
79
+ return {
80
+ signal: controller.signal,
81
+ cancel: () => {
82
+ clearTimeout(timer);
83
+ },
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Race a promise against a timeout. Rejects with RenderTimeoutError
89
+ * if the promise does not resolve within `timeoutMs`.
90
+ *
91
+ * Used to guard individual `rscReader.read()` calls inside pullLoop.
92
+ */
93
+ export function withTimeout<T>(
94
+ promise: Promise<T>,
95
+ timeoutMs: number,
96
+ context?: string
97
+ ): Promise<T> {
98
+ let timer: ReturnType<typeof setTimeout>;
99
+ const timeoutPromise = new Promise<never>((_resolve, reject) => {
100
+ timer = setTimeout(() => {
101
+ reject(new RenderTimeoutError(timeoutMs, context));
102
+ }, timeoutMs);
103
+ });
104
+
105
+ return Promise.race([promise, timeoutPromise]).finally(() => {
106
+ clearTimeout(timer!);
107
+ });
108
+ }
@@ -10,8 +10,6 @@
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';
17
15
 
@@ -22,30 +20,6 @@ export { requestContextAls };
22
20
  // the ALS context persists for the entire request lifecycle including
23
21
  // async stream consumption by React's renderToReadableStream.
24
22
 
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
23
  // ─── Public API ───────────────────────────────────────────────────────────
50
24
 
51
25
  /**
@@ -109,12 +83,6 @@ export function cookies(): RequestCookies {
109
83
  return map.size;
110
84
  },
111
85
 
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
86
  set(name: string, value: string, options?: CookieOptions): void {
119
87
  assertMutable(store, 'set');
120
88
  if (store.flushed) {
@@ -127,21 +95,10 @@ export function cookies(): RequestCookies {
127
95
  }
128
96
  return;
129
97
  }
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
98
  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);
99
+ store.cookieJar.set(name, { name, value, options: opts });
100
+ // Read-your-own-writes: update the parsed cookies map
101
+ map.set(name, value);
145
102
  },
146
103
 
147
104
  delete(name: string, options?: Pick<CookieOptions, 'path' | 'domain'>): void {
@@ -190,25 +147,31 @@ export function cookies(): RequestCookies {
190
147
  }
191
148
 
192
149
  /**
193
- * Returns a Promise resolving to the current request's search params.
150
+ * Returns a Promise resolving to the current request's raw URLSearchParams.
151
+ *
152
+ * For typed, parsed search params, import the definition from params.ts
153
+ * and call `.load()` or `.parse()`:
154
+ *
155
+ * ```ts
156
+ * import { searchParams } from './params'
157
+ * const parsed = await searchParams.load()
158
+ * ```
194
159
  *
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`.
160
+ * Or explicitly:
199
161
  *
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.
162
+ * ```ts
163
+ * import { rawSearchParams } from '@timber-js/app/server'
164
+ * import { searchParams } from './params'
165
+ * const parsed = searchParams.parse(await rawSearchParams())
166
+ * ```
202
167
  *
203
168
  * Throws if called outside a request context.
204
169
  */
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>> {
170
+ export function rawSearchParams(): Promise<URLSearchParams> {
208
171
  const store = requestContextAls.getStore();
209
172
  if (!store) {
210
173
  throw new Error(
211
- '[timber] searchParams() called outside of a request context. ' +
174
+ '[timber] rawSearchParams() called outside of a request context. ' +
212
175
  'It can only be used in middleware, access checks, server components, and server actions.'
213
176
  );
214
177
  }
@@ -216,15 +179,69 @@ export function searchParams(): Promise<URLSearchParams | Record<string, unknown
216
179
  }
217
180
 
218
181
  /**
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.
182
+ * Returns a Promise resolving to the current request's coerced segment params.
183
+ *
184
+ * Segment params are set by the pipeline after route matching and param
185
+ * coercion (via params.ts codecs). When no params.ts exists, values are
186
+ * raw strings. When codecs are defined, values are already coerced
187
+ * (e.g., `id` is a `number` if `defineSegmentParams({ id: z.coerce.number() })`).
188
+ *
189
+ * This is the primary way page and layout components access route params:
190
+ *
191
+ * ```ts
192
+ * import { rawSegmentParams } from '@timber-js/app/server'
193
+ *
194
+ * export default async function Page() {
195
+ * const { slug } = await rawSegmentParams()
196
+ * // ...
197
+ * }
198
+ * ```
199
+ *
200
+ * Throws if called outside a request context.
222
201
  */
223
- export function setParsedSearchParams(parsed: Record<string, unknown>): void {
202
+ export function rawSegmentParams(): Promise<Record<string, string | string[]>> {
224
203
  const store = requestContextAls.getStore();
225
- if (store) {
226
- store.searchParamsPromise = Promise.resolve(parsed);
204
+ if (!store) {
205
+ throw new Error(
206
+ '[timber] rawSegmentParams() called outside of a request context. ' +
207
+ 'It can only be used in middleware, access checks, server components, and server actions.'
208
+ );
209
+ }
210
+ if (!store.segmentParamsPromise) {
211
+ throw new Error(
212
+ '[timber] rawSegmentParams() called before route matching completed. ' +
213
+ 'Segment params are not available until after the route is matched.'
214
+ );
215
+ }
216
+ return store.segmentParamsPromise;
217
+ }
218
+
219
+ /**
220
+ * Set the segment params promise on the current request context.
221
+ * Called by the pipeline after route matching and param coercion.
222
+ *
223
+ * @internal — framework use only
224
+ */
225
+ export function setSegmentParams(params: Record<string, string | string[]>): void {
226
+ const store = requestContextAls.getStore();
227
+ if (!store) {
228
+ throw new Error('[timber] setSegmentParams() called outside of a request context.');
227
229
  }
230
+ store.segmentParamsPromise = Promise.resolve(params);
231
+ }
232
+
233
+ /**
234
+ * Returns the raw search string from the current request URL (e.g. "?foo=bar").
235
+ * Synchronous — safe for use in `redirect()` which throws synchronously.
236
+ *
237
+ * Returns empty string if called outside a request context (non-throwing for
238
+ * use in redirect's optional preserveSearchParams path).
239
+ *
240
+ * @internal — used by redirect() for preserveSearchParams support.
241
+ */
242
+ export function getRequestSearchString(): string {
243
+ const store = requestContextAls.getStore();
244
+ return store?.searchString ?? '';
228
245
  }
229
246
 
230
247
  // ─── Types ────────────────────────────────────────────────────────────────
@@ -257,12 +274,6 @@ export interface CookieOptions {
257
274
  sameSite?: 'strict' | 'lax' | 'none';
258
275
  /** Partitioned (CHIPS) — isolate cookie per top-level site. Default: false. */
259
276
  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
277
  }
267
278
 
268
279
  const DEFAULT_COOKIE_OPTIONS: CookieOptions = {
@@ -287,14 +298,6 @@ export interface RequestCookies {
287
298
  getAll(): Array<{ name: string; value: string }>;
288
299
  /** Number of cookies. */
289
300
  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
301
  /** Set a cookie. Only available in mutable contexts (middleware, actions, route handlers). */
299
302
  set(name: string, value: string, options?: CookieOptions): void;
300
303
  /** Delete a cookie. Only available in mutable contexts. */
@@ -316,11 +319,13 @@ export interface RequestCookies {
316
319
  */
317
320
  export function runWithRequestContext<T>(req: Request, fn: () => T): T {
318
321
  const originalCopy = new Headers(req.headers);
322
+ const parsedUrl = new URL(req.url);
319
323
  const store: RequestContextStore = {
320
324
  headers: freezeHeaders(req.headers),
321
325
  originalHeaders: originalCopy,
322
326
  cookieHeader: req.headers.get('cookie') ?? '',
323
- searchParamsPromise: Promise.resolve(new URL(req.url).searchParams),
327
+ searchParamsPromise: Promise.resolve(parsedUrl.searchParams),
328
+ searchString: parsedUrl.search,
324
329
  cookieJar: new Map(),
325
330
  flushed: false,
326
331
  mutableContext: false,
@@ -354,6 +359,35 @@ export function markResponseFlushed(): void {
354
359
  }
355
360
  }
356
361
 
362
+ /**
363
+ * Build a Map of cookie name → value reflecting the current request's
364
+ * read-your-own-writes state. Includes incoming cookies plus any
365
+ * mutations from cookies().set() / cookies().delete() in the same request.
366
+ *
367
+ * Used by SSR renderers to populate NavContext.cookies so that
368
+ * useCookie()'s server snapshot matches the actual response state.
369
+ *
370
+ * See design/29-cookies.md §"Read-Your-Own-Writes"
371
+ * See design/triage/TIM-441-cookie-api-triage.md §4
372
+ */
373
+ export function getCookiesForSsr(): Map<string, string> {
374
+ const store = requestContextAls.getStore();
375
+ if (!store) {
376
+ throw new Error('[timber] getCookiesForSsr() called outside of a request context.');
377
+ }
378
+
379
+ // Trigger lazy parsing if not yet done
380
+ if (!store.parsedCookies) {
381
+ store.parsedCookies = parseCookieHeader(store.cookieHeader);
382
+ }
383
+
384
+ // The parsedCookies map already reflects read-your-own-writes:
385
+ // - cookies().set() updates the map via map.set(name, value)
386
+ // - cookies().delete() removes from the map via map.delete(name)
387
+ // Return a copy so callers can't mutate the internal map.
388
+ return new Map(store.parsedCookies);
389
+ }
390
+
357
391
  /**
358
392
  * Collect all Set-Cookie headers from the cookie jar.
359
393
  * Called by the framework at flush time to apply cookies to the response.
@@ -467,47 +501,6 @@ function parseCookieHeader(header: string): Map<string, string> {
467
501
  return map;
468
502
  }
469
503
 
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
504
  /** Serialize a CookieEntry into a Set-Cookie header value. */
512
505
  function serializeCookieEntry(entry: CookieEntry): string {
513
506
  const parts = [`${entry.name}=${entry.value}`];