feat: implement M3 multi-browser support (Firefox, Edge, Safari)
Add browser abstraction layer (compat.ts) that normalizes Chrome/Firefox/ Edge/Safari extension APIs behind a unified promise-based interface. Replace all direct chrome.* calls with compat layer across service worker, sync engine, badge, storage, popup, and options modules. - Browser-specific manifests in manifests/ (Firefox MV3 with gecko settings, Edge/Safari variants) - Multi-target build system: `npm run build` produces all four browser builds in build/<browser>/ - Per-browser build scripts: build:chrome, build:firefox, build:edge, build:safari - Auto-detects browser at runtime for platform-specific device registration - All 38 existing tests pass, typecheck clean Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* CookieBridge background service worker.
|
||||
* Manages device identity, connection lifecycle, and cookie sync.
|
||||
* Uses the browser-agnostic compat layer for cross-browser support.
|
||||
*/
|
||||
import {
|
||||
generateKeyPair,
|
||||
@@ -15,6 +16,7 @@ import { ApiClient } from "../lib/api-client";
|
||||
import { SyncEngine } from "../lib/sync";
|
||||
import { setIconState, clearSyncBadge } from "../lib/badge";
|
||||
import { MESSAGE_TYPES, POLL_INTERVAL_MS, type Envelope } from "../lib/protocol";
|
||||
import { alarms, runtime, cookies, getPlatformName } from "../lib/compat";
|
||||
|
||||
let connection: ConnectionManager | null = null;
|
||||
let syncEngine: SyncEngine | null = null;
|
||||
@@ -77,18 +79,18 @@ async function handleIncomingMessage(envelope: Envelope, peers: PeerDevice[]) {
|
||||
}
|
||||
|
||||
function startPolling(intervalSec: number) {
|
||||
chrome.alarms.create(pollAlarmName, {
|
||||
alarms.create(pollAlarmName, {
|
||||
periodInMinutes: Math.max(intervalSec / 60, 1 / 60),
|
||||
});
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
chrome.alarms.clear(pollAlarmName);
|
||||
alarms.clear(pollAlarmName);
|
||||
}
|
||||
|
||||
// --- Alarm handler for HTTP polling ---
|
||||
|
||||
chrome.alarms.onAlarm.addListener(async (alarm) => {
|
||||
alarms.onAlarm.addListener(async (alarm) => {
|
||||
if (alarm.name !== pollAlarmName) return;
|
||||
if (!api) return;
|
||||
|
||||
@@ -114,8 +116,8 @@ export interface ExtensionMessage {
|
||||
payload?: unknown;
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(
|
||||
(message: ExtensionMessage, _sender, sendResponse) => {
|
||||
runtime.onMessage.addListener(
|
||||
(message: ExtensionMessage, _sender: any, sendResponse: (response: any) => void) => {
|
||||
handleMessage(message).then(sendResponse).catch((err) => {
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
@@ -226,8 +228,8 @@ async function getStatus(): Promise<{
|
||||
let cookieCount = 0;
|
||||
if (state.whitelist.length > 0) {
|
||||
for (const domain of state.whitelist) {
|
||||
const cookies = await chrome.cookies.getAll({ domain });
|
||||
cookieCount += cookies.length;
|
||||
const domainCookies = await cookies.getAll({ domain });
|
||||
cookieCount += domainCookies.length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +255,7 @@ async function registerDevice(params: { name: string }) {
|
||||
const device = await client.registerDevice({
|
||||
deviceId,
|
||||
name: params.name,
|
||||
platform: "chrome-extension",
|
||||
platform: getPlatformName(),
|
||||
encPub: serialized.encPub,
|
||||
});
|
||||
|
||||
@@ -303,11 +305,11 @@ async function acceptPairing(params: { code: string }) {
|
||||
|
||||
// --- Start on install/startup ---
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
runtime.onInstalled.addListener(() => {
|
||||
initialize();
|
||||
});
|
||||
|
||||
chrome.runtime.onStartup.addListener(() => {
|
||||
runtime.onStartup.addListener(() => {
|
||||
initialize();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Badge/icon management — updates extension icon color and badge text
|
||||
* based on connection status and sync activity.
|
||||
* Uses the browser-agnostic compat layer.
|
||||
*
|
||||
* States:
|
||||
* - gray: Not logged in / no device identity
|
||||
@@ -8,6 +9,7 @@
|
||||
* - green: Syncing (with count badge)
|
||||
* - red: Error / disconnected
|
||||
*/
|
||||
import { action, storage } from "./compat";
|
||||
|
||||
type IconColor = "gray" | "blue" | "green" | "red";
|
||||
|
||||
@@ -29,29 +31,29 @@ export async function setIconState(
|
||||
) {
|
||||
switch (state) {
|
||||
case "not_logged_in":
|
||||
await chrome.action.setIcon({ path: iconSet("gray") });
|
||||
await chrome.action.setBadgeText({ text: "" });
|
||||
await action.setIcon({ path: iconSet("gray") });
|
||||
await action.setBadgeText({ text: "" });
|
||||
break;
|
||||
|
||||
case "connected":
|
||||
await chrome.action.setIcon({ path: iconSet("blue") });
|
||||
await chrome.action.setBadgeText({ text: "" });
|
||||
await action.setIcon({ path: iconSet("blue") });
|
||||
await action.setBadgeText({ text: "" });
|
||||
break;
|
||||
|
||||
case "syncing":
|
||||
await chrome.action.setIcon({ path: iconSet("green") });
|
||||
await action.setIcon({ path: iconSet("green") });
|
||||
if (syncCount && syncCount > 0) {
|
||||
await chrome.action.setBadgeText({
|
||||
await action.setBadgeText({
|
||||
text: syncCount > 99 ? "99+" : String(syncCount),
|
||||
});
|
||||
await chrome.action.setBadgeBackgroundColor({ color: "#22C55E" });
|
||||
await 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" });
|
||||
await action.setIcon({ path: iconSet("red") });
|
||||
await action.setBadgeText({ text: "!" });
|
||||
await action.setBadgeBackgroundColor({ color: "#EF4444" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -59,7 +61,7 @@ export async function setIconState(
|
||||
/** Clear the sync badge after a delay. */
|
||||
export function clearSyncBadge(delayMs = 3000) {
|
||||
setTimeout(async () => {
|
||||
const state = await chrome.storage.local.get(["apiToken"]);
|
||||
const state = await storage.get(["apiToken"]);
|
||||
if (state.apiToken) {
|
||||
await setIconState("connected");
|
||||
} else {
|
||||
|
||||
278
extension/src/lib/compat.ts
Normal file
278
extension/src/lib/compat.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
/**
|
||||
* Browser compatibility layer — provides a unified API across Chrome, Firefox, Edge, and Safari.
|
||||
*
|
||||
* Firefox uses the `browser.*` namespace with promise-based APIs.
|
||||
* Chrome/Edge use `chrome.*` with callback-based APIs.
|
||||
* Safari uses `browser.*` (WebExtensions) with some limitations.
|
||||
*
|
||||
* This module detects the environment and exports a normalized `browserAPI`
|
||||
* that always returns promises and works across all supported browsers.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
type BrowserType = "chrome" | "firefox" | "safari" | "edge" | "unknown";
|
||||
|
||||
function detectBrowser(): BrowserType {
|
||||
if (typeof globalThis !== "undefined") {
|
||||
const ua =
|
||||
typeof navigator !== "undefined" ? navigator.userAgent || "" : "";
|
||||
|
||||
// Safari: check before Chrome since Safari UA may contain "Chrome" in some contexts
|
||||
if (
|
||||
ua.includes("Safari") &&
|
||||
!ua.includes("Chrome") &&
|
||||
!ua.includes("Chromium")
|
||||
) {
|
||||
return "safari";
|
||||
}
|
||||
// Firefox
|
||||
if (
|
||||
typeof (globalThis as any).browser !== "undefined" &&
|
||||
typeof (globalThis as any).browser.runtime !== "undefined" &&
|
||||
ua.includes("Firefox")
|
||||
) {
|
||||
return "firefox";
|
||||
}
|
||||
// Edge (Chromium-based)
|
||||
if (ua.includes("Edg/")) {
|
||||
return "edge";
|
||||
}
|
||||
// Chrome
|
||||
if (
|
||||
typeof (globalThis as any).chrome !== "undefined" &&
|
||||
typeof (globalThis as any).chrome.runtime !== "undefined"
|
||||
) {
|
||||
return "chrome";
|
||||
}
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export const BROWSER: BrowserType = detectBrowser();
|
||||
|
||||
/**
|
||||
* Get the raw extension API object (`browser` on Firefox/Safari, `chrome` on Chrome/Edge).
|
||||
*/
|
||||
function getRawAPI(): any {
|
||||
if (
|
||||
typeof (globalThis as any).browser !== "undefined" &&
|
||||
(globalThis as any).browser.runtime
|
||||
) {
|
||||
return (globalThis as any).browser;
|
||||
}
|
||||
if (
|
||||
typeof (globalThis as any).chrome !== "undefined" &&
|
||||
(globalThis as any).chrome.runtime
|
||||
) {
|
||||
return (globalThis as any).chrome;
|
||||
}
|
||||
throw new Error("No extension API found");
|
||||
}
|
||||
|
||||
const raw = getRawAPI();
|
||||
|
||||
/**
|
||||
* Wraps a callback-style Chrome API call into a promise.
|
||||
* Firefox's `browser.*` already returns promises so we detect and pass through.
|
||||
*/
|
||||
function promisify<T>(fn: (...args: any[]) => any, ...args: any[]): Promise<T> {
|
||||
const result = fn(...args);
|
||||
if (result && typeof result.then === "function") {
|
||||
return result;
|
||||
}
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
fn(...args, (val: T) => {
|
||||
if (raw.runtime.lastError) {
|
||||
reject(new Error(raw.runtime.lastError.message));
|
||||
} else {
|
||||
resolve(val);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Storage ───
|
||||
|
||||
export const storage = {
|
||||
async get(keys: string | string[] | null): Promise<Record<string, any>> {
|
||||
return promisify((k: any, cb?: any) => raw.storage.local.get(k, cb), keys);
|
||||
},
|
||||
async set(items: Record<string, any>): Promise<void> {
|
||||
return promisify((i: any, cb?: any) => raw.storage.local.set(i, cb), items);
|
||||
},
|
||||
async clear(): Promise<void> {
|
||||
return promisify((cb?: any) => raw.storage.local.clear(cb));
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Cookies ───
|
||||
|
||||
export interface CompatCookie {
|
||||
domain: string;
|
||||
name: string;
|
||||
value: string;
|
||||
path: string;
|
||||
secure: boolean;
|
||||
httpOnly: boolean;
|
||||
sameSite: string;
|
||||
expirationDate?: number;
|
||||
}
|
||||
|
||||
export interface CompatCookieChangeInfo {
|
||||
cookie: CompatCookie;
|
||||
removed: boolean;
|
||||
cause: string;
|
||||
}
|
||||
|
||||
export const cookies = {
|
||||
async getAll(details: { domain?: string; url?: string }): Promise<CompatCookie[]> {
|
||||
return promisify(
|
||||
(d: any, cb?: any) => raw.cookies.getAll(d, cb),
|
||||
details,
|
||||
);
|
||||
},
|
||||
async set(details: Record<string, any>): Promise<CompatCookie | null> {
|
||||
return promisify(
|
||||
(d: any, cb?: any) => raw.cookies.set(d, cb),
|
||||
details,
|
||||
);
|
||||
},
|
||||
async remove(details: { url: string; name: string }): Promise<any> {
|
||||
return promisify(
|
||||
(d: any, cb?: any) => raw.cookies.remove(d, cb),
|
||||
details,
|
||||
);
|
||||
},
|
||||
onChanged: {
|
||||
addListener(callback: (changeInfo: CompatCookieChangeInfo) => void) {
|
||||
raw.cookies.onChanged.addListener(callback);
|
||||
},
|
||||
removeListener(callback: (changeInfo: CompatCookieChangeInfo) => void) {
|
||||
raw.cookies.onChanged.removeListener(callback);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Tabs ───
|
||||
|
||||
export const tabs = {
|
||||
async query(queryInfo: {
|
||||
active?: boolean;
|
||||
currentWindow?: boolean;
|
||||
}): Promise<Array<{ url?: string; id?: number }>> {
|
||||
return promisify(
|
||||
(q: any, cb?: any) => raw.tabs.query(q, cb),
|
||||
queryInfo,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Alarms ───
|
||||
|
||||
export const alarms = {
|
||||
create(name: string, alarmInfo: { periodInMinutes: number }): void {
|
||||
raw.alarms.create(name, alarmInfo);
|
||||
},
|
||||
clear(name: string): void {
|
||||
raw.alarms.clear(name);
|
||||
},
|
||||
onAlarm: {
|
||||
addListener(callback: (alarm: { name: string }) => void): void {
|
||||
raw.alarms.onAlarm.addListener(callback);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Runtime ───
|
||||
|
||||
export const runtime = {
|
||||
onMessage: {
|
||||
addListener(
|
||||
callback: (
|
||||
message: any,
|
||||
sender: any,
|
||||
sendResponse: (response: any) => void,
|
||||
) => boolean | void,
|
||||
): void {
|
||||
raw.runtime.onMessage.addListener(callback);
|
||||
},
|
||||
},
|
||||
sendMessage(message: any): Promise<any> {
|
||||
// Firefox returns a promise, Chrome uses callbacks
|
||||
const result = raw.runtime.sendMessage(message);
|
||||
if (result && typeof result.then === "function") {
|
||||
return result;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
raw.runtime.sendMessage(message, (response: any) => {
|
||||
if (raw.runtime.lastError) {
|
||||
reject(new Error(raw.runtime.lastError.message));
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
onInstalled: {
|
||||
addListener(callback: () => void): void {
|
||||
raw.runtime.onInstalled.addListener(callback);
|
||||
},
|
||||
},
|
||||
onStartup: {
|
||||
addListener(callback: () => void): void {
|
||||
raw.runtime.onStartup.addListener(callback);
|
||||
},
|
||||
},
|
||||
openOptionsPage(): void {
|
||||
raw.runtime.openOptionsPage();
|
||||
},
|
||||
get lastError(): { message?: string } | null | undefined {
|
||||
return raw.runtime.lastError;
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Action (browserAction on older Firefox) ───
|
||||
|
||||
function getActionAPI(): any {
|
||||
return raw.action || raw.browserAction;
|
||||
}
|
||||
|
||||
export const action = {
|
||||
async setIcon(details: { path: Record<string, string> }): Promise<void> {
|
||||
const api = getActionAPI();
|
||||
if (!api) return;
|
||||
return promisify((d: any, cb?: any) => api.setIcon(d, cb), details);
|
||||
},
|
||||
async setBadgeText(details: { text: string }): Promise<void> {
|
||||
const api = getActionAPI();
|
||||
if (!api) return;
|
||||
return promisify((d: any, cb?: any) => api.setBadgeText(d, cb), details);
|
||||
},
|
||||
async setBadgeBackgroundColor(details: {
|
||||
color: string | [number, number, number, number];
|
||||
}): Promise<void> {
|
||||
const api = getActionAPI();
|
||||
if (!api) return;
|
||||
return promisify(
|
||||
(d: any, cb?: any) => api.setBadgeBackgroundColor(d, cb),
|
||||
details,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Platform detection for device registration ───
|
||||
|
||||
export function getPlatformName(): string {
|
||||
switch (BROWSER) {
|
||||
case "firefox":
|
||||
return "firefox-extension";
|
||||
case "safari":
|
||||
return "safari-extension";
|
||||
case "edge":
|
||||
return "edge-extension";
|
||||
case "chrome":
|
||||
default:
|
||||
return "chrome-extension";
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
/**
|
||||
* Chrome storage wrapper — provides typed access to extension state.
|
||||
* Storage wrapper — provides typed access to extension state.
|
||||
* Uses the browser-agnostic compat layer.
|
||||
*/
|
||||
import type { SerializedKeyPair } from "./crypto";
|
||||
import { storage } from "./compat";
|
||||
|
||||
export interface PeerDevice {
|
||||
deviceId: string;
|
||||
@@ -64,7 +66,7 @@ const DEFAULT_STATE: ExtensionState = {
|
||||
|
||||
/** Get full extension state, merging defaults. */
|
||||
export async function getState(): Promise<ExtensionState> {
|
||||
const data = await chrome.storage.local.get(null);
|
||||
const data = await storage.get(null);
|
||||
return { ...DEFAULT_STATE, ...data } as ExtensionState;
|
||||
}
|
||||
|
||||
@@ -72,12 +74,12 @@ export async function getState(): Promise<ExtensionState> {
|
||||
export async function setState(
|
||||
partial: Partial<ExtensionState>,
|
||||
): Promise<void> {
|
||||
await chrome.storage.local.set(partial);
|
||||
await storage.set(partial);
|
||||
}
|
||||
|
||||
/** Reset all state to defaults. */
|
||||
export async function clearState(): Promise<void> {
|
||||
await chrome.storage.local.clear();
|
||||
await storage.clear();
|
||||
}
|
||||
|
||||
/** Check if a domain matches a pattern (supports leading wildcard *.). */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* Cookie sync engine — monitors chrome.cookies, syncs with relay server.
|
||||
* Cookie sync engine — monitors browser cookies, syncs with relay server.
|
||||
* Uses the browser-agnostic compat layer.
|
||||
*/
|
||||
import type { CookieEntry, CookieSyncPayload, Envelope } from "./protocol";
|
||||
import { MESSAGE_TYPES } from "./protocol";
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
import type { ConnectionManager } from "./connection";
|
||||
import type { ApiClient } from "./api-client";
|
||||
import { getState, setState, isDomainAllowed, type PeerDevice } from "./storage";
|
||||
import { cookies, tabs, type CompatCookie, type CompatCookieChangeInfo } from "./compat";
|
||||
|
||||
type CookieKey = string;
|
||||
|
||||
@@ -26,7 +28,7 @@ function cookieKey(domain: string, name: string, path: string): CookieKey {
|
||||
return `${domain}|${name}|${path}`;
|
||||
}
|
||||
|
||||
function chromeCookieToEntry(cookie: chrome.cookies.Cookie): CookieEntry {
|
||||
function browserCookieToEntry(cookie: CompatCookie): CookieEntry {
|
||||
return {
|
||||
domain: cookie.domain,
|
||||
name: cookie.name,
|
||||
@@ -46,7 +48,7 @@ function chromeCookieToEntry(cookie: chrome.cookies.Cookie): CookieEntry {
|
||||
}
|
||||
|
||||
export class SyncEngine {
|
||||
private cookies = new Map<CookieKey, TrackedCookie>();
|
||||
private cookieMap = new Map<CookieKey, TrackedCookie>();
|
||||
private lamportClock = 0;
|
||||
private deviceId: string;
|
||||
private keys: DeviceKeyPair;
|
||||
@@ -67,11 +69,11 @@ export class SyncEngine {
|
||||
|
||||
/** Start monitoring cookie changes. */
|
||||
start() {
|
||||
chrome.cookies.onChanged.addListener(this.handleCookieChange);
|
||||
cookies.onChanged.addListener(this.handleCookieChange);
|
||||
}
|
||||
|
||||
stop() {
|
||||
chrome.cookies.onChanged.removeListener(this.handleCookieChange);
|
||||
cookies.onChanged.removeListener(this.handleCookieChange);
|
||||
}
|
||||
|
||||
/** Handle incoming envelope from WebSocket. */
|
||||
@@ -102,16 +104,16 @@ export class SyncEngine {
|
||||
|
||||
/** Sync a specific domain's cookies to all peers. */
|
||||
async syncDomain(domain: string) {
|
||||
const cookies = await chrome.cookies.getAll({ domain });
|
||||
if (cookies.length === 0) return;
|
||||
const allCookies = await cookies.getAll({ domain });
|
||||
if (allCookies.length === 0) return;
|
||||
|
||||
const entries = cookies.map(chromeCookieToEntry);
|
||||
const entries = allCookies.map(browserCookieToEntry);
|
||||
await this.syncEntriesToPeers(entries, "set");
|
||||
}
|
||||
|
||||
/** Sync the current tab's cookies. */
|
||||
async syncCurrentTab() {
|
||||
const [tab] = await chrome.tabs.query({
|
||||
const [tab] = await tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
@@ -130,7 +132,7 @@ export class SyncEngine {
|
||||
}
|
||||
|
||||
private handleCookieChange = async (
|
||||
changeInfo: chrome.cookies.CookieChangeInfo,
|
||||
changeInfo: CompatCookieChangeInfo,
|
||||
) => {
|
||||
const { cookie, removed, cause } = changeInfo;
|
||||
|
||||
@@ -149,18 +151,18 @@ export class SyncEngine {
|
||||
if (!isDomainAllowed(cookie.domain, state.whitelist, state.blacklist))
|
||||
return;
|
||||
|
||||
const entry = chromeCookieToEntry(cookie);
|
||||
const entry = browserCookieToEntry(cookie);
|
||||
const action = removed ? "delete" : "set";
|
||||
|
||||
if (!removed) {
|
||||
this.lamportClock++;
|
||||
this.cookies.set(key, {
|
||||
this.cookieMap.set(key, {
|
||||
entry,
|
||||
lamportTs: this.lamportClock,
|
||||
sourceDeviceId: this.deviceId,
|
||||
});
|
||||
} else {
|
||||
this.cookies.delete(key);
|
||||
this.cookieMap.delete(key);
|
||||
}
|
||||
|
||||
await this.syncEntriesToPeers([entry], action);
|
||||
@@ -199,18 +201,18 @@ export class SyncEngine {
|
||||
|
||||
for (const entry of payload.cookies) {
|
||||
const key = cookieKey(entry.domain, entry.name, entry.path);
|
||||
const existing = this.cookies.get(key);
|
||||
const existing = this.cookieMap.get(key);
|
||||
|
||||
if (payload.action === "delete") {
|
||||
if (this.shouldApply(existing, payload.lamportTs, sourceDeviceId)) {
|
||||
this.cookies.delete(key);
|
||||
this.cookieMap.delete(key);
|
||||
applied.push(entry);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.shouldApply(existing, payload.lamportTs, sourceDeviceId)) {
|
||||
this.cookies.set(key, {
|
||||
this.cookieMap.set(key, {
|
||||
entry,
|
||||
lamportTs: payload.lamportTs,
|
||||
sourceDeviceId,
|
||||
@@ -242,11 +244,11 @@ export class SyncEngine {
|
||||
const url = `http${entry.secure ? "s" : ""}://${entry.domain.replace(/^\./, "")}${entry.path}`;
|
||||
|
||||
if (action === "delete") {
|
||||
await chrome.cookies.remove({ url, name: entry.name });
|
||||
await cookies.remove({ url, name: entry.name });
|
||||
return;
|
||||
}
|
||||
|
||||
const details: chrome.cookies.SetDetails = {
|
||||
const details: Record<string, any> = {
|
||||
url,
|
||||
name: entry.name,
|
||||
value: entry.value,
|
||||
@@ -264,6 +266,6 @@ export class SyncEngine {
|
||||
details.expirationDate = new Date(entry.expiresAt).getTime() / 1000;
|
||||
}
|
||||
|
||||
await chrome.cookies.set(details);
|
||||
await cookies.set(details);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
/**
|
||||
* Options page script — manages extension settings.
|
||||
* Uses the browser-agnostic compat layer.
|
||||
*/
|
||||
import { runtime, storage } from "../lib/compat";
|
||||
|
||||
export {};
|
||||
|
||||
// --- Messaging helper ---
|
||||
|
||||
function sendMessage(type: string, payload?: unknown): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.runtime.sendMessage({ type, payload }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
return;
|
||||
}
|
||||
if (response?.error) {
|
||||
reject(new Error(response.error));
|
||||
return;
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
async function sendMessage(type: string, payload?: unknown): Promise<any> {
|
||||
const response = await runtime.sendMessage({ type, payload });
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// --- Elements ---
|
||||
@@ -59,7 +54,7 @@ let blacklist: string[] = [];
|
||||
// --- Load Settings ---
|
||||
|
||||
async function loadSettings() {
|
||||
const state = await chrome.storage.local.get(null);
|
||||
const state = await storage.get(null);
|
||||
|
||||
optDeviceName.textContent = state.deviceName || "—";
|
||||
optDeviceId.textContent = state.deviceId || "—";
|
||||
@@ -168,7 +163,7 @@ btnAddBlacklist.addEventListener("click", () => {
|
||||
|
||||
// Save
|
||||
btnSave.addEventListener("click", async () => {
|
||||
await chrome.storage.local.set({
|
||||
await storage.set({
|
||||
serverUrl: optServerUrl.value.trim(),
|
||||
connectionMode: optConnectionMode.value,
|
||||
pollIntervalSec: parseInt(optPollInterval.value),
|
||||
@@ -247,7 +242,7 @@ btnClearData.addEventListener("click", async () => {
|
||||
return;
|
||||
}
|
||||
await sendMessage("LOGOUT");
|
||||
await chrome.storage.local.clear();
|
||||
await storage.clear();
|
||||
alert("All data cleared.");
|
||||
window.close();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/**
|
||||
* Popup script — controls the popup UI interactions.
|
||||
* Uses the browser-agnostic compat layer.
|
||||
*/
|
||||
import { runtime, tabs } from "../lib/compat";
|
||||
|
||||
export {};
|
||||
|
||||
// --- Elements ---
|
||||
@@ -40,20 +43,12 @@ const btnPairingCancel = document.getElementById("btn-pairing-cancel") as HTMLBu
|
||||
|
||||
// --- Messaging helper ---
|
||||
|
||||
function sendMessage(type: string, payload?: unknown): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.runtime.sendMessage({ type, payload }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
return;
|
||||
}
|
||||
if (response?.error) {
|
||||
reject(new Error(response.error));
|
||||
return;
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
async function sendMessage(type: string, payload?: unknown): Promise<any> {
|
||||
const response = await runtime.sendMessage({ type, payload });
|
||||
if (response?.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// --- UI Updates ---
|
||||
@@ -142,7 +137,7 @@ btnSyncTab.addEventListener("click", async () => {
|
||||
});
|
||||
|
||||
btnAddWhitelist.addEventListener("click", async () => {
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
const [tab] = await tabs.query({ active: true, currentWindow: true });
|
||||
if (!tab?.url) return;
|
||||
|
||||
try {
|
||||
@@ -198,7 +193,7 @@ btnPairingCancel.addEventListener("click", () => {
|
||||
|
||||
btnSettings.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
chrome.runtime.openOptionsPage();
|
||||
runtime.openOptionsPage();
|
||||
});
|
||||
|
||||
// --- Init ---
|
||||
|
||||
Reference in New Issue
Block a user