tangerine 2.1.0 → 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 +147 -35
  2. package/index.d.ts +31 -1
  3. package/index.js +38 -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,25 +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-02-26
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 | Feb 26, 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 |
549
570
  | v25.2.1 | linux | x64 | Dec 21, 2025 |
550
- | v25.3.0 | linux | x64 | Jan 14, 2026 |
551
- | v25.4.0 | linux | x64 | Jan 20, 2026 |
552
- | v25.5.0 | linux | x64 | Jan 27, 2026 |
553
- | v25.6.0 | linux | x64 | Feb 4, 2026 |
554
- | v25.6.1 | linux | x64 | Feb 11, 2026 |
555
- | v25.7.0 | linux | x64 | Feb 25, 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 |
556
578
 
557
579
  <details>
558
580
  <summary>Click to expand detailed benchmark results</summary>
@@ -857,12 +879,12 @@ Fastest without caching is: tangerine.reverse GET without caching
857
879
 
858
880
  ```text
859
881
  Started: lookup
860
- tangerine.lookup POST with caching using Cloudflare x 328,178 ops/sec ±1.46% (90 runs sampled)
861
- tangerine.lookup POST without caching using Cloudflare x 263 ops/sec ±1.89% (78 runs sampled)
862
- tangerine.lookup GET with caching using Cloudflare x 315,952 ops/sec ±0.27% (90 runs sampled)
863
- tangerine.lookup GET without caching using Cloudflare x 227 ops/sec ±3.20% (78 runs sampled)
864
- dns.promises.lookup with caching using Cloudflare x 1,047 ops/sec ±195.98% (80 runs sampled)
865
- dns.promises.lookup without caching using Cloudflare x 2,273 ops/sec ±0.36% (86 runs sampled)
882
+ tangerine.lookup POST with caching using Cloudflare x 333,315 ops/sec ±1.61% (85 runs sampled)
883
+ tangerine.lookup POST without caching using Cloudflare x 227 ops/sec ±1.93% (82 runs sampled)
884
+ tangerine.lookup GET with caching using Cloudflare x 320,901 ops/sec ±0.68% (90 runs sampled)
885
+ tangerine.lookup GET without caching using Cloudflare x 262 ops/sec ±2.60% (81 runs sampled)
886
+ dns.promises.lookup with caching using Cloudflare x 10,104,926 ops/sec ±1.20% (84 runs sampled)
887
+ dns.promises.lookup without caching using Cloudflare x 2,172 ops/sec ±0.39% (86 runs sampled)
866
888
  Fastest without caching is: dns.promises.lookup without caching using Cloudflare
867
889
  ```
868
890
 
@@ -870,32 +892,77 @@ Fastest without caching is: dns.promises.lookup without caching using Cloudflare
870
892
 
871
893
  ```text
872
894
  Started: resolve
873
- tangerine.resolve POST with caching using Cloudflare x 1,146,234 ops/sec ±0.38% (88 runs sampled)
874
- tangerine.resolve POST without caching using Cloudflare x 228 ops/sec ±1.86% (83 runs sampled)
875
- tangerine.resolve GET with caching using Cloudflare x 1,121,526 ops/sec ±0.51% (87 runs sampled)
876
- tangerine.resolve GET without caching using Cloudflare x 293 ops/sec ±1.77% (77 runs sampled)
877
- tangerine.resolve POST with caching using Google x 1,116,293 ops/sec ±0.94% (87 runs sampled)
878
- tangerine.resolve POST without caching using Google x 262 ops/sec ±4.45% (83 runs sampled)
879
- tangerine.resolve GET with caching using Google x 1,127,465 ops/sec ±0.31% (90 runs sampled)
880
- tangerine.resolve GET without caching using Google x 265 ops/sec ±1.09% (83 runs sampled)
881
- resolver.resolve with caching using Cloudflare x 8,518,907 ops/sec ±1.01% (86 runs sampled)
882
- resolver.resolve without caching using Cloudflare x 82.99 ops/sec ±142.66% (36 runs sampled)
883
- Fastest without caching is: tangerine.resolve GET without caching using Cloudflare
895
+ tangerine.resolve POST with caching using Cloudflare x 1,155,993 ops/sec ±0.68% (89 runs sampled)
896
+ tangerine.resolve POST without caching using Cloudflare x 238 ops/sec ±1.44% (82 runs sampled)
897
+ tangerine.resolve GET with caching using Cloudflare x 1,130,246 ops/sec ±0.43% (89 runs sampled)
898
+ tangerine.resolve GET without caching using Cloudflare x 271 ops/sec ±1.13% (85 runs sampled)
899
+ tangerine.resolve POST with caching using Google x 534 ops/sec ±195.91% (89 runs sampled)
900
+ tangerine.resolve POST without caching using Google x 180 ops/sec ±23.75% (71 runs sampled)
901
+ tangerine.resolve GET with caching using Google x 1,127,220 ops/sec ±0.26% (91 runs sampled)
902
+ tangerine.resolve GET without caching using Google x 194 ops/sec ±16.84% (63 runs sampled)
903
+ resolver.resolve with caching using Cloudflare x 8,475,317 ops/sec ±0.93% (83 runs sampled)
904
+ resolver.resolve without caching using Cloudflare x 345 ops/sec ±1.00% (78 runs sampled)
905
+ Fastest without caching is: resolver.resolve without caching using Cloudflare
884
906
  ```
