@profullstack/x402-gateway 0.10.0 → 0.10.2
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 +2 -2
- package/index.d.ts +2 -1
- package/package.json +1 -1
- package/src/index.js +23 -6
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ npm install @profullstack/x402-gateway
|
|
|
18
18
|
| --- | --- |
|
|
19
19
|
| A person, Googlebot, Bingbot, Applebot, OAI-SearchBot, Claude-SearchBot, PerplexityBot… | the site, untouched |
|
|
20
20
|
| GPTBot, ClaudeBot, CCBot, meta-externalagent, Bytespider, Applebot-Extended… | `402` with an x402 offer, or the HTML sales page if it asked for HTML |
|
|
21
|
-
| Anyone at `/crawl` | the sales page (HTML) or the offer (JSON), and the place to pay |
|
|
21
|
+
| Anyone at `/crawl` | the sales page (HTML, or always with `?format=html`) or the offer (JSON), and the place to pay. The 402 prose points at `/crawl?format=html`, so a client that sent no Accept header still reaches the explanation |
|
|
22
22
|
| A request with a valid pass | the site |
|
|
23
23
|
|
|
24
24
|
The sales page explains the price, how to pay with an x402 client, and how to pay with the CoinPay CLI:
|
|
@@ -95,7 +95,7 @@ export const GET = robotsRoute(gateway, { disallow: ['/login', '/api/'] });
|
|
|
95
95
|
| `aiIndex` | none | extra agents to charge and refuse in robots.txt. Since 0.10 the AI answer-engine indexers (`AI_INDEX_AGENTS`: meta-webindexer, YouBot, ExaSearchBot, AionBot) are welcome retrieval crawlers; pass `aiIndex: AI_INDEX_AGENTS` to charge them anyway |
|
|
96
96
|
| `verifiedRetrieval` | `VERIFIED_RETRIEVAL` | retrieval crawlers believed only from their operator's published addresses (OAI-SearchBot, ChatGPT-User, PerplexityBot, Perplexity-User); from elsewhere they are metered. Other retrieval and AI chat fetchers (Claude-User, Claude-SearchBot, Meta-ExternalFetcher, DuckAssistBot, MistralAI-User, the AI indexers) publish no addresses and are believed by name |
|
|
97
97
|
| `chargeImpostors` | `true` | a request wearing a `welcome` crawler's name (Googlebot, bingbot, Applebot, DuckDuckBot, Cloudflare-*) from an address outside that operator's published list is charged. Never judged without a readable address |
|
|
98
|
-
| `challenge` | off | `{ paths: ['/i/'], cookie?: 'pf_js' }`: every GET to these paths must run a one-line script (sets a per-address, per-day HMAC cookie and reloads) or gets a 402. Welcome crawlers (address-verified search crawlers, retrieval crawlers, AI chat fetchers) skip it; a client that did not ask for HTML gets the JSON offer instead of the script; sessions and passes never see it. For residential swarms that wear a current browser string and never run the page |
|
|
98
|
+
| `challenge` | off | `{ paths: ['/i/'], cookie?: 'pf_js', when?: (request, url) => boolean }`: every GET to these paths (that `when` accepts, if given) must run a one-line script (sets a per-address, per-day HMAC cookie and reloads) or gets a 402. Welcome crawlers (address-verified search crawlers, retrieval crawlers, AI chat fetchers) skip it; a client that did not ask for HTML gets the JSON offer instead of the script; sessions and passes never see it. For residential swarms that wear a current browser string and never run the page |
|
|
99
99
|
| `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 |
|
|
100
100
|
| `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 |
|
|
101
101
|
| `training`, `retrieval` | the lists in `./agents` | |
|
package/index.d.ts
CHANGED
|
@@ -77,7 +77,8 @@ export interface GatewayOptions {
|
|
|
77
77
|
/** Charge a request wearing a welcome search crawler's name from outside its published ranges. Default true. */
|
|
78
78
|
chargeImpostors?: boolean;
|
|
79
79
|
/** Require a one-line script (cookie + reload) on these paths before serving them. Off by default. */
|
|
80
|
-
|
|
80
|
+
/** Only GETs to `paths` that `when` accepts (default: all of them), e.g. `(req, url) => url.search.length > 1` for faceted URLs. */
|
|
81
|
+
challenge?: { paths: string[]; cookie?: string; when?: (request: Request, url: URL) => boolean } | false;
|
|
81
82
|
/** SEO resellers: charged, and refused in robots.txt. Default SCRAPER_AGENTS. */
|
|
82
83
|
scrapers?: string[];
|
|
83
84
|
/** Headless browsers that name themselves: charged. Default HEADLESS_AGENTS. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/x402-gateway",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
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/index.js
CHANGED
|
@@ -83,7 +83,7 @@ export { isPowerKey, keyFrom, PROFULLSTACK_KEYS, verifyKey } from '@profullstack
|
|
|
83
83
|
* @param {string[]} [options.aiIndex] extra agents to charge and refuse in robots.txt, e.g. AI_INDEX_AGENTS; default none (they are welcome retrieval crawlers since 0.10)
|
|
84
84
|
* @param {{token: string, cidrs: string[]}[]|false} [options.verifiedRetrieval] retrieval crawlers believed only from their published addresses; default VERIFIED_RETRIEVAL
|
|
85
85
|
* @param {boolean} [options.chargeImpostors=true] charge a request wearing a welcome search crawler's name from outside its published ranges
|
|
86
|
-
* @param {{paths: string[], cookie?: string}|false} [options.challenge]
|
|
86
|
+
* @param {{paths: string[], cookie?: string, when?: (request: Request, url: URL) => boolean}|false} [options.challenge] requests to these paths (and, with `when`, only those it accepts, e.g. faceted URLs) must run a one-line script (cookie + reload) first; off by default
|
|
87
87
|
* @param {{token: string, cidrs: string[]}[]|false} [options.welcome] search crawlers never metered when name and address agree; default WELCOME_CRAWLERS
|
|
88
88
|
* @param {(request: Request) => boolean} [options.exempt] requests never charged, e.g. ones carrying a signed-in cookie
|
|
89
89
|
* @param {string} [options.secret] pass signing secret; defaults to the CoinPay key
|
|
@@ -106,6 +106,13 @@ export function createGateway(options = {}) {
|
|
|
106
106
|
const money = (cents) => `${(cents / 100).toFixed(2)} ${o.currency}`;
|
|
107
107
|
const price = money(o.priceCents);
|
|
108
108
|
const buyUrl = `${o.siteUrl}${o.path}`;
|
|
109
|
+
/*
|
|
110
|
+
* Where the prose points. A bare GET of the sales page answers JSON, because
|
|
111
|
+
* the body is the only place an x402 client reads the offer from, so a
|
|
112
|
+
* pointer to it would loop for the reader who needs the explanation most.
|
|
113
|
+
* `?format=html` is the explainer whatever the Accept header says.
|
|
114
|
+
*/
|
|
115
|
+
const howUrl = `${buyUrl}?format=html`;
|
|
109
116
|
|
|
110
117
|
/**
|
|
111
118
|
* How many terms a request is asking to buy: `?days=N`, clamped to
|
|
@@ -374,7 +381,7 @@ export function createGateway(options = {}) {
|
|
|
374
381
|
);
|
|
375
382
|
}
|
|
376
383
|
|
|
377
|
-
if (wantsHtml(request.headers.get('accept'))) {
|
|
384
|
+
if (wantsHtml(request.headers.get('accept')) || new URL(request.url).searchParams.get('format') === 'html') {
|
|
378
385
|
return html(o.page(pageCtx(asked, usage, quota)), 402, rateHeaders);
|
|
379
386
|
}
|
|
380
387
|
|
|
@@ -401,7 +408,7 @@ export function createGateway(options = {}) {
|
|
|
401
408
|
);
|
|
402
409
|
}
|
|
403
410
|
|
|
404
|
-
return json(receipt(asked, { error: `Payment required for training crawlers. Read ${
|
|
411
|
+
return json(receipt(asked, { error: `Payment required for training crawlers. Read ${howUrl} for how.` }), 402);
|
|
405
412
|
}
|
|
406
413
|
|
|
407
414
|
/**
|
|
@@ -460,13 +467,19 @@ export function createGateway(options = {}) {
|
|
|
460
467
|
* that asked for HTML gets the one-line script; anything else gets the
|
|
461
468
|
* JSON offer, since it was never going to run a script.
|
|
462
469
|
*/
|
|
463
|
-
if (
|
|
470
|
+
if (
|
|
471
|
+
o.challenge &&
|
|
472
|
+
secret &&
|
|
473
|
+
meters(o.challenge, path) &&
|
|
474
|
+
(!o.challenge.when || o.challenge.when(request, new URL(request.url))) &&
|
|
475
|
+
(request.method === 'GET' || request.method === 'HEAD')
|
|
476
|
+
) {
|
|
464
477
|
const ip = clientIp(request);
|
|
465
478
|
if (ip && !(await passedChallenge(request, { secret, cookie: o.challenge.cookie, ip }))) {
|
|
466
479
|
if (!wantsHtml(request.headers.get('accept'))) {
|
|
467
480
|
return json(
|
|
468
481
|
receipt(1, {
|
|
469
|
-
error: `${path} is read in a browser (it sets a cookie with one line of script) or with a pass. Read ${
|
|
482
|
+
error: `${path} is read in a browser (it sets a cookie with one line of script) or with a pass. Read ${howUrl} for how.`,
|
|
470
483
|
}),
|
|
471
484
|
402,
|
|
472
485
|
);
|
|
@@ -622,7 +635,11 @@ function normalise(options) {
|
|
|
622
635
|
aiIndex,
|
|
623
636
|
challenge:
|
|
624
637
|
options.challenge && Array.isArray(options.challenge.paths) && options.challenge.paths.length
|
|
625
|
-
? {
|
|
638
|
+
? {
|
|
639
|
+
paths: options.challenge.paths,
|
|
640
|
+
cookie: options.challenge.cookie ?? 'pf_js',
|
|
641
|
+
when: typeof options.challenge.when === 'function' ? options.challenge.when : null,
|
|
642
|
+
}
|
|
626
643
|
: null,
|
|
627
644
|
welcome: options.welcome === false ? null : (options.welcome ?? WELCOME_CRAWLERS),
|
|
628
645
|
verifiedRetrieval: options.verifiedRetrieval === false ? null : (options.verifiedRetrieval ?? VERIFIED_RETRIEVAL),
|