whoispk 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,108 +1,176 @@
1
- whoispk
2
- =======
1
+ <div align="center">
3
2
 
4
- [![npm version](http://img.shields.io/npm/v/whoispk.svg?style=flat)](https://npmjs.org/package/whoispk "View this project on npm")
5
- [![npm](https://img.shields.io/npm/dt/whoispk.svg)]()
6
- [![Build Status](https://travis-ci.org/abbassiddiqi/whoispk.svg?branch=master)](https://travis-ci.org/abbassiddiqi/whoispk)
7
- [![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg?colorB=9b59b6)](http://opensource.org/licenses/MIT)
3
+ # whoispk
8
4
 
9
- Issue WHOIS lookup for PKNIC (.pk) domain names from the command line.
5
+ **Check whether a `.pk` domain is taken, from your terminal or from Node.js.**
10
6
 
11
- ## Installation
7
+ [![npm version](https://img.shields.io/npm/v/whoispk.svg)](https://www.npmjs.com/package/whoispk)
8
+ [![npm downloads](https://img.shields.io/npm/dt/whoispk.svg)](https://www.npmjs.com/package/whoispk)
9
+ [![CI](https://github.com/abbassiddiqi/whoispk/actions/workflows/ci.yml/badge.svg)](https://github.com/abbassiddiqi/whoispk/actions/workflows/ci.yml)
10
+ [![node](https://img.shields.io/node/v/whoispk.svg)](package.json)
11
+ [![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](package.json)
12
+ [![MIT license](https://img.shields.io/badge/license-MIT-9b59b6.svg)](LICENSE)
12
13
 
13
- ### Global
14
+ <img src="docs/assets/demo.svg" alt="whoispk checking three domains: daraz.pk is registered, magicnotatall.pk is available, x.pk is not registrable" width="720">
15
+
16
+ </div>
17
+
18
+ `.pk` domains don't have a public WHOIS server, so the usual `whois` command can't look them up. whoispk asks PKNIC, the `.pk` registry, and gives you a clear answer: **registered**, **available**, or **not registrable**. It also shows the creation date, expiry date and nameservers.
19
+
20
+ ## Quick start
21
+
22
+ No install needed:
23
+
24
+ ```sh
25
+ npx whoispk yourbrand
26
+ ```
27
+
28
+ `.pk` is added for you. `yourbrand`, `yourbrand.pk` and `https://www.yourbrand.pk/` all look up the same domain, and `.com.pk`, `.org.pk` and the other second-level names work too.
29
+
30
+ To have the `whoispk` command always available:
14
31
 
15
32
  ```sh
16
33
  npm install -g whoispk
17
34
  ```
18
35
 
19
- #### Usage
36
+ ## Features
20
37
 
21
- whoispk [options] address
38
+ - **Clear answers.** Each domain is registered, available, or not registrable. For a name PKNIC rejects, you get its reason.
39
+ - **Many domains at once.** Pass several names, or pipe in a list. Lookups run in parallel and print in your order.
40
+ - **Made for scripts.** `--json` output, exit codes you can rely on, and no spinner in piped output.
41
+ - **Also a library.** A promise-based API with TypeScript types, timeouts, retries and cancelling.
42
+ - **Small.** No dependencies, about 10 kB installed. It uses Node's built-in `fetch`.
43
+ - **Watched for breakage.** Tests run every week against the real PKNIC site, so a change to their page gets noticed quickly.
22
44
 
23
- Options:
24
- -V, --version output the version number
25
- -h, --help output usage information
45
+ ## Recipes
26
46
 
27
- ### Local
47
+ **Check a shortlist of name ideas**, one per line (`#` starts a comment):
28
48
 
29
49
  ```sh
30
- npm install whoispk
50
+ whoispk --json < ideas.txt | jq -r '.[] | select(.available) | .domain'
31
51
  ```
32
52
 
33
- #### Usage
53
+ **Use it in a shell script:**
54
+
55
+ ```sh
56
+ if whoispk --json chaiwala | jq -e '.available' > /dev/null; then
57
+ echo "chaiwala.pk is free. Register it!"
58
+ fi
59
+ ```
60
+
61
+ **Warn before your domains expire:**
34
62
 
35
63
  ```js
36
- const whoispk = require('whoispk');
64
+ import { lookup } from 'whoispk';
37
65
 
38
- whoispk.lookup('daraz.pk', function (err, domainInfo) {
66
+ for (const name of ['mybrand.pk', 'mybrand.com.pk']) {
67
+ const r = await lookup(name);
68
+ if (!r.registered) continue;
69
+
70
+ const days = Math.round((Date.parse(r.expireDate) - Date.now()) / 86_400_000);
71
+ if (days < 30) console.warn(`${r.domain} expires in ${days} days (${r.expireDate})`);
72
+ }
73
+ ```
39
74
 
40
- if (err) {
41
- throw err;
42
- }
75
+ ## CLI
43
76
 
44
- if (domainInfo.isFound) {
45
- console.log(domainInfo.data);
46
- } else {
47
- console.log('Domain not Found');
48
- }
77
+ ```
78
+ whoispk [options] <domain...>
49
79
 
50
- });
80
+ -j, --json Print results as JSON
81
+ -t, --timeout <ms> Per-request timeout (default: 15000)
82
+ -c, --concurrency <n> Parallel lookups (default: 3)
83
+ --no-color Disable colours (NO_COLOR is respected too)
51
84
  ```
52
85
 
53
- or using ES6 Promises:
86
+ The exit code is `0` when every lookup finished, `1` when any failed (e.g. PKNIC couldn't be reached), and `2` for bad usage.
87
+
88
+ See the **[CLI reference](docs/cli.md)** for the JSON format, stdin input and more.
89
+
90
+ ## Library
91
+
92
+ ```sh
93
+ npm install whoispk
94
+ ```
54
95
 
55
96
  ```js
56
- const whoispk = require('whoispk');
57
-
58
- whoispk
59
- .lookup('daraz.pk')
60
- .then(function (domainInfo) {
61
-
62
- if (domainInfo.isFound) {
63
- console.log(domainInfo.data);
64
- } else {
65
- console.log('Domain not Found');
66
- }
67
- })
68
- .catch(function (err) {
69
- throw err;
70
- });
97
+ import { lookup } from 'whoispk'; // or: const { lookup } = require('whoispk');
98
+
99
+ const result = await lookup('daraz.pk');
71
100
  ```
72
101
 
73
- The console will show following information if domainInfo.isFound is true:
74
102
  ```js
75
103
  {
76
- domain_name: 'daraz.pk',
77
- registrant: 'Jade E Services Pakistan {Pvt} Limited',
78
- address: 'Clifton Karachi; South Sindh 75600Pakistan',
79
- create_date: 'May 14, 2012',
80
- expire_date: 'May 14, 2018',
81
- agent_org: 'iS-Fun Internet Services GmbH',
82
- technical_contact: [
83
- 'Jade E Services Pakistan {Pvt} Limited',
84
- 'Raja Muneeb Idrees M Admin',
85
- 'Clifton, Karachi; South Sindh75600'
86
- ],
87
- billing_contact: [
88
- 'iS-Fun Internet Services GmbH',
89
- 'Robin Fundinger',
90
- 'Karlsbad, 76307'
91
- ],
92
- nameservers: [
93
- 'kip.ns.cloudflare.com',
94
- 'naomi.ns.cloudflare.com'
95
- ]
104
+ domain: 'daraz.pk',
105
+ status: 'registered', // 'registered' | 'available' | 'invalid'
106
+ registered: true,
107
+ available: false,
108
+ createDate: '2012-05-14', // YYYY-MM-DD, or null
109
+ expireDate: '2028-05-14', // YYYY-MM-DD, or null
110
+ nameservers: ['ns1.alibabadns.cn', 'ns1.alibabadns.com', 'ns2.alibabadns.cn', 'ns2.alibabadns.com'],
111
+ reasons: [], // PKNIC's reasons, when status is 'invalid'
112
+ fields: { 'Domain Name': 'daraz.pk', 'Create Date': 'May 14, 2012', 'Expire Date': 'May 14, 2028' }
96
113
  }
97
114
  ```
98
115
 
99
- Source of information: http://pknic.net.pk
116
+ Options include `timeout`, `retries`, `signal` and `fetch`. Failures reject with a `WhoisPkError` that has a `code`, such as `TIMEOUT` or `NETWORK_ERROR`. See the **[API reference](docs/api.md)** for all of it.
117
+
118
+ ## FAQ
119
+
120
+ <details>
121
+ <summary><b>Why isn't the owner's name or contact shown?</b></summary>
122
+
123
+ PKNIC stopped publishing registrant, address and contact details because of GDPR and ICANN WHOIS policy. Only the dates and nameservers are public now. If you own the domain, you can see the full record by logging in to your PKNIC account.
124
+ </details>
125
+
126
+ <details>
127
+ <summary><b>Does "available" mean I can definitely register it?</b></summary>
128
+
129
+ Not quite. It means PKNIC has no record of the domain. PKNIC only checks whether the name is allowed, for example that it isn't reserved or blocked, when you actually register it. Names it rejects straight away, such as ones that are too short, show up as **not registrable**.
130
+ </details>
131
+
132
+ <details>
133
+ <summary><b>How do I register a domain I found?</b></summary>
134
+
135
+ Through [PKNIC](https://pknic.net.pk) or one of its registrars. whoispk only looks domains up.
136
+ </details>
137
+
138
+ <details>
139
+ <summary><b>It says <code>PARSE_ERROR</code>. What now?</b></summary>
140
+
141
+ PKNIC has probably changed its lookup page. Please [open an issue](https://github.com/abbassiddiqi/whoispk/issues). [How it works](docs/how-it-works.md) explains the parser, if you'd like to send a fix.
142
+ </details>
143
+
144
+ <details>
145
+ <summary><b>Is there a rate limit?</b></summary>
146
+
147
+ PKNIC doesn't publish one, but please be considerate. The CLI runs at most 3 lookups at a time by default, and retries only once, after a pause. Avoid checking thousands of names in a tight loop.
148
+ </details>
149
+
150
+ <details>
151
+ <summary><b>I'm upgrading from 1.x.</b></summary>
152
+
153
+ Version 2 needs Node.js 20.19 or later. The callback API is gone; use the promise. Dates are now `YYYY-MM-DD`. See the [changelog](CHANGELOG.md).
154
+ </details>
155
+
156
+ ## Documentation
157
+
158
+ | Guide | What's in it |
159
+ | --- | --- |
160
+ | [CLI reference](docs/cli.md) | Every option, stdin input, JSON output, exit codes, `jq` recipes |
161
+ | [API reference](docs/api.md) | `lookup`, `parse`, `normalizeDomain`, result fields, errors |
162
+ | [How it works](docs/how-it-works.md) | How PKNIC's page is read, and what to do when it changes |
163
+ | [Changelog](CHANGELOG.md) | What changed in each release |
164
+ | [Releasing](docs/releasing.md) | How maintainers publish a new version to npm |
100
165
 
101
166
  ## Contributing
102
167
 
103
- - Discuss ideas in issues section
104
- - Spread the word
168
+ Bug reports and pull requests are welcome. [CONTRIBUTING.md](CONTRIBUTING.md) covers setup, tests and guidelines.
169
+
170
+ whoispk started in 2017. You can read about the original version in [this Medium post](https://medium.com/@abbassiddiqi/whoispk-command-line-tool-5ea275926688).
105
171
 
106
172
  ## License
107
173
 
108
- whoispk is available under [the MIT license](https://github.com/abbassiddiqi/whoispk/blob/master/LICENSE).
174
+ [MIT](LICENSE) © Abbas Siddiqi
175
+
176
+ whoispk isn't affiliated with or endorsed by PKNIC. Results come from PKNIC's public lookup page and may change without notice.
package/bin/whoispk.js CHANGED
@@ -1,68 +1,259 @@
1
1
  #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const chalk = require('chalk');
5
- const program = require('commander');
6
- const changeCase = require('change-case');
7
-
8
- const pkg = require('../package.json');
9
- const whoispk = require('../lib/whoispk.js');
10
-
11
- program
12
- .version(pkg.version)
13
- .description(pkg.description)
14
- .arguments('<addr>')
15
- .action(getDomainInfo)
16
- .usage('[options] address')
17
- .parse(process.argv);
18
-
19
- if (!program.args.length) {
20
- program.help();
21
- }
22
-
23
- function getDomainInfo (address) {
24
-
25
- whoispk
26
- .lookup(address)
27
- .then(function (domainInfo) {
28
- printDomainInfo(domainInfo);
29
- process.exit(0);
30
- })
31
- .catch(function (err) {
32
- printErrorMessage();
33
- process.exit(1);
34
- });
2
+ import { readFileSync } from 'node:fs';
3
+ import { parseArgs } from 'node:util';
4
+ import { lookup, ENDPOINT } from '../src/index.js';
5
+
6
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
7
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
8
+
9
+ const HELP = `
10
+ Usage: whoispk [options] <domain...>
11
+
12
+ Check whether .pk domains are registered, using PKNIC's public lookup.
13
+ ".pk" is appended when missing, so "whoispk magic" looks up magic.pk.
14
+ Domains can also be piped in, one per line: cat domains.txt | whoispk
15
+
16
+ Options:
17
+ -j, --json Print results as JSON
18
+ -t, --timeout <ms> Per-request timeout in milliseconds (default: 15000)
19
+ -c, --concurrency <n> Parallel lookups when checking several domains (default: 3)
20
+ --no-color Disable colours (also respects NO_COLOR)
21
+ -v, --version Print the version
22
+ -h, --help Show this help
23
+
24
+ Exit codes:
25
+ 0 all lookups completed
26
+ 1 one or more lookups failed (network error, PKNIC down, …)
27
+ 2 invalid usage
28
+ `;
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Argument parsing
32
+ // ---------------------------------------------------------------------------
33
+
34
+ let args;
35
+ try {
36
+ args = parseArgs({
37
+ allowPositionals: true,
38
+ options: {
39
+ json: { type: 'boolean', short: 'j' },
40
+ timeout: { type: 'string', short: 't' },
41
+ concurrency: { type: 'string', short: 'c' },
42
+ 'no-color': { type: 'boolean' },
43
+ version: { type: 'boolean', short: 'v' },
44
+ help: { type: 'boolean', short: 'h' },
45
+ },
46
+ });
47
+ } catch (err) {
48
+ usageError(err.message);
35
49
  }
36
50
 
37
- function printDomainInfo (domainInfo) {
51
+ const { values: opts, positionals } = args;
52
+
53
+ if (opts.help) {
54
+ process.stdout.write(HELP.trimStart());
55
+ process.exit(0);
56
+ }
57
+ if (opts.version) {
58
+ console.log(pkg.version);
59
+ process.exit(0);
60
+ }
38
61
 
39
- if (!domainInfo.isFound) {
40
- console.log("");
41
- console.log(" Domain not found: " + chalk.inverse(domainInfo.address));
42
- console.log(chalk.italic.dim(" This domain is not registered and may be available for registration to you."));
43
- console.log("");
62
+ const timeout = parsePositiveInt(opts.timeout, 15_000, '--timeout');
63
+ const concurrency = parsePositiveInt(opts.concurrency, 3, '--concurrency');
64
+
65
+ let domains = positionals;
66
+ if (!domains.length && !process.stdin.isTTY) {
67
+ domains = (await readStdin())
68
+ .split(/\r?\n/)
69
+ .flatMap((line) => line.replace(/#.*/, '').split(/[\s,]+/))
70
+ .filter(Boolean);
71
+ }
72
+ if (!domains.length) {
73
+ process.stdout.write(HELP.trimStart());
74
+ process.exit(2);
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Styling
79
+ // ---------------------------------------------------------------------------
80
+
81
+ const useColor =
82
+ !opts['no-color'] &&
83
+ !('NO_COLOR' in process.env) &&
84
+ (process.env.FORCE_COLOR ? process.env.FORCE_COLOR !== '0' : Boolean(process.stdout.isTTY));
85
+
86
+ const ansi = (open, close) => (s) => (useColor ? `\x1b[${open}m${s}\x1b[${close}m` : String(s));
87
+ const c = {
88
+ bold: ansi(1, 22),
89
+ dim: ansi(2, 22),
90
+ red: ansi(31, 39),
91
+ green: ansi(32, 39),
92
+ yellow: ansi(33, 39),
93
+ cyan: ansi(36, 39),
94
+ };
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Run lookups (bounded concurrency, printed in input order)
98
+ // ---------------------------------------------------------------------------
99
+
100
+ const endpoint = process.env.WHOISPK_ENDPOINT || ENDPOINT;
101
+ const spinner = createSpinner(!opts.json && process.stderr.isTTY);
102
+ const limit = createLimiter(concurrency);
103
+
104
+ let done = 0;
105
+ spinner.update(progressText(done, domains.length));
106
+
107
+ const pending = domains.map((domain) =>
108
+ limit(() => lookup(domain, { timeout, endpoint }))
109
+ .then(
110
+ (result) => ({ ok: true, input: domain, result }),
111
+ (error) => ({ ok: false, input: domain, error }),
112
+ )
113
+ .finally(() => spinner.update(progressText(++done, domains.length))),
114
+ );
115
+
116
+ let failed = 0;
117
+ const jsonResults = [];
118
+
119
+ for (const [i, promise] of pending.entries()) {
120
+ const outcome = await promise;
121
+ if (!outcome.ok) failed++;
122
+
123
+ if (opts.json) {
124
+ jsonResults.push(
125
+ outcome.ok
126
+ ? outcome.result
127
+ : { domain: outcome.input, status: 'error', error: { code: outcome.error.code ?? null, message: outcome.error.message } },
128
+ );
129
+ continue;
44
130
  }
45
131
 
46
- if (domainInfo.isFound) {
47
- console.log("");
48
- for (const prop in domainInfo.data) {
49
- if (typeof domainInfo.data[prop] == "object") {
50
- console.log("");
51
- console.log(" " + chalk.italic(changeCase.titleCase(prop) + ":"));
52
- domainInfo.data[prop].forEach(function (item) {
53
- console.log("\t\t" + chalk(item));
54
- });
55
- } else {
56
- console.log(" " + chalk.italic(changeCase.titleCase(prop) + ":\t") + chalk(domainInfo.data[prop]) );
57
- }
58
- }
59
- console.log("");
132
+ spinner.clear();
133
+ if (i > 0) console.log('');
134
+ console.log(outcome.ok ? formatResult(outcome.result) : formatError(outcome.input, outcome.error));
135
+ }
136
+
137
+ spinner.stop();
138
+
139
+ if (opts.json) {
140
+ console.log(JSON.stringify(jsonResults.length === 1 ? jsonResults[0] : jsonResults, null, 2));
141
+ }
142
+
143
+ process.exitCode = failed ? 1 : 0;
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Formatting
147
+ // ---------------------------------------------------------------------------
148
+
149
+ function formatResult(r) {
150
+ const lines = [];
151
+ const label = (s) => ` ${c.dim(s.padEnd(13))}`;
152
+
153
+ if (r.status === 'registered') {
154
+ lines.push(`${c.bold(r.domain)} ${c.red('● Registered')}`);
155
+ if (r.createDate) lines.push(`${label('Created')}${formatDate(r.createDate)}`);
156
+ if (r.expireDate) lines.push(`${label('Expires')}${formatDate(r.expireDate)} ${c.dim(`(${relative(r.expireDate)})`)}`);
157
+ r.nameservers.forEach((ns, i) => lines.push(`${label(i === 0 ? 'Nameservers' : '')}${ns}`));
158
+ } else if (r.status === 'available') {
159
+ lines.push(`${c.bold(r.domain)} ${c.green('● Available')}`);
160
+ lines.push(` ${c.dim('Not registered — may be available for registration.')}`);
161
+ } else {
162
+ lines.push(`${c.bold(r.domain)} ${c.yellow('● Not registrable')}`);
163
+ for (const reason of r.reasons) lines.push(` ${c.dim(reason)}`);
60
164
  }
165
+
166
+ return lines.join('\n');
167
+ }
168
+
169
+ function formatError(domain, err) {
170
+ return `${c.bold(domain)} ${c.red('✖ Lookup failed')}\n ${c.dim(err.message)}`;
171
+ }
172
+
173
+ function formatDate(iso) {
174
+ const [y, m, d] = iso.split('-').map(Number);
175
+ return `${MONTHS[m - 1]} ${d}, ${y}`;
176
+ }
177
+
178
+ function relative(iso) {
179
+ const days = Math.round((Date.parse(`${iso}T00:00:00Z`) - Date.now()) / 86_400_000);
180
+ if (days === 0) return 'expires today';
181
+ const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'always' });
182
+ const abs = Math.abs(days);
183
+ const [value, unit] =
184
+ abs >= 330 ? [Math.round(days / 365), 'year'] : abs >= 45 ? [Math.round(days / 30), 'month'] : [days, 'day'];
185
+ return `${days < 0 ? 'expired ' : 'expires '}${rtf.format(value, unit)}`;
186
+ }
187
+
188
+ function progressText(n, total) {
189
+ return total === 1 ? `Looking up ${domains[0]} on PKNIC…` : `Looking up domains on PKNIC… ${n}/${total}`;
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Helpers
194
+ // ---------------------------------------------------------------------------
195
+
196
+ function createSpinner(enabled) {
197
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
198
+ let text = '';
199
+ let frame = 0;
200
+ let timer = null;
201
+ const render = () => process.stderr.write(`\r\x1b[K${c.cyan(frames[frame++ % frames.length])} ${text}`);
202
+ return {
203
+ update(t) {
204
+ if (!enabled) return;
205
+ text = t;
206
+ if (!timer) {
207
+ timer = setInterval(render, 80);
208
+ timer.unref();
209
+ }
210
+ render();
211
+ },
212
+ clear() {
213
+ if (enabled && timer) process.stderr.write('\r\x1b[K');
214
+ },
215
+ stop() {
216
+ if (!enabled) return;
217
+ clearInterval(timer);
218
+ timer = null;
219
+ process.stderr.write('\r\x1b[K');
220
+ },
221
+ };
222
+ }
223
+
224
+ function createLimiter(max) {
225
+ let active = 0;
226
+ const queue = [];
227
+ const next = () => {
228
+ if (active >= max || !queue.length) return;
229
+ active++;
230
+ const { fn, resolve, reject } = queue.shift();
231
+ fn().then(resolve, reject).finally(() => {
232
+ active--;
233
+ next();
234
+ });
235
+ };
236
+ return (fn) =>
237
+ new Promise((resolve, reject) => {
238
+ queue.push({ fn, resolve, reject });
239
+ next();
240
+ });
241
+ }
242
+
243
+ function parsePositiveInt(raw, fallback, name) {
244
+ if (raw === undefined) return fallback;
245
+ const n = Number(raw);
246
+ if (!Number.isInteger(n) || n <= 0) usageError(`${name} must be a positive integer`);
247
+ return n;
248
+ }
249
+
250
+ function usageError(message) {
251
+ console.error(`whoispk: ${message}\nRun "whoispk --help" for usage.`);
252
+ process.exit(2);
61
253
  }
62
254
 
63
- function printErrorMessage() {
64
- console.log("");
65
- console.log(chalk.red(" Error!") + " Server connection Error.");
66
- console.log(" " + " Please try again.");
67
- console.log("");
255
+ async function readStdin() {
256
+ let data = '';
257
+ for await (const chunk of process.stdin) data += chunk;
258
+ return data;
68
259
  }
package/package.json CHANGED
@@ -1,48 +1,55 @@
1
1
  {
2
2
  "name": "whoispk",
3
- "version": "1.1.0",
4
- "description": "A robust WHOIS client for PKNIC (.pk) domains",
3
+ "version": "2.0.0",
4
+ "description": "Check .pk domain availability and registration details from PKNIC, from the command line or Node.js",
5
5
  "keywords": [
6
6
  "whois",
7
7
  "pk",
8
8
  "pknic",
9
9
  "whoispk",
10
- "pakistan"
10
+ "pakistan",
11
+ "domain",
12
+ "availability",
13
+ "cli"
11
14
  ],
12
- "main": "index.js",
13
- "preferGlobal": true,
15
+ "type": "module",
16
+ "main": "./src/index.js",
17
+ "types": "./src/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./src/index.d.ts",
21
+ "default": "./src/index.js"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
14
25
  "bin": {
15
- "whoispk": "./bin/whoispk.js"
26
+ "whoispk": "bin/whoispk.js"
27
+ },
28
+ "files": [
29
+ "bin",
30
+ "src"
31
+ ],
32
+ "engines": {
33
+ "node": ">=20.19"
16
34
  },
17
35
  "scripts": {
18
- "test": "mocha"
36
+ "test": "node --test",
37
+ "test:live": "WHOISPK_LIVE=1 node --test",
38
+ "fixtures": "node scripts/update-fixtures.js",
39
+ "prepublishOnly": "npm test"
19
40
  },
20
41
  "author": {
21
42
  "name": "Abbas Siddiqi",
22
43
  "email": "abbas.siddiqi1@gmail.com",
23
- "url": "abbassiddiqi.com"
44
+ "url": "https://abbassiddiqi.com"
24
45
  },
25
46
  "repository": {
26
47
  "type": "git",
27
- "url": "git://github.com/abbassiddiqi/whoispk.git"
48
+ "url": "git+https://github.com/abbassiddiqi/whoispk.git"
28
49
  },
29
50
  "bugs": {
30
51
  "url": "https://github.com/abbassiddiqi/whoispk/issues"
31
52
  },
32
- "license": {
33
- "type": "MIT",
34
- "url": "https://github.com/abbassiddiqi/whoispk/blob/master/LICENSE"
35
- },
36
- "dependencies": {
37
- "axios": "^0.16.2",
38
- "chalk": "^2.1.0",
39
- "change-case": "^3.0.1",
40
- "cheerio": "^1.0.0-rc.2",
41
- "commander": "^2.11.0",
42
- "querystring": "^0.2.0"
43
- },
44
- "devDependencies": {
45
- "chai": "^4.1.1",
46
- "mocha": "^3.5.0"
47
- }
53
+ "homepage": "https://github.com/abbassiddiqi/whoispk#readme",
54
+ "license": "MIT"
48
55
  }
package/src/errors.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Error thrown by whoispk. `code` is one of:
3
+ * INVALID_INPUT, HTTP_ERROR, NETWORK_ERROR, TIMEOUT, ABORTED, PARSE_ERROR.
4
+ */
5
+ export class WhoisPkError extends Error {
6
+ constructor(message, { code, cause, status } = {}) {
7
+ super(message, cause ? { cause } : undefined);
8
+ this.name = 'WhoisPkError';
9
+ this.code = code;
10
+ if (status !== undefined) this.status = status;
11
+ }
12
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,62 @@
1
+ export type DomainStatus = 'registered' | 'available' | 'invalid';
2
+
3
+ export interface LookupResult {
4
+ /** The normalised domain that was looked up, e.g. "daraz.pk". */
5
+ domain: string;
6
+ status: DomainStatus;
7
+ registered: boolean;
8
+ /** True when PKNIC reports the domain as not registered (it may be available to register). */
9
+ available: boolean;
10
+ /** Registration date as YYYY-MM-DD, when registered. */
11
+ createDate: string | null;
12
+ /** Expiry date as YYYY-MM-DD, when registered. */
13
+ expireDate: string | null;
14
+ /** DNS entries listed by PKNIC, in the order shown. */
15
+ nameservers: string[];
16
+ /** Why PKNIC rejected the name, when status is "invalid". */
17
+ reasons: string[];
18
+ /** All labelled fields from the record table, verbatim (e.g. { "Create Date": "Nov 7, 2013" }). */
19
+ fields: Record<string, string>;
20
+ }
21
+
22
+ export interface LookupOptions {
23
+ /** Per-request timeout in milliseconds. Default 15000. */
24
+ timeout?: number;
25
+ /** Retries on network errors, timeouts, 429 and 5xx. Default 1. */
26
+ retries?: number;
27
+ signal?: AbortSignal;
28
+ /** Override the PKNIC lookup URL. */
29
+ endpoint?: string;
30
+ /** Custom fetch implementation. Defaults to the global fetch. */
31
+ fetch?: typeof globalThis.fetch;
32
+ }
33
+
34
+ export type WhoisPkErrorCode =
35
+ | 'INVALID_INPUT'
36
+ | 'HTTP_ERROR'
37
+ | 'NETWORK_ERROR'
38
+ | 'TIMEOUT'
39
+ | 'ABORTED'
40
+ | 'PARSE_ERROR';
41
+
42
+ export class WhoisPkError extends Error {
43
+ code: WhoisPkErrorCode;
44
+ /** HTTP status, for HTTP_ERROR. */
45
+ status?: number;
46
+ }
47
+
48
+ export const ENDPOINT: string;
49
+ export const USER_AGENT: string;
50
+
51
+ export function lookup(domain: string, options?: LookupOptions): Promise<LookupResult>;
52
+ export function parse(html: string): Omit<LookupResult, 'domain'> & { domain: string | null };
53
+ export function normalizeDomain(input: string): string;
54
+
55
+ declare const whoispk: {
56
+ lookup: typeof lookup;
57
+ parse: typeof parse;
58
+ normalizeDomain: typeof normalizeDomain;
59
+ WhoisPkError: typeof WhoisPkError;
60
+ ENDPOINT: string;
61
+ };
62
+ export default whoispk;
package/src/index.js ADDED
@@ -0,0 +1,118 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { WhoisPkError } from './errors.js';
3
+ import { parse } from './parse.js';
4
+
5
+ export { parse, WhoisPkError };
6
+
7
+ export const ENDPOINT = 'https://pk6.pknic.net.pk/pk5/lookup.PK';
8
+
9
+ const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
10
+ export const USER_AGENT = `whoispk/${version} (+https://github.com/abbassiddiqi/whoispk)`;
11
+
12
+ /**
13
+ * Normalise user input into a bare .pk domain name.
14
+ *
15
+ * "https://www.Daraz.pk/some/path" -> "daraz.pk"
16
+ * "magic" -> "magic.pk"
17
+ * "example.com.pk." -> "example.com.pk"
18
+ *
19
+ * @param {string} input
20
+ * @returns {string}
21
+ */
22
+ export function normalizeDomain(input) {
23
+ if (typeof input !== 'string' || !input.trim()) {
24
+ throw new WhoisPkError('A domain name is required', { code: 'INVALID_INPUT' });
25
+ }
26
+
27
+ let domain = input.trim().toLowerCase();
28
+ domain = domain.replace(/^[a-z][a-z0-9+.-]*:\/\//, ''); // scheme
29
+ domain = domain.replace(/^[^@/]*@/, ''); // userinfo
30
+ domain = domain.replace(/[/?#].*$/, ''); // path, query, fragment
31
+ domain = domain.replace(/:\d+$/, ''); // port
32
+ domain = domain.replace(/\.+$/, ''); // trailing dot(s)
33
+ domain = domain.replace(/^www\./, '');
34
+
35
+ if (!domain) {
36
+ throw new WhoisPkError(`"${input}" is not a valid domain name`, { code: 'INVALID_INPUT' });
37
+ }
38
+ if (!domain.endsWith('.pk') && domain !== 'pk') domain += '.pk';
39
+
40
+ return domain;
41
+ }
42
+
43
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
44
+
45
+ async function request(domain, { endpoint, timeout, signal, fetch: fetchImpl }) {
46
+ const signals = [AbortSignal.timeout(timeout)];
47
+ if (signal) signals.push(signal);
48
+
49
+ let response;
50
+ try {
51
+ response = await fetchImpl(endpoint, {
52
+ method: 'POST',
53
+ headers: {
54
+ 'content-type': 'application/x-www-form-urlencoded',
55
+ accept: 'text/html',
56
+ 'user-agent': USER_AGENT,
57
+ },
58
+ body: new URLSearchParams({ name: domain }).toString(),
59
+ signal: AbortSignal.any(signals),
60
+ });
61
+ } catch (err) {
62
+ if (signal?.aborted) {
63
+ throw new WhoisPkError('Lookup aborted', { code: 'ABORTED', cause: err });
64
+ }
65
+ if (err?.name === 'TimeoutError') {
66
+ throw new WhoisPkError(`PKNIC did not respond within ${timeout} ms`, { code: 'TIMEOUT', cause: err });
67
+ }
68
+ throw new WhoisPkError(`Could not reach PKNIC: ${err?.cause?.message ?? err?.message ?? err}`, {
69
+ code: 'NETWORK_ERROR',
70
+ cause: err,
71
+ });
72
+ }
73
+
74
+ if (!response.ok) {
75
+ throw new WhoisPkError(`PKNIC responded with HTTP ${response.status}`, {
76
+ code: 'HTTP_ERROR',
77
+ status: response.status,
78
+ });
79
+ }
80
+
81
+ return response.text();
82
+ }
83
+
84
+ /**
85
+ * Look up a .pk domain on PKNIC.
86
+ *
87
+ * @param {string} input Domain name, e.g. "daraz.pk" (".pk" is appended if missing).
88
+ * @param {import('./index.js').LookupOptions} [options]
89
+ * @returns {Promise<import('./index.js').LookupResult>}
90
+ */
91
+ export async function lookup(input, options = {}) {
92
+ const {
93
+ timeout = 15_000,
94
+ retries = 1,
95
+ signal,
96
+ endpoint = ENDPOINT,
97
+ fetch: fetchImpl = globalThis.fetch,
98
+ } = options;
99
+
100
+ const domain = normalizeDomain(input);
101
+
102
+ for (let attempt = 0; ; attempt++) {
103
+ try {
104
+ const html = await request(domain, { endpoint, timeout, signal, fetch: fetchImpl });
105
+ const result = parse(html);
106
+ return { ...result, domain: result.domain ?? domain };
107
+ } catch (err) {
108
+ const retryable =
109
+ err.code === 'NETWORK_ERROR' ||
110
+ err.code === 'TIMEOUT' ||
111
+ (err.code === 'HTTP_ERROR' && (err.status >= 500 || err.status === 429));
112
+ if (!retryable || attempt >= retries) throw err;
113
+ await sleep(500 * 2 ** attempt);
114
+ }
115
+ }
116
+ }
117
+
118
+ export default { lookup, parse, normalizeDomain, WhoisPkError, ENDPOINT };
package/src/parse.js ADDED
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Parser for the HTML returned by PKNIC's public lookup page
3
+ * (https://pk6.pknic.net.pk/pk5/lookup.PK).
4
+ *
5
+ * PKNIC embeds machine-readable marker tags in the page, e.g.
6
+ * `<XPC_DOMAIN_NOT_AVAILABLE/>` and `<XPC_RESULT_NOTFOUND/>`. Those are used to
7
+ * decide the status; visible text is only used as a fallback and to extract
8
+ * the record details.
9
+ */
10
+
11
+ import { WhoisPkError } from './errors.js';
12
+
13
+ const MONTHS = {
14
+ jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6,
15
+ jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12,
16
+ };
17
+
18
+ const ENTITIES = { nbsp: ' ', amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
19
+
20
+ /** Decode the handful of HTML entities PKNIC uses. */
21
+ function decodeEntities(str) {
22
+ return str.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (match, entity) => {
23
+ if (entity[0] === '#') {
24
+ const code = entity[1] === 'x' || entity[1] === 'X'
25
+ ? parseInt(entity.slice(2), 16)
26
+ : parseInt(entity.slice(1), 10);
27
+ return Number.isFinite(code) ? String.fromCodePoint(code) : match;
28
+ }
29
+ return ENTITIES[entity.toLowerCase()] ?? match;
30
+ });
31
+ }
32
+
33
+ /** Strip tags, decode entities and collapse whitespace. */
34
+ function toText(html) {
35
+ return decodeEntities(html.replace(/<[^>]*>/g, ' '))
36
+ .replace(/\s+/g, ' ')
37
+ .trim();
38
+ }
39
+
40
+ /** "Nov 7, 2013" -> "2013-11-07"; returns null if it doesn't look like a date. */
41
+ export function toIsoDate(text) {
42
+ const m = /^([a-z]{3})[a-z]*\.?\s+(\d{1,2}),?\s+(\d{4})$/i.exec(text.trim());
43
+ if (!m) return null;
44
+ const month = MONTHS[m[1].toLowerCase()];
45
+ if (!month) return null;
46
+ return `${m[3]}-${String(month).padStart(2, '0')}-${m[2].padStart(2, '0')}`;
47
+ }
48
+
49
+ /** The main content cell of the page (left column), without HTML comments. */
50
+ function mainSection(html) {
51
+ const body = html.replace(/<!--[\s\S]*?-->/g, '');
52
+ const start = body.search(/id=["']?Tmain\b/i);
53
+ const end = body.search(/id=["']?Tside\b/i);
54
+ if (start !== -1 && end > start) return body.slice(start, end);
55
+ return body;
56
+ }
57
+
58
+ /**
59
+ * Parse the record table into labelled fields and nameservers.
60
+ * Rows are laid out as [spacer, label, spacer, value]. A label with no value
61
+ * (e.g. "DNS Nameservers") starts a list; following unlabelled rows belong to it.
62
+ */
63
+ function parseRecordTable(section) {
64
+ const fields = {};
65
+ const lists = {};
66
+ let currentList = null;
67
+
68
+ // Split on <tr> rather than matching <tr>…</tr>: PKNIC's markup leaves some rows unclosed.
69
+ for (const row of section.split(/<tr\b[^>]*>/i).slice(1)) {
70
+ const cells = row
71
+ .replace(/<\/table>[\s\S]*$/i, '')
72
+ .split(/<t[dh]\b[^>]*>/i)
73
+ .slice(1)
74
+ .map(toText);
75
+ if (cells.length < 4) continue;
76
+
77
+ const label = cells[1].replace(/:$/, '').trim();
78
+ const value = cells[3];
79
+
80
+ if (label && value) {
81
+ fields[label] = value;
82
+ currentList = null;
83
+ } else if (label) {
84
+ currentList = label;
85
+ lists[label] = [];
86
+ } else if (value && currentList) {
87
+ lists[currentList].push(value);
88
+ }
89
+ }
90
+
91
+ return { fields, lists };
92
+ }
93
+
94
+ function findField(fields, pattern) {
95
+ const key = Object.keys(fields).find((k) => pattern.test(k));
96
+ return key === undefined ? undefined : fields[key];
97
+ }
98
+
99
+ /**
100
+ * Parse a PKNIC lookup response.
101
+ *
102
+ * @param {string} html
103
+ * @returns {import('./index.js').LookupResult}
104
+ */
105
+ export function parse(html) {
106
+ if (typeof html !== 'string') {
107
+ throw new TypeError('parse() expects an HTML string');
108
+ }
109
+
110
+ const markers = new Set([...html.matchAll(/<XPC_([A-Z_]+)\s*\/?>/g)].map((m) => m[1]));
111
+ const section = mainSection(html);
112
+
113
+ const base = {
114
+ domain: null,
115
+ status: null,
116
+ registered: false,
117
+ available: false,
118
+ createDate: null,
119
+ expireDate: null,
120
+ nameservers: [],
121
+ reasons: [],
122
+ fields: {},
123
+ };
124
+
125
+ // --- Available -----------------------------------------------------------
126
+ const notFound = /Domain not found:\s*(?:<[^>]*>\s*)*([^<\s]+)/i.exec(section);
127
+ if (markers.has('RESULT_NOTFOUND') || markers.has('DOMAIN_MAYBE_AVAILABLE') || notFound) {
128
+ return {
129
+ ...base,
130
+ domain: notFound ? toText(notFound[1]).toLowerCase() : null,
131
+ status: 'available',
132
+ available: true,
133
+ };
134
+ }
135
+
136
+ // --- Invalid / not registrable --------------------------------------------
137
+ const errors = [...section.matchAll(/<li\b[^>]*class=["']?error["']?[^>]*>([\s\S]*?)(?=<\/li>|<li\b|<\/ul>|<br|<\/div>)/gi)]
138
+ .map((m) => toText(m[1]))
139
+ .filter(Boolean);
140
+ if (errors.length || /has the following issues/i.test(section)) {
141
+ const searched = /Your searched domain:\s*<b>([\s\S]*?)<\/b>/i.exec(section);
142
+ const domain = searched ? toText(searched[1]).toLowerCase() : null;
143
+ // Reasons are prefixed with the domain ("x.pk: Invalid Domain name …"); drop that prefix.
144
+ const reasons = errors.map((e) =>
145
+ domain && e.toLowerCase().startsWith(`${domain}:`) ? e.slice(domain.length + 1).trim() : e,
146
+ );
147
+ return { ...base, domain, status: 'invalid', reasons };
148
+ }
149
+
150
+ // --- Registered ------------------------------------------------------------
151
+ const heading = /The Domain record for\s*([^<\s]+)/i.exec(section);
152
+ if (heading || /Domain is Registered/i.test(section)) {
153
+ const { fields, lists } = parseRecordTable(section);
154
+ const created = findField(fields, /^create/i);
155
+ const expires = findField(fields, /^expir/i);
156
+ const nsKey = Object.keys(lists).find((k) => /name\s*servers?/i.test(k));
157
+ const domain = findField(fields, /^domain name$/i) ?? (heading ? toText(heading[1]) : null);
158
+
159
+ return {
160
+ ...base,
161
+ domain: domain ? domain.toLowerCase() : null,
162
+ status: 'registered',
163
+ registered: true,
164
+ createDate: created ? toIsoDate(created) : null,
165
+ expireDate: expires ? toIsoDate(expires) : null,
166
+ nameservers: nsKey ? lists[nsKey] : [],
167
+ fields,
168
+ };
169
+ }
170
+
171
+ throw new WhoisPkError(
172
+ 'Unrecognised response from PKNIC. The lookup page layout may have changed; please open an issue at https://github.com/abbassiddiqi/whoispk/issues',
173
+ { code: 'PARSE_ERROR' },
174
+ );
175
+ }
package/.npmignore DELETED
@@ -1,6 +0,0 @@
1
- # dependencies
2
- node_modules
3
-
4
- # misc
5
- .DS_Store
6
- npm-debug.log*
package/.travis.yml DELETED
@@ -1,6 +0,0 @@
1
- language: node_js
2
- node_js:
3
- - "7"
4
- cache:
5
- directories:
6
- - "node_modules"
package/index.js DELETED
@@ -1 +0,0 @@
1
- module.exports = require('./lib/whoispk.js');
package/lib/whoispk.js DELETED
@@ -1,109 +0,0 @@
1
- 'use strict';
2
-
3
- const axios = require('axios');
4
- const cheerio = require('cheerio');
5
- const changeCase = require('change-case');
6
- const querystring = require('querystring');
7
-
8
- exports.lookup = lookup;
9
-
10
- function lookup (addr, options, done) {
11
-
12
- if (typeof addr === "undefined" && !addr) {
13
- throw new Error("address cannot be empty");
14
- }
15
-
16
- // A callback function is provided
17
- if (typeof done === "undefined" && typeof options === "function") {
18
- done = options;
19
- options = {};
20
- }
21
-
22
- // Called via Promise, no Callback function given
23
- if (
24
- typeof done === "undefined" &&
25
- (typeof options === "object" || typeof options === "undefined")
26
- ) {
27
- options = {};
28
- return fetchDomainInfo(addr);
29
- }
30
-
31
- Promise.resolve(fetchDomainInfo(addr))
32
- .then(function (domainInfo) {
33
- done(null, domainInfo);
34
- })
35
- .catch(function (err) {
36
- done(err);
37
- });
38
- }
39
-
40
- function fetchDomainInfo (addr) {
41
-
42
- const pknicUrl = "https://pk6.pknic.net.pk/pk5/lookup.PK";
43
-
44
- return axios
45
- .post(pknicUrl, querystring.stringify({ name: addr }))
46
- .then(function (response) {
47
-
48
- const domainInfo = parseHTMLResponse(response.data);
49
- domainInfo.address = addr;
50
-
51
- return domainInfo;
52
- });
53
- }
54
-
55
- function parseHTMLResponse (data) {
56
-
57
- const domainInfo = {
58
- isFound: false
59
- };
60
-
61
- const $ = cheerio.load(data);
62
- const $formBox = $(".formbox","#Tmain");
63
- const $whiteBox = $(".whitebox", "#Tmain");
64
-
65
- if ($formBox.length) {
66
- // Domain Not Found
67
- }
68
-
69
- if ($whiteBox.length) {
70
- // Domain Found
71
-
72
- domainInfo.isFound = true;
73
- domainInfo.data = {};
74
-
75
- const $rows = $whiteBox.find("tr");
76
-
77
- let lastHeading = "";
78
-
79
- $rows.each(function(i, row) {
80
-
81
- const $row = cheerio(row);
82
-
83
- if ($row.children().length == 4) {
84
-
85
- let key = $row.children().eq(1).text().replace(/\s+/g,' ').trim();
86
- let value = $row.children().eq(3).text().replace(/\s+/g,' ').trim();
87
-
88
- key = changeCase.snakeCase(key);
89
- key = key.replace(/\:$/,'');
90
-
91
- if (key && value) {
92
- if (key == "registrant_name") { key = "registrant"};
93
- if (key == "agent_organization") {key = "agent_org"};
94
- domainInfo.data[key] = value;
95
- }
96
- if (key && !value) {
97
- lastHeading = key;
98
- domainInfo.data[lastHeading] = [];
99
- }
100
- if (!key && value) {
101
- domainInfo.data[lastHeading].push(value);
102
- }
103
- }
104
-
105
- });
106
- }
107
-
108
- return domainInfo;
109
- }
package/test/test.js DELETED
@@ -1,77 +0,0 @@
1
- const chai = require('chai');
2
- const expect = require('chai').expect;
3
-
4
- const whoispk = require('../lib/whoispk.js');
5
-
6
- describe('whoispk', function () {
7
-
8
- describe('lookup', function () {
9
-
10
- it('returns a promise', function () {
11
- const promise = whoispk.lookup('daraz.pk');
12
- expect(promise.then).to.be.a('function');
13
- });
14
-
15
- it('calls callback', function (done) {
16
- this.timeout(4000);
17
- whoispk.lookup('daraz.pk', function (err, domainInfo) {
18
- expect(err).to.be.null;
19
- expect(domainInfo).to.be.an('object');
20
-
21
- done();
22
- });
23
- });
24
-
25
- it('returns domain information via Promise', function (done) {
26
- this.timeout(4000);
27
- whoispk
28
- .lookup('daraz.pk')
29
- .then(function (domainInfo) {
30
- expect(domainInfo).to.be.an('object');
31
- expect(domainInfo.isFound).to.be.true;
32
- done();
33
- })
34
- .catch(function (err) {
35
- done(err);
36
- });
37
- });
38
-
39
- it('returns domain information via Callback', function (done) {
40
- this.timeout(4000);
41
- whoispk
42
- .lookup('daraz.pk', function (err, domainInfo) {
43
- if (err) {
44
- done(err);
45
- } else {
46
- expect(domainInfo).to.be.an('object');
47
- expect(domainInfo.isFound).to.be.true;
48
- done();
49
- }
50
- });
51
- });
52
-
53
- it('returns domain not found via Promise', function (done) {
54
- this.timeout(4000);
55
- whoispk
56
- .lookup('a.pk')
57
- .then(function (domainInfo) {
58
- expect(domainInfo.isFound).to.be.false;
59
- done();
60
- })
61
- .catch(function (err) {
62
- done(err);
63
- });
64
- });
65
-
66
- it('returns domain not found via Callback', function (done) {
67
- this.timeout(4000);
68
- whoispk.lookup('a.pk', function (err, domainInfo) {
69
- expect(err).to.be.null;
70
- expect(domainInfo.isFound).to.be.false;
71
- done();
72
- });
73
- });
74
-
75
- });
76
-
77
- });