tangerine 2.0.0 → 2.0.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 +17 -17
  2. package/index.d.ts +653 -0
  3. package/index.js +170 -1
  4. package/package.json +5 -3
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
  </div>
16
16
  <hr />
17
17
  <div align="center">
18
- ⚡ <a href="#tangerine-benchmarks"><i><u><strong>AS FAST AS</strong></u></i></a> native <a href="https://nodejs.org/api/dns.html" target="_blank">Node.js <code>dns</code></a>! 🚀 &bull; Supports Node v17+ with ESM/CJS &bull; Made for <a href="https://forwardemail.net" target="_blank"><strong>Forward Email</strong></a>.
18
+ ⚡ <a href="#tangerine-benchmarks"><i><u><strong>AS FAST AS</strong></u></i></a> native <a href="https://nodejs.org/api/dns.html" target="_blank">Node.js <code>dns</code></a>! 🚀 &bull; Supports Node v18+ with ESM/CJS &bull; Made for <a href="https://forwardemail.net" target="_blank"><strong>Forward Email</strong></a>.
19
19
  </div>
20
20
  <hr />
21
21
 
@@ -383,7 +383,7 @@ Similar to the `options` argument from `new dns.promises.Resolver(options)` invo
383
383
  | `requestOptions.method` | `String` | Defaults to `"GET"` (must be either `"GET"` or `"POST"`, case-insensitive depending on library you use). | Default HTTP method to use for DNS over HTTP ("DoH") requests. |
384
384
  | `requestOptions.headers` | `Object` | Defaults to `{ 'content-type': 'application/dns-message', 'user-agent': pkg.name + "/" + pkg.version, accept: 'application/dns-message' }`. | Default HTTP headers to use for DNS over HTTP ("DoH") requests. |
385
385
  | `protocol` | `String` | Defaults to `"https"`. | Default HTTP protocol to use for DNS over HTTPS ("DoH") requests. |
386
- | `dnsOrder` | `String` | Defaults to `"verbatim"` for Node.js v17.0.0+ and `"ipv4first"` for older versions. | Sets the default result order of `lookup` invocations (see [dns.setDefaultResultOrder](https://nodejs.org/api/dns.html#dnssetdefaultresultorderorder) for more insight). |
386
+ | `dnsOrder` | `String` | Defaults to `"verbatim"` for Node.js v18.0.0+ and `"ipv4first"` for older versions. | Sets the default result order of `lookup` invocations (see [dns.setDefaultResultOrder](https://nodejs.org/api/dns.html#dnssetdefaultresultorderorder) for more insight). |
387
387
  | `logger` | `Object` | `false` | This is the default logger. We recommend using [Cabin](https://github.com/cabinjs) instead of using `console` as your default logger. Set this value to `false` to disable logging entirely (uses noop function). |
388
388
  | `id` | `Number` or `Function` | `0` | Default `id` to be passed for DNS packet creation. This could alternatively be a synchronous or asynchronous function that returns a `Number` (e.g. `id: () => Tangerine.getRandomInt(1, 65534)`). |
389
389
  | `concurrency` | `Number` | `os.cpus().length` | Default concurrency to use for `resolveAny` lookup via [p-map](https://github.com/sindresorhus/p-map). The default value is the number of CPU's available to the system using the Node.js `os` module [os.cpus()](https://nodejs.org/api/os.html#oscpus) method. |
@@ -550,7 +550,7 @@ We have written extensive benchmarks to show that :tangerine: Tangerine is as fa
550
550
 
551
551
  **lookup:**
552
552
 
553
- ```
553
+ ```text
554
554
  Started: lookup
555
555
  tangerine.lookup POST with caching using Cloudflare x 757 ops/sec ±195.51% (88 runs sampled)
556
556
  tangerine.lookup POST without caching using Cloudflare x 120 ops/sec ±1.43% (81 runs sampled)
@@ -563,7 +563,7 @@ Fastest without caching is: dns.promises.lookup without caching using Cloudflare
563
563
 
564
564
  **resolve:**
565
565
 
566
- ```
566
+ ```text
567
567
  Started: resolve
568
568
  tangerine.resolve POST with caching using Cloudflare x 953 ops/sec ±195.82% (88 runs sampled)
569
569
  tangerine.resolve POST without caching using Cloudflare x 116 ops/sec ±1.16% (80 runs sampled)
@@ -580,7 +580,7 @@ Fastest without caching is: tangerine.resolve POST without caching using Google
580
580
 
581
581
  **reverse:**
582
582
 
583
- ```
583
+ ```text
584
584
  Started: reverse
585
585
  tangerine.reverse GET with caching x 628 ops/sec ±195.51% (84 runs sampled)
586
586
  tangerine.reverse GET without caching x 118 ops/sec ±1.12% (80 runs sampled)
@@ -595,7 +595,7 @@ Fastest without caching is: dns.promises.reverse without caching
595
595
 
596
596
  **lookup:**
597
597
 
598
- ```
598
+ ```text
599
599
  Started: lookup
600
600
  tangerine.lookup POST with caching using Cloudflare x 795 ops/sec ±195.51% (87 runs sampled)
601
601
  tangerine.lookup POST without caching using Cloudflare x 306 ops/sec ±1.81% (83 runs sampled)
@@ -608,7 +608,7 @@ Fastest without caching is: dns.promises.lookup without caching using Cloudflare
608
608
 
609
609
  **resolve:**
610
610
 
611
- ```
611
+ ```text
612
612
  Started: resolve
613
613
  tangerine.resolve POST with caching using Cloudflare x 1,016 ops/sec ±195.82% (89 runs sampled)
614
614
  tangerine.resolve POST without caching using Cloudflare x 303 ops/sec ±3.25% (79 runs sampled)
@@ -625,7 +625,7 @@ Fastest without caching is: tangerine.resolve GET without caching using Google
625
625
 
626
626
  **reverse:**
627
627
 
628
- ```
628
+ ```text
629
629
  Started: reverse
630
630
  tangerine.reverse GET with caching x 1,002 ops/sec ±195.36% (87 runs sampled)
631
631
  tangerine.reverse GET without caching x 323 ops/sec ±1.36% (85 runs sampled)
@@ -640,7 +640,7 @@ Fastest without caching is: tangerine.reverse GET without caching
640
640
 
641
641
  **lookup:**
642
642
 
643
- ```
643
+ ```text
644
644
  Started: lookup
645
645
  tangerine.lookup POST with caching using Cloudflare x 330,006 ops/sec ±7.57% (90 runs sampled)
646
646
  tangerine.lookup POST without caching using Cloudflare x 287 ops/sec ±1.96% (84 runs sampled)
@@ -653,7 +653,7 @@ Fastest without caching is: dns.promises.lookup without caching using Cloudflare
653
653
 
654
654
  **resolve:**
655
655
 
656
- ```
656
+ ```text
657
657
  Started: resolve
658
658
  tangerine.resolve POST with caching using Cloudflare x 1,150,690 ops/sec ±0.43% (90 runs sampled)
659
659
  tangerine.resolve POST without caching using Cloudflare x 284 ops/sec ±1.36% (82 runs sampled)
@@ -670,7 +670,7 @@ Fastest without caching is: tangerine.resolve GET without caching using Google
670
670
 
671
671
  **reverse:**
672
672
 
673
- ```
673
+ ```text
674
674
  spawnSync /bin/sh ETIMEDOUT
675
675
  ```
676
676
 
@@ -678,7 +678,7 @@ spawnSync /bin/sh ETIMEDOUT
678
678
 
679
679
  **lookup:**
680
680
 
681
- ```
681
+ ```text
682
682
  Started: lookup
683
683
  tangerine.lookup POST with caching using Cloudflare x 1,775 ops/sec ±194.98% (90 runs sampled)
684
684
  tangerine.lookup POST without caching using Cloudflare x 295 ops/sec ±10.41% (81 runs sampled)
@@ -691,7 +691,7 @@ Fastest without caching is: dns.promises.lookup without caching using Cloudflare
691
691
 
692
692
  **resolve:**
693
693
 
694
- ```
694
+ ```text
695
695
  Started: resolve
696
696
  tangerine.resolve POST with caching using Cloudflare x 1,164,729 ops/sec ±0.27% (90 runs sampled)
697
697
  tangerine.resolve POST without caching using Cloudflare x 316 ops/sec ±1.55% (82 runs sampled)
@@ -708,7 +708,7 @@ Fastest without caching is: tangerine.resolve GET without caching using Google
708
708
 
709
709
  **reverse:**
710
710
 
711
- ```
711
+ ```text
712
712
  spawnSync /bin/sh ETIMEDOUT
713
713
  ```
714
714
 
@@ -716,7 +716,7 @@ spawnSync /bin/sh ETIMEDOUT
716
716
 
717
717
  **lookup:**
718
718
 
719
- ```
719
+ ```text
720
720
  Started: lookup
721
721
  tangerine.lookup POST with caching using Cloudflare x 1,504 ops/sec ±195.19% (89 runs sampled)
722
722
  tangerine.lookup POST without caching using Cloudflare x 118 ops/sec ±2.46% (81 runs sampled)
@@ -729,7 +729,7 @@ Fastest without caching is: dns.promises.lookup without caching using Cloudflare
729
729
 
730
730
  **resolve:**
731
731
 
732
- ```
732
+ ```text
733
733
  Started: resolve
734
734
  tangerine.resolve POST with caching using Cloudflare x 1,200,168 ops/sec ±1.70% (90 runs sampled)
735
735
  tangerine.resolve POST without caching using Cloudflare x 132 ops/sec ±0.46% (88 runs sampled)
@@ -746,7 +746,7 @@ Fastest without caching is: resolver.resolve without caching using Cloudflare
746
746
 
747
747
  **reverse:**
748
748
 
749
- ```
749
+ ```text
750
750
  Started: reverse
751
751
  tangerine.reverse GET with caching x 342,190 ops/sec ±8.40% (90 runs sampled)
752
752
  tangerine.reverse GET without caching x 128 ops/sec ±0.83% (86 runs sampled)
package/index.d.ts ADDED
@@ -0,0 +1,653 @@
1
+ // Type definitions for tangerine
2
+ // Project: https://github.com/forwardemail/tangerine
3
+ // Definitions by: Forward Email <https://forwardemail.net>
4
+
5
+ /* eslint-disable @typescript-eslint/naming-convention */
6
+ /* eslint-disable @typescript-eslint/member-ordering */
7
+
8
+ import { Resolver } from 'node:dns/promises';
9
+ import type { LookupAddress, LookupOptions } from 'node:dns';
10
+
11
+ export type TangerineOptions = {
12
+ /**
13
+ * Timeout in milliseconds for DNS queries.
14
+ * @default 5000
15
+ */
16
+ timeout?: number;
17
+
18
+ /**
19
+ * Number of retry attempts for DNS queries.
20
+ * @default 4
21
+ */
22
+ tries?: number;
23
+
24
+ /**
25
+ * DNS-over-HTTPS servers to use.
26
+ * @default new Set(['1.1.1.1', '1.0.0.1'])
27
+ */
28
+ servers?: Set<string> | string[];
29
+
30
+ /**
31
+ * Request options passed to the HTTP client.
32
+ */
33
+ requestOptions?: {
34
+ method?: 'GET' | 'POST' | 'get' | 'post';
35
+ headers?: Record<string, string>;
36
+ };
37
+
38
+ /**
39
+ * Protocol to use for DNS-over-HTTPS requests.
40
+ * @default 'https'
41
+ */
42
+ protocol?: 'http' | 'https';
43
+
44
+ /**
45
+ * DNS result ordering.
46
+ * @default 'verbatim' for Node.js >= 17.0.0, 'ipv4first' otherwise
47
+ */
48
+ dnsOrder?: 'verbatim' | 'ipv4first';
49
+
50
+ /**
51
+ * Logger instance for debugging.
52
+ * Set to `false` to disable logging.
53
+ * @default false
54
+ */
55
+ logger?:
56
+ | false
57
+ | {
58
+ info: (...args: unknown[]) => void;
59
+ warn: (...args: unknown[]) => void;
60
+ error: (...args: unknown[]) => void;
61
+ };
62
+
63
+ /**
64
+ * ID generator for DNS packets.
65
+ * Can be a number or a function that returns a number (sync or async).
66
+ * @default 0
67
+ */
68
+ id?: number | (() => number) | (() => Promise<number>);
69
+
70
+ /**
71
+ * Concurrency limit for resolveAny queries.
72
+ * @default os.cpus().length
73
+ */
74
+ concurrency?: number;
75
+
76
+ /**
77
+ * Default IPv4 address for local binding.
78
+ * @default '0.0.0.0'
79
+ */
80
+ ipv4?: string;
81
+
82
+ /**
83
+ * Default IPv6 address for local binding.
84
+ * @default '::0'
85
+ */
86
+ ipv6?: string;
87
+
88
+ /**
89
+ * Port for IPv4 local binding.
90
+ */
91
+ ipv4Port?: number;
92
+
93
+ /**
94
+ * Port for IPv6 local binding.
95
+ */
96
+ ipv6Port?: number;
97
+
98
+ /**
99
+ * Cache instance for storing DNS results.
100
+ * Set to `false` to disable caching.
101
+ * @default new Map()
102
+ */
103
+ cache?: Map<string, unknown> | false;
104
+
105
+ /**
106
+ * Default TTL in seconds for cached DNS results.
107
+ * @default 300
108
+ */
109
+ defaultTTLSeconds?: number;
110
+
111
+ /**
112
+ * Maximum TTL in seconds for cached DNS results.
113
+ * @default 86400
114
+ */
115
+ maxTTLSeconds?: number;
116
+
117
+ /**
118
+ * Function to generate cache arguments.
119
+ */
120
+ setCacheArgs?: (
121
+ key: string,
122
+ result: { expires: number; ttl: number }
123
+ ) => unknown[];
124
+
125
+ /**
126
+ * Whether to return HTTP errors as DNS errors.
127
+ * @default false
128
+ */
129
+ returnHTTPErrors?: boolean;
130
+
131
+ /**
132
+ * Whether to rotate servers on errors.
133
+ * @default true
134
+ */
135
+ smartRotate?: boolean;
136
+
137
+ /**
138
+ * Default error message for unsuccessful HTTP responses.
139
+ * @default 'Unsuccessful HTTP response'
140
+ */
141
+ defaultHTTPErrorMessage?: string;
142
+ };
143
+
144
+ export type DnsRecordType =
145
+ | 'A'
146
+ | 'A6'
147
+ | 'AAAA'
148
+ | 'AFSDB'
149
+ | 'AMTRELAY'
150
+ | 'ANY'
151
+ | 'APL'
152
+ | 'ATMA'
153
+ | 'AVC'
154
+ | 'AXFR'
155
+ | 'CAA'
156
+ | 'CDNSKEY'
157
+ | 'CDS'
158
+ | 'CERT'
159
+ | 'CNAME'
160
+ | 'CSYNC'
161
+ | 'DHCID'
162
+ | 'DLV'
163
+ | 'DNAME'
164
+ | 'DNSKEY'
165
+ | 'DOA'
166
+ | 'DS'
167
+ | 'EID'
168
+ | 'EUI48'
169
+ | 'EUI64'
170
+ | 'GID'
171
+ | 'GPOS'
172
+ | 'HINFO'
173
+ | 'HIP'
174
+ | 'HTTPS'
175
+ | 'IPSECKEY'
176
+ | 'ISDN'
177
+ | 'IXFR'
178
+ | 'KEY'
179
+ | 'KX'
180
+ | 'L32'
181
+ | 'L64'
182
+ | 'LOC'
183
+ | 'LP'
184
+ | 'MAILA'
185
+ | 'MAILB'
186
+ | 'MB'
187
+ | 'MD'
188
+ | 'MF'
189
+ | 'MG'
190
+ | 'MINFO'
191
+ | 'MR'
192
+ | 'MX'
193
+ | 'NAPTR'
194
+ | 'NID'
195
+ | 'NIMLOC'
196
+ | 'NINFO'
197
+ | 'NS'
198
+ | 'NSAP'
199
+ | 'NSAP-PTR'
200
+ | 'NSEC'
201
+ | 'NSEC3'
202
+ | 'NSEC3PARAM'
203
+ | 'NULL'
204
+ | 'NXT'
205
+ | 'OPENPGPKEY'
206
+ | 'OPT'
207
+ | 'PTR'
208
+ | 'PX'
209
+ | 'RKEY'
210
+ | 'RP'
211
+ | 'RRSIG'
212
+ | 'RT'
213
+ | 'Reserved'
214
+ | 'SIG'
215
+ | 'SINK'
216
+ | 'SMIMEA'
217
+ | 'SOA'
218
+ | 'SPF'
219
+ | 'SRV'
220
+ | 'SSHFP'
221
+ | 'SVCB'
222
+ | 'TA'
223
+ | 'TALINK'
224
+ | 'TKEY'
225
+ | 'TLSA'
226
+ | 'TSIG'
227
+ | 'TXT'
228
+ | 'UID'
229
+ | 'UINFO'
230
+ | 'UNSPEC'
231
+ | 'URI'
232
+ | 'WKS'
233
+ | 'X25'
234
+ | 'ZONEMD';
235
+
236
+ export type MxRecord = {
237
+ priority: number;
238
+ exchange: string;
239
+ type?: 'MX';
240
+ };
241
+
242
+ export type NaptrRecord = {
243
+ flags: string;
244
+ service: string;
245
+ regexp: string;
246
+ replacement: string;
247
+ order: number;
248
+ preference: number;
249
+ type?: 'NAPTR';
250
+ };
251
+
252
+ export type SoaRecord = {
253
+ nsname: string;
254
+ hostmaster: string;
255
+ serial: number;
256
+ refresh: number;
257
+ retry: number;
258
+ expire: number;
259
+ minttl: number;
260
+ type?: 'SOA';
261
+ };
262
+
263
+ export type SrvRecord = {
264
+ priority: number;
265
+ weight: number;
266
+ port: number;
267
+ name: string;
268
+ type?: 'SRV';
269
+ };
270
+
271
+ export type CaaRecord = {
272
+ critical: number;
273
+ issue?: string;
274
+ issuewild?: string;
275
+ iodef?: string;
276
+ contactemail?: string;
277
+ contactphone?: string;
278
+ type?: 'CAA';
279
+ };
280
+
281
+ export type CertRecord = {
282
+ certType: number | string;
283
+ keyTag: number;
284
+ algorithm: number;
285
+ certificate: Uint8Array | string;
286
+ };
287
+
288
+ export type TlsaRecord = {
289
+ usage: number;
290
+ selector: number;
291
+ mtype: number;
292
+ matchingType: number;
293
+ cert: Uint8Array;
294
+ certificate: Uint8Array;
295
+ };
296
+
297
+ export type HttpsRecord = {
298
+ name: string;
299
+ ttl: number;
300
+ type: 'HTTPS';
301
+ priority: number;
302
+ target: string;
303
+ params: Record<string, unknown>;
304
+ };
305
+
306
+ export type SvcbRecord = {
307
+ name: string;
308
+ ttl: number;
309
+ type: 'SVCB';
310
+ priority: number;
311
+ target: string;
312
+ params: Record<string, unknown>;
313
+ };
314
+
315
+ export type AnyRecord = {
316
+ type: string;
317
+ [key: string]: unknown;
318
+ };
319
+
320
+ export type ResolveOptions = {
321
+ ttl?: boolean;
322
+ };
323
+
324
+ export type RecordWithTtl = {
325
+ address: string;
326
+ ttl: number;
327
+ };
328
+
329
+ export type SpoofPacket = {
330
+ id: number;
331
+ type: 'response';
332
+ flags: number;
333
+ flag_qr: boolean;
334
+ opcode: string;
335
+ flag_aa: boolean;
336
+ flag_tc: boolean;
337
+ flag_rd: boolean;
338
+ flag_ra: boolean;
339
+ flag_z: boolean;
340
+ flag_ad: boolean;
341
+ flag_cd: boolean;
342
+ rcode: string;
343
+ questions: Array<{ name: string; type: string; class: string }>;
344
+ answers: Array<{
345
+ name: string;
346
+ type: string;
347
+ ttl: number;
348
+ class: string;
349
+ flush: boolean;
350
+ data: unknown;
351
+ }>;
352
+ authorities: unknown[];
353
+ additionals: unknown[];
354
+ ttl: number;
355
+ expires: number;
356
+ };
357
+
358
+ export type RequestFunction = (
359
+ url: string,
360
+ options: Record<string, unknown>
361
+ ) => Promise<{
362
+ statusCode: number;
363
+ headers: Record<string, string>;
364
+ body: {
365
+ arrayBuffer: () => Promise<ArrayBuffer>;
366
+ };
367
+ }>;
368
+
369
+ declare class Tangerine extends Resolver {
370
+ /**
371
+ * Path to the hosts file.
372
+ */
373
+ static HOSTFILE: string;
374
+
375
+ /**
376
+ * Parsed hosts from the hosts file.
377
+ */
378
+ static HOSTS: Array<{ ip: string; hosts: string[] }>;
379
+
380
+ /**
381
+ * Set of valid DNS record types.
382
+ */
383
+ static TYPES: Set<string>;
384
+
385
+ /**
386
+ * Set of DNS error codes.
387
+ */
388
+ static CODES: Set<string>;
389
+
390
+ /**
391
+ * Record types supported by resolveAny.
392
+ */
393
+ static ANY_TYPES: string[];
394
+
395
+ /**
396
+ * Record types supported by native DNS.
397
+ */
398
+ static NATIVE_TYPES: Set<string>;
399
+
400
+ /**
401
+ * Check if a port number is valid.
402
+ */
403
+ static isValidPort(port: number): boolean;
404
+
405
+ /**
406
+ * Get address configuration types based on network interfaces.
407
+ */
408
+ static getAddrConfigTypes(): 0 | 4 | 6;
409
+
410
+ /**
411
+ * Get a random integer between min and max (inclusive).
412
+ */
413
+ static getRandomInt(min: number, max: number): number;
414
+
415
+ /**
416
+ * Combine multiple errors into a single error.
417
+ */
418
+ static combineErrors(errors: Error[]): Error;
419
+
420
+ /**
421
+ * Create a DNS error with the specified code.
422
+ */
423
+ static createError(
424
+ name: string,
425
+ rrtype: string,
426
+ code: string,
427
+ errno?: number
428
+ ): Error;
429
+
430
+ /**
431
+ * Options passed to the constructor.
432
+ */
433
+ options: TangerineOptions;
434
+
435
+ /**
436
+ * Set of active abort controllers.
437
+ */
438
+ abortControllers: Set<AbortController>;
439
+
440
+ /**
441
+ * Create a new Tangerine DNS resolver instance.
442
+ * @param options - Configuration options
443
+ * @param request - HTTP request function (default: undici.request)
444
+ */
445
+ constructor(options?: TangerineOptions, request?: RequestFunction);
446
+
447
+ /**
448
+ * Set local addresses for DNS queries.
449
+ */
450
+ setLocalAddress(ipv4?: string, ipv6?: string): void;
451
+
452
+ /**
453
+ * Resolve a hostname to IP addresses.
454
+ */
455
+ lookup(
456
+ name: string,
457
+ options?: LookupOptions
458
+ ): Promise<LookupAddress | LookupAddress[]>;
459
+
460
+ /**
461
+ * Perform a reverse DNS lookup.
462
+ */
463
+ lookupService(
464
+ address: string,
465
+ port: number,
466
+ abortController?: AbortController,
467
+ purgeCache?: boolean
468
+ ): Promise<{ hostname: string; service: string }>;
469
+
470
+ /**
471
+ * Reverse DNS lookup for an IP address.
472
+ */
473
+ reverse(
474
+ ip: string,
475
+ abortController?: AbortController,
476
+ purgeCache?: boolean
477
+ ): Promise<string[]>;
478
+
479
+ /**
480
+ * Resolve IPv4 addresses.
481
+ */
482
+ resolve4(
483
+ name: string,
484
+ options?: ResolveOptions,
485
+ abortController?: AbortController
486
+ ): Promise<string[] | RecordWithTtl[]>;
487
+
488
+ /**
489
+ * Resolve IPv6 addresses.
490
+ */
491
+ resolve6(
492
+ name: string,
493
+ options?: ResolveOptions,
494
+ abortController?: AbortController
495
+ ): Promise<string[] | RecordWithTtl[]>;
496
+
497
+ /**
498
+ * Resolve CAA records.
499
+ */
500
+ resolveCaa(
501
+ name: string,
502
+ options?: ResolveOptions,
503
+ abortController?: AbortController
504
+ ): Promise<CaaRecord[]>;
505
+
506
+ /**
507
+ * Resolve CNAME records.
508
+ */
509
+ resolveCname(
510
+ name: string,
511
+ options?: ResolveOptions,
512
+ abortController?: AbortController
513
+ ): Promise<string[]>;
514
+
515
+ /**
516
+ * Resolve MX records.
517
+ */
518
+ resolveMx(
519
+ name: string,
520
+ options?: ResolveOptions,
521
+ abortController?: AbortController
522
+ ): Promise<MxRecord[]>;
523
+
524
+ /**
525
+ * Resolve NAPTR records.
526
+ */
527
+ resolveNaptr(
528
+ name: string,
529
+ options?: ResolveOptions,
530
+ abortController?: AbortController
531
+ ): Promise<NaptrRecord[]>;
532
+
533
+ /**
534
+ * Resolve NS records.
535
+ */
536
+ resolveNs(
537
+ name: string,
538
+ options?: ResolveOptions,
539
+ abortController?: AbortController
540
+ ): Promise<string[]>;
541
+
542
+ /**
543
+ * Resolve PTR records.
544
+ */
545
+ resolvePtr(
546
+ name: string,
547
+ options?: ResolveOptions,
548
+ abortController?: AbortController
549
+ ): Promise<string[]>;
550
+
551
+ /**
552
+ * Resolve SOA records.
553
+ */
554
+ resolveSoa(
555
+ name: string,
556
+ options?: ResolveOptions,
557
+ abortController?: AbortController
558
+ ): Promise<SoaRecord>;
559
+
560
+ /**
561
+ * Resolve SRV records.
562
+ */
563
+ resolveSrv(
564
+ name: string,
565
+ options?: ResolveOptions,
566
+ abortController?: AbortController
567
+ ): Promise<SrvRecord[]>;
568
+
569
+ /**
570
+ * Resolve TXT records.
571
+ */
572
+ resolveTxt(
573
+ name: string,
574
+ options?: ResolveOptions,
575
+ abortController?: AbortController
576
+ ): Promise<string[][]>;
577
+
578
+ /**
579
+ * Resolve CERT records.
580
+ */
581
+ resolveCert(
582
+ name: string,
583
+ options?: ResolveOptions,
584
+ abortController?: AbortController
585
+ ): Promise<CertRecord[]>;
586
+
587
+ /**
588
+ * Resolve TLSA records.
589
+ */
590
+ resolveTlsa(
591
+ name: string,
592
+ options?: ResolveOptions,
593
+ abortController?: AbortController
594
+ ): Promise<TlsaRecord[]>;
595
+
596
+ /**
597
+ * Get the list of DNS servers.
598
+ */
599
+ getServers(): string[];
600
+
601
+ /**
602
+ * Cancel all pending DNS queries.
603
+ */
604
+ cancel(): void;
605
+
606
+ /**
607
+ * Resolve any record type.
608
+ */
609
+ resolveAny(
610
+ name: string,
611
+ options?: ResolveOptions,
612
+ abortController?: AbortController
613
+ ): Promise<AnyRecord[]>;
614
+
615
+ /**
616
+ * Set the default result order for DNS queries.
617
+ */
618
+ setDefaultResultOrder(dnsOrder: 'verbatim' | 'ipv4first'): void;
619
+
620
+ /**
621
+ * Set the DNS servers to use.
622
+ */
623
+ setServers(servers: string[]): void;
624
+
625
+ /**
626
+ * Create a spoofed DNS packet for testing.
627
+ * @param name - The hostname
628
+ * @param rrtype - The record type
629
+ * @param answers - Array of answers
630
+ * @param json - Whether to return JSON string (default: false)
631
+ * @param expires - Expiration time in milliseconds (default: 300000 = 5 minutes)
632
+ */
633
+ spoofPacket(
634
+ name: string,
635
+ rrtype: DnsRecordType,
636
+ answers?: unknown[],
637
+ json?: boolean,
638
+ expires?: number | Date
639
+ ): SpoofPacket | string;
640
+
641
+ /**
642
+ * Resolve DNS records of a specific type.
643
+ */
644
+ resolve(
645
+ name: string,
646
+ rrtype?: DnsRecordType,
647
+ options?: ResolveOptions,
648
+ abortController?: AbortController
649
+ ): Promise<unknown>;
650
+ }
651
+
652
+ export default Tangerine;
653
+ export { Tangerine };
package/index.js CHANGED
@@ -57,6 +57,13 @@ const NODE_MAJOR_VERSION = Number.parseInt(
57
57
  10
58
58
  );
59
59
 
60
+ // HTTPS and SVCB record types are not yet supported by dns-packet
61
+ // We map them to UNKNOWN_65 and UNKNOWN_64 respectively
62
+ // See: https://github.com/mafintosh/dns-packet/pull/104
63
+ const HTTPS_SVCB_TYPE_MAP = {
64
+ HTTPS: 'UNKNOWN_65',
65
+ SVCB: 'UNKNOWN_64'
66
+ };
60
67
  // <https://github.com/szmarczak/cacheable-lookup/pull/76>
61
68
  class Tangerine extends dns.promises.Resolver {
62
69
  static HOSTFILE = HOSTFILE;
@@ -279,6 +286,8 @@ class Tangerine extends dns.promises.Resolver {
279
286
  'TXT',
280
287
  'UID',
281
288
  'UINFO',
289
+ 'UNKNOWN_64',
290
+ 'UNKNOWN_65',
282
291
  'UNSPEC',
283
292
  'URI',
284
293
  'WKS',
@@ -1542,7 +1551,7 @@ class Tangerine extends dns.promises.Resolver {
1542
1551
  }
1543
1552
 
1544
1553
  // eslint-disable-next-line max-params
1545
- spoofPacket(name, rrtype, answers = [], json = false, expires = 30000) {
1554
+ spoofPacket(name, rrtype, answers = [], json = false, expires = 300000) {
1546
1555
  if (typeof name !== 'string') {
1547
1556
  const err = new TypeError('The "name" argument must be of type string.');
1548
1557
  err.code = 'ERR_INVALID_ARG_TYPE';
@@ -1635,6 +1644,13 @@ class Tangerine extends dns.promises.Resolver {
1635
1644
  throw err;
1636
1645
  }
1637
1646
 
1647
+ // Map HTTPS/SVCB to UNKNOWN_65/UNKNOWN_64 for dns-packet compatibility
1648
+ // Store original rrtype for later use
1649
+ const originalRrtype = rrtype;
1650
+ if (HTTPS_SVCB_TYPE_MAP[rrtype]) {
1651
+ rrtype = HTTPS_SVCB_TYPE_MAP[rrtype];
1652
+ }
1653
+
1638
1654
  // edge case where c-ares detects "." as start of string or malformed hostnames
1639
1655
  // <https://github.com/c-ares/c-ares/blob/38b30bc922c21faa156939bde15ea35332c30e08/src/lib/ares_getaddrinfo.c#L829>
1640
1656
  if (
@@ -2189,6 +2205,159 @@ class Tangerine extends dns.promises.Resolver {
2189
2205
  });
2190
2206
  }
2191
2207
 
2208
+ // HTTPS and SVCB records (types 65 and 64)
2209
+ // These are mapped from HTTPS/SVCB to UNKNOWN_65/UNKNOWN_64 for dns-packet compatibility
2210
+ // <https://datatracker.ietf.org/doc/html/rfc9460>
2211
+ case 'UNKNOWN_65':
2212
+ case 'UNKNOWN_64': {
2213
+ return result.answers.map((answer) => {
2214
+ // The data is in wire format, we need to parse it
2215
+ // SVCB/HTTPS RDATA format:
2216
+ // - SvcPriority (2 bytes)
2217
+ // - TargetName (variable length, DNS name format)
2218
+ // - SvcParams (variable length, key-value pairs)
2219
+ const obj = {
2220
+ name: answer.name,
2221
+ ttl: answer.ttl,
2222
+ type: originalRrtype // Use the original type (HTTPS or SVCB)
2223
+ };
2224
+
2225
+ if (Buffer.isBuffer(answer.data)) {
2226
+ // Parse the wire format
2227
+ obj.priority = answer.data.subarray(0, 2).readUInt16BE();
2228
+
2229
+ // Parse the target name (DNS name format)
2230
+ let offset = 2;
2231
+ const labels = [];
2232
+ while (offset < answer.data.length) {
2233
+ const labelLen = answer.data[offset];
2234
+ if (labelLen === 0) {
2235
+ offset++;
2236
+ break;
2237
+ }
2238
+
2239
+ labels.push(
2240
+ answer.data
2241
+ .subarray(offset + 1, offset + 1 + labelLen)
2242
+ .toString()
2243
+ );
2244
+ offset += 1 + labelLen;
2245
+ }
2246
+
2247
+ obj.target = labels.length > 0 ? labels.join('.') : '.';
2248
+
2249
+ // Parse SvcParams if there's remaining data
2250
+ obj.params = {};
2251
+ while (offset + 4 <= answer.data.length) {
2252
+ const paramKey = answer.data
2253
+ .subarray(offset, offset + 2)
2254
+ .readUInt16BE();
2255
+ const paramLen = answer.data
2256
+ .subarray(offset + 2, offset + 4)
2257
+ .readUInt16BE();
2258
+ offset += 4;
2259
+
2260
+ if (offset + paramLen > answer.data.length) break;
2261
+
2262
+ const paramValue = answer.data.subarray(
2263
+ offset,
2264
+ offset + paramLen
2265
+ );
2266
+ offset += paramLen;
2267
+
2268
+ // Map known SvcParam keys to names
2269
+ // <https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml>
2270
+ const paramNames = {
2271
+ 0: 'mandatory',
2272
+ 1: 'alpn',
2273
+ 2: 'no-default-alpn',
2274
+ 3: 'port',
2275
+ 4: 'ipv4hint',
2276
+ 5: 'ech',
2277
+ 6: 'ipv6hint',
2278
+ 7: 'dohpath'
2279
+ };
2280
+
2281
+ const paramName = paramNames[paramKey] || `key${paramKey}`;
2282
+
2283
+ // Parse specific param types
2284
+ switch (paramKey) {
2285
+ case 1: {
2286
+ // alpn - list of ALPN protocol IDs
2287
+ const alpns = [];
2288
+ let alpnOffset = 0;
2289
+ while (alpnOffset < paramValue.length) {
2290
+ const alpnLen = paramValue[alpnOffset];
2291
+ alpns.push(
2292
+ paramValue
2293
+ .subarray(alpnOffset + 1, alpnOffset + 1 + alpnLen)
2294
+ .toString()
2295
+ );
2296
+ alpnOffset += 1 + alpnLen;
2297
+ }
2298
+
2299
+ obj.params[paramName] = alpns;
2300
+ break;
2301
+ }
2302
+
2303
+ case 3: {
2304
+ // port
2305
+ obj.params[paramName] = paramValue.readUInt16BE();
2306
+ break;
2307
+ }
2308
+
2309
+ case 4: {
2310
+ // ipv4hint - list of IPv4 addresses
2311
+ const ips = [];
2312
+ for (let i = 0; i < paramValue.length; i += 4) {
2313
+ ips.push(
2314
+ `${paramValue[i]}.${paramValue[i + 1]}.${paramValue[i + 2]}.${paramValue[i + 3]}`
2315
+ );
2316
+ }
2317
+
2318
+ obj.params[paramName] = ips;
2319
+ break;
2320
+ }
2321
+
2322
+ case 6: {
2323
+ // ipv6hint - list of IPv6 addresses
2324
+ const ip6s = [];
2325
+ for (let i = 0; i < paramValue.length; i += 16) {
2326
+ const parts = [];
2327
+ for (let j = 0; j < 16; j += 2) {
2328
+ parts.push(
2329
+ paramValue.subarray(i + j, i + j + 2).toString('hex')
2330
+ );
2331
+ }
2332
+
2333
+ ip6s.push(parts.join(':'));
2334
+ }
2335
+
2336
+ obj.params[paramName] = ip6s;
2337
+ break;
2338
+ }
2339
+
2340
+ case 7: {
2341
+ // dohpath
2342
+ obj.params[paramName] = paramValue.toString();
2343
+ break;
2344
+ }
2345
+
2346
+ default: {
2347
+ // Store as hex for unknown params
2348
+ obj.params[paramName] = paramValue.toString('hex');
2349
+ }
2350
+ }
2351
+ }
2352
+ } else {
2353
+ // Data is already parsed (from cache or JSON)
2354
+ Object.assign(obj, answer.data);
2355
+ }
2356
+
2357
+ return obj;
2358
+ });
2359
+ }
2360
+
2192
2361
  default: {
2193
2362
  this.options.logger.error(
2194
2363
  new Error(
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.0.0",
4
+ "version": "2.0.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"
@@ -59,7 +59,8 @@
59
59
  "node": ">=18"
60
60
  },
61
61
  "files": [
62
- "index.js"
62
+ "index.js",
63
+ "index.d.ts"
63
64
  ],
64
65
  "homepage": "https://github.com/forwardemail/nodejs-dns-over-https-tangerine",
65
66
  "keywords": [
@@ -162,5 +163,6 @@
162
163
  "prepare": "husky install",
163
164
  "pretest": "npm run lint",
164
165
  "test": "npm run nyc"
165
- }
166
+ },
167
+ "types": "index.d.ts"
166
168
  }