Skip to content

Commit 7ef12b4

Browse files
ralyodioclaude
andauthored
TronBrowser's own Web Push service (a setting, defaulting to tronbrowser.dev) (#120)
* Give TronBrowser its own Web Push service, and make it a setting ungoogled-chromium ships without a push service and a page cannot choose one, so pushManager.subscribe() failed on every site with 'Registration failed - push service error' (reproduced in the bundled 152 engine). - services/api: an RFC 8030 push service at /api/1/push. Site servers send standard aes128gcm + VAPID pushes (the sender's key must match the one the site subscribed with); ciphertext is queued until the browser acks it and delivered live over a WebSocket. 404/410/413/415/429 as senders expect. - extension: push-page.js (MAIN world) replaces PushManager.subscribe / getSubscription / permissionState; push-client.js holds the subscription keys, registers, decrypts (RFC 8291, WebCrypto) and shows the notification. - setting: Settings -> Push notifications. Default tronbrowser.dev, any compatible https URL (checked against GET / first), or off (engine's own). - /push docs page, privacy entry, migration 0007. Verified end to end in the real engine: subscribe returns a PushSubscription, a real sendPush gets 201, the notification is created with its click URL and the message is acked over the socket within 3s. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Declare the chrome global in the push client test Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent f338918 commit 7ef12b4

19 files changed

Lines changed: 1347 additions & 3 deletions

File tree

‎Caddyfile‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
# (no content hashing yet — don't let stale JS strand logged-in users).
5353
@images path *.svg *.png *.ico
5454
header @images Cache-Control "public, max-age=86400"
55-
@code path *.js *.css *.html / /privacy /login /settings /dns
55+
@code path *.js *.css *.html / /privacy /login /settings /dns /push
5656
header @code Cache-Control "public, max-age=60, must-revalidate"
5757

5858
encode gzip zstd
@@ -66,6 +66,8 @@
6666
rewrite @settings /settings.html
6767
@dns path /dns
6868
rewrite @dns /dns.html
69+
@push path /push
70+
rewrite @push /push.html
6971

7072
file_server
7173

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { decideInstallTarget, lookupInstalled } from './install-state.js';
22
import { PIT_SOCKS_PORT, pitProxyConfig } from './pit-proxy.js';
3+
import { installPush } from './push-client.js';
4+
5+
// Web Push through TronBrowser's configured push service (the engine has none).
6+
installPush();
37

48
// Open the AI side panel when the toolbar action is clicked.
59
chrome.sidePanel

