Skip to content

Commit 36589e9

Browse files
ralyodioclaude
andcommitted
feat(resolve): choose whether Moshpit overrides clearnet for a contested name
Two namespaces now answer to the same shape of name. `profullstack.ai` is a real clearnet domain someone can squat, and it is also a name the Moshpit registry can hold. Something has to decide which one a navigation means, and that cannot be hardcoded: a user who has never heard of Moshpit must keep getting clearnet, while an operator who registered the name expects their version to win. So it is a setting, with two honest positions: clearnet (default) — clearnet owns any name clearnet can answer; Moshpit is consulted only where DNS came up empty, making the registry a backfill that fills gaps rather than shadowing the existing web. Default because silently redirecting a domain that resolves perfectly well is indistinguishable from hijacking it. moshpit — a registered name wins even when clearnet has an answer. This is the override: the point of registering profullstack.ai in Moshpit is that your version is the one you get. Names under an ending clearnet has never heard of (.yeah, .sploof) resolve through Moshpit in either mode — nothing conflicts, and refusing them would defeat the namespace. decideResolution is pure and total: every branch returns a decision with a reason, so the caller never invents behaviour for an unhandled combination and an override never looks like a glitch. A registry that is down, slow, or serving nonsense yields null and falls back to clearnet — resolution sits in front of every navigation, so a registry outage must not take the ordinary web down with it. parseRegistryName refuses anything that is not exactly one label and one TLD, so `a.b.c`, `localhost` and IP literals are never sent to the registry as if they were names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 2214c1b commit 36589e9

4 files changed

Lines changed: 357 additions & 0 deletions

File tree

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,25 @@ <h2>Account</h2>
7777
<label for="syncUrl">Self-hosted sync backend URL (optional)</label>
7878
<input id="syncUrl" placeholder="leave blank to use the managed cloud" />
7979

