zynor
v0.0.63
Published
A promise-based DNS resolver that aggregates multiple DNS-over-HTTPS (DoH) providers with concurrency control
Downloads
615
Maintainers
Readme
zynor
A DNS resolution and email intelligence library for Node.js. Zynor combines four DNS providers behind a single API with built-in concurrency control, caching, automatic failover, and a full-featured email validation engine.
Table of Contents
- Installation
- Quick Start
- DNS Resolution
- DNS Caching
- Provider Configuration
- Email Validation
- Error Handling
- TypeScript Support
- License
Installation
npm install zynoryarn add zynorpnpm add zynorRequirements: Node.js 18.0.0 or higher.
Quick Start
import { resolve4, resolveMx } from "zynor";
// Resolve IPv4 addresses
const ips = await resolve4("example.com");
// => ['93.184.216.34']
// Resolve mail servers
const mx = await resolveMx("example.com");
// => [{ priority: 10, exchange: 'mail.example.com' }]Import a function, pass a hostname, get results. No configuration needed.
For more control:
import { Zynor } from "zynor";
const dns = new Zynor({
google: { enabled: true, limit: { concurrency: 10 } },
cloudflare: { enabled: true, limit: { concurrency: 5 } },
quad9: { enabled: false },
native: { enabled: true },
});
const ips = await dns.resolve4("example.com");CommonJS is also supported:
const { resolve4, Zynor } = require("zynor");DNS Resolution
Standalone Functions
You don't need to create an instance or configure anything to start resolving DNS records. Every DNS method is exported as a standalone function you can import and call directly. Each function handles provider selection, queuing, caching, and error translation behind the scenes — just pass a hostname and get back structured results:
import {
resolve4, // A records (IPv4)
resolve6, // AAAA records (IPv6)
resolveMx, // MX records (mail servers)
resolveTxt, // TXT records (SPF, DKIM, verification)
resolveCname, // CNAME records (aliases)
resolveNs, // NS records (name servers)
resolveSoa, // SOA records (zone authority)
resolveSrv, // SRV records (service discovery)
resolvePtr, // PTR records (reverse DNS)
resolveCaa, // CAA records (certificate authority)
resolveNaptr, // NAPTR records
resolveTlsa, // TLSA records (DANE)
resolveAny, // ALL available records
reverse, // Reverse DNS lookup
resolve, // Generic resolve by record type
} from "zynor";Each function accepts an optional provider name as the last argument:
const ips = await resolve4("example.com", "google");
const mx = await resolveMx("example.com", "cloudflare");
const ns = await resolve("example.com", "NS");
const nsViaGoogle = await resolve("example.com", "NS", "google");Request TTL information for A and AAAA records:
const withTtl = await resolve4("example.com", { ttl: true });
// => [{ address: '93.184.216.34', ttl: 300 }]Zynor Class
The Zynor class gives full control over providers, concurrency, and caching:
import { Zynor } from "zynor";
const dns = new Zynor({
google: {
enabled: true,
limit: { concurrency: 10, interval: 1000, intervalCap: 500 },
},
cloudflare: {
enabled: true,
limit: { concurrency: 10 },
},
quad9: { enabled: false },
native: { enabled: true },
cache: {
enabled: true,
maxSize: 100_000,
dnsTtl: 1_440_000,
},
});
const ips = await dns.resolve4("example.com");
const mx = await dns.resolveMx("example.com", "google");
const txt = await dns.resolveTxt("example.com", { timeout: 5000 });Supported Record Types
| Record Type | Method | Returns | Description |
| ----------- | ---------------- | --------------- | ------------------------------------------------------------------ |
| A | resolve4() | string[] | IPv4 addresses for a domain |
| AAAA | resolve6() | string[] | IPv6 addresses for a domain |
| MX | resolveMx() | MxRecord[] | Mail servers with priority, used to find where email is handled |
| TXT | resolveTxt() | string[][] | Text records holding SPF rules, DKIM keys, and verification tokens |
| CNAME | resolveCname() | string[] | Canonical name aliases showing what a hostname points to |
| NS | resolveNs() | string[] | Authoritative name servers for a domain |
| SOA | resolveSoa() | SoaRecord | Zone serial number, refresh intervals, and admin contact |
| SRV | resolveSrv() | SrvRecord[] | Service discovery records with priority, weight, and port |
| PTR | resolvePtr() | string[] | Maps an IP address back to a hostname |
| CAA | resolveCaa() | CaaRecord[] | Which certificate authorities can issue certificates for a domain |
| NAPTR | resolveNaptr() | NaptrRecord[] | Complex service resolution for ENUM and SIP routing |
| TLSA | resolveTlsa() | TlsaRecord[] | DANE TLS certificate pinning |
| ANY | resolveAny() | AnyRecord[] | All available record types in a single query |
const mx = await resolveMx("gmail.com");
// => [
// { priority: 5, exchange: 'gmail-smtp-in.l.google.com' },
// { priority: 10, exchange: 'alt1.gmail-smtp-in.l.google.com' },
// ]
const soa = await resolveSoa("example.com");
// => {
// nsname: 'ns1.example.com',
// hostmaster: 'admin.example.com',
// serial: 2024010101,
// refresh: 3600,
// retry: 900,
// expire: 604800,
// minttl: 86400
// }
const srv = await resolveSrv("_sip._tcp.example.com");
// => [{ priority: 10, weight: 5, port: 5060, name: 'sip.example.com' }]
const hosts = await reverse("8.8.8.8");
// => ['dns.google']Provider Selection
When you don't specify which provider to use, zynor picks the best one for you automatically. It looks at how busy each provider's queue is and routes your request to the one with the most available capacity. This means that under heavy load, requests naturally spread across all your enabled providers rather than piling up on a single one — giving you faster response times and better reliability without any manual balancing on your part.
If you need a specific provider for a particular query (for example, Quad9 for its built-in malware filtering, or native for internal DNS resolution), you can always override the automatic selection:
const ips = await resolve4("example.com", "google");
const mx = await dns.resolveMx("example.com", "cloudflare");Four providers are included:
| Provider | Protocol | Best For |
| -------------- | ---------------------------- | ---------------------------------------------------- |
| native | Node.js dns/promises | Local/internal DNS, system DNS settings, development |
| google | DNS-over-HTTPS | Production, global services, DNSSEC validation |
| cloudflare | DNS-over-HTTPS | Privacy-sensitive apps, fast global resolution |
| quad9 | DNS-over-HTTPS (wire format) | Security-focused apps, built-in malware blocking |
AbortSignal and Timeout
Every DNS method in zynor can be cancelled mid-flight or given a time limit. This is essential for production applications where you can't afford to wait forever on a DNS query that might be hanging due to network issues. The default timeout is 30 seconds, but you can set your own per-request timeout, pass in an AbortSignal for manual cancellation, or combine both — whichever fires first will cancel the operation and free up resources immediately.
// Timeout after 5 seconds
const ips = await resolve4("example.com", { timeout: 5000 });
// Manual cancellation
const controller = new AbortController();
setTimeout(() => controller.abort(), 3000);
const mx = await resolveMx("example.com", { signal: controller.signal });
// Both together -- whichever fires first cancels the operation
const txt = await resolveTxt("example.com", {
signal: controller.signal,
timeout: 10000,
});Cancellation works across the entire operation: queue wait, network request, and response parsing.
DNS Caching
Request Deduplication
If your application fires off the same DNS query from multiple places at the same time — say, ten different parts of your code all resolve example.com within the same moment — zynor recognizes that these are duplicate requests and only sends a single query to the DNS provider. Every caller that asked for the same record receives the same result as soon as it comes back. This eliminates redundant network calls, reduces your DNS provider usage, and speeds up your application when multiple components depend on the same domain.
// Only ONE DNS query is made, all three get the same result
const [a, b, c] = await Promise.all([
resolve4("example.com"),
resolve4("example.com"),
resolve4("example.com"),
]);Results are then stored in an LRU cache so subsequent requests are served instantly without any network call.
LruCache
Zynor includes a general-purpose Least Recently Used (LRU) cache that you can use in your own application code — it's the same cache that powers zynor's internal DNS caching. An LRU cache automatically evicts the oldest unused entries when it reaches its maximum size, and each entry can have a time-to-live (TTL) so stale data expires on its own. This is useful for caching API responses, database queries, computed results, or anything else you want to keep in memory temporarily without worrying about unbounded growth:
import { LruCache } from "zynor";
const cache = new LruCache<string>(500); // max 500 entries
cache.set("key", "value", 60_000); // expires in 60 seconds
cache.get("key"); // => "value"
cache.peek("key"); // => "value" (without affecting eviction order)
cache.has("key"); // => true
cache.ttl("key"); // => remaining ms until expiry
cache.delete("key"); // => true
cache.size; // => 0
cache.prune(); // remove all expired entries, returns count
cache.clear(); // remove everything
// Iterate non-expired entries
for (const key of cache.keys()) {
/* ... */
}
for (const value of cache.values()) {
/* ... */
}
for (const [key, value] of cache.entries()) {
/* ... */
}Provider Configuration
Concurrency and Rate Limiting
Each DNS provider runs its own independent request queue, which means you have fine-grained control over how aggressively your application talks to each provider. You can set how many requests are allowed to be in flight at the same time (concurrency), define a rate-limit window (interval in milliseconds), and cap how many requests can be sent within that window (intervalCap). This prevents you from being throttled or banned by DNS providers and lets you tune performance based on your use case — high throughput for batch processing, or conservative limits for shared environments:
const dns = new Zynor({
google: {
enabled: true,
limit: {
concurrency: 10, // max 10 requests in flight at once
interval: 1000, // rate limit window in ms
intervalCap: 500, // max requests per window
carryoverConcurrencyCount: true,
},
},
cloudflare: {
enabled: true,
limit: { concurrency: 5 },
},
quad9: { enabled: false },
native: {
enabled: true,
limit: { concurrency: 10, interval: 1000, intervalCap: 750 },
},
});| Option | Type | Description |
| --------------------------------- | --------- | ----------------------------------------------- |
| enabled | boolean | Turn the provider on or off. Default: true. |
| limit.concurrency | number | Maximum requests running simultaneously. |
| limit.interval | number | Time window in ms for rate limiting. |
| limit.intervalCap | number | Maximum requests allowed per interval window. |
| limit.carryoverConcurrencyCount | boolean | Carry over concurrency count between intervals. |
Runtime Configuration Updates
You can reconfigure providers on the fly without having to tear down and recreate your Zynor instance. This is useful when you need to react to changing conditions — for example, disabling a provider that's returning errors, increasing concurrency during off-peak hours, or enabling a provider that was previously turned off. All in-flight requests continue with their original settings; only new requests pick up the changes:
dns.setConfig("google", { enabled: false });
dns.setConfig("cloudflare", { limit: { concurrency: 20 } });
dns.setConfigs({
google: { enabled: true, limit: { concurrency: 15 } },
cloudflare: { enabled: false },
});Email Validation
A full email intelligence engine that validates addresses, identifies the hosting provider, detects disposable and role-based emails, and resolves webmail URLs.
Basic Validation
Validates an email address in a single call — checking syntax, screening for disposable domains, detecting role-based prefixes like admin@ or support@, and identifying the email provider by domain or MX record lookup. Returns the provider name, free/paid status, role flag, and webmail URL when available.
import { Zynor } from "zynor";
const validator = Zynor.emailValidator;
const result = await validator.validate("[email protected]");
if (result.success) {
console.log(result.data.provider); // "Gmail"
console.log(result.data.email); // "[email protected]"
console.log(result.data.isFree); // true
console.log(result.data.role); // false
console.log(result.data.webmail); // "https://mail.google.com"
} else {
console.log(result.type); // "Syntax" | "Invalid" | "Rejected" | "Disposable" | "Error"
console.log(result.email); // the email that failed
}Validation checks syntax, inappropriate terms, rejected patterns (noreply, marketing senders, platform notifications), disposable domains, and then identifies the email provider through domain matching and MX record analysis.
Success response:
{
success: true,
data: {
provider: string, // "Gmail", "Office 365", "Zoho", etc.
email: string, // normalized (lowercased) email
isFree: boolean, // true for free providers (Gmail, Yahoo, etc.)
role: boolean, // true for admin@, support@, info@, etc.
webmail?: string, // webmail URL if known
}
}Error response:
{
success: false,
type: "Syntax" | "Rejected" | "Invalid" | "Disposable" | "Error",
email: string,
role: boolean,
message?: string, // only for "Error" type
}Deep Validation
When basic validation can't identify the provider, deep validation goes further — probing the domain's web presence and querying IP intelligence services to classify the hosting provider. The deep phase is time-capped (default 3 seconds) and results are cached, so repeated lookups are instant.
const validator = Zynor.createEmailValidator({
options: {
enableDeepValidation: true,
deepValidationOptions: {
maxTimeout: 3000, // max 3 seconds for the deep validation phase
http: { timeout: 2500, maxRetry: 0 },
},
ipResolver: {
ipApi: true,
findip: { enabled: true, apiKey: ["your-api-key"] },
},
},
});
const result = await validator.validate("[email protected]", true);Deep validation with abort support:
const result = await validator.validate("[email protected]", {
deep: true,
timeout: 5000,
signal: controller.signal,
});The deep phase is always capped by maxTimeout (default 3s) so it never blocks indefinitely.
Bulk Validation
Validate hundreds or thousands of email addresses concurrently with configurable parallelism. Results come back in the same order as the input array, making it easy to map results back to your original data:
const results = await Zynor.validateBulk(
["[email protected]", "[email protected]", "[email protected]"],
{
concurrency: 10,
deep: false,
timeout: 30000,
},
);
// results[0] => { success: true, data: { provider: "Gmail", ... } }
// results[2] => { success: false, type: "Disposable", ... }Results are returned in the same order as the input array. Concurrency defaults to the sum of all enabled provider concurrency limits.
Provider Detection
Zynor identifies 60+ email providers by direct domain matching and 100+ by analyzing MX record patterns. Known domains like gmail.com or outlook.com are detected instantly without any DNS query. For custom domains (like [email protected]), zynor looks up the domain's MX records to figure out which provider handles their email:
const validator = Zynor.emailValidator;
// Instant detection by domain (no DNS needed)
await validator.validate("[email protected]"); // provider: "Gmail"
await validator.validate("[email protected]"); // provider: "Outlook"
await validator.validate("[email protected]"); // provider: "ProtonMail"
await validator.validate("[email protected]"); // provider: "QQ"
await validator.validate("[email protected]"); // provider: "Mail.ru"
await validator.validate("[email protected]"); // provider: "iCloud"
await validator.validate("[email protected]"); // provider: "Zoho"
await validator.validate("[email protected]"); // provider: "Fastmail"
// Detection by MX record (requires one DNS lookup)
await validator.validate("[email protected]");
// MX points to Google => provider: "Gmail"
await validator.validate("[email protected]");
// MX points to Outlook => provider: "Office 365"
await validator.validate("[email protected]");
// MX points to Amazon SES => provider: "Amazon"Detected providers include Gmail, Outlook, Office 365, Yahoo, Turbify, AOL, iCloud, ProtonMail, Zoho, Mail.ru, Yandex, GMX, Web.de, Mail.com, Fastmail, AT&T, Comcast, QQ, 163, Naver, Daum, Rackspace, Mimecast, Godaddy, Namecheap, SendGrid, Mailgun, Postmark, Amazon, Elastic Email, Zendesk, Intermedia, Mailjet, Front, and many more.
Disposable Email Detection
Detects disposable (temporary) email domains and automated/marketing sender patterns:
const validator = Zynor.emailValidator;
validator.isDisposable("[email protected]"); // true
validator.isDisposable("[email protected]"); // true
validator.isDisposable("[email protected]"); // false
// Also caught during validation
const result = await validator.validate("[email protected]");
// => { success: false, type: "Disposable", ... }Additionally catches automated senders (noreply@, notifications@), marketing platforms (@mailchimp.com, @sendgrid.net, @hubspot.com), and platform notifications (GitHub, Amazon, Facebook, LinkedIn).
Role-Based Email Detection
Detects 250+ role-based prefixes -- addresses that represent a function rather than a person:
const result = await validator.validate("[email protected]");
console.log(result.success && result.data.role); // true
const result2 = await validator.validate("[email protected]");
console.log(result2.success && result2.data.role); // falseRecognized prefixes include admin, support, info, sales, billing, help, contact, hr, legal, marketing, security, postmaster, webmaster, noreply, abuse, careers, ceo, press, media, feedback, office, accounting, engineering, operations, and many more.
Free Domain Detection
Check whether an email or domain belongs to a free email provider:
validator.isFreeDomain("[email protected]"); // true
validator.isFreeDomain("yahoo.com"); // true
validator.isFreeDomain("[email protected]"); // false
// Also in validation results
const result = await validator.validate("[email protected]");
console.log(result.success && result.data.isFree); // trueWebmail URL Lookup
Resolve webmail login URLs for 500+ domains including regional variants:
const validator = Zynor.emailValidator;
validator.getProviderWebmailUrl("Gmail");
// => "https://mail.google.com"
validator.getProviderWebmailUrl("outlook.com");
// => "https://outlook.live.com/mail/"
validator.getProviderWebmailUrl("[email protected]");
// => "https://fr.mail.yahoo.com/"
validator.getProviderWebmailUrl("Gmail", "google.com");
// => "https://mail.google.com"
validator.getProviderWebmailUrl("unknown-provider.com");
// => nullRegional examples: yahoo.fr resolves to https://fr.mail.yahoo.com/, hotmail.de to https://outlook.live.com/mail/, gmx.at to https://www.gmx.at/mail/, att.net to https://currently.att.yahoo.com/.
Also included in validation results:
const result = await validator.validate("[email protected]");
console.log(result.success && result.data.webmail);
// => "https://mail.google.com"Email Extraction
Extract valid emails from mixed input formats:
const validator = Zynor.emailValidator;
// Plain emails
validator.extractEmails(["[email protected]"]);
// => ["[email protected]"]
// Comma-separated
validator.extractEmails(["[email protected],[email protected]"]);
// => ["[email protected]", "[email protected]"]
// URL-encoded
validator.extractEmails(["user%40example.com"]);
// => ["[email protected]"]
// JSON arrays
validator.extractEmails(['["[email protected]", "[email protected]"]']);
// => ["[email protected]", "[email protected]"]
// Mixed formats
validator.extractEmails([
"[email protected]",
"[email protected],[email protected]",
'["[email protected]"]',
]);
// => ["[email protected]", "[email protected]", "[email protected]", "[email protected]"]Utility methods:
validator.isEmail("[email protected]"); // true
validator.isEmail("[email protected]"); // false
validator.isJson('["[email protected]", "[email protected]"]');
// => ["[email protected]", "[email protected]"]
validator.isJson("not json");
// => nullPersistent Caching
Validation results are automatically cached to disk and survive process restarts:
- Results are cached at the domain level so different emails at the same domain share one entry.
- Entries expire after 48 hours.
- If a domain was cached as "Unknown" from basic validation, requesting deep validation bypasses the cache and performs a fresh lookup.
- Falls back to in-memory only if the disk cache directory is not writable.
Error Handling
Specific error classes for each failure mode:
import {
InvalidHostnameError,
NoEnabledProvidersError,
ProviderNotEnabledError,
InvalidProviderError,
} from "zynor";
try {
await resolve4("");
} catch (err) {
if (err instanceof InvalidHostnameError) {
// empty or invalid hostname
}
}
try {
await resolve4("example.com", "google");
} catch (err) {
if (err instanceof ProviderNotEnabledError) {
// provider is disabled
}
}DNS errors are returned as Node.js-compatible error objects with code, errno, syscall, and hostname:
try {
await resolve4("this-domain-does-not-exist.invalid");
} catch (err) {
err.code; // "ENOTFOUND"
err.syscall; // "queryA"
err.hostname; // "this-domain-does-not-exist.invalid"
}TypeScript Support
Written in TypeScript with every type exported:
import {
Zynor,
ZynorConfig,
ProviderConfig,
ProviderName,
RecordType,
AbortOptions,
MxRecord,
SoaRecord,
SrvRecord,
CaaRecord,
NaptrRecord,
TlsaRecord,
AnyRecord,
RecordWithTtl,
ResolveOptions,
ResolveWithTtlOptions,
LruCache,
CacheConfig,
EmailValidator,
EmailResponse,
ValidationConfig,
NoEnabledProvidersError,
InvalidHostnameError,
ProviderNotEnabledError,
InvalidProviderError,
} from "zynor";Full overload signatures with correct return types:
const strings: string[] = await resolve4("example.com");
const withTtl: RecordWithTtl[] = await resolve4("example.com", { ttl: true });
const mx: MxRecord[] = await resolveMx("example.com");
const soa: SoaRecord = await resolveSoa("example.com");