@bankofai/x402-cli 1.0.1-beta.9 → 1.0.1
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 +1 -1
- package/dist/gateway/cli.js +0 -0
- package/dist/gateway/rate-limit.js +34 -0
- package/dist/gateway/server.js +5 -10
- package/package.json +4 -3
- package/dist/provider-config.js +0 -94
package/README.md
CHANGED
package/dist/gateway/cli.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export class FixedWindowRateLimiter {
|
|
2
|
+
limit;
|
|
3
|
+
windowMs;
|
|
4
|
+
cleanupIntervalMs;
|
|
5
|
+
entries = new Map();
|
|
6
|
+
nextCleanupAt = 0;
|
|
7
|
+
constructor(limit, windowMs = 60_000, cleanupIntervalMs = 60_000) {
|
|
8
|
+
this.limit = limit;
|
|
9
|
+
this.windowMs = windowMs;
|
|
10
|
+
this.cleanupIntervalMs = cleanupIntervalMs;
|
|
11
|
+
}
|
|
12
|
+
consume(key, now = Date.now()) {
|
|
13
|
+
if (now >= this.nextCleanupAt) {
|
|
14
|
+
for (const [entryKey, entry] of this.entries) {
|
|
15
|
+
if (entry.resetAt <= now)
|
|
16
|
+
this.entries.delete(entryKey);
|
|
17
|
+
}
|
|
18
|
+
this.nextCleanupAt = now + this.cleanupIntervalMs;
|
|
19
|
+
}
|
|
20
|
+
let entry = this.entries.get(key);
|
|
21
|
+
if (!entry || entry.resetAt <= now) {
|
|
22
|
+
entry = { count: 0, resetAt: now + this.windowMs };
|
|
23
|
+
this.entries.set(key, entry);
|
|
24
|
+
}
|
|
25
|
+
entry.count += 1;
|
|
26
|
+
return {
|
|
27
|
+
allowed: entry.count <= this.limit,
|
|
28
|
+
retryAfter: Math.max(1, Math.ceil((entry.resetAt - now) / 1000)),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
get size() {
|
|
32
|
+
return this.entries.size;
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/gateway/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import http from "node:http";
|
|
|
2
2
|
import { timingSafeEqual } from "node:crypto";
|
|
3
3
|
import { URL } from "node:url";
|
|
4
4
|
import { endpointFor, paymentRequirements, priceUsd } from "./config.js";
|
|
5
|
+
import { FixedWindowRateLimiter } from "./rate-limit.js";
|
|
5
6
|
import { decodeSignature, encodeRequired, encodeResponse, headers, matchRequirement } from "./x402.js";
|
|
6
7
|
class HttpError extends Error {
|
|
7
8
|
status;
|
|
@@ -312,7 +313,7 @@ async function forward(metrics, entry, request, response, routePath, body, payme
|
|
|
312
313
|
export function createGatewayServer(providers) {
|
|
313
314
|
const publicBaseUrl = configuredPublicBaseUrl();
|
|
314
315
|
const metrics = createMetrics();
|
|
315
|
-
const
|
|
316
|
+
const rateLimiter = new FixedWindowRateLimiter(RATE_LIMIT_PER_MINUTE);
|
|
316
317
|
let activeRequests = 0;
|
|
317
318
|
const server = http.createServer(async (request, response) => {
|
|
318
319
|
let countedActive = false;
|
|
@@ -384,16 +385,10 @@ export function createGatewayServer(providers) {
|
|
|
384
385
|
}
|
|
385
386
|
const now = Date.now();
|
|
386
387
|
const address = clientAddress(request);
|
|
387
|
-
|
|
388
|
-
if (!rate
|
|
389
|
-
rate = { count: 0, resetAt: now + 60_000 };
|
|
390
|
-
rateLimits.set(address, rate);
|
|
391
|
-
}
|
|
392
|
-
rate.count += 1;
|
|
393
|
-
if (rate.count > RATE_LIMIT_PER_MINUTE) {
|
|
388
|
+
const rate = rateLimiter.consume(address, now);
|
|
389
|
+
if (!rate.allowed) {
|
|
394
390
|
metrics.rejectedRequests += 1;
|
|
395
|
-
|
|
396
|
-
throw new HttpError(429, "gateway rate limited", undefined, { "retry-after": String(retryAfter) });
|
|
391
|
+
throw new HttpError(429, "gateway rate limited", undefined, { "retry-after": String(rate.retryAfter) });
|
|
397
392
|
}
|
|
398
393
|
if (activeRequests >= MAX_CONCURRENT_REQUESTS) {
|
|
399
394
|
metrics.rejectedRequests += 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bankofai/x402-cli",
|
|
3
|
-
"version": "1.0.1
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"x402-cli": "dist/cli.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"
|
|
12
|
+
"clean": "node scripts/clean.mjs",
|
|
13
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
13
14
|
"bundle:gateway": "node scripts/bundle-gateway.mjs",
|
|
14
15
|
"prepack": "npm run build && npm run bundle:gateway",
|
|
15
16
|
"test": "npm run build && node --test tests/*.test.mjs",
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"@bankofai/x402-core": "1.0.1",
|
|
25
26
|
"@bankofai/x402-evm": "1.0.1",
|
|
26
|
-
"@bankofai/x402-gateway": "1.0.1
|
|
27
|
+
"@bankofai/x402-gateway": "1.0.1",
|
|
27
28
|
"@bankofai/x402-tron": "1.0.1",
|
|
28
29
|
"tronweb": "6.4.0",
|
|
29
30
|
"viem": "^2.55.0"
|
package/dist/provider-config.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import YAML from "yaml";
|
|
4
|
-
import { normalizeNetwork } from "./tokens.js";
|
|
5
|
-
function readYaml(file) {
|
|
6
|
-
return YAML.parse(fs.readFileSync(file, "utf8"));
|
|
7
|
-
}
|
|
8
|
-
function expandEnv(value) {
|
|
9
|
-
return value.replace(/\$\{([^}]+)\}/g, (_, name) => {
|
|
10
|
-
if (!(name in process.env))
|
|
11
|
-
throw new Error(`environment variable \${${name}} is not set`);
|
|
12
|
-
return process.env[name] ?? "";
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
function expandDeep(value) {
|
|
16
|
-
if (typeof value === "string")
|
|
17
|
-
return expandEnv(value);
|
|
18
|
-
if (Array.isArray(value))
|
|
19
|
-
return value.map(expandDeep);
|
|
20
|
-
if (value && typeof value === "object")
|
|
21
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, expandDeep(item)]));
|
|
22
|
-
return value;
|
|
23
|
-
}
|
|
24
|
-
export function providerFiles(root) {
|
|
25
|
-
const stat = fs.statSync(root);
|
|
26
|
-
if (stat.isFile())
|
|
27
|
-
return [root];
|
|
28
|
-
const out = [];
|
|
29
|
-
for (const entry of fs.readdirSync(root, { recursive: true })) {
|
|
30
|
-
const file = path.join(root, String(entry));
|
|
31
|
-
if (file.endsWith("provider.yml") || file.endsWith("provider.yaml"))
|
|
32
|
-
out.push(file);
|
|
33
|
-
}
|
|
34
|
-
return out.sort();
|
|
35
|
-
}
|
|
36
|
-
export function providerPrice(endpoint) {
|
|
37
|
-
return endpoint?.metering?.dimensions?.[0]?.tiers?.[0]?.price_usd ?? 0;
|
|
38
|
-
}
|
|
39
|
-
export function validateProvider(provider, file = "provider.yml") {
|
|
40
|
-
const required = [["name", provider?.name], ["forward_url", provider?.forward_url], ["operator.network", provider?.operator?.network], ["operator.recipient", provider?.operator?.recipient]];
|
|
41
|
-
for (const [name, value] of required)
|
|
42
|
-
if (typeof value !== "string" || !value.trim())
|
|
43
|
-
throw new Error(`${file}: ${name} is required`);
|
|
44
|
-
try {
|
|
45
|
-
const url = new URL(provider.forward_url);
|
|
46
|
-
if (!["http:", "https:"].includes(url.protocol))
|
|
47
|
-
throw new Error("unsupported protocol");
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
throw new Error(`${file}: forward_url must be a valid http(s) URL`);
|
|
51
|
-
}
|
|
52
|
-
if (!Array.isArray(provider.endpoints) || !provider.endpoints.length)
|
|
53
|
-
throw new Error(`${file}: endpoints must contain at least one endpoint`);
|
|
54
|
-
const seen = new Set();
|
|
55
|
-
for (const endpoint of provider.endpoints) {
|
|
56
|
-
if (typeof endpoint.method !== "string" || typeof endpoint.path !== "string")
|
|
57
|
-
throw new Error(`${file}: each endpoint needs method and path`);
|
|
58
|
-
if (!endpoint.method.trim() || !endpoint.path.trim() || !endpoint.path.startsWith("/"))
|
|
59
|
-
throw new Error(`${file}: endpoint method/path must be non-empty and path must start with /`);
|
|
60
|
-
const price = providerPrice(endpoint);
|
|
61
|
-
if (!Number.isFinite(price) || price < 0)
|
|
62
|
-
throw new Error(`${file}: endpoint price_usd must be a finite number >= 0`);
|
|
63
|
-
const key = `${endpoint.method.toUpperCase()} ${endpoint.path}`;
|
|
64
|
-
if (seen.has(key))
|
|
65
|
-
throw new Error(`${file}: duplicate endpoint ${key}`);
|
|
66
|
-
seen.add(key);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
export function loadProviderFile(file) {
|
|
70
|
-
const provider = expandDeep(readYaml(file));
|
|
71
|
-
validateProvider(provider, file);
|
|
72
|
-
provider.operator.network = normalizeNetwork(provider.operator.network);
|
|
73
|
-
provider.operator.scheme = provider.operator.scheme ?? "exact";
|
|
74
|
-
return provider;
|
|
75
|
-
}
|
|
76
|
-
export function providerAssetTransferMethod(provider) {
|
|
77
|
-
return provider.operator?.asset_transfer_method ?? provider.operator?.assetTransferMethod ?? "permit2";
|
|
78
|
-
}
|
|
79
|
-
export function providerScheme(provider) {
|
|
80
|
-
return provider.operator?.scheme ?? "exact";
|
|
81
|
-
}
|
|
82
|
-
export function providerCatalog(provider) {
|
|
83
|
-
return {
|
|
84
|
-
name: provider.name, title: provider.title ?? provider.name, description: provider.description ?? "",
|
|
85
|
-
category: provider.category ?? "other", service_url: provider.display?.service_url, tags: provider.display?.tags ?? [],
|
|
86
|
-
network: normalizeNetwork(provider.operator.network), currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
|
|
87
|
-
endpoints: (provider.endpoints ?? []).map((endpoint) => ({
|
|
88
|
-
method: endpoint.method.toUpperCase(), path: `/providers/${provider.name}${endpoint.path}`, upstream_path: endpoint.path,
|
|
89
|
-
description: endpoint.description ?? "",
|
|
90
|
-
paid: providerPrice(endpoint) > 0 ? { scheme: providerScheme(provider), network: normalizeNetwork(provider.operator.network), currency: provider.operator.currencies?.usd?.[0] ?? "USDT", price_usd: providerPrice(endpoint) } : null,
|
|
91
|
-
x402_routes: providerPrice(endpoint) > 0 ? [{ provider: provider.name, network: normalizeNetwork(provider.operator.network), scheme: providerScheme(provider), assetTransferMethod: providerAssetTransferMethod(provider), url: `/providers/${provider.name}${endpoint.path}` }] : [],
|
|
92
|
-
})),
|
|
93
|
-
};
|
|
94
|
-
}
|