@sdxc/doh 2026.10.6 → 2026.10.10
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 +141 -37
- package/dist/caa/evaluate.d.ts +20 -0
- package/dist/caa/evaluate.js +87 -0
- package/dist/caa/may-issue.d.ts +21 -0
- package/dist/caa/may-issue.js +21 -0
- package/dist/caa/property.d.ts +19 -0
- package/dist/caa/property.js +54 -0
- package/dist/caa/relevant-set.d.ts +35 -0
- package/dist/caa/relevant-set.js +61 -0
- package/dist/caa/types.d.ts +137 -0
- package/dist/caa/types.js +0 -0
- package/dist/caa.d.ts +13 -0
- package/dist/caa.js +12 -0
- package/dist/errors.d.ts +4 -5
- package/dist/errors.js +5 -5
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/resolve.js +10 -7
- package/dist/types.d.ts +22 -69
- package/dist/types.js +0 -8
- package/package.json +5 -3
- package/dist/character-strings.d.ts +0 -22
- package/dist/character-strings.js +0 -72
- package/dist/parse-record-data.d.ts +0 -23
- package/dist/parse-record-data.js +0 -283
- package/dist/record-types.d.ts +0 -14
- package/dist/record-types.js +0 -54
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import { failure, isFailure, success } from "@sdxc/result";
|
|
2
|
-
import { decodeOctets, readCharacterStrings } from "./character-strings.js";
|
|
3
|
-
import { RecordDataError } from "./errors.js";
|
|
4
|
-
import { canonicalType } from "./record-types.js";
|
|
5
|
-
/**
|
|
6
|
-
* A dotted-quad IPv4 address without leading zeros, which `inet_aton` would read as octal
|
|
7
|
-
* and browsers as decimal, so an address carrying them has no single meaning.
|
|
8
|
-
*/
|
|
9
|
-
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}$/;
|
|
10
|
-
/**
|
|
11
|
-
* Reads a record's data into its typed fields. Types without a typed reading come back
|
|
12
|
-
* as `{ type, data }` with the data unchanged. Names are lowercased without the trailing
|
|
13
|
-
* dot, except the root, which stays `"."`.
|
|
14
|
-
*
|
|
15
|
-
* @param type - The record type, as a mnemonic or RFC 3597 `TYPEnnn`.
|
|
16
|
-
* @param data - The RDATA in presentation format.
|
|
17
|
-
* @returns The record's fields, or why the data does not fit the type.
|
|
18
|
-
* @template Type - The record type, which selects the fields returned.
|
|
19
|
-
* @example parseRecordData("MX", "10 mx.example.com.") // success({ type: "MX", preference: 10, exchange: "mx.example.com" })
|
|
20
|
-
*/
|
|
21
|
-
export function parseRecordData(type, data) {
|
|
22
|
-
let canonical = canonicalType(type);
|
|
23
|
-
let value = data.trim();
|
|
24
|
-
let parsed = value.startsWith("\\#")
|
|
25
|
-
? parseGeneric(canonical, value)
|
|
26
|
-
: parsePresentation(canonical, value);
|
|
27
|
-
return parsed;
|
|
28
|
-
}
|
|
29
|
-
/** Fails with the reason a record's data does not fit its type. */
|
|
30
|
-
function invalid(type, data) {
|
|
31
|
-
return failure(new RecordDataError(`Invalid ${type} record data: ${JSON.stringify(data)}`));
|
|
32
|
-
}
|
|
33
|
-
/** Folds a domain name: lowercased, trailing dot dropped, the root kept as `"."`. */
|
|
34
|
-
function normalizeName(name) {
|
|
35
|
-
let lower = name.toLowerCase();
|
|
36
|
-
return lower.length > 1 && lower.endsWith(".") ? lower.slice(0, -1) : lower;
|
|
37
|
-
}
|
|
38
|
-
/** Reads an unsigned decimal field no larger than `max`, or `null`. */
|
|
39
|
-
function readNumber(field, max) {
|
|
40
|
-
if (field === undefined || !/^\d+$/.test(field))
|
|
41
|
-
return null;
|
|
42
|
-
let number = Number(field);
|
|
43
|
-
return number <= max ? number : null;
|
|
44
|
-
}
|
|
45
|
-
/** An IPv6 literal in RFC 5952 canonical form, or `null` when the text is not one. */
|
|
46
|
-
function canonicalIpv6(value) {
|
|
47
|
-
if (!value.includes(":"))
|
|
48
|
-
return null;
|
|
49
|
-
let url = URL.parse(`http://[${value}]/`);
|
|
50
|
-
return url ? url.hostname.slice(1, -1) : null;
|
|
51
|
-
}
|
|
52
|
-
/** Reads presentation-format data for the typed record types. */
|
|
53
|
-
function parsePresentation(type, value) {
|
|
54
|
-
let fields = value.split(/\s+/).filter((field) => field.length > 0);
|
|
55
|
-
switch (type) {
|
|
56
|
-
case "A":
|
|
57
|
-
return IPV4_PATTERN.test(value) ? success({ type, address: value }) : invalid(type, value);
|
|
58
|
-
case "AAAA": {
|
|
59
|
-
let address = canonicalIpv6(value);
|
|
60
|
-
return address ? success({ type, address }) : invalid(type, value);
|
|
61
|
-
}
|
|
62
|
-
case "CNAME":
|
|
63
|
-
case "NS": {
|
|
64
|
-
if (fields.length !== 1 || fields[0] === undefined)
|
|
65
|
-
return invalid(type, value);
|
|
66
|
-
let name = normalizeName(fields[0]);
|
|
67
|
-
return success(type === "CNAME" ? { type, target: name } : { type, host: name });
|
|
68
|
-
}
|
|
69
|
-
case "MX": {
|
|
70
|
-
let preference = readNumber(fields[0], 0xffff);
|
|
71
|
-
if (fields.length !== 2 || preference === null || fields[1] === undefined)
|
|
72
|
-
return invalid(type, value);
|
|
73
|
-
return success({ type, preference, exchange: normalizeName(fields[1]) });
|
|
74
|
-
}
|
|
75
|
-
case "TXT": {
|
|
76
|
-
let strings = readCharacterStrings(value);
|
|
77
|
-
if (isFailure(strings))
|
|
78
|
-
return strings;
|
|
79
|
-
return success({ type, text: strings.data.join(""), strings: strings.data });
|
|
80
|
-
}
|
|
81
|
-
case "CAA": {
|
|
82
|
-
let match = /^(\d+)\s+([A-Za-z0-9]+)\s+(.+)$/.exec(value);
|
|
83
|
-
let flags = readNumber(match?.[1], 0xff);
|
|
84
|
-
if (!match || flags === null || match[2] === undefined || match[3] === undefined)
|
|
85
|
-
return invalid(type, value);
|
|
86
|
-
let raw = match[3];
|
|
87
|
-
let text = raw;
|
|
88
|
-
if (raw.startsWith('"')) {
|
|
89
|
-
let strings = readCharacterStrings(raw);
|
|
90
|
-
if (isFailure(strings))
|
|
91
|
-
return strings;
|
|
92
|
-
text = strings.data.join("");
|
|
93
|
-
}
|
|
94
|
-
return success({
|
|
95
|
-
type,
|
|
96
|
-
critical: (flags & 0x80) !== 0,
|
|
97
|
-
tag: match[2].toLowerCase(),
|
|
98
|
-
value: text,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
case "SRV": {
|
|
102
|
-
let [priority, weight, port] = fields.slice(0, 3).map((field) => readNumber(field, 0xffff));
|
|
103
|
-
let target = fields[3];
|
|
104
|
-
if (fields.length !== 4 ||
|
|
105
|
-
priority == null ||
|
|
106
|
-
weight == null ||
|
|
107
|
-
port == null ||
|
|
108
|
-
target === undefined) {
|
|
109
|
-
return invalid(type, value);
|
|
110
|
-
}
|
|
111
|
-
return success({ type, priority, weight, port, target: normalizeName(target) });
|
|
112
|
-
}
|
|
113
|
-
case "SOA": {
|
|
114
|
-
let [primary, mailbox] = fields;
|
|
115
|
-
let numbers = fields.slice(2).map((field) => readNumber(field, 0xffffffff));
|
|
116
|
-
if (fields.length !== 7 ||
|
|
117
|
-
primary === undefined ||
|
|
118
|
-
mailbox === undefined ||
|
|
119
|
-
numbers.some((n) => n === null)) {
|
|
120
|
-
return invalid(type, value);
|
|
121
|
-
}
|
|
122
|
-
let [serial, refresh, retry, expire, minimum] = numbers;
|
|
123
|
-
return success({
|
|
124
|
-
type,
|
|
125
|
-
primary: normalizeName(primary),
|
|
126
|
-
mailbox: normalizeName(mailbox),
|
|
127
|
-
serial,
|
|
128
|
-
refresh,
|
|
129
|
-
retry,
|
|
130
|
-
expire,
|
|
131
|
-
minimum,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
default:
|
|
135
|
-
return success({ type, data: value });
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/** Decodes RFC 3597 generic data (`\# <length> <hex>`) and reads the octets for the type. */
|
|
139
|
-
function parseGeneric(type, value) {
|
|
140
|
-
let match = /^\\#\s+(\d+)((?:\s*[0-9a-fA-F]+)*)\s*$/.exec(value);
|
|
141
|
-
if (!match)
|
|
142
|
-
return invalid(type, value);
|
|
143
|
-
let hex = (match[2] ?? "").replace(/\s+/g, "");
|
|
144
|
-
let length = Number(match[1]);
|
|
145
|
-
if (hex.length !== length * 2)
|
|
146
|
-
return invalid(type, value);
|
|
147
|
-
let octets = new Uint8Array(length);
|
|
148
|
-
for (let index = 0; index < length; index++)
|
|
149
|
-
octets[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
150
|
-
let fields = new WireReader(octets).record(type);
|
|
151
|
-
if (fields === "unknown")
|
|
152
|
-
return success({ type, data: value });
|
|
153
|
-
return fields ? success({ type, ...fields }) : invalid(type, value);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Reads the wire form of RDATA front to back. Every read returns `null` past the end, and a
|
|
157
|
-
* record parses only when it consumes every octet, since trailing bytes mean a wrong type.
|
|
158
|
-
*/
|
|
159
|
-
class WireReader {
|
|
160
|
-
#octets;
|
|
161
|
-
#offset = 0;
|
|
162
|
-
/** @param octets - The RDATA octets. */
|
|
163
|
-
constructor(octets) {
|
|
164
|
-
this.#octets = octets;
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* The typed fields for `type`, `null` when the octets do not fit it, or `"unknown"` for a
|
|
168
|
-
* type without a typed reading.
|
|
169
|
-
*/
|
|
170
|
-
record(type) {
|
|
171
|
-
let fields = this.#fields(type);
|
|
172
|
-
if (fields === null || fields === "unknown")
|
|
173
|
-
return fields;
|
|
174
|
-
return this.#offset === this.#octets.length ? fields : null;
|
|
175
|
-
}
|
|
176
|
-
/** Reads the type's fields, leaving the offset after the last octet read. */
|
|
177
|
-
#fields(type) {
|
|
178
|
-
switch (type) {
|
|
179
|
-
case "A": {
|
|
180
|
-
let bytes = this.#bytes(4);
|
|
181
|
-
return bytes ? { address: Array.from(bytes).join(".") } : null;
|
|
182
|
-
}
|
|
183
|
-
case "AAAA": {
|
|
184
|
-
let bytes = this.#bytes(16);
|
|
185
|
-
if (!bytes)
|
|
186
|
-
return null;
|
|
187
|
-
let groups = Array.from({ length: 8 }, (_, index) => (((bytes[index * 2] ?? 0) << 8) | (bytes[index * 2 + 1] ?? 0)).toString(16));
|
|
188
|
-
return { address: canonicalIpv6(groups.join(":")) ?? groups.join(":") };
|
|
189
|
-
}
|
|
190
|
-
case "CNAME": {
|
|
191
|
-
let target = this.#name();
|
|
192
|
-
return target === null ? null : { target };
|
|
193
|
-
}
|
|
194
|
-
case "NS": {
|
|
195
|
-
let host = this.#name();
|
|
196
|
-
return host === null ? null : { host };
|
|
197
|
-
}
|
|
198
|
-
case "MX": {
|
|
199
|
-
let preference = this.#uint(2);
|
|
200
|
-
let exchange = this.#name();
|
|
201
|
-
return preference === null || exchange === null ? null : { preference, exchange };
|
|
202
|
-
}
|
|
203
|
-
case "TXT": {
|
|
204
|
-
let strings = [];
|
|
205
|
-
while (this.#offset < this.#octets.length) {
|
|
206
|
-
let length = this.#uint(1);
|
|
207
|
-
let bytes = length === null ? null : this.#bytes(length);
|
|
208
|
-
if (!bytes)
|
|
209
|
-
return null;
|
|
210
|
-
strings.push(decodeOctets(bytes));
|
|
211
|
-
}
|
|
212
|
-
return { text: strings.join(""), strings };
|
|
213
|
-
}
|
|
214
|
-
case "CAA": {
|
|
215
|
-
let flags = this.#uint(1);
|
|
216
|
-
let tagLength = this.#uint(1);
|
|
217
|
-
let tag = tagLength === null ? null : this.#bytes(tagLength);
|
|
218
|
-
if (flags === null || !tag)
|
|
219
|
-
return null;
|
|
220
|
-
let rest = this.#bytes(this.#octets.length - this.#offset) ?? new Uint8Array();
|
|
221
|
-
return {
|
|
222
|
-
critical: (flags & 0x80) !== 0,
|
|
223
|
-
tag: decodeOctets(tag).toLowerCase(),
|
|
224
|
-
value: decodeOctets(rest),
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
case "SRV": {
|
|
228
|
-
let priority = this.#uint(2);
|
|
229
|
-
let weight = this.#uint(2);
|
|
230
|
-
let port = this.#uint(2);
|
|
231
|
-
let target = this.#name();
|
|
232
|
-
if (priority === null || weight === null || port === null || target === null)
|
|
233
|
-
return null;
|
|
234
|
-
return { priority, weight, port, target };
|
|
235
|
-
}
|
|
236
|
-
case "SOA": {
|
|
237
|
-
let primary = this.#name();
|
|
238
|
-
let mailbox = this.#name();
|
|
239
|
-
let numbers = Array.from({ length: 5 }, () => this.#uint(4));
|
|
240
|
-
if (primary === null || mailbox === null || numbers.some((n) => n === null))
|
|
241
|
-
return null;
|
|
242
|
-
let [serial, refresh, retry, expire, minimum] = numbers;
|
|
243
|
-
return { primary, mailbox, serial, refresh, retry, expire, minimum };
|
|
244
|
-
}
|
|
245
|
-
default:
|
|
246
|
-
return "unknown";
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
/** The next `length` octets, or `null` past the end. */
|
|
250
|
-
#bytes(length) {
|
|
251
|
-
if (this.#offset + length > this.#octets.length)
|
|
252
|
-
return null;
|
|
253
|
-
let bytes = this.#octets.subarray(this.#offset, this.#offset + length);
|
|
254
|
-
this.#offset += length;
|
|
255
|
-
return bytes;
|
|
256
|
-
}
|
|
257
|
-
/** A big-endian unsigned integer of `size` octets. */
|
|
258
|
-
#uint(size) {
|
|
259
|
-
let bytes = this.#bytes(size);
|
|
260
|
-
if (!bytes)
|
|
261
|
-
return null;
|
|
262
|
-
return bytes.reduce((total, byte) => total * 256 + byte, 0);
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* An uncompressed domain name, folded like presentation names. RFC 3597 forbids
|
|
266
|
-
* compression in generic data, so a compression pointer fails the read.
|
|
267
|
-
*/
|
|
268
|
-
#name() {
|
|
269
|
-
let labels = [];
|
|
270
|
-
while (true) {
|
|
271
|
-
let length = this.#uint(1);
|
|
272
|
-
if (length === null || length > 63)
|
|
273
|
-
return null;
|
|
274
|
-
if (length === 0)
|
|
275
|
-
break;
|
|
276
|
-
let label = this.#bytes(length);
|
|
277
|
-
if (!label)
|
|
278
|
-
return null;
|
|
279
|
-
labels.push(String.fromCharCode(...label));
|
|
280
|
-
}
|
|
281
|
-
return labels.length === 0 ? "." : normalizeName(labels.join("."));
|
|
282
|
-
}
|
|
283
|
-
}
|
package/dist/record-types.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Maps record type mnemonics to the IANA codes the JSON API writes in `type`, and back,
|
|
3
|
-
* with RFC 3597's `TYPEnnn` spelling for any code without a mnemonic here.
|
|
4
|
-
*
|
|
5
|
-
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
-
* @copyright Sergio Xalambrí 2026
|
|
7
|
-
*/
|
|
8
|
-
/** The mnemonic for a type code, or `TYPEnnn` when it has none here. */
|
|
9
|
-
export declare function typeName(code: number): string;
|
|
10
|
-
/**
|
|
11
|
-
* The spelling of a type as the query's `type` parameter and as a record's `type`: the
|
|
12
|
-
* uppercase mnemonic, and a known mnemonic for `TYPEnnn` when one exists.
|
|
13
|
-
*/
|
|
14
|
-
export declare function canonicalType(type: string): string;
|
package/dist/record-types.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Maps record type mnemonics to the IANA codes the JSON API writes in `type`, and back,
|
|
3
|
-
* with RFC 3597's `TYPEnnn` spelling for any code without a mnemonic here.
|
|
4
|
-
*
|
|
5
|
-
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
6
|
-
* @copyright Sergio Xalambrí 2026
|
|
7
|
-
*/
|
|
8
|
-
/** The IANA codes of the record types a caller is likely to ask for or see in an answer. */
|
|
9
|
-
const TYPE_CODES = {
|
|
10
|
-
A: 1,
|
|
11
|
-
NS: 2,
|
|
12
|
-
CNAME: 5,
|
|
13
|
-
SOA: 6,
|
|
14
|
-
PTR: 12,
|
|
15
|
-
HINFO: 13,
|
|
16
|
-
MX: 15,
|
|
17
|
-
TXT: 16,
|
|
18
|
-
AAAA: 28,
|
|
19
|
-
LOC: 29,
|
|
20
|
-
SRV: 33,
|
|
21
|
-
NAPTR: 35,
|
|
22
|
-
DNAME: 39,
|
|
23
|
-
DS: 43,
|
|
24
|
-
SSHFP: 44,
|
|
25
|
-
RRSIG: 46,
|
|
26
|
-
NSEC: 47,
|
|
27
|
-
DNSKEY: 48,
|
|
28
|
-
NSEC3: 50,
|
|
29
|
-
NSEC3PARAM: 51,
|
|
30
|
-
TLSA: 52,
|
|
31
|
-
SMIMEA: 53,
|
|
32
|
-
CDS: 59,
|
|
33
|
-
CDNSKEY: 60,
|
|
34
|
-
OPENPGPKEY: 61,
|
|
35
|
-
SVCB: 64,
|
|
36
|
-
HTTPS: 65,
|
|
37
|
-
URI: 256,
|
|
38
|
-
CAA: 257,
|
|
39
|
-
};
|
|
40
|
-
/** The mnemonic for each code in {@link TYPE_CODES}. */
|
|
41
|
-
const TYPE_NAMES = new Map(Object.entries(TYPE_CODES).map(([name, code]) => [code, name]));
|
|
42
|
-
/** The mnemonic for a type code, or `TYPEnnn` when it has none here. */
|
|
43
|
-
export function typeName(code) {
|
|
44
|
-
return TYPE_NAMES.get(code) ?? `TYPE${code}`;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* The spelling of a type as the query's `type` parameter and as a record's `type`: the
|
|
48
|
-
* uppercase mnemonic, and a known mnemonic for `TYPEnnn` when one exists.
|
|
49
|
-
*/
|
|
50
|
-
export function canonicalType(type) {
|
|
51
|
-
let upper = type.trim().toUpperCase();
|
|
52
|
-
let generic = /^TYPE(\d+)$/.exec(upper);
|
|
53
|
-
return generic ? typeName(Number(generic[1])) : upper;
|
|
54
|
-
}
|