gaurv007 commited on
Commit
abe38ff
Β·
verified Β·
1 Parent(s): a07bb67

Fix extension: remove module type, add 45s timeout for HF cold start, robust error handling

Browse files
Files changed (2) hide show
  1. extension/background.js +108 -147
  2. extension/manifest.json +2 -6
extension/background.js CHANGED
@@ -1,162 +1,151 @@
1
  /**
2
  * ClauseGuard β€” Background Service Worker (Manifest V3)
3
- * Handles: API calls, auth token management, side panel control, usage tracking
4
  */
5
 
6
  const API_BASE = "https://gaurv007-clauseguard-api.hf.space";
7
  const FREE_SCANS_PER_MONTH = 10;
 
8
 
9
  // ─── Side Panel ───
10
- chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }).catch(() => {});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  // ─── Message Router ───
13
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
14
- switch (message.type) {
15
- case "ANALYZE_TEXT":
16
- handleAnalyze(message.payload, sender.tab?.id).then(sendResponse);
17
- return true; // keep channel open for async
18
-
19
- case "GET_AUTH":
20
- getAuth().then(sendResponse);
21
- return true;
22
-
23
- case "CHECK_USAGE":
24
- checkUsage().then(sendResponse);
25
- return true;
26
-
27
- case "OPEN_SIDEPANEL":
28
- if (sender.tab?.id) {
29
- chrome.sidePanel.open({ tabId: sender.tab.id });
30
- }
31
- break;
32
-
33
- case "GET_RESULTS":
34
- getStoredResults(sender.tab?.id || message.tabId).then(sendResponse);
35
- return true;
36
- }
37
  });
38
 
39
- // ─── External message from website (auth token) ───
40
  chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
41
  const allowed = ["https://app.clauseguard.com", "https://clauseguard.com"];
42
- if (!allowed.some(origin => sender.origin?.startsWith(origin))) return;
43
-
44
  if (message.type === "SET_AUTH") {
45
- chrome.storage.sync.set({
46
- authToken: message.token,
47
- plan: message.plan || "free",
48
- email: message.email || "",
49
- });
50
  sendResponse({ success: true });
51
  }
52
-
53
  if (message.type === "LOGOUT") {
54
  chrome.storage.sync.remove(["authToken", "plan", "email"]);
55
  sendResponse({ success: true });
56
  }
57
  });
58
 
59
- // ─── Core: Analyze Text ───
60
  async function handleAnalyze(payload, tabId) {
61
- try {
62
- // Check usage limits
63
- const usage = await checkUsage();
64
- if (!usage.allowed) {
65
- return { error: "limit_reached", message: "Free scan limit reached. Upgrade to Pro for unlimited scans.", usage };
66
- }
67
-
68
- const { text, url } = payload;
69
 
70
- // Split text into clauses
71
- const clauses = splitIntoClauses(text);
72
- if (clauses.length === 0) {
73
- return { error: "no_clauses", message: "No analyzable clauses found on this page." };
74
- }
75
 
76
- // Call API
 
 
77
  const auth = await getAuth();
78
- const response = await fetch(`${API_BASE}/api/analyze`, {
79
  method: "POST",
80
  headers: {
81
  "Content-Type": "application/json",
82
  ...(auth.token ? { Authorization: `Bearer ${auth.token}` } : {}),
83
  },
84
  body: JSON.stringify({ clauses, source_url: url }),
85
- });
86
 
87
- if (!response.ok) {
88
- throw new Error(`API error: ${response.status}`);
89
- }
90
-
91
- const results = await response.json();
 
92
 
93
- // Store results for this tab
 
94
  await chrome.storage.local.set({ [`results_${tabId}`]: results });
95
-
96
- // Increment usage
97
- await incrementUsage();
98
-
99
- // Update badge
100
  const flagged = results.results?.filter(r => r.categories?.length > 0).length || 0;
101
  if (flagged > 0) {
102
  chrome.action.setBadgeText({ text: String(flagged), tabId });
103
  chrome.action.setBadgeBackgroundColor({ color: flagged > 3 ? "#ef4444" : "#f59e0b", tabId });
 
 
104
  }
105
-
106
- return results;
107
- } catch (err) {
108
- console.error("ClauseGuard analyze error:", err);
109
- // Fallback: use local pattern matching
110
- return localAnalyze(payload.text, payload.url);
111
  }
 
 
 
112
  }
