@walkeros/server-transformer-bot 4.4.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,11 +1,29 @@
1
1
  import { Mapping, Transformer } from '@walkeros/core';
2
2
 
3
+ /**
4
+ * Request-context model.
5
+ *
6
+ * Every published bot heuristic carries an unstated assumption about how the
7
+ * request was made, and almost all of them assume a top-level navigation. A
8
+ * collector sees beacons, pixels, fetches and server-to-server posts, where the
9
+ * same header value can mean the opposite thing. a wildcard `Accept` from a
10
+ * browser UA is normal on a beacon and anomalous on a pixel.
11
+ *
12
+ * `auto` never determines a context. Absence of `Sec-Fetch-*` is both a signal
13
+ * worth scoring and the reason auto-derivation fails, and the request method
14
+ * does not rescue it: a GET is a pixel, a navigation or a `fetch()` GET, and
15
+ * the first two expect a typed `Accept` while the third correctly sends a wildcard.
16
+ * So context-dependent checks run only under a pinned context. The failure mode
17
+ * is "we scored less", never "we scored wrong".
18
+ */
19
+ type BotContext = 'auto' | 'navigation' | 'pixel' | 'beacon' | 'fetch' | 'server';
20
+
3
21
  /**
4
22
  * Input signal sources. Each is a Mapping.Value resolved via getMappingValue
5
23
  * against { event, ingest }.
6
24
  *
7
- * v1 only reads `userAgent`. The remaining fields are reserved for v1.1
8
- * (header consistency heuristics) so the public schema stays stable.
25
+ * `ja4` and `headerNames` are reserved: they are resolved and passed through,
26
+ * but no heuristic consumes them.
9
27
  */
10
28
  interface BotInput {
11
29
  userAgent?: Mapping.Value;
@@ -19,22 +37,144 @@ interface BotInput {
19
37
  secChUa?: Mapping.Value;
20
38
  secChUaMobile?: Mapping.Value;
21
39
  secChUaPlatform?: Mapping.Value;
40
+ accept?: Mapping.Value;
41
+ contentType?: Mapping.Value;
42
+ referer?: Mapping.Value;
43
+ signatureAgent?: Mapping.Value;
44
+ method?: Mapping.Value;
45
+ ja4?: Mapping.Value;
46
+ headerNames?: Mapping.Value;
22
47
  }
23
48
  /**
24
- * Output paths. Empty string or omitted = skip writing that field.
49
+ * Output paths. `false` disables a field, any dot path renames and reroutes it.
25
50
  * Paths starting with `ingest.` route to context.ingest (pipeline scratch);
26
51
  * everything else routes to the event.
27
52
  */
28
53
  interface BotOutput {
29
- botScore?: string;
30
- agentScore?: string;
31
- agentProduct?: string;
54
+ botScore?: string | false;
55
+ botCategory?: string | false;
56
+ botProduct?: string | false;
57
+ botReasons?: string | false;
32
58
  }
33
59
  interface BotSettings {
34
60
  input?: BotInput;
35
61
  output?: BotOutput;
62
+ /**
63
+ * How the request reached the collector. An enum literal pins one context
64
+ * for every request; any other Mapping.Value (dot-path string, {key},
65
+ * {value}, {fn}, or a fallback array) resolves per request against
66
+ * { event, ingest }. A result that is not a valid context yields "auto".
67
+ */
68
+ context?: BotContext | Mapping.Value;
69
+ /** Graded-layer cut between `human` and `suspicious`. */
70
+ suspiciousAt?: number;
36
71
  }
37
72
 
38
73
  declare const transformerBot: Transformer.Init<Transformer.Types<BotSettings>>;
39
74
 
40
- export { type BotInput, type BotOutput, type BotSettings, transformerBot as default, transformerBot };
75
+ /**
76
+ * Curated non-AI crawler UA-substring map (2026-Q3).
77
+ *
78
+ * Sibling of `agents.ts`, which holds the AI agents and AI crawlers. The two
79
+ * files stay separate on purpose: an AhrefsBot hit and a GPTBot hit answer
80
+ * different questions. `agents.ts` is scanned FIRST, this file second; adding a
81
+ * row whose match is a substring of an AI token would therefore be shadowed.
82
+ *
83
+ * Each entry: a substring matched case-insensitively against the User-Agent, a
84
+ * product label, and the category written to botCategory.
85
+ *
86
+ * Category semantics:
87
+ * - 'search-crawler' — a search engine index; correlates with organic discoverability
88
+ * - 'seo-tool' — third-party commercial crawler building its own data set
89
+ * - 'monitor' — uptime and synthetic monitoring, usually the site owner's own
90
+ * - 'link-preview' — link unfurler, meaning a person just shared this URL
91
+ *
92
+ * Order matters: first-hit wins. More-specific entries must precede broader
93
+ * ones. Three orderings are load-bearing and non-obvious:
94
+ * - `adidxbot` before `bingbot`: every adidxbot UA carries the literal string
95
+ * `bingbot` in its trailing info URL.
96
+ * - `TelegramBot` before `Twitterbot`: Telegram's UA is `TelegramBot (like TwitterBot)`.
97
+ * - `Googlebot` last in the Google block: it is a substring of `Googlebot-Image/1.0`
98
+ * and `Googlebot-Video/1.0`.
99
+ *
100
+ * A UA match is a claim, not proof: Screaming Frog ships Googlebot and Bingbot
101
+ * presets, and any client can send any UA. Vendor IP ranges and reverse DNS
102
+ * suffixes for the entries that publish them are listed in
103
+ * `docs/research/2026-08-20-search-crawler-list.md` and are consumed once
104
+ * identity verification exists.
105
+ *
106
+ * Vendor docs of record (verified 2026-08):
107
+ * Google: https://developers.google.com/search/docs/crawling-indexing/google-common-crawlers
108
+ * Microsoft: https://www.bing.com/webmasters/help/which-crawlers-does-bing-use-8c184ec0
109
+ * Apple: https://support.apple.com/en-us/119829
110
+ * DuckDuckGo: https://duckduckgo.com/duckduckbot
111
+ * Yandex: https://yandex.com/support/webmaster/robot-workings/check-yandex-robots.html
112
+ * Baidu: http://help.baidu.com/question?prod_en=master&class=Baiduspider
113
+ * Huawei: https://aspiegel.com/petalbot
114
+ * Yahoo: https://help.yahoo.com/kb/SLN22600.html
115
+ * Ahrefs: https://ahrefs.com/robot
116
+ * Semrush: https://www.semrush.com/bot/
117
+ * Majestic: https://mj12bot.com/
118
+ * Screaming Frog: https://www.screamingfrog.co.uk/seo-spider/user-guide/configuration/
119
+ * UptimeRobot: https://help.uptimerobot.com/en/articles/11358489-what-is-the-uptimerobot-user-agent-string
120
+ * Meta: https://developers.facebook.com/docs/sharing/webmasters/web-crawlers
121
+ * X: https://developer.x.com/en/docs/x-for-websites/cards/guides/getting-started
122
+ * Slack: https://api.slack.com/robots
123
+ * WhatsApp: https://developers.facebook.com/documentation/business-messaging/whatsapp/link-previews/
124
+ *
125
+ * Entries without a readable vendor page (SeznamBot, Yeti, Discordbot,
126
+ * TelegramBot, Pingdom, StatusCake, DotBot) rest on the self-reference in the
127
+ * UA string itself. Reviewed quarterly.
128
+ *
129
+ * Deliberately excluded: `Googlebot-News`, `Google-Extended` and
130
+ * `Applebot-Extended` are robots.txt directives that never appear in a UA
131
+ * header. `Google-Agent` and `Google-GeminiNotebook` are AI agents and live in
132
+ * `agents.ts`.
133
+ */
134
+ type CrawlerCategory = 'search-crawler' | 'seo-tool' | 'monitor' | 'link-preview';
135
+
136
+ /**
137
+ * Request signals handed to the scorer. An absent value and an empty string
138
+ * are equivalent: both mean the signal was not present on the request.
139
+ */
140
+ interface Signals {
141
+ userAgent?: string;
142
+ ip?: string;
143
+ acceptLanguage?: string;
144
+ acceptEncoding?: string;
145
+ secFetchSite?: string;
146
+ secFetchMode?: string;
147
+ secFetchDest?: string;
148
+ secFetchUser?: string;
149
+ secChUa?: string;
150
+ secChUaMobile?: string;
151
+ secChUaPlatform?: string;
152
+ accept?: string;
153
+ contentType?: string;
154
+ referer?: string;
155
+ signatureAgent?: string;
156
+ method?: string;
157
+ /** Reserved: resolved but not consumed. */
158
+ ja4?: string;
159
+ /** Reserved: resolved but not consumed. */
160
+ headerNames?: string;
161
+ }
162
+ /**
163
+ * What kind of client issued the request. `botScore` says how automated it is,
164
+ * this says what it is.
165
+ */
166
+ type BotCategory = 'human' | 'suspicious' | 'automation' | CrawlerCategory | 'ai-agent' | 'ai-crawler' | 'unknown';
167
+ interface ScoreResult {
168
+ /**
169
+ * Automation likelihood, 0-99, higher = more automated. `null` means not
170
+ * measured, never "human".
171
+ */
172
+ botScore: number | null;
173
+ botCategory: BotCategory;
174
+ /** Identified product, set only when a named detector matched. */
175
+ botProduct?: string;
176
+ /** Stable reason codes. Semver-stable public API. */
177
+ botReasons: string[];
178
+ }
179
+
180
+ export { type BotCategory, type BotContext, type BotInput, type BotOutput, type BotSettings, type ScoreResult, type Signals, transformerBot as default, transformerBot };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,29 @@
1
1
  import { Mapping, Transformer } from '@walkeros/core';
2
2
 
3
+ /**
4
+ * Request-context model.
5
+ *
6
+ * Every published bot heuristic carries an unstated assumption about how the
7
+ * request was made, and almost all of them assume a top-level navigation. A
8
+ * collector sees beacons, pixels, fetches and server-to-server posts, where the
9
+ * same header value can mean the opposite thing. a wildcard `Accept` from a
10
+ * browser UA is normal on a beacon and anomalous on a pixel.
11
+ *
12
+ * `auto` never determines a context. Absence of `Sec-Fetch-*` is both a signal
13
+ * worth scoring and the reason auto-derivation fails, and the request method
14
+ * does not rescue it: a GET is a pixel, a navigation or a `fetch()` GET, and
15
+ * the first two expect a typed `Accept` while the third correctly sends a wildcard.
16
+ * So context-dependent checks run only under a pinned context. The failure mode
17
+ * is "we scored less", never "we scored wrong".
18
+ */
19
+ type BotContext = 'auto' | 'navigation' | 'pixel' | 'beacon' | 'fetch' | 'server';
20
+
3
21
  /**
4
22
  * Input signal sources. Each is a Mapping.Value resolved via getMappingValue
5
23
  * against { event, ingest }.
6
24
  *
7
- * v1 only reads `userAgent`. The remaining fields are reserved for v1.1
8
- * (header consistency heuristics) so the public schema stays stable.
25
+ * `ja4` and `headerNames` are reserved: they are resolved and passed through,
26
+ * but no heuristic consumes them.
9
27
  */
10
28
  interface BotInput {
11
29
  userAgent?: Mapping.Value;
@@ -19,22 +37,144 @@ interface BotInput {
19
37
  secChUa?: Mapping.Value;
20
38
  secChUaMobile?: Mapping.Value;
21
39
  secChUaPlatform?: Mapping.Value;
40
+ accept?: Mapping.Value;
41
+ contentType?: Mapping.Value;
42
+ referer?: Mapping.Value;
43
+ signatureAgent?: Mapping.Value;
44
+ method?: Mapping.Value;
45
+ ja4?: Mapping.Value;
46
+ headerNames?: Mapping.Value;
22
47
  }
23
48
  /**
24
- * Output paths. Empty string or omitted = skip writing that field.
49
+ * Output paths. `false` disables a field, any dot path renames and reroutes it.
25
50
  * Paths starting with `ingest.` route to context.ingest (pipeline scratch);
26
51
  * everything else routes to the event.
27
52
  */
28
53
  interface BotOutput {
29
- botScore?: string;
30
- agentScore?: string;
31
- agentProduct?: string;
54
+ botScore?: string | false;
55
+ botCategory?: string | false;
56
+ botProduct?: string | false;
57
+ botReasons?: string | false;
32
58
  }
33
59
  interface BotSettings {
34
60
  input?: BotInput;
35
61
  output?: BotOutput;
62
+ /**
63
+ * How the request reached the collector. An enum literal pins one context
64
+ * for every request; any other Mapping.Value (dot-path string, {key},
65
+ * {value}, {fn}, or a fallback array) resolves per request against
66
+ * { event, ingest }. A result that is not a valid context yields "auto".
67
+ */
68
+ context?: BotContext | Mapping.Value;
69
+ /** Graded-layer cut between `human` and `suspicious`. */
70
+ suspiciousAt?: number;
36
71
  }
37
72
 
38
73
  declare const transformerBot: Transformer.Init<Transformer.Types<BotSettings>>;
39
74
 
40
- export { type BotInput, type BotOutput, type BotSettings, transformerBot as default, transformerBot };
75
+ /**
76
+ * Curated non-AI crawler UA-substring map (2026-Q3).
77
+ *
78
+ * Sibling of `agents.ts`, which holds the AI agents and AI crawlers. The two
79
+ * files stay separate on purpose: an AhrefsBot hit and a GPTBot hit answer
80
+ * different questions. `agents.ts` is scanned FIRST, this file second; adding a
81
+ * row whose match is a substring of an AI token would therefore be shadowed.
82
+ *
83
+ * Each entry: a substring matched case-insensitively against the User-Agent, a
84
+ * product label, and the category written to botCategory.
85
+ *
86
+ * Category semantics:
87
+ * - 'search-crawler' — a search engine index; correlates with organic discoverability
88
+ * - 'seo-tool' — third-party commercial crawler building its own data set
89
+ * - 'monitor' — uptime and synthetic monitoring, usually the site owner's own
90
+ * - 'link-preview' — link unfurler, meaning a person just shared this URL
91
+ *
92
+ * Order matters: first-hit wins. More-specific entries must precede broader
93
+ * ones. Three orderings are load-bearing and non-obvious:
94
+ * - `adidxbot` before `bingbot`: every adidxbot UA carries the literal string
95
+ * `bingbot` in its trailing info URL.
96
+ * - `TelegramBot` before `Twitterbot`: Telegram's UA is `TelegramBot (like TwitterBot)`.
97
+ * - `Googlebot` last in the Google block: it is a substring of `Googlebot-Image/1.0`
98
+ * and `Googlebot-Video/1.0`.
99
+ *
100
+ * A UA match is a claim, not proof: Screaming Frog ships Googlebot and Bingbot
101
+ * presets, and any client can send any UA. Vendor IP ranges and reverse DNS
102
+ * suffixes for the entries that publish them are listed in
103
+ * `docs/research/2026-08-20-search-crawler-list.md` and are consumed once
104
+ * identity verification exists.
105
+ *
106
+ * Vendor docs of record (verified 2026-08):
107
+ * Google: https://developers.google.com/search/docs/crawling-indexing/google-common-crawlers
108
+ * Microsoft: https://www.bing.com/webmasters/help/which-crawlers-does-bing-use-8c184ec0
109
+ * Apple: https://support.apple.com/en-us/119829
110
+ * DuckDuckGo: https://duckduckgo.com/duckduckbot
111
+ * Yandex: https://yandex.com/support/webmaster/robot-workings/check-yandex-robots.html
112
+ * Baidu: http://help.baidu.com/question?prod_en=master&class=Baiduspider
113
+ * Huawei: https://aspiegel.com/petalbot
114
+ * Yahoo: https://help.yahoo.com/kb/SLN22600.html
115
+ * Ahrefs: https://ahrefs.com/robot
116
+ * Semrush: https://www.semrush.com/bot/
117
+ * Majestic: https://mj12bot.com/
118
+ * Screaming Frog: https://www.screamingfrog.co.uk/seo-spider/user-guide/configuration/
119
+ * UptimeRobot: https://help.uptimerobot.com/en/articles/11358489-what-is-the-uptimerobot-user-agent-string
120
+ * Meta: https://developers.facebook.com/docs/sharing/webmasters/web-crawlers
121
+ * X: https://developer.x.com/en/docs/x-for-websites/cards/guides/getting-started
122
+ * Slack: https://api.slack.com/robots
123
+ * WhatsApp: https://developers.facebook.com/documentation/business-messaging/whatsapp/link-previews/
124
+ *
125
+ * Entries without a readable vendor page (SeznamBot, Yeti, Discordbot,
126
+ * TelegramBot, Pingdom, StatusCake, DotBot) rest on the self-reference in the
127
+ * UA string itself. Reviewed quarterly.
128
+ *
129
+ * Deliberately excluded: `Googlebot-News`, `Google-Extended` and
130
+ * `Applebot-Extended` are robots.txt directives that never appear in a UA
131
+ * header. `Google-Agent` and `Google-GeminiNotebook` are AI agents and live in
132
+ * `agents.ts`.
133
+ */
134
+ type CrawlerCategory = 'search-crawler' | 'seo-tool' | 'monitor' | 'link-preview';
135
+
136
+ /**
137
+ * Request signals handed to the scorer. An absent value and an empty string
138
+ * are equivalent: both mean the signal was not present on the request.
139
+ */
140
+ interface Signals {
141
+ userAgent?: string;
142
+ ip?: string;
143
+ acceptLanguage?: string;
144
+ acceptEncoding?: string;
145
+ secFetchSite?: string;
146
+ secFetchMode?: string;
147
+ secFetchDest?: string;
148
+ secFetchUser?: string;
149
+ secChUa?: string;
150
+ secChUaMobile?: string;
151
+ secChUaPlatform?: string;
152
+ accept?: string;
153
+ contentType?: string;
154
+ referer?: string;
155
+ signatureAgent?: string;
156
+ method?: string;
157
+ /** Reserved: resolved but not consumed. */
158
+ ja4?: string;
159
+ /** Reserved: resolved but not consumed. */
160
+ headerNames?: string;
161
+ }
162
+ /**
163
+ * What kind of client issued the request. `botScore` says how automated it is,
164
+ * this says what it is.
165
+ */
166
+ type BotCategory = 'human' | 'suspicious' | 'automation' | CrawlerCategory | 'ai-agent' | 'ai-crawler' | 'unknown';
167
+ interface ScoreResult {
168
+ /**
169
+ * Automation likelihood, 0-99, higher = more automated. `null` means not
170
+ * measured, never "human".
171
+ */
172
+ botScore: number | null;
173
+ botCategory: BotCategory;
174
+ /** Identified product, set only when a named detector matched. */
175
+ botProduct?: string;
176
+ /** Stable reason codes. Semver-stable public API. */
177
+ botReasons: string[];
178
+ }
179
+
180
+ export { type BotCategory, type BotContext, type BotInput, type BotOutput, type BotSettings, type ScoreResult, type Signals, transformerBot as default, transformerBot };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var e,t=Object.defineProperty,o=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,c=Object.prototype.hasOwnProperty,n={};((e,o)=>{for(var r in o)t(e,r,{get:o[r],enumerable:!0})})(n,{default:()=>d,transformerBot:()=>d}),module.exports=(e=n,((e,n,a,s)=>{if(n&&"object"==typeof n||"function"==typeof n)for(let p of r(n))c.call(e,p)||p===a||t(e,p,{get:()=>n[p],enumerable:!(s=o(n,p))||s.enumerable});return e})(t({},"__esModule",{value:!0}),e));var a=require("@walkeros/core"),s=require("isbot"),p=[{match:"ChatGPT-User",product:"ChatGPT-User",purpose:"user-action"},{match:"ChatGPT-Agent",product:"ChatGPT-Agent",purpose:"user-action"},{match:"OAI-SearchBot",product:"OAI-SearchBot",purpose:"search-index"},{match:"GPTBot",product:"GPTBot",purpose:"training"},{match:"Claude-SearchBot",product:"Claude-SearchBot",purpose:"search-index"},{match:"Claude-User",product:"Claude-User",purpose:"user-action"},{match:"Claude-Code",product:"Claude-Code",purpose:"user-action"},{match:"ClaudeBot",product:"ClaudeBot",purpose:"training"},{match:"anthropic-ai",product:"anthropic-ai",purpose:"training"},{match:"Perplexity-User",product:"Perplexity-User",purpose:"user-action"},{match:"PerplexityBot",product:"PerplexityBot",purpose:"search-index"},{match:"MistralAI-User",product:"MistralAI-User",purpose:"user-action"},{match:"Meta-ExternalFetcher",product:"Meta-ExternalFetcher",purpose:"user-action"},{match:"Meta-ExternalAgent",product:"Meta-ExternalAgent",purpose:"training"},{match:"Google-CloudVertexBot",product:"Google-CloudVertexBot",purpose:"training"},{match:"Google-Extended",product:"Google-Extended",purpose:"training"},{match:"Applebot-Extended",product:"Applebot-Extended",purpose:"training"},{match:"Amazonbot",product:"Amazonbot",purpose:"training"},{match:"DuckAssistBot",product:"DuckAssistBot",purpose:"user-action"},{match:"Bytespider",product:"Bytespider",purpose:"training"},{match:"CCBot",product:"CCBot",purpose:"training"}];function u(e){if(!e)return{botScore:70,agentScore:0,agentProduct:void 0};const t=function(e){const t=e.toLowerCase(),o=p.find(e=>t.includes(e.match.toLowerCase()));return{isBot:!e||(0,s.isbot)(e)||void 0!==o,agent:o?{product:o.product,purpose:o.purpose}:void 0}}(e);let o=0;return t.agent?o="user-action"===t.agent.purpose?90:95:t.isBot&&(o=80),{botScore:o,agentScore:t.agent?95:0,agentProduct:t.agent?.product}}var i={userAgent:"ingest.userAgent",ip:"ingest.ip",acceptLanguage:"ingest.acceptLanguage",acceptEncoding:"ingest.acceptEncoding",secFetchSite:"ingest.secFetchSite",secFetchMode:"ingest.secFetchMode",secFetchDest:"ingest.secFetchDest",secFetchUser:"ingest.secFetchUser",secChUa:"ingest.secChUa",secChUaMobile:"ingest.secChUaMobile",secChUaPlatform:"ingest.secChUaPlatform"},g={botScore:"user.botScore",agentScore:"user.agentScore",agentProduct:""};var d=e=>{const{config:t}=e,o=t.settings??{},r={...i,...o.input??{}},c={...g,...o.output??{}};return{type:"bot",config:t,async push(e,t){const{ingest:o,collector:n}=t,s={event:e,ingest:o},p=await(0,a.getMappingValue)(s,r.userAgent,{collector:n}),i=u("string"==typeof p?p:"");let g=e;const d=(e,t)=>{if(e&&void 0!==t)if(e.startsWith("ingest.")){const r=e.slice(7);if(!r)return;!function(e,t,o){const r=t.split(".");let c=e;for(let e=0;e<r.length-1;e++){const t=r[e],o=c[t];"object"==typeof o&&null!==o||(c[t]={}),c=c[t]}c[r[r.length-1]]=o}(o,r,t)}else g=(0,a.setByPath)(g,e,t)};return d(c.botScore??"",i.botScore),d(c.agentScore??"",i.agentScore),d(c.agentProduct??"",i.agentProduct),{event:g}}}};//# sourceMappingURL=index.js.map
1
+ "use strict";var e,t=Object.defineProperty,o=Object.getOwnPropertyDescriptor,c=Object.getOwnPropertyNames,r=Object.prototype.hasOwnProperty,a={};((e,o)=>{for(var c in o)t(e,c,{get:o[c],enumerable:!0})})(a,{default:()=>L,transformerBot:()=>L}),module.exports=(e=a,((e,a,s,n)=>{if(a&&"object"==typeof a||"function"==typeof a)for(let i of c(a))r.call(e,i)||i===s||t(e,i,{get:()=>a[i],enumerable:!(n=o(a,i))||n.enumerable});return e})(t({},"__esModule",{value:!0}),e));var s=require("@walkeros/core"),n=new Set(["auto","navigation","pixel","beacon","fetch","server"]);function i(e){return"string"==typeof e&&n.has(e)}var u={navigation:{dest:"document",modes:["navigate"],allowsFetchUser:!0,typedAccept:!0},pixel:{dest:"image",modes:["no-cors"],allowsFetchUser:!1,typedAccept:!0},beacon:{dest:"empty",modes:["no-cors"],allowsFetchUser:!1,typedAccept:!1,contentTypePrefix:"text/plain"},fetch:{dest:"empty",modes:["cors","no-cors"],allowsFetchUser:!1,typedAccept:!1}};function p(e){return"server"===e?void 0:u[e]}var h=require("isbot"),d=/^\s*"([^"]*)"\s*(?:;\s*v\s*=\s*"?([^";]*)"?)?\s*$/,g=e=>"notabrand"===e.replace(/[^a-zA-Z0-9]/g,"").toLowerCase();var l=require("isbot"),m=[{match:"ChatGPT-User",product:"ChatGPT-User",purpose:"user-action"},{match:"ChatGPT-Agent",product:"ChatGPT-Agent",purpose:"user-action"},{match:"OAI-SearchBot",product:"OAI-SearchBot",purpose:"search-index"},{match:"GPTBot",product:"GPTBot",purpose:"training"},{match:"Claude-SearchBot",product:"Claude-SearchBot",purpose:"search-index"},{match:"Claude-User",product:"Claude-User",purpose:"user-action"},{match:"Claude-Code",product:"Claude-Code",purpose:"user-action"},{match:"ClaudeBot",product:"ClaudeBot",purpose:"training"},{match:"anthropic-ai",product:"anthropic-ai",purpose:"training"},{match:"Perplexity-User",product:"Perplexity-User",purpose:"user-action"},{match:"PerplexityBot",product:"PerplexityBot",purpose:"search-index"},{match:"MistralAI-User",product:"MistralAI-User",purpose:"user-action"},{match:"Meta-ExternalFetcher",product:"Meta-ExternalFetcher",purpose:"user-action"},{match:"Meta-ExternalAgent",product:"Meta-ExternalAgent",purpose:"training"},{match:"Google-CloudVertexBot",product:"Google-CloudVertexBot",purpose:"training"},{match:"Google-GeminiNotebook",product:"Google-GeminiNotebook",purpose:"user-action"},{match:"Google-Agent",product:"Google-Agent",purpose:"user-action"},{match:"Amazonbot",product:"Amazonbot",purpose:"training"},{match:"DuckAssistBot",product:"DuckAssistBot",purpose:"user-action"},{match:"Bytespider",product:"Bytespider",purpose:"training"},{match:"CCBot",product:"CCBot",purpose:"training"}],b=[{match:"Googlebot-Image",product:"Googlebot Image",category:"search-crawler"},{match:"Googlebot-Video",product:"Googlebot Video",category:"search-crawler"},{match:"Storebot-Google",product:"Google StoreBot",category:"search-crawler"},{match:"Google-InspectionTool",product:"Google InspectionTool",category:"search-crawler"},{match:"GoogleOther-Image",product:"GoogleOther Image",category:"search-crawler"},{match:"GoogleOther-Video",product:"GoogleOther Video",category:"search-crawler"},{match:"GoogleOther",product:"GoogleOther",category:"search-crawler"},{match:"Googlebot",product:"Googlebot",category:"search-crawler"},{match:"adidxbot",product:"AdIdxBot",category:"search-crawler"},{match:"bingbot",product:"Bingbot",category:"search-crawler"},{match:"Applebot",product:"Applebot",category:"search-crawler"},{match:"DuckDuckBot",product:"DuckDuckBot",category:"search-crawler"},{match:"YandexBot",product:"YandexBot",category:"search-crawler"},{match:"Baiduspider",product:"Baiduspider",category:"search-crawler"},{match:"PetalBot",product:"PetalBot",category:"search-crawler"},{match:"SeznamBot",product:"SeznamBot",category:"search-crawler"},{match:"Yeti/",product:"Naver Yeti",category:"search-crawler"},{match:"Slurp",product:"Yahoo Slurp",category:"search-crawler"},{match:"AhrefsSiteAudit",product:"Ahrefs Site Audit",category:"seo-tool"},{match:"AhrefsBot",product:"AhrefsBot",category:"seo-tool"},{match:"SemrushBot",product:"SemrushBot",category:"seo-tool"},{match:"SiteAuditBot",product:"Semrush Site Audit",category:"seo-tool"},{match:"DotBot",product:"Moz DotBot",category:"seo-tool"},{match:"MJ12bot",product:"MJ12bot",category:"seo-tool"},{match:"Screaming Frog SEO Spider",product:"Screaming Frog",category:"seo-tool"},{match:"UptimeRobot",product:"UptimeRobot",category:"monitor"},{match:"Pingdom",product:"Pingdom",category:"monitor"},{match:"StatusCake",product:"StatusCake",category:"monitor"},{match:"facebookexternalhit",product:"Meta external hit",category:"link-preview"},{match:"TelegramBot",product:"TelegramBot",category:"link-preview"},{match:"Twitterbot",product:"Twitterbot",category:"link-preview"},{match:"LinkedInBot",product:"LinkedInBot",category:"link-preview"},{match:"Slackbot-LinkExpanding",product:"Slack link expanding",category:"link-preview"},{match:"Slackbot",product:"Slackbot",category:"link-preview"},{match:"Discordbot",product:"Discordbot",category:"link-preview"},{match:"WhatsApp/",product:"WhatsApp",category:"link-preview"}];var f=/(CriOS|FxiOS|EdgiOS|OPiOS|EdgA?iOS)\//,y=/Chrom(?:e|ium)\/(\d+)/,w=/Firefox\/(\d+)/,C=/Version\/(\d+)(?:\.(\d+))?/,S=16,v=4;var A=["secFetchSite","secFetchMode","secFetchDest"],x=["acceptLanguage","acceptEncoding"],_={Windows:/Windows/i,macOS:/Macintosh|Mac OS X/i,Android:/Android/i,iOS:/iPhone|iPad|iPod/i,Linux:/Linux/i,"Chrome OS":/CrOS/i,"Chromium OS":/CrOS/i},B=/Mobile|Android|iPhone|iPad|iPod/i,U=e=>"string"==typeof e&&""!==e;function P(e,t,o){const c=o.secFetchDest,r=o.secFetchMode;if(t){if(U(c)&&c!==e.dest||U(r)&&!e.modes.includes(r))return"impossible"}else if("document"===c||"navigate"===r||!e.allowsFetchUser&&U(o.secFetchUser))return"impossible";return U(c)&&c!==e.dest||U(r)&&!e.modes.includes(r)?"mismatch":"ok"}function F(e,t){if(void 0===t.chromiumMajor)return!1;const o=function(e){if(!e)return[];const t=function(e){const t=[];let o="",c=!1;for(const r of e)'"'===r&&(c=!c),","!==r||c?o+=r:(t.push(o),o="");if(!c)return t.push(o),t}(e);if(!t)return[];const o=[];for(const e of t){const t=d.exec(e);if(!t)return[];const c=t[1];g(c)||o.push({brand:c,version:t[2]??""})}return o}(e);if(!o.length)return!1;const c=String(t.chromiumMajor);return!o.some(e=>e.version===c)}function M(e,t,o){const c=[],r=o?p(o):void 0;return r&&("impossible"===P(r,"navigation"===o,e)&&c.push("fetchmeta_impossible_for_context"),function(e,t){const o=e.contentTypePrefix;return!(!o||!U(t.contentType)||t.contentType.toLowerCase().startsWith(o))}(r,e)&&c.push("content_type_impossible_for_context")),function(e,t){if(U(e.secChUaPlatform)){const o=function(e){const t=/^\s*"([^"]*)"\s*$/.exec(e);return t?t[1]:void 0}(e.secChUaPlatform),c=o?_[o]:void 0;if(c&&!c.test(t))return!0;if("Linux"===o&&/Android/i.test(t))return!0}return!(!U(e.secChUaMobile)||!0!==function(e){const t=e.trim();return"?1"===t||"?0"!==t&&void 0}(e.secChUaMobile)||B.test(t))}(e,t)&&c.push("ch_platform_contradiction"),c}function k(e,t,o,c){const r=[],a=function(e){const t=f.test(e),o=y.exec(e),c=!t&&o?Number(o[1]):void 0;let r=void 0!==c&&c>=76;if(!r&&!t){const t=w.exec(e);if(t)r=Number(t[1])>=90;else if(void 0===c&&/Safari\//.test(e)){const t=C.exec(e);if(t){const e=Number(t[1]),o=Number(t[2]??0);r=e>S||e===S&&o>=v}}}return{sendsClientHints:void 0!==c&&c>=89,chromiumMajor:c,shipsFetchMetadata:r}}(t),s=o?p(o):void 0;U(e.secChUa)?F(e.secChUa,a)&&r.push({code:"ch_version_mismatch",weight:30}):c.has("secChUa")&&a.sendsClientHints&&r.push({code:"ch_missing_on_chromium",weight:25}),s?.typedAccept&&U(e.accept)&&"*/*"===e.accept.split(",")[0].split(";")[0].trim()&&r.push({code:"accept_generic_on_typed_context",weight:25});const n=A.filter(e=>c.has(e));return n.length&&n.every(t=>!U(e[t]))&&a.shipsFetchMetadata?r.push({code:"fetchmeta_missing_on_modern_ua",weight:15}):s&&"mismatch"===P(s,"navigation"===o,e)&&r.push({code:"fetchmeta_profile_mismatch",weight:15}),c.has("acceptLanguage")&&!U(e.acceptLanguage)&&r.push({code:"accept_language_missing",weight:10}),c.has("acceptEncoding")&&!U(e.acceptEncoding)&&r.push({code:"accept_encoding_missing",weight:5}),r}function O(e,t={}){const o=function(e){return e&&"auto"!==e?e:void 0}(t.context),c=new Set(t.declared??[]),r=t.suspiciousAt??25,a=e.userAgent??"",s=[];if(U(e.signatureAgent)&&s.push("signature_agent_present"),o||s.push("context_undetermined"),c.has("secChUa")||s.push("ch_not_declared"),A.some(e=>c.has(e))||s.push("fetchmeta_not_declared"),x.some(e=>c.has(e))||s.push("accept_not_declared"),!(e=>Object.values(e).some(U))(e))return{botScore:null,botCategory:"unknown",botReasons:s};if(!a)return{botScore:70,botCategory:"automation",botReasons:["ua_missing",...s]};const n=M(e,a,o),i=function(e){const t=e.toLowerCase(),o=m.find(e=>t.includes(e.match.toLowerCase()));return{isBot:!e||(0,l.isbot)(e)||void 0!==o,agent:o?{product:o.product,purpose:o.purpose}:void 0}}(a),u=i.agent?void 0:function(e){const t=e.toLowerCase();return b.find(e=>t.includes(e.match.toLowerCase()))}(a),p=i.agent?{product:i.agent.product,category:"user-action"===i.agent.purpose?"ai-agent":"ai-crawler"}:u?{product:u.product,category:u.category}:void 0;if(p)return n.length?{botScore:90,botCategory:"automation",botReasons:["ua_named_bot",...n,"identity_claim_contradicted",...s]}:{botScore:90,botCategory:p.category,botProduct:p.product,botReasons:["ua_named_bot",...s]};if((0,h.isbot)(a))return{botScore:80,botCategory:"automation",botReasons:["ua_isbot",...s]};if(n.length)return{botScore:75,botCategory:"automation",botReasons:[...n,...s]};const d=k(e,a,o,c),g=Math.min(d.reduce((e,t)=>e+t.weight,0),60);return{botScore:g,botCategory:g>=r?"suspicious":"human",botReasons:[...d.map(e=>e.code),...s]}}var G={userAgent:"ingest.userAgent",ip:"ingest.ip",acceptLanguage:"ingest.acceptLanguage",acceptEncoding:"ingest.acceptEncoding",secFetchSite:"ingest.secFetchSite",secFetchMode:"ingest.secFetchMode",secFetchDest:"ingest.secFetchDest",secFetchUser:"ingest.secFetchUser",secChUa:"ingest.secChUa",secChUaMobile:"ingest.secChUaMobile",secChUaPlatform:"ingest.secChUaPlatform",accept:"ingest.accept",contentType:"ingest.contentType",referer:"ingest.referer",signatureAgent:"ingest.signatureAgent",method:"ingest.method",ja4:"ingest.ja4",headerNames:"ingest.headerNames"},T={botScore:"user.botScore",botCategory:"user.botCategory",botProduct:"user.botProduct",botReasons:"ingest.bot.reasons"};var L=e=>{const{config:t}=e,o=t.settings??{},c={...G,...o.input??{}},r={...T,...o.output??{}},a=Object.keys(o.input??{});return{type:"bot",config:t,async push(e,t){const{ingest:n,collector:u}=t,p={event:e,ingest:n},h=async e=>{const t=await(0,s.getMappingValue)(p,e,{collector:u});return"string"==typeof t&&""!==t?t:void 0},d={userAgent:await h(c.userAgent),ip:await h(c.ip),acceptLanguage:await h(c.acceptLanguage),acceptEncoding:await h(c.acceptEncoding),secFetchSite:await h(c.secFetchSite),secFetchMode:await h(c.secFetchMode),secFetchDest:await h(c.secFetchDest),secFetchUser:await h(c.secFetchUser),secChUa:await h(c.secChUa),secChUaMobile:await h(c.secChUaMobile),secChUaPlatform:await h(c.secChUaPlatform),accept:await h(c.accept),contentType:await h(c.contentType),referer:await h(c.referer),signatureAgent:await h(c.signatureAgent),method:await h(c.method),ja4:await h(c.ja4),headerNames:await h(c.headerNames)};let g;if(void 0!==o.context)if(i(o.context))g=o.context;else{const e=await(0,s.getMappingValue)(p,o.context,{collector:u});g=i(e)?e:"auto"}const l=O(d,{context:g,suspiciousAt:o.suspiciousAt,declared:a});let m=e;const b=(e,t)=>{if(e&&void 0!==t)if(e.startsWith("ingest.")){const o=e.slice(7);if(!o)return;!function(e,t,o){const c=t.split(".");let r=e;for(let e=0;e<c.length-1;e++){const t=c[e],o=r[t];"object"==typeof o&&null!==o||(r[t]={}),r=r[t]}r[c[c.length-1]]=o}(n,o,t)}else m=(0,s.setByPath)(m,e,t)};return b(r.botScore,l.botScore),b(r.botCategory,l.botCategory),b(r.botProduct,l.botProduct),b(r.botReasons,l.botReasons),{event:m}}}};//# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/transformer.ts","../src/detect/ua.ts","../src/data/agents.ts","../src/detect/score.ts"],"sourcesContent":["export { transformerBot } from './transformer';\nexport type { BotSettings, BotInput, BotOutput } from './types';\nexport { transformerBot as default } from './transformer';\n","import type { Transformer } from '@walkeros/core';\nimport { getMappingValue, setByPath } from '@walkeros/core';\nimport { computeScore } from './detect/score';\nimport type { BotInput, BotOutput, BotSettings } from './types';\n\nconst DEFAULT_INPUT: Required<BotInput> = {\n userAgent: 'ingest.userAgent',\n ip: 'ingest.ip',\n acceptLanguage: 'ingest.acceptLanguage',\n acceptEncoding: 'ingest.acceptEncoding',\n secFetchSite: 'ingest.secFetchSite',\n secFetchMode: 'ingest.secFetchMode',\n secFetchDest: 'ingest.secFetchDest',\n secFetchUser: 'ingest.secFetchUser',\n secChUa: 'ingest.secChUa',\n secChUaMobile: 'ingest.secChUaMobile',\n secChUaPlatform: 'ingest.secChUaPlatform',\n};\n\nconst DEFAULT_OUTPUT: Required<BotOutput> = {\n botScore: 'user.botScore',\n agentScore: 'user.agentScore',\n agentProduct: '', // off by default\n};\n\n/**\n * Mutating dot-path setter for ingest writes.\n *\n * We can't use @walkeros/core setByPath here: it clones-and-returns (immutable),\n * but ingest is the pipeline's mutable scratch context. We need in-place writes\n * so subsequent transformers in the chain see the values.\n */\nfunction setNestedPath(\n obj: Record<string, unknown>,\n path: string,\n value: unknown,\n): void {\n const keys = path.split('.');\n let cur: Record<string, unknown> = obj;\n for (let i = 0; i < keys.length - 1; i++) {\n const k = keys[i];\n const next = cur[k];\n if (typeof next !== 'object' || next === null) cur[k] = {};\n cur = cur[k] as Record<string, unknown>;\n }\n cur[keys[keys.length - 1]] = value;\n}\n\nexport const transformerBot: Transformer.Init<\n Transformer.Types<BotSettings>\n> = (context) => {\n const { config } = context;\n const settings: BotSettings = config.settings ?? {};\n const input: Required<BotInput> = {\n ...DEFAULT_INPUT,\n ...(settings.input ?? {}),\n };\n const output: BotOutput = {\n ...DEFAULT_OUTPUT,\n ...(settings.output ?? {}),\n };\n\n return {\n // Init's input config type is Partial<Settings>; the instance config type\n // is Settings. Same cast pattern the fingerprint transformer uses.\n type: 'bot',\n config: config as Transformer.Config<Transformer.Types<BotSettings>>,\n\n async push(event, ctx) {\n const { ingest, collector } = ctx;\n const source = { event, ingest };\n\n // v1 only reads userAgent. Other input fields are reserved for v1.1\n // (header heuristics); resolved-but-unused here would be wasteful, so\n // they are intentionally not read yet.\n const uaValue = await getMappingValue(source, input.userAgent, {\n collector,\n });\n const ua = typeof uaValue === 'string' ? uaValue : '';\n const score = computeScore(ua);\n\n let nextEvent = event;\n\n const writeOutput = (path: string, value: unknown) => {\n if (!path || value === undefined) return;\n if (path.startsWith('ingest.')) {\n const subPath = path.slice('ingest.'.length);\n if (!subPath) return;\n setNestedPath(ingest, subPath, value);\n } else {\n nextEvent = setByPath(nextEvent, path, value);\n }\n };\n\n writeOutput(output.botScore ?? '', score.botScore);\n writeOutput(output.agentScore ?? '', score.agentScore);\n writeOutput(output.agentProduct ?? '', score.agentProduct);\n\n return { event: nextEvent };\n },\n };\n};\n","import { isbot } from 'isbot';\nimport { agents, type AgentEntry } from '../data/agents';\n\nexport interface UAResult {\n isBot: boolean;\n agent?: { product: string; purpose: AgentEntry['purpose'] };\n}\n\nexport function detectUA(ua: string): UAResult {\n const lower = ua.toLowerCase();\n const matched = agents.find((a) => lower.includes(a.match.toLowerCase()));\n return {\n isBot: !ua || isbot(ua) || matched !== undefined,\n agent: matched\n ? { product: matched.product, purpose: matched.purpose }\n : undefined,\n };\n}\n","/**\n * Curated AI agent UA-substring map (2026-Q2).\n *\n * Each entry: substring matched case-insensitively against the User-Agent,\n * a product label written to event.user.agentProduct, and the purpose category.\n *\n * Purpose semantics:\n * - 'training' — crawls for model training; usually filter from analytics\n * - 'search-index' — crawls to power AI search answers; AEO-relevant\n * - 'user-action' — fetch initiated by a human via an AI tool; often kept as traffic\n *\n * Order matters: first-hit wins. More-specific entries must precede broader ones.\n *\n * Vendor docs of record (verified 2026-05):\n * OpenAI: https://platform.openai.com/docs/bots\n * Anthropic: https://support.claude.com/en/articles/8896518\n * Perplexity: https://docs.perplexity.ai/guides/bots\n * Meta: https://developers.facebook.com/docs/sharing/webmasters/web-crawlers\n * Google: https://developers.google.com/search/docs/crawling-indexing/google-common-crawlers\n * Apple: https://support.apple.com/en-us/119829\n * DuckDuckGo: https://duckduckgo.com/duckduckbot\n * Common Crawl: https://commoncrawl.org/faq\n * Amazon: https://developer.amazon.com/amazonbot\n *\n * Community cross-reference: https://github.com/ai-robots-txt/ai.robots.txt\n *\n * Reviewed quarterly. Source-of-truth references are listed above.\n */\nexport interface AgentEntry {\n match: string;\n product: string;\n purpose: 'training' | 'search-index' | 'user-action';\n}\n\nexport const agents: AgentEntry[] = [\n // --- OpenAI ---\n { match: 'ChatGPT-User', product: 'ChatGPT-User', purpose: 'user-action' },\n { match: 'ChatGPT-Agent', product: 'ChatGPT-Agent', purpose: 'user-action' },\n { match: 'OAI-SearchBot', product: 'OAI-SearchBot', purpose: 'search-index' },\n { match: 'GPTBot', product: 'GPTBot', purpose: 'training' },\n\n // --- Anthropic ---\n // Claude-SearchBot must precede Claude-User (defensive specificity for composite UAs)\n {\n match: 'Claude-SearchBot',\n product: 'Claude-SearchBot',\n purpose: 'search-index',\n },\n { match: 'Claude-User', product: 'Claude-User', purpose: 'user-action' },\n { match: 'Claude-Code', product: 'Claude-Code', purpose: 'user-action' },\n { match: 'ClaudeBot', product: 'ClaudeBot', purpose: 'training' },\n // Legacy: only used by older Anthropic crawlers; kept for back-compat with old logs.\n { match: 'anthropic-ai', product: 'anthropic-ai', purpose: 'training' },\n\n // --- Perplexity ---\n {\n match: 'Perplexity-User',\n product: 'Perplexity-User',\n purpose: 'user-action',\n },\n {\n match: 'PerplexityBot',\n product: 'PerplexityBot',\n purpose: 'search-index',\n },\n\n // --- Mistral ---\n {\n match: 'MistralAI-User',\n product: 'MistralAI-User',\n purpose: 'user-action',\n },\n\n // --- Meta ---\n {\n match: 'Meta-ExternalFetcher',\n product: 'Meta-ExternalFetcher',\n purpose: 'user-action',\n },\n {\n match: 'Meta-ExternalAgent',\n product: 'Meta-ExternalAgent',\n purpose: 'training',\n },\n\n // --- Google ---\n {\n match: 'Google-CloudVertexBot',\n product: 'Google-CloudVertexBot',\n purpose: 'training',\n },\n {\n match: 'Google-Extended',\n product: 'Google-Extended',\n purpose: 'training',\n },\n\n // --- Apple ---\n {\n match: 'Applebot-Extended',\n product: 'Applebot-Extended',\n purpose: 'training',\n },\n\n // --- Amazon ---\n { match: 'Amazonbot', product: 'Amazonbot', purpose: 'training' },\n\n // --- DuckDuckGo ---\n {\n match: 'DuckAssistBot',\n product: 'DuckAssistBot',\n purpose: 'user-action',\n },\n\n // --- ByteDance ---\n { match: 'Bytespider', product: 'Bytespider', purpose: 'training' },\n\n // --- Common Crawl ---\n { match: 'CCBot', product: 'CCBot', purpose: 'training' },\n];\n","import { detectUA } from './ua';\n\nexport interface ScoreResult {\n /** 0-99, higher = more bot. v1 emits discrete values: 0, 70, 80, 90, 95. */\n botScore: number;\n /**\n * 0-99, higher = more likely an AI agent. v1 emits only 0 or 95\n * (binary UA-map match). Graduated values (e.g. 70 for unverified UA\n * claim, 99 for IP-reverse-DNS verified) are planned for v1.1.\n */\n agentScore: number;\n /** Matched AI agent UA substring, when one was found. */\n agentProduct?: string;\n}\n\n/**\n * v1: UA-only.\n *\n * botScore baseline:\n * - Empty UA → 70 (real browsers rarely strip UA)\n * - AI training crawler → 95\n * - AI user-action → 90\n * - isbot true → 80\n * - Otherwise → 0\n *\n * Header heuristics (Sec-Fetch missing, Sec-CH-UA major mismatch,\n * Accept-Language stripping) are intentionally deferred to v1.1 —\n * see the README \"Not in v1\" section and the research file.\n */\nexport function computeScore(ua: string): ScoreResult {\n if (!ua) {\n return { botScore: 70, agentScore: 0, agentProduct: undefined };\n }\n\n const uaResult = detectUA(ua);\n\n let botScore = 0;\n if (uaResult.agent) {\n botScore = uaResult.agent.purpose === 'user-action' ? 90 : 95;\n } else if (uaResult.isBot) {\n botScore = 80;\n }\n\n return {\n botScore,\n agentScore: uaResult.agent ? 95 : 0,\n agentProduct: uaResult.agent?.product,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/transformer.ts","../src/detect/context.ts","../src/detect/score.ts","../src/detect/secChUa.ts","../src/detect/ua.ts","../src/data/agents.ts","../src/data/crawlers.ts"],"sourcesContent":["export { transformerBot } from './transformer';\nexport type { BotSettings, BotInput, BotOutput } from './types';\nexport type { BotContext } from './detect/context';\nexport type { BotCategory, ScoreResult, Signals } from './detect/score';\nexport { transformerBot as default } from './transformer';\n","import type { Mapping, Transformer } from '@walkeros/core';\nimport { getMappingValue, setByPath } from '@walkeros/core';\nimport { isBotContext, type BotContext } from './detect/context';\nimport { computeScore, type SignalName, type Signals } from './detect/score';\nimport type { BotInput, BotOutput, BotSettings } from './types';\n\nconst DEFAULT_INPUT: Required<BotInput> = {\n userAgent: 'ingest.userAgent',\n ip: 'ingest.ip',\n acceptLanguage: 'ingest.acceptLanguage',\n acceptEncoding: 'ingest.acceptEncoding',\n secFetchSite: 'ingest.secFetchSite',\n secFetchMode: 'ingest.secFetchMode',\n secFetchDest: 'ingest.secFetchDest',\n secFetchUser: 'ingest.secFetchUser',\n secChUa: 'ingest.secChUa',\n secChUaMobile: 'ingest.secChUaMobile',\n secChUaPlatform: 'ingest.secChUaPlatform',\n accept: 'ingest.accept',\n contentType: 'ingest.contentType',\n referer: 'ingest.referer',\n signatureAgent: 'ingest.signatureAgent',\n method: 'ingest.method',\n ja4: 'ingest.ja4',\n headerNames: 'ingest.headerNames',\n};\n\nconst DEFAULT_OUTPUT: Required<BotOutput> = {\n botScore: 'user.botScore',\n botCategory: 'user.botCategory',\n botProduct: 'user.botProduct',\n // Reasons are a pipeline diagnostic, so they stay off the analytics event.\n botReasons: 'ingest.bot.reasons',\n};\n\n/**\n * Mutating dot-path setter for ingest writes.\n *\n * We can't use @walkeros/core setByPath here: it clones-and-returns (immutable),\n * but ingest is the pipeline's mutable scratch context. We need in-place writes\n * so subsequent transformers in the chain see the values.\n */\nfunction setNestedPath(\n obj: Record<string, unknown>,\n path: string,\n value: unknown,\n): void {\n const keys = path.split('.');\n let cur: Record<string, unknown> = obj;\n for (let i = 0; i < keys.length - 1; i++) {\n const k = keys[i];\n const next = cur[k];\n if (typeof next !== 'object' || next === null) cur[k] = {};\n cur = cur[k] as Record<string, unknown>;\n }\n cur[keys[keys.length - 1]] = value;\n}\n\nexport const transformerBot: Transformer.Init<\n Transformer.Types<BotSettings>\n> = (context) => {\n const { config } = context;\n const settings: BotSettings = config.settings ?? {};\n const input: Required<BotInput> = {\n ...DEFAULT_INPUT,\n ...(settings.input ?? {}),\n };\n const output: BotOutput = {\n ...DEFAULT_OUTPUT,\n ...(settings.output ?? {}),\n };\n // Declaring a name in settings.input is the operator asserting the signal is\n // wired. Resolution still falls back to DEFAULT_INPUT; declaration is a\n // separate fact, and the only reliable one for an absence-based check.\n const declared = Object.keys(settings.input ?? {}) as SignalName[];\n\n return {\n // Init's input config type is Partial<Settings>; the instance config type\n // is Settings. Same cast pattern the fingerprint transformer uses.\n type: 'bot',\n config: config as Transformer.Config<Transformer.Types<BotSettings>>,\n\n async push(event, ctx) {\n const { ingest, collector } = ctx;\n const source = { event, ingest };\n\n const resolve = async (\n value: Mapping.Value,\n ): Promise<string | undefined> => {\n const resolved = await getMappingValue(source, value, { collector });\n return typeof resolved === 'string' && resolved !== ''\n ? resolved\n : undefined;\n };\n\n const signals: Signals = {\n userAgent: await resolve(input.userAgent),\n ip: await resolve(input.ip),\n acceptLanguage: await resolve(input.acceptLanguage),\n acceptEncoding: await resolve(input.acceptEncoding),\n secFetchSite: await resolve(input.secFetchSite),\n secFetchMode: await resolve(input.secFetchMode),\n secFetchDest: await resolve(input.secFetchDest),\n secFetchUser: await resolve(input.secFetchUser),\n secChUa: await resolve(input.secChUa),\n secChUaMobile: await resolve(input.secChUaMobile),\n secChUaPlatform: await resolve(input.secChUaPlatform),\n accept: await resolve(input.accept),\n contentType: await resolve(input.contentType),\n referer: await resolve(input.referer),\n signatureAgent: await resolve(input.signatureAgent),\n method: await resolve(input.method),\n ja4: await resolve(input.ja4),\n headerNames: await resolve(input.headerNames),\n };\n\n let context: BotContext | undefined;\n if (settings.context !== undefined) {\n if (isBotContext(settings.context)) {\n context = settings.context;\n } else {\n const resolved = await getMappingValue(source, settings.context, {\n collector,\n });\n context = isBotContext(resolved) ? resolved : 'auto';\n }\n }\n\n const score = computeScore(signals, {\n context,\n suspiciousAt: settings.suspiciousAt,\n declared,\n });\n\n let nextEvent = event;\n\n const writeOutput = (\n path: string | false | undefined,\n value: unknown,\n ) => {\n if (!path || value === undefined) return;\n if (path.startsWith('ingest.')) {\n const subPath = path.slice('ingest.'.length);\n if (!subPath) return;\n setNestedPath(ingest, subPath, value);\n } else {\n nextEvent = setByPath(nextEvent, path, value);\n }\n };\n\n writeOutput(output.botScore, score.botScore);\n writeOutput(output.botCategory, score.botCategory);\n writeOutput(output.botProduct, score.botProduct);\n writeOutput(output.botReasons, score.botReasons);\n\n return { event: nextEvent };\n },\n };\n};\n","/**\n * Request-context model.\n *\n * Every published bot heuristic carries an unstated assumption about how the\n * request was made, and almost all of them assume a top-level navigation. A\n * collector sees beacons, pixels, fetches and server-to-server posts, where the\n * same header value can mean the opposite thing. a wildcard `Accept` from a\n * browser UA is normal on a beacon and anomalous on a pixel.\n *\n * `auto` never determines a context. Absence of `Sec-Fetch-*` is both a signal\n * worth scoring and the reason auto-derivation fails, and the request method\n * does not rescue it: a GET is a pixel, a navigation or a `fetch()` GET, and\n * the first two expect a typed `Accept` while the third correctly sends a wildcard.\n * So context-dependent checks run only under a pinned context. The failure mode\n * is \"we scored less\", never \"we scored wrong\".\n */\n\nexport type BotContext =\n | 'auto'\n | 'navigation'\n | 'pixel'\n | 'beacon'\n | 'fetch'\n | 'server';\n\nconst BOT_CONTEXTS: ReadonlySet<string> = new Set<BotContext>([\n 'auto',\n 'navigation',\n 'pixel',\n 'beacon',\n 'fetch',\n 'server',\n]);\n\n/** True when the value is one of the six BotContext literals. */\nexport function isBotContext(value: unknown): value is BotContext {\n return typeof value === 'string' && BOT_CONTEXTS.has(value);\n}\n\nexport type PinnedContext = Exclude<BotContext, 'auto'>;\n\nexport interface ContextProfile {\n /** The only Sec-Fetch-Dest a real browser sends in this context. */\n dest: string;\n /** Sec-Fetch-Mode values a real browser can send in this context. */\n modes: string[];\n /** Sec-Fetch-User is only ever sent on a user-activated navigation. */\n allowsFetchUser: boolean;\n /** Browsers send a media-type-specific Accept here, never a bare wildcard. */\n typedAccept: boolean;\n /** Lowercased Content-Type prefix the context forces, when it forces one. */\n contentTypePrefix?: string;\n}\n\n/**\n * Browser-truth profiles. `server` has no entry: no browser is involved, so\n * every browser-shaped check is meaningless there.\n */\nexport const CONTEXT_PROFILES: Record<\n Exclude<PinnedContext, 'server'>,\n ContextProfile\n> = {\n navigation: {\n dest: 'document',\n modes: ['navigate'],\n allowsFetchUser: true,\n typedAccept: true,\n },\n pixel: {\n dest: 'image',\n modes: ['no-cors'],\n allowsFetchUser: false,\n typedAccept: true,\n },\n beacon: {\n dest: 'empty',\n modes: ['no-cors'],\n allowsFetchUser: false,\n typedAccept: false,\n contentTypePrefix: 'text/plain',\n },\n fetch: {\n dest: 'empty',\n modes: ['cors', 'no-cors'],\n allowsFetchUser: false,\n typedAccept: false,\n },\n};\n\n/** Resolves the pinned context, or undefined when checks must not run. */\nexport function pinnedContext(\n context: BotContext | undefined,\n): PinnedContext | undefined {\n return context && context !== 'auto' ? context : undefined;\n}\n\n/** The profile for a pinned context, or undefined for `server`. */\nexport function contextProfile(\n context: PinnedContext,\n): ContextProfile | undefined {\n return context === 'server' ? undefined : CONTEXT_PROFILES[context];\n}\n","import { isbot } from 'isbot';\nimport type { CrawlerCategory } from '../data/crawlers';\nimport {\n contextProfile,\n pinnedContext,\n type BotContext,\n type ContextProfile,\n} from './context';\nimport {\n parseSecChUa,\n parseSecChUaMobile,\n parseSecChUaPlatform,\n} from './secChUa';\nimport { detectCrawler, detectUA, parseUAFamily, type UAFamily } from './ua';\n\n/**\n * Request signals handed to the scorer. An absent value and an empty string\n * are equivalent: both mean the signal was not present on the request.\n */\nexport interface Signals {\n userAgent?: string;\n ip?: string;\n acceptLanguage?: string;\n acceptEncoding?: string;\n secFetchSite?: string;\n secFetchMode?: string;\n secFetchDest?: string;\n secFetchUser?: string;\n secChUa?: string;\n secChUaMobile?: string;\n secChUaPlatform?: string;\n accept?: string;\n contentType?: string;\n referer?: string;\n signatureAgent?: string;\n method?: string;\n /** Reserved: resolved but not consumed. */\n ja4?: string;\n /** Reserved: resolved but not consumed. */\n headerNames?: string;\n}\n\nexport type SignalName = keyof Signals;\n\n/**\n * What kind of client issued the request. `botScore` says how automated it is,\n * this says what it is.\n */\nexport type BotCategory =\n | 'human'\n | 'suspicious'\n | 'automation'\n | CrawlerCategory\n | 'ai-agent'\n | 'ai-crawler'\n | 'unknown';\n\nexport interface ScoreResult {\n /**\n * Automation likelihood, 0-99, higher = more automated. `null` means not\n * measured, never \"human\".\n */\n botScore: number | null;\n botCategory: BotCategory;\n /** Identified product, set only when a named detector matched. */\n botProduct?: string;\n /** Stable reason codes. Semver-stable public API. */\n botReasons: string[];\n}\n\nexport interface ScoreOptions {\n /** Pinning a context is what enables the context-dependent checks. */\n context?: BotContext;\n /** Graded-layer cut between `human` and `suspicious`. */\n suspiciousAt?: number;\n /**\n * Input names the operator explicitly listed in `settings.input`. An\n * absence-based heuristic runs only for a declared name, because \"the client\n * did not send this header\" and \"the operator never mapped it\" both resolve\n * to undefined and only the operator knows which one it is.\n */\n declared?: readonly SignalName[];\n}\n\nexport const DEFAULT_SUSPICIOUS_AT = 25;\n\n/** The graded layer caps here; the deterministic layer starts at 70. */\nconst GRADED_CAP = 60;\n\nconst SCORE_UA_MISSING = 70;\nconst SCORE_CONTRADICTION = 75;\nconst SCORE_ISBOT = 80;\nconst SCORE_NAMED_BOT = 90;\n\nconst FETCH_META_NAMES: SignalName[] = [\n 'secFetchSite',\n 'secFetchMode',\n 'secFetchDest',\n];\nconst ACCEPT_NAMES: SignalName[] = ['acceptLanguage', 'acceptEncoding'];\n\n/** Sec-CH-UA-Platform values, and the UA token each one requires. */\nconst PLATFORM_UA_TOKENS: Record<string, RegExp> = {\n Windows: /Windows/i,\n macOS: /Macintosh|Mac OS X/i,\n Android: /Android/i,\n iOS: /iPhone|iPad|iPod/i,\n Linux: /Linux/i,\n 'Chrome OS': /CrOS/i,\n 'Chromium OS': /CrOS/i,\n};\n\nconst MOBILE_UA_TOKENS = /Mobile|Android|iPhone|iPad|iPod/i;\n\ninterface GradedFinding {\n code: string;\n weight: number;\n}\n\nconst signalPresent = (value: string | undefined): value is string =>\n typeof value === 'string' && value !== '';\n\nconst hasAnySignal = (signals: Signals): boolean =>\n Object.values(signals).some(signalPresent);\n\n/** True when the first media range of an Accept header is a bare wildcard. */\nfunction isGenericAccept(accept: string): boolean {\n const first = accept.split(',')[0].split(';')[0].trim();\n return first === '*/*';\n}\n\n/**\n * Compares Fetch Metadata against the context's browser-truth profile.\n * `impossible` means no real browser can produce this here; `mismatch` means\n * the values are merely off-profile.\n */\nfunction fetchMetaVerdict(\n profile: ContextProfile,\n isNavigation: boolean,\n signals: Signals,\n): 'ok' | 'impossible' | 'mismatch' {\n const dest = signals.secFetchDest;\n const mode = signals.secFetchMode;\n\n if (isNavigation) {\n if (\n (signalPresent(dest) && dest !== profile.dest) ||\n (signalPresent(mode) && !profile.modes.includes(mode))\n )\n return 'impossible';\n } else if (\n dest === 'document' ||\n mode === 'navigate' ||\n (!profile.allowsFetchUser && signalPresent(signals.secFetchUser))\n ) {\n return 'impossible';\n }\n\n if (\n (signalPresent(dest) && dest !== profile.dest) ||\n (signalPresent(mode) && !profile.modes.includes(mode))\n )\n return 'mismatch';\n\n return 'ok';\n}\n\n/** A Content-Type the context cannot produce, e.g. anything but text/plain on a beacon. */\nfunction contentTypeImpossible(\n profile: ContextProfile,\n signals: Signals,\n): boolean {\n const prefix = profile.contentTypePrefix;\n if (!prefix || !signalPresent(signals.contentType)) return false;\n return !signals.contentType.toLowerCase().startsWith(prefix);\n}\n\n/** The client-hint platform description disagrees with the UA it accompanies. */\nfunction chPlatformContradiction(signals: Signals, ua: string): boolean {\n if (signalPresent(signals.secChUaPlatform)) {\n const platform = parseSecChUaPlatform(signals.secChUaPlatform);\n const required = platform ? PLATFORM_UA_TOKENS[platform] : undefined;\n if (required && !required.test(ua)) return true;\n // Every Android UA also says Linux, so Linux additionally excludes Android.\n if (platform === 'Linux' && /Android/i.test(ua)) return true;\n }\n\n if (\n signalPresent(signals.secChUaMobile) &&\n parseSecChUaMobile(signals.secChUaMobile) === true &&\n !MOBILE_UA_TOKENS.test(ua)\n )\n return true;\n\n return false;\n}\n\n/** No non-GREASE brand agrees with the Chromium major the UA claims. */\nfunction chVersionMismatch(secChUa: string, family: UAFamily): boolean {\n if (family.chromiumMajor === undefined) return false;\n const brands = parseSecChUa(secChUa);\n if (!brands.length) return false;\n const major = String(family.chromiumMajor);\n return !brands.some((brand) => brand.version === major);\n}\n\nfunction detectContradictions(\n signals: Signals,\n ua: string,\n context: ReturnType<typeof pinnedContext>,\n): string[] {\n const codes: string[] = [];\n const profile = context ? contextProfile(context) : undefined;\n\n if (profile) {\n if (\n fetchMetaVerdict(profile, context === 'navigation', signals) ===\n 'impossible'\n )\n codes.push('fetchmeta_impossible_for_context');\n if (contentTypeImpossible(profile, signals))\n codes.push('content_type_impossible_for_context');\n }\n\n if (chPlatformContradiction(signals, ua))\n codes.push('ch_platform_contradiction');\n\n return codes;\n}\n\nfunction gradedFindings(\n signals: Signals,\n ua: string,\n context: ReturnType<typeof pinnedContext>,\n declared: ReadonlySet<SignalName>,\n): GradedFinding[] {\n const findings: GradedFinding[] = [];\n const family = parseUAFamily(ua);\n const profile = context ? contextProfile(context) : undefined;\n\n if (signalPresent(signals.secChUa)) {\n if (chVersionMismatch(signals.secChUa, family))\n findings.push({ code: 'ch_version_mismatch', weight: 30 });\n } else if (declared.has('secChUa') && family.sendsClientHints) {\n findings.push({ code: 'ch_missing_on_chromium', weight: 25 });\n }\n\n if (\n profile?.typedAccept &&\n signalPresent(signals.accept) &&\n isGenericAccept(signals.accept)\n )\n findings.push({ code: 'accept_generic_on_typed_context', weight: 25 });\n\n const fetchMetaDeclared = FETCH_META_NAMES.filter((name) =>\n declared.has(name),\n );\n if (\n fetchMetaDeclared.length &&\n fetchMetaDeclared.every((name) => !signalPresent(signals[name])) &&\n family.shipsFetchMetadata\n ) {\n findings.push({ code: 'fetchmeta_missing_on_modern_ua', weight: 15 });\n } else if (\n profile &&\n fetchMetaVerdict(profile, context === 'navigation', signals) === 'mismatch'\n ) {\n findings.push({ code: 'fetchmeta_profile_mismatch', weight: 15 });\n }\n\n if (declared.has('acceptLanguage') && !signalPresent(signals.acceptLanguage))\n findings.push({ code: 'accept_language_missing', weight: 10 });\n\n if (declared.has('acceptEncoding') && !signalPresent(signals.acceptEncoding))\n findings.push({ code: 'accept_encoding_missing', weight: 5 });\n\n return findings;\n}\n\n/**\n * Scores a request. Pure and synchronous: every signal arrives in the bag, and\n * nothing here performs I/O.\n *\n * Deterministic precedence, most specific first, first match wins:\n * 1. UA absent -> 70, automation\n * 2. AI agent UA map -> 90, ai-agent or ai-crawler\n * 3. Non-AI crawler UA map -> 90, category from the entry\n * 4. isbot -> 80, automation\n * 5. Impossible-for-context values -> 75, automation\n *\n * A UA-map match co-occurring with a contradiction keeps its score, because the\n * client is still software, but loses its claimed identity: the category drops\n * to `automation` and no `botProduct` is written.\n *\n * When no rung fires, the graded layer adds weighted evidence and caps at 60,\n * so every emitted score is attributable to exactly one layer.\n */\nexport function computeScore(\n signals: Signals,\n options: ScoreOptions = {},\n): ScoreResult {\n const context = pinnedContext(options.context);\n const declared = new Set(options.declared ?? []);\n const suspiciousAt = options.suspiciousAt ?? DEFAULT_SUSPICIOUS_AT;\n const ua = signals.userAgent ?? '';\n\n // Configuration facts, identical for every event on a given pipeline. They\n // are what turns botReasons into a wiring diagnostic.\n const notes: string[] = [];\n if (signalPresent(signals.signatureAgent))\n notes.push('signature_agent_present');\n if (!context) notes.push('context_undetermined');\n if (!declared.has('secChUa')) notes.push('ch_not_declared');\n if (!FETCH_META_NAMES.some((name) => declared.has(name)))\n notes.push('fetchmeta_not_declared');\n if (!ACCEPT_NAMES.some((name) => declared.has(name)))\n notes.push('accept_not_declared');\n\n if (!hasAnySignal(signals))\n return { botScore: null, botCategory: 'unknown', botReasons: notes };\n\n if (!ua)\n return {\n botScore: SCORE_UA_MISSING,\n botCategory: 'automation',\n botReasons: ['ua_missing', ...notes],\n };\n\n const contradictions = detectContradictions(signals, ua, context);\n const uaResult = detectUA(ua);\n const crawler = uaResult.agent ? undefined : detectCrawler(ua);\n\n const named: { product: string; category: BotCategory } | undefined =\n uaResult.agent\n ? {\n product: uaResult.agent.product,\n category:\n uaResult.agent.purpose === 'user-action'\n ? 'ai-agent'\n : 'ai-crawler',\n }\n : crawler\n ? { product: crawler.product, category: crawler.category }\n : undefined;\n\n if (named) {\n if (contradictions.length)\n return {\n botScore: SCORE_NAMED_BOT,\n botCategory: 'automation',\n botReasons: [\n 'ua_named_bot',\n ...contradictions,\n 'identity_claim_contradicted',\n ...notes,\n ],\n };\n\n return {\n botScore: SCORE_NAMED_BOT,\n botCategory: named.category,\n botProduct: named.product,\n botReasons: ['ua_named_bot', ...notes],\n };\n }\n\n if (isbot(ua))\n return {\n botScore: SCORE_ISBOT,\n botCategory: 'automation',\n botReasons: ['ua_isbot', ...notes],\n };\n\n if (contradictions.length)\n return {\n botScore: SCORE_CONTRADICTION,\n botCategory: 'automation',\n botReasons: [...contradictions, ...notes],\n };\n\n const findings = gradedFindings(signals, ua, context, declared);\n const score = Math.min(\n findings.reduce((sum, finding) => sum + finding.weight, 0),\n GRADED_CAP,\n );\n\n return {\n botScore: score,\n botCategory: score >= suspiciousAt ? 'suspicious' : 'human',\n botReasons: [...findings.map((finding) => finding.code), ...notes],\n };\n}\n","/**\n * Parser for the three low-entropy User-Agent client hints, covering the\n * RFC 8941 structured-field subset these headers actually use: an sf-list of\n * sf-strings with a `v` parameter, an sf-boolean, and a bare sf-string.\n *\n * Chromium injects deliberately fake GREASE brands in any position, with any\n * name and rotating punctuation, so filtering is mandatory before any brand or\n * version comparison.\n */\n\nexport interface ChUaBrand {\n /** Brand as sent, e.g. 'Chromium', 'Google Chrome'. */\n brand: string;\n /** Significant (major) version as sent, e.g. '124'. */\n version: string;\n}\n\nconst ITEM = /^\\s*\"([^\"]*)\"\\s*(?:;\\s*v\\s*=\\s*\"?([^\";]*)\"?)?\\s*$/;\n\n/** GREASE brands normalize to `notabrand` once punctuation and case are dropped. */\nconst isGrease = (brand: string): boolean =>\n brand.replace(/[^a-zA-Z0-9]/g, '').toLowerCase() === 'notabrand';\n\n/** Splits an sf-list on commas that sit outside quoted strings. */\nfunction splitItems(header: string): string[] | undefined {\n const items: string[] = [];\n let current = '';\n let inQuotes = false;\n\n for (const char of header) {\n if (char === '\"') inQuotes = !inQuotes;\n if (char === ',' && !inQuotes) {\n items.push(current);\n current = '';\n continue;\n }\n current += char;\n }\n\n if (inQuotes) return undefined;\n items.push(current);\n return items;\n}\n\n/** Parses a Sec-CH-UA header value into GREASE-filtered brands. Malformed input yields []. */\nexport function parseSecChUa(header: string): ChUaBrand[] {\n if (!header) return [];\n\n const items = splitItems(header);\n if (!items) return [];\n\n const brands: ChUaBrand[] = [];\n for (const item of items) {\n const match = ITEM.exec(item);\n if (!match) return [];\n const brand = match[1];\n if (isGrease(brand)) continue;\n brands.push({ brand, version: match[2] ?? '' });\n }\n\n return brands;\n}\n\n/** '?1' -> true, '?0' -> false, anything else -> undefined. */\nexport function parseSecChUaMobile(header: string): boolean | undefined {\n const value = header.trim();\n if (value === '?1') return true;\n if (value === '?0') return false;\n return undefined;\n}\n\n/** Unquotes an sf-string, e.g. '\"macOS\"' -> 'macOS'. Malformed input yields undefined. */\nexport function parseSecChUaPlatform(header: string): string | undefined {\n const match = /^\\s*\"([^\"]*)\"\\s*$/.exec(header);\n return match ? match[1] : undefined;\n}\n","import { isbot } from 'isbot';\nimport { agents, type AgentEntry } from '../data/agents';\nimport { crawlers, type CrawlerEntry } from '../data/crawlers';\n\nexport interface UAResult {\n isBot: boolean;\n agent?: { product: string; purpose: AgentEntry['purpose'] };\n}\n\nexport function detectUA(ua: string): UAResult {\n const lower = ua.toLowerCase();\n const matched = agents.find((a) => lower.includes(a.match.toLowerCase()));\n return {\n isBot: !ua || isbot(ua) || matched !== undefined,\n agent: matched\n ? { product: matched.product, purpose: matched.purpose }\n : undefined,\n };\n}\n\n/** Non-AI crawler lookup. `agents` is scanned first, so an AI token wins. */\nexport function detectCrawler(ua: string): CrawlerEntry | undefined {\n const lower = ua.toLowerCase();\n return crawlers.find((c) => lower.includes(c.match.toLowerCase()));\n}\n\nexport interface UAFamily {\n /**\n * A Chromium build that ships the low-entropy client hints. The iOS\n * wrappers (CriOS, EdgiOS, OPiOS) are WebKit underneath and send none.\n */\n sendsClientHints: boolean;\n /** Claimed Chromium major version, when the UA states one. */\n chromiumMajor?: number;\n /** The claimed browser version is one that shipped Fetch Metadata. */\n shipsFetchMetadata: boolean;\n}\n\nconst IOS_WRAPPER = /(CriOS|FxiOS|EdgiOS|OPiOS|EdgA?iOS)\\//;\nconst CHROMIUM_MAJOR = /Chrom(?:e|ium)\\/(\\d+)/;\nconst FIREFOX_MAJOR = /Firefox\\/(\\d+)/;\nconst SAFARI_VERSION = /Version\\/(\\d+)(?:\\.(\\d+))?/;\n\n/** Client hints shipped in Chrome 89, Fetch Metadata in Chrome 76. */\nconst CHROMIUM_CLIENT_HINTS = 89;\nconst CHROMIUM_FETCH_METADATA = 76;\nconst FIREFOX_FETCH_METADATA = 90;\nconst SAFARI_FETCH_METADATA = { major: 16, minor: 4 };\n\n/**\n * Reads what a UA claims about itself. Every field fails closed: a version that\n * cannot be read confidently reports the capability as absent, so an\n * absence-based heuristic never fires on a UA we could not parse.\n */\nexport function parseUAFamily(ua: string): UAFamily {\n const iosWrapper = IOS_WRAPPER.test(ua);\n const chromiumMatch = CHROMIUM_MAJOR.exec(ua);\n const chromiumMajor =\n !iosWrapper && chromiumMatch ? Number(chromiumMatch[1]) : undefined;\n\n let shipsFetchMetadata =\n chromiumMajor !== undefined && chromiumMajor >= CHROMIUM_FETCH_METADATA;\n\n if (!shipsFetchMetadata && !iosWrapper) {\n const firefoxMatch = FIREFOX_MAJOR.exec(ua);\n if (firefoxMatch) {\n shipsFetchMetadata = Number(firefoxMatch[1]) >= FIREFOX_FETCH_METADATA;\n } else if (chromiumMajor === undefined && /Safari\\//.test(ua)) {\n const safariMatch = SAFARI_VERSION.exec(ua);\n if (safariMatch) {\n const major = Number(safariMatch[1]);\n const minor = Number(safariMatch[2] ?? 0);\n shipsFetchMetadata =\n major > SAFARI_FETCH_METADATA.major ||\n (major === SAFARI_FETCH_METADATA.major &&\n minor >= SAFARI_FETCH_METADATA.minor);\n }\n }\n }\n\n return {\n sendsClientHints:\n chromiumMajor !== undefined && chromiumMajor >= CHROMIUM_CLIENT_HINTS,\n chromiumMajor,\n shipsFetchMetadata,\n };\n}\n","/**\n * Curated AI agent UA-substring map (2026-Q3).\n *\n * Each entry: substring matched case-insensitively against the User-Agent,\n * a product label written to botProduct, and the purpose category.\n *\n * Purpose semantics:\n * - 'training' — crawls for model training; usually filter from analytics\n * - 'search-index' — crawls to power AI search answers; AEO-relevant\n * - 'user-action' — fetch initiated by a human via an AI tool; often kept as traffic\n *\n * Order matters: first-hit wins. More-specific entries must precede broader ones.\n * This file is scanned BEFORE `crawlers.ts`, so an AI token wins a collision.\n *\n * Every entry must be a substring of a real User-Agent header. robots.txt\n * opt-out directives (`Google-Extended`, `Applebot-Extended`, `Googlebot-News`)\n * look like agent names but never appear in a UA, so they can never match and\n * do not belong here. Google states Google-Extended \"doesn't have a separate\n * HTTP request user agent string\".\n *\n * Vendor docs of record (verified 2026-08):\n * OpenAI: https://developers.openai.com/api/docs/bots\n * Anthropic: https://support.claude.com/en/articles/8896518\n * Perplexity: https://docs.perplexity.ai/guides/bots\n * Meta: https://developers.facebook.com/docs/sharing/webmasters/web-crawlers\n * Google: https://developers.google.com/search/docs/crawling-indexing/google-common-crawlers\n * https://developers.google.com/search/docs/crawling-indexing/google-user-triggered-fetchers\n * DuckDuckGo: https://duckduckgo.com/duckduckbot\n * Common Crawl: https://commoncrawl.org/faq\n * Amazon: https://developer.amazon.com/amazonbot\n *\n * Community cross-reference: https://github.com/ai-robots-txt/ai.robots.txt\n *\n * Reviewed quarterly. Source-of-truth references are listed above.\n */\nexport interface AgentEntry {\n match: string;\n product: string;\n purpose: 'training' | 'search-index' | 'user-action';\n}\n\nexport const agents: AgentEntry[] = [\n // --- OpenAI ---\n { match: 'ChatGPT-User', product: 'ChatGPT-User', purpose: 'user-action' },\n { match: 'ChatGPT-Agent', product: 'ChatGPT-Agent', purpose: 'user-action' },\n { match: 'OAI-SearchBot', product: 'OAI-SearchBot', purpose: 'search-index' },\n { match: 'GPTBot', product: 'GPTBot', purpose: 'training' },\n\n // --- Anthropic ---\n // Claude-SearchBot must precede Claude-User (defensive specificity for composite UAs)\n {\n match: 'Claude-SearchBot',\n product: 'Claude-SearchBot',\n purpose: 'search-index',\n },\n { match: 'Claude-User', product: 'Claude-User', purpose: 'user-action' },\n { match: 'Claude-Code', product: 'Claude-Code', purpose: 'user-action' },\n { match: 'ClaudeBot', product: 'ClaudeBot', purpose: 'training' },\n // Legacy: only used by older Anthropic crawlers; kept for back-compat with old logs.\n { match: 'anthropic-ai', product: 'anthropic-ai', purpose: 'training' },\n\n // --- Perplexity ---\n {\n match: 'Perplexity-User',\n product: 'Perplexity-User',\n purpose: 'user-action',\n },\n {\n match: 'PerplexityBot',\n product: 'PerplexityBot',\n purpose: 'search-index',\n },\n\n // --- Mistral ---\n {\n match: 'MistralAI-User',\n product: 'MistralAI-User',\n purpose: 'user-action',\n },\n\n // --- Meta ---\n {\n match: 'Meta-ExternalFetcher',\n product: 'Meta-ExternalFetcher',\n purpose: 'user-action',\n },\n {\n match: 'Meta-ExternalAgent',\n product: 'Meta-ExternalAgent',\n purpose: 'training',\n },\n\n // --- Google ---\n {\n match: 'Google-CloudVertexBot',\n product: 'Google-CloudVertexBot',\n purpose: 'training',\n },\n {\n match: 'Google-GeminiNotebook',\n product: 'Google-GeminiNotebook',\n purpose: 'user-action',\n },\n { match: 'Google-Agent', product: 'Google-Agent', purpose: 'user-action' },\n\n // --- Amazon ---\n { match: 'Amazonbot', product: 'Amazonbot', purpose: 'training' },\n\n // --- DuckDuckGo ---\n {\n match: 'DuckAssistBot',\n product: 'DuckAssistBot',\n purpose: 'user-action',\n },\n\n // --- ByteDance ---\n { match: 'Bytespider', product: 'Bytespider', purpose: 'training' },\n\n // --- Common Crawl ---\n { match: 'CCBot', product: 'CCBot', purpose: 'training' },\n];\n","/**\n * Curated non-AI crawler UA-substring map (2026-Q3).\n *\n * Sibling of `agents.ts`, which holds the AI agents and AI crawlers. The two\n * files stay separate on purpose: an AhrefsBot hit and a GPTBot hit answer\n * different questions. `agents.ts` is scanned FIRST, this file second; adding a\n * row whose match is a substring of an AI token would therefore be shadowed.\n *\n * Each entry: a substring matched case-insensitively against the User-Agent, a\n * product label, and the category written to botCategory.\n *\n * Category semantics:\n * - 'search-crawler' — a search engine index; correlates with organic discoverability\n * - 'seo-tool' — third-party commercial crawler building its own data set\n * - 'monitor' — uptime and synthetic monitoring, usually the site owner's own\n * - 'link-preview' — link unfurler, meaning a person just shared this URL\n *\n * Order matters: first-hit wins. More-specific entries must precede broader\n * ones. Three orderings are load-bearing and non-obvious:\n * - `adidxbot` before `bingbot`: every adidxbot UA carries the literal string\n * `bingbot` in its trailing info URL.\n * - `TelegramBot` before `Twitterbot`: Telegram's UA is `TelegramBot (like TwitterBot)`.\n * - `Googlebot` last in the Google block: it is a substring of `Googlebot-Image/1.0`\n * and `Googlebot-Video/1.0`.\n *\n * A UA match is a claim, not proof: Screaming Frog ships Googlebot and Bingbot\n * presets, and any client can send any UA. Vendor IP ranges and reverse DNS\n * suffixes for the entries that publish them are listed in\n * `docs/research/2026-08-20-search-crawler-list.md` and are consumed once\n * identity verification exists.\n *\n * Vendor docs of record (verified 2026-08):\n * Google: https://developers.google.com/search/docs/crawling-indexing/google-common-crawlers\n * Microsoft: https://www.bing.com/webmasters/help/which-crawlers-does-bing-use-8c184ec0\n * Apple: https://support.apple.com/en-us/119829\n * DuckDuckGo: https://duckduckgo.com/duckduckbot\n * Yandex: https://yandex.com/support/webmaster/robot-workings/check-yandex-robots.html\n * Baidu: http://help.baidu.com/question?prod_en=master&class=Baiduspider\n * Huawei: https://aspiegel.com/petalbot\n * Yahoo: https://help.yahoo.com/kb/SLN22600.html\n * Ahrefs: https://ahrefs.com/robot\n * Semrush: https://www.semrush.com/bot/\n * Majestic: https://mj12bot.com/\n * Screaming Frog: https://www.screamingfrog.co.uk/seo-spider/user-guide/configuration/\n * UptimeRobot: https://help.uptimerobot.com/en/articles/11358489-what-is-the-uptimerobot-user-agent-string\n * Meta: https://developers.facebook.com/docs/sharing/webmasters/web-crawlers\n * X: https://developer.x.com/en/docs/x-for-websites/cards/guides/getting-started\n * Slack: https://api.slack.com/robots\n * WhatsApp: https://developers.facebook.com/documentation/business-messaging/whatsapp/link-previews/\n *\n * Entries without a readable vendor page (SeznamBot, Yeti, Discordbot,\n * TelegramBot, Pingdom, StatusCake, DotBot) rest on the self-reference in the\n * UA string itself. Reviewed quarterly.\n *\n * Deliberately excluded: `Googlebot-News`, `Google-Extended` and\n * `Applebot-Extended` are robots.txt directives that never appear in a UA\n * header. `Google-Agent` and `Google-GeminiNotebook` are AI agents and live in\n * `agents.ts`.\n */\nexport type CrawlerCategory =\n | 'search-crawler'\n | 'seo-tool'\n | 'monitor'\n | 'link-preview';\n\nexport interface CrawlerEntry {\n match: string;\n product: string;\n category: CrawlerCategory;\n}\n\nexport const crawlers: CrawlerEntry[] = [\n // --- Search engines ---\n {\n match: 'Googlebot-Image',\n product: 'Googlebot Image',\n category: 'search-crawler',\n },\n {\n match: 'Googlebot-Video',\n product: 'Googlebot Video',\n category: 'search-crawler',\n },\n {\n match: 'Storebot-Google',\n product: 'Google StoreBot',\n category: 'search-crawler',\n },\n {\n match: 'Google-InspectionTool',\n product: 'Google InspectionTool',\n category: 'search-crawler',\n },\n {\n match: 'GoogleOther-Image',\n product: 'GoogleOther Image',\n category: 'search-crawler',\n },\n {\n match: 'GoogleOther-Video',\n product: 'GoogleOther Video',\n category: 'search-crawler',\n },\n { match: 'GoogleOther', product: 'GoogleOther', category: 'search-crawler' },\n { match: 'Googlebot', product: 'Googlebot', category: 'search-crawler' },\n { match: 'adidxbot', product: 'AdIdxBot', category: 'search-crawler' },\n { match: 'bingbot', product: 'Bingbot', category: 'search-crawler' },\n { match: 'Applebot', product: 'Applebot', category: 'search-crawler' },\n { match: 'DuckDuckBot', product: 'DuckDuckBot', category: 'search-crawler' },\n { match: 'YandexBot', product: 'YandexBot', category: 'search-crawler' },\n { match: 'Baiduspider', product: 'Baiduspider', category: 'search-crawler' },\n { match: 'PetalBot', product: 'PetalBot', category: 'search-crawler' },\n { match: 'SeznamBot', product: 'SeznamBot', category: 'search-crawler' },\n { match: 'Yeti/', product: 'Naver Yeti', category: 'search-crawler' },\n { match: 'Slurp', product: 'Yahoo Slurp', category: 'search-crawler' },\n\n // --- SEO tooling ---\n {\n match: 'AhrefsSiteAudit',\n product: 'Ahrefs Site Audit',\n category: 'seo-tool',\n },\n { match: 'AhrefsBot', product: 'AhrefsBot', category: 'seo-tool' },\n { match: 'SemrushBot', product: 'SemrushBot', category: 'seo-tool' },\n {\n match: 'SiteAuditBot',\n product: 'Semrush Site Audit',\n category: 'seo-tool',\n },\n { match: 'DotBot', product: 'Moz DotBot', category: 'seo-tool' },\n { match: 'MJ12bot', product: 'MJ12bot', category: 'seo-tool' },\n {\n match: 'Screaming Frog SEO Spider',\n product: 'Screaming Frog',\n category: 'seo-tool',\n },\n\n // --- Uptime and synthetic monitoring ---\n { match: 'UptimeRobot', product: 'UptimeRobot', category: 'monitor' },\n { match: 'Pingdom', product: 'Pingdom', category: 'monitor' },\n { match: 'StatusCake', product: 'StatusCake', category: 'monitor' },\n\n // --- Link unfurlers ---\n {\n match: 'facebookexternalhit',\n product: 'Meta external hit',\n category: 'link-preview',\n },\n { match: 'TelegramBot', product: 'TelegramBot', category: 'link-preview' },\n { match: 'Twitterbot', product: 'Twitterbot', category: 'link-preview' },\n { match: 'LinkedInBot', product: 'LinkedInBot', category: 'link-preview' },\n {\n match: 'Slackbot-LinkExpanding',\n product: 'Slack link expanding',\n category: 'link-preview',\n },\n { match: 'Slackbot', product: 'Slackbot', category: 'link-preview' },\n { match: 'Discordbot', product: 'Discordbot', category: 'link-preview' },\n { match: 'WhatsApp/', product: 'WhatsApp', category: 'link-preview' },\n];\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACCA,kBAA2C;;;ACwB3C,IAAM,eAAoC,oBAAI,IAAgB;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGM,SAAS,aAAa,OAAqC;AAChE,SAAO,OAAO,UAAU,YAAY,aAAa,IAAI,KAAK;AAC5D;AAqBO,IAAM,mBAGT;AAAA,EACF,YAAY;AAAA,IACV,MAAM;AAAA,IACN,OAAO,CAAC,UAAU;AAAA,IAClB,iBAAiB;AAAA,IACjB,aAAa;AAAA,EACf;AAAA,EACA,OAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO,CAAC,SAAS;AAAA,IACjB,iBAAiB;AAAA,IACjB,aAAa;AAAA,EACf;AAAA,EACA,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,OAAO,CAAC,SAAS;AAAA,IACjB,iBAAiB;AAAA,IACjB,aAAa;AAAA,IACb,mBAAmB;AAAA,EACrB;AAAA,EACA,OAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO,CAAC,QAAQ,SAAS;AAAA,IACzB,iBAAiB;AAAA,IACjB,aAAa;AAAA,EACf;AACF;AAGO,SAAS,cACd,SAC2B;AAC3B,SAAO,WAAW,YAAY,SAAS,UAAU;AACnD;AAGO,SAAS,eACd,SAC4B;AAC5B,SAAO,YAAY,WAAW,SAAY,iBAAiB,OAAO;AACpE;;;ACrGA,IAAAA,gBAAsB;;;ACiBtB,IAAM,OAAO;AAGb,IAAM,WAAW,CAAC,UAChB,MAAM,QAAQ,iBAAiB,EAAE,EAAE,YAAY,MAAM;AAGvD,SAAS,WAAW,QAAsC;AACxD,QAAM,QAAkB,CAAC;AACzB,MAAI,UAAU;AACd,MAAI,WAAW;AAEf,aAAW,QAAQ,QAAQ;AACzB,QAAI,SAAS,IAAK,YAAW,CAAC;AAC9B,QAAI,SAAS,OAAO,CAAC,UAAU;AAC7B,YAAM,KAAK,OAAO;AAClB,gBAAU;AACV;AAAA,IACF;AACA,eAAW;AAAA,EACb;AAEA,MAAI,SAAU,QAAO;AACrB,QAAM,KAAK,OAAO;AAClB,SAAO;AACT;AAGO,SAAS,aAAa,QAA6B;AACxD,MAAI,CAAC,OAAQ,QAAO,CAAC;AAErB,QAAM,QAAQ,WAAW,MAAM;AAC/B,MAAI,CAAC,MAAO,QAAO,CAAC;AAEpB,QAAM,SAAsB,CAAC;AAC7B,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,KAAK,KAAK,IAAI;AAC5B,QAAI,CAAC,MAAO,QAAO,CAAC;AACpB,UAAM,QAAQ,MAAM,CAAC;AACrB,QAAI,SAAS,KAAK,EAAG;AACrB,WAAO,KAAK,EAAE,OAAO,SAAS,MAAM,CAAC,KAAK,GAAG,CAAC;AAAA,EAChD;AAEA,SAAO;AACT;AAGO,SAAS,mBAAmB,QAAqC;AACtE,QAAM,QAAQ,OAAO,KAAK;AAC1B,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,UAAU,KAAM,QAAO;AAC3B,SAAO;AACT;AAGO,SAAS,qBAAqB,QAAoC;AACvE,QAAM,QAAQ,oBAAoB,KAAK,MAAM;AAC7C,SAAO,QAAQ,MAAM,CAAC,IAAI;AAC5B;;;AC3EA,mBAAsB;;;ACyCf,IAAM,SAAuB;AAAA;AAAA,EAElC,EAAE,OAAO,gBAAgB,SAAS,gBAAgB,SAAS,cAAc;AAAA,EACzE,EAAE,OAAO,iBAAiB,SAAS,iBAAiB,SAAS,cAAc;AAAA,EAC3E,EAAE,OAAO,iBAAiB,SAAS,iBAAiB,SAAS,eAAe;AAAA,EAC5E,EAAE,OAAO,UAAU,SAAS,UAAU,SAAS,WAAW;AAAA;AAAA;AAAA,EAI1D;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAAA,EACA,EAAE,OAAO,eAAe,SAAS,eAAe,SAAS,cAAc;AAAA,EACvE,EAAE,OAAO,eAAe,SAAS,eAAe,SAAS,cAAc;AAAA,EACvE,EAAE,OAAO,aAAa,SAAS,aAAa,SAAS,WAAW;AAAA;AAAA,EAEhE,EAAE,OAAO,gBAAgB,SAAS,gBAAgB,SAAS,WAAW;AAAA;AAAA,EAGtE;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAAA;AAAA,EAGA;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAAA;AAAA,EAGA;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAAA;AAAA,EAGA;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAAA,EACA,EAAE,OAAO,gBAAgB,SAAS,gBAAgB,SAAS,cAAc;AAAA;AAAA,EAGzE,EAAE,OAAO,aAAa,SAAS,aAAa,SAAS,WAAW;AAAA;AAAA,EAGhE;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAAA;AAAA,EAGA,EAAE,OAAO,cAAc,SAAS,cAAc,SAAS,WAAW;AAAA;AAAA,EAGlE,EAAE,OAAO,SAAS,SAAS,SAAS,SAAS,WAAW;AAC1D;;;ACjDO,IAAM,WAA2B;AAAA;AAAA,EAEtC;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,UAAU;AAAA,EACZ;AAAA,EACA;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,UAAU;AAAA,EACZ;AAAA,EACA,EAAE,OAAO,eAAe,SAAS,eAAe,UAAU,iBAAiB;AAAA,EAC3E,EAAE,OAAO,aAAa,SAAS,aAAa,UAAU,iBAAiB;AAAA,EACvE,EAAE,OAAO,YAAY,SAAS,YAAY,UAAU,iBAAiB;AAAA,EACrE,EAAE,OAAO,WAAW,SAAS,WAAW,UAAU,iBAAiB;AAAA,EACnE,EAAE,OAAO,YAAY,SAAS,YAAY,UAAU,iBAAiB;AAAA,EACrE,EAAE,OAAO,eAAe,SAAS,eAAe,UAAU,iBAAiB;AAAA,EAC3E,EAAE,OAAO,aAAa,SAAS,aAAa,UAAU,iBAAiB;AAAA,EACvE,EAAE,OAAO,eAAe,SAAS,eAAe,UAAU,iBAAiB;AAAA,EAC3E,EAAE,OAAO,YAAY,SAAS,YAAY,UAAU,iBAAiB;AAAA,EACrE,EAAE,OAAO,aAAa,SAAS,aAAa,UAAU,iBAAiB;AAAA,EACvE,EAAE,OAAO,SAAS,SAAS,cAAc,UAAU,iBAAiB;AAAA,EACpE,EAAE,OAAO,SAAS,SAAS,eAAe,UAAU,iBAAiB;AAAA;AAAA,EAGrE;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,UAAU;AAAA,EACZ;AAAA,EACA,EAAE,OAAO,aAAa,SAAS,aAAa,UAAU,WAAW;AAAA,EACjE,EAAE,OAAO,cAAc,SAAS,cAAc,UAAU,WAAW;AAAA,EACnE;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,UAAU;AAAA,EACZ;AAAA,EACA,EAAE,OAAO,UAAU,SAAS,cAAc,UAAU,WAAW;AAAA,EAC/D,EAAE,OAAO,WAAW,SAAS,WAAW,UAAU,WAAW;AAAA,EAC7D;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,UAAU;AAAA,EACZ;AAAA;AAAA,EAGA,EAAE,OAAO,eAAe,SAAS,eAAe,UAAU,UAAU;AAAA,EACpE,EAAE,OAAO,WAAW,SAAS,WAAW,UAAU,UAAU;AAAA,EAC5D,EAAE,OAAO,cAAc,SAAS,cAAc,UAAU,UAAU;AAAA;AAAA,EAGlE;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,UAAU;AAAA,EACZ;AAAA,EACA,EAAE,OAAO,eAAe,SAAS,eAAe,UAAU,eAAe;AAAA,EACzE,EAAE,OAAO,cAAc,SAAS,cAAc,UAAU,eAAe;AAAA,EACvE,EAAE,OAAO,eAAe,SAAS,eAAe,UAAU,eAAe;AAAA,EACzE;AAAA,IACE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,UAAU;AAAA,EACZ;AAAA,EACA,EAAE,OAAO,YAAY,SAAS,YAAY,UAAU,eAAe;AAAA,EACnE,EAAE,OAAO,cAAc,SAAS,cAAc,UAAU,eAAe;AAAA,EACvE,EAAE,OAAO,aAAa,SAAS,YAAY,UAAU,eAAe;AACtE;;;AFtJO,SAAS,SAAS,IAAsB;AAC7C,QAAM,QAAQ,GAAG,YAAY;AAC7B,QAAM,UAAU,OAAO,KAAK,CAAC,MAAM,MAAM,SAAS,EAAE,MAAM,YAAY,CAAC,CAAC;AACxE,SAAO;AAAA,IACL,OAAO,CAAC,UAAM,oBAAM,EAAE,KAAK,YAAY;AAAA,IACvC,OAAO,UACH,EAAE,SAAS,QAAQ,SAAS,SAAS,QAAQ,QAAQ,IACrD;AAAA,EACN;AACF;AAGO,SAAS,cAAc,IAAsC;AAClE,QAAM,QAAQ,GAAG,YAAY;AAC7B,SAAO,SAAS,KAAK,CAAC,MAAM,MAAM,SAAS,EAAE,MAAM,YAAY,CAAC,CAAC;AACnE;AAcA,IAAM,cAAc;AACpB,IAAM,iBAAiB;AACvB,IAAM,gBAAgB;AACtB,IAAM,iBAAiB;AAGvB,IAAM,wBAAwB;AAC9B,IAAM,0BAA0B;AAChC,IAAM,yBAAyB;AAC/B,IAAM,wBAAwB,EAAE,OAAO,IAAI,OAAO,EAAE;AAO7C,SAAS,cAAc,IAAsB;AAClD,QAAM,aAAa,YAAY,KAAK,EAAE;AACtC,QAAM,gBAAgB,eAAe,KAAK,EAAE;AAC5C,QAAM,gBACJ,CAAC,cAAc,gBAAgB,OAAO,cAAc,CAAC,CAAC,IAAI;AAE5D,MAAI,qBACF,kBAAkB,UAAa,iBAAiB;AAElD,MAAI,CAAC,sBAAsB,CAAC,YAAY;AACtC,UAAM,eAAe,cAAc,KAAK,EAAE;AAC1C,QAAI,cAAc;AAChB,2BAAqB,OAAO,aAAa,CAAC,CAAC,KAAK;AAAA,IAClD,WAAW,kBAAkB,UAAa,WAAW,KAAK,EAAE,GAAG;AAC7D,YAAM,cAAc,eAAe,KAAK,EAAE;AAC1C,UAAI,aAAa;AACf,cAAM,QAAQ,OAAO,YAAY,CAAC,CAAC;AACnC,cAAM,QAAQ,OAAO,YAAY,CAAC,KAAK,CAAC;AACxC,6BACE,QAAQ,sBAAsB,SAC7B,UAAU,sBAAsB,SAC/B,SAAS,sBAAsB;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,kBACE,kBAAkB,UAAa,iBAAiB;AAAA,IAClD;AAAA,IACA;AAAA,EACF;AACF;;;AFFO,IAAM,wBAAwB;AAGrC,IAAM,aAAa;AAEnB,IAAM,mBAAmB;AACzB,IAAM,sBAAsB;AAC5B,IAAM,cAAc;AACpB,IAAM,kBAAkB;AAExB,IAAM,mBAAiC;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AACF;AACA,IAAM,eAA6B,CAAC,kBAAkB,gBAAgB;AAGtE,IAAM,qBAA6C;AAAA,EACjD,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS;AAAA,EACT,KAAK;AAAA,EACL,OAAO;AAAA,EACP,aAAa;AAAA,EACb,eAAe;AACjB;AAEA,IAAM,mBAAmB;AAOzB,IAAM,gBAAgB,CAAC,UACrB,OAAO,UAAU,YAAY,UAAU;AAEzC,IAAM,eAAe,CAAC,YACpB,OAAO,OAAO,OAAO,EAAE,KAAK,aAAa;AAG3C,SAAS,gBAAgB,QAAyB;AAChD,QAAM,QAAQ,OAAO,MAAM,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK;AACtD,SAAO,UAAU;AACnB;AAOA,SAAS,iBACP,SACA,cACA,SACkC;AAClC,QAAM,OAAO,QAAQ;AACrB,QAAM,OAAO,QAAQ;AAErB,MAAI,cAAc;AAChB,QACG,cAAc,IAAI,KAAK,SAAS,QAAQ,QACxC,cAAc,IAAI,KAAK,CAAC,QAAQ,MAAM,SAAS,IAAI;AAEpD,aAAO;AAAA,EACX,WACE,SAAS,cACT,SAAS,cACR,CAAC,QAAQ,mBAAmB,cAAc,QAAQ,YAAY,GAC/D;AACA,WAAO;AAAA,EACT;AAEA,MACG,cAAc,IAAI,KAAK,SAAS,QAAQ,QACxC,cAAc,IAAI,KAAK,CAAC,QAAQ,MAAM,SAAS,IAAI;AAEpD,WAAO;AAET,SAAO;AACT;AAGA,SAAS,sBACP,SACA,SACS;AACT,QAAM,SAAS,QAAQ;AACvB,MAAI,CAAC,UAAU,CAAC,cAAc,QAAQ,WAAW,EAAG,QAAO;AAC3D,SAAO,CAAC,QAAQ,YAAY,YAAY,EAAE,WAAW,MAAM;AAC7D;AAGA,SAAS,wBAAwB,SAAkB,IAAqB;AACtE,MAAI,cAAc,QAAQ,eAAe,GAAG;AAC1C,UAAM,WAAW,qBAAqB,QAAQ,eAAe;AAC7D,UAAM,WAAW,WAAW,mBAAmB,QAAQ,IAAI;AAC3D,QAAI,YAAY,CAAC,SAAS,KAAK,EAAE,EAAG,QAAO;AAE3C,QAAI,aAAa,WAAW,WAAW,KAAK,EAAE,EAAG,QAAO;AAAA,EAC1D;AAEA,MACE,cAAc,QAAQ,aAAa,KACnC,mBAAmB,QAAQ,aAAa,MAAM,QAC9C,CAAC,iBAAiB,KAAK,EAAE;AAEzB,WAAO;AAET,SAAO;AACT;AAGA,SAAS,kBAAkB,SAAiB,QAA2B;AACrE,MAAI,OAAO,kBAAkB,OAAW,QAAO;AAC/C,QAAM,SAAS,aAAa,OAAO;AACnC,MAAI,CAAC,OAAO,OAAQ,QAAO;AAC3B,QAAM,QAAQ,OAAO,OAAO,aAAa;AACzC,SAAO,CAAC,OAAO,KAAK,CAAC,UAAU,MAAM,YAAY,KAAK;AACxD;AAEA,SAAS,qBACP,SACA,IACA,SACU;AACV,QAAM,QAAkB,CAAC;AACzB,QAAM,UAAU,UAAU,eAAe,OAAO,IAAI;AAEpD,MAAI,SAAS;AACX,QACE,iBAAiB,SAAS,YAAY,cAAc,OAAO,MAC3D;AAEA,YAAM,KAAK,kCAAkC;AAC/C,QAAI,sBAAsB,SAAS,OAAO;AACxC,YAAM,KAAK,qCAAqC;AAAA,EACpD;AAEA,MAAI,wBAAwB,SAAS,EAAE;AACrC,UAAM,KAAK,2BAA2B;AAExC,SAAO;AACT;AAEA,SAAS,eACP,SACA,IACA,SACA,UACiB;AACjB,QAAM,WAA4B,CAAC;AACnC,QAAM,SAAS,cAAc,EAAE;AAC/B,QAAM,UAAU,UAAU,eAAe,OAAO,IAAI;AAEpD,MAAI,cAAc,QAAQ,OAAO,GAAG;AAClC,QAAI,kBAAkB,QAAQ,SAAS,MAAM;AAC3C,eAAS,KAAK,EAAE,MAAM,uBAAuB,QAAQ,GAAG,CAAC;AAAA,EAC7D,WAAW,SAAS,IAAI,SAAS,KAAK,OAAO,kBAAkB;AAC7D,aAAS,KAAK,EAAE,MAAM,0BAA0B,QAAQ,GAAG,CAAC;AAAA,EAC9D;AAEA,MACE,SAAS,eACT,cAAc,QAAQ,MAAM,KAC5B,gBAAgB,QAAQ,MAAM;AAE9B,aAAS,KAAK,EAAE,MAAM,mCAAmC,QAAQ,GAAG,CAAC;AAEvE,QAAM,oBAAoB,iBAAiB;AAAA,IAAO,CAAC,SACjD,SAAS,IAAI,IAAI;AAAA,EACnB;AACA,MACE,kBAAkB,UAClB,kBAAkB,MAAM,CAAC,SAAS,CAAC,cAAc,QAAQ,IAAI,CAAC,CAAC,KAC/D,OAAO,oBACP;AACA,aAAS,KAAK,EAAE,MAAM,kCAAkC,QAAQ,GAAG,CAAC;AAAA,EACtE,WACE,WACA,iBAAiB,SAAS,YAAY,cAAc,OAAO,MAAM,YACjE;AACA,aAAS,KAAK,EAAE,MAAM,8BAA8B,QAAQ,GAAG,CAAC;AAAA,EAClE;AAEA,MAAI,SAAS,IAAI,gBAAgB,KAAK,CAAC,cAAc,QAAQ,cAAc;AACzE,aAAS,KAAK,EAAE,MAAM,2BAA2B,QAAQ,GAAG,CAAC;AAE/D,MAAI,SAAS,IAAI,gBAAgB,KAAK,CAAC,cAAc,QAAQ,cAAc;AACzE,aAAS,KAAK,EAAE,MAAM,2BAA2B,QAAQ,EAAE,CAAC;AAE9D,SAAO;AACT;AAoBO,SAAS,aACd,SACA,UAAwB,CAAC,GACZ;AACb,QAAM,UAAU,cAAc,QAAQ,OAAO;AAC7C,QAAM,WAAW,IAAI,IAAI,QAAQ,YAAY,CAAC,CAAC;AAC/C,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,KAAK,QAAQ,aAAa;AAIhC,QAAM,QAAkB,CAAC;AACzB,MAAI,cAAc,QAAQ,cAAc;AACtC,UAAM,KAAK,yBAAyB;AACtC,MAAI,CAAC,QAAS,OAAM,KAAK,sBAAsB;AAC/C,MAAI,CAAC,SAAS,IAAI,SAAS,EAAG,OAAM,KAAK,iBAAiB;AAC1D,MAAI,CAAC,iBAAiB,KAAK,CAAC,SAAS,SAAS,IAAI,IAAI,CAAC;AACrD,UAAM,KAAK,wBAAwB;AACrC,MAAI,CAAC,aAAa,KAAK,CAAC,SAAS,SAAS,IAAI,IAAI,CAAC;AACjD,UAAM,KAAK,qBAAqB;AAElC,MAAI,CAAC,aAAa,OAAO;AACvB,WAAO,EAAE,UAAU,MAAM,aAAa,WAAW,YAAY,MAAM;AAErE,MAAI,CAAC;AACH,WAAO;AAAA,MACL,UAAU;AAAA,MACV,aAAa;AAAA,MACb,YAAY,CAAC,cAAc,GAAG,KAAK;AAAA,IACrC;AAEF,QAAM,iBAAiB,qBAAqB,SAAS,IAAI,OAAO;AAChE,QAAM,WAAW,SAAS,EAAE;AAC5B,QAAM,UAAU,SAAS,QAAQ,SAAY,cAAc,EAAE;AAE7D,QAAM,QACJ,SAAS,QACL;AAAA,IACE,SAAS,SAAS,MAAM;AAAA,IACxB,UACE,SAAS,MAAM,YAAY,gBACvB,aACA;AAAA,EACR,IACA,UACE,EAAE,SAAS,QAAQ,SAAS,UAAU,QAAQ,SAAS,IACvD;AAER,MAAI,OAAO;AACT,QAAI,eAAe;AACjB,aAAO;AAAA,QACL,UAAU;AAAA,QACV,aAAa;AAAA,QACb,YAAY;AAAA,UACV;AAAA,UACA,GAAG;AAAA,UACH;AAAA,UACA,GAAG;AAAA,QACL;AAAA,MACF;AAEF,WAAO;AAAA,MACL,UAAU;AAAA,MACV,aAAa,MAAM;AAAA,MACnB,YAAY,MAAM;AAAA,MAClB,YAAY,CAAC,gBAAgB,GAAG,KAAK;AAAA,IACvC;AAAA,EACF;AAEA,UAAI,qBAAM,EAAE;AACV,WAAO;AAAA,MACL,UAAU;AAAA,MACV,aAAa;AAAA,MACb,YAAY,CAAC,YAAY,GAAG,KAAK;AAAA,IACnC;AAEF,MAAI,eAAe;AACjB,WAAO;AAAA,MACL,UAAU;AAAA,MACV,aAAa;AAAA,MACb,YAAY,CAAC,GAAG,gBAAgB,GAAG,KAAK;AAAA,IAC1C;AAEF,QAAM,WAAW,eAAe,SAAS,IAAI,SAAS,QAAQ;AAC9D,QAAM,QAAQ,KAAK;AAAA,IACjB,SAAS,OAAO,CAAC,KAAK,YAAY,MAAM,QAAQ,QAAQ,CAAC;AAAA,IACzD;AAAA,EACF;AAEA,SAAO;AAAA,IACL,UAAU;AAAA,IACV,aAAa,SAAS,eAAe,eAAe;AAAA,IACpD,YAAY,CAAC,GAAG,SAAS,IAAI,CAAC,YAAY,QAAQ,IAAI,GAAG,GAAG,KAAK;AAAA,EACnE;AACF;;;AFjYA,IAAM,gBAAoC;AAAA,EACxC,WAAW;AAAA,EACX,IAAI;AAAA,EACJ,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,cAAc;AAAA,EACd,cAAc;AAAA,EACd,cAAc;AAAA,EACd,SAAS;AAAA,EACT,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,QAAQ;AAAA,EACR,aAAa;AAAA,EACb,SAAS;AAAA,EACT,gBAAgB;AAAA,EAChB,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,aAAa;AACf;AAEA,IAAM,iBAAsC;AAAA,EAC1C,UAAU;AAAA,EACV,aAAa;AAAA,EACb,YAAY;AAAA;AAAA,EAEZ,YAAY;AACd;AASA,SAAS,cACP,KACA,MACA,OACM;AACN,QAAM,OAAO,KAAK,MAAM,GAAG;AAC3B,MAAI,MAA+B;AACnC,WAAS,IAAI,GAAG,IAAI,KAAK,SAAS,GAAG,KAAK;AACxC,UAAM,IAAI,KAAK,CAAC;AAChB,UAAM,OAAO,IAAI,CAAC;AAClB,QAAI,OAAO,SAAS,YAAY,SAAS,KAAM,KAAI,CAAC,IAAI,CAAC;AACzD,UAAM,IAAI,CAAC;AAAA,EACb;AACA,MAAI,KAAK,KAAK,SAAS,CAAC,CAAC,IAAI;AAC/B;AAEO,IAAM,iBAET,CAAC,YAAY;AACf,QAAM,EAAE,OAAO,IAAI;AACnB,QAAM,WAAwB,OAAO,YAAY,CAAC;AAClD,QAAM,QAA4B;AAAA,IAChC,GAAG;AAAA,IACH,GAAI,SAAS,SAAS,CAAC;AAAA,EACzB;AACA,QAAM,SAAoB;AAAA,IACxB,GAAG;AAAA,IACH,GAAI,SAAS,UAAU,CAAC;AAAA,EAC1B;AAIA,QAAM,WAAW,OAAO,KAAK,SAAS,SAAS,CAAC,CAAC;AAEjD,SAAO;AAAA;AAAA;AAAA,IAGL,MAAM;AAAA,IACN;AAAA,IAEA,MAAM,KAAK,OAAO,KAAK;AACrB,YAAM,EAAE,QAAQ,UAAU,IAAI;AAC9B,YAAM,SAAS,EAAE,OAAO,OAAO;AAE/B,YAAM,UAAU,OACd,UACgC;AAChC,cAAM,WAAW,UAAM,6BAAgB,QAAQ,OAAO,EAAE,UAAU,CAAC;AACnE,eAAO,OAAO,aAAa,YAAY,aAAa,KAChD,WACA;AAAA,MACN;AAEA,YAAM,UAAmB;AAAA,QACvB,WAAW,MAAM,QAAQ,MAAM,SAAS;AAAA,QACxC,IAAI,MAAM,QAAQ,MAAM,EAAE;AAAA,QAC1B,gBAAgB,MAAM,QAAQ,MAAM,cAAc;AAAA,QAClD,gBAAgB,MAAM,QAAQ,MAAM,cAAc;AAAA,QAClD,cAAc,MAAM,QAAQ,MAAM,YAAY;AAAA,QAC9C,cAAc,MAAM,QAAQ,MAAM,YAAY;AAAA,QAC9C,cAAc,MAAM,QAAQ,MAAM,YAAY;AAAA,QAC9C,cAAc,MAAM,QAAQ,MAAM,YAAY;AAAA,QAC9C,SAAS,MAAM,QAAQ,MAAM,OAAO;AAAA,QACpC,eAAe,MAAM,QAAQ,MAAM,aAAa;AAAA,QAChD,iBAAiB,MAAM,QAAQ,MAAM,eAAe;AAAA,QACpD,QAAQ,MAAM,QAAQ,MAAM,MAAM;AAAA,QAClC,aAAa,MAAM,QAAQ,MAAM,WAAW;AAAA,QAC5C,SAAS,MAAM,QAAQ,MAAM,OAAO;AAAA,QACpC,gBAAgB,MAAM,QAAQ,MAAM,cAAc;AAAA,QAClD,QAAQ,MAAM,QAAQ,MAAM,MAAM;AAAA,QAClC,KAAK,MAAM,QAAQ,MAAM,GAAG;AAAA,QAC5B,aAAa,MAAM,QAAQ,MAAM,WAAW;AAAA,MAC9C;AAEA,UAAIC;AACJ,UAAI,SAAS,YAAY,QAAW;AAClC,YAAI,aAAa,SAAS,OAAO,GAAG;AAClC,UAAAA,WAAU,SAAS;AAAA,QACrB,OAAO;AACL,gBAAM,WAAW,UAAM,6BAAgB,QAAQ,SAAS,SAAS;AAAA,YAC/D;AAAA,UACF,CAAC;AACD,UAAAA,WAAU,aAAa,QAAQ,IAAI,WAAW;AAAA,QAChD;AAAA,MACF;AAEA,YAAM,QAAQ,aAAa,SAAS;AAAA,QAClC,SAAAA;AAAA,QACA,cAAc,SAAS;AAAA,QACvB;AAAA,MACF,CAAC;AAED,UAAI,YAAY;AAEhB,YAAM,cAAc,CAClB,MACA,UACG;AACH,YAAI,CAAC,QAAQ,UAAU,OAAW;AAClC,YAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,gBAAM,UAAU,KAAK,MAAM,UAAU,MAAM;AAC3C,cAAI,CAAC,QAAS;AACd,wBAAc,QAAQ,SAAS,KAAK;AAAA,QACtC,OAAO;AACL,0BAAY,uBAAU,WAAW,MAAM,KAAK;AAAA,QAC9C;AAAA,MACF;AAEA,kBAAY,OAAO,UAAU,MAAM,QAAQ;AAC3C,kBAAY,OAAO,aAAa,MAAM,WAAW;AACjD,kBAAY,OAAO,YAAY,MAAM,UAAU;AAC/C,kBAAY,OAAO,YAAY,MAAM,UAAU;AAE/C,aAAO,EAAE,OAAO,UAAU;AAAA,IAC5B;AAAA,EACF;AACF;","names":["import_isbot","context"]}
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{getMappingValue as t,setByPath as e}from"@walkeros/core";import{isbot as o}from"isbot";var r=[{match:"ChatGPT-User",product:"ChatGPT-User",purpose:"user-action"},{match:"ChatGPT-Agent",product:"ChatGPT-Agent",purpose:"user-action"},{match:"OAI-SearchBot",product:"OAI-SearchBot",purpose:"search-index"},{match:"GPTBot",product:"GPTBot",purpose:"training"},{match:"Claude-SearchBot",product:"Claude-SearchBot",purpose:"search-index"},{match:"Claude-User",product:"Claude-User",purpose:"user-action"},{match:"Claude-Code",product:"Claude-Code",purpose:"user-action"},{match:"ClaudeBot",product:"ClaudeBot",purpose:"training"},{match:"anthropic-ai",product:"anthropic-ai",purpose:"training"},{match:"Perplexity-User",product:"Perplexity-User",purpose:"user-action"},{match:"PerplexityBot",product:"PerplexityBot",purpose:"search-index"},{match:"MistralAI-User",product:"MistralAI-User",purpose:"user-action"},{match:"Meta-ExternalFetcher",product:"Meta-ExternalFetcher",purpose:"user-action"},{match:"Meta-ExternalAgent",product:"Meta-ExternalAgent",purpose:"training"},{match:"Google-CloudVertexBot",product:"Google-CloudVertexBot",purpose:"training"},{match:"Google-Extended",product:"Google-Extended",purpose:"training"},{match:"Applebot-Extended",product:"Applebot-Extended",purpose:"training"},{match:"Amazonbot",product:"Amazonbot",purpose:"training"},{match:"DuckAssistBot",product:"DuckAssistBot",purpose:"user-action"},{match:"Bytespider",product:"Bytespider",purpose:"training"},{match:"CCBot",product:"CCBot",purpose:"training"}];function c(t){if(!t)return{botScore:70,agentScore:0,agentProduct:void 0};const e=function(t){const e=t.toLowerCase(),c=r.find(t=>e.includes(t.match.toLowerCase()));return{isBot:!t||o(t)||void 0!==c,agent:c?{product:c.product,purpose:c.purpose}:void 0}}(t);let c=0;return e.agent?c="user-action"===e.agent.purpose?90:95:e.isBot&&(c=80),{botScore:c,agentScore:e.agent?95:0,agentProduct:e.agent?.product}}var a={userAgent:"ingest.userAgent",ip:"ingest.ip",acceptLanguage:"ingest.acceptLanguage",acceptEncoding:"ingest.acceptEncoding",secFetchSite:"ingest.secFetchSite",secFetchMode:"ingest.secFetchMode",secFetchDest:"ingest.secFetchDest",secFetchUser:"ingest.secFetchUser",secChUa:"ingest.secChUa",secChUaMobile:"ingest.secChUaMobile",secChUaPlatform:"ingest.secChUaPlatform"},n={botScore:"user.botScore",agentScore:"user.agentScore",agentProduct:""};var s=o=>{const{config:r}=o,s=r.settings??{},p={...a,...s.input??{}},u={...n,...s.output??{}};return{type:"bot",config:r,async push(o,r){const{ingest:a,collector:n}=r,s={event:o,ingest:a},i=await t(s,p.userAgent,{collector:n}),d=c("string"==typeof i?i:"");let g=o;const h=(t,o)=>{if(t&&void 0!==o)if(t.startsWith("ingest.")){const e=t.slice(7);if(!e)return;!function(t,e,o){const r=e.split(".");let c=t;for(let t=0;t<r.length-1;t++){const e=r[t],o=c[e];"object"==typeof o&&null!==o||(c[e]={}),c=c[e]}c[r[r.length-1]]=o}(a,e,o)}else g=e(g,t,o)};return h(u.botScore??"",d.botScore),h(u.agentScore??"",d.agentScore),h(u.agentProduct??"",d.agentProduct),{event:g}}}};export{s as default,s as transformerBot};//# sourceMappingURL=index.mjs.map
1
+ import{getMappingValue as t,setByPath as e}from"@walkeros/core";var o=new Set(["auto","navigation","pixel","beacon","fetch","server"]);function c(t){return"string"==typeof t&&o.has(t)}var r={navigation:{dest:"document",modes:["navigate"],allowsFetchUser:!0,typedAccept:!0},pixel:{dest:"image",modes:["no-cors"],allowsFetchUser:!1,typedAccept:!0},beacon:{dest:"empty",modes:["no-cors"],allowsFetchUser:!1,typedAccept:!1,contentTypePrefix:"text/plain"},fetch:{dest:"empty",modes:["cors","no-cors"],allowsFetchUser:!1,typedAccept:!1}};function a(t){return"server"===t?void 0:r[t]}import{isbot as s}from"isbot";var n=/^\s*"([^"]*)"\s*(?:;\s*v\s*=\s*"?([^";]*)"?)?\s*$/,i=t=>"notabrand"===t.replace(/[^a-zA-Z0-9]/g,"").toLowerCase();import{isbot as u}from"isbot";var p=[{match:"ChatGPT-User",product:"ChatGPT-User",purpose:"user-action"},{match:"ChatGPT-Agent",product:"ChatGPT-Agent",purpose:"user-action"},{match:"OAI-SearchBot",product:"OAI-SearchBot",purpose:"search-index"},{match:"GPTBot",product:"GPTBot",purpose:"training"},{match:"Claude-SearchBot",product:"Claude-SearchBot",purpose:"search-index"},{match:"Claude-User",product:"Claude-User",purpose:"user-action"},{match:"Claude-Code",product:"Claude-Code",purpose:"user-action"},{match:"ClaudeBot",product:"ClaudeBot",purpose:"training"},{match:"anthropic-ai",product:"anthropic-ai",purpose:"training"},{match:"Perplexity-User",product:"Perplexity-User",purpose:"user-action"},{match:"PerplexityBot",product:"PerplexityBot",purpose:"search-index"},{match:"MistralAI-User",product:"MistralAI-User",purpose:"user-action"},{match:"Meta-ExternalFetcher",product:"Meta-ExternalFetcher",purpose:"user-action"},{match:"Meta-ExternalAgent",product:"Meta-ExternalAgent",purpose:"training"},{match:"Google-CloudVertexBot",product:"Google-CloudVertexBot",purpose:"training"},{match:"Google-GeminiNotebook",product:"Google-GeminiNotebook",purpose:"user-action"},{match:"Google-Agent",product:"Google-Agent",purpose:"user-action"},{match:"Amazonbot",product:"Amazonbot",purpose:"training"},{match:"DuckAssistBot",product:"DuckAssistBot",purpose:"user-action"},{match:"Bytespider",product:"Bytespider",purpose:"training"},{match:"CCBot",product:"CCBot",purpose:"training"}],h=[{match:"Googlebot-Image",product:"Googlebot Image",category:"search-crawler"},{match:"Googlebot-Video",product:"Googlebot Video",category:"search-crawler"},{match:"Storebot-Google",product:"Google StoreBot",category:"search-crawler"},{match:"Google-InspectionTool",product:"Google InspectionTool",category:"search-crawler"},{match:"GoogleOther-Image",product:"GoogleOther Image",category:"search-crawler"},{match:"GoogleOther-Video",product:"GoogleOther Video",category:"search-crawler"},{match:"GoogleOther",product:"GoogleOther",category:"search-crawler"},{match:"Googlebot",product:"Googlebot",category:"search-crawler"},{match:"adidxbot",product:"AdIdxBot",category:"search-crawler"},{match:"bingbot",product:"Bingbot",category:"search-crawler"},{match:"Applebot",product:"Applebot",category:"search-crawler"},{match:"DuckDuckBot",product:"DuckDuckBot",category:"search-crawler"},{match:"YandexBot",product:"YandexBot",category:"search-crawler"},{match:"Baiduspider",product:"Baiduspider",category:"search-crawler"},{match:"PetalBot",product:"PetalBot",category:"search-crawler"},{match:"SeznamBot",product:"SeznamBot",category:"search-crawler"},{match:"Yeti/",product:"Naver Yeti",category:"search-crawler"},{match:"Slurp",product:"Yahoo Slurp",category:"search-crawler"},{match:"AhrefsSiteAudit",product:"Ahrefs Site Audit",category:"seo-tool"},{match:"AhrefsBot",product:"AhrefsBot",category:"seo-tool"},{match:"SemrushBot",product:"SemrushBot",category:"seo-tool"},{match:"SiteAuditBot",product:"Semrush Site Audit",category:"seo-tool"},{match:"DotBot",product:"Moz DotBot",category:"seo-tool"},{match:"MJ12bot",product:"MJ12bot",category:"seo-tool"},{match:"Screaming Frog SEO Spider",product:"Screaming Frog",category:"seo-tool"},{match:"UptimeRobot",product:"UptimeRobot",category:"monitor"},{match:"Pingdom",product:"Pingdom",category:"monitor"},{match:"StatusCake",product:"StatusCake",category:"monitor"},{match:"facebookexternalhit",product:"Meta external hit",category:"link-preview"},{match:"TelegramBot",product:"TelegramBot",category:"link-preview"},{match:"Twitterbot",product:"Twitterbot",category:"link-preview"},{match:"LinkedInBot",product:"LinkedInBot",category:"link-preview"},{match:"Slackbot-LinkExpanding",product:"Slack link expanding",category:"link-preview"},{match:"Slackbot",product:"Slackbot",category:"link-preview"},{match:"Discordbot",product:"Discordbot",category:"link-preview"},{match:"WhatsApp/",product:"WhatsApp",category:"link-preview"}];var d=/(CriOS|FxiOS|EdgiOS|OPiOS|EdgA?iOS)\//,g=/Chrom(?:e|ium)\/(\d+)/,l=/Firefox\/(\d+)/,m=/Version\/(\d+)(?:\.(\d+))?/,b=16,f=4;var y=["secFetchSite","secFetchMode","secFetchDest"],w=["acceptLanguage","acceptEncoding"],C={Windows:/Windows/i,macOS:/Macintosh|Mac OS X/i,Android:/Android/i,iOS:/iPhone|iPad|iPod/i,Linux:/Linux/i,"Chrome OS":/CrOS/i,"Chromium OS":/CrOS/i},S=/Mobile|Android|iPhone|iPad|iPod/i,v=t=>"string"==typeof t&&""!==t;function A(t,e,o){const c=o.secFetchDest,r=o.secFetchMode;if(e){if(v(c)&&c!==t.dest||v(r)&&!t.modes.includes(r))return"impossible"}else if("document"===c||"navigate"===r||!t.allowsFetchUser&&v(o.secFetchUser))return"impossible";return v(c)&&c!==t.dest||v(r)&&!t.modes.includes(r)?"mismatch":"ok"}function x(t,e){if(void 0===e.chromiumMajor)return!1;const o=function(t){if(!t)return[];const e=function(t){const e=[];let o="",c=!1;for(const r of t)'"'===r&&(c=!c),","!==r||c?o+=r:(e.push(o),o="");if(!c)return e.push(o),e}(t);if(!e)return[];const o=[];for(const t of e){const e=n.exec(t);if(!e)return[];const c=e[1];i(c)||o.push({brand:c,version:e[2]??""})}return o}(t);if(!o.length)return!1;const c=String(e.chromiumMajor);return!o.some(t=>t.version===c)}function _(t,e,o){const c=[],r=o?a(o):void 0;return r&&("impossible"===A(r,"navigation"===o,t)&&c.push("fetchmeta_impossible_for_context"),function(t,e){const o=t.contentTypePrefix;return!(!o||!v(e.contentType)||e.contentType.toLowerCase().startsWith(o))}(r,t)&&c.push("content_type_impossible_for_context")),function(t,e){if(v(t.secChUaPlatform)){const o=function(t){const e=/^\s*"([^"]*)"\s*$/.exec(t);return e?e[1]:void 0}(t.secChUaPlatform),c=o?C[o]:void 0;if(c&&!c.test(e))return!0;if("Linux"===o&&/Android/i.test(e))return!0}return!(!v(t.secChUaMobile)||!0!==function(t){const e=t.trim();return"?1"===e||"?0"!==e&&void 0}(t.secChUaMobile)||S.test(e))}(t,e)&&c.push("ch_platform_contradiction"),c}function B(t,e,o,c){const r=[],s=function(t){const e=d.test(t),o=g.exec(t),c=!e&&o?Number(o[1]):void 0;let r=void 0!==c&&c>=76;if(!r&&!e){const e=l.exec(t);if(e)r=Number(e[1])>=90;else if(void 0===c&&/Safari\//.test(t)){const e=m.exec(t);if(e){const t=Number(e[1]),o=Number(e[2]??0);r=t>b||t===b&&o>=f}}}return{sendsClientHints:void 0!==c&&c>=89,chromiumMajor:c,shipsFetchMetadata:r}}(e),n=o?a(o):void 0;v(t.secChUa)?x(t.secChUa,s)&&r.push({code:"ch_version_mismatch",weight:30}):c.has("secChUa")&&s.sendsClientHints&&r.push({code:"ch_missing_on_chromium",weight:25}),n?.typedAccept&&v(t.accept)&&"*/*"===t.accept.split(",")[0].split(";")[0].trim()&&r.push({code:"accept_generic_on_typed_context",weight:25});const i=y.filter(t=>c.has(t));return i.length&&i.every(e=>!v(t[e]))&&s.shipsFetchMetadata?r.push({code:"fetchmeta_missing_on_modern_ua",weight:15}):n&&"mismatch"===A(n,"navigation"===o,t)&&r.push({code:"fetchmeta_profile_mismatch",weight:15}),c.has("acceptLanguage")&&!v(t.acceptLanguage)&&r.push({code:"accept_language_missing",weight:10}),c.has("acceptEncoding")&&!v(t.acceptEncoding)&&r.push({code:"accept_encoding_missing",weight:5}),r}function U(t,e={}){const o=function(t){return t&&"auto"!==t?t:void 0}(e.context),c=new Set(e.declared??[]),r=e.suspiciousAt??25,a=t.userAgent??"",n=[];if(v(t.signatureAgent)&&n.push("signature_agent_present"),o||n.push("context_undetermined"),c.has("secChUa")||n.push("ch_not_declared"),y.some(t=>c.has(t))||n.push("fetchmeta_not_declared"),w.some(t=>c.has(t))||n.push("accept_not_declared"),!(t=>Object.values(t).some(v))(t))return{botScore:null,botCategory:"unknown",botReasons:n};if(!a)return{botScore:70,botCategory:"automation",botReasons:["ua_missing",...n]};const i=_(t,a,o),d=function(t){const e=t.toLowerCase(),o=p.find(t=>e.includes(t.match.toLowerCase()));return{isBot:!t||u(t)||void 0!==o,agent:o?{product:o.product,purpose:o.purpose}:void 0}}(a),g=d.agent?void 0:function(t){const e=t.toLowerCase();return h.find(t=>e.includes(t.match.toLowerCase()))}(a),l=d.agent?{product:d.agent.product,category:"user-action"===d.agent.purpose?"ai-agent":"ai-crawler"}:g?{product:g.product,category:g.category}:void 0;if(l)return i.length?{botScore:90,botCategory:"automation",botReasons:["ua_named_bot",...i,"identity_claim_contradicted",...n]}:{botScore:90,botCategory:l.category,botProduct:l.product,botReasons:["ua_named_bot",...n]};if(s(a))return{botScore:80,botCategory:"automation",botReasons:["ua_isbot",...n]};if(i.length)return{botScore:75,botCategory:"automation",botReasons:[...i,...n]};const m=B(t,a,o,c),b=Math.min(m.reduce((t,e)=>t+e.weight,0),60);return{botScore:b,botCategory:b>=r?"suspicious":"human",botReasons:[...m.map(t=>t.code),...n]}}var F={userAgent:"ingest.userAgent",ip:"ingest.ip",acceptLanguage:"ingest.acceptLanguage",acceptEncoding:"ingest.acceptEncoding",secFetchSite:"ingest.secFetchSite",secFetchMode:"ingest.secFetchMode",secFetchDest:"ingest.secFetchDest",secFetchUser:"ingest.secFetchUser",secChUa:"ingest.secChUa",secChUaMobile:"ingest.secChUaMobile",secChUaPlatform:"ingest.secChUaPlatform",accept:"ingest.accept",contentType:"ingest.contentType",referer:"ingest.referer",signatureAgent:"ingest.signatureAgent",method:"ingest.method",ja4:"ingest.ja4",headerNames:"ingest.headerNames"},P={botScore:"user.botScore",botCategory:"user.botCategory",botProduct:"user.botProduct",botReasons:"ingest.bot.reasons"};var k=o=>{const{config:r}=o,a=r.settings??{},s={...F,...a.input??{}},n={...P,...a.output??{}},i=Object.keys(a.input??{});return{type:"bot",config:r,async push(o,r){const{ingest:u,collector:p}=r,h={event:o,ingest:u},d=async e=>{const o=await t(h,e,{collector:p});return"string"==typeof o&&""!==o?o:void 0},g={userAgent:await d(s.userAgent),ip:await d(s.ip),acceptLanguage:await d(s.acceptLanguage),acceptEncoding:await d(s.acceptEncoding),secFetchSite:await d(s.secFetchSite),secFetchMode:await d(s.secFetchMode),secFetchDest:await d(s.secFetchDest),secFetchUser:await d(s.secFetchUser),secChUa:await d(s.secChUa),secChUaMobile:await d(s.secChUaMobile),secChUaPlatform:await d(s.secChUaPlatform),accept:await d(s.accept),contentType:await d(s.contentType),referer:await d(s.referer),signatureAgent:await d(s.signatureAgent),method:await d(s.method),ja4:await d(s.ja4),headerNames:await d(s.headerNames)};let l;if(void 0!==a.context)if(c(a.context))l=a.context;else{const e=await t(h,a.context,{collector:p});l=c(e)?e:"auto"}const m=U(g,{context:l,suspiciousAt:a.suspiciousAt,declared:i});let b=o;const f=(t,o)=>{if(t&&void 0!==o)if(t.startsWith("ingest.")){const e=t.slice(7);if(!e)return;!function(t,e,o){const c=e.split(".");let r=t;for(let t=0;t<c.length-1;t++){const e=c[t],o=r[e];"object"==typeof o&&null!==o||(r[e]={}),r=r[e]}r[c[c.length-1]]=o}(u,e,o)}else b=e(b,t,o)};return f(n.botScore,m.botScore),f(n.botCategory,m.botCategory),f(n.botProduct,m.botProduct),f(n.botReasons,m.botReasons),{event:b}}}};export{k as default,k as transformerBot};//# sourceMappingURL=index.mjs.map