|
| 1 | +// Sentience Chrome Extension - Background Service Worker |
| 2 | +// Auto-generated from modular source |
| 3 | +import init, { analyze_page_with_options, analyze_page, prune_for_api } from '../pkg/sentience_core.js'; |
| 4 | + |
| 5 | +// background.js - Service Worker with WASM (CSP-Immune!) |
| 6 | +// This runs in an isolated environment, completely immune to page CSP policies |
| 7 | + |
| 8 | + |
| 9 | +console.log('[Sentience Background] Initializing...'); |
| 10 | + |
| 11 | +// Global WASM initialization state |
| 12 | +let wasmReady = false; |
| 13 | +let wasmInitPromise = null; |
| 14 | + |
| 15 | +/** |
| 16 | + * Initialize WASM module - called once on service worker startup |
| 17 | + * Uses static imports (not dynamic import()) which is required for Service Workers |
| 18 | + */ |
| 19 | +async function initWASM() { |
| 20 | + if (wasmReady) return; |
| 21 | + if (wasmInitPromise) return wasmInitPromise; |
| 22 | + |
| 23 | + wasmInitPromise = (async () => { |
| 24 | + try { |
| 25 | + console.log('[Sentience Background] Loading WASM module...'); |
| 26 | + |
| 27 | + // Define the js_click_element function that WASM expects |
| 28 | + // In Service Workers, use 'globalThis' instead of 'window' |
| 29 | + // In background context, we can't actually click, so we log a warning |
| 30 | + globalThis.js_click_element = () => { |
| 31 | + console.warn('[Sentience Background] js_click_element called in background (ignored)'); |
| 32 | + }; |
| 33 | + |
| 34 | + // Initialize WASM - this calls the init() function from the static import |
| 35 | + // The init() function handles fetching and instantiating the .wasm file |
| 36 | + await init(); |
| 37 | + |
| 38 | + wasmReady = true; |
| 39 | + console.log('[Sentience Background] ✓ WASM ready!'); |
| 40 | + console.log( |
| 41 | + '[Sentience Background] Available functions: analyze_page, analyze_page_with_options, prune_for_api' |
| 42 | + ); |
| 43 | + } catch (error) { |
| 44 | + console.error('[Sentience Background] WASM initialization failed:', error); |
| 45 | + throw error; |
| 46 | + } |
| 47 | + })(); |
| 48 | + |
| 49 | + return wasmInitPromise; |
| 50 | +} |
| 51 | + |
| 52 | +// Initialize WASM on service worker startup |
| 53 | +initWASM().catch((err) => { |
| 54 | + console.error('[Sentience Background] Failed to initialize WASM:', err); |
| 55 | +}); |
| 56 | + |
| 57 | +/** |
| 58 | + * Message handler for all extension communication |
| 59 | + * Includes global error handling to prevent extension crashes |
| 60 | + */ |
| 61 | +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { |
| 62 | + // Global error handler to prevent extension crashes |
| 63 | + try { |
| 64 | + // Handle screenshot requests (existing functionality) |
| 65 | + if (request.action === 'captureScreenshot') { |
| 66 | + handleScreenshotCapture(sender.tab.id, request.options) |
| 67 | + .then((screenshot) => { |
| 68 | + sendResponse({ success: true, screenshot }); |
| 69 | + }) |
| 70 | + .catch((error) => { |
| 71 | + console.error('[Sentience Background] Screenshot capture failed:', error); |
| 72 | + sendResponse({ |
| 73 | + success: false, |
| 74 | + error: error.message || 'Screenshot capture failed', |
| 75 | + }); |
| 76 | + }); |
| 77 | + return true; // Async response |
| 78 | + } |
| 79 | + |
| 80 | + // Handle WASM processing requests (NEW!) |
| 81 | + if (request.action === 'processSnapshot') { |
| 82 | + handleSnapshotProcessing(request.rawData, request.options) |
| 83 | + .then((result) => { |
| 84 | + sendResponse({ success: true, result }); |
| 85 | + }) |
| 86 | + .catch((error) => { |
| 87 | + console.error('[Sentience Background] Snapshot processing failed:', error); |
| 88 | + sendResponse({ |
| 89 | + success: false, |
| 90 | + error: error.message || 'Snapshot processing failed', |
| 91 | + }); |
| 92 | + }); |
| 93 | + return true; // Async response |
| 94 | + } |
| 95 | + |
| 96 | + // Unknown action |
| 97 | + console.warn('[Sentience Background] Unknown action:', request.action); |
| 98 | + sendResponse({ success: false, error: 'Unknown action' }); |
| 99 | + return false; |
| 100 | + } catch (error) { |
| 101 | + // Catch any synchronous errors that might crash the extension |
| 102 | + console.error('[Sentience Background] Fatal error in message handler:', error); |
| 103 | + try { |
| 104 | + sendResponse({ |
| 105 | + success: false, |
| 106 | + error: `Fatal error: ${error.message || 'Unknown error'}`, |
| 107 | + }); |
| 108 | + } catch (e) { |
| 109 | + // If sendResponse already called, ignore |
| 110 | + } |
| 111 | + return false; |
| 112 | + } |
| 113 | +}); |
| 114 | + |
| 115 | +/** |
| 116 | + * Handle screenshot capture (existing functionality) |
| 117 | + */ |
| 118 | +async function handleScreenshotCapture(_tabId, options = {}) { |
| 119 | + try { |
| 120 | + const { format = 'png', quality = 90 } = options; |
| 121 | + |
| 122 | + const dataUrl = await chrome.tabs.captureVisibleTab(null, { |
| 123 | + format, |
| 124 | + quality, |
| 125 | + }); |
| 126 | + |
| 127 | + console.log( |
| 128 | + `[Sentience Background] Screenshot captured: ${format}, size: ${dataUrl.length} bytes` |
| 129 | + ); |
| 130 | + return dataUrl; |
| 131 | + } catch (error) { |
| 132 | + console.error('[Sentience Background] Screenshot error:', error); |
| 133 | + throw new Error(`Failed to capture screenshot: ${error.message}`); |
| 134 | + } |
| 135 | +} |
| 136 | + |
| 137 | +/** |
| 138 | + * Handle snapshot processing with WASM (NEW!) |
| 139 | + * This is where the magic happens - completely CSP-immune! |
| 140 | + * Includes safeguards to prevent crashes and hangs. |
| 141 | + * |
| 142 | + * @param {Array} rawData - Raw element data from injected_api.js |
| 143 | + * @param {Object} options - Snapshot options (limit, filter, etc.) |
| 144 | + * @returns {Promise<Object>} Processed snapshot result |
| 145 | + */ |
| 146 | +async function handleSnapshotProcessing(rawData, options = {}) { |
| 147 | + const MAX_ELEMENTS = 10000; // Safety limit to prevent hangs |
| 148 | + const startTime = performance.now(); |
| 149 | + |
| 150 | + try { |
| 151 | + // Safety check: limit element count to prevent hangs |
| 152 | + if (!Array.isArray(rawData)) { |
| 153 | + throw new Error('rawData must be an array'); |
| 154 | + } |
| 155 | + |
| 156 | + if (rawData.length > MAX_ELEMENTS) { |
| 157 | + console.warn( |
| 158 | + `[Sentience Background] ⚠️ Large dataset: ${rawData.length} elements. Limiting to ${MAX_ELEMENTS} to prevent hangs.` |
| 159 | + ); |
| 160 | + rawData = rawData.slice(0, MAX_ELEMENTS); |
| 161 | + } |
| 162 | + |
| 163 | + // Ensure WASM is initialized |
| 164 | + await initWASM(); |
| 165 | + if (!wasmReady) { |
| 166 | + throw new Error('WASM module not initialized'); |
| 167 | + } |
| 168 | + |
| 169 | + console.log( |
| 170 | + `[Sentience Background] Processing ${rawData.length} elements with options:`, |
| 171 | + options |
| 172 | + ); |
| 173 | + |
| 174 | + // Run WASM processing using the imported functions directly |
| 175 | + // Wrap in try-catch with timeout protection |
| 176 | + let analyzedElements; |
| 177 | + try { |
| 178 | + // Use a timeout wrapper to prevent infinite hangs |
| 179 | + const wasmPromise = new Promise((resolve, reject) => { |
| 180 | + try { |
| 181 | + let result; |
| 182 | + if (options.limit || options.filter) { |
| 183 | + result = analyze_page_with_options(rawData, options); |
| 184 | + } else { |
| 185 | + result = analyze_page(rawData); |
| 186 | + } |
| 187 | + resolve(result); |
| 188 | + } catch (e) { |
| 189 | + reject(e); |
| 190 | + } |
| 191 | + }); |
| 192 | + |
| 193 | + // Add timeout protection (18 seconds - less than content.js timeout) |
| 194 | + analyzedElements = await Promise.race([ |
| 195 | + wasmPromise, |
| 196 | + new Promise((_, reject) => |
| 197 | + setTimeout(() => reject(new Error('WASM processing timeout (>18s)')), 18000) |
| 198 | + ), |
| 199 | + ]); |
| 200 | + } catch (e) { |
| 201 | + const errorMsg = e.message || 'Unknown WASM error'; |
| 202 | + console.error(`[Sentience Background] WASM analyze_page failed: ${errorMsg}`, e); |
| 203 | + throw new Error(`WASM analyze_page failed: ${errorMsg}`); |
| 204 | + } |
| 205 | + |
| 206 | + // Prune elements for API (prevents 413 errors on large sites) |
| 207 | + let prunedRawData; |
| 208 | + try { |
| 209 | + prunedRawData = prune_for_api(rawData); |
| 210 | + } catch (e) { |
| 211 | + console.warn('[Sentience Background] prune_for_api failed, using original data:', e); |
| 212 | + prunedRawData = rawData; |
| 213 | + } |
| 214 | + |
| 215 | + const duration = performance.now() - startTime; |
| 216 | + console.log( |
| 217 | + `[Sentience Background] ✓ Processed: ${analyzedElements.length} analyzed, ${prunedRawData.length} pruned (${duration.toFixed(1)}ms)` |
| 218 | + ); |
| 219 | + |
| 220 | + return { |
| 221 | + elements: analyzedElements, |
| 222 | + raw_elements: prunedRawData, |
| 223 | + }; |
| 224 | + } catch (error) { |
| 225 | + const duration = performance.now() - startTime; |
| 226 | + console.error(`[Sentience Background] Processing error after ${duration.toFixed(1)}ms:`, error); |
| 227 | + throw error; |
| 228 | + } |
| 229 | +} |
| 230 | + |
| 231 | +console.log('[Sentience Background] Service worker ready'); |
| 232 | + |
| 233 | +// Global error handlers to prevent extension crashes |
| 234 | +self.addEventListener('error', (event) => { |
| 235 | + console.error('[Sentience Background] Global error caught:', event.error); |
| 236 | + event.preventDefault(); // Prevent extension crash |
| 237 | +}); |
| 238 | + |
| 239 | +self.addEventListener('unhandledrejection', (event) => { |
| 240 | + console.error('[Sentience Background] Unhandled promise rejection:', event.reason); |
| 241 | + event.preventDefault(); // Prevent extension crash |
| 242 | +}); |
0 commit comments