# zynor

[![npm version](https://badge.fury.io/js/zynor.svg)](https://badge.fury.io/js/zynor)
[![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
[![Node.js](https://img.shields.io/badge/Node.js-18%2B-green.svg)](https://nodejs.org/)

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](#installation)
- [Quick Start](#quick-start)
- [DNS Resolution](#dns-resolution)
  - [Standalone Functions](#standalone-functions)
  - [Zynor Class](#zynor-class)
  - [Supported Record Types](#supported-record-types)
  - [Provider Selection](#provider-selection)
  - [AbortSignal and Timeout](#abortsignal-and-timeout)
- [DNS Caching](#dns-caching)
  - [Request Deduplication](#request-deduplication)
  - [LruCache](#lrucache)
- [Provider Configuration](#provider-configuration)
  - [Concurrency and Rate Limiting](#concurrency-and-rate-limiting)
  - [Runtime Configuration Updates](#runtime-configuration-updates)
- [Email Validation](#email-validation)
  - [Basic Validation](#basic-validation)
  - [Deep Validation](#deep-validation)
  - [Bulk Validation](#bulk-validation)
  - [Provider Detection](#provider-detection)
  - [Disposable Email Detection](#disposable-email-detection)
  - [Role-Based Email Detection](#role-based-email-detection)
  - [Free Domain Detection](#free-domain-detection)
  - [Webmail URL Lookup](#webmail-url-lookup)
  - [Email Extraction](#email-extraction)
  - [Persistent Caching](#persistent-caching)
- [Error Handling](#error-handling)
- [TypeScript Support](#typescript-support)
- [License](#license)

---

## Installation

```bash
npm install zynor
```

```bash
yarn add zynor
```

```bash
pnpm add zynor
```

**Requirements:** Node.js 18.0.0 or higher.

---

## Quick Start

```typescript
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:

```typescript
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:

```javascript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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                       |

```typescript
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:

```typescript
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.

```typescript
// 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.

```typescript
// 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:

```typescript
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:

```typescript
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:

```typescript
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.

```typescript
import { Zynor } from "zynor";

const validator = Zynor.emailValidator;

const result = await validator.validate("user@gmail.com");

if (result.success) {
  console.log(result.data.provider); // "Gmail"
  console.log(result.data.email); // "user@gmail.com"
  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:**

```typescript
{
  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:**

```typescript
{
  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.

```typescript
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("employee@obscure-company.com", true);
```

Deep validation with abort support:

```typescript
const result = await validator.validate("user@company.com", {
  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:

```typescript
const results = await Zynor.validateBulk(
  ["user@gmail.com", "admin@company.com", "test@mailinator.com"],
  {
    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 `user@company.com`), zynor looks up the domain's MX records to figure out which provider handles their email:

```typescript
const validator = Zynor.emailValidator;

// Instant detection by domain (no DNS needed)
await validator.validate("user@gmail.com"); // provider: "Gmail"
await validator.validate("user@outlook.com"); // provider: "Outlook"
await validator.validate("user@protonmail.com"); // provider: "ProtonMail"
await validator.validate("user@qq.com"); // provider: "QQ"
await validator.validate("user@mail.ru"); // provider: "Mail.ru"
await validator.validate("user@icloud.com"); // provider: "iCloud"
await validator.validate("user@zoho.com"); // provider: "Zoho"
await validator.validate("user@fastmail.com"); // provider: "Fastmail"

// Detection by MX record (requires one DNS lookup)
await validator.validate("user@company.com");
// MX points to Google => provider: "Gmail"

await validator.validate("user@enterprise.org");
// MX points to Outlook => provider: "Office 365"

await validator.validate("user@startup.io");
// 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:

```typescript
const validator = Zynor.emailValidator;

validator.isDisposable("test@mailinator.com"); // true
validator.isDisposable("user@guerrillamail.com"); // true
validator.isDisposable("user@gmail.com"); // false

// Also caught during validation
const result = await validator.validate("test@tempmail.com");
// => { 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:

```typescript
const result = await validator.validate("admin@company.com");
console.log(result.success && result.data.role); // true

const result2 = await validator.validate("john.doe@company.com");
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:

```typescript
validator.isFreeDomain("user@gmail.com"); // true
validator.isFreeDomain("yahoo.com"); // true
validator.isFreeDomain("user@company.com"); // false

// Also in validation results
const result = await validator.validate("user@gmail.com");
console.log(result.success && result.data.isFree); // true
```

### Webmail URL Lookup

Resolve webmail login URLs for 500+ domains including regional variants:

```typescript
const validator = Zynor.emailValidator;

validator.getProviderWebmailUrl("Gmail");
// => "https://mail.google.com"

validator.getProviderWebmailUrl("outlook.com");
// => "https://outlook.live.com/mail/"

validator.getProviderWebmailUrl("user@yahoo.fr");
// => "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:

```typescript
const result = await validator.validate("user@gmail.com");
console.log(result.success && result.data.webmail);
// => "https://mail.google.com"
```

### Email Extraction

Extract valid emails from mixed input formats:

```typescript
const validator = Zynor.emailValidator;

// Plain emails
validator.extractEmails(["user@example.com"]);
// => ["user@example.com"]

// Comma-separated
validator.extractEmails(["a@test.com,b@test.com"]);
// => ["a@test.com", "b@test.com"]

// URL-encoded
validator.extractEmails(["user%40example.com"]);
// => ["user@example.com"]

// JSON arrays
validator.extractEmails(['["a@test.com", "b@test.com"]']);
// => ["a@test.com", "b@test.com"]

// Mixed formats
validator.extractEmails([
  "one@test.com",
  "two@test.com,three@test.com",
  '["four@test.com"]',
]);
// => ["one@test.com", "two@test.com", "three@test.com", "four@test.com"]
```

Utility methods:

```typescript
validator.isEmail("john.doe@example.com"); // true
validator.isEmail("x@y.co"); // false

validator.isJson('["a@test.com", "b@test.com"]');
// => ["a@test.com", "b@test.com"]

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:

```typescript
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`:

```typescript
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:

```typescript
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:

```typescript
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");
```

---
