@sdxc/doh 2026.10.6 → 2026.10.9

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.
@@ -0,0 +1,137 @@
1
+ import type { ZoneFile } from "@sdxc/zone-file";
2
+ /**
3
+ * The types of the CAA policy reading: what one record asks of a CA, the RRset RFC 8659
4
+ * applies to a name, the certificate request to decide, and the verdict, every one plain
5
+ * data but the lookup failure, so a verdict can be logged or stored as is.
6
+ *
7
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
8
+ * @copyright Sergio Xalambrí 2026
9
+ */
10
+ import type { DoHError } from "../errors.js";
11
+ import type { DoH } from "../types.js";
12
+ /**
13
+ * Groups the CAA policy types under a single import surface.
14
+ */
15
+ export declare namespace CAA {
16
+ /** One CAA record's data, from an answer or from `parseRecordData("CAA", …)` on a zone-file line. */
17
+ type Record = ZoneFile.CAAData;
18
+ /** One `key=value` from an `issue` or `issuewild` value, in published order. */
19
+ interface Parameter {
20
+ key: string;
21
+ value: string;
22
+ }
23
+ /** An `issue` or `issuewild` property. */
24
+ interface IssueProperty {
25
+ kind: "issue" | "issuewild";
26
+ critical: boolean;
27
+ /** The issuer domain, lowercased; `null` forbids issuance, as an empty or malformed value does. */
28
+ issuer: string | null;
29
+ /** The value failed RFC 8659's grammar, which forbids issuance the way an empty issuer does. */
30
+ malformed: boolean;
31
+ parameters: Parameter[];
32
+ }
33
+ /** Where a CA reports a refused or violating request. */
34
+ interface IodefProperty {
35
+ kind: "iodef";
36
+ critical: boolean;
37
+ /** The value when it parses as a `mailto:`, `http:` or `https:` URL, `null` otherwise. */
38
+ url: string | null;
39
+ }
40
+ /** A property this package does not interpret; with `critical` set it forbids issuance. */
41
+ interface UnknownProperty {
42
+ kind: "unknown";
43
+ critical: boolean;
44
+ tag: string;
45
+ value: string;
46
+ }
47
+ /** What one CAA record asks of a CA. */
48
+ type Property = IssueProperty | IodefProperty | UnknownProperty;
49
+ /** The RRset RFC 8659 §3 applies to a domain, and how it was found. */
50
+ interface RelevantSet {
51
+ /** The name whose query returned the records, the domain or an ancestor; `null` when none did. */
52
+ name: string | null;
53
+ records: DoH.CAARecord[];
54
+ /** Records of the set whose data did not parse; a CA cannot satisfy a set it cannot read. */
55
+ unparsed: DoH.UnknownRecord[];
56
+ /** Aliases the resolver followed for `name`; the records are owned by the chain's end. */
57
+ chain: DoH.CNAMERecord[];
58
+ /** Every name queried, nearest first. */
59
+ queried: string[];
60
+ /** Every answer in the climb carried the AD flag, denials of existence included. */
61
+ authenticated: boolean;
62
+ }
63
+ /** A certificate request to decide. */
64
+ interface Request {
65
+ /** The name on the certificate; `*.example.com` asks about a wildcard certificate. */
66
+ domain: string;
67
+ /** The CA's CAA identifiers, such as `letsencrypt.org`; any one matching authorizes. */
68
+ issuer: string | readonly string[];
69
+ /** RFC 8657 `accounturi` of the requesting account; omitted, the check is skipped. */
70
+ accountUri?: string;
71
+ /** RFC 8657 method label, such as `dns-01`; omitted, the check is skipped. */
72
+ validationMethod?: string;
73
+ }
74
+ /** The domain and its ancestors publish no CAA record, so any CA may issue. */
75
+ interface NoPolicy {
76
+ allowed: true;
77
+ reason: "no-policy";
78
+ }
79
+ /** The RRset holds no property that applies to the request, such as only `iodef`. */
80
+ interface Unrestricted {
81
+ allowed: true;
82
+ reason: "unrestricted";
83
+ }
84
+ /** `property` names one of the request's issuers and its parameters are satisfied. */
85
+ interface Authorized {
86
+ allowed: true;
87
+ reason: "authorized";
88
+ property: IssueProperty;
89
+ }
90
+ /** Every applicable property names no issuer: the policy forbids issuance outright. */
91
+ interface Forbidden {
92
+ allowed: false;
93
+ reason: "forbidden";
94
+ }
95
+ /** The applicable properties name issuers, none of them the request's. */
96
+ interface NotAuthorized {
97
+ allowed: false;
98
+ reason: "not-authorized";
99
+ /** The issuers the applicable properties do name, for a message the owner can act on. */
100
+ issuers: string[];
101
+ }
102
+ /** `property` names the request's issuer, and its RFC 8657 parameters exclude the request. */
103
+ interface ParameterMismatch {
104
+ allowed: false;
105
+ reason: "account-mismatch" | "validation-method-mismatch";
106
+ property: IssueProperty;
107
+ }
108
+ /** `property` is critical and of a kind no CA here is known to understand. */
109
+ interface CriticalTag {
110
+ allowed: false;
111
+ reason: "critical-tag";
112
+ property: UnknownProperty;
113
+ }
114
+ /** The relevant RRset holds a record whose data does not parse. */
115
+ interface Unreadable {
116
+ allowed: false;
117
+ reason: "unreadable";
118
+ record: DoH.UnknownRecord;
119
+ }
120
+ /** A query in the climb failed, which a CA reads as a refusal (RFC 8659 §6). */
121
+ interface LookupFailed {
122
+ allowed: false;
123
+ reason: "lookup-failed";
124
+ /** The name whose query failed. */
125
+ name: string;
126
+ queried: string[];
127
+ error: DoHError;
128
+ }
129
+ /** Where a verdict's policy came from. */
130
+ interface Located {
131
+ relevant: RelevantSet;
132
+ }
133
+ /** What one RRset says about one request. */
134
+ type Decision = NoPolicy | Unrestricted | Authorized | Forbidden | NotAuthorized | ParameterMismatch | CriticalTag;
135
+ /** What DNS says about one request; every failure reads `allowed: false`. */
136
+ type Verdict = ((Decision | Unreadable) & Located) | LookupFailed;
137
+ }
File without changes
package/dist/caa.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The WebPKI reading of CAA records (RFC 8659, RFC 8657): what a record asks of a CA, the
3
+ * RRset that applies to a name, and whether a CA may issue for it, kept apart from the DNS
4
+ * reading at the package root so every CAA answer stays as small as the record itself.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ export type { CAA } from "./caa/types.js";
10
+ export { evaluateCaa } from "./caa/evaluate.js";
11
+ export { mayIssue } from "./caa/may-issue.js";
12
+ export { parseCaaProperty } from "./caa/property.js";
13
+ export { findRelevantCaa } from "./caa/relevant-set.js";
package/dist/caa.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The WebPKI reading of CAA records (RFC 8659, RFC 8657): what a record asks of a CA, the
3
+ * RRset that applies to a name, and whether a CA may issue for it, kept apart from the DNS
4
+ * reading at the package root so every CAA answer stays as small as the record itself.
5
+ *
6
+ * @author [Sergio Xalambrí](https://sergiodxa.com)
7
+ * @copyright Sergio Xalambrí 2026
8
+ */
9
+ export { evaluateCaa } from "./caa/evaluate.js";
10
+ export { mayIssue } from "./caa/may-issue.js";
11
+ export { parseCaaProperty } from "./caa/property.js";
12
+ export { findRelevantCaa } from "./caa/relevant-set.js";
package/dist/errors.d.ts CHANGED
@@ -17,11 +17,14 @@ export declare class NameNotFoundError extends DoHError {
17
17
  * (RFC 2308), or `null` when the answer carried no SOA.
18
18
  */
19
19
  readonly ttl: number | null;
20
+ /** The AD flag: the resolver validated the denial of existence with DNSSEC. */
21
+ readonly authenticated: boolean;
20
22
  /**
21
23
  * @param queried - The name that was looked up.
22
24
  * @param ttl - The negative-caching TTL, when known.
25
+ * @param authenticated - Whether the answer carried the AD flag.
23
26
  */
24
- constructor(queried: string, ttl: number | null);
27
+ constructor(queried: string, ttl: number | null, authenticated?: boolean);
25
28
  }
26
29
  /** RCODE 2 (SERVFAIL): the resolver could not answer, a DNSSEC validation failure included. */
27
30
  export declare class ServerFailureError extends DoHError {
@@ -51,7 +54,3 @@ export declare class TransportError extends DoHError {
51
54
  */
52
55
  constructor(message: string, status: number | null, options?: ErrorOptions);
53
56
  }
54
- /** RDATA that does not parse for its type; `resolve` keeps such a record in `unparsed`. */
55
- export declare class RecordDataError extends Error {
56
- name: string;
57
- }
package/dist/errors.js CHANGED
@@ -17,13 +17,17 @@ export class NameNotFoundError extends DoHError {
17
17
  * (RFC 2308), or `null` when the answer carried no SOA.
18
18
  */
19
19
  ttl;
20
+ /** The AD flag: the resolver validated the denial of existence with DNSSEC. */
21
+ authenticated;
20
22
  /**
21
23
  * @param queried - The name that was looked up.
22
24
  * @param ttl - The negative-caching TTL, when known.
25
+ * @param authenticated - Whether the answer carried the AD flag.
23
26
  */
24
- constructor(queried, ttl) {
27
+ constructor(queried, ttl, authenticated = false) {
25
28
  super(`${queried} does not exist (NXDOMAIN)`);
26
29
  this.ttl = ttl;
30
+ this.authenticated = authenticated;
27
31
  }
28
32
  }
29
33
  /** RCODE 2 (SERVFAIL): the resolver could not answer, a DNSSEC validation failure included. */
@@ -62,7 +66,3 @@ export class TransportError extends DoHError {
62
66
  this.status = status;
63
67
  }
64
68
  }
65
- /** RDATA that does not parse for its type; `resolve` keeps such a record in `unparsed`. */
66
- export class RecordDataError extends Error {
67
- name = "RecordDataError";
68
- }
package/dist/index.d.ts CHANGED
@@ -7,8 +7,7 @@
7
7
  * @copyright Sergio Xalambrí 2026
8
8
  */
9
9
  export type { DoH } from "./types.js";
10
- export { DoHError, NameNotFoundError, RecordDataError, ResponseCodeError, ServerFailureError, TransportError, } from "./errors.js";
11
- export { parseRecordData } from "./parse-record-data.js";
10
+ export { DoHError, NameNotFoundError, ResponseCodeError, ServerFailureError, TransportError, } from "./errors.js";
12
11
  export { resolve } from "./resolve.js";
13
12
  export { CLOUDFLARE, GOOGLE } from "./resolvers.js";
14
13
  export { checkCname, verifyTxtRecord } from "./verify.js";
package/dist/index.js CHANGED
@@ -6,8 +6,7 @@
6
6
  * @author [Sergio Xalambrí](https://sergiodxa.com)
7
7
  * @copyright Sergio Xalambrí 2026
8
8
  */
9
- export { DoHError, NameNotFoundError, RecordDataError, ResponseCodeError, ServerFailureError, TransportError, } from "./errors.js";
10
- export { parseRecordData } from "./parse-record-data.js";
9
+ export { DoHError, NameNotFoundError, ResponseCodeError, ServerFailureError, TransportError, } from "./errors.js";
11
10
  export { resolve } from "./resolve.js";
12
11
  export { CLOUDFLARE, GOOGLE } from "./resolvers.js";
13
12
  export { checkCname, verifyTxtRecord } from "./verify.js";
package/dist/resolve.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import { failure, isSuccess, success } from "@sdxc/result";
2
+ import { canonicalType, parseRecordData, typeName } from "@sdxc/zone-file";
2
3
  import * as s from "remix/data-schema";
3
4
  import { NameNotFoundError, ResponseCodeError, ServerFailureError, TransportError, } from "./errors.js";
4
- import { parseRecordData } from "./parse-record-data.js";
5
- import { canonicalType, typeName } from "./record-types.js";
6
5
  import { CLOUDFLARE } from "./resolvers.js";
7
6
  /** One resource record as the JSON API writes it. */
8
7
  const WIRE_RECORD_SCHEMA = s.object({
@@ -56,8 +55,10 @@ export async function resolve(name, type, options = {}) {
56
55
  return envelope;
57
56
  let durationMs = Math.round(performance.now() - started);
58
57
  let { Status: rcode } = envelope.data;
59
- if (rcode === 3)
60
- return failure(new NameNotFoundError(name, negativeTtl(envelope.data)));
58
+ if (rcode === 3) {
59
+ let authenticated = envelope.data.AD ?? false;
60
+ return failure(new NameNotFoundError(name, negativeTtl(envelope.data), authenticated));
61
+ }
61
62
  if (rcode === 2)
62
63
  return failure(new ServerFailureError(name));
63
64
  if (rcode !== 0)
@@ -70,15 +71,17 @@ export async function resolve(name, type, options = {}) {
70
71
  if (recordType !== asked && recordType !== "CNAME")
71
72
  continue;
72
73
  let base = { name: normalizeName(record.name), ttl: record.TTL };
73
- let data = parseRecordData(recordType, record.data);
74
74
  if (recordType === asked) {
75
+ let data = parseRecordData(recordType, record.data);
75
76
  if (isSuccess(data))
76
77
  records.push({ ...base, ...data.data });
77
78
  else
78
79
  unparsed.push({ ...base, type: recordType, data: record.data });
79
80
  }
80
- else if (isSuccess(data)) {
81
- chain.push({ ...base, ...data.data });
81
+ else {
82
+ let data = parseRecordData("CNAME", record.data);
83
+ if (isSuccess(data))
84
+ chain.push({ ...base, ...data.data });
82
85
  }
83
86
  }
84
87
  let ttls = [...records, ...unparsed].map((record) => record.ttl);
package/dist/types.d.ts CHANGED
@@ -6,12 +6,13 @@
6
6
  * @author [Sergio Xalambrí](https://sergiodxa.com)
7
7
  * @copyright Sergio Xalambrí 2026
8
8
  */
9
+ import type { ZoneFile } from "@sdxc/zone-file";
9
10
  /**
10
11
  * Groups the DNS-over-HTTPS types under a single import surface.
11
12
  */
12
13
  export declare namespace DoH {
13
- /** A record type by mnemonic; the nine typed ones autocomplete, any other name is accepted. */
14
- type RecordType = "A" | "AAAA" | "CNAME" | "TXT" | "MX" | "NS" | "CAA" | "SOA" | "SRV" | (string & {});
14
+ /** A record type by mnemonic; the typed ones autocomplete, any other name is accepted. */
15
+ type RecordType = ZoneFile.RecordType;
15
16
  /**
16
17
  * A DoH endpoint and the encoding it answers in. `format` leaves room for an RFC 8484
17
18
  * `application/dns-message` resolver as a new value with no change to `resolve`.
@@ -27,80 +28,32 @@ export declare namespace DoH {
27
28
  /** Seconds the resolver allows the record to be cached. */
28
29
  ttl: number;
29
30
  }
30
- /** An IPv4 address, in dotted-quad form. */
31
- interface ARecord extends RecordBase {
32
- type: "A";
33
- address: string;
34
- }
35
- /** An IPv6 address. */
36
- interface AAAARecord extends RecordBase {
37
- type: "AAAA";
38
- /** In RFC 5952 canonical form: lowercase, zero runs compressed. */
39
- address: string;
40
- }
31
+ /** The record a query for `Type` returns: its typed data, owner and TTL. */
32
+ type RecordFor<Type extends RecordType> = ZoneFile.RecordData<Type> & RecordBase;
33
+ /** An IPv4 address. */
34
+ type ARecord = RecordFor<"A">;
35
+ /** An IPv6 address, in RFC 5952 canonical form. */
36
+ type AAAARecord = RecordFor<"AAAA">;
41
37
  /** An alias: `name` resolves as `target` does. */
42
- interface CNAMERecord extends RecordBase {
43
- type: "CNAME";
44
- target: string;
45
- }
38
+ type CNAMERecord = RecordFor<"CNAME">;
46
39
  /** A name server authoritative for the zone at `name`. */
47
- interface NSRecord extends RecordBase {
48
- type: "NS";
49
- host: string;
50
- }
40
+ type NSRecord = RecordFor<"NS">;
41
+ /** A pointer to another name, the answer to a reverse lookup. */
42
+ type PTRRecord = RecordFor<"PTR">;
43
+ /** An alias for every name below `name`. */
44
+ type DNAMERecord = RecordFor<"DNAME">;
51
45
  /** A mail exchanger; lower `preference` values are tried first. */
52
- interface MXRecord extends RecordBase {
53
- type: "MX";
54
- preference: number;
55
- /** The mail host; `"."` is an RFC 7505 null MX, a domain that accepts no mail. */
56
- exchange: string;
57
- }
46
+ type MXRecord = RecordFor<"MX">;
58
47
  /** Free text, such as SPF policies, DKIM keys and verification tokens. */
59
- interface TXTRecord extends RecordBase {
60
- type: "TXT";
61
- /** The character-strings concatenated with nothing between them, as SPF and DKIM read it. */
62
- text: string;
63
- /** The character-strings as published, for the rare record whose boundaries matter. */
64
- strings: string[];
65
- }
48
+ type TXTRecord = RecordFor<"TXT">;
66
49
  /** Which certificate authorities may issue for the name (RFC 8659). */
67
- interface CAARecord extends RecordBase {
68
- type: "CAA";
69
- /** The issuer-critical flag: a CA that does not understand `tag` must refuse to issue. */
70
- critical: boolean;
71
- tag: string;
72
- value: string;
73
- }
74
- /** Where a service runs; lower `priority` first, `weight` spreads load within one priority. */
75
- interface SRVRecord extends RecordBase {
76
- type: "SRV";
77
- priority: number;
78
- weight: number;
79
- port: number;
80
- target: string;
81
- }
50
+ type CAARecord = RecordFor<"CAA">;
51
+ /** Where a service runs. */
52
+ type SRVRecord = RecordFor<"SRV">;
82
53
  /** The zone's start of authority, found at its apex and in negative answers. */
83
- interface SOARecord extends RecordBase {
84
- type: "SOA";
85
- primary: string;
86
- /** The responsible mailbox in DNS form: its first label is the local part. */
87
- mailbox: string;
88
- serial: number;
89
- refresh: number;
90
- retry: number;
91
- expire: number;
92
- /** Bounds how long a negative answer may be cached (RFC 2308). */
93
- minimum: number;
94
- }
54
+ type SOARecord = RecordFor<"SOA">;
95
55
  /** A record of a type without a typed reading, or one whose data failed to parse, with its raw RDATA. */
96
- interface UnknownRecord extends RecordBase {
97
- type: string;
98
- data: string;
99
- }
100
- /** The record a query for `Type` returns. */
101
- type RecordFor<Type extends RecordType> = Type extends "A" ? ARecord : Type extends "AAAA" ? AAAARecord : Type extends "CNAME" ? CNAMERecord : Type extends "TXT" ? TXTRecord : Type extends "MX" ? MXRecord : Type extends "NS" ? NSRecord : Type extends "CAA" ? CAARecord : Type extends "SOA" ? SOARecord : Type extends "SRV" ? SRVRecord : UnknownRecord;
102
- /** A record's type-specific fields, what `parseRecordData` reads out of RDATA. */
103
- type RecordData<Type extends RecordType> = Omit<RecordFor<Type>, "name" | "ttl">;
56
+ type UnknownRecord = ZoneFile.UnknownData & RecordBase;
104
57
  /** A successful (NOERROR) answer to one query. */
105
58
  interface Answer<Type extends RecordType> {
106
59
  /** The queried name, lowercased and without the trailing dot. */
package/dist/types.js CHANGED
@@ -1,8 +0,0 @@
1
- /**
2
- * The types a DNS-over-HTTPS lookup reads and returns: the resolver it talks to, one
3
- * interface per typed record, and the answer that splits records of the asked type from
4
- * the CNAME chain that led to them.
5
- *
6
- * @author [Sergio Xalambrí](https://sergiodxa.com)
7
- * @copyright Sergio Xalambrí 2026
8
- */
package/package.json CHANGED
@@ -1,17 +1,19 @@
1
1
  {
2
2
  "name": "@sdxc/doh",
3
- "version": "2026.10.6",
3
+ "version": "2026.10.9",
4
4
  "description": "Typed DNS over HTTPS lookups",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "exports": {
8
- ".": "./dist/index.js"
8
+ ".": "./dist/index.js",
9
+ "./caa": "./dist/caa.js"
9
10
  },
10
11
  "dependencies": {
11
12
  "@sdxc/result": "2026.10.6",
13
+ "@sdxc/zone-file": "2026.10.9",
12
14
  "remix": "3.0.0"
13
15
  },
14
- "gitHead": "b1067eb20fc47ec51ff8c4de47d07dc3f9c00466",
16
+ "gitHead": "64fa732ed0a8ca8693a35aeef6345ae5be2534ef",
15
17
  "publishConfig": {
16
18
  "access": "public"
17
19
  },
@@ -1,22 +0,0 @@
1
- /**
2
- * Reads the character-strings of TXT RDATA in presentation format (RFC 1035 section 5.1):
3
- * quoted strings keep their spaces, bare words split on whitespace, and `\"`, `\\` and
4
- * `\DDD` escapes are decoded, so a resolver's quoting never leaks into the text.
5
- *
6
- * @author [Sergio Xalambrí](https://sergiodxa.com)
7
- * @copyright Sergio Xalambrí 2026
8
- */
9
- import type { Result } from "@sdxc/result";
10
- import { RecordDataError } from "./errors.js";
11
- /**
12
- * Splits TXT presentation data into its character-strings. Escapes are decoded to octets
13
- * and each string is read as UTF-8, so text a resolver writes as `\195\169` comes back as
14
- * `é`. Fails on an unterminated quote or a decimal escape above 255.
15
- *
16
- * @param data - One or more quoted or bare character-strings.
17
- * @returns The strings in order, empty strings included.
18
- * @example readCharacterStrings('"v=DKIM1; p=AAA" "BBB"') // success(["v=DKIM1; p=AAA", "BBB"])
19
- */
20
- export declare function readCharacterStrings(data: string): Result<string[], RecordDataError>;
21
- /** Decodes octets as UTF-8, the reading applied to every character-string. */
22
- export declare function decodeOctets(octets: Uint8Array): string;
@@ -1,72 +0,0 @@
1
- import { failure, success } from "@sdxc/result";
2
- import { RecordDataError } from "./errors.js";
3
- /** Decodes a character-string's octets as UTF-8, replacing invalid sequences with U+FFFD. */
4
- const DECODER = new TextDecoder();
5
- /** Encodes literal characters into the octets they stand for. */
6
- const ENCODER = new TextEncoder();
7
- /** Whether a character separates character-strings. */
8
- function isSpace(char) {
9
- return char === " " || char === "\t" || char === "\n" || char === "\r";
10
- }
11
- /**
12
- * Splits TXT presentation data into its character-strings. Escapes are decoded to octets
13
- * and each string is read as UTF-8, so text a resolver writes as `\195\169` comes back as
14
- * `é`. Fails on an unterminated quote or a decimal escape above 255.
15
- *
16
- * @param data - One or more quoted or bare character-strings.
17
- * @returns The strings in order, empty strings included.
18
- * @example readCharacterStrings('"v=DKIM1; p=AAA" "BBB"') // success(["v=DKIM1; p=AAA", "BBB"])
19
- */
20
- export function readCharacterStrings(data) {
21
- let chars = Array.from(data);
22
- let strings = [];
23
- let index = 0;
24
- while (index < chars.length) {
25
- if (isSpace(chars[index])) {
26
- index += 1;
27
- continue;
28
- }
29
- let quoted = chars[index] === '"';
30
- if (quoted)
31
- index += 1;
32
- let octets = [];
33
- let terminated = !quoted;
34
- while (index < chars.length) {
35
- let char = chars[index] ?? "";
36
- if (quoted && char === '"') {
37
- terminated = true;
38
- index += 1;
39
- break;
40
- }
41
- if (!quoted && isSpace(char))
42
- break;
43
- if (char === "\\") {
44
- let digits = chars.slice(index + 1, index + 4).join("");
45
- if (/^\d{3}$/.test(digits)) {
46
- let octet = Number(digits);
47
- if (octet > 255)
48
- return failure(new RecordDataError(`Invalid escape \\${digits} in TXT data`));
49
- octets.push(octet);
50
- index += 4;
51
- continue;
52
- }
53
- let next = chars[index + 1];
54
- if (next === undefined)
55
- return failure(new RecordDataError("TXT data ends in a lone backslash"));
56
- octets.push(...ENCODER.encode(next));
57
- index += 2;
58
- continue;
59
- }
60
- octets.push(...ENCODER.encode(char));
61
- index += 1;
62
- }
63
- if (!terminated)
64
- return failure(new RecordDataError("TXT data has an unterminated quoted string"));
65
- strings.push(DECODER.decode(new Uint8Array(octets)));
66
- }
67
- return success(strings);
68
- }
69
- /** Decodes octets as UTF-8, the reading applied to every character-string. */
70
- export function decodeOctets(octets) {
71
- return DECODER.decode(octets);
72
- }
@@ -1,23 +0,0 @@
1
- /**
2
- * Reads record data in presentation format, as a DoH JSON answer or a zone file writes it,
3
- * into typed fields per record type. RFC 3597 generic data (`\# len hex`) is decoded from
4
- * its wire form, which is how some resolvers answer types such as CAA.
5
- *
6
- * @author [Sergio Xalambrí](https://sergiodxa.com)
7
- * @copyright Sergio Xalambrí 2026
8
- */
9
- import type { Result } from "@sdxc/result";
10
- import type { DoH } from "./types.js";
11
- import { RecordDataError } from "./errors.js";
12
- /**
13
- * Reads a record's data into its typed fields. Types without a typed reading come back
14
- * as `{ type, data }` with the data unchanged. Names are lowercased without the trailing
15
- * dot, except the root, which stays `"."`.
16
- *
17
- * @param type - The record type, as a mnemonic or RFC 3597 `TYPEnnn`.
18
- * @param data - The RDATA in presentation format.
19
- * @returns The record's fields, or why the data does not fit the type.
20
- * @template Type - The record type, which selects the fields returned.
21
- * @example parseRecordData("MX", "10 mx.example.com.") // success({ type: "MX", preference: 10, exchange: "mx.example.com" })
22
- */
23
- export declare function parseRecordData<Type extends DoH.RecordType>(type: Type, data: string): Result<DoH.RecordData<Type>, RecordDataError>;