#!/usr/bin/env node /** * Cordn-native browser probe. * * The Node live test proves the MLS logic; this proves the **browser runtime** * of the same stack — the built native chunk actually loading in Chromium, then * a real session against the coordinator over a browser WebSocket, with MLS * crypto and the Dexie/IndexedDB store. * * It is deliberately NOT part of `npm test` (it needs network + Chromium): * * node scripts/cordn-browser-probe.mjs * * Exit 0 = the browser stack worked end to end. */ import { createServer } from 'node:http'; import { spawn } from 'node:child_process'; import { readFile, readdir } from 'node:fs/promises'; import { existsSync, rmSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join, extname, normalize } from 'node:path'; import { tmpdir } from 'node:os'; import net from 'node:net'; import { randomBytes } from 'node:crypto'; import WebSocket from 'ws'; const root = join(dirname(fileURLToPath(import.meta.url)), '..'); const distDir = join(root, 'dist'); const assetsDir = join(distDir, 'assets'); const chromePath = process.env.CHROME_PATH || '/snap/bin/chromium'; const log = (m) => console.log(`[cordn-probe] ${m}`); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); // The SkateSpots coordinator (public facts, same as src/lib/coordinator.ts). const COORDINATOR_PUBKEY = '02816a05b1d4e70a7df9c712ef6f7272774269d65bd8a2f262ce0330d0885693'; const COORDINATOR_RELAY = 'wss://relay.skate-spots.org'; if (!existsSync(join(distDir, 'index.html'))) { console.error('[cordn-probe] dist/index.html missing — run `npm run build` first.'); process.exit(1); } if (!existsSync(chromePath)) { console.error(`[cordn-probe] Chromium not found at ${chromePath}. Set CHROME_PATH.`); process.exit(1); } // Find the lazy chunk that carries the native Cordn module (the barrel chunk — // it contains the coordinator method strings and the store factory). const assets = (await readdir(assetsDir)).filter((f) => f.endsWith('.js')); let nativeChunk = null; for (const f of assets) { const text = await readFile(join(assetsDir, f), 'utf8'); if (text.includes('kp_publish') && text.includes('createCordnStore')) { nativeChunk = f; break; } } if (!nativeChunk) { console.error('[cordn-probe] could not find the native Cordn chunk in dist/assets (build it, and make sure something imports src/lib/cordn/native).'); process.exit(1); } log(`native chunk: ${nativeChunk}`); const MIME = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.webmanifest': 'application/manifest+json', '.png': 'image/png', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.wasm': 'application/wasm', }; const freePort = () => new Promise((res) => { const s = net.createServer(); s.on('error', () => res(0)); s.listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => res(p)); }); }); const servePort = await freePort(); const server = createServer(async (req, res) => { try { const urlPath = decodeURIComponent((req.url || '/').split('?')[0]); let filePath = join(distDir, normalize(urlPath).replace(/^(\.\.[/\\])+/, '')); if (urlPath === '/' || !extname(filePath)) filePath = join(distDir, 'index.html'); let body; try { body = await readFile(filePath); } catch { filePath = join(distDir, 'index.html'); body = await readFile(filePath); } res.writeHead(200, { 'Content-Type': MIME[extname(filePath)] || 'application/octet-stream' }); res.end(body); } catch { res.writeHead(500); res.end('error'); } }); await new Promise((r) => server.listen(servePort, '127.0.0.1', r)); const appUrl = `http://127.0.0.1:${servePort}/`; const cdpPort = await freePort(); const profileDir = join(tmpdir(), `skatespots-cordn-probe-${process.pid}-${cdpPort}`); const chrome = spawn(chromePath, [ '--headless=new', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage', '--disable-extensions', '--no-first-run', '--no-default-browser-check', `--user-data-dir=${profileDir}`, `--remote-debugging-port=${cdpPort}`, appUrl, ], { stdio: ['ignore', 'ignore', 'pipe'] }); chrome.stderr.on('data', () => {}); const cleanup = () => { try { chrome.kill('SIGKILL'); } catch {} try { server.close(); } catch {} try { rmSync(profileDir, { recursive: true, force: true }); } catch {} }; process.on('exit', cleanup); async function findPageTarget() { for (let i = 0; i < 60; i++) { try { const list = await (await fetch(`http://127.0.0.1:${cdpPort}/json`)).json(); const p = list.find((t) => t.type === 'page' && t.url.startsWith('http')); if (p) return p; } catch {} await sleep(300); } return null; } const target = await findPageTarget(); if (!target) { console.error('[cordn-probe] FAIL: no Chromium page target'); cleanup(); process.exit(1); } const ws = new WebSocket(target.webSocketDebuggerUrl, { maxPayload: 64 * 1024 * 1024 }); let id = 0; const pend = new Map(); const exceptions = []; ws.on('message', (d) => { const m = JSON.parse(d); if (m.id && pend.has(m.id)) { pend.get(m.id)(m); pend.delete(m.id); return; } if (m.method === 'Runtime.exceptionThrown') { const e = m.params.exceptionDetails; exceptions.push(e.exception?.description || e.text || 'unknown exception'); } }); const send = (method, params = {}) => new Promise((res) => { const i = ++id; pend.set(i, res); ws.send(JSON.stringify({ id: i, method, params })); }); await new Promise((r) => ws.on('open', r)); await send('Runtime.enable'); await send('Page.enable'); // Wait for the app to mount so the module graph is warm. for (let i = 0; i < 40; i++) { const kids = (await send('Runtime.evaluate', { expression: 'document.getElementById("root")?.children.length ?? 0', returnByValue: true }))?.result?.result?.value; if (kids > 0) break; await sleep(300); } const privateKeyHex = randomBytes(32).toString('hex'); const accountKey = `probe-${Date.now()}`; const groupId = `browser-probe-${Date.now()}`; const probe = `(async () => { const ns = await import('/assets/${nativeChunk}'); const m = ns.CordnSession ? ns : (ns.a || ns.default || ns); const store = m.createCordnStore('${accountKey}'); const session = await m.CordnSession.open({ coordinator: { serverPubkey: '${COORDINATOR_PUBKEY}', relays: ['${COORDINATOR_RELAY}'] }, privateKeyHex: '${privateKeyHex}', store, }); await session.generateKeyPackage('probe'); await session.publishKeyPackage('probe'); const group = await session.createGroup({ groupId: '${groupId}', name: 'Browser probe' }); await session.send(group.gid, 'hello from headless chromium'); const local = session.messages(group.gid).map((x) => x.content); await session.disconnect(); // Reopen with the same IndexedDB store: the group must restore. const reopened = await m.CordnSession.open({ coordinator: { serverPubkey: '${COORDINATOR_PUBKEY}', relays: ['${COORDINATOR_RELAY}'] }, privateKeyHex: '${privateKeyHex}', store: m.createCordnStore('${accountKey}'), }); const restored = reopened.listGroups().length; const restoredMsgs = reopened.listGroups()[0] ? reopened.messages(reopened.listGroups()[0].gid).length : 0; await reopened.disconnect(); return { ok: true, pubkey: session.pubkey, gid: group.gid, local, restored, restoredMsgs }; })()`; const result = await send('Runtime.evaluate', { expression: probe, returnByValue: true, awaitPromise: true }); const value = result?.result?.result?.value; const thrown = result?.result?.exceptionDetails; ws.close(); if (thrown) { console.error('[cordn-probe] FAIL — the browser threw:'); console.error(' ' + (thrown.exception?.description || thrown.text)); process.exit(1); } if (exceptions.length > 0) { console.error('[cordn-probe] FAIL — uncaught exceptions in the page:'); for (const e of exceptions) console.error(' • ' + e); process.exit(1); } if (!value?.ok) { console.error('[cordn-probe] FAIL — unexpected result:', JSON.stringify(value)); process.exit(1); } log(`PASS — native stack ran in Chromium (${COORDINATOR_RELAY})`); log(` pubkey: ${value.pubkey}`); log(` group: ${value.gid}`); log(` sent locally: ${JSON.stringify(value.local)}`); log(` restored (IDB): ${value.restored} group(s), ${value.restoredMsgs} message(s)`); process.exit(0);