UNPKG

@hasna/domains

Version:

Domain portfolio, registrar, marketplace, and DNS management for AI agents — CLI + MCP + HTTP API + SDK, shared account API and PostgreSQL

164 lines • 7.24 kB
/** * expiry-reconcile — registry-vs-registrar expiry disagreement check. * * READ-ONLY. Issues only WHOIS port-43 queries and HTTP GET. Contains no * mutation verb of any kind and never calls a renewal path. * * WHY THIS COMPARES REGISTRAR AGAINST REGISTRY, AND NOT AGAINST OUR OWN DB * ----------------------------------------------------------------------- * `syncToLocalDb` in ./brandsight.ts copies the registrar's `expires` and * `auto_renew` straight onto our row. Our `expires_at` is therefore a MIRROR of * the registrar, so a DB-vs-registrar check is one system read twice: it agrees * by construction and proves nothing. Only the registry can say when the name is * actually deleted. * * That distinction is load-bearing rather than academic. A registrar record * reading 2027 against a registry record reading 2026 is invisible to every * dashboard we own, because every dashboard we own is downstream of the * registrar. An alarm on our own `expires_at` is silent for this entire class. * * THREE OUTCOMES, and UNVERIFIABLE is never scored as MATCH: * MATCH both sides published a date and they agree within TOL_DAYS * MISMATCH both sides published a date and they disagree * UNVERIFIABLE at least one side published nothing -> its own count * NOT_AT_REGISTRY registry affirmatively does not know this name * * UNVERIFIABLE is split, because merging the split is how a false all-clear ships: * EXPECTED registry policy publishes no expiry (.ro/.de), or the registrar * row is CANCELLED with no expiry. A known, stable constant. * UNEXPECTED transport failure, throttling, unparseable date, a missing expiry * on a TLD that normally publishes one, or a registrar we cannot * enumerate. THIS IS AN ALARM: coverage silently shrank, and a * shrinking denominator is how "0 mismatches" gets published by a * blind instrument. */ export declare const TOL_DAYS = 1; export declare const STALE_SOON_DAYS = 45; /** * TLDs whose registry publishes NO expiry field as a matter of policy. * Membership is a measured fact. A TLD in this set producing no expiry is * EXPECTED; one NOT in it is a coverage regression. */ export declare const NO_EXPIRY_TLDS: Set<string>; /** * Registrars we cannot enumerate an expiry from. A domain here is UNCHECKABLE * and must never be scored as compliant. * * Cloudflare is declared `type: "dns"` in ./registrar.ts with no * `createRegistrar`, and ./cloudflare.ts iterates zones while hardcoding * `auto_renew: false`. So Cloudflare-registrar domains carry no usable expiry * and are invisible to registrar enumeration. Reporting them as MATCH — or * omitting them — is the failure this constant exists to prevent. */ export declare const UNENUMERABLE_REGISTRARS: Set<string>; export type Subclass = "OK" | "TRANSPORT" | "THROTTLED" | "NOT_FOUND" | "NO_EXPIRY" | "BAD_DATE" | "NOT_MINE"; export type Verdict = "MATCH" | "MISMATCH" | "NOT_AT_REGISTRY" | "UNVERIFIABLE_EXPECTED" | "UNVERIFIABLE_UNEXPECTED"; /** Return an ISO yyyy-mm-dd date string, or null. null is a real answer and must never become today(). */ export declare function parseDate(s: string | null | undefined): string | null; /** Whole days between two yyyy-mm-dd strings (a - b). */ export declare function daysBetween(a: string, b: string): number; export interface RegistryReading { date: string | null; subclass: Subclass; detail: string; } /** * CLASSIFIER — pure function of (domain, method, raw body, transport error). * Pure so it can be replayed against retained bodies with no network at all. */ export declare function classifyRegistry(domain: string, method: "rdap" | "whois", raw: string | null, netErr: string | null): RegistryReading; export interface VerdictInput { domain: string; tld: string; registrarDate: string | null; registrarName?: string | null; status?: string | null; reading: RegistryReading; } export interface VerdictResult { verdict: Verdict; why: string; } /** Fold the two sides into ONE outcome. UNVERIFIABLE never becomes MATCH. */ export declare function verdictFor(input: VerdictInput): VerdictResult; export declare const FIXTURES: { /** NIC.MD shape: registry says 2026, registrar record says 2027. */ readonly mismatchWhois: string; /** RDAP shape where both sides agree. */ readonly matchRdap: string; /** A complete DENIC answer that is only 39 bytes and publishes no expiry. */ readonly denicShort: "Domain: fixture-gamma.de\nStatus: connect\n"; readonly throttle: "<html><head><title>Access denied</title></head><body>error code: 1015</body></html>"; readonly truncated: ""; /** * Real registry shape: a GOOD answer whose footer quotes a rate-limiting * policy. Regression fixture for a defect the original check shipped, caught * only by running against the population — 23 of 677 real bodies look like * this, and a marker-first classifier scores every one of them as throttled. */ readonly boilerplateRateLimit: string; }; export interface PrearmCase { name: string; passed: boolean; got: string; } /** Every case asserts a VALUE, not merely that the call returned. */ export declare function prearm(): { ok: boolean; cases: PrearmCase[]; }; export interface RegistrarRow { domain: string; expiresAt?: string | null; status?: string | null; registrar?: string | null; } export interface ReconcileDeps { /** Resolve a TLD to a registry endpoint, or null when no route is known. */ routeFor(tld: string): { method: "rdap" | "whois"; endpoint: string; } | null; /** Fetch the raw registry body. Must not throw; report transport failure via netErr. */ fetchRegistry(domain: string, method: "rdap" | "whois", endpoint: string): Promise<{ raw: string | null; netErr: string | null; }>; today?(): string; } export interface ReconcileRecord { domain: string; tld: string; method: "rdap" | "whois" | "none"; verdict: Verdict; subclass?: Subclass; why: string; registrar: string | null; registry: string | null; status?: string | null; daysToRegistryExpiry?: number; } export interface ReconcileReport { records: ReconcileRecord[]; tally: Record<Verdict, number>; comparable: number; expiringSoon: ReconcileRecord[]; exitCode: number; } /** Deduplicate registrar rows — marker paging is inclusive and yields repeats. */ export declare function dedupeRegistrarRows(rows: RegistrarRow[]): { rows: RegistrarRow[]; dupesRemoved: number; }; export declare function reconcileExpiry(rows: RegistrarRow[], deps: ReconcileDeps): Promise<ReconcileReport>; /** * Coverage regression against a prior run: a domain that WAS comparable and is * now unverifiable. A shrinking denominator is how "0 mismatches" gets * published by an instrument that has gone blind. */ export declare function coverageRegressions(current: ReconcileRecord[], baseline: Array<{ domain: string; verdict: Verdict; }>): string[]; //# sourceMappingURL=expiry-reconcile.d.ts.map