// Background service worker. // * Trusted input via the Chrome DevTools Protocol — Flow's editor ignores synthetic // DOM events (isTrusted=false); CDP Input.* events are trusted so it registers them. // * Asset download — fetches the harvested media URL with the page's session cookies // (host_permission covers labs.google) and hands the bytes to the local server. Runs // in the worker, so it's not blocked by the page's CSP. const attached = new Set(); function attach(tabId) { return new Promise((res, rej) => chrome.debugger.attach({ tabId }, '1.3', () => chrome.runtime.lastError ? rej(chrome.runtime.lastError) : res())); } function cmd(tabId, method, params) { return new Promise((res, rej) => chrome.debugger.sendCommand({ tabId }, method, params || {}, (r) => chrome.runtime.lastError ? rej(chrome.runtime.lastError) : res(r))); } async function ensure(tabId) { if (attached.has(tabId)) return; try { await attach(tabId); attached.add(tabId); } catch (e) { if (/already attached/i.test(String(e.message || e))) attached.add(tabId); else throw e; } } const ENTER = { key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 }; const ESC = { key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27 }; // While a harvest pass runs, route every browser download into flowrinse/harvest/ // and count them — the content script uses the count to VERIFY each 1K click fired. let harvestRouting = false; let harvestCount = 0; chrome.downloads.onDeterminingFilename.addListener((item, suggest) => { if (!harvestRouting) return; harvestCount++; suggest({ filename: `flowrinse/harvest/${(item.filename || 'asset').split(/[\\/]/).pop()}`, conflictAction: 'uniquify' }); }); chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { const tabId = sender.tab && sender.tab.id; (async () => { try { if (msg.type === 'insertText') { await ensure(tabId); await cmd(tabId, 'Input.insertText', { text: msg.text }); sendResponse({ ok: true }); } else if (msg.type === 'pressEnter') { await ensure(tabId); await cmd(tabId, 'Input.dispatchKeyEvent', { type: 'keyDown', ...ENTER }); await cmd(tabId, 'Input.dispatchKeyEvent', { type: 'keyUp', ...ENTER }); sendResponse({ ok: true }); } else if (msg.type === 'download') { // Browser-native download: uses the page's session cookies, follows the media // redirect, no CSP/CORS/base64. Lands under the browser's Downloads dir. const dlId = await new Promise((res, rej) => chrome.downloads.download( { url: msg.url, filename: msg.filename, conflictAction: 'uniquify', saveAs: false }, (id) => chrome.runtime.lastError ? rej(chrome.runtime.lastError) : res(id))); sendResponse({ ok: dlId != null, id: dlId }); } else if (msg.type === 'hoverAt') { await ensure(tabId); await cmd(tabId, 'Input.dispatchMouseEvent', { type: 'mouseMoved', x: msg.x, y: msg.y }); sendResponse({ ok: true }); } else if (msg.type === 'clickAt') { // Trusted click at viewport coords — Flow's menus ignore synthetic .click(). await ensure(tabId); await cmd(tabId, 'Input.dispatchMouseEvent', { type: 'mouseMoved', x: msg.x, y: msg.y }); await cmd(tabId, 'Input.dispatchMouseEvent', { type: 'mousePressed', x: msg.x, y: msg.y, button: 'left', clickCount: 1 }); await cmd(tabId, 'Input.dispatchMouseEvent', { type: 'mouseReleased', x: msg.x, y: msg.y, button: 'left', clickCount: 1 }); sendResponse({ ok: true }); } else if (msg.type === 'pressEscape') { await ensure(tabId); await cmd(tabId, 'Input.dispatchKeyEvent', { type: 'keyDown', ...ESC }); await cmd(tabId, 'Input.dispatchKeyEvent', { type: 'keyUp', ...ESC }); sendResponse({ ok: true }); } else if (msg.type === 'harvestRouting') { harvestRouting = !!msg.on; sendResponse({ ok: true }); } else if (msg.type === 'harvestStats') { sendResponse({ ok: true, count: harvestCount }); } else if (msg.type === 'hardReload') { // Cache-bypass reload — the by-hand fix for a wedged Flow session. if (attached.has(tabId)) { chrome.debugger.detach({ tabId }, () => {}); attached.delete(tabId); } chrome.tabs.reload(tabId, { bypassCache: true }); sendResponse({ ok: true }); } else if (msg.type === 'detach') { if (attached.has(tabId)) { chrome.debugger.detach({ tabId }, () => {}); attached.delete(tabId); } sendResponse({ ok: true }); } else { sendResponse({ ok: false, error: 'unknown message' }); } } catch (e) { sendResponse({ ok: false, error: String(e.message || e) }); } })(); return true; // async response }); chrome.debugger.onDetach.addListener((src) => { if (src.tabId) attached.delete(src.tabId); });