@statlark/bots 0.1.0 → 0.1.1

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
@@ -3,8 +3,8 @@
3
3
  See which AI assistants, search engines and training crawlers read your site.
4
4
 
5
5
  Most crawlers request raw HTML and never run JavaScript, so a tracker script can't see them.
6
- This reports them from your server instead. It adds no delay to your pages: reports are sent
7
- in the background, time out after 2 seconds, and never throw or fail a request.
6
+ This reports them from your server instead. Reports go out in the background, so they don't
7
+ hold up your responses. They time out after 2 seconds and never throw or fail a request.
8
8
 
9
9
  Full guide: <https://statlark.com/docs/bot-traffic>
10
10
 
@@ -20,7 +20,8 @@ put it in browser code. With no key, the package warns once and does nothing.
20
20
 
21
21
  ## Next.js
22
22
 
23
- In `proxy.ts` (`middleware.ts` before Next 16), first, before any login check:
23
+ In `proxy.ts`, first, before any login check. Before Next 16 the file is `middleware.ts` and
24
+ the function is `middleware`:
24
25
 
25
26
  ```ts
26
27
  import { NextResponse, type NextFetchEvent, type NextRequest } from "next/server";
@@ -49,9 +50,23 @@ export default {
49
50
  };
50
51
  ```
51
52
 
52
- Static files never reach a Worker by default, so set `assets.run_worker_first` in your Wrangler
53
- config, or existing pages and `robots.txt` go uncounted. On Pages, `_routes.json` must not exclude
54
- them.
53
+ Static files never reach a Worker by default, so existing pages and `robots.txt` go uncounted
54
+ unless the Worker runs first. Set `run_worker_first` to patterns that leave your asset folders
55
+ out:
56
+
57
+ ```toml
58
+ [assets]
59
+ directory = "./public"
60
+ binding = "ASSETS"
61
+ run_worker_first = ["/*", "!/assets/*"] # use your own asset folders
62
+ ```
63
+
64
+ Every request that reaches the Worker counts towards Cloudflare's limits. On the Workers Free
65
+ plan that's 100,000 a day, and past it those URLs fail until midnight UTC, your homepage
66
+ included. Excluding your asset folders keeps those requests free. Exclude folders, not file
67
+ types: a path that matches an exclusion never reaches your Worker. On Pages, requests that
68
+ reach a Function count the same way, so keep asset folders out in `_routes.json`, but not your
69
+ pages or `robots.txt`.
55
70
 
56
71
  ## Express
57
72
 
@@ -60,12 +75,22 @@ import express from "express";
60
75
  import { statlarkBots } from "@statlark/bots/express";
61
76
 
62
77
  const app = express();
63
- app.set("trust proxy", 1); // your exact number of proxies
64
78
  app.use(statlarkBots());
