siteread 0.2.1 → 0.2.2
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 +9 -4
- package/package.json +10 -1
- package/src/ip-ranges.js +30 -13
- package/src/verifier.js +2 -1
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -81,8 +81,9 @@ so `--host` keeps them and says so. For nginx JSON, the common field names are r
|
|
|
81
81
|
`http_user_agent` or `user_agent`, `http_referer` or `referer`, `host` or `http_host`), with
|
|
82
82
|
`escape=json` or nginx's default escaping.
|
|
83
83
|
|
|
84
|
-
`npx siteread --help` lists every option. The `seo-read` skill
|
|
85
|
-
`
|
|
84
|
+
`npx siteread --help` lists every option. The `seo-read` skill, in siteread's Claude Code plugin
|
|
85
|
+
(`/plugin install siteread@siteread` after `/plugin marketplace add synacl-iot/siteread`), turns the
|
|
86
|
+
JSON into a weekly memo.
|
|
86
87
|
|
|
87
88
|
## Share (opt-in)
|
|
88
89
|
|
|
@@ -295,8 +296,12 @@ All of them answer `{ "creationTime": …, "prefixes": [{ "ipv4Prefix": … } |
|
|
|
295
296
|
failure, not as an empty list.
|
|
296
297
|
- **Drop:** a copy nobody could refresh for 8 days is dropped, so a vendor's moved ranges cannot mark
|
|
297
298
|
its new IPs as spoofed forever. With no copy, the verdict is `unknown`, never `spoofed`.
|
|
298
|
-
- **Bounds:** a body over 2,000,000 characters
|
|
299
|
-
/
|
|
299
|
+
- **Bounds:** a body over 2,000,000 characters or more than 20,000 prefixes is rejected, and a list
|
|
300
|
+
holding a prefix broader than /16 (IPv4) or /32 (IPv6) is refused whole, keeping the previous
|
|
301
|
+
copy. The broadest real prefix in any list is a /16 (IPv4) and a /64 (IPv6); a `/0` would verify
|
|
302
|
+
every address, and since the CLI reads the hosted feed, the limit is what stops a tampered feed
|
|
303
|
+
from vouching for a whole provider's range. From the feed, a refused list is fetched from its
|
|
304
|
+
vendor instead.
|
|
300
305
|
|
|
301
306
|
## Privacy
|
|
302
307
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "siteread",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Is that crawler who it says it is? Checks an IP + User-Agent against the AI and search vendors' published IP lists. Zero dependencies.",
|
|
5
|
+
"homepage": "https://siteread.dev",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/synacl-iot/siteread.git",
|
|
9
|
+
"directory": "packages/verify"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/synacl-iot/siteread/issues"
|
|
13
|
+
},
|
|
5
14
|
"type": "module",
|
|
6
15
|
"engines": {
|
|
7
16
|
"node": ">=20"
|
package/src/ip-ranges.js
CHANGED
|
@@ -32,9 +32,15 @@ export const FETCH_TIMEOUT_MS = 8_000;
|
|
|
32
32
|
// A list is a few hundred prefixes; these bound what a broken (or hostile) response can cost.
|
|
33
33
|
export const MAX_BODY_CHARS = 2_000_000;
|
|
34
34
|
export const MAX_PREFIXES = 20_000;
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
35
|
+
// The broadest prefix a list may hold. A /0 would verify EVERY address, and since the CLI reads
|
|
36
|
+
// the hosted feed by default, the feed's server vouches for every client's "verified": if it were
|
|
37
|
+
// ever taken over, it could add a broad range to any agent and every spoofer in it would pass.
|
|
38
|
+
// Measured 2026-10-22, the broadest real prefix in any list is OpenAI's /16 (chatgpt-user) for
|
|
39
|
+
// IPv4 and Google's /64 for IPv6, so these limits cost nothing real and cut the worst case from
|
|
40
|
+
// a /8 to a /16. A list holding a broader prefix is refused whole (its previous copy kept, the
|
|
41
|
+
// error reported), never trimmed: trimming would silently turn a vendor's real new range into
|
|
42
|
+
// "spoofed", and a refusal says out loud that a limit or a list needs a look.
|
|
43
|
+
export const MIN_PREFIX = Object.freeze({ ipv4: 16, ipv6: 32 });
|
|
38
44
|
// Two processes sharing a store can disagree on the time by a little. A fetchedAt further in
|
|
39
45
|
// the future than this is not skew: it would keep a list "fresh" forever and defeat the drop.
|
|
40
46
|
const CLOCK_SKEW_MS = 5 * 60_000;
|
|
@@ -64,8 +70,14 @@ export const SOURCES = Object.freeze([
|
|
|
64
70
|
{ agent: 'bingbot', url: 'https://www.bing.com/toolbox/bingbot.json' },
|
|
65
71
|
].map(Object.freeze));
|
|
66
72
|
|
|
67
|
-
/** 'a.b.c.d/n' or 'x::/n' → { type, net, prefix, cidr } (normalised), or null. */
|
|
73
|
+
/** 'a.b.c.d/n' or 'x::/n' → { type, net, prefix, cidr } (normalised), or null; MIN_PREFIX applies. */
|
|
68
74
|
export function parseCidr(raw) {
|
|
75
|
+
const p = cidrOf(raw);
|
|
76
|
+
return p && p.prefix >= MIN_PREFIX[p.type] ? p : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** parseCidr without the breadth limit, so a list parser can tell "too broad" from "not a prefix". */
|
|
80
|
+
function cidrOf(raw) {
|
|
69
81
|
if (typeof raw !== 'string') return null;
|
|
70
82
|
const s = raw.trim();
|
|
71
83
|
const slash = s.indexOf('/');
|
|
@@ -76,30 +88,35 @@ export function parseCidr(raw) {
|
|
|
76
88
|
const prefix = Number(bits);
|
|
77
89
|
const type = net.isIPv4(addr) ? 'ipv4' : net.isIPv6(addr) ? 'ipv6' : null;
|
|
78
90
|
if (!type) return null;
|
|
79
|
-
if (prefix
|
|
91
|
+
if (prefix > (type === 'ipv4' ? 32 : 128)) return null;
|
|
80
92
|
return { type, net: addr.toLowerCase(), prefix, cidr: `${addr.toLowerCase()}/${prefix}` };
|
|
81
93
|
}
|
|
82
94
|
|
|
83
|
-
|
|
95
|
+
// One prefix of a list: skipped when it is not a prefix at all, the whole list refused when it
|
|
96
|
+
// is one broader than MIN_PREFIX (see there).
|
|
97
|
+
function addPrefix(out, raw) {
|
|
98
|
+
const p = cidrOf(raw);
|
|
99
|
+
if (!p) return;
|
|
100
|
+
if (p.prefix < MIN_PREFIX[p.type]) throw new Error(`${p.cidr} is broader than /${MIN_PREFIX[p.type]}; list refused`);
|
|
101
|
+
out.add(p.cidr);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The vendors' shared format: { prefixes: [{ ipv4Prefix } | { ipv6Prefix }] } → [cidr]. Throws on a too-broad prefix. */
|
|
84
105
|
export function parseList(body) {
|
|
85
106
|
const items = Array.isArray(body?.prefixes) ? body.prefixes : [];
|
|
86
107
|
const out = new Set();
|
|
87
108
|
for (const it of items) {
|
|
88
109
|
if (out.size >= MAX_PREFIXES) break;
|
|
89
|
-
|
|
90
|
-
if (p) out.add(p.cidr);
|
|
110
|
+
addPrefix(out, it?.ipv4Prefix ?? it?.ipv6Prefix);
|
|
91
111
|
}
|
|
92
112
|
return [...out];
|
|
93
113
|
}
|
|
94
114
|
|
|
95
|
-
/** A stored copy's prefixes ([cidr]) → [cidr], re-validated and bounded. */
|
|
115
|
+
/** A feed's or a stored copy's prefixes ([cidr]) → [cidr], re-validated and bounded. Throws on a too-broad prefix. */
|
|
96
116
|
export function parsePrefixes(list) {
|
|
97
117
|
if (!Array.isArray(list)) return [];
|
|
98
118
|
const out = new Set();
|
|
99
|
-
for (const c of list.slice(0, MAX_PREFIXES))
|
|
100
|
-
const p = parseCidr(c);
|
|
101
|
-
if (p) out.add(p.cidr);
|
|
102
|
-
}
|
|
119
|
+
for (const c of list.slice(0, MAX_PREFIXES)) addPrefix(out, c);
|
|
103
120
|
return [...out];
|
|
104
121
|
}
|
|
105
122
|
|
package/src/verifier.js
CHANGED
|
@@ -95,7 +95,8 @@ export function createVerifier({
|
|
|
95
95
|
const lists = body && typeof body.lists === 'object' && body.lists ? body.lists : {};
|
|
96
96
|
for (const { agent } of due) {
|
|
97
97
|
const l = Object.hasOwn(lists, agent) ? lists[agent] : null;
|
|
98
|
-
|
|
98
|
+
let prefixes;
|
|
99
|
+
try { prefixes = parsePrefixes(l?.prefixes); } catch (err) { fail(out, agent, err, 'feed'); continue; }
|
|
99
100
|
// A future fetchedAt is clamped to now: it would otherwise keep the copy fresh forever.
|
|
100
101
|
const at = Math.min(fetchedAtMs(l), t);
|
|
101
102
|
const stored = fetchedAtMs(doc.lists[agent]);
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Kept in step with package.json by test/package.test.js (no JSON import: Node 20 needs no flag for this).
|
|
2
|
-
export const VERSION = '0.2.
|
|
2
|
+
export const VERSION = '0.2.2';
|