Files
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

146 lines
6.0 KiB
TypeScript

/**
* In-memory localStorage/sessionStorage shim, injected as the FIRST thing in the
* document so the page's own scripts see working storage.
*
* The interactive iframe is sandboxed `allow-scripts` WITHOUT `allow-same-origin`
* (intentional — combining them negates the sandbox for LLM-authored HTML). In a
* null-origin document, touching `window.localStorage` throws a SecurityError;
* many generated pages read/write storage in their setup code, so that throw
* crashes the script before anything renders → a blank/black widget. This shim
* replaces both storages with an in-memory implementation when the real ones are
* inaccessible, keeping the sandbox intact while letting storage-using pages run.
*/
const STORAGE_SHIM = `<script data-iframe-storage-shim>
(function () {
function makeStore() {
var data = Object.create(null);
return {
getItem: function (k) { k = String(k); return Object.prototype.hasOwnProperty.call(data, k) ? data[k] : null; },
setItem: function (k, v) { data[String(k)] = String(v); },
removeItem: function (k) { delete data[String(k)]; },
clear: function () { data = Object.create(null); },
key: function (i) { var keys = Object.keys(data); return i < keys.length ? keys[i] : null; },
get length() { return Object.keys(data).length; }
};
}
['localStorage', 'sessionStorage'].forEach(function (name) {
var ok = false;
try { var s = window[name]; if (s) { s.getItem('__probe__'); ok = true; } } catch (e) { ok = false; }
if (!ok) {
try { Object.defineProperty(window, name, { value: makeStore(), configurable: true }); } catch (e) {}
}
});
})();
</script>`;
/**
* Runtime-error capture, injected as the VERY FIRST script so it observes errors
* from the storage shim and every page script that follows. Generated interactive
* pages frequently die on a runtime error (a `JSON.parse` of malformed config, a
* reference to a CDN lib that failed to load, …) → the script aborts and the
* widget renders blank. The sandboxed (null-origin) iframe can't be read by the
* editor, but it CAN `postMessage` out: this forwards `window.onerror`, unhandled
* rejections and `console.error` to the parent, which stores them per scene and
* feeds them to the editor agent — so it can diagnose a blank page instead of
* guessing. Only touches `window.*` so it stays sandbox-safe and unit-testable.
*
* The most important errors (a `JSON.parse` that aborts setup) fire SYNCHRONOUSLY
* while srcDoc parses — potentially before the parent has subscribed its `message`
* listener (which it installs from a passive effect after inserting the iframe).
* To avoid losing exactly the errors this feature exists to surface, every post is
* also buffered, and the shim re-emits the whole buffer when the parent sends a
* `{ __maicErrorReplayRequest: true }` message once its listener is ready. The
* parent dedups, so the live + replayed copies collapse to one.
*/
const ERROR_CAPTURE_SHIM = `<script data-iframe-error-shim>
(function () {
var buffer = [];
function emit(errorKind, message) {
try {
window.parent.postMessage(
{ __maicInteractive: true, kind: 'runtime-error', errorKind: errorKind, message: message },
'*'
);
} catch (e) {}
}
function post(errorKind, message) {
message = String(message).slice(0, 1200);
if (buffer.length < 50) buffer.push([errorKind, message]);
emit(errorKind, message);
}
window.addEventListener('message', function (e) {
var d = e && e.data;
if (d && d.__maicErrorReplayRequest === true) {
for (var i = 0; i < buffer.length; i++) emit(buffer[i][0], buffer[i][1]);
}
});
window.addEventListener('error', function (e) {
if (e && e.message) {
post('error', e.message + (e.filename ? ' (' + e.filename + ':' + (e.lineno || 0) + ')' : ''));
} else if (e && e.target && (e.target.src || e.target.href)) {
post('resource', 'Failed to load resource: ' + (e.target.src || e.target.href));
}
}, true);
window.addEventListener('unhandledrejection', function (e) {
var r = e && e.reason;
post('unhandledrejection', (r && (r.stack || r.message)) || r || 'unhandled promise rejection');
});
try {
var c = window.console;
if (c && c.error) {
var _ce = c.error;
c.error = function () {
try { post('console.error', Array.prototype.map.call(arguments, function (a) { return (a && a.stack) || String(a); }).join(' ')); } catch (e) {}
return _ce.apply(c, arguments);
};
}
} catch (e) {}
})();
</script>`;
/**
* Patch embedded HTML to display correctly inside an iframe.
*
* Injects a runtime-error capture shim + a storage shim (so sandboxed pages that
* use localStorage don't crash) plus CSS that ensures proper sizing and scrolling
* behavior when HTML content is rendered via srcDoc in an iframe. The shims are
* placed first so they run before the page's own scripts (error capture first, so
* it also observes the storage shim).
*/
export function patchHtmlForIframe(html: string): string {
const iframeCss = `<style data-iframe-patch>
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow-x: hidden;
overflow-y: auto;
}
/* Fix min-h-screen: in iframes 100vh is the iframe height, which is correct,
but ensure body actually fills it */
body { min-height: 100vh; }
</style>`;
const injection = '\n' + ERROR_CAPTURE_SHIM + '\n' + STORAGE_SHIM + '\n' + iframeCss;
// Insert right after <head> or at the start of the document
const headIdx = html.indexOf('<head>');
if (headIdx !== -1) {
const insertPos = headIdx + 6; // after <head>
return html.substring(0, insertPos) + injection + html.substring(insertPos);
}
const headWithAttrs = html.indexOf('<head ');
if (headWithAttrs !== -1) {
const closeAngle = html.indexOf('>', headWithAttrs);
if (closeAngle !== -1) {
const insertPos = closeAngle + 1;
return html.substring(0, insertPos) + injection + html.substring(insertPos);
}
}
// Fallback: prepend
return injection + html;
}