65
79
  ```
66
80
 
67
- Behind a proxy, `req.ip` is only right if `trust proxy` is an **exact hop count**. Never set it to
68
- `true`: that makes `req.ip` the client-supplied leftmost entry.
81
+ The IP comes from `req.ip`, so Express's `trust proxy` setting decides it, for your whole app:
82
+
83
+ - Nothing in front of Node: don't set `trust proxy`. With any value, `req.ip` comes from the
84
+ `X-Forwarded-For` the client sent.
85
+ - Behind proxies you control that add `X-Forwarded-For`: set it to exactly that many hops, for
86
+ example `app.set("trust proxy", 1)` for one. Never `true`, and never more hops than there are.
87
+ - Already set in your app: leave it as it is.
88
+ - Not sure: leave it out. Real crawlers then read as fake, which is safe, and the guide's
89
+ troubleshooting section has the fix.
90
+
91
+ This package is ESM-only. `require()` works on Node ^20.19 or >=22.12; on older versions use
92
+ `import` or `await import()`. A CommonJS TypeScript project needs `"module": "nodenext"` on
93
+ TypeScript 5.8 or later.
69
94
 
70
95
  ## Any other server
71
96
 
@@ -90,9 +115,16 @@ Filter by user agent first, so you don't send every visitor to us. The guide has
90
115
 
91
116
  ## What's sent
92
117
 
93
- The page's origin and path (the query string and fragment are stripped first), the user agent,
94
- and, for crawlers with a published address list, the IP. The IP is used to check the crawler is
95
- genuine, then discarded. Statlark keeps the crawler's name, the path and a count.
118
+ The page's origin and path (the query string and fragment are stripped first) and the user
119
+ agent. When an IP is available, a crawler with a published address list also sends it, so
120
+ Statlark can check the crawler is genuine. Any other request that looks like a crawler sends
121
+ `source` instead: a SHA-256 hash of your bot key and the address, with an IPv6 address cut to
122
+ its /64 first, used only to rate-limit crawlers that can't be checked. Neither is written to
123
+ Statlark's database. The hash is a pseudonym, not anonymous: anyone holding your key could work
124
+ out an IPv4 address from it. Statlark keeps the crawler's name, the path and a count.
125
+
126
+ Framework paths such as `/_next/` and `/api/` aren't sent, and neither is a user agent the
127
+ server has just said it doesn't recognise, for an hour.
96
128
 
97
129
  ## License
98
130
 
package/dist/express.d.ts CHANGED
@@ -2,8 +2,9 @@ import { type TrackOptions } from "./index.js";
2
2
  /**
3
3
  * `app.use(statlarkBots())`. Fire and forget: the request carries on immediately.
4
4
  *
5
- * `req.ip` is only right behind a proxy if `trust proxy` is set to an exact hop count. Never
6
- * set it to `true`, which makes `req.ip` the client-supplied leftmost entry.
5
+ * The IP comes from `req.ip`, so `trust proxy` decides it. With nothing in front of Node, don't
6
+ * set it. Behind proxies you control, set the exact number of hops. Never set it to `true`, or
7
+ * to more hops than there are: `req.ip` then comes from a header the client can write.
7
8
  */
8
9
  export declare function statlarkBots(options?: TrackOptions): (req: any, res: any, next: () => void) => void;
9
10
  //# sourceMappingURL=express.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAE7D;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,OAAO,CAAC,EAAE,YAAY,GACrB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,IAAI,KAAK,IAAI,CAiBhD"}
1
+ {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAE7D;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,OAAO,CAAC,EAAE,YAAY,GACrB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,IAAI,KAAK,IAAI,CAiBhD"}
package/dist/express.js CHANGED
@@ -2,8 +2,9 @@ import { reportBotHit } from "./index.js";
2
2
  /**
3
3
  * `app.use(statlarkBots())`. Fire and forget: the request carries on immediately.
4
4
  *
5
- * `req.ip` is only right behind a proxy if `trust proxy` is set to an exact hop count. Never
6
- * set it to `true`, which makes `req.ip` the client-supplied leftmost entry.
5
+ * The IP comes from `req.ip`, so `trust proxy` decides it. With nothing in front of Node, don't
6
+ * set it. Behind proxies you control, set the exact number of hops. Never set it to `true`, or
7
+ * to more hops than there are: `req.ip` then comes from a header the client can write.
7
8
  */
8
9
  export function statlarkBots(options) {
9
10
  return (req, _res, next) => {
@@ -1 +1 @@
1
- {"version":3,"file":"express.js","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAqB,MAAM,YAAY,CAAC;AAE7D;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAC1B,OAAsB;IAEtB,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;QACzB,IAAI,CAAC;YACH,KAAK,YAAY,CACf;gBACE,GAAG,EAAE,GAAG,GAAG,CAAC,QAAQ,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,WAAW,EAAE;gBAC7D,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC;gBAChC,EAAE,EAAE,GAAG,CAAC,EAAE;aACX,EACD,OAAO,CACR,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,0BAA0B;QAC5B,CAAC;QACD,qFAAqF;QACrF,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAqB,MAAM,YAAY,CAAC;AAE7D;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAC1B,OAAsB;IAEtB,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;QACzB,IAAI,CAAC;YACH,KAAK,YAAY,CACf;gBACE,GAAG,EAAE,GAAG,GAAG,CAAC,QAAQ,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,WAAW,EAAE;gBAC7D,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC;gBAChC,EAAE,EAAE,GAAG,CAAC,EAAE;aACX,EACD,OAAO,CACR,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,0BAA0B;QAC5B,CAAC;QACD,qFAAqF;QACrF,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
package/dist/index.d.ts CHANGED
@@ -12,7 +12,11 @@ export type BotHit = {
12
12
  /** An absolute URL or a path. The query string and fragment are never sent. */
13
13
  url: string;
14
14
  userAgent: string | null | undefined;
15
- /** The crawler's address, if you have a reliable one. Used for the check, then discarded. */
15
+ /**
16
+ * The crawler's address, if you have a reliable one. Sent for crawlers with a published list,
17
+ * to check them. For other crawlers only a hash of it, keyed with your bot key, is sent, for a
18
+ * rate limit. Neither is written to Statlark's database.
19
+ */
16
20
  ip?: string | null;
17
21
  };
18
22
  export type TrackOptions = {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAgCH,MAAM,MAAM,MAAM,GAAG;IACnB,+EAA+E;IAC/E,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACrC,6FAA6F;IAC7F,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,6DAA6D;IAC7D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CAChC,CAAC;AAsCF;sDACsD;AACtD,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyEzF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA4CH,MAAM,MAAM,MAAM,GAAG;IACnB,+EAA+E;IAC/E,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACrC;;;;OAIG;IACH,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,6DAA6D;IAC7D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CAChC,CAAC;AAoEF;sDACsD;AACtD,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6FzF"}
package/dist/index.js CHANGED
@@ -15,17 +15,29 @@ const MAX_STOP_MS = 3600000;
15
15
  const FAILURE_BACKOFF_MS = 60000;
16
16
  const MAX_FAILURES = 3;
17
17
  const MAX_IN_FLIGHT = 20;
18
+ // The server refuses a longer user agent, so don't send one.
19
+ const MAX_USER_AGENT = 512;
20
+ // After the server says a user agent isn't a crawler it knows, skip it for an hour. The hour
21
+ // keeps a crawler newly added to the server's list from waiting on an upgrade (docs/21 D12).
22
+ const UNKNOWN_TTL_MS = 3600000;
23
+ const MAX_UNKNOWN = 500;
18
24
  // Cheap first filter. Statlark's server decides what's really a crawler (docs/21 D12), so
19
25
  // this only has to skip ordinary browsers without a network call. Keep it broad: a crawler
20
26
  // this misses never reaches the server, so adding it to the list wouldn't be enough.
21
27
  const CANDIDATE = /bot\b|bot\/|crawl|spider|slurp|-user\b|-index\b|-training\b|-agent\b|amzn-|google-|googleother|meta-|bytespider|cohere|facebookexternalhit/i;
22
28
  const SKIP = /\.(?:js|mjs|css|map|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|eot|mp4|webm|mp3|wasm)$/i;
29
+ // The server drops these paths, so don't spend a request on them.
30
+ const FRAMEWORK = /^\/(?:_next|_vercel|api|\.well-known)(?:\/|$)/;
31
+ // A key is printable ASCII. Anything else, such as a pasted line break, can't go in a header.
32
+ const BAD_KEY = /[^\x21-\x7e]/;
23
33
  // Crawlers with a published list: the only ones whose IP is worth sending (D19).
24
34
  const HAS_LIST = /chatgpt-user|oai-searchbot|oai-adsbot|gptbot|claude|perplexity|google-agent|google-cloudvertexbot|googleother|googlebot|applebot|duckduckbot|duckassistbot|mistralai-(?:user|index)|ccbot|ahrefsbot/i;
25
35
  // Stands in for the origin of a bare path, so `new URL` can parse one.
26
36
  const NO_ORIGIN = "https://statlark.invalid";
27
37
  // Per key, so one bad key on a multi-tenant host can't stop another tenant's.
28
38
  const state = new Map();
39
+ // User agents the server answered unknown_crawler for, with when to try again.
40
+ const unknown = new Map();
29
41
  let inFlight = 0;
30
42
  let warnedNoKey = false;
31
43
  function stateFor(key) {
@@ -48,6 +60,38 @@ async function sha256Hex(text) {
48
60
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
49
61
  return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
50
62
  }
63
+ /**
64
+ * What a rate limit should treat as one source: an IPv4 address, or an IPv6 /64, since one host
65
+ * can hold a whole /64. A mapped IPv4 address stays whole, so dual-stack servers don't put every
66
+ * IPv4 client in one bucket.
67
+ */
68
+ function sourceAddress(ip) {
69
+ const address = ip.split("%")[0].toLowerCase();
70
+ if (!address.includes(":") || address.includes("."))
71
+ return address;
72
+ const [head, tail] = address.split("::");
73
+ if (address.split("::").length > 2)
74
+ return address;
75
+ const front = head ? head.split(":") : [];
76
+ const back = tail === undefined ? [] : tail ? tail.split(":") : [];
77
+ const groups = tail === undefined ? front : [...front, ...Array(8 - front.length - back.length).fill("0"), ...back];
78
+ if (groups.length !== 8 || !groups.every((g) => /^[0-9a-f]{1,4}$/.test(g)))
79
+ return address;
80
+ return `${groups.slice(0, 4).map((g) => g.replace(/^0+(?=.)/, "")).join(":")}::/64`;
81
+ }
82
+ /** Statlark's own errors are JSON with a string `error`. A 401, 402 or 403 without that came
83
+ * from something on the way, so it says nothing about the key or the plan. */
84
+ async function isStatlarkError(res) {
85
+ if (!res.headers.get("content-type")?.includes("application/json"))
86
+ return false;
87
+ try {
88
+ const reply = (await res.json());
89
+ return typeof reply?.error === "string";
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
51
95
  function warnOnce(entry, message) {
52
96
  if (entry.warned)
53
97
  return;
@@ -70,12 +114,19 @@ export async function reportBotHit(hit, options = {}) {
70
114
  if (typeof window !== "undefined")
71
115
  return; // never run in a browser
72
116
  const userAgent = hit.userAgent;
73
- if (!userAgent || !CANDIDATE.test(userAgent))
117
+ if (!userAgent || userAgent.length > MAX_USER_AGENT || !CANDIDATE.test(userAgent))
74
118
  return;
119
+ const retryAt = unknown.get(userAgent);
120
+ if (retryAt !== undefined) {
121
+ if (Date.now() < retryAt)
122
+ return;
123
+ unknown.delete(userAgent);
124
+ }
75
125
  const page = pageUrl(hit.url);
76
- if (!page || SKIP.test(page.pathname))
126
+ if (!page || SKIP.test(page.pathname) || FRAMEWORK.test(page.pathname))
77
127
  return;
78
- const key = options.key ?? (typeof process !== "undefined" ? process.env.STATLARK_BOT_KEY : undefined);
128
+ // Trimmed, so a key pasted with a trailing line break still works.
129
+ const key = (options.key ?? (typeof process !== "undefined" ? process.env.STATLARK_BOT_KEY : undefined))?.trim();
79
130
  if (!key) {
80
131
  if (!warnedNoKey) {
81
132
  warnedNoKey = true;
@@ -84,6 +135,10 @@ export async function reportBotHit(hit, options = {}) {
84
135
  return;
85
136
  }
86
137
  entry = stateFor(key);
138
+ if (BAD_KEY.test(key)) {
139
+ warnOnce(entry, "[statlark] Bot tracking is off: the bot key contains a character a key can't have, such as a line break. Copy it again.");
140
+ return;
141
+ }
87
142
  if (Date.now() < entry.stoppedUntil || inFlight >= MAX_IN_FLIGHT)
88
143
  return;
89
144
  inFlight += 1;
@@ -93,7 +148,7 @@ export async function reportBotHit(hit, options = {}) {
93
148
  if (HAS_LIST.test(userAgent))
94
149
  body.ip = hit.ip;
95
150
  else
96
- body.source = await sha256Hex(`${key}:${hit.ip}`);
151
+ body.source = await sha256Hex(`${key}:${sourceAddress(hit.ip)}`);
97
152
  }
98
153
  const res = await fetch(options.endpoint ?? ENDPOINT, {
99
154
  method: "POST",
@@ -102,8 +157,14 @@ export async function reportBotHit(hit, options = {}) {
102
157
  signal: AbortSignal.timeout(TIMEOUT_MS),
103
158
  });
104
159
  if (res.status === 401 || res.status === 402 || res.status === 403) {
105
- entry.stoppedUntil = Date.now() + AUTH_BACKOFF_MS;
106
- warnOnce(entry, `[statlark] Bot tracking stopped: the server answered ${res.status}. Check your bot key and plan.`);
160
+ if (await isStatlarkError(res)) {
161
+ entry.stoppedUntil = Date.now() + AUTH_BACKOFF_MS;
162
+ warnOnce(entry, `[statlark] Bot tracking stopped: the server answered ${res.status}. Check your bot key and plan.`);
163
+ }
164
+ else {
165
+ // A layer on the way, such as a firewall, answered. Not a verdict on the key.
166
+ countFailure(entry);
167
+ }
107
168
  }
108
169
  else if (res.status === 429) {
109
170
  const seconds = Number(res.headers.get("retry-after"));
@@ -118,6 +179,11 @@ export async function reportBotHit(hit, options = {}) {
118
179
  }
119
180
  if (res.status === 200) {
120
181
  const reply = (await res.json());
182
+ if (reply.reason === "unknown_crawler") {
183
+ if (unknown.size >= MAX_UNKNOWN)
184
+ unknown.delete(unknown.keys().next().value);
185
+ unknown.set(userAgent, Date.now() + UNKNOWN_TTL_MS);
186
+ }
121
187
  if (reply.paused_until) {
122
188
  // Capped, so an upgrade takes effect within the hour.
123
189
  const until = Date.parse(reply.paused_until);
@@ -125,7 +191,7 @@ export async function reportBotHit(hit, options = {}) {
125
191
  entry.stoppedUntil = Number.isNaN(until) ? cap : Math.min(until, cap);
126
192
  }
127
193
  }
128
- else {
194
+ else if (!res.bodyUsed) {
129
195
  await res.body?.cancel();
130
196
  }
131
197
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/next.d.ts CHANGED
@@ -7,7 +7,8 @@ type Evt = {
7
7
  waitUntil(p: Promise<unknown>): void;
8
8
  };
9
9
  /**
10
- * Call this first in `proxy.ts` (`middleware.ts` before Next 16), before any login check.
10
+ * Call this first in `proxy.ts`, before any login check. Before Next 16 the file is
11
+ * `middleware.ts` and the function is `middleware`.
11
12
  *
12
13
  * Only `x-real-ip` on Vercel is trusted. `x-forwarded-for` is never used: outside Vercel its
13
14
  * leftmost entry is client-supplied, so a scanner could forge a verified request. Behind
@@ -1 +1 @@
1
- {"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAE7D,KAAK,GAAG,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAC7C,KAAK,GAAG,GAAG;IAAE,SAAS,CAAC,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;CAAE,CAAC;AAEpD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CACtB,OAAO,EAAE,GAAG,EACZ,KAAK,EAAE,GAAG,EACV,OAAO,CAAC,EAAE,YAAY,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC9C,IAAI,CAcN"}
1
+ {"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAE7D,KAAK,GAAG,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAC7C,KAAK,GAAG,GAAG;IAAE,SAAS,CAAC,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAA;CAAE,CAAC;AAEpD;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CACtB,OAAO,EAAE,GAAG,EACZ,KAAK,EAAE,GAAG,EACV,OAAO,CAAC,EAAE,YAAY,GAAG;IAAE,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC9C,IAAI,CAcN"}
package/dist/next.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { reportBotHit } from "./index.js";
2
2
  /**
3
- * Call this first in `proxy.ts` (`middleware.ts` before Next 16), before any login check.
3
+ * Call this first in `proxy.ts`, before any login check. Before Next 16 the file is
4
+ * `middleware.ts` and the function is `middleware`.
4
5
  *
5
6
  * Only `x-real-ip` on Vercel is trusted. `x-forwarded-for` is never used: outside Vercel its
6
7
  * leftmost entry is client-supplied, so a scanner could forge a verified request. Behind
package/dist/next.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"next.js","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAqB,MAAM,YAAY,CAAC;AAK7D;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CACtB,OAAY,EACZ,KAAU,EACV,OAA+C;IAE/C,6FAA6F;IAC7F,6DAA6D;IAC7D,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC/E,MAAM,EAAE,GAAG,OAAO,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC/E,MAAM,OAAO,GAAG,YAAY,CAC1B,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,EACtE,OAAO,CACR,CAAC;QACF,KAAK,EAAE,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,0BAA0B;IAC5B,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"next.js","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAqB,MAAM,YAAY,CAAC;AAK7D;;;;;;;GAOG;AACH,MAAM,UAAU,QAAQ,CACtB,OAAY,EACZ,KAAU,EACV,OAA+C;IAE/C,6FAA6F;IAC7F,6DAA6D;IAC7D,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC/E,MAAM,EAAE,GAAG,OAAO,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC/E,MAAM,OAAO,GAAG,YAAY,CAC1B,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,EACtE,OAAO,CACR,CAAC;QACF,KAAK,EAAE,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,0BAA0B;IAC5B,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,11 +1,18 @@
1
1
  {
2
2
  "name": "@statlark/bots",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Statlark bot tracking: reports which AI assistants, search engines and training crawlers request your pages, from your server. Adapters for Next.js, Cloudflare and Express. No runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
9
+ "typesVersions": {
10
+ "*": {
11
+ "next": ["dist/next.d.ts"],
12
+ "cloudflare": ["dist/cloudflare.d.ts"],
13
+ "express": ["dist/express.d.ts"]
14
+ }
15
+ },
9
16
  "exports": {
10
17
  ".": {
11
18
  "types": "./dist/index.d.ts",
@@ -31,7 +38,7 @@
31
38
  },
32
39
  "sideEffects": false,
33
40
  "engines": {
34
- "node": ">=20.19"
41
+ "node": "^20.19.0 || >=22.12.0"
35
42
  },
36
43
  "scripts": {
37
44
  "build": "tsc",
package/src/express.ts CHANGED
@@ -3,8 +3,9 @@ import { reportBotHit, type TrackOptions } from "./index.js";
3
3
  /**
4
4
  * `app.use(statlarkBots())`. Fire and forget: the request carries on immediately.
5
5
  *
6
- * `req.ip` is only right behind a proxy if `trust proxy` is set to an exact hop count. Never
7
- * set it to `true`, which makes `req.ip` the client-supplied leftmost entry.
6
+ * The IP comes from `req.ip`, so `trust proxy` decides it. With nothing in front of Node, don't
7
+ * set it. Behind proxies you control, set the exact number of hops. Never set it to `true`, or
8
+ * to more hops than there are: `req.ip` then comes from a header the client can write.
8
9
  */
9
10
  export function statlarkBots(
10
11
  options?: TrackOptions,
package/src/index.ts CHANGED
@@ -16,6 +16,12 @@ const MAX_STOP_MS = 3_600_000;
16
16
  const FAILURE_BACKOFF_MS = 60_000;
17
17
  const MAX_FAILURES = 3;
18
18
  const MAX_IN_FLIGHT = 20;
19
+ // The server refuses a longer user agent, so don't send one.
20
+ const MAX_USER_AGENT = 512;
21
+ // After the server says a user agent isn't a crawler it knows, skip it for an hour. The hour
22
+ // keeps a crawler newly added to the server's list from waiting on an upgrade (docs/21 D12).
23
+ const UNKNOWN_TTL_MS = 3_600_000;
24
+ const MAX_UNKNOWN = 500;
19
25
 
20
26
  // Cheap first filter. Statlark's server decides what's really a crawler (docs/21 D12), so
21
27
  // this only has to skip ordinary browsers without a network call. Keep it broad: a crawler
@@ -24,6 +30,10 @@ const CANDIDATE =
24
30
  /bot\b|bot\/|crawl|spider|slurp|-user\b|-index\b|-training\b|-agent\b|amzn-|google-|googleother|meta-|bytespider|cohere|facebookexternalhit/i;
25
31
  const SKIP =
26
32
  /\.(?:js|mjs|css|map|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|eot|mp4|webm|mp3|wasm)$/i;
33
+ // The server drops these paths, so don't spend a request on them.
34
+ const FRAMEWORK = /^\/(?:_next|_vercel|api|\.well-known)(?:\/|$)/;
35
+ // A key is printable ASCII. Anything else, such as a pasted line break, can't go in a header.
36
+ const BAD_KEY = /[^\x21-\x7e]/;
27
37
 
28
38
  // Crawlers with a published list: the only ones whose IP is worth sending (D19).
29
39
  const HAS_LIST =
@@ -36,6 +46,8 @@ type KeyState = { stoppedUntil: number; failures: number; warned: boolean };
36
46
 
37
47
  // Per key, so one bad key on a multi-tenant host can't stop another tenant's.
38
48
  const state = new Map<string, KeyState>();
49
+ // User agents the server answered unknown_crawler for, with when to try again.
50
+ const unknown = new Map<string, number>();
39
51
  let inFlight = 0;
40
52
  let warnedNoKey = false;
41
53
 
@@ -43,7 +55,11 @@ export type BotHit = {
43
55
  /** An absolute URL or a path. The query string and fragment are never sent. */
44
56
  url: string;
45
57
  userAgent: string | null | undefined;
46
- /** The crawler's address, if you have a reliable one. Used for the check, then discarded. */
58
+ /**
59
+ * The crawler's address, if you have a reliable one. Sent for crawlers with a published list,
60
+ * to check them. For other crawlers only a hash of it, keyed with your bot key, is sent, for a
61
+ * rate limit. Neither is written to Statlark's database.
62
+ */
47
63
  ip?: string | null;
48
64
  };
49
65
 
@@ -76,6 +92,36 @@ async function sha256Hex(text: string): Promise<string> {
76
92
  return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
77
93
  }
78
94
 
95
+ /**
96
+ * What a rate limit should treat as one source: an IPv4 address, or an IPv6 /64, since one host
97
+ * can hold a whole /64. A mapped IPv4 address stays whole, so dual-stack servers don't put every
98
+ * IPv4 client in one bucket.
99
+ */
100
+ function sourceAddress(ip: string): string {
101
+ const address = ip.split("%")[0].toLowerCase();
102
+ if (!address.includes(":") || address.includes(".")) return address;
103
+ const [head, tail] = address.split("::");
104
+ if (address.split("::").length > 2) return address;
105
+ const front = head ? head.split(":") : [];
106
+ const back = tail === undefined ? [] : tail ? tail.split(":") : [];
107
+ const groups =
108
+ tail === undefined ? front : [...front, ...Array(8 - front.length - back.length).fill("0"), ...back];
109
+ if (groups.length !== 8 || !groups.every((g) => /^[0-9a-f]{1,4}$/.test(g))) return address;
110
+ return `${groups.slice(0, 4).map((g) => g.replace(/^0+(?=.)/, "")).join(":")}::/64`;
111
+ }
112
+
113
+ /** Statlark's own errors are JSON with a string `error`. A 401, 402 or 403 without that came
114
+ * from something on the way, so it says nothing about the key or the plan. */
115
+ async function isStatlarkError(res: Response): Promise<boolean> {
116
+ if (!res.headers.get("content-type")?.includes("application/json")) return false;
117
+ try {
118
+ const reply = (await res.json()) as { error?: unknown } | null;
119
+ return typeof reply?.error === "string";
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
124
+
79
125
  function warnOnce(entry: KeyState, message: string): void {
80
126
  if (entry.warned) return;
81
127
  entry.warned = true;
@@ -98,12 +144,19 @@ export async function reportBotHit(hit: BotHit, options: TrackOptions = {}): Pro
98
144
  try {
99
145
  if (typeof window !== "undefined") return; // never run in a browser
100
146
  const userAgent = hit.userAgent;
101
- if (!userAgent || !CANDIDATE.test(userAgent)) return;
147
+ if (!userAgent || userAgent.length > MAX_USER_AGENT || !CANDIDATE.test(userAgent)) return;
148
+ const retryAt = unknown.get(userAgent);
149
+ if (retryAt !== undefined) {
150
+ if (Date.now() < retryAt) return;
151
+ unknown.delete(userAgent);
152
+ }
102
153
  const page = pageUrl(hit.url);
103
- if (!page || SKIP.test(page.pathname)) return;
154
+ if (!page || SKIP.test(page.pathname) || FRAMEWORK.test(page.pathname)) return;
104
155
 
105
- const key =
106
- options.key ?? (typeof process !== "undefined" ? process.env.STATLARK_BOT_KEY : undefined);
156
+ // Trimmed, so a key pasted with a trailing line break still works.
157
+ const key = (
158
+ options.key ?? (typeof process !== "undefined" ? process.env.STATLARK_BOT_KEY : undefined)
159
+ )?.trim();
107
160
  if (!key) {
108
161
  if (!warnedNoKey) {
109
162
  warnedNoKey = true;
@@ -113,6 +166,10 @@ export async function reportBotHit(hit: BotHit, options: TrackOptions = {}): Pro
113
166
  }
114
167
 
115
168
  entry = stateFor(key);
169
+ if (BAD_KEY.test(key)) {
170
+ warnOnce(entry, "[statlark] Bot tracking is off: the bot key contains a character a key can't have, such as a line break. Copy it again.");
171
+ return;
172
+ }
116
173
  if (Date.now() < entry.stoppedUntil || inFlight >= MAX_IN_FLIGHT) return;
117
174
 
118
175
  inFlight += 1;
@@ -121,7 +178,7 @@ export async function reportBotHit(hit: BotHit, options: TrackOptions = {}): Pro
121
178
  const body: Record<string, string> = { url: page.send, user_agent: userAgent };
122
179
  if (hit.ip) {
123
180
  if (HAS_LIST.test(userAgent)) body.ip = hit.ip;
124
- else body.source = await sha256Hex(`${key}:${hit.ip}`);
181
+ else body.source = await sha256Hex(`${key}:${sourceAddress(hit.ip)}`);
125
182
  }
126
183
 
127
184
  const res = await fetch(options.endpoint ?? ENDPOINT, {
@@ -132,8 +189,13 @@ export async function reportBotHit(hit: BotHit, options: TrackOptions = {}): Pro
132
189
  });
133
190
 
134
191
  if (res.status === 401 || res.status === 402 || res.status === 403) {
135
- entry.stoppedUntil = Date.now() + AUTH_BACKOFF_MS;
136
- warnOnce(entry, `[statlark] Bot tracking stopped: the server answered ${res.status}. Check your bot key and plan.`);
192
+ if (await isStatlarkError(res)) {
193
+ entry.stoppedUntil = Date.now() + AUTH_BACKOFF_MS;
194
+ warnOnce(entry, `[statlark] Bot tracking stopped: the server answered ${res.status}. Check your bot key and plan.`);
195
+ } else {
196
+ // A layer on the way, such as a firewall, answered. Not a verdict on the key.
197
+ countFailure(entry);
198
+ }
137
199
  } else if (res.status === 429) {
138
200
  const seconds = Number(res.headers.get("retry-after"));
139
201
  const wait = seconds > 0 ? seconds * 1000 : FAILURE_BACKOFF_MS;
@@ -145,14 +207,18 @@ export async function reportBotHit(hit: BotHit, options: TrackOptions = {}): Pro
145
207
  }
146
208
 
147
209
  if (res.status === 200) {
148
- const reply = (await res.json()) as { paused_until?: string };
210
+ const reply = (await res.json()) as { paused_until?: string; reason?: string };
211
+ if (reply.reason === "unknown_crawler") {
212
+ if (unknown.size >= MAX_UNKNOWN) unknown.delete(unknown.keys().next().value as string);
213
+ unknown.set(userAgent, Date.now() + UNKNOWN_TTL_MS);
214
+ }
149
215
  if (reply.paused_until) {
150
216
  // Capped, so an upgrade takes effect within the hour.
151
217
  const until = Date.parse(reply.paused_until);
152
218
  const cap = Date.now() + MAX_STOP_MS;
153
219
  entry.stoppedUntil = Number.isNaN(until) ? cap : Math.min(until, cap);
154
220
  }
155
- } else {
221
+ } else if (!res.bodyUsed) {
156
222
  await res.body?.cancel();
157
223
  }
158
224
  } catch (e) {
package/src/next.ts CHANGED
@@ -4,7 +4,8 @@ type Req = { url: string; headers: Headers };
4
4
  type Evt = { waitUntil(p: Promise<unknown>): void };
5
5
 
6
6
  /**
7
- * Call this first in `proxy.ts` (`middleware.ts` before Next 16), before any login check.
7
+ * Call this first in `proxy.ts`, before any login check. Before Next 16 the file is
8
+ * `middleware.ts` and the function is `middleware`.
8
9
  *
9
10
  * Only `x-real-ip` on Vercel is trusted. `x-forwarded-for` is never used: outside Vercel its
10
11
  * leftmost entry is client-supplied, so a scanner could forge a verified request. Behind