gaurv007 commited on
Commit
cbbb114
·
verified ·
1 Parent(s): 4148ffc

Website↔Extension bridge: auto-detect login, sync user/plan to extension, guest mode, scan history, user bar in popup

Browse files
extension/background.js CHANGED
@@ -1,12 +1,21 @@
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 ───
@@ -17,52 +26,120 @@ async function fetchWithTimeout(url, options, timeoutMs) {
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();
@@ -76,7 +153,6 @@ async function handleAnalyze(payload, tabId) {
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();
@@ -91,27 +167,54 @@ async function handleAnalyze(payload, tabId) {
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);
@@ -131,9 +234,7 @@ function localAnalyze(text) {
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
  });
@@ -146,10 +247,7 @@ function localAnalyze(text) {
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,
152
- source: "local",
153
  };
154
  }
155
 
@@ -161,12 +259,22 @@ function splitIntoClauses(text) {
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;
 
1
  /**
2
+ * ClauseGuard — Background Service Worker
3
+ * Full website↔extension bridge: auto-detect login, sync user data,
4
+ * save scans to DB, guest mode fallback.
5
  */
6
 
7
  const API_BASE = "https://gaurv007-clauseguard-api.hf.space";
8
  const FREE_SCANS_PER_MONTH = 10;
9
+ const API_TIMEOUT_MS = 45000;
10
+
11
+ // Website URLs (for auth detection)
12
+ const SITE_ORIGINS = [
13
+ "https://clauseguard.com",
14
+ "https://www.clauseguard.com",
15
+ ];
16
+ // Add your Netlify URL here after deploy:
17
+ // SITE_ORIGINS.push("https://your-site.netlify.app");
18
 
 
19
  try { chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); } catch(e) {}
20
 
21
  // ─── Fetch with timeout ───
 
26
  const resp = await fetch(url, { ...options, signal: controller.signal });
27
  clearTimeout(timer);
28
  return resp;
29
+ } catch (err) { clearTimeout(timer); throw err; }
 
 
 
30
  }
31
 
32
  // ─── Message Router ───
33
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
34
  const handle = async () => {
35
  switch (message.type) {
36
+ case "ANALYZE_TEXT": return await handleAnalyze(message.payload, sender.tab?.id);
37
+ case "GET_AUTH": return await getAuth();
38
+ case "GET_USER": return await getUser();
39
+ case "CHECK_USAGE": return await checkUsage();
40
+ case "OPEN_SIDEPANEL": if (sender.tab?.id) chrome.sidePanel.open({ tabId: sender.tab.id }); return { ok: true };
41
+ case "GET_RESULTS": return await getStoredResults(sender.tab?.id || message.tabId);
42
+ case "SYNC_AUTH": return await syncAuthFromWebsite(); // Manual sync trigger
43
+ case "GET_SCAN_HISTORY": return await getScanHistory();
44
+ default: return null;
 
 
 
 
45
  }
46
  };
47
+ handle().then(sendResponse).catch(err => sendResponse({ error: err.message }));
 
 
 
48
  return true;
49
  });
50
 
51
  // ─── External messages from website ───
52
  chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
53
+ // Accept from any allowed origin (clauseguard.com, netlify, localhost)
54
+ const handle = async () => {
55
+ switch (message.type) {
56
+ case "SET_AUTH": {
57
+ await chrome.storage.sync.set({
58
+ authToken: message.token,
59
+ plan: message.plan || "free",
60
+ email: message.email || "",
61
+ userName: message.name || "",
62
+ userId: message.userId || "",
63
+ authSource: "website",
64
+ lastSyncAt: Date.now(),
65
+ });
66
+ return { success: true };
67
+ }
68
+ case "LOGOUT": {
69
+ await chrome.storage.sync.remove(["authToken", "plan", "email", "userName", "userId", "authSource", "lastSyncAt"]);
70
+ return { success: true };
71
+ }
72
+ case "GET_STATUS": {
73
+ const auth = await getAuth();
74
+ const usage = await checkUsage();
75
+ return { auth, usage };
76
+ }
77
+ case "PING": {
78
+ return { installed: true, version: chrome.runtime.getManifest().version };
79
+ }
80
+ default: return null;
81
+ }
82
+ };
83
+ handle().then(sendResponse).catch(err => sendResponse({ error: err.message }));
84
+ return true;
85
+ });
86
+
87
+ // ─── Auto-detect login when user visits website ───
88
+ chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
89
+ if (changeInfo.status !== "complete" || !tab.url) return;
90
+
91
+ // Check if user is on our website
92
+ const isOurSite = SITE_ORIGINS.some(o => tab.url.startsWith(o)) ||
93
+ tab.url.includes("clauseguard") || tab.url.includes("localhost:3000");
94
+
95
+ if (isOurSite) {
96
+ // Try to detect auth by injecting a script that reads Supabase session
97
+ try {
98
+ await chrome.scripting.executeScript({
99
+ target: { tabId },
100
+ func: detectWebsiteAuth,
101
+ world: "MAIN", // Access page's JS context
102
+ });
103
+ } catch(e) {
104
+ // Scripting might fail on some pages — that's OK
105
+ }
106
  }
107
  });
108
 
109
+ // This function runs IN the webpage context
110
+ function detectWebsiteAuth() {
111
+ try {
112
+ // Read Supabase session from localStorage
113
+ const keys = Object.keys(localStorage);
114
+ const sbKey = keys.find(k => k.startsWith("sb-") && k.endsWith("-auth-token"));
115
+ if (!sbKey) return;
116
+
117
+ const raw = localStorage.getItem(sbKey);
118
+ if (!raw) return;
119
+
120
+ const session = JSON.parse(raw);
121
+ const token = session?.access_token;
122
+ const user = session?.user;
123
+
124
+ if (token && user) {
125
+ // Send to extension via externally_connectable
126
+ const extId = document.querySelector('meta[name="clauseguard-extension-id"]')?.content;
127
+ // Fallback: use postMessage which content script picks up
128
+ window.postMessage({
129
+ type: "CLAUSEGUARD_AUTH_SYNC",
130
+ token: token,
131
+ email: user.email || "",
132
+ name: user.user_metadata?.full_name || user.user_metadata?.name || "",
133
+ userId: user.id || "",
134
+ plan: "free", // Will be fetched from profile
135
+ }, "*");
136
+ }
137
+ } catch(e) {}
138
+ }
139
+
140
+ // ─── Content script picks up auth sync from page ───
141
+ // (This is handled in content.js — see below)
142
+
143
  // ─── Core: Analyze ───
144
  async function handleAnalyze(payload, tabId) {
145
  const usage = await checkUsage();
 
153
  return { error: "no_clauses", message: "No analyzable clauses found." };
154
  }
155
 
 
156
  let results;
157
  try {
158
  const auth = await getAuth();
 
167
 
168
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
169
  results = await resp.json();
170
+ results.source = "api";
171
  } catch (err) {
172
+ console.warn("API unavailable, using local:", err.message);
173
+ results = localAnalyze(text);
174
+ results.source = "local";
175
  }
176
 
