/** * ClauseGuard — Background Service Worker v3.0 * FIXED: API payload now sends {text, source_url} (not {clauses}) * FIXED: Error handling and retry logic */ // FIX v4.2: Corrected API_BASE URL to match the actual Gradio Space const API_BASE = "https://gaurv007-clauseguard.hf.space"; const FREE_SCANS_PER_MONTH = 10; const API_TIMEOUT_MS = 45000; const SITE_ORIGINS = [ "https://clauseguardweb.netlify.app", ]; try { chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); } catch(e) {} // ─── Fetch with timeout ─── async function fetchWithTimeout(url, options, timeoutMs) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const resp = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timer); return resp; } catch (err) { clearTimeout(timer); throw err; } } // ─── Message Router ─── chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { const handle = async () => { switch (message.type) { case "ANALYZE_TEXT": return await handleAnalyze(message.payload, sender.tab?.id); case "GET_AUTH": return await getAuth(); case "GET_USER": return await getUser(); case "CHECK_USAGE": return await checkUsage(); case "OPEN_SIDEPANEL": if (sender.tab?.id) chrome.sidePanel.open({ tabId: sender.tab.id }); return { ok: true }; case "GET_RESULTS": return await getStoredResults(sender.tab?.id || message.tabId); case "SYNC_AUTH": return await syncAuthFromWebsite(); case "GET_SCAN_HISTORY": return await getScanHistory(); default: return null; } }; handle().then(sendResponse).catch(err => sendResponse({ error: err.message })); return true; }); // ─── External messages from website ─── chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => { const handle = async () => { switch (message.type) { case "SET_AUTH": { await chrome.storage.sync.set({ authToken: message.token, plan: message.plan || "free", email: message.email || "", userName: message.name || "", userId: message.userId || "", authSource: "website", lastSyncAt: Date.now(), }); return { success: true }; } case "LOGOUT": { await chrome.storage.sync.remove(["authToken", "plan", "email", "userName", "userId", "authSource", "lastSyncAt"]); return { success: true }; } case "GET_STATUS": { const auth = await getAuth(); const usage = await checkUsage(); return { auth, usage }; } case "PING": { return { installed: true, version: chrome.runtime.getManifest().version }; } default: return null; } }; handle().then(sendResponse).catch(err => sendResponse({ error: err.message })); return true; }); // ─── Core: Analyze ─── async function handleAnalyze(payload, tabId) { const usage = await checkUsage(); if (!usage.allowed) { return { error: "limit_reached", message: "Free scan limit reached.", usage }; } const { text, url } = payload; if (!text || text.trim().length < 100) { return { error: "too_short", message: "Not enough text to analyze." }; } let results; try { const auth = await getAuth(); // FIXED: Send {text, source_url} not {clauses} const resp = await fetchWithTimeout(`${API_BASE}/api/analyze`, { method: "POST", headers: { "Content-Type": "application/json", ...(auth.token ? { Authorization: `Bearer ${auth.token}` } : {}), }, body: JSON.stringify({ text: text.substring(0, 100000), source_url: url }), }, API_TIMEOUT_MS); if (resp.status === 429) { return { error: "rate_limited", message: "Too many requests. Please wait a moment." }; } if (!resp.ok) throw new Error(`HTTP ${resp.status}`); results = await resp.json(); results.source = "api"; } catch (err) { console.warn("API unavailable, using local:", err.message); results = localAnalyze(text); results.source = "local"; } // Store results if (tabId) { await chrome.storage.local.set({ [`results_${tabId}`]: results }); const flagged = results.results?.filter(r => r.categories?.length > 0).length || results.flagged_count || 0; chrome.action.setBadgeText({ text: flagged > 0 ? String(flagged) : "", tabId }); if (flagged > 0) chrome.action.setBadgeBackgroundColor({ color: flagged > 3 ? "#ef4444" : "#f59e0b", tabId }); } // Save scan to history const scanRecord = { url: url || "", risk_score: results.risk_score, grade: results.grade, flagged_count: results.flagged_count, total_clauses: results.total_clauses, source: results.source, scanned_at: Date.now(), }; const { scanHistory = [] } = await chrome.storage.local.get("scanHistory"); scanHistory.unshift(scanRecord); if (scanHistory.length > 50) scanHistory.length = 50; await chrome.storage.local.set({ scanHistory }); await incrementUsage(); return results; } // ─── Get scan history ─── async function getScanHistory() { const { scanHistory = [] } = await chrome.storage.local.get("scanHistory"); return { history: scanHistory }; } // ─── Sync auth from website ─── async function syncAuthFromWebsite() { return await getAuth(); } // ─── Local fallback ─── function localAnalyze(text) { const clauses = splitIntoClauses(text); const patterns = { 0: [/arbitrat/i, /binding arbitration/i, /waive.*right.*court/i], 1: [/sole discretion/i, /reserves? the right to (modify|change|update)/i, /at any time.*without.*notice/i], 2: [/remove.*content.*without/i, /right to remove/i], 3: [/exclusive jurisdiction/i, /courts? of.*(california|delaware|new york|ireland)/i, /submit to.*jurisdiction/i], 4: [/governed by.*laws? of/i, /shall be governed/i], 5: [/not liable/i, /shall not be (liable|responsible)/i, /in no event.*liable/i, /limitation of liability/i, /without warranty/i], 6: [/terminat.*at any time/i, /suspend.*account.*without/i, /we may (terminat|suspend)/i], 7: [/by (using|accessing).*you agree/i, /continued use.*constitutes/i], }; const names = ["Arbitration","Unilateral change","Content removal","Jurisdiction","Choice of law","Limitation of liability","Unilateral termination","Contract by using"]; const sevMap = {0:"HIGH",1:"MEDIUM",2:"MEDIUM",3:"MEDIUM",4:"MEDIUM",5:"HIGH",6:"HIGH",7:"LOW"}; const results = clauses.map(clause => { const categories = []; for (const [id, pats] of Object.entries(patterns)) { if (pats.some(p => p.test(clause))) categories.push({ name: names[id], severity: sevMap[id] }); } return { text: clause, categories }; }); const flagged = results.filter(r => r.categories.length > 0); const sev = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 }; flagged.forEach(r => r.categories.forEach(c => { if (sev.hasOwnProperty(c.severity)) sev[c.severity]++; else sev.MEDIUM++; // default for unknown severity })); // FIX v4.2: Use the same diminishing-returns formula as the backend (app.py) // instead of normalizing by clause count (which gave different scores) const weighted = sev.CRITICAL*40 + sev.HIGH*20 + sev.MEDIUM*10 + sev.LOW*3; const risk = Math.min(100, Math.round(100 * (1 - (1 / (1 + weighted / 30))))); return { risk_score: risk, grade: risk >= 70 ? "F" : risk >= 50 ? "D" : risk >= 30 ? "C" : risk >= 15 ? "B" : "A", total_clauses: clauses.length, flagged_count: flagged.length, results, }; } // ─── Utils ─── function splitIntoClauses(text) { return text.replace(/\n{2,}/g, "\n").trim() .split(/(?<=[.!?])\s+(?=[A-Z0-9(])|(?:\n)(?=\d+[.)]\s|\([a-z]\)\s)/) .map(c => c.trim()).filter(c => c.length > 30); } async function getAuth() { return new Promise(r => chrome.storage.sync.get( ["authToken","plan","email","userName","userId","authSource","lastSyncAt"], d => r({ token: d.authToken||null, plan: d.plan||"free", email: d.email||null, name: d.userName||null, userId: d.userId||null, isLoggedIn: !!d.authToken, isPro: d.plan==="pro"||d.plan==="team", isGuest: !d.authToken, authSource: d.authSource||null, lastSyncAt: d.lastSyncAt||null, }))); } async function getUser() { const auth = await getAuth(); const usage = await checkUsage(); return { ...auth, ...usage }; } async function checkUsage() { return new Promise(r => chrome.storage.sync.get(["plan","scansThisMonth","monthResetAt"], d => { const now = new Date(), reset = d.monthResetAt ? new Date(d.monthResetAt) : null; if (!reset || now.getMonth() !== reset.getMonth() || now.getFullYear() !== reset.getFullYear()) { chrome.storage.sync.set({ scansThisMonth: 0, monthResetAt: now.toISOString() }); r({ allowed: true, used: 0, limit: FREE_SCANS_PER_MONTH, plan: d.plan||"free" }); return; } const used = d.scansThisMonth||0, plan = d.plan||"free"; r({ allowed: plan!=="free"||used chrome.storage.sync.get(["scansThisMonth"], d => chrome.storage.sync.set({ scansThisMonth: (d.scansThisMonth||0)+1 }, r))); } async function getStoredResults(tabId) { return new Promise(r => chrome.storage.local.get([`results_${tabId}`], d => r(d[`results_${tabId}`]||null))); } chrome.tabs.onRemoved.addListener(tabId => chrome.storage.local.remove([`results_${tabId}`]));