Spaces:
Sleeping
Sleeping
File size: 7,089 Bytes
9548e93 74baa7b 9548e93 74baa7b cbbb114 74baa7b cbbb114 74baa7b cbbb114 74baa7b cbbb114 74baa7b cbbb114 74baa7b cbbb114 9548e93 cbbb114 9548e93 cbbb114 9548e93 cbbb114 9548e93 cbbb114 74baa7b 9548e93 cbbb114 9548e93 cbbb114 9548e93 cbbb114 9548e93 cbbb114 9548e93 cbbb114 74baa7b 9548e93 cbbb114 9548e93 74baa7b 9548e93 cbbb114 9548e93 cbbb114 9548e93 cbbb114 9548e93 cbbb114 9548e93 cbbb114 9548e93 cbbb114 9548e93 74baa7b 9548e93 cbbb114 9548e93 cbbb114 9548e93 cbbb114 9548e93 cbbb114 9548e93 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | /**
* 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 }); }
});
})();
|