ClauseGuard / extension /background.js
gaurv007's picture
πŸ”§ v4.2: Critical bug fixes + performance optimizations (7 bugs, 4 perf improvements) (#3)
f4ccb3e
raw
history blame
9.68 kB
/**
* 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<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}`]));