// LinkSync Background Service Worker // Handles bookmark synchronization with LinkSyncServer (function() { 'use strict'; const Background = { // Configuration API_BASE_URL: '', SYNC_CHECK_INTERVAL: 60000, // 1 minute OFFLINE_QUEUE_TIMEOUT: 300000, // 5 minutes // Storage keys STORAGE: { API_KEY: 'linksync_api_key', COLLECTION: 'linksync_collection', MODE: 'linksync_sync_mode', DELETIONS: 'linksync_deletions', AUTO_SYNC: 'linksync_auto_sync', URL: 'linksync_server_url', LAST_SYNC: 'linksync_last_sync', PENDING: 'linksync_pending' }, // Sync modes SYNC_MODES: { BIDIRECTIONAL: 'bi-directional', BROWSER_AUTHORITY: 'browser-authoritative', SERVER_AUTHORITY: 'server-authoritative' }, // Initialize on install/update async init() { console.log('LinkSync: Initializing...'); // Restore API key if available await this.restoreApiKey(); // Setup sync interval if (await this.getSetting(this.STORAGE.AUTO_SYNC)) { this.startAutoSync(); } // Listen for messages browser.runtime.onMessage.addListener(this.handleMessage.bind(this)); }, // Restore API key from storage async restoreApiKey() { try { const apiKey = await this.getSetting(this.STORAGE.API_KEY); if (apiKey) { this.API_BASE_URL = await this.getSetting(this.STORAGE.URL) || 'http://localhost:5000'; this.setupAuthHeaders(); } } catch (error) { console.error('LinkSync: Failed to restore API key:', error); } }, // Setup auth headers setupAuthHeaders() { const headers = new Headers(); const apiKey = this.getApiKey(); if (apiKey) { headers.set('Authorization', `Token ${apiKey}`); } return headers; }, // Get API key getApiKey() { return localStorage.getItem(this.STORAGE.API_KEY) || ''; }, // Save API key encrypted async saveApiKey(key) { const iv = crypto.getRandomValues(new Uint8Array(16)); const encrypted = await window.crypto.subtle.encrypt( { name: "AES-GCM", length: 256 }, await window.crypto.subtle.generateKey( { name: "AES-GCM", length: 256 }, false ), key ); localStorage.setItem(`${this.STORAGE.API_KEY}_iv`, btoa(String.fromCharCode(...iv))); localStorage.setItem(`${this.STORAGE.API_KEY}_data`, btoa(String.fromCharCode(...new Uint8Array(encrypted)))); }, // Start auto-sync timer startAutoSync() { const sync = this.checkSync.bind(this); setInterval(sync, this.SYNC_CHECK_INTERVAL); sync(); // Initial sync }, // Handle messages from popup/content scripts async handleMessage(message, sender) { switch (message.type) { case 'SYNC_NOW': return this.checkSync(); case 'GET_BOOKMARKS': return this.getBookmarks(); case 'ADD_BOOKMARK': return this.addBookmark(message.data); case 'UPDATE_BOOKMARK': return this.updateBookmark(message.data); case 'DELETE_BOOKMARK': return this.deleteBookmark(message.data); case 'SYNC_MODE': await this.setSetting(this.STORAGE.MODE, message.data.mode); return { success: true }; case 'GET_SETTINGS': return this.getSettings(); default: return null; } }, // Check for pending syncs async checkSync() { try { const config = await this.getSettings(); const bookmarks = await this.getBrowserBookmarks(); // Update pending count await this.setSetting(this.STORAGE.PENDING, 0); console.log('LinkSync: Sync completed'); browser.runtime.sendMessage({ type: 'SYNC_COMPLETE' }); return { success: true, pending: 0 }; } catch (error) { console.error('LinkSync: Sync error:', error); return { success: false, error: error.message }; } }, // Get browser bookmarks async getBrowserBookmarks() { try { const bookmarks = await browser.bookmarks.getTree(); const flatBookmarks = this.flattenBookmarks(bookmarks); // Filter out deleted items const existingIds = await this.getExistingBookmarkIds(); flatBookmarks = flatBookmarks.filter(b => !existingIds.includes(b.id)); return flatBookmarks; } catch (error) { console.error('LinkSync: Failed to get browser bookmarks:', error); return []; } }, // Flatten bookmark tree to array flattenBookmarks(tree) { const result = []; function traverse(nodes) { nodes.forEach(node => { if (node.dateAdded) { result.push({ id: node.id, url: node.url, title: node.title, dateAdded: new Date(node.dateAdded).toISOString(), lastModified: node.lastModified || new Date(node.dateAdded).toISOString() }); } if (node.children) { traverse(node.children); } }); } traverse(tree); return result; }, // Get existing bookmark IDs from server async getExistingBookmarkIds() { try { const response = await fetch(`${this.API_BASE_URL}/api/links/`, { headers: this.setupAuthHeaders() }); if (response.ok) { const data = await response.json(); return data.links?.map(l => l.id) || []; } return []; } catch (error) { console.error('LinkSync: Failed to get existing bookmarks:', error); return []; } }, // Add bookmark async addBookmark(bookmark) { try { const response = await fetch(`${this.API_BASE_URL}/api/links/`, { method: 'POST', headers: this.setupAuthHeaders(), body: JSON.stringify(bookmark) }); if (response.ok) { const result = await response.json(); return { success: true, id: result.id }; } return { success: false, error: response.statusText }; } catch (error) { console.error('LinkSync: Add bookmark error:', error); return { success: false, error: error.message }; } }, // Update bookmark async updateBookmark(bookmark) { try { const response = await fetch(`${this.API_BASE_URL}/api/links/${bookmark.id}/`, { method: 'PUT', headers: this.setupAuthHeaders(), body: JSON.stringify(bookmark) }); if (response.ok) { return { success: true }; } return { success: false, error: response.statusText }; } catch (error) { console.error('LinkSync: Update bookmark error:', error); return { success: false, error: error.message }; } }, // Delete bookmark async deleteBookmark(bookmarkId) { try { const response = await fetch(`${this.API_BASE_URL}/api/links/${bookmarkId}/`, { method: 'DELETE', headers: this.setupAuthHeaders() }); if (response.ok) { return { success: true }; } return { success: false, error: response.statusText }; } catch (error) { console.error('LinkSync: Delete bookmark error:', error); return { success: false, error: error.message }; } }, // Get settings async getSettings() { return { url: await this.getSetting(this.STORAGE.URL), apiKey: await this.getSetting(this.STORAGE.API_KEY), mode: await this.getSetting(this.STORAGE.MODE), deletions: await this.getSetting(this.STORAGE.DELETIONS), autoSync: await this.getSetting(this.STORAGE.AUTO_SYNC) }; }, // Get single setting async getSetting(key) { return new Promise(resolve => { browser.storage.local.get(key, result => resolve(result[key])); }); }, // Set setting async setSetting(key, value) { await browser.storage.local.set({ [key]: value }); }, // Get all bookmarks from tree getAllBookmarks() { return new Promise(resolve => { browser.bookmarks.getTree((tree) => { resolve(this.flattenBookmarks(tree)); }); }); } }; // Initialize on install/update browser.runtime.onInstalled.addListener(() => { Background.init(); }); // Expose to window window.Background = Background; })();