import { getRelativeRecordName, normalizeBasePath, validateCustomDomainBasePath, } from '@mintlify/common'; import { addLog, ErrorLog, removeLastLog, SpinnerLog, SuccessLog } from '@mintlify/previewing'; import { Box, Text } from 'ink'; import { authenticatedFetch } from './authenticatedFetch.js'; import { getConfigValue } from './config.js'; import { API_URL, CUSTOM_DOMAIN_CNAME_TARGET } from './constants.js'; import { getAccessToken } from './keyring.js'; import { getCliSubdomains } from './status.js'; interface DnsRecord { type: string; domain: string; value: string; isConfigured?: boolean; } interface CustomDomainCheckResponse { cloudflare?: { dnsRecords?: DnsRecord[] }; } const DNS_POLL_INTERVAL_MS = 5_000; const DNS_POLL_MAX_ATTEMPTS = 3; const DOMAIN_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i; export async function addDomain(domain: string, basePath?: string): Promise { if (!DOMAIN_PATTERN.test(domain)) { addLog( ); return; } const normalizedBasePath = normalizeBasePath(basePath?.trim()); if (normalizedBasePath) { const basePathError = validateCustomDomainBasePath(normalizedBasePath); if (basePathError) { addLog(); return; } } const accessToken = await getAccessToken(); if (!accessToken) { addLog(); return; } const subdomains = await getCliSubdomains(accessToken); const configuredSubdomain = getConfigValue('subdomain'); const subdomain = configuredSubdomain && subdomains.includes(configuredSubdomain) ? configuredSubdomain : subdomains[0]; if (!subdomain) { addLog(); return; } try { const addRes = await authenticatedFetch( `${API_URL}/api/cli/deployment/custom-domain/${subdomain}/${domain}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ basePath: normalizedBasePath ? '/docs' : '' }), } ); if (!addRes.ok) { const body = await addRes.json().catch(() => ({})); addLog(); return; } } catch { addLog(); return; } let basePathSaved = false; let basePathPending = false; if (normalizedBasePath) { addLog(); try { const basePathRes = await authenticatedFetch( `${API_URL}/api/cli/deployment/base-path/${subdomain}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ basePath: normalizedBasePath }), } ); removeLastLog(); if (basePathRes.ok) { const body = (await basePathRes.json().catch(() => ({}))) as { pendingBasePathUpdate?: unknown; }; basePathSaved = true; basePathPending = Boolean(body.pendingBasePathUpdate); } else { const body = (await basePathRes.json().catch(() => ({}))) as { error?: string; issues?: Array<{ message?: string }>; }; const reason = body.issues?.[0]?.message ?? body.error ?? 'unknown error'; addLog(); } } catch { removeLastLog(); addLog( ); } } const maxWaitSeconds = ((DNS_POLL_MAX_ATTEMPTS - 1) * DNS_POLL_INTERVAL_MS) / 1000; addLog( ); let dnsRecords: DnsRecord[] = []; let recordsReady = false; try { for (let attempt = 0; attempt < DNS_POLL_MAX_ATTEMPTS; attempt++) { const checkRes = await authenticatedFetch( `${API_URL}/api/cli/deployment/custom-domain/${subdomain}/${domain}` ); if (checkRes.ok) { const body = (await checkRes.json()) as CustomDomainCheckResponse; dnsRecords = body.cloudflare?.dnsRecords ?? []; const stillGenerating = dnsRecords.some((record) => record.value === 'loading'); if (dnsRecords.length > 0 && !stillGenerating) { recordsReady = true; break; } } if (attempt < DNS_POLL_MAX_ATTEMPTS - 1) { await new Promise((resolve) => setTimeout(resolve, DNS_POLL_INTERVAL_MS)); } } } catch { removeLastLog(); addLog(); return; } removeLastLog(); const readyTxtRecords = dnsRecords.filter( (record) => record.type === 'TXT' && record.value !== 'loading' ); const successMessage = basePathPending ? `domain ${domain} added; base path ${normalizedBasePath} queued for the next deploy` : basePathSaved ? `domain ${domain} added (serving at ${normalizedBasePath})` : `domain ${domain} added`; addLog(); addLog( Add these DNS records with your domain provider: {readyTxtRecords.map((record) => ( TXT {getRelativeRecordName(record.domain, domain)} → {record.value} ))} CNAME {getRelativeRecordName(domain, domain)} → {CUSTOM_DOMAIN_CNAME_TARGET} Add the TXT records first; add the CNAME once they're validated. {basePathPending && ( The base path takes effect when that deploy finishes; the current path stays up until then. )} {basePathSaved && ( The CNAME sends all paths on {domain} to Mintlify. Only add it if this domain hosts nothing else. Otherwise, set up a reverse proxy for {normalizedBasePath}: https://www.mintlify.com/docs/deploy/docs-subpath )} {!recordsReady && ( Some TXT records are still being generated. Check the dashboard in a moment for the rest. )} ); }