Spaces:
Running
Running
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 +5 -55
- extension/content.js +32 -19
- web/components/extension-bridge.tsx +68 -37
extension/background.js
CHANGED
|
@@ -84,61 +84,11 @@ chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) =>
|
|
| 84 |
return true;
|
| 85 |
});
|
| 86 |
|
| 87 |
-
//
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
*/
|
| 5 |
|
| 6 |
(() => {
|
|
@@ -11,23 +16,34 @@
|
|
| 11 |
let isScanning = false;
|
| 12 |
let currentHighlights = [];
|
| 13 |
|
| 14 |
-
// ─── Auth Bridge
|
|
|
|
| 15 |
window.addEventListener("message", (event) => {
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
}).catch(() => {});
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
| 22 |
chrome.storage.sync.set({
|
| 23 |
-
authToken:
|
| 24 |
-
email:
|
| 25 |
-
userName:
|
| 26 |
-
userId:
|
| 27 |
-
plan:
|
| 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) {
|
| 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
|
| 4 |
import { createClient } from "@/lib/supabase/client";
|
| 5 |
|
| 6 |
/**
|
| 7 |
-
* ExtensionBridge —
|
| 8 |
-
*
|
| 9 |
-
*
|
| 10 |
*/
|
| 11 |
export function ExtensionBridge() {
|
| 12 |
-
const [extensionInstalled, setExtensionInstalled] = useState(false);
|
| 13 |
const supabase = createClient();
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
useEffect(() => {
|
| 16 |
-
// Sync
|
|
|
|
|
|
|
|
|
|
| 17 |
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
|
| 18 |
-
|
| 19 |
-
|
| 20 |
const { data: profile } = await supabase
|
| 21 |
.from("profiles")
|
| 22 |
.select("plan, full_name")
|
| 23 |
.eq("id", session.user.id)
|
| 24 |
-
.single()
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
plan: profile?.plan || "free",
|
| 34 |
-
}, "*");
|
| 35 |
}
|
| 36 |
|
| 37 |
if (event === "SIGNED_OUT") {
|
| 38 |
-
|
| 39 |
}
|
| 40 |
});
|
| 41 |
|
| 42 |
-
// Also sync
|
| 43 |
-
|
| 44 |
-
if (
|
| 45 |
-
|
| 46 |
-
|
| 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 () =>
|
|
|
|
|
|
|
|
|
|
| 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 |
}
|