fix: prefer the registry's dates when merging WHOIS referrals

Merging a registry and a registrar reply kept the earliest creation and the
latest updated/expiration dates across both, while RDAP reports the registry's.
The same domain therefore differed by source: google.com's updated date was
2019 over RDAP but MarkMonitor's 2024 over WHOIS, and amnic.net's creation came
from a registrar printing local time without a zone. Keep the registry's dates
and fill only what it lacks from the registrar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 5.5 committed 2026-10-03 15:38:06 -04:00
1 parent 7e2b2473e8
commit fb7e279a27
2 files changed
+27 -17

No files matched your search

+20
View File
@@ -57,3 +57,23 @@ test("mergeWhoisRecords keeps the registrar's fuller registrant over the registr
{ type: "registrant", name: "Jane Doe", organization: "Acme Inc", email: "jane@acme.example" },
]);
});
test("mergeWhoisRecords keeps the registry's dates and only fills gaps from the registrar", () => {
const registry = normalizeWhois(
"google.com",
"com",
"Domain Name: GOOGLE.COM\nUpdated Date: 2019-09-09T15:39:04Z\nCreation Date: 1997-09-15T04:00:00Z\n",
"whois.verisign-grs.com",
);
const registrar = normalizeWhois(
"google.com",
"com",
"Domain Name: google.com\nUpdated Date: 2024-08-02T02:17:33+0000\nCreation Date: 1997-09-15T07:00:00+0000\nRegistrar Registration Expiration Date: 2028-09-13T07:00:00+0000\n",
"whois.markmonitor.com",
);
const merged = mergeWhoisRecords(registry, [registrar]);
expect(merged.updatedDate).toBe("2019-09-09T15:39:04Z");
expect(merged.creationDate).toBe("1997-09-15T04:00:00Z");
// The registry gave no expiration, so the registrar's fills it
expect(merged.expirationDate).toBe("2028-09-13T07:00:00Z");
});
+7 -17
View File
@@ -15,7 +15,7 @@ function dedupeStatuses(a?: DomainRecord["statuses"], b?: DomainRecord["statuses
return out.length ? out : undefined;
}
/** Conservative merge: start with base; fill missing scalars; union arrays; prefer more informative dates. */
/** Conservative merge: start with the registry's record (base); fill missing scalars and dates; union arrays. */
export function mergeWhoisRecords(base: DomainRecord, others: DomainRecord[]): DomainRecord {
const merged: DomainRecord = { ...base };
for (const cur of others) {
@@ -24,10 +24,12 @@ export function mergeWhoisRecords(base: DomainRecord, others: DomainRecord[]): D
merged.registrar = merged.registrar ?? cur.registrar;
merged.reseller = merged.reseller ?? cur.reseller;
merged.statuses = dedupeStatuses(merged.statuses, cur.statuses);
// Dates: prefer earliest creation, latest updated/expiration when available
merged.creationDate = preferEarliestIso(merged.creationDate, cur.creationDate);
merged.updatedDate = preferLatestIso(merged.updatedDate, cur.updatedDate);
merged.expirationDate = preferLatestIso(merged.expirationDate, cur.expirationDate);
// Dates: the registry's (the base record's) are authoritative, as in RDAP; a registrar's
// reply only fills in what the registry lacks. Its own "Updated Date" tracks its records, and
// some registrars print local times without a zone.
merged.creationDate = merged.creationDate ?? cur.creationDate;
merged.updatedDate = merged.updatedDate ?? cur.updatedDate;
merged.expirationDate = merged.expirationDate ?? cur.expirationDate;
merged.deletionDate = merged.deletionDate ?? cur.deletionDate;
merged.transferLock = Boolean(merged.transferLock || cur.transferLock);
merged.dnssec = merged.dnssec ?? cur.dnssec;
@@ -41,15 +43,3 @@ export function mergeWhoisRecords(base: DomainRecord, others: DomainRecord[]): D
}
return merged;
}
function preferEarliestIso(a?: string, b?: string): string | undefined {
if (!a) return b;
if (!b) return a;
return new Date(a) <= new Date(b) ? a : b;
}
function preferLatestIso(a?: string, b?: string): string | undefined {
if (!a) return b;
if (!b) return a;
return new Date(a) >= new Date(b) ? a : b;
}