From 6782624b67320b656c1aff498caf2e89f6f9ff83 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Fri, 28 Aug 2026 20:28:29 +0000 Subject: [PATCH 1/2] Backfill a Moshpit name with a clearnet twin A pit name cannot be reached from outside the pit and cannot hold a certificate, because no CA will issue for an ending ICANN does not delegate. That is the ceiling on the namespace: people take the clean name and hand out an ugly domain anyway, because the ugly one works. A twin is a real registered domain a name publishes as its way in -- `financial.advisors` backfilled by `financial-advisors.net`. The pit name stays the identity; the domain is only transport. The transform is deterministic in both directions, because a pit label may not contain a hyphen. So a twin has exactly one hyphen in its stem and splits back into exactly one name with no lookup, which is what lets a client holding only the domain name the pit name it belongs to. Four things the design holds to: - A twin never touches `prefer`. It is a domain that already answers in the legacy root, so folding it into precedence would have the pit outrank DNS for names DNS handed it -- indistinguishable from the hijack the clearnet-wins default exists to prevent. Covered by a test that compares a backfilled name against a bare one. - Ownership is proven before a twin is served, with one TXT record that does two jobs: publishing it proves control of the domain, and the same record is the reverse pointer that lets someone arriving at the domain discover the name. Two records would have allowed a domain to prove itself and never advertise the name, which is the state where nobody learns the clean name exists. - A lapsed domain does not fail closed, it fails into whoever catches the drop. So the link is dropped on our clock, a week ahead of the registrar's, and it is read at query time rather than swept -- a sweep that has not run yet is a window serving a link already known to be dead. - A domain whose stem reads as a different name is refused. Letting `red-eggs.net` back `blue.eggs` would make the cheap computation and the published proof disagree, and any client trusting the former gets sent somewhere its owner never pointed it. Releasing a name takes its twin with it, deleted explicitly like the pins and records above it, since foreign keys are not enforced here. It matters more than either: an inherited twin would point the next holder's visitors at a stranger's site under their own name. Not built yet: the checkout. TWIN_PRICE_USD is settled and quoted by the API, but nothing charges it, and buying the domain on a customer's behalf needs a registrar integration that is its own unit of work. 51 new tests; 626 pass in apps/pwa. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GWhPX5Uzd29whRg5WPAYM7 --- apps/pwa/src/lib/moshpit-twin.mjs | 255 ++++++++++++++ apps/pwa/src/migrations/015_moshpit_twins.sql | 59 ++++ apps/pwa/src/moshpit.mjs | 297 ++++++++++++++++ apps/pwa/src/routes/moshpit.mjs | 154 +++++++++ apps/pwa/test/moshpit-twin-name.test.mjs | 185 ++++++++++ apps/pwa/test/moshpit-twin-route.test.mjs | 187 ++++++++++ apps/pwa/test/moshpit-twins.test.mjs | 327 ++++++++++++++++++ 7 files changed, 1464 insertions(+) create mode 100644 apps/pwa/src/lib/moshpit-twin.mjs create mode 100644 apps/pwa/src/migrations/015_moshpit_twins.sql create mode 100644 apps/pwa/test/moshpit-twin-name.test.mjs create mode 100644 apps/pwa/test/moshpit-twin-route.test.mjs create mode 100644 apps/pwa/test/moshpit-twins.test.mjs diff --git a/apps/pwa/src/lib/moshpit-twin.mjs b/apps/pwa/src/lib/moshpit-twin.mjs new file mode 100644 index 00000000..74b87052 --- /dev/null +++ b/apps/pwa/src/lib/moshpit-twin.mjs @@ -0,0 +1,255 @@ +// The clearnet twin: what a Moshpit name looks like on the legacy internet. +// +// `financial.advisors` has no answer in the public root and never will. No CA +// will issue for an ending ICANN does not delegate, so the name cannot carry a +// certificate and cannot be reached by anyone who has not installed a resolver. +// That is the whole ceiling on the namespace: people like the clean name and +// then hand out an ugly one anyway, because the ugly one is the one that works. +// +// A twin is the way out. `financial-advisors.net` can be registered, certified +// and reached by anybody, and the pit name is the identity it publishes under. +// The pit name stays canonical; the twin is transport. +// +// Deliberately free of any database import, for the same reason moshpit-name is: +// a client -- the tronbrowser.dev extension, the DNS bridge -- needs these rules +// too, and none of them have a libSQL connection. src/moshpit.mjs owns storage. +import { normalizeLabel, normalizeTld, parseMoshpitName } from "./moshpit-name.mjs"; + +/** + * The endings a twin is offered under, in the order people want them. + * + * All three are unclaimed as Moshpit endings and reserved in RESERVED_TLDS, so + * a twin can never collide with an ending somebody holds. That is not luck -- + * `com`, `net` and `org` were reserved precisely because they collide with the + * legacy internet in ways that would only ever confuse, and this is the one + * place where that collision is the point. + */ +export const TWIN_TLDS = ["com", "net", "org"]; + +/** A hostname label on the legacy internet, where -- unlike in the pit -- dashes are allowed. */ +const DOMAIN_LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/; + +/** + * Normalise a clearnet domain, or null when it could never be one. + * + * Forgiving about what arrives because the field is typed by hand and people + * paste a URL with a path still on it. Strict in one place beyond DNS: the last + * label must be alphabetic and at least two characters, which refuses + * `1.2.3.4`. An address is a well-formed sequence of labels, and accepting one + * here would mean recording a "domain" that has no registrar to expire at. + */ +export function normalizeDomain(input) { + const raw = String(input ?? "").trim().toLowerCase() + .replace(/^[a-z][a-z0-9+.-]*:\/\//, "") // a pasted URL + .replace(/[/?#].*$/, "") // ...with a path on it + .replace(/^\.+/, "") + .replace(/\.+$/, ""); // and a root dot, sometimes + if (!raw || raw.length > 253) return null; + const labels = raw.split("."); + if (labels.length < 2) return null; + if (!labels.every((l) => DOMAIN_LABEL.test(l))) return null; + if (!/^[a-z]{2,}$/.test(labels[labels.length - 1])) return null; + return raw; +} + +/** + * `blue.eggs` + `net` -> `blue-eggs.net`, or null when it will not fit. + * + * A dot collapsing into a hyphen, and it is deterministic in BOTH directions + * for one reason: a Moshpit label may not contain a hyphen. That rule exists to + * stop look-alike squatting (see LABEL in moshpit-name.mjs) and this inherits it + * for free -- a twin has exactly one hyphen in its stem, so it splits back into + * exactly one `