@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.
@@ -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
- constructor(status, publicMessage, message = publicMessage) {
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
- const MAX_BODY_BYTES = Number(process.env.X402_GATEWAY_MAX_BODY_BYTES ?? 1_000_000);
20
- const FACILITATOR_TIMEOUT_MS = Number(process.env.X402_GATEWAY_FACILITATOR_TIMEOUT_MS ?? 10_000);
21
- const UPSTREAM_TIMEOUT_MS = Number(process.env.X402_GATEWAY_UPSTREAM_TIMEOUT_MS ?? 30_000);
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
- const metrics = {
51
- requests: 0,
52
- paidRequests: 0,
53
- verifyFailures: 0,
54
- settleFailures: 0,
55
- feeQuoteFailures: 0,
56
- upstreamFailures: 0,
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
- return await fetch(url, { ...init, signal: controller.signal });
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?.name === "AbortError")
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.authorization = `Bearer ${entry.facilitatorApiKey}`;
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.text();
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
- throw new HttpError(502, "facilitator request failed", `facilitator ${path} failed: ${response.status} ${text}`);
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
- async function attachFeeQuotes(entry, requirements, context) {
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 quotes = await facilitatorPost(entry, "/fee_quote", {
110
- paymentRequirements: requirements,
111
- context,
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 (error) {
122
- metrics.feeQuoteFailures += 1;
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 (settle?.success === true ||
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
- return auth === `Bearer ${token}`;
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
- const upstream = new URL(routePath + (sourceUrl.search || ""), entry.config.forward_url);
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(Buffer.from(await upstreamResponse.arrayBuffer()));
310
+ response.end(responseBody);
221
311
  }
222
312
  export function createGatewayServer(providers) {
223
- return http.createServer(async (request, response) => {
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 requirements = paymentRequirements(entry.config, priceUsd(endpoint, requestParams(url, body, request)));
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 = await attachFeeQuotes(entry, requirements);
415
+ const accepts = requirements;
299
416
  const challenge = {
300
417
  x402Version: 2,
301
418
  error: "Payment required",
302
- resource: { url: url.pathname },
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
- await forward(entry, request, response, routePath, body, settle);
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
  }
@@ -1,9 +1,9 @@
1
1
  export const TOKENS = {
2
- "tron:mainnet": {
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:nile": {
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
- return ({
20
- "tron-mainnet": "tron:mainnet",
21
- "tron-shasta": "tron:shasta",
22
- "tron-nile": "tron:nile",
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
+ }