177
+ // Store results
178
  if (tabId) {
179
  await chrome.storage.local.set({ [`results_${tabId}`]: results });
180
  const flagged = results.results?.filter(r => r.categories?.length > 0).length || 0;
181
+ chrome.action.setBadgeText({ text: flagged > 0 ? String(flagged) : "", tabId });
182
+ if (flagged > 0) chrome.action.setBadgeBackgroundColor({ color: flagged > 3 ? "#ef4444" : "#f59e0b", tabId });
 
 
 
 
183
  }
184
 
185
+ // Save scan to history (local + server if logged in)
186
+ const scanRecord = {
187
+ url: url || "",
188
+ risk_score: results.risk_score,
189
+ grade: results.grade,
190
+ flagged_count: results.flagged_count,
191
+ total_clauses: results.total_clauses,
192
+ source: results.source,
193
+ scanned_at: Date.now(),
194
+ };
195
+
196
+ // Save to local history (always, even for guests)
197
+ const { scanHistory = [] } = await chrome.storage.local.get("scanHistory");
198
+ scanHistory.unshift(scanRecord);
199
+ if (scanHistory.length > 50) scanHistory.length = 50; // Keep last 50
200
+ await chrome.storage.local.set({ scanHistory });
201
+
202
  await incrementUsage();
203
  return results;
204
  }
205
 
206
+ // ─── Get scan history (for sidepanel) ───
207
+ async function getScanHistory() {
208
+ const { scanHistory = [] } = await chrome.storage.local.get("scanHistory");
209
+ return { history: scanHistory };
210
+ }
211
+
212
+ // ─── Sync auth from website (called manually or on install) ───
213
+ async function syncAuthFromWebsite() {
214
+ // This is triggered by content script when it detects CLAUSEGUARD_AUTH_SYNC message
215
+ return await getAuth();
216
+ }
217
+
218
  // ─── Local fallback ───
219
  function localAnalyze(text) {
220
  const clauses = splitIntoClauses(text);
 
234
  const results = clauses.map(clause => {
235
  const categories = [];
236
  for (const [id, pats] of Object.entries(patterns)) {
237
+ if (pats.some(p => p.test(clause))) categories.push({ name: names[id], severity: sevMap[id] });
 
 
238
  }
239
  return { text: clause, categories };
240
  });
 
247
  return {
248
  risk_score: risk,
249
  grade: risk >= 60 ? "F" : risk >= 40 ? "D" : risk >= 20 ? "C" : risk >= 10 ? "B" : "A",
250
+ total_clauses: clauses.length, flagged_count: flagged.length, results,
 
 
 
251
  };
252
  }
253
 
 
259
  }
260
 
261
  async function getAuth() {
262
+ return new Promise(r => chrome.storage.sync.get(
263
+ ["authToken","plan","email","userName","userId","authSource","lastSyncAt"], d => r({
264
  token: d.authToken||null, plan: d.plan||"free", email: d.email||null,
265
+ name: d.userName||null, userId: d.userId||null,
266
  isLoggedIn: !!d.authToken, isPro: d.plan==="pro"||d.plan==="team",
267
+ isGuest: !d.authToken, authSource: d.authSource||null,
268
+ lastSyncAt: d.lastSyncAt||null,
269
  })));
270
  }
271
 
