diff --git a/apps/pwa/src/lib/moshpit-name.mjs b/apps/pwa/src/lib/moshpit-name.mjs index f7be06c..19d1952 100644 --- a/apps/pwa/src/lib/moshpit-name.mjs +++ b/apps/pwa/src/lib/moshpit-name.mjs @@ -226,20 +226,25 @@ export function shortCount(n) { } /** - * The most a child name should cost per year. + * The most a child name should cost. Once, for good. * * This is the `me.whatever` price — what a buyer pays to mint a name under an * ending someone else holds. It is not the price of `.whatever` itself, which - * is a separate thing the registry does not charge for yet. + * is a separate thing. * * $2 flat. PRD 0005 R3 wrote this as $1.99; the extra cent buys nothing but a * price tag that looks like a supermarket shelf, and every number a person has * to reason about here — a default, a cap, a per-line override — reads better * round. The PRD number is superseded by this one. * - * The ceiling is on the annual registration/renewal price only. A one-time - * Buy Now resale transfers ownership rather than starting a term, and §10.2.4 - * puts no ceiling on that. + * Not an annual price, and never was in practice: `moshpit_names` has never had + * an expiry column and nothing has ever renewed a name. The PRD called it a + * yearly fee, the schema sold it outright, and this comment used to describe + * the PRD. It now describes what the code does, which is the thing buyers were + * actually getting. + * + * A one-time Buy Now resale transfers ownership rather than starting anything, + * and §10.2.4 puts no ceiling on that. */ export const MAX_CHILD_PRICE_USD = 2; @@ -247,7 +252,7 @@ export const MAX_CHILD_PRICE_USD = 2; export const CHILD_PRICE_USD = MAX_CHILD_PRICE_USD; /** - * What a direct ending costs per year: `.whatever` itself. + * What a direct ending costs, once: `.whatever` itself. * * Nothing charges this yet — `registerTld` inserts a row and claiming is free. * It lives here anyway so the two prices sit together and the number is settled @@ -257,6 +262,12 @@ export const CHILD_PRICE_USD = MAX_CHILD_PRICE_USD; * $5 flat, for the same reason the child price is $2: PRD 0005 §10.1 wrote * these as $4.99 and $1.99, and the trailing cents buy nothing but a price tag * shaped like a supermarket shelf. + * + * Paid once and held for good. §5's one-year term with renewals is withdrawn + * (migration 016). A name that lapses is a name somebody else can catch, and + * the whole reason to be here is to stop settling for the hyphenated version of + * the name you wanted — an annual invoice with a drop date attached is the + * thing people are leaving, not something to sell them again. */ export const ENDING_PRICE_USD = 5; diff --git a/apps/pwa/src/lib/moshpit-twin.mjs b/apps/pwa/src/lib/moshpit-twin.mjs new file mode 100644 index 0000000..74b8705 --- /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 `