Skip to content

Commit af8d250

Browse files
committed
fix: preserve canonical routed preview origins
1 parent 95c3bb3 commit af8d250

11 files changed

Lines changed: 313 additions & 44 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@
164164
"test:artifact-reference-dtos": "tsx tests/artifact-reference-dtos.test.ts",
165165
"test:artifact-path-primitives": "tsx tests/artifact-path-primitives.test.ts",
166166
"test:browser-callback-materialization-contracts": "tsx tests/browser-callback-materialization-contracts.test.ts",
167+
"test:browser-canonical-preview-origin": "tsx tests/browser-canonical-preview-origin.test.ts",
167168
"test:materialize-replay-package-command": "tsx tests/materialize-replay-package-command.test.ts",
168169
"test:source-package-compiler-primitives": "tsx tests/source-package-compiler-primitives.test.ts",
169170
"test:source-root-preparation": "tsx tests/source-root-preparation.test.ts",

packages/runtime-core/src/artifact-review.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ export interface ArtifactReviewBrowserSummary {
9292
localPreviewOrigin?: string
9393
requestedPreviewOrigin?: string
9494
effectivePreviewOrigin?: string
95+
canonicalBrowserOrigin?: string
96+
localProxyOrigin?: string
97+
upstreamRuntimeOrigin?: string
9598
finalUrl?: string
9699
windowLocationOrigin?: string
97100
viewport?: {

packages/runtime-playground/src/browser-actions-runner.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export async function runBrowserActionsCommand({
125125
const startedAtMs = Date.now()
126126
const progress = createBrowserProbeProgressTracker(startedAt, 0)
127127
const browser = await launchChromiumBrowser()
128-
const topology = browserPreviewTopology(args, runtimeSpec, server.serverUrl)
128+
const topology = browserPreviewTopology(args, runtimeSpec, server.serverUrl, server.previewProxyDiagnostics?.targetOrigin)
129129
const { preview, networkPolicy } = topology
130130
let requestedUrl = initialUrl ? topology.resolveUrl(initialUrl) : preview.effectiveOrigin
131131
let finalUrl = requestedUrl
@@ -145,10 +145,11 @@ export async function runBrowserActionsCommand({
145145

146146
try {
147147
const context = browserPreviewNeedsContextRouting(networkPolicy) || !!storageStateImport ? await browser.newContext({
148+
...topology.contextOptions(),
148149
...(storageStateImport ? { storageState: storageStateImport.storageState } : {}),
149150
}) : null
150151
if (context) {
151-
await routeBrowserPreviewContextNetwork(context, networkPolicy, preview.effectiveOrigin)
152+
await routeBrowserPreviewContextNetwork(context, networkPolicy, topology.origins.localProxyOrigin)
152153
}
153154
const page = context ? await context.newPage() : await browser.newPage()
154155
if (onProgress) {

packages/runtime-playground/src/browser-artifacts.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export interface BrowserArtifactBase {
1717
localPreviewOrigin?: string
1818
requestedPreviewOrigin?: string
1919
effectivePreviewOrigin?: string
20+
canonicalBrowserOrigin?: string
21+
localProxyOrigin?: string
22+
upstreamRuntimeOrigin?: string
2023
prePageScript?: BrowserProbeScriptMetadata
2124
files: BrowserArtifactFiles
2225
summary: BrowserArtifactSummary
@@ -1043,6 +1046,9 @@ export function browserReviewSummary(probes: BrowserArtifact[]): ArtifactReviewB
10431046
localPreviewOrigin: probe.localPreviewOrigin,
10441047
requestedPreviewOrigin: probe.requestedPreviewOrigin,
10451048
effectivePreviewOrigin: probe.effectivePreviewOrigin,
1049+
canonicalBrowserOrigin: probe.canonicalBrowserOrigin,
1050+
localProxyOrigin: probe.localProxyOrigin,
1051+
upstreamRuntimeOrigin: probe.upstreamRuntimeOrigin,
10461052
finalUrl: probe.summary.finalUrl,
10471053
windowLocationOrigin: probe.summary.windowLocationOrigin,
10481054
viewport: probe.summary.viewport,

packages/runtime-playground/src/browser-multi-actor-scenario-runner.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export async function runBrowserMultiActorScenarioCommand(input: {
2828
// Traces are always retained for replay, even when callers narrow display captures.
2929
const captures = new Set([...(scenario.captures ?? ["steps", "console", "errors", "network", "screenshot"]), "trace"])
3030
const artifacts = new BrowserArtifactSession(artifactRoot, "files/browser", { source: "wordpress.browser-scenario", operation: "browser-multi-actor-scenario" })
31-
const topology = browserPreviewTopology([], runtimeSpec, server.serverUrl)
31+
const routeHost = runtimeSpec.preview?.siteUrl ? new URL(runtimeSpec.preview.siteUrl).hostname : ""
32+
const topology = browserPreviewTopology(routeHost ? [`route-host=${routeHost}`] : [], runtimeSpec, server.serverUrl, server.previewProxyDiagnostics?.targetOrigin)
3233
const browser = await launchChromiumBrowser()
3334
const evidence: Record<string, ActorEvidence> = {}
3435
let result: BrowserMultiActorScenarioResult | undefined
@@ -43,8 +44,8 @@ export async function runBrowserMultiActorScenarioCommand(input: {
4344
const session = wordpressUserSessionFromCommandArgs([`session=${actor.userSession}`], runtimeSpec)
4445
if (!session) throw new Error(`Actor ${actor.name} requires user session ${actor.userSession}`)
4546
const userId = await actorUserId(actor.name, session.user.userId, session.user, runtimeSpec, runPlaygroundCommand, server)
46-
const context = await browser.newContext()
47-
await routeBrowserPreviewContextNetwork(context, topology.networkPolicy, topology.preview.effectiveOrigin)
47+
const context = await browser.newContext(topology.contextOptions())
48+
await routeBrowserPreviewContextNetwork(context, topology.networkPolicy, topology.origins.localProxyOrigin)
4849
const page = await context.newPage()
4950
await context.tracing.start({ screenshots: true, snapshots: true })
5051
await installWordPressAdminAuthCookies({ command: "wordpress.browser-scenario", cookieUrls: topology.authCookieUrls([topology.resolveUrl(scenario.url)]), page, runPlaygroundCommand, runtimeSpec, server, userId })

packages/runtime-playground/src/browser-preview-routing.ts

Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface BrowserPreviewNetworkPolicy {
1414
recordExternal: boolean
1515
stats: Map<string, { requests: number; external: boolean; blocked: number; routed: number }>
1616
routedRedirectEscapes: Array<{ rawOrigin: string; effectiveOrigin: string; reason: string }>
17+
preserveRoutedOrigin: boolean
1718
}
1819

1920
export interface BrowserPreviewNavigationDecision {
@@ -33,10 +34,20 @@ export interface BrowserPreviewTopology {
3334
preview: BrowserProbePreviewRouting
3435
networkPolicy: BrowserPreviewNetworkPolicy
3536
routedHosts: string[]
36-
origins: { localPreviewOrigin: string; requestedPreviewOrigin?: string; effectivePreviewOrigin: string }
37+
origins: BrowserPreviewOrigins
3738
navigationScope: BrowserPreviewNavigationScope
3839
resolveUrl(pathOrUrl: string): string
3940
authCookieUrls(targetUrls: string[]): string[]
41+
contextOptions(): { proxy?: { server: string } }
42+
}
43+
44+
export interface BrowserPreviewOrigins {
45+
localPreviewOrigin: string
46+
requestedPreviewOrigin?: string
47+
effectivePreviewOrigin: string
48+
canonicalBrowserOrigin: string
49+
localProxyOrigin: string
50+
upstreamRuntimeOrigin?: string
4051
}
4152

4253
export interface BrowserPreviewRouteTracker {
@@ -86,23 +97,27 @@ export function browserPreviewRouting(args: string[], runtimeSpec: RuntimeCreate
8697
}
8798
}
8899

89-
export function browserPreviewTopology(args: string[], runtimeSpec: RuntimeCreateSpec | undefined, localPreviewOrigin: string): BrowserPreviewTopology {
90-
const preview = browserPreviewRouting(args, runtimeSpec, localPreviewOrigin)
100+
export function browserPreviewTopology(args: string[], runtimeSpec: RuntimeCreateSpec | undefined, localPreviewOrigin: string, upstreamRuntimeOrigin?: string): BrowserPreviewTopology {
91101
const routedHosts = commaListArg(args, "route-host")
102+
const preview = browserPreviewRouting(args, runtimeSpec, localPreviewOrigin)
103+
applyCanonicalRoutedPreviewOrigin(preview, runtimeSpec?.preview?.siteUrl, routedHosts)
92104
const networkPolicy = browserPreviewNetworkPolicy(args, routedHosts, preview)
93105

94106
return {
95107
preview,
96108
networkPolicy,
97109
routedHosts,
98-
origins: browserPreviewOrigins(preview),
110+
origins: browserPreviewOrigins(preview, upstreamRuntimeOrigin),
99111
navigationScope: browserPreviewNavigationScope(preview.effectiveOrigin, networkPolicy),
100112
resolveUrl(pathOrUrl) {
101113
return resolveBrowserPreviewUrl(pathOrUrl, preview.effectiveOrigin)
102114
},
103115
authCookieUrls(targetUrls) {
104116
return browserPreviewAuthCookieUrls(localPreviewOrigin, routedHosts, targetUrls)
105117
},
118+
contextOptions() {
119+
return networkPolicy.preserveRoutedOrigin ? { proxy: { server: new URL(localPreviewOrigin).origin } } : {}
120+
},
106121
}
107122
}
108123

@@ -135,12 +150,50 @@ export function browserPreviewNavigationScope(effectivePreviewOrigin: string, po
135150
}
136151
}
137152

138-
export function browserPreviewOrigins(preview: BrowserProbePreviewRouting): { localPreviewOrigin: string; requestedPreviewOrigin?: string; effectivePreviewOrigin: string } {
153+
export function browserPreviewOrigins(preview: BrowserProbePreviewRouting, upstreamRuntimeOrigin?: string): BrowserPreviewOrigins {
139154
return {
140155
localPreviewOrigin: preview.localOrigin,
141-
requestedPreviewOrigin: preview.publicOrigin,
156+
...(preview.publicOrigin ? { requestedPreviewOrigin: preview.publicOrigin } : {}),
142157
effectivePreviewOrigin: preview.effectiveOrigin,
158+
canonicalBrowserOrigin: new URL(preview.effectiveOrigin).origin,
159+
localProxyOrigin: new URL(preview.localOrigin).origin,
160+
...(upstreamRuntimeOrigin ? { upstreamRuntimeOrigin: new URL(upstreamRuntimeOrigin).origin } : {}),
161+
}
162+
}
163+
164+
function applyCanonicalRoutedPreviewOrigin(preview: BrowserProbePreviewRouting, siteUrl: string | undefined, routedHosts: string[]): void {
165+
if (preview.effectiveMode !== "local" || !siteUrl) {
166+
return
167+
}
168+
169+
let canonical: URL
170+
try {
171+
canonical = new URL(siteUrl)
172+
} catch {
173+
return
174+
}
175+
const host = normalizeBrowserPreviewHost(canonical.hostname)
176+
if (!routedHosts.map(normalizeBrowserPreviewHost).includes(host)) {
177+
return
178+
}
179+
180+
if (canonical.protocol !== "http:") {
181+
preview.diagnostics.push({
182+
code: "preview-canonical-origin-preservation-inconclusive",
183+
severity: "error",
184+
message: "The local Playground provider cannot preserve this declared canonical preview protocol.",
185+
details: { status: "inconclusive", canonicalOrigin: canonical.origin, supportedProtocols: ["http:"] },
186+
})
187+
return
143188
}
189+
190+
preview.effectiveOrigin = canonical.toString()
191+
preview.diagnostics.push({
192+
code: "preview-canonical-routed-origin",
193+
severity: "info",
194+
message: "The declared routed preview alias is the browser-visible origin.",
195+
details: { canonicalOrigin: canonical.origin, localProxyOrigin: new URL(preview.localOrigin).origin },
196+
})
144197
}
145198

146199
export function browserPreviewReadinessError(preview: BrowserProbePreviewRouting): Error | undefined {
@@ -207,6 +260,7 @@ export function browserPreviewNetworkPolicy(args: string[], routeHosts: string[]
207260
recordExternal: strictBooleanArg(args, "record-external", false),
208261
stats: new Map(),
209262
routedRedirectEscapes: [],
263+
preserveRoutedOrigin: new URL(preview.effectiveOrigin).origin !== new URL(preview.localOrigin).origin && preview.effectiveMode === "local",
210264
}
211265
}
212266

@@ -293,28 +347,38 @@ async function routeBrowserPreviewNetwork(routePattern: (url: string, handler: (
293347
stat.requests += 1
294348
stat.external = !policy.firstPartyHosts.has(host)
295349

296-
if (policy.blockHosts.has(host) || (policy.mode === "block" && stat.external && !policy.allowHosts.has(host))) {
350+
if (policy.blockHosts.has(host)) {
297351
stat.blocked += 1
298352
await route.abort("blockedbyclient")
299353
return
300354
}
301355

302-
if (!policy.routeHosts.has(host)) {
303-
await route.continue()
356+
if (policy.routeHosts.has(host)) {
357+
stat.routed += 1
358+
if (policy.preserveRoutedOrigin) {
359+
await route.continue()
360+
return
361+
}
362+
const task = fulfillBrowserPreviewRoutedHost(route, requestUrl, policy, origin)
363+
tracker?.pending.add(task)
364+
try {
365+
await task
366+
} catch (error) {
367+
tracker?.errors.push(error)
368+
throw error
369+
} finally {
370+
tracker?.pending.delete(task)
371+
}
304372
return
305373
}
306374

307-
stat.routed += 1
308-
const task = fulfillBrowserPreviewRoutedHost(route, requestUrl, policy, origin)
309-
tracker?.pending.add(task)
310-
try {
311-
await task
312-
} catch (error) {
313-
tracker?.errors.push(error)
314-
throw error
315-
} finally {
316-
tracker?.pending.delete(task)
375+
if (policy.preserveRoutedOrigin || (policy.mode === "block" && stat.external && !policy.allowHosts.has(host)) || (request.resourceType() === "document" && stat.external)) {
376+
stat.blocked += 1
377+
await route.abort("blockedbyclient")
378+
return
317379
}
380+
381+
await route.continue()
318382
})
319383
}
320384

packages/runtime-playground/src/browser-probe-runner.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ export async function runSingleBrowserProbeCommand({
242242
const activeDiagnosticProviders = runPlan.diagnosticProviders ?? diagnosticProviders ?? []
243243
const captureSelection = browserProbeCaptureSelection(capture, assertions)
244244
const prePageScriptMetadata = prePageScript ? browserProbeScriptMetadata(prePageScript) : undefined
245-
const topology = browserPreviewTopology(args, runtimeSpec, server.serverUrl)
245+
const topology = browserPreviewTopology(args, runtimeSpec, server.serverUrl, server.previewProxyDiagnostics?.targetOrigin)
246246
const { preview, networkPolicy } = topology
247247
const routeTracker = createBrowserPreviewRouteTracker()
248248
const targetUrl = topology.resolveUrl(runPlan.url)
@@ -304,6 +304,7 @@ export async function runSingleBrowserProbeCommand({
304304
const contextPermissions = browserProbeContextPermissions(requestedContext)
305305
context = browserPreviewNeedsContextRouting(networkPolicy) || !!storageStateImport || requestedContext.device || requestedContext.geolocation || requestedContext.locale || requestedContext.timezone || requestedContext.userAgent || contextPermissions.length > 0
306306
? await browser.newContext({
307+
...topology.contextOptions(),
307308
...(deviceProfile ?? {}),
308309
...(storageStateImport ? { storageState: storageStateImport.storageState } : {}),
309310
...(requestedContext.locale ? { locale: requestedContext.locale } : {}),
@@ -316,7 +317,7 @@ export async function runSingleBrowserProbeCommand({
316317
await context.grantPermissions(contextPermissions)
317318
}
318319
if (context && browserPreviewNeedsContextRouting(networkPolicy)) {
319-
await routeBrowserPreviewContextNetwork(context, networkPolicy, preview.effectiveOrigin, routeTracker)
320+
await routeBrowserPreviewContextNetwork(context, networkPolicy, topology.origins.localProxyOrigin, routeTracker)
320321
}
321322
page = context ? await context.newPage() : await browser.newPage()
322323
if (requestedContext.geolocation?.permission === "denied") geolocationPermissionCleanup = await applyPlaywrightGeolocationPermission(page, "denied")
@@ -343,7 +344,7 @@ export async function runSingleBrowserProbeCommand({
343344
await applyBrowserProbeThrottleProfile(page, throttleProfile)
344345
}
345346
if (!context && browserPreviewNeedsContextRouting(networkPolicy)) {
346-
await routeBrowserPreviewPageNetwork(page, networkPolicy, preview.effectiveOrigin, routeTracker)
347+
await routeBrowserPreviewPageNetwork(page, networkPolicy, topology.origins.localProxyOrigin, routeTracker)
347348
}
348349
await page.addInitScript(BROWSER_PROBE_STATE_INIT_SCRIPT)
349350
if (lifecycleSelectors.length > 0) {

0 commit comments

Comments
 (0)