From 7dd98326295f74ac947593e41209fd4e57bb3d01 Mon Sep 17 00:00:00 2001 From: RaviTejaKNTS Date: Mon, 2 Nov 2026 14:37:57 +0530 Subject: [PATCH 1/2] Fix IANA TLD directory parser and refresh reference data --- lib/domain/data/public-suffix.json | 3 +- lib/domain/data/rdap-bootstrap.json | 6 +- lib/domain/data/rdap-overrides.json | 8 - lib/domain/data/tld-directory.json | 69 +++--- lib/domain/data/tlds.json | 5 +- .../domain-tools/refresh-tld-directory.mjs | 99 ++++++--- .../shared/fixtures/iana-root-db.html | 197 ++++++++++++++++++ .../shared/tld-directory-refresh.test.mjs | 74 +++++++ 8 files changed, 372 insertions(+), 89 deletions(-) create mode 100644 tests/pages/domain-tools/shared/fixtures/iana-root-db.html create mode 100644 tests/pages/domain-tools/shared/tld-directory-refresh.test.mjs diff --git a/lib/domain/data/public-suffix.json b/lib/domain/data/public-suffix.json index bdc9b01d..185ca06c 100644 --- a/lib/domain/data/public-suffix.json +++ b/lib/domain/data/public-suffix.json @@ -1,6 +1,6 @@ { "source": "https://publicsuffix.org/list/public_suffix_list.dat", - "fetchedAt": "2026-10-14", + "fetchedAt": "2026-11-02", "section": "ICANN", "exceptions": [ "www.ck", @@ -6252,7 +6252,6 @@ "health", "healthcare", "help", - "helsinki", "here", "hermes", "hiphop", diff --git a/lib/domain/data/rdap-bootstrap.json b/lib/domain/data/rdap-bootstrap.json index 56f08d37..9a5d5286 100644 --- a/lib/domain/data/rdap-bootstrap.json +++ b/lib/domain/data/rdap-bootstrap.json @@ -1,7 +1,7 @@ { "source": "https://data.iana.org/rdap/dns.json", - "fetchedAt": "2026-10-14", - "publication": "2026-09-30T23:00:03Z", + "fetchedAt": "2026-11-02", + "publication": "2026-10-28T22:00:04Z", "services": { "kg": "http://rdap.cctld.kg/", "mg": "http://rdap.nic.mg/", @@ -399,7 +399,6 @@ "hdfc": "https://rdap.identitydigital.services/rdap/", "hdfcbank": "https://rdap.identitydigital.services/rdap/", "healthcare": "https://rdap.identitydigital.services/rdap/", - "helsinki": "https://rdap.identitydigital.services/rdap/", "hermes": "https://rdap.identitydigital.services/rdap/", "hkt": "https://rdap.identitydigital.services/rdap/", "hockey": "https://rdap.identitydigital.services/rdap/", @@ -552,6 +551,7 @@ "sarl": "https://rdap.identitydigital.services/rdap/", "saxo": "https://rdap.identitydigital.services/rdap/", "sbi": "https://rdap.identitydigital.services/rdap/", + "sc": "https://rdap.identitydigital.services/rdap/", "scholarships": "https://rdap.identitydigital.services/rdap/", "school": "https://rdap.identitydigital.services/rdap/", "schule": "https://rdap.identitydigital.services/rdap/", diff --git a/lib/domain/data/rdap-overrides.json b/lib/domain/data/rdap-overrides.json index eb90c542..eb297130 100644 --- a/lib/domain/data/rdap-overrides.json +++ b/lib/domain/data/rdap-overrides.json @@ -165,14 +165,6 @@ "notFoundStatus": 404, "verifiedAt": "2026-10-14" }, - "sc": { - "url": "https://rdap.identitydigital.services/rdap/", - "operator": "Identity Digital", - "verifiedWith": "nic.sc", - "notFoundWith": "stack127-check-a7f8c9e2d4b6.sc", - "notFoundStatus": 404, - "verifiedAt": "2026-10-14" - }, "sh": { "url": "https://rdap.identitydigital.services/rdap/", "operator": "Identity Digital", diff --git a/lib/domain/data/tld-directory.json b/lib/domain/data/tld-directory.json index 5db9ff00..19ea72d8 100644 --- a/lib/domain/data/tld-directory.json +++ b/lib/domain/data/tld-directory.json @@ -3,14 +3,14 @@ "rootDb": "https://www.iana.org/domains/root/db", "rootZone": "https://www.internic.net/domain/root.zone" }, - "fetchedAt": "2026-10-14", - "tldListVersion": "2026101300", + "fetchedAt": "2026-11-02", + "tldListVersion": "2026110200", "counts": { - "total": 1437, - "signed": 1351, - "rdapCovered": 1229, + "total": 1436, + "signed": 1350, + "rdapCovered": 1228, "whoisCovered": 77, - "lookupCovered": 1306, + "lookupCovered": 1305, "uncovered": 131, "unassigned": 0, "missingType": 0 @@ -292,7 +292,7 @@ { "tld": "aero", "unicode": null, - "type": "sponsored", + "type": "generic", "organisation": "Societe Internationale de Telecommunications Aeronautique (SITA INC USA)", "assigned": true, "rdap": "bootstrap", @@ -955,7 +955,7 @@ { "tld": "asia", "unicode": null, - "type": "sponsored", + "type": "generic", "organisation": "DotAsia Organisation Ltd.", "assigned": true, "rdap": "bootstrap", @@ -1761,7 +1761,7 @@ { "tld": "biz", "unicode": null, - "type": "generic-restricted", + "type": "generic", "organisation": "Registry Services, LLC", "assigned": true, "rdap": "bootstrap", @@ -2684,7 +2684,7 @@ { "tld": "cat", "unicode": null, - "type": "sponsored", + "type": "generic", "organisation": "Fundacio puntCAT", "assigned": true, "rdap": "bootstrap", @@ -3361,7 +3361,7 @@ "tld": "cologne", "unicode": null, "type": "generic", - "organisation": "dotKoeln GmbH", + "organisation": "domainworx Service & Management GmbH", "assigned": true, "rdap": "bootstrap", "rdapOperator": null, @@ -3555,7 +3555,7 @@ { "tld": "coop", "unicode": null, - "type": "sponsored", + "type": "generic", "organisation": "DotCooperation LLC", "assigned": true, "rdap": "bootstrap", @@ -4634,7 +4634,7 @@ { "tld": "edu", "unicode": null, - "type": "sponsored", + "type": "generic", "organisation": "EDUCAUSE", "assigned": true, "rdap": "whois", @@ -6415,7 +6415,7 @@ { "tld": "gov", "unicode": null, - "type": "sponsored", + "type": "generic", "organisation": "Cybersecurity and Infrastructure Security Agency", "assigned": true, "rdap": "bootstrap", @@ -6815,19 +6815,6 @@ "dnssec": true, "isIdn": false }, - { - "tld": "helsinki", - "unicode": null, - "type": "generic", - "organisation": "City of Helsinki", - "assigned": true, - "rdap": "bootstrap", - "rdapOperator": null, - "lookupSource": "bootstrap", - "lookupHost": "rdap.identitydigital.services", - "dnssec": true, - "isIdn": false - }, { "tld": "here", "unicode": null, @@ -7598,7 +7585,7 @@ { "tld": "int", "unicode": null, - "type": "sponsored", + "type": "generic", "organisation": "Internet Assigned Numbers Authority", "assigned": true, "rdap": "bootstrap", @@ -7975,7 +7962,7 @@ { "tld": "jobs", "unicode": null, - "type": "sponsored", + "type": "generic", "organisation": "Employ Media LLC", "assigned": true, "rdap": "bootstrap", @@ -8301,7 +8288,7 @@ "tld": "koeln", "unicode": null, "type": "generic", - "organisation": "dotKoeln GmbH", + "organisation": "domainworx Service & Management GmbH", "assigned": true, "rdap": "bootstrap", "rdapOperator": null, @@ -9730,7 +9717,7 @@ { "tld": "mil", "unicode": null, - "type": "sponsored", + "type": "generic", "organisation": "DoD Network Information Center", "assigned": true, "rdap": "none", @@ -10224,7 +10211,7 @@ { "tld": "museum", "unicode": null, - "type": "sponsored", + "type": "generic", "organisation": "Museum Domain Management Association", "assigned": true, "rdap": "bootstrap", @@ -10354,7 +10341,7 @@ { "tld": "name", "unicode": null, - "type": "generic-restricted", + "type": "generic", "organisation": "VeriSign Information Services, Inc.", "assigned": true, "rdap": "bootstrap", @@ -11836,7 +11823,7 @@ { "tld": "post", "unicode": null, - "type": "sponsored", + "type": "generic", "organisation": "Universal Postal Union", "assigned": true, "rdap": "bootstrap", @@ -11901,7 +11888,7 @@ { "tld": "pro", "unicode": null, - "type": "generic-restricted", + "type": "generic", "organisation": "Identity Digital Limited", "assigned": true, "rdap": "bootstrap", @@ -13035,9 +13022,9 @@ "type": "country-code", "organisation": "VCS Pty Ltd", "assigned": true, - "rdap": "override", - "rdapOperator": "Identity Digital", - "lookupSource": "override", + "rdap": "bootstrap", + "rdapOperator": null, + "lookupSource": "bootstrap", "lookupHost": "rdap.identitydigital.services", "dnssec": true, "isIdn": false @@ -14605,7 +14592,7 @@ { "tld": "tel", "unicode": null, - "type": "sponsored", + "type": "generic", "organisation": "Telnames Ltd.", "assigned": true, "rdap": "bootstrap", @@ -15138,7 +15125,7 @@ { "tld": "travel", "unicode": null, - "type": "sponsored", + "type": "generic", "organisation": "Dog Beach, LLC", "assigned": true, "rdap": "bootstrap", @@ -18401,7 +18388,7 @@ { "tld": "xxx", "unicode": null, - "type": "sponsored", + "type": "generic", "organisation": "ICM Registry LLC", "assigned": true, "rdap": "bootstrap", diff --git a/lib/domain/data/tlds.json b/lib/domain/data/tlds.json index 904a3268..a24866be 100644 --- a/lib/domain/data/tlds.json +++ b/lib/domain/data/tlds.json @@ -1,7 +1,7 @@ { "source": "https://data.iana.org/TLD/tlds-alpha-by-domain.txt", - "fetchedAt": "2026-10-14", - "version": "2026101300", + "fetchedAt": "2026-11-02", + "version": "2026110200", "tlds": [ "aaa", "aarp", @@ -526,7 +526,6 @@ "health", "healthcare", "help", - "helsinki", "here", "hermes", "hiphop", diff --git a/scripts/domain-tools/refresh-tld-directory.mjs b/scripts/domain-tools/refresh-tld-directory.mjs index 852d615b..e242cf8e 100644 --- a/scripts/domain-tools/refresh-tld-directory.mjs +++ b/scripts/domain-tools/refresh-tld-directory.mjs @@ -21,6 +21,7 @@ import { readFile, writeFile } from "node:fs/promises"; import path from "node:path"; +import { pathToFileURL } from "node:url"; const root = process.cwd(); const outFile = path.join(root, "lib/domain/data/tld-directory.json"); @@ -60,48 +61,78 @@ function stripTags(value) { .trim(); } +const KNOWN_TYPES = new Set([ + "generic", "country-code", "sponsored", "infrastructure", "generic-restricted", "test", +]); + /** - * IANA renders one row per TLD: name, type, sponsoring organisation. - * - * The A-label comes from the row's link href rather than from the visible text. - * Right-to-left endings are displayed wrapped in bidi control marks, so - * ".ישראל" arrives as U+200F + ".ישראל" + U+200E and does not convert back to its - * A-label. The href is unambiguous, so use that and keep the cleaned display name - * only for showing to a reader. + * Current columns: TLD, Status, Type, TLD Manager / Purpose, then script metadata. + * Only the anchor's leading text is the display name; its children contain icons, + * script pills and the A-label. The href is the authority for the ASCII name. */ -function parseRootDb(html) { - const rows = html.match(/([\s\S]*?)<\/tr>/g) ?? []; +export function parseRootDb(html, requiredTlds = null) { + const required = requiredTlds === null ? null : new Set(requiredTlds); + const headers = [...html.matchAll(/]*>([\s\S]*?)<\/th>/gi)] + .slice(0, 4).map((match) => stripTags(match[1])); + if (headers.join("|") !== "TLD|Status|Type|TLD Manager / Purpose") { + throw new Error("IANA root database columns changed. Refusing to write a partial dataset."); + } + + const rows = html.match(/]*>[\s\S]*?<\/tr>/gi) ?? []; const entries = []; for (const row of rows) { - const cells = [...row.matchAll(/]*>([\s\S]*?)<\/td>/g)]; - if (cells.length < 3) continue; - const [nameCell, typeCell, organisationCell] = cells.map((match) => match[1]); - - const href = nameCell.match(/\/domains\/root\/db\/([^."]+)\.html/); - const displayed = stripTags(nameCell) - // Strip the bidi control characters IANA wraps RTL names in. + if (!row.includes("/domains/root/db/")) continue; + const cells = [...row.matchAll(/]*>([\s\S]*?)<\/td>/gi)].map((match) => match[1]); + const [nameCell = "", statusCell = "", typeCell = "", organisationCell = ""] = cells; + const link = nameCell.match(/]*href=["']\/domains\/root\/db\/([a-z0-9-]+)(?:\.html)?["'][^>]*>([\s\S]*?)<\/a>/i); + if (!link || cells.length < 4) { + throw new Error("Malformed IANA root database row. Refusing to write a partial dataset."); + } + const tld = link[1].toLowerCase(); + // IANA also lists retired and eligible names, sometimes without managers. + // Validate every current root-zone ending, not those historical records. + if (required && !required.has(tld)) continue; + const displayed = stripTags(link[2].split("<")[0]) .replace(/[‎‏‪-‮⁦-⁩]/g, "") - .replace(/^\./, "") - .trim(); - - const tld = (href?.[1] ?? displayed).toLowerCase(); - if (!tld) continue; - - const type = stripTags(typeCell); + .trim().replace(/^\.|\.$/g, ""); + if (!displayed) throw new Error(`Missing IANA display name for .${tld}. Refusing to write.`); + + const rawType = stripTags(typeCell).toLowerCase(); + // The new table includes reserved names outside the root zone. They are not + // TLDs in this dataset. .arpa is the active Special Use (infrastructure) row. + if (rawType === "special use" && stripTags(statusCell) === "Reserved") continue; + const type = rawType === "country code" ? "country-code" + : rawType === "special use" && tld === "arpa" ? "infrastructure" : rawType; const organisation = stripTags(organisationCell); - entries.push({ tld, - displayed: displayed && displayed !== tld ? displayed : null, + displayed: displayed !== tld ? displayed : null, type, - // IANA writes "Not assigned" for delegated-but-unused endings. - organisation: organisation && organisation !== "Not assigned" ? organisation : null, + organisation: organisation === "Not assigned" ? null : organisation, assigned: organisation !== "Not assigned", }); } + validateRootDb(entries); return entries; } +/** Validate both parsed facts and coverage before the caller can write anything. */ +export function validateRootDb(entries, requiredTlds = []) { + const seen = new Set(); + for (const entry of entries) { + if (seen.has(entry.tld)) throw new Error(`Duplicate IANA row for .${entry.tld}. Refusing to write.`); + seen.add(entry.tld); + if (!KNOWN_TYPES.has(entry.type)) { + throw new Error(`Invalid IANA type for .${entry.tld}: "${entry.type}". Refusing to write.`); + } + if (!entry.organisation && !(entry.organisation === null && entry.assigned === false)) { + throw new Error(`Missing IANA organisation for .${entry.tld}. Refusing to write.`); + } + } + const missing = requiredTlds.filter((tld) => !seen.has(tld)); + if (missing.length) throw new Error(`Missing IANA rows for: ${missing.join(", ")}. Refusing to write a partial dataset.`); +} + /** Every TLD with at least one DS record in the root is signed. */ function parseSignedTlds(zone) { const signed = new Set(); @@ -125,7 +156,7 @@ async function main() { const [rootDbHtml, rootZone] = await Promise.all([get(SOURCES.rootDb), get(SOURCES.rootZone)]); - const iana = parseRootDb(rootDbHtml); + const iana = parseRootDb(rootDbHtml, tldData.tlds); if (iana.length < MIN_EXPECTED_ROWS) { throw new Error( `Only parsed ${iana.length} rows from the IANA root database, expected at least ${MIN_EXPECTED_ROWS}. ` + @@ -133,6 +164,8 @@ async function main() { ); } + validateRootDb(iana, tldData.tlds); + const signed = parseSignedTlds(rootZone); if (signed.size < 500) { throw new Error(`Only found ${signed.size} signed endings in the root zone, which looks wrong. Refusing to write.`); @@ -202,7 +235,9 @@ async function main() { if (missingType) console.log(`No type from IANA: ${missingType}`); } -main().catch((error) => { - console.error(error.message); - process.exit(1); -}); +if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) { + main().catch((error) => { + console.error(error.message); + process.exit(1); + }); +} diff --git a/tests/pages/domain-tools/shared/fixtures/iana-root-db.html b/tests/pages/domain-tools/shared/fixtures/iana-root-db.html new file mode 100644 index 00000000..0c4250ae --- /dev/null +++ b/tests/pages/domain-tools/shared/fixtures/iana-root-db.html @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TLDStatusTypeTLD Manager / PurposeScriptWriting SystemSort
+ +.aaa + + + + + + + + + +Active + + + + + + +Generic + + +American Automobile Association, Inc. + +Latn + +Latin + +en||aaa +
+ +‏ישראל.‎ + + + +Hebrew +
+xn--4dbrk0ce +
+
+ + + + +Active + + + + + + +Country Code + + +The Israel Internet Association (RA) + +Hebr + +Hebrew + +he||ישראל +
+ +.bl + + + + + + + + + +Eligible + + + + + + +Country Code + + +Not assigned + +Latn + +Latin + +en||bl +
+ +.arpa + + + + + + + + + +Active + + + + + + +Special Use + + +Internet Architecture Board (IAB) + +Latn + +Latin + +en||arpa +
+ +.example + + + + + + + + + +Reserved + + + + + + +Special Use + + +Reserved for use in documentation and examples. + +Latn + +Latin + +en||example +
diff --git a/tests/pages/domain-tools/shared/tld-directory-refresh.test.mjs b/tests/pages/domain-tools/shared/tld-directory-refresh.test.mjs new file mode 100644 index 00000000..d6e89355 --- /dev/null +++ b/tests/pages/domain-tools/shared/tld-directory-refresh.test.mjs @@ -0,0 +1,74 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFile, mkdtemp, mkdir, writeFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; +import { parseRootDb, validateRootDb } from "../../../../scripts/domain-tools/refresh-tld-directory.mjs"; + +const fixture = await readFile(new URL("./fixtures/iana-root-db.html", import.meta.url), "utf8"); + +test("parses current columns, clean IDN names, and real unassigned managers", () => { + assert.deepEqual(parseRootDb(fixture), [ + { tld: "aaa", displayed: null, type: "generic", organisation: "American Automobile Association, Inc.", assigned: true }, + { tld: "xn--4dbrk0ce", displayed: "ישראל", type: "country-code", organisation: "The Israel Internet Association (RA)", assigned: true }, + { tld: "bl", displayed: null, type: "country-code", organisation: null, assigned: false }, + { tld: "arpa", displayed: null, type: "infrastructure", organisation: "Internet Architecture Board (IAB)", assigned: true }, + ]); +}); + +test("accepts all supported types and legacy .html links", () => { + for (const type of ["generic", "country-code", "sponsored", "infrastructure", "generic-restricted", "test"]) { + assert.equal(parseRootDb(fixture.replace("Generic", type))[0].type, type); + } + assert.equal(parseRootDb(fixture.replace('/db/aaa"', '/db/aaa.html"'))[0].tld, "aaa"); +}); + +test("rejects blank type, unknown type, and a status in the type column", () => { + for (const value of ["", "Active", "unexpected"]) { + assert.throws(() => parseRootDb(fixture.replace("Generic", value)), /Invalid IANA type for .aaa/); + } +}); + +test("rejects a blank manager rather than treating it as Not assigned", () => { + assert.throws(() => parseRootDb(fixture.replace("American Automobile Association, Inc.", "")), /Missing IANA organisation for .aaa/); + assert.throws(() => parseRootDb(fixture.replace("Not assigned", "")), /Missing IANA organisation for .bl/); +}); + +test("rejects changed columns and malformed TLD links", () => { + assert.throws(() => parseRootDb(fixture.replace("Status", "Other")), /columns changed/); + assert.throws(() => parseRootDb(fixture.replace('/db/aaa"', '/db/aaa/unknown"')), /Malformed IANA/); +}); + +test("requires every current root ending and rejects duplicates", () => { + const entries = parseRootDb(fixture); + assert.throws(() => validateRootDb(entries, ["aaa", "com"]), /Missing IANA rows for: com/); + assert.throws(() => validateRootDb([...entries, entries[0]]), /Duplicate IANA row/); +}); + +test("validates every current ending while ignoring historical rows outside the root", () => { + const brokenHistoricalRow = fixture.replace("Not assigned", ""); + assert.deepEqual(parseRootDb(brokenHistoricalRow, ["aaa"]), parseRootDb(fixture).slice(0, 1)); + assert.throws(() => parseRootDb(brokenHistoricalRow, ["aaa", "bl"]), /Missing IANA organisation for .bl/); +}); + +test("a failed refresh exits with an error and leaves the committed output untouched", async () => { + const directory = await mkdtemp(path.join(tmpdir(), "stack127-tld-refresh-")); + try { + const output = path.join(directory, "lib/domain/data/tld-directory.json"); + await mkdir(path.dirname(output), { recursive: true }); + await writeFile(output, "previous valid dataset\n"); + const invalidHtml = fixture.replace("Generic", "Active"); + const preload = `globalThis.fetch = async () => ({ ok: true, text: async () => ${JSON.stringify(invalidHtml)} });`; + const result = spawnSync(process.execPath, [ + "--experimental-strip-types", "--import", `data:text/javascript;base64,${Buffer.from(preload).toString("base64")}`, + fileURLToPath(new URL("../../../../scripts/domain-tools/refresh-tld-directory.mjs", import.meta.url)), + ], { cwd: directory, encoding: "utf8" }); + assert.equal(result.status, 1); + assert.match(result.stderr, /Invalid IANA type for .aaa/); + assert.equal(await readFile(output, "utf8"), "previous valid dataset\n"); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); From ee065f78022cd4e4b28395980c81f875dbda48c7 Mon Sep 17 00:00:00 2001 From: RaviTejaKNTS Date: Mon, 2 Nov 2026 14:46:35 +0530 Subject: [PATCH 2/2] Reject retired required TLDs and align directory types --- .../tld-directory/content/types.mdx | 6 ++-- .../(directories)/tld-directory/tld-table.tsx | 8 +++-- .../domain-tools/refresh-tld-directory.mjs | 7 ++++ .../shared/tld-directory-refresh.test.mjs | 34 ++++++++++++++----- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/app/(site)/(directories)/tld-directory/content/types.mdx b/app/(site)/(directories)/tld-directory/content/types.mdx index 82d9ce1f..c605a3d4 100644 --- a/app/(site)/(directories)/tld-directory/content/types.mdx +++ b/app/(site)/(directories)/tld-directory/content/types.mdx @@ -1,11 +1,9 @@ ## What do the domain extension types mean? -The type is IANA's label for how an ending is run, and it hints at who can register under it. +The type is IANA's label for an ending. Generic endings can still restrict who can register, as `.edu` and `.gov` do. | Type | What it is | | --- | --- | -| generic | Most endings, such as `.com` and `.xyz`. Most arrived from 2013 on, through ICANN's New gTLD Program. Brand endings, such as `.bmw`, are used only by their owner. | +| generic | Endings such as `.com`, `.xyz`, `.edu`, `.gov`, `.biz`, and `.pro`. Brand endings, such as `.bmw`, are used only by their owner. | | country-code | Two-letter endings for a country or territory, such as `.uk`, plus versions in other scripts, such as `.中国`, shown with their `xn--` form. Each sets its own rules. | -| sponsored | Endings for one community, such as `.edu` and `.gov`, often with eligibility rules. | -| generic-restricted | Older generic endings that launched with limits on who could register, such as `.biz` and `.pro`. | | infrastructure | `.arpa`, used for reverse DNS. You can't register a name under it. | diff --git a/app/(site)/(directories)/tld-directory/tld-table.tsx b/app/(site)/(directories)/tld-directory/tld-table.tsx index 0324d29b..c163c0c7 100644 --- a/app/(site)/(directories)/tld-directory/tld-table.tsx +++ b/app/(site)/(directories)/tld-directory/tld-table.tsx @@ -14,7 +14,6 @@ export type TldRow = { isIdn: boolean; }; -const TYPES = ["generic", "country-code", "sponsored", "infrastructure", "generic-restricted", "test"]; const PAGE_SIZE = 100; export function TldTable({ rows }: { rows: TldRow[] }) { @@ -24,6 +23,11 @@ export function TldTable({ rows }: { rows: TldRow[] }) { const [dnssec, setDnssec] = useState(""); const [limit, setLimit] = useState(PAGE_SIZE); + const types = useMemo( + () => [...new Set(rows.map((row) => row.type).filter((value): value is string => Boolean(value)))].sort(), + [rows], + ); + const filtered = useMemo(() => { const needle = query.trim().toLowerCase().replace(/^\./, ""); return rows.filter((row) => { @@ -65,7 +69,7 @@ export function TldTable({ rows }: { rows: TldRow[] }) { Type