plevinjs 0.1.11 β 0.2.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 +148 -292
- package/dist/address.d.ts.map +1 -1
- package/dist/address.js +5 -15
- package/dist/address.js.map +1 -1
- package/dist/derive.d.ts.map +1 -1
- package/dist/derive.js +0 -3
- package/dist/derive.js.map +1 -1
- package/dist/extra.d.ts.map +1 -1
- package/dist/extra.js +3 -9
- package/dist/extra.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -20
- package/dist/index.js.map +1 -1
- package/dist/lzma.d.ts +43 -0
- package/dist/lzma.d.ts.map +1 -0
- package/dist/lzma.js +235 -0
- package/dist/lzma.js.map +1 -0
- package/dist/naming.d.ts +9 -1
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +132 -10
- package/dist/naming.js.map +1 -1
- package/dist/plevin.min.js +8 -8
- package/dist/reader.d.ts +9 -24
- package/dist/reader.d.ts.map +1 -1
- package/dist/reader.js +76 -106
- package/dist/reader.js.map +1 -1
- package/package.json +3 -3
- package/dist/zstd.d.ts +0 -31
- package/dist/zstd.d.ts.map +0 -1
- package/dist/zstd.js +0 -574
- package/dist/zstd.js.map +0 -1
package/README.md
CHANGED
|
@@ -7,124 +7,113 @@
|
|
|
7
7
|
</a>
|
|
8
8
|
|
|
9
9
|
**Location, network and abuse information for any IP address in one offline file.**<br>
|
|
10
|
-
No API, no rate limit, no lookup leaving the machine
|
|
10
|
+
No API, no rate limit, no lookup leaving the machine or the browser tab.
|
|
11
11
|
|
|
12
12
|
[](https://www.npmjs.com/package/plevinjs)
|
|
13
13
|
[](https://www.npmjs.com/package/plevinjs?activeTab=code)
|
|
14
14
|
[](https://github.com/tn3w/plevin/blob/master/LICENSE)
|
|
15
|
-
[](#
|
|
16
|
-
[](#speed)
|
|
15
|
+
[](#fields)
|
|
16
|
+
[](#speed)
|
|
18
17
|
|
|
19
18
|
</div>
|
|
20
19
|
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
21
22
|
```bash
|
|
22
23
|
npm install plevinjs
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
```js
|
|
26
|
-
import {
|
|
27
|
+
import { open } from "plevinjs";
|
|
27
28
|
|
|
28
29
|
const db = await open("https://plevin.tn3w.dev/db/plevin.plv");
|
|
29
|
-
const found = db.lookup("1.1.1.1");
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Always a `Result`, never `undefined`; it throws for anything that is not an address, and
|
|
33
|
-
a number reads as v6 only above `0xffffffff`, so `lookup(1)` is `0.0.0.1`.
|
|
30
|
+
const found = db.lookup("1.1.1.1"); // string, number, bigint or bytes
|
|
34
31
|
|
|
35
|
-
```js
|
|
36
32
|
found.place.city.name; // 'Brisbane'
|
|
37
|
-
found.place.city.region.name; // 'Queensland'
|
|
38
|
-
found.place.country.name; // 'Australia'
|
|
39
33
|
found.place.time.local; // '2026-08-13T23:27:58+10:00'
|
|
40
|
-
|
|
41
|
-
found.network.asn; // 13335
|
|
42
34
|
found.network.operator.brand; // 'Cloudflare'
|
|
43
35
|
found.network.cidr; // '1.1.1.0/24'
|
|
44
|
-
|
|
45
|
-
const exit = db.lookup("185.220.101.1");
|
|
46
|
-
exit.abuse.service; // 'tor_exit_node'
|
|
47
|
-
exit.abuse.risk; // 0.98
|
|
48
|
-
exit.abuse.is_tor_exit_node; // true
|
|
36
|
+
db.lookup("185.220.101.1").abuse.service; // 'tor_exit_node'
|
|
49
37
|
```
|
|
50
38
|
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
- `lookup` always answers a `Result` and throws for non-addresses.
|
|
40
|
+
- Numbers up to `0xffffffff` are v4, so `lookup(1)` is `0.0.0.1`.
|
|
41
|
+
- Pure ESM, zero dependencies: Node, Deno, Bun, Cloudflare Workers, browsers.
|
|
53
42
|
|
|
54
|
-
|
|
55
|
-
<script type="module">
|
|
56
|
-
import { open } from "https://cdn.jsdelivr.net/npm/plevinjs";
|
|
57
|
-
|
|
58
|
-
const db = await open("https://plevin.tn3w.dev/db/plevin.place-country-code.plv");
|
|
59
|
-
const { flag, name } = db.lookup("8.8.8.8").place.country;
|
|
60
|
-
document.body.textContent = `${flag} ${name}`; // 'πΊπΈ United States'
|
|
61
|
-
</script>
|
|
62
|
-
```
|
|
43
|
+
## Opening a database
|
|
63
44
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
45
|
+
| where | how |
|
|
46
|
+
| --------------- | ---------------------------------------------------------- |
|
|
47
|
+
| browser, worker | `await open(url)` or `await open(response)` |
|
|
48
|
+
| Node, Deno, Bun | `openFile(path)` from `plevinjs/node`; `PLEVIN_DB` if no path |
|
|
49
|
+
| bytes in hand | `new Plevin(uint8Array)` |
|
|
68
50
|
|
|
69
|
-
|
|
51
|
+
Open once, reuse for every lookup. Nothing is downloaded or cached for you.
|
|
70
52
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
53
|
+
| file | size | carries |
|
|
54
|
+
| ----------------------------------------------------- | ------- | ---------------------------------------- |
|
|
55
|
+
| `plevin.plv` | 16.4 MB | every field |
|
|
56
|
+
| `plevin.metro-place.plv` | 5.1 MB | city, region, postal, coordinates, metro |
|
|
57
|
+
| `plevin.network.plv` | 7.0 MB | ASN, operator, routing |
|
|
58
|
+
| `plevin.abuse-level-abuse-provider-abuse-service.plv` | 3.8 MB | abuse level, service and provider |
|
|
59
|
+
| `plevin.place-country-code.plv` | 376 KB | country code |
|
|
76
60
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
read once.
|
|
61
|
+
All are served with open CORS from [plevin.tn3w.dev/db](https://plevin.tn3w.dev/db/);
|
|
62
|
+
GitHub release downloads send no CORS header.
|
|
80
63
|
|
|
81
|
-
|
|
82
|
-
| --- | --- | --- |
|
|
83
|
-
| `plevin.plv` | 17.3 MB | every field |
|
|
84
|
-
| `plevin.metro-place.plv` | 5.7 MB | city, region, postal, coordinates, metro |
|
|
85
|
-
| `plevin.network.plv` | 7.3 MB | ASN, operator, routing |
|
|
86
|
-
| `plevin.abuse-level-abuse-provider-abuse-service.plv` | 3.5 MB | abuse level, service and provider |
|
|
87
|
-
| `plevin.place-country-code.plv` | 390 KB | the country code |
|
|
64
|
+
## In a browser
|
|
88
65
|
|
|
89
|
-
|
|
66
|
+
No build step:
|
|
90
67
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
68
|
+
```html
|
|
69
|
+
<script type="module">
|
|
70
|
+
import { open } from "https://cdn.jsdelivr.net/npm/plevinjs";
|
|
94
71
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
72
|
+
const db = await open("https://plevin.tn3w.dev/db/plevin.place-country-code.plv");
|
|
73
|
+
const { flag, name } = db.lookup("8.8.8.8").place.country;
|
|
74
|
+
document.body.textContent = `${flag} ${name}`; // 'πΊπΈ United States'
|
|
75
|
+
</script>
|
|
99
76
|
```
|
|
100
77
|
|
|
101
|
-
|
|
102
|
-
|
|
78
|
+
| URL | serves |
|
|
79
|
+
| ---------------------------------------------- | --------------------------------- |
|
|
80
|
+
| `https://cdn.jsdelivr.net/npm/plevinjs` | `plevin.min.js`, one 52 kB file |
|
|
81
|
+
| `https://unpkg.com/plevinjs` | the same |
|
|
82
|
+
| `https://esm.sh/plevinjs` | the modules, imports rewritten |
|
|
83
|
+
| `https://plevin.tn3w.dev/plevin/plevin.min.js` | the bundle beside the databases |
|
|
84
|
+
|
|
85
|
+
- **Pin a version for production:** `cdn.jsdelivr.net/npm/plevinjs@0.2.0`.
|
|
86
|
+
- **Pick the smallest build:** the country build is 376 KB against 16.4 MB.
|
|
87
|
+
- **Cache the file** so it downloads once per visitor:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
const store = await caches.open("plevin");
|
|
91
|
+
const url = "https://plevin.tn3w.dev/db/plevin.plv";
|
|
92
|
+
if (!(await store.match(url))) await store.add(url);
|
|
93
|
+
const db = new Plevin(new Uint8Array(await (await store.match(url)).arrayBuffer()));
|
|
94
|
+
```
|
|
103
95
|
|
|
104
|
-
##
|
|
96
|
+
## Fields
|
|
105
97
|
|
|
106
98
|
<picture>
|
|
107
99
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tn3w/plevin/master/.github/fields-dark.png">
|
|
108
100
|
<img src="https://raw.githubusercontent.com/tn3w/plevin/master/.github/fields-light.png" width="840" alt="address to place, network and abuse">
|
|
109
101
|
</picture>
|
|
110
102
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
[Python package](https://github.com/tn3w/plevin/blob/master/python/README.md#every-field)
|
|
116
|
-
answers with, to the letter.
|
|
103
|
+
Identical to the
|
|
104
|
+
[Python package](https://github.com/tn3w/plevin/blob/master/python/README.md#fields),
|
|
105
|
+
field for field; see there for what each one means. A missing value is `null`, never
|
|
106
|
+
`""` or `0`.
|
|
117
107
|
|
|
118
108
|
```js
|
|
109
|
+
found.place;
|
|
119
110
|
{
|
|
120
|
-
lat: -27.4675, lon: 153.0281, accuracy: 200, confidence: 36,
|
|
121
|
-
granularity: 'city',
|
|
111
|
+
lat: -27.4675, lon: 153.0281, accuracy: 200, confidence: 36, granularity: 'city',
|
|
122
112
|
city: {
|
|
123
113
|
id: 2174003, name: 'Brisbane', ascii: 'Brisbane', country: 'AU',
|
|
124
114
|
population: 2780063, elevation: 27, postal: '4000', postal_partial: null,
|
|
125
115
|
timezone: 'Australia/Brisbane', type: 'regional capital', capital: 'region',
|
|
126
|
-
region: { id: 2152274, code: '04', iso: 'AU-QLD', name: 'Queensland',
|
|
127
|
-
type: 'State' },
|
|
116
|
+
region: { id: 2152274, code: '04', iso: 'AU-QLD', name: 'Queensland', type: 'State' },
|
|
128
117
|
district: { id: 7839562, code: '31000', name: 'Brisbane' },
|
|
129
118
|
metro: null,
|
|
130
119
|
},
|
|
@@ -138,14 +127,8 @@ answers with, to the letter.
|
|
|
138
127
|
dst_start: null, dst_end: null,
|
|
139
128
|
},
|
|
140
129
|
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
`country` and `time` are derived, not stored: `country` out of an ISO 3166 table in the
|
|
144
|
-
package, `time` out of the runtime's own `Intl` zone data, so neither needs an install
|
|
145
|
-
and neither is a network call. Where the host's zone data is older or newer than the
|
|
146
|
-
one the Python package reads, a daylight boundary may move by a transition.
|
|
147
130
|
|
|
148
|
-
|
|
131
|
+
found.network;
|
|
149
132
|
{
|
|
150
133
|
asn: 13335, handle: 'CLOUDFLARENET', prefix: 24, cidr: '1.1.1.0/24',
|
|
151
134
|
start: '1.1.1.0', end: '1.1.1.255', rir: 'apnic', rpki: 'valid', roas: 1,
|
|
@@ -159,278 +142,151 @@ one the Python package reads, a daylight boundary may move by a transition.
|
|
|
159
142
|
carrier: { user_type: 'hosting', user_count: 19, mcc: null, mnc: null,
|
|
160
143
|
is_mobile: false },
|
|
161
144
|
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
`cidr` is the announcement the address falls in, masked out of the address itself.
|
|
165
|
-
`rir` is the registry that holds the address rather than the one that registered the
|
|
166
|
-
ASN. `rpki` is `valid`, `invalid` or `unknown` and `roas` how many ROAs agree. `brand`
|
|
167
|
-
drops the legal form and the words every network carries, so `GOOGLE` and `Google LLC`
|
|
168
|
-
both read `Google`.
|
|
169
145
|
|
|
170
|
-
Where nothing is announced, the registries still answer: `asn`, `rpki` and `roas` fall
|
|
171
|
-
silent, `cidr` becomes the block a registry gave out, and `handle` and `operator` name
|
|
172
|
-
whoever holds it.
|
|
173
|
-
|
|
174
|
-
```js
|
|
175
|
-
db.lookup("36.50.238.1").network;
|
|
176
|
-
{ asn: null, handle: 'GMTECH-BD', cidr: '36.50.238.0/23', rir: 'apnic',
|
|
177
|
-
operator: { company: 'GM Tech', ... }, ... }
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
```js
|
|
181
146
|
db.lookup("185.220.101.1").abuse;
|
|
182
147
|
{
|
|
183
148
|
name: 'Tor', provider: 'Tor', service: 'tor_exit_node', evidence: 'measured',
|
|
184
|
-
level: 'high', risk: 0.
|
|
185
|
-
is_malicious: true, is_anycast: false,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
149
|
+
level: 'high', risk: 0.99, network_risk: 0.86, last_seen_days: 1,
|
|
150
|
+
is_malicious: true, is_anycast: false, is_satellite: false,
|
|
151
|
+
is_hosting_provider: true, is_proxy: false, is_public_proxy: false,
|
|
152
|
+
is_residential_proxy: false, is_anonymous_vpn: false, is_tor_exit_node: true,
|
|
153
|
+
is_private_relay: false, is_anonymous: true,
|
|
189
154
|
}
|
|
190
155
|
```
|
|
191
156
|
|
|
192
|
-
|
|
193
|
-
`medium` from 0.60, `high` from 0.80, and `null` below that, where the reports are too
|
|
194
|
-
thin to call. `is_malicious` is true wherever a level stands.
|
|
157
|
+
JavaScript specifics:
|
|
195
158
|
|
|
196
|
-
`
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
the rest, capped at 0.99. `provider` is who runs the service: the feed's `name` where
|
|
201
|
-
one names it, else the brand of the network the address sits in, and `null` where the
|
|
202
|
-
address runs no service at all.
|
|
159
|
+
- `country` comes from an ISO 3166 table in the package and `time` from the runtime's
|
|
160
|
+
`Intl` data, so neither needs an install or a network call. A different zone
|
|
161
|
+
database can move a daylight-saving boundary.
|
|
162
|
+
- `number` is a `bigint` for v6, so `JSON.stringify` needs a replacer.
|
|
203
163
|
|
|
204
|
-
|
|
164
|
+
Address fields (`compressed`, `expanded`, `arpa`, `is_*`, `tunnel`, `embedded_ipv4`,
|
|
165
|
+
`decimal_ipv4`, `as_*`) work without any database.
|
|
205
166
|
|
|
206
|
-
|
|
167
|
+
## DNS
|
|
207
168
|
|
|
208
|
-
|
|
209
|
-
const found = db.lookup("2606:4700::1111");
|
|
210
|
-
found.number; // 50543257672059871404715951523469725969n
|
|
211
|
-
found.compressed // '2606:4700::1111'
|
|
212
|
-
found.expanded; // '2606:4700:0000:0000:0000:0000:0000:1111'
|
|
213
|
-
found.arpa; // '1.1.1.1.0.0.β¦.0.7.4.6.0.6.2.ip6.arpa'
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
`number` is a `bigint` for v6 and a `number` for v4, so `JSON.stringify` needs a
|
|
217
|
-
replacer where you hand a v6 result on. Then `is_global` and `is_bogon`, `is_private`,
|
|
218
|
-
`is_loopback`, `is_multicast`, `is_reserved`, `is_link_local`, `is_unique_local`,
|
|
219
|
-
`is_documentation`, `is_shared` (100.64/10) and `is_benchmark` (198.18/15), bisected
|
|
220
|
-
out of the IANA special-purpose registries.
|
|
169
|
+
`lookup` stays synchronous and offline. `resolve` adds DNS only when asked:
|
|
221
170
|
|
|
222
171
|
```js
|
|
223
|
-
db.
|
|
224
|
-
|
|
225
|
-
|
|
172
|
+
(await db.resolve("8.8.8.8", { dns: true })).dns;
|
|
173
|
+
{
|
|
174
|
+
asked: '8.8.8.8', hostname: 'dns.google', hostnames: ['dns.google'],
|
|
175
|
+
ipv4: '8.8.4.4', ipv6: '2001:4860:4860::8888',
|
|
176
|
+
ipv4_addresses: ['8.8.4.4', '8.8.8.8'],
|
|
177
|
+
ipv6_addresses: ['2001:4860:4860::8888', '2001:4860:4860::8844'],
|
|
178
|
+
alias: null, zone: '8.8.8.in-addr.arpa', zone_primary: 'ns1.google.com',
|
|
179
|
+
zone_contact: 'dns-admin@google.com', is_confirmed: true, is_signed: true,
|
|
180
|
+
}
|
|
226
181
|
```
|
|
227
182
|
|
|
228
|
-
|
|
183
|
+
- **Node, Deno, Bun:** raw DNS over UDP, raced across the system resolver plus
|
|
184
|
+
1.1.1.1, 8.8.8.8 and 9.9.9.9, with TCP on truncation.
|
|
185
|
+
- **Browsers, workers:** DNS-over-HTTPS to Cloudflare and Google.
|
|
186
|
+
- **Cache:** answers are kept for one hour.
|
|
229
187
|
|
|
230
|
-
|
|
231
|
-
db.lookup("2001:67c:e60:c0c:192:42:116:55").decimal_ipv4; // '192.42.116.55'
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
`decimal_ipv4` is a guess and never a tunnel: the last four hextets where an operator
|
|
235
|
-
wrote a v4 address into them as decimal. It is `null` wherever a real tunnel answers.
|
|
188
|
+
## Own address
|
|
236
189
|
|
|
237
190
|
```js
|
|
238
|
-
|
|
239
|
-
db.lookup("8.8.8.8").as_6to4; // '2002:808:808::'
|
|
240
|
-
db.lookup("8.8.8.8").as_nat64; // '64:ff9b::808:808'
|
|
241
|
-
```
|
|
191
|
+
import { publicAddress } from "plevinjs";
|
|
242
192
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
`embedded_ipv4` already says what it carries.
|
|
246
|
-
|
|
247
|
-
## What DNS says, where you ask for it
|
|
248
|
-
|
|
249
|
-
`lookup` never leaves the machine and stays synchronous. `resolve` is the same lookup
|
|
250
|
-
with DNS behind a flag, and does nothing more than `lookup` unless the flag is set:
|
|
251
|
-
|
|
252
|
-
```js
|
|
253
|
-
(await db.resolve("8.8.8.8", { dns: true })).dns;
|
|
254
|
-
{
|
|
255
|
-
asked: '8.8.8.8',
|
|
256
|
-
hostname: 'dns.google',
|
|
257
|
-
hostnames: [ 'dns.google' ],
|
|
258
|
-
ipv4: '8.8.4.4',
|
|
259
|
-
ipv6: '2001:4860:4860::8888',
|
|
260
|
-
ipv4_addresses: [ '8.8.4.4', '8.8.8.8' ],
|
|
261
|
-
ipv6_addresses: [ '2001:4860:4860::8888', '2001:4860:4860::8844' ],
|
|
262
|
-
alias: null,
|
|
263
|
-
zone: '8.8.8.in-addr.arpa',
|
|
264
|
-
zone_primary: 'ns1.google.com',
|
|
265
|
-
zone_contact: 'dns-admin@google.com',
|
|
266
|
-
is_confirmed: true,
|
|
267
|
-
is_signed: true,
|
|
268
|
-
}
|
|
193
|
+
const own = await publicAddress(); // '203.0.113.42', or null
|
|
194
|
+
own && db.lookup(own);
|
|
269
195
|
```
|
|
270
196
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
verdict and `alias` a CNAME in the way; `asked` is the address actually asked about,
|
|
277
|
-
which for a tunnel is the v4 it carries. Four questions go out in two rounds, PTR and
|
|
278
|
-
SOA on the reverse name together and then A and AAAA of the hostname: Node, Deno and
|
|
279
|
-
Bun write those onto the wire themselves and send them to every server at once, so the
|
|
280
|
-
machine's own from `node:dns` and 1.1.1.1, 8.8.8.8 and 9.9.9.9, so first real answer
|
|
281
|
-
winning, TCP where one comes back truncated, while a browser or a worker, having no
|
|
282
|
-
datagram, sends the same queries to Cloudflare and Google over DNS-over-HTTPS. Answers
|
|
283
|
-
are kept for an hour, and nothing is asked where the flag is off, which keeps a bundled
|
|
284
|
-
reader as offline as it was.
|
|
285
|
-
|
|
286
|
-
## One ASN, and the networks a name belongs to
|
|
197
|
+
- **How:** one STUN binding request, answered in tens of milliseconds.
|
|
198
|
+
- **Browsers:** go through `RTCPeerConnection` instead.
|
|
199
|
+
- **Fallback:** after 2 s, asks `api.ipify.org`, then `icanhazip.com`.
|
|
200
|
+
|
|
201
|
+
## ASNs
|
|
287
202
|
|
|
288
203
|
```js
|
|
289
204
|
const found = db.system("AS13335"); // 'AS13335', 'as13335' or 13335
|
|
290
|
-
found.handle; // 'CLOUDFLARENET'
|
|
291
205
|
found.network.operator.brand; // 'Cloudflare'
|
|
292
206
|
found.abuse.network_risk; // 0.14
|
|
293
207
|
|
|
294
208
|
db.search("hetzner").map((one) => one.asn);
|
|
295
209
|
// [24940, 212317, 213230, 215859]
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
`system()` answers a `System`, so the `asn`, the `handle`, the same `network` with its
|
|
299
|
-
operator and carrier, and the ASN's own `abuse` record, without anything only an address
|
|
300
|
-
fixes: no prefix, no CIDR, no RPKI, no place. `found` is `false` where the file carries
|
|
301
|
-
no such ASN. It bisects the network table, so no spine is read and nothing about the
|
|
302
|
-
address lookup changes.
|
|
303
210
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
one lowercase text built on the first search, 0.32 s, and kept from then on.
|
|
309
|
-
|
|
310
|
-
## What an ASN announces
|
|
311
|
-
|
|
312
|
-
```js
|
|
313
|
-
const routes = db.routes("AS13335"); // written however system() takes it
|
|
314
|
-
routes.ipv4.length; // 1506
|
|
315
|
-
routes.ipv4[0].cidr; // '152.114.0.0/17', the widest first
|
|
316
|
-
routes.ipv4[0].addresses; // 32768
|
|
317
|
-
routes.ipv4_addresses; // 616704, the space of all of them
|
|
318
|
-
routes.ipv6_addresses >> 64n; // 75037868032n, as /64 networks
|
|
211
|
+
const routes = db.routes("AS13335");
|
|
212
|
+
routes.ipv4.length; // 1411
|
|
213
|
+
routes.ipv4[0].cidr; // '152.114.0.0/17', widest first
|
|
214
|
+
routes.ipv6_addresses >> 64n; // space as /64 networks, a bigint
|
|
319
215
|
```
|
|
320
216
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
`
|
|
217
|
+
| call | answers |
|
|
218
|
+
| ------------------------- | -------------------------------------------------------- |
|
|
219
|
+
| `system(asn)` | `System`: handle, network, operator, carrier, ASN abuse; `found` false if unknown |
|
|
220
|
+
| `search(text, limit=20)` | `System`s whose handle or company matches, best first |
|
|
221
|
+
| `routes(asn)` | `Routes`: every announced prefix as a `Span`, widest first |
|
|
326
222
|
|
|
327
|
-
|
|
328
|
-
time: 159 ms for the first ASN asked about and 2 to 9 ms for every one after it, each
|
|
329
|
-
answer kept. Nothing an address lookup reads is touched.
|
|
330
|
-
|
|
331
|
-
## In a browser
|
|
223
|
+
## HTTP
|
|
332
224
|
|
|
333
|
-
|
|
334
|
-
CDN serves it as it is.
|
|
225
|
+
The same JSON without a file, CORS open, no key:
|
|
335
226
|
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const db = await open(
|
|
341
|
-
"https://plevin.tn3w.dev/db/plevin.place-country-code.plv"
|
|
342
|
-
);
|
|
343
|
-
const { flag, name } = db.lookup("1.1.1.1").place.country;
|
|
344
|
-
document.body.textContent = `${flag} ${name}`; // 'πΊπΈ United States'
|
|
345
|
-
</script>
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
Open the smallest build a page actually needs: `plevin.place-country-code.plv` is
|
|
349
|
-
390 KB against the 17.3 MB of `plevin.plv`, so the first lookup lands in a moment
|
|
350
|
-
rather than a download.
|
|
351
|
-
|
|
352
|
-
| | |
|
|
353
|
-
| --- | --- |
|
|
354
|
-
| `https://cdn.jsdelivr.net/npm/plevinjs` | `dist/plevin.min.js`, the whole reader in one file |
|
|
355
|
-
| `https://unpkg.com/plevinjs` | the same as jsDelivr |
|
|
356
|
-
| `https://esm.sh/plevinjs` | the modules as published, imports rewritten |
|
|
357
|
-
| `https://plevin.tn3w.dev/plevin/plevin.min.js` | the reader beside the databases |
|
|
358
|
-
|
|
359
|
-
jsDelivr and unpkg serve the bundle named by the `jsdelivr`/`unpkg` fields, 44 kB of
|
|
360
|
-
JavaScript with no further requests. The bare `dist/index.js` is not usable from those
|
|
361
|
-
URLs: it imports `./reader.js` and neighbours, which resolve against `/npm/` there and
|
|
362
|
-
404. Pin a version for anything that ships: `cdn.jsdelivr.net/npm/plevinjs@0.1.2`.
|
|
363
|
-
`plevinjs/node` is the only entry that touches Node, so a CDN import never reaches for
|
|
364
|
-
`node:fs`.
|
|
365
|
-
|
|
366
|
-
18 MB crosses the wire once, so keep it out of the critical path and out of the next
|
|
367
|
-
visit's way:
|
|
368
|
-
|
|
369
|
-
```js
|
|
370
|
-
const store = await caches.open("plevin");
|
|
371
|
-
const url = "https://plevin.tn3w.dev/db/plevin.plv";
|
|
372
|
-
if (!(await store.match(url))) await store.add(url);
|
|
373
|
-
|
|
374
|
-
const held = await store.match(url);
|
|
375
|
-
const db = new Plevin(new Uint8Array(await held.arrayBuffer()));
|
|
227
|
+
```bash
|
|
228
|
+
curl https://plevin.tn3w.dev/api/1.1.1.1 # any address
|
|
229
|
+
curl https://plevin.tn3w.dev/api/me # the caller
|
|
230
|
+
curl https://plevin.tn3w.dev/api/about # build and fields
|
|
376
231
|
```
|
|
377
232
|
|
|
378
|
-
|
|
379
|
-
[The lookup page](https://plevin.tn3w.dev/) is the whole idea in one file of
|
|
380
|
-
plain JavaScript: it reads the database in the tab and calls out only for the visitor's
|
|
381
|
-
own address and for hostnames.
|
|
233
|
+
Self-host with [`worker/`](https://github.com/tn3w/plevin/blob/master/worker).
|
|
382
234
|
|
|
383
235
|
## Speed
|
|
384
236
|
|
|
385
|
-
|
|
237
|
+
Full file, Node 26, one core:
|
|
386
238
|
|
|
387
|
-
| | |
|
|
388
|
-
|
|
|
389
|
-
| open
|
|
390
|
-
| first
|
|
391
|
-
| repeats
|
|
392
|
-
|
|
|
393
|
-
|
|
|
394
|
-
|
|
|
395
|
-
| search
|
|
239
|
+
| operation | speed |
|
|
240
|
+
| ------------ | -------------------------------------- |
|
|
241
|
+
| open | 9 ms, header only |
|
|
242
|
+
| first lookup | 70 ms |
|
|
243
|
+
| repeats | 5,000,000/s |
|
|
244
|
+
| random v4 | 8,000/s cold, 72,000/s warm |
|
|
245
|
+
| `system` | 1,000,000/s |
|
|
246
|
+
| `routes` | 100β160 ms, then cached |
|
|
247
|
+
| `search` | 1 ms, after 0.07 s building the index |
|
|
396
248
|
|
|
397
|
-
Blocks decode
|
|
398
|
-
|
|
249
|
+
Blocks decode lazily, only as far as a lookup reaches, and stay decoded: about 160 MB
|
|
250
|
+
of heap with the whole world touched.
|
|
399
251
|
|
|
400
|
-
##
|
|
252
|
+
## LZMA
|
|
401
253
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
package carries one, condensed from [fzstd](https://github.com/101arrowz/fzstd) (MIT)
|
|
405
|
-
with the dictionary support it leaves out, and verified block for block against
|
|
406
|
-
libzstd over the whole database.
|
|
254
|
+
Blocks are raw LZMA1, which neither `DecompressionStream` nor `zlib` reads, so the
|
|
255
|
+
package ships its own decoder, checked against liblzma on every block.
|
|
407
256
|
|
|
408
257
|
```js
|
|
409
|
-
import { decompress
|
|
258
|
+
import { decompress } from "plevinjs/lzma";
|
|
410
259
|
|
|
411
|
-
decompress(
|
|
412
|
-
decompress(frame, loadDictionary(trained)); // with a trained dictionary
|
|
260
|
+
decompress(block, [3, 0, 0]); // tuning: literal context, literal position, match position bits
|
|
413
261
|
```
|
|
414
262
|
|
|
415
263
|
## Development
|
|
416
264
|
|
|
265
|
+
| module | job |
|
|
266
|
+
| ------------ | --------------------------------------------------- |
|
|
267
|
+
| `index.ts` | `Plevin`, `open`, result shaping |
|
|
268
|
+
| `reader.ts` | file format: sections, blocks, groups, bisection |
|
|
269
|
+
| `lzma.ts` | resumable LZMA1 decoder |
|
|
270
|
+
| `address.ts` | parsing, spelling, special ranges |
|
|
271
|
+
| `naming.ts` | DNS and own address |
|
|
272
|
+
| `extra.ts` | country and clock, from `countries.ts`/`zones.ts` |
|
|
273
|
+
| `derive.ts` | brand, domain, capital |
|
|
274
|
+
| `models.ts` | published types |
|
|
275
|
+
| `node.ts` | `openFile`, the only `node:fs` import |
|
|
276
|
+
|
|
417
277
|
```bash
|
|
418
|
-
cd js
|
|
419
278
|
npm ci
|
|
420
|
-
npm test
|
|
421
|
-
npm run
|
|
422
|
-
npm run typecheck # tsc, strict
|
|
423
|
-
npm run build # dist/, ESM and .d.ts, plus the CDN bundle
|
|
424
|
-
npm run bundle # dist/plevin.min.js only (esbuild)
|
|
279
|
+
npm test && npm run lint && npm run typecheck
|
|
280
|
+
npm run build # dist/ and the CDN bundle
|
|
425
281
|
|
|
426
282
|
node test/compare.ts ../plevin.plv sample.json # field for field against Python
|
|
427
|
-
node test/blocks.ts ../plevin.plv 100000 # every block against
|
|
283
|
+
node test/blocks.ts ../plevin.plv 100000 # every block against liblzma
|
|
428
284
|
```
|
|
429
285
|
|
|
430
286
|
## License
|
|
431
287
|
|
|
432
288
|
Apache 2.0, see [LICENSE](https://github.com/tn3w/plevin/blob/master/LICENSE). The
|
|
433
|
-
database carries the licenses of
|
|
434
|
-
[
|
|
289
|
+
database carries the licenses of its
|
|
290
|
+
[sources](https://github.com/tn3w/plevin/blob/master/builder/README.md#sources).
|
|
435
291
|
|
|
436
292
|
<!-- brand: Noto Sans 800, wordmark bar #1868f2 place, #6f42c1 network, #2ea043 abuse; #7d8894 address, ink #0b1220 light, #f0f6fc dark -->
|
package/dist/address.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAAA,wDAAwD;AAExD,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAE1D,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,QAAQ,IAAI,CAAC;AAC1B,eAAO,MAAM,SAAS,IAAI,CAAC;AAC3B,eAAO,MAAM,QAAQ,IAAI,CAAC;AAC1B,eAAO,MAAM,UAAU,KAAK,CAAC;AAC7B,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,aAAa,KAAK,CAAC;AAChC,eAAO,MAAM,MAAM,MAAM,CAAC;AAC1B,eAAO,MAAM,SAAS,MAAM,CAAC;AAE7B,eAAO,MAAM,MAAM,gBAAgB,CAAC;AACpC,eAAO,MAAM,SAAS,SAAS,CAAC;AAChC,eAAO,MAAM,MAAM,WAAW,CAAC;AAC/B,eAAO,MAAM,KAAK,UAAU,CAAC;AAuF7B,sFAAsF;AACtF,eAAO,MAAM,KAAK,UAAW,KAAK,KAAG,CAAC,MAAM,GAAG,MAAM,EAAE,OAAO,CA6B7D,CAAC;AA2BF,gFAAgF;AAChF,eAAO,MAAM,OAAO,UACX,MAAM,GAAG,MAAM,QAChB,OAAO,KACZ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAAA,wDAAwD;AAExD,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAE1D,eAAO,MAAM,OAAO,IAAI,CAAC;AACzB,eAAO,MAAM,QAAQ,IAAI,CAAC;AAC1B,eAAO,MAAM,SAAS,IAAI,CAAC;AAC3B,eAAO,MAAM,QAAQ,IAAI,CAAC;AAC1B,eAAO,MAAM,UAAU,KAAK,CAAC;AAC7B,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,aAAa,KAAK,CAAC;AAChC,eAAO,MAAM,MAAM,MAAM,CAAC;AAC1B,eAAO,MAAM,SAAS,MAAM,CAAC;AAE7B,eAAO,MAAM,MAAM,gBAAgB,CAAC;AACpC,eAAO,MAAM,SAAS,SAAS,CAAC;AAChC,eAAO,MAAM,MAAM,WAAW,CAAC;AAC/B,eAAO,MAAM,KAAK,UAAU,CAAC;AAuF7B,sFAAsF;AACtF,eAAO,MAAM,KAAK,UAAW,KAAK,KAAG,CAAC,MAAM,GAAG,MAAM,EAAE,OAAO,CA6B7D,CAAC;AA2BF,gFAAgF;AAChF,eAAO,MAAM,OAAO,UACX,MAAM,GAAG,MAAM,QAChB,OAAO,KACZ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAezB,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,OAAO,UAAW,MAAM,GAAG,MAAM,QAAQ,OAAO,KAAG,MACvC,CAAC;AAc1B,kFAAkF;AAClF,eAAO,MAAM,OAAO,UAAW,MAAM,GAAG,MAAM,QAAQ,OAAO,KAAG,MAa/D,CAAC;AAEF,0EAA0E;AAC1E,eAAO,MAAM,MAAM,UACV,MAAM,GAAG,MAAM,QAChB,OAAO,KACZ,CAAC,MAAM,GAAG,IAAI,EAAE,MAAM,GAAG,IAAI,CAW/B,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,OAAO,UAAW,MAAM,GAAG,MAAM,QAAQ,OAAO,KAAG,MAAM,GAAG,IAUxE,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,OAAO,UACX,MAAM,GAAG,MAAM,QAChB,OAAO,KACZ,CAAC,MAAM,GAAG,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,GAAG,IAAI,CAQ9C,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,IAAI,UACR,MAAM,GAAG,MAAM,QAChB,OAAO,UACL,MAAM,KACb,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAOzB,CAAC"}
|
package/dist/address.js
CHANGED
|
@@ -168,21 +168,11 @@ export const spelled = (value, wide) => {
|
|
|
168
168
|
const nibbles = value.toString(16).padStart(32, "0");
|
|
169
169
|
const arpa = `${[...nibbles].reverse().join(".")}.ip6.arpa`;
|
|
170
170
|
const mapped = held.slice(0, 5).every((group) => group === 0) && held[5] === 0xffff;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
`${shortest(parts, held.slice(0, 6))}:${quad}`,
|
|
177
|
-
`${full.join(":")}:${quad}`,
|
|
178
|
-
arpa,
|
|
179
|
-
];
|
|
180
|
-
}
|
|
181
|
-
return [
|
|
182
|
-
shortest(held.map((group) => group.toString(16)), held),
|
|
183
|
-
held.map((group) => group.toString(16).padStart(4, "0")).join(":"),
|
|
184
|
-
arpa,
|
|
185
|
-
];
|
|
171
|
+
const groups = mapped ? held.slice(0, 6) : held;
|
|
172
|
+
const quad = mapped ? `:${dotted(Number(value & 0xffffffffn))}` : "";
|
|
173
|
+
const hex = groups.map((group) => group.toString(16));
|
|
174
|
+
const full = groups.map((group) => group.toString(16).padStart(4, "0"));
|
|
175
|
+
return [shortest(hex, groups) + quad, full.join(":") + quad, arpa];
|
|
186
176
|
};
|
|
187
177
|
/** An address as text: v4 from its octets, v6 through the shortening rules. */
|
|
188
178
|
export const written = (value, wide) => spelled(value, wide)[0];
|