@bankofai/x402-cli 1.0.1-beta.4 → 1.0.1-beta.5
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 +20 -2
- package/dist/cli.js +18 -6
- package/dist/gateway/config.js +12 -6
- package/dist/gateway/server.js +67 -19
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -13,6 +13,15 @@ from the payment token, so the payer does not need TRX.
|
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
|
+
Install the CLI package:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @bankofai/x402-cli
|
|
20
|
+
x402-cli --version
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For repository development:
|
|
24
|
+
|
|
16
25
|
```bash
|
|
17
26
|
npm install
|
|
18
27
|
npm run build
|
|
@@ -61,8 +70,8 @@ The server exposes:
|
|
|
61
70
|
|
|
62
71
|
- `GET /health`
|
|
63
72
|
- `GET /.well-known/x402`
|
|
64
|
-
-
|
|
65
|
-
-
|
|
73
|
+
- `/pay` returns `402 Payment Required` without a payment signature
|
|
74
|
+
- The signed retry uses the same HTTP method, then verifies and settles with the facilitator
|
|
66
75
|
|
|
67
76
|
### Pay
|
|
68
77
|
|
|
@@ -75,6 +84,9 @@ node dist/cli.js pay http://127.0.0.1:4020/pay \
|
|
|
75
84
|
--token USDT
|
|
76
85
|
```
|
|
77
86
|
|
|
87
|
+
For automated or unfamiliar endpoints, set `--max-amount` or
|
|
88
|
+
`--max-raw-amount` before allowing the CLI to sign a payment.
|
|
89
|
+
|
|
78
90
|
Pay a TRON GasFree endpoint (the CLI normally selects this automatically from
|
|
79
91
|
the server challenge):
|
|
80
92
|
|
|
@@ -90,6 +102,12 @@ Use `--gasfree-api-url <url>` or `X402_GASFREE_API_URL` to override the SDK's
|
|
|
90
102
|
default relayer endpoint.
|
|
91
103
|
|
|
92
104
|
For EVM networks use `EVM_PRIVATE_KEY` or `PRIVATE_KEY`.
|
|
105
|
+
Prefer environment variables over `--private-key` in shared environments,
|
|
106
|
+
because command-line arguments may be visible to other local processes.
|
|
107
|
+
|
|
108
|
+
If the gateway settles a payment but the upstream request fails, JSON error
|
|
109
|
+
output includes `error.details.paymentResponse` for reconciliation. Do not retry
|
|
110
|
+
such a request blindly; inspect the transaction and provider behavior first.
|
|
93
111
|
|
|
94
112
|
### Roundtrip
|
|
95
113
|
|
package/dist/cli.js
CHANGED
|
@@ -18,11 +18,13 @@ class CliError extends Error {
|
|
|
18
18
|
code;
|
|
19
19
|
hint;
|
|
20
20
|
exitCode;
|
|
21
|
-
|
|
21
|
+
details;
|
|
22
|
+
constructor(code, message, hint, exitCode = 1, details) {
|
|
22
23
|
super(message);
|
|
23
24
|
this.code = code;
|
|
24
25
|
this.hint = hint;
|
|
25
26
|
this.exitCode = exitCode;
|
|
27
|
+
this.details = details;
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
function parseArgs(argv) {
|
|
@@ -144,6 +146,8 @@ function emit(args) {
|
|
|
144
146
|
process.stderr.write(` ${args.error.message}\n`);
|
|
145
147
|
if (args.error.hint)
|
|
146
148
|
process.stderr.write(` hint: ${args.error.hint}\n`);
|
|
149
|
+
if (args.error.details !== undefined)
|
|
150
|
+
process.stderr.write(` details: ${JSON.stringify(args.error.details)}\n`);
|
|
147
151
|
return;
|
|
148
152
|
}
|
|
149
153
|
const suffix = [args.network, args.scheme].filter(Boolean).join(" ");
|
|
@@ -171,6 +175,7 @@ function classify(error) {
|
|
|
171
175
|
code: error.code,
|
|
172
176
|
message,
|
|
173
177
|
hint: error.hint,
|
|
178
|
+
details: error.details,
|
|
174
179
|
};
|
|
175
180
|
}
|
|
176
181
|
const lower = message.toLowerCase();
|
|
@@ -737,7 +742,7 @@ async function fetchWithTimeout(input, init = {}, timeout = DEFAULT_TIMEOUT_MS,
|
|
|
737
742
|
return await fetch(input, { ...init, signal: controller.signal });
|
|
738
743
|
}
|
|
739
744
|
catch (error) {
|
|
740
|
-
if (error
|
|
745
|
+
if (error instanceof Error && error.name === "AbortError")
|
|
741
746
|
throw new Error(`${label} timed out after ${timeout}ms`);
|
|
742
747
|
throw error;
|
|
743
748
|
}
|
|
@@ -1182,7 +1187,13 @@ function selectRequirement(accepts, options) {
|
|
|
1182
1187
|
if (scheme && scheme !== req.scheme)
|
|
1183
1188
|
return false;
|
|
1184
1189
|
if (token) {
|
|
1185
|
-
|
|
1190
|
+
let tokenInfo;
|
|
1191
|
+
try {
|
|
1192
|
+
tokenInfo = getToken(req.network, token);
|
|
1193
|
+
}
|
|
1194
|
+
catch {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1186
1197
|
if (tokenInfo.address.toLowerCase() !== req.asset.toLowerCase())
|
|
1187
1198
|
return false;
|
|
1188
1199
|
}
|
|
@@ -1266,7 +1277,9 @@ async function pay(url, options) {
|
|
|
1266
1277
|
};
|
|
1267
1278
|
if (!paid.ok) {
|
|
1268
1279
|
const retryAfter = paid.headers.get("retry-after");
|
|
1269
|
-
throw new
|
|
1280
|
+
throw new CliError(paid.status === 429 ? "RATE_LIMITED" : "HTTP_ERROR", `HTTP ${paid.status} from ${url}${retryAfter ? ` (retry after ${retryAfter}s)` : ""}: ${typeof body === "string" ? body.slice(0, 500) : JSON.stringify(body).slice(0, 500)}`, paymentResponse
|
|
1281
|
+
? "The gateway reports that payment was settled; retain paymentResponse for support or reconciliation."
|
|
1282
|
+
: "Inspect the HTTP response and retry only when it is safe to do so.", 1, result);
|
|
1270
1283
|
}
|
|
1271
1284
|
emit({
|
|
1272
1285
|
command: "client",
|
|
@@ -1278,8 +1291,7 @@ async function pay(url, options) {
|
|
|
1278
1291
|
}
|
|
1279
1292
|
async function roundtrip(options) {
|
|
1280
1293
|
const port = Number(opt(options, "port", "4020"));
|
|
1281
|
-
serve(options);
|
|
1282
|
-
await delay(500);
|
|
1294
|
+
await serve(options);
|
|
1283
1295
|
await pay(`http://127.0.0.1:${port}/pay`, options);
|
|
1284
1296
|
process.exit(0);
|
|
1285
1297
|
}
|
package/dist/gateway/config.js
CHANGED
|
@@ -104,15 +104,21 @@ export function loadProvider(file) {
|
|
|
104
104
|
validateProvider(config, file);
|
|
105
105
|
config.operator.network = normalizeNetwork(config.operator.network);
|
|
106
106
|
normalizePaymentProtocol(config, file);
|
|
107
|
+
const facilitatorUrl = config.operator.facilitator_url ||
|
|
108
|
+
process.env.X402_FACILITATOR_URL ||
|
|
109
|
+
process.env.FACILITATOR_URL ||
|
|
110
|
+
"https://facilitator.bankofai.io";
|
|
111
|
+
assertHttpUrl(facilitatorUrl, `${file}: facilitator URL`);
|
|
112
|
+
const configuredApiKeyEnv = config.operator.facilitator_api_key_env;
|
|
113
|
+
if (configuredApiKeyEnv && !process.env[configuredApiKeyEnv]) {
|
|
114
|
+
throw new Error(`${file}: environment variable ${configuredApiKeyEnv} is not set`);
|
|
115
|
+
}
|
|
107
116
|
return {
|
|
108
117
|
config,
|
|
109
|
-
facilitatorUrl
|
|
110
|
-
process.env.X402_FACILITATOR_URL ||
|
|
111
|
-
process.env.FACILITATOR_URL ||
|
|
112
|
-
"https://facilitator.bankofai.io",
|
|
118
|
+
facilitatorUrl,
|
|
113
119
|
facilitatorApiKey: config.operator.facilitator_api_key ||
|
|
114
|
-
(
|
|
115
|
-
? process.env[
|
|
120
|
+
(configuredApiKeyEnv
|
|
121
|
+
? process.env[configuredApiKeyEnv]
|
|
116
122
|
: undefined) ||
|
|
117
123
|
process.env.X402_FACILITATOR_API_KEY ||
|
|
118
124
|
process.env.FACILITATOR_API_KEY,
|
package/dist/gateway/server.js
CHANGED
|
@@ -18,9 +18,20 @@ class RequestTooLargeError extends HttpError {
|
|
|
18
18
|
super(413, "request body too large");
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
21
|
+
function positiveIntegerEnv(name, fallback) {
|
|
22
|
+
const raw = process.env[name];
|
|
23
|
+
if (raw === undefined || raw === "")
|
|
24
|
+
return fallback;
|
|
25
|
+
if (!/^\d+$/.test(raw))
|
|
26
|
+
throw new Error(`${name} must be a positive integer`);
|
|
27
|
+
const value = Number(raw);
|
|
28
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
29
|
+
throw new Error(`${name} must be a positive integer`);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
const MAX_BODY_BYTES = positiveIntegerEnv("X402_GATEWAY_MAX_BODY_BYTES", 1_000_000);
|
|
33
|
+
const FACILITATOR_TIMEOUT_MS = positiveIntegerEnv("X402_GATEWAY_FACILITATOR_TIMEOUT_MS", 10_000);
|
|
34
|
+
const UPSTREAM_TIMEOUT_MS = positiveIntegerEnv("X402_GATEWAY_UPSTREAM_TIMEOUT_MS", 30_000);
|
|
24
35
|
const STRIP_REQUEST_HEADERS = new Set([
|
|
25
36
|
"host",
|
|
26
37
|
"connection",
|
|
@@ -48,14 +59,22 @@ const STRIP_RESPONSE_HEADERS = new Set([
|
|
|
48
59
|
"authorization",
|
|
49
60
|
"proxy-authorization",
|
|
50
61
|
"set-cookie",
|
|
62
|
+
"payment-required",
|
|
63
|
+
"x-payment-required",
|
|
64
|
+
"payment-signature",
|
|
65
|
+
"x-payment",
|
|
66
|
+
"payment-response",
|
|
67
|
+
"x-payment-response",
|
|
51
68
|
]);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
function createMetrics() {
|
|
70
|
+
return {
|
|
71
|
+
requests: 0,
|
|
72
|
+
paidRequests: 0,
|
|
73
|
+
verifyFailures: 0,
|
|
74
|
+
settleFailures: 0,
|
|
75
|
+
upstreamFailures: 0,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
59
78
|
async function readBody(request) {
|
|
60
79
|
const chunks = [];
|
|
61
80
|
let total = 0;
|
|
@@ -79,7 +98,7 @@ async function fetchWithTimeout(url, init, timeoutMs, label = "upstream") {
|
|
|
79
98
|
return await fetch(url, { ...init, signal: controller.signal });
|
|
80
99
|
}
|
|
81
100
|
catch (error) {
|
|
82
|
-
if (error
|
|
101
|
+
if (error instanceof Error && error.name === "AbortError")
|
|
83
102
|
throw new HttpError(504, `${label} request timed out`);
|
|
84
103
|
throw error;
|
|
85
104
|
}
|
|
@@ -112,12 +131,25 @@ async function facilitatorPost(entry, path, body) {
|
|
|
112
131
|
}
|
|
113
132
|
return data;
|
|
114
133
|
}
|
|
115
|
-
function
|
|
134
|
+
function configuredPublicBaseUrl() {
|
|
135
|
+
const value = process.env.X402_GATEWAY_PUBLIC_BASE_URL?.trim();
|
|
136
|
+
if (!value)
|
|
137
|
+
return undefined;
|
|
138
|
+
try {
|
|
139
|
+
const url = new URL(value);
|
|
140
|
+
if (!["http:", "https:"].includes(url.protocol))
|
|
141
|
+
throw new Error("unsupported protocol");
|
|
142
|
+
return `${value.replace(/\/+$/, "")}/`;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
throw new Error("X402_GATEWAY_PUBLIC_BASE_URL must be a valid http(s) URL");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function resourceUrl(url, publicBaseUrl) {
|
|
116
149
|
const path = `${url.pathname}${url.search}`;
|
|
117
|
-
const publicBaseUrl = process.env.X402_GATEWAY_PUBLIC_BASE_URL?.trim();
|
|
118
150
|
if (!publicBaseUrl)
|
|
119
151
|
return path;
|
|
120
|
-
return new URL(path,
|
|
152
|
+
return new URL(path, publicBaseUrl).toString();
|
|
121
153
|
}
|
|
122
154
|
function logFacilitatorFailure(entry, path, response, body, data) {
|
|
123
155
|
const requirement = body?.paymentRequirements;
|
|
@@ -205,7 +237,7 @@ function upstreamUrl(entry, request, routePath) {
|
|
|
205
237
|
}
|
|
206
238
|
return upstream;
|
|
207
239
|
}
|
|
208
|
-
async function forward(entry, request, response, routePath, body, paymentResponse) {
|
|
240
|
+
async function forward(metrics, entry, request, response, routePath, body, paymentResponse) {
|
|
209
241
|
const upstream = upstreamUrl(entry, request, routePath);
|
|
210
242
|
let upstreamResponse;
|
|
211
243
|
try {
|
|
@@ -229,10 +261,13 @@ async function forward(entry, request, response, routePath, body, paymentRespons
|
|
|
229
261
|
});
|
|
230
262
|
if (paymentResponse)
|
|
231
263
|
responseHeaders[headers.response] = encodeResponse(paymentResponse);
|
|
264
|
+
const responseBody = Buffer.from(await upstreamResponse.arrayBuffer());
|
|
232
265
|
response.writeHead(upstreamResponse.status, responseHeaders);
|
|
233
|
-
response.end(
|
|
266
|
+
response.end(responseBody);
|
|
234
267
|
}
|
|
235
268
|
export function createGatewayServer(providers) {
|
|
269
|
+
const publicBaseUrl = configuredPublicBaseUrl();
|
|
270
|
+
const metrics = createMetrics();
|
|
236
271
|
return http.createServer(async (request, response) => {
|
|
237
272
|
try {
|
|
238
273
|
metrics.requests += 1;
|
|
@@ -302,7 +337,7 @@ export function createGatewayServer(providers) {
|
|
|
302
337
|
const body = await readBody(request);
|
|
303
338
|
const requirements = paymentRequirements(entry.config, priceUsd(endpoint, requestParams(url, body, request)));
|
|
304
339
|
if (!requirements.length) {
|
|
305
|
-
await forward(entry, request, response, routePath, body);
|
|
340
|
+
await forward(metrics, entry, request, response, routePath, body);
|
|
306
341
|
return;
|
|
307
342
|
}
|
|
308
343
|
const paymentHeader = request.headers[headers.signature.toLowerCase()];
|
|
@@ -311,7 +346,7 @@ export function createGatewayServer(providers) {
|
|
|
311
346
|
const challenge = {
|
|
312
347
|
x402Version: 2,
|
|
313
348
|
error: "Payment required",
|
|
314
|
-
resource: { url: resourceUrl(url) },
|
|
349
|
+
resource: { url: resourceUrl(url, publicBaseUrl) },
|
|
315
350
|
accepts,
|
|
316
351
|
};
|
|
317
352
|
json(response, 402, challenge, { [headers.required]: encodeRequired(challenge) });
|
|
@@ -356,7 +391,20 @@ export function createGatewayServer(providers) {
|
|
|
356
391
|
return;
|
|
357
392
|
}
|
|
358
393
|
metrics.paidRequests += 1;
|
|
359
|
-
|
|
394
|
+
try {
|
|
395
|
+
await forward(metrics, entry, request, response, routePath, body, settle);
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
const status = error instanceof HttpError ? error.status : 502;
|
|
399
|
+
const extraHeaders = error instanceof HttpError ? error.responseHeaders : {};
|
|
400
|
+
json(response, status, {
|
|
401
|
+
error: "upstream failed after payment settlement",
|
|
402
|
+
settled: true,
|
|
403
|
+
}, {
|
|
404
|
+
...extraHeaders,
|
|
405
|
+
[headers.response]: encodeResponse(settle),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
360
408
|
}
|
|
361
409
|
catch (error) {
|
|
362
410
|
if (error instanceof HttpError) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bankofai/x402-cli",
|
|
3
|
-
"version": "1.0.1-beta.
|
|
3
|
+
"version": "1.0.1-beta.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@bankofai/x402-core": "1.0.1-beta.4",
|
|
24
24
|
"@bankofai/x402-evm": "1.0.1-beta.4",
|
|
25
|
-
"@bankofai/x402-gateway": "1.0.1-beta.
|
|
25
|
+
"@bankofai/x402-gateway": "1.0.1-beta.5",
|
|
26
26
|
"@bankofai/x402-tron": "1.0.1-beta.4",
|
|
27
27
|
"tronweb": "6.4.0",
|
|
28
28
|
"viem": "^2.55.0",
|
|
@@ -32,5 +32,8 @@
|
|
|
32
32
|
"@types/node": "^24.10.1",
|
|
33
33
|
"tsx": "^4.20.6",
|
|
34
34
|
"typescript": "^5.9.3"
|
|
35
|
+
},
|
|
36
|
+
"overrides": {
|
|
37
|
+
"ws": "8.21.0"
|
|
35
38
|
}
|
|
36
39
|
}
|