Skip to content
DNSControlEngineering

DNSControl's dnsconfig.js, in a browser

Run dnsconfig.js in the browser without the DNSControl CLI. See what DNSMigrator imports, how the Web Worker is sandboxed, and where preview differs.

6 min readDNSMigrator team

Pasting a dnsconfig.js file into a browser does not require uploading creds.json, running Node.js, or giving a page access to your filesystem. DNSMigrator evaluates the configuration language in a dedicated Web Worker, exposes DNSControl-style DSL functions, and collects a zone model. The important boundary is that this imports configuration intent, not an arbitrary Node program.

dnsconfig.js in the browser: how the worker runs#

DNSControl describes its input as a JavaScript DSL. Calls such as NewDnsProvider, D, A, and MX construct a configuration that preview or push later compares with providers. It is JavaScript, so variables, arrays, helper functions, and loops can reduce repetition.

A small file looks like this:

dnsconfig.js
var REG_NONE = NewRegistrar("none");
var DSP_MAIN = NewDnsProvider("main", "CLOUDFLAREAPI");

D("example.com", REG_NONE,
  DnsProvider(DSP_MAIN),
  DefaultTTL(300),
  A("@", "192.0.2.44"),
  AAAA("@", "2001:db8::44"),
  CNAME("www", "example.com."),
  MX("@", 10, "mail.example.net."),
  TXT("@", "v=spf1 include:_spf.example.net -all"),
  IGNORE("_acme-challenge", "TXT", "*")
);

REG_NONE says DNSControl is not managing a registrar for this domain. DSP_MAIN names a DNS provider configuration; credentials are normally resolved separately by DNSControl. D declares the zone and receives record and domain modifiers.

The browser importer does not need the secret behind main. It records the declared provider name and maps the provider type to DNSMigrator's catalog. The collected result contains zones, providers, registrars, warnings, and errors. You choose or create actual provider connections separately.

See DNSControl's primary references for D, NewDnsProvider, and DnsProvider. Our mapping is documented in Coming from DNSControl and dnsconfig.js import and export.

How dnsconfig.js runs in the browser worker#

The web app sends the script and optional provider-type hints to dnsconfig.worker.ts. The worker creates a fresh runtime, takes the names of the runtime's allowed globals, and evaluates the script with those names as function arguments under strict mode. It then serializes the collected model to JSON and posts that result back.

Before evaluation, the worker explicitly disables a list of browser capabilities: fetch, XMLHttpRequest, WebSocket, EventSource, importScripts, IndexedDB, Cache Storage, BroadcastChannel, nested workers, WebTransport, navigator, and location. The runtime supplies DNS functions rather than Node globals. Provider credentials are not sent with the script.

The worker boundary keeps evaluation off the page’s main execution stack, and the importer has no intended network or persistent-browser-storage path. The wrapper rejects scripts over 1 MB and terminates the worker after three seconds. It is still a configuration evaluator, not a general hostile-code execution service, and memory-heavy code can pressure the browser before termination. Only paste code you trust.

The core runtime, not the worker, implements the DSL. It builds raw domains and records, normalizes names and TTLs, merges RRsets, validates record syntax, expands builders and per-record IP transforms, and records ignore rules and import transforms for later planning. That separation makes the same parser testable without a browser.

Supported language surface#

The importer supports the core domain declarations:

  • D, D_EXTEND, DEFAULTS, DOMAIN_ELSEWHERE, and DOMAIN_ELSEWHERE_AUTO
  • NewRegistrar, NewDnsProvider, DnsProvider, DefaultTTL, TTL, and NAMESERVER
  • record functions for the release’s canonical types, including A, AAAA, TXT, HTTPS, SVCB, DS, TLSA, LOC, aliases, redirects, and the supported Cloudflare, Akamai, LuaDNS, ClouDNS, AdGuard Home, and MikroTik extensions
  • IGNORE, IGNORE_NAME, IGNORE_TARGET, IGNORE_EXTERNAL_DNS, NO_PURGE, and PURGE
  • AUTODNSSEC_ON, AUTODNSSEC_OFF, import transforms, reverse-zone helpers, and record builders

Record functions whose types are outside the release’s canonical model are reported in import warnings and skipped rather than being stored as records.

For example, a builder remains declarative JavaScript:

dnsconfig.js
D("example.com", REG_NONE,
  DnsProvider(DSP_MAIN),
  SPF_BUILDER({
    label: "@",
    parts: [
      "v=spf1",
      "include:_spf.mail.example",
      "ip4:192.0.2.0/24",
      "-all"
    ]
  })
);

