gaurv007 commited on
Commit
74baa7b
·
verified ·
1 Parent(s): d219561

Fix auth sync: handle INITIAL_SESSION + TOKEN_REFRESHED events, sync on tab visibility change, remove flaky scripting.executeScript approach, add console.log for debugging

Browse files
extension/background.js CHANGED
@@ -84,61 +84,11 @@ chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) =>
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) {
 
84
  return true;
85
  });
86
 
87
+ // Auth sync is handled by:
88
+ // 1. Website's ExtensionBridge component sends postMessage on auth change
89
+ // 2. Content script (content.js) picks it up via window.addEventListener("message")
90
+ // 3. Content script writes to chrome.storage.sync
91
+ // No injection needed — this is the reliable path.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
  // ─── Core: Analyze ───
94
  async function handleAnalyze(payload, tabId) {
extension/content.js CHANGED
@@ -1,6 +1,11 @@
1
  /**
2
  * ClauseGuard — Content Script
3
- * Page scanning + highlighting + auth bridge (listens for website auth sync).
 
 
 
 
 
4
  */
5
 
6
  (() => {
@@ -11,23 +16,34 @@
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
 
@@ -51,7 +67,7 @@
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
 
@@ -78,10 +94,7 @@
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
  }
@@ -96,7 +109,7 @@
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) {}
@@ -128,7 +141,7 @@
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();
 
1
  /**
2
  * ClauseGuard — Content Script
3
+ * Page scanning + highlighting + auth bridge.
4
+ *
5
+ * Auth bridge: listens for postMessage from the website's ExtensionBridge component.
6
+ * Content scripts CAN receive window.postMessage from the page — they share the same
7
+ * window object. The message handler checks event.source === window to ensure it's
8
+ * from the same page (not an iframe).
9
  */
10
 
11
  (() => {
 
16
  let isScanning = false;
17
  let currentHighlights = [];
18
 
19
+ // ─── Auth Bridge ───
20
+ // Listen for auth sync from our website (ExtensionBridge component sends this)
21
  window.addEventListener("message", (event) => {
22
+ // Only accept from same window (not iframes)
23
+ if (event.source !== window) return;
24
+ if (!event.data || event.data.type !== "CLAUSEGUARD_AUTH_SYNC") return;
 
25
 
26
+ const { token, email, name, userId, plan } = event.data;
27
+
28
+ if (token) {
29
+ // User is logged in — store auth
30
  chrome.storage.sync.set({
31
+ authToken: token,
32
+ email: email || "",
33
+ userName: name || "",
34
+ userId: userId || "",
35
+ plan: plan || "free",
36
  authSource: "website",
37
  lastSyncAt: Date.now(),
38
+ }, () => {
39
+ console.log("ClauseGuard: auth synced from website —", email, plan);
40
  });
41
+ } else {
42
+ // User logged out — clear auth
43
+ chrome.storage.sync.remove(
44
+ ["authToken", "email", "userName", "userId", "plan", "authSource", "lastSyncAt"],
45
+ () => { console.log("ClauseGuard: auth cleared (logout)"); }
46
+ );
47
  }
48
  });
49
 
 
67
  try {
68
  const results = await chrome.runtime.sendMessage({ type: "ANALYZE_TEXT", payload: { text, url: window.location.href } });
69
  if (results && !results.error) { clearHighlights(); highlightResults(results); }
70
+ } catch (err) { /* extension context invalidated — ignore */ }
71
  isScanning = false;
72
  }
73
 
 
94
  let node;
95
  while ((node = walker.nextNode())) {
96
  const idx = node.textContent.toLowerCase().indexOf(searchLower);
97
+ if (idx !== -1) { results.push({ node, startOffset: idx, endOffset: Math.min(idx + searchText.length, node.textContent.length) }); break; }
 
 
 
98
  }
99
  return results;
100
  }
 
109
  mark.dataset.categories = JSON.stringify(clauseData.categories);
110
  mark.addEventListener("mouseenter", showTooltip);
111
  mark.addEventListener("mouseleave", hideTooltip);
112
+ mark.addEventListener("click", () => { try { chrome.runtime.sendMessage({ type: "OPEN_SIDEPANEL" }); } catch {} });
113
  range.surroundContents(mark);
114
  currentHighlights.push(mark);
115
  } catch (e) {}
 
