@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.
Files changed (3) hide show
  1. package/index.d.ts +24 -1
  2. package/package.json +41 -11
  3. 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.4.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": { "type": "git", "url": "git+https://github.com/profullstack/x402-gateway.git" },
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": { "url": "https://github.com/profullstack/x402-gateway/issues" },
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
- ".": { "types": "./index.d.ts", "import": "./src/index.js" },
30
- "./hono": { "types": "./index.d.ts", "import": "./src/hono.js" },
31
- "./next": { "types": "./index.d.ts", "import": "./src/next.js" },
32
- "./robots": { "types": "./index.d.ts", "import": "./src/robots.js" },
33
- "./agents": { "types": "./index.d.ts", "import": "./src/agents.js" },
34
- "./edge": { "types": "./index.d.ts", "import": "./src/edge.js" }
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": ["src", "index.d.ts", "README.md", "LICENSE"],
38
- "engines": { "node": ">=20.11" },
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: o.freeQuota
130
+ const pageCtx = (days = 1, usage = null, quota = o.freeQuota) => ({
131
+ quota: quota
132
132
  ? {
133
- requests: o.freeQuota.requests,
134
- windowSeconds: o.freeQuota.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
- const rateHeaders = usage ? quotaHeaders(o.freeQuota, usage) : {};
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: ${o.freeQuota.requests} requests per ` +
286
- `${o.freeQuota.windowSeconds}s. It resets in ${usage.resetSeconds}s. ` +
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: o.freeQuota.requests,
290
- windowSeconds: o.freeQuota.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 }),