/* * BiDi probe — talks WebDriver BiDi to a Firefox-family browser started with * --remote-debugging-port 9222. Installs the sux zip (if asked), finds the * bsky.app tab, reloads it, captures console output, and inspects the live * page state (interceptor flag, __suxDiag, feed DOM anchors, xrpc calls). * * node scripts/bidi-probe.mjs [--install] */ const PORT = 9222; const ZIP = new URL("../web-ext-artifacts/sux_jev_feed_filter-0.1.0.zip", import.meta.url).pathname; const DO_INSTALL = process.argv.includes("--install"); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const ws = new WebSocket(`ws://localhost:${PORT}/session`); let seq = 0; const pending = new Map(); const events = []; function send(method, params = {}, sessionId = null, timeoutMs = 20000) { const id = ++seq; const msg = { id, method, params }; if (sessionId) msg.sessionId = sessionId; ws.send(JSON.stringify(msg)); return new Promise((resolve, reject) => { const t = setTimeout(() => { pending.delete(id); reject(new Error(`timeout: ${method}`)); }, timeoutMs); pending.set(id, { resolve, reject, t }); }); } await new Promise((res, rej) => { ws.onopen = res; ws.onerror = () => rej(new Error("could not connect — is Twilight running with --remote-debugging-port 9222?")); }); ws.onmessage = (ev) => { const msg = JSON.parse(ev.data); if (msg.id && pending.has(msg.id)) { const p = pending.get(msg.id); pending.delete(msg.id); clearTimeout(p.t); if (msg.type === "success") p.resolve(msg.result); else p.reject(new Error(`${msg.error.error}: ${msg.error.message}`)); } else if (msg.type === "event") { events.push(msg); } }; const out = {}; const { sessionId } = await send("session.new", { capabilities: {} }); out.session = sessionId; await send("session.subscribe", { events: ["log.entryAdded"] }, sessionId).catch(() => {}); if (DO_INSTALL) { try { const r = await send( "webExtension.install", { extensionData: { type: "path", path: ZIP } }, sessionId, 30000 ); out.install = { ok: true, extension: r.extension }; } catch (e) { out.install = { ok: false, error: e.message }; } } // find the bsky.app tab, creating one if absent let tree = await send("browsingContext.getTree", {}, sessionId); let ctx = tree.contexts.find((c) => (c.url || "").includes("bsky.app")); if (!ctx) { ctx = await send("browsingContext.create", { type: "tab", url: "https://bsky.app/" }, sessionId); await sleep(9000); } out.context = { id: ctx.context, url: ctx.url || "" }; // reload to re-run the content script, then let it work await send("browsingContext.reload", { context: ctx.context, ignoreCache: true }, sessionId).catch((e) => { out.reloadError = e.message; }); await sleep(10000); async function evalIn(expression) { const r = await send( "script.evaluate", { expression, target: { context: ctx.context }, awaitPromise: true, resultOwnership: "none" }, sessionId ); if (r.result.type === "object" && "value" in r.result) return r.result.value; if ("value" in r.result) return r.result.value; return r.result; } out.probe = await evalIn(`(() => { const norm = (href) => { if (!href) return null; const h = href.replace(/^https?:\\/\\/[^/]+/i, "").split("?")[0].split("#")[0]; const m = h.match(/^\\/profile\\/([^/]+)\\/post\\/([^/]+)\\/?$/); return m ? "profile/" + m[1].toLowerCase() + "/post/" + m[2] : null; }; const anchors = Array.from(document.querySelectorAll('a[href*="/post/"]')); const valid = anchors.filter((a) => norm(a.getAttribute("href"))); const itemContainer = (anchor) => { let node = anchor.parentElement; for (let depth = 0; node && node !== document.body && depth < 12; depth++) { const parent = node.parentElement; if (parent) { const sib = parent.querySelectorAll('a[href*="/post/"]'); for (let i = 0; i < sib.length; i++) if (!node.contains(sib[i])) return node; } node = parent; } return anchor.parentElement; }; const first = valid.slice(0, 5).map((a) => { const c = itemContainer(a); return { href: a.getAttribute("href").slice(0, 90), container: c.tagName + "." + String(c.className).split(" ").slice(0, 3).join("."), depth: (() => { let d = 0, n = a; while (n && n !== document.body) { d++; n = n.parentElement; } return d; })(), }; }); const xrpc = performance.getEntriesByType("resource").map((r) => r.name).filter((n) => n.includes("/xrpc/")).map((n) => n.split("?")[0].slice(-60)).slice(-12); return { url: location.href, ready: document.readyState, interceptor: !!window.__suxInterceptor, diag: window.__suxDiag || null, anchorCount: anchors.length, validAnchors: valid.length, firstContainers: first, xrpcCalls: xrpc, feedText: (document.body.innerText || "").slice(0, 200), }; })()`); await sleep(1500); out.console = events .filter((e) => e.method === "log.entryAdded") .map((e) => e.params) .filter((p) => p.text && (p.text.includes("sux") || p.level === "error")) .slice(-30) .map((p) => `[${p.level}] ${p.text} (${p.source?.realm || ""})`); console.log(JSON.stringify(out, null, 2)); try { await send("session.end", {}, sessionId, 5000); } catch { /* leave session */ } ws.close(); process.exit(0);