function generatePeerId() { const bytes = new Uint8Array(8); crypto.getRandomValues(bytes); return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); } async function generateSecret() { const secret = new Uint8Array(32); crypto.getRandomValues(secret); return secret; } async function commit(secret) { const hash = await crypto.subtle.digest('SHA-256', secret); return new Uint8Array(hash); } function arraysEqual(a, b) { if (a.length !== b.length) return false; let diff = 0; for (let i = 0; i < a.length; i++) { diff |= a[i] ^ b[i]; } return diff === 0; } function combineAll(secrets) { const result = new Uint8Array(32); for (const secret of secrets) { for (let i = 0; i < 32; i++) { result[i] ^= secret[i]; } } return result; } async function computePeerResult(sharedSeed, peerId) { const data = new TextEncoder().encode(peerId); const combined = new Uint8Array(sharedSeed.length + data.length); combined.set(sharedSeed); combined.set(data, sharedSeed.length); const hash = await crypto.subtle.digest('SHA-256', combined); return new Uint8Array(hash)[0] & 1; } function bufToBase64(buf) { const bytes = new Uint8Array(buf); let binary = ''; for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } function base64ToBuf(base64) { const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes; } function hexToBuf(hex) { const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); } return bytes; } /* ─── QR compression: deflate-raw + base45 ─── */ const SDP_ALLOW_A_PREFIXES = ['a=ice-ufrag:', 'a=ice-pwd:', 'a=fingerprint:', 'a=candidate:', 'a=end-of-candidates']; function stripSDP(sdp) { return sdp.split(/\r?\n/).filter(line => { if (!line) return false; if (line.startsWith('a=')) { return SDP_ALLOW_A_PREFIXES.some(p => line.startsWith(p)); } return true; }).map(line => { if (!line.startsWith('a=candidate:')) return line; const parts = line.split(' '); if (parts[2] === 'tcp') return null; if (parts[7] !== 'host') return null; if (parts[4] && parts[4].includes(':')) return null; return line; }).filter(Boolean).join('\r\n'); } async function pack(obj) { const json = JSON.stringify(obj); const blob = new Blob([json]); const cs = new CompressionStream('deflate-raw'); const ab = await new Response(blob.stream().pipeThrough(cs)).arrayBuffer(); return new Uint8Array(ab); } async function unpack(bytes) { const blob = new Blob([bytes]); const ds = new DecompressionStream('deflate-raw'); const ab = await new Response(blob.stream().pipeThrough(ds)).arrayBuffer(); return JSON.parse(new TextDecoder().decode(ab)); } const B45 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'; function encode45(buf) { const bytes = new Uint8Array(buf); let s = ''; for (let i = 0; i < bytes.length; i += 2) { if (i + 1 < bytes.length) { const v = bytes[i] * 256 + bytes[i + 1]; s += B45[v % 45] + B45[Math.floor(v / 45) % 45] + B45[Math.floor(v / 2025)]; } else { const v = bytes[i]; s += B45[v % 45] + B45[Math.floor(v / 45)]; } } return s; } function decode45(str) { const bytes = []; for (let i = 0; i < str.length; i += 3) { if (i + 2 < str.length) { const v = B45.indexOf(str[i]) + B45.indexOf(str[i + 1]) * 45 + B45.indexOf(str[i + 2]) * 2025; bytes.push(Math.floor(v / 256), v % 256); } else { const v = B45.indexOf(str[i]) + B45.indexOf(str[i + 1]) * 45; bytes.push(v); } } return Uint8Array.from(bytes); }