import { createServer } from 'node:http'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { WebSocketServer } from 'ws'; import demoSession from '../fixtures/demo-session.json' with { type: 'json' }; import webContract from '../protocol/web-contract.json' with { type: 'json' }; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const rootDir = path.resolve(__dirname, '..'); const distDir = path.join(rootDir, 'dist'); const DEFAULT_REMOTE_PORT = webContract.defaultRemotePort; const DEFAULT_REMOTE_HOST = webContract.defaultRemoteHost; const REMOTE_BASE_PATH = webContract.basePath; const REMOTE_WS_PATH = webContract.webSocketPath; const REMOTE_HEALTH_PATH = webContract.healthPath; const REMOTE_ASSETS_PREFIX = webContract.assetsPath; const host = process.env.HOST || DEFAULT_REMOTE_HOST; const port = Number(process.env.PORT || DEFAULT_REMOTE_PORT); const trainerMeta = structuredClone(demoSession.trainerMeta); const trainerValues = structuredClone(demoSession.trainerValues); const wss = new WebSocketServer({ noServer: true }); function jsonMessage(type, payload, requestId = null) { return JSON.stringify({ type, version: webContract.protocolVersion, requestId, payload, }); } function sendSnapshot(ws) { ws.send( jsonMessage('trainer_meta', trainerMeta) ); ws.send( jsonMessage('trainer_values', trainerValues) ); } function broadcast(type, payload, requestId = null) { const serialized = jsonMessage(type, payload, requestId); for (const client of wss.clients) { if (client.readyState === 1) { client.send(serialized); } } } function normalizeValue(target, value) { const cheat = trainerMeta.schema.cheats.find((entry) => entry.target === target); if (!cheat) { return value; } if (cheat.type === 'toggle') { return Boolean(value); } if (cheat.type === 'slider' || cheat.type === 'number') { const numeric = typeof value === 'string' ? Number(value) : value; return Number.isFinite(numeric) ? numeric : trainerValues.values[target]; } return value; } function contentTypeFor(filePath) { if (filePath.endsWith('.html')) return 'text/html; charset=utf-8'; if (filePath.endsWith('.js')) return 'application/javascript; charset=utf-8'; if (filePath.endsWith('.css')) return 'text/css; charset=utf-8'; if (filePath.endsWith('.json')) return 'application/json; charset=utf-8'; if (filePath.endsWith('.svg')) return 'image/svg+xml'; return 'application/octet-stream'; } async function serveFile(res, filePath) { try { const content = await readFile(filePath); res.writeHead(200, { 'Content-Type': contentTypeFor(filePath), 'Cache-Control': 'no-store', }); res.end(content); } catch { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('Not found'); } } const server = createServer(async (req, res) => { const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`); if (url.pathname === '/' || url.pathname === '') { res.writeHead(302, { Location: REMOTE_BASE_PATH }); res.end(); return; } if (url.pathname === REMOTE_BASE_PATH.slice(0, -1)) { res.writeHead(302, { Location: REMOTE_BASE_PATH }); res.end(); return; } if (url.pathname === REMOTE_BASE_PATH) { await serveFile(res, path.join(distDir, 'index.html')); return; } if (url.pathname === REMOTE_HEALTH_PATH) { res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ ok: true, trainerId: trainerMeta.trainer.trainerId })); return; } if (url.pathname.startsWith(REMOTE_ASSETS_PREFIX)) { const relativePath = url.pathname.replace(REMOTE_BASE_PATH, ''); await serveFile(res, path.join(distDir, relativePath)); return; } res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('Not found'); }); server.on('upgrade', (request, socket, head) => { const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`); if (url.pathname !== REMOTE_WS_PATH) { socket.destroy(); return; } wss.handleUpgrade(request, socket, head, (ws) => { wss.emit('connection', ws, request); }); }); wss.on('connection', (ws) => { ws.on('error', console.error); ws.on('message', (raw) => { try { const message = JSON.parse(String(raw)); if (message?.version !== webContract.protocolVersion || typeof message?.type !== 'string' || !message?.payload) { ws.send(jsonMessage('error', { code: 'invalid_message', message: 'Expected a compatible protocol envelope.', }, message?.requestId ?? null)); return; } if (message?.type === 'hello') { ws.send( jsonMessage('hello_ack', { sessionId: `sess_${Date.now()}`, accepted: true, serverVersion: '0.1.0-demo', protocolVersion: webContract.protocolVersion, }, message.requestId ?? null) ); sendSnapshot(ws); return; } if (message?.type === 'set_value') { const target = message.payload?.target; if (message.payload?.trainerId !== trainerMeta.trainer.trainerId || typeof target !== 'string' || !(target in trainerValues.values)) { ws.send( jsonMessage('set_value_result', { ok: false, trainerId: trainerMeta.trainer.trainerId, target: typeof target === 'string' ? target : '', error: { code: 'invalid_target', message: 'Unknown cheat target.', }, }, message.requestId ?? null) ); return; } const previousValue = trainerValues.values[target]; const nextValue = normalizeValue(target, message.payload?.value); trainerValues.values[target] = nextValue; ws.send( jsonMessage('set_value_result', { ok: true, trainerId: trainerMeta.trainer.trainerId, target, }, message.requestId ?? null) ); broadcast('value_changed', { trainerId: trainerMeta.trainer.trainerId, target, value: nextValue, oldValue: previousValue, source: 'remote', cheatId: message.payload?.cheatId, }); } } catch (error) { ws.send( jsonMessage('error', { code: 'invalid_message', message: error instanceof Error ? error.message : 'Failed to parse client message.', }) ); } }); }); server.listen(port, host, () => { console.log(`Wand web panel bridge listening on http://${host === DEFAULT_REMOTE_HOST ? 'localhost' : host}:${port}${REMOTE_BASE_PATH}`); });