// One temporary host + one phone. Only controller and RTC signaling messages cross this boundary. export const ROOM_TTL = 2 * 60 * 60 * 1000; export const INPUT_TIMEOUT = 1000; export const validBits = bits => Number.isInteger(bits) && bits >= 0 && bits <= 0xfff0 && !(bits & 15); export const token = () => Array.from(crypto.getRandomValues(new Uint8Array(16)), byte => byte.toString(16).padStart(2, '0')).join(''); export const validToken = value => /^[a-f0-9]{32}$/.test(value ?? ''); const contexts = ['title', 'play', 'pause', 'controllers', 'leaderboard', 'entry', 'options']; // Sockets expose send/close and attachment getters; both Workers and the local server reuse this. export class PairingRoom { constructor(config, sockets, now = Date.now()) { this.config = config; this.sockets = sockets; this.now = now; } peer(role) { return this.sockets.find(socket => socket.meta().role === role); } send(socket, message) { try { socket?.send(JSON.stringify(message)); } catch { /* Close/watchdog releases input. */ } } receive(socket, raw) { if (typeof raw !== 'string' || raw.length > 16384 || this.now >= this.config.expires) return socket.close(1008, 'Pairing expired'); let message; try { message = JSON.parse(raw); } catch { return socket.close(1008, 'Invalid message'); } if (!message || typeof message !== 'object') return socket.close(1008, 'Invalid message'); const meta = socket.meta(); if (!meta.role) { const role = message.role; if (message.type !== 'auth' || !['host', 'phone'].includes(role) || message.key !== this.config[role === 'host' ? 'hostKey' : 'joinKey'] || this.peer(role) || (role === 'phone' && !this.peer('host'))) return socket.close(1008, 'Pairing unavailable'); socket.attach({...meta, role, windowAt: this.now, count: 0}); this.send(socket, {type: 'ready', expires: this.config.expires}); this.send(this.peer('host'), {type: 'presence', connected: !!this.peer('phone')}); return; } // Bound message floods without blocking ordinary touch changes and heartbeats. const count = this.now - meta.windowAt >= 1000 ? 1 : meta.count + 1; if (count > 120) return socket.close(1008, 'Too many messages'); socket.attach({...meta, count, windowAt: this.now - meta.windowAt >= 1000 ? this.now : meta.windowAt}); if (message.type === 'ping') return this.send(socket, {type: 'pong', at: message.at}); const target = this.peer(meta.role === 'host' ? 'phone' : 'host'); if (meta.role === 'phone' && message.type === 'input' && validBits(message.bits) && Number.isSafeInteger(message.seq) && message.seq >= 0) return this.send(target, {type: 'input', bits: message.bits, seq: message.seq}); if (meta.role === 'phone' && message.type === 'pause' && (message.active === undefined || typeof message.active === 'boolean')) return this.send(target, {type: 'pause', active: message.active === true}); if (meta.role === 'host' && message.type === 'state' && typeof message.active === 'boolean' && typeof message.ready === 'boolean' && (message.seq === undefined || (Number.isSafeInteger(message.seq) && message.seq >= 0)) && (message.context === undefined || contexts.includes(message.context))) return this.send(target, {type: 'state', active: message.active, ready: message.ready, ...(message.seq === undefined ? {} : {seq: message.seq}), ...(message.context === undefined ? {} : {context: message.context})}); if (meta.role === 'phone' && message.type === 'view' && typeof message.enabled === 'boolean' && Number.isSafeInteger(message.seq) && message.seq >= 0) return this.send(target, {type: 'view', enabled: message.enabled, seq: message.seq}); if (meta.role === 'host' && message.type === 'view-state' && ['off', 'connecting', 'live', 'unavailable'].includes(message.status) && Number.isSafeInteger(message.seq) && message.seq >= 0) return this.send(target, {type: 'view-state', status: message.status, seq: message.seq}); if (message.type === 'signal' && message.signal && typeof message.signal === 'object') return this.send(target, {type: 'signal', signal: message.signal}); socket.close(1008, 'Invalid controller message'); } closed(socket) { if (socket.meta().role === 'host') for (const other of this.sockets) other.close(1000, 'Game screen disconnected'); if (socket.meta().role === 'phone') this.send(this.peer('host'), {type: 'presence', connected: false}); } }