Skip to content

Commit ff794c3

Browse files
feat(dns): live delegation checks, registrar-aware setup, and record presets (#106)
* feat(dns): live delegation checks, registrar-aware setup, and record presets * update skil;ls * updates
1 parent 6c05e7f commit ff794c3

19 files changed

Lines changed: 1262 additions & 83 deletions

File tree

.changeset/spicy-otters-share.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bunny.net/cli": patch
3+
---
4+
5+
feat(dns): verify registrar delegation with a live nameserver lookup (instead of the API's NameserversDetected flag, which defaults to true on a fresh zone) across `dns zones ns`, `dns zones list`, and pull-zone setup; `dns zones add` and `ns` now give registrar-aware setup steps (registrar named via RDAP) and skip them when the domain is already delegated; add `dns records preset` with email, verification, and security presets (Google Workspace, Microsoft 365, Zoho, Mailgun, Resend, Proton, Bluesky, DMARC, CAA, no-email) plus a preset option in the `records add` wizard; color table heads bunny orange

AGENTS.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ bunny-cli/
175175
│ │ ├── client-options.ts # clientOptions() helper — builds ClientOptions from ResolvedConfig
176176
│ │ ├── define-command.ts # Command factory (see "Command Pattern" below)
177177
│ │ ├── define-namespace.ts # Namespace/group factory for subcommand trees
178+
│ │ ├── dns-nameservers.ts # BUNNY_NAMESERVERS + expectedNameservers(zone) + checkDelegation()/checkDelegations(): reads the parent zone's NS referral (raw UDP query of the registry, not the recursive answer a child host could spoof; falls back to dns.resolveNs when the referral is unreadable), matches the full expected set both ways, ground truth over bunny's NameserversDetected flag which defaults true on a fresh zone; checkDelegations is bounded-concurrency for the zone list
178179
│ │ ├── dns-record-types.ts # Canonical DNS record-type name⇄integer map (RECORD_TYPES) + recordTypeLabel(); shared by commands/dns + core/hostnames
179180
│ │ ├── errors.ts # Re-exports UserError/ApiError from @bunny.net/openapi-client + ConfigError
180181
│ │ ├── format.ts # Shared table/key-value rendering (text, table, csv, markdown)
@@ -191,6 +192,8 @@ bunny-cli/
191192
│ │ │ └── commands.ts # createHostnamesCommands(): add/ssl/list/remove factory parameterized by a pull-zone resolver
192193
│ │ ├── logger.ts # Chalk-based structured logger
193194
│ │ ├── manifest.ts # .bunny/ context file resolution (load, save, resolveManifestId)
195+
│ │ ├── registrar.ts # detectRegistrar()/parseRegistrar(): best-effort registrar name via RDAP (used by dns zones add to name the registrar in next-steps)
196+
│ │ ├── registrar.test.ts # Tests for parseRegistrar RDAP parsing + legal-suffix tidying
194197
│ │ ├── stats.ts # Shared stats rendering: sumChart(), renderBarChart(), formatBucketLabel() (UTC date labels), BAR_WIDTH (used by dns/zone/stats + scripts/stats)
195198
│ │ ├── stats.test.ts # Tests for stats helpers
196199
│ │ ├── types.ts # GlobalArgs, OutputFormat, and shared type definitions
@@ -301,21 +304,24 @@ bunny-cli/
301304
│ │ │ ├── record/ # `dns records` — entries within a zone (canonical: records; aliases: record, rec)
302305
│ │ │ │ ├── index.ts # defineNamespace("records", ...)
303306
│ │ │ │ ├── list.ts # List records in a zone (alias: ls)
304-
│ │ │ │ ├── add.ts # Add a record (positional grammar per type, or interactive wizard; --pull-zone/--script). Interactively, A/AAAA/CNAME/TXT offer static vs script-computed (Scriptable DNS) via pickOrCreateDnsScript: pick or create+seed a DNS script and write a SCRIPT record
307+
│ │ │ │ ├── add.ts # Add a record (positional grammar per type, or interactive wizard; --pull-zone/--script). Interactive wizard first offers "single record" vs a preset (pickAndApplyPreset); A/AAAA/CNAME/TXT offer static vs script-computed (Scriptable DNS) via pickOrCreateDnsScript: pick or create+seed a DNS script and write a SCRIPT record
305308
│ │ │ │ ├── update.ts # Update a record (alias: edit; prompts to pick zone+record when omitted)
306309
│ │ │ │ ├── remove.ts # Remove a record (alias: rm; prompts to pick zone+record when omitted)
310+
│ │ │ │ ├── preset.ts # `records preset [name] [domain]` (`list` lists; `--param key=value` repeatable for non-interactive runs): pick/apply a preset, gather params (flags then prompts in text mode), summarize, confirm, bulk-write. `--output json` writes then serializes the result; mid-sequence failures report how many records landed. Exports pickAndApplyPreset reused by add.ts
311+
│ │ │ │ ├── presets.ts # Preset catalog (data + pure build fns): google-workspace, microsoft365/outlook, zoho, mailgun, resend, proton, bluesky, dmarc, caa, no-email. findPreset(id|alias)
312+
│ │ │ │ ├── presets.test.ts # Tests for the pure preset build fns + alias resolution
307313
│ │ │ │ ├── import.ts # Import records from a BIND zone file (prompts for zone/file when omitted)
308314
│ │ │ │ └── export.ts # Export records as a BIND zone file (stdout, --file <path>, or --save → <domain>.zone)
309315
│ │ │ └── zone/ # `dns zones` — the zone itself (canonical: zones; aliases: zone; hidden: domain, domains)
310316
│ │ │ ├── index.ts # defineNamespace("zones", ...) + dnsZoneHiddenAliases (domain/domains)
311-
│ │ │ ├── list.ts # List all DNS zones (alias: ls)
312-
│ │ │ ├── add.ts # Create a DNS zone
317+
│ │ │ ├── list.ts # List all DNS zones (alias: ls); Nameservers column from a live per-zone NS lookup, not bunny's NameserversDetected flag
318+
│ │ │ ├── add.ts # Create a DNS zone, then print the bunny nameservers to set (naming the registrar via core/registrar.ts when RDAP resolves it)
313319
│ │ │ ├── link.ts # Link this directory to a zone → .bunny/dns.json (arg, else pick interactively)
314320
│ │ │ ├── unlink.ts # Remove .bunny/dns.json (alias-free; --force skips confirm)
315321
│ │ │ ├── show.ts # Show zone details (nameservers, SOA, DNSSEC, logging, record count)
316322
│ │ │ ├── remove.ts # Delete a DNS zone and its records (alias: rm)
317323
│ │ │ ├── stats.ts # Show DNS query statistics (TotalQueriesServed, by-type bar chart in text mode)
318-
│ │ │ ├── nameservers.ts # Show registrar nameservers (alias: ns; custom if set, else kiki/coco.bunny.net)
324+
│ │ │ ├── nameservers.ts # Check delegation (alias: ns): live NS lookup; on success a confirmation, otherwise the same "update your nameservers at [registrar]" guidance as zone add
319325
│ │ │ ├── dnssec/
320326
│ │ │ │ ├── index.ts # defineNamespace("dnssec", ...)
321327
│ │ │ │ ├── enable.ts # Enable DNSSEC, print DS record for the registrar
@@ -896,17 +902,18 @@ bunny
896902
│ │ ├── update [domain] [id] [--name] [--value] [--type] [--ttl] [--priority] [--weight] [--port] [--flags] [--tag] [--comment] [--disabled] [--pull-zone] [--script]
897903
│ │ │ Update a DNS record (alias: edit; prompts to pick zone+record when omitted)
898904
│ │ ├── remove [domain] [id] [--force] Remove a DNS record (alias: rm; prompts to pick zone+record when omitted)
905+
│ │ ├── preset [name] [domain] [--param key=value] Apply a preset record set (`preset list` lists; email providers, verification, security; --param repeatable for non-interactive runs)
899906
│ │ ├── import [domain] [file] Import records from a BIND zone file (prompts for zone/file when omitted)
900907
│ │ └── export [domain] [--file] [--save] Export a zone as a BIND zone file (stdout, --file <path>, or --save → <domain>.zone)
901908
│ └── zones (canonical; aliases: zone; hidden: domain, domains)
902909
│ ├── list List all DNS zones (alias: ls)
903-
│ ├── add <domain> Create a DNS zone
910+
│ ├── add <domain> Create a DNS zone (then prints the bunny nameservers to set, naming the registrar via RDAP when detectable)
904911
│ ├── link [domain] Link this directory to a zone → .bunny/dns.json (pick interactively when omitted)
905912
│ ├── unlink [--force] Remove .bunny/dns.json, unlinking this directory
906913
│ ├── show [domain] Show zone details (nameservers, SOA, DNSSEC, logging, record count)
907914
│ ├── remove [domain] [--force] Delete a DNS zone and its records (alias: rm)
908915
│ ├── stats [domain] [--from] [--to] Show DNS query statistics for a zone (defaults to last 30 days; text mode renders a bar chart)
909-
│ ├── nameservers [domain] (alias: ns) Show the nameservers to set at the registrar (custom if enabled, else bunny.net defaults)
916+
│ ├── nameservers [domain] (alias: ns) Live-check whether the registrar delegates to bunny.net; confirm on success, else show the nameservers to set at the named registrar
910917
│ ├── dnssec
911918
│ │ ├── enable [domain] Enable DNSSEC and print the DS record for the registrar
912919
│ │ └── disable [domain] [--force] Disable DNSSEC

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ bun ny apps deploy # first run? Imports docker-compose.
4848
bun ny apps link # interactive: pick from existing apps on the account
4949
bun ny apps link <app-id> # link a specific app to this directory (writes .bunny/app.json)
5050
bun ny apps unlink # remove .bunny/app.json
51+
bun ny dns zones add example.com # create a zone; prints registrar-aware setup steps (skipped if already delegated)
52+
bun ny dns zones nameservers example.com # live-check whether the registrar delegates to bunny
53+
bun ny dns records preset list # list DNS record presets (email providers, verification, security)
54+
bun ny dns records preset google-workspace example.com # apply a preset record set
55+
bun ny dns records preset bluesky example.com --param did=did:plc:abc123 # apply a preset non-interactively
5156
```
5257

5358
### Available Scripts

packages/cli/src/commands/dns/record/add.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from "../record-types.ts";
2121
import type { AnswerKind } from "../scripts/constants.ts";
2222
import { pickOrCreateDnsScript } from "../scripts/interactive.ts";
23+
import { pickAndApplyPreset } from "./preset.ts";
2324

2425
type AddDnsRecordModel = components["schemas"]["AddDnsRecordModel"];
2526
type RecordLinks = Pick<AddDnsRecordModel, "PullZoneId" | "ScriptId">;
@@ -268,6 +269,25 @@ export const dnsAddCommand = defineCommand<AddArgs>({
268269

269270
let record: AddDnsRecordModel;
270271
if (interactive) {
272+
// Offer a ready-made preset before falling back to building a single record by hand.
273+
const { mode } = await prompts({
274+
type: "select",
275+
name: "mode",
276+
message: "What would you like to add?",
277+
choices: [
278+
{ title: "A single record", value: "manual" },
279+
{
280+
title: "A preset (email providers, verification, security)",
281+
value: "preset",
282+
},
283+
],
284+
});
285+
if (mode === undefined) throw new UserError("A choice is required.");
286+
if (mode === "preset") {
287+
await pickAndApplyPreset({ client, zone, output });
288+
return;
289+
}
290+
271291
const { typeValue } = await prompts({
272292
type: "select",
273293
name: "typeValue",

packages/cli/src/commands/dns/record/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { dnsAddCommand } from "./add.ts";
33
import { dnsExportCommand } from "./export.ts";
44
import { dnsImportCommand } from "./import.ts";
55
import { dnsRecordListCommand } from "./list.ts";
6+
import { dnsPresetCommand } from "./preset.ts";
67
import { dnsRemoveCommand } from "./remove.ts";
78
import { dnsUpdateCommand } from "./update.ts";
89

@@ -14,6 +15,7 @@ export const dnsRecordNamespace = defineNamespace(
1415
dnsAddCommand,
1516
dnsUpdateCommand,
1617
dnsRemoveCommand,
18+
dnsPresetCommand,
1719
dnsImportCommand,
1820
dnsExportCommand,
1921
],

0 commit comments

Comments
 (0)