/* AutoFirmer Capture — MV3 service worker. * * Loop: every tick, scrape the tab you are looking at and POST it to * /api/autobuyer/capture. The dashboard's AutoBuyer page renders whatever * landed there. * * Nothing here has a switch. Capture is scoped instead: only the ACTIVE tab of * the last-focused window, and only when that tab is one of the firm sites in * host_permissions. Sit on any other page and nothing is read or sent. * * The locate queue is served whenever clicks are waiting. The AutoBuyer is * ready at all times; a run happens because someone asked for one. * * On a firm site, the person's 2FA code for it shows in a corner chip. */ const DEFAULTS = { apiBase: 'http://localhost:3000', pollSeconds: 3, // Optional CSS selector — matching elements get their on-screen position // measured alongside the HTML. elementSelector: '', }; let timer = null; let pendingLocate = 0; // ── Config / state ────────────────────────────────────────────────────────── async function getConfig() { const stored = await chrome.storage.local.get(Object.keys(DEFAULTS)); const cfg = { ...DEFAULTS, ...stored }; cfg.apiBase = String(cfg.apiBase).replace(/\/+$/, ''); return cfg; } /** Mirror the worker's status into storage so the popup can render it. */ async function setState(patch) { const { state: prev } = await chrome.storage.local.get('state'); await chrome.storage.local.set({ state: { ...(prev || {}), ...patch, updatedAt: Date.now() } }); } /** `!` when the dashboard is unreachable, blank otherwise. A blank badge is the * working state — there is nothing to switch on, so nothing to advertise. */ function setBadge(connected) { chrome.action.setBadgeText({ text: connected ? '' : '!' }); chrome.action.setBadgeBackgroundColor({ color: '#ef4444' }); } // ── The injected scraper ──────────────────────────────────────────────────── /** Runs in the page's own world. Must be self-contained — nothing from this * file's scope is available inside it. */ function pageCapture(selector) { // getBoundingClientRect() measures from the top-left of the viewport, so // rect.left / rect.top ARE the element's position relative to the window. // + scrollX/scrollY -> position within the document // + screenX/screenY and the chrome height -> absolute desktop coordinates, // which is what an OS-level clicker needs. const chromeHeight = window.outerHeight - window.innerHeight; const elements = selector ? Array.from(document.querySelectorAll(selector)).slice(0, 200).map((el, index) => { const r = el.getBoundingClientRect(); return { index, tag: el.tagName.toLowerCase(), id: el.id || null, text: (el.textContent || '').trim().slice(0, 80), // Off-screen or display:none elements still return a rect — all zeroes. visible: r.width > 0 && r.height > 0, inViewport: r.top < window.innerHeight && r.bottom > 0 && r.left < window.innerWidth && r.right > 0, viewport: { x: r.left, y: r.top, width: r.width, height: r.height }, page: { x: r.left + window.scrollX, y: r.top + window.scrollY }, // Centre of the element in desktop coordinates (CSS pixels — // multiply by devicePixelRatio for physical pixels on HiDPI). screen: { x: window.screenX + r.left + r.width / 2, y: window.screenY + chromeHeight + r.top + r.height / 2, }, }; }) : []; return { url: location.href, title: document.title, html: document.documentElement.outerHTML, viewport: { scrollX: window.scrollX, scrollY: window.scrollY, innerWidth: window.innerWidth, innerHeight: window.innerHeight, outerWidth: window.outerWidth, outerHeight: window.outerHeight, screenX: window.screenX, screenY: window.screenY, chromeHeight, devicePixelRatio: window.devicePixelRatio, }, elements, }; } // ── Capture pipeline ──────────────────────────────────────────────────────── /** The sites this extension is allowed to touch, taken from the manifest rather * than from a setting. * * These are the same hosts host_permissions grants, so they cannot drift out of * sync with what the extension can actually read, and adding a firm — which * means adding its host to the manifest anyway — scopes capture automatically. * localhost is filtered out: that is the dashboard, and capturing it would just * mirror our own output back. */ function firmPatterns() { return chrome.runtime.getManifest().host_permissions .filter((p) => !/\/\/(localhost|127\.0\.0\.1)/.test(p)); } /** The firm tab you are actually looking at, or nothing. * * Deliberately no fallback to "some other firm tab that happens to be open": * capture should report the page in front of you, not a stale background tab * you left on a broker site hours ago. Chrome does the pattern matching, so a * tab outside host_permissions cannot be selected here at all. */ async function pickTab() { const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true, url: firmPatterns(), }); return tab && /^https?:/.test(tab.url || '') ? tab : undefined; } async function captureAndSend(cfg) { const tab = await pickTab(); if (!tab) { await setState({ connected: true, error: null, waiting: 'Not on a firm page — nothing captured' }); return; } const [injection] = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: pageCapture, args: [cfg.elementSelector || ''], }); const result = injection?.result; if (!result) throw new Error('Injection returned nothing'); const res = await fetch(`${cfg.apiBase}/api/autobuyer/capture`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...result, // Stamp the build so the dashboard side can tell which version of this // worker is actually live — Chrome keeps running the old one until the // extension is reloaded, which is invisible from the server otherwise. viewport: { ...result.viewport, extVersion: chrome.runtime.getManifest().version }, capturedAt: Date.now(), }), }); if (!res.ok) throw new Error(`Capture POST failed: HTTP ${res.status}`); await setState({ connected: true, error: null, waiting: null, lastCapture: { url: result.url, title: result.title, bytes: result.html.length, at: Date.now() }, }); } /** Which browser is hosting this extension. * * The clicker has to raise *this* browser before clicking, and picking by list * order gets it wrong the moment two are installed — it would raise Edge while * the coordinates came from a tab in Chrome, landing every click in the wrong * window. So the answer travels with the measurement. */ function detectBrowser() { const ua = navigator.userAgent || ''; if (/\bEdg\//.test(ua)) return 'edge'; if (/\bOPR\//.test(ua)) return 'opera'; if (/\bVivaldi\//.test(ua)) return 'vivaldi'; try { if (navigator.brave) return 'brave'; // Brave otherwise reports as Chrome } catch { /* not Brave */ } return 'chrome'; } // ── Locate: turn a CSS selector into desktop coordinates ──────────────────── /** Runs in the page. Scrolls the element into view, then reports where it ended up. * `relative` is set when this runs inside a child frame: the frame's own * window.screenX and getBoundingClientRect are relative to that frame, not the * OS screen, so the desktop coordinate cannot be computed here — a cross-origin * child cannot read where it sits inside its parent. In that mode it returns the * element's position within THIS frame's viewport and leaves the composition to * the background script, which adds the parent