npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

zynor

v0.0.63

Published

A promise-based DNS resolver that aggregates multiple DNS-over-HTTPS (DoH) providers with concurrency control

Downloads

615

Readme

zynor

npm version TypeScript Node.js

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

npm install zynor
yarn add zynor
pnpm add zynor

Requirements: 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); // false

Recognized 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); // true

Webmail 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");
// => null

Regional 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");
// => null

Persistent 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");