@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 CHANGED
@@ -16,7 +16,7 @@ from the payment token, so the payer does not need TRX.
16
16
  Install the CLI package:
17
17
 
18
18
  ```bash
19
- npm install -g @bankofai/x402-cli@beta
19
+ npm install -g @bankofai/x402-cli@1.0.1
20
20
  x402-cli --version
21
21
  ```
22
22
 
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
+ }
@@ -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 rateLimits = new Map();
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
- let rate = rateLimits.get(address);
388
- if (!rate || rate.resetAt <= now) {
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
- const retryAfter = Math.max(1, Math.ceil((rate.resetAt - now) / 1000));
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-beta.9",
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
- "build": "tsc -p tsconfig.json",
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-beta.10",
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"
@@ -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
- }