diff --git a/src/index.ts b/src/index.ts index 4a21d81..8c09e19 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +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"; +import { aliDnsResolver, cloudflareDnsResolver, googleDnsResolver, proxyDnsResolver } from "./util/dns-resolvers"; const { trace } = getLogger("index"); @@ -24,7 +24,12 @@ interface GenericObject { export type CustomDnsResolver = (domain: string) => Promise; -export const defaultDnsResolvers: CustomDnsResolver[] = [googleDnsResolver, cloudflareDnsResolver, aliDnsResolver]; +export const defaultDnsResolvers: CustomDnsResolver[] = [ + googleDnsResolver, + cloudflareDnsResolver, + proxyDnsResolver, + aliDnsResolver, +]; /** * Returns true for strings that are openattestation records diff --git a/src/util/dns-resolvers/dns-resolvers.test.ts b/src/util/dns-resolvers/dns-resolvers.test.ts index 9866c5c..aa15cf8 100644 --- a/src/util/dns-resolvers/dns-resolvers.test.ts +++ b/src/util/dns-resolvers/dns-resolvers.test.ts @@ -3,6 +3,7 @@ import { http, HttpResponse } from "msw"; import { aliDnsResolver } from "./ali-dns-resolver"; import { cloudflareDnsResolver } from "./cloudflare-dns-resolver"; import { googleDnsResolver } from "./google-dns-resolver"; +import { proxyDnsResolver } from "./proxy-dns-resolver"; const emptyDnsJson = { Status: 0, @@ -118,3 +119,37 @@ describe("aliDnsResolver", () => { await expect(aliDnsResolver("")).rejects.toThrow("Domain is required"); }); }); + +describe("proxyDnsResolver", () => { + let server: SetupServerApi | undefined; + + afterEach(() => { + server?.close(); + }); + + test("requests proxy DNS JSON with name, TXT type, and encoded query", async () => { + server = setupServer( + http.get("https://dns.opencerts.io/resolve", ({ request }) => { + const url = new URL(request.url); + expect(url.searchParams.get("name")).toBe("oc example.test"); + expect(url.searchParams.get("type")).toBe("TXT"); + return HttpResponse.json(emptyDnsJson); + }) + ); + server.listen(); + + const out = await proxyDnsResolver("oc example.test"); + expect(out).toMatchObject({ Status: 0, Answer: [] }); + }); + + test("throws when proxy DNS returns non-2xx", async () => { + server = setupServer(http.get("https://dns.opencerts.io/resolve", () => new HttpResponse(null, { status: 502 }))); + server.listen(); + + await expect(proxyDnsResolver("oc.example.test")).rejects.toThrow(/HTTP 502/); + }); + + test("throws when domain is empty", async () => { + await expect(proxyDnsResolver("")).rejects.toThrow("Domain is required"); + }); +}); diff --git a/src/util/dns-resolvers/index.ts b/src/util/dns-resolvers/index.ts index c622480..ed54466 100644 --- a/src/util/dns-resolvers/index.ts +++ b/src/util/dns-resolvers/index.ts @@ -1,3 +1,4 @@ export * from "./google-dns-resolver"; export * from "./cloudflare-dns-resolver"; export * from "./ali-dns-resolver"; +export * from "./proxy-dns-resolver"; diff --git a/src/util/dns-resolvers/proxy-dns-resolver.ts b/src/util/dns-resolvers/proxy-dns-resolver.ts new file mode 100644 index 0000000..1e91312 --- /dev/null +++ b/src/util/dns-resolvers/proxy-dns-resolver.ts @@ -0,0 +1,28 @@ +import type { CustomDnsResolver, IDNSQueryResponse } from "../.."; + +/** Server-side DoH proxy that forwards to Google/Cloudflare upstream — reachable from regions where direct access to those endpoints is blocked. */ +export const proxyDnsResolver: CustomDnsResolver = async (domain) => { + const url = new URL("https://dns.opencerts.io/resolve"); + + if (!domain) { + throw new Error("Domain is required"); + } + + url.searchParams.set("name", domain); + url.searchParams.set("type", "TXT"); + + const res = await fetch(url); + + if (!res.ok) { + throw new Error(`Proxy DNS request failed: HTTP ${res.status}`); + } + + let data; + try { + data = await res.json(); + } catch { + throw new Error("Failed to parse DNS response JSON"); + } + + return data as IDNSQueryResponse; +};