80+
<h2>Name resolution</h2>
81+
<p class="hint">
82+
Moshpit names (<code>fuck.yeah</code>, <code>original.sploof</code>) always resolve here —
83+
clearnet has never heard of those endings. This setting only decides what happens when
84+
<em>both</em> namespaces answer, e.g. <code>profullstack.ai</code>.
85+
</p>
86+
<label for="moshpitMode">When a name exists in both</label>
87+
<select id="moshpitMode">
88+
<option value="clearnet">Clearnet wins — use Moshpit only to fill gaps (default)</option>
89+
<option value="moshpit">Moshpit wins — override the clearnet domain</option>
90+
</select>
91+
<p class="hint">
92+
Overriding means a domain someone else holds on clearnet resolves to <em>your</em> Moshpit
93+
registration instead. Left on the default, a domain that already works is never redirected.
94+
</p>
95+
<label for="moshpitRegistry">Registry (advanced)</label>
96+
<input id="moshpitRegistry" placeholder="https://pit.moshcode.sh" />
97+
<div id="savedMoshpit" class="hint"></div>
98+
8099
<h2>AI providers (bring your own keys)</h2>
81100
<p class="hint">Add keys for as many providers as you like, then pick a default for
82101
the sidebar. Keys are stored on your account (encrypted end-to-end when a vault

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,10 @@ async function loadAll() {
366366
buildProviders(provs, aiDefault);
367367
el("cpClient").value = coinpayConfig?.clientId || "";
368368
el("syncUrl").value = syncConfig?.url || "";
369+
const moshpit = (await chrome.storage.local.get("moshpitConfig")).moshpitConfig || {};
370+
// Absent means never configured, which is the default — not "off".
371+
el("moshpitMode").value = moshpit.mode === "moshpit" ? "moshpit" : "clearnet";
372+
el("moshpitRegistry").value = moshpit.registryBase || "";
369373
await renderAccount();
370374
await mountSections(); // Search / Markets / Sports / RSS feeds (shared module)
371375
await renderBtr();
@@ -383,6 +387,22 @@ el("syncUrl").addEventListener("change", async () => {
383387
syncConfig: { url: el("syncUrl").value.trim() },
384388
});
385389
});
390+
async function saveMoshpit() {
391+
await chrome.storage.local.set({
392+
moshpitConfig: {
393+
mode: el("moshpitMode").value === "moshpit" ? "moshpit" : "clearnet",
394+
registryBase: el("moshpitRegistry").value.trim(),
395+
},
396+
});
397+
flash(
398+
"savedMoshpit",
399+
el("moshpitMode").value === "moshpit"
400+
? "Moshpit will override clearnet for names it holds"
401+
: "Clearnet wins; Moshpit fills gaps only",
402+
);
403+
}
404+
el("moshpitMode").addEventListener("change", saveMoshpit);
405+
el("moshpitRegistry").addEventListener("change", saveMoshpit);
386406
async function get(k) {
387407
return (await chrome.storage.local.get(k))[k] || {};
388408
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
3+
import {
4+
DEFAULT_RESOLVE_MODE,
5+
decideResolution,
6+
gatewayUrlFor,
7+
lookupMoshpit,
8+
parseRegistryName,
9+
type MoshpitLookup,
10+
} from './moshpit-resolve';
11+
12+
const registered = (resolved: string): MoshpitLookup => ({ registered: true, resolved });
13+
const unregistered: MoshpitLookup = { registered: false, resolved: '' };
14+
15+
describe('decideResolution — clearnet mode (the default)', () => {
16+
it('defaults to clearnet', () => {
17+
expect(DEFAULT_RESOLVE_MODE).toBe('clearnet');
18+
});
19+
20+
it('leaves a working clearnet domain alone even when Moshpit holds the name', () => {
21+
// The squatting case, from the safe side: someone holds profullstack.ai on
22+
// clearnet AND we hold it in Moshpit. Default must not hijack it — silently
23+
// redirecting a domain that resolves is indistinguishable from a takeover.
24+
const d = decideResolution({
25+
hostname: 'profullstack.ai',
26+
mode: 'clearnet',
27+
clearnetResolves: true,
28+
moshpit: registered('profullstack.ai'),
29+
});
30+
expect(d.use).toBe('clearnet');
31+
expect(d.reason).toMatch(/backfill/i);
32+
});
33+
34+
it('backfills a name clearnet cannot answer', () => {
35+
const d = decideResolution({
36+
hostname: 'original.sploof',
37+
mode: 'clearnet',
38+
clearnetResolves: false,
39+
moshpit: registered('original.sploof'),
40+
});
41+
expect(d.use).toBe('moshpit');
42+
expect(d.resolved).toBe('original.sploof');
43+
});
44+
});
45+
46+
describe('decideResolution — moshpit mode (the override)', () => {
47+
it('overrides a live clearnet domain', () => {
48+
// The whole point of registering profullstack.ai in Moshpit: your version
49+
// wins regardless of who holds the clearnet domain.
50+
const d = decideResolution({
51+
hostname: 'profullstack.ai',
52+
mode: 'moshpit',
53+
clearnetResolves: true,
54+
moshpit: registered('profullstack.ai'),
55+
});
56+
expect(d.use).toBe('moshpit');
57+
expect(d.reason).toMatch(/overriding the clearnet domain/i);
58+
expect(d.resolved).toBe('profullstack.ai');
59+
});
60+
61+
it('follows an alias to its target', () => {
62+
const d = decideResolution({
63+
hostname: 'profullstack.agentic',
64+
mode: 'moshpit',
65+
clearnetResolves: false,
66+
moshpit: registered('profullstack.agent'),
67+
});
68+
expect(d.resolved).toBe('profullstack.agent');
69+
});
70+
71+
it('still falls through to clearnet for a name Moshpit does not hold', () => {
72+
const d = decideResolution({
73+
hostname: 'example.com',
74+
mode: 'moshpit',
75+
clearnetResolves: true,
76+
moshpit: unregistered,
77+
});
78+
expect(d.use).toBe('clearnet');
79+
});
80+
});
81+
82+
describe('decideResolution — a registry outage must not break browsing', () => {
83+
it.each(['clearnet', 'moshpit'] as const)('falls back to clearnet in %s mode', (mode) => {
84+
const d = decideResolution({
85+
hostname: 'profullstack.ai',
86+
mode,
87+
clearnetResolves: true,
88+
moshpit: null,
89+
});
90+
expect(d.use).toBe('clearnet');
91+
expect(d.reason).toMatch(/unreachable|not consulted/i);
92+
});
93+
94+
it('always explains itself', () => {
95+
// Every branch carries a reason, so an override never looks like a glitch.
96+
for (const mode of ['clearnet', 'moshpit'] as const) {
97+
for (const clearnetResolves of [true, false]) {
98+
for (const moshpit of [null, unregistered, registered('x.y')]) {
99+
const d = decideResolution({ hostname: 'x.y', mode, clearnetResolves, moshpit });
100+
expect(d.reason.length).toBeGreaterThan(0);
101+
}
102+
}
103+
}
104+
});
105+
});
106+
107+
describe('parseRegistryName', () => {
108+
it('accepts exactly one label and one TLD', () => {
109+
expect(parseRegistryName('fuck.yeah')).toEqual({ label: 'fuck', tld: 'yeah' });
110+
expect(parseRegistryName('California.Oranges')).toEqual({ label: 'california', tld: 'oranges' });
111+
expect(parseRegistryName('original.sploof.')).toEqual({ label: 'original', tld: 'sploof' });
112+
});
113+
114+
it('rejects anything that is not a registry name', () => {
115+
// Sending these to the registry would be asking about a name that cannot
116+
// exist — and acting on the answer would misroute ordinary browsing.
117+
expect(parseRegistryName('a.b.c')).toBeNull();
118+
expect(parseRegistryName('localhost')).toBeNull();
119+
expect(parseRegistryName('192.168.1.1')).toBeNull();
120+
expect(parseRegistryName('box.example.com:9161')).toBeNull();
121+
expect(parseRegistryName('')).toBeNull();
122+
expect(parseRegistryName('-bad.yeah')).toBeNull();
123+
});
124+
});
125+
126+
describe('lookupMoshpit', () => {
127+
const okFetch = (body: unknown): typeof fetch =>
128+
vi.fn(async () => ({ ok: true, json: async () => body })) as unknown as typeof fetch;
129+
130+
it('reads the registry answer', async () => {
131+
const result = await lookupMoshpit('profullstack.agentic', {
132+
fetchImpl: okFetch({ name: 'profullstack.agentic', resolved: 'profullstack.agent', registered: true }),
133+
});
134+
expect(result).toEqual({ registered: true, resolved: 'profullstack.agent' });
135+
});
136+
137+
it('never asks about a name the registry could not hold', async () => {
138+
const fetchImpl = okFetch({ registered: true, resolved: 'x' });
139+
expect(await lookupMoshpit('a.b.c', { fetchImpl })).toBeNull();
140+
expect(fetchImpl).not.toHaveBeenCalled();
141+
});
142+
143+
it('returns null rather than throwing when the registry is down', async () => {
144+
const dead = vi.fn(async () => {
145+
throw new Error('ECONNREFUSED');
146+
}) as unknown as typeof fetch;
147+
expect(await lookupMoshpit('fuck.yeah', { fetchImpl: dead })).toBeNull();
148+
});
149+
150+
it('returns null on a nonsense payload', async () => {
151+
expect(await lookupMoshpit('fuck.yeah', { fetchImpl: okFetch({ nope: 1 }) })).toBeNull();
152+
});
153+
});
154+
155+
describe('gatewayUrlFor', () => {
156+
it('builds a gateway URL and tolerates a trailing slash', () => {
157+
expect(gatewayUrlFor('fuck.yeah')).toBe('https://pit.moshcode.sh/n/fuck.yeah');
158+
expect(gatewayUrlFor('fuck.yeah', 'https://my.pit/')).toBe('https://my.pit/n/fuck.yeah');
159+
});
160+
});
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/**
2+
* Moshpit name resolution, and how it coexists with clearnet DNS.
3+
*
4+
* Two namespaces now answer to the same shape of name. `profullstack.ai` is a
5+
* real clearnet domain someone can squat, and it is *also* a name the Moshpit
6+
* registry can hold. Something has to decide which one a navigation means, and
7+
* that decision cannot be hardcoded: a user who has never heard of Moshpit must
8+
* keep getting clearnet, while an operator who registered the name in Moshpit
9+
* expects their version to win.
10+
*
11+
* So it is a setting, with two honest positions:
12+
*
13+
* 'clearnet' (default) — clearnet owns any name clearnet can answer. Moshpit
14+
* is consulted only where DNS came up empty, which makes the registry a
15+
* *backfill*: it fills the gaps rather than shadowing the existing web.
16+
* Chosen as the default because silently redirecting a domain that
17+
* resolves perfectly well is indistinguishable from hijacking it.
18+
*
19+
* 'moshpit' — a name registered in Moshpit wins, even when clearnet has an
20+
* answer for it. This is the override: the point of registering
21+
* `profullstack.ai` in Moshpit is that your version is the one you get,
22+
* regardless of who holds the clearnet domain.
23+
*
24+
* Names under a TLD that clearnet has never heard of (`.eggs`, `.sploof`)
25+
* resolve through Moshpit in either mode — there is nothing to conflict with,
26+
* and refusing to resolve them would defeat the entire namespace.
27+
*/
28+
29+
export type ResolveMode = 'clearnet' | 'moshpit';
30+
31+
export const DEFAULT_RESOLVE_MODE: ResolveMode = 'clearnet';
32+
33+
/** The public registry. Overridable so a self-hosted pit can be pointed at. */
34+
export const DEFAULT_REGISTRY_BASE = 'https://pit.moshcode.sh';
35+
36+
export interface MoshpitLookup {
37+
/** The registry holds this name. */
38+
registered: boolean;
39+
/** Where it actually points once aliases are followed (`foo.agent`). */
40+
resolved: string;
41+
}
42+
43+
export interface ResolveInputs {
44+
hostname: string;
45+
mode: ResolveMode;
46+
/** Whether ordinary DNS has an answer. */
47+
clearnetResolves: boolean;
48+
/** Registry answer, or null when it was not consulted / was unreachable. */
49+
moshpit: MoshpitLookup | null;
50+
}
51+
52+
export interface ResolveDecision {
53+
/** Which namespace serves this navigation. */
54+
use: 'clearnet' | 'moshpit';
55+
/** Why — surfaced in the UI so an override never looks like a glitch. */
56+
reason: string;
57+
/** The name to fetch through the gateway. Only set when `use` is 'moshpit'. */
58+
resolved?: string;
59+
}
60+
61+
/**
62+
* Decide which namespace a hostname belongs to.
63+
*
64+
* Deliberately pure and total: every branch returns a decision with a reason,
65+
* so the caller never has to invent behaviour for an unhandled combination, and
66+
* the whole policy is testable without a network or a browser.
67+
*/
68+
export function decideResolution(inputs: ResolveInputs): ResolveDecision {
69+
const { mode, clearnetResolves, moshpit } = inputs;
70+
71+
// The registry could not be reached, or was never asked. Falling back to
72+
// clearnet is the only safe move: a registry outage must not take the
73+
// ordinary web down with it.
74+
if (!moshpit || !moshpit.registered) {
75+
return {
76+
use: 'clearnet',
77+
reason: moshpit ? 'not registered in Moshpit' : 'Moshpit registry not consulted or unreachable',
78+
};
79+
}
80+
81+
if (mode === 'moshpit') {
82+
return {
83+
use: 'moshpit',
84+
reason: clearnetResolves
85+
? 'registered in Moshpit — overriding the clearnet domain'
86+
: 'registered in Moshpit',
87+
resolved: moshpit.resolved,
88+
};
89+
}
90+
91+
// clearnet mode: the registry only fills gaps.
92+
if (clearnetResolves) {
93+
return { use: 'clearnet', reason: 'clearnet answers for this name (Moshpit set to backfill only)' };
94+
}
95+
return {
96+
use: 'moshpit',
97+
reason: 'clearnet has no answer — resolved through Moshpit',
98+
resolved: moshpit.resolved,
99+
};
100+
}
101+
102+
/**
103+
* Split a hostname the way the registry does: exactly one label and one TLD.
104+
* Anything else (`a.b.c`, a bare `localhost`, an IP) is not a Moshpit name and
105+
* must never be sent to the registry as if it were.
106+
*/
107+
export function parseRegistryName(hostname: string): { label: string; tld: string } | null {
108+
const host = hostname.trim().toLowerCase().replace(/\.$/, '');
109+
if (!host || host.includes(':')) return null;
110+
// An IPv4 literal is not a name.
111+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return null;
112+
const parts = host.split('.');
113+
if (parts.length !== 2) return null;
114+
const [label, tld] = parts;
115+
const LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
116+
if (!LABEL.test(label) || !LABEL.test(tld)) return null;
117+
return { label, tld };
118+
}
119+
120+
/** The URL that serves a resolved Moshpit name through the gateway. */
121+
export function gatewayUrlFor(resolved: string, registryBase = DEFAULT_REGISTRY_BASE): string {
122+
return `${registryBase.replace(/\/+$/, '')}/n/${encodeURIComponent(resolved)}`;
123+
}
124+
125+
/**
126+
* Ask the registry about a name.
127+
*
128+
* Any failure returns null rather than throwing: resolution sits in front of
129+
* every navigation, so a registry that is slow, down, or serving nonsense must
130+
* degrade to "clearnet as usual" instead of breaking browsing.
131+
*/
132+
export async function lookupMoshpit(
133+
hostname: string,
134+
options: { registryBase?: string; fetchImpl?: typeof fetch; timeoutMs?: number } = {},
135+
): Promise<MoshpitLookup | null> {
136+
const parsed = parseRegistryName(hostname);
137+
if (!parsed) return null;
138+
139+
const base = (options.registryBase ?? DEFAULT_REGISTRY_BASE).replace(/\/+$/, '');
140+
const fetchImpl = options.fetchImpl ?? fetch;
141+
const controller = new AbortController();
142+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 4000);
143+
try {
144+
const url = `${base}/api/moshpit/resolve?name=${encodeURIComponent(`${parsed.label}.${parsed.tld}`)}`;
145+
const res = await fetchImpl(url, { signal: controller.signal });
146+
if (!res.ok) return null;
147+
const json = (await res.json()) as { registered?: boolean; resolved?: string; name?: string };
148+
if (typeof json?.registered !== 'boolean') return null;
149+
return {
150+
registered: json.registered,
151+
resolved: typeof json.resolved === 'string' ? json.resolved : `${parsed.label}.${parsed.tld}`,
152+
};
153+
} catch {
154+
return null;
155+
} finally {
156+
clearTimeout(timer);
157+
}
158+
}

0 commit comments

Comments
 (0)