Spaces:
Sleeping
Sleeping
| /** | |
| * ClauseGuard β Background Service Worker v3.0 | |
| * FIXED: API payload now sends {text, source_url} (not {clauses}) | |
| * FIXED: Error handling and retry logic | |
| */ | |
| const API_BASE = "https://gaurv007-clauseguard-api.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 = { HIGH: 0, MEDIUM: 0, LOW: 0 }; | |
| flagged.forEach(r => r.categories.forEach(c => sev[c.severity]++)); | |
| const risk = Math.min(100, Math.round((sev.HIGH*20 + sev.MEDIUM*10 + sev.LOW*5) / Math.max(1, clauses.length) * 100)); | |
| return { | |
| risk_score: risk, | |
| grade: risk >= 60 ? "F" : risk >= 40 ? "D" : risk >= 20 ? "C" : risk >= 10 ? "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<FREE_SCANS_PER_MONTH, used, limit: plan==="free"?FREE_SCANS_PER_MONTH:Infinity, plan }); | |
| })); | |
| } | |
| async function incrementUsage() { | |
| return new Promise(r => 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}`])); | |