// Background service worker: API proxy + screenshot crop. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (!message || !message.type) { return false; } if (message.type === "rcs.api") { (async () => { try { const data = await proxyApi(message); sendResponse({ ok: true, data }); } catch (err) { sendResponse({ ok: false, error: String(err && err.message ? err.message : err) }); } })(); return true; } if (message.type === "rcs.redditJson") { (async () => { try { const data = await fetchRedditJson(message.path); sendResponse({ ok: true, data }); } catch (err) { sendResponse({ ok: false, error: String(err && err.message ? err.message : err) }); } })(); return true; } if (message.type === "rcs.captureCrop") { const tabId = sender.tab && sender.tab.id; if (tabId == null) { sendResponse({ ok: false, error: "no tab" }); return false; } (async () => { try { const dataUrl = await chrome.tabs.captureVisibleTab(sender.tab.windowId, { format: "png" }); const cropped = await cropDataUrl(dataUrl, message.rect, message.dpr || 1); sendResponse({ ok: true, dataUrl: cropped }); } catch (err) { sendResponse({ ok: false, error: String(err && err.message ? err.message : err) }); } })(); return true; } return false; }); async function fetchRedditJson(path) { const url = new URL(path, "https://www.reddit.com"); if (url.hostname !== "www.reddit.com" && url.hostname !== "old.reddit.com") { throw new Error("reddit host not allowed"); } const response = await fetch(url.toString(), { method: "GET", headers: { Accept: "application/json", }, credentials: "include", }); const text = await response.text(); if (!response.ok) { throw new Error(`reddit ${response.status}: ${text.slice(0, 160)}`); } try { return JSON.parse(text); } catch { throw new Error("reddit response was not JSON (blocked or logged-out wall?)"); } } async function getSettings() { const stored = await chrome.storage.sync.get({ backendUrl: "", apiKey: "", }); return { backendUrl: (stored.backendUrl || "").replace(/\/+$/, ""), apiKey: stored.apiKey || "", }; } async function proxyApi(message) { const settings = await getSettings(); if (!settings.backendUrl) { throw new Error("Backend URL not configured"); } const headers = Object.assign( { Accept: "application/json" }, message.headers || {} ); if (settings.apiKey) { headers["X-API-Key"] = settings.apiKey; } const init = { method: message.method || "GET", headers, credentials: "include", }; if (message.body != null && message.method && message.method.toUpperCase() !== "GET") { init.body = message.body; } const response = await fetch(`${settings.backendUrl}${message.path}`, init); const text = await response.text(); let data = null; try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; } if (!response.ok) { throw new Error(proxyAuthErrorMessage(response.status, text, data)); } // Pangolin may return 200 HTML login/redirect instead of JSON if (data && data.raw != null && looksLikeHtml(data.raw)) { throw new Error(proxyAuthErrorMessage(response.status, text, data)); } return data; } function looksLikeHtml(text) { return typeof text === "string" && /^\s* { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); }