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();