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:
@@ -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>
|
||||
|
@@ -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();
|
||||
}}
|
||||
/>
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
interface ExportData {
|
||||
whois: unknown;
|
||||
registration: unknown;
|
||||
dns: unknown;
|
||||
hosting: unknown;
|
||||
certificates: unknown;
|
||||
|
@@ -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 };
|
||||
}
|
||||
|
@@ -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,
|
||||
|
18
package.json
18
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": {
|
||||
|
258
pnpm-lock.yaml
generated
258
pnpm-lock.yaml
generated
@@ -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: {}
|
||||
|
@@ -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;
|
||||
},
|
||||
);
|
||||
|
@@ -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(
|
||||
|
@@ -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);
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
52
server/services/registration.ts
Normal file
52
server/services/registration.ts
Normal 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;
|
||||
}
|
@@ -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;
|
||||
}
|
Reference in New Issue
Block a user