From 36589e97091cbef362991f80aa416971ea86ae23 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Fri, 31 Jul 2026 02:50:29 +0000 Subject: [PATCH] feat(resolve): choose whether Moshpit overrides clearnet for a contested name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../extensions/ai-sidebar/options.html | 19 +++ apps/desktop/extensions/ai-sidebar/options.js | 20 +++ apps/desktop/src/moshpit-resolve.test.ts | 160 ++++++++++++++++++ apps/desktop/src/moshpit-resolve.ts | 158 +++++++++++++++++ 4 files changed, 357 insertions(+) create mode 100644 apps/desktop/src/moshpit-resolve.test.ts create mode 100644 apps/desktop/src/moshpit-resolve.ts diff --git a/apps/desktop/extensions/ai-sidebar/options.html b/apps/desktop/extensions/ai-sidebar/options.html index 7281e0c..921e15d 100644 --- a/apps/desktop/extensions/ai-sidebar/options.html +++ b/apps/desktop/extensions/ai-sidebar/options.html @@ -77,6 +77,25 @@

Account

+

Name resolution

+

+ Moshpit names (fuck.yeah, original.sploof) always resolve here — + clearnet has never heard of those endings. This setting only decides what happens when + both namespaces answer, e.g. profullstack.ai. +

+ + +

+ Overriding means a domain someone else holds on clearnet resolves to your Moshpit + registration instead. Left on the default, a domain that already works is never redirected. +

+ + +
+

AI providers (bring your own keys)

