@apideck/agent-analytics 0.13.0 → 0.14.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.
@@ -1 +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"]}
1
+ {"version":3,"sources":["../src/bots.ts","../src/bot-ranges.ts","../src/cidr.ts","../src/webbotauth.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","DIRECTORY_PATH","KEY_CACHE","parseSfString","raw","m","parseSignatureInput","header","label","inner","paramText","components","p","kv","k","vRaw","v","str","parseSignature","bin","buf","buildSignatureBase","req","input","url","lines","component","name","loadKeys","origin","opts","fetchImpl","ttl","res","body","keys","jwk","key","jwkThumbprint","canonical","digest","b","cachedKeys","hit","c","clearKeyCache","verifyWebBotAuth","agentHeader","inputHeader","sigHeader","signerOrigin","u","now","maxAge","base","sig","webBotAuthVerifier","CLIENT_SIDE_AGENT_PATTERN","SERVER_SIDE_CRAWLER_PATTERN","COMPILED","rangesFor","vendor","verifiableVendors","verifyBotIdentity","ua","claimed","isKnownCrawler","inRange","clientIpFromRequest","first","verifyRequest","combinedVerifier","signed","byRange"],"mappings":"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mBAAA,CACA,mBAAA,CACA,iBAAA,CACA,kBAAA,CACA,oBACA,kBAAA,CACA,kBAAA,CACA,oBAAA,CACA,oBAAA,CACA,oBAAA,CACA,oBAAA,CACA,qBACA,mBAAA,CACA,oBAAA,CACA,mBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBACA,oBAAA,CACA,oBAAA,CACA,oBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,qBACA,oBAAA,CACA,oBAAA,CACA,mBAAA,CACA,kBAAA,CACA,oBAAA,CACA,oBAAA,CACA,oBACA,mBAAA,CACA,mBAAA,CACA,oBAAA,CACA,oBAAA,CACA,kBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,oBAAA,CACA,iBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,qBACA,kBAAA,CACA,mBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBAAA,CACA,qBACA,oBAAA,CACA,gBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBAAA,CACA,mBAAA,CACA,oBACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,iBAAA,CACA,gBAAA,CACA,mBACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,gBAAA,CACA,kBAAA,CACA,mBACA,iBAAA,CACA,iBAAA,CACA,gBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,kBACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,kBAAA,CACA,iBAAA,CACA,oBACA,iBAAA,CACA,iBAAA,CACA,eAAA,CACA,iBAAA,CACA,mBAAA,CACA,iBAAA,CACA,oBACA,kBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,iBAAA,CACA,kBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,gBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,kBAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBACA,kBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,iBAAA,CACA,gBACA,gBAAA,CACA,gBAAA,CACA,gBAAA,CACA,gBAAA,CACA,iBAAA,CACA,iBAAA,CACA,kBACA,gBAAA,CACA,kBAAA,CACA,gBAAA,CACA,iBAAA,CACA,iBAAA,CACA,eAAA,CACA,mBACA,mBAAA,CACA,gBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,gBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,kBACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,gBAAA,CACA,kBAAA,CACA,gBAAA,CACA,eAAA,CACA,gBAAA,CACA,gBAAA,CACA,gBAAA,CACA,gBAAA,CACA,gBAAA,CACA,iBACA,eAAA,CACA,iBAAA,CACA,gBAAA,CACA,gBAAA,CACA,iBAAA,CACA,kBAAA,CACA,mBACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,mBACA,kBAAA,CACA,kBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBAAA,CACA,mBAAA,CACA,oBACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,mBACA,mBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,kBACA,kBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,mBAAA,CACA,kBAAA,CACA,mBACA,kBAAA,CACA,mBAAA,CACA,iBAAA,CACA,kBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBAAA,CACA,mBAAA,CACA,kBAAA,CACA,kBACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,kBAAA,CACA,oBACA,kBAAA,CACA,iBAAA,CACA,eAAA,CACA,iBAAA,CACA,eAAA,CACA,iBAAA,CACA,gBACA,iBAAA,CACA,eAAA,CACA,eAAA,CACA,iBAAA,CACA,eAAA,CACA,iBAAA,CACA,gBACA,iBAAA,CACA,eAAA,CACA,gBAAA,CACA,eAAA,CACA,eAAA,CACA,eAAA,CACA,gBACA,mBAAA,CACA,cAAA,CACA,iBAAA,CACA,iBAAA,CACA,iBAAA,CACA,kBAAA,CACA,kBAAA,CACA,iBAAA,CACA,gBAAA,CACA,gBAAA,CACA,iBAAA,CACA,iBAAA,CACA,eAAA,CACA,kBAAA,CACA,kBACA,iBAAA,CACA,gBAAA,CACA,gBACF,CAAA,CACA,MAAA,CAAQ,CACN,qBACA,iBAAA,CACA,gBAAA,CACA,kBAAA,CACA,kBAAA,CACA,mBAAA,CACA,kBAAA,CACA,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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","/**\n * Web Bot Auth — cryptographic agent verification.\n *\n * An RFC 9421 HTTP Message Signatures profile, backed by Cloudflare, Amazon,\n * Akamai and OpenAI, with an IETF working group chartered in 2026. An agent\n * signs each request with Ed25519 and publishes its public keys at a\n * well-known JWKS directory, so a site can verify the claim without knowing\n * anything about the agent in advance.\n *\n * This strictly dominates the published-IP-range check in `verify.ts`:\n *\n * published ranges Web Bot Auth\n * 4 vendors any agent that signs\n * rots, needs refresh CI no freshness problem\n * stale list -> false cannot produce a false 'spoofed'\n * 'spoofed' on real bots\n * can't cover agents on signable from anywhere\n * the user's own machine\n *\n * Ranges remain the fallback for vendors that have not adopted signing yet.\n *\n * Three headers carry the proof:\n * Signature-Agent: \"https://operator.example.com\" <- key directory origin\n * Signature-Input: sig=(...);keyid=\"...\";tag=\"web-bot-auth\";created=...\n * Signature: sig=:base64:\n */\n\nimport type { BotVerificationLike } from './types.js'\n\n/** Where a signer publishes its keys, per the profile. */\nconst DIRECTORY_PATH = '/.well-known/http-message-signatures-directory'\n\nexport type WebBotAuthVerdict =\n | 'verified'\n | 'invalid-signature'\n | 'unknown-key'\n | 'expired'\n | 'malformed'\n | 'not-signed'\n\nexport interface WebBotAuthResult {\n verdict: WebBotAuthVerdict\n /** Origin from `Signature-Agent`, when the request carried one. */\n signerOrigin?: string\n /** JWK thumbprint the signature claimed. */\n keyId?: string\n}\n\nexport interface WebBotAuthOptions {\n /** Override `fetch` (tests, pinned runtimes). */\n fetchImpl?: typeof fetch\n /** How long to cache a signer's key directory. Defaults to 1 hour. */\n keyTtlMs?: number\n /** Reject signatures older than this, independent of `expires`. Default 5 min. */\n maxAgeSeconds?: number\n /**\n * Restrict which origins may sign. Anything else is `unknown-key`. Leave\n * unset to accept any signer that presents a valid signature over keys it\n * publishes — the signature proves control of the origin, not that you want\n * to hear from it.\n */\n allowedSigners?: readonly string[]\n}\n\ninterface CachedKeys {\n keys: Map<string, CryptoKey>\n expiresAt: number\n}\n\nconst KEY_CACHE = new Map<string, Promise<CachedKeys>>()\n\n/* -------------------------------------------------------------------------\n * Structured-field parsing\n *\n * A full RFC 8941 parser is far more than this profile needs. These handle the\n * shapes Web Bot Auth actually emits, and return null rather than guessing on\n * anything else — a malformed header must never read as a valid signature.\n * ---------------------------------------------------------------------- */\n\n/** `\"https://example.com\"` -> `https://example.com` */\nfunction parseSfString(raw: string | null): string | null {\n if (!raw) return null\n const m = raw.trim().match(/^\"([^\"]*)\"$/)\n return m ? (m[1] ?? null) : null\n}\n\nexport interface SignatureInput {\n label: string\n /** Covered component identifiers, in signing order. */\n components: string[]\n keyid?: string\n alg?: string\n tag?: string\n created?: number\n expires?: number\n /** The raw inner-list + params text, needed verbatim for the signature base. */\n raw: string\n}\n\n/** Parse `label=(\"a\" \"b\");keyid=\"k\";tag=\"web-bot-auth\"`. */\nexport function parseSignatureInput(header: string | null): SignatureInput | null {\n if (!header) return null\n const m = header.trim().match(/^([A-Za-z0-9_-]+)=(\\((.*?)\\)(.*))$/)\n if (!m) return null\n const [, label, raw, inner, paramText] = m\n if (!label || raw === undefined) return null\n\n const components = (inner ?? '').match(/\"[^\"]*\"(?:;[^ )]*)?/g)?.map((s) => s) ?? []\n\n const out: SignatureInput = { label, components, raw }\n for (const p of (paramText ?? '').split(';')) {\n const kv = p.match(/^([a-z]+)=(.*)$/)\n if (!kv) continue\n const [, k, vRaw] = kv\n const v = vRaw ?? ''\n const str = v.startsWith('\"') ? v.slice(1, -1) : v\n if (k === 'keyid') out.keyid = str\n else if (k === 'alg') out.alg = str\n else if (k === 'tag') out.tag = str\n else if (k === 'created') out.created = Number(str)\n else if (k === 'expires') out.expires = Number(str)\n }\n return out\n}\n\n/** Parse `label=:base64:` into raw signature bytes. */\nexport function parseSignature(header: string | null, label: string): ArrayBuffer | null {\n if (!header) return null\n const m = header.match(new RegExp(`(?:^|,)\\\\s*${label}=:([A-Za-z0-9+/=]+):`))\n if (!m?.[1]) return null\n try {\n const bin = atob(m[1])\n const buf = new ArrayBuffer(bin.length)\n const out = new Uint8Array(buf)\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)\n return buf\n } catch {\n return null\n }\n}\n\n/**\n * Rebuild the RFC 9421 signature base: one line per covered component, then\n * the `@signature-params` line carrying the inner list verbatim.\n */\nexport function buildSignatureBase(req: Request, input: SignatureInput): string | null {\n const url = new URL(req.url)\n const lines: string[] = []\n\n for (const component of input.components) {\n const name = component.match(/^\"([^\"]*)\"/)?.[1]\n if (name === undefined) return null\n let value: string | null\n\n switch (name) {\n case '@method':\n value = req.method.toUpperCase()\n break\n case '@authority':\n value = url.host\n break\n case '@scheme':\n value = url.protocol.replace(':', '')\n break\n case '@target-uri':\n value = url.toString()\n break\n case '@path':\n value = url.pathname\n break\n case '@query':\n value = url.search || '?'\n break\n default:\n // Anything else is a header name, lowercase per the spec.\n if (name.startsWith('@')) return null // derived component we don't model\n value = req.headers.get(name)\n break\n }\n\n // A covered component the request doesn't carry makes the base\n // unreconstructable — that is a verification failure, not a skip.\n if (value === null) return null\n lines.push(`${component}: ${value.trim()}`)\n }\n\n lines.push(`\"@signature-params\": ${input.raw}`)\n return lines.join('\\n')\n}\n\n/* ------------------------------------------------------------------------ */\n\nasync function loadKeys(\n origin: string,\n opts: WebBotAuthOptions\n): Promise<CachedKeys> {\n const fetchImpl = opts.fetchImpl ?? fetch\n const ttl = opts.keyTtlMs ?? 3_600_000\n const res = await fetchImpl(`${origin}${DIRECTORY_PATH}`, {\n headers: { accept: 'application/http-message-signatures-directory+json, application/json' }\n })\n if (!res.ok) throw new Error(`key directory ${res.status}`)\n const body = (await res.json()) as { keys?: unknown[] }\n const keys = new Map<string, CryptoKey>()\n\n for (const raw of body.keys ?? []) {\n const jwk = raw as JsonWebKey & { kid?: string }\n if (jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519') continue\n try {\n const key = await crypto.subtle.importKey('jwk', jwk, { name: 'Ed25519' }, false, ['verify'])\n // Index by both the advertised kid and the RFC 7638 thumbprint, since\n // the profile identifies keys by thumbprint but directories often also\n // publish a kid.\n if (jwk.kid) keys.set(jwk.kid, key)\n keys.set(await jwkThumbprint(jwk), key)\n } catch {\n // A single unusable key must not poison the whole directory.\n }\n }\n return { keys, expiresAt: Date.now() + ttl }\n}\n\n/** RFC 7638 JWK thumbprint, base64url of SHA-256 over the canonical members. */\nexport async function jwkThumbprint(jwk: JsonWebKey): Promise<string> {\n const canonical = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x })\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(canonical))\n let bin = ''\n for (const b of new Uint8Array(digest)) bin += String.fromCharCode(b)\n return btoa(bin).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nfunction cachedKeys(origin: string, opts: WebBotAuthOptions): Promise<CachedKeys> {\n const hit = KEY_CACHE.get(origin)\n if (hit) {\n // Re-check expiry once resolved; a rejected or stale entry is dropped.\n return hit\n .then((c) => {\n if (c.expiresAt > Date.now()) return c\n KEY_CACHE.delete(origin)\n return cachedKeys(origin, opts)\n })\n .catch(() => {\n KEY_CACHE.delete(origin)\n return loadKeys(origin, opts)\n })\n }\n const p = loadKeys(origin, opts)\n KEY_CACHE.set(origin, p)\n // Don't cache a failed fetch.\n p.catch(() => KEY_CACHE.delete(origin))\n return p\n}\n\n/** Drop cached signer keys. Exposed for tests and key-rotation handling. */\nexport function clearKeyCache(): void {\n KEY_CACHE.clear()\n}\n\n/**\n * Verify a request's Web Bot Auth signature.\n *\n * Requires a network fetch the first time a signer is seen; keys are then\n * cached per origin for `keyTtlMs`. Unsigned requests return `'not-signed'`\n * immediately with no I/O, which is the overwhelmingly common path today.\n */\nexport async function verifyWebBotAuth(\n req: Request,\n opts: WebBotAuthOptions = {}\n): Promise<WebBotAuthResult> {\n const agentHeader = req.headers.get('signature-agent')\n const inputHeader = req.headers.get('signature-input')\n const sigHeader = req.headers.get('signature')\n if (!inputHeader || !sigHeader) return { verdict: 'not-signed' }\n\n const input = parseSignatureInput(inputHeader)\n if (!input) return { verdict: 'malformed' }\n // The tag scopes a signature to bot authentication. Without it, this is some\n // other RFC 9421 use and not ours to judge.\n if (input.tag && input.tag !== 'web-bot-auth') return { verdict: 'not-signed' }\n\n const signerOrigin = parseSfString(agentHeader)\n if (!signerOrigin) return { verdict: 'malformed' }\n let origin: string\n try {\n const u = new URL(signerOrigin)\n if (u.protocol !== 'https:') return { verdict: 'malformed' }\n origin = u.origin\n } catch {\n return { verdict: 'malformed' }\n }\n\n if (opts.allowedSigners && !opts.allowedSigners.includes(origin)) {\n return { verdict: 'unknown-key', signerOrigin: origin, ...(input.keyid ? { keyId: input.keyid } : {}) }\n }\n\n const now = Math.floor(Date.now() / 1000)\n const maxAge = opts.maxAgeSeconds ?? 300\n if (input.expires !== undefined && input.expires < now) {\n return { verdict: 'expired', signerOrigin: origin }\n }\n if (input.created !== undefined && now - input.created > maxAge) {\n return { verdict: 'expired', signerOrigin: origin }\n }\n\n const base = buildSignatureBase(req, input)\n if (!base) return { verdict: 'malformed', signerOrigin: origin }\n\n const sig = parseSignature(sigHeader, input.label)\n if (!sig) return { verdict: 'malformed', signerOrigin: origin }\n\n let keys: CachedKeys\n try {\n keys = await cachedKeys(origin, opts)\n } catch {\n // Directory unreachable — we cannot judge, and must not call it invalid.\n return { verdict: 'unknown-key', signerOrigin: origin }\n }\n\n const key = input.keyid ? keys.keys.get(input.keyid) : undefined\n if (!key) {\n return { verdict: 'unknown-key', signerOrigin: origin, ...(input.keyid ? { keyId: input.keyid } : {}) }\n }\n\n const ok = await crypto.subtle.verify(\n { name: 'Ed25519' },\n key,\n sig,\n new TextEncoder().encode(base)\n )\n return {\n verdict: ok ? 'verified' : 'invalid-signature',\n signerOrigin: origin,\n ...(input.keyid ? { keyId: input.keyid } : {})\n }\n}\n\n/**\n * Adapter to the shared verdict shape used by `trackVisit` and `agentPolicy`.\n *\n * Deliberately conservative about `spoofed`: only a signature that is present\n * and fails cryptographically earns it. A missing signature is `unverifiable`,\n * because most agents do not sign yet and treating silence as forgery would\n * mislabel nearly all real traffic.\n */\nexport function webBotAuthVerifier(\n opts: WebBotAuthOptions = {}\n): (req: Request) => Promise<BotVerificationLike> {\n return async (req: Request): Promise<BotVerificationLike> => {\n const r = await verifyWebBotAuth(req, opts)\n switch (r.verdict) {\n case 'verified':\n return { verdict: 'verified', verified: true, reason: 'web-bot-auth' }\n case 'invalid-signature':\n return { verdict: 'spoofed', verified: false, reason: 'web-bot-auth-signature-invalid' }\n case 'expired':\n return { verdict: 'spoofed', verified: false, reason: 'web-bot-auth-expired' }\n case 'malformed':\n case 'unknown-key':\n return { verdict: 'unverifiable', verified: null, reason: `web-bot-auth-${r.verdict}` }\n default:\n return { verdict: 'unverifiable', verified: null, reason: 'not-signed' }\n }\n }\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\nexport {\n clearKeyCache,\n jwkThumbprint,\n verifyWebBotAuth,\n webBotAuthVerifier\n} from './webbotauth.js'\nexport type { WebBotAuthOptions, WebBotAuthResult, WebBotAuthVerdict } from './webbotauth.js'\n\nimport { verifyWebBotAuth, type WebBotAuthOptions } from './webbotauth.js'\nimport type { BotVerificationLike } from './types.js'\n\n/**\n * Verifier that prefers a cryptographic signature and falls back to published\n * IP ranges.\n *\n * Ordering matters. Web Bot Auth proves control of a signing key, works for\n * any agent that adopts it, and cannot go stale. IP ranges cover four vendors,\n * rot between refreshes, and cannot see an agent running on a user's own\n * machine. So a signature — valid or invalid — is always the answer when one\n * is present; ranges only speak when the request is unsigned.\n *\n * As signing adoption grows this quietly shifts from mostly-ranges to\n * mostly-signatures with no change at the call site.\n */\nexport function combinedVerifier(\n opts: WebBotAuthOptions = {}\n): (req: Request) => Promise<BotVerificationLike> {\n return async (req: Request): Promise<BotVerificationLike> => {\n const signed = await verifyWebBotAuth(req, opts)\n\n if (signed.verdict === 'verified') {\n return { verdict: 'verified', verified: true, reason: 'web-bot-auth' }\n }\n // A signature that is present and fails is decisive — do not let a lucky\n // IP-range hit launder a forged signature into 'verified'.\n if (signed.verdict === 'invalid-signature' || signed.verdict === 'expired') {\n return { verdict: 'spoofed', verified: false, reason: `web-bot-auth-${signed.verdict}` }\n }\n\n const byRange = verifyRequest(req)\n if (byRange.verdict !== 'not-claimed' && byRange.verdict !== 'unverifiable') {\n return { ...byRange, reason: byRange.reason ?? 'published-ip-range' }\n }\n return byRange\n }\n}\n"]}
package/dist/verify.d.cts CHANGED
@@ -1,3 +1,77 @@
1
+ import { B as BotVerificationLike } from './types-Dw43eu7D.cjs';
2
+
3
+ /**
4
+ * Web Bot Auth — cryptographic agent verification.
5
+ *
6
+ * An RFC 9421 HTTP Message Signatures profile, backed by Cloudflare, Amazon,
7
+ * Akamai and OpenAI, with an IETF working group chartered in 2026. An agent
8
+ * signs each request with Ed25519 and publishes its public keys at a
9
+ * well-known JWKS directory, so a site can verify the claim without knowing
10
+ * anything about the agent in advance.
11
+ *
12
+ * This strictly dominates the published-IP-range check in `verify.ts`:
13
+ *
14
+ * published ranges Web Bot Auth
15
+ * 4 vendors any agent that signs
16
+ * rots, needs refresh CI no freshness problem
17
+ * stale list -> false cannot produce a false 'spoofed'
18
+ * 'spoofed' on real bots
19
+ * can't cover agents on signable from anywhere
20
+ * the user's own machine
21
+ *
22
+ * Ranges remain the fallback for vendors that have not adopted signing yet.
23
+ *
24
+ * Three headers carry the proof:
25
+ * Signature-Agent: "https://operator.example.com" <- key directory origin
26
+ * Signature-Input: sig=(...);keyid="...";tag="web-bot-auth";created=...
27
+ * Signature: sig=:base64:
28
+ */
29
+
30
+ type WebBotAuthVerdict = 'verified' | 'invalid-signature' | 'unknown-key' | 'expired' | 'malformed' | 'not-signed';
31
+ interface WebBotAuthResult {
32
+ verdict: WebBotAuthVerdict;
33
+ /** Origin from `Signature-Agent`, when the request carried one. */
34
+ signerOrigin?: string;
35
+ /** JWK thumbprint the signature claimed. */
36
+ keyId?: string;
37
+ }
38
+ interface WebBotAuthOptions {
39
+ /** Override `fetch` (tests, pinned runtimes). */
40
+ fetchImpl?: typeof fetch;
41
+ /** How long to cache a signer's key directory. Defaults to 1 hour. */
42
+ keyTtlMs?: number;
43
+ /** Reject signatures older than this, independent of `expires`. Default 5 min. */
44
+ maxAgeSeconds?: number;
45
+ /**
46
+ * Restrict which origins may sign. Anything else is `unknown-key`. Leave
47
+ * unset to accept any signer that presents a valid signature over keys it
48
+ * publishes — the signature proves control of the origin, not that you want
49
+ * to hear from it.
50
+ */
51
+ allowedSigners?: readonly string[];
52
+ }
53
+ /** RFC 7638 JWK thumbprint, base64url of SHA-256 over the canonical members. */
54
+ declare function jwkThumbprint(jwk: JsonWebKey): Promise<string>;
55
+ /** Drop cached signer keys. Exposed for tests and key-rotation handling. */
56
+ declare function clearKeyCache(): void;
57
+ /**
58
+ * Verify a request's Web Bot Auth signature.
59
+ *
60
+ * Requires a network fetch the first time a signer is seen; keys are then
61
+ * cached per origin for `keyTtlMs`. Unsigned requests return `'not-signed'`
62
+ * immediately with no I/O, which is the overwhelmingly common path today.
63
+ */
64
+ declare function verifyWebBotAuth(req: Request, opts?: WebBotAuthOptions): Promise<WebBotAuthResult>;
65
+ /**
66
+ * Adapter to the shared verdict shape used by `trackVisit` and `agentPolicy`.
67
+ *
68
+ * Deliberately conservative about `spoofed`: only a signature that is present
69
+ * and fails cryptographically earns it. A missing signature is `unverifiable`,
70
+ * because most agents do not sign yet and treating silence as forgery would
71
+ * mislabel nearly all real traffic.
72
+ */
73
+ declare function webBotAuthVerifier(opts?: WebBotAuthOptions): (req: Request) => Promise<BotVerificationLike>;
74
+
1
75
  /**
2
76
  * Verdict on whether a request's claimed crawler identity holds up against the
3
77
  * vendor's published IP ranges.
@@ -47,4 +121,19 @@ declare function clientIpFromRequest(req: Request): string;
47
121
  /** Verify straight from a request object. */
