@agentstats/sdk 0.0.0-stage → 0.4.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,516 @@
1
+ //#region src/bots.d.ts
2
+ /**
3
+ * Known bots, AI crawlers and AI agents, recognised by their User-Agent. This is the
4
+ * table the AgentStats server classifies hits with, so what `detectBot()` says in your
5
+ * app is what your dashboard shows.
6
+ *
7
+ * Sources: vendor documentation (OpenAI, Anthropic, Perplexity, Google, Microsoft,
8
+ * Apple, Common Crawl, DuckDuckGo, Mistral) and the ai-robots-txt community list,
9
+ * checked September 2026.
10
+ */
11
+ type BotCategory = 'training' | 'search' | 'assistant' | 'agent' | 'search_engine' | 'other';
12
+ /** Categories that count as "AI traffic". Search engines and other bots are shown separately. */
13
+ export declare const AI_CATEGORIES: BotCategory[];
14
+ export declare const isAiCategory: (c: string) => boolean;
15
+ interface BotProduct {
16
+ name: string;
17
+ operator: string;
18
+ }
19
+ /** Products group several bots from one vendor ("ChatGPT" = GPTBot + OAI-SearchBot + ChatGPT-User + ChatGPT agent). */
20
+ export declare const BOT_PRODUCTS: {
21
+ chatgpt: {
22
+ name: string;
23
+ operator: string;
24
+ };
25
+ claude: {
26
+ name: string;
27
+ operator: string;
28
+ };
29
+ perplexity: {
30
+ name: string;
31
+ operator: string;
32
+ };
33
+ gemini: {
34
+ name: string;
35
+ operator: string;
36
+ };
37
+ copilot: {
38
+ name: string;
39
+ operator: string;
40
+ };
41
+ meta: {
42
+ name: string;
43
+ operator: string;
44
+ };
45
+ apple: {
46
+ name: string;
47
+ operator: string;
48
+ };
49
+ amazon: {
50
+ name: string;
51
+ operator: string;
52
+ };
53
+ mistral: {
54
+ name: string;
55
+ operator: string;
56
+ };
57
+ deepseek: {
58
+ name: string;
59
+ operator: string;
60
+ };
61
+ bytedance: {
62
+ name: string;
63
+ operator: string;
64
+ };
65
+ xai: {
66
+ name: string;
67
+ operator: string;
68
+ };
69
+ duckduckgo: {
70
+ name: string;
71
+ operator: string;
72
+ };
73
+ commoncrawl: {
74
+ name: string;
75
+ operator: string;
76
+ };
77
+ cohere: {
78
+ name: string;
79
+ operator: string;
80
+ };
81
+ you: {
82
+ name: string;
83
+ operator: string;
84
+ };
85
+ qwen: {
86
+ name: string;
87
+ operator: string;
88
+ };
89
+ kimi: {
90
+ name: string;
91
+ operator: string;
92
+ };
93
+ baidu: {
94
+ name: string;
95
+ operator: string;
96
+ };
97
+ yandex: {
98
+ name: string;
99
+ operator: string;
100
+ };
101
+ huawei: {
102
+ name: string;
103
+ operator: string;
104
+ };
105
+ ai2: {
106
+ name: string;
107
+ operator: string;
108
+ };
109
+ brave: {
110
+ name: string;
111
+ operator: string;
112
+ };
113
+ kagi: {
114
+ name: string;
115
+ operator: string;
116
+ };
117
+ phind: {
118
+ name: string;
119
+ operator: string;
120
+ };
121
+ poe: {
122
+ name: string;
123
+ operator: string;
124
+ };
125
+ manus: {
126
+ name: string;
127
+ operator: string;
128
+ };
129
+ coding: {
130
+ name: string;
131
+ operator: string;
132
+ };
133
+ aidata: {
134
+ name: string;
135
+ operator: string;
136
+ };
137
+ google: {
138
+ name: string;
139
+ operator: string;
140
+ };
141
+ automation: {
142
+ name: string;
143
+ operator: string;
144
+ };
145
+ seo: {
146
+ name: string;
147
+ operator: string;
148
+ };
149
+ social: {
150
+ name: string;
151
+ operator: string;
152
+ };
153
+ monitor: {
154
+ name: string;
155
+ operator: string;
156
+ };
157
+ tools: {
158
+ name: string;
159
+ operator: string;
160
+ };
161
+ other: {
162
+ name: string;
163
+ operator: string;
164
+ };
165
+ };
166
+ type BotProductId = keyof typeof BOT_PRODUCTS;
167
+ interface BotPattern {
168
+ id: string;
169
+ name: string;
170
+ product: BotProductId;
171
+ category: BotCategory;
172
+ /** Matched against the User-Agent header. `null` = only identified another way (signature). */
173
+ pattern: RegExp | null;
174
+ }
175
+ /**
176
+ * Order matters: the first matching pattern wins, so specific tokens come
177
+ * before the vendor's generic ones and the catch-all patterns come last.
178
+ */
179
+ export declare const BOT_PATTERNS: BotPattern[];
180
+ /** Identify a bot from its User-Agent. Returns null for (what looks like) a normal browser. */
181
+ export declare function classifyUserAgent(ua: string | null | undefined): BotPattern | null;
182
+ /**
183
+ * Which AI assistant sent a human to this page, from the Referer header and/or
184
+ * the landing URL's `utm_source`. Returns the product id, or null.
185
+ */
186
+ export declare function classifyReferral(referrer: string | null | undefined, url: string | null | undefined): BotProductId | null;
187
+ interface BotDetection {
188
+ /** Any automated client: AI crawlers and agents, search engines, scripts, monitors, link previews. */
189
+ isBot: boolean;
190
+ /** An AI crawler, assistant fetch or agent (categories training, search, assistant, agent). */
191
+ isAI: boolean;
192
+ /** Stable id, e.g. `gptbot`, `claude-user`, `google-agent`. */
193
+ id: string | null;
194
+ /** Display name, e.g. `GPTBot`. */
195
+ name: string | null;
196
+ category: BotCategory | null;
197
+ /** Product it belongs to, e.g. `ChatGPT`. */
198
+ product: string | null;
199
+ /** Who runs it, e.g. `OpenAI`. */
200
+ operator: string | null;
201
+ /** Loads pages in a real browser, so it runs JavaScript and can call your WebMCP tools. */
202
+ browser: boolean;
203
+ /** Sent Web Bot Auth signature headers. Not checked here: the dashboard verifies the signature. */
204
+ signed: boolean;
205
+ /** For people (not bots): the AI assistant that sent them here, e.g. `ChatGPT`, from the Referer or utm_source. */
206
+ referral: string | null;
207
+ }
208
+ type HeaderSource = {
209
+ get: (name: string) => string | null;
210
+ } | Record<string, string | string[] | undefined>;
211
+ /** A fetch `Request`, a `Headers` object, a Node `IncomingMessage`, or anything with `headers` (and optionally `url`). */
212
+ type DetectInput = Request | Headers | {
213
+ headers: HeaderSource;
214
+ url?: string;
215
+ } | null | undefined;
216
+ /**
217
+ * What kind of client sent this request, from its User-Agent, Web Bot Auth headers and referrer.
218
+ * Synchronous and dependency-free, so it works in any server, middleware or edge function.
219
+ *
220
+ * It reports who the client *claims* to be. Anyone can send `User-Agent: GPTBot`, so don't
221
+ * use it to grant access; your dashboard verifies each hit against the vendor's IP ranges,
222
+ * reverse DNS and signatures.
223
+ *
224
+ * detectBot(request) // fetch Request (Next, SvelteKit, Astro, Hono, Workers…)
225
+ * detectBot(await headers()) // Next.js server components
226
+ * detectBot(req) // Node / Express IncomingMessage
227
+ */
228
+ export declare function detectBot(input: DetectInput): BotDetection;
229
+ //#endregion
230
+ //#region src/server.d.ts
231
+ export declare const SERVER_SDK_VERSION = "0.3.0";
232
+ export interface ServerOptions {
233
+ /**
234
+ * Secret server key from Site settings (`al_srv_…`). Never expose it to browsers.
235
+ * Without one, the adapters still detect bots but nothing is sent.
236
+ */
237
+ serverKey?: string | undefined;
238
+ /** Hits endpoint, e.g. `https://your-host/api/ingest/hits`. Defaults to `AGENTSTATS_ENDPOINT` or the build-time default. */
239
+ endpoint?: string;
240
+ /**
241
+ * Secret key of an API in your project (Project settings → APIs, `al_api_…`). Turns on API
242
+ * tracking for the requests `apiRoutes` matches. Works with or without `serverKey`.
243
+ */
244
+ apiKey?: string | undefined;
245
+ /** API ingest endpoint. Defaults to `endpoint` with `/hits` replaced by `/api`. */
246
+ apiEndpoint?: string;
247
+ /**
248
+ * Which requests are API calls. Default: every request when there's no `serverKey` (an
249
+ * API-only server), else paths under `/api` and `/v1`, `/v2`…
250
+ */
251
+ apiRoutes?: RegExp | ((path: string) => boolean);
252
+ /**
253
+ * Who made a call, so one agent's calls group into a session: the credential, or anything that
254
+ * identifies the caller (an API key id, an OAuth client and user). It's sent as an HMAC keyed
255
+ * with your API key, never as is. Return `{ id, agent }` to name the agent behind a credential
256
+ * you know, e.g. `{ id: clientId, agent: 'claude-ai' }` for your OAuth client for Claude.
257
+ * Default: the `Authorization`, `X-API-Key` or `API-Key` header. `false`: don't group by caller.
258
+ */
259
+ apiCaller?: false | ((header: (name: string) => string | null | undefined) => string | {
260
+ id: string;
261
+ agent?: string;
262
+ } | null | undefined);
263
+ /** Count human page views (a number per day, nothing else) so the dashboard can show AI share of traffic. Default true. */
264
+ countHumans?: boolean;
265
+ /**
266
+ * Set a short-lived `_al_v` cookie on responses to AI agents that browse with a real browser
267
+ * (ChatGPT agent, Google-Agent, headless browsers). The browser SDK reads it to link that visit
268
+ * to the WebMCP tool calls the agent makes. Never set for humans. Default true.
269
+ */
270
+ visitCookie?: boolean;
271
+ /**
272
+ * Express/Node only: read the client IP, protocol and host from proxy headers (CF-Connecting-IP,
273
+ * X-Real-IP, X-Forwarded-For, …). By default this is automatic: headers are trusted only when the
274
+ * connection comes from a private or loopback address, i.e. from your own proxy or load balancer.
275
+ * `true` always trusts them, `false` never does. The fetch adapter always reads platform headers.
276
+ */
277
+ trustProxy?: boolean;
278
+ /**
279
+ * Read the client IP from this header and nothing else, e.g. `cf-connecting-ip` behind Cloudflare.
280
+ * Only set it if your proxy or platform always overwrites that header, or clients can forge their IP.
281
+ * Without it, Vercel, Netlify, Fly and Cloudflare Workers are recognised and their header is used.
282
+ */
283
+ ipHeader?: string;
284
+ /** Skip requests whose path matches (health checks, internal APIs, …). */
285
+ ignore?: RegExp | ((path: string) => boolean);
286
+ /** Batch size / interval for long-running servers. Defaults 100 hits / 5000 ms. */
287
+ maxBatch?: number;
288
+ flushIntervalMs?: number;
289
+ debug?: boolean;
290
+ disabled?: boolean;
291
+ fetch?: typeof fetch;
292
+ }
293
+ export interface HitRecord {
294
+ ts: number;
295
+ method: string;
296
+ url: string;
297
+ status: number | null;
298
+ ua: string | null;
299
+ ip: string | null;
300
+ referrer: string | null;
301
+ headers?: Record<string, string>;
302
+ visit_id?: string;
303
+ /** Asked for Markdown (Accept: text/markdown, or a .md URL), and got it (the response's content type). */
304
+ accept_md?: boolean;
305
+ served_md?: boolean;
306
+ }
307
+ /** One API call by an agent, as sent to the API ingest. */
308
+ export interface ApiRecord {
309
+ ts: number;
310
+ method: string;
311
+ /** Route pattern (`/v1/users/:id`): the framework's when known, else the path with values replaced. */
312
+ route: string;
313
+ status: number | null;
314
+ durationMs: number | null;
315
+ ua: string | null;
316
+ /** Only for signed requests and agent User-Agents, to verify them; discarded by the server. */
317
+ ip: string | null;
318
+ /** Only for signed requests, to verify the signature; discarded by the server. */
319
+ url: string | null;
320
+ headers: Record<string, string>;
321
+ agent: string | null;
322
+ run: string | null;
323
+ actor: string | null;
324
+ actorAgent: string | null;
325
+ }
326
+ type HeaderGetter = (name: string) => string | null | undefined;
327
+ interface Decision {
328
+ /** `api`: a call by an agent; `api-other`: any other API call (only counted). */
329
+ kind: 'bot' | 'referral' | 'human' | 'api' | 'api-other' | 'ignore';
330
+ detection: BotDetection;
331
+ record?: HitRecord;
332
+ api?: ApiRecord;
333
+ /** The caller's credential (raw until the batch is sent, when it's replaced by its HMAC). */
334
+ caller?: string | null;
335
+ /** performance.now() when the request arrived, for the call's duration. */
336
+ started?: number;
337
+ visitId?: string;
338
+ /** Set-Cookie value to add to the response, if any. */
339
+ cookie?: string;
340
+ }
341
+ /** A path as a route pattern: values (ids, UUIDs, tokens, emails) replaced by `:id`. */
342
+ export declare function routeFromPath(path: string): string;
343
+ /** A framework route pattern as `/users/:id`: SvelteKit and Astro `[id]`/`[...rest]`, minus route groups. */
344
+ export declare function normalizeRoutePattern(pattern: string): string;
345
+ type WaitUntil = (p: Promise<unknown>) => void;
346
+ /** Loopback, private (RFC 1918 / ULA), link-local and carrier-grade NAT addresses: never a crawler's real IP. */
347
+ export declare function isPrivateIp(ip: string | null | undefined): boolean;
348
+ export declare class AgentStatsServer {
349
+ readonly options: Required<Omit<ServerOptions, 'ignore' | 'fetch' | 'endpoint' | 'trustProxy' | 'ipHeader' | 'serverKey' | 'apiKey' | 'apiEndpoint' | 'apiRoutes' | 'apiCaller'>> & Pick<ServerOptions, 'ignore' | 'trustProxy' | 'ipHeader' | 'serverKey' | 'apiKey' | 'apiRoutes' | 'apiCaller'> & {
350
+ endpoint: string;
351
+ apiEndpoint: string;
352
+ };
353
+ private queue;
354
+ private humans;
355
+ private humanCount;
356
+ private apiQueue;
357
+ private apiOther;
358
+ private apiOtherCount;
359
+ private apiHmacKey;
360
+ /** HMACs of recent callers: an agent reuses its credential, so each is computed once. */
361
+ private apiHmacs;
362
+ /** Bot and AI traffic is sent (there's a serverKey). */
363
+ private readonly hitsOn;
364
+ /** API calls are tracked (there's an apiKey). */
365
+ private readonly apiOn;
366
+ private timer;
367
+ private lastFlush;
368
+ private inflight;
369
+ private _fetch;
370
+ constructor(opts: ServerOptions);
371
+ /** Express / Connect middleware: `app.use(lens.express())`. The detection is on `res.locals.bot`. */
372
+ express(): (req: NodeLikeRequest, res: NodeLikeResponse, next?: (err?: unknown) => void) => void;
373
+ /** Plain Node `http`. Call before the response is written. Returns what `detectBot` says about the request. */
374
+ node(req: NodeLikeRequest, res: NodeLikeResponse): BotDetection;
375
+ /**
376
+ * Fetch-API requests (Next.js proxy/middleware, Cloudflare Workers, Bun, Deno).
377
+ * Pass `response` to let AgentStats set the visit cookie on it, and `waitUntil` on serverless/edge
378
+ * so the hit is sent after the response. Returns what `detectBot` says about the request.
379
+ */
380
+ track(request: Request, init?: {
381
+ status?: number | null;
382
+ ip?: string | null;
383
+ response?: {
384
+ headers: Headers;
385
+ } | null;
386
+ waitUntil?: WaitUntil;
387
+ route?: string;
388
+ }): BotDetection;
389
+ /** Hono middleware: `app.use(lens.hono())`. The detection is on `c.get('bot')`. */
390
+ hono(): (c: {
391
+ req: {
392
+ raw: Request;
393
+ routePath?: string;
394
+ matchedRoutes?: {
395
+ path: string;
396
+ method: string;
397
+ }[];
398
+ };
399
+ res: Response;
400
+ env?: any;
401
+ set?(key: string, value: any): void;
402
+ executionCtx?: {
403
+ waitUntil: WaitUntil;
404
+ };
405
+ }, next: () => Promise<void>) => Promise<void>;
406
+ /** SvelteKit: `export const handle = lens.sveltekit()` in `src/hooks.server.ts` (or inside `sequence()`). The detection is on `event.locals.bot`. */
407
+ sveltekit(): <R extends Response>({ event, resolve }: {
408
+ event: SvelteKitEventLike;
409
+ resolve: (event: any) => R | Promise<R>;
410
+ }) => Promise<R>;
411
+ /** Astro: `export const onRequest = lens.astro()` in `src/middleware.ts` (or inside `sequence()`). The detection is on `Astro.locals.bot`. */
412
+ astro(): (context: AstroContextLike, next: () => Promise<Response>) => Promise<Response>;
413
+ /**
414
+ * Nuxt / Nitro plugin: `export default defineNitroPlugin(lens.nitro())` in `server/plugins/agentstats.ts`.
415
+ * The detection is on `event.context.bot` in server routes, and on `useRequestEvent()?.context.bot` in pages.
416
+ * Works on every Nitro preset (Node, Vercel, Netlify, Cloudflare…).
417
+ */
418
+ nitro(): (nitroApp: {
419
+ hooks: {
420
+ hook: (name: any, fn: (...args: any[]) => unknown) => unknown;
421
+ };
422
+ }) => void;
423
+ /**
424
+ * Wrap a `fetch(request, env, ctx)` handler (Cloudflare Workers, Bun.serve, Deno.serve), or a Next.js
425
+ * route handler. `route` names the handler's route pattern for API tracking, e.g. `/api/users/[id]`.
426
+ */
427
+ wrapFetch<A extends unknown[]>(handler: (request: Request, ...rest: A) => Response | Promise<Response>, opts?: {
428
+ route?: string;
429
+ }): (request: Request, ...rest: A) => Promise<Response>;
430
+ /** Send everything queued now. */
431
+ flush(): Promise<void>;
432
+ private flushHits;
433
+ private flushApi;
434
+ /** HMAC-SHA256 of a credential keyed with the API key (hex, 128 bits), or null without Web Crypto. */
435
+ private hmac;
436
+ private computeHmac;
437
+ /** Decide what a request is (synchronously, so the visit cookie can still be set). */
438
+ decide(method: string, url: string, header: HeaderGetter, ip: string | null): Decision;
439
+ private isApiPath;
440
+ /** An API call: by an agent (kept, with how we know) or not (only counted). */
441
+ private decideApi;
442
+ /** Note whether the response to a bot was Markdown, from its content type (when the adapter can see it). */
443
+ private setContentType;
444
+ /** Use the framework's route pattern for an API call when it fits the request's path. */
445
+ private setRoute;
446
+ /** Queue a decided request once its status is known. */
447
+ commit(d: Decision, status: number | null, waitUntil?: WaitUntil | undefined): void;
448
+ private fetchDecision;
449
+ private fetchFinish;
450
+ private h3Decision;
451
+ /** Client IP of a fetch-API request: the configured header, else the platform's, else the usual forwarding headers. */
452
+ private fetchIp;
453
+ /** Whether this request came through a proxy whose forwarding headers we should believe. */
454
+ private viaProxy;
455
+ private nodeUrl;
456
+ private nodeIp;
457
+ private log;
458
+ }
459
+ export interface NodeLikeRequest {
460
+ method?: string;
461
+ url?: string;
462
+ originalUrl?: string;
463
+ headers: Record<string, string | string[] | undefined>;
464
+ socket?: {
465
+ remoteAddress?: string;
466
+ encrypted?: boolean;
467
+ };
468
+ /** Express: the matched route and the mount path of its router. */
469
+ route?: {
470
+ path?: unknown;
471
+ };
472
+ baseUrl?: string;
473
+ }
474
+ export interface NodeLikeResponse {
475
+ statusCode?: number;
476
+ /** Express */
477
+ locals?: Record<string, unknown>;
478
+ headersSent?: boolean;
479
+ getHeader?: (name: string) => unknown;
480
+ setHeader?: (name: string, value: string | string[]) => unknown;
481
+ once?: (event: 'finish', cb: () => void) => unknown;
482
+ }
483
+ export interface SvelteKitEventLike {
484
+ request: Request;
485
+ locals: object;
486
+ platform?: unknown;
487
+ isSubRequest?: boolean;
488
+ getClientAddress: () => string;
489
+ route?: {
490
+ id: string | null;
491
+ };
492
+ }
493
+ export interface AstroContextLike {
494
+ request: Request;
495
+ locals: object;
496
+ clientAddress?: string;
497
+ isPrerendered?: boolean;
498
+ routePattern?: string;
499
+ }
500
+ export interface H3EventLike {
501
+ method?: string;
502
+ path?: string;
503
+ headers?: Headers;
504
+ context: Record<string | symbol, unknown>;
505
+ node?: {
506
+ req: NodeLikeRequest;
507
+ res: NodeLikeResponse;
508
+ };
509
+ web?: {
510
+ request?: Request;
511
+ };
512
+ waitUntil?: WaitUntil;
513
+ }
514
+ export declare function createAgentStats(options: ServerOptions): AgentStatsServer;
515
+ //#endregion
516
+ export { type BotCategory, type BotDetection, type BotPattern, type BotProduct, type BotProductId, type DetectInput, createAgentStats as default };