@bankofai/x402-cli 1.0.1-beta.7 → 1.0.1-beta.9

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/dist/daemon.js ADDED
@@ -0,0 +1,47 @@
1
+ import net from "node:net";
2
+ import { spawn } from "node:child_process";
3
+ import { setTimeout as delay } from "node:timers/promises";
4
+ import { opt, outputMode } from "./args.js";
5
+ import { emit } from "./output.js";
6
+ function stripFlag(argv, flag) {
7
+ return argv.filter(item => item !== flag);
8
+ }
9
+ async function waitForPort(host, port, timeout = 5_000) {
10
+ const deadline = Date.now() + timeout;
11
+ while (Date.now() < deadline) {
12
+ const connected = await new Promise(resolve => {
13
+ const socket = net.createConnection({ host, port });
14
+ socket.setTimeout(250);
15
+ socket.once("connect", () => { socket.destroy(); resolve(true); });
16
+ socket.once("error", () => resolve(false));
17
+ socket.once("timeout", () => { socket.destroy(); resolve(false); });
18
+ });
19
+ if (connected)
20
+ return;
21
+ await delay(50);
22
+ }
23
+ throw new Error(`daemon did not become ready on ${host}:${port}`);
24
+ }
25
+ export async function startServeDaemon(argv, options, requirement, script) {
26
+ if (!requirement.payTo)
27
+ throw new Error("--pay-to is required");
28
+ const daemonArgs = stripFlag(stripFlag(argv, "--daemon"), "-d");
29
+ const child = spawn(process.execPath, [script, ...daemonArgs], { detached: true, stdio: "ignore", env: process.env });
30
+ child.unref();
31
+ const host = opt(options, "host", "127.0.0.1");
32
+ const port = Number(opt(options, "port", "4020"));
33
+ const resourceUrl = opt(options, "resource-url", `http://${host}:${port}/pay`);
34
+ try {
35
+ await waitForPort(host === "0.0.0.0" ? "127.0.0.1" : host === "::" ? "::1" : host, port);
36
+ }
37
+ catch (error) {
38
+ if (child.pid)
39
+ try {
40
+ process.kill(child.pid);
41
+ }
42
+ catch { /* child already exited */ }
43
+ throw error;
44
+ }
45
+ emit({ command: "server", mode: outputMode(options), network: requirement.network, scheme: requirement.scheme,
46
+ result: { pid: child.pid, pay_url: resourceUrl, resource_url: resourceUrl, daemon: true } });
47
+ }
@@ -0,0 +1,56 @@
1
+ import { paymentRequirements, priceUsd } from "./config.js";
2
+ function configs(input) {
3
+ return input instanceof Map ? [...input.values()].map(entry => entry.config) : [...input];
4
+ }
5
+ export function providerCatalogProjection(provider) {
6
+ return {
7
+ name: provider.name,
8
+ title: provider.title ?? provider.name,
9
+ description: "",
10
+ category: "other",
11
+ network: provider.operator.network,
12
+ currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
13
+ endpoints: (provider.endpoints ?? []).map(endpoint => {
14
+ const price = priceUsd(endpoint);
15
+ const requirements = paymentRequirements(provider, price);
16
+ return {
17
+ method: endpoint.method.toUpperCase(),
18
+ path: `/providers/${provider.name}${endpoint.path}`,
19
+ upstream_path: endpoint.path,
20
+ description: "",
21
+ paid: price > 0 ? {
22
+ scheme: requirements[0]?.scheme,
23
+ network: requirements[0]?.network,
24
+ currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
25
+ price_usd: price,
26
+ } : null,
27
+ x402_routes: requirements.map(requirement => ({
28
+ provider: provider.name,
29
+ network: requirement.network,
30
+ scheme: requirement.scheme,
31
+ ...(requirement.extra?.assetTransferMethod ? { assetTransferMethod: requirement.extra.assetTransferMethod } : {}),
32
+ url: `/providers/${provider.name}${endpoint.path}`,
33
+ })),
34
+ };
35
+ }),
36
+ };
37
+ }
38
+ export function buildGatewayCatalog(input) {
39
+ return { version: 1, generatedAt: new Date().toISOString(), providers: configs(input).map(providerCatalogProjection) };
40
+ }
41
+ export function providerPaymentAssets(input) {
42
+ return configs(input).flatMap(provider => (provider.endpoints ?? []).flatMap(endpoint => {
43
+ const price = priceUsd(endpoint);
44
+ const currencies = provider.operator.currencies?.usd ?? ["USDT"];
45
+ return paymentRequirements(provider, price).map((requirement, index) => ({
46
+ provider: provider.name,
47
+ method: endpoint.method,
48
+ path: `/providers/${provider.name}${endpoint.path}`,
49
+ network: requirement.network,
50
+ currency: currencies[index % currencies.length],
51
+ price_usd: price,
52
+ scheme: requirement.scheme,
53
+ ...(requirement.extra?.assetTransferMethod ? { assetTransferMethod: requirement.extra.assetTransferMethod } : {}),
54
+ }));
55
+ }));
56
+ }
File without changes
@@ -30,6 +30,11 @@ function assertHttpUrl(value, name) {
30
30
  const url = new URL(value);
31
31
  if (!["http:", "https:"].includes(url.protocol))
32
32
  throw new Error("unsupported protocol");
33
+ if (url.username || url.password || url.search || url.hash)
34
+ throw new Error("credentials, query, and fragment are not allowed");
35
+ if (url.protocol === "http:" && !["localhost", "127.0.0.1", "::1"].includes(url.hostname) && process.env.X402_GATEWAY_ALLOW_INSECURE_HTTP !== "true") {
36
+ throw new Error("remote HTTP is not allowed");
37
+ }
33
38
  }
