tangerine 2.1.1 → 2.1.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.
Files changed (4) hide show
  1. package/README.md +58 -38
  2. package/index.d.ts +31 -1
  3. package/index.js +24 -3
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -237,6 +237,26 @@ tangerine.resolve('forwardemail.net').then(console.log);
237
237
 
238
238
  ### `tangerine.resolve(hostname[, rrtype, options, abortController])`
239
239
 
240
+ Tangerine supports the following additional properties in the `options` Object argument:
241
+
242
+ * `ecsSubnet` (String) - EDNS Client Subnet (ECS) option for geolocation-aware DNS responses.
243
+ * `purgeCache` (Boolean) - If `true`, bypass and refresh the cached result.
244
+ * `dnssecSecure` (Boolean) - If `true`, set the EDNS0 DO (DNSSEC OK) flag in the outgoing DoH query and return a `{ secure, answers }` object instead of the normal result array. The `secure` property is `true` when the upstream resolver (Cloudflare/Google) has validated the response via DNSSEC (i.e. the AD flag is set). This is useful for [RFC 7672 Section 2.2.2](https://datatracker.ietf.org/doc/html/rfc7672#section-2.2.2) DANE implementations that need to check whether an MX host's zone is DNSSEC-signed before attempting TLSA lookups.
245
+
246
+ ```js
247
+ const tangerine = new Tangerine();
248
+
249
+ // Check if a domain's zone is DNSSEC-signed
250
+ const result = await tangerine.resolve('cloudflare.com', 'A', { dnssecSecure: true });
251
+ console.log(result);
252
+ // { secure: true, answers: [{ name: 'cloudflare.com', type: 'A', ... }] }
253
+
254
+ // Non-DNSSEC zone
255
+ const result2 = await tangerine.resolve('google.com', 'A', { dnssecSecure: true });
256
+ console.log(result2);
257
+ // { secure: false, answers: [{ name: 'google.com', type: 'A', ... }] }
258
+ ```
259
+
240
260
  ### `tangerine.resolve4(hostname[, options, abortController])`
241
261
 
242
262
  Tangerine supports a new `ecsSubnet` property in the `options` Object argument.
@@ -534,27 +554,27 @@ We have written extensive benchmarks to show that :tangerine: Tangerine is as fa
534
554
 
535
555
  #### Latest Automated Benchmark Results
536
556
 
537
- **Last Updated:** 2026-03-05
557
+ **Last Updated:** 2026-03-07
538
558
 
539
559
  | Node Version | Platform | Arch | Timestamp |
540
560
  | ------------ | -------- | ---- | ------------ |
541
561
  | v18.20.8 | linux | x64 | Dec 21, 2025 |
542
- | v20.19.6 | linux | x64 | Jan 22, 2026 |
543
- | v20.20.0 | linux | x64 | Feb 25, 2026 |
562
+ | v20.19.6 | linux | x64 | Jan 21, 2026 |
563
+ | v20.20.0 | linux | x64 | Feb 24, 2026 |
544
564
  | v22.21.1 | linux | x64 | Dec 21, 2025 |
545
- | v22.22.0 | linux | x64 | Jan 23, 2026 |
565
+ | v22.22.0 | linux | x64 | Jan 22, 2026 |
546
566
  | v24.12.0 | linux | x64 | Dec 21, 2025 |
547
- | v24.13.0 | linux | x64 | Feb 19, 2026 |
548
- | v24.13.1 | linux | x64 | Mar 4, 2026 |
549
- | v24.14.0 | linux | x64 | Mar 5, 2026 |
567
+ | v24.13.0 | linux | x64 | Feb 18, 2026 |
568
+ | v24.13.1 | linux | x64 | Mar 3, 2026 |
569
+ | v24.14.0 | linux | x64 | Mar 6, 2026 |
550
570
  | v25.2.1 | linux | x64 | Dec 21, 2025 |
551
- | v25.3.0 | linux | x64 | Jan 14, 2026 |
552
- | v25.4.0 | linux | x64 | Jan 20, 2026 |
553
- | v25.5.0 | linux | x64 | Jan 27, 2026 |
554
- | v25.6.0 | linux | x64 | Feb 4, 2026 |
555
- | v25.6.1 | linux | x64 | Feb 11, 2026 |
556
- | v25.7.0 | linux | x64 | Feb 25, 2026 |
557
- | v25.8.0 | linux | x64 | Mar 4, 2026 |
571
+ | v25.3.0 | linux | x64 | Jan 13, 2026 |
572
+ | v25.4.0 | linux | x64 | Jan 19, 2026 |
573
+ | v25.5.0 | linux | x64 | Jan 26, 2026 |
574
+ | v25.6.0 | linux | x64 | Feb 3, 2026 |
575
+ | v25.6.1 | linux | x64 | Feb 10, 2026 |
576
+ | v25.7.0 | linux | x64 | Feb 24, 2026 |
577
+ | v25.8.0 | linux | x64 | Mar 3, 2026 |
558
578
 
559
579
  <details>
560
580
  <summary>Click to expand detailed benchmark results</summary>
@@ -904,12 +924,12 @@ Fastest without caching is: tangerine.reverse GET without caching
904
924
 
905
925
  ```text
906
926
  Started: lookup
907
- tangerine.lookup POST with caching using Cloudflare x 1,403 ops/sec ±195.16% (90 runs sampled)
908
- tangerine.lookup POST without caching using Cloudflare x 70.67 ops/sec ±3.34% (84 runs sampled)
909
- tangerine.lookup GET with caching using Cloudflare x 317,797 ops/sec ±0.25% (91 runs sampled)
910
- tangerine.lookup GET without caching using Cloudflare x 63.36 ops/sec ±6.51% (78 runs sampled)
911
- dns.promises.lookup with caching using Cloudflare x 9,932,885 ops/sec ±1.21% (87 runs sampled)
912
- dns.promises.lookup without caching using Cloudflare x 2,310 ops/sec ±0.39% (89 runs sampled)
927
+ tangerine.lookup POST with caching using Cloudflare x 1,326 ops/sec ±195.20% (85 runs sampled)
928
+ tangerine.lookup POST without caching using Cloudflare x 59.26 ops/sec ±6.44% (74 runs sampled)
929
+ tangerine.lookup GET with caching using Cloudflare x 314,095 ops/sec ±0.32% (90 runs sampled)
930
+ tangerine.lookup GET without caching using Cloudflare x 63.80 ops/sec ±2.69% (77 runs sampled)
931
+ dns.promises.lookup with caching using Cloudflare x 10,223,830 ops/sec ±0.86% (85 runs sampled)
932
+ dns.promises.lookup without caching using Cloudflare x 2,175 ops/sec ±0.84% (87 runs sampled)
913
933
  Fastest without caching is: dns.promises.lookup without caching using Cloudflare
914
934
  ```
915
935
 
@@ -917,30 +937,30 @@ Fastest without caching is: dns.promises.lookup without caching using Cloudflare
917
937
 
918
938
  ```text
919
939
  Started: resolve
920
- tangerine.resolve POST with caching using Cloudflare x 1,269 ops/sec ±195.78% (89 runs sampled)
921
- tangerine.resolve POST without caching using Cloudflare x 85.54 ops/sec ±0.68% (81 runs sampled)
922
- tangerine.resolve GET with caching using Cloudflare x 1,117,794 ops/sec ±0.40% (90 runs sampled)
923
- tangerine.resolve GET without caching using Cloudflare x 69.12 ops/sec ±5.91% (84 runs sampled)
924
- tangerine.resolve POST with caching using Google x 1,675 ops/sec ±195.71% (90 runs sampled)
925
- tangerine.resolve POST without caching using Google x 57.66 ops/sec ±13.41% (79 runs sampled)
926
- tangerine.resolve GET with caching using Google x 1,117,350 ops/sec ±0.62% (90 runs sampled)
927
- tangerine.resolve GET without caching using Google x 56.23 ops/sec ±5.75% (72 runs sampled)
928
- resolver.resolve with caching using Cloudflare x 8,740,674 ops/sec ±0.82% (83 runs sampled)
929
- resolver.resolve without caching using Cloudflare x 71.31 ops/sec ±1.31% (72 runs sampled)
930
- Fastest without caching is: tangerine.resolve POST without caching using Cloudflare
940
+ tangerine.resolve POST with caching using Cloudflare x 1,293 ops/sec ±195.78% (88 runs sampled)
941
+ tangerine.resolve POST without caching using Cloudflare x 70.18 ops/sec ±3.02% (84 runs sampled)
942
+ tangerine.resolve GET with caching using Cloudflare x 1,118,050 ops/sec ±0.39% (90 runs sampled)
943
+ tangerine.resolve GET without caching using Cloudflare x 62.96 ops/sec ±2.78% (76 runs sampled)
944
+ tangerine.resolve POST with caching using Google x 1,652 ops/sec ±195.71% (90 runs sampled)
945
+ tangerine.resolve POST without caching using Google x 58.58 ops/sec ±8.14% (78 runs sampled)
946
+ tangerine.resolve GET with caching using Google x 1,103,516 ops/sec ±1.21% (87 runs sampled)
947
+ tangerine.resolve GET without caching using Google x 61.12 ops/sec ±3.21% (75 runs sampled)
948
+ resolver.resolve with caching using Cloudflare x 8,524,784 ops/sec ±0.95% (87 runs sampled)
949
+ resolver.resolve without caching using Cloudflare x 70.34 ops/sec ±1.22% (68 runs sampled)
950
+ Fastest without caching is: resolver.resolve without caching using Cloudflare
931
951
  ```
932
952
 
933
953
  **reverse:**
934
954
 
935
955
  ```text
936
956
  Started: reverse
937
- tangerine.reverse GET with caching x 1,286 ops/sec ±195.26% (90 runs sampled)
938
- tangerine.reverse GET without caching x 85.95 ops/sec ±0.80% (81 runs sampled)
939
- resolver.reverse with caching x 8,753,410 ops/sec ±0.98% (85 runs sampled)
940
- resolver.reverse without caching x 72.25 ops/sec ±1.42% (74 runs sampled)
941
- dns.promises.reverse with caching x 8,707,156 ops/sec ±0.92% (84 runs sampled)
942
- dns.promises.reverse without caching x 71.90 ops/sec ±1.46% (70 runs sampled)
943
- Fastest without caching is: tangerine.reverse GET without caching
957
+ tangerine.reverse GET with caching x 1,298 ops/sec ±195.25% (90 runs sampled)
958
+ tangerine.reverse GET without caching x 70.67 ops/sec ±2.91% (84 runs sampled)
959
+ resolver.reverse with caching x 8,799,672 ops/sec ±0.94% (85 runs sampled)
960
+ resolver.reverse without caching x 71.99 ops/sec ±1.45% (70 runs sampled)
961
+ dns.promises.reverse with caching x 8,834,313 ops/sec ±0.77% (85 runs sampled)
962
+ dns.promises.reverse without caching x 70.74 ops/sec ±1.11% (70 runs sampled)
963
+ Fastest without caching is: resolver.reverse without caching, dns.promises.reverse without caching, tangerine.reverse GET without caching
944
964
  ```
945
965
 
946
966
  ##### Node.js v25.2.1
package/index.d.ts CHANGED
@@ -326,6 +326,35 @@ export type AnyRecord = {
326
326
 
327
327
  export type ResolveOptions = {
328
328
  ttl?: boolean;
329
+ /**
330
+ * If true, set the EDNS0 DO (DNSSEC OK) flag in the outgoing DoH query
331
+ * and return a `{ secure, answers }` object instead of the normal result.
332
+ * The `secure` property is true when the upstream resolver has validated
333
+ * the response via DNSSEC (i.e. the AD flag is set).
334
+ */
335
+ dnssecSecure?: boolean;
336
+ /**
337
+ * EDNS Client Subnet (ECS) option for geolocation-aware DNS responses.
338
+ */
339
+ ecsSubnet?: string;
340
+ /**
341
+ * If true, bypass and refresh the cached result.
342
+ */
343
+ purgeCache?: boolean;
344
+ };
345
+
346
+ export type DnssecResult = {
347
+ /** Whether the DNS response was DNSSEC-validated (AD flag set). */
348
+ secure: boolean;
349
+ /** The DNS answer records from the response. */
350
+ answers: Array<{
351
+ name: string;
352
+ type: string;
353
+ ttl: number;
354
+ class: string;
355
+ flush: boolean;
356
+ data: unknown;
357
+ }>;
329
358
  };
330
359
 
331
360
  export type RecordWithTtl = {
@@ -647,13 +676,14 @@ declare class Tangerine extends Resolver {
647
676
 
648
677
  /**
649
678
  * Resolve DNS records of a specific type.
679
+ * When `options.dnssecSecure` is true, returns `DnssecResult` instead.
650
680
  */
651
681
  resolve(
652
682
  name: string,
653
683
  rrtype?: DnsRecordType,
654
684
  options?: ResolveOptions,
655
685
  abortController?: AbortController
656
- ): Promise<unknown>;
686
+ ): Promise<unknown | DnssecResult>;
657
687
  }
658
688
 
659
689
  export default Tangerine;
package/index.js CHANGED
@@ -1141,13 +1141,14 @@ class Tangerine extends dns.promises.Resolver {
1141
1141
 
1142
1142
  // <https://github.com/hildjj/dohdec/tree/main/pkg/dohdec>
1143
1143
 
1144
- async #query(name, rrtype = 'A', ecsSubnet, abortController) {
1144
+ async #query(name, rrtype = 'A', ecsSubnet, abortController, dnssec) {
1145
1145
  if (!dohdec) await pWaitFor(() => Boolean(dohdec));
1146
1146
  debug('query', {
1147
1147
  name,
1148
1148
  nameToASCII: toASCII(name),
1149
1149
  rrtype,
1150
1150
  ecsSubnet,
1151
+ dnssec,
1151
1152
  abortController
1152
1153
  });
1153
1154
  // <https://github.com/hildjj/dohdec/blob/43564118c40f2127af871bdb4d40f615409d4b9c/pkg/dohdec/lib/dnsUtils.js#L161>
@@ -1160,7 +1161,13 @@ class Tangerine extends dns.promises.Resolver {
1160
1161
  // mirrors dns module behavior
1161
1162
  name: toASCII(name),
1162
1163
  // <https://github.com/mafintosh/dns-packet/pull/47#issuecomment-1435818437>
1163
- ecsSubnet
1164
+ ecsSubnet,
1165
+ // When dnssec is true, set the AD flag in the query and the DO
1166
+ // (DNSSEC OK) flag in the EDNS0 OPT record so the upstream resolver
1167
+ // returns DNSSEC validation status via the AD flag in the response.
1168
+ // <https://datatracker.ietf.org/doc/html/rfc3225>
1169
+ // <https://datatracker.ietf.org/doc/html/rfc4035#section-3.2.1>
1170
+ dnssec
1164
1171
  });
1165
1172
  try {
1166
1173
  // mirror the behavior as noted in built-in DNS
@@ -1824,6 +1831,12 @@ class Tangerine extends dns.promises.Resolver {
1824
1831
  delete options.ecsSubnet;
1825
1832
  }
1826
1833
 
1834
+ // dnssecSecure support: set the EDNS0 DO (DNSSEC OK) flag in the
1835
+ // outgoing DoH query so the upstream resolver returns the AD flag.
1836
+ // NOTE: do not delete options.dnssecSecure here — it is checked
1837
+ // after #query() to decide the return format.
1838
+ const dnssec = Boolean(options?.dnssecSecure);
1839
+
1827
1840
  const key = (
1828
1841
  ecsSubnet ? `${rrtype}:${ecsSubnet}:${name}` : `${rrtype}:${name}`
1829
1842
  ).toLowerCase();
@@ -1932,7 +1945,13 @@ class Tangerine extends dns.promises.Resolver {
1932
1945
 
1933
1946
  try {
1934
1947
  // setImmediate(() => this.cancel());
1935
- result = await this.#query(name, rrtype, ecsSubnet, abortController);
1948
+ result = await this.#query(
1949
+ name,
1950
+ rrtype,
1951
+ ecsSubnet,
1952
+ abortController,
1953
+ dnssec
1954
+ );
1936
1955
  } finally {
1937
1956
  if (mustReleaseAbortController) {
1938
1957
  this.#releaseAbortController(abortController);
@@ -2032,6 +2051,8 @@ class Tangerine extends dns.promises.Resolver {
2032
2051
  // RFC 7672 Section 2.2.2: Expose DNSSEC validation status (AD flag)
2033
2052
  // When options.dnssecSecure is true, return an object with the answers
2034
2053
  // and a boolean indicating whether the response was DNSSEC-validated.
2054
+ // The AD flag is set by the upstream DoH resolver (Cloudflare/Google)
2055
+ // when the zone is DNSSEC-signed and validation succeeds.
2035
2056
  // This allows callers (e.g. mx-connect DANE) to check if the MX host's
2036
2057
  // zone is signed before attempting TLSA lookups.
2037
2058
  //
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tangerine",
3
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": "2.1.1",
4
+ "version": "2.1.2",
5
5
  "author": "Forward Email (https://forwardemail.net)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/forwardemail/nodejs-dns-over-https-tangerine/issues"