ClauseGuard / extension /content.js
gaurv007's picture
Fix auth sync: handle INITIAL_SESSION + TOKEN_REFRESHED events, sync on tab visibility change, remove flaky scripting.executeScript approach, add console.log for debugging
74baa7b verified
raw
history blame
7.09 kB
/**
* 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 = `<div class="clauseguard-tooltip-header">ClauseGuard</div>` +
cats.map(c => `<div class="clauseguard-tooltip-item"><span class="clauseguard-tooltip-badge clauseguard-badge-${c.severity.toLowerCase()}">${c.severity}</span><span>${c.name}</span></div>`).join("") +
`<div class="clauseguard-tooltip-footer">Click for details</div>`;
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 }); }
});
})();