@profullstack/x402-gateway 0.7.0 → 0.8.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 +5 -1
- package/index.d.ts +22 -0
- package/package.json +1 -1
- package/src/agents.js +47 -0
- package/src/edge.js +94 -0
- package/src/index.js +50 -8
package/README.md
CHANGED
|
@@ -89,7 +89,11 @@ export const GET = robotsRoute(gateway, { disallow: ['/login', '/api/'] });
|
|
|
89
89
|
| `header` | `x-crawl-pass` | where the pass goes; `Authorization: Bearer` works too |
|
|
90
90
|
| `path` | `/crawl` | the sales page |
|
|
91
91
|
| `openPaths` | `[]` | extra paths a refused crawler may read (`robots.txt`, the sales page, `security.txt` and `.well-known/` always are) |
|
|
92
|
-
| `isPaidAgent` | training
|
|
92
|
+
| `isPaidAgent` | training + scraper + headless lists | `(userAgent) => boolean` |
|
|
93
|
+
| `scrapers` | `SCRAPER_AGENTS` | SEO resellers (AhrefsBot, SemrushBot, MJ12bot, DataForSeoBot, ...): charged and refused in robots.txt |
|
|
94
|
+
| `headless` | `HEADLESS_AGENTS` | headless browsers that name themselves (Lightpanda, HeadlessChrome, PhantomJS): charged |
|
|
95
|
+
| `chargeStaleBrowsers` | `true` | charge a desktop Chrome/Edge/Firefox string about two years behind its auto-updates. Scraper rotation pools copy old strings; people's browsers update themselves. Phones, Firefox ESRs and the last version an old OS can run are never judged |
|
|
96
|
+
| `welcome` | `WELCOME_CRAWLERS` | Googlebot, bingbot and Applebot read unmetered when their address is in the operator's published range. A copied string from anywhere else is metered like everyone; `false` turns this off |
|
|
93
97
|
| `training`, `retrieval` | the lists in `./agents` | |
|
|
94
98
|
| `secret` | the CoinPay key | pass signing secret |
|
|
95
99
|
| `page` | built in | `(ctx) => html` |
|
package/index.d.ts
CHANGED
|
@@ -68,6 +68,14 @@ export interface GatewayOptions {
|
|
|
68
68
|
denyCidrs?: string[];
|
|
69
69
|
/** Charge a request that claims "Chrome/…" but lacks the Sec-Fetch-Mode header every Chromium sends. Default false. */
|
|
70
70
|
chargeSpoofedBrowsers?: boolean;
|
|
71
|
+
/** Charge a desktop browser string ~2 years behind its auto-updates (a scraper rotation pool). Default true. */
|
|
72
|
+
chargeStaleBrowsers?: boolean;
|
|
73
|
+
/** SEO resellers: charged, and refused in robots.txt. Default SCRAPER_AGENTS. */
|
|
74
|
+
scrapers?: string[];
|
|
75
|
+
/** Headless browsers that name themselves: charged. Default HEADLESS_AGENTS. */
|
|
76
|
+
headless?: string[];
|
|
77
|
+
/** Search crawlers never metered when their address matches their name. Default WELCOME_CRAWLERS; false disables. */
|
|
78
|
+
welcome?: WelcomeCrawler[] | false;
|
|
71
79
|
/** Requests never charged, e.g. ones carrying a signed-in cookie. */
|
|
72
80
|
exempt?: (request: Request) => boolean;
|
|
73
81
|
/** Pass signing secret. Defaults to the CoinPay key. */
|
|
@@ -191,6 +199,10 @@ export interface Gateway {
|
|
|
191
199
|
passFrom: (request: Request) => string | null;
|
|
192
200
|
/** Whether that token opens the site: a live day pass this gateway minted, or a valid, unrevoked Power Key. */
|
|
193
201
|
verifyPass: (token: string | null) => Promise<boolean>;
|
|
202
|
+
/** Whether the gateway's own rules charge this request (lists, spoofed or stale browser). */
|
|
203
|
+
isPaid: (request: Request) => boolean;
|
|
204
|
+
/** Whether this is a welcome search crawler whose address matches its name. */
|
|
205
|
+
isWelcome: (request: Request) => boolean;
|
|
194
206
|
/** The x402 offer this gateway would make, without answering a request. */
|
|
195
207
|
offer: (days?: number) => Offer;
|
|
196
208
|
/** The full 402 body around that offer: the offer plus the pass terms. */
|
|
@@ -201,6 +213,13 @@ export function createGateway(options: GatewayOptions): Gateway;
|
|
|
201
213
|
export function wantsHtml(accept?: string | null): boolean;
|
|
202
214
|
|
|
203
215
|
export const TRAINING_AGENTS: string[];
|
|
216
|
+
export const SCRAPER_AGENTS: string[];
|
|
217
|
+
export const HEADLESS_AGENTS: string[];
|
|
218
|
+
export interface WelcomeCrawler {
|
|
219
|
+
token: string;
|
|
220
|
+
cidrs: string[];
|
|
221
|
+
}
|
|
222
|
+
export const WELCOME_CRAWLERS: WelcomeCrawler[];
|
|
204
223
|
export const RETRIEVAL_AGENTS: string[];
|
|
205
224
|
export function isTrainingAgent(userAgent?: string | null, agents?: string[]): boolean;
|
|
206
225
|
|
|
@@ -213,6 +232,9 @@ export function compileCidrs(list?: string[]): Cidr[];
|
|
|
213
232
|
export function inCidrs(ip: string, compiled: Cidr[]): boolean;
|
|
214
233
|
export function clientIp(request: Request): string;
|
|
215
234
|
export function isSpoofedBrowser(request: Request): boolean;
|
|
235
|
+
export function isStaleBrowser(userAgent: string, now?: number, maxMajorsBehind?: number): boolean;
|
|
236
|
+
export function currentMajor(browser: 'chrome' | 'firefox', now?: number): number;
|
|
237
|
+
export function welcomeCrawler(request: Request, list: WelcomeCrawler[] | null | undefined): string | null;
|
|
216
238
|
export function renderPage(ctx: PageContext): string;
|
|
217
239
|
|
|
218
240
|
export function mintPass(args: { secret: string; ref: string | null; expiresAt: number; now?: number }): Promise<{ token: string; expiresAt: number; ref: string | null }>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/x402-gateway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Sell crawl access to AI training crawlers by the day over x402, settled by CoinPay. One middleware: 402 with an offer, a sales page with CLI instructions, signed passes, Profullstack Power Keys honoured as a pass, and a robots.txt that keeps search crawlers welcome.",
|
|
6
6
|
"keywords": [
|
package/src/agents.js
CHANGED
|
@@ -24,8 +24,35 @@ export const TRAINING_AGENTS = [
|
|
|
24
24
|
'FacebookBot',
|
|
25
25
|
'Bytespider',
|
|
26
26
|
'Applebot-Extended',
|
|
27
|
+
// Amazon documents Amazonbot's crawl as one that "may be used to train
|
|
28
|
+
// Amazon AI models", and it sends no reader back from any of it.
|
|
29
|
+
'Amazonbot',
|
|
27
30
|
];
|
|
28
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Crawlers that resell what they read and send nobody back: SEO backlink and
|
|
34
|
+
* keyword databases. They do obey robots.txt, so they are refused there as well
|
|
35
|
+
* as charged, which is the cheapest outcome of all: they stop asking.
|
|
36
|
+
*/
|
|
37
|
+
export const SCRAPER_AGENTS = [
|
|
38
|
+
'AhrefsBot',
|
|
39
|
+
'SemrushBot',
|
|
40
|
+
'MJ12bot',
|
|
41
|
+
'DataForSeoBot',
|
|
42
|
+
'DotBot',
|
|
43
|
+
'BLEXBot',
|
|
44
|
+
'serpstatbot',
|
|
45
|
+
'Barkrowler',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Headless browsers that say so. Nobody reads a page through one of these; a
|
|
50
|
+
* scrape does, and it runs the page's scripts while it is at it. Lightpanda was
|
|
51
|
+
* 151k requests from 63k addresses on one site in a day (2026-10-19). They do
|
|
52
|
+
* not read robots.txt, so they are charged, never named there.
|
|
53
|
+
*/
|
|
54
|
+
export const HEADLESS_AGENTS = ['Lightpanda', 'HeadlessChrome', 'PhantomJS'];
|
|
55
|
+
|
|
29
56
|
/**
|
|
30
57
|
* Retrieval crawlers, named in robots.txt so their operators can see they are
|
|
31
58
|
* welcome. Google-Extended is Google's training token but Google documents it
|
|
@@ -57,3 +84,23 @@ export function isTrainingAgent(userAgent = '', agents = TRAINING_AGENTS) {
|
|
|
57
84
|
if (!ua) return false;
|
|
58
85
|
return lower(agents).some((t) => ua.includes(t));
|
|
59
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Search crawlers that are welcome everywhere, with the address ranges their
|
|
90
|
+
* operators publish. A user agent alone is not enough to spare a caller from
|
|
91
|
+
* the free allowance -- "Googlebot" is the first string a scraper copies -- so
|
|
92
|
+
* a crawler is only spared when its address agrees with its name.
|
|
93
|
+
*
|
|
94
|
+
* Ranges are the stable blocks of each operator's published list
|
|
95
|
+
* (developers.google.com/static/search/apis/ipranges/googlebot.json,
|
|
96
|
+
* bing.com/toolbox/bingbot.json, Apple's 17.0.0.0/8); a crawl from outside them
|
|
97
|
+
* is simply metered like everyone else, which fails safe.
|
|
98
|
+
*/
|
|
99
|
+
export const WELCOME_CRAWLERS = [
|
|
100
|
+
{ token: 'Googlebot', cidrs: ['66.249.64.0/19', '192.178.4.0/22', '34.100.182.96/28'] },
|
|
101
|
+
{
|
|
102
|
+
token: 'bingbot',
|
|
103
|
+
cidrs: ['40.77.167.0/24', '157.55.39.0/24', '207.46.13.0/24', '52.167.144.0/24', '40.77.188.0/22', '20.15.133.160/27'],
|
|
104
|
+
},
|
|
105
|
+
{ token: 'Applebot', cidrs: ['17.0.0.0/8'] },
|
|
106
|
+
];
|
package/src/edge.js
CHANGED
|
@@ -111,3 +111,97 @@ export function isSpoofedBrowser(request) {
|
|
|
111
111
|
if (DECLARES_ITSELF.test(ua)) return false;
|
|
112
112
|
return !request.headers.has('sec-fetch-mode');
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
/* ---------------------------------------------------------- stale strings -- */
|
|
116
|
+
|
|
117
|
+
/*
|
|
118
|
+
* Where each browser's version stood on a known day, and how fast it moves:
|
|
119
|
+
* both ship a major every four weeks. Computing "current" from the date keeps
|
|
120
|
+
* the check from going stale itself, which a hard-coded number would within a
|
|
121
|
+
* year.
|
|
122
|
+
*/
|
|
123
|
+
const DAY = 86_400_000;
|
|
124
|
+
const CADENCE = 28 * DAY;
|
|
125
|
+
const CHROME_ANCHOR = { major: 120, at: Date.UTC(2023, 11, 5) };
|
|
126
|
+
const FIREFOX_ANCHOR = { major: 121, at: Date.UTC(2023, 11, 19) };
|
|
127
|
+
|
|
128
|
+
/** The major a browser that auto-updates would be on at `now`. */
|
|
129
|
+
export function currentMajor(browser, now = Date.now()) {
|
|
130
|
+
const a = browser === 'firefox' ? FIREFOX_ANCHOR : CHROME_ANCHOR;
|
|
131
|
+
return a.major + Math.max(0, Math.floor((now - a.at) / CADENCE));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/*
|
|
135
|
+
* The last version an old operating system can run, and Firefox's extended
|
|
136
|
+
* support releases. A person stuck on one of these is still a person, so these
|
|
137
|
+
* are never called stale, however old.
|
|
138
|
+
*/
|
|
139
|
+
const TERMINAL_CHROME = [
|
|
140
|
+
{ major: 109, os: /Windows NT 6\./ }, // Windows 7, 8, 8.1
|
|
141
|
+
{ major: 116, os: /Macintosh/ }, // macOS 10.13, 10.14
|
|
142
|
+
{ major: 128, os: /Macintosh/ }, // macOS 10.15
|
|
143
|
+
{ major: 138, os: /Macintosh/ }, // macOS 11
|
|
144
|
+
];
|
|
145
|
+
const FIREFOX_ESR = [115, 128, 140, 153];
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A desktop browser string far older than anything that auto-updates.
|
|
149
|
+
*
|
|
150
|
+
* Chrome, Edge and Firefox update themselves every four weeks, so a desktop
|
|
151
|
+
* "Chrome/120" two years after Chrome 120 shipped is a string copied into a
|
|
152
|
+
* scraper's rotation pool, not a browser. That is exactly what the swarms do:
|
|
153
|
+
* on 2026-10-19 one pool of ten strings (Chrome 118-120, Edge 119-120, Firefox
|
|
154
|
+
* 120-121) walked watchnews.now and r4ck.dev from thousands of residential
|
|
155
|
+
* addresses, running the page scripts as it went.
|
|
156
|
+
*
|
|
157
|
+
* Only desktop strings are judged. Phones and embedded webviews legitimately
|
|
158
|
+
* sit years behind, and a declared crawler is judged by the lists instead.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} ua
|
|
161
|
+
* @param {number} [now]
|
|
162
|
+
* @param {number} [maxMajorsBehind=24] about 22 months of releases
|
|
163
|
+
*/
|
|
164
|
+
export function isStaleBrowser(ua, now = Date.now(), maxMajorsBehind = 24) {
|
|
165
|
+
const s = String(ua ?? '');
|
|
166
|
+
if (!/Windows NT|Macintosh|X11; (?:Linux|Ubuntu|Fedora)/.test(s)) return false;
|
|
167
|
+
if (/Mobile|Android|iPhone|iPad|CrOS/.test(s)) return false;
|
|
168
|
+
if (DECLARES_ITSELF.test(s)) return false;
|
|
169
|
+
|
|
170
|
+
const ff = /\bFirefox\/(\d+)/.exec(s);
|
|
171
|
+
if (ff) {
|
|
172
|
+
const major = Number(ff[1]);
|
|
173
|
+
if (FIREFOX_ESR.includes(major)) return false;
|
|
174
|
+
return major < currentMajor('firefox', now) - maxMajorsBehind;
|
|
175
|
+
}
|
|
176
|
+
const chrome = /\bChrome\/(\d+)/.exec(s);
|
|
177
|
+
if (chrome) {
|
|
178
|
+
const major = Number(chrome[1]);
|
|
179
|
+
if (TERMINAL_CHROME.some((t) => t.major === major && t.os.test(s))) return false;
|
|
180
|
+
return major < currentMajor('chrome', now) - maxMajorsBehind;
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* -------------------------------------------------------- welcome crawlers -- */
|
|
186
|
+
|
|
187
|
+
const COMPILED = new WeakMap();
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The welcome crawler a request is, judged by name AND address, or null.
|
|
191
|
+
*
|
|
192
|
+
* @param {Request} request
|
|
193
|
+
* @param {{token: string, cidrs: string[]}[]} list e.g. WELCOME_CRAWLERS
|
|
194
|
+
*/
|
|
195
|
+
export function welcomeCrawler(request, list) {
|
|
196
|
+
if (!list?.length) return null;
|
|
197
|
+
const ua = (request.headers.get('user-agent') ?? '').toLowerCase();
|
|
198
|
+
if (!ua) return null;
|
|
199
|
+
const ip = clientIp(request);
|
|
200
|
+
for (const c of list) {
|
|
201
|
+
if (!ua.includes(c.token.toLowerCase())) continue;
|
|
202
|
+
let compiled = COMPILED.get(c);
|
|
203
|
+
if (!compiled) COMPILED.set(c, (compiled = compileCidrs(c.cidrs)));
|
|
204
|
+
if (inCidrs(ip, compiled)) return c.token;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HEADER as POWER_KEY_HEADER, isPowerKey, keyFrom, PROFULLSTACK_KEYS, SHOP_URL, verifyKey } from '@profullstack/keys';
|
|
2
|
-
import { isTrainingAgent, RETRIEVAL_AGENTS, TRAINING_AGENTS } from './agents.js';
|
|
3
|
-
import { clientIp, compileCidrs, inCidrs, isSpoofedBrowser } from './edge.js';
|
|
2
|
+
import { HEADLESS_AGENTS, isTrainingAgent, RETRIEVAL_AGENTS, SCRAPER_AGENTS, TRAINING_AGENTS, WELCOME_CRAWLERS } from './agents.js';
|
|
3
|
+
import { clientIp, compileCidrs, inCidrs, isSpoofedBrowser, isStaleBrowser, welcomeCrawler } from './edge.js';
|
|
4
4
|
import { memoryQuotaStore, meters, normaliseQuota, quotaHeaders, spend } from './quota.js';
|
|
5
5
|
import { renderPage } from './page.js';
|
|
6
6
|
import { mintPass, readPass } from './pass.js';
|
|
@@ -16,8 +16,8 @@ import {
|
|
|
16
16
|
verifyAndSettle,
|
|
17
17
|
} from './x402.js';
|
|
18
18
|
|
|
19
|
-
export { isTrainingAgent, RETRIEVAL_AGENTS, TRAINING_AGENTS } from './agents.js';
|
|
20
|
-
export { clientIp, compileCidrs, inCidrs, isSpoofedBrowser, parseCidr } from './edge.js';
|
|
19
|
+
export { HEADLESS_AGENTS, isTrainingAgent, RETRIEVAL_AGENTS, SCRAPER_AGENTS, TRAINING_AGENTS, WELCOME_CRAWLERS } from './agents.js';
|
|
20
|
+
export { clientIp, compileCidrs, currentMajor, inCidrs, isSpoofedBrowser, isStaleBrowser, parseCidr, welcomeCrawler } from './edge.js';
|
|
21
21
|
export { renderPage } from './page.js';
|
|
22
22
|
export { mintPass, readPass } from './pass.js';
|
|
23
23
|
export { robotsTxt } from './robots.js';
|
|
@@ -56,6 +56,10 @@ export { isPowerKey, keyFrom, PROFULLSTACK_KEYS, verifyKey } from '@profullstack
|
|
|
56
56
|
* @param {(ua: string) => boolean} [options.isPaidAgent]
|
|
57
57
|
* @param {string[]} [options.denyCidrs] IPv4 ranges answered 403 before anything else, e.g. a VPS fleet's provider
|
|
58
58
|
* @param {boolean} [options.chargeSpoofedBrowsers=false] charge a "Chrome/…" request that lacks the Sec-Fetch-Mode header every Chromium sends
|
|
59
|
+
* @param {boolean} [options.chargeStaleBrowsers=true] charge a desktop browser string two years behind its auto-updates (a rotation pool)
|
|
60
|
+
* @param {string[]} [options.scrapers] SEO resellers charged and refused in robots.txt; default SCRAPER_AGENTS
|
|
61
|
+
* @param {string[]} [options.headless] headless browsers charged; default HEADLESS_AGENTS
|
|
62
|
+
* @param {{token: string, cidrs: string[]}[]|false} [options.welcome] search crawlers never metered when name and address agree; default WELCOME_CRAWLERS
|
|
59
63
|
* @param {(request: Request) => boolean} [options.exempt] requests never charged, e.g. ones carrying a signed-in cookie
|
|
60
64
|
* @param {string} [options.secret] pass signing secret; defaults to the CoinPay key
|
|
61
65
|
* @param {Record<string,string>|false} [options.powerKeys] kid -> public key of the Power Keys honoured as a pass; defaults to Profullstack's, `false` switches it off
|
|
@@ -376,12 +380,22 @@ export function createGateway(options = {}) {
|
|
|
376
380
|
const token = passFrom(request);
|
|
377
381
|
if (await honours(token)) return null;
|
|
378
382
|
|
|
383
|
+
const ua = request.headers.get('user-agent') ?? '';
|
|
379
384
|
const pays =
|
|
380
|
-
o.isPaidAgent(
|
|
381
|
-
(o.chargeSpoofedBrowsers && isSpoofedBrowser(request))
|
|
385
|
+
o.isPaidAgent(ua) ||
|
|
386
|
+
(o.chargeSpoofedBrowsers && isSpoofedBrowser(request)) ||
|
|
387
|
+
(o.chargeStaleBrowsers && isStaleBrowser(ua));
|
|
382
388
|
if (pays && !isOpen(path)) return sell(request);
|
|
383
389
|
if (isOpen(path)) return null;
|
|
384
390
|
|
|
391
|
+
/*
|
|
392
|
+
* A search crawler that is who it says it is reads free and unmetered.
|
|
393
|
+
* Googlebot walking a large site from a handful of addresses outruns any
|
|
394
|
+
* per-address allowance, and answering it 402 is how a site leaves the
|
|
395
|
+
* index. Name and address must agree, so copying the string buys nothing.
|
|
396
|
+
*/
|
|
397
|
+
if (welcomeCrawler(request, o.welcome)) return null;
|
|
398
|
+
|
|
385
399
|
/*
|
|
386
400
|
* Everyone else gets the free allowance. Running out is answered with a
|
|
387
401
|
* price rather than a 429: the caller has just shown it wants more than
|
|
@@ -419,6 +433,21 @@ export function createGateway(options = {}) {
|
|
|
419
433
|
*/
|
|
420
434
|
passFrom,
|
|
421
435
|
verifyPass: honours,
|
|
436
|
+
/**
|
|
437
|
+
* Whether a request is a charged crawler by this gateway's rules (the
|
|
438
|
+
* lists, a spoofed or a stale browser string), and whether it is a welcome
|
|
439
|
+
* search crawler whose address matches its name. Exposed so a throttle
|
|
440
|
+
* that meters every route charges and spares exactly the same callers.
|
|
441
|
+
*/
|
|
442
|
+
isPaid: (request) => {
|
|
443
|
+
const ua = request.headers.get('user-agent') ?? '';
|
|
444
|
+
return (
|
|
445
|
+
o.isPaidAgent(ua) ||
|
|
446
|
+
(o.chargeSpoofedBrowsers && isSpoofedBrowser(request)) ||
|
|
447
|
+
(o.chargeStaleBrowsers && isStaleBrowser(ua))
|
|
448
|
+
);
|
|
449
|
+
},
|
|
450
|
+
isWelcome: (request) => Boolean(welcomeCrawler(request, o.welcome)),
|
|
422
451
|
/**
|
|
423
452
|
* The x402 offer this gateway would make, and the full receipt body around
|
|
424
453
|
* it, without answering a request.
|
|
@@ -435,7 +464,13 @@ export function createGateway(options = {}) {
|
|
|
435
464
|
receipt,
|
|
436
465
|
/** robots.txt with this gateway's lists and sales path. */
|
|
437
466
|
robotsTxt: (extra = {}) =>
|
|
438
|
-
robotsTxt({
|
|
467
|
+
robotsTxt({
|
|
468
|
+
siteUrl: o.siteUrl,
|
|
469
|
+
path: o.path,
|
|
470
|
+
training: [...o.training, ...o.scrapers],
|
|
471
|
+
retrieval: o.retrieval,
|
|
472
|
+
...extra,
|
|
473
|
+
}),
|
|
439
474
|
/** The sales page as HTML, for a site that mounts it on a route of its own. */
|
|
440
475
|
page: () => o.page(pageCtx()),
|
|
441
476
|
};
|
|
@@ -484,6 +519,9 @@ function normalise(options) {
|
|
|
484
519
|
const siteUrl = String(options.siteUrl ?? '').replace(/\/+$/, '');
|
|
485
520
|
if (!siteUrl) throw new Error('createGateway needs siteUrl');
|
|
486
521
|
const training = options.training ?? TRAINING_AGENTS;
|
|
522
|
+
const scrapers = options.scrapers ?? SCRAPER_AGENTS;
|
|
523
|
+
const headless = options.headless ?? HEADLESS_AGENTS;
|
|
524
|
+
const paid = [...training, ...scrapers, ...headless];
|
|
487
525
|
return {
|
|
488
526
|
siteUrl,
|
|
489
527
|
siteName: options.siteName || new URL(siteUrl).hostname,
|
|
@@ -501,10 +539,14 @@ function normalise(options) {
|
|
|
501
539
|
openPaths: options.openPaths ?? [],
|
|
502
540
|
denyCidrs: options.denyCidrs ?? [],
|
|
503
541
|
chargeSpoofedBrowsers: Boolean(options.chargeSpoofedBrowsers),
|
|
542
|
+
chargeStaleBrowsers: options.chargeStaleBrowsers !== false,
|
|
504
543
|
exempt: options.exempt ?? null,
|
|
505
544
|
training,
|
|
545
|
+
scrapers,
|
|
546
|
+
headless,
|
|
547
|
+
welcome: options.welcome === false ? null : (options.welcome ?? WELCOME_CRAWLERS),
|
|
506
548
|
retrieval: options.retrieval ?? RETRIEVAL_AGENTS,
|
|
507
|
-
isPaidAgent: options.isPaidAgent ?? ((ua) => isTrainingAgent(ua,
|
|
549
|
+
isPaidAgent: options.isPaidAgent ?? ((ua) => isTrainingAgent(ua, paid)),
|
|
508
550
|
secret: options.secret ?? '',
|
|
509
551
|
page: options.page ?? renderPage,
|
|
510
552
|
contact: options.contact ?? '',
|