@waniwani/relay 0.3.0 → 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.
- package/README.md +50 -6
- package/dist/index.d.ts +21 -4
- package/dist/index.js +581 -163
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -17,14 +17,17 @@ The [setup screen](https://app.waniwani.ai/agent-traffic/setup) in Waniwani give
|
|
|
17
17
|
### `isAgent(request)`
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
|
-
function isAgent(
|
|
20
|
+
function isAgent(
|
|
21
|
+
request: Request | CloudFrontRequest,
|
|
22
|
+
options?: Pick<RelayOptions, "region" | "apiUrl" | "waitUntil" | "rules">,
|
|
23
|
+
): boolean;
|
|
21
24
|
```
|
|
22
25
|
|
|
23
|
-
Tells whether the request comes from an AI agent. It reads the headers and the client IP only
|
|
26
|
+
Tells whether the request comes from an AI agent. It reads the headers and the client IP only, never waits on the network and never throws. It returns `true` when any of these hold:
|
|
24
27
|
|
|
25
|
-
- The `User-Agent` header contains a token from the AI bot list, for example `ChatGPT-User`, `Claude-User`, `GPTBot` or `PerplexityBot`. The list
|
|
28
|
+
- The `User-Agent` header contains a token from the AI bot list, for example `ChatGPT-User`, `Claude-User`, `GPTBot` or `PerplexityBot`. The list holds the AI bots in the Waniwani bot catalog and the AI bots on [bots.fyi](https://bots.fyi): crawlers that collect training data, AI search bots, fetchers that act for a user and browsing agents. Classic search indexers such as `Googlebot` and `Bingbot`, browsers, `curl` and uptime monitors are not on it.
|
|
26
29
|
- The request has a `Signature-Agent` header ([Web Bot Auth](https://datatracker.ietf.org/wg/webbotauth/about/)).
|
|
27
|
-
- The client IP sits inside an IP range that an AI vendor publishes for its bots
|
|
30
|
+
- The client IP sits inside an IP range that an AI vendor publishes for its bots, for example the lists of OpenAI, Anthropic, Perplexity, Mistral, Apple, Common Crawl and DuckDuckGo (DuckAssistBot).
|
|
28
31
|
- The request has a `DPoP` header, or its `Authorization` header uses the `DPoP` scheme.
|
|
29
32
|
|
|
30
33
|
### `recordAgentVisit(request, options?)`
|
|
@@ -46,6 +49,7 @@ Posts one event for the request to Waniwani after the response ([`AgentVisitReco
|
|
|
46
49
|
| `apiUrl` | The host of `region` | Another Waniwani host, for local development |
|
|
47
50
|
| `key` | `process.env.WANIWANI_AGENT_TRAFFIC_KEY` | The key from the setup screen, `wwt_<token>.<org key>` |
|
|
48
51
|
| `waitUntil` | Found by itself on Vercel, none on other platforms | Keeps the post alive after the response |
|
|
52
|
+
| `rules` | `"remote"` | `"remote"` loads the newest bot list from Waniwani every 12 hours, `"bundled"` uses only the list that ships in the package |
|
|
49
53
|
| `omit` | `[]` | Header names, and the fields `matchKey`, `userAgent` and `referer`, to leave out of every event. An event without `matchKey` still counts, but Waniwani then joins it with the chat embed's events on site, user agent and path only, which is less exact. |
|
|
50
54
|
|
|
51
55
|
#### `AgentVisitRecord`
|
|
@@ -84,6 +88,46 @@ The `headers` field can hold only these request headers:
|
|
|
84
88
|
| AWS CloudFront (Lambda@Edge) | Yes, before the handler returns. Lambda freezes the function when the handler returns, so a post event request that is not awaited is lost. | 1 s, no retry |
|
|
85
89
|
| Other | No, when you pass `waitUntil` | 2 s, one retry after 500 ms |
|
|
86
90
|
|
|
91
|
+
### `serveAgentPage(request, options?)`
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
function serveAgentPage(
|
|
95
|
+
request: Request,
|
|
96
|
+
options?: Pick<RelayOptions, "region" | "apiUrl" | "key">,
|
|
97
|
+
): Promise<Response | null>;
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Serves the agent page of a request: a text-first copy of the page that Waniwani renders and stores. It returns that page when all of these hold, and `null` otherwise, so your middleware passes the request on to your site:
|
|
101
|
+
|
|
102
|
+
- A key is set.
|
|
103
|
+
- The method is `GET` or `HEAD`.
|
|
104
|
+
- Agent pages are on in Waniwani for the hostname of the request, and its path is in the list of agent page paths. An entry is an exact path such as `/pricing`, or a folder such as `/products/*`, which covers `/products` and every path under it. `*` covers every path.
|
|
105
|
+
- The `User-Agent` belongs to an AI agent that acts for a user, for example `ChatGPT-User` or `Claude-User`. Crawlers that collect training data get your site as usual.
|
|
106
|
+
- Waniwani sends the stored page in time. The rules and the deadlines come from your Waniwani settings.
|
|
107
|
+
|
|
108
|
+
The response carries the stored content type, `x-waniwani-agent-page: hit`, `cache-control: private, no-store` and `vary: user-agent, accept`. A `HEAD` request gets the same headers with no body. The settings load on the first call, with a 300 ms limit, and stay in memory for 60 seconds. After that the relay keeps serving the cached settings while it loads new ones in the background. The function never throws, and the promise never rejects. It takes a `Request` only, so it works on Vercel and Cloudflare Workers.
|
|
109
|
+
|
|
110
|
+
On Vercel, in `middleware.ts`:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { isAgent, recordAgentVisit, serveAgentPage } from "@waniwani/relay";
|
|
114
|
+
import { NextResponse, type NextRequest } from "next/server";
|
|
115
|
+
|
|
116
|
+
export async function middleware(request: NextRequest) {
|
|
117
|
+
if (isAgent(request)) {
|
|
118
|
+
recordAgentVisit(request);
|
|
119
|
+
return (await serveAgentPage(request)) ?? NextResponse.next();
|
|
120
|
+
}
|
|
121
|
+
return NextResponse.next();
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
In a Cloudflare Worker, pass the key and fall back to your origin:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
return (await serveAgentPage(request, { key: env.WANIWANI_AGENT_TRAFFIC_KEY })) ?? fetch(request);
|
|
129
|
+
```
|
|
130
|
+
|
|
87
131
|
## Dependencies
|
|
88
132
|
|
|
89
133
|
The package has no runtime dependencies. The IP range check uses `BlockList` from `node:net`.
|
|
@@ -94,10 +138,10 @@ The package has no runtime dependencies. The IP range check uses `BlockList` fro
|
|
|
94
138
|
bun install
|
|
95
139
|
bun run build # typecheck, lint and dist/index.js
|
|
96
140
|
bun run test # unit tests (run the build first, one test imports dist)
|
|
97
|
-
bun run rules #
|
|
141
|
+
bun run rules # writes the two src/logic/*.generated.json files from the app rules file
|
|
98
142
|
```
|
|
99
143
|
|
|
100
|
-
`
|
|
144
|
+
`bun run rules` reads `https://app.waniwani.ai/api/v2/agent-traffic/rules`, or the URL in `RULES_URL`, and writes `src/logic/agents.generated.json` and `src/logic/ip-ranges.generated.json`, the fallback list that ships in the package. Never edit them by hand. `npm version` runs it before every release and commits the two files with the new version. When the rules route does not answer, the release stops before the version changes.
|
|
101
145
|
|
|
102
146
|
## License
|
|
103
147
|
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,11 @@ declare const settings: {
|
|
|
20
20
|
requestTimeoutMs: number;
|
|
21
21
|
cloudFrontTimeoutMs: number;
|
|
22
22
|
retryDelayMs: number;
|
|
23
|
+
rulesRefreshMs: number;
|
|
24
|
+
rulesRetryMs: number;
|
|
25
|
+
siteRulesTimeoutMs: number;
|
|
26
|
+
siteRulesRefreshMs: number;
|
|
27
|
+
siteRulesRetryMs: number;
|
|
23
28
|
};
|
|
24
29
|
|
|
25
30
|
type Region = keyof typeof settings.apiUrls;
|
|
@@ -34,7 +39,11 @@ type RelayOptions = {
|
|
|
34
39
|
waitUntil?: (promise: Promise<unknown>) => void;
|
|
35
40
|
/** Header names, and the fields `matchKey`, `userAgent` or `referer`, to leave out of every event. */
|
|
36
41
|
omit?: Array<string>;
|
|
42
|
+
/** `"remote"` loads the newest AI bot list from WaniWani every 12 hours. `"bundled"` keeps the list shipped in this version. Defaults to `"remote"`. */
|
|
43
|
+
rules?: "remote" | "bundled";
|
|
37
44
|
};
|
|
45
|
+
type IsAgentOptions = Pick<RelayOptions, "region" | "apiUrl" | "waitUntil" | "rules">;
|
|
46
|
+
type ServeAgentPageOptions = Pick<RelayOptions, "region" | "apiUrl" | "key">;
|
|
38
47
|
type AgentVisitRecord = {
|
|
39
48
|
occurredAt: string;
|
|
40
49
|
method: string;
|
|
@@ -48,14 +57,22 @@ type AgentVisitRecord = {
|
|
|
48
57
|
relayVersion: string;
|
|
49
58
|
platform?: Platform;
|
|
50
59
|
};
|
|
51
|
-
/**
|
|
52
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Tells whether a request, or the `cf.request` of a Lambda@Edge event, comes from an AI agent. Reads headers only and never waits on the network.
|
|
62
|
+
* Every 12 hours it starts one background fetch of the newest AI bot list, unless `rules` is `"bundled"`.
|
|
63
|
+
*/
|
|
64
|
+
declare function isAgent(request: Request | CloudFrontRequest, options?: IsAgentOptions): boolean;
|
|
53
65
|
/**
|
|
54
66
|
* Posts one event for this request to WaniWani after the response.
|
|
55
67
|
* Sends nothing when no key is set. Never throws, never rejects and never touches the request.
|
|
56
68
|
* The promise settles when the post is done, so Lambda@Edge, which has no `waitUntil`, awaits it.
|
|
57
69
|
*/
|
|
58
70
|
declare function recordAgentVisit(request: Request | CloudFrontRequest, options?: RelayOptions): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* The stored agent page for this request, when its site lists the path and the user agent acts for a user. Otherwise null, and the caller serves the origin.
|
|
73
|
+
* Never throws and never rejects.
|
|
74
|
+
*/
|
|
75
|
+
declare function serveAgentPage(request: Request, options?: ServeAgentPageOptions): Promise<Response | null>;
|
|
59
76
|
|
|
60
|
-
export { isAgent, recordAgentVisit };
|
|
61
|
-
export type { AgentVisitRecord, CloudFrontRequest, Region, RelayOptions };
|
|
77
|
+
export { isAgent, recordAgentVisit, serveAgentPage };
|
|
78
|
+
export type { AgentVisitRecord, CloudFrontRequest, IsAgentOptions, Region, RelayOptions, ServeAgentPageOptions };
|