Initial commit: LinkSyncServer and LinkSyncExtension projects with complete documentation, models, API endpoints, tests, and extension implementation

This commit is contained in:
DavidSaylor
2026-05-11 17:37:10 -05:00
parent ad0b12b452
commit aed69afdfd
691 changed files with 181874 additions and 28 deletions

View File

@@ -0,0 +1,312 @@
// 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;
})();