@dnsid-ai/transport 0.24.0 → 0.24.2

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 CHANGED
@@ -39,6 +39,6 @@ const safeFetch = createSsrfSafeFetch({ dnsServer: process.env.DNSID_DNS_SERVER
39
39
 
40
40
  Most Node applications should use this through `@dnsid-ai/sdk/node` or profile defaults.
41
41
 
42
- Node's TXT lookup API exposes neither remaining TTLs nor DNSSEC validation state. Both system and configured-server resolvers therefore return TTL `0` and `DNSSECState.UNKNOWN`: fresh verification works in `auto` mode, but SDK identity caching is disabled and `validated`/`required` modes reject the unknown DNSSEC state. To enable caching, inject a `DNSResolver` that supplies real remaining TTLs; the published zone TTL is not a safe substitute for a recursive resolver's remaining TTL.
42
+ The default resolver queries the DNS servers returned by Node's `dns.getServers()` in order, using UDP with TCP fallback to capture the recursive server's remaining TXT TTL (capped at one day). `dnsServer` can override this list. On failures or empty answers it falls back to Node's native TXT lookup with TTL `0`, disabling identity caching for that result. Node's server list may omit macOS scoped/split-DNS routing, so even positive wire answers are not guaranteed to match the platform resolver in those setups. Neither path validates DNSSEC (`UNKNOWN`); SERVFAIL is a resolution error. `validated`/`required` DNSSEC modes still need an injected validating resolver. Cached verification still re-fetches status unless `verification.statusCheckInterval` is set.
43
43
 
44
44
  `createSsrfSafeFetch()` enforces unsafe-address rejection in the lookup used by the outgoing socket and returns redirects without following them automatically. Trusted test/private deployments may pass `privateAddressHosts`: exact hostnames or leading-dot suffixes such as `.test` (label-bounded, case-insensitive); only RFC 1918/ULA private and loopback results are then accepted for matching hosts, while link-local, mixed public/private, and IP-literal URLs remain blocked. Nothing is allowed by default, not even `.test`; a local `dnsid` stack needs `privateAddressHosts: ['.test']` (or `DNSID_PRIVATE_HOSTS=.test` through `loadEnvironment`). Profile packages that accept a custom `fetch` cannot force equivalent behavior on arbitrary implementations, so custom fetch injection remains trusted infrastructure.
package/dist/index.cjs CHANGED
@@ -44,10 +44,136 @@ module.exports = __toCommonJS(index_exports);
44
44
  var dnsPromises = __toESM(require("dns/promises"), 1);
45
45
  var fs = __toESM(require("fs"), 1);
46
46
  var https = __toESM(require("https"), 1);
47
- var net = __toESM(require("net"), 1);
47
+ var net2 = __toESM(require("net"), 1);
48
48
  var tls = __toESM(require("tls"), 1);
49
49
  var import_undici = require("undici");
50
+ var import_protocol2 = require("@dnsid-ai/protocol");
51
+
52
+ // src/txt-wire.ts
53
+ var import_node_crypto = require("crypto");
54
+ var dgram = __toESM(require("dgram"), 1);
55
+ var net = __toESM(require("net"), 1);
56
+ var import_dns_packet = __toESM(require("dns-packet"), 1);
50
57
  var import_protocol = require("@dnsid-ai/protocol");
58
+ var normalize = (name) => name.replace(/\.$/, "").toLowerCase();
59
+ var badResponse = () => new Error("invalid DNS TXT response");
60
+ var remainingTtl = (ttl) => ttl !== void 0 && Number.isInteger(ttl) && ttl >= 0 && ttl <= 2147483647 ? ttl : 0;
61
+ async function resolveTxtWithTtl(name, host, port, signal) {
62
+ const deadline = AbortSignal.any([signal ?? new AbortController().signal, AbortSignal.timeout(2e3)]);
63
+ let owner = normalize(name);
64
+ const seen = /* @__PURE__ */ new Set([owner]);
65
+ let hops = 0;
66
+ let ttl = Infinity;
67
+ while (true) {
68
+ deadline.throwIfAborted();
69
+ const queried = owner;
70
+ const response = await query(owner, host, port, deadline);
71
+ if (response.rcode === "NXDOMAIN") return [[], import_protocol.DNSSECState.UNKNOWN];
72
+ if (response.rcode !== "NOERROR") throw new Error(`DNS query failed: ${response.rcode}`);
73
+ const answers = response.answers ?? [];
74
+ while (true) {
75
+ const cnames = answers.filter((a) => a.type === "CNAME" && normalize(a.name) === owner && a.class === "IN");
76
+ if (!cnames.length) break;
77
+ if (cnames.length !== 1 || ++hops > 8) throw badResponse();
78
+ const cname = cnames[0];
79
+ if (typeof cname.data !== "string") throw badResponse();
80
+ ttl = Math.min(ttl, remainingTtl(cname.ttl));
81
+ owner = normalize(cname.data);
82
+ if (seen.has(owner)) throw badResponse();
83
+ seen.add(owner);
84
+ }
85
+ const txt = answers.filter((a) => a.type === "TXT" && normalize(a.name) === owner && a.class === "IN");
86
+ if (txt.length) {
87
+ ttl = Math.min(ttl, ...txt.map((a) => remainingTtl(a.ttl)));
88
+ const decoder = new TextDecoder("utf-8", { fatal: true });
89
+ return [txt.map((a) => {
90
+ if (!Array.isArray(a.data) || !a.data.every(Buffer.isBuffer)) throw badResponse();
91
+ return { strings: a.data.map((part) => decoder.decode(part)), ttl: Math.min(ttl, 86400) };
92
+ }), import_protocol.DNSSECState.UNKNOWN];
93
+ }
94
+ if (owner === queried) return [[], import_protocol.DNSSECState.UNKNOWN];
95
+ }
96
+ }
97
+ async function query(name, host, port, signal) {
98
+ const id = (0, import_node_crypto.randomInt)(65536);
99
+ const request = {
100
+ type: "query",
101
+ id,
102
+ flags: import_dns_packet.default.RECURSION_DESIRED,
103
+ questions: [{ type: "TXT", class: "IN", name }],
104
+ additionals: [{ type: "OPT", name: ".", udpPayloadSize: 1232 }]
105
+ };
106
+ const valid = (data) => {
107
+ try {
108
+ const response2 = import_dns_packet.default.decode(data);
109
+ if (import_dns_packet.default.decode.bytes > data.length || response2.type !== "response" || response2.id !== id || response2.opcode !== "QUERY" || response2.questions?.length !== 1 || response2.questions[0].type !== "TXT" || response2.questions[0].class !== "IN" || normalize(response2.questions[0].name) !== name) return null;
110
+ return response2;
111
+ } catch {
112
+ return null;
113
+ }
114
+ };
115
+ const response = await udp(import_dns_packet.default.encode(request), host, port, signal, valid);
116
+ if (!response.flag_tc) return response;
117
+ const tcpResponse = valid(await tcp(import_dns_packet.default.streamEncode(request), host, port, signal));
118
+ if (!tcpResponse || tcpResponse.flag_tc) throw badResponse();
119
+ return tcpResponse;
120
+ }
121
+ function udp(buf, host, port, signal, valid) {
122
+ return new Promise((resolve, reject) => {
123
+ const sock = dgram.createSocket(net.isIP(host) === 6 ? "udp6" : "udp4");
124
+ let finished = false;
125
+ const done = (error, response) => {
126
+ if (finished) return;
127
+ finished = true;
128
+ signal.removeEventListener("abort", onAbort);
129
+ sock.close();
130
+ if (error) reject(error);
131
+ else resolve(response);
132
+ };
133
+ const onAbort = () => done(signal.reason ?? new Error("DNS query aborted"));
134
+ signal.addEventListener("abort", onAbort, { once: true });
135
+ sock.on("error", done);
136
+ sock.on("message", (data) => {
137
+ const response = valid(data);
138
+ if (response) done(void 0, response);
139
+ });
140
+ if (signal.aborted) return onAbort();
141
+ sock.connect(port, host, () => {
142
+ if (!finished) sock.send(buf);
143
+ });
144
+ });
145
+ }
146
+ function tcp(buf, host, port, signal) {
147
+ return new Promise((resolve, reject) => {
148
+ const sock = net.connect({ host, port });
149
+ let data = Buffer.alloc(0);
150
+ let finished = false;
151
+ const done = (error, response) => {
152
+ if (finished) return;
153
+ finished = true;
154
+ signal.removeEventListener("abort", onAbort);
155
+ sock.destroy();
156
+ if (error) reject(error);
157
+ else resolve(response);
158
+ };
159
+ const onAbort = () => done(signal.reason ?? new Error("DNS query aborted"));
160
+ signal.addEventListener("abort", onAbort, { once: true });
161
+ sock.on("error", done);
162
+ sock.on("end", () => done(badResponse()));
163
+ sock.on("connect", () => sock.write(buf));
164
+ sock.on("data", (chunk) => {
165
+ if (!Buffer.isBuffer(chunk) || data.length + chunk.length > 65537) return done(badResponse());
166
+ data = Buffer.concat([data, chunk]);
167
+ if (data.length >= 2 && data.length >= data.readUInt16BE(0) + 2) {
168
+ if (data.length !== data.readUInt16BE(0) + 2) return done(badResponse());
169
+ done(void 0, data.subarray(2));
170
+ }
171
+ });
172
+ if (signal.aborted) onAbort();
173
+ });
174
+ }
175
+
176
+ // src/index.ts
51
177
  var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
52
178
  var DEFAULT_MAX_RESPONSE_BYTES = 1 * 1024 * 1024;
53
179
  var DNSID_USER_AGENT = "dnsid-ts";
@@ -55,21 +181,21 @@ function createDnsidFetch(config) {
55
181
  if (!config.dnsServer && !config.caBundlePath) {
56
182
  return globalThis.fetch.bind(globalThis);
57
183
  }
58
- const connect = {};
59
- if (config.dnsServer) connect.lookup = createLookup(config.dnsServer);
60
- if (config.caBundlePath) connect.ca = [...tls.rootCertificates, fs.readFileSync(config.caBundlePath, "utf8")];
61
- return createFetchWithDispatcher(new import_undici.Agent({ connect }));
184
+ const connect2 = {};
185
+ if (config.dnsServer) connect2.lookup = createLookup(config.dnsServer);
186
+ if (config.caBundlePath) connect2.ca = [...tls.rootCertificates, fs.readFileSync(config.caBundlePath, "utf8")];
187
+ return createFetchWithDispatcher(new import_undici.Agent({ connect: connect2 }));
62
188
  }
63
189
  function createSsrfSafeFetch(config = {}, options = {}) {
64
- const privateAddressHosts = (options.privateAddressHosts ?? []).map(import_protocol.normalizePrivateAddressHost);
65
- const connect = { lookup: createSsrfSafeLookup(config.dnsServer, privateAddressHosts) };
66
- if (config.caBundlePath) connect.ca = [...tls.rootCertificates, fs.readFileSync(config.caBundlePath, "utf8")];
67
- const fetchWithSafeLookup = createFetchWithDispatcher(new import_undici.Agent({ connect }));
190
+ const privateAddressHosts = (options.privateAddressHosts ?? []).map(import_protocol2.normalizePrivateAddressHost);
191
+ const connect2 = { lookup: createSsrfSafeLookup(config.dnsServer, privateAddressHosts) };
192
+ if (config.caBundlePath) connect2.ca = [...tls.rootCertificates, fs.readFileSync(config.caBundlePath, "utf8")];
193
+ const fetchWithSafeLookup = createFetchWithDispatcher(new import_undici.Agent({ connect: connect2 }));
68
194
  return async (input, init) => {
69
195
  const url = new URL(input instanceof Request ? input.url : input);
70
196
  if (isUnsafeIp(url.hostname)) {
71
- throw new import_protocol.VerificationError(`unsafe target IP address: ${url.hostname}`, {
72
- code: import_protocol.VerificationCode.TLSError
197
+ throw new import_protocol2.VerificationError(`unsafe target IP address: ${url.hostname}`, {
198
+ code: import_protocol2.VerificationCode.TLSError
73
199
  });
74
200
  }
75
201
  return fetchWithSafeLookup(input, { ...init, redirect: "manual" });
@@ -84,25 +210,35 @@ function createFetchWithDispatcher(dispatcher) {
84
210
  };
85
211
  }
86
212
  function createDefaultDnsResolver(config) {
87
- return config.dnsServer ? createDnsResolverFromServer(config.dnsServer) : createSystemDnsResolver();
213
+ if (config.dnsServer) return createDnsResolverFromServer(config.dnsServer);
214
+ const system = createSystemDnsResolver();
215
+ return {
216
+ async fetchTXT(name, options) {
217
+ for (const server of dnsPromises.getServers()) {
218
+ options?.signal?.throwIfAborted();
219
+ try {
220
+ const result = await createDnsResolverFromServer(server).fetchTXT(name, options);
221
+ if (result[0].length) return result;
222
+ break;
223
+ } catch (error) {
224
+ if (options?.signal?.aborted) throw error;
225
+ }
226
+ }
227
+ options?.signal?.throwIfAborted();
228
+ return system.fetchTXT(name, options);
229
+ }
230
+ };
88
231
  }
89
232
  function createDnsResolverFromServer(server) {
90
233
  const parsed = parseDnsServer(server);
91
- if (!parsed || net.isIP(parsed.host)) {
92
- const resolver = new dnsPromises.Resolver();
93
- resolver.setServers([server]);
94
- return createSystemDnsResolver(resolver);
95
- }
96
- let resolverPromise = null;
234
+ if (!parsed) throw new import_protocol2.ArgumentError("invalid DNS server");
235
+ const port = Number(parsed.port ?? 53);
236
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new import_protocol2.ArgumentError(`invalid DNS server port: ${parsed.port}`);
237
+ let hostPromise;
97
238
  return {
98
- async fetchTXT(name) {
99
- resolverPromise ??= (async () => {
100
- const { address } = await dnsPromises.lookup(parsed.host);
101
- const resolver = new dnsPromises.Resolver();
102
- resolver.setServers([formatDnsServer(address, parsed.port)]);
103
- return resolver;
104
- })();
105
- return fetchTxtViaSystem(name, await resolverPromise);
239
+ async fetchTXT(name, options) {
240
+ hostPromise ??= net2.isIP(parsed.host) ? Promise.resolve(parsed.host) : dnsPromises.lookup(parsed.host).then(({ address }) => address);
241
+ return resolveTxtWithTtl(name, await hostPromise, port, options?.signal);
106
242
  }
107
243
  };
108
244
  }
@@ -119,7 +255,7 @@ function parseDnsServer(server) {
119
255
  return { host: server };
120
256
  }
121
257
  function formatDnsServer(address, port) {
122
- const host = net.isIP(address) === 6 ? `[${address}]` : address;
258
+ const host = net2.isIP(address) === 6 ? `[${address}]` : address;
123
259
  return port ? `${host}:${port}` : host;
124
260
  }
125
261
  function createLookup(dnsServer) {
@@ -127,7 +263,7 @@ function createLookup(dnsServer) {
127
263
  async function resolver() {
128
264
  resolverPromise ??= (async () => {
129
265
  const parsed = parseDnsServer(dnsServer);
130
- const server = parsed && !net.isIP(parsed.host) ? formatDnsServer((await dnsPromises.lookup(parsed.host)).address, parsed.port) : dnsServer;
266
+ const server = parsed && !net2.isIP(parsed.host) ? formatDnsServer((await dnsPromises.lookup(parsed.host)).address, parsed.port) : dnsServer;
131
267
  const r = new dnsPromises.Resolver();
132
268
  r.setServers([server]);
133
269
  return r;
@@ -158,7 +294,7 @@ function createSsrfSafeLookup(dnsServer, privateAddressHosts = []) {
158
294
  if (!dnsServer) throw new Error("no custom DNS server configured");
159
295
  resolverPromise ??= (async () => {
160
296
  const parsed = parseDnsServer(dnsServer);
161
- const server = parsed && !net.isIP(parsed.host) ? formatDnsServer((await dnsPromises.lookup(parsed.host)).address, parsed.port) : dnsServer;
297
+ const server = parsed && !net2.isIP(parsed.host) ? formatDnsServer((await dnsPromises.lookup(parsed.host)).address, parsed.port) : dnsServer;
162
298
  const r = new dnsPromises.Resolver();
163
299
  r.setServers([server]);
164
300
  return r;
@@ -173,8 +309,8 @@ function createSsrfSafeLookup(dnsServer, privateAddressHosts = []) {
173
309
  const addresses = await resolveAddresses(hostname, family, dnsServer ? await customResolver() : void 0);
174
310
  const unsafe = addresses.find(({ address }) => isUnsafeIp(address));
175
311
  if (unsafe && !(privateAddressHostMatches(hostname, privateAddressHosts) && addresses.every(({ address }) => isPrivateOrLoopbackIp(address)))) {
176
- throw new import_protocol.VerificationError(`unsafe resolved IP address for ${hostname}: ${unsafe.address}`, {
177
- code: import_protocol.VerificationCode.TLSError
312
+ throw new import_protocol2.VerificationError(`unsafe resolved IP address for ${hostname}: ${unsafe.address}`, {
313
+ code: import_protocol2.VerificationCode.TLSError
178
314
  });
179
315
  }
180
316
  if (all) cb(null, addresses);
@@ -190,7 +326,7 @@ function privateAddressHostMatches(hostname, privateAddressHosts) {
190
326
  return privateAddressHosts.some((entry) => entry.startsWith(".") ? host === entry.slice(1) || host.endsWith(entry) : host === entry);
191
327
  }
192
328
  async function resolveAddresses(hostname, family, resolver) {
193
- const ipFamily = net.isIP(hostname);
329
+ const ipFamily = net2.isIP(hostname);
194
330
  if (ipFamily === 4 || ipFamily === 6) return [{ address: hostname, family: ipFamily }];
195
331
  if (!resolver) {
196
332
  const records = await dnsPromises.lookup(hostname, { all: true, family: family === 4 || family === 6 ? family : 0 });
@@ -209,7 +345,7 @@ function isUnsafeIp(address) {
209
345
  if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
210
346
  const zoneIndex = host.indexOf("%");
211
347
  const normalized = zoneIndex === -1 ? host : host.slice(0, zoneIndex);
212
- const family = net.isIP(normalized);
348
+ const family = net2.isIP(normalized);
213
349
  if (family === 4) return isUnsafeIpv4(normalized);
214
350
  if (family === 6) return isUnsafeIpv6(normalized);
215
351
  return false;
@@ -219,7 +355,7 @@ function isPrivateOrLoopbackIp(address) {
219
355
  if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
220
356
  const zoneIndex = host.indexOf("%");
221
357
  const normalized = zoneIndex === -1 ? host : host.slice(0, zoneIndex);
222
- const family = net.isIP(normalized);
358
+ const family = net2.isIP(normalized);
223
359
  if (family === 4) {
224
360
  const [a, b] = normalized.split(".").map(Number);
225
361
  return a === 10 || a === 127 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
@@ -319,15 +455,13 @@ function createSystemDnsResolver(resolver = dnsPromises) {
319
455
  async function fetchTxtViaSystem(name, resolver) {
320
456
  try {
321
457
  const records = await resolver.resolveTxt(name);
322
- return [records.map((strings) => ({ strings, ttl: 0 })), import_protocol.DNSSECState.UNKNOWN];
458
+ return [records.map((strings) => ({ strings, ttl: 0 })), import_protocol2.DNSSECState.UNKNOWN];
323
459
  } catch (err) {
324
460
  const nodeErr = err;
325
461
  switch (nodeErr.code) {
326
462
  case "ENODATA":
327
463
  case "ENOTFOUND":
328
- return [[], import_protocol.DNSSECState.UNKNOWN];
329
- case "ESERVFAIL":
330
- return [[], import_protocol.DNSSECState.FAILED];
464
+ return [[], import_protocol2.DNSSECState.UNKNOWN];
331
465
  default:
332
466
  throw err;
333
467
  }
@@ -339,26 +473,26 @@ function hostAllowed(host, allowedHost, domainBoundary = false) {
339
473
  var protocolAgent = new https.Agent({ keepAlive: true, maxCachedSessions: 0 });
340
474
  function fetchWithRedirects(url, opts, depth) {
341
475
  if (depth > 5) {
342
- return Promise.reject(new import_protocol.VerificationError("too many redirects", { code: import_protocol.VerificationCode.TLSError }));
476
+ return Promise.reject(new import_protocol2.VerificationError("too many redirects", { code: import_protocol2.VerificationCode.TLSError }));
343
477
  }
344
478
  let urlObj;
345
479
  try {
346
480
  urlObj = new URL(url);
347
481
  } catch {
348
- return Promise.reject(new import_protocol.VerificationError(`invalid URL: ${url}`, { code: import_protocol.VerificationCode.TLSError }));
482
+ return Promise.reject(new import_protocol2.VerificationError(`invalid URL: ${url}`, { code: import_protocol2.VerificationCode.TLSError }));
349
483
  }
350
484
  if (urlObj.protocol !== "https:") {
351
- return Promise.reject(new import_protocol.VerificationError("only HTTPS URLs are permitted", { code: import_protocol.VerificationCode.TLSError }));
485
+ return Promise.reject(new import_protocol2.VerificationError("only HTTPS URLs are permitted", { code: import_protocol2.VerificationCode.TLSError }));
352
486
  }
353
487
  if (opts.allowedHost && !hostAllowed(urlObj.hostname, opts.allowedHost, opts.domainBoundary)) {
354
- return Promise.reject(new import_protocol.VerificationError(
488
+ return Promise.reject(new import_protocol2.VerificationError(
355
489
  `URL host not permitted: expected ${opts.allowedHost}, got ${urlObj.hostname}`,
356
- { code: import_protocol.VerificationCode.TLSError }
490
+ { code: import_protocol2.VerificationCode.TLSError }
357
491
  ));
358
492
  }
359
493
  if (isUnsafeIp(urlObj.hostname)) {
360
- return Promise.reject(new import_protocol.VerificationError(`unsafe HTTPS target IP address: ${urlObj.hostname}`, {
361
- code: import_protocol.VerificationCode.TLSError
494
+ return Promise.reject(new import_protocol2.VerificationError(`unsafe HTTPS target IP address: ${urlObj.hostname}`, {
495
+ code: import_protocol2.VerificationCode.TLSError
362
496
  }));
363
497
  }
364
498
  return new Promise((resolve, reject) => {
@@ -369,7 +503,7 @@ function fetchWithRedirects(url, opts, depth) {
369
503
  headers: {
370
504
  "User-Agent": DNSID_USER_AGENT
371
505
  },
372
- lookup: createSsrfSafeLookup(opts.dnsServer, (opts.privateAddressHosts ?? []).map(import_protocol.normalizePrivateAddressHost))
506
+ lookup: createSsrfSafeLookup(opts.dnsServer, (opts.privateAddressHosts ?? []).map(import_protocol2.normalizePrivateAddressHost))
373
507
  };
374
508
  if (opts.caBundlePath) requestOptions.ca = [...tls.rootCertificates, fs.readFileSync(opts.caBundlePath, "utf8")];
375
509
  const req = https.get(url, requestOptions, (res) => {
@@ -377,24 +511,24 @@ function fetchWithRedirects(url, opts, depth) {
377
511
  const location = res.headers.location;
378
512
  if (!location) {
379
513
  res.resume();
380
- return reject(new import_protocol.VerificationError("redirect with no Location header", { code: import_protocol.VerificationCode.TLSError }));
514
+ return reject(new import_protocol2.VerificationError("redirect with no Location header", { code: import_protocol2.VerificationCode.TLSError }));
381
515
  }
382
516
  let redirectUrl;
383
517
  try {
384
518
  redirectUrl = new URL(location, url);
385
519
  } catch {
386
520
  res.resume();
387
- return reject(new import_protocol.VerificationError(`invalid redirect URL: ${location}`, { code: import_protocol.VerificationCode.TLSError }));
521
+ return reject(new import_protocol2.VerificationError(`invalid redirect URL: ${location}`, { code: import_protocol2.VerificationCode.TLSError }));
388
522
  }
389
523
  if (redirectUrl.protocol !== "https:") {
390
524
  res.resume();
391
- return reject(new import_protocol.VerificationError("redirect to non-HTTPS URL", { code: import_protocol.VerificationCode.TLSError }));
525
+ return reject(new import_protocol2.VerificationError("redirect to non-HTTPS URL", { code: import_protocol2.VerificationCode.TLSError }));
392
526
  }
393
527
  if (opts.allowedHost && !hostAllowed(redirectUrl.hostname, opts.allowedHost, opts.domainBoundary)) {
394
528
  res.resume();
395
- return reject(new import_protocol.VerificationError(
529
+ return reject(new import_protocol2.VerificationError(
396
530
  `redirect changes host: expected ${opts.allowedHost}, got ${redirectUrl.hostname}`,
397
- { code: import_protocol.VerificationCode.TLSError }
531
+ { code: import_protocol2.VerificationCode.TLSError }
398
532
  ));
399
533
  }
400
534
  res.resume();
@@ -402,9 +536,9 @@ function fetchWithRedirects(url, opts, depth) {
402
536
  }
403
537
  if (res.statusCode !== 200) {
404
538
  res.resume();
405
- return reject(new import_protocol.VerificationError(
539
+ return reject(new import_protocol2.VerificationError(
406
540
  `unexpected HTTP status ${res.statusCode}`,
407
- { code: import_protocol.VerificationCode.TLSError, transient: (res.statusCode ?? 0) >= 500 }
541
+ { code: import_protocol2.VerificationCode.TLSError, transient: (res.statusCode ?? 0) >= 500 }
408
542
  ));
409
543
  }
410
544
  const socket = res.socket;
@@ -420,9 +554,9 @@ function fetchWithRedirects(url, opts, depth) {
420
554
  const contentLength = Number(res.headers["content-length"]);
421
555
  if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) {
422
556
  res.resume();
423
- return reject(new import_protocol.VerificationError(
557
+ return reject(new import_protocol2.VerificationError(
424
558
  `response body exceeds ${maxBodyBytes} byte limit`,
425
- { code: import_protocol.VerificationCode.TLSError }
559
+ { code: import_protocol2.VerificationCode.TLSError }
426
560
  ));
427
561
  }
428
562
  let body = "";
@@ -435,9 +569,9 @@ function fetchWithRedirects(url, opts, depth) {
435
569
  if (bodyBytes > maxBodyBytes) {
436
570
  tooLarge = true;
437
571
  req.destroy();
438
- return reject(new import_protocol.VerificationError(
572
+ return reject(new import_protocol2.VerificationError(
439
573
  `response body exceeds ${maxBodyBytes} byte limit`,
440
- { code: import_protocol.VerificationCode.TLSError }
574
+ { code: import_protocol2.VerificationCode.TLSError }
441
575
  ));
442
576
  }
443
577
  body += chunk;
@@ -446,12 +580,12 @@ function fetchWithRedirects(url, opts, depth) {
446
580
  try {
447
581
  resolve({ data: JSON.parse(body), tlsCert });
448
582
  } catch {
449
- reject(new import_protocol.VerificationError("response body is not valid JSON", { code: import_protocol.VerificationCode.RecordInvalid }));
583
+ reject(new import_protocol2.VerificationError("response body is not valid JSON", { code: import_protocol2.VerificationCode.RecordInvalid }));
450
584
  }
451
585
  });
452
586
  res.on("error", (err) => {
453
- reject(new import_protocol.VerificationError(`response stream error: ${err.message}`, {
454
- code: import_protocol.VerificationCode.TLSError,
587
+ reject(new import_protocol2.VerificationError(`response stream error: ${err.message}`, {
588
+ code: import_protocol2.VerificationCode.TLSError,
455
589
  transient: true
456
590
  }));
457
591
  });
@@ -459,16 +593,16 @@ function fetchWithRedirects(url, opts, depth) {
459
593
  const timeoutMs = opts.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
460
594
  req.setTimeout(timeoutMs, () => {
461
595
  req.destroy();
462
- reject(new import_protocol.VerificationError(`request timed out after ${timeoutMs}ms`, {
463
- code: import_protocol.VerificationCode.TLSError,
596
+ reject(new import_protocol2.VerificationError(`request timed out after ${timeoutMs}ms`, {
597
+ code: import_protocol2.VerificationCode.TLSError,
464
598
  transient: true
465
599
  }));
466
600
  });
467
601
  req.on("error", (err) => {
468
602
  const nodeErr = err;
469
603
  const isTls = nodeErr.code === "DEPTH_ZERO_SELF_SIGNED_CERT" || nodeErr.code === "CERT_HAS_EXPIRED" || nodeErr.code === "ERR_TLS_CERT_ALTNAME_INVALID" || nodeErr.code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE";
470
- reject(new import_protocol.VerificationError(`HTTPS fetch failed: ${err.message}`, {
471
- code: import_protocol.VerificationCode.TLSError,
604
+ reject(new import_protocol2.VerificationError(`HTTPS fetch failed: ${err.message}`, {
605
+ code: import_protocol2.VerificationCode.TLSError,
472
606
  transient: !isTls
473
607
  }));
474
608
  });
package/dist/index.d.cts CHANGED
@@ -61,8 +61,8 @@ interface SsrfSafeFetchOptions {
61
61
  * const fetchImpl = createDnsidFetch(config);
62
62
  * const dnsResolver = createDefaultDnsResolver(config);
63
63
  *
64
- * const res = await fetchImpl('https://agent.example.com/.well-known/jwks.json');
65
- * const [records, dnssec] = await dnsResolver.fetchTXT('_dnsid.agent.example.com');
64
+ * const res = await fetchImpl('https://agent.example/.well-known/jwks.json');
65
+ * const [records, dnssec] = await dnsResolver.fetchTXT('_dnsid.agent.example');
66
66
  * ```
67
67
  */
68
68
  declare function createDnsidFetch(config: TransportConfig): FetchLike;
@@ -85,10 +85,9 @@ declare function createSsrfSafeFetch(config?: TransportConfig, options?: SsrfSaf
85
85
  /**
86
86
  * Creates the DNS resolver used to fetch DNSid identity records (TXT).
87
87
  *
88
- * Uses the configured DNS server when present, otherwise the system resolver.
89
- * Either way the underlying lookup cannot observe DNSSEC validation, so
90
- * results carry `DNSSECState.UNKNOWN` (or `FAILED` when the query SERVFAILs).
91
- * Node's TXT API omits TTLs, so results use TTL 0 to disable SDK caching.
88
+ * Uses the configured server, or queries Node's system DNS server list in order.
89
+ * Wire replies supply remaining TXT TTLs; if those servers cannot answer, the
90
+ * system TXT API is the fallback (TTL 0). DNSSEC state is always `UNKNOWN`.
92
91
  */
93
92
  declare function createDefaultDnsResolver(config: Pick<TransportConfig, 'dnsServer'>): DNSResolver;
94
93
  /**
@@ -96,10 +95,9 @@ declare function createDefaultDnsResolver(config: Pick<TransportConfig, 'dnsServ
96
95
  *
97
96
  * Accepts `host`, `host:port`, or `[ipv6]:port`. A hostname (rather than an
98
97
  * IP literal) is resolved once via the system resolver on first use, then
99
- * cached for the resolver's lifetime. TXT answers are returned with TTL 0
100
- * (no SDK caching) and `DNSSECState.UNKNOWN`; `ENODATA`/`ENOTFOUND` yield an empty
101
- * record set, `ESERVFAIL` yields `DNSSECState.FAILED`, and other DNS errors
102
- * are rethrown as-is.
98
+ * cached for the resolver's lifetime. TXT answers carry the remaining TTL
99
+ * (capped at one day) and `DNSSECState.UNKNOWN`; NXDOMAIN/NODATA yield an empty
100
+ * record set, and other errors (including SERVFAIL) reject.
103
101
  *
104
102
  * @param server - DNS server address, optionally with port.
105
103
  */
package/dist/index.d.ts CHANGED
@@ -61,8 +61,8 @@ interface SsrfSafeFetchOptions {
61
61
  * const fetchImpl = createDnsidFetch(config);
62
62
  * const dnsResolver = createDefaultDnsResolver(config);
63
63
  *
64
- * const res = await fetchImpl('https://agent.example.com/.well-known/jwks.json');
65
- * const [records, dnssec] = await dnsResolver.fetchTXT('_dnsid.agent.example.com');
64
+ * const res = await fetchImpl('https://agent.example/.well-known/jwks.json');
65
+ * const [records, dnssec] = await dnsResolver.fetchTXT('_dnsid.agent.example');
66
66
  * ```
67
67
  */
68
68
  declare function createDnsidFetch(config: TransportConfig): FetchLike;
@@ -85,10 +85,9 @@ declare function createSsrfSafeFetch(config?: TransportConfig, options?: SsrfSaf
85
85
  /**
86
86
  * Creates the DNS resolver used to fetch DNSid identity records (TXT).
87
87
  *
88
- * Uses the configured DNS server when present, otherwise the system resolver.
89
- * Either way the underlying lookup cannot observe DNSSEC validation, so
90
- * results carry `DNSSECState.UNKNOWN` (or `FAILED` when the query SERVFAILs).
91
- * Node's TXT API omits TTLs, so results use TTL 0 to disable SDK caching.
88
+ * Uses the configured server, or queries Node's system DNS server list in order.
89
+ * Wire replies supply remaining TXT TTLs; if those servers cannot answer, the
90
+ * system TXT API is the fallback (TTL 0). DNSSEC state is always `UNKNOWN`.
92
91
  */
93
92
  declare function createDefaultDnsResolver(config: Pick<TransportConfig, 'dnsServer'>): DNSResolver;
94
93
  /**
@@ -96,10 +95,9 @@ declare function createDefaultDnsResolver(config: Pick<TransportConfig, 'dnsServ
96
95
  *
97
96
  * Accepts `host`, `host:port`, or `[ipv6]:port`. A hostname (rather than an
98
97
  * IP literal) is resolved once via the system resolver on first use, then
99
- * cached for the resolver's lifetime. TXT answers are returned with TTL 0
100
- * (no SDK caching) and `DNSSECState.UNKNOWN`; `ENODATA`/`ENOTFOUND` yield an empty
101
- * record set, `ESERVFAIL` yields `DNSSECState.FAILED`, and other DNS errors
102
- * are rethrown as-is.
98
+ * cached for the resolver's lifetime. TXT answers carry the remaining TTL
99
+ * (capped at one day) and `DNSSECState.UNKNOWN`; NXDOMAIN/NODATA yield an empty
100
+ * record set, and other errors (including SERVFAIL) reject.
103
101
  *
104
102
  * @param server - DNS server address, optionally with port.
105
103
  */
package/dist/index.js CHANGED
@@ -2,10 +2,136 @@
2
2
  import * as dnsPromises from "dns/promises";
3
3
  import * as fs from "fs";
4
4
  import * as https from "https";
5
- import * as net from "net";
5
+ import * as net2 from "net";
6
6
  import * as tls from "tls";
7
7
  import { Agent as Agent2, fetch as undiciFetch } from "undici";
8
- import { DNSSECState, VerificationCode, VerificationError, normalizePrivateAddressHost } from "@dnsid-ai/protocol";
8
+ import { ArgumentError, DNSSECState as DNSSECState2, VerificationCode, VerificationError, normalizePrivateAddressHost } from "@dnsid-ai/protocol";
9
+
10
+ // src/txt-wire.ts
11
+ import { randomInt } from "crypto";
12
+ import * as dgram from "dgram";
13
+ import * as net from "net";
14
+ import packet from "dns-packet";
15
+ import { DNSSECState } from "@dnsid-ai/protocol";
16
+ var normalize = (name) => name.replace(/\.$/, "").toLowerCase();
17
+ var badResponse = () => new Error("invalid DNS TXT response");
18
+ var remainingTtl = (ttl) => ttl !== void 0 && Number.isInteger(ttl) && ttl >= 0 && ttl <= 2147483647 ? ttl : 0;
19
+ async function resolveTxtWithTtl(name, host, port, signal) {
20
+ const deadline = AbortSignal.any([signal ?? new AbortController().signal, AbortSignal.timeout(2e3)]);
21
+ let owner = normalize(name);
22
+ const seen = /* @__PURE__ */ new Set([owner]);
23
+ let hops = 0;
24
+ let ttl = Infinity;
25
+ while (true) {
26
+ deadline.throwIfAborted();
27
+ const queried = owner;
28
+ const response = await query(owner, host, port, deadline);
29
+ if (response.rcode === "NXDOMAIN") return [[], DNSSECState.UNKNOWN];
30
+ if (response.rcode !== "NOERROR") throw new Error(`DNS query failed: ${response.rcode}`);
31
+ const answers = response.answers ?? [];
32
+ while (true) {
33
+ const cnames = answers.filter((a) => a.type === "CNAME" && normalize(a.name) === owner && a.class === "IN");
34
+ if (!cnames.length) break;
35
+ if (cnames.length !== 1 || ++hops > 8) throw badResponse();
36
+ const cname = cnames[0];
37
+ if (typeof cname.data !== "string") throw badResponse();
38
+ ttl = Math.min(ttl, remainingTtl(cname.ttl));
39
+ owner = normalize(cname.data);
40
+ if (seen.has(owner)) throw badResponse();
41
+ seen.add(owner);
42
+ }
43
+ const txt = answers.filter((a) => a.type === "TXT" && normalize(a.name) === owner && a.class === "IN");
44
+ if (txt.length) {
45
+ ttl = Math.min(ttl, ...txt.map((a) => remainingTtl(a.ttl)));
46
+ const decoder = new TextDecoder("utf-8", { fatal: true });
47
+ return [txt.map((a) => {
48
+ if (!Array.isArray(a.data) || !a.data.every(Buffer.isBuffer)) throw badResponse();
49
+ return { strings: a.data.map((part) => decoder.decode(part)), ttl: Math.min(ttl, 86400) };
50
+ }), DNSSECState.UNKNOWN];
51
+ }
52
+ if (owner === queried) return [[], DNSSECState.UNKNOWN];
53
+ }
54
+ }
55
+ async function query(name, host, port, signal) {
56
+ const id = randomInt(65536);
57
+ const request = {
58
+ type: "query",
59
+ id,
60
+ flags: packet.RECURSION_DESIRED,
61
+ questions: [{ type: "TXT", class: "IN", name }],
62
+ additionals: [{ type: "OPT", name: ".", udpPayloadSize: 1232 }]
63
+ };
64
+ const valid = (data) => {
65
+ try {
66
+ const response2 = packet.decode(data);
67
+ if (packet.decode.bytes > data.length || response2.type !== "response" || response2.id !== id || response2.opcode !== "QUERY" || response2.questions?.length !== 1 || response2.questions[0].type !== "TXT" || response2.questions[0].class !== "IN" || normalize(response2.questions[0].name) !== name) return null;
68
+ return response2;
69
+ } catch {
70
+ return null;
71
+ }
72
+ };
73
+ const response = await udp(packet.encode(request), host, port, signal, valid);
74
+ if (!response.flag_tc) return response;
75
+ const tcpResponse = valid(await tcp(packet.streamEncode(request), host, port, signal));
76
+ if (!tcpResponse || tcpResponse.flag_tc) throw badResponse();
77
+ return tcpResponse;
78
+ }
79
+ function udp(buf, host, port, signal, valid) {
80
+ return new Promise((resolve, reject) => {
81
+ const sock = dgram.createSocket(net.isIP(host) === 6 ? "udp6" : "udp4");
82
+ let finished = false;
83
+ const done = (error, response) => {
84
+ if (finished) return;
85
+ finished = true;
86
+ signal.removeEventListener("abort", onAbort);
87
+ sock.close();
88
+ if (error) reject(error);
89
+ else resolve(response);
90
+ };
91
+ const onAbort = () => done(signal.reason ?? new Error("DNS query aborted"));
92
+ signal.addEventListener("abort", onAbort, { once: true });
93
+ sock.on("error", done);
94
+ sock.on("message", (data) => {
95
+ const response = valid(data);
96
+ if (response) done(void 0, response);
97
+ });
98
+ if (signal.aborted) return onAbort();
99
+ sock.connect(port, host, () => {
100
+ if (!finished) sock.send(buf);
101
+ });
102
+ });
103
+ }
104
+ function tcp(buf, host, port, signal) {
105
+ return new Promise((resolve, reject) => {
106
+ const sock = net.connect({ host, port });
107
+ let data = Buffer.alloc(0);
108
+ let finished = false;
109
+ const done = (error, response) => {
110
+ if (finished) return;
111
+ finished = true;
112
+ signal.removeEventListener("abort", onAbort);
113
+ sock.destroy();
114
+ if (error) reject(error);
115
+ else resolve(response);
116
+ };
117
+ const onAbort = () => done(signal.reason ?? new Error("DNS query aborted"));
118
+ signal.addEventListener("abort", onAbort, { once: true });
119
+ sock.on("error", done);
120
+ sock.on("end", () => done(badResponse()));
121
+ sock.on("connect", () => sock.write(buf));
122
+ sock.on("data", (chunk) => {
123
+ if (!Buffer.isBuffer(chunk) || data.length + chunk.length > 65537) return done(badResponse());
124
+ data = Buffer.concat([data, chunk]);
125
+ if (data.length >= 2 && data.length >= data.readUInt16BE(0) + 2) {
126
+ if (data.length !== data.readUInt16BE(0) + 2) return done(badResponse());
127
+ done(void 0, data.subarray(2));
128
+ }
129
+ });
130
+ if (signal.aborted) onAbort();
131
+ });
132
+ }
133
+
134
+ // src/index.ts
9
135
  var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
10
136
  var DEFAULT_MAX_RESPONSE_BYTES = 1 * 1024 * 1024;
11
137
  var DNSID_USER_AGENT = "dnsid-ts";
@@ -13,16 +139,16 @@ function createDnsidFetch(config) {
13
139
  if (!config.dnsServer && !config.caBundlePath) {
14
140
  return globalThis.fetch.bind(globalThis);
15
141
  }
16
- const connect = {};
17
- if (config.dnsServer) connect.lookup = createLookup(config.dnsServer);
18
- if (config.caBundlePath) connect.ca = [...tls.rootCertificates, fs.readFileSync(config.caBundlePath, "utf8")];
19
- return createFetchWithDispatcher(new Agent2({ connect }));
142
+ const connect2 = {};
143
+ if (config.dnsServer) connect2.lookup = createLookup(config.dnsServer);
144
+ if (config.caBundlePath) connect2.ca = [...tls.rootCertificates, fs.readFileSync(config.caBundlePath, "utf8")];
145
+ return createFetchWithDispatcher(new Agent2({ connect: connect2 }));
20
146
  }
21
147
  function createSsrfSafeFetch(config = {}, options = {}) {
22
148
  const privateAddressHosts = (options.privateAddressHosts ?? []).map(normalizePrivateAddressHost);
23
- const connect = { lookup: createSsrfSafeLookup(config.dnsServer, privateAddressHosts) };
24
- if (config.caBundlePath) connect.ca = [...tls.rootCertificates, fs.readFileSync(config.caBundlePath, "utf8")];
25
- const fetchWithSafeLookup = createFetchWithDispatcher(new Agent2({ connect }));
149
+ const connect2 = { lookup: createSsrfSafeLookup(config.dnsServer, privateAddressHosts) };
150
+ if (config.caBundlePath) connect2.ca = [...tls.rootCertificates, fs.readFileSync(config.caBundlePath, "utf8")];
151
+ const fetchWithSafeLookup = createFetchWithDispatcher(new Agent2({ connect: connect2 }));
26
152
  return async (input, init) => {
27
153
  const url = new URL(input instanceof Request ? input.url : input);
28
154
  if (isUnsafeIp(url.hostname)) {
@@ -42,25 +168,35 @@ function createFetchWithDispatcher(dispatcher) {
42
168
  };
43
169
  }
44
170
  function createDefaultDnsResolver(config) {
45
- return config.dnsServer ? createDnsResolverFromServer(config.dnsServer) : createSystemDnsResolver();
171
+ if (config.dnsServer) return createDnsResolverFromServer(config.dnsServer);
172
+ const system = createSystemDnsResolver();
173
+ return {
174
+ async fetchTXT(name, options) {
175
+ for (const server of dnsPromises.getServers()) {
176
+ options?.signal?.throwIfAborted();
177
+ try {
178
+ const result = await createDnsResolverFromServer(server).fetchTXT(name, options);
179
+ if (result[0].length) return result;
180
+ break;
181
+ } catch (error) {
182
+ if (options?.signal?.aborted) throw error;
183
+ }
184
+ }
185
+ options?.signal?.throwIfAborted();
186
+ return system.fetchTXT(name, options);
187
+ }
188
+ };
46
189
  }
47
190
  function createDnsResolverFromServer(server) {
48
191
  const parsed = parseDnsServer(server);
49
- if (!parsed || net.isIP(parsed.host)) {
50
- const resolver = new dnsPromises.Resolver();
51
- resolver.setServers([server]);
52
- return createSystemDnsResolver(resolver);
53
- }
54
- let resolverPromise = null;
192
+ if (!parsed) throw new ArgumentError("invalid DNS server");
193
+ const port = Number(parsed.port ?? 53);
194
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new ArgumentError(`invalid DNS server port: ${parsed.port}`);
195
+ let hostPromise;
55
196
  return {
56
- async fetchTXT(name) {
57
- resolverPromise ??= (async () => {
58
- const { address } = await dnsPromises.lookup(parsed.host);
59
- const resolver = new dnsPromises.Resolver();
60
- resolver.setServers([formatDnsServer(address, parsed.port)]);
61
- return resolver;
62
- })();
63
- return fetchTxtViaSystem(name, await resolverPromise);
197
+ async fetchTXT(name, options) {
198
+ hostPromise ??= net2.isIP(parsed.host) ? Promise.resolve(parsed.host) : dnsPromises.lookup(parsed.host).then(({ address }) => address);
199
+ return resolveTxtWithTtl(name, await hostPromise, port, options?.signal);
64
200
  }
65
201
  };
66
202
  }
@@ -77,7 +213,7 @@ function parseDnsServer(server) {
77
213
  return { host: server };
78
214
  }
79
215
  function formatDnsServer(address, port) {
80
- const host = net.isIP(address) === 6 ? `[${address}]` : address;
216
+ const host = net2.isIP(address) === 6 ? `[${address}]` : address;
81
217
  return port ? `${host}:${port}` : host;
82
218
  }
83
219
  function createLookup(dnsServer) {
@@ -85,7 +221,7 @@ function createLookup(dnsServer) {
85
221
  async function resolver() {
86
222
  resolverPromise ??= (async () => {
87
223
  const parsed = parseDnsServer(dnsServer);
88
- const server = parsed && !net.isIP(parsed.host) ? formatDnsServer((await dnsPromises.lookup(parsed.host)).address, parsed.port) : dnsServer;
224
+ const server = parsed && !net2.isIP(parsed.host) ? formatDnsServer((await dnsPromises.lookup(parsed.host)).address, parsed.port) : dnsServer;
89
225
  const r = new dnsPromises.Resolver();
90
226
  r.setServers([server]);
91
227
  return r;
@@ -116,7 +252,7 @@ function createSsrfSafeLookup(dnsServer, privateAddressHosts = []) {
116
252
  if (!dnsServer) throw new Error("no custom DNS server configured");
117
253
  resolverPromise ??= (async () => {
118
254
  const parsed = parseDnsServer(dnsServer);
119
- const server = parsed && !net.isIP(parsed.host) ? formatDnsServer((await dnsPromises.lookup(parsed.host)).address, parsed.port) : dnsServer;
255
+ const server = parsed && !net2.isIP(parsed.host) ? formatDnsServer((await dnsPromises.lookup(parsed.host)).address, parsed.port) : dnsServer;
120
256
  const r = new dnsPromises.Resolver();
121
257
  r.setServers([server]);
122
258
  return r;
@@ -148,7 +284,7 @@ function privateAddressHostMatches(hostname, privateAddressHosts) {
148
284
  return privateAddressHosts.some((entry) => entry.startsWith(".") ? host === entry.slice(1) || host.endsWith(entry) : host === entry);
149
285
  }
150
286
  async function resolveAddresses(hostname, family, resolver) {
151
- const ipFamily = net.isIP(hostname);
287
+ const ipFamily = net2.isIP(hostname);
152
288
  if (ipFamily === 4 || ipFamily === 6) return [{ address: hostname, family: ipFamily }];
153
289
  if (!resolver) {
154
290
  const records = await dnsPromises.lookup(hostname, { all: true, family: family === 4 || family === 6 ? family : 0 });
@@ -167,7 +303,7 @@ function isUnsafeIp(address) {
167
303
  if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
168
304
  const zoneIndex = host.indexOf("%");
169
305
  const normalized = zoneIndex === -1 ? host : host.slice(0, zoneIndex);
170
- const family = net.isIP(normalized);
306
+ const family = net2.isIP(normalized);
171
307
  if (family === 4) return isUnsafeIpv4(normalized);
172
308
  if (family === 6) return isUnsafeIpv6(normalized);
173
309
  return false;
@@ -177,7 +313,7 @@ function isPrivateOrLoopbackIp(address) {
177
313
  if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
178
314
  const zoneIndex = host.indexOf("%");
179
315
  const normalized = zoneIndex === -1 ? host : host.slice(0, zoneIndex);
180
- const family = net.isIP(normalized);
316
+ const family = net2.isIP(normalized);
181
317
  if (family === 4) {
182
318
  const [a, b] = normalized.split(".").map(Number);
183
319
  return a === 10 || a === 127 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
@@ -277,15 +413,13 @@ function createSystemDnsResolver(resolver = dnsPromises) {
277
413
  async function fetchTxtViaSystem(name, resolver) {
278
414
  try {
279
415
  const records = await resolver.resolveTxt(name);
280
- return [records.map((strings) => ({ strings, ttl: 0 })), DNSSECState.UNKNOWN];
416
+ return [records.map((strings) => ({ strings, ttl: 0 })), DNSSECState2.UNKNOWN];
281
417
  } catch (err) {
282
418
  const nodeErr = err;
283
419
  switch (nodeErr.code) {
284
420
  case "ENODATA":
285
421
  case "ENOTFOUND":
286
- return [[], DNSSECState.UNKNOWN];
287
- case "ESERVFAIL":
288
- return [[], DNSSECState.FAILED];
422
+ return [[], DNSSECState2.UNKNOWN];
289
423
  default:
290
424
  throw err;
291
425
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnsid-ai/transport",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
4
4
  "description": "Node.js DNS and HTTPS transport implementations for DNSid.",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -38,7 +38,8 @@
38
38
  "typecheck": "tsc --noEmit"
39
39
  },
40
40
  "dependencies": {
41
- "@dnsid-ai/protocol": "^0.24.0",
41
+ "@dnsid-ai/protocol": "^0.24.2",
42
+ "dns-packet": "5.6.1",
42
43
  "undici": "^8.10.2"
43
44
  },
44
45
  "publishConfig": {