‎apps/desktop/extensions/ai-sidebar/manifest.json‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
"scripting",
1919
"proxy",
2020
"privacy",
21-
"notifications"
21+
"notifications",
22+
"alarms",
23+
"contentSettings"
2224
],
2325
"host_permissions": [
2426
"https://api.openai.com/*",
@@ -40,6 +42,29 @@
4042
"type": "module"
4143
},
4244
"content_scripts": [
45+
{
46+
"matches": [
47+
"https://*/*",
48+
"http://localhost/*",
49+
"http://127.0.0.1/*"
50+
],
51+
"js": [
52+
"push-page.js"
53+
],
54+
"run_at": "document_start",
55+
"world": "MAIN"
56+
},
57+
{
58+
"matches": [
59+
"https://*/*",
60+
"http://localhost/*",
61+
"http://127.0.0.1/*"
62+
],
63+
"js": [
64+
"push-bridge.js"
65+
],
66+
"run_at": "document_start"
67+
},
4368
{
4469
"matches": [
4570
"https://chromewebstore.google.com/detail/*",

‎apps/desktop/extensions/ai-sidebar/options.html‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,25 @@ <h2>Name resolution</h2>
8989
name still needs the certificate that <code>moshcode dns enable</code> installs.
9090
</p>
9191

92+
<h2>Push notifications</h2>
93+
<p class="hint">
94+
Sites send browser notifications through a push service the browser chooses.
95+
The engine TronBrowser runs on ships without one, so TronBrowser brings its
96+
own. Messages reach it encrypted to a key only this browser holds.
97+
</p>
98+
<label for="pushMode">Push service</label>
99+
<select id="pushMode">
100+
<option value="default">TronBrowser (tronbrowser.dev/api/1/push)</option>
101+
<option value="custom">Another push service…</option>
102+
<option value="off">Off (the engine's own behaviour)</option>
103+
</select>
104+
<div id="pushCustomRow" hidden>
105+
<label for="pushUrl">Push service URL</label>
106+
<input id="pushUrl" placeholder="https://push.example.com/api/1/push" />
107+
</div>
108+
<button id="savePush">Save</button><span id="savedPush" class="saved"></span>
109+
<p class="hint">Changing it signs sites out of push; they subscribe again the next time they ask.</p>
110+
92111
<h2>AI providers (bring your own keys)</h2>
93112
<p class="hint">Add keys for as many providers as you like, then pick a default for
94113
the sidebar. Keys are stored on your account (encrypted end-to-end when a vault

‎apps/desktop/extensions/ai-sidebar/options.js‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "./coinpay-auth.js";
1010
import { pushSettings, pullSettings } from "./settings-store.js";
1111
import { encryptVault, decryptVault } from "./vault.js";
12+
import { DEFAULT_PUSH_SERVICE, pushServiceFrom } from "./push-client.js";
1213
import {
1314
connect as btrConnect,
1415
disconnect as btrDisconnect,
@@ -391,6 +392,42 @@ function flash(id, msg) {
391392
el(id).textContent = msg;
392393
setTimeout(() => (el(id).textContent = ""), 1600);
393394
}
395+
396+
/* ---------- Push service (push-client.js reads `pushService`) ---------- */
397+
async function mountPush() {
398+
const { pushService } = await chrome.storage.local.get("pushService");
399+
const mode = pushService === "off" ? "off" : pushService && pushService !== DEFAULT_PUSH_SERVICE ? "custom" : "default";
400+
el("pushMode").value = mode;
401+
el("pushUrl").value = mode === "custom" ? pushService : "";
402+
el("pushCustomRow").hidden = mode !== "custom";
403+
el("pushMode").addEventListener("change", () => {
404+
el("pushCustomRow").hidden = el("pushMode").value !== "custom";
405+
});
406+
el("savePush").addEventListener("click", async () => {
407+
const choice = el("pushMode").value;
408+
if (choice === "off") {
409+
await chrome.storage.local.set({ pushService: "off" });
410+
return flash("savedPush", "push off ✓");
411+
}
412+
if (choice === "default") {
413+
await chrome.storage.local.remove("pushService");
414+
return flash("savedPush", "saved ✓");
415+
}
416+
const url = pushServiceFrom(el("pushUrl").value);
417+
if (!url || url === DEFAULT_PUSH_SERVICE) return flash("savedPush", "enter an https:// URL");
418+
// Check it speaks our protocol before switching every site over to it.
419+
try {
420+
const info = await (await fetch(url)).json();
421+
if (info.service !== "tronbrowser-push") throw new Error("not a compatible push service");
422+
} catch (e) {
423+
return flash("savedPush", `can't use that URL: ${e.message}`);
424+
}
425+
await chrome.storage.local.set({ pushService: url });
426+
flash("savedPush", "saved ✓");
427+
});
428+
}
429+
mountPush();
430+
394431
function escape(s) {
395432
const d = document.createElement("div");
396433
d.textContent = s || "";
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Isolated-world half of the pushManager replacement: forwards push-page.js's
2+
// requests to the background and hands the answers back. The background reads
3+
// the origin from Chrome's sender info, so nothing the page sends here can act
4+
// for another site.
5+
(() => {
6+
if (window.top !== window) return;
7+
const TYPES = new Set(['push:subscribe', 'push:get', 'push:unsubscribe']);
8+
window.addEventListener('message', (event) => {
9+
const data = event.data;
10+
if (event.source !== window || data?.__tronPush !== 'request' || !TYPES.has(data.type)) return;
11+
const reply = (result) =>
12+
window.postMessage({ __tronPush: 'response', id: data.id, result }, location.origin);
13+
try {
14+
chrome.runtime.sendMessage(
15+
{ type: data.type, scope: typeof data.scope === 'string' ? data.scope : undefined, applicationServerKey: data.applicationServerKey ?? null },
16+
(result) => {
17+
if (chrome.runtime.lastError) reply({ error: 'AbortError', message: 'Registration failed - push service error' });
18+
else reply(result);
19+
},
20+
);
21+
} catch {
22+
// The extension was reloaded under this page; behave as if push is off.
23+
reply({ off: true });
24+
}
25+
});
26+
})();

0 commit comments

Comments
 (0)