feat(core): render HTML artifacts inline without a remote container - #5
Merged
Merged
Conversation
Artifact previews loaded https://affine.run/static/container.html and posted the HTML into it. That remote page was the isolation boundary, so every preview depended on affine.run being reachable — blank previews on self-hosted deploys, offline sessions, or any network hiccup. Render locally via srcdoc instead, with a sandbox that grants allow-scripts but deliberately omits allow-same-origin. The frame gets an opaque origin, so artifacts stay interactive while losing all access to the host document, storage, cookies, and auth. This is stricter isolation than the remote flow it replaces, which ran with allow-same-origin and performed no origin check on incoming messages. An injected bootstrap reports content height, readiness, and errors over postMessage. The host authenticates those by window handle (event.source === iframe.contentWindow), since an opaque guest must post with a wildcard target and the marker alone is guessable. Sizing is gated behind a new autoResize property. The chat preview panel stretches the frame to height:100% via CSS, so writing an inline height there would override that rule and break the panel; code-artifact.ts passes autoResize=false. Doc-inline previews size to content, clamped to 640px, replacing the hard-coded 544px. Code/Preview toggling previously swapped between two different lit templates, which tore down the iframe and discarded artifact state on every toggle. Both views now stay mounted, toggled with [hidden] and keyed by toolCallId. Verified in real Chromium (artifact-sandbox.integration.spec.ts): scripts and forms work, zero network requests, window.origin is 'null', the guest cannot reach parent.document or host storage/cookies, and state survives a hidden toggle but resets on re-creation. A dedicated group adds allow-same-origin in the test only to prove that flag is what would collapse the boundary. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Renders AI HTML artifacts locally instead of loading the remote page
https://affine.run/static/container.htmland posting the HTML into it.That remote page was the isolation boundary — it ran artifact scripts on a foreign origin. The cost was a hard runtime dependency on affine.run for what is otherwise a purely local render, which means blank previews on a self-hosted deploy, offline, or on any network hiccup.
How
srcdocreplaces the remotesrc+ cross-windowpostMessagehandshake.allow-scriptsbut deliberately omitsallow-same-origin. The frame gets an opaque origin, so artifacts stay interactive while losing all access to the host document, storage, cookies, and auth. This is stricter than what it replaces — the old flow ran withallow-same-origin, and the remote container performed no origin check on incoming messages.postMessage. The host authenticates by window handle (event.source === iframe.contentWindow), because an opaque guest must post with a wildcard target and the marker alone is guessable.autoResizeprop. The chat preview panel stretches the frame toheight:100%via CSS, so writing an inline height there would override that rule and break the panel —code-artifact.tspassesautoResize=false. Doc-inline previews size to content, clamped to 640px, replacing the hard-coded 544px.[hidden]and keyed bytoolCallId.Verification
Unit + real-Chromium integration (
artifact-sandbox.integration.spec.ts), 74 new tests:window.originis'null'; guest cannot reachparent.document, hostlocalStorage, or cookieshiddentoggle; resets on re-creation (the old behaviour, kept as a test)allow-same-originin the test only to prove that flag is what would collapse the boundary; a third test asserts production never grants itChecked on this branch:
blocksuitespecsyarn affine @affine/web buildaffine.run/static/containeraffine-artifactbootstraptsc -b@affine/auth.d.tsnot built locally); no Electron job in the self-host imageNotably, the browser enforces this harder than specified: Chromium throws
SecurityErrorondocument.cookiein the guest rather than returning empty.Deploy impact
Merging this updates
requesty-provider-spec, which triggersrequesty-selfhost-image.yml→ pushesghcr.io/zebster-cmd/affine:requestyfor Coolify to redeploy.Known limitations
affine:code previewblock) are verified at the renderer level, not by driving a running AFFiNE instance with a live AI response.localStoragewill now throw instead of silently working, since the guest is on an opaque origin. Judged acceptable for one-shot generated artifacts.Planning artifacts:
openspec/changes/add-inline-html-renderer/🤖 Generated with Claude Code