diff --git a/.github/workflows/types.yml b/.github/workflows/types.yml
new file mode 100644
index 0000000..a7648bc
--- /dev/null
+++ b/.github/workflows/types.yml
@@ -0,0 +1,29 @@
+name: TypeScript types
+
+on:
+ push:
+ branches: [master, main]
+ paths:
+ - 'ts/index.d.ts'
+ - 'index.js'
+ - 'ts/typings-check.ts'
+ - 'ts/tsconfig.json'
+ - '.github/workflows/types.yml'
+ pull_request:
+ types: [opened, synchronize, reopened]
+
+permissions:
+ contents: read
+
+jobs:
+ typecheck:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - uses: actions/setup-node@v6
+ with:
+ node-version: 24
+ - name: Install type-check dependencies
+ run: npm install --no-save typescript @types/node
+ - name: Check index.d.ts
+ run: npx tsc --project ts/tsconfig.json
diff --git a/package.json b/package.json
index c4ba401..3e3b93b 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"version": "2.1.0",
"description": "A DNS Server and Client Implementation in Pure JavaScript with no dependencies.",
"main": "index.js",
+ "types": "ts/index.d.ts",
"scripts": {
"test": "node --test",
"test:coverage": "node --test --experimental-test-coverage",
diff --git a/ts/index.d.ts b/ts/index.d.ts
new file mode 100644
index 0000000..36355aa
--- /dev/null
+++ b/ts/index.d.ts
@@ -0,0 +1,364 @@
+///
+
+import * as dgram from 'node:dgram';
+import { EventEmitter } from 'node:events';
+import * as http from 'node:http';
+import * as net from 'node:net';
+
+// ─── Main DNS class ───────────────────────────────────────────────────────────
+
+declare class DNS extends EventEmitter {
+ constructor(options?: Partial);
+
+ resolve(
+ domain: string,
+ type?: string,
+ cls?: number,
+ options?: DNS.ResolveOptions,
+ ): Promise;
+
+ resolveA(domain: string, clientIp?: string): Promise;
+ resolveAAAA(domain: string): Promise;
+ resolveMX(domain: string): Promise;
+ resolveCNAME(domain: string): Promise;
+ resolvePTR(domain: string): Promise;
+ resolveDNSKEY(domain: string): Promise;
+ resolveRRSIG(domain: string): Promise;
+
+ static createServer(options: DNS.CreateServerOptions): DNS.DnsServer;
+ static createUDPServer(options?: DNS.UdpServerOptions | DNS.DnsHandler): DNS.UDPServer;
+ static createTCPServer(options?: DNS.DnsHandler): DNS.TCPServer;
+ static createDOHServer(options?: DNS.DohServerOptions): DNS.DOHServer;
+
+ static UDPClient(options?: DNS.UdpClientOptions): DNS.DnsResolver;
+ static TCPClient(options?: DNS.TcpClientOptions): DNS.DnsResolver;
+ static DOHClient(options?: DNS.DohClientOptions): DNS.DnsResolver;
+ static GoogleClient(): DNS.DnsResolver;
+}
+
+// ─── Namespace (all exported sub-types live here) ─────────────────────────────
+
+declare namespace DNS {
+
+ // ── Packet ──────────────────────────────────────────────────────────────────
+
+ class Packet {
+ header: Packet.Header;
+ questions: Packet.Question[];
+ answers: Packet.Resource[];
+ authorities: Packet.Resource[];
+ additionals: Packet.Resource[];
+ recursive: boolean;
+
+ constructor(
+ data?: Packet | Packet.Header | Packet.Question | Packet.Resource | string | any[],
+ );
+
+ toBuffer(): Buffer;
+ toBase64URL(): string;
+
+ // ── Static constants ────────────────────────────────────────────────────
+
+ static TYPE: {
+ A : 0x01;
+ NS : 0x02;
+ MD : 0x03;
+ MF : 0x04;
+ CNAME : 0x05;
+ SOA : 0x06;
+ MB : 0x07;
+ MG : 0x08;
+ MR : 0x09;
+ NULL : 0x0a;
+ WKS : 0x0b;
+ PTR : 0x0c;
+ HINFO : 0x0d;
+ MINFO : 0x0e;
+ MX : 0x0f;
+ TXT : 0x10;
+ AAAA : 0x1c;
+ SRV : 0x21;
+ EDNS : 0x29;
+ SPF : 0x63;
+ AXFR : 0xfc;
+ MAILB : 0xfd;
+ MAILA : 0xfe;
+ ANY : 0xff;
+ CAA : 0x101;
+ DNSKEY : 0x30;
+ };
+
+ static CLASS: {
+ IN : 0x01;
+ CS : 0x02;
+ CH : 0x03;
+ HS : 0x04;
+ ANY : 0xff;
+ };
+
+ static EDNS_OPTION_CODE: {
+ ECS: 0x08;
+ };
+
+ // ── Static helpers ──────────────────────────────────────────────────────
+
+ static parse(buffer: Buffer): Packet;
+ static createResponseFromRequest(request: Packet): Packet;
+ static createResourceFromQuestion(
+ base: Packet.Question,
+ record: Partial,
+ ): Packet.Resource;
+ static readStream(socket: NodeJS.ReadableStream): Promise;
+ static toIPv6(parts: number[]): string;
+ static fromIPv6(address: string): string[];
+ static uuid(): number;
+
+ // ── Sub-constructors ────────────────────────────────────────────────────
+
+ static Header: {
+ new(header?: Partial): Packet.Header;
+ parse(reader: Buffer | Packet.Reader): Packet.Header;
+ };
+
+ static Question: {
+ new(
+ name?: string | Partial,
+ type?: number,
+ cls?: number,
+ ): Packet.Question;
+ parse(reader: Buffer | Packet.Reader): Packet.Question;
+ decode(reader: Buffer | Packet.Reader): Packet.Question;
+ encode(question: Packet.Question, writer?: Packet.Writer): Buffer;
+ };
+
+ static Resource: {
+ new(
+ name?: string | Partial,
+ type?: number,
+ cls?: number,
+ ttl?: number,
+ ): Packet.Resource;
+ parse(reader: Buffer | Packet.Reader): Packet.Resource;
+ decode(reader: Buffer | Packet.Reader): Packet.Resource;
+ encode(resource: Packet.Resource, writer?: Packet.Writer): Buffer;
+ EDNS(rdata: object[]): Packet.Resource;
+ };
+
+ static Name: {
+ COPY: 0xc0;
+ encode(domain: string, writer?: Packet.Writer): Buffer;
+ decode(reader: Buffer | Packet.Reader): string;
+ };
+
+ static Reader: new(buffer: Buffer, offset?: number) => Packet.Reader;
+ static Writer: new() => Packet.Writer;
+ }
+
+ namespace Packet {
+ interface Header {
+ id: number;
+ qr: 0 | 1;
+ opcode: number;
+ aa: 0 | 1;
+ tc: 0 | 1;
+ rd: 0 | 1;
+ ra: 0 | 1;
+ z: number;
+ rcode: number;
+ qdcount: number;
+ ancount: number;
+ nscount: number;
+ arcount: number;
+ toBuffer(writer?: Writer): Buffer;
+ }
+
+ interface Question {
+ name: string;
+ type: number;
+ class: number;
+ toBuffer(writer?: Writer): Buffer;
+ }
+
+ /** Union of all possible DNS resource record shapes. */
+ interface Resource {
+ name: string;
+ type: number;
+ class: number;
+ ttl: number;
+ // A / AAAA
+ address?: string;
+ // MX
+ exchange?: string;
+ priority?: number;
+ // CNAME / PTR / NS
+ domain?: string;
+ ns?: string;
+ // TXT / SPF
+ data?: string | string[];
+ // SOA
+ primary?: string;
+ admin?: string;
+ serial?: number;
+ refresh?: number;
+ retry?: number;
+ expiration?: number;
+ minimum?: number;
+ // SRV
+ weight?: number;
+ port?: number;
+ target?: string;
+ // CAA
+ flags?: number;
+ tag?: string;
+ value?: string;
+ // DNSKEY
+ algorithm?: number;
+ keyTag?: number;
+ publicKey?: string;
+ toBuffer(writer?: Writer): Buffer;
+ }
+
+ interface Reader {
+ offset: number;
+ read(bits: number): number;
+ }
+
+ interface Writer {
+ buffer: number[];
+ write(value: number, bits: number): void;
+ writeBuffer(writer: Writer): void;
+ toBuffer(): Buffer;
+ }
+ }
+
+ // ── Servers ─────────────────────────────────────────────────────────────────
+
+ class UDPServer extends dgram.Socket {
+ constructor(options?: UdpServerOptions | DnsHandler);
+ handle(data: Buffer, rinfo: dgram.RemoteInfo): void;
+ response(rinfo: dgram.RemoteInfo, message: Packet | Buffer): Promise;
+ listen(port?: number, address?: string): Promise;
+ on(event: 'request', listener: DnsHandler): this;
+ on(event: 'requestError', listener: (error: Error) => void): this;
+ on(event: 'listening', listener: () => void): this;
+ on(event: 'close', listener: () => void): this;
+ on(event: string, listener: (...args: any[]) => void): this;
+ }
+
+ class TCPServer extends net.Server {
+ constructor(options?: DnsHandler);
+ on(event: 'request', listener: DnsHandler): this;
+ on(event: 'requestError', listener: (error: Error) => void): this;
+ on(event: string, listener: (...args: any[]) => void): this;
+ }
+
+ class DOHServer extends EventEmitter {
+ constructor(options?: DohServerOptions);
+ listen(port?: number, address?: string): void;
+ address(): net.AddressInfo | null;
+ close(): void;
+ on(event: 'request', listener: DnsHandler): this;
+ on(event: 'requestError', listener: (error: Error) => void): this;
+ on(event: 'listening', listener: (address: net.AddressInfo) => void): this;
+ on(event: 'close', listener: () => void): this;
+ on(event: string, listener: (...args: any[]) => void): this;
+ }
+
+ class DnsServer extends EventEmitter {
+ constructor(options: CreateServerOptions);
+ addresses(): ServerAddresses;
+ listen(options?: DnsServerListenOptions): Promise;
+ close(): Promise;
+ on(event: 'request', listener: DnsHandler): this;
+ on(event: 'requestError', listener: (error: Error) => void): this;
+ on(event: 'listening', listener: (addresses: ServerAddresses) => void): this;
+ on(event: 'close', listener: () => void): this;
+ on(event: 'error', listener: (error: Error, transport: 'udp' | 'tcp' | 'doh') => void): this;
+ on(event: string, listener: (...args: any[]) => void): this;
+ }
+
+ // ── Handler & resolver callable types ────────────────────────────────────────
+
+ type DnsHandler = (
+ request: Packet,
+ send: (response: Packet | Buffer) => Promise,
+ client: dgram.RemoteInfo | net.Socket | http.IncomingMessage,
+ ) => void;
+
+ type DnsResolver = (
+ name: string,
+ type?: string,
+ cls?: number,
+ options?: ResolveOptions,
+ ) => Promise;
+
+ // ── Options ──────────────────────────────────────────────────────────────────
+
+ interface ClientOptions {
+ port: number;
+ retries: number;
+ timeout: number;
+ recursive: boolean;
+ resolverProtocol: 'UDP' | 'TCP' | 'DOH' | 'Google';
+ nameServers: string[];
+ rootServers: string[];
+ }
+
+ interface ResolveOptions {
+ recursive?: boolean;
+ /** EDNS ECS client subnet in CIDR notation, e.g. `"1.2.3.4/24"` */
+ clientIp?: string;
+ }
+
+ interface UdpClientOptions {
+ dns?: string;
+ port?: number;
+ socketType?: dgram.SocketType;
+ timeout?: number;
+ }
+
+ interface TcpClientOptions {
+ dns: string;
+ protocol?: 'tcp:' | 'tls:';
+ port?: number;
+ }
+
+ interface DohClientOptions {
+ dns: string;
+ }
+
+ interface UdpServerOptions {
+ type?: 'udp4' | 'udp6';
+ }
+
+ interface DohServerOptions {
+ port?: number;
+ ssl?: boolean;
+ cors?: boolean | string | ((origin: string) => boolean);
+ [key: string]: any;
+ }
+
+ type ListenOptions = number | { port?: number; address?: string };
+
+ interface DnsServerListenOptions {
+ udp?: ListenOptions;
+ tcp?: ListenOptions;
+ doh?: ListenOptions;
+ }
+
+ interface CreateServerOptions {
+ udp?: boolean | UdpServerOptions;
+ tcp?: boolean;
+ doh?: boolean | DohServerOptions;
+ handle?: DnsHandler;
+ maxConcurrent?: number;
+ }
+
+ interface ServerAddresses {
+ udp?: net.AddressInfo;
+ tcp?: net.AddressInfo;
+ doh?: net.AddressInfo;
+ }
+}
+
+export = DNS;
diff --git a/ts/tsconfig.json b/ts/tsconfig.json
new file mode 100644
index 0000000..ea9a023
--- /dev/null
+++ b/ts/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "target": "ES2019",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "ignoreDeprecations": "6.0",
+ "typeRoots": ["../node_modules/@types"]
+ },
+ "include": ["typings-check.ts"]
+}
diff --git a/ts/typings-check.ts b/ts/typings-check.ts
new file mode 100644
index 0000000..c36ae62
--- /dev/null
+++ b/ts/typings-check.ts
@@ -0,0 +1,123 @@
+/**
+ * Type-check smoke test for index.d.ts.
+ *
+ * This file is NOT executed at runtime. It exists solely so that `tsc` can
+ * verify that index.d.ts accurately describes the package's public API.
+ * CI runs `tsc --project tsconfig.json` and fails if there are type errors.
+ *
+ * Add a line here whenever a new public API surface is added to index.js.
+ */
+
+import DNS = require('./index');
+import type { AddressInfo } from 'node:net';
+
+const { Packet } = DNS;
+
+// ── DNS instance (high-level resolver) ────────────────────────────────────────
+
+const dns = new DNS({ nameServers: ['8.8.8.8'], port: 53, recursive: true });
+
+void dns.resolve('example.com', 'A');
+void dns.resolveA('example.com');
+void dns.resolveA('example.com', '1.2.3.4');
+void dns.resolveAAAA('example.com');
+void dns.resolveMX('example.com');
+void dns.resolveCNAME('www.example.com');
+void dns.resolvePTR('1.0.0.127.in-addr.arpa');
+void dns.resolveDNSKEY('example.com');
+void dns.resolveRRSIG('example.com');
+
+dns.resolve('example.com').then((packet: DNS.Packet) => {
+ const hdr: DNS.Packet.Header = packet.header;
+ const _id: number = hdr.id;
+ const _rc: number = hdr.rcode;
+ const answer: DNS.Packet.Resource = packet.answers[0];
+ const _addr: string | undefined = answer.address;
+ const _ttl: number = answer.ttl;
+});
+
+// ── Packet static constants ───────────────────────────────────────────────────
+
+const _typeA: number = Packet.TYPE.A;
+const _typeAAAA: number = Packet.TYPE.AAAA;
+const _typeMX: number = Packet.TYPE.MX;
+const _typeDNSKEY: number = Packet.TYPE.DNSKEY;
+const _classIN: number = Packet.CLASS.IN;
+const _ecsCode: number = Packet.EDNS_OPTION_CODE.ECS;
+
+// ── Packet static helpers ─────────────────────────────────────────────────────
+
+const buf = Buffer.alloc(12);
+const parsed: DNS.Packet = Packet.parse(buf);
+const response: DNS.Packet = Packet.createResponseFromRequest(parsed);
+response.header.rcode = 3; // NXDOMAIN
+
+const q: DNS.Packet.Question = parsed.questions[0];
+if (q) {
+ Packet.createResourceFromQuestion(q, { address: '1.2.3.4', ttl: 60 });
+}
+
+// ── Packet encode / round-trip ────────────────────────────────────────────────
+
+const pkt = new Packet();
+pkt.header.qr = 1;
+const encoded: Buffer = pkt.toBuffer();
+Packet.parse(encoded);
+
+// ── Multi-server (DnsServer) ──────────────────────────────────────────────────
+
+const server: DNS.DnsServer = DNS.createServer({
+ udp: true,
+ tcp: true,
+ maxConcurrent: 100,
+ handle: (request, send, _client) => {
+ const res = Packet.createResponseFromRequest(request);
+ res.header.rcode = 5; // REFUSED
+ void send(res);
+ },
+});
+
+server.listen({ udp: { port: 53 }, tcp: 5353 }).then((addrs: DNS.ServerAddresses) => {
+ const _udp: AddressInfo | undefined = addrs.udp;
+});
+
+server.on('request', (req, send, _client) => {
+ void send(Packet.createResponseFromRequest(req));
+});
+
+server.on('error', (err: Error, transport: 'udp' | 'tcp' | 'doh') => {
+ void err;
+ void transport;
+});
+
+void server.close();
+
+// ── Individual server factories ───────────────────────────────────────────────
+
+const udpServer: DNS.UDPServer = DNS.createUDPServer({ type: 'udp4' });
+udpServer.on('request', (req, send) => { void send(req); });
+
+const tcpServer: DNS.TCPServer = DNS.createTCPServer();
+tcpServer.on('request', (req, send) => { void send(req); });
+
+const dohServer: DNS.DOHServer = DNS.createDOHServer({ ssl: false, cors: true });
+dohServer.on('request', (req, send) => { void send(req); });
+
+// ── Client factories ──────────────────────────────────────────────────────────
+
+const udpClient: DNS.DnsResolver = DNS.UDPClient({ dns: '8.8.8.8', port: 53 });
+const tcpClient: DNS.DnsResolver = DNS.TCPClient({ dns: '8.8.8.8', protocol: 'tcp:' });
+const dohClient: DNS.DnsResolver = DNS.DOHClient({ dns: 'https://cloudflare-dns.com/dns-query' });
+const googleClient: DNS.DnsResolver = DNS.GoogleClient();
+
+void udpClient('example.com', 'A');
+void tcpClient('example.com', 'MX');
+void dohClient('example.com', 'AAAA');
+void googleClient('example.com');
+
+// ── Sub-class type assignability ──────────────────────────────────────────────
+
+const _dnsServer: DNS.DnsServer = DNS.createServer({ udp: true });
+const _udp: DNS.UDPServer = DNS.createUDPServer();
+const _tcp: DNS.TCPServer = DNS.createTCPServer();
+const _doh: DNS.DOHServer = DNS.createDOHServer();