import * as plugins from './plugins.js';
import { RustDnsBridge, } from './classes.rustdnsbridge.js';
export class DnsServer extends plugins.events.EventEmitter {
    constructor(options) {
        super();
        this.options = options;
        this.handlers = [];
        this.handlerIdCounter = 0;
        /** Patterns already warned about, so a hot loop cannot flood the log. */
        this.warnedOutOfZonePatterns = new Set();
        /** Stable for the lifetime of this server, so the apex SOA does not churn. */
        this.soaSerial = Math.floor(Date.now() / 1000);
        // Track initialization state
        this.bridgeSpawned = false;
        // Legacy server references (kept for backward-compatible test access)
        this.httpsServer = null;
        this.udpServer = null;
        this.tcpServer = null;
        this.bridge = new RustDnsBridge();
        // Wire up the dnsQuery event to run TypeScript handlers
        this.bridge.on('dnsQuery', async (event) => {
            try {
                const startTime = Date.now();
                const resolution = this.resolveQuery(event);
                const responseTimeMs = Date.now() - startTime;
                this.emit('query', {
                    questions: event.questions,
                    answered: resolution.answers.length > 0,
                    responseKind: resolution.responseKind,
                    authorityZone: resolution.authorityZone,
                    suppressedHandlers: resolution.suppressedHandlers,
                    responseTimeMs,
                    timestamp: startTime,
                });
                if (resolution.responseKind === 'refused' ||
                    resolution.suppressedHandlers.length > 0) {
                    this.emit('refused', {
                        questions: event.questions,
                        responseKind: resolution.responseKind,
                        authorityZone: resolution.authorityZone,
                        suppressedHandlers: resolution.suppressedHandlers,
                        timestamp: startTime,
                    });
                }
                await this.bridge.sendQueryResult(event.correlationId, resolution.answers, resolution.responseKind, resolution.authorityZone || undefined);
            }
            catch (err) {
                console.error('Error handling DNS query:', err);
                try {
                    await this.bridge.sendQueryResult(event.correlationId, [], 'servfail');
                }
                catch (sendErr) {
                    console.error('Error sending empty query result:', sendErr);
                }
            }
        });
    }
    /**
     * Register a DNS handler for a domain pattern and record types.
     *
     * Registering a handler is not by itself a claim of authority. Answers only
     * carry the AA bit when the queried name falls inside a configured
     * `authoritativeZones` entry. A handler left at the default
     * `authoritative` mode whose match falls outside every configured zone is
     * suppressed rather than answered; pass `{ authority: 'non-authoritative' }`
     * to serve such names deliberately, without claiming authority for them.
     *
     * @returns a handle identifying exactly this registration.
     */
    registerHandler(domainPattern, recordTypes, handler, options = {}) {
        const authority = options.authority || 'authoritative';
        const id = `dnsh_${++this.handlerIdCounter}`;
        const entry = {
            id,
            domainPattern,
            recordTypes: [...recordTypes],
            authority,
            owner: options.owner,
            handler,
        };
        this.handlers.push(entry);
        if (authority === 'authoritative' &&
            !this.patternCanMatchAuthoritativeZone(domainPattern) &&
            !this.warnedOutOfZonePatterns.has(domainPattern.toLowerCase())) {
            // Loud at the point of the mistake, rather than silently at query time.
            this.warnedOutOfZonePatterns.add(domainPattern.toLowerCase());
            console.warn(`smartdns: handler for '${domainPattern}' cannot match any configured authoritative zone ` +
                `(${this.normalizedAuthoritativeZones().join(', ') || 'none'}). Its answers will be ` +
                `suppressed. Add the zone to authoritativeZones, or register with ` +
                `{ authority: 'non-authoritative' } if this is a deliberate overlay.`);
        }
        return {
            id,
            domainPattern,
            recordTypes: entry.recordTypes,
            authority,
            owner: options.owner,
            unregister: () => this.unregisterHandlerById(id),
        };
    }
    /**
     * Unregister every handler registered for this exact pattern whose record
     * types cover all of `recordTypes`.
     *
     * This is deliberately coarse and cannot distinguish between two consumers
     * that registered the same pattern. Prefer the handle returned by
     * `registerHandler`, or `unregisterHandlerById` / `unregisterHandlersByOwner`.
     */
    unregisterHandler(domainPattern, recordTypes) {
        const initialLength = this.handlers.length;
        this.warnedOutOfZonePatterns.delete(domainPattern.toLowerCase());
        const normalizedDomainPattern = domainPattern.toLowerCase();
        const normalizedRecordTypes = recordTypes.map(type => type.toUpperCase());
        this.handlers = this.handlers.filter(handler => !(handler.domainPattern.toLowerCase() === normalizedDomainPattern &&
            normalizedRecordTypes.every(type => handler.recordTypes.map(recordType => recordType.toUpperCase()).includes(type))));
        return this.handlers.length < initialLength;
    }
    /**
     * Remove exactly one registration, by the id from `registerHandler`.
     * Leaves every other handler on the same pattern untouched.
     */
    unregisterHandlerById(handlerId) {
        const initialLength = this.handlers.length;
        this.handlers = this.handlers.filter((handler) => {
            if (handler.id !== handlerId) {
                return true;
            }
            // Release the warn-once slot so dynamic patterns cannot accumulate.
            this.warnedOutOfZonePatterns.delete(handler.domainPattern.toLowerCase());
            return false;
        });
        return this.handlers.length < initialLength;
    }
    /**
     * Remove every registration made with the given `owner`.
     * @returns the number of handlers removed.
     */
    unregisterHandlersByOwner(owner) {
        const initialLength = this.handlers.length;
        this.handlers = this.handlers.filter((handler) => {
            if (handler.owner !== owner) {
                return true;
            }
            // Release the warn-once slot so dynamic patterns cannot accumulate.
            this.warnedOutOfZonePatterns.delete(handler.domainPattern.toLowerCase());
            return false;
        });
        return initialLength - this.handlers.length;
    }
    /** Every currently registered handler, for introspection and leak checks. */
    listHandlers() {
        return this.handlers.map((handler) => ({
            id: handler.id,
            domainPattern: handler.domainPattern,
            recordTypes: [...handler.recordTypes],
            authority: handler.authority,
            owner: handler.owner,
        }));
    }
    /**
     * Start the DNS server.
     */
    async start() {
        // Validate interface addresses if provided
        const udpInterface = this.options.udpBindInterface || '0.0.0.0';
        const tcpInterface = this.options.tcpBindInterface || udpInterface;
        const httpsInterface = this.options.httpsBindInterface || '0.0.0.0';
        const tcpPort = this.options.tcpPort || this.options.udpPort;
        if (this.options.udpBindInterface && !this.isValidIpAddress(this.options.udpBindInterface)) {
            throw new Error(`Invalid UDP bind interface: ${this.options.udpBindInterface}`);
        }
        if (this.options.tcpBindInterface && !this.isValidIpAddress(this.options.tcpBindInterface)) {
            throw new Error(`Invalid TCP bind interface: ${this.options.tcpBindInterface}`);
        }
        if (this.options.httpsBindInterface && !this.isValidIpAddress(this.options.httpsBindInterface)) {
            throw new Error(`Invalid HTTPS bind interface: ${this.options.httpsBindInterface}`);
        }
        // Spawn the Rust binary
        if (!this.bridgeSpawned) {
            const spawned = await this.bridge.spawn();
            if (!spawned) {
                throw new Error('Failed to spawn rustdns binary');
            }
            this.bridgeSpawned = true;
        }
        // Build config for Rust
        const config = {
            udpPort: this.options.udpPort,
            tcpPort,
            httpsPort: this.options.httpsPort,
            udpBindInterface: udpInterface,
            tcpBindInterface: tcpInterface,
            httpsBindInterface: httpsInterface,
            httpsKey: this.options.httpsKey || '',
            httpsCert: this.options.httpsCert || '',
            dnssecZone: this.options.dnssecZone,
            dnssecAlgorithm: 'ECDSA',
            primaryNameserver: this.options.primaryNameserver || `ns1.${this.options.dnssecZone}`,
            soaSerial: this.soaSerial,
            enableLocalhostHandling: this.options.enableLocalhostHandling !== false,
            manualUdpMode: this.options.manualUdpMode || false,
            manualTcpMode: this.options.manualTcpMode || false,
            manualHttpsMode: this.options.manualHttpsMode || false,
        };
        try {
            await this.bridge.startServer(config);
        }
        catch (error) {
            this.bridge.kill();
            this.bridgeSpawned = false;
            throw error;
        }
        // Set legacy markers for backward-compatible test checks
        this.httpsServer = { _rustBridgeManaged: true };
        this.udpServer = { _rustBridgeManaged: true };
        this.tcpServer = { _rustBridgeManaged: true };
        const udpManual = this.options.manualUdpMode || false;
        const tcpManual = this.options.manualTcpMode || false;
        const httpsManual = this.options.manualHttpsMode || false;
        if (udpManual && tcpManual && httpsManual) {
            console.log('DNS server started in full manual mode - ready to accept connections');
        }
        else if (udpManual || tcpManual || httpsManual) {
            const activeTransports = [];
            const manualTransports = [];
            udpManual ? manualTransports.push('UDP') : activeTransports.push(`UDP ${udpInterface}:${this.options.udpPort}`);
            tcpManual ? manualTransports.push('TCP') : activeTransports.push(`TCP ${tcpInterface}:${tcpPort}`);
            httpsManual ? manualTransports.push('HTTPS') : activeTransports.push(`HTTPS ${httpsInterface}:${this.options.httpsPort}`);
            console.log(`DNS server started (${activeTransports.join(', ')}${manualTransports.length ? `; manual: ${manualTransports.join(', ')}` : ''})`);
        }
        else {
            console.log(`DNS server started (UDP: ${udpInterface}:${this.options.udpPort}, TCP: ${tcpInterface}:${tcpPort}, HTTPS: ${httpsInterface}:${this.options.httpsPort})`);
        }
    }
    /**
     * Stop the DNS server.
     */
    async stop() {
        if (this.bridgeSpawned) {
            try {
                await this.bridge.stopServer();
            }
            catch (err) {
                // Ignore errors during stop (process may have already exited)
            }
            this.bridge.kill();
            this.bridgeSpawned = false;
        }
        this.httpsServer = null;
        this.udpServer = null;
        this.tcpServer = null;
    }
    /**
     * Initialize servers (no-op with Rust bridge, kept for API compatibility).
     */
    initializeServers() {
        // No-op — Rust bridge handles server initialization via start()
    }
    /**
     * Initialize UDP server (no-op with Rust bridge).
     */
    initializeUdpServer() {
        // No-op
    }
    /**
     * Initialize HTTPS server (no-op with Rust bridge).
     */
    initializeHttpsServer() {
        // No-op
    }
    /**
     * Handle a raw TCP socket for HTTPS/DoH.
     * In Rust mode, this is not directly supported — use processRawDnsPacket instead.
     */
    handleHttpsSocket(socket) {
        console.warn('handleHttpsSocket: direct socket handling not available with Rust bridge. Use processRawDnsPacket instead.');
        socket.end();
    }
    /**
     * Handle a UDP message manually.
     */
    handleUdpMessage(msg, rinfo, responseCallback) {
        // Async processing via Rust bridge. This is a UDP datagram, so the
        // response must be truncated to the client's advertised payload size.
        this.processRawDnsPacketAsync(msg, 'udp')
            .then((responseData) => {
            if (responseCallback) {
                responseCallback(responseData, rinfo);
            }
        })
            .catch((err) => {
            console.error('Error processing UDP DNS request:', err);
        });
    }
    /**
     * Process a raw DNS packet asynchronously via Rust bridge.
     *
     * Pass `transport: 'udp'` when the response will be sent as a UDP
     * datagram: the response is then truncated (TC bit set) to the client's
     * advertised EDNS0 payload size, or 512 bytes without EDNS0. Without it,
     * no size bound is applied — correct for TCP and DoH framing.
     */
    async processRawDnsPacketAsync(packet, transport) {
        if (!this.bridgeSpawned) {
            throw new Error('DNS server not started — call start() first');
        }
        return this.bridge.processPacket(packet, transport);
    }
    /**
     * Retrieve SSL certificate for specified domains using Let's Encrypt
     */
    async retrieveSslCertificate(domainNames, options = {}) {
        const opts = {
            email: options.email || 'admin@example.com',
            staging: options.staging !== undefined ? options.staging : false,
            certDir: options.certDir || './certs'
        };
        if (!plugins.fs.existsSync(opts.certDir)) {
            plugins.fs.mkdirSync(opts.certDir, { recursive: true });
        }
        const authorizedDomains = this.filterAuthorizedDomains(domainNames);
        if (authorizedDomains.length === 0) {
            console.error('None of the provided domains are authorized for this DNS server');
            return { cert: '', key: '', success: false };
        }
        console.log(`Retrieving SSL certificate for domains: ${authorizedDomains.join(', ')}`);
        // Declared outside the try so a failure part-way through cannot leave
        // challenge handlers registered.
        const challengeHandlers = [];
        try {
            // @ts-ignore - acmeClientOverride is added for testing purposes
            const acmeClient = this.acmeClientOverride || await import('acme-client');
            const accountKeyPath = plugins.path.join(opts.certDir, 'account.key');
            let accountKey;
            if (plugins.fs.existsSync(accountKeyPath)) {
                accountKey = plugins.fs.readFileSync(accountKeyPath);
            }
            else {
                const { privateKey } = plugins.crypto.generateKeyPairSync('rsa', {
                    modulusLength: 2048,
                    publicKeyEncoding: { type: 'spki', format: 'pem' },
                    privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
                });
                accountKey = Buffer.from(privateKey);
                plugins.fs.writeFileSync(accountKeyPath, accountKey);
            }
            const client = new acmeClient.Client({
                directoryUrl: opts.staging
                    ? acmeClient.directory.letsencrypt.staging
                    : acmeClient.directory.letsencrypt.production,
                accountKey: accountKey
            });
            await client.createAccount({
                termsOfServiceAgreed: true,
                contact: [`mailto:${opts.email}`]
            });
            const order = await client.createOrder({
                identifiers: authorizedDomains.map(domain => ({
                    type: 'dns',
                    value: domain
                }))
            });
            const authorizations = await client.getAuthorizations(order);
            for (const auth of authorizations) {
                const domain = auth.identifier.value;
                const challenge = auth.challenges.find((c) => c.type === 'dns-01');
                if (!challenge) {
                    throw new Error(`No DNS-01 challenge found for ${domain}`);
                }
                const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
                const recordValue = this.getDnsRecordValueForChallenge(keyAuthorization);
                const challengeDomain = `_acme-challenge.${domain}`;
                console.log(`Setting up TXT record for ${challengeDomain}: ${recordValue}`);
                const challengeRegistration = this.registerHandler(challengeDomain, ['TXT'], (question) => {
                    if (question.name === challengeDomain && question.type === 'TXT') {
                        return {
                            name: question.name,
                            type: 'TXT',
                            class: 'IN',
                            ttl: 300,
                            data: [recordValue]
                        };
                    }
                    return null;
                });
                challengeHandlers.push({ domain, registration: challengeRegistration });
                await new Promise(resolve => setTimeout(resolve, 2000));
                await client.completeChallenge(challenge);
                await client.waitForValidStatus(challenge);
                console.log(`Challenge for ${domain} validated successfully!`);
            }
            const domainKeyPath = plugins.path.join(opts.certDir, `${authorizedDomains[0]}.key`);
            const { privateKey } = plugins.crypto.generateKeyPairSync('rsa', {
                modulusLength: 2048,
                publicKeyEncoding: { type: 'spki', format: 'pem' },
                privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
            });
            plugins.fs.writeFileSync(domainKeyPath, privateKey);
            const csrResult = await acmeClient.forge.createCsr({
                commonName: authorizedDomains[0],
                altNames: authorizedDomains
            });
            await client.finalizeOrder(order, csrResult.csr);
            const certificate = await client.getCertificate(order);
            const certPath = plugins.path.join(opts.certDir, `${authorizedDomains[0]}.cert`);
            plugins.fs.writeFileSync(certPath, certificate);
            this.options.httpsCert = certificate;
            this.options.httpsKey = privateKey;
            // Update certs on Rust bridge if running
            if (this.bridgeSpawned) {
                try {
                    await this.bridge.updateCerts(privateKey, certificate);
                }
                catch (err) {
                    console.error('Error updating certs on Rust bridge:', err);
                }
            }
            return {
                cert: certificate,
                key: privateKey,
                success: true
            };
        }
        catch (error) {
            console.error('Error retrieving SSL certificate:', error);
            return { cert: '', key: '', success: false };
        }
        finally {
            for (const handler of challengeHandlers) {
                // Remove exactly this challenge handler. The coarse
                // unregisterHandler(pattern, types) would also take out any other
                // TXT handler a consumer had registered on the same name.
                handler.registration.unregister();
                console.log(`Cleaned up challenge handler for ${handler.domain}`);
            }
        }
    }
    /**
     * Filter domains to include only those the server is authoritative for.
     */
    filterAuthorizedDomains(domainNames) {
        const authorizedDomains = [];
        for (const domain of domainNames) {
            if (domain.startsWith('*.')) {
                const baseDomain = domain.substring(2);
                if (this.isAuthorizedForDomain(baseDomain)) {
                    authorizedDomains.push(domain);
                }
            }
            else if (this.isAuthorizedForDomain(domain)) {
                authorizedDomains.push(domain);
            }
        }
        return authorizedDomains;
    }
    // ── Private helpers ───────────────────────────────────────────────
    /**
     * Resolve a DNS query event from Rust using TypeScript handlers.
     *
     * Authority is decided by configuration, never by the presence of a
     * handler. A handler match is only served as an authoritative answer when
     * the queried name falls inside a configured authoritative zone.
     */
    resolveQuery(event) {
        const answers = [];
        const suppressedHandlers = [];
        const authorityZone = this.getAuthorityZone(event);
        let sawNonAuthoritativeAnswer = false;
        let nameExists = false;
        for (const q of event.questions) {
            const normalizedQuestionName = this.normalizeName(q.name);
            const normalizedQuestionType = q.type.toUpperCase();
            const questionZone = this.zoneForName(normalizedQuestionName);
            // RFC 8020: NXDOMAIN asserts that nothing exists at or below the name,
            // so it is only safe when the name is not the zone apex (which always
            // exists — it answers its own SOA) and no handler pattern can match at
            // or below it (an empty non-terminal exists by virtue of the names
            // beneath it).
            if (questionZone &&
                (normalizedQuestionName === questionZone ||
                    this.nameHasCoveredDescendants(normalizedQuestionName))) {
                nameExists = true;
            }
            const question = {
                name: normalizedQuestionName,
                type: normalizedQuestionType,
                class: q.class,
            };
            for (const handlerEntry of this.handlers) {
                const normalizedDomainPattern = handlerEntry.domainPattern.toLowerCase();
                if (!plugins.minimatch.minimatch(normalizedQuestionName, normalizedDomainPattern)) {
                    continue;
                }
                // The name is served by *some* handler, which is what separates
                // NODATA from NXDOMAIN below.
                nameExists = true;
                const normalizedRecordTypes = handlerEntry.recordTypes.map(recordType => recordType.toUpperCase());
                if (!normalizedRecordTypes.includes(normalizedQuestionType)) {
                    continue;
                }
                if (handlerEntry.authority === 'authoritative' && !questionZone) {
                    // Answering here would publish a claim of authority over a
                    // delegation this server does not hold. Withhold it, and make the
                    // withholding attributable.
                    suppressedHandlers.push({
                        questionName: normalizedQuestionName,
                        questionType: normalizedQuestionType,
                        domainPattern: handlerEntry.domainPattern,
                        handlerId: handlerEntry.id,
                        owner: handlerEntry.owner,
                    });
                    continue;
                }
                const answer = handlerEntry.handler(question);
                if (answer) {
                    answers.push({
                        name: answer.name,
                        type: answer.type,
                        class: typeof answer.class === 'number' ? 'IN' : (answer.class || 'IN'),
                        ttl: answer.ttl || 300,
                        data: answer.data,
                    });
                    if (handlerEntry.authority === 'non-authoritative' || !questionZone) {
                        sawNonAuthoritativeAnswer = true;
                    }
                }
            }
            // An authoritative server must be able to answer SOA for its own apex.
            if (answers.length === 0 &&
                normalizedQuestionType === 'SOA' &&
                questionZone &&
                normalizedQuestionName === questionZone) {
                answers.push(this.buildApexSoaAnswer(questionZone));
            }
        }
        if (answers.length > 0) {
            // AA is a property of the message, so a single non-authoritative
            // contribution downgrades the whole response.
            return {
                answers,
                responseKind: authorityZone && !sawNonAuthoritativeAnswer ? 'answer' : 'nonAuthoritativeAnswer',
                authorityZone,
                suppressedHandlers,
            };
        }
        if (authorityZone) {
            // NXDOMAIN only when the name provably does not exist: no handler
            // serves it, it is not a zone apex, and nothing can exist below it.
            // Otherwise the safe answer is NODATA, which never poisons a negative
            // cache for names or types this server does happen to hold.
            return {
                answers,
                responseKind: nameExists ? 'authoritativeNegative' : 'authoritativeNameError',
                authorityZone,
                suppressedHandlers,
            };
        }
        return { answers, responseKind: 'refused', authorityZone: null, suppressedHandlers };
    }
    /** Strip the trailing root dot and case, so matching is RFC 4343 correct. */
    normalizeName(name) {
        return name.toLowerCase().replace(/\.$/, '');
    }
    /** Configured authoritative zones, most specific first. */
    normalizedAuthoritativeZones() {
        return (this.options.authoritativeZones?.length
            ? this.options.authoritativeZones
            : [this.options.dnssecZone])
            .map((zone) => this.normalizeName(zone))
            .filter(Boolean)
            .sort((a, b) => b.length - a.length);
    }
    /** The most specific configured zone containing `name`, if any. */
    zoneForName(name) {
        const normalizedName = this.normalizeName(name);
        return (this.normalizedAuthoritativeZones().find((zone) => normalizedName === zone || normalizedName.endsWith(`.${zone}`)) || null);
    }
    /**
     * Whether a pattern is anchored inside a configured authoritative zone.
     * Used only to warn at registration time; query-time authority is always
     * decided from the actual question name, never from this.
     */
    patternCanMatchAuthoritativeZone(domainPattern) {
        return this.zoneForName(domainPattern) !== null;
    }
    /**
     * Whether something this server serves exists strictly below `name`.
     * Such a name is an empty non-terminal: it holds no records of its own,
     * but names beneath it exist, so the correct negative answer for it is
     * NODATA — never NXDOMAIN, which would deny the whole subtree (RFC 8020).
     *
     * Descendants exist when a configured authoritative zone sits below the
     * name (its apex exists by configuration), or when a handler pattern is
     * anchored below it (literal `.name` suffix, or a wildcard that matches a
     * probe label). Mid-label wildcard patterns cannot be fully inverted by
     * this check; see readme.hints.md.
     */
    nameHasCoveredDescendants(name) {
        if (this.normalizedAuthoritativeZones().some((zone) => zone.endsWith(`.${name}`))) {
            return true;
        }
        return this.handlers.some((handlerEntry) => {
            const pattern = handlerEntry.domainPattern.toLowerCase();
            return (pattern.endsWith(`.${name}`) ||
                plugins.minimatch.minimatch(`_ent_probe_.${name}`, pattern));
        });
    }
    /** The SOA this server publishes at the apex of a zone it is authoritative for. */
    buildApexSoaAnswer(zone) {
        return {
            name: zone,
            type: 'SOA',
            class: 'IN',
            ttl: 3600,
            data: {
                mname: this.options.primaryNameserver || `ns1.${this.options.dnssecZone}`,
                rname: `hostmaster.${zone}`,
                serial: this.soaSerial,
                refresh: 3600,
                retry: 600,
                expire: 604800,
                minimum: 86400,
            },
        };
    }
    /** Resolve the most specific configured authority shared by all questions. */
    getAuthorityZone(event) {
        if (event.questions.length === 0)
            return null;
        const matchedZones = event.questions.map((question) => this.zoneForName(question.name));
        const matchedZone = matchedZones[0];
        return matchedZone && matchedZones.every((zone) => zone === matchedZone)
            ? matchedZone
            : null;
    }
    getDnsRecordValueForChallenge(keyAuthorization) {
        const digest = plugins.crypto
            .createHash('sha256')
            .update(keyAuthorization)
            .digest('base64')
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=/g, '');
        return digest;
    }
    isValidIpAddress(ip) {
        const ipv4Pattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
        const ipv6Pattern = /^(::1|::)$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
        return ipv4Pattern.test(ip) || ipv6Pattern.test(ip);
    }
    isAuthorizedForDomain(domain) {
        // A DNS-01 challenge only completes when the `_acme-challenge.` TXT
        // handler can answer authoritatively, and authority comes solely from
        // configured zones — handler registrations play no part. A domain is in
        // the same configured zone as its challenge name, so the zone lookup on
        // the domain itself is the whole check.
        return this.zoneForName(domain) !== null;
    }
}
//# sourceMappingURL=data:application/json;base64, 