The wire between the page and the sandbox a listener's song plays in (sandbox.mjs is the page's end, sandbox/player.mjs the frame's). The frame runs someone else's code, so everything it sends is untrusted: each message is rebuilt here from the fields the page reads, each checked for its type and bounded in size, and anything else is dropped. Nothing from the frame is ever evaluated, rendered as HTML, or passed on as it came.
Page → frame: play { code, run, replay? } (replay: a recorded performance's
{ jevs, changed } for the frame's jev()s to play, as the page replays a
site song's; performance.mjs), stop, react { segment, kind }, answer { id,
status, headers, body } (a Jev call the page made for the frame), and
resume (the page was clicked: start the frame's audio).
Frame → page: ready, state { playing, cycle, cps, error, run } (run is
the last play's, so the page knows which play a state is about), log { message,
kind }, booth { run, booth } (what Jev decided, for the booth and the
mixer), levels { levels } (the meter), audio { state }, ask { id, body,
lead, attempt } (a Jev call, which the page makes without the visitor's
session: see sandbox.mjs).
20export const MAX_CODE = 1024 * 1024;
the relay's own cap on a Jev request (worker/src/relay.ts MAX_BYTES)
22export const MAX_ASK_BYTES = 64 * 1024; 23export const MAX_ANSWER_BYTES = 256 * 1024; 24const MAX_TEXT = 4000; 25const MAX_NAME = 100; 26const MAX_SEGMENTS = 1024; 27const MAX_QUESTIONS = 32; 28const MAX_OPTIONS = 255; // TypeSafe's own limit for a choice 29const MAX_LEVELS = 10; // and for a score 30const MAX_PARTS = 32;
32const isPlain = (x) => 33 x !== null && typeof x === 'object' && !Array.isArray(x) && Object.getPrototypeOf(x) === Object.prototype; 34const str = (x, max = MAX_TEXT) => (typeof x === 'string' && x.length <= max ? x : undefined); 35const num = (x) => (typeof x === 'number' && Number.isFinite(x) ? x : undefined); 36const int = (x, lo, hi) => (Number.isInteger(x) && x >= lo && x <= hi ? x : undefined); 37const bool = (x) => (typeof x === 'boolean' ? x : undefined);
an object without the keys whose value is undefined
39const compact = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
Plain JSON data, bounded: what Jev was told (a decision's states) is
shown as JSON text, so only its shape and size need checking. budget
counts nodes across one message.
44function plainData(x, budget, depth = 0) { 45 if (budget.n-- <= 0 || depth > 8) return undefined; 46 if (x === null || typeof x === 'boolean') return x; 47 if (typeof x === 'number') return Number.isFinite(x) ? x : null; 48 if (typeof x === 'string') return x.length <= MAX_TEXT ? x : x.slice(0, MAX_TEXT); 49 if (Array.isArray(x)) return x.slice(0, 256).map((v) => plainData(v, budget, depth + 1) ?? null); 50 if (!isPlain(x)) return undefined; 51 const out = {}; 52 for (const [k, v] of Object.entries(x).slice(0, 256)) { 53 const clean = plainData(v, budget, depth + 1); 54 if (clean !== undefined && k.length <= MAX_NAME) out[k] = clean; 55 } 56 return out; 57}
How a finished section measured (meter.mjs's hear(), as Jev was told it), which the booth's detail tables.
61function measuredRow(m) { 62 if (!isPlain(m) || str(m.cycles, 40) === undefined) return undefined; 63 const parts = {}; 64 if (isPlain(m.parts)) { 65 for (const [name, p] of Object.entries(m.parts).slice(0, MAX_PARTS)) { 66 if (name.length > MAX_NAME || !isPlain(p)) continue; 67 parts[name] = compact({ loudnessDbfs: num(p.loudnessDbfs), peakDbfs: num(p.peakDbfs), silentShare: num(p.silentShare) }); 68 } 69 } 70 return compact({ 71 cycles: m.cycles, 72 loudnessDbfs: num(m.loudnessDbfs), 73 peakDbfs: num(m.peakDbfs), 74 clipped: bool(m.clipped), 75 parts, 76 }); 77}
79function state(s, budget) { 80 const clean = plainData(s, budget); 81 if (!isPlain(clean)) return undefined; 82 if ('measuredSound' in clean) { 83 const rows = Array.isArray(s.measuredSound) ? s.measuredSound.slice(0, 64).map(measuredRow).filter(Boolean) : []; 84 if (rows.length) clean.measuredSound = rows; 85 else delete clean.measuredSound; 86 } 87 return clean; 88} 89 90function question(q) { 91 if (!isPlain(q)) return undefined; 92 const name = str(q.name, MAX_NAME); 93 const instructions = q.instructions === undefined ? undefined : str(q.instructions); 94 if (!name || (q.instructions !== undefined && instructions === undefined)) return undefined; 95 let criteria; 96 if (q.type === 'choice') { 97 if (!isPlain(q.criteria)) return undefined; 98 const entries = Object.entries(q.criteria); 99 if (entries.length > MAX_OPTIONS || entries.some(([k, v]) => k.length > MAX_NAME || str(v) === undefined)) return undefined; 100 criteria = Object.fromEntries(entries); 101 } else if (q.type === 'score') { 102 if (!Array.isArray(q.criteria) || q.criteria.length < 2 || q.criteria.length > MAX_LEVELS) return undefined; 103 if (q.criteria.some((v) => str(v) === undefined)) return undefined; 104 criteria = [...q.criteria]; 105 } else if (q.type === 'noul') { 106 if (q.criteria !== undefined) { 107 if (!isPlain(q.criteria) || str(q.criteria.true) === undefined || str(q.criteria.false) === undefined) return undefined; 108 criteria = { true: q.criteria.true, false: q.criteria.false }; 109 } 110 } else { 111 return undefined; 112 } 113 return compact({ name, type: q.type, instructions, criteria }); 114} 115 116function probabilities(p) { 117 if (!isPlain(p)) return undefined; 118 const entries = Object.entries(p).filter(([k, v]) => k.length <= MAX_NAME && num(v) !== undefined); 119 return Object.fromEntries(entries.slice(0, MAX_OPTIONS)); 120}
One answer, as its question's type needs it: the booth calls toFixed on a score and shows a choice as text, so a value without its field is dropped.
124function answer(a, type) { 125 if (!isPlain(a)) return undefined; 126 const common = { 127 confidence: num(a.confidence), 128 probabilities: probabilities(a.probabilities), 129 fallback: bool(a.fallback), 130 forced: bool(a.forced), 131 absent: bool(a.absent), 132 }; 133 if (type === 'choice' || type === 'form') { 134 const choice = str(a.choice, MAX_NAME); 135 if (choice === undefined) return undefined; 136 return compact({ ...common, choice, sampled: num(a.sampled), top: str(a.top, MAX_NAME) }); 137 } 138 if (type === 'score') { 139 const score = num(a.score); 140 return score === undefined ? undefined : compact({ ...common, score }); 141 } 142 const noul = num(a.noul); 143 return noul === undefined ? undefined : compact({ ...common, noul }); 144}
146const STATUSES = new Set(['opening', 'asking', 'answered', 'fallback']); 147 148function decision(d, types, budget) { 149 if (!isPlain(d) || !STATUSES.has(d.status)) return undefined; 150 const segment = int(d.segment, 0, MAX_SEGMENTS); 151 const cycles = str(d.cycles, 40); 152 if (segment === undefined || cycles === undefined) return undefined; 153 const values = {}; 154 if (isPlain(d.values)) { 155 for (const [name, a] of Object.entries(d.values)) { 156 if (!types.has(name)) continue; 157 const clean = answer(a, types.get(name)); 158 if (clean) values[name] = clean; 159 } 160 } 161 const problems = Array.isArray(d.problems) ? d.problems.slice(0, 20).map((p) => str(p)).filter(Boolean) : undefined; 162 const states = Array.isArray(d.states) ? d.states.slice(0, 8).map((s) => state(s, budget)).filter(Boolean) : undefined; 163 return compact({ 164 segment, 165 cycles, 166 status: d.status, 167 values, 168 problems, 169 reason: str(d.reason), 170 ms: num(d.ms), 171 // a late answer's first cycle, and a segment the song ended before: 172 // what the recorder (performance.mjs) and the request log read 173 from: int(d.from, 0, 1e7), 174 ended: d.ended === true ? true : undefined, 175 states, 176 }); 177}
The frame's jev()s one by one, in declaration order (each jev()'s own decisions), which a recorded performance keeps apart (performance.mjs's performanceOf): at most MAX_JEVS, on the booth's cadence, each decision checked as the booth's are.
183const MAX_JEVS = 4; 184function views(list, every, questions, types, budget) { 185 if (!Array.isArray(list) || !list.length || list.length > MAX_JEVS) return undefined; 186 const out = []; 187 for (const v of list) { 188 if (!isPlain(v) || num(v.every) !== every || !Array.isArray(v.decisions)) return undefined; 189 let form = null; 190 if (v.form !== null && v.form !== undefined) { 191 const name = isPlain(v.form) ? str(v.form.name, MAX_NAME) : undefined; 192 if (!name || !questions.some((q) => q.name === name && q.type === 'choice')) return undefined; 193 form = { name }; 194 } 195 const decisions = []; 196 v.decisions.slice(0, MAX_SEGMENTS).forEach((d, i) => { 197 const clean = d === undefined || d === null ? undefined : decision(d, types, budget); 198 if (clean && clean.segment === i) decisions[i] = clean; 199 }); 200 out.push({ every, form, decisions }); 201 } 202 return out; 203}
A recorded performance for the frame to replay (fromPage's play), as the Worker stores it (worker/src/listening.ts): only what jevCore's setReplay reads, bounded.
as many as the site stores in one (worker/src/listening.ts)
212const MAX_TAKE_SEGMENTS = 64; 213export function cleanReplay(r) { 214 if (!isPlain(r) || !Array.isArray(r.jevs) || !r.jevs.length || r.jevs.length > MAX_JEVS) return undefined; 215 const jevs = []; 216 for (const j of r.jevs) { 217 if (!isPlain(j) || !Array.isArray(j.segments) || j.segments.length > MAX_TAKE_SEGMENTS) return undefined; 218 const segments = []; 219 for (const seg of j.segments) { 220 if (!isPlain(seg) || !REPLAY_STATUSES.has(seg.status) || !isPlain(seg.values)) return undefined; 221 const values = {}; 222 for (const [name, a] of Object.entries(seg.values).slice(0, MAX_QUESTIONS)) { 223 const type = answerType(a); 224 const clean = name.length <= MAX_NAME && type ? answer(a, type) : undefined; 225 if (!clean) return undefined; 226 values[name] = clean; 227 } 228 segments.push(compact({ status: seg.status, ms: num(seg.ms), from: int(seg.from, 0, 1e7), values })); 229 } 230 jevs.push({ segments }); 231 } 232 return { jevs, changed: r.changed === true }; 233}
What Jev decided in the frame, as the booth, the mixer and the jev panel read it (boothStore.mjs's combine()), rebuilt from checked fields; null when it is not one.
238export function cleanBooth(b) { 239 if (!isPlain(b)) return null; 240 const every = num(b.every); 241 const segments = b.segments === Infinity ? Infinity : int(b.segments, 0, MAX_SEGMENTS); 242 const last = b.last === null ? null : int(b.last, 0, MAX_SEGMENTS); 243 if (!(every > 0) || every > MAX_SEGMENTS || segments === undefined || last === undefined) return null; 244 if (!Array.isArray(b.questions) || !Array.isArray(b.decisions)) return null; 245 const questions = b.questions.slice(0, MAX_QUESTIONS).map(question).filter(Boolean); 246 let form = null; 247 if (isPlain(b.form)) { 248 const name = str(b.form.name, MAX_NAME); 249 const sections = Array.isArray(b.form.sections) 250 ? b.form.sections.slice(0, MAX_SEGMENTS).filter((s) => str(s, MAX_NAME) !== undefined) 251 : []; 252 if (name && questions.some((q) => q.name === name && q.type === 'choice')) form = { name, sections }; 253 } 254 const types = new Map(questions.map((q) => [q.name, q.name === form?.name ? 'form' : q.type])); 255 const budget = { n: 20000 }; 256 const decisions = []; 257 b.decisions.slice(0, MAX_SEGMENTS).forEach((d, i) => { 258 const clean = d === undefined || d === null ? undefined : decision(d, types, budget); 259 if (clean && clean.segment === i) decisions[i] = clean; 260 }); 261 const reactions = []; 262 if (Array.isArray(b.reactions)) { 263 b.reactions.slice(0, MAX_SEGMENTS).forEach((r, i) => { 264 const fire = isPlain(r) ? int(r.fire, 0, 1e6) : undefined; 265 const sleep = isPlain(r) ? int(r.sleep, 0, 1e6) : undefined; 266 if (fire !== undefined && sleep !== undefined) reactions[i] = { fire, sleep }; 267 }); 268 } 269 const about = 270 typeof b.about === 'string' ? str(b.about) ?? '' : isPlain(b.about) && str(b.about.title) ? { title: b.about.title } : ''; 271 // each jev() apart, for a recorded performance; the booth works without 272 const jevs = b.jevs === undefined ? undefined : views(b.jevs, every, questions, types, budget); 273 // whether the frame is replaying a recorded performance (it is then not 274 // recorded again), and whether the song changed since 275 const replay = isPlain(b.replay) ? { changed: b.replay.changed === true } : null; 276 const model = str(b.model, MAX_NAME) ?? ''; 277 return compact({ 278 model, 279 about, 280 every, 281 segments, 282 questions, 283 form, 284 last, 285 decisions, 286 reactions, 287 replay, 288 jevs: jevs?.map((v) => ({ ...v, model })), 289 }); 290}
The meter's newest reading (meter.mjs latest()), for the mixer.
293export function cleanLevels(l) { 294 if (l === null) return null; 295 if (!isPlain(l) || !(num(l.rms) >= 0) || !(num(l.peak) >= 0)) return undefined; 296 const parts = {}; 297 if (isPlain(l.parts)) { 298 for (const [name, p] of Object.entries(l.parts).slice(0, MAX_PARTS)) { 299 if (name.length <= MAX_NAME && isPlain(p) && num(p.rms) >= 0 && num(p.peak) >= 0) parts[name] = { rms: p.rms, peak: p.peak }; 300 } 301 } 302 return { rms: l.rms, peak: l.peak, parts }; 303}
305const LOG_KINDS = new Set(['', 'error', 'warning', 'highlight']);
A message from the frame, rebuilt; null when it is not one.
308export function fromFrame(m) { 309 if (!isPlain(m) || typeof m.type !== 'string') return null; 310 switch (m.type) { 311 case 'ready': 312 return { type: 'ready' }; 313 case 'state': { 314 const playing = bool(m.playing); 315 const cycle = m.cycle === null ? null : num(m.cycle); 316 const cps = m.cps === null ? null : num(m.cps); 317 const error = m.error === null ? null : str(m.error, 500); 318 // which play it reports on (the page's `run`, 0 before any) 319 const run = m.run === undefined ? 0 : int(m.run, 0, Number.MAX_SAFE_INTEGER); 320 if (playing === undefined || cycle === undefined || cps === undefined || error === undefined || run === undefined) return null; 321 return { type: 'state', playing, cycle, cps: cps > 0 ? cps : null, error, run }; 322 } 323 case 'log': { 324 const message = typeof m.message === 'string' ? m.message.slice(0, 500) : null; 325 const kind = LOG_KINDS.has(m.kind) ? m.kind : ''; 326 return message === null ? null : { type: 'log', message, kind }; 327 } 328 case 'booth': { 329 const run = int(m.run, 0, Number.MAX_SAFE_INTEGER); 330 if (run === undefined) return null; 331 if (m.booth === null) return { type: 'booth', run, booth: null }; 332 const booth = cleanBooth(m.booth); 333 return booth ? { type: 'booth', run, booth } : null; 334 } 335 case 'levels': { 336 const levels = cleanLevels(m.levels); 337 return levels === undefined ? null : { type: 'levels', levels }; 338 } 339 case 'audio': 340 return ['running', 'suspended', 'closed', 'interrupted'].includes(m.state) ? { type: 'audio', state: m.state } : null; 341 case 'ask': { 342 const id = int(m.id, 0, Number.MAX_SAFE_INTEGER); 343 const body = str(m.body, MAX_ASK_BYTES); 344 const lead = m.lead === null ? null : num(m.lead); 345 const attempt = int(m.attempt, 0, 9); 346 if (id === undefined || body === undefined || lead === undefined || attempt === undefined) return null; 347 return { type: 'ask', id, body, lead, attempt }; 348 } 349 default: 350 return null; 351 } 352}
A message from the page, as the frame checks it.
355export function fromPage(m) { 356 if (!isPlain(m) || typeof m.type !== 'string') return null; 357 switch (m.type) { 358 case 'play': { 359 const code = str(m.code, MAX_CODE); 360 const run = m.run === undefined ? 0 : int(m.run, 0, Number.MAX_SAFE_INTEGER); 361 const replay = m.replay === undefined || m.replay === null ? null : cleanReplay(m.replay); 362 if (code === undefined || run === undefined || replay === undefined) return null; 363 return { type: 'play', code, run, replay }; 364 } 365 case 'stop': 366 case 'resume': 367 return { type: m.type }; 368 case 'react': { 369 const segment = int(m.segment, 0, MAX_SEGMENTS); 370 return segment !== undefined && (m.kind === 'fire' || m.kind === 'sleep') ? { type: 'react', segment, kind: m.kind } : null; 371 } 372 case 'answer': { 373 const id = int(m.id, 0, Number.MAX_SAFE_INTEGER); 374 if (id === undefined) return null; 375 if (m.error !== undefined) return { type: 'answer', id, error: str(m.error, 500) ?? 'failed' }; 376 const status = int(m.status, 200, 599); 377 const body = str(m.body, MAX_ANSWER_BYTES); 378 if (status === undefined || body === undefined || !isPlain(m.headers)) return null; 379 const headers = Object.fromEntries( 380 Object.entries(m.headers).filter(([k, v]) => k.length <= 64 && str(v, 200) !== undefined), 381 ); 382 return { type: 'answer', id, status, headers, body }; 383 } 384 default: 385 return null; 386 } 387}
What the frame sends of its booth (boothStore.mjs's combine()): plain data only. Functions stay in the frame; each view's decisions go as data, and of a replay only whether it is one and whether the song changed.
392export function boothData(booth) { 393 if (!booth) return null; 394 let data; 395 try { 396 // JSON drops functions (and turns holes into null); segments may be 397 // Infinity, which JSON cannot carry and structured cloning can 398 data = JSON.parse( 399 JSON.stringify({ 400 model: booth.model, 401 about: booth.about, 402 every: booth.every, 403 questions: booth.questions, 404 form: booth.form ? { name: booth.form.name, sections: booth.form.sections } : null, 405 last: booth.last, 406 decisions: booth.decisions, 407 reactions: booth.reactions, 408 // each jev() apart (boothStore.mjs's combine() keeps them as `jevs`) 409 jevs: (booth.jevs ?? [booth]).map((v) => ({ 410 every: v.every, 411 form: v.form ? { name: v.form.name } : null, 412 decisions: v.decisions, 413 })), 414 replay: booth.replay ? { changed: Boolean(booth.replay.changed) } : null, 415 }), 416 ); 417 } catch { 418 return null; 419 } 420 return { ...data, segments: booth.segments }; 421}
Text for the page's console, which renders its messages as HTML (repl/components/panel/ConsoleTab.jsx): anything a listener wrote is escaped first.