xytara 3.1.13 → 3.1.15
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 +38 -4
- package/BSV_TERANODE_PLATFORM_DISCONTINUITY_RUNBOOK.md +126 -126
- package/REAL_PAYMENT_SETUP.md +14 -7
- package/RELEASE_NOTES.md +9 -4
- package/fixtures/catalog.js +22 -0
- package/lib/a2a_rpc.js +287 -280
- package/lib/account_auth.js +585 -585
- package/lib/agent_identity.js +162 -162
- package/lib/asyncapi_contract.js +3 -3
- package/lib/bolt_adapter.js +187 -187
- package/lib/bolt_payment_front.js +224 -224
- package/lib/buyer_event_log.js +215 -0
- package/lib/buyer_safety_policy.js +132 -0
- package/lib/checkout_processor_adapter.js +217 -217
- package/lib/command_flow.js +911 -722
- package/lib/commerce_artifacts.js +591 -591
- package/lib/commerce_bridge.js +274 -274
- package/lib/commerce_cases.js +399 -399
- package/lib/commerce_client.js +7694 -7694
- package/lib/commerce_quotes.js +29 -0
- package/lib/commerce_runtime.js +322 -322
- package/lib/execution_dedupe.js +86 -86
- package/lib/external_credit_bridge.js +142 -142
- package/lib/http_transport.js +99 -81
- package/lib/l402_adapter.js +241 -241
- package/lib/l402_payment_front.js +265 -265
- package/lib/mpp_adapter.js +237 -237
- package/lib/openapi_contract.js +4 -0
- package/lib/payment_config.js +257 -253
- package/lib/payment_verification.js +357 -357
- package/lib/provider_payout_execution.js +155 -155
- package/lib/provider_registry.js +852 -842
- package/lib/rate_limiter.js +81 -81
- package/lib/runtime_state_store.js +495 -369
- package/lib/settlement_bsv_live.js +600 -600
- package/lib/settlement_bsv_teranode.js +117 -117
- package/lib/stripe_mpp_payment_front.js +258 -258
- package/lib/x402_facilitator.js +198 -191
- package/lib/x402_lane_contract.js +5 -3
- package/lib/x402_settlement_recovery.js +175 -0
- package/package.json +9 -3
- package/scripts/buyer_events_daily_report.js +79 -0
- package/scripts/state_backup_offsite.js +196 -0
- package/scripts/verify_a2a_rpc.js +131 -105
- package/scripts/verify_agent_identity.js +153 -153
- package/scripts/verify_all.js +1 -1
- package/scripts/verify_bolt_adapter.js +128 -128
- package/scripts/verify_buyer_event_log.js +123 -0
- package/scripts/verify_buyer_safety.js +141 -0
- package/scripts/verify_cdp_facilitator.js +110 -110
- package/scripts/verify_checkout_processor.js +109 -109
- package/scripts/verify_commerce_bridge.js +177 -177
- package/scripts/verify_execution_dedupe.js +189 -189
- package/scripts/verify_execution_surfacing.js +76 -76
- package/scripts/verify_final_activation.js +191 -191
- package/scripts/verify_l402_adapter.js +86 -86
- package/scripts/verify_lifecycle_spend_binding.js +88 -0
- package/scripts/verify_mpp_adapter.js +128 -128
- package/scripts/verify_production_readiness.js +268 -268
- package/scripts/verify_rate_limiter.js +52 -52
- package/scripts/verify_refund_credit.js +106 -106
- package/scripts/verify_release_candidate.js +81 -81
- package/scripts/verify_runtime_state_guard.js +117 -0
- package/scripts/verify_service.js +15421 -15421
- package/scripts/verify_verify_execute_settle.js +209 -208
- package/scripts/verify_x402_facilitator.js +307 -286
- package/scripts/verify_x402_settlement_recovery.js +153 -0
- package/server.js +12242 -11974
package/lib/x402_facilitator.js
CHANGED
|
@@ -1,191 +1,198 @@
|
|
|
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, bodyExcerpt: text ? text.slice(0, 400) : "" };
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
ok:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return { ok:
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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, bodyExcerpt: text ? text.slice(0, 400) : "" };
|
|
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
|
+
// maxTimeoutSeconds is not part of the match (validBefore inside the signed authorization is what
|
|
137
|
+
// binds time), so echo the buyer's value: requirements must equal paymentPayload.accepted even if
|
|
138
|
+
// the buyer signed against an older challenge with a different window.
|
|
139
|
+
const acceptedTimeout = Number(paymentPayload.accepted && paymentPayload.accepted.maxTimeoutSeconds);
|
|
140
|
+
const requirementsForFacilitator = {
|
|
141
|
+
...paymentRequirements,
|
|
142
|
+
...(Number.isFinite(acceptedTimeout) && acceptedTimeout > 0 ? { maxTimeoutSeconds: acceptedTimeout } : {})
|
|
143
|
+
};
|
|
144
|
+
const verification = await facilitator.verify(paymentPayload, requirementsForFacilitator);
|
|
145
|
+
// A facilitator signals an invalid payment either with a 200 and isValid:false (x402.org) or
|
|
146
|
+
// with a non-2xx status and the same body (Coinbase CDP returns HTTP 400). Treat any response
|
|
147
|
+
// that carries an isValid flag as a verification result; only a missing/garbled body is a
|
|
148
|
+
// genuine transport failure.
|
|
149
|
+
if (!verification.json || typeof verification.json.isValid === "undefined") {
|
|
150
|
+
const body = verification.bodyExcerpt ? ` body=${verification.bodyExcerpt}` : "";
|
|
151
|
+
return { ok: false, status: 402, reason: "facilitator_unreachable", detail: verification.error || `facilitator verify returned HTTP ${verification.status}${body}` };
|
|
152
|
+
}
|
|
153
|
+
if (verification.json.isValid !== true) {
|
|
154
|
+
const detail = normalizeString(verification.json.invalidReason) || normalizeString(verification.json.invalidMessage) || "facilitator rejected the payment";
|
|
155
|
+
return { ok: false, status: 402, reason: "payment_verification_failed", detail };
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
ok: true,
|
|
159
|
+
authorization: {
|
|
160
|
+
paymentPayload,
|
|
161
|
+
paymentRequirements: requirementsForFacilitator,
|
|
162
|
+
payer: normalizeString(verification.json.payer) || null
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Settle a verified payment on-chain via the facilitator (unless settlement is disabled).
|
|
168
|
+
// Returns { ok:true, settlement } or { ok:false, status:402, reason, detail }.
|
|
169
|
+
async function settleX402Payment({ authorization, facilitator, settle }) {
|
|
170
|
+
if (settle === false) {
|
|
171
|
+
return { ok: true, settlement: { success: true, skipped: true, reason: "settlement_disabled_by_operator", payer: authorization.payer } };
|
|
172
|
+
}
|
|
173
|
+
const settlement = await facilitator.settle(authorization.paymentPayload, authorization.paymentRequirements);
|
|
174
|
+
// As with verify, a facilitator may report a failed settlement with a non-2xx status and a
|
|
175
|
+
// body carrying success:false; only a missing/garbled body is a genuine transport failure.
|
|
176
|
+
if (!settlement.json || typeof settlement.json.success === "undefined") {
|
|
177
|
+
const body = settlement.bodyExcerpt ? ` body=${settlement.bodyExcerpt}` : "";
|
|
178
|
+
return { ok: false, status: 402, reason: "facilitator_unreachable", detail: settlement.error || `facilitator settle returned HTTP ${settlement.status}${body}` };
|
|
179
|
+
}
|
|
180
|
+
if (settlement.json.success !== true) {
|
|
181
|
+
const detail = normalizeString(settlement.json.errorReason) || normalizeString(settlement.json.errorMessage) || "facilitator could not settle the payment";
|
|
182
|
+
return { ok: false, status: 402, reason: "payment_settlement_failed", detail };
|
|
183
|
+
}
|
|
184
|
+
return { ok: true, settlement: settlement.json };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function encodePaymentResponse(settlement) {
|
|
188
|
+
return Buffer.from(JSON.stringify(settlement), "utf8").toString("base64");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
decodePaymentSignatureHeader,
|
|
193
|
+
matchAcceptedRequirements,
|
|
194
|
+
createFacilitatorClient,
|
|
195
|
+
verifyX402Payment,
|
|
196
|
+
settleX402Payment,
|
|
197
|
+
encodePaymentResponse
|
|
198
|
+
};
|
|
@@ -26,7 +26,8 @@ function buildX402LanePack() {
|
|
|
26
26
|
lane_state: "first_party_x402_present",
|
|
27
27
|
default_protocol: centerSummary.default_protocol,
|
|
28
28
|
default_payment_protocol: "x402",
|
|
29
|
-
|
|
29
|
+
// The x402 lane settles USDC on Base through an x402 facilitator (EIP-3009 exact scheme).
|
|
30
|
+
default_settlement_mode: "x402_evm_exact",
|
|
30
31
|
default_transaction_center: center,
|
|
31
32
|
default_path: defaultX402Path,
|
|
32
33
|
release_default_path: releasePack.default_path,
|
|
@@ -93,8 +94,9 @@ function buildX402LanePack() {
|
|
|
93
94
|
}
|
|
94
95
|
],
|
|
95
96
|
settlement_posture: {
|
|
96
|
-
default_mode:
|
|
97
|
-
current_native_lane: "
|
|
97
|
+
default_mode: "x402_evm_exact",
|
|
98
|
+
current_native_lane: "x402_evm_exact",
|
|
99
|
+
runtime_default_settlement_mode: centerSummary.default_settlement_mode,
|
|
98
100
|
native_readiness_surface: "/v1/settlement/bsv-teranode/readiness",
|
|
99
101
|
challenge_quote_hints: [
|
|
100
102
|
"payment quotes carry settlement-aware runtime linkage",
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { decodePaymentSignatureHeader } = require("./x402_facilitator");
|
|
4
|
+
|
|
5
|
+
// Settlement-outcome recovery for the deferred x402 lane.
|
|
6
|
+
//
|
|
7
|
+
// A facilitator /settle that times out or drops the connection does NOT mean the transfer failed:
|
|
8
|
+
// the facilitator may already have broadcast it. Telling the buyer "you were not charged" and
|
|
9
|
+
// withholding the result could leave them charged with nothing. Instead we ask the chain: an
|
|
10
|
+
// EIP-3009 authorization that was used is a settled payment (USDC authorizationState(authorizer,
|
|
11
|
+
// nonce) == true). And every settled (or not-yet-confirmed) result is kept briefly under its
|
|
12
|
+
// payment nonce, so a buyer who lost the response — or was told to wait — can resend the SAME
|
|
13
|
+
// payment signature and receive the result instead of paying again.
|
|
14
|
+
|
|
15
|
+
const AUTHORIZATION_STATE_SELECTOR = "0xe94a0102"; // authorizationState(address,bytes32)
|
|
16
|
+
const DEFAULT_RPC_URLS = {
|
|
17
|
+
"eip155:8453": "https://mainnet.base.org",
|
|
18
|
+
"eip155:84532": "https://sepolia.base.org"
|
|
19
|
+
};
|
|
20
|
+
const REPLAY_TTL_MS = 15 * 60 * 1000;
|
|
21
|
+
const REPLAY_MAX = 1000;
|
|
22
|
+
|
|
23
|
+
function rpcUrlFor(network) {
|
|
24
|
+
const key = `XYTARA_RPC_URL_${String(network || "").replace(/[^0-9A-Za-z]/g, "_").toUpperCase()}`;
|
|
25
|
+
const fromEnv = String(process.env[key] || "").trim();
|
|
26
|
+
return fromEnv || DEFAULT_RPC_URLS[String(network || "").toLowerCase()] || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hex32(value, bytes) {
|
|
30
|
+
const clean = String(value || "").toLowerCase().replace(/^0x/, "");
|
|
31
|
+
if (!/^[0-9a-f]*$/.test(clean) || clean.length > bytes * 2) return null;
|
|
32
|
+
return clean.padStart(64, "0");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// true = authorization used (payment settled), false = not used, null = could not tell.
|
|
36
|
+
async function authorizationUsed({ network, asset, authorizer, nonce }, options = {}) {
|
|
37
|
+
const url = rpcUrlFor(network);
|
|
38
|
+
const from = hex32(authorizer, 20);
|
|
39
|
+
const n = hex32(nonce, 32);
|
|
40
|
+
if (!url || !from || !n || !/^0x[0-9a-fA-F]{40}$/.test(String(asset || ""))) return null;
|
|
41
|
+
const fetchImpl = options.fetch || fetch;
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetchImpl(url, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: { "content-type": "application/json" },
|
|
46
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_call", params: [{ to: asset, data: `${AUTHORIZATION_STATE_SELECTOR}${from}${n}` }, "latest"] }),
|
|
47
|
+
signal: AbortSignal.timeout(options.timeoutMs || 5000)
|
|
48
|
+
});
|
|
49
|
+
const json = await response.json();
|
|
50
|
+
if (!json || typeof json.result !== "string" || !/^0x[0-9a-fA-F]+$/.test(json.result)) return null;
|
|
51
|
+
return BigInt(json.result) !== 0n;
|
|
52
|
+
} catch (_) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A just-broadcast transfer may not be mined yet: poll a few times before concluding.
|
|
58
|
+
async function confirmAuthorizationUsed(ref, options = {}) {
|
|
59
|
+
const attempts = options.attempts || 3;
|
|
60
|
+
const delayMs = options.delayMs === undefined ? 2000 : options.delayMs;
|
|
61
|
+
let sawAnswer = false;
|
|
62
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
63
|
+
const used = await authorizationUsed(ref, options);
|
|
64
|
+
if (used === true) return true;
|
|
65
|
+
if (used === false) sawAnswer = true;
|
|
66
|
+
if (i < attempts - 1 && delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
67
|
+
}
|
|
68
|
+
return sawAnswer ? false : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The on-chain reference of a deferred x402 verification (or of a raw payment header).
|
|
72
|
+
function authorizationRefFromVerification(verification) {
|
|
73
|
+
const deferred = verification && verification.deferred_settlement;
|
|
74
|
+
const auth = deferred && deferred.authorization;
|
|
75
|
+
const paymentPayload = auth && auth.paymentPayload;
|
|
76
|
+
const signed = paymentPayload && paymentPayload.payload && paymentPayload.payload.authorization;
|
|
77
|
+
const requirements = (auth && auth.paymentRequirements) || (paymentPayload && paymentPayload.accepted) || {};
|
|
78
|
+
if (!signed || !signed.nonce || !signed.from) return null;
|
|
79
|
+
return { network: requirements.network, asset: requirements.asset, authorizer: signed.from, nonce: signed.nonce };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function authorizationRefFromHeaders(headers) {
|
|
83
|
+
const decoded = decodePaymentSignatureHeader((headers || {})["payment-signature"]);
|
|
84
|
+
const signed = decoded && decoded.payload && decoded.payload.authorization;
|
|
85
|
+
if (!signed || !signed.nonce || !signed.from) return null;
|
|
86
|
+
return { network: decoded.accepted.network, asset: decoded.accepted.asset, authorizer: signed.from, nonce: signed.nonce };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function replayKey(ref) {
|
|
90
|
+
return ref ? `${String(ref.network || "").toLowerCase()}:${String(ref.nonce || "").toLowerCase()}` : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function ensureReplayStore(state) {
|
|
94
|
+
if (!(state.x402SettledReplays instanceof Map)) {
|
|
95
|
+
// In-memory only: non-enumerable, so it is never written to the state snapshot.
|
|
96
|
+
Object.defineProperty(state, "x402SettledReplays", { value: new Map(), enumerable: false, writable: true });
|
|
97
|
+
}
|
|
98
|
+
return state.x402SettledReplays;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function pruneReplays(store) {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
for (const [key, entry] of store) {
|
|
104
|
+
if (entry.expires_at_ms <= now) store.delete(key);
|
|
105
|
+
}
|
|
106
|
+
while (store.size > REPLAY_MAX) store.delete(store.keys().next().value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// entry: { status: "settled" | "unconfirmed", ref, response?, finalize?: async (settlement) => response }
|
|
110
|
+
function rememberReplay(state, ref, entry) {
|
|
111
|
+
const key = replayKey(ref);
|
|
112
|
+
if (!key) return;
|
|
113
|
+
const store = ensureReplayStore(state);
|
|
114
|
+
pruneReplays(store);
|
|
115
|
+
store.set(key, { ...entry, ref, expires_at_ms: Date.now() + REPLAY_TTL_MS });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// A resent payment that we already settled (or were confirming) gets its result, not a re-run.
|
|
119
|
+
// The payment authorization becomes public on-chain once settled, so it is not enough on its own:
|
|
120
|
+
// the resent REQUEST must match the original too (request_digest), or nothing is returned.
|
|
121
|
+
async function resolveReplay(state, headers, options = {}) {
|
|
122
|
+
const ref = authorizationRefFromHeaders(headers);
|
|
123
|
+
const key = replayKey(ref);
|
|
124
|
+
if (!key) return null;
|
|
125
|
+
const store = ensureReplayStore(state);
|
|
126
|
+
pruneReplays(store);
|
|
127
|
+
const entry = store.get(key);
|
|
128
|
+
if (!entry) return null;
|
|
129
|
+
if (String(entry.ref.authorizer || "").toLowerCase() !== String(ref.authorizer || "").toLowerCase()) return null;
|
|
130
|
+
if (!entry.request_digest || entry.request_digest !== options.request_digest) {
|
|
131
|
+
return {
|
|
132
|
+
status: 409,
|
|
133
|
+
payload: {
|
|
134
|
+
ok: false,
|
|
135
|
+
reason: "payment_already_used",
|
|
136
|
+
detail: "This payment authorization was already used for a different request. Sign a new payment for this request."
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (entry.status === "settled" && entry.response) {
|
|
141
|
+
return { ...entry.response, payload: { ...entry.response.payload, replayed: true } };
|
|
142
|
+
}
|
|
143
|
+
const used = await confirmAuthorizationUsed(entry.ref, { attempts: 1, ...(options.rpc || {}) });
|
|
144
|
+
if (used === true && typeof entry.finalize === "function") {
|
|
145
|
+
const response = await entry.finalize({ success: true, transaction: null, network: entry.ref.network, payer: entry.ref.authorizer, recovered_from: "authorization_state" });
|
|
146
|
+
store.set(key, { ...entry, status: "settled", response, finalize: undefined });
|
|
147
|
+
return { ...response, payload: { ...response.payload, replayed: true } };
|
|
148
|
+
}
|
|
149
|
+
if (used === false) {
|
|
150
|
+
// Never settled: forget it so the payment is verified and executed normally.
|
|
151
|
+
store.delete(key);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
status: 409,
|
|
156
|
+
payload: {
|
|
157
|
+
ok: false,
|
|
158
|
+
reason: "settlement_outcome_unknown",
|
|
159
|
+
detail: "We are still confirming whether this payment settled on-chain. Resend the same payment signature in a minute to receive the result; do not sign a new payment.",
|
|
160
|
+
payment_settled: null
|
|
161
|
+
},
|
|
162
|
+
headers: { "Retry-After": "30" }
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
AUTHORIZATION_STATE_SELECTOR,
|
|
168
|
+
authorizationRefFromHeaders,
|
|
169
|
+
authorizationRefFromVerification,
|
|
170
|
+
authorizationUsed,
|
|
171
|
+
confirmAuthorizationUsed,
|
|
172
|
+
rememberReplay,
|
|
173
|
+
resolveReplay,
|
|
174
|
+
rpcUrlFor
|
|
175
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xytara",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.15",
|
|
4
4
|
"description": "x402 pay-per-call agent-commerce runtime: gasless USDC on Base, an open settling provider network with a ledger-reconciled revenue split and automated on-chain payouts (key-less runtime, delegated signer), and bounded cryptographic delegated spend. Proven live on Base mainnet.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
"verify:production-readiness": "node scripts/verify_production_readiness.js",
|
|
107
107
|
"verify:final-activation": "node scripts/verify_final_activation.js",
|
|
108
108
|
"verify:x402-facilitator": "node scripts/verify_x402_facilitator.js",
|
|
109
|
-
"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 && node scripts/verify_a2a_rpc.js && node scripts/verify_checkout_processor.js && node scripts/verify_l402_adapter.js && node scripts/verify_bolt_adapter.js && node scripts/verify_mpp_adapter.js && node scripts/verify_rate_limiter.js && node scripts/verify_execution_surfacing.js && node scripts/verify_agent_identity.js && node scripts/verify_verify_execute_settle.js && node scripts/verify_execution_dedupe.js && node scripts/verify_refund_credit.js && node scripts/verify_commerce_bridge.js && node scripts/verify_settlement.js && node scripts/verify_provider_network.js && node scripts/verify_external_execution_retry.js",
|
|
109
|
+
"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 && node scripts/verify_a2a_rpc.js && node scripts/verify_checkout_processor.js && node scripts/verify_l402_adapter.js && node scripts/verify_bolt_adapter.js && node scripts/verify_mpp_adapter.js && node scripts/verify_rate_limiter.js && node scripts/verify_execution_surfacing.js && node scripts/verify_agent_identity.js && node scripts/verify_verify_execute_settle.js && node scripts/verify_execution_dedupe.js && node scripts/verify_refund_credit.js && node scripts/verify_commerce_bridge.js && node scripts/verify_settlement.js && node scripts/verify_provider_network.js && node scripts/verify_external_execution_retry.js && node scripts/verify_runtime_state_guard.js && node scripts/state_backup_offsite.js selftest && node scripts/verify_lifecycle_spend_binding.js && node scripts/verify_buyer_safety.js && node scripts/verify_buyer_event_log.js && node scripts/verify_x402_settlement_recovery.js",
|
|
110
110
|
"prepublishOnly": "npm run verify:release-candidate",
|
|
111
111
|
"verify:cdp-facilitator": "node scripts/verify_cdp_facilitator.js",
|
|
112
112
|
"verify:a2a-rpc": "node scripts/verify_a2a_rpc.js",
|
|
@@ -123,6 +123,12 @@
|
|
|
123
123
|
"verify:commerce-bridge": "node scripts/verify_commerce_bridge.js",
|
|
124
124
|
"verify:settlement": "node scripts/verify_settlement.js",
|
|
125
125
|
"verify:provider-network": "node scripts/verify_provider_network.js",
|
|
126
|
-
"verify:external-execution-retry": "node scripts/verify_external_execution_retry.js"
|
|
126
|
+
"verify:external-execution-retry": "node scripts/verify_external_execution_retry.js",
|
|
127
|
+
"verify:runtime-state-guard": "node scripts/verify_runtime_state_guard.js",
|
|
128
|
+
"verify:state-backup": "node scripts/state_backup_offsite.js selftest",
|
|
129
|
+
"verify:lifecycle-spend-binding": "node scripts/verify_lifecycle_spend_binding.js",
|
|
130
|
+
"verify:buyer-safety": "node scripts/verify_buyer_safety.js",
|
|
131
|
+
"verify:buyer-event-log": "node scripts/verify_buyer_event_log.js",
|
|
132
|
+
"verify:x402-settlement-recovery": "node scripts/verify_x402_settlement_recovery.js"
|
|
127
133
|
}
|
|
128
134
|
}
|