Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion apps/desktop/extensions/ai-sidebar/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
66 changes: 58 additions & 8 deletions apps/desktop/extensions/ai-sidebar/install-helper.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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/<slug>/<id>
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() {
Expand All @@ -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';
Expand All @@ -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);
}

Expand Down
Loading