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

Update rdapper to ^0.5.0 and refactor domain validation logic to streamline domain input processing

This commit is contained in:
2025-10-10 11:02:54 -04:00
parent 0017e3fceb
commit c3a641b6fc
7 changed files with 89 additions and 120 deletions

View File

@@ -4,10 +4,7 @@ import { notFound, redirect } from "next/navigation";
import { DomainReportView } from "@/components/domain/domain-report-view";
import { DomainSsrAnalytics } from "@/components/domain/domain-ssr-analytics";
import { normalizeDomainInput } from "@/lib/domain";
import {
isBlacklistedDomainLike,
toRegistrableDomain,
} from "@/lib/domain-server";
import { toRegistrableDomain } from "@/lib/domain-server";
import { getQueryClient } from "@/trpc/query-client";
import { trpc } from "@/trpc/server";
@@ -22,9 +19,8 @@ export async function generateMetadata({
const decoded = decodeURIComponent(raw);
const normalized = normalizeDomainInput(decoded);
const isBlacklisted = isBlacklistedDomainLike(normalized);
const isRegistrable = toRegistrableDomain(normalized);
if (!isRegistrable || isBlacklisted) notFound();
if (!isRegistrable) notFound();
return {
title: `Domain Report: ${normalized} — Hoot`,
@@ -41,9 +37,8 @@ export default async function DomainPage({
const decoded = decodeURIComponent(raw);
const normalized = normalizeDomainInput(decoded);
const isBlacklisted = isBlacklistedDomainLike(normalized);
const isRegistrable = toRegistrableDomain(normalized);
if (!isRegistrable || isBlacklisted) notFound();
if (!isRegistrable) notFound();
// Canonicalize URL to the normalized domain
if (normalized !== decoded) {

View File

@@ -1,10 +1,6 @@
/* @vitest-environment node */
import { describe, expect, it } from "vitest";
import {
isAcceptableDomainInput,
isBlacklistedDomainLike,
toRegistrableDomain,
} from "./domain-server";
import { toRegistrableDomain } from "./domain-server";
describe("toRegistrableDomain", () => {
it("reduces subdomains to eTLD+1", () => {
@@ -21,29 +17,25 @@ describe("toRegistrableDomain", () => {
it("normalizes case and trailing dot", () => {
expect(toRegistrableDomain("EXAMPLE.COM.")).toBe("example.com");
});
});
describe("isAcceptableDomainInput", () => {
it("is true only for ICANN eTLD+1 inputs", () => {
expect(isAcceptableDomainInput("sub.EXAMPLE.com")).toBe(true);
expect(isAcceptableDomainInput("localhost")).toBe(false);
expect(isAcceptableDomainInput("256.256.256.256")).toBe(false);
});
});
describe("isBlacklistedDomainLike", () => {
it("returns true for common sourcemap-like suffixes", () => {
expect(isBlacklistedDomainLike("styles.css.map")).toBe(true);
expect(isBlacklistedDomainLike("app.js.map")).toBe(true);
expect(isBlacklistedDomainLike("foo.ts.map")).toBe(true);
expect(isBlacklistedDomainLike("bundle.mjs.map")).toBe(true);
expect(isBlacklistedDomainLike("bundle.cjs.map")).toBe(true);
it("is non-null only for ICANN eTLD+1 inputs", () => {
expect(toRegistrableDomain("sub.EXAMPLE.com")).toBe("example.com");
expect(toRegistrableDomain("localhost")).toBeNull();
expect(toRegistrableDomain("256.256.256.256")).toBeNull();
});
it("returns false for normal domains and non-sourcemap assets", () => {
expect(isBlacklistedDomainLike("example.com")).toBe(false);
expect(isBlacklistedDomainLike("example.org")).toBe(false);
expect(isBlacklistedDomainLike("file.css")).toBe(false);
expect(isBlacklistedDomainLike("file.map")).toBe(false);
it("is null for common sourcemap-like suffixes", () => {
expect(toRegistrableDomain("styles.css.map")).toBeNull();
expect(toRegistrableDomain("app.js.map")).toBeNull();
expect(toRegistrableDomain("foo.ts.map")).toBeNull();
expect(toRegistrableDomain("bundle.mjs.map")).toBeNull();
expect(toRegistrableDomain("bundle.cjs.map")).toBeNull();
expect(toRegistrableDomain("file.css")).toBeNull();
});
it("is non-null for normal domains and non-sourcemap assets", () => {
expect(toRegistrableDomain("example.com")).toBe("example.com");
expect(toRegistrableDomain("example.org")).toBe("example.org");
expect(toRegistrableDomain("file.map")).toBe("file.map");
});
});

View File

@@ -1,49 +1,20 @@
// Server-only domain helpers using tldts
// Server-only domain helpers using rdapper
// Note: Do not import this file in client components.
import "server-only";
import { parse } from "tldts";
import { toRegistrableDomain as toRegistrableDomainRdapper } from "rdapper";
import { BLACKLISTED_SUFFIXES } from "@/lib/constants";
/**
* Normalize arbitrary input (domain or URL) to its registrable domain (eTLD+1).
* Returns null when the input is not a valid ICANN domain (e.g., invalid TLD, IPs).
*/
// A simple wrapper around rdapper's toRegistrableDomain that also checks if
// the domain is blacklisted below
export function toRegistrableDomain(input: string): string | null {
const raw = (input ?? "").trim();
if (raw === "") return null;
const result = parse(raw);
// Reject IPs and non-ICANN/public suffixes.
if (result.isIp) return null;
if (!result.isIcann) return null;
const domain = result.domain ?? "";
if (domain === "") return null;
return domain.toLowerCase();
}
/**
* Quick boolean check for acceptable domains using tldts parsing.
*/
export function isAcceptableDomainInput(input: string): boolean {
return toRegistrableDomain(input) !== null;
}
/**
* Returns true when the provided input looks like a blacklisted file-like path
* or when its final label (TLD-like) is in our blacklist. This protects the
* domain route from accidentally attempting lookups for 404 asset paths like
* ".css.map".
*/
export function isBlacklistedDomainLike(input: string): boolean {
const value = (input ?? "").trim().toLowerCase();
if (value === "") return false;
if (value === "") return null;
// Shortcut: exact suffixes such as ".css.map" that frequently appear
for (const suffix of BLACKLISTED_SUFFIXES) {
if (value.endsWith(suffix)) return true;
if (value.endsWith(suffix)) return null;
}
return false;
return toRegistrableDomainRdapper(value);
}

View File

@@ -501,6 +501,12 @@ export const EMAIL_PROVIDERS: Array<
category: "email",
rule: { kind: "mxSuffix", suffix: "hey.com" },
},
{
name: "Symantec Email Security",
domain: "broadcom.com",
category: "email",
rule: { kind: "mxSuffix", suffix: "messagelabs.com" },
},
];
/**

View File

@@ -52,7 +52,7 @@
"posthog-node": "^5.9.5",
"puppeteer-core": "24.22.3",
"radix-ui": "^1.4.3",
"rdapper": "^0.4.1",
"rdapper": "^0.5.0",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-map-gl": "^8.1.0",
@@ -61,7 +61,6 @@
"sonner": "^2.0.7",
"superjson": "^2.2.2",
"tailwind-merge": "^3.3.1",
"tldts": "^7.0.17",
"uploadthing": "^7.7.4",
"uuid": "^13.0.0",
"vaul": "^1.1.2",

96
pnpm-lock.yaml generated
View File

@@ -93,8 +93,8 @@ importers:
specifier: ^1.4.3
version: 1.4.3(@types/react-dom@19.1.9(@types/react@19.1.16))(@types/react@19.1.16)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
rdapper:
specifier: ^0.4.1
version: 0.4.1
specifier: ^0.5.0
version: 0.5.0
react:
specifier: 19.1.1
version: 19.1.1
@@ -119,9 +119,6 @@ importers:
tailwind-merge:
specifier: ^3.3.1
version: 3.3.1
tldts:
specifier: ^7.0.17
version: 7.0.17
uploadthing:
specifier: ^7.7.4
version: 7.7.4(next@15.6.0-canary.39(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(tailwindcss@4.1.14)
@@ -2135,11 +2132,16 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
bare-events@2.7.0:
resolution: {integrity: sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==}
bare-events@2.8.0:
resolution: {integrity: sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA==}
peerDependencies:
bare-abort-controller: '*'
peerDependenciesMeta:
bare-abort-controller:
optional: true
bare-fs@4.4.5:
resolution: {integrity: sha512-TCtu93KGLu6/aiGWzMr12TmSRS6nKdfhAnzTQRbXoSWxkbb9eRd53jQ51jG7g1gYjjtto3hbBrrhzg6djcgiKg==}
bare-fs@4.4.10:
resolution: {integrity: sha512-arqVF+xX/rJHwrONZaSPhlzleT2gXwVs9rsAe1p1mIVwWZI2A76/raio+KwwxfWMO8oV9Wo90EaUkS2QwVmy4w==}
engines: {bare: '>=1.16.0'}
peerDependencies:
bare-buffer: '*'
@@ -2171,8 +2173,8 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.8.13:
resolution: {integrity: sha512-7s16KR8io8nIBWQyCYhmFhd+ebIzb9VKTzki+wOJXHTxTnV6+mFGH3+Jwn1zoKaY9/H9T/0BcKCZnzXljPnpSQ==}
baseline-browser-mapping@2.8.16:
resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==}
hasBin: true
basic-ftp@5.0.5:
@@ -2223,8 +2225,8 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
caniuse-lite@1.0.30001748:
resolution: {integrity: sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==}
caniuse-lite@1.0.30001749:
resolution: {integrity: sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==}
chai@5.3.3:
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
@@ -2299,8 +2301,8 @@ packages:
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
engines: {node: '>=12.13'}
core-js@3.45.1:
resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==}
core-js@3.46.0:
resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==}
cosmiconfig@9.0.0:
resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==}
@@ -2434,8 +2436,8 @@ packages:
effect@3.17.7:
resolution: {integrity: sha512-dpt0ONUn3zzAuul6k4nC/coTTw27AL5nhkORXgTi6NfMPzqWYa1M05oKmOMTxpVSTKepqXVcW9vIwkuaaqx9zA==}
electron-to-chromium@1.5.232:
resolution: {integrity: sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==}
electron-to-chromium@1.5.234:
resolution: {integrity: sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -3244,9 +3246,6 @@ packages:
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
psl@1.15.0:
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
pump@3.0.3:
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
@@ -3282,8 +3281,8 @@ packages:
'@types/react-dom':
optional: true
rdapper@0.4.1:
resolution: {integrity: sha512-XKrQ7VwdD+cgH8DFt213pAoB8tUdw7aJCa0xV8DV31XY1qGQsfIioF6zOfyQJldQXHvTmtou9GfnsXE02dt6MQ==}
rdapper@0.5.0:
resolution: {integrity: sha512-IbCXWOvzxasg6vvmj1f67AR4h4pcOLx94Ys8VN3ZHDroOU83ZuDHuq4GYsZEKbxnYijAklzR26MIw3/gOreDzw==}
engines: {node: '>=18.17'}
react-dom@19.1.1:
@@ -4548,6 +4547,7 @@ snapshots:
tar-fs: 3.1.1
yargs: 17.7.2
transitivePeerDependencies:
- bare-abort-controller
- bare-buffer
- react-native-b4a
- supports-color
@@ -5374,6 +5374,7 @@ snapshots:
follow-redirects: 1.15.11
tar-fs: 3.1.1
transitivePeerDependencies:
- bare-abort-controller
- bare-buffer
- debug
- react-native-b4a
@@ -5781,16 +5782,17 @@ snapshots:
balanced-match@1.0.2: {}
bare-events@2.7.0: {}
bare-events@2.8.0: {}
bare-fs@4.4.5:
bare-fs@4.4.10:
dependencies:
bare-events: 2.7.0
bare-events: 2.8.0
bare-path: 3.0.0
bare-stream: 2.7.0(bare-events@2.7.0)
bare-stream: 2.7.0(bare-events@2.8.0)
bare-url: 2.2.2
fast-fifo: 1.3.2
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
optional: true
@@ -5802,12 +5804,13 @@ snapshots:
bare-os: 3.6.2
optional: true
bare-stream@2.7.0(bare-events@2.7.0):
bare-stream@2.7.0(bare-events@2.8.0):
dependencies:
streamx: 2.23.0
optionalDependencies:
bare-events: 2.7.0
bare-events: 2.8.0
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
optional: true
@@ -5818,7 +5821,7 @@ snapshots:
base64-js@1.5.1: {}
baseline-browser-mapping@2.8.13: {}
baseline-browser-mapping@2.8.16: {}
basic-ftp@5.0.5: {}
@@ -5836,9 +5839,9 @@ snapshots:
browserslist@4.26.3:
dependencies:
baseline-browser-mapping: 2.8.13
caniuse-lite: 1.0.30001748
electron-to-chromium: 1.5.232
baseline-browser-mapping: 2.8.16
caniuse-lite: 1.0.30001749
electron-to-chromium: 1.5.234
node-releases: 2.0.23
update-browserslist-db: 1.1.3(browserslist@4.26.3)
@@ -5872,7 +5875,7 @@ snapshots:
callsites@3.1.0: {}
caniuse-lite@1.0.30001748: {}
caniuse-lite@1.0.30001749: {}
chai@5.3.3:
dependencies:
@@ -5966,7 +5969,7 @@ snapshots:
dependencies:
is-what: 4.1.16
core-js@3.45.1: {}
core-js@3.46.0: {}
cosmiconfig@9.0.0(typescript@5.9.3):
dependencies:
@@ -6102,7 +6105,7 @@ snapshots:
'@standard-schema/spec': 1.0.0
fast-check: 3.23.2
electron-to-chromium@1.5.232: {}
electron-to-chromium@1.5.234: {}
emoji-regex@8.0.0: {}
@@ -6202,7 +6205,9 @@ snapshots:
events-universal@1.0.1:
dependencies:
bare-events: 2.7.0
bare-events: 2.8.0
transitivePeerDependencies:
- bare-abort-controller
events@3.3.0: {}
@@ -6745,7 +6750,7 @@ snapshots:
dependencies:
'@next/env': 15.6.0-canary.39
'@swc/helpers': 0.5.15
caniuse-lite: 1.0.30001748
caniuse-lite: 1.0.30001749
postcss: 8.4.31
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
@@ -6881,7 +6886,7 @@ snapshots:
posthog-js@1.275.0:
dependencies:
'@posthog/core': 1.2.4
core-js: 3.45.1
core-js: 3.46.0
fflate: 0.4.8
preact: 10.27.2
web-vitals: 4.2.4
@@ -6921,10 +6926,6 @@ snapshots:
proxy-from-env@1.1.0: {}
psl@1.15.0:
dependencies:
punycode: 2.3.1
pump@3.0.3:
dependencies:
end-of-stream: 1.4.5
@@ -6942,6 +6943,7 @@ snapshots:
webdriver-bidi-protocol: 0.2.11
ws: 8.18.3
transitivePeerDependencies:
- bare-abort-controller
- bare-buffer
- bufferutil
- react-native-b4a
@@ -6957,6 +6959,7 @@ snapshots:
puppeteer-core: 24.22.3
typed-query-selector: 2.12.0
transitivePeerDependencies:
- bare-abort-controller
- bare-buffer
- bufferutil
- react-native-b4a
@@ -7031,9 +7034,9 @@ snapshots:
'@types/react': 19.1.16
'@types/react-dom': 19.1.9(@types/react@19.1.16)
rdapper@0.4.1:
rdapper@0.5.0:
dependencies:
psl: 1.15.0
tldts: 7.0.17
react-dom@19.1.1(react@19.1.1):
dependencies:
@@ -7276,6 +7279,7 @@ snapshots:
fast-fifo: 1.3.2
text-decoder: 1.2.3
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
string-width@4.2.3:
@@ -7352,9 +7356,10 @@ snapshots:
pump: 3.0.3
tar-stream: 3.1.7
optionalDependencies:
bare-fs: 4.4.5
bare-fs: 4.4.10
bare-path: 3.0.0
transitivePeerDependencies:
- bare-abort-controller
- bare-buffer
- react-native-b4a
@@ -7364,6 +7369,7 @@ snapshots:
fast-fifo: 1.3.2
streamx: 2.23.0
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
tar@7.5.1:

View File

@@ -1,6 +1,6 @@
import { toRegistrableDomain } from "rdapper";
import z from "zod";
import { normalizeDomainInput } from "@/lib/domain";
import { isAcceptableDomainInput } from "@/lib/domain-server";
import {
CertificatesSchema,
DnsResolveResultSchema,
@@ -22,7 +22,7 @@ import { createTRPCRouter, loggedProcedure } from "@/trpc/init";
export const domainInput = z
.object({ domain: z.string().min(1) })
.transform(({ domain }) => ({ domain: normalizeDomainInput(domain) }))
.refine(({ domain }) => isAcceptableDomainInput(domain), {
.refine(({ domain }) => toRegistrableDomain(domain) !== null, {
message: "Invalid domain",
path: ["domain"],
});