const $ = (selector, root = document) => root.querySelector(selector); const TOKEN_KEY = `wolloseum.session.${document.body?.dataset.page || 'app'}`; const encoder = new TextEncoder(); function setMessage(element, text, kind = '') { if (!element) return; element.textContent = text; element.className = `message ${kind}`.trim(); } function fromBase64(value) { return Uint8Array.from(atob(value), character => character.charCodeAt(0)); } function readDerElement(bytes, offset) { const start = offset; const tag = bytes[offset++]; let length = bytes[offset++]; if (tag === undefined || length === undefined) return null; if (length & 0x80) { const lengthBytes = length & 0x7f; if (!lengthBytes || lengthBytes > 4 || offset + lengthBytes > bytes.length) return null; length = 0; for (let index = 0; index < lengthBytes; index++) length = length * 256 + bytes[offset++]; } const contentStart = offset; const end = contentStart + length; if (end > bytes.length) return null; return { tag, start, contentStart, end }; } function derLength(length) { if (length < 0x80) return [length]; const bytes = []; while (length) { bytes.unshift(length & 0xff); length >>>= 8; } return [0x80 | bytes.length, ...bytes]; } function normalizeEd25519Pkcs8(bytes) { const sequence = readDerElement(bytes, 0); const version = sequence?.tag === 0x30 && sequence.end === bytes.length ? readDerElement(bytes, sequence.contentStart) : null; if (version?.tag !== 0x02 || version.end !== version.contentStart + 1) return bytes; if (bytes[version.contentStart] === 0) return bytes; if (bytes[version.contentStart] !== 1) return bytes; const algorithm = readDerElement(bytes, version.end); const privateKey = algorithm?.tag === 0x30 ? readDerElement(bytes, algorithm.end) : null; const publicKey = privateKey?.tag === 0x04 ? readDerElement(bytes, privateKey.end) : null; if (algorithm?.tag !== 0x30 || privateKey?.tag !== 0x04 || publicKey?.tag !== 0x81 || publicKey.end !== sequence.end) { return bytes; } const body = new Uint8Array(3 + algorithm.end - algorithm.start + privateKey.end - privateKey.start); body.set([0x02, 0x01, 0x00]); body.set(bytes.subarray(algorithm.start, algorithm.end), 3); body.set(bytes.subarray(privateKey.start, privateKey.end), 3 + algorithm.end - algorithm.start); const encodedLength = derLength(body.length); const normalized = new Uint8Array(1 + encodedLength.length + body.length); normalized.set([0x30, ...encodedLength]); normalized.set(body, 1 + encodedLength.length); return normalized; } function toBase64(value) { return btoa(String.fromCharCode(...new Uint8Array(value))); } function toPem(label, value) { const base64 = toBase64(value).match(/.{1,64}/g).join('\n'); return `-----BEGIN ${label}-----\n${base64}\n-----END ${label}-----\n`; } async function api(path, options = {}) { const headers = new Headers(options.headers || {}); const token = localStorage.getItem(TOKEN_KEY); if (token) headers.set('Authorization', `Bearer ${token}`); if (options.body && typeof options.body === 'string') headers.set('Content-Type', 'application/json'); const response = await fetch(path, { ...options, headers }); if (response.status === 204) return null; const contentType = response.headers.get('content-type') || ''; const body = contentType.includes('application/json') ? await response.json() : await response.text(); if (!response.ok) throw new Error(body?.error || `Request failed (${response.status})`); return body; } async function readCredential(file) { const text = await file.text(); try { const bundle = JSON.parse(text); if (bundle.client_id && bundle.private_key_pem) return bundle; } catch (_) { } const identity = $('#identity-id').value.trim(); if (!identity) throw new Error('Enter the identity ID printed by the server or supplied by your administrator.'); return { client_id: identity, private_key_pem: text }; } async function signIn() { const file = $('#private-key-file').files[0]; if (!file) throw new Error('Choose your private key file.'); const credential = await readCredential(file); const encodedKey = credential.private_key_pem.match(/-----BEGIN PRIVATE KEY-----([\s\S]*?)-----END PRIVATE KEY-----/); if (!encodedKey) throw new Error('Expected an Ed25519 PKCS#8 private key.'); const pkcs8 = normalizeEd25519Pkcs8(fromBase64(encodedKey[1].replace(/\s/g, ''))); let privateKey; try { privateKey = await crypto.subtle.importKey('pkcs8', pkcs8, { name: 'Ed25519' }, false, ['sign']); } catch (error) { if (error.name === 'NotSupportedError') { throw new Error('This browser does not support Ed25519 Web Crypto. Update the browser and try again.'); } throw new Error(`Ed25519 key import failed (${error.name}): ${error.message}. Choose data/keys/admin-private.pem or a generated identity JSON bundle.`); } const issued = await api('/api/auth/challenge', { method: 'POST', body: JSON.stringify({ client_id: credential.client_id }), }); const challenge = fromBase64(issued.challenge); let signature; try { signature = await crypto.subtle.sign('Ed25519', privateKey, challenge); } catch (error) { throw new Error(`Ed25519 challenge signing failed (${error.name}): ${error.message}.`); } const session = await api('/api/auth/login', { method: 'POST', body: JSON.stringify({ client_id: credential.client_id, challenge: issued.challenge, signature: toBase64(signature) }), }); localStorage.setItem(TOKEN_KEY, session.token); return api('/api/me'); } function logout() { localStorage.removeItem(TOKEN_KEY); location.reload(); } function connectEvents(onEvent, onConnection = () => { }) { let reconnectDelay = 1000; let socket; let stopped = false; const connect = () => { if (stopped) return; const scheme = location.protocol === 'https:' ? 'wss:' : 'ws:'; socket = new WebSocket(`${scheme}//${location.host}/api/events`); socket.onopen = () => { reconnectDelay = 1000; onConnection(true); }; socket.onmessage = message => { try { onEvent(JSON.parse(message.data)); } catch (_) { } }; socket.onclose = () => { onConnection(false); if (!stopped) setTimeout(connect, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 2, 15000); }; socket.onerror = () => socket.close(); }; connect(); return () => { stopped = true; socket?.close(); }; } function eventRow(event) { const row = document.createElement('div'); row.className = 'event-row'; const sequence = document.createElement('span'); sequence.className = 'event-sequence'; sequence.textContent = `#${event.sequence}`; const name = document.createElement('span'); name.className = 'event-name'; name.textContent = event.event; const data = document.createElement('span'); data.className = 'event-data'; data.textContent = JSON.stringify(event.data ?? {}); row.append(sequence, name, data); return row; } function bootViewer() { const list = $('#event-list'); const empty = $('#event-empty'); const status = $('#socket-status'); const sequence = $('#latest-sequence'); const total = $('#event-count'); const search = $('#event-search'); let count = 0; const events = []; const redraw = () => { list.replaceChildren(); const query = search.value.trim().toLowerCase(); const visible = events.filter(event => `${event.event} ${JSON.stringify(event.data)}`.toLowerCase().includes(query)); empty.classList.toggle('hidden', visible.length > 0); visible.forEach(event => list.append(eventRow(event))); }; search.addEventListener('input', redraw); connectEvents(event => { count += 1; events.unshift(event); if (events.length > 250) events.pop(); sequence.textContent = String(event.sequence); total.textContent = String(count); redraw(); }, connected => { status.classList.toggle('live', connected); status.classList.toggle('offline', !connected); $('#socket-label').textContent = connected ? 'LIVE' : 'RECONNECTING'; }); } function bootLogin() { const form = $('#login-form'); const panel = $('#login-panel'); const message = $('#login-message'); const page = document.body.dataset.page; const app = page === 'admin' ? $('#admin-app') : $('#user-app'); const account = $('#account-name'); const reveal = async identity => { const requiredAdmin = page === 'admin'; if (identity.is_admin !== requiredAdmin) { throw new Error(requiredAdmin ? 'This key does not belong to an administrator.' : 'Use the administration page to sign in with an admin key.'); } panel.classList.add('hidden'); app.classList.remove('hidden'); account.textContent = identity.username; if (requiredAdmin) await bootAdmin(identity); else await bootUser(identity); }; form.addEventListener('submit', async event => { event.preventDefault(); const button = $('button[type="submit"]', form); button.disabled = true; setMessage(message, 'Verifying challenge...'); try { await reveal(await signIn()); } catch (error) { setMessage(message, error.message, 'error'); } finally { button.disabled = false; } }); $('#logout-button, #admin-logout').addEventListener('click', logout); $('#private-key-file').addEventListener('change', async event => { const file = event.currentTarget.files[0]; if (!file || !file.name.endsWith('.json')) return; try { const bundle = JSON.parse(await file.text()); if (bundle.client_id) $('#identity-id').value = bundle.client_id; } catch (_) { } }); if (localStorage.getItem(TOKEN_KEY)) { api('/api/me').then(reveal).catch(() => localStorage.removeItem(TOKEN_KEY)); } } async function bootUser(identity) { $('#player-id').textContent = identity.client_id; $('#agent-digest').textContent = identity.agent_sha256 || 'No agent uploaded'; $('#queued-agent-digest').textContent = identity.queued_agent_sha256 || 'None'; const status = await api('/api/status'); paintGameStatus(status.running); $('#agent-form').addEventListener('submit', async event => { event.preventDefault(); const file = $('#agent-file').files[0]; if (!file) return setMessage($('#agent-message'), 'Choose a compiled WASM module.', 'error'); const button = $('button[type="submit"]', event.currentTarget); button.disabled = true; setMessage($('#agent-message'), `Uploading ${file.name}...`); try { const result = await api('/api/update_agent', { method: 'POST', body: await file.arrayBuffer() }); $('#queued-agent-digest').textContent = result.sha256; setMessage($('#agent-message'), `Agent queued | ${result.bytes.toLocaleString()} bytes`, 'success'); } catch (error) { setMessage($('#agent-message'), error.message, 'error'); } finally { button.disabled = false; } }); connectEvents(event => { if (event.event === 'snapshot' || event.event === 'game_state_changed') paintGameStatus(event.data.running); if (event.event === 'agents_activated') { api('/api/me').then(identity => { $('#agent-digest').textContent = identity.agent_sha256 || 'No agent uploaded'; $('#queued-agent-digest').textContent = identity.queued_agent_sha256 || 'None'; }).catch(error => setMessage($('#agent-message'), error.message, 'error')); } }); } function paintGameStatus(running) { const chip = $('#game-status'); if (!chip) return; chip.classList.toggle('running', running); chip.classList.toggle('paused', !running); $('#game-status-label').textContent = running ? 'RUNNING' : 'PAUSED'; const copy = $('#game-status-copy'); if (copy) copy.textContent = running ? 'RUNNING' : 'PAUSED'; } async function bootAdmin() { const status = await api('/api/status'); paintGameStatus(status.running); $('#game-control').addEventListener('click', async () => { const running = $('#game-status-label').textContent === 'RUNNING'; const button = $('#game-control'); button.disabled = true; try { await api(running ? '/api/stop_game' : '/api/start_game', { method: 'POST', body: '' }); paintGameStatus(!running); button.textContent = running ? 'Resume simulation' : 'Pause simulation'; } catch (error) { setMessage($('#admin-message'), error.message, 'error'); } finally { button.disabled = false; } }); $('#game-control').textContent = status.running ? 'Pause simulation' : 'Resume simulation'; $('#user-form').addEventListener('submit', createUser); $('#core-form').addEventListener('submit', uploadCore); $('#simulation-settings-form').addEventListener('submit', saveSimulationSettings); await loadSimulationSettings(); await refreshUsers(); connectEvents(event => { if (event.event === 'snapshot' || event.event === 'game_state_changed') { paintGameStatus(event.data.running); $('#game-control').textContent = event.data.running ? 'Pause simulation' : 'Resume simulation'; } if (event.event === 'user_created' || event.event === 'user_deleted' || event.event === 'agent_queued' || event.event === 'agents_activated') refreshUsers(); }); } async function loadSimulationSettings() { try { const settings = await api('/api/settings'); $('#ticks-per-day').value = settings.ticks_per_day; $('#days-per-week').value = settings.days_per_week; $('#days-per-month').value = settings.days_per_month; $('#agent-activation').value = settings.agent_activation; } catch (error) { setMessage($('#simulation-settings-message'), error.message, 'error'); } } async function saveSimulationSettings(event) { event.preventDefault(); const form = event.currentTarget; const button = $('button[type="submit"]', form); button.disabled = true; const settings = { ticks_per_day: Number($('#ticks-per-day').value), days_per_week: Number($('#days-per-week').value), days_per_month: Number($('#days-per-month').value), agent_activation: $('#agent-activation').value, }; try { await api('/api/settings', { method: 'PUT', body: JSON.stringify(settings) }); setMessage($('#simulation-settings-message'), 'Settings saved.', 'success'); } catch (error) { setMessage($('#simulation-settings-message'), error.message, 'error'); } finally { button.disabled = false; } } async function createUser(event) { event.preventDefault(); const username = $('#new-username').value.trim(); const button = $('button[type="submit"]', event.currentTarget); const downloadLink = $('#identity-download'); if (downloadLink.dataset.objectUrl) URL.revokeObjectURL(downloadLink.dataset.objectUrl); delete downloadLink.dataset.objectUrl; downloadLink.removeAttribute('href'); downloadLink.classList.add('hidden'); button.disabled = true; setMessage($('#create-user-message'), 'Generating identity keypair...'); try { const pair = await crypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify']); const publicKey = await crypto.subtle.exportKey('raw', pair.publicKey); const privateKey = await crypto.subtle.exportKey('pkcs8', pair.privateKey); const identity = await api('/api/create_user', { method: 'POST', body: JSON.stringify({ username, public_key: toBase64(publicKey) }), }); const bundle = JSON.stringify({ username, client_id: identity.client_id, private_key_pem: toPem('PRIVATE KEY', privateKey) }, null, 2); downloadLink.dataset.objectUrl = URL.createObjectURL(new Blob([bundle], { type: 'application/json' })); downloadLink.href = downloadLink.dataset.objectUrl; downloadLink.download = `${username}-identity.json`; downloadLink.classList.remove('hidden'); $('#new-username').value = ''; setMessage($('#create-user-message'), `Created ${username}. Download the private-key bundle before closing this page.`, 'success'); await refreshUsers(); } catch (error) { setMessage($('#create-user-message'), error.message, 'error'); } finally { button.disabled = false; } } async function refreshUsers() { try { const users = await api('/api/users'); const list = $('#user-list'); list.replaceChildren(); users.forEach(user => { const row = document.createElement('div'); row.className = 'user-row'; const info = document.createElement('div'); const name = document.createElement('div'); name.className = 'user-name'; name.textContent = `${user.username}${user.is_admin ? ' | admin' : ''}`; const id = document.createElement('div'); id.className = 'user-id'; id.textContent = user.client_id; const meta = document.createElement('div'); meta.className = 'user-meta'; const agentStatus = [ user.has_agent ? `Active | ${user.agent_sha256 || 'uploaded'}` : 'No active agent', user.queued_agent_sha256 ? `Queued | ${user.queued_agent_sha256}` : '', ].filter(Boolean); meta.textContent = agentStatus.join(' | '); info.append(name, id, meta); row.append(info); if (!user.is_admin) { const remove = document.createElement('button'); remove.className = 'button danger'; remove.type = 'button'; remove.textContent = 'Remove'; remove.addEventListener('click', async () => { if (!confirm(`Remove ${user.username} and their uploaded agent?`)) return; try { await api('/api/delete_user', { method: 'POST', body: JSON.stringify({ client_id: user.client_id }) }); await refreshUsers(); } catch (error) { setMessage($('#admin-message'), error.message, 'error'); } }); row.append(remove); } list.append(row); }); $('#user-count').textContent = String(users.filter(user => !user.is_admin).length); } catch (error) { setMessage($('#admin-message'), error.message, 'error'); } } async function uploadCore(event) { event.preventDefault(); const file = $('#core-file').files[0]; if (!file) return setMessage($('#core-message'), 'Choose a WASM core module.', 'error'); const button = $('button[type="submit"]', event.currentTarget); button.disabled = true; setMessage($('#core-message'), `Validating ${file.name}...`); try { const result = await api('/api/upload_core', { method: 'POST', body: await file.arrayBuffer() }); setMessage($('#core-message'), `Core module accepted | ${result.sha256}`, 'success'); } catch (error) { setMessage($('#core-message'), error.message, 'error'); } finally { button.disabled = false; } } if (document.body.dataset.page === 'viewer') bootViewer(); else bootLogin();