Skip to content

Commit 2c1dedc

Browse files
fix(pit): allow numeric hostname labels (#119)
1 parent 6c7c906 commit 2c1dedc

4 files changed

Lines changed: 33 additions & 13 deletions

File tree

‎apps/pwa/src/lib/moshpit-name.mjs‎

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,25 @@ export const RESERVED_TLDS = new Set([
2828
/** A TLD label: lowercase letters, digits and dashes; no leading/trailing dash. */
2929
const LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
3030

31+
/** A hostname label. Unlike a TLD, an all-numeric label is valid. */
32+
export function normalizeLabel(input) {
33+
const raw = String(input ?? "").trim().toLowerCase();
34+
return raw && raw.length <= 63 && LABEL.test(raw) ? raw : null;
35+
}
36+
3137
/**
3238
* Normalise user input into a bare TLD label, or null when it could never be
3339
* one. Accepts ".eggs", "eggs", " .EGGS " -- people type the dot.
3440
*/
3541
export function normalizeTld(input) {
3642
const raw = String(input ?? "").trim().toLowerCase().replace(/^\.+/, "");
37-
if (!raw || raw.length > 63) return null;
3843
// A dot means they gave a domain, not a TLD. Say so rather than silently
3944
// registering the wrong thing.
40-
if (raw.includes(".")) return null;
41-
if (!LABEL.test(raw)) return null;
45+
const label = normalizeLabel(raw);
46+
if (!label) return null;
4247
// All-numeric would be ambiguous against an IPv4 literal in a hostname.
43-
if (/^\d+$/.test(raw)) return null;
44-
return raw;
48+
if (/^\d+$/.test(label)) return null;
49+
return label;
4550
}
4651

4752
/** Why a TLD cannot be registered, or null when it is fine. */
@@ -64,10 +69,10 @@ export function parseMoshpitName(input) {
6469
const parts = raw.split(".");
6570
if (parts.length !== 2) return null;
6671
const [label, tld] = parts;
67-
// Both halves are hostname labels, and normalizeTld already encodes exactly
68-
// that rule -- so reuse it rather than keeping a second copy that can drift.
69-
if (!normalizeTld(label) || !normalizeTld(tld)) return null;
70-
return { label, tld };
72+
const normalizedLabel = normalizeLabel(label);
73+
const normalizedTld = normalizeTld(tld);
74+
if (!normalizedLabel || !normalizedTld) return null;
75+
return { label: normalizedLabel, tld: normalizedTld };
7176
}
7277

7378
/* ---- resolution precedence (tronbrowser.dev) ---- */

‎apps/pwa/src/moshpit.mjs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
// checkable rather than trusted.
1212

1313
import { get, all, run } from "./db.mjs";
14-
import { normalizeTld, parseMoshpitName, tldRejection } from "./lib/moshpit-name.mjs";
14+
import { normalizeLabel, normalizeTld, parseMoshpitName, tldRejection } from "./lib/moshpit-name.mjs";
1515

1616
export {
17-
RESERVED_TLDS, RESOLVE_MODES, normalizeTld, parseMoshpitName, tldRejection,
17+
RESERVED_TLDS, RESOLVE_MODES, normalizeLabel, normalizeTld, parseMoshpitName, tldRejection,
1818
normalizeMode, resolutionPreference,
1919
} from "./lib/moshpit-name.mjs";
2020

@@ -174,7 +174,7 @@ export async function clearExempt({ tld: tldInput, label: labelInput, userId })
174174

175175
async function ownedTldAndLabel(tldInput, labelInput, userId) {
176176
const tld = normalizeTld(tldInput);
177-
const label = normalizeTld(labelInput);
177+
const label = normalizeLabel(labelInput);
178178
if (!tld || !label) return { ok: false, error: "not a valid name" };
179179
const owner = await getTld(tld);
180180
if (!owner) return { ok: false, error: `.${tld} is not registered` };

‎apps/pwa/test/moshpit-name.test.mjs‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import assert from "node:assert/strict";
55
import test from "node:test";
66

77
import {
8-
RESERVED_TLDS, normalizeTld, tldRejection, parseMoshpitName,
8+
RESERVED_TLDS, normalizeLabel, normalizeTld, tldRejection, parseMoshpitName,
99
normalizeMode, resolutionPreference,
1010
} from "../src/lib/moshpit-name.mjs";
1111

@@ -29,6 +29,12 @@ test("normalizeTld rejects what could never be a TLD", () => {
2929
assert.equal(normalizeTld(undefined), null);
3030
});
3131

32+
test("hostname labels may be numeric even though TLDs may not", () => {
33+
assert.equal(normalizeLabel("123"), "123");
34+
assert.equal(normalizeTld("123"), null);
35+
assert.deepEqual(parseMoshpitName("123.eggs"), { label: "123", tld: "eggs" });
36+
});
37+
3238
test("reserved names cannot be claimed", () => {
3339
for (const name of ["bank", "apple", "gov", "moshpit", "com"]) {
3440
assert.ok(RESERVED_TLDS.has(name), `${name} should be reserved`);
@@ -45,6 +51,7 @@ test("a TLD needs at least two characters", () => {
4551
test("parseMoshpitName splits exactly one dot", () => {
4652
assert.deepEqual(parseMoshpitName("foo.agentic"), { label: "foo", tld: "agentic" });
4753
assert.deepEqual(parseMoshpitName(" FOO.Agentic "), { label: "foo", tld: "agentic" });
54+
assert.deepEqual(parseMoshpitName("123.agentic"), { label: "123", tld: "agentic" });
4855
assert.equal(parseMoshpitName("a.b.c"), null, "the namespace is one level deep");
4956
assert.equal(parseMoshpitName("nodot"), null);
5057
assert.equal(parseMoshpitName(""), null);

‎apps/pwa/test/moshpit-registry.test.mjs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,14 @@ test("moshpit registry", { skip: installed ? false : "pwa dependencies not insta
108108
assert.equal((await m.resolveMoshpitName("other.agentic")).resolved, "other.agent");
109109
});
110110

111+
await t.test("numeric labels resolve and can be exempted", async () => {
112+
assert.equal((await m.resolveMoshpitName("123.agentic")).resolved, "123.agent");
113+
assert.equal((await m.setExempt({ tld: "agentic", label: "123", userId: ALICE })).ok, true);
114+
const r = await m.resolveMoshpitName("123.agentic");
115+
assert.equal(r.resolved, "123.agentic");
116+
assert.equal(r.exempt, true);
117+
});
118+
111119
await t.test("exemptions survive the alias being repointed", async () => {
112120
await m.clearAlias("agentic", ALICE);
113121
await m.setAlias({ from: "agentic", to: "agents", userId: ALICE });

0 commit comments

Comments
 (0)