xytara 3.1.6 → 3.1.7
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/RELEASE_NOTES.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
`xytara` 3.1.
|
|
1
|
+
`xytara` 3.1.7 completes the Coinbase CDP facilitator path, proven with a real Base mainnet settlement.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A facilitator can signal a verification or settlement result either with a 200 and the result in the body, or with a non-2xx status and the same body. Coinbase CDP returns HTTP 400 for an invalid payment (with `{isValid:false, invalidReason, invalidMessage}`). The client previously treated any non-2xx as an unreachable transport error and never read the body, so a CDP verdict was misclassified. This release reads the result from the body regardless of HTTP status, which was the last blocker for Base mainnet settlement through CDP.
|
|
4
4
|
|
|
5
5
|
Highlights:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
- adds
|
|
10
|
-
- no change
|
|
7
|
+
- the facilitator client treats any `/verify` or `/settle` response that carries an `isValid` or `success` flag as the facilitator's result, regardless of HTTP status; only a missing or unparseable body is a transport failure
|
|
8
|
+
- failure details now surface the facilitator's `invalidReason` / `invalidMessage` / `errorReason` and a truncated response-body excerpt, so operators can see exactly why a facilitator rejected a payment
|
|
9
|
+
- adds regression checks for facilitators that return a non-2xx status with a result body
|
|
10
|
+
- no change for facilitators that return 200 with the result; the default Base Sepolia path is unaffected
|
|
11
|
+
- verified end to end: a real Base mainnet USDC payment settled on-chain through the CDP facilitator and minted self-serve credits
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
"source_ref": "spine.job.commitment",
|
|
7
7
|
"committed_ref": "transaction.job.commitment",
|
|
8
8
|
"precommit_pack_ref": "precommit.job.commitment.readiness",
|
|
9
|
-
"transaction_id": "
|
|
10
|
-
"receipt_id": "
|
|
11
|
-
"proof_ref": "
|
|
9
|
+
"transaction_id": "txn_2b8e48010d89",
|
|
10
|
+
"receipt_id": "rcpt_5b59fa10c38d",
|
|
11
|
+
"proof_ref": "proof_rcpt_5b59fa10c38d",
|
|
12
12
|
"payment_protocol": "x402",
|
|
13
13
|
"settlement_mode": "evm_payment",
|
|
14
14
|
"treasury_destination_id": "treasury_acct_demo",
|
package/lib/x402_facilitator.js
CHANGED
|
@@ -98,7 +98,7 @@ function createFacilitatorClient(config) {
|
|
|
98
98
|
} catch (_) {
|
|
99
99
|
json = null;
|
|
100
100
|
}
|
|
101
|
-
return { ok: response.ok, status: response.status, json };
|
|
101
|
+
return { ok: response.ok, status: response.status, json, bodyExcerpt: text ? text.slice(0, 400) : "" };
|
|
102
102
|
} catch (error) {
|
|
103
103
|
return { ok: false, status: 0, json: null, error: error && error.message ? error.message : "facilitator_request_failed" };
|
|
104
104
|
} finally {
|
|
@@ -135,11 +135,17 @@ async function verifyX402Payment({ paymentHeader, accepts, resourceUrl, facilita
|
|
|
135
135
|
void resourceUrl;
|
|
136
136
|
const requirementsForFacilitator = { ...paymentRequirements };
|
|
137
137
|
const verification = await facilitator.verify(paymentPayload, requirementsForFacilitator);
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
// A facilitator signals an invalid payment either with a 200 and isValid:false (x402.org) or
|
|
139
|
+
// with a non-2xx status and the same body (Coinbase CDP returns HTTP 400). Treat any response
|
|
140
|
+
// that carries an isValid flag as a verification result; only a missing/garbled body is a
|
|
141
|
+
// genuine transport failure.
|
|
142
|
+
if (!verification.json || typeof verification.json.isValid === "undefined") {
|
|
143
|
+
const body = verification.bodyExcerpt ? ` body=${verification.bodyExcerpt}` : "";
|
|
144
|
+
return { ok: false, status: 402, reason: "facilitator_unreachable", detail: verification.error || `facilitator verify returned HTTP ${verification.status}${body}` };
|
|
140
145
|
}
|
|
141
146
|
if (verification.json.isValid !== true) {
|
|
142
|
-
|
|
147
|
+
const detail = normalizeString(verification.json.invalidReason) || normalizeString(verification.json.invalidMessage) || "facilitator rejected the payment";
|
|
148
|
+
return { ok: false, status: 402, reason: "payment_verification_failed", detail };
|
|
143
149
|
}
|
|
144
150
|
return {
|
|
145
151
|
ok: true,
|
|
@@ -158,11 +164,15 @@ async function settleX402Payment({ authorization, facilitator, settle }) {
|
|
|
158
164
|
return { ok: true, settlement: { success: true, skipped: true, reason: "settlement_disabled_by_operator", payer: authorization.payer } };
|
|
159
165
|
}
|
|
160
166
|
const settlement = await facilitator.settle(authorization.paymentPayload, authorization.paymentRequirements);
|
|
161
|
-
|
|
162
|
-
|
|
167
|
+
// As with verify, a facilitator may report a failed settlement with a non-2xx status and a
|
|
168
|
+
// body carrying success:false; only a missing/garbled body is a genuine transport failure.
|
|
169
|
+
if (!settlement.json || typeof settlement.json.success === "undefined") {
|
|
170
|
+
const body = settlement.bodyExcerpt ? ` body=${settlement.bodyExcerpt}` : "";
|
|
171
|
+
return { ok: false, status: 402, reason: "facilitator_unreachable", detail: settlement.error || `facilitator settle returned HTTP ${settlement.status}${body}` };
|
|
163
172
|
}
|
|
164
173
|
if (settlement.json.success !== true) {
|
|
165
|
-
|
|
174
|
+
const detail = normalizeString(settlement.json.errorReason) || normalizeString(settlement.json.errorMessage) || "facilitator could not settle the payment";
|
|
175
|
+
return { ok: false, status: 402, reason: "payment_settlement_failed", detail };
|
|
166
176
|
}
|
|
167
177
|
return { ok: true, settlement: settlement.json };
|
|
168
178
|
}
|
package/package.json
CHANGED
|
@@ -32,20 +32,22 @@ function startMockFacilitator() {
|
|
|
32
32
|
let body = {};
|
|
33
33
|
try { body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"); } catch (_) { body = {}; }
|
|
34
34
|
const sig = body && body.paymentPayload && body.paymentPayload.payload ? String(body.paymentPayload.payload.signature || "") : "";
|
|
35
|
-
const reply = (payload) => {
|
|
35
|
+
const reply = (payload, status) => {
|
|
36
36
|
const text = JSON.stringify(payload);
|
|
37
|
-
res.writeHead(200, { "content-type": "application/json", "content-length": Buffer.byteLength(text) });
|
|
37
|
+
res.writeHead(status || 200, { "content-type": "application/json", "content-length": Buffer.byteLength(text) });
|
|
38
38
|
res.end(text);
|
|
39
39
|
};
|
|
40
40
|
if (req.method === "POST" && req.url === "/verify") {
|
|
41
41
|
calls.verify += 1;
|
|
42
42
|
calls.lastVerifyBody = body;
|
|
43
43
|
if (sig === "reject-me") return reply({ isValid: false, invalidReason: "invalid_signature" });
|
|
44
|
+
if (sig === "cdp-invalid") return reply({ isValid: false, invalidReason: "invalid_payload", invalidMessage: "contract call failed: execution reverted", payer: "0x1111111111111111111111111111111111111111" }, 400);
|
|
44
45
|
return reply({ isValid: true, payer: "0x1111111111111111111111111111111111111111" });
|
|
45
46
|
}
|
|
46
47
|
if (req.method === "POST" && req.url === "/settle") {
|
|
47
48
|
calls.settle += 1;
|
|
48
49
|
if (sig === "settle-fails") return reply({ success: false, errorReason: "insufficient_funds" });
|
|
50
|
+
if (sig === "cdp-settle-fail") return reply({ success: false, errorReason: "insufficient_funds" }, 400);
|
|
49
51
|
return reply({ success: true, transaction: "0xmocktx", network: "eip155:84532", payer: "0x1111111111111111111111111111111111111111" });
|
|
50
52
|
}
|
|
51
53
|
res.writeHead(404); res.end();
|
|
@@ -166,6 +168,18 @@ async function main() {
|
|
|
166
168
|
});
|
|
167
169
|
assert.notStrictEqual(nativeAttempt.json && nativeAttempt.json.reason, "payment_verification_mode_unsupported", "native route must fall back to local_signed under x402_facilitator");
|
|
168
170
|
|
|
171
|
+
// 7b. CDP-style facilitator: a non-2xx verify carrying isValid:false is a verification
|
|
172
|
+
// failure, not an unreachable transport error.
|
|
173
|
+
const cdpInvalid = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("cdp-invalid") });
|
|
174
|
+
assert.strictEqual(cdpInvalid.status, 402, "cdp-style 400 verify must still 402");
|
|
175
|
+
assert.strictEqual(cdpInvalid.json.reason, "payment_verification_failed", "cdp-style 400 must map to payment_verification_failed, not facilitator_unreachable");
|
|
176
|
+
assert.strictEqual(cdpInvalid.json.detail, "invalid_payload", "cdp-style verify detail must surface invalidReason");
|
|
177
|
+
|
|
178
|
+
// 7c. CDP-style settle failure: a non-2xx settle carrying success:false is a settlement failure.
|
|
179
|
+
const cdpSettleFail = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("cdp-settle-fail") });
|
|
180
|
+
assert.strictEqual(cdpSettleFail.status, 402, "cdp-style 400 settle must still 402");
|
|
181
|
+
assert.strictEqual(cdpSettleFail.json.reason, "payment_settlement_failed", "cdp-style 400 settle must map to payment_settlement_failed");
|
|
182
|
+
|
|
169
183
|
// 8. Self-serve credits top-up: unpaid -> 402 advertising accepts + credits_per_topup.
|
|
170
184
|
const topupUnpaid = await post(baseUrl, "/x402/credits/topup", { account_id: "acct_topup" });
|
|
171
185
|
assert.strictEqual(topupUnpaid.status, 402, "unpaid topup must 402");
|