Guided wizards, opt-in every time, and automatic DNS as the first
A job that has a wizard now asks "Guide me / I'll do it myself" each time it starts; nothing is remembered. The shared wizard shell gives every guide a stepper, a side panel on what each step does and how to undo it, and the way forward or back. Automatic DNS, from a domain's DNS section: - finds where the domain's DNS is hosted from its SOA and NS records, and offers that host when the server can drive it; - for the major hosts, steps to create the narrowest credential, and the field named as the steps name it; - records grouped by what they do, TLSA off unless the zone is signed; - saves the provider and switches the domain over, removing the provider again if the switch fails; - watches the publishing task and public DNS, ticking each record green, and boils a host's refusal down to its distinct messages; - for hosts it can't drive, or domains not in DNS yet, every record laid out for copying, with the same live checks.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* Where is a domain's DNS hosted? Found the way any resolver would: the SOA
|
||||
* record names the zone that holds the domain (which may be a parent zone),
|
||||
* and the zone's NS records name the host. The host is then matched against
|
||||
* nameserver patterns we know for the providers the server can drive.
|
||||
*/
|
||||
import { dohQuery } from './liveCheck';
|
||||
|
||||
export interface DnsHosting {
|
||||
/** The zone holding the domain, e.g. example.com for mail.example.com. */
|
||||
zone: string;
|
||||
nameservers: string[];
|
||||
/** The server's provider type, when the host is one it can update. */
|
||||
variant: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nameserver suffixes by provider type. Only hosts whose nameservers are
|
||||
* unambiguous are listed: a wrong guess costs the user more than no guess.
|
||||
*/
|
||||
const NAMESERVERS: [RegExp, string][] = [
|
||||
[/\.ns\.cloudflare\.com$/, 'Cloudflare'],
|
||||
[/\.awsdns-\d+\.(com|net|org|co\.uk)$/, 'Route53'],
|
||||
[/^ns-cloud-[a-z]\d+\.googledomains\.com$/, 'GoogleCloudDns'],
|
||||
[/\.azure-dns\.(com|net|org|info)$/, 'AzureDns'],
|
||||
[/^ns\d\.digitalocean\.com$/, 'DigitalOcean'],
|
||||
[/\.ns\.hetzner\.(com|de)$/, 'Hetzner'],
|
||||
[/\.(ovh\.net|ovh\.ca|anycast\.me)$/, 'Ovh'],
|
||||
[/\.domaincontrol\.com$/, 'Godaddy'],
|
||||
[/\.porkbun\.com$/, 'Porkbun'],
|
||||
[/^ns\d\.desec\.(io|org)$/, 'DeSEC'],
|
||||
[/\.linode\.com$/, 'Linode'],
|
||||
[/\.vultr\.com$/, 'Vultr'],
|
||||
[/\.gandi\.net$|\.gandi-ns\.(fr|com|net)$/, 'GandiV5'],
|
||||
[/\.registrar-servers\.com$/, 'Namecheap'],
|
||||
[/\.dnsimple(-edge)?\.(com|net|org|info)$/, 'Dnsimple'],
|
||||
[/\.bunny\.net$/, 'Bunny'],
|
||||
[/\.nsone\.net$/, 'Ns1'],
|
||||
[/\.ui-dns\.(com|de|org|biz)$/, 'Ionos'],
|
||||
[/\.dnsmadeeasy\.com$/, 'DnsMadeEasy'],
|
||||
[/\.cloudns\.net$/, 'ClouDns'],
|
||||
[/^ns\d\.he\.net$/, 'Hurricane'],
|
||||
[/\.vercel-dns\.com$/, 'Vercel'],
|
||||
[/\.name\.com$/, 'NameDotCom'],
|
||||
[/\.inwx\.(de|net|eu)$/, 'Inwx'],
|
||||
[/\.transip\.(nl|net|eu)$/, 'Transip'],
|
||||
[/\.scw\.cloud$/, 'Scaleway'],
|
||||
[/\.infomaniak\.ch$/, 'Infomaniak'],
|
||||
[/\.dns-parking\.com$/, 'Hostinger'],
|
||||
[/\.akam\.net$/, 'EdgeDns'],
|
||||
[/\.exoscale\.(ch|net|io|com)$/, 'Exoscale'],
|
||||
[/\.netcup\.net$/, 'Netcup'],
|
||||
[/\.joker\.com$/, 'Joker'],
|
||||
[/\.glesys\.se$/, 'Glesys'],
|
||||
[/\.dreamhost\.com$/, 'Dreamhost'],
|
||||
[/\.easydns\.(com|net|org|info)$/, 'EasyDns'],
|
||||
[/\.ultradns\.(com|net|org|biz|info|co\.uk)$/, 'UltraDns'],
|
||||
[/\.mythic-beasts\.com$/, 'MythicBeasts'],
|
||||
[/\.luadns\.net$/, 'LuaDns'],
|
||||
[/\.spaceship\.net$/, 'Spaceship'],
|
||||
[/\.hosting\.de$/, 'HostingDe'],
|
||||
];
|
||||
|
||||
/** The provider type for a set of nameservers, if they all point to one we know. */
|
||||
export function providerForNameservers(nameservers: string[]): string | null {
|
||||
const hits = new Set<string>();
|
||||
for (const ns of nameservers) {
|
||||
const host = ns.toLowerCase().replace(/\.$/, '');
|
||||
const match = NAMESERVERS.find(([re]) => re.test(host));
|
||||
if (!match) return null;
|
||||
hits.add(match[1]);
|
||||
}
|
||||
return hits.size === 1 ? [...hits][0] : null;
|
||||
}
|
||||
|
||||
/** Find the zone and host for a domain; null when it isn't in public DNS yet. */
|
||||
export async function detectHosting(domain: string, signal?: AbortSignal): Promise<DnsHosting | null> {
|
||||
const soa = await dohQuery(domain, 'SOA', signal);
|
||||
const zoneRecord = [...soa.answer, ...soa.authority].find((a) => a.type === 6);
|
||||
const zone = zoneRecord?.name.replace(/\.$/, '').toLowerCase();
|
||||
// NXDOMAIN answers carry the TLD's SOA: that's "not registered", not a zone.
|
||||
if (!zone || !zone.includes('.') || soa.status === 3) return null;
|
||||
const ns = await dohQuery(zone, 'NS', signal);
|
||||
const nameservers = ns.answer.filter((a) => a.type === 2).map((a) => a.data.replace(/\.$/, '').toLowerCase());
|
||||
if (nameservers.length === 0) return null;
|
||||
return { zone, nameservers, variant: providerForNameservers(nameservers) };
|
||||
}
|
||||
Reference in New Issue
Block a user