113
 
114
- // ─── Local Fallback Analyzer (regex patterns) ───
115
- function localAnalyze(text, url) {
116
  const clauses = splitIntoClauses(text);
117
  const patterns = {
118
- 0: [/arbitrat/i, /binding arbitration/i, /waive.*right.*court/i, /class action waiver/i],
119
- 1: [/sole discretion/i, /reserves? the right to (modify|change|update|amend)/i, /at any time.*without (prior )?notice/i],
120
- 2: [/remove.*content.*without/i, /right to remove/i, /we may.*remove/i],
121
- 3: [/exclusive jurisdiction/i, /courts? of.*(california|delaware|new york|ireland|england)/i, /submit to.*jurisdiction/i],
122
- 4: [/governed by.*laws? of/i, /shall be governed/i, /laws of the state of/i],
123
  5: [/not liable/i, /shall not be (liable|responsible)/i, /in no event.*liable/i, /limitation of liability/i, /without warranty/i],
124
- 6: [/terminat.*at any time/i, /suspend.*account.*without/i, /we may (terminat|suspend|discontinu)/i],
125
- 7: [/by (using|accessing).*you agree/i, /continued use.*constitutes? acceptance/i],
126
  };
127
-
128
- const labelNames = [
129
- "Limitation of liability", "Unilateral termination", "Unilateral change",
130
- "Content removal", "Contract by using", "Choice of law", "Jurisdiction", "Arbitration",
131
- ];
132
- const highRisk = new Set([0, 1, 6]); // liability, termination, arbitration (indices in labelNames mapping)
133
- const severityMap = { 0: "HIGH", 1: "HIGH", 2: "MEDIUM", 3: "MEDIUM", 4: "MEDIUM", 5: "HIGH", 6: "HIGH", 7: "LOW" };
134
 
135
  const results = clauses.map(clause => {
136
  const categories = [];
137
  for (const [id, pats] of Object.entries(patterns)) {
138
  if (pats.some(p => p.test(clause))) {
139
- categories.push({
140
- id: Number(id),
141
- name: labelNames[id],
142
- severity: severityMap[id],
143
- });
144
  }
145
  }
146
  return { text: clause, categories };
147
  });
148
 
149
  const flagged = results.filter(r => r.categories.length > 0);
150
- const sevCounts = { HIGH: 0, MEDIUM: 0, LOW: 0 };
151
- flagged.forEach(r => r.categories.forEach(c => sevCounts[c.severity]++));
152
-
153
- const riskScore = Math.min(100, Math.round(
154
- (sevCounts.HIGH * 20 + sevCounts.MEDIUM * 10 + sevCounts.LOW * 5) / Math.max(1, clauses.length) * 100
155
- ));
156
 
157
  return {
158
- risk_score: riskScore,
159
- grade: riskScore >= 60 ? "F" : riskScore >= 40 ? "D" : riskScore >= 20 ? "C" : riskScore >= 10 ? "B" : "A",
160
  total_clauses: clauses.length,
161
  flagged_count: flagged.length,
162
  results,
@@ -164,68 +153,40 @@ function localAnalyze(text, url) {
164
  };
165
  }
166
 
167
- // ─── Clause Splitter ───
168
  function splitIntoClauses(text) {
169
- const cleaned = text.replace(/\n{2,}/g, "\n").trim();
170
- const clauses = cleaned.split(/(?<=[.!?])\s+(?=[A-Z0-9(])|(?:\n)(?=\d+[.)]\s|\([a-z]\)\s|β€’\s|-\s)/);
171
- return clauses.map(c => c.trim()).filter(c => c.length > 30);
172
  }
173
 
174
- // ─── Auth Helpers ───
175
  async function getAuth() {
176
- return new Promise(resolve => {
177
- chrome.storage.sync.get(["authToken", "plan", "email"], data => {
178
- resolve({
179
- token: data.authToken || null,
180
- plan: data.plan || "free",
181
- email: data.email || null,
182
- isLoggedIn: !!data.authToken,
183
- isPro: data.plan === "pro" || data.plan === "team",
184
- });
185
- });
186
- });
187
  }
188
 
189
- // ─── Usage Tracking ───
190
  async function checkUsage() {
191
- return new Promise(resolve => {
192
- chrome.storage.sync.get(["plan", "scansThisMonth", "monthResetAt"], data => {
193
- const now = new Date();
194
- const resetAt = data.monthResetAt ? new Date(data.monthResetAt) : null;
195
-
196
- // Reset counter on new month
197
- if (!resetAt || now.getMonth() !== resetAt.getMonth() || now.getFullYear() !== resetAt.getFullYear()) {
198
- chrome.storage.sync.set({ scansThisMonth: 0, monthResetAt: now.toISOString() });
199
- resolve({ allowed: true, used: 0, limit: FREE_SCANS_PER_MONTH, plan: data.plan || "free" });
200
- return;
201
- }
202
-
203
- const used = data.scansThisMonth || 0;
204
- const plan = data.plan || "free";
205
- const allowed = plan !== "free" || used < FREE_SCANS_PER_MONTH;
206
-
207
- resolve({ allowed, used, limit: plan === "free" ? FREE_SCANS_PER_MONTH : Infinity, plan });
208
- });
209
- });
210
  }
211
 
212
  async function incrementUsage() {
213
- return new Promise(resolve => {
214
- chrome.storage.sync.get(["scansThisMonth"], data => {
215
- chrome.storage.sync.set({ scansThisMonth: (data.scansThisMonth || 0) + 1 }, resolve);
216
- });
217
- });
218
  }
219
 
220
  async function getStoredResults(tabId) {
221
- return new Promise(resolve => {
222
- chrome.storage.local.get([`results_${tabId}`], data => {
223
- resolve(data[`results_${tabId}`] || null);
224
- });
225
- });
226
  }
227
 
228
- // ─── Tab cleanup ───
229
- chrome.tabs.onRemoved.addListener(tabId => {
230
- chrome.storage.local.remove([`results_${tabId}`]);
231
- });
 
1
  /**
2
  * ClauseGuard β€” Background Service Worker (Manifest V3)
 
3
  */
4
 
5
  const API_BASE = "https://gaurv007-clauseguard-api.hf.space";
6
  const FREE_SCANS_PER_MONTH = 10;
7
+ const API_TIMEOUT_MS = 45000; // 45s β€” HF free tier can take 30s to wake
8
 
9
  // ─── Side Panel ───
10
+ try { chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); } catch(e) {}
11
+
12
+ // ─── Fetch with timeout ───
13
+ async function fetchWithTimeout(url, options, timeoutMs) {
14
+ const controller = new AbortController();
15
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
16
+ try {
17
+ const resp = await fetch(url, { ...options, signal: controller.signal });
18
+ clearTimeout(timer);
19
+ return resp;
20
+ } catch (err) {
21
+ clearTimeout(timer);
22
+ throw err;
23
+ }
24
+ }
25
 
