xytara 3.1.1 → 3.1.2
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/.env.example +8 -1
- package/README.md +4 -0
- package/REAL_PAYMENT_SETUP.md +29 -1
- package/RELEASE_NOTES.md +10 -13
- package/examples/generated.carried-handoff.json +30 -0
- package/lib/command_flow.js +8 -3
- package/lib/payment_config.js +46 -0
- package/lib/payment_verification.js +51 -3
- package/lib/x402_facilitator.js +155 -0
- package/package.json +3 -2
- package/scripts/verify_x402_facilitator.js +174 -0
- package/server.js +34 -18
package/.env.example
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
XYTARA_HOST=127.0.0.1
|
|
2
2
|
XYTARA_PORT=4320
|
|
3
3
|
XYTARA_BASE_URL=http://127.0.0.1:4320
|
|
4
|
-
# local_signed is the default;
|
|
4
|
+
# local_signed is the default; presence (accepts any non-empty payment header) is for local demos;
|
|
5
|
+
# x402_facilitator is trustless on-chain pay-per-call (see REAL_PAYMENT_SETUP.md and the vars below)
|
|
5
6
|
XYTARA_PAYMENT_VERIFICATION_MODE=local_signed
|
|
7
|
+
# used only when mode is x402_facilitator:
|
|
8
|
+
XYTARA_X402_ACCEPTS_JSON=[]
|
|
9
|
+
XYTARA_X402_FACILITATOR_URL=https://x402.org/facilitator
|
|
10
|
+
XYTARA_X402_FACILITATOR_HEADERS_JSON={}
|
|
11
|
+
XYTARA_X402_FACILITATOR_TIMEOUT_MS=15000
|
|
12
|
+
XYTARA_X402_SETTLE=true
|
|
6
13
|
XYTARA_PAYMENT_MAX_AGE_SECONDS=300
|
|
7
14
|
XYTARA_WALLET_REGISTRY_JSON=[]
|
|
8
15
|
XYTARA_TREASURY_DESTINATION_ID=
|
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
`xytara` is the runtime and payment layer for the public two-repo stack.
|
|
4
4
|
|
|
5
|
+
> New to the code? Read [`ARCHITECTURE.md`](ARCHITECTURE.md) first. It separates
|
|
6
|
+
> the small execution-and-settlement engine (~9,300 lines) from the large
|
|
7
|
+
> machine-readable breadth catalog, so the real runtime is legible in one sitting.
|
|
8
|
+
|
|
5
9
|
It exists to let agents, developers, and systems:
|
|
6
10
|
|
|
7
11
|
- discover tasks and workflows
|
package/REAL_PAYMENT_SETUP.md
CHANGED
|
@@ -4,7 +4,7 @@ This guide explains how to move `xytara` from local placeholder payment acceptan
|
|
|
4
4
|
|
|
5
5
|
## What Changed
|
|
6
6
|
|
|
7
|
-
`xytara` supports
|
|
7
|
+
`xytara` supports three payment-acceptance modes. `local_signed` is the default; `presence` and `x402_facilitator` are opt-in.
|
|
8
8
|
|
|
9
9
|
- `presence`
|
|
10
10
|
- development/demo mode only, enabled with `XYTARA_PAYMENT_VERIFICATION_MODE=presence`
|
|
@@ -17,6 +17,34 @@ This guide explains how to move `xytara` from local placeholder payment acceptan
|
|
|
17
17
|
- quote replay and nonce replay are blocked
|
|
18
18
|
- accepted payments are routed into your configured merchant treasury destination posture
|
|
19
19
|
|
|
20
|
+
- `x402_facilitator`
|
|
21
|
+
- trustless, on-chain, pay-per-call, the same mechanism proven live on the MCP executor
|
|
22
|
+
- the `/x402/commands/execute` and `/x402/mcp/tools/invoke` routes return a `402` that
|
|
23
|
+
advertises the configured `accepts` (network, asset, amount, payTo) in the
|
|
24
|
+
`PAYMENT-REQUIRED` header; a buyer signs a gasless x402 USDC authorization (EIP-3009)
|
|
25
|
+
- on the paid retry, a facilitator verifies the payment and settles it on-chain before any
|
|
26
|
+
work runs; the response carries a `PAYMENT-RESPONSE` settlement receipt
|
|
27
|
+
- fails closed: if the facilitator is unreachable or the payment does not settle, nothing runs
|
|
28
|
+
- the buyer needs no native gas token; the facilitator broadcasts the transfer
|
|
29
|
+
- enable with `XYTARA_PAYMENT_VERIFICATION_MODE=x402_facilitator` and the x402 env below:
|
|
30
|
+
|
|
31
|
+
```env
|
|
32
|
+
XYTARA_PAYMENT_VERIFICATION_MODE=x402_facilitator
|
|
33
|
+
# what buyers pay, advertised in the 402 (Base Sepolia test USDC shown):
|
|
34
|
+
XYTARA_X402_ACCEPTS_JSON=[{"scheme":"exact","network":"eip155:84532","asset":"0x036CbD53842c5426634e7929541eC2318f3dCF7e","amount":"1000","payTo":"<your-evm-address>","extra":{"name":"USDC","version":"2"},"maxTimeoutSeconds":60}]
|
|
35
|
+
XYTARA_X402_FACILITATOR_URL=https://x402.org/facilitator
|
|
36
|
+
# optional:
|
|
37
|
+
XYTARA_X402_FACILITATOR_HEADERS_JSON={}
|
|
38
|
+
XYTARA_X402_FACILITATOR_TIMEOUT_MS=15000
|
|
39
|
+
XYTARA_X402_SETTLE=true
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Confirm the facilitator settles every network you advertise, and verify the mode end to end
|
|
43
|
+
against a mock facilitator with `npm run verify:x402-facilitator`. The payment a buyer signs
|
|
44
|
+
is identical to the one in `services/mcp-execution-target/buyer-demo/` (an EIP-3009 USDC
|
|
45
|
+
authorization in a `PAYMENT-SIGNATURE` header); only the request body differs, since the main
|
|
46
|
+
runtime takes a command-execute body rather than an MCP JSON-RPC call.
|
|
47
|
+
|
|
20
48
|
## Minimum Production-Oriented Configuration
|
|
21
49
|
|
|
22
50
|
Set these environment variables:
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
`xytara` 3.1.
|
|
1
|
+
`xytara` 3.1.2 adds trustless, on-chain, pay-per-call settlement to the main runtime.
|
|
2
2
|
|
|
3
|
-
This release
|
|
3
|
+
This release brings the facilitator verify-and-settle mechanism already proven on the public MCP executor to the runtime itself, so every x402 route can settle a real payment on-chain before any work runs, not just the standalone tool server.
|
|
4
4
|
|
|
5
5
|
Highlights:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- the
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- the service verifier now covers unauthenticated mint, wrong operator tokens, credit overdraw, forged and unsigned checkout webhooks, replayed payment nonces, forged payment signatures, unregistered wallets, and query-token rejection; the MCP executor verifier runs against a mock facilitator
|
|
16
|
-
- the MCP executor publish workflow skips publishing when the registry already carries the current version
|
|
17
|
-
- bundled examples configure a local demo wallet and demo operator tokens explicitly instead of relying on open defaults
|
|
7
|
+
- adds a third payment-acceptance mode, `x402_facilitator`, enabled with `XYTARA_PAYMENT_VERIFICATION_MODE=x402_facilitator`
|
|
8
|
+
- when enabled, `/x402/commands/execute` and `/x402/mcp/tools/invoke` return a `402` that advertises the configured `accepts` (network, asset, amount, payTo) in the `PAYMENT-REQUIRED` header
|
|
9
|
+
- a buyer signs a gasless USDC authorization (EIP-3009); on the paid retry a facilitator verifies and settles it on-chain before any work runs, and the response carries a `PAYMENT-RESPONSE` settlement receipt
|
|
10
|
+
- fails closed: if the facilitator is unreachable or the payment does not settle, nothing runs and no result is returned
|
|
11
|
+
- the mode governs only the x402 routes; native and instant lanes fall back to `local_signed`, so no existing paid path is disabled
|
|
12
|
+
- keeps `local_signed` as the default and `presence` as the local-development opt-in
|
|
13
|
+
- adds `npm run verify:x402-facilitator`, an end-to-end check against a mock facilitator covering the advertised challenge, malformed and mismatched payments, facilitator rejection, failed settlement, and a settled success with receipt
|
|
14
|
+
- documents the mode and its environment in `REAL_PAYMENT_SETUP.md` and the architecture in `ARCHITECTURE.md`
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"handoff_bundle": {
|
|
3
|
+
"ok": true,
|
|
4
|
+
"handoff_kind": "interaction_result_package",
|
|
5
|
+
"result_package_ref": "result.spine.job.commitment",
|
|
6
|
+
"source_ref": "spine.job.commitment",
|
|
7
|
+
"committed_ref": "transaction.job.commitment",
|
|
8
|
+
"precommit_pack_ref": "precommit.job.commitment.readiness",
|
|
9
|
+
"transaction_id": "txn_7f5d7cd15658",
|
|
10
|
+
"receipt_id": "rcpt_2684569e5241",
|
|
11
|
+
"proof_ref": "proof_rcpt_2684569e5241",
|
|
12
|
+
"payment_protocol": "x402",
|
|
13
|
+
"settlement_mode": "evm_payment",
|
|
14
|
+
"treasury_destination_id": "treasury_acct_demo",
|
|
15
|
+
"task_refs": [
|
|
16
|
+
"jobs.quote",
|
|
17
|
+
"admission.preview",
|
|
18
|
+
"jobs.reserve",
|
|
19
|
+
"jobs.commit"
|
|
20
|
+
],
|
|
21
|
+
"delivery_count": 1,
|
|
22
|
+
"payment_ledger_count": 1,
|
|
23
|
+
"economic_consequence_handoff_bundle": null,
|
|
24
|
+
"seam_layers": [
|
|
25
|
+
"execution_consequence",
|
|
26
|
+
"payment_settlement_treasury",
|
|
27
|
+
"proof_handoff"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
package/lib/command_flow.js
CHANGED
|
@@ -311,9 +311,10 @@ async function executeCommandRequest(state, payload, headers, options) {
|
|
|
311
311
|
};
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
const paymentVerification = verifyPaymentAcceptance(state, headers || {}, requiredHeaders, quote, {
|
|
314
|
+
const paymentVerification = await verifyPaymentAcceptance(state, headers || {}, requiredHeaders, quote, {
|
|
315
315
|
payment_protocol: opts.paymentProtocol || (payload && payload.payment_protocol) || null,
|
|
316
|
-
authorization_scheme: opts.authorizationScheme || null
|
|
316
|
+
authorization_scheme: opts.authorizationScheme || null,
|
|
317
|
+
resource_url: opts.resourceUrl || null
|
|
317
318
|
});
|
|
318
319
|
if (!paymentVerification.ok) {
|
|
319
320
|
return {
|
|
@@ -321,6 +322,7 @@ async function executeCommandRequest(state, payload, headers, options) {
|
|
|
321
322
|
payload: {
|
|
322
323
|
ok: false,
|
|
323
324
|
reason: paymentVerification.reason,
|
|
325
|
+
...(paymentVerification.detail ? { detail: paymentVerification.detail } : {}),
|
|
324
326
|
quote
|
|
325
327
|
}
|
|
326
328
|
};
|
|
@@ -345,7 +347,10 @@ async function executeCommandRequest(state, payload, headers, options) {
|
|
|
345
347
|
payload: createCommandResult(state, payload, quote, {
|
|
346
348
|
paymentVerification,
|
|
347
349
|
externalExecution: externalExecutionOptions.externalExecution || null
|
|
348
|
-
})
|
|
350
|
+
}),
|
|
351
|
+
...(paymentVerification.payment_response_base64
|
|
352
|
+
? { paymentResponse: paymentVerification.payment_response_base64 }
|
|
353
|
+
: {})
|
|
349
354
|
};
|
|
350
355
|
}
|
|
351
356
|
|
package/lib/payment_config.js
CHANGED
|
@@ -121,6 +121,7 @@ function loadPaymentConfig() {
|
|
|
121
121
|
verification_mode_input: verificationModeInput,
|
|
122
122
|
payment_max_age_seconds: Number.isFinite(maxAgeSeconds) && maxAgeSeconds > 0 ? maxAgeSeconds : 300,
|
|
123
123
|
wallet_registry: loadWalletRegistry(),
|
|
124
|
+
x402: loadX402FacilitatorConfig(),
|
|
124
125
|
treasury_destination_id: String(process.env.XYTARA_TREASURY_DESTINATION_ID || "").trim() || null,
|
|
125
126
|
treasury_destination_kind: String(process.env.XYTARA_TREASURY_DESTINATION_KIND || "").trim() || null,
|
|
126
127
|
treasury_account_id: String(process.env.XYTARA_TREASURY_ACCOUNT_ID || "").trim() || null,
|
|
@@ -129,6 +130,51 @@ function loadPaymentConfig() {
|
|
|
129
130
|
};
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
// Configuration for the x402_facilitator verification mode: the on-chain, trustless
|
|
134
|
+
// pay-per-call path. The advertised accepts tell a buyer how to pay; the facilitator
|
|
135
|
+
// verifies and settles the signed payment before any work runs.
|
|
136
|
+
function loadX402FacilitatorConfig() {
|
|
137
|
+
let accepts = [];
|
|
138
|
+
const rawAccepts = String(process.env.XYTARA_X402_ACCEPTS_JSON || "").trim();
|
|
139
|
+
if (rawAccepts) {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(rawAccepts);
|
|
142
|
+
if (Array.isArray(parsed)) {
|
|
143
|
+
accepts = parsed
|
|
144
|
+
.map((entry) => ({
|
|
145
|
+
scheme: String((entry && entry.scheme) || "exact").trim(),
|
|
146
|
+
network: String((entry && entry.network) || "").trim(),
|
|
147
|
+
asset: String((entry && entry.asset) || "").trim(),
|
|
148
|
+
amount: String((entry && entry.amount) || "").trim(),
|
|
149
|
+
payTo: String((entry && entry.payTo) || "").trim(),
|
|
150
|
+
...(entry && entry.extra && typeof entry.extra === "object" ? { extra: entry.extra } : {}),
|
|
151
|
+
...(Number(entry && entry.maxTimeoutSeconds) > 0 ? { maxTimeoutSeconds: Number(entry.maxTimeoutSeconds) } : {})
|
|
152
|
+
}))
|
|
153
|
+
.filter((entry) => entry.network && entry.asset && entry.amount && entry.payTo);
|
|
154
|
+
}
|
|
155
|
+
} catch (_) {
|
|
156
|
+
accepts = [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
let headers = {};
|
|
160
|
+
const rawHeaders = String(process.env.XYTARA_X402_FACILITATOR_HEADERS_JSON || "").trim();
|
|
161
|
+
if (rawHeaders) {
|
|
162
|
+
try {
|
|
163
|
+
const parsed = JSON.parse(rawHeaders);
|
|
164
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) headers = parsed;
|
|
165
|
+
} catch (_) {
|
|
166
|
+
headers = {};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
accepts,
|
|
171
|
+
facilitator_url: String(process.env.XYTARA_X402_FACILITATOR_URL || "https://x402.org/facilitator").trim().replace(/\/$/, ""),
|
|
172
|
+
facilitator_headers: headers,
|
|
173
|
+
facilitator_timeout_ms: Math.max(1000, Number(process.env.XYTARA_X402_FACILITATOR_TIMEOUT_MS || 15000) || 15000),
|
|
174
|
+
settle: String(process.env.XYTARA_X402_SETTLE || "true").trim().toLowerCase() !== "false"
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
132
178
|
function buildConfiguredTreasuryDestination(accountId, settlementMode) {
|
|
133
179
|
const config = loadPaymentConfig();
|
|
134
180
|
const normalizedSettlementMode = normalizeSettlementMode(settlementMode);
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require("crypto");
|
|
4
4
|
const { loadPaymentConfig } = require("./payment_config");
|
|
5
|
+
const {
|
|
6
|
+
createFacilitatorClient,
|
|
7
|
+
verifyX402Payment,
|
|
8
|
+
settleX402Payment,
|
|
9
|
+
encodePaymentResponse
|
|
10
|
+
} = require("./x402_facilitator");
|
|
5
11
|
const { parseL402Authorization, verifyL402AccessToken } = require("./l402_payment_front");
|
|
6
12
|
const { parseBoltAuthorization, verifyBoltAccessToken } = require("./bolt_payment_front");
|
|
7
13
|
const { parseStripeMppAuthorization, verifyStripeMppAccessToken } = require("./stripe_mpp_payment_front");
|
|
@@ -157,10 +163,48 @@ function verifyPaymentPayloadAgainstQuote(paymentPayload, quote, walletId, maxAg
|
|
|
157
163
|
return { ok: true, age_seconds: ageSeconds };
|
|
158
164
|
}
|
|
159
165
|
|
|
160
|
-
function verifyPaymentAcceptance(state, headers, requiredHeaders, quote, options) {
|
|
166
|
+
async function verifyPaymentAcceptance(state, headers, requiredHeaders, quote, options) {
|
|
161
167
|
const config = loadPaymentConfig();
|
|
162
168
|
const required = Array.isArray(requiredHeaders) ? requiredHeaders : [];
|
|
163
169
|
const opts = options || {};
|
|
170
|
+
// Trustless on-chain pay-per-call: the PAYMENT-SIGNATURE carries an x402 v2 payment,
|
|
171
|
+
// which a facilitator verifies and settles on-chain before any work runs. This path is
|
|
172
|
+
// opt-in (XYTARA_PAYMENT_VERIFICATION_MODE=x402_facilitator) and fails closed if the
|
|
173
|
+
// facilitator is unreachable or the payment does not settle.
|
|
174
|
+
if (config.verification_mode === "x402_facilitator" && required.includes("payment-signature")) {
|
|
175
|
+
const h = headers || {};
|
|
176
|
+
const facilitator = createFacilitatorClient({
|
|
177
|
+
url: config.x402.facilitator_url,
|
|
178
|
+
headers: config.x402.facilitator_headers,
|
|
179
|
+
timeoutMs: config.x402.facilitator_timeout_ms
|
|
180
|
+
});
|
|
181
|
+
const auth = await verifyX402Payment({
|
|
182
|
+
paymentHeader: h["payment-signature"],
|
|
183
|
+
accepts: config.x402.accepts,
|
|
184
|
+
resourceUrl: opts.resource_url || null,
|
|
185
|
+
facilitator
|
|
186
|
+
});
|
|
187
|
+
if (!auth.ok) {
|
|
188
|
+
return { ok: false, status: auth.status || 402, reason: auth.reason, detail: auth.detail || null };
|
|
189
|
+
}
|
|
190
|
+
const settled = await settleX402Payment({ authorization: auth.authorization, facilitator, settle: config.x402.settle });
|
|
191
|
+
if (!settled.ok) {
|
|
192
|
+
return { ok: false, status: settled.status || 402, reason: settled.reason, detail: settled.detail || null };
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
ok: true,
|
|
196
|
+
verification_mode: "x402_facilitator",
|
|
197
|
+
wallet_id: auth.authorization.payer,
|
|
198
|
+
payment_payload: {
|
|
199
|
+
x402_payer: auth.authorization.payer,
|
|
200
|
+
x402_network: auth.authorization.paymentRequirements.network,
|
|
201
|
+
x402_asset: auth.authorization.paymentRequirements.asset,
|
|
202
|
+
x402_amount: auth.authorization.paymentRequirements.amount
|
|
203
|
+
},
|
|
204
|
+
settlement: settled.settlement,
|
|
205
|
+
payment_response_base64: encodePaymentResponse(settled.settlement)
|
|
206
|
+
};
|
|
207
|
+
}
|
|
164
208
|
if (required.includes("authorization")) {
|
|
165
209
|
if (opts.authorization_scheme === "mpp" || opts.payment_protocol === "mpp") {
|
|
166
210
|
return verifyStripeMppAccessToken(headers || {}, quote);
|
|
@@ -171,7 +215,11 @@ function verifyPaymentAcceptance(state, headers, requiredHeaders, quote, options
|
|
|
171
215
|
return verifyL402AccessToken(headers || {}, quote);
|
|
172
216
|
}
|
|
173
217
|
const payment = parsePayment(headers || {}, requiredHeaders, opts);
|
|
174
|
-
|
|
218
|
+
// x402_facilitator only governs the x402 (payment-signature) routes, handled above.
|
|
219
|
+
// On the native and instant routes it falls back to local_signed, so enabling the mode
|
|
220
|
+
// adds trustless on-chain settlement without disabling the existing paid lanes.
|
|
221
|
+
const nativeMode = config.verification_mode === "x402_facilitator" ? "local_signed" : config.verification_mode;
|
|
222
|
+
if (nativeMode === "presence") {
|
|
175
223
|
return {
|
|
176
224
|
ok: true,
|
|
177
225
|
verification_mode: "presence",
|
|
@@ -180,7 +228,7 @@ function verifyPaymentAcceptance(state, headers, requiredHeaders, quote, options
|
|
|
180
228
|
};
|
|
181
229
|
}
|
|
182
230
|
|
|
183
|
-
if (
|
|
231
|
+
if (nativeMode !== "local_signed") {
|
|
184
232
|
return {
|
|
185
233
|
ok: false,
|
|
186
234
|
status: 400,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Self-contained x402 v2 facilitator client for the main runtime.
|
|
4
|
+
//
|
|
5
|
+
// This mirrors the proven logic in services/mcp-execution-target/server.js but takes
|
|
6
|
+
// all configuration as explicit arguments so it has no dependency on any global state
|
|
7
|
+
// and can be reused by any paid route. It is intentionally duplicated rather than shared
|
|
8
|
+
// with the standalone MCP executor, which deploys from its own subdirectory.
|
|
9
|
+
//
|
|
10
|
+
// The trust model is the same one proven live: this process never verifies balances or
|
|
11
|
+
// broadcasts transfers itself. It asks an x402 facilitator to verify the buyer's signed
|
|
12
|
+
// EIP-3009 authorization and to settle it on-chain, and refuses to serve work unless the
|
|
13
|
+
// facilitator confirms both. If the facilitator is unreachable, access fails closed.
|
|
14
|
+
|
|
15
|
+
function normalizeString(value) {
|
|
16
|
+
return typeof value === "string" ? value.trim() : "";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeAddress(value) {
|
|
20
|
+
return normalizeString(value).toLowerCase();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Decode a PAYMENT-SIGNATURE header into an x402 v2 payment payload.
|
|
24
|
+
function decodePaymentSignatureHeader(value) {
|
|
25
|
+
const raw = normalizeString(value);
|
|
26
|
+
if (!raw) return null;
|
|
27
|
+
for (const encoding of ["base64url", "base64"]) {
|
|
28
|
+
try {
|
|
29
|
+
const decoded = Buffer.from(raw, encoding).toString("utf8");
|
|
30
|
+
const parsed = JSON.parse(decoded);
|
|
31
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
32
|
+
if (Number(parsed.x402Version) !== 2) continue;
|
|
33
|
+
if (!parsed.accepted || typeof parsed.accepted !== "object") continue;
|
|
34
|
+
if (!parsed.payload || typeof parsed.payload !== "object") continue;
|
|
35
|
+
return parsed;
|
|
36
|
+
} catch (_) {
|
|
37
|
+
// try the next encoding
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Find the advertised requirements entry the buyer claims to be paying.
|
|
44
|
+
function matchAcceptedRequirements(paymentPayload, accepts) {
|
|
45
|
+
const accepted = paymentPayload && paymentPayload.accepted ? paymentPayload.accepted : null;
|
|
46
|
+
if (!accepted || !Array.isArray(accepts)) return null;
|
|
47
|
+
return (
|
|
48
|
+
accepts.find(
|
|
49
|
+
(entry) =>
|
|
50
|
+
entry.scheme === normalizeString(accepted.scheme) &&
|
|
51
|
+
entry.network === normalizeString(accepted.network) &&
|
|
52
|
+
normalizeAddress(entry.asset) === normalizeAddress(accepted.asset) &&
|
|
53
|
+
normalizeAddress(entry.payTo) === normalizeAddress(accepted.payTo) &&
|
|
54
|
+
String(entry.amount) === String(accepted.amount)
|
|
55
|
+
) || null
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createFacilitatorClient(config) {
|
|
60
|
+
const url = normalizeString(config && config.url).replace(/\/$/, "");
|
|
61
|
+
const headers = config && config.headers && typeof config.headers === "object" ? config.headers : {};
|
|
62
|
+
const timeoutMs = Math.max(1000, Number(config && config.timeoutMs) || 15000);
|
|
63
|
+
|
|
64
|
+
async function post(path, body) {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(`${url}${path}`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "content-type": "application/json", accept: "application/json", ...headers },
|
|
71
|
+
body: JSON.stringify(body),
|
|
72
|
+
signal: controller.signal
|
|
73
|
+
});
|
|
74
|
+
const text = await response.text();
|
|
75
|
+
let json = null;
|
|
76
|
+
try {
|
|
77
|
+
json = text ? JSON.parse(text) : null;
|
|
78
|
+
} catch (_) {
|
|
79
|
+
json = null;
|
|
80
|
+
}
|
|
81
|
+
return { ok: response.ok, status: response.status, json };
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return { ok: false, status: 0, json: null, error: error && error.message ? error.message : "facilitator_request_failed" };
|
|
84
|
+
} finally {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
verify: (payload, requirements) => post("/verify", { x402Version: 2, paymentPayload: payload, paymentRequirements: requirements }),
|
|
91
|
+
settle: (payload, requirements) => post("/settle", { x402Version: 2, paymentPayload: payload, paymentRequirements: requirements })
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Verify a buyer's x402 payment against the advertised requirements via the facilitator.
|
|
96
|
+
// Returns { ok:true, authorization } or { ok:false, status:402, reason, detail }.
|
|
97
|
+
async function verifyX402Payment({ paymentHeader, accepts, resourceUrl, facilitator }) {
|
|
98
|
+
const header = normalizeString(paymentHeader);
|
|
99
|
+
if (!header) {
|
|
100
|
+
return { ok: false, status: 402, reason: "payment_required" };
|
|
101
|
+
}
|
|
102
|
+
const paymentPayload = decodePaymentSignatureHeader(header);
|
|
103
|
+
if (!paymentPayload) {
|
|
104
|
+
return { ok: false, status: 402, reason: "payment_signature_invalid", detail: "PAYMENT-SIGNATURE must be a base64 JSON x402 v2 payment payload" };
|
|
105
|
+
}
|
|
106
|
+
const paymentRequirements = matchAcceptedRequirements(paymentPayload, accepts);
|
|
107
|
+
if (!paymentRequirements) {
|
|
108
|
+
return { ok: false, status: 402, reason: "payment_requirements_mismatch", detail: "accepted requirements do not match any advertised payment option" };
|
|
109
|
+
}
|
|
110
|
+
const requirementsForFacilitator = { ...paymentRequirements, resource: resourceUrl };
|
|
111
|
+
const verification = await facilitator.verify(paymentPayload, requirementsForFacilitator);
|
|
112
|
+
if (!verification.ok || !verification.json) {
|
|
113
|
+
return { ok: false, status: 402, reason: "facilitator_unreachable", detail: verification.error || `facilitator verify returned HTTP ${verification.status}` };
|
|
114
|
+
}
|
|
115
|
+
if (verification.json.isValid !== true) {
|
|
116
|
+
return { ok: false, status: 402, reason: "payment_verification_failed", detail: normalizeString(verification.json.invalidReason) || "facilitator rejected the payment" };
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
authorization: {
|
|
121
|
+
paymentPayload,
|
|
122
|
+
paymentRequirements: requirementsForFacilitator,
|
|
123
|
+
payer: normalizeString(verification.json.payer) || null
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Settle a verified payment on-chain via the facilitator (unless settlement is disabled).
|
|
129
|
+
// Returns { ok:true, settlement } or { ok:false, status:402, reason, detail }.
|
|
130
|
+
async function settleX402Payment({ authorization, facilitator, settle }) {
|
|
131
|
+
if (settle === false) {
|
|
132
|
+
return { ok: true, settlement: { success: true, skipped: true, reason: "settlement_disabled_by_operator", payer: authorization.payer } };
|
|
133
|
+
}
|
|
134
|
+
const settlement = await facilitator.settle(authorization.paymentPayload, authorization.paymentRequirements);
|
|
135
|
+
if (!settlement.ok || !settlement.json) {
|
|
136
|
+
return { ok: false, status: 402, reason: "facilitator_unreachable", detail: settlement.error || `facilitator settle returned HTTP ${settlement.status}` };
|
|
137
|
+
}
|
|
138
|
+
if (settlement.json.success !== true) {
|
|
139
|
+
return { ok: false, status: 402, reason: "payment_settlement_failed", detail: normalizeString(settlement.json.errorReason) || "facilitator could not settle the payment" };
|
|
140
|
+
}
|
|
141
|
+
return { ok: true, settlement: settlement.json };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function encodePaymentResponse(settlement) {
|
|
145
|
+
return Buffer.from(JSON.stringify(settlement), "utf8").toString("base64");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
decodePaymentSignatureHeader,
|
|
150
|
+
matchAcceptedRequirements,
|
|
151
|
+
createFacilitatorClient,
|
|
152
|
+
verifyX402Payment,
|
|
153
|
+
settleX402Payment,
|
|
154
|
+
encodePaymentResponse
|
|
155
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xytara",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "Agent-commerce runtime for quote, pay, execute, deliver, meter, and integrate.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -99,7 +99,8 @@
|
|
|
99
99
|
"verify:release-candidate": "node scripts/verify_release_candidate.js",
|
|
100
100
|
"verify:production-readiness": "node scripts/verify_production_readiness.js",
|
|
101
101
|
"verify:final-activation": "node scripts/verify_final_activation.js",
|
|
102
|
-
"verify:
|
|
102
|
+
"verify:x402-facilitator": "node scripts/verify_x402_facilitator.js",
|
|
103
|
+
"verify:all": "node scripts/verify_all.js && node scripts/verify_integrations.js && node scripts/verify_adapters.js && node scripts/verify_control_plane_contracts.js && node scripts/verify_control_plane_routes.js && node scripts/verify_framework_provider_promotion.js && node scripts/verify_operator_observability_boundary.js && node scripts/verify_pricing_experiment_plan.js && node scripts/verify_treasury_public_boundary.js && node scripts/verify_market_lanes.js && node scripts/verify_market_lane_depth.js && node scripts/verify_market_lane_traces.js && node scripts/verify_market_lane_promotion.js && node scripts/verify_provider_runtime_breadth.js && node scripts/verify_provider_runtime_journeys.js && node scripts/verify_lifecycle_continuity.js && node scripts/verify_first_run_conversions.js && node scripts/verify_delivery_summary.js && node scripts/verify_runtime_durability.js && node scripts/verify_revenue_launch_paths.js && node scripts/verify_full_catalog.js && node scripts/verify_tooling.js && node scripts/verify_examples.js && node scripts/verify_service.js && node scripts/verify_x402_facilitator.js",
|
|
103
104
|
"prepublishOnly": "npm run verify:release-candidate"
|
|
104
105
|
}
|
|
105
106
|
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Verifies the x402_facilitator payment mode end to end against a mock facilitator:
|
|
4
|
+
// - an unpaid x402 execute returns 402 and advertises the configured accepts
|
|
5
|
+
// - a well-formed payment is verified and settled, the command runs, and the
|
|
6
|
+
// response carries a PAYMENT-RESPONSE settlement receipt
|
|
7
|
+
// - malformed, mismatched, facilitator-rejected, and unsettled payments all 402
|
|
8
|
+
//
|
|
9
|
+
// No real network or chain is used; the mock facilitator stands in for x402.org.
|
|
10
|
+
|
|
11
|
+
const assert = require("assert");
|
|
12
|
+
const http = require("http");
|
|
13
|
+
|
|
14
|
+
const ACCEPTS = [
|
|
15
|
+
{
|
|
16
|
+
scheme: "exact",
|
|
17
|
+
network: "eip155:84532",
|
|
18
|
+
asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
19
|
+
amount: "1000",
|
|
20
|
+
payTo: "0x7cd2E0Ed2721319B20FAac4109774d030833F84D",
|
|
21
|
+
extra: { name: "USDC", version: "2" },
|
|
22
|
+
maxTimeoutSeconds: 60
|
|
23
|
+
}
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function startMockFacilitator() {
|
|
27
|
+
const calls = { verify: 0, settle: 0 };
|
|
28
|
+
const server = http.createServer((req, res) => {
|
|
29
|
+
const chunks = [];
|
|
30
|
+
req.on("data", (c) => chunks.push(c));
|
|
31
|
+
req.on("end", () => {
|
|
32
|
+
let body = {};
|
|
33
|
+
try { body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"); } catch (_) { body = {}; }
|
|
34
|
+
const sig = body && body.paymentPayload && body.paymentPayload.payload ? String(body.paymentPayload.payload.signature || "") : "";
|
|
35
|
+
const reply = (payload) => {
|
|
36
|
+
const text = JSON.stringify(payload);
|
|
37
|
+
res.writeHead(200, { "content-type": "application/json", "content-length": Buffer.byteLength(text) });
|
|
38
|
+
res.end(text);
|
|
39
|
+
};
|
|
40
|
+
if (req.method === "POST" && req.url === "/verify") {
|
|
41
|
+
calls.verify += 1;
|
|
42
|
+
if (sig === "reject-me") return reply({ isValid: false, invalidReason: "invalid_signature" });
|
|
43
|
+
return reply({ isValid: true, payer: "0x1111111111111111111111111111111111111111" });
|
|
44
|
+
}
|
|
45
|
+
if (req.method === "POST" && req.url === "/settle") {
|
|
46
|
+
calls.settle += 1;
|
|
47
|
+
if (sig === "settle-fails") return reply({ success: false, errorReason: "insufficient_funds" });
|
|
48
|
+
return reply({ success: true, transaction: "0xmocktx", network: "eip155:84532", payer: "0x1111111111111111111111111111111111111111" });
|
|
49
|
+
}
|
|
50
|
+
res.writeHead(404); res.end();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
return new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve({ server, calls, port: server.address().port })));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function paymentHeader(signature, overrides = {}) {
|
|
57
|
+
const accepted = ACCEPTS[0];
|
|
58
|
+
return Buffer.from(JSON.stringify({
|
|
59
|
+
x402Version: 2,
|
|
60
|
+
accepted: { ...accepted, ...overrides.accepted },
|
|
61
|
+
payload: {
|
|
62
|
+
signature,
|
|
63
|
+
authorization: {
|
|
64
|
+
from: "0x1111111111111111111111111111111111111111",
|
|
65
|
+
to: accepted.payTo,
|
|
66
|
+
value: accepted.amount,
|
|
67
|
+
validAfter: "0",
|
|
68
|
+
validBefore: String(Math.floor(Date.now() / 1000) + 600),
|
|
69
|
+
nonce: `0x${"ab".repeat(32)}`
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}), "utf8").toString("base64");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function post(baseUrl, path, body, headers = {}) {
|
|
76
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "content-type": "application/json", ...headers },
|
|
79
|
+
body: JSON.stringify(body)
|
|
80
|
+
});
|
|
81
|
+
let json = null;
|
|
82
|
+
try { json = await res.json(); } catch (_) { json = null; }
|
|
83
|
+
return { status: res.status, json, headers: res.headers };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function main() {
|
|
87
|
+
const facilitator = await startMockFacilitator();
|
|
88
|
+
const previous = {
|
|
89
|
+
mode: process.env.XYTARA_PAYMENT_VERIFICATION_MODE,
|
|
90
|
+
accepts: process.env.XYTARA_X402_ACCEPTS_JSON,
|
|
91
|
+
facUrl: process.env.XYTARA_X402_FACILITATOR_URL
|
|
92
|
+
};
|
|
93
|
+
process.env.XYTARA_PAYMENT_VERIFICATION_MODE = "x402_facilitator";
|
|
94
|
+
process.env.XYTARA_X402_ACCEPTS_JSON = JSON.stringify(ACCEPTS);
|
|
95
|
+
process.env.XYTARA_X402_FACILITATOR_URL = `http://127.0.0.1:${facilitator.port}`;
|
|
96
|
+
|
|
97
|
+
delete require.cache[require.resolve("../server")];
|
|
98
|
+
const { createServer } = require("../server");
|
|
99
|
+
const server = createServer();
|
|
100
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
101
|
+
const baseUrl = `http://127.0.0.1:${server.address().port}`;
|
|
102
|
+
const execBody = { account_id: "acct_x402", command: "x402 facilitator demo", task_ref: "trust.verify", body: { subject_id: "s-1" } };
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// 1. Unpaid -> 402 advertising accepts.
|
|
106
|
+
const unpaid = await post(baseUrl, "/x402/commands/execute", execBody);
|
|
107
|
+
assert.strictEqual(unpaid.status, 402, "unpaid x402 execute must return 402");
|
|
108
|
+
const required = unpaid.json && unpaid.json.payment_required;
|
|
109
|
+
assert.ok(required, "402 must carry payment_required");
|
|
110
|
+
assert.strictEqual(required.x402Version, 2, "challenge must be x402 v2");
|
|
111
|
+
assert.ok(Array.isArray(required.accepts) && required.accepts.length === 1, "challenge must advertise accepts");
|
|
112
|
+
assert.strictEqual(required.accepts[0].network, "eip155:84532", "advertised network mismatch");
|
|
113
|
+
assert.strictEqual(typeof unpaid.headers.get("payment-required"), "string", "PAYMENT-REQUIRED header missing");
|
|
114
|
+
|
|
115
|
+
// 2. Bogus header -> 402 payment_signature_invalid.
|
|
116
|
+
const bogus = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": "not-a-payment" });
|
|
117
|
+
assert.strictEqual(bogus.status, 402, "bogus payment header must 402");
|
|
118
|
+
assert.strictEqual(bogus.json.reason, "payment_signature_invalid", "bogus reason mismatch");
|
|
119
|
+
|
|
120
|
+
// 3. Mismatched requirements -> 402 before the facilitator.
|
|
121
|
+
const verifyBefore = facilitator.calls.verify;
|
|
122
|
+
const mismatch = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("ok", { accepted: { amount: "9" } }) });
|
|
123
|
+
assert.strictEqual(mismatch.status, 402, "mismatched requirements must 402");
|
|
124
|
+
assert.strictEqual(mismatch.json.reason, "payment_requirements_mismatch", "mismatch reason");
|
|
125
|
+
assert.strictEqual(facilitator.calls.verify, verifyBefore, "facilitator must not be called on a requirements mismatch");
|
|
126
|
+
|
|
127
|
+
// 4. Facilitator rejects -> 402 payment_verification_failed.
|
|
128
|
+
const rejected = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("reject-me") });
|
|
129
|
+
assert.strictEqual(rejected.status, 402, "facilitator-rejected payment must 402");
|
|
130
|
+
assert.strictEqual(rejected.json.reason, "payment_verification_failed", "rejection reason");
|
|
131
|
+
assert.strictEqual(rejected.json.detail, "invalid_signature", "rejection detail");
|
|
132
|
+
|
|
133
|
+
// 5. Settlement fails -> 402, no tool output.
|
|
134
|
+
const settleFails = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("settle-fails") });
|
|
135
|
+
assert.strictEqual(settleFails.status, 402, "failed settlement must 402");
|
|
136
|
+
assert.strictEqual(settleFails.json.reason, "payment_settlement_failed", "settlement failure reason");
|
|
137
|
+
|
|
138
|
+
// 6. Verified + settled -> 200 with a result and a PAYMENT-RESPONSE receipt.
|
|
139
|
+
const settleBefore = facilitator.calls.settle;
|
|
140
|
+
const paid = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("ok") });
|
|
141
|
+
assert.strictEqual(paid.status, 200, "verified+settled payment must 200");
|
|
142
|
+
assert.strictEqual(typeof paid.json.transaction.transaction_id, "string", "paid result missing transaction");
|
|
143
|
+
assert.strictEqual(paid.json.transaction.status, "completed", "paid transaction must be completed");
|
|
144
|
+
assert.strictEqual(paid.json.transaction.payment.verification_mode, "x402_facilitator", "verification mode not recorded");
|
|
145
|
+
assert.strictEqual(facilitator.calls.settle, settleBefore + 1, "facilitator settle must be called once");
|
|
146
|
+
const receiptHeader = paid.headers.get("payment-response");
|
|
147
|
+
assert.strictEqual(typeof receiptHeader, "string", "PAYMENT-RESPONSE header missing on a settled 200");
|
|
148
|
+
const receipt = JSON.parse(Buffer.from(receiptHeader, "base64").toString("utf8"));
|
|
149
|
+
assert.strictEqual(receipt.success, true, "settlement receipt must report success");
|
|
150
|
+
assert.strictEqual(receipt.transaction, "0xmocktx", "settlement receipt transaction mismatch");
|
|
151
|
+
|
|
152
|
+
// 7. Native routes must not be disabled by x402_facilitator: a native paid attempt
|
|
153
|
+
// falls back to local_signed semantics, never payment_verification_mode_unsupported.
|
|
154
|
+
const nativeAttempt = await post(baseUrl, "/v1/commands/execute", execBody, {
|
|
155
|
+
"x-wallet-id": "wallet-not-registered",
|
|
156
|
+
"x-payment": "encoded",
|
|
157
|
+
"x-payment-signature": "sig"
|
|
158
|
+
});
|
|
159
|
+
assert.notStrictEqual(nativeAttempt.json && nativeAttempt.json.reason, "payment_verification_mode_unsupported", "native route must fall back to local_signed under x402_facilitator");
|
|
160
|
+
|
|
161
|
+
console.log("xytara verify:x402-facilitator passed");
|
|
162
|
+
} finally {
|
|
163
|
+
await new Promise((resolve) => server.close(resolve));
|
|
164
|
+
await new Promise((resolve) => facilitator.server.close(resolve));
|
|
165
|
+
if (previous.mode === undefined) delete process.env.XYTARA_PAYMENT_VERIFICATION_MODE; else process.env.XYTARA_PAYMENT_VERIFICATION_MODE = previous.mode;
|
|
166
|
+
if (previous.accepts === undefined) delete process.env.XYTARA_X402_ACCEPTS_JSON; else process.env.XYTARA_X402_ACCEPTS_JSON = previous.accepts;
|
|
167
|
+
if (previous.facUrl === undefined) delete process.env.XYTARA_X402_FACILITATOR_URL; else process.env.XYTARA_X402_FACILITATOR_URL = previous.facUrl;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
main().catch((error) => {
|
|
172
|
+
console.error(error && error.message ? error.message : error);
|
|
173
|
+
process.exitCode = 1;
|
|
174
|
+
});
|
package/server.js
CHANGED
|
@@ -49,6 +49,28 @@ function buildPaymentRequiredHeaders(paymentRequiredPayload) {
|
|
|
49
49
|
"Cache-Control": "no-store"
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
// The public resource URL of the current request, used as the x402 payment resource.
|
|
54
|
+
function buildRequestResourceUrl(req, url) {
|
|
55
|
+
const headers = (req && req.headers) || {};
|
|
56
|
+
const forwardedProto = String(headers["x-forwarded-proto"] || "").split(",")[0].trim();
|
|
57
|
+
const protocol = forwardedProto || (process.env.RENDER ? "https" : "http");
|
|
58
|
+
const host = String(headers["x-forwarded-host"] || headers.host || "").split(",")[0].trim();
|
|
59
|
+
if (!host) return null;
|
|
60
|
+
return `${protocol}://${host}${url.pathname}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Response headers for an x402 route: PAYMENT-REQUIRED on a 402 challenge, or
|
|
64
|
+
// PAYMENT-RESPONSE carrying the settlement receipt on a settled 200.
|
|
65
|
+
function buildX402ResponseHeaders(result) {
|
|
66
|
+
if (result && result.status === 402 && result.payload && result.payload.payment_required) {
|
|
67
|
+
return buildPaymentRequiredHeaders(result.payload.payment_required);
|
|
68
|
+
}
|
|
69
|
+
if (result && result.status === 200 && result.paymentResponse) {
|
|
70
|
+
return { "PAYMENT-RESPONSE": result.paymentResponse, "Cache-Control": "no-store" };
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
52
74
|
const {
|
|
53
75
|
completePendingExecution,
|
|
54
76
|
createCommandResult,
|
|
@@ -8898,24 +8920,21 @@ async function routeRequest(req, res) {
|
|
|
8898
8920
|
...(body && typeof body === "object" && !Array.isArray(body) ? body : {}),
|
|
8899
8921
|
payment_protocol: "x402"
|
|
8900
8922
|
};
|
|
8923
|
+
const x402Accepts = loadPaymentConfig().x402.accepts;
|
|
8901
8924
|
const result = await executeCommandRequest(state, payload, req.headers, {
|
|
8902
8925
|
requiredHeaders: ["payment-signature"],
|
|
8926
|
+
resourceUrl: buildRequestResourceUrl(req, url),
|
|
8903
8927
|
challengePayload: (quote) => ({
|
|
8904
8928
|
ok: false,
|
|
8905
8929
|
payment_required: {
|
|
8930
|
+
x402Version: 2,
|
|
8906
8931
|
scheme: "exact",
|
|
8907
|
-
quote
|
|
8932
|
+
quote,
|
|
8933
|
+
...(x402Accepts.length > 0 ? { accepts: x402Accepts } : {})
|
|
8908
8934
|
}
|
|
8909
8935
|
})
|
|
8910
8936
|
});
|
|
8911
|
-
sendJson(
|
|
8912
|
-
res,
|
|
8913
|
-
result.status,
|
|
8914
|
-
result.payload,
|
|
8915
|
-
result.status === 402 && result.payload && result.payload.payment_required
|
|
8916
|
-
? buildPaymentRequiredHeaders(result.payload.payment_required)
|
|
8917
|
-
: undefined
|
|
8918
|
-
);
|
|
8937
|
+
sendJson(res, result.status, result.payload, buildX402ResponseHeaders(result));
|
|
8919
8938
|
return;
|
|
8920
8939
|
}
|
|
8921
8940
|
|
|
@@ -10566,24 +10585,21 @@ async function routeRequest(req, res) {
|
|
|
10566
10585
|
if (req.method === "POST" && url.pathname === "/x402/mcp/tools/invoke") {
|
|
10567
10586
|
const body = await readJsonBody(req);
|
|
10568
10587
|
const payload = buildMcpInvokePayload(body, { paymentProtocol: "x402" });
|
|
10588
|
+
const x402Accepts = loadPaymentConfig().x402.accepts;
|
|
10569
10589
|
const result = await executeCommandRequest(state, payload, req.headers, {
|
|
10570
10590
|
requiredHeaders: ["payment-signature"],
|
|
10591
|
+
resourceUrl: buildRequestResourceUrl(req, url),
|
|
10571
10592
|
challengePayload: (quote) => ({
|
|
10572
10593
|
ok: false,
|
|
10573
10594
|
payment_required: {
|
|
10595
|
+
x402Version: 2,
|
|
10574
10596
|
scheme: "exact",
|
|
10575
|
-
quote
|
|
10597
|
+
quote,
|
|
10598
|
+
...(x402Accepts.length > 0 ? { accepts: x402Accepts } : {})
|
|
10576
10599
|
}
|
|
10577
10600
|
})
|
|
10578
10601
|
});
|
|
10579
|
-
sendJson(
|
|
10580
|
-
res,
|
|
10581
|
-
result.status,
|
|
10582
|
-
result.payload,
|
|
10583
|
-
result.status === 402 && result.payload && result.payload.payment_required
|
|
10584
|
-
? buildPaymentRequiredHeaders(result.payload.payment_required)
|
|
10585
|
-
: undefined
|
|
10586
|
-
);
|
|
10602
|
+
sendJson(res, result.status, result.payload, buildX402ResponseHeaders(result));
|
|
10587
10603
|
return;
|
|
10588
10604
|
}
|
|
10589
10605
|
|