/**
* Domain availability checking and suggestion service
* Uses free WHOIS and DNS APIs to check domain availability
*/
// Import comprehensive WHOIS server list (877 TLDs)
// Source: https://whoislist.org/whois_servers.json
import whoisServersData from "../../whois-servers.json" assert { type: "json" };
export type DomainStatus = "AVAILABLE" | "TAKEN" | "UNKNOWN" | "ERROR";
export interface DomainCheckResult {
domain: string;
status: DomainStatus;
tld: string;
message?: string;
}
export interface WhoisInfo {
domain: string;
registrar?: string;
createdDate?: string;
expirationDate?: string;
updatedDate?: string;
nameServers?: string[];
status?: string[];
raw?: string;
}
// Popular TLDs to check
export const POPULAR_TLDS = [
"com",
"net",
"org",
"io",
"co",
"ai",
"dev",
"app",
"tech",
"xyz",
];
/**
* Check domain availability using DNS lookup
* This is a fast method that works for most domains
*/
async function checkViaDNS(domain: string): Promise<boolean | null> {
try {
const result = await Bun.dns.lookup(domain, { family: 4 });
// If we get DNS records, domain is taken
return result.length > 0 ? false : null;
} catch (error) {
// DNS lookup failed - domain might be available or error occurred
const errorMsg = error instanceof Error ? error.message : String(error);
if (errorMsg.includes("NXDOMAIN") || errorMsg.includes("ENOTFOUND") || errorMsg.includes("getaddrinfo")) {
return true; // Likely available
}
return null; // Inconclusive
}
}
/**
* Check domain availability using RDAP (Registration Data Access Protocol)
* RDAP is the modern replacement for WHOIS
*/
async function checkViaRDAP(domain: string): Promise<DomainStatus> {
const tld = domain.split(".").pop()?.toLowerCase();
// RDAP bootstrap URLs for TLDs that have reliable RDAP servers
// Note: Many TLDs (.io, .co, .ai, etc.) don't have working RDAP - skip them
const rdapServers: Record<string, string> = {
com: "https://rdap.verisign.com/com/v1/domain/",
net: "https://rdap.verisign.com/net/v1/domain/",
org: "https://rdap.publicinterestregistry.org/rdap/domain/",
dev: "https://rdap.nic.google/domain/",
app: "https://rdap.nic.google/domain/",
};
// Only use RDAP for TLDs with known reliable servers
const specificServer = rdapServers[tld || ""];
if (!specificServer) {
// Skip RDAP for TLDs without reliable servers
return "UNKNOWN";
}
const rdapUrl = `${specificServer}${domain}`;
try {
const response = await fetch(rdapUrl, {
headers: { Accept: "application/rdap+json" },
signal: AbortSignal.timeout(5000),
redirect: "follow",
});
if (response.status === 404) {
return "AVAILABLE";
}
if (response.ok || response.status === 302) {
return "TAKEN";
}
return "UNKNOWN";
} catch {
return "UNKNOWN";
}
}
// WHOIS servers for different TLDs (877 TLDs from whoislist.org)
const WHOIS_SERVERS: Record<string, string> = whoisServersData as Record<string, string>;
/**
* Check domain using direct WHOIS server query via TCP
*/
async function checkViaDirectWhois(domain: string): Promise<DomainStatus> {
const tld = domain.split(".").pop()?.toLowerCase();
const whoisServer = WHOIS_SERVERS[tld || ""] || `whois.nic.${tld}`;
try {
const chunks: Buffer[] = [];
let resolved = false;
const result = await Promise.race([
new Promise<string>((resolve, reject) => {
Bun.connect({
hostname: whoisServer,
port: 43,
socket: {
data(_socket, data) {
chunks.push(data);
},
open(socket) {
socket.write(`${domain}\r\n`);
},
close() {
if (!resolved) {
resolved = true;
resolve(Buffer.concat(chunks).toString());
}
},
error(_socket, error) {
if (!resolved) {
resolved = true;
reject(error);
}
},
connectError(_socket, error) {
if (!resolved) {
resolved = true;
reject(error);
}
},
},
});
}),
Bun.sleep(5000).then(() => {
resolved = true;
return chunks.length > 0 ? Buffer.concat(chunks).toString() : "";
}),
]);
const responseLower = result.toLowerCase();
// Check for registration patterns FIRST (domain is taken)
// This is more reliable than "not found" patterns
const registeredPatterns = [
"domain name:",
"registrar:",
"creation date:",
"created:",
"registry domain id:",
"registrant:",
"name server:",
"nserver:",
"dnssec:",
"status: ok",
"status: active",
"status: registered",
"domain status:",
];
if (registeredPatterns.some(p => responseLower.includes(p))) {
return "TAKEN";
}
// Check for "not found" patterns (domain is available)
const notFoundPatterns = [
"no match for",
"not found",
"no entries found",
"no data found",
"domain not found",
"no object found",
"status: available",
"status: free",
"not registered",
];
if (notFoundPatterns.some(p => responseLower.includes(p))) {
return "AVAILABLE";
}
return "UNKNOWN";
} catch {
return "UNKNOWN";
}
}
/**
* Check domain using a free WHOIS API service
*/
async function checkViaWhoisAPI(domain: string): Promise<DomainStatus> {
try {
// Using whoisjson.com free tier (limited requests)
const response = await fetch(
`https://whoisjson.com/api/v1/whois?domain=${encodeURIComponent(domain)}`,
{
signal: AbortSignal.timeout(10000),
}
);
if (!response.ok) {
return "UNKNOWN";
}
const data = (await response.json()) as { status?: string; domain_name?: string };
if (data.status === "available" || !data.domain_name) {
return "AVAILABLE";
}
return "TAKEN";
} catch {
return "UNKNOWN";
}
}
/**
* Check a single domain's availability
*/
export async function checkDomain(domain: string): Promise<DomainCheckResult> {
const normalizedDomain = domain.toLowerCase().trim();
const parts = normalizedDomain.split(".");
const tld = parts.length > 1 ? parts[parts.length - 1] ?? "unknown" : "unknown";
// Validate domain format
if (parts.length < 2 || !parts[0]) {
return {
domain: normalizedDomain,
status: "ERROR",
tld,
message: "Invalid domain format",
};
}
// Try DNS check first (fastest)
const dnsResult = await checkViaDNS(normalizedDomain);
if (dnsResult === true) {
// DNS says no A records, but MUST verify with RDAP/WHOIS
// Many registered domains don't have A records (parked, premium, etc.)
const rdapStatus = await checkViaRDAP(normalizedDomain);
if (rdapStatus === "TAKEN") {
return {
domain: normalizedDomain,
status: "TAKEN",
tld,
message: "Domain is registered (verified via RDAP)",
};
}
if (rdapStatus === "AVAILABLE") {
return {
domain: normalizedDomain,
status: "AVAILABLE",
tld,
message: "Domain appears to be available (verified via RDAP)",
};
}
// RDAP failed - try direct WHOIS query
const whoisStatus = await checkViaDirectWhois(normalizedDomain);
if (whoisStatus === "TAKEN") {
return {
domain: normalizedDomain,
status: "TAKEN",
tld,
message: "Domain is registered (verified via WHOIS)",
};
}
if (whoisStatus === "AVAILABLE") {
return {
domain: normalizedDomain,
status: "AVAILABLE",
tld,
message: "Domain appears to be available (verified via WHOIS)",
};
}
// Both RDAP and WHOIS failed - NEVER assume available
// Short/premium domains often have no DNS but are registered
return {
domain: normalizedDomain,
status: "UNKNOWN",
tld,
message: "Could not verify availability - no DNS records and verification failed",
};
}
if (dnsResult === false) {
return {
domain: normalizedDomain,
status: "TAKEN",
tld,
message: "Domain has DNS records",
};
}
// DNS was inconclusive, try RDAP
const rdapStatus = await checkViaRDAP(normalizedDomain);
if (rdapStatus !== "UNKNOWN") {
return {
domain: normalizedDomain,
status: rdapStatus,
tld,
};
}
// Fall back to WHOIS API
const whoisStatus = await checkViaWhoisAPI(normalizedDomain);
return {
domain: normalizedDomain,
status: whoisStatus,
tld,
message: whoisStatus === "UNKNOWN" ? "Could not determine availability" : undefined,
};
}
/**
* Check multiple domains in bulk
*/
export async function checkDomainsBulk(
domains: string[]
): Promise<DomainCheckResult[]> {
const results = await Promise.all(domains.map(checkDomain));
return results;
}
/**
* Generate domain name suggestions based on a keyword
*/
export function generateSuggestions(
keyword: string,
tlds: string[] = POPULAR_TLDS
): string[] {
const cleanKeyword = keyword.toLowerCase().replace(/[^a-z0-9-]/g, "");
const suggestions: string[] = [];
// Base domain with each TLD
for (const tld of tlds) {
suggestions.push(`${cleanKeyword}.${tld}`);
}
// Common prefixes
const prefixes = ["get", "my", "the", "go", "try", "use", "hey"];
for (const prefix of prefixes) {
suggestions.push(`${prefix}${cleanKeyword}.com`);
}
// Common suffixes
const suffixes = ["app", "hq", "io", "hub", "lab", "now", "pro", "ai"];
for (const suffix of suffixes) {
suggestions.push(`${cleanKeyword}${suffix}.com`);
}
// With hyphens (if keyword has multiple words or is long)
if (cleanKeyword.length > 6) {
const midpoint = Math.floor(cleanKeyword.length / 2);
const hyphenated = `${cleanKeyword.slice(0, midpoint)}-${cleanKeyword.slice(midpoint)}`;
suggestions.push(`${hyphenated}.com`);
}
// Abbreviations for longer keywords
if (cleanKeyword.length > 8) {
const abbrev = cleanKeyword
.split(/[aeiou]/i)
.join("")
.slice(0, 6);
if (abbrev.length >= 3) {
suggestions.push(`${abbrev}.com`);
suggestions.push(`${abbrev}.io`);
}
}
return [...new Set(suggestions)]; // Remove duplicates
}
/**
* Get WHOIS information for a domain
*/
export async function getWhoisInfo(domain: string): Promise<WhoisInfo | null> {
const normalizedDomain = domain.toLowerCase().trim();
try {
// Try RDAP first (modern, JSON-based)
const tld = normalizedDomain.split(".").pop()?.toLowerCase();
const rdapServers: Record<string, string> = {
com: "https://rdap.verisign.com/com/v1/domain/",
net: "https://rdap.verisign.com/net/v1/domain/",
org: "https://rdap.publicinterestregistry.org/rdap/domain/",
};
const rdapUrl =
rdapServers[tld || ""] || `https://rdap.org/domain/${normalizedDomain}`;
const response = await fetch(`${rdapUrl}${normalizedDomain}`, {
headers: { Accept: "application/rdap+json" },
signal: AbortSignal.timeout(10000),
});
if (!response.ok) {
return null;
}
interface RDAPEvent {
eventAction: string;
eventDate?: string;
}
interface RDAPEntity {
roles?: string[];
vcardArray?: [string, Array<[string, unknown, string, string]>];
}
interface RDAPNameserver {
ldhName: string;
}
interface RDAPResponse {
events?: RDAPEvent[];
entities?: RDAPEntity[];
nameservers?: RDAPNameserver[];
status?: string[];
}
const data = (await response.json()) as RDAPResponse;
// Parse RDAP response
const events = data.events || [];
const registrationEvent = events.find(
(e) => e.eventAction === "registration"
);
const expirationEvent = events.find(
(e) => e.eventAction === "expiration"
);
const lastChangedEvent = events.find(
(e) => e.eventAction === "last changed"
);
const entities = data.entities || [];
const registrarEntity = entities.find((e) =>
e.roles?.includes("registrar")
);
return {
domain: normalizedDomain,
registrar: registrarEntity?.vcardArray?.[1]?.find(
(v) => v[0] === "fn"
)?.[3],
createdDate: registrationEvent?.eventDate,
expirationDate: expirationEvent?.eventDate,
updatedDate: lastChangedEvent?.eventDate,
nameServers: data.nameservers?.map(
(ns) => ns.ldhName
),
status: data.status,
};
} catch {
return null;
}
}
/**
* Find domains across multiple TLDs
*/
export async function findDomainsAcrossTLDs(
name: string,
tlds: string[] = POPULAR_TLDS
): Promise<DomainCheckResult[]> {
const domains = tlds.map((tld) => `${name}.${tld}`);
return checkDomainsBulk(domains);
}