@apideck/agent-analytics 0.13.0 → 0.15.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.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { T as TrackVisitOptions, B as BotVerificationLike, C as CaptureEvent, A as AnalyticsAdapter } from './types-sQoQK-ox.cjs';
1
+ import { T as TrackVisitOptions, B as BotVerificationLike, C as CaptureEvent, A as AnalyticsAdapter } from './types-Dw43eu7D.cjs';
2
2
  export { posthogAnalytics } from './adapters/posthog.cjs';
3
3
  export { webhookAnalytics } from './adapters/webhook.cjs';
4
4
 
@@ -219,6 +219,16 @@ interface AgentPolicyOptions {
219
219
  * an attacker picks their own verdict.
220
220
  */
221
221
  verify?: (req: Request) => BotVerificationLike;
222
+ /**
223
+ * A verification already computed elsewhere. Use this when your verifier is
224
+ * async — {@link verifyWebBotAuth} fetches a key directory, so the natural
225
+ * verifier from `@apideck/agent-analytics/verify` returns a promise and
226
+ * cannot be passed to `verify` on this synchronous function.
227
+ *
228
+ * {@link paymentGate} does this for you: it awaits the verifier and forwards
229
+ * the result here.
230
+ */
231
+ verification?: BotVerificationLike;
222
232
  /** What to do with bulk training crawlers. Defaults to `'meter'`. */
223
233
  onTraining?: AgentAction;
224
234
  /** What to do with retrieval agents. Defaults to `'allow'` — see AgentIntent. */
@@ -230,7 +240,16 @@ interface AgentPolicyOptions {
230
240
  /** Vendor labels or UA substrings always allowed, whatever the intent. */
231
241
  allowList?: readonly string[];
232
242
  }
233
- /** Classify why an agent is here, from its user agent alone. */
243
+ /**
244
+ * Classify why an agent is here, from its user agent alone.
245
+ *
246
+ * This must return exactly what {@link agentPolicy} reports for the same UA.
247
+ * It previously did not: the `tooling` promotion for HTTP-library UAs lived
248
+ * only inside `agentPolicy`, so `agentIntent('curl/8.4.0')` said `'unknown'`
249
+ * while the policy said `'tooling'` — two exported functions disagreeing on
250
+ * every HTTP client, with no way for a caller to know which was right. The
251
+ * invariant is pinned by a test.
252
+ */
234
253
  declare function agentIntent(userAgent: string | null | undefined): AgentIntent;
235
254
  /**
236
255
  * Decide what to do with a request. Pure classification plus policy — no
@@ -246,6 +265,536 @@ declare function agentIntent(userAgent: string | null | undefined): AgentIntent;
246
265
  */
247
266
  declare function agentPolicy(req: Request, opts?: AgentPolicyOptions): AgentDecision;
248
267
 
268
+ /**
269
+ * Charge for training crawls. **EXPERIMENTAL.**
270
+ *
271
+ * The protocols this speaks are weeks old and moving. x402 and MPP are both
272
+ * live but their specs are unstable, MPP's settlement-confirmation header was
273
+ * not pinned publicly at the time of writing, and no agent in our production
274
+ * traffic has yet presented a payment credential. Expect this API to change
275
+ * without a major version while that settles — everything else in the package
276
+ * is stable, this is not.
277
+ *
278
+ * Today the industry's answer to bulk AI crawling is `Disallow` — over 2.5
279
+ * million sites block AI training in robots.txt. That leaves money on the
280
+ * table and depends on the crawler's goodwill to work at all.
281
+ *
282
+ * The alternative is to let them train and price it. That only works if you
283
+ * can tell training from retrieval, because they have opposite economics: a
284
+ * `GPTBot` fetch is corpus collection you get nothing back for, while a
285
+ * `ChatGPT-User` fetch is a person asking about you — charging for the second
286
+ * is charging for your own distribution. {@link agentPolicy} draws that line;
287
+ * this module turns a `'charge'` decision into the HTTP challenge.
288
+ *
289
+ * Two protocols, one status code. Both settle at the HTTP layer and both use
290
+ * 402, but the framing differs:
291
+ *
292
+ * x402 PAYMENT-REQUIRED: <base64 JSON> -> PAYMENT-SIGNATURE
293
+ * MPP WWW-Authenticate: Payment id="…" -> Authorization: Payment …
294
+ *
295
+ * MPP reuses standard HTTP authentication framing; x402 defines its own
296
+ * headers. They do not collide, so a single 402 can advertise both and let the
297
+ * agent pick — which is what {@link paymentRequired} does when given both.
298
+ *
299
+ * Scope: this emits the 402 and reads the client's payment header. It does not
300
+ * settle anything. Settlement belongs to an x402 facilitator or Stripe's MPP —
301
+ * a library that held money would inherit PCI scope and stop being something
302
+ * you can drop into middleware.
303
+ */
304
+
305
+ /**
306
+ * One way a client may pay. Field names follow x402's `PaymentRequirements`;
307
+ * values are yours — the library never invents an amount, network or asset.
308
+ */
309
+ interface PaymentRequirements {
310
+ scheme: string;
311
+ network: string;
312
+ maxAmountRequired: string;
313
+ resource: string;
314
+ description?: string;
315
+ mimeType?: string;
316
+ payTo: string;
317
+ maxTimeoutSeconds?: number;
318
+ asset: string;
319
+ extra?: Record<string, unknown>;
320
+ }
321
+ /** Which settlement protocol a challenge speaks. */
322
+ type PaymentProtocol = 'x402' | 'mpp';
323
+ /** x402: base64 JSON in a `PAYMENT-REQUIRED` header. */
324
+ interface X402Challenge {
325
+ protocol: 'x402';
326
+ /** Accepted payment methods, in preference order. At least one. */
327
+ accepts: readonly PaymentRequirements[];
328
+ /** Protocol version. Defaults to 1. */
329
+ x402Version?: number;
330
+ }
331
+ /**
332
+ * MPP: an RFC 9110 `WWW-Authenticate: Payment` challenge.
333
+ *
334
+ * Field values are yours. `request` carries the encoded challenge payload your
335
+ * MPP provider generates — the library does not construct or price it.
336
+ */
337
+ interface MppChallenge {
338
+ protocol: 'mpp';
339
+ /** Challenge identifier. */
340
+ id: string;
341
+ /** Authentication realm. */
342
+ realm: string;
343
+ /** Payment method, e.g. `'tempo'`. */
344
+ method: string;
345
+ /** Transaction intent, e.g. `'charge'`. */
346
+ intent?: string;
347
+ /** Encoded challenge data from your provider. */
348
+ request?: string;
349
+ }
350
+ type PaymentChallenge = X402Challenge | MppChallenge;
351
+ interface PaymentChallengeOptions {
352
+ /**
353
+ * Challenges to advertise. Supplying both an x402 and an MPP challenge is
354
+ * valid and usually correct: they use non-colliding headers, so one 402 can
355
+ * offer both and the agent takes whichever it speaks.
356
+ */
357
+ challenges: readonly PaymentChallenge[];
358
+ /**
359
+ * `Content-Signal` to send with the challenge. Defaults to
360
+ * `search=yes, ai-input=yes, ai-train=paid` — the whole point being that
361
+ * training is available rather than forbidden.
362
+ */
363
+ contentSignal?: string;
364
+ /** Extra response headers. */
365
+ headers?: Record<string, string>;
366
+ /** Human-readable body. Agents read the header; people read logs. */
367
+ body?: string;
368
+ }
369
+ /**
370
+ * Build a 402 challenge.
371
+ *
372
+ * @example
373
+ * ```ts
374
+ * const decision = agentPolicy(req, { onTraining: 'charge' })
375
+ * if (decision.action === 'charge') {
376
+ * return paymentRequired({
377
+ * challenges: [
378
+ * {
379
+ * protocol: 'x402',
380
+ * accepts: [{
381
+ * scheme: 'exact',
382
+ * network: 'base',
383
+ * maxAmountRequired: '1000', // your price, your units
384
+ * resource: req.url,
385
+ * payTo: process.env.WALLET!,
386
+ * asset: process.env.USDC!
387
+ * }]
388
+ * },
389
+ * { protocol: 'mpp', id: challengeId, realm: 'example.com', method: 'tempo', intent: 'charge' }
390
+ * ]
391
+ * })
392
+ * }
393
+ * ```
394
+ */
395
+ declare function paymentRequired(opts: PaymentChallengeOptions): Response;
396
+ /** A payment credential the client sent back, and which protocol it speaks. */
397
+ interface SubmittedPayment {
398
+ protocol: PaymentProtocol;
399
+ /** Raw header value, for handing to a facilitator. */
400
+ value: string;
401
+ }
402
+ /**
403
+ * Read the client's payment credential, whichever protocol it used.
404
+ *
405
+ * x402 sends `PAYMENT-SIGNATURE`; MPP sends `Authorization: Payment …`. The
406
+ * `Payment` scheme check matters — a site behind normal auth will also have a
407
+ * Bearer or Basic `Authorization` header, and mistaking that for a payment
408
+ * would be a security-relevant confusion.
409
+ */
410
+ declare function paymentPayload(req: Request): SubmittedPayment | null;
411
+ /**
412
+ * True when the client attached a payment credential — i.e. this is the retry
413
+ * after a 402, not a fresh unpaid request.
414
+ *
415
+ * Presence is not proof. Hand the value to your facilitator to verify and
416
+ * settle; only then serve the resource.
417
+ */
418
+ declare function hasPaymentPayload(req: Request): boolean;
419
+ /**
420
+ * Attach a facilitator's settlement result to a successful response.
421
+ *
422
+ * x402 defines `PAYMENT-RESPONSE` for this. MPP's public spec did not pin a
423
+ * settlement-confirmation header at the time of writing, so pass `header` to
424
+ * name whatever your provider expects rather than have the library guess.
425
+ */
426
+ declare function withSettlement(res: Response, settlement: unknown, opts?: {
427
+ header?: string;
428
+ }): Response;
429
+ /**
430
+ * Convenience: turn an {@link AgentDecision} straight into a response, or
431
+ * `null` when the request should simply be served.
432
+ *
433
+ * Returns 403 for `'block'`, a 402 challenge for `'charge'`, and `null` for
434
+ * `'allow'` and `'meter'` — metering is an accounting concern, not a gate, so
435
+ * the request still gets served while `trackVisit` records it.
436
+ */
437
+ declare function respondToDecision(decision: AgentDecision, opts: PaymentChallengeOptions): Response | null;
438
+
439
+ /**
440
+ * The paid-access gate: policy decides *whether* to charge, a gateway decides
441
+ * *how*. **EXPERIMENTAL** — see `payments.ts`. The classification and policy
442
+ * layers underneath are stable; the payment surface is not.
443
+ *
444
+ * The split matters. We own classification — telling a training crawl from a
445
+ * retrieval fetch, which is the part nobody else does and the part that makes
446
+ * charging sane. Settlement is somebody else's job: Stripe's MPP SDK, an x402
447
+ * facilitator, whatever comes next. A library that held money would inherit PCI
448
+ * scope and stop being something you drop into middleware.
449
+ *
450
+ * So gateways are injected, exactly like analytics adapters, and this module
451
+ * takes no dependency on Stripe or any chain.
452
+ */
453
+
454
+ /**
455
+ * Outcome of handing a request to a payment gateway.
456
+ *
457
+ * - `challenge` — respond with this. The client has not paid.
458
+ * - `paid` — settled; serve the resource. `receipt` decorates the response with
459
+ * whatever proof the protocol expects.
460
+ */
461
+ type GatewayResult = {
462
+ status: 'challenge';
463
+ response: Response;
464
+ } | {
465
+ status: 'paid';
466
+ receipt?: (res: Response) => Response;
467
+ };
468
+ interface PaymentGateway {
469
+ handle(req: Request): Promise<GatewayResult>;
470
+ }
471
+ /**
472
+ * Wrap Stripe's MPP SDK.
473
+ *
474
+ * `Mppx.compose(...)` returns a handler that either yields a 402 with a
475
+ * `.challenge` response, or a settled result with `.withReceipt(res)`. This
476
+ * adapts that shape without importing it — pass the composed handler in.
477
+ *
478
+ * @example
479
+ * ```ts
480
+ * const mppx = Mppx.create({ methods: [...], secretKey })
481
+ * const handler = Mppx.compose(
482
+ * mppx.tempo.charge({ amount: '0.01', recipient }),
483
+ * mppx.stripe.charge({ amount: '0.50', currency: 'usd' })
484
+ * )
485
+ * const gateway = mppxGateway(handler)
486
+ * ```
487
+ */
488
+ declare function mppxGateway(handler: (req: Request) => Promise<MppxResponse> | MppxResponse): PaymentGateway;
489
+ /** The subset of Stripe's MPP response we rely on. Structural, not imported. */
490
+ interface MppxResponse {
491
+ status: number;
492
+ challenge: Response;
493
+ withReceipt?: (res: Response) => Response;
494
+ }
495
+ interface X402GatewayOptions extends PaymentChallengeOptions {
496
+ /**
497
+ * Verify and settle a `PAYMENT-SIGNATURE` payload with your facilitator.
498
+ * Resolve truthy to serve the resource, falsy to re-challenge.
499
+ */
500
+ settle: (payload: string, req: Request) => Promise<boolean> | boolean;
501
+ /** Attach the facilitator's settlement result to the served response. */
502
+ receipt?: (res: Response) => Response;
503
+ }
504
+ /**
505
+ * Gateway using this library's own challenge builder plus a facilitator you
506
+ * supply. For x402, or for MPP if you are not using Stripe's SDK.
507
+ */
508
+ declare function x402Gateway(opts: X402GatewayOptions): PaymentGateway;
509
+ /** One unit of billable agent traffic. */
510
+ interface MeterRecord {
511
+ decision: AgentDecision;
512
+ /** Units consumed. One request is one unit unless you price by bytes or tokens. */
513
+ units: number;
514
+ path: string;
515
+ method: string;
516
+ }
517
+ /**
518
+ * Where billable usage goes.
519
+ *
520
+ * Metering is the model to ship first: it needs no crawler cooperation, works
521
+ * today, and produces the number you would negotiate a licence with. Charging
522
+ * per request is what the protocols define but not what a training sweep can
523
+ * actually do — no crawler in the wild retries a 402.
524
+ */
525
+ interface Meter {
526
+ record(entry: MeterRecord): Promise<void> | void;
527
+ }
528
+ interface PaymentGateOptions extends Omit<AgentPolicyOptions, 'verify'> {
529
+ gateway: PaymentGateway;
530
+ /**
531
+ * Sink for billable traffic. Called for every `'meter'` decision — serve the
532
+ * request, count it, bill out of band.
533
+ *
534
+ * Errors are swallowed: a metering failure must not turn into a failed
535
+ * response, for the same reason analytics failures do not.
536
+ */
537
+ meter?: Meter;
538
+ /**
539
+ * Identity verifier, sync or async. Unlike {@link agentPolicy}'s option this
540
+ * accepts a promise, because `paymentGate` is already async and can await it.
541
+ * That matters: `combinedVerifier()` and `webBotAuthVerifier()` are async by
542
+ * necessity — Web Bot Auth fetches the signer's key directory — so without
543
+ * this they could not be used with policy or payments at all.
544
+ */
545
+ verify?: (req: Request) => BotVerificationLike | Promise<BotVerificationLike>;
546
+ /**
547
+ * Called for every decision, paid or not — wire it to your metering so
548
+ * `'meter'` traffic is actually counted rather than merely allowed.
549
+ */
550
+ onDecision?: (decision: AgentDecision) => void;
551
+ }
552
+ /**
553
+ * Full gate: classify, decide, and either let the request through or return the
554
+ * response it should get instead.
555
+ *
556
+ * Returns `null` when the request should be served normally. That covers
557
+ * `'allow'`, `'meter'` (accounting, not a gate) and any request that has already
558
+ * paid — in which case `receipt` is handed back so you can decorate the response
559
+ * you were going to send anyway.
560
+ *
561
+ * @example
562
+ * ```ts
563
+ * const gate = await paymentGate(req, {
564
+ * onTraining: 'charge',
565
+ * verify: combinedVerifier(),
566
+ * gateway: mppxGateway(handler),
567
+ * onDecision: (d) => void trackVisit(req, { analytics, properties: { action: d.action } })
568
+ * })
569
+ * if (gate.response) return gate.response
570
+ * return gate.decorate(await serve(req))
571
+ * ```
572
+ */
573
+ declare function paymentGate(req: Request, opts: PaymentGateOptions): Promise<{
574
+ decision: AgentDecision;
575
+ /** Respond with this instead of serving, when set. */
576
+ response: Response | null;
577
+ /** Wrap the response you were going to send. Identity when nothing to add. */
578
+ decorate: (res: Response) => Response;
579
+ }>;
580
+
581
+ /**
582
+ * Quota and entitlements — the model that actually works for training crawls.
583
+ * **EXPERIMENTAL**, like the rest of the payment surface.
584
+ *
585
+ * Per-request 402 is what x402 and MPP define, and it is the wrong shape for a
586
+ * training sweep. On one production site training traffic is ~199,000 requests a
587
+ * month. Charging each one means three times the traffic (402, pay, retry),
588
+ * 199,000 settlements whose per-transaction cost exceeds any sane per-page
589
+ * price, and — decisively — no crawler in the wild implements the retry, so a
590
+ * per-request 402 is just blocking with extra steps.
591
+ *
592
+ * Two workable shapes instead, both supported here:
593
+ *
594
+ * METER Serve the request, count it, bill out of band. Needs no crawler
595
+ * cooperation and works today. This is the one to ship.
596
+ *
597
+ * ENTITLEMENT Challenge once with a bulk offer, take payment, issue a
598
+ * credential. Every later request presents it and is served
599
+ * directly, decrementing quota. One settlement per licence rather
600
+ * than per page.
601
+ *
602
+ * MPP's reusable `Authorization: Payment` credential fits entitlements better
603
+ * than x402's per-resource signature, which proves payment for one URL.
604
+ */
605
+
606
+ /** What a buyer holds after paying. */
607
+ interface Entitlement {
608
+ /** Opaque licence id, for your own accounting. */
609
+ id: string;
610
+ /**
611
+ * Units left. Omit for an unmetered licence — `consume` is still called, so
612
+ * you can count without capping.
613
+ */
614
+ remaining?: number;
615
+ /** Expiry as epoch seconds. Omit for no expiry. */
616
+ expiresAt?: number;
617
+ }
618
+ /**
619
+ * Where entitlements live. A KV namespace, Redis, your database — anything
620
+ * reachable from the edge. The library deliberately ships no storage: quota
621
+ * state is yours, and so is the money it represents.
622
+ */
623
+ interface EntitlementStore {
624
+ /** Resolve the credential a client presented. Return null to challenge. */
625
+ lookup(credential: string, req: Request): Promise<Entitlement | null> | Entitlement | null;
626
+ /**
627
+ * Record consumption after a request is admitted. Called for every served
628
+ * request, including unmetered licences, so this doubles as your meter.
629
+ */
630
+ consume?(entitlement: Entitlement, req: Request): Promise<void> | void;
631
+ }
632
+ /**
633
+ * What is for sale. Folded into the challenge so an agent sees a bulk product
634
+ * rather than a price for the single page it happened to ask for.
635
+ */
636
+ interface BulkOffer {
637
+ /** e.g. 1_000_000 */
638
+ units: number;
639
+ /** e.g. `'pages'` */
640
+ unit: string;
641
+ /** Licence lifetime in seconds. */
642
+ validForSeconds: number;
643
+ /** Total price, in whatever units your challenge already uses. */
644
+ price: string;
645
+ /** Summary surfaced to the agent. */
646
+ description?: string;
647
+ }
648
+ interface EntitlementGatewayOptions extends PaymentChallengeOptions {
649
+ store: EntitlementStore;
650
+ /** The bulk product the 402 advertises. */
651
+ offer: BulkOffer;
652
+ /**
653
+ * Emit `x-quota-remaining` on served responses so a paying crawler can see
654
+ * its balance and slow down before running out.
655
+ *
656
+ * Off by default: this header is **not** part of x402 or MPP. It is a
657
+ * convenience, and a crawler that does not know it will ignore it.
658
+ */
659
+ exposeRemaining?: boolean;
660
+ }
661
+ /**
662
+ * Gateway that honours a bulk licence instead of charging per request.
663
+ *
664
+ * A request carrying a valid credential is served and its quota decremented —
665
+ * no challenge, no round-trip. A request without one gets a single 402
666
+ * advertising the bulk offer.
667
+ *
668
+ * @example
669
+ * ```ts
670
+ * const gateway = entitlementGateway({
671
+ * store: myKvStore,
672
+ * offer: { units: 1_000_000, unit: 'pages', validForSeconds: 2_592_000, price: '$400' },
673
+ * challenges: [{ protocol: 'mpp', id, realm: 'example.com', method: 'tempo' }]
674
+ * })
675
+ * ```
676
+ */
677
+ declare function entitlementGateway(opts: EntitlementGatewayOptions): PaymentGateway;
678
+ /**
679
+ * In-memory store. For tests and local development only — an edge runtime
680
+ * gives each instance its own memory, so quota would neither be shared nor
681
+ * survive a deploy. Use KV, Redis, or your database in production.
682
+ */
683
+ declare function memoryEntitlementStore(seed?: Record<string, Entitlement>): EntitlementStore & {
684
+ entries(): Record<string, Entitlement>;
685
+ };
686
+
687
+ /**
688
+ * Recommend Vercel WAF rules from observed agent traffic.
689
+ *
690
+ * This generates *proposals*, never live changes. Every recommendation comes out
691
+ * with `action: 'log'`, because a firewall rule's blast radius is unpredictable
692
+ * until real traffic hits it and a bad `deny` takes out real users or your SEO.
693
+ * Vercel's own guidance is log → review → preview → production; the `eventual`
694
+ * field records where a rule is meant to end up, and `cli` emits the command for
695
+ * the *current* stage only.
696
+ *
697
+ * Two hard rules, both from measurement rather than taste:
698
+ *
699
+ * 1. Retrieval agents and search crawlers are never proposed for blocking.
700
+ * 60% of AI traffic on one production site is retrieval — a person asked a
701
+ * question and an assistant went to read the page. Blocking that is
702
+ * blocking your own distribution. The recommender emits a `bypass` rule to
703
+ * protect them *first*, so later rules cannot catch them.
704
+ *
705
+ * 2. Training crawlers get rate limits, not denials, by default. The point is
706
+ * to bound cost, not to disappear from corpora.
707
+ *
708
+ * Only abuse gets a denial: an identity that failed cryptographic or IP
709
+ * verification, or a single address behaving like a scraper.
710
+ */
711
+
712
+ /** A Vercel WAF condition. Mirrors the CLI's `--condition` JSON. */
713
+ interface FirewallCondition {
714
+ type: 'user_agent' | 'ip_address' | 'geo_as_number' | 'geo_country' | 'path' | 'method' | 'environment' | 'ja4_digest';
715
+ op: 'eq' | 'neq' | 'sub' | 'pre' | 'suf' | 're' | 'inc' | 'ninc' | 'gt' | 'gte';
716
+ value?: string | number | Array<string | number>;
717
+ key?: string;
718
+ neg?: boolean;
719
+ }
720
+ type FirewallAction = 'log' | 'deny' | 'challenge' | 'bypass' | 'rate_limit';
721
+ interface RateLimitSpec {
722
+ /** Seconds, 10–3600. */
723
+ window: number;
724
+ /** Max requests per window. */
725
+ requests: number;
726
+ /** What happens on breach. */
727
+ action: 'rate_limit' | 'deny' | 'challenge' | 'log';
728
+ keys: Array<'ip' | 'ja4'>;
729
+ }
730
+ interface FirewallRecommendation {
731
+ name: string;
732
+ /** Why this rule is proposed, in one sentence. */
733
+ rationale: string;
734
+ /** The measurement behind it. Never propose a rule without evidence. */
735
+ evidence: string;
736
+ /** OR of ANDs: outer array is groups, inner is conditions within a group. */
737
+ groups: FirewallCondition[][];
738
+ /** Always `'log'` or `'bypass'` — see the module note. */
739
+ action: FirewallAction;
740
+ /** Where this rule is intended to end up after review. */
741
+ eventual: FirewallAction;
742
+ rateLimit?: RateLimitSpec;
743
+ /** How likely this is to catch traffic you wanted. */
744
+ risk: 'low' | 'medium' | 'high';
745
+ /** What could go wrong, when it is not obvious. */
746
+ caveat?: string;
747
+ /** Ready-to-run CLI for the *current* stage. */
748
+ cli: string;
749
+ /** Equivalent `--json` payload. */
750
+ json: unknown;
751
+ }
752
+ /** One aggregated slice of observed traffic. */
753
+ interface TrafficObservation {
754
+ userAgent: string;
755
+ botName: string;
756
+ intent: AgentIntent;
757
+ requests: number;
758
+ ip?: string;
759
+ /** Autonomous system number, if you resolved one. */
760
+ asn?: number;
761
+ /** Distinct paths this slice touched — a scraper sweeps, a reader does not. */
762
+ distinctPaths?: number;
763
+ /** Verification verdict, if you ran one. */
764
+ verification?: 'verified' | 'spoofed' | 'unverifiable' | 'not-claimed';
765
+ country?: string;
766
+ }
767
+ interface RecommendOptions {
768
+ /**
769
+ * Requests-per-slice above which a single IP is considered abusive. Defaults
770
+ * to 10x the median across observations, floored at 500.
771
+ */
772
+ abuseThreshold?: number;
773
+ /** Rate-limit budget proposed for training crawlers. Defaults to 600/hour. */
774
+ trainingBudget?: {
775
+ window: number;
776
+ requests: number;
777
+ };
778
+ /** Skip the protective bypass rule. Rarely a good idea. */
779
+ omitProtectiveBypass?: boolean;
780
+ }
781
+ /**
782
+ * Turn observations into staged WAF proposals.
783
+ *
784
+ * @example
785
+ * ```ts
786
+ * const rules = recommendFirewallRules(observations)
787
+ * for (const r of rules) {
788
+ * console.log(`# ${r.name} — ${r.rationale}`)
789
+ * console.log(`# evidence: ${r.evidence}`)
790
+ * console.log(r.cli)
791
+ * }
792
+ * ```
793
+ */
794
+ declare function recommendFirewallRules(observations: readonly TrafficObservation[], opts?: RecommendOptions): FirewallRecommendation[];
795
+ /** Render recommendations as a runnable, commented shell script. */
796
+ declare function firewallScript(recommendations: readonly FirewallRecommendation[]): string;
797
+
249
798
  /**
250
799
  * Escape hatch for wiring a callback directly as an analytics adapter.
251
800
  * Useful when you want to log events, pipe them through your own SDK, or
@@ -258,4 +807,4 @@ declare function agentPolicy(req: Request, opts?: AgentPolicyOptions): AgentDeci
258
807
  */
259
808
  declare function customAnalytics(capture: (event: CaptureEvent) => Promise<void> | void): AnalyticsAdapter;
260
809
 
261
- export { AI_BOT_PATTERN, type AgentAction, type AgentClassification, type AgentDecision, type AgentIntent, type AgentKind, type AgentPolicyOptions, AnalyticsAdapter, BotVerificationLike, CaptureEvent, CaptureTransportError, HTTP_CLIENT_PATTERN, HashSecretError, type HeadlessDetection, TrackVisitOptions, agentIntent, agentPolicy, classifyAgent, classifyRequest, customAnalytics, detectHeadless, firstUserAgentProduct, hashId, isAiBot, isHttpClient, parseBotName, randomSecret, trackVisit };
810
+ export { AI_BOT_PATTERN, type AgentAction, type AgentClassification, type AgentDecision, type AgentIntent, type AgentKind, type AgentPolicyOptions, AnalyticsAdapter, BotVerificationLike, type BulkOffer, CaptureEvent, CaptureTransportError, type Entitlement, type EntitlementGatewayOptions, type EntitlementStore, type FirewallAction, type FirewallCondition, type FirewallRecommendation, type GatewayResult, HTTP_CLIENT_PATTERN, HashSecretError, type HeadlessDetection, type Meter, type MeterRecord, type MppChallenge, type MppxResponse, type PaymentChallenge, type PaymentChallengeOptions, type PaymentGateOptions, type PaymentGateway, type PaymentProtocol, type PaymentRequirements, type RateLimitSpec, type RecommendOptions, type SubmittedPayment, TrackVisitOptions, type TrafficObservation, type X402Challenge, type X402GatewayOptions, agentIntent, agentPolicy, classifyAgent, classifyRequest, customAnalytics, detectHeadless, entitlementGateway, firewallScript, firstUserAgentProduct, hasPaymentPayload, hashId, isAiBot, isHttpClient, memoryEntitlementStore, mppxGateway, parseBotName, paymentGate, paymentPayload, paymentRequired, randomSecret, recommendFirewallRules, respondToDecision, trackVisit, withSettlement, x402Gateway };