@timber-js/app 0.2.0-alpha.4 → 0.2.0-alpha.40
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.
- package/LICENSE +8 -0
- package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
- package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
- package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
- package/dist/_chunks/debug-ECi_61pb.js +108 -0
- package/dist/_chunks/debug-ECi_61pb.js.map +1 -0
- package/dist/_chunks/define-cookie-BmKbSyp0.js +93 -0
- package/dist/_chunks/define-cookie-BmKbSyp0.js.map +1 -0
- package/dist/_chunks/error-boundary-BAN3751q.js +211 -0
- package/dist/_chunks/error-boundary-BAN3751q.js.map +1 -0
- package/dist/_chunks/{format-CwdaB0_2.js → format-cX7wzEp2.js} +2 -2
- package/dist/_chunks/{format-CwdaB0_2.js.map → format-cX7wzEp2.js.map} +1 -1
- package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
- package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
- package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
- package/dist/_chunks/{request-context-CZJi4CuK.js → request-context-BxYIJM24.js} +93 -69
- package/dist/_chunks/request-context-BxYIJM24.js.map +1 -0
- package/dist/_chunks/segment-context-C6byCyZU.js +69 -0
- package/dist/_chunks/segment-context-C6byCyZU.js.map +1 -0
- package/dist/_chunks/stale-reload-C0ValzG7.js +47 -0
- package/dist/_chunks/stale-reload-C0ValzG7.js.map +1 -0
- package/dist/_chunks/{tracing-Cwn7697K.js → tracing-CuXiCP5p.js} +17 -3
- package/dist/_chunks/{tracing-Cwn7697K.js.map → tracing-CuXiCP5p.js.map} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-BvW0TKDn.js} +1 -1
- package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-BvW0TKDn.js.map} +1 -1
- package/dist/_chunks/wrappers-C6J0nNji.js +331 -0
- package/dist/_chunks/wrappers-C6J0nNji.js.map +1 -0
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts +17 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +56 -13
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/fast-hash.d.ts +22 -0
- package/dist/cache/fast-hash.d.ts.map +1 -0
- package/dist/cache/index.d.ts +5 -2
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +88 -18
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/register-cached-function.d.ts.map +1 -1
- package/dist/cache/singleflight.d.ts +18 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +10 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -125
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +213 -93
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts +22 -8
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +2 -2
- package/dist/client/router.d.ts +25 -3
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +23 -2
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +1 -1
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-context.d.ts +1 -1
- package/dist/client/segment-context.d.ts.map +1 -1
- package/dist/client/segment-merger.d.ts.map +1 -1
- package/dist/client/stale-reload.d.ts +15 -0
- package/dist/client/stale-reload.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts +1 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/transition-root.d.ts +1 -1
- package/dist/client/transition-root.d.ts.map +1 -1
- package/dist/client/use-params.d.ts +2 -2
- package/dist/client/use-params.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts +1 -1
- package/dist/codec.d.ts +21 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/cookies/define-cookie.d.ts +33 -12
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -83
- package/dist/fonts/css.d.ts +1 -0
- package/dist/fonts/css.d.ts.map +1 -1
- package/dist/fonts/local.d.ts +4 -2
- package/dist/fonts/local.d.ts.map +1 -1
- package/dist/index.d.ts +112 -35
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +635 -233
- package/dist/index.js.map +1 -1
- package/dist/params/define.d.ts +76 -0
- package/dist/params/define.d.ts.map +1 -0
- package/dist/params/index.d.ts +8 -0
- package/dist/params/index.d.ts.map +1 -0
- package/dist/params/index.js +104 -0
- package/dist/params/index.js.map +1 -0
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/client-chunks.d.ts +32 -0
- package/dist/plugins/client-chunks.d.ts.map +1 -0
- package/dist/plugins/dev-error-overlay.d.ts +26 -1
- package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts +7 -0
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +9 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/mdx.d.ts +6 -0
- package/dist/plugins/mdx.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts +2 -2
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/status-file-lint.d.ts +2 -1
- package/dist/routing/status-file-lint.d.ts.map +1 -1
- package/dist/routing/types.d.ts +6 -4
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/rsc-runtime/rsc.d.ts +1 -1
- package/dist/rsc-runtime/rsc.d.ts.map +1 -1
- package/dist/rsc-runtime/ssr.d.ts +12 -0
- package/dist/rsc-runtime/ssr.d.ts.map +1 -1
- package/dist/search-params/codecs.d.ts +1 -1
- package/dist/search-params/define.d.ts +153 -0
- package/dist/search-params/define.d.ts.map +1 -0
- package/dist/search-params/index.d.ts +4 -5
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +3 -474
- package/dist/search-params/registry.d.ts +1 -1
- package/dist/search-params/wrappers.d.ts +53 -0
- package/dist/search-params/wrappers.d.ts.map +1 -0
- package/dist/server/access-gate.d.ts +4 -0
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/action-encryption.d.ts +76 -0
- package/dist/server/action-encryption.d.ts.map +1 -0
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +18 -4
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +2 -2
- package/dist/server/debug.d.ts +46 -15
- package/dist/server/debug.d.ts.map +1 -1
- package/dist/server/default-logger.d.ts +22 -0
- package/dist/server/default-logger.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/early-hints.d.ts +13 -5
- package/dist/server/early-hints.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +4 -0
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/flight-injection-state.d.ts +78 -0
- package/dist/server/flight-injection-state.d.ts.map +1 -0
- package/dist/server/flight-scripts.d.ts +39 -0
- package/dist/server/flight-scripts.d.ts.map +1 -0
- package/dist/server/flush.d.ts.map +1 -1
- package/dist/server/form-data.d.ts +29 -0
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +5 -11
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1975 -1649
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts +24 -7
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +77 -0
- package/dist/server/node-stream-transforms.d.ts.map +1 -0
- package/dist/server/pipeline.d.ts +7 -4
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +30 -3
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-timeout.d.ts +51 -0
- package/dist/server/render-timeout.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +65 -38
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +7 -0
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-handler.d.ts.map +1 -1
- package/dist/server/route-matcher.d.ts +2 -2
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +46 -3
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +6 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +9 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +22 -0
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +39 -21
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +10 -0
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +19 -12
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +1 -3
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/version-skew.d.ts +61 -0
- package/dist/server/version-skew.d.ts.map +1 -0
- package/dist/server/waituntil-bridge.d.ts.map +1 -1
- package/dist/shared/merge-search-params.d.ts +22 -0
- package/dist/shared/merge-search-params.d.ts.map +1 -0
- package/dist/shims/navigation-client.d.ts +1 -1
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +1 -1
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/utils/state-machine.d.ts +80 -0
- package/dist/utils/state-machine.d.ts.map +1 -0
- package/package.json +17 -14
- package/src/adapters/compress-module.ts +24 -4
- package/src/adapters/nitro.ts +58 -9
- package/src/cache/fast-hash.ts +34 -0
- package/src/cache/index.ts +5 -2
- package/src/cache/register-cached-function.ts +7 -3
- package/src/cache/singleflight.ts +62 -4
- package/src/cache/timber-cache.ts +34 -26
- package/src/cli.ts +0 -0
- package/src/client/browser-entry.ts +94 -90
- package/src/client/error-boundary.tsx +18 -1
- package/src/client/index.ts +10 -1
- package/src/client/link.tsx +78 -19
- package/src/client/navigation-context.ts +2 -2
- package/src/client/router.ts +105 -60
- package/src/client/rsc-fetch.ts +63 -2
- package/src/client/segment-cache.ts +1 -1
- package/src/client/segment-context.ts +6 -1
- package/src/client/segment-merger.ts +2 -8
- package/src/client/stale-reload.ts +32 -6
- package/src/client/top-loader.tsx +10 -9
- package/src/client/transition-root.tsx +7 -1
- package/src/client/use-params.ts +3 -3
- package/src/client/use-query-states.ts +1 -1
- package/src/codec.ts +21 -0
- package/src/cookies/define-cookie.ts +69 -18
- package/src/fonts/css.ts +2 -1
- package/src/fonts/local.ts +7 -3
- package/src/index.ts +280 -85
- package/src/params/define.ts +260 -0
- package/src/params/index.ts +28 -0
- package/src/plugins/adapter-build.ts +6 -0
- package/src/plugins/build-manifest.ts +11 -0
- package/src/plugins/client-chunks.ts +65 -0
- package/src/plugins/dev-error-overlay.ts +70 -1
- package/src/plugins/dev-server.ts +38 -4
- package/src/plugins/entries.ts +12 -11
- package/src/plugins/fonts.ts +171 -19
- package/src/plugins/mdx.ts +9 -5
- package/src/plugins/routing.ts +40 -14
- package/src/plugins/server-bundle.ts +32 -1
- package/src/plugins/shims.ts +1 -1
- package/src/plugins/static-build.ts +8 -4
- package/src/routing/codegen.ts +109 -88
- package/src/routing/scanner.ts +55 -6
- package/src/routing/status-file-lint.ts +2 -1
- package/src/routing/types.ts +7 -4
- package/src/rsc-runtime/rsc.ts +2 -0
- package/src/rsc-runtime/ssr.ts +50 -0
- package/src/rsc-runtime/vendor-types.d.ts +7 -0
- package/src/search-params/codecs.ts +1 -1
- package/src/search-params/define.ts +504 -0
- package/src/search-params/index.ts +12 -18
- package/src/search-params/registry.ts +1 -1
- package/src/search-params/wrappers.ts +85 -0
- package/src/server/access-gate.tsx +40 -9
- package/src/server/action-client.ts +14 -5
- package/src/server/action-encryption.ts +144 -0
- package/src/server/action-handler.ts +19 -2
- package/src/server/als-registry.ts +18 -4
- package/src/server/build-manifest.ts +4 -4
- package/src/server/compress.ts +25 -7
- package/src/server/debug.ts +55 -17
- package/src/server/default-logger.ts +98 -0
- package/src/server/deny-renderer.ts +2 -1
- package/src/server/early-hints.ts +36 -15
- package/src/server/error-boundary-wrapper.ts +57 -14
- package/src/server/flight-injection-state.ts +152 -0
- package/src/server/flight-scripts.ts +59 -0
- package/src/server/flush.ts +2 -1
- package/src/server/form-data.ts +76 -0
- package/src/server/html-injectors.ts +103 -66
- package/src/server/index.ts +9 -4
- package/src/server/logger.ts +38 -35
- package/src/server/node-stream-transforms.ts +381 -0
- package/src/server/pipeline.ts +131 -39
- package/src/server/primitives.ts +47 -5
- package/src/server/render-timeout.ts +108 -0
- package/src/server/request-context.ts +112 -119
- package/src/server/route-element-builder.ts +106 -114
- package/src/server/route-handler.ts +2 -1
- package/src/server/route-matcher.ts +2 -2
- package/src/server/rsc-entry/error-renderer.ts +5 -3
- package/src/server/rsc-entry/helpers.ts +122 -3
- package/src/server/rsc-entry/index.ts +125 -49
- package/src/server/rsc-entry/rsc-payload.ts +52 -12
- package/src/server/rsc-entry/rsc-stream.ts +33 -8
- package/src/server/rsc-entry/ssr-renderer.ts +40 -13
- package/src/server/slot-resolver.ts +199 -210
- package/src/server/ssr-entry.ts +169 -17
- package/src/server/ssr-render.ts +266 -67
- package/src/server/tracing.ts +23 -0
- package/src/server/tree-builder.ts +91 -57
- package/src/server/types.ts +1 -3
- package/src/server/version-skew.ts +104 -0
- package/src/server/waituntil-bridge.ts +4 -1
- package/src/shared/merge-search-params.ts +48 -0
- package/src/shims/navigation-client.ts +1 -1
- package/src/shims/navigation.ts +1 -1
- package/src/utils/state-machine.ts +111 -0
- package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
- package/dist/_chunks/debug-B4WUeqJ-.js +0 -75
- package/dist/_chunks/debug-B4WUeqJ-.js.map +0 -1
- package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
- package/dist/_chunks/request-context-CZJi4CuK.js.map +0 -1
- package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
- package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
- package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
- package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
- package/dist/client/error-boundary.js.map +0 -1
- package/dist/cookies/index.js.map +0 -1
- package/dist/plugins/dynamic-transform.d.ts +0 -72
- package/dist/plugins/dynamic-transform.d.ts.map +0 -1
- package/dist/search-params/analyze.d.ts +0 -54
- package/dist/search-params/analyze.d.ts.map +0 -1
- package/dist/search-params/builtin-codecs.d.ts +0 -105
- package/dist/search-params/builtin-codecs.d.ts.map +0 -1
- package/dist/search-params/create.d.ts +0 -106
- package/dist/search-params/create.d.ts.map +0 -1
- package/dist/search-params/index.js.map +0 -1
- package/dist/server/prerender.d.ts +0 -77
- package/dist/server/prerender.d.ts.map +0 -1
- package/dist/server/response-cache.d.ts +0 -53
- package/dist/server/response-cache.d.ts.map +0 -1
- package/src/plugins/dynamic-transform.ts +0 -161
- package/src/search-params/analyze.ts +0 -192
- package/src/search-params/builtin-codecs.ts +0 -228
- package/src/search-params/create.ts +0 -321
- package/src/server/prerender.ts +0 -139
- package/src/server/response-cache.ts +0 -277
package/src/server/primitives.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
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
|
|
142
|
-
// Read-your-own-writes: update the parsed cookies map
|
|
143
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
201
|
-
*
|
|
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
|
|
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]
|
|
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
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
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
|
|
202
|
+
export function rawSegmentParams(): Promise<Record<string, string | string[]>> {
|
|
224
203
|
const store = requestContextAls.getStore();
|
|
225
|
-
if (store) {
|
|
226
|
-
|
|
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(
|
|
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}`];
|