xytara 3.1.4 → 3.1.6
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 +4 -0
- package/REAL_PAYMENT_SETUP.md +12 -8
- package/RELEASE_NOTES.md +6 -5
- package/examples/generated.carried-handoff.json +3 -3
- package/lib/cdp_jwt.js +57 -0
- package/lib/payment_config.js +253 -242
- package/lib/payment_verification.js +313 -312
- package/lib/x402_facilitator.js +181 -155
- package/package.json +4 -3
- package/scripts/verify_cdp_facilitator.js +110 -0
- package/scripts/verify_x402_facilitator.js +202 -194
- package/server.js +11227 -11226
package/lib/x402_facilitator.js
CHANGED
|
@@ -1,155 +1,181 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (!parsed
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
return { ok: false, status: 402, reason: "
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { buildCdpBearerToken } = require("./cdp_jwt");
|
|
4
|
+
|
|
5
|
+
// Self-contained x402 v2 facilitator client for the main runtime.
|
|
6
|
+
//
|
|
7
|
+
// This mirrors the proven logic in services/mcp-execution-target/server.js but takes
|
|
8
|
+
// all configuration as explicit arguments so it has no dependency on any global state
|
|
9
|
+
// and can be reused by any paid route. It is intentionally duplicated rather than shared
|
|
10
|
+
// with the standalone MCP executor, which deploys from its own subdirectory.
|
|
11
|
+
//
|
|
12
|
+
// The trust model is the same one proven live: this process never verifies balances or
|
|
13
|
+
// broadcasts transfers itself. It asks an x402 facilitator to verify the buyer's signed
|
|
14
|
+
// EIP-3009 authorization and to settle it on-chain, and refuses to serve work unless the
|
|
15
|
+
// facilitator confirms both. If the facilitator is unreachable, access fails closed.
|
|
16
|
+
|
|
17
|
+
function normalizeString(value) {
|
|
18
|
+
return typeof value === "string" ? value.trim() : "";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeAddress(value) {
|
|
22
|
+
return normalizeString(value).toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Decode a PAYMENT-SIGNATURE header into an x402 v2 payment payload.
|
|
26
|
+
function decodePaymentSignatureHeader(value) {
|
|
27
|
+
const raw = normalizeString(value);
|
|
28
|
+
if (!raw) return null;
|
|
29
|
+
for (const encoding of ["base64url", "base64"]) {
|
|
30
|
+
try {
|
|
31
|
+
const decoded = Buffer.from(raw, encoding).toString("utf8");
|
|
32
|
+
const parsed = JSON.parse(decoded);
|
|
33
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
34
|
+
if (Number(parsed.x402Version) !== 2) continue;
|
|
35
|
+
if (!parsed.accepted || typeof parsed.accepted !== "object") continue;
|
|
36
|
+
if (!parsed.payload || typeof parsed.payload !== "object") continue;
|
|
37
|
+
return parsed;
|
|
38
|
+
} catch (_) {
|
|
39
|
+
// try the next encoding
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Find the advertised requirements entry the buyer claims to be paying.
|
|
46
|
+
function matchAcceptedRequirements(paymentPayload, accepts) {
|
|
47
|
+
const accepted = paymentPayload && paymentPayload.accepted ? paymentPayload.accepted : null;
|
|
48
|
+
if (!accepted || !Array.isArray(accepts)) return null;
|
|
49
|
+
return (
|
|
50
|
+
accepts.find(
|
|
51
|
+
(entry) =>
|
|
52
|
+
entry.scheme === normalizeString(accepted.scheme) &&
|
|
53
|
+
entry.network === normalizeString(accepted.network) &&
|
|
54
|
+
normalizeAddress(entry.asset) === normalizeAddress(accepted.asset) &&
|
|
55
|
+
normalizeAddress(entry.payTo) === normalizeAddress(accepted.payTo) &&
|
|
56
|
+
String(entry.amount) === String(accepted.amount)
|
|
57
|
+
) || null
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createFacilitatorClient(config) {
|
|
62
|
+
const url = normalizeString(config && config.url).replace(/\/$/, "");
|
|
63
|
+
const headers = config && config.headers && typeof config.headers === "object" ? config.headers : {};
|
|
64
|
+
const timeoutMs = Math.max(1000, Number(config && config.timeoutMs) || 15000);
|
|
65
|
+
// Optional Coinbase CDP auth: when a CDP Secret API Key is configured, a fresh short-lived
|
|
66
|
+
// EdDSA Bearer JWT is minted per request (CDP rejects static tokens). This is what unlocks
|
|
67
|
+
// the CDP-hosted facilitator and Base mainnet settlement.
|
|
68
|
+
const cdp = config && config.cdp && typeof config.cdp === "object" ? config.cdp : null;
|
|
69
|
+
const cdpKeyId = cdp ? normalizeString(cdp.key_id || cdp.keyId) : "";
|
|
70
|
+
const cdpKeySecret = cdp ? String((cdp.key_secret || cdp.keySecret) || "").trim() : "";
|
|
71
|
+
const cdpEnabled = Boolean(cdpKeyId && cdpKeySecret);
|
|
72
|
+
|
|
73
|
+
async function post(path, body) {
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
76
|
+
let authHeaders = {};
|
|
77
|
+
if (cdpEnabled) {
|
|
78
|
+
try {
|
|
79
|
+
const target = new URL(`${url}${path}`);
|
|
80
|
+
const token = buildCdpBearerToken({ keyId: cdpKeyId, keySecret: cdpKeySecret, method: "POST", host: target.host, path: target.pathname });
|
|
81
|
+
authHeaders = { Authorization: `Bearer ${token}` };
|
|
82
|
+
} catch (error) {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
return { ok: false, status: 0, json: null, error: error && error.message ? error.message : "cdp_jwt_failed" };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const response = await fetch(`${url}${path}`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: { "content-type": "application/json", accept: "application/json", ...headers, ...authHeaders },
|
|
91
|
+
body: JSON.stringify(body),
|
|
92
|
+
signal: controller.signal
|
|
93
|
+
});
|
|
94
|
+
const text = await response.text();
|
|
95
|
+
let json = null;
|
|
96
|
+
try {
|
|
97
|
+
json = text ? JSON.parse(text) : null;
|
|
98
|
+
} catch (_) {
|
|
99
|
+
json = null;
|
|
100
|
+
}
|
|
101
|
+
return { ok: response.ok, status: response.status, json };
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return { ok: false, status: 0, json: null, error: error && error.message ? error.message : "facilitator_request_failed" };
|
|
104
|
+
} finally {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
verify: (payload, requirements) => post("/verify", { x402Version: 2, paymentPayload: payload, paymentRequirements: requirements }),
|
|
111
|
+
settle: (payload, requirements) => post("/settle", { x402Version: 2, paymentPayload: payload, paymentRequirements: requirements })
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Verify a buyer's x402 payment against the advertised requirements via the facilitator.
|
|
116
|
+
// Returns { ok:true, authorization } or { ok:false, status:402, reason, detail }.
|
|
117
|
+
async function verifyX402Payment({ paymentHeader, accepts, resourceUrl, facilitator }) {
|
|
118
|
+
const header = normalizeString(paymentHeader);
|
|
119
|
+
if (!header) {
|
|
120
|
+
return { ok: false, status: 402, reason: "payment_required" };
|
|
121
|
+
}
|
|
122
|
+
const paymentPayload = decodePaymentSignatureHeader(header);
|
|
123
|
+
if (!paymentPayload) {
|
|
124
|
+
return { ok: false, status: 402, reason: "payment_signature_invalid", detail: "PAYMENT-SIGNATURE must be a base64 JSON x402 v2 payment payload" };
|
|
125
|
+
}
|
|
126
|
+
const paymentRequirements = matchAcceptedRequirements(paymentPayload, accepts);
|
|
127
|
+
if (!paymentRequirements) {
|
|
128
|
+
return { ok: false, status: 402, reason: "payment_requirements_mismatch", detail: "accepted requirements do not match any advertised payment option" };
|
|
129
|
+
}
|
|
130
|
+
// The x402 v2 PaymentRequirements shape is exactly {scheme, network, asset, amount, payTo,
|
|
131
|
+
// maxTimeoutSeconds, extra} with no `resource` field (resource lives on the payload in v2).
|
|
132
|
+
// Adding one makes strict facilitators (e.g. Coinbase CDP) reject the request with HTTP 400
|
|
133
|
+
// and makes paymentRequirements diverge from paymentPayload.accepted, so send it clean.
|
|
134
|
+
// resourceUrl is accepted for API compatibility but intentionally not sent to the facilitator.
|
|
135
|
+
void resourceUrl;
|
|
136
|
+
const requirementsForFacilitator = { ...paymentRequirements };
|
|
137
|
+
const verification = await facilitator.verify(paymentPayload, requirementsForFacilitator);
|
|
138
|
+
if (!verification.ok || !verification.json) {
|
|
139
|
+
return { ok: false, status: 402, reason: "facilitator_unreachable", detail: verification.error || `facilitator verify returned HTTP ${verification.status}` };
|
|
140
|
+
}
|
|
141
|
+
if (verification.json.isValid !== true) {
|
|
142
|
+
return { ok: false, status: 402, reason: "payment_verification_failed", detail: normalizeString(verification.json.invalidReason) || "facilitator rejected the payment" };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
ok: true,
|
|
146
|
+
authorization: {
|
|
147
|
+
paymentPayload,
|
|
148
|
+
paymentRequirements: requirementsForFacilitator,
|
|
149
|
+
payer: normalizeString(verification.json.payer) || null
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Settle a verified payment on-chain via the facilitator (unless settlement is disabled).
|
|
155
|
+
// Returns { ok:true, settlement } or { ok:false, status:402, reason, detail }.
|
|
156
|
+
async function settleX402Payment({ authorization, facilitator, settle }) {
|
|
157
|
+
if (settle === false) {
|
|
158
|
+
return { ok: true, settlement: { success: true, skipped: true, reason: "settlement_disabled_by_operator", payer: authorization.payer } };
|
|
159
|
+
}
|
|
160
|
+
const settlement = await facilitator.settle(authorization.paymentPayload, authorization.paymentRequirements);
|
|
161
|
+
if (!settlement.ok || !settlement.json) {
|
|
162
|
+
return { ok: false, status: 402, reason: "facilitator_unreachable", detail: settlement.error || `facilitator settle returned HTTP ${settlement.status}` };
|
|
163
|
+
}
|
|
164
|
+
if (settlement.json.success !== true) {
|
|
165
|
+
return { ok: false, status: 402, reason: "payment_settlement_failed", detail: normalizeString(settlement.json.errorReason) || "facilitator could not settle the payment" };
|
|
166
|
+
}
|
|
167
|
+
return { ok: true, settlement: settlement.json };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function encodePaymentResponse(settlement) {
|
|
171
|
+
return Buffer.from(JSON.stringify(settlement), "utf8").toString("base64");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
decodePaymentSignatureHeader,
|
|
176
|
+
matchAcceptedRequirements,
|
|
177
|
+
createFacilitatorClient,
|
|
178
|
+
verifyX402Payment,
|
|
179
|
+
settleX402Payment,
|
|
180
|
+
encodePaymentResponse
|
|
181
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xytara",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.6",
|
|
4
4
|
"description": "Agent-commerce runtime for quote, pay, execute, deliver, meter, and integrate.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -100,7 +100,8 @@
|
|
|
100
100
|
"verify:production-readiness": "node scripts/verify_production_readiness.js",
|
|
101
101
|
"verify:final-activation": "node scripts/verify_final_activation.js",
|
|
102
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",
|
|
104
|
-
"prepublishOnly": "npm run verify:release-candidate"
|
|
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 && node scripts/verify_cdp_facilitator.js",
|
|
104
|
+
"prepublishOnly": "npm run verify:release-candidate",
|
|
105
|
+
"verify:cdp-facilitator": "node scripts/verify_cdp_facilitator.js"
|
|
105
106
|
}
|
|
106
107
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Verifies the Coinbase CDP auth path for the x402 facilitator client: when a CDP Secret
|
|
4
|
+
// API Key is configured, the client must mint a fresh, valid EdDSA Bearer JWT per request
|
|
5
|
+
// (CDP rejects static tokens). A mock facilitator stands in for CDP and cryptographically
|
|
6
|
+
// verifies the token's signature and claims before accepting the call. No real network/chain.
|
|
7
|
+
|
|
8
|
+
const assert = require("assert");
|
|
9
|
+
const http = require("http");
|
|
10
|
+
const crypto = require("crypto");
|
|
11
|
+
|
|
12
|
+
const { createFacilitatorClient } = require("../lib/x402_facilitator");
|
|
13
|
+
const { loadPaymentConfig } = require("../lib/payment_config");
|
|
14
|
+
|
|
15
|
+
// A throwaway Ed25519 key, exported as the base64 64-byte (seed||public) secret CDP uses.
|
|
16
|
+
function makeCdpKey() {
|
|
17
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
18
|
+
const seed = privateKey.export({ format: "der", type: "pkcs8" }).subarray(16); // last 32 bytes
|
|
19
|
+
const pub = publicKey.export({ format: "der", type: "spki" }).subarray(12); // last 32 bytes
|
|
20
|
+
return { publicKey, keySecret: Buffer.concat([seed, pub]).toString("base64") };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function startCdpMock(expectedPublicKey, seen) {
|
|
24
|
+
const server = http.createServer((req, res) => {
|
|
25
|
+
const chunks = [];
|
|
26
|
+
req.on("data", (c) => chunks.push(c));
|
|
27
|
+
req.on("end", () => {
|
|
28
|
+
const reply = (code, payload) => {
|
|
29
|
+
const text = JSON.stringify(payload);
|
|
30
|
+
res.writeHead(code, { "content-type": "application/json", "content-length": Buffer.byteLength(text) });
|
|
31
|
+
res.end(text);
|
|
32
|
+
};
|
|
33
|
+
const auth = String((req.headers.authorization || ""));
|
|
34
|
+
if (!auth.startsWith("Bearer ")) return reply(401, { error: "missing_bearer" });
|
|
35
|
+
const token = auth.slice("Bearer ".length);
|
|
36
|
+
const parts = token.split(".");
|
|
37
|
+
if (parts.length !== 3) return reply(401, { error: "malformed_jwt" });
|
|
38
|
+
const header = JSON.parse(Buffer.from(parts[0], "base64").toString("utf8"));
|
|
39
|
+
const claims = JSON.parse(Buffer.from(parts[1], "base64").toString("utf8"));
|
|
40
|
+
const sig = Buffer.from(parts[2].replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
41
|
+
const signatureValid = crypto.verify(null, Buffer.from(`${parts[0]}.${parts[1]}`), expectedPublicKey, sig);
|
|
42
|
+
// Record what the facilitator observed so the test can assert on it.
|
|
43
|
+
seen.push({ header, claims, signatureValid, path: req.url, nonce: header.nonce });
|
|
44
|
+
if (header.alg !== "EdDSA") return reply(401, { error: "bad_alg" });
|
|
45
|
+
if (!signatureValid) return reply(401, { error: "bad_signature" });
|
|
46
|
+
if (claims.iss !== "cdp") return reply(401, { error: "bad_iss" });
|
|
47
|
+
if (!Array.isArray(claims.aud) || claims.aud[0] !== "cdp_service") return reply(401, { error: "bad_aud" });
|
|
48
|
+
const now = Math.floor(Date.now() / 1000);
|
|
49
|
+
if (!(claims.exp > now && claims.exp - claims.nbf <= 120)) return reply(401, { error: "bad_exp" });
|
|
50
|
+
if (!(claims.uri.startsWith("POST 127.0.0.1:") && claims.uri.endsWith(req.url))) return reply(401, { error: "bad_uri", uri: claims.uri });
|
|
51
|
+
if (req.url === "/verify") return reply(200, { isValid: true, payer: "0x2222222222222222222222222222222222222222" });
|
|
52
|
+
if (req.url === "/settle") return reply(200, { success: true, transaction: "0xcdpmocktx", network: "eip155:8453" });
|
|
53
|
+
return reply(404, { error: "not_found" });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
return new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve({ server, port: server.address().port })));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const PAYLOAD = { x402Version: 2, accepted: { scheme: "exact" }, payload: { signature: "ok", authorization: {} } };
|
|
60
|
+
const REQS = { scheme: "exact", network: "eip155:8453", asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", amount: "10000", payTo: "0xabc", resource: "https://x/y" };
|
|
61
|
+
|
|
62
|
+
async function main() {
|
|
63
|
+
const { publicKey, keySecret } = makeCdpKey();
|
|
64
|
+
const seen = [];
|
|
65
|
+
const mock = await startCdpMock(publicKey, seen);
|
|
66
|
+
const baseUrl = `http://127.0.0.1:${mock.port}`;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// 1. Config loader picks up the CDP env into config.x402.cdp.
|
|
70
|
+
const prev = { id: process.env.XYTARA_X402_CDP_KEY_ID, sec: process.env.XYTARA_X402_CDP_KEY_SECRET };
|
|
71
|
+
process.env.XYTARA_X402_CDP_KEY_ID = "11111111-2222-3333-4444-555555555555";
|
|
72
|
+
process.env.XYTARA_X402_CDP_KEY_SECRET = keySecret;
|
|
73
|
+
const cfg = loadPaymentConfig();
|
|
74
|
+
assert.ok(cfg.x402.cdp && cfg.x402.cdp.key_id === "11111111-2222-3333-4444-555555555555", "config must load CDP key id");
|
|
75
|
+
assert.strictEqual(cfg.x402.cdp.key_secret, keySecret, "config must load CDP key secret");
|
|
76
|
+
|
|
77
|
+
// 2. The client attaches a valid per-request Bearer that the mock CDP accepts.
|
|
78
|
+
const client = createFacilitatorClient({ url: baseUrl, cdp: cfg.x402.cdp, timeoutMs: 5000 });
|
|
79
|
+
const verify = await client.verify(PAYLOAD, REQS);
|
|
80
|
+
assert.strictEqual(verify.ok, true, "verify must succeed with a valid CDP Bearer");
|
|
81
|
+
assert.strictEqual(verify.json.isValid, true, "mock CDP must accept the token on /verify");
|
|
82
|
+
const settle = await client.settle(PAYLOAD, REQS);
|
|
83
|
+
assert.strictEqual(settle.ok, true, "settle must succeed with a valid CDP Bearer");
|
|
84
|
+
assert.strictEqual(settle.json.transaction, "0xcdpmocktx", "settle must return the mock tx");
|
|
85
|
+
|
|
86
|
+
// 3. Each request minted its own token (fresh nonce), signature valid, uri bound to the path.
|
|
87
|
+
assert.strictEqual(seen.length, 2, "mock must have seen two authenticated calls");
|
|
88
|
+
assert.ok(seen.every((e) => e.signatureValid), "every token signature must verify");
|
|
89
|
+
assert.notStrictEqual(seen[0].nonce, seen[1].nonce, "each request must mint a fresh token (distinct nonce)");
|
|
90
|
+
assert.ok(seen[0].claims.uri.endsWith("/verify") && seen[1].claims.uri.endsWith("/settle"), "uri claim must bind to each request path");
|
|
91
|
+
|
|
92
|
+
// 4. Without CDP creds, no Authorization header is sent (static-facilitator path unaffected).
|
|
93
|
+
const seen2 = [];
|
|
94
|
+
const mock2 = await startCdpMock(publicKey, seen2);
|
|
95
|
+
const plainClient = createFacilitatorClient({ url: `http://127.0.0.1:${mock2.port}`, timeoutMs: 5000 });
|
|
96
|
+
const plain = await plainClient.verify(PAYLOAD, REQS);
|
|
97
|
+
assert.strictEqual(plain.status, 401, "without CDP creds the mock sees no Bearer and rejects");
|
|
98
|
+
assert.strictEqual(seen2.length, 0, "no JWT should be minted when CDP is not configured");
|
|
99
|
+
await new Promise((r) => mock2.server.close(r));
|
|
100
|
+
|
|
101
|
+
if (prev.id === undefined) delete process.env.XYTARA_X402_CDP_KEY_ID; else process.env.XYTARA_X402_CDP_KEY_ID = prev.id;
|
|
102
|
+
if (prev.sec === undefined) delete process.env.XYTARA_X402_CDP_KEY_SECRET; else process.env.XYTARA_X402_CDP_KEY_SECRET = prev.sec;
|
|
103
|
+
|
|
104
|
+
console.log("xytara verify:cdp-facilitator passed");
|
|
105
|
+
} finally {
|
|
106
|
+
await new Promise((r) => mock.server.close(r));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main().catch((e) => { console.error(e && e.message ? e.message : e); process.exitCode = 1; });
|