@rangojs/router 0.0.0-experimental.32 → 0.0.0-experimental.3232cd17
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 +4 -0
- package/README.md +198 -44
- package/dist/bin/rango.js +287 -105
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3248 -1117
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +73 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +107 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +245 -21
- package/skills/caching/SKILL.md +302 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +270 -30
- package/skills/host-router/SKILL.md +82 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +49 -5
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +294 -30
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +250 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +97 -5
- package/skills/router-setup/SKILL.md +90 -5
- 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 +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -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 +121 -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 +120 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +67 -40
- 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/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +86 -147
- 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/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +148 -19
- package/src/browser/navigation-client.ts +187 -67
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/partial-update.ts +123 -94
- package/src/browser/prefetch/cache.ts +214 -36
- package/src/browser/prefetch/fetch.ts +260 -38
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +93 -11
- package/src/browser/react/NavigationProvider.tsx +115 -34
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +49 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -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-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +22 -5
- 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 +52 -1
- package/src/browser/rsc-router.tsx +215 -76
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -50
- package/src/browser/types.ts +95 -11
- 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 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +137 -32
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +149 -43
- package/src/cache/cache-scope.ts +148 -81
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2550 -93
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +78 -27
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +108 -290
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- 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 +52 -26
- package/src/index.ts +100 -38
- package/src/internal-debug.ts +2 -4
- 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/network-error-thrower.tsx +1 -6
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +437 -274
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +52 -10
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -41
- package/src/router/intercept-resolution.ts +25 -23
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +213 -30
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +120 -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 +27 -6
- package/src/router/match-middleware/cache-lookup.ts +205 -249
- package/src/router/match-middleware/cache-store.ts +45 -32
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -40
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +173 -143
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +109 -63
- package/src/router/prerender-match.ts +190 -54
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +63 -55
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +6 -28
- package/src/router/router-interfaces.ts +100 -35
- package/src/router/router-options.ts +91 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +242 -75
- package/src/router/segment-resolution/helpers.ts +63 -24
- package/src/router/segment-resolution/loader-cache.ts +41 -37
- package/src/router/segment-resolution/revalidation.ts +456 -372
- 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 +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +2 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +91 -46
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +134 -43
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -383
- package/src/rsc/helpers.ts +162 -46
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +30 -3
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +90 -63
- package/src/rsc/rsc-rendering.ts +56 -54
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -67
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +25 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +134 -0
- package/src/segment-system.tsx +272 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +26 -24
- package/src/server/loader-registry.ts +10 -28
- package/src/server/request-context.ts +338 -126
- package/src/ssr/index.tsx +23 -15
- 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 +618 -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 +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -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 +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -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 +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- 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 +233 -81
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +107 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -14
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- 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 -18
- 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 +192 -99
- 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 +51 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +187 -69
- package/src/vite/plugins/cjs-to-esm.ts +8 -18
- 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 +49 -98
- package/src/vite/plugins/expose-id-utils.ts +11 -50
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
- package/src/vite/plugins/expose-internal-ids.ts +554 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +41 -20
- package/src/vite/plugins/virtual-entries.ts +2 -17
- package/src/vite/rango.ts +257 -289
- package/src/vite/router-discovery.ts +930 -140
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -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 +27 -29
- package/src/vite/utils/shared-utils.ts +92 -42
- package/src/browser/action-response-classifier.ts +0 -99
- 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/router/middleware-cookies.ts +0 -55
package/src/rsc/helpers.ts
CHANGED
|
@@ -8,8 +8,86 @@ import {
|
|
|
8
8
|
_getRequestContext,
|
|
9
9
|
getLocationState,
|
|
10
10
|
} from "../server/request-context.js";
|
|
11
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
11
12
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
13
|
+
import { isRedirectResponse } from "../response-utils.js";
|
|
14
|
+
import {
|
|
15
|
+
EXTERNAL_REDIRECT_MARKER,
|
|
16
|
+
isExternalRedirect,
|
|
17
|
+
markExternalRedirect,
|
|
18
|
+
} from "../redirect-origin.js";
|
|
12
19
|
import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
|
|
20
|
+
import { formatCacheSignalHeader } from "../router/telemetry.js";
|
|
21
|
+
import type { RscPayload } from "./types.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* DEVELOPMENT/TEST ONLY. When the debug cache signal gate is on,
|
|
25
|
+
* match/matchPartial populate ctx._cacheSignal. Emit it as the X-Rango-Cache
|
|
26
|
+
* header. When the gate is off, ctx._cacheSignal is undefined and NOTHING is
|
|
27
|
+
* attached — output is byte-identical to the default. Header mutation failures
|
|
28
|
+
* are swallowed so immutable Response headers (e.g. protocol-switch) are safe.
|
|
29
|
+
*/
|
|
30
|
+
function applyCacheSignalHeader(target: Headers, ctx: RequestContext): void {
|
|
31
|
+
const signal = ctx._cacheSignal;
|
|
32
|
+
if (!signal || signal.length === 0) return;
|
|
33
|
+
try {
|
|
34
|
+
target.set("X-Rango-Cache", formatCacheSignalHeader(signal));
|
|
35
|
+
} catch {
|
|
36
|
+
// Headers immutable — skip.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Copy stub headers from the request context onto a target Headers instance:
|
|
42
|
+
* append Set-Cookie entries, set everything else only if absent. Header
|
|
43
|
+
* mutation failures are swallowed so the same logic works against Response
|
|
44
|
+
* headers that may be immutable (e.g. Cloudflare protocol-switch responses).
|
|
45
|
+
*/
|
|
46
|
+
function applyStubHeaders(target: Headers, stub: Headers): void {
|
|
47
|
+
stub.forEach((value, name) => {
|
|
48
|
+
try {
|
|
49
|
+
// The reserved external-redirect marker is internal and never a trust
|
|
50
|
+
// signal; never copy a stub value (e.g. a stray ctx.header() call) onto a
|
|
51
|
+
// browser-facing response. The opt-in is the out-of-band brand.
|
|
52
|
+
if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
|
|
53
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
54
|
+
target.append(name, value);
|
|
55
|
+
} else if (!target.has(name)) {
|
|
56
|
+
target.set(name, value);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// Headers immutable — skip.
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Drain ctx._onResponseCallbacks onto a response. Swapping the array before
|
|
66
|
+
* iteration prevents re-entrant registrations from double-firing and matches
|
|
67
|
+
* the contract that each callback runs at most once per request.
|
|
68
|
+
*/
|
|
69
|
+
function drainOnResponseCallbacks(
|
|
70
|
+
ctx: RequestContext,
|
|
71
|
+
response: Response,
|
|
72
|
+
): Response {
|
|
73
|
+
const callbacks = ctx._onResponseCallbacks;
|
|
74
|
+
if (callbacks.length === 0) return response;
|
|
75
|
+
ctx._onResponseCallbacks = [];
|
|
76
|
+
// An onResponse callback may return a NEW Response (e.g. to add a header),
|
|
77
|
+
// which drops the out-of-band external-redirect brand (brand is keyed on
|
|
78
|
+
// Response object identity). Preserve a redirect(url, { external: true })
|
|
79
|
+
// opt-in across that rebuild so a callback can't silently neutralize the
|
|
80
|
+
// off-host redirect at the guard chokepoint.
|
|
81
|
+
const wasExternal = isExternalRedirect(response);
|
|
82
|
+
let result = response;
|
|
83
|
+
for (const callback of callbacks) {
|
|
84
|
+
result = callback(result) ?? result;
|
|
85
|
+
}
|
|
86
|
+
if (wasExternal && !isExternalRedirect(result)) {
|
|
87
|
+
markExternalRedirect(result);
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
13
91
|
|
|
14
92
|
/**
|
|
15
93
|
* Check if a request body has content to decode
|
|
@@ -39,40 +117,24 @@ export function createResponseWithMergedHeaders(
|
|
|
39
117
|
return new Response(body, init);
|
|
40
118
|
}
|
|
41
119
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// merge points (e.g. executeMiddleware) do not duplicate them.
|
|
120
|
+
// Delete Set-Cookie from the stub after consuming so downstream merge
|
|
121
|
+
// points (e.g. executeMiddleware) don't duplicate them.
|
|
45
122
|
const mergedHeaders = new Headers(init.headers);
|
|
46
|
-
ctx.res.headers
|
|
47
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
48
|
-
mergedHeaders.append(name, value);
|
|
49
|
-
} else if (!mergedHeaders.has(name)) {
|
|
50
|
-
// Only set if not already present in init.headers
|
|
51
|
-
mergedHeaders.set(name, value);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
123
|
+
applyStubHeaders(mergedHeaders, ctx.res.headers);
|
|
54
124
|
ctx.res.headers.delete("set-cookie");
|
|
125
|
+
applyCacheSignalHeader(mergedHeaders, ctx);
|
|
55
126
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
127
|
+
// ctx.res.status overrides init.status when explicitly set (e.g. 404 for
|
|
128
|
+
// notFound, 500 for error). Default ctx.res.status is 200.
|
|
58
129
|
const status = ctx.res.status !== 200 ? ctx.res.status : init.status;
|
|
59
130
|
|
|
60
|
-
|
|
131
|
+
const response = new Response(body, {
|
|
61
132
|
...init,
|
|
62
133
|
status,
|
|
63
134
|
headers: mergedHeaders,
|
|
64
135
|
});
|
|
65
136
|
|
|
66
|
-
|
|
67
|
-
// Drain the array so that downstream callers (e.g. finalizeResponse)
|
|
68
|
-
// do not re-execute the same callbacks on this response.
|
|
69
|
-
const callbacks = ctx._onResponseCallbacks;
|
|
70
|
-
ctx._onResponseCallbacks = [];
|
|
71
|
-
for (const callback of callbacks) {
|
|
72
|
-
response = callback(response) ?? response;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return response;
|
|
137
|
+
return drainOnResponseCallbacks(ctx, response);
|
|
76
138
|
}
|
|
77
139
|
|
|
78
140
|
/**
|
|
@@ -91,8 +153,20 @@ export function createSimpleRedirectResponse(redirectUrl: string): Response {
|
|
|
91
153
|
|
|
92
154
|
/**
|
|
93
155
|
* Carry over headers from a source redirect Response to a wrapper Response.
|
|
94
|
-
* Skips Location and X-RSC-Redirect (intentionally replaced by the wrapper)
|
|
95
|
-
*
|
|
156
|
+
* Skips Location and X-RSC-Redirect (intentionally replaced by the wrapper) and
|
|
157
|
+
* appends Set-Cookie to avoid clobbering multiple cookie headers.
|
|
158
|
+
*
|
|
159
|
+
* This is a GENERIC copier used by every redirect-rebuild path (PE
|
|
160
|
+
* extractRedirectResponse, the SPA intercept below, the guard's neutralize
|
|
161
|
+
* rebuild), so it has two redirect-specific jobs:
|
|
162
|
+
*
|
|
163
|
+
* 1. NEVER copy the reserved external-redirect header: it is no longer a trust
|
|
164
|
+
* signal (the opt-in is the out-of-band brand), and a forged value from a
|
|
165
|
+
* proxied upstream must not ride a rebuilt response to the browser.
|
|
166
|
+
* 2. Transfer the out-of-band external brand: a rebuilt document-native redirect
|
|
167
|
+
* has to carry the opt-in to the guard chokepoint, which reads and clears it.
|
|
168
|
+
* Without this transfer, redirect(url, { external: true }) would be silently
|
|
169
|
+
* neutralized on any rebuild path (fail-closed, but a feature regression).
|
|
96
170
|
*/
|
|
97
171
|
export function carryOverRedirectHeaders(
|
|
98
172
|
source: Response,
|
|
@@ -101,12 +175,16 @@ export function carryOverRedirectHeaders(
|
|
|
101
175
|
source.headers.forEach((value, name) => {
|
|
102
176
|
const lower = name.toLowerCase();
|
|
103
177
|
if (lower === "location" || lower === "x-rsc-redirect") return;
|
|
178
|
+
if (lower === EXTERNAL_REDIRECT_MARKER) return;
|
|
104
179
|
if (lower === "set-cookie") {
|
|
105
180
|
target.headers.append(name, value);
|
|
106
181
|
} else if (!target.headers.has(name)) {
|
|
107
182
|
target.headers.set(name, value);
|
|
108
183
|
}
|
|
109
184
|
});
|
|
185
|
+
if (isExternalRedirect(source)) {
|
|
186
|
+
markExternalRedirect(target);
|
|
187
|
+
}
|
|
110
188
|
}
|
|
111
189
|
|
|
112
190
|
/**
|
|
@@ -120,28 +198,62 @@ export function interceptRedirectForPartial(
|
|
|
120
198
|
createRedirectFlightResponse: (
|
|
121
199
|
redirectUrl: string,
|
|
122
200
|
locationState?: Record<string, unknown>,
|
|
201
|
+
external?: boolean,
|
|
123
202
|
) => Response,
|
|
124
203
|
): Response | null {
|
|
125
|
-
|
|
126
|
-
if (!(response.status >= 300 && response.status < 400 && redirectUrl)) {
|
|
204
|
+
if (!isRedirectResponse(response)) {
|
|
127
205
|
return null;
|
|
128
206
|
}
|
|
207
|
+
const redirectUrl = response.headers.get("Location")!;
|
|
208
|
+
// redirect(url, { external: true }) marks an explicit off-host redirect via
|
|
209
|
+
// the out-of-band brand (not a wire header). On the SPA/action channel the
|
|
210
|
+
// intent must travel as a Flight payload (metadata.redirect.external) so the
|
|
211
|
+
// client does a scheme-validated hard navigation (location.assign) rather than
|
|
212
|
+
// a partial fetch. The client re-validates the scheme; see partial-update.ts.
|
|
213
|
+
const external = isExternalRedirect(response);
|
|
129
214
|
const locationState = getLocationState();
|
|
130
215
|
let intercepted: Response;
|
|
131
216
|
if (locationState) {
|
|
132
217
|
intercepted = createRedirectFlightResponse(
|
|
133
218
|
redirectUrl,
|
|
134
219
|
resolveLocationStateEntries(locationState),
|
|
220
|
+
external,
|
|
135
221
|
);
|
|
222
|
+
} else if (external) {
|
|
223
|
+
intercepted = createRedirectFlightResponse(redirectUrl, undefined, true);
|
|
136
224
|
} else {
|
|
137
225
|
intercepted = createSimpleRedirectResponse(redirectUrl);
|
|
138
226
|
}
|
|
139
227
|
|
|
140
228
|
carryOverRedirectHeaders(response, intercepted);
|
|
229
|
+
// Defense-in-depth at the SPA browser-facing exit: carryOverRedirectHeaders
|
|
230
|
+
// already refuses to copy the reserved marker, but strip any value that might
|
|
231
|
+
// exist on `intercepted` so a forged header can never ride the 200/204 to the
|
|
232
|
+
// browser. The external intent travels in metadata.redirect.external (Flight),
|
|
233
|
+
// where the client re-validates the scheme.
|
|
234
|
+
try {
|
|
235
|
+
intercepted.headers.delete(EXTERNAL_REDIRECT_MARKER);
|
|
236
|
+
} catch {
|
|
237
|
+
// Immutable headers: the marker was never copied here, so this is inert.
|
|
238
|
+
}
|
|
141
239
|
|
|
142
240
|
return intercepted;
|
|
143
241
|
}
|
|
144
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Attach location state set during a request to a payload's metadata.
|
|
245
|
+
* No-op if no location state was set. Callers must ensure payload.metadata
|
|
246
|
+
* is populated (the non-null assertion holds for the partial/action payloads
|
|
247
|
+
* that reach this helper).
|
|
248
|
+
*/
|
|
249
|
+
export function attachLocationStateIfPresent(payload: RscPayload): void {
|
|
250
|
+
const locationState = getLocationState();
|
|
251
|
+
if (locationState) {
|
|
252
|
+
payload.metadata!.locationState =
|
|
253
|
+
resolveLocationStateEntries(locationState);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
145
257
|
/**
|
|
146
258
|
* Only cache successful responses. Non-200 statuses (errors, redirects) are
|
|
147
259
|
* not cached -- notFound() produces 500 in response routes, and explicit
|
|
@@ -168,31 +280,35 @@ export function buildRouteMiddlewareEntries<TEnv>(
|
|
|
168
280
|
regex: null,
|
|
169
281
|
paramNames: [],
|
|
170
282
|
handler: mw.handler,
|
|
171
|
-
mountPrefix: null,
|
|
172
283
|
} as MiddlewareEntry<TEnv>,
|
|
173
284
|
params: mw.params,
|
|
174
285
|
}));
|
|
175
286
|
}
|
|
176
287
|
|
|
177
288
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
289
|
+
* Merge stub headers from the request context onto an existing Response in
|
|
290
|
+
* place, then drain onResponse callbacks. Used when a Response cannot flow
|
|
291
|
+
* through `new Response()` — status 101 is outside the constructor's
|
|
292
|
+
* 200-599 range, and the Cloudflare-specific `webSocket` property would be
|
|
293
|
+
* lost on reconstruction.
|
|
183
294
|
*/
|
|
184
|
-
export function
|
|
295
|
+
export function mergeStubHeadersAndFinalize(response: Response): Response {
|
|
185
296
|
const ctx = _getRequestContext();
|
|
186
|
-
if (!ctx
|
|
187
|
-
return response;
|
|
188
|
-
}
|
|
297
|
+
if (!ctx) return response;
|
|
189
298
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
299
|
+
applyStubHeaders(response.headers, ctx.res.headers);
|
|
300
|
+
ctx.res.headers.delete("set-cookie");
|
|
301
|
+
|
|
302
|
+
return drainOnResponseCallbacks(ctx, response);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Run onResponse callbacks on an existing Response. Used by code paths that
|
|
307
|
+
* bypass createResponseWithMergedHeaders (e.g. middleware short-circuits)
|
|
308
|
+
* but still need ctx.onResponse() callbacks to fire.
|
|
309
|
+
*/
|
|
310
|
+
export function finalizeResponse(response: Response): Response {
|
|
311
|
+
const ctx = _getRequestContext();
|
|
312
|
+
if (!ctx) return response;
|
|
313
|
+
return drainOnResponseCallbacks(ctx, response);
|
|
198
314
|
}
|
package/src/rsc/index.ts
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared serialization for `json()` response-route results.
|
|
3
|
+
*
|
|
4
|
+
* Kept in its own lightweight module (depends only on `errors.js`) so the
|
|
5
|
+
* `dispatch()` testing primitive can import it WITHOUT dragging in
|
|
6
|
+
* `response-route-handler.ts`'s heavy runtime graph, which transitively reaches
|
|
7
|
+
* a Vite virtual module and breaks a plain (non-Vite) vitest import.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { RouterError } from "../errors.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Serialize a `json()` response-route result, rejecting a nested unresolved
|
|
14
|
+
* Promise (the forgotten-await footgun: `() => ({ data: fetchSomething() })`).
|
|
15
|
+
* `JSON.stringify` would silently emit `{}` for a Promise, shipping empty data;
|
|
16
|
+
* the RSC pipeline awaits nested promises but this path does not. Throwing
|
|
17
|
+
* `RESPONSE_NOT_SERIALIZABLE` makes the failure loud.
|
|
18
|
+
*
|
|
19
|
+
* Shared by the production response-route handler and the `dispatch()` testing
|
|
20
|
+
* primitive so a `dispatch` json test of a forgotten await fails exactly where
|
|
21
|
+
* production 500s, instead of going green.
|
|
22
|
+
*/
|
|
23
|
+
export function stringifyJsonRouteResult(result: unknown): string {
|
|
24
|
+
return JSON.stringify(result, (_key, value) => {
|
|
25
|
+
if (
|
|
26
|
+
value != null &&
|
|
27
|
+
typeof (value as { then?: unknown }).then === "function"
|
|
28
|
+
) {
|
|
29
|
+
throw new RouterError(
|
|
30
|
+
"RESPONSE_NOT_SERIALIZABLE",
|
|
31
|
+
"A json() response route returned a Promise (likely a forgotten " +
|
|
32
|
+
"await). Await async values before returning so they serialize, " +
|
|
33
|
+
"instead of emitting an empty {}.",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
});
|
|
38
|
+
}
|
package/src/rsc/loader-fetch.ts
CHANGED
|
@@ -168,8 +168,19 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
168
168
|
loaderResult: unknown;
|
|
169
169
|
}
|
|
170
170
|
const loaderPayload: LoaderPayload = { loaderResult: result };
|
|
171
|
-
const rscStream =
|
|
172
|
-
|
|
171
|
+
const rscStream = ctx.renderToReadableStream<LoaderPayload>(
|
|
172
|
+
loaderPayload,
|
|
173
|
+
{
|
|
174
|
+
onError: (error: unknown) => {
|
|
175
|
+
ctx.callOnError(error, "rendering", {
|
|
176
|
+
request,
|
|
177
|
+
url,
|
|
178
|
+
env,
|
|
179
|
+
loaderName: loaderId,
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
);
|
|
173
184
|
|
|
174
185
|
return createResponseWithMergedHeaders(rscStream, {
|
|
175
186
|
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
@@ -199,7 +210,16 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
199
210
|
name: err.name,
|
|
200
211
|
},
|
|
201
212
|
};
|
|
202
|
-
const rscStream = ctx.renderToReadableStream(errorPayload
|
|
213
|
+
const rscStream = ctx.renderToReadableStream(errorPayload, {
|
|
214
|
+
onError: (error: unknown) => {
|
|
215
|
+
ctx.callOnError(error, "rendering", {
|
|
216
|
+
request,
|
|
217
|
+
url,
|
|
218
|
+
env,
|
|
219
|
+
loaderName: loaderId,
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
});
|
|
203
223
|
|
|
204
224
|
return createResponseWithMergedHeaders(rscStream, {
|
|
205
225
|
status: 500,
|
package/src/rsc/manifest-init.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
setRouteTrie,
|
|
14
14
|
setRouterManifest,
|
|
15
15
|
setRouterTrie,
|
|
16
|
+
setRouterPrecomputedEntries,
|
|
16
17
|
} from "../route-map-builder.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -31,48 +32,18 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
31
32
|
): Promise<void> {
|
|
32
33
|
const { generateManifestFull } =
|
|
33
34
|
await import("../build/generate-manifest.js");
|
|
34
|
-
const generated = generateManifestFull(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// Override with prefix from include() entries so the trie
|
|
47
|
-
// returns the correct sp for lazy entry lookup in findMatch.
|
|
48
|
-
// Walk recursively to include routes in nested includes.
|
|
49
|
-
if (generated.prefixTree) {
|
|
50
|
-
const visitPrefixNode = (node: any): void => {
|
|
51
|
-
const sp = node.staticPrefix || "";
|
|
52
|
-
for (const route of node.routes || []) {
|
|
53
|
-
routeToStaticPrefix[route] = sp;
|
|
54
|
-
}
|
|
55
|
-
for (const child of Object.values(node.children || {})) {
|
|
56
|
-
visitPrefixNode(child);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
for (const node of Object.values(generated.prefixTree)) {
|
|
60
|
-
visitPrefixNode(node);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
const trie = buildRouteTrie(
|
|
64
|
-
generated.routeManifest,
|
|
65
|
-
generated._routeAncestry,
|
|
66
|
-
routeToStaticPrefix,
|
|
67
|
-
generated.routeTrailingSlash,
|
|
68
|
-
generated.prerenderRoutes
|
|
69
|
-
? new Set(generated.prerenderRoutes)
|
|
70
|
-
: undefined,
|
|
71
|
-
generated.passthroughRoutes
|
|
72
|
-
? new Set(generated.passthroughRoutes)
|
|
73
|
-
: undefined,
|
|
74
|
-
generated.responseTypeRoutes,
|
|
75
|
-
);
|
|
35
|
+
const generated = generateManifestFull(
|
|
36
|
+
router.urlpatterns,
|
|
37
|
+
undefined,
|
|
38
|
+
router.basename ? { urlPrefix: router.basename } : undefined,
|
|
39
|
+
);
|
|
40
|
+
// Build the trie through the SAME shared helper the production discovery uses
|
|
41
|
+
// (discover-routers.ts), so the dev runtime-rebuilt trie and the prod
|
|
42
|
+
// serialized trie cannot drift. buildPerRouterTrie returns null when there
|
|
43
|
+
// are no routes.
|
|
44
|
+
const { buildPerRouterTrie } = await import("../build/route-trie.js");
|
|
45
|
+
const trie = buildPerRouterTrie(generated);
|
|
46
|
+
if (trie) {
|
|
76
47
|
setRouterTrie(router.id, trie);
|
|
77
48
|
// Set global trie only if not already set by another router
|
|
78
49
|
if (!getRouteTrie()) {
|
|
@@ -80,6 +51,26 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
80
51
|
}
|
|
81
52
|
}
|
|
82
53
|
setRouterManifest(router.id, generated.routeManifest);
|
|
54
|
+
|
|
55
|
+
// Match the production discovery path: precompute leaf-include entries so the
|
|
56
|
+
// match-time shortcut in evaluateLazyEntry applies in dev/Cloudflare too.
|
|
57
|
+
// Without this, dev re-runs each matched leaf include's handler at match time
|
|
58
|
+
// (evaluateLazyEntry) AND again at render time (loadManifest); with it, the
|
|
59
|
+
// match-time run is skipped and the handler runs once per first request.
|
|
60
|
+
// Identical route ownership to the handler path (the shortcut is guarded by
|
|
61
|
+
// the same prefixIsShared and #506 checks production uses).
|
|
62
|
+
const { flattenLeafEntries } = await import("../build/prefix-tree-utils.js");
|
|
63
|
+
const precomputed: Array<{
|
|
64
|
+
staticPrefix: string;
|
|
65
|
+
routes: Record<string, string>;
|
|
66
|
+
}> = [];
|
|
67
|
+
flattenLeafEntries(
|
|
68
|
+
generated.prefixTree,
|
|
69
|
+
generated.routeManifest,
|
|
70
|
+
precomputed,
|
|
71
|
+
);
|
|
72
|
+
setRouterPrecomputedEntries(router.id, precomputed);
|
|
73
|
+
|
|
83
74
|
// Merge into global manifest (needed for reverse/href across routers)
|
|
84
75
|
const existing = hasCachedManifest() ? getGlobalRouteMap() : {};
|
|
85
76
|
setCachedManifest({ ...existing, ...generated.routeManifest });
|
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
|
}
|