feat: implement M2 Chrome browser extension

Build the CookieBridge Chrome extension (Manifest V3) with:

- Background service worker: cookie monitoring via chrome.cookies.onChanged,
  WebSocket connection to relay server with auto-reconnect, HTTP polling
  fallback, device registration and pairing flow
- Browser-compatible crypto: libsodium-wrappers-sumo for XChaCha20-Poly1305
  encryption, Ed25519 signing, X25519 key exchange (mirrors server's
  sodium-native API)
- Popup UI: device registration, connection status indicator (gray/blue/
  green/red), cookie/device/sync stats, one-click current site sync,
  whitelist quick-add, device pairing with 6-digit code
- Options page: server URL config, connection mode (auto/WS/polling),
  poll interval slider, auto-sync toggle, domain whitelist/blacklist
  management, paired device list, key export/import, data clearing
- Sync engine: LWW conflict resolution with Lamport clocks (same as
  server), bidirectional cookie sync with all paired peers, echo
  suppression to prevent sync loops
- Badge management: icon color reflects state (gray=not logged in,
  blue=connected, green=syncing with count, red=error)
- Build system: esbuild bundling for Chrome 120+, TypeScript with
  strict mode, clean type checking

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
徐枫
2026-03-17 16:30:18 +08:00
parent 1bd7a34de8
commit dc3be4d73f
36 changed files with 3549 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
/**
* Badge/icon management — updates extension icon color and badge text
* based on connection status and sync activity.
*
* States:
* - gray: Not logged in / no device identity
* - blue: Connected, idle
* - green: Syncing (with count badge)
* - red: Error / disconnected
*/
type IconColor = "gray" | "blue" | "green" | "red";
function iconPath(color: IconColor, size: number): string {
return `src/icons/icon-${color}-${size}.png`;
}
function iconSet(color: IconColor): Record<string, string> {
return {
"16": iconPath(color, 16),
"48": iconPath(color, 48),
"128": iconPath(color, 128),
};
}
export async function setIconState(
state: "not_logged_in" | "connected" | "syncing" | "error",
syncCount?: number,
) {
switch (state) {
case "not_logged_in":
await chrome.action.setIcon({ path: iconSet("gray") });
await chrome.action.setBadgeText({ text: "" });
break;
case "connected":
await chrome.action.setIcon({ path: iconSet("blue") });
await chrome.action.setBadgeText({ text: "" });
break;
case "syncing":
await chrome.action.setIcon({ path: iconSet("green") });
if (syncCount && syncCount > 0) {
await chrome.action.setBadgeText({
text: syncCount > 99 ? "99+" : String(syncCount),
});
await chrome.action.setBadgeBackgroundColor({ color: "#22C55E" });
}
break;
case "error":
await chrome.action.setIcon({ path: iconSet("red") });
await chrome.action.setBadgeText({ text: "!" });
await chrome.action.setBadgeBackgroundColor({ color: "#EF4444" });
break;
}
}
/** Clear the sync badge after a delay. */
export function clearSyncBadge(delayMs = 3000) {
setTimeout(async () => {
const state = await chrome.storage.local.get(["apiToken"]);
if (state.apiToken) {
await setIconState("connected");
} else {
await setIconState("not_logged_in");
}
}, delayMs);
}