From cf39c5633d658a23ca00e06e83881753fb7917e5 Mon Sep 17 00:00:00 2001
From: Anthony Ettinger
Date: Fri, 31 Jul 2026 04:35:37 +0000
Subject: [PATCH 1/2] feat(moshpit): actually resolve Moshpit names in the
browser, and park unpointed ones
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The Moshpit settings on the options page were written to chrome.storage and
never read by anything. The resolution policy lived in apps/desktop/src as
TypeScript that the extension cannot import and that had no callers. So
setting a registry did nothing and `california.oranges` died on
ERR_NAME_NOT_RESOLVED — the feature was UI and dead code, end to end.
- moshpit.js: a port of the policy into the extension (plain JS, no build
step). moshpit-resolve.ts stays the reference implementation; a test runs
BOTH over the same input space and requires identical answers, because a
port that silently drifts is worse than no port.
- background.js: two webNavigation hooks, because "does clearnet answer?" is
only knowable at two moments. onErrorOccurred (DNS came up empty) is the
backfill path and the only one active in the default mode, so nothing that
already works is touched. onBeforeNavigate runs only in 'moshpit' mode,
where a registered name is meant to beat clearnet.
- Parking: a name in the namespace should not dead-end on a DNS error.
Unclaimed, or claimed but not pointed at an address yet, now lands on
moshcoding.com/parking?name=… — but only where clearnet has nothing, so a
working domain is never replaced by a parking page. A registry that was
unreachable still degrades to clearnet rather than lying with a parking
page for a name it merely failed to look up.
- Manifest: webNavigation permission, and a module service worker for the
import.
No redirect loops: every destination has three labels, so parseRegistryName
rejects it on the way back through.
Co-Authored-By: Claude Opus 5 (1M context)
---
.../extensions/ai-sidebar/background.js | 67 ++++++
.../extensions/ai-sidebar/manifest.json | 4 +-
apps/desktop/extensions/ai-sidebar/moshpit.js | 190 ++++++++++++++++++
.../extensions/ai-sidebar/moshpit.test.js | 145 +++++++++++++
.../extensions/ai-sidebar/options.html | 6 +
apps/desktop/src/moshpit-resolve.test.ts | 56 ++++++
apps/desktop/src/moshpit-resolve.ts | 85 ++++++--
7 files changed, 533 insertions(+), 20 deletions(-)
create mode 100644 apps/desktop/extensions/ai-sidebar/moshpit.js
create mode 100644 apps/desktop/extensions/ai-sidebar/moshpit.test.js
diff --git a/apps/desktop/extensions/ai-sidebar/background.js b/apps/desktop/extensions/ai-sidebar/background.js
index e0f4992..9f61814 100644
--- a/apps/desktop/extensions/ai-sidebar/background.js
+++ b/apps/desktop/extensions/ai-sidebar/background.js
@@ -1,3 +1,5 @@
+import { destinationFor, moshpitConfig, parseRegistryName } from './moshpit.js';
+
// Open the AI side panel when the toolbar action is clicked.
chrome.sidePanel
.setPanelBehavior({ openPanelOnActionClick: true })
@@ -288,3 +290,68 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
}
} catch (_) { /* best effort */ }
})();
+
+// --- Moshpit name resolution ---------------------------------------------
+// This is what makes the Moshpit settings on the options page actually do
+// something: until now they were written to storage and never read.
+//
+// Two hooks, because "does clearnet answer for this name?" is only knowable at
+// two different moments:
+//
+// onErrorOccurred — DNS came up empty (ERR_NAME_NOT_RESOLVED). This is the
+// backfill path, and the ONLY one active in the default 'clearnet' mode, so
+// someone who has never heard of Moshpit gets ordinary browsing plus a
+// rescued error page. Nothing that already works is touched.
+//
+// onBeforeNavigate — consulted ONLY in 'moshpit' mode, where a registered
+// name is meant to win even though clearnet has an answer. It costs a
+// registry round-trip before navigation, which is why the default mode
+// never goes near it.
+//
+// No redirect loop: every destination we send a tab to (pit.moshcode.sh/n/…,
+// app.moshcode.sh/pit) has three labels, so parseRegistryName rejects it and
+// the hooks ignore it on the way back through.
+
+const DNS_FAILED = new Set([
+ 'net::ERR_NAME_NOT_RESOLVED',
+ 'net::ERR_NAME_RESOLUTION_FAILED',
+]);
+
+function moshpitHostname(url) {
+ try {
+ const u = new URL(url);
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';
+ return parseRegistryName(u.hostname) ? u.hostname : '';
+ } catch {
+ return '';
+ }
+}
+
+async function sendTabTo(tabId, url) {
+ try {
+ await chrome.tabs.update(tabId, { url });
+ } catch (err) {
+ console.warn('moshpit redirect:', err);
+ }
+}
+
+chrome.webNavigation?.onErrorOccurred.addListener(async (details) => {
+ if (details.frameId !== 0) return; // top-level navigations only
+ if (!DNS_FAILED.has(details.error)) return;
+ const hostname = moshpitHostname(details.url);
+ if (!hostname) return;
+ const dest = await destinationFor(hostname, false);
+ if (dest) await sendTabTo(details.tabId, dest);
+});
+
+chrome.webNavigation?.onBeforeNavigate.addListener(async (details) => {
+ if (details.frameId !== 0) return;
+ const hostname = moshpitHostname(details.url);
+ if (!hostname) return;
+ // The default mode must never pre-empt a working clearnet domain — bail out
+ // before the registry is ever contacted.
+ const { mode } = await moshpitConfig();
+ if (mode !== 'moshpit') return;
+ const dest = await destinationFor(hostname, true);
+ if (dest) await sendTabTo(details.tabId, dest);
+});
diff --git a/apps/desktop/extensions/ai-sidebar/manifest.json b/apps/desktop/extensions/ai-sidebar/manifest.json
index f7225f5..cea39a9 100644
--- a/apps/desktop/extensions/ai-sidebar/manifest.json
+++ b/apps/desktop/extensions/ai-sidebar/manifest.json
@@ -15,6 +15,7 @@
"tabs",
"activeTab",
"scripting",
+ "webNavigation",
"proxy",
"privacy",
"notifications"
@@ -35,7 +36,8 @@
"https://*/*"
],
"background": {
- "service_worker": "background.js"
+ "service_worker": "background.js",
+ "type": "module"
},
"content_scripts": [
{
diff --git a/apps/desktop/extensions/ai-sidebar/moshpit.js b/apps/desktop/extensions/ai-sidebar/moshpit.js
new file mode 100644
index 0000000..ce497e8
--- /dev/null
+++ b/apps/desktop/extensions/ai-sidebar/moshpit.js
@@ -0,0 +1,190 @@
+// Moshpit name resolution for the extension.
+//
+// The policy here is a straight port of apps/desktop/src/moshpit-resolve.ts —
+// same names, same semantics — because the extension is plain JS with no build
+// step and cannot import the launcher's TypeScript. The TS module stays the
+// reference implementation and keeps the exhaustive unit tests; this file is
+// what actually runs in the browser. Keep them in sync.
+//
+// See that module's header for why 'clearnet' is the default: silently
+// redirecting a domain that resolves perfectly well is indistinguishable from
+// hijacking it.
+
+export const DEFAULT_REGISTRY_BASE = 'https://pit.moshcode.sh';
+export const DEFAULT_CONSOLE_BASE = 'https://app.moshcode.sh';
+
+// The one label meaning "manage this namespace" rather than "visit a name".
+// Reserved, not claimable — otherwise whoever holds `.eggs` could register
+// `mosh.eggs` and own the page people use to check who holds `.eggs`.
+export const CONSOLE_LABEL = 'mosh';
+
+// Where a name with no destination yet is parked. A name inside the namespace
+// should never dead-end on ERR_NAME_NOT_RESOLVED.
+export const DEFAULT_PARKING_BASE = 'https://moshcoding.com';
+
+/** The parking page for a name with no destination yet. */
+export function parkingUrlFor(name, parkingBase = DEFAULT_PARKING_BASE) {
+ return `${parkingBase.replace(/\/+$/, '')}/parking?name=${encodeURIComponent(name)}`;
+}
+
+/** Read the settings the options page writes. */
+export async function moshpitConfig() {
+ const { moshpitConfig: cfg } = await chrome.storage.local.get('moshpitConfig');
+ return {
+ mode: cfg?.mode === 'moshpit' ? 'moshpit' : 'clearnet',
+ registryBase: (cfg?.registryBase || DEFAULT_REGISTRY_BASE).replace(/\/+$/, ''),
+ consoleBase: (cfg?.consoleBase || DEFAULT_CONSOLE_BASE).replace(/\/+$/, ''),
+ parkingBase: (cfg?.parkingBase || DEFAULT_PARKING_BASE).replace(/\/+$/, ''),
+ };
+}
+
+/**
+ * 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) {
+ const host = String(hostname || '').trim().toLowerCase().replace(/\.$/, '');
+ if (!host || host.includes(':')) return null;
+ 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 Pit URL for a `mosh.` hostname, or null when it isn't one. */
+export function consoleUrlFor(hostname, consoleBase = DEFAULT_CONSOLE_BASE) {
+ const parsed = parseRegistryName(hostname);
+ if (!parsed || parsed.label !== CONSOLE_LABEL) return null;
+ return `${consoleBase.replace(/\/+$/, '')}/pit?tld=${encodeURIComponent(parsed.tld)}`;
+}
+
+/** The URL that serves a resolved Moshpit name through the gateway. */
+export function gatewayUrlFor(resolved, registryBase = DEFAULT_REGISTRY_BASE) {
+ 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".
+ */
+export async function lookupMoshpit(hostname, { registryBase, timeoutMs = 4000 } = {}) {
+ const parsed = parseRegistryName(hostname);
+ if (!parsed) return null;
+ const base = (registryBase || DEFAULT_REGISTRY_BASE).replace(/\/+$/, '');
+ const controller = new AbortController();
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
+ try {
+ const name = `${parsed.label}.${parsed.tld}`;
+ const res = await fetch(`${base}/api/moshpit/resolve?name=${encodeURIComponent(name)}`, {
+ signal: controller.signal,
+ });
+ if (!res.ok) return null;
+ const json = await res.json();
+ if (typeof json?.registered !== 'boolean') return null;
+ return {
+ registered: json.registered,
+ resolved: typeof json.resolved === 'string' ? json.resolved : name,
+ };
+ } catch {
+ return null;
+ } finally {
+ clearTimeout(timer);
+ }
+}
+
+/**
+ * Decide which namespace a hostname belongs to. Pure and total: every branch
+ * returns a decision with a reason, so the caller never has to invent
+ * behaviour for an unhandled combination.
+ */
+export function decideResolution({ hostname, mode, clearnetResolves, moshpit, consoleBase, parkingBase }) {
+ // `mosh.` is the registration console for `.`, not a name to fetch.
+ // It obeys the SAME precedence as any other Moshpit answer rather than taking
+ // an exemption — `mosh.org` and `mosh.com` are real clearnet domains.
+ const consoleUrl = consoleUrlFor(hostname, consoleBase);
+ if (consoleUrl) {
+ if (mode === 'clearnet' && clearnetResolves) {
+ return { use: 'clearnet', reason: 'clearnet answers for this name (Moshpit set to backfill only)' };
+ }
+ const tld = parseRegistryName(hostname)?.tld;
+ return {
+ use: 'register',
+ reason: `${CONSOLE_LABEL}.${tld} is the registration console for .${tld}`,
+ url: consoleUrl,
+ };
+ }
+
+ // A registry outage must not take the ordinary web down with it — nor lie
+ // with a parking page for a name it simply failed to look up.
+ if (!moshpit) {
+ return { use: 'clearnet', reason: 'Moshpit registry not consulted or unreachable' };
+ }
+
+ // Claimed AND pointed somewhere — the precedence rules apply to it.
+ if (moshpit.registered && moshpit.resolved) {
+ if (mode === 'moshpit') {
+ return {
+ use: 'moshpit',
+ reason: clearnetResolves
+ ? 'registered in Moshpit — overriding the clearnet domain'
+ : 'registered in Moshpit',
+ resolved: moshpit.resolved,
+ };
+ }
+ 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,
+ };
+ }
+
+ // Unclaimed, or claimed but not pointed at an address yet — park it, so
+ // `california.oranges` explains itself instead of looking broken. Only ever
+ // where clearnet has nothing.
+ if (!clearnetResolves && parseRegistryName(hostname)) {
+ return {
+ use: 'park',
+ reason: moshpit.registered
+ ? 'registered in Moshpit but not pointed at an address yet'
+ : 'unclaimed Moshpit name — parked',
+ url: parkingUrlFor(String(hostname).trim().toLowerCase().replace(/\.$/, ''), parkingBase),
+ };
+ }
+ if (clearnetResolves) {
+ return {
+ use: 'clearnet',
+ reason: moshpit.registered
+ ? 'registered in Moshpit but not pointed anywhere yet'
+ : 'not registered in Moshpit',
+ };
+ }
+ return { use: 'clearnet', reason: 'not a Moshpit name' };
+}
+
+/**
+ * The whole policy for one navigation, as a URL to send the tab to (or null to
+ * leave it alone). `clearnetResolves` is supplied by the caller because only it
+ * knows whether DNS actually answered.
+ */
+export async function destinationFor(hostname, clearnetResolves) {
+ if (!parseRegistryName(hostname)) return null;
+ const { mode, registryBase, consoleBase, parkingBase } = await moshpitConfig();
+
+ // Skip the registry round-trip entirely for the console label — it is
+ // reserved, so no lookup can change the answer.
+ const isConsole = !!consoleUrlFor(hostname, consoleBase);
+ const moshpit = isConsole ? null : await lookupMoshpit(hostname, { registryBase });
+
+ const decision = decideResolution({ hostname, mode, clearnetResolves, moshpit, consoleBase, parkingBase });
+ if (decision.use === 'register' || decision.use === 'park') return decision.url;
+ if (decision.use === 'moshpit') return gatewayUrlFor(decision.resolved, registryBase);
+ return null;
+}
diff --git a/apps/desktop/extensions/ai-sidebar/moshpit.test.js b/apps/desktop/extensions/ai-sidebar/moshpit.test.js
new file mode 100644
index 0000000..73301ef
--- /dev/null
+++ b/apps/desktop/extensions/ai-sidebar/moshpit.test.js
@@ -0,0 +1,145 @@
+// The extension's moshpit.js is a hand port of ../../src/moshpit-resolve.ts,
+// because the extension is plain JS with no build step. A port that silently
+// drifts from its reference is worse than no port at all — it makes the browser
+// behave differently from the module everyone reads and tests. So these tests
+// run BOTH implementations over the same inputs and require identical answers.
+
+import { describe, expect, it } from 'vitest';
+
+import * as ts from '../../src/moshpit-resolve';
+import * as js from './moshpit.js';
+
+const HOSTNAMES = [
+ 'mosh.eggs',
+ 'MOSH.Whatever',
+ 'mosh.org',
+ 'moshy.eggs',
+ 'eggs.mosh',
+ 'fuck.yeah',
+ 'profullstack.ai',
+ 'a.b.c',
+ 'localhost',
+ '127.0.0.1',
+ 'mosh',
+ '',
+ 'x.y',
+];
+
+describe('moshpit.js is faithful to moshpit-resolve.ts', () => {
+ it('parseRegistryName agrees on every hostname shape', () => {
+ for (const h of HOSTNAMES) {
+ expect(js.parseRegistryName(h), h).toEqual(ts.parseRegistryName(h));
+ }
+ });
+
+ it('consoleUrlFor agrees on every hostname shape', () => {
+ for (const h of HOSTNAMES) {
+ expect(js.consoleUrlFor(h), h).toEqual(ts.consoleUrlFor(h));
+ }
+ });
+
+ it('gatewayUrlFor agrees', () => {
+ expect(js.gatewayUrlFor('fuck.yeah')).toBe(ts.gatewayUrlFor('fuck.yeah'));
+ expect(js.gatewayUrlFor('fuck.yeah', 'https://my.pit/')).toBe(
+ ts.gatewayUrlFor('fuck.yeah', 'https://my.pit/'),
+ );
+ });
+
+ it('decideResolution agrees across the whole input space', () => {
+ const lookups = [null, { registered: false, resolved: '' }, { registered: true, resolved: 'r.eggs' }];
+ for (const hostname of HOSTNAMES) {
+ for (const mode of ['clearnet', 'moshpit']) {
+ for (const clearnetResolves of [true, false]) {
+ for (const moshpit of lookups) {
+ const inputs = { hostname, mode, clearnetResolves, moshpit };
+ expect(js.decideResolution(inputs), JSON.stringify(inputs)).toEqual(
+ ts.decideResolution(inputs),
+ );
+ }
+ }
+ }
+ }
+ });
+
+ it('shares the same constants', () => {
+ expect(js.CONSOLE_LABEL).toBe(ts.CONSOLE_LABEL);
+ expect(js.DEFAULT_CONSOLE_BASE).toBe(ts.DEFAULT_CONSOLE_BASE);
+ expect(js.DEFAULT_REGISTRY_BASE).toBe(ts.DEFAULT_REGISTRY_BASE);
+ });
+});
+
+describe('destinationFor — the URL a navigation actually ends up at', () => {
+ const withConfig = (moshpitConfig) => {
+ globalThis.chrome = {
+ storage: { local: { get: async () => ({ moshpitConfig }) } },
+ };
+ };
+
+ it('sends mosh. to the Pit without ever contacting the registry', async () => {
+ withConfig({ mode: 'clearnet' });
+ globalThis.fetch = () => {
+ throw new Error('registry must not be consulted for the reserved label');
+ };
+ expect(await js.destinationFor('mosh.eggs', false)).toBe(
+ 'https://app.moshcode.sh/pit?tld=eggs',
+ );
+ });
+
+ it('leaves a working clearnet domain alone in the default mode', async () => {
+ withConfig({ mode: 'clearnet' });
+ expect(await js.destinationFor('mosh.org', true)).toBeNull();
+ });
+
+ it('ignores anything that is not a single label + TLD', async () => {
+ withConfig({ mode: 'moshpit' });
+ expect(await js.destinationFor('a.b.c', false)).toBeNull();
+ expect(await js.destinationFor('pit.moshcode.sh', false)).toBeNull(); // no redirect loop
+ expect(await js.destinationFor('app.moshcode.sh', false)).toBeNull();
+ });
+
+ it('routes a registered name through the gateway when DNS came up empty', async () => {
+ withConfig({ mode: 'clearnet' });
+ globalThis.fetch = async () => ({
+ ok: true,
+ json: async () => ({ registered: true, resolved: 'original.sploof' }),
+ });
+ expect(await js.destinationFor('alias.sploof', false)).toBe(
+ 'https://pit.moshcode.sh/n/original.sploof',
+ );
+ });
+
+ it('degrades to clearnet when the registry is unreachable', async () => {
+ withConfig({ mode: 'moshpit' });
+ globalThis.fetch = async () => {
+ throw new Error('ECONNREFUSED');
+ };
+ expect(await js.destinationFor('fuck.yeah', true)).toBeNull();
+ });
+
+ it('honours a self-hosted registry base', async () => {
+ withConfig({ mode: 'clearnet', registryBase: 'https://my.pit/' });
+ let asked = '';
+ globalThis.fetch = async (url) => {
+ asked = url;
+ return { ok: true, json: async () => ({ registered: true, resolved: 'a.eggs' }) };
+ };
+ expect(await js.destinationFor('a.eggs', false)).toBe('https://my.pit/n/a.eggs');
+ expect(asked).toContain('https://my.pit/api/moshpit/resolve');
+ });
+});
+
+describe('destinationFor — parking', () => {
+ it('sends an unclaimed name to the parking page', async () => {
+ globalThis.chrome = { storage: { local: { get: async () => ({ moshpitConfig: { mode: 'clearnet' } }) } } };
+ globalThis.fetch = async () => ({ ok: true, json: async () => ({ registered: false }) });
+ expect(await js.destinationFor('california.oranges', false)).toBe(
+ 'https://moshcoding.com/parking?name=california.oranges',
+ );
+ });
+
+ it('leaves a working clearnet domain alone rather than parking it', async () => {
+ globalThis.chrome = { storage: { local: { get: async () => ({ moshpitConfig: { mode: 'clearnet' } }) } } };
+ globalThis.fetch = async () => ({ ok: true, json: async () => ({ registered: false }) });
+ expect(await js.destinationFor('example.com', true)).toBeNull();
+ });
+});
diff --git a/apps/desktop/extensions/ai-sidebar/options.html b/apps/desktop/extensions/ai-sidebar/options.html
index 921e15d..d8d9c95 100644
--- a/apps/desktop/extensions/ai-sidebar/options.html
+++ b/apps/desktop/extensions/ai-sidebar/options.html
@@ -92,6 +92,12 @@
Name resolution
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.
+
+ To claim a namespace, type mosh. followed by the ending you want —
+ mosh.eggs opens the Pit for .eggs, where you register it and
+ every name.eggs under it. mosh.anything is reserved, so
+ nobody can register it and impersonate that page.
+
diff --git a/apps/desktop/src/moshpit-resolve.test.ts b/apps/desktop/src/moshpit-resolve.test.ts
index 3e3acca..b4f5b0e 100644
--- a/apps/desktop/src/moshpit-resolve.test.ts
+++ b/apps/desktop/src/moshpit-resolve.test.ts
@@ -234,3 +234,59 @@ describe('decideResolution — the registration console', () => {
expect(d.url).toBe('https://my.console/pit?tld=eggs');
});
});
+
+describe('decideResolution — parking unpointed names', () => {
+ it('parks an unclaimed name instead of dead-ending on a DNS error', () => {
+ const d = decideResolution({
+ hostname: 'california.oranges',
+ mode: 'clearnet',
+ clearnetResolves: false,
+ moshpit: unregistered,
+ });
+ expect(d.use).toBe('park');
+ expect(d.url).toBe('https://moshcoding.com/parking?name=california.oranges');
+ });
+
+ it('parks a claimed name that is not pointed at an address yet', () => {
+ const d = decideResolution({
+ hostname: 'california.oranges',
+ mode: 'moshpit',
+ clearnetResolves: false,
+ moshpit: { registered: true, resolved: '' },
+ });
+ expect(d.use).toBe('park');
+ });
+
+ it('never replaces a working clearnet domain with a parking page', () => {
+ for (const mode of ['clearnet', 'moshpit'] as const) {
+ const d = decideResolution({
+ hostname: 'example.com',
+ mode,
+ clearnetResolves: true,
+ moshpit: unregistered,
+ });
+ expect(d.use).toBe('clearnet');
+ }
+ });
+
+ it('does not park when the registry was unreachable — that would be a lie', () => {
+ const d = decideResolution({
+ hostname: 'california.oranges',
+ mode: 'clearnet',
+ clearnetResolves: false,
+ moshpit: null,
+ });
+ expect(d.use).toBe('clearnet');
+ });
+
+ it('honours an overridden parking base', () => {
+ const d = decideResolution({
+ hostname: 'california.oranges',
+ mode: 'clearnet',
+ clearnetResolves: false,
+ moshpit: unregistered,
+ parkingBase: 'https://my.park/',
+ });
+ expect(d.url).toBe('https://my.park/parking?name=california.oranges');
+ });
+});
diff --git a/apps/desktop/src/moshpit-resolve.ts b/apps/desktop/src/moshpit-resolve.ts
index 89232ce..c43077d 100644
--- a/apps/desktop/src/moshpit-resolve.ts
+++ b/apps/desktop/src/moshpit-resolve.ts
@@ -49,6 +49,24 @@ export const DEFAULT_CONSOLE_BASE = 'https://app.moshcode.sh';
*/
export const CONSOLE_LABEL = 'mosh';
+/**
+ * Where a name that has no destination yet is parked.
+ *
+ * A name inside the Moshpit namespace should never dead-end on a DNS error:
+ * `california.oranges` is a perfectly good name that simply hasn't been pointed
+ * at an IP, and "this name is unclaimed / unpointed, here's how to take it" is
+ * a far more useful answer than ERR_NAME_NOT_RESOLVED.
+ */
+export const DEFAULT_PARKING_BASE = 'https://moshcoding.com';
+
+/** The parking page for a name with no destination yet. */
+export function parkingUrlFor(
+ name: string,
+ parkingBase: string = DEFAULT_PARKING_BASE,
+): string {
+ return `${parkingBase.replace(/\/+$/, '')}/parking?name=${encodeURIComponent(name)}`;
+}
+
export interface MoshpitLookup {
/** The registry holds this name. */
registered: boolean;
@@ -65,11 +83,13 @@ export interface ResolveInputs {
moshpit: MoshpitLookup | null;
/** The Pit console. Overridable alongside a self-hosted registry. */
consoleBase?: string;
+ /** Where unpointed names are parked. */
+ parkingBase?: string;
}
export interface ResolveDecision {
/** Which namespace serves this navigation. */
- use: 'clearnet' | 'moshpit' | 'register';
+ use: 'clearnet' | 'moshpit' | 'register' | 'park';
/** 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'. */
@@ -103,7 +123,7 @@ export function consoleUrlFor(
* the whole policy is testable without a network or a browser.
*/
export function decideResolution(inputs: ResolveInputs): ResolveDecision {
- const { hostname, mode, clearnetResolves, moshpit, consoleBase } = inputs;
+ const { hostname, mode, clearnetResolves, moshpit, consoleBase, parkingBase } = inputs;
// `mosh.` is the registration console for `.`, not a name to fetch.
// It obeys the SAME precedence as any other Moshpit answer rather than
@@ -125,33 +145,60 @@ export function decideResolution(inputs: ResolveInputs): ResolveDecision {
// 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',
- };
+ // ordinary web down with it — nor lie with a parking page for a name it
+ // simply failed to look up.
+ if (!moshpit) {
+ return { use: 'clearnet', reason: 'Moshpit registry not consulted or unreachable' };
}
- if (mode === 'moshpit') {
+ // Claimed AND pointed somewhere — the precedence rules below apply to it.
+ if (moshpit.registered && moshpit.resolved) {
+ 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: clearnetResolves
- ? 'registered in Moshpit — overriding the clearnet domain'
- : 'registered in Moshpit',
+ reason: 'clearnet has no answer — resolved through Moshpit',
resolved: moshpit.resolved,
};
}
- // clearnet mode: the registry only fills gaps.
+ // Unclaimed, or claimed but not pointed at an address yet. A name in this
+ // namespace should not dead-end on ERR_NAME_NOT_RESOLVED — park it, so
+ // `california.oranges` explains itself instead of looking broken.
+ //
+ // Only ever where clearnet has nothing: a domain that already works is never
+ // replaced by a parking page, which is the same rule the override obeys.
+ if (!clearnetResolves && parseRegistryName(hostname)) {
+ return {
+ use: 'park',
+ reason: moshpit.registered
+ ? 'registered in Moshpit but not pointed at an address yet'
+ : 'unclaimed Moshpit name — parked',
+ url: parkingUrlFor(hostname.trim().toLowerCase().replace(/\.$/, ''), parkingBase),
+ };
+ }
if (clearnetResolves) {
- return { use: 'clearnet', reason: 'clearnet answers for this name (Moshpit set to backfill only)' };
+ return {
+ use: 'clearnet',
+ reason: moshpit.registered
+ ? 'registered in Moshpit but not pointed anywhere yet'
+ : 'not registered in Moshpit',
+ };
}
- return {
- use: 'moshpit',
- reason: 'clearnet has no answer — resolved through Moshpit',
- resolved: moshpit.resolved,
- };
+ // Not a registry-shaped name (`a.b.c`, an IP, a bare host). Nothing in the
+ // Moshpit namespace can speak for it, parking included.
+ return { use: 'clearnet', reason: 'not a Moshpit name' };
}
/**
From c3de9d5eef3425cf24586d6b762fa080c00ac5ce Mon Sep 17 00:00:00 2001
From: Anthony Ettinger
Date: Fri, 31 Jul 2026 04:40:40 +0000
Subject: [PATCH 2/2] fix(moshpit): park on a null target, not a truthy
resolved
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Probing the live registry showed the payload is richer than the code assumed:
california.oranges -> {registered:true, name_registered:true, target:null,
resolved:'california.oranges', mode, prefer, aliased}
`resolved` echoes the name whether or not it points anywhere, so testing it
marked every claimed name as live — nothing would have parked, and the exact
case this was built for would have gone to the gateway. `target` is the
address, and null is what 'not pointed at an IP yet' actually looks like.
Also read name_registered (is THIS name claimed) in preference to registered
(is the TLD claimed), falling back for older registry builds.
Covered by a test using the verbatim california.oranges response.
Co-Authored-By: Claude Opus 5 (1M context)
---
apps/desktop/extensions/ai-sidebar/moshpit.js | 11 ++--
.../extensions/ai-sidebar/moshpit.test.js | 53 +++++++++++++++++--
apps/desktop/src/moshpit-resolve.test.ts | 25 +++++++--
apps/desktop/src/moshpit-resolve.ts | 26 +++++++--
4 files changed, 99 insertions(+), 16 deletions(-)
diff --git a/apps/desktop/extensions/ai-sidebar/moshpit.js b/apps/desktop/extensions/ai-sidebar/moshpit.js
index ce497e8..6532cf7 100644
--- a/apps/desktop/extensions/ai-sidebar/moshpit.js
+++ b/apps/desktop/extensions/ai-sidebar/moshpit.js
@@ -85,10 +85,15 @@ export async function lookupMoshpit(hostname, { registryBase, timeoutMs = 4000 }
});
if (!res.ok) return null;
const json = await res.json();
- if (typeof json?.registered !== 'boolean') return null;
+ // `registered` means the TLD is claimed; `name_registered` means THIS name
+ // is. `target` is the address — null until the name points somewhere, which
+ // is what decides parked vs live. `resolved` echoes the name either way.
+ const claimed = typeof json?.name_registered === 'boolean' ? json.name_registered : json?.registered;
+ if (typeof claimed !== 'boolean') return null;
return {
- registered: json.registered,
+ registered: claimed,
resolved: typeof json.resolved === 'string' ? json.resolved : name,
+ target: typeof json.target === 'string' && json.target ? json.target : null,
};
} catch {
return null;
@@ -126,7 +131,7 @@ export function decideResolution({ hostname, mode, clearnetResolves, moshpit, co
}
// Claimed AND pointed somewhere — the precedence rules apply to it.
- if (moshpit.registered && moshpit.resolved) {
+ if (moshpit.registered && moshpit.target) {
if (mode === 'moshpit') {
return {
use: 'moshpit',
diff --git a/apps/desktop/extensions/ai-sidebar/moshpit.test.js b/apps/desktop/extensions/ai-sidebar/moshpit.test.js
index 73301ef..7819b47 100644
--- a/apps/desktop/extensions/ai-sidebar/moshpit.test.js
+++ b/apps/desktop/extensions/ai-sidebar/moshpit.test.js
@@ -46,7 +46,12 @@ describe('moshpit.js is faithful to moshpit-resolve.ts', () => {
});
it('decideResolution agrees across the whole input space', () => {
- const lookups = [null, { registered: false, resolved: '' }, { registered: true, resolved: 'r.eggs' }];
+ const lookups = [
+ null,
+ { registered: false, resolved: '', target: null },
+ { registered: true, resolved: 'r.eggs', target: null }, // claimed, unpointed -> parks
+ { registered: true, resolved: 'r.eggs', target: '203.0.113.7' }, // live
+ ];
for (const hostname of HOSTNAMES) {
for (const mode of ['clearnet', 'moshpit']) {
for (const clearnetResolves of [true, false]) {
@@ -101,7 +106,7 @@ describe('destinationFor — the URL a navigation actually ends up at', () => {
withConfig({ mode: 'clearnet' });
globalThis.fetch = async () => ({
ok: true,
- json: async () => ({ registered: true, resolved: 'original.sploof' }),
+ json: async () => ({ name_registered: true, resolved: 'original.sploof', target: '203.0.113.7' }),
});
expect(await js.destinationFor('alias.sploof', false)).toBe(
'https://pit.moshcode.sh/n/original.sploof',
@@ -121,7 +126,7 @@ describe('destinationFor — the URL a navigation actually ends up at', () => {
let asked = '';
globalThis.fetch = async (url) => {
asked = url;
- return { ok: true, json: async () => ({ registered: true, resolved: 'a.eggs' }) };
+ return { ok: true, json: async () => ({ name_registered: true, resolved: 'a.eggs', target: '203.0.113.7' }) };
};
expect(await js.destinationFor('a.eggs', false)).toBe('https://my.pit/n/a.eggs');
expect(asked).toContain('https://my.pit/api/moshpit/resolve');
@@ -131,7 +136,7 @@ describe('destinationFor — the URL a navigation actually ends up at', () => {
describe('destinationFor — parking', () => {
it('sends an unclaimed name to the parking page', async () => {
globalThis.chrome = { storage: { local: { get: async () => ({ moshpitConfig: { mode: 'clearnet' } }) } } };
- globalThis.fetch = async () => ({ ok: true, json: async () => ({ registered: false }) });
+ globalThis.fetch = async () => ({ ok: true, json: async () => ({ name_registered: false, target: null }) });
expect(await js.destinationFor('california.oranges', false)).toBe(
'https://moshcoding.com/parking?name=california.oranges',
);
@@ -139,7 +144,45 @@ describe('destinationFor — parking', () => {
it('leaves a working clearnet domain alone rather than parking it', async () => {
globalThis.chrome = { storage: { local: { get: async () => ({ moshpitConfig: { mode: 'clearnet' } }) } } };
- globalThis.fetch = async () => ({ ok: true, json: async () => ({ registered: false }) });
+ globalThis.fetch = async () => ({ ok: true, json: async () => ({ name_registered: false, target: null }) });
expect(await js.destinationFor('example.com', true)).toBeNull();
});
});
+
+describe('lookupMoshpit — the registry payload as it really is', () => {
+ const asRegistry = (payload) => {
+ globalThis.fetch = async () => ({ ok: true, json: async () => payload });
+ };
+
+ it('parks a claimed-but-unpointed name — the real california.oranges response', async () => {
+ globalThis.chrome = { storage: { local: { get: async () => ({ moshpitConfig: { mode: 'clearnet' } }) } } };
+ // Verbatim from https://pit.moshcode.sh/api/moshpit/resolve?name=california.oranges
+ asRegistry({
+ name: 'california.oranges',
+ resolved: 'california.oranges',
+ aliased: false,
+ registered: true,
+ name_registered: true,
+ target: null,
+ mode: 'clearnet',
+ prefer: 'fallback',
+ });
+ expect(await js.destinationFor('california.oranges', false)).toBe(
+ 'https://moshcoding.com/parking?name=california.oranges',
+ );
+ });
+
+ it('reads name_registered, not the TLD-level registered flag', async () => {
+ asRegistry({ registered: true, name_registered: false, resolved: 'x.eggs', target: null });
+ expect(await js.lookupMoshpit('x.eggs')).toEqual({
+ registered: false,
+ resolved: 'x.eggs',
+ target: null,
+ });
+ });
+
+ it('treats a present target as the address the name points at', async () => {
+ asRegistry({ name_registered: true, resolved: 'x.eggs', target: '203.0.113.7' });
+ expect((await js.lookupMoshpit('x.eggs')).target).toBe('203.0.113.7');
+ });
+});
diff --git a/apps/desktop/src/moshpit-resolve.test.ts b/apps/desktop/src/moshpit-resolve.test.ts
index b4f5b0e..51365f5 100644
--- a/apps/desktop/src/moshpit-resolve.test.ts
+++ b/apps/desktop/src/moshpit-resolve.test.ts
@@ -10,8 +10,10 @@ import {
consoleUrlFor,
} from './moshpit-resolve';
-const registered = (resolved: string): MoshpitLookup => ({ registered: true, resolved });
-const unregistered: MoshpitLookup = { registered: false, resolved: '' };
+const registered = (resolved: string): MoshpitLookup => ({ registered: true, resolved, target: '203.0.113.7' });
+const unregistered: MoshpitLookup = { registered: false, resolved: '', target: null };
+/** Claimed, but never pointed at an address — the state every name starts in. */
+const unpointed = (name: string): MoshpitLookup => ({ registered: true, resolved: name, target: null });
describe('decideResolution — clearnet mode (the default)', () => {
it('defaults to clearnet', () => {
@@ -132,7 +134,22 @@ describe('lookupMoshpit', () => {
const result = await lookupMoshpit('profullstack.agentic', {
fetchImpl: okFetch({ name: 'profullstack.agentic', resolved: 'profullstack.agent', registered: true }),
});
- expect(result).toEqual({ registered: true, resolved: 'profullstack.agent' });
+ expect(result).toEqual({ registered: true, resolved: 'profullstack.agent', target: null });
+ });
+
+ it('prefers name_registered over the TLD-level registered flag', async () => {
+ const result = await lookupMoshpit('x.eggs', {
+ // `.eggs` is claimed by someone, but `x.eggs` itself is not.
+ fetchImpl: okFetch({ registered: true, name_registered: false, resolved: 'x.eggs', target: null }),
+ });
+ expect(result).toEqual({ registered: false, resolved: 'x.eggs', target: null });
+ });
+
+ it('carries the target through — it is what separates live from parked', async () => {
+ const result = await lookupMoshpit('x.eggs', {
+ fetchImpl: okFetch({ name_registered: true, resolved: 'x.eggs', target: '203.0.113.7' }),
+ });
+ expect(result?.target).toBe('203.0.113.7');
});
it('never asks about a name the registry could not hold', async () => {
@@ -252,7 +269,7 @@ describe('decideResolution — parking unpointed names', () => {
hostname: 'california.oranges',
mode: 'moshpit',
clearnetResolves: false,
- moshpit: { registered: true, resolved: '' },
+ moshpit: unpointed('california.oranges'),
});
expect(d.use).toBe('park');
});
diff --git a/apps/desktop/src/moshpit-resolve.ts b/apps/desktop/src/moshpit-resolve.ts
index c43077d..e07f287 100644
--- a/apps/desktop/src/moshpit-resolve.ts
+++ b/apps/desktop/src/moshpit-resolve.ts
@@ -72,6 +72,15 @@ export interface MoshpitLookup {
registered: boolean;
/** Where it actually points once aliases are followed (`foo.agent`). */
resolved: string;
+ /**
+ * The address the name points at, or null when it has none yet.
+ *
+ * This — not `resolved` — is what "pointed at an IP" means. The registry
+ * echoes the name back in `resolved` whether or not it has a destination, so
+ * testing `resolved` would mark every claimed name as live and nothing would
+ * ever park.
+ */
+ target: string | null;
}
export interface ResolveInputs {
@@ -152,7 +161,7 @@ export function decideResolution(inputs: ResolveInputs): ResolveDecision {
}
// Claimed AND pointed somewhere — the precedence rules below apply to it.
- if (moshpit.registered && moshpit.resolved) {
+ if (moshpit.registered && moshpit.target) {
if (mode === 'moshpit') {
return {
use: 'moshpit',
@@ -251,11 +260,20 @@ export async function lookupMoshpit(
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;
+ const json = (await res.json()) as {
+ registered?: boolean;
+ name_registered?: boolean;
+ resolved?: string;
+ target?: string | null;
+ };
+ // `registered` means the TLD is claimed; `name_registered` means THIS name
+ // is. Prefer the specific one, falling back for older registry builds.
+ const claimed = typeof json?.name_registered === 'boolean' ? json.name_registered : json?.registered;
+ if (typeof claimed !== 'boolean') return null;
return {
- registered: json.registered,
+ registered: claimed,
resolved: typeof json.resolved === 'string' ? json.resolved : `${parsed.label}.${parsed.tld}`,
+ target: typeof json.target === 'string' && json.target ? json.target : null,
};
} catch {
return null;