@profullstack/x402-gateway 0.4.0 → 0.6.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/index.d.ts +24 -1
- package/package.json +41 -11
- package/src/index.js +46 -11
package/index.d.ts
CHANGED
|
@@ -149,9 +149,24 @@ export interface RobotsOptions {
|
|
|
149
149
|
comments?: string[];
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
/** What the caller spent against an allowance, as `spend()` reports it. */
|
|
153
|
+
export interface QuotaUsage {
|
|
154
|
+
count: number;
|
|
155
|
+
remaining: number;
|
|
156
|
+
resetSeconds: number;
|
|
157
|
+
overLimit: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The allowance a 402 should quote, when the caller kept the count itself. */
|
|
161
|
+
export interface SellContext {
|
|
162
|
+
usage?: QuotaUsage | null;
|
|
163
|
+
/** Defaults to the gateway's own `freeQuota`. */
|
|
164
|
+
quota?: { requests: number; windowSeconds: number } | null;
|
|
165
|
+
}
|
|
166
|
+
|
|
152
167
|
export interface Gateway {
|
|
153
168
|
handle: Handle;
|
|
154
|
-
sell: (request: Request) => Promise<Response>;
|
|
169
|
+
sell: (request: Request, context?: SellContext) => Promise<Response>;
|
|
155
170
|
enabled: boolean;
|
|
156
171
|
options: Required<Omit<GatewayOptions, 'onSale' | 'fetch' | 'page' | 'isPaidAgent'>> & {
|
|
157
172
|
onSale: GatewayOptions['onSale'] | null;
|
|
@@ -161,6 +176,14 @@ export interface Gateway {
|
|
|
161
176
|
};
|
|
162
177
|
robotsTxt: (extra?: RobotsOptions) => string;
|
|
163
178
|
page: () => string;
|
|
179
|
+
/** The pass this request presents, from the gateway's header or a bearer token. */
|
|
180
|
+
passFrom: (request: Request) => string | null;
|
|
181
|
+
/** Whether that token is a live pass this gateway minted. */
|
|
182
|
+
verifyPass: (token: string | null) => Promise<boolean>;
|
|
183
|
+
/** The x402 offer this gateway would make, without answering a request. */
|
|
184
|
+
offer: (days?: number) => Offer;
|
|
185
|
+
/** The full 402 body around that offer: the offer plus the pass terms. */
|
|
186
|
+
receipt: (days?: number, extra?: Record<string, unknown>) => Offer & Record<string, unknown>;
|
|
164
187
|
}
|
|
165
188
|
|
|
166
189
|
export function createGateway(options: GatewayOptions): Gateway;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/x402-gateway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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, and a robots.txt that keeps search crawlers welcome.",
|
|
6
6
|
"keywords": [
|
|
@@ -20,22 +20,52 @@
|
|
|
20
20
|
"middleware",
|
|
21
21
|
"paywall"
|
|
22
22
|
],
|
|
23
|
-
"repository": {
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/profullstack/x402-gateway.git"
|
|
26
|
+
},
|
|
24
27
|
"homepage": "https://github.com/profullstack/x402-gateway#readme",
|
|
25
|
-
"bugs": {
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/profullstack/x402-gateway/issues"
|
|
30
|
+
},
|
|
26
31
|
"license": "MIT",
|
|
27
32
|
"author": "Profullstack, LLC",
|
|
28
33
|
"exports": {
|
|
29
|
-
".": {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"./
|
|
34
|
-
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./index.d.ts",
|
|
36
|
+
"import": "./src/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./hono": {
|
|
39
|
+
"types": "./index.d.ts",
|
|
40
|
+
"import": "./src/hono.js"
|
|
41
|
+
},
|
|
42
|
+
"./next": {
|
|
43
|
+
"types": "./index.d.ts",
|
|
44
|
+
"import": "./src/next.js"
|
|
45
|
+
},
|
|
46
|
+
"./robots": {
|
|
47
|
+
"types": "./index.d.ts",
|
|
48
|
+
"import": "./src/robots.js"
|
|
49
|
+
},
|
|
50
|
+
"./agents": {
|
|
51
|
+
"types": "./index.d.ts",
|
|
52
|
+
"import": "./src/agents.js"
|
|
53
|
+
},
|
|
54
|
+
"./edge": {
|
|
55
|
+
"types": "./index.d.ts",
|
|
56
|
+
"import": "./src/edge.js"
|
|
57
|
+
}
|
|
35
58
|
},
|
|
36
59
|
"types": "./index.d.ts",
|
|
37
|
-
"files": [
|
|
38
|
-
|
|
60
|
+
"files": [
|
|
61
|
+
"src",
|
|
62
|
+
"index.d.ts",
|
|
63
|
+
"README.md",
|
|
64
|
+
"LICENSE"
|
|
65
|
+
],
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=20.11"
|
|
68
|
+
},
|
|
39
69
|
"sideEffects": false,
|
|
40
70
|
"scripts": {
|
|
41
71
|
"test": "node --test"
|
package/src/index.js
CHANGED
|
@@ -127,11 +127,11 @@ export function createGateway(options = {}) {
|
|
|
127
127
|
headers: { 'content-type': 'text/html; charset=utf-8', ...noStore, ...headers },
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
-
const pageCtx = (days = 1, usage = null) => ({
|
|
131
|
-
quota:
|
|
130
|
+
const pageCtx = (days = 1, usage = null, quota = o.freeQuota) => ({
|
|
131
|
+
quota: quota
|
|
132
132
|
? {
|
|
133
|
-
requests:
|
|
134
|
-
windowSeconds:
|
|
133
|
+
requests: quota.requests,
|
|
134
|
+
windowSeconds: quota.windowSeconds,
|
|
135
135
|
used: usage?.count ?? null,
|
|
136
136
|
resetSeconds: usage?.resetSeconds ?? null,
|
|
137
137
|
exceeded: Boolean(usage?.overLimit),
|
|
@@ -177,7 +177,15 @@ export function createGateway(options = {}) {
|
|
|
177
177
|
// Present when the free allowance is what stopped this request, rather than
|
|
178
178
|
// the crawler lists. It changes what the 402 says, not what it costs.
|
|
179
179
|
const usage = context.usage ?? null;
|
|
180
|
-
|
|
180
|
+
/*
|
|
181
|
+
* The allowance the caller was measured against. Defaults to this
|
|
182
|
+
* gateway's own, but a caller that keeps its own counter -- an app-wide
|
|
183
|
+
* throttle metering every route, not just the crawler lists -- hands its
|
|
184
|
+
* own in, so the 402 quotes the limit that actually stopped the request
|
|
185
|
+
* rather than one the gateway happens to hold.
|
|
186
|
+
*/
|
|
187
|
+
const quota = context.quota ?? o.freeQuota;
|
|
188
|
+
const rateHeaders = usage && quota ? quotaHeaders(quota, usage) : {};
|
|
181
189
|
|
|
182
190
|
if (proofHeader) {
|
|
183
191
|
if (!enabled) return json(receipt(asked, { error: 'Payments are not switched on here.' }), 402);
|
|
@@ -272,22 +280,22 @@ export function createGateway(options = {}) {
|
|
|
272
280
|
}
|
|
273
281
|
|
|
274
282
|
if (wantsHtml(request.headers.get('accept'))) {
|
|
275
|
-
return html(o.page(pageCtx(asked, usage)), 402, rateHeaders);
|
|
283
|
+
return html(o.page(pageCtx(asked, usage, quota)), 402, rateHeaders);
|
|
276
284
|
}
|
|
277
285
|
|
|
278
|
-
if (usage) {
|
|
286
|
+
if (usage && quota) {
|
|
279
287
|
// Say what ran out, when it comes back, and what a pass costs, in that
|
|
280
288
|
// order. A caller reading this is deciding between waiting, rotating
|
|
281
289
|
// addresses, and paying, and the numbers are the argument.
|
|
282
290
|
return json(
|
|
283
291
|
receipt(asked, {
|
|
284
292
|
error:
|
|
285
|
-
`Free allowance used: ${
|
|
286
|
-
`${
|
|
293
|
+
`Free allowance used: ${quota.requests} requests per ` +
|
|
294
|
+
`${quota.windowSeconds}s. It resets in ${usage.resetSeconds}s. ` +
|
|
287
295
|
`A pass removes the limit for ${price} a day.`,
|
|
288
296
|
quota: {
|
|
289
|
-
requests:
|
|
290
|
-
windowSeconds:
|
|
297
|
+
requests: quota.requests,
|
|
298
|
+
windowSeconds: quota.windowSeconds,
|
|
291
299
|
used: usage.count,
|
|
292
300
|
resetSeconds: usage.resetSeconds,
|
|
293
301
|
},
|
|
@@ -367,6 +375,33 @@ export function createGateway(options = {}) {
|
|
|
367
375
|
sell,
|
|
368
376
|
enabled,
|
|
369
377
|
options: o,
|
|
378
|
+
/**
|
|
379
|
+
* The pass a request presents, and whether it is one this gateway minted
|
|
380
|
+
* and still honours.
|
|
381
|
+
*
|
|
382
|
+
* Exposed because the pass has to be honoured by everything that could
|
|
383
|
+
* refuse a request, not only by `handle`. An app-wide throttle that meters
|
|
384
|
+
* every route has to skip whoever already paid, and recomputing the
|
|
385
|
+
* signing-secret fallback (`secret || coinpay.apiKey`) on its side is
|
|
386
|
+
* exactly the kind of duplicate rule that drifts and starts charging
|
|
387
|
+
* paying crawlers twice.
|
|
388
|
+
*/
|
|
389
|
+
passFrom,
|
|
390
|
+
verifyPass: async (token) => Boolean(token && (await readPass(token, { secret }))),
|
|
391
|
+
/**
|
|
392
|
+
* The x402 offer this gateway would make, and the full receipt body around
|
|
393
|
+
* it, without answering a request.
|
|
394
|
+
*
|
|
395
|
+
* A site with its own refusal -- a tiered limiter whose 429 body says which
|
|
396
|
+
* rung the caller is on and what the next one costs -- wants to keep that
|
|
397
|
+
* body and add a machine-readable offer to it, so an x402 client can pay
|
|
398
|
+
* on the spot instead of being told in prose to go and fetch /crawl. The
|
|
399
|
+
* alternative is every such site rebuilding payTo, price, currency and
|
|
400
|
+
* network from its own copy of the config, which is how two of them end up
|
|
401
|
+
* quoting different numbers.
|
|
402
|
+
*/
|
|
403
|
+
offer,
|
|
404
|
+
receipt,
|
|
370
405
|
/** robots.txt with this gateway's lists and sales path. */
|
|
371
406
|
robotsTxt: (extra = {}) =>
|
|
372
407
|
robotsTxt({ siteUrl: o.siteUrl, path: o.path, training: o.training, retrieval: o.retrieval, ...extra }),
|