34
39
  catch {
35
40
  throw new Error(`${name} must be a valid http(s) URL`);
@@ -80,6 +85,13 @@ function validateProvider(config, file) {
80
85
  if (authMethod && !["header", "query_param", "access_token", "oauth2"].includes(authMethod)) {
81
86
  throw new Error(`${file}: unsupported routing.auth.method ${authMethod}`);
82
87
  }
88
+ const auth = config.routing?.auth;
89
+ if (auth) {
90
+ if (!auth.value && !auth.value_from_env)
91
+ throw new Error(`${file}: routing.auth requires value or value_from_env`);
92
+ if (auth.value_from_env && !process.env[auth.value_from_env])
93
+ throw new Error(`${file}: environment variable ${auth.value_from_env} is not set`);
94
+ }
83
95
  if (!config.endpoints?.length)
84
96
  throw new Error(`${file}: endpoints must contain at least one endpoint`);
85
97
  const seen = new Set();
@@ -104,6 +116,7 @@ export function loadProvider(file) {
104
116
  validateProvider(config, file);
105
117
  config.operator.network = normalizeNetwork(config.operator.network);
106
118
  normalizePaymentProtocol(config, file);
119
+ validatePaymentCapabilities(config, file);
107
120
  const facilitatorUrl = config.operator.facilitator_url ||
108
121
  process.env.X402_FACILITATOR_URL ||
109
122
  process.env.FACILITATOR_URL ||
@@ -125,10 +138,13 @@ export function loadProvider(file) {
125
138
  };
126
139
  }
127
140
  function normalizePaymentProtocol(config, file) {
128
- const rawSchemes = config.operator.schemes?.length
129
- ? config.operator.schemes
130
- : [config.operator.protocol || config.operator.scheme || "exact"];
141
+ if (config.operator.schemes !== undefined && (!Array.isArray(config.operator.schemes) || !config.operator.schemes.length)) {
142
+ throw new Error(`${file}: operator.schemes must be a non-empty string array`);
143
+ }
144
+ const rawSchemes = config.operator.schemes ?? [config.operator.protocol || config.operator.scheme || "exact"];
131
145
  const schemes = [...new Set(rawSchemes.map(value => {
146
+ if (typeof value !== "string" || !value.trim())
147
+ throw new Error(`${file}: operator.schemes must contain non-empty strings`);
132
148
  const raw = String(value).toLowerCase();
133
149
  const normalized = raw.replace(/[-:\s]/g, "_");
134
150
  if (!["exact", "exact_gasfree", "exact_permit", "permit2", "exact_permit2"].includes(normalized)) {
@@ -151,6 +167,33 @@ function normalizePaymentProtocol(config, file) {
151
167
  delete config.operator.assetTransferMethod;
152
168
  }
153
169
  }
170
+ function validatePaymentCapabilities(config, file) {
171
+ const symbols = config.operator.currencies?.usd ?? ["USDT"];
172
+ if (!Array.isArray(symbols) || !symbols.length || symbols.some(symbol => typeof symbol !== "string" || !symbol.trim())) {
173
+ throw new Error(`${file}: operator.currencies.usd must be a non-empty string array`);
174
+ }
175
+ if (new Set(symbols.map(symbol => symbol.toUpperCase())).size !== symbols.length)
176
+ throw new Error(`${file}: operator.currencies.usd must not contain duplicates`);
177
+ for (const symbol of symbols)
178
+ getToken(config.operator.network, symbol);
179
+ const recipient = config.recipients?.[config.operator.recipient]?.account ?? config.operator.recipient;
180
+ assertString(recipient, `${file}: resolved recipient`);
181
+ const validRecipient = config.operator.network.startsWith("tron:")
182
+ ? /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(recipient)
183
+ : /^0x[0-9a-fA-F]{40}$/.test(recipient);
184
+ if (!validRecipient)
185
+ throw new Error(`${file}: operator.recipient must be a valid address or a resolvable recipient alias`);
186
+ for (const endpoint of config.endpoints ?? []) {
187
+ if (!endpoint.metering)
188
+ continue;
189
+ const prices = [endpoint.metering.dimensions?.[0]?.tiers?.[0]?.price_usd,
190
+ ...(endpoint.metering.variants ?? []).map(variant => variant.dimensions?.[0]?.tiers?.[0]?.price_usd)]
191
+ .filter((price) => typeof price === "number" && price > 0);
192
+ for (const price of prices)
193
+ if (!paymentRequirements(config, price).length)
194
+ throw new Error(`${file}: paid endpoint cannot generate payment requirements`);
195
+ }
196
+ }
154
197
  export function loadProviders(providerPath) {
155
198
  const stat = fs.statSync(providerPath);
156
199
  const files = stat.isDirectory()
@@ -174,6 +217,8 @@ export function endpointFor(provider, method, routePath) {
174
217
  return provider.endpoints?.find(endpoint => endpoint.method.toUpperCase() === method.toUpperCase() && pathMatches(endpoint.path, routePath));
175
218
  }
176
219
  function pathMatches(template, routePath) {
220
+ if (!routePath.startsWith("/") || routePath.startsWith("//") || routePath.includes("\\") || routePath.includes("\0") || /%(?:2f|5c)/i.test(routePath))
221
+ return false;
177
222
  const templateParts = template.split("/").filter(Boolean);
178
223
  const routeParts = routePath.split("/").filter(Boolean);
179
224
  if (templateParts.length !== routeParts.length)
@@ -0,0 +1,3 @@
1
+ export { endpointFor, loadProvider, loadProviders, paymentRequirements, priceUsd, } from "./config.js";
2
+ export { getToken, normalizeNetwork, toSmallestUnit, TOKENS, } from "./tokens.js";
3
+ export { buildGatewayCatalog, providerCatalogProjection, providerPaymentAssets } from "./catalog.js";
@@ -1,4 +1,5 @@
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";
@@ -25,13 +26,16 @@ function positiveIntegerEnv(name, fallback) {
25
26
  if (!/^\d+$/.test(raw))
26
27
  throw new Error(`${name} must be a positive integer`);
27
28
  const value = Number(raw);
28
- if (!Number.isSafeInteger(value) || value <= 0)
29
- throw new Error(`${name} must be a positive integer`);
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`);
30
31
  return value;
31
32
  }
32
33
  const MAX_BODY_BYTES = positiveIntegerEnv("X402_GATEWAY_MAX_BODY_BYTES", 1_000_000);
33
34
  const FACILITATOR_TIMEOUT_MS = positiveIntegerEnv("X402_GATEWAY_FACILITATOR_TIMEOUT_MS", 10_000);
34
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);
35
39
  const STRIP_REQUEST_HEADERS = new Set([
36
40
  "host",
37
41
  "connection",
@@ -49,6 +53,8 @@ const STRIP_REQUEST_HEADERS = new Set([
49
53
  "payment-signature",
50
54
  "payment-required",
51
55
  "x-payment-required",
56
+ "payment-response",
57
+ "x-payment-response",
52
58
  "accept-encoding",
53
59
  ]);
54
60
  const STRIP_RESPONSE_HEADERS = new Set([
@@ -73,6 +79,7 @@ function createMetrics() {
73
79
  verifyFailures: 0,
74
80
  settleFailures: 0,
75
81
  upstreamFailures: 0,
82
+ rejectedRequests: 0,
76
83
  };
77
84
  }
78
85
  async function readBody(request) {
@@ -88,14 +95,17 @@ async function readBody(request) {
88
95
  return Buffer.concat(chunks);
89
96
  }
90
97
  function json(response, status, body, extraHeaders = {}) {
91
- 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 });
92
99
  response.end(JSON.stringify(body));
93
100
  }
94
101
  async function fetchWithTimeout(url, init, timeoutMs, label = "upstream") {
95
102
  const controller = new AbortController();
96
103
  const timer = setTimeout(() => controller.abort(), timeoutMs);
97
104
  try {
98
- 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;
99
109
  }
100
110
  catch (error) {
101
111
  if (error instanceof Error && error.name === "AbortError")
@@ -106,13 +116,30 @@ async function fetchWithTimeout(url, init, timeoutMs, label = "upstream") {
106
116
  clearTimeout(timer);
107
117
  }
108
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
+ }
109
136
  async function facilitatorPost(entry, path, body) {
110
137
  const headers = { "content-type": "application/json" };
111
138
  if (entry.facilitatorApiKey) {
112
139
  headers["x-api-key"] = entry.facilitatorApiKey;
113
140
  }
114
- const response = await fetchWithTimeout(new URL(path, entry.facilitatorUrl), { method: "POST", headers, body: JSON.stringify(body) }, FACILITATOR_TIMEOUT_MS, "facilitator");
115
- 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");
116
143
  let data = {};
117
144
  try {
118
145
  data = text ? JSON.parse(text) : {};
@@ -173,19 +200,19 @@ function isVerifySuccess(verify) {
173
200
  return verify?.valid === true || verify?.isValid === true;
174
201
  }
175
202
  function isSettleSuccess(settle) {
176
- if (settle?.success === false || settle?.settled === false)
177
- return false;
178
- return (settle?.success === true ||
179
- settle?.settled === true ||
180
- (typeof settle?.transaction === "string" && settle.transaction.length > 0) ||
181
- (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;
182
204
  }
183
205
  function isAdminAllowed(request) {
184
206
  const token = process.env.X402_GATEWAY_ADMIN_TOKEN;
185
207
  if (!token)
186
208
  return process.env.X402_GATEWAY_ADMIN_ALLOW_PUBLIC === "true";
187
209
  const auth = request.headers.authorization ?? "";
188
- 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";
189
216
  }
190
217
  function requestParams(url, body, request) {
191
218
  const params = {};
@@ -212,11 +239,12 @@ function requestParams(url, body, request) {
212
239
  }
213
240
  function upstreamHeaders(request, entry) {
214
241
  const headersOut = new Headers();
242
+ const connectionHeaders = new Set(String(request.headers.connection ?? "").split(",").map(value => value.trim().toLowerCase()).filter(Boolean));
215
243
  for (const [key, value] of Object.entries(request.headers)) {
216
244
  if (!value)
217
245
  continue;
218
246
  const lower = key.toLowerCase();
219
- if (STRIP_REQUEST_HEADERS.has(lower))
247
+ if (STRIP_REQUEST_HEADERS.has(lower) || connectionHeaders.has(lower))
220
248
  continue;
221
249
  headersOut.set(key, Array.isArray(value) ? value.join(",") : value);
222
250
  }
@@ -231,7 +259,14 @@ function upstreamHeaders(request, entry) {
231
259
  }
232
260
  function upstreamUrl(entry, request, routePath) {
233
261
  const sourceUrl = new URL(request.url ?? "/", "http://local");
234
- 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");
235
270
  const auth = entry.config.routing?.auth;
236
271
  const value = auth?.value ?? (auth?.value_from_env ? process.env[auth.value_from_env] : undefined);
237
272
  if (auth?.method === "query_param" && value) {
@@ -263,14 +298,24 @@ async function forward(metrics, entry, request, response, routePath, body, payme
263
298
  });
264
299
  if (paymentResponse)
265
300
  responseHeaders[headers.response] = encodeResponse(paymentResponse);
266
- const responseBody = Buffer.from(await upstreamResponse.arrayBuffer());
301
+ let responseBody;
302
+ try {
303
+ responseBody = await readResponseBytes(upstreamResponse);
304
+ }
305
+ catch (error) {
306
+ metrics.upstreamFailures += 1;
307
+ throw error;
308
+ }
267
309
  response.writeHead(upstreamResponse.status, responseHeaders);
268
310
  response.end(responseBody);
269
311
  }
270
312
  export function createGatewayServer(providers) {
271
313
  const publicBaseUrl = configuredPublicBaseUrl();
272
314
  const metrics = createMetrics();
273
- return http.createServer(async (request, response) => {
315
+ const rateLimits = new Map();
316
+ let activeRequests = 0;
317
+ const server = http.createServer(async (request, response) => {
318
+ let countedActive = false;
274
319
  try {
275
320
  metrics.requests += 1;
276
321
  const url = new URL(request.url ?? "/", "http://local");
@@ -316,6 +361,7 @@ export function createGatewayServer(providers) {
316
361
  `x402_gateway_verify_failures_total ${metrics.verifyFailures}`,
317
362
  `x402_gateway_settle_failures_total ${metrics.settleFailures}`,
318
363
  `x402_gateway_upstream_failures_total ${metrics.upstreamFailures}`,
364
+ `x402_gateway_rejected_requests_total ${metrics.rejectedRequests}`,
319
365
  "",
320
366
  ].join("\n"));
321
367
  return;
@@ -336,8 +382,30 @@ export function createGatewayServer(providers) {
336
382
  json(response, 404, { error: "endpoint not found" });
337
383
  return;
338
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;
339
404
  const body = await readBody(request);
340
- 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");
341
409
  if (!requirements.length) {
342
410
  await forward(metrics, entry, request, response, routePath, body);
343
411
  return;
@@ -416,5 +484,14 @@ export function createGatewayServer(providers) {
416
484
  console.error(error);
417
485
  json(response, 500, { error: "internal server error" });
418
486
  }
487
+ finally {
488
+ if (countedActive)
489
+ activeRequests -= 1;
490
+ }
419
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;
420
497
  }
@@ -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
+ }