// LinkSync API Client // Handles all communication with LinkSyncServer const API = { baseUrl: "", token: "", maxRetries: 3, retryDelay: 1000, timeout: 10000, async init() { const settings = await this.getSettings(); this.baseUrl = (settings.serverUrl || "http://localhost:5000").replace(/\/+$/, ""); this.token = settings.apiKey || ""; }, async getSettings() { return new Promise((resolve) => { browser.storage.local.get( ["linksync_server_url", "linksync_api_key"], (result) => resolve(result) ); }); }, async request(method, path, body = null) { await this.init(); if (!this.token) { throw new Error("Not authenticated. Set your API key in settings."); } let lastError; for (let attempt = 1; attempt <= this.maxRetries; attempt++) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); const options = { method, headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.token}`, }, signal: controller.signal, }; if (body && method !== "GET") { options.body = JSON.stringify(body); } const response = await fetch(`${this.baseUrl}${path}`, options); clearTimeout(timeoutId); if (response.status === 401) { throw new Error("Authentication failed. Check your API key."); } if (response.status === 429) { const retryAfter = parseInt(response.headers.get("Retry-After"), 10) || this.retryDelay * attempt; await this.delay(retryAfter); continue; } if (!response.ok) { const errorData = await response.json().catch(() => null); throw new Error( errorData?.detail || `Server error: ${response.status} ${response.statusText}` ); } const contentType = response.headers.get("content-type"); if (contentType && contentType.includes("application/json")) { return await response.json(); } return null; } catch (error) { lastError = error; if (error.name === "AbortError") { lastError = new Error("Request timed out"); } if (attempt < this.maxRetries) { await this.delay(this.retryDelay * attempt); } } } throw lastError || new Error("Request failed after retries"); }, async get(path) { return this.request("GET", path); }, async post(path, body) { return this.request("POST", path, body); }, async put(path, body) { return this.request("PUT", path, body); }, async delete(path) { return this.request("DELETE", path); }, async login(username, password) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); const formData = new URLSearchParams(); formData.append("username", username); formData.append("password", password); const settings = await this.getSettings(); this.baseUrl = (settings.serverUrl || "http://localhost:5000").replace(/\/+$/, ""); const response = await fetch(`${this.baseUrl}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: formData.toString(), signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { const error = await response.json().catch(() => null); throw new Error(error?.detail || "Login failed"); } const data = await response.json(); this.token = data.access_token; await browser.storage.local.set({ linksync_api_key: data.access_token }); return data; }, async testConnection() { await this.init(); const response = await fetch(`${this.baseUrl}/health`, { method: "GET", signal: AbortSignal.timeout(this.timeout), }); if (!response.ok) throw new Error(`Server returned ${response.status}`); return await response.json(); }, // Links async getLinks(params = {}) { const qs = new URLSearchParams(params).toString(); return this.get(`/api/links/${qs ? "?" + qs : ""}`); }, async createLink(data) { return this.post("/api/links/", data); }, async updateLink(id, data) { return this.put(`/api/links/${id}/`, data); }, async deleteLink(id) { return this.delete(`/api/links/${id}/`); }, // Collections async getCollections() { return this.get("/api/collections/"); }, async createCollection(data) { return this.post("/api/collections/", data); }, async updateCollection(id, data) { return this.put(`/api/collections/${id}/`, data); }, async deleteCollection(id) { return this.delete(`/api/collections/${id}/`); }, async refreshCollection(id) { return this.post(`/api/collections/${id}/refresh`, {}); }, // Queries async parseQuery(expression) { return this.post(`/api/queries/parse?expression=${encodeURIComponent(expression)}`); }, async executeQuery(expression, limit = 50) { return this.post( `/api/queries/execute?expression=${encodeURIComponent(expression)}&limit=${limit}` ); }, // Sync async sync(config, browserBookmarks) { return this.post("/api/sync/", { mode: config.mode, deletions_enabled: config.deletions, browser_bookmarks: browserBookmarks, }); }, // Admin async getAdminStats() { return this.get("/api/admin/stats"); }, delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }, };