26
  // ─── Message Router ───
27
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
28
+ const handle = async () => {
29
+ switch (message.type) {
30
+ case "ANALYZE_TEXT":
31
+ return await handleAnalyze(message.payload, sender.tab?.id);
32
+ case "GET_AUTH":
33
+ return await getAuth();
34
+ case "CHECK_USAGE":
35
+ return await checkUsage();
36
+ case "OPEN_SIDEPANEL":
37
+ if (sender.tab?.id) chrome.sidePanel.open({ tabId: sender.tab.id });
38
+ return { ok: true };
39
+ case "GET_RESULTS":
40
+ return await getStoredResults(sender.tab?.id || message.tabId);
41
+ default:
42
+ return null;
43
+ }
44
+ };
45
+ handle().then(sendResponse).catch(err => {
46
+ console.error("ClauseGuard handler error:", err);
47
+ sendResponse({ error: err.message });
48
+ });
49
+ return true;
 
50
  });
51
 
52
+ // ─── External messages from website ───
53
  chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
54
  const allowed = ["https://app.clauseguard.com", "https://clauseguard.com"];
55
+ if (!allowed.some(o => sender.origin?.startsWith(o))) return;
 
56
  if (message.type === "SET_AUTH") {
57
+ chrome.storage.sync.set({ authToken: message.token, plan: message.plan || "free", email: message.email || "" });
 
 
 
 
58
  sendResponse({ success: true });
59
  }
 
60
  if (message.type === "LOGOUT") {
61
  chrome.storage.sync.remove(["authToken", "plan", "email"]);
62
  sendResponse({ success: true });
63
  }
64
  });
65
 
