@timber-js/app 0.2.0-alpha.185 → 0.2.0-alpha.187
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-O_LsyCE4.js → actions-35jnMdeJ.js} +4 -3
- package/dist/_chunks/{actions-O_LsyCE4.js.map → actions-35jnMdeJ.js.map} +1 -1
- package/dist/_chunks/als-registry-C6kcfprT.js +41 -0
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -0
- package/dist/_chunks/als-slots-BEEIPKYm.js +26 -0
- package/dist/_chunks/als-slots-BEEIPKYm.js.map +1 -0
- package/dist/_chunks/base-path-DaQrzbez.js +80 -0
- package/dist/_chunks/base-path-DaQrzbez.js.map +1 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js +86 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
- package/dist/_chunks/build-output-helper-C3DrfzZR.js +369 -0
- package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +1 -0
- package/dist/_chunks/{cache-api-B-lhk9p4.js → cache-api-DjNrIWRR.js} +8 -14
- package/dist/_chunks/cache-api-DjNrIWRR.js.map +1 -0
- package/dist/_chunks/cli-check-CpmN7Nh-.js +256 -0
- package/dist/_chunks/cli-check-CpmN7Nh-.js.map +1 -0
- package/dist/_chunks/cli-schema-sync-CGMp_Psg.js +298 -0
- package/dist/_chunks/cli-schema-sync-CGMp_Psg.js.map +1 -0
- package/dist/_chunks/{cloudflare-CnT5Lr7U.js → cloudflare-CGP6BZKO.js} +92 -14
- package/dist/_chunks/{cloudflare-CnT5Lr7U.js.map → cloudflare-CGP6BZKO.js.map} +1 -1
- package/dist/_chunks/convention-lint-kXsgc_-7.js +784 -0
- package/dist/_chunks/convention-lint-kXsgc_-7.js.map +1 -0
- package/dist/_chunks/error-boundary-D-ODYX41.js +353 -0
- package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -0
- package/dist/_chunks/{fast-hash-D6hIVt1Y.js → fast-hash-C-tVYrLb.js} +6 -1
- package/dist/_chunks/fast-hash-C-tVYrLb.js.map +1 -0
- package/dist/_chunks/file-cache-DmX7OqZP.js +454 -0
- package/dist/_chunks/file-cache-DmX7OqZP.js.map +1 -0
- package/dist/_chunks/fs-identity-D7vb6GdI.js +208 -0
- package/dist/_chunks/fs-identity-D7vb6GdI.js.map +1 -0
- package/dist/_chunks/{logger-AWfuX-KJ.js → logger-D8xJZXIN.js} +34 -51
- package/dist/_chunks/logger-D8xJZXIN.js.map +1 -0
- package/dist/_chunks/mdx-file-C005ay-P.js +25 -0
- package/dist/_chunks/mdx-file-C005ay-P.js.map +1 -0
- package/dist/_chunks/{plugin-context---kTF5v8.js → plugin-context-rCinWLiE.js} +8 -2
- package/dist/_chunks/plugin-context-rCinWLiE.js.map +1 -0
- package/dist/_chunks/purge-store-Byr8XOjU.js +14 -0
- package/dist/_chunks/purge-store-Byr8XOjU.js.map +1 -0
- package/dist/_chunks/{resolve-schema-Dz3fcFUo.js → resolve-schema-5ma5pp1b.js} +2 -2
- package/dist/_chunks/{resolve-schema-Dz3fcFUo.js.map → resolve-schema-5ma5pp1b.js.map} +1 -1
- package/dist/_chunks/{router-ref-CtmF-aPv.js → router-ref-DuYuV_0Q.js} +2 -2
- package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -0
- package/dist/_chunks/rsc-cache-key-DD0fl_-s.js +243 -0
- package/dist/_chunks/rsc-cache-key-DD0fl_-s.js.map +1 -0
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +40 -0
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +1 -0
- package/dist/_chunks/rsc-media-type-DRqE_lD_.js +46 -0
- package/dist/_chunks/rsc-media-type-DRqE_lD_.js.map +1 -0
- package/dist/_chunks/rsc-payload-path-B_LBodc2.js +48 -0
- package/dist/_chunks/rsc-payload-path-B_LBodc2.js.map +1 -0
- package/dist/_chunks/scanner-C8b0Gcw3.js +1564 -0
- package/dist/_chunks/scanner-C8b0Gcw3.js.map +1 -0
- package/dist/_chunks/schema-bridge-Cc2Gngu1.js +199 -0
- package/dist/_chunks/schema-bridge-Cc2Gngu1.js.map +1 -0
- package/dist/_chunks/{segment-classify-Byy425ng.js → segment-classify-C539Pa2O.js} +23 -2
- package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -0
- package/dist/_chunks/segment-context-CjOlyB8Y.js +97 -0
- package/dist/_chunks/segment-context-CjOlyB8Y.js.map +1 -0
- package/dist/_chunks/{canonicalize-DQHyFClh.js → segment-keys-BawYuNFO.js} +61 -2
- package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -0
- package/dist/_chunks/slot-params-BCTmZkQB.js +76 -0
- package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -0
- package/dist/_chunks/{ssr-data-BOWsq18U.js → ssr-data-14MXm7Pj.js} +17 -2
- package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -0
- package/dist/_chunks/{use-query-states-DFvWd-EA.js → use-query-states-BbU5Ge1V.js} +74 -26
- package/dist/_chunks/use-query-states-BbU5Ge1V.js.map +1 -0
- package/dist/_chunks/use-segment-params-C4r4BD9T.js +398 -0
- package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -0
- package/dist/_chunks/walkers-RzN6AFjr.js +141 -0
- package/dist/_chunks/walkers-RzN6AFjr.js.map +1 -0
- package/dist/adapters/build-output-helper.d.ts +37 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/compress-module.d.ts +12 -0
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/fs-identity.d.ts +64 -0
- package/dist/adapters/fs-identity.d.ts.map +1 -0
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +56 -5
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +57 -1
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/types.d.ts +13 -1
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cache/fast-hash.d.ts +5 -0
- package/dist/cache/fast-hash.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cdn/cloudflare-purge.js +30 -0
- package/dist/cdn/cloudflare-purge.js.map +1 -0
- package/dist/cdn/fastly-purge.js +33 -0
- package/dist/cdn/fastly-purge.js.map +1 -0
- package/dist/cdn/index.js +94 -0
- package/dist/cdn/index.js.map +1 -0
- package/dist/cdn/workers-cache-purge.js +35 -0
- package/dist/cdn/workers-cache-purge.js.map +1 -0
- package/dist/cli-check.d.ts +153 -0
- package/dist/cli-check.d.ts.map +1 -0
- package/dist/cli.d.ts +34 -8
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +46 -21
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts +20 -9
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/index.d.ts +1 -1
- package/dist/client/browser-entry/index.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts +4 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +3 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/browser-entry/rsc-stream.d.ts +8 -0
- package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts +7 -0
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -162
- package/dist/client/history.d.ts +13 -6
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +42 -41
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +4 -3
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +877 -311
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts +1 -7
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-api.d.ts +2 -2
- package/dist/client/navigation-commit.d.ts +87 -0
- package/dist/client/navigation-commit.d.ts.map +1 -0
- package/dist/client/navigation-context.d.ts +37 -20
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +63 -23
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/params-context.d.ts +81 -0
- package/dist/client/params-context.d.ts.map +1 -0
- package/dist/client/router-effects.d.ts +129 -0
- package/dist/client/router-effects.d.ts.map +1 -0
- package/dist/client/router-lifecycle.d.ts +55 -0
- package/dist/client/router-lifecycle.d.ts.map +1 -0
- package/dist/client/router-pipeline.d.ts +84 -0
- package/dist/client/router-pipeline.d.ts.map +1 -0
- package/dist/client/router-ref.d.ts +1 -1
- package/dist/client/router-ref.d.ts.map +1 -1
- package/dist/client/router-skew.d.ts +21 -0
- package/dist/client/router-skew.d.ts.map +1 -0
- package/dist/client/router-types.d.ts +207 -0
- package/dist/client/router-types.d.ts.map +1 -0
- package/dist/client/router.d.ts +1 -182
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +47 -26
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +87 -34
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/ssr-data.d.ts +6 -0
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/stale-client.d.ts +69 -0
- package/dist/client/stale-client.d.ts.map +1 -0
- package/dist/client/state.d.ts +10 -1
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts +2 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/unload-guard.d.ts +5 -0
- package/dist/client/unload-guard.d.ts.map +1 -1
- package/dist/client/use-cookie.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts +9 -3
- package/dist/client/use-query-states.d.ts.map +1 -1
- package/dist/client/use-router.d.ts +1 -1
- package/dist/client/use-segment-params.d.ts +18 -8
- package/dist/client/use-segment-params.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +5 -5
- package/dist/cookies/index.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1009 -759
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- package/dist/plugin-context.d.ts +46 -0
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-manifest.d.ts +1 -1
- package/dist/plugins/build-manifest.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/request-dep/analysis.d.ts +106 -0
- package/dist/plugins/request-dep/analysis.d.ts.map +1 -0
- package/dist/plugins/request-dep/index.d.ts +75 -0
- package/dist/plugins/request-dep/index.d.ts.map +1 -0
- package/dist/plugins/request-dep/origin.d.ts +61 -0
- package/dist/plugins/request-dep/origin.d.ts.map +1 -0
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +160 -8
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/codegen-shared.d.ts +3 -44
- package/dist/routing/codegen-shared.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +28 -31
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts +51 -0
- package/dist/routing/codegen-write.d.ts.map +1 -0
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/collision-probe.d.ts +104 -0
- package/dist/routing/collision-probe.d.ts.map +1 -0
- package/dist/routing/collision-spaces.d.ts +26 -0
- package/dist/routing/collision-spaces.d.ts.map +1 -0
- package/dist/routing/convention-lint.d.ts +18 -4
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/export-detect.d.ts +16 -0
- package/dist/routing/export-detect.d.ts.map +1 -1
- package/dist/routing/index.js +4 -3
- package/dist/routing/interception.d.ts +235 -6
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/routing/link-codegen.d.ts +19 -4
- package/dist/routing/link-codegen.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts +1 -7
- package/dist/routing/manifest-codegen.d.ts.map +1 -1
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/segment-classify.d.ts +33 -0
- package/dist/routing/segment-classify.d.ts.map +1 -1
- package/dist/routing/segment-keys.d.ts +95 -0
- package/dist/routing/segment-keys.d.ts.map +1 -0
- package/dist/routing/slot-placement.d.ts +48 -0
- package/dist/routing/slot-placement.d.ts.map +1 -0
- package/dist/routing/types.d.ts +0 -6
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/routing/walkers.d.ts +6 -0
- package/dist/routing/walkers.d.ts.map +1 -1
- package/dist/schema-bridge.d.ts +60 -9
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +62 -14
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.d.ts +0 -1
- package/dist/search-params/index.d.ts.map +1 -1
- package/dist/search-params/index.js +221 -10
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/parse-total.d.ts +70 -0
- package/dist/search-params/parse-total.d.ts.map +1 -0
- package/dist/search-params/wrappers.d.ts +26 -3
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/server/access-gate.d.ts +19 -8
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +6 -3
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +18 -1
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +13 -1
- package/dist/server/build-manifest.d.ts.map +1 -1
- package/dist/server/chain-url-parts.d.ts +82 -0
- package/dist/server/chain-url-parts.d.ts.map +1 -0
- package/dist/server/children-interception.d.ts +49 -0
- package/dist/server/children-interception.d.ts.map +1 -0
- package/dist/server/compress.d.ts.map +1 -1
- package/dist/server/default-status-page.d.ts +45 -0
- package/dist/server/default-status-page.d.ts.map +1 -0
- package/dist/server/deny-boundary.d.ts +148 -15
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +15 -4
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +85 -15
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/head-response.d.ts +35 -0
- package/dist/server/head-response.d.ts.map +1 -0
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +3 -1
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +712 -418
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-collector.d.ts +52 -0
- package/dist/server/metadata-collector.d.ts.map +1 -0
- package/dist/server/param-coercion.d.ts +22 -6
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-helpers.d.ts +0 -18
- package/dist/server/pipeline-helpers.d.ts.map +1 -1
- package/dist/server/pipeline-interception.d.ts +59 -9
- package/dist/server/pipeline-interception.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +11 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +23 -0
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/publish-params.d.ts +33 -0
- package/dist/server/publish-params.d.ts.map +1 -0
- package/dist/server/request-context.d.ts +48 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +22 -3
- 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 +13 -0
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-cache-key-guard.d.ts +78 -0
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
- package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
- package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts +15 -3
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +2 -17
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +0 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +1 -2
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +15 -2
- 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/skippable-prefix.d.ts +23 -0
- package/dist/server/skippable-prefix.d.ts.map +1 -0
- package/dist/server/slot-interception.d.ts +80 -0
- package/dist/server/slot-interception.d.ts.map +1 -0
- package/dist/server/slot-resolver.d.ts +70 -8
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +17 -0
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts +0 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts +24 -13
- package/dist/server/ssr-wrappers.d.ts.map +1 -1
- package/dist/server/state-tree-diff.d.ts +6 -27
- package/dist/server/state-tree-diff.d.ts.map +1 -1
- package/dist/server/static-generator.d.ts +6 -0
- package/dist/server/static-generator.d.ts.map +1 -1
- package/dist/server/static-not-found.d.ts +52 -0
- package/dist/server/static-not-found.d.ts.map +1 -0
- package/dist/server/status-code-resolver.d.ts +8 -1
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts +28 -36
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/server/types.d.ts +12 -7
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/utils/element-type.d.ts +40 -0
- package/dist/server/utils/element-type.d.ts.map +1 -0
- package/dist/shared/als-slots.d.ts +41 -0
- package/dist/shared/als-slots.d.ts.map +1 -0
- package/dist/shared/base-path.d.ts +56 -0
- package/dist/shared/base-path.d.ts.map +1 -0
- package/dist/shared/param-value.d.ts +80 -0
- package/dist/shared/param-value.d.ts.map +1 -0
- package/dist/shared/payload-root.d.ts +108 -0
- package/dist/shared/payload-root.d.ts.map +1 -0
- package/dist/shared/rsc-cache-key.d.ts +163 -0
- package/dist/shared/rsc-cache-key.d.ts.map +1 -0
- package/dist/shared/rsc-error-envelope.d.ts +25 -0
- package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
- package/dist/shared/rsc-manifest.d.ts +28 -0
- package/dist/shared/rsc-manifest.d.ts.map +1 -0
- package/dist/shared/rsc-media-type.d.ts +40 -0
- package/dist/shared/rsc-media-type.d.ts.map +1 -0
- package/dist/shared/rsc-payload-path.d.ts +42 -0
- package/dist/shared/rsc-payload-path.d.ts.map +1 -0
- package/dist/shared/segment-info.d.ts +59 -0
- package/dist/shared/segment-info.d.ts.map +1 -0
- package/dist/shared/slot-params.d.ts +51 -0
- package/dist/shared/slot-params.d.ts.map +1 -0
- package/dist/shared/static-platform-files.d.ts +71 -0
- package/dist/shared/static-platform-files.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +1 -1
- package/docs/api/31-api-client.mdx +4 -2
- package/docs/api/33-api-search-params.mdx +38 -16
- package/docs/api/35-api-typescript.mdx +3 -3
- package/docs/api/36-cli.mdx +34 -7
- package/docs/learn/00-introduction.mdx +1 -1
- package/docs/learn/02-pages-and-layouts.mdx +2 -2
- package/docs/learn/05-typed-params.mdx +8 -8
- package/docs/learn/07-typed-routes.mdx +12 -7
- package/docs/learn/11-error-handling.mdx +18 -0
- package/docs/learn/12-client-navigation.mdx +2 -2
- package/docs/learn/14-deploying.mdx +23 -0
- package/docs/more/01-advanced-routing.mdx +173 -3
- package/docs/more/03-coming-from-nextjs.mdx +2 -2
- package/docs/more/04-metadata-and-fonts.mdx +1 -1
- package/docs/more/50-ai-agent-instructions.mdx +6 -4
- package/package.json +9 -6
- package/src/adapters/build-output-helper.ts +116 -3
- package/src/adapters/cloudflare.ts +90 -12
- package/src/adapters/compress-module.ts +79 -1
- package/src/adapters/fs-identity.ts +156 -0
- package/src/adapters/nitro.ts +6 -6
- package/src/adapters/shared.ts +196 -21
- package/src/adapters/types.ts +13 -1
- package/src/cache/fast-hash.ts +5 -0
- package/src/cli-check.ts +458 -0
- package/src/cli.ts +59 -24
- package/src/client/browser-entry/action-dispatch.ts +11 -5
- package/src/client/browser-entry/hydrate.ts +40 -29
- package/src/client/browser-entry/index.ts +15 -6
- package/src/client/browser-entry/post-hydration.ts +10 -9
- package/src/client/browser-entry/router-init.ts +104 -48
- package/src/client/browser-entry/rsc-stream.ts +11 -2
- package/src/client/error-boundary.tsx +85 -16
- package/src/client/history.ts +13 -6
- package/src/client/index.ts +14 -3
- package/src/client/internal.ts +4 -3
- package/src/client/link.tsx +120 -72
- package/src/client/navigation-api.ts +3 -3
- package/src/client/navigation-commit.ts +197 -0
- package/src/client/navigation-context.ts +40 -67
- package/src/client/navigation-root.tsx +326 -101
- package/src/client/params-context.ts +192 -0
- package/src/client/router-effects.ts +231 -0
- package/src/client/router-lifecycle.ts +288 -0
- package/src/client/router-pipeline.ts +393 -0
- package/src/client/router-ref.ts +1 -1
- package/src/client/router-skew.ts +30 -0
- package/src/client/router-types.ts +230 -0
- package/src/client/router.ts +243 -707
- package/src/client/rsc-fetch.ts +183 -106
- package/src/client/segment-cache.ts +129 -44
- package/src/client/ssr-data.ts +6 -0
- package/src/client/stale-client.ts +191 -0
- package/src/client/state.ts +14 -1
- package/src/client/top-loader.tsx +2 -1
- package/src/client/unload-guard.ts +34 -2
- package/src/client/use-cookie.ts +9 -2
- package/src/client/use-query-states.ts +102 -39
- package/src/client/use-router.ts +1 -1
- package/src/client/use-segment-params.ts +41 -18
- package/src/cookies/define-cookie.ts +6 -1
- package/src/index.ts +25 -4
- package/src/plugin-context.ts +58 -0
- package/src/plugins/adapter-build.ts +4 -0
- package/src/plugins/build-manifest.ts +34 -13
- package/src/plugins/entries.ts +4 -0
- package/src/plugins/fonts.ts +11 -8
- package/src/plugins/request-dep/analysis.ts +447 -0
- package/src/plugins/request-dep/index.ts +376 -0
- package/src/plugins/request-dep/origin.ts +310 -0
- package/src/plugins/routing.ts +97 -102
- package/src/plugins/shims.ts +0 -1
- package/src/plugins/static-build.ts +391 -95
- package/src/routing/codegen-shared.ts +3 -79
- package/src/routing/codegen-types.ts +28 -31
- package/src/routing/codegen-write.ts +139 -0
- package/src/routing/codegen.ts +86 -172
- package/src/routing/collision-probe.ts +438 -0
- package/src/routing/collision-spaces.ts +281 -0
- package/src/routing/convention-lint.ts +139 -40
- package/src/routing/export-detect.ts +151 -7
- package/src/routing/interception.ts +896 -50
- package/src/routing/link-codegen.ts +32 -65
- package/src/routing/manifest-codegen.ts +6 -62
- package/src/routing/scanner.ts +117 -92
- package/src/routing/segment-classify.ts +57 -0
- package/src/routing/segment-keys.ts +146 -0
- package/src/routing/slot-placement.ts +97 -0
- package/src/routing/types.ts +0 -6
- package/src/routing/walkers.ts +8 -0
- package/src/schema-bridge.ts +180 -58
- package/src/search-params/define.ts +108 -66
- package/src/search-params/index.ts +0 -1
- package/src/search-params/parse-total.ts +78 -0
- package/src/search-params/wrappers.ts +60 -11
- package/src/server/access-gate.tsx +60 -40
- package/src/server/action-handler.ts +4 -5
- package/src/server/actions.ts +6 -3
- package/src/server/als-registry.ts +18 -1
- package/src/server/build-manifest.ts +14 -1
- package/src/server/chain-url-parts.ts +163 -0
- package/src/server/children-interception.ts +115 -0
- package/src/server/compress.ts +9 -1
- package/src/server/default-status-page.ts +127 -0
- package/src/server/deny-boundary.ts +269 -41
- package/src/server/deny-renderer.ts +111 -39
- package/src/server/error-boundary-wrapper.ts +166 -79
- package/src/server/head-response.ts +54 -0
- package/src/server/index.ts +1 -3
- package/src/server/internal.ts +2 -2
- package/src/server/metadata-collector.ts +115 -0
- package/src/server/param-coercion.ts +51 -69
- package/src/server/pipeline-helpers.ts +2 -47
- package/src/server/pipeline-interception.ts +81 -32
- package/src/server/pipeline-outcome.ts +4 -5
- package/src/server/pipeline-phases.ts +77 -25
- package/src/server/pipeline.ts +57 -10
- package/src/server/prebuilt-runtime.ts +4 -4
- package/src/server/primitives.ts +25 -0
- package/src/server/publish-params.ts +42 -0
- package/src/server/request-context.ts +97 -18
- package/src/server/route-element-builder.ts +241 -253
- package/src/server/route-handler.ts +8 -8
- package/src/server/route-matcher.ts +21 -9
- package/src/server/rsc-cache-key-guard.ts +164 -0
- package/src/server/rsc-entry/api-handler.ts +6 -8
- package/src/server/rsc-entry/deny-fallback.ts +3 -2
- package/src/server/rsc-entry/error-renderer.ts +94 -35
- package/src/server/rsc-entry/helpers.ts +33 -28
- package/src/server/rsc-entry/index.ts +36 -33
- package/src/server/rsc-entry/render-route.ts +18 -10
- package/src/server/rsc-entry/revalidate-renderer.ts +3 -2
- package/src/server/rsc-entry/rsc-payload.ts +113 -40
- package/src/server/rsc-entry/rsc-stream.ts +29 -5
- package/src/server/rsc-entry/ssr-renderer.ts +38 -22
- package/src/server/skippable-prefix.ts +92 -0
- package/src/server/slot-interception.ts +168 -0
- package/src/server/slot-resolver.ts +263 -158
- package/src/server/ssr-bridge-types.ts +18 -0
- package/src/server/ssr-entry.ts +20 -10
- package/src/server/ssr-wrappers.tsx +33 -32
- package/src/server/state-tree-diff.ts +14 -55
- package/src/server/static-generator.ts +68 -29
- package/src/server/static-not-found.ts +134 -0
- package/src/server/status-code-resolver.ts +28 -11
- package/src/server/tree-builder.ts +35 -218
- package/src/server/types.ts +12 -7
- package/src/server/utils/element-type.ts +72 -0
- package/src/shared/als-slots.ts +72 -0
- package/src/shared/base-path.ts +80 -0
- package/src/shared/param-value.ts +244 -0
- package/src/shared/payload-root.ts +151 -0
- package/src/shared/rsc-cache-key.ts +269 -0
- package/src/shared/rsc-error-envelope.ts +31 -0
- package/src/shared/rsc-manifest.ts +29 -0
- package/src/shared/rsc-media-type.ts +43 -0
- package/src/shared/rsc-payload-path.ts +45 -0
- package/src/shared/segment-info.ts +60 -0
- package/src/shared/slot-params.ts +111 -0
- package/src/shared/static-platform-files.ts +75 -0
- package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
- package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
- package/dist/_chunks/cache-api-B-lhk9p4.js.map +0 -1
- package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-BTWEKJXo.js +0 -668
- package/dist/_chunks/cli-schema-sync-BTWEKJXo.js.map +0 -1
- package/dist/_chunks/define-COtkxMRT.js +0 -191
- package/dist/_chunks/define-COtkxMRT.js.map +0 -1
- package/dist/_chunks/define-c4au4I9R.js +0 -82
- package/dist/_chunks/define-c4au4I9R.js.map +0 -1
- package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
- package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
- package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
- package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
- package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
- package/dist/_chunks/registry-DbJPKoBp.js +0 -20
- package/dist/_chunks/registry-DbJPKoBp.js.map +0 -1
- package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
- package/dist/_chunks/schema-bridge-DT_Tn0Xf.js +0 -119
- package/dist/_chunks/schema-bridge-DT_Tn0Xf.js.map +0 -1
- package/dist/_chunks/segment-classify-Byy425ng.js.map +0 -1
- package/dist/_chunks/segment-context-ZDnXDkbz.js +0 -34
- package/dist/_chunks/segment-context-ZDnXDkbz.js.map +0 -1
- package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
- package/dist/_chunks/use-query-states-DFvWd-EA.js.map +0 -1
- package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
- package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
- package/dist/_chunks/walkers-_6zKFlch.js +0 -1002
- package/dist/_chunks/walkers-_6zKFlch.js.map +0 -1
- package/dist/client/error-boundary.js.map +0 -1
- package/dist/plugins/request-dep.d.ts +0 -32
- package/dist/plugins/request-dep.d.ts.map +0 -1
- package/dist/search-params/registry.d.ts +0 -20
- package/dist/search-params/registry.d.ts.map +0 -1
- package/dist/segment-params/define.d.ts +0 -89
- package/dist/segment-params/define.d.ts.map +0 -1
- package/dist/segment-params/index.d.ts +0 -3
- package/dist/segment-params/index.d.ts.map +0 -1
- package/dist/segment-params/index.js +0 -2
- package/src/plugins/request-dep.ts +0 -499
- package/src/search-params/registry.ts +0 -31
- package/src/segment-params/define.ts +0 -249
- package/src/segment-params/index.ts +0 -9
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical form of a segment param value.
|
|
3
|
+
*
|
|
4
|
+
* `getSegmentParams()` on the server and `useSegmentParams()` on the client
|
|
5
|
+
* must return the same value — same prototype, same own properties. This
|
|
6
|
+
* module is how that is guaranteed, and the mechanism matters:
|
|
7
|
+
*
|
|
8
|
+
* **Both sides are images of one normalizer.** A codec's output is deep-cloned
|
|
9
|
+
* into a closed canonical domain at coercion time, and *that clone* is what the
|
|
10
|
+
* server returns and what goes on the wire. Server and client agree by
|
|
11
|
+
* construction, because neither holds the codec's original.
|
|
12
|
+
*
|
|
13
|
+
* That replaces predicting what React Flight would do to an arbitrary value.
|
|
14
|
+
* Prediction is what the previous design attempted and it cannot be finished:
|
|
15
|
+
* the question "will Flight's decode output be observably identical to this
|
|
16
|
+
* input?" ranges over every way a JS object can carry state that is not in its
|
|
17
|
+
* serialization — own keys, enumerability, symbols, holes, accessors,
|
|
18
|
+
* subclassing, exotic objects — and a `Proxy` defeats it outright, since a
|
|
19
|
+
* lying `getPrototypeOf` trap makes any static check unsound. Twelve review
|
|
20
|
+
* findings on PR #992 were each one more case of that set, and the set does
|
|
21
|
+
* not close.
|
|
22
|
+
*
|
|
23
|
+
* Normalizing closes it. The only remaining question is whether Flight
|
|
24
|
+
* round-trips the normalizer's *output range* — a finite domain we defined,
|
|
25
|
+
* covered by `tests/e2e/segment-params.test.ts` against the real serializer.
|
|
26
|
+
*
|
|
27
|
+
* ## The domain
|
|
28
|
+
*
|
|
29
|
+
* - `undefined`, `null`, `string`, `number`, `boolean`, `bigint`
|
|
30
|
+
* - null-prototype objects, own enumerable string keys only
|
|
31
|
+
* - dense arrays
|
|
32
|
+
* - `Date`, `Map`, `Set` — rebuilt, never the codec's instance
|
|
33
|
+
*
|
|
34
|
+
* ...and it is a **tree**: no value appears twice, and nothing is cyclic.
|
|
35
|
+
* That is not a simplification, it is the last prediction being removed.
|
|
36
|
+
* Flight encodes a `Date` by value at each occurrence but tracks plain objects
|
|
37
|
+
* for reference dedup, so preserving aliasing would carry it across for some
|
|
38
|
+
* types and drop it for others — and knowing which is exactly the kind of
|
|
39
|
+
* guess this design exists to stop making (codex, PR #992). A tree has no
|
|
40
|
+
* sharing to preserve or lose, so what Flight does about it is unobservable.
|
|
41
|
+
*
|
|
42
|
+
* Anything else throws, naming the param and what it was. Note what is *not*
|
|
43
|
+
* a rejection: a `Date` subclass normalizes to a `Date`, a decorated array to
|
|
44
|
+
* a plain one, a sparse array to a dense one, a getter to the value it
|
|
45
|
+
* returned. Those used to be twelve separate checks. They are not divergences
|
|
46
|
+
* any more, because the normalization happens once and both sides see its
|
|
47
|
+
* result — so they need no checks at all.
|
|
48
|
+
*
|
|
49
|
+
* Own properties that Flight would drop (non-enumerable, symbol-keyed) are
|
|
50
|
+
* dropped here instead, symmetrically. Prototype pollution is handled by the
|
|
51
|
+
* same pass: `__proto__` is never copied, and every object is
|
|
52
|
+
* `Object.create(null)` (design/13-security.md #36b/#36c, TIM-873).
|
|
53
|
+
*
|
|
54
|
+
* ## The prototype flip
|
|
55
|
+
*
|
|
56
|
+
* Flight refuses to serialize a null-prototype object, so the canonical form
|
|
57
|
+
* cannot go on the wire as-is. `toPlainRecord` and `toNullProtoRecord` flip
|
|
58
|
+
* the prototype on the way out and back. They are exact inverses and both
|
|
59
|
+
* total, because they only ever see canonical values — there is nothing to
|
|
60
|
+
* validate and no shape to special-case.
|
|
61
|
+
*
|
|
62
|
+
* See design/41-global-params.md §"Transport".
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/** Types the canonical form rebuilds rather than rejects. */
|
|
66
|
+
const REBUILT = ['Date', 'Map', 'Set'] as const;
|
|
67
|
+
|
|
68
|
+
/** The domain, for error messages. Derived, never a second hand-written list. */
|
|
69
|
+
const DOMAIN = `a string, number, boolean, bigint, null, undefined, a plain object, an array, or ${REBUILT.join('/')}`;
|
|
70
|
+
|
|
71
|
+
function reject(path: string, was: string, hint: string): never {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`[timber] Segment param "${path || '(root)'}" was coerced to ${was}, which cannot ` +
|
|
74
|
+
`cross to the client. A param value may be ${DOMAIN}.\n` +
|
|
75
|
+
` ${hint}\n` +
|
|
76
|
+
` See design/41-global-params.md §"Transport".`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function describe(value: object): string {
|
|
81
|
+
return (value as { constructor?: { name?: string } }).constructor?.name ?? 'an object';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Deep-clone a codec's output into the canonical form, or throw.
|
|
86
|
+
*
|
|
87
|
+
* `seen` accumulates every object the walk has entered, and a second encounter
|
|
88
|
+
* is rejected. That is one rule for two things — a shared reference and a cycle
|
|
89
|
+
* are both "this object again" — and it is what keeps the result a tree.
|
|
90
|
+
*/
|
|
91
|
+
export function normalizeParamValue(value: unknown, path = ''): unknown {
|
|
92
|
+
return normalize(value, path, new Set());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalize(value: unknown, path: string, seen: Set<object>): unknown {
|
|
96
|
+
if (value === null) return null;
|
|
97
|
+
|
|
98
|
+
const kind = typeof value;
|
|
99
|
+
if (kind === 'string' || kind === 'number' || kind === 'boolean' || kind === 'bigint') {
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
if (kind === 'undefined') return undefined;
|
|
103
|
+
if (kind === 'function') {
|
|
104
|
+
reject(path, 'a function', 'Return the data it would produce, not the function.');
|
|
105
|
+
}
|
|
106
|
+
if (kind === 'symbol') {
|
|
107
|
+
reject(path, 'a symbol', 'Return a string instead — a symbol has no wire representation.');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const object = value as object;
|
|
111
|
+
if (seen.has(object)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`[timber] Segment param "${path || '(root)'}" is a value that already appears ` +
|
|
114
|
+
`elsewhere in the same param — a shared reference or a cycle. A param value is a ` +
|
|
115
|
+
`tree: React Flight carries a repeated \`Date\` by value and a repeated object by ` +
|
|
116
|
+
`reference, so sharing would survive for some types and not others.\n` +
|
|
117
|
+
` Return separate values, or move the shared part outside the params.\n` +
|
|
118
|
+
` See design/41-global-params.md §"Transport".`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
seen.add(object);
|
|
122
|
+
|
|
123
|
+
if (Array.isArray(object)) {
|
|
124
|
+
// Read by index, so holes become `undefined` — on both sides, which is
|
|
125
|
+
// the point. A subclass and any own properties are left behind for the
|
|
126
|
+
// same reason: the clone is a plain, dense array either way.
|
|
127
|
+
const out: unknown[] = [];
|
|
128
|
+
for (let index = 0; index < object.length; index++) {
|
|
129
|
+
out.push(normalize((object as unknown[])[index], `${path}[${index}]`, seen));
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (object instanceof Date) {
|
|
135
|
+
// A fresh Date, so a subclass or an attached property cannot travel half
|
|
136
|
+
// way and be dropped by the wire.
|
|
137
|
+
return new Date(object.getTime());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (object instanceof Map) {
|
|
141
|
+
const out = new Map<unknown, unknown>();
|
|
142
|
+
for (const [key, entry] of object) {
|
|
143
|
+
out.set(
|
|
144
|
+
normalize(key, `${path}<key>`, seen),
|
|
145
|
+
normalize(entry, `${path}.get(${keyLabel(key)})`, seen)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (object instanceof Set) {
|
|
152
|
+
const out = new Set<unknown>();
|
|
153
|
+
let index = 0;
|
|
154
|
+
for (const entry of object) out.add(normalize(entry, `${path}[${index++}]`, seen));
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Everything else is judged by its prototype. A `Proxy` can lie here, and
|
|
159
|
+
// that is fine: whatever it answers, the clone below reads through it once
|
|
160
|
+
// and both sides see the same snapshot.
|
|
161
|
+
const prototype = Object.getPrototypeOf(object);
|
|
162
|
+
if (prototype !== null && prototype !== Object.prototype) {
|
|
163
|
+
reject(
|
|
164
|
+
path,
|
|
165
|
+
`a ${describe(object)} instance`,
|
|
166
|
+
'Return a plain object with the fields you need.'
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Own *enumerable string* keys only — the rest is what Flight would drop,
|
|
171
|
+
// so dropping it here keeps the two sides identical. `__proto__` is skipped
|
|
172
|
+
// rather than copied: it has a language-level setter that would change the
|
|
173
|
+
// prototype chain of the copy (TIM-655, TIM-855, TIM-873).
|
|
174
|
+
const out: Record<string, unknown> = Object.create(null);
|
|
175
|
+
for (const key of Object.keys(object)) {
|
|
176
|
+
if (key === '__proto__') continue;
|
|
177
|
+
out[key] = normalize(
|
|
178
|
+
(object as Record<string, unknown>)[key],
|
|
179
|
+
path ? `${path}.${key}` : key,
|
|
180
|
+
seen
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function keyLabel(key: unknown): string {
|
|
187
|
+
return typeof key === 'string' ? JSON.stringify(key) : String(key);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── Prototype flip ──────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Canonical form → wire form. React Flight rejects a null prototype
|
|
194
|
+
* ("Classes or null prototypes are not supported"), so objects cross as plain
|
|
195
|
+
* ones and are restored on arrival.
|
|
196
|
+
*/
|
|
197
|
+
export function toPlainRecord<T>(value: T): T {
|
|
198
|
+
return reproto(value, {}) as T;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Wire form → canonical form. The exact inverse of `toPlainRecord`. */
|
|
202
|
+
export function toNullProtoRecord<T>(value: T): T {
|
|
203
|
+
return reproto(value, null) as T;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Rebuild a canonical value with objects on `proto`.
|
|
208
|
+
*
|
|
209
|
+
* Total by construction: its input is always canonical, so the cases are the
|
|
210
|
+
* domain and nothing else. No validation, no shape it might not have seen, and
|
|
211
|
+
* no cycle to guard — a canonical value is a tree. That is what moving the
|
|
212
|
+
* checking to `normalizeParamValue` bought.
|
|
213
|
+
*/
|
|
214
|
+
function reproto(value: unknown, proto: object | null): unknown {
|
|
215
|
+
if (value === null || typeof value !== 'object') return value;
|
|
216
|
+
|
|
217
|
+
const object = value as object;
|
|
218
|
+
|
|
219
|
+
// A Date carries no nested values, so it crosses as itself.
|
|
220
|
+
if (object instanceof Date) return object;
|
|
221
|
+
|
|
222
|
+
if (Array.isArray(object)) {
|
|
223
|
+
return (object as unknown[]).map((item) => reproto(item, proto));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (object instanceof Map) {
|
|
227
|
+
const out = new Map<unknown, unknown>();
|
|
228
|
+
for (const [key, entry] of object) out.set(reproto(key, proto), reproto(entry, proto));
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (object instanceof Set) {
|
|
233
|
+
const out = new Set<unknown>();
|
|
234
|
+
for (const entry of object) out.add(reproto(entry, proto));
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const out: Record<string, unknown> = proto === null ? Object.create(null) : {};
|
|
239
|
+
for (const key of Object.keys(object)) {
|
|
240
|
+
if (key === '__proto__') continue;
|
|
241
|
+
out[key] = reproto((object as Record<string, unknown>)[key], proto);
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape of an RSC payload's root row.
|
|
3
|
+
*
|
|
4
|
+
* Every tree the server sends the browser is serialized as
|
|
5
|
+
* `{ tree, params, slotParams }` — the route's element tree, plus the coerced
|
|
6
|
+
* params it rendered with, as *siblings* rather than as a wrapper around the
|
|
7
|
+
* tree. See design/41-global-params.md §"Transport".
|
|
8
|
+
*
|
|
9
|
+
* Two reasons it is a sibling field rather than a provider inside the payload
|
|
10
|
+
* (TIM-1297, which is TIM-1294's mechanism corrected):
|
|
11
|
+
*
|
|
12
|
+
* 1. **A provider inside the payload cannot serve a partial navigation.** The
|
|
13
|
+
* client keeps the previous tree and splices the new payload in at the
|
|
14
|
+
* first updated `SegmentOutlet`, so the payload's own provider lands
|
|
15
|
+
* *below* the retained region — and the retained region's root is the
|
|
16
|
+
* *departing* route's provider, which therefore wins for every reader in a
|
|
17
|
+
* skipped layout. Wrapping something above it does not help: context
|
|
18
|
+
* resolves to the nearest provider, and the departing one is nearer.
|
|
19
|
+
* Params belong to the *navigation*, so the provider has to be owned by the
|
|
20
|
+
* client, above the merge point, and there must be exactly one.
|
|
21
|
+
*
|
|
22
|
+
* 2. **Element props are not readable at the instant the root resolves.**
|
|
23
|
+
* Flight outlines a React element's props and fills the record in after the
|
|
24
|
+
* root row resolves, so a reader that unblocks on the root sees `{}` — and
|
|
25
|
+
* since the object is filled *in place*, its identity never changes and any
|
|
26
|
+
* memo keyed on it caches the empty snapshot permanently. A plain model
|
|
27
|
+
* field has no such window: the root chunk stays blocked until everything
|
|
28
|
+
* it references has resolved.
|
|
29
|
+
*
|
|
30
|
+
* This module is the single declaration of that shape. The server builds roots
|
|
31
|
+
* through `server/publish-params.ts` and the client reads them through the
|
|
32
|
+
* helpers here — producer and consumer do not each describe the payload.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { SlotParamsRecord } from './slot-params.js';
|
|
36
|
+
|
|
37
|
+
/** The params published beside a tree. */
|
|
38
|
+
export interface PublishedParams {
|
|
39
|
+
/** The main route's coerced params. */
|
|
40
|
+
params: Record<string, string | string[]>;
|
|
41
|
+
/** Per-slot params, or null when the route rendered no slots with params. */
|
|
42
|
+
slotParams: SlotParamsRecord | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Params ready to publish: settled, or a promise the root provider suspends on.
|
|
47
|
+
*
|
|
48
|
+
* Both forms exist on purpose. The hydration path cannot await — `hydrateRoot`
|
|
49
|
+
* has to be called before the inline Flight stream has necessarily produced
|
|
50
|
+
* the root row — so it hands over the promise and suspends exactly where React
|
|
51
|
+
* was already going to suspend on the tree. Every other path settles it inside
|
|
52
|
+
* the navigation transition first, which is what keeps a decode failure inside
|
|
53
|
+
* the app's own error boundaries instead of above them (TIM-1297).
|
|
54
|
+
*/
|
|
55
|
+
export type ParamsSource = PublishedParams | Promise<PublishedParams>;
|
|
56
|
+
|
|
57
|
+
/** The root row of an RSC payload. */
|
|
58
|
+
export interface RscPayloadRoot extends PublishedParams {
|
|
59
|
+
/** The route's element tree. */
|
|
60
|
+
tree: unknown;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* What a reader gets when the value it was handed is not a payload root.
|
|
65
|
+
*
|
|
66
|
+
* In the browser this is unreachable for a real response: all four producers
|
|
67
|
+
* of a route payload go through `withPublishedParams`, and a client talking to
|
|
68
|
+
* a different build is answered with 204 at Stage 1c
|
|
69
|
+
* (`server/pipeline-phases.ts`) before any payload exists. It is reachable in
|
|
70
|
+
* the router's test/fallback path, where `decodeRsc` is absent and the
|
|
71
|
+
* "payload" is the raw response text.
|
|
72
|
+
*/
|
|
73
|
+
export const NO_PUBLISHED_PARAMS: PublishedParams = { params: {}, slotParams: null };
|
|
74
|
+
|
|
75
|
+
/** True when `value` carries published params — a root, or a read of one. */
|
|
76
|
+
function hasPublishedParams(value: unknown): value is PublishedParams {
|
|
77
|
+
return (
|
|
78
|
+
typeof value === 'object' &&
|
|
79
|
+
value !== null &&
|
|
80
|
+
'params' in value &&
|
|
81
|
+
typeof (value as PublishedParams).params === 'object' &&
|
|
82
|
+
(value as PublishedParams).params !== null
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Read the params published beside a tree.
|
|
88
|
+
*
|
|
89
|
+
* Accepts a payload root *or* a `PublishedParams` already split off one, so
|
|
90
|
+
* the client can pre-resolve on the navigation path and still hand the same
|
|
91
|
+
* value to the same provider. Never throws.
|
|
92
|
+
*/
|
|
93
|
+
export function readPublishedParams(source: unknown): PublishedParams {
|
|
94
|
+
if (!hasPublishedParams(source)) return NO_PUBLISHED_PARAMS;
|
|
95
|
+
return { params: source.params, slotParams: source.slotParams ?? null };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Read the renderable tree out of a payload root.
|
|
100
|
+
*
|
|
101
|
+
* Returns the value unchanged when it is not a root — the router's fallback
|
|
102
|
+
* path stores raw response text under the same name, and a test asserting on
|
|
103
|
+
* that text should see the text.
|
|
104
|
+
*/
|
|
105
|
+
export function readPayloadTree(root: unknown): unknown {
|
|
106
|
+
if (typeof root === 'object' && root !== null && 'tree' in root && hasPublishedParams(root)) {
|
|
107
|
+
return (root as RscPayloadRoot).tree;
|
|
108
|
+
}
|
|
109
|
+
return root;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A decoded payload, split into the two things the client does with it. */
|
|
113
|
+
export interface SplitPayload {
|
|
114
|
+
/** The renderable tree — an element, or a thenable React suspends on. */
|
|
115
|
+
tree: unknown;
|
|
116
|
+
/**
|
|
117
|
+
* The params published beside it. Settled when the root already is, so the
|
|
118
|
+
* hydration path can render without an extra await.
|
|
119
|
+
*
|
|
120
|
+
* **Never rejects.** A decode failure has to surface where React renders
|
|
121
|
+
* `tree`, so the error boundary *around the tree* catches it. Rejecting here
|
|
122
|
+
* too would take down the provider above every boundary in the app, turning
|
|
123
|
+
* a contained failure into a blank page (TIM-1297).
|
|
124
|
+
*/
|
|
125
|
+
params: ParamsSource;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Split a decoded payload root into its tree and its params.
|
|
130
|
+
*
|
|
131
|
+
* Accepts a settled root or a thenable of one, and preserves which it was: a
|
|
132
|
+
* settled root splits synchronously, so no code path gains a suspend point it
|
|
133
|
+
* did not already have.
|
|
134
|
+
*/
|
|
135
|
+
export function splitPayloadRoot(root: unknown): SplitPayload {
|
|
136
|
+
if (
|
|
137
|
+
typeof root !== 'object' ||
|
|
138
|
+
root === null ||
|
|
139
|
+
typeof (root as PromiseLike<unknown>).then !== 'function'
|
|
140
|
+
) {
|
|
141
|
+
return { tree: readPayloadTree(root), params: readPublishedParams(root) };
|
|
142
|
+
}
|
|
143
|
+
const settled = root as PromiseLike<unknown>;
|
|
144
|
+
const tree = Promise.resolve(settled).then(readPayloadTree);
|
|
145
|
+
// The rejection still reaches React through its own read of `tree`; this
|
|
146
|
+
// only marks it handled for the case where nothing renders it (a partial
|
|
147
|
+
// navigation whose segment update finds no matching outlet).
|
|
148
|
+
tree.catch(() => {});
|
|
149
|
+
const params = Promise.resolve(settled).then(readPublishedParams, () => NO_PUBLISHED_PARAMS);
|
|
150
|
+
return { tree, params };
|
|
151
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `_rsc` payload cache key — derived identically on both sides.
|
|
3
|
+
*
|
|
4
|
+
* The client puts this in the payload URL so the document and its Flight
|
|
5
|
+
* payload occupy different cache keys (TIM-1268). The server recomputes it
|
|
6
|
+
* from the received headers so a request cannot claim one client's key while
|
|
7
|
+
* sending another client's headers — see `server/rsc-cache-key-guard.ts`.
|
|
8
|
+
* Both sides must agree exactly, which is why the derivation lives here and
|
|
9
|
+
* not in either one.
|
|
10
|
+
*
|
|
11
|
+
* Isomorphic: no server or client imports. The browser bundle audit
|
|
12
|
+
* (`tests/client-bundle-audit.test.ts`) enforces the client half of that.
|
|
13
|
+
*
|
|
14
|
+
* See design/19-client-navigation.md §"The `_rsc` cache key".
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Digest length in hex characters. 128 bits: the origin guard compares a
|
|
19
|
+
* caller-supplied key against a recomputed one, so the relevant bound is a
|
|
20
|
+
* birthday attack (an attacker who can influence a victim's `X-Timber-URL`
|
|
21
|
+
* gets to search both sides), not second preimage. 64 bits would fall to
|
|
22
|
+
* ~2^32 work; 128 puts it at 2^64.
|
|
23
|
+
*/
|
|
24
|
+
const KEY_HEX_LENGTH = 32;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Marks a value as a framework-issued key, and versions the derivation.
|
|
28
|
+
*
|
|
29
|
+
* Without it, an application value that happened to be 32 lowercase hex — an
|
|
30
|
+
* MD5, a token — is indistinguishable from a forged claim: both are "the
|
|
31
|
+
* right shape, the wrong digest", and no information in the value separates
|
|
32
|
+
* them. So the exemption cannot key on shape alone.
|
|
33
|
+
*
|
|
34
|
+
* The prefix resolves that because it is anchored to the address the
|
|
35
|
+
* victim's own client uses. A payload lives at `?_rsc=1.<digest>`, so
|
|
36
|
+
* anything aimed at that cache entry must carry the prefix and therefore
|
|
37
|
+
* gets validated; anything without it cannot be addressing a payload URL at
|
|
38
|
+
* all, and is exempt as application data. An attacker cannot both hit the
|
|
39
|
+
* victim's cache key and dodge the check.
|
|
40
|
+
*
|
|
41
|
+
* The version component also gives the derivation a migration path: the
|
|
42
|
+
* client and the origin must agree byte for byte, so a future change to the
|
|
43
|
+
* canonical form can be rolled out by bumping this instead of a flag day.
|
|
44
|
+
*/
|
|
45
|
+
const KEY_PREFIX = '1.';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The request headers that vary an RSC payload response, in canonical
|
|
49
|
+
* casing — this list is also emitted verbatim as the response's `Vary`
|
|
50
|
+
* tokens (`server/pipeline.ts`), since "what varies the response" is one
|
|
51
|
+
* question with one answer, not two lists that happen to agree.
|
|
52
|
+
*
|
|
53
|
+
* This list is the contract, and it is enforced rather than trusted:
|
|
54
|
+
* `tests/rsc-cache-key.test.ts` asserts that the header names
|
|
55
|
+
* `buildRscHeaders()` can emit are exactly this set. A header added to the
|
|
56
|
+
* request without being added here fails that test, because a varying input
|
|
57
|
+
* outside the key is precisely the cache-poisoning hazard the key exists to
|
|
58
|
+
* close.
|
|
59
|
+
*
|
|
60
|
+
* **No credentials, session identifiers, or user-supplied content may be
|
|
61
|
+
* added.** Every value here is hashed into a URL, and URLs are logged by
|
|
62
|
+
* proxies, CDNs, and origin access logs. `fnv1aHash` is a cache-key hash,
|
|
63
|
+
* not a KDF — a low-entropy secret would be recoverable by brute force.
|
|
64
|
+
* Authentication travels on cookies, which never enter the key.
|
|
65
|
+
*/
|
|
66
|
+
export const RSC_KEY_HEADERS = [
|
|
67
|
+
'Accept',
|
|
68
|
+
'X-Timber-State-Tree',
|
|
69
|
+
'X-Timber-URL',
|
|
70
|
+
'X-Timber-Deployment-Id',
|
|
71
|
+
] as const;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether a value could be a key this module produced.
|
|
75
|
+
*
|
|
76
|
+
* `_rsc` is not a reserved parameter name — an application may already use
|
|
77
|
+
* it for its own search state, including for a value that happens to look
|
|
78
|
+
* like a digest. Anything without the `KEY_PREFIX` is therefore application
|
|
79
|
+
* data, not a claim on a payload cache key, and the origin guard leaves it
|
|
80
|
+
* alone rather than penalizing the response.
|
|
81
|
+
*
|
|
82
|
+
* Derived from `KEY_PREFIX` and `KEY_HEX_LENGTH` rather than spelled again,
|
|
83
|
+
* so the shape cannot drift from the digest that has to satisfy it.
|
|
84
|
+
*
|
|
85
|
+
* Ignoring a malformed value costs no protection: clients only ever send
|
|
86
|
+
* well-formed keys, so a URL bearing a malformed one is an address no
|
|
87
|
+
* legitimate request will ever ask for, and nothing can be poisoned at an
|
|
88
|
+
* address nobody reads.
|
|
89
|
+
*/
|
|
90
|
+
export function isRscCacheKeyShape(value: string): boolean {
|
|
91
|
+
if (!value.startsWith(KEY_PREFIX)) return false;
|
|
92
|
+
const digest = value.slice(KEY_PREFIX.length);
|
|
93
|
+
return digest.length === KEY_HEX_LENGTH && /^[0-9a-f]+$/.test(digest);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The query parameter carrying the payload cache key.
|
|
98
|
+
*
|
|
99
|
+
* Spelled here rather than at each of the four places that read or write it
|
|
100
|
+
* — the client that appends it, the origin guard that validates it, and the
|
|
101
|
+
* strip below — because it is a wire contract between two independent
|
|
102
|
+
* implementations, exactly like the digest itself.
|
|
103
|
+
*/
|
|
104
|
+
export const RSC_KEY_PARAM = '_rsc';
|
|
105
|
+
|
|
106
|
+
// ─── Keeping the key out of application state ────────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Decode one raw `name` or `value` the way `URLSearchParams` does:
|
|
110
|
+
* `application/x-www-form-urlencoded`, so `+` is a space.
|
|
111
|
+
*
|
|
112
|
+
* Malformed percent-escapes throw in `decodeURIComponent`. `URLSearchParams`
|
|
113
|
+
* leaves those bytes as-is rather than throwing, and so do we — a pair we
|
|
114
|
+
* cannot decode is by definition not the framework's, so it is kept.
|
|
115
|
+
*/
|
|
116
|
+
function formDecode(raw: string): string {
|
|
117
|
+
try {
|
|
118
|
+
return decodeURIComponent(raw.replace(/\+/g, ' '));
|
|
119
|
+
} catch {
|
|
120
|
+
return raw;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Remove framework-issued `_rsc` values from a raw query string.
|
|
126
|
+
*
|
|
127
|
+
* Only values matching `isRscCacheKeyShape` are removed. `_rsc` is not a
|
|
128
|
+
* reserved name (see that function), so `/search?_rsc=foo` is application
|
|
129
|
+
* state and survives — the same exemption the origin guard applies, derived
|
|
130
|
+
* from the same predicate so the two cannot disagree about what "ours"
|
|
131
|
+
* means.
|
|
132
|
+
*
|
|
133
|
+
* Operates on the raw string rather than round-tripping through
|
|
134
|
+
* `URLSearchParams`, so every surviving pair keeps its original bytes.
|
|
135
|
+
* Re-serializing would rewrite `?b` as `?b=` and `?a=%7E` as `?a=~` on RSC
|
|
136
|
+
* navigations only, which is the same class of inconsistency this function
|
|
137
|
+
* exists to remove.
|
|
138
|
+
*
|
|
139
|
+
* Matching decodes the name, because `?%5Frsc=<key>` is a `_rsc` parameter
|
|
140
|
+
* as far as `URLSearchParams` is concerned — the same reasoning as the
|
|
141
|
+
* guard's fast path.
|
|
142
|
+
*
|
|
143
|
+
* @param search - `url.search`, with or without the leading `?`.
|
|
144
|
+
* @returns The query string including a leading `?`, or `''` if empty.
|
|
145
|
+
*/
|
|
146
|
+
export function stripRscCacheKey(search: string): string {
|
|
147
|
+
const query = search.startsWith('?') ? search.slice(1) : search;
|
|
148
|
+
if (query === '') return '';
|
|
149
|
+
const kept = query.split('&').filter((pair) => {
|
|
150
|
+
const eq = pair.indexOf('=');
|
|
151
|
+
if (formDecode(eq === -1 ? pair : pair.slice(0, eq)) !== RSC_KEY_PARAM) return true;
|
|
152
|
+
return !isRscCacheKeyShape(eq === -1 ? '' : formDecode(pair.slice(eq + 1)));
|
|
153
|
+
});
|
|
154
|
+
const result = kept.join('&');
|
|
155
|
+
return result === '' ? '' : `?${result}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A request's search state as application code should see it. */
|
|
159
|
+
export interface AppVisibleSearch {
|
|
160
|
+
/** Parsed params, with the framework's cache key removed. */
|
|
161
|
+
params: URLSearchParams;
|
|
162
|
+
/** The matching raw query string — `''` or `?…`. */
|
|
163
|
+
search: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The search state application code sees, for any request URL.
|
|
168
|
+
*
|
|
169
|
+
* The `_rsc` key rides on the payload URL of every RSC navigation and on no
|
|
170
|
+
* other request, so reading search params straight off `req.url` makes the
|
|
171
|
+
* same page observe different search state depending on whether it was
|
|
172
|
+
* reached by a full load or a client navigation — and lets the cache key
|
|
173
|
+
* leak into whatever the page builds from it: a canonical URL, a pagination
|
|
174
|
+
* link, an analytics payload (TIM-1272).
|
|
175
|
+
*
|
|
176
|
+
* Every application-facing derivation of search state goes through here.
|
|
177
|
+
* `params` and `search` are returned together, from one strip, because they
|
|
178
|
+
* are two views of one answer and were previously two expressions that had
|
|
179
|
+
* to agree.
|
|
180
|
+
*
|
|
181
|
+
* The raw request URL is deliberately untouched: `server/rsc-cache-key-guard.ts`
|
|
182
|
+
* still reads the parameter from `req.url` to bind the key to the request.
|
|
183
|
+
*/
|
|
184
|
+
export function appVisibleSearch(url: URL): AppVisibleSearch {
|
|
185
|
+
const search = stripRscCacheKey(url.search);
|
|
186
|
+
return { params: new URLSearchParams(search), search };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Reads a header value by name. Must be case-insensitive — `Headers.get()`
|
|
191
|
+
* already is, and `recordLookup` below makes a plain record so.
|
|
192
|
+
*/
|
|
193
|
+
export type HeaderLookup = (name: string) => string | null | undefined;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Compute the `_rsc` cache key for a request.
|
|
197
|
+
*
|
|
198
|
+
* Absent headers are omitted rather than sent as empty, so "header missing"
|
|
199
|
+
* and "header present but empty" produce different keys.
|
|
200
|
+
*
|
|
201
|
+
* Values are length-prefixed rather than delimiter-separated. Any delimiter
|
|
202
|
+
* can be forged by a value that contains it: with a `\0` separator, an
|
|
203
|
+
* `accept` of `a\nx-timber-url\0b` serializes identically to an `accept` of
|
|
204
|
+
* `a` alongside an `x-timber-url` of `b`, which would let one header claim
|
|
205
|
+
* another's key. HTTP forbids control characters in header values and both
|
|
206
|
+
* `fetch()` and the server runtime reject them, so this is defense in depth
|
|
207
|
+
* — but a length prefix is unambiguous for *any* content, which is a
|
|
208
|
+
* property worth having in a function whose whole job is to be agreed on by
|
|
209
|
+
* two independent implementations.
|
|
210
|
+
*
|
|
211
|
+
* SHA-256, truncated to 128 bits. A fast non-cryptographic hash is the
|
|
212
|
+
* obvious choice for a cache key and is the wrong one here: the origin
|
|
213
|
+
* *compares* this value against a recomputed one, which makes it a check.
|
|
214
|
+
* FNV-1a — used for timber's other cache keys — is a multiply-xor over a
|
|
215
|
+
* fixed field, so an attacker controlling a few bytes of any input (their
|
|
216
|
+
* own `X-Timber-URL`, say) can solve for a chosen digest rather than search
|
|
217
|
+
* for one, and forge a key belonging to somebody else.
|
|
218
|
+
*
|
|
219
|
+
* Returns `null` when no digest is available — `crypto.subtle` is absent in
|
|
220
|
+
* non-secure browsing contexts (plain `http://` on a LAN address). Callers
|
|
221
|
+
* must treat that as "no key", never as "key matches": the client omits the
|
|
222
|
+
* parameter and the server refuses to share-cache. That returns those
|
|
223
|
+
* clients to the pre-TIM-1268 posture, where `Vary` alone separates HTML
|
|
224
|
+
* from Flight — acceptable, because a non-secure origin has no shared-CDN
|
|
225
|
+
* caching story to protect in the first place.
|
|
226
|
+
*/
|
|
227
|
+
export async function rscCacheKey(lookup: HeaderLookup): Promise<string | null> {
|
|
228
|
+
const subtle = globalThis.crypto?.subtle;
|
|
229
|
+
if (!subtle) return null;
|
|
230
|
+
let canonical = '';
|
|
231
|
+
for (const name of RSC_KEY_HEADERS) {
|
|
232
|
+
const value = lookup(name);
|
|
233
|
+
if (value == null) continue;
|
|
234
|
+
// Lowercased in the canonical form so the two implementations cannot
|
|
235
|
+
// disagree over the casing of the constant they share.
|
|
236
|
+
canonical += `${name.toLowerCase()}:${value.length}:${value}`;
|
|
237
|
+
}
|
|
238
|
+
const digest = await subtle.digest('SHA-256', new TextEncoder().encode(canonical));
|
|
239
|
+
let hex = '';
|
|
240
|
+
for (const byte of new Uint8Array(digest, 0, KEY_HEX_LENGTH / 2)) {
|
|
241
|
+
hex += byte.toString(16).padStart(2, '0');
|
|
242
|
+
}
|
|
243
|
+
return KEY_PREFIX + hex;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* An unverifiable stand-in key, for the one case where the digest cannot be
|
|
248
|
+
* computed: `crypto.subtle` is absent in non-secure browsing contexts.
|
|
249
|
+
* `crypto.getRandomValues` is not gated that way, so it is available exactly
|
|
250
|
+
* where the digest is not.
|
|
251
|
+
*
|
|
252
|
+
* Carries `KEY_PREFIX` like any other key, so the origin reads it as a claim
|
|
253
|
+
* it cannot verify — and refuses to share-cache the response — rather than
|
|
254
|
+
* exempting it as application data. Lives here so the prefix has one home.
|
|
255
|
+
*/
|
|
256
|
+
export function randomRscCacheKey(): string {
|
|
257
|
+
const bytes = new Uint8Array(KEY_HEX_LENGTH / 2);
|
|
258
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
259
|
+
return KEY_PREFIX + [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Build a case-insensitive lookup over a plain header record. */
|
|
263
|
+
export function recordLookup(headers: Record<string, string>): HeaderLookup {
|
|
264
|
+
const lowered: Record<string, string> = {};
|
|
265
|
+
for (const name of Object.keys(headers)) {
|
|
266
|
+
lowered[name.toLowerCase()] = headers[name];
|
|
267
|
+
}
|
|
268
|
+
return (name) => lowered[name.toLowerCase()];
|
|
269
|
+
}
|