885
907
 
886
908
  **reverse:**
887
909
 
888
910
  ```text
889
911
  Started: reverse
890
- tangerine.reverse GET with caching x 337,325 ops/sec ±0.52% (89 runs sampled)
891
- tangerine.reverse GET without caching x 233 ops/sec ±2.59% (79 runs sampled)
892
- resolver.reverse with caching x 17.19 ops/sec ±196.00% (86 runs sampled)
893
- resolver.reverse without caching x 6.84 ops/sec ±154.89% (71 runs sampled)
894
- dns.promises.reverse with caching x 8,315,699 ops/sec ±1.33% (78 runs sampled)
895
- dns.promises.reverse without caching x 0.77 ops/sec ±164.54% (37 runs sampled)
912
+ tangerine.reverse GET with caching x 331,840 ops/sec ±0.57% (89 runs sampled)
913
+ tangerine.reverse GET without caching x 241 ops/sec ±1.30% (80 runs sampled)
914
+ resolver.reverse with caching x 8,769,670 ops/sec ±0.60% (87 runs sampled)
915
+ resolver.reverse without caching x 24.95 ops/sec ±193.07% (22 runs sampled)
916
+ dns.promises.reverse with caching x 8,895,968 ops/sec ±0.71% (88 runs sampled)
917
+ dns.promises.reverse without caching x 0.05 ops/sec ±112.55% (5 runs sampled)
896
918
  Fastest without caching is: tangerine.reverse GET without caching
897
919
  ```
898
920
 
921
+ ##### Node.js v24.14.0
922
+
923
+ **lookup:**
924
+
925
+ ```text
926
+ Started: lookup
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)
933
+ Fastest without caching is: dns.promises.lookup without caching using Cloudflare
934
+ ```
935
+
936
+ **resolve:**
937
+
938
+ ```text
939
+ Started: resolve
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
951
+ ```
952
+
953
+ **reverse:**
954
+
955
+ ```text
956
+ Started: reverse
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
964
+ ```
965
+
899
966
  ##### Node.js v25.2.1
900
967
 
901
968
  **lookup:**
@@ -1211,6 +1278,51 @@ dns.promises.reverse without caching x 67.14 ops/sec ±0.80% (79 runs sampled)
1211
1278
  Fastest without caching is: dns.promises.reverse without caching, resolver.reverse without caching
