dns CLI for managing DNS records

Files
README.md

dns #

Query DNS, and manage DNS records and zones at hosting providers, from the shell. Every provider is a plugin. The app and the plugins are bundles, and they install with bundle.

Install bundle if you don't yet have it. Then click the link to install dns: nzip:pipobscure.com/dns

Or, from a terminal:

bundle install @pipobscure.com/dns
bundle search --for @pipobscure.com/dns                          # the provider plugins
bundle install --for @pipobscure.com/dns @pipobscure.com/desec   # one of them
dns login example.com --provider desec
dns list example.com

See packages/dns/README.md for using it.

This repository #

packages/dns the app, @pipobscure/dns: the dns command, and a library other apps can take as a plugin
packages/provider/* the provider plugins this project publishes, one package each (@pipobscure/dns-<provider>)
tools/pack.ts builds every bundle into build/
tools/smoke.ts runs the built app with every built plugin installed
tools/release.sh the release, run on the spindle for a version tag
.tangled/workflows CI and release, on tangled's spindle

Nothing here is published to npm. Every package is private, and it ships as a bundle.

The sources are erasable-syntax TypeScript that node runs as it is, from disk and from inside an archive. There is no build step: what is in a bundle is what is in src/.

npm ci
npm run dns -- providers      # the app from source, with every plugin linked from its folder
npm test                      # every package's tests
npm run typecheck
npm run pack && npm run smoke # build the bundles, and run them as installed

Node 26.11.1 or later, because node:vfs is what bundles run on.

Releasing #

Everything is released together, at one version:

npm run version:set 0.2.0
git commit -am v0.2.0 && git tag -a v0.2.0 -m v0.2.0 && git push --follow-tags

The tag starts release.yml on the spindle. It tests, builds and smoke-tests every bundle, and attaches them to the tag as release files. It then attests each one (bundle attest --kind published) and publishes the listings: dns for the app, and one for each plugin, named for its provider (desec, cloudflare) and listed --for the app. Being listed for the app is what marks a plugin, so the dns- its package and file carry is left out of its listing. It needs the secrets TANGLED_HANDLE and TANGLED_APP_PASSWORD (tangled secret set …). Run by hand (tangled run trigger -w release.yml), it is a dry run.

The release job gets the tangled CLI with bundle install, accepting it only with its publisher's attestation (ci/policy.json).

Writing a provider plugin #

Anyone can write and publish a provider. A plugin is a package that dns imports by name, and its default export is a Provider, as provider.ts describes:

import type { Provider } from '@pipobscure/dns/provider';

const provider: Provider = {
    api: 1,
    name: 'example',                     // what `--provider` and accounts call it
    title: 'Example DNS',
    settings: [{ name: 'token', description: 'an API token', secret: true, env: 'EXAMPLE_TOKEN' }],
    open(settings, context) {
        return {
            async zones() { … },             // [{ name: 'example.com' }]
            async records(zone, filter) { … },  // [{ name, type, ttl, records: ['presentation data'] }]
            async apply(zone, changes) { … },   // { action: 'set', rrset } | { action: 'delete', name, type }
        };
    },
};
export default provider;
  • Name it as you like. The package name, the listing name and the provider name can all differ. dns imports a plugin once to learn what it offers and caches that by its build, and dns login records which package an account's provider came from. This project's own plugins are @pipobscure/dns-<provider>, in packages/provider/<provider>, listed as <provider>.
  • Speak RRsets in presentation format. Names are fully qualified, without the trailing dot. Record data is zone-file text with absolute names (10 mail.example.com.). TXT is quoted strings. Convert to and from your API's shapes with context.rdata.
  • Use context.http for requests. It handles JSON, gives error messages from the response body, waits out rate limits, and logs under --verbose. A plugin needs no dependencies, and imports nothing of dns at run time, only types.
  • login is optional: implement it if your provider can do better than asking for each setting, such as creating a scoped token from a username and password.
  • Develop it linked: bundle install --for @pipobscure/dns ./my-plugin links the folder, and the installed dns loads your plugin from it as it is, every run, so a change is there at once. Installing the released plugin replaces the link.
  • Test against fakeContext in packages/dns/test/support.ts. provider/desec is the reference.
  • Ship it as a plain bundle (no launcher), and publish it for the app: bundle publish --for @pipobscure.com/dns <provider> <url>, named for the provider it adds.

Licence #

EUPL-1.2