@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/CHANGELOG.md +25 -0
- package/README.md +283 -53
- package/dist/dev.d.mts +108 -9
- package/dist/dev.d.ts +108 -9
- package/dist/dev.js +1 -1
- package/dist/dev.js.map +1 -1
- package/dist/dev.mjs +1 -1
- package/dist/dev.mjs.map +1 -1
- package/dist/index.d.mts +147 -7
- package/dist/index.d.ts +147 -7
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/walkerOS.json +759 -38
- package/package.json +2 -2
package/dist/dev.d.ts
CHANGED
|
@@ -103,12 +103,93 @@ declare const SettingsSchema: z.ZodObject<{
|
|
|
103
103
|
value: z.ZodOptional<z.ZodUnknown>;
|
|
104
104
|
fn: z.ZodOptional<z.ZodString>;
|
|
105
105
|
}, z.core.$strip>]>>]>>;
|
|
106
|
+
accept: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
107
|
+
key: z.ZodOptional<z.ZodString>;
|
|
108
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
109
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
110
|
+
}, z.core.$strip>]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
111
|
+
key: z.ZodOptional<z.ZodString>;
|
|
112
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
113
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
114
|
+
}, z.core.$strip>]>>]>>;
|
|
115
|
+
contentType: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
116
|
+
key: z.ZodOptional<z.ZodString>;
|
|
117
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
118
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
119
|
+
}, z.core.$strip>]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
120
|
+
key: z.ZodOptional<z.ZodString>;
|
|
121
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
122
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
123
|
+
}, z.core.$strip>]>>]>>;
|
|
124
|
+
referer: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
125
|
+
key: z.ZodOptional<z.ZodString>;
|
|
126
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
127
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
128
|
+
}, z.core.$strip>]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
129
|
+
key: z.ZodOptional<z.ZodString>;
|
|
130
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
131
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
132
|
+
}, z.core.$strip>]>>]>>;
|
|
133
|
+
signatureAgent: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
134
|
+
key: z.ZodOptional<z.ZodString>;
|
|
135
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
136
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
137
|
+
}, z.core.$strip>]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
138
|
+
key: z.ZodOptional<z.ZodString>;
|
|
139
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
140
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
141
|
+
}, z.core.$strip>]>>]>>;
|
|
142
|
+
method: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
143
|
+
key: z.ZodOptional<z.ZodString>;
|
|
144
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
145
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
146
|
+
}, z.core.$strip>]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
147
|
+
key: z.ZodOptional<z.ZodString>;
|
|
148
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
149
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
150
|
+
}, z.core.$strip>]>>]>>;
|
|
151
|
+
ja4: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
152
|
+
key: z.ZodOptional<z.ZodString>;
|
|
153
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
154
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
155
|
+
}, z.core.$strip>]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
156
|
+
key: z.ZodOptional<z.ZodString>;
|
|
157
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
158
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
159
|
+
}, z.core.$strip>]>>]>>;
|
|
160
|
+
headerNames: z.ZodOptional<z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
161
|
+
key: z.ZodOptional<z.ZodString>;
|
|
162
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
163
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
164
|
+
}, z.core.$strip>]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
165
|
+
key: z.ZodOptional<z.ZodString>;
|
|
166
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
167
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
168
|
+
}, z.core.$strip>]>>]>>;
|
|
106
169
|
}, z.core.$strip>>;
|
|
107
170
|
output: z.ZodOptional<z.ZodObject<{
|
|
108
|
-
botScore: z.ZodOptional<z.ZodString
|
|
109
|
-
|
|
110
|
-
|
|
171
|
+
botScore: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
|
|
172
|
+
botCategory: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
|
|
173
|
+
botProduct: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
|
|
174
|
+
botReasons: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
|
|
111
175
|
}, z.core.$strip>>;
|
|
176
|
+
context: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
|
|
177
|
+
auto: "auto";
|
|
178
|
+
navigation: "navigation";
|
|
179
|
+
pixel: "pixel";
|
|
180
|
+
beacon: "beacon";
|
|
181
|
+
fetch: "fetch";
|
|
182
|
+
server: "server";
|
|
183
|
+
}>, z.ZodString, z.ZodObject<{
|
|
184
|
+
key: z.ZodOptional<z.ZodString>;
|
|
185
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
186
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
187
|
+
}, z.core.$strip>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
188
|
+
key: z.ZodOptional<z.ZodString>;
|
|
189
|
+
value: z.ZodOptional<z.ZodUnknown>;
|
|
190
|
+
fn: z.ZodOptional<z.ZodString>;
|
|
191
|
+
}, z.core.$strip>]>>]>>;
|
|
192
|
+
suspiciousAt: z.ZodOptional<z.ZodNumber>;
|
|
112
193
|
}, z.core.$strip>;
|
|
113
194
|
type Settings = z.infer<typeof SettingsSchema>;
|
|
114
195
|
|
|
@@ -121,24 +202,42 @@ declare namespace index$1 {
|
|
|
121
202
|
export { type index$1_Settings as Settings, index$1_SettingsSchema as SettingsSchema, index$1_settings as settings };
|
|
122
203
|
}
|
|
123
204
|
|
|
124
|
-
/** Real Chrome
|
|
205
|
+
/** Real Chrome, nothing to report. */
|
|
125
206
|
declare const humanChrome: Flow.StepExample;
|
|
126
|
-
/** GPTBot
|
|
207
|
+
/** GPTBot, an AI training crawler. */
|
|
127
208
|
declare const gptBotCrawler: Flow.StepExample;
|
|
128
|
-
/** ChatGPT-User
|
|
209
|
+
/** ChatGPT-User, an AI agent acting for a person. */
|
|
129
210
|
declare const chatgptUserAgent: Flow.StepExample;
|
|
130
|
-
/**
|
|
211
|
+
/** Googlebot, a search crawler: the one named bot that renders JavaScript. */
|
|
212
|
+
declare const searchCrawler: Flow.StepExample;
|
|
213
|
+
/**
|
|
214
|
+
* The same request, twice, through one instance configured
|
|
215
|
+
* `context: [{ key: "ingest.transport" }, { value: "beacon" }]`.
|
|
216
|
+
*/
|
|
217
|
+
declare const pixelWildcardAccept: Flow.StepExample;
|
|
218
|
+
/** Byte-identical headers, no annotation: the fallback entry decides. */
|
|
219
|
+
declare const unannotatedBeaconFallback: Flow.StepExample;
|
|
220
|
+
/** A Chromium UA whose client hints disagree with it. */
|
|
221
|
+
declare const headerMismatch: Flow.StepExample;
|
|
222
|
+
/** curl, caught by isbot but not named. */
|
|
131
223
|
declare const curlClient: Flow.StepExample;
|
|
132
|
-
/**
|
|
224
|
+
/** No User-Agent on an otherwise wired request. */
|
|
133
225
|
declare const missingUA: Flow.StepExample;
|
|
226
|
+
/** Nothing in ingest at all: not measured, which is not the same as human. */
|
|
227
|
+
declare const unwiredPipeline: Flow.StepExample;
|
|
134
228
|
|
|
135
229
|
declare const step_chatgptUserAgent: typeof chatgptUserAgent;
|
|
136
230
|
declare const step_curlClient: typeof curlClient;
|
|
137
231
|
declare const step_gptBotCrawler: typeof gptBotCrawler;
|
|
232
|
+
declare const step_headerMismatch: typeof headerMismatch;
|
|
138
233
|
declare const step_humanChrome: typeof humanChrome;
|
|
139
234
|
declare const step_missingUA: typeof missingUA;
|
|
235
|
+
declare const step_pixelWildcardAccept: typeof pixelWildcardAccept;
|
|
236
|
+
declare const step_searchCrawler: typeof searchCrawler;
|
|
237
|
+
declare const step_unannotatedBeaconFallback: typeof unannotatedBeaconFallback;
|
|
238
|
+
declare const step_unwiredPipeline: typeof unwiredPipeline;
|
|
140
239
|
declare namespace step {
|
|
141
|
-
export { step_chatgptUserAgent as chatgptUserAgent, step_curlClient as curlClient, step_gptBotCrawler as gptBotCrawler, step_humanChrome as humanChrome, step_missingUA as missingUA };
|
|
240
|
+
export { step_chatgptUserAgent as chatgptUserAgent, step_curlClient as curlClient, step_gptBotCrawler as gptBotCrawler, step_headerMismatch as headerMismatch, step_humanChrome as humanChrome, step_missingUA as missingUA, step_pixelWildcardAccept as pixelWildcardAccept, step_searchCrawler as searchCrawler, step_unannotatedBeaconFallback as unannotatedBeaconFallback, step_unwiredPipeline as unwiredPipeline };
|
|
142
241
|
}
|
|
143
242
|
|
|
144
243
|
declare const index_step: typeof step;
|
package/dist/dev.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var e,t=Object.defineProperty,r=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,n=Object.prototype.hasOwnProperty,i=(e,r)=>{for(var o in r)t(e,o,{get:r[o],enumerable:!0})},a={};i(a,{examples:()=>h,hints:()=>w,schemas:()=>s}),module.exports=(e=a,((e,i,a,s)=>{if(i&&"object"==typeof i||"function"==typeof i)for(let c of o(i))n.call(e,c)||c===a||t(e,c,{get:()=>i[c],enumerable:!(s=r(i,c))||s.enumerable});return e})(t({},"__esModule",{value:!0}),e));var s={};i(s,{SettingsSchema:()=>p,settings:()=>d});var c=require("@walkeros/core/dev"),l=require("@walkeros/core/dev"),g=l.z.union([l.z.string().describe('Dot-notation path like "ingest.userAgent"'),l.z.object({key:l.z.string().optional(),value:l.z.unknown().optional(),fn:l.z.string().optional()}).describe("Mapping value object")]),u=l.z.union([g,l.z.array(g).describe("Array of fallback values, tried in order")]),p=l.z.object({input:l.z.object({userAgent:u.optional(),ip:u.optional(),acceptLanguage:u.optional(),acceptEncoding:u.optional(),secFetchSite:u.optional(),secFetchMode:u.optional(),secFetchDest:u.optional(),secFetchUser:u.optional(),secChUa:u.optional(),secChUaMobile:u.optional(),secChUaPlatform:u.optional()}).optional().describe("Input signal sources, resolved via getMappingValue against { event, ingest }. v1 only reads userAgent; other fields reserved for v1.1 header heuristics."),output:l.z.object({botScore:l.z.string().optional().describe('Path for bot score (0-99, higher = more bot). Default: "user.botScore". Use "ingest.*" to route to pipeline scratch instead of the event. Empty string or omit = skip.'),agentScore:l.z.string().optional().describe('Path for AI agent score (0-99). v1 emits 0 (no match) or 95 (UA-map match). Default: "user.agentScore".'),agentProduct:l.z.string().optional().describe('Path for matched UA substring (e.g. "ChatGPT-User"). Off by default — set to enable.')}).optional().describe("Output paths for bot/agent annotations.")}).describe("Bot detection transformer: annotates events with bot and AI-agent scores."),d=(0,c.zodToSchema)(p),h={};i(h,{step:()=>b});var b={};i(b,{chatgptUserAgent:()=>A,curlClient:()=>y,gptBotCrawler:()=>v,humanChrome:()=>f,missingUA:()=>S});var m={name:"page view",data:{title:"Home",id:"/"},id:"ev-1700000600",trigger:"load",entity:"page",action:"view",timestamp:1700000600,source:{type:"express",platform:"server"}},f={title:"Human visitor (Chrome)",description:"Modern Chrome UA. No bot or agent signals.",in:{...m},out:[["return",{event:{...m,user:{botScore:0,agentScore:0}}}]]},v={title:"GPTBot training crawler",description:"OpenAI training crawler. Both botScore and agentScore are high.",in:{...m,id:"ev-1700000601"},out:[["return",{event:{...m,id:"ev-1700000601",user:{botScore:95,agentScore:95}}}]]},A={title:"ChatGPT-User (user-action AI)",description:"A real human routed an AI to fetch this page. botScore high but lower than crawlers — agentProduct lets destinations keep this traffic.",in:{...m,id:"ev-1700000602"},out:[["return",{event:{...m,id:"ev-1700000602",user:{botScore:90,agentScore:95}}}]]},y={public:!1,description:"curl client — caught by isbot. agentScore zero.",in:{...m,id:"ev-1700000603"},out:[["return",{event:{...m,id:"ev-1700000603",user:{botScore:80,agentScore:0}}}]]},S={public:!1,description:"No User-Agent — baseline 70 (UA stripping is overwhelmingly bots or hardened privacy tools).",in:{...m,id:"ev-1700000604"},out:[["return",{event:{...m,id:"ev-1700000604",user:{botScore:70,agentScore:0}}}]]},w={"ingest-prerequisite":{text:'The bot transformer reads userAgent from ctx.ingest (path "ingest.userAgent" by default). The upstream server source must populate it via config.ingest, which MUST use the map operator with direct request field paths (no req. prefix); a bare object like { userAgent: "req.headers.user-agent" } is silently inert and leaves ingest empty. Without populated ingest the UA is empty and every event scores 70 (baseline for missing UA).',code:[{lang:"json",code:JSON.stringify({sources:{express:{package:"@walkeros/server-source-express",config:{ingest:{map:{userAgent:{key:"headers.user-agent"}}}}}},transformers:{bot:{package:"@walkeros/server-transformer-bot"}}},null,2)}]},"output-routing":{text:"Outputs default to event.user.botScore and event.user.agentScore. Redirect to ingest.* to keep the analytics event clean while still routing on the score downstream. Empty string (or omit) skips writing that field entirely. agentProduct is off by default — set it to enable writing the matched UA substring.",code:[{lang:"json",code:JSON.stringify({transformers:{bot:{package:"@walkeros/server-transformer-bot",config:{settings:{output:{botScore:"ingest.bot.score",agentScore:"ingest.bot.agent",agentProduct:"user.agentProduct"}}}}}},null,2)}]},"destination-filtering":{text:'Recommended destination-mapping recipes. Drop all bots: botScore > 50. Drop crawlers but keep user-action AI traffic: botScore > 50 AND agentProduct NOT LIKE "%-User". AI traffic report: group by agentProduct WHERE agentScore > 50. The transformer never drops events — filtering is always a destination decision.'},"detection-scope":{text:"v1 is UA-only: wraps isbot (curl, wget, headless Chrome defaults, well-known crawlers) plus a curated AI-agent UA map (OpenAI, Anthropic, Perplexity, Mistral, Meta, Google, Apple, Amazon, DuckDuckGo, ByteDance, Common Crawl). It will NOT catch: residential-proxy + stealth Chrome + realistic behavior; reverse-DNS-verified search engines; client-side runtime tells. v1.1 adds header consistency heuristics (Sec-Fetch, Sec-CH-UA, Accept-Language) with proper GREASE handling. For commercial-grade detection use Cloudflare Bot Management, DataDome, or HUMAN."}};//# sourceMappingURL=dev.js.map
|
|
1
|
+
"use strict";var e,t=Object.defineProperty,a=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,n=Object.prototype.hasOwnProperty,r=(e,a)=>{for(var o in a)t(e,o,{get:a[o],enumerable:!0})},s={};r(s,{examples:()=>f,hints:()=>D,schemas:()=>i}),module.exports=(e=s,((e,r,s,i)=>{if(r&&"object"==typeof r||"function"==typeof r)for(let c of o(r))n.call(e,c)||c===s||t(e,c,{get:()=>r[c],enumerable:!(i=a(r,c))||i.enumerable});return e})(t({},"__esModule",{value:!0}),e));var i={};r(i,{SettingsSchema:()=>b,settings:()=>m});var c=require("@walkeros/core/dev"),l=require("@walkeros/core/dev"),d=l.z.union([l.z.string().describe('Dot-notation path like "ingest.userAgent"'),l.z.object({key:l.z.string().optional(),value:l.z.unknown().optional(),fn:l.z.string().optional()}).describe("Mapping value object")]),h=l.z.union([d,l.z.array(d).describe("Array of fallback values, tried in order")]),p=l.z.string().regex(/\./,{message:'A string context is either a BotContext literal or a dot-path lookup like "ingest.transport"; to pin a literal inside a fallback chain use {"value": "beacon"}'}),u=l.z.object({key:l.z.string().optional(),value:l.z.unknown().optional(),fn:l.z.string().optional()}),g=e=>l.z.union([l.z.string(),l.z.literal(!1)]).optional().describe(e),b=l.z.object({input:l.z.object({userAgent:h.optional(),ip:h.optional(),acceptLanguage:h.optional(),acceptEncoding:h.optional(),secFetchSite:h.optional(),secFetchMode:h.optional(),secFetchDest:h.optional(),secFetchUser:h.optional(),secChUa:h.optional(),secChUaMobile:h.optional(),secChUaPlatform:h.optional(),accept:h.optional(),contentType:h.optional(),referer:h.optional(),signatureAgent:h.optional(),method:h.optional(),ja4:h.optional(),headerNames:h.optional()}).optional().describe('Input signal sources, resolved via getMappingValue against { event, ingest }. Each defaults to "ingest.<name>". Listing a name here also declares that the signal is wired in your pipeline, which is what enables the absence-based checks for its family (client hints, Fetch Metadata, Accept-Language/Encoding). "ja4" and "headerNames" are reserved and unconsumed.'),output:l.z.object({botScore:g('Path for the automation likelihood (0-99, higher = more automated, null when not measured). Default: "user.botScore". Use "ingest.*" to route to pipeline scratch instead of the event, or false to disable.'),botCategory:g('Path for the client category: human, suspicious, automation, search-crawler, seo-tool, monitor, link-preview, ai-agent, ai-crawler, unknown. Default: "user.botCategory".'),botProduct:g('Path for the identified product (e.g. "ChatGPT-User", "Googlebot"), written only when a named detector matched. Default: "user.botProduct".'),botReasons:g('Path for the reason-code array. Default: "ingest.bot.reasons", so the codes stay available to the pipeline without weighting the analytics payload. Codes ending in _not_declared report which signal families are unwired.')}).optional().describe("Output paths for the bot annotations."),context:l.z.union([l.z.enum(["auto","navigation","pixel","beacon","fetch","server"]),p,u,l.z.array(l.z.union([p,u]))]).optional().describe('How the request reaches the collector. An enum literal ("beacon") pins one context for every request. Any other form is a Mapping.Value resolved per request against { event, ingest }: a dot-path string ("ingest.transport"), a {key}/{value}/{fn} object, or a fallback array tried in order ([{key: "ingest.transport"}, {value: "beacon"}]). Wire transport truth in via the source config, e.g. express ingest map transport: {key: "query.transport"} with a ?transport= param on the collect URL. A result that is not a valid context falls back to "auto" (context-independent checks only, reported as "context_undetermined"): scored less, never scored wrong.'),suspiciousAt:l.z.number().optional().describe('Graded-layer cut between category "human" and "suspicious". Default: 25. Does not affect the deterministic scores (70 and above).')}).describe("Bot detection transformer: annotates events with an automation likelihood, a client category, the identified product and reason codes."),m=(0,c.zodToSchema)(b),f={};r(f,{step:()=>y});var y={};r(y,{chatgptUserAgent:()=>A,curlClient:()=>z,gptBotCrawler:()=>k,headerMismatch:()=>S,humanChrome:()=>w,missingUA:()=>q,pixelWildcardAccept:()=>C,searchCrawler:()=>x,unannotatedBeaconFallback:()=>U,unwiredPipeline:()=>P});var v={name:"page view",data:{title:"Home",id:"/"},id:"ev-1700000600",trigger:"load",entity:"page",action:"view",timestamp:1700000600,source:{type:"express",platform:"server"}},w={title:"Human visitor (Chrome)",description:"Modern Chrome UA. No bot signals.",in:{...v},out:[["return",{event:{...v,user:{botScore:0,botCategory:"human"}}}]]},k={title:"GPTBot training crawler",description:"OpenAI training crawler. The category says what it is, the product says which one.",in:{...v,id:"ev-1700000601"},out:[["return",{event:{...v,id:"ev-1700000601",user:{botScore:90,botCategory:"ai-crawler",botProduct:"GPTBot"}}}]]},A={title:"ChatGPT-User (AI agent)",description:"A person routed an AI to fetch this page. Same score as a crawler because it is still software; the category is what lets a destination keep this traffic.",in:{...v,id:"ev-1700000602"},out:[["return",{event:{...v,id:"ev-1700000602",user:{botScore:90,botCategory:"ai-agent",botProduct:"ChatGPT-User"}}}]]},x={title:"Googlebot (search crawler)",description:"Correlates with organic discoverability, so it is worth separating from both AI crawlers and unnamed automation.",in:{...v,id:"ev-1700000605"},out:[["return",{event:{...v,id:"ev-1700000605",user:{botScore:90,botCategory:"search-crawler",botProduct:"Googlebot"}}}]]},C={title:"Wildcard Accept on an annotated pixel",description:"The pixel embed URL carries ?transport=pixel and the source lifts it into ingest, so this request is scored against the pixel profile. Browsers send a typed image Accept when they load an image, so the wildcard is worth 25.",in:{...v,id:"ev-1700000608"},out:[["return",{event:{...v,id:"ev-1700000608",user:{botScore:25,botCategory:"suspicious"}}}]]},U={title:"Unannotated request falls back to beacon",description:'Same instance, same headers, no ?transport= on the request, so the trailing { value: "beacon" } pins beacon. A wildcard Accept is exactly what navigator.sendBeacon sends, and the identical request now scores 0. This is what pinning per request buys: one header, two correct readings.',in:{...v,id:"ev-1700000609"},out:[["return",{event:{...v,id:"ev-1700000609",user:{botScore:0,botCategory:"human"}}}]]},S={title:"Client hints contradict the UA",description:'The UA claims Chrome 124, Sec-CH-UA says Chromium 98. Graded evidence, not proof: frozen WebView UAs and enterprise UA-reduction policies produce the same mismatch, so the outcome is "suspicious" and never "automation".',in:{...v,id:"ev-1700000606"},out:[["return",{event:{...v,id:"ev-1700000606",user:{botScore:30,botCategory:"suspicious"}}}]]},z={public:!1,description:"curl. isbot recognises it as automated without naming a product, so it sits a rung below the UA maps.",in:{...v,id:"ev-1700000603"},out:[["return",{event:{...v,id:"ev-1700000603",user:{botScore:80,botCategory:"automation"}}}]]},q={public:!1,description:"No User-Agent, but the pipeline is delivering other signals. UA stripping is overwhelmingly bots or hardened privacy tools.",in:{...v,id:"ev-1700000604"},out:[["return",{event:{...v,id:"ev-1700000604",user:{botScore:70,botCategory:"automation"}}}]]},P={public:!1,description:'No signal resolved, so the score is null rather than 0. Writing null overwrites any client-supplied value and still evaluates false under a "botScore > 50" filter. ingest.bot.reasons names the missing mappings.',in:{...v,id:"ev-1700000607"},out:[["return",{event:{...v,id:"ev-1700000607",user:{botScore:null,botCategory:"unknown"}}}]]},j=[{key:"ingest.transport"},{value:"beacon"}],D={"ingest-prerequisite":{text:'The bot transformer reads its signals from ctx.ingest (path "ingest.<name>" by default). The upstream server source must populate them via config.ingest, which MUST use the map operator with direct request field paths (no req. prefix); a bare object like { userAgent: "req.headers.user-agent" } is silently inert and leaves ingest empty. With nothing in ingest the score is null and the category is "unknown". The map is also where the request context enters: lift the sender annotation with transport: { key: "query.transport" } and one instance then serves every transport, see the transport-wiring hint.',code:[{lang:"json",code:JSON.stringify({sources:{express:{package:"@walkeros/server-source-express",config:{ingest:{map:{userAgent:{key:"headers.user-agent"},acceptLanguage:{key:"headers.accept-language"},acceptEncoding:{key:"headers.accept-encoding"},secFetchSite:{key:"headers.sec-fetch-site"},secFetchMode:{key:"headers.sec-fetch-mode"},secFetchDest:{key:"headers.sec-fetch-dest"},secFetchUser:{key:"headers.sec-fetch-user"},secChUa:{key:"headers.sec-ch-ua"},secChUaMobile:{key:"headers.sec-ch-ua-mobile"},secChUaPlatform:{key:"headers.sec-ch-ua-platform"},accept:{key:"headers.accept"},contentType:{key:"headers.content-type"},referer:{key:"headers.referer"},signatureAgent:{key:"headers.signature-agent"},method:{key:"method"},transport:{key:"query.transport"}}}}}},transformers:{bot:{package:"@walkeros/server-transformer-bot",config:{settings:{context:j}}}}},null,2)}]},"transport-wiring":{text:'settings.context accepts a Mapping.Value, so one instance covers a deployment that receives beacons, pixels and fetches at the same endpoint. The sender declares the transport, the source lifts it, the transformer resolves it per request: add ?transport=beacon to the collect URL and ?transport=pixel to the pixel embed URL, then map transport: { key: "query.transport" } in the source (the normalized express scope exposes the parsed query, so no extra plumbing is needed). Give the bot config a fallback array so unannotated senders still land on a sane profile: [{ "key": "ingest.transport" }, { "value": "beacon" }]. Mind the shape: a bare string is read as a lookup path and must contain a dot, so a literal inside the array is written { "value": "beacon" } and never "beacon". Anything that resolves outside the vocabulary falls back to "auto", which scores less rather than scoring wrong. A query parameter is a claim by whoever controls the sender, which is fine for separating your own transports and worthless against a client that wants to be scored as a beacon; when you need server truth instead, give pixels their own route and derive the context from the request path.',code:[{lang:"html",code:'<img src="https://collect.example.com/px.gif?transport=pixel" width="1" height="1" alt="">'},{lang:"json",code:JSON.stringify({sources:{express:{package:"@walkeros/server-source-express",config:{ingest:{map:{userAgent:{key:"headers.user-agent"},transport:{key:"query.transport"}}}}}},transformers:{bot:{package:"@walkeros/server-transformer-bot",config:{settings:{context:j}}}}},null,2)}]},"declared-signals":{text:'Absence is ambiguous: "the client sent no Sec-CH-UA" and "the operator never mapped Sec-CH-UA" both arrive as undefined. So an absence-based check runs only when its input name appears explicitly in settings.input, which is you asserting the signal is wired. Reading still falls back to the defaults, so listing a name with its default path costs nothing and unlocks the check. Until then ingest.bot.reasons reports ch_not_declared, fetchmeta_not_declared or accept_not_declared, naming exactly which mapping to add.',code:[{lang:"json",code:JSON.stringify({transformers:{bot:{package:"@walkeros/server-transformer-bot",config:{settings:{context:"beacon",input:{acceptLanguage:"ingest.acceptLanguage",acceptEncoding:"ingest.acceptEncoding",secFetchSite:"ingest.secFetchSite",secFetchMode:"ingest.secFetchMode",secFetchDest:"ingest.secFetchDest",secChUa:"ingest.secChUa"}}}}}},null,2)}]},"output-routing":{text:'Outputs default to event.user.botScore, event.user.botCategory, event.user.botProduct and ingest.bot.reasons. Any value can be a dot path to rename and reroute the field, or false to disable it. An "ingest." prefix keeps a field off the analytics event while still routing on it downstream. Note that ingest is shared by every event in a scope and the write is an assignment, so if a before-chain step fans one input into several events, point botReasons at an event path to keep per-event attribution.',code:[{lang:"json",code:JSON.stringify({transformers:{bot:{package:"@walkeros/server-transformer-bot",config:{settings:{output:{botScore:"ingest.bot.score",botCategory:"ingest.bot.category",botProduct:!1,botReasons:"ingest.bot.reasons"}}}}}},null,2)}]},"destination-filtering":{text:'Recommended destination-mapping recipes. Drop everything automated: botScore > 50. Drop crawlers but keep AI traffic a person triggered: botScore > 50 AND botCategory != "ai-agent". Keep link unfurls, which mean somebody just shared the URL: botCategory != "link-preview". AI visibility report: group by botProduct WHERE botCategory IN ("ai-agent", "ai-crawler"). Note that botScore is null for category "unknown", which means not measured rather than human, and evaluates false under a > 50 filter. The transformer never drops events; filtering is always a destination decision.'},"detection-scope":{text:"Detection runs in two layers. Deterministic, first match wins: missing UA (70), AI agent or crawler UA map (90, with the category and product), non-AI crawler map for search engines, SEO tools, monitors and link unfurlers (90), isbot (80), and values impossible for the pinned context (75). Graded, capped at 60, when no deterministic rung fires: client-hint version mismatch, client hints missing on a Chromium UA, wildcard Accept on a typed context, Fetch Metadata missing or off-profile, and missing Accept-Language or Accept-Encoding. It will NOT catch: residential-proxy plus stealth Chrome with realistic behavior; in-browser agents such as Claude for Chrome, which drive a real session and are header-identical to it; UA-spoofing privacy tools. For commercial-grade detection use Cloudflare Bot Management, DataDome, or HUMAN."}};//# sourceMappingURL=dev.js.map
|
package/dist/dev.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/dev.ts","../src/schemas/index.ts","../src/schemas/settings.ts","../src/examples/index.ts","../src/examples/step.ts","../src/hints.ts"],"sourcesContent":["export * as schemas from './schemas';\nexport * as examples from './examples';\nexport { hints } from './hints';\n","import { zodToSchema } from '@walkeros/core/dev';\nimport { SettingsSchema } from './settings';\n\nexport { SettingsSchema, type Settings } from './settings';\nexport const settings = zodToSchema(SettingsSchema);\n","import { z } from '@walkeros/core/dev';\n\nconst SingleMappingValueSchema = z.union([\n z.string().describe('Dot-notation path like \"ingest.userAgent\"'),\n z\n .object({\n key: z.string().optional(),\n value: z.unknown().optional(),\n fn: z.string().optional(),\n })\n .describe('Mapping value object'),\n]);\n\nconst MappingValueSchema = z.union([\n SingleMappingValueSchema,\n z\n .array(SingleMappingValueSchema)\n .describe('Array of fallback values, tried in order'),\n]);\n\nexport const SettingsSchema = z\n .object({\n input: z\n .object({\n userAgent: MappingValueSchema.optional(),\n ip: MappingValueSchema.optional(),\n acceptLanguage: MappingValueSchema.optional(),\n acceptEncoding: MappingValueSchema.optional(),\n secFetchSite: MappingValueSchema.optional(),\n secFetchMode: MappingValueSchema.optional(),\n secFetchDest: MappingValueSchema.optional(),\n secFetchUser: MappingValueSchema.optional(),\n secChUa: MappingValueSchema.optional(),\n secChUaMobile: MappingValueSchema.optional(),\n secChUaPlatform: MappingValueSchema.optional(),\n })\n .optional()\n .describe(\n 'Input signal sources, resolved via getMappingValue against { event, ingest }. v1 only reads userAgent; other fields reserved for v1.1 header heuristics.',\n ),\n output: z\n .object({\n botScore: z\n .string()\n .optional()\n .describe(\n 'Path for bot score (0-99, higher = more bot). Default: \"user.botScore\". Use \"ingest.*\" to route to pipeline scratch instead of the event. Empty string or omit = skip.',\n ),\n agentScore: z\n .string()\n .optional()\n .describe(\n 'Path for AI agent score (0-99). v1 emits 0 (no match) or 95 (UA-map match). Default: \"user.agentScore\".',\n ),\n agentProduct: z\n .string()\n .optional()\n .describe(\n 'Path for matched UA substring (e.g. \"ChatGPT-User\"). Off by default — set to enable.',\n ),\n })\n .optional()\n .describe('Output paths for bot/agent annotations.'),\n })\n .describe(\n 'Bot detection transformer: annotates events with bot and AI-agent scores.',\n );\n\nexport type Settings = z.infer<typeof SettingsSchema>;\n","export * as step from './step';\n","import type { Flow } from '@walkeros/core';\n\nconst baseEvent = {\n name: 'page view',\n data: { title: 'Home', id: '/' },\n id: 'ev-1700000600',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000600,\n source: { type: 'express', platform: 'server' as const },\n};\n\n/** Real Chrome — botScore 0, agentScore 0. */\nexport const humanChrome: Flow.StepExample = {\n title: 'Human visitor (Chrome)',\n description: 'Modern Chrome UA. No bot or agent signals.',\n in: { ...baseEvent },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n user: { botScore: 0, agentScore: 0 },\n },\n },\n ],\n ],\n};\n\n/** GPTBot — training crawler. */\nexport const gptBotCrawler: Flow.StepExample = {\n title: 'GPTBot training crawler',\n description:\n 'OpenAI training crawler. Both botScore and agentScore are high.',\n in: { ...baseEvent, id: 'ev-1700000601' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000601',\n user: { botScore: 95, agentScore: 95 },\n },\n },\n ],\n ],\n};\n\n/** ChatGPT-User — user-action AI agent. */\nexport const chatgptUserAgent: Flow.StepExample = {\n title: 'ChatGPT-User (user-action AI)',\n description:\n 'A real human routed an AI to fetch this page. botScore high but lower than crawlers — agentProduct lets destinations keep this traffic.',\n in: { ...baseEvent, id: 'ev-1700000602' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000602',\n user: { botScore: 90, agentScore: 95 },\n },\n },\n ],\n ],\n};\n\n/** curl — caught by isbot. */\nexport const curlClient: Flow.StepExample = {\n public: false,\n description: 'curl client — caught by isbot. agentScore zero.',\n in: { ...baseEvent, id: 'ev-1700000603' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000603',\n user: { botScore: 80, agentScore: 0 },\n },\n },\n ],\n ],\n};\n\n/** Empty / missing UA — score 70 (suspicious; real browsers rarely strip UA). */\nexport const missingUA: Flow.StepExample = {\n public: false,\n description:\n 'No User-Agent — baseline 70 (UA stripping is overwhelmingly bots or hardened privacy tools).',\n in: { ...baseEvent, id: 'ev-1700000604' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000604',\n user: { botScore: 70, agentScore: 0 },\n },\n },\n ],\n ],\n};\n","import type { Hint } from '@walkeros/core';\n\nexport const hints: Hint.Hints = {\n 'ingest-prerequisite': {\n text: 'The bot transformer reads userAgent from ctx.ingest (path \"ingest.userAgent\" by default). The upstream server source must populate it via config.ingest, which MUST use the map operator with direct request field paths (no req. prefix); a bare object like { userAgent: \"req.headers.user-agent\" } is silently inert and leaves ingest empty. Without populated ingest the UA is empty and every event scores 70 (baseline for missing UA).',\n code: [\n {\n lang: 'json',\n code: JSON.stringify(\n {\n sources: {\n express: {\n package: '@walkeros/server-source-express',\n config: {\n ingest: {\n map: {\n userAgent: { key: 'headers.user-agent' },\n },\n },\n },\n },\n },\n transformers: {\n bot: {\n package: '@walkeros/server-transformer-bot',\n },\n },\n },\n null,\n 2,\n ),\n },\n ],\n },\n 'output-routing': {\n text: 'Outputs default to event.user.botScore and event.user.agentScore. Redirect to ingest.* to keep the analytics event clean while still routing on the score downstream. Empty string (or omit) skips writing that field entirely. agentProduct is off by default — set it to enable writing the matched UA substring.',\n code: [\n {\n lang: 'json',\n code: JSON.stringify(\n {\n transformers: {\n bot: {\n package: '@walkeros/server-transformer-bot',\n config: {\n settings: {\n output: {\n botScore: 'ingest.bot.score',\n agentScore: 'ingest.bot.agent',\n agentProduct: 'user.agentProduct',\n },\n },\n },\n },\n },\n },\n null,\n 2,\n ),\n },\n ],\n },\n 'destination-filtering': {\n text: 'Recommended destination-mapping recipes. Drop all bots: botScore > 50. Drop crawlers but keep user-action AI traffic: botScore > 50 AND agentProduct NOT LIKE \"%-User\". AI traffic report: group by agentProduct WHERE agentScore > 50. The transformer never drops events — filtering is always a destination decision.',\n },\n 'detection-scope': {\n text: 'v1 is UA-only: wraps isbot (curl, wget, headless Chrome defaults, well-known crawlers) plus a curated AI-agent UA map (OpenAI, Anthropic, Perplexity, Mistral, Meta, Google, Apple, Amazon, DuckDuckGo, ByteDance, Common Crawl). It will NOT catch: residential-proxy + stealth Chrome + realistic behavior; reverse-DNS-verified search engines; client-side runtime tells. v1.1 adds header consistency heuristics (Sec-Fetch, Sec-CH-UA, Accept-Language) with proper GREASE handling. For commercial-grade detection use Cloudflare Bot Management, DataDome, or HUMAN.',\n },\n};\n"],"mappings":"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","names":["import_dev"]}
|
|
1
|
+
{"version":3,"sources":["../src/dev.ts","../src/schemas/index.ts","../src/schemas/settings.ts","../src/examples/index.ts","../src/examples/step.ts","../src/hints.ts"],"sourcesContent":["export * as schemas from './schemas';\nexport * as examples from './examples';\nexport { hints } from './hints';\n","import { zodToSchema } from '@walkeros/core/dev';\nimport { SettingsSchema } from './settings';\n\nexport { SettingsSchema, type Settings } from './settings';\nexport const settings = zodToSchema(SettingsSchema);\n","import { z } from '@walkeros/core/dev';\n\nconst SingleMappingValueSchema = z.union([\n z.string().describe('Dot-notation path like \"ingest.userAgent\"'),\n z\n .object({\n key: z.string().optional(),\n value: z.unknown().optional(),\n fn: z.string().optional(),\n })\n .describe('Mapping value object'),\n]);\n\nconst MappingValueSchema = z.union([\n SingleMappingValueSchema,\n z\n .array(SingleMappingValueSchema)\n .describe('Array of fallback values, tried in order'),\n]);\n\n// A string context is either a BotContext literal or a lookup path. The dot is\n// what tells them apart, so a typo fails validation instead of silently\n// becoming a path lookup that resolves to nothing. It has to be `.regex`, not\n// `.refine`: only the former survives into the generated JSON Schema, which is\n// what `walkeros validate` and the MCP catalog actually check against.\nconst DottedPathSchema = z.string().regex(/\\./, {\n message:\n 'A string context is either a BotContext literal or a dot-path lookup like \"ingest.transport\"; to pin a literal inside a fallback chain use {\"value\": \"beacon\"}',\n});\n\nconst ContextObjectSchema = z.object({\n key: z.string().optional(),\n value: z.unknown().optional(),\n fn: z.string().optional(),\n});\n\nconst OutputPathSchema = (description: string) =>\n z\n .union([z.string(), z.literal(false)])\n .optional()\n .describe(description);\n\nexport const SettingsSchema = z\n .object({\n input: z\n .object({\n userAgent: MappingValueSchema.optional(),\n ip: MappingValueSchema.optional(),\n acceptLanguage: MappingValueSchema.optional(),\n acceptEncoding: MappingValueSchema.optional(),\n secFetchSite: MappingValueSchema.optional(),\n secFetchMode: MappingValueSchema.optional(),\n secFetchDest: MappingValueSchema.optional(),\n secFetchUser: MappingValueSchema.optional(),\n secChUa: MappingValueSchema.optional(),\n secChUaMobile: MappingValueSchema.optional(),\n secChUaPlatform: MappingValueSchema.optional(),\n accept: MappingValueSchema.optional(),\n contentType: MappingValueSchema.optional(),\n referer: MappingValueSchema.optional(),\n signatureAgent: MappingValueSchema.optional(),\n method: MappingValueSchema.optional(),\n ja4: MappingValueSchema.optional(),\n headerNames: MappingValueSchema.optional(),\n })\n .optional()\n .describe(\n 'Input signal sources, resolved via getMappingValue against { event, ingest }. Each defaults to \"ingest.<name>\". Listing a name here also declares that the signal is wired in your pipeline, which is what enables the absence-based checks for its family (client hints, Fetch Metadata, Accept-Language/Encoding). \"ja4\" and \"headerNames\" are reserved and unconsumed.',\n ),\n output: z\n .object({\n botScore: OutputPathSchema(\n 'Path for the automation likelihood (0-99, higher = more automated, null when not measured). Default: \"user.botScore\". Use \"ingest.*\" to route to pipeline scratch instead of the event, or false to disable.',\n ),\n botCategory: OutputPathSchema(\n 'Path for the client category: human, suspicious, automation, search-crawler, seo-tool, monitor, link-preview, ai-agent, ai-crawler, unknown. Default: \"user.botCategory\".',\n ),\n botProduct: OutputPathSchema(\n 'Path for the identified product (e.g. \"ChatGPT-User\", \"Googlebot\"), written only when a named detector matched. Default: \"user.botProduct\".',\n ),\n botReasons: OutputPathSchema(\n 'Path for the reason-code array. Default: \"ingest.bot.reasons\", so the codes stay available to the pipeline without weighting the analytics payload. Codes ending in _not_declared report which signal families are unwired.',\n ),\n })\n .optional()\n .describe('Output paths for the bot annotations.'),\n context: z\n .union([\n z.enum(['auto', 'navigation', 'pixel', 'beacon', 'fetch', 'server']),\n DottedPathSchema,\n ContextObjectSchema,\n z.array(z.union([DottedPathSchema, ContextObjectSchema])),\n ])\n .optional()\n .describe(\n 'How the request reaches the collector. An enum literal (\"beacon\") pins one context for every request. Any other form is a Mapping.Value resolved per request against { event, ingest }: a dot-path string (\"ingest.transport\"), a {key}/{value}/{fn} object, or a fallback array tried in order ([{key: \"ingest.transport\"}, {value: \"beacon\"}]). Wire transport truth in via the source config, e.g. express ingest map transport: {key: \"query.transport\"} with a ?transport= param on the collect URL. A result that is not a valid context falls back to \"auto\" (context-independent checks only, reported as \"context_undetermined\"): scored less, never scored wrong.',\n ),\n suspiciousAt: z\n .number()\n .optional()\n .describe(\n 'Graded-layer cut between category \"human\" and \"suspicious\". Default: 25. Does not affect the deterministic scores (70 and above).',\n ),\n })\n .describe(\n 'Bot detection transformer: annotates events with an automation likelihood, a client category, the identified product and reason codes.',\n );\n\nexport type Settings = z.infer<typeof SettingsSchema>;\n","export * as step from './step';\n","import type { Flow } from '@walkeros/core';\n\nconst baseEvent = {\n name: 'page view',\n data: { title: 'Home', id: '/' },\n id: 'ev-1700000600',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000600,\n source: { type: 'express', platform: 'server' as const },\n};\n\n/** Real Chrome, nothing to report. */\nexport const humanChrome: Flow.StepExample = {\n title: 'Human visitor (Chrome)',\n description: 'Modern Chrome UA. No bot signals.',\n in: { ...baseEvent },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n user: { botScore: 0, botCategory: 'human' },\n },\n },\n ],\n ],\n};\n\n/** GPTBot, an AI training crawler. */\nexport const gptBotCrawler: Flow.StepExample = {\n title: 'GPTBot training crawler',\n description:\n 'OpenAI training crawler. The category says what it is, the product says which one.',\n in: { ...baseEvent, id: 'ev-1700000601' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000601',\n user: {\n botScore: 90,\n botCategory: 'ai-crawler',\n botProduct: 'GPTBot',\n },\n },\n },\n ],\n ],\n};\n\n/** ChatGPT-User, an AI agent acting for a person. */\nexport const chatgptUserAgent: Flow.StepExample = {\n title: 'ChatGPT-User (AI agent)',\n description:\n 'A person routed an AI to fetch this page. Same score as a crawler because it is still software; the category is what lets a destination keep this traffic.',\n in: { ...baseEvent, id: 'ev-1700000602' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000602',\n user: {\n botScore: 90,\n botCategory: 'ai-agent',\n botProduct: 'ChatGPT-User',\n },\n },\n },\n ],\n ],\n};\n\n/** Googlebot, a search crawler: the one named bot that renders JavaScript. */\nexport const searchCrawler: Flow.StepExample = {\n title: 'Googlebot (search crawler)',\n description:\n 'Correlates with organic discoverability, so it is worth separating from both AI crawlers and unnamed automation.',\n in: { ...baseEvent, id: 'ev-1700000605' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000605',\n user: {\n botScore: 90,\n botCategory: 'search-crawler',\n botProduct: 'Googlebot',\n },\n },\n },\n ],\n ],\n};\n\n/**\n * The same request, twice, through one instance configured\n * `context: [{ key: \"ingest.transport\" }, { value: \"beacon\" }]`.\n */\nexport const pixelWildcardAccept: Flow.StepExample = {\n title: 'Wildcard Accept on an annotated pixel',\n description:\n 'The pixel embed URL carries ?transport=pixel and the source lifts it into ingest, so this request is scored against the pixel profile. Browsers send a typed image Accept when they load an image, so the wildcard is worth 25.',\n in: { ...baseEvent, id: 'ev-1700000608' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000608',\n user: { botScore: 25, botCategory: 'suspicious' },\n },\n },\n ],\n ],\n};\n\n/** Byte-identical headers, no annotation: the fallback entry decides. */\nexport const unannotatedBeaconFallback: Flow.StepExample = {\n title: 'Unannotated request falls back to beacon',\n description:\n 'Same instance, same headers, no ?transport= on the request, so the trailing { value: \"beacon\" } pins beacon. A wildcard Accept is exactly what navigator.sendBeacon sends, and the identical request now scores 0. This is what pinning per request buys: one header, two correct readings.',\n in: { ...baseEvent, id: 'ev-1700000609' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000609',\n user: { botScore: 0, botCategory: 'human' },\n },\n },\n ],\n ],\n};\n\n/** A Chromium UA whose client hints disagree with it. */\nexport const headerMismatch: Flow.StepExample = {\n title: 'Client hints contradict the UA',\n description:\n 'The UA claims Chrome 124, Sec-CH-UA says Chromium 98. Graded evidence, not proof: frozen WebView UAs and enterprise UA-reduction policies produce the same mismatch, so the outcome is \"suspicious\" and never \"automation\".',\n in: { ...baseEvent, id: 'ev-1700000606' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000606',\n user: { botScore: 30, botCategory: 'suspicious' },\n },\n },\n ],\n ],\n};\n\n/** curl, caught by isbot but not named. */\nexport const curlClient: Flow.StepExample = {\n public: false,\n description:\n 'curl. isbot recognises it as automated without naming a product, so it sits a rung below the UA maps.',\n in: { ...baseEvent, id: 'ev-1700000603' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000603',\n user: { botScore: 80, botCategory: 'automation' },\n },\n },\n ],\n ],\n};\n\n/** No User-Agent on an otherwise wired request. */\nexport const missingUA: Flow.StepExample = {\n public: false,\n description:\n 'No User-Agent, but the pipeline is delivering other signals. UA stripping is overwhelmingly bots or hardened privacy tools.',\n in: { ...baseEvent, id: 'ev-1700000604' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000604',\n user: { botScore: 70, botCategory: 'automation' },\n },\n },\n ],\n ],\n};\n\n/** Nothing in ingest at all: not measured, which is not the same as human. */\nexport const unwiredPipeline: Flow.StepExample = {\n public: false,\n description:\n 'No signal resolved, so the score is null rather than 0. Writing null overwrites any client-supplied value and still evaluates false under a \"botScore > 50\" filter. ingest.bot.reasons names the missing mappings.',\n in: { ...baseEvent, id: 'ev-1700000607' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000607',\n user: { botScore: null, botCategory: 'unknown' },\n },\n },\n ],\n ],\n};\n","import type { Hint } from '@walkeros/core';\n\nconst INGEST_MAP = {\n userAgent: { key: 'headers.user-agent' },\n acceptLanguage: { key: 'headers.accept-language' },\n acceptEncoding: { key: 'headers.accept-encoding' },\n secFetchSite: { key: 'headers.sec-fetch-site' },\n secFetchMode: { key: 'headers.sec-fetch-mode' },\n secFetchDest: { key: 'headers.sec-fetch-dest' },\n secFetchUser: { key: 'headers.sec-fetch-user' },\n secChUa: { key: 'headers.sec-ch-ua' },\n secChUaMobile: { key: 'headers.sec-ch-ua-mobile' },\n secChUaPlatform: { key: 'headers.sec-ch-ua-platform' },\n accept: { key: 'headers.accept' },\n contentType: { key: 'headers.content-type' },\n referer: { key: 'headers.referer' },\n signatureAgent: { key: 'headers.signature-agent' },\n method: { key: 'method' },\n transport: { key: 'query.transport' },\n};\n\nconst CONTEXT_FROM_TRANSPORT = [\n { key: 'ingest.transport' },\n { value: 'beacon' },\n];\n\nexport const hints: Hint.Hints = {\n 'ingest-prerequisite': {\n text: 'The bot transformer reads its signals from ctx.ingest (path \"ingest.<name>\" by default). The upstream server source must populate them via config.ingest, which MUST use the map operator with direct request field paths (no req. prefix); a bare object like { userAgent: \"req.headers.user-agent\" } is silently inert and leaves ingest empty. With nothing in ingest the score is null and the category is \"unknown\". The map is also where the request context enters: lift the sender annotation with transport: { key: \"query.transport\" } and one instance then serves every transport, see the transport-wiring hint.',\n code: [\n {\n lang: 'json',\n code: JSON.stringify(\n {\n sources: {\n express: {\n package: '@walkeros/server-source-express',\n config: { ingest: { map: INGEST_MAP } },\n },\n },\n transformers: {\n bot: {\n package: '@walkeros/server-transformer-bot',\n config: { settings: { context: CONTEXT_FROM_TRANSPORT } },\n },\n },\n },\n null,\n 2,\n ),\n },\n ],\n },\n 'transport-wiring': {\n text: 'settings.context accepts a Mapping.Value, so one instance covers a deployment that receives beacons, pixels and fetches at the same endpoint. The sender declares the transport, the source lifts it, the transformer resolves it per request: add ?transport=beacon to the collect URL and ?transport=pixel to the pixel embed URL, then map transport: { key: \"query.transport\" } in the source (the normalized express scope exposes the parsed query, so no extra plumbing is needed). Give the bot config a fallback array so unannotated senders still land on a sane profile: [{ \"key\": \"ingest.transport\" }, { \"value\": \"beacon\" }]. Mind the shape: a bare string is read as a lookup path and must contain a dot, so a literal inside the array is written { \"value\": \"beacon\" } and never \"beacon\". Anything that resolves outside the vocabulary falls back to \"auto\", which scores less rather than scoring wrong. A query parameter is a claim by whoever controls the sender, which is fine for separating your own transports and worthless against a client that wants to be scored as a beacon; when you need server truth instead, give pixels their own route and derive the context from the request path.',\n code: [\n {\n lang: 'html',\n code: '<img src=\"https://collect.example.com/px.gif?transport=pixel\" width=\"1\" height=\"1\" alt=\"\">',\n },\n {\n lang: 'json',\n code: JSON.stringify(\n {\n sources: {\n express: {\n package: '@walkeros/server-source-express',\n config: {\n ingest: {\n map: {\n userAgent: { key: 'headers.user-agent' },\n transport: { key: 'query.transport' },\n },\n },\n },\n },\n },\n transformers: {\n bot: {\n package: '@walkeros/server-transformer-bot',\n config: { settings: { context: CONTEXT_FROM_TRANSPORT } },\n },\n },\n },\n null,\n 2,\n ),\n },\n ],\n },\n 'declared-signals': {\n text: 'Absence is ambiguous: \"the client sent no Sec-CH-UA\" and \"the operator never mapped Sec-CH-UA\" both arrive as undefined. So an absence-based check runs only when its input name appears explicitly in settings.input, which is you asserting the signal is wired. Reading still falls back to the defaults, so listing a name with its default path costs nothing and unlocks the check. Until then ingest.bot.reasons reports ch_not_declared, fetchmeta_not_declared or accept_not_declared, naming exactly which mapping to add.',\n code: [\n {\n lang: 'json',\n code: JSON.stringify(\n {\n transformers: {\n bot: {\n package: '@walkeros/server-transformer-bot',\n config: {\n settings: {\n context: 'beacon',\n input: {\n acceptLanguage: 'ingest.acceptLanguage',\n acceptEncoding: 'ingest.acceptEncoding',\n secFetchSite: 'ingest.secFetchSite',\n secFetchMode: 'ingest.secFetchMode',\n secFetchDest: 'ingest.secFetchDest',\n secChUa: 'ingest.secChUa',\n },\n },\n },\n },\n },\n },\n null,\n 2,\n ),\n },\n ],\n },\n 'output-routing': {\n text: 'Outputs default to event.user.botScore, event.user.botCategory, event.user.botProduct and ingest.bot.reasons. Any value can be a dot path to rename and reroute the field, or false to disable it. An \"ingest.\" prefix keeps a field off the analytics event while still routing on it downstream. Note that ingest is shared by every event in a scope and the write is an assignment, so if a before-chain step fans one input into several events, point botReasons at an event path to keep per-event attribution.',\n code: [\n {\n lang: 'json',\n code: JSON.stringify(\n {\n transformers: {\n bot: {\n package: '@walkeros/server-transformer-bot',\n config: {\n settings: {\n output: {\n botScore: 'ingest.bot.score',\n botCategory: 'ingest.bot.category',\n botProduct: false,\n botReasons: 'ingest.bot.reasons',\n },\n },\n },\n },\n },\n },\n null,\n 2,\n ),\n },\n ],\n },\n 'destination-filtering': {\n text: 'Recommended destination-mapping recipes. Drop everything automated: botScore > 50. Drop crawlers but keep AI traffic a person triggered: botScore > 50 AND botCategory != \"ai-agent\". Keep link unfurls, which mean somebody just shared the URL: botCategory != \"link-preview\". AI visibility report: group by botProduct WHERE botCategory IN (\"ai-agent\", \"ai-crawler\"). Note that botScore is null for category \"unknown\", which means not measured rather than human, and evaluates false under a > 50 filter. The transformer never drops events; filtering is always a destination decision.',\n },\n 'detection-scope': {\n text: 'Detection runs in two layers. Deterministic, first match wins: missing UA (70), AI agent or crawler UA map (90, with the category and product), non-AI crawler map for search engines, SEO tools, monitors and link unfurlers (90), isbot (80), and values impossible for the pinned context (75). Graded, capped at 60, when no deterministic rung fires: client-hint version mismatch, client hints missing on a Chromium UA, wildcard Accept on a typed context, Fetch Metadata missing or off-profile, and missing Accept-Language or Accept-Encoding. It will NOT catch: residential-proxy plus stealth Chrome with realistic behavior; in-browser agents such as Claude for Chrome, which drive a real session and are header-identical to it; UA-spoofing privacy tools. For commercial-grade detection use Cloudflare Bot Management, DataDome, or HUMAN.',\n },\n};\n"],"mappings":"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","names":["import_dev"]}
|
package/dist/dev.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var e=Object.defineProperty,t=(t,r)=>{for(var o in r)e(t,o,{get:r[o],enumerable:!0})},r={};t(r,{SettingsSchema:()=>s,settings:()=>c});import{zodToSchema as o}from"@walkeros/core/dev";import{z as n}from"@walkeros/core/dev";var i=n.union([n.string().describe('Dot-notation path like "ingest.userAgent"'),n.object({key:n.string().optional(),value:n.unknown().optional(),fn:n.string().optional()}).describe("Mapping value object")]),a=n.union([i,n.array(i).describe("Array of fallback values, tried in order")]),s=n.object({input:n.object({userAgent:a.optional(),ip:a.optional(),acceptLanguage:a.optional(),acceptEncoding:a.optional(),secFetchSite:a.optional(),secFetchMode:a.optional(),secFetchDest:a.optional(),secFetchUser:a.optional(),secChUa:a.optional(),secChUaMobile:a.optional(),secChUaPlatform:a.optional()}).optional().describe("Input signal sources, resolved via getMappingValue against { event, ingest }. v1 only reads userAgent; other fields reserved for v1.1 header heuristics."),output:n.object({botScore:n.string().optional().describe('Path for bot score (0-99, higher = more bot). Default: "user.botScore". Use "ingest.*" to route to pipeline scratch instead of the event. Empty string or omit = skip.'),agentScore:n.string().optional().describe('Path for AI agent score (0-99). v1 emits 0 (no match) or 95 (UA-map match). Default: "user.agentScore".'),agentProduct:n.string().optional().describe('Path for matched UA substring (e.g. "ChatGPT-User"). Off by default — set to enable.')}).optional().describe("Output paths for bot/agent annotations.")}).describe("Bot detection transformer: annotates events with bot and AI-agent scores."),c=o(s),l={};t(l,{step:()=>g});var g={};t(g,{chatgptUserAgent:()=>h,curlClient:()=>b,gptBotCrawler:()=>d,humanChrome:()=>u,missingUA:()=>m});var p={name:"page view",data:{title:"Home",id:"/"},id:"ev-1700000600",trigger:"load",entity:"page",action:"view",timestamp:1700000600,source:{type:"express",platform:"server"}},u={title:"Human visitor (Chrome)",description:"Modern Chrome UA. No bot or agent signals.",in:{...p},out:[["return",{event:{...p,user:{botScore:0,agentScore:0}}}]]},d={title:"GPTBot training crawler",description:"OpenAI training crawler. Both botScore and agentScore are high.",in:{...p,id:"ev-1700000601"},out:[["return",{event:{...p,id:"ev-1700000601",user:{botScore:95,agentScore:95}}}]]},h={title:"ChatGPT-User (user-action AI)",description:"A real human routed an AI to fetch this page. botScore high but lower than crawlers — agentProduct lets destinations keep this traffic.",in:{...p,id:"ev-1700000602"},out:[["return",{event:{...p,id:"ev-1700000602",user:{botScore:90,agentScore:95}}}]]},b={public:!1,description:"curl client — caught by isbot. agentScore zero.",in:{...p,id:"ev-1700000603"},out:[["return",{event:{...p,id:"ev-1700000603",user:{botScore:80,agentScore:0}}}]]},m={public:!1,description:"No User-Agent — baseline 70 (UA stripping is overwhelmingly bots or hardened privacy tools).",in:{...p,id:"ev-1700000604"},out:[["return",{event:{...p,id:"ev-1700000604",user:{botScore:70,agentScore:0}}}]]},v={"ingest-prerequisite":{text:'The bot transformer reads userAgent from ctx.ingest (path "ingest.userAgent" by default). The upstream server source must populate it via config.ingest, which MUST use the map operator with direct request field paths (no req. prefix); a bare object like { userAgent: "req.headers.user-agent" } is silently inert and leaves ingest empty. Without populated ingest the UA is empty and every event scores 70 (baseline for missing UA).',code:[{lang:"json",code:JSON.stringify({sources:{express:{package:"@walkeros/server-source-express",config:{ingest:{map:{userAgent:{key:"headers.user-agent"}}}}}},transformers:{bot:{package:"@walkeros/server-transformer-bot"}}},null,2)}]},"output-routing":{text:"Outputs default to event.user.botScore and event.user.agentScore. Redirect to ingest.* to keep the analytics event clean while still routing on the score downstream. Empty string (or omit) skips writing that field entirely. agentProduct is off by default — set it to enable writing the matched UA substring.",code:[{lang:"json",code:JSON.stringify({transformers:{bot:{package:"@walkeros/server-transformer-bot",config:{settings:{output:{botScore:"ingest.bot.score",agentScore:"ingest.bot.agent",agentProduct:"user.agentProduct"}}}}}},null,2)}]},"destination-filtering":{text:'Recommended destination-mapping recipes. Drop all bots: botScore > 50. Drop crawlers but keep user-action AI traffic: botScore > 50 AND agentProduct NOT LIKE "%-User". AI traffic report: group by agentProduct WHERE agentScore > 50. The transformer never drops events — filtering is always a destination decision.'},"detection-scope":{text:"v1 is UA-only: wraps isbot (curl, wget, headless Chrome defaults, well-known crawlers) plus a curated AI-agent UA map (OpenAI, Anthropic, Perplexity, Mistral, Meta, Google, Apple, Amazon, DuckDuckGo, ByteDance, Common Crawl). It will NOT catch: residential-proxy + stealth Chrome + realistic behavior; reverse-DNS-verified search engines; client-side runtime tells. v1.1 adds header consistency heuristics (Sec-Fetch, Sec-CH-UA, Accept-Language) with proper GREASE handling. For commercial-grade detection use Cloudflare Bot Management, DataDome, or HUMAN."}};export{l as examples,v as hints,r as schemas};//# sourceMappingURL=dev.mjs.map
|
|
1
|
+
var e=Object.defineProperty,t=(t,a)=>{for(var o in a)e(t,o,{get:a[o],enumerable:!0})},a={};t(a,{SettingsSchema:()=>d,settings:()=>h});import{zodToSchema as o}from"@walkeros/core/dev";import{z as n}from"@walkeros/core/dev";var r=n.union([n.string().describe('Dot-notation path like "ingest.userAgent"'),n.object({key:n.string().optional(),value:n.unknown().optional(),fn:n.string().optional()}).describe("Mapping value object")]),s=n.union([r,n.array(r).describe("Array of fallback values, tried in order")]),i=n.string().regex(/\./,{message:'A string context is either a BotContext literal or a dot-path lookup like "ingest.transport"; to pin a literal inside a fallback chain use {"value": "beacon"}'}),c=n.object({key:n.string().optional(),value:n.unknown().optional(),fn:n.string().optional()}),l=e=>n.union([n.string(),n.literal(!1)]).optional().describe(e),d=n.object({input:n.object({userAgent:s.optional(),ip:s.optional(),acceptLanguage:s.optional(),acceptEncoding:s.optional(),secFetchSite:s.optional(),secFetchMode:s.optional(),secFetchDest:s.optional(),secFetchUser:s.optional(),secChUa:s.optional(),secChUaMobile:s.optional(),secChUaPlatform:s.optional(),accept:s.optional(),contentType:s.optional(),referer:s.optional(),signatureAgent:s.optional(),method:s.optional(),ja4:s.optional(),headerNames:s.optional()}).optional().describe('Input signal sources, resolved via getMappingValue against { event, ingest }. Each defaults to "ingest.<name>". Listing a name here also declares that the signal is wired in your pipeline, which is what enables the absence-based checks for its family (client hints, Fetch Metadata, Accept-Language/Encoding). "ja4" and "headerNames" are reserved and unconsumed.'),output:n.object({botScore:l('Path for the automation likelihood (0-99, higher = more automated, null when not measured). Default: "user.botScore". Use "ingest.*" to route to pipeline scratch instead of the event, or false to disable.'),botCategory:l('Path for the client category: human, suspicious, automation, search-crawler, seo-tool, monitor, link-preview, ai-agent, ai-crawler, unknown. Default: "user.botCategory".'),botProduct:l('Path for the identified product (e.g. "ChatGPT-User", "Googlebot"), written only when a named detector matched. Default: "user.botProduct".'),botReasons:l('Path for the reason-code array. Default: "ingest.bot.reasons", so the codes stay available to the pipeline without weighting the analytics payload. Codes ending in _not_declared report which signal families are unwired.')}).optional().describe("Output paths for the bot annotations."),context:n.union([n.enum(["auto","navigation","pixel","beacon","fetch","server"]),i,c,n.array(n.union([i,c]))]).optional().describe('How the request reaches the collector. An enum literal ("beacon") pins one context for every request. Any other form is a Mapping.Value resolved per request against { event, ingest }: a dot-path string ("ingest.transport"), a {key}/{value}/{fn} object, or a fallback array tried in order ([{key: "ingest.transport"}, {value: "beacon"}]). Wire transport truth in via the source config, e.g. express ingest map transport: {key: "query.transport"} with a ?transport= param on the collect URL. A result that is not a valid context falls back to "auto" (context-independent checks only, reported as "context_undetermined"): scored less, never scored wrong.'),suspiciousAt:n.number().optional().describe('Graded-layer cut between category "human" and "suspicious". Default: 25. Does not affect the deterministic scores (70 and above).')}).describe("Bot detection transformer: annotates events with an automation likelihood, a client category, the identified product and reason codes."),h=o(d),p={};t(p,{step:()=>u});var u={};t(u,{chatgptUserAgent:()=>f,curlClient:()=>A,gptBotCrawler:()=>m,headerMismatch:()=>k,humanChrome:()=>b,missingUA:()=>x,pixelWildcardAccept:()=>y,searchCrawler:()=>v,unannotatedBeaconFallback:()=>w,unwiredPipeline:()=>C});var g={name:"page view",data:{title:"Home",id:"/"},id:"ev-1700000600",trigger:"load",entity:"page",action:"view",timestamp:1700000600,source:{type:"express",platform:"server"}},b={title:"Human visitor (Chrome)",description:"Modern Chrome UA. No bot signals.",in:{...g},out:[["return",{event:{...g,user:{botScore:0,botCategory:"human"}}}]]},m={title:"GPTBot training crawler",description:"OpenAI training crawler. The category says what it is, the product says which one.",in:{...g,id:"ev-1700000601"},out:[["return",{event:{...g,id:"ev-1700000601",user:{botScore:90,botCategory:"ai-crawler",botProduct:"GPTBot"}}}]]},f={title:"ChatGPT-User (AI agent)",description:"A person routed an AI to fetch this page. Same score as a crawler because it is still software; the category is what lets a destination keep this traffic.",in:{...g,id:"ev-1700000602"},out:[["return",{event:{...g,id:"ev-1700000602",user:{botScore:90,botCategory:"ai-agent",botProduct:"ChatGPT-User"}}}]]},v={title:"Googlebot (search crawler)",description:"Correlates with organic discoverability, so it is worth separating from both AI crawlers and unnamed automation.",in:{...g,id:"ev-1700000605"},out:[["return",{event:{...g,id:"ev-1700000605",user:{botScore:90,botCategory:"search-crawler",botProduct:"Googlebot"}}}]]},y={title:"Wildcard Accept on an annotated pixel",description:"The pixel embed URL carries ?transport=pixel and the source lifts it into ingest, so this request is scored against the pixel profile. Browsers send a typed image Accept when they load an image, so the wildcard is worth 25.",in:{...g,id:"ev-1700000608"},out:[["return",{event:{...g,id:"ev-1700000608",user:{botScore:25,botCategory:"suspicious"}}}]]},w={title:"Unannotated request falls back to beacon",description:'Same instance, same headers, no ?transport= on the request, so the trailing { value: "beacon" } pins beacon. A wildcard Accept is exactly what navigator.sendBeacon sends, and the identical request now scores 0. This is what pinning per request buys: one header, two correct readings.',in:{...g,id:"ev-1700000609"},out:[["return",{event:{...g,id:"ev-1700000609",user:{botScore:0,botCategory:"human"}}}]]},k={title:"Client hints contradict the UA",description:'The UA claims Chrome 124, Sec-CH-UA says Chromium 98. Graded evidence, not proof: frozen WebView UAs and enterprise UA-reduction policies produce the same mismatch, so the outcome is "suspicious" and never "automation".',in:{...g,id:"ev-1700000606"},out:[["return",{event:{...g,id:"ev-1700000606",user:{botScore:30,botCategory:"suspicious"}}}]]},A={public:!1,description:"curl. isbot recognises it as automated without naming a product, so it sits a rung below the UA maps.",in:{...g,id:"ev-1700000603"},out:[["return",{event:{...g,id:"ev-1700000603",user:{botScore:80,botCategory:"automation"}}}]]},x={public:!1,description:"No User-Agent, but the pipeline is delivering other signals. UA stripping is overwhelmingly bots or hardened privacy tools.",in:{...g,id:"ev-1700000604"},out:[["return",{event:{...g,id:"ev-1700000604",user:{botScore:70,botCategory:"automation"}}}]]},C={public:!1,description:'No signal resolved, so the score is null rather than 0. Writing null overwrites any client-supplied value and still evaluates false under a "botScore > 50" filter. ingest.bot.reasons names the missing mappings.',in:{...g,id:"ev-1700000607"},out:[["return",{event:{...g,id:"ev-1700000607",user:{botScore:null,botCategory:"unknown"}}}]]},U=[{key:"ingest.transport"},{value:"beacon"}],S={"ingest-prerequisite":{text:'The bot transformer reads its signals from ctx.ingest (path "ingest.<name>" by default). The upstream server source must populate them via config.ingest, which MUST use the map operator with direct request field paths (no req. prefix); a bare object like { userAgent: "req.headers.user-agent" } is silently inert and leaves ingest empty. With nothing in ingest the score is null and the category is "unknown". The map is also where the request context enters: lift the sender annotation with transport: { key: "query.transport" } and one instance then serves every transport, see the transport-wiring hint.',code:[{lang:"json",code:JSON.stringify({sources:{express:{package:"@walkeros/server-source-express",config:{ingest:{map:{userAgent:{key:"headers.user-agent"},acceptLanguage:{key:"headers.accept-language"},acceptEncoding:{key:"headers.accept-encoding"},secFetchSite:{key:"headers.sec-fetch-site"},secFetchMode:{key:"headers.sec-fetch-mode"},secFetchDest:{key:"headers.sec-fetch-dest"},secFetchUser:{key:"headers.sec-fetch-user"},secChUa:{key:"headers.sec-ch-ua"},secChUaMobile:{key:"headers.sec-ch-ua-mobile"},secChUaPlatform:{key:"headers.sec-ch-ua-platform"},accept:{key:"headers.accept"},contentType:{key:"headers.content-type"},referer:{key:"headers.referer"},signatureAgent:{key:"headers.signature-agent"},method:{key:"method"},transport:{key:"query.transport"}}}}}},transformers:{bot:{package:"@walkeros/server-transformer-bot",config:{settings:{context:U}}}}},null,2)}]},"transport-wiring":{text:'settings.context accepts a Mapping.Value, so one instance covers a deployment that receives beacons, pixels and fetches at the same endpoint. The sender declares the transport, the source lifts it, the transformer resolves it per request: add ?transport=beacon to the collect URL and ?transport=pixel to the pixel embed URL, then map transport: { key: "query.transport" } in the source (the normalized express scope exposes the parsed query, so no extra plumbing is needed). Give the bot config a fallback array so unannotated senders still land on a sane profile: [{ "key": "ingest.transport" }, { "value": "beacon" }]. Mind the shape: a bare string is read as a lookup path and must contain a dot, so a literal inside the array is written { "value": "beacon" } and never "beacon". Anything that resolves outside the vocabulary falls back to "auto", which scores less rather than scoring wrong. A query parameter is a claim by whoever controls the sender, which is fine for separating your own transports and worthless against a client that wants to be scored as a beacon; when you need server truth instead, give pixels their own route and derive the context from the request path.',code:[{lang:"html",code:'<img src="https://collect.example.com/px.gif?transport=pixel" width="1" height="1" alt="">'},{lang:"json",code:JSON.stringify({sources:{express:{package:"@walkeros/server-source-express",config:{ingest:{map:{userAgent:{key:"headers.user-agent"},transport:{key:"query.transport"}}}}}},transformers:{bot:{package:"@walkeros/server-transformer-bot",config:{settings:{context:U}}}}},null,2)}]},"declared-signals":{text:'Absence is ambiguous: "the client sent no Sec-CH-UA" and "the operator never mapped Sec-CH-UA" both arrive as undefined. So an absence-based check runs only when its input name appears explicitly in settings.input, which is you asserting the signal is wired. Reading still falls back to the defaults, so listing a name with its default path costs nothing and unlocks the check. Until then ingest.bot.reasons reports ch_not_declared, fetchmeta_not_declared or accept_not_declared, naming exactly which mapping to add.',code:[{lang:"json",code:JSON.stringify({transformers:{bot:{package:"@walkeros/server-transformer-bot",config:{settings:{context:"beacon",input:{acceptLanguage:"ingest.acceptLanguage",acceptEncoding:"ingest.acceptEncoding",secFetchSite:"ingest.secFetchSite",secFetchMode:"ingest.secFetchMode",secFetchDest:"ingest.secFetchDest",secChUa:"ingest.secChUa"}}}}}},null,2)}]},"output-routing":{text:'Outputs default to event.user.botScore, event.user.botCategory, event.user.botProduct and ingest.bot.reasons. Any value can be a dot path to rename and reroute the field, or false to disable it. An "ingest." prefix keeps a field off the analytics event while still routing on it downstream. Note that ingest is shared by every event in a scope and the write is an assignment, so if a before-chain step fans one input into several events, point botReasons at an event path to keep per-event attribution.',code:[{lang:"json",code:JSON.stringify({transformers:{bot:{package:"@walkeros/server-transformer-bot",config:{settings:{output:{botScore:"ingest.bot.score",botCategory:"ingest.bot.category",botProduct:!1,botReasons:"ingest.bot.reasons"}}}}}},null,2)}]},"destination-filtering":{text:'Recommended destination-mapping recipes. Drop everything automated: botScore > 50. Drop crawlers but keep AI traffic a person triggered: botScore > 50 AND botCategory != "ai-agent". Keep link unfurls, which mean somebody just shared the URL: botCategory != "link-preview". AI visibility report: group by botProduct WHERE botCategory IN ("ai-agent", "ai-crawler"). Note that botScore is null for category "unknown", which means not measured rather than human, and evaluates false under a > 50 filter. The transformer never drops events; filtering is always a destination decision.'},"detection-scope":{text:"Detection runs in two layers. Deterministic, first match wins: missing UA (70), AI agent or crawler UA map (90, with the category and product), non-AI crawler map for search engines, SEO tools, monitors and link unfurlers (90), isbot (80), and values impossible for the pinned context (75). Graded, capped at 60, when no deterministic rung fires: client-hint version mismatch, client hints missing on a Chromium UA, wildcard Accept on a typed context, Fetch Metadata missing or off-profile, and missing Accept-Language or Accept-Encoding. It will NOT catch: residential-proxy plus stealth Chrome with realistic behavior; in-browser agents such as Claude for Chrome, which drive a real session and are header-identical to it; UA-spoofing privacy tools. For commercial-grade detection use Cloudflare Bot Management, DataDome, or HUMAN."}};export{p as examples,S as hints,a as schemas};//# sourceMappingURL=dev.mjs.map
|
package/dist/dev.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/schemas/index.ts","../src/schemas/settings.ts","../src/examples/index.ts","../src/examples/step.ts","../src/hints.ts"],"sourcesContent":["import { zodToSchema } from '@walkeros/core/dev';\nimport { SettingsSchema } from './settings';\n\nexport { SettingsSchema, type Settings } from './settings';\nexport const settings = zodToSchema(SettingsSchema);\n","import { z } from '@walkeros/core/dev';\n\nconst SingleMappingValueSchema = z.union([\n z.string().describe('Dot-notation path like \"ingest.userAgent\"'),\n z\n .object({\n key: z.string().optional(),\n value: z.unknown().optional(),\n fn: z.string().optional(),\n })\n .describe('Mapping value object'),\n]);\n\nconst MappingValueSchema = z.union([\n SingleMappingValueSchema,\n z\n .array(SingleMappingValueSchema)\n .describe('Array of fallback values, tried in order'),\n]);\n\nexport const SettingsSchema = z\n .object({\n input: z\n .object({\n userAgent: MappingValueSchema.optional(),\n ip: MappingValueSchema.optional(),\n acceptLanguage: MappingValueSchema.optional(),\n acceptEncoding: MappingValueSchema.optional(),\n secFetchSite: MappingValueSchema.optional(),\n secFetchMode: MappingValueSchema.optional(),\n secFetchDest: MappingValueSchema.optional(),\n secFetchUser: MappingValueSchema.optional(),\n secChUa: MappingValueSchema.optional(),\n secChUaMobile: MappingValueSchema.optional(),\n secChUaPlatform: MappingValueSchema.optional(),\n })\n .optional()\n .describe(\n 'Input signal sources, resolved via getMappingValue against { event, ingest }. v1 only reads userAgent; other fields reserved for v1.1 header heuristics.',\n ),\n output: z\n .object({\n botScore: z\n .string()\n .optional()\n .describe(\n 'Path for bot score (0-99, higher = more bot). Default: \"user.botScore\". Use \"ingest.*\" to route to pipeline scratch instead of the event. Empty string or omit = skip.',\n ),\n agentScore: z\n .string()\n .optional()\n .describe(\n 'Path for AI agent score (0-99). v1 emits 0 (no match) or 95 (UA-map match). Default: \"user.agentScore\".',\n ),\n agentProduct: z\n .string()\n .optional()\n .describe(\n 'Path for matched UA substring (e.g. \"ChatGPT-User\"). Off by default — set to enable.',\n ),\n })\n .optional()\n .describe('Output paths for bot/agent annotations.'),\n })\n .describe(\n 'Bot detection transformer: annotates events with bot and AI-agent scores.',\n );\n\nexport type Settings = z.infer<typeof SettingsSchema>;\n","export * as step from './step';\n","import type { Flow } from '@walkeros/core';\n\nconst baseEvent = {\n name: 'page view',\n data: { title: 'Home', id: '/' },\n id: 'ev-1700000600',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000600,\n source: { type: 'express', platform: 'server' as const },\n};\n\n/** Real Chrome — botScore 0, agentScore 0. */\nexport const humanChrome: Flow.StepExample = {\n title: 'Human visitor (Chrome)',\n description: 'Modern Chrome UA. No bot or agent signals.',\n in: { ...baseEvent },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n user: { botScore: 0, agentScore: 0 },\n },\n },\n ],\n ],\n};\n\n/** GPTBot — training crawler. */\nexport const gptBotCrawler: Flow.StepExample = {\n title: 'GPTBot training crawler',\n description:\n 'OpenAI training crawler. Both botScore and agentScore are high.',\n in: { ...baseEvent, id: 'ev-1700000601' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000601',\n user: { botScore: 95, agentScore: 95 },\n },\n },\n ],\n ],\n};\n\n/** ChatGPT-User — user-action AI agent. */\nexport const chatgptUserAgent: Flow.StepExample = {\n title: 'ChatGPT-User (user-action AI)',\n description:\n 'A real human routed an AI to fetch this page. botScore high but lower than crawlers — agentProduct lets destinations keep this traffic.',\n in: { ...baseEvent, id: 'ev-1700000602' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000602',\n user: { botScore: 90, agentScore: 95 },\n },\n },\n ],\n ],\n};\n\n/** curl — caught by isbot. */\nexport const curlClient: Flow.StepExample = {\n public: false,\n description: 'curl client — caught by isbot. agentScore zero.',\n in: { ...baseEvent, id: 'ev-1700000603' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000603',\n user: { botScore: 80, agentScore: 0 },\n },\n },\n ],\n ],\n};\n\n/** Empty / missing UA — score 70 (suspicious; real browsers rarely strip UA). */\nexport const missingUA: Flow.StepExample = {\n public: false,\n description:\n 'No User-Agent — baseline 70 (UA stripping is overwhelmingly bots or hardened privacy tools).',\n in: { ...baseEvent, id: 'ev-1700000604' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000604',\n user: { botScore: 70, agentScore: 0 },\n },\n },\n ],\n ],\n};\n","import type { Hint } from '@walkeros/core';\n\nexport const hints: Hint.Hints = {\n 'ingest-prerequisite': {\n text: 'The bot transformer reads userAgent from ctx.ingest (path \"ingest.userAgent\" by default). The upstream server source must populate it via config.ingest, which MUST use the map operator with direct request field paths (no req. prefix); a bare object like { userAgent: \"req.headers.user-agent\" } is silently inert and leaves ingest empty. Without populated ingest the UA is empty and every event scores 70 (baseline for missing UA).',\n code: [\n {\n lang: 'json',\n code: JSON.stringify(\n {\n sources: {\n express: {\n package: '@walkeros/server-source-express',\n config: {\n ingest: {\n map: {\n userAgent: { key: 'headers.user-agent' },\n },\n },\n },\n },\n },\n transformers: {\n bot: {\n package: '@walkeros/server-transformer-bot',\n },\n },\n },\n null,\n 2,\n ),\n },\n ],\n },\n 'output-routing': {\n text: 'Outputs default to event.user.botScore and event.user.agentScore. Redirect to ingest.* to keep the analytics event clean while still routing on the score downstream. Empty string (or omit) skips writing that field entirely. agentProduct is off by default — set it to enable writing the matched UA substring.',\n code: [\n {\n lang: 'json',\n code: JSON.stringify(\n {\n transformers: {\n bot: {\n package: '@walkeros/server-transformer-bot',\n config: {\n settings: {\n output: {\n botScore: 'ingest.bot.score',\n agentScore: 'ingest.bot.agent',\n agentProduct: 'user.agentProduct',\n },\n },\n },\n },\n },\n },\n null,\n 2,\n ),\n },\n ],\n },\n 'destination-filtering': {\n text: 'Recommended destination-mapping recipes. Drop all bots: botScore > 50. Drop crawlers but keep user-action AI traffic: botScore > 50 AND agentProduct NOT LIKE \"%-User\". AI traffic report: group by agentProduct WHERE agentScore > 50. The transformer never drops events — filtering is always a destination decision.',\n },\n 'detection-scope': {\n text: 'v1 is UA-only: wraps isbot (curl, wget, headless Chrome defaults, well-known crawlers) plus a curated AI-agent UA map (OpenAI, Anthropic, Perplexity, Mistral, Meta, Google, Apple, Amazon, DuckDuckGo, ByteDance, Common Crawl). It will NOT catch: residential-proxy + stealth Chrome + realistic behavior; reverse-DNS-verified search engines; client-side runtime tells. v1.1 adds header consistency heuristics (Sec-Fetch, Sec-CH-UA, Accept-Language) with proper GREASE handling. For commercial-grade detection use Cloudflare Bot Management, DataDome, or HUMAN.',\n },\n};\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/schemas/index.ts","../src/schemas/settings.ts","../src/examples/index.ts","../src/examples/step.ts","../src/hints.ts"],"sourcesContent":["import { zodToSchema } from '@walkeros/core/dev';\nimport { SettingsSchema } from './settings';\n\nexport { SettingsSchema, type Settings } from './settings';\nexport const settings = zodToSchema(SettingsSchema);\n","import { z } from '@walkeros/core/dev';\n\nconst SingleMappingValueSchema = z.union([\n z.string().describe('Dot-notation path like \"ingest.userAgent\"'),\n z\n .object({\n key: z.string().optional(),\n value: z.unknown().optional(),\n fn: z.string().optional(),\n })\n .describe('Mapping value object'),\n]);\n\nconst MappingValueSchema = z.union([\n SingleMappingValueSchema,\n z\n .array(SingleMappingValueSchema)\n .describe('Array of fallback values, tried in order'),\n]);\n\n// A string context is either a BotContext literal or a lookup path. The dot is\n// what tells them apart, so a typo fails validation instead of silently\n// becoming a path lookup that resolves to nothing. It has to be `.regex`, not\n// `.refine`: only the former survives into the generated JSON Schema, which is\n// what `walkeros validate` and the MCP catalog actually check against.\nconst DottedPathSchema = z.string().regex(/\\./, {\n message:\n 'A string context is either a BotContext literal or a dot-path lookup like \"ingest.transport\"; to pin a literal inside a fallback chain use {\"value\": \"beacon\"}',\n});\n\nconst ContextObjectSchema = z.object({\n key: z.string().optional(),\n value: z.unknown().optional(),\n fn: z.string().optional(),\n});\n\nconst OutputPathSchema = (description: string) =>\n z\n .union([z.string(), z.literal(false)])\n .optional()\n .describe(description);\n\nexport const SettingsSchema = z\n .object({\n input: z\n .object({\n userAgent: MappingValueSchema.optional(),\n ip: MappingValueSchema.optional(),\n acceptLanguage: MappingValueSchema.optional(),\n acceptEncoding: MappingValueSchema.optional(),\n secFetchSite: MappingValueSchema.optional(),\n secFetchMode: MappingValueSchema.optional(),\n secFetchDest: MappingValueSchema.optional(),\n secFetchUser: MappingValueSchema.optional(),\n secChUa: MappingValueSchema.optional(),\n secChUaMobile: MappingValueSchema.optional(),\n secChUaPlatform: MappingValueSchema.optional(),\n accept: MappingValueSchema.optional(),\n contentType: MappingValueSchema.optional(),\n referer: MappingValueSchema.optional(),\n signatureAgent: MappingValueSchema.optional(),\n method: MappingValueSchema.optional(),\n ja4: MappingValueSchema.optional(),\n headerNames: MappingValueSchema.optional(),\n })\n .optional()\n .describe(\n 'Input signal sources, resolved via getMappingValue against { event, ingest }. Each defaults to \"ingest.<name>\". Listing a name here also declares that the signal is wired in your pipeline, which is what enables the absence-based checks for its family (client hints, Fetch Metadata, Accept-Language/Encoding). \"ja4\" and \"headerNames\" are reserved and unconsumed.',\n ),\n output: z\n .object({\n botScore: OutputPathSchema(\n 'Path for the automation likelihood (0-99, higher = more automated, null when not measured). Default: \"user.botScore\". Use \"ingest.*\" to route to pipeline scratch instead of the event, or false to disable.',\n ),\n botCategory: OutputPathSchema(\n 'Path for the client category: human, suspicious, automation, search-crawler, seo-tool, monitor, link-preview, ai-agent, ai-crawler, unknown. Default: \"user.botCategory\".',\n ),\n botProduct: OutputPathSchema(\n 'Path for the identified product (e.g. \"ChatGPT-User\", \"Googlebot\"), written only when a named detector matched. Default: \"user.botProduct\".',\n ),\n botReasons: OutputPathSchema(\n 'Path for the reason-code array. Default: \"ingest.bot.reasons\", so the codes stay available to the pipeline without weighting the analytics payload. Codes ending in _not_declared report which signal families are unwired.',\n ),\n })\n .optional()\n .describe('Output paths for the bot annotations.'),\n context: z\n .union([\n z.enum(['auto', 'navigation', 'pixel', 'beacon', 'fetch', 'server']),\n DottedPathSchema,\n ContextObjectSchema,\n z.array(z.union([DottedPathSchema, ContextObjectSchema])),\n ])\n .optional()\n .describe(\n 'How the request reaches the collector. An enum literal (\"beacon\") pins one context for every request. Any other form is a Mapping.Value resolved per request against { event, ingest }: a dot-path string (\"ingest.transport\"), a {key}/{value}/{fn} object, or a fallback array tried in order ([{key: \"ingest.transport\"}, {value: \"beacon\"}]). Wire transport truth in via the source config, e.g. express ingest map transport: {key: \"query.transport\"} with a ?transport= param on the collect URL. A result that is not a valid context falls back to \"auto\" (context-independent checks only, reported as \"context_undetermined\"): scored less, never scored wrong.',\n ),\n suspiciousAt: z\n .number()\n .optional()\n .describe(\n 'Graded-layer cut between category \"human\" and \"suspicious\". Default: 25. Does not affect the deterministic scores (70 and above).',\n ),\n })\n .describe(\n 'Bot detection transformer: annotates events with an automation likelihood, a client category, the identified product and reason codes.',\n );\n\nexport type Settings = z.infer<typeof SettingsSchema>;\n","export * as step from './step';\n","import type { Flow } from '@walkeros/core';\n\nconst baseEvent = {\n name: 'page view',\n data: { title: 'Home', id: '/' },\n id: 'ev-1700000600',\n trigger: 'load',\n entity: 'page',\n action: 'view',\n timestamp: 1700000600,\n source: { type: 'express', platform: 'server' as const },\n};\n\n/** Real Chrome, nothing to report. */\nexport const humanChrome: Flow.StepExample = {\n title: 'Human visitor (Chrome)',\n description: 'Modern Chrome UA. No bot signals.',\n in: { ...baseEvent },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n user: { botScore: 0, botCategory: 'human' },\n },\n },\n ],\n ],\n};\n\n/** GPTBot, an AI training crawler. */\nexport const gptBotCrawler: Flow.StepExample = {\n title: 'GPTBot training crawler',\n description:\n 'OpenAI training crawler. The category says what it is, the product says which one.',\n in: { ...baseEvent, id: 'ev-1700000601' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000601',\n user: {\n botScore: 90,\n botCategory: 'ai-crawler',\n botProduct: 'GPTBot',\n },\n },\n },\n ],\n ],\n};\n\n/** ChatGPT-User, an AI agent acting for a person. */\nexport const chatgptUserAgent: Flow.StepExample = {\n title: 'ChatGPT-User (AI agent)',\n description:\n 'A person routed an AI to fetch this page. Same score as a crawler because it is still software; the category is what lets a destination keep this traffic.',\n in: { ...baseEvent, id: 'ev-1700000602' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000602',\n user: {\n botScore: 90,\n botCategory: 'ai-agent',\n botProduct: 'ChatGPT-User',\n },\n },\n },\n ],\n ],\n};\n\n/** Googlebot, a search crawler: the one named bot that renders JavaScript. */\nexport const searchCrawler: Flow.StepExample = {\n title: 'Googlebot (search crawler)',\n description:\n 'Correlates with organic discoverability, so it is worth separating from both AI crawlers and unnamed automation.',\n in: { ...baseEvent, id: 'ev-1700000605' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000605',\n user: {\n botScore: 90,\n botCategory: 'search-crawler',\n botProduct: 'Googlebot',\n },\n },\n },\n ],\n ],\n};\n\n/**\n * The same request, twice, through one instance configured\n * `context: [{ key: \"ingest.transport\" }, { value: \"beacon\" }]`.\n */\nexport const pixelWildcardAccept: Flow.StepExample = {\n title: 'Wildcard Accept on an annotated pixel',\n description:\n 'The pixel embed URL carries ?transport=pixel and the source lifts it into ingest, so this request is scored against the pixel profile. Browsers send a typed image Accept when they load an image, so the wildcard is worth 25.',\n in: { ...baseEvent, id: 'ev-1700000608' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000608',\n user: { botScore: 25, botCategory: 'suspicious' },\n },\n },\n ],\n ],\n};\n\n/** Byte-identical headers, no annotation: the fallback entry decides. */\nexport const unannotatedBeaconFallback: Flow.StepExample = {\n title: 'Unannotated request falls back to beacon',\n description:\n 'Same instance, same headers, no ?transport= on the request, so the trailing { value: \"beacon\" } pins beacon. A wildcard Accept is exactly what navigator.sendBeacon sends, and the identical request now scores 0. This is what pinning per request buys: one header, two correct readings.',\n in: { ...baseEvent, id: 'ev-1700000609' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000609',\n user: { botScore: 0, botCategory: 'human' },\n },\n },\n ],\n ],\n};\n\n/** A Chromium UA whose client hints disagree with it. */\nexport const headerMismatch: Flow.StepExample = {\n title: 'Client hints contradict the UA',\n description:\n 'The UA claims Chrome 124, Sec-CH-UA says Chromium 98. Graded evidence, not proof: frozen WebView UAs and enterprise UA-reduction policies produce the same mismatch, so the outcome is \"suspicious\" and never \"automation\".',\n in: { ...baseEvent, id: 'ev-1700000606' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000606',\n user: { botScore: 30, botCategory: 'suspicious' },\n },\n },\n ],\n ],\n};\n\n/** curl, caught by isbot but not named. */\nexport const curlClient: Flow.StepExample = {\n public: false,\n description:\n 'curl. isbot recognises it as automated without naming a product, so it sits a rung below the UA maps.',\n in: { ...baseEvent, id: 'ev-1700000603' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000603',\n user: { botScore: 80, botCategory: 'automation' },\n },\n },\n ],\n ],\n};\n\n/** No User-Agent on an otherwise wired request. */\nexport const missingUA: Flow.StepExample = {\n public: false,\n description:\n 'No User-Agent, but the pipeline is delivering other signals. UA stripping is overwhelmingly bots or hardened privacy tools.',\n in: { ...baseEvent, id: 'ev-1700000604' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000604',\n user: { botScore: 70, botCategory: 'automation' },\n },\n },\n ],\n ],\n};\n\n/** Nothing in ingest at all: not measured, which is not the same as human. */\nexport const unwiredPipeline: Flow.StepExample = {\n public: false,\n description:\n 'No signal resolved, so the score is null rather than 0. Writing null overwrites any client-supplied value and still evaluates false under a \"botScore > 50\" filter. ingest.bot.reasons names the missing mappings.',\n in: { ...baseEvent, id: 'ev-1700000607' },\n out: [\n [\n 'return',\n {\n event: {\n ...baseEvent,\n id: 'ev-1700000607',\n user: { botScore: null, botCategory: 'unknown' },\n },\n },\n ],\n ],\n};\n","import type { Hint } from '@walkeros/core';\n\nconst INGEST_MAP = {\n userAgent: { key: 'headers.user-agent' },\n acceptLanguage: { key: 'headers.accept-language' },\n acceptEncoding: { key: 'headers.accept-encoding' },\n secFetchSite: { key: 'headers.sec-fetch-site' },\n secFetchMode: { key: 'headers.sec-fetch-mode' },\n secFetchDest: { key: 'headers.sec-fetch-dest' },\n secFetchUser: { key: 'headers.sec-fetch-user' },\n secChUa: { key: 'headers.sec-ch-ua' },\n secChUaMobile: { key: 'headers.sec-ch-ua-mobile' },\n secChUaPlatform: { key: 'headers.sec-ch-ua-platform' },\n accept: { key: 'headers.accept' },\n contentType: { key: 'headers.content-type' },\n referer: { key: 'headers.referer' },\n signatureAgent: { key: 'headers.signature-agent' },\n method: { key: 'method' },\n transport: { key: 'query.transport' },\n};\n\nconst CONTEXT_FROM_TRANSPORT = [\n { key: 'ingest.transport' },\n { value: 'beacon' },\n];\n\nexport const hints: Hint.Hints = {\n 'ingest-prerequisite': {\n text: 'The bot transformer reads its signals from ctx.ingest (path \"ingest.<name>\" by default). The upstream server source must populate them via config.ingest, which MUST use the map operator with direct request field paths (no req. prefix); a bare object like { userAgent: \"req.headers.user-agent\" } is silently inert and leaves ingest empty. With nothing in ingest the score is null and the category is \"unknown\". The map is also where the request context enters: lift the sender annotation with transport: { key: \"query.transport\" } and one instance then serves every transport, see the transport-wiring hint.',\n code: [\n {\n lang: 'json',\n code: JSON.stringify(\n {\n sources: {\n express: {\n package: '@walkeros/server-source-express',\n config: { ingest: { map: INGEST_MAP } },\n },\n },\n transformers: {\n bot: {\n package: '@walkeros/server-transformer-bot',\n config: { settings: { context: CONTEXT_FROM_TRANSPORT } },\n },\n },\n },\n null,\n 2,\n ),\n },\n ],\n },\n 'transport-wiring': {\n text: 'settings.context accepts a Mapping.Value, so one instance covers a deployment that receives beacons, pixels and fetches at the same endpoint. The sender declares the transport, the source lifts it, the transformer resolves it per request: add ?transport=beacon to the collect URL and ?transport=pixel to the pixel embed URL, then map transport: { key: \"query.transport\" } in the source (the normalized express scope exposes the parsed query, so no extra plumbing is needed). Give the bot config a fallback array so unannotated senders still land on a sane profile: [{ \"key\": \"ingest.transport\" }, { \"value\": \"beacon\" }]. Mind the shape: a bare string is read as a lookup path and must contain a dot, so a literal inside the array is written { \"value\": \"beacon\" } and never \"beacon\". Anything that resolves outside the vocabulary falls back to \"auto\", which scores less rather than scoring wrong. A query parameter is a claim by whoever controls the sender, which is fine for separating your own transports and worthless against a client that wants to be scored as a beacon; when you need server truth instead, give pixels their own route and derive the context from the request path.',\n code: [\n {\n lang: 'html',\n code: '<img src=\"https://collect.example.com/px.gif?transport=pixel\" width=\"1\" height=\"1\" alt=\"\">',\n },\n {\n lang: 'json',\n code: JSON.stringify(\n {\n sources: {\n express: {\n package: '@walkeros/server-source-express',\n config: {\n ingest: {\n map: {\n userAgent: { key: 'headers.user-agent' },\n transport: { key: 'query.transport' },\n },\n },\n },\n },\n },\n transformers: {\n bot: {\n package: '@walkeros/server-transformer-bot',\n config: { settings: { context: CONTEXT_FROM_TRANSPORT } },\n },\n },\n },\n null,\n 2,\n ),\n },\n ],\n },\n 'declared-signals': {\n text: 'Absence is ambiguous: \"the client sent no Sec-CH-UA\" and \"the operator never mapped Sec-CH-UA\" both arrive as undefined. So an absence-based check runs only when its input name appears explicitly in settings.input, which is you asserting the signal is wired. Reading still falls back to the defaults, so listing a name with its default path costs nothing and unlocks the check. Until then ingest.bot.reasons reports ch_not_declared, fetchmeta_not_declared or accept_not_declared, naming exactly which mapping to add.',\n code: [\n {\n lang: 'json',\n code: JSON.stringify(\n {\n transformers: {\n bot: {\n package: '@walkeros/server-transformer-bot',\n config: {\n settings: {\n context: 'beacon',\n input: {\n acceptLanguage: 'ingest.acceptLanguage',\n acceptEncoding: 'ingest.acceptEncoding',\n secFetchSite: 'ingest.secFetchSite',\n secFetchMode: 'ingest.secFetchMode',\n secFetchDest: 'ingest.secFetchDest',\n secChUa: 'ingest.secChUa',\n },\n },\n },\n },\n },\n },\n null,\n 2,\n ),\n },\n ],\n },\n 'output-routing': {\n text: 'Outputs default to event.user.botScore, event.user.botCategory, event.user.botProduct and ingest.bot.reasons. Any value can be a dot path to rename and reroute the field, or false to disable it. An \"ingest.\" prefix keeps a field off the analytics event while still routing on it downstream. Note that ingest is shared by every event in a scope and the write is an assignment, so if a before-chain step fans one input into several events, point botReasons at an event path to keep per-event attribution.',\n code: [\n {\n lang: 'json',\n code: JSON.stringify(\n {\n transformers: {\n bot: {\n package: '@walkeros/server-transformer-bot',\n config: {\n settings: {\n output: {\n botScore: 'ingest.bot.score',\n botCategory: 'ingest.bot.category',\n botProduct: false,\n botReasons: 'ingest.bot.reasons',\n },\n },\n },\n },\n },\n },\n null,\n 2,\n ),\n },\n ],\n },\n 'destination-filtering': {\n text: 'Recommended destination-mapping recipes. Drop everything automated: botScore > 50. Drop crawlers but keep AI traffic a person triggered: botScore > 50 AND botCategory != \"ai-agent\". Keep link unfurls, which mean somebody just shared the URL: botCategory != \"link-preview\". AI visibility report: group by botProduct WHERE botCategory IN (\"ai-agent\", \"ai-crawler\"). Note that botScore is null for category \"unknown\", which means not measured rather than human, and evaluates false under a > 50 filter. The transformer never drops events; filtering is always a destination decision.',\n },\n 'detection-scope': {\n text: 'Detection runs in two layers. Deterministic, first match wins: missing UA (70), AI agent or crawler UA map (90, with the category and product), non-AI crawler map for search engines, SEO tools, monitors and link unfurlers (90), isbot (80), and values impossible for the pinned context (75). Graded, capped at 60, when no deterministic rung fires: client-hint version mismatch, client hints missing on a Chromium UA, wildcard Accept on a typed context, Fetch Metadata missing or off-profile, and missing Accept-Language or Accept-Encoding. It will NOT catch: residential-proxy plus stealth Chrome with realistic behavior; in-browser agents such as Claude for Chrome, which drive a real session and are header-identical to it; UA-spoofing privacy tools. For commercial-grade detection use Cloudflare Bot Management, DataDome, or HUMAN.',\n },\n};\n"],"mappings":"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","names":[]}
|