mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-03 00:02:08 +00:00
feat: stop cross-origin isolating the raw app editor (#11411)
* feat: stop cross-origin isolating the raw app editor Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166irwM15U8vZjc58MsU54U * docs: drop stale raw app editor reload comments Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166irwM15U8vZjc58MsU54U * feat: drop wm_coep from the default raw app embed snippet Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166irwM15U8vZjc58MsU54U * chore: pin the UI builder that type-checks without cross-origin isolation Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166irwM15U8vZjc58MsU54U * docs: name the proxy-isolated case behind the remaining isolation checks Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166irwM15U8vZjc58MsU54U --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2f87bc6a2d
commit
6064aecdec
@@ -51,30 +51,17 @@ const TWO_HUNDRED: &str = "200.html";
|
||||
/// Check if the original path requires cross-origin isolation headers.
|
||||
///
|
||||
/// CANONICAL COEP RATIONALE (the dev-server mirror in `frontend/vite.config.js`
|
||||
/// and the navigation guards in `frontend/src/routes/(root)/(logged)/+layout.svelte`
|
||||
/// point here): the headers are needed for SharedArrayBuffer and TypeScript
|
||||
/// workers (raw app editor at `/apps_raw/edit|add`, in-browser bundler at
|
||||
/// `/ui_builder/`). The raw app *viewer* (`/apps_raw/get/`) must NOT get them:
|
||||
/// COEP `require-corp` blocks the viewed app's cross-origin subresources
|
||||
/// (external images, embeds) that lack CORP — and since headers stick to the
|
||||
/// document, apps would break on a page reload while working when reached via
|
||||
/// client-side navigation.
|
||||
///
|
||||
/// Public apps (`/public/` and custom paths `/a/`) opt in via the `wm_coep`
|
||||
/// query param: a public (raw) app must set COEP to be embeddable as an iframe
|
||||
/// inside a cross-origin-isolated page (which requires the embedded document to
|
||||
/// also set COEP). It is opt-in rather than always-on because cross-origin
|
||||
/// isolation also blocks subresources without CORP (e.g. external image URLs
|
||||
/// or embeds used by classic apps), so we only enable it when the embedder
|
||||
/// explicitly requests it.
|
||||
/// points here). Only public apps (`/public/` and custom paths `/a/`) ever get
|
||||
/// them, and only when they opt in via the `wm_coep` query param: a public (raw)
|
||||
/// app must set COEP to be embeddable as an iframe inside a cross-origin-isolated
|
||||
/// page (which requires the embedded document to also set COEP). It is opt-in
|
||||
/// rather than always-on because COEP `require-corp` blocks subresources without
|
||||
/// CORP (external image URLs, embeds), and since headers stick to the document,
|
||||
/// isolating an SPA route leaks into every page reached from it client-side.
|
||||
#[cfg(feature = "static_frontend")]
|
||||
fn needs_cross_origin_isolation(original_path: &str, query: Option<&str>) -> bool {
|
||||
// no trailing slash on edit/add: matches the +layout.svelte guards
|
||||
original_path.starts_with("/apps_raw/edit")
|
||||
|| original_path.starts_with("/apps_raw/add")
|
||||
|| original_path.starts_with("/ui_builder/")
|
||||
|| ((original_path.starts_with("/public/") || original_path.starts_with("/a/"))
|
||||
&& query_has_flag(query, "wm_coep"))
|
||||
(original_path.starts_with("/public/") || original_path.starts_with("/a/"))
|
||||
&& query_has_flag(query, "wm_coep")
|
||||
}
|
||||
|
||||
/// Returns true if `query` contains the given flag key (with or without a
|
||||
@@ -98,8 +85,6 @@ fn serve_path(path: &str, original_path: &str, query: Option<&str>) -> Response<
|
||||
.header(header::CONTENT_TYPE, mime.as_ref())
|
||||
.header(header::ACCESS_CONTROL_ALLOW_ORIGIN, "*");
|
||||
|
||||
// Add cross-origin isolation headers only for paths that need them
|
||||
// (apps_raw editor needs SharedArrayBuffer for TypeScript workers)
|
||||
if needs_cross_origin_isolation(original_path, query) {
|
||||
res = res
|
||||
.header("Cross-Origin-Opener-Policy", "same-origin")
|
||||
@@ -167,12 +152,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_needs_cross_origin_isolation() {
|
||||
// editor + bundler are always isolated, regardless of query
|
||||
assert!(needs_cross_origin_isolation("/apps_raw/edit/foo", None));
|
||||
assert!(needs_cross_origin_isolation("/apps_raw/add", None));
|
||||
assert!(needs_cross_origin_isolation("/ui_builder/index.html", None));
|
||||
|
||||
// the raw app viewer must NOT be isolated
|
||||
// the raw app editor, its UI builder frames and the viewer are never isolated
|
||||
assert!(!needs_cross_origin_isolation("/apps_raw/edit/foo", None));
|
||||
assert!(!needs_cross_origin_isolation("/apps_raw/add", None));
|
||||
assert!(!needs_cross_origin_isolation(
|
||||
"/ui_builder/index.html",
|
||||
None
|
||||
));
|
||||
assert!(!needs_cross_origin_isolation(
|
||||
"/apps_raw/get/u/foo/bar",
|
||||
None
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"baseUrl": "https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev",
|
||||
"version": "60b6904",
|
||||
"sha256": "85b68e8418c97d96f3a419b3e345ef8a1987976943b8439a7df841f9217a9b34"
|
||||
"version": "9fddc52",
|
||||
"sha256": "b45558951f3d4c9dce7b88b5139d66b0f5f01fe561a22efcb0b8400a13240522"
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ os.chdir('ui_builder_serve')
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
class H(SimpleHTTPRequestHandler):
|
||||
def end_headers(self):
|
||||
self.send_header('Cross-Origin-Opener-Policy', 'same-origin')
|
||||
self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
|
||||
self.send_header('Cross-Origin-Resource-Policy', 'cross-origin')
|
||||
super().end_headers()
|
||||
HTTPServer(('', 4000), H).serve_forever()
|
||||
|
||||
@@ -76,10 +76,9 @@
|
||||
hideSecretUrl?: boolean
|
||||
preserveOnBehalfOf?: boolean
|
||||
labels?: string[] | undefined
|
||||
// Raw apps need cross-origin isolation (wm_coep) to be embeddable. Classic
|
||||
// (low-code) apps must NOT get the flag — it would force COEP on the
|
||||
// document and break no-CORP cross-origin subresources (external images,
|
||||
// {@html} embeds, CDN imports).
|
||||
// Only raw apps are offered the `wm_coep` embedding hint: on a classic
|
||||
// (low-code) app COEP breaks no-CORP cross-origin subresources (external
|
||||
// images, {@html} embeds, CDN imports) that the app cannot opt out of.
|
||||
rawApp?: boolean
|
||||
/** True while the editor is on a draft-only URL (`/edit/u/{user}/draft_{uuid}`
|
||||
* with no deployed row yet). Suppresses the public-secret-URL fetch
|
||||
@@ -195,15 +194,9 @@
|
||||
// token as a seatless guest. Uses the custom URL when set, else the public secret URL.
|
||||
let guestJwtBase = $derived(customPath !== undefined ? fullCustomUrl : secretUrlHref)
|
||||
|
||||
// When embedding a raw app in an iframe inside another Windmill app (or any
|
||||
// cross-origin-isolated page), the embedded document must set COEP. The
|
||||
// `wm_coep` flag opts the public app into the cross-origin isolation headers.
|
||||
// Only raw apps get it — for classic (low-code) apps COEP would break
|
||||
// no-CORP cross-origin subresources, so their snippet stays a plain iframe.
|
||||
let embedMode = $state(false)
|
||||
function toEmbedSnippet(url: string): string {
|
||||
const finalUrl = rawApp ? `${url}${url.includes('?') ? '&' : '?'}wm_coep=on` : url
|
||||
return `<iframe src="${finalUrl}" title="Windmill app" width="100%" height="600"></iframe>`
|
||||
return `<iframe src="${url}" title="Windmill app" width="100%" height="600"></iframe>`
|
||||
}
|
||||
async function getSecretUrl() {
|
||||
secretUrl = await AppService.getPublicSecretOfApp({
|
||||
@@ -568,11 +561,12 @@
|
||||
{#if embedMode}
|
||||
Paste this iframe snippet into another app.
|
||||
{#if rawApp}
|
||||
The <code>wm_coep</code> flag <Tooltip
|
||||
>Sets the cross-origin isolation headers (COEP) so the app can be embedded inside
|
||||
another Windmill app or any cross-origin-isolated page. Without it the browser blocks
|
||||
the iframe.</Tooltip
|
||||
> lets it load inside a cross-origin-isolated page.
|
||||
To embed it in a cross-origin isolated page, add <code>wm_coep=on</code>
|
||||
<Tooltip
|
||||
>Sets the cross-origin isolation headers (COEP) on the app, which such a page requires
|
||||
of its iframes. The app then only loads cross-origin resources that send CORS or a
|
||||
Cross-Origin-Resource-Policy header.</Tooltip
|
||||
> to the URL.
|
||||
{/if}
|
||||
(if requiring login, top-level domain of embedding app must be the same as the one of Windmill)
|
||||
{:else}
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
// consulted hostname to a third party from the user's browser — an accepted
|
||||
// tradeoff for now (blocked/air-gapped environments degrade to the Globe
|
||||
// icon via onerror). Hit gstatic directly rather than www.google.com/s2/
|
||||
// favicons: the app is served with COEP require-corp, and the s2 redirect
|
||||
// hop carries no Cross-Origin-Resource-Policy header, so the browser blocks
|
||||
// the image. The gstatic endpoint itself responds with CORP: cross-origin.
|
||||
// favicons: the s2 redirect hop carries no Cross-Origin-Resource-Policy
|
||||
// header, so a cross-origin isolated page would block the image. The
|
||||
// gstatic endpoint itself responds with CORP: cross-origin.
|
||||
function faviconUrl(hostname: string): string {
|
||||
return `https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=https://${encodeURIComponent(hostname)}&size=64`
|
||||
}
|
||||
|
||||
@@ -132,9 +132,8 @@ export function bundleRawAppFiles({
|
||||
|
||||
iframe.title = 'Raw app bundler'
|
||||
iframe.tabIndex = -1
|
||||
// No `credentialless`: /ui_builder/* is itself served with COEP=require-corp, so it
|
||||
// embeds in isolated pages as is, while a credentialless frame gets an empty cookie
|
||||
// jar, which would leave its npm installer unauthenticated against /api/w/*.
|
||||
// No `credentialless`: a credentialless frame gets an empty cookie jar, which would
|
||||
// leave its npm installer unauthenticated against /api/w/*.
|
||||
iframe.style.position = 'fixed'
|
||||
iframe.style.width = '0'
|
||||
iframe.style.height = '0'
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
async function importRaw() {
|
||||
const parsed = importType === 'yaml' ? YAML.parse(pendingRaw) : JSON.parse(pendingRaw)
|
||||
if (appKind === 'fullcode') {
|
||||
// Navigation to /apps_raw/add triggers a full page reload (for cross-origin isolation),
|
||||
// so the in-memory importStore would be lost. Use sessionStorage instead.
|
||||
// The raw app editor reads the payload from sessionStorage on mount.
|
||||
sessionStorage.setItem('rawAppImport', JSON.stringify(parsed))
|
||||
await goto('/apps_raw/add')
|
||||
} else {
|
||||
|
||||
@@ -405,8 +405,7 @@
|
||||
importScriptStore.set(parsed)
|
||||
await goto(`${base}/scripts/add?import=true`)
|
||||
} else if (importKind === 'app-fullcode') {
|
||||
// /apps_raw/add does a full reload (cross-origin isolation), so the in-memory
|
||||
// store would be lost — hand the payload over via sessionStorage instead.
|
||||
// The raw app editor reads the payload from sessionStorage on mount.
|
||||
sessionStorage.setItem('rawAppImport', JSON.stringify(parsed))
|
||||
await goto(`${base}/apps_raw/add`)
|
||||
} else {
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Modal from '../common/modal/Modal.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
|
||||
interface Props {
|
||||
/** Inline preview iframe rendering the user's app (same-origin app-preview.html) */
|
||||
iframe: HTMLIFrameElement | undefined
|
||||
}
|
||||
|
||||
let { iframe }: Props = $props()
|
||||
|
||||
let open = $state(false)
|
||||
|
||||
// One toast per remote origin — a page pulling many assets from one host
|
||||
// should not stack toasts.
|
||||
const warnedOrigins = new Set<string>()
|
||||
|
||||
const RESOURCE_TAGS = new Set(['IMG', 'SCRIPT', 'LINK', 'AUDIO', 'VIDEO', 'SOURCE', 'IFRAME'])
|
||||
|
||||
// The editor is cross-origin isolated (COEP require-corp, needed for the
|
||||
// SharedArrayBuffer-based TS workers), and that extends to the nested preview:
|
||||
// cross-origin resources lacking CORS/CORP headers are blocked there while the
|
||||
// deployed app (served without COEP) loads them fine — warn instead of staying silent.
|
||||
function onResourceError(e: Event) {
|
||||
// Elements belong to the preview window's realm, so no instanceof checks —
|
||||
// duck-type instead. Resource load errors don't bubble but do capture
|
||||
// through the window; runtime ErrorEvents target the window itself and are
|
||||
// skipped by the tagName check.
|
||||
const el = e.target as (Element & { currentSrc?: string; src?: string; href?: string }) | null
|
||||
if (!el?.tagName || !RESOURCE_TAGS.has(el.tagName)) return
|
||||
const raw = el.currentSrc || el.src || el.href
|
||||
if (typeof raw !== 'string' || !raw) return
|
||||
let url: URL
|
||||
try {
|
||||
url = new URL(raw)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
// Same-origin failures (plain 404s) and non-http(s) schemes are not COEP blocks.
|
||||
if (!/^https?:$/.test(url.protocol) || url.origin === window.location.origin) return
|
||||
if (warnedOrigins.has(url.origin)) return
|
||||
warnedOrigins.add(url.origin)
|
||||
// `error` events carry no failure reason, so a 404/DNS failure on a
|
||||
// cross-origin URL looks identical to a COEP block — hedge the wording.
|
||||
sendUserToast(
|
||||
`Cross-origin resource (${url.host}) failed to load — likely the editor's COEP/CORS isolation. A valid URL will still load on the deployed app.`,
|
||||
'warning',
|
||||
[{ label: 'Read more', callback: () => (open = true) }],
|
||||
undefined,
|
||||
10000
|
||||
)
|
||||
}
|
||||
|
||||
export function attachTo(win: Window | null | undefined) {
|
||||
win?.addEventListener('error', onResourceError, true)
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const el = iframe
|
||||
if (!el) return
|
||||
const attach = () => attachTo(el.contentWindow)
|
||||
el.addEventListener('load', attach)
|
||||
// The iframe may already be loaded when this mounts; addEventListener
|
||||
// dedupes the (handler, capture) pair, so the load event re-firing on the
|
||||
// same window cannot double-attach.
|
||||
if (el.contentDocument?.readyState === 'complete') attach()
|
||||
return () => el.removeEventListener('load', attach)
|
||||
})
|
||||
</script>
|
||||
|
||||
<Modal bind:open title="Cross-origin resources in the editor preview" kind="X">
|
||||
<div class="flex flex-col gap-3">
|
||||
<p>
|
||||
The app editor runs in a <b>cross-origin isolated</b> context (COOP/COEP headers). This is
|
||||
required for <code>SharedArrayBuffer</code>, which powers the TypeScript language workers and
|
||||
lets the editor build and preview your frontend live in the browser.
|
||||
</p>
|
||||
<p>
|
||||
A side effect is that the browser refuses to load cross-origin resources (images, scripts,
|
||||
stylesheets, media…) unless the remote server explicitly opts in with CORS or a
|
||||
<code>Cross-Origin-Resource-Policy</code> header. Resources from servers that don't are
|
||||
blocked <b>in the editor preview only</b>. When this is the cause, the browser console shows
|
||||
<code>ERR_BLOCKED_BY_RESPONSE</code> — a plain 404 or DNS error instead means the URL itself is
|
||||
broken and will fail on the deployed app too.
|
||||
</p>
|
||||
<p>
|
||||
The deployed app is served without these headers, so the same resources load normally there —
|
||||
open the deployed app link to verify. If you control the remote server, sending
|
||||
<code>Cross-Origin-Resource-Policy: cross-origin</code> makes the resource load in the editor too.
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -29,7 +29,6 @@
|
||||
} from './utils'
|
||||
import { runDomQueryOnHtml, type RawAppDomQuery, type RawAppDomRequester } from './rawAppDom'
|
||||
import InlineElementPrompt from './InlineElementPrompt.svelte'
|
||||
import RawAppCoepWarning from './RawAppCoepWarning.svelte'
|
||||
import DarkModeObserver from '../DarkModeObserver.svelte'
|
||||
import { getAppliedDarkModeVariant, type DarkModeVariant } from '$lib/darkModeVariant'
|
||||
import RawAppSidebar from './RawAppSidebar.svelte'
|
||||
@@ -383,7 +382,6 @@
|
||||
let iframe: HTMLIFrameElement | undefined = $state(undefined)
|
||||
const PREVIEW_SHELL_URL = '/ui_builder/app-preview.html'
|
||||
let previewIframe: HTMLIFrameElement | undefined = $state(undefined)
|
||||
let coepWarning: RawAppCoepWarning | undefined = $state(undefined)
|
||||
let previewIframeLoaded = $state(false)
|
||||
let lastBuild: { css: string; js: string } | undefined = undefined
|
||||
// Detached preview tab/window rendering the same app-preview bundle as the
|
||||
@@ -396,6 +394,10 @@
|
||||
let externalPreviewReady = $state(false)
|
||||
let inspectorEnabled = $state(false)
|
||||
let bundlerType: 'esbuild' | 'rolldown' = $state('esbuild')
|
||||
// rolldown's wasm build uses shared memory, which only exists in a cross-origin
|
||||
// isolated document. Windmill never serves the editor isolated, so the switch
|
||||
// only shows behind a reverse proxy that adds COOP/COEP to the whole site.
|
||||
const rolldownAvailable = globalThis.crossOriginIsolated === true
|
||||
|
||||
// Build/bundler logs forwarded from the UI Builder iframe. We render
|
||||
// them as an overlay inside the preview pane (right side) so they're
|
||||
@@ -1338,9 +1340,6 @@
|
||||
) {
|
||||
externalPreviewReady = true
|
||||
feedExternalPreview()
|
||||
// The detached window is cross-origin isolated like the inline preview,
|
||||
// so blocked external resources warrant the same COEP warning.
|
||||
coepWarning?.attachTo(externalPreviewWindow)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1677,9 +1676,6 @@
|
||||
win.addEventListener('load', () => {
|
||||
externalPreviewReady = true
|
||||
feedExternalPreview()
|
||||
// Attach here too: against an artifact that predates the handshake, this
|
||||
// is the only place the freshly opened window is ever seen loaded.
|
||||
coepWarning?.attachTo(win)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2620,21 +2616,23 @@
|
||||
>
|
||||
{#snippet trailing()}
|
||||
<div class="flex items-center gap-1 px-2">
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
title="Switch bundler"
|
||||
onClick={() => {
|
||||
const next = bundlerType === 'esbuild' ? 'rolldown' : 'esbuild'
|
||||
bundlerType = next
|
||||
iframe?.contentWindow?.postMessage(
|
||||
{ type: 'setBundlerType', bundlerType: next },
|
||||
'*'
|
||||
)
|
||||
}}
|
||||
>
|
||||
{bundlerType}
|
||||
</Button>
|
||||
{#if rolldownAvailable}
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
title="Switch bundler"
|
||||
onClick={() => {
|
||||
const next = bundlerType === 'esbuild' ? 'rolldown' : 'esbuild'
|
||||
bundlerType = next
|
||||
iframe?.contentWindow?.postMessage(
|
||||
{ type: 'setBundlerType', bundlerType: next },
|
||||
'*'
|
||||
)
|
||||
}}
|
||||
>
|
||||
{bundlerType}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
variant="subtle"
|
||||
unifiedSize="sm"
|
||||
@@ -2704,7 +2702,6 @@
|
||||
src={PREVIEW_SHELL_URL}
|
||||
class="w-full flex-1 block"
|
||||
></iframe>
|
||||
<RawAppCoepWarning bind:this={coepWarning} iframe={previewIframe} />
|
||||
{#if buildError}
|
||||
<!-- top-12 clears the tab bar; `before:bg-surface` backs the
|
||||
Alert's translucent red; `isolate` pins the pseudo's stacking context. -->
|
||||
|
||||
@@ -94,8 +94,9 @@
|
||||
// to the wrapper document: under a COEP `require-corp` embedder, a nested
|
||||
// document is only allowed to load if it asserts COEP itself, so the
|
||||
// backend adds the header when the flag is present. Also request it when
|
||||
// this document is itself cross-origin isolated (e.g. the raw app editor)
|
||||
// — the wrapper would otherwise be blocked outright, URL flag or not.
|
||||
// this document is itself cross-origin isolated without the flag (a proxy
|
||||
// adding COOP/COEP to the whole site) — the wrapper would otherwise be
|
||||
// blocked outright.
|
||||
const coep =
|
||||
new URLSearchParams(window.location.search).has('wm_coep') || window.crossOriginIsolated
|
||||
? 'wm_coep=1&'
|
||||
|
||||
@@ -422,8 +422,7 @@
|
||||
// Inside a sessions-preview iframe, hand an editor-route navigation up to the
|
||||
// parent so it mounts the in-process editor (sharing the session runtime)
|
||||
// instead of booting a second, disconnected editor in this frame. Cancel so
|
||||
// the heavy editor never mounts here at all. Runs before the apps_raw reload
|
||||
// below so a raw-app editor promotes rather than full-reloading the iframe.
|
||||
// the heavy editor never mounts here at all.
|
||||
if (isSessionPreviewFrame()) {
|
||||
const target = previewEditorTarget(navigation.to?.url)
|
||||
if (target) {
|
||||
@@ -463,37 +462,6 @@
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Force page reload when navigating to /apps_raw/add or /apps_raw/edit
|
||||
// This ensures the cross-origin isolation headers are fetched from the server
|
||||
// which are required for SharedArrayBuffer and TypeScript workers to work correctly
|
||||
const toPath = navigation.to?.url.pathname
|
||||
const currentPath = navigation.from?.url.pathname
|
||||
const isEditorPath = (p: string | undefined) =>
|
||||
!!p && (p.startsWith('/apps_raw/add') || p.startsWith('/apps_raw/edit'))
|
||||
if (isEditorPath(toPath)) {
|
||||
// Reload if we're not on an apps_raw path, or if we're on the raw app viewer
|
||||
// (/apps_raw/get/): the viewer doesn't have cross-origin isolation headers, so
|
||||
// we need a full reload to fetch them for the editor.
|
||||
if (!currentPath?.startsWith('/apps_raw/') || currentPath?.startsWith('/apps_raw/get/')) {
|
||||
navigation.cancel()
|
||||
window.location.href = navigation.to!.url.href
|
||||
}
|
||||
} else if (toPath && isEditorPath(currentPath)) {
|
||||
// Reverse of the guard above: leaving the isolated editor document must
|
||||
// also fully reload, or its COEP header sticks for the rest of the SPA
|
||||
// session and blocks CORP-less cross-origin subresources (e.g. images in
|
||||
// a viewed app — see needs_cross_origin_isolation in static_assets.rs).
|
||||
// Key off the path, never `window.crossOriginIsolated`: a deployment may
|
||||
// isolate the whole site (frontend/static/_headers does, for Cloudflare
|
||||
// Pages), and there the flag is true on every page — turning every
|
||||
// navigation into a full page load, while the reload it forces cannot
|
||||
// clear an isolation the next document asserts too. Among the routes this
|
||||
// layout governs, only the editor is served the headers, so entering it is
|
||||
// the only way into an isolated document here.
|
||||
navigation.cancel()
|
||||
window.location.href = navigation.to!.url.href
|
||||
}
|
||||
})
|
||||
|
||||
let innerWidth = $state(BROWSER ? window.innerWidth : 2000)
|
||||
|
||||
@@ -233,9 +233,8 @@
|
||||
// Explicit path seed: the fork-a-draft handoff re-homes the source
|
||||
// path into the forker's namespace and passes it here.
|
||||
const pathParam = page.url.searchParams.get('seed_path')
|
||||
// One-shot YAML/JSON import handoff. Carried via $importStore, or
|
||||
// sessionStorage when /apps_raw's full page reload would drop in-memory
|
||||
// state. Wrapped exports carry { summary, value, policy }; bare ones the value.
|
||||
// One-shot YAML/JSON import handoff, carried via $importStore or
|
||||
// sessionStorage. Wrapped exports carry { summary, value, policy }; bare ones the value.
|
||||
let importRaw: any = $importStore
|
||||
if ($importStore) {
|
||||
$importStore = undefined
|
||||
|
||||
@@ -371,8 +371,7 @@
|
||||
buttonProps: {
|
||||
onClick: async () => {
|
||||
const app = createRawAppFromFlow(flow.path, flow.summary, flow.schema)
|
||||
// /apps_raw/add hard-reloads (cross-origin isolation), so the
|
||||
// in-memory importStore would be dropped; hand off via sessionStorage.
|
||||
// The raw app editor reads the payload from sessionStorage on mount.
|
||||
sessionStorage.setItem('rawAppImport', JSON.stringify(app))
|
||||
await goto('/apps_raw/add')
|
||||
},
|
||||
|
||||
@@ -534,8 +534,7 @@
|
||||
buttonProps: {
|
||||
onClick: async () => {
|
||||
const app = createRawAppFromScript(script.path, script.summary, script.schema)
|
||||
// /apps_raw/add hard-reloads (cross-origin isolation), so the
|
||||
// in-memory importStore would be dropped; hand off via sessionStorage.
|
||||
// The raw app editor reads the payload from sessionStorage on mount.
|
||||
sessionStorage.setItem('rawAppImport', JSON.stringify(app))
|
||||
await goto('/apps_raw/add')
|
||||
},
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
/*
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
@@ -199,17 +199,14 @@ function isolateAuthCookie(proxy) {
|
||||
|
||||
// Cross-origin isolation headers, scoped to mirror the production predicate —
|
||||
// see `needs_cross_origin_isolation` in backend/windmill-api/src/static_assets.rs
|
||||
// for which paths need them and why the raw app viewer must be excluded.
|
||||
// for why only opted-in public apps get them.
|
||||
// `enforce: 'pre'` so these headers are set before SvelteKit's sirv static
|
||||
// handler serves `static/` files and ends the response without calling next().
|
||||
function needsCrossOriginIsolation(url) {
|
||||
const [path, query = ''] = url.split('?')
|
||||
return (
|
||||
path.startsWith('/apps_raw/edit') ||
|
||||
path.startsWith('/apps_raw/add') ||
|
||||
path.startsWith('/ui_builder/') ||
|
||||
((path.startsWith('/public/') || path.startsWith('/a/')) &&
|
||||
new URLSearchParams(query).has('wm_coep'))
|
||||
(path.startsWith('/public/') || path.startsWith('/a/')) &&
|
||||
new URLSearchParams(query).has('wm_coep')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -313,8 +310,6 @@ const config = {
|
||||
target: 'http://localhost:4000',
|
||||
changeOrigin: true,
|
||||
headers: {
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
'Cross-Origin-Resource-Policy': 'cross-origin'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,6 @@ export async function onRequest(context) {
|
||||
);
|
||||
|
||||
newResponse.headers.set("Cross-Origin-Resource-Policy", "cross-origin");
|
||||
newResponse.headers.set("Cross-Origin-Opener-Policy", "same-origin");
|
||||
newResponse.headers.set("Cross-Origin-Embedder-Policy", "require-corp");
|
||||
return newResponse;
|
||||
} catch (e) {
|
||||
return new Response(e.message, { status: 500 });
|
||||
|
||||
Reference in New Issue
Block a user