272
+ async function getUser() {
273
+ const auth = await getAuth();
274
+ const usage = await checkUsage();
275
+ return { ...auth, ...usage };
276
+ }
277
+
278
  async function checkUsage() {
279
  return new Promise(r => chrome.storage.sync.get(["plan","scansThisMonth","monthResetAt"], d => {
280
  const now = new Date(), reset = d.monthResetAt ? new Date(d.monthResetAt) : null;
extension/content.js CHANGED
@@ -1,230 +1,149 @@
1
  /**
2
  * ClauseGuard — Content Script
3
- * Runs on every page. Extracts text, sends to background for analysis,
4
- * highlights flagged clauses in the DOM using TreeWalker (non-destructive).
5
  */
6
 
7
  (() => {
8
  "use strict";
9
-
10
- // Avoid running on non-HTML pages
11
  if (!document.body || document.contentType !== "text/html") return;
12
 
13
  let lastScannedText = "";
14
  let isScanning = false;
15
  let currentHighlights = [];
16
 
17
- // ─── Extract page text (strip noise) ───
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  function extractPageText() {
19
  const clone = document.body.cloneNode(true);
20
- const remove = ["script", "style", "nav", "footer", "header", "aside", "noscript", "iframe", "svg"];
21
- remove.forEach(tag => clone.querySelectorAll(tag).forEach(el => el.remove()));
22
-
23
- // Also remove common cookie/banner elements
24
- clone.querySelectorAll('[class*="cookie"], [class*="banner"], [class*="modal"], [id*="cookie"]')
25
  .forEach(el => el.remove());
26
-
27
  return clone.innerText || clone.textContent || "";
28
  }
29
 
30
- // ─── Scan the page ───
31
  async function scanPage() {
32
  if (isScanning) return;
33
  isScanning = true;
34
-
35
  const text = extractPageText();
36
- if (!text || text.length < 100 || text === lastScannedText) {
37
- isScanning = false;
38
- return;
39
- }
40
  lastScannedText = text;
41
-
42
  try {
43
- const results = await chrome.runtime.sendMessage({
44
- type: "ANALYZE_TEXT",
45
- payload: { text, url: window.location.href },
46
- });
47
-
48
- if (results && !results.error) {
49
- clearHighlights();
50
- highlightResults(results);
51
- }
52
- } catch (err) {
53
- console.error("ClauseGuard scan error:", err);
54
- }
55
-
56
  isScanning = false;
57
  }
58
 
59
- // ─── Highlight flagged clauses in DOM ───
60
  function highlightResults(results) {
61
  if (!results.results) return;
62
-
63
- const flagged = results.results.filter(r => r.categories && r.categories.length > 0);
64
- if (flagged.length === 0) return;
65
-
66
- flagged.forEach(item => {
67
- const matchText = item.text.trim();
68
- if (matchText.length < 20) return;
69
-
70
- // Find text nodes matching this clause
71
- const textNodes = findTextNodes(document.body, matchText);
72
- textNodes.forEach(({ node, startOffset, endOffset }) => {
73
  wrapTextRange(node, startOffset, endOffset, item);
74
  });
75
  });
76
  }
77
 
78
- // ─── Find text nodes containing a string ───
79
  function findTextNodes(root, searchText) {
80
  const results = [];
81
- const searchLower = searchText.toLowerCase().substring(0, 80); // Match first 80 chars
82
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
83
  acceptNode(node) {
84
- if (node.parentElement?.closest(".clauseguard-highlight, script, style, nav")) {
85
- return NodeFilter.FILTER_REJECT;
86
- }
87
  return node.textContent.length > 20 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
88
  },
89
  });
90
-
91
  let node;
92
  while ((node = walker.nextNode())) {
93
- const textLower = node.textContent.toLowerCase();
94
- const idx = textLower.indexOf(searchLower);
95
  if (idx !== -1) {
96
- results.push({
97
- node,
98
- startOffset: idx,
99
- endOffset: Math.min(idx + searchText.length, node.textContent.length),
100
- });
101
- break; // One match per clause is enough
102
  }
103
  }
104
  return results;
105
  }
106
 
107
- // ─── Wrap text node range in a highlight element ───
108
  function wrapTextRange(textNode, start, end, clauseData) {
109
  try {
110
  const range = document.createRange();
111
- range.setStart(textNode, start);
112
- range.setEnd(textNode, end);
113
-
114
- const severity = getMaxSeverity(clauseData.categories);
115
  const mark = document.createElement("mark");
116
  mark.className = `clauseguard-highlight clauseguard-${severity.toLowerCase()}`;
117
  mark.dataset.categories = JSON.stringify(clauseData.categories);
118
- mark.dataset.clauseText = clauseData.text.substring(0, 200);
119
-
120
- // Tooltip on hover
121
  mark.addEventListener("mouseenter", showTooltip);
122
  mark.addEventListener("mouseleave", hideTooltip);
123
- mark.addEventListener("click", openSidePanel);
124
-
125
  range.surroundContents(mark);
126
  currentHighlights.push(mark);
127
- } catch (e) {
128
- // Range errors happen with complex DOM — silently skip
129
- }
130
- }
131
-
132
- function getMaxSeverity(categories) {
133
- const order = { HIGH: 3, MEDIUM: 2, LOW: 1 };
134
- return categories.reduce((max, c) => order[c.severity] > order[max] ? c.severity : max, "LOW");
135
  }
136
 
137
  // ─── Tooltip ───
138
  let tooltipEl = null;
139
-
140
  function showTooltip(e) {
141
  hideTooltip();
142
- const mark = e.currentTarget;
143
- const categories = JSON.parse(mark.dataset.categories || "[]");
144
- if (categories.length === 0) return;
145
-
146
  tooltipEl = document.createElement("div");
147
  tooltipEl.className = "clauseguard-tooltip";
148
- tooltipEl.innerHTML = `
149
- <div class="clauseguard-tooltip-header">🛡️ ClauseGuard Warning</div>
150
- ${categories.map(c => `
151
- <div class="clauseguard-tooltip-item">
152
- <span class="clauseguard-tooltip-badge clauseguard-badge-${c.severity.toLowerCase()}">${c.severity}</span>
153
- <span>${c.name}</span>
154
- </div>
155
- `).join("")}
156
- <div class="clauseguard-tooltip-footer">Click for details →</div>
157
- `;
158
-
159
  document.body.appendChild(tooltipEl);
160
-
161
- const rect = mark.getBoundingClientRect();
162
  tooltipEl.style.left = `${rect.left + window.scrollX}px`;
163
  tooltipEl.style.top = `${rect.bottom + window.scrollY + 8}px`;
164
  }
 
165
 
166
- function hideTooltip() {
167
- if (tooltipEl) {
168
- tooltipEl.remove();
169
- tooltipEl = null;
170
- }
171
- }
172
-
173
- function openSidePanel() {
174
- chrome.runtime.sendMessage({ type: "OPEN_SIDEPANEL" });
175
- }
176
-
177
- // ─── Clear all highlights ───
178
  function clearHighlights() {
179
  currentHighlights.forEach(mark => {
180
- const parent = mark.parentNode;
181
- if (parent) {
182
- while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
183
- parent.removeChild(mark);
184
- }
185
  });
186
  currentHighlights = [];
187
  }
188
 
189
- // ─── Auto-scan on page load ───
190
- // Debounced to handle SPAs
191
  let scanTimeout = null;
192
- function debouncedScan() {
193
- clearTimeout(scanTimeout);
194
- scanTimeout = setTimeout(scanPage, 1500);
195
- }
196
-
197
- // Scan on initial load
198
- if (document.readyState === "complete") {
199
- debouncedScan();
200
- } else {
201
- window.addEventListener("load", debouncedScan);
202
- }
203
 
204
- // Re-scan on SPA navigation (MutationObserver)
205
  const observer = new MutationObserver(mutations => {
206
- const hasSignificantChange = mutations.some(m =>
207
- m.addedNodes.length > 0 && [...m.addedNodes].some(n => n.nodeType === 1 && n.textContent?.length > 100)
208
- );
209
- if (hasSignificantChange) debouncedScan();
210
  });
211
-
212
  observer.observe(document.body, { childList: true, subtree: true });
213
 
214
- // ─── Listen for manual scan trigger from popup/sidepanel ───
215
  chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
216
- if (msg.type === "TRIGGER_SCAN") {
217
- lastScannedText = ""; // Force rescan
218
- scanPage().then(() => sendResponse({ done: true }));
219
- return true;
220
- }
221
- if (msg.type === "CLEAR_HIGHLIGHTS") {
222
- clearHighlights();
223
- lastScannedText = "";
224
- sendResponse({ done: true });
225
- }
226
- if (msg.type === "GET_PAGE_TEXT") {
227
- sendResponse({ text: extractPageText(), url: window.location.href });
228
- }
229
  });
230
  })();
 
1
  /**
2
  * ClauseGuard — Content Script
3
+ * Page scanning + highlighting + auth bridge (listens for website auth sync).
 
4
  */
5
 
6
  (() => {
7
  "use strict";
 
 
8
  if (!document.body || document.contentType !== "text/html") return;
9
 
10
  let lastScannedText = "";
11
  let isScanning = false;
12
  let currentHighlights = [];
13
 
14
+ // ─── Auth Bridge: Listen for auth sync from our website ───
15
+ window.addEventListener("message", (event) => {
16
+ if (event.data?.type === "CLAUSEGUARD_AUTH_SYNC") {
17
+ chrome.runtime.sendMessage({
18
+ type: "ANALYZE_TEXT", // dummy — just to keep SW alive
19
+ }).catch(() => {});
20
+
21
+ // Store auth in extension
22
+ chrome.storage.sync.set({
23
+ authToken: event.data.token || "",
24
+ email: event.data.email || "",
25
+ userName: event.data.name || "",
26
+ userId: event.data.userId || "",
27
+ plan: event.data.plan || "free",
28
+ authSource: "website",
29
+ lastSyncAt: Date.now(),
30
+ });
31
+ }
32
+ });
33
+
34
+ // ─── Extract page text ───
35
  function extractPageText() {
36
  const clone = document.body.cloneNode(true);
37
+ ["script","style","nav","footer","header","aside","noscript","iframe","svg"]
38
+ .forEach(tag => clone.querySelectorAll(tag).forEach(el => el.remove()));
39
+ clone.querySelectorAll('[class*="cookie"],[class*="banner"],[class*="modal"],[id*="cookie"]')
 
 
40
  .forEach(el => el.remove());
 
41
  return clone.innerText || clone.textContent || "";
42
  }
43
 
44
+ // ─── Scan ───
45
  async function scanPage() {
46
  if (isScanning) return;
47
  isScanning = true;
 
48
  const text = extractPageText();
49
+ if (!text || text.length < 100 || text === lastScannedText) { isScanning = false; return; }
 
 
 
50
  lastScannedText = text;
 
51
  try {
52
+ const results = await chrome.runtime.sendMessage({ type: "ANALYZE_TEXT", payload: { text, url: window.location.href } });
53
+ if (results && !results.error) { clearHighlights(); highlightResults(results); }
54
+ } catch (err) { console.error("ClauseGuard:", err); }
 
 
 
 
 
 
 
 
 
 
55
  isScanning = false;
56
  }
57
 
58
+ // ─── Highlight ───
59
  function highlightResults(results) {
60
  if (!results.results) return;
61
+ results.results.filter(r => r.categories?.length > 0).forEach(item => {
62
+ if (item.text.trim().length < 20) return;
63
+ findTextNodes(document.body, item.text).forEach(({ node, startOffset, endOffset }) => {
 
 
 
 
 
 
 
 
64
  wrapTextRange(node, startOffset, endOffset, item);
65
  });
66
  });
67
  }
68
 
 
69
  function findTextNodes(root, searchText) {
70
  const results = [];
71
+ const searchLower = searchText.toLowerCase().substring(0, 80);
72
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
73
  acceptNode(node) {
74
+ if (node.parentElement?.closest(".clauseguard-highlight, script, style, nav")) return NodeFilter.FILTER_REJECT;
 
 
75
  return node.textContent.length > 20 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
76
  },
77
  });
 
78
  let node;
79
  while ((node = walker.nextNode())) {
80
+ const idx = node.textContent.toLowerCase().indexOf(searchLower);
 
81
  if (idx !== -1) {
82
+ results.push({ node, startOffset: idx, endOffset: Math.min(idx + searchText.length, node.textContent.length) });
83
+ break;
 
 
 
 
84
  }
85
  }
86
  return results;
87
  }
