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