// puppeteer-core for the browser tests, with one difference: a page's // sockets to public relays never leave the machine. Throwaway test wallets // boot, subscribe and publish like real ones, and a suite run from one home // address gets that address banned (nos.lol, relay.damus.io, 2026-10-07). // So every socket to a host that isn't local is answered in the page by a // stand-in relay that has nothing stored: EVENT gets OK, REQ gets EOSE, // COUNT gets 0. Local relays (ws://127.0.0.1:…, a test's own) are untouched, // and a test's own WebSocket stubs still see every send, as before. // // A test that reads real content off the public relays launches with // { relays: 'read' }: its sockets are real, but nothing it publishes leaves // the page (each EVENT is answered OK in place). LIVE_RELAYS=read does the // same for any test. { relays: 'live' } (a test of a public service, like // the Cordn coordinator) or LIVE_RELAYS=1 turns the guard off, and // LIVE_RELAYS=relay.example.com,other.example lets just those hosts through. // puppeteer.connect (driving a real Chrome) is never guarded. // // Use: import puppeteer from './puppeteer.js'; (in place of 'puppeteer-core') import real from 'puppeteer-core'; const live = (process.env.LIVE_RELAYS || '').trim(); const allow = live && live !== '1' && live !== 'read' ? live.split(',').map((h) => h.trim()).filter(Boolean) : []; // Runs in the page before anything else. function relayGuard(allow, readOnly) { if (window.__relayGuard) return; window.__relayGuard = { blocked: [], sent: 0, events: [] }; // events: what didn't go out const Real = window.WebSocket; const nativeSend = Real.prototype.send; const nativeClose = Real.prototype.close; const fakes = new WeakSet(); const reading = new WeakSet(); // real public sockets in read mode const local = (h) => h === 'localhost' || h === '[::1]' || h === '0.0.0.0' || /^127\./.test(h) || h.endsWith('.localhost'); const answer = (ws, msg) => setTimeout(() => { if (ws.readyState !== 1) return; ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(msg) })); }, 5); const relay = (ws, d) => { let m; try { m = JSON.parse(d); } catch { return; } if (!Array.isArray(m)) return; window.__relayGuard.sent++; if (m[0] === 'EVENT' && m[1]?.id) { window.__relayGuard.events.push(m[1]); answer(ws, ['OK', m[1].id, true, '']); } else if (m[0] === 'REQ') answer(ws, ['EOSE', m[1]]); else if (m[0] === 'COUNT') answer(ws, ['COUNT', m[1], { count: 0 }]); else if (m[0] === 'AUTH' && m[1]?.id) answer(ws, ['OK', m[1].id, true, '']); }; // Every send goes through here, so a test that wraps // WebSocket.prototype.send (before or after) still sees a stand-in's. Real.prototype.send = function (d) { if (fakes.has(this)) { if (this.readyState !== 1) throw new DOMException('not open', 'InvalidStateError'); return relay(this, d); } if (reading.has(this) && typeof d === 'string' && /^\s*\[\s*"EVENT"/.test(d)) return relay(this, d); return nativeSend.call(this, d); }; Real.prototype.close = function (code, reason) { if (!fakes.has(this)) return nativeClose.call(this, code, reason); if (this.readyState >= 2) return; this.readyState = 2; setTimeout(() => { this.readyState = 3; this.dispatchEvent(new CloseEvent('close', { code: code || 1000, reason: reason || '', wasClean: true })); }, 0); }; const handlerProp = (ws, type) => { let fn = null; const call = (e) => fn && fn.call(ws, e); ws.addEventListener(type, call); Object.defineProperty(ws, 'on' + type, { get: () => fn, set: (v) => { fn = typeof v === 'function' ? v : null; }, configurable: true }); }; function WebSocket(url, protocols) { if (!new.target) throw new TypeError("Failed to construct 'WebSocket': Please use the 'new' operator"); let u; try { u = new URL(url, location.href); } catch { return new Real(url, protocols); } if (u.protocol !== 'wss:' && u.protocol !== 'ws:') return new Real(url, protocols); if (local(u.hostname) || allow.includes(u.hostname)) return new Real(url, protocols); if (readOnly) { const ws = new Real(url, protocols); reading.add(ws); return ws; } window.__relayGuard.blocked.push(u.href); // a genuine EventTarget (so dispatchEvent works) that passes instanceof const ws = Object.setPrototypeOf(new EventTarget(), Real.prototype); fakes.add(ws); Object.defineProperties(ws, { url: { value: u.href }, protocol: { value: '' }, extensions: { value: '' }, bufferedAmount: { value: 0 }, binaryType: { value: 'blob', writable: true }, readyState: { value: 0, writable: true }, }); for (const t of ['open', 'message', 'close', 'error']) handlerProp(ws, t); setTimeout(() => { if (ws.readyState !== 0) return; ws.readyState = 1; ws.dispatchEvent(new Event('open')); }, 0); return ws; } WebSocket.prototype = Real.prototype; for (const k of ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']) WebSocket[k] = Real[k]; window.WebSocket = WebSocket; } const guarded = new WeakSet(); const guardPage = async (page, ro) => { if (!page || guarded.has(page)) return; guarded.add(page); await page.evaluateOnNewDocument(relayGuard, allow, ro).catch(() => {}); }; const guardContext = (ctx, ro) => { const newPage = ctx.newPage.bind(ctx); ctx.newPage = async (...a) => { const p = await newPage(...a); await guardPage(p, ro); return p; }; }; const guardBrowser = async (browser, ro) => { guardContext(browser, ro); for (const name of ['createBrowserContext', 'createIncognitoBrowserContext']) { if (typeof browser[name] !== 'function') continue; const make = browser[name].bind(browser); browser[name] = async (...a) => { const c = await make(...a); guardContext(c, ro); return c; }; } // pages it opened itself (popups, the first tab) browser.on('targetcreated', async (t) => { if (t.type() === 'page') await guardPage(await t.page().catch(() => null), ro); }); for (const p of await browser.pages()) await guardPage(p, ro); }; const launch = async ({ relays, ...opts } = {}) => { // a machine without /usr/bin/google-chrome names its browser: CHROME=/path/to/chrome if (process.env.CHROME) opts.executablePath = process.env.CHROME; const browser = await real.launch(opts); if (live !== '1' && relays !== 'live') await guardBrowser(browser, live === 'read' || relays === 'read'); return browser; }; export default new Proxy(real, { get(t, k) { if (k === 'launch') return launch; const v = t[k]; return typeof v === 'function' ? v.bind(t) : v; }, });