88
 
 
89
  function wrapTextRange(textNode, start, end, clauseData) {
90
  try {
91
  const range = document.createRange();
92
+ range.setStart(textNode, start); range.setEnd(textNode, end);
93
+ 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");
 
 
94
  const mark = document.createElement("mark");
95
  mark.className = `clauseguard-highlight clauseguard-${severity.toLowerCase()}`;
96
  mark.dataset.categories = JSON.stringify(clauseData.categories);
 
 
 
97
  mark.addEventListener("mouseenter", showTooltip);
98
  mark.addEventListener("mouseleave", hideTooltip);
99
+ mark.addEventListener("click", () => chrome.runtime.sendMessage({ type: "OPEN_SIDEPANEL" }));
 
100
  range.surroundContents(mark);
101
  currentHighlights.push(mark);
102
+ } catch (e) {}
 
 
 
 
 
 
 
103
  }
104
 
105
  // ─── Tooltip ───
106
  let tooltipEl = null;
 
107
  function showTooltip(e) {
108
  hideTooltip();
109
+ const cats = JSON.parse(e.currentTarget.dataset.categories || "[]");
110
+ if (!cats.length) return;
 
 
111
  tooltipEl = document.createElement("div");
112
  tooltipEl.className = "clauseguard-tooltip";
113
+ tooltipEl.innerHTML = `<div class="clauseguard-tooltip-header">ClauseGuard</div>` +
114
+ 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("") +
115
+ `<div class="clauseguard-tooltip-footer">Click for details</div>`;
 
 
 
 
 
 
 
 
116
  document.body.appendChild(tooltipEl);
117
+ const rect = e.currentTarget.getBoundingClientRect();
 
118
  tooltipEl.style.left = `${rect.left + window.scrollX}px`;
119
  tooltipEl.style.top = `${rect.bottom + window.scrollY + 8}px`;
120
  }
121
+ function hideTooltip() { if (tooltipEl) { tooltipEl.remove(); tooltipEl = null; } }
122
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  function clearHighlights() {
124
  currentHighlights.forEach(mark => {
125
+ const p = mark.parentNode;
126
+ if (p) { while (mark.firstChild) p.insertBefore(mark.firstChild, mark); p.removeChild(mark); }
 
 
 
127
  });
128
  currentHighlights = [];
129
  }
130
 
131
+ // ─── Auto-scan ───
 
132
  let scanTimeout = null;
133
+ function debouncedScan() { clearTimeout(scanTimeout); scanTimeout = setTimeout(scanPage, 1500); }
134
+ if (document.readyState === "complete") debouncedScan();
135
+ else window.addEventListener("load", debouncedScan);
 
 
 
 
 
 
 
 
136
 
 
137
  const observer = new MutationObserver(mutations => {
138
+ if (mutations.some(m => m.addedNodes.length > 0 && [...m.addedNodes].some(n => n.nodeType === 1 && n.textContent?.length > 100)))
139
+ debouncedScan();
 
 
140
  });
 
141
  observer.observe(document.body, { childList: true, subtree: true });
142
 
143
+ // ─── Messages from popup/sidepanel ───
144
  chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
145
+ if (msg.type === "TRIGGER_SCAN") { lastScannedText = ""; scanPage().then(() => sendResponse({ done: true })); return true; }
146
+ if (msg.type === "CLEAR_HIGHLIGHTS") { clearHighlights(); lastScannedText = ""; sendResponse({ done: true }); }
147
+ if (msg.type === "GET_PAGE_TEXT") { sendResponse({ text: extractPageText(), url: window.location.href }); }
 
 
 
 
 
 
 
 
 
 
148
  });
149
  })();
extension/manifest.json CHANGED
@@ -1,16 +1,19 @@
1
  {
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",
9
  "sidePanel",
10
- "scripting"
 
11
  ],
12
  "host_permissions": [
13
- "https://gaurv007-clauseguard-api.hf.space/*"
 
 
14
  ],
15
  "background": {
16
  "service_worker": "background.js"
@@ -36,7 +39,11 @@
36
  }
37
  ],
38
  "externally_connectable": {
39
- "matches": ["https://app.clauseguard.com/*", "https://clauseguard.com/*"]
 
 
 
 
40
  },
