@chainfuse/helpers 4.2.14 → 4.3.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/dist/dns.d.mts +81 -71
  2. package/dist/dns.mjs +325 -35
  3. package/package.json +6 -4
package/dist/dns.d.mts CHANGED
@@ -1,74 +1,84 @@
1
- /**
2
- * Supported parameters for Cloudflare's 1.1.1.1 DNS over HTTPS API
3
- */
4
- export interface DohBodyRequest {
5
- /** Query name (required) */
6
- name: string;
7
- /** Query type - either a numeric value or text. Default is 'A' */
8
- type?: string | number;
9
- /**
10
- * DO bit - whether the client wants DNSSEC data.
11
- * Either empty or one of `0`, `false`, `1`, or `true`. Default is `false`
12
- */
13
- do?: string | number | boolean;
1
+ import type { CacheStorageLike } from '@chainfuse/types';
2
+ import type { DurableObjectState, ExecutionContext } from '@cloudflare/workers-types/experimental';
3
+ import * as dnsPacket from 'dns-packet';
4
+ import * as zm from 'zod/mini';
5
+ export declare enum DNSRecordType {
6
+ 'A (IPv4 Address)' = "A",
7
+ 'AAAA (IPv6 Address)' = "AAAA",
8
+ 'AFSDB (AFS database)' = "AFSDB",
9
+ 'APL (Address Prefix List)' = "APL",
10
+ 'AXFR (Zone transfer)' = "AXFR",
11
+ 'CAA (CA authorizations)' = "CAA",
12
+ 'CDNSKEY (Child DNSKEY)' = "CDNSKEY",
13
+ 'CDS (Child DS)' = "CDS",
14
+ 'CERT (Certificate)' = "CERT",
15
+ 'CNAME (Canonical Name)' = "CNAME",
16
+ 'DNAME (Delegation Name)' = "DNAME",
17
+ 'DHCID (DHCP Identifier)' = "DHCID",
18
+ 'DLV (DNSSEC Lookaside Validation)' = "DLV",
19
+ 'DNSKEY (DNSSEC Key)' = "DNSKEY",
20
+ 'DS (Delegation Signer)' = "DS",
21
+ 'HINFO (Host Info)' = "HINFO",
22
+ 'HIP (Host Identity Protocol)' = "HIP",
23
+ 'IXFR (Incremental Zone Transfer)' = "IXFR",
24
+ 'IPSECKEY (IPSEC Key)' = "IPSECKEY",
25
+ 'KEY (Key record)' = "KEY",
26
+ 'KX (Key Exchanger)' = "KX",
27
+ 'LOC (Location)' = "LOC",
28
+ 'MX (Mail Exchange)' = "MX",
29
+ 'NAPTR (Name Authority Pointer)' = "NAPTR",
30
+ 'NS (Name Server)' = "NS",
31
+ 'NSEC (Next Secure)' = "NSEC",
32
+ 'NSEC3 (Next Secure v3)' = "NSEC3",
33
+ 'NSEC3PARAM (NSEC3 Parameters)' = "NSEC3PARAM",
34
+ 'NULL (Experimental null RR)' = "NULL",
35
+ 'OPT (EDNS Options)' = "OPT",
36
+ 'PTR (Pointer)' = "PTR",
37
+ 'RRSIG (DNSSEC Signature)' = "RRSIG",
38
+ 'RP (Responsible Person)' = "RP",
39
+ 'SIG (Signature)' = "SIG",
40
+ 'SOA (Start of Authority)' = "SOA",
41
+ 'SRV (Service)' = "SRV",
42
+ 'SSHFP (SSH Fingerprint)' = "SSHFP",
43
+ 'TA (DNSSEC Trust Anchor)' = "TA",
44
+ 'TKEY (Transaction Key)' = "TKEY",
45
+ 'TLSA (certificate associations)' = "TLSA",
46
+ 'TSIG (Transaction Signature)' = "TSIG",
47
+ 'TXT (Text)' = "TXT",
48
+ 'URI (Uniform Resource Identifier)' = "URI"
49
+ }
50
+ export declare class DnsHelpers<C extends CacheStorageLike, EC extends Pick<ExecutionContext | DurableObjectState, 'waitUntil'> = Pick<ExecutionContext | DurableObjectState, 'waitUntil'>> {
51
+ private nameservers;
52
+ private cache?;
53
+ private backgroundContext?;
54
+ static readonly constructorArgs: zm.ZodMiniObject<{
55
+ nameservers: zm.ZodMiniArray<zm.ZodMiniCodec<zm.ZodMiniURL, zm.ZodMiniCustom<URL, URL>>>;
56
+ }, zm.z.core.$strip>;
14
57
  /**
15
- * CD bit - disable validation.
16
- * Either empty or one of `0`, `false`, `1`, or `true`. Default is `false`
58
+ * Create a DNS helper instance.
59
+ * @param args Parsed constructor args containing the resolver nameserver URLs.
60
+ * @param cacheStore Optional CacheStorage-like implementation to persist DNS lookups; if null, cache is disabled.
17
61
  */
18
- cd?: string | number | boolean;
19
- }
20
- /**
21
- * A record structure with name, type, TTL, and data.
22
- */
23
- interface Record {
24
- /** The record owner */
25
- name: string;
26
- /** The type of DNS record. Defined here: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4 */
27
- type: number;
28
- /** The number of seconds the answer can be stored in cache before it is considered stale */
29
- TTL: number;
30
- /** The value of the DNS record for the given name and type. The data will be in text for standardized record types and in hex for unknown types */
31
- data: string;
32
- }
33
- /**
34
- * A successful DNS response from Cloudflare's 1.1.1.1 DNS over HTTPS API
35
- */
36
- export interface DohSuccessfulResponse {
37
- /** The Response Code of the DNS Query. Defined here: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6 */
38
- Status: number;
39
- /** True if the truncated bit was set. This happens when the DNS answer is larger than a single UDP or TCP packet */
40
- TC: boolean;
41
- /** True if the Recursive Desired bit was set. This is always set to true for Cloudflare DNS over HTTPS */
42
- RD: boolean;
43
- /** True if the Recursion Available bit was set. This is always set to true for Cloudflare DNS over HTTPS */
44
- RA: boolean;
45
- /** True if every record in the answer was verified with DNSSEC */
46
- AD: boolean;
47
- /** True if the client asked to disable DNSSEC validation. In this case, Cloudflare will still fetch the DNSSEC-related records, but it will not attempt to validate the records */
48
- CD: boolean;
49
- /** The record name requested */
50
- Question: Record[];
51
- /** The answer record */
52
- Answer?: Record[];
53
- /** The authority record */
54
- Authority: Record[];
55
- /** The additional record */
56
- Additional: Record[];
57
- /** List of EDE messages. Refer to Extended DNS error codes for more information */
58
- Comment?: string[];
59
- }
60
- /**
61
- * An error response from Cloudflare's 1.1.1.1 DNS over HTTPS API
62
- */
63
- export interface DohErrorResponse {
64
- /** An explanation of the error that occurred */
65
- error: string;
66
- }
67
- export declare class DnsHelpers {
68
- private nameserver_url;
69
- constructor(nameserver_url: string | URL);
70
- query(qName: string, qType?: string | number, qDo?: string | number | boolean, qCd?: string | number | boolean, timeout?: number): Promise<DohSuccessfulResponse | DohErrorResponse>;
71
- private makeGetQuery;
72
- private sendDohMsg;
62
+ constructor(args: zm.input<(typeof DnsHelpers)['constructorArgs']>, cacheStore?: C | null, backgroundContext?: EC);
63
+ static readonly queryArgs: zm.ZodMiniObject<{
64
+ questions: zm.ZodMiniArray<zm.ZodMiniObject<{
65
+ hostname: zm.ZodMiniString<string>;
66
+ recordType: zm.ZodMiniDefault<zm.ZodMiniEnum<typeof DNSRecordType>>;
67
+ }, zm.z.core.$strip>>;
68
+ flags: zm.ZodMiniDefault<zm.ZodMiniObject<{
69
+ recursion: zm.ZodMiniDefault<zm.ZodMiniBoolean<boolean>>;
70
+ dnssecCheck: zm.ZodMiniDefault<zm.ZodMiniBoolean<boolean>>;
71
+ }, zm.z.core.$strip>>;
72
+ timeout: zm.ZodMiniUnion<readonly [zm.ZodMiniCustom<AbortSignal, AbortSignal>, zm.ZodMiniPipe<zm.ZodMiniDefault<zm.ZodMiniNumberFormat>, zm.ZodMiniTransform<AbortSignal, number>>]>;
73
+ }, zm.z.core.$strip>;
74
+ query(_args: zm.input<(typeof DnsHelpers)['queryArgs']>): Promise<{
75
+ readonly flags: {
76
+ readonly 'Official Authority': boolean;
77
+ readonly 'DNSSEC Verified': boolean;
78
+ readonly 'Recursion Available': boolean;
79
+ readonly Truncated: boolean;
80
+ };
81
+ readonly answers: dnsPacket.Answer[] | undefined;
82
+ }>;
83
+ private _query;
73
84
  }
74
- export {};
package/dist/dns.mjs CHANGED
@@ -1,46 +1,336 @@
1
+ import * as dnsPacket from 'dns-packet';
2
+ import * as zm from 'zod/mini';
3
+ import { CryptoHelpers } from "./crypto.mjs";
4
+ export var DNSRecordType;
5
+ (function (DNSRecordType) {
6
+ DNSRecordType["A (IPv4 Address)"] = "A";
7
+ DNSRecordType["AAAA (IPv6 Address)"] = "AAAA";
8
+ DNSRecordType["AFSDB (AFS database)"] = "AFSDB";
9
+ DNSRecordType["APL (Address Prefix List)"] = "APL";
10
+ DNSRecordType["AXFR (Zone transfer)"] = "AXFR";
11
+ DNSRecordType["CAA (CA authorizations)"] = "CAA";
12
+ DNSRecordType["CDNSKEY (Child DNSKEY)"] = "CDNSKEY";
13
+ DNSRecordType["CDS (Child DS)"] = "CDS";
14
+ DNSRecordType["CERT (Certificate)"] = "CERT";
15
+ DNSRecordType["CNAME (Canonical Name)"] = "CNAME";
16
+ DNSRecordType["DNAME (Delegation Name)"] = "DNAME";
17
+ DNSRecordType["DHCID (DHCP Identifier)"] = "DHCID";
18
+ DNSRecordType["DLV (DNSSEC Lookaside Validation)"] = "DLV";
19
+ DNSRecordType["DNSKEY (DNSSEC Key)"] = "DNSKEY";
20
+ DNSRecordType["DS (Delegation Signer)"] = "DS";
21
+ DNSRecordType["HINFO (Host Info)"] = "HINFO";
22
+ DNSRecordType["HIP (Host Identity Protocol)"] = "HIP";
23
+ DNSRecordType["IXFR (Incremental Zone Transfer)"] = "IXFR";
24
+ DNSRecordType["IPSECKEY (IPSEC Key)"] = "IPSECKEY";
25
+ DNSRecordType["KEY (Key record)"] = "KEY";
26
+ DNSRecordType["KX (Key Exchanger)"] = "KX";
27
+ DNSRecordType["LOC (Location)"] = "LOC";
28
+ DNSRecordType["MX (Mail Exchange)"] = "MX";
29
+ DNSRecordType["NAPTR (Name Authority Pointer)"] = "NAPTR";
30
+ DNSRecordType["NS (Name Server)"] = "NS";
31
+ DNSRecordType["NSEC (Next Secure)"] = "NSEC";
32
+ DNSRecordType["NSEC3 (Next Secure v3)"] = "NSEC3";
33
+ DNSRecordType["NSEC3PARAM (NSEC3 Parameters)"] = "NSEC3PARAM";
34
+ DNSRecordType["NULL (Experimental null RR)"] = "NULL";
35
+ DNSRecordType["OPT (EDNS Options)"] = "OPT";
36
+ DNSRecordType["PTR (Pointer)"] = "PTR";
37
+ DNSRecordType["RRSIG (DNSSEC Signature)"] = "RRSIG";
38
+ DNSRecordType["RP (Responsible Person)"] = "RP";
39
+ DNSRecordType["SIG (Signature)"] = "SIG";
40
+ DNSRecordType["SOA (Start of Authority)"] = "SOA";
41
+ DNSRecordType["SRV (Service)"] = "SRV";
42
+ DNSRecordType["SSHFP (SSH Fingerprint)"] = "SSHFP";
43
+ DNSRecordType["TA (DNSSEC Trust Anchor)"] = "TA";
44
+ DNSRecordType["TKEY (Transaction Key)"] = "TKEY";
45
+ DNSRecordType["TLSA (certificate associations)"] = "TLSA";
46
+ DNSRecordType["TSIG (Transaction Signature)"] = "TSIG";
47
+ DNSRecordType["TXT (Text)"] = "TXT";
48
+ DNSRecordType["URI (Uniform Resource Identifier)"] = "URI";
49
+ })(DNSRecordType || (DNSRecordType = {}));
1
50
  export class DnsHelpers {
2
- nameserver_url;
3
- constructor(nameserver_url) {
4
- this.nameserver_url = new URL(nameserver_url);
51
+ nameservers;
52
+ cache;
53
+ backgroundContext;
54
+ static constructorArgs = zm.object({
55
+ nameservers: zm
56
+ .array(zm.codec(zm.url({ protocol: /^(https|tls)$/, hostname: zm.regexes.domain }).check(zm.trim(), zm.minLength(1)), zm.instanceof(URL), {
57
+ decode: (urlString) => new URL(urlString),
58
+ encode: (url) => url.href,
59
+ }))
60
+ .check(zm.minLength(1)),
61
+ });
62
+ /**
63
+ * Create a DNS helper instance.
64
+ * @param args Parsed constructor args containing the resolver nameserver URLs.
65
+ * @param cacheStore Optional CacheStorage-like implementation to persist DNS lookups; if null, cache is disabled.
66
+ */
67
+ constructor(args, cacheStore, backgroundContext) {
68
+ const { nameservers } = DnsHelpers.constructorArgs.parse(args);
69
+ this.nameservers = nameservers;
70
+ if (cacheStore !== null) {
71
+ cacheStore ??= globalThis.caches;
72
+ if ('open' in cacheStore && typeof cacheStore.open === 'function') {
73
+ this.cache = cacheStore.open('dns');
74
+ }
75
+ else {
76
+ throw new Error('Cache store must be a CacheStorage (or equivalent of)');
77
+ }
78
+ }
79
+ this.backgroundContext = backgroundContext;
5
80
  }
6
- query(qName, qType = 'A', qDo = false, qCd = false, timeout = 10 * 1000) {
7
- return new Promise((resolve, reject) => {
8
- this.sendDohMsg(timeout, this.makeGetQuery(this.nameserver_url, qName, qType, qDo, qCd))
9
- .then((response) => {
10
- if (response.ok) {
11
- response.json().then(resolve).catch(reject);
81
+ static queryArgs = zm.object({
82
+ questions: zm
83
+ .array(zm.object({
84
+ hostname: zm.string().check(zm.trim(), zm.minLength(1),
85
+ // DNS dpec is 255 - final period and non-printed zero octect for root
86
+ zm.maxLength(253), zm.regex(zm.regexes.domain)),
87
+ recordType: zm._default(zm.enum(DNSRecordType), DNSRecordType['A (IPv4 Address)']),
88
+ }))
89
+ .check(zm.minLength(1)),
90
+ flags: zm._default(zm.object({
91
+ recursion: zm._default(zm.boolean(), true),
92
+ dnssecCheck: zm._default(zm.boolean(), true),
93
+ }), { recursion: true, dnssecCheck: true }),
94
+ timeout: zm.union([
95
+ zm.instanceof(AbortSignal),
96
+ zm.pipe(zm._default(zm.int().check(zm.positive()), 30 * 1000), zm.transform((ms) => AbortSignal.timeout(ms))),
97
+ ]),
98
+ });
99
+ query(_args) {
100
+ const args = DnsHelpers.queryArgs.parse(_args);
101
+ // Throw immediately if already aborted
102
+ args.timeout.throwIfAborted();
103
+ return Promise.race([
104
+ // Passthrough signal and carry over the rest
105
+ this._query(args.timeout, args.questions, args.flags),
106
+ // Shortcircuit on abort
107
+ new Promise((_, reject) => args.timeout.addEventListener('abort', () => {
108
+ if (args.timeout.reason instanceof DOMException) {
109
+ reject(new Error(`${args.timeout.reason.name}: ${args.timeout.reason.message}`, { cause: args.timeout.reason.cause }));
110
+ }
111
+ else if (args.timeout.reason instanceof Error) {
112
+ reject(args.timeout.reason);
113
+ }
114
+ else if (typeof args.timeout.reason === 'string') {
115
+ reject(new Error(args.timeout.reason));
116
+ }
117
+ else if (args.timeout.reason) {
118
+ reject(new Error(JSON.stringify(args.timeout.reason)));
12
119
  }
13
120
  else {
14
- // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
15
- reject(response.status);
121
+ reject(new Error('AbortError'));
16
122
  }
17
- })
18
- .catch(reject);
19
- });
123
+ }, { once: true })),
124
+ ]);
20
125
  }
21
- makeGetQuery(url, qName, qType = 'A', qDo = false, qCd = false) {
22
- url.searchParams.set('name', qName);
23
- url.searchParams.set('type', qType.toString());
24
- url.searchParams.set('do', qDo.toString());
25
- url.searchParams.set('cd', qCd.toString());
26
- return url;
27
- }
28
- async sendDohMsg(timeout = 10 * 1000, url = this.nameserver_url) {
29
- const controller = new AbortController();
30
- const timer = setTimeout(() => controller.abort(), timeout);
31
- const response = await fetch(url, {
32
- method: 'GET',
33
- headers: {
34
- Accept: 'application/dns-json',
35
- },
36
- signal: controller.signal,
37
- });
38
- clearTimeout(timer);
39
- if (response.ok || response.status === 304) {
40
- return response;
126
+ async _query(signal, questions, flags) {
127
+ let cacheRequest;
128
+ let computedflags = 0;
129
+ if ('recursion' in flags && flags.recursion === true) {
130
+ computedflags |= dnsPacket.RECURSION_DESIRED;
131
+ }
132
+ if ('dnssecCheck' in flags) {
133
+ if (flags.dnssecCheck) {
134
+ computedflags |= dnsPacket.DNSSEC_OK;
135
+ }
136
+ else {
137
+ computedflags |= dnsPacket.CHECKING_DISABLED;
138
+ }
139
+ }
140
+ let responsePacket;
141
+ const errors = [];
142
+ for (const nameserver of this.nameservers) {
143
+ // Reset
144
+ cacheRequest = undefined;
145
+ responsePacket = undefined;
146
+ if (this.cache) {
147
+ const cacheServerUrl = new URL(nameserver);
148
+ // Cache hard requires http or https protocol
149
+ cacheServerUrl.protocol = 'https:';
150
+ // For cache niceness, follow similar to `application/dns-json` format
151
+ cacheServerUrl.searchParams.set('questions', await CryptoHelpers.getHash('SHA-256', JSON.stringify(questions)));
152
+ cacheServerUrl.searchParams.set('flags', computedflags.toString());
153
+ cacheRequest = new Request(cacheServerUrl, { headers: { Accept: 'application/dns-json' }, signal });
154
+ responsePacket = await (await this.cache).match(cacheRequest).then((response) => {
155
+ if (response?.ok) {
156
+ return response.json().then((json) => json);
157
+ }
158
+ else {
159
+ return undefined;
160
+ }
161
+ });
162
+ }
163
+ const fromCache = Boolean(responsePacket);
164
+ try {
165
+ responsePacket ??= await (() => {
166
+ const queryPacket = {
167
+ type: 'query',
168
+ // 1 (inclusive) to 65536 (exclusive)
169
+ id: Math.floor(Math.random() * 65535) + 1,
170
+ flags: computedflags,
171
+ questions: questions.map((q) => ({
172
+ name: q.hostname,
173
+ type: q.recordType,
174
+ class: 'IN',
175
+ })),
176
+ };
177
+ if (nameserver.protocol === 'https:') {
178
+ const dnsQueryBuf = dnsPacket.encode(queryPacket);
179
+ return fetch(nameserver, {
180
+ method: 'POST',
181
+ headers: {
182
+ 'Content-Type': 'application/dns-message',
183
+ Accept: 'application/dns-message',
184
+ },
185
+ signal,
186
+ body: new Uint8Array(dnsQueryBuf),
187
+ })
188
+ .then(async (response) => {
189
+ if (response.ok) {
190
+ return response.arrayBuffer();
191
+ }
192
+ else {
193
+ throw new Error(`${response?.status} ${response?.statusText}`, { cause: await response?.text() });
194
+ }
195
+ })
196
+ .then((buf) => import('node:buffer').then(({ Buffer }) => dnsPacket.decode(Buffer.from(buf))));
197
+ }
198
+ else if (nameserver.protocol === 'tls:') {
199
+ const dnsQueryBuf = dnsPacket.streamEncode(queryPacket);
200
+ return Promise.all([import('node:tls'), import('node:buffer')]).then(([{ connect }, { Buffer }]) => new Promise((resolve, reject) => {
201
+ // Setup TLS client
202
+ const client = connect({
203
+ // RFC 7858 requires 1.2+
204
+ minVersion: 'TLSv1.2',
205
+ port: nameserver.port === '' ? 853 : parseInt(nameserver.port, 10),
206
+ host: nameserver.hostname,
207
+ ...(nameserver.pathname !== '' && { path: nameserver.pathname }),
208
+ }, () => {
209
+ client.write(dnsQueryBuf);
210
+ });
211
+ // Setup abort handling
212
+ const onAbort = () => {
213
+ const error = (() => {
214
+ if (signal.reason instanceof DOMException) {
215
+ return new Error(`${signal.reason.name}: ${signal.reason.message}`, { cause: signal.reason.cause });
216
+ }
217
+ else if (signal.reason instanceof Error) {
218
+ return signal.reason;
219
+ }
220
+ else if (typeof signal.reason === 'string') {
221
+ return new Error(signal.reason);
222
+ }
223
+ else if (signal.reason) {
224
+ return new Error(JSON.stringify(signal.reason));
225
+ }
226
+ else {
227
+ return new Error('AbortError');
228
+ }
229
+ })();
230
+ client.destroy(error);
231
+ reject(error);
232
+ };
233
+ signal.addEventListener('abort', onAbort, { once: true });
234
+ // Finish setting up client
235
+ client.once('error', reject);
236
+ let rawBody = Buffer.from(new Uint8Array(0));
237
+ let expectedLength = 0;
238
+ client.on('data', (data) => {
239
+ if (rawBody.byteLength === 0) {
240
+ expectedLength = data.readUInt16BE(0);
241
+ if (expectedLength < 12) {
242
+ reject(new Error('Below DNS minimum packet length (DNS Header is 12 bytes)'));
243
+ }
244
+ rawBody = Buffer.from(data);
245
+ }
246
+ else {
247
+ rawBody = Buffer.concat([rawBody, data]);
248
+ }
249
+ /**
250
+ * @link https://tools.ietf.org/html/rfc7858#section-3.3
251
+ * @link https://tools.ietf.org/html/rfc1035#section-4.2.2
252
+ * The message is prefixed with a two byte length field which gives the message length, excluding the two byte length field.
253
+ */
254
+ if (rawBody.length === expectedLength + 2) {
255
+ client.destroy();
256
+ resolve(dnsPacket.streamDecode(rawBody));
257
+ }
258
+ });
259
+ client.once('end', () => signal.removeEventListener('abort', onAbort));
260
+ }));
261
+ }
262
+ else {
263
+ throw new Error(`Unsupported protocol: ${nameserver.protocol}`);
264
+ }
265
+ })().then(async (packet) => {
266
+ const formattedPacket = {
267
+ ...packet,
268
+ answers: await import('node:buffer').then(({ Buffer }) => (packet.answers ?? []).map((a) => {
269
+ if ('data' in a) {
270
+ if (Array.isArray(a.data)) {
271
+ return { ...a, data: a.data.map((part) => (typeof part === 'string' ? part : part.toString('utf8'))) };
272
+ }
273
+ else if (Buffer.isBuffer(a.data)) {
274
+ return { ...a, data: a.data.toString('utf8') };
275
+ }
276
+ else if (typeof a.data === 'object') {
277
+ return { ...a, data: a.data };
278
+ }
279
+ else {
280
+ return { ...a, data: a.data.toString() };
281
+ }
282
+ }
283
+ else {
284
+ return a;
285
+ }
286
+ })),
287
+ };
288
+ if (this.cache && !fromCache) {
289
+ // Re-assign response to make it mutable
290
+ const cacheResponse = new Response(JSON.stringify(formattedPacket), { headers: { 'Content-Type': 'application/dns-json' } });
291
+ const cachePromise = (async () => {
292
+ const answersWithTtl = (formattedPacket.answers ?? []).filter((a) => 'ttl' in a);
293
+ const ttl = answersWithTtl.length > 0 ? Math.min(...answersWithTtl.map((a) => ('ttl' in a ? a.ttl : 0))) : 0;
294
+ cacheResponse.headers.set('Cache-Control', `public, max-age=${ttl}, s-maxage=${ttl}`);
295
+ await CryptoHelpers.generateETag(cacheResponse)
296
+ .then((etag) => cacheResponse.headers.set('ETag', etag))
297
+ .catch((err) => console.warn('ETag generation failed', err));
298
+ return (await this.cache).put(cacheRequest, cacheResponse);
299
+ })();
300
+ if (this.backgroundContext) {
301
+ this.backgroundContext.waitUntil(cachePromise);
302
+ }
303
+ else {
304
+ await cachePromise;
305
+ }
306
+ }
307
+ // Go back to nice JSON format
308
+ return formattedPacket;
309
+ });
310
+ if (responsePacket)
311
+ break;
312
+ }
313
+ catch (err) {
314
+ errors.push(err instanceof Error ? err : new Error(String(err)));
315
+ continue;
316
+ }
317
+ }
318
+ if (responsePacket) {
319
+ return {
320
+ flags: {
321
+ 'Official Authority': Boolean((responsePacket.flags ?? 0) & dnsPacket.AUTHORITATIVE_ANSWER),
322
+ 'DNSSEC Verified': Boolean((responsePacket.flags ?? 0) & dnsPacket.AUTHENTIC_DATA),
323
+ 'Recursion Available': Boolean((responsePacket.flags ?? 0) & dnsPacket.RECURSION_AVAILABLE),
324
+ Truncated: Boolean((responsePacket.flags ?? 0) & dnsPacket.TRUNCATED_RESPONSE),
325
+ },
326
+ answers: responsePacket.answers,
327
+ };
41
328
  }
42
329
  else {
43
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
330
+ if (errors.length > 0) {
331
+ throw new AggregateError(errors, 'No nameserver responded');
332
+ }
333
+ throw new Error('No nameserver responded');
44
334
  }
45
335
  }
46
336
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chainfuse/helpers",
3
- "version": "4.2.14",
3
+ "version": "4.3.0",
4
4
  "description": "",
5
5
  "author": "ChainFuse",
6
6
  "homepage": "https://github.com/ChainFuse/packages/tree/main/packages/helpers#readme",
@@ -84,13 +84,15 @@
84
84
  "@discordjs/rest": "^2.6.0",
85
85
  "chalk": "^5.6.2",
86
86
  "cloudflare": "^5.2.0",
87
+ "dns-packet": "^5.6.1",
87
88
  "drizzle-orm": "^0.45.1",
88
89
  "strip-ansi": "^7.1.2",
89
90
  "uuid": "^13.0.0",
90
- "zod": "^4.3.5"
91
+ "zod": "^4.3.6"
91
92
  },
92
93
  "devDependencies": {
93
- "@cloudflare/workers-types": "^4.20260120.0"
94
+ "@cloudflare/workers-types": "^4.20260128.0",
95
+ "@types/dns-packet": "^5.6.5"
94
96
  },
95
- "gitHead": "7465800b60661c071d9ca65c41368bb474e6c94c"
97
+ "gitHead": "3a294f5b789b6beedafb3aa9925a797c26fdc00a"
96
98
  }