@apideck/agent-analytics 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,11 +1,12 @@
1
- import { T as TrackVisitOptions, C as CaptureEvent, A as AnalyticsAdapter } from './types-DKqlfVz6.js';
1
+ import { T as TrackVisitOptions, C as CaptureEvent, A as AnalyticsAdapter } from './types-sQoQK-ox.js';
2
+ export { B as BotVerificationLike } from './types-sQoQK-ox.js';
2
3
  export { posthogAnalytics } from './adapters/posthog.js';
3
4
  export { webhookAnalytics } from './adapters/webhook.js';
4
5
 
5
6
  /**
6
- * Capture an event describing the incoming request. Fire-and-forget: awaits
7
- * the adapter but swallows errors so a downed analytics backend never breaks
8
- * the response path. Callers typically don't await the returned promise.
7
+ * Capture an event describing the incoming request. Fire-and-forget: awaits the
8
+ * adapter but routes errors to {@link TrackVisitOptions.onError} rather than
9
+ * letting them reach the response path. Callers typically don't await it.
9
10
  *
10
11
  * By default, captures every request so coding-agent traffic (axios, curl,
11
12
  * Electron, …) shows up alongside branded crawlers. Set `onlyBots: true` to
@@ -131,110 +132,46 @@ declare function classifyAgent(userAgent: string | null | undefined): AgentClass
131
132
  declare function classifyRequest(req: Request): AgentClassification;
132
133
 
133
134
  /**
134
- * djb2 hash returning an 8-char hex string prefixed with `anon_`. Used to
135
- * build stable anonymous distinct-ids from `ip:ua:...` tuples without
136
- * collecting identifying data. Not cryptographic — collisions are fine for
137
- * analytics segmentation.
138
- */
139
- declare function hashId(input: string): string;
140
-
141
- /**
142
- * Verdict on whether a request's claimed crawler identity holds up against the
143
- * vendor's published IP ranges.
135
+ * Keyed, non-reversible anonymous identifiers.
144
136
  *
145
- * - `'verified'` the UA claims a vendor we can check, and the client IP is
146
- * inside that vendor's published ranges. High confidence.
147
- * - `'spoofed'` the UA claims a vendor we can check, and the IP is **not**
148
- * in its ranges. Someone is impersonating the crawler.
149
- * - `'unverifiable'` the UA claims a vendor that publishes no feed we bundle
150
- * (Bytespider, Amazon, Meta, …), or no usable client IP was available.
151
- * - `'not-claimed'` the UA doesn't claim a verifiable crawler at all. This is
152
- * the normal verdict for browsers and HTTP clients; it is *not* a negative
153
- * finding.
137
+ * The previous implementation was an unsalted 32-bit djb2 over `ip:userAgent`.
138
+ * Because the user agent is emitted in plaintext on the same event, an attacker
139
+ * held half the preimage and only had to search the IPv4 space recovering a
140
+ * residential IP took 75 seconds single-threaded. That is pseudonymisation, not
141
+ * anonymisation, and it does not survive GDPR Recital 26.
142
+ *
143
+ * This uses HMAC-SHA-256 with a caller-supplied secret, truncated to 64 bits.
144
+ * Web Crypto is available on Vercel Edge, Cloudflare Workers, Deno and Node 18+.
154
145
  */