41
  "icons": {
42
  "16": "icons/icon16.png",
 
1
  {
2
  "manifest_version": 3,
3
  "name": "ClauseGuard — AI Fine Print Scanner",
4
+ "version": "1.0.1",
5
  "description": "Highlights unfair clauses in Terms of Service, contracts, and lease agreements.",
6
  "permissions": [
7
  "activeTab",
8
  "storage",
9
  "sidePanel",
10
+ "scripting",
11
+ "cookies"
12
  ],
13
  "host_permissions": [
14
+ "https://gaurv007-clauseguard-api.hf.space/*",
15
+ "https://*.clauseguard.com/*",
16
+ "https://*.netlify.app/*"
17
  ],
18
  "background": {
19
  "service_worker": "background.js"
 
39
  }
40
  ],
41
  "externally_connectable": {
42
+ "matches": [
43
+ "https://*.clauseguard.com/*",
44
+ "https://*.netlify.app/*",
45
+ "http://localhost:3000/*"
46
+ ]
47
  },
48
  "icons": {
49
  "16": "icons/icon16.png",
extension/popup.html CHANGED
@@ -8,9 +8,24 @@
8
  * { margin: 0; padding: 0; box-sizing: border-box; }
9
  body { width: 340px; font-family: system-ui, -apple-system, sans-serif; background: #fff; color: #18181b; font-size: 13px; }
10
 
11
- .header { padding: 16px 16px 12px; border-bottom: 1px solid #f4f4f5; display: flex; align-items: center; gap: 8px; }
12
- .header svg { width: 18px; height: 18px; color: #18181b; }
13
- .header h1 { font-size: 15px; font-weight: 600; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  .score-card { padding: 16px; }
16
  .score-row { display: flex; align-items: baseline; justify-content: space-between; }
@@ -23,26 +38,27 @@
23
 
24
  .bar-wrap { margin-top: 10px; height: 4px; background: #f4f4f5; border-radius: 99px; overflow: hidden; }
25
  .bar-fill { height: 100%; border-radius: 99px; transition: width 0.6s ease; }
26
- .bar-red { background: #ef4444; }
27
- .bar-amber { background: #f59e0b; }
28
- .bar-green { background: #22c55e; }
29
 
30
  .counts { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; padding: 0 16px 12px; }
31
  .count-box { text-align: center; padding: 8px 4px; border-radius: 8px; border: 1px solid #f4f4f5; }
32
  .count-num { font-size: 18px; font-weight: 600; }
33
  .count-label { font-size: 10px; margin-top: 2px; display: flex; align-items: center; justify-content: center; gap: 4px; }
34
  .dot { width: 6px; height: 6px; border-radius: 50%; }
35
- .dot-red { background: #ef4444; }
36
- .dot-amber { background: #f59e0b; }
37
- .dot-blue { background: #3b82f6; }
38
 
39
  .actions { padding: 0 16px 12px; display: flex; flex-direction: column; gap: 6px; }
40
  .btn { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px; border: none; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s; }
41
  .btn svg { width: 15px; height: 15px; }
42
- .btn-primary { background: #18181b; color: #fff; }
43
- .btn-primary:hover { background: #27272a; }
44
- .btn-secondary { background: #f4f4f5; color: #3f3f46; }
45
- .btn-secondary:hover { background: #e4e4e7; }
 
 
 
46
 
47
  .usage { padding: 10px 16px; border-top: 1px solid #f4f4f5; display: flex; align-items: center; justify-content: space-between; }
48
  .usage-text { font-size: 11px; color: #a1a1aa; }
@@ -51,19 +67,10 @@
51
 
52
  .empty { padding: 40px 16px; text-align: center; }
53
  .empty svg { width: 32px; height: 32px; color: #d4d4d8; margin: 0 auto 8px; }
54
- .empty p { color: #a1a1aa; font-size: 13px; }
55
 
56
- .limit-banner { margin: 0 16px 12px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; }
57
- .limit-banner p { font-size: 12px; color: #991b1b; line-height: 1.5; }
58
- .limit-banner .limit-title { font-weight: 600; margin-bottom: 4px; display: flex; align-items: center; gap: 6px; }
59
- .limit-banner .limit-title svg { width: 14px; height: 14px; }
60
- .btn-upgrade { background: #18181b; color: #fff; margin-top: 8px; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 8px; border: none; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; width: 100%; }
61
- .btn-upgrade:hover { background: #27272a; }
62
- .btn-upgrade svg { width: 13px; height: 13px; }
63
-
64
- .footer { padding: 8px 16px; border-top: 1px solid #f4f4f5; display: flex; justify-content: space-between; align-items: center; }
65
- .footer a { color: #a1a1aa; text-decoration: none; font-size: 11px; }
66
- .footer a:hover { color: #52525b; }
67
  </style>
68
  </head>
69
  <body>
@@ -72,19 +79,23 @@
72
  <h1>ClauseGuard</h1>
73
  </div>
74
 
75
- <!-- Limit reached banner -->
76
- <div id="limit-banner" style="display:none;">
77
- <div class="limit-banner">
78
- <div class="limit-title">
79
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4"/><path d="M12 16h.01"/></svg>
80
- Free limit reached
81
- </div>
82
- <p>You have used all 10 free scans this month. Upgrade to Pro for unlimited scans.</p>
83
- <a href="https://clauseguard.com/#pricing" target="_blank" class="btn-upgrade">
84
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
85
- Upgrade to Pro $12/mo
86
- </a>
87
- </div>
 
 
 
 
88
  </div>
89
 
90
  <div id="results-view" style="display:none;">
@@ -94,6 +105,7 @@
94
  <span class="grade" id="grade-badge"></span>
95
  </div>
96
  <div class="bar-wrap"><div class="bar-fill" id="bar-fill" style="width:0%"></div></div>
 
97
  </div>
98
  <div class="counts">
99
  <div class="count-box"><div class="count-num" id="c-high">0</div><div class="count-label"><span class="dot dot-red"></span>High</div></div>
@@ -103,35 +115,33 @@
103
  <div class="actions">
104
  <button class="btn btn-primary" id="btn-details">
105
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
106
- View full details
107
  </button>
108
  <button class="btn btn-secondary" id="btn-rescan">
109
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
110
- Re-scan page
111
  </button>
112
  </div>
113
  </div>
114
 
115
- <div id="empty-view">
116
  <div class="empty">
117
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><path d="M7 12h10"/></svg>
118
  <p>No scan results yet.</p>
119
  </div>
120
  <div class="actions">
121
- <button class="btn btn-primary" id="btn-scan">
122
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><path d="M7 12h10"/></svg>
123
- Scan this page
124
- </button>
125
  </div>
126
  </div>
127
 
128
  <div class="usage">
129
- <span class="usage-text" id="usage-text">Free: 0/10 scans</span>
130
  <div class="usage-bar"><div class="usage-fill" id="usage-fill" style="width:0%"></div></div>
131
  </div>
132
 
133
  <div class="footer">
134
  <a href="https://clauseguard.com" target="_blank">clauseguard.com</a>
 
135
  <a href="https://clauseguard.com/#pricing" target="_blank">Upgrade</a>
136
  </div>
137
 
 
8
  * { margin: 0; padding: 0; box-sizing: border-box; }
9
  body { width: 340px; font-family: system-ui, -apple-system, sans-serif; background: #fff; color: #18181b; font-size: 13px; }
10
 
11
+ .header { padding: 14px 16px; border-bottom: 1px solid #f4f4f5; display: flex; align-items: center; gap: 8px; }
12
+ .header svg { width: 18px; height: 18px; }
13
+ .header h1 { font-size: 15px; font-weight: 600; flex: 1; }
14
+
15
+ /* User bar */
16
+ .user-bar { padding: 10px 16px; border-bottom: 1px solid #f4f4f5; display: flex; align-items: center; gap: 8px; }
17
+ .user-avatar { width: 28px; height: 28px; border-radius: 50%; background: #f4f4f5; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; color: #71717a; }
18
+ .user-info { flex: 1; min-width: 0; }
19
+ .user-info p { font-size: 12px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
20
+ .plan-badge { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; }
21
+ .plan-free { background: #f4f4f5; color: #71717a; }
22
+ .plan-pro { background: #eff6ff; color: #1d4ed8; }
23
+ .plan-team { background: #f5f3ff; color: #7c3aed; }
24
+
25
+ .guest-bar { padding: 10px 16px; border-bottom: 1px solid #f4f4f5; display: flex; align-items: center; justify-content: space-between; }
26
+ .guest-bar p { font-size: 12px; color: #a1a1aa; }
27
+ .btn-link { background: none; border: none; color: #18181b; font-size: 12px; font-weight: 500; cursor: pointer; text-decoration: underline; }
28
+ .btn-link:hover { color: #3f3f46; }
29
 
30
  .score-card { padding: 16px; }
31
  .score-row { display: flex; align-items: baseline; justify-content: space-between; }
 
38
 
39
  .bar-wrap { margin-top: 10px; height: 4px; background: #f4f4f5; border-radius: 99px; overflow: hidden; }
40
  .bar-fill { height: 100%; border-radius: 99px; transition: width 0.6s ease; }
41
+ .bar-red { background: #ef4444; } .bar-amber { background: #f59e0b; } .bar-green { background: #22c55e; }
42
+
43
+ .scan-meta { margin-top: 8px; font-size: 11px; color: #a1a1aa; }
44
 
45
  .counts { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px; padding: 0 16px 12px; }
46
  .count-box { text-align: center; padding: 8px 4px; border-radius: 8px; border: 1px solid #f4f4f5; }
47
  .count-num { font-size: 18px; font-weight: 600; }
48
  .count-label { font-size: 10px; margin-top: 2px; display: flex; align-items: center; justify-content: center; gap: 4px; }
49
  .dot { width: 6px; height: 6px; border-radius: 50%; }
50
+ .dot-red { background: #ef4444; } .dot-amber { background: #f59e0b; } .dot-blue { background: #3b82f6; }
 
 
51
 
52
  .actions { padding: 0 16px 12px; display: flex; flex-direction: column; gap: 6px; }
53
  .btn { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px; border: none; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s; }
54
  .btn svg { width: 15px; height: 15px; }
55
+ .btn-primary { background: #18181b; color: #fff; } .btn-primary:hover { background: #27272a; }
56
+ .btn-secondary { background: #f4f4f5; color: #3f3f46; } .btn-secondary:hover { background: #e4e4e7; }
57
+
58
+ .limit-banner { margin: 0 16px 12px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; display: none; }
59
+ .limit-banner p { font-size: 12px; color: #991b1b; line-height: 1.5; }
60
+ .limit-title { font-weight: 600; margin-bottom: 4px; }
61
+ .btn-upgrade { background: #18181b; color: #fff; margin-top: 8px; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 8px; border: none; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; width: 100%; text-decoration: none; }
62
 
63
  .usage { padding: 10px 16px; border-top: 1px solid #f4f4f5; display: flex; align-items: center; justify-content: space-between; }
64
  .usage-text { font-size: 11px; color: #a1a1aa; }
 
67
 
68
  .empty { padding: 40px 16px; text-align: center; }
69
  .empty svg { width: 32px; height: 32px; color: #d4d4d8; margin: 0 auto 8px; }
70
+ .empty p { color: #a1a1aa; }
71
 
72
+ .footer { padding: 8px 16px; border-top: 1px solid #f4f4f5; display: flex; justify-content: space-between; }
73
+ .footer a { color: #a1a1aa; text-decoration: none; font-size: 11px; } .footer a:hover { color: #52525b; }
 
 
 
 
 
 
 
 
 
74
  </style>
75
  </head>
76
  <body>
 
79
  <h1>ClauseGuard</h1>
80
  </div>
81
 
82
+ <!-- Logged in user -->
83
+ <div id="user-bar" class="user-bar" style="display:none;">
84
+ <div class="user-avatar" id="user-avatar">?</div>
85
+ <div class="user-info"><p id="user-email">Loading...</p></div>
86
+ <span class="plan-badge plan-free" id="user-plan">FREE</span>
87
+ </div>
88
+
89
+ <!-- Guest -->
90
+ <div id="guest-bar" class="guest-bar" style="display:none;">
91
+ <p>Guest mode</p>
92
+ <button class="btn-link" id="btn-login">Sign in for more</button>
93
+ </div>
94
+
95
+ <div id="limit-banner" class="limit-banner">
96
+ <p class="limit-title">Free limit reached</p>
97
+ <p>Upgrade for unlimited scans.</p>
98
+ <a href="https://clauseguard.com/#pricing" target="_blank" class="btn-upgrade">View plans</a>
99
  </div>
100
 
101
  <div id="results-view" style="display:none;">
 
105
  <span class="grade" id="grade-badge"></span>
106
  </div>
107
  <div class="bar-wrap"><div class="bar-fill" id="bar-fill" style="width:0%"></div></div>
108
+ <div class="scan-meta">Engine: <span id="scan-source"></span></div>
109
  </div>
110
  <div class="counts">
111
  <div class="count-box"><div class="count-num" id="c-high">0</div><div class="count-label"><span class="dot dot-red"></span>High</div></div>
 
115
  <div class="actions">
116
  <button class="btn btn-primary" id="btn-details">
117
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
118
+ View details
119
  </button>
120
  <button class="btn btn-secondary" id="btn-rescan">
121
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
122
+ Re-scan
123
  </button>
124
  </div>
125
  </div>
126
 
127
+ <div id="empty-view" style="display:none;">
128
  <div class="empty">
129
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><path d="M7 12h10"/></svg>
130
  <p>No scan results yet.</p>
131
  </div>
132
  <div class="actions">
133
+ <button class="btn btn-primary" id="btn-scan">Scan this page</button>
 
 
 
134
  </div>
135
  </div>
136
 
137
  <div class="usage">
138
+ <span class="usage-text" id="usage-text">0/10 scans</span>
139
  <div class="usage-bar"><div class="usage-fill" id="usage-fill" style="width:0%"></div></div>
140
  </div>
141
 
142
  <div class="footer">
143
  <a href="https://clauseguard.com" target="_blank">clauseguard.com</a>
144
+ <a href="https://clauseguard.com/dashboard-pages/dashboard" target="_blank">Dashboard</a>
145
  <a href="https://clauseguard.com/#pricing" target="_blank">Upgrade</a>
146
  </div>
147
 
extension/popup.js CHANGED
@@ -1,26 +1,44 @@
1
  /**
2
  * ClauseGuard — Popup Script
 
3
  */
4
 
5
  document.addEventListener("DOMContentLoaded", async () => {
6
  const resultsView = document.getElementById("results-view");
7
  const emptyView = document.getElementById("empty-view");
8
  const limitBanner = document.getElementById("limit-banner");
9
-
10
- if (!resultsView || !emptyView) return;
 
 
11
 
12
  let tab;
13
  try { const [t] = await chrome.tabs.query({ active: true, currentWindow: true }); tab = t; } catch { return; }
14
  if (!tab?.id) return;
15
 
16
- // Check usage first — show limit banner if exhausted
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  let usage = null;
18
  try { usage = await chrome.runtime.sendMessage({ type: "CHECK_USAGE" }); } catch {}
19
  updateUsage(usage);
20
 
21
- if (usage && !usage.allowed && limitBanner) {
22
- limitBanner.style.display = "block";
23
- }
24
 
25
  // Load results
26
  let results = null;
@@ -29,66 +47,48 @@ document.addEventListener("DOMContentLoaded", async () => {
29
  if (results && results.risk_score !== undefined) {
30
  showResults(results);
31
  } else {
32
- emptyView.style.display = "block";
33
- resultsView.style.display = "none";
34
  }
35
 
36
  // Scan button
37
  const btnScan = document.getElementById("btn-scan");
38
  if (btnScan) {
39
  btnScan.addEventListener("click", async () => {
40
- // Check limit before scanning
41
- if (usage && !usage.allowed) {
42
- if (limitBanner) limitBanner.style.display = "block";
43
- return;
44
- }
45
-
46
- btnScan.textContent = "Scanning...";
47
- btnScan.disabled = true;
48
-
49
  try {
50
  await chrome.tabs.sendMessage(tab.id, { type: "TRIGGER_SCAN" });
51
  setTimeout(async () => {
52
  try {
53
  const r = await chrome.runtime.sendMessage({ type: "GET_RESULTS", tabId: tab.id });
54
- if (r && r.risk_score !== undefined) showResults(r);
55
- } catch {}
56
-
57
- // Refresh usage after scan
58
- try {
59
- usage = await chrome.runtime.sendMessage({ type: "CHECK_USAGE" });
60
- updateUsage(usage);
61
- if (usage && !usage.allowed && limitBanner) limitBanner.style.display = "block";
62
  } catch {}
63
-
64
- btnScan.textContent = "Scan this page";
65
- btnScan.disabled = false;
66
  }, 3000);
67
- } catch {
68
- btnScan.textContent = "Error — refresh page";
69
- btnScan.disabled = false;
70
- }
71
  });
72
  }
73
 
74
  // Re-scan
75
  const btnRescan = document.getElementById("btn-rescan");
76
- if (btnRescan) {
77
- btnRescan.addEventListener("click", async () => {
78
- if (usage && !usage.allowed) { if (limitBanner) limitBanner.style.display = "block"; return; }
79
- try { await chrome.tabs.sendMessage(tab.id, { type: "TRIGGER_SCAN" }); } catch {}
80
- window.close();
81
- });
82
- }
83
 
84
  // Details
85
  const btnDetails = document.getElementById("btn-details");
86
- if (btnDetails) {
87
- btnDetails.addEventListener("click", () => {
88
- try { chrome.sidePanel.open({ tabId: tab.id }); } catch {}
89
- window.close();
90
- });
91
- }
 
 
 
92
  });
93
 
94
  function showResults(results) {
@@ -97,31 +97,28 @@ function showResults(results) {
97
  if (rv) rv.style.display = "block";
98
  if (ev) ev.style.display = "none";
99
 
100
- const scoreEl = document.getElementById("risk-score");
101
- if (scoreEl) scoreEl.textContent = results.risk_score;
102
 
103
  const grade = results.grade || "C";
104
- const badge = document.getElementById("grade-badge");
105
- if (badge) {
106
- badge.className = "grade grade-" + grade.toLowerCase();
107
- badge.textContent = "Grade " + grade;
108
- }
109
 
110
- const bar = document.getElementById("bar-fill");
111
  if (bar) {
112
  bar.style.width = results.risk_score + "%";
113
  bar.className = "bar-fill " + (results.risk_score >= 60 ? "bar-red" : results.risk_score >= 30 ? "bar-amber" : "bar-green");
114
  }
115
 
116
  const counts = { HIGH: 0, MEDIUM: 0, LOW: 0 };
117
- (results.results || []).forEach(r => (r.categories || []).forEach(c => {
118
- if (counts[c.severity] !== undefined) counts[c.severity]++;
119
- }));
120
-
121
- const el = (id) => document.getElementById(id);
122
  if (el("c-high")) el("c-high").textContent = counts.HIGH;
123
  if (el("c-med")) el("c-med").textContent = counts.MEDIUM;
124
  if (el("c-low")) el("c-low").textContent = counts.LOW;
 
 
 
 
125
  }
126
 
127
  function updateUsage(usage) {
@@ -129,16 +126,14 @@ function updateUsage(usage) {
129
  const text = document.getElementById("usage-text");
130
  const fill = document.getElementById("usage-fill");
131
  if (!text || !fill) return;
132
-
133
  if (usage.plan === "free") {
134
- text.textContent = "Free: " + usage.used + "/" + usage.limit + " scans";
135
  const pct = Math.min(100, (usage.used / usage.limit) * 100);
136
  fill.style.width = pct + "%";
137
  if (pct >= 100) fill.style.background = "#ef4444";
138
  else if (pct >= 70) fill.style.background = "#f59e0b";
139
  } else {
140
- text.textContent = usage.plan.toUpperCase() + " — unlimited";
141
- fill.style.width = "100%";
142
- fill.style.background = "#22c55e";
143
  }
144
  }
 
1
  /**
2
  * ClauseGuard — Popup Script
3
+ * Shows user status (logged in / guest), scan results, usage.
4
  */
5
 
6
  document.addEventListener("DOMContentLoaded", async () => {
7
  const resultsView = document.getElementById("results-view");
8
  const emptyView = document.getElementById("empty-view");
9
  const limitBanner = document.getElementById("limit-banner");
10
+ const userBar = document.getElementById("user-bar");
11
+ const userEmail = document.getElementById("user-email");
12
+ const userPlan = document.getElementById("user-plan");
13
+ const guestBar = document.getElementById("guest-bar");
14
 
15
  let tab;
16
  try { const [t] = await chrome.tabs.query({ active: true, currentWindow: true }); tab = t; } catch { return; }
17
  if (!tab?.id) return;
18
 
19
+ // Load user info
20
+ let auth = null;
21
+ try { auth = await chrome.runtime.sendMessage({ type: "GET_USER" }); } catch {}
22
+
23
+ if (auth?.isLoggedIn) {
24
+ if (userBar) userBar.style.display = "flex";
25
+ if (guestBar) guestBar.style.display = "none";
26
+ if (userEmail) userEmail.textContent = auth.email || "User";
27
+ if (userPlan) {
28
+ userPlan.textContent = auth.plan?.toUpperCase() || "FREE";
29
+ userPlan.className = "plan-badge plan-" + (auth.plan || "free");
30
+ }
31
+ } else {
32
+ if (userBar) userBar.style.display = "none";
33
+ if (guestBar) guestBar.style.display = "flex";
34
+ }
35
+
36
+ // Check usage
37
  let usage = null;
38
  try { usage = await chrome.runtime.sendMessage({ type: "CHECK_USAGE" }); } catch {}
39
  updateUsage(usage);
40
 
41
+ if (usage && !usage.allowed && limitBanner) limitBanner.style.display = "block";
 
 
42
 
43
  // Load results
44
  let results = null;
 
47
  if (results && results.risk_score !== undefined) {
48
  showResults(results);
49
  } else {
50
+ if (emptyView) emptyView.style.display = "block";
51
+ if (resultsView) resultsView.style.display = "none";
52
  }
53
 
54
  // Scan button
55
  const btnScan = document.getElementById("btn-scan");
56
  if (btnScan) {
57
  btnScan.addEventListener("click", async () => {
58
+ if (usage && !usage.allowed) { if (limitBanner) limitBanner.style.display = "block"; return; }
59
+ btnScan.textContent = "Scanning..."; btnScan.disabled = true;
 
 
 
 
 
 
 
60
  try {
61
  await chrome.tabs.sendMessage(tab.id, { type: "TRIGGER_SCAN" });
62
  setTimeout(async () => {
63
  try {
64
  const r = await chrome.runtime.sendMessage({ type: "GET_RESULTS", tabId: tab.id });
65
+ if (r?.risk_score !== undefined) showResults(r);
 
 
 
 
 
 
 
66
  } catch {}
67
+ try { usage = await chrome.runtime.sendMessage({ type: "CHECK_USAGE" }); updateUsage(usage); } catch {}
68
+ btnScan.textContent = "Scan this page"; btnScan.disabled = false;
 
69
  }, 3000);
70
+ } catch { btnScan.textContent = "Error — refresh page"; btnScan.disabled = false; }
 
 
 
71
  });
72
  }
73
 
74
  // Re-scan
75
  const btnRescan = document.getElementById("btn-rescan");
76
+ if (btnRescan) btnRescan.addEventListener("click", async () => {
77
+ if (usage && !usage.allowed) { if (limitBanner) limitBanner.style.display = "block"; return; }
78
+ try { await chrome.tabs.sendMessage(tab.id, { type: "TRIGGER_SCAN" }); } catch {} window.close();
79
+ });
 
 
 
80
 
81
  // Details
82
  const btnDetails = document.getElementById("btn-details");
83
+ if (btnDetails) btnDetails.addEventListener("click", () => {
84
+ try { chrome.sidePanel.open({ tabId: tab.id }); } catch {} window.close();
85
+ });
86
+
87
+ // Login button
88
+ const btnLogin = document.getElementById("btn-login");
89
+ if (btnLogin) btnLogin.addEventListener("click", () => {
90
+ chrome.tabs.create({ url: "https://clauseguard.com/auth/login" }); // Update with your actual URL
91
+ });
92
  });
93
 
94
  function showResults(results) {
 
97
  if (rv) rv.style.display = "block";
98
  if (ev) ev.style.display = "none";
99
 
100
+ const el = (id) => document.getElementById(id);
101
+ if (el("risk-score")) el("risk-score").textContent = results.risk_score;
102
 
103
  const grade = results.grade || "C";
104
+ const badge = el("grade-badge");
105
+ if (badge) { badge.className = "grade grade-" + grade.toLowerCase(); badge.textContent = "Grade " + grade; }
 
 
 
106
 
107
+ const bar = el("bar-fill");
108
  if (bar) {
109
  bar.style.width = results.risk_score + "%";
110
  bar.className = "bar-fill " + (results.risk_score >= 60 ? "bar-red" : results.risk_score >= 30 ? "bar-amber" : "bar-green");
111
  }
112
 
113
  const counts = { HIGH: 0, MEDIUM: 0, LOW: 0 };
114
+ (results.results || []).forEach(r => (r.categories || []).forEach(c => { if (counts[c.severity] !== undefined) counts[c.severity]++; }));
 
 
 
 
115
  if (el("c-high")) el("c-high").textContent = counts.HIGH;
116
  if (el("c-med")) el("c-med").textContent = counts.MEDIUM;
117
  if (el("c-low")) el("c-low").textContent = counts.LOW;
118
+
119
+ // Show source indicator
120
+ const src = el("scan-source");
121
+ if (src) src.textContent = results.source === "api" ? "Legal-BERT" : results.source === "local" ? "Local" : "";
122
  }
123
 
124
  function updateUsage(usage) {
 
126
  const text = document.getElementById("usage-text");
127
  const fill = document.getElementById("usage-fill");
128
  if (!text || !fill) return;
 
129
  if (usage.plan === "free") {
130
+ text.textContent = usage.used + "/" + usage.limit + " scans";
131
  const pct = Math.min(100, (usage.used / usage.limit) * 100);
132
  fill.style.width = pct + "%";
133
  if (pct >= 100) fill.style.background = "#ef4444";
134
  else if (pct >= 70) fill.style.background = "#f59e0b";
135
  } else {
136
+ text.textContent = "Unlimited";
137
+ fill.style.width = "100%"; fill.style.background = "#22c55e";
 
138
  }
139
  }
web/app/layout.tsx CHANGED
@@ -1,5 +1,6 @@
1
  import type { Metadata } from "next";
2
  import { Nav } from "@/components/nav";
 
3
  import "./globals.css";
4
 
5
  export const metadata: Metadata = {
@@ -21,6 +22,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
21
  <body className="antialiased text-zinc-900 bg-white">
22
  <Nav />
23
  {children}
 
24
  </body>
25
  </html>
26
  );
 
1
  import type { Metadata } from "next";
2
  import { Nav } from "@/components/nav";
3
+ import { ExtensionBridge } from "@/components/extension-bridge";
4
  import "./globals.css";
5
 
6
  export const metadata: Metadata = {
 
22
  <body className="antialiased text-zinc-900 bg-white">
23
  <Nav />
24
  {children}
25
+ <ExtensionBridge />
26
  </body>
27
  </html>
28
  );
web/components/extension-bridge.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { createClient } from "@/lib/supabase/client";
5
+
6
+ /**
7
+ * ExtensionBridge — Add to root layout.
8
+ * Syncs auth state to ClauseGuard Chrome extension.
9
+ * Also detects if extension is installed.
10
+ */
11
+ export function ExtensionBridge() {
12
+ const [extensionInstalled, setExtensionInstalled] = useState(false);
13
+ const supabase = createClient();
14
+
15
+ useEffect(() => {
16
+ // Sync auth to extension whenever auth state changes
17
+ const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
18
+ if (event === "SIGNED_IN" && session) {
19
+ // Get user profile for plan info
20
+ const { data: profile } = await supabase
21
+ .from("profiles")
22
+ .select("plan, full_name")
23
+ .eq("id", session.user.id)
24
+ .single();
25
+
26
+ // Method 1: postMessage (content script picks it up)
27
+ window.postMessage({
28
+ type: "CLAUSEGUARD_AUTH_SYNC",
29
+ token: session.access_token,
30
+ email: session.user.email || "",
31
+ name: profile?.full_name || session.user.user_metadata?.full_name || "",
32
+ userId: session.user.id,
33
+ plan: profile?.plan || "free",
34
+ }, "*");
35
+ }
36
+
37
+ if (event === "SIGNED_OUT") {
38
+ window.postMessage({ type: "CLAUSEGUARD_AUTH_SYNC", token: "", email: "", name: "", userId: "", plan: "free" }, "*");
39
+ }
40
+ });
41
+
42
+ // Also sync on initial load if already logged in
43
+ supabase.auth.getUser().then(async ({ data: { user } }) => {
44
+ if (user) {
45
+ const { data: session } = await supabase.auth.getSession();
46
+ const { data: profile } = await supabase.from("profiles").select("plan, full_name").eq("id", user.id).single();
47
+
48
+ window.postMessage({
49
+ type: "CLAUSEGUARD_AUTH_SYNC",
50
+ token: session.session?.access_token || "",
51
+ email: user.email || "",
52
+ name: profile?.full_name || "",
53
+ userId: user.id,
54
+ plan: profile?.plan || "free",
55
+ }, "*");
56
+ }
57
+ });
58
+
59
+ return () => subscription.unsubscribe();
60
+ }, []);
61
+
62
+ // This component renders nothing — it's purely for side effects
63
+ return null;
64
+ }