From 89dba9d6f93f5accbcc0ac05bdadbfcff63be6a6 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Wed, 21 Oct 2026 14:03:57 +0000 Subject: [PATCH] fix(tlds): read IANA's redesigned root zone database page IANA redesigned https://www.iana.org/domains/root/db: links lost their .html suffix, a status column now sits before the type, the manager cell is marked col-manager, and types are title case ("Country Code"). The old regex matched nothing, so every TLD sync logged [tlds] root zone database failed: root zone database parsed to 0 rows and type/manager stopped updating. Parse row by row and read both layouts; types are normalised to the existing vocabulary (country-code), and a retired row (status Retired, empty manager) keeps the "Not assigned" manager the retired-label pass keys on. The live page parses to 1,603 rows. Co-Authored-By: Claude Opus 5.5 --- packages/core/src/tlds/sources.js | 46 +++++++++++++++++++++++++------ test/tlds.test.js | 32 +++++++++++++++++++++ 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/packages/core/src/tlds/sources.js b/packages/core/src/tlds/sources.js index 088268e3..84a56203 100644 --- a/packages/core/src/tlds/sources.js +++ b/packages/core/src/tlds/sources.js @@ -56,17 +56,47 @@ export const decodeEntities = (s) => /** * The root zone database page: one table row per label with its type and * manager. The link, not the text, carries the ASCII label: an IDN row shows - * `.рф` and links to `xn--p1ai.html`. "Not assigned" is IANA's word for a - * retired label, kept as the manager so a reader sees it. + * `.рф` and links to `xn--p1ai` (`xn--p1ai.html` before IANA's 2026 redesign). + * "Not assigned" is IANA's word for a retired label, kept as the manager so a + * reader sees it. + * + * Two layouts are read. The old one is `label | type | manager`. The redesign + * (live by 2026-10-21) is `label | status | type | manager | script | ...`, with + * the manager cell marked `col-manager`, types in title case ("Country Code"), + * and a retired label shown as status "Retired" with an empty manager. Types are + * normalised to the old lower-case, dashed vocabulary (`country-code`). */ export function parseRootDb(html) { const out = []; - const re = - /href="\/domains\/root\/db\/([^"/]+)\.html"[^>]*>[\s\S]*?<\/td>\s*([\s\S]*?)<\/td>\s*([\s\S]*?)<\/td>/g; - for (const m of String(html ?? '').matchAll(re)) { - const tld = decodeURIComponent(m[1]).toLowerCase(); - if (!/^[a-z0-9-]+$/.test(tld)) continue; - out.push({ tld, type: decodeEntities(m[2]) || null, manager: decodeEntities(m[3]) || null }); + const seen = new Set(); + const text = (cell) => decodeEntities(cell.replace(/<[^>]*>/g, ' ')); + for (const row of String(html ?? '') + .split(/]*)>([\s\S]*?)<\/td>/gi)]; + const at = cells.findIndex((c) => /col-manager/.test(c[1])); + let type; + let manager; + if (at > 0) { + type = text(cells[at - 1][2]); + manager = text(cells[at][2]); + const status = at > 1 ? text(cells[1][2]) : ''; + if (!manager && /^retired$/i.test(status)) manager = 'Not assigned'; + } else { + if (cells.length < 3) continue; + type = text(cells[1][2]); + manager = text(cells[2][2]); + } + seen.add(tld); + out.push({ + tld, + type: type ? type.toLowerCase().replace(/\s+/g, '-') : null, + manager: manager || null, + }); } return out; } diff --git a/test/tlds.test.js b/test/tlds.test.js index fa6e5846..f54ae807 100644 --- a/test/tlds.test.js +++ b/test/tlds.test.js @@ -145,6 +145,38 @@ describe('IANA parsers', () => { expect(rows.find((r) => r.tld === 'ai').manager).toBe('Government of Anguilla & Co'); }); + test('the root zone database, redesigned: status column, col-manager, title-case types', () => { + const page = ` + + + + + + + + + + + + + + + + + + + + + + +
.aaa Active Generic American Automobile Association, Inc. Latn Latin en||aaa
.ai Active Country Code Government of Anguilla & Co Latn
.测试 Retired Test Hani
`; + expect(src.parseRootDb(page)).toEqual([ + { tld: 'aaa', type: 'generic', manager: 'American Automobile Association, Inc.' }, + { tld: 'ai', type: 'country-code', manager: 'Government of Anguilla & Co' }, + { tld: 'xn--0zwm56d', type: 'test', manager: 'Not assigned' }, + ]); + }); + test('RDAP bootstrap: https wins, trailing slash added', () => { const rows = src.parseRdapBootstrap(RDAP_BOOT); expect(rows).toContainEqual({ tld: 'com', rdap: 'https://rdap.verisign.com/com/v1/' });