66
+ // ─── Core: Analyze ───
67
  async function handleAnalyze(payload, tabId) {
68
+ const usage = await checkUsage();
69
+ if (!usage.allowed) {
70
+ return { error: "limit_reached", message: "Free scan limit reached.", usage };
71
+ }
 
 
 
 
72
 
73
+ const { text, url } = payload;
74
+ const clauses = splitIntoClauses(text);
75
+ if (clauses.length === 0) {
76
+ return { error: "no_clauses", message: "No analyzable clauses found." };
77
+ }
78
 
79
+ // Try API first, fall back to local
80
+ let results;
81
+ try {
82
  const auth = await getAuth();
83
+ const resp = await fetchWithTimeout(`${API_BASE}/api/analyze`, {
84
  method: "POST",
85
  headers: {
86
  "Content-Type": "application/json",
87
  ...(auth.token ? { Authorization: `Bearer ${auth.token}` } : {}),
88
  },
89
  body: JSON.stringify({ clauses, source_url: url }),
90
+ }, API_TIMEOUT_MS);
91
 
92
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
93
+ results = await resp.json();
94
+ } catch (err) {
95
+ console.warn("API unavailable, using local analysis:", err.message);
96
+ results = localAnalyze(text, url);
97
+ }
98
 
99
+ // Store + badge
100
+ if (tabId) {
101
  await chrome.storage.local.set({ [`results_${tabId}`]: results });
 
 
 
 
 
102
  const flagged = results.results?.filter(r => r.categories?.length > 0).length || 0;
103
  if (flagged > 0) {
104
  chrome.action.setBadgeText({ text: String(flagged), tabId });
105
  chrome.action.setBadgeBackgroundColor({ color: flagged > 3 ? "#ef4444" : "#f59e0b", tabId });
106
+ } else {
107
+ chrome.action.setBadgeText({ text: "", tabId });
108
  }
 
 
 
 
 
 
109
  }
110
+
111
+ await incrementUsage();
112
+ return results;
113
  }
114
 
115
+ // ─── Local fallback ───
116
+ function localAnalyze(text) {
117
  const clauses = splitIntoClauses(text);
118
  const patterns = {
119
+ 0: [/arbitrat/i, /binding arbitration/i, /waive.*right.*court/i],
120
+ 1: [/sole discretion/i, /reserves? the right to (modify|change|update)/i, /at any time.*without.*notice/i],
121
+ 2: [/remove.*content.*without/i, /right to remove/i],
122
+ 3: [/exclusive jurisdiction/i, /courts? of.*(california|delaware|new york|ireland)/i, /submit to.*jurisdiction/i],
123
+ 4: [/governed by.*laws? of/i, /shall be governed/i],
124
  5: [/not liable/i, /shall not be (liable|responsible)/i, /in no event.*liable/i, /limitation of liability/i, /without warranty/i],
125
+ 6: [/terminat.*at any time/i, /suspend.*account.*without/i, /we may (terminat|suspend)/i],
126
+ 7: [/by (using|accessing).*you agree/i, /continued use.*constitutes/i],
127
  };
128
+ const names = ["Arbitration","Unilateral change","Content removal","Jurisdiction","Choice of law","Limitation of liability","Unilateral termination","Contract by using"];
129
+ const sevMap = {0:"HIGH",1:"MEDIUM",2:"MEDIUM",3:"MEDIUM",4:"MEDIUM",5:"HIGH",6:"HIGH",7:"LOW"};
 
 
 
 
 
130
 
131
  const results = clauses.map(clause => {
132
  const categories = [];
133
  for (const [id, pats] of Object.entries(patterns)) {
134
  if (pats.some(p => p.test(clause))) {
135
+ categories.push({ name: names[id], severity: sevMap[id] });
 
 
 
 
136
  }
137
  }
138
  return { text: clause, categories };
139
  });
140
 
141
  const flagged = results.filter(r => r.categories.length > 0);
142
+ const sev = { HIGH: 0, MEDIUM: 0, LOW: 0 };
143
+ flagged.forEach(r => r.categories.forEach(c => sev[c.severity]++));
144
+ const risk = Math.min(100, Math.round((sev.HIGH*20 + sev.MEDIUM*10 + sev.LOW*5) / Math.max(1, clauses.length) * 100));
 
 
 
145
 
146
  return {
147
+ risk_score: risk,
148
+ grade: risk >= 60 ? "F" : risk >= 40 ? "D" : risk >= 20 ? "C" : risk >= 10 ? "B" : "A",
149
  total_clauses: clauses.length,
150
  flagged_count: flagged.length,
151
  results,
 
153
  };
154
  }
