diff --git a/backend/windmill-api/src/static_assets.rs b/backend/windmill-api/src/static_assets.rs
index 22645a2ac4..66dc4da9df 100644
--- a/backend/windmill-api/src/static_assets.rs
+++ b/backend/windmill-api/src/static_assets.rs
@@ -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
diff --git a/frontend/scripts/ui_builder_artifact.json b/frontend/scripts/ui_builder_artifact.json
index 68c1c558b1..8940f265ce 100644
--- a/frontend/scripts/ui_builder_artifact.json
+++ b/frontend/scripts/ui_builder_artifact.json
@@ -1,5 +1,5 @@
{
"baseUrl": "https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev",
- "version": "60b6904",
- "sha256": "85b68e8418c97d96f3a419b3e345ef8a1987976943b8439a7df841f9217a9b34"
+ "version": "9fddc52",
+ "sha256": "b45558951f3d4c9dce7b88b5139d66b0f5f01fe561a22efcb0b8400a13240522"
}
diff --git a/frontend/serve_ui_builder.sh b/frontend/serve_ui_builder.sh
index e396c6f1b4..4bf7e48a4d 100755
--- a/frontend/serve_ui_builder.sh
+++ b/frontend/serve_ui_builder.sh
@@ -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()
diff --git a/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte
index 4f27eb6b26..d062247fa4 100644
--- a/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte
+++ b/frontend/src/lib/components/apps/editor/AppEditorHeaderDeploy.svelte
@@ -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 ``
+ return ``
}
async function getSecretUrl() {
secretUrl = await AppService.getPublicSecretOfApp({
@@ -568,11 +561,12 @@
{#if embedMode}
Paste this iframe snippet into another app.
{#if rawApp}
- The wm_coep flag 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. lets it load inside a cross-origin-isolated page.
+ To embed it in a cross-origin isolated page, add wm_coep=on
+ 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. to the URL.
{/if}
(if requiring login, top-level domain of embedding app must be the same as the one of Windmill)
{:else}
diff --git a/frontend/src/lib/components/copilot/chat/WebSearchSourcesDisplay.svelte b/frontend/src/lib/components/copilot/chat/WebSearchSourcesDisplay.svelte
index 0808d6e172..191f6c7267 100644
--- a/frontend/src/lib/components/copilot/chat/WebSearchSourcesDisplay.svelte
+++ b/frontend/src/lib/components/copilot/chat/WebSearchSourcesDisplay.svelte
@@ -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`
}
diff --git a/frontend/src/lib/components/copilot/chat/global/rawAppBundlerBridge.ts b/frontend/src/lib/components/copilot/chat/global/rawAppBundlerBridge.ts
index dd7c04c29a..62dce8af5c 100644
--- a/frontend/src/lib/components/copilot/chat/global/rawAppBundlerBridge.ts
+++ b/frontend/src/lib/components/copilot/chat/global/rawAppBundlerBridge.ts
@@ -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'
diff --git a/frontend/src/lib/components/flows/CreateActionsApp.svelte b/frontend/src/lib/components/flows/CreateActionsApp.svelte
index 7ae5e9850c..d75d1b33d3 100644
--- a/frontend/src/lib/components/flows/CreateActionsApp.svelte
+++ b/frontend/src/lib/components/flows/CreateActionsApp.svelte
@@ -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 {
diff --git a/frontend/src/lib/components/home/CreateActionsMenu.svelte b/frontend/src/lib/components/home/CreateActionsMenu.svelte
index 34e1de8494..a400a73df0 100644
--- a/frontend/src/lib/components/home/CreateActionsMenu.svelte
+++ b/frontend/src/lib/components/home/CreateActionsMenu.svelte
@@ -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 {
diff --git a/frontend/src/lib/components/raw_apps/RawAppCoepWarning.svelte b/frontend/src/lib/components/raw_apps/RawAppCoepWarning.svelte
deleted file mode 100644
index fca969de27..0000000000
--- a/frontend/src/lib/components/raw_apps/RawAppCoepWarning.svelte
+++ /dev/null
@@ -1,92 +0,0 @@
-
-
-
-
-
- The app editor runs in a cross-origin isolated context (COOP/COEP headers). This is
- required for SharedArrayBuffer, which powers the TypeScript language workers and
- lets the editor build and preview your frontend live in the browser.
-
-
- 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
- Cross-Origin-Resource-Policy header. Resources from servers that don't are
- blocked in the editor preview only. When this is the cause, the browser console shows
- ERR_BLOCKED_BY_RESPONSE — a plain 404 or DNS error instead means the URL itself is
- broken and will fail on the deployed app too.
-
-
- 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
- Cross-Origin-Resource-Policy: cross-origin makes the resource load in the editor too.
-
-
-
diff --git a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte
index 78d3ba8102..98c436b0bd 100644
--- a/frontend/src/lib/components/raw_apps/RawAppEditor.svelte
+++ b/frontend/src/lib/components/raw_apps/RawAppEditor.svelte
@@ -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()}