@timber-js/app 0.2.0-alpha.196 → 0.2.0-alpha.198
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/dist/_chunks/{actions-CWYtq6ii.js → actions-BS-m5SLv.js} +3 -3
- package/dist/_chunks/{actions-CWYtq6ii.js.map → actions-BS-m5SLv.js.map} +1 -1
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/{build-manifest-DWppEdLB.js → build-manifest-DTmSGLRz.js} +51 -2
- package/dist/_chunks/build-manifest-DTmSGLRz.js.map +1 -0
- package/dist/_chunks/{cache-api-CQeYzA5g.js → cache-api-DqzgTEqk.js} +4 -49
- package/dist/_chunks/cache-api-DqzgTEqk.js.map +1 -0
- package/dist/_chunks/{chains-h7EO-u3n.js → chains-CZG7E5zg.js} +2 -2
- package/dist/_chunks/{chains-h7EO-u3n.js.map → chains-CZG7E5zg.js.map} +1 -1
- package/dist/_chunks/{cli-check-BVthpfLS.js → cli-check-dVDi1GQz.js} +3 -3
- package/dist/_chunks/{cli-check-BVthpfLS.js.map → cli-check-dVDi1GQz.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-3Wutm8pH.js → cli-schema-sync-DTy_-Msq.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-3Wutm8pH.js.map → cli-schema-sync-DTy_-Msq.js.map} +1 -1
- package/dist/_chunks/{cloudflare-BKJC3SC_.js → cloudflare-BFb__LYG.js} +2 -2
- package/dist/_chunks/{cloudflare-BKJC3SC_.js.map → cloudflare-BFb__LYG.js.map} +1 -1
- package/dist/_chunks/{convention-lint-DO10_pVl.js → convention-lint-Ph6luW4c.js} +4 -2
- package/dist/_chunks/convention-lint-Ph6luW4c.js.map +1 -0
- package/dist/_chunks/{error-boundary-D-lkwyaD.js → error-boundary-BvRCCmbN.js} +3 -3
- package/dist/_chunks/{error-boundary-D-lkwyaD.js.map → error-boundary-BvRCCmbN.js.map} +1 -1
- package/dist/_chunks/{href-validation-CMc5JRls.js → href-validation-BIrxavIy.js} +74 -2
- package/dist/_chunks/href-validation-BIrxavIy.js.map +1 -0
- package/dist/_chunks/{live-graph-Bx4HodF1.js → live-graph-BXDsdzBv.js} +3 -3
- package/dist/_chunks/{live-graph-Bx4HodF1.js.map → live-graph-BXDsdzBv.js.map} +1 -1
- package/dist/_chunks/{logger-pumCm3Il.js → logger-DDirEsn7.js} +3 -4
- package/dist/_chunks/{logger-pumCm3Il.js.map → logger-DDirEsn7.js.map} +1 -1
- package/dist/_chunks/navigation-root-B00jjGd5.js +233 -0
- package/dist/_chunks/navigation-root-B00jjGd5.js.map +1 -0
- package/dist/_chunks/{segment-context-CjOlyB8Y.js → param-value-C8TNYchQ.js} +2 -33
- package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
- package/dist/_chunks/{poison-scan-BAxfTT5L.js → poison-scan-BoDLgbix.js} +2 -2
- package/dist/_chunks/{poison-scan-BAxfTT5L.js.map → poison-scan-BoDLgbix.js.map} +1 -1
- package/dist/_chunks/{router-ref-BzqbPwYC.js → router-ref-8gr8qsxN.js} +2 -2
- package/dist/_chunks/{router-ref-BzqbPwYC.js.map → router-ref-8gr8qsxN.js.map} +1 -1
- package/dist/_chunks/{rsc-cache-key-DD0fl_-s.js → rsc-cache-key-ClUiXQnK.js} +2 -2
- package/dist/_chunks/{rsc-cache-key-DD0fl_-s.js.map → rsc-cache-key-ClUiXQnK.js.map} +1 -1
- package/dist/_chunks/{scanner-BRIOmHE2.js → scanner-tdFPvDYi.js} +174 -7
- package/dist/_chunks/scanner-tdFPvDYi.js.map +1 -0
- package/dist/_chunks/segment-context-D9_89u34.js +34 -0
- package/dist/_chunks/segment-context-D9_89u34.js.map +1 -0
- package/dist/_chunks/singleflight-2lUWfcAk.js +54 -0
- package/dist/_chunks/singleflight-2lUWfcAk.js.map +1 -0
- package/dist/_chunks/{ssr-data-Ya2HJPFp.js → ssr-data-BQGhTPAK.js} +2 -17
- package/dist/_chunks/ssr-data-BQGhTPAK.js.map +1 -0
- package/dist/_chunks/{walkers-BU6z9xRV.js → walkers-DNX05dC0.js} +2 -2
- package/dist/_chunks/{walkers-BU6z9xRV.js.map → walkers-DNX05dC0.js.map} +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/nitro.d.ts +1 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cache/index.js +1 -1
- package/dist/cache/singleflight.d.ts +2 -0
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/hydrate.d.ts +21 -15
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/index.d.ts +4 -3
- package/dist/client/browser-entry/index.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +17 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/global-context.d.ts +15 -0
- package/dist/client/global-context.d.ts.map +1 -0
- package/dist/client/index.js +138 -35
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +0 -1
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +206 -55
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/location-search.d.ts +12 -0
- package/dist/client/location-search.d.ts.map +1 -0
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/navigation-commit.d.ts +18 -0
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +13 -11
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +47 -108
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/navigation-transition.d.ts +136 -0
- package/dist/client/navigation-transition.d.ts.map +1 -0
- package/dist/client/nuqs-adapter.d.ts.map +1 -1
- package/dist/client/params-context.d.ts +4 -5
- package/dist/client/params-context.d.ts.map +1 -1
- package/dist/client/react-root.d.ts +44 -0
- package/dist/client/react-root.d.ts.map +1 -0
- package/dist/client/router-pipeline.d.ts +2 -2
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +12 -2
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +39 -0
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-context.d.ts.map +1 -1
- package/dist/client/segment-outlet.d.ts +25 -14
- package/dist/client/segment-outlet.d.ts.map +1 -1
- package/dist/client/segment-update-context.d.ts +3 -9
- package/dist/client/segment-update-context.d.ts.map +1 -1
- package/dist/client/slot-content-cache-context.d.ts +35 -0
- package/dist/client/slot-content-cache-context.d.ts.map +1 -0
- package/dist/client/ssr-data.d.ts +8 -2
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/state.d.ts +0 -15
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/use-pathname.d.ts +13 -11
- package/dist/client/use-pathname.d.ts.map +1 -1
- package/dist/client/use-search-params.d.ts +13 -13
- package/dist/client/use-search-params.d.ts.map +1 -1
- package/dist/client/use-segment-params.d.ts +18 -68
- package/dist/client/use-segment-params.d.ts.map +1 -1
- package/dist/config-types.d.ts +17 -0
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/holding-server.d.ts +15 -10
- package/dist/dev-tools/holding-server.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -43
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +2 -2
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception-overlap.d.ts +35 -0
- package/dist/routing/interception-overlap.d.ts.map +1 -0
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/rsc-runtime/ssr.d.ts +3 -1
- package/dist/rsc-runtime/ssr.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +6 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/csp-nonce.d.ts +45 -0
- package/dist/server/csp-nonce.d.ts.map +1 -0
- package/dist/server/default-status-page.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/flight-scripts.d.ts +5 -2
- package/dist/server/flight-scripts.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +17 -2
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +3 -2
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +86 -37
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-render.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +3 -17
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/nuqs-ssr-provider.d.ts +7 -3
- package/dist/server/nuqs-ssr-provider.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/prebuilt/key-discipline.d.ts +32 -3
- package/dist/server/prebuilt/key-discipline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-utils.d.ts +4 -3
- package/dist/server/render-utils.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +22 -2
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +5 -1
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts +59 -27
- package/dist/server/ssr-wrappers.d.ts.map +1 -1
- package/dist/server/types.d.ts +10 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/shims/navigation-rsc.d.ts +21 -0
- package/dist/shims/navigation-rsc.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +1 -0
- package/docs/api/35-api-typescript.mdx +4 -83
- package/docs/learn/03-fetching-data.mdx +1 -1
- package/docs/learn/{03b-access-control.mdx → 04-access-control.mdx} +2 -17
- package/docs/learn/05-the-flush-point.mdx +175 -0
- package/docs/learn/{05-typed-params.mdx → 06-typed-params.mdx} +1 -1
- package/docs/learn/07-typed-routes.mdx +25 -49
- package/docs/learn/{08-streaming.mdx → 09-streaming.mdx} +1 -7
- package/docs/learn/{10-middleware.mdx → 11-middleware.mdx} +1 -0
- package/package.json +3 -3
- package/src/adapters/nitro.ts +7 -7
- package/src/cache/singleflight.ts +5 -0
- package/src/client/browser-entry/hydrate.ts +54 -104
- package/src/client/browser-entry/index.ts +16 -6
- package/src/client/browser-entry/post-hydration.ts +3 -2
- package/src/client/browser-entry/router-init.ts +84 -33
- package/src/client/global-context.ts +31 -0
- package/src/client/internal.ts +1 -2
- package/src/client/link.tsx +18 -18
- package/src/client/location-search.ts +15 -0
- package/src/client/navigation-api.ts +4 -2
- package/src/client/navigation-commit.ts +48 -2
- package/src/client/navigation-context.ts +25 -37
- package/src/client/navigation-root.tsx +55 -411
- package/src/client/navigation-transition.ts +278 -0
- package/src/client/nuqs-adapter.tsx +4 -5
- package/src/client/params-context.ts +13 -18
- package/src/client/react-root.ts +72 -0
- package/src/client/router-lifecycle.ts +1 -1
- package/src/client/router-pipeline.ts +96 -22
- package/src/client/router-types.ts +12 -2
- package/src/client/router.ts +48 -36
- package/src/client/segment-cache.ts +70 -2
- package/src/client/segment-context.ts +7 -4
- package/src/client/segment-outlet.tsx +41 -86
- package/src/client/segment-update-context.ts +7 -26
- package/src/client/slot-content-cache-context.ts +43 -0
- package/src/client/ssr-data.ts +8 -2
- package/src/client/state.ts +0 -26
- package/src/client/use-pathname.ts +21 -31
- package/src/client/use-search-params.ts +31 -29
- package/src/client/use-segment-params.ts +27 -126
- package/src/config-types.ts +17 -0
- package/src/config-validation.ts +17 -0
- package/src/dev-tools/holding-server.ts +23 -12
- package/src/index.ts +26 -11
- package/src/plugins/dev-server.ts +9 -12
- package/src/plugins/entries.ts +3 -0
- package/src/plugins/shims.ts +8 -7
- package/src/plugins/static-build.ts +9 -5
- package/src/react-canary.d.ts +2 -0
- package/src/routing/codegen-write.ts +2 -0
- package/src/routing/interception-overlap.ts +141 -0
- package/src/routing/interception.ts +118 -5
- package/src/rsc-runtime/ssr.ts +3 -2
- package/src/server/als-registry.ts +6 -0
- package/src/server/csp-nonce.ts +70 -0
- package/src/server/default-status-page.ts +1 -0
- package/src/server/deny-renderer.ts +7 -3
- package/src/server/flight-scripts.ts +9 -4
- package/src/server/html-injector-core.ts +26 -9
- package/src/server/html-injectors.ts +8 -8
- package/src/server/metadata-render.ts +26 -4
- package/src/server/node-stream-transforms.ts +7 -20
- package/src/server/nuqs-ssr-provider.tsx +8 -7
- package/src/server/pipeline-phases.ts +5 -0
- package/src/server/prebuilt/key-discipline.ts +82 -13
- package/src/server/prebuilt-runtime.ts +2 -2
- package/src/server/primitives.ts +4 -4
- package/src/server/render-utils.ts +8 -4
- package/src/server/rsc-entry/action-middleware-runner.ts +7 -0
- package/src/server/rsc-entry/error-renderer.ts +5 -2
- package/src/server/rsc-entry/index.ts +8 -0
- package/src/server/rsc-entry/ssr-renderer.ts +11 -4
- package/src/server/ssr-bridge-types.ts +22 -2
- package/src/server/ssr-entry.ts +35 -28
- package/src/server/ssr-render.ts +13 -4
- package/src/server/ssr-wrappers.tsx +81 -61
- package/src/server/types.ts +10 -0
- package/src/shared/slot-params.ts +3 -4
- package/src/shims/navigation-rsc.ts +47 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +0 -1
- package/dist/_chunks/cache-api-CQeYzA5g.js.map +0 -1
- package/dist/_chunks/convention-lint-DO10_pVl.js.map +0 -1
- package/dist/_chunks/href-validation-CMc5JRls.js.map +0 -1
- package/dist/_chunks/scanner-BRIOmHE2.js.map +0 -1
- package/dist/_chunks/segment-context-CjOlyB8Y.js.map +0 -1
- package/dist/_chunks/slot-params-BCTmZkQB.js +0 -76
- package/dist/_chunks/slot-params-BCTmZkQB.js.map +0 -1
- package/dist/_chunks/ssr-data-Ya2HJPFp.js.map +0 -1
- package/dist/_chunks/use-segment-params-DzTBpkvj.js +0 -398
- package/dist/_chunks/use-segment-params-DzTBpkvj.js.map +0 -1
- package/docs/learn/04-loading-states.mdx +0 -67
- package/docs/learn/04b-the-flush-point.mdx +0 -115
- package/docs/learn/12-client-navigation.mdx +0 -176
- package/docs/learn/13-configuration.mdx +0 -166
- package/docs/more/01-advanced-routing.mdx +0 -344
- package/docs/more/02-advanced-forms.mdx +0 -137
- package/docs/more/03-coming-from-nextjs.mdx +0 -186
- package/docs/more/04-metadata-and-fonts.mdx +0 -193
- package/docs/more/04b-mdx.mdx +0 -229
- package/docs/more/05-content-collections.mdx +0 -90
- package/docs/more/06-instrumentation.mdx +0 -214
- package/docs/more/07-security.mdx +0 -129
- package/docs/more/08-developer-experience.mdx +0 -134
- package/docs/more/40-why-timber.mdx +0 -50
- package/docs/more/41-timber-vs-nextjs.mdx +0 -81
- package/docs/more/42-timber-vs-others.mdx +0 -68
- package/docs/more/50-ai-agent-instructions.mdx +0 -171
- /package/docs/learn/{06-forms-and-actions.mdx → 08-forms-and-actions.mdx} +0 -0
- /package/docs/learn/{09-caching.mdx → 10-caching.mdx} +0 -0
- /package/docs/learn/{11-error-handling.mdx → 12-error-handling.mdx} +0 -0
- /package/docs/learn/{14-deploying.mdx → 13-deploying.mdx} +0 -0
|
@@ -24,6 +24,7 @@ import { pipeline } from 'node:stream/promises';
|
|
|
24
24
|
import {
|
|
25
25
|
BufferAggregator,
|
|
26
26
|
FlightInjectorCore,
|
|
27
|
+
type FlightInjectorOptions,
|
|
27
28
|
SUFFIX_BYTES,
|
|
28
29
|
createInjectorState,
|
|
29
30
|
createSuffixState,
|
|
@@ -34,7 +35,7 @@ import {
|
|
|
34
35
|
} from './html-injector-core.ts';
|
|
35
36
|
import { logStreamingError } from './logger.ts';
|
|
36
37
|
import { createTickCoalescer } from './coalesce-tick.ts';
|
|
37
|
-
import { isAbortError,
|
|
38
|
+
import { isAbortError, noindexScript } from './render-utils.ts';
|
|
38
39
|
|
|
39
40
|
// ─── Buffered Transform ──────────────────────────────────────────────────────
|
|
40
41
|
|
|
@@ -167,21 +168,6 @@ export function createNodeHeadInjector(headHtml: string): Transform {
|
|
|
167
168
|
|
|
168
169
|
// ─── RSC Flight Injection ────────────────────────────────────────────────────
|
|
169
170
|
|
|
170
|
-
/**
|
|
171
|
-
* Options for the Node.js flight injector.
|
|
172
|
-
*/
|
|
173
|
-
export interface NodeFlightInjectorOptions {
|
|
174
|
-
/**
|
|
175
|
-
* Timeout in milliseconds for individual RSC stream reads. If a single
|
|
176
|
-
* `rscReader.read()` call does not resolve within this duration, the
|
|
177
|
-
* read is aborted and the stream errors with a `RenderTimeoutError`.
|
|
178
|
-
* Default: 30000 (30s).
|
|
179
|
-
*/
|
|
180
|
-
renderTimeoutMs?: number;
|
|
181
|
-
/** Signal that fires when an unhandled RSC error is detected. */
|
|
182
|
-
errorSignal?: AbortSignal;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
171
|
/**
|
|
186
172
|
* Node.js `Transform` that merges RSC script tags into the HTML stream.
|
|
187
173
|
*
|
|
@@ -202,7 +188,7 @@ export interface NodeFlightInjectorOptions {
|
|
|
202
188
|
*/
|
|
203
189
|
export function createNodeFlightInjector(
|
|
204
190
|
rscStream: ReadableStream<Uint8Array> | undefined,
|
|
205
|
-
options?:
|
|
191
|
+
options?: FlightInjectorOptions
|
|
206
192
|
): Transform {
|
|
207
193
|
if (!rscStream) {
|
|
208
194
|
return new Transform({
|
|
@@ -212,7 +198,7 @@ export function createNodeFlightInjector(
|
|
|
212
198
|
});
|
|
213
199
|
}
|
|
214
200
|
|
|
215
|
-
const core = new FlightInjectorCore(rscStream, options
|
|
201
|
+
const core = new FlightInjectorCore(rscStream, options);
|
|
216
202
|
|
|
217
203
|
const drain = (): void => {
|
|
218
204
|
while (core.pending.length > 0) transform.push(Buffer.from(core.pending.shift()!));
|
|
@@ -300,7 +286,8 @@ export function createNodeFlightInjector(
|
|
|
300
286
|
export function runNodePipelineWithErrorHandling(
|
|
301
287
|
source: Readable,
|
|
302
288
|
transforms: readonly [Transform, ...Transform[]],
|
|
303
|
-
signal?: AbortSignal
|
|
289
|
+
signal?: AbortSignal,
|
|
290
|
+
nonce?: string
|
|
304
291
|
): PassThrough {
|
|
305
292
|
const output = new PassThrough();
|
|
306
293
|
const last = transforms[transforms.length - 1]!;
|
|
@@ -329,7 +316,7 @@ export function runNodePipelineWithErrorHandling(
|
|
|
329
316
|
return;
|
|
330
317
|
}
|
|
331
318
|
logStreamingError({ error });
|
|
332
|
-
output.end(Buffer.from(
|
|
319
|
+
output.end(Buffer.from(noindexScript(nonce), 'utf-8'));
|
|
333
320
|
});
|
|
334
321
|
|
|
335
322
|
return output;
|
|
@@ -45,17 +45,18 @@ function makeNuqsSsrAdapter(searchParams: URLSearchParams) {
|
|
|
45
45
|
* Wrap the SSR element tree with a nuqs adapter context.
|
|
46
46
|
*
|
|
47
47
|
* Called by ssr-entry.ts before passing the element to renderSsrStream.
|
|
48
|
-
* Takes the NavContext
|
|
48
|
+
* Takes the NavContext raw query string and provides it to nuqs hooks
|
|
49
49
|
* running during SSR so they render with the correct initial values.
|
|
50
50
|
*
|
|
51
|
-
*
|
|
51
|
+
* The raw string — not a collapsed record — is what preserves repeated
|
|
52
|
+
* query keys (`?tag=a&tag=b`) for nuqs multi parsers, matching the raw
|
|
53
|
+
* `location.search` the client adapter reads after hydration (TIM-1428).
|
|
54
|
+
*
|
|
55
|
+
* @param search - The request's raw app-visible query string (`''` or `?…`)
|
|
52
56
|
* @param children - The React element tree to wrap
|
|
53
57
|
*/
|
|
54
|
-
export function withNuqsSsrAdapter(
|
|
55
|
-
|
|
56
|
-
children: ReactNode
|
|
57
|
-
): ReactNode {
|
|
58
|
-
const searchParams = new URLSearchParams(searchParamsRecord);
|
|
58
|
+
export function withNuqsSsrAdapter(search: string, children: ReactNode): ReactNode {
|
|
59
|
+
const searchParams = new URLSearchParams(search);
|
|
59
60
|
const Provider = makeNuqsSsrAdapter(searchParams);
|
|
60
61
|
// AdapterProvider types require children in the props object (not as 3rd arg)
|
|
61
62
|
// eslint-disable-next-line react/no-children-prop
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
setMatchedSegmentPath,
|
|
24
24
|
} from './request-context.ts';
|
|
25
25
|
import { withSpan } from './tracing.ts';
|
|
26
|
+
import { requestCspNonce } from './csp-nonce.ts';
|
|
26
27
|
import { logRenderError } from './logger.ts';
|
|
27
28
|
import { RedirectSignal, DenySignal } from './primitives.ts';
|
|
28
29
|
import { ParamCoercionError } from './route-element-builder.ts';
|
|
@@ -126,6 +127,10 @@ export async function runMiddlewarePhase(
|
|
|
126
127
|
requestHeaders: requestHeaderOverlay,
|
|
127
128
|
headers: responseHeaders,
|
|
128
129
|
segmentParams: match.segmentParams,
|
|
130
|
+
// Lazy: reading it is the opt-in that makes the render nonce its scripts.
|
|
131
|
+
get nonce() {
|
|
132
|
+
return requestCspNonce();
|
|
133
|
+
},
|
|
129
134
|
earlyHints: (hints) => {
|
|
130
135
|
for (const hint of hints) {
|
|
131
136
|
// Match Cloudflare's cached Early Hints attribute order: `as` before `rel`.
|
|
@@ -6,13 +6,19 @@
|
|
|
6
6
|
* values in cached output are the data-leak danger zone (design/06
|
|
7
7
|
* security note). During a capture render the ALS store is wrapped in a
|
|
8
8
|
* transparent observing proxy; any request-context read produces a
|
|
9
|
-
*
|
|
9
|
+
* structured, rate-limited production diagnostic.
|
|
10
|
+
*
|
|
11
|
+
* The diagnostic fires per offending component, rate-limited to once per
|
|
12
|
+
* interval (default 60 s) — not once per process. This ensures the
|
|
13
|
+
* signal stays visible in log aggregators across restarts and deploys.
|
|
14
|
+
*
|
|
15
|
+
* When `strictKeyDiscipline` is enabled (via timber.config.ts
|
|
16
|
+
* `cache.strictKeyDiscipline: true`), the diagnostic is promoted to a
|
|
17
|
+
* thrown error that prevents the tainted payload from being cached.
|
|
10
18
|
*/
|
|
11
19
|
|
|
12
20
|
import type { RequestContextStore } from '../als-registry.ts';
|
|
13
|
-
|
|
14
|
-
/** Components already warned — exported so tests can reset between cases. */
|
|
15
|
-
export const keyDisciplineWarned = new Set<string>();
|
|
21
|
+
import { getLogger } from '../logger.ts';
|
|
16
22
|
|
|
17
23
|
const REQUEST_CONTEXT_FIELDS: Record<string, string> = {
|
|
18
24
|
headers: 'headers()',
|
|
@@ -20,6 +26,35 @@ const REQUEST_CONTEXT_FIELDS: Record<string, string> = {
|
|
|
20
26
|
searchParams: 'searchParams',
|
|
21
27
|
};
|
|
22
28
|
|
|
29
|
+
/** Rate-limit interval: one diagnostic per component per 60 seconds. */
|
|
30
|
+
const RATE_LIMIT_MS = 60_000;
|
|
31
|
+
|
|
32
|
+
/** Last emission timestamp per component ID. */
|
|
33
|
+
const lastEmitted = new Map<string, number>();
|
|
34
|
+
|
|
35
|
+
let strict = false;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Configure strict key-discipline mode. When true, a violation throws
|
|
39
|
+
* instead of logging — the tainted payload is never cached.
|
|
40
|
+
* Called at boot from the RSC entry with the resolved config value.
|
|
41
|
+
*/
|
|
42
|
+
export function setKeyDisciplineStrict(enabled: boolean): void {
|
|
43
|
+
strict = enabled;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether the proxy should be created for this component on this render.
|
|
48
|
+
* Returns true when strict mode is on (always check) or enough time has
|
|
49
|
+
* elapsed since the last diagnostic for this component.
|
|
50
|
+
*/
|
|
51
|
+
export function shouldCheckKeyDiscipline(id: string): boolean {
|
|
52
|
+
if (strict) return true;
|
|
53
|
+
const last = lastEmitted.get(id);
|
|
54
|
+
if (last === undefined) return true;
|
|
55
|
+
return Date.now() - last >= RATE_LIMIT_MS;
|
|
56
|
+
}
|
|
57
|
+
|
|
23
58
|
/**
|
|
24
59
|
* Wrap a store with a proxy that records per-request field access.
|
|
25
60
|
* The proxy forwards all reads/writes transparently — it only observes.
|
|
@@ -39,15 +74,49 @@ export function createKeyDisciplineProxy(store: RequestContextStore): {
|
|
|
39
74
|
return { proxy, accessedApis };
|
|
40
75
|
}
|
|
41
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Emit the key-discipline diagnostic or throw in strict mode.
|
|
79
|
+
*
|
|
80
|
+
* In strict mode: throws an error so the tainted payload is never cached.
|
|
81
|
+
* In default mode: emits a structured error-level diagnostic through the
|
|
82
|
+
* logger (same path as render errors), rate-limited per component.
|
|
83
|
+
*/
|
|
42
84
|
export function emitKeyDisciplineWarning(id: string, apis: Set<string>): void {
|
|
43
|
-
if (keyDisciplineWarned.has(id)) return;
|
|
44
|
-
keyDisciplineWarned.add(id);
|
|
45
85
|
const apiList = Array.from(apis).join(', ');
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
86
|
+
|
|
87
|
+
if (strict) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`[timber] cache.component key-discipline violation: "${id}" reads ${apiList} ` +
|
|
90
|
+
`during a cache capture render. The cache key does not include ` +
|
|
91
|
+
`request-derived values — caching this output would serve one ` +
|
|
92
|
+
`user's data to another. Either pass request-derived values as ` +
|
|
93
|
+
`props (included in the cache key) or remove the ${apiList} call. ` +
|
|
94
|
+
`To downgrade this to a warning, set cache.strictKeyDiscipline: false ` +
|
|
95
|
+
`in timber.config.ts.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const last = lastEmitted.get(id);
|
|
101
|
+
if (last !== undefined && now - last < RATE_LIMIT_MS) return;
|
|
102
|
+
lastEmitted.set(id, now);
|
|
103
|
+
|
|
104
|
+
getLogger().error('cache.component cross-user data leak risk', {
|
|
105
|
+
component: id,
|
|
106
|
+
accessedApis: Array.from(apis),
|
|
107
|
+
message:
|
|
108
|
+
`cache.component "${id}" reads ${apiList} but the cache key is ` +
|
|
109
|
+
`computed from props and segment params only. If the output varies ` +
|
|
110
|
+
`by ${apiList}, different users may see each other's cached content. ` +
|
|
111
|
+
`Either pass request-derived values as props (included in the cache ` +
|
|
112
|
+
`key) or remove the ${apiList} call.`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Reset all diagnostic state. Exported for tests only.
|
|
118
|
+
*/
|
|
119
|
+
export function resetKeyDisciplineState(): void {
|
|
120
|
+
lastEmitted.clear();
|
|
121
|
+
strict = false;
|
|
53
122
|
}
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
import {
|
|
49
49
|
createKeyDisciplineProxy,
|
|
50
50
|
emitKeyDisciplineWarning,
|
|
51
|
-
|
|
51
|
+
shouldCheckKeyDiscipline,
|
|
52
52
|
} from './prebuilt/key-discipline.ts';
|
|
53
53
|
import {
|
|
54
54
|
isCaptureActive,
|
|
@@ -348,7 +348,7 @@ async function captureComponentPayload(
|
|
|
348
348
|
// store to detect request-context reads during the capture render.
|
|
349
349
|
// The proxy is transparent — it never blocks or throws; it only observes.
|
|
350
350
|
const store = requestContextAls.getStore();
|
|
351
|
-
const discipline = store &&
|
|
351
|
+
const discipline = store && shouldCheckKeyDiscipline(id) ? createKeyDisciplineProxy(store) : null;
|
|
352
352
|
|
|
353
353
|
const doCapture = async (): Promise<Uint8Array> => {
|
|
354
354
|
beginCapture(id);
|
package/src/server/primitives.ts
CHANGED
|
@@ -476,11 +476,11 @@ export function _resetWaitUntilWarning(): void {
|
|
|
476
476
|
* react-dom/server which cannot be loaded in the RSC environment.
|
|
477
477
|
*/
|
|
478
478
|
export class SsrStreamError extends Error {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
) {
|
|
479
|
+
override readonly cause: unknown;
|
|
480
|
+
|
|
481
|
+
constructor(message: string, cause: unknown) {
|
|
483
482
|
super(message);
|
|
484
483
|
this.name = 'SsrStreamError';
|
|
484
|
+
this.cause = cause;
|
|
485
485
|
}
|
|
486
486
|
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* ssr-render.ts, and rsc-entry/helpers.ts. See TIM-1071 item 1.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { nonceAttr } from './csp-nonce.ts';
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Check if an error is an abort error (connection closed by client).
|
|
11
13
|
*
|
|
@@ -24,8 +26,10 @@ export function isAbortError(error: unknown): boolean {
|
|
|
24
26
|
* document head. Injected when a render-phase error replaces the shell so
|
|
25
27
|
* search engines don't index error pages.
|
|
26
28
|
*
|
|
27
|
-
* MUST be byte-identical across all stream wrappers (Node and Web)
|
|
28
|
-
* ensure consistent Content-Length headers
|
|
29
|
+
* MUST be byte-identical across all stream wrappers (Node and Web) for a
|
|
30
|
+
* given nonce to ensure consistent Content-Length headers — which is why
|
|
31
|
+
* both wrappers call this one function rather than inlining the string.
|
|
29
32
|
*/
|
|
30
|
-
export
|
|
31
|
-
|
|
33
|
+
export function noindexScript(nonce?: string): string {
|
|
34
|
+
return `<script${nonceAttr(nonce)}>document.head.appendChild(Object.assign(document.createElement("meta"),{name:"robots",content:"noindex"}))</script>`;
|
|
35
|
+
}
|
|
@@ -30,6 +30,7 @@ import { DenySignal, RedirectSignal } from '../primitives.ts';
|
|
|
30
30
|
import { ParamCoercionError } from '../route-element-builder.ts';
|
|
31
31
|
import type { RouteMatch } from '../pipeline.ts';
|
|
32
32
|
import type { MiddlewareContext } from '../types.ts';
|
|
33
|
+
import { requestCspNonce } from '../csp-nonce.ts';
|
|
33
34
|
|
|
34
35
|
/** Coerce a matched route's segment params via its `params.ts` codecs. */
|
|
35
36
|
export type CoerceSegmentParamsFn = (match: RouteMatch) => Promise<void>;
|
|
@@ -112,6 +113,12 @@ export async function runMiddlewareForAction(
|
|
|
112
113
|
// Provide a no-op so user middleware can call `ctx.earlyHints()`
|
|
113
114
|
// unconditionally without branching on request kind.
|
|
114
115
|
earlyHints: () => {},
|
|
116
|
+
// Same reasoning: an action response carries no inline scripts, so a
|
|
117
|
+
// nonce has nothing to stamp. Still a real value so middleware that
|
|
118
|
+
// builds its CSP header unconditionally keeps working.
|
|
119
|
+
get nonce() {
|
|
120
|
+
return requestCspNonce();
|
|
121
|
+
},
|
|
115
122
|
};
|
|
116
123
|
|
|
117
124
|
// ─── Run the chain ──────────────────────────────────────────────
|
|
@@ -23,6 +23,7 @@ import type { ManifestSegmentNode } from '../route-matcher.ts';
|
|
|
23
23
|
import { DenySignal, RenderError } from '../primitives.ts';
|
|
24
24
|
import type { ClientBootstrapConfig } from '../html-injectors.ts';
|
|
25
25
|
import { flightInitScript, htmlEscapeJsonString } from '../flight-scripts.ts';
|
|
26
|
+
import { getCspNonce } from '../csp-nonce.ts';
|
|
26
27
|
import { renderDenyPage, renderDenyPageAsRsc } from '../deny-renderer.ts';
|
|
27
28
|
import type { DefaultPageRenderer, LayoutEntry } from '../deny-renderer.ts';
|
|
28
29
|
import type { NavContext } from '../ssr-bridge-types.ts';
|
|
@@ -324,15 +325,17 @@ async function renderErrorPageViaRsc(
|
|
|
324
325
|
// bootstrap script. Read here for the SSR-side ALS context below.
|
|
325
326
|
const slotParams = getSlotParamsForClient();
|
|
326
327
|
|
|
328
|
+
const nonce = getCspNonce();
|
|
327
329
|
const navContext: NavContext = {
|
|
328
330
|
pathname: url.pathname,
|
|
331
|
+
search: appVisibleSearch(url).search,
|
|
329
332
|
segmentParams: match.segmentParams,
|
|
330
|
-
searchParams: Object.fromEntries(appVisibleSearch(url).params),
|
|
331
333
|
slotParams,
|
|
332
334
|
statusCode: status,
|
|
333
335
|
responseHeaders,
|
|
334
|
-
headHtml: flightInitScript(),
|
|
336
|
+
headHtml: flightInitScript(nonce),
|
|
335
337
|
bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
|
|
338
|
+
nonce,
|
|
336
339
|
rscStream: inlineStream,
|
|
337
340
|
signal: req.signal,
|
|
338
341
|
cookies: getCookiesForSsr(),
|
|
@@ -64,6 +64,7 @@ import { requestContextAls } from '../als-registry.ts';
|
|
|
64
64
|
import { capturePrebuiltPayloads } from '../prebuilt-builder.ts';
|
|
65
65
|
import { setPrebuiltPayloadSource } from '../prebuilt/payload-source.ts';
|
|
66
66
|
import { setRscFunctions, setComponentRefreshTimeout } from '../prebuilt-runtime.ts';
|
|
67
|
+
import { setKeyDisciplineStrict } from '../prebuilt/key-discipline.ts';
|
|
67
68
|
import { createAutoSitemapHandler } from '../sitemap-handler.ts';
|
|
68
69
|
import { generateStaticSite } from '../static-generator.ts';
|
|
69
70
|
|
|
@@ -505,6 +506,13 @@ if (renderTimeoutMs) {
|
|
|
505
506
|
setComponentRefreshTimeout(renderTimeoutMs);
|
|
506
507
|
}
|
|
507
508
|
|
|
509
|
+
const cacheConfig = (config as Record<string, unknown>).cache as
|
|
510
|
+
| { strictKeyDiscipline?: boolean }
|
|
511
|
+
| undefined;
|
|
512
|
+
if (cacheConfig?.strictKeyDiscipline === true) {
|
|
513
|
+
setKeyDisciplineStrict(true);
|
|
514
|
+
}
|
|
515
|
+
|
|
508
516
|
// Run instrumentation before the capture-mode guard — components may
|
|
509
517
|
// depend on globals or SDKs initialized in register().
|
|
510
518
|
await loadInstrumentation(loadUserInstrumentation);
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import type { ClientBootstrapConfig } from '../html-injectors.ts';
|
|
16
16
|
import { flightInitScript } from '../flight-scripts.ts';
|
|
17
|
+
import { getCspNonce, nonceAttr } from '../csp-nonce.ts';
|
|
17
18
|
import { appVisibleSearch } from '../../shared/rsc-cache-key.ts';
|
|
18
19
|
import { getSlotParamsForClient } from '../request-context.ts';
|
|
19
20
|
import type { LayoutEntry } from '../deny-renderer.ts';
|
|
@@ -179,10 +180,15 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
179
180
|
// `getSegmentParams(slotPath)` from.
|
|
180
181
|
const slotParams = getSlotParamsForClient();
|
|
181
182
|
|
|
183
|
+
// Middleware has already run; if it read `ctx.nonce` for its CSP header,
|
|
184
|
+
// that is the value every inline script must wear.
|
|
185
|
+
const nonce = getCspNonce();
|
|
186
|
+
|
|
187
|
+
const reqUrl = new URL(req.url);
|
|
182
188
|
const navContext: NavContext = {
|
|
183
|
-
pathname:
|
|
189
|
+
pathname: reqUrl.pathname,
|
|
190
|
+
search: appVisibleSearch(reqUrl).search,
|
|
184
191
|
segmentParams: match.segmentParams,
|
|
185
|
-
searchParams: Object.fromEntries(appVisibleSearch(new URL(req.url)).params),
|
|
186
192
|
slotParams,
|
|
187
193
|
statusCode: 200,
|
|
188
194
|
responseHeaders,
|
|
@@ -191,12 +197,13 @@ export async function renderSsrResponse(opts: SsrRenderOptions): Promise<Respons
|
|
|
191
197
|
clientBootstrap.preloadLinks +
|
|
192
198
|
// Initialize __timber_f in <head> so it exists before any streaming
|
|
193
199
|
// chunk scripts arrive in <body>. See flight-scripts.ts, LOCAL-415.
|
|
194
|
-
(clientJsDisabled ? '' : flightInitScript()),
|
|
200
|
+
(clientJsDisabled ? '' : flightInitScript(nonce)),
|
|
195
201
|
getHeadSegmentScript: clientJsDisabled
|
|
196
202
|
? undefined
|
|
197
203
|
: () =>
|
|
198
|
-
`<script>self.__timber_segments=${htmlEscapeJsonString(JSON.stringify(buildSegmentInfo(segments, layoutComponents, opts.slotSkipInfo, undefined, getFirstDeniedSegmentIndex())))}</script>`,
|
|
204
|
+
`<script${nonceAttr(nonce)}>self.__timber_segments=${htmlEscapeJsonString(JSON.stringify(buildSegmentInfo(segments, layoutComponents, opts.slotSkipInfo, undefined, getFirstDeniedSegmentIndex())))}</script>`,
|
|
199
205
|
bootstrapScriptContent: clientBootstrap.bootstrapScriptContent,
|
|
206
|
+
nonce,
|
|
200
207
|
// Skip RSC inline stream when client JS is disabled — no client to hydrate.
|
|
201
208
|
rscStream: clientJsDisabled ? undefined : inlineStream,
|
|
202
209
|
deferSuspenseFor: deferSuspenseFor > 0 ? deferSuspenseFor : undefined,
|
|
@@ -41,10 +41,20 @@ import type { SlotParamsRecord } from '../shared/slot-params.ts';
|
|
|
41
41
|
export interface NavContext {
|
|
42
42
|
/** The requested pathname */
|
|
43
43
|
pathname: string;
|
|
44
|
+
/**
|
|
45
|
+
* The app-visible raw query string (`''` or `?…`), with the framework's
|
|
46
|
+
* `_rsc` cache key stripped — from `appVisibleSearch()`. The single
|
|
47
|
+
* representation of the request's search state on this contract: it feeds
|
|
48
|
+
* `NavigationProvider`, the nuqs SSR adapter, and the ALS-backed SSR data,
|
|
49
|
+
* so `useSearchParams()`, nuqs hooks, and `<Link preserveSearchParams>`
|
|
50
|
+
* read the identical value they will read from the address bar after
|
|
51
|
+
* hydration. The raw string preserves repeated keys (`?tag=a&tag=b`),
|
|
52
|
+
* which a collapsed `Record<string, string>` cannot represent — do not
|
|
53
|
+
* reintroduce one beside this field (TIM-1428).
|
|
54
|
+
*/
|
|
55
|
+
search: string;
|
|
44
56
|
/** Extracted route segment params. Post-coercion, values may be non-string (TIM-1347). */
|
|
45
57
|
segmentParams: CoercedParams;
|
|
46
|
-
/** Search params from the URL */
|
|
47
|
-
searchParams: Record<string, string>;
|
|
48
58
|
/**
|
|
49
59
|
* Per-slot segment params, keyed by slot tree path. Absent when the request
|
|
50
60
|
* rendered no slot with params of its own. Feeds `useSegmentParams(path)`
|
|
@@ -59,6 +69,16 @@ export interface NavContext {
|
|
|
59
69
|
headHtml: string;
|
|
60
70
|
/** Inline JS for React's bootstrapScriptContent — kicks off module loading */
|
|
61
71
|
bootstrapScriptContent: string;
|
|
72
|
+
/**
|
|
73
|
+
* Request CSP nonce, generated when middleware read `ctx.nonce` for its
|
|
74
|
+
* `Content-Security-Policy` header (`getCspNonce()`). SSR passes it to
|
|
75
|
+
* Fizz (`nonce` option → bootstrap + runtime scripts), to the Flight
|
|
76
|
+
* decoder (preinit scripts), and to every timber-generated inline
|
|
77
|
+
* `<script>`. Undefined when no middleware asked for one, in which case
|
|
78
|
+
* the markup is byte-identical to a nonce-less render.
|
|
79
|
+
* See design/13-security.md §"Content Security Policy" (TIM-1414).
|
|
80
|
+
*/
|
|
81
|
+
nonce?: string;
|
|
62
82
|
/** Tee'd RSC stream for client-side hydration (inlined into HTML) */
|
|
63
83
|
rscStream?: ReadableStream<Uint8Array>;
|
|
64
84
|
/** Max Suspense hold window (ms). SSR delays the first flush by this
|
package/src/server/ssr-entry.ts
CHANGED
|
@@ -39,9 +39,8 @@ import { SsrStreamError } from './primitives.ts';
|
|
|
39
39
|
import { createBufferedTransformStream, injectHead, injectRscPayload } from './html-injectors.ts';
|
|
40
40
|
import { wrapSsrElement } from './ssr-wrappers.tsx';
|
|
41
41
|
import { withSpan } from './tracing.ts';
|
|
42
|
-
import { setCurrentParams } from '../client/use-segment-params.ts';
|
|
43
42
|
import { registerSsrDataProvider, type SsrData } from '../client/ssr-data.ts';
|
|
44
|
-
import {
|
|
43
|
+
import { splitPayloadRoot } from '../shared/payload-root.ts';
|
|
45
44
|
import type { NavContext } from './ssr-bridge-types.ts';
|
|
46
45
|
|
|
47
46
|
// Pre-import Node.js stream modules at module load time — not per-request.
|
|
@@ -123,7 +122,7 @@ export async function handleSsr(
|
|
|
123
122
|
// useSearchParams, useCookie, useParams, etc.).
|
|
124
123
|
const ssrData: SsrData = {
|
|
125
124
|
pathname: navContext.pathname,
|
|
126
|
-
|
|
125
|
+
search: navContext.search,
|
|
127
126
|
cookies: navContext.cookies ?? new Map(),
|
|
128
127
|
params: navContext.segmentParams,
|
|
129
128
|
slotParams: navContext.slotParams,
|
|
@@ -136,12 +135,6 @@ export async function handleSsr(
|
|
|
136
135
|
// Client hooks read from getSsrData() which delegates to this
|
|
137
136
|
// ALS store via the registered provider.
|
|
138
137
|
return ssrDataAls.run(ssrData, async () => {
|
|
139
|
-
// Also set the module-level currentParams for useSegmentParams().
|
|
140
|
-
// useParams reads from getSsrData() during SSR (ALS-backed),
|
|
141
|
-
// but setCurrentParams is kept for the client-side path where
|
|
142
|
-
// the segment router updates params on navigation.
|
|
143
|
-
setCurrentParams(navContext.segmentParams);
|
|
144
|
-
|
|
145
138
|
// Decode the RSC stream into a React element tree.
|
|
146
139
|
// createFromReadableStream resolves client component references
|
|
147
140
|
// (from "use client" modules) using the SSR environment's module
|
|
@@ -152,30 +145,41 @@ export async function handleSsr(
|
|
|
152
145
|
// On Node.js: convert Web ReadableStream → Node Readable → createFromNodeStream
|
|
153
146
|
// (eliminates Promise-per-chunk overhead from Web Streams reader)
|
|
154
147
|
// On Workers: createFromReadableStream (Web Streams are V8-native C++ there)
|
|
148
|
+
//
|
|
149
|
+
// The request CSP nonce goes to the Flight decoder too: Flight emits
|
|
150
|
+
// `<script>` preinits for client-reference modules, and those need
|
|
151
|
+
// the nonce as much as timber's own inline tags do (TIM-1414).
|
|
152
|
+
const nonce = navContext.nonce;
|
|
155
153
|
let root: unknown;
|
|
156
154
|
if (hasNodeStreamDecode && _nodeStreamImports) {
|
|
157
155
|
const nodeRscStream = _nodeStreamImports.ReadableFromWeb(
|
|
158
156
|
rscStream as import('stream/web').ReadableStream
|
|
159
157
|
);
|
|
160
|
-
root = createFromNodeStream(nodeRscStream);
|
|
158
|
+
root = createFromNodeStream(nodeRscStream, { nonce });
|
|
161
159
|
} else {
|
|
162
|
-
root = createFromReadableStream(rscStream);
|
|
160
|
+
root = createFromReadableStream(rscStream, { nonce });
|
|
163
161
|
}
|
|
164
162
|
// The payload root is `{ tree, params, slotParams }` — params ride
|
|
165
|
-
// beside the tree rather than in a provider around it (TIM-1297).
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
163
|
+
// beside the tree rather than in a provider around it (TIM-1297).
|
|
164
|
+
// splitPayloadRoot is the same split the browser performs: the tree
|
|
165
|
+
// renders, and the params half feeds PayloadRoot inside the wrapper
|
|
166
|
+
// chain so `useSegmentParams()` reads the identical derivation on
|
|
167
|
+
// both sides (TIM-1424).
|
|
168
|
+
const { tree, params } = splitPayloadRoot(root);
|
|
169
|
+
const element = tree as React.ReactNode;
|
|
170
170
|
const _decodeEnd = performance.now();
|
|
171
171
|
|
|
172
|
-
// Wrap with the same component tree
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// hydration mismatches in libraries like Radix UI. See TIM-532.
|
|
172
|
+
// Wrap with the same component tree the client hydrates —
|
|
173
|
+
// real NavigationProvider and PayloadRoot, not no-op mirrors —
|
|
174
|
+
// so hooks resolve identically and useId() matches on both sides.
|
|
175
|
+
// See TIM-532, TIM-1424.
|
|
177
176
|
const hasTopLoader = _runtimeConfig.topLoader?.enabled !== false;
|
|
178
|
-
const wrappedElement = wrapSsrElement(element,
|
|
177
|
+
const wrappedElement = wrapSsrElement(element, {
|
|
178
|
+
pathname: navContext.pathname,
|
|
179
|
+
search: navContext.search,
|
|
180
|
+
paramsSource: params,
|
|
181
|
+
hasTopLoader,
|
|
182
|
+
});
|
|
179
183
|
|
|
180
184
|
// Render to HTML stream (waits for onShellReady).
|
|
181
185
|
//
|
|
@@ -201,6 +205,7 @@ export async function handleSsr(
|
|
|
201
205
|
try {
|
|
202
206
|
nodeHtmlStream = await renderSsrNodeStream(wrappedElement, {
|
|
203
207
|
bootstrapScriptContent: navContext.bootstrapScriptContent || undefined,
|
|
208
|
+
nonce,
|
|
204
209
|
deferSuspenseFor: navContext.deferSuspenseFor,
|
|
205
210
|
signal: navContext.signal,
|
|
206
211
|
renderTimeoutMs,
|
|
@@ -240,13 +245,15 @@ export async function handleSsr(
|
|
|
240
245
|
const flightInjector = createNodeFlightInjector(navContext.rscStream, {
|
|
241
246
|
renderTimeoutMs,
|
|
242
247
|
errorSignal: navContext._errorSignal,
|
|
248
|
+
nonce,
|
|
243
249
|
});
|
|
244
250
|
const moveSuffix = createNodeMoveSuffixTransform();
|
|
245
251
|
|
|
246
252
|
const output = runNodePipelineWithErrorHandling(
|
|
247
253
|
nodeHtmlStream,
|
|
248
254
|
[bufferedTransform, headInjector, flightInjector, moveSuffix],
|
|
249
|
-
navContext.signal
|
|
255
|
+
navContext.signal,
|
|
256
|
+
nonce
|
|
250
257
|
);
|
|
251
258
|
const _pipelineEnd = performance.now();
|
|
252
259
|
|
|
@@ -271,6 +278,7 @@ export async function handleSsr(
|
|
|
271
278
|
try {
|
|
272
279
|
htmlStream = await renderSsrStream(wrappedElement, {
|
|
273
280
|
bootstrapScriptContent: navContext.bootstrapScriptContent || undefined,
|
|
281
|
+
nonce,
|
|
274
282
|
deferSuspenseFor: navContext.deferSuspenseFor,
|
|
275
283
|
signal: navContext.signal,
|
|
276
284
|
renderTimeoutMs,
|
|
@@ -300,12 +308,11 @@ export async function handleSsr(
|
|
|
300
308
|
const resolvedHeadHtml = navContext.headHtml + (navContext.getHeadSegmentScript?.() ?? '');
|
|
301
309
|
let outputStream = htmlStream.pipeThrough(createBufferedTransformStream());
|
|
302
310
|
outputStream = injectHead(outputStream, resolvedHeadHtml);
|
|
303
|
-
outputStream = injectRscPayload(
|
|
304
|
-
outputStream,
|
|
305
|
-
navContext.rscStream,
|
|
311
|
+
outputStream = injectRscPayload(outputStream, navContext.rscStream, {
|
|
306
312
|
renderTimeoutMs,
|
|
307
|
-
navContext._errorSignal
|
|
308
|
-
|
|
313
|
+
errorSignal: navContext._errorSignal,
|
|
314
|
+
nonce,
|
|
315
|
+
});
|
|
309
316
|
const _pipelineEnd = performance.now();
|
|
310
317
|
|
|
311
318
|
navContext._ssrTimings = {
|