npm package: @janmarten/dns.d
Description: [![Build](https://github.com/janmartenjongerius/dns.d/actions/workflows/build.yml/badge.svg)](https://github.com/janmartenjongerius/dns.d/actions/workflows/build.yml) ![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubuserconte
Latest version: 0.1.0

# Declarative DNS configuration 

[![Build](https://github.com/janmartenjongerius/dns.d/actions/workflows/build.yml/badge.svg)](https://github.com/janmartenjongerius/dns.d/actions/workflows/build.yml)
![Endpoint Badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fjanmartenjongerius%2F70694ffe1e7271d4e8c8931e58c800a0%2Fraw%2Fdafd3ef56f28e125f3fa33482e838b8ef74f7237%2Fdns.d-lcov-coverage.json)

This library is built to declaratively manage DNS records and DNS zone 
configuration, using a fluid interface.
It can be extremely useful in declarative DNS record back-ups or when using
Infrastructure as Code (IaC) to provision a managed DNS zone.

## Features

The following are core features of this library:

- Declarative configuration of DNS records
- Human-readable interface for non-descript and difficult to understand DNS components
- Safe by design
  - data is stored in private properties
  - immutable record objects
- Automatic normalization, quoting and partitioning of TXT records
- Fluid support for different DNS providers
- Query records through the DNS zone like using the 
  [`dig`](https://en.wikipedia.org/wiki/Dig_(command)) command on an actual DNS zone.

Some DX considerations:

- TypeScript `.d.ts` declaration files provided
- JavaScript source map files provided
- Autocompletion of parameters in modern IDEs

## Installation

```bash
npm install @janmarten/dns.d
```

## Usage

This library can be used to declare DNS zones and their records.

### Create a DNS zone

The following shows how to declare a new DNS zone and some corresponding mail
DNS records.

```javascript
import dns from '@janmarten/dns.d';
import {DmarcAlignment, DmarcPolicy} from "@janmarten/dns.d/record/txt/dmarc";

const zone = dns.Zone('example.com');

zone.txt.SpfRecord('')
    .withPolicy('~all')
    .withInclude('_spf.google.com');

zone.txt.DkimRecord('gmail')
    .withTtl(600)
    .withPublicKey('foo');

zone.txt.DmarcRecord('')
    .withPolicy(DmarcPolicy.Reject)
    .withDkimAlignment(DmarcAlignment.Strict)
    .withSpfAlignment(DmarcAlignment.Strict)
    .withAggregateReportReceiver('foo+rua@example.com')
    .withForensicReportReceiver('foo+ruf@example.com')
    .withReportInterval(7 * 24 * 3600);

console.log(JSON.stringify(zone));
```

This will output:

```json
{
  "domain": "example.com",
  "records": [
    {
      "name": "",
      "type": "TXT",
      "ttl": 300,
      "content": "\"v=spf1 include:_spf.google.com ~all\""
    },
    {
      "name": "gmail._domainkey",
      "type": "TXT",
      "ttl": 600,
      "content": "\"v=DKIM1; k=rsa; p=foo\""
    },
    {
      "name": "",
      "type": "TXT",
      "ttl": 300,
      "content": "\"v=DMARC1; p=reject; adkim=s; aspf=s; rua=mailto:foo+rua@example.com; ruf=mailto:foo+ruf@example.com; ri=604800\""
    }
  ]
}
```

## Create a single DNS record

Rather than create a DNS zone, singular records can be created instead.

```javascript
import dns from '@janmarten/dns.d';

const record = dns.txt.SpfRecord('@')
    .withInclude('_spf.google.com')
    .withPolicy('~all');

console.log(JSON.stringify(record));
```

```json
{
  "name": "@",
  "type": "TXT",
  "ttl": 300,
  "content": "\"v=spf1 include:_spf.google.com ~all\""
}
```

> **Note:** DNS record objects are immutable and their `with*` methods produce a
> new object with the patched configuration.

When using the DNS records outside a DNS zone, it is up to the user to
determine if the DNS record names should contain just the subdomain name or the
full DNS name (including the zone domain).

## Supported DNS record types

The following DNS record types are supported. Each record is registered as a
factory and configurator on the `dns` top level object for creating records and
on the DNS Zone object produced from `dns.Zone('domain.tld')`, further described
as `zone`, such that mentions of `dns.*.*` can be replaced with `zone.*.*` to
get the equivalent behavior from the DNS zone.

### TXT records

The following are supported specialized factories for the TXT record type.
If the specialized type you are looking for is not implemented, please use the
`GenericTxtRecord` to circumvent this.
Contributions with new specialized types are welcomed.

| Factory                 | Record             | Description                                                                                                   |
|:------------------------|:-------------------|:--------------------------------------------------------------------------------------------------------------|
| `dns.txt.GenericRecord` | `GenericTxtRecord` | Used to create a TXT record without constraints on the record content.                                        |
| `dns.txt.SpfRecord`     | `SpfRecord`        | Specialized record object to configure Sender Policy Framework records.                                       |
| `dns.txt.DkimRecord`    | `DkimRecord`       | Specialized record object to configure DomainKeys Identified Mail records.                                    |
| `dns.txt.DmarcRecord`   | `DmarcRecord`      | Specialized record object to configure Domain-based Message Authentication Reporting and Conformance records. |

## Query the DNS zone

To query for DNS records inside the zone, use the `dig` method.

```typescript
function dig(type: DnsRecordType, name?: string | RegExp): DnsRecords;
```

Where `type` must be a valid DNS record type and `name` can either be the exact
name of the record or a regular expression to match the record name.

By default, the `name` parameter defaults to `/^@?$/` which matches both `@` and
empty names.

```javascript
import dns from '@janmarten/dns.d';

const zone = dns.Zone('example.com');

zone.txt.SpfRecord('');
zone.txt.DkimRecord('gmail');
zone.txt.DmarcRecord('');

const [spf, dmarc] = zone.dig('TXT');
const [dkim] = zone.dig('TXT', 'gmail._domainkey');

console.log({spf, dmarc, dkim});
```

This outputs:

```
{ spf: SpfRecord {}, dmarc: DmarcRecord {}, dkim: DkimRecord {} }
```

In order to find all DKIM records on a DNS zone, use a regular expression like so:

```javascript
import dns from '@janmarten/dns.d';

const zone = dns.Zone('example.com');

zone.txt.DkimRecord('gmail');
zone.txt.DkimRecord('outlook');

const dkimRecords = zone.dig('TXT', /^.+\._domainkey$/);

console.log(dkimRecords);
```

```
[ DkimRecord {}, DkimRecord {} ]
```

## DNS Provider specific configuration

The DNS zone builder can be configured with provider specific configuration to
ensure the resulting DNS records are valid.

To use a Google Cloud specific configuration, use the `gcp` object to create
your DNS zone instead.

```javascript
import gcp from '@janmarten/dns.d';

const zone = gcp.Zone('example.com');
```

From that point forward, any normalization of DNS records happens in a Google
Cloud DNS context.

To implement support for a lesser known DNS provider, use your own configuration:

```javascript
import {createDnsProvider} from '@janmarten/dns.d';

const dns = createDnsProvider({
    recordResourceMaximumLength: 12,
});
```

### DNS provider configuration

The following configuration can be provided in the `context` parameter of the
`createDnsProvider` factory:

> Properties ending in `?` are optional and can be omitted.

| Property                       | Type   | Description                                            |
|:-------------------------------|:-------|:-------------------------------------------------------|
| `recordResourceMaximumLength?` | Number | The partition size used to split up TXT record values. |

## DNS Zone Helpers

To convert the configuration to a provider specific DNS zone, a helper is available.

```javascript
import {convertZoneToRecords} from "@janmarten/dns.d/zone/gcp";

console.log(
    JSON.stringify(
        convertZoneToRecords(zone)
    )
);
```

When applied to the `zone` as configured in the first usage example, it outputs:

```JSON
[
  {
    "type": "TXT",
    "name": "example.com.",
    "ttl": 300,
    "rrdatas": [
      "\"v=spf1 include:_spf.google.com ~all\"",
      "\"v=DMARC1; p=reject; adkim=s; aspf=s; rua=mailto:foo+rua@example.com; ruf=mailto:foo+ruf@example.com; ri=604800\""
    ]
  },
  {
    "type": "TXT",
    "name": "gmail._domainkey.example.com.",
    "ttl": 600,
    "rrdatas": [
      "\"v=DKIM1; k=rsa; p=foo\""
    ]
  }
]

```

Note that the above output shows the successful combination of multiple TXT
records on the same DNS name.

### Google Cloud DNS helpers

The Google Cloud DNS helpers can be imported from `@janmarten/dns.d/zone/gcp`.

#### Convert zone to Google Cloud DNS records

Convert a DNS zone to DNS records compatible with Google Cloud DNS configuration.

```typescript
function convertZoneToRecords(zone: DnsZone): GoogleCloudDnsRecordDTO[];
```

In order to successfully convert DNS records to a Google Cloud DNS record list,
all the records of the Zone must be known, to ensure that records of the same
record type and DNS name are combined into a single record.

> **Warning** If multiple records with the same type and DNS name are found with
> different TTL values, the helper will produce an error asking to correct this.

#### Normalize Google Cloud DNS name

Normalize the given DNS zone and DNS record into a DNS name.

```typescript
function normalizeDnsName(zone: DnsZone, record: DnsRecord): string;
```
E.g. `DnsZone<domain=example.com>` + `DnsRecord<name=www>` normalizes to `www.example.com.`

This normalizer also supports `@` as alias to the domain apex, such that a DNS
record with name `@` for a zone with domain `example.com` produces
`example.com.` as DNS name.

## Upcoming functionality

The following is a list of upcoming functionality:

- SPF features:
  - Mechanisms:
    - `ip4:` Specifies an IPv4 address or range allowed to send emails.
    - `ip6:` Specifies an IPv6 address or range allowed to send emails.
    - `a`, `a:` Matches if the sender's IP matches any A or AAAA record of the domain.
    - `mx`, `mx:` Matches if the sender's IP matches any MX record of the domain.
    - `ptr` Matches if the reverse DNS lookup for the sender's IP points to a domain in the SPF record.
  - Modifiers:
    - `redirect=` Redirects SPF evaluation to another domain's SPF record. This is useful when another domain is managing SPF policies for you.
    - `exp=` Provides a custom explanation string when an SPF FAIL result occurs.
  - Validators:
    - SPF record should not exceed 10 DNS lookups
- DKIM features:
  - `g` (Granularity) Specifies which email addresses are allowed to use the DKIM key.
  - `s` (Services) Defines which services or protocols can use the key for signing.
  - `t` (Flags) Specifies flags to control DKIM behavior.
  - `n` (Policy) Provides a human-readable note about the key.
- DMARC features:
  - `sp` Specifies a separate DMARC policy for subdomains if different from the main domain's policy.
  - `pct` Specifies the percentage of messages subject to the policy. Useful for gradual enforcement.
  - `rf` Specifies the format for forensic reports.
  - `fo` Defines the conditions for sending failure (forensic) reports.
  - `i` Provides additional options for identifier alignment
- More record types (`A`, `AAAA`, `CNAME`, etc.)
- Support configuration of DNSSEC on the DNS zone