A listener's song plays in a sandbox, never in this page.
Listeners' songs are someone else's code. Evaluated here, on the site's
origin, they could do anything the page can: for a signed-in visitor,
post comments, publish, or spend their daily Jev budget with their
session. So the page never evaluates them. They play in an iframe with an
opaque origin (sandbox="allow-scripts", never allow-same-origin),
served with a strict CSP (sandboxPolicy.mjs): it has no cookies, no
storage of the site's, no way into this page (only postMessage), no
navigation of the top page, no forms or popups, and it reaches only the
site's own files and the sample hosts songs use.
This module is the page's end:
- Provenance. The editor's code is
foreignfrom the moment a listener's song, or code from a link (Strudel's #code or ?hash), is loaded into it, until code of the site's own (a site song, a pattern, an example) replaces it. The visitor's edits keep it foreign: it is still that listener's code.guardEditormakes the editor evaluate foreign code only in the sandbox: play, ctrl+enter, block evaluation and the header's button all go there, and the page's ownreplrefuses it outright. A trusted loader that forgets to say so only leaves code in the sandbox; a foreign one is marked where it loads (showForeign), and a listener song's object carries its code asforeignCode(listeners.mjs), so no path that plays songs can take it. - The frame. One per foreign load (a new song gets a fresh frame, so no song's code outlives it), made on the first play. It posts only data (sandboxProtocol.mjs rebuilds every message), which the booth, the mixer, the playhead and the console show.
- Jev. The frame's jev() calls come here as messages and go to the relay
from this page. A listener's song or a link's code is asked with
credentials: 'omit': never the visitor's session, so never their budget; the relay counts them per address, as anyone signed out. Code the visitor's own AI played through the hosted MCP (myTabs.mjs) is asked with this page's session, so it is charged to the visitor's own daily budget, as their other Jev calls are: the visitor connected that app to act for them, and anonymous calls would let it spend the address's shared allowance instead. The frame never holds the session either way: the page makes the call, and the frame gets only the answer. The relay itself refuses cross-origin calls, so the frame cannot reach it directly. A song asking more than half the relay's per-visitor rate is refused here, as allSongs.test.mjs refuses a site song that would.
48export const SANDBOX_PATH = '/jev/sandbox/';
half the relay's JEV_LIMIT (60 a minute, worker/wrangler.json), as allSongs.test.mjs holds site songs to; sandbox.test.mjs checks it
how many messages a frame may send a second before the rest are dropped
54const MESSAGES_PER_S = 200;
── provenance ────────────────────────────────────
null: the editor's code is the site's or the visitor's own. Otherwise
where it came from: { song } (a listener's, from listeners.mjs),
{ link: true } (code in the address) or { ai: name } (code the visitor's
own AI played through the hosted MCP, myTabs.mjs; name is the app's
own, unverified, and only ever shown as text).
62let foreign = null;
what the page shows of it (a new object on every change, for React)
The cycle the sandbox is on, from its last report, or null when it is not playing (the booth's and mixer's playhead).
88export function sandboxCycle() { 89 if (state.status !== 'playing' || state.cycle === null) return null; 90 const elapsed = (performance.now() - state.at) / 1000; 91 return state.cycle + (state.cps ? elapsed * state.cps : 0); 92} 93export const sandboxLevels = () => (state.status === 'playing' ? levels : null);
A recorded performance of a listener's song, armed for the sandbox: { songId, jevs, changed }. Every play of that song while it is loaded sends it (fromPage's play), so the frame's jev()s play it and ask Jev nothing, as a site song's replay does in the page (performance.mjs). Anything else loaded into the editor disarms it.
100let replay = null; 101export function armSandboxReplay(songId, performance) { 102 replay = performance ? { songId, jevs: performance.jevs, changed: Boolean(performance.changed) } : null; 103} 104const replayFor = () => 105 replay && foreign?.song?.id === replay.songId ? { jevs: replay.jevs, changed: replay.changed } : null;
Foreign code into the editor: a listener's song (its foreignCode), or
code from a link. The last sandbox goes; the next play makes a new one.
The site's or the visitor's own code into the editor.
── the frame ─────────────────────────────────────
What the frame logs, raw, for whoever asked (myTabs.mjs's get_logs); the page's console gets it escaped, as before.
Each play is numbered, and the frame's state names the play it is about, so playForeign can tell its song's start from the song before.
148function style(el, visible) { 149 Object.assign(el.style, visible 150 ? { position: 'fixed', right: '16px', bottom: '16px', width: '320px', height: '96px', opacity: '1', pointerEvents: 'auto', zIndex: '60', border: '1px solid currentColor', borderRadius: '6px', background: 'var(--background, #222)' } 151 : { position: 'fixed', right: '0', bottom: '0', width: '1px', height: '1px', opacity: '0', pointerEvents: 'none', zIndex: '-1', border: '0' }); 152} 153 154function create() { 155 const el = document.createElement('iframe'); 156 // allow-scripts only: an opaque origin, no forms, popups, modals or top 157 // navigation. Never add allow-same-origin: with it the frame would be 158 // this site, and could reach into this page and its cookies. 159 el.setAttribute('sandbox', 'allow-scripts'); 160 // the page's click may start the frame's audio (browsers keep audio off 161 // until a click, and let a frame use its page's only when allowed) 162 el.setAttribute('allow', 'autoplay'); 163 el.setAttribute('referrerpolicy', 'no-referrer'); 164 el.title = "the sandbox a listener's song plays in"; 165 style(el, false); 166 let resolveReady, rejectReady; 167 const ready = new Promise((resolve, reject) => { 168 resolveReady = resolve; 169 rejectReady = reject; 170 }); 171 ready.catch(() => {}); 172 // whose budget the frame's Jev calls are charged to is fixed with the 173 // frame: provenance changing destroys it (showForeign, markOwn) 174 const f = { el, ready, resolveReady, run: ++runs, token: {}, bucket: { n: MESSAGES_PER_S, at: performance.now() }, asks: [], loads: 0, charge: Boolean(foreign?.ai) }; 175 // A second load is the frame navigating itself away (listener code can: 176 // the sandbox cannot forbid it). Whatever loaded is still sandboxed, but 177 // it is no longer the player: end it. 178 el.addEventListener('load', () => { 179 f.loads += 1; 180 if (f.loads > 1 && frame === f) { 181 logger("A listener's song navigated its sandbox away, so it was stopped.", 'warning'); 182 destroy(); 183 } 184 }); 185 setTimeout(() => rejectReady(new Error('the sandbox did not start')), READY_MS); 186 el.src = SANDBOX_PATH; 187 document.body.append(el); 188 return f; 189} 190 191function destroy() { 192 if (frame) { 193 frame.el.remove(); 194 frame = null; 195 boothStore.showRemote(null); 196 } 197 levels = null; 198 if (state.status !== 'idle') set({ status: 'idle', cycle: null, cps: null, error: null, audio: null }); 199} 200 201function send(message) { 202 frame?.el.contentWindow?.postMessage(message, '*'); 203}
Plays the editor's (foreign) code in the sandbox, making it first.
206export async function playInSandbox(code) { 207 // numbered now, before any await, so playForeign knows which it is 208 const run = ++plays; 209 if (!frame) frame = create(); 210 const f = frame; 211 set({ status: 'loading', error: null }); 212 try { 213 await f.ready; 214 } catch (e) { 215 if (frame === f) { 216 set({ status: 'error', error: e.message }); 217 logger(`The sandbox for this listener's song could not start (${escapeHtml(e.message)}).`, 'error'); 218 } 219 return; 220 } 221 if (frame !== f) return; 222 send({ type: 'resume' }); 223 send({ type: 'play', code, run, replay: replayFor() }); 224}
226const AUDIO_HELD = "this tab's sound is held until someone clicks the page (or the ▶ in its sandbox box)";
Plays foreign code in the sandbox and resolves once it is playing ({ ok: true }) or failed ({ error }), for the hosted MCP's play_code.
230export function playForeign(code, { timeoutMs = 25_000 } = {}) { 231 waiter?.finish({ error: 'another play replaced it' }); 232 return new Promise((resolve) => { 233 const w = { 234 run: plays + 1, 235 finish(result) { 236 if (waiter !== w) return; 237 waiter = null; 238 clearTimeout(timer); 239 resolve(result); 240 }, 241 }; 242 waiter = w; 243 const timer = setTimeout( 244 () => w.finish({ error: state.audio && state.audio !== 'running' ? AUDIO_HELD : 'the sandbox did not start playing in time' }), 245 timeoutMs, 246 ); 247 playInSandbox(code).then(() => { 248 if (state.status === 'error') w.finish({ error: `the sandbox could not start (${state.error})` }); 249 }); 250 }); 251}
The page was clicked: that click may start the frame's audio.
260if (typeof window !== 'undefined') { 261 window.addEventListener('pointerdown', () => frame && send({ type: 'resume' }), true); 262 window.addEventListener('message', (e) => { 263 const f = frame; 264 // only this page's own sandbox, and only while it is opaque: a frame 265 // that somehow had this origin would send its origin, not "null" 266 if (!f || e.source !== f.el.contentWindow || e.origin !== 'null') return; 267 const now = performance.now(); 268 const b = f.bucket; 269 b.n = Math.min(MESSAGES_PER_S, b.n + ((now - b.at) / 1000) * MESSAGES_PER_S); 270 b.at = now; 271 if (b.n < 1) return; 272 b.n -= 1; 273 const m = fromFrame(e.data); 274 if (m) receive(f, m); 275 }); 276}
278function receive(f, m) { 279 switch (m.type) { 280 case 'ready': 281 f.resolveReady(); 282 return; 283 case 'state': 284 if (m.error) { 285 logger(`[sandbox] ${escapeHtml(m.error)}`, 'error'); 286 logged(m.error, 'error'); 287 } 288 if (waiter && m.run === waiter.run) { 289 if (m.error) waiter.finish({ error: m.error }); 290 else if (m.playing) waiter.finish({ ok: true }); 291 } 292 // a play first stops what played before: that stop leaves it loading 293 set({ 294 status: m.playing ? 'playing' : state.status === 'loading' && !m.error ? 'loading' : 'stopped', 295 cycle: m.cycle, 296 cps: m.cps, 297 at: performance.now(), 298 error: m.error, 299 }); 300 return; 301 case 'log': 302 // the console renders HTML: a listener's text is escaped 303 logger(`[sandbox] ${escapeHtml(m.message)}`, m.kind || undefined); 304 logged(m.message, m.kind); 305 return; 306 case 'booth': 307 if (f.boothRun !== m.run) { 308 f.boothRun = m.run; 309 f.token = {}; 310 } 311 boothStore.showRemote( 312 m.booth && { 313 ...m.booth, 314 // the same evaluation keeps one identity (JevBooth's identityOf) 315 views: [f.token], 316 // played in the frame: the recorder reads its song from `foreign` 317 sandboxed: true, 318 react: (segment, kind) => send({ type: 'react', segment, kind }), 319 }, 320 ); 321 return; 322 case 'levels': 323 levels = m.levels; 324 return; 325 case 'audio': 326 style(f.el, m.state !== 'running' && sandbox.playing()); 327 set({ audio: m.state }); 328 return; 329 case 'ask': 330 ask(f, m); 331 return; 332 } 333}
A Jev call for the frame, made anonymously (see the top).
336async function ask(f, { id, body, lead, attempt }) { 337 const now = Date.now(); 338 f.asks = f.asks.filter((t) => now - t < 60_000); 339 if (f.asks.length >= ASKS_PER_MINUTE) { 340 const wait = Math.ceil((60_000 - (now - f.asks[0])) / 1000); 341 send({ 342 type: 'answer', 343 id, 344 status: 429, 345 headers: { 'retry-after': String(wait) }, 346 body: `this song asks Jev more than ${ASKS_PER_MINUTE} times a minute`, 347 }); 348 return; 349 } 350 f.asks.push(now); 351 const headers = { 'Content-Type': 'application/json', [ATTEMPT_HEADER]: String(attempt) }; 352 if (lead !== null) headers[LEAD_HEADER] = lead.toFixed(2); 353 try { 354 const res = await fetch(RELAY, { method: 'POST', credentials: f.charge ? 'same-origin' : 'omit', headers, body }); 355 if (f.charge) noteBudgetHeader(res.headers.get(BUDGET_HEADER)); 356 const text = (await res.text()).slice(0, MAX_ANSWER_BYTES); 357 const pass = {}; 358 for (const name of ['retry-after', 'retry-after-ms', 'jev-cache']) { 359 const v = res.headers.get(name); 360 if (v !== null) pass[name] = v.slice(0, 200); 361 } 362 if (frame === f) send({ type: 'answer', id, status: res.status, headers: pass, body: text }); 363 } catch (e) { 364 if (frame === f) send({ type: 'answer', id, error: 'the relay could not be reached' }); 365 } 366}
── the editor ──────────────────────────────────── Foreign code is evaluated only in the sandbox; see the top.
370export function guardEditor(editor) { 371 const evaluate = editor.evaluate.bind(editor); 372 const stop = editor.stop.bind(editor); 373 const repl = editor.repl; 374 const replEvaluate = repl.evaluate; 375 const replEvaluateBlock = repl.evaluateBlock; 376 const refuse = () => { 377 const e = new Error("a listener's code plays only in the sandbox"); 378 logger(e.message, 'warning'); 379 return Promise.reject(e); 380 }; 381 editor.evaluate = async (autostart = true) => { 382 if (foreign) { 383 if (!autostart) return refuse(); 384 editor.flash(); 385 repl.stop(); 386 return playInSandbox(editor.code); 387 } 388 stopSandbox(); 389 return evaluate(autostart); 390 }; 391 editor.stop = async () => { 392 stopSandbox(); 393 return stop(); 394 }; 395 editor.toggle = async () => { 396 if (foreign) return sandbox.playing() ? stopSandbox() : editor.evaluate(); 397 if (repl.scheduler.started) return stop(); 398 return editor.evaluate(); 399 }; 400 // The page's own start that was already under way (awaiting its samples in 401 // beforeStart) when foreign code arrived must not go on to play: the 402 // scheduler's stop() does not cancel a start in flight 403 // (packages/core/cyclist.mjs), so a site song clicked a moment before a 404 // listener's song, or before the listener's AI played, would sound under it. 405 const scheduler = repl.scheduler; 406 const beforeStart = scheduler.beforeStart; 407 scheduler.beforeStart = async () => { 408 await beforeStart?.(); 409 if (foreign) throw new Error('not started: the editor now holds code that plays in the sandbox'); 410 }; 411 repl.evaluate = (code, autostart) => (foreign ? refuse() : replEvaluate(code, autostart)); 412 repl.evaluateBlock = (code, autostart, options) => 413 foreign ? editor.evaluate() : replEvaluateBlock(code, autostart, options); 414 return editor; 415}