mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 08:02:40 +00:00
feat(frontend): warn when COEP blocks cross-origin resources in raw app editor preview
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gr3udvsSKYyH6mnDvGDqEE
This commit is contained in:
co-authored by
Claude Fable 5
parent
68debab877
commit
bfab8bc841
@@ -0,0 +1,90 @@
|
||||
<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 page is cross-origin isolated (COEP require-corp — required for
|
||||
// the SharedArrayBuffer-based TS workers and in-browser bundling), and COEP
|
||||
// forces every nested document, including the preview, to be isolated too. So
|
||||
// cross-origin resources without CORS/CORP headers are blocked in the preview
|
||||
// while loading fine on the deployed app: surface that as a warning instead
|
||||
// of a silently broken image.
|
||||
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)
|
||||
sendUserToast(
|
||||
`The editor preview blocked a cross-origin resource (${url.host}) due to COEP/CORS isolation. It will load normally 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>.
|
||||
</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>
|
||||
@@ -28,6 +28,7 @@
|
||||
} 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 RawAppSidebar from './RawAppSidebar.svelte'
|
||||
import type { Modules } from './RawAppModules.svelte'
|
||||
@@ -299,6 +300,7 @@
|
||||
|
||||
let iframe: HTMLIFrameElement | undefined = $state(undefined)
|
||||
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
|
||||
@@ -1104,6 +1106,9 @@
|
||||
e.origin === window.location.origin
|
||||
) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -2324,6 +2329,7 @@
|
||||
src="/ui_builder/app-preview.html"
|
||||
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. -->
|
||||
|
||||
Reference in New Issue
Block a user