Add keys for as many providers as you like, then pick a default for the sidebar. Keys are stored on your account (encrypted end-to-end when a vault diff --git a/apps/desktop/extensions/ai-sidebar/options.js b/apps/desktop/extensions/ai-sidebar/options.js index 3ddb85c..8ffe2dc 100644 --- a/apps/desktop/extensions/ai-sidebar/options.js +++ b/apps/desktop/extensions/ai-sidebar/options.js @@ -366,6 +366,10 @@ async function loadAll() { buildProviders(provs, aiDefault); el("cpClient").value = coinpayConfig?.clientId || ""; el("syncUrl").value = syncConfig?.url || ""; + const moshpit = (await chrome.storage.local.get("moshpitConfig")).moshpitConfig || {}; + // Absent means never configured, which is the default — not "off". + el("moshpitMode").value = moshpit.mode === "moshpit" ? "moshpit" : "clearnet"; + el("moshpitRegistry").value = moshpit.registryBase || ""; await renderAccount(); await mountSections(); // Search / Markets / Sports / RSS feeds (shared module) await renderBtr(); @@ -383,6 +387,22 @@ el("syncUrl").addEventListener("change", async () => { syncConfig: { url: el("syncUrl").value.trim() }, }); }); +async function saveMoshpit() { + await chrome.storage.local.set({ + moshpitConfig: { + mode: el("moshpitMode").value === "moshpit" ? "moshpit" : "clearnet", + registryBase: el("moshpitRegistry").value.trim(), + }, + }); + flash( + "savedMoshpit", + el("moshpitMode").value === "moshpit" + ? "Moshpit will override clearnet for names it holds" + : "Clearnet wins; Moshpit fills gaps only", + ); +} +el("moshpitMode").addEventListener("change", saveMoshpit); +el("moshpitRegistry").addEventListener("change", saveMoshpit); async function get(k) { return (await chrome.storage.local.get(k))[k] || {}; } diff --git a/apps/desktop/src/moshpit-resolve.test.ts b/apps/desktop/src/moshpit-resolve.test.ts new file mode 100644 index 0000000..4fd34fc --- /dev/null +++ b/apps/desktop/src/moshpit-resolve.test.ts @@ -0,0 +1,160 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { + DEFAULT_RESOLVE_MODE, + decideResolution, + gatewayUrlFor, + lookupMoshpit, + parseRegistryName, + type MoshpitLookup, +} from './moshpit-resolve'; + +const registered = (resolved: string): MoshpitLookup => ({ registered: true, resolved }); +const unregistered: MoshpitLookup = { registered: false, resolved: '' }; + +describe('decideResolution — clearnet mode (the default)', () => { + it('defaults to clearnet', () => { + expect(DEFAULT_RESOLVE_MODE).toBe('clearnet'); + }); + + it('leaves a working clearnet domain alone even when Moshpit holds the name', () => { + // The squatting case, from the safe side: someone holds profullstack.ai on + // clearnet AND we hold it in Moshpit. Default must not hijack it — silently + // redirecting a domain that resolves is indistinguishable from a takeover. + const d = decideResolution({ + hostname: 'profullstack.ai', + mode: 'clearnet', + clearnetResolves: true, + moshpit: registered('profullstack.ai'), + }); + expect(d.use).toBe('clearnet'); + expect(d.reason).toMatch(/backfill/i); + }); + + it('backfills a name clearnet cannot answer', () => { + const d = decideResolution({ + hostname: 'original.sploof', + mode: 'clearnet', + clearnetResolves: false, + moshpit: registered('original.sploof'), + }); + expect(d.use).toBe('moshpit'); + expect(d.resolved).toBe('original.sploof'); + }); +}); + +describe('decideResolution — moshpit mode (the override)', () => { + it('overrides a live clearnet domain', () => { + // The whole point of registering profullstack.ai in Moshpit: your version + // wins regardless of who holds the clearnet domain. + const d = decideResolution({ + hostname: 'profullstack.ai', + mode: 'moshpit', + clearnetResolves: true, + moshpit: registered('profullstack.ai'), + }); + expect(d.use).toBe('moshpit'); + expect(d.reason).toMatch(/overriding the clearnet domain/i); + expect(d.resolved).toBe('profullstack.ai'); + }); + + it('follows an alias to its target', () => { + const d = decideResolution({ + hostname: 'profullstack.agentic', + mode: 'moshpit', + clearnetResolves: false, + moshpit: registered('profullstack.agent'), + }); + expect(d.resolved).toBe('profullstack.agent'); + }); + + it('still falls through to clearnet for a name Moshpit does not hold', () => { + const d = decideResolution({ + hostname: 'example.com', + mode: 'moshpit', + clearnetResolves: true, + moshpit: unregistered, + }); + expect(d.use).toBe('clearnet'); + }); +}); + +describe('decideResolution — a registry outage must not break browsing', () => { + it.each(['clearnet', 'moshpit'] as const)('falls back to clearnet in %s mode', (mode) => { + const d = decideResolution({ + hostname: 'profullstack.ai', + mode, + clearnetResolves: true, + moshpit: null, + }); + expect(d.use).toBe('clearnet'); + expect(d.reason).toMatch(/unreachable|not consulted/i); + }); + + it('always explains itself', () => { + // Every branch carries a reason, so an override never looks like a glitch. + for (const mode of ['clearnet', 'moshpit'] as const) { + for (const clearnetResolves of [true, false]) { + for (const moshpit of [null, unregistered, registered('x.y')]) { + const d = decideResolution({ hostname: 'x.y', mode, clearnetResolves, moshpit }); + expect(d.reason.length).toBeGreaterThan(0); + } + } + } + }); +}); + +describe('parseRegistryName', () => { + it('accepts exactly one label and one TLD', () => { + expect(parseRegistryName('fuck.yeah')).toEqual({ label: 'fuck', tld: 'yeah' }); + expect(parseRegistryName('California.Oranges')).toEqual({ label: 'california', tld: 'oranges' }); + expect(parseRegistryName('original.sploof.')).toEqual({ label: 'original', tld: 'sploof' }); + }); + + it('rejects anything that is not a registry name', () => { + // Sending these to the registry would be asking about a name that cannot + // exist — and acting on the answer would misroute ordinary browsing. + expect(parseRegistryName('a.b.c')).toBeNull(); + expect(parseRegistryName('localhost')).toBeNull(); + expect(parseRegistryName('192.168.1.1')).toBeNull(); + expect(parseRegistryName('box.example.com:9161')).toBeNull(); + expect(parseRegistryName('')).toBeNull(); + expect(parseRegistryName('-bad.yeah')).toBeNull(); + }); +}); + +describe('lookupMoshpit', () => { + const okFetch = (body: unknown): typeof fetch => + vi.fn(async () => ({ ok: true, json: async () => body })) as unknown as typeof fetch; + + it('reads the registry answer', async () => { + const result = await lookupMoshpit('profullstack.agentic', { + fetchImpl: okFetch({ name: 'profullstack.agentic', resolved: 'profullstack.agent', registered: true }), + }); + expect(result).toEqual({ registered: true, resolved: 'profullstack.agent' }); + }); + + it('never asks about a name the registry could not hold', async () => { + const fetchImpl = okFetch({ registered: true, resolved: 'x' }); + expect(await lookupMoshpit('a.b.c', { fetchImpl })).toBeNull(); + expect(fetchImpl).not.toHaveBeenCalled(); + }); + + it('returns null rather than throwing when the registry is down', async () => { + const dead = vi.fn(async () => { + throw new Error('ECONNREFUSED'); + }) as unknown as typeof fetch; + expect(await lookupMoshpit('fuck.yeah', { fetchImpl: dead })).toBeNull(); + }); + + it('returns null on a nonsense payload', async () => { + expect(await lookupMoshpit('fuck.yeah', { fetchImpl: okFetch({ nope: 1 }) })).toBeNull(); + }); +}); + +describe('gatewayUrlFor', () => { + it('builds a gateway URL and tolerates a trailing slash', () => { + expect(gatewayUrlFor('fuck.yeah')).toBe('https://pit.moshcode.sh/n/fuck.yeah'); + expect(gatewayUrlFor('fuck.yeah', 'https://my.pit/')).toBe('https://my.pit/n/fuck.yeah'); + }); +}); diff --git a/apps/desktop/src/moshpit-resolve.ts b/apps/desktop/src/moshpit-resolve.ts new file mode 100644 index 0000000..ef48a00 --- /dev/null +++ b/apps/desktop/src/moshpit-resolve.ts @@ -0,0 +1,158 @@ +/** + * Moshpit name resolution, and how it coexists with clearnet DNS. + * + * 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 decision cannot be hardcoded: a user who has never heard of Moshpit must + * keep getting clearnet, while an operator who registered the name in Moshpit + * 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, which makes the registry a + * *backfill*: it fills the gaps rather than shadowing the existing web. + * Chosen as the default because silently redirecting a domain that + * resolves perfectly well is indistinguishable from hijacking it. + * + * 'moshpit' — a name registered in Moshpit wins, even when clearnet has an + * answer for it. This is the override: the point of registering + * `profullstack.ai` in Moshpit is that your version is the one you get, + * regardless of who holds the clearnet domain. + * + * Names under a TLD that clearnet has never heard of (`.eggs`, `.sploof`) + * resolve through Moshpit in either mode — there is nothing to conflict with, + * and refusing to resolve them would defeat the entire namespace. + */ + +export type ResolveMode = 'clearnet' | 'moshpit'; + +export const DEFAULT_RESOLVE_MODE: ResolveMode = 'clearnet'; + +/** The public registry. Overridable so a self-hosted pit can be pointed at. */ +export const DEFAULT_REGISTRY_BASE = 'https://pit.moshcode.sh'; + +export interface MoshpitLookup { + /** The registry holds this name. */ + registered: boolean; + /** Where it actually points once aliases are followed (`foo.agent`). */ + resolved: string; +} + +export interface ResolveInputs { + hostname: string; + mode: ResolveMode; + /** Whether ordinary DNS has an answer. */ + clearnetResolves: boolean; + /** Registry answer, or null when it was not consulted / was unreachable. */ + moshpit: MoshpitLookup | null; +} + +export interface ResolveDecision { + /** Which namespace serves this navigation. */ + use: 'clearnet' | 'moshpit'; + /** Why — surfaced in the UI so an override never looks like a glitch. */ + reason: string; + /** The name to fetch through the gateway. Only set when `use` is 'moshpit'. */ + resolved?: string; +} + +/** + * Decide which namespace a hostname belongs to. + * + * Deliberately pure and total: every branch returns a decision with a reason, + * so the caller never has to invent behaviour for an unhandled combination, and + * the whole policy is testable without a network or a browser. + */ +export function decideResolution(inputs: ResolveInputs): ResolveDecision { + const { mode, clearnetResolves, moshpit } = inputs; + + // The registry could not be reached, or was never asked. Falling back to + // clearnet is the only safe move: a registry outage must not take the + // ordinary web down with it. + if (!moshpit || !moshpit.registered) { + return { + use: 'clearnet', + reason: moshpit ? 'not registered in Moshpit' : 'Moshpit registry not consulted or unreachable', + }; + } + + if (mode === 'moshpit') { + return { + use: 'moshpit', + reason: clearnetResolves + ? 'registered in Moshpit — overriding the clearnet domain' + : 'registered in Moshpit', + resolved: moshpit.resolved, + }; + } + + // clearnet mode: the registry only fills gaps. + if (clearnetResolves) { + return { use: 'clearnet', reason: 'clearnet answers for this name (Moshpit set to backfill only)' }; + } + return { + use: 'moshpit', + reason: 'clearnet has no answer — resolved through Moshpit', + resolved: moshpit.resolved, + }; +} + +/** + * Split a hostname the way the registry does: exactly one label and one TLD. + * Anything else (`a.b.c`, a bare `localhost`, an IP) is not a Moshpit name and + * must never be sent to the registry as if it were. + */ +export function parseRegistryName(hostname: string): { label: string; tld: string } | null { + const host = hostname.trim().toLowerCase().replace(/\.$/, ''); + if (!host || host.includes(':')) return null; + // An IPv4 literal is not a name. + if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return null; + const parts = host.split('.'); + if (parts.length !== 2) return null; + const [label, tld] = parts; + const LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/; + if (!LABEL.test(label) || !LABEL.test(tld)) return null; + return { label, tld }; +} + +/** The URL that serves a resolved Moshpit name through the gateway. */ +export function gatewayUrlFor(resolved: string, registryBase = DEFAULT_REGISTRY_BASE): string { + return `${registryBase.replace(/\/+$/, '')}/n/${encodeURIComponent(resolved)}`; +} + +/** + * Ask the registry about a name. + * + * Any failure returns null rather than throwing: resolution sits in front of + * every navigation, so a registry that is slow, down, or serving nonsense must + * degrade to "clearnet as usual" instead of breaking browsing. + */ +export async function lookupMoshpit( + hostname: string, + options: { registryBase?: string; fetchImpl?: typeof fetch; timeoutMs?: number } = {}, +): Promise { + const parsed = parseRegistryName(hostname); + if (!parsed) return null; + + const base = (options.registryBase ?? DEFAULT_REGISTRY_BASE).replace(/\/+$/, ''); + const fetchImpl = options.fetchImpl ?? fetch; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 4000); + try { + const url = `${base}/api/moshpit/resolve?name=${encodeURIComponent(`${parsed.label}.${parsed.tld}`)}`; + const res = await fetchImpl(url, { signal: controller.signal }); + if (!res.ok) return null; + const json = (await res.json()) as { registered?: boolean; resolved?: string; name?: string }; + if (typeof json?.registered !== 'boolean') return null; + return { + registered: json.registered, + resolved: typeof json.resolved === 'string' ? json.resolved : `${parsed.label}.${parsed.tld}`, + }; + } catch { + return null; + } finally { + clearTimeout(timer); + } +}