141
  currentHighlights = [];
142
  }
143
 
144
+ // ─── Auto-scan (debounced) ───
145
  let scanTimeout = null;
146
  function debouncedScan() { clearTimeout(scanTimeout); scanTimeout = setTimeout(scanPage, 1500); }
147
  if (document.readyState === "complete") debouncedScan();
web/components/extension-bridge.tsx CHANGED
@@ -1,64 +1,95 @@
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
  }
 
1
  "use client";
2
 
3
+ import { useEffect } from "react";
4
  import { createClient } from "@/lib/supabase/client";
5
 
6
  /**
7
+ * ExtensionBridge — syncs auth to Chrome extension.
8
+ * Uses window.postMessage which content script picks up.
9
+ * Handles: initial load, sign in, sign out, token refresh, tab switch.
10
  */
11
  export function ExtensionBridge() {
 
12
  const supabase = createClient();
13
 
14
+ function sendAuthToExtension(token: string, email: string, name: string, userId: string, plan: string) {
15
+ // Content script listens for this via window.addEventListener("message")
16
+ window.postMessage({
17
+ type: "CLAUSEGUARD_AUTH_SYNC",
18
+ token, email, name, userId, plan,
19
+ }, window.location.origin);
20
+ }
21
+
22
+ async function syncCurrentUser() {
23
+ try {
24
+ const { data: { user } } = await supabase.auth.getUser();
25
+ if (!user) {
26
+ sendAuthToExtension("", "", "", "", "free");
27
+ return;
28
+ }
29
+
30
+ const { data: { session } } = await supabase.auth.getSession();
31
+ if (!session) {
32
+ sendAuthToExtension("", "", "", "", "free");
33
+ return;
34
+ }
35
+
36
+ const { data: profile } = await supabase
37
+ .from("profiles")
38
+ .select("plan, full_name")
39
+ .eq("id", user.id)
40
+ .single();
41
+
42
+ sendAuthToExtension(
43
+ session.access_token,
44
+ user.email || "",
45
+ profile?.full_name || user.user_metadata?.full_name || user.user_metadata?.name || "",
46
+ user.id,
47
+ profile?.plan || "free",
48
+ );
49
+ } catch {}
50
+ }
51
+
52
  useEffect(() => {
53
+ // Sync on initial page load (already logged in user opens new tab)
54
+ syncCurrentUser();
55
+
56
+ // Sync on every auth state change
57
  const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
58
+ // Handle ALL events that mean "user is logged in"
59
+ if (session && (event === "SIGNED_IN" || event === "INITIAL_SESSION" || event === "TOKEN_REFRESHED")) {
60
  const { data: profile } = await supabase
61
  .from("profiles")
62
  .select("plan, full_name")
63
  .eq("id", session.user.id)
64
+ .single()
65
+ .then(r => r)
66
+ .catch(() => ({ data: null }));
67
 
68
+ sendAuthToExtension(
69
+ session.access_token,
70
+ session.user.email || "",
71
+ profile?.full_name || session.user.user_metadata?.full_name || "",
72
+ session.user.id,
73
+ profile?.plan || "free",
74
+ );
 
 
75
  }
76
 
77
  if (event === "SIGNED_OUT") {
78
+ sendAuthToExtension("", "", "", "", "free");
79
  }
80
  });
81
 
82
+ // Also sync when tab becomes visible (user switches back to this tab)
83
+ function onVisible() {
84
+ if (document.visibilityState === "visible") syncCurrentUser();
85
+ }
86
+ document.addEventListener("visibilitychange", onVisible);
 
 
 
 
 
 
 
 
 
 
 
87
 
88
+ return () => {
89
+ subscription.unsubscribe();
90
+ document.removeEventListener("visibilitychange", onVisible);
91
+ };
92
  }, []);
93
 
 
94
  return null;
95
  }