whoispk 2.0.0 → 2.1.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 +19 -0
- package/bin/whoispk.js +64 -12
- package/package.json +1 -1
- package/src/index.d.ts +7 -0
- package/src/index.js +54 -18
package/README.md
CHANGED
|
@@ -27,6 +27,12 @@ npx whoispk yourbrand
|
|
|
27
27
|
|
|
28
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
29
|
|
|
30
|
+
To check a name under every extension at once:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
npx whoispk --all yourbrand
|
|
34
|
+
```
|
|
35
|
+
|
|
30
36
|
To have the `whoispk` command always available:
|
|
31
37
|
|
|
32
38
|
```sh
|
|
@@ -36,6 +42,7 @@ npm install -g whoispk
|
|
|
36
42
|
## Features
|
|
37
43
|
|
|
38
44
|
- **Clear answers.** Each domain is registered, available, or not registrable. For a name PKNIC rejects, you get its reason.
|
|
45
|
+
- **Every extension at once.** `--all` checks a name under `.pk`, `.com.pk`, `.net.pk`, `.org.pk`, `.biz.pk`, `.web.pk` and `.fam.pk`, one line each.
|
|
39
46
|
- **Many domains at once.** Pass several names, or pipe in a list. Lookups run in parallel and print in your order.
|
|
40
47
|
- **Made for scripts.** `--json` output, exit codes you can rely on, and no spinner in piped output.
|
|
41
48
|
- **Also a library.** A promise-based API with TypeScript types, timeouts, retries and cancelling.
|
|
@@ -44,6 +51,16 @@ npm install -g whoispk
|
|
|
44
51
|
|
|
45
52
|
## Recipes
|
|
46
53
|
|
|
54
|
+
**Find a free name** across every extension for a few ideas:
|
|
55
|
+
|
|
56
|
+
```console
|
|
57
|
+
$ whoispk --all chaiwala dhaba
|
|
58
|
+
chaiwala.pk ● Registered expires Mar 20, 2027
|
|
59
|
+
chaiwala.com.pk ● Registered expires Oct 10, 2030
|
|
60
|
+
chaiwala.net.pk ● Available
|
|
61
|
+
…
|
|
62
|
+
```
|
|
63
|
+
|
|
47
64
|
**Check a shortlist of name ideas**, one per line (`#` starts a comment):
|
|
48
65
|
|
|
49
66
|
```sh
|
|
@@ -77,6 +94,8 @@ for (const name of ['mybrand.pk', 'mybrand.com.pk']) {
|
|
|
77
94
|
```
|
|
78
95
|
whoispk [options] <domain...>
|
|
79
96
|
|
|
97
|
+
-a, --all Check every public extension (.pk, .com.pk, .net.pk, …)
|
|
98
|
+
-s, --short One line per domain (default with --all)
|
|
80
99
|
-j, --json Print results as JSON
|
|
81
100
|
-t, --timeout <ms> Per-request timeout (default: 15000)
|
|
82
101
|
-c, --concurrency <n> Parallel lookups (default: 3)
|
package/bin/whoispk.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
|
-
import { lookup, ENDPOINT } from '../src/index.js';
|
|
4
|
+
import { lookup, normalizeDomain, expandExtensions, EXTENSIONS, ENDPOINT } from '../src/index.js';
|
|
5
5
|
|
|
6
6
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
7
7
|
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
@@ -14,12 +14,19 @@ Check whether .pk domains are registered, using PKNIC's public lookup.
|
|
|
14
14
|
Domains can also be piped in, one per line: cat domains.txt | whoispk
|
|
15
15
|
|
|
16
16
|
Options:
|
|
17
|
-
-
|
|
18
|
-
-
|
|
17
|
+
-a, --all Check every public extension: ${EXTENSIONS.map((e) => '.' + e).join(' ')}
|
|
18
|
+
-s, --short One line per domain (the default with --all; --no-short for details)
|
|
19
|
+
-j, --json Print results as JSON
|
|
20
|
+
-t, --timeout <ms> Per-request timeout in milliseconds (default: 15000)
|
|
19
21
|
-c, --concurrency <n> Parallel lookups when checking several domains (default: 3)
|
|
20
|
-
--no-color
|
|
21
|
-
-v, --version
|
|
22
|
-
-h, --help
|
|
22
|
+
--no-color Disable colours (also respects NO_COLOR)
|
|
23
|
+
-v, --version Print the version
|
|
24
|
+
-h, --help Show this help
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
whoispk daraz.pk
|
|
28
|
+
whoispk --all chaiwala
|
|
29
|
+
whoispk --json daraz google.com.pk
|
|
23
30
|
|
|
24
31
|
Exit codes:
|
|
25
32
|
0 all lookups completed
|
|
@@ -36,6 +43,9 @@ try {
|
|
|
36
43
|
args = parseArgs({
|
|
37
44
|
allowPositionals: true,
|
|
38
45
|
options: {
|
|
46
|
+
all: { type: 'boolean', short: 'a' },
|
|
47
|
+
short: { type: 'boolean', short: 's' },
|
|
48
|
+
'no-short': { type: 'boolean' },
|
|
39
49
|
json: { type: 'boolean', short: 'j' },
|
|
40
50
|
timeout: { type: 'string', short: 't' },
|
|
41
51
|
concurrency: { type: 'string', short: 'c' },
|
|
@@ -62,18 +72,30 @@ if (opts.version) {
|
|
|
62
72
|
const timeout = parsePositiveInt(opts.timeout, 15_000, '--timeout');
|
|
63
73
|
const concurrency = parsePositiveInt(opts.concurrency, 3, '--concurrency');
|
|
64
74
|
|
|
65
|
-
let
|
|
66
|
-
if (!
|
|
67
|
-
|
|
75
|
+
let inputs = positionals;
|
|
76
|
+
if (!inputs.length && !process.stdin.isTTY) {
|
|
77
|
+
inputs = (await readStdin())
|
|
68
78
|
.split(/\r?\n/)
|
|
69
79
|
.flatMap((line) => line.replace(/#.*/, '').split(/[\s,]+/))
|
|
70
80
|
.filter(Boolean);
|
|
71
81
|
}
|
|
72
|
-
if (!
|
|
82
|
+
if (!inputs.length) {
|
|
73
83
|
process.stdout.write(HELP.trimStart());
|
|
74
84
|
process.exit(2);
|
|
75
85
|
}
|
|
76
86
|
|
|
87
|
+
// Expand --all, normalise, and drop duplicates. Inputs that can't be normalised are kept
|
|
88
|
+
// as typed, so their lookup reports the problem in place.
|
|
89
|
+
const domains = [
|
|
90
|
+
...new Set(
|
|
91
|
+
inputs
|
|
92
|
+
.flatMap((input) => (opts.all ? attempt(() => expandExtensions(input), [input]) : [input]))
|
|
93
|
+
.map((input) => attempt(() => normalizeDomain(input), input)),
|
|
94
|
+
),
|
|
95
|
+
];
|
|
96
|
+
const short = opts['no-short'] ? false : Boolean(opts.short || opts.all);
|
|
97
|
+
const nameWidth = Math.max(...domains.map((d) => d.length));
|
|
98
|
+
|
|
77
99
|
// ---------------------------------------------------------------------------
|
|
78
100
|
// Styling
|
|
79
101
|
// ---------------------------------------------------------------------------
|
|
@@ -130,8 +152,14 @@ for (const [i, promise] of pending.entries()) {
|
|
|
130
152
|
}
|
|
131
153
|
|
|
132
154
|
spinner.clear();
|
|
133
|
-
if (
|
|
134
|
-
|
|
155
|
+
if (short) {
|
|
156
|
+
// Blank line between groups of names, e.g. between "magic.*" and "chaiwala.*" with --all.
|
|
157
|
+
if (i > 0 && opts.all && baseName(domains[i]) !== baseName(domains[i - 1])) console.log('');
|
|
158
|
+
console.log(formatShort(outcome));
|
|
159
|
+
} else {
|
|
160
|
+
if (i > 0) console.log('');
|
|
161
|
+
console.log(outcome.ok ? formatResult(outcome.result) : formatError(outcome.input, outcome.error));
|
|
162
|
+
}
|
|
135
163
|
}
|
|
136
164
|
|
|
137
165
|
spinner.stop();
|
|
@@ -166,6 +194,22 @@ function formatResult(r) {
|
|
|
166
194
|
return lines.join('\n');
|
|
167
195
|
}
|
|
168
196
|
|
|
197
|
+
function formatShort({ ok, input, result: r, error }) {
|
|
198
|
+
const name = c.bold((ok ? r.domain : input).padEnd(nameWidth));
|
|
199
|
+
const status = (text, colour) => colour(text.padEnd(18));
|
|
200
|
+
if (!ok) return `${name} ${status('✖ Lookup failed', c.red)} ${c.dim(error.message)}`;
|
|
201
|
+
if (r.status === 'registered') {
|
|
202
|
+
const expiry = r.expireDate ? c.dim(`expires ${formatDate(r.expireDate)}`) : '';
|
|
203
|
+
return `${name} ${status('● Registered', c.red)} ${expiry}`.trimEnd();
|
|
204
|
+
}
|
|
205
|
+
if (r.status === 'available') return `${name} ${c.green('● Available')}`;
|
|
206
|
+
return `${name} ${status('● Not registrable', c.yellow)} ${c.dim(r.reasons.join('; '))}`.trimEnd();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function baseName(domain) {
|
|
210
|
+
return domain.split('.')[0];
|
|
211
|
+
}
|
|
212
|
+
|
|
169
213
|
function formatError(domain, err) {
|
|
170
214
|
return `${c.bold(domain)} ${c.red('✖ Lookup failed')}\n ${c.dim(err.message)}`;
|
|
171
215
|
}
|
|
@@ -240,6 +284,14 @@ function createLimiter(max) {
|
|
|
240
284
|
});
|
|
241
285
|
}
|
|
242
286
|
|
|
287
|
+
function attempt(fn, fallback) {
|
|
288
|
+
try {
|
|
289
|
+
return fn();
|
|
290
|
+
} catch {
|
|
291
|
+
return fallback;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
243
295
|
function parsePositiveInt(raw, fallback, name) {
|
|
244
296
|
if (raw === undefined) return fallback;
|
|
245
297
|
const n = Number(raw);
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -52,10 +52,17 @@ export function lookup(domain: string, options?: LookupOptions): Promise<LookupR
|
|
|
52
52
|
export function parse(html: string): Omit<LookupResult, 'domain'> & { domain: string | null };
|
|
53
53
|
export function normalizeDomain(input: string): string;
|
|
54
54
|
|
|
55
|
+
/** The .pk extensions anyone can register: pk, com.pk, net.pk, org.pk, biz.pk, web.pk, fam.pk. */
|
|
56
|
+
export const EXTENSIONS: readonly string[];
|
|
57
|
+
/** "magic" or "magic.com.pk" -> one domain per entry in EXTENSIONS. */
|
|
58
|
+
export function expandExtensions(input: string): string[];
|
|
59
|
+
|
|
55
60
|
declare const whoispk: {
|
|
56
61
|
lookup: typeof lookup;
|
|
57
62
|
parse: typeof parse;
|
|
58
63
|
normalizeDomain: typeof normalizeDomain;
|
|
64
|
+
expandExtensions: typeof expandExtensions;
|
|
65
|
+
EXTENSIONS: readonly string[];
|
|
59
66
|
WhoisPkError: typeof WhoisPkError;
|
|
60
67
|
ENDPOINT: string;
|
|
61
68
|
};
|
package/src/index.js
CHANGED
|
@@ -40,15 +40,47 @@ export function normalizeDomain(input) {
|
|
|
40
40
|
return domain;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The .pk extensions anyone can register. PKNIC also has restricted ones
|
|
45
|
+
* (edu.pk, gov.pk, gok.pk, gob.pk, gop.pk, gos.pk) for institutions only.
|
|
46
|
+
*/
|
|
47
|
+
export const EXTENSIONS = Object.freeze(['pk', 'com.pk', 'net.pk', 'org.pk', 'biz.pk', 'web.pk', 'fam.pk']);
|
|
48
|
+
|
|
49
|
+
const SECOND_LEVEL = /\.(?:com|net|org|biz|web|fam|edu|gov|gok|gob|gop|gos)$/;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Expand a name into one domain per public .pk extension.
|
|
53
|
+
*
|
|
54
|
+
* "magic" or "magic.com.pk" -> ["magic.pk", "magic.com.pk", "magic.net.pk", …]
|
|
55
|
+
*
|
|
56
|
+
* @param {string} input
|
|
57
|
+
* @returns {string[]}
|
|
58
|
+
*/
|
|
59
|
+
export function expandExtensions(input) {
|
|
60
|
+
const base = normalizeDomain(input).replace(/\.pk$/, '').replace(SECOND_LEVEL, '');
|
|
61
|
+
if (!base) {
|
|
62
|
+
throw new WhoisPkError(`"${input}" has no name to check`, { code: 'INVALID_INPUT' });
|
|
63
|
+
}
|
|
64
|
+
return EXTENSIONS.map((ext) => `${base}.${ext}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
68
|
|
|
45
69
|
async function request(domain, { endpoint, timeout, signal, fetch: fetchImpl }) {
|
|
46
|
-
|
|
47
|
-
|
|
70
|
+
// A regular (ref'd) timer rather than AbortSignal.timeout(): the latter doesn't keep the
|
|
71
|
+
// process alive, so a stalled request could let Node exit before the timeout ever fires.
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
let timedOut = false;
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
timedOut = true;
|
|
76
|
+
controller.abort(new DOMException(`Timed out after ${timeout} ms`, 'TimeoutError'));
|
|
77
|
+
}, timeout);
|
|
78
|
+
const forwardAbort = () => controller.abort(signal.reason);
|
|
79
|
+
if (signal?.aborted) forwardAbort();
|
|
80
|
+
else signal?.addEventListener('abort', forwardAbort, { once: true });
|
|
48
81
|
|
|
49
|
-
let response;
|
|
50
82
|
try {
|
|
51
|
-
response = await fetchImpl(endpoint, {
|
|
83
|
+
const response = await fetchImpl(endpoint, {
|
|
52
84
|
method: 'POST',
|
|
53
85
|
headers: {
|
|
54
86
|
'content-type': 'application/x-www-form-urlencoded',
|
|
@@ -56,29 +88,33 @@ async function request(domain, { endpoint, timeout, signal, fetch: fetchImpl })
|
|
|
56
88
|
'user-agent': USER_AGENT,
|
|
57
89
|
},
|
|
58
90
|
body: new URLSearchParams({ name: domain }).toString(),
|
|
59
|
-
signal:
|
|
91
|
+
signal: controller.signal,
|
|
60
92
|
});
|
|
93
|
+
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
throw new WhoisPkError(`PKNIC responded with HTTP ${response.status}`, {
|
|
96
|
+
code: 'HTTP_ERROR',
|
|
97
|
+
status: response.status,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return await response.text();
|
|
61
102
|
} catch (err) {
|
|
103
|
+
if (err instanceof WhoisPkError) throw err;
|
|
104
|
+
if (timedOut) {
|
|
105
|
+
throw new WhoisPkError(`PKNIC did not respond within ${timeout} ms`, { code: 'TIMEOUT', cause: err });
|
|
106
|
+
}
|
|
62
107
|
if (signal?.aborted) {
|
|
63
108
|
throw new WhoisPkError('Lookup aborted', { code: 'ABORTED', cause: err });
|
|
64
109
|
}
|
|
65
|
-
if (err?.name === 'TimeoutError') {
|
|
66
|
-
throw new WhoisPkError(`PKNIC did not respond within ${timeout} ms`, { code: 'TIMEOUT', cause: err });
|
|
67
|
-
}
|
|
68
110
|
throw new WhoisPkError(`Could not reach PKNIC: ${err?.cause?.message ?? err?.message ?? err}`, {
|
|
69
111
|
code: 'NETWORK_ERROR',
|
|
70
112
|
cause: err,
|
|
71
113
|
});
|
|
114
|
+
} finally {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
signal?.removeEventListener('abort', forwardAbort);
|
|
72
117
|
}
|
|
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
118
|
}
|
|
83
119
|
|
|
84
120
|
/**
|
|
@@ -115,4 +151,4 @@ export async function lookup(input, options = {}) {
|
|
|
115
151
|
}
|
|
116
152
|
}
|
|
117
153
|
|
|
118
|
-
export default { lookup, parse, normalizeDomain, WhoisPkError, ENDPOINT };
|
|
154
|
+
export default { lookup, parse, normalizeDomain, expandExtensions, EXTENSIONS, WhoisPkError, ENDPOINT };
|