The SPF runtime can resolve nested includes when a DNS resolver is supplied to the core async collector. The browser worker intentionally creates the runtime without a resolver. If this file requests SPF flattening, synchronous collection warns that flattening needs a resolver and leaves the policy unflattened rather than fetching from arbitrary names named by the script.

Where browser import differs from dnscontrol preview#

File and network composition do not map safely to a pasted browser document. The runtime defines require, require_glob, glob, FETCH, fetch, and HASH as unavailable. Calling one produces a direct error telling you that the function is not available in the browser importer. Paste the required local file contents into one script or resolve generated data before import.

This DNSControl pattern, for example, is not importable as written:

dnsconfig.js
require("./zones/example.js");
var ranges = FETCH("https://vendor.example/ip-ranges.txt");

The limitation is useful: a config import cannot quietly read another local path or turn a URL in pasted code into an outbound request. It also means import is not byte-for-byte DNSControl execution. Review warnings and errors instead of assuming every JavaScript helper has an equivalent.

Dynamic DNSControl code can generate many concrete records. DNSMigrator stores the collected result, not the generator's original control flow. A loop and 100 literal calls that produce the same RRsets become the same zone model.

Export dnsconfig.js from the canonical zone#

Export works in the opposite direction. DNSMigrator takes saved desired zones and emits a DNSControl-compatible script with registrar and provider declarations, one D call per zone, provider modifiers, defaults, ignore rules, transforms, absent records, and record calls.

A generated fragment might be:

dnsconfig.js
// Generated by DNSMigrator. DNSControl-compatible dnsconfig.js.

var REG_NONE = NewRegistrar("none");
var DSP_MAIN = NewDnsProvider("main", "CLOUDFLAREAPI");

D("example.com", REG_NONE,
  DnsProvider(DSP_MAIN),
  DefaultTTL(300),
  A("@", "192.0.2.44"),
  MX("@", 10, "mail.example.net.")
);

The exporter defaults includeSecrets to false. A Hurricane Electric dynamic-DNS key stored in record metadata, for example, is omitted and produces a warning unless a caller explicitly opts into secret export. Provider API credentials are connection data and do not belong in the generated zone script.

Export preserves supported canonical semantics, not source formatting. Comments, variable names, helper functions, loop structure, and record order from an imported file are not a round trip. Some canonical metadata also has no exact DNSControl expression. The exporter emits warnings when it drops or approximates such data; for example, current routing export handles weighted Route 53 metadata but warns for other routing policies.

A practical import/export workflow#

  1. Remove secrets from the config and keep creds.json out of the paste.
  2. Inline files referenced by require and precompute data fetched by custom code.
  3. Import, then read every warning and error.
  4. Check provider mappings rather than trusting guessed names such as prod or main.
  5. Review the normalized zones, record counts, ignore rules, purge behavior, aliases, and provider-specific records.
  6. Connect providers independently and use a preview before any push.
  7. Export the saved zone and run DNSControl's own preview if DNSControl will consume the result.
  8. Keep generated scripts in version control only after checking that no secret-bearing metadata was included.

You can use the browser for the common DNSControl workflow without pretending it is a Node runtime. The narrower contract—JavaScript in, canonical DNS intent out—is what makes the feature reviewable. Check each construct against the DNSControl parity reference, compare the broader DNSControl alternatives, and use preview and push only after resolving import warnings. If you export BIND as part of the handoff, run it through the zone-file validator.

What is dnsconfig.js?
It is DNSControl’s JavaScript input file. Calls such as D, A, MX, and DnsProvider construct desired DNS data that DNSControl can compare with live providers during preview or push.
Can I import dnsconfig.js without creds.json?
Yes. DNSMigrator imports configuration intent without receiving provider credentials from creds.json; you map declared providers to encrypted workspace connections separately. Remove any secrets that were embedded directly in the JavaScript before pasting it.
Is browser import the same as dnscontrol preview?
No. Import evaluates a supported DSL with network, files, modules, and several helpers disabled, then returns a canonical model. dnscontrol preview also reads live providers with credentials and calculates corrections, so run the official command when you need exact DNSControl behavior.
Does DNSMigrator support DNSControl TypeScript?
The importer evaluates JavaScript syntax and DNSControl-style globals; it does not run a TypeScript compiler in the browser. Compile TypeScript-specific syntax and resolve imports before pasting the resulting trusted JavaScript.
Can exported dnsconfig.js round-trip the original source?
It round-trips supported DNS intent, not source text. Comments, variable names, helper functions, loops, record order, and unavailable metadata may change or produce warnings, so review the generated file and run dnscontrol preview before push.