From b2e52f7eae492ec30f86660e462e732b8640b5fb Mon Sep 17 00:00:00 2001 From: rcholic Date: Fri, 23 Jan 2026 03:53:46 +0000 Subject: [PATCH 1/2] chore: sync extension files from sentience-chrome v2.8.0 --- sentience/extension/background.js | 8 +- sentience/extension/content.js | 18 +- sentience/extension/dist/background.js | 242 -- sentience/extension/dist/content.js | 456 --- sentience/extension/dist/injected_api.js | 2749 ----------------- sentience/extension/injected_api.js | 168 +- sentience/extension/manifest.json | 8 +- sentience/extension/pkg/sentience_core.d.ts | 44 +- sentience/extension/pkg/sentience_core.js | 708 ++--- .../extension/pkg/sentience_core_bg.wasm | Bin 111775 -> 110696 bytes sentience/extension/release.json | 60 +- 11 files changed, 392 insertions(+), 4069 deletions(-) delete mode 100644 sentience/extension/dist/background.js delete mode 100644 sentience/extension/dist/content.js delete mode 100644 sentience/extension/dist/injected_api.js diff --git a/sentience/extension/background.js b/sentience/extension/background.js index 02c0408..2923f55 100644 --- a/sentience/extension/background.js +++ b/sentience/extension/background.js @@ -1,4 +1,4 @@ -import init, { analyze_page_with_options, analyze_page, prune_for_api } from "./pkg/sentience_core.js"; +import init, { analyze_page_with_options, analyze_page, prune_for_api } from "../pkg/sentience_core.js"; let wasmReady = !1, wasmInitPromise = null; @@ -28,14 +28,14 @@ async function handleSnapshotProcessing(rawData, options = {}) { const startTime = performance.now(); try { if (!Array.isArray(rawData)) throw new Error("rawData must be an array"); - if (rawData.length > 1e4 && (rawData = rawData.slice(0, 1e4)), await initWASM(), + if (rawData.length > 1e4 && (rawData = rawData.slice(0, 1e4)), await initWASM(), !wasmReady) throw new Error("WASM module not initialized"); let analyzedElements, prunedRawData; try { const wasmPromise = new Promise((resolve, reject) => { try { let result; - result = options.limit || options.filter ? analyze_page_with_options(rawData, options) : analyze_page(rawData), + result = options.limit || options.filter ? analyze_page_with_options(rawData, options) : analyze_page(rawData), resolve(result); } catch (e) { reject(e); @@ -101,4 +101,4 @@ initWASM().catch(err => {}), chrome.runtime.onMessage.addListener((request, send event.preventDefault(); }), self.addEventListener("unhandledrejection", event => { event.preventDefault(); -}); +}); \ No newline at end of file diff --git a/sentience/extension/content.js b/sentience/extension/content.js index 97923a2..b65cfb5 100644 --- a/sentience/extension/content.js +++ b/sentience/extension/content.js @@ -82,7 +82,7 @@ if (!elements || !Array.isArray(elements)) return; removeOverlay(); const host = document.createElement("div"); - host.id = OVERLAY_HOST_ID, host.style.cssText = "\n position: fixed !important;\n top: 0 !important;\n left: 0 !important;\n width: 100vw !important;\n height: 100vh !important;\n pointer-events: none !important;\n z-index: 2147483647 !important;\n margin: 0 !important;\n padding: 0 !important;\n ", + host.id = OVERLAY_HOST_ID, host.style.cssText = "\n position: fixed !important;\n top: 0 !important;\n left: 0 !important;\n width: 100vw !important;\n height: 100vh !important;\n pointer-events: none !important;\n z-index: 2147483647 !important;\n margin: 0 !important;\n padding: 0 !important;\n ", document.body.appendChild(host); const shadow = host.attachShadow({ mode: "closed" @@ -94,15 +94,15 @@ let color; color = isTarget ? "#FF0000" : isPrimary ? "#0066FF" : "#00FF00"; const importanceRatio = maxImportance > 0 ? importance / maxImportance : .5, borderOpacity = isTarget ? 1 : isPrimary ? .9 : Math.max(.4, .5 + .5 * importanceRatio), fillOpacity = .2 * borderOpacity, borderWidth = isTarget ? 2 : isPrimary ? 1.5 : Math.max(.5, Math.round(2 * importanceRatio)), hexOpacity = Math.round(255 * fillOpacity).toString(16).padStart(2, "0"), box = document.createElement("div"); - if (box.style.cssText = `\n position: absolute;\n left: ${bbox.x}px;\n top: ${bbox.y}px;\n width: ${bbox.width}px;\n height: ${bbox.height}px;\n border: ${borderWidth}px solid ${color};\n background-color: ${color}${hexOpacity};\n box-sizing: border-box;\n opacity: ${borderOpacity};\n pointer-events: none;\n `, + if (box.style.cssText = `\n position: absolute;\n left: ${bbox.x}px;\n top: ${bbox.y}px;\n width: ${bbox.width}px;\n height: ${bbox.height}px;\n border: ${borderWidth}px solid ${color};\n background-color: ${color}${hexOpacity};\n box-sizing: border-box;\n opacity: ${borderOpacity};\n pointer-events: none;\n `, importance > 0 || isPrimary) { const badge = document.createElement("span"); - badge.textContent = isPrimary ? `⭐${importance}` : `${importance}`, badge.style.cssText = `\n position: absolute;\n top: -18px;\n left: 0;\n background: ${color};\n color: white;\n font-size: 11px;\n font-weight: bold;\n padding: 2px 6px;\n font-family: Arial, sans-serif;\n border-radius: 3px;\n opacity: 0.95;\n white-space: nowrap;\n pointer-events: none;\n `, + badge.textContent = isPrimary ? `⭐${importance}` : `${importance}`, badge.style.cssText = `\n position: absolute;\n top: -18px;\n left: 0;\n background: ${color};\n color: white;\n font-size: 11px;\n font-weight: bold;\n padding: 2px 6px;\n font-family: Arial, sans-serif;\n border-radius: 3px;\n opacity: 0.95;\n white-space: nowrap;\n pointer-events: none;\n `, box.appendChild(badge); } if (isTarget) { const targetIndicator = document.createElement("span"); - targetIndicator.textContent = "🎯", targetIndicator.style.cssText = "\n position: absolute;\n top: -18px;\n right: 0;\n font-size: 16px;\n pointer-events: none;\n ", + targetIndicator.textContent = "🎯", targetIndicator.style.cssText = "\n position: absolute;\n top: -18px;\n right: 0;\n font-size: 16px;\n pointer-events: none;\n ", box.appendChild(targetIndicator); } shadow.appendChild(box); @@ -122,7 +122,7 @@ if (!grids || !Array.isArray(grids)) return; removeOverlay(); const host = document.createElement("div"); - host.id = OVERLAY_HOST_ID, host.style.cssText = "\n position: fixed !important;\n top: 0 !important;\n left: 0 !important;\n width: 100vw !important;\n height: 100vh !important;\n pointer-events: none !important;\n z-index: 2147483647 !important;\n margin: 0 !important;\n padding: 0 !important;\n ", + host.id = OVERLAY_HOST_ID, host.style.cssText = "\n position: fixed !important;\n top: 0 !important;\n left: 0 !important;\n width: 100vw !important;\n height: 100vh !important;\n pointer-events: none !important;\n z-index: 2147483647 !important;\n margin: 0 !important;\n padding: 0 !important;\n ", document.body.appendChild(host); const shadow = host.attachShadow({ mode: "closed" @@ -138,10 +138,10 @@ let labelText = grid.label ? `Grid ${grid.grid_id}: ${grid.label}` : `Grid ${grid.grid_id}`; grid.is_dominant && (labelText = `⭐ ${labelText} (dominant)`); const badge = document.createElement("span"); - if (badge.textContent = labelText, badge.style.cssText = `\n position: absolute;\n top: -18px;\n left: 0;\n background: ${color};\n color: white;\n font-size: 11px;\n font-weight: bold;\n padding: 2px 6px;\n font-family: Arial, sans-serif;\n border-radius: 3px;\n opacity: 0.95;\n white-space: nowrap;\n pointer-events: none;\n `, + if (badge.textContent = labelText, badge.style.cssText = `\n position: absolute;\n top: -18px;\n left: 0;\n background: ${color};\n color: white;\n font-size: 11px;\n font-weight: bold;\n padding: 2px 6px;\n font-family: Arial, sans-serif;\n border-radius: 3px;\n opacity: 0.95;\n white-space: nowrap;\n pointer-events: none;\n `, box.appendChild(badge), isTarget) { const targetIndicator = document.createElement("span"); - targetIndicator.textContent = "🎯", targetIndicator.style.cssText = "\n position: absolute;\n top: -18px;\n right: 0;\n font-size: 16px;\n pointer-events: none;\n ", + targetIndicator.textContent = "🎯", targetIndicator.style.cssText = "\n position: absolute;\n top: -18px;\n right: 0;\n font-size: 16px;\n pointer-events: none;\n ", box.appendChild(targetIndicator); } shadow.appendChild(box); @@ -155,7 +155,7 @@ let overlayTimeout = null; function removeOverlay() { const existing = document.getElementById(OVERLAY_HOST_ID); - existing && existing.remove(), overlayTimeout && (clearTimeout(overlayTimeout), + existing && existing.remove(), overlayTimeout && (clearTimeout(overlayTimeout), overlayTimeout = null); } -}(); +}(); \ No newline at end of file diff --git a/sentience/extension/dist/background.js b/sentience/extension/dist/background.js deleted file mode 100644 index 1f64f84..0000000 --- a/sentience/extension/dist/background.js +++ /dev/null @@ -1,242 +0,0 @@ -// Sentience Chrome Extension - Background Service Worker -// Auto-generated from modular source -import init, { analyze_page_with_options, analyze_page, prune_for_api } from '../pkg/sentience_core.js'; - -// background.js - Service Worker with WASM (CSP-Immune!) -// This runs in an isolated environment, completely immune to page CSP policies - - -console.log('[Sentience Background] Initializing...'); - -// Global WASM initialization state -let wasmReady = false; -let wasmInitPromise = null; - -/** - * Initialize WASM module - called once on service worker startup - * Uses static imports (not dynamic import()) which is required for Service Workers - */ -async function initWASM() { - if (wasmReady) return; - if (wasmInitPromise) return wasmInitPromise; - - wasmInitPromise = (async () => { - try { - console.log('[Sentience Background] Loading WASM module...'); - - // Define the js_click_element function that WASM expects - // In Service Workers, use 'globalThis' instead of 'window' - // In background context, we can't actually click, so we log a warning - globalThis.js_click_element = () => { - console.warn('[Sentience Background] js_click_element called in background (ignored)'); - }; - - // Initialize WASM - this calls the init() function from the static import - // The init() function handles fetching and instantiating the .wasm file - await init(); - - wasmReady = true; - console.log('[Sentience Background] ✓ WASM ready!'); - console.log( - '[Sentience Background] Available functions: analyze_page, analyze_page_with_options, prune_for_api' - ); - } catch (error) { - console.error('[Sentience Background] WASM initialization failed:', error); - throw error; - } - })(); - - return wasmInitPromise; -} - -// Initialize WASM on service worker startup -initWASM().catch((err) => { - console.error('[Sentience Background] Failed to initialize WASM:', err); -}); - -/** - * Message handler for all extension communication - * Includes global error handling to prevent extension crashes - */ -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - // Global error handler to prevent extension crashes - try { - // Handle screenshot requests (existing functionality) - if (request.action === 'captureScreenshot') { - handleScreenshotCapture(sender.tab.id, request.options) - .then((screenshot) => { - sendResponse({ success: true, screenshot }); - }) - .catch((error) => { - console.error('[Sentience Background] Screenshot capture failed:', error); - sendResponse({ - success: false, - error: error.message || 'Screenshot capture failed', - }); - }); - return true; // Async response - } - - // Handle WASM processing requests (NEW!) - if (request.action === 'processSnapshot') { - handleSnapshotProcessing(request.rawData, request.options) - .then((result) => { - sendResponse({ success: true, result }); - }) - .catch((error) => { - console.error('[Sentience Background] Snapshot processing failed:', error); - sendResponse({ - success: false, - error: error.message || 'Snapshot processing failed', - }); - }); - return true; // Async response - } - - // Unknown action - console.warn('[Sentience Background] Unknown action:', request.action); - sendResponse({ success: false, error: 'Unknown action' }); - return false; - } catch (error) { - // Catch any synchronous errors that might crash the extension - console.error('[Sentience Background] Fatal error in message handler:', error); - try { - sendResponse({ - success: false, - error: `Fatal error: ${error.message || 'Unknown error'}`, - }); - } catch (e) { - // If sendResponse already called, ignore - } - return false; - } -}); - -/** - * Handle screenshot capture (existing functionality) - */ -async function handleScreenshotCapture(_tabId, options = {}) { - try { - const { format = 'png', quality = 90 } = options; - - const dataUrl = await chrome.tabs.captureVisibleTab(null, { - format, - quality, - }); - - console.log( - `[Sentience Background] Screenshot captured: ${format}, size: ${dataUrl.length} bytes` - ); - return dataUrl; - } catch (error) { - console.error('[Sentience Background] Screenshot error:', error); - throw new Error(`Failed to capture screenshot: ${error.message}`); - } -} - -/** - * Handle snapshot processing with WASM (NEW!) - * This is where the magic happens - completely CSP-immune! - * Includes safeguards to prevent crashes and hangs. - * - * @param {Array} rawData - Raw element data from injected_api.js - * @param {Object} options - Snapshot options (limit, filter, etc.) - * @returns {Promise} Processed snapshot result - */ -async function handleSnapshotProcessing(rawData, options = {}) { - const MAX_ELEMENTS = 10000; // Safety limit to prevent hangs - const startTime = performance.now(); - - try { - // Safety check: limit element count to prevent hangs - if (!Array.isArray(rawData)) { - throw new Error('rawData must be an array'); - } - - if (rawData.length > MAX_ELEMENTS) { - console.warn( - `[Sentience Background] ⚠️ Large dataset: ${rawData.length} elements. Limiting to ${MAX_ELEMENTS} to prevent hangs.` - ); - rawData = rawData.slice(0, MAX_ELEMENTS); - } - - // Ensure WASM is initialized - await initWASM(); - if (!wasmReady) { - throw new Error('WASM module not initialized'); - } - - console.log( - `[Sentience Background] Processing ${rawData.length} elements with options:`, - options - ); - - // Run WASM processing using the imported functions directly - // Wrap in try-catch with timeout protection - let analyzedElements; - try { - // Use a timeout wrapper to prevent infinite hangs - const wasmPromise = new Promise((resolve, reject) => { - try { - let result; - if (options.limit || options.filter) { - result = analyze_page_with_options(rawData, options); - } else { - result = analyze_page(rawData); - } - resolve(result); - } catch (e) { - reject(e); - } - }); - - // Add timeout protection (18 seconds - less than content.js timeout) - analyzedElements = await Promise.race([ - wasmPromise, - new Promise((_, reject) => - setTimeout(() => reject(new Error('WASM processing timeout (>18s)')), 18000) - ), - ]); - } catch (e) { - const errorMsg = e.message || 'Unknown WASM error'; - console.error(`[Sentience Background] WASM analyze_page failed: ${errorMsg}`, e); - throw new Error(`WASM analyze_page failed: ${errorMsg}`); - } - - // Prune elements for API (prevents 413 errors on large sites) - let prunedRawData; - try { - prunedRawData = prune_for_api(rawData); - } catch (e) { - console.warn('[Sentience Background] prune_for_api failed, using original data:', e); - prunedRawData = rawData; - } - - const duration = performance.now() - startTime; - console.log( - `[Sentience Background] ✓ Processed: ${analyzedElements.length} analyzed, ${prunedRawData.length} pruned (${duration.toFixed(1)}ms)` - ); - - return { - elements: analyzedElements, - raw_elements: prunedRawData, - }; - } catch (error) { - const duration = performance.now() - startTime; - console.error(`[Sentience Background] Processing error after ${duration.toFixed(1)}ms:`, error); - throw error; - } -} - -console.log('[Sentience Background] Service worker ready'); - -// Global error handlers to prevent extension crashes -self.addEventListener('error', (event) => { - console.error('[Sentience Background] Global error caught:', event.error); - event.preventDefault(); // Prevent extension crash -}); - -self.addEventListener('unhandledrejection', (event) => { - console.error('[Sentience Background] Unhandled promise rejection:', event.reason); - event.preventDefault(); // Prevent extension crash -}); diff --git a/sentience/extension/dist/content.js b/sentience/extension/dist/content.js deleted file mode 100644 index ee6efa2..0000000 --- a/sentience/extension/dist/content.js +++ /dev/null @@ -1,456 +0,0 @@ -// Sentience Chrome Extension - Content Script -// Auto-generated from modular source -(function () { - 'use strict'; - - // content.js - ISOLATED WORLD (Bridge between Main World and Background) - console.log('[Sentience Bridge] Loaded.'); - - // Detect if we're in a child frame (for iframe support) - const isChildFrame = window !== window.top; - if (isChildFrame) { - console.log('[Sentience Bridge] Running in child frame:', window.location.href); - } - - // 1. Pass Extension ID to Main World (So API knows where to find resources) - document.documentElement.dataset.sentienceExtensionId = chrome.runtime.id; - - // 2. Message Router - Handles all communication between page and background - window.addEventListener('message', (event) => { - // Security check: only accept messages from same window - if (event.source !== window) return; - - // Route different message types - switch (event.data.type) { - case 'SENTIENCE_SCREENSHOT_REQUEST': - handleScreenshotRequest(event.data); - break; - - case 'SENTIENCE_SNAPSHOT_REQUEST': - handleSnapshotRequest(event.data); - break; - - case 'SENTIENCE_SHOW_OVERLAY': - handleShowOverlay(event.data); - break; - - case 'SENTIENCE_CLEAR_OVERLAY': - handleClearOverlay(); - break; - - case 'SENTIENCE_SHOW_GRID_OVERLAY': - handleShowGridOverlay(event.data); - break; - } - }); - - /** - * Handle screenshot requests (existing functionality) - */ - function handleScreenshotRequest(data) { - chrome.runtime.sendMessage({ action: 'captureScreenshot', options: data.options }, (response) => { - window.postMessage( - { - type: 'SENTIENCE_SCREENSHOT_RESULT', - requestId: data.requestId, - screenshot: response?.success ? response.screenshot : null, - error: response?.error, - }, - '*' - ); - }); - } - - /** - * Handle snapshot processing requests (NEW!) - * Sends raw DOM data to background worker for WASM processing - * Includes timeout protection to prevent extension crashes - */ - function handleSnapshotRequest(data) { - const startTime = performance.now(); - const TIMEOUT_MS = 20000; // 20 seconds (longer than injected_api timeout) - let responded = false; - - // Timeout protection: if background doesn't respond, send error - const timeoutId = setTimeout(() => { - if (!responded) { - responded = true; - const duration = performance.now() - startTime; - console.error(`[Sentience Bridge] ⚠️ WASM processing timeout after ${duration.toFixed(1)}ms`); - window.postMessage( - { - type: 'SENTIENCE_SNAPSHOT_RESULT', - requestId: data.requestId, - error: 'WASM processing timeout - background script may be unresponsive', - duration, - }, - '*' - ); - } - }, TIMEOUT_MS); - - try { - chrome.runtime.sendMessage( - { - action: 'processSnapshot', - rawData: data.rawData, - options: data.options, - }, - (response) => { - if (responded) return; // Already responded via timeout - responded = true; - clearTimeout(timeoutId); - - const duration = performance.now() - startTime; - - // Handle Chrome extension errors (e.g., background script crashed) - if (chrome.runtime.lastError) { - console.error( - '[Sentience Bridge] Chrome runtime error:', - chrome.runtime.lastError.message - ); - window.postMessage( - { - type: 'SENTIENCE_SNAPSHOT_RESULT', - requestId: data.requestId, - error: `Chrome runtime error: ${chrome.runtime.lastError.message}`, - duration, - }, - '*' - ); - return; - } - - if (response?.success) { - console.log(`[Sentience Bridge] ✓ WASM processing complete in ${duration.toFixed(1)}ms`); - window.postMessage( - { - type: 'SENTIENCE_SNAPSHOT_RESULT', - requestId: data.requestId, - elements: response.result.elements, - raw_elements: response.result.raw_elements, - duration, - }, - '*' - ); - } else { - console.error('[Sentience Bridge] WASM processing failed:', response?.error); - window.postMessage( - { - type: 'SENTIENCE_SNAPSHOT_RESULT', - requestId: data.requestId, - error: response?.error || 'Processing failed', - duration, - }, - '*' - ); - } - } - ); - } catch (error) { - if (!responded) { - responded = true; - clearTimeout(timeoutId); - const duration = performance.now() - startTime; - console.error('[Sentience Bridge] Exception sending message:', error); - window.postMessage( - { - type: 'SENTIENCE_SNAPSHOT_RESULT', - requestId: data.requestId, - error: `Failed to send message: ${error.message}`, - duration, - }, - '*' - ); - } - } - } - - // ============================================================================ - // Visual Overlay - Shadow DOM Implementation - // ============================================================================ - - const OVERLAY_HOST_ID = 'sentience-overlay-host'; - let overlayTimeout = null; - - /** - * Show visual overlay highlighting elements using Shadow DOM - * @param {Object} data - Message data with elements and targetElementId - */ - function handleShowOverlay(data) { - const { elements, targetElementId } = data; - - if (!elements || !Array.isArray(elements)) { - console.warn('[Sentience Bridge] showOverlay: elements must be an array'); - return; - } - - removeOverlay(); - - // Create host with Shadow DOM for CSS isolation - const host = document.createElement('div'); - host.id = OVERLAY_HOST_ID; - host.style.cssText = ` - position: fixed !important; - top: 0 !important; - left: 0 !important; - width: 100vw !important; - height: 100vh !important; - pointer-events: none !important; - z-index: 2147483647 !important; - margin: 0 !important; - padding: 0 !important; - `; - document.body.appendChild(host); - - // Attach shadow root (closed mode for security and CSS isolation) - const shadow = host.attachShadow({ mode: 'closed' }); - - // Calculate max importance for scaling - const maxImportance = Math.max(...elements.map((e) => e.importance || 0), 1); - - elements.forEach((element) => { - const bbox = element.bbox; - if (!bbox) return; - - const isTarget = element.id === targetElementId; - const isPrimary = element.visual_cues?.is_primary || false; - const importance = element.importance || 0; - - // Color: Red (target), Blue (primary), Green (regular) - let color; - if (isTarget) color = '#FF0000'; - else if (isPrimary) color = '#0066FF'; - else color = '#00FF00'; - - // Scale opacity and border width based on importance - const importanceRatio = maxImportance > 0 ? importance / maxImportance : 0.5; - const borderOpacity = isTarget - ? 1.0 - : isPrimary - ? 0.9 - : Math.max(0.4, 0.5 + importanceRatio * 0.5); - const fillOpacity = borderOpacity * 0.2; - const borderWidth = isTarget - ? 2 - : isPrimary - ? 1.5 - : Math.max(0.5, Math.round(importanceRatio * 2)); - - // Convert fill opacity to hex for background-color - const hexOpacity = Math.round(fillOpacity * 255) - .toString(16) - .padStart(2, '0'); - - // Create box with semi-transparent fill - const box = document.createElement('div'); - box.style.cssText = ` - position: absolute; - left: ${bbox.x}px; - top: ${bbox.y}px; - width: ${bbox.width}px; - height: ${bbox.height}px; - border: ${borderWidth}px solid ${color}; - background-color: ${color}${hexOpacity}; - box-sizing: border-box; - opacity: ${borderOpacity}; - pointer-events: none; - `; - - // Add badge showing importance score - if (importance > 0 || isPrimary) { - const badge = document.createElement('span'); - badge.textContent = isPrimary ? `⭐${importance}` : `${importance}`; - badge.style.cssText = ` - position: absolute; - top: -18px; - left: 0; - background: ${color}; - color: white; - font-size: 11px; - font-weight: bold; - padding: 2px 6px; - font-family: Arial, sans-serif; - border-radius: 3px; - opacity: 0.95; - white-space: nowrap; - pointer-events: none; - `; - box.appendChild(badge); - } - - // Add target emoji for target element - if (isTarget) { - const targetIndicator = document.createElement('span'); - targetIndicator.textContent = '🎯'; - targetIndicator.style.cssText = ` - position: absolute; - top: -18px; - right: 0; - font-size: 16px; - pointer-events: none; - `; - box.appendChild(targetIndicator); - } - - shadow.appendChild(box); - }); - - console.log(`[Sentience Bridge] Overlay shown for ${elements.length} elements`); - - // Auto-remove after 5 seconds - overlayTimeout = setTimeout(() => { - removeOverlay(); - console.log('[Sentience Bridge] Overlay auto-cleared after 5 seconds'); - }, 5000); - } - - /** - * Show grid overlay highlighting detected grids - * @param {Object} data - Message data with grids and targetGridId - */ - function handleShowGridOverlay(data) { - const { grids, targetGridId } = data; - - if (!grids || !Array.isArray(grids)) { - console.warn('[Sentience Bridge] showGridOverlay: grids must be an array'); - return; - } - - removeOverlay(); - - // Create host with Shadow DOM for CSS isolation - const host = document.createElement('div'); - host.id = OVERLAY_HOST_ID; - host.style.cssText = ` - position: fixed !important; - top: 0 !important; - left: 0 !important; - width: 100vw !important; - height: 100vh !important; - pointer-events: none !important; - z-index: 2147483647 !important; - margin: 0 !important; - padding: 0 !important; - `; - document.body.appendChild(host); - - // Attach shadow root (closed mode for security and CSS isolation) - const shadow = host.attachShadow({ mode: 'closed' }); - - grids.forEach((grid) => { - const bbox = grid.bbox; - if (!bbox) return; - - const isTarget = grid.grid_id === targetGridId; - const isDominant = grid.is_dominant === true; - - // Grid colors: Red for target, Orange for dominant, Purple for regular - let color = '#9B59B6'; // Purple (default) - if (isTarget) { - color = '#FF0000'; // Red for target - } else if (isDominant) { - color = '#FF8C00'; // Orange for dominant group - } - const borderStyle = isTarget ? 'solid' : 'dashed'; // Dashed for grids - const borderWidth = isTarget ? 3 : isDominant ? 2.5 : 2; - const opacity = isTarget ? 1.0 : isDominant ? 0.9 : 0.8; - const fillOpacity = opacity * 0.1; // 10% opacity for grids (lighter than elements) - - // Convert fill opacity to hex for background-color - const hexOpacity = Math.round(fillOpacity * 255) - .toString(16) - .padStart(2, '0'); - - // Create grid box - const box = document.createElement('div'); - box.style.cssText = ` - position: absolute; - left: ${bbox.x}px; - top: ${bbox.y}px; - width: ${bbox.width}px; - height: ${bbox.height}px; - border: ${borderWidth}px ${borderStyle} ${color}; - background-color: ${color}${hexOpacity}; - box-sizing: border-box; - opacity: ${opacity}; - pointer-events: none; - `; - - // Add badge with grid_id and label - let labelText = grid.label ? `Grid ${grid.grid_id}: ${grid.label}` : `Grid ${grid.grid_id}`; - // Add dominant indicator if this is the dominant group - if (grid.is_dominant) { - labelText = `⭐ ${labelText} (dominant)`; - } - const badge = document.createElement('span'); - badge.textContent = labelText; - badge.style.cssText = ` - position: absolute; - top: -18px; - left: 0; - background: ${color}; - color: white; - font-size: 11px; - font-weight: bold; - padding: 2px 6px; - font-family: Arial, sans-serif; - border-radius: 3px; - opacity: 0.95; - white-space: nowrap; - pointer-events: none; - `; - box.appendChild(badge); - - // Add target indicator if target - if (isTarget) { - const targetIndicator = document.createElement('span'); - targetIndicator.textContent = '🎯'; - targetIndicator.style.cssText = ` - position: absolute; - top: -18px; - right: 0; - font-size: 16px; - pointer-events: none; - `; - box.appendChild(targetIndicator); - } - - shadow.appendChild(box); - }); - - console.log(`[Sentience Bridge] Grid overlay shown for ${grids.length} grids`); - - // Auto-remove after 5 seconds - overlayTimeout = setTimeout(() => { - removeOverlay(); - console.log('[Sentience Bridge] Grid overlay auto-cleared after 5 seconds'); - }, 5000); - } - - /** - * Clear overlay manually - */ - function handleClearOverlay() { - removeOverlay(); - console.log('[Sentience Bridge] Overlay cleared manually'); - } - - /** - * Remove overlay from DOM - */ - function removeOverlay() { - const existing = document.getElementById(OVERLAY_HOST_ID); - if (existing) { - existing.remove(); - } - - if (overlayTimeout) { - clearTimeout(overlayTimeout); - overlayTimeout = null; - } - } - - // console.log('[Sentience Bridge] Ready - Extension ID:', chrome.runtime.id); - -})(); diff --git a/sentience/extension/dist/injected_api.js b/sentience/extension/dist/injected_api.js deleted file mode 100644 index e6ff2f9..0000000 --- a/sentience/extension/dist/injected_api.js +++ /dev/null @@ -1,2749 +0,0 @@ -// Sentience Chrome Extension - Injected API -// Auto-generated from modular source -(function () { - 'use strict'; - - // utils.js - Helper Functions (CSP-Resistant) - // All utility functions needed for DOM data collection - - // --- HELPER: Deep Walker with Native Filter --- - function getAllElements(root = document) { - const elements = []; - const filter = { - acceptNode(node) { - // Skip metadata and script/style tags - if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'META', 'LINK', 'HEAD'].includes(node.tagName)) { - return NodeFilter.FILTER_REJECT; - } - // Skip deep SVG children - if (node.parentNode && node.parentNode.tagName === 'SVG' && node.tagName !== 'SVG') { - return NodeFilter.FILTER_REJECT; - } - return NodeFilter.FILTER_ACCEPT; - }, - }; - - const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filter); - while (walker.nextNode()) { - const node = walker.currentNode; - if (node.isConnected) { - elements.push(node); - if (node.shadowRoot) elements.push(...getAllElements(node.shadowRoot)); - } - } - return elements; - } - - // ============================================================================ - // CAPTCHA DETECTION (detection only, no solving/bypass logic) - // ============================================================================ - - const CAPTCHA_DETECTED_THRESHOLD = 0.7; - const CAPTCHA_MAX_EVIDENCE = 5; - const CAPTCHA_TEXT_MAX_LEN = 2000; - - const CAPTCHA_TEXT_KEYWORDS = [ - 'verify you are human', - 'captcha', - 'human verification', - 'unusual traffic', - 'are you a robot', - 'security check', - 'prove you are human', - 'bot detection', - 'automated access', - ]; - - const CAPTCHA_URL_HINTS = ['captcha', 'challenge', 'verify']; - - const CAPTCHA_IFRAME_HINTS = { - recaptcha: ['recaptcha', 'google.com/recaptcha'], - hcaptcha: ['hcaptcha.com'], - turnstile: ['challenges.cloudflare.com', 'turnstile'], - arkose: ['arkoselabs.com', 'funcaptcha.com', 'client-api.arkoselabs.com'], - awswaf: ['amazonaws.com/captcha', 'awswaf.com'], - }; - - const CAPTCHA_SCRIPT_HINTS = { - recaptcha: ['recaptcha'], - hcaptcha: ['hcaptcha'], - turnstile: ['turnstile', 'challenges.cloudflare.com'], - arkose: ['arkoselabs', 'funcaptcha'], - awswaf: ['captcha.awswaf', 'awswaf-captcha'], - }; - - const CAPTCHA_CONTAINER_SELECTORS = [ - // reCAPTCHA - { selector: '.g-recaptcha', provider: 'recaptcha' }, - { selector: '#g-recaptcha', provider: 'recaptcha' }, - { selector: '[data-sitekey]', provider: 'unknown' }, - { selector: 'iframe[title*="recaptcha" i]', provider: 'recaptcha' }, - // hCaptcha - { selector: '.h-captcha', provider: 'hcaptcha' }, - { selector: '#h-captcha', provider: 'hcaptcha' }, - { selector: 'iframe[title*="hcaptcha" i]', provider: 'hcaptcha' }, - // Cloudflare Turnstile - { selector: '.cf-turnstile', provider: 'turnstile' }, - { selector: '[data-cf-turnstile-sitekey]', provider: 'turnstile' }, - { selector: 'iframe[src*="challenges.cloudflare.com"]', provider: 'turnstile' }, - // Arkose Labs / FunCaptcha - { selector: '#FunCaptcha', provider: 'arkose' }, - { selector: '.funcaptcha', provider: 'arkose' }, - { selector: '[data-arkose-public-key]', provider: 'arkose' }, - { selector: 'iframe[src*="arkoselabs"]', provider: 'arkose' }, - // AWS WAF CAPTCHA - { selector: '#captcha-container', provider: 'awswaf' }, - { selector: '[data-awswaf-captcha]', provider: 'awswaf' }, - // Generic - { selector: 'iframe[title*="captcha" i]', provider: 'unknown' }, - ]; - - function addEvidence(list, value) { - if (!value) return; - if (list.length >= CAPTCHA_MAX_EVIDENCE) return; - list.push(value); - } - - function truncateText(text, maxLen) { - if (!text) return ''; - if (text.length <= maxLen) return text; - return text.slice(0, maxLen); - } - - function collectVisibleTextSnippet() { - try { - const candidates = document.querySelectorAll( - 'h1, h2, h3, h4, p, label, button, form, div, span' - ); - let combined = ''; - let count = 0; - for (const node of candidates) { - if (count >= 30 || combined.length >= CAPTCHA_TEXT_MAX_LEN) break; - if (!node || typeof node.innerText !== 'string') continue; - if (!node.offsetWidth && !node.offsetHeight && !node.getClientRects().length) continue; - const text = node.innerText.replace(/\s+/g, ' ').trim(); - if (!text) continue; - combined += `${text} `; - count += 1; - } - combined = combined.trim(); - if (combined) { - return truncateText(combined, CAPTCHA_TEXT_MAX_LEN); - } - } catch (e) { - // ignore - } - - try { - let bodyText = document.body?.innerText || ''; - if (!bodyText && document.body?.textContent) { - bodyText = document.body.textContent; - } - return truncateText(bodyText.replace(/\s+/g, ' ').trim(), CAPTCHA_TEXT_MAX_LEN); - } catch (e) { - return ''; - } - } - - function matchHints(value, hints) { - const lower = String(value || '').toLowerCase(); - if (!lower) return false; - return hints.some((hint) => lower.includes(hint)); - } - - function detectCaptcha() { - const evidence = { - text_hits: [], - selector_hits: [], - iframe_src_hits: [], - url_hits: [], - }; - - let hasIframeHit = false; - let hasContainerHit = false; - let hasScriptHit = false; - let hasKeywordHit = false; - let hasUrlHit = false; - - const providerSignals = { - recaptcha: 0, - hcaptcha: 0, - turnstile: 0, - arkose: 0, - awswaf: 0, - }; - - // Iframe hints (strongest signal) - try { - const iframes = document.querySelectorAll('iframe'); - for (const iframe of iframes) { - const src = iframe.getAttribute('src') || ''; - const title = iframe.getAttribute('title') || ''; - if (src) { - for (const [provider, hints] of Object.entries(CAPTCHA_IFRAME_HINTS)) { - if (matchHints(src, hints)) { - hasIframeHit = true; - providerSignals[provider] += 1; - addEvidence(evidence.iframe_src_hits, truncateText(src, 120)); - } - } - } - if (title && matchHints(title, ['captcha', 'recaptcha'])) { - hasContainerHit = true; - addEvidence(evidence.selector_hits, 'iframe[title*="captcha"]'); - } - if (evidence.iframe_src_hits.length >= CAPTCHA_MAX_EVIDENCE) break; - } - } catch (e) { - // ignore - } - - // Script hints - try { - const scripts = document.querySelectorAll('script[src]'); - for (const script of scripts) { - const src = script.getAttribute('src') || ''; - if (!src) continue; - for (const [provider, hints] of Object.entries(CAPTCHA_SCRIPT_HINTS)) { - if (matchHints(src, hints)) { - hasScriptHit = true; - providerSignals[provider] += 1; - addEvidence(evidence.selector_hits, `script[src*="${hints[0]}"]`); - } - } - if (evidence.selector_hits.length >= CAPTCHA_MAX_EVIDENCE) break; - } - } catch (e) { - // ignore - } - - // Container selectors - for (const { selector, provider } of CAPTCHA_CONTAINER_SELECTORS) { - try { - const hit = document.querySelector(selector); - if (hit) { - hasContainerHit = true; - addEvidence(evidence.selector_hits, selector); - if (provider !== 'unknown') { - providerSignals[provider] += 1; - } - } - } catch (e) { - // ignore invalid selectors - } - } - - // Text keyword hints - const textSnippet = collectVisibleTextSnippet(); - if (textSnippet) { - const lowerText = textSnippet.toLowerCase(); - for (const keyword of CAPTCHA_TEXT_KEYWORDS) { - if (lowerText.includes(keyword)) { - hasKeywordHit = true; - addEvidence(evidence.text_hits, keyword); - } - } - } - - // URL hints - try { - const url = window.location?.href || ''; - const lowerUrl = url.toLowerCase(); - for (const hint of CAPTCHA_URL_HINTS) { - if (lowerUrl.includes(hint)) { - hasUrlHit = true; - addEvidence(evidence.url_hits, hint); - } - } - } catch (e) { - // ignore - } - - // Confidence scoring - let confidence = 0.0; - if (hasIframeHit) confidence += 0.7; - if (hasContainerHit) confidence += 0.5; - if (hasScriptHit) confidence += 0.5; - if (hasKeywordHit) confidence += 0.3; - if (hasUrlHit) confidence += 0.2; - confidence = Math.min(1.0, confidence); - - if (hasIframeHit) { - confidence = Math.max(confidence, 0.8); - } - - if (hasKeywordHit && !hasIframeHit && !hasContainerHit && !hasScriptHit && !hasUrlHit) { - confidence = Math.min(confidence, 0.4); - } - - const detected = confidence >= CAPTCHA_DETECTED_THRESHOLD; - - let providerHint = null; - if (providerSignals.recaptcha > 0) { - providerHint = 'recaptcha'; - } else if (providerSignals.hcaptcha > 0) { - providerHint = 'hcaptcha'; - } else if (providerSignals.turnstile > 0) { - providerHint = 'turnstile'; - } else if (providerSignals.arkose > 0) { - providerHint = 'arkose'; - } else if (providerSignals.awswaf > 0) { - providerHint = 'awswaf'; - } else if (detected) { - providerHint = 'unknown'; - } - - return { - detected, - provider_hint: providerHint, - confidence, - evidence, - }; - } - - // ============================================================================ - // LABEL INFERENCE SYSTEM - // ============================================================================ - - // Default inference configuration (conservative - Stage 1 equivalent) - const DEFAULT_INFERENCE_CONFIG = { - // Allowed tag names that can be inference sources - allowedTags: ['label', 'span', 'div'], - - // Allowed ARIA roles that can be inference sources - allowedRoles: [], - - // Class name patterns (substring match, case-insensitive) - allowedClassPatterns: [], - - // DOM tree traversal limits - maxParentDepth: 2, // Max 2 levels up DOM tree - maxSiblingDistance: 1, // Only immediate previous/next sibling - - // Container requirements (no distance-based checks) - requireSameContainer: true, // Must share common parent - containerTags: ['form', 'fieldset', 'div'], - - // Enable/disable specific inference methods - methods: { - explicitLabel: true, // el.labels API - ariaLabelledby: true, // aria-labelledby attribute - parentTraversal: true, // Check parent/grandparent - siblingProximity: true, // Check preceding sibling (same container only) - }, - }; - - // Merge user config with defaults - function mergeInferenceConfig(userConfig = {}) { - return { - ...DEFAULT_INFERENCE_CONFIG, - ...userConfig, - methods: { - ...DEFAULT_INFERENCE_CONFIG.methods, - ...(userConfig.methods || {}), - }, - }; - } - - // Check if element matches inference source criteria - function isInferenceSource(el, config) { - if (!el || !el.tagName) return false; - - const tag = el.tagName.toLowerCase(); - const role = el.getAttribute ? el.getAttribute('role') : ''; - const className = ((el.className || '') + '').toLowerCase(); - - // Check tag name - if (config.allowedTags.includes(tag)) { - return true; - } - - // Check role - if (config.allowedRoles.length > 0 && role && config.allowedRoles.includes(role)) { - return true; - } - - // Check class patterns - if (config.allowedClassPatterns.length > 0) { - for (const pattern of config.allowedClassPatterns) { - if (className.includes(pattern.toLowerCase())) { - return true; - } - } - } - - return false; - } - - // Helper: Find common parent element - function findCommonParent(el1, el2) { - if (!el1 || !el2) return null; - - // Get document reference safely for stopping conditions - // eslint-disable-next-line no-undef - const doc = - (typeof global !== 'undefined' && global.document) || - (typeof window !== 'undefined' && window.document) || - (typeof document !== 'undefined' && document) || - null; - - const parents1 = []; - let current = el1; - // Collect all parents (including el1 itself) - while (current) { - parents1.push(current); - // Stop if no parent - if (!current.parentElement) { - break; - } - // Stop at body or documentElement if they exist - if (doc && (current === doc.body || current === doc.documentElement)) { - break; - } - current = current.parentElement; - } - - // Check if el2 or any of its parents are in parents1 - current = el2; - while (current) { - // Use indexOf for more reliable comparison (handles object identity) - if (parents1.indexOf(current) !== -1) { - return current; - } - // Stop if no parent - if (!current.parentElement) { - break; - } - // Stop at body or documentElement if they exist - if (doc && (current === doc.body || current === doc.documentElement)) { - break; - } - current = current.parentElement; - } - - return null; - } - - // Helper: Check if element is a valid container - function isValidContainer(el, validTags) { - if (!el || !el.tagName) return false; - const tag = el.tagName.toLowerCase(); - // Handle both string and object className - let className = ''; - try { - className = (el.className || '') + ''; - } catch (e) { - className = ''; - } - return ( - validTags.includes(tag) || - className.toLowerCase().includes('form') || - className.toLowerCase().includes('field') - ); - } - - // Helper: Check container requirements (no distance-based checks) - function isInSameValidContainer(element, candidate, limits) { - if (!element || !candidate) return false; - - // Check same container requirement - if (limits.requireSameContainer) { - const commonParent = findCommonParent(element, candidate); - if (!commonParent) { - return false; - } - // Check if common parent is a valid container - if (!isValidContainer(commonParent, limits.containerTags)) { - return false; - } - } - - return true; - } - - // Main inference function - function getInferredLabel(el, options = {}) { - if (!el) return null; - - const { - enableInference = true, - inferenceConfig = {}, // User-provided config, merged with defaults - } = options; - - if (!enableInference) return null; - - // OPTIMIZATION: If element already has text or aria-label, skip inference entirely - // Check this BEFORE checking labels, so we don't infer if element already has text - // Note: For INPUT elements, we check value/placeholder, not innerText - // For IMG elements, we check alt, not innerText - // For other elements, innerText is considered explicit text - const ariaLabel = el.getAttribute ? el.getAttribute('aria-label') : null; - const hasAriaLabel = ariaLabel && ariaLabel.trim(); - const hasInputValue = el.tagName === 'INPUT' && (el.value || el.placeholder); - const hasImgAlt = el.tagName === 'IMG' && el.alt; - // For non-input/img elements, check innerText - but only if it's not empty - // Access innerText safely - it might be a getter or property - let innerTextValue = ''; - try { - innerTextValue = el.innerText || ''; - } catch (e) { - // If innerText access fails, treat as empty - innerTextValue = ''; - } - const hasInnerText = - el.tagName !== 'INPUT' && el.tagName !== 'IMG' && innerTextValue && innerTextValue.trim(); - - if (hasAriaLabel || hasInputValue || hasImgAlt || hasInnerText) { - return null; - } - - // Merge config with defaults - const config = mergeInferenceConfig(inferenceConfig); - - // Method 1: Explicit label association (el.labels API) - if (config.methods.explicitLabel && el.labels && el.labels.length > 0) { - const label = el.labels[0]; - if (isInferenceSource(label, config)) { - const text = (label.innerText || '').trim(); - if (text) { - return { - text, - source: 'explicit_label', - }; - } - } - } - - // Method 2: aria-labelledby (supports space-separated list of IDs) - // NOTE: aria-labelledby is an EXPLICIT reference, so it should work with ANY element - // regardless of inference source criteria. The config only controls whether this method runs. - if (config.methods.ariaLabelledby && el.hasAttribute && el.hasAttribute('aria-labelledby')) { - const labelIdsAttr = el.getAttribute('aria-labelledby'); - if (labelIdsAttr) { - // Split by whitespace to support multiple IDs (space-separated list) - const labelIds = labelIdsAttr.split(/\s+/).filter((id) => id.trim()); - const labelTexts = []; - - // Helper function to get document.getElementById from available contexts - const getDocument = () => { - // eslint-disable-next-line no-undef - if (typeof global !== 'undefined' && global.document) { - // eslint-disable-next-line no-undef - return global.document; - } - if (typeof window !== 'undefined' && window.document) { - return window.document; - } - if (typeof document !== 'undefined') { - return document; - } - return null; - }; - - const doc = getDocument(); - if (!doc || !doc.getElementById) ; else { - // Process each ID in the space-separated list - for (const labelId of labelIds) { - if (!labelId.trim()) continue; - - let labelEl = null; - try { - labelEl = doc.getElementById(labelId); - } catch (e) { - // If getElementById fails, skip this ID - continue; - } - - // aria-labelledby is an explicit reference - use ANY element, not just those matching inference criteria - // This follows ARIA spec: aria-labelledby can reference any element in the document - if (labelEl) { - // Extract text from the referenced element - let text = ''; - try { - // Try innerText first (preferred for visible text) - text = (labelEl.innerText || '').trim(); - // Fallback to textContent if innerText is empty - if (!text && labelEl.textContent) { - text = labelEl.textContent.trim(); - } - // Fallback to aria-label if available - if (!text && labelEl.getAttribute) { - const ariaLabel = labelEl.getAttribute('aria-label'); - if (ariaLabel) { - text = ariaLabel.trim(); - } - } - } catch (e) { - // If text extraction fails, skip this element - continue; - } - - if (text) { - labelTexts.push(text); - } - } - } - } - - // Combine multiple label texts (space-separated) - if (labelTexts.length > 0) { - return { - text: labelTexts.join(' '), - source: 'aria_labelledby', - }; - } - } - } - - // Method 3: Parent/grandparent traversal - if (config.methods.parentTraversal) { - let parent = el.parentElement; - let depth = 0; - while (parent && depth < config.maxParentDepth) { - if (isInferenceSource(parent, config)) { - const text = (parent.innerText || '').trim(); - if (text) { - return { - text, - source: 'parent_label', - }; - } - } - parent = parent.parentElement; - depth++; - } - } - - // Method 4: Preceding sibling (no distance-based checks, only DOM structure) - if (config.methods.siblingProximity) { - const prev = el.previousElementSibling; - if (prev && isInferenceSource(prev, config)) { - // Only check if they're in the same valid container (no pixel distance) - if ( - isInSameValidContainer(el, prev, { - requireSameContainer: config.requireSameContainer, - containerTags: config.containerTags, - }) - ) { - const text = (prev.innerText || '').trim(); - if (text) { - return { - text, - source: 'sibling_label', - }; - } - } - } - } - - return null; - } - - // --- HELPER: Nearby Static Text (cheap, best-effort) --- - // Returns a short, single-line snippet near the element (sibling/parent). - function getNearbyText(el, options = {}) { - if (!el) return null; - - const maxLen = typeof options.maxLen === 'number' ? options.maxLen : 80; - const ownText = normalizeNearbyText(el.innerText || ''); - - const candidates = []; - - const collect = (node) => { - if (!node) return; - let text = ''; - try { - text = normalizeNearbyText(node.innerText || node.textContent || ''); - } catch (e) { - text = ''; - } - if (!text || text === ownText) return; - candidates.push(text); - }; - - // Prefer immediate siblings - collect(el.previousElementSibling); - collect(el.nextElementSibling); - - // Fallback: short parent text (avoid large blocks) - if (candidates.length === 0 && el.parentElement) { - let parentText = ''; - try { - parentText = normalizeNearbyText(el.parentElement.innerText || ''); - } catch (e) { - parentText = ''; - } - if (parentText && parentText !== ownText && parentText.length <= 120) { - candidates.push(parentText); - } - } - - if (candidates.length === 0) return null; - - let text = candidates[0]; - if (text.length > maxLen) { - text = text.slice(0, maxLen).trim(); - } - return text || null; - } - - function normalizeNearbyText(text) { - if (!text) return ''; - return text.replace(/\s+/g, ' ').trim(); - } - - // Helper: Check if element is interactable (should have role inferred) - function isInteractableElement(el) { - if (!el || !el.tagName) return false; - - const tag = el.tagName.toLowerCase(); - const role = el.getAttribute ? el.getAttribute('role') : null; - const hasTabIndex = el.hasAttribute ? el.hasAttribute('tabindex') : false; - const hasHref = el.tagName === 'A' && (el.hasAttribute ? el.hasAttribute('href') : false); - - // Native interactive elements - const interactiveTags = [ - 'button', - 'input', - 'textarea', - 'select', - 'option', - 'details', - 'summary', - 'a', - ]; - if (interactiveTags.includes(tag)) { - // For , only if it has href - if (tag === 'a' && !hasHref) return false; - return true; - } - - // Elements with explicit interactive roles - const interactiveRoles = [ - 'button', - 'link', - 'tab', - 'menuitem', - 'checkbox', - 'radio', - 'switch', - 'slider', - 'combobox', - 'textbox', - 'searchbox', - 'spinbutton', - ]; - if (role && interactiveRoles.includes(role.toLowerCase())) { - return true; - } - - // Focusable elements (tabindex makes them interactive) - if (hasTabIndex) { - return true; - } - - // Elements with event handlers (custom interactive elements) - if (el.onclick || el.onkeydown || el.onkeypress || el.onkeyup) { - return true; - } - - // Check for inline event handlers via attributes - if (el.getAttribute) { - const hasInlineHandler = - el.getAttribute('onclick') || - el.getAttribute('onkeydown') || - el.getAttribute('onkeypress') || - el.getAttribute('onkeyup'); - if (hasInlineHandler) { - return true; - } - } - - return false; - } - - // Helper: Infer ARIA role for interactable elements - function getInferredRole(el, options = {}) { - const { - enableInference = true, - // inferenceConfig reserved for future extensibility - } = options; - - if (!enableInference) return null; - - // Only infer roles for interactable elements - if (!isInteractableElement(el)) { - return null; - } - - // CRITICAL: Only infer if element has NO aria-label AND NO explicit role - const hasAriaLabel = el.getAttribute ? el.getAttribute('aria-label') : null; - const hasExplicitRole = el.getAttribute ? el.getAttribute('role') : null; - - if (hasAriaLabel || hasExplicitRole) { - return null; // Skip inference if element already has aria-label or role - } - - // If element is native semantic HTML, it already has a role - const tag = el.tagName.toLowerCase(); - const semanticTags = ['button', 'a', 'input', 'textarea', 'select', 'option']; - if (semanticTags.includes(tag)) { - return null; // Native HTML already has role - } - - // Infer role based on element behavior or context - // Check for click handlers first (most common) - if (el.onclick || (el.getAttribute && el.getAttribute('onclick'))) { - return 'button'; - } - - // Check for keyboard handlers - if ( - el.onkeydown || - el.onkeypress || - el.onkeyup || - (el.getAttribute && - (el.getAttribute('onkeydown') || el.getAttribute('onkeypress') || el.getAttribute('onkeyup'))) - ) { - return 'button'; // Default to button for keyboard-interactive elements - } - - // Focusable div/span likely needs a role - if (el.hasAttribute && el.hasAttribute('tabindex') && (tag === 'div' || tag === 'span')) { - return 'button'; // Default assumption for focusable elements - } - - return null; - } - - // --- HELPER: Smart Text Extractor --- - function getText(el) { - if (el.getAttribute('aria-label')) return el.getAttribute('aria-label'); - if (el.tagName === 'INPUT') { - // Privacy: never return password values - const t = (el.getAttribute && el.getAttribute('type')) || el.type || ''; - if (String(t).toLowerCase() === 'password') { - return el.placeholder || ''; - } - return el.value || el.placeholder || ''; - } - if (el.tagName === 'IMG') return el.alt || ''; - return (el.innerText || '').replace(/\s+/g, ' ').trim().substring(0, 100); - } - - // Best-effort accessible name extraction for controls (used for v1 state-aware assertions) - function getAccessibleName(el) { - if (!el || !el.getAttribute) return ''; - - // 1) aria-label - const ariaLabel = el.getAttribute('aria-label'); - if (ariaLabel && ariaLabel.trim()) return ariaLabel.trim().substring(0, 200); - - // 2) aria-labelledby (space-separated IDs) - const labelledBy = el.getAttribute('aria-labelledby'); - if (labelledBy && labelledBy.trim()) { - const ids = labelledBy.split(/\s+/).filter((id) => id.trim()); - const texts = []; - for (const id of ids) { - try { - const ref = document.getElementById(id); - if (!ref) continue; - const txt = (ref.innerText || ref.textContent || ref.getAttribute?.('aria-label') || '') - .toString() - .trim(); - if (txt) texts.push(txt); - } catch (e) { - // ignore - } - } - if (texts.length > 0) return texts.join(' ').substring(0, 200); - } - - // 3) has the text - // Case 2: Spans that wrap links (parent spans like HN's "titleline") - child is the actionable element - // This significantly reduces element count on link-heavy pages (HN, Reddit, search results) - const tagName = el.tagName.toLowerCase(); - if (tagName === 'span') { - // Case 1: Span is inside a link (any ancestor ) - if (el.closest('a')) { - return; // Skip - parent link has the content - } - // Case 2: Span contains a link as ANY descendant (wrapper span) - // HN structure: Title... - // Also handles: ... - const childLink = el.querySelector('a[href]'); // Find ANY descendant link with href - if (childLink && childLink.href) { - return; // Skip - descendant link is the actionable element - } - // Debug: Log spans with "titleline" class that weren't filtered - if (options.debug && el.className && el.className.includes('titleline')) { - console.log('[SentienceAPI] DEBUG: titleline span NOT filtered', { - className: el.className, - text: el.textContent?.slice(0, 50), - childLink: childLink, - hasChildHref: childLink?.href, - }); - } - } - - window.sentience_registry[idx] = el; - - // Input type is needed for safe value redaction (passwords) and state-aware assertions - const inputType = - tagName === 'input' - ? toSafeString((el.getAttribute && el.getAttribute('type')) || el.type || null) - : null; - const isPasswordInput = inputType && inputType.toLowerCase() === 'password'; - - // Use getSemanticText for inference support (falls back to getText if no inference) - const semanticText = getSemanticText(el, { - enableInference: options.enableInference !== false, // Default: true - inferenceConfig: options.inferenceConfig, // Pass configurable inference settings - }); - const textVal = semanticText.text || getText(el); // Fallback to getText for backward compat - - // Infer role for interactable elements (only if no aria-label and no explicit role) - const inferredRole = getInferredRole(el, { - enableInference: options.enableInference !== false, - inferenceConfig: options.inferenceConfig, - }); - const inView = isInViewport(rect); - - // Get computed style once (needed for both occlusion check and data collection) - const style = window.getComputedStyle(el); - - // Only check occlusion for elements likely to be occluded (optimized) - // This avoids layout thrashing for the vast majority of elements - const occluded = inView ? isOccluded(el, rect, style) : false; - - // Get effective background color (traverses DOM to find non-transparent color) - const effectiveBgColor = getEffectiveBackgroundColor(el); - - // Safe value extraction (PII-aware) - let safeValue = null; - let valueRedacted = null; - try { - if (el.value !== undefined || (el.getAttribute && el.getAttribute('value') !== null)) { - if (isPasswordInput) { - safeValue = null; - valueRedacted = 'true'; - } else { - const rawValue = - el.value !== undefined ? String(el.value) : String(el.getAttribute('value')); - safeValue = rawValue.length > 200 ? rawValue.substring(0, 200) : rawValue; - valueRedacted = 'false'; - } - } - } catch (e) { - // ignore - } - - // Best-effort accessible name (label-like, not the typed value) - const accessibleName = toSafeString(getAccessibleName(el) || null); - - const nearbyText = isInteractableElement(el) ? getNearbyText(el, { maxLen: 80 }) : null; - - rawData.push({ - id: idx, - tag: tagName, - rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, - styles: { - display: toSafeString(style.display), - visibility: toSafeString(style.visibility), - opacity: toSafeString(style.opacity), - z_index: toSafeString(style.zIndex || 'auto'), - position: toSafeString(style.position), - bg_color: toSafeString(effectiveBgColor || style.backgroundColor), - color: toSafeString(style.color), - cursor: toSafeString(style.cursor), - font_weight: toSafeString(style.fontWeight), - font_size: toSafeString(style.fontSize), - }, - attributes: { - role: toSafeString(el.getAttribute('role')), - type_: toSafeString(el.getAttribute('type')), - input_type: inputType, - aria_label: - semanticText?.source === 'explicit_aria_label' - ? semanticText.text - : toSafeString(el.getAttribute('aria-label')), // Keep original for backward compat - name: accessibleName, - inferred_label: - semanticText?.source && - !['explicit_aria_label', 'input_value', 'img_alt', 'inner_text'].includes( - semanticText.source - ) - ? toSafeString(semanticText.text) - : null, - label_source: semanticText?.source || null, // Track source for gateway - inferred_role: inferredRole ? toSafeString(inferredRole) : null, // Inferred role for interactable elements - nearby_text: toSafeString(nearbyText), - // Get href: check element first, then traverse up to find parent link - // This ensures nested spans inside links inherit the href - href: toSafeString( - el.href || el.getAttribute('href') || (el.closest && el.closest('a')?.href) || null - ), - class: toSafeString(getClassName(el)), - // Capture dynamic input state (not just initial attributes) - value: safeValue !== null ? toSafeString(safeValue) : null, - value_redacted: valueRedacted, - checked: el.checked !== undefined ? String(el.checked) : null, - disabled: el.disabled !== undefined ? String(el.disabled) : null, - aria_checked: toSafeString(el.getAttribute('aria-checked')), - aria_disabled: toSafeString(el.getAttribute('aria-disabled')), - aria_expanded: toSafeString(el.getAttribute('aria-expanded')), - }, - text: toSafeString(textVal), - in_viewport: inView, - is_occluded: occluded, - // Phase 1: Pass scroll position for doc_y computation in WASM - scroll_y: window.scrollY, - }); - }); - - console.log(`[SentienceAPI] Collected ${rawData.length} elements from main frame`); - - // Step 1.5: Collect iframe snapshots and FLATTEN immediately - // "Flatten Early" architecture: Merge iframe elements into main array before WASM - // This allows WASM to process all elements uniformly (no recursion needed) - const allRawElements = [...rawData]; // Start with main frame elements - let totalIframeElements = 0; - - if (options.collectIframes !== false) { - try { - console.log(`[SentienceAPI] Starting iframe collection...`); - const iframeSnapshots = await collectIframeSnapshots(options); - console.log( - `[SentienceAPI] Iframe collection complete. Received ${iframeSnapshots.size} snapshot(s)` - ); - - if (iframeSnapshots.size > 0) { - // FLATTEN IMMEDIATELY: Don't nest them. Just append them with coordinate translation. - iframeSnapshots.forEach((iframeSnapshot, iframeEl) => { - // Debug: Log structure to verify data is correct - // console.log(`[SentienceAPI] Processing iframe snapshot:`, iframeSnapshot); - - if (iframeSnapshot && iframeSnapshot.raw_elements) { - const rawElementsCount = iframeSnapshot.raw_elements.length; - console.log( - `[SentienceAPI] Processing ${rawElementsCount} elements from iframe (src: ${iframeEl.src || 'unknown'})` - ); - // Get iframe's bounding rect (offset for coordinate translation) - const iframeRect = iframeEl.getBoundingClientRect(); - const offset = { x: iframeRect.x, y: iframeRect.y }; - - // Get iframe context for frame switching (Playwright needs this) - const iframeSrc = iframeEl.src || iframeEl.getAttribute('src') || ''; - let isSameOrigin = false; - try { - // Try to access contentWindow to check if same-origin - isSameOrigin = iframeEl.contentWindow !== null; - } catch (e) { - isSameOrigin = false; - } - - // Adjust coordinates and add iframe context to each element - const adjustedElements = iframeSnapshot.raw_elements.map((el) => { - const adjusted = { ...el }; - - // Adjust rect coordinates to parent viewport - if (adjusted.rect) { - adjusted.rect = { - ...adjusted.rect, - x: adjusted.rect.x + offset.x, - y: adjusted.rect.y + offset.y, - }; - } - - // Add iframe context so agents can switch frames in Playwright - adjusted.iframe_context = { - src: iframeSrc, - is_same_origin: isSameOrigin, - }; - - return adjusted; - }); - - // Append flattened iframe elements to main array - allRawElements.push(...adjustedElements); - totalIframeElements += adjustedElements.length; - } - }); - - // console.log(`[SentienceAPI] Merged ${iframeSnapshots.size} iframe(s). Total elements: ${allRawElements.length} (${rawData.length} main + ${totalIframeElements} iframe)`); - } - } catch (error) { - console.warn('[SentienceAPI] Iframe collection failed:', error); - } - } - - // Step 2: Send EVERYTHING to WASM (One giant flat list) - // Now WASM prunes iframe elements and main elements in one pass! - // No recursion needed - everything is already flat - console.log( - `[SentienceAPI] Sending ${allRawElements.length} total elements to WASM (${rawData.length} main + ${totalIframeElements} iframe)` - ); - const fallbackElementsFromRaw = (raw) => - (raw || []).map((r) => { - const rect = (r && r.rect) || { x: 0, y: 0, width: 0, height: 0 }; - const attrs = (r && r.attributes) || {}; - const role = - attrs.role || - (r && (r.inferred_role || r.inferredRole)) || - (r && r.tag === 'a' ? 'link' : 'generic'); - const href = attrs.href || (r && r.href) || null; - const isClickable = - role === 'link' || - role === 'button' || - role === 'textbox' || - role === 'checkbox' || - role === 'radio' || - role === 'combobox' || - !!href; - - return { - id: Number((r && r.id) || 0), - role: String(role || 'generic'), - text: (r && (r.text || r.semantic_text || r.semanticText)) || null, - importance: 1, - bbox: { - x: Number(rect.x || 0), - y: Number(rect.y || 0), - width: Number(rect.width || 0), - height: Number(rect.height || 0), - }, - visual_cues: { - is_primary: false, - is_clickable: !!isClickable, - }, - in_viewport: true, - is_occluded: !!(r && (r.occluded || r.is_occluded)), - z_index: 0, - name: attrs.aria_label || attrs.ariaLabel || null, - value: (r && r.value) || null, - input_type: attrs.type_ || attrs.type || null, - checked: typeof (r && r.checked) === 'boolean' ? r.checked : null, - disabled: typeof (r && r.disabled) === 'boolean' ? r.disabled : null, - expanded: typeof (r && r.expanded) === 'boolean' ? r.expanded : null, - }; - }); - - let processed = null; - try { - processed = await processSnapshotInBackground(allRawElements, options); - } catch (error) { - console.warn( - '[SentienceAPI] WASM processing failed; falling back to raw mapping:', - error - ); - processed = { - elements: fallbackElementsFromRaw(allRawElements), - raw_elements: allRawElements, - duration: null, - }; - } - - if (!processed || !processed.elements) { - processed = { - elements: fallbackElementsFromRaw(allRawElements), - raw_elements: allRawElements, - duration: null, - }; - } - - // Step 3: Capture screenshot if requested - let screenshot = null; - if (options.screenshot) { - screenshot = await captureScreenshot(options.screenshot); - } - - // Step 4: Clean and return - const cleanedElements = cleanElement(processed.elements); - const cleanedRawElements = cleanElement(processed.raw_elements); - - // FIXED: Removed undefined 'totalIframeRawElements' - // FIXED: Logic updated for "Flatten Early" architecture. - // processed.elements ALREADY contains the merged iframe elements, - // so we simply use .length. No addition needed. - - const totalCount = cleanedElements.length; - const totalRaw = cleanedRawElements.length; - const iframeCount = totalIframeElements || 0; - - console.log( - `[SentienceAPI] ✓ Complete: ${totalCount} Smart Elements, ${totalRaw} Raw Elements (includes ${iframeCount} from iframes) (WASM took ${processed.duration?.toFixed(1)}ms)` - ); - - // Snapshot diagnostics (Phase 2): report stability metrics from the page context. - // Confidence/exhaustion is computed in the Gateway/SDKs; the extension supplies raw metrics. - let diagnostics = undefined; - try { - const lastMutationTs = window.__sentience_lastMutationTs; - const now = performance.now(); - const quietMs = - typeof lastMutationTs === 'number' && Number.isFinite(lastMutationTs) - ? Math.max(0, now - lastMutationTs) - : null; - const nodeCount = document.querySelectorAll('*').length; - - // P1-01: best-effort signal that structure may be insufficient (vision executor recommended). - // Keep heuristics conservative: we only set requires_vision when we see clear structural blockers. - let requiresVision = false; - let requiresVisionReason = null; - const canvasCount = document.getElementsByTagName('canvas').length; - if (canvasCount > 0) { - requiresVision = true; - requiresVisionReason = `canvas:${canvasCount}`; - } - - diagnostics = { - metrics: { - ready_state: document.readyState || null, - quiet_ms: quietMs, - node_count: nodeCount, - }, - captcha: detectCaptcha(), - requires_vision: requiresVision, - requires_vision_reason: requiresVisionReason, - }; - } catch (e) { - // ignore - } - - return { - status: 'success', - url: window.location.href, - viewport: { - width: window.innerWidth, - height: window.innerHeight, - }, - elements: cleanedElements, - raw_elements: cleanedRawElements, - screenshot, - diagnostics, - }; - } catch (error) { - console.error('[SentienceAPI] snapshot() failed:', error); - console.error('[SentienceAPI] Error stack:', error.stack); - return { - status: 'error', - error: error.message || 'Unknown error', - stack: error.stack, - }; - } - } - - // read.js - Content Reading Methods - - // 2. Read Content (unchanged) - function read(options = {}) { - const format = options.format || 'raw'; - let content; - - if (format === 'raw') { - content = getRawHTML(document.body); - } else if (format === 'markdown') { - content = convertToMarkdown(document.body); - } else { - content = convertToText(document.body); - } - - return { - status: 'success', - url: window.location.href, - format, - content, - length: content.length, - }; - } - - // 2b. Find Text Rectangle - Get exact pixel coordinates of specific text - function findTextRect(options = {}) { - const { - text, - containerElement = document.body, - caseSensitive = false, - wholeWord = false, - maxResults = 10, - } = options; - - if (!text || text.trim().length === 0) { - return { - status: 'error', - error: 'Text parameter is required', - }; - } - - const results = []; - const searchText = caseSensitive ? text : text.toLowerCase(); - - // Helper function to find text in a single text node - function findInTextNode(textNode) { - const nodeText = textNode.nodeValue; - const searchableText = caseSensitive ? nodeText : nodeText.toLowerCase(); - - let startIndex = 0; - while (startIndex < nodeText.length && results.length < maxResults) { - const foundIndex = searchableText.indexOf(searchText, startIndex); - - if (foundIndex === -1) break; - - // Check whole word matching if required - if (wholeWord) { - const before = foundIndex > 0 ? nodeText[foundIndex - 1] : ' '; - const after = - foundIndex + text.length < nodeText.length ? nodeText[foundIndex + text.length] : ' '; - - // Check if surrounded by word boundaries - if (!/\s/.test(before) || !/\s/.test(after)) { - startIndex = foundIndex + 1; - continue; - } - } - - try { - // Create range for this occurrence - const range = document.createRange(); - range.setStart(textNode, foundIndex); - range.setEnd(textNode, foundIndex + text.length); - - const rect = range.getBoundingClientRect(); - - // Only include visible rectangles - if (rect.width > 0 && rect.height > 0) { - results.push({ - text: nodeText.substring(foundIndex, foundIndex + text.length), - rect: { - x: rect.left + window.scrollX, - y: rect.top + window.scrollY, - width: rect.width, - height: rect.height, - left: rect.left + window.scrollX, - top: rect.top + window.scrollY, - right: rect.right + window.scrollX, - bottom: rect.bottom + window.scrollY, - }, - viewport_rect: { - x: rect.left, - y: rect.top, - width: rect.width, - height: rect.height, - }, - context: { - before: nodeText.substring(Math.max(0, foundIndex - 20), foundIndex), - after: nodeText.substring( - foundIndex + text.length, - Math.min(nodeText.length, foundIndex + text.length + 20) - ), - }, - in_viewport: - rect.top >= 0 && - rect.left >= 0 && - rect.bottom <= window.innerHeight && - rect.right <= window.innerWidth, - }); - } - } catch (e) { - console.warn('[SentienceAPI] Failed to get rect for text:', e); - } - - startIndex = foundIndex + 1; - } - } - - // Tree walker to find all text nodes - const walker = document.createTreeWalker(containerElement, NodeFilter.SHOW_TEXT, { - acceptNode(node) { - // Skip script, style, and empty text nodes - const parent = node.parentElement; - if (!parent) return NodeFilter.FILTER_REJECT; - - const tagName = parent.tagName.toLowerCase(); - if (tagName === 'script' || tagName === 'style' || tagName === 'noscript') { - return NodeFilter.FILTER_REJECT; - } - - // Skip whitespace-only nodes - if (!node.nodeValue || node.nodeValue.trim().length === 0) { - return NodeFilter.FILTER_REJECT; - } - - // Check if element is visible - const computedStyle = window.getComputedStyle(parent); - if ( - computedStyle.display === 'none' || - computedStyle.visibility === 'hidden' || - computedStyle.opacity === '0' - ) { - return NodeFilter.FILTER_REJECT; - } - - return NodeFilter.FILTER_ACCEPT; - }, - }); - - // Walk through all text nodes - let currentNode; - while ((currentNode = walker.nextNode()) && results.length < maxResults) { - findInTextNode(currentNode); - } - - return { - status: 'success', - query: text, - case_sensitive: caseSensitive, - whole_word: wholeWord, - matches: results.length, - results, - viewport: { - width: window.innerWidth, - height: window.innerHeight, - scroll_x: window.scrollX, - scroll_y: window.scrollY, - }, - }; - } - - // click.js - Click Action Method - - // 3. Click Action (unchanged) - function click(id) { - const el = window.sentience_registry[id]; - if (el) { - el.click(); - el.focus(); - return true; - } - return false; - } - - // registry.js - Inspector Mode / Golden Set Collection - - // 4. Inspector Mode: Start Recording for Golden Set Collection - function startRecording(options = {}) { - const { - highlightColor = '#ff0000', - successColor = '#00ff00', - autoDisableTimeout = 30 * 60 * 1000, // 30 minutes default - keyboardShortcut = 'Ctrl+Shift+I', - } = options; - - console.log( - '🔴 [Sentience] Recording Mode STARTED. Click an element to copy its Ground Truth JSON.' - ); - console.log(` Press ${keyboardShortcut} or call stopRecording() to stop.`); - - // Validate registry is populated - if (!window.sentience_registry || window.sentience_registry.length === 0) { - console.warn( - '⚠️ Registry empty. Call `await window.sentience.snapshot()` first to populate registry.' - ); - alert('Registry empty. Run `await window.sentience.snapshot()` first!'); - return () => {}; // Return no-op cleanup function - } - - // Create reverse mapping for O(1) lookup (fixes registry lookup bug) - window.sentience_registry_map = new Map(); - window.sentience_registry.forEach((el, idx) => { - if (el) window.sentience_registry_map.set(el, idx); - }); - - // Create highlight box overlay - let highlightBox = document.getElementById('sentience-highlight-box'); - if (!highlightBox) { - highlightBox = document.createElement('div'); - highlightBox.id = 'sentience-highlight-box'; - highlightBox.style.cssText = ` - position: fixed; - pointer-events: none; - z-index: 2147483647; - border: 2px solid ${highlightColor}; - background: rgba(255, 0, 0, 0.1); - display: none; - transition: all 0.1s ease; - box-sizing: border-box; - `; - document.body.appendChild(highlightBox); - } - - // Create visual indicator (red border on page when recording) - let recordingIndicator = document.getElementById('sentience-recording-indicator'); - if (!recordingIndicator) { - recordingIndicator = document.createElement('div'); - recordingIndicator.id = 'sentience-recording-indicator'; - recordingIndicator.style.cssText = ` - position: fixed; - top: 0; - left: 0; - right: 0; - height: 3px; - background: ${highlightColor}; - z-index: 2147483646; - pointer-events: none; - `; - document.body.appendChild(recordingIndicator); - } - recordingIndicator.style.display = 'block'; - - // Hover handler (visual feedback) - const mouseOverHandler = (e) => { - const el = e.target; - if (!el || el === highlightBox || el === recordingIndicator) return; - - const rect = el.getBoundingClientRect(); - highlightBox.style.display = 'block'; - highlightBox.style.top = rect.top + window.scrollY + 'px'; - highlightBox.style.left = rect.left + window.scrollX + 'px'; - highlightBox.style.width = rect.width + 'px'; - highlightBox.style.height = rect.height + 'px'; - }; - - // Click handler (capture ground truth data) - const clickHandler = (e) => { - e.preventDefault(); - e.stopPropagation(); - - const el = e.target; - if (!el || el === highlightBox || el === recordingIndicator) return; - - // Use Map for reliable O(1) lookup - const sentienceId = window.sentience_registry_map.get(el); - if (sentienceId === undefined) { - console.warn('⚠️ Element not found in Sentience Registry. Did you run snapshot() first?'); - alert('Element not in registry. Run `await window.sentience.snapshot()` first!'); - return; - } - - // Extract raw data (ground truth + raw signals, NOT model outputs) - const rawData = extractRawElementData(el); - const selector = getUniqueSelector(el); - const role = el.getAttribute('role') || el.tagName.toLowerCase(); - const text = getText(el); - - // Build golden set JSON (ground truth + raw signals only) - const snippet = { - task: `Interact with ${text.substring(0, 20)}${text.length > 20 ? '...' : ''}`, - url: window.location.href, - timestamp: new Date().toISOString(), - target_criteria: { - id: sentienceId, - selector, - role, - text: text.substring(0, 50), - }, - debug_snapshot: rawData, - }; - - // Copy to clipboard - const jsonString = JSON.stringify(snippet, null, 2); - navigator.clipboard - .writeText(jsonString) - .then(() => { - console.log('✅ Copied Ground Truth to clipboard:', snippet); - - // Flash green to indicate success - highlightBox.style.border = `2px solid ${successColor}`; - highlightBox.style.background = 'rgba(0, 255, 0, 0.2)'; - setTimeout(() => { - highlightBox.style.border = `2px solid ${highlightColor}`; - highlightBox.style.background = 'rgba(255, 0, 0, 0.1)'; - }, 500); - }) - .catch((err) => { - console.error('❌ Failed to copy to clipboard:', err); - alert('Failed to copy to clipboard. Check console for JSON.'); - }); - }; - - // Auto-disable timeout - let timeoutId = null; - - // Cleanup function to stop recording (defined before use) - const stopRecording = () => { - document.removeEventListener('mouseover', mouseOverHandler, true); - document.removeEventListener('click', clickHandler, true); - document.removeEventListener('keydown', keyboardHandler, true); - - if (timeoutId) { - clearTimeout(timeoutId); - timeoutId = null; - } - - if (highlightBox) { - highlightBox.style.display = 'none'; - } - - if (recordingIndicator) { - recordingIndicator.style.display = 'none'; - } - - // Clean up registry map (optional, but good practice) - if (window.sentience_registry_map) { - window.sentience_registry_map.clear(); - } - - // Remove global reference - if (window.sentience_stopRecording === stopRecording) { - delete window.sentience_stopRecording; - } - - console.log('⚪ [Sentience] Recording Mode STOPPED.'); - }; - - // Keyboard shortcut handler (defined after stopRecording) - const keyboardHandler = (e) => { - // Ctrl+Shift+I or Cmd+Shift+I - if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'I') { - e.preventDefault(); - stopRecording(); - } - }; - - // Attach event listeners (use capture phase to intercept early) - document.addEventListener('mouseover', mouseOverHandler, true); - document.addEventListener('click', clickHandler, true); - document.addEventListener('keydown', keyboardHandler, true); - - // Set up auto-disable timeout - if (autoDisableTimeout > 0) { - timeoutId = setTimeout(() => { - console.log('⏰ [Sentience] Recording Mode auto-disabled after timeout.'); - stopRecording(); - }, autoDisableTimeout); - } - - // Store stop function globally for keyboard shortcut access - window.sentience_stopRecording = stopRecording; - - return stopRecording; - } - - // overlay.js - Visual Overlay Methods - - /** - * Show overlay highlighting specific elements with Shadow DOM - * @param {Array} elements - List of elements with bbox, importance, visual_cues - * @param {number} targetElementId - Optional ID of target element (shown in red) - */ - function showOverlay(elements, targetElementId = null) { - if (!elements || !Array.isArray(elements)) { - console.warn('[Sentience] showOverlay: elements must be an array'); - return; - } - - window.postMessage( - { - type: 'SENTIENCE_SHOW_OVERLAY', - elements, - targetElementId, - timestamp: Date.now(), - }, - '*' - ); - - console.log(`[Sentience] Overlay requested for ${elements.length} elements`); - } - - /** - * Show grid overlay highlighting detected grids - * @param {Array} grids - Array of GridInfo objects from SDK's get_grid_bounds() - * @param {number|null} targetGridId - Optional grid ID to highlight in red - */ - function showGrid(grids, targetGridId = null) { - if (!grids || !Array.isArray(grids)) { - console.warn('[Sentience] showGrid: grids must be an array'); - return; - } - - window.postMessage( - { - type: 'SENTIENCE_SHOW_GRID_OVERLAY', - grids, - targetGridId, - timestamp: Date.now(), - }, - '*' - ); - - console.log(`[Sentience] Grid overlay requested for ${grids.length} grids`); - } - - /** - * Clear overlay manually - */ - function clearOverlay() { - window.postMessage( - { - type: 'SENTIENCE_CLEAR_OVERLAY', - }, - '*' - ); - console.log('[Sentience] Overlay cleared'); - } - - // index.js - Main Entry Point for Injected API - // This script ONLY collects raw DOM data and sends it to background for processing - - - (async () => { - // console.log('[SentienceAPI] Initializing (CSP-Resistant Mode)...'); - - // Wait for Extension ID from content.js - const getExtensionId = () => document.documentElement.dataset.sentienceExtensionId; - let extId = getExtensionId(); - - if (!extId) { - await new Promise((resolve) => { - const check = setInterval(() => { - extId = getExtensionId(); - if (extId) { - clearInterval(check); - resolve(); - } - }, 50); - setTimeout(() => resolve(), 5000); // Max 5s wait - }); - } - - if (!extId) { - console.error('[SentienceAPI] Failed to get extension ID'); - return; - } - - // console.log('[SentienceAPI] Extension ID:', extId); - - // Registry for click actions (still needed for click() function) - window.sentience_registry = []; - - // --- GLOBAL API --- - window.sentience = { - snapshot, - read, - findTextRect, - click, - startRecording, - showOverlay, - showGrid, - clearOverlay, - }; - - // Setup iframe handler when script loads (only once) - if (!window.sentience_iframe_handler_setup) { - setupIframeSnapshotHandler(); - window.sentience_iframe_handler_setup = true; - } - - console.log('[SentienceAPI] ✓ Ready! (CSP-Resistant - WASM runs in background)'); - })(); - -})(); diff --git a/sentience/extension/injected_api.js b/sentience/extension/injected_api.js index b10f596..da5363b 100644 --- a/sentience/extension/injected_api.js +++ b/sentience/extension/injected_api.js @@ -103,9 +103,9 @@ const iframes = document.querySelectorAll("iframe"); for (const iframe of iframes) { const src = iframe.getAttribute("src") || "", title = iframe.getAttribute("title") || ""; - if (src) for (const [provider, hints] of Object.entries(CAPTCHA_IFRAME_HINTS)) matchHints(src, hints) && (hasIframeHit = !0, + if (src) for (const [provider, hints] of Object.entries(CAPTCHA_IFRAME_HINTS)) matchHints(src, hints) && (hasIframeHit = !0, providerSignals[provider] += 1, addEvidence(evidence.iframe_src_hits, truncateText(src, 120))); - if (title && matchHints(title, [ "captcha", "recaptcha" ]) && (hasContainerHit = !0, + if (title && matchHints(title, [ "captcha", "recaptcha" ]) && (hasContainerHit = !0, addEvidence(evidence.selector_hits, 'iframe[title*="captcha"]')), evidence.iframe_src_hits.length >= 5) break; } } catch (e) {} @@ -114,14 +114,14 @@ for (const script of scripts) { const src = script.getAttribute("src") || ""; if (src) { - for (const [provider, hints] of Object.entries(CAPTCHA_SCRIPT_HINTS)) matchHints(src, hints) && (hasScriptHit = !0, + for (const [provider, hints] of Object.entries(CAPTCHA_SCRIPT_HINTS)) matchHints(src, hints) && (hasScriptHit = !0, providerSignals[provider] += 1, addEvidence(evidence.selector_hits, `script[src*="${hints[0]}"]`)); if (evidence.selector_hits.length >= 5) break; } } } catch (e) {} for (const {selector: selector, provider: provider} of CAPTCHA_CONTAINER_SELECTORS) try { - document.querySelector(selector) && (hasContainerHit = !0, addEvidence(evidence.selector_hits, selector), + document.querySelector(selector) && (hasContainerHit = !0, addEvidence(evidence.selector_hits, selector), "unknown" !== provider && (providerSignals[provider] += 1)); } catch (e) {} const textSnippet = function() { @@ -139,7 +139,7 @@ } catch (e) {} try { let bodyText = document.body?.innerText || ""; - return !bodyText && document.body?.textContent && (bodyText = document.body.textContent), + return !bodyText && document.body?.textContent && (bodyText = document.body.textContent), truncateText(bodyText.replace(/\s+/g, " ").trim(), 2e3); } catch (e) { return ""; @@ -147,21 +147,21 @@ }(); if (textSnippet) { const lowerText = textSnippet.toLowerCase(); - for (const keyword of CAPTCHA_TEXT_KEYWORDS) lowerText.includes(keyword) && (hasKeywordHit = !0, + for (const keyword of CAPTCHA_TEXT_KEYWORDS) lowerText.includes(keyword) && (hasKeywordHit = !0, addEvidence(evidence.text_hits, keyword)); } try { const lowerUrl = (window.location?.href || "").toLowerCase(); - for (const hint of CAPTCHA_URL_HINTS) lowerUrl.includes(hint) && (hasUrlHit = !0, + for (const hint of CAPTCHA_URL_HINTS) lowerUrl.includes(hint) && (hasUrlHit = !0, addEvidence(evidence.url_hits, hint)); } catch (e) {} let confidence = 0; - hasIframeHit && (confidence += .7), hasContainerHit && (confidence += .5), hasScriptHit && (confidence += .5), - hasKeywordHit && (confidence += .3), hasUrlHit && (confidence += .2), confidence = Math.min(1, confidence), + hasIframeHit && (confidence += .7), hasContainerHit && (confidence += .5), hasScriptHit && (confidence += .5), + hasKeywordHit && (confidence += .3), hasUrlHit && (confidence += .2), confidence = Math.min(1, confidence), hasIframeHit && (confidence = Math.max(confidence, .8)), !hasKeywordHit || hasIframeHit || hasContainerHit || hasScriptHit || hasUrlHit || (confidence = Math.min(confidence, .4)); const detected = confidence >= .7; let providerHint = null; - return providerSignals.recaptcha > 0 ? providerHint = "recaptcha" : providerSignals.hcaptcha > 0 ? providerHint = "hcaptcha" : providerSignals.turnstile > 0 ? providerHint = "turnstile" : providerSignals.arkose > 0 ? providerHint = "arkose" : providerSignals.awswaf > 0 ? providerHint = "awswaf" : detected && (providerHint = "unknown"), + return providerSignals.recaptcha > 0 ? providerHint = "recaptcha" : providerSignals.hcaptcha > 0 ? providerHint = "hcaptcha" : providerSignals.turnstile > 0 ? providerHint = "turnstile" : providerSignals.arkose > 0 ? providerHint = "arkose" : providerSignals.awswaf > 0 ? providerHint = "awswaf" : detected && (providerHint = "unknown"), { detected: detected, provider_hint: providerHint, @@ -271,7 +271,7 @@ if (labelEl) { let text = ""; try { - if (text = (labelEl.innerText || "").trim(), !text && labelEl.textContent && (text = labelEl.textContent.trim()), + if (text = (labelEl.innerText || "").trim(), !text && labelEl.textContent && (text = labelEl.textContent.trim()), !text && labelEl.getAttribute) { const ariaLabel = labelEl.getAttribute("aria-label"); ariaLabel && (text = ariaLabel.trim()); @@ -466,7 +466,7 @@ }); const checkStable = () => { const timeSinceLastChange = Date.now() - lastChange, totalWait = Date.now() - startTime; - timeSinceLastChange >= quietPeriod || totalWait >= maxWait ? (observer.disconnect(), + timeSinceLastChange >= quietPeriod || totalWait >= maxWait ? (observer.disconnect(), resolve()) : setTimeout(checkStable, 50); }; checkStable(); @@ -492,7 +492,7 @@ }); const checkQuiet = () => { const timeSinceLastChange = Date.now() - lastChange, totalWait = Date.now() - startTime; - timeSinceLastChange >= quietPeriod || totalWait >= maxWait ? (quietObserver.disconnect(), + timeSinceLastChange >= quietPeriod || totalWait >= maxWait ? (quietObserver.disconnect(), resolve()) : setTimeout(checkQuiet, 50); }; checkQuiet(); @@ -607,7 +607,7 @@ }(el); let safeValue = null, valueRedacted = null; try { - if (void 0 !== el.value || el.getAttribute && null !== el.getAttribute("value")) if (isPasswordInput) safeValue = null, + if (void 0 !== el.value || el.getAttribute && null !== el.getAttribute("value")) if (isPasswordInput) safeValue = null, valueRedacted = "true"; else { const rawValue = void 0 !== el.value ? String(el.value) : String(el.getAttribute("value")); safeValue = rawValue.length > 200 ? rawValue.substring(0, 200) : rawValue, valueRedacted = "false"; @@ -734,8 +734,8 @@ const requestId = `iframe-${idx}-${Date.now()}`, timeout = setTimeout(() => { resolve(null); }, 5e3), listener = event => { - "SENTIENCE_IFRAME_SNAPSHOT_RESPONSE" === event.data?.type && event.data, "SENTIENCE_IFRAME_SNAPSHOT_RESPONSE" === event.data?.type && event.data?.requestId === requestId && (clearTimeout(timeout), - window.removeEventListener("message", listener), event.data.error ? resolve(null) : (event.data.snapshot, + "SENTIENCE_IFRAME_SNAPSHOT_RESPONSE" === event.data?.type && event.data, "SENTIENCE_IFRAME_SNAPSHOT_RESPONSE" === event.data?.type && event.data?.requestId === requestId && (clearTimeout(timeout), + window.removeEventListener("message", listener), event.data.error ? resolve(null) : (event.data.snapshot, resolve({ iframe: iframe, data: event.data.snapshot, @@ -751,7 +751,7 @@ ...options, collectIframes: !0 } - }, "*") : (clearTimeout(timeout), window.removeEventListener("message", listener), + }, "*") : (clearTimeout(timeout), window.removeEventListener("message", listener), resolve(null)); } catch (error) { clearTimeout(timeout), window.removeEventListener("message", listener), resolve(null); @@ -792,9 +792,7 @@ } }); } catch (error) {} - let processed = null; - try { - processed = await function(rawData, options) { + const processed = await function(rawData, options) { return new Promise((resolve, reject) => { const requestId = Math.random().toString(36).substring(7); let resolved = !1; @@ -803,7 +801,7 @@ }, 25e3), listener = e => { if ("SENTIENCE_SNAPSHOT_RESULT" === e.data.type && e.data.requestId === requestId) { if (resolved) return; - resolved = !0, clearTimeout(timeout), window.removeEventListener("message", listener), + resolved = !0, clearTimeout(timeout), window.removeEventListener("message", listener), e.data.error ? reject(new Error(e.data.error)) : resolve({ elements: e.data.elements, raw_elements: e.data.raw_elements, @@ -820,92 +818,17 @@ options: options }, "*"); } catch (error) { - resolved || (resolved = !0, clearTimeout(timeout), window.removeEventListener("message", listener), + resolved || (resolved = !0, clearTimeout(timeout), window.removeEventListener("message", listener), reject(new Error(`Failed to send snapshot request: ${error.message}`))); } }); }(allRawElements, options); - } catch (error) { - processed = { - elements: (allRawElements || []).map(r => { - const rect = r && r.rect || { - x: 0, - y: 0, - width: 0, - height: 0 - }, attrs = r && r.attributes || {}, role = attrs.role || r && (r.inferred_role || r.inferredRole) || ("a" === r.tag ? "link" : "generic"); - return { - id: Number(r && r.id || 0), - role: String(role || "generic"), - text: r && (r.text || r.semantic_text || r.semanticText) || null, - importance: 1, - bbox: { - x: Number(rect.x || 0), - y: Number(rect.y || 0), - width: Number(rect.width || 0), - height: Number(rect.height || 0) - }, - visual_cues: { - is_primary: !1, - is_clickable: !1 - }, - in_viewport: !0, - is_occluded: !!(r && (r.occluded || r.is_occluded)), - z_index: 0, - name: attrs.aria_label || attrs.ariaLabel || null, - value: r && r.value || null, - input_type: attrs.type_ || attrs.type || null, - checked: "boolean" == typeof r.checked ? r.checked : null, - disabled: "boolean" == typeof r.disabled ? r.disabled : null, - expanded: "boolean" == typeof r.expanded ? r.expanded : null - }; - }), - raw_elements: allRawElements, - duration: null - }; - } - if (!processed || !processed.elements) processed = { - elements: (allRawElements || []).map(r => { - const rect = r && r.rect || { - x: 0, - y: 0, - width: 0, - height: 0 - }, attrs = r && r.attributes || {}, role = attrs.role || r && (r.inferred_role || r.inferredRole) || ("a" === r.tag ? "link" : "generic"); - return { - id: Number(r && r.id || 0), - role: String(role || "generic"), - text: r && (r.text || r.semantic_text || r.semanticText) || null, - importance: 1, - bbox: { - x: Number(rect.x || 0), - y: Number(rect.y || 0), - width: Number(rect.width || 0), - height: Number(rect.height || 0) - }, - visual_cues: { - is_primary: !1, - is_clickable: !1 - }, - in_viewport: !0, - is_occluded: !!(r && (r.occluded || r.is_occluded)), - z_index: 0, - name: attrs.aria_label || attrs.ariaLabel || null, - value: r && r.value || null, - input_type: attrs.type_ || attrs.type || null, - checked: "boolean" == typeof r.checked ? r.checked : null, - disabled: "boolean" == typeof r.disabled ? r.disabled : null, - expanded: "boolean" == typeof r.expanded ? r.expanded : null - }; - }), - raw_elements: allRawElements, - duration: null - }; + if (!processed || !processed.elements) throw new Error("WASM processing returned invalid result"); let screenshot = null; options.screenshot && (screenshot = await function(options) { return new Promise(resolve => { const requestId = Math.random().toString(36).substring(7), listener = e => { - "SENTIENCE_SCREENSHOT_RESULT" === e.data.type && e.data.requestId === requestId && (window.removeEventListener("message", listener), + "SENTIENCE_SCREENSHOT_RESULT" === e.data.type && e.data.requestId === requestId && (window.removeEventListener("message", listener), resolve(e.data.screenshot)); }; window.addEventListener("message", listener), window.postMessage({ @@ -922,13 +845,18 @@ let diagnostics; try { const lastMutationTs = window.__sentience_lastMutationTs, now = performance.now(), quietMs = "number" == typeof lastMutationTs && Number.isFinite(lastMutationTs) ? Math.max(0, now - lastMutationTs) : null, nodeCount = document.querySelectorAll("*").length; + let requiresVision = !1, requiresVisionReason = null; + const canvasCount = document.getElementsByTagName("canvas").length; + canvasCount > 0 && (requiresVision = !0, requiresVisionReason = `canvas:${canvasCount}`), diagnostics = { metrics: { ready_state: document.readyState || null, quiet_ms: quietMs, node_count: nodeCount }, - captcha: detectCaptcha() + captcha: detectCaptcha(), + requires_vision: requiresVision, + requires_vision_reason: requiresVisionReason }; } catch (e) {} return { @@ -965,15 +893,15 @@ } if (node.nodeType !== Node.ELEMENT_NODE) return; const tag = node.tagName.toLowerCase(); - if ("h1" === tag && (markdown += "\n# "), "h2" === tag && (markdown += "\n## "), - "h3" === tag && (markdown += "\n### "), "li" === tag && (markdown += "\n- "), insideLink || "p" !== tag && "div" !== tag && "br" !== tag || (markdown += "\n"), - "strong" !== tag && "b" !== tag || (markdown += "**"), "em" !== tag && "i" !== tag || (markdown += "_"), - "a" === tag && (markdown += "[", insideLink = !0), node.shadowRoot ? Array.from(node.shadowRoot.childNodes).forEach(walk) : node.childNodes.forEach(walk), + if ("h1" === tag && (markdown += "\n# "), "h2" === tag && (markdown += "\n## "), + "h3" === tag && (markdown += "\n### "), "li" === tag && (markdown += "\n- "), insideLink || "p" !== tag && "div" !== tag && "br" !== tag || (markdown += "\n"), + "strong" !== tag && "b" !== tag || (markdown += "**"), "em" !== tag && "i" !== tag || (markdown += "_"), + "a" === tag && (markdown += "[", insideLink = !0), node.shadowRoot ? Array.from(node.shadowRoot.childNodes).forEach(walk) : node.childNodes.forEach(walk), "a" === tag) { const href = node.getAttribute("href"); markdown += href ? `](${href})` : "]", insideLink = !1; } - "strong" !== tag && "b" !== tag || (markdown += "**"), "em" !== tag && "i" !== tag || (markdown += "_"), + "strong" !== tag && "b" !== tag || (markdown += "**"), "em" !== tag && "i" !== tag || (markdown += "_"), insideLink || "h1" !== tag && "h2" !== tag && "h3" !== tag && "p" !== tag && "div" !== tag || (markdown += "\n"); }(tempDiv), markdown.replace(/\n{3,}/g, "\n\n").trim(); }(document.body) : function(root) { @@ -986,7 +914,7 @@ const style = window.getComputedStyle(node); if ("none" === style.display || "hidden" === style.visibility) return; const isBlock = "block" === style.display || "flex" === style.display || "P" === node.tagName || "DIV" === node.tagName; - isBlock && (text += " "), node.shadowRoot ? Array.from(node.shadowRoot.childNodes).forEach(walk) : node.childNodes.forEach(walk), + isBlock && (text += " "), node.shadowRoot ? Array.from(node.shadowRoot.childNodes).forEach(walk) : node.childNodes.forEach(walk), isBlock && (text += "\n"); } } else text += node.textContent; @@ -1085,25 +1013,25 @@ } function startRecording(options = {}) { const {highlightColor: highlightColor = "#ff0000", successColor: successColor = "#00ff00", autoDisableTimeout: autoDisableTimeout = 18e5, keyboardShortcut: keyboardShortcut = "Ctrl+Shift+I"} = options; - if (!window.sentience_registry || 0 === window.sentience_registry.length) return alert("Registry empty. Run `await window.sentience.snapshot()` first!"), + if (!window.sentience_registry || 0 === window.sentience_registry.length) return alert("Registry empty. Run `await window.sentience.snapshot()` first!"), () => {}; window.sentience_registry_map = new Map, window.sentience_registry.forEach((el, idx) => { el && window.sentience_registry_map.set(el, idx); }); let highlightBox = document.getElementById("sentience-highlight-box"); - highlightBox || (highlightBox = document.createElement("div"), highlightBox.id = "sentience-highlight-box", - highlightBox.style.cssText = `\n position: fixed;\n pointer-events: none;\n z-index: 2147483647;\n border: 2px solid ${highlightColor};\n background: rgba(255, 0, 0, 0.1);\n display: none;\n transition: all 0.1s ease;\n box-sizing: border-box;\n `, + highlightBox || (highlightBox = document.createElement("div"), highlightBox.id = "sentience-highlight-box", + highlightBox.style.cssText = `\n position: fixed;\n pointer-events: none;\n z-index: 2147483647;\n border: 2px solid ${highlightColor};\n background: rgba(255, 0, 0, 0.1);\n display: none;\n transition: all 0.1s ease;\n box-sizing: border-box;\n `, document.body.appendChild(highlightBox)); let recordingIndicator = document.getElementById("sentience-recording-indicator"); - recordingIndicator || (recordingIndicator = document.createElement("div"), recordingIndicator.id = "sentience-recording-indicator", - recordingIndicator.style.cssText = `\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n height: 3px;\n background: ${highlightColor};\n z-index: 2147483646;\n pointer-events: none;\n `, + recordingIndicator || (recordingIndicator = document.createElement("div"), recordingIndicator.id = "sentience-recording-indicator", + recordingIndicator.style.cssText = `\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n height: 3px;\n background: ${highlightColor};\n z-index: 2147483646;\n pointer-events: none;\n `, document.body.appendChild(recordingIndicator)), recordingIndicator.style.display = "block"; const mouseOverHandler = e => { const el = e.target; if (!el || el === highlightBox || el === recordingIndicator) return; const rect = el.getBoundingClientRect(); - highlightBox.style.display = "block", highlightBox.style.top = rect.top + window.scrollY + "px", - highlightBox.style.left = rect.left + window.scrollX + "px", highlightBox.style.width = rect.width + "px", + highlightBox.style.display = "block", highlightBox.style.top = rect.top + window.scrollY + "px", + highlightBox.style.left = rect.left + window.scrollX + "px", highlightBox.style.width = rect.width + "px", highlightBox.style.height = rect.height + "px"; }, clickHandler = e => { e.preventDefault(), e.stopPropagation(); @@ -1180,7 +1108,7 @@ debug_snapshot: rawData }, jsonString = JSON.stringify(snippet, null, 2); navigator.clipboard.writeText(jsonString).then(() => { - highlightBox.style.border = `2px solid ${successColor}`, highlightBox.style.background = "rgba(0, 255, 0, 0.2)", + highlightBox.style.border = `2px solid ${successColor}`, highlightBox.style.background = "rgba(0, 255, 0, 0.2)", setTimeout(() => { highlightBox.style.border = `2px solid ${highlightColor}`, highlightBox.style.background = "rgba(255, 0, 0, 0.1)"; }, 500); @@ -1190,15 +1118,15 @@ }; let timeoutId = null; const stopRecording = () => { - document.removeEventListener("mouseover", mouseOverHandler, !0), document.removeEventListener("click", clickHandler, !0), - document.removeEventListener("keydown", keyboardHandler, !0), timeoutId && (clearTimeout(timeoutId), - timeoutId = null), highlightBox && (highlightBox.style.display = "none"), recordingIndicator && (recordingIndicator.style.display = "none"), + document.removeEventListener("mouseover", mouseOverHandler, !0), document.removeEventListener("click", clickHandler, !0), + document.removeEventListener("keydown", keyboardHandler, !0), timeoutId && (clearTimeout(timeoutId), + timeoutId = null), highlightBox && (highlightBox.style.display = "none"), recordingIndicator && (recordingIndicator.style.display = "none"), window.sentience_registry_map && window.sentience_registry_map.clear(), window.sentience_stopRecording === stopRecording && delete window.sentience_stopRecording; }, keyboardHandler = e => { - (e.ctrlKey || e.metaKey) && e.shiftKey && "I" === e.key && (e.preventDefault(), + (e.ctrlKey || e.metaKey) && e.shiftKey && "I" === e.key && (e.preventDefault(), stopRecording()); }; - return document.addEventListener("mouseover", mouseOverHandler, !0), document.addEventListener("click", clickHandler, !0), + return document.addEventListener("mouseover", mouseOverHandler, !0), document.addEventListener("click", clickHandler, !0), document.addEventListener("keydown", keyboardHandler, !0), autoDisableTimeout > 0 && (timeoutId = setTimeout(() => { stopRecording(); }, autoDisableTimeout)), window.sentience_stopRecording = stopRecording, stopRecording; @@ -1267,4 +1195,4 @@ } }), window.sentience_iframe_handler_setup = !0)); })(); -}(); +}(); \ No newline at end of file diff --git a/sentience/extension/manifest.json b/sentience/extension/manifest.json index 23b3562..c1c6e6f 100644 --- a/sentience/extension/manifest.json +++ b/sentience/extension/manifest.json @@ -1,12 +1,12 @@ { "manifest_version": 3, "name": "Sentience Semantic Visual Grounding Extractor", - "version": "2.7.0", + "version": "2.8.0", "description": "Extract semantic visual grounding data from web pages", "permissions": ["activeTab", "scripting"], "host_permissions": [""], "background": { - "service_worker": "dist/background.js", + "service_worker": "background.js", "type": "module" }, "web_accessible_resources": [ @@ -18,13 +18,13 @@ "content_scripts": [ { "matches": [""], - "js": ["dist/content.js"], + "js": ["content.js"], "run_at": "document_start", "all_frames": true }, { "matches": [""], - "js": ["dist/injected_api.js"], + "js": ["injected_api.js"], "run_at": "document_idle", "world": "MAIN", "all_frames": true diff --git a/sentience/extension/pkg/sentience_core.d.ts b/sentience/extension/pkg/sentience_core.d.ts index e280c26..39ef420 100644 --- a/sentience/extension/pkg/sentience_core.d.ts +++ b/sentience/extension/pkg/sentience_core.d.ts @@ -18,34 +18,34 @@ export function prune_for_api(val: any): any; export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; export interface InitOutput { - readonly memory: WebAssembly.Memory; - readonly analyze_page: (a: number) => number; - readonly analyze_page_with_options: (a: number, b: number) => number; - readonly decide_and_act: (a: number) => void; - readonly prune_for_api: (a: number) => number; - readonly __wbindgen_export: (a: number, b: number) => number; - readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number; - readonly __wbindgen_export3: (a: number) => void; + readonly memory: WebAssembly.Memory; + readonly analyze_page: (a: number) => number; + readonly analyze_page_with_options: (a: number, b: number) => number; + readonly decide_and_act: (a: number) => void; + readonly prune_for_api: (a: number) => number; + readonly __wbindgen_export: (a: number, b: number) => number; + readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_export3: (a: number) => void; } export type SyncInitInput = BufferSource | WebAssembly.Module; /** -* Instantiates the given `module`, which can either be bytes or -* a precompiled `WebAssembly.Module`. -* -* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. -* -* @returns {InitOutput} -*/ + * Instantiates the given `module`, which can either be bytes or + * a precompiled `WebAssembly.Module`. + * + * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. + * + * @returns {InitOutput} + */ export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; /** -* If `module_or_path` is {RequestInfo} or {URL}, makes a request and -* for everything else, calls `WebAssembly.instantiate` directly. -* -* @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. -* -* @returns {Promise} -*/ + * If `module_or_path` is {RequestInfo} or {URL}, makes a request and + * for everything else, calls `WebAssembly.instantiate` directly. + * + * @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. + * + * @returns {Promise} + */ export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; diff --git a/sentience/extension/pkg/sentience_core.js b/sentience/extension/pkg/sentience_core.js index b232d13..bb9cae0 100644 --- a/sentience/extension/pkg/sentience_core.js +++ b/sentience/extension/pkg/sentience_core.js @@ -1,112 +1,247 @@ -let wasm; +export function analyze_page(val) { + return takeObject(wasm.analyze_page(addHeapObject(val))); +} + +export function analyze_page_with_options(val, options) { + return takeObject(wasm.analyze_page_with_options(addHeapObject(val), addHeapObject(options))); +} + +export function decide_and_act(_raw_elements) { + wasm.decide_and_act(addHeapObject(_raw_elements)); +} + +export function prune_for_api(val) { + return takeObject(wasm.prune_for_api(addHeapObject(val))); +} + +function __wbg_get_imports() { + const import0 = { + __proto__: null, + __wbg_Error_8c4e43fe74559d73: function(arg0, arg1) { + return addHeapObject(Error(getStringFromWasm0(arg0, arg1))); + }, + __wbg_Number_04624de7d0e8332d: function(arg0) { + return Number(getObject(arg0)); + }, + __wbg___wbindgen_bigint_get_as_i64_8fcf4ce7f1ca72a2: function(arg0, arg1) { + const v = getObject(arg1), ret = "bigint" == typeof v ? v : void 0; + getDataViewMemory0().setBigInt64(arg0 + 8, isLikeNone(ret) ? BigInt(0) : ret, !0), + getDataViewMemory0().setInt32(arg0 + 0, !isLikeNone(ret), !0); + }, + __wbg___wbindgen_boolean_get_bbbb1c18aa2f5e25: function(arg0) { + const v = getObject(arg0), ret = "boolean" == typeof v ? v : void 0; + return isLikeNone(ret) ? 16777215 : ret ? 1 : 0; + }, + __wbg___wbindgen_debug_string_0bc8482c6e3508ae: function(arg0, arg1) { + const ptr1 = passStringToWasm0(debugString(getObject(arg1)), wasm.__wbindgen_export, wasm.__wbindgen_export2), len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4, len1, !0), getDataViewMemory0().setInt32(arg0 + 0, ptr1, !0); + }, + __wbg___wbindgen_in_47fa6863be6f2f25: function(arg0, arg1) { + return getObject(arg0) in getObject(arg1); + }, + __wbg___wbindgen_is_bigint_31b12575b56f32fc: function(arg0) { + return "bigint" == typeof getObject(arg0); + }, + __wbg___wbindgen_is_function_0095a73b8b156f76: function(arg0) { + return "function" == typeof getObject(arg0); + }, + __wbg___wbindgen_is_object_5ae8e5880f2c1fbd: function(arg0) { + const val = getObject(arg0); + return "object" == typeof val && null !== val; + }, + __wbg___wbindgen_is_undefined_9e4d92534c42d778: function(arg0) { + return void 0 === getObject(arg0); + }, + __wbg___wbindgen_jsval_eq_11888390b0186270: function(arg0, arg1) { + return getObject(arg0) === getObject(arg1); + }, + __wbg___wbindgen_jsval_loose_eq_9dd77d8cd6671811: function(arg0, arg1) { + return getObject(arg0) == getObject(arg1); + }, + __wbg___wbindgen_number_get_8ff4255516ccad3e: function(arg0, arg1) { + const obj = getObject(arg1), ret = "number" == typeof obj ? obj : void 0; + getDataViewMemory0().setFloat64(arg0 + 8, isLikeNone(ret) ? 0 : ret, !0), getDataViewMemory0().setInt32(arg0 + 0, !isLikeNone(ret), !0); + }, + __wbg___wbindgen_string_get_72fb696202c56729: function(arg0, arg1) { + const obj = getObject(arg1), ret = "string" == typeof obj ? obj : void 0; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2), len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4, len1, !0), getDataViewMemory0().setInt32(arg0 + 0, ptr1, !0); + }, + __wbg___wbindgen_throw_be289d5034ed271b: function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }, + __wbg_call_389efe28435a9388: function() { + return handleError(function(arg0, arg1) { + return addHeapObject(getObject(arg0).call(getObject(arg1))); + }, arguments); + }, + __wbg_done_57b39ecd9addfe81: function(arg0) { + return getObject(arg0).done; + }, + __wbg_error_9a7fe3f932034cde: function(arg0) {}, + __wbg_get_9b94d73e6221f75c: function(arg0, arg1) { + return addHeapObject(getObject(arg0)[arg1 >>> 0]); + }, + __wbg_get_b3ed3ad4be2bc8ac: function() { + return handleError(function(arg0, arg1) { + return addHeapObject(Reflect.get(getObject(arg0), getObject(arg1))); + }, arguments); + }, + __wbg_get_with_ref_key_1dc361bd10053bfe: function(arg0, arg1) { + return addHeapObject(getObject(arg0)[getObject(arg1)]); + }, + __wbg_instanceof_ArrayBuffer_c367199e2fa2aa04: function(arg0) { + let result; + try { + result = getObject(arg0) instanceof ArrayBuffer; + } catch (_) { + result = !1; + } + return result; + }, + __wbg_instanceof_Uint8Array_9b9075935c74707c: function(arg0) { + let result; + try { + result = getObject(arg0) instanceof Uint8Array; + } catch (_) { + result = !1; + } + return result; + }, + __wbg_isArray_d314bb98fcf08331: function(arg0) { + return Array.isArray(getObject(arg0)); + }, + __wbg_isSafeInteger_bfbc7332a9768d2a: function(arg0) { + return Number.isSafeInteger(getObject(arg0)); + }, + __wbg_iterator_6ff6560ca1568e55: function() { + return addHeapObject(Symbol.iterator); + }, + __wbg_js_click_element_2fe1e774f3d232c7: function(arg0) { + js_click_element(arg0); + }, + __wbg_length_32ed9a279acd054c: function(arg0) { + return getObject(arg0).length; + }, + __wbg_length_35a7bace40f36eac: function(arg0) { + return getObject(arg0).length; + }, + __wbg_new_361308b2356cecd0: function() { + return addHeapObject(new Object); + }, + __wbg_new_3eb36ae241fe6f44: function() { + return addHeapObject(new Array); + }, + __wbg_new_dd2b680c8bf6ae29: function(arg0) { + return addHeapObject(new Uint8Array(getObject(arg0))); + }, + __wbg_next_3482f54c49e8af19: function() { + return handleError(function(arg0) { + return addHeapObject(getObject(arg0).next()); + }, arguments); + }, + __wbg_next_418f80d8f5303233: function(arg0) { + return addHeapObject(getObject(arg0).next); + }, + __wbg_prototypesetcall_bdcdcc5842e4d77d: function(arg0, arg1, arg2) { + Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), getObject(arg2)); + }, + __wbg_set_3f1d0b984ed272ed: function(arg0, arg1, arg2) { + getObject(arg0)[takeObject(arg1)] = takeObject(arg2); + }, + __wbg_set_f43e577aea94465b: function(arg0, arg1, arg2) { + getObject(arg0)[arg1 >>> 0] = takeObject(arg2); + }, + __wbg_value_0546255b415e96c1: function(arg0) { + return addHeapObject(getObject(arg0).value); + }, + __wbindgen_cast_0000000000000001: function(arg0) { + return addHeapObject(arg0); + }, + __wbindgen_cast_0000000000000002: function(arg0, arg1) { + return addHeapObject(getStringFromWasm0(arg0, arg1)); + }, + __wbindgen_cast_0000000000000003: function(arg0) { + return addHeapObject(BigInt.asUintN(64, arg0)); + }, + __wbindgen_object_clone_ref: function(arg0) { + return addHeapObject(getObject(arg0)); + }, + __wbindgen_object_drop_ref: function(arg0) { + takeObject(arg0); + } + }; + return { + __proto__: null, + "./sentience_core_bg.js": import0 + }; +} function addHeapObject(obj) { - if (heap_next === heap.length) heap.push(heap.length + 1); + heap_next === heap.length && heap.push(heap.length + 1); const idx = heap_next; - heap_next = heap[idx]; - - heap[idx] = obj; - return idx; + return heap_next = heap[idx], heap[idx] = obj, idx; } function debugString(val) { - // primitive types const type = typeof val; - if (type == 'number' || type == 'boolean' || val == null) { - return `${val}`; - } - if (type == 'string') { - return `"${val}"`; - } - if (type == 'symbol') { + if ("number" == type || "boolean" == type || null == val) return `${val}`; + if ("string" == type) return `"${val}"`; + if ("symbol" == type) { const description = val.description; - if (description == null) { - return 'Symbol'; - } else { - return `Symbol(${description})`; - } + return null == description ? "Symbol" : `Symbol(${description})`; } - if (type == 'function') { + if ("function" == type) { const name = val.name; - if (typeof name == 'string' && name.length > 0) { - return `Function(${name})`; - } else { - return 'Function'; - } + return "string" == typeof name && name.length > 0 ? `Function(${name})` : "Function"; } - // objects if (Array.isArray(val)) { const length = val.length; - let debug = '['; - if (length > 0) { - debug += debugString(val[0]); - } - for(let i = 1; i < length; i++) { - debug += ', ' + debugString(val[i]); - } - debug += ']'; - return debug; + let debug = "["; + length > 0 && (debug += debugString(val[0])); + for (let i = 1; i < length; i++) debug += ", " + debugString(val[i]); + return debug += "]", debug; } - // Test for built-in const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); let className; - if (builtInMatches && builtInMatches.length > 1) { - className = builtInMatches[1]; - } else { - // Failed to match the standard '[object ClassName]' - return toString.call(val); - } - if (className == 'Object') { - // we're a user defined class or Object - // JSON.stringify avoids problems with cycles, and is generally much - // easier than looping through ownProperties of `val`. - try { - return 'Object(' + JSON.stringify(val) + ')'; - } catch (_) { - return 'Object'; - } - } - // errors - if (val instanceof Error) { - return `${val.name}: ${val.message}\n${val.stack}`; + if (!(builtInMatches && builtInMatches.length > 1)) return toString.call(val); + if (className = builtInMatches[1], "Object" == className) try { + return "Object(" + JSON.stringify(val) + ")"; + } catch (_) { + return "Object"; } - // TODO we could test for more things here, like `Set`s and `Map`s. - return className; + return val instanceof Error ? `${val.name}: ${val.message}\n${val.stack}` : className; } function dropObject(idx) { - if (idx < 132) return; - heap[idx] = heap_next; - heap_next = idx; + idx < 132 || (heap[idx] = heap_next, heap_next = idx); } function getArrayU8FromWasm0(ptr, len) { - ptr = ptr >>> 0; - return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); + return ptr >>>= 0, getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); } let cachedDataViewMemory0 = null; + function getDataViewMemory0() { - if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { - cachedDataViewMemory0 = new DataView(wasm.memory.buffer); - } - return cachedDataViewMemory0; + return (null === cachedDataViewMemory0 || !0 === cachedDataViewMemory0.buffer.detached || void 0 === cachedDataViewMemory0.buffer.detached && cachedDataViewMemory0.buffer !== wasm.memory.buffer) && (cachedDataViewMemory0 = new DataView(wasm.memory.buffer)), + cachedDataViewMemory0; } function getStringFromWasm0(ptr, len) { - ptr = ptr >>> 0; - return decodeText(ptr, len); + return decodeText(ptr >>>= 0, len); } let cachedUint8ArrayMemory0 = null; + function getUint8ArrayMemory0() { - if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { - cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); - } - return cachedUint8ArrayMemory0; + return null !== cachedUint8ArrayMemory0 && 0 !== cachedUint8ArrayMemory0.byteLength || (cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer)), + cachedUint8ArrayMemory0; } -function getObject(idx) { return heap[idx]; } +function getObject(idx) { + return heap[idx]; +} function handleError(f, args) { try { @@ -116,414 +251,121 @@ function handleError(f, args) { } } -let heap = new Array(128).fill(undefined); -heap.push(undefined, null, true, false); +let heap = new Array(128).fill(void 0); + +heap.push(void 0, null, !0, !1); let heap_next = heap.length; function isLikeNone(x) { - return x === undefined || x === null; + return null == x; } function passStringToWasm0(arg, malloc, realloc) { - if (realloc === undefined) { - const buf = cachedTextEncoder.encode(arg); - const ptr = malloc(buf.length, 1) >>> 0; - getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); - WASM_VECTOR_LEN = buf.length; - return ptr; + if (void 0 === realloc) { + const buf = cachedTextEncoder.encode(arg), ptr = malloc(buf.length, 1) >>> 0; + return getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf), WASM_VECTOR_LEN = buf.length, + ptr; } - - let len = arg.length; - let ptr = malloc(len, 1) >>> 0; - + let len = arg.length, ptr = malloc(len, 1) >>> 0; const mem = getUint8ArrayMemory0(); - let offset = 0; - - for (; offset < len; offset++) { + for (;offset < len; offset++) { const code = arg.charCodeAt(offset); - if (code > 0x7F) break; + if (code > 127) break; mem[ptr + offset] = code; } if (offset !== len) { - if (offset !== 0) { - arg = arg.slice(offset); - } - ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + 0 !== offset && (arg = arg.slice(offset)), ptr = realloc(ptr, len, len = offset + 3 * arg.length, 1) >>> 0; const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); - const ret = cachedTextEncoder.encodeInto(arg, view); - - offset += ret.written; - ptr = realloc(ptr, len, offset, 1) >>> 0; + offset += cachedTextEncoder.encodeInto(arg, view).written, ptr = realloc(ptr, len, offset, 1) >>> 0; } - - WASM_VECTOR_LEN = offset; - return ptr; + return WASM_VECTOR_LEN = offset, ptr; } function takeObject(idx) { const ret = getObject(idx); - dropObject(idx); - return ret; + return dropObject(idx), ret; } -let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +let cachedTextDecoder = new TextDecoder("utf-8", { + ignoreBOM: !0, + fatal: !0 +}); + cachedTextDecoder.decode(); + const MAX_SAFARI_DECODE_BYTES = 2146435072; -let numBytesDecoded = 0; -function decodeText(ptr, len) { - numBytesDecoded += len; - if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { - cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); - cachedTextDecoder.decode(); - numBytesDecoded = len; - } - return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); -} -const cachedTextEncoder = new TextEncoder(); +let numBytesDecoded = 0; -if (!('encodeInto' in cachedTextEncoder)) { - cachedTextEncoder.encodeInto = function (arg, view) { - const buf = cachedTextEncoder.encode(arg); - view.set(buf); - return { - read: arg.length, - written: buf.length - }; - } +function decodeText(ptr, len) { + return numBytesDecoded += len, numBytesDecoded >= MAX_SAFARI_DECODE_BYTES && (cachedTextDecoder = new TextDecoder("utf-8", { + ignoreBOM: !0, + fatal: !0 + }), cachedTextDecoder.decode(), numBytesDecoded = len), cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); } -let WASM_VECTOR_LEN = 0; +const cachedTextEncoder = new TextEncoder; -/** - * @param {any} val - * @returns {any} - */ -export function analyze_page(val) { - const ret = wasm.analyze_page(addHeapObject(val)); - return takeObject(ret); -} - -/** - * @param {any} val - * @param {any} options - * @returns {any} - */ -export function analyze_page_with_options(val, options) { - const ret = wasm.analyze_page_with_options(addHeapObject(val), addHeapObject(options)); - return takeObject(ret); -} +"encodeInto" in cachedTextEncoder || (cachedTextEncoder.encodeInto = function(arg, view) { + const buf = cachedTextEncoder.encode(arg); + return view.set(buf), { + read: arg.length, + written: buf.length + }; +}); -/** - * @param {any} _raw_elements - */ -export function decide_and_act(_raw_elements) { - wasm.decide_and_act(addHeapObject(_raw_elements)); -} +let wasmModule, wasm, WASM_VECTOR_LEN = 0; -/** - * Prune raw elements before sending to API - * This is a "dumb" filter that reduces payload size without leaking proprietary IP - * Filters out: tiny elements, invisible elements, non-interactive wrapper divs - * Amazon: 5000-6000 elements -> ~200-400 elements (~95% reduction) - * @param {any} val - * @returns {any} - */ -export function prune_for_api(val) { - const ret = wasm.prune_for_api(addHeapObject(val)); - return takeObject(ret); +function __wbg_finalize_init(instance, module) { + return wasm = instance.exports, wasmModule = module, cachedDataViewMemory0 = null, + cachedUint8ArrayMemory0 = null, wasm; } -const EXPECTED_RESPONSE_TYPES = new Set(['basic', 'cors', 'default']); - async function __wbg_load(module, imports) { - if (typeof Response === 'function' && module instanceof Response) { - if (typeof WebAssembly.instantiateStreaming === 'function') { - try { - return await WebAssembly.instantiateStreaming(module, imports); - } catch (e) { - const validResponse = module.ok && EXPECTED_RESPONSE_TYPES.has(module.type); - - if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { - console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); - - } else { - throw e; + if ("function" == typeof Response && module instanceof Response) { + if ("function" == typeof WebAssembly.instantiateStreaming) try { + return await WebAssembly.instantiateStreaming(module, imports); + } catch (e) { + if (!(module.ok && function(type) { + switch (type) { + case "basic": + case "cors": + case "default": + return !0; } - } + return !1; + }(module.type)) || "application/wasm" === module.headers.get("Content-Type")) throw e; } - const bytes = await module.arrayBuffer(); return await WebAssembly.instantiate(bytes, imports); - } else { + } + { const instance = await WebAssembly.instantiate(module, imports); - - if (instance instanceof WebAssembly.Instance) { - return { instance, module }; - } else { - return instance; - } + return instance instanceof WebAssembly.Instance ? { + instance: instance, + module: module + } : instance; } } -function __wbg_get_imports() { - const imports = {}; - imports.wbg = {}; - imports.wbg.__wbg_Error_52673b7de5a0ca89 = function(arg0, arg1) { - const ret = Error(getStringFromWasm0(arg0, arg1)); - return addHeapObject(ret); - }; - imports.wbg.__wbg_Number_2d1dcfcf4ec51736 = function(arg0) { - const ret = Number(getObject(arg0)); - return ret; - }; - imports.wbg.__wbg___wbindgen_bigint_get_as_i64_6e32f5e6aff02e1d = function(arg0, arg1) { - const v = getObject(arg1); - const ret = typeof(v) === 'bigint' ? v : undefined; - getDataViewMemory0().setBigInt64(arg0 + 8 * 1, isLikeNone(ret) ? BigInt(0) : ret, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); - }; - imports.wbg.__wbg___wbindgen_boolean_get_dea25b33882b895b = function(arg0) { - const v = getObject(arg0); - const ret = typeof(v) === 'boolean' ? v : undefined; - return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0; - }; - imports.wbg.__wbg___wbindgen_debug_string_adfb662ae34724b6 = function(arg0, arg1) { - const ret = debugString(getObject(arg1)); - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }; - imports.wbg.__wbg___wbindgen_in_0d3e1e8f0c669317 = function(arg0, arg1) { - const ret = getObject(arg0) in getObject(arg1); - return ret; - }; - imports.wbg.__wbg___wbindgen_is_bigint_0e1a2e3f55cfae27 = function(arg0) { - const ret = typeof(getObject(arg0)) === 'bigint'; - return ret; - }; - imports.wbg.__wbg___wbindgen_is_function_8d400b8b1af978cd = function(arg0) { - const ret = typeof(getObject(arg0)) === 'function'; - return ret; - }; - imports.wbg.__wbg___wbindgen_is_object_ce774f3490692386 = function(arg0) { - const val = getObject(arg0); - const ret = typeof(val) === 'object' && val !== null; - return ret; - }; - imports.wbg.__wbg___wbindgen_is_undefined_f6b95eab589e0269 = function(arg0) { - const ret = getObject(arg0) === undefined; - return ret; - }; - imports.wbg.__wbg___wbindgen_jsval_eq_b6101cc9cef1fe36 = function(arg0, arg1) { - const ret = getObject(arg0) === getObject(arg1); - return ret; - }; - imports.wbg.__wbg___wbindgen_jsval_loose_eq_766057600fdd1b0d = function(arg0, arg1) { - const ret = getObject(arg0) == getObject(arg1); - return ret; - }; - imports.wbg.__wbg___wbindgen_number_get_9619185a74197f95 = function(arg0, arg1) { - const obj = getObject(arg1); - const ret = typeof(obj) === 'number' ? obj : undefined; - getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); - }; - imports.wbg.__wbg___wbindgen_string_get_a2a31e16edf96e42 = function(arg0, arg1) { - const obj = getObject(arg1); - const ret = typeof(obj) === 'string' ? obj : undefined; - var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2); - var len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }; - imports.wbg.__wbg___wbindgen_throw_dd24417ed36fc46e = function(arg0, arg1) { - throw new Error(getStringFromWasm0(arg0, arg1)); - }; - imports.wbg.__wbg_call_abb4ff46ce38be40 = function() { return handleError(function (arg0, arg1) { - const ret = getObject(arg0).call(getObject(arg1)); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_done_62ea16af4ce34b24 = function(arg0) { - const ret = getObject(arg0).done; - return ret; - }; - imports.wbg.__wbg_error_7bc7d576a6aaf855 = function(arg0) { - console.error(getObject(arg0)); - }; - imports.wbg.__wbg_get_6b7bd52aca3f9671 = function(arg0, arg1) { - const ret = getObject(arg0)[arg1 >>> 0]; - return addHeapObject(ret); - }; - imports.wbg.__wbg_get_af9dab7e9603ea93 = function() { return handleError(function (arg0, arg1) { - const ret = Reflect.get(getObject(arg0), getObject(arg1)); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_get_with_ref_key_1dc361bd10053bfe = function(arg0, arg1) { - const ret = getObject(arg0)[getObject(arg1)]; - return addHeapObject(ret); - }; - imports.wbg.__wbg_instanceof_ArrayBuffer_f3320d2419cd0355 = function(arg0) { - let result; - try { - result = getObject(arg0) instanceof ArrayBuffer; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_instanceof_Uint8Array_da54ccc9d3e09434 = function(arg0) { - let result; - try { - result = getObject(arg0) instanceof Uint8Array; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }; - imports.wbg.__wbg_isArray_51fd9e6422c0a395 = function(arg0) { - const ret = Array.isArray(getObject(arg0)); - return ret; - }; - imports.wbg.__wbg_isSafeInteger_ae7d3f054d55fa16 = function(arg0) { - const ret = Number.isSafeInteger(getObject(arg0)); - return ret; - }; - imports.wbg.__wbg_iterator_27b7c8b35ab3e86b = function() { - const ret = Symbol.iterator; - return addHeapObject(ret); - }; - imports.wbg.__wbg_js_click_element_2fe1e774f3d232c7 = function(arg0) { - js_click_element(arg0); - }; - imports.wbg.__wbg_length_22ac23eaec9d8053 = function(arg0) { - const ret = getObject(arg0).length; - return ret; - }; - imports.wbg.__wbg_length_d45040a40c570362 = function(arg0) { - const ret = getObject(arg0).length; - return ret; - }; - imports.wbg.__wbg_new_1ba21ce319a06297 = function() { - const ret = new Object(); - return addHeapObject(ret); - }; - imports.wbg.__wbg_new_25f239778d6112b9 = function() { - const ret = new Array(); - return addHeapObject(ret); - }; - imports.wbg.__wbg_new_6421f6084cc5bc5a = function(arg0) { - const ret = new Uint8Array(getObject(arg0)); - return addHeapObject(ret); - }; - imports.wbg.__wbg_next_138a17bbf04e926c = function(arg0) { - const ret = getObject(arg0).next; - return addHeapObject(ret); - }; - imports.wbg.__wbg_next_3cfe5c0fe2a4cc53 = function() { return handleError(function (arg0) { - const ret = getObject(arg0).next(); - return addHeapObject(ret); - }, arguments) }; - imports.wbg.__wbg_prototypesetcall_dfe9b766cdc1f1fd = function(arg0, arg1, arg2) { - Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), getObject(arg2)); - }; - imports.wbg.__wbg_set_3f1d0b984ed272ed = function(arg0, arg1, arg2) { - getObject(arg0)[takeObject(arg1)] = takeObject(arg2); - }; - imports.wbg.__wbg_set_7df433eea03a5c14 = function(arg0, arg1, arg2) { - getObject(arg0)[arg1 >>> 0] = takeObject(arg2); - }; - imports.wbg.__wbg_value_57b7b035e117f7ee = function(arg0) { - const ret = getObject(arg0).value; - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_cast_2241b6af4c4b2941 = function(arg0, arg1) { - // Cast intrinsic for `Ref(String) -> Externref`. - const ret = getStringFromWasm0(arg0, arg1); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_cast_4625c577ab2ec9ee = function(arg0) { - // Cast intrinsic for `U64 -> Externref`. - const ret = BigInt.asUintN(64, arg0); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_cast_d6cd19b81560fd6e = function(arg0) { - // Cast intrinsic for `F64 -> Externref`. - const ret = arg0; - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_object_clone_ref = function(arg0) { - const ret = getObject(arg0); - return addHeapObject(ret); - }; - imports.wbg.__wbindgen_object_drop_ref = function(arg0) { - takeObject(arg0); - }; - - return imports; -} - -function __wbg_finalize_init(instance, module) { - wasm = instance.exports; - __wbg_init.__wbindgen_wasm_module = module; - cachedDataViewMemory0 = null; - cachedUint8ArrayMemory0 = null; - - - - return wasm; -} - function initSync(module) { - if (wasm !== undefined) return wasm; - - - if (typeof module !== 'undefined') { - if (Object.getPrototypeOf(module) === Object.prototype) { - ({module} = module) - } else { - console.warn('using deprecated parameters for `initSync()`; pass a single object instead') - } - } - + if (void 0 !== wasm) return wasm; + void 0 !== module && Object.getPrototypeOf(module) === Object.prototype && ({module: module} = module); const imports = __wbg_get_imports(); - if (!(module instanceof WebAssembly.Module)) { - module = new WebAssembly.Module(module); - } - const instance = new WebAssembly.Instance(module, imports); - return __wbg_finalize_init(instance, module); + module instanceof WebAssembly.Module || (module = new WebAssembly.Module(module)); + return __wbg_finalize_init(new WebAssembly.Instance(module, imports), module); } async function __wbg_init(module_or_path) { - if (wasm !== undefined) return wasm; - - - if (typeof module_or_path !== 'undefined') { - if (Object.getPrototypeOf(module_or_path) === Object.prototype) { - ({module_or_path} = module_or_path) - } else { - console.warn('using deprecated parameters for the initialization function; pass a single object instead') - } - } - - if (typeof module_or_path === 'undefined') { - module_or_path = new URL('sentience_core_bg.wasm', import.meta.url); - } + if (void 0 !== wasm) return wasm; + void 0 !== module_or_path && Object.getPrototypeOf(module_or_path) === Object.prototype && ({module_or_path: module_or_path} = module_or_path), + void 0 === module_or_path && (module_or_path = new URL("sentience_core_bg.wasm", import.meta.url)); const imports = __wbg_get_imports(); - - if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { - module_or_path = fetch(module_or_path); - } - - const { instance, module } = await __wbg_load(await module_or_path, imports); - + ("string" == typeof module_or_path || "function" == typeof Request && module_or_path instanceof Request || "function" == typeof URL && module_or_path instanceof URL) && (module_or_path = fetch(module_or_path)); + const {instance: instance, module: module} = await __wbg_load(await module_or_path, imports); return __wbg_finalize_init(instance, module); } -export { initSync }; -export default __wbg_init; +export { initSync, __wbg_init as default }; \ No newline at end of file diff --git a/sentience/extension/pkg/sentience_core_bg.wasm b/sentience/extension/pkg/sentience_core_bg.wasm index bcbccbe8d06c994fcb661b7d94d000f57beefd10..f3d9ece73b657b9e7684e138a240144ec97e0a51 100644 GIT binary patch delta 43246 zcmce<3t&{m6*qoo?mn}-$qj^O9^736Bs}we075Pm1OY+o0}+jRY=95~BmtyVHV9Ru zRMD$mRM4m>C}2@g@x8uLQL)7q6)d*eQl*L(TdXMG@66oYYyxTRzu)%{nmcpm%?--|D!w&saL-}Go5PJQbHa@^;mVr%P2tMwS;Oj^Z#p@sR~JR*P?kBRi?WQ$$|cpa zD#J}p^P4Kmt4iy_g>~hH1$l)q@G^1?7s`MV(Apry<-pt7Ud&VL`aIysDtIysD-)uehk@^wlCD)<9Wr zLDT$}`7KKqgqy=HHB}7_mDRO1wKX-xWkm(yqT15ZTE*Q($LX?x+WC#)%Hq=M!t!tp zx~{FQ3zy}ezFYJvX`NTOxT;}MxDq2QDJU+kF3K+smzUIZUCq3MXXchym*VbYi~6vm!NxC8hc0<>7+5s)DMjyrM3LWD|W>xTO+3V9tf%l7fQ#y3*nr z#nr{!6Bep4TwO>jRFq#AE~zUjQkbX{Ih8%_N+5J)T~T4UxU{q?Tvc9FR8m~sC6PO= zZgEvhj783%e$Cwp`RX z%|ETG3r}op3C{{QRaVzk*OV3(7F3m&mXy^NRQ)GX68}aDOY#fz%Bl+ri%V+28F@Bc zP9;k}yUUhvQ&kHju%xc8q_`xnrYgUa6%Y^f~DFRLrdt1YW5F3c+|C@k!P7?Jws z#9D<#Wd(JR$D;CZSyf$r*Wzq10MTSlW9_VPW96L2%A(S`s*L0v&{mjkth z8nw+29m%;MM_GAoF*I4YwxBe>T5)#4&*n)-dL%!;tgNiCJg+)0zpSL7G_T8EY^FSS z(Y$JKM;>Ijs5V?$n->Oa71Sz@E;_OG>^Pxk%W8_kMTK?Y(xT$xa>$(Gn%PCQ!ID1` zDbie7J!jUO#+J&${ObII;?m;k;*z?;g1W8+!hFz~ky~6921AvV<<%9`(gu}7 zzn2#jW0xx`fTAz!vU1+d&Rzu#^XE5*E5i#b%WG*@D66S0fqpH^&+n2;%pUG+5V5AN zs3u%mmtRv=T2NKcHCd**`$h5~ybzR{l5k;hURhPwB-azTp{A<21(fLUldpuj;HH09 zGeT_?bUC4aSCwiOD*aT`ag)RAQ(b|8H{kO5TyBpqE6L~fsDb1x?hbf7ZkH#?6>tZF z9;fDZy4i)5qoZp*sHe0qW}QO?P`y#(}_{=#1K-8g3qN`g$Sfb2@$U2gPSZB(4WY zDu&|a7z=(--Q#q6ydDpCd7NHnl1p`S&b_J&?P7`;f!Boxdc{ys0pmkQ;>YC=1c0jo z1vnCB{|4CQQaKIG=j7buc1MzYUJNgp13S&vv%Amd_d9*wB%b8qNnUC;u>lu%su%;R z%AQQB&pUQh?Ahnia{|FYXGtoDLuo5;MQ?o*6m9`cySmH7xyExN47*v)`HugS|BZjazvM^xSNv=KHv2n2#t-w{L2s+As>%Pt z?q^HhRV{uWyHWj<`USh0@8g!boHzbiJ;5L2kvrHn$2;ns!}wnHCdYd9PIaC73%=kl z5Ru=iFYr6rZH^bzP3k$T`RD3i)mt1%s}4JFa=f-;!>?-TcKV&7Ftos5s*=*|GAHUai zpOAzm_Pgs1RhzuyNcVo8WWAs6oDkqTqlaqVnNj#7CLjh>pV`}dK5JN7X3$Sl%`|ex z2fFDFJ*8d`nlERL`R^u3qn-EPO>nbWq>q^c_S1> z_ZV0dkDhK|3Hti*D}>BwOPEd#umI0rqV6 z!~vbDv6EOZy#ou{>l8fgQY6+lC*_XH#%%nj*E;P5P&&)Z+(9ov&HQZ;DVy$t&tsE! zga^-d{iymozRFKxt~Er>Z%kHL5fKt&^JnMP?#Ruj5wWZo0|@pw4Z?qHVPeNVPt!z4 zgbpmHr|TZ#eVWaB(_1uhfMkf%YHqKxAb!9fRvVq!J z<-PilCI*f>hif4qQX-AexY;;jusNyx)^1Kt2ry7{c!pi^>+&-)JPU_U@5b#Y6`yO1UXT6&vGs0iRI7aDT@Jm?{XK#&lZFl-{CvnNLOPbFF_ zuoIT)o+S&ZvbkwQPJyl%p86oQ=_;X)k-)$)8ovwZHtkS{C_vRf|CCWB=`>sq_ORG+ zmGC;P>Iw)dcUa#R_uZuyyuDfh^k z&roju=oK2)5aYla#m2$Mj7hG=tP;AOI;PKHNtNTQoh&X%7J%Y(aY?$EV`fHZ3N#(C z&(r|sLHT_$KM7313`f0j>|m6*>({MFx3*rjPBK?UTZyg%raR67I$ zAnyrz!I&i+Sx^?3Is6l}&A}ghQhEt8eX-85e^h*cipQbR?H`(}Duzdn&`^*E7b*}* zv4$laP+Fdo{Yv;z(X_LKKl35=;Vt1iMFQ)&of3lFSU8d14M{2C`x4;0MS8D*A4tI2 zE7JQTqTrJRr2Qg&NWhOsB#}NO(uYO*SOU&rkv=NmClYXuiu7>-UwMEADsherc6V1_T9IBW;Oi68>qL5;fNxAlw~2I{fNxDmZx-pz2ci*- zVrMLa^mdWnE-E~eklrQIy9E5@gmhG-qXNE1q<4$-z6AJgk=`rd2NH1hA{|kT{i5KL z1f=~UeMrEMh_u8xB+`ck{8$3cVUa#6;3pDrj*9eg0bfaMWJsLj0^SOyFNyFqqCg_G ziu5Wf;J1jhK;o-JdaZ!3Prz9#((43#V*<`Pk!}<4tqC}7BE4C_cZzgGB5f9#?V{kB z1f=aEy-UDfPQckkX(+ptdQv%xabIJfeIZEuP#L$(wy4RQvA+3iZ3_Wv&EQM0wRm6h!m3_F384B#k7qLPkkIb=O1)HL}Qf>w|6*;v3I8 z6M|^Ieb!m&qJP5c(5Hj?G>4AMLd)iqaoHNgHkbtMsKKtw<-0yKmyhe2*@blLSP5Y4 z9GA_w`Tn?q^K1QTf<~+eVIt5ww0F4V-r?3=_TB-(ifc(F=q4>GK%kdY(b(2VMPD|4 z02*I8{_Jj;!1#azx)YOz(jJfP?%?=zHq-oeyn&wG9eU>ISxxMj$9q;2dRBv+dKRFN zYWK{MN6%}{9uR?I6&qf?TmxH0D)!Q1w-c)-Neitym&QhG2*u0AnnDp$9`m->6j-M& zDV8!Q78AJ8&+3!_G&vX{Q=Ln!1E^eUbQHo~V0UUpCT8@9`2$n{{}ZOy?*Jx|&iwA| zK62YLuz=X;`b`+1VHXlzqH(bqn-@;V8Lx`AW4vq64H&AN6>Up`Q$a%-guRJVFKCX& z2}HC%cw%M_b|~;vHn4n~`>1^*e3|>IGZbU32jd2m8~fFIUC}m}FHOv2ZRW9wRqPt` zqH{hUvgan)aUXDOLYi?vreUg(seu~WlN_5c(tDM9?a$1dKWSRTU}pzZo#*h$^y`2c zQl?Pecv+zieqi8HDwrTpwC^cTdmv?>agvy|UO67Up?z1qo~(V!Xm-2TMwOt$aFK09 zm^+=J?)2Q{45f)e!=)X^AbG_VdUrj|cpLce&?N{}2=)%aoB}5D`zWs>Pqxf4*?}{t zVm!vDGUpD8iR_LKiyPmrHSeC>C-N=T^-x>fSPrUSY^W-lrL63R+Z|s^5IH^rP1>ys zWSW;K8W%vfn4UdTVcH0c-?w8Z#t}I#&u|PACLImaScBFD=0VEQn9?yC^h0!`Lc_et ztecX{j+$$yWM6n_Js?i)dsCt;j^gcnb0~lN%BAe&NnhmFIUc+J&!(t5D-_7!={hcyZ zGj-tLop-{OfXlP%o=`{+-K!%;Wjxjq)~4$KK7Ob9*i=1jJC)JsyX$H?2H-HZ-f8}I zY5^#nF|9oE6;@QRbFc^erC?9uTw-eD6D*1pDVaagDSu4ngFPYD!S1N$H(GCwSIOZ~ zhvdtb9U%vm3o<*BxvG#Jz(7O;Vq6mZ^re$R;0t}41ulZ|25JV)=X zJLZOZ>Afd3!;Xcr6a!Sz;yCOjG^%(z6cD1`A*o_4-HoIMR7S6g&mJIZ2o%f%Blfh9 zup9FB5J}6@5Y4}z*MF)+7XUFs2O3u)4S|aEv@XJdfas)DahUu%j}4n9_%- zd@tmnwKWQU7xMC%5Vk$8U~d50PzIaC0ax2YJVq`i*L}HMDOKCt`XtHby`kdu6o^T# z3OF@HvZd9B(g4*uLeW|;Ks|A&o+4^(Hy{4#MeI}4dBMQ6?e0?a`214th=pT3|E`<40L1sl&+_q$5f>Ch!S7dWN7 z7L+`K9iWdr7b;-appVr=v88(9zFwV4x zRNbX*77K?;hp-EMEA}7{wSiqmd)X|%D1G=bw1E9h?0G2=d9gpL#wTsicY{%c?aSCl znO!7q@0lwu8aUvP3N9ksF{5644JHdc+encj3Dt#&uI|*1n0qgJnKhZWUOczw7Vb|W zZgi5@_9}$b;M!f>>^VJ)tul+JmwR^;OeNTo>HW+eGrODHFL9gOr>C&ZW^{Vc`zDn; zsQk0({n#(eq)X;{-yxWbV9PG)$8Iq1x?~dj+WhR2GVgm-l0+qgXY>Q5C(W45R+#H% zjPU*;-p+?J`kA*)?`{Sz?Zvj4{VyHl{S%e>sMhpL`?2fHD=*FVei4V=FJP}+n&bVJ zV9A8{k4yWR|C-U)96mG19x~6HInaBZi82qBEuYzs|57#Yof-1p7ze&3fnNY<#esb; z6PP0|3wiG*pp&YHCGgtI26;CU&`ZE4FOy@wY=%1(WY6J;)Sw2MAK>-cHdDQPsK=)5 zgJ#*~g)yRT;AYF^<6AmpN!w_w;RqN!$d?q)wRA zXA$P|S%cD_NNCU73G3BaxxhLKtlMvG?-e1k4!su7CalWYgVLX)wup<+BSL2#ur>;; zw}JI)0v0@g4p`miP+Mot8I=AuVG&EAEyN%@U|l1yw$2%x{#(K#Spt@$6V}%PE2Dl` z`ul`M5&|r-9&{#W(<}^)wAs87Sf3CU@hh;PaXR$4UtsaMC6UcgPsSQn4;6((Yb
GZ(V?UBHfn3fZwak4LPzEX#~)3 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 delta 44355 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 diff --git a/sentience/extension/release.json b/sentience/extension/release.json index 291d211..89b5c5a 100644 --- a/sentience/extension/release.json +++ b/sentience/extension/release.json @@ -1,9 +1,9 @@ { - "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/277802850", - "assets_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/277802850/assets", - "upload_url": "https://uploads.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/277802850/assets{?name,label}", - "html_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/tag/v2.7.0", - "id": 277802850, + "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/279228428", + "assets_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/279228428/assets", + "upload_url": "https://uploads.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/279228428/assets{?name,label}", + "html_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/tag/v2.8.0", + "id": 279228428, "author": { "login": "rcholic", "id": 135060, @@ -25,21 +25,21 @@ "user_view_type": "public", "site_admin": false }, - "node_id": "RE_kwDOQshiJ84Qju9i", - "tag_name": "v2.7.0", + "node_id": "RE_kwDOQshiJ84QpLAM", + "tag_name": "v2.8.0", "target_commitish": "main", - "name": "Release v2.7.0", + "name": "Release v2.8.0", "draft": false, "immutable": false, "prerelease": false, - "created_at": "2026-01-19T05:08:35Z", - "updated_at": "2026-01-19T05:09:41Z", - "published_at": "2026-01-19T05:09:31Z", + "created_at": "2026-01-23T03:51:24Z", + "updated_at": "2026-01-23T03:52:39Z", + "published_at": "2026-01-23T03:52:16Z", "assets": [ { - "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/assets/342599449", - "id": 342599449, - "node_id": "RA_kwDOQshiJ84Ua6cZ", + "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/assets/344613562", + "id": 344613562, + "node_id": "RA_kwDOQshiJ84UimK6", "name": "extension-files.tar.gz", "label": "", "uploader": { @@ -65,17 +65,17 @@ }, "content_type": "application/gzip", "state": "uploaded", - "size": 79222, - "digest": "sha256:e0cad96e20e539d62a4777f1c5baedb1c8bd02cd26a8e38f7c1b9c804325f068", - "download_count": 6, - "created_at": "2026-01-19T05:09:41Z", - "updated_at": "2026-01-19T05:09:41Z", - "browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v2.7.0/extension-files.tar.gz" + "size": 78756, + "digest": "sha256:5835596decbd70f2c8d0d06a6cbd45222a40b4cb1e150612415bae28878a7ecb", + "download_count": 0, + "created_at": "2026-01-23T03:52:39Z", + "updated_at": "2026-01-23T03:52:39Z", + "browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v2.8.0/extension-files.tar.gz" }, { - "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/assets/342599448", - "id": 342599448, - "node_id": "RA_kwDOQshiJ84Ua6cY", + "url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/assets/344613561", + "id": 344613561, + "node_id": "RA_kwDOQshiJ84UimK5", "name": "extension-package.zip", "label": "", "uploader": { @@ -101,15 +101,15 @@ }, "content_type": "application/zip", "state": "uploaded", - "size": 80692, - "digest": "sha256:83de4c4c54f401fc6404c27ef01a9205cf8e3566e81f62a7bc08f7103a2b0cc5", + "size": 80234, + "digest": "sha256:dcd4d9a8e555d6f48751ddf140fae1d468a170e6160a156e69d03ab2490a1392", "download_count": 0, - "created_at": "2026-01-19T05:09:41Z", - "updated_at": "2026-01-19T05:09:41Z", - "browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v2.7.0/extension-package.zip" + "created_at": "2026-01-23T03:52:39Z", + "updated_at": "2026-01-23T03:52:39Z", + "browser_download_url": "https://github.com/SentienceAPI/Sentience-Geometry-Chrome-Extension/releases/download/v2.8.0/extension-package.zip" } ], - "tarball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/tarball/v2.7.0", - "zipball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/zipball/v2.7.0", + "tarball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/tarball/v2.8.0", + "zipball_url": "https://api.github.com/repos/SentienceAPI/Sentience-Geometry-Chrome-Extension/zipball/v2.8.0", "body": "" } From 0f1f82193089cc8366fd4452a8d9429cfbb5094b Mon Sep 17 00:00:00 2001 From: SentienceDEV Date: Thu, 22 Jan 2026 20:54:17 -0800 Subject: [PATCH 2/2] fix extension path --- sentience/extension/background.js | 310 ++- sentience/extension/content.js | 611 +++-- sentience/extension/injected_api.js | 3845 +++++++++++++++++++-------- 3 files changed, 3375 insertions(+), 1391 deletions(-) diff --git a/sentience/extension/background.js b/sentience/extension/background.js index 2923f55..33e0b13 100644 --- a/sentience/extension/background.js +++ b/sentience/extension/background.js @@ -1,104 +1,242 @@ -import init, { analyze_page_with_options, analyze_page, prune_for_api } from "../pkg/sentience_core.js"; +// Sentience Chrome Extension - Background Service Worker +// Auto-generated from modular source +import init, { analyze_page_with_options, analyze_page, prune_for_api } from './pkg/sentience_core.js'; -let wasmReady = !1, wasmInitPromise = null; +// background.js - Service Worker with WASM (CSP-Immune!) +// This runs in an isolated environment, completely immune to page CSP policies + +console.log('[Sentience Background] Initializing...'); + +// Global WASM initialization state +let wasmReady = false; +let wasmInitPromise = null; + +/** + * Initialize WASM module - called once on service worker startup + * Uses static imports (not dynamic import()) which is required for Service Workers + */ async function initWASM() { - if (!wasmReady) return wasmInitPromise || (wasmInitPromise = (async () => { - try { - globalThis.js_click_element = () => {}, await init(), wasmReady = !0; - } catch (error) { - throw error; - } - })(), wasmInitPromise); -} + if (wasmReady) return; + if (wasmInitPromise) return wasmInitPromise; -async function handleScreenshotCapture(_tabId, options = {}) { + wasmInitPromise = (async () => { try { - const {format: format = "png", quality: quality = 90} = options; - return await chrome.tabs.captureVisibleTab(null, { - format: format, - quality: quality - }); + console.log('[Sentience Background] Loading WASM module...'); + + // Define the js_click_element function that WASM expects + // In Service Workers, use 'globalThis' instead of 'window' + // In background context, we can't actually click, so we log a warning + globalThis.js_click_element = () => { + console.warn('[Sentience Background] js_click_element called in background (ignored)'); + }; + + // Initialize WASM - this calls the init() function from the static import + // The init() function handles fetching and instantiating the .wasm file + await init(); + + wasmReady = true; + console.log('[Sentience Background] ✓ WASM ready!'); + console.log( + '[Sentience Background] Available functions: analyze_page, analyze_page_with_options, prune_for_api' + ); } catch (error) { - throw new Error(`Failed to capture screenshot: ${error.message}`); + console.error('[Sentience Background] WASM initialization failed:', error); + throw error; } + })(); + + return wasmInitPromise; } +// Initialize WASM on service worker startup +initWASM().catch((err) => { + console.error('[Sentience Background] Failed to initialize WASM:', err); +}); + +/** + * Message handler for all extension communication + * Includes global error handling to prevent extension crashes + */ +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + // Global error handler to prevent extension crashes + try { + // Handle screenshot requests (existing functionality) + if (request.action === 'captureScreenshot') { + handleScreenshotCapture(sender.tab.id, request.options) + .then((screenshot) => { + sendResponse({ success: true, screenshot }); + }) + .catch((error) => { + console.error('[Sentience Background] Screenshot capture failed:', error); + sendResponse({ + success: false, + error: error.message || 'Screenshot capture failed', + }); + }); + return true; // Async response + } + + // Handle WASM processing requests (NEW!) + if (request.action === 'processSnapshot') { + handleSnapshotProcessing(request.rawData, request.options) + .then((result) => { + sendResponse({ success: true, result }); + }) + .catch((error) => { + console.error('[Sentience Background] Snapshot processing failed:', error); + sendResponse({ + success: false, + error: error.message || 'Snapshot processing failed', + }); + }); + return true; // Async response + } + + // Unknown action + console.warn('[Sentience Background] Unknown action:', request.action); + sendResponse({ success: false, error: 'Unknown action' }); + return false; + } catch (error) { + // Catch any synchronous errors that might crash the extension + console.error('[Sentience Background] Fatal error in message handler:', error); + try { + sendResponse({ + success: false, + error: `Fatal error: ${error.message || 'Unknown error'}`, + }); + } catch (e) { + // If sendResponse already called, ignore + } + return false; + } +}); + +/** + * Handle screenshot capture (existing functionality) + */ +async function handleScreenshotCapture(_tabId, options = {}) { + try { + const { format = 'png', quality = 90 } = options; + + const dataUrl = await chrome.tabs.captureVisibleTab(null, { + format, + quality, + }); + + console.log( + `[Sentience Background] Screenshot captured: ${format}, size: ${dataUrl.length} bytes` + ); + return dataUrl; + } catch (error) { + console.error('[Sentience Background] Screenshot error:', error); + throw new Error(`Failed to capture screenshot: ${error.message}`); + } +} + +/** + * Handle snapshot processing with WASM (NEW!) + * This is where the magic happens - completely CSP-immune! + * Includes safeguards to prevent crashes and hangs. + * + * @param {Array} rawData - Raw element data from injected_api.js + * @param {Object} options - Snapshot options (limit, filter, etc.) + * @returns {Promise} Processed snapshot result + */ async function handleSnapshotProcessing(rawData, options = {}) { - const startTime = performance.now(); + const MAX_ELEMENTS = 10000; // Safety limit to prevent hangs + const startTime = performance.now(); + + try { + // Safety check: limit element count to prevent hangs + if (!Array.isArray(rawData)) { + throw new Error('rawData must be an array'); + } + + if (rawData.length > MAX_ELEMENTS) { + console.warn( + `[Sentience Background] ⚠️ Large dataset: ${rawData.length} elements. Limiting to ${MAX_ELEMENTS} to prevent hangs.` + ); + rawData = rawData.slice(0, MAX_ELEMENTS); + } + + // Ensure WASM is initialized + await initWASM(); + if (!wasmReady) { + throw new Error('WASM module not initialized'); + } + + console.log( + `[Sentience Background] Processing ${rawData.length} elements with options:`, + options + ); + + // Run WASM processing using the imported functions directly + // Wrap in try-catch with timeout protection + let analyzedElements; try { - if (!Array.isArray(rawData)) throw new Error("rawData must be an array"); - if (rawData.length > 1e4 && (rawData = rawData.slice(0, 1e4)), await initWASM(), - !wasmReady) throw new Error("WASM module not initialized"); - let analyzedElements, prunedRawData; - try { - const wasmPromise = new Promise((resolve, reject) => { - try { - let result; - result = options.limit || options.filter ? analyze_page_with_options(rawData, options) : analyze_page(rawData), - resolve(result); - } catch (e) { - reject(e); - } - }); - analyzedElements = await Promise.race([ wasmPromise, new Promise((_, reject) => setTimeout(() => reject(new Error("WASM processing timeout (>18s)")), 18e3)) ]); - } catch (e) { - const errorMsg = e.message || "Unknown WASM error"; - throw new Error(`WASM analyze_page failed: ${errorMsg}`); - } + // Use a timeout wrapper to prevent infinite hangs + const wasmPromise = new Promise((resolve, reject) => { try { - prunedRawData = prune_for_api(rawData); + let result; + if (options.limit || options.filter) { + result = analyze_page_with_options(rawData, options); + } else { + result = analyze_page(rawData); + } + resolve(result); } catch (e) { - prunedRawData = rawData; + reject(e); } - performance.now(); - return { - elements: analyzedElements, - raw_elements: prunedRawData - }; - } catch (error) { - performance.now(); - throw error; + }); + + // Add timeout protection (18 seconds - less than content.js timeout) + analyzedElements = await Promise.race([ + wasmPromise, + new Promise((_, reject) => + setTimeout(() => reject(new Error('WASM processing timeout (>18s)')), 18000) + ), + ]); + } catch (e) { + const errorMsg = e.message || 'Unknown WASM error'; + console.error(`[Sentience Background] WASM analyze_page failed: ${errorMsg}`, e); + throw new Error(`WASM analyze_page failed: ${errorMsg}`); } -} -initWASM().catch(err => {}), chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + // Prune elements for API (prevents 413 errors on large sites) + let prunedRawData; try { - return "captureScreenshot" === request.action ? (handleScreenshotCapture(sender.tab.id, request.options).then(screenshot => { - sendResponse({ - success: !0, - screenshot: screenshot - }); - }).catch(error => { - sendResponse({ - success: !1, - error: error.message || "Screenshot capture failed" - }); - }), !0) : "processSnapshot" === request.action ? (handleSnapshotProcessing(request.rawData, request.options).then(result => { - sendResponse({ - success: !0, - result: result - }); - }).catch(error => { - sendResponse({ - success: !1, - error: error.message || "Snapshot processing failed" - }); - }), !0) : (sendResponse({ - success: !1, - error: "Unknown action" - }), !1); - } catch (error) { - try { - sendResponse({ - success: !1, - error: `Fatal error: ${error.message || "Unknown error"}` - }); - } catch (e) {} - return !1; + prunedRawData = prune_for_api(rawData); + } catch (e) { + console.warn('[Sentience Background] prune_for_api failed, using original data:', e); + prunedRawData = rawData; } -}), self.addEventListener("error", event => { - event.preventDefault(); -}), self.addEventListener("unhandledrejection", event => { - event.preventDefault(); -}); \ No newline at end of file + + const duration = performance.now() - startTime; + console.log( + `[Sentience Background] ✓ Processed: ${analyzedElements.length} analyzed, ${prunedRawData.length} pruned (${duration.toFixed(1)}ms)` + ); + + return { + elements: analyzedElements, + raw_elements: prunedRawData, + }; + } catch (error) { + const duration = performance.now() - startTime; + console.error(`[Sentience Background] Processing error after ${duration.toFixed(1)}ms:`, error); + throw error; + } +} + +console.log('[Sentience Background] Service worker ready'); + +// Global error handlers to prevent extension crashes +self.addEventListener('error', (event) => { + console.error('[Sentience Background] Global error caught:', event.error); + event.preventDefault(); // Prevent extension crash +}); + +self.addEventListener('unhandledrejection', (event) => { + console.error('[Sentience Background] Unhandled promise rejection:', event.reason); + event.preventDefault(); // Prevent extension crash +}); diff --git a/sentience/extension/content.js b/sentience/extension/content.js index b65cfb5..ee6efa2 100644 --- a/sentience/extension/content.js +++ b/sentience/extension/content.js @@ -1,161 +1,456 @@ -!function() { - "use strict"; - window, window.top; - document.documentElement.dataset.sentienceExtensionId = chrome.runtime.id, window.addEventListener("message", event => { - var data; - if (event.source === window) switch (event.data.type) { - case "SENTIENCE_SCREENSHOT_REQUEST": - data = event.data, chrome.runtime.sendMessage({ - action: "captureScreenshot", - options: data.options - }, response => { - window.postMessage({ - type: "SENTIENCE_SCREENSHOT_RESULT", - requestId: data.requestId, - screenshot: response?.success ? response.screenshot : null, - error: response?.error - }, "*"); - }); - break; - - case "SENTIENCE_SNAPSHOT_REQUEST": - !function(data) { - const startTime = performance.now(); - let responded = !1; - const timeoutId = setTimeout(() => { - if (!responded) { - responded = !0; - const duration = performance.now() - startTime; - window.postMessage({ - type: "SENTIENCE_SNAPSHOT_RESULT", - requestId: data.requestId, - error: "WASM processing timeout - background script may be unresponsive", - duration: duration - }, "*"); - } - }, 2e4); - try { - chrome.runtime.sendMessage({ - action: "processSnapshot", - rawData: data.rawData, - options: data.options - }, response => { - if (responded) return; - responded = !0, clearTimeout(timeoutId); - const duration = performance.now() - startTime; - chrome.runtime.lastError ? window.postMessage({ - type: "SENTIENCE_SNAPSHOT_RESULT", - requestId: data.requestId, - error: `Chrome runtime error: ${chrome.runtime.lastError.message}`, - duration: duration - }, "*") : response?.success ? window.postMessage({ - type: "SENTIENCE_SNAPSHOT_RESULT", - requestId: data.requestId, - elements: response.result.elements, - raw_elements: response.result.raw_elements, - duration: duration - }, "*") : window.postMessage({ - type: "SENTIENCE_SNAPSHOT_RESULT", - requestId: data.requestId, - error: response?.error || "Processing failed", - duration: duration - }, "*"); - }); - } catch (error) { - if (!responded) { - responded = !0, clearTimeout(timeoutId); - const duration = performance.now() - startTime; - window.postMessage({ - type: "SENTIENCE_SNAPSHOT_RESULT", - requestId: data.requestId, - error: `Failed to send message: ${error.message}`, - duration: duration - }, "*"); - } - } - }(event.data); - break; - - case "SENTIENCE_SHOW_OVERLAY": - !function(data) { - const {elements: elements, targetElementId: targetElementId} = data; - if (!elements || !Array.isArray(elements)) return; - removeOverlay(); - const host = document.createElement("div"); - host.id = OVERLAY_HOST_ID, host.style.cssText = "\n position: fixed !important;\n top: 0 !important;\n left: 0 !important;\n width: 100vw !important;\n height: 100vh !important;\n pointer-events: none !important;\n z-index: 2147483647 !important;\n margin: 0 !important;\n padding: 0 !important;\n ", - document.body.appendChild(host); - const shadow = host.attachShadow({ - mode: "closed" - }), maxImportance = Math.max(...elements.map(e => e.importance || 0), 1); - elements.forEach(element => { - const bbox = element.bbox; - if (!bbox) return; - const isTarget = element.id === targetElementId, isPrimary = element.visual_cues?.is_primary || !1, importance = element.importance || 0; - let color; - color = isTarget ? "#FF0000" : isPrimary ? "#0066FF" : "#00FF00"; - const importanceRatio = maxImportance > 0 ? importance / maxImportance : .5, borderOpacity = isTarget ? 1 : isPrimary ? .9 : Math.max(.4, .5 + .5 * importanceRatio), fillOpacity = .2 * borderOpacity, borderWidth = isTarget ? 2 : isPrimary ? 1.5 : Math.max(.5, Math.round(2 * importanceRatio)), hexOpacity = Math.round(255 * fillOpacity).toString(16).padStart(2, "0"), box = document.createElement("div"); - if (box.style.cssText = `\n position: absolute;\n left: ${bbox.x}px;\n top: ${bbox.y}px;\n width: ${bbox.width}px;\n height: ${bbox.height}px;\n border: ${borderWidth}px solid ${color};\n background-color: ${color}${hexOpacity};\n box-sizing: border-box;\n opacity: ${borderOpacity};\n pointer-events: none;\n `, - importance > 0 || isPrimary) { - const badge = document.createElement("span"); - badge.textContent = isPrimary ? `⭐${importance}` : `${importance}`, badge.style.cssText = `\n position: absolute;\n top: -18px;\n left: 0;\n background: ${color};\n color: white;\n font-size: 11px;\n font-weight: bold;\n padding: 2px 6px;\n font-family: Arial, sans-serif;\n border-radius: 3px;\n opacity: 0.95;\n white-space: nowrap;\n pointer-events: none;\n `, - box.appendChild(badge); - } - if (isTarget) { - const targetIndicator = document.createElement("span"); - targetIndicator.textContent = "🎯", targetIndicator.style.cssText = "\n position: absolute;\n top: -18px;\n right: 0;\n font-size: 16px;\n pointer-events: none;\n ", - box.appendChild(targetIndicator); - } - shadow.appendChild(box); - }), overlayTimeout = setTimeout(() => { - removeOverlay(); - }, 5e3); - }(event.data); - break; - - case "SENTIENCE_CLEAR_OVERLAY": - removeOverlay(); - break; - - case "SENTIENCE_SHOW_GRID_OVERLAY": - !function(data) { - const {grids: grids, targetGridId: targetGridId} = data; - if (!grids || !Array.isArray(grids)) return; - removeOverlay(); - const host = document.createElement("div"); - host.id = OVERLAY_HOST_ID, host.style.cssText = "\n position: fixed !important;\n top: 0 !important;\n left: 0 !important;\n width: 100vw !important;\n height: 100vh !important;\n pointer-events: none !important;\n z-index: 2147483647 !important;\n margin: 0 !important;\n padding: 0 !important;\n ", - document.body.appendChild(host); - const shadow = host.attachShadow({ - mode: "closed" - }); - grids.forEach(grid => { - const bbox = grid.bbox; - if (!bbox) return; - const isTarget = grid.grid_id === targetGridId, isDominant = !0 === grid.is_dominant; - let color = "#9B59B6"; - isTarget ? color = "#FF0000" : isDominant && (color = "#FF8C00"); - const borderStyle = isTarget ? "solid" : "dashed", borderWidth = isTarget ? 3 : isDominant ? 2.5 : 2, opacity = isTarget ? 1 : isDominant ? .9 : .8, fillOpacity = .1 * opacity, hexOpacity = Math.round(255 * fillOpacity).toString(16).padStart(2, "0"), box = document.createElement("div"); - box.style.cssText = `\n position: absolute;\n left: ${bbox.x}px;\n top: ${bbox.y}px;\n width: ${bbox.width}px;\n height: ${bbox.height}px;\n border: ${borderWidth}px ${borderStyle} ${color};\n background-color: ${color}${hexOpacity};\n box-sizing: border-box;\n opacity: ${opacity};\n pointer-events: none;\n `; - let labelText = grid.label ? `Grid ${grid.grid_id}: ${grid.label}` : `Grid ${grid.grid_id}`; - grid.is_dominant && (labelText = `⭐ ${labelText} (dominant)`); - const badge = document.createElement("span"); - if (badge.textContent = labelText, badge.style.cssText = `\n position: absolute;\n top: -18px;\n left: 0;\n background: ${color};\n color: white;\n font-size: 11px;\n font-weight: bold;\n padding: 2px 6px;\n font-family: Arial, sans-serif;\n border-radius: 3px;\n opacity: 0.95;\n white-space: nowrap;\n pointer-events: none;\n `, - box.appendChild(badge), isTarget) { - const targetIndicator = document.createElement("span"); - targetIndicator.textContent = "🎯", targetIndicator.style.cssText = "\n position: absolute;\n top: -18px;\n right: 0;\n font-size: 16px;\n pointer-events: none;\n ", - box.appendChild(targetIndicator); - } - shadow.appendChild(box); - }), overlayTimeout = setTimeout(() => { - removeOverlay(); - }, 5e3); - }(event.data); +// Sentience Chrome Extension - Content Script +// Auto-generated from modular source +(function () { + 'use strict'; + + // content.js - ISOLATED WORLD (Bridge between Main World and Background) + console.log('[Sentience Bridge] Loaded.'); + + // Detect if we're in a child frame (for iframe support) + const isChildFrame = window !== window.top; + if (isChildFrame) { + console.log('[Sentience Bridge] Running in child frame:', window.location.href); + } + + // 1. Pass Extension ID to Main World (So API knows where to find resources) + document.documentElement.dataset.sentienceExtensionId = chrome.runtime.id; + + // 2. Message Router - Handles all communication between page and background + window.addEventListener('message', (event) => { + // Security check: only accept messages from same window + if (event.source !== window) return; + + // Route different message types + switch (event.data.type) { + case 'SENTIENCE_SCREENSHOT_REQUEST': + handleScreenshotRequest(event.data); + break; + + case 'SENTIENCE_SNAPSHOT_REQUEST': + handleSnapshotRequest(event.data); + break; + + case 'SENTIENCE_SHOW_OVERLAY': + handleShowOverlay(event.data); + break; + + case 'SENTIENCE_CLEAR_OVERLAY': + handleClearOverlay(); + break; + + case 'SENTIENCE_SHOW_GRID_OVERLAY': + handleShowGridOverlay(event.data); + break; + } + }); + + /** + * Handle screenshot requests (existing functionality) + */ + function handleScreenshotRequest(data) { + chrome.runtime.sendMessage({ action: 'captureScreenshot', options: data.options }, (response) => { + window.postMessage( + { + type: 'SENTIENCE_SCREENSHOT_RESULT', + requestId: data.requestId, + screenshot: response?.success ? response.screenshot : null, + error: response?.error, + }, + '*' + ); + }); + } + + /** + * Handle snapshot processing requests (NEW!) + * Sends raw DOM data to background worker for WASM processing + * Includes timeout protection to prevent extension crashes + */ + function handleSnapshotRequest(data) { + const startTime = performance.now(); + const TIMEOUT_MS = 20000; // 20 seconds (longer than injected_api timeout) + let responded = false; + + // Timeout protection: if background doesn't respond, send error + const timeoutId = setTimeout(() => { + if (!responded) { + responded = true; + const duration = performance.now() - startTime; + console.error(`[Sentience Bridge] ⚠️ WASM processing timeout after ${duration.toFixed(1)}ms`); + window.postMessage( + { + type: 'SENTIENCE_SNAPSHOT_RESULT', + requestId: data.requestId, + error: 'WASM processing timeout - background script may be unresponsive', + duration, + }, + '*' + ); + } + }, TIMEOUT_MS); + + try { + chrome.runtime.sendMessage( + { + action: 'processSnapshot', + rawData: data.rawData, + options: data.options, + }, + (response) => { + if (responded) return; // Already responded via timeout + responded = true; + clearTimeout(timeoutId); + + const duration = performance.now() - startTime; + + // Handle Chrome extension errors (e.g., background script crashed) + if (chrome.runtime.lastError) { + console.error( + '[Sentience Bridge] Chrome runtime error:', + chrome.runtime.lastError.message + ); + window.postMessage( + { + type: 'SENTIENCE_SNAPSHOT_RESULT', + requestId: data.requestId, + error: `Chrome runtime error: ${chrome.runtime.lastError.message}`, + duration, + }, + '*' + ); + return; + } + + if (response?.success) { + console.log(`[Sentience Bridge] ✓ WASM processing complete in ${duration.toFixed(1)}ms`); + window.postMessage( + { + type: 'SENTIENCE_SNAPSHOT_RESULT', + requestId: data.requestId, + elements: response.result.elements, + raw_elements: response.result.raw_elements, + duration, + }, + '*' + ); + } else { + console.error('[Sentience Bridge] WASM processing failed:', response?.error); + window.postMessage( + { + type: 'SENTIENCE_SNAPSHOT_RESULT', + requestId: data.requestId, + error: response?.error || 'Processing failed', + duration, + }, + '*' + ); + } } + ); + } catch (error) { + if (!responded) { + responded = true; + clearTimeout(timeoutId); + const duration = performance.now() - startTime; + console.error('[Sentience Bridge] Exception sending message:', error); + window.postMessage( + { + type: 'SENTIENCE_SNAPSHOT_RESULT', + requestId: data.requestId, + error: `Failed to send message: ${error.message}`, + duration, + }, + '*' + ); + } + } + } + + // ============================================================================ + // Visual Overlay - Shadow DOM Implementation + // ============================================================================ + + const OVERLAY_HOST_ID = 'sentience-overlay-host'; + let overlayTimeout = null; + + /** + * Show visual overlay highlighting elements using Shadow DOM + * @param {Object} data - Message data with elements and targetElementId + */ + function handleShowOverlay(data) { + const { elements, targetElementId } = data; + + if (!elements || !Array.isArray(elements)) { + console.warn('[Sentience Bridge] showOverlay: elements must be an array'); + return; + } + + removeOverlay(); + + // Create host with Shadow DOM for CSS isolation + const host = document.createElement('div'); + host.id = OVERLAY_HOST_ID; + host.style.cssText = ` + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100vw !important; + height: 100vh !important; + pointer-events: none !important; + z-index: 2147483647 !important; + margin: 0 !important; + padding: 0 !important; + `; + document.body.appendChild(host); + + // Attach shadow root (closed mode for security and CSS isolation) + const shadow = host.attachShadow({ mode: 'closed' }); + + // Calculate max importance for scaling + const maxImportance = Math.max(...elements.map((e) => e.importance || 0), 1); + + elements.forEach((element) => { + const bbox = element.bbox; + if (!bbox) return; + + const isTarget = element.id === targetElementId; + const isPrimary = element.visual_cues?.is_primary || false; + const importance = element.importance || 0; + + // Color: Red (target), Blue (primary), Green (regular) + let color; + if (isTarget) color = '#FF0000'; + else if (isPrimary) color = '#0066FF'; + else color = '#00FF00'; + + // Scale opacity and border width based on importance + const importanceRatio = maxImportance > 0 ? importance / maxImportance : 0.5; + const borderOpacity = isTarget + ? 1.0 + : isPrimary + ? 0.9 + : Math.max(0.4, 0.5 + importanceRatio * 0.5); + const fillOpacity = borderOpacity * 0.2; + const borderWidth = isTarget + ? 2 + : isPrimary + ? 1.5 + : Math.max(0.5, Math.round(importanceRatio * 2)); + + // Convert fill opacity to hex for background-color + const hexOpacity = Math.round(fillOpacity * 255) + .toString(16) + .padStart(2, '0'); + + // Create box with semi-transparent fill + const box = document.createElement('div'); + box.style.cssText = ` + position: absolute; + left: ${bbox.x}px; + top: ${bbox.y}px; + width: ${bbox.width}px; + height: ${bbox.height}px; + border: ${borderWidth}px solid ${color}; + background-color: ${color}${hexOpacity}; + box-sizing: border-box; + opacity: ${borderOpacity}; + pointer-events: none; + `; + + // Add badge showing importance score + if (importance > 0 || isPrimary) { + const badge = document.createElement('span'); + badge.textContent = isPrimary ? `⭐${importance}` : `${importance}`; + badge.style.cssText = ` + position: absolute; + top: -18px; + left: 0; + background: ${color}; + color: white; + font-size: 11px; + font-weight: bold; + padding: 2px 6px; + font-family: Arial, sans-serif; + border-radius: 3px; + opacity: 0.95; + white-space: nowrap; + pointer-events: none; + `; + box.appendChild(badge); + } + + // Add target emoji for target element + if (isTarget) { + const targetIndicator = document.createElement('span'); + targetIndicator.textContent = '🎯'; + targetIndicator.style.cssText = ` + position: absolute; + top: -18px; + right: 0; + font-size: 16px; + pointer-events: none; + `; + box.appendChild(targetIndicator); + } + + shadow.appendChild(box); }); - const OVERLAY_HOST_ID = "sentience-overlay-host"; - let overlayTimeout = null; - function removeOverlay() { - const existing = document.getElementById(OVERLAY_HOST_ID); - existing && existing.remove(), overlayTimeout && (clearTimeout(overlayTimeout), - overlayTimeout = null); + + console.log(`[Sentience Bridge] Overlay shown for ${elements.length} elements`); + + // Auto-remove after 5 seconds + overlayTimeout = setTimeout(() => { + removeOverlay(); + console.log('[Sentience Bridge] Overlay auto-cleared after 5 seconds'); + }, 5000); + } + + /** + * Show grid overlay highlighting detected grids + * @param {Object} data - Message data with grids and targetGridId + */ + function handleShowGridOverlay(data) { + const { grids, targetGridId } = data; + + if (!grids || !Array.isArray(grids)) { + console.warn('[Sentience Bridge] showGridOverlay: grids must be an array'); + return; } -}(); \ No newline at end of file + + removeOverlay(); + + // Create host with Shadow DOM for CSS isolation + const host = document.createElement('div'); + host.id = OVERLAY_HOST_ID; + host.style.cssText = ` + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100vw !important; + height: 100vh !important; + pointer-events: none !important; + z-index: 2147483647 !important; + margin: 0 !important; + padding: 0 !important; + `; + document.body.appendChild(host); + + // Attach shadow root (closed mode for security and CSS isolation) + const shadow = host.attachShadow({ mode: 'closed' }); + + grids.forEach((grid) => { + const bbox = grid.bbox; + if (!bbox) return; + + const isTarget = grid.grid_id === targetGridId; + const isDominant = grid.is_dominant === true; + + // Grid colors: Red for target, Orange for dominant, Purple for regular + let color = '#9B59B6'; // Purple (default) + if (isTarget) { + color = '#FF0000'; // Red for target + } else if (isDominant) { + color = '#FF8C00'; // Orange for dominant group + } + const borderStyle = isTarget ? 'solid' : 'dashed'; // Dashed for grids + const borderWidth = isTarget ? 3 : isDominant ? 2.5 : 2; + const opacity = isTarget ? 1.0 : isDominant ? 0.9 : 0.8; + const fillOpacity = opacity * 0.1; // 10% opacity for grids (lighter than elements) + + // Convert fill opacity to hex for background-color + const hexOpacity = Math.round(fillOpacity * 255) + .toString(16) + .padStart(2, '0'); + + // Create grid box + const box = document.createElement('div'); + box.style.cssText = ` + position: absolute; + left: ${bbox.x}px; + top: ${bbox.y}px; + width: ${bbox.width}px; + height: ${bbox.height}px; + border: ${borderWidth}px ${borderStyle} ${color}; + background-color: ${color}${hexOpacity}; + box-sizing: border-box; + opacity: ${opacity}; + pointer-events: none; + `; + + // Add badge with grid_id and label + let labelText = grid.label ? `Grid ${grid.grid_id}: ${grid.label}` : `Grid ${grid.grid_id}`; + // Add dominant indicator if this is the dominant group + if (grid.is_dominant) { + labelText = `⭐ ${labelText} (dominant)`; + } + const badge = document.createElement('span'); + badge.textContent = labelText; + badge.style.cssText = ` + position: absolute; + top: -18px; + left: 0; + background: ${color}; + color: white; + font-size: 11px; + font-weight: bold; + padding: 2px 6px; + font-family: Arial, sans-serif; + border-radius: 3px; + opacity: 0.95; + white-space: nowrap; + pointer-events: none; + `; + box.appendChild(badge); + + // Add target indicator if target + if (isTarget) { + const targetIndicator = document.createElement('span'); + targetIndicator.textContent = '🎯'; + targetIndicator.style.cssText = ` + position: absolute; + top: -18px; + right: 0; + font-size: 16px; + pointer-events: none; + `; + box.appendChild(targetIndicator); + } + + shadow.appendChild(box); + }); + + console.log(`[Sentience Bridge] Grid overlay shown for ${grids.length} grids`); + + // Auto-remove after 5 seconds + overlayTimeout = setTimeout(() => { + removeOverlay(); + console.log('[Sentience Bridge] Grid overlay auto-cleared after 5 seconds'); + }, 5000); + } + + /** + * Clear overlay manually + */ + function handleClearOverlay() { + removeOverlay(); + console.log('[Sentience Bridge] Overlay cleared manually'); + } + + /** + * Remove overlay from DOM + */ + function removeOverlay() { + const existing = document.getElementById(OVERLAY_HOST_ID); + if (existing) { + existing.remove(); + } + + if (overlayTimeout) { + clearTimeout(overlayTimeout); + overlayTimeout = null; + } + } + + // console.log('[Sentience Bridge] Ready - Extension ID:', chrome.runtime.id); + +})(); diff --git a/sentience/extension/injected_api.js b/sentience/extension/injected_api.js index da5363b..e6ff2f9 100644 --- a/sentience/extension/injected_api.js +++ b/sentience/extension/injected_api.js @@ -1,1198 +1,2749 @@ -!function() { - "use strict"; - function getAllElements(root = document) { - const elements = [], filter = { - acceptNode: node => [ "SCRIPT", "STYLE", "NOSCRIPT", "META", "LINK", "HEAD" ].includes(node.tagName) || node.parentNode && "SVG" === node.parentNode.tagName && "SVG" !== node.tagName ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT - }, walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filter); - for (;walker.nextNode(); ) { - const node = walker.currentNode; - node.isConnected && (elements.push(node), node.shadowRoot && elements.push(...getAllElements(node.shadowRoot))); +// Sentience Chrome Extension - Injected API +// Auto-generated from modular source +(function () { + 'use strict'; + + // utils.js - Helper Functions (CSP-Resistant) + // All utility functions needed for DOM data collection + + // --- HELPER: Deep Walker with Native Filter --- + function getAllElements(root = document) { + const elements = []; + const filter = { + acceptNode(node) { + // Skip metadata and script/style tags + if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'META', 'LINK', 'HEAD'].includes(node.tagName)) { + return NodeFilter.FILTER_REJECT; } - return elements; - } - const CAPTCHA_TEXT_KEYWORDS = [ "verify you are human", "captcha", "human verification", "unusual traffic", "are you a robot", "security check", "prove you are human", "bot detection", "automated access" ], CAPTCHA_URL_HINTS = [ "captcha", "challenge", "verify" ], CAPTCHA_IFRAME_HINTS = { - recaptcha: [ "recaptcha", "google.com/recaptcha" ], - hcaptcha: [ "hcaptcha.com" ], - turnstile: [ "challenges.cloudflare.com", "turnstile" ], - arkose: [ "arkoselabs.com", "funcaptcha.com", "client-api.arkoselabs.com" ], - awswaf: [ "amazonaws.com/captcha", "awswaf.com" ] - }, CAPTCHA_SCRIPT_HINTS = { - recaptcha: [ "recaptcha" ], - hcaptcha: [ "hcaptcha" ], - turnstile: [ "turnstile", "challenges.cloudflare.com" ], - arkose: [ "arkoselabs", "funcaptcha" ], - awswaf: [ "captcha.awswaf", "awswaf-captcha" ] - }, CAPTCHA_CONTAINER_SELECTORS = [ { - selector: ".g-recaptcha", - provider: "recaptcha" - }, { - selector: "#g-recaptcha", - provider: "recaptcha" - }, { - selector: "[data-sitekey]", - provider: "unknown" - }, { - selector: 'iframe[title*="recaptcha" i]', - provider: "recaptcha" - }, { - selector: ".h-captcha", - provider: "hcaptcha" - }, { - selector: "#h-captcha", - provider: "hcaptcha" - }, { - selector: 'iframe[title*="hcaptcha" i]', - provider: "hcaptcha" - }, { - selector: ".cf-turnstile", - provider: "turnstile" - }, { - selector: "[data-cf-turnstile-sitekey]", - provider: "turnstile" - }, { - selector: 'iframe[src*="challenges.cloudflare.com"]', - provider: "turnstile" - }, { - selector: "#FunCaptcha", - provider: "arkose" - }, { - selector: ".funcaptcha", - provider: "arkose" - }, { - selector: "[data-arkose-public-key]", - provider: "arkose" - }, { - selector: 'iframe[src*="arkoselabs"]', - provider: "arkose" - }, { - selector: "#captcha-container", - provider: "awswaf" - }, { - selector: "[data-awswaf-captcha]", - provider: "awswaf" - }, { - selector: 'iframe[title*="captcha" i]', - provider: "unknown" - } ]; - function addEvidence(list, value) { - value && (list.length >= 5 || list.push(value)); - } - function truncateText(text, maxLen) { - return text ? text.length <= maxLen ? text : text.slice(0, maxLen) : ""; - } - function matchHints(value, hints) { - const lower = String(value || "").toLowerCase(); - return !!lower && hints.some(hint => lower.includes(hint)); - } - function detectCaptcha() { - const evidence = { - text_hits: [], - selector_hits: [], - iframe_src_hits: [], - url_hits: [] - }; - let hasIframeHit = !1, hasContainerHit = !1, hasScriptHit = !1, hasKeywordHit = !1, hasUrlHit = !1; - const providerSignals = { - recaptcha: 0, - hcaptcha: 0, - turnstile: 0, - arkose: 0, - awswaf: 0 - }; - try { - const iframes = document.querySelectorAll("iframe"); - for (const iframe of iframes) { - const src = iframe.getAttribute("src") || "", title = iframe.getAttribute("title") || ""; - if (src) for (const [provider, hints] of Object.entries(CAPTCHA_IFRAME_HINTS)) matchHints(src, hints) && (hasIframeHit = !0, - providerSignals[provider] += 1, addEvidence(evidence.iframe_src_hits, truncateText(src, 120))); - if (title && matchHints(title, [ "captcha", "recaptcha" ]) && (hasContainerHit = !0, - addEvidence(evidence.selector_hits, 'iframe[title*="captcha"]')), evidence.iframe_src_hits.length >= 5) break; - } - } catch (e) {} - try { - const scripts = document.querySelectorAll("script[src]"); - for (const script of scripts) { - const src = script.getAttribute("src") || ""; - if (src) { - for (const [provider, hints] of Object.entries(CAPTCHA_SCRIPT_HINTS)) matchHints(src, hints) && (hasScriptHit = !0, - providerSignals[provider] += 1, addEvidence(evidence.selector_hits, `script[src*="${hints[0]}"]`)); - if (evidence.selector_hits.length >= 5) break; - } - } - } catch (e) {} - for (const {selector: selector, provider: provider} of CAPTCHA_CONTAINER_SELECTORS) try { - document.querySelector(selector) && (hasContainerHit = !0, addEvidence(evidence.selector_hits, selector), - "unknown" !== provider && (providerSignals[provider] += 1)); - } catch (e) {} - const textSnippet = function() { - try { - const candidates = document.querySelectorAll("h1, h2, h3, h4, p, label, button, form, div, span"); - let combined = "", count = 0; - for (const node of candidates) { - if (count >= 30 || combined.length >= 2e3) break; - if (!node || "string" != typeof node.innerText) continue; - if (!node.offsetWidth && !node.offsetHeight && !node.getClientRects().length) continue; - const text = node.innerText.replace(/\s+/g, " ").trim(); - text && (combined += `${text} `, count += 1); - } - if (combined = combined.trim(), combined) return truncateText(combined, 2e3); - } catch (e) {} - try { - let bodyText = document.body?.innerText || ""; - return !bodyText && document.body?.textContent && (bodyText = document.body.textContent), - truncateText(bodyText.replace(/\s+/g, " ").trim(), 2e3); - } catch (e) { - return ""; + // Skip deep SVG children + if (node.parentNode && node.parentNode.tagName === 'SVG' && node.tagName !== 'SVG') { + return NodeFilter.FILTER_REJECT; + } + return NodeFilter.FILTER_ACCEPT; + }, + }; + + const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filter); + while (walker.nextNode()) { + const node = walker.currentNode; + if (node.isConnected) { + elements.push(node); + if (node.shadowRoot) elements.push(...getAllElements(node.shadowRoot)); + } + } + return elements; + } + + // ============================================================================ + // CAPTCHA DETECTION (detection only, no solving/bypass logic) + // ============================================================================ + + const CAPTCHA_DETECTED_THRESHOLD = 0.7; + const CAPTCHA_MAX_EVIDENCE = 5; + const CAPTCHA_TEXT_MAX_LEN = 2000; + + const CAPTCHA_TEXT_KEYWORDS = [ + 'verify you are human', + 'captcha', + 'human verification', + 'unusual traffic', + 'are you a robot', + 'security check', + 'prove you are human', + 'bot detection', + 'automated access', + ]; + + const CAPTCHA_URL_HINTS = ['captcha', 'challenge', 'verify']; + + const CAPTCHA_IFRAME_HINTS = { + recaptcha: ['recaptcha', 'google.com/recaptcha'], + hcaptcha: ['hcaptcha.com'], + turnstile: ['challenges.cloudflare.com', 'turnstile'], + arkose: ['arkoselabs.com', 'funcaptcha.com', 'client-api.arkoselabs.com'], + awswaf: ['amazonaws.com/captcha', 'awswaf.com'], + }; + + const CAPTCHA_SCRIPT_HINTS = { + recaptcha: ['recaptcha'], + hcaptcha: ['hcaptcha'], + turnstile: ['turnstile', 'challenges.cloudflare.com'], + arkose: ['arkoselabs', 'funcaptcha'], + awswaf: ['captcha.awswaf', 'awswaf-captcha'], + }; + + const CAPTCHA_CONTAINER_SELECTORS = [ + // reCAPTCHA + { selector: '.g-recaptcha', provider: 'recaptcha' }, + { selector: '#g-recaptcha', provider: 'recaptcha' }, + { selector: '[data-sitekey]', provider: 'unknown' }, + { selector: 'iframe[title*="recaptcha" i]', provider: 'recaptcha' }, + // hCaptcha + { selector: '.h-captcha', provider: 'hcaptcha' }, + { selector: '#h-captcha', provider: 'hcaptcha' }, + { selector: 'iframe[title*="hcaptcha" i]', provider: 'hcaptcha' }, + // Cloudflare Turnstile + { selector: '.cf-turnstile', provider: 'turnstile' }, + { selector: '[data-cf-turnstile-sitekey]', provider: 'turnstile' }, + { selector: 'iframe[src*="challenges.cloudflare.com"]', provider: 'turnstile' }, + // Arkose Labs / FunCaptcha + { selector: '#FunCaptcha', provider: 'arkose' }, + { selector: '.funcaptcha', provider: 'arkose' }, + { selector: '[data-arkose-public-key]', provider: 'arkose' }, + { selector: 'iframe[src*="arkoselabs"]', provider: 'arkose' }, + // AWS WAF CAPTCHA + { selector: '#captcha-container', provider: 'awswaf' }, + { selector: '[data-awswaf-captcha]', provider: 'awswaf' }, + // Generic + { selector: 'iframe[title*="captcha" i]', provider: 'unknown' }, + ]; + + function addEvidence(list, value) { + if (!value) return; + if (list.length >= CAPTCHA_MAX_EVIDENCE) return; + list.push(value); + } + + function truncateText(text, maxLen) { + if (!text) return ''; + if (text.length <= maxLen) return text; + return text.slice(0, maxLen); + } + + function collectVisibleTextSnippet() { + try { + const candidates = document.querySelectorAll( + 'h1, h2, h3, h4, p, label, button, form, div, span' + ); + let combined = ''; + let count = 0; + for (const node of candidates) { + if (count >= 30 || combined.length >= CAPTCHA_TEXT_MAX_LEN) break; + if (!node || typeof node.innerText !== 'string') continue; + if (!node.offsetWidth && !node.offsetHeight && !node.getClientRects().length) continue; + const text = node.innerText.replace(/\s+/g, ' ').trim(); + if (!text) continue; + combined += `${text} `; + count += 1; + } + combined = combined.trim(); + if (combined) { + return truncateText(combined, CAPTCHA_TEXT_MAX_LEN); + } + } catch (e) { + // ignore + } + + try { + let bodyText = document.body?.innerText || ''; + if (!bodyText && document.body?.textContent) { + bodyText = document.body.textContent; + } + return truncateText(bodyText.replace(/\s+/g, ' ').trim(), CAPTCHA_TEXT_MAX_LEN); + } catch (e) { + return ''; + } + } + + function matchHints(value, hints) { + const lower = String(value || '').toLowerCase(); + if (!lower) return false; + return hints.some((hint) => lower.includes(hint)); + } + + function detectCaptcha() { + const evidence = { + text_hits: [], + selector_hits: [], + iframe_src_hits: [], + url_hits: [], + }; + + let hasIframeHit = false; + let hasContainerHit = false; + let hasScriptHit = false; + let hasKeywordHit = false; + let hasUrlHit = false; + + const providerSignals = { + recaptcha: 0, + hcaptcha: 0, + turnstile: 0, + arkose: 0, + awswaf: 0, + }; + + // Iframe hints (strongest signal) + try { + const iframes = document.querySelectorAll('iframe'); + for (const iframe of iframes) { + const src = iframe.getAttribute('src') || ''; + const title = iframe.getAttribute('title') || ''; + if (src) { + for (const [provider, hints] of Object.entries(CAPTCHA_IFRAME_HINTS)) { + if (matchHints(src, hints)) { + hasIframeHit = true; + providerSignals[provider] += 1; + addEvidence(evidence.iframe_src_hits, truncateText(src, 120)); } - }(); - if (textSnippet) { - const lowerText = textSnippet.toLowerCase(); - for (const keyword of CAPTCHA_TEXT_KEYWORDS) lowerText.includes(keyword) && (hasKeywordHit = !0, - addEvidence(evidence.text_hits, keyword)); + } } - try { - const lowerUrl = (window.location?.href || "").toLowerCase(); - for (const hint of CAPTCHA_URL_HINTS) lowerUrl.includes(hint) && (hasUrlHit = !0, - addEvidence(evidence.url_hits, hint)); - } catch (e) {} - let confidence = 0; - hasIframeHit && (confidence += .7), hasContainerHit && (confidence += .5), hasScriptHit && (confidence += .5), - hasKeywordHit && (confidence += .3), hasUrlHit && (confidence += .2), confidence = Math.min(1, confidence), - hasIframeHit && (confidence = Math.max(confidence, .8)), !hasKeywordHit || hasIframeHit || hasContainerHit || hasScriptHit || hasUrlHit || (confidence = Math.min(confidence, .4)); - const detected = confidence >= .7; - let providerHint = null; - return providerSignals.recaptcha > 0 ? providerHint = "recaptcha" : providerSignals.hcaptcha > 0 ? providerHint = "hcaptcha" : providerSignals.turnstile > 0 ? providerHint = "turnstile" : providerSignals.arkose > 0 ? providerHint = "arkose" : providerSignals.awswaf > 0 ? providerHint = "awswaf" : detected && (providerHint = "unknown"), - { - detected: detected, - provider_hint: providerHint, - confidence: confidence, - evidence: evidence - }; + if (title && matchHints(title, ['captcha', 'recaptcha'])) { + hasContainerHit = true; + addEvidence(evidence.selector_hits, 'iframe[title*="captcha"]'); + } + if (evidence.iframe_src_hits.length >= CAPTCHA_MAX_EVIDENCE) break; + } + } catch (e) { + // ignore } - const DEFAULT_INFERENCE_CONFIG = { - allowedTags: [ "label", "span", "div" ], - allowedRoles: [], - allowedClassPatterns: [], - maxParentDepth: 2, - maxSiblingDistance: 1, - requireSameContainer: !0, - containerTags: [ "form", "fieldset", "div" ], - methods: { - explicitLabel: !0, - ariaLabelledby: !0, - parentTraversal: !0, - siblingProximity: !0 + + // Script hints + try { + const scripts = document.querySelectorAll('script[src]'); + for (const script of scripts) { + const src = script.getAttribute('src') || ''; + if (!src) continue; + for (const [provider, hints] of Object.entries(CAPTCHA_SCRIPT_HINTS)) { + if (matchHints(src, hints)) { + hasScriptHit = true; + providerSignals[provider] += 1; + addEvidence(evidence.selector_hits, `script[src*="${hints[0]}"]`); + } } + if (evidence.selector_hits.length >= CAPTCHA_MAX_EVIDENCE) break; + } + } catch (e) { + // ignore + } + + // Container selectors + for (const { selector, provider } of CAPTCHA_CONTAINER_SELECTORS) { + try { + const hit = document.querySelector(selector); + if (hit) { + hasContainerHit = true; + addEvidence(evidence.selector_hits, selector); + if (provider !== 'unknown') { + providerSignals[provider] += 1; + } + } + } catch (e) { + // ignore invalid selectors + } + } + + // Text keyword hints + const textSnippet = collectVisibleTextSnippet(); + if (textSnippet) { + const lowerText = textSnippet.toLowerCase(); + for (const keyword of CAPTCHA_TEXT_KEYWORDS) { + if (lowerText.includes(keyword)) { + hasKeywordHit = true; + addEvidence(evidence.text_hits, keyword); + } + } + } + + // URL hints + try { + const url = window.location?.href || ''; + const lowerUrl = url.toLowerCase(); + for (const hint of CAPTCHA_URL_HINTS) { + if (lowerUrl.includes(hint)) { + hasUrlHit = true; + addEvidence(evidence.url_hits, hint); + } + } + } catch (e) { + // ignore + } + + // Confidence scoring + let confidence = 0.0; + if (hasIframeHit) confidence += 0.7; + if (hasContainerHit) confidence += 0.5; + if (hasScriptHit) confidence += 0.5; + if (hasKeywordHit) confidence += 0.3; + if (hasUrlHit) confidence += 0.2; + confidence = Math.min(1.0, confidence); + + if (hasIframeHit) { + confidence = Math.max(confidence, 0.8); + } + + if (hasKeywordHit && !hasIframeHit && !hasContainerHit && !hasScriptHit && !hasUrlHit) { + confidence = Math.min(confidence, 0.4); + } + + const detected = confidence >= CAPTCHA_DETECTED_THRESHOLD; + + let providerHint = null; + if (providerSignals.recaptcha > 0) { + providerHint = 'recaptcha'; + } else if (providerSignals.hcaptcha > 0) { + providerHint = 'hcaptcha'; + } else if (providerSignals.turnstile > 0) { + providerHint = 'turnstile'; + } else if (providerSignals.arkose > 0) { + providerHint = 'arkose'; + } else if (providerSignals.awswaf > 0) { + providerHint = 'awswaf'; + } else if (detected) { + providerHint = 'unknown'; + } + + return { + detected, + provider_hint: providerHint, + confidence, + evidence, + }; + } + + // ============================================================================ + // LABEL INFERENCE SYSTEM + // ============================================================================ + + // Default inference configuration (conservative - Stage 1 equivalent) + const DEFAULT_INFERENCE_CONFIG = { + // Allowed tag names that can be inference sources + allowedTags: ['label', 'span', 'div'], + + // Allowed ARIA roles that can be inference sources + allowedRoles: [], + + // Class name patterns (substring match, case-insensitive) + allowedClassPatterns: [], + + // DOM tree traversal limits + maxParentDepth: 2, // Max 2 levels up DOM tree + maxSiblingDistance: 1, // Only immediate previous/next sibling + + // Container requirements (no distance-based checks) + requireSameContainer: true, // Must share common parent + containerTags: ['form', 'fieldset', 'div'], + + // Enable/disable specific inference methods + methods: { + explicitLabel: true, // el.labels API + ariaLabelledby: true, // aria-labelledby attribute + parentTraversal: true, // Check parent/grandparent + siblingProximity: true, // Check preceding sibling (same container only) + }, + }; + + // Merge user config with defaults + function mergeInferenceConfig(userConfig = {}) { + return { + ...DEFAULT_INFERENCE_CONFIG, + ...userConfig, + methods: { + ...DEFAULT_INFERENCE_CONFIG.methods, + ...(userConfig.methods || {}), + }, }; - function isInferenceSource(el, config) { - if (!el || !el.tagName) return !1; - const tag = el.tagName.toLowerCase(), role = el.getAttribute ? el.getAttribute("role") : "", className = ((el.className || "") + "").toLowerCase(); - if (config.allowedTags.includes(tag)) return !0; - if (config.allowedRoles.length > 0 && role && config.allowedRoles.includes(role)) return !0; - if (config.allowedClassPatterns.length > 0) for (const pattern of config.allowedClassPatterns) if (className.includes(pattern.toLowerCase())) return !0; - return !1; - } - function isInSameValidContainer(element, candidate, limits) { - if (!element || !candidate) return !1; - if (limits.requireSameContainer) { - const commonParent = function(el1, el2) { - if (!el1 || !el2) return null; - const doc = "undefined" != typeof global && global.document || "undefined" != typeof window && window.document || "undefined" != typeof document && document || null, parents1 = []; - let current = el1; - for (;current && (parents1.push(current), current.parentElement) && (!doc || current !== doc.body && current !== doc.documentElement); ) current = current.parentElement; - for (current = el2; current; ) { - if (-1 !== parents1.indexOf(current)) return current; - if (!current.parentElement) break; - if (doc && (current === doc.body || current === doc.documentElement)) break; - current = current.parentElement; + } + + // Check if element matches inference source criteria + function isInferenceSource(el, config) { + if (!el || !el.tagName) return false; + + const tag = el.tagName.toLowerCase(); + const role = el.getAttribute ? el.getAttribute('role') : ''; + const className = ((el.className || '') + '').toLowerCase(); + + // Check tag name + if (config.allowedTags.includes(tag)) { + return true; + } + + // Check role + if (config.allowedRoles.length > 0 && role && config.allowedRoles.includes(role)) { + return true; + } + + // Check class patterns + if (config.allowedClassPatterns.length > 0) { + for (const pattern of config.allowedClassPatterns) { + if (className.includes(pattern.toLowerCase())) { + return true; + } + } + } + + return false; + } + + // Helper: Find common parent element + function findCommonParent(el1, el2) { + if (!el1 || !el2) return null; + + // Get document reference safely for stopping conditions + // eslint-disable-next-line no-undef + const doc = + (typeof global !== 'undefined' && global.document) || + (typeof window !== 'undefined' && window.document) || + (typeof document !== 'undefined' && document) || + null; + + const parents1 = []; + let current = el1; + // Collect all parents (including el1 itself) + while (current) { + parents1.push(current); + // Stop if no parent + if (!current.parentElement) { + break; + } + // Stop at body or documentElement if they exist + if (doc && (current === doc.body || current === doc.documentElement)) { + break; + } + current = current.parentElement; + } + + // Check if el2 or any of its parents are in parents1 + current = el2; + while (current) { + // Use indexOf for more reliable comparison (handles object identity) + if (parents1.indexOf(current) !== -1) { + return current; + } + // Stop if no parent + if (!current.parentElement) { + break; + } + // Stop at body or documentElement if they exist + if (doc && (current === doc.body || current === doc.documentElement)) { + break; + } + current = current.parentElement; + } + + return null; + } + + // Helper: Check if element is a valid container + function isValidContainer(el, validTags) { + if (!el || !el.tagName) return false; + const tag = el.tagName.toLowerCase(); + // Handle both string and object className + let className = ''; + try { + className = (el.className || '') + ''; + } catch (e) { + className = ''; + } + return ( + validTags.includes(tag) || + className.toLowerCase().includes('form') || + className.toLowerCase().includes('field') + ); + } + + // Helper: Check container requirements (no distance-based checks) + function isInSameValidContainer(element, candidate, limits) { + if (!element || !candidate) return false; + + // Check same container requirement + if (limits.requireSameContainer) { + const commonParent = findCommonParent(element, candidate); + if (!commonParent) { + return false; + } + // Check if common parent is a valid container + if (!isValidContainer(commonParent, limits.containerTags)) { + return false; + } + } + + return true; + } + + // Main inference function + function getInferredLabel(el, options = {}) { + if (!el) return null; + + const { + enableInference = true, + inferenceConfig = {}, // User-provided config, merged with defaults + } = options; + + if (!enableInference) return null; + + // OPTIMIZATION: If element already has text or aria-label, skip inference entirely + // Check this BEFORE checking labels, so we don't infer if element already has text + // Note: For INPUT elements, we check value/placeholder, not innerText + // For IMG elements, we check alt, not innerText + // For other elements, innerText is considered explicit text + const ariaLabel = el.getAttribute ? el.getAttribute('aria-label') : null; + const hasAriaLabel = ariaLabel && ariaLabel.trim(); + const hasInputValue = el.tagName === 'INPUT' && (el.value || el.placeholder); + const hasImgAlt = el.tagName === 'IMG' && el.alt; + // For non-input/img elements, check innerText - but only if it's not empty + // Access innerText safely - it might be a getter or property + let innerTextValue = ''; + try { + innerTextValue = el.innerText || ''; + } catch (e) { + // If innerText access fails, treat as empty + innerTextValue = ''; + } + const hasInnerText = + el.tagName !== 'INPUT' && el.tagName !== 'IMG' && innerTextValue && innerTextValue.trim(); + + if (hasAriaLabel || hasInputValue || hasImgAlt || hasInnerText) { + return null; + } + + // Merge config with defaults + const config = mergeInferenceConfig(inferenceConfig); + + // Method 1: Explicit label association (el.labels API) + if (config.methods.explicitLabel && el.labels && el.labels.length > 0) { + const label = el.labels[0]; + if (isInferenceSource(label, config)) { + const text = (label.innerText || '').trim(); + if (text) { + return { + text, + source: 'explicit_label', + }; + } + } + } + + // Method 2: aria-labelledby (supports space-separated list of IDs) + // NOTE: aria-labelledby is an EXPLICIT reference, so it should work with ANY element + // regardless of inference source criteria. The config only controls whether this method runs. + if (config.methods.ariaLabelledby && el.hasAttribute && el.hasAttribute('aria-labelledby')) { + const labelIdsAttr = el.getAttribute('aria-labelledby'); + if (labelIdsAttr) { + // Split by whitespace to support multiple IDs (space-separated list) + const labelIds = labelIdsAttr.split(/\s+/).filter((id) => id.trim()); + const labelTexts = []; + + // Helper function to get document.getElementById from available contexts + const getDocument = () => { + // eslint-disable-next-line no-undef + if (typeof global !== 'undefined' && global.document) { + // eslint-disable-next-line no-undef + return global.document; + } + if (typeof window !== 'undefined' && window.document) { + return window.document; + } + if (typeof document !== 'undefined') { + return document; + } + return null; + }; + + const doc = getDocument(); + if (!doc || !doc.getElementById) ; else { + // Process each ID in the space-separated list + for (const labelId of labelIds) { + if (!labelId.trim()) continue; + + let labelEl = null; + try { + labelEl = doc.getElementById(labelId); + } catch (e) { + // If getElementById fails, skip this ID + continue; + } + + // aria-labelledby is an explicit reference - use ANY element, not just those matching inference criteria + // This follows ARIA spec: aria-labelledby can reference any element in the document + if (labelEl) { + // Extract text from the referenced element + let text = ''; + try { + // Try innerText first (preferred for visible text) + text = (labelEl.innerText || '').trim(); + // Fallback to textContent if innerText is empty + if (!text && labelEl.textContent) { + text = labelEl.textContent.trim(); } - return null; - }(element, candidate); - if (!commonParent) return !1; - if (!function(el, validTags) { - if (!el || !el.tagName) return !1; - const tag = el.tagName.toLowerCase(); - let className = ""; - try { - className = (el.className || "") + ""; - } catch (e) { - className = ""; + // Fallback to aria-label if available + if (!text && labelEl.getAttribute) { + const ariaLabel = labelEl.getAttribute('aria-label'); + if (ariaLabel) { + text = ariaLabel.trim(); + } } - return validTags.includes(tag) || className.toLowerCase().includes("form") || className.toLowerCase().includes("field"); - }(commonParent, limits.containerTags)) return !1; + } catch (e) { + // If text extraction fails, skip this element + continue; + } + + if (text) { + labelTexts.push(text); + } + } + } } - return !0; - } - function getInferredLabel(el, options = {}) { - if (!el) return null; - const {enableInference: enableInference = !0, inferenceConfig: inferenceConfig = {}} = options; - if (!enableInference) return null; - const ariaLabel = el.getAttribute ? el.getAttribute("aria-label") : null, hasAriaLabel = ariaLabel && ariaLabel.trim(), hasInputValue = "INPUT" === el.tagName && (el.value || el.placeholder), hasImgAlt = "IMG" === el.tagName && el.alt; - let innerTextValue = ""; - try { - innerTextValue = el.innerText || ""; - } catch (e) { - innerTextValue = ""; + + // Combine multiple label texts (space-separated) + if (labelTexts.length > 0) { + return { + text: labelTexts.join(' '), + source: 'aria_labelledby', + }; } - const hasInnerText = "INPUT" !== el.tagName && "IMG" !== el.tagName && innerTextValue && innerTextValue.trim(); - if (hasAriaLabel || hasInputValue || hasImgAlt || hasInnerText) return null; - const config = function(userConfig = {}) { + } + } + + // Method 3: Parent/grandparent traversal + if (config.methods.parentTraversal) { + let parent = el.parentElement; + let depth = 0; + while (parent && depth < config.maxParentDepth) { + if (isInferenceSource(parent, config)) { + const text = (parent.innerText || '').trim(); + if (text) { return { - ...DEFAULT_INFERENCE_CONFIG, - ...userConfig, - methods: { - ...DEFAULT_INFERENCE_CONFIG.methods, - ...userConfig.methods || {} - } + text, + source: 'parent_label', }; - }(inferenceConfig); - if (config.methods.explicitLabel && el.labels && el.labels.length > 0) { - const label = el.labels[0]; - if (isInferenceSource(label, config)) { - const text = (label.innerText || "").trim(); - if (text) return { - text: text, - source: "explicit_label" - }; - } + } } - if (config.methods.ariaLabelledby && el.hasAttribute && el.hasAttribute("aria-labelledby")) { - const labelIdsAttr = el.getAttribute("aria-labelledby"); - if (labelIdsAttr) { - const labelIds = labelIdsAttr.split(/\s+/).filter(id => id.trim()), labelTexts = [], doc = (() => "undefined" != typeof global && global.document ? global.document : "undefined" != typeof window && window.document ? window.document : "undefined" != typeof document ? document : null)(); - if (doc && doc.getElementById) for (const labelId of labelIds) { - if (!labelId.trim()) continue; - let labelEl = null; - try { - labelEl = doc.getElementById(labelId); - } catch (e) { - continue; - } - if (labelEl) { - let text = ""; - try { - if (text = (labelEl.innerText || "").trim(), !text && labelEl.textContent && (text = labelEl.textContent.trim()), - !text && labelEl.getAttribute) { - const ariaLabel = labelEl.getAttribute("aria-label"); - ariaLabel && (text = ariaLabel.trim()); - } - } catch (e) { - continue; - } - text && labelTexts.push(text); - } - } else ; - if (labelTexts.length > 0) return { - text: labelTexts.join(" "), - source: "aria_labelledby" - }; - } - } - if (config.methods.parentTraversal) { - let parent = el.parentElement, depth = 0; - for (;parent && depth < config.maxParentDepth; ) { - if (isInferenceSource(parent, config)) { - const text = (parent.innerText || "").trim(); - if (text) return { - text: text, - source: "parent_label" - }; - } - parent = parent.parentElement, depth++; - } + parent = parent.parentElement; + depth++; + } + } + + // Method 4: Preceding sibling (no distance-based checks, only DOM structure) + if (config.methods.siblingProximity) { + const prev = el.previousElementSibling; + if (prev && isInferenceSource(prev, config)) { + // Only check if they're in the same valid container (no pixel distance) + if ( + isInSameValidContainer(el, prev, { + requireSameContainer: config.requireSameContainer, + containerTags: config.containerTags, + }) + ) { + const text = (prev.innerText || '').trim(); + if (text) { + return { + text, + source: 'sibling_label', + }; + } } - if (config.methods.siblingProximity) { - const prev = el.previousElementSibling; - if (prev && isInferenceSource(prev, config) && isInSameValidContainer(el, prev, { - requireSameContainer: config.requireSameContainer, - containerTags: config.containerTags - })) { - const text = (prev.innerText || "").trim(); - if (text) return { - text: text, - source: "sibling_label" - }; - } + } + } + + return null; + } + + // --- HELPER: Nearby Static Text (cheap, best-effort) --- + // Returns a short, single-line snippet near the element (sibling/parent). + function getNearbyText(el, options = {}) { + if (!el) return null; + + const maxLen = typeof options.maxLen === 'number' ? options.maxLen : 80; + const ownText = normalizeNearbyText(el.innerText || ''); + + const candidates = []; + + const collect = (node) => { + if (!node) return; + let text = ''; + try { + text = normalizeNearbyText(node.innerText || node.textContent || ''); + } catch (e) { + text = ''; + } + if (!text || text === ownText) return; + candidates.push(text); + }; + + // Prefer immediate siblings + collect(el.previousElementSibling); + collect(el.nextElementSibling); + + // Fallback: short parent text (avoid large blocks) + if (candidates.length === 0 && el.parentElement) { + let parentText = ''; + try { + parentText = normalizeNearbyText(el.parentElement.innerText || ''); + } catch (e) { + parentText = ''; + } + if (parentText && parentText !== ownText && parentText.length <= 120) { + candidates.push(parentText); + } + } + + if (candidates.length === 0) return null; + + let text = candidates[0]; + if (text.length > maxLen) { + text = text.slice(0, maxLen).trim(); + } + return text || null; + } + + function normalizeNearbyText(text) { + if (!text) return ''; + return text.replace(/\s+/g, ' ').trim(); + } + + // Helper: Check if element is interactable (should have role inferred) + function isInteractableElement(el) { + if (!el || !el.tagName) return false; + + const tag = el.tagName.toLowerCase(); + const role = el.getAttribute ? el.getAttribute('role') : null; + const hasTabIndex = el.hasAttribute ? el.hasAttribute('tabindex') : false; + const hasHref = el.tagName === 'A' && (el.hasAttribute ? el.hasAttribute('href') : false); + + // Native interactive elements + const interactiveTags = [ + 'button', + 'input', + 'textarea', + 'select', + 'option', + 'details', + 'summary', + 'a', + ]; + if (interactiveTags.includes(tag)) { + // For , only if it has href + if (tag === 'a' && !hasHref) return false; + return true; + } + + // Elements with explicit interactive roles + const interactiveRoles = [ + 'button', + 'link', + 'tab', + 'menuitem', + 'checkbox', + 'radio', + 'switch', + 'slider', + 'combobox', + 'textbox', + 'searchbox', + 'spinbutton', + ]; + if (role && interactiveRoles.includes(role.toLowerCase())) { + return true; + } + + // Focusable elements (tabindex makes them interactive) + if (hasTabIndex) { + return true; + } + + // Elements with event handlers (custom interactive elements) + if (el.onclick || el.onkeydown || el.onkeypress || el.onkeyup) { + return true; + } + + // Check for inline event handlers via attributes + if (el.getAttribute) { + const hasInlineHandler = + el.getAttribute('onclick') || + el.getAttribute('onkeydown') || + el.getAttribute('onkeypress') || + el.getAttribute('onkeyup'); + if (hasInlineHandler) { + return true; + } + } + + return false; + } + + // Helper: Infer ARIA role for interactable elements + function getInferredRole(el, options = {}) { + const { + enableInference = true, + // inferenceConfig reserved for future extensibility + } = options; + + if (!enableInference) return null; + + // Only infer roles for interactable elements + if (!isInteractableElement(el)) { + return null; + } + + // CRITICAL: Only infer if element has NO aria-label AND NO explicit role + const hasAriaLabel = el.getAttribute ? el.getAttribute('aria-label') : null; + const hasExplicitRole = el.getAttribute ? el.getAttribute('role') : null; + + if (hasAriaLabel || hasExplicitRole) { + return null; // Skip inference if element already has aria-label or role + } + + // If element is native semantic HTML, it already has a role + const tag = el.tagName.toLowerCase(); + const semanticTags = ['button', 'a', 'input', 'textarea', 'select', 'option']; + if (semanticTags.includes(tag)) { + return null; // Native HTML already has role + } + + // Infer role based on element behavior or context + // Check for click handlers first (most common) + if (el.onclick || (el.getAttribute && el.getAttribute('onclick'))) { + return 'button'; + } + + // Check for keyboard handlers + if ( + el.onkeydown || + el.onkeypress || + el.onkeyup || + (el.getAttribute && + (el.getAttribute('onkeydown') || el.getAttribute('onkeypress') || el.getAttribute('onkeyup'))) + ) { + return 'button'; // Default to button for keyboard-interactive elements + } + + // Focusable div/span likely needs a role + if (el.hasAttribute && el.hasAttribute('tabindex') && (tag === 'div' || tag === 'span')) { + return 'button'; // Default assumption for focusable elements + } + + return null; + } + + // --- HELPER: Smart Text Extractor --- + function getText(el) { + if (el.getAttribute('aria-label')) return el.getAttribute('aria-label'); + if (el.tagName === 'INPUT') { + // Privacy: never return password values + const t = (el.getAttribute && el.getAttribute('type')) || el.type || ''; + if (String(t).toLowerCase() === 'password') { + return el.placeholder || ''; + } + return el.value || el.placeholder || ''; + } + if (el.tagName === 'IMG') return el.alt || ''; + return (el.innerText || '').replace(/\s+/g, ' ').trim().substring(0, 100); + } + + // Best-effort accessible name extraction for controls (used for v1 state-aware assertions) + function getAccessibleName(el) { + if (!el || !el.getAttribute) return ''; + + // 1) aria-label + const ariaLabel = el.getAttribute('aria-label'); + if (ariaLabel && ariaLabel.trim()) return ariaLabel.trim().substring(0, 200); + + // 2) aria-labelledby (space-separated IDs) + const labelledBy = el.getAttribute('aria-labelledby'); + if (labelledBy && labelledBy.trim()) { + const ids = labelledBy.split(/\s+/).filter((id) => id.trim()); + const texts = []; + for (const id of ids) { + try { + const ref = document.getElementById(id); + if (!ref) continue; + const txt = (ref.innerText || ref.textContent || ref.getAttribute?.('aria-label') || '') + .toString() + .trim(); + if (txt) texts.push(txt); + } catch (e) { + // ignore } + } + if (texts.length > 0) return texts.join(' ').substring(0, 200); + } + + // 3) has the text + // Case 2: Spans that wrap links (parent spans like HN's "titleline") - child is the actionable element + // This significantly reduces element count on link-heavy pages (HN, Reddit, search results) + const tagName = el.tagName.toLowerCase(); + if (tagName === 'span') { + // Case 1: Span is inside a link (any ancestor ) + if (el.closest('a')) { + return; // Skip - parent link has the content + } + // Case 2: Span contains a link as ANY descendant (wrapper span) + // HN structure: Title... + // Also handles: ... + const childLink = el.querySelector('a[href]'); // Find ANY descendant link with href + if (childLink && childLink.href) { + return; // Skip - descendant link is the actionable element + } + // Debug: Log spans with "titleline" class that weren't filtered + if (options.debug && el.className && el.className.includes('titleline')) { + console.log('[SentienceAPI] DEBUG: titleline span NOT filtered', { + className: el.className, + text: el.textContent?.slice(0, 50), + childLink: childLink, + hasChildHref: childLink?.href, + }); + } + } + + window.sentience_registry[idx] = el; + + // Input type is needed for safe value redaction (passwords) and state-aware assertions + const inputType = + tagName === 'input' + ? toSafeString((el.getAttribute && el.getAttribute('type')) || el.type || null) + : null; + const isPasswordInput = inputType && inputType.toLowerCase() === 'password'; + + // Use getSemanticText for inference support (falls back to getText if no inference) + const semanticText = getSemanticText(el, { + enableInference: options.enableInference !== false, // Default: true + inferenceConfig: options.inferenceConfig, // Pass configurable inference settings }); - clone.querySelectorAll("a[href]").forEach(link => { - const href = link.getAttribute("href"); - if (href && !href.startsWith("http://") && !href.startsWith("https://") && !href.startsWith("#")) try { - link.setAttribute("href", new URL(href, document.baseURI).href); - } catch (e) {} + const textVal = semanticText.text || getText(el); // Fallback to getText for backward compat + + // Infer role for interactable elements (only if no aria-label and no explicit role) + const inferredRole = getInferredRole(el, { + enableInference: options.enableInference !== false, + inferenceConfig: options.inferenceConfig, }); - return clone.querySelectorAll("img[src]").forEach(img => { - const src = img.getAttribute("src"); - if (src && !src.startsWith("http://") && !src.startsWith("https://") && !src.startsWith("data:")) try { - img.setAttribute("src", new URL(src, document.baseURI).href); - } catch (e) {} - }), clone.innerHTML; - } - function cleanElement(obj) { - if (Array.isArray(obj)) return obj.map(cleanElement); - if (null !== obj && "object" == typeof obj) { - const cleaned = {}; - for (const [key, value] of Object.entries(obj)) if (null != value) if ("object" == typeof value) { - const deepClean = cleanElement(value); - Object.keys(deepClean).length > 0 && (cleaned[key] = deepClean); - } else cleaned[key] = value; - return cleaned; + const inView = isInViewport(rect); + + // Get computed style once (needed for both occlusion check and data collection) + const style = window.getComputedStyle(el); + + // Only check occlusion for elements likely to be occluded (optimized) + // This avoids layout thrashing for the vast majority of elements + const occluded = inView ? isOccluded(el, rect, style) : false; + + // Get effective background color (traverses DOM to find non-transparent color) + const effectiveBgColor = getEffectiveBackgroundColor(el); + + // Safe value extraction (PII-aware) + let safeValue = null; + let valueRedacted = null; + try { + if (el.value !== undefined || (el.getAttribute && el.getAttribute('value') !== null)) { + if (isPasswordInput) { + safeValue = null; + valueRedacted = 'true'; + } else { + const rawValue = + el.value !== undefined ? String(el.value) : String(el.getAttribute('value')); + safeValue = rawValue.length > 200 ? rawValue.substring(0, 200) : rawValue; + valueRedacted = 'false'; + } + } + } catch (e) { + // ignore } - return obj; - } - async function snapshot(options = {}) { + + // Best-effort accessible name (label-like, not the typed value) + const accessibleName = toSafeString(getAccessibleName(el) || null); + + const nearbyText = isInteractableElement(el) ? getNearbyText(el, { maxLen: 80 }) : null; + + rawData.push({ + id: idx, + tag: tagName, + rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, + styles: { + display: toSafeString(style.display), + visibility: toSafeString(style.visibility), + opacity: toSafeString(style.opacity), + z_index: toSafeString(style.zIndex || 'auto'), + position: toSafeString(style.position), + bg_color: toSafeString(effectiveBgColor || style.backgroundColor), + color: toSafeString(style.color), + cursor: toSafeString(style.cursor), + font_weight: toSafeString(style.fontWeight), + font_size: toSafeString(style.fontSize), + }, + attributes: { + role: toSafeString(el.getAttribute('role')), + type_: toSafeString(el.getAttribute('type')), + input_type: inputType, + aria_label: + semanticText?.source === 'explicit_aria_label' + ? semanticText.text + : toSafeString(el.getAttribute('aria-label')), // Keep original for backward compat + name: accessibleName, + inferred_label: + semanticText?.source && + !['explicit_aria_label', 'input_value', 'img_alt', 'inner_text'].includes( + semanticText.source + ) + ? toSafeString(semanticText.text) + : null, + label_source: semanticText?.source || null, // Track source for gateway + inferred_role: inferredRole ? toSafeString(inferredRole) : null, // Inferred role for interactable elements + nearby_text: toSafeString(nearbyText), + // Get href: check element first, then traverse up to find parent link + // This ensures nested spans inside links inherit the href + href: toSafeString( + el.href || el.getAttribute('href') || (el.closest && el.closest('a')?.href) || null + ), + class: toSafeString(getClassName(el)), + // Capture dynamic input state (not just initial attributes) + value: safeValue !== null ? toSafeString(safeValue) : null, + value_redacted: valueRedacted, + checked: el.checked !== undefined ? String(el.checked) : null, + disabled: el.disabled !== undefined ? String(el.disabled) : null, + aria_checked: toSafeString(el.getAttribute('aria-checked')), + aria_disabled: toSafeString(el.getAttribute('aria-disabled')), + aria_expanded: toSafeString(el.getAttribute('aria-expanded')), + }, + text: toSafeString(textVal), + in_viewport: inView, + is_occluded: occluded, + // Phase 1: Pass scroll position for doc_y computation in WASM + scroll_y: window.scrollY, + }); + }); + + console.log(`[SentienceAPI] Collected ${rawData.length} elements from main frame`); + + // Step 1.5: Collect iframe snapshots and FLATTEN immediately + // "Flatten Early" architecture: Merge iframe elements into main array before WASM + // This allows WASM to process all elements uniformly (no recursion needed) + const allRawElements = [...rawData]; // Start with main frame elements + let totalIframeElements = 0; + + if (options.collectIframes !== false) { try { - !1 !== options.waitForStability && await async function(options = {}) { - const {minNodeCount: minNodeCount = 500, quietPeriod: quietPeriod = 200, maxWait: maxWait = 5e3} = options, startTime = Date.now(); + console.log(`[SentienceAPI] Starting iframe collection...`); + const iframeSnapshots = await collectIframeSnapshots(options); + console.log( + `[SentienceAPI] Iframe collection complete. Received ${iframeSnapshots.size} snapshot(s)` + ); + + if (iframeSnapshots.size > 0) { + // FLATTEN IMMEDIATELY: Don't nest them. Just append them with coordinate translation. + iframeSnapshots.forEach((iframeSnapshot, iframeEl) => { + // Debug: Log structure to verify data is correct + // console.log(`[SentienceAPI] Processing iframe snapshot:`, iframeSnapshot); + + if (iframeSnapshot && iframeSnapshot.raw_elements) { + const rawElementsCount = iframeSnapshot.raw_elements.length; + console.log( + `[SentienceAPI] Processing ${rawElementsCount} elements from iframe (src: ${iframeEl.src || 'unknown'})` + ); + // Get iframe's bounding rect (offset for coordinate translation) + const iframeRect = iframeEl.getBoundingClientRect(); + const offset = { x: iframeRect.x, y: iframeRect.y }; + + // Get iframe context for frame switching (Playwright needs this) + const iframeSrc = iframeEl.src || iframeEl.getAttribute('src') || ''; + let isSameOrigin = false; try { - window.__sentience_lastMutationTs = performance.now(); - } catch (e) {} - return new Promise(resolve => { - if (document.querySelectorAll("*").length >= minNodeCount) { - let lastChange = Date.now(); - const observer = new MutationObserver(() => { - lastChange = Date.now(); - try { - window.__sentience_lastMutationTs = performance.now(); - } catch (e) {} - }); - observer.observe(document.body, { - childList: !0, - subtree: !0, - attributes: !1 - }); - const checkStable = () => { - const timeSinceLastChange = Date.now() - lastChange, totalWait = Date.now() - startTime; - timeSinceLastChange >= quietPeriod || totalWait >= maxWait ? (observer.disconnect(), - resolve()) : setTimeout(checkStable, 50); - }; - checkStable(); - } else { - const observer = new MutationObserver(() => { - const currentCount = document.querySelectorAll("*").length, totalWait = Date.now() - startTime; - try { - window.__sentience_lastMutationTs = performance.now(); - } catch (e) {} - if (currentCount >= minNodeCount) { - observer.disconnect(); - let lastChange = Date.now(); - const quietObserver = new MutationObserver(() => { - lastChange = Date.now(); - try { - window.__sentience_lastMutationTs = performance.now(); - } catch (e) {} - }); - quietObserver.observe(document.body, { - childList: !0, - subtree: !0, - attributes: !1 - }); - const checkQuiet = () => { - const timeSinceLastChange = Date.now() - lastChange, totalWait = Date.now() - startTime; - timeSinceLastChange >= quietPeriod || totalWait >= maxWait ? (quietObserver.disconnect(), - resolve()) : setTimeout(checkQuiet, 50); - }; - checkQuiet(); - } else totalWait >= maxWait && (observer.disconnect(), resolve()); - }); - observer.observe(document.body, { - childList: !0, - subtree: !0, - attributes: !1 - }), setTimeout(() => { - observer.disconnect(), resolve(); - }, maxWait); - } - }); - }(options.waitForStability || {}); - const rawData = []; - window.sentience_registry = []; - getAllElements().forEach((el, idx) => { - if (!el.getBoundingClientRect) return; - const rect = el.getBoundingClientRect(); - if (rect.width < 5 || rect.height < 5) return; - const tagName = el.tagName.toLowerCase(); - if ("span" === tagName) { - if (el.closest("a")) return; - const childLink = el.querySelector("a[href]"); - if (childLink && childLink.href) return; - options.debug && el.className && el.className.includes("titleline"); + // Try to access contentWindow to check if same-origin + isSameOrigin = iframeEl.contentWindow !== null; + } catch (e) { + isSameOrigin = false; } - window.sentience_registry[idx] = el; - const inputType = "input" === tagName ? toSafeString(el.getAttribute && el.getAttribute("type") || el.type || null) : null, isPasswordInput = inputType && "password" === inputType.toLowerCase(), semanticText = function(el, options = {}) { - if (!el) return { - text: "", - source: null - }; - const explicitAriaLabel = el.getAttribute ? el.getAttribute("aria-label") : null; - if (explicitAriaLabel && explicitAriaLabel.trim()) return { - text: explicitAriaLabel.trim(), - source: "explicit_aria_label" - }; - if ("INPUT" === el.tagName) { - const t = el.getAttribute && el.getAttribute("type") || el.type || "", isPassword = "password" === String(t).toLowerCase(), value = (isPassword ? el.placeholder || "" : el.value || el.placeholder || "").trim(); - if (value) return { - text: value, - source: isPassword ? "input_placeholder" : "input_value" - }; - } - if ("IMG" === el.tagName) { - const alt = (el.alt || "").trim(); - if (alt) return { - text: alt, - source: "img_alt" - }; - } - const innerText = (el.innerText || "").trim(); - if (innerText) return { - text: innerText.substring(0, 100), - source: "inner_text" - }; - const inferred = getInferredLabel(el, { - enableInference: !1 !== options.enableInference, - inferenceConfig: options.inferenceConfig - }); - return inferred || { - text: "", - source: null + + // Adjust coordinates and add iframe context to each element + const adjustedElements = iframeSnapshot.raw_elements.map((el) => { + const adjusted = { ...el }; + + // Adjust rect coordinates to parent viewport + if (adjusted.rect) { + adjusted.rect = { + ...adjusted.rect, + x: adjusted.rect.x + offset.x, + y: adjusted.rect.y + offset.y, }; - }(el, { - enableInference: !1 !== options.enableInference, - inferenceConfig: options.inferenceConfig - }), textVal = semanticText.text || getText(el), inferredRole = function(el, options = {}) { - const {enableInference: enableInference = !0} = options; - if (!enableInference) return null; - if (!isInteractableElement(el)) return null; - const hasAriaLabel = el.getAttribute ? el.getAttribute("aria-label") : null, hasExplicitRole = el.getAttribute ? el.getAttribute("role") : null; - if (hasAriaLabel || hasExplicitRole) return null; - const tag = el.tagName.toLowerCase(); - return [ "button", "a", "input", "textarea", "select", "option" ].includes(tag) ? null : el.onclick || el.getAttribute && el.getAttribute("onclick") || el.onkeydown || el.onkeypress || el.onkeyup || el.getAttribute && (el.getAttribute("onkeydown") || el.getAttribute("onkeypress") || el.getAttribute("onkeyup")) || el.hasAttribute && el.hasAttribute("tabindex") && ("div" === tag || "span" === tag) ? "button" : null; - }(el, { - enableInference: !1 !== options.enableInference, - inferenceConfig: options.inferenceConfig - }), inView = function(rect) { - return rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0; - }(rect), style = window.getComputedStyle(el), occluded = !!inView && function(el, rect, style) { - const zIndex = parseInt(style.zIndex, 10); - if ("static" === style.position && (isNaN(zIndex) || zIndex <= 10)) return !1; - const cx = rect.x + rect.width / 2, cy = rect.y + rect.height / 2; - if (cx < 0 || cx > window.innerWidth || cy < 0 || cy > window.innerHeight) return !1; - const topEl = document.elementFromPoint(cx, cy); - return !!topEl && !(el === topEl || el.contains(topEl) || topEl.contains(el)); - }(el, rect, style), effectiveBgColor = function(el) { - if (!el) return null; - if ("SVG" === el.tagName) { - const svgColor = getSVGColor(el); - if (svgColor) return svgColor; - } - let current = el, depth = 0; - for (;current && depth < 10; ) { - const style = window.getComputedStyle(current); - if ("SVG" === current.tagName) { - const svgColor = getSVGColor(current); - if (svgColor) return svgColor; - } - const bgColor = style.backgroundColor; - if (bgColor && "transparent" !== bgColor && "rgba(0, 0, 0, 0)" !== bgColor) { - const rgbaMatch = bgColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/); - if (!rgbaMatch) return bgColor.startsWith("rgb("), bgColor; - if ((rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 1) >= .9) return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`; - } - current = current.parentElement, depth++; - } - return null; - }(el); - let safeValue = null, valueRedacted = null; - try { - if (void 0 !== el.value || el.getAttribute && null !== el.getAttribute("value")) if (isPasswordInput) safeValue = null, - valueRedacted = "true"; else { - const rawValue = void 0 !== el.value ? String(el.value) : String(el.getAttribute("value")); - safeValue = rawValue.length > 200 ? rawValue.substring(0, 200) : rawValue, valueRedacted = "false"; - } - } catch (e) {} - const accessibleName = toSafeString(function(el) { - if (!el || !el.getAttribute) return ""; - const ariaLabel = el.getAttribute("aria-label"); - if (ariaLabel && ariaLabel.trim()) return ariaLabel.trim().substring(0, 200); - const labelledBy = el.getAttribute("aria-labelledby"); - if (labelledBy && labelledBy.trim()) { - const ids = labelledBy.split(/\s+/).filter(id => id.trim()), texts = []; - for (const id of ids) try { - const ref = document.getElementById(id); - if (!ref) continue; - const txt = (ref.innerText || ref.textContent || ref.getAttribute?.("aria-label") || "").toString().trim(); - txt && texts.push(txt); - } catch (e) {} - if (texts.length > 0) return texts.join(" ").substring(0, 200); - } - try { - if (el.labels && el.labels.length > 0) { - const t = (el.labels[0].innerText || el.labels[0].textContent || "").toString().trim(); - if (t) return t.substring(0, 200); - } - } catch (e) {} - try { - const parentLabel = el.closest && el.closest("label"); - if (parentLabel) { - const t = (parentLabel.innerText || parentLabel.textContent || "").toString().trim(); - if (t) return t.substring(0, 200); - } - } catch (e) {} - const tag = (el.tagName || "").toUpperCase(); - if ("INPUT" === tag || "TEXTAREA" === tag) { - const ph = (el.getAttribute("placeholder") || "").toString().trim(); - if (ph) return ph.substring(0, 200); - } - const title = el.getAttribute("title"); - return title && title.trim() ? title.trim().substring(0, 200) : ""; - }(el) || null), nearbyText = isInteractableElement(el) ? function(el, options = {}) { - if (!el) return null; - const maxLen = "number" == typeof options.maxLen ? options.maxLen : 80, ownText = normalizeNearbyText(el.innerText || ""), candidates = [], collect = node => { - if (!node) return; - let text = ""; - try { - text = normalizeNearbyText(node.innerText || node.textContent || ""); - } catch (e) { - text = ""; - } - text && text !== ownText && candidates.push(text); - }; - if (collect(el.previousElementSibling), collect(el.nextElementSibling), 0 === candidates.length && el.parentElement) { - let parentText = ""; - try { - parentText = normalizeNearbyText(el.parentElement.innerText || ""); - } catch (e) { - parentText = ""; - } - parentText && parentText !== ownText && parentText.length <= 120 && candidates.push(parentText); - } - if (0 === candidates.length) return null; - let text = candidates[0]; - return text.length > maxLen && (text = text.slice(0, maxLen).trim()), text || null; - }(el, { - maxLen: 80 - }) : null; - rawData.push({ - id: idx, - tag: tagName, - rect: { - x: rect.x, - y: rect.y, - width: rect.width, - height: rect.height - }, - styles: { - display: toSafeString(style.display), - visibility: toSafeString(style.visibility), - opacity: toSafeString(style.opacity), - z_index: toSafeString(style.zIndex || "auto"), - position: toSafeString(style.position), - bg_color: toSafeString(effectiveBgColor || style.backgroundColor), - color: toSafeString(style.color), - cursor: toSafeString(style.cursor), - font_weight: toSafeString(style.fontWeight), - font_size: toSafeString(style.fontSize) - }, - attributes: { - role: toSafeString(el.getAttribute("role")), - type_: toSafeString(el.getAttribute("type")), - input_type: inputType, - aria_label: "explicit_aria_label" === semanticText?.source ? semanticText.text : toSafeString(el.getAttribute("aria-label")), - name: accessibleName, - inferred_label: semanticText?.source && ![ "explicit_aria_label", "input_value", "img_alt", "inner_text" ].includes(semanticText.source) ? toSafeString(semanticText.text) : null, - label_source: semanticText?.source || null, - inferred_role: inferredRole ? toSafeString(inferredRole) : null, - nearby_text: toSafeString(nearbyText), - href: toSafeString(el.href || el.getAttribute("href") || el.closest && el.closest("a")?.href || null), - class: toSafeString(getClassName(el)), - value: null !== safeValue ? toSafeString(safeValue) : null, - value_redacted: valueRedacted, - checked: void 0 !== el.checked ? String(el.checked) : null, - disabled: void 0 !== el.disabled ? String(el.disabled) : null, - aria_checked: toSafeString(el.getAttribute("aria-checked")), - aria_disabled: toSafeString(el.getAttribute("aria-disabled")), - aria_expanded: toSafeString(el.getAttribute("aria-expanded")) - }, - text: toSafeString(textVal), - in_viewport: inView, - is_occluded: occluded, - scroll_y: window.scrollY + } + + // Add iframe context so agents can switch frames in Playwright + adjusted.iframe_context = { + src: iframeSrc, + is_same_origin: isSameOrigin, + }; + + return adjusted; }); + + // Append flattened iframe elements to main array + allRawElements.push(...adjustedElements); + totalIframeElements += adjustedElements.length; + } }); - const allRawElements = [ ...rawData ]; - let totalIframeElements = 0; - if (!1 !== options.collectIframes) try { - const iframeSnapshots = await async function(options = {}) { - const iframeData = new Map, iframes = Array.from(document.querySelectorAll("iframe")); - if (0 === iframes.length) return iframeData; - const iframePromises = iframes.map((iframe, idx) => { - const src = iframe.src || ""; - return src.includes("doubleclick") || src.includes("googleadservices") || src.includes("ads system") ? Promise.resolve(null) : new Promise(resolve => { - const requestId = `iframe-${idx}-${Date.now()}`, timeout = setTimeout(() => { - resolve(null); - }, 5e3), listener = event => { - "SENTIENCE_IFRAME_SNAPSHOT_RESPONSE" === event.data?.type && event.data, "SENTIENCE_IFRAME_SNAPSHOT_RESPONSE" === event.data?.type && event.data?.requestId === requestId && (clearTimeout(timeout), - window.removeEventListener("message", listener), event.data.error ? resolve(null) : (event.data.snapshot, - resolve({ - iframe: iframe, - data: event.data.snapshot, - error: null - }))); - }; - window.addEventListener("message", listener); - try { - iframe.contentWindow ? iframe.contentWindow.postMessage({ - type: "SENTIENCE_IFRAME_SNAPSHOT_REQUEST", - requestId: requestId, - options: { - ...options, - collectIframes: !0 - } - }, "*") : (clearTimeout(timeout), window.removeEventListener("message", listener), - resolve(null)); - } catch (error) { - clearTimeout(timeout), window.removeEventListener("message", listener), resolve(null); - } - }); - }); - return (await Promise.all(iframePromises)).forEach((result, idx) => { - result && result.data && !result.error ? iframeData.set(iframes[idx], result.data) : result && result.error; - }), iframeData; - }(options); - iframeSnapshots.size > 0 && iframeSnapshots.forEach((iframeSnapshot, iframeEl) => { - if (iframeSnapshot && iframeSnapshot.raw_elements) { - iframeSnapshot.raw_elements.length; - const iframeRect = iframeEl.getBoundingClientRect(), offset = { - x: iframeRect.x, - y: iframeRect.y - }, iframeSrc = iframeEl.src || iframeEl.getAttribute("src") || ""; - let isSameOrigin = !1; - try { - isSameOrigin = null !== iframeEl.contentWindow; - } catch (e) { - isSameOrigin = !1; - } - const adjustedElements = iframeSnapshot.raw_elements.map(el => { - const adjusted = { - ...el - }; - return adjusted.rect && (adjusted.rect = { - ...adjusted.rect, - x: adjusted.rect.x + offset.x, - y: adjusted.rect.y + offset.y - }), adjusted.iframe_context = { - src: iframeSrc, - is_same_origin: isSameOrigin - }, adjusted; - }); - allRawElements.push(...adjustedElements), totalIframeElements += adjustedElements.length; - } - }); - } catch (error) {} - const processed = await function(rawData, options) { - return new Promise((resolve, reject) => { - const requestId = Math.random().toString(36).substring(7); - let resolved = !1; - const timeout = setTimeout(() => { - resolved || (resolved = !0, window.removeEventListener("message", listener), reject(new Error("WASM processing timeout - extension may be unresponsive. Try reloading the extension."))); - }, 25e3), listener = e => { - if ("SENTIENCE_SNAPSHOT_RESULT" === e.data.type && e.data.requestId === requestId) { - if (resolved) return; - resolved = !0, clearTimeout(timeout), window.removeEventListener("message", listener), - e.data.error ? reject(new Error(e.data.error)) : resolve({ - elements: e.data.elements, - raw_elements: e.data.raw_elements, - duration: e.data.duration - }); - } - }; - window.addEventListener("message", listener); - try { - window.postMessage({ - type: "SENTIENCE_SNAPSHOT_REQUEST", - requestId: requestId, - rawData: rawData, - options: options - }, "*"); - } catch (error) { - resolved || (resolved = !0, clearTimeout(timeout), window.removeEventListener("message", listener), - reject(new Error(`Failed to send snapshot request: ${error.message}`))); - } - }); - }(allRawElements, options); - if (!processed || !processed.elements) throw new Error("WASM processing returned invalid result"); - let screenshot = null; - options.screenshot && (screenshot = await function(options) { - return new Promise(resolve => { - const requestId = Math.random().toString(36).substring(7), listener = e => { - "SENTIENCE_SCREENSHOT_RESULT" === e.data.type && e.data.requestId === requestId && (window.removeEventListener("message", listener), - resolve(e.data.screenshot)); - }; - window.addEventListener("message", listener), window.postMessage({ - type: "SENTIENCE_SCREENSHOT_REQUEST", - requestId: requestId, - options: options - }, "*"), setTimeout(() => { - window.removeEventListener("message", listener), resolve(null); - }, 1e4); - }); - }(options.screenshot)); - const cleanedElements = cleanElement(processed.elements), cleanedRawElements = cleanElement(processed.raw_elements); - cleanedElements.length, cleanedRawElements.length; - let diagnostics; - try { - const lastMutationTs = window.__sentience_lastMutationTs, now = performance.now(), quietMs = "number" == typeof lastMutationTs && Number.isFinite(lastMutationTs) ? Math.max(0, now - lastMutationTs) : null, nodeCount = document.querySelectorAll("*").length; - let requiresVision = !1, requiresVisionReason = null; - const canvasCount = document.getElementsByTagName("canvas").length; - canvasCount > 0 && (requiresVision = !0, requiresVisionReason = `canvas:${canvasCount}`), - diagnostics = { - metrics: { - ready_state: document.readyState || null, - quiet_ms: quietMs, - node_count: nodeCount - }, - captcha: detectCaptcha(), - requires_vision: requiresVision, - requires_vision_reason: requiresVisionReason - }; - } catch (e) {} - return { - status: "success", - url: window.location.href, - viewport: { - width: window.innerWidth, - height: window.innerHeight - }, - elements: cleanedElements, - raw_elements: cleanedRawElements, - screenshot: screenshot, - diagnostics: diagnostics - }; + + // console.log(`[SentienceAPI] Merged ${iframeSnapshots.size} iframe(s). Total elements: ${allRawElements.length} (${rawData.length} main + ${totalIframeElements} iframe)`); + } } catch (error) { - return { - status: "error", - error: error.message || "Unknown error", - stack: error.stack - }; + console.warn('[SentienceAPI] Iframe collection failed:', error); } - } - function read(options = {}) { - const format = options.format || "raw"; - let content; - return content = "raw" === format ? getRawHTML(document.body) : "markdown" === format ? function(root) { - const rawHTML = getRawHTML(root), tempDiv = document.createElement("div"); - tempDiv.innerHTML = rawHTML; - let markdown = "", insideLink = !1; - return function walk(node) { - if (node.nodeType === Node.TEXT_NODE) { - const text = node.textContent.replace(/[\r\n]+/g, " ").replace(/\s+/g, " "); - return void (text.trim() && (markdown += text)); - } - if (node.nodeType !== Node.ELEMENT_NODE) return; - const tag = node.tagName.toLowerCase(); - if ("h1" === tag && (markdown += "\n# "), "h2" === tag && (markdown += "\n## "), - "h3" === tag && (markdown += "\n### "), "li" === tag && (markdown += "\n- "), insideLink || "p" !== tag && "div" !== tag && "br" !== tag || (markdown += "\n"), - "strong" !== tag && "b" !== tag || (markdown += "**"), "em" !== tag && "i" !== tag || (markdown += "_"), - "a" === tag && (markdown += "[", insideLink = !0), node.shadowRoot ? Array.from(node.shadowRoot.childNodes).forEach(walk) : node.childNodes.forEach(walk), - "a" === tag) { - const href = node.getAttribute("href"); - markdown += href ? `](${href})` : "]", insideLink = !1; - } - "strong" !== tag && "b" !== tag || (markdown += "**"), "em" !== tag && "i" !== tag || (markdown += "_"), - insideLink || "h1" !== tag && "h2" !== tag && "h3" !== tag && "p" !== tag && "div" !== tag || (markdown += "\n"); - }(tempDiv), markdown.replace(/\n{3,}/g, "\n\n").trim(); - }(document.body) : function(root) { - let text = ""; - return function walk(node) { - if (node.nodeType !== Node.TEXT_NODE) { - if (node.nodeType === Node.ELEMENT_NODE) { - const tag = node.tagName.toLowerCase(); - if ([ "nav", "footer", "header", "script", "style", "noscript", "iframe", "svg" ].includes(tag)) return; - const style = window.getComputedStyle(node); - if ("none" === style.display || "hidden" === style.visibility) return; - const isBlock = "block" === style.display || "flex" === style.display || "P" === node.tagName || "DIV" === node.tagName; - isBlock && (text += " "), node.shadowRoot ? Array.from(node.shadowRoot.childNodes).forEach(walk) : node.childNodes.forEach(walk), - isBlock && (text += "\n"); - } - } else text += node.textContent; - }(root || document.body), text.replace(/\n{3,}/g, "\n\n").trim(); - }(document.body), { - status: "success", - url: window.location.href, - format: format, - content: content, - length: content.length + } + + // Step 2: Send EVERYTHING to WASM (One giant flat list) + // Now WASM prunes iframe elements and main elements in one pass! + // No recursion needed - everything is already flat + console.log( + `[SentienceAPI] Sending ${allRawElements.length} total elements to WASM (${rawData.length} main + ${totalIframeElements} iframe)` + ); + const fallbackElementsFromRaw = (raw) => + (raw || []).map((r) => { + const rect = (r && r.rect) || { x: 0, y: 0, width: 0, height: 0 }; + const attrs = (r && r.attributes) || {}; + const role = + attrs.role || + (r && (r.inferred_role || r.inferredRole)) || + (r && r.tag === 'a' ? 'link' : 'generic'); + const href = attrs.href || (r && r.href) || null; + const isClickable = + role === 'link' || + role === 'button' || + role === 'textbox' || + role === 'checkbox' || + role === 'radio' || + role === 'combobox' || + !!href; + + return { + id: Number((r && r.id) || 0), + role: String(role || 'generic'), + text: (r && (r.text || r.semantic_text || r.semanticText)) || null, + importance: 1, + bbox: { + x: Number(rect.x || 0), + y: Number(rect.y || 0), + width: Number(rect.width || 0), + height: Number(rect.height || 0), + }, + visual_cues: { + is_primary: false, + is_clickable: !!isClickable, + }, + in_viewport: true, + is_occluded: !!(r && (r.occluded || r.is_occluded)), + z_index: 0, + name: attrs.aria_label || attrs.ariaLabel || null, + value: (r && r.value) || null, + input_type: attrs.type_ || attrs.type || null, + checked: typeof (r && r.checked) === 'boolean' ? r.checked : null, + disabled: typeof (r && r.disabled) === 'boolean' ? r.disabled : null, + expanded: typeof (r && r.expanded) === 'boolean' ? r.expanded : null, + }; + }); + + let processed = null; + try { + processed = await processSnapshotInBackground(allRawElements, options); + } catch (error) { + console.warn( + '[SentienceAPI] WASM processing failed; falling back to raw mapping:', + error + ); + processed = { + elements: fallbackElementsFromRaw(allRawElements), + raw_elements: allRawElements, + duration: null, }; - } - function findTextRect(options = {}) { - const {text: text, containerElement: containerElement = document.body, caseSensitive: caseSensitive = !1, wholeWord: wholeWord = !1, maxResults: maxResults = 10} = options; - if (!text || 0 === text.trim().length) return { - status: "error", - error: "Text parameter is required" + } + + if (!processed || !processed.elements) { + processed = { + elements: fallbackElementsFromRaw(allRawElements), + raw_elements: allRawElements, + duration: null, }; - const results = [], searchText = caseSensitive ? text : text.toLowerCase(); - function findInTextNode(textNode) { - const nodeText = textNode.nodeValue, searchableText = caseSensitive ? nodeText : nodeText.toLowerCase(); - let startIndex = 0; - for (;startIndex < nodeText.length && results.length < maxResults; ) { - const foundIndex = searchableText.indexOf(searchText, startIndex); - if (-1 === foundIndex) break; - if (wholeWord) { - const before = foundIndex > 0 ? nodeText[foundIndex - 1] : " ", after = foundIndex + text.length < nodeText.length ? nodeText[foundIndex + text.length] : " "; - if (!/\s/.test(before) || !/\s/.test(after)) { - startIndex = foundIndex + 1; - continue; - } - } - try { - const range = document.createRange(); - range.setStart(textNode, foundIndex), range.setEnd(textNode, foundIndex + text.length); - const rect = range.getBoundingClientRect(); - rect.width > 0 && rect.height > 0 && results.push({ - text: nodeText.substring(foundIndex, foundIndex + text.length), - rect: { - x: rect.left + window.scrollX, - y: rect.top + window.scrollY, - width: rect.width, - height: rect.height, - left: rect.left + window.scrollX, - top: rect.top + window.scrollY, - right: rect.right + window.scrollX, - bottom: rect.bottom + window.scrollY - }, - viewport_rect: { - x: rect.left, - y: rect.top, - width: rect.width, - height: rect.height - }, - context: { - before: nodeText.substring(Math.max(0, foundIndex - 20), foundIndex), - after: nodeText.substring(foundIndex + text.length, Math.min(nodeText.length, foundIndex + text.length + 20)) - }, - in_viewport: rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth - }); - } catch (e) {} - startIndex = foundIndex + 1; - } + } + + // Step 3: Capture screenshot if requested + let screenshot = null; + if (options.screenshot) { + screenshot = await captureScreenshot(options.screenshot); + } + + // Step 4: Clean and return + const cleanedElements = cleanElement(processed.elements); + const cleanedRawElements = cleanElement(processed.raw_elements); + + // FIXED: Removed undefined 'totalIframeRawElements' + // FIXED: Logic updated for "Flatten Early" architecture. + // processed.elements ALREADY contains the merged iframe elements, + // so we simply use .length. No addition needed. + + const totalCount = cleanedElements.length; + const totalRaw = cleanedRawElements.length; + const iframeCount = totalIframeElements || 0; + + console.log( + `[SentienceAPI] ✓ Complete: ${totalCount} Smart Elements, ${totalRaw} Raw Elements (includes ${iframeCount} from iframes) (WASM took ${processed.duration?.toFixed(1)}ms)` + ); + + // Snapshot diagnostics (Phase 2): report stability metrics from the page context. + // Confidence/exhaustion is computed in the Gateway/SDKs; the extension supplies raw metrics. + let diagnostics = undefined; + try { + const lastMutationTs = window.__sentience_lastMutationTs; + const now = performance.now(); + const quietMs = + typeof lastMutationTs === 'number' && Number.isFinite(lastMutationTs) + ? Math.max(0, now - lastMutationTs) + : null; + const nodeCount = document.querySelectorAll('*').length; + + // P1-01: best-effort signal that structure may be insufficient (vision executor recommended). + // Keep heuristics conservative: we only set requires_vision when we see clear structural blockers. + let requiresVision = false; + let requiresVisionReason = null; + const canvasCount = document.getElementsByTagName('canvas').length; + if (canvasCount > 0) { + requiresVision = true; + requiresVisionReason = `canvas:${canvasCount}`; } - const walker = document.createTreeWalker(containerElement, NodeFilter.SHOW_TEXT, { - acceptNode(node) { - const parent = node.parentElement; - if (!parent) return NodeFilter.FILTER_REJECT; - const tagName = parent.tagName.toLowerCase(); - if ("script" === tagName || "style" === tagName || "noscript" === tagName) return NodeFilter.FILTER_REJECT; - if (!node.nodeValue || 0 === node.nodeValue.trim().length) return NodeFilter.FILTER_REJECT; - const computedStyle = window.getComputedStyle(parent); - return "none" === computedStyle.display || "hidden" === computedStyle.visibility || "0" === computedStyle.opacity ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT; - } - }); - let currentNode; - for (;(currentNode = walker.nextNode()) && results.length < maxResults; ) findInTextNode(currentNode); - return { - status: "success", - query: text, - case_sensitive: caseSensitive, - whole_word: wholeWord, - matches: results.length, - results: results, - viewport: { - width: window.innerWidth, - height: window.innerHeight, - scroll_x: window.scrollX, - scroll_y: window.scrollY - } + + diagnostics = { + metrics: { + ready_state: document.readyState || null, + quiet_ms: quietMs, + node_count: nodeCount, + }, + captcha: detectCaptcha(), + requires_vision: requiresVision, + requires_vision_reason: requiresVisionReason, }; + } catch (e) { + // ignore + } + + return { + status: 'success', + url: window.location.href, + viewport: { + width: window.innerWidth, + height: window.innerHeight, + }, + elements: cleanedElements, + raw_elements: cleanedRawElements, + screenshot, + diagnostics, + }; + } catch (error) { + console.error('[SentienceAPI] snapshot() failed:', error); + console.error('[SentienceAPI] Error stack:', error.stack); + return { + status: 'error', + error: error.message || 'Unknown error', + stack: error.stack, + }; } - function click(id) { - const el = window.sentience_registry[id]; - return !!el && (el.click(), el.focus(), !0); + } + + // read.js - Content Reading Methods + + // 2. Read Content (unchanged) + function read(options = {}) { + const format = options.format || 'raw'; + let content; + + if (format === 'raw') { + content = getRawHTML(document.body); + } else if (format === 'markdown') { + content = convertToMarkdown(document.body); + } else { + content = convertToText(document.body); } - function startRecording(options = {}) { - const {highlightColor: highlightColor = "#ff0000", successColor: successColor = "#00ff00", autoDisableTimeout: autoDisableTimeout = 18e5, keyboardShortcut: keyboardShortcut = "Ctrl+Shift+I"} = options; - if (!window.sentience_registry || 0 === window.sentience_registry.length) return alert("Registry empty. Run `await window.sentience.snapshot()` first!"), - () => {}; - window.sentience_registry_map = new Map, window.sentience_registry.forEach((el, idx) => { - el && window.sentience_registry_map.set(el, idx); - }); - let highlightBox = document.getElementById("sentience-highlight-box"); - highlightBox || (highlightBox = document.createElement("div"), highlightBox.id = "sentience-highlight-box", - highlightBox.style.cssText = `\n position: fixed;\n pointer-events: none;\n z-index: 2147483647;\n border: 2px solid ${highlightColor};\n background: rgba(255, 0, 0, 0.1);\n display: none;\n transition: all 0.1s ease;\n box-sizing: border-box;\n `, - document.body.appendChild(highlightBox)); - let recordingIndicator = document.getElementById("sentience-recording-indicator"); - recordingIndicator || (recordingIndicator = document.createElement("div"), recordingIndicator.id = "sentience-recording-indicator", - recordingIndicator.style.cssText = `\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n height: 3px;\n background: ${highlightColor};\n z-index: 2147483646;\n pointer-events: none;\n `, - document.body.appendChild(recordingIndicator)), recordingIndicator.style.display = "block"; - const mouseOverHandler = e => { - const el = e.target; - if (!el || el === highlightBox || el === recordingIndicator) return; - const rect = el.getBoundingClientRect(); - highlightBox.style.display = "block", highlightBox.style.top = rect.top + window.scrollY + "px", - highlightBox.style.left = rect.left + window.scrollX + "px", highlightBox.style.width = rect.width + "px", - highlightBox.style.height = rect.height + "px"; - }, clickHandler = e => { - e.preventDefault(), e.stopPropagation(); - const el = e.target; - if (!el || el === highlightBox || el === recordingIndicator) return; - const sentienceId = window.sentience_registry_map.get(el); - if (void 0 === sentienceId) return void alert("Element not in registry. Run `await window.sentience.snapshot()` first!"); - const rawData = function(el) { - const style = window.getComputedStyle(el), rect = el.getBoundingClientRect(); - return { - tag: el.tagName, - rect: { - x: Math.round(rect.x), - y: Math.round(rect.y), - width: Math.round(rect.width), - height: Math.round(rect.height) - }, - styles: { - cursor: style.cursor || null, - backgroundColor: style.backgroundColor || null, - color: style.color || null, - fontWeight: style.fontWeight || null, - fontSize: style.fontSize || null, - display: style.display || null, - position: style.position || null, - zIndex: style.zIndex || null, - opacity: style.opacity || null, - visibility: style.visibility || null - }, - attributes: { - role: el.getAttribute("role") || null, - type: el.getAttribute("type") || null, - ariaLabel: el.getAttribute("aria-label") || null, - id: el.id || null, - className: el.className || null - } - }; - }(el), selector = function(el) { - if (!el || !el.tagName) return ""; - if (el.id) return `#${el.id}`; - for (const attr of el.attributes) if (attr.name.startsWith("data-") || "aria-label" === attr.name) { - const value = attr.value ? attr.value.replace(/"/g, '\\"') : ""; - return `${el.tagName.toLowerCase()}[${attr.name}="${value}"]`; - } - const path = []; - let current = el; - for (;current && current !== document.body && current !== document.documentElement; ) { - let selector = current.tagName.toLowerCase(); - if (current.id) { - selector = `#${current.id}`, path.unshift(selector); - break; - } - if (current.className && "string" == typeof current.className) { - const classes = current.className.trim().split(/\s+/).filter(c => c); - classes.length > 0 && (selector += `.${classes[0]}`); - } - if (current.parentElement) { - const sameTagSiblings = Array.from(current.parentElement.children).filter(s => s.tagName === current.tagName), index = sameTagSiblings.indexOf(current); - (index > 0 || sameTagSiblings.length > 1) && (selector += `:nth-of-type(${index + 1})`); - } - path.unshift(selector), current = current.parentElement; - } - return path.join(" > ") || el.tagName.toLowerCase(); - }(el), role = el.getAttribute("role") || el.tagName.toLowerCase(), text = getText(el), snippet = { - task: `Interact with ${text.substring(0, 20)}${text.length > 20 ? "..." : ""}`, - url: window.location.href, - timestamp: (new Date).toISOString(), - target_criteria: { - id: sentienceId, - selector: selector, - role: role, - text: text.substring(0, 50) - }, - debug_snapshot: rawData - }, jsonString = JSON.stringify(snippet, null, 2); - navigator.clipboard.writeText(jsonString).then(() => { - highlightBox.style.border = `2px solid ${successColor}`, highlightBox.style.background = "rgba(0, 255, 0, 0.2)", - setTimeout(() => { - highlightBox.style.border = `2px solid ${highlightColor}`, highlightBox.style.background = "rgba(255, 0, 0, 0.1)"; - }, 500); - }).catch(err => { - alert("Failed to copy to clipboard. Check console for JSON."); + + return { + status: 'success', + url: window.location.href, + format, + content, + length: content.length, + }; + } + + // 2b. Find Text Rectangle - Get exact pixel coordinates of specific text + function findTextRect(options = {}) { + const { + text, + containerElement = document.body, + caseSensitive = false, + wholeWord = false, + maxResults = 10, + } = options; + + if (!text || text.trim().length === 0) { + return { + status: 'error', + error: 'Text parameter is required', + }; + } + + const results = []; + const searchText = caseSensitive ? text : text.toLowerCase(); + + // Helper function to find text in a single text node + function findInTextNode(textNode) { + const nodeText = textNode.nodeValue; + const searchableText = caseSensitive ? nodeText : nodeText.toLowerCase(); + + let startIndex = 0; + while (startIndex < nodeText.length && results.length < maxResults) { + const foundIndex = searchableText.indexOf(searchText, startIndex); + + if (foundIndex === -1) break; + + // Check whole word matching if required + if (wholeWord) { + const before = foundIndex > 0 ? nodeText[foundIndex - 1] : ' '; + const after = + foundIndex + text.length < nodeText.length ? nodeText[foundIndex + text.length] : ' '; + + // Check if surrounded by word boundaries + if (!/\s/.test(before) || !/\s/.test(after)) { + startIndex = foundIndex + 1; + continue; + } + } + + try { + // Create range for this occurrence + const range = document.createRange(); + range.setStart(textNode, foundIndex); + range.setEnd(textNode, foundIndex + text.length); + + const rect = range.getBoundingClientRect(); + + // Only include visible rectangles + if (rect.width > 0 && rect.height > 0) { + results.push({ + text: nodeText.substring(foundIndex, foundIndex + text.length), + rect: { + x: rect.left + window.scrollX, + y: rect.top + window.scrollY, + width: rect.width, + height: rect.height, + left: rect.left + window.scrollX, + top: rect.top + window.scrollY, + right: rect.right + window.scrollX, + bottom: rect.bottom + window.scrollY, + }, + viewport_rect: { + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height, + }, + context: { + before: nodeText.substring(Math.max(0, foundIndex - 20), foundIndex), + after: nodeText.substring( + foundIndex + text.length, + Math.min(nodeText.length, foundIndex + text.length + 20) + ), + }, + in_viewport: + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= window.innerHeight && + rect.right <= window.innerWidth, }); - }; - let timeoutId = null; - const stopRecording = () => { - document.removeEventListener("mouseover", mouseOverHandler, !0), document.removeEventListener("click", clickHandler, !0), - document.removeEventListener("keydown", keyboardHandler, !0), timeoutId && (clearTimeout(timeoutId), - timeoutId = null), highlightBox && (highlightBox.style.display = "none"), recordingIndicator && (recordingIndicator.style.display = "none"), - window.sentience_registry_map && window.sentience_registry_map.clear(), window.sentience_stopRecording === stopRecording && delete window.sentience_stopRecording; - }, keyboardHandler = e => { - (e.ctrlKey || e.metaKey) && e.shiftKey && "I" === e.key && (e.preventDefault(), - stopRecording()); - }; - return document.addEventListener("mouseover", mouseOverHandler, !0), document.addEventListener("click", clickHandler, !0), - document.addEventListener("keydown", keyboardHandler, !0), autoDisableTimeout > 0 && (timeoutId = setTimeout(() => { - stopRecording(); - }, autoDisableTimeout)), window.sentience_stopRecording = stopRecording, stopRecording; - } - function showOverlay(elements, targetElementId = null) { - elements && Array.isArray(elements) && window.postMessage({ - type: "SENTIENCE_SHOW_OVERLAY", - elements: elements, - targetElementId: targetElementId, - timestamp: Date.now() - }, "*"); - } - function showGrid(grids, targetGridId = null) { - grids && Array.isArray(grids) && window.postMessage({ - type: "SENTIENCE_SHOW_GRID_OVERLAY", - grids: grids, - targetGridId: targetGridId, - timestamp: Date.now() - }, "*"); - } - function clearOverlay() { - window.postMessage({ - type: "SENTIENCE_CLEAR_OVERLAY" - }, "*"); - } - (async () => { - const getExtensionId = () => document.documentElement.dataset.sentienceExtensionId; - let extId = getExtensionId(); - extId || await new Promise(resolve => { - const check = setInterval(() => { - extId = getExtensionId(), extId && (clearInterval(check), resolve()); - }, 50); - setTimeout(() => resolve(), 5e3); - }), extId && (window.sentience_registry = [], window.sentience = { - snapshot: snapshot, - read: read, - findTextRect: findTextRect, - click: click, - startRecording: startRecording, - showOverlay: showOverlay, - showGrid: showGrid, - clearOverlay: clearOverlay - }, window.sentience_iframe_handler_setup || (window.addEventListener("message", async event => { - if ("SENTIENCE_IFRAME_SNAPSHOT_REQUEST" === event.data?.type) { - const {requestId: requestId, options: options} = event.data; - try { - const snapshotOptions = { - ...options, - collectIframes: !0, - waitForStability: (options.waitForStability, !1) - }, snapshot = await window.sentience.snapshot(snapshotOptions); - event.source && event.source.postMessage && event.source.postMessage({ - type: "SENTIENCE_IFRAME_SNAPSHOT_RESPONSE", - requestId: requestId, - snapshot: snapshot, - error: null - }, "*"); - } catch (error) { - event.source && event.source.postMessage && event.source.postMessage({ - type: "SENTIENCE_IFRAME_SNAPSHOT_RESPONSE", - requestId: requestId, - snapshot: null, - error: error.message - }, "*"); - } - } - }), window.sentience_iframe_handler_setup = !0)); - })(); -}(); \ No newline at end of file + } + } catch (e) { + console.warn('[SentienceAPI] Failed to get rect for text:', e); + } + + startIndex = foundIndex + 1; + } + } + + // Tree walker to find all text nodes + const walker = document.createTreeWalker(containerElement, NodeFilter.SHOW_TEXT, { + acceptNode(node) { + // Skip script, style, and empty text nodes + const parent = node.parentElement; + if (!parent) return NodeFilter.FILTER_REJECT; + + const tagName = parent.tagName.toLowerCase(); + if (tagName === 'script' || tagName === 'style' || tagName === 'noscript') { + return NodeFilter.FILTER_REJECT; + } + + // Skip whitespace-only nodes + if (!node.nodeValue || node.nodeValue.trim().length === 0) { + return NodeFilter.FILTER_REJECT; + } + + // Check if element is visible + const computedStyle = window.getComputedStyle(parent); + if ( + computedStyle.display === 'none' || + computedStyle.visibility === 'hidden' || + computedStyle.opacity === '0' + ) { + return NodeFilter.FILTER_REJECT; + } + + return NodeFilter.FILTER_ACCEPT; + }, + }); + + // Walk through all text nodes + let currentNode; + while ((currentNode = walker.nextNode()) && results.length < maxResults) { + findInTextNode(currentNode); + } + + return { + status: 'success', + query: text, + case_sensitive: caseSensitive, + whole_word: wholeWord, + matches: results.length, + results, + viewport: { + width: window.innerWidth, + height: window.innerHeight, + scroll_x: window.scrollX, + scroll_y: window.scrollY, + }, + }; + } + + // click.js - Click Action Method + + // 3. Click Action (unchanged) + function click(id) { + const el = window.sentience_registry[id]; + if (el) { + el.click(); + el.focus(); + return true; + } + return false; + } + + // registry.js - Inspector Mode / Golden Set Collection + + // 4. Inspector Mode: Start Recording for Golden Set Collection + function startRecording(options = {}) { + const { + highlightColor = '#ff0000', + successColor = '#00ff00', + autoDisableTimeout = 30 * 60 * 1000, // 30 minutes default + keyboardShortcut = 'Ctrl+Shift+I', + } = options; + + console.log( + '🔴 [Sentience] Recording Mode STARTED. Click an element to copy its Ground Truth JSON.' + ); + console.log(` Press ${keyboardShortcut} or call stopRecording() to stop.`); + + // Validate registry is populated + if (!window.sentience_registry || window.sentience_registry.length === 0) { + console.warn( + '⚠️ Registry empty. Call `await window.sentience.snapshot()` first to populate registry.' + ); + alert('Registry empty. Run `await window.sentience.snapshot()` first!'); + return () => {}; // Return no-op cleanup function + } + + // Create reverse mapping for O(1) lookup (fixes registry lookup bug) + window.sentience_registry_map = new Map(); + window.sentience_registry.forEach((el, idx) => { + if (el) window.sentience_registry_map.set(el, idx); + }); + + // Create highlight box overlay + let highlightBox = document.getElementById('sentience-highlight-box'); + if (!highlightBox) { + highlightBox = document.createElement('div'); + highlightBox.id = 'sentience-highlight-box'; + highlightBox.style.cssText = ` + position: fixed; + pointer-events: none; + z-index: 2147483647; + border: 2px solid ${highlightColor}; + background: rgba(255, 0, 0, 0.1); + display: none; + transition: all 0.1s ease; + box-sizing: border-box; + `; + document.body.appendChild(highlightBox); + } + + // Create visual indicator (red border on page when recording) + let recordingIndicator = document.getElementById('sentience-recording-indicator'); + if (!recordingIndicator) { + recordingIndicator = document.createElement('div'); + recordingIndicator.id = 'sentience-recording-indicator'; + recordingIndicator.style.cssText = ` + position: fixed; + top: 0; + left: 0; + right: 0; + height: 3px; + background: ${highlightColor}; + z-index: 2147483646; + pointer-events: none; + `; + document.body.appendChild(recordingIndicator); + } + recordingIndicator.style.display = 'block'; + + // Hover handler (visual feedback) + const mouseOverHandler = (e) => { + const el = e.target; + if (!el || el === highlightBox || el === recordingIndicator) return; + + const rect = el.getBoundingClientRect(); + highlightBox.style.display = 'block'; + highlightBox.style.top = rect.top + window.scrollY + 'px'; + highlightBox.style.left = rect.left + window.scrollX + 'px'; + highlightBox.style.width = rect.width + 'px'; + highlightBox.style.height = rect.height + 'px'; + }; + + // Click handler (capture ground truth data) + const clickHandler = (e) => { + e.preventDefault(); + e.stopPropagation(); + + const el = e.target; + if (!el || el === highlightBox || el === recordingIndicator) return; + + // Use Map for reliable O(1) lookup + const sentienceId = window.sentience_registry_map.get(el); + if (sentienceId === undefined) { + console.warn('⚠️ Element not found in Sentience Registry. Did you run snapshot() first?'); + alert('Element not in registry. Run `await window.sentience.snapshot()` first!'); + return; + } + + // Extract raw data (ground truth + raw signals, NOT model outputs) + const rawData = extractRawElementData(el); + const selector = getUniqueSelector(el); + const role = el.getAttribute('role') || el.tagName.toLowerCase(); + const text = getText(el); + + // Build golden set JSON (ground truth + raw signals only) + const snippet = { + task: `Interact with ${text.substring(0, 20)}${text.length > 20 ? '...' : ''}`, + url: window.location.href, + timestamp: new Date().toISOString(), + target_criteria: { + id: sentienceId, + selector, + role, + text: text.substring(0, 50), + }, + debug_snapshot: rawData, + }; + + // Copy to clipboard + const jsonString = JSON.stringify(snippet, null, 2); + navigator.clipboard + .writeText(jsonString) + .then(() => { + console.log('✅ Copied Ground Truth to clipboard:', snippet); + + // Flash green to indicate success + highlightBox.style.border = `2px solid ${successColor}`; + highlightBox.style.background = 'rgba(0, 255, 0, 0.2)'; + setTimeout(() => { + highlightBox.style.border = `2px solid ${highlightColor}`; + highlightBox.style.background = 'rgba(255, 0, 0, 0.1)'; + }, 500); + }) + .catch((err) => { + console.error('❌ Failed to copy to clipboard:', err); + alert('Failed to copy to clipboard. Check console for JSON.'); + }); + }; + + // Auto-disable timeout + let timeoutId = null; + + // Cleanup function to stop recording (defined before use) + const stopRecording = () => { + document.removeEventListener('mouseover', mouseOverHandler, true); + document.removeEventListener('click', clickHandler, true); + document.removeEventListener('keydown', keyboardHandler, true); + + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + + if (highlightBox) { + highlightBox.style.display = 'none'; + } + + if (recordingIndicator) { + recordingIndicator.style.display = 'none'; + } + + // Clean up registry map (optional, but good practice) + if (window.sentience_registry_map) { + window.sentience_registry_map.clear(); + } + + // Remove global reference + if (window.sentience_stopRecording === stopRecording) { + delete window.sentience_stopRecording; + } + + console.log('⚪ [Sentience] Recording Mode STOPPED.'); + }; + + // Keyboard shortcut handler (defined after stopRecording) + const keyboardHandler = (e) => { + // Ctrl+Shift+I or Cmd+Shift+I + if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'I') { + e.preventDefault(); + stopRecording(); + } + }; + + // Attach event listeners (use capture phase to intercept early) + document.addEventListener('mouseover', mouseOverHandler, true); + document.addEventListener('click', clickHandler, true); + document.addEventListener('keydown', keyboardHandler, true); + + // Set up auto-disable timeout + if (autoDisableTimeout > 0) { + timeoutId = setTimeout(() => { + console.log('⏰ [Sentience] Recording Mode auto-disabled after timeout.'); + stopRecording(); + }, autoDisableTimeout); + } + + // Store stop function globally for keyboard shortcut access + window.sentience_stopRecording = stopRecording; + + return stopRecording; + } + + // overlay.js - Visual Overlay Methods + + /** + * Show overlay highlighting specific elements with Shadow DOM + * @param {Array} elements - List of elements with bbox, importance, visual_cues + * @param {number} targetElementId - Optional ID of target element (shown in red) + */ + function showOverlay(elements, targetElementId = null) { + if (!elements || !Array.isArray(elements)) { + console.warn('[Sentience] showOverlay: elements must be an array'); + return; + } + + window.postMessage( + { + type: 'SENTIENCE_SHOW_OVERLAY', + elements, + targetElementId, + timestamp: Date.now(), + }, + '*' + ); + + console.log(`[Sentience] Overlay requested for ${elements.length} elements`); + } + + /** + * Show grid overlay highlighting detected grids + * @param {Array} grids - Array of GridInfo objects from SDK's get_grid_bounds() + * @param {number|null} targetGridId - Optional grid ID to highlight in red + */ + function showGrid(grids, targetGridId = null) { + if (!grids || !Array.isArray(grids)) { + console.warn('[Sentience] showGrid: grids must be an array'); + return; + } + + window.postMessage( + { + type: 'SENTIENCE_SHOW_GRID_OVERLAY', + grids, + targetGridId, + timestamp: Date.now(), + }, + '*' + ); + + console.log(`[Sentience] Grid overlay requested for ${grids.length} grids`); + } + + /** + * Clear overlay manually + */ + function clearOverlay() { + window.postMessage( + { + type: 'SENTIENCE_CLEAR_OVERLAY', + }, + '*' + ); + console.log('[Sentience] Overlay cleared'); + } + + // index.js - Main Entry Point for Injected API + // This script ONLY collects raw DOM data and sends it to background for processing + + + (async () => { + // console.log('[SentienceAPI] Initializing (CSP-Resistant Mode)...'); + + // Wait for Extension ID from content.js + const getExtensionId = () => document.documentElement.dataset.sentienceExtensionId; + let extId = getExtensionId(); + + if (!extId) { + await new Promise((resolve) => { + const check = setInterval(() => { + extId = getExtensionId(); + if (extId) { + clearInterval(check); + resolve(); + } + }, 50); + setTimeout(() => resolve(), 5000); // Max 5s wait + }); + } + + if (!extId) { + console.error('[SentienceAPI] Failed to get extension ID'); + return; + } + + // console.log('[SentienceAPI] Extension ID:', extId); + + // Registry for click actions (still needed for click() function) + window.sentience_registry = []; + + // --- GLOBAL API --- + window.sentience = { + snapshot, + read, + findTextRect, + click, + startRecording, + showOverlay, + showGrid, + clearOverlay, + }; + + // Setup iframe handler when script loads (only once) + if (!window.sentience_iframe_handler_setup) { + setupIframeSnapshotHandler(); + window.sentience_iframe_handler_setup = true; + } + + console.log('[SentienceAPI] ✓ Ready! (CSP-Resistant - WASM runs in background)'); + })(); + +})();