155
- type VerificationVerdict = 'verified' | 'spoofed' | 'unverifiable' | 'not-claimed';
156
- /** Why a request could not be judged. Only set when verdict is 'unverifiable'. */
157
- type UnverifiableReason = 'no-published-ranges' | 'client-side-agent' | 'no-client-ip';
158
- interface BotVerification {
159
- verdict: VerificationVerdict;
160
- reason?: UnverifiableReason;
161
- /** Vendor label the UA claims, when it claims one. */
162
- claimed: string | null;
163
- /**
164
- * Convenience boolean for filtering: `true` only for `'verified'`, `false`
165
- * only for `'spoofed'`, `null` when no judgement was possible. Deliberately
166
- * tri-state — collapsing "unverifiable" into `false` would brand every
167
- * Bytespider and Amazonbot hit an impostor.
168
- */
169
- verified: boolean | null;
146
+ /** Thrown when a secret is missing or unusable. */
147
+ declare class HashSecretError extends Error {
148
+ constructor(message: string);
170
149
  }
171
- /** Vendor labels this build can produce a verified/spoofed verdict for. */
172
- declare function verifiableVendors(): readonly string[];
173
150
  /**
174
- * Check a claimed crawler identity against the vendor's published IP ranges.
175
- *
176
- * Pass the client IP you already trust — on Vercel and Cloudflare that is the
177
- * first hop of `x-forwarded-for`. If your edge doesn't strip client-supplied
178
- * `X-Forwarded-For`, an attacker controls this value and a `'verified'` verdict
179
- * means nothing; verify your proxy's behaviour before relying on it.
151
+ * Hash `input` under `secret`, returning `anon_` followed by 16 hex characters
152
+ * (64 bits — collision-free well past any realistic distinct-visitor count).
153
+ *
154
+ * The secret must be stable across instances for identifiers to be comparable
155
+ * over time, and secret from anyone who can read your events: publishing it
156
+ * makes the identifier exactly as reversible as the old implementation was.
157
+ * Rotating it deliberately breaks continuity, which is correct behaviour for a
158
+ * privacy-preserving id.
180
159
  */
181
- declare function verifyBotIdentity(userAgent: string | null | undefined, ip: string | null | undefined): BotVerification;
160
+ declare function hashId(input: string, secret: string): Promise<string>;
182
161
  /**
183
- * Extract the client IP the way {@link trackVisit} does first hop of
184
- * `x-forwarded-for`, falling back to the platform-specific headers.
162
+ * Generate a random secret. Used as the default when none is configured, so the
163
+ * privacy-preserving path is the one you get by doing nothing. Identifiers are
164
+ * then only stable within a single instance's lifetime — set a real secret when
165
+ * you need them comparable across instances and over time.
185
166
  */
186
- declare function clientIpFromRequest(req: Request): string;
187
- /** Verify straight from a request object. */
188
- declare function verifyRequest(req: Request): BotVerification;
167
+ declare function randomSecret(): string;
189
168
 
190
- /**
191
- * Published crawler IP ranges, vendor by vendor.
192
- *
193
- * GENERATED FILE — do not edit by hand. Regenerate with:
194
- *
195
- * node scripts/refresh-bot-ranges.mjs
196
- *
197
- * Keys match the labels {@link parseBotName} returns, so a claimed identity
198
- * maps to its range list without a translation table.
199
- *
200
- * Only vendors that publish a machine-readable feed appear here. A vendor's
201
- * absence means "cannot be verified", never "not a real bot" — see
202
- * {@link verifyBotIdentity} for how that distinction is surfaced.
203
- *
204
- * Freshness is the whole ballgame: these lists rotate. Almost every OpenAI
205
- * prefix is an Azure block and every Anthropic prefix is GCP or similar, so
206
- * "came from a datacenter" proves nothing on its own — only membership in the
207
- * current published list does. A stale snapshot produces false 'spoofed'
208
- * verdicts on legitimate crawlers, which is the failure mode to fear.
209
- */
210
- /** When these ranges were captured from the vendor feeds (UTC). */
211
- declare const BOT_RANGES_CAPTURED_AT = "2026-08-02T00:00:00Z";
212
- declare const BOT_IP_RANGES: Readonly<Record<string, readonly string[]>>;
213
- /** Vendor labels this build can verify. Anything else yields a null verdict. */
214
- declare const VERIFIABLE_VENDORS: readonly string[];
215
-
216
- interface V4Range {
217
- net: number;
218
- mask: number;
169
+ /** Thrown when the analytics backend rejects, errors, or times out a capture. */
170
+ declare class CaptureTransportError extends Error {
171
+ readonly status: number | undefined;
172
+ readonly body: string | undefined;
173
+ constructor(message: string, status?: number, body?: string);
219
174
  }
220
- interface V6Range {
221
- net: bigint;
222
- bits: number;
223
- }
224
- interface CompiledRanges {
225
- v4: V4Range[];
226
- v6: V6Range[];
227
- }
228
- /**
229
- * Pre-compile a list of CIDR strings into numeric form. Invalid entries are
230
- * dropped rather than thrown — a malformed line in a vendor's published feed
231
- * shouldn't take down the whole check.
232
- */
233
- declare function compileRanges(cidrs: readonly string[]): CompiledRanges;
234
- /** True when `ip` falls inside any range in the pre-compiled set. */
235
- declare function ipInRanges(ip: string, ranges: CompiledRanges): boolean;
236
- /** Convenience wrapper — compiles on every call, so prefer {@link ipInRanges}. */
237
- declare function ipInCidr(ip: string, cidr: string): boolean;
238
175
 
239
176
  /**
240
177
  * Escape hatch for wiring a callback directly as an analytics adapter.
@@ -248,4 +185,4 @@ declare function ipInCidr(ip: string, cidr: string): boolean;
248
185
  */
249
186
  declare function customAnalytics(capture: (event: CaptureEvent) => Promise<void> | void): AnalyticsAdapter;
250
187
 
251
- export { AI_BOT_PATTERN, type AgentClassification, type AgentKind, AnalyticsAdapter, BOT_IP_RANGES, BOT_RANGES_CAPTURED_AT, type BotVerification, CaptureEvent, type CompiledRanges, HTTP_CLIENT_PATTERN, type HeadlessDetection, TrackVisitOptions, VERIFIABLE_VENDORS, type VerificationVerdict, classifyAgent, classifyRequest, clientIpFromRequest, compileRanges, customAnalytics, detectHeadless, firstUserAgentProduct, hashId, ipInCidr, ipInRanges, isAiBot, isHttpClient, parseBotName, trackVisit, verifiableVendors, verifyBotIdentity, verifyRequest };
188
+ export { AI_BOT_PATTERN, type AgentClassification, type AgentKind, AnalyticsAdapter, CaptureEvent, CaptureTransportError, HTTP_CLIENT_PATTERN, HashSecretError, type HeadlessDetection, TrackVisitOptions, classifyAgent, classifyRequest, customAnalytics, detectHeadless, firstUserAgentProduct, hashId, isAiBot, isHttpClient, parseBotName, randomSecret, trackVisit };