@traceten/ai-crawl 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +260 -0
- package/dist/adapters/cloudflare-pages.d.ts +42 -0
- package/dist/adapters/cloudflare-pages.d.ts.map +1 -0
- package/dist/adapters/cloudflare-pages.js +46 -0
- package/dist/adapters/cloudflare-pages.js.map +1 -0
- package/dist/adapters/cloudflare-workers.d.ts +41 -0
- package/dist/adapters/cloudflare-workers.d.ts.map +1 -0
- package/dist/adapters/cloudflare-workers.js +49 -0
- package/dist/adapters/cloudflare-workers.js.map +1 -0
- package/dist/adapters/express.d.ts +49 -0
- package/dist/adapters/express.d.ts.map +1 -0
- package/dist/adapters/express.js +91 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/adapters/hono.d.ts +48 -0
- package/dist/adapters/hono.d.ts.map +1 -0
- package/dist/adapters/hono.js +64 -0
- package/dist/adapters/hono.js.map +1 -0
- package/dist/adapters/next.d.ts +41 -0
- package/dist/adapters/next.d.ts.map +1 -0
- package/dist/adapters/next.js +70 -0
- package/dist/adapters/next.js.map +1 -0
- package/dist/config.d.ts +21 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +99 -0
- package/dist/config.js.map +1 -0
- package/dist/crawlers.d.ts +68 -0
- package/dist/crawlers.d.ts.map +1 -0
- package/dist/crawlers.js +248 -0
- package/dist/crawlers.js.map +1 -0
- package/dist/filter.d.ts +33 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +169 -0
- package/dist/filter.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/ip.d.ts +35 -0
- package/dist/ip.d.ts.map +1 -0
- package/dist/ip.js +109 -0
- package/dist/ip.js.map +1 -0
- package/dist/matcher.d.ts +44 -0
- package/dist/matcher.d.ts.map +1 -0
- package/dist/matcher.js +111 -0
- package/dist/matcher.js.map +1 -0
- package/dist/report.d.ts +43 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +116 -0
- package/dist/report.js.map +1 -0
- package/dist/track.d.ts +30 -0
- package/dist/track.d.ts.map +1 -0
- package/dist/track.js +96 -0
- package/dist/track.js.map +1 -0
- package/dist/types.d.ts +184 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/package.json +87 -0
- package/src/adapters/cloudflare-pages.ts +64 -0
- package/src/adapters/cloudflare-workers.ts +70 -0
- package/src/adapters/express.ts +113 -0
- package/src/adapters/hono.ts +89 -0
- package/src/adapters/next.ts +87 -0
- package/src/config.ts +127 -0
- package/src/crawlers.ts +269 -0
- package/src/filter.ts +178 -0
- package/src/index.ts +46 -0
- package/src/ip.ts +112 -0
- package/src/matcher.ts +119 -0
- package/src/report.ts +149 -0
- package/src/track.ts +117 -0
- package/src/types.ts +190 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types for @traceten/ai-crawl.
|
|
3
|
+
*
|
|
4
|
+
* The package is a PRE-FILTER only: it decides "is this plausibly an AI
|
|
5
|
+
* crawler worth reporting" locally, and sends a minimal payload. Provider
|
|
6
|
+
* classification, category, verification and confidence are all decided
|
|
7
|
+
* server-side, so the crawler list can change without customers upgrading.
|
|
8
|
+
* The local token list is a cost filter, not the source of truth.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The four-category taxonomy Traceten uses server-side. Used locally ONLY to
|
|
13
|
+
* apply the `disable*` opt-outs — the authoritative category is assigned
|
|
14
|
+
* server-side.
|
|
15
|
+
*/
|
|
16
|
+
export type CrawlerCategory = "answer_fetch" | "search_index" | "training" | "ai_crawler";
|
|
17
|
+
|
|
18
|
+
/** Result of the local two-tier user-agent match. */
|
|
19
|
+
export interface CrawlerMatch {
|
|
20
|
+
/** Exact agent token that matched (tier 1), or `null` for a tier-2 provider-alias match. */
|
|
21
|
+
readonly agent: string | null;
|
|
22
|
+
/** Coarse provider slug (e.g. `"openai"`). */
|
|
23
|
+
readonly provider: string;
|
|
24
|
+
/** Local category guess — used only for the `disable*` opt-outs. */
|
|
25
|
+
readonly category: CrawlerCategory;
|
|
26
|
+
/** `"exact"` = tier-1 agent token; `"provider"` = tier-2 coarse alias. */
|
|
27
|
+
readonly tier: "exact" | "provider";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** User-supplied configuration. Validate with {@link import("./config.js").defineAiCrawlConfig}. */
|
|
31
|
+
export interface AiCrawlConfig {
|
|
32
|
+
/**
|
|
33
|
+
* The site's public site key — the `ttid_…` value shown on the dashboard's
|
|
34
|
+
* install page (`data-site`). Case-sensitive. Sent as `site_id` on the
|
|
35
|
+
* wire. Required.
|
|
36
|
+
*/
|
|
37
|
+
siteId: string;
|
|
38
|
+
/**
|
|
39
|
+
* Server-side auth token (`tt_bot_...`). REQUIRED — `POST /v1/ai-crawls`
|
|
40
|
+
* rejects unauthenticated reports. Unlike the browser snippet, a
|
|
41
|
+
* server-side package can hold a real secret; requiring it removes the
|
|
42
|
+
* cost-attack and data-poisoning surface. Missing/malformed tokens fail
|
|
43
|
+
* loudly at construction rather than silently sending requests that 401.
|
|
44
|
+
*/
|
|
45
|
+
authToken: string;
|
|
46
|
+
/** Ingestion endpoint. Default: `https://ingest.traceten.com/v1/ai-crawls`. */
|
|
47
|
+
endpoint?: string;
|
|
48
|
+
/** HTTP methods to consider. Default `["GET", "HEAD"]`. Replaces the default. */
|
|
49
|
+
allowedMethods?: readonly string[];
|
|
50
|
+
/** Extra path prefixes to deny, ADDED to the built-in list (never replaces it). */
|
|
51
|
+
extraDenyPathPrefixes?: readonly string[];
|
|
52
|
+
/** Extra file extensions to deny (with or without leading dot), ADDED to the built-in list. */
|
|
53
|
+
extraDenyExtensions?: readonly string[];
|
|
54
|
+
/** Skip reporting `answer_fetch` crawlers (ChatGPT-User, Claude-User, …). */
|
|
55
|
+
disableAnswerFetch?: boolean;
|
|
56
|
+
/** Skip reporting `search_index` crawlers (OAI-SearchBot, PerplexityBot, …). */
|
|
57
|
+
disableSearchCrawlers?: boolean;
|
|
58
|
+
/** Skip reporting `training` crawlers (GPTBot, ClaudeBot, CCBot, …). */
|
|
59
|
+
disableTrainingCrawlers?: boolean;
|
|
60
|
+
/** Skip reporting `ai_crawler` (uncategorised) crawlers and tier-2 provider-alias matches. */
|
|
61
|
+
disableOtherCrawlers?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Trust `x-forwarded-for` when resolving the crawler IP. Default `false`:
|
|
64
|
+
* XFF is attacker-controlled unless a proxy you operate strips/appends it,
|
|
65
|
+
* so it is never trusted blindly. Set `true` only when your app sits behind
|
|
66
|
+
* proxies you control, and set {@link proxyDepth} to how many of them
|
|
67
|
+
* append to XFF.
|
|
68
|
+
*/
|
|
69
|
+
trustProxy?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Trust the `cf-connecting-ip` header. Default `false`: on an origin NOT
|
|
72
|
+
* behind Cloudflare, any client can forge this header, and a forged
|
|
73
|
+
* vendor-range IP paired with a vendor UA is exactly the spoof the
|
|
74
|
+
* server-side verification exists to catch. The two Cloudflare adapters
|
|
75
|
+
* enable it automatically (the platform strips and rewrites the header
|
|
76
|
+
* there). Set it manually only when your origin genuinely sits behind
|
|
77
|
+
* Cloudflare (e.g. Express behind orange-cloud DNS).
|
|
78
|
+
*/
|
|
79
|
+
trustCfConnectingIp?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Number of trusted reverse proxies that append to `x-forwarded-for`.
|
|
82
|
+
* Default `1`. With N trusted proxies the crawler IP is the Nth entry from
|
|
83
|
+
* the right (rightmost-untrusted-aware selection). Only used when
|
|
84
|
+
* {@link trustProxy} is `true`.
|
|
85
|
+
*/
|
|
86
|
+
proxyDepth?: number;
|
|
87
|
+
/**
|
|
88
|
+
* Origin (e.g. `"https://example.com"`) used to rebuild the reported URL
|
|
89
|
+
* when the runtime sees an internal one (containers / reverse proxies that
|
|
90
|
+
* construct `request.url` from an internal host).
|
|
91
|
+
*/
|
|
92
|
+
publicOrigin?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Called when a report does not reach the endpoint: a non-2xx response, or
|
|
95
|
+
* a network/timeout failure.
|
|
96
|
+
*
|
|
97
|
+
* Delivery stays silent by contract. This never throws into your request
|
|
98
|
+
* path, nothing is retried, and the response is not surfaced any other way.
|
|
99
|
+
* Without it a rejected report is indistinguishable from a delivered one, so
|
|
100
|
+
* a misconfigured token or blocked egress reads as "no crawlers visited"
|
|
101
|
+
* indefinitely. Wire it to your logger in staging at minimum.
|
|
102
|
+
*
|
|
103
|
+
* Your callback is itself wrapped in try/catch. Throwing from it cannot
|
|
104
|
+
* break the host response.
|
|
105
|
+
*/
|
|
106
|
+
onError?: (error: AiCrawlDeliveryError) => void;
|
|
107
|
+
/** Injectable fetch, for tests. Defaults to the global `fetch`. */
|
|
108
|
+
fetch?: typeof fetch;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Why a report failed to land. Passed to {@link AiCrawlConfig.onError}. */
|
|
112
|
+
export interface AiCrawlDeliveryError {
|
|
113
|
+
/**
|
|
114
|
+
* `"http"` when the endpoint answered with a non-2xx status.
|
|
115
|
+
* `"network"` when the request never completed (DNS, TLS, timeout, abort).
|
|
116
|
+
*/
|
|
117
|
+
kind: "http" | "network";
|
|
118
|
+
/** Status when `kind` is `"http"`. A 401/403 means the token or the origin. */
|
|
119
|
+
status?: number;
|
|
120
|
+
/** The thrown value when `kind` is `"network"`. */
|
|
121
|
+
cause?: unknown;
|
|
122
|
+
/** The endpoint the report was addressed to. */
|
|
123
|
+
endpoint: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Config after validation + defaulting. Produced by `defineAiCrawlConfig`. */
|
|
127
|
+
export interface ResolvedAiCrawlConfig {
|
|
128
|
+
readonly siteId: string;
|
|
129
|
+
readonly authToken: string;
|
|
130
|
+
readonly endpoint: string;
|
|
131
|
+
readonly allowedMethods: readonly string[];
|
|
132
|
+
readonly denyPathPrefixes: readonly string[];
|
|
133
|
+
readonly denyExtensions: readonly string[];
|
|
134
|
+
readonly disableAnswerFetch: boolean;
|
|
135
|
+
readonly disableSearchCrawlers: boolean;
|
|
136
|
+
readonly disableTrainingCrawlers: boolean;
|
|
137
|
+
readonly disableOtherCrawlers: boolean;
|
|
138
|
+
readonly trustProxy: boolean;
|
|
139
|
+
readonly trustCfConnectingIp: boolean;
|
|
140
|
+
readonly proxyDepth: number;
|
|
141
|
+
readonly publicOrigin: string | undefined;
|
|
142
|
+
readonly onError: ((error: AiCrawlDeliveryError) => void) | undefined;
|
|
143
|
+
readonly fetch: typeof fetch | undefined;
|
|
144
|
+
/** Brand so per-request paths can tell a validated config from a raw one. */
|
|
145
|
+
readonly __resolved: true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Wire payload for `POST /v1/ai-crawls`. snake_case on the wire, per
|
|
150
|
+
* Traceten's wire-format convention.
|
|
151
|
+
*
|
|
152
|
+
* `ip` is the CRAWLER's IP as observed by the customer's server — the only
|
|
153
|
+
* place it is observable, since the crawler's TCP connection terminates
|
|
154
|
+
* there. Traceten's edge verifies it against vendor-published ranges while
|
|
155
|
+
* it is in scope; retention is verdict-dependent and decided server-side.
|
|
156
|
+
* The field is omitted entirely when no trustworthy value is derivable —
|
|
157
|
+
* never guessed.
|
|
158
|
+
*
|
|
159
|
+
* ⚠️ `ip` is a CLAIM made by customer-controlled software, not network
|
|
160
|
+
* proof. The receiving service must verify it (CIDR / rDNS) before treating
|
|
161
|
+
* it as crawler infrastructure, and must never let an unverified claimed IP
|
|
162
|
+
* mint a verified/raw-retained row.
|
|
163
|
+
*/
|
|
164
|
+
export interface AiCrawlWirePayload {
|
|
165
|
+
site_id: string;
|
|
166
|
+
/** Full request URL (absolute where derivable). */
|
|
167
|
+
url: string;
|
|
168
|
+
method: string;
|
|
169
|
+
/** Response status where the adapter can observe it; omitted otherwise. */
|
|
170
|
+
status?: number;
|
|
171
|
+
user_agent: string;
|
|
172
|
+
/** Crawler IP. Omitted rather than wrong. */
|
|
173
|
+
ip?: string;
|
|
174
|
+
/** Epoch milliseconds at observation time. */
|
|
175
|
+
ts: number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Runtime-agnostic view of a request, produced by each adapter.
|
|
180
|
+
* Header names are looked up case-insensitively by the adapter.
|
|
181
|
+
*/
|
|
182
|
+
export interface RequestFacts {
|
|
183
|
+
method: string;
|
|
184
|
+
/** Absolute URL if the runtime provides one, else a path like `/docs/x`. */
|
|
185
|
+
url: string;
|
|
186
|
+
/** Case-insensitive header lookup; returns `null` when absent. */
|
|
187
|
+
header: (name: string) => string | null;
|
|
188
|
+
/** Socket remote address, where the runtime exposes one (Node servers). */
|
|
189
|
+
socketAddr?: string | undefined;
|
|
190
|
+
}
|