From 02ef7f61b258474b34ce4ae3082be066af3172ba Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Wed, 24 Sep 2025 20:51:25 -0400 Subject: [PATCH] Refactor domain registration handling to use rdapper and remove WHOIS dependencies --- .nvmrc | 2 +- app/[domain]/page.tsx | 8 +- components/domain/domain-report-view.tsx | 32 ++- components/domain/export-data.ts | 2 +- .../domain/sections/registration-section.tsx | 69 ++++- hooks/use-domain-queries.ts | 16 +- package.json | 18 +- pnpm-lock.yaml | 258 +++++++++--------- server/prefetch/domain.ts | 14 +- server/routers/domain.ts | 7 +- server/services/rdap-bootstrap.ts | 30 -- server/services/rdap-parser.ts | 116 -------- server/services/rdap.ts | 118 -------- server/services/registration.ts | 52 ++++ server/services/whois.ts | 152 ----------- 15 files changed, 294 insertions(+), 600 deletions(-) delete mode 100644 server/services/rdap-bootstrap.ts delete mode 100644 server/services/rdap-parser.ts delete mode 100644 server/services/rdap.ts create mode 100644 server/services/registration.ts delete mode 100644 server/services/whois.ts diff --git a/.nvmrc b/.nvmrc index 3a6161c..ed27c90 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.19.0 \ No newline at end of file +22.20.0 \ No newline at end of file diff --git a/app/[domain]/page.tsx b/app/[domain]/page.tsx index 1cd6bc6..bc9b047 100644 --- a/app/[domain]/page.tsx +++ b/app/[domain]/page.tsx @@ -6,7 +6,7 @@ import { DomainReportView } from "@/components/domain/domain-report-view"; import { DomainSsrAnalytics } from "@/components/domain/domain-ssr-analytics"; import { normalizeDomainInput } from "@/lib/domain"; import { toRegistrableDomain } from "@/lib/domain-server"; -import { prefetchWhois } from "@/server/prefetch/domain"; +import { prefetchRegistration } from "@/server/prefetch/domain"; export const experimental_ppr = true; @@ -43,7 +43,7 @@ export default async function DomainPage({ // Preserve PPR by isolating cookie read & analytics to a dynamic island - const whois = await prefetchWhois(normalized); + const registration = await prefetchRegistration(normalized); return (
@@ -55,8 +55,8 @@ export default async function DomainPage({ />
diff --git a/components/domain/domain-report-view.tsx b/components/domain/domain-report-view.tsx index b972f1f..9632b28 100644 --- a/components/domain/domain-report-view.tsx +++ b/components/domain/domain-report-view.tsx @@ -2,6 +2,7 @@ import { Download, ExternalLink } from "lucide-react"; import Link from "next/link"; +import type { DomainRecord } from "rdapper"; import { Accordion } from "@/components/ui/accordion"; import { Button } from "@/components/ui/button"; import { captureClient } from "@/lib/analytics/client"; @@ -20,24 +21,28 @@ import { RegistrationSection } from "./sections/registration-section"; export function DomainReportView({ domain, - initialWhois, + initialRegistration, initialRegistered, }: { domain: string; - initialWhois?: import("@/server/services/rdap-parser").Whois; + initialRegistration?: DomainRecord; initialRegistered?: boolean; }) { - const { whois, dns, hosting, certs, headers, allSectionsReady } = - useDomainQueries(domain, { initialWhois, initialRegistered }); + const { registration, dns, hosting, certs, headers, allSectionsReady } = + useDomainQueries(domain, { initialRegistration, initialRegistered }); const { showTtls, setShowTtls } = useTtlPreferences(); // Manage domain history - useDomainHistory(domain, whois.isSuccess, whois.data?.registered ?? false); + useDomainHistory( + domain, + registration.isSuccess, + registration.data?.isRegistered ?? false, + ); const handleExportJson = () => { captureClient("export_json_clicked", { domain }); exportDomainData(domain, { - whois: whois.data, + registration: registration.data, dns: dns.data, hosting: hosting.data, certificates: certs.data, @@ -46,12 +51,13 @@ export function DomainReportView({ }; // Show loading state until WHOIS completes - if (whois.isLoading) { + if (registration.isLoading) { return ; } // Show unregistered state if domain is not registered - const isUnregistered = whois.isSuccess && whois.data?.registered === false; + const isUnregistered = + registration.isSuccess && registration.data?.isRegistered === false; if (isUnregistered) { captureClient("report_unregistered_viewed", { domain }); return ; @@ -92,15 +98,15 @@ export function DomainReportView({ { captureClient("section_refetch_clicked", { domain, - section: "whois", + section: "registration", }); - whois.refetch(); + registration.refetch(); }} /> diff --git a/components/domain/export-data.ts b/components/domain/export-data.ts index ce4740b..a1bc5e7 100644 --- a/components/domain/export-data.ts +++ b/components/domain/export-data.ts @@ -1,5 +1,5 @@ interface ExportData { - whois: unknown; + registration: unknown; dns: unknown; hosting: unknown; certificates: unknown; diff --git a/components/domain/sections/registration-section.tsx b/components/domain/sections/registration-section.tsx index 6a592f2..e3bee75 100644 --- a/components/domain/sections/registration-section.tsx +++ b/components/domain/sections/registration-section.tsx @@ -1,5 +1,6 @@ "use client"; +import type { DomainRecord } from "rdapper"; import { ErrorWithRetry } from "@/components/domain/error-with-retry"; import { Favicon } from "@/components/domain/favicon"; import { KeyValue } from "@/components/domain/key-value"; @@ -7,10 +8,11 @@ import { Section } from "@/components/domain/section"; import { Skeletons } from "@/components/domain/skeletons"; import { Badge } from "@/components/ui/badge"; import { formatDate, formatRegistrant } from "@/lib/format"; +import { resolveRegistrarDomain } from "@/lib/providers/detection"; import { SECTION_DEFS } from "./sections-meta"; -type Registrar = { name: string; domain: string | null }; -type Registrant = { organization: string; country: string; state?: string }; +type RegistrarView = { name: string; domain: string | null }; +type RegistrantView = { organization: string; country: string; state?: string }; export function RegistrationSection({ data, @@ -18,18 +20,21 @@ export function RegistrationSection({ isError, onRetry, }: { - data?: { - registrar: Registrar; - creationDate: string; - expirationDate: string; - registrant: Registrant; - source?: string; - } | null; + data?: DomainRecord | null; isLoading: boolean; isError: boolean; onRetry: () => void; }) { const Def = SECTION_DEFS.registration; + const registrar: RegistrarView | null = data + ? { + name: (data.registrar?.name || "").trim() || "Unknown", + domain: deriveRegistrarDomain(data), + } + : null; + const registrant: RegistrantView | null = data + ? extractRegistrantView(data) + : null; return (
@@ -59,11 +64,19 @@ export function RegistrationSection({ } /> - - + + ) : isError ? ( @@ -74,3 +87,29 @@ export function RegistrationSection({
); } + +function deriveRegistrarDomain(record: DomainRecord): string | null { + const url = record.registrar?.url; + if (url) { + try { + const host = new URL(url).hostname; + return host || null; + } catch {} + } + return resolveRegistrarDomain(record.registrar?.name || ""); +} + +function extractRegistrantView(record: DomainRecord): RegistrantView | null { + const registrant = record.contacts?.find((c) => c.type === "registrant"); + if (!registrant) return null; + const organization = + (registrant.organization || registrant.name || "").toString().trim() || + "Unknown"; + const country = ( + registrant.country || + registrant.countryCode || + "" + ).toString(); + const state = (registrant.state || "").toString() || undefined; + return { organization, country, state }; +} diff --git a/hooks/use-domain-queries.ts b/hooks/use-domain-queries.ts index dd4264f..d545d83 100644 --- a/hooks/use-domain-queries.ts +++ b/hooks/use-domain-queries.ts @@ -1,9 +1,9 @@ import { useQuery } from "@tanstack/react-query"; +import type { DomainRecord } from "rdapper"; import { useTRPC } from "@/lib/trpc/client"; -import type { Whois } from "@/server/services/rdap-parser"; type UseDomainQueriesOptions = { - initialWhois?: Whois; + initialRegistration?: DomainRecord; initialRegistered?: boolean; }; @@ -12,12 +12,12 @@ export function useDomainQueries( opts?: UseDomainQueriesOptions, ) { const trpc = useTRPC(); - const whois = useQuery( - trpc.domain.whois.queryOptions( + const registration = useQuery( + trpc.domain.registration.queryOptions( { domain }, { enabled: !!domain, - initialData: opts?.initialWhois, + initialData: opts?.initialRegistration, staleTime: 30 * 60_000, // 30 minutes, avoid churn refetchOnWindowFocus: false, refetchOnReconnect: false, @@ -27,7 +27,7 @@ export function useDomainQueries( ); const registered = - (opts?.initialRegistered ?? whois.data?.registered) === true; + (opts?.initialRegistered ?? registration.data?.isRegistered) === true; const dns = useQuery( trpc.domain.dns.queryOptions( @@ -82,7 +82,7 @@ export function useDomainQueries( ); const allSectionsReady = - whois.isSuccess && + registration.isSuccess && registered && dns.isSuccess && hosting.isSuccess && @@ -90,7 +90,7 @@ export function useDomainQueries( headers.isSuccess; return { - whois, + registration, dns, hosting, certs, diff --git a/package.json b/package.json index d60da5a..d96441b 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@tanstack/react-query": "^5.90.1", - "@tanstack/react-query-devtools": "^5.90.1", + "@tanstack/react-query": "^5.90.2", + "@tanstack/react-query-devtools": "^5.90.2", "@trpc/client": "^11.5.1", "@trpc/server": "^11.5.1", "@trpc/tanstack-react-query": "^11.5.1", @@ -33,16 +33,17 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", - "framer-motion": "^12.23.18", + "framer-motion": "^12.23.21", "icojs": "^0.19.5", "ipaddr.js": "^2.2.0", "lucide-react": "^0.544.0", "mapbox-gl": "^3.15.0", - "next": "15.6.0-canary.20", + "next": "15.6.0-canary.28", "next-themes": "^0.4.6", - "posthog-js": "^1.268.0", - "posthog-node": "^5.8.6", + "posthog-js": "^1.268.4", + "posthog-node": "^5.9.1", "radix-ui": "^1.4.3", + "rdapper": "^0.1.0", "react": "19.1.1", "react-dom": "19.1.1", "react-map-gl": "^8.0.4", @@ -51,10 +52,9 @@ "sonner": "^2.0.7", "superjson": "^2.2.2", "tailwind-merge": "^3.3.1", - "tldts": "^7.0.15", + "tldts": "^7.0.16", "uuid": "^13.0.0", "vaul": "^1.1.2", - "whoiser": "2.0.0-beta.8", "zod": "^4.1.11" }, "devDependencies": { @@ -65,7 +65,7 @@ "@types/react-dom": "19.1.9", "babel-plugin-react-compiler": "19.1.0-rc.3", "tailwindcss": "^4.1.13", - "tw-animate-css": "^1.3.8", + "tw-animate-css": "^1.4.0", "typescript": "5.9.2" }, "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1ab069..30e495b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,11 +9,11 @@ importers: .: dependencies: '@tanstack/react-query': - specifier: ^5.90.1 - version: 5.90.1(react@19.1.1) + specifier: ^5.90.2 + version: 5.90.2(react@19.1.1) '@tanstack/react-query-devtools': - specifier: ^5.90.1 - version: 5.90.1(@tanstack/react-query@5.90.1(react@19.1.1))(react@19.1.1) + specifier: ^5.90.2 + version: 5.90.2(@tanstack/react-query@5.90.2(react@19.1.1))(react@19.1.1) '@trpc/client': specifier: ^11.5.1 version: 11.5.1(@trpc/server@11.5.1(typescript@5.9.2))(typescript@5.9.2) @@ -22,13 +22,13 @@ importers: version: 11.5.1(typescript@5.9.2) '@trpc/tanstack-react-query': specifier: ^11.5.1 - version: 11.5.1(@tanstack/react-query@5.90.1(react@19.1.1))(@trpc/client@11.5.1(@trpc/server@11.5.1(typescript@5.9.2))(typescript@5.9.2))(@trpc/server@11.5.1(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2) + version: 11.5.1(@tanstack/react-query@5.90.2(react@19.1.1))(@trpc/client@11.5.1(@trpc/server@11.5.1(typescript@5.9.2))(typescript@5.9.2))(@trpc/server@11.5.1(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2) '@upstash/redis': specifier: ^1.35.4 version: 1.35.4 '@vercel/analytics': specifier: ^1.5.0 - version: 1.5.0(next@15.6.0-canary.20(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) + version: 1.5.0(next@15.6.0-canary.28(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1) '@vercel/blob': specifier: ^2.0.0 version: 2.0.0 @@ -48,8 +48,8 @@ importers: specifier: ^4.1.0 version: 4.1.0 framer-motion: - specifier: ^12.23.18 - version: 12.23.18(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: ^12.23.21 + version: 12.23.21(react-dom@19.1.1(react@19.1.1))(react@19.1.1) icojs: specifier: ^0.19.5 version: 0.19.5(@jimp/custom@0.22.12) @@ -63,20 +63,23 @@ importers: specifier: ^3.15.0 version: 3.15.0 next: - specifier: 15.6.0-canary.20 - version: 15.6.0-canary.20(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: 15.6.0-canary.28 + version: 15.6.0-canary.28(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) posthog-js: - specifier: ^1.268.0 - version: 1.268.0 + specifier: ^1.268.4 + version: 1.268.4 posthog-node: - specifier: ^5.8.6 - version: 5.8.6 + specifier: ^5.9.1 + version: 5.9.1 radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + rdapper: + specifier: ^0.1.0 + version: 0.1.0 react: specifier: 19.1.1 version: 19.1.1 @@ -102,17 +105,14 @@ importers: specifier: ^3.3.1 version: 3.3.1 tldts: - specifier: ^7.0.15 - version: 7.0.15 + specifier: ^7.0.16 + version: 7.0.16 uuid: specifier: ^13.0.0 version: 13.0.0 vaul: specifier: ^1.1.2 version: 1.1.2(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - whoiser: - specifier: 2.0.0-beta.8 - version: 2.0.0-beta.8 zod: specifier: ^4.1.11 version: 4.1.11 @@ -139,8 +139,8 @@ importers: specifier: ^4.1.13 version: 4.1.13 tw-animate-css: - specifier: ^1.3.8 - version: 1.3.8 + specifier: ^1.4.0 + version: 1.4.0 typescript: specifier: 5.9.2 version: 5.9.2 @@ -431,59 +431,59 @@ packages: resolution: {integrity: sha512-cOZZOVhDSulgK0meTsTkmNXb1ahVvmTmWmfx9gRBwc6hq98wS9JP35ESIoNq3xqEan+UN+gn8187Z6E4NKhLsw==} hasBin: true - '@next/env@15.6.0-canary.20': - resolution: {integrity: sha512-+ljGWYCPxG5SNlTecwlcVcBnARQNv/CjzD73VlJg2oMvRnVrLCr+1zrjY1KnOVF4KsDxVTCD52V92YeAaJojNw==} + '@next/env@15.6.0-canary.28': + resolution: {integrity: sha512-KSh4jFpun92TgB5Y7j8FRIOaJ2ycEdlvDK+ywgDWRT778b2tnzWI1bvUQw2cAhmbeOnmFoF1z/Rc8IInAY3KJg==} - '@next/swc-darwin-arm64@15.6.0-canary.20': - resolution: {integrity: sha512-UFv71kNjbKhzdd7nd6f4UKALqKzal/f+dZ9X/ld9rfUmE/sVsCpBqbzu/Uw8KtGVwz1TKcsuR5A+p79SRhY39Q==} + '@next/swc-darwin-arm64@15.6.0-canary.28': + resolution: {integrity: sha512-CFQIO001er7RJraTaBSOqG0r+AQJNgmZT+SKl166hh9x2gRN+2IVi6jGm6UFrXQthzaSy20nRVQTzOT9MjU82g==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.6.0-canary.20': - resolution: {integrity: sha512-Re+46/ZpzquBczPruty09ywO/uTVo2i6yeCr1+x7YpgEj/7jevIIOr9qHoWtTxdceco8+NwmjyPX3jKwqK/IEQ==} + '@next/swc-darwin-x64@15.6.0-canary.28': + resolution: {integrity: sha512-h6lOh7sOYd8NaneX3UJciYZNmLaAlytN6nY3o6pncfEPabiQvOJR3VKooRZZwpQIDw99p4RpuL92jRLZDhx0uQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.6.0-canary.20': - resolution: {integrity: sha512-NdReZ2W87z8HttNuWgDPlcpBkQdzaG0WfB2KCwH17mT3NNhaZ3WCrRfp1FICiMIB/TjNi8ewjqYb+7J4vrslBg==} + '@next/swc-linux-arm64-gnu@15.6.0-canary.28': + resolution: {integrity: sha512-T5qv8Sac34MASCagLhxHWW9i6zE4lxOBdKJLbOjqYRPLFXO7Nhw4PpVYRg1aMQMzmk8jK//U5LO1CX7UB7/vIQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.6.0-canary.20': - resolution: {integrity: sha512-3ItqqT6eRyz3tTtO0H+s/ZnmHWLioNxvNQ+NrCopprMQX4aCqT14g8juSCWyCxUpUCJewu1qzH1b8MG/w49ynA==} + '@next/swc-linux-arm64-musl@15.6.0-canary.28': + resolution: {integrity: sha512-FPzXhh7wuTPzSJ69kVqS9IcT08cEkn94qdRwdvidl+NbUCWlR8t4XRAG5yqTwvq8qKUWTQwv49zGZI/CKtAnbw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.6.0-canary.20': - resolution: {integrity: sha512-4MwMRDSdkDk1URFfq8Jh4N2Ck6BY9QjSVukV0PqaF1BIncOdSd+OhdmawAI5h3GAmyhkEXajmqGz33U66uEHAg==} + '@next/swc-linux-x64-gnu@15.6.0-canary.28': + resolution: {integrity: sha512-gt5weHg0VbcOeIZqJVzm8xy4DoYnQsMQamdCw3ypoUVnP/Ij+a6B37o6puyNUmkr3aHwwjkxQN74IjO3BZvwWQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.6.0-canary.20': - resolution: {integrity: sha512-XLGkiwp5z7BUd6DbpAkU+Y12JbckzEhwcRmPC9LMWgPzZovsFscjrDyTmYuyRMqaq2qKP+TmXWBwkHvalH8JEw==} + '@next/swc-linux-x64-musl@15.6.0-canary.28': + resolution: {integrity: sha512-pWozTRnEqZusLgbOhdzU0L2PZhlhZbXTb60xQ1IRTatv8aIIxE1jZx3AWpRJiprHRfkwYaqvYN7ZvGtSC+ZcHA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.6.0-canary.20': - resolution: {integrity: sha512-rRmwdrIt4g/oX9m/oOiOvXf35cwmyDUbAgSJeE/sB5QZYz7dOgx7Cfj3K5YJJ8fYPCVIO9cALQCeWZuvIrVCBw==} + '@next/swc-win32-arm64-msvc@15.6.0-canary.28': + resolution: {integrity: sha512-B54DtCQV0lBVH4PvWEfAszUk0TEvtnssyArEX8IRHw70AzHEGTk7qXf0oHhIikL2OxbqxcaK41kxaIpBn/cPnQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.6.0-canary.20': - resolution: {integrity: sha512-3jfmbFAOLgRvqs5TKclq3u25lS7ctB/RwLiflbCq8pd9rmu0kIoUlFQP8kiX67bNLSv/p6tWYCd1XMEbyMRn2w==} + '@next/swc-win32-x64-msvc@15.6.0-canary.28': + resolution: {integrity: sha512-SYZb0YGH/XRZ0IZZ5MJhSXAuMP4iYoXzgJldTlYuJ0kcbwJ2DobsooO6hoPmHwywxkRbhGe7hUEi6Fu9u/UXQA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@posthog/core@1.1.0': - resolution: {integrity: sha512-igElrcnRPJh2nWYACschjH4OwGwzSa6xVFzRDVzpnjirUivdJ8nv4hE+H31nvwE56MFhvvglfHuotnWLMcRW7w==} + '@posthog/core@1.2.1': + resolution: {integrity: sha512-zNw96BipqM5/Tf161Q8/K5zpwGY3ezfb2wz+Yc3fIT5OQHW8eEzkQldPgtFKMUkqImc73ukEa2IdUpS6vEGH7w==} '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -1269,20 +1269,20 @@ packages: '@tailwindcss/postcss@4.1.13': resolution: {integrity: sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==} - '@tanstack/query-core@5.90.1': - resolution: {integrity: sha512-hmi8i+mWP3QnD8yq3+6LWri9IEZAlFbpbM/UVB+TJtp5RIxUfzuARqyW39b+HCfBKKnFKSHWMXNB5YN8lo/E/Q==} + '@tanstack/query-core@5.90.2': + resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} '@tanstack/query-devtools@5.90.1': resolution: {integrity: sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==} - '@tanstack/react-query-devtools@5.90.1': - resolution: {integrity: sha512-otOjczcUft13RZlWi8JHIWei1MLuRf0AK+hyEEAm0/IqKUbs/0ZXj0wlc+P1XdE0nYcOMuHK69496FvP/9g3LQ==} + '@tanstack/react-query-devtools@5.90.2': + resolution: {integrity: sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ==} peerDependencies: - '@tanstack/react-query': ^5.90.1 + '@tanstack/react-query': ^5.90.2 react: ^18 || ^19 - '@tanstack/react-query@5.90.1': - resolution: {integrity: sha512-tN7Fx2HuV2SBhl+STgL8enbfSInRoNU1B1+5LIU62klcMElE4lFzol4aReuRSUeD6ntzPayK0KrM6w9+ZlHEkw==} + '@tanstack/react-query@5.90.2': + resolution: {integrity: sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==} peerDependencies: react: ^18 || ^19 @@ -1498,8 +1498,8 @@ packages: resolution: {integrity: sha512-69NZfbKIzux1vBOd31al3XnMnH+2mqDhEgLdpygErm4d60N+UwA5Sq5WFjmEDQzumgB9fElojGwWG0vybVfFmA==} engines: {node: '>=8.6'} - detect-libc@2.1.0: - resolution: {integrity: sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==} + detect-libc@2.1.1: + resolution: {integrity: sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==} engines: {node: '>=8'} detect-node-es@1.1.0: @@ -1542,8 +1542,8 @@ packages: resolution: {integrity: sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==} engines: {node: '>=18'} - framer-motion@12.23.18: - resolution: {integrity: sha512-HBVXBL5x3nk/0WrYM5G4VgjBey99ytVYET5AX17s/pcnlH90cyaxVUqgoN8cpF4+PqZRVOhwWsv28F+hxA9Tzg==} + framer-motion@12.23.21: + resolution: {integrity: sha512-UWDtzzPdRA3UpSNGril5HjUtPF1Uo/BCt5VKG/YQ8tVpSkAZ22+q8o+hYO0C1uDAZuotQjcfzsTsDtQxD46E/Q==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -1727,8 +1727,8 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} - motion-dom@12.23.18: - resolution: {integrity: sha512-9piw3uOcP6DpS0qpnDF95bLDzmgMxLOg/jghLnHwYJ0YFizzuvbH/L8106dy39JNgHYmXFUTztoP9JQvUqlBwQ==} + motion-dom@12.23.21: + resolution: {integrity: sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==} motion-utils@12.23.6: resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} @@ -1750,8 +1750,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@15.6.0-canary.20: - resolution: {integrity: sha512-FzC5rYa5JgeITRnWX69kqrwM2xgaDlSO1EoPDtmMewpAH/H5Yh1D7+MaYQr+cyfDM0luSpqD6PJd2ej8950RTw==} + next@15.6.0-canary.28: + resolution: {integrity: sha512-xy9s2B5ztJXhUV+sGV6Us42SI38h0rtWqhls9rWD9VSVzdukX+6WvvkSOP6YwB3oiF0aung+u1/GOPK5nNBIQg==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -1815,8 +1815,8 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - posthog-js@1.268.0: - resolution: {integrity: sha512-rEtziXONYXi+KKXBTzkxCTsHHKohLQvyAF2uEdXMwmL1vLW+f9rbroa2XuA9QUrvfboJXb5Pvysa+HnFnWnUcw==} + posthog-js@1.268.4: + resolution: {integrity: sha512-kbE8SeH4Hi6uETEzO4EVANULz1ncw+PXC/SMfDdByf4Qt0a/AKoxjlGCZwHuZuflQmBfTwwQcjHeQxnmIxti1A==} peerDependencies: '@rrweb/types': 2.0.0-alpha.17 rrweb-snapshot: 2.0.0-alpha.17 @@ -1826,8 +1826,8 @@ packages: rrweb-snapshot: optional: true - posthog-node@5.8.6: - resolution: {integrity: sha512-iQFj/cop5yZONzorCF35m9GgXHH0wSkDikJ/DNVBvY7Kbh+1oX1C0x20EsGMi/eJBWskdqyu4a+GuTIjeEpSrw==} + posthog-node@5.9.1: + resolution: {integrity: sha512-Tydweh2Q3s2dy1b77NOYOaBfphSUNd6zmEPbU7yCuWnz8vU0nk2jObDRUQClTMGJZnr+HSj6ZVWvosrAN1d1dQ==} engines: {node: '>=20'} potpack@2.1.0: @@ -1843,9 +1843,12 @@ packages: protocol-buffers-schema@3.6.0: resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} - punycode-esm@1.0.15: - resolution: {integrity: sha512-pR7pzaunGU4g3v3vMIXD9WnrGUiEBs2ezcVYr9piTC/HVem9F+MJ+JNdSeYD9pK7EumLMUMO9F3Gsa3RS+o7pA==} - engines: {node: '>=20'} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} quickselect@3.0.0: resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} @@ -1863,6 +1866,10 @@ packages: '@types/react-dom': optional: true + rdapper@0.1.0: + resolution: {integrity: sha512-J5OymROOxvLICZJl7tL0vjj5HmjPdu59btZ+RY0FRkG+bvP1sW+x3Vwi+sQuCF7SsYZM0QdegpA1RP32+q57Uw==} + engines: {node: '>=18.17'} + react-dom@19.1.1: resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} peerDependencies: @@ -2035,8 +2042,8 @@ packages: resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} engines: {node: '>=6'} - tar@7.4.4: - resolution: {integrity: sha512-O1z7ajPkjTgEgmTGz0v9X4eqeEXTDREPTO77pVC1Nbs86feBU1Zhdg+edzavPmYW1olxkwsqA2v4uOw6E8LeDg==} + tar@7.5.1: + resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} engines: {node: '>=18'} throttleit@2.1.0: @@ -2052,11 +2059,11 @@ packages: tinyqueue@3.0.0: resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==} - tldts-core@7.0.15: - resolution: {integrity: sha512-YBkp2VfS9VTRMPNL2PA6PMESmxV1JEVoAr5iBlZnB5JG3KUrWzNCB3yNNkRa2FZkqClaBgfNYCp8PgpYmpjkZw==} + tldts-core@7.0.16: + resolution: {integrity: sha512-XHhPmHxphLi+LGbH0G/O7dmUH9V65OY20R7vH8gETHsp5AZCjBk9l8sqmRKLaGOxnETU7XNSDUPtewAy/K6jbA==} - tldts@7.0.15: - resolution: {integrity: sha512-heYRCiGLhtI+U/D0V8YM3QRwPfsLJiP+HX+YwiHZTnWzjIKC+ZCxQRYlzvOoTEc6KIP62B1VeAN63diGCng2hg==} + tldts@7.0.16: + resolution: {integrity: sha512-5bdPHSwbKTeHmXrgecID4Ljff8rQjv7g8zKQPkCozRo2HWWni+p310FSn5ImI+9kWw9kK4lzOB5q/a6iv0IJsw==} hasBin: true to-data-view@1.1.0: @@ -2080,8 +2087,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tw-animate-css@1.3.8: - resolution: {integrity: sha512-Qrk3PZ7l7wUcGYhwZloqfkWCmaXZAoqjkdbIDvzfGshwGtexa/DAs9koXxIkrpEasyevandomzCBAV1Yyop5rw==} + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} @@ -2159,9 +2166,6 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - whoiser@2.0.0-beta.8: - resolution: {integrity: sha512-b5URu8ZGLfLRimfsvYifj6qe/R9a3FGBrnmFwcVa/c5pykb9R/bYsUIwhTzaVzcVx5liVLe3UTV6UUUt6HeYnA==} - yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -2412,33 +2416,33 @@ snapshots: rw: 1.3.3 sort-object: 3.0.3 - '@next/env@15.6.0-canary.20': {} + '@next/env@15.6.0-canary.28': {} - '@next/swc-darwin-arm64@15.6.0-canary.20': + '@next/swc-darwin-arm64@15.6.0-canary.28': optional: true - '@next/swc-darwin-x64@15.6.0-canary.20': + '@next/swc-darwin-x64@15.6.0-canary.28': optional: true - '@next/swc-linux-arm64-gnu@15.6.0-canary.20': + '@next/swc-linux-arm64-gnu@15.6.0-canary.28': optional: true - '@next/swc-linux-arm64-musl@15.6.0-canary.20': + '@next/swc-linux-arm64-musl@15.6.0-canary.28': optional: true - '@next/swc-linux-x64-gnu@15.6.0-canary.20': + '@next/swc-linux-x64-gnu@15.6.0-canary.28': optional: true - '@next/swc-linux-x64-musl@15.6.0-canary.20': + '@next/swc-linux-x64-musl@15.6.0-canary.28': optional: true - '@next/swc-win32-arm64-msvc@15.6.0-canary.20': + '@next/swc-win32-arm64-msvc@15.6.0-canary.28': optional: true - '@next/swc-win32-x64-msvc@15.6.0-canary.20': + '@next/swc-win32-x64-msvc@15.6.0-canary.28': optional: true - '@posthog/core@1.1.0': {} + '@posthog/core@1.2.1': {} '@radix-ui/number@1.1.1': {} @@ -3241,8 +3245,8 @@ snapshots: '@tailwindcss/oxide@4.1.13': dependencies: - detect-libc: 2.1.0 - tar: 7.4.4 + detect-libc: 2.1.1 + tar: 7.5.1 optionalDependencies: '@tailwindcss/oxide-android-arm64': 4.1.13 '@tailwindcss/oxide-darwin-arm64': 4.1.13 @@ -3265,19 +3269,19 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.13 - '@tanstack/query-core@5.90.1': {} + '@tanstack/query-core@5.90.2': {} '@tanstack/query-devtools@5.90.1': {} - '@tanstack/react-query-devtools@5.90.1(@tanstack/react-query@5.90.1(react@19.1.1))(react@19.1.1)': + '@tanstack/react-query-devtools@5.90.2(@tanstack/react-query@5.90.2(react@19.1.1))(react@19.1.1)': dependencies: '@tanstack/query-devtools': 5.90.1 - '@tanstack/react-query': 5.90.1(react@19.1.1) + '@tanstack/react-query': 5.90.2(react@19.1.1) react: 19.1.1 - '@tanstack/react-query@5.90.1(react@19.1.1)': + '@tanstack/react-query@5.90.2(react@19.1.1)': dependencies: - '@tanstack/query-core': 5.90.1 + '@tanstack/query-core': 5.90.2 react: 19.1.1 '@tokenizer/token@0.3.0': {} @@ -3291,9 +3295,9 @@ snapshots: dependencies: typescript: 5.9.2 - '@trpc/tanstack-react-query@11.5.1(@tanstack/react-query@5.90.1(react@19.1.1))(@trpc/client@11.5.1(@trpc/server@11.5.1(typescript@5.9.2))(typescript@5.9.2))(@trpc/server@11.5.1(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2)': + '@trpc/tanstack-react-query@11.5.1(@tanstack/react-query@5.90.2(react@19.1.1))(@trpc/client@11.5.1(@trpc/server@11.5.1(typescript@5.9.2))(typescript@5.9.2))(@trpc/server@11.5.1(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2)': dependencies: - '@tanstack/react-query': 5.90.1(react@19.1.1) + '@tanstack/react-query': 5.90.2(react@19.1.1) '@trpc/client': 11.5.1(@trpc/server@11.5.1(typescript@5.9.2))(typescript@5.9.2) '@trpc/server': 11.5.1(typescript@5.9.2) react: 19.1.1 @@ -3332,9 +3336,9 @@ snapshots: dependencies: uncrypto: 0.1.3 - '@vercel/analytics@1.5.0(next@15.6.0-canary.20(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)': + '@vercel/analytics@1.5.0(next@15.6.0-canary.28(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(react@19.1.1)': optionalDependencies: - next: 15.6.0-canary.20(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + next: 15.6.0-canary.28(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react: 19.1.1 '@vercel/blob@2.0.0': @@ -3461,7 +3465,7 @@ snapshots: decode-bmp: 0.2.1 to-data-view: 1.1.0 - detect-libc@2.1.0: {} + detect-libc@2.1.1: {} detect-node-es@1.1.0: {} @@ -3502,9 +3506,9 @@ snapshots: token-types: 6.1.1 uint8array-extras: 1.5.0 - framer-motion@12.23.18(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + framer-motion@12.23.21(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - motion-dom: 12.23.18 + motion-dom: 12.23.21 motion-utils: 12.23.6 tslib: 2.8.1 optionalDependencies: @@ -3610,7 +3614,7 @@ snapshots: lightningcss@1.30.1: dependencies: - detect-libc: 2.1.0 + detect-libc: 2.1.1 optionalDependencies: lightningcss-darwin-arm64: 1.30.1 lightningcss-darwin-x64: 1.30.1 @@ -3675,7 +3679,7 @@ snapshots: dependencies: minipass: 7.1.2 - motion-dom@12.23.18: + motion-dom@12.23.21: dependencies: motion-utils: 12.23.6 @@ -3692,9 +3696,9 @@ snapshots: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - next@15.6.0-canary.20(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + next@15.6.0-canary.28(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - '@next/env': 15.6.0-canary.20 + '@next/env': 15.6.0-canary.28 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001743 postcss: 8.4.31 @@ -3702,14 +3706,14 @@ snapshots: react-dom: 19.1.1(react@19.1.1) styled-jsx: 5.1.6(react@19.1.1) optionalDependencies: - '@next/swc-darwin-arm64': 15.6.0-canary.20 - '@next/swc-darwin-x64': 15.6.0-canary.20 - '@next/swc-linux-arm64-gnu': 15.6.0-canary.20 - '@next/swc-linux-arm64-musl': 15.6.0-canary.20 - '@next/swc-linux-x64-gnu': 15.6.0-canary.20 - '@next/swc-linux-x64-musl': 15.6.0-canary.20 - '@next/swc-win32-arm64-msvc': 15.6.0-canary.20 - '@next/swc-win32-x64-msvc': 15.6.0-canary.20 + '@next/swc-darwin-arm64': 15.6.0-canary.28 + '@next/swc-darwin-x64': 15.6.0-canary.28 + '@next/swc-linux-arm64-gnu': 15.6.0-canary.28 + '@next/swc-linux-arm64-musl': 15.6.0-canary.28 + '@next/swc-linux-x64-gnu': 15.6.0-canary.28 + '@next/swc-linux-x64-musl': 15.6.0-canary.28 + '@next/swc-win32-arm64-msvc': 15.6.0-canary.28 + '@next/swc-win32-x64-msvc': 15.6.0-canary.28 babel-plugin-react-compiler: 19.1.0-rc.3 sharp: 0.34.4 transitivePeerDependencies: @@ -3750,17 +3754,17 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - posthog-js@1.268.0: + posthog-js@1.268.4: dependencies: - '@posthog/core': 1.1.0 + '@posthog/core': 1.2.1 core-js: 3.45.1 fflate: 0.4.8 preact: 10.27.2 web-vitals: 4.2.4 - posthog-node@5.8.6: + posthog-node@5.9.1: dependencies: - '@posthog/core': 1.1.0 + '@posthog/core': 1.2.1 potpack@2.1.0: {} @@ -3770,7 +3774,11 @@ snapshots: protocol-buffers-schema@3.6.0: {} - punycode-esm@1.0.15: {} + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + punycode@2.3.1: {} quickselect@3.0.0: {} @@ -3837,6 +3845,10 @@ snapshots: '@types/react': 19.1.13 '@types/react-dom': 19.1.9(@types/react@19.1.13) + rdapper@0.1.0: + dependencies: + psl: 1.15.0 + react-dom@19.1.1(react@19.1.1): dependencies: react: 19.1.1 @@ -3924,7 +3936,7 @@ snapshots: sharp@0.34.4: dependencies: '@img/colour': 1.0.0 - detect-libc: 2.1.0 + detect-libc: 2.1.1 semver: 7.7.2 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.4 @@ -4009,7 +4021,7 @@ snapshots: tapable@2.2.3: {} - tar@7.4.4: + tar@7.5.1: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -4025,11 +4037,11 @@ snapshots: tinyqueue@3.0.0: {} - tldts-core@7.0.15: {} + tldts-core@7.0.16: {} - tldts@7.0.15: + tldts@7.0.16: dependencies: - tldts-core: 7.0.15 + tldts-core: 7.0.16 to-data-view@1.1.0: {} @@ -4050,7 +4062,7 @@ snapshots: tslib@2.8.1: {} - tw-animate-css@1.3.8: {} + tw-animate-css@1.4.0: {} typescript@5.9.2: {} @@ -4118,10 +4130,6 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 - whoiser@2.0.0-beta.8: - dependencies: - punycode-esm: 1.0.15 - yallist@5.0.0: {} zod@4.1.11: {} diff --git a/server/prefetch/domain.ts b/server/prefetch/domain.ts index d2d7d6e..da7ddbb 100644 --- a/server/prefetch/domain.ts +++ b/server/prefetch/domain.ts @@ -1,11 +1,13 @@ import "server-only"; +import type { DomainRecord } from "rdapper"; import { cache } from "react"; import { createServerCaller } from "../caller"; -import type { Whois } from "../services/rdap-parser"; -export const prefetchWhois = cache(async (domain: string): Promise => { - const caller = await createServerCaller(); - const whois = await caller.domain.whois({ domain }); - return whois; -}); +export const prefetchRegistration = cache( + async (domain: string): Promise => { + const caller = await createServerCaller(); + const registration = await caller.domain.registration({ domain }); + return registration; + }, +); diff --git a/server/routers/domain.ts b/server/routers/domain.ts index dcb5186..dd45ade 100644 --- a/server/routers/domain.ts +++ b/server/routers/domain.ts @@ -2,13 +2,16 @@ import { resolveAll } from "../services/dns"; import { getOrCreateFaviconBlobUrl } from "../services/favicon"; import { probeHeaders } from "../services/headers"; import { detectHosting } from "../services/hosting"; -import { fetchWhois } from "../services/rdap"; +import { getRegistration } from "../services/registration"; import { getCertificates } from "../services/tls"; import { router } from "../trpc"; import { createDomainProcedure } from "./domain-procedure"; export const domainRouter = router({ - whois: createDomainProcedure(fetchWhois, "WHOIS lookup failed"), + registration: createDomainProcedure( + getRegistration, + "Registration lookup failed", + ), dns: createDomainProcedure(resolveAll, "DNS resolution failed"), hosting: createDomainProcedure(detectHosting, "Hosting detection failed"), certificates: createDomainProcedure( diff --git a/server/services/rdap-bootstrap.ts b/server/services/rdap-bootstrap.ts deleted file mode 100644 index 4029b3c..0000000 --- a/server/services/rdap-bootstrap.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { unstable_cache } from "next/cache"; - -type IanaDnsBootstrap = { services?: [string[], string[]][] }; - -const fetchIanaDns = unstable_cache( - async (): Promise => { - const res = await fetch("https://data.iana.org/rdap/dns.json", { - // Persist in Next.js data cache on Vercel - next: { revalidate: 24 * 60 * 60 }, - }); - if (!res.ok) { - throw new Error(`Failed to load IANA RDAP bootstrap: ${res.status}`); - } - return (await res.json()) as IanaDnsBootstrap; - }, - ["iana-rdap-dns.json"], - { revalidate: 24 * 60 * 60 }, -); - -export async function getRdapBaseForTld(tld: string): Promise { - const iana = await fetchIanaDns(); - const entry = iana.services?.find((s) => s[0].includes(tld)); - const base = entry?.[1]?.[0] ?? null; - return base ? base.replace(/\/$/, "") : null; -} - -export async function isTldRdapSupported(tld: string): Promise { - const base = await getRdapBaseForTld(tld); - return Boolean(base); -} diff --git a/server/services/rdap-parser.ts b/server/services/rdap-parser.ts deleted file mode 100644 index 7bb2c79..0000000 --- a/server/services/rdap-parser.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { resolveRegistrarDomain } from "@/lib/providers/detection"; - -export type Whois = { - source?: "rdap" | "whois"; - registrar: { name: string; domain: string | null }; - creationDate: string; - expirationDate: string; - registrant: { organization: string; country: string; state?: string }; - status?: string[]; - registered: boolean; -}; - -type RdapEvent = { eventAction?: string; eventDate?: string }; -type RdapEntity = { roles?: string[]; vcardArray?: [string, unknown[]] }; -type RdapJson = { - registrar?: { name?: string }; - entities?: RdapEntity[]; - events?: RdapEvent[]; - status?: string[]; -}; - -export function parseRdapResponse(json: RdapJson): Whois { - const registrarInfo = extractRegistrarInfo(json); - const registrantInfo = extractRegistrantInfo(json); - const eventDates = extractEventDates(json); - - const status = json.status ?? []; - const registered = !status.some((s) => s.toLowerCase() === "available"); - - return { - source: "rdap", - registrar: registrarInfo, - creationDate: eventDates.creation, - expirationDate: eventDates.expiration, - registrant: registrantInfo, - status, - registered, - }; -} - -function extractRegistrarInfo(json: RdapJson): { - name: string; - domain: string | null; -} { - const registrarName = - json.registrar?.name ?? - findVcardValue(findEntity(json.entities, "registrar"), "fn") ?? - "Unknown"; - - return { - name: registrarName, - domain: resolveRegistrarDomain(registrarName) || null, - }; -} - -function extractRegistrantInfo(json: RdapJson): { - organization: string; - country: string; - state?: string; -} { - const registrantEnt = findEntity(json.entities, "registrant"); - const organization = findVcardValue(registrantEnt, "org") ?? ""; - const adr = findVcardEntry(registrantEnt, "adr"); - - const country = - Array.isArray(adr?.[3]) && typeof adr?.[3]?.[6] === "string" - ? (adr?.[3]?.[6] as string) - : ""; - - const state = - Array.isArray(adr?.[3]) && typeof adr?.[3]?.[4] === "string" - ? (adr?.[3]?.[4] as string) - : undefined; - - return { organization, country, state }; -} - -function extractEventDates(json: RdapJson): { - creation: string; - expiration: string; -} { - const creationDate = - json.events?.find((e) => e.eventAction === "registration")?.eventDate ?? ""; - - const expirationDate = - json.events?.find((e) => e.eventAction === "expiration")?.eventDate ?? ""; - - return { creation: creationDate, expiration: expirationDate }; -} - -function findEntity( - entities: RdapEntity[] | undefined, - role: string, -): RdapEntity | undefined { - return entities?.find((e) => e.roles?.includes(role)); -} - -function findVcardEntry( - entity: RdapEntity | undefined, - key: string, -): unknown[] | undefined { - const arr = entity?.vcardArray?.[1]; - if (!Array.isArray(arr)) return undefined; - return arr.find((v) => Array.isArray(v) && v[0] === key) as - | unknown[] - | undefined; -} - -function findVcardValue( - entity: RdapEntity | undefined, - key: string, -): string | undefined { - const entry = findVcardEntry(entity, key); - const value = entry?.[3]; - return typeof value === "string" ? value : undefined; -} diff --git a/server/services/rdap.ts b/server/services/rdap.ts deleted file mode 100644 index 52850de..0000000 --- a/server/services/rdap.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { captureServer } from "@/lib/analytics/server"; -import { toRegistrableDomain } from "@/lib/domain-server"; -import { cacheGet, cacheSet, ns } from "@/lib/redis"; -import { getRdapBaseForTld } from "./rdap-bootstrap"; -import { parseRdapResponse, type Whois } from "./rdap-parser"; -import { fetchWhoisTcp } from "./whois"; - -type RdapJson = { - registrar?: { name?: string }; - entities?: RdapEntity[]; - events?: RdapEvent[]; - status?: string[]; -}; - -// Re-export types for backward compatibility -type RdapEvent = { eventAction?: string; eventDate?: string }; -type RdapEntity = { roles?: string[]; vcardArray?: [string, unknown[]] }; - -export async function fetchWhois(domain: string): Promise { - const registrable = toRegistrableDomain(domain); - if (!registrable) throw new Error("Invalid domain"); - const key = ns("reg", registrable.toLowerCase()); - const cached = await cacheGet(key); - if (cached) return cached; - - try { - const startedAt = Date.now(); - const rdapBase = await rdapBaseForDomain(registrable); - if (!rdapBase) { - // TLD has no RDAP support → go straight to WHOIS - await captureServer("rdap_lookup", { - domain: registrable, - outcome: "bootstrap_missing", - cached: false, - }); - return await fetchWhoisTcp(registrable); - } - const url = `${rdapBase}/domain/${encodeURIComponent(registrable)}`; - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 3000); - let res: Response; - try { - res = await fetch(url, { - headers: { - accept: "application/rdap+json", - "user-agent": "hoot.sh/0.1 (+https://hoot.sh)", - }, - signal: controller.signal, - }); - } finally { - clearTimeout(timeout); - } - - if (res.status === 404) { - const result: Whois = { - source: "rdap", - registrar: { name: "", domain: null }, - creationDate: "", - expirationDate: "", - registrant: { organization: "", country: "" }, - status: ["available"], - registered: false, - }; - await cacheSet(key, result, 60 * 60); - await captureServer("rdap_lookup", { - domain: registrable, - outcome: "404_unregistered", - cached: false, - duration_ms: Date.now() - startedAt, - }); - return result; - } - - if (!res.ok) { - // Fallback to WHOIS on RDAP upstream errors - await captureServer("rdap_lookup", { - domain: registrable, - outcome: "error_fallback", - cached: false, - duration_ms: Date.now() - startedAt, - }); - await captureServer("whois_fallback_triggered", { - domain: registrable, - reason: "rdap_error", - }); - return await fetchWhoisTcp(registrable); - } - - const json = (await res.json()) as unknown as RdapJson; - const result = parseRdapResponse(json); - await cacheSet(key, result, result.registered ? 24 * 60 * 60 : 60 * 60); - await captureServer("rdap_lookup", { - domain: registrable, - outcome: "ok", - cached: false, - duration_ms: Date.now() - startedAt, - }); - return result; - } catch (_err) { - // RDAP lookup failed (network/timeout/bootstrap). Fall back to WHOIS. - await captureServer("rdap_lookup", { - domain: registrable, - outcome: "error_fallback", - cached: false, - }); - await captureServer("whois_fallback_triggered", { - domain: registrable, - reason: "network_or_timeout", - }); - return await fetchWhoisTcp(registrable); - } -} - -async function rdapBaseForDomain(domain: string): Promise { - const tld = domain.split(".").pop() || "com"; - const base = await getRdapBaseForTld(tld); - return base; -} diff --git a/server/services/registration.ts b/server/services/registration.ts new file mode 100644 index 0000000..3e3c77a --- /dev/null +++ b/server/services/registration.ts @@ -0,0 +1,52 @@ +import { type DomainRecord, lookupDomain } from "rdapper"; +import { captureServer } from "@/lib/analytics/server"; +import { toRegistrableDomain } from "@/lib/domain-server"; +import { cacheGet, cacheSet, ns } from "@/lib/redis"; + +/** + * Fetch domain registration using rdapper and cache the normalized DomainRecord. + */ +export async function getRegistration(domain: string): Promise { + const registrable = toRegistrableDomain(domain); + if (!registrable) throw new Error("Invalid domain"); + + const key = ns("reg", registrable.toLowerCase()); + const cached = await cacheGet(key); + if (cached) { + await captureServer("registration_lookup", { + domain: registrable, + outcome: "cache_hit", + cached: true, + source: cached.source, + }); + return cached; + } + + const startedAt = Date.now(); + const { ok, record, error } = await lookupDomain(registrable, { + timeoutMs: 15000, + followWhoisReferral: true, + }); + + if (!ok || !record) { + await captureServer("registration_lookup", { + domain: registrable, + outcome: "error", + cached: false, + error: error || "unknown", + }); + throw new Error(error || "Registration lookup failed"); + } + + const ttl = record.isRegistered ? 24 * 60 * 60 : 60 * 60; + await cacheSet(key, record, ttl); + await captureServer("registration_lookup", { + domain: registrable, + outcome: record.isRegistered ? "ok" : "unregistered", + cached: false, + duration_ms: Date.now() - startedAt, + source: record.source, + }); + + return record; +} diff --git a/server/services/whois.ts b/server/services/whois.ts deleted file mode 100644 index 772dfaa..0000000 --- a/server/services/whois.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { firstResult, whoisDomain } from "whoiser"; -import { captureServer } from "@/lib/analytics/server"; -import { toRegistrableDomain } from "@/lib/domain-server"; -import { resolveRegistrarDomain } from "@/lib/providers/detection"; -import { cacheGet, cacheSet, ns } from "@/lib/redis"; -import type { Whois } from "./rdap-parser"; - -/** - * Fetch WHOIS data over TCP (port 43) and normalize to our Whois shape. - * Uses whoiser parser and extracts non-PII fields only. - */ -export async function fetchWhoisTcp(domain: string): Promise { - const registrable = toRegistrableDomain(domain); - if (!registrable) throw new Error("Invalid domain"); - - const key = ns("reg", registrable.toLowerCase()); - const cached = await cacheGet(key); - if (cached) { - await captureServer("whois_lookup", { - domain: registrable, - outcome: "cache_hit", - cached: true, - }); - return cached; - } - - const startedAt = Date.now(); - const results = await whoisDomain(registrable, { - timeout: 4000, - follow: 2, - ignorePrivacy: true, - raw: false, - }); - - const result: Record | null = firstResult(results); - if (!result) { - // Treat as unregistered/empty - const empty: Whois = { - source: "whois", - registrar: { name: "", domain: null }, - creationDate: "", - expirationDate: "", - registrant: { organization: "", country: "" }, - status: [], - registered: false, - }; - await cacheSet(key, empty, 60 * 60); - await captureServer("whois_lookup", { - domain: registrable, - outcome: "empty", - cached: false, - duration_ms: Date.now() - startedAt, - }); - return empty; - } - - const registrar = pickString(result, [ - "Registrar", - "Sponsoring Registrar", - "Registrar Name", - ]); - const creationDate = pickString(result, [ - "Created Date", - "Creation Date", - "registered", - "Domain record activated", - ]); - const expirationDate = pickString(result, [ - "Expiry Date", - "Expiration Date", - "expire", - "Domain expires", - ]); - const statusCandidate = (result as Record)["Domain Status"]; - const statusArr = Array.isArray(statusCandidate) - ? (statusCandidate.filter((v) => typeof v === "string") as string[]) - : []; - - // Registrant details (best-effort, PII minimized) - const registrantOrg = pickString(result, [ - "Registrant Organization", - "Registrant Organisation", - "owner orgname", - "Registrant Name", - ]); - const registrantCountry = pickString(result, ["Registrant Country"]); - const registrantState = pickString(result, [ - "Registrant State/Province", - "Registrant State", - ]); - - // Heuristic: consider registered if we have dates, registrar, or statuses - const textCandidate = (result as Record).text; - const text = Array.isArray(textCandidate) - ? (textCandidate.filter((v) => typeof v === "string") as string[]) - : []; - const errorText = String((result as Record).error || ""); - const nameServersCandidate = (result as Record)[ - "Name Server" - ]; - const hasNameServers = Array.isArray(nameServersCandidate) - ? nameServersCandidate.length > 0 - : false; - const notFound = - errorText.toLowerCase().includes("not found") || - text.some((t) => /no match|not found|no entries found/i.test(t)); - const hasSignals = Boolean( - registrar || - creationDate || - expirationDate || - statusArr.length > 0 || - hasNameServers, - ); - const registered = hasSignals && !notFound; - - const normalized: Whois = { - source: "whois", - registrar: { - name: registrar || "", - domain: resolveRegistrarDomain(registrar || "") || null, - }, - creationDate: creationDate || "", - expirationDate: expirationDate || "", - registrant: { - organization: registrantOrg || "", - country: registrantCountry || "", - state: registrantState || undefined, - }, - status: statusArr, - registered, - }; - - await cacheSet(key, normalized, registered ? 24 * 60 * 60 : 60 * 60); - await captureServer("whois_lookup", { - domain: registrable, - outcome: "ok", - cached: false, - duration_ms: Date.now() - startedAt, - }); - return normalized; -} - -function pickString( - obj: Record, - keys: string[], -): string | undefined { - for (const k of keys) { - const v = obj[k]; - if (typeof v === "string" && v.trim() !== "") return v; - } - return undefined; -}