Spaces:
Running
Running
| 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); | |
| }); | |
| } | |