@rangojs/router 0.0.0-experimental.9c9afef3 → 0.0.0-experimental.a014d2b7
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/AGENTS.md +8 -0
- package/README.md +245 -49
- package/dist/bin/rango.js +440 -133
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3373 -1176
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +68 -14
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +224 -32
- package/skills/caching/SKILL.md +279 -17
- package/skills/composability/SKILL.md +27 -3
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +250 -30
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +87 -18
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +235 -9
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +771 -0
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +77 -7
- package/skills/prerender/SKILL.md +172 -125
- package/skills/rango/SKILL.md +251 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +70 -5
- package/skills/router-setup/SKILL.md +65 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +130 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/typesafety/SKILL.md +322 -29
- package/skills/use-cache/SKILL.md +57 -14
- package/skills/view-transitions/SKILL.md +337 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +1 -66
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +192 -150
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +131 -30
- package/src/browser/navigation-client.ts +186 -100
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +157 -74
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +165 -112
- package/src/browser/prefetch/cache.ts +205 -62
- package/src/browser/prefetch/fetch.ts +347 -39
- package/src/browser/prefetch/queue.ts +42 -8
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +102 -15
- package/src/browser/react/NavigationProvider.tsx +295 -119
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/deferred-handle-resolution.ts +75 -0
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +23 -69
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +32 -7
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +114 -24
- package/src/browser/scroll-restoration.ts +37 -22
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +222 -72
- package/src/browser/types.ts +102 -12
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +165 -36
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +125 -24
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +260 -94
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +224 -41
- package/src/cache/cache-scope.ts +188 -82
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +1989 -378
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +89 -21
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +10 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +119 -290
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +18 -6
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +62 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +99 -13
- package/src/index.ts +139 -19
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +461 -304
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +143 -69
- package/src/route-definition/index.ts +4 -2
- package/src/route-definition/redirect.ts +51 -10
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +37 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +52 -31
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +48 -24
- package/src/router/lazy-includes.ts +15 -52
- package/src/router/loader-resolution.ts +268 -56
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +40 -42
- package/src/router/match-api.ts +124 -204
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +170 -276
- package/src/router/match-middleware/cache-store.ts +64 -52
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +45 -14
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +87 -39
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -140
- package/src/router/middleware.ts +266 -169
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +132 -90
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +195 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +115 -35
- package/src/router/router-options.ts +172 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +162 -84
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +76 -39
- package/src/router/segment-resolution/revalidation.ts +351 -321
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +56 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +163 -59
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +157 -54
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +655 -529
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +122 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +131 -14
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +99 -189
- package/src/rsc/rsc-rendering.ts +109 -74
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +287 -115
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +29 -9
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +236 -202
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +224 -52
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +38 -46
- package/src/server/request-context.ts +401 -173
- package/src/ssr/index.tsx +24 -16
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +357 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +125 -71
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +40 -11
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +12 -7
- package/src/types/segments.ts +136 -15
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +68 -18
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +214 -132
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +57 -4
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +6 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +178 -5
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +553 -317
- package/src/vite/plugins/performance-tracks.ts +64 -170
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +39 -25
- package/src/vite/rango.ts +118 -114
- package/src/vite/router-discovery.ts +941 -142
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +81 -34
- package/src/vite/utils/shared-utils.ts +92 -42
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/debug-channel.ts +0 -93
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
package/src/rsc/origin-guard.ts
CHANGED
|
@@ -9,11 +9,31 @@
|
|
|
9
9
|
* navigations, bookmarks, and non-browser clients don't send Origin.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import type { RequestPlan } from "../router/request-classification.js";
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Request phase that triggered the origin check.
|
|
14
16
|
*/
|
|
15
17
|
export type OriginCheckPhase = "action" | "loader" | "pe-form";
|
|
16
18
|
|
|
19
|
+
// Exhaustive over RequestPlan modes so a new mode must be classified here (the
|
|
20
|
+
// security gate) instead of silently falling through to no origin check.
|
|
21
|
+
export const ORIGIN_CHECK_PHASE_BY_MODE: Record<
|
|
22
|
+
RequestPlan["mode"],
|
|
23
|
+
OriginCheckPhase | null
|
|
24
|
+
> = {
|
|
25
|
+
action: "action",
|
|
26
|
+
loader: "loader",
|
|
27
|
+
"pe-render": "pe-form",
|
|
28
|
+
"full-render": null,
|
|
29
|
+
"partial-render": null,
|
|
30
|
+
response: null,
|
|
31
|
+
redirect: null,
|
|
32
|
+
"version-mismatch": null,
|
|
33
|
+
// Terminal: handled before the origin guard (emits X-RSC-Reload, no execution).
|
|
34
|
+
"app-switch": null,
|
|
35
|
+
};
|
|
36
|
+
|
|
17
37
|
/**
|
|
18
38
|
* Context passed to the originCheck callback.
|
|
19
39
|
*/
|
|
@@ -49,11 +69,8 @@ export type OriginCheckConfig<TEnv = any> =
|
|
|
49
69
|
* Returns true to allow, false to reject.
|
|
50
70
|
*/
|
|
51
71
|
export function defaultOriginCheck(request: Request, url: URL): boolean {
|
|
52
|
-
// 1. Read Origin header (present on all cross-origin requests and
|
|
53
|
-
// same-origin POST/PUT/PATCH/DELETE in modern browsers)
|
|
54
72
|
let requestOrigin = request.headers.get("origin");
|
|
55
73
|
|
|
56
|
-
// 2. Fallback to Referer if Origin is absent (some proxies strip it)
|
|
57
74
|
if (!requestOrigin) {
|
|
58
75
|
const referer = request.headers.get("referer");
|
|
59
76
|
if (referer) {
|
|
@@ -65,23 +82,20 @@ export function defaultOriginCheck(request: Request, url: URL): boolean {
|
|
|
65
82
|
}
|
|
66
83
|
}
|
|
67
84
|
|
|
68
|
-
// 3. No Origin or Referer — allow (can't be browser-initiated CSRF)
|
|
69
85
|
if (!requestOrigin) return true;
|
|
70
86
|
|
|
71
|
-
// "null" origin comes from privacy-sensitive contexts (data: URLs,
|
|
72
|
-
// sandboxed iframes, cross-origin redirects). Reject it.
|
|
73
87
|
if (requestOrigin === "null") return false;
|
|
74
88
|
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
const
|
|
89
|
+
// An Origin/Referer is present, so this is a browser request worth checking.
|
|
90
|
+
// Establish the expected origin from the Host header only -- browsers always
|
|
91
|
+
// send Host alongside Origin (runtimes synthesize it from the HTTP/2
|
|
92
|
+
// :authority), so a missing Host here is anomalous. Fail closed rather than
|
|
93
|
+
// fall back to url.host (derived from the request line) when the trusted Host
|
|
94
|
+
// cannot be established.
|
|
95
|
+
const expectedHost = request.headers.get("host");
|
|
96
|
+
if (!expectedHost) return false;
|
|
82
97
|
|
|
83
|
-
|
|
84
|
-
const expectedOrigin = `${expectedProtocol}//${expectedHost}`;
|
|
98
|
+
const expectedOrigin = `${url.protocol}//${expectedHost}`;
|
|
85
99
|
|
|
86
100
|
return requestOrigin.toLowerCase() === expectedOrigin.toLowerCase();
|
|
87
101
|
}
|
|
@@ -116,14 +130,15 @@ export async function checkRequestOrigin<TEnv = any>(
|
|
|
116
130
|
// Disabled by explicit opt-out
|
|
117
131
|
if (config === false) return null;
|
|
118
132
|
|
|
119
|
-
// Default
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
// Default (true/undefined) becomes a callback returning boolean, so the
|
|
134
|
+
// Response|true|reject resolution below is written once.
|
|
135
|
+
const check: (
|
|
136
|
+
ctx: OriginCheckContext<TEnv>,
|
|
137
|
+
) => boolean | Response | Promise<boolean | Response> =
|
|
138
|
+
config === true || config === undefined
|
|
139
|
+
? () => defaultOriginCheck(request, url)
|
|
140
|
+
: config;
|
|
125
141
|
|
|
126
|
-
// Custom function — build context and call
|
|
127
142
|
const ctx: OriginCheckContext<TEnv> = {
|
|
128
143
|
request,
|
|
129
144
|
url,
|
|
@@ -133,9 +148,8 @@ export async function checkRequestOrigin<TEnv = any>(
|
|
|
133
148
|
defaultCheck: () => defaultOriginCheck(request, url),
|
|
134
149
|
};
|
|
135
150
|
|
|
136
|
-
const result = await
|
|
151
|
+
const result = await check(ctx);
|
|
137
152
|
|
|
138
153
|
if (result instanceof Response) return result;
|
|
139
|
-
|
|
140
|
-
return createForbiddenResponse(request);
|
|
154
|
+
return result === true ? null : createForbiddenResponse(request);
|
|
141
155
|
}
|
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
getRequestContext,
|
|
11
11
|
setRequestContextParams,
|
|
12
12
|
} from "../server/request-context.js";
|
|
13
13
|
import { getSSRSetup } from "./ssr-setup.js";
|
|
14
14
|
import type { MiddlewareFn } from "../router/middleware.js";
|
|
15
15
|
import { executeMiddleware } from "../router/middleware.js";
|
|
16
|
+
import { observePhase, PHASES } from "../router/instrument.js";
|
|
17
|
+
import { gateTransitions } from "./transition-gate.js";
|
|
16
18
|
import type { RscPayload, ReactFormState } from "./types.js";
|
|
17
19
|
import {
|
|
18
20
|
createResponseWithMergedHeaders,
|
|
@@ -44,7 +46,7 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
44
46
|
env: TEnv,
|
|
45
47
|
url: URL,
|
|
46
48
|
isAction: boolean,
|
|
47
|
-
handleStore: ReturnType<typeof
|
|
49
|
+
handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
|
|
48
50
|
nonce: string | undefined,
|
|
49
51
|
routeMwInfo?: PeRouteMiddlewareInfo,
|
|
50
52
|
): Promise<Response | null> {
|
|
@@ -115,6 +117,13 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
115
117
|
// Execute action and return HTML
|
|
116
118
|
let actionResult: unknown = undefined;
|
|
117
119
|
let reactFormState: ReactFormState | null = null;
|
|
120
|
+
// Status for the fall-through re-render after a boundaryless action error.
|
|
121
|
+
// When an action throws and NO error boundary matches, the PE path re-renders
|
|
122
|
+
// the page (below) the same way it would after a successful action. Without
|
|
123
|
+
// this the re-render serves HTTP 200, diverging from the JS path which serves
|
|
124
|
+
// 500 (server-action.ts sets actionStatus=500 for the same boundaryless error).
|
|
125
|
+
// 500 is carried only into the final HTML response, never the redirect branch.
|
|
126
|
+
let boundarylessErrorStatus: number | undefined;
|
|
118
127
|
|
|
119
128
|
if (isUseActionState) {
|
|
120
129
|
// Decode and extract action identity before execution so error
|
|
@@ -124,7 +133,11 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
124
133
|
const boundAction = await ctx.decodeAction(formData);
|
|
125
134
|
// React's custom .bind() preserves $$id on server references.
|
|
126
135
|
useActionStateId = (boundAction as { $$id?: string }).$$id ?? undefined;
|
|
127
|
-
|
|
136
|
+
// Meter the no-JS form action as the action phase, same as the JS path.
|
|
137
|
+
actionResult = await observePhase(
|
|
138
|
+
PHASES.action(useActionStateId ?? "useActionState"),
|
|
139
|
+
() => boundAction(),
|
|
140
|
+
);
|
|
128
141
|
} catch (error) {
|
|
129
142
|
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
130
143
|
const redirectResponse = extractRedirectResponse(error);
|
|
@@ -140,6 +153,7 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
140
153
|
handleStore,
|
|
141
154
|
nonce,
|
|
142
155
|
useActionStateId,
|
|
156
|
+
true, // an action ran and threw
|
|
143
157
|
);
|
|
144
158
|
if (errorHtml) return errorHtml;
|
|
145
159
|
|
|
@@ -151,10 +165,21 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
151
165
|
handledByBoundary: false,
|
|
152
166
|
});
|
|
153
167
|
console.error("[RSC] Progressive enhancement action error:", error);
|
|
168
|
+
// No boundary matched — the fall-through re-render must carry 500 to match
|
|
169
|
+
// the JS path's boundaryless-error status (server-action.ts).
|
|
170
|
+
boundarylessErrorStatus = 500;
|
|
154
171
|
}
|
|
155
172
|
} else if (isDirectAction && directActionId) {
|
|
156
173
|
const temporaryReferences = ctx.createTemporaryReferenceSet();
|
|
157
174
|
|
|
175
|
+
// INTENTIONAL JS/PE divergence (do NOT "fix" to match the JS reject path).
|
|
176
|
+
// On the JS path React Flight-encodes the action args, so decodeReply
|
|
177
|
+
// succeeds or a failure means a malformed body (rejected). On the no-JS PE
|
|
178
|
+
// path the browser submits a raw <form action={fn}> POST with NO encoded
|
|
179
|
+
// args, so decodeReply throws by design and the raw FormData IS the action
|
|
180
|
+
// argument (the React form-action convention: fn(formData)). Removing this
|
|
181
|
+
// fallback breaks every unbound no-JS form action (verified: it fails the
|
|
182
|
+
// progressive-enhancement dev+prod e2e suite). See #572 (decided: keep).
|
|
158
183
|
let args: unknown[] = [];
|
|
159
184
|
try {
|
|
160
185
|
args = await ctx.decodeReply(formData, { temporaryReferences });
|
|
@@ -164,7 +189,9 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
164
189
|
|
|
165
190
|
try {
|
|
166
191
|
const loadedAction = await ctx.loadServerAction(directActionId);
|
|
167
|
-
actionResult = await
|
|
192
|
+
actionResult = await observePhase(PHASES.action(directActionId), () =>
|
|
193
|
+
loadedAction.apply(null, args),
|
|
194
|
+
);
|
|
168
195
|
} catch (error) {
|
|
169
196
|
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
170
197
|
const redirectResponse = extractRedirectResponse(error);
|
|
@@ -180,6 +207,7 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
180
207
|
handleStore,
|
|
181
208
|
nonce,
|
|
182
209
|
directActionId,
|
|
210
|
+
true, // an action ran and threw
|
|
183
211
|
);
|
|
184
212
|
if (errorHtml) return errorHtml;
|
|
185
213
|
|
|
@@ -191,6 +219,9 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
191
219
|
handledByBoundary: false,
|
|
192
220
|
});
|
|
193
221
|
console.error("[RSC] Progressive enhancement action error:", error);
|
|
222
|
+
// No boundary matched — the fall-through re-render must carry 500 to match
|
|
223
|
+
// the JS path's boundaryless-error status (server-action.ts).
|
|
224
|
+
boundarylessErrorStatus = 500;
|
|
194
225
|
}
|
|
195
226
|
}
|
|
196
227
|
|
|
@@ -226,11 +257,29 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
226
257
|
// cookies set by route middleware are available during re-render — matching
|
|
227
258
|
// the behavior of JS-enabled requests.
|
|
228
259
|
const renderPage = async (): Promise<Response> => {
|
|
260
|
+
// Preserve the original POST request's headers (Authorization, Cookie,
|
|
261
|
+
// custom headers) so loaders that read request headers/cookies behave
|
|
262
|
+
// identically under PE and the JS action path. Drop body-framing headers
|
|
263
|
+
// from the bodyless GET and force the HTML accept.
|
|
264
|
+
const headers = new Headers(request.headers);
|
|
265
|
+
headers.delete("content-type");
|
|
266
|
+
headers.delete("content-length");
|
|
267
|
+
headers.delete("content-encoding");
|
|
268
|
+
headers.delete("transfer-encoding");
|
|
269
|
+
headers.set("accept", "text/html");
|
|
229
270
|
const renderRequest = new Request(url.toString(), {
|
|
230
271
|
method: "GET",
|
|
231
|
-
headers
|
|
272
|
+
headers,
|
|
232
273
|
});
|
|
233
274
|
|
|
275
|
+
// JS/PE parity: this is an action's revalidation render, so mark it BEFORE
|
|
276
|
+
// matching — a stale `foregroundOnAction` cache entry must re-execute in the
|
|
277
|
+
// foreground during the re-render, exactly as the JS path's
|
|
278
|
+
// revalidateAfterAction does. The transition({ when }) gate fields below are
|
|
279
|
+
// set post-match (the gate reads them after rendering); foregroundOnAction
|
|
280
|
+
// reads _inActionRevalidation during the match, so it must be set here.
|
|
281
|
+
getRequestContext()._inActionRevalidation = true;
|
|
282
|
+
|
|
234
283
|
const match = await ctx.router.match(renderRequest, { env });
|
|
235
284
|
|
|
236
285
|
if (match.redirect) {
|
|
@@ -240,26 +289,46 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
240
289
|
});
|
|
241
290
|
}
|
|
242
291
|
|
|
292
|
+
// Expose the no-JS action to the transition({ when }) gate. currentUrl/Params
|
|
293
|
+
// are absent on this full-render path (no navigation snapshot); useActionState
|
|
294
|
+
// ids are block-scoped, so only a direct action id is available here.
|
|
295
|
+
// actionUrl is the page the action was submitted from (this request's url).
|
|
296
|
+
const peReqCtx = getRequestContext();
|
|
297
|
+
peReqCtx._gateActionId = directActionId ?? undefined;
|
|
298
|
+
peReqCtx._gateActionUrl = new URL(url);
|
|
299
|
+
peReqCtx._gateActionResult = actionResult;
|
|
300
|
+
peReqCtx._gateFormData = formData;
|
|
301
|
+
|
|
243
302
|
const payload: RscPayload = {
|
|
244
303
|
metadata: {
|
|
245
304
|
pathname: url.pathname,
|
|
246
|
-
|
|
305
|
+
routerId: ctx.router.id,
|
|
306
|
+
basename: ctx.router.basename,
|
|
307
|
+
segments: gateTransitions(
|
|
308
|
+
match.segments,
|
|
309
|
+
getRequestContext(),
|
|
310
|
+
ctx.router.onError,
|
|
311
|
+
),
|
|
247
312
|
matched: match.matched,
|
|
248
313
|
diff: match.diff,
|
|
314
|
+
resolvedIds: match.resolvedIds,
|
|
315
|
+
params: match.params,
|
|
249
316
|
isPartial: false,
|
|
250
317
|
rootLayout: ctx.router.rootLayout,
|
|
251
318
|
handles: handleStore.stream(),
|
|
252
319
|
version: ctx.version,
|
|
320
|
+
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
253
321
|
themeConfig: ctx.router.themeConfig,
|
|
254
322
|
warmupEnabled: ctx.router.warmupEnabled,
|
|
255
|
-
|
|
323
|
+
strictMode: ctx.router.strictMode,
|
|
324
|
+
initialTheme: getRequestContext().theme,
|
|
256
325
|
},
|
|
257
|
-
formState: actionResult,
|
|
258
326
|
};
|
|
259
327
|
|
|
260
|
-
const debugChannel = requireRequestContext()._debugChannel;
|
|
261
328
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
262
|
-
|
|
329
|
+
onError: (error: unknown) => {
|
|
330
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
331
|
+
},
|
|
263
332
|
});
|
|
264
333
|
// metricsStore=undefined is safe: the handler already stashed the early
|
|
265
334
|
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
@@ -271,6 +340,8 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
271
340
|
url,
|
|
272
341
|
undefined,
|
|
273
342
|
);
|
|
343
|
+
// reactFormState carries the useActionState payload via the SSR-option path
|
|
344
|
+
// (renderToReadableStream({ formState })); it does NOT travel on RscPayload.
|
|
274
345
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
275
346
|
formState: reactFormState,
|
|
276
347
|
nonce,
|
|
@@ -278,6 +349,13 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
278
349
|
});
|
|
279
350
|
|
|
280
351
|
return createResponseWithMergedHeaders(htmlStream, {
|
|
352
|
+
// boundarylessErrorStatus is set only when the action threw and no error
|
|
353
|
+
// boundary matched; it makes the re-render carry 500 like the JS path.
|
|
354
|
+
// The redirect branch above returns before this, so a redirect re-render
|
|
355
|
+
// keeps its 308 and is never overridden.
|
|
356
|
+
...(boundarylessErrorStatus !== undefined
|
|
357
|
+
? { status: boundarylessErrorStatus }
|
|
358
|
+
: {}),
|
|
281
359
|
headers: { "content-type": "text/html;charset=utf-8" },
|
|
282
360
|
});
|
|
283
361
|
};
|
|
@@ -312,10 +390,25 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
312
390
|
env: TEnv,
|
|
313
391
|
url: URL,
|
|
314
392
|
error: unknown,
|
|
315
|
-
handleStore: ReturnType<typeof
|
|
393
|
+
handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
|
|
316
394
|
nonce: string | undefined,
|
|
317
395
|
actionId?: string | null,
|
|
396
|
+
// True when an action actually ran and threw (vs a malformed form body, where
|
|
397
|
+
// no action executed). Drives _inActionRevalidation for JS/PE parity — it must
|
|
398
|
+
// NOT be inferred from actionId, since a useActionState bound action can run
|
|
399
|
+
// and throw with no $$id (actionId === undefined) yet still be an action error.
|
|
400
|
+
actionRan = false,
|
|
318
401
|
): Promise<Response | null> {
|
|
402
|
+
// JS/PE parity for an action-triggered error re-render: a stale
|
|
403
|
+
// `foregroundOnAction` cache entry inside the error boundary must foreground
|
|
404
|
+
// too, exactly as the JS path (revalidateAfterAction sets this unconditionally
|
|
405
|
+
// before rendering the error boundary). Set BEFORE matchError (the cached fn
|
|
406
|
+
// runs during it). Gated on actionRan, NOT actionId — a malformed form body
|
|
407
|
+
// (actionRan=false) ran no action and must keep SWR.
|
|
408
|
+
if (actionRan) {
|
|
409
|
+
getRequestContext()._inActionRevalidation = true;
|
|
410
|
+
}
|
|
411
|
+
|
|
319
412
|
let errorResult;
|
|
320
413
|
try {
|
|
321
414
|
errorResult = await ctx.router.matchError(request, { env }, error, "route");
|
|
@@ -342,24 +435,48 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
342
435
|
|
|
343
436
|
setRequestContextParams(errorResult.params, errorResult.routeName);
|
|
344
437
|
|
|
438
|
+
// Only the failing action id + URL are in scope here (no formData/actionResult
|
|
439
|
+
// thread into this helper). Expose the URL only when the action id is known:
|
|
440
|
+
// this helper also handles malformed form bodies before action detection, and
|
|
441
|
+
// those should not look like action-triggered renders to transition({ when }).
|
|
442
|
+
if (actionId != null) {
|
|
443
|
+
const peErrCtx = getRequestContext();
|
|
444
|
+
peErrCtx._gateActionId = actionId;
|
|
445
|
+
peErrCtx._gateActionUrl = new URL(url);
|
|
446
|
+
}
|
|
447
|
+
|
|
345
448
|
const payload: RscPayload = {
|
|
346
449
|
metadata: {
|
|
347
450
|
pathname: url.pathname,
|
|
348
|
-
|
|
451
|
+
routerId: ctx.router.id,
|
|
452
|
+
basename: ctx.router.basename,
|
|
453
|
+
segments: gateTransitions(
|
|
454
|
+
errorResult.segments,
|
|
455
|
+
getRequestContext(),
|
|
456
|
+
ctx.router.onError,
|
|
457
|
+
),
|
|
349
458
|
matched: errorResult.matched,
|
|
350
459
|
diff: errorResult.diff,
|
|
460
|
+
resolvedIds: errorResult.resolvedIds,
|
|
461
|
+
params: errorResult.params,
|
|
351
462
|
isPartial: false,
|
|
352
463
|
isError: true,
|
|
353
464
|
rootLayout: ctx.router.rootLayout,
|
|
354
465
|
handles: handleStore.stream(),
|
|
355
466
|
version: ctx.version,
|
|
467
|
+
stateCookieName: ctx.router.resolvedStateCookieName,
|
|
356
468
|
themeConfig: ctx.router.themeConfig,
|
|
357
469
|
warmupEnabled: ctx.router.warmupEnabled,
|
|
358
|
-
|
|
470
|
+
strictMode: ctx.router.strictMode,
|
|
471
|
+
initialTheme: getRequestContext().theme,
|
|
359
472
|
},
|
|
360
473
|
};
|
|
361
474
|
|
|
362
|
-
const rscStream = ctx.renderToReadableStream<RscPayload>(payload
|
|
475
|
+
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
476
|
+
onError: (error: unknown) => {
|
|
477
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
478
|
+
},
|
|
479
|
+
});
|
|
363
480
|
// metricsStore=undefined is safe: the handler already stashed the early
|
|
364
481
|
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
365
482
|
// without falling back to a fresh startSSRSetup.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side open-redirect guard.
|
|
3
|
+
*
|
|
4
|
+
* Applied to the FINAL handler response (the single top-level return in
|
|
5
|
+
* `handler.ts`) so every browser-followed redirect honors the same same-origin
|
|
6
|
+
* rule the client enforces (`browser/validate-redirect-origin.ts`), via the one
|
|
7
|
+
* shared resolver in `redirect-origin.ts`. This is the server half of the
|
|
8
|
+
* client's existing guard: the client can only validate redirects its own JS
|
|
9
|
+
* navigates to (the SPA/fetch channel), so document-native redirects -- a no-JS
|
|
10
|
+
* PE form POST, a full-page GET `match.redirect`, a middleware `redirect()`
|
|
11
|
+
* short-circuit, a response-route 3xx -- reach the browser with no client in the
|
|
12
|
+
* loop. They all funnel through one handler return, so guarding there covers
|
|
13
|
+
* every one and any future redirect exit.
|
|
14
|
+
*
|
|
15
|
+
* Soft (SPA/Flight) redirects are 200/204 responses (`X-RSC-Redirect` header or
|
|
16
|
+
* `metadata.redirect` payload) and are NOT redirect Responses, so they never
|
|
17
|
+
* reach this guard -- they stay validated client-side.
|
|
18
|
+
*
|
|
19
|
+
* Behavior on a `Location` header:
|
|
20
|
+
* - same-origin / relative -> passes through unchanged
|
|
21
|
+
* - `redirect(url, { external: true })` (out-of-band brand present) and an
|
|
22
|
+
* http(s) target -> allowed (explicit, auditable, unforgeable opt-in)
|
|
23
|
+
* - branded but a non-http(s) target (e.g. `javascript:`) -> neutralized: the
|
|
24
|
+
* opt-in waives the same-origin rule, NOT scheme safety
|
|
25
|
+
* - cross-origin without the brand -> Location rewritten to the basename root
|
|
26
|
+
* (a safe same-origin landing, the document analog of the client's "stay put");
|
|
27
|
+
* dev logs the blocked target and points to `{ external: true }`.
|
|
28
|
+
*
|
|
29
|
+
* The opt-in is an out-of-band brand on the Response object (isExternalRedirect),
|
|
30
|
+
* never a wire header: a header is forgeable by an attacker-controlled upstream
|
|
31
|
+
* response a proxy-style response route copies through, which would defeat the
|
|
32
|
+
* guard without app code ever opting in. The reserved header name is stripped
|
|
33
|
+
* defensively so a forged value can never reach the browser.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { isRedirectResponse } from "../response-utils.js";
|
|
37
|
+
import {
|
|
38
|
+
resolveSameOriginRedirect,
|
|
39
|
+
resolveExternalRedirect,
|
|
40
|
+
isExternalRedirect,
|
|
41
|
+
EXTERNAL_REDIRECT_MARKER,
|
|
42
|
+
} from "../redirect-origin.js";
|
|
43
|
+
import { carryOverRedirectHeaders } from "./helpers.js";
|
|
44
|
+
|
|
45
|
+
export function guardOutgoingRedirect(
|
|
46
|
+
response: Response,
|
|
47
|
+
requestOrigin: string,
|
|
48
|
+
basename: string | undefined,
|
|
49
|
+
): Response {
|
|
50
|
+
// Only 3xx + Location responses (document-native redirects) are guarded.
|
|
51
|
+
if (!isRedirectResponse(response)) {
|
|
52
|
+
return response;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The reserved marker is never a trust signal. Strip any value -- forged by a
|
|
56
|
+
// proxied upstream or otherwise -- so it can never reach the browser. Trust
|
|
57
|
+
// comes solely from the out-of-band brand below.
|
|
58
|
+
try {
|
|
59
|
+
response.headers.delete(EXTERNAL_REDIRECT_MARKER);
|
|
60
|
+
} catch {
|
|
61
|
+
// Some platform responses carry immutable headers; the header is inert on
|
|
62
|
+
// the browser, so a failed strip is harmless.
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// isRedirectResponse guarantees a truthy Location.
|
|
66
|
+
const location = response.headers.get("Location")!;
|
|
67
|
+
|
|
68
|
+
// Explicit opt-in via redirect(url, { external: true }): allow an off-host
|
|
69
|
+
// target, but only an http(s) one. external waives the same-origin rule, not
|
|
70
|
+
// scheme safety -- a branded javascript:/data: target falls through to be
|
|
71
|
+
// neutralized so it can never become a scriptable navigation downstream.
|
|
72
|
+
if (isExternalRedirect(response)) {
|
|
73
|
+
if (resolveExternalRedirect(location, requestOrigin) !== null) {
|
|
74
|
+
return response;
|
|
75
|
+
}
|
|
76
|
+
} else if (resolveSameOriginRedirect(location, requestOrigin) !== null) {
|
|
77
|
+
return response;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Cross-origin (or unsafe-scheme external): neutralize to a safe same-origin
|
|
81
|
+
// landing.
|
|
82
|
+
const safeTarget = basename && basename !== "/" ? basename : "/";
|
|
83
|
+
if (process.env.NODE_ENV !== "production") {
|
|
84
|
+
console.error(
|
|
85
|
+
`[rango] Blocked cross-origin redirect to "${location}"; sent to ` +
|
|
86
|
+
`"${safeTarget}" instead. To redirect off-host on purpose, use ` +
|
|
87
|
+
`redirect(url, { external: true }).`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const blocked = new Response(null, {
|
|
92
|
+
status: response.status,
|
|
93
|
+
headers: { Location: safeTarget },
|
|
94
|
+
});
|
|
95
|
+
// Preserve cookies and any other headers (Set-Cookie, Server-Timing, ...);
|
|
96
|
+
// carryOverRedirectHeaders intentionally skips Location.
|
|
97
|
+
carryOverRedirectHeaders(response, blocked);
|
|
98
|
+
return blocked;
|
|
99
|
+
}
|