diff --git a/apps/desktop/extensions/ai-sidebar/background.js b/apps/desktop/extensions/ai-sidebar/background.js index 745244b..8fbf47e 100644 --- a/apps/desktop/extensions/ai-sidebar/background.js +++ b/apps/desktop/extensions/ai-sidebar/background.js @@ -15,8 +15,79 @@ chrome.runtime.onInstalled.addListener(async () => { if (!aiConfig || !aiConfig.model) chrome.runtime.openOptionsPage(); }); +// --- Extension store resolution (Tron store first, Chrome Web Store fallback) - +// The install-helper content script runs on Chrome Web Store detail pages. Since +// we do NOT publish on the Chrome Web Store, its "Add to TronBrowser" button must +// prefer the TronBrowser store: given the extension's slug/name, check +// tronbrowser.dev's store and, when a live listing exists, install from there; +// otherwise the content script falls back to the Chrome Web Store CRX. +// +// We resolve here in the background (not the content script) because the SW has +// host permissions for tronbrowser.dev, so the fetch isn't blocked by the store +// page's CSP/CORS. +const TRON_STORE_API = 'https://tronbrowser.dev/api/store'; + +// A listing is installable only if it's live and has a downloadable artifact. +function tronListingUsable(ext) { + return !!(ext && ext.status === 'live' && ext.version && (ext.version.crxUrl || ext.version.bundleUrl)); +} + +async function fetchTronListing(path) { + const ctrl = new AbortController(); + const t = setTimeout(() => ctrl.abort(), 6000); + try { + const res = await fetch(`${TRON_STORE_API}${path}`, { signal: ctrl.signal }); + if (!res.ok) return null; + return await res.json().catch(() => null); + } catch (_) { + return null; + } finally { + clearTimeout(t); + } +} + +// Look the extension up in the TronBrowser store. The Chrome Web Store slug +// usually matches the store slug for extensions we've republished; if it doesn't, +// fall back to a name search and require an exact slug/name match (never install +// an unrelated result). Returns the usable listing, or null. +async function resolveTronStore(slug, name) { + if (slug) { + const ext = await fetchTronListing(`/extensions/${encodeURIComponent(slug)}`); + if (tronListingUsable(ext)) return ext; + } + const q = (name || slug || '').trim(); + if (q) { + const data = await fetchTronListing(`/extensions?q=${encodeURIComponent(q)}`); + const list = (data && data.extensions) || []; + const nlc = (name || '').toLowerCase(); + const hit = + (slug && list.find((e) => e.slug === slug)) || + (nlc && list.find((e) => (e.name || '').toLowerCase() === nlc)) || + null; + if (tronListingUsable(hit)) return hit; + } + return null; +} + // Let pages (e.g. the new tab) ask to open the side panel. -chrome.runtime.onMessage.addListener((msg, sender) => { +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + if (msg?.type === 'resolve-tron-store') { + (async () => { + const ext = await resolveTronStore(msg.slug, msg.name).catch(() => null); + if (ext) { + sendResponse({ + found: true, + slug: ext.slug, + name: ext.name, + downloadUrl: `${TRON_STORE_API}/extensions/${encodeURIComponent(ext.slug)}/download`, + }); + } else { + sendResponse({ found: false }); + } + })(); + return true; // async sendResponse + } + if (msg?.type === 'open-sidepanel' && chrome.sidePanel?.open) { const opts = sender.tab?.id != null ? { tabId: sender.tab.id } : {}; chrome.sidePanel.open(opts).catch((err) => console.warn('sidePanel open:', err)); diff --git a/apps/desktop/extensions/ai-sidebar/install-helper.js b/apps/desktop/extensions/ai-sidebar/install-helper.js index 2f216f1..cca1f5b 100644 --- a/apps/desktop/extensions/ai-sidebar/install-helper.js +++ b/apps/desktop/extensions/ai-sidebar/install-helper.js @@ -1,4 +1,4 @@ -// Make Chrome Web Store installs work on Ungoogled Chromium. +// Make extension installs work on Ungoogled Chromium. // // Google disables its native "Add to Chrome" button on non-official Chrome, so // it stays greyed out. We inject our own working button on extension detail @@ -7,15 +7,28 @@ // `extension-mime-request-handling = Always prompt for install` flag. If the // flag isn't active for some reason, the CRX simply downloads and can be // dragged onto chrome://extensions (Developer mode) instead. +// +// TronBrowser does NOT publish on the Chrome Web Store, so the button checks the +// TronBrowser store FIRST (by the page's slug/name, resolved in the background +// service worker which has host permissions). When a live TronBrowser-store +// listing exists we install from there; only when it doesn't do we fall back to +// the Chrome Web Store CRX. (function () { // Chrome extension IDs are 32 chars in a-p. New store URL: // https://chromewebstore.google.com/detail// - const ID_RE = /\/detail\/(?:[^/]+\/)?([a-p]{32})/; + const DETAIL_RE = /\/detail\/(?:([^/]+)\/)?([a-p]{32})/; + + function parseDetail() { + const m = location.pathname.match(DETAIL_RE); + if (!m) return null; + return { slug: m[1] || '', id: m[2] }; + } - function extId() { - const m = location.pathname.match(ID_RE); - return m ? m[1] : ''; + // The listing's human name, from the tab title ("uBlock Origin - Chrome Web + // Store"), used as a secondary lookup key when the slug doesn't match. + function extName() { + return (document.title || '').replace(/\s*[-–|]\s*Chrome Web Store\s*$/i, '').trim(); } function chromeVersion() { @@ -29,9 +42,31 @@ '&x=' + encodeURIComponent('id=' + id + '&installsource=ondemand&uc'); } + // Ask the background worker whether the TronBrowser store has this extension. + // Resolves to a Tron-store download URL, or null to use the Chrome CRX. Never + // rejects — any failure (SW asleep, offline, not listed) falls back to Chrome. + function resolveTronDownload(slug, name) { + return new Promise((resolve) => { + let settled = false; + const done = (v) => { if (!settled) { settled = true; resolve(v); } }; + const timer = setTimeout(() => done(null), 5000); // never hang the click + try { + chrome.runtime.sendMessage({ type: 'resolve-tron-store', slug, name }, (resp) => { + clearTimeout(timer); + if (chrome.runtime.lastError || !resp || !resp.found || !resp.downloadUrl) { done(null); return; } + done(resp.downloadUrl); + }); + } catch (_) { + clearTimeout(timer); + done(null); + } + }); + } + function addButton() { - const id = extId(); - if (!id || document.getElementById('tron-install-btn')) return; + const detail = parseDetail(); + if (!detail || document.getElementById('tron-install-btn')) return; + const { slug, id } = detail; const btn = document.createElement('button'); btn.id = 'tron-install-btn'; btn.type = 'button'; @@ -44,7 +79,22 @@ 'padding:12px 18px', 'font:700 14px ui-monospace,Menlo,monospace', 'cursor:pointer', 'box-shadow:0 6px 24px rgba(0,0,0,.5)', ].join(';'); - btn.addEventListener('click', () => { window.location.href = crxUrl(id); }); + + // Resolve the TronBrowser store up front so the button reflects where the + // install will come from; cache the promise so a click never re-resolves. + const tronTarget = resolveTronDownload(slug, extName()); + tronTarget.then((url) => { + if (url) { + btn.textContent = '⬇ Add from TronBrowser Store'; + btn.title = 'Install from the TronBrowser store (not published on the Chrome Web Store)'; + } + }); + + btn.addEventListener('click', async () => { + // Tron store FIRST, Chrome Web Store CRX as the fallback. + const url = (await tronTarget) || crxUrl(id); + window.location.href = url; + }); document.body.appendChild(btn); }