1
mirror of https://github.com/jakejarvis/hoot.git synced 2025-10-18 14:24:26 -04:00

Refactor domain registration handling to use rdapper and remove WHOIS dependencies

This commit is contained in:
2025-09-24 20:51:25 -04:00
parent 9f5b975daa
commit 02ef7f61b2
15 changed files with 294 additions and 600 deletions

2
.nvmrc
View File

@@ -1 +1 @@
22.19.0
22.20.0

View File

@@ -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 (
<div className="container mx-auto max-w-4xl px-4 py-6">
@@ -55,8 +55,8 @@ export default async function DomainPage({
/>
<DomainReportView
domain={normalized}
initialWhois={whois}
initialRegistered={whois?.registered === true}
initialRegistration={registration}
initialRegistered={registration?.isRegistered === true}
/>
</Suspense>
</div>

View File

@@ -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 <DomainLoadingState />;
}
// 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 <DomainUnregisteredState domain={domain} />;
@@ -92,15 +98,15 @@ export function DomainReportView({
<Accordion type="multiple" className="space-y-4">
<RegistrationSection
data={whois.data || null}
isLoading={whois.isLoading}
isError={!!whois.isError}
data={registration.data || null}
isLoading={registration.isLoading}
isError={!!registration.isError}
onRetry={() => {
captureClient("section_refetch_clicked", {
domain,
section: "whois",
section: "registration",
});
whois.refetch();
registration.refetch();
}}
/>

View File

@@ -1,5 +1,5 @@
interface ExportData {
whois: unknown;
registration: unknown;
dns: unknown;
hosting: unknown;
certificates: unknown;

View File

@@ -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 (
<Section
title={Def.title}
@@ -43,11 +48,11 @@ export function RegistrationSection({
<>
<KeyValue
label="Registrar"
value={data.registrar.name}
value={registrar?.name || ""}
leading={
data.registrar.domain ? (
registrar?.domain ? (
<Favicon
domain={data.registrar.domain}
domain={registrar.domain}
size={16}
className="rounded"
/>
@@ -59,11 +64,19 @@ export function RegistrationSection({
</Badge>
}
/>
<KeyValue label="Created" value={formatDate(data.creationDate)} />
<KeyValue label="Expires" value={formatDate(data.expirationDate)} />
<KeyValue
label="Created"
value={formatDate(data.creationDate || "")}
/>
<KeyValue
label="Expires"
value={formatDate(data.expirationDate || "")}
/>
<KeyValue
label="Registrant"
value={formatRegistrant(data.registrant)}
value={formatRegistrant(
registrant ?? { organization: "Unknown", country: "" },
)}
/>
</>
) : isError ? (
@@ -74,3 +87,29 @@ export function RegistrationSection({
</Section>
);
}
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 };
}

View File

@@ -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,

View File

@@ -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": {

258
pnpm-lock.yaml generated
View File

@@ -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: {}

View File

@@ -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<Whois> => {
const caller = await createServerCaller();
const whois = await caller.domain.whois({ domain });
return whois;
});
export const prefetchRegistration = cache(
async (domain: string): Promise<DomainRecord> => {
const caller = await createServerCaller();
const registration = await caller.domain.registration({ domain });
return registration;
},
);

View File

@@ -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(

View File

@@ -1,30 +0,0 @@
import { unstable_cache } from "next/cache";
type IanaDnsBootstrap = { services?: [string[], string[]][] };
const fetchIanaDns = unstable_cache(
async (): Promise<IanaDnsBootstrap> => {
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<string | null> {
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<boolean> {
const base = await getRdapBaseForTld(tld);
return Boolean(base);
}

View File

@@ -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;
}

View File

@@ -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<Whois> {
const registrable = toRegistrableDomain(domain);
if (!registrable) throw new Error("Invalid domain");
const key = ns("reg", registrable.toLowerCase());
const cached = await cacheGet<Whois>(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<string | null> {
const tld = domain.split(".").pop() || "com";
const base = await getRdapBaseForTld(tld);
return base;
}

View File

@@ -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<DomainRecord> {
const registrable = toRegistrableDomain(domain);
if (!registrable) throw new Error("Invalid domain");
const key = ns("reg", registrable.toLowerCase());
const cached = await cacheGet<DomainRecord>(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;
}

View File

@@ -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<Whois> {
const registrable = toRegistrableDomain(domain);
if (!registrable) throw new Error("Invalid domain");
const key = ns("reg", registrable.toLowerCase());
const cached = await cacheGet<Whois>(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<string, unknown> | 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<string, unknown>)["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<string, unknown>).text;
const text = Array.isArray(textCandidate)
? (textCandidate.filter((v) => typeof v === "string") as string[])
: [];
const errorText = String((result as Record<string, unknown>).error || "");
const nameServersCandidate = (result as Record<string, unknown>)[
"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<string, unknown>,
keys: string[],
): string | undefined {
for (const k of keys) {
const v = obj[k];
if (typeof v === "string" && v.trim() !== "") return v;
}
return undefined;
}