1212
1279
  ```
1213
1280
 
1281
+ ##### Node.js v25.8.0
1282
+
1283
+ **lookup:**
1284
+
1285
+ ```text
1286
+ Started: lookup
1287
+ tangerine.lookup POST with caching using Cloudflare x 348,234 ops/sec ±1.43% (89 runs sampled)
1288
+ tangerine.lookup POST without caching using Cloudflare x 245 ops/sec ±1.75% (83 runs sampled)
1289
+ tangerine.lookup GET with caching using Cloudflare x 340,115 ops/sec ±0.72% (89 runs sampled)
1290
+ tangerine.lookup GET without caching using Cloudflare x 223 ops/sec ±2.25% (81 runs sampled)
1291
+ dns.promises.lookup with caching using Cloudflare x 1,759 ops/sec ±195.97% (88 runs sampled)
1292
+ dns.promises.lookup without caching using Cloudflare x 2,223 ops/sec ±0.55% (87 runs sampled)
1293
+ Fastest without caching is: dns.promises.lookup without caching using Cloudflare
1294
+ ```
1295
+
1296
+ **resolve:**
1297
+
1298
+ ```text
1299
+ Started: resolve
1300
+ tangerine.resolve POST with caching using Cloudflare x 1,224,642 ops/sec ±0.58% (87 runs sampled)
1301
+ tangerine.resolve POST without caching using Cloudflare x 248 ops/sec ±1.54% (83 runs sampled)
1302
+ tangerine.resolve GET with caching using Cloudflare x 1,168,649 ops/sec ±0.55% (85 runs sampled)
1303
+ tangerine.resolve GET without caching using Cloudflare x 264 ops/sec ±1.04% (82 runs sampled)
1304
+ tangerine.resolve POST with caching using Google x 1,411 ops/sec ±195.76% (87 runs sampled)
1305
+ tangerine.resolve POST without caching using Google x 178 ops/sec ±24.83% (67 runs sampled)
1306
+ tangerine.resolve GET with caching using Google x 1,188,395 ops/sec ±0.40% (88 runs sampled)
1307
+ tangerine.resolve GET without caching using Google x 237 ops/sec ±14.93% (74 runs sampled)
1308
+ resolver.resolve with caching using Cloudflare x 8.39 ops/sec ±196.00% (84 runs sampled)
1309
+ resolver.resolve without caching using Cloudflare x 109 ops/sec ±113.88% (68 runs sampled)
1310
+ Fastest without caching is: tangerine.resolve GET without caching using Cloudflare, tangerine.resolve POST without caching using Google
1311
+ ```
1312
+
1313
+ **reverse:**
1314
+
1315
+ ```text
1316
+ Started: reverse
1317
+ tangerine.reverse GET with caching x 346,448 ops/sec ±4.35% (86 runs sampled)
1318
+ tangerine.reverse GET without caching x 259 ops/sec ±1.42% (83 runs sampled)
1319
+ resolver.reverse with caching x 9,199,217 ops/sec ±0.52% (88 runs sampled)
1320
+ resolver.reverse without caching x 13.67 ops/sec ±188.06% (45 runs sampled)
1321
+ dns.promises.reverse with caching x 9,227,637 ops/sec ±0.33% (89 runs sampled)
1322
+ dns.promises.reverse without caching x 0.07 ops/sec ±133.35% (5 runs sampled)
1323
+ Fastest without caching is: tangerine.reverse GET without caching
1324
+ ```
1325
+
1214
1326
  </details>
1215
1327
 
1216
1328
  <!-- BENCHMARK_RESULTS_END -->
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);
@@ -2028,6 +2047,22 @@ class Tangerine extends dns.promises.Resolver {
2028
2047
  if (result.answers.length === 0 && !options.noThrowOnNODATA)
2029
2048
  throw this.constructor.createError(name, rrtype, dns.NODATA);
2030
2049
 
2050
+ //
2051
+ // RFC 7672 Section 2.2.2: Expose DNSSEC validation status (AD flag)
2052
+ // When options.dnssecSecure is true, return an object with the answers
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.
2056
+ // This allows callers (e.g. mx-connect DANE) to check if the MX host's
2057
+ // zone is signed before attempting TLSA lookups.
2058
+ //
2059
+ if (options?.dnssecSecure) {
2060
+ return {
2061
+ secure: Boolean(result.flag_ad),
2062
+ answers: result.answers
2063
+ };
2064
+ }
2065
+
2031
2066
  // filter the answers for the same type
2032
2067
  result.answers = result.answers.filter((answer) => answer.type === rrtype);
2033
2068
 
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.0",
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"