Skip to content

Commit d98c3f2

Browse files
ralyodioclaude
andcommitted
v0.1.3: de-google the extension — T logo, DuckDuckGo, CoinPay login, RSS new tab
Browser (extension layer, works today on the borrowed Ungoogled/Chromium): - launcher runs ONLY Ungoogled Chromium / Chromium (incl. Flatpak), NEVER Google Chrome; clear install guidance + TRONBROWSER_BROWSER override - branded new-tab page: T logo, DuckDuckGo search (default search override = de-googled omnibox), quick links, AI sidebar shortcut - RSS feed reader on the new tab seeded from the user's OPML (Profullstack feeds); add/remove feeds + OPML import/export in Settings; 15-min cache - CoinPay OAuth sign-in (chrome.identity + PKCE) — all auth is CoinPay, not Google - AI settings auto-open on first run; T-logo extension icons - settings sync layer: cloud (Turso-backed api.tronbrowser.dev) by default when signed in, self-hosted URL override, local fallback (best-effort) DB: migration 0002 — anonymous accounts (CoinPay sub, OPTIONAL email) + per-user settings blob for cloud sync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 34b3c33 commit d98c3f2

40 files changed

Lines changed: 715 additions & 105 deletions

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,17 @@ chrome.action.onClicked.addListener((tab) => {
88
chrome.sidePanel.open({ tabId: tab.id }).catch((err) => console.warn('sidePanel open:', err));
99
}
1010
});
11+
12+
// First run with no AI model configured → open settings so keys can be set.
13+
chrome.runtime.onInstalled.addListener(async () => {
14+
const { aiConfig } = await chrome.storage.local.get('aiConfig');
15+
if (!aiConfig || !aiConfig.model) chrome.runtime.openOptionsPage();
16+
});
17+
18+
// Let pages (e.g. the new tab) ask to open the side panel.
19+
chrome.runtime.onMessage.addListener((msg, sender) => {
20+
if (msg?.type === 'open-sidepanel' && chrome.sidePanel?.open) {
21+
const opts = sender.tab?.id != null ? { tabId: sender.tab.id } : {};
22+
chrome.sidePanel.open(opts).catch((err) => console.warn('sidePanel open:', err));
23+
}
24+
});
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// CoinPay OAuth (Authorization Code + PKCE) via chrome.identity. This is the
2+
// TronBrowser login — NOT Google. Endpoints default to hosted CoinPay and are
3+
// overridable for self-hosted CoinPay in Settings.
4+
const DEFAULTS = {
5+
authorizeUrl: 'https://coinpay.profullstack.com/oauth/authorize',
6+
tokenUrl: 'https://coinpay.profullstack.com/oauth/token',
7+
scopes: ['wallet:read', 'payments:x402'],
8+
};
9+
10+
async function cfg() {
11+
const { coinpayConfig } = await chrome.storage.local.get('coinpayConfig');
12+
return {
13+
clientId: coinpayConfig?.clientId || 'tronbrowser',
14+
authorizeUrl: coinpayConfig?.authorizeUrl || DEFAULTS.authorizeUrl,
15+
tokenUrl: coinpayConfig?.tokenUrl || DEFAULTS.tokenUrl,
16+
};
17+
}
18+
19+
function b64url(buf) {
20+
return btoa(String.fromCharCode(...new Uint8Array(buf)))
21+
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
22+
}
23+
24+
async function pkce() {
25+
const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
26+
const challenge = b64url(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)));
27+
return { verifier, challenge };
28+
}
29+
30+
export async function coinpaySignIn() {
31+
const c = await cfg();
32+
const redirectUri = chrome.identity.getRedirectURL();
33+
const state = b64url(crypto.getRandomValues(new Uint8Array(16)));
34+
const { verifier, challenge } = await pkce();
35+
36+
const u = new URL(c.authorizeUrl);
37+
u.searchParams.set('response_type', 'code');
38+
u.searchParams.set('client_id', c.clientId);
39+
u.searchParams.set('redirect_uri', redirectUri);
40+
u.searchParams.set('scope', DEFAULTS.scopes.join(' '));
41+
u.searchParams.set('state', state);
42+
u.searchParams.set('code_challenge', challenge);
43+
u.searchParams.set('code_challenge_method', 'S256');
44+
45+
const redirect = await chrome.identity.launchWebAuthFlow({ url: u.toString(), interactive: true });
46+
const params = new URL(redirect).searchParams;
47+
if (params.get('state') !== state) throw new Error('state mismatch');
48+
const code = params.get('code');
49+
if (!code) throw new Error(params.get('error') || 'no authorization code');
50+
51+
const body = new URLSearchParams({
52+
grant_type: 'authorization_code',
53+
code,
54+
redirect_uri: redirectUri,
55+
client_id: c.clientId,
56+
code_verifier: verifier,
57+
});
58+
const res = await fetch(c.tokenUrl, {
59+
method: 'POST',
60+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
61+
body,
62+
});
63+
if (!res.ok) throw new Error('token exchange failed (' + res.status + ')');
64+
const tok = await res.json();
65+
await chrome.storage.local.set({
66+
coinpay: {
67+
accessToken: tok.access_token,
68+
refreshToken: tok.refresh_token,
69+
expiresAt: Date.now() + (tok.expires_in ? tok.expires_in * 1000 : 3600000),
70+
label: tok.email || tok.sub || '',
71+
},
72+
});
73+
return true;
74+
}
75+
76+
export async function coinpayState() {
77+
const { coinpay } = await chrome.storage.local.get('coinpay');
78+
if (coinpay?.accessToken && coinpay.expiresAt > Date.now()) {
79+
return { signedIn: true, label: coinpay.label, token: coinpay.accessToken };
80+
}
81+
return { signedIn: false };
82+
}
83+
84+
export async function coinpaySignOut() {
85+
await chrome.storage.local.remove('coinpay');
86+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Default RSS subscriptions (from the user's OPML). Overridable via OPML import
2+
// in Settings (stored in chrome.storage.local under "feeds").
3+
export const DEFAULT_FEEDS = [
4+
{ category: 'Food', title: 'Taste of Home', xmlUrl: 'https://www.tasteofhome.com/feed/', htmlUrl: 'https://www.tasteofhome.com/' },
5+
{ category: 'Profullstack, Inc.', title: 'BitTorrented Blog', xmlUrl: 'https://bittorrented.com/blog/rss.xml', htmlUrl: 'https://bittorrented.com/blog' },
6+
{ category: 'Profullstack, Inc.', title: 'bl0ggers Blog', xmlUrl: 'https://bl0ggers.com/blog/rss.xml', htmlUrl: 'https://bl0ggers.com/blog' },
7+
{ category: 'Profullstack, Inc.', title: 'c0mpute blog', xmlUrl: 'https://c0mpute.com/blog/rss.xml', htmlUrl: 'https://c0mpute.com/blog' },
8+
{ category: 'Profullstack, Inc.', title: 'c0upons Blog', xmlUrl: 'https://c0upons.com/blog/rss.xml', htmlUrl: 'https://c0upons.com/blog' },
9+
{ category: 'Profullstack, Inc.', title: 'CoinPay Blog', xmlUrl: 'https://coinpayportal.com/blog/rss.xml', htmlUrl: 'https://coinpayportal.com/blog' },
10+
{ category: 'Profullstack, Inc.', title: 'CrawlProof blog', xmlUrl: 'https://crawlproof.com/blog/rss.xml', htmlUrl: 'https://crawlproof.com/blog' },
11+
{ category: 'Profullstack, Inc.', title: 'd0rz blog', xmlUrl: 'https://d0rz.com/blog/rss.xml', htmlUrl: 'https://d0rz.com/blog' },
12+
{ category: 'Profullstack, Inc.', title: 'PairUX Blog', xmlUrl: 'https://pairux.com/blog/rss.xml', htmlUrl: 'https://pairux.com/blog' },
13+
{ category: 'Profullstack, Inc.', title: 'QryptChat Blog', xmlUrl: 'https://qrypt.chat/blog/rss.xml', htmlUrl: 'https://qrypt.chat/blog' },
14+
{ category: 'Profullstack, Inc.', title: 'SaaSRow Blog', xmlUrl: 'https://saasrow.com/blog/rss.xml', htmlUrl: 'https://saasrow.com/blog' },
15+
{ category: 'Profullstack, Inc.', title: 'sh1pt Blog', xmlUrl: 'https://sh1pt.com/blog/rss.xml', htmlUrl: 'https://sh1pt.com/blog' },
16+
{ category: 'Profullstack, Inc.', title: 'ThreatCrush Blog', xmlUrl: 'https://threatcrush.com/blog/rss.xml', htmlUrl: 'https://threatcrush.com/blog' },
17+
{ category: 'Profullstack, Inc.', title: 'ugig blog', xmlUrl: 'https://ugig.net/blog/rss.xml', htmlUrl: 'https://ugig.net/blog' },
18+
{ category: 'Profullstack, Inc.', title: 'vu1nz Blog', xmlUrl: 'https://vu1nz.com/blog/rss.xml', htmlUrl: 'https://vu1nz.com/blog' },
19+
{ category: 'Projects', title: 'MLT', xmlUrl: 'https://www.mltframework.org/feed.xml', htmlUrl: 'https://mltframework.org/' },
20+
];
21+
22+
/** Parse an OPML string into the feeds array. */
23+
export function parseOpml(xml) {
24+
const doc = new DOMParser().parseFromString(xml, 'text/xml');
25+
const out = [];
26+
for (const node of doc.querySelectorAll('outline[xmlUrl]')) {
27+
const parent = node.parentElement;
28+
const category = parent && parent.tagName === 'outline'
29+
? parent.getAttribute('text') || parent.getAttribute('title') || 'Feeds'
30+
: 'Feeds';
31+
out.push({
32+
category,
33+
title: node.getAttribute('text') || node.getAttribute('title') || node.getAttribute('xmlUrl'),
34+
xmlUrl: node.getAttribute('xmlUrl'),
35+
htmlUrl: node.getAttribute('htmlUrl') || node.getAttribute('xmlUrl'),
36+
});
37+
}
38+
return out;
39+
}
40+
41+
/** Parse an RSS/Atom feed document into {title, link, date} items. */
42+
export function parseFeed(xml) {
43+
const doc = new DOMParser().parseFromString(xml, 'text/xml');
44+
const items = [];
45+
// RSS
46+
for (const it of doc.querySelectorAll('item')) {
47+
items.push({
48+
title: text(it, 'title'),
49+
link: text(it, 'link'),
50+
date: text(it, 'pubDate') || text(it, 'date'),
51+
});
52+
}
53+
// Atom
54+
if (items.length === 0) {
55+
for (const e of doc.querySelectorAll('entry')) {
56+
const link = e.querySelector('link');
57+
items.push({
58+
title: text(e, 'title'),
59+
link: link ? link.getAttribute('href') : '',
60+
date: text(e, 'updated') || text(e, 'published'),
61+
});
62+
}
63+
}
64+
return items;
65+
}
66+
67+
function text(parent, tag) {
68+
const el = parent.querySelector(tag);
69+
return el ? el.textContent.trim() : '';
70+
}
71+
72+
/** Serialize feeds back to OPML (grouped by category). */
73+
export function toOpml(feeds) {
74+
const byCat = {};
75+
for (const f of feeds) (byCat[f.category || 'Feeds'] ||= []).push(f);
76+
const esc = (s) => String(s || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
77+
let body = '';
78+
for (const [cat, items] of Object.entries(byCat)) {
79+
body += ` <outline text="${esc(cat)}" title="${esc(cat)}">\n`;
80+
for (const f of items) {
81+
body += ` <outline text="${esc(f.title)}" title="${esc(f.title)}" type="rss" xmlUrl="${esc(f.xmlUrl)}" htmlUrl="${esc(f.htmlUrl)}"/>\n`;
82+
}
83+
body += ' </outline>\n';
84+
}
85+
return `<?xml version="1.0" encoding="UTF-8"?>
86+
<opml version="2.0">
87+
<head>
88+
<title>TronBrowser RSS Subscriptions</title>
89+
<dateCreated>${new Date().toUTCString()}</dateCreated>
90+
</head>
91+
<body>
92+
${body} </body>
93+
</opml>
94+
`;
95+
}
96+
97+
/** Load feeds from storage, falling back to the OPML defaults. */
98+
export async function loadFeeds() {
99+
const { feeds } = await chrome.storage.local.get('feeds');
100+
return Array.isArray(feeds) && feeds.length ? feeds : DEFAULT_FEEDS;
101+
}
102+
103+
export async function saveFeeds(feeds) {
104+
await chrome.storage.local.set({ feeds });
105+
}
2.82 KB
Loading
329 Bytes
Loading
634 Bytes
Loading
985 Bytes
Loading
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
{
22
"manifest_version": 3,
3-
"name": "TronBrowser AI Sidebar",
4-
"version": "0.1.2",
5-
"description": "Privacy-first AI sidebar — bring your own keys (Claude, GPT, Gemini, DeepSeek, Perplexity, Kimi, Qwen) or run local.",
3+
"name": "TronBrowser",
4+
"version": "0.1.3",
5+
"description": "TronBrowser — privacy-first, AI-native. Branded new tab, DuckDuckGo search, CoinPay login, and a bring-your-own-keys AI sidebar.",
6+
"icons": {
7+
"16": "icons/icon-16.png",
8+
"32": "icons/icon-32.png",
9+
"48": "icons/icon-48.png",
10+
"128": "icons/icon-128.png"
11+
},
612
"permissions": [
713
"storage",
814
"sidePanel",
915
"tabs",
1016
"activeTab",
11-
"scripting"
17+
"scripting",
18+
"identity"
1219
],
1320
"host_permissions": [
1421
"https://api.openai.com/*",
@@ -19,17 +26,37 @@
1926
"https://router.huggingface.co/*",
2027
"https://api.moonshot.ai/*",
2128
"https://dashscope.aliyuncs.com/*",
29+
"https://coinpay.profullstack.com/*",
30+
"https://api.tronbrowser.dev/*",
2231
"http://localhost/*",
23-
"http://127.0.0.1/*"
32+
"http://127.0.0.1/*",
33+
"https://*/*"
2434
],
2535
"background": {
2636
"service_worker": "background.js"
2737
},
2838
"action": {
29-
"default_title": "TronBrowser AI"
39+
"default_title": "TronBrowser AI",
40+
"default_icon": {
41+
"16": "icons/icon-16.png",
42+
"32": "icons/icon-32.png"
43+
}
3044
},
3145
"side_panel": {
3246
"default_path": "sidepanel.html"
3347
},
34-
"options_page": "options.html"
48+
"options_page": "options.html",
49+
"chrome_url_overrides": {
50+
"newtab": "newtab.html"
51+
},
52+
"chrome_settings_overrides": {
53+
"search_provider": {
54+
"name": "DuckDuckGo",
55+
"keyword": "ddg",
56+
"search_url": "https://duckduckgo.com/?q={searchTerms}",
57+
"favicon_url": "https://duckduckgo.com/favicon.ico",
58+
"encoding": "UTF-8",
59+
"is_default": true
60+
}
61+
}
3562
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
:root {
2+
--bg: #05070d; --panel: #0b1020; --line: #1b2540;
3+
--cyan: #34e7ff; --fg: #cfe8ff; --muted: #6f86b3;
4+
}
5+
* { box-sizing: border-box; }
6+
body {
7+
margin: 0; min-height: 100vh; color: var(--fg);
8+
background: radial-gradient(1100px 560px at 50% -10%, #0a1430 0%, var(--bg) 55%);
9+
font: 15px/1.55 ui-monospace, "SF Mono", Menlo, monospace;
10+
}
11+
.top { display: flex; justify-content: flex-end; align-items: center; gap: 12px; padding: 14px 18px; }
12+
.account { color: var(--muted); font-size: 13px; }
13+
.signin { background: var(--cyan); color: #04060c; border: 0; border-radius: 8px;
14+
padding: 7px 14px; font: inherit; font-weight: 700; cursor: pointer; }
15+
.signin.out { background: transparent; color: var(--fg); border: 1px solid var(--line); }
16+
.center { text-align: center; padding: 24px 16px 8px; }
17+
.logo { filter: drop-shadow(0 0 16px rgba(52,231,255,.5)); }
18+
.brand { font-size: 34px; font-weight: 800; letter-spacing: 2px; margin: 10px 0 2px; color: #fff; }
19+
.brand span { color: var(--cyan); }
20+
.tag { color: var(--muted); margin: 0 0 18px; }
21+
.search { margin: 0 auto; max-width: 620px; }
22+
.search input { width: 100%; padding: 14px 18px; border-radius: 12px; font: inherit;
23+
background: #04060c; color: var(--fg); border: 1px solid var(--line); }
24+
.search input:focus { outline: none; border-color: var(--cyan); }
25+
.links { margin: 16px 0 8px; display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; }
26+
.links a { color: var(--cyan); text-decoration: none; font-size: 14px; }
27+
.feeds { max-width: 1100px; margin: 24px auto 64px; padding: 0 18px; }
28+
.feeds-head { display: flex; align-items: baseline; gap: 12px; border-bottom: 1px solid var(--line); padding-bottom: 8px; }
29+
.feeds-head h2 { margin: 0; font-size: 16px; color: #fff; }
30+
.feeds-head button { background: transparent; border: 1px solid var(--line); color: var(--muted);
31+
border-radius: 6px; padding: 2px 10px; cursor: pointer; font: inherit; font-size: 12px; }
32+
.feedgrid { margin-top: 16px; columns: 3 280px; column-gap: 18px; }
33+
.feedcard { break-inside: avoid; background: var(--panel); border: 1px solid var(--line);
34+
border-radius: 10px; padding: 12px 14px; margin-bottom: 18px; }
35+
.feedcard h3 { margin: 0 0 8px; font-size: 13px; color: var(--cyan); }
36+
.feedcard ul { list-style: none; margin: 0; padding: 0; }
37+
.feedcard li { margin: 0 0 7px; }
38+
.feedcard a { color: var(--fg); text-decoration: none; font-size: 13px; }
39+
.feedcard a:hover { color: var(--cyan); }
40+
.feedcard .date { color: var(--muted); font-size: 11px; }
41+
.feedcard .err { color: #ff8a9b; font-size: 12px; }
42+
.muted { color: var(--muted); }

0 commit comments

Comments
 (0)