Bonsai-Chat-WebGPU / src /lib /js-sandbox.ts
WaveCut's picture
Release browser-local Bonsai WebGPU chat
0ed8124 verified
Raw
History Blame Contribute Delete
5.79 kB
export interface JavaScriptResult {
value: string;
logs: string[];
durationMs: number;
}
const MAX_SOURCE_BYTES = 64 * 1024;
const MAX_RESULT_CHARACTERS = 16 * 1024;
const OPAQUE_WORKER_SOURCE = `
(() => {
const send = self.postMessage.bind(self);
const render = (value) => {
if (typeof value === 'string') return value;
try { return JSON.stringify(value); } catch { return String(value); }
};
const blocked = () => Promise.reject(new Error('Network access is disabled in js_eval'));
const hide = (target, key, value) => {
try { Object.defineProperty(target, key, { value, writable: false, configurable: false }); } catch {}
};
hide(self, 'fetch', blocked);
hide(self, 'XMLHttpRequest', undefined);
hide(self, 'WebSocket', undefined);
hide(self, 'EventSource', undefined);
hide(self, 'SharedWorker', undefined);
hide(self, 'Worker', undefined);
hide(self, 'BroadcastChannel', undefined);
hide(self, 'importScripts', undefined);
hide(self, 'indexedDB', undefined);
hide(self, 'caches', undefined);
hide(self.navigator, 'storage', undefined);
hide(self, 'postMessage', undefined);
hide(self, 'close', undefined);
self.onmessage = async ({ data }) => {
const logs = [];
const capture = (...values) => logs.push(values.map(render).join(' '));
const console = { log: capture, info: capture, warn: capture, error: capture };
try {
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
const execute = new AsyncFunction('console', '"use strict";\\n' + data.source);
const value = await execute(console);
send({ ok: true, value: render(value), logs });
} catch (error) {
send({ ok: false, error: error?.message ?? String(error), logs });
}
};
})();
`;
function frameSource(channel: string): string {
const policy = [
"default-src 'none'",
"base-uri 'none'",
"connect-src 'none'",
"form-action 'none'",
"frame-src 'none'",
"img-src 'none'",
"media-src 'none'",
"object-src 'none'",
"style-src 'none'",
"script-src 'unsafe-inline' 'unsafe-eval' blob:",
'worker-src blob:',
].join('; ');
return `<!doctype html><meta http-equiv="Content-Security-Policy" content="${policy}"><script>
(() => {
const channel = ${JSON.stringify(channel)};
const workerCode = ${JSON.stringify(OPAQUE_WORKER_SOURCE)};
let worker = null;
addEventListener('message', (event) => {
if (event.source !== parent || event.data?.channel !== channel || event.data?.kind !== 'run') return;
const workerUrl = URL.createObjectURL(new Blob([workerCode], { type: 'text/javascript' }));
worker = new Worker(workerUrl);
URL.revokeObjectURL(workerUrl);
worker.onmessage = ({ data }) => parent.postMessage({ channel, kind: 'result', payload: data }, '*');
worker.onerror = (error) => parent.postMessage({ channel, kind: 'result', payload: { ok: false, error: error.message || 'js_eval worker failed', logs: [] } }, '*');
worker.postMessage({ source: event.data.source });
});
parent.postMessage({ channel, kind: 'ready' }, '*');
})();
<\/script>`;
}
export function runJavaScript(
source: string,
timeoutMs = 5000,
signal?: AbortSignal,
): Promise<JavaScriptResult> {
if (new TextEncoder().encode(source).byteLength > MAX_SOURCE_BYTES) {
return Promise.reject(new Error('js_eval source exceeds 64 KiB'));
}
if (typeof document === 'undefined' || !document.documentElement) {
return Promise.reject(new Error('js_eval requires a browser document'));
}
const channel = crypto.randomUUID();
const iframe = document.createElement('iframe');
iframe.hidden = true;
iframe.setAttribute('sandbox', 'allow-scripts');
iframe.setAttribute('credentialless', '');
iframe.srcdoc = frameSource(channel);
const startedAt = performance.now();
return new Promise((resolve, reject) => {
const cleanup = () => {
window.removeEventListener('message', onMessage);
signal?.removeEventListener('abort', onAbort);
iframe.remove();
};
const timer = window.setTimeout(() => {
cleanup();
reject(new Error(`js_eval exceeded ${timeoutMs} ms`));
}, timeoutMs);
const finish = (callback: () => void) => {
window.clearTimeout(timer);
cleanup();
callback();
};
const onMessage = (event: MessageEvent<Record<string, unknown>>) => {
if (event.source !== iframe.contentWindow || event.data?.channel !== channel) return;
if (event.data.kind === 'ready') {
iframe.contentWindow?.postMessage({ channel, kind: 'run', source }, '*');
return;
}
if (event.data.kind !== 'result') return;
const payload = event.data.payload;
if (typeof payload !== 'object' || payload === null) {
finish(() => reject(new Error('js_eval sandbox returned an invalid payload')));
return;
}
const result = payload as Record<string, unknown>;
const logs = Array.isArray(result.logs) ? result.logs.map(String).slice(0, 100) : [];
if (result.ok !== true) {
finish(() => reject(new Error(typeof result.error === 'string' ? result.error : 'js_eval failed')));
return;
}
finish(() => resolve({
value: String(result.value ?? '').slice(0, MAX_RESULT_CHARACTERS),
logs: logs.map((line) => line.slice(0, 2048)),
durationMs: performance.now() - startedAt,
}));
};
const onAbort = () => finish(() => reject(new DOMException('The tool operation was aborted.', 'AbortError')));
window.addEventListener('message', onMessage);
if (signal?.aborted) {
onAbort();
return;
}
signal?.addEventListener('abort', onAbort, { once: true });
document.documentElement.append(iframe);
});
}