/** * ClauseGuard — Content Script * Page scanning + highlighting + auth bridge. * * Auth bridge: listens for postMessage from the website's ExtensionBridge component. * Content scripts CAN receive window.postMessage from the page — they share the same * window object. The message handler checks event.source === window to ensure it's * from the same page (not an iframe). */ (() => { "use strict"; if (!document.body || document.contentType !== "text/html") return; let lastScannedText = ""; let isScanning = false; let currentHighlights = []; // ─── Auth Bridge ─── // Listen for auth sync from our website (ExtensionBridge component sends this) window.addEventListener("message", (event) => { // Only accept from same window (not iframes) if (event.source !== window) return; if (!event.data || event.data.type !== "CLAUSEGUARD_AUTH_SYNC") return; const { token, email, name, userId, plan } = event.data; if (token) { // User is logged in — store auth chrome.storage.sync.set({ authToken: token, email: email || "", userName: name || "", userId: userId || "", plan: plan || "free", authSource: "website", lastSyncAt: Date.now(), }, () => { console.log("ClauseGuard: auth synced from website —", email, plan); }); } else { // User logged out — clear auth chrome.storage.sync.remove( ["authToken", "email", "userName", "userId", "plan", "authSource", "lastSyncAt"], () => { console.log("ClauseGuard: auth cleared (logout)"); } ); } }); // ─── Extract page text ─── function extractPageText() { const clone = document.body.cloneNode(true); ["script","style","nav","footer","header","aside","noscript","iframe","svg"] .forEach(tag => clone.querySelectorAll(tag).forEach(el => el.remove())); clone.querySelectorAll('[class*="cookie"],[class*="banner"],[class*="modal"],[id*="cookie"]') .forEach(el => el.remove()); return clone.innerText || clone.textContent || ""; } // ─── Scan ─── async function scanPage() { if (isScanning) return; isScanning = true; const text = extractPageText(); if (!text || text.length < 100 || text === lastScannedText) { isScanning = false; return; } lastScannedText = text; try { const results = await chrome.runtime.sendMessage({ type: "ANALYZE_TEXT", payload: { text, url: window.location.href } }); if (results && !results.error) { clearHighlights(); highlightResults(results); } } catch (err) { /* extension context invalidated — ignore */ } isScanning = false; } // ─── Highlight ─── function highlightResults(results) { if (!results.results) return; results.results.filter(r => r.categories?.length > 0).forEach(item => { if (item.text.trim().length < 20) return; findTextNodes(document.body, item.text).forEach(({ node, startOffset, endOffset }) => { wrapTextRange(node, startOffset, endOffset, item); }); }); } function findTextNodes(root, searchText) { const results = []; const searchLower = searchText.toLowerCase().substring(0, 80); const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode(node) { if (node.parentElement?.closest(".clauseguard-highlight, script, style, nav")) return NodeFilter.FILTER_REJECT; return node.textContent.length > 20 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; }, }); let node; while ((node = walker.nextNode())) { const idx = node.textContent.toLowerCase().indexOf(searchLower); if (idx !== -1) { results.push({ node, startOffset: idx, endOffset: Math.min(idx + searchText.length, node.textContent.length) }); break; } } return results; } function wrapTextRange(textNode, start, end, clauseData) { try { const range = document.createRange(); range.setStart(textNode, start); range.setEnd(textNode, end); const severity = clauseData.categories.reduce((m, c) => ({ HIGH:3,MEDIUM:2,LOW:1 }[c.severity] > ({ HIGH:3,MEDIUM:2,LOW:1 }[m]) ? c.severity : m), "LOW"); const mark = document.createElement("mark"); mark.className = `clauseguard-highlight clauseguard-${severity.toLowerCase()}`; mark.dataset.categories = JSON.stringify(clauseData.categories); mark.addEventListener("mouseenter", showTooltip); mark.addEventListener("mouseleave", hideTooltip); mark.addEventListener("click", () => { try { chrome.runtime.sendMessage({ type: "OPEN_SIDEPANEL" }); } catch {} }); range.surroundContents(mark); currentHighlights.push(mark); } catch (e) {} } // ─── Tooltip ─── let tooltipEl = null; function showTooltip(e) { hideTooltip(); const cats = JSON.parse(e.currentTarget.dataset.categories || "[]"); if (!cats.length) return; tooltipEl = document.createElement("div"); tooltipEl.className = "clauseguard-tooltip"; tooltipEl.innerHTML = `
ClauseGuard
` + cats.map(c => `
${c.severity}${c.name}
`).join("") + ``; document.body.appendChild(tooltipEl); const rect = e.currentTarget.getBoundingClientRect(); tooltipEl.style.left = `${rect.left + window.scrollX}px`; tooltipEl.style.top = `${rect.bottom + window.scrollY + 8}px`; } function hideTooltip() { if (tooltipEl) { tooltipEl.remove(); tooltipEl = null; } } function clearHighlights() { currentHighlights.forEach(mark => { const p = mark.parentNode; if (p) { while (mark.firstChild) p.insertBefore(mark.firstChild, mark); p.removeChild(mark); } }); currentHighlights = []; } // ─── Auto-scan (debounced) ─── let scanTimeout = null; function debouncedScan() { clearTimeout(scanTimeout); scanTimeout = setTimeout(scanPage, 1500); } if (document.readyState === "complete") debouncedScan(); else window.addEventListener("load", debouncedScan); const observer = new MutationObserver(mutations => { if (mutations.some(m => m.addedNodes.length > 0 && [...m.addedNodes].some(n => n.nodeType === 1 && n.textContent?.length > 100))) debouncedScan(); }); observer.observe(document.body, { childList: true, subtree: true }); // ─── Messages from popup/sidepanel ─── chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg.type === "TRIGGER_SCAN") { lastScannedText = ""; scanPage().then(() => sendResponse({ done: true })); return true; } if (msg.type === "CLEAR_HIGHLIGHTS") { clearHighlights(); lastScannedText = ""; sendResponse({ done: true }); } if (msg.type === "GET_PAGE_TEXT") { sendResponse({ text: extractPageText(), url: window.location.href }); } }); })();