@bankofai/x402-cli 1.0.1-beta.0 → 1.0.1-beta.10
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 +56 -20
- package/dist/args.js +96 -0
- package/dist/catalog-commands.js +558 -0
- package/dist/cli.js +104 -1372
- package/dist/daemon.js +47 -0
- package/dist/gateway/catalog.js +56 -0
- package/dist/gateway/config.js +88 -18
- package/dist/gateway/index.js +3 -0
- package/dist/gateway/server.js +192 -53
- package/dist/gateway/tokens.js +19 -7
- package/dist/gateway-commands.js +145 -0
- package/dist/help.js +169 -0
- package/dist/http-client.js +82 -0
- package/dist/output.js +91 -0
- package/dist/tokens.js +22 -10
- package/dist/x402.js +25 -11
- package/package.json +12 -8
package/dist/gateway/server.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
3
|
import { URL } from "node:url";
|
|
3
4
|
import { endpointFor, paymentRequirements, priceUsd } from "./config.js";
|
|
4
5
|
import { decodeSignature, encodeRequired, encodeResponse, headers, matchRequirement } from "./x402.js";
|
|
5
6
|
class HttpError extends Error {
|
|
6
7
|
status;
|
|
7
8
|
publicMessage;
|
|
8
|
-
|
|
9
|
+
responseHeaders;
|
|
10
|
+
constructor(status, publicMessage, message = publicMessage, responseHeaders = {}) {
|
|
9
11
|
super(message);
|
|
10
12
|
this.status = status;
|
|
11
13
|
this.publicMessage = publicMessage;
|
|
14
|
+
this.responseHeaders = responseHeaders;
|
|
12
15
|
}
|
|
13
16
|
}
|
|
14
17
|
class RequestTooLargeError extends HttpError {
|
|
@@ -16,9 +19,23 @@ class RequestTooLargeError extends HttpError {
|
|
|
16
19
|
super(413, "request body too large");
|
|
17
20
|
}
|
|
18
21
|
}
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
+
function positiveIntegerEnv(name, fallback) {
|
|
23
|
+
const raw = process.env[name];
|
|
24
|
+
if (raw === undefined || raw === "")
|
|
25
|
+
return fallback;
|
|
26
|
+
if (!/^\d+$/.test(raw))
|
|
27
|
+
throw new Error(`${name} must be a positive integer`);
|
|
28
|
+
const value = Number(raw);
|
|
29
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > 2_147_483_647)
|
|
30
|
+
throw new Error(`${name} must be an integer between 1 and 2147483647`);
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
const MAX_BODY_BYTES = positiveIntegerEnv("X402_GATEWAY_MAX_BODY_BYTES", 1_000_000);
|
|
34
|
+
const FACILITATOR_TIMEOUT_MS = positiveIntegerEnv("X402_GATEWAY_FACILITATOR_TIMEOUT_MS", 10_000);
|
|
35
|
+
const UPSTREAM_TIMEOUT_MS = positiveIntegerEnv("X402_GATEWAY_UPSTREAM_TIMEOUT_MS", 30_000);
|
|
36
|
+
const MAX_RESPONSE_BYTES = positiveIntegerEnv("X402_GATEWAY_MAX_RESPONSE_BYTES", 10_000_000);
|
|
37
|
+
const MAX_CONCURRENT_REQUESTS = positiveIntegerEnv("X402_GATEWAY_MAX_CONCURRENT_REQUESTS", 100);
|
|
38
|
+
const RATE_LIMIT_PER_MINUTE = positiveIntegerEnv("X402_GATEWAY_RATE_LIMIT_PER_MINUTE", 300);
|
|
22
39
|
const STRIP_REQUEST_HEADERS = new Set([
|
|
23
40
|
"host",
|
|
24
41
|
"connection",
|
|
@@ -36,6 +53,8 @@ const STRIP_REQUEST_HEADERS = new Set([
|
|
|
36
53
|
"payment-signature",
|
|
37
54
|
"payment-required",
|
|
38
55
|
"x-payment-required",
|
|
56
|
+
"payment-response",
|
|
57
|
+
"x-payment-response",
|
|
39
58
|
"accept-encoding",
|
|
40
59
|
]);
|
|
41
60
|
const STRIP_RESPONSE_HEADERS = new Set([
|
|
@@ -46,15 +65,23 @@ const STRIP_RESPONSE_HEADERS = new Set([
|
|
|
46
65
|
"authorization",
|
|
47
66
|
"proxy-authorization",
|
|
48
67
|
"set-cookie",
|
|
68
|
+
"payment-required",
|
|
69
|
+
"x-payment-required",
|
|
70
|
+
"payment-signature",
|
|
71
|
+
"x-payment",
|
|
72
|
+
"payment-response",
|
|
73
|
+
"x-payment-response",
|
|
49
74
|
]);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
75
|
+
function createMetrics() {
|
|
76
|
+
return {
|
|
77
|
+
requests: 0,
|
|
78
|
+
paidRequests: 0,
|
|
79
|
+
verifyFailures: 0,
|
|
80
|
+
settleFailures: 0,
|
|
81
|
+
upstreamFailures: 0,
|
|
82
|
+
rejectedRequests: 0,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
58
85
|
async function readBody(request) {
|
|
59
86
|
const chunks = [];
|
|
60
87
|
let total = 0;
|
|
@@ -68,17 +95,20 @@ async function readBody(request) {
|
|
|
68
95
|
return Buffer.concat(chunks);
|
|
69
96
|
}
|
|
70
97
|
function json(response, status, body, extraHeaders = {}) {
|
|
71
|
-
response.writeHead(status, { "content-type": "application/json", ...extraHeaders });
|
|
98
|
+
response.writeHead(status, { "content-type": "application/json", "cache-control": "no-store", "x-content-type-options": "nosniff", ...extraHeaders });
|
|
72
99
|
response.end(JSON.stringify(body));
|
|
73
100
|
}
|
|
74
101
|
async function fetchWithTimeout(url, init, timeoutMs, label = "upstream") {
|
|
75
102
|
const controller = new AbortController();
|
|
76
103
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
77
104
|
try {
|
|
78
|
-
|
|
105
|
+
const response = await fetch(url, { ...init, redirect: "manual", signal: controller.signal });
|
|
106
|
+
if (response.status >= 300 && response.status < 400)
|
|
107
|
+
throw new HttpError(502, `${label} redirect refused`);
|
|
108
|
+
return response;
|
|
79
109
|
}
|
|
80
110
|
catch (error) {
|
|
81
|
-
if (error
|
|
111
|
+
if (error instanceof Error && error.name === "AbortError")
|
|
82
112
|
throw new HttpError(504, `${label} request timed out`);
|
|
83
113
|
throw error;
|
|
84
114
|
}
|
|
@@ -86,59 +116,103 @@ async function fetchWithTimeout(url, init, timeoutMs, label = "upstream") {
|
|
|
86
116
|
clearTimeout(timer);
|
|
87
117
|
}
|
|
88
118
|
}
|
|
119
|
+
async function readResponseBytes(response, limit = MAX_RESPONSE_BYTES) {
|
|
120
|
+
const declared = Number(response.headers.get("content-length"));
|
|
121
|
+
if (Number.isFinite(declared) && declared > limit)
|
|
122
|
+
throw new HttpError(502, "upstream response too large");
|
|
123
|
+
if (!response.body)
|
|
124
|
+
return Buffer.alloc(0);
|
|
125
|
+
const chunks = [];
|
|
126
|
+
let total = 0;
|
|
127
|
+
for await (const chunk of response.body) {
|
|
128
|
+
const buffer = Buffer.from(chunk);
|
|
129
|
+
total += buffer.length;
|
|
130
|
+
if (total > limit)
|
|
131
|
+
throw new HttpError(502, "upstream response too large");
|
|
132
|
+
chunks.push(buffer);
|
|
133
|
+
}
|
|
134
|
+
return Buffer.concat(chunks);
|
|
135
|
+
}
|
|
89
136
|
async function facilitatorPost(entry, path, body) {
|
|
90
137
|
const headers = { "content-type": "application/json" };
|
|
91
138
|
if (entry.facilitatorApiKey) {
|
|
92
|
-
headers
|
|
139
|
+
headers["x-api-key"] = entry.facilitatorApiKey;
|
|
93
140
|
}
|
|
94
|
-
const response = await fetchWithTimeout(new URL(path, entry.facilitatorUrl), { method: "POST", headers, body: JSON.stringify(body) }, FACILITATOR_TIMEOUT_MS, "facilitator");
|
|
95
|
-
const text = await response.
|
|
141
|
+
const response = await fetchWithTimeout(new URL(path.replace(/^\/+/, ""), `${entry.facilitatorUrl.replace(/\/+$/, "")}/`), { method: "POST", headers, body: JSON.stringify(body) }, FACILITATOR_TIMEOUT_MS, "facilitator");
|
|
142
|
+
const text = (await readResponseBytes(response, Math.min(MAX_RESPONSE_BYTES, 1_000_000))).toString("utf8");
|
|
96
143
|
let data = {};
|
|
97
144
|
try {
|
|
98
145
|
data = text ? JSON.parse(text) : {};
|
|
99
146
|
}
|
|
100
147
|
catch {
|
|
148
|
+
logFacilitatorFailure(entry, path, response, body, { code: "invalid_json" });
|
|
101
149
|
throw new HttpError(502, "facilitator returned invalid response");
|
|
102
150
|
}
|
|
103
|
-
if (!response.ok)
|
|
104
|
-
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
logFacilitatorFailure(entry, path, response, body, data);
|
|
153
|
+
if (response.status === 429) {
|
|
154
|
+
const retryAfter = response.headers.get("retry-after");
|
|
155
|
+
throw new HttpError(429, "facilitator rate limited", `facilitator ${path} rate limited`, retryAfter ? { "retry-after": retryAfter } : {});
|
|
156
|
+
}
|
|
157
|
+
throw new HttpError(502, "facilitator request failed", `facilitator ${path} failed: ${response.status}`);
|
|
158
|
+
}
|
|
105
159
|
return data;
|
|
106
160
|
}
|
|
107
|
-
|
|
161
|
+
function configuredPublicBaseUrl() {
|
|
162
|
+
const value = process.env.X402_GATEWAY_PUBLIC_BASE_URL?.trim();
|
|
163
|
+
if (!value)
|
|
164
|
+
return undefined;
|
|
108
165
|
try {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
const list = Array.isArray(quotes) ? quotes : quotes.quotes ?? quotes.fees ?? [];
|
|
114
|
-
return requirements.map(requirement => {
|
|
115
|
-
const quote = list.find((item) => item.scheme === requirement.scheme &&
|
|
116
|
-
item.network === requirement.network &&
|
|
117
|
-
String(item.asset).toLowerCase() === requirement.asset.toLowerCase());
|
|
118
|
-
return quote?.fee ? { ...requirement, extra: { ...requirement.extra, fee: quote.fee } } : requirement;
|
|
119
|
-
});
|
|
166
|
+
const url = new URL(value);
|
|
167
|
+
if (!["http:", "https:"].includes(url.protocol))
|
|
168
|
+
throw new Error("unsupported protocol");
|
|
169
|
+
return `${value.replace(/\/+$/, "")}/`;
|
|
120
170
|
}
|
|
121
|
-
catch
|
|
122
|
-
|
|
123
|
-
console.warn("fee quote failed", error);
|
|
124
|
-
return requirements;
|
|
171
|
+
catch {
|
|
172
|
+
throw new Error("X402_GATEWAY_PUBLIC_BASE_URL must be a valid http(s) URL");
|
|
125
173
|
}
|
|
126
174
|
}
|
|
175
|
+
function resourceUrl(url, publicBaseUrl) {
|
|
176
|
+
const path = `${url.pathname}${url.search}`;
|
|
177
|
+
if (!publicBaseUrl)
|
|
178
|
+
return path;
|
|
179
|
+
return new URL(path, publicBaseUrl).toString();
|
|
180
|
+
}
|
|
181
|
+
function logFacilitatorFailure(entry, path, response, body, data) {
|
|
182
|
+
const requirement = body?.paymentRequirements;
|
|
183
|
+
const nestedError = data?.error && typeof data.error === "object" ? data.error : undefined;
|
|
184
|
+
const message = nestedError?.message ?? data?.message ?? data?.detail ??
|
|
185
|
+
(typeof data?.error === "string" ? data.error : undefined);
|
|
186
|
+
console.error(JSON.stringify({
|
|
187
|
+
event: "facilitator_request_failed",
|
|
188
|
+
provider: entry.config.name,
|
|
189
|
+
endpoint: path,
|
|
190
|
+
status: response.status,
|
|
191
|
+
scheme: requirement?.scheme,
|
|
192
|
+
network: requirement?.network,
|
|
193
|
+
errorCode: nestedError?.code ?? data?.code,
|
|
194
|
+
errorMessage: typeof message === "string" ? message.slice(0, 200) : undefined,
|
|
195
|
+
retryAfter: response.headers.get("retry-after") ?? undefined,
|
|
196
|
+
cfRay: response.headers.get("cf-ray") ?? undefined,
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
127
199
|
function isVerifySuccess(verify) {
|
|
128
200
|
return verify?.valid === true || verify?.isValid === true;
|
|
129
201
|
}
|
|
130
202
|
function isSettleSuccess(settle) {
|
|
131
|
-
return
|
|
132
|
-
settle?.settled === true ||
|
|
133
|
-
(typeof settle?.transaction === "string" && settle.transaction.length > 0) ||
|
|
134
|
-
(typeof settle?.txHash === "string" && settle.txHash.length > 0));
|
|
203
|
+
return settle?.success === true && typeof settle?.transaction === "string" && settle.transaction.length > 0 && typeof settle?.network === "string" && settle.network.length > 0;
|
|
135
204
|
}
|
|
136
205
|
function isAdminAllowed(request) {
|
|
137
206
|
const token = process.env.X402_GATEWAY_ADMIN_TOKEN;
|
|
138
207
|
if (!token)
|
|
139
208
|
return process.env.X402_GATEWAY_ADMIN_ALLOW_PUBLIC === "true";
|
|
140
209
|
const auth = request.headers.authorization ?? "";
|
|
141
|
-
|
|
210
|
+
const expected = Buffer.from(`Bearer ${token}`);
|
|
211
|
+
const actual = Buffer.from(auth);
|
|
212
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
213
|
+
}
|
|
214
|
+
function clientAddress(request) {
|
|
215
|
+
return request.socket.remoteAddress ?? "unknown";
|
|
142
216
|
}
|
|
143
217
|
function requestParams(url, body, request) {
|
|
144
218
|
const params = {};
|
|
@@ -165,11 +239,12 @@ function requestParams(url, body, request) {
|
|
|
165
239
|
}
|
|
166
240
|
function upstreamHeaders(request, entry) {
|
|
167
241
|
const headersOut = new Headers();
|
|
242
|
+
const connectionHeaders = new Set(String(request.headers.connection ?? "").split(",").map(value => value.trim().toLowerCase()).filter(Boolean));
|
|
168
243
|
for (const [key, value] of Object.entries(request.headers)) {
|
|
169
244
|
if (!value)
|
|
170
245
|
continue;
|
|
171
246
|
const lower = key.toLowerCase();
|
|
172
|
-
if (STRIP_REQUEST_HEADERS.has(lower))
|
|
247
|
+
if (STRIP_REQUEST_HEADERS.has(lower) || connectionHeaders.has(lower))
|
|
173
248
|
continue;
|
|
174
249
|
headersOut.set(key, Array.isArray(value) ? value.join(",") : value);
|
|
175
250
|
}
|
|
@@ -184,7 +259,14 @@ function upstreamHeaders(request, entry) {
|
|
|
184
259
|
}
|
|
185
260
|
function upstreamUrl(entry, request, routePath) {
|
|
186
261
|
const sourceUrl = new URL(request.url ?? "/", "http://local");
|
|
187
|
-
|
|
262
|
+
if (!routePath.startsWith("/") || routePath.startsWith("//") || routePath.includes("\\") || routePath.includes("\0") || /%(?:2f|5c)/i.test(routePath))
|
|
263
|
+
throw new HttpError(400, "invalid provider path");
|
|
264
|
+
const base = new URL(entry.config.forward_url);
|
|
265
|
+
const upstream = new URL(base);
|
|
266
|
+
upstream.pathname = routePath;
|
|
267
|
+
upstream.search = sourceUrl.search;
|
|
268
|
+
if (upstream.origin !== base.origin)
|
|
269
|
+
throw new HttpError(400, "invalid provider path");
|
|
188
270
|
const auth = entry.config.routing?.auth;
|
|
189
271
|
const value = auth?.value ?? (auth?.value_from_env ? process.env[auth.value_from_env] : undefined);
|
|
190
272
|
if (auth?.method === "query_param" && value) {
|
|
@@ -192,7 +274,7 @@ function upstreamUrl(entry, request, routePath) {
|
|
|
192
274
|
}
|
|
193
275
|
return upstream;
|
|
194
276
|
}
|
|
195
|
-
async function forward(entry, request, response, routePath, body, paymentResponse) {
|
|
277
|
+
async function forward(metrics, entry, request, response, routePath, body, paymentResponse) {
|
|
196
278
|
const upstream = upstreamUrl(entry, request, routePath);
|
|
197
279
|
let upstreamResponse;
|
|
198
280
|
try {
|
|
@@ -216,11 +298,24 @@ async function forward(entry, request, response, routePath, body, paymentRespons
|
|
|
216
298
|
});
|
|
217
299
|
if (paymentResponse)
|
|
218
300
|
responseHeaders[headers.response] = encodeResponse(paymentResponse);
|
|
301
|
+
let responseBody;
|
|
302
|
+
try {
|
|
303
|
+
responseBody = await readResponseBytes(upstreamResponse);
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
metrics.upstreamFailures += 1;
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
219
309
|
response.writeHead(upstreamResponse.status, responseHeaders);
|
|
220
|
-
response.end(
|
|
310
|
+
response.end(responseBody);
|
|
221
311
|
}
|
|
222
312
|
export function createGatewayServer(providers) {
|
|
223
|
-
|
|
313
|
+
const publicBaseUrl = configuredPublicBaseUrl();
|
|
314
|
+
const metrics = createMetrics();
|
|
315
|
+
const rateLimits = new Map();
|
|
316
|
+
let activeRequests = 0;
|
|
317
|
+
const server = http.createServer(async (request, response) => {
|
|
318
|
+
let countedActive = false;
|
|
224
319
|
try {
|
|
225
320
|
metrics.requests += 1;
|
|
226
321
|
const url = new URL(request.url ?? "/", "http://local");
|
|
@@ -265,8 +360,8 @@ export function createGatewayServer(providers) {
|
|
|
265
360
|
`x402_gateway_paid_requests_total ${metrics.paidRequests}`,
|
|
266
361
|
`x402_gateway_verify_failures_total ${metrics.verifyFailures}`,
|
|
267
362
|
`x402_gateway_settle_failures_total ${metrics.settleFailures}`,
|
|
268
|
-
`x402_gateway_fee_quote_failures_total ${metrics.feeQuoteFailures}`,
|
|
269
363
|
`x402_gateway_upstream_failures_total ${metrics.upstreamFailures}`,
|
|
364
|
+
`x402_gateway_rejected_requests_total ${metrics.rejectedRequests}`,
|
|
270
365
|
"",
|
|
271
366
|
].join("\n"));
|
|
272
367
|
return;
|
|
@@ -287,19 +382,41 @@ export function createGatewayServer(providers) {
|
|
|
287
382
|
json(response, 404, { error: "endpoint not found" });
|
|
288
383
|
return;
|
|
289
384
|
}
|
|
385
|
+
const now = Date.now();
|
|
386
|
+
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) {
|
|
394
|
+
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) });
|
|
397
|
+
}
|
|
398
|
+
if (activeRequests >= MAX_CONCURRENT_REQUESTS) {
|
|
399
|
+
metrics.rejectedRequests += 1;
|
|
400
|
+
throw new HttpError(503, "gateway is busy", undefined, { "retry-after": "1" });
|
|
401
|
+
}
|
|
402
|
+
activeRequests += 1;
|
|
403
|
+
countedActive = true;
|
|
290
404
|
const body = await readBody(request);
|
|
291
|
-
const
|
|
405
|
+
const price = priceUsd(endpoint, requestParams(url, body, request));
|
|
406
|
+
const requirements = paymentRequirements(entry.config, price);
|
|
407
|
+
if (price > 0 && !requirements.length)
|
|
408
|
+
throw new HttpError(500, "paid endpoint has no payment requirements");
|
|
292
409
|
if (!requirements.length) {
|
|
293
|
-
await forward(entry, request, response, routePath, body);
|
|
410
|
+
await forward(metrics, entry, request, response, routePath, body);
|
|
294
411
|
return;
|
|
295
412
|
}
|
|
296
413
|
const paymentHeader = request.headers[headers.signature.toLowerCase()];
|
|
297
414
|
if (!paymentHeader || Array.isArray(paymentHeader)) {
|
|
298
|
-
const accepts =
|
|
415
|
+
const accepts = requirements;
|
|
299
416
|
const challenge = {
|
|
300
417
|
x402Version: 2,
|
|
301
418
|
error: "Payment required",
|
|
302
|
-
resource: { url: url
|
|
419
|
+
resource: { url: resourceUrl(url, publicBaseUrl) },
|
|
303
420
|
accepts,
|
|
304
421
|
};
|
|
305
422
|
json(response, 402, challenge, { [headers.required]: encodeRequired(challenge) });
|
|
@@ -344,15 +461,37 @@ export function createGatewayServer(providers) {
|
|
|
344
461
|
return;
|
|
345
462
|
}
|
|
346
463
|
metrics.paidRequests += 1;
|
|
347
|
-
|
|
464
|
+
try {
|
|
465
|
+
await forward(metrics, entry, request, response, routePath, body, settle);
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
const status = error instanceof HttpError ? error.status : 502;
|
|
469
|
+
const extraHeaders = error instanceof HttpError ? error.responseHeaders : {};
|
|
470
|
+
json(response, status, {
|
|
471
|
+
error: "upstream failed after payment settlement",
|
|
472
|
+
settled: true,
|
|
473
|
+
}, {
|
|
474
|
+
...extraHeaders,
|
|
475
|
+
[headers.response]: encodeResponse(settle),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
348
478
|
}
|
|
349
479
|
catch (error) {
|
|
350
480
|
if (error instanceof HttpError) {
|
|
351
|
-
json(response, error.status, { error: error.publicMessage });
|
|
481
|
+
json(response, error.status, { error: error.publicMessage }, error.responseHeaders);
|
|
352
482
|
return;
|
|
353
483
|
}
|
|
354
484
|
console.error(error);
|
|
355
485
|
json(response, 500, { error: "internal server error" });
|
|
356
486
|
}
|
|
487
|
+
finally {
|
|
488
|
+
if (countedActive)
|
|
489
|
+
activeRequests -= 1;
|
|
490
|
+
}
|
|
357
491
|
});
|
|
492
|
+
server.requestTimeout = UPSTREAM_TIMEOUT_MS + FACILITATOR_TIMEOUT_MS * 2 + 5_000;
|
|
493
|
+
server.headersTimeout = Math.min(server.requestTimeout, 60_000);
|
|
494
|
+
server.keepAliveTimeout = 5_000;
|
|
495
|
+
server.maxConnections = MAX_CONCURRENT_REQUESTS * 2;
|
|
496
|
+
return server;
|
|
358
497
|
}
|
package/dist/gateway/tokens.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const TOKENS = {
|
|
2
|
-
"tron:
|
|
2
|
+
"tron:0x2b6653dc": {
|
|
3
3
|
USDT: { address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", decimals: 6, name: "Tether USD", symbol: "USDT", assetTransferMethod: "permit2" },
|
|
4
4
|
USDD: { address: "TXDk8mbtRbXeYuMNS83CfKPaYYT8XWv9Hz", decimals: 18, name: "Decentralized USD", symbol: "USDD", assetTransferMethod: "permit2" },
|
|
5
5
|
},
|
|
6
|
-
"tron:
|
|
6
|
+
"tron:0xcd8690dc": {
|
|
7
7
|
USDT: { address: "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf", decimals: 6, name: "Tether USD", symbol: "USDT", assetTransferMethod: "permit2" },
|
|
8
8
|
USDD: { address: "TGjgvdTWWrybVLaVeFqSyVqJQWjxqRYbaK", decimals: 18, name: "Decentralized USD", symbol: "USDD", assetTransferMethod: "permit2" },
|
|
9
9
|
},
|
|
@@ -16,13 +16,25 @@ export const TOKENS = {
|
|
|
16
16
|
},
|
|
17
17
|
};
|
|
18
18
|
export function normalizeNetwork(network) {
|
|
19
|
-
|
|
20
|
-
"tron-mainnet": "tron:
|
|
21
|
-
"tron
|
|
22
|
-
|
|
19
|
+
const legacyTronIds = {
|
|
20
|
+
"tron-mainnet": "tron:0x2b6653dc",
|
|
21
|
+
"tron:mainnet": "tron:0x2b6653dc",
|
|
22
|
+
mainnet: "tron:0x2b6653dc",
|
|
23
|
+
"tron-shasta": "tron:0x94a9059e",
|
|
24
|
+
"tron:shasta": "tron:0x94a9059e",
|
|
25
|
+
shasta: "tron:0x94a9059e",
|
|
26
|
+
"tron-nile": "tron:0xcd8690dc",
|
|
27
|
+
"tron:nile": "tron:0xcd8690dc",
|
|
28
|
+
nile: "tron:0xcd8690dc",
|
|
29
|
+
};
|
|
30
|
+
const canonical = legacyTronIds[network];
|
|
31
|
+
if (canonical) {
|
|
32
|
+
throw new Error(`legacy TRON network identifier ${network} is not supported; use ${canonical}`);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
23
35
|
"bsc-mainnet": "eip155:56",
|
|
24
36
|
"bsc-testnet": "eip155:97",
|
|
25
|
-
}[network] ?? network
|
|
37
|
+
}[network] ?? network;
|
|
26
38
|
}
|
|
27
39
|
export function getToken(network, symbol) {
|
|
28
40
|
const normalized = normalizeNetwork(network);
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { opt, outputMode } from "./args.js";
|
|
7
|
+
import { emit, printJson } from "./output.js";
|
|
8
|
+
import { buildGatewayCatalog, loadProviders, providerPaymentAssets } from "@bankofai/x402-gateway";
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
function executableInPath(name) {
|
|
11
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
12
|
+
if (!dir)
|
|
13
|
+
continue;
|
|
14
|
+
const candidate = path.join(dir, name);
|
|
15
|
+
try {
|
|
16
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
17
|
+
return candidate;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Try the next PATH entry.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
function resolveGatewayPackageRuntime() {
|
|
26
|
+
try {
|
|
27
|
+
return path.join(path.dirname(require.resolve("@bankofai/x402-gateway/package.json")), "dist", "cli.js");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function gatewayCommand(options) {
|
|
34
|
+
const explicit = opt(options, "gateway-bin");
|
|
35
|
+
const gatewayPackageRuntime = resolveGatewayPackageRuntime();
|
|
36
|
+
const candidates = [
|
|
37
|
+
explicit ? { file: explicit, source: "--gateway-bin" } : undefined,
|
|
38
|
+
gatewayPackageRuntime ? { file: gatewayPackageRuntime, source: "@bankofai/x402-gateway dependency" } : undefined,
|
|
39
|
+
{ file: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "gateway", "cli.js"), source: "bundled gateway runtime" },
|
|
40
|
+
executableInPath("x402-gateway") ? { file: executableInPath("x402-gateway"), source: "PATH x402-gateway" } : undefined,
|
|
41
|
+
{ file: path.resolve(process.cwd(), "../x402-gateway/dist/cli.js"), source: "sibling ../x402-gateway" },
|
|
42
|
+
].filter(Boolean);
|
|
43
|
+
for (const candidate of candidates) {
|
|
44
|
+
try {
|
|
45
|
+
fs.accessSync(candidate.file, fs.constants.R_OK);
|
|
46
|
+
if (candidate.file.endsWith(".js")) {
|
|
47
|
+
return { command: process.execPath, argsPrefix: [candidate.file], source: candidate.source };
|
|
48
|
+
}
|
|
49
|
+
return { command: candidate.file, argsPrefix: [], source: candidate.source };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Try the next candidate.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw new Error("x402-gateway runtime not found. Install @bankofai/x402-gateway, run from a checkout with ../x402-gateway/dist/cli.js, or pass --gateway-bin <path>.");
|
|
56
|
+
}
|
|
57
|
+
export async function gatewayStart(args, options) {
|
|
58
|
+
const gateway = gatewayCommand(options);
|
|
59
|
+
const child = spawn(gateway.command, [...gateway.argsPrefix, ...args], {
|
|
60
|
+
stdio: "inherit",
|
|
61
|
+
env: process.env,
|
|
62
|
+
});
|
|
63
|
+
await new Promise((resolve, reject) => {
|
|
64
|
+
child.on("exit", code => code === 0 ? resolve() : reject(new Error(`gateway exited with ${code}`)));
|
|
65
|
+
child.on("error", reject);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export function gatewayCheck(target, options) {
|
|
69
|
+
const providers = loadProviders(target);
|
|
70
|
+
emit({
|
|
71
|
+
command: "gateway check",
|
|
72
|
+
mode: outputMode(options),
|
|
73
|
+
result: { providers: [...providers.keys()], count: providers.size },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function gatewayScaffold(name, options) {
|
|
77
|
+
const outputDir = opt(options, "output-dir", path.join("providers", name));
|
|
78
|
+
const forwardUrl = opt(options, "forward-url", "https://api.example.com");
|
|
79
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
80
|
+
const body = `name: ${name}
|
|
81
|
+
title: "${name}"
|
|
82
|
+
description: "x402 provider"
|
|
83
|
+
category: data
|
|
84
|
+
version: v1
|
|
85
|
+
|
|
86
|
+
forward_url: ${forwardUrl}
|
|
87
|
+
|
|
88
|
+
routing:
|
|
89
|
+
type: proxy
|
|
90
|
+
|
|
91
|
+
operator:
|
|
92
|
+
network: tron:0xcd8690dc
|
|
93
|
+
currencies:
|
|
94
|
+
usd: ["USDT"]
|
|
95
|
+
recipient: <provider-recipient-address>
|
|
96
|
+
scheme: exact
|
|
97
|
+
protocol: exact
|
|
98
|
+
asset_transfer_method: permit2
|
|
99
|
+
facilitator_url: https://facilitator.bankofai.io
|
|
100
|
+
facilitator_api_key: <facilitator-api-key>
|
|
101
|
+
valid_for_seconds: 300
|
|
102
|
+
|
|
103
|
+
endpoints:
|
|
104
|
+
- method: GET
|
|
105
|
+
path: /v1/ping
|
|
106
|
+
metering:
|
|
107
|
+
dimensions:
|
|
108
|
+
- tiers:
|
|
109
|
+
- price_usd: 0.0001
|
|
110
|
+
`;
|
|
111
|
+
fs.writeFileSync(path.join(outputDir, "provider.yml"), body);
|
|
112
|
+
emit({
|
|
113
|
+
command: "gateway scaffold",
|
|
114
|
+
mode: outputMode(options),
|
|
115
|
+
result: { file: path.join(outputDir, "provider.yml") },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export function catalogBuild(target, options) {
|
|
119
|
+
const providers = loadProviders(target);
|
|
120
|
+
const catalog = buildGatewayCatalog(providers);
|
|
121
|
+
const output = opt(options, "output", opt(options, "dist-dir") ? path.join(opt(options, "dist-dir"), "catalog.json") : undefined);
|
|
122
|
+
if (output) {
|
|
123
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
124
|
+
fs.writeFileSync(output, JSON.stringify(catalog, null, 2));
|
|
125
|
+
emit({
|
|
126
|
+
command: "catalog build",
|
|
127
|
+
mode: outputMode(options),
|
|
128
|
+
result: { output, count: providers.size },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else if (outputMode(options) === "json") {
|
|
132
|
+
emit({ command: "catalog build", mode: "json", result: catalog });
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
printJson(catalog);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export function catalogPayAssets(target, options) {
|
|
139
|
+
const rows = providerPaymentAssets(loadProviders(target));
|
|
140
|
+
emit({
|
|
141
|
+
command: "gateway catalog pay-assets",
|
|
142
|
+
mode: outputMode(options),
|
|
143
|
+
result: { count: rows.length, assets: rows },
|
|
144
|
+
});
|
|
145
|
+
}
|