@apideck/agent-analytics 0.10.0 → 0.12.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 +132 -0
- package/dist/adapters/posthog.cjs +2 -30
- package/dist/adapters/posthog.cjs.map +1 -1
- package/dist/adapters/posthog.d.cts +7 -1
- package/dist/adapters/posthog.d.ts +7 -1
- package/dist/adapters/posthog.js +2 -28
- package/dist/adapters/posthog.js.map +1 -1
- package/dist/adapters/webhook.cjs +1 -23
- package/dist/adapters/webhook.cjs.map +1 -1
- package/dist/adapters/webhook.d.cts +3 -1
- package/dist/adapters/webhook.d.ts +3 -1
- package/dist/adapters/webhook.js +1 -21
- package/dist/adapters/webhook.js.map +1 -1
- package/dist/index.cjs +2 -302
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -10
- package/dist/index.d.ts +45 -10
- package/dist/index.js +2 -287
- package/dist/index.js.map +1 -1
- package/dist/markdown.cjs +2 -67
- package/dist/markdown.cjs.map +1 -1
- package/dist/markdown.js +2 -63
- package/dist/markdown.js.map +1 -1
- package/dist/{types-B7jSKtLz.d.cts → types-sQoQK-ox.d.cts} +48 -1
- package/dist/{types-B7jSKtLz.d.ts → types-sQoQK-ox.d.ts} +48 -1
- package/dist/verify.cjs +3 -0
- package/dist/verify.cjs.map +1 -0
- package/dist/verify.d.cts +50 -0
- package/dist/verify.d.ts +50 -0
- package/dist/verify.js +3 -0
- package/dist/verify.js.map +1 -0
- package/package.json +7 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bots.ts","../src/bot-ranges.ts","../src/cidr.ts","../src/verify.ts"],"names":["parseBotName","userAgent","s","BOT_IP_RANGES","VERIFIABLE_VENDORS","ipv4ToInt","ip","parts","out","part","n","ipv6ToBigInt","text","zone","tail","lastColon","maybeV4","halves","head","rest","groups","fill","g","compileRanges","cidrs","v4","v4Wide","v6","cidr","slash","addr","bits","net","mask","entry","bucket","list","ipInRanges","ranges","trimmed","value","V4_MAPPED_PREFIX","low","matchV4","r","CLIENT_SIDE_AGENT_PATTERN","SERVER_SIDE_CRAWLER_PATTERN","COMPILED","rangesFor","vendor","c","verifiableVendors","verifyBotIdentity","ua","claimed","isKnownCrawler","inRange","clientIpFromRequest","req","first","verifyRequest"],"mappings":"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CACA,oBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,oBAAA,CACA,qBACA,oBAAA,CACA,mBAAA,CACA,mBACA,oBAAA,CACA,oBAAA,CACA,oBACA,mBAAA,CACA,mBAAA,CACA,oBAAA,CACA,oBAAA,CACA,kBAAA,CACA,mBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,qBACA,iBAAA,CACA,mBAAA,CACA,oBACA,mBAAA,CACA,oBAAA,CACA,kBAAA,CACA,mBAAA,CACA,mBAAA,CACA,kBAAA,CACA,mBACA,mBAAA,CACA,oBAAA,CACA,qBACA,gBAAA,CACA,kBAAA,CACA,mBACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,oBACA,iBAAA,CACA,gBAAA,CACA,mBACA,iBAAA,CACA,iBAAA,CACA,kBACA,iBAAA,CACA,gBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBACA,kBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,kBAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBACA,iBAAA,CACA,eAAA,CACA,kBACA,mBAAA,CACA,iBAAA,CACA,oBACA,kBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBACA,mBAAA,CACA,mBAAA,CACA,oBACA,mBAAA,CACA,mBAAA,CACA,oBACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,mBACA,iBAAA,CACA,kBAAA,CACA,oBACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBACA,iBAAA,CACA,iBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,iBACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,mBACA,iBAAA,CACA,mBAAA,CACA,mBACA,kBAAA,CACA,iBAAA,CACA,mBACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,kBAAA,CACA,iBAAA,CACA,mBACA,iBAAA,CACA,kBAAA,CACA,mBACA,iBAAA,CACA,iBAAA,CACA,gBACA,gBAAA,CACA,gBAAA,CACA,gBAAA,CACA,gBAAA,CACA,iBAAA,CACA,iBAAA,CACA,kBACA,gBAAA,CACA,kBAAA,CACA,iBACA,iBAAA,CACA,iBAAA,CACA,gBACA,kBAAA,CACA,mBAAA,CACA,gBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,gBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBACA,kBAAA,CACA,gBAAA,CACA,gBACA,gBAAA,CACA,gBAAA,CACA,iBACA,gBAAA,CACA,gBAAA,CACA,gBAAA,CACA,eAAA,CACA,iBAAA,CACA,gBAAA,CACA,iBACA,iBAAA,CACA,kBAAA,CACA,mBACA,iBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,oBACA,mBAAA,CACA,mBAAA,CACA,mBACA,kBAAA,CACA,mBAAA,CACA,oBACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,iBAAA,CACA,mBACA,mBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,kBACA,kBAAA,CACA,mBAAA,CACA,oBACA,mBAAA,CACA,mBAAA,CACA,oBACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,oBACA,kBAAA,CACA,kBAAA,CACA,mBACA,mBAAA,CACA,iBAAA,CACA,mBACA,iBAAA,CACA,kBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,oBACA,mBAAA,CACA,kBAAA,CACA,kBACA,kBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBAAA,CACA,kBAAA,CACA,iBAAA,CACA,gBACA,iBAAA,CACA,eAAA,CACA,iBAAA,CACA,eAAA,CACA,iBAAA,CACA,eAAA,CACA,gBACA,iBAAA,CACA,eAAA,CACA,iBAAA,CACA,eAAA,CACA,iBAAA,CACA,eAAA,CACA,iBACA,eAAA,CACA,eAAA,CACA,gBACA,eAAA,CACA,mBAAA,CACA,eACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,iBACA,gBAAA,CACA,iBAAA,CACA,kBACA,eAAA,CACA,kBAAA,CACA,kBACA,iBAAA,CACA,gBAAA,CACA,gBACF,CAAA,CACA,MAAA,CAAQ,CACN,qBACA,iBAAA,CACA,gBAAA,CACA,mBACA,kBAAA,CACA,mBAAA,CACA,mBACA,kBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBAAA,CACA,oBACA,mBAAA,CACA,mBAAA,CACA,mBACA,kBAAA,CACA,mBAAA,CACA,kBACF,CAAA,CACA,UAAA,CAAY,CACV,mBAAA,CACA,kBAAA,CACA,gBAAA,CACA,gBACA,kBAAA,CACA,kBAAA,CACA,iBACA,kBACF,CAAA,CACA,MAAO,CACL,gBAAA,CACA,gBAAA,CACA,gBAAA,CACA,mBAAA,CACA,mBAAA,CACA,oBACA,iBAAA,CACA,iBAAA,CACA,iBACA,gBAAA,CACA,gBAAA,CACA,gBACF,CACF,CAAA,CAGaC,CAAAA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","file":"verify.cjs","sourcesContent":["/**\n * User-agent substrings that identify **publicly declared** AI crawlers — the\n * branded bots that identify themselves by name (OpenAI's GPTBot, Anthropic's\n * ClaudeBot, Perplexity-User, Google-Extended, etc.). High-confidence: when\n * this matches, the request almost certainly comes from that vendor's crawler\n * fleet.\n *\n * Does NOT include **coding-agent traffic** (Claude Code, Cline, Cursor,\n * Windsurf, Aider, OpenCode, VS Code). Those tools use generic HTTP library\n * UAs (axios, curl, got, colly, Electron) or spoof full browser UAs — they\n * can't be distinguished from non-AI traffic by UA alone. See\n * {@link HTTP_CLIENT_PATTERN} for the loose heuristic layer.\n *\n * Sources consulted when updating: darkvisitors.com, vendor docs from OpenAI,\n * Anthropic, Google, Perplexity, Cohere, Apple, Bytedance.\n */\nexport const AI_BOT_PATTERN =\n /ClaudeBot|Claude-User|Claude-SearchBot|Claude-Web|Anthropic|GPTBot|ChatGPT-User|OAI-SearchBot|PerplexityBot|Perplexity-User|Google-Extended|Google-CloudVertexBot|Google-Agent|GoogleAgent-Mariner|Gemini-Deep-Research|Applebot|cohere|Bytespider|CCBot|Amazonbot|Amzn-SearchBot|NovaAct|AzureAI-SearchBot|Meta-ExternalAgent|meta-externalfetcher|meta-webindexer|FacebookBot|DuckAssistBot|MistralAI-User|YouBot|AI2Bot|Diffbot|DeepSeek|PanguBot|Webzio-Extended|omgili|Timpibot|Grok|Manus-User|quillbot|MyCentralAIScraperBot|Cursor|Windsurf/i\n\n/**\n * HTTP library / runtime signatures frequently used by coding agents. Matching\n * any of these is a **loose** signal — legitimate curl scripts, CI jobs, and\n * server-to-server traffic use the same libraries. Use this for the wider\n * net (`coding_agent_hint: true`) and pair with other signals (request\n * shape, JA4 fingerprint, path patterns) for higher confidence.\n *\n * Based on behavioural signatures observed by Addy Osmani:\n * Claude Code → axios/1.8.4\n * Cline, Junie → curl/8.4.0\n * Cursor → got (sindresorhus/got)\n * Windsurf → colly\n * VS Code → Electron / Chromium\n *\n * Aider and OpenCode use Playwright-driven full Mozilla/Safari UAs and are\n * indistinguishable from real browsers at the UA layer.\n */\nexport const HTTP_CLIENT_PATTERN =\n /axios\\/|curl\\/|(?:^|[\\s(])got(?:\\/|[\\s(])|\\bcolly\\b|Electron\\/|node-fetch\\/|python-requests\\/|Go-http-client\\/|okhttp\\/|aiohttp\\/|Deno\\//i\n\nexport function isAiBot(userAgent: string | null | undefined): boolean {\n if (!userAgent) return false\n return AI_BOT_PATTERN.test(userAgent)\n}\n\nexport function isHttpClient(userAgent: string | null | undefined): boolean {\n if (!userAgent) return false\n return HTTP_CLIENT_PATTERN.test(userAgent)\n}\n\n/**\n * Map a user-agent string to a coarse, human-readable label. Returns one of:\n *\n * - A branded-crawler name (`'Claude'`, `'ChatGPT'`, …) — pair with\n * {@link isAiBot} for `is_ai_bot: true` segmentation.\n * - An HTTP-library name (`'curl'`, `'axios'`, `'got'`, `'colly'`,\n * `'Electron'`, …) — hint of a coding agent or automation; not\n * conclusive. Pair with {@link isHttpClient}.\n * - `'Browser'` for typical desktop browsers (possibly spoofed by\n * Playwright-based agents like Aider/OpenCode — this label alone can't\n * tell you).\n * - `'Other'` for anything unrecognised or empty input.\n */\nexport function parseBotName(userAgent: string | null | undefined): string {\n if (!userAgent || typeof userAgent !== 'string') return 'Other'\n const s = userAgent.toLowerCase()\n\n // Publicly declared AI crawlers (high confidence).\n if (s.includes('chatgpt-user') || s.includes('gptbot') || s.includes('oai-searchbot') || s.includes('openai'))\n return 'ChatGPT'\n if (\n s.includes('claudebot') ||\n s.includes('claude-user') ||\n s.includes('claude-searchbot') ||\n s.includes('claude-web') ||\n s.includes('anthropic')\n )\n return 'Claude'\n if (s.includes('perplexitybot') || s.includes('perplexity-user')) return 'Perplexity'\n if (s.includes('ccbot')) return 'Common Crawl'\n if (\n s.includes('google-extended') ||\n s.includes('googlebot') ||\n s.includes('google-cloudvertexbot') ||\n s.includes('google-agent') ||\n s.includes('googleagent-mariner') ||\n s.includes('gemini-deep-research')\n )\n return 'Google'\n if (s.includes('applebot')) return 'Apple'\n if (s.includes('bingbot')) return 'Bing'\n if (s.includes('bytespider')) return 'Bytespider'\n if (s.includes('amazonbot') || s.includes('amzn-searchbot') || s.includes('novaact')) return 'Amazon'\n if (\n s.includes('meta-externalagent') ||\n s.includes('meta-externalfetcher') ||\n s.includes('meta-webindexer') ||\n s.includes('facebookbot')\n )\n return 'Meta'\n if (s.includes('mistralai-user')) return 'Mistral'\n if (s.includes('duckassistbot')) return 'DuckDuckGo'\n if (s.includes('youbot')) return 'You.com'\n if (s.includes('diffbot')) return 'Diffbot'\n if (s.includes('ai2bot')) return 'AI2'\n if (s.includes('cohere')) return 'Cohere'\n if (s.includes('cursor')) return 'Cursor'\n if (s.includes('windsurf')) return 'Windsurf'\n if (s.includes('deepseek')) return 'DeepSeek'\n if (s.includes('pangubot')) return 'Huawei'\n if (s.includes('webzio') || s.includes('omgili')) return 'Webz.io'\n if (s.includes('timpibot')) return 'Timpi'\n if (s.includes('grok') || s.includes('xai-')) return 'xAI'\n if (s.includes('manus-user')) return 'Manus'\n if (s.includes('quillbot')) return 'QuillBot'\n if (s.includes('azureai-searchbot')) return 'Microsoft'\n if (s.includes('mycentralaiscraperbot')) return 'MyCentralAI'\n if (s.includes('petalbot')) return 'PetalBot'\n\n // SEO crawlers and monitoring bots.\n if (s.includes('ahrefsbot')) return 'Ahrefs'\n if (s.includes('semrushbot')) return 'Semrush'\n if (s.includes('mj12bot')) return 'Majestic'\n if (s.includes('dotbot')) return 'Moz'\n if (s.includes('rogerbot')) return 'Moz'\n if (s.includes('screaming frog')) return 'Screaming Frog'\n if (s.includes('sitebulb')) return 'Sitebulb'\n if (s.includes('linkfluence')) return 'Linkfluence'\n if (s.includes('dataforseo')) return 'DataForSEO'\n if (s.includes('serpstatbot')) return 'Serpstat'\n\n // Monitoring and feed bots.\n if (s.includes('uptimerobot')) return 'UptimeRobot'\n if (s.includes('pingdom')) return 'Pingdom'\n if (s.includes('statuscake')) return 'StatusCake'\n if (s.includes('newrelicpinger')) return 'New Relic'\n if (s.includes('datadogagent') || s.includes('datadog')) return 'Datadog'\n if (s.includes('slackbot')) return 'Slack'\n if (s.includes('twitterbot')) return 'Twitter'\n if (s.includes('linkedinbot')) return 'LinkedIn'\n if (s.includes('discordbot')) return 'Discord'\n if (s.includes('telegrambot')) return 'Telegram'\n if (s.includes('whatsapp')) return 'WhatsApp'\n\n // AI search and indexing bots.\n if (s.includes('linkupbot')) return 'Linkup'\n if (s.includes('sogou')) return 'Sogou'\n if (s.includes('yandexbot')) return 'Yandex'\n if (s.includes('baiduspider')) return 'Baidu'\n\n // Link preview fetchers.\n if (s.includes('facebookexternalhit')) return 'Facebook'\n if (s.includes('com.apple.webkit')) return 'Apple URL Preview'\n\n // Uptime and monitoring.\n if (s.includes('ohdear')) return 'Oh Dear'\n\n // Generic scrapers.\n if (s.includes('scrapy')) return 'Scrapy'\n if (s.includes('headlesschrome')) return 'Headless Chrome'\n if (s.includes('phantomjs')) return 'PhantomJS'\n if (s.includes('wget')) return 'wget'\n if (s.includes('httpie')) return 'HTTPie'\n if (s.includes('guzzlehttp')) return 'Guzzle'\n\n // HTTP library / runtime signatures (loose — coding agent or automation).\n // Check Electron before Browser since Electron UAs contain Chrome/Safari.\n if (s.includes('electron/')) return 'Electron'\n if (/curl\\//.test(s)) return 'curl'\n if (/axios\\//.test(s)) return 'axios'\n if (/(?:^|[\\s(])got(?:\\/|[\\s(])/.test(s)) return 'got'\n if (/\\bcolly\\b/.test(s)) return 'colly'\n if (/node-fetch\\//.test(s)) return 'node-fetch'\n if (/python-requests\\//.test(s)) return 'python-requests'\n if (/go-http-client\\//.test(s)) return 'Go http client'\n if (/okhttp\\//.test(s)) return 'OkHttp'\n if (/aiohttp\\//.test(s)) return 'aiohttp'\n if (/deno\\//.test(s)) return 'Deno'\n\n // Real browsers (or UAs spoofed to look like them — see Aider/OpenCode note).\n if (s.includes('mozilla') || s.includes('chrome') || s.includes('safari') || s.includes('firefox'))\n return 'Browser'\n\n return 'Other'\n}\n\n/**\n * Return the first product token from a UA header, useful for segmenting by\n * client without hard-coding every bot name. Falls back to `'Other'` for empty\n * input.\n */\nexport function firstUserAgentProduct(userAgent: string | null | undefined): string {\n if (!userAgent || typeof userAgent !== 'string') return 'Other'\n const compatibleMatch = userAgent.match(/compatible;\\s*([^/;\\s]+)(?:\\/[^\\s;]*)?/i)\n if (compatibleMatch && compatibleMatch[1]) return compatibleMatch[1].trim()\n const first = userAgent.trim().split('/')[0]?.trim().split(/\\s+/)[0]?.trim()\n return first || 'Other'\n}\n\n/**\n * Detect likely headless/automated browsers by checking for missing headers\n * that real browsers always send. Playwright, Puppeteer, and similar tools\n * spoof the UA but often omit standard browser headers.\n *\n * Signals checked (each scores 1 point):\n * - Missing `Accept-Language` — every real browser sends this\n * - Missing `Sec-Fetch-Mode` — sent by all modern browsers\n * - Missing `Sec-CH-UA` — Client Hints, Chromium 89+\n * - `Sec-CH-UA` contains \"HeadlessChrome\"\n * - Missing or bare Accept header — browsers send detailed accept lists\n * - `Connection: close` with browser UA — browsers use keep-alive\n *\n * Returns a score (0-6), the signals that fired, and a boolean `likely`\n * flag (score >= 2 with a browser-like UA).\n */\nexport function detectHeadless(req: Request): HeadlessDetection {\n const signals: string[] = []\n const ua = (req.headers.get('user-agent') || '').toLowerCase()\n const isBrowserUA =\n ua.includes('mozilla') || ua.includes('chrome') || ua.includes('safari') || ua.includes('firefox')\n\n if (!isBrowserUA) return { score: 0, signals: [], likely: false }\n\n if (!req.headers.get('accept-language')) {\n signals.push('missing-accept-language')\n }\n if (!req.headers.get('sec-fetch-mode')) {\n signals.push('missing-sec-fetch-mode')\n }\n const secChUa = req.headers.get('sec-ch-ua')\n if (!secChUa) {\n signals.push('missing-sec-ch-ua')\n } else if (secChUa.toLowerCase().includes('headlesschrome')) {\n signals.push('headless-chrome-hint')\n }\n const accept = req.headers.get('accept') || ''\n if (!accept || accept === '*/*') {\n signals.push('missing-or-bare-accept')\n }\n if ((req.headers.get('connection') || '').toLowerCase() === 'close') {\n signals.push('connection-close')\n }\n\n const score = signals.length\n return { score, signals, likely: score >= 2 }\n}\n\nexport interface HeadlessDetection {\n /** Number of suspicious signals found (0-6). */\n score: number\n /** Names of the specific signals that fired. */\n signals: string[]\n /** True when score >= 2 — strong headless indication. */\n likely: boolean\n}\n\nexport type AgentKind =\n | 'declared-crawler'\n | 'coding-agent-hint'\n | 'headless-likely'\n | 'browser'\n | 'other'\n\nexport interface AgentClassification {\n /**\n * Categorical tag for the request:\n *\n * - `'declared-crawler'` — {@link AI_BOT_PATTERN} matched. High confidence.\n * - `'coding-agent-hint'` — {@link HTTP_CLIENT_PATTERN} matched. Loose\n * signal; could be a coding agent, a curl script, or any automation.\n * - `'headless-likely'` — Browser-like UA but missing standard headers.\n * Strong signal of Playwright/Puppeteer automation (Aider, OpenCode, etc.).\n * - `'browser'` — Looks like a real browser with expected headers present.\n * - `'other'` — Unrecognised or empty.\n */\n kind: AgentKind\n /** Human-readable label, same string {@link parseBotName} returns. */\n label: string\n /** Strict: `true` only when the UA matches a branded AI crawler. */\n isAiBot: boolean\n /** Loose: `true` for known HTTP-library / automation UAs. */\n codingAgentHint: boolean\n /** Headless browser detection result. Only populated when `req` is passed. */\n headless?: HeadlessDetection\n}\n\n/**\n * UA-only classification. Use {@link classifyRequest} for full detection\n * including headless browser heuristics.\n */\nexport function classifyAgent(userAgent: string | null | undefined): AgentClassification {\n const label = parseBotName(userAgent)\n const aiBot = isAiBot(userAgent)\n const httpClient = isHttpClient(userAgent)\n\n let kind: AgentKind\n if (aiBot) kind = 'declared-crawler'\n else if (httpClient) kind = 'coding-agent-hint'\n else if (label === 'Browser') kind = 'browser'\n else kind = 'other'\n\n return { kind, label, isAiBot: aiBot, codingAgentHint: httpClient }\n}\n\n/**\n * Full request classification — combines UA parsing with header-based\n * headless detection. When a browser-like UA is missing standard headers,\n * the kind is promoted from `'browser'` to `'headless-likely'`.\n */\nexport function classifyRequest(req: Request): AgentClassification {\n const userAgent = req.headers.get('user-agent') || ''\n const base = classifyAgent(userAgent)\n const headless = detectHeadless(req)\n\n let kind = base.kind\n let label = base.label\n if (kind === 'browser' && headless.likely) {\n kind = 'headless-likely'\n // Relabel too. Leaving it as 'Browser' meant automation with a spoofed\n // browser UA — 79% of one production site's agent traffic — was\n // indistinguishable from a human in any `bot_name` breakdown, and was\n // silently excluded by the obvious `bot_name != 'Browser'` filter.\n label = 'Headless'\n }\n\n return { ...base, kind, label, headless }\n}\n","/**\n * Published crawler IP ranges, vendor by vendor.\n *\n * GENERATED FILE — do not edit by hand. Regenerate with:\n *\n * node scripts/refresh-bot-ranges.mjs\n *\n * Keys match the labels {@link parseBotName} returns, so a claimed identity\n * maps to its range list without a translation table.\n *\n * Only vendors that publish a machine-readable feed appear here. A vendor's\n * absence means \"cannot be verified\", never \"not a real bot\" — see\n * {@link verifyBotIdentity} for how that distinction is surfaced.\n *\n * Freshness is the whole ballgame: these lists rotate. Almost every OpenAI\n * prefix is an Azure block and every Anthropic prefix is GCP or similar, so\n * \"came from a datacenter\" proves nothing on its own — only membership in the\n * current published list does. A stale snapshot produces false 'spoofed'\n * verdicts on legitimate crawlers, which is the failure mode to fear.\n */\n\n/** When these ranges were captured from the vendor feeds (UTC). */\nexport const BOT_RANGES_CAPTURED_AT = '2026-08-02T00:00:00Z'\n\nexport const BOT_IP_RANGES: Readonly<Record<string, readonly string[]>> = {\n ChatGPT: [\n '104.208.184.192/28',\n '104.208.184.208/28',\n '104.210.139.192/28',\n '104.210.139.224/28',\n '104.210.140.128/28',\n '128.85.198.32/28',\n '13.65.138.112/28',\n '13.65.138.96/28',\n '13.67.72.16/28',\n '13.70.107.160/28',\n '13.71.2.208/28',\n '13.76.115.224/28',\n '13.76.115.240/28',\n '13.76.116.80/28',\n '13.76.32.208/28',\n '13.83.167.128/28',\n '13.83.237.176/28',\n '132.196.82.48/28',\n '132.196.86.0/24',\n '134.149.233.80/28',\n '135.116.136.160/28',\n '135.13.64.240/28',\n '135.220.73.208/28',\n '135.220.73.240/28',\n '135.234.64.0/24',\n '135.237.131.208/28',\n '135.237.133.48/28',\n '137.135.191.176/28',\n '138.91.30.48/28',\n '138.91.46.96/28',\n '145.132.1.32/28',\n '145.132.136.96/28',\n '145.133.0.176/28',\n '158.158.5.32/28',\n '168.63.252.240/28',\n '172.162.248.64/28',\n '172.170.1.80/28',\n '172.170.225.0/28',\n '172.170.241.80/28',\n '172.170.8.208/28',\n '172.171.4.176/28',\n '172.175.152.224/28',\n '172.178.140.144/28',\n '172.178.141.112/28',\n '172.178.141.128/28',\n '172.182.193.224/28',\n '172.182.193.80/28',\n '172.182.194.144/28',\n '172.182.194.32/28',\n '172.182.195.48/28',\n '172.182.202.0/25',\n '172.182.204.0/24',\n '172.182.207.0/25',\n '172.182.209.208/28',\n '172.182.211.192/28',\n '172.182.213.192/28',\n '172.182.214.0/24',\n '172.182.215.0/24',\n '172.182.224.0/28',\n '172.183.143.224/28',\n '172.183.222.128/28',\n '172.192.112.208/28',\n '172.192.88.192/28',\n '172.192.97.32/28',\n '172.197.160.192/28',\n '172.197.161.208/28',\n '172.197.170.80/28',\n '172.197.203.16/28',\n '172.199.137.80/28',\n '172.202.102.112/28',\n '172.203.190.128/28',\n '172.204.27.16/28',\n '172.204.28.224/28',\n '172.204.96.32/28',\n '172.204.96.48/28',\n '172.204.96.80/28',\n '172.205.189.192/28',\n '172.207.1.32/28',\n '172.208.128.32/28',\n '172.208.128.48/28',\n '172.212.159.64/28',\n '172.212.172.160/28',\n '172.213.21.16/28',\n '172.215.215.32/28',\n '172.215.218.96/28',\n '191.233.1.112/28',\n '191.233.1.128/28',\n '191.233.194.32/28',\n '191.233.196.112/28',\n '191.233.199.160/28',\n '191.233.2.0/28',\n '191.235.66.16/28',\n '191.235.99.80/28',\n '191.237.249.64/28',\n '191.239.245.16/28',\n '20.102.212.144/28',\n '20.113.211.112/28',\n '20.113.225.112/28',\n '20.125.112.224/28',\n '20.125.144.144/28',\n '20.125.66.80/28',\n '20.14.99.96/28',\n '20.161.75.208/28',\n '20.168.18.32/28',\n '20.168.7.192/28',\n '20.168.7.240/28',\n '20.169.6.224/28',\n '20.169.7.48/28',\n '20.169.72.112/28',\n '20.169.73.176/28',\n '20.169.73.32/28',\n '20.169.73.64/28',\n '20.169.77.0/25',\n '20.169.78.112/28',\n '20.169.78.128/28',\n '20.169.78.144/28',\n '20.169.78.160/28',\n '20.169.78.176/28',\n '20.169.78.192/28',\n '20.169.78.208/28',\n '20.169.78.48/28',\n '20.169.78.64/28',\n '20.169.78.80/28',\n '20.169.78.96/28',\n '20.169.86.224/28',\n '20.169.86.240/28',\n '20.169.87.112/28',\n '20.17.108.96/28',\n '20.170.184.16/28',\n '20.170.184.32/28',\n '20.170.184.48/28',\n '20.170.184.64/28',\n '20.170.184.80/28',\n '20.171.123.64/28',\n '20.171.206.0/24',\n '20.171.207.0/24',\n '20.171.53.224/28',\n '20.172.29.32/28',\n '20.193.233.240/28',\n '20.193.50.32/28',\n '20.194.0.208/28',\n '20.194.1.0/28',\n '20.198.67.96/28',\n '20.199.211.160/28',\n '20.199.242.0/28',\n '20.200.212.240/28',\n '20.204.24.240/28',\n '20.210.154.128/28',\n '20.210.174.208/28',\n '20.210.211.192/28',\n '20.215.187.208/28',\n '20.215.188.192/28',\n '20.215.214.16/28',\n '20.215.219.128/28',\n '20.215.219.160/28',\n '20.215.219.208/28',\n '20.215.220.112/28',\n '20.215.220.128/28',\n '20.215.220.144/28',\n '20.215.220.160/28',\n '20.215.220.176/28',\n '20.215.220.192/28',\n '20.215.220.208/28',\n '20.215.220.64/28',\n '20.215.220.80/28',\n '20.215.220.96/28',\n '20.218.30.240/28',\n '20.219.71.192/28',\n '20.222.36.192/28',\n '20.226.32.80/28',\n '20.227.140.32/28',\n '20.228.106.176/28',\n '20.235.75.208/28',\n '20.235.87.224/28',\n '20.249.63.208/28',\n '20.25.151.224/28',\n '20.250.136.64/28',\n '20.250.136.80/28',\n '20.250.6.128/28',\n '20.27.94.128/28',\n '20.42.10.176/28',\n '20.45.178.144/28',\n '20.48.120.208/28',\n '20.52.125.160/28',\n '20.55.129.0/28',\n '20.55.229.144/28',\n '20.57.199.192/28',\n '20.63.180.96/28',\n '20.63.221.64/28',\n '20.79.59.112/28',\n '20.81.183.64/28',\n '20.83.243.176/28',\n '20.97.189.96/28',\n '23.102.140.144/28',\n '23.102.141.32/28',\n '23.98.142.176/28',\n '23.98.179.16/28',\n '23.98.186.176/28',\n '23.98.186.192/28',\n '23.98.186.64/28',\n '23.98.186.96/28',\n '4.151.119.48/28',\n '4.151.241.240/28',\n '4.151.71.176/28',\n '4.189.118.208/28',\n '4.189.119.48/28',\n '4.196.118.112/28',\n '4.197.115.112/28',\n '4.197.19.176/28',\n '4.197.22.112/28',\n '4.197.64.0/28',\n '4.197.64.16/28',\n '4.197.64.48/28',\n '4.197.64.64/28',\n '4.198.72.16/28',\n '4.198.96.112/28',\n '4.201.232.64/28',\n '4.201.232.80/28',\n '4.203.96.80/28',\n '4.205.128.176/28',\n '4.218.24.64/28',\n '4.226.200.16/28',\n '4.226.226.32/28',\n '4.227.36.0/25',\n '40.116.73.208/28',\n '40.122.235.112/28',\n '40.67.175.0/25',\n '40.67.183.160/28',\n '40.67.183.176/28',\n '40.78.161.48/28',\n '40.81.134.128/28',\n '40.81.134.144/28',\n '40.81.234.144/28',\n '40.81.67.96/28',\n '40.84.181.32/28',\n '40.84.221.208/28',\n '40.84.221.224/28',\n '40.90.214.16/28',\n '48.193.44.32/28',\n '48.221.184.112/28',\n '48.221.184.80/28',\n '48.221.184.96/28',\n '48.221.40.176/28',\n '51.107.70.192/28',\n '51.116.2.80/28',\n '51.116.221.96/28',\n '51.56.40.80/28',\n '51.57.0.96/28',\n '51.59.24.64/28',\n '51.59.24.80/28',\n '51.59.40.80/28',\n '51.59.40.96/28',\n '51.59.48.80/28',\n '51.59.48.96/28',\n '51.8.102.0/24',\n '51.8.155.112/28',\n '51.8.155.48/28',\n '51.8.155.64/28',\n '51.8.187.224/28',\n '52.148.129.32/28',\n '52.153.130.64/28',\n '52.154.22.48/28',\n '52.156.77.144/28',\n '52.159.227.32/28',\n '52.159.249.96/28',\n '52.161.49.224/28',\n '52.161.49.96/28',\n '52.165.212.16/28',\n '52.165.212.32/28',\n '52.165.212.48/28',\n '52.172.129.160/28',\n '52.172.251.112/28',\n '52.173.219.112/28',\n '52.173.219.96/28',\n '52.173.221.16/28',\n '52.173.221.176/28',\n '52.173.221.208/28',\n '52.173.234.16/28',\n '52.173.234.80/28',\n '52.173.235.80/28',\n '52.183.217.240/28',\n '52.187.246.128/28',\n '52.190.137.144/28',\n '52.190.137.16/28',\n '52.190.139.48/28',\n '52.190.142.64/28',\n '52.190.190.16/28',\n '52.225.75.208/28',\n '52.230.152.0/24',\n '52.230.163.32/28',\n '52.230.164.176/28',\n '52.231.30.48/28',\n '52.231.34.176/28',\n '52.231.39.144/28',\n '52.231.39.192/28',\n '52.231.49.48/28',\n '52.231.50.64/28',\n '52.236.94.144/28',\n '52.241.146.208/28',\n '52.242.132.224/28',\n '52.242.132.240/28',\n '52.242.245.208/28',\n '52.252.113.240/28',\n '52.255.109.112/28',\n '52.255.109.128/28',\n '52.255.109.144/28',\n '52.255.109.80/28',\n '52.255.109.96/28',\n '52.255.111.0/28',\n '52.255.111.112/28',\n '52.255.111.32/28',\n '52.255.111.48/28',\n '52.255.111.80/28',\n '57.154.174.112/28',\n '57.154.175.0/28',\n '57.154.187.32/28',\n '68.154.28.96/28',\n '68.218.30.112/28',\n '68.220.57.64/28',\n '68.221.67.192/28',\n '68.221.67.224/28',\n '68.221.67.240/28',\n '70.153.139.208/28',\n '70.153.189.192/28',\n '70.153.190.16/28',\n '70.153.76.16/28',\n '70.153.87.224/28',\n '70.156.144.64/28',\n '70.156.152.80/28',\n '70.156.152.96/28',\n '74.161.200.96/28',\n '74.224.217.64/28',\n '74.226.253.160/28',\n '74.249.86.176/28',\n '74.7.175.128/25',\n '74.7.227.0/25',\n '74.7.227.128/25',\n '74.7.228.0/25',\n '74.7.228.128/25',\n '74.7.229.0/25',\n '74.7.229.128/25',\n '74.7.230.0/25',\n '74.7.241.0/25',\n '74.7.241.128/25',\n '74.7.242.0/25',\n '74.7.242.128/25',\n '74.7.243.0/25',\n '74.7.243.128/25',\n '74.7.244.0/25',\n '74.7.35.112/28',\n '74.7.35.48/28',\n '74.7.36.64/28',\n '74.7.36.80/28',\n '74.7.36.96/28',\n '85.211.241.128/28',\n '9.129.0.0/17',\n '9.160.128.16/28',\n '9.160.128.32/28',\n '9.160.128.64/28',\n '9.160.163.128/28',\n '9.160.164.128/28',\n '9.160.34.144/28',\n '9.160.36.16/28',\n '9.160.96.16/28',\n '9.163.101.48/28',\n '9.205.25.128/28',\n '9.205.8.48/28',\n '9.223.181.208/28',\n '9.234.96.192/28',\n '9.234.97.128/28',\n '9.234.97.96/28',\n '9.235.40.32/28',\n ],\n Claude: [\n '136.107.176.208/32',\n '216.73.216.0/22',\n '34.11.34.31/32',\n '34.150.241.79/32',\n '34.162.191.81/32',\n '34.162.230.222/32',\n '34.162.244.71/32',\n '34.182.140.95/32',\n '34.182.161.143/32',\n '34.182.218.27/32',\n '34.182.220.85/32',\n '34.182.222.37/32',\n '34.182.225.167/32',\n '34.182.226.151/32',\n '34.182.226.221/32',\n '34.186.108.163/32',\n '34.85.172.162/32',\n '35.221.29.174/32',\n '35.245.175.129/32',\n '35.245.89.239/32',\n ],\n Perplexity: [\n '107.20.236.150/32',\n '18.210.92.235/32',\n '18.97.1.228/30',\n '18.97.9.96/29',\n '3.211.124.183/32',\n '3.222.232.239/32',\n '3.224.62.45/32',\n '3.231.139.107/32',\n ],\n Apple: [\n '17.22.237.0/24',\n '17.22.245.0/24',\n '17.22.253.0/24',\n '17.241.193.160/27',\n '17.241.200.160/27',\n '17.241.208.160/27',\n '17.241.219.0/24',\n '17.241.227.0/24',\n '17.241.75.0/24',\n '17.246.15.0/24',\n '17.246.19.0/24',\n '17.246.23.0/24',\n ],\n}\n\n/** Vendor labels this build can verify. Anything else yields a null verdict. */\nexport const VERIFIABLE_VENDORS: readonly string[] = Object.keys(BOT_IP_RANGES)\n","/**\n * Dependency-free CIDR matching for IPv4 and IPv6.\n *\n * Kept separate from {@link verifyBotIdentity} so the matching logic can be\n * tested in isolation — a wrong answer here silently turns a real crawler into\n * a \"spoofed\" one, which is worse than not verifying at all.\n *\n * Everything is parsed once into numeric form (`number` for v4, `bigint` for\n * v6) and compared with masks. No allocation per request beyond the parse of\n * the incoming IP.\n */\n\n/** Parse dotted-quad IPv4 into a 32-bit unsigned integer. */\nexport function ipv4ToInt(ip: string): number | null {\n const parts = ip.split('.')\n if (parts.length !== 4) return null\n let out = 0\n for (const part of parts) {\n // Reject empty, non-numeric, out-of-range, and leading-zero forms\n // (`01.2.3.4` is ambiguous — some parsers read it as octal).\n if (!/^\\d{1,3}$/.test(part)) return null\n if (part.length > 1 && part[0] === '0') return null\n const n = Number(part)\n if (n > 255) return null\n out = (out << 8) | n\n }\n // `>>> 0` converts the signed 32-bit result back to unsigned.\n return out >>> 0\n}\n\n/**\n * Parse IPv6 (including `::` compression and IPv4-mapped tails like\n * `::ffff:1.2.3.4`) into a 128-bit BigInt.\n */\nexport function ipv6ToBigInt(ip: string): bigint | null {\n let text = ip\n // Strip a zone index (`fe80::1%eth0`) — irrelevant for range membership.\n const zone = text.indexOf('%')\n if (zone !== -1) text = text.slice(0, zone)\n if (!text || text.indexOf(':') === -1) return null\n\n // An embedded IPv4 tail contributes the low 32 bits.\n let tail: number | null = null\n const lastColon = text.lastIndexOf(':')\n const maybeV4 = text.slice(lastColon + 1)\n if (maybeV4.indexOf('.') !== -1) {\n tail = ipv4ToInt(maybeV4)\n if (tail === null) return null\n text = text.slice(0, lastColon + 1) + '0:0'\n }\n\n const halves = text.split('::')\n if (halves.length > 2) return null\n const head = halves[0] ? halves[0].split(':') : []\n const rest = halves.length === 2 ? (halves[1] ? halves[1].split(':') : []) : null\n\n let groups: string[]\n if (rest === null) {\n groups = head\n if (groups.length !== 8) return null\n } else {\n const fill = 8 - head.length - rest.length\n if (fill < 0) return null\n groups = [...head, ...Array(fill).fill('0'), ...rest]\n }\n\n let out = 0n\n for (const g of groups) {\n if (!/^[0-9a-fA-F]{1,4}$/.test(g)) return null\n out = (out << 16n) | BigInt(parseInt(g, 16))\n }\n // Overwrite the low 32 bits with the embedded IPv4 value when present.\n if (tail !== null) out = ((out >> 32n) << 32n) | BigInt(tail >>> 0)\n return out\n}\n\ninterface V4Range {\n net: number\n mask: number\n}\ninterface V6Range {\n net: bigint\n bits: number\n}\n\nexport interface CompiledRanges {\n /**\n * IPv4 ranges bucketed by first octet. A vendor can publish hundreds of\n * prefixes (OpenAI: 372) and a linear scan walked all of them on every\n * request; bucketing turns the common case into one map lookup plus a\n * handful of comparisons. Prefixes shorter than /8 span several buckets and\n * are held in `v4Wide`, which stays tiny.\n */\n v4: Map<number, V4Range[]>\n v4Wide: V4Range[]\n v6: V6Range[]\n}\n\n/**\n * Pre-compile a list of CIDR strings into numeric form. Invalid entries are\n * dropped rather than thrown — a malformed line in a vendor's published feed\n * shouldn't take down the whole check.\n */\nexport function compileRanges(cidrs: readonly string[]): CompiledRanges {\n const v4 = new Map<number, V4Range[]>()\n const v4Wide: V4Range[] = []\n const v6: V6Range[] = []\n for (const cidr of cidrs) {\n const slash = cidr.lastIndexOf('/')\n if (slash === -1) continue\n const addr = cidr.slice(0, slash)\n const bits = Number(cidr.slice(slash + 1))\n if (!Number.isInteger(bits) || bits < 0) continue\n\n if (addr.indexOf(':') !== -1) {\n if (bits > 128) continue\n const net = ipv6ToBigInt(addr)\n if (net === null) continue\n v6.push({ net: bits === 0 ? 0n : (net >> BigInt(128 - bits)) << BigInt(128 - bits), bits })\n } else {\n if (bits > 32) continue\n const net = ipv4ToInt(addr)\n if (net === null) continue\n // `bits === 0` needs special handling: `<<32` is a no-op in JS, not zero.\n const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0\n const entry = { net: (net & mask) >>> 0, mask }\n if (bits >= 8) {\n const bucket = entry.net >>> 24\n const list = v4.get(bucket)\n if (list) list.push(entry)\n else v4.set(bucket, [entry])\n } else {\n v4Wide.push(entry)\n }\n }\n }\n return { v4, v4Wide, v6 }\n}\n\n/** True when `ip` falls inside any range in the pre-compiled set. */\nexport function ipInRanges(ip: string, ranges: CompiledRanges): boolean {\n if (!ip) return false\n const trimmed = ip.trim()\n if (!trimmed) return false\n\n if (trimmed.indexOf(':') !== -1) {\n const value = ipv6ToBigInt(trimmed)\n if (value === null) return false\n // An IPv4-mapped address (::ffff:a.b.c.d) must also be checked against the\n // v4 list — Vercel and Cloudflare both emit this form on dual-stack edges.\n const V4_MAPPED_PREFIX = 0xffffn << 32n\n if ((value >> 32n) === V4_MAPPED_PREFIX >> 32n) {\n const low = Number(value & 0xffffffffn) >>> 0\n if (matchV4(low, ranges)) return true\n }\n for (const r of ranges.v6) {\n if (r.bits === 0) return true\n if ((value >> BigInt(128 - r.bits)) << BigInt(128 - r.bits) === r.net) return true\n }\n return false\n }\n\n const value = ipv4ToInt(trimmed)\n if (value === null) return false\n return matchV4(value, ranges)\n}\n\nfunction matchV4(value: number, ranges: CompiledRanges): boolean {\n const bucket = ranges.v4.get(value >>> 24)\n if (bucket) {\n for (const r of bucket) {\n if (((value & r.mask) >>> 0) === r.net) return true\n }\n }\n for (const r of ranges.v4Wide) {\n if (((value & r.mask) >>> 0) === r.net) return true\n }\n return false\n}\n\n/** Convenience wrapper — compiles on every call, so prefer {@link ipInRanges}. */\nexport function ipInCidr(ip: string, cidr: string): boolean {\n return ipInRanges(ip, compileRanges([cidr]))\n}\n","import { parseBotName } from './bots.js'\nimport { BOT_IP_RANGES, VERIFIABLE_VENDORS } from './bot-ranges.js'\nimport { compileRanges, ipInRanges, type CompiledRanges } from './cidr.js'\n\n/**\n * Verdict on whether a request's claimed crawler identity holds up against the\n * vendor's published IP ranges.\n *\n * - `'verified'` — the UA claims a vendor we can check, and the client IP is\n * inside that vendor's published ranges. High confidence.\n * - `'spoofed'` — the UA claims a vendor we can check, and the IP is **not**\n * in its ranges. Someone is impersonating the crawler.\n * - `'unverifiable'` — the UA claims a vendor that publishes no feed we bundle\n * (Bytespider, Amazon, Meta, …), or no usable client IP was available.\n * - `'not-claimed'` — the UA doesn't claim a verifiable crawler at all. This is\n * the normal verdict for browsers and HTTP clients; it is *not* a negative\n * finding.\n */\nexport type VerificationVerdict = 'verified' | 'spoofed' | 'unverifiable' | 'not-claimed'\n\n/** Why a request could not be judged. Only set when verdict is 'unverifiable'. */\nexport type UnverifiableReason = 'no-published-ranges' | 'client-side-agent' | 'no-client-ip'\n\nexport interface BotVerification {\n verdict: VerificationVerdict\n reason?: UnverifiableReason\n /** Vendor label the UA claims, when it claims one. */\n claimed: string | null\n /**\n * Convenience boolean for filtering: `true` only for `'verified'`, `false`\n * only for `'spoofed'`, `null` when no judgement was possible. Deliberately\n * tri-state — collapsing \"unverifiable\" into `false` would brand every\n * Bytespider and Amazonbot hit an impostor.\n */\n verified: boolean | null\n}\n\n/**\n * Products that fetch from the **end user's own device**, not from the\n * vendor's infrastructure.\n *\n * A published range list only covers a vendor's server-side crawler fleet.\n * When the fetch originates on a developer's laptop, the client IP is theirs\n * and will never appear in the vendor's ranges — so a range check produces\n * 'spoofed' for entirely legitimate traffic.\n *\n * This is not cosmetic. Measured against 30 days of production data:\n *\n * Claude-User (claude-code CLI) 6,492 events 4,486 IPs 0% in range\n * Perplexity-User 493 events 148 IPs 0% in range\n * ClaudeBot 13,671 events 236 IPs 96% in range\n * PerplexityBot 6,897 events 158 IPs 91% in range\n *\n * Treating the first two as impostors would have falsely accused ~7k real\n * fetches a month. The distinction is per *product*, not per vendor, and not\n * inferable from a `-User` suffix — OpenAI's ChatGPT-User fetches server-side\n * from Azure and verifies at ~99%.\n *\n * Only products belonging to a vendor we hold ranges for need listing here.\n * Cursor, Windsurf, Cline and Aider were previously included but were dead\n * branches: their vendors publish no feed, so they exit earlier as\n * 'no-published-ranges' or 'not-claimed' and never reach this test.\n */\nconst CLIENT_SIDE_AGENT_PATTERN = /claude-code|perplexity-user/i\n\n/**\n * Products known to fetch server-side from ranges the vendor publishes. Only\n * these can earn a 'verified' or 'spoofed' verdict; anything else is reported\n * 'unverifiable' so the data never overstates what was actually checked.\n */\nconst SERVER_SIDE_CRAWLER_PATTERN =\n /ClaudeBot|Claude-SearchBot|GPTBot|OAI-SearchBot|ChatGPT-User|PerplexityBot|Applebot/i\n\n// Compiled on first use per vendor, not at module load. Eagerly parsing all\n// 412 prefixes cost cold-start CPU on every request path that imported this\n// module, including the majority that never verify anything.\nconst COMPILED = new Map<string, CompiledRanges>()\n\nfunction rangesFor(vendor: string): CompiledRanges | undefined {\n if (!(vendor in BOT_IP_RANGES)) return undefined\n let c = COMPILED.get(vendor)\n if (!c) {\n c = compileRanges(BOT_IP_RANGES[vendor] ?? [])\n COMPILED.set(vendor, c)\n }\n return c\n}\n\n/** Vendor labels this build can produce a verified/spoofed verdict for. */\nexport function verifiableVendors(): readonly string[] {\n return VERIFIABLE_VENDORS\n}\n\n/**\n * Check a claimed crawler identity against the vendor's published IP ranges.\n *\n * Pass the client IP you already trust — on Vercel and Cloudflare that is the\n * first hop of `x-forwarded-for`. If your edge doesn't strip client-supplied\n * `X-Forwarded-For`, an attacker controls this value and a `'verified'` verdict\n * means nothing; verify your proxy's behaviour before relying on it.\n */\nexport function verifyBotIdentity(\n userAgent: string | null | undefined,\n ip: string | null | undefined\n): BotVerification {\n const ua = userAgent ?? ''\n const claimed = parseBotName(userAgent)\n const ranges = rangesFor(claimed)\n\n if (!ranges) {\n // Either not a crawler at all, or a crawler with no published feed. Both\n // are \"no judgement\", but the caller may want to tell them apart.\n const isKnownCrawler = claimed !== 'Other' && claimed !== 'Browser'\n return {\n verdict: isKnownCrawler ? 'unverifiable' : 'not-claimed',\n ...(isKnownCrawler ? { reason: 'no-published-ranges' as const } : {}),\n claimed: isKnownCrawler ? claimed : null,\n verified: null\n }\n }\n\n // Order matters: a client-side agent must be excluded before the range check,\n // and the server-side allowlist gates everything else, so an unrecognised\n // product from a covered vendor is never accused on a partial range list.\n if (CLIENT_SIDE_AGENT_PATTERN.test(ua) || !SERVER_SIDE_CRAWLER_PATTERN.test(ua)) {\n return {\n verdict: 'unverifiable',\n reason: 'client-side-agent',\n claimed,\n verified: null\n }\n }\n\n const trimmed = (ip ?? '').trim()\n if (!trimmed) {\n return { verdict: 'unverifiable', reason: 'no-client-ip', claimed, verified: null }\n }\n\n const inRange = ipInRanges(trimmed, ranges)\n return {\n verdict: inRange ? 'verified' : 'spoofed',\n claimed,\n verified: inRange\n }\n}\n\n/**\n * Extract the client IP the way {@link trackVisit} does — first hop of\n * `x-forwarded-for`, falling back to the platform-specific headers.\n */\nexport function clientIpFromRequest(req: Request): string {\n const forwarded = req.headers.get('x-forwarded-for') || ''\n const first = forwarded.split(',')[0]?.trim()\n if (first) return first\n return (req.headers.get('cf-connecting-ip') || req.headers.get('x-real-ip') || '').trim()\n}\n\n/** Verify straight from a request object. */\nexport function verifyRequest(req: Request): BotVerification {\n return verifyBotIdentity(req.headers.get('user-agent'), clientIpFromRequest(req))\n}\n"]}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verdict on whether a request's claimed crawler identity holds up against the
|
|
3
|
+
* vendor's published IP ranges.
|
|
4
|
+
*
|
|
5
|
+
* - `'verified'` — the UA claims a vendor we can check, and the client IP is
|
|
6
|
+
* inside that vendor's published ranges. High confidence.
|
|
7
|
+
* - `'spoofed'` — the UA claims a vendor we can check, and the IP is **not**
|
|
8
|
+
* in its ranges. Someone is impersonating the crawler.
|
|
9
|
+
* - `'unverifiable'` — the UA claims a vendor that publishes no feed we bundle
|
|
10
|
+
* (Bytespider, Amazon, Meta, …), or no usable client IP was available.
|
|
11
|
+
* - `'not-claimed'` — the UA doesn't claim a verifiable crawler at all. This is
|
|
12
|
+
* the normal verdict for browsers and HTTP clients; it is *not* a negative
|
|
13
|
+
* finding.
|
|
14
|
+
*/
|
|
15
|
+
type VerificationVerdict = 'verified' | 'spoofed' | 'unverifiable' | 'not-claimed';
|
|
16
|
+
/** Why a request could not be judged. Only set when verdict is 'unverifiable'. */
|
|
17
|
+
type UnverifiableReason = 'no-published-ranges' | 'client-side-agent' | 'no-client-ip';
|
|
18
|
+
interface BotVerification {
|
|
19
|
+
verdict: VerificationVerdict;
|
|
20
|
+
reason?: UnverifiableReason;
|
|
21
|
+
/** Vendor label the UA claims, when it claims one. */
|
|
22
|
+
claimed: string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Convenience boolean for filtering: `true` only for `'verified'`, `false`
|
|
25
|
+
* only for `'spoofed'`, `null` when no judgement was possible. Deliberately
|
|
26
|
+
* tri-state — collapsing "unverifiable" into `false` would brand every
|
|
27
|
+
* Bytespider and Amazonbot hit an impostor.
|
|
28
|
+
*/
|
|
29
|
+
verified: boolean | null;
|
|
30
|
+
}
|
|
31
|
+
/** Vendor labels this build can produce a verified/spoofed verdict for. */
|
|
32
|
+
declare function verifiableVendors(): readonly string[];
|
|
33
|
+
/**
|
|
34
|
+
* Check a claimed crawler identity against the vendor's published IP ranges.
|
|
35
|
+
*
|
|
36
|
+
* Pass the client IP you already trust — on Vercel and Cloudflare that is the
|
|
37
|
+
* first hop of `x-forwarded-for`. If your edge doesn't strip client-supplied
|
|
38
|
+
* `X-Forwarded-For`, an attacker controls this value and a `'verified'` verdict
|
|
39
|
+
* means nothing; verify your proxy's behaviour before relying on it.
|
|
40
|
+
*/
|
|
41
|
+
declare function verifyBotIdentity(userAgent: string | null | undefined, ip: string | null | undefined): BotVerification;
|
|
42
|
+
/**
|
|
43
|
+
* Extract the client IP the way {@link trackVisit} does — first hop of
|
|
44
|
+
* `x-forwarded-for`, falling back to the platform-specific headers.
|
|
45
|
+
*/
|
|
46
|
+
declare function clientIpFromRequest(req: Request): string;
|
|
47
|
+
/** Verify straight from a request object. */
|
|
48
|
+
declare function verifyRequest(req: Request): BotVerification;
|
|
49
|
+
|
|
50
|
+
export { type BotVerification, type UnverifiableReason, type VerificationVerdict, clientIpFromRequest, verifiableVendors, verifyBotIdentity, verifyRequest };
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verdict on whether a request's claimed crawler identity holds up against the
|
|
3
|
+
* vendor's published IP ranges.
|
|
4
|
+
*
|
|
5
|
+
* - `'verified'` — the UA claims a vendor we can check, and the client IP is
|
|
6
|
+
* inside that vendor's published ranges. High confidence.
|
|
7
|
+
* - `'spoofed'` — the UA claims a vendor we can check, and the IP is **not**
|
|
8
|
+
* in its ranges. Someone is impersonating the crawler.
|
|
9
|
+
* - `'unverifiable'` — the UA claims a vendor that publishes no feed we bundle
|
|
10
|
+
* (Bytespider, Amazon, Meta, …), or no usable client IP was available.
|
|
11
|
+
* - `'not-claimed'` — the UA doesn't claim a verifiable crawler at all. This is
|
|
12
|
+
* the normal verdict for browsers and HTTP clients; it is *not* a negative
|
|
13
|
+
* finding.
|
|
14
|
+
*/
|
|
15
|
+
type VerificationVerdict = 'verified' | 'spoofed' | 'unverifiable' | 'not-claimed';
|
|
16
|
+
/** Why a request could not be judged. Only set when verdict is 'unverifiable'. */
|
|
17
|
+
type UnverifiableReason = 'no-published-ranges' | 'client-side-agent' | 'no-client-ip';
|
|
18
|
+
interface BotVerification {
|
|
19
|
+
verdict: VerificationVerdict;
|
|
20
|
+
reason?: UnverifiableReason;
|
|
21
|
+
/** Vendor label the UA claims, when it claims one. */
|
|
22
|
+
claimed: string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Convenience boolean for filtering: `true` only for `'verified'`, `false`
|
|
25
|
+
* only for `'spoofed'`, `null` when no judgement was possible. Deliberately
|
|
26
|
+
* tri-state — collapsing "unverifiable" into `false` would brand every
|
|
27
|
+
* Bytespider and Amazonbot hit an impostor.
|
|
28
|
+
*/
|
|
29
|
+
verified: boolean | null;
|
|
30
|
+
}
|
|
31
|
+
/** Vendor labels this build can produce a verified/spoofed verdict for. */
|
|
32
|
+
declare function verifiableVendors(): readonly string[];
|
|
33
|
+
/**
|
|
34
|
+
* Check a claimed crawler identity against the vendor's published IP ranges.
|
|
35
|
+
*
|
|
36
|
+
* Pass the client IP you already trust — on Vercel and Cloudflare that is the
|
|
37
|
+
* first hop of `x-forwarded-for`. If your edge doesn't strip client-supplied
|
|
38
|
+
* `X-Forwarded-For`, an attacker controls this value and a `'verified'` verdict
|
|
39
|
+
* means nothing; verify your proxy's behaviour before relying on it.
|
|
40
|
+
*/
|
|
41
|
+
declare function verifyBotIdentity(userAgent: string | null | undefined, ip: string | null | undefined): BotVerification;
|
|
42
|
+
/**
|
|
43
|
+
* Extract the client IP the way {@link trackVisit} does — first hop of
|
|
44
|
+
* `x-forwarded-for`, falling back to the platform-specific headers.
|
|
45
|
+
*/
|
|
46
|
+
declare function clientIpFromRequest(req: Request): string;
|
|
47
|
+
/** Verify straight from a request object. */
|
|
48
|
+
declare function verifyRequest(req: Request): BotVerification;
|
|
49
|
+
|
|
50
|
+
export { type BotVerification, type UnverifiableReason, type VerificationVerdict, clientIpFromRequest, verifiableVendors, verifyBotIdentity, verifyRequest };
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
function b(t){if(!t||typeof t!="string")return "Other";let e=t.toLowerCase();return e.includes("chatgpt-user")||e.includes("gptbot")||e.includes("oai-searchbot")||e.includes("openai")?"ChatGPT":e.includes("claudebot")||e.includes("claude-user")||e.includes("claude-searchbot")||e.includes("claude-web")||e.includes("anthropic")?"Claude":e.includes("perplexitybot")||e.includes("perplexity-user")?"Perplexity":e.includes("ccbot")?"Common Crawl":e.includes("google-extended")||e.includes("googlebot")||e.includes("google-cloudvertexbot")||e.includes("google-agent")||e.includes("googleagent-mariner")||e.includes("gemini-deep-research")?"Google":e.includes("applebot")?"Apple":e.includes("bingbot")?"Bing":e.includes("bytespider")?"Bytespider":e.includes("amazonbot")||e.includes("amzn-searchbot")||e.includes("novaact")?"Amazon":e.includes("meta-externalagent")||e.includes("meta-externalfetcher")||e.includes("meta-webindexer")||e.includes("facebookbot")?"Meta":e.includes("mistralai-user")?"Mistral":e.includes("duckassistbot")?"DuckDuckGo":e.includes("youbot")?"You.com":e.includes("diffbot")?"Diffbot":e.includes("ai2bot")?"AI2":e.includes("cohere")?"Cohere":e.includes("cursor")?"Cursor":e.includes("windsurf")?"Windsurf":e.includes("deepseek")?"DeepSeek":e.includes("pangubot")?"Huawei":e.includes("webzio")||e.includes("omgili")?"Webz.io":e.includes("timpibot")?"Timpi":e.includes("grok")||e.includes("xai-")?"xAI":e.includes("manus-user")?"Manus":e.includes("quillbot")?"QuillBot":e.includes("azureai-searchbot")?"Microsoft":e.includes("mycentralaiscraperbot")?"MyCentralAI":e.includes("petalbot")?"PetalBot":e.includes("ahrefsbot")?"Ahrefs":e.includes("semrushbot")?"Semrush":e.includes("mj12bot")?"Majestic":e.includes("dotbot")||e.includes("rogerbot")?"Moz":e.includes("screaming frog")?"Screaming Frog":e.includes("sitebulb")?"Sitebulb":e.includes("linkfluence")?"Linkfluence":e.includes("dataforseo")?"DataForSEO":e.includes("serpstatbot")?"Serpstat":e.includes("uptimerobot")?"UptimeRobot":e.includes("pingdom")?"Pingdom":e.includes("statuscake")?"StatusCake":e.includes("newrelicpinger")?"New Relic":e.includes("datadogagent")||e.includes("datadog")?"Datadog":e.includes("slackbot")?"Slack":e.includes("twitterbot")?"Twitter":e.includes("linkedinbot")?"LinkedIn":e.includes("discordbot")?"Discord":e.includes("telegrambot")?"Telegram":e.includes("whatsapp")?"WhatsApp":e.includes("linkupbot")?"Linkup":e.includes("sogou")?"Sogou":e.includes("yandexbot")?"Yandex":e.includes("baiduspider")?"Baidu":e.includes("facebookexternalhit")?"Facebook":e.includes("com.apple.webkit")?"Apple URL Preview":e.includes("ohdear")?"Oh Dear":e.includes("scrapy")?"Scrapy":e.includes("headlesschrome")?"Headless Chrome":e.includes("phantomjs")?"PhantomJS":e.includes("wget")?"wget":e.includes("httpie")?"HTTPie":e.includes("guzzlehttp")?"Guzzle":e.includes("electron/")?"Electron":/curl\//.test(e)?"curl":/axios\//.test(e)?"axios":/(?:^|[\s(])got(?:\/|[\s(])/.test(e)?"got":/\bcolly\b/.test(e)?"colly":/node-fetch\//.test(e)?"node-fetch":/python-requests\//.test(e)?"python-requests":/go-http-client\//.test(e)?"Go http client":/okhttp\//.test(e)?"OkHttp":/aiohttp\//.test(e)?"aiohttp":/deno\//.test(e)?"Deno":e.includes("mozilla")||e.includes("chrome")||e.includes("safari")||e.includes("firefox")?"Browser":"Other"}var d={ChatGPT:["104.208.184.192/28","104.208.184.208/28","104.210.139.192/28","104.210.139.224/28","104.210.140.128/28","128.85.198.32/28","13.65.138.112/28","13.65.138.96/28","13.67.72.16/28","13.70.107.160/28","13.71.2.208/28","13.76.115.224/28","13.76.115.240/28","13.76.116.80/28","13.76.32.208/28","13.83.167.128/28","13.83.237.176/28","132.196.82.48/28","132.196.86.0/24","134.149.233.80/28","135.116.136.160/28","135.13.64.240/28","135.220.73.208/28","135.220.73.240/28","135.234.64.0/24","135.237.131.208/28","135.237.133.48/28","137.135.191.176/28","138.91.30.48/28","138.91.46.96/28","145.132.1.32/28","145.132.136.96/28","145.133.0.176/28","158.158.5.32/28","168.63.252.240/28","172.162.248.64/28","172.170.1.80/28","172.170.225.0/28","172.170.241.80/28","172.170.8.208/28","172.171.4.176/28","172.175.152.224/28","172.178.140.144/28","172.178.141.112/28","172.178.141.128/28","172.182.193.224/28","172.182.193.80/28","172.182.194.144/28","172.182.194.32/28","172.182.195.48/28","172.182.202.0/25","172.182.204.0/24","172.182.207.0/25","172.182.209.208/28","172.182.211.192/28","172.182.213.192/28","172.182.214.0/24","172.182.215.0/24","172.182.224.0/28","172.183.143.224/28","172.183.222.128/28","172.192.112.208/28","172.192.88.192/28","172.192.97.32/28","172.197.160.192/28","172.197.161.208/28","172.197.170.80/28","172.197.203.16/28","172.199.137.80/28","172.202.102.112/28","172.203.190.128/28","172.204.27.16/28","172.204.28.224/28","172.204.96.32/28","172.204.96.48/28","172.204.96.80/28","172.205.189.192/28","172.207.1.32/28","172.208.128.32/28","172.208.128.48/28","172.212.159.64/28","172.212.172.160/28","172.213.21.16/28","172.215.215.32/28","172.215.218.96/28","191.233.1.112/28","191.233.1.128/28","191.233.194.32/28","191.233.196.112/28","191.233.199.160/28","191.233.2.0/28","191.235.66.16/28","191.235.99.80/28","191.237.249.64/28","191.239.245.16/28","20.102.212.144/28","20.113.211.112/28","20.113.225.112/28","20.125.112.224/28","20.125.144.144/28","20.125.66.80/28","20.14.99.96/28","20.161.75.208/28","20.168.18.32/28","20.168.7.192/28","20.168.7.240/28","20.169.6.224/28","20.169.7.48/28","20.169.72.112/28","20.169.73.176/28","20.169.73.32/28","20.169.73.64/28","20.169.77.0/25","20.169.78.112/28","20.169.78.128/28","20.169.78.144/28","20.169.78.160/28","20.169.78.176/28","20.169.78.192/28","20.169.78.208/28","20.169.78.48/28","20.169.78.64/28","20.169.78.80/28","20.169.78.96/28","20.169.86.224/28","20.169.86.240/28","20.169.87.112/28","20.17.108.96/28","20.170.184.16/28","20.170.184.32/28","20.170.184.48/28","20.170.184.64/28","20.170.184.80/28","20.171.123.64/28","20.171.206.0/24","20.171.207.0/24","20.171.53.224/28","20.172.29.32/28","20.193.233.240/28","20.193.50.32/28","20.194.0.208/28","20.194.1.0/28","20.198.67.96/28","20.199.211.160/28","20.199.242.0/28","20.200.212.240/28","20.204.24.240/28","20.210.154.128/28","20.210.174.208/28","20.210.211.192/28","20.215.187.208/28","20.215.188.192/28","20.215.214.16/28","20.215.219.128/28","20.215.219.160/28","20.215.219.208/28","20.215.220.112/28","20.215.220.128/28","20.215.220.144/28","20.215.220.160/28","20.215.220.176/28","20.215.220.192/28","20.215.220.208/28","20.215.220.64/28","20.215.220.80/28","20.215.220.96/28","20.218.30.240/28","20.219.71.192/28","20.222.36.192/28","20.226.32.80/28","20.227.140.32/28","20.228.106.176/28","20.235.75.208/28","20.235.87.224/28","20.249.63.208/28","20.25.151.224/28","20.250.136.64/28","20.250.136.80/28","20.250.6.128/28","20.27.94.128/28","20.42.10.176/28","20.45.178.144/28","20.48.120.208/28","20.52.125.160/28","20.55.129.0/28","20.55.229.144/28","20.57.199.192/28","20.63.180.96/28","20.63.221.64/28","20.79.59.112/28","20.81.183.64/28","20.83.243.176/28","20.97.189.96/28","23.102.140.144/28","23.102.141.32/28","23.98.142.176/28","23.98.179.16/28","23.98.186.176/28","23.98.186.192/28","23.98.186.64/28","23.98.186.96/28","4.151.119.48/28","4.151.241.240/28","4.151.71.176/28","4.189.118.208/28","4.189.119.48/28","4.196.118.112/28","4.197.115.112/28","4.197.19.176/28","4.197.22.112/28","4.197.64.0/28","4.197.64.16/28","4.197.64.48/28","4.197.64.64/28","4.198.72.16/28","4.198.96.112/28","4.201.232.64/28","4.201.232.80/28","4.203.96.80/28","4.205.128.176/28","4.218.24.64/28","4.226.200.16/28","4.226.226.32/28","4.227.36.0/25","40.116.73.208/28","40.122.235.112/28","40.67.175.0/25","40.67.183.160/28","40.67.183.176/28","40.78.161.48/28","40.81.134.128/28","40.81.134.144/28","40.81.234.144/28","40.81.67.96/28","40.84.181.32/28","40.84.221.208/28","40.84.221.224/28","40.90.214.16/28","48.193.44.32/28","48.221.184.112/28","48.221.184.80/28","48.221.184.96/28","48.221.40.176/28","51.107.70.192/28","51.116.2.80/28","51.116.221.96/28","51.56.40.80/28","51.57.0.96/28","51.59.24.64/28","51.59.24.80/28","51.59.40.80/28","51.59.40.96/28","51.59.48.80/28","51.59.48.96/28","51.8.102.0/24","51.8.155.112/28","51.8.155.48/28","51.8.155.64/28","51.8.187.224/28","52.148.129.32/28","52.153.130.64/28","52.154.22.48/28","52.156.77.144/28","52.159.227.32/28","52.159.249.96/28","52.161.49.224/28","52.161.49.96/28","52.165.212.16/28","52.165.212.32/28","52.165.212.48/28","52.172.129.160/28","52.172.251.112/28","52.173.219.112/28","52.173.219.96/28","52.173.221.16/28","52.173.221.176/28","52.173.221.208/28","52.173.234.16/28","52.173.234.80/28","52.173.235.80/28","52.183.217.240/28","52.187.246.128/28","52.190.137.144/28","52.190.137.16/28","52.190.139.48/28","52.190.142.64/28","52.190.190.16/28","52.225.75.208/28","52.230.152.0/24","52.230.163.32/28","52.230.164.176/28","52.231.30.48/28","52.231.34.176/28","52.231.39.144/28","52.231.39.192/28","52.231.49.48/28","52.231.50.64/28","52.236.94.144/28","52.241.146.208/28","52.242.132.224/28","52.242.132.240/28","52.242.245.208/28","52.252.113.240/28","52.255.109.112/28","52.255.109.128/28","52.255.109.144/28","52.255.109.80/28","52.255.109.96/28","52.255.111.0/28","52.255.111.112/28","52.255.111.32/28","52.255.111.48/28","52.255.111.80/28","57.154.174.112/28","57.154.175.0/28","57.154.187.32/28","68.154.28.96/28","68.218.30.112/28","68.220.57.64/28","68.221.67.192/28","68.221.67.224/28","68.221.67.240/28","70.153.139.208/28","70.153.189.192/28","70.153.190.16/28","70.153.76.16/28","70.153.87.224/28","70.156.144.64/28","70.156.152.80/28","70.156.152.96/28","74.161.200.96/28","74.224.217.64/28","74.226.253.160/28","74.249.86.176/28","74.7.175.128/25","74.7.227.0/25","74.7.227.128/25","74.7.228.0/25","74.7.228.128/25","74.7.229.0/25","74.7.229.128/25","74.7.230.0/25","74.7.241.0/25","74.7.241.128/25","74.7.242.0/25","74.7.242.128/25","74.7.243.0/25","74.7.243.128/25","74.7.244.0/25","74.7.35.112/28","74.7.35.48/28","74.7.36.64/28","74.7.36.80/28","74.7.36.96/28","85.211.241.128/28","9.129.0.0/17","9.160.128.16/28","9.160.128.32/28","9.160.128.64/28","9.160.163.128/28","9.160.164.128/28","9.160.34.144/28","9.160.36.16/28","9.160.96.16/28","9.163.101.48/28","9.205.25.128/28","9.205.8.48/28","9.223.181.208/28","9.234.96.192/28","9.234.97.128/28","9.234.97.96/28","9.235.40.32/28"],Claude:["136.107.176.208/32","216.73.216.0/22","34.11.34.31/32","34.150.241.79/32","34.162.191.81/32","34.162.230.222/32","34.162.244.71/32","34.182.140.95/32","34.182.161.143/32","34.182.218.27/32","34.182.220.85/32","34.182.222.37/32","34.182.225.167/32","34.182.226.151/32","34.182.226.221/32","34.186.108.163/32","34.85.172.162/32","35.221.29.174/32","35.245.175.129/32","35.245.89.239/32"],Perplexity:["107.20.236.150/32","18.210.92.235/32","18.97.1.228/30","18.97.9.96/29","3.211.124.183/32","3.222.232.239/32","3.224.62.45/32","3.231.139.107/32"],Apple:["17.22.237.0/24","17.22.245.0/24","17.22.253.0/24","17.241.193.160/27","17.241.200.160/27","17.241.208.160/27","17.241.219.0/24","17.241.227.0/24","17.241.75.0/24","17.246.15.0/24","17.246.19.0/24","17.246.23.0/24"]},h=Object.keys(d);function p(t){let e=t.split(".");if(e.length!==4)return null;let i=0;for(let n of e){if(!/^\d{1,3}$/.test(n)||n.length>1&&n[0]==="0")return null;let o=Number(n);if(o>255)return null;i=i<<8|o;}return i>>>0}function x(t){let e=t,i=e.indexOf("%");if(i!==-1&&(e=e.slice(0,i)),!e||e.indexOf(":")===-1)return null;let n=null,o=e.lastIndexOf(":"),l=e.slice(o+1);if(l.indexOf(".")!==-1){if(n=p(l),n===null)return null;e=e.slice(0,o+1)+"0:0";}let r=e.split("::");if(r.length>2)return null;let s=r[0]?r[0].split(":"):[],c=r.length===2?r[1]?r[1].split(":"):[]:null,f;if(c===null){if(f=s,f.length!==8)return null}else {let a=8-s.length-c.length;if(a<0)return null;f=[...s,...Array(a).fill("0"),...c];}let u=0n;for(let a of f){if(!/^[0-9a-fA-F]{1,4}$/.test(a))return null;u=u<<16n|BigInt(parseInt(a,16));}return n!==null&&(u=u>>32n<<32n|BigInt(n>>>0)),u}function B(t){let e=new Map,i=[],n=[];for(let o of t){let l=o.lastIndexOf("/");if(l===-1)continue;let r=o.slice(0,l),s=Number(o.slice(l+1));if(!(!Number.isInteger(s)||s<0))if(r.indexOf(":")!==-1){if(s>128)continue;let c=x(r);if(c===null)continue;n.push({net:s===0?0n:c>>BigInt(128-s)<<BigInt(128-s),bits:s});}else {if(s>32)continue;let c=p(r);if(c===null)continue;let f=s===0?0:4294967295<<32-s>>>0,u={net:(c&f)>>>0,mask:f};if(s>=8){let a=u.net>>>24,g=e.get(a);g?g.push(u):e.set(a,[u]);}else i.push(u);}}return {v4:e,v4Wide:i,v6:n}}function R(t,e){if(!t)return false;let i=t.trim();if(!i)return false;if(i.indexOf(":")!==-1){let o=x(i);if(o===null)return false;let l=0xffffn<<32n;if(o>>32n===l>>32n){let r=Number(o&0xffffffffn)>>>0;if(m(r,e))return true}for(let r of e.v6)if(r.bits===0||o>>BigInt(128-r.bits)<<BigInt(128-r.bits)===r.net)return true;return false}let n=p(i);return n===null?false:m(n,e)}function m(t,e){let i=e.v4.get(t>>>24);if(i){for(let n of i)if((t&n.mask)>>>0===n.net)return true}for(let n of e.v4Wide)if((t&n.mask)>>>0===n.net)return true;return false}var A=/claude-code|perplexity-user/i,C=/ClaudeBot|Claude-SearchBot|GPTBot|OAI-SearchBot|ChatGPT-User|PerplexityBot|Applebot/i,y=new Map;function k(t){if(!(t in d))return;let e=y.get(t);return e||(e=B(d[t]??[]),y.set(t,e)),e}function V(){return h}function I(t,e){let i=t??"",n=b(t),o=k(n);if(!o){let s=n!=="Other"&&n!=="Browser";return {verdict:s?"unverifiable":"not-claimed",...s?{reason:"no-published-ranges"}:{},claimed:s?n:null,verified:null}}if(A.test(i)||!C.test(i))return {verdict:"unverifiable",reason:"client-side-agent",claimed:n,verified:null};let l=(e??"").trim();if(!l)return {verdict:"unverifiable",reason:"no-client-ip",claimed:n,verified:null};let r=R(l,o);return {verdict:r?"verified":"spoofed",claimed:n,verified:r}}function w(t){let i=(t.headers.get("x-forwarded-for")||"").split(",")[0]?.trim();return i||(t.headers.get("cf-connecting-ip")||t.headers.get("x-real-ip")||"").trim()}function D(t){return I(t.headers.get("user-agent"),w(t))}
|
|
2
|
+
export{w as clientIpFromRequest,V as verifiableVendors,I as verifyBotIdentity,D as verifyRequest};//# sourceMappingURL=verify.js.map
|
|
3
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bots.ts","../src/bot-ranges.ts","../src/cidr.ts","../src/verify.ts"],"names":["parseBotName","userAgent","s","BOT_IP_RANGES","VERIFIABLE_VENDORS","ipv4ToInt","ip","parts","out","part","n","ipv6ToBigInt","text","zone","tail","lastColon","maybeV4","halves","head","rest","groups","fill","g","compileRanges","cidrs","v4","v4Wide","v6","cidr","slash","addr","bits","net","mask","entry","bucket","list","ipInRanges","ranges","trimmed","value","V4_MAPPED_PREFIX","low","matchV4","r","CLIENT_SIDE_AGENT_PATTERN","SERVER_SIDE_CRAWLER_PATTERN","COMPILED","rangesFor","vendor","c","verifiableVendors","verifyBotIdentity","ua","claimed","isKnownCrawler","inRange","clientIpFromRequest","req","first","verifyRequest"],"mappings":"AA8DO,SAASA,CAAAA,CAAaC,EAA8C,CACzE,GAAI,CAACA,CAAAA,EAAa,OAAOA,CAAAA,EAAc,QAAA,CAAU,OAAO,OAAA,CACxD,IAAMC,CAAAA,CAAID,CAAAA,CAAU,aAAY,CAGhC,OAAIC,EAAE,QAAA,CAAS,cAAc,CAAA,EAAKA,CAAAA,CAAE,QAAA,CAAS,QAAQ,GAAKA,CAAAA,CAAE,QAAA,CAAS,eAAe,CAAA,EAAKA,CAAAA,CAAE,SAAS,QAAQ,CAAA,CACnG,SAAA,CAEPA,CAAAA,CAAE,QAAA,CAAS,WAAW,GACtBA,CAAAA,CAAE,QAAA,CAAS,aAAa,CAAA,EACxBA,CAAAA,CAAE,SAAS,kBAAkB,CAAA,EAC7BA,CAAAA,CAAE,QAAA,CAAS,YAAY,CAAA,EACvBA,EAAE,QAAA,CAAS,WAAW,EAEf,QAAA,CACLA,CAAAA,CAAE,SAAS,eAAe,CAAA,EAAKA,CAAAA,CAAE,QAAA,CAAS,iBAAiB,CAAA,CAAU,aACrEA,CAAAA,CAAE,QAAA,CAAS,OAAO,CAAA,CAAU,cAAA,CAE9BA,EAAE,QAAA,CAAS,iBAAiB,CAAA,EAC5BA,CAAAA,CAAE,QAAA,CAAS,WAAW,GACtBA,CAAAA,CAAE,QAAA,CAAS,uBAAuB,CAAA,EAClCA,CAAAA,CAAE,SAAS,cAAc,CAAA,EACzBA,CAAAA,CAAE,QAAA,CAAS,qBAAqB,CAAA,EAChCA,EAAE,QAAA,CAAS,sBAAsB,CAAA,CAE1B,QAAA,CACLA,CAAAA,CAAE,QAAA,CAAS,UAAU,CAAA,CAAU,OAAA,CAC/BA,CAAAA,CAAE,QAAA,CAAS,SAAS,CAAA,CAAU,OAC9BA,CAAAA,CAAE,QAAA,CAAS,YAAY,CAAA,CAAU,YAAA,CACjCA,EAAE,QAAA,CAAS,WAAW,CAAA,EAAKA,CAAAA,CAAE,QAAA,CAAS,gBAAgB,GAAKA,CAAAA,CAAE,QAAA,CAAS,SAAS,CAAA,CAAU,QAAA,CAE3FA,EAAE,QAAA,CAAS,oBAAoB,CAAA,EAC/BA,CAAAA,CAAE,QAAA,CAAS,sBAAsB,GACjCA,CAAAA,CAAE,QAAA,CAAS,iBAAiB,CAAA,EAC5BA,CAAAA,CAAE,SAAS,aAAa,CAAA,CAEjB,MAAA,CACLA,CAAAA,CAAE,QAAA,CAAS,gBAAgB,EAAU,SAAA,CACrCA,CAAAA,CAAE,SAAS,eAAe,CAAA,CAAU,aACpCA,CAAAA,CAAE,QAAA,CAAS,QAAQ,CAAA,CAAU,SAAA,CAC7BA,CAAAA,CAAE,SAAS,SAAS,CAAA,CAAU,UAC9BA,CAAAA,CAAE,QAAA,CAAS,QAAQ,CAAA,CAAU,KAAA,CAC7BA,CAAAA,CAAE,QAAA,CAAS,QAAQ,CAAA,CAAU,SAC7BA,CAAAA,CAAE,QAAA,CAAS,QAAQ,CAAA,CAAU,QAAA,CAC7BA,EAAE,QAAA,CAAS,UAAU,CAAA,CAAU,UAAA,CAC/BA,CAAAA,CAAE,QAAA,CAAS,UAAU,CAAA,CAAU,UAAA,CAC/BA,EAAE,QAAA,CAAS,UAAU,EAAU,QAAA,CAC/BA,CAAAA,CAAE,QAAA,CAAS,QAAQ,CAAA,EAAKA,CAAAA,CAAE,SAAS,QAAQ,CAAA,CAAU,SAAA,CACrDA,CAAAA,CAAE,QAAA,CAAS,UAAU,EAAU,OAAA,CAC/BA,CAAAA,CAAE,QAAA,CAAS,MAAM,CAAA,EAAKA,CAAAA,CAAE,SAAS,MAAM,CAAA,CAAU,MACjDA,CAAAA,CAAE,QAAA,CAAS,YAAY,CAAA,CAAU,OAAA,CACjCA,CAAAA,CAAE,QAAA,CAAS,UAAU,CAAA,CAAU,WAC/BA,CAAAA,CAAE,QAAA,CAAS,mBAAmB,CAAA,CAAU,WAAA,CACxCA,EAAE,QAAA,CAAS,uBAAuB,CAAA,CAAU,aAAA,CAC5CA,CAAAA,CAAE,QAAA,CAAS,UAAU,CAAA,CAAU,UAAA,CAG/BA,EAAE,QAAA,CAAS,WAAW,EAAU,QAAA,CAChCA,CAAAA,CAAE,QAAA,CAAS,YAAY,CAAA,CAAU,SAAA,CACjCA,EAAE,QAAA,CAAS,SAAS,EAAU,UAAA,CAC9BA,CAAAA,CAAE,SAAS,QAAQ,CAAA,EACnBA,CAAAA,CAAE,QAAA,CAAS,UAAU,CAAA,CAAU,MAC/BA,CAAAA,CAAE,QAAA,CAAS,gBAAgB,CAAA,CAAU,gBAAA,CACrCA,EAAE,QAAA,CAAS,UAAU,CAAA,CAAU,UAAA,CAC/BA,CAAAA,CAAE,QAAA,CAAS,aAAa,CAAA,CAAU,aAAA,CAClCA,EAAE,QAAA,CAAS,YAAY,EAAU,YAAA,CACjCA,CAAAA,CAAE,QAAA,CAAS,aAAa,CAAA,CAAU,UAAA,CAGlCA,EAAE,QAAA,CAAS,aAAa,EAAU,aAAA,CAClCA,CAAAA,CAAE,SAAS,SAAS,CAAA,CAAU,SAAA,CAC9BA,CAAAA,CAAE,QAAA,CAAS,YAAY,EAAU,YAAA,CACjCA,CAAAA,CAAE,QAAA,CAAS,gBAAgB,CAAA,CAAU,WAAA,CACrCA,EAAE,QAAA,CAAS,cAAc,CAAA,EAAKA,CAAAA,CAAE,QAAA,CAAS,SAAS,EAAU,SAAA,CAC5DA,CAAAA,CAAE,SAAS,UAAU,CAAA,CAAU,QAC/BA,CAAAA,CAAE,QAAA,CAAS,YAAY,CAAA,CAAU,SAAA,CACjCA,CAAAA,CAAE,SAAS,aAAa,CAAA,CAAU,WAClCA,CAAAA,CAAE,QAAA,CAAS,YAAY,CAAA,CAAU,SAAA,CACjCA,CAAAA,CAAE,QAAA,CAAS,aAAa,CAAA,CAAU,WAClCA,CAAAA,CAAE,QAAA,CAAS,UAAU,CAAA,CAAU,UAAA,CAG/BA,EAAE,QAAA,CAAS,WAAW,CAAA,CAAU,QAAA,CAChCA,CAAAA,CAAE,QAAA,CAAS,OAAO,CAAA,CAAU,OAAA,CAC5BA,EAAE,QAAA,CAAS,WAAW,EAAU,QAAA,CAChCA,CAAAA,CAAE,QAAA,CAAS,aAAa,CAAA,CAAU,OAAA,CAGlCA,EAAE,QAAA,CAAS,qBAAqB,EAAU,UAAA,CAC1CA,CAAAA,CAAE,SAAS,kBAAkB,CAAA,CAAU,mBAAA,CAGvCA,CAAAA,CAAE,QAAA,CAAS,QAAQ,EAAU,SAAA,CAG7BA,CAAAA,CAAE,SAAS,QAAQ,CAAA,CAAU,SAC7BA,CAAAA,CAAE,QAAA,CAAS,gBAAgB,CAAA,CAAU,iBAAA,CACrCA,CAAAA,CAAE,SAAS,WAAW,CAAA,CAAU,YAChCA,CAAAA,CAAE,QAAA,CAAS,MAAM,CAAA,CAAU,MAAA,CAC3BA,CAAAA,CAAE,QAAA,CAAS,QAAQ,CAAA,CAAU,SAC7BA,CAAAA,CAAE,QAAA,CAAS,YAAY,CAAA,CAAU,QAAA,CAIjCA,CAAAA,CAAE,SAAS,WAAW,CAAA,CAAU,UAAA,CAChC,QAAA,CAAS,IAAA,CAAKA,CAAC,EAAU,MAAA,CACzB,SAAA,CAAU,KAAKA,CAAC,CAAA,CAAU,QAC1B,4BAAA,CAA6B,IAAA,CAAKA,CAAC,CAAA,CAAU,KAAA,CAC7C,WAAA,CAAY,KAAKA,CAAC,CAAA,CAAU,QAC5B,cAAA,CAAe,IAAA,CAAKA,CAAC,CAAA,CAAU,YAAA,CAC/B,mBAAA,CAAoB,IAAA,CAAKA,CAAC,CAAA,CAAU,kBACpC,kBAAA,CAAmB,IAAA,CAAKA,CAAC,CAAA,CAAU,gBAAA,CACnC,WAAW,IAAA,CAAKA,CAAC,CAAA,CAAU,QAAA,CAC3B,WAAA,CAAY,IAAA,CAAKA,CAAC,CAAA,CAAU,SAAA,CAC5B,SAAS,IAAA,CAAKA,CAAC,EAAU,MAAA,CAGzBA,CAAAA,CAAE,QAAA,CAAS,SAAS,CAAA,EAAKA,CAAAA,CAAE,SAAS,QAAQ,CAAA,EAAKA,EAAE,QAAA,CAAS,QAAQ,GAAKA,CAAAA,CAAE,QAAA,CAAS,SAAS,CAAA,CACxF,SAAA,CAEF,OACT,CC/JO,IAAMC,CAAAA,CAA6D,CACxE,OAAA,CAAS,CACP,qBACA,oBAAA,CACA,oBAAA,CACA,oBAAA,CACA,oBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBACA,gBAAA,CACA,kBAAA,CACA,iBACA,kBAAA,CACA,kBAAA,CACA,kBACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,mBAAA,CACA,qBACA,kBAAA,CACA,mBAAA,CACA,mBAAA,CACA,iBAAA,CACA,oBAAA,CACA,mBAAA,CACA,qBACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,iBAAA,CACA,oBACA,mBAAA,CACA,iBAAA,CACA,mBACA,mBAAA,CACA,kBAAA,CACA,mBACA,oBAAA,CACA,oBAAA,CACA,oBAAA,CACA,oBAAA,CACA,oBAAA,CACA,mBAAA,CACA,qBACA,mBAAA,CACA,mBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,qBACA,oBAAA,CACA,oBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,oBAAA,CACA,qBACA,oBAAA,CACA,mBAAA,CACA,mBACA,oBAAA,CACA,oBAAA,CACA,oBACA,mBAAA,CACA,mBAAA,CACA,oBAAA,CACA,oBAAA,CACA,kBAAA,CACA,mBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,qBACA,iBAAA,CACA,mBAAA,CACA,oBACA,mBAAA,CACA,oBAAA,CACA,kBAAA,CACA,mBAAA,CACA,mBAAA,CACA,kBAAA,CACA,mBACA,mBAAA,CACA,oBAAA,CACA,qBACA,gBAAA,CACA,kBAAA,CACA,mBACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,oBACA,iBAAA,CACA,gBAAA,CACA,mBACA,iBAAA,CACA,iBAAA,CACA,kBACA,iBAAA,CACA,gBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBACA,kBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,kBAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBACA,iBAAA,CACA,eAAA,CACA,kBACA,mBAAA,CACA,iBAAA,CACA,oBACA,kBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBACA,mBAAA,CACA,mBAAA,CACA,oBACA,mBAAA,CACA,mBAAA,CACA,oBACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,mBACA,iBAAA,CACA,kBAAA,CACA,oBACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBACA,iBAAA,CACA,iBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,iBACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,mBACA,iBAAA,CACA,mBAAA,CACA,mBACA,kBAAA,CACA,iBAAA,CACA,mBACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,kBAAA,CACA,iBAAA,CACA,mBACA,iBAAA,CACA,kBAAA,CACA,mBACA,iBAAA,CACA,iBAAA,CACA,gBACA,gBAAA,CACA,gBAAA,CACA,gBAAA,CACA,gBAAA,CACA,iBAAA,CACA,iBAAA,CACA,kBACA,gBAAA,CACA,kBAAA,CACA,iBACA,iBAAA,CACA,iBAAA,CACA,gBACA,kBAAA,CACA,mBAAA,CACA,gBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,gBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBACA,kBAAA,CACA,gBAAA,CACA,gBACA,gBAAA,CACA,gBAAA,CACA,iBACA,gBAAA,CACA,gBAAA,CACA,gBAAA,CACA,eAAA,CACA,iBAAA,CACA,gBAAA,CACA,iBACA,iBAAA,CACA,kBAAA,CACA,mBACA,iBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,oBACA,mBAAA,CACA,mBAAA,CACA,mBACA,kBAAA,CACA,mBAAA,CACA,oBACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,iBAAA,CACA,mBACA,mBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,kBACA,kBAAA,CACA,mBAAA,CACA,oBACA,mBAAA,CACA,mBAAA,CACA,oBACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,oBACA,kBAAA,CACA,kBAAA,CACA,mBACA,mBAAA,CACA,iBAAA,CACA,mBACA,iBAAA,CACA,kBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,oBACA,mBAAA,CACA,kBAAA,CACA,kBACA,kBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBAAA,CACA,kBAAA,CACA,iBAAA,CACA,gBACA,iBAAA,CACA,eAAA,CACA,iBAAA,CACA,eAAA,CACA,iBAAA,CACA,eAAA,CACA,gBACA,iBAAA,CACA,eAAA,CACA,iBAAA,CACA,eAAA,CACA,iBAAA,CACA,eAAA,CACA,iBACA,eAAA,CACA,eAAA,CACA,gBACA,eAAA,CACA,mBAAA,CACA,eACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,iBACA,gBAAA,CACA,iBAAA,CACA,kBACA,eAAA,CACA,kBAAA,CACA,kBACA,iBAAA,CACA,gBAAA,CACA,gBACF,CAAA,CACA,MAAA,CAAQ,CACN,qBACA,iBAAA,CACA,gBAAA,CACA,mBACA,kBAAA,CACA,mBAAA,CACA,mBACA,kBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBAAA,CACA,oBACA,mBAAA,CACA,mBAAA,CACA,mBACA,kBAAA,CACA,mBAAA,CACA,kBACF,CAAA,CACA,UAAA,CAAY,CACV,mBAAA,CACA,kBAAA,CACA,gBAAA,CACA,gBACA,kBAAA,CACA,kBAAA,CACA,iBACA,kBACF,CAAA,CACA,MAAO,CACL,gBAAA,CACA,gBAAA,CACA,gBAAA,CACA,mBAAA,CACA,mBAAA,CACA,oBACA,iBAAA,CACA,iBAAA,CACA,iBACA,gBAAA,CACA,gBAAA,CACA,gBACF,CACF,CAAA,CAGaC,CAAAA,CAAwC,MAAA,CAAO,IAAA,CAAKD,CAAa,ECnbvE,SAASE,CAAAA,CAAUC,EAA2B,CACnD,IAAMC,EAAQD,CAAAA,CAAG,KAAA,CAAM,GAAG,CAAA,CAC1B,GAAIC,CAAAA,CAAM,SAAW,CAAA,CAAG,OAAO,IAAA,CAC/B,IAAIC,CAAAA,CAAM,CAAA,CACV,QAAWC,CAAAA,IAAQF,CAAAA,CAAO,CAIxB,GADI,CAAC,WAAA,CAAY,KAAKE,CAAI,CAAA,EACtBA,EAAK,MAAA,CAAS,CAAA,EAAKA,EAAK,CAAC,CAAA,GAAM,GAAA,CAAK,OAAO,IAAA,CAC/C,IAAMC,EAAI,MAAA,CAAOD,CAAI,EACrB,GAAIC,CAAAA,CAAI,IAAK,OAAO,IAAA,CACpBF,CAAAA,CAAOA,CAAAA,EAAO,CAAA,CAAKE,EACrB,CAEA,OAAOF,CAAAA,GAAQ,CACjB,CAMO,SAASG,EAAaL,CAAAA,CAA2B,CACtD,IAAIM,CAAAA,CAAON,CAAAA,CAELO,CAAAA,CAAOD,EAAK,OAAA,CAAQ,GAAG,EAE7B,GADIC,CAAAA,GAAS,KAAID,CAAAA,CAAOA,CAAAA,CAAK,KAAA,CAAM,CAAA,CAAGC,CAAI,CAAA,CAAA,CACtC,CAACD,CAAAA,EAAQA,CAAAA,CAAK,QAAQ,GAAG,CAAA,GAAM,GAAI,OAAO,IAAA,CAG9C,IAAIE,CAAAA,CAAsB,IAAA,CACpBC,CAAAA,CAAYH,EAAK,WAAA,CAAY,GAAG,EAChCI,CAAAA,CAAUJ,CAAAA,CAAK,MAAMG,CAAAA,CAAY,CAAC,CAAA,CACxC,GAAIC,CAAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA,GAAM,EAAA,CAAI,CAE/B,GADAF,CAAAA,CAAOT,EAAUW,CAAO,CAAA,CACpBF,CAAAA,GAAS,IAAA,CAAM,OAAO,IAAA,CAC1BF,EAAOA,CAAAA,CAAK,KAAA,CAAM,CAAA,CAAGG,CAAAA,CAAY,CAAC,CAAA,CAAI,MACxC,CAEA,IAAME,CAAAA,CAASL,CAAAA,CAAK,KAAA,CAAM,IAAI,EAC9B,GAAIK,CAAAA,CAAO,OAAS,CAAA,CAAG,OAAO,KAC9B,IAAMC,CAAAA,CAAOD,CAAAA,CAAO,CAAC,CAAA,CAAIA,CAAAA,CAAO,CAAC,CAAA,CAAE,KAAA,CAAM,GAAG,CAAA,CAAI,GAC1CE,CAAAA,CAAOF,CAAAA,CAAO,MAAA,GAAW,CAAA,CAAKA,CAAAA,CAAO,CAAC,EAAIA,CAAAA,CAAO,CAAC,EAAE,KAAA,CAAM,GAAG,EAAI,EAAC,CAAK,IAAA,CAEzEG,CAAAA,CACJ,GAAID,CAAAA,GAAS,MAEX,GADAC,CAAAA,CAASF,EACLE,CAAAA,CAAO,MAAA,GAAW,EAAG,OAAO,IAAA,CAAA,KAC3B,CACL,IAAMC,CAAAA,CAAO,CAAA,CAAIH,EAAK,MAAA,CAASC,CAAAA,CAAK,OACpC,GAAIE,CAAAA,CAAO,EAAG,OAAO,IAAA,CACrBD,CAAAA,CAAS,CAAC,GAAGF,CAAAA,CAAM,GAAG,KAAA,CAAMG,CAAI,EAAE,IAAA,CAAK,GAAG,EAAG,GAAGF,CAAI,EACtD,CAEA,IAAIX,CAAAA,CAAM,GACV,IAAA,IAAWc,CAAAA,IAAKF,EAAQ,CACtB,GAAI,CAAC,oBAAA,CAAqB,IAAA,CAAKE,CAAC,CAAA,CAAG,OAAO,IAAA,CAC1Cd,EAAOA,CAAAA,EAAO,GAAA,CAAO,MAAA,CAAO,QAAA,CAASc,CAAAA,CAAG,EAAE,CAAC,EAC7C,CAEA,OAAIR,CAAAA,GAAS,IAAA,GAAMN,CAAAA,CAAQA,GAAO,GAAA,EAAQ,GAAA,CAAO,OAAOM,CAAAA,GAAS,CAAC,GAC3DN,CACT,CA6BO,SAASe,CAAAA,CAAcC,CAAAA,CAA0C,CACtE,IAAMC,CAAAA,CAAK,IAAI,IACTC,CAAAA,CAAoB,GACpBC,CAAAA,CAAgB,EAAC,CACvB,IAAA,IAAWC,CAAAA,IAAQJ,CAAAA,CAAO,CACxB,IAAMK,CAAAA,CAAQD,EAAK,WAAA,CAAY,GAAG,EAClC,GAAIC,CAAAA,GAAU,EAAA,CAAI,SAClB,IAAMC,CAAAA,CAAOF,EAAK,KAAA,CAAM,CAAA,CAAGC,CAAK,CAAA,CAC1BE,CAAAA,CAAO,OAAOH,CAAAA,CAAK,KAAA,CAAMC,CAAAA,CAAQ,CAAC,CAAC,CAAA,CACzC,GAAI,EAAA,CAAC,MAAA,CAAO,UAAUE,CAAI,CAAA,EAAKA,EAAO,CAAA,CAAA,CAEtC,GAAID,CAAAA,CAAK,OAAA,CAAQ,GAAG,CAAA,GAAM,GAAI,CAC5B,GAAIC,EAAO,GAAA,CAAK,SAChB,IAAMC,CAAAA,CAAMrB,CAAAA,CAAamB,CAAI,CAAA,CAC7B,GAAIE,CAAAA,GAAQ,KAAM,SAClBL,CAAAA,CAAG,KAAK,CAAE,GAAA,CAAKI,IAAS,CAAA,CAAI,EAAA,CAAMC,CAAAA,EAAO,MAAA,CAAO,GAAA,CAAMD,CAAI,GAAM,MAAA,CAAO,GAAA,CAAMA,CAAI,CAAA,CAAG,IAAA,CAAAA,CAAK,CAAC,EAC5F,CAAA,KAAO,CACL,GAAIA,CAAAA,CAAO,EAAA,CAAI,SACf,IAAMC,CAAAA,CAAM3B,EAAUyB,CAAI,CAAA,CAC1B,GAAIE,CAAAA,GAAQ,IAAA,CAAM,SAElB,IAAMC,CAAAA,CAAOF,CAAAA,GAAS,EAAI,CAAA,CAAK,UAAA,EAAe,GAAKA,CAAAA,GAAW,CAAA,CACxDG,EAAQ,CAAE,GAAA,CAAA,CAAMF,CAAAA,CAAMC,CAAAA,IAAU,CAAA,CAAG,IAAA,CAAAA,CAAK,CAAA,CAC9C,GAAIF,GAAQ,CAAA,CAAG,CACb,IAAMI,CAAAA,CAASD,CAAAA,CAAM,GAAA,GAAQ,EAAA,CACvBE,CAAAA,CAAOX,CAAAA,CAAG,IAAIU,CAAM,CAAA,CACtBC,EAAMA,CAAAA,CAAK,IAAA,CAAKF,CAAK,CAAA,CACpBT,CAAAA,CAAG,GAAA,CAAIU,CAAAA,CAAQ,CAACD,CAAK,CAAC,EAC7B,CAAA,KACER,EAAO,IAAA,CAAKQ,CAAK,EAErB,CACF,CACA,OAAO,CAAE,EAAA,CAAAT,CAAAA,CAAI,OAAAC,CAAAA,CAAQ,EAAA,CAAAC,CAAG,CAC1B,CAGO,SAASU,CAAAA,CAAW/B,CAAAA,CAAYgC,CAAAA,CAAiC,CACtE,GAAI,CAAChC,EAAI,OAAO,MAAA,CAChB,IAAMiC,CAAAA,CAAUjC,CAAAA,CAAG,MAAK,CACxB,GAAI,CAACiC,CAAAA,CAAS,OAAO,MAAA,CAErB,GAAIA,CAAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA,GAAM,EAAA,CAAI,CAC/B,IAAMC,CAAAA,CAAQ7B,CAAAA,CAAa4B,CAAO,CAAA,CAClC,GAAIC,CAAAA,GAAU,KAAM,OAAO,MAAA,CAG3B,IAAMC,CAAAA,CAAmB,OAAA,EAAW,IACpC,GAAKD,CAAAA,EAAS,GAAA,GAASC,CAAAA,EAAoB,GAAA,CAAK,CAC9C,IAAMC,CAAAA,CAAM,MAAA,CAAOF,EAAQ,WAAW,CAAA,GAAM,EAC5C,GAAIG,CAAAA,CAAQD,CAAAA,CAAKJ,CAAM,CAAA,CAAG,OAAO,KACnC,CACA,IAAA,IAAW,KAAKA,CAAAA,CAAO,EAAA,CAErB,GADI,CAAA,CAAE,IAAA,GAAS,CAAA,EACVE,CAAAA,EAAS,MAAA,CAAO,GAAA,CAAM,EAAE,IAAI,CAAA,EAAM,OAAO,GAAA,CAAM,CAAA,CAAE,IAAI,CAAA,GAAM,CAAA,CAAE,GAAA,CAAK,OAAO,KAAA,CAEhF,OAAO,MACT,CAEA,IAAMA,EAAQnC,CAAAA,CAAUkC,CAAO,EAC/B,OAAIC,CAAAA,GAAU,IAAA,CAAa,KAAA,CACpBG,CAAAA,CAAQH,CAAAA,CAAOF,CAAM,CAC9B,CAEA,SAASK,CAAAA,CAAQH,CAAAA,CAAeF,EAAiC,CAC/D,IAAMH,CAAAA,CAASG,CAAAA,CAAO,EAAA,CAAG,GAAA,CAAIE,IAAU,EAAE,CAAA,CACzC,GAAIL,CAAAA,CAAAA,CACF,IAAA,IAAWS,KAAKT,CAAAA,CACd,GAAA,CAAMK,CAAAA,CAAQI,CAAAA,CAAE,IAAA,IAAU,CAAA,GAAOA,EAAE,GAAA,CAAK,OAAO,KAAA,CAGnD,IAAA,IAAWA,CAAAA,IAAKN,CAAAA,CAAO,OACrB,GAAA,CAAME,CAAAA,CAAQI,CAAAA,CAAE,IAAA,IAAU,CAAA,GAAOA,CAAAA,CAAE,IAAK,OAAO,KAAA,CAEjD,OAAO,MACT,CCnHA,IAAMC,CAAAA,CAA4B,8BAAA,CAO5BC,CAAAA,CACJ,sFAAA,CAKIC,CAAAA,CAAW,IAAI,IAErB,SAASC,CAAAA,CAAUC,EAA4C,CAC7D,GAAI,EAAEA,CAAAA,IAAU9C,CAAAA,CAAAA,CAAgB,OAChC,IAAI+C,CAAAA,CAAIH,CAAAA,CAAS,IAAIE,CAAM,CAAA,CAC3B,OAAKC,CAAAA,GACHA,CAAAA,CAAI3B,EAAcpB,CAAAA,CAAc8C,CAAM,CAAA,EAAK,EAAE,CAAA,CAC7CF,EAAS,GAAA,CAAIE,CAAAA,CAAQC,CAAC,CAAA,CAAA,CAEjBA,CACT,CAGO,SAASC,CAAAA,EAAuC,CACrD,OAAO/C,CACT,CAUO,SAASgD,CAAAA,CACdnD,CAAAA,CACAK,EACiB,CACjB,IAAM+C,EAAKpD,CAAAA,EAAa,EAAA,CAClBqD,CAAAA,CAAUtD,CAAAA,CAAaC,CAAS,CAAA,CAChCqC,EAASU,CAAAA,CAAUM,CAAO,EAEhC,GAAI,CAAChB,EAAQ,CAGX,IAAMiB,CAAAA,CAAiBD,CAAAA,GAAY,OAAA,EAAWA,CAAAA,GAAY,UAC1D,OAAO,CACL,QAASC,CAAAA,CAAiB,cAAA,CAAiB,cAC3C,GAAIA,CAAAA,CAAiB,CAAE,MAAA,CAAQ,qBAA+B,CAAA,CAAI,EAAC,CACnE,OAAA,CAASA,CAAAA,CAAiBD,CAAAA,CAAU,IAAA,CACpC,QAAA,CAAU,IACZ,CACF,CAKA,GAAIT,CAAAA,CAA0B,IAAA,CAAKQ,CAAE,GAAK,CAACP,CAAAA,CAA4B,KAAKO,CAAE,CAAA,CAC5E,OAAO,CACL,OAAA,CAAS,cAAA,CACT,MAAA,CAAQ,mBAAA,CACR,OAAA,CAAAC,EACA,QAAA,CAAU,IACZ,EAGF,IAAMf,CAAAA,CAAAA,CAAWjC,GAAM,EAAA,EAAI,IAAA,EAAK,CAChC,GAAI,CAACiC,CAAAA,CACH,OAAO,CAAE,OAAA,CAAS,eAAgB,MAAA,CAAQ,cAAA,CAAgB,QAAAe,CAAAA,CAAS,QAAA,CAAU,IAAK,CAAA,CAGpF,IAAME,CAAAA,CAAUnB,EAAWE,CAAAA,CAASD,CAAM,CAAA,CAC1C,OAAO,CACL,OAAA,CAASkB,EAAU,UAAA,CAAa,SAAA,CAChC,OAAA,CAAAF,CAAAA,CACA,QAAA,CAAUE,CACZ,CACF,CAMO,SAASC,EAAoBC,CAAAA,CAAsB,CAExD,IAAMC,CAAAA,CAAAA,CADYD,CAAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,iBAAiB,CAAA,EAAK,IAChC,KAAA,CAAM,GAAG,EAAE,CAAC,CAAA,EAAG,MAAK,CAC5C,OAAIC,CAAAA,EAAAA,CACID,CAAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,kBAAkB,CAAA,EAAKA,CAAAA,CAAI,QAAQ,GAAA,CAAI,WAAW,GAAK,EAAA,EAAI,IAAA,EACrF,CAGO,SAASE,CAAAA,CAAcF,EAA+B,CAC3D,OAAON,CAAAA,CAAkBM,CAAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,YAAY,CAAA,CAAGD,CAAAA,CAAoBC,CAAG,CAAC,CAClF","file":"verify.js","sourcesContent":["/**\n * User-agent substrings that identify **publicly declared** AI crawlers — the\n * branded bots that identify themselves by name (OpenAI's GPTBot, Anthropic's\n * ClaudeBot, Perplexity-User, Google-Extended, etc.). High-confidence: when\n * this matches, the request almost certainly comes from that vendor's crawler\n * fleet.\n *\n * Does NOT include **coding-agent traffic** (Claude Code, Cline, Cursor,\n * Windsurf, Aider, OpenCode, VS Code). Those tools use generic HTTP library\n * UAs (axios, curl, got, colly, Electron) or spoof full browser UAs — they\n * can't be distinguished from non-AI traffic by UA alone. See\n * {@link HTTP_CLIENT_PATTERN} for the loose heuristic layer.\n *\n * Sources consulted when updating: darkvisitors.com, vendor docs from OpenAI,\n * Anthropic, Google, Perplexity, Cohere, Apple, Bytedance.\n */\nexport const AI_BOT_PATTERN =\n /ClaudeBot|Claude-User|Claude-SearchBot|Claude-Web|Anthropic|GPTBot|ChatGPT-User|OAI-SearchBot|PerplexityBot|Perplexity-User|Google-Extended|Google-CloudVertexBot|Google-Agent|GoogleAgent-Mariner|Gemini-Deep-Research|Applebot|cohere|Bytespider|CCBot|Amazonbot|Amzn-SearchBot|NovaAct|AzureAI-SearchBot|Meta-ExternalAgent|meta-externalfetcher|meta-webindexer|FacebookBot|DuckAssistBot|MistralAI-User|YouBot|AI2Bot|Diffbot|DeepSeek|PanguBot|Webzio-Extended|omgili|Timpibot|Grok|Manus-User|quillbot|MyCentralAIScraperBot|Cursor|Windsurf/i\n\n/**\n * HTTP library / runtime signatures frequently used by coding agents. Matching\n * any of these is a **loose** signal — legitimate curl scripts, CI jobs, and\n * server-to-server traffic use the same libraries. Use this for the wider\n * net (`coding_agent_hint: true`) and pair with other signals (request\n * shape, JA4 fingerprint, path patterns) for higher confidence.\n *\n * Based on behavioural signatures observed by Addy Osmani:\n * Claude Code → axios/1.8.4\n * Cline, Junie → curl/8.4.0\n * Cursor → got (sindresorhus/got)\n * Windsurf → colly\n * VS Code → Electron / Chromium\n *\n * Aider and OpenCode use Playwright-driven full Mozilla/Safari UAs and are\n * indistinguishable from real browsers at the UA layer.\n */\nexport const HTTP_CLIENT_PATTERN =\n /axios\\/|curl\\/|(?:^|[\\s(])got(?:\\/|[\\s(])|\\bcolly\\b|Electron\\/|node-fetch\\/|python-requests\\/|Go-http-client\\/|okhttp\\/|aiohttp\\/|Deno\\//i\n\nexport function isAiBot(userAgent: string | null | undefined): boolean {\n if (!userAgent) return false\n return AI_BOT_PATTERN.test(userAgent)\n}\n\nexport function isHttpClient(userAgent: string | null | undefined): boolean {\n if (!userAgent) return false\n return HTTP_CLIENT_PATTERN.test(userAgent)\n}\n\n/**\n * Map a user-agent string to a coarse, human-readable label. Returns one of:\n *\n * - A branded-crawler name (`'Claude'`, `'ChatGPT'`, …) — pair with\n * {@link isAiBot} for `is_ai_bot: true` segmentation.\n * - An HTTP-library name (`'curl'`, `'axios'`, `'got'`, `'colly'`,\n * `'Electron'`, …) — hint of a coding agent or automation; not\n * conclusive. Pair with {@link isHttpClient}.\n * - `'Browser'` for typical desktop browsers (possibly spoofed by\n * Playwright-based agents like Aider/OpenCode — this label alone can't\n * tell you).\n * - `'Other'` for anything unrecognised or empty input.\n */\nexport function parseBotName(userAgent: string | null | undefined): string {\n if (!userAgent || typeof userAgent !== 'string') return 'Other'\n const s = userAgent.toLowerCase()\n\n // Publicly declared AI crawlers (high confidence).\n if (s.includes('chatgpt-user') || s.includes('gptbot') || s.includes('oai-searchbot') || s.includes('openai'))\n return 'ChatGPT'\n if (\n s.includes('claudebot') ||\n s.includes('claude-user') ||\n s.includes('claude-searchbot') ||\n s.includes('claude-web') ||\n s.includes('anthropic')\n )\n return 'Claude'\n if (s.includes('perplexitybot') || s.includes('perplexity-user')) return 'Perplexity'\n if (s.includes('ccbot')) return 'Common Crawl'\n if (\n s.includes('google-extended') ||\n s.includes('googlebot') ||\n s.includes('google-cloudvertexbot') ||\n s.includes('google-agent') ||\n s.includes('googleagent-mariner') ||\n s.includes('gemini-deep-research')\n )\n return 'Google'\n if (s.includes('applebot')) return 'Apple'\n if (s.includes('bingbot')) return 'Bing'\n if (s.includes('bytespider')) return 'Bytespider'\n if (s.includes('amazonbot') || s.includes('amzn-searchbot') || s.includes('novaact')) return 'Amazon'\n if (\n s.includes('meta-externalagent') ||\n s.includes('meta-externalfetcher') ||\n s.includes('meta-webindexer') ||\n s.includes('facebookbot')\n )\n return 'Meta'\n if (s.includes('mistralai-user')) return 'Mistral'\n if (s.includes('duckassistbot')) return 'DuckDuckGo'\n if (s.includes('youbot')) return 'You.com'\n if (s.includes('diffbot')) return 'Diffbot'\n if (s.includes('ai2bot')) return 'AI2'\n if (s.includes('cohere')) return 'Cohere'\n if (s.includes('cursor')) return 'Cursor'\n if (s.includes('windsurf')) return 'Windsurf'\n if (s.includes('deepseek')) return 'DeepSeek'\n if (s.includes('pangubot')) return 'Huawei'\n if (s.includes('webzio') || s.includes('omgili')) return 'Webz.io'\n if (s.includes('timpibot')) return 'Timpi'\n if (s.includes('grok') || s.includes('xai-')) return 'xAI'\n if (s.includes('manus-user')) return 'Manus'\n if (s.includes('quillbot')) return 'QuillBot'\n if (s.includes('azureai-searchbot')) return 'Microsoft'\n if (s.includes('mycentralaiscraperbot')) return 'MyCentralAI'\n if (s.includes('petalbot')) return 'PetalBot'\n\n // SEO crawlers and monitoring bots.\n if (s.includes('ahrefsbot')) return 'Ahrefs'\n if (s.includes('semrushbot')) return 'Semrush'\n if (s.includes('mj12bot')) return 'Majestic'\n if (s.includes('dotbot')) return 'Moz'\n if (s.includes('rogerbot')) return 'Moz'\n if (s.includes('screaming frog')) return 'Screaming Frog'\n if (s.includes('sitebulb')) return 'Sitebulb'\n if (s.includes('linkfluence')) return 'Linkfluence'\n if (s.includes('dataforseo')) return 'DataForSEO'\n if (s.includes('serpstatbot')) return 'Serpstat'\n\n // Monitoring and feed bots.\n if (s.includes('uptimerobot')) return 'UptimeRobot'\n if (s.includes('pingdom')) return 'Pingdom'\n if (s.includes('statuscake')) return 'StatusCake'\n if (s.includes('newrelicpinger')) return 'New Relic'\n if (s.includes('datadogagent') || s.includes('datadog')) return 'Datadog'\n if (s.includes('slackbot')) return 'Slack'\n if (s.includes('twitterbot')) return 'Twitter'\n if (s.includes('linkedinbot')) return 'LinkedIn'\n if (s.includes('discordbot')) return 'Discord'\n if (s.includes('telegrambot')) return 'Telegram'\n if (s.includes('whatsapp')) return 'WhatsApp'\n\n // AI search and indexing bots.\n if (s.includes('linkupbot')) return 'Linkup'\n if (s.includes('sogou')) return 'Sogou'\n if (s.includes('yandexbot')) return 'Yandex'\n if (s.includes('baiduspider')) return 'Baidu'\n\n // Link preview fetchers.\n if (s.includes('facebookexternalhit')) return 'Facebook'\n if (s.includes('com.apple.webkit')) return 'Apple URL Preview'\n\n // Uptime and monitoring.\n if (s.includes('ohdear')) return 'Oh Dear'\n\n // Generic scrapers.\n if (s.includes('scrapy')) return 'Scrapy'\n if (s.includes('headlesschrome')) return 'Headless Chrome'\n if (s.includes('phantomjs')) return 'PhantomJS'\n if (s.includes('wget')) return 'wget'\n if (s.includes('httpie')) return 'HTTPie'\n if (s.includes('guzzlehttp')) return 'Guzzle'\n\n // HTTP library / runtime signatures (loose — coding agent or automation).\n // Check Electron before Browser since Electron UAs contain Chrome/Safari.\n if (s.includes('electron/')) return 'Electron'\n if (/curl\\//.test(s)) return 'curl'\n if (/axios\\//.test(s)) return 'axios'\n if (/(?:^|[\\s(])got(?:\\/|[\\s(])/.test(s)) return 'got'\n if (/\\bcolly\\b/.test(s)) return 'colly'\n if (/node-fetch\\//.test(s)) return 'node-fetch'\n if (/python-requests\\//.test(s)) return 'python-requests'\n if (/go-http-client\\//.test(s)) return 'Go http client'\n if (/okhttp\\//.test(s)) return 'OkHttp'\n if (/aiohttp\\//.test(s)) return 'aiohttp'\n if (/deno\\//.test(s)) return 'Deno'\n\n // Real browsers (or UAs spoofed to look like them — see Aider/OpenCode note).\n if (s.includes('mozilla') || s.includes('chrome') || s.includes('safari') || s.includes('firefox'))\n return 'Browser'\n\n return 'Other'\n}\n\n/**\n * Return the first product token from a UA header, useful for segmenting by\n * client without hard-coding every bot name. Falls back to `'Other'` for empty\n * input.\n */\nexport function firstUserAgentProduct(userAgent: string | null | undefined): string {\n if (!userAgent || typeof userAgent !== 'string') return 'Other'\n const compatibleMatch = userAgent.match(/compatible;\\s*([^/;\\s]+)(?:\\/[^\\s;]*)?/i)\n if (compatibleMatch && compatibleMatch[1]) return compatibleMatch[1].trim()\n const first = userAgent.trim().split('/')[0]?.trim().split(/\\s+/)[0]?.trim()\n return first || 'Other'\n}\n\n/**\n * Detect likely headless/automated browsers by checking for missing headers\n * that real browsers always send. Playwright, Puppeteer, and similar tools\n * spoof the UA but often omit standard browser headers.\n *\n * Signals checked (each scores 1 point):\n * - Missing `Accept-Language` — every real browser sends this\n * - Missing `Sec-Fetch-Mode` — sent by all modern browsers\n * - Missing `Sec-CH-UA` — Client Hints, Chromium 89+\n * - `Sec-CH-UA` contains \"HeadlessChrome\"\n * - Missing or bare Accept header — browsers send detailed accept lists\n * - `Connection: close` with browser UA — browsers use keep-alive\n *\n * Returns a score (0-6), the signals that fired, and a boolean `likely`\n * flag (score >= 2 with a browser-like UA).\n */\nexport function detectHeadless(req: Request): HeadlessDetection {\n const signals: string[] = []\n const ua = (req.headers.get('user-agent') || '').toLowerCase()\n const isBrowserUA =\n ua.includes('mozilla') || ua.includes('chrome') || ua.includes('safari') || ua.includes('firefox')\n\n if (!isBrowserUA) return { score: 0, signals: [], likely: false }\n\n if (!req.headers.get('accept-language')) {\n signals.push('missing-accept-language')\n }\n if (!req.headers.get('sec-fetch-mode')) {\n signals.push('missing-sec-fetch-mode')\n }\n const secChUa = req.headers.get('sec-ch-ua')\n if (!secChUa) {\n signals.push('missing-sec-ch-ua')\n } else if (secChUa.toLowerCase().includes('headlesschrome')) {\n signals.push('headless-chrome-hint')\n }\n const accept = req.headers.get('accept') || ''\n if (!accept || accept === '*/*') {\n signals.push('missing-or-bare-accept')\n }\n if ((req.headers.get('connection') || '').toLowerCase() === 'close') {\n signals.push('connection-close')\n }\n\n const score = signals.length\n return { score, signals, likely: score >= 2 }\n}\n\nexport interface HeadlessDetection {\n /** Number of suspicious signals found (0-6). */\n score: number\n /** Names of the specific signals that fired. */\n signals: string[]\n /** True when score >= 2 — strong headless indication. */\n likely: boolean\n}\n\nexport type AgentKind =\n | 'declared-crawler'\n | 'coding-agent-hint'\n | 'headless-likely'\n | 'browser'\n | 'other'\n\nexport interface AgentClassification {\n /**\n * Categorical tag for the request:\n *\n * - `'declared-crawler'` — {@link AI_BOT_PATTERN} matched. High confidence.\n * - `'coding-agent-hint'` — {@link HTTP_CLIENT_PATTERN} matched. Loose\n * signal; could be a coding agent, a curl script, or any automation.\n * - `'headless-likely'` — Browser-like UA but missing standard headers.\n * Strong signal of Playwright/Puppeteer automation (Aider, OpenCode, etc.).\n * - `'browser'` — Looks like a real browser with expected headers present.\n * - `'other'` — Unrecognised or empty.\n */\n kind: AgentKind\n /** Human-readable label, same string {@link parseBotName} returns. */\n label: string\n /** Strict: `true` only when the UA matches a branded AI crawler. */\n isAiBot: boolean\n /** Loose: `true` for known HTTP-library / automation UAs. */\n codingAgentHint: boolean\n /** Headless browser detection result. Only populated when `req` is passed. */\n headless?: HeadlessDetection\n}\n\n/**\n * UA-only classification. Use {@link classifyRequest} for full detection\n * including headless browser heuristics.\n */\nexport function classifyAgent(userAgent: string | null | undefined): AgentClassification {\n const label = parseBotName(userAgent)\n const aiBot = isAiBot(userAgent)\n const httpClient = isHttpClient(userAgent)\n\n let kind: AgentKind\n if (aiBot) kind = 'declared-crawler'\n else if (httpClient) kind = 'coding-agent-hint'\n else if (label === 'Browser') kind = 'browser'\n else kind = 'other'\n\n return { kind, label, isAiBot: aiBot, codingAgentHint: httpClient }\n}\n\n/**\n * Full request classification — combines UA parsing with header-based\n * headless detection. When a browser-like UA is missing standard headers,\n * the kind is promoted from `'browser'` to `'headless-likely'`.\n */\nexport function classifyRequest(req: Request): AgentClassification {\n const userAgent = req.headers.get('user-agent') || ''\n const base = classifyAgent(userAgent)\n const headless = detectHeadless(req)\n\n let kind = base.kind\n let label = base.label\n if (kind === 'browser' && headless.likely) {\n kind = 'headless-likely'\n // Relabel too. Leaving it as 'Browser' meant automation with a spoofed\n // browser UA — 79% of one production site's agent traffic — was\n // indistinguishable from a human in any `bot_name` breakdown, and was\n // silently excluded by the obvious `bot_name != 'Browser'` filter.\n label = 'Headless'\n }\n\n return { ...base, kind, label, headless }\n}\n","/**\n * Published crawler IP ranges, vendor by vendor.\n *\n * GENERATED FILE — do not edit by hand. Regenerate with:\n *\n * node scripts/refresh-bot-ranges.mjs\n *\n * Keys match the labels {@link parseBotName} returns, so a claimed identity\n * maps to its range list without a translation table.\n *\n * Only vendors that publish a machine-readable feed appear here. A vendor's\n * absence means \"cannot be verified\", never \"not a real bot\" — see\n * {@link verifyBotIdentity} for how that distinction is surfaced.\n *\n * Freshness is the whole ballgame: these lists rotate. Almost every OpenAI\n * prefix is an Azure block and every Anthropic prefix is GCP or similar, so\n * \"came from a datacenter\" proves nothing on its own — only membership in the\n * current published list does. A stale snapshot produces false 'spoofed'\n * verdicts on legitimate crawlers, which is the failure mode to fear.\n */\n\n/** When these ranges were captured from the vendor feeds (UTC). */\nexport const BOT_RANGES_CAPTURED_AT = '2026-08-02T00:00:00Z'\n\nexport const BOT_IP_RANGES: Readonly<Record<string, readonly string[]>> = {\n ChatGPT: [\n '104.208.184.192/28',\n '104.208.184.208/28',\n '104.210.139.192/28',\n '104.210.139.224/28',\n '104.210.140.128/28',\n '128.85.198.32/28',\n '13.65.138.112/28',\n '13.65.138.96/28',\n '13.67.72.16/28',\n '13.70.107.160/28',\n '13.71.2.208/28',\n '13.76.115.224/28',\n '13.76.115.240/28',\n '13.76.116.80/28',\n '13.76.32.208/28',\n '13.83.167.128/28',\n '13.83.237.176/28',\n '132.196.82.48/28',\n '132.196.86.0/24',\n '134.149.233.80/28',\n '135.116.136.160/28',\n '135.13.64.240/28',\n '135.220.73.208/28',\n '135.220.73.240/28',\n '135.234.64.0/24',\n '135.237.131.208/28',\n '135.237.133.48/28',\n '137.135.191.176/28',\n '138.91.30.48/28',\n '138.91.46.96/28',\n '145.132.1.32/28',\n '145.132.136.96/28',\n '145.133.0.176/28',\n '158.158.5.32/28',\n '168.63.252.240/28',\n '172.162.248.64/28',\n '172.170.1.80/28',\n '172.170.225.0/28',\n '172.170.241.80/28',\n '172.170.8.208/28',\n '172.171.4.176/28',\n '172.175.152.224/28',\n '172.178.140.144/28',\n '172.178.141.112/28',\n '172.178.141.128/28',\n '172.182.193.224/28',\n '172.182.193.80/28',\n '172.182.194.144/28',\n '172.182.194.32/28',\n '172.182.195.48/28',\n '172.182.202.0/25',\n '172.182.204.0/24',\n '172.182.207.0/25',\n '172.182.209.208/28',\n '172.182.211.192/28',\n '172.182.213.192/28',\n '172.182.214.0/24',\n '172.182.215.0/24',\n '172.182.224.0/28',\n '172.183.143.224/28',\n '172.183.222.128/28',\n '172.192.112.208/28',\n '172.192.88.192/28',\n '172.192.97.32/28',\n '172.197.160.192/28',\n '172.197.161.208/28',\n '172.197.170.80/28',\n '172.197.203.16/28',\n '172.199.137.80/28',\n '172.202.102.112/28',\n '172.203.190.128/28',\n '172.204.27.16/28',\n '172.204.28.224/28',\n '172.204.96.32/28',\n '172.204.96.48/28',\n '172.204.96.80/28',\n '172.205.189.192/28',\n '172.207.1.32/28',\n '172.208.128.32/28',\n '172.208.128.48/28',\n '172.212.159.64/28',\n '172.212.172.160/28',\n '172.213.21.16/28',\n '172.215.215.32/28',\n '172.215.218.96/28',\n '191.233.1.112/28',\n '191.233.1.128/28',\n '191.233.194.32/28',\n '191.233.196.112/28',\n '191.233.199.160/28',\n '191.233.2.0/28',\n '191.235.66.16/28',\n '191.235.99.80/28',\n '191.237.249.64/28',\n '191.239.245.16/28',\n '20.102.212.144/28',\n '20.113.211.112/28',\n '20.113.225.112/28',\n '20.125.112.224/28',\n '20.125.144.144/28',\n '20.125.66.80/28',\n '20.14.99.96/28',\n '20.161.75.208/28',\n '20.168.18.32/28',\n '20.168.7.192/28',\n '20.168.7.240/28',\n '20.169.6.224/28',\n '20.169.7.48/28',\n '20.169.72.112/28',\n '20.169.73.176/28',\n '20.169.73.32/28',\n '20.169.73.64/28',\n '20.169.77.0/25',\n '20.169.78.112/28',\n '20.169.78.128/28',\n '20.169.78.144/28',\n '20.169.78.160/28',\n '20.169.78.176/28',\n '20.169.78.192/28',\n '20.169.78.208/28',\n '20.169.78.48/28',\n '20.169.78.64/28',\n '20.169.78.80/28',\n '20.169.78.96/28',\n '20.169.86.224/28',\n '20.169.86.240/28',\n '20.169.87.112/28',\n '20.17.108.96/28',\n '20.170.184.16/28',\n '20.170.184.32/28',\n '20.170.184.48/28',\n '20.170.184.64/28',\n '20.170.184.80/28',\n '20.171.123.64/28',\n '20.171.206.0/24',\n '20.171.207.0/24',\n '20.171.53.224/28',\n '20.172.29.32/28',\n '20.193.233.240/28',\n '20.193.50.32/28',\n '20.194.0.208/28',\n '20.194.1.0/28',\n '20.198.67.96/28',\n '20.199.211.160/28',\n '20.199.242.0/28',\n '20.200.212.240/28',\n '20.204.24.240/28',\n '20.210.154.128/28',\n '20.210.174.208/28',\n '20.210.211.192/28',\n '20.215.187.208/28',\n '20.215.188.192/28',\n '20.215.214.16/28',\n '20.215.219.128/28',\n '20.215.219.160/28',\n '20.215.219.208/28',\n '20.215.220.112/28',\n '20.215.220.128/28',\n '20.215.220.144/28',\n '20.215.220.160/28',\n '20.215.220.176/28',\n '20.215.220.192/28',\n '20.215.220.208/28',\n '20.215.220.64/28',\n '20.215.220.80/28',\n '20.215.220.96/28',\n '20.218.30.240/28',\n '20.219.71.192/28',\n '20.222.36.192/28',\n '20.226.32.80/28',\n '20.227.140.32/28',\n '20.228.106.176/28',\n '20.235.75.208/28',\n '20.235.87.224/28',\n '20.249.63.208/28',\n '20.25.151.224/28',\n '20.250.136.64/28',\n '20.250.136.80/28',\n '20.250.6.128/28',\n '20.27.94.128/28',\n '20.42.10.176/28',\n '20.45.178.144/28',\n '20.48.120.208/28',\n '20.52.125.160/28',\n '20.55.129.0/28',\n '20.55.229.144/28',\n '20.57.199.192/28',\n '20.63.180.96/28',\n '20.63.221.64/28',\n '20.79.59.112/28',\n '20.81.183.64/28',\n '20.83.243.176/28',\n '20.97.189.96/28',\n '23.102.140.144/28',\n '23.102.141.32/28',\n '23.98.142.176/28',\n '23.98.179.16/28',\n '23.98.186.176/28',\n '23.98.186.192/28',\n '23.98.186.64/28',\n '23.98.186.96/28',\n '4.151.119.48/28',\n '4.151.241.240/28',\n '4.151.71.176/28',\n '4.189.118.208/28',\n '4.189.119.48/28',\n '4.196.118.112/28',\n '4.197.115.112/28',\n '4.197.19.176/28',\n '4.197.22.112/28',\n '4.197.64.0/28',\n '4.197.64.16/28',\n '4.197.64.48/28',\n '4.197.64.64/28',\n '4.198.72.16/28',\n '4.198.96.112/28',\n '4.201.232.64/28',\n '4.201.232.80/28',\n '4.203.96.80/28',\n '4.205.128.176/28',\n '4.218.24.64/28',\n '4.226.200.16/28',\n '4.226.226.32/28',\n '4.227.36.0/25',\n '40.116.73.208/28',\n '40.122.235.112/28',\n '40.67.175.0/25',\n '40.67.183.160/28',\n '40.67.183.176/28',\n '40.78.161.48/28',\n '40.81.134.128/28',\n '40.81.134.144/28',\n '40.81.234.144/28',\n '40.81.67.96/28',\n '40.84.181.32/28',\n '40.84.221.208/28',\n '40.84.221.224/28',\n '40.90.214.16/28',\n '48.193.44.32/28',\n '48.221.184.112/28',\n '48.221.184.80/28',\n '48.221.184.96/28',\n '48.221.40.176/28',\n '51.107.70.192/28',\n '51.116.2.80/28',\n '51.116.221.96/28',\n '51.56.40.80/28',\n '51.57.0.96/28',\n '51.59.24.64/28',\n '51.59.24.80/28',\n '51.59.40.80/28',\n '51.59.40.96/28',\n '51.59.48.80/28',\n '51.59.48.96/28',\n '51.8.102.0/24',\n '51.8.155.112/28',\n '51.8.155.48/28',\n '51.8.155.64/28',\n '51.8.187.224/28',\n '52.148.129.32/28',\n '52.153.130.64/28',\n '52.154.22.48/28',\n '52.156.77.144/28',\n '52.159.227.32/28',\n '52.159.249.96/28',\n '52.161.49.224/28',\n '52.161.49.96/28',\n '52.165.212.16/28',\n '52.165.212.32/28',\n '52.165.212.48/28',\n '52.172.129.160/28',\n '52.172.251.112/28',\n '52.173.219.112/28',\n '52.173.219.96/28',\n '52.173.221.16/28',\n '52.173.221.176/28',\n '52.173.221.208/28',\n '52.173.234.16/28',\n '52.173.234.80/28',\n '52.173.235.80/28',\n '52.183.217.240/28',\n '52.187.246.128/28',\n '52.190.137.144/28',\n '52.190.137.16/28',\n '52.190.139.48/28',\n '52.190.142.64/28',\n '52.190.190.16/28',\n '52.225.75.208/28',\n '52.230.152.0/24',\n '52.230.163.32/28',\n '52.230.164.176/28',\n '52.231.30.48/28',\n '52.231.34.176/28',\n '52.231.39.144/28',\n '52.231.39.192/28',\n '52.231.49.48/28',\n '52.231.50.64/28',\n '52.236.94.144/28',\n '52.241.146.208/28',\n '52.242.132.224/28',\n '52.242.132.240/28',\n '52.242.245.208/28',\n '52.252.113.240/28',\n '52.255.109.112/28',\n '52.255.109.128/28',\n '52.255.109.144/28',\n '52.255.109.80/28',\n '52.255.109.96/28',\n '52.255.111.0/28',\n '52.255.111.112/28',\n '52.255.111.32/28',\n '52.255.111.48/28',\n '52.255.111.80/28',\n '57.154.174.112/28',\n '57.154.175.0/28',\n '57.154.187.32/28',\n '68.154.28.96/28',\n '68.218.30.112/28',\n '68.220.57.64/28',\n '68.221.67.192/28',\n '68.221.67.224/28',\n '68.221.67.240/28',\n '70.153.139.208/28',\n '70.153.189.192/28',\n '70.153.190.16/28',\n '70.153.76.16/28',\n '70.153.87.224/28',\n '70.156.144.64/28',\n '70.156.152.80/28',\n '70.156.152.96/28',\n '74.161.200.96/28',\n '74.224.217.64/28',\n '74.226.253.160/28',\n '74.249.86.176/28',\n '74.7.175.128/25',\n '74.7.227.0/25',\n '74.7.227.128/25',\n '74.7.228.0/25',\n '74.7.228.128/25',\n '74.7.229.0/25',\n '74.7.229.128/25',\n '74.7.230.0/25',\n '74.7.241.0/25',\n '74.7.241.128/25',\n '74.7.242.0/25',\n '74.7.242.128/25',\n '74.7.243.0/25',\n '74.7.243.128/25',\n '74.7.244.0/25',\n '74.7.35.112/28',\n '74.7.35.48/28',\n '74.7.36.64/28',\n '74.7.36.80/28',\n '74.7.36.96/28',\n '85.211.241.128/28',\n '9.129.0.0/17',\n '9.160.128.16/28',\n '9.160.128.32/28',\n '9.160.128.64/28',\n '9.160.163.128/28',\n '9.160.164.128/28',\n '9.160.34.144/28',\n '9.160.36.16/28',\n '9.160.96.16/28',\n '9.163.101.48/28',\n '9.205.25.128/28',\n '9.205.8.48/28',\n '9.223.181.208/28',\n '9.234.96.192/28',\n '9.234.97.128/28',\n '9.234.97.96/28',\n '9.235.40.32/28',\n ],\n Claude: [\n '136.107.176.208/32',\n '216.73.216.0/22',\n '34.11.34.31/32',\n '34.150.241.79/32',\n '34.162.191.81/32',\n '34.162.230.222/32',\n '34.162.244.71/32',\n '34.182.140.95/32',\n '34.182.161.143/32',\n '34.182.218.27/32',\n '34.182.220.85/32',\n '34.182.222.37/32',\n '34.182.225.167/32',\n '34.182.226.151/32',\n '34.182.226.221/32',\n '34.186.108.163/32',\n '34.85.172.162/32',\n '35.221.29.174/32',\n '35.245.175.129/32',\n '35.245.89.239/32',\n ],\n Perplexity: [\n '107.20.236.150/32',\n '18.210.92.235/32',\n '18.97.1.228/30',\n '18.97.9.96/29',\n '3.211.124.183/32',\n '3.222.232.239/32',\n '3.224.62.45/32',\n '3.231.139.107/32',\n ],\n Apple: [\n '17.22.237.0/24',\n '17.22.245.0/24',\n '17.22.253.0/24',\n '17.241.193.160/27',\n '17.241.200.160/27',\n '17.241.208.160/27',\n '17.241.219.0/24',\n '17.241.227.0/24',\n '17.241.75.0/24',\n '17.246.15.0/24',\n '17.246.19.0/24',\n '17.246.23.0/24',\n ],\n}\n\n/** Vendor labels this build can verify. Anything else yields a null verdict. */\nexport const VERIFIABLE_VENDORS: readonly string[] = Object.keys(BOT_IP_RANGES)\n","/**\n * Dependency-free CIDR matching for IPv4 and IPv6.\n *\n * Kept separate from {@link verifyBotIdentity} so the matching logic can be\n * tested in isolation — a wrong answer here silently turns a real crawler into\n * a \"spoofed\" one, which is worse than not verifying at all.\n *\n * Everything is parsed once into numeric form (`number` for v4, `bigint` for\n * v6) and compared with masks. No allocation per request beyond the parse of\n * the incoming IP.\n */\n\n/** Parse dotted-quad IPv4 into a 32-bit unsigned integer. */\nexport function ipv4ToInt(ip: string): number | null {\n const parts = ip.split('.')\n if (parts.length !== 4) return null\n let out = 0\n for (const part of parts) {\n // Reject empty, non-numeric, out-of-range, and leading-zero forms\n // (`01.2.3.4` is ambiguous — some parsers read it as octal).\n if (!/^\\d{1,3}$/.test(part)) return null\n if (part.length > 1 && part[0] === '0') return null\n const n = Number(part)\n if (n > 255) return null\n out = (out << 8) | n\n }\n // `>>> 0` converts the signed 32-bit result back to unsigned.\n return out >>> 0\n}\n\n/**\n * Parse IPv6 (including `::` compression and IPv4-mapped tails like\n * `::ffff:1.2.3.4`) into a 128-bit BigInt.\n */\nexport function ipv6ToBigInt(ip: string): bigint | null {\n let text = ip\n // Strip a zone index (`fe80::1%eth0`) — irrelevant for range membership.\n const zone = text.indexOf('%')\n if (zone !== -1) text = text.slice(0, zone)\n if (!text || text.indexOf(':') === -1) return null\n\n // An embedded IPv4 tail contributes the low 32 bits.\n let tail: number | null = null\n const lastColon = text.lastIndexOf(':')\n const maybeV4 = text.slice(lastColon + 1)\n if (maybeV4.indexOf('.') !== -1) {\n tail = ipv4ToInt(maybeV4)\n if (tail === null) return null\n text = text.slice(0, lastColon + 1) + '0:0'\n }\n\n const halves = text.split('::')\n if (halves.length > 2) return null\n const head = halves[0] ? halves[0].split(':') : []\n const rest = halves.length === 2 ? (halves[1] ? halves[1].split(':') : []) : null\n\n let groups: string[]\n if (rest === null) {\n groups = head\n if (groups.length !== 8) return null\n } else {\n const fill = 8 - head.length - rest.length\n if (fill < 0) return null\n groups = [...head, ...Array(fill).fill('0'), ...rest]\n }\n\n let out = 0n\n for (const g of groups) {\n if (!/^[0-9a-fA-F]{1,4}$/.test(g)) return null\n out = (out << 16n) | BigInt(parseInt(g, 16))\n }\n // Overwrite the low 32 bits with the embedded IPv4 value when present.\n if (tail !== null) out = ((out >> 32n) << 32n) | BigInt(tail >>> 0)\n return out\n}\n\ninterface V4Range {\n net: number\n mask: number\n}\ninterface V6Range {\n net: bigint\n bits: number\n}\n\nexport interface CompiledRanges {\n /**\n * IPv4 ranges bucketed by first octet. A vendor can publish hundreds of\n * prefixes (OpenAI: 372) and a linear scan walked all of them on every\n * request; bucketing turns the common case into one map lookup plus a\n * handful of comparisons. Prefixes shorter than /8 span several buckets and\n * are held in `v4Wide`, which stays tiny.\n */\n v4: Map<number, V4Range[]>\n v4Wide: V4Range[]\n v6: V6Range[]\n}\n\n/**\n * Pre-compile a list of CIDR strings into numeric form. Invalid entries are\n * dropped rather than thrown — a malformed line in a vendor's published feed\n * shouldn't take down the whole check.\n */\nexport function compileRanges(cidrs: readonly string[]): CompiledRanges {\n const v4 = new Map<number, V4Range[]>()\n const v4Wide: V4Range[] = []\n const v6: V6Range[] = []\n for (const cidr of cidrs) {\n const slash = cidr.lastIndexOf('/')\n if (slash === -1) continue\n const addr = cidr.slice(0, slash)\n const bits = Number(cidr.slice(slash + 1))\n if (!Number.isInteger(bits) || bits < 0) continue\n\n if (addr.indexOf(':') !== -1) {\n if (bits > 128) continue\n const net = ipv6ToBigInt(addr)\n if (net === null) continue\n v6.push({ net: bits === 0 ? 0n : (net >> BigInt(128 - bits)) << BigInt(128 - bits), bits })\n } else {\n if (bits > 32) continue\n const net = ipv4ToInt(addr)\n if (net === null) continue\n // `bits === 0` needs special handling: `<<32` is a no-op in JS, not zero.\n const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0\n const entry = { net: (net & mask) >>> 0, mask }\n if (bits >= 8) {\n const bucket = entry.net >>> 24\n const list = v4.get(bucket)\n if (list) list.push(entry)\n else v4.set(bucket, [entry])\n } else {\n v4Wide.push(entry)\n }\n }\n }\n return { v4, v4Wide, v6 }\n}\n\n/** True when `ip` falls inside any range in the pre-compiled set. */\nexport function ipInRanges(ip: string, ranges: CompiledRanges): boolean {\n if (!ip) return false\n const trimmed = ip.trim()\n if (!trimmed) return false\n\n if (trimmed.indexOf(':') !== -1) {\n const value = ipv6ToBigInt(trimmed)\n if (value === null) return false\n // An IPv4-mapped address (::ffff:a.b.c.d) must also be checked against the\n // v4 list — Vercel and Cloudflare both emit this form on dual-stack edges.\n const V4_MAPPED_PREFIX = 0xffffn << 32n\n if ((value >> 32n) === V4_MAPPED_PREFIX >> 32n) {\n const low = Number(value & 0xffffffffn) >>> 0\n if (matchV4(low, ranges)) return true\n }\n for (const r of ranges.v6) {\n if (r.bits === 0) return true\n if ((value >> BigInt(128 - r.bits)) << BigInt(128 - r.bits) === r.net) return true\n }\n return false\n }\n\n const value = ipv4ToInt(trimmed)\n if (value === null) return false\n return matchV4(value, ranges)\n}\n\nfunction matchV4(value: number, ranges: CompiledRanges): boolean {\n const bucket = ranges.v4.get(value >>> 24)\n if (bucket) {\n for (const r of bucket) {\n if (((value & r.mask) >>> 0) === r.net) return true\n }\n }\n for (const r of ranges.v4Wide) {\n if (((value & r.mask) >>> 0) === r.net) return true\n }\n return false\n}\n\n/** Convenience wrapper — compiles on every call, so prefer {@link ipInRanges}. */\nexport function ipInCidr(ip: string, cidr: string): boolean {\n return ipInRanges(ip, compileRanges([cidr]))\n}\n","import { parseBotName } from './bots.js'\nimport { BOT_IP_RANGES, VERIFIABLE_VENDORS } from './bot-ranges.js'\nimport { compileRanges, ipInRanges, type CompiledRanges } from './cidr.js'\n\n/**\n * Verdict on whether a request's claimed crawler identity holds up against the\n * vendor's published IP ranges.\n *\n * - `'verified'` — the UA claims a vendor we can check, and the client IP is\n * inside that vendor's published ranges. High confidence.\n * - `'spoofed'` — the UA claims a vendor we can check, and the IP is **not**\n * in its ranges. Someone is impersonating the crawler.\n * - `'unverifiable'` — the UA claims a vendor that publishes no feed we bundle\n * (Bytespider, Amazon, Meta, …), or no usable client IP was available.\n * - `'not-claimed'` — the UA doesn't claim a verifiable crawler at all. This is\n * the normal verdict for browsers and HTTP clients; it is *not* a negative\n * finding.\n */\nexport type VerificationVerdict = 'verified' | 'spoofed' | 'unverifiable' | 'not-claimed'\n\n/** Why a request could not be judged. Only set when verdict is 'unverifiable'. */\nexport type UnverifiableReason = 'no-published-ranges' | 'client-side-agent' | 'no-client-ip'\n\nexport interface BotVerification {\n verdict: VerificationVerdict\n reason?: UnverifiableReason\n /** Vendor label the UA claims, when it claims one. */\n claimed: string | null\n /**\n * Convenience boolean for filtering: `true` only for `'verified'`, `false`\n * only for `'spoofed'`, `null` when no judgement was possible. Deliberately\n * tri-state — collapsing \"unverifiable\" into `false` would brand every\n * Bytespider and Amazonbot hit an impostor.\n */\n verified: boolean | null\n}\n\n/**\n * Products that fetch from the **end user's own device**, not from the\n * vendor's infrastructure.\n *\n * A published range list only covers a vendor's server-side crawler fleet.\n * When the fetch originates on a developer's laptop, the client IP is theirs\n * and will never appear in the vendor's ranges — so a range check produces\n * 'spoofed' for entirely legitimate traffic.\n *\n * This is not cosmetic. Measured against 30 days of production data:\n *\n * Claude-User (claude-code CLI) 6,492 events 4,486 IPs 0% in range\n * Perplexity-User 493 events 148 IPs 0% in range\n * ClaudeBot 13,671 events 236 IPs 96% in range\n * PerplexityBot 6,897 events 158 IPs 91% in range\n *\n * Treating the first two as impostors would have falsely accused ~7k real\n * fetches a month. The distinction is per *product*, not per vendor, and not\n * inferable from a `-User` suffix — OpenAI's ChatGPT-User fetches server-side\n * from Azure and verifies at ~99%.\n *\n * Only products belonging to a vendor we hold ranges for need listing here.\n * Cursor, Windsurf, Cline and Aider were previously included but were dead\n * branches: their vendors publish no feed, so they exit earlier as\n * 'no-published-ranges' or 'not-claimed' and never reach this test.\n */\nconst CLIENT_SIDE_AGENT_PATTERN = /claude-code|perplexity-user/i\n\n/**\n * Products known to fetch server-side from ranges the vendor publishes. Only\n * these can earn a 'verified' or 'spoofed' verdict; anything else is reported\n * 'unverifiable' so the data never overstates what was actually checked.\n */\nconst SERVER_SIDE_CRAWLER_PATTERN =\n /ClaudeBot|Claude-SearchBot|GPTBot|OAI-SearchBot|ChatGPT-User|PerplexityBot|Applebot/i\n\n// Compiled on first use per vendor, not at module load. Eagerly parsing all\n// 412 prefixes cost cold-start CPU on every request path that imported this\n// module, including the majority that never verify anything.\nconst COMPILED = new Map<string, CompiledRanges>()\n\nfunction rangesFor(vendor: string): CompiledRanges | undefined {\n if (!(vendor in BOT_IP_RANGES)) return undefined\n let c = COMPILED.get(vendor)\n if (!c) {\n c = compileRanges(BOT_IP_RANGES[vendor] ?? [])\n COMPILED.set(vendor, c)\n }\n return c\n}\n\n/** Vendor labels this build can produce a verified/spoofed verdict for. */\nexport function verifiableVendors(): readonly string[] {\n return VERIFIABLE_VENDORS\n}\n\n/**\n * Check a claimed crawler identity against the vendor's published IP ranges.\n *\n * Pass the client IP you already trust — on Vercel and Cloudflare that is the\n * first hop of `x-forwarded-for`. If your edge doesn't strip client-supplied\n * `X-Forwarded-For`, an attacker controls this value and a `'verified'` verdict\n * means nothing; verify your proxy's behaviour before relying on it.\n */\nexport function verifyBotIdentity(\n userAgent: string | null | undefined,\n ip: string | null | undefined\n): BotVerification {\n const ua = userAgent ?? ''\n const claimed = parseBotName(userAgent)\n const ranges = rangesFor(claimed)\n\n if (!ranges) {\n // Either not a crawler at all, or a crawler with no published feed. Both\n // are \"no judgement\", but the caller may want to tell them apart.\n const isKnownCrawler = claimed !== 'Other' && claimed !== 'Browser'\n return {\n verdict: isKnownCrawler ? 'unverifiable' : 'not-claimed',\n ...(isKnownCrawler ? { reason: 'no-published-ranges' as const } : {}),\n claimed: isKnownCrawler ? claimed : null,\n verified: null\n }\n }\n\n // Order matters: a client-side agent must be excluded before the range check,\n // and the server-side allowlist gates everything else, so an unrecognised\n // product from a covered vendor is never accused on a partial range list.\n if (CLIENT_SIDE_AGENT_PATTERN.test(ua) || !SERVER_SIDE_CRAWLER_PATTERN.test(ua)) {\n return {\n verdict: 'unverifiable',\n reason: 'client-side-agent',\n claimed,\n verified: null\n }\n }\n\n const trimmed = (ip ?? '').trim()\n if (!trimmed) {\n return { verdict: 'unverifiable', reason: 'no-client-ip', claimed, verified: null }\n }\n\n const inRange = ipInRanges(trimmed, ranges)\n return {\n verdict: inRange ? 'verified' : 'spoofed',\n claimed,\n verified: inRange\n }\n}\n\n/**\n * Extract the client IP the way {@link trackVisit} does — first hop of\n * `x-forwarded-for`, falling back to the platform-specific headers.\n */\nexport function clientIpFromRequest(req: Request): string {\n const forwarded = req.headers.get('x-forwarded-for') || ''\n const first = forwarded.split(',')[0]?.trim()\n if (first) return first\n return (req.headers.get('cf-connecting-ip') || req.headers.get('x-real-ip') || '').trim()\n}\n\n/** Verify straight from a request object. */\nexport function verifyRequest(req: Request): BotVerification {\n return verifyBotIdentity(req.headers.get('user-agent'), clientIpFromRequest(req))\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apideck/agent-analytics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Track AI agent and bot traffic to your Next.js / Vercel app — PostHog, webhooks, or any custom analytics backend. Detects Claude, ChatGPT, Perplexity, Google-Extended, and more.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
"import": "./dist/markdown.js",
|
|
39
39
|
"require": "./dist/markdown.cjs"
|
|
40
40
|
},
|
|
41
|
+
"./verify": {
|
|
42
|
+
"types": "./dist/verify.d.ts",
|
|
43
|
+
"import": "./dist/verify.js",
|
|
44
|
+
"require": "./dist/verify.cjs"
|
|
45
|
+
},
|
|
41
46
|
"./posthog": {
|
|
42
47
|
"types": "./dist/adapters/posthog.d.ts",
|
|
43
48
|
"import": "./dist/adapters/posthog.js",
|
|
@@ -72,7 +77,7 @@
|
|
|
72
77
|
"vitest": "^2.1.0"
|
|
73
78
|
},
|
|
74
79
|
"engines": {
|
|
75
|
-
"node": ">=
|
|
80
|
+
"node": ">=20"
|
|
76
81
|
},
|
|
77
82
|
"publishConfig": {
|
|
78
83
|
"access": "public"
|