Skip to content

Commit ef291ef

Browse files
committed
fix: preserve canonical routed preview origins
1 parent 6445ee1 commit ef291ef

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
@@ -165,6 +165,7 @@
165165
"test:artifact-reference-dtos": "tsx tests/artifact-reference-dtos.test.ts",
166166
"test:artifact-path-primitives": "tsx tests/artifact-path-primitives.test.ts",
167167
"test:browser-callback-materialization-contracts": "tsx tests/browser-callback-materialization-contracts.test.ts",
168+
"test:browser-canonical-preview-origin": "tsx tests/browser-canonical-preview-origin.test.ts",
168169
"test:materialize-replay-package-command": "tsx tests/materialize-replay-package-command.test.ts",
169170
"test:source-package-compiler-primitives": "tsx tests/source-package-compiler-primitives.test.ts",
170171
"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
@@ -17,6 +17,7 @@ export interface BrowserPreviewNetworkPolicy {
1717
recordExternal: boolean
1818
stats: Map<string, { requests: number; external: boolean; blocked: number; routed: number }>
1919
routedRedirectEscapes: Array<{ rawOrigin: string; effectiveOrigin: string; reason: string }>
20+
preserveRoutedOrigin: boolean
2021
}
2122

2223
export interface BrowserPreviewNavigationDecision {
@@ -44,10 +45,20 @@ export interface BrowserPreviewTopology {
4445
preview: BrowserProbePreviewRouting
4546
networkPolicy: BrowserPreviewNetworkPolicy
4647
routedHosts: string[]
47-
origins: { localPreviewOrigin: string; requestedPreviewOrigin?: string; effectivePreviewOrigin: string }
48+
origins: BrowserPreviewOrigins
4849
navigationScope: BrowserPreviewNavigationScope
4950
resolveUrl(pathOrUrl: string): string
5051
authCookieUrls(targetUrls: string[]): string[]
52+
contextOptions(): { proxy?: { server: string } }
53+
}
54+
55+
export interface BrowserPreviewOrigins {
56+
localPreviewOrigin: string
57+
requestedPreviewOrigin?: string
58+
effectivePreviewOrigin: string
59+
canonicalBrowserOrigin: string
60+
localProxyOrigin: string
61+
upstreamRuntimeOrigin?: string
5162
}
5263

5364
export interface BrowserPreviewRouteTracker {
@@ -97,23 +108,27 @@ export function browserPreviewRouting(args: string[], runtimeSpec: RuntimeCreate
97108
}
98109
}
99110

100-
export function browserPreviewTopology(args: string[], runtimeSpec: RuntimeCreateSpec | undefined, localPreviewOrigin: string): BrowserPreviewTopology {
101-
const preview = browserPreviewRouting(args, runtimeSpec, localPreviewOrigin)
111+
export function browserPreviewTopology(args: string[], runtimeSpec: RuntimeCreateSpec | undefined, localPreviewOrigin: string, upstreamRuntimeOrigin?: string): BrowserPreviewTopology {
102112
const routedHosts = commaListArg(args, "route-host")
113+
const preview = browserPreviewRouting(args, runtimeSpec, localPreviewOrigin)
114+
applyCanonicalRoutedPreviewOrigin(preview, runtimeSpec?.preview?.siteUrl, routedHosts)
103115
const networkPolicy = browserPreviewNetworkPolicy(args, routedHosts, preview)
104116

105117
return {
106118
preview,
107119
networkPolicy,
108120
routedHosts,
109-
origins: browserPreviewOrigins(preview),
121+
origins: browserPreviewOrigins(preview, upstreamRuntimeOrigin),
110122
navigationScope: browserPreviewNavigationScope(preview.effectiveOrigin, networkPolicy),
111123
resolveUrl(pathOrUrl) {
112124
return resolveBrowserPreviewUrl(pathOrUrl, preview.effectiveOrigin)
113125
},
114126
authCookieUrls(targetUrls) {
115127
return browserPreviewAuthCookieUrls(localPreviewOrigin, routedHosts, targetUrls)
116128
},
129+
contextOptions() {
130+
return networkPolicy.preserveRoutedOrigin ? { proxy: { server: new URL(localPreviewOrigin).origin } } : {}
131+
},
117132
}
118133
}
119134

@@ -146,14 +161,52 @@ export function browserPreviewNavigationScope(effectivePreviewOrigin: string, po
146161
}
147162
}
148163

149-
export function browserPreviewOrigins(preview: BrowserProbePreviewRouting): { localPreviewOrigin: string; requestedPreviewOrigin?: string; effectivePreviewOrigin: string } {
164+
export function browserPreviewOrigins(preview: BrowserProbePreviewRouting, upstreamRuntimeOrigin?: string): BrowserPreviewOrigins {
150165
return {
151166
localPreviewOrigin: preview.localOrigin,
152-
requestedPreviewOrigin: preview.publicOrigin,
167+
...(preview.publicOrigin ? { requestedPreviewOrigin: preview.publicOrigin } : {}),
153168
effectivePreviewOrigin: preview.effectiveOrigin,
169+
canonicalBrowserOrigin: new URL(preview.effectiveOrigin).origin,
170+
localProxyOrigin: new URL(preview.localOrigin).origin,
171+
...(upstreamRuntimeOrigin ? { upstreamRuntimeOrigin: new URL(upstreamRuntimeOrigin).origin } : {}),
154172
}
155173
}
156174

175+
function applyCanonicalRoutedPreviewOrigin(preview: BrowserProbePreviewRouting, siteUrl: string | undefined, routedHosts: string[]): void {
176+
if (preview.effectiveMode !== "local" || !siteUrl) {
177+
return
178+
}
179+
180+
let canonical: URL
181+
try {
182+
canonical = new URL(siteUrl)
183+
} catch {
184+
return
185+
}
186+
const host = normalizeBrowserPreviewHost(canonical.hostname)
187+
if (!routedHosts.map(normalizeBrowserPreviewHost).includes(host)) {
188+
return
189+
}
190+
191+
if (canonical.protocol !== "http:") {
192+
preview.diagnostics.push({
193+
code: "preview-canonical-origin-preservation-inconclusive",
194+
severity: "error",
195+
message: "The local Playground provider cannot preserve this declared canonical preview protocol.",
196+
details: { status: "inconclusive", canonicalOrigin: canonical.origin, supportedProtocols: ["http:"] },
197+
})
198+
return
199+
}
200+
201+
preview.effectiveOrigin = canonical.toString()
202+
preview.diagnostics.push({
203+
code: "preview-canonical-routed-origin",
204+
severity: "info",
205+
message: "The declared routed preview alias is the browser-visible origin.",
206+
details: { canonicalOrigin: canonical.origin, localProxyOrigin: new URL(preview.localOrigin).origin },
207+
})
208+
}
209+
157210
export function browserPreviewReadinessError(preview: BrowserProbePreviewRouting): Error | undefined {
158211
const diagnostic = preview.diagnostics.find((item) => item.severity === "error")
159212
if (!diagnostic) {
@@ -218,6 +271,7 @@ export function browserPreviewNetworkPolicy(args: string[], routeHosts: string[]
218271
recordExternal: strictBooleanArg(args, "record-external", false),
219272
stats: new Map(),
220273
routedRedirectEscapes: [],
274+
preserveRoutedOrigin: new URL(preview.effectiveOrigin).origin !== new URL(preview.localOrigin).origin && preview.effectiveMode === "local",
221275
}
222276
}
223277

@@ -321,28 +375,38 @@ async function routeBrowserPreviewNetwork(routePattern: (url: string, handler: (
321375
stat.requests += 1
322376
stat.external = !policy.firstPartyHosts.has(host)
323377

324-
if (policy.blockHosts.has(host) || (policy.mode === "block" && stat.external && !policy.allowHosts.has(host))) {
378+
if (policy.blockHosts.has(host)) {
325379
stat.blocked += 1
326380
await route.abort("blockedbyclient")
327381
return
328382
}
329383

330-
if (!policy.routeHosts.has(host)) {
331-
await route.continue()
384+
if (policy.routeHosts.has(host)) {
385+
stat.routed += 1
386+
if (policy.preserveRoutedOrigin) {
387+
await route.continue()
388+
return
389+
}
390+
const task = fulfillBrowserPreviewRoutedHost(route, requestUrl, policy, origin)
391+
tracker?.pending.add(task)
392+
try {
393+
await task
394+
} catch (error) {
395+
tracker?.errors.push(sanitizeBrowserPreviewRouteError(error))
396+
await route.abort("failed").catch(() => undefined)
397+
} finally {
398+
tracker?.pending.delete(task)
399+
}
332400
return
333401
}
334402

335-
stat.routed += 1
336-
const task = fulfillBrowserPreviewRoutedHost(route, requestUrl, policy, origin)
337-
tracker?.pending.add(task)
338-
try {
339-
await task
340-
} catch (error) {
341-
tracker?.errors.push(sanitizeBrowserPreviewRouteError(error))
342-
await route.abort("failed").catch(() => undefined)
343-
} finally {
344-
tracker?.pending.delete(task)
403+
if (policy.preserveRoutedOrigin || (policy.mode === "block" && stat.external && !policy.allowHosts.has(host)) || (request.resourceType() === "document" && stat.external)) {
404+
stat.blocked += 1
405+
await route.abort("blockedbyclient")
406+
return
345407
}
408+
409+
await route.continue()
346410
})
347411
}
348412

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)