155
 
156
+ // ─── Utils ───
157
  function splitIntoClauses(text) {
158
+ return text.replace(/\n{2,}/g, "\n").trim()
159
+ .split(/(?<=[.!?])\s+(?=[A-Z0-9(])|(?:\n)(?=\d+[.)]\s|\([a-z]\)\s)/)
160
+ .map(c => c.trim()).filter(c => c.length > 30);
161
  }
162
 
 
163
  async function getAuth() {
164
+ return new Promise(r => chrome.storage.sync.get(["authToken","plan","email"], d => r({
165
+ token: d.authToken||null, plan: d.plan||"free", email: d.email||null,
166
+ isLoggedIn: !!d.authToken, isPro: d.plan==="pro"||d.plan==="team",
167
+ })));
 
 
 
 
 
 
 
168
  }
169
 
 
170
  async function checkUsage() {
171
+ return new Promise(r => chrome.storage.sync.get(["plan","scansThisMonth","monthResetAt"], d => {
172
+ const now = new Date(), reset = d.monthResetAt ? new Date(d.monthResetAt) : null;
173
+ if (!reset || now.getMonth() !== reset.getMonth() || now.getFullYear() !== reset.getFullYear()) {
174
+ chrome.storage.sync.set({ scansThisMonth: 0, monthResetAt: now.toISOString() });
175
+ r({ allowed: true, used: 0, limit: FREE_SCANS_PER_MONTH, plan: d.plan||"free" });
176
+ return;
177
+ }
178
+ const used = d.scansThisMonth||0, plan = d.plan||"free";
179
+ r({ allowed: plan!=="free"||used<FREE_SCANS_PER_MONTH, used, limit: plan==="free"?FREE_SCANS_PER_MONTH:Infinity, plan });
180
+ }));
 
 
 
 
 
 
 
 
 
181
  }
182
 
183
  async function incrementUsage() {
184
+ return new Promise(r => chrome.storage.sync.get(["scansThisMonth"], d =>
185
+ chrome.storage.sync.set({ scansThisMonth: (d.scansThisMonth||0)+1 }, r)));
 
 
 
186
  }
187
 
188
  async function getStoredResults(tabId) {
189
+ return new Promise(r => chrome.storage.local.get([`results_${tabId}`], d => r(d[`results_${tabId}`]||null)));
 
 
 
 
190
  }
191
 
192
+ chrome.tabs.onRemoved.addListener(tabId => chrome.storage.local.remove([`results_${tabId}`]));
 
 
 
extension/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
  "manifest_version": 3,
3
  "name": "ClauseGuard β€” AI Fine Print Scanner",
4
  "version": "1.0.0",
5
- "description": "Instantly highlights unfair clauses in Terms of Service, contracts, and lease agreements. Powered by Legal AI.",
6
  "permissions": [
7
  "activeTab",
8
  "storage",
@@ -13,8 +13,7 @@
13
  "https://gaurv007-clauseguard-api.hf.space/*"
14
  ],
15
  "background": {
16
- "service_worker": "background.js",
17
- "type": "module"
18
  },
19
  "action": {
20
  "default_popup": "popup.html",
@@ -44,8 +43,5 @@
44
  "32": "icons/icon32.png",
45
  "48": "icons/icon48.png",
46
  "128": "icons/icon128.png"
47
- },
48
- "content_security_policy": {
49
- "extension_pages": "script-src 'self'; object-src 'self'"
50
  }
51
  }
 
2
  "manifest_version": 3,
3
  "name": "ClauseGuard β€” AI Fine Print Scanner",
4
  "version": "1.0.0",
5
+ "description": "Highlights unfair clauses in Terms of Service, contracts, and lease agreements.",
6
  "permissions": [
7
  "activeTab",
8
  "storage",
 
13
  "https://gaurv007-clauseguard-api.hf.space/*"
14
  ],
15
  "background": {
16
+ "service_worker": "background.js"
 
17
  },
18
  "action": {
19
  "default_popup": "popup.html",
 
43
  "32": "icons/icon32.png",
44
  "48": "icons/icon48.png",
45
  "128": "icons/icon128.png"
 
 
 
46
  }
47
  }