diff --git a/extension/lib/api.js b/extension/lib/api.js new file mode 100644 index 0000000..60fc95d --- /dev/null +++ b/extension/lib/api.js @@ -0,0 +1,93 @@ +/** + * FC backend client. Talks to /api/credentials (FC-3b), /api/sources + * (FC-3a), and the new /api/extension/* endpoints (FC-3g). + */ + +class FabledCuratorAPI { + constructor() { + this.baseUrl = null; + this.apiKey = null; + } + + async init() { + const cfg = await browser.storage.local.get(['apiUrl', 'apiKey']); + this.baseUrl = cfg.apiUrl || null; + this.apiKey = cfg.apiKey || null; + return this.isConfigured(); + } + + isConfigured() { + return !!(this.baseUrl && this.apiKey); + } + + async request(method, endpoint, data = null) { + if (!this.isConfigured()) { + throw new Error('Not configured. Set FC URL + API key in settings.'); + } + const url = `${this.baseUrl.replace(/\/+$/, '')}${endpoint}`; + const options = { + method, + headers: { + 'Content-Type': 'application/json', + 'X-Extension-Key': this.apiKey, + }, + }; + if (data) options.body = JSON.stringify(data); + + let response; + try { + response = await fetch(url, options); + } catch (e) { + throw new Error('Cannot connect to FabledCurator. Check URL.'); + } + + if (!response.ok) { + let message; + try { + const body = await response.json(); + message = body.error || `HTTP ${response.status}`; + if (body.detail) message += `: ${body.detail}`; + } catch { + message = `HTTP ${response.status}: ${response.statusText}`; + } + const err = new Error(message); + err.status = response.status; + throw err; + } + + if (response.status === 204) return { success: true }; + return response.json(); + } + + // FC-3b — credentials. + uploadCredentials(platform, credentialType, data) { + return this.request('POST', '/credentials', { + platform, + credential_type: credentialType, + data, + }); + } + getCredentials() { + return this.request('GET', '/credentials'); + } + + // FC-3a — sources. + listSources() { + return this.request('GET', '/sources'); + } + triggerSourceCheck(sourceId) { + return this.request('POST', `/sources/${sourceId}/check`); + } + + // FC-3g — extension-specific. + quickAddSource(url) { + return this.request('POST', '/extension/quick-add-source', { url }); + } + + // Connection test = the cheapest read with auth. + testConnection() { + return this.request('GET', '/credentials'); + } +} + +const api = new FabledCuratorAPI(); diff --git a/extension/lib/cookies.js b/extension/lib/cookies.js new file mode 100644 index 0000000..b13ca4d --- /dev/null +++ b/extension/lib/cookies.js @@ -0,0 +1,56 @@ +/** + * Cookie extraction + Netscape format conversion. + * Direct port of GS extension/lib/cookies.js. + */ + +async function extractCookiesForPlatform(platformKey) { + const platform = PLATFORMS[platformKey]; + if (!platform) throw new Error(`Unknown platform: ${platformKey}`); + + const all = []; + for (const domain of platform.domains) { + try { + const cookies = await browser.cookies.getAll({ domain }); + all.push(...cookies); + } catch (e) { + console.warn(`cookies.getAll failed for ${domain}:`, e); + } + } + return deduplicateCookies(all); +} + +function deduplicateCookies(cookies) { + const seen = new Map(); + for (const c of cookies) { + const key = `${c.name}|${c.domain}|${c.path}`; + if (!seen.has(key)) { + seen.set(key, c); + } else { + const existing = seen.get(key); + if (c.expirationDate && (!existing.expirationDate || c.expirationDate > existing.expirationDate)) { + seen.set(key, c); + } + } + } + return Array.from(seen.values()); +} + +function toNetscapeFormat(cookies) { + const lines = ['# Netscape HTTP Cookie File']; + for (const c of cookies) { + let domain = c.domain.replace(/^\.?www\./, '.'); + if (!domain.startsWith('.')) domain = '.' + domain; + const secure = c.secure ? 'TRUE' : 'FALSE'; + const expiration = c.expirationDate ? Math.floor(c.expirationDate) : 0; + lines.push([domain, 'TRUE', c.path || '/', secure, String(expiration), c.name, c.value].join('\t')); + } + return lines.join('\n'); +} + +async function getCookieCount(platformKey) { + try { + return (await extractCookiesForPlatform(platformKey)).length; + } catch { + return 0; + } +}