From a94cb8e263d25fd2fa2b647143be2e4e19a62e37 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 7 Jul 2026 02:23:51 +0000 Subject: [PATCH] Install-helper: check TronBrowser store before Chrome Web Store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Add to TronBrowser" button injected on Chrome Web Store detail pages jumped straight to Google's CRX. Since we don't publish on the Chrome Web Store, it now resolves the TronBrowser store first (by the page's slug, then an exact name-search match) via the background service worker — which has host permissions for tronbrowser.dev so the lookup isn't blocked by the store page's CSP/CORS. When a live listing with a downloadable artifact exists, the button installs from the TronBrowser store and relabels; otherwise it falls back to the Chrome Web Store CRX. Co-Authored-By: Claude Opus 4.8 --- .../extensions/ai-sidebar/background.js | 73 ++++++++++++++++++- .../extensions/ai-sidebar/install-helper.js | 66 +++++++++++++++-- 2 files changed, 130 insertions(+), 9 deletions(-) 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); }