document.addEventListener('DOMContentLoaded', async function() { const loading = document.getElementById('__bundler_loading'); function setStatus(msg) { if (loading) loading.textContent = msg; }
const FONT_MIME = /^(font[/]|application[/](x-)?font-|application[/]vnd\.ms-fontobject)/i;
const MIME_TOKEN = /^[\w.+-]+[/][\w.+-]+$/;
function toBase64(bytes) {
let bin = '';
for (let i = 0; i < bytes.length; i += 0x8000) {
bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));
}
return btoa(bin);
}
// Error sink persists across replaceWith since it's on window, not the DOM.
window.addEventListener('error', function(e) {
// Failed resource loads (CSP-blocked links, scripts, images) fire plain
// events at the element — warn only; real JS errors carry message/error.
if (!e.message && !e.error && e.target && e.target !== window) {
console.warn('[bundle] resource failed to load:',
e.target.tagName, String(e.target.src || e.target.href || ''));
return;
}
var p = document.body || document.documentElement;
var d = document.getElementById('__bundler_err') || p.appendChild(document.createElement('div'));
d.id = '__bundler_err';
d.style.cssText = 'position:fixed;bottom:12px;left:12px;right:12px;font:12px/1.4 ui-monospace,monospace;background:#2a1215;color:#ff8a80;padding:10px 14px;border-radius:8px;border:1px solid #5c2b2e;z-index:99999;white-space:pre-wrap;max-height:40vh;overflow:auto';
d.textContent = (d.textContent ? d.textContent + String.fromCharCode(10) : '') +
'[bundle] ' + (e.message || e.type) +
(e.filename ? ' (' + e.filename.slice(0, 60) + ':' + e.lineno + ')' : '');
}, true);
try {
const manifestEl = document.querySelector('script[type="__bundler/manifest"]');
const templateEl = document.querySelector('script[type="__bundler/template"]');
if (!manifestEl || !templateEl) {
setStatus('Error: missing bundle data');
console.error('[bundler] Missing script tags — manifestEl:', !!manifestEl, 'templateEl:', !!templateEl);
return;
}
const manifest = JSON.parse(manifestEl.textContent);
let template = JSON.parse(templateEl.textContent);
// Nested page bundles (iframe targets). Each ships ONCE, in the ROOT
// document's manifest; frame srcs carry an about:blank#
// ── 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#
// 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 = '