import { env } from "cloudflare:workers"; import { z } from "zod"; import { badRequest, HttpError } from "../errors"; const API = "https://api.cloudflare.com/client/v4"; const HTTP_UNAVAILABLE = 503; const HTTP_BAD_GATEWAY = 502; const hostnameSchema = z.object({ id: z.string(), hostname: z.string(), status: z.string(), ssl: z .object({ status: z.string().optional(), txt_name: z.string().optional(), txt_value: z.string().optional(), validation_records: z.array(z.object({ txt_name: z.string().optional(), txt_value: z.string().optional() })), }) .partial() .optional(), verification_errors: z.array(z.string()).optional(), ownership_verification: z.object({ type: z.string(), name: z.string(), value: z.string() }).optional(), }); const envelope = z.object({ success: z.boolean(), errors: z.array(z.object({ message: z.string() })).default([]), result: z.unknown(), }); export type CustomHostname = z.infer; /** Whether Cloudflare for SaaS is configured: a zone with a fallback origin and an API token secret. */ export function customDomainsEnabled(): boolean { return Boolean(env.CUSTOM_DOMAINS_ZONE_ID && env.CF_API_TOKEN); } /** The hostname customers CNAME their domain to: the zone's SaaS fallback origin. */ export function customDomainsTarget(): string | null { return env.CUSTOM_DOMAINS_TARGET?.trim().toLowerCase() || null; } async function call(path: string, init: RequestInit = {}): Promise { if (!env.CUSTOM_DOMAINS_ZONE_ID || !env.CF_API_TOKEN) throw new HttpError(HTTP_UNAVAILABLE, "Custom domains are not configured"); const response = await fetch(`${API}/zones/${env.CUSTOM_DOMAINS_ZONE_ID}/custom_hostnames${path}`, { ...init, headers: { authorization: `Bearer ${env.CF_API_TOKEN}`, "content-type": "application/json" }, }); const parsed = envelope.safeParse(await response.json().catch(() => null)); if (!parsed.success) throw new HttpError(HTTP_BAD_GATEWAY, "Unexpected response from Cloudflare"); if (!parsed.data.success) { throw badRequest(parsed.data.errors.map((error) => error.message).join("; ") || "Cloudflare rejected the request"); } return parsed.data.result; } export async function createCustomHostname(hostname: string): Promise { const result = await call("", { method: "POST", body: JSON.stringify({ hostname, // TXT validation proves control of the domain's DNS; HTTP validation would succeed for anyone when a // stale CNAME still points at the fallback origin. ssl: { method: "txt", type: "dv", settings: { http2: "on", min_tls_version: "1.2", tls_1_3: "on" } }, }), }); return hostnameSchema.parse(result); } export async function getCustomHostname(id: string): Promise { return hostnameSchema.parse(await call(`/${encodeURIComponent(id)}`)); } export async function deleteCustomHostname(id: string): Promise { await call(`/${encodeURIComponent(id)}`, { method: "DELETE" }); } export function domainStatus(hostname: CustomHostname): string { const ssl = hostname.ssl?.status ?? "pending"; return hostname.status === "active" && ssl === "active" ? "active" : `${hostname.status} (certificate ${ssl})`; }