Skip to content

Commit c79d28e

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents 4f21583 + 60b2675 commit c79d28e

31 files changed

Lines changed: 647 additions & 51 deletions

File tree

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { destinationFor, moshpitConfig, parseRegistryName } from './moshpit.js';
2+
13
// Open the AI side panel when the toolbar action is clicked.
24
chrome.sidePanel
35
.setPanelBehavior({ openPanelOnActionClick: true })
@@ -288,3 +290,68 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
288290
}
289291
} catch (_) { /* best effort */ }
290292
})();
293+
294+
// --- Moshpit name resolution ---------------------------------------------
295+
// This is what makes the Moshpit settings on the options page actually do
296+
// something: until now they were written to storage and never read.
297+
//
298+
// Two hooks, because "does clearnet answer for this name?" is only knowable at
299+
// two different moments:
300+
//
301+
// onErrorOccurred — DNS came up empty (ERR_NAME_NOT_RESOLVED). This is the
302+
// backfill path, and the ONLY one active in the default 'clearnet' mode, so
303+
// someone who has never heard of Moshpit gets ordinary browsing plus a
304+
// rescued error page. Nothing that already works is touched.
305+
//
306+
// onBeforeNavigate — consulted ONLY in 'moshpit' mode, where a registered
307+
// name is meant to win even though clearnet has an answer. It costs a
308+
// registry round-trip before navigation, which is why the default mode
309+
// never goes near it.
310+
//
311+
// No redirect loop: every destination we send a tab to (pit.moshcode.sh/n/…,
312+
// app.moshcode.sh/pit) has three labels, so parseRegistryName rejects it and
313+
// the hooks ignore it on the way back through.
314+
315+
const DNS_FAILED = new Set([
316+
'net::ERR_NAME_NOT_RESOLVED',
317+
'net::ERR_NAME_RESOLUTION_FAILED',
318+
]);
319+
320+
function moshpitHostname(url) {
321+
try {
322+
const u = new URL(url);
323+
if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';
324+
return parseRegistryName(u.hostname) ? u.hostname : '';
325+
} catch {
326+
return '';
327+
}
328+
}
329+
330+
async function sendTabTo(tabId, url) {
331+
try {
332+
await chrome.tabs.update(tabId, { url });
333+
} catch (err) {
334+
console.warn('moshpit redirect:', err);
335+
}
336+
}
337+
338+
chrome.webNavigation?.onErrorOccurred.addListener(async (details) => {
339+
if (details.frameId !== 0) return; // top-level navigations only
340+
if (!DNS_FAILED.has(details.error)) return;
341+
const hostname = moshpitHostname(details.url);
342+
if (!hostname) return;
343+
const dest = await destinationFor(hostname, false);
344+
if (dest) await sendTabTo(details.tabId, dest);
345+
});
346+
347+
chrome.webNavigation?.onBeforeNavigate.addListener(async (details) => {
348+
if (details.frameId !== 0) return;
349+
const hostname = moshpitHostname(details.url);
350+
if (!hostname) return;
351+
// The default mode must never pre-empt a working clearnet domain — bail out
352+
// before the registry is ever contacted.
353+
const { mode } = await moshpitConfig();
354+
if (mode !== 'moshpit') return;
355+
const dest = await destinationFor(hostname, true);
356+
if (dest) await sendTabTo(details.tabId, dest);
357+
});

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "TronBrowser",
4-
"version": "3.8.4",
4+
"version": "3.8.5",
55
"description": "TronBrowser — privacy-first, AI-native. Branded new tab, private search, CoinPay login, and a bring-your-own-keys AI sidebar.",
66
"icons": {
77
"16": "icons/icon-16.png",
@@ -15,6 +15,7 @@
1515
"tabs",
1616
"activeTab",
1717
"scripting",
18+
"webNavigation",
1819
"proxy",
1920
"privacy",
2021
"notifications"
@@ -35,7 +36,8 @@
3536
"https://*/*"
3637
],
3738
"background": {
38-
"service_worker": "background.js"
39+
"service_worker": "background.js",
40+
"type": "module"
3941
},
4042
"content_scripts": [
4143
{
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// Moshpit name resolution for the extension.
2+
//
3+
// The policy here is a straight port of apps/desktop/src/moshpit-resolve.ts —
4+
// same names, same semantics — because the extension is plain JS with no build
5+
// step and cannot import the launcher's TypeScript. The TS module stays the
6+
// reference implementation and keeps the exhaustive unit tests; this file is
7+
// what actually runs in the browser. Keep them in sync.
8+
//
9+
// See that module's header for why 'clearnet' is the default: silently
10+
// redirecting a domain that resolves perfectly well is indistinguishable from
11+
// hijacking it.
12+
13+
export const DEFAULT_REGISTRY_BASE = 'https://pit.moshcode.sh';
14+
export const DEFAULT_CONSOLE_BASE = 'https://app.moshcode.sh';
15+
16+
// The one label meaning "manage this namespace" rather than "visit a name".
17+
// Reserved, not claimable — otherwise whoever holds `.eggs` could register
18+
// `mosh.eggs` and own the page people use to check who holds `.eggs`.
19+
export const CONSOLE_LABEL = 'mosh';
20+
21+
// Where a name with no destination yet is parked. A name inside the namespace
22+
// should never dead-end on ERR_NAME_NOT_RESOLVED.
23+
export const DEFAULT_PARKING_BASE = 'https://moshcoding.com';
24+
25+
/** The parking page for a name with no destination yet. */
26+
export function parkingUrlFor(name, parkingBase = DEFAULT_PARKING_BASE) {
27+
return `${parkingBase.replace(/\/+$/, '')}/parking?name=${encodeURIComponent(name)}`;
28+
}
29+
30+
/** Read the settings the options page writes. */
31+
export async function moshpitConfig() {
32+
const { moshpitConfig: cfg } = await chrome.storage.local.get('moshpitConfig');
33+
return {
34+
mode: cfg?.mode === 'moshpit' ? 'moshpit' : 'clearnet',
35+
registryBase: (cfg?.registryBase || DEFAULT_REGISTRY_BASE).replace(/\/+$/, ''),
36+
consoleBase: (cfg?.consoleBase || DEFAULT_CONSOLE_BASE).replace(/\/+$/, ''),
37+
parkingBase: (cfg?.parkingBase || DEFAULT_PARKING_BASE).replace(/\/+$/, ''),
38+
};
39+
}
40+
41+
/**
42+
* Split a hostname the way the registry does: exactly one label and one TLD.
43+
* Anything else (`a.b.c`, a bare `localhost`, an IP) is not a Moshpit name and
44+
* must never be sent to the registry as if it were.
45+
*/
46+
export function parseRegistryName(hostname) {
47+
const host = String(hostname || '').trim().toLowerCase().replace(/\.$/, '');
48+
if (!host || host.includes(':')) return null;
49+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return null;
50+
const parts = host.split('.');
51+
if (parts.length !== 2) return null;
52+
const [label, tld] = parts;
53+
const LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
54+
if (!LABEL.test(label) || !LABEL.test(tld)) return null;
55+
return { label, tld };
56+
}
57+
58+
/** The Pit URL for a `mosh.<tld>` hostname, or null when it isn't one. */
59+
export function consoleUrlFor(hostname, consoleBase = DEFAULT_CONSOLE_BASE) {
60+
const parsed = parseRegistryName(hostname);
61+
if (!parsed || parsed.label !== CONSOLE_LABEL) return null;
62+
return `${consoleBase.replace(/\/+$/, '')}/pit?tld=${encodeURIComponent(parsed.tld)}`;
63+
}
64+
65+
/** The URL that serves a resolved Moshpit name through the gateway. */
66+
export function gatewayUrlFor(resolved, registryBase = DEFAULT_REGISTRY_BASE) {
67+
return `${registryBase.replace(/\/+$/, '')}/n/${encodeURIComponent(resolved)}`;
68+
}
69+
70+
/**
71+
* Ask the registry about a name. Any failure returns null rather than throwing:
72+
* resolution sits in front of every navigation, so a registry that is slow,
73+
* down, or serving nonsense must degrade to "clearnet as usual".
74+
*/
75+
export async function lookupMoshpit(hostname, { registryBase, timeoutMs = 4000 } = {}) {
76+
const parsed = parseRegistryName(hostname);
77+
if (!parsed) return null;
78+
const base = (registryBase || DEFAULT_REGISTRY_BASE).replace(/\/+$/, '');
79+
const controller = new AbortController();
80+
const timer = setTimeout(() => controller.abort(), timeoutMs);
81+
try {
82+
const name = `${parsed.label}.${parsed.tld}`;
83+
const res = await fetch(`${base}/api/moshpit/resolve?name=${encodeURIComponent(name)}`, {
84+
signal: controller.signal,
85+
});
86+
if (!res.ok) return null;
87+
const json = await res.json();
88+
// `registered` means the TLD is claimed; `name_registered` means THIS name
89+
// is. `target` is the address — null until the name points somewhere, which
90+
// is what decides parked vs live. `resolved` echoes the name either way.
91+
const claimed = typeof json?.name_registered === 'boolean' ? json.name_registered : json?.registered;
92+
if (typeof claimed !== 'boolean') return null;
93+
return {
94+
registered: claimed,
95+
resolved: typeof json.resolved === 'string' ? json.resolved : name,
96+
target: typeof json.target === 'string' && json.target ? json.target : null,
97+
};
98+
} catch {
99+
return null;
100+
} finally {
101+
clearTimeout(timer);
102+
}
103+
}
104+
105+
/**
106+
* Decide which namespace a hostname belongs to. Pure and total: every branch
107+
* returns a decision with a reason, so the caller never has to invent
108+
* behaviour for an unhandled combination.
109+
*/
110+
export function decideResolution({ hostname, mode, clearnetResolves, moshpit, consoleBase, parkingBase }) {
111+
// `mosh.<tld>` is the registration console for `.<tld>`, not a name to fetch.
112+
// It obeys the SAME precedence as any other Moshpit answer rather than taking
113+
// an exemption — `mosh.org` and `mosh.com` are real clearnet domains.
114+
const consoleUrl = consoleUrlFor(hostname, consoleBase);
115+
if (consoleUrl) {
116+
if (mode === 'clearnet' && clearnetResolves) {
117+
return { use: 'clearnet', reason: 'clearnet answers for this name (Moshpit set to backfill only)' };
118+
}
119+
const tld = parseRegistryName(hostname)?.tld;
120+
return {
121+
use: 'register',
122+
reason: `${CONSOLE_LABEL}.${tld} is the registration console for .${tld}`,
123+
url: consoleUrl,
124+
};
125+
}
126+
127+
// A registry outage must not take the ordinary web down with it — nor lie
128+
// with a parking page for a name it simply failed to look up.
129+
if (!moshpit) {
130+
return { use: 'clearnet', reason: 'Moshpit registry not consulted or unreachable' };
131+
}
132+
133+
// Claimed AND pointed somewhere — the precedence rules apply to it.
134+
if (moshpit.registered && moshpit.target) {
135+
if (mode === 'moshpit') {
136+
return {
137+
use: 'moshpit',
138+
reason: clearnetResolves
139+
? 'registered in Moshpit — overriding the clearnet domain'
140+
: 'registered in Moshpit',
141+
resolved: moshpit.resolved,
142+
};
143+
}
144+
if (clearnetResolves) {
145+
return { use: 'clearnet', reason: 'clearnet answers for this name (Moshpit set to backfill only)' };
146+
}
147+
return {
148+
use: 'moshpit',
149+
reason: 'clearnet has no answer — resolved through Moshpit',
150+
resolved: moshpit.resolved,
151+
};
152+
}
153+
154+
// Unclaimed, or claimed but not pointed at an address yet — park it, so
155+
// `california.oranges` explains itself instead of looking broken. Only ever
156+
// where clearnet has nothing.
157+
if (!clearnetResolves && parseRegistryName(hostname)) {
158+
return {
159+
use: 'park',
160+
reason: moshpit.registered
161+
? 'registered in Moshpit but not pointed at an address yet'
162+
: 'unclaimed Moshpit name — parked',
163+
url: parkingUrlFor(String(hostname).trim().toLowerCase().replace(/\.$/, ''), parkingBase),
164+
};
165+
}
166+
if (clearnetResolves) {
167+
return {
168+
use: 'clearnet',
169+
reason: moshpit.registered
170+
? 'registered in Moshpit but not pointed anywhere yet'
171+
: 'not registered in Moshpit',
172+
};
173+
}
174+
return { use: 'clearnet', reason: 'not a Moshpit name' };
175+
}
176+
177+
/**
178+
* The whole policy for one navigation, as a URL to send the tab to (or null to
179+
* leave it alone). `clearnetResolves` is supplied by the caller because only it
180+
* knows whether DNS actually answered.
181+
*/
182+
export async function destinationFor(hostname, clearnetResolves) {
183+
if (!parseRegistryName(hostname)) return null;
184+
const { mode, registryBase, consoleBase, parkingBase } = await moshpitConfig();
185+
186+
// Skip the registry round-trip entirely for the console label — it is
187+
// reserved, so no lookup can change the answer.
188+
const isConsole = !!consoleUrlFor(hostname, consoleBase);
189+
const moshpit = isConsole ? null : await lookupMoshpit(hostname, { registryBase });
190+
191+
const decision = decideResolution({ hostname, mode, clearnetResolves, moshpit, consoleBase, parkingBase });
192+
if (decision.use === 'register' || decision.use === 'park') return decision.url;
193+
if (decision.use === 'moshpit') return gatewayUrlFor(decision.resolved, registryBase);
194+
return null;
195+
}

0 commit comments

Comments
 (0)