@@ -1816,6 +1816,30 @@ export function configDnsDomains(sites: Record<string, any>): string[] {
18161816 return [ ...domains ]
18171817}
18181818
1819+ /**
1820+ * Infer a DNS provider from a zone's authoritative nameservers.
1821+ *
1822+ * Provider API probes are intentionally the primary detection mechanism, but
1823+ * some registrars disable record API access per-domain. In that state the
1824+ * provider still owns the zone and should receive the attempted write so the
1825+ * deploy reports the real authorization error instead of incorrectly calling
1826+ * the zone externally managed.
1827+ */
1828+ export function dnsProviderNameFromNameservers ( nameservers : string [ ] ) : 'porkbun' | 'cloudflare' | 'route53' | 'godaddy' | null {
1829+ const normalized = nameservers . map ( name => name . toLowerCase ( ) . replace ( / \. $ / , '' ) )
1830+
1831+ if ( normalized . some ( name => name . endsWith ( '.porkbun.com' ) ) )
1832+ return 'porkbun'
1833+ if ( normalized . some ( name => name . endsWith ( '.ns.cloudflare.com' ) ) )
1834+ return 'cloudflare'
1835+ if ( normalized . some ( name => / ( ^ | \. ) a w s d n s - \d + \. (?: c o m | n e t | o r g | c o \. u k ) $ / . test ( name ) ) )
1836+ return 'route53'
1837+ if ( normalized . some ( name => name . endsWith ( '.domaincontrol.com' ) ) )
1838+ return 'godaddy'
1839+
1840+ return null
1841+ }
1842+
18191843async function reconcileConfigDns ( sites : Record < string , any > , logger : typeof log ) : Promise < void > {
18201844 const projectDnsConfig = await loadProjectDnsConfig ( dnsConfig )
18211845 const declared = ( [ 'a' , 'aaaa' , 'cname' , 'mx' , 'txt' ] as const )
@@ -1886,6 +1910,8 @@ async function reconcileHetznerDns(sites: Record<string, any>, ip: string, logge
18861910 providerConfigs . push ( { provider : 'porkbun' , apiKey : process . env . PORKBUN_API_KEY , secretKey : process . env . PORKBUN_SECRET_KEY } )
18871911 if ( process . env . CLOUDFLARE_API_TOKEN )
18881912 providerConfigs . push ( { provider : 'cloudflare' , apiToken : process . env . CLOUDFLARE_API_TOKEN } )
1913+ if ( process . env . GODADDY_API_KEY && process . env . GODADDY_API_SECRET )
1914+ providerConfigs . push ( { provider : 'godaddy' , apiKey : process . env . GODADDY_API_KEY , apiSecret : process . env . GODADDY_API_SECRET , environment : process . env . GODADDY_ENVIRONMENT } )
18891915 if ( process . env . AWS_ACCESS_KEY_ID || process . env . AWS_PROFILE )
18901916 providerConfigs . push ( { provider : 'route53' } )
18911917
@@ -1896,7 +1922,7 @@ async function reconcileHetznerDns(sites: Record<string, any>, ip: string, logge
18961922 return
18971923 }
18981924
1899- const { detectDnsProvider } = await import ( '@stacksjs/ts-cloud' ) as any
1925+ const { createDnsProvider , detectDnsProvider } = await import ( '@stacksjs/ts-cloud' ) as any
19001926 logger . info ( 'Reconciling DNS records...' )
19011927
19021928 // Best-effort A-record lookup so externally managed domains that already
@@ -1911,9 +1937,25 @@ async function reconcileHetznerDns(sites: Record<string, any>, ip: string, logge
19111937 }
19121938 }
19131939
1940+ const resolveAuthoritativeNameservers = async ( domain : string ) : Promise < string [ ] > => {
1941+ try {
1942+ const { resolveNs } = await import ( 'node:dns/promises' )
1943+ return await resolveNs ( domain )
1944+ }
1945+ catch {
1946+ return [ ]
1947+ }
1948+ }
1949+
19141950 for ( const domain of domains ) {
19151951 try {
1916- const provider = await detectDnsProvider ( domain , providerConfigs )
1952+ let provider = await detectDnsProvider ( domain , providerConfigs )
1953+ if ( ! provider ) {
1954+ const providerName = dnsProviderNameFromNameservers ( await resolveAuthoritativeNameservers ( domain ) )
1955+ const providerConfig = providerConfigs . find ( config => config . provider === providerName )
1956+ if ( providerConfig )
1957+ provider = createDnsProvider ( providerConfig )
1958+ }
19171959 if ( ! provider ) {
19181960 // No configured provider owns this zone — the records may still be
19191961 // correct (managed at the registrar). Only warn when they aren't.
0 commit comments