tangerine 1.3.0 → 1.4.0

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.
Files changed (3) hide show
  1. package/README.md +52 -36
  2. package/index.js +163 -63
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  </div>
12
12
  <br />
13
13
  <div align="center">
14
- 🍊 <a href="https://github.com/forwardemail/tangerine" target="_blank">Tangerine</a> is the best <a href="https://nodejs.org" target="_blank">Node.js</a> drop-in replacement for <a href="https://nodejs.org/api/dns.html#resolveroptions" target="_blank">dns.promises.Resolver</a> using <a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank">DNS over HTTPS</a> ("DoH") via <a href="https://github.com/nodejs/undici" target="_blank">undici</a> with built-in retries, timeouts, smart server rotation, <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController" target="_blank">AbortControllers</a>, and caching support for multiple backends (with TTL support).
14
+ 🍊 <a href="https://github.com/forwardemail/tangerine" target="_blank">Tangerine</a> is the best <a href="https://nodejs.org" target="_blank">Node.js</a> drop-in replacement for <a href="https://nodejs.org/api/dns.html#resolveroptions" target="_blank">dns.promises.Resolver</a> using <a href="https://en.wikipedia.org/wiki/DNS_over_HTTPS" target="_blank">DNS over HTTPS</a> ("DoH") via <a href="https://github.com/nodejs/undici" target="_blank">undici</a> with built-in retries, timeouts, smart server rotation, <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController" target="_blank">AbortControllers</a>, and caching support for multiple backends (with TTL and purge support).
15
15
  </div>
16
16
  <hr />
17
17
  <div align="center">
@@ -37,21 +37,21 @@
37
37
  * [`tangerine.cancel()`](#tangerinecancel)
38
38
  * [`tangerine.getServers()`](#tangerinegetservers)
39
39
  * [`tangerine.lookup(hostname[, options])`](#tangerinelookuphostname-options)
40
- * [`tangerine.lookupService(address, port, abortController)`](#tangerinelookupserviceaddress-port-abortcontroller)
40
+ * [`tangerine.lookupService(address, port[, abortController, purgeCache])`](#tangerinelookupserviceaddress-port-abortcontroller-purgecache)
41
41
  * [`tangerine.resolve(hostname[, rrtype, options, abortController])`](#tangerineresolvehostname-rrtype-options-abortcontroller)
42
42
  * [`tangerine.resolve4(hostname[, options, abortController])`](#tangerineresolve4hostname-options-abortcontroller)
43
43
  * [`tangerine.resolve6(hostname[, options, abortController])`](#tangerineresolve6hostname-options-abortcontroller)
44
- * [`tangerine.resolveAny(hostname[, abortController])`](#tangerineresolveanyhostname-abortcontroller)
45
- * [`tangerine.resolveCaa(hostname[, abortController]))`](#tangerineresolvecaahostname-abortcontroller)
46
- * [`tangerine.resolveCname(hostname[, abortController]))`](#tangerineresolvecnamehostname-abortcontroller)
47
- * [`tangerine.resolveMx(hostname[, abortController]))`](#tangerineresolvemxhostname-abortcontroller)
48
- * [`tangerine.resolveNaptr(hostname[, abortController]))`](#tangerineresolvenaptrhostname-abortcontroller)
49
- * [`tangerine.resolveNs(hostname[, abortController]))`](#tangerineresolvenshostname-abortcontroller)
50
- * [`tangerine.resolvePtr(hostname[, abortController]))`](#tangerineresolveptrhostname-abortcontroller)
51
- * [`tangerine.resolveSoa(hostname[, abortController]))`](#tangerineresolvesoahostname-abortcontroller)
52
- * [`tangerine.resolveSrv(hostname[, abortController]))`](#tangerineresolvesrvhostname-abortcontroller)
53
- * [`tangerine.resolveTxt(hostname[, abortController]))`](#tangerineresolvetxthostname-abortcontroller)
54
- * [`tangerine.reverse(ip[, abortController])`](#tangerinereverseip-abortcontroller)
44
+ * [`tangerine.resolveAny(hostname[, options, abortController])`](#tangerineresolveanyhostname-options-abortcontroller)
45
+ * [`tangerine.resolveCaa(hostname[, options, abortController]))`](#tangerineresolvecaahostname-options-abortcontroller)
46
+ * [`tangerine.resolveCname(hostname[, options, abortController]))`](#tangerineresolvecnamehostname-options-abortcontroller)
47
+ * [`tangerine.resolveMx(hostname[, options, abortController]))`](#tangerineresolvemxhostname-options-abortcontroller)
48
+ * [`tangerine.resolveNaptr(hostname[, options, abortController]))`](#tangerineresolvenaptrhostname-options-abortcontroller)
49
+ * [`tangerine.resolveNs(hostname[, options, abortController]))`](#tangerineresolvenshostname-options-abortcontroller)
50
+ * [`tangerine.resolvePtr(hostname[, options, abortController]))`](#tangerineresolveptrhostname-options-abortcontroller)
51
+ * [`tangerine.resolveSoa(hostname[, options, abortController]))`](#tangerineresolvesoahostname-options-abortcontroller)
52
+ * [`tangerine.resolveSrv(hostname[, options, abortController]))`](#tangerineresolvesrvhostname-options-abortcontroller)
53
+ * [`tangerine.resolveTxt(hostname[, options, abortController]))`](#tangerineresolvetxthostname-options-abortcontroller)
54
+ * [`tangerine.reverse(ip[, abortController, purgeCache])`](#tangerinereverseip-abortcontroller-purgecache)
55
55
  * [`tangerine.setDefaultResultOrder(order)`](#tangerinesetdefaultresultorderorder)
56
56
  * [`tangerine.setServers(servers)`](#tangerinesetserversservers)
57
57
  * [Options](#options)
@@ -97,7 +97,7 @@ Our team at [Forward Email](https://forwardemail.net) (100% open-source and priv
97
97
  * Once popular packages such as [native-dns](https://github.com/tjfontaine/node-dns/issues/111) and [dnscached](https://github.com/yahoo/dnscache/issues/28) are archived or deprecated.
98
98
  * [Other packages](https://www.npmjs.com/search?q=dns%20cache) only provide `lookup` functions, have a limited sub-set of methods such as [@zeit/dns-cached-resolver](https://github.com/vercel/dns-cached-resolve), or are unmaintained.
99
99
  * Act as a 1:1 drop-in replacement for `dns.promises.Resolver` with DNS over HTTPS ("DoH").
100
- * Support caching for multiple backends (with TTL support), retries, smart server rotation, and [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) usage.
100
+ * Support caching for multiple backends (with TTL and purge support), retries, smart server rotation, and [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) usage.
101
101
  * Provide out of the box support for both ECMAScript modules (ESM) **and** CommonJS (CJS) (see discussions [for](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c) and [against](https://gist.github.com/joepie91/bca2fda868c1e8b2c2caf76af7dfcad3)).
102
102
  * The native Node.js `dns` module does not support caching out of the box – which is a [highly requested feature](https://github.com/nodejs/node/issues/5893) (but belongs in userland).
103
103
  * Writing tests against DNS-related infrastructure requires either hacky DNS mocking or a DNS server (manipulating cache is much easier).
@@ -225,8 +225,11 @@ tangerine.resolve('forwardemail.net').then(console.log);
225
225
  * Specify default request options based off the library under `requestOptions` below
226
226
  * Instance methods of [dns.promises.Resolver](https://nodejs.org/api/dns.html) are mirrored to :tangerine: Tangerine.
227
227
  * Resolver methods accept an optional `abortController` argument, which is an instance of [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). Note that :tangerine: Tangerine manages `AbortController` usage internally – so you most likely won't need to pass your own (see [index.js](https://github.com/forwardemail/tangerine/blob/main/index.js) for more insight).
228
- * See the complete list of [Options](#options) below.
228
+ * Resolver methods that accept `options` argument also accept an optional `options.purgeCache` option.
229
+ * Resolver methods support a `purgeCache` option as either `options.purgeCache` (Boolean) via `options` argument or `purgeCache` (Boolean) argument – see [API](#api) and [Cache](#cache) for more insight.
230
+ * If set to `true`, then the result will be re-queried and re-cached – see [Cache](#cache) documentation for more insight.
229
231
  * Instances of `new Tangerine()` are instances of `dns.promises.Resolver` via `class Tangerine extends dns.promises.Resolver { ... }` (namely for compatibility with projects such as [cacheable-lookup](https://github.com/szmarczak/cacheable-lookup)).
232
+ * See the complete list of [Options](#options) below.
230
233
 
231
234
  ### `tangerine.cancel()`
232
235
 
@@ -234,7 +237,7 @@ tangerine.resolve('forwardemail.net').then(console.log);
234
237
 
235
238
  ### `tangerine.lookup(hostname[, options])`
236
239
 
237
- ### `tangerine.lookupService(address, port, abortController)`
240
+ ### `tangerine.lookupService(address, port[, abortController, purgeCache])`
238
241
 
239
242
  ### `tangerine.resolve(hostname[, rrtype, options, abortController])`
240
243
 
@@ -246,27 +249,27 @@ Tangerine supports a new `ecsSubnet` property in the `options` Object argument.
246
249
 
247
250
  Tangerine supports a new `ecsSubnet` property in the `options` Object argument.
248
251
 
249
- ### `tangerine.resolveAny(hostname[, abortController])`
252
+ ### `tangerine.resolveAny(hostname[, options, abortController])`
250
253
 
251
- ### `tangerine.resolveCaa(hostname[, abortController]))`
254
+ ### `tangerine.resolveCaa(hostname[, options, abortController]))`
252
255
 
253
- ### `tangerine.resolveCname(hostname[, abortController]))`
256
+ ### `tangerine.resolveCname(hostname[, options, abortController]))`
254
257
 
255
- ### `tangerine.resolveMx(hostname[, abortController]))`
258
+ ### `tangerine.resolveMx(hostname[, options, abortController]))`
256
259
 
257
- ### `tangerine.resolveNaptr(hostname[, abortController]))`
260
+ ### `tangerine.resolveNaptr(hostname[, options, abortController]))`
258
261
 
259
- ### `tangerine.resolveNs(hostname[, abortController]))`
262
+ ### `tangerine.resolveNs(hostname[, options, abortController]))`
260
263
 
261
- ### `tangerine.resolvePtr(hostname[, abortController]))`
264
+ ### `tangerine.resolvePtr(hostname[, options, abortController]))`
262
265
 
263
- ### `tangerine.resolveSoa(hostname[, abortController]))`
266
+ ### `tangerine.resolveSoa(hostname[, options, abortController]))`
264
267
 
265
- ### `tangerine.resolveSrv(hostname[, abortController]))`
268
+ ### `tangerine.resolveSrv(hostname[, options, abortController]))`
266
269
 
267
- ### `tangerine.resolveTxt(hostname[, abortController]))`
270
+ ### `tangerine.resolveTxt(hostname[, options, abortController]))`
268
271
 
269
- ### `tangerine.reverse(ip[, abortController])`
272
+ ### `tangerine.reverse(ip[, abortController, purgeCache])`
270
273
 
271
274
  ### `tangerine.setDefaultResultOrder(order)`
272
275
 
@@ -358,6 +361,19 @@ without cache: 98.25ms
358
361
  with cache: 0.091ms
359
362
  ```
360
363
 
364
+ You can also force the cache to be purged and reset to a new value:
365
+
366
+ ```js
367
+ await tangerine.resolve('forwardemail.net'); // cached
368
+ await tangerine.resolve('forwardemail.net'); // uses cached value
369
+ await tangerine.resolve('forwardemail.net'); // uses cached value
370
+ await tangerine.resolve('forwardemail.net', { purgeCache: true }); // re-cached
371
+ await tangerine.resolve('forwardemail.net'); // uses cached value
372
+ await tangerine.resolve('forwardemail.net'); // uses cached value
373
+ ```
374
+
375
+ This purge cache feature is useful for DNS records that have recently changed and have had their caches purged at the relevant DNS provider (e.g. [Cloudflare's Purge Cache tool](https://1.1.1.1/purge-cache/)).
376
+
361
377
 
362
378
  ## Debugging
363
379
 
@@ -385,10 +401,10 @@ npm install
385
401
  npm run benchmarks
386
402
  ```
387
403
 
388
- You can also specify optional custom environment variables to test against real-world or locally running servers (instead of using mocked in-memory servers):
404
+ You can also specify optional custom environment variables to test against real-world or locally running servers (instead of using mocked in-memory servers) for the [HTTP Library Benchmarks](#http-library-benchmarks):
389
405
 
390
406
  ```sh
391
- BENCHMARK_PROTOCOL="http" BENCHMARK_HOST="127.0.0.1" BENCHMARK_PORT="4000" BENCHMARK_PATH="/v1/test" npm run benchmarks
407
+ BENCHMARK_PROTOCOL="http" BENCHMARK_HOST="127.0.0.1" BENCHMARK_PORT="4000" BENCHMARK_PATH="/v1/test" node benchmarks/http
392
408
  ```
393
409
 
394
410
  ### Tangerine Benchmarks
@@ -400,7 +416,7 @@ The latest benchmark results are viewable on GitHub under this repository's [Git
400
416
  > [Node 16 on ubuntu-latest](https://github.com/forwardemail/tangerine/actions/runs/4297805550/jobs/7491228635#step:6:1)
401
417
 
402
418
  ```diff
403
- > node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse
419
+ node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse
404
420
 
405
421
  Started: lookup
406
422
  tangerine.lookup POST with caching using Cloudflare x 735 ops/sec ±195.35% (88 runs sampled)
@@ -437,7 +453,7 @@ dns.promises.reverse with caching x 5,123,900 ops/sec ±0.96% (85 runs sampled)
437
453
  > [Node 18 on ubuntu latest](https://github.com/forwardemail/tangerine/actions/runs/4297805550/jobs/7491228742#step:6:1)
438
454
 
439
455
  ```diff
440
- > node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse && node benchmarks/http
456
+ node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse && node benchmarks/http
441
457
 
442
458
  Started: lookup
443
459
  tangerine.lookup POST with caching using Cloudflare x 666 ops/sec ±195.48% (87 runs sampled)
@@ -482,10 +498,10 @@ Provided below are additional benchmark tests we have run:
482
498
  > Node v18.14.2 on MacBook Air M1 16GB (without VPN):
483
499
 
484
500
  ```diff
485
- > node --version
501
+ node --version
486
502
  v18.14.2
487
503
 
488
- > node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse
504
+ node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse
489
505
 
490
506
  Started: lookup
491
507
  tangerine.lookup POST with caching using Cloudflare x 1,035 ops/sec ±195.73% (91 runs sampled)
@@ -522,10 +538,10 @@ Fastest without caching is: dns.promises.reverse without caching, resolver.rever
522
538
  > Node v18.14.2 on MacBook Air M1 16GB (with DNS blackholed VPN) – **this highlights the DNS blackhole problem**:
523
539
 
524
540
  ```diff
525
- > node --version
541
+ node --version
526
542
  v18.14.2
527
543
 
528
- > node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse
544
+ node benchmarks/lookup && node benchmarks/resolve && node benchmarks/reverse
529
545
 
530
546
  Started: lookup
531
547
  tangerine.lookup POST with caching using Cloudflare x 1,327 ops/sec ±195.65% (89 runs sampled)
@@ -570,7 +586,7 @@ Originally we wrote this library using [got](https://github.com/sindresorhus/got
570
586
  > Node v18.14.2 on MacBook Air M1 16GB (using real-world API server):
571
587
 
572
588
  ```sh
573
- > node --version
589
+ node --version
574
590
  v18.14.2
575
591
 
576
592
  > BENCHMARK_HOST="127.0.0.1" BENCHMARK_PORT="4000" BENCHMARK_PATH="/v1/test" node benchmarks/http
package/index.js CHANGED
@@ -7,6 +7,7 @@ const { debuglog } = require('node:util');
7
7
  const { getEventListeners, setMaxListeners } = require('node:events');
8
8
  const { isIP, isIPv4, isIPv6 } = require('node:net');
9
9
 
10
+ const { toASCII } = require('punycode/');
10
11
  const getStream = require('get-stream');
11
12
  const ipaddr = require('ipaddr.js');
12
13
  const mergeOptions = require('merge-options');
@@ -464,7 +465,7 @@ class Tangerine extends dns.promises.Resolver {
464
465
 
465
466
  options = { family: options };
466
467
  } else if (
467
- typeof options.family !== 'undefined' &&
468
+ typeof options?.family !== 'undefined' &&
468
469
  ![0, 4, 6, 'IPv4', 'IPv6'].includes(options.family)
469
470
  ) {
470
471
  // validate family
@@ -475,12 +476,12 @@ class Tangerine extends dns.promises.Resolver {
475
476
  throw err;
476
477
  }
477
478
 
478
- if (options.family === 'IPv4') options.family = 4;
479
- else if (options.family === 'IPv6') options.family = 6;
479
+ if (options?.family === 'IPv4') options.family = 4;
480
+ else if (options?.family === 'IPv6') options.family = 6;
480
481
 
481
482
  // validate hints
482
483
  // eslint-disable-next-line no-bitwise
483
- if ((options.hints & ~(dns.ADDRCONFIG | dns.ALL | dns.V4MAPPED)) !== 0) {
484
+ if ((options?.hints & ~(dns.ADDRCONFIG | dns.ALL | dns.V4MAPPED)) !== 0) {
484
485
  const err = new TypeError(
485
486
  `The argument 'hints' is invalid. Received ${options.hints}`
486
487
  );
@@ -488,16 +489,55 @@ class Tangerine extends dns.promises.Resolver {
488
489
  throw err;
489
490
  }
490
491
 
491
- // resolve the first A or AAAA record
492
+ // purge cache support
493
+ let purgeCache;
494
+ if (options?.purgeCache) {
495
+ purgeCache = true;
496
+ delete options.purgeCache;
497
+ }
498
+
499
+ if (options.hints) {
500
+ switch (options.hints) {
501
+ case dns.ADDRCONFIG: {
502
+ options.family = this.constructor.getAddrConfigTypes();
503
+ break;
504
+ }
505
+
506
+ // eslint-disable-next-line no-bitwise
507
+ case dns.ADDRCONFIG | dns.V4MAPPED: {
508
+ options.family = this.constructor.getAddrConfigTypes();
509
+ break;
510
+ }
511
+
512
+ // eslint-disable-next-line no-bitwise
513
+ case dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL: {
514
+ options.family = this.constructor.getAddrConfigTypes();
515
+
516
+ break;
517
+ }
518
+
519
+ default: {
520
+ break;
521
+ }
522
+ }
523
+ }
524
+
525
+ // resolve the first A or AAAA record (conditionally)
492
526
  let answers = [];
493
527
 
494
528
  try {
495
- answers = await Promise.any([
496
- this.resolve4(name),
497
- // the only downside here is that if one succeeds the other won't be aborted
498
- // (an alternative approach could be implemented, but would have to wrap around ENOTFOUND err
499
- this.resolve6(name)
529
+ // `any` or `all` is based off !options.family || options.family === 0
530
+ // (according to official nodejs dns.lookup docs)
531
+ answers = await Promise[
532
+ typeof options.family === 'undefined' || options.family === 0
533
+ ? 'all'
534
+ : 'any'
535
+ ]([
536
+ // the only downside here is that if one succeeds the other won't be aborted (iff "any")
537
+ this.resolve4(name, { purgeCache, noThrowOnNODATA: true }),
538
+ this.resolve6(name, { purgeCache, noThrowOnNODATA: true })
500
539
  ]);
540
+ answers = answers.flat();
501
541
  } catch (_err) {
502
542
  debug(_err);
503
543
 
@@ -522,9 +562,18 @@ class Tangerine extends dns.promises.Resolver {
522
562
  throw err;
523
563
  }
524
564
 
565
+ // if no results then throw ENODATA
566
+ if (answers.length === 0) {
567
+ const err = this.constructor.createError(name, '', dns.NODATA);
568
+ // remap and perform syscall
569
+ err.syscall = 'getaddrinfo';
570
+ // err.errno = -3008;
571
+ throw err;
572
+ }
573
+
525
574
  // respect options from dns module
526
575
  // <https://nodejs.org/api/dns.html#dnspromiseslookuphostname-options>
527
- // - [x]: `family` (4, 6, or 0, default is 0)
576
+ // - [x] `family` (4, 6, or 0, default is 0)
528
577
  // - [x] `hints` multiple flags may be passed by bitwise OR'ing values
529
578
  // - [x] `all` (iff true, then return all results, otherwise single result)
530
579
  // - [x] `verbatim` - if `true` then return as-is, otherwise use dns order
@@ -541,14 +590,8 @@ class Tangerine extends dns.promises.Resolver {
541
590
  // dns.ALL:
542
591
  // If dns.V4MAPPED is specified, return resolved IPv6 addresses as well as IPv4 mapped IPv6 addresses.
543
592
  //
544
- const { hints } = options;
545
- if (hints) {
546
- switch (hints) {
547
- case dns.ADDRCONFIG: {
548
- options.family = this.constructor.getAddrConfigTypes();
549
- break;
550
- }
551
-
593
+ if (options.hints) {
594
+ switch (options.hints) {
552
595
  case dns.V4MAPPED: {
553
596
  if (options.family === 6 && !answers.some((answer) => isIPv6(answer)))
554
597
  answers = answers.map((answer) =>
@@ -564,7 +607,6 @@ class Tangerine extends dns.promises.Resolver {
564
607
 
565
608
  // eslint-disable-next-line no-bitwise
566
609
  case dns.ADDRCONFIG | dns.V4MAPPED: {
567
- options.family = this.constructor.getAddrConfigTypes();
568
610
  if (options.family === 6 && !answers.some((answer) => isIPv6(answer)))
569
611
  answers = answers.map((answer) =>
570
612
  ipaddr.parse(answer).toIPv4MappedAddress().toString()
@@ -584,7 +626,6 @@ class Tangerine extends dns.promises.Resolver {
584
626
 
585
627
  // eslint-disable-next-line no-bitwise
586
628
  case dns.ADDRCONFIG | dns.V4MAPPED | dns.ALL: {
587
- options.family = this.constructor.getAddrConfigTypes();
588
629
  if (options.family === 6 && !answers.some((answer) => isIPv6(answer)))
589
630
  answers = answers.map((answer) =>
590
631
  ipaddr.parse(answer).toIPv4MappedAddress().toString()
@@ -605,9 +646,20 @@ class Tangerine extends dns.promises.Resolver {
605
646
  else if (options.family === 6)
606
647
  answers = answers.filter((answer) => isIPv6(answer));
607
648
 
649
+ //
608
650
  // respect sort order from `setDefaultResultOrder` method
609
- if (options.verbatim !== true && this.options.dnsOrder === 'ipv4first')
610
- answers = answers.sort((answer) => (isIPv4(answer) ? 0 : 1));
651
+ //
652
+ // NOTE: we need to optimize this sort logic at some point
653
+ //
654
+ if (options.verbatim !== true && this.options.dnsOrder === 'ipv4first') {
655
+ answers = answers.sort((a, b) => {
656
+ const aFamily = isIP(a);
657
+ const bFamily = isIP(b);
658
+ if (aFamily < bFamily) return -1;
659
+ if (aFamily > bFamily) return 1;
660
+ return 0;
661
+ });
662
+ }
611
663
 
612
664
  return options.all === true
613
665
  ? answers.map((answer) => ({
@@ -618,7 +670,7 @@ class Tangerine extends dns.promises.Resolver {
618
670
  }
619
671
 
620
672
  // <https://man7.org/linux/man-pages/man3/getnameinfo.3.html>
621
- async lookupService(address, port, abortController) {
673
+ async lookupService(address, port, abortController, purgeCache = false) {
622
674
  if (!address || !port) {
623
675
  const err = new TypeError(
624
676
  'The "address" and "port" arguments must be specified.'
@@ -647,7 +699,11 @@ class Tangerine extends dns.promises.Resolver {
647
699
 
648
700
  // reverse lookup
649
701
  try {
650
- const [hostname] = await this.reverse(address, abortController);
702
+ const [hostname] = await this.reverse(
703
+ address,
704
+ abortController,
705
+ purgeCache
706
+ );
651
707
  return { hostname, service: name };
652
708
  } catch (err) {
653
709
  err.syscall = 'getnameinfo';
@@ -655,7 +711,7 @@ class Tangerine extends dns.promises.Resolver {
655
711
  }
656
712
  }
657
713
 
658
- async reverse(ip, abortController) {
714
+ async reverse(ip, abortController, purgeCache = false) {
659
715
  // basically reverse the IP and then perform PTR lookup
660
716
  if (typeof ip !== 'string') {
661
717
  const err = new TypeError('The "ip" argument must be of type string.');
@@ -677,7 +733,12 @@ class Tangerine extends dns.promises.Resolver {
677
733
 
678
734
  // perform resolvePTR
679
735
  try {
680
- const answers = await this.resolve(name, 'PTR', {}, abortController);
736
+ const answers = await this.resolve(
737
+ name,
738
+ 'PTR',
739
+ { purgeCache },
740
+ abortController
741
+ );
681
742
  return answers;
682
743
  } catch (err) {
683
744
  // remap syscall
@@ -699,40 +760,40 @@ class Tangerine extends dns.promises.Resolver {
699
760
  return this.resolve(name, 'AAAA', options, abortController);
700
761
  }
701
762
 
702
- resolveCaa(name, abortController) {
703
- return this.resolve(name, 'CAA', {}, abortController);
763
+ resolveCaa(name, options, abortController) {
764
+ return this.resolve(name, 'CAA', options, abortController);
704
765
  }
705
766
 
706
- resolveCname(name, abortController) {
707
- return this.resolve(name, 'CNAME', {}, abortController);
767
+ resolveCname(name, options, abortController) {
768
+ return this.resolve(name, 'CNAME', options, abortController);
708
769
  }
709
770
 
710
- resolveMx(name, abortController) {
711
- return this.resolve(name, 'MX', {}, abortController);
771
+ resolveMx(name, options, abortController) {
772
+ return this.resolve(name, 'MX', options, abortController);
712
773
  }
713
774
 
714
- resolveNaptr(name, abortController) {
715
- return this.resolve(name, 'NAPTR', {}, abortController);
775
+ resolveNaptr(name, options, abortController) {
776
+ return this.resolve(name, 'NAPTR', options, abortController);
716
777
  }
717
778
 
718
- resolveNs(name, abortController) {
719
- return this.resolve(name, 'NS', {}, abortController);
779
+ resolveNs(name, options, abortController) {
780
+ return this.resolve(name, 'NS', options, abortController);
720
781
  }
721
782
 
722
- resolvePtr(name, abortController) {
723
- return this.resolve(name, 'PTR', {}, abortController);
783
+ resolvePtr(name, options, abortController) {
784
+ return this.resolve(name, 'PTR', options, abortController);
724
785
  }
725
786
 
726
- resolveSoa(name, abortController) {
727
- return this.resolve(name, 'SOA', {}, abortController);
787
+ resolveSoa(name, options, abortController) {
788
+ return this.resolve(name, 'SOA', options, abortController);
728
789
  }
729
790
 
730
- resolveSrv(name, abortController) {
731
- return this.resolve(name, 'SRV', {}, abortController);
791
+ resolveSrv(name, options, abortController) {
792
+ return this.resolve(name, 'SRV', options, abortController);
732
793
  }
733
794
 
734
- resolveTxt(name, abortController) {
735
- return this.resolve(name, 'TXT', {}, abortController);
795
+ resolveTxt(name, options, abortController) {
796
+ return this.resolve(name, 'TXT', options, abortController);
736
797
  }
737
798
 
738
799
  // 1:1 mapping with node's official dns.promises API
@@ -789,7 +850,13 @@ class Tangerine extends dns.promises.Resolver {
789
850
  // eslint-disable-next-line complexity
790
851
  async #query(name, rrtype = 'A', ecsSubnet, abortController) {
791
852
  if (!dohdec) await pWaitFor(() => Boolean(dohdec));
792
- debug('query', { name, rrtype, ecsSubnet, abortController });
853
+ debug('query', {
854
+ name,
855
+ nameToASCII: toASCII(name),
856
+ rrtype,
857
+ ecsSubnet,
858
+ abortController
859
+ });
793
860
  // <https://github.com/hildjj/dohdec/blob/43564118c40f2127af871bdb4d40f615409d4b9c/pkg/dohdec/lib/dnsUtils.js#L161>
794
861
  const pkt = dohdec.DNSoverHTTPS.makePacket({
795
862
  id:
@@ -797,7 +864,8 @@ class Tangerine extends dns.promises.Resolver {
797
864
  ? await this.options.id()
798
865
  : this.options.id,
799
866
  rrtype,
800
- name,
867
+ // mirrors dns module behavior
868
+ name: toASCII(name),
801
869
  // <https://github.com/mafintosh/dns-packet/pull/47#issuecomment-1435818437>
802
870
  ecsSubnet
803
871
  });
@@ -932,7 +1000,7 @@ class Tangerine extends dns.promises.Resolver {
932
1000
  }
933
1001
  }
934
1002
 
935
- #resolveByType(name, parentAbortController) {
1003
+ #resolveByType(name, options = {}, parentAbortController) {
936
1004
  return async (type) => {
937
1005
  const abortController = new AbortController();
938
1006
  this.abortControllers.add(abortController);
@@ -956,7 +1024,7 @@ class Tangerine extends dns.promises.Resolver {
956
1024
  case 'A': {
957
1025
  const result = await this.resolve4(
958
1026
  name,
959
- { ttl: true },
1027
+ { ...options, ttl: true },
960
1028
  abortController
961
1029
  );
962
1030
  return result.map((r) => ({ type, ...r }));
@@ -965,49 +1033,73 @@ class Tangerine extends dns.promises.Resolver {
965
1033
  case 'AAAA': {
966
1034
  const result = await this.resolve6(
967
1035
  name,
968
- { ttl: true },
1036
+ { ...options, ttl: true },
969
1037
  abortController
970
1038
  );
971
1039
  return result.map((r) => ({ type, ...r }));
972
1040
  }
973
1041
 
974
1042
  case 'CNAME': {
975
- const result = await this.resolveCname(name, abortController);
1043
+ const result = await this.resolveCname(
1044
+ name,
1045
+ options,
1046
+ abortController
1047
+ );
976
1048
  return result.map((value) => ({ type, value }));
977
1049
  }
978
1050
 
979
1051
  case 'MX': {
980
- const result = await this.resolveMx(name, abortController);
1052
+ const result = await this.resolveMx(name, options, abortController);
981
1053
  return result.map((r) => ({ type, ...r }));
982
1054
  }
983
1055
 
984
1056
  case 'NAPTR': {
985
- const result = await this.resolveNaptr(name, abortController);
1057
+ const result = await this.resolveNaptr(
1058
+ name,
1059
+ options,
1060
+ abortController
1061
+ );
986
1062
  return result.map((value) => ({ type, value }));
987
1063
  }
988
1064
 
989
1065
  case 'NS': {
990
- const result = await this.resolveNs(name, abortController);
1066
+ const result = await this.resolveNs(name, options, abortController);
991
1067
  return result.map((value) => ({ type, value }));
992
1068
  }
993
1069
 
994
1070
  case 'PTR': {
995
- const result = await this.resolvePtr(name, abortController);
1071
+ const result = await this.resolvePtr(
1072
+ name,
1073
+ options,
1074
+ abortController
1075
+ );
996
1076
  return result.map((value) => ({ type, value }));
997
1077
  }
998
1078
 
999
1079
  case 'SOA': {
1000
- const result = await this.resolveSoa(name, abortController);
1080
+ const result = await this.resolveSoa(
1081
+ name,
1082
+ options,
1083
+ abortController
1084
+ );
1001
1085
  return { type, ...result };
1002
1086
  }
1003
1087
 
1004
1088
  case 'SRV': {
1005
- const result = await this.resolveSrv(name, abortController);
1089
+ const result = await this.resolveSrv(
1090
+ name,
1091
+ options,
1092
+ abortController
1093
+ );
1006
1094
  return result.map((value) => ({ type, value }));
1007
1095
  }
1008
1096
 
1009
1097
  case 'TXT': {
1010
- const result = await this.resolveTxt(name, abortController);
1098
+ const result = await this.resolveTxt(
1099
+ name,
1100
+ options,
1101
+ abortController
1102
+ );
1011
1103
  return result.map((entries) => ({ type, entries }));
1012
1104
  }
1013
1105
 
@@ -1025,7 +1117,7 @@ class Tangerine extends dns.promises.Resolver {
1025
1117
  }
1026
1118
 
1027
1119
  // <https://nodejs.org/api/dns.html#dnspromisesresolveanyhostname>
1028
- async resolveAny(name, abortController) {
1120
+ async resolveAny(name, options = {}, abortController) {
1029
1121
  if (typeof name !== 'string') {
1030
1122
  const err = new TypeError('The "name" argument must be of type string.');
1031
1123
  err.code = 'ERR_INVALID_ARG_TYPE';
@@ -1056,7 +1148,7 @@ class Tangerine extends dns.promises.Resolver {
1056
1148
 
1057
1149
  const results = await pMap(
1058
1150
  this.constructor.ANY_TYPES,
1059
- this.#resolveByType(name, abortController),
1151
+ this.#resolveByType(name, options, abortController),
1060
1152
  // <https://developers.cloudflare.com/fundamentals/api/reference/limits/>
1061
1153
  { concurrency: this.options.concurrency, signal: abortController.signal }
1062
1154
  );
@@ -1114,9 +1206,16 @@ class Tangerine extends dns.promises.Resolver {
1114
1206
  throw err;
1115
1207
  }
1116
1208
 
1209
+ // purge cache support
1210
+ let purgeCache;
1211
+ if (options?.purgeCache) {
1212
+ purgeCache = true;
1213
+ delete options.purgeCache;
1214
+ }
1215
+
1117
1216
  // ecsSubnet support
1118
1217
  let ecsSubnet;
1119
- if (options.ecsSubnet) {
1218
+ if (options?.ecsSubnet) {
1120
1219
  ecsSubnet = options.ecsSubnet;
1121
1220
  delete options.ecsSubnet;
1122
1221
  }
@@ -1127,7 +1226,7 @@ class Tangerine extends dns.promises.Resolver {
1127
1226
 
1128
1227
  let result;
1129
1228
  let data;
1130
- if (this.options.cache) {
1229
+ if (this.options.cache && !purgeCache) {
1131
1230
  //
1132
1231
  // NOTE: we store `result.ttl` which was the lowest TTL determined
1133
1232
  // (this saves us from duplicating the same `...sort().filter(Number.isFinite)` logic)
@@ -1309,7 +1408,8 @@ class Tangerine extends dns.promises.Resolver {
1309
1408
  }
1310
1409
 
1311
1410
  // if no results then throw ENODATA
1312
- if (result.answers.length === 0)
1411
+ // (hidden option for `lookup` to prevent errors being thrown)
1412
+ if (result.answers.length === 0 && !options.noThrowOnNODATA)
1313
1413
  throw this.constructor.createError(name, rrtype, dns.NODATA);
1314
1414
 
1315
1415
  // filter the answers for the same type
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tangerine",
3
- "description": "Tangerine is the best Node.js drop-in replacement for dns.promises.Resolver using DNS over HTTPS (\"DoH\") via undici with built-in retries, timeouts, smart server rotation, AbortControllers, and caching support for multiple backends (with TTL support).",
4
- "version": "1.3.0",
3
+ "description": "Tangerine is the best Node.js drop-in replacement for dns.promises.Resolver using DNS over HTTPS (\"DoH\") via undici with built-in retries, timeouts, smart server rotation, AbortControllers, and caching support for multiple backends (with TTL and purge support).",
4
+ "version": "1.4.0",
5
5
  "author": "Forward Email (https://forwardemail.net)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/forwardemail/tangerine/issues"
@@ -20,6 +20,7 @@
20
20
  "p-timeout": "4",
21
21
  "p-wait-for": "3",
22
22
  "port-numbers": "^6.0.1",
23
+ "punycode": "^2.3.0",
23
24
  "semver": "^7.3.8"
24
25
  },
25
26
  "devDependencies": {