Spaces:
Running
Running
File size: 5,785 Bytes
0ed8124 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | 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);
});
}
|