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 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 for Claude Code, in this project's
85
- `skill/` folder, turns the JSON into a weekly memo.
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, more than 20,000 prefixes, or a prefix broader than
299
- /8 (IPv4) or /16 (IPv6) is rejected. A `/0` would verify every address.
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.1",
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
- // No vendor publishes anything near this broad. A /0 in a list would verify EVERY address,
36
- // so a prefix shorter than these is a broken list, not a big one.
37
- export const MIN_PREFIX = Object.freeze({ ipv4: 8, ipv6: 16 });
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 < MIN_PREFIX[type] || prefix > (type === 'ipv4' ? 32 : 128)) return null;
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
- /** The vendors' shared format: { prefixes: [{ ipv4Prefix } | { ipv6Prefix }] } → [cidr]. */
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
- const p = parseCidr(it?.ipv4Prefix ?? it?.ipv6Prefix);
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
- const prefixes = parsePrefixes(l?.prefixes);
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.1';
2
+ export const VERSION = '0.2.2';