@structured-world/structured-public-domains 0.0.7 → 0.0.8
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 +26 -0
- package/dist/chunk-EON4VJGA.js +121 -0
- package/dist/index.d.cts +2 -10
- package/dist/index.d.ts +2 -10
- package/dist/index.js +1 -123
- package/dist/psl.bin +0 -0
- package/dist/tiny.cjs +293 -0
- package/dist/tiny.d.cts +45 -0
- package/dist/tiny.d.ts +45 -0
- package/dist/tiny.js +167 -0
- package/dist/trie-jGeN4GI6.d.cts +11 -0
- package/dist/trie-jGeN4GI6.d.ts +11 -0
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -77,6 +77,32 @@ const bytes = pslData(); // Uint8Array — a defensive copy of the trie blob
|
|
|
77
77
|
The JS lookup is verified byte-for-byte against the Rust implementation over the
|
|
78
78
|
entire PSL on every CI run, so both languages return identical results.
|
|
79
79
|
|
|
80
|
+
### Tiny build (runtime-fetched, no embedded data)
|
|
81
|
+
|
|
82
|
+
For consumers who want always-fresh PSL **without bumping the package version**,
|
|
83
|
+
the `/tiny` entry ships *without* the embedded blob. It fetches the prebuilt
|
|
84
|
+
binary trie at runtime and caches it locally (Node: temp file with a TTL;
|
|
85
|
+
browser: CacheStorage). After the first `await load()`, the lookup API is
|
|
86
|
+
identical and synchronous.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { load, registrableDomain } from '@structured-world/structured-public-domains/tiny';
|
|
90
|
+
|
|
91
|
+
await load(); // fetch + cache once (default: jsDelivr CDN)
|
|
92
|
+
registrableDomain('sub.example.co.uk'); // "example.co.uk"
|
|
93
|
+
|
|
94
|
+
// Options: custom source, TTL, cache dir, or force refresh.
|
|
95
|
+
await load({ url: 'https://psl.example.com/psl.bin', cacheTtlMs: 3_600_000, force: true });
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The default source is the same `psl.bin` served from this package's jsDelivr CDN,
|
|
99
|
+
pinned to the installed `major.minor` range — so it always tracks the latest
|
|
100
|
+
PSL-data patch release (same trie format) but never a future format-breaking
|
|
101
|
+
version the bundled parser can't read. Results are identical to the embedded
|
|
102
|
+
build. Use the full `.` entry when you want zero network and instant startup; use
|
|
103
|
+
`/tiny` when install size and
|
|
104
|
+
always-current data matter more.
|
|
105
|
+
|
|
80
106
|
## Performance
|
|
81
107
|
|
|
82
108
|
Benchmarks on Apple M-series (criterion, `cargo bench`):
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/trie.ts
|
|
2
|
+
var utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
3
|
+
function parseTrie(data) {
|
|
4
|
+
const cursor = { pos: 0 };
|
|
5
|
+
const root = parseNode(data, cursor);
|
|
6
|
+
if (cursor.pos !== data.length) {
|
|
7
|
+
throw new Error("PSL data: trailing bytes after root node");
|
|
8
|
+
}
|
|
9
|
+
return root;
|
|
10
|
+
}
|
|
11
|
+
function parseNode(data, cursor) {
|
|
12
|
+
const flags = byteAt(data, cursor.pos++);
|
|
13
|
+
if ((flags & -2) !== 0) {
|
|
14
|
+
throw new Error("PSL data: reserved flag bits set");
|
|
15
|
+
}
|
|
16
|
+
const lo = byteAt(data, cursor.pos++);
|
|
17
|
+
const hi = byteAt(data, cursor.pos++);
|
|
18
|
+
let numChildren = lo | hi << 8;
|
|
19
|
+
const MIN_CHILD_ENCODED_LEN = 5;
|
|
20
|
+
const remaining = data.length - cursor.pos;
|
|
21
|
+
if (remaining < 0 || numChildren > Math.floor(remaining / MIN_CHILD_ENCODED_LEN)) {
|
|
22
|
+
throw new Error("PSL data: num_children exceeds remaining bytes");
|
|
23
|
+
}
|
|
24
|
+
const labels = [];
|
|
25
|
+
const children = [];
|
|
26
|
+
let prev;
|
|
27
|
+
for (; numChildren > 0; numChildren--) {
|
|
28
|
+
const labelLen = byteAt(data, cursor.pos++);
|
|
29
|
+
if (labelLen === 0) {
|
|
30
|
+
throw new Error("PSL data: empty label");
|
|
31
|
+
}
|
|
32
|
+
const labelEnd = cursor.pos + labelLen;
|
|
33
|
+
if (labelEnd > data.length) {
|
|
34
|
+
throw new Error("PSL data: label runs past end of data");
|
|
35
|
+
}
|
|
36
|
+
const label = decodeLabel(data, cursor.pos, labelEnd);
|
|
37
|
+
cursor.pos = labelEnd;
|
|
38
|
+
if (prev !== void 0 && !(label > prev)) {
|
|
39
|
+
throw new Error("PSL data: children not strictly sorted");
|
|
40
|
+
}
|
|
41
|
+
prev = label;
|
|
42
|
+
labels.push(label);
|
|
43
|
+
children.push(parseNode(data, cursor));
|
|
44
|
+
}
|
|
45
|
+
return { suffixBoundary: (flags & 1) !== 0, labels, children };
|
|
46
|
+
}
|
|
47
|
+
function byteAt(data, i) {
|
|
48
|
+
if (i >= data.length) {
|
|
49
|
+
throw new Error("PSL data: unexpected end of data");
|
|
50
|
+
}
|
|
51
|
+
return data[i];
|
|
52
|
+
}
|
|
53
|
+
function decodeLabel(data, start, end) {
|
|
54
|
+
for (let i = start; i < end; i++) {
|
|
55
|
+
if (data[i] >= 128) return utf8.decode(data.subarray(start, end));
|
|
56
|
+
}
|
|
57
|
+
let s = "";
|
|
58
|
+
for (let i = start; i < end; i++) s += String.fromCharCode(data[i]);
|
|
59
|
+
return s;
|
|
60
|
+
}
|
|
61
|
+
function indexOfChild(node, label) {
|
|
62
|
+
let lo = 0;
|
|
63
|
+
let hi = node.labels.length - 1;
|
|
64
|
+
while (lo <= hi) {
|
|
65
|
+
const mid = lo + hi >>> 1;
|
|
66
|
+
const cur = node.labels[mid];
|
|
67
|
+
if (cur === label) return mid;
|
|
68
|
+
if (cur < label) lo = mid + 1;
|
|
69
|
+
else hi = mid - 1;
|
|
70
|
+
}
|
|
71
|
+
return -1;
|
|
72
|
+
}
|
|
73
|
+
function childOf(node, label) {
|
|
74
|
+
const i = indexOfChild(node, label);
|
|
75
|
+
return i < 0 ? void 0 : node.children[i];
|
|
76
|
+
}
|
|
77
|
+
function hasChild(node, label) {
|
|
78
|
+
return indexOfChild(node, label) >= 0;
|
|
79
|
+
}
|
|
80
|
+
function lookupTrie(root, domain) {
|
|
81
|
+
const trimmed = domain.trim();
|
|
82
|
+
const stripped = trimmed.endsWith(".") ? trimmed.slice(0, -1) : trimmed;
|
|
83
|
+
if (stripped === "") return void 0;
|
|
84
|
+
const labels = stripped.split(".").reverse();
|
|
85
|
+
for (const label of labels) {
|
|
86
|
+
if (label === "" || label === "*" || label.startsWith("!")) return void 0;
|
|
87
|
+
}
|
|
88
|
+
let node = root;
|
|
89
|
+
let suffixDepth = 0;
|
|
90
|
+
let known = false;
|
|
91
|
+
for (let depth = 0; depth < labels.length; depth++) {
|
|
92
|
+
const label = labels[depth].toLowerCase();
|
|
93
|
+
if (hasChild(node, "*")) {
|
|
94
|
+
if (hasChild(node, "!" + label)) {
|
|
95
|
+
suffixDepth = depth;
|
|
96
|
+
} else {
|
|
97
|
+
suffixDepth = depth + 1;
|
|
98
|
+
}
|
|
99
|
+
known = true;
|
|
100
|
+
}
|
|
101
|
+
const child = childOf(node, label);
|
|
102
|
+
if (child !== void 0) {
|
|
103
|
+
if (child.suffixBoundary) {
|
|
104
|
+
suffixDepth = depth + 1;
|
|
105
|
+
known = true;
|
|
106
|
+
}
|
|
107
|
+
node = child;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
if (suffixDepth === 0) {
|
|
113
|
+
suffixDepth = 1;
|
|
114
|
+
known = false;
|
|
115
|
+
}
|
|
116
|
+
const suffix = labels.slice(0, suffixDepth).reverse().map((l) => l.toLowerCase()).join(".");
|
|
117
|
+
const registrableDomain = labels.length > suffixDepth ? `${labels[suffixDepth].toLowerCase()}.${suffix}` : void 0;
|
|
118
|
+
return { suffix, registrableDomain, known };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export { lookupTrie, parseTrie };
|
package/dist/index.d.cts
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
interface DomainInfo {
|
|
3
|
-
/** The public suffix (e.g. `"co.uk"`, `"com"`, `"github.io"`). */
|
|
4
|
-
readonly suffix: string;
|
|
5
|
-
/** The registrable domain (eTLD+1), or `undefined` if the input is itself a suffix. */
|
|
6
|
-
readonly registrableDomain: string | undefined;
|
|
7
|
-
/** Whether the suffix matched an explicit PSL rule (vs the `*` fallback). */
|
|
8
|
-
readonly known: boolean;
|
|
9
|
-
}
|
|
1
|
+
import { D as DomainInfo } from './trie-jGeN4GI6.cjs';
|
|
10
2
|
|
|
11
3
|
/**
|
|
12
4
|
* Look up a domain in the Public Suffix List.
|
|
@@ -41,4 +33,4 @@ declare function isKnownSuffix(domain: string): boolean;
|
|
|
41
33
|
*/
|
|
42
34
|
declare function pslData(): Uint8Array;
|
|
43
35
|
|
|
44
|
-
export {
|
|
36
|
+
export { DomainInfo, isKnownSuffix, lookup, pslData, registrableDomain };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
interface DomainInfo {
|
|
3
|
-
/** The public suffix (e.g. `"co.uk"`, `"com"`, `"github.io"`). */
|
|
4
|
-
readonly suffix: string;
|
|
5
|
-
/** The registrable domain (eTLD+1), or `undefined` if the input is itself a suffix. */
|
|
6
|
-
readonly registrableDomain: string | undefined;
|
|
7
|
-
/** Whether the suffix matched an explicit PSL rule (vs the `*` fallback). */
|
|
8
|
-
readonly known: boolean;
|
|
9
|
-
}
|
|
1
|
+
import { D as DomainInfo } from './trie-jGeN4GI6.js';
|
|
10
2
|
|
|
11
3
|
/**
|
|
12
4
|
* Look up a domain in the Public Suffix List.
|
|
@@ -41,4 +33,4 @@ declare function isKnownSuffix(domain: string): boolean;
|
|
|
41
33
|
*/
|
|
42
34
|
declare function pslData(): Uint8Array;
|
|
43
35
|
|
|
44
|
-
export {
|
|
36
|
+
export { DomainInfo, isKnownSuffix, lookup, pslData, registrableDomain };
|
package/dist/index.js
CHANGED
|
@@ -1,128 +1,6 @@
|
|
|
1
|
+
import { lookupTrie, parseTrie } from './chunk-EON4VJGA.js';
|
|
1
2
|
import { PSL_BASE64 } from './psl-data.cjs';
|
|
2
3
|
|
|
3
|
-
// src/index.ts
|
|
4
|
-
|
|
5
|
-
// src/trie.ts
|
|
6
|
-
var utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
7
|
-
function parseTrie(data) {
|
|
8
|
-
const cursor = { pos: 0 };
|
|
9
|
-
const root = parseNode(data, cursor);
|
|
10
|
-
if (cursor.pos !== data.length) {
|
|
11
|
-
throw new Error("PSL data: trailing bytes after root node");
|
|
12
|
-
}
|
|
13
|
-
return root;
|
|
14
|
-
}
|
|
15
|
-
function parseNode(data, cursor) {
|
|
16
|
-
const flags = byteAt(data, cursor.pos++);
|
|
17
|
-
if ((flags & -2) !== 0) {
|
|
18
|
-
throw new Error("PSL data: reserved flag bits set");
|
|
19
|
-
}
|
|
20
|
-
const lo = byteAt(data, cursor.pos++);
|
|
21
|
-
const hi = byteAt(data, cursor.pos++);
|
|
22
|
-
let numChildren = lo | hi << 8;
|
|
23
|
-
const MIN_CHILD_ENCODED_LEN = 5;
|
|
24
|
-
const remaining = data.length - cursor.pos;
|
|
25
|
-
if (remaining < 0 || numChildren > Math.floor(remaining / MIN_CHILD_ENCODED_LEN)) {
|
|
26
|
-
throw new Error("PSL data: num_children exceeds remaining bytes");
|
|
27
|
-
}
|
|
28
|
-
const labels = [];
|
|
29
|
-
const children = [];
|
|
30
|
-
let prev;
|
|
31
|
-
for (; numChildren > 0; numChildren--) {
|
|
32
|
-
const labelLen = byteAt(data, cursor.pos++);
|
|
33
|
-
if (labelLen === 0) {
|
|
34
|
-
throw new Error("PSL data: empty label");
|
|
35
|
-
}
|
|
36
|
-
const labelEnd = cursor.pos + labelLen;
|
|
37
|
-
if (labelEnd > data.length) {
|
|
38
|
-
throw new Error("PSL data: label runs past end of data");
|
|
39
|
-
}
|
|
40
|
-
const label = decodeLabel(data, cursor.pos, labelEnd);
|
|
41
|
-
cursor.pos = labelEnd;
|
|
42
|
-
if (prev !== void 0 && !(label > prev)) {
|
|
43
|
-
throw new Error("PSL data: children not strictly sorted");
|
|
44
|
-
}
|
|
45
|
-
prev = label;
|
|
46
|
-
labels.push(label);
|
|
47
|
-
children.push(parseNode(data, cursor));
|
|
48
|
-
}
|
|
49
|
-
return { suffixBoundary: (flags & 1) !== 0, labels, children };
|
|
50
|
-
}
|
|
51
|
-
function byteAt(data, i) {
|
|
52
|
-
if (i >= data.length) {
|
|
53
|
-
throw new Error("PSL data: unexpected end of data");
|
|
54
|
-
}
|
|
55
|
-
return data[i];
|
|
56
|
-
}
|
|
57
|
-
function decodeLabel(data, start, end) {
|
|
58
|
-
for (let i = start; i < end; i++) {
|
|
59
|
-
if (data[i] >= 128) return utf8.decode(data.subarray(start, end));
|
|
60
|
-
}
|
|
61
|
-
let s = "";
|
|
62
|
-
for (let i = start; i < end; i++) s += String.fromCharCode(data[i]);
|
|
63
|
-
return s;
|
|
64
|
-
}
|
|
65
|
-
function indexOfChild(node, label) {
|
|
66
|
-
let lo = 0;
|
|
67
|
-
let hi = node.labels.length - 1;
|
|
68
|
-
while (lo <= hi) {
|
|
69
|
-
const mid = lo + hi >>> 1;
|
|
70
|
-
const cur = node.labels[mid];
|
|
71
|
-
if (cur === label) return mid;
|
|
72
|
-
if (cur < label) lo = mid + 1;
|
|
73
|
-
else hi = mid - 1;
|
|
74
|
-
}
|
|
75
|
-
return -1;
|
|
76
|
-
}
|
|
77
|
-
function childOf(node, label) {
|
|
78
|
-
const i = indexOfChild(node, label);
|
|
79
|
-
return i < 0 ? void 0 : node.children[i];
|
|
80
|
-
}
|
|
81
|
-
function hasChild(node, label) {
|
|
82
|
-
return indexOfChild(node, label) >= 0;
|
|
83
|
-
}
|
|
84
|
-
function lookupTrie(root, domain) {
|
|
85
|
-
const trimmed = domain.trim();
|
|
86
|
-
const stripped = trimmed.endsWith(".") ? trimmed.slice(0, -1) : trimmed;
|
|
87
|
-
if (stripped === "") return void 0;
|
|
88
|
-
const labels = stripped.split(".").reverse();
|
|
89
|
-
for (const label of labels) {
|
|
90
|
-
if (label === "" || label === "*" || label.startsWith("!")) return void 0;
|
|
91
|
-
}
|
|
92
|
-
let node = root;
|
|
93
|
-
let suffixDepth = 0;
|
|
94
|
-
let known = false;
|
|
95
|
-
for (let depth = 0; depth < labels.length; depth++) {
|
|
96
|
-
const label = labels[depth].toLowerCase();
|
|
97
|
-
if (hasChild(node, "*")) {
|
|
98
|
-
if (hasChild(node, "!" + label)) {
|
|
99
|
-
suffixDepth = depth;
|
|
100
|
-
} else {
|
|
101
|
-
suffixDepth = depth + 1;
|
|
102
|
-
}
|
|
103
|
-
known = true;
|
|
104
|
-
}
|
|
105
|
-
const child = childOf(node, label);
|
|
106
|
-
if (child !== void 0) {
|
|
107
|
-
if (child.suffixBoundary) {
|
|
108
|
-
suffixDepth = depth + 1;
|
|
109
|
-
known = true;
|
|
110
|
-
}
|
|
111
|
-
node = child;
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
break;
|
|
115
|
-
}
|
|
116
|
-
if (suffixDepth === 0) {
|
|
117
|
-
suffixDepth = 1;
|
|
118
|
-
known = false;
|
|
119
|
-
}
|
|
120
|
-
const suffix = labels.slice(0, suffixDepth).reverse().map((l) => l.toLowerCase()).join(".");
|
|
121
|
-
const registrableDomain2 = labels.length > suffixDepth ? `${labels[suffixDepth].toLowerCase()}.${suffix}` : void 0;
|
|
122
|
-
return { suffix, registrableDomain: registrableDomain2, known };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// src/index.ts
|
|
126
4
|
var cachedBytes;
|
|
127
5
|
var cachedTrie;
|
|
128
6
|
function decodeBase64(b64) {
|
package/dist/psl.bin
ADDED
|
Binary file
|
package/dist/tiny.cjs
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/trie.ts
|
|
4
|
+
var utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
5
|
+
function parseTrie(data) {
|
|
6
|
+
const cursor = { pos: 0 };
|
|
7
|
+
const root = parseNode(data, cursor);
|
|
8
|
+
if (cursor.pos !== data.length) {
|
|
9
|
+
throw new Error("PSL data: trailing bytes after root node");
|
|
10
|
+
}
|
|
11
|
+
return root;
|
|
12
|
+
}
|
|
13
|
+
function parseNode(data, cursor) {
|
|
14
|
+
const flags = byteAt(data, cursor.pos++);
|
|
15
|
+
if ((flags & -2) !== 0) {
|
|
16
|
+
throw new Error("PSL data: reserved flag bits set");
|
|
17
|
+
}
|
|
18
|
+
const lo = byteAt(data, cursor.pos++);
|
|
19
|
+
const hi = byteAt(data, cursor.pos++);
|
|
20
|
+
let numChildren = lo | hi << 8;
|
|
21
|
+
const MIN_CHILD_ENCODED_LEN = 5;
|
|
22
|
+
const remaining = data.length - cursor.pos;
|
|
23
|
+
if (remaining < 0 || numChildren > Math.floor(remaining / MIN_CHILD_ENCODED_LEN)) {
|
|
24
|
+
throw new Error("PSL data: num_children exceeds remaining bytes");
|
|
25
|
+
}
|
|
26
|
+
const labels = [];
|
|
27
|
+
const children = [];
|
|
28
|
+
let prev;
|
|
29
|
+
for (; numChildren > 0; numChildren--) {
|
|
30
|
+
const labelLen = byteAt(data, cursor.pos++);
|
|
31
|
+
if (labelLen === 0) {
|
|
32
|
+
throw new Error("PSL data: empty label");
|
|
33
|
+
}
|
|
34
|
+
const labelEnd = cursor.pos + labelLen;
|
|
35
|
+
if (labelEnd > data.length) {
|
|
36
|
+
throw new Error("PSL data: label runs past end of data");
|
|
37
|
+
}
|
|
38
|
+
const label = decodeLabel(data, cursor.pos, labelEnd);
|
|
39
|
+
cursor.pos = labelEnd;
|
|
40
|
+
if (prev !== void 0 && !(label > prev)) {
|
|
41
|
+
throw new Error("PSL data: children not strictly sorted");
|
|
42
|
+
}
|
|
43
|
+
prev = label;
|
|
44
|
+
labels.push(label);
|
|
45
|
+
children.push(parseNode(data, cursor));
|
|
46
|
+
}
|
|
47
|
+
return { suffixBoundary: (flags & 1) !== 0, labels, children };
|
|
48
|
+
}
|
|
49
|
+
function byteAt(data, i) {
|
|
50
|
+
if (i >= data.length) {
|
|
51
|
+
throw new Error("PSL data: unexpected end of data");
|
|
52
|
+
}
|
|
53
|
+
return data[i];
|
|
54
|
+
}
|
|
55
|
+
function decodeLabel(data, start, end) {
|
|
56
|
+
for (let i = start; i < end; i++) {
|
|
57
|
+
if (data[i] >= 128) return utf8.decode(data.subarray(start, end));
|
|
58
|
+
}
|
|
59
|
+
let s = "";
|
|
60
|
+
for (let i = start; i < end; i++) s += String.fromCharCode(data[i]);
|
|
61
|
+
return s;
|
|
62
|
+
}
|
|
63
|
+
function indexOfChild(node, label) {
|
|
64
|
+
let lo = 0;
|
|
65
|
+
let hi = node.labels.length - 1;
|
|
66
|
+
while (lo <= hi) {
|
|
67
|
+
const mid = lo + hi >>> 1;
|
|
68
|
+
const cur = node.labels[mid];
|
|
69
|
+
if (cur === label) return mid;
|
|
70
|
+
if (cur < label) lo = mid + 1;
|
|
71
|
+
else hi = mid - 1;
|
|
72
|
+
}
|
|
73
|
+
return -1;
|
|
74
|
+
}
|
|
75
|
+
function childOf(node, label) {
|
|
76
|
+
const i = indexOfChild(node, label);
|
|
77
|
+
return i < 0 ? void 0 : node.children[i];
|
|
78
|
+
}
|
|
79
|
+
function hasChild(node, label) {
|
|
80
|
+
return indexOfChild(node, label) >= 0;
|
|
81
|
+
}
|
|
82
|
+
function lookupTrie(root, domain) {
|
|
83
|
+
const trimmed = domain.trim();
|
|
84
|
+
const stripped = trimmed.endsWith(".") ? trimmed.slice(0, -1) : trimmed;
|
|
85
|
+
if (stripped === "") return void 0;
|
|
86
|
+
const labels = stripped.split(".").reverse();
|
|
87
|
+
for (const label of labels) {
|
|
88
|
+
if (label === "" || label === "*" || label.startsWith("!")) return void 0;
|
|
89
|
+
}
|
|
90
|
+
let node = root;
|
|
91
|
+
let suffixDepth = 0;
|
|
92
|
+
let known = false;
|
|
93
|
+
for (let depth = 0; depth < labels.length; depth++) {
|
|
94
|
+
const label = labels[depth].toLowerCase();
|
|
95
|
+
if (hasChild(node, "*")) {
|
|
96
|
+
if (hasChild(node, "!" + label)) {
|
|
97
|
+
suffixDepth = depth;
|
|
98
|
+
} else {
|
|
99
|
+
suffixDepth = depth + 1;
|
|
100
|
+
}
|
|
101
|
+
known = true;
|
|
102
|
+
}
|
|
103
|
+
const child = childOf(node, label);
|
|
104
|
+
if (child !== void 0) {
|
|
105
|
+
if (child.suffixBoundary) {
|
|
106
|
+
suffixDepth = depth + 1;
|
|
107
|
+
known = true;
|
|
108
|
+
}
|
|
109
|
+
node = child;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
if (suffixDepth === 0) {
|
|
115
|
+
suffixDepth = 1;
|
|
116
|
+
known = false;
|
|
117
|
+
}
|
|
118
|
+
const suffix = labels.slice(0, suffixDepth).reverse().map((l) => l.toLowerCase()).join(".");
|
|
119
|
+
const registrableDomain2 = labels.length > suffixDepth ? `${labels[suffixDepth].toLowerCase()}.${suffix}` : void 0;
|
|
120
|
+
return { suffix, registrableDomain: registrableDomain2, known };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/tiny.ts
|
|
124
|
+
var PSL_RANGE = "0.0" ;
|
|
125
|
+
var DEFAULT_PSL_URL = `https://cdn.jsdelivr.net/npm/@structured-world/structured-public-domains@${PSL_RANGE}/dist/psl.bin`;
|
|
126
|
+
var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
127
|
+
var isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
128
|
+
var cachedBytes;
|
|
129
|
+
var cachedTrie;
|
|
130
|
+
var inFlight;
|
|
131
|
+
var latestGen = 0;
|
|
132
|
+
var latestSettled;
|
|
133
|
+
function load(opts = {}) {
|
|
134
|
+
if (cachedTrie !== void 0 && opts.force !== true) return Promise.resolve();
|
|
135
|
+
if (inFlight !== void 0 && opts.force !== true) return inFlight;
|
|
136
|
+
const gen = ++latestGen;
|
|
137
|
+
const pending = doLoad(opts, gen);
|
|
138
|
+
latestSettled = pending;
|
|
139
|
+
inFlight = pending;
|
|
140
|
+
pending.finally(() => {
|
|
141
|
+
if (inFlight === pending) inFlight = void 0;
|
|
142
|
+
}).catch(() => void 0);
|
|
143
|
+
return pending;
|
|
144
|
+
}
|
|
145
|
+
function mirrorLatest() {
|
|
146
|
+
return latestSettled ?? Promise.resolve();
|
|
147
|
+
}
|
|
148
|
+
async function doLoad(opts, gen) {
|
|
149
|
+
let data;
|
|
150
|
+
let trie;
|
|
151
|
+
try {
|
|
152
|
+
[data, trie] = await fetchAndParse(opts);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
if (gen !== latestGen) return mirrorLatest();
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
if (gen !== latestGen) return mirrorLatest();
|
|
158
|
+
cachedBytes = data;
|
|
159
|
+
cachedTrie = trie;
|
|
160
|
+
}
|
|
161
|
+
async function fetchAndParse(opts) {
|
|
162
|
+
const url = opts.url ?? DEFAULT_PSL_URL;
|
|
163
|
+
const ttlMs = opts.cacheTtlMs ?? DEFAULT_TTL_MS;
|
|
164
|
+
const useCache = opts.cache ?? true;
|
|
165
|
+
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
166
|
+
if (useCache && opts.force !== true) {
|
|
167
|
+
const cached = await readCache(url, ttlMs, opts.cacheDir);
|
|
168
|
+
if (cached !== void 0) {
|
|
169
|
+
try {
|
|
170
|
+
return [cached, parseTrie(cached)];
|
|
171
|
+
} catch {
|
|
172
|
+
await deleteCache(url, opts.cacheDir).catch(() => void 0);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (typeof doFetch !== "function") {
|
|
177
|
+
throw new Error("no fetch implementation available; pass `fetch` in LoadOptions");
|
|
178
|
+
}
|
|
179
|
+
const data = await fetchBytes(doFetch, url);
|
|
180
|
+
const trie = parseTrie(data);
|
|
181
|
+
if (useCache) await writeCache(url, data, opts.cacheDir).catch(() => void 0);
|
|
182
|
+
return [data, trie];
|
|
183
|
+
}
|
|
184
|
+
function isLoaded() {
|
|
185
|
+
return cachedTrie !== void 0;
|
|
186
|
+
}
|
|
187
|
+
function requireTrie() {
|
|
188
|
+
if (cachedTrie === void 0) {
|
|
189
|
+
throw new Error("PSL data not loaded \u2014 call `await load()` before lookups");
|
|
190
|
+
}
|
|
191
|
+
return cachedTrie;
|
|
192
|
+
}
|
|
193
|
+
function lookup(domain) {
|
|
194
|
+
return lookupTrie(requireTrie(), domain);
|
|
195
|
+
}
|
|
196
|
+
function registrableDomain(domain) {
|
|
197
|
+
return lookup(domain)?.registrableDomain;
|
|
198
|
+
}
|
|
199
|
+
function isKnownSuffix(domain) {
|
|
200
|
+
return lookup(domain)?.known ?? false;
|
|
201
|
+
}
|
|
202
|
+
function pslData() {
|
|
203
|
+
if (cachedBytes === void 0) {
|
|
204
|
+
throw new Error("PSL data not loaded \u2014 call `await load()` before pslData()");
|
|
205
|
+
}
|
|
206
|
+
return cachedBytes.slice();
|
|
207
|
+
}
|
|
208
|
+
async function fetchBytes(doFetch, url) {
|
|
209
|
+
const res = await doFetch(url);
|
|
210
|
+
if (!res.ok) {
|
|
211
|
+
throw new Error(`failed to fetch PSL data from ${url}: ${res.status} ${res.statusText}`);
|
|
212
|
+
}
|
|
213
|
+
const data = new Uint8Array(await res.arrayBuffer());
|
|
214
|
+
if (data.length < 1024) {
|
|
215
|
+
throw new Error(`PSL data from ${url} is implausibly small (${data.length} bytes)`);
|
|
216
|
+
}
|
|
217
|
+
return data;
|
|
218
|
+
}
|
|
219
|
+
function cacheKey(url) {
|
|
220
|
+
let h = 2166136261;
|
|
221
|
+
for (let i = 0; i < url.length; i++) {
|
|
222
|
+
h ^= url.charCodeAt(i);
|
|
223
|
+
h = Math.imul(h, 16777619);
|
|
224
|
+
}
|
|
225
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
226
|
+
}
|
|
227
|
+
async function readCache(url, ttlMs, cacheDir) {
|
|
228
|
+
try {
|
|
229
|
+
return isNode ? await readNodeCache(url, ttlMs, cacheDir) : await readBrowserCache(url, ttlMs);
|
|
230
|
+
} catch {
|
|
231
|
+
return void 0;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function writeCache(url, data, cacheDir) {
|
|
235
|
+
if (isNode) await writeNodeCache(url, data, cacheDir);
|
|
236
|
+
else await writeBrowserCache(url, data);
|
|
237
|
+
}
|
|
238
|
+
async function deleteCache(url, cacheDir) {
|
|
239
|
+
if (isNode) {
|
|
240
|
+
const { fs, file } = await nodeCache(url, cacheDir);
|
|
241
|
+
fs.rmSync(file, { force: true });
|
|
242
|
+
} else if (typeof caches !== "undefined") {
|
|
243
|
+
const cache = await caches.open("structured-public-domains");
|
|
244
|
+
await cache.delete(url);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
var nodeRequire = (m) => import(
|
|
248
|
+
/* @vite-ignore */
|
|
249
|
+
m
|
|
250
|
+
);
|
|
251
|
+
async function nodeCache(url, cacheDir) {
|
|
252
|
+
const fs = await nodeRequire("node:fs");
|
|
253
|
+
const os = await nodeRequire("node:os");
|
|
254
|
+
const path = await nodeRequire("node:path");
|
|
255
|
+
const dir = cacheDir ?? path.join(os.tmpdir(), "structured-public-domains-cache");
|
|
256
|
+
return { fs, dir, file: path.join(dir, `psl-${cacheKey(url)}.bin`) };
|
|
257
|
+
}
|
|
258
|
+
async function readNodeCache(url, ttlMs, cacheDir) {
|
|
259
|
+
const { fs, file } = await nodeCache(url, cacheDir);
|
|
260
|
+
const stat = fs.statSync(file);
|
|
261
|
+
if (Date.now() - stat.mtimeMs >= ttlMs) return void 0;
|
|
262
|
+
return new Uint8Array(fs.readFileSync(file));
|
|
263
|
+
}
|
|
264
|
+
async function writeNodeCache(url, data, cacheDir) {
|
|
265
|
+
const { fs, dir, file } = await nodeCache(url, cacheDir);
|
|
266
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
267
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
268
|
+
fs.writeFileSync(tmp, data);
|
|
269
|
+
fs.renameSync(tmp, file);
|
|
270
|
+
}
|
|
271
|
+
async function readBrowserCache(url, ttlMs) {
|
|
272
|
+
if (typeof caches === "undefined") return void 0;
|
|
273
|
+
const cache = await caches.open("structured-public-domains");
|
|
274
|
+
const res = await cache.match(url);
|
|
275
|
+
if (res == null) return void 0;
|
|
276
|
+
const cachedAt = Number(res.headers.get("x-cached-at") ?? "0");
|
|
277
|
+
if (!Number.isFinite(cachedAt) || Date.now() - cachedAt >= ttlMs) return void 0;
|
|
278
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
279
|
+
}
|
|
280
|
+
async function writeBrowserCache(url, data) {
|
|
281
|
+
if (typeof caches === "undefined") return;
|
|
282
|
+
const cache = await caches.open("structured-public-domains");
|
|
283
|
+
const body = data.slice();
|
|
284
|
+
await cache.put(url, new Response(body, { headers: { "x-cached-at": String(Date.now()) } }));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
exports.DEFAULT_PSL_URL = DEFAULT_PSL_URL;
|
|
288
|
+
exports.isKnownSuffix = isKnownSuffix;
|
|
289
|
+
exports.isLoaded = isLoaded;
|
|
290
|
+
exports.load = load;
|
|
291
|
+
exports.lookup = lookup;
|
|
292
|
+
exports.pslData = pslData;
|
|
293
|
+
exports.registrableDomain = registrableDomain;
|
package/dist/tiny.d.cts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { D as DomainInfo } from './trie-jGeN4GI6.cjs';
|
|
2
|
+
|
|
3
|
+
/** Default data source: the prebuilt trie served from this package via jsDelivr's CDN. */
|
|
4
|
+
declare const DEFAULT_PSL_URL: string;
|
|
5
|
+
/** Options for {@link load}. */
|
|
6
|
+
interface LoadOptions {
|
|
7
|
+
/** Where to fetch `psl.bin` from. Defaults to {@link DEFAULT_PSL_URL}. */
|
|
8
|
+
url?: string;
|
|
9
|
+
/** Custom fetch implementation (e.g. for proxies/tests). Defaults to the global `fetch`. */
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
/** Enable the persistent local cache (Node file / browser CacheStorage). Default `true`. */
|
|
12
|
+
cache?: boolean;
|
|
13
|
+
/** Cache freshness window in milliseconds. Default 24h. */
|
|
14
|
+
cacheTtlMs?: number;
|
|
15
|
+
/** Node only: directory for the cache file. Defaults to an OS temp subdir. */
|
|
16
|
+
cacheDir?: string;
|
|
17
|
+
/** Re-fetch even if already loaded / cached. Default `false`. */
|
|
18
|
+
force?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Fetch, cache, and parse the PSL trie. Idempotent: a second call is a no-op
|
|
22
|
+
* unless `force` is set. Must be awaited before {@link lookup} and friends.
|
|
23
|
+
*
|
|
24
|
+
* @throws if the data cannot be fetched or is implausibly small / corrupt.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { load, registrableDomain } from '@structured-world/structured-public-domains/tiny';
|
|
29
|
+
* await load();
|
|
30
|
+
* registrableDomain('sub.example.co.uk'); // "example.co.uk"
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
declare function load(opts?: LoadOptions): Promise<void>;
|
|
34
|
+
/** Whether {@link load} has completed and the lookup API is ready. */
|
|
35
|
+
declare function isLoaded(): boolean;
|
|
36
|
+
/** Look up a domain. Requires a prior `await load()`. See the main entry's `lookup`. */
|
|
37
|
+
declare function lookup(domain: string): DomainInfo | undefined;
|
|
38
|
+
/** Extract the registrable domain (eTLD+1). Requires a prior `await load()`. */
|
|
39
|
+
declare function registrableDomain(domain: string): string | undefined;
|
|
40
|
+
/** Whether a domain's suffix is a known PSL entry. Requires a prior `await load()`. */
|
|
41
|
+
declare function isKnownSuffix(domain: string): boolean;
|
|
42
|
+
/** The raw binary trie blob that was loaded (defensive copy). Requires a prior `await load()`. */
|
|
43
|
+
declare function pslData(): Uint8Array;
|
|
44
|
+
|
|
45
|
+
export { DEFAULT_PSL_URL, DomainInfo, type LoadOptions, isKnownSuffix, isLoaded, load, lookup, pslData, registrableDomain };
|
package/dist/tiny.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { D as DomainInfo } from './trie-jGeN4GI6.js';
|
|
2
|
+
|
|
3
|
+
/** Default data source: the prebuilt trie served from this package via jsDelivr's CDN. */
|
|
4
|
+
declare const DEFAULT_PSL_URL: string;
|
|
5
|
+
/** Options for {@link load}. */
|
|
6
|
+
interface LoadOptions {
|
|
7
|
+
/** Where to fetch `psl.bin` from. Defaults to {@link DEFAULT_PSL_URL}. */
|
|
8
|
+
url?: string;
|
|
9
|
+
/** Custom fetch implementation (e.g. for proxies/tests). Defaults to the global `fetch`. */
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
/** Enable the persistent local cache (Node file / browser CacheStorage). Default `true`. */
|
|
12
|
+
cache?: boolean;
|
|
13
|
+
/** Cache freshness window in milliseconds. Default 24h. */
|
|
14
|
+
cacheTtlMs?: number;
|
|
15
|
+
/** Node only: directory for the cache file. Defaults to an OS temp subdir. */
|
|
16
|
+
cacheDir?: string;
|
|
17
|
+
/** Re-fetch even if already loaded / cached. Default `false`. */
|
|
18
|
+
force?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Fetch, cache, and parse the PSL trie. Idempotent: a second call is a no-op
|
|
22
|
+
* unless `force` is set. Must be awaited before {@link lookup} and friends.
|
|
23
|
+
*
|
|
24
|
+
* @throws if the data cannot be fetched or is implausibly small / corrupt.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { load, registrableDomain } from '@structured-world/structured-public-domains/tiny';
|
|
29
|
+
* await load();
|
|
30
|
+
* registrableDomain('sub.example.co.uk'); // "example.co.uk"
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
declare function load(opts?: LoadOptions): Promise<void>;
|
|
34
|
+
/** Whether {@link load} has completed and the lookup API is ready. */
|
|
35
|
+
declare function isLoaded(): boolean;
|
|
36
|
+
/** Look up a domain. Requires a prior `await load()`. See the main entry's `lookup`. */
|
|
37
|
+
declare function lookup(domain: string): DomainInfo | undefined;
|
|
38
|
+
/** Extract the registrable domain (eTLD+1). Requires a prior `await load()`. */
|
|
39
|
+
declare function registrableDomain(domain: string): string | undefined;
|
|
40
|
+
/** Whether a domain's suffix is a known PSL entry. Requires a prior `await load()`. */
|
|
41
|
+
declare function isKnownSuffix(domain: string): boolean;
|
|
42
|
+
/** The raw binary trie blob that was loaded (defensive copy). Requires a prior `await load()`. */
|
|
43
|
+
declare function pslData(): Uint8Array;
|
|
44
|
+
|
|
45
|
+
export { DEFAULT_PSL_URL, DomainInfo, type LoadOptions, isKnownSuffix, isLoaded, load, lookup, pslData, registrableDomain };
|
package/dist/tiny.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { parseTrie, lookupTrie } from './chunk-EON4VJGA.js';
|
|
2
|
+
|
|
3
|
+
// src/tiny.ts
|
|
4
|
+
var PSL_RANGE = "0.0" ;
|
|
5
|
+
var DEFAULT_PSL_URL = `https://cdn.jsdelivr.net/npm/@structured-world/structured-public-domains@${PSL_RANGE}/dist/psl.bin`;
|
|
6
|
+
var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
7
|
+
var isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
8
|
+
var cachedBytes;
|
|
9
|
+
var cachedTrie;
|
|
10
|
+
var inFlight;
|
|
11
|
+
var latestGen = 0;
|
|
12
|
+
var latestSettled;
|
|
13
|
+
function load(opts = {}) {
|
|
14
|
+
if (cachedTrie !== void 0 && opts.force !== true) return Promise.resolve();
|
|
15
|
+
if (inFlight !== void 0 && opts.force !== true) return inFlight;
|
|
16
|
+
const gen = ++latestGen;
|
|
17
|
+
const pending = doLoad(opts, gen);
|
|
18
|
+
latestSettled = pending;
|
|
19
|
+
inFlight = pending;
|
|
20
|
+
pending.finally(() => {
|
|
21
|
+
if (inFlight === pending) inFlight = void 0;
|
|
22
|
+
}).catch(() => void 0);
|
|
23
|
+
return pending;
|
|
24
|
+
}
|
|
25
|
+
function mirrorLatest() {
|
|
26
|
+
return latestSettled ?? Promise.resolve();
|
|
27
|
+
}
|
|
28
|
+
async function doLoad(opts, gen) {
|
|
29
|
+
let data;
|
|
30
|
+
let trie;
|
|
31
|
+
try {
|
|
32
|
+
[data, trie] = await fetchAndParse(opts);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (gen !== latestGen) return mirrorLatest();
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
if (gen !== latestGen) return mirrorLatest();
|
|
38
|
+
cachedBytes = data;
|
|
39
|
+
cachedTrie = trie;
|
|
40
|
+
}
|
|
41
|
+
async function fetchAndParse(opts) {
|
|
42
|
+
const url = opts.url ?? DEFAULT_PSL_URL;
|
|
43
|
+
const ttlMs = opts.cacheTtlMs ?? DEFAULT_TTL_MS;
|
|
44
|
+
const useCache = opts.cache ?? true;
|
|
45
|
+
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
46
|
+
if (useCache && opts.force !== true) {
|
|
47
|
+
const cached = await readCache(url, ttlMs, opts.cacheDir);
|
|
48
|
+
if (cached !== void 0) {
|
|
49
|
+
try {
|
|
50
|
+
return [cached, parseTrie(cached)];
|
|
51
|
+
} catch {
|
|
52
|
+
await deleteCache(url, opts.cacheDir).catch(() => void 0);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (typeof doFetch !== "function") {
|
|
57
|
+
throw new Error("no fetch implementation available; pass `fetch` in LoadOptions");
|
|
58
|
+
}
|
|
59
|
+
const data = await fetchBytes(doFetch, url);
|
|
60
|
+
const trie = parseTrie(data);
|
|
61
|
+
if (useCache) await writeCache(url, data, opts.cacheDir).catch(() => void 0);
|
|
62
|
+
return [data, trie];
|
|
63
|
+
}
|
|
64
|
+
function isLoaded() {
|
|
65
|
+
return cachedTrie !== void 0;
|
|
66
|
+
}
|
|
67
|
+
function requireTrie() {
|
|
68
|
+
if (cachedTrie === void 0) {
|
|
69
|
+
throw new Error("PSL data not loaded \u2014 call `await load()` before lookups");
|
|
70
|
+
}
|
|
71
|
+
return cachedTrie;
|
|
72
|
+
}
|
|
73
|
+
function lookup(domain) {
|
|
74
|
+
return lookupTrie(requireTrie(), domain);
|
|
75
|
+
}
|
|
76
|
+
function registrableDomain(domain) {
|
|
77
|
+
return lookup(domain)?.registrableDomain;
|
|
78
|
+
}
|
|
79
|
+
function isKnownSuffix(domain) {
|
|
80
|
+
return lookup(domain)?.known ?? false;
|
|
81
|
+
}
|
|
82
|
+
function pslData() {
|
|
83
|
+
if (cachedBytes === void 0) {
|
|
84
|
+
throw new Error("PSL data not loaded \u2014 call `await load()` before pslData()");
|
|
85
|
+
}
|
|
86
|
+
return cachedBytes.slice();
|
|
87
|
+
}
|
|
88
|
+
async function fetchBytes(doFetch, url) {
|
|
89
|
+
const res = await doFetch(url);
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
throw new Error(`failed to fetch PSL data from ${url}: ${res.status} ${res.statusText}`);
|
|
92
|
+
}
|
|
93
|
+
const data = new Uint8Array(await res.arrayBuffer());
|
|
94
|
+
if (data.length < 1024) {
|
|
95
|
+
throw new Error(`PSL data from ${url} is implausibly small (${data.length} bytes)`);
|
|
96
|
+
}
|
|
97
|
+
return data;
|
|
98
|
+
}
|
|
99
|
+
function cacheKey(url) {
|
|
100
|
+
let h = 2166136261;
|
|
101
|
+
for (let i = 0; i < url.length; i++) {
|
|
102
|
+
h ^= url.charCodeAt(i);
|
|
103
|
+
h = Math.imul(h, 16777619);
|
|
104
|
+
}
|
|
105
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
106
|
+
}
|
|
107
|
+
async function readCache(url, ttlMs, cacheDir) {
|
|
108
|
+
try {
|
|
109
|
+
return isNode ? await readNodeCache(url, ttlMs, cacheDir) : await readBrowserCache(url, ttlMs);
|
|
110
|
+
} catch {
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function writeCache(url, data, cacheDir) {
|
|
115
|
+
if (isNode) await writeNodeCache(url, data, cacheDir);
|
|
116
|
+
else await writeBrowserCache(url, data);
|
|
117
|
+
}
|
|
118
|
+
async function deleteCache(url, cacheDir) {
|
|
119
|
+
if (isNode) {
|
|
120
|
+
const { fs, file } = await nodeCache(url, cacheDir);
|
|
121
|
+
fs.rmSync(file, { force: true });
|
|
122
|
+
} else if (typeof caches !== "undefined") {
|
|
123
|
+
const cache = await caches.open("structured-public-domains");
|
|
124
|
+
await cache.delete(url);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
var nodeRequire = (m) => import(
|
|
128
|
+
/* @vite-ignore */
|
|
129
|
+
m
|
|
130
|
+
);
|
|
131
|
+
async function nodeCache(url, cacheDir) {
|
|
132
|
+
const fs = await nodeRequire("node:fs");
|
|
133
|
+
const os = await nodeRequire("node:os");
|
|
134
|
+
const path = await nodeRequire("node:path");
|
|
135
|
+
const dir = cacheDir ?? path.join(os.tmpdir(), "structured-public-domains-cache");
|
|
136
|
+
return { fs, dir, file: path.join(dir, `psl-${cacheKey(url)}.bin`) };
|
|
137
|
+
}
|
|
138
|
+
async function readNodeCache(url, ttlMs, cacheDir) {
|
|
139
|
+
const { fs, file } = await nodeCache(url, cacheDir);
|
|
140
|
+
const stat = fs.statSync(file);
|
|
141
|
+
if (Date.now() - stat.mtimeMs >= ttlMs) return void 0;
|
|
142
|
+
return new Uint8Array(fs.readFileSync(file));
|
|
143
|
+
}
|
|
144
|
+
async function writeNodeCache(url, data, cacheDir) {
|
|
145
|
+
const { fs, dir, file } = await nodeCache(url, cacheDir);
|
|
146
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
147
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
148
|
+
fs.writeFileSync(tmp, data);
|
|
149
|
+
fs.renameSync(tmp, file);
|
|
150
|
+
}
|
|
151
|
+
async function readBrowserCache(url, ttlMs) {
|
|
152
|
+
if (typeof caches === "undefined") return void 0;
|
|
153
|
+
const cache = await caches.open("structured-public-domains");
|
|
154
|
+
const res = await cache.match(url);
|
|
155
|
+
if (res == null) return void 0;
|
|
156
|
+
const cachedAt = Number(res.headers.get("x-cached-at") ?? "0");
|
|
157
|
+
if (!Number.isFinite(cachedAt) || Date.now() - cachedAt >= ttlMs) return void 0;
|
|
158
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
159
|
+
}
|
|
160
|
+
async function writeBrowserCache(url, data) {
|
|
161
|
+
if (typeof caches === "undefined") return;
|
|
162
|
+
const cache = await caches.open("structured-public-domains");
|
|
163
|
+
const body = data.slice();
|
|
164
|
+
await cache.put(url, new Response(body, { headers: { "x-cached-at": String(Date.now()) } }));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { DEFAULT_PSL_URL, isKnownSuffix, isLoaded, load, lookup, pslData, registrableDomain };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Result of a PSL lookup. */
|
|
2
|
+
interface DomainInfo {
|
|
3
|
+
/** The public suffix (e.g. `"co.uk"`, `"com"`, `"github.io"`). */
|
|
4
|
+
readonly suffix: string;
|
|
5
|
+
/** The registrable domain (eTLD+1), or `undefined` if the input is itself a suffix. */
|
|
6
|
+
readonly registrableDomain: string | undefined;
|
|
7
|
+
/** Whether the suffix matched an explicit PSL rule (vs the `*` fallback). */
|
|
8
|
+
readonly known: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type { DomainInfo as D };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Result of a PSL lookup. */
|
|
2
|
+
interface DomainInfo {
|
|
3
|
+
/** The public suffix (e.g. `"co.uk"`, `"com"`, `"github.io"`). */
|
|
4
|
+
readonly suffix: string;
|
|
5
|
+
/** The registrable domain (eTLD+1), or `undefined` if the input is itself a suffix. */
|
|
6
|
+
readonly registrableDomain: string | undefined;
|
|
7
|
+
/** Whether the suffix matched an explicit PSL rule (vs the `*` fallback). */
|
|
8
|
+
readonly known: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type { DomainInfo as D };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@structured-world/structured-public-domains",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Compact Public Suffix List (PSL) as a native TypeScript package: embedded ~108KB binary trie, synchronous zero-dependency lookups, ESM + CommonJS. Checked daily against publicsuffix.org.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,18 @@
|
|
|
31
31
|
"types": "./dist/index.d.cts",
|
|
32
32
|
"default": "./dist/index.cjs"
|
|
33
33
|
}
|
|
34
|
-
}
|
|
34
|
+
},
|
|
35
|
+
"./tiny": {
|
|
36
|
+
"import": {
|
|
37
|
+
"types": "./dist/tiny.d.ts",
|
|
38
|
+
"default": "./dist/tiny.js"
|
|
39
|
+
},
|
|
40
|
+
"require": {
|
|
41
|
+
"types": "./dist/tiny.d.cts",
|
|
42
|
+
"default": "./dist/tiny.cjs"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"./psl.bin": "./dist/psl.bin"
|
|
35
46
|
},
|
|
36
47
|
"sideEffects": false,
|
|
37
48
|
"files": [
|
|
@@ -46,7 +57,7 @@
|
|
|
46
57
|
},
|
|
47
58
|
"scripts": {
|
|
48
59
|
"gen:data": "node scripts/gen-data.mjs",
|
|
49
|
-
"build": "npm run gen:data && tsup && node -e \"const fs=require('fs');fs.copyFileSync('src/psl-data.cjs','dist/psl-data.cjs');fs.copyFileSync('../README.md','README.md')\"",
|
|
60
|
+
"build": "npm run gen:data && tsup && node -e \"const fs=require('fs');fs.copyFileSync('src/psl-data.cjs','dist/psl-data.cjs');fs.copyFileSync('../src/psl.bin','dist/psl.bin');fs.copyFileSync('../README.md','README.md')\"",
|
|
50
61
|
"test": "npm run gen:data && vitest run",
|
|
51
62
|
"typecheck": "tsc --noEmit",
|
|
52
63
|
"prepublishOnly": "npm run build"
|