knock 0.1.0 → 1.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/LICENSE +21 -0
- package/README.md +188 -10
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +154 -0
- package/dist/index.d.ts +148 -0
- package/dist/index.js +278 -0
- package/dist/mcp.d.ts +14 -0
- package/dist/mcp.js +319 -0
- package/dist/psl.d.ts +36 -0
- package/dist/psl.js +125 -0
- package/lists/public_suffix_list.dat +16501 -0
- package/llms.txt +70 -0
- package/package.json +53 -19
- package/.npmignore +0 -1
- package/knock.js +0 -10
- package/lib.js +0 -49
package/dist/psl.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public Suffix List support — reduce a hostname to its registrable domain.
|
|
3
|
+
*
|
|
4
|
+
* Where a name may be registered (`co.uk`, `github.io`, `s3.amazonaws.com`)
|
|
5
|
+
* cannot be inferred from string rules alone, so knock uses the Public Suffix
|
|
6
|
+
* List. The data is vendored in `lists/public_suffix_list.dat`; refresh it with
|
|
7
|
+
* `npm run update-psl`.
|
|
8
|
+
*
|
|
9
|
+
* @see https://github.com/publicsuffix/list/wiki/Format for the rule format and
|
|
10
|
+
* the matching algorithm implemented in {@link publicSuffixLength}.
|
|
11
|
+
*/
|
|
12
|
+
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import { domainToASCII, fileURLToPath } from 'node:url';
|
|
14
|
+
/** Absolute path to the vendored Public Suffix List. */
|
|
15
|
+
export const publicSuffixListPath = fileURLToPath(new URL('../lists/public_suffix_list.dat', import.meta.url));
|
|
16
|
+
// Punycode a rule so it matches the ASCII hostnames the WHATWG URL parser
|
|
17
|
+
// produces (the list is UTF-8; hostnames come through as xn--…). A leftmost
|
|
18
|
+
// `*` wildcard is preserved.
|
|
19
|
+
function toAscii(name) {
|
|
20
|
+
if (name === '*')
|
|
21
|
+
return '*';
|
|
22
|
+
if (name.startsWith('*.')) {
|
|
23
|
+
const rest = domainToASCII(name.slice(2));
|
|
24
|
+
return rest ? `*.${rest}` : name.toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
return domainToASCII(name) || name.toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parse Public Suffix List text into a rule map. Each line is read only up to
|
|
30
|
+
* the first whitespace; `//` lines and blanks are skipped. The ICANN/PRIVATE
|
|
31
|
+
* section is tracked from the `===BEGIN …===` markers.
|
|
32
|
+
*/
|
|
33
|
+
export function parsePublicSuffixList(text) {
|
|
34
|
+
const rules = new Map();
|
|
35
|
+
let icann = true;
|
|
36
|
+
for (const rawLine of String(text).split(/\r?\n/)) {
|
|
37
|
+
if (rawLine.includes('===BEGIN ICANN DOMAINS===')) {
|
|
38
|
+
icann = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (rawLine.includes('===BEGIN PRIVATE DOMAINS===')) {
|
|
42
|
+
icann = false;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const token = rawLine.trim().split(/\s+/)[0] ?? '';
|
|
46
|
+
if (token === '' || token.startsWith('//'))
|
|
47
|
+
continue;
|
|
48
|
+
const exception = token.startsWith('!');
|
|
49
|
+
rules.set(toAscii(exception ? token.slice(1) : token), { exception, icann });
|
|
50
|
+
}
|
|
51
|
+
return rules;
|
|
52
|
+
}
|
|
53
|
+
const cache = new Map();
|
|
54
|
+
/** Read and parse a Public Suffix List file, memoized per path. */
|
|
55
|
+
export function loadPublicSuffixList(path = publicSuffixListPath) {
|
|
56
|
+
let pending = cache.get(path);
|
|
57
|
+
if (!pending) {
|
|
58
|
+
pending = readFile(path, 'utf8')
|
|
59
|
+
.then(parsePublicSuffixList)
|
|
60
|
+
.catch((error) => {
|
|
61
|
+
cache.delete(path);
|
|
62
|
+
throw error;
|
|
63
|
+
});
|
|
64
|
+
cache.set(path, pending);
|
|
65
|
+
}
|
|
66
|
+
return pending;
|
|
67
|
+
}
|
|
68
|
+
// Number of labels in the public suffix of `labels`, per the PSL algorithm:
|
|
69
|
+
// an exception rule prevails over any wildcard/normal rule; otherwise the rule
|
|
70
|
+
// with the most labels prevails; with no match the default rule is `*` (one
|
|
71
|
+
// label). An exception rule's suffix is one label shorter than the rule.
|
|
72
|
+
function publicSuffixLength(labels, rules, icannOnly) {
|
|
73
|
+
let best = null;
|
|
74
|
+
for (let i = 0; i < labels.length; i += 1) {
|
|
75
|
+
const length = labels.length - i;
|
|
76
|
+
const exact = labels.slice(i).join('.');
|
|
77
|
+
const wildcard = ['*', ...labels.slice(i + 1)].join('.');
|
|
78
|
+
for (const key of exact === wildcard ? [exact] : [exact, wildcard]) {
|
|
79
|
+
const rule = rules.get(key);
|
|
80
|
+
if (!rule || (icannOnly && !rule.icann))
|
|
81
|
+
continue;
|
|
82
|
+
if (!best ||
|
|
83
|
+
(rule.exception && !best.exception) ||
|
|
84
|
+
(rule.exception === best.exception && length > best.length)) {
|
|
85
|
+
best = { exception: rule.exception, length };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!best)
|
|
90
|
+
return 1;
|
|
91
|
+
return best.exception ? best.length - 1 : best.length;
|
|
92
|
+
}
|
|
93
|
+
function cleanHost(hostname) {
|
|
94
|
+
return String(hostname).toLowerCase().replace(/\.+$/, '');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The public suffix of `hostname` (e.g. "co.uk", "github.io"), or null when the
|
|
98
|
+
* hostname has fewer labels than the matching rule. `hostname` should already
|
|
99
|
+
* be a bare, lowercased, punycode host as produced by `normalizeDomain`.
|
|
100
|
+
*/
|
|
101
|
+
export function getPublicSuffix(hostname, rules, options = {}) {
|
|
102
|
+
const host = cleanHost(hostname);
|
|
103
|
+
if (host === '')
|
|
104
|
+
return null;
|
|
105
|
+
const labels = host.split('.');
|
|
106
|
+
const length = publicSuffixLength(labels, rules, options.icannOnly ?? false);
|
|
107
|
+
if (length > labels.length)
|
|
108
|
+
return null;
|
|
109
|
+
return labels.slice(labels.length - length).join('.');
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The registrable domain of `hostname` — its public suffix plus one label
|
|
113
|
+
* (e.g. "www.example.co.uk" → "example.co.uk"). Returns null when `hostname` is
|
|
114
|
+
* itself a public suffix (or shorter), i.e. has no registrable domain.
|
|
115
|
+
*/
|
|
116
|
+
export function getRegistrableDomain(hostname, rules, options = {}) {
|
|
117
|
+
const host = cleanHost(hostname);
|
|
118
|
+
if (host === '')
|
|
119
|
+
return null;
|
|
120
|
+
const labels = host.split('.');
|
|
121
|
+
const length = publicSuffixLength(labels, rules, options.icannOnly ?? false);
|
|
122
|
+
if (labels.length <= length)
|
|
123
|
+
return null;
|
|
124
|
+
return labels.slice(labels.length - length - 1).join('.');
|
|
125
|
+
}
|