@sdxc/zone-file 2026.10.13 → 2026.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -5
- package/dist/addresses.d.ts +17 -0
- package/dist/addresses.js +50 -0
- package/dist/cli-contract.d.ts +33 -0
- package/dist/cli-contract.js +81 -0
- package/dist/cli-format.d.ts +21 -0
- package/dist/cli-format.js +138 -0
- package/dist/cli-input.d.ts +47 -0
- package/dist/cli-input.js +73 -0
- package/dist/cli-parse.d.ts +21 -0
- package/dist/cli-parse.js +43 -0
- package/dist/cli-validate.d.ts +21 -0
- package/dist/cli-validate.js +40 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.js +19 -0
- package/dist/dnssec.d.ts +18 -0
- package/dist/dnssec.js +53 -0
- package/dist/format-record-data.d.ts +3 -4
- package/dist/format-record-data.js +24 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/octets.d.ts +15 -0
- package/dist/octets.js +37 -0
- package/dist/parse-record-data.js +115 -16
- package/dist/parse.js +2 -0
- package/dist/record-types.d.ts +8 -0
- package/dist/record-types.js +23 -0
- package/dist/svc-params.d.ts +24 -0
- package/dist/svc-params.js +338 -0
- package/dist/types.d.ts +91 -2
- package/package.json +9 -4
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `sdxc-zone-file` command-line program: the contract in `cli-contract.ts` with each command
|
|
4
|
+
* mapped onto its handler, which loads only when that command runs. Exit codes: 0 done, 1 the
|
|
5
|
+
* zone file has entries it could not use, 2 usage or unreadable input.
|
|
6
|
+
*
|
|
7
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
8
|
+
* @copyright Sergio Xalambrí 2026
|
|
9
|
+
*/
|
|
10
|
+
/** The `sdxc-zone-file` program, run by the bin entry below and by tests through `invoke()`. */
|
|
11
|
+
export declare const cli: import("@sdxc/cli").CLI<import("@sdxc/cli").Group<Omit<{
|
|
12
|
+
readonly description: "sdxc-zone-file — read, check and write RFC 1035 DNS zone files";
|
|
13
|
+
readonly validate: import("@sdxc/cli").Command<{
|
|
14
|
+
readonly zone: import("@sdxc/cli").Argument<string>;
|
|
15
|
+
}, {
|
|
16
|
+
readonly origin: import("@sdxc/cli").ValueFlag<string, true, false, false>;
|
|
17
|
+
readonly ttl: import("@sdxc/cli").ValueFlag<number, false, false, false>;
|
|
18
|
+
readonly json: import("@sdxc/cli").Flag<boolean>;
|
|
19
|
+
}>;
|
|
20
|
+
readonly parse: import("@sdxc/cli").Command<{
|
|
21
|
+
readonly zone: import("@sdxc/cli").Argument<string>;
|
|
22
|
+
}, {
|
|
23
|
+
readonly origin: import("@sdxc/cli").ValueFlag<string, true, false, false>;
|
|
24
|
+
readonly ttl: import("@sdxc/cli").ValueFlag<number, false, false, false>;
|
|
25
|
+
readonly json: import("@sdxc/cli").Flag<boolean>;
|
|
26
|
+
}>;
|
|
27
|
+
readonly format: import("@sdxc/cli").Command<{
|
|
28
|
+
readonly input: import("@sdxc/cli").Argument<string>;
|
|
29
|
+
}, {
|
|
30
|
+
readonly origin: import("@sdxc/cli").ValueFlag<string, true, false, false>;
|
|
31
|
+
readonly ttl: import("@sdxc/cli").ValueFlag<number, false, false, false>;
|
|
32
|
+
readonly relative: import("@sdxc/cli").Flag<boolean>;
|
|
33
|
+
}>;
|
|
34
|
+
}, "description" | "hidden" | "arguments" | "flags" | "tasks">, Record<never, never>, Record<never, never>>, []>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `sdxc-zone-file` command-line program: the contract in `cli-contract.ts` with each command
|
|
4
|
+
* mapped onto its handler, which loads only when that command runs. Exit codes: 0 done, 1 the
|
|
5
|
+
* zone file has entries it could not use, 2 usage or unreadable input.
|
|
6
|
+
*
|
|
7
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
8
|
+
* @copyright Sergio Xalambrí 2026
|
|
9
|
+
*/
|
|
10
|
+
import { createCLI, lazy } from "@sdxc/cli";
|
|
11
|
+
import { ZONE_FILE_COMMANDS } from "./cli-contract.js";
|
|
12
|
+
/** The `sdxc-zone-file` program, run by the bin entry below and by tests through `invoke()`. */
|
|
13
|
+
export const cli = createCLI({ name: "sdxc-zone-file", commands: ZONE_FILE_COMMANDS });
|
|
14
|
+
cli.map(ZONE_FILE_COMMANDS.validate, lazy(() => import("./cli-validate.js")));
|
|
15
|
+
cli.map(ZONE_FILE_COMMANDS.parse, lazy(() => import("./cli-parse.js")));
|
|
16
|
+
cli.map(ZONE_FILE_COMMANDS.format, lazy(() => import("./cli-format.js")));
|
|
17
|
+
if (import.meta.main) {
|
|
18
|
+
process.exit(await cli.run(process.argv.slice(2)));
|
|
19
|
+
}
|
package/dist/dnssec.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DNSSEC pieces DS and DNSKEY data share: the algorithm mnemonics RFC 4034 lets a zone
|
|
3
|
+
* file write in place of a number, and the key tag a DS record names its DNSKEY by, so a
|
|
4
|
+
* delegation's DS set can be matched against the child's keys.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
/** An algorithm field as a number, read from its decimal or its mnemonic; `null` for neither. */
|
|
10
|
+
export declare function readAlgorithm(field: string | undefined): number | null;
|
|
11
|
+
/**
|
|
12
|
+
* The key tag of DNSKEY RDATA (RFC 4034 Appendix B): the octets summed as big-endian 16-bit
|
|
13
|
+
* words with the carry folded back in once, and for an RSA/MD5 (algorithm 1) key the
|
|
14
|
+
* modulus's second- and third-to-last octets, as that appendix specifies for it.
|
|
15
|
+
*
|
|
16
|
+
* @param rdata - The DNSKEY RDATA in wire form: flags, protocol, algorithm and key.
|
|
17
|
+
*/
|
|
18
|
+
export declare function keyTag(rdata: Uint8Array): number;
|
package/dist/dnssec.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DNSSEC pieces DS and DNSKEY data share: the algorithm mnemonics RFC 4034 lets a zone
|
|
3
|
+
* file write in place of a number, and the key tag a DS record names its DNSKEY by, so a
|
|
4
|
+
* delegation's DS set can be matched against the child's keys.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
/** The DNSSEC algorithm numbers by IANA mnemonic; the uppercase mnemonic reads as its number. */
|
|
10
|
+
const ALGORITHMS = {
|
|
11
|
+
RSAMD5: 1,
|
|
12
|
+
DH: 2,
|
|
13
|
+
DSA: 3,
|
|
14
|
+
RSASHA1: 5,
|
|
15
|
+
"DSA-NSEC3-SHA1": 6,
|
|
16
|
+
"RSASHA1-NSEC3-SHA1": 7,
|
|
17
|
+
RSASHA256: 8,
|
|
18
|
+
RSASHA512: 10,
|
|
19
|
+
"ECC-GOST": 12,
|
|
20
|
+
ECDSAP256SHA256: 13,
|
|
21
|
+
ECDSAP384SHA384: 14,
|
|
22
|
+
ED25519: 15,
|
|
23
|
+
ED448: 16,
|
|
24
|
+
INDIRECT: 252,
|
|
25
|
+
PRIVATEDNS: 253,
|
|
26
|
+
PRIVATEOID: 254,
|
|
27
|
+
};
|
|
28
|
+
/** An algorithm field as a number, read from its decimal or its mnemonic; `null` for neither. */
|
|
29
|
+
export function readAlgorithm(field) {
|
|
30
|
+
if (field === undefined)
|
|
31
|
+
return null;
|
|
32
|
+
if (/^\d+$/.test(field)) {
|
|
33
|
+
let number = Number(field);
|
|
34
|
+
return number <= 0xff ? number : null;
|
|
35
|
+
}
|
|
36
|
+
return ALGORITHMS[field.toUpperCase()] ?? null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The key tag of DNSKEY RDATA (RFC 4034 Appendix B): the octets summed as big-endian 16-bit
|
|
40
|
+
* words with the carry folded back in once, and for an RSA/MD5 (algorithm 1) key the
|
|
41
|
+
* modulus's second- and third-to-last octets, as that appendix specifies for it.
|
|
42
|
+
*
|
|
43
|
+
* @param rdata - The DNSKEY RDATA in wire form: flags, protocol, algorithm and key.
|
|
44
|
+
*/
|
|
45
|
+
export function keyTag(rdata) {
|
|
46
|
+
if (rdata[3] === 1 && rdata.length >= 7)
|
|
47
|
+
return ((rdata[rdata.length - 3] ?? 0) << 8) | (rdata[rdata.length - 2] ?? 0);
|
|
48
|
+
let sum = 0;
|
|
49
|
+
for (let index = 0; index < rdata.length; index++)
|
|
50
|
+
sum += index % 2 === 0 ? (rdata[index] ?? 0) << 8 : (rdata[index] ?? 0);
|
|
51
|
+
sum += (sum >> 16) & 0xffff;
|
|
52
|
+
return sum & 0xffff;
|
|
53
|
+
}
|
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type * as ZoneFile from "./types.js";
|
|
10
10
|
/**
|
|
11
|
-
* Prints a record's data in canonical presentation format
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `parseRecordData(data.type, formatRecordData(data))` gives back `data`.
|
|
11
|
+
* Prints a record's data in canonical presentation format: names absolute, free text quoted
|
|
12
|
+
* with every octet outside printable ASCII as `\DDD`, hex lowercase, SvcParams in key order,
|
|
13
|
+
* untyped data as is. `parseRecordData(data.type, formatRecordData(data))` gives back `data`.
|
|
15
14
|
*
|
|
16
15
|
* @param data - Record data, as `parseRecordData` produces it.
|
|
17
16
|
* @returns The RDATA in presentation format.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { absoluteName } from "./names.js";
|
|
2
|
+
import { printSvcParams } from "./svc-params.js";
|
|
2
3
|
/** Encodes a character-string into the octets `\DDD` escapes stand for. */
|
|
3
4
|
const ENCODER = new TextEncoder();
|
|
4
5
|
/**
|
|
5
|
-
* Prints a record's data in canonical presentation format
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `parseRecordData(data.type, formatRecordData(data))` gives back `data`.
|
|
6
|
+
* Prints a record's data in canonical presentation format: names absolute, free text quoted
|
|
7
|
+
* with every octet outside printable ASCII as `\DDD`, hex lowercase, SvcParams in key order,
|
|
8
|
+
* untyped data as is. `parseRecordData(data.type, formatRecordData(data))` gives back `data`.
|
|
9
9
|
*
|
|
10
10
|
* @param data - Record data, as `parseRecordData` produces it.
|
|
11
11
|
* @returns The RDATA in presentation format.
|
|
@@ -62,6 +62,26 @@ export function printRecordData(data, printName) {
|
|
|
62
62
|
soa.minimum,
|
|
63
63
|
].join(" ");
|
|
64
64
|
}
|
|
65
|
+
case "SVCB":
|
|
66
|
+
case "HTTPS": {
|
|
67
|
+
let { priority, target, params } = data;
|
|
68
|
+
let printed = printSvcParams(params, quote);
|
|
69
|
+
return `${priority} ${printName(target)}${printed ? ` ${printed}` : ""}`;
|
|
70
|
+
}
|
|
71
|
+
case "TLSA": {
|
|
72
|
+
let tlsa = data;
|
|
73
|
+
return `${tlsa.usage} ${tlsa.selector} ${tlsa.matchingType} ${tlsa.certificateAssociation}`;
|
|
74
|
+
}
|
|
75
|
+
case "DS":
|
|
76
|
+
case "CDS": {
|
|
77
|
+
let ds = data;
|
|
78
|
+
return `${ds.keyTag} ${ds.algorithm} ${ds.digestType} ${ds.digest}`;
|
|
79
|
+
}
|
|
80
|
+
case "DNSKEY":
|
|
81
|
+
case "CDNSKEY": {
|
|
82
|
+
let key = data;
|
|
83
|
+
return `${key.flags} ${key.protocol} ${key.algorithm} ${key.publicKey}`;
|
|
84
|
+
}
|
|
65
85
|
default:
|
|
66
86
|
return String(data.data);
|
|
67
87
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,5 +12,5 @@ export { RecordDataError, ZoneFileError } from "./errors.js";
|
|
|
12
12
|
export { formatRecordData } from "./format-record-data.js";
|
|
13
13
|
export { parse } from "./parse.js";
|
|
14
14
|
export { parseRecordData } from "./parse-record-data.js";
|
|
15
|
-
export { canonicalType, typeName } from "./record-types.js";
|
|
15
|
+
export { canonicalType, typeCode, typeName } from "./record-types.js";
|
|
16
16
|
export { stringify } from "./stringify.js";
|
package/dist/index.js
CHANGED
|
@@ -10,5 +10,5 @@ export { RecordDataError, ZoneFileError } from "./errors.js";
|
|
|
10
10
|
export { formatRecordData } from "./format-record-data.js";
|
|
11
11
|
export { parse } from "./parse.js";
|
|
12
12
|
export { parseRecordData } from "./parse-record-data.js";
|
|
13
|
-
export { canonicalType, typeName } from "./record-types.js";
|
|
13
|
+
export { canonicalType, typeCode, typeName } from "./record-types.js";
|
|
14
14
|
export { stringify } from "./stringify.js";
|
package/dist/octets.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hex and base64 codecs for the binary fields of DNSSEC, DANE and SVCB records, which the
|
|
3
|
+
* presentation format writes as text a zone file may split across whitespace and lines.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
/** The octets a hex string spells, whitespace ignored; `null` for an odd or non-hex run, or none. */
|
|
9
|
+
export declare function decodeHex(text: string): Uint8Array | null;
|
|
10
|
+
/** Octets as lowercase hex, the spelling DS digests and TLSA data compare in. */
|
|
11
|
+
export declare function encodeHex(octets: Uint8Array): string;
|
|
12
|
+
/** The octets a base64 string spells, whitespace ignored; `null` for text that is not base64, or none. */
|
|
13
|
+
export declare function decodeBase64(text: string): Uint8Array | null;
|
|
14
|
+
/** Octets as padded base64, the spelling DNSKEY keys and SVCB `ech` print in. */
|
|
15
|
+
export declare function encodeBase64(octets: Uint8Array): string;
|
package/dist/octets.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hex and base64 codecs for the binary fields of DNSSEC, DANE and SVCB records, which the
|
|
3
|
+
* presentation format writes as text a zone file may split across whitespace and lines.
|
|
4
|
+
*
|
|
5
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
+
* @copyright Sergio Xalambrí 2026
|
|
7
|
+
*/
|
|
8
|
+
/** Hex text in pairs, with any whitespace between digits already removed. */
|
|
9
|
+
const HEX_PATTERN = /^(?:[0-9a-fA-F]{2})+$/;
|
|
10
|
+
/** Canonical base64: complete quads, padding only at the end. */
|
|
11
|
+
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
12
|
+
/** The octets a hex string spells, whitespace ignored; `null` for an odd or non-hex run, or none. */
|
|
13
|
+
export function decodeHex(text) {
|
|
14
|
+
let hex = text.replace(/\s+/g, "");
|
|
15
|
+
if (!HEX_PATTERN.test(hex))
|
|
16
|
+
return null;
|
|
17
|
+
let octets = new Uint8Array(hex.length / 2);
|
|
18
|
+
for (let index = 0; index < octets.length; index++)
|
|
19
|
+
octets[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
20
|
+
return octets;
|
|
21
|
+
}
|
|
22
|
+
/** Octets as lowercase hex, the spelling DS digests and TLSA data compare in. */
|
|
23
|
+
export function encodeHex(octets) {
|
|
24
|
+
return Array.from(octets, (octet) => octet.toString(16).padStart(2, "0")).join("");
|
|
25
|
+
}
|
|
26
|
+
/** The octets a base64 string spells, whitespace ignored; `null` for text that is not base64, or none. */
|
|
27
|
+
export function decodeBase64(text) {
|
|
28
|
+
let base64 = text.replace(/\s+/g, "");
|
|
29
|
+
if (base64.length === 0 || !BASE64_PATTERN.test(base64))
|
|
30
|
+
return null;
|
|
31
|
+
let binary = atob(base64);
|
|
32
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
33
|
+
}
|
|
34
|
+
/** Octets as padded base64, the spelling DNSKEY keys and SVCB `ech` print in. */
|
|
35
|
+
export function encodeBase64(octets) {
|
|
36
|
+
return btoa(Array.from(octets, (octet) => String.fromCharCode(octet)).join(""));
|
|
37
|
+
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { failure, isFailure, success } from "@sdxc/result";
|
|
2
|
+
import { canonicalIpv6, isIpv4, printIpv6 } from "./addresses.js";
|
|
2
3
|
import { decodeOctets, readCharacterStrings } from "./character-strings.js";
|
|
4
|
+
import { keyTag, readAlgorithm } from "./dnssec.js";
|
|
3
5
|
import { RecordDataError } from "./errors.js";
|
|
4
6
|
import { canonicalName, printLabels } from "./names.js";
|
|
7
|
+
import { decodeBase64, decodeHex, encodeBase64, encodeHex } from "./octets.js";
|
|
5
8
|
import { canonicalType } from "./record-types.js";
|
|
9
|
+
import { readSvcParams, readWireSvcParams } from "./svc-params.js";
|
|
6
10
|
import { readTtl } from "./ttl.js";
|
|
7
|
-
/**
|
|
8
|
-
* A dotted-quad IPv4 address without leading zeros, which `inet_aton` would read as octal
|
|
9
|
-
* and browsers as decimal, so an address carrying them has no single meaning.
|
|
10
|
-
*/
|
|
11
|
-
const IPV4_PATTERN = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
|
|
12
11
|
/**
|
|
13
12
|
* Reads a record's data into its typed fields; untyped types come back as `{ type, data }`.
|
|
14
13
|
* Names come back canonical (lowercased, escapes resolved, no trailing dot, the root as
|
|
@@ -39,19 +38,41 @@ function readNumber(field, max) {
|
|
|
39
38
|
let number = Number(field);
|
|
40
39
|
return number <= max ? number : null;
|
|
41
40
|
}
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The fields of DNSKEY data from its wire octets, the key tag computed over them, so the
|
|
43
|
+
* presentation and generic spellings of one key agree on every field.
|
|
44
|
+
*/
|
|
45
|
+
function dnskeyFields(rdata) {
|
|
46
|
+
let flags = ((rdata[0] ?? 0) << 8) | (rdata[1] ?? 0);
|
|
47
|
+
return {
|
|
48
|
+
flags,
|
|
49
|
+
zoneKey: (flags & 0x0100) !== 0,
|
|
50
|
+
secureEntryPoint: (flags & 0x0001) !== 0,
|
|
51
|
+
protocol: rdata[2] ?? 0,
|
|
52
|
+
algorithm: rdata[3] ?? 0,
|
|
53
|
+
publicKey: encodeBase64(rdata.subarray(4)),
|
|
54
|
+
keyTag: keyTag(rdata),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Reads the leading `count` whitespace-separated fields and the rest of the text after them. */
|
|
58
|
+
function splitLeading(value, count) {
|
|
59
|
+
let fields = [];
|
|
60
|
+
let rest = value;
|
|
61
|
+
for (let index = 0; index < count; index++) {
|
|
62
|
+
let match = /^(\S+)\s*/.exec(rest);
|
|
63
|
+
if (!match)
|
|
64
|
+
break;
|
|
65
|
+
fields.push(match[1] ?? "");
|
|
66
|
+
rest = rest.slice(match[0].length);
|
|
67
|
+
}
|
|
68
|
+
return [fields, rest];
|
|
48
69
|
}
|
|
49
70
|
/** Reads presentation-format data for the typed record types. */
|
|
50
71
|
function parsePresentation(type, value) {
|
|
51
72
|
let fields = value.split(/\s+/).filter((field) => field.length > 0);
|
|
52
73
|
switch (type) {
|
|
53
74
|
case "A":
|
|
54
|
-
return
|
|
75
|
+
return isIpv4(value) ? success({ type, address: value }) : invalid(type, value);
|
|
55
76
|
case "AAAA": {
|
|
56
77
|
let address = canonicalIpv6(value);
|
|
57
78
|
return address ? success({ type, address }) : invalid(type, value);
|
|
@@ -133,6 +154,50 @@ function parsePresentation(type, value) {
|
|
|
133
154
|
minimum,
|
|
134
155
|
});
|
|
135
156
|
}
|
|
157
|
+
case "SVCB":
|
|
158
|
+
case "HTTPS": {
|
|
159
|
+
let [[priorityField, targetField], rest] = splitLeading(value, 2);
|
|
160
|
+
let priority = readNumber(priorityField, 0xffff);
|
|
161
|
+
let target = targetField === undefined ? null : canonicalName(targetField);
|
|
162
|
+
let params = readSvcParams(rest);
|
|
163
|
+
if (priority === null || target === null || params === null)
|
|
164
|
+
return invalid(type, value);
|
|
165
|
+
return success({ type, priority, target, params });
|
|
166
|
+
}
|
|
167
|
+
case "TLSA": {
|
|
168
|
+
let [leading, rest] = splitLeading(value, 3);
|
|
169
|
+
let [usage, selector, matchingType] = leading.map((field) => readNumber(field, 0xff));
|
|
170
|
+
let association = decodeHex(rest);
|
|
171
|
+
if (usage == null || selector == null || matchingType == null || !association)
|
|
172
|
+
return invalid(type, value);
|
|
173
|
+
let certificateAssociation = encodeHex(association);
|
|
174
|
+
return success({ type, usage, selector, matchingType, certificateAssociation });
|
|
175
|
+
}
|
|
176
|
+
case "DS":
|
|
177
|
+
case "CDS": {
|
|
178
|
+
let [[tagField, algorithmField, digestTypeField], rest] = splitLeading(value, 3);
|
|
179
|
+
let keyTag = readNumber(tagField, 0xffff);
|
|
180
|
+
let algorithm = readAlgorithm(algorithmField);
|
|
181
|
+
let digestType = readNumber(digestTypeField, 0xff);
|
|
182
|
+
let digest = decodeHex(rest);
|
|
183
|
+
if (keyTag === null || algorithm === null || digestType === null || !digest)
|
|
184
|
+
return invalid(type, value);
|
|
185
|
+
return success({ type, keyTag, algorithm, digestType, digest: encodeHex(digest) });
|
|
186
|
+
}
|
|
187
|
+
case "DNSKEY":
|
|
188
|
+
case "CDNSKEY": {
|
|
189
|
+
let [[flagsField, protocolField, algorithmField], rest] = splitLeading(value, 3);
|
|
190
|
+
let flags = readNumber(flagsField, 0xffff);
|
|
191
|
+
let protocol = readNumber(protocolField, 0xff);
|
|
192
|
+
let algorithm = readAlgorithm(algorithmField);
|
|
193
|
+
let key = decodeBase64(rest);
|
|
194
|
+
if (flags === null || protocol === null || algorithm === null || !key)
|
|
195
|
+
return invalid(type, value);
|
|
196
|
+
let rdata = new Uint8Array(4 + key.length);
|
|
197
|
+
rdata.set([flags >> 8, flags & 0xff, protocol, algorithm]);
|
|
198
|
+
rdata.set(key, 4);
|
|
199
|
+
return success({ type, ...dnskeyFields(rdata) });
|
|
200
|
+
}
|
|
136
201
|
default:
|
|
137
202
|
return success({ type, data: value });
|
|
138
203
|
}
|
|
@@ -187,10 +252,7 @@ class WireReader {
|
|
|
187
252
|
}
|
|
188
253
|
case "AAAA": {
|
|
189
254
|
let bytes = this.#bytes(16);
|
|
190
|
-
|
|
191
|
-
return null;
|
|
192
|
-
let groups = Array.from({ length: 8 }, (_, index) => (((bytes[index * 2] ?? 0) << 8) | (bytes[index * 2 + 1] ?? 0)).toString(16));
|
|
193
|
-
return { address: canonicalIpv6(groups.join(":")) ?? groups.join(":") };
|
|
255
|
+
return bytes ? { address: printIpv6(bytes) } : null;
|
|
194
256
|
}
|
|
195
257
|
case "CNAME":
|
|
196
258
|
case "PTR":
|
|
@@ -250,6 +312,39 @@ class WireReader {
|
|
|
250
312
|
let [serial, refresh, retry, expire, minimum] = numbers;
|
|
251
313
|
return { primary, mailbox, serial, refresh, retry, expire, minimum };
|
|
252
314
|
}
|
|
315
|
+
case "SVCB":
|
|
316
|
+
case "HTTPS": {
|
|
317
|
+
let priority = this.#uint(2);
|
|
318
|
+
let target = this.#name();
|
|
319
|
+
let params = readWireSvcParams(this.#rest());
|
|
320
|
+
if (priority === null || target === null || params === null)
|
|
321
|
+
return null;
|
|
322
|
+
return { priority, target, params };
|
|
323
|
+
}
|
|
324
|
+
case "TLSA": {
|
|
325
|
+
let [usage, selector, matchingType] = [this.#uint(1), this.#uint(1), this.#uint(1)];
|
|
326
|
+
let association = this.#rest();
|
|
327
|
+
if (usage === null || selector === null || matchingType === null)
|
|
328
|
+
return null;
|
|
329
|
+
if (association.length === 0)
|
|
330
|
+
return null;
|
|
331
|
+
return { usage, selector, matchingType, certificateAssociation: encodeHex(association) };
|
|
332
|
+
}
|
|
333
|
+
case "DS":
|
|
334
|
+
case "CDS": {
|
|
335
|
+
let [keyTag, algorithm, digestType] = [this.#uint(2), this.#uint(1), this.#uint(1)];
|
|
336
|
+
let digest = this.#rest();
|
|
337
|
+
if (keyTag === null || algorithm === null || digestType === null)
|
|
338
|
+
return null;
|
|
339
|
+
if (digest.length === 0)
|
|
340
|
+
return null;
|
|
341
|
+
return { keyTag, algorithm, digestType, digest: encodeHex(digest) };
|
|
342
|
+
}
|
|
343
|
+
case "DNSKEY":
|
|
344
|
+
case "CDNSKEY": {
|
|
345
|
+
let rdata = this.#rest();
|
|
346
|
+
return rdata.length > 4 ? dnskeyFields(rdata) : null;
|
|
347
|
+
}
|
|
253
348
|
default:
|
|
254
349
|
return "unknown";
|
|
255
350
|
}
|
|
@@ -262,6 +357,10 @@ class WireReader {
|
|
|
262
357
|
this.#offset += length;
|
|
263
358
|
return bytes;
|
|
264
359
|
}
|
|
360
|
+
/** Every octet left, which the types ending in a variable-length field read as that field. */
|
|
361
|
+
#rest() {
|
|
362
|
+
return this.#bytes(this.#octets.length - this.#offset) ?? new Uint8Array();
|
|
363
|
+
}
|
|
265
364
|
/** A big-endian unsigned integer of `size` octets. */
|
|
266
365
|
#uint(size) {
|
|
267
366
|
let bytes = this.#bytes(size);
|
package/dist/parse.js
CHANGED
package/dist/record-types.d.ts
CHANGED
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
import type * as ZoneFile from "./types.js";
|
|
9
9
|
/** The mnemonic for a type code, or `TYPEnnn` when it has none here. */
|
|
10
10
|
export declare function typeName(code: number): string;
|
|
11
|
+
/**
|
|
12
|
+
* The IANA code of a type, given as a mnemonic or `TYPEnnn`, so a caller building a query
|
|
13
|
+
* message can name the type the way a zone file does; `null` for a mnemonic it does not know.
|
|
14
|
+
*
|
|
15
|
+
* @example typeCode("HTTPS") // 65
|
|
16
|
+
* @example typeCode("TYPE65280") // 65280
|
|
17
|
+
*/
|
|
18
|
+
export declare function typeCode(type: string): number | null;
|
|
11
19
|
/**
|
|
12
20
|
* The spelling of a type as a record's `type`: the uppercase mnemonic, and a known
|
|
13
21
|
* mnemonic for `TYPEnnn` when one exists.
|
package/dist/record-types.js
CHANGED
|
@@ -43,6 +43,13 @@ const TYPED_TYPES = new Set([
|
|
|
43
43
|
"CAA",
|
|
44
44
|
"SRV",
|
|
45
45
|
"SOA",
|
|
46
|
+
"SVCB",
|
|
47
|
+
"HTTPS",
|
|
48
|
+
"TLSA",
|
|
49
|
+
"DS",
|
|
50
|
+
"CDS",
|
|
51
|
+
"DNSKEY",
|
|
52
|
+
"CDNSKEY",
|
|
46
53
|
]);
|
|
47
54
|
/** The mnemonic for each code in {@link TYPE_CODES}. */
|
|
48
55
|
const TYPE_NAMES = new Map(Object.entries(TYPE_CODES).map(([name, code]) => [code, name]));
|
|
@@ -50,6 +57,22 @@ const TYPE_NAMES = new Map(Object.entries(TYPE_CODES).map(([name, code]) => [cod
|
|
|
50
57
|
export function typeName(code) {
|
|
51
58
|
return TYPE_NAMES.get(code) ?? `TYPE${code}`;
|
|
52
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The IANA code of a type, given as a mnemonic or `TYPEnnn`, so a caller building a query
|
|
62
|
+
* message can name the type the way a zone file does; `null` for a mnemonic it does not know.
|
|
63
|
+
*
|
|
64
|
+
* @example typeCode("HTTPS") // 65
|
|
65
|
+
* @example typeCode("TYPE65280") // 65280
|
|
66
|
+
*/
|
|
67
|
+
export function typeCode(type) {
|
|
68
|
+
let upper = type.trim().toUpperCase();
|
|
69
|
+
let generic = /^TYPE(\d{1,5})$/.exec(upper);
|
|
70
|
+
if (generic) {
|
|
71
|
+
let code = Number(generic[1]);
|
|
72
|
+
return code <= 0xffff ? code : null;
|
|
73
|
+
}
|
|
74
|
+
return TYPE_CODES[upper] ?? null;
|
|
75
|
+
}
|
|
53
76
|
/**
|
|
54
77
|
* The spelling of a type as a record's `type`: the uppercase mnemonic, and a known
|
|
55
78
|
* mnemonic for `TYPEnnn` when one exists.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type * as ZoneFile from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reads the params of SVCB or HTTPS presentation data, the fields after the target.
|
|
4
|
+
* Fails on an unknown or repeated key and on a value its key does not accept.
|
|
5
|
+
*
|
|
6
|
+
* @param text - Zero or more `key[=value]` fields.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readSvcParams(text: string): ZoneFile.SvcParams | null;
|
|
9
|
+
/**
|
|
10
|
+
* Reads the params of SVCB or HTTPS wire data, the octets after the target. Keys must
|
|
11
|
+
* strictly increase, as RFC 9460 requires of the wire form.
|
|
12
|
+
*
|
|
13
|
+
* @param octets - The SvcParams octets, `key length value` repeated.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readWireSvcParams(octets: Uint8Array): ZoneFile.SvcParams | null;
|
|
16
|
+
/**
|
|
17
|
+
* Prints params in canonical presentation form, keys in ascending number order and values
|
|
18
|
+
* quoted wherever they are free text, so two spellings of one record print alike.
|
|
19
|
+
*
|
|
20
|
+
* @param params - The params to print.
|
|
21
|
+
* @param quote - Prints a character-string with its escapes.
|
|
22
|
+
* @returns The fields joined by spaces; empty for no params.
|
|
23
|
+
*/
|
|
24
|
+
export declare function printSvcParams(params: ZoneFile.SvcParams, quote: (text: string) => string): string;
|