fc3g(ext): cookies extraction + FC backend API client
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user