48
122
  declare function verifyRequest(req: Request): BotVerification;
49
123
 
50
- export { type BotVerification, type UnverifiableReason, type VerificationVerdict, clientIpFromRequest, verifiableVendors, verifyBotIdentity, verifyRequest };
124
+ /**
125
+ * Verifier that prefers a cryptographic signature and falls back to published
126
+ * IP ranges.
127
+ *
128
+ * Ordering matters. Web Bot Auth proves control of a signing key, works for
129
+ * any agent that adopts it, and cannot go stale. IP ranges cover four vendors,
130
+ * rot between refreshes, and cannot see an agent running on a user's own
131
+ * machine. So a signature — valid or invalid — is always the answer when one
132
+ * is present; ranges only speak when the request is unsigned.
133
+ *
134
+ * As signing adoption grows this quietly shifts from mostly-ranges to
135
+ * mostly-signatures with no change at the call site.
136
+ */
137
+ declare function combinedVerifier(opts?: WebBotAuthOptions): (req: Request) => Promise<BotVerificationLike>;
138
+
139
+ export { type BotVerification, type UnverifiableReason, type VerificationVerdict, type WebBotAuthOptions, type WebBotAuthResult, type WebBotAuthVerdict, clearKeyCache, clientIpFromRequest, combinedVerifier, jwkThumbprint, verifiableVendors, verifyBotIdentity, verifyRequest, verifyWebBotAuth, webBotAuthVerifier };
package/dist/verify.d.ts CHANGED
@@ -1,3 +1,77 @@
1
+ import { B as BotVerificationLike } from './types-Dw43eu7D.js';
2
+
3
+ /**
4
+ * Web Bot Auth — cryptographic agent verification.
5
+ *
6
+ * An RFC 9421 HTTP Message Signatures profile, backed by Cloudflare, Amazon,
7
+ * Akamai and OpenAI, with an IETF working group chartered in 2026. An agent
8
+ * signs each request with Ed25519 and publishes its public keys at a
9
+ * well-known JWKS directory, so a site can verify the claim without knowing
10
+ * anything about the agent in advance.
11
+ *
12
+ * This strictly dominates the published-IP-range check in `verify.ts`:
13
+ *
14
+ * published ranges Web Bot Auth
15
+ * 4 vendors any agent that signs
16
+ * rots, needs refresh CI no freshness problem
17
+ * stale list -> false cannot produce a false 'spoofed'
18
+ * 'spoofed' on real bots
19
+ * can't cover agents on signable from anywhere
20
+ * the user's own machine
21
+ *
22
+ * Ranges remain the fallback for vendors that have not adopted signing yet.
23
+ *
24
+ * Three headers carry the proof:
25
+ * Signature-Agent: "https://operator.example.com" <- key directory origin
26
+ * Signature-Input: sig=(...);keyid="...";tag="web-bot-auth";created=...
27
+ * Signature: sig=:base64:
28
+ */
29
+
30
+ type WebBotAuthVerdict = 'verified' | 'invalid-signature' | 'unknown-key' | 'expired' | 'malformed' | 'not-signed';
31
+ interface WebBotAuthResult {
32
+ verdict: WebBotAuthVerdict;
33
+ /** Origin from `Signature-Agent`, when the request carried one. */
34
+ signerOrigin?: string;
35
+ /** JWK thumbprint the signature claimed. */
36
+ keyId?: string;
37
+ }
38
+ interface WebBotAuthOptions {
39
+ /** Override `fetch` (tests, pinned runtimes). */
40
+ fetchImpl?: typeof fetch;
41
+ /** How long to cache a signer's key directory. Defaults to 1 hour. */
42
+ keyTtlMs?: number;
43
+ /** Reject signatures older than this, independent of `expires`. Default 5 min. */
44
+ maxAgeSeconds?: number;
45
+ /**
46
+ * Restrict which origins may sign. Anything else is `unknown-key`. Leave
47
+ * unset to accept any signer that presents a valid signature over keys it
48
+ * publishes — the signature proves control of the origin, not that you want
49
+ * to hear from it.
50
+ */
51
+ allowedSigners?: readonly string[];
52
+ }
53
+ /** RFC 7638 JWK thumbprint, base64url of SHA-256 over the canonical members. */
54
+ declare function jwkThumbprint(jwk: JsonWebKey): Promise<string>;
55
+ /** Drop cached signer keys. Exposed for tests and key-rotation handling. */
56
+ declare function clearKeyCache(): void;
57
+ /**
58
+ * Verify a request's Web Bot Auth signature.
59
+ *
60
+ * Requires a network fetch the first time a signer is seen; keys are then
61
+ * cached per origin for `keyTtlMs`. Unsigned requests return `'not-signed'`
62
+ * immediately with no I/O, which is the overwhelmingly common path today.
63
+ */
64
+ declare function verifyWebBotAuth(req: Request, opts?: WebBotAuthOptions): Promise<WebBotAuthResult>;
65
+ /**
66
+ * Adapter to the shared verdict shape used by `trackVisit` and `agentPolicy`.
67
+ *
68
+ * Deliberately conservative about `spoofed`: only a signature that is present
69
+ * and fails cryptographically earns it. A missing signature is `unverifiable`,
70
+ * because most agents do not sign yet and treating silence as forgery would
71
+ * mislabel nearly all real traffic.
72
+ */
73
+ declare function webBotAuthVerifier(opts?: WebBotAuthOptions): (req: Request) => Promise<BotVerificationLike>;
74
+
1
75
  /**
2
76
  * Verdict on whether a request's claimed crawler identity holds up against the
3
77
  * vendor's published IP ranges.
@@ -47,4 +121,19 @@ declare function clientIpFromRequest(req: Request): string;
47
121
  /** Verify straight from a request object. */
48
122
  declare function verifyRequest(req: Request): BotVerification;
49
123
 
50
- export { type BotVerification, type UnverifiableReason, type VerificationVerdict, clientIpFromRequest, verifiableVendors, verifyBotIdentity, verifyRequest };
124
+ /**
125
+ * Verifier that prefers a cryptographic signature and falls back to published
126
+ * IP ranges.
127
+ *
128
+ * Ordering matters. Web Bot Auth proves control of a signing key, works for
129
+ * any agent that adopts it, and cannot go stale. IP ranges cover four vendors,
130
+ * rot between refreshes, and cannot see an agent running on a user's own
131
+ * machine. So a signature — valid or invalid — is always the answer when one
132
+ * is present; ranges only speak when the request is unsigned.
133
+ *
134
+ * As signing adoption grows this quietly shifts from mostly-ranges to
135
+ * mostly-signatures with no change at the call site.
136
+ */
137
+ declare function combinedVerifier(opts?: WebBotAuthOptions): (req: Request) => Promise<BotVerificationLike>;
138
+
139
+ export { type BotVerification, type UnverifiableReason, type VerificationVerdict, type WebBotAuthOptions, type WebBotAuthResult, type WebBotAuthVerdict, clearKeyCache, clientIpFromRequest, combinedVerifier, jwkThumbprint, verifiableVendors, verifyBotIdentity, verifyRequest, verifyWebBotAuth, webBotAuthVerifier };