From 94ceabd898dbd5014e29f511ba39c430a21f360e Mon Sep 17 00:00:00 2001 From: SJC Date: Thu, 2 Apr 2026 15:15:45 +0800 Subject: [PATCH 1/9] feat: add AliDNS and Cloudflare DNS resolvers with corresponding tests --- src/index.test.ts | 46 +++++----- src/index.ts | 31 +++---- src/util/dns-resolvers/ali-dns-resolver.ts | 8 ++ .../dns-resolvers/cloudflare-dns-resolver.ts | 9 ++ src/util/dns-resolvers/dns-resolvers.test.ts | 87 +++++++++++++++++++ src/util/dns-resolvers/google-dns-resolver.ts | 8 ++ src/util/dns-resolvers/index.ts | 3 + 7 files changed, 150 insertions(+), 42 deletions(-) create mode 100644 src/util/dns-resolvers/ali-dns-resolver.ts create mode 100644 src/util/dns-resolvers/cloudflare-dns-resolver.ts create mode 100644 src/util/dns-resolvers/dns-resolvers.test.ts create mode 100644 src/util/dns-resolvers/google-dns-resolver.ts create mode 100644 src/util/dns-resolvers/index.ts diff --git a/src/index.test.ts b/src/index.test.ts index 54b5f5e..d66343a 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,6 +1,14 @@ import { setupServer, SetupServerApi } from "msw/node"; import { http, HttpResponse } from "msw"; -import { CustomDnsResolver, getDocumentStoreRecords, queryDns, parseDocumentStoreResults, getDnsDidRecords } from "."; +import { + aliDnsResolver, + cloudflareDnsResolver, + getDocumentStoreRecords, + getDnsDidRecords, + googleDnsResolver, + parseDocumentStoreResults, + queryDns, +} from "."; import { DnsproveStatusCode } from "./common/error"; describe("getCertStoreRecords", () => { @@ -208,22 +216,7 @@ describe("queryDns", () => { Comment: "Response from 205.251.199.177.", }; - const testDnsResolvers: CustomDnsResolver[] = [ - async (domain) => { - const data = await fetch(`https://dns.google/resolve?name=${domain}&type=TXT`, { - method: "GET", - }); - - return data.json(); - }, - async (domain) => { - const data = await fetch(`https://cloudflare-dns.com/dns-query?name=${domain}&type=TXT`, { - method: "GET", - headers: { accept: "application/dns-json", contentType: "application/json", connection: "keep-alive" }, - }); - return data.json(); - }, - ]; + const testDnsResolvers = [googleDnsResolver, cloudflareDnsResolver, aliDnsResolver]; afterEach(() => { server.close(); @@ -270,14 +263,16 @@ describe("queryDns", () => { http.get("https://cloudflare-dns.com/dns-query", (_) => { return new HttpResponse(null, { status: 500 }); }), + http.get("https://dns.alidns.com/resolve", (_) => { + return new HttpResponse(null, { status: 500 }); + }), ]; server = setupServer(...handlers); server.listen(); - try { - await queryDns("https://donotuse.openattestation.com", testDnsResolvers); - } catch (e: any) { - expect(e.code).toStrictEqual(DnsproveStatusCode.IDNS_QUERY_ERROR_GENERAL); - } + + await expect(queryDns("https://donotuse.openattestation.com", testDnsResolvers)).rejects.toMatchObject({ + code: DnsproveStatusCode.IDNS_QUERY_ERROR_GENERAL, + }); }); }); @@ -321,6 +316,13 @@ describe("getDocumentStoreRecords for Astron", () => { addr: "0x18bc0127Ae33389cD96593a1a612774fD14c0737", dnssec: false, }, + { + type: "openatts", + net: "ethereum", + netId: "1338", + addr: "0x94FD21A026E29E0686583b8be71Cb28a8ca1A8d4", + dnssec: false, + }, { type: "openatts", net: "ethereum", diff --git a/src/index.ts b/src/index.ts index 7bc7f78..4a21d81 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import { OpenAttestationDNSTextRecord, OpenAttestationDNSTextRecordT } from "./r import { OpenAttestationDnsDidRecord, OpenAttestationDnsDidRecordT } from "./records/dnsDid"; import { getLogger } from "./util/logger"; import { CodedError, DnsproveStatusCode } from "./common/error"; +import { aliDnsResolver, cloudflareDnsResolver, googleDnsResolver } from "./util/dns-resolvers"; const { trace } = getLogger("index"); @@ -23,22 +24,7 @@ interface GenericObject { export type CustomDnsResolver = (domain: string) => Promise; -export const defaultDnsResolvers: CustomDnsResolver[] = [ - async (domain) => { - const data = await fetch(`https://dns.google/resolve?name=${domain}&type=TXT`, { - method: "GET", - }); - - return data.json(); - }, - async (domain) => { - const data = await fetch(`https://cloudflare-dns.com/dns-query?name=${domain}&type=TXT`, { - method: "GET", - headers: { accept: "application/dns-json", contentType: "application/json", connection: "keep-alive" }, - }); - return data.json(); - }, -]; +export const defaultDnsResolvers: CustomDnsResolver[] = [googleDnsResolver, cloudflareDnsResolver, aliDnsResolver]; /** * Returns true for strings that are openattestation records @@ -115,8 +101,10 @@ export const parseOpenAttestationRecord = (record: string): GenericObject => { trace(`Parsing record: ${record}`); const keyValuePairs = record.trim().split(" "); // tokenize into key=value elements const recordObject = {} as GenericObject; - // @ts-ignore: we already checked for this token - recordObject.type = keyValuePairs.shift(); + const typeToken = keyValuePairs.shift(); + if (typeToken !== undefined) { + recordObject.type = typeToken; + } keyValuePairs.reduce(addKeyValuePairToObject, recordObject); return recordObject; }; @@ -153,6 +141,7 @@ const parseOpenAttestationRecords = (recordSet: IDNSRecord[] = []): GenericObjec /** * Takes a DNS-TXT Record set and returns openattestation document store records if any * @param recordSet Refer to tests for examples + * @param dnssec Resolver AD (authenticated data) flag; applied as each record's `dnssec` field */ export const parseDocumentStoreResults = ( recordSet: IDNSRecord[] = [], @@ -177,6 +166,7 @@ export const parseDnsDidResults = (recordSet: IDNSRecord[] = [], dnssec: boolean /** * Queries a given domain and parses the results to retrieve openattestation document store records if any * @param domain e.g: "example.openattestation.com" + * @param customDnsResolvers Optional resolver list; built-in HTTP DNS chain is used when omitted * @example * > getDocumentStoreRecords("example.openattestation.com") * > [ { type: 'openatts', @@ -191,7 +181,7 @@ export const getDocumentStoreRecords = async ( ): Promise => { trace(`Received request to resolve ${domain}`); - const dnsResolvers = customDnsResolvers || defaultDnsResolvers; + const dnsResolvers = customDnsResolvers ?? defaultDnsResolvers; const results = await queryDns(domain, dnsResolvers); const answers = results.Answer || []; @@ -207,7 +197,7 @@ export const getDnsDidRecords = async ( ): Promise => { trace(`Received request to resolve ${domain}`); - const dnsResolvers = customDnsResolvers || defaultDnsResolvers; + const dnsResolvers = customDnsResolvers ?? defaultDnsResolvers; const results = await queryDns(domain, dnsResolvers); const answers = results.Answer || []; @@ -218,3 +208,4 @@ export const getDnsDidRecords = async ( }; export { OpenAttestationDNSTextRecord, OpenAttestationDnsDidRecord }; +export * from "./util/dns-resolvers"; diff --git a/src/util/dns-resolvers/ali-dns-resolver.ts b/src/util/dns-resolvers/ali-dns-resolver.ts new file mode 100644 index 0000000..d02f133 --- /dev/null +++ b/src/util/dns-resolvers/ali-dns-resolver.ts @@ -0,0 +1,8 @@ +import type { CustomDnsResolver, IDNSQueryResponse } from "../.."; + +export const aliDnsResolver: CustomDnsResolver = async (domain) => { + const res = await fetch(`https://dns.alidns.com/resolve?name=${domain}&type=16`, { + method: "GET", + }); + return res.json() as Promise; +}; diff --git a/src/util/dns-resolvers/cloudflare-dns-resolver.ts b/src/util/dns-resolvers/cloudflare-dns-resolver.ts new file mode 100644 index 0000000..bfafdfb --- /dev/null +++ b/src/util/dns-resolvers/cloudflare-dns-resolver.ts @@ -0,0 +1,9 @@ +import type { CustomDnsResolver, IDNSQueryResponse } from "../.."; + +export const cloudflareDnsResolver: CustomDnsResolver = async (domain) => { + const res = await fetch(`https://cloudflare-dns.com/dns-query?name=${domain}&type=TXT`, { + method: "GET", + headers: { accept: "application/dns-json", contentType: "application/json", connection: "keep-alive" }, + }); + return res.json() as Promise; +}; diff --git a/src/util/dns-resolvers/dns-resolvers.test.ts b/src/util/dns-resolvers/dns-resolvers.test.ts new file mode 100644 index 0000000..20f8b71 --- /dev/null +++ b/src/util/dns-resolvers/dns-resolvers.test.ts @@ -0,0 +1,87 @@ +import { setupServer, SetupServerApi } from "msw/node"; +import { http, HttpResponse } from "msw"; +import { aliDnsResolver } from "./ali-dns-resolver"; +import { cloudflareDnsResolver } from "./cloudflare-dns-resolver"; +import { googleDnsResolver } from "./google-dns-resolver"; + +const emptyDnsJson = { + Status: 0, + TC: false, + RD: true, + RA: true, + AD: false, + CD: false, + Answer: [] as [], +}; + +describe("googleDnsResolver", () => { + let server: SetupServerApi; + + afterEach(() => { + server.close(); + }); + + test("requests Google DNS JSON with name and TXT type", async () => { + server = setupServer( + http.get("https://dns.google/resolve", ({ request }) => { + const url = new URL(request.url); + expect(url.searchParams.get("name")).toBe("my.domain.test"); + expect(url.searchParams.get("type")).toBe("TXT"); + return HttpResponse.json(emptyDnsJson); + }) + ); + server.listen(); + + const out = await googleDnsResolver("my.domain.test"); + expect(out).toMatchObject({ Status: 0, Answer: [] }); + }); +}); + +describe("cloudflareDnsResolver", () => { + let server: SetupServerApi; + + afterEach(() => { + server.close(); + }); + + test("requests Cloudflare DNS JSON with name, TXT type, and expected headers", async () => { + server = setupServer( + http.get("https://cloudflare-dns.com/dns-query", ({ request }) => { + const url = new URL(request.url); + expect(url.searchParams.get("name")).toBe("cf.example.test"); + expect(url.searchParams.get("type")).toBe("TXT"); + expect(request.headers.get("accept")).toBe("application/dns-json"); + expect(request.headers.get("connection")).toBe("keep-alive"); + expect(request.headers.get("contenttype")).toBe("application/json"); + return HttpResponse.json(emptyDnsJson); + }) + ); + server.listen(); + + const out = await cloudflareDnsResolver("cf.example.test"); + expect(out).toMatchObject({ Status: 0, Answer: [] }); + }); +}); + +describe("aliDnsResolver", () => { + let server: SetupServerApi; + + afterEach(() => { + server.close(); + }); + + test("requests Ali DNS JSON with name and type 16 (TXT)", async () => { + server = setupServer( + http.get("https://dns.alidns.com/resolve", ({ request }) => { + const url = new URL(request.url); + expect(url.searchParams.get("name")).toBe("ali.example.test"); + expect(url.searchParams.get("type")).toBe("16"); + return HttpResponse.json(emptyDnsJson); + }) + ); + server.listen(); + + const out = await aliDnsResolver("ali.example.test"); + expect(out).toMatchObject({ Status: 0, Answer: [] }); + }); +}); diff --git a/src/util/dns-resolvers/google-dns-resolver.ts b/src/util/dns-resolvers/google-dns-resolver.ts new file mode 100644 index 0000000..26fcc65 --- /dev/null +++ b/src/util/dns-resolvers/google-dns-resolver.ts @@ -0,0 +1,8 @@ +import type { CustomDnsResolver, IDNSQueryResponse } from "../.."; + +export const googleDnsResolver: CustomDnsResolver = async (domain) => { + const res = await fetch(`https://dns.google/resolve?name=${domain}&type=TXT`, { + method: "GET", + }); + return res.json() as Promise; +}; diff --git a/src/util/dns-resolvers/index.ts b/src/util/dns-resolvers/index.ts new file mode 100644 index 0000000..c622480 --- /dev/null +++ b/src/util/dns-resolvers/index.ts @@ -0,0 +1,3 @@ +export * from "./google-dns-resolver"; +export * from "./cloudflare-dns-resolver"; +export * from "./ali-dns-resolver"; From 02c4ae1b0419e58ff7e57b6532c594f3ed09e793 Mon Sep 17 00:00:00 2001 From: SJC Date: Thu, 2 Apr 2026 15:45:19 +0800 Subject: [PATCH 2/9] feat: enhance DNS resolvers with error handling and URL search parameters --- src/util/dns-resolvers/ali-dns-resolver.ts | 13 +++-- .../dns-resolvers/cloudflare-dns-resolver.ts | 13 ++++- src/util/dns-resolvers/dns-resolvers.test.ts | 47 ++++++++++++++----- src/util/dns-resolvers/google-dns-resolver.ts | 13 +++-- 4 files changed, 67 insertions(+), 19 deletions(-) diff --git a/src/util/dns-resolvers/ali-dns-resolver.ts b/src/util/dns-resolvers/ali-dns-resolver.ts index d02f133..20a3832 100644 --- a/src/util/dns-resolvers/ali-dns-resolver.ts +++ b/src/util/dns-resolvers/ali-dns-resolver.ts @@ -1,8 +1,15 @@ import type { CustomDnsResolver, IDNSQueryResponse } from "../.."; export const aliDnsResolver: CustomDnsResolver = async (domain) => { - const res = await fetch(`https://dns.alidns.com/resolve?name=${domain}&type=16`, { - method: "GET", - }); + const url = new URL("https://dns.alidns.com/resolve"); + url.searchParams.set("name", domain); + url.searchParams.set("type", "16"); + + const res = await fetch(url, { method: "GET" }); + + if (!res.ok) { + throw new Error(`Ali DNS request failed: HTTP ${res.status}`); + } + return res.json() as Promise; }; diff --git a/src/util/dns-resolvers/cloudflare-dns-resolver.ts b/src/util/dns-resolvers/cloudflare-dns-resolver.ts index bfafdfb..1a088e7 100644 --- a/src/util/dns-resolvers/cloudflare-dns-resolver.ts +++ b/src/util/dns-resolvers/cloudflare-dns-resolver.ts @@ -1,9 +1,18 @@ import type { CustomDnsResolver, IDNSQueryResponse } from "../.."; export const cloudflareDnsResolver: CustomDnsResolver = async (domain) => { - const res = await fetch(`https://cloudflare-dns.com/dns-query?name=${domain}&type=TXT`, { + const url = new URL("https://cloudflare-dns.com/dns-query"); + url.searchParams.set("name", domain); + url.searchParams.set("type", "TXT"); + + const res = await fetch(url, { method: "GET", - headers: { accept: "application/dns-json", contentType: "application/json", connection: "keep-alive" }, + headers: { Accept: "application/dns-json" }, }); + + if (!res.ok) { + throw new Error(`Cloudflare DNS request failed: HTTP ${res.status}`); + } + return res.json() as Promise; }; diff --git a/src/util/dns-resolvers/dns-resolvers.test.ts b/src/util/dns-resolvers/dns-resolvers.test.ts index 20f8b71..efe5be4 100644 --- a/src/util/dns-resolvers/dns-resolvers.test.ts +++ b/src/util/dns-resolvers/dns-resolvers.test.ts @@ -21,20 +21,29 @@ describe("googleDnsResolver", () => { server.close(); }); - test("requests Google DNS JSON with name and TXT type", async () => { + test("requests Google DNS JSON with name, TXT type, and encoded query", async () => { server = setupServer( http.get("https://dns.google/resolve", ({ request }) => { const url = new URL(request.url); - expect(url.searchParams.get("name")).toBe("my.domain.test"); + expect(url.searchParams.get("name")).toBe("my domain.test"); expect(url.searchParams.get("type")).toBe("TXT"); return HttpResponse.json(emptyDnsJson); }) ); server.listen(); - const out = await googleDnsResolver("my.domain.test"); + const out = await googleDnsResolver("my domain.test"); expect(out).toMatchObject({ Status: 0, Answer: [] }); }); + + test("throws when Google DNS returns non-2xx", async () => { + server = setupServer( + http.get("https://dns.google/resolve", () => new HttpResponse(null, { status: 503 })) + ); + server.listen(); + + await expect(googleDnsResolver("my.domain.test")).rejects.toThrow(/HTTP 503/); + }); }); describe("cloudflareDnsResolver", () => { @@ -44,23 +53,30 @@ describe("cloudflareDnsResolver", () => { server.close(); }); - test("requests Cloudflare DNS JSON with name, TXT type, and expected headers", async () => { + test("requests Cloudflare DNS JSON with name, TXT type, Accept header, and encoded query", async () => { server = setupServer( http.get("https://cloudflare-dns.com/dns-query", ({ request }) => { const url = new URL(request.url); - expect(url.searchParams.get("name")).toBe("cf.example.test"); + expect(url.searchParams.get("name")).toBe("cf example.test"); expect(url.searchParams.get("type")).toBe("TXT"); expect(request.headers.get("accept")).toBe("application/dns-json"); - expect(request.headers.get("connection")).toBe("keep-alive"); - expect(request.headers.get("contenttype")).toBe("application/json"); return HttpResponse.json(emptyDnsJson); }) ); server.listen(); - const out = await cloudflareDnsResolver("cf.example.test"); + const out = await cloudflareDnsResolver("cf example.test"); expect(out).toMatchObject({ Status: 0, Answer: [] }); }); + + test("throws when Cloudflare DNS returns non-2xx", async () => { + server = setupServer( + http.get("https://cloudflare-dns.com/dns-query", () => new HttpResponse(null, { status: 502 })) + ); + server.listen(); + + await expect(cloudflareDnsResolver("cf.example.test")).rejects.toThrow(/HTTP 502/); + }); }); describe("aliDnsResolver", () => { @@ -70,18 +86,27 @@ describe("aliDnsResolver", () => { server.close(); }); - test("requests Ali DNS JSON with name and type 16 (TXT)", async () => { + test("requests Ali DNS JSON with name, type 16 (TXT), and encoded query", async () => { server = setupServer( http.get("https://dns.alidns.com/resolve", ({ request }) => { const url = new URL(request.url); - expect(url.searchParams.get("name")).toBe("ali.example.test"); + expect(url.searchParams.get("name")).toBe("ali example.test"); expect(url.searchParams.get("type")).toBe("16"); return HttpResponse.json(emptyDnsJson); }) ); server.listen(); - const out = await aliDnsResolver("ali.example.test"); + const out = await aliDnsResolver("ali example.test"); expect(out).toMatchObject({ Status: 0, Answer: [] }); }); + + test("throws when Ali DNS returns non-2xx", async () => { + server = setupServer( + http.get("https://dns.alidns.com/resolve", () => new HttpResponse(null, { status: 503 })) + ); + server.listen(); + + await expect(aliDnsResolver("ali.example.test")).rejects.toThrow(/HTTP 503/); + }); }); diff --git a/src/util/dns-resolvers/google-dns-resolver.ts b/src/util/dns-resolvers/google-dns-resolver.ts index 26fcc65..34bb550 100644 --- a/src/util/dns-resolvers/google-dns-resolver.ts +++ b/src/util/dns-resolvers/google-dns-resolver.ts @@ -1,8 +1,15 @@ import type { CustomDnsResolver, IDNSQueryResponse } from "../.."; export const googleDnsResolver: CustomDnsResolver = async (domain) => { - const res = await fetch(`https://dns.google/resolve?name=${domain}&type=TXT`, { - method: "GET", - }); + const url = new URL("https://dns.google/resolve"); + url.searchParams.set("name", domain); + url.searchParams.set("type", "TXT"); + + const res = await fetch(url, { method: "GET" }); + + if (!res.ok) { + throw new Error(`Google DNS request failed: HTTP ${res.status}`); + } + return res.json() as Promise; }; From 67ee6a6fa3583c7cdf220f84c626943a2836a30b Mon Sep 17 00:00:00 2001 From: SJC Date: Fri, 3 Apr 2026 16:55:35 +0800 Subject: [PATCH 3/9] feat: add domain validation to DNS resolvers --- src/util/dns-resolvers/ali-dns-resolver.ts | 5 ++++ .../dns-resolvers/cloudflare-dns-resolver.ts | 5 ++++ src/util/dns-resolvers/dns-resolvers.test.ts | 24 ++++++++++++++----- src/util/dns-resolvers/google-dns-resolver.ts | 5 ++++ 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/util/dns-resolvers/ali-dns-resolver.ts b/src/util/dns-resolvers/ali-dns-resolver.ts index 20a3832..ed92a44 100644 --- a/src/util/dns-resolvers/ali-dns-resolver.ts +++ b/src/util/dns-resolvers/ali-dns-resolver.ts @@ -2,6 +2,11 @@ import type { CustomDnsResolver, IDNSQueryResponse } from "../.."; export const aliDnsResolver: CustomDnsResolver = async (domain) => { const url = new URL("https://dns.alidns.com/resolve"); + + if (!domain) { + throw new Error("Domain is required"); + } + url.searchParams.set("name", domain); url.searchParams.set("type", "16"); diff --git a/src/util/dns-resolvers/cloudflare-dns-resolver.ts b/src/util/dns-resolvers/cloudflare-dns-resolver.ts index 1a088e7..90aa51f 100644 --- a/src/util/dns-resolvers/cloudflare-dns-resolver.ts +++ b/src/util/dns-resolvers/cloudflare-dns-resolver.ts @@ -2,6 +2,11 @@ import type { CustomDnsResolver, IDNSQueryResponse } from "../.."; export const cloudflareDnsResolver: CustomDnsResolver = async (domain) => { const url = new URL("https://cloudflare-dns.com/dns-query"); + + if (!domain) { + throw new Error("Domain is required"); + } + url.searchParams.set("name", domain); url.searchParams.set("type", "TXT"); diff --git a/src/util/dns-resolvers/dns-resolvers.test.ts b/src/util/dns-resolvers/dns-resolvers.test.ts index efe5be4..5f21531 100644 --- a/src/util/dns-resolvers/dns-resolvers.test.ts +++ b/src/util/dns-resolvers/dns-resolvers.test.ts @@ -15,10 +15,10 @@ const emptyDnsJson = { }; describe("googleDnsResolver", () => { - let server: SetupServerApi; + let server: SetupServerApi | undefined; afterEach(() => { - server.close(); + server?.close(); }); test("requests Google DNS JSON with name, TXT type, and encoded query", async () => { @@ -44,13 +44,17 @@ describe("googleDnsResolver", () => { await expect(googleDnsResolver("my.domain.test")).rejects.toThrow(/HTTP 503/); }); + + test("throws when domain is empty", async () => { + await expect(googleDnsResolver("")).rejects.toThrow("Domain is required"); + }); }); describe("cloudflareDnsResolver", () => { - let server: SetupServerApi; + let server: SetupServerApi | undefined; afterEach(() => { - server.close(); + server?.close(); }); test("requests Cloudflare DNS JSON with name, TXT type, Accept header, and encoded query", async () => { @@ -77,13 +81,17 @@ describe("cloudflareDnsResolver", () => { await expect(cloudflareDnsResolver("cf.example.test")).rejects.toThrow(/HTTP 502/); }); + + test("throws when domain is empty", async () => { + await expect(cloudflareDnsResolver("")).rejects.toThrow("Domain is required"); + }); }); describe("aliDnsResolver", () => { - let server: SetupServerApi; + let server: SetupServerApi | undefined; afterEach(() => { - server.close(); + server?.close(); }); test("requests Ali DNS JSON with name, type 16 (TXT), and encoded query", async () => { @@ -109,4 +117,8 @@ describe("aliDnsResolver", () => { await expect(aliDnsResolver("ali.example.test")).rejects.toThrow(/HTTP 503/); }); + + test("throws when domain is empty", async () => { + await expect(aliDnsResolver("")).rejects.toThrow("Domain is required"); + }); }); diff --git a/src/util/dns-resolvers/google-dns-resolver.ts b/src/util/dns-resolvers/google-dns-resolver.ts index 34bb550..199636a 100644 --- a/src/util/dns-resolvers/google-dns-resolver.ts +++ b/src/util/dns-resolvers/google-dns-resolver.ts @@ -2,6 +2,11 @@ import type { CustomDnsResolver, IDNSQueryResponse } from "../.."; export const googleDnsResolver: CustomDnsResolver = async (domain) => { const url = new URL("https://dns.google/resolve"); + + if (!domain) { + throw new Error("Domain is required"); + } + url.searchParams.set("name", domain); url.searchParams.set("type", "TXT"); From 8cba2d97f1698f242f4f3e95077a653c045edd41 Mon Sep 17 00:00:00 2001 From: SJC Date: Fri, 3 Apr 2026 17:09:44 +0800 Subject: [PATCH 4/9] feat: use constant for TXT record type in AliDNS resolver --- src/util/dns-resolvers/ali-dns-resolver.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/util/dns-resolvers/ali-dns-resolver.ts b/src/util/dns-resolvers/ali-dns-resolver.ts index ed92a44..7c4cf0d 100644 --- a/src/util/dns-resolvers/ali-dns-resolver.ts +++ b/src/util/dns-resolvers/ali-dns-resolver.ts @@ -1,5 +1,7 @@ import type { CustomDnsResolver, IDNSQueryResponse } from "../.."; +/** Ali DNS JSON API uses numeric RRTYPE; 16 = TXT */ +const ALI_DNS_TXT_QUERY_TYPE = "16"; export const aliDnsResolver: CustomDnsResolver = async (domain) => { const url = new URL("https://dns.alidns.com/resolve"); @@ -8,7 +10,7 @@ export const aliDnsResolver: CustomDnsResolver = async (domain) => { } url.searchParams.set("name", domain); - url.searchParams.set("type", "16"); + url.searchParams.set("type", ALI_DNS_TXT_QUERY_TYPE); const res = await fetch(url, { method: "GET" }); From 0ab62262789c761a11e673534d0b92a58b6b4ffd Mon Sep 17 00:00:00 2001 From: SJC Date: Fri, 3 Apr 2026 17:17:22 +0800 Subject: [PATCH 5/9] feat: improve error handling for JSON parsing in DNS resolvers --- src/util/dns-resolvers/ali-dns-resolver.ts | 9 ++++++++- src/util/dns-resolvers/cloudflare-dns-resolver.ts | 9 ++++++++- src/util/dns-resolvers/google-dns-resolver.ts | 9 ++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/util/dns-resolvers/ali-dns-resolver.ts b/src/util/dns-resolvers/ali-dns-resolver.ts index 7c4cf0d..eeb9705 100644 --- a/src/util/dns-resolvers/ali-dns-resolver.ts +++ b/src/util/dns-resolvers/ali-dns-resolver.ts @@ -18,5 +18,12 @@ export const aliDnsResolver: CustomDnsResolver = async (domain) => { throw new Error(`Ali DNS request failed: HTTP ${res.status}`); } - return res.json() as Promise; + let data; + try { + data = await res.json(); + } catch { + throw new Error("Failed to parse DNS response JSON"); + } + + return data as IDNSQueryResponse; }; diff --git a/src/util/dns-resolvers/cloudflare-dns-resolver.ts b/src/util/dns-resolvers/cloudflare-dns-resolver.ts index 90aa51f..ed45cd3 100644 --- a/src/util/dns-resolvers/cloudflare-dns-resolver.ts +++ b/src/util/dns-resolvers/cloudflare-dns-resolver.ts @@ -19,5 +19,12 @@ export const cloudflareDnsResolver: CustomDnsResolver = async (domain) => { throw new Error(`Cloudflare DNS request failed: HTTP ${res.status}`); } - return res.json() as Promise; + let data; + try { + data = await res.json(); + } catch { + throw new Error("Failed to parse DNS response JSON"); + } + + return data as IDNSQueryResponse; }; diff --git a/src/util/dns-resolvers/google-dns-resolver.ts b/src/util/dns-resolvers/google-dns-resolver.ts index 199636a..906b5f0 100644 --- a/src/util/dns-resolvers/google-dns-resolver.ts +++ b/src/util/dns-resolvers/google-dns-resolver.ts @@ -16,5 +16,12 @@ export const googleDnsResolver: CustomDnsResolver = async (domain) => { throw new Error(`Google DNS request failed: HTTP ${res.status}`); } - return res.json() as Promise; + let data; + try { + data = await res.json(); + } catch { + throw new Error("Failed to parse DNS response JSON"); + } + + return data as IDNSQueryResponse; }; From ae5176e531061e650e161bf00b3346b4fa6e8aa6 Mon Sep 17 00:00:00 2001 From: SJC Date: Fri, 3 Apr 2026 17:18:04 +0800 Subject: [PATCH 6/9] feat: simplify fetch calls in DNS resolvers by removing redundant method specification --- src/util/dns-resolvers/ali-dns-resolver.ts | 2 +- src/util/dns-resolvers/cloudflare-dns-resolver.ts | 1 - src/util/dns-resolvers/google-dns-resolver.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/util/dns-resolvers/ali-dns-resolver.ts b/src/util/dns-resolvers/ali-dns-resolver.ts index eeb9705..506d75f 100644 --- a/src/util/dns-resolvers/ali-dns-resolver.ts +++ b/src/util/dns-resolvers/ali-dns-resolver.ts @@ -12,7 +12,7 @@ export const aliDnsResolver: CustomDnsResolver = async (domain) => { url.searchParams.set("name", domain); url.searchParams.set("type", ALI_DNS_TXT_QUERY_TYPE); - const res = await fetch(url, { method: "GET" }); + const res = await fetch(url); if (!res.ok) { throw new Error(`Ali DNS request failed: HTTP ${res.status}`); diff --git a/src/util/dns-resolvers/cloudflare-dns-resolver.ts b/src/util/dns-resolvers/cloudflare-dns-resolver.ts index ed45cd3..65369a4 100644 --- a/src/util/dns-resolvers/cloudflare-dns-resolver.ts +++ b/src/util/dns-resolvers/cloudflare-dns-resolver.ts @@ -11,7 +11,6 @@ export const cloudflareDnsResolver: CustomDnsResolver = async (domain) => { url.searchParams.set("type", "TXT"); const res = await fetch(url, { - method: "GET", headers: { Accept: "application/dns-json" }, }); diff --git a/src/util/dns-resolvers/google-dns-resolver.ts b/src/util/dns-resolvers/google-dns-resolver.ts index 906b5f0..f55ddfd 100644 --- a/src/util/dns-resolvers/google-dns-resolver.ts +++ b/src/util/dns-resolvers/google-dns-resolver.ts @@ -10,7 +10,7 @@ export const googleDnsResolver: CustomDnsResolver = async (domain) => { url.searchParams.set("name", domain); url.searchParams.set("type", "TXT"); - const res = await fetch(url, { method: "GET" }); + const res = await fetch(url); if (!res.ok) { throw new Error(`Google DNS request failed: HTTP ${res.status}`); From 351544bda7824a4d013171bf0caed36672914641 Mon Sep 17 00:00:00 2001 From: SJC Date: Mon, 13 Apr 2026 11:32:15 +0800 Subject: [PATCH 7/9] feat: update test cases to use trustvc.io domain and disable DNSSEC --- src/index.test.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/index.test.ts b/src/index.test.ts index d66343a..a15dd47 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -16,11 +16,11 @@ describe("getCertStoreRecords", () => { type: "openatts", net: "ethereum", netId: "3", - dnssec: true, + dnssec: false, addr: "0x2f60375e8144e16Adf1979936301D8341D58C36C", }; test("it should work", async () => { - const records = await getDocumentStoreRecords("donotuse.openattestation.com"); + const records = await getDocumentStoreRecords("donotuse.trustvc.io"); expect(records).toStrictEqual([sampleDnsTextRecordWithDnssec]); }); @@ -35,14 +35,14 @@ describe("getCertStoreRecords", () => { describe("getDnsDidRecords", () => { test("it should work", async () => { - const records = await getDnsDidRecords("donotuse.openattestation.com"); + const records = await getDnsDidRecords("donotuse.trustvc.io"); expect(records).toStrictEqual([ { type: "openatts", algorithm: "dns-did", publicKey: "did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller", version: "1.0", - dnssec: true, + dnssec: false, }, ]); }); @@ -185,29 +185,29 @@ describe("queryDns", () => { RA: true, AD: true, CD: false, - Question: [{ name: "donotuse.openattestation.com.", type: 16 }], + Question: [{ name: "donotuse.trustvc.io.", type: 16 }], Answer: [ { - name: "donotuse.openattestation.com.", + name: "donotuse.trustvc.io.", type: 16, TTL: 300, data: "openatts a=dns-did; p=did:ethr:0xE712878f6E8d5d4F9e87E10DA604F9cB564C9a89#controller; v=1.0;", }, { - name: "donotuse.openattestation.com.", + name: "donotuse.trustvc.io.", type: 16, TTL: 300, data: "openatts DO NOT ADD ANY RECORDS BEYOND THIS AS THIS DOMAIN IS USED FOR DNSPROVE NPM LIBRARY INTEGRATION TESTS", }, { - name: "donotuse.openattestation.com.", + name: "donotuse.trustvc.io.", type: 16, TTL: 300, data: "openatts fooooooobarrrrrrrrr this entry exists to ensure validation works", }, { - name: "donotuse.openattestation.com.", + name: "donotuse.trustvc.io.", type: 16, TTL: 300, data: "openatts net=ethereum netId=3 addr=0x2f60375e8144e16Adf1979936301D8341D58C36C", @@ -232,7 +232,7 @@ describe("queryDns", () => { server = setupServer(...handlers); server.listen(); - const records = await queryDns("https://donotuse.openattestation.com", testDnsResolvers); + const records = await queryDns("https://donotuse.trustvc.io", testDnsResolvers); const sortedAnswer = records?.Answer.sort((a, b) => a.data.localeCompare(b.data)); expect(sortedAnswer).toMatchObject(sampleResponse.Answer); }); @@ -249,7 +249,7 @@ describe("queryDns", () => { server = setupServer(...handlers); server.listen(); - const records = await queryDns("https://donotuse.openattestation.com", testDnsResolvers); + const records = await queryDns("https://donotuse.trustvc.io", testDnsResolvers); const sortedAnswer = records?.Answer.sort((a, b) => a.data.localeCompare(b.data)); expect(sortedAnswer).toMatchObject(sampleResponse.Answer); @@ -270,7 +270,7 @@ describe("queryDns", () => { server = setupServer(...handlers); server.listen(); - await expect(queryDns("https://donotuse.openattestation.com", testDnsResolvers)).rejects.toMatchObject({ + await expect(queryDns("https://donotuse.trustvc.io", testDnsResolvers)).rejects.toMatchObject({ code: DnsproveStatusCode.IDNS_QUERY_ERROR_GENERAL, }); }); From 9885826b975a16d565fabafe282311cac67a0bd8 Mon Sep 17 00:00:00 2001 From: SJC Date: Mon, 13 Apr 2026 11:35:53 +0800 Subject: [PATCH 8/9] feat: add fallback mechanism to third DNS resolver when first two are down --- src/index.test.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/index.test.ts b/src/index.test.ts index a15dd47..8ce97b2 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -255,6 +255,27 @@ describe("queryDns", () => { expect(sortedAnswer).toMatchObject(sampleResponse.Answer); }); + test("Should fallback to third dns when first and second dns is down", async () => { + const handlers = [ + http.get("https://dns.google/resolve", (_) => { + return new HttpResponse(null, { status: 500 }); + }), + http.get("https://cloudflare-dns.com/dns-query", (_) => { + return new HttpResponse(null, { status: 500 }); + }), + http.get("https://dns.alidns.com/resolve", (_) => { + return HttpResponse.json(sampleResponse); + }), + ]; + server = setupServer(...handlers); + server.listen(); + + const records = await queryDns("https://donotuse.trustvc.io", testDnsResolvers); + + const sortedAnswer = records?.Answer.sort((a, b) => a.data.localeCompare(b.data)); + expect(sortedAnswer).toMatchObject(sampleResponse.Answer); + }); + test("Should throw error when all dns provided are down", async () => { const handlers = [ http.get("https://dns.google/resolve", (_) => { From d2eb43cf9e4008376099fb4581fbe4e64cd4d39f Mon Sep 17 00:00:00 2001 From: SJC Date: Thu, 16 Apr 2026 18:07:35 +0800 Subject: [PATCH 9/9] feat: streamline server setup in DNS resolver tests --- src/util/dns-resolvers/dns-resolvers.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/util/dns-resolvers/dns-resolvers.test.ts b/src/util/dns-resolvers/dns-resolvers.test.ts index 5f21531..9866c5c 100644 --- a/src/util/dns-resolvers/dns-resolvers.test.ts +++ b/src/util/dns-resolvers/dns-resolvers.test.ts @@ -37,9 +37,7 @@ describe("googleDnsResolver", () => { }); test("throws when Google DNS returns non-2xx", async () => { - server = setupServer( - http.get("https://dns.google/resolve", () => new HttpResponse(null, { status: 503 })) - ); + server = setupServer(http.get("https://dns.google/resolve", () => new HttpResponse(null, { status: 503 }))); server.listen(); await expect(googleDnsResolver("my.domain.test")).rejects.toThrow(/HTTP 503/); @@ -110,9 +108,7 @@ describe("aliDnsResolver", () => { }); test("throws when Ali DNS returns non-2xx", async () => { - server = setupServer( - http.get("https://dns.alidns.com/resolve", () => new HttpResponse(null, { status: 503 })) - ); + server = setupServer(http.get("https://dns.alidns.com/resolve", () => new HttpResponse(null, { status: 503 }))); server.listen(); await expect(aliDnsResolver("ali.example.test")).rejects.toThrow(/HTTP 503/);