import * as plugins from '../plugins.js'; import { DnsProviderDoc, DomainDoc, DnsRecordDoc } from '../db/documents/index.js'; import type { IDcRouterOptions } from '../classes.dcrouter.js'; import type { IDnsProviderClient } from './providers/interfaces.js'; import type { TDnsRecordType, TDnsRecordSource } from '../../dist_ts_interfaces/data/dns-record.js'; import type { TDnsProviderType, TDnsProviderCredentials, IDnsProviderPublic, IProviderDomainListing } from '../../dist_ts_interfaces/data/dns-provider.js'; import { type IDomainOwnershipZone, type TDomainOwnership } from './domain-ownership.js'; /** * Where a runtime DnsServer handler came from. * * - 'persisted' → rebuilt from a DnsRecordDoc row. * - 'generated-default' → synthesised by dcrouter (apex NS today; SOA and other * zone defaults would join this origin), so it has no DB row to enumerate and * must be torn down from the registry. */ export type TDnsRuntimeRegistrationOrigin = 'persisted' | 'generated-default'; /** One runtime DnsServer handler key owned by a DomainDoc. */ export interface IDnsRuntimeRegistration { name: string; type: TDnsRecordType; origin: TDnsRuntimeRegistrationOrigin; } export interface IDnsRecordDeleteResult { id: string; success: boolean; message?: string; } /** * DnsManager — owns runtime DNS state on top of the embedded DnsServer. * * Responsibilities: * - Load Domain/DnsRecord docs from the DB on start * - Register dcrouter-hosted domain records with smartdns.DnsServer at startup * - Provide CRUD methods used by OpsServer handlers (dcrouter-hosted domains hit * smartdns, provider domains hit the provider API) * - Expose a provider lookup used by the ACME DNS-01 wiring in setupSmartProxy() * * Provider-managed domains are NEVER served from the embedded DnsServer — the * provider stays authoritative. We only mirror their records locally for the UI * and to track providerRecordIds for updates / deletes. */ export declare class DnsManager { private options; /** * Reference to the active smartdns DnsServer (set by DcRouter once it exists). * May be undefined if dnsNsDomains isn't configured. */ dnsServer?: plugins.smartdns.dnsServerMod.DnsServer; /** * Cached provider clients, keyed by DnsProviderDoc.id. * Created lazily when a provider is first needed. */ private providerClients; /** * Per-domain registry of the runtime DnsServer handlers this manager owns, * keyed by DomainDoc.id then by `name|type`. * * Teardown must remove exactly what registration added — no more, no less. * Every key therefore retains all SmartDNS registration handles for the RRset; * the coarse `unregisterHandler(pattern, types)` API is never used. Recording * the actual registrations, with their origin, prevents deletion or authority * reconciliation from removing another owner's identical handler. */ private runtimeRegistrations; /** Serializes managed-mail reconciliation with destructive domain/provider deletion. */ private managedMailDnsMutationChain; private acceptsManagedMailDnsMutations; /** Supplies the delegation-verified authority set. */ private authorityZonesResolver?; constructor(options: IDcRouterOptions); runManagedMailDnsMutationExclusive(task: () => Promise): Promise; private trackRuntimeRegistration; private unregisterRuntimeRegistration; /** Registrations currently owned by a domain. Exposed for teardown assertions. */ listRuntimeRegistrations(domainId: string): IDnsRuntimeRegistration[]; /** * Unregister every runtime handler owned by a domain and forget them. * * Called from every path that ends dcrouter's authority over a zone. Without * it, deleting a DomainDoc removed the DB source of truth (so record queries * started answering REFUSED) while the generated apex NS handler kept answering * `aa` with our nameservers until the process was restarted — stale authority * live in memory, with nothing telling the next operator to restart. A restart * is not an acceptable delete procedure: it costs 30–60 s of total public * outage. */ private tearDownDomainRuntimeRegistrations; start(): Promise; stop(): Promise; /** * Wire the embedded DnsServer instance after it has been created by * DcRouter.setupDnsWithSocketHandler(). After this, local records on * dcrouter-hosted domains loaded from the DB are registered with the server. */ attachDnsServer(dnsServer: plugins.smartdns.dnsServerMod.DnsServer): Promise; /** Remove manager-owned handlers from the current server before replacement or shutdown. */ detachDnsServer(expectedServerArg?: plugins.smartdns.dnsServerMod.DnsServer): boolean; /** * Register all records from dcrouter-hosted domains in the DB with the * embedded DnsServer. Called once after attachDnsServer(). */ private applyDcrouterDomainsToDnsServer; /** * Authoritative zones must answer apex NS queries or their delegation is * lame — Let's Encrypt's DNS-01 resolver SERVFAILs on such zones. This is the * only place generated apex NS records come from, for every authoritative * zone: they are served from options.dnsNsDomains unless explicit apex NS * records exist in the DB. DnsServerRuntime used to emit a static set for * bootstrap `dnsScopes` zones, which could neither appear for a zone verified * after startup nor disappear for one whose authority was revoked. * * Ownership is verified first. SmartDNS suppresses default-authority handlers * outside the live authority set, and a zone only enters that set after its * delegation is proven, so registration and authority use the same evidence. */ private registerAuthoritativeZoneDefaults; /** Ownership inputs for every managed zone, fetched once per evaluation pass. */ listOwnershipZones(): Promise; /** * The authority set used as an ownership proof: the delegation-verified zones * held in the database, and nothing else. Empty until the resolver is wired, * which fails closed — an unwired manager proves ownership of nothing rather * than falling back to a declared list that no longer exists. */ getAuthorityZones(): string[]; /** * Supply the effective authority set. Injected rather than imported so * DnsManager keeps a single direction of dependency and stays unit-testable. */ setAuthorityZonesResolver(resolver?: () => string[]): void; /** * Re-derive every generated apex NS registration against the current authority * set. Zones that gained proof start being served; zones that lost it are torn * down — both in-process, with no restart. * * Throws on the first failure so the caller can roll the change back rather * than leave the router half-converted. */ reconcileAuthoritativeZones(): Promise<{ registered: string[]; unregistered: string[]; }>; /** Keep `DomainDoc.authoritative` honest after an authority change. */ syncAuthoritativeFlags(): Promise; /** * Can we prove the zone containing `fqdn` is ours? This gates certificate * requirements and authoritative DNS. See ts/dns/domain-ownership.ts. */ resolveDomainOwnership(fqdn: string): Promise; /** * Ownership of a zone that is about to become dcrouter-hosted, evaluated * against its post-write shape. Create and provider→dcrouter migration must * not read their own pre-write row: the provider link they are removing would * otherwise still count as the proof. */ private resolveOwnershipForPendingDcrouterZone; /** * Register a single record with the embedded DnsServer. The handler closure * captures the record fields, so updates require a re-register cycle. */ private registerRecordWithDnsServer; private rrsetKey; /** * Rebuild one authoritative RRset atomically from persisted rows. * * The unregister-then-rebuild cycle also replaces a generated default on the * same key (an explicit apex NS row supersedes the generated one), so the * registry is updated to match what is actually registered afterwards. */ private refreshLocalRrset; private parseRecordData; /** * Get the provider client for a given DnsProviderDoc id, instantiating * (and caching) it on first use. */ getProviderClientById(providerId: string): Promise; /** * Find the IDnsProviderClient that owns the given FQDN (by walking up its * labels to find a matching DomainDoc with `source === 'provider'`). * Returns null if no provider claims this FQDN. * * Used by: * - SmartAcme DNS-01 wiring in setupSmartProxy() * - DnsRecordHandler when creating provider records */ getProviderClientForDomain(fqdn: string): Promise; /** * Find the DomainDoc that covers a given FQDN, regardless of source * (dcrouter-hosted or provider-managed). Uses longest-suffix match. */ findDomainForFqdn(fqdn: string): Promise; /** * Delete DNS records matching a name and type under a domain. * When value is provided, only that exact record is removed so parallel ACME * challenges for the same host can coexist. */ deleteRecordsByNameAndType(domainId: string, name: string, type: TDnsRecordType, value?: string): Promise; /** * True if any domain is under management (dcrouter-hosted or provider-managed). * Used by setupSmartProxy() to decide whether to wire SmartAcme with a DNS-01 handler. */ hasAnyManagedDomain(): Promise; /** * Build an IConvenientDnsProvider that routes ACME DNS-01 challenges through * the DnsManager abstraction. Challenges are dispatched via createRecord() / * deleteRecord(), which transparently handle both dcrouter-hosted zones * (embedded DnsServer) and provider-managed zones (e.g. Cloudflare API). * * Only domains under management (with a DomainDoc in DB) are supported — * this acts as the management gate for certificate issuance. */ buildAcmeConvenientDnsProvider(): plugins.tsclass.network.IConvenientDnsProvider; listProviders(): Promise; getProvider(id: string): Promise; createProvider(args: { name: string; type: TDnsProviderType; credentials: TDnsProviderCredentials; createdBy: string; }): Promise; updateProvider(id: string, args: { name?: string; credentials?: TDnsProviderCredentials; }): Promise; deleteProvider(id: string, force: boolean): Promise<{ success: boolean; message?: string; }>; private deleteProviderUnlocked; testProvider(id: string): Promise<{ ok: boolean; error?: string; testedAt: number; }>; listProviderDomains(providerId: string): Promise; listDomains(): Promise; getDomain(id: string): Promise; /** * Create a dcrouter-hosted domain. dcrouter serves DNS records for it via the * embedded smartdns.DnsServer. * * `authoritative` reflects whether ownership can actually be proven — it used * to be hard-coded to `true`, which let an ops-API caller self-assert authority * over any zone on the internet. Creating the record still succeeds (it is the * container records and provider migration need), but an unverified zone is * recorded as non-authoritative and gets no generated apex NS handler. */ createDcrouterDomain(args: { name: string; description?: string; createdBy: string; }): Promise; /** * Import one or more domains from a provider, pulling all of their DNS * records into local DnsRecordDocs. */ importDomainsFromProvider(args: { providerId: string; domainNames: string[]; createdBy: string; }): Promise; updateDomain(id: string, args: { description?: string; }): Promise; /** * Delete a domain and all of its DNS records. For provider domains, only * removes the local mirror — does NOT touch the provider. * For dcrouter-hosted domains, also unregisters records from the embedded * DnsServer. * * The unregister path is complete in-process: every runtime handler this * manager registered for the domain — persisted RRsets and generated defaults * alike — is removed from the registry, so deletion never leaves stale * authority answering `aa` until an unrelated restart. Nameserver glue * handlers and other domains' handlers are never touched, because only the * domain's own recorded registrations are removed. */ deleteDomain(id: string): Promise; private deleteDomainUnlocked; private getManagedMailDnsReferences; private managedMailDnsReferenceMessage; /** * Force-resync a provider-managed domain: re-pull all records from the * provider API, replacing the cached DnsRecordDocs. */ syncDomain(id: string): Promise<{ success: boolean; recordCount?: number; listedProviderRecordIds?: string[]; message?: string; }>; listRecordsForDomain(domainId: string): Promise; getRecord(id: string): Promise; /** * Migrate a domain between dcrouter-hosted and provider-managed. * Transfers all records to the target and updates domain metadata. */ migrateDomain(args: { id: string; targetSource: 'dcrouter' | 'provider'; targetProviderId?: string; deleteExistingProviderRecords?: boolean; }): Promise<{ success: boolean; recordsMigrated?: number; message?: string; }>; /** * Migrate domain from dcrouter-hosted (or another provider) to an external DNS provider. */ private migrateToDnsProvider; /** * Migrate domain from provider-managed to dcrouter-hosted (authoritative). */ private migrateToDcrouter; createRecord(args: { domainId: string; name: string; type: TDnsRecordType; value: string; ttl?: number; proxied?: boolean; createdBy: string; managedBy?: string; managedOwnerId?: string; managedRecordKey?: string; }): Promise<{ success: boolean; id?: string; message?: string; }>; /** * Adopt an already-correct record into an automation lifecycle without * mutating provider DNS. The expected value fields make adoption fail closed * if the provider mirror changed between inspection and persistence. */ adoptRecordManagement(args: { id: string; name: string; type: TDnsRecordType; value: string; ttl: number; proxied: boolean; managedBy: string; managedOwnerId: string; managedRecordKey: string; }): Promise<{ success: boolean; message?: string; }>; updateRecord(args: { id: string; name?: string; type?: TDnsRecordType; value?: string; ttl?: number; proxied?: boolean; }): Promise<{ success: boolean; message?: string; }>; deleteRecord(id: string): Promise<{ success: boolean; message?: string; }>; deleteRecords(ids: string[]): Promise; private isProviderRecordNotFoundError; /** * Unregister a record's handler from the embedded DnsServer. */ unregisterRecordFromDnsServer(rec: DnsRecordDoc): void; private createSyncedRecord; /** * Convert a DnsProviderDoc to its public, secret-stripped representation * for the OpsServer API. */ toPublicProvider(doc: DnsProviderDoc): IDnsProviderPublic; /** * Convert a DomainDoc to its plain interface representation. */ toPublicDomain(doc: DomainDoc): { id: string; name: string; source: 'dcrouter' | 'provider'; providerId?: string; authoritative: boolean; nameservers?: string[]; externalZoneId?: string; lastSyncedAt?: number; description?: string; createdAt: number; updatedAt: number; createdBy: string; }; /** * Convert a DnsRecordDoc to its plain interface representation. */ toPublicRecord(doc: DnsRecordDoc): { id: string; domainId: string; name: string; type: TDnsRecordType; value: string; ttl: number; proxied?: boolean; source: TDnsRecordSource; providerRecordId?: string; createdAt: number; updatedAt: number; createdBy: string; }; }