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:
2026-09-19 01:41:00 -07:00
parent 2e02532279
commit 5641560a91
18 changed files with 2435 additions and 1 deletions
+155
View File
@@ -0,0 +1,155 @@
/*
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* The DNS providers the guided setup leads with, and how to get each one's
* credentials. The server supports many more; they're all offered under
* "Another provider", with their fields taken from the server's schema.
*
* `variant` is the server's name for the provider type. The steps are ours:
* keep them short, and aim for the narrowest credential that works.
*/
export interface ProviderGuide {
variant: string;
name: string;
/** One line on the tile. */
blurb: string;
/** How to make a credential, in order. */
steps: string[];
/** Where to start, on the provider's own site. */
link?: string;
/** The fields named the way the steps above name them. */
fields?: Record<string, { label: string; hint?: string }>;
}
export const FEATURED: ProviderGuide[] = [
{
variant: 'Cloudflare',
name: 'Cloudflare',
blurb: 'An API token limited to this one zone.',
steps: [
'In the Cloudflare dashboard, open My Profile → API Tokens and choose Create Token.',
'Use the "Edit zone DNS" template.',
'Under Zone Resources, pick Include → Specific zone → your domain, so the token can touch nothing else.',
'Create the token and paste it below. Cloudflare shows it only once.',
],
link: 'https://dash.cloudflare.com/profile/api-tokens',
fields: { secret: { label: 'API token', hint: 'The token from step 4.' } },
},
{
variant: 'Route53',
name: 'Amazon Route 53',
blurb: 'An IAM access key allowed to change one hosted zone.',
steps: [
'In IAM, create a user or role for the mail server.',
'Give it a policy that allows route53:ChangeResourceRecordSets and route53:ListResourceRecordSets on your hosted zone only, plus route53:ListHostedZonesByName.',
'Create an access key for it and paste the key ID and secret below.',
],
link: 'https://console.aws.amazon.com/iam/',
},
{
variant: 'GoogleCloudDns',
name: 'Google Cloud DNS',
blurb: 'A service account with the DNS Administrator role.',
steps: [
'In IAM & Admin → Service Accounts, create an account for the mail server.',
'Grant it the DNS Administrator role on the project that holds your zone.',
'Create a JSON key for it and paste the details below.',
],
link: 'https://console.cloud.google.com/iam-admin/serviceaccounts',
},
{
variant: 'AzureDns',
name: 'Azure DNS',
blurb: 'An app registration with DNS Zone Contributor on your zone.',
steps: [
'Register an application in Microsoft Entra ID and create a client secret.',
'On your DNS zone, open Access control (IAM) and give the app the DNS Zone Contributor role.',
'Paste the tenant, client and subscription IDs and the secret below.',
],
link: 'https://portal.azure.com/',
},
{
variant: 'DigitalOcean',
name: 'DigitalOcean',
blurb: 'A personal access token with domain access.',
steps: [
'Open API → Tokens and generate a new token.',
'Give it the "domain" scopes (read and update) only.',
'Paste the token below.',
],
link: 'https://cloud.digitalocean.com/account/api/tokens',
fields: { secret: { label: 'Access token' } },
},
{
variant: 'Hetzner',
name: 'Hetzner DNS',
blurb: 'A DNS API token.',
steps: ['In the Hetzner DNS console, open API tokens and create one.', 'Paste the token below.'],
link: 'https://dns.hetzner.com/settings/api-token',
fields: { secret: { label: 'API token' } },
},
{
variant: 'Ovh',
name: 'OVHcloud',
blurb: 'An application key and consumer key for the DNS API.',
steps: [
'Create API keys for your region, allowing GET, POST, PUT and DELETE on /domain/zone/*.',
'Paste the application key, application secret and consumer key below.',
],
link: 'https://api.ovh.com/createToken/',
},
{
variant: 'Godaddy',
name: 'GoDaddy',
blurb: 'A production API key and secret.',
steps: [
'In the GoDaddy developer portal, create a Production API key.',
'Paste the key and secret below. GoDaddy only allows API access on some account types.',
],
link: 'https://developer.godaddy.com/keys',
},
{
variant: 'Porkbun',
name: 'Porkbun',
blurb: 'An API key pair, with API access turned on for the domain.',
steps: [
'Open Account → API Access and create an API key.',
'In Domain Management, turn on API Access for this domain.',
'Paste the API key and secret below.',
],
link: 'https://porkbun.com/account/api',
},
{
variant: 'DeSEC',
name: 'deSEC',
blurb: 'A token, ideally limited to this domain.',
steps: [
'In deSEC, open Token Management and create a token.',
'Restrict it to this domain if you can, and paste it below.',
],
link: 'https://desec.io/tokens',
fields: { secret: { label: 'Token' } },
},
];
/** Provider types the guided setup doesn't offer: retired ones. */
export const HIDDEN_VARIANTS = new Set(['Deprecated1']);
/**
* Fields the guided setup leaves at the server's defaults: timing and
* bookkeeping. They stay editable on the DNS provider's own page.
*/
export const ADVANCED_FIELDS = new Set([
'description',
'memberTenantId',
'pollingInterval',
'propagationDelay',
'propagationTimeout',
'timeout',
'ttl',
]);