// its DIRECT parent over the relay protocol below — so each frame
// navigation stays same-origin with its initiator and a deduped page
// loads at any depth on any host, including a file:// download's
// opaque origins (where a blob minted by one document cannot be
// navigated to by another).
const pageOrderEl = document.querySelector('script[type="__bundler/page_order"]');
const pageOrder = pageOrderEl ? JSON.parse(pageOrderEl.textContent) : [];
const pageSet = new Set(pageOrder);
const pageTexts = {};
// ── Nested-page text protocol (parent-chain relay) ──
// Page bundles ship once, in the ROOT document's manifest. Every
// document mints the blobs for its OWN frames, obtaining page text
// through a strict parent-chain relay: a document only ever SENDS to
// its direct parent (which, for every document the publisher's own
// runtime minted, is another publisher document) and only ACCEPTS
// text from that direct parent; requests from below are honored only
// for the document's own child frames, and the ROOT never sends
// upward at all. That last rule is load-bearing: the artifact host's
// frame-ancestors allows claude.ai (and friends) to frame an
// artifact, so window.top can be a FOREIGN document — a top-addressed
// protocol would hand page uuids to that ancestor and accept forged
// "page text" from it, minting attacker HTML as an artifact-origin
// blob document. The chain never touches the ancestor: requests stop
// at the root, replies are origin-addressed per hop (never '*' when
// an origin exists), and both directions are origin-gated (on https
// a foreign sender's origin can never equal this document's; in
// opaque contexts — file:// — everything serializes as 'null', where
// an embedded EXTERNAL frame still fails the gate by carrying its
// own real origin, and a sandboxed child of such a frame is excluded
// by the own-child-frames check plus uuid unguessability). Shape is
// deny-by-default: one request form, one reply form, no error
// replies.
// window.origin is the SERIALIZED SECURITY origin ('null' in opaque
// contexts such as a sandboxed preview iframe) — location.origin is
// computed from the URL and stays 'https://…' even when the document
// is sandboxed, which would misgate every opaque chain.
const OWN_TARGET = /^https?:[/][/]/.test(window.origin || '')
? window.origin
: '*';
function trustedOrigin(e) {
if (e.origin === window.origin) return true; // https chains; opaque==opaque
// file:// trees: Chrome serializes the file document's origin as
// 'file://' while the blob: documents it mints serialize as 'null',
// so the two legitimate shapes can never string-match — accept the
// pair in both directions. https origins never serialize as either,
// and sender identity (own child frame / direct parent) is checked
// separately.
return (
(e.origin === 'null' &&
(window.origin === 'file://' ||
window.location.protocol === 'file:')) ||
(/^file:/.test(e.origin) && window.origin === 'null')
);
}
// Non-empty only in the root document (nested bundles ship an empty
// island), so it doubles as the root marker: the root answers from
// local text and treats a miss as authoritative — never relaying to
// its (possibly foreign) parent.
const isPageRoot = pageOrder.length > 0;
const blobUrls = {};
const resourceBlobs = {};
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
const pendingFrames = {}; // uuid -> [{el, frag}] awaiting text for own frames
const pendingRelays = {}; // uuid -> [childWindow] awaiting text to pass down
function mountPage(el, frag, text) {
const pageBlob = new Blob([text], { type: 'text/html' });
const u = URL.createObjectURL(pageBlob);
resourceBlobs[u] = pageBlob;
el.src = u + frag;
}
function deliverPage(uuid, text) {
const frames = pendingFrames[uuid];
delete pendingFrames[uuid];
if (frames) for (const f of frames) mountPage(f.el, f.frag, text);
const relays = pendingRelays[uuid];
delete pendingRelays[uuid];
if (relays) {
for (const cw of relays) {
try { cw.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
}
}
}
function ownFrameWindows() {
const out = [];
for (const f of Array.from(document.querySelectorAll('iframe'))) {
try { if (f.contentWindow) out.push(f.contentWindow); } catch (err) { /* detached */ }
}
return out;
}
window.addEventListener('message', function (e) {
const d = e.data;
if (!d || typeof d !== 'object' || !e.source) return;
// Origin gate first; the structural checks below (own-child-frames
// for requests, direct-parent + solicited-uuid for replies, the
// root's never-relay rule) are what hold in fully opaque chains
// where origin strings can't discriminate — a sandboxed root's
// FOREIGN wrapper still fails all three even though both serialize
// as real-vs-'null' rather than matching.
if (!trustedOrigin(e)) return;
if (typeof d.__bundler_need === 'string' && UUID_SHAPE.test(d.__bundler_need)) {
// Requests are honored only for this document's own child frames.
if (ownFrameWindows().indexOf(e.source) < 0) return;
const uuid = d.__bundler_need;
const text = pageTexts[uuid];
if (typeof text === 'string') {
try { e.source.postMessage({ __bundler_page: uuid, __bundler_text: text }, OWN_TARGET); } catch (err) { /* gone */ }
return;
}
if (isPageRoot || window.parent === window) return; // authoritative miss
const waiting = pendingRelays[uuid];
if (waiting) {
if (waiting.indexOf(e.source) < 0) waiting.push(e.source);
return;
}
// Never-answered uuids would otherwise accumulate forever under a
// request spray; past the cap (far above any real page count) new
// relays are dropped rather than remembered.
if (Object.keys(pendingRelays).length >= 128) return;
pendingRelays[uuid] = [e.source];
window.parent.postMessage({ __bundler_need: uuid }, OWN_TARGET);
} else if (
typeof d.__bundler_page === 'string' &&
UUID_SHAPE.test(d.__bundler_page) &&
typeof d.__bundler_text === 'string'
) {
// The shape test is load-bearing beyond tidiness: the solicited
// check below uses the in operator, which matches prototype keys
// ('__proto__', 'constructor') — an unshaped key would reach
// deliverPage and throw, painting the error banner.
// Text is accepted only from the direct parent — the document
// that minted this one.
if (e.source !== window.parent || window.parent === window) return;
if (!(d.__bundler_page in pendingFrames) && !(d.__bundler_page in pendingRelays)) return;
deliverPage(d.__bundler_page, d.__bundler_text);
}
});
const uuids = Object.keys(manifest);
setStatus('Unpacking ' + uuids.length + ' assets...');
await Promise.all(uuids.map(async (uuid) => {
const entry = manifest[uuid];
try {
const binaryStr = atob(entry.data);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i);
let finalBytes = bytes;
if (entry.compressed) {
if (typeof DecompressionStream !== 'undefined') {
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
const reader = ds.readable.getReader();
writer.write(bytes);
writer.close();
const chunks = [];
let totalLen = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
totalLen += value.length;
}
finalBytes = new Uint8Array(totalLen);
let offset = 0;
for (const chunk of chunks) { finalBytes.set(chunk, offset); offset += chunk.length; }
} else {
console.warn('DecompressionStream not available, asset ' + uuid + ' may not render');
}
}
if (pageSet.has(uuid)) {
pageTexts[uuid] = new TextDecoder().decode(finalBytes);
return;
}
if (FONT_MIME.test(entry.mime) && MIME_TOKEN.test(entry.mime)) {
// Strict artifact hosts allow font-src data: but not blob:.
blobUrls[uuid] = 'data:' + entry.mime + ';base64,' +
(entry.compressed ? toBase64(finalBytes) : entry.data);
} else {
const blob = new Blob([finalBytes], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
} catch (err) {
console.error('Failed to decode asset ' + uuid + ':', err);
const blob = new Blob([], { type: entry.mime });
blobUrls[uuid] = URL.createObjectURL(blob);
resourceBlobs[blobUrls[uuid]] = blob;
}
}));
const extResEl = document.querySelector('script[type="__bundler/ext_resources"]');
const extResources = extResEl ? JSON.parse(extResEl.textContent) : [];
const resourceMap = {};
for (const entry of extResources) {
if (blobUrls[entry.uuid]) resourceMap[entry.id] = blobUrls[entry.uuid];
}
// Artifact-host CSP (connect-src 'self') refuses fetch() of blob: URLs —
// consumers read these Blobs directly. Survives the swap like the error sink.
window.__resourceBlobs = resourceBlobs;
setStatus('Rendering...');
// Page uuids have no blob here — they ride inside about:blank#
// frame markers the pass below resolves after the swap.
for (const uuid of uuids) {
if (pageSet.has(uuid)) continue;
template = template.split(uuid).join(blobUrls[uuid]);
}
// Strip integrity + crossorigin — blob URLs from a file:// document inherit
// a null origin, so crossorigin forces a CORS fetch that SRI then rejects.
// The manifest bytes are ours; SRI protects against CDN compromise, not this.
template = template.replace(/\s+integrity="[^"]*"/gi, '').replace(/\s+crossorigin="[^"]*"/gi, '');
const resourceScript = '