@apideck/agent-analytics 0.14.0 → 0.16.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.
@@ -0,0 +1,317 @@
1
+ import { b as AgentDecision, c as AgentPolicyOptions } from './policy-DMTBUe4F.js';
2
+ import { B as BotVerificationLike } from './types-Dw43eu7D.js';
3
+
4
+ /**
5
+ * Charge for training crawls. **EXPERIMENTAL.**
6
+ *
7
+ * The protocols this speaks are weeks old and moving. x402 and MPP are both
8
+ * live but their specs are unstable, MPP's settlement-confirmation header was
9
+ * not pinned publicly at the time of writing, and no agent in our production
10
+ * traffic has yet presented a payment credential. Expect this API to change
11
+ * without a major version while that settles — everything else in the package
12
+ * is stable, this is not.
13
+ *
14
+ * Today the industry's answer to bulk AI crawling is `Disallow` — over 2.5
15
+ * million sites block AI training in robots.txt. That leaves money on the
16
+ * table and depends on the crawler's goodwill to work at all.
17
+ *
18
+ * The alternative is to let them train and price it. That only works if you
19
+ * can tell training from retrieval, because they have opposite economics: a
20
+ * `GPTBot` fetch is corpus collection you get nothing back for, while a
21
+ * `ChatGPT-User` fetch is a person asking about you — charging for the second
22
+ * is charging for your own distribution. {@link agentPolicy} draws that line;
23
+ * this module turns a `'charge'` decision into the HTTP challenge.
24
+ *
25
+ * Two protocols, one status code. Both settle at the HTTP layer and both use
26
+ * 402, but the framing differs:
27
+ *
28
+ * x402 PAYMENT-REQUIRED: <base64 JSON> -> PAYMENT-SIGNATURE
29
+ * MPP WWW-Authenticate: Payment id="…" -> Authorization: Payment …
30
+ *
31
+ * MPP reuses standard HTTP authentication framing; x402 defines its own
32
+ * headers. They do not collide, so a single 402 can advertise both and let the
33
+ * agent pick — which is what {@link paymentRequired} does when given both.
34
+ *
35
+ * Scope: this emits the 402 and reads the client's payment header. It does not
36
+ * settle anything. Settlement belongs to an x402 facilitator or Stripe's MPP —
37
+ * a library that held money would inherit PCI scope and stop being something
38
+ * you can drop into middleware.
39
+ */
40
+
41
+ /**
42
+ * One way a client may pay. Field names follow x402's `PaymentRequirements`;
43
+ * values are yours — the library never invents an amount, network or asset.
44
+ */
45
+ interface PaymentRequirements {
46
+ scheme: string;
47
+ network: string;
48
+ maxAmountRequired: string;
49
+ resource: string;
50
+ description?: string;
51
+ mimeType?: string;
52
+ payTo: string;
53
+ maxTimeoutSeconds?: number;
54
+ asset: string;
55
+ extra?: Record<string, unknown>;
56
+ }
57
+ /** Which settlement protocol a challenge speaks. */
58
+ type PaymentProtocol = 'x402' | 'mpp';
59
+ /** x402: base64 JSON in a `PAYMENT-REQUIRED` header. */
60
+ interface X402Challenge {
61
+ protocol: 'x402';
62
+ /** Accepted payment methods, in preference order. At least one. */
63
+ accepts: readonly PaymentRequirements[];
64
+ /** Protocol version. Defaults to 1. */
65
+ x402Version?: number;
66
+ }
67
+ /**
68
+ * MPP: an RFC 9110 `WWW-Authenticate: Payment` challenge.
69
+ *
70
+ * Field values are yours. `request` carries the encoded challenge payload your
71
+ * MPP provider generates — the library does not construct or price it.
72
+ */
73
+ interface MppChallenge {
74
+ protocol: 'mpp';
75
+ /** Challenge identifier. */
76
+ id: string;
77
+ /** Authentication realm. */
78
+ realm: string;
79
+ /** Payment method, e.g. `'tempo'`. */
80
+ method: string;
81
+ /** Transaction intent, e.g. `'charge'`. */
82
+ intent?: string;
83
+ /** Encoded challenge data from your provider. */
84
+ request?: string;
85
+ }
86
+ type PaymentChallenge = X402Challenge | MppChallenge;
87
+ interface PaymentChallengeOptions {
88
+ /**
89
+ * Challenges to advertise. Supplying both an x402 and an MPP challenge is
90
+ * valid and usually correct: they use non-colliding headers, so one 402 can
91
+ * offer both and the agent takes whichever it speaks.
92
+ */
93
+ challenges: readonly PaymentChallenge[];
94
+ /**
95
+ * `Content-Signal` to send with the challenge. Defaults to
96
+ * `search=yes, ai-input=yes, ai-train=paid` — the whole point being that
97
+ * training is available rather than forbidden.
98
+ */
99
+ contentSignal?: string;
100
+ /** Extra response headers. */
101
+ headers?: Record<string, string>;
102
+ /** Human-readable body. Agents read the header; people read logs. */
103
+ body?: string;
104
+ }
105
+ /**
106
+ * Build a 402 challenge.
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * const decision = agentPolicy(req, { onTraining: 'charge' })
111
+ * if (decision.action === 'charge') {
112
+ * return paymentRequired({
113
+ * challenges: [
114
+ * {
115
+ * protocol: 'x402',
116
+ * accepts: [{
117
+ * scheme: 'exact',
118
+ * network: 'base',
119
+ * maxAmountRequired: '1000', // your price, your units
120
+ * resource: req.url,
121
+ * payTo: process.env.WALLET!,
122
+ * asset: process.env.USDC!
123
+ * }]
124
+ * },
125
+ * { protocol: 'mpp', id: challengeId, realm: 'example.com', method: 'tempo', intent: 'charge' }
126
+ * ]
127
+ * })
128
+ * }
129
+ * ```
130
+ */
131
+ declare function paymentRequired(opts: PaymentChallengeOptions): Response;
132
+ /** A payment credential the client sent back, and which protocol it speaks. */
133
+ interface SubmittedPayment {
134
+ protocol: PaymentProtocol;
135
+ /** Raw header value, for handing to a facilitator. */
136
+ value: string;
137
+ }
138
+ /**
139
+ * Read the client's payment credential, whichever protocol it used.
140
+ *
141
+ * x402 sends `PAYMENT-SIGNATURE`; MPP sends `Authorization: Payment …`. The
142
+ * `Payment` scheme check matters — a site behind normal auth will also have a
143
+ * Bearer or Basic `Authorization` header, and mistaking that for a payment
144
+ * would be a security-relevant confusion.
145
+ */
146
+ declare function paymentPayload(req: Request): SubmittedPayment | null;
147
+ /**
148
+ * True when the client attached a payment credential — i.e. this is the retry
149
+ * after a 402, not a fresh unpaid request.
150
+ *
151
+ * Presence is not proof. Hand the value to your facilitator to verify and
152
+ * settle; only then serve the resource.
153
+ */
154
+ declare function hasPaymentPayload(req: Request): boolean;
155
+ /**
156
+ * Attach a facilitator's settlement result to a successful response.
157
+ *
158
+ * x402 defines `PAYMENT-RESPONSE` for this. MPP's public spec did not pin a
159
+ * settlement-confirmation header at the time of writing, so pass `header` to
160
+ * name whatever your provider expects rather than have the library guess.
161
+ */
162
+ declare function withSettlement(res: Response, settlement: unknown, opts?: {
163
+ header?: string;
164
+ }): Response;
165
+ /**
166
+ * Convenience: turn an {@link AgentDecision} straight into a response, or
167
+ * `null` when the request should simply be served.
168
+ *
169
+ * Returns 403 for `'block'`, a 402 challenge for `'charge'`, and `null` for
170
+ * `'allow'` and `'meter'` — metering is an accounting concern, not a gate, so
171
+ * the request still gets served while `trackVisit` records it.
172
+ */
173
+ declare function respondToDecision(decision: AgentDecision, opts: PaymentChallengeOptions): Response | null;
174
+
175
+ /**
176
+ * The paid-access gate: policy decides *whether* to charge, a gateway decides
177
+ * *how*. **EXPERIMENTAL** — see `payments.ts`. The classification and policy
178
+ * layers underneath are stable; the payment surface is not.
179
+ *
180
+ * The split matters. We own classification — telling a training crawl from a
181
+ * retrieval fetch, which is the part nobody else does and the part that makes
182
+ * charging sane. Settlement is somebody else's job: Stripe's MPP SDK, an x402
183
+ * facilitator, whatever comes next. A library that held money would inherit PCI
184
+ * scope and stop being something you drop into middleware.
185
+ *
186
+ * So gateways are injected, exactly like analytics adapters, and this module
187
+ * takes no dependency on Stripe or any chain.
188
+ */
189
+
190
+ /**
191
+ * Outcome of handing a request to a payment gateway.
192
+ *
193
+ * - `challenge` — respond with this. The client has not paid.
194
+ * - `paid` — settled; serve the resource. `receipt` decorates the response with
195
+ * whatever proof the protocol expects.
196
+ */
197
+ type GatewayResult = {
198
+ status: 'challenge';
199
+ response: Response;
200
+ } | {
201
+ status: 'paid';
202
+ receipt?: (res: Response) => Response;
203
+ };
204
+ interface PaymentGateway {
205
+ handle(req: Request): Promise<GatewayResult>;
206
+ }
207
+ /**
208
+ * Wrap Stripe's MPP SDK.
209
+ *
210
+ * `Mppx.compose(...)` returns a handler that either yields a 402 with a
211
+ * `.challenge` response, or a settled result with `.withReceipt(res)`. This
212
+ * adapts that shape without importing it — pass the composed handler in.
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * const mppx = Mppx.create({ methods: [...], secretKey })
217
+ * const handler = Mppx.compose(
218
+ * mppx.tempo.charge({ amount: '0.01', recipient }),
219
+ * mppx.stripe.charge({ amount: '0.50', currency: 'usd' })
220
+ * )
221
+ * const gateway = mppxGateway(handler)
222
+ * ```
223
+ */
224
+ declare function mppxGateway(handler: (req: Request) => Promise<MppxResponse> | MppxResponse): PaymentGateway;
225
+ /** The subset of Stripe's MPP response we rely on. Structural, not imported. */
226
+ interface MppxResponse {
227
+ status: number;
228
+ challenge: Response;
229
+ withReceipt?: (res: Response) => Response;
230
+ }
231
+ interface X402GatewayOptions extends PaymentChallengeOptions {
232
+ /**
233
+ * Verify and settle a `PAYMENT-SIGNATURE` payload with your facilitator.
234
+ * Resolve truthy to serve the resource, falsy to re-challenge.
235
+ */
236
+ settle: (payload: string, req: Request) => Promise<boolean> | boolean;
237
+ /** Attach the facilitator's settlement result to the served response. */
238
+ receipt?: (res: Response) => Response;
239
+ }
240
+ /**
241
+ * Gateway using this library's own challenge builder plus a facilitator you
242
+ * supply. For x402, or for MPP if you are not using Stripe's SDK.
243
+ */
244
+ declare function x402Gateway(opts: X402GatewayOptions): PaymentGateway;
245
+ /** One unit of billable agent traffic. */
246
+ interface MeterRecord {
247
+ decision: AgentDecision;
248
+ /** Units consumed. One request is one unit unless you price by bytes or tokens. */
249
+ units: number;
250
+ path: string;
251
+ method: string;
252
+ }
253
+ /**
254
+ * Where billable usage goes.
255
+ *
256
+ * Metering is the model to ship first: it needs no crawler cooperation, works
257
+ * today, and produces the number you would negotiate a licence with. Charging
258
+ * per request is what the protocols define but not what a training sweep can
259
+ * actually do — no crawler in the wild retries a 402.
260
+ */
261
+ interface Meter {
262
+ record(entry: MeterRecord): Promise<void> | void;
263
+ }
264
+ interface PaymentGateOptions extends Omit<AgentPolicyOptions, 'verify'> {
265
+ gateway: PaymentGateway;
266
+ /**
267
+ * Sink for billable traffic. Called for every `'meter'` decision — serve the
268
+ * request, count it, bill out of band.
269
+ *
270
+ * Errors are swallowed: a metering failure must not turn into a failed
271
+ * response, for the same reason analytics failures do not.
272
+ */
273
+ meter?: Meter;
274
+ /**
275
+ * Identity verifier, sync or async. Unlike {@link agentPolicy}'s option this
276
+ * accepts a promise, because `paymentGate` is already async and can await it.
277
+ * That matters: `combinedVerifier()` and `webBotAuthVerifier()` are async by
278
+ * necessity — Web Bot Auth fetches the signer's key directory — so without
279
+ * this they could not be used with policy or payments at all.
280
+ */
281
+ verify?: (req: Request) => BotVerificationLike | Promise<BotVerificationLike>;
282
+ /**
283
+ * Called for every decision, paid or not — wire it to your metering so
284
+ * `'meter'` traffic is actually counted rather than merely allowed.
285
+ */
286
+ onDecision?: (decision: AgentDecision) => void;
287
+ }
288
+ /**
289
+ * Full gate: classify, decide, and either let the request through or return the
290
+ * response it should get instead.
291
+ *
292
+ * Returns `null` when the request should be served normally. That covers
293
+ * `'allow'`, `'meter'` (accounting, not a gate) and any request that has already
294
+ * paid — in which case `receipt` is handed back so you can decorate the response
295
+ * you were going to send anyway.
296
+ *
297
+ * @example
298
+ * ```ts
299
+ * const gate = await paymentGate(req, {
300
+ * onTraining: 'charge',
301
+ * verify: combinedVerifier(),
302
+ * gateway: mppxGateway(handler),
303
+ * onDecision: (d) => void trackVisit(req, { analytics, properties: { action: d.action } })
304
+ * })
305
+ * if (gate.response) return gate.response
306
+ * return gate.decorate(await serve(req))
307
+ * ```
308
+ */
309
+ declare function paymentGate(req: Request, opts: PaymentGateOptions): Promise<{
310
+ decision: AgentDecision;
311
+ /** Respond with this instead of serving, when set. */
312
+ response: Response | null;
313
+ /** Wrap the response you were going to send. Identity when nothing to add. */
314
+ decorate: (res: Response) => Response;
315
+ }>;
316
+
317
+ export { type GatewayResult as G, type Meter as M, type PaymentChallenge as P, type SubmittedPayment as S, type X402Challenge as X, type MeterRecord as a, type MppChallenge as b, type MppxResponse as c, type PaymentChallengeOptions as d, type PaymentGateOptions as e, type PaymentGateway as f, type PaymentProtocol as g, type PaymentRequirements as h, type X402GatewayOptions as i, hasPaymentPayload as j, paymentPayload as k, paymentRequired as l, mppxGateway as m, paymentGate as p, respondToDecision as r, withSettlement as w, x402Gateway as x };
package/dist/index.cjs CHANGED
@@ -1,3 +1,3 @@
1
- 'use strict';var B=/ClaudeBot|Claude-User|Claude-SearchBot|Claude-Web|Anthropic|GPTBot|ChatGPT-User|OAI-SearchBot|PerplexityBot|Perplexity-User|Google-Extended|Google-CloudVertexBot|Google-Agent|GoogleAgent-Mariner|Gemini-Deep-Research|Applebot|cohere|Bytespider|CCBot|Amazonbot|Amzn-SearchBot|NovaAct|AzureAI-SearchBot|Meta-ExternalAgent|meta-externalfetcher|meta-webindexer|FacebookBot|DuckAssistBot|MistralAI-User|YouBot|AI2Bot|Diffbot|DeepSeek|PanguBot|Webzio-Extended|omgili|Timpibot|Grok|Manus-User|quillbot|MyCentralAIScraperBot|Cursor|Windsurf/i,S=/axios\/|curl\/|(?:^|[\s(])got(?:\/|[\s(])|\bcolly\b|Electron\/|node-fetch\/|python-requests\/|Go-http-client\/|okhttp\/|aiohttp\/|Deno\//i;function f(t){return t?B.test(t):false}function m(t){return t?S.test(t):false}function T(t){if(!t||typeof t!="string")return "Other";let e=t.toLowerCase();return e.includes("chatgpt-user")||e.includes("gptbot")||e.includes("oai-searchbot")||e.includes("openai")?"ChatGPT":e.includes("claudebot")||e.includes("claude-user")||e.includes("claude-searchbot")||e.includes("claude-web")||e.includes("anthropic")?"Claude":e.includes("perplexitybot")||e.includes("perplexity-user")?"Perplexity":e.includes("ccbot")?"Common Crawl":e.includes("google-extended")||e.includes("googlebot")||e.includes("google-cloudvertexbot")||e.includes("google-agent")||e.includes("googleagent-mariner")||e.includes("gemini-deep-research")?"Google":e.includes("applebot")?"Apple":e.includes("bingbot")?"Bing":e.includes("bytespider")?"Bytespider":e.includes("amazonbot")||e.includes("amzn-searchbot")||e.includes("novaact")?"Amazon":e.includes("meta-externalagent")||e.includes("meta-externalfetcher")||e.includes("meta-webindexer")||e.includes("facebookbot")?"Meta":e.includes("mistralai-user")?"Mistral":e.includes("duckassistbot")?"DuckDuckGo":e.includes("youbot")?"You.com":e.includes("diffbot")?"Diffbot":e.includes("ai2bot")?"AI2":e.includes("cohere")?"Cohere":e.includes("cursor")?"Cursor":e.includes("windsurf")?"Windsurf":e.includes("deepseek")?"DeepSeek":e.includes("pangubot")?"Huawei":e.includes("webzio")||e.includes("omgili")?"Webz.io":e.includes("timpibot")?"Timpi":e.includes("grok")||e.includes("xai-")?"xAI":e.includes("manus-user")?"Manus":e.includes("quillbot")?"QuillBot":e.includes("azureai-searchbot")?"Microsoft":e.includes("mycentralaiscraperbot")?"MyCentralAI":e.includes("petalbot")?"PetalBot":e.includes("ahrefsbot")?"Ahrefs":e.includes("semrushbot")?"Semrush":e.includes("mj12bot")?"Majestic":e.includes("dotbot")||e.includes("rogerbot")?"Moz":e.includes("screaming frog")?"Screaming Frog":e.includes("sitebulb")?"Sitebulb":e.includes("linkfluence")?"Linkfluence":e.includes("dataforseo")?"DataForSEO":e.includes("serpstatbot")?"Serpstat":e.includes("uptimerobot")?"UptimeRobot":e.includes("pingdom")?"Pingdom":e.includes("statuscake")?"StatusCake":e.includes("newrelicpinger")?"New Relic":e.includes("datadogagent")||e.includes("datadog")?"Datadog":e.includes("slackbot")?"Slack":e.includes("twitterbot")?"Twitter":e.includes("linkedinbot")?"LinkedIn":e.includes("discordbot")?"Discord":e.includes("telegrambot")?"Telegram":e.includes("whatsapp")?"WhatsApp":e.includes("linkupbot")?"Linkup":e.includes("sogou")?"Sogou":e.includes("yandexbot")?"Yandex":e.includes("baiduspider")?"Baidu":e.includes("facebookexternalhit")?"Facebook":e.includes("com.apple.webkit")?"Apple URL Preview":e.includes("ohdear")?"Oh Dear":e.includes("scrapy")?"Scrapy":e.includes("headlesschrome")?"Headless Chrome":e.includes("phantomjs")?"PhantomJS":e.includes("wget")?"wget":e.includes("httpie")?"HTTPie":e.includes("guzzlehttp")?"Guzzle":e.includes("electron/")?"Electron":/curl\//.test(e)?"curl":/axios\//.test(e)?"axios":/(?:^|[\s(])got(?:\/|[\s(])/.test(e)?"got":/\bcolly\b/.test(e)?"colly":/node-fetch\//.test(e)?"node-fetch":/python-requests\//.test(e)?"python-requests":/go-http-client\//.test(e)?"Go http client":/okhttp\//.test(e)?"OkHttp":/aiohttp\//.test(e)?"aiohttp":/deno\//.test(e)?"Deno":e.includes("mozilla")||e.includes("chrome")||e.includes("safari")||e.includes("firefox")?"Browser":"Other"}function M(t){if(!t||typeof t!="string")return "Other";let e=t.match(/compatible;\s*([^/;\s]+)(?:\/[^\s;]*)?/i);return e&&e[1]?e[1].trim():t.trim().split("/")[0]?.trim().split(/\s+/)[0]?.trim()||"Other"}function b(t){let e=[],r=(t.headers.get("user-agent")||"").toLowerCase();if(!(r.includes("mozilla")||r.includes("chrome")||r.includes("safari")||r.includes("firefox")))return {score:0,signals:[],likely:false};t.headers.get("accept-language")||e.push("missing-accept-language"),t.headers.get("sec-fetch-mode")||e.push("missing-sec-fetch-mode");let n=t.headers.get("sec-ch-ua");n?n.toLowerCase().includes("headlesschrome")&&e.push("headless-chrome-hint"):e.push("missing-sec-ch-ua");let i=t.headers.get("accept")||"";(!i||i==="*/*")&&e.push("missing-or-bare-accept"),(t.headers.get("connection")||"").toLowerCase()==="close"&&e.push("connection-close");let s=e.length;return {score:s,signals:e,likely:s>=2}}function v(t){let e=T(t),r=f(t),o=m(t),n;return r?n="declared-crawler":o?n="coding-agent-hint":e==="Browser"?n="browser":n="other",{kind:n,label:e,isAiBot:r,codingAgentHint:o}}function p(t){let e=t.headers.get("user-agent")||"",r=v(e),o=b(t),n=r.kind,i=r.label;return n==="browser"&&o.likely&&(n="headless-likely",i="Headless"),{...r,kind:n,label:i,headless:o}}var g=class extends Error{constructor(e){super(e),this.name="HashSecretError";}};function A(){let t=globalThis.crypto;if(!t?.subtle)throw new g("Web Crypto is unavailable. agent-analytics requires Node >= 20, or any runtime exposing globalThis.crypto.subtle (Vercel Edge, Cloudflare Workers, Deno, browsers).");return t.subtle}var E=new Map;function N(t){let e=E.get(t);return e||(e=A().importKey("raw",new TextEncoder().encode(t),{name:"HMAC",hash:"SHA-256"},false,["sign"]),E.set(t,e)),e}async function x(t,e){if(typeof e!="string"||e.length===0)throw new g("hashId requires a non-empty secret");let r=await A().sign("HMAC",await N(e),new TextEncoder().encode(t)),o=new Uint8Array(r,0,8),n="";for(let i of o)n+=i.toString(16).padStart(2,"0");return "anon_"+n}function w(){let t=new Uint8Array(32);A(),globalThis.crypto.getRandomValues(t);let e="";for(let r of t)e+=r.toString(16).padStart(2,"0");return e}var k,I=false;function z(t){if(t)return t;let e=typeof process<"u"?process.env?.AGENT_ANALYTICS_ID_SECRET:void 0;return e||(k||(k=w(),I||(I=true,console.warn("[agent-analytics] No idSecret or AGENT_ANALYTICS_ID_SECRET set. Using a per-instance random secret: distinctIds will not correlate across instances or deploys."))),k)}async function O(t,e){let r=t.headers.get("user-agent")||"",o=e.onlyBots??false,n=e.skipBrowsers??false;if(!(o&&!f(r))&&!(n&&!f(r)&&!m(r)&&!b(t).likely))try{let i="/",s="";try{let C=new URL(t.url);i=C.pathname,s=C.origin;}catch{i=t.url||"/";}let a=e.origin??s,y=(t.headers.get("x-forwarded-for")||"").split(",")[0]?.trim()??"",h=t.headers.get("referer"),R=e.captureCountry&&(t.headers.get("x-vercel-ip-country")||t.headers.get("cf-ipcountry")||t.headers.get("x-country-code"))||null,_=e.captureGeo?U(t):null,c=p(t),d=e.verify?await e.verify(t):null,D=c.kind==="headless-likely"||c.kind==="browser",H=await x(`${y}:${r}`,z(e.idSecret)),G={event:e.eventName??"agent_visit",distinctId:H,timestamp:new Date().toISOString(),properties:{...e.properties,$process_person_profile:!1,$current_url:a?`${a}${i}`:i,path:i,method:t.method,...e.captureCountry?{country_code:R}:{},..._??{},...e.captureIp?{client_ip:y||null}:{},user_agent:r,is_ai_bot:c.isAiBot,bot_name:c.label,ua_category:c.kind,coding_agent_hint:c.codingAgentHint,...D?{headless_score:c.headless?.score??0,headless_likely:c.headless?.likely??!1}:{},...d?{bot_verified:d.verified,bot_verification:d.verdict,...d.reason?{bot_verification_reason:d.reason}:{}}:{},referer:h,source:e.source??null}};await e.analytics.capture(G);}catch(i){e.onError?.(i instanceof Error?i:new Error(String(i)));}}function U(t){let e=n=>{if(!n)return "";try{return decodeURIComponent(n)}catch{return n}},r=[["region",e(t.headers.get("x-vercel-ip-country-region"))],["city",e(t.headers.get("x-vercel-ip-city"))],["latitude",t.headers.get("x-vercel-ip-latitude")??""],["longitude",t.headers.get("x-vercel-ip-longitude")??""],["timezone",t.headers.get("x-vercel-ip-timezone")??""]],o={};for(let[n,i]of r)i&&(o[n]=i);return o}var u=class extends Error{status;body;constructor(e,r,o){super(e),this.name="CaptureTransportError",this.status=r,this.body=o;}};var L=/ChatGPT-User|OAI-SearchBot|Claude-User|Claude-SearchBot|Perplexity-User|claude-code|DuckAssistBot|MistralAI-User|Gemini-Deep-Research|Manus-User|YouBot/i,$=/GPTBot|ClaudeBot|Claude-Web|CCBot|Bytespider|Amazonbot|Amzn-SearchBot|Meta-ExternalAgent|meta-externalfetcher|meta-webindexer|FacebookBot|Google-Extended|Applebot-Extended|AI2Bot|Diffbot|omgili|Webzio-Extended|Timpibot|PanguBot|cohere|DeepSeek|Grok|quillbot|MyCentralAIScraperBot|NovaAct|AzureAI-SearchBot|Google-CloudVertexBot/i,j=/bingbot|Googlebot|DuckDuckBot|YandexBot|Baiduspider|PetalBot|Sogou|Applebot(?!-Extended)/i;function P(t){let e=t??"";return e?L.test(e)?"retrieval":$.test(e)?"training":j.test(e)?"search":"unknown":"unknown"}function V(t,e={}){let r=t.headers.get("user-agent")||"",o=p(t),n=o.label,i=P(r);if(i==="unknown"&&o.codingAgentHint&&(i="tooling"),e.allowList?.some(h=>h===n||r.toLowerCase().includes(h.toLowerCase())))return {action:"allow",intent:i,label:n,reason:"on allowList"};let a;return e.verify&&(a=e.verify(t).verdict,a==="spoofed")?{action:"block",intent:i,label:n,verification:a,reason:`${n} claimed but client IP is outside its published ranges`}:{action:i==="training"?e.onTraining??"meter":i==="retrieval"?e.onRetrieval??"allow":i==="search"?e.onSearch??"allow":i==="tooling"?e.onTooling??"allow":"allow",intent:i,label:n,...a?{verification:a}:{},reason:{retrieval:"a person is waiting on this answer",training:"bulk corpus collection",search:"search index crawler",tooling:"coding agent or HTTP client",unknown:"not a recognised agent"}[i]}}function W(t){let e=t.host??"https://us.i.posthog.com",r=(/^https?:\/\//.test(e)?e:`https://${e}`).replace(/\/$/,""),o=(t.path??"/i/v0/e/").replace(/^(?!\/)/,"/"),n=`${r}${o}`,i=t.fetchImpl??fetch;return {async capture(s){let a={api_key:t.apiKey,event:s.event,distinct_id:s.distinctId,timestamp:s.timestamp,properties:s.properties},l=await i(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a),keepalive:true,signal:AbortSignal.timeout(t.timeoutMs??3e3)});if(!l.ok)throw new u(`PostHog capture failed: ${l.status} ${l.statusText}`,l.status,await l.text().catch(()=>{}))}}}function K(t){let e=t.fetchImpl??fetch,r=t.transform??(o=>o);return {async capture(o){let n=await e(t.url,{method:"POST",headers:{"Content-Type":"application/json",...t.headers??{}},body:JSON.stringify(r(o)),keepalive:true,signal:AbortSignal.timeout(t.timeoutMs??3e3)});if(!n.ok)throw new u(`Webhook capture failed: ${n.status} ${n.statusText}`,n.status,await n.text().catch(()=>{}))}}}function F(t){return {capture:t}}
2
- exports.AI_BOT_PATTERN=B;exports.CaptureTransportError=u;exports.HTTP_CLIENT_PATTERN=S;exports.HashSecretError=g;exports.agentIntent=P;exports.agentPolicy=V;exports.classifyAgent=v;exports.classifyRequest=p;exports.customAnalytics=F;exports.detectHeadless=b;exports.firstUserAgentProduct=M;exports.hashId=x;exports.isAiBot=f;exports.isHttpClient=m;exports.parseBotName=T;exports.posthogAnalytics=W;exports.randomSecret=w;exports.trackVisit=O;exports.webhookAnalytics=K;//# sourceMappingURL=index.cjs.map
1
+ 'use strict';var S=/ClaudeBot|Claude-User|Claude-SearchBot|Claude-Web|Anthropic|GPTBot|ChatGPT-User|OAI-SearchBot|PerplexityBot|Perplexity-User|Google-Extended|Google-CloudVertexBot|Google-Agent|GoogleAgent-Mariner|Gemini-Deep-Research|Applebot|cohere|Bytespider|CCBot|Amazonbot|Amzn-SearchBot|NovaAct|AzureAI-SearchBot|Meta-ExternalAgent|meta-externalfetcher|meta-webindexer|FacebookBot|DuckAssistBot|MistralAI-User|YouBot|AI2Bot|Diffbot|DeepSeek|PanguBot|Webzio-Extended|omgili|Timpibot|Grok|Manus-User|quillbot|MyCentralAIScraperBot|Cursor|Windsurf/i,T=/axios\/|curl\/|(?:^|[\s(])got(?:\/|[\s(])|\bcolly\b|Electron\/|node-fetch\/|python-requests\/|Go-http-client\/|okhttp\/|aiohttp\/|Deno\//i;function h(t){return t?S.test(t):false}function p(t){return t?T.test(t):false}function v(t){if(!t||typeof t!="string")return "Other";let e=t.toLowerCase();return e.includes("chatgpt-user")||e.includes("gptbot")||e.includes("oai-searchbot")||e.includes("openai")?"ChatGPT":e.includes("claudebot")||e.includes("claude-user")||e.includes("claude-searchbot")||e.includes("claude-web")||e.includes("anthropic")?"Claude":e.includes("perplexitybot")||e.includes("perplexity-user")?"Perplexity":e.includes("ccbot")?"Common Crawl":e.includes("google-extended")||e.includes("googlebot")||e.includes("google-cloudvertexbot")||e.includes("google-agent")||e.includes("googleagent-mariner")||e.includes("gemini-deep-research")?"Google":e.includes("applebot")?"Apple":e.includes("bingbot")?"Bing":e.includes("bytespider")?"Bytespider":e.includes("amazonbot")||e.includes("amzn-searchbot")||e.includes("novaact")?"Amazon":e.includes("meta-externalagent")||e.includes("meta-externalfetcher")||e.includes("meta-webindexer")||e.includes("facebookbot")?"Meta":e.includes("mistralai-user")?"Mistral":e.includes("duckassistbot")?"DuckDuckGo":e.includes("youbot")?"You.com":e.includes("diffbot")?"Diffbot":e.includes("ai2bot")?"AI2":e.includes("cohere")?"Cohere":e.includes("cursor")?"Cursor":e.includes("windsurf")?"Windsurf":e.includes("deepseek")?"DeepSeek":e.includes("pangubot")?"Huawei":e.includes("webzio")||e.includes("omgili")?"Webz.io":e.includes("timpibot")?"Timpi":e.includes("grok")||e.includes("xai-")?"xAI":e.includes("manus-user")?"Manus":e.includes("quillbot")?"QuillBot":e.includes("azureai-searchbot")?"Microsoft":e.includes("mycentralaiscraperbot")?"MyCentralAI":e.includes("petalbot")?"PetalBot":e.includes("ahrefsbot")?"Ahrefs":e.includes("semrushbot")?"Semrush":e.includes("mj12bot")?"Majestic":e.includes("dotbot")||e.includes("rogerbot")?"Moz":e.includes("screaming frog")?"Screaming Frog":e.includes("sitebulb")?"Sitebulb":e.includes("linkfluence")?"Linkfluence":e.includes("dataforseo")?"DataForSEO":e.includes("serpstatbot")?"Serpstat":e.includes("uptimerobot")?"UptimeRobot":e.includes("pingdom")?"Pingdom":e.includes("statuscake")?"StatusCake":e.includes("newrelicpinger")?"New Relic":e.includes("datadogagent")||e.includes("datadog")?"Datadog":e.includes("slackbot")?"Slack":e.includes("twitterbot")?"Twitter":e.includes("linkedinbot")?"LinkedIn":e.includes("discordbot")?"Discord":e.includes("telegrambot")?"Telegram":e.includes("whatsapp")?"WhatsApp":e.includes("linkupbot")?"Linkup":e.includes("sogou")?"Sogou":e.includes("yandexbot")?"Yandex":e.includes("baiduspider")?"Baidu":e.includes("facebookexternalhit")?"Facebook":e.includes("com.apple.webkit")?"Apple URL Preview":e.includes("ohdear")?"Oh Dear":e.includes("scrapy")?"Scrapy":e.includes("headlesschrome")?"Headless Chrome":e.includes("phantomjs")?"PhantomJS":e.includes("wget")?"wget":e.includes("httpie")?"HTTPie":e.includes("guzzlehttp")?"Guzzle":e.includes("electron/")?"Electron":/curl\//.test(e)?"curl":/axios\//.test(e)?"axios":/(?:^|[\s(])got(?:\/|[\s(])/.test(e)?"got":/\bcolly\b/.test(e)?"colly":/node-fetch\//.test(e)?"node-fetch":/python-requests\//.test(e)?"python-requests":/go-http-client\//.test(e)?"Go http client":/okhttp\//.test(e)?"OkHttp":/aiohttp\//.test(e)?"aiohttp":/deno\//.test(e)?"Deno":e.includes("mozilla")||e.includes("chrome")||e.includes("safari")||e.includes("firefox")?"Browser":"Other"}function M(t){if(!t||typeof t!="string")return "Other";let e=t.match(/compatible;\s*([^/;\s]+)(?:\/[^\s;]*)?/i);return e&&e[1]?e[1].trim():t.trim().split("/")[0]?.trim().split(/\s+/)[0]?.trim()||"Other"}function A(t){let e=[],r=(t.headers.get("user-agent")||"").toLowerCase();if(!(r.includes("mozilla")||r.includes("chrome")||r.includes("safari")||r.includes("firefox")))return {score:0,signals:[],likely:false};t.headers.get("accept-language")||e.push("missing-accept-language"),t.headers.get("sec-fetch-mode")||e.push("missing-sec-fetch-mode");let n=t.headers.get("sec-ch-ua");n?n.toLowerCase().includes("headlesschrome")&&e.push("headless-chrome-hint"):e.push("missing-sec-ch-ua");let i=t.headers.get("accept")||"";(!i||i==="*/*")&&e.push("missing-or-bare-accept"),(t.headers.get("connection")||"").toLowerCase()==="close"&&e.push("connection-close");let s=e.length;return {score:s,signals:e,likely:s>=2}}function P(t){let e=v(t),r=h(t),o=p(t),n;return r?n="declared-crawler":o?n="coding-agent-hint":e==="Browser"?n="browser":n="other",{kind:n,label:e,isAiBot:r,codingAgentHint:o}}function m(t){let e=t.headers.get("user-agent")||"",r=P(e),o=A(t),n=r.kind,i=r.label;return n==="browser"&&o.likely&&(n="headless-likely",i="Headless"),{...r,kind:n,label:i,headless:o}}var y=class extends Error{constructor(e){super(e),this.name="HashSecretError";}};function x(){let t=globalThis.crypto;if(!t?.subtle)throw new y("Web Crypto is unavailable. agent-analytics requires Node >= 20, or any runtime exposing globalThis.crypto.subtle (Vercel Edge, Cloudflare Workers, Deno, browsers).");return t.subtle}var E=new Map;function O(t){let e=E.get(t);return e||(e=x().importKey("raw",new TextEncoder().encode(t),{name:"HMAC",hash:"SHA-256"},false,["sign"]),E.set(t,e)),e}async function w(t,e){if(typeof e!="string"||e.length===0)throw new y("hashId requires a non-empty secret");let r=await x().sign("HMAC",await O(e),new TextEncoder().encode(t)),o=new Uint8Array(r,0,8),n="";for(let i of o)n+=i.toString(16).padStart(2,"0");return "anon_"+n}function k(){let t=new Uint8Array(32);x(),globalThis.crypto.getRandomValues(t);let e="";for(let r of t)e+=r.toString(16).padStart(2,"0");return e}var C,I=false;function N(t){if(t)return t;let e=typeof process<"u"?process.env?.AGENT_ANALYTICS_ID_SECRET:void 0;return e||(C||(C=k(),I||(I=true,console.warn("[agent-analytics] No idSecret or AGENT_ANALYTICS_ID_SECRET set. Using a per-instance random secret: distinctIds will not correlate across instances or deploys."))),C)}async function z(t,e){let r=t.headers.get("user-agent")||"",o=e.onlyBots??false,n=e.skipBrowsers??false;if(!(o&&!h(r))&&!(n&&!h(r)&&!p(r)&&!A(t).likely))try{let i="/",s="";try{let B=new URL(t.url);i=B.pathname,s=B.origin;}catch{i=t.url||"/";}let c=e.origin??s,f=(t.headers.get("x-forwarded-for")||"").split(",")[0]?.trim()??"",l=t.headers.get("referer"),b=e.captureCountry&&(t.headers.get("x-vercel-ip-country")||t.headers.get("cf-ipcountry")||t.headers.get("x-country-code"))||null,_=e.captureGeo?L(t):null,u=m(t),g=e.verify?await e.verify(t):null,D=u.kind==="headless-likely"||u.kind==="browser",G=await w(`${f}:${r}`,N(e.idSecret)),H={event:e.eventName??"agent_visit",distinctId:G,timestamp:new Date().toISOString(),properties:{...e.properties,$process_person_profile:!1,$current_url:c?`${c}${i}`:i,path:i,method:t.method,...e.captureCountry?{country_code:b}:{},..._??{},...e.captureIp?{client_ip:f||null}:{},user_agent:r,is_ai_bot:u.isAiBot,bot_name:u.label,ua_category:u.kind,coding_agent_hint:u.codingAgentHint,...D?{headless_score:u.headless?.score??0,headless_likely:u.headless?.likely??!1}:{},...g?{bot_verified:g.verified,bot_verification:g.verdict,...g.reason?{bot_verification_reason:g.reason}:{}}:{},referer:l,source:e.source??null}};await e.analytics.capture(H);}catch(i){e.onError?.(i instanceof Error?i:new Error(String(i)));}}function L(t){let e=n=>{if(!n)return "";try{return decodeURIComponent(n)}catch{return n}},r=[["region",e(t.headers.get("x-vercel-ip-country-region"))],["city",e(t.headers.get("x-vercel-ip-city"))],["latitude",t.headers.get("x-vercel-ip-latitude")??""],["longitude",t.headers.get("x-vercel-ip-longitude")??""],["timezone",t.headers.get("x-vercel-ip-timezone")??""]],o={};for(let[n,i]of r)i&&(o[n]=i);return o}var d=class extends Error{status;body;constructor(e,r,o){super(e),this.name="CaptureTransportError",this.status=r,this.body=o;}};var U=/ChatGPT-User|OAI-SearchBot|Claude-User|Claude-SearchBot|Perplexity-User|claude-code|DuckAssistBot|MistralAI-User|Gemini-Deep-Research|Manus-User|YouBot/i,$=/GPTBot|ClaudeBot|Claude-Web|CCBot|Bytespider|Amazonbot|Amzn-SearchBot|Meta-ExternalAgent|meta-externalfetcher|meta-webindexer|FacebookBot|Google-Extended|Applebot-Extended|AI2Bot|Diffbot|omgili|Webzio-Extended|Timpibot|PanguBot|cohere|DeepSeek|Grok|quillbot|MyCentralAIScraperBot|NovaAct|AzureAI-SearchBot|Google-CloudVertexBot/i,j=/bingbot|Googlebot|DuckDuckBot|YandexBot|Baiduspider|PetalBot|Sogou|Applebot(?!-Extended)/i;function R(t){let e=t??"";return e?U.test(e)?"retrieval":$.test(e)?"training":j.test(e)?"search":p(e)?"tooling":"unknown":"unknown"}function V(t,e={}){let r=t.headers.get("user-agent")||"",n=m(t).label,i=R(r);if(e.allowList?.some(b=>b===n||r.toLowerCase().includes(b.toLowerCase())))return {action:"allow",intent:i,label:n,reason:"on allowList"};let c=e.verification??(e.verify?e.verify(t):void 0),a;return c&&(a=c.verdict,a==="spoofed")?{action:"block",intent:i,label:n,verification:a,reason:`${n} claimed but client IP is outside its published ranges`}:{action:i==="training"?e.onTraining??"meter":i==="retrieval"?e.onRetrieval??"allow":i==="search"?e.onSearch??"allow":i==="tooling"?e.onTooling??"allow":"allow",intent:i,label:n,...a?{verification:a}:{},reason:{retrieval:"a person is waiting on this answer",training:"bulk corpus collection",search:"search index crawler",tooling:"coding agent or HTTP client",unknown:"not a recognised agent"}[i]}}function W(t){let e=t.host??"https://us.i.posthog.com",r=(/^https?:\/\//.test(e)?e:`https://${e}`).replace(/\/$/,""),o=(t.path??"/i/v0/e/").replace(/^(?!\/)/,"/"),n=`${r}${o}`,i=t.fetchImpl??fetch;return {async capture(s){let c=s.properties.user_agent,a=s.properties.client_ip,f={api_key:t.apiKey,event:s.event,distinct_id:s.distinctId,timestamp:s.timestamp,properties:{...s.properties,...typeof c=="string"&&c?{$raw_user_agent:c}:{},...typeof a=="string"&&a?{$ip:a}:{}}},l=await i(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(f),keepalive:true,signal:AbortSignal.timeout(t.timeoutMs??3e3)});if(!l.ok)throw new d(`PostHog capture failed: ${l.status} ${l.statusText}`,l.status,await l.text().catch(()=>{}))}}}function K(t){let e=t.fetchImpl??fetch,r=t.transform??(o=>o);return {async capture(o){let n=await e(t.url,{method:"POST",headers:{"Content-Type":"application/json",...t.headers??{}},body:JSON.stringify(r(o)),keepalive:true,signal:AbortSignal.timeout(t.timeoutMs??3e3)});if(!n.ok)throw new d(`Webhook capture failed: ${n.status} ${n.statusText}`,n.status,await n.text().catch(()=>{}))}}}function F(t){return {capture:t}}
2
+ exports.AI_BOT_PATTERN=S;exports.CaptureTransportError=d;exports.HTTP_CLIENT_PATTERN=T;exports.HashSecretError=y;exports.agentIntent=R;exports.agentPolicy=V;exports.classifyAgent=P;exports.classifyRequest=m;exports.customAnalytics=F;exports.detectHeadless=A;exports.firstUserAgentProduct=M;exports.hashId=w;exports.isAiBot=h;exports.isHttpClient=p;exports.parseBotName=v;exports.posthogAnalytics=W;exports.randomSecret=k;exports.trackVisit=z;exports.webhookAnalytics=K;//# sourceMappingURL=index.cjs.map
3
3
  //# sourceMappingURL=index.cjs.map