Skip to content

Commit a94cb8e

Browse files
ralyodioclaude
andcommitted
Install-helper: check TronBrowser store before Chrome Web Store
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 <noreply@anthropic.com>
1 parent d1deea2 commit a94cb8e

2 files changed

Lines changed: 130 additions & 9 deletions

File tree

apps/desktop/extensions/ai-sidebar/background.js

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,79 @@ chrome.runtime.onInstalled.addListener(async () => {
1515
if (!aiConfig || !aiConfig.model) chrome.runtime.openOptionsPage();
1616
});
1717

18+
// --- Extension store resolution (Tron store first, Chrome Web Store fallback) -
19+
// The install-helper content script runs on Chrome Web Store detail pages. Since
20+
// we do NOT publish on the Chrome Web Store, its "Add to TronBrowser" button must
21+
// prefer the TronBrowser store: given the extension's slug/name, check
22+
// tronbrowser.dev's store and, when a live listing exists, install from there;
23+
// otherwise the content script falls back to the Chrome Web Store CRX.
24+
//
25+
// We resolve here in the background (not the content script) because the SW has
26+
// host permissions for tronbrowser.dev, so the fetch isn't blocked by the store
27+
// page's CSP/CORS.
28+
const TRON_STORE_API = 'https://tronbrowser.dev/api/store';
29+
30+
// A listing is installable only if it's live and has a downloadable artifact.
31+
function tronListingUsable(ext) {
32+
return !!(ext && ext.status === 'live' && ext.version && (ext.version.crxUrl || ext.version.bundleUrl));
33+
}
34+
35+
async function fetchTronListing(path) {
36+
const ctrl = new AbortController();
37+
const t = setTimeout(() => ctrl.abort(), 6000);
38+
try {
39+
const res = await fetch(`${TRON_STORE_API}${path}`, { signal: ctrl.signal });
40+
if (!res.ok) return null;
41+
return await res.json().catch(() => null);
42+
} catch (_) {
43+
return null;
44+
} finally {
45+
clearTimeout(t);
46+
}
47+
}
48+
49+
// Look the extension up in the TronBrowser store. The Chrome Web Store slug
50+
// usually matches the store slug for extensions we've republished; if it doesn't,
51+
// fall back to a name search and require an exact slug/name match (never install
52+
// an unrelated result). Returns the usable listing, or null.
53+
async function resolveTronStore(slug, name) {
54+
if (slug) {
55+
const ext = await fetchTronListing(`/extensions/${encodeURIComponent(slug)}`);
56+
if (tronListingUsable(ext)) return ext;
57+
}
58+
const q = (name || slug || '').trim();
59+
if (q) {
60+
const data = await fetchTronListing(`/extensions?q=${encodeURIComponent(q)}`);
61+
const list = (data && data.extensions) || [];
62+
const nlc = (name || '').toLowerCase();
63+
const hit =
64+
(slug && list.find((e) => e.slug === slug)) ||
65+
(nlc && list.find((e) => (e.name || '').toLowerCase() === nlc)) ||
66+
null;
67+
if (tronListingUsable(hit)) return hit;
68+
}
69+
return null;
70+
}
71+
1872
// Let pages (e.g. the new tab) ask to open the side panel.
19-
chrome.runtime.onMessage.addListener((msg, sender) => {
73+
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
74+
if (msg?.type === 'resolve-tron-store') {
75+
(async () => {
76+
const ext = await resolveTronStore(msg.slug, msg.name).catch(() => null);
77+
if (ext) {
78+
sendResponse({
79+
found: true,
80+
slug: ext.slug,
81+
name: ext.name,
82+
downloadUrl: `${TRON_STORE_API}/extensions/${encodeURIComponent(ext.slug)}/download`,
83+
});
84+
} else {
85+
sendResponse({ found: false });
86+
}
87+
})();
88+
return true; // async sendResponse
89+
}
90+
2091
if (msg?.type === 'open-sidepanel' && chrome.sidePanel?.open) {
2192
const opts = sender.tab?.id != null ? { tabId: sender.tab.id } : {};
2293
chrome.sidePanel.open(opts).catch((err) => console.warn('sidePanel open:', err));

apps/desktop/extensions/ai-sidebar/install-helper.js

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Make Chrome Web Store installs work on Ungoogled Chromium.
1+
// Make extension installs work on Ungoogled Chromium.
22
//
33
// Google disables its native "Add to Chrome" button on non-official Chrome, so
44
// it stays greyed out. We inject our own working button on extension detail
@@ -7,15 +7,28 @@
77
// `extension-mime-request-handling = Always prompt for install` flag. If the
88
// flag isn't active for some reason, the CRX simply downloads and can be
99
// dragged onto chrome://extensions (Developer mode) instead.
10+
//
11+
// TronBrowser does NOT publish on the Chrome Web Store, so the button checks the
12+
// TronBrowser store FIRST (by the page's slug/name, resolved in the background
13+
// service worker which has host permissions). When a live TronBrowser-store
14+
// listing exists we install from there; only when it doesn't do we fall back to
15+
// the Chrome Web Store CRX.
1016

1117
(function () {
1218
// Chrome extension IDs are 32 chars in a-p. New store URL:
1319
// https://chromewebstore.google.com/detail/<slug>/<id>
14-
const ID_RE = /\/detail\/(?:[^/]+\/)?([a-p]{32})/;
20+
const DETAIL_RE = /\/detail\/(?:([^/]+)\/)?([a-p]{32})/;
21+
22+
function parseDetail() {
23+
const m = location.pathname.match(DETAIL_RE);
24+
if (!m) return null;
25+
return { slug: m[1] || '', id: m[2] };
26+
}
1527

16-
function extId() {
17-
const m = location.pathname.match(ID_RE);
18-
return m ? m[1] : '';
28+
// The listing's human name, from the tab title ("uBlock Origin - Chrome Web
29+
// Store"), used as a secondary lookup key when the slug doesn't match.
30+
function extName() {
31+
return (document.title || '').replace(/\s*[-|]\s*Chrome Web Store\s*$/i, '').trim();
1932
}
2033

2134
function chromeVersion() {
@@ -29,9 +42,31 @@
2942
'&x=' + encodeURIComponent('id=' + id + '&installsource=ondemand&uc');
3043
}
3144

45+
// Ask the background worker whether the TronBrowser store has this extension.
46+
// Resolves to a Tron-store download URL, or null to use the Chrome CRX. Never
47+
// rejects — any failure (SW asleep, offline, not listed) falls back to Chrome.
48+
function resolveTronDownload(slug, name) {
49+
return new Promise((resolve) => {
50+
let settled = false;
51+
const done = (v) => { if (!settled) { settled = true; resolve(v); } };
52+
const timer = setTimeout(() => done(null), 5000); // never hang the click
53+
try {
54+
chrome.runtime.sendMessage({ type: 'resolve-tron-store', slug, name }, (resp) => {
55+
clearTimeout(timer);
56+
if (chrome.runtime.lastError || !resp || !resp.found || !resp.downloadUrl) { done(null); return; }
57+
done(resp.downloadUrl);
58+
});
59+
} catch (_) {
60+
clearTimeout(timer);
61+
done(null);
62+
}
63+
});
64+
}
65+
3266
function addButton() {
33-
const id = extId();
34-
if (!id || document.getElementById('tron-install-btn')) return;
67+
const detail = parseDetail();
68+
if (!detail || document.getElementById('tron-install-btn')) return;
69+
const { slug, id } = detail;
3570
const btn = document.createElement('button');
3671
btn.id = 'tron-install-btn';
3772
btn.type = 'button';
@@ -44,7 +79,22 @@
4479
'padding:12px 18px', 'font:700 14px ui-monospace,Menlo,monospace',
4580
'cursor:pointer', 'box-shadow:0 6px 24px rgba(0,0,0,.5)',
4681
].join(';');
47-
btn.addEventListener('click', () => { window.location.href = crxUrl(id); });
82+
83+
// Resolve the TronBrowser store up front so the button reflects where the
84+
// install will come from; cache the promise so a click never re-resolves.
85+
const tronTarget = resolveTronDownload(slug, extName());
86+
tronTarget.then((url) => {
87+
if (url) {
88+
btn.textContent = '⬇ Add from TronBrowser Store';
89+
btn.title = 'Install from the TronBrowser store (not published on the Chrome Web Store)';
90+
}
91+
});
92+
93+
btn.addEventListener('click', async () => {
94+
// Tron store FIRST, Chrome Web Store CRX as the fallback.
95+
const url = (await tronTarget) || crxUrl(id);
96+
window.location.href = url;
97+
});
4898
document.body.appendChild(btn);
4999
}
50100

0 commit comments

Comments
 (0)