diff --git a/apps/builder/app/builder/features/topbar/add-domain.tsx b/apps/builder/app/builder/features/topbar/add-domain.tsx
index 5b928280454e..e2de69130de0 100644
--- a/apps/builder/app/builder/features/topbar/add-domain.tsx
+++ b/apps/builder/app/builder/features/topbar/add-domain.tsx
@@ -54,7 +54,7 @@ export const AddDomain = ({
});
// enforce www subdomain when no support for cname flattening
// and root cname can conflict with MX or NS
- if (!registrar.cnameFlattening) {
+ if (registrar.known && !registrar.cnameFlattening) {
domain = `www.${domain}`;
}
}
diff --git a/apps/builder/app/shared/help.tsx b/apps/builder/app/shared/help.tsx
index 665359ffcc83..1fb41fee10f2 100644
--- a/apps/builder/app/shared/help.tsx
+++ b/apps/builder/app/shared/help.tsx
@@ -6,12 +6,6 @@ import {
} from "@webstudio-is/icons";
export const help = [
- {
- label: "Support hub",
- url: "https://help.webstudio.is/",
- icon: ,
- target: "embed",
- },
{
label: "Video tutorials",
url: "https://wstd.us/101",
diff --git a/packages/domain/src/rdap.ts b/packages/domain/src/rdap.ts
new file mode 100644
index 000000000000..926079dbea8b
--- /dev/null
+++ b/packages/domain/src/rdap.ts
@@ -0,0 +1,104 @@
+interface DNSList {
+ description: string;
+ publication: string;
+ services: [string[], string[]][];
+ version: string;
+}
+
+// Cache map to store TLD (top level domain) → RDAP server URL mappings
+const dnsCache = new Map();
+
+/**
+ * Fetch the IANA DNS RDAP bootstrap JSON.
+ */
+const fetchDnsList = async (): Promise => {
+ try {
+ const response = await fetch("https://data.iana.org/rdap/dns.json", {
+ headers: {
+ accept: "application/json,application/rdap+json",
+ },
+ });
+ if (response.ok) {
+ return response.json();
+ }
+ } catch {
+ // empty block
+ }
+};
+
+/**
+ * Find the RDAP server URL for a given top-level domain (TLD).
+ */
+const findRdapServer = async (topLevelDomain: string) => {
+ if (dnsCache.size === 0) {
+ const dns = await fetchDnsList();
+ if (dns) {
+ for (const [domains, [server]] of dns.services) {
+ for (const domain of domains) {
+ dnsCache.set(domain, server);
+ }
+ }
+ }
+ }
+ return dnsCache.get(topLevelDomain);
+};
+
+/**
+ * Extract the top-level domain from a full domain string.
+ * Unicode is converted to ascii
+ */
+const getTopLevelDomain = (domain: string) => {
+ try {
+ return new URL(`https://${domain}`).hostname.split(".").at(-1);
+ } catch {
+ // invalid domain
+ }
+};
+
+const fetchRdap = async (
+ rdapServer: string,
+ domain: string
+): Promise => {
+ try {
+ const response = await fetch(`${rdapServer}domain/${domain}`, {
+ headers: {
+ accept: "application/json,application/rdap+json",
+ },
+ });
+ if (response.ok) {
+ return response.text();
+ }
+ } catch {
+ // empty block
+ }
+};
+
+/**
+ * Determine whether a domain is using Cloudflare nameservers.
+ * 1. Parse TLD from domain.
+ * 2. Lookup RDAP server for that TLD.
+ * 3. Fetch RDAP data for the domain.
+ * 4. Search the raw response for ".ns.cloudflare.com".
+ */
+export const isDomainUsingCloudflareNameservers = async (domain: string) => {
+ const topLevelDomain = getTopLevelDomain(domain);
+ if (!topLevelDomain) {
+ throw new Error("Could not parse the top level domain.");
+ }
+
+ const rdapServer = await findRdapServer(topLevelDomain);
+ if (!rdapServer) {
+ console.error(
+ "RDAP Server for the given top level domain could not be found."
+ );
+ return undefined;
+ }
+
+ const data = await fetchRdap(rdapServer, domain);
+ if (data) {
+ // detect by nameservers rather than registrar url
+ // sometimes stored as *.NS.CLOUDFLARE.COM
+ return data.toLowerCase().includes(".ns.cloudflare.com");
+ }
+ return false;
+};
diff --git a/packages/domain/src/trpc/domain.ts b/packages/domain/src/trpc/domain.ts
index 8f6bee07b122..2b140ea1d979 100644
--- a/packages/domain/src/trpc/domain.ts
+++ b/packages/domain/src/trpc/domain.ts
@@ -5,11 +5,7 @@ import { createProductionBuild } from "@webstudio-is/project-build/index.server"
import { router, procedure } from "@webstudio-is/trpc-interface/index.server";
import { Templates } from "@webstudio-is/sdk";
import { db } from "../db";
-
-const rdap = [
- "https://rdap.cloudflare.com/rdap/v1/domain/",
- "https://rdap.namecheap.com/domain/",
-];
+import { isDomainUsingCloudflareNameservers } from "../rdap";
export const domainRouter = router({
getEntriToken: procedure.query(async ({ ctx }) => {
@@ -32,23 +28,12 @@ export const domainRouter = router({
findDomainRegistrar: procedure
.input(z.object({ domain: z.string() }))
.query(async ({ input }) => {
- try {
- for (const rdapEndpoint of rdap) {
- const response = await fetch(`${rdapEndpoint}${input.domain}`);
- if (response.ok) {
- const data = await response.text();
- return {
- // detect by nameservers rather than registrar url
- cnameFlattening: data.includes(".ns.cloudflare.com"),
- };
- }
- }
- } catch {
- // empty block
- }
+ const isCloudflare = await isDomainUsingCloudflareNameservers(
+ input.domain
+ );
return {
- name: "other",
- cnameFlattening: false,
+ known: isCloudflare !== undefined,
+ cnameFlattening: isCloudflare === true,
};
}),