xytara 3.1.13 → 3.1.14
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/BSV_TERANODE_PLATFORM_DISCONTINUITY_RUNBOOK.md +126 -126
- package/RELEASE_NOTES.md +6 -4
- package/lib/a2a_rpc.js +280 -280
- package/lib/account_auth.js +585 -585
- package/lib/agent_identity.js +162 -162
- package/lib/bolt_adapter.js +187 -187
- package/lib/bolt_payment_front.js +224 -224
- package/lib/checkout_processor_adapter.js +217 -217
- package/lib/command_flow.js +722 -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 +81 -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/payment_config.js +253 -253
- package/lib/payment_verification.js +357 -357
- package/lib/provider_payout_execution.js +155 -155
- package/lib/provider_registry.js +842 -842
- package/lib/rate_limiter.js +81 -81
- package/lib/runtime_state_store.js +491 -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 +191 -191
- package/package.json +5 -3
- package/scripts/state_backup_offsite.js +196 -0
- package/scripts/verify_a2a_rpc.js +105 -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_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_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 +208 -208
- package/scripts/verify_x402_facilitator.js +286 -286
- package/server.js +11976 -11974
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
// Verifies the deferred-x402 execution dedupe guard:
|
|
4
|
-
// - two CONCURRENT commands carrying the SAME signed authorization (same EIP-3009
|
|
5
|
-
// nonce) against a slow executor result in exactly ONE execution and ONE settle;
|
|
6
|
-
// the duplicate is rejected 409 payment_authorization_in_progress and not charged;
|
|
7
|
-
// - a later command with a DIFFERENT authorization is not blocked;
|
|
8
|
-
// - unit level: the guard is a no-op for non-deferred verifications, fails open when
|
|
9
|
-
// the store is missing, rejects an in-flight duplicate, frees the slot on release,
|
|
10
|
-
// and prunes entries past their TTL.
|
|
11
|
-
//
|
|
12
|
-
// No real network or chain is used.
|
|
13
|
-
|
|
14
|
-
const assert = require("assert");
|
|
15
|
-
const http = require("http");
|
|
16
|
-
const { claimDeferredExecution, releaseDeferredExecution, deferredAuthorizationKey } = require("../lib/execution_dedupe");
|
|
17
|
-
|
|
18
|
-
const ACCEPTS = [
|
|
19
|
-
{
|
|
20
|
-
scheme: "exact",
|
|
21
|
-
network: "eip155:84532",
|
|
22
|
-
asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
23
|
-
amount: "1000",
|
|
24
|
-
payTo: "0x7cd2E0Ed2721319B20FAac4109774d030833F84D",
|
|
25
|
-
extra: { name: "USDC", version: "2" },
|
|
26
|
-
maxTimeoutSeconds: 60
|
|
27
|
-
}
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
function startMockServer() {
|
|
31
|
-
const calls = { verify: 0, settle: 0, execSlow: 0 };
|
|
32
|
-
const server = http.createServer((req, res) => {
|
|
33
|
-
const chunks = [];
|
|
34
|
-
req.on("data", (c) => chunks.push(c));
|
|
35
|
-
req.on("end", () => {
|
|
36
|
-
let body = {};
|
|
37
|
-
try { body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"); } catch (_) { body = {}; }
|
|
38
|
-
const reply = (payload, status) => {
|
|
39
|
-
const text = JSON.stringify(payload);
|
|
40
|
-
res.writeHead(status || 200, { "content-type": "application/json", "content-length": Buffer.byteLength(text) });
|
|
41
|
-
res.end(text);
|
|
42
|
-
};
|
|
43
|
-
if (req.method === "POST" && req.url === "/verify") { calls.verify += 1; return reply({ isValid: true, payer: "0x1111111111111111111111111111111111111111" }); }
|
|
44
|
-
if (req.method === "POST" && req.url === "/settle") { calls.settle += 1; return reply({ success: true, transaction: "0xmocktx", network: "eip155:84532", payer: "0x1111111111111111111111111111111111111111" }); }
|
|
45
|
-
if (req.method === "POST" && req.url === "/exec-slow") {
|
|
46
|
-
calls.execSlow += 1;
|
|
47
|
-
return setTimeout(() => reply({ ok: true, status: "completed", output: { value: "slow-ok" } }), 350);
|
|
48
|
-
}
|
|
49
|
-
if (req.method === "POST" && req.url === "/exec-fail") {
|
|
50
|
-
calls.execFail = (calls.execFail || 0) + 1;
|
|
51
|
-
return reply({ ok: false, reason: "data_required" }, 502);
|
|
52
|
-
}
|
|
53
|
-
res.writeHead(404); res.end();
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
return new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve({ server, calls, port: server.address().port })));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function paymentHeader(nonceHex) {
|
|
60
|
-
const accepted = ACCEPTS[0];
|
|
61
|
-
return Buffer.from(JSON.stringify({
|
|
62
|
-
x402Version: 2,
|
|
63
|
-
accepted,
|
|
64
|
-
payload: {
|
|
65
|
-
signature: "ok",
|
|
66
|
-
authorization: {
|
|
67
|
-
from: "0x1111111111111111111111111111111111111111",
|
|
68
|
-
to: accepted.payTo,
|
|
69
|
-
value: accepted.amount,
|
|
70
|
-
validAfter: "0",
|
|
71
|
-
validBefore: String(Math.floor(Date.now() / 1000) + 600),
|
|
72
|
-
nonce: nonceHex
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}), "utf8").toString("base64");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function post(baseUrl, path, body, headers = {}) {
|
|
79
|
-
const res = await fetch(`${baseUrl}${path}`, { method: "POST", headers: { "content-type": "application/json", ...headers }, body: JSON.stringify(body) });
|
|
80
|
-
let json = null;
|
|
81
|
-
try { json = await res.json(); } catch (_) { json = null; }
|
|
82
|
-
return { status: res.status, json, receipt: res.headers.get("payment-response") };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function runUnitChecks() {
|
|
86
|
-
// No-op for a non-deferred verification.
|
|
87
|
-
assert.deepStrictEqual(claimDeferredExecution({ inFlightPaymentAuthorizations: new Map() }, { ok: true }), { ok: true, key: null }, "non-deferred must be a no-op claim");
|
|
88
|
-
// Key derivation.
|
|
89
|
-
const verification = { deferred_settlement: { protocol: "x402_facilitator", authorization: { paymentPayload: { payload: { authorization: { from: "0xABC", nonce: "0xDEAD" } } } } } };
|
|
90
|
-
assert.strictEqual(deferredAuthorizationKey(verification), "x402:0xabc:0xdead", "key must be lowercased from:nonce");
|
|
91
|
-
// Fail open with no store.
|
|
92
|
-
assert.deepStrictEqual(claimDeferredExecution({}, verification), { ok: true, key: null }, "missing store must fail open");
|
|
93
|
-
// In-flight duplicate rejected, then freed on release.
|
|
94
|
-
const state = { inFlightPaymentAuthorizations: new Map() };
|
|
95
|
-
const first = claimDeferredExecution(state, verification);
|
|
96
|
-
assert.strictEqual(first.ok, true, "first claim must succeed");
|
|
97
|
-
const dup = claimDeferredExecution(state, verification);
|
|
98
|
-
assert.strictEqual(dup.ok, false, "second concurrent claim must be rejected");
|
|
99
|
-
assert.strictEqual(dup.status, 409, "duplicate must be 409");
|
|
100
|
-
assert.strictEqual(dup.reason, "payment_authorization_in_progress", "duplicate reason");
|
|
101
|
-
releaseDeferredExecution(state, first.key);
|
|
102
|
-
assert.strictEqual(claimDeferredExecution(state, verification).ok, true, "claim must succeed again after release");
|
|
103
|
-
// TTL prune: an expired entry is forgotten on the next claim.
|
|
104
|
-
const stale = { inFlightPaymentAuthorizations: new Map([["x402:0xabc:0xdead", { status: "in_flight", expires_at: Date.now() - 1 }]]) };
|
|
105
|
-
assert.strictEqual(claimDeferredExecution(stale, verification).ok, true, "an expired slot must be pruned and re-claimable");
|
|
106
|
-
console.log(" ok: unit checks (no-op, key, fail-open, duplicate, release, ttl-prune)");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async function main() {
|
|
110
|
-
runUnitChecks();
|
|
111
|
-
|
|
112
|
-
const mock = await startMockServer();
|
|
113
|
-
const mockBase = `http://127.0.0.1:${mock.port}`;
|
|
114
|
-
const prev = {
|
|
115
|
-
mode: process.env.XYTARA_PAYMENT_VERIFICATION_MODE,
|
|
116
|
-
accepts: process.env.XYTARA_X402_ACCEPTS_JSON,
|
|
117
|
-
facUrl: process.env.XYTARA_X402_FACILITATOR_URL,
|
|
118
|
-
targets: process.env.XYTARA_EXECUTION_TARGETS_JSON
|
|
119
|
-
};
|
|
120
|
-
process.env.XYTARA_PAYMENT_VERIFICATION_MODE = "x402_facilitator";
|
|
121
|
-
process.env.XYTARA_X402_ACCEPTS_JSON = JSON.stringify(ACCEPTS);
|
|
122
|
-
process.env.XYTARA_X402_FACILITATOR_URL = mockBase;
|
|
123
|
-
process.env.XYTARA_EXECUTION_TARGETS_JSON = JSON.stringify([
|
|
124
|
-
{ target_id: "exec_slow", task_ref: "exec.slow", endpoint_url: `${mockBase}/exec-slow` },
|
|
125
|
-
{ target_id: "exec_fail", task_ref: "exec.fail", endpoint_url: `${mockBase}/exec-fail` }
|
|
126
|
-
]);
|
|
127
|
-
|
|
128
|
-
delete require.cache[require.resolve("../server")];
|
|
129
|
-
const { createServer } = require("../server");
|
|
130
|
-
const server = createServer();
|
|
131
|
-
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
132
|
-
const baseUrl = `http://127.0.0.1:${server.address().port}`;
|
|
133
|
-
const execBody = { account_id: "acct_dedupe", command: "slow exec", task_ref: "exec.slow", body: { subject_id: "s-1" } };
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
// Two concurrent requests sharing ONE authorization (same nonce) -> one wins, one 409.
|
|
137
|
-
const sharedNonce = `0x${"ab".repeat(32)}`;
|
|
138
|
-
const [a, b] = await Promise.all([
|
|
139
|
-
post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader(sharedNonce) }),
|
|
140
|
-
post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader(sharedNonce) })
|
|
141
|
-
]);
|
|
142
|
-
const statuses = [a.status, b.status].sort();
|
|
143
|
-
assert.deepStrictEqual(statuses, [200, 409], `exactly one must win and one must 409, got ${statuses}`);
|
|
144
|
-
const winner = a.status === 200 ? a : b;
|
|
145
|
-
const loser = a.status === 409 ? a : b;
|
|
146
|
-
assert.strictEqual(loser.json.reason, "payment_authorization_in_progress", "the duplicate must report in_progress");
|
|
147
|
-
assert.strictEqual(loser.json.payment_settled, false, "the rejected duplicate must not be charged");
|
|
148
|
-
assert.strictEqual(loser.receipt, null, "the rejected duplicate must carry no receipt");
|
|
149
|
-
assert.strictEqual(typeof winner.receipt, "string", "the winner must carry a settlement receipt");
|
|
150
|
-
assert.strictEqual(mock.calls.execSlow, 1, "the external target must have executed exactly once");
|
|
151
|
-
assert.strictEqual(mock.calls.settle, 1, "settlement must have happened exactly once");
|
|
152
|
-
|
|
153
|
-
// A different authorization (fresh nonce) is not blocked.
|
|
154
|
-
const other = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader(`0x${"cd".repeat(32)}`) });
|
|
155
|
-
assert.strictEqual(other.status, 200, "a distinct authorization must not be blocked");
|
|
156
|
-
assert.strictEqual(mock.calls.execSlow, 2, "the distinct authorization must execute");
|
|
157
|
-
|
|
158
|
-
console.log(" ok: concurrent same-authorization -> one execution, one settle, duplicate 409");
|
|
159
|
-
|
|
160
|
-
// Slot is released on a FAILED call, so a legitimate retry (even reusing the same
|
|
161
|
-
// still-unspent nonce) is not wrongly 409'd. Two SEQUENTIAL same-nonce calls to a
|
|
162
|
-
// failing executor must BOTH reach execution and neither be charged.
|
|
163
|
-
const failNonce = `0x${"ef".repeat(32)}`;
|
|
164
|
-
const failBody = { account_id: "acct_dedupe", command: "failing exec", task_ref: "exec.fail", body: { subject_id: "s-2" } };
|
|
165
|
-
const f1 = await post(baseUrl, "/x402/commands/execute", failBody, { "payment-signature": paymentHeader(failNonce) });
|
|
166
|
-
const f2 = await post(baseUrl, "/x402/commands/execute", failBody, { "payment-signature": paymentHeader(failNonce) });
|
|
167
|
-
assert.strictEqual(f1.status, 502, "first failing call must 502 (not 409)");
|
|
168
|
-
assert.strictEqual(f2.status, 502, "retry after failure must reach execution, not be blocked as a duplicate");
|
|
169
|
-
assert.strictEqual(f1.json.payment_settled, false, "failed call must not be charged");
|
|
170
|
-
assert.strictEqual(f2.json.payment_settled, false, "retry must not be charged");
|
|
171
|
-
assert.strictEqual(mock.calls.execFail, 2, "both failing attempts must have executed (slot released each time)");
|
|
172
|
-
console.log(" ok: failed call releases the slot -> retry not blocked, nothing charged");
|
|
173
|
-
|
|
174
|
-
console.log("xytara verify:execution-dedupe passed");
|
|
175
|
-
} finally {
|
|
176
|
-
await new Promise((resolve) => server.close(resolve));
|
|
177
|
-
await new Promise((resolve) => mock.server.close(resolve));
|
|
178
|
-
const restore = (k, v) => { if (v === undefined) delete process.env[k]; else process.env[k] = v; };
|
|
179
|
-
restore("XYTARA_PAYMENT_VERIFICATION_MODE", prev.mode);
|
|
180
|
-
restore("XYTARA_X402_ACCEPTS_JSON", prev.accepts);
|
|
181
|
-
restore("XYTARA_X402_FACILITATOR_URL", prev.facUrl);
|
|
182
|
-
restore("XYTARA_EXECUTION_TARGETS_JSON", prev.targets);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
main().catch((error) => {
|
|
187
|
-
console.error(error && error.message ? error.message : error);
|
|
188
|
-
process.exitCode = 1;
|
|
189
|
-
});
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Verifies the deferred-x402 execution dedupe guard:
|
|
4
|
+
// - two CONCURRENT commands carrying the SAME signed authorization (same EIP-3009
|
|
5
|
+
// nonce) against a slow executor result in exactly ONE execution and ONE settle;
|
|
6
|
+
// the duplicate is rejected 409 payment_authorization_in_progress and not charged;
|
|
7
|
+
// - a later command with a DIFFERENT authorization is not blocked;
|
|
8
|
+
// - unit level: the guard is a no-op for non-deferred verifications, fails open when
|
|
9
|
+
// the store is missing, rejects an in-flight duplicate, frees the slot on release,
|
|
10
|
+
// and prunes entries past their TTL.
|
|
11
|
+
//
|
|
12
|
+
// No real network or chain is used.
|
|
13
|
+
|
|
14
|
+
const assert = require("assert");
|
|
15
|
+
const http = require("http");
|
|
16
|
+
const { claimDeferredExecution, releaseDeferredExecution, deferredAuthorizationKey } = require("../lib/execution_dedupe");
|
|
17
|
+
|
|
18
|
+
const ACCEPTS = [
|
|
19
|
+
{
|
|
20
|
+
scheme: "exact",
|
|
21
|
+
network: "eip155:84532",
|
|
22
|
+
asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
23
|
+
amount: "1000",
|
|
24
|
+
payTo: "0x7cd2E0Ed2721319B20FAac4109774d030833F84D",
|
|
25
|
+
extra: { name: "USDC", version: "2" },
|
|
26
|
+
maxTimeoutSeconds: 60
|
|
27
|
+
}
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function startMockServer() {
|
|
31
|
+
const calls = { verify: 0, settle: 0, execSlow: 0 };
|
|
32
|
+
const server = http.createServer((req, res) => {
|
|
33
|
+
const chunks = [];
|
|
34
|
+
req.on("data", (c) => chunks.push(c));
|
|
35
|
+
req.on("end", () => {
|
|
36
|
+
let body = {};
|
|
37
|
+
try { body = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"); } catch (_) { body = {}; }
|
|
38
|
+
const reply = (payload, status) => {
|
|
39
|
+
const text = JSON.stringify(payload);
|
|
40
|
+
res.writeHead(status || 200, { "content-type": "application/json", "content-length": Buffer.byteLength(text) });
|
|
41
|
+
res.end(text);
|
|
42
|
+
};
|
|
43
|
+
if (req.method === "POST" && req.url === "/verify") { calls.verify += 1; return reply({ isValid: true, payer: "0x1111111111111111111111111111111111111111" }); }
|
|
44
|
+
if (req.method === "POST" && req.url === "/settle") { calls.settle += 1; return reply({ success: true, transaction: "0xmocktx", network: "eip155:84532", payer: "0x1111111111111111111111111111111111111111" }); }
|
|
45
|
+
if (req.method === "POST" && req.url === "/exec-slow") {
|
|
46
|
+
calls.execSlow += 1;
|
|
47
|
+
return setTimeout(() => reply({ ok: true, status: "completed", output: { value: "slow-ok" } }), 350);
|
|
48
|
+
}
|
|
49
|
+
if (req.method === "POST" && req.url === "/exec-fail") {
|
|
50
|
+
calls.execFail = (calls.execFail || 0) + 1;
|
|
51
|
+
return reply({ ok: false, reason: "data_required" }, 502);
|
|
52
|
+
}
|
|
53
|
+
res.writeHead(404); res.end();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
return new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve({ server, calls, port: server.address().port })));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function paymentHeader(nonceHex) {
|
|
60
|
+
const accepted = ACCEPTS[0];
|
|
61
|
+
return Buffer.from(JSON.stringify({
|
|
62
|
+
x402Version: 2,
|
|
63
|
+
accepted,
|
|
64
|
+
payload: {
|
|
65
|
+
signature: "ok",
|
|
66
|
+
authorization: {
|
|
67
|
+
from: "0x1111111111111111111111111111111111111111",
|
|
68
|
+
to: accepted.payTo,
|
|
69
|
+
value: accepted.amount,
|
|
70
|
+
validAfter: "0",
|
|
71
|
+
validBefore: String(Math.floor(Date.now() / 1000) + 600),
|
|
72
|
+
nonce: nonceHex
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}), "utf8").toString("base64");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function post(baseUrl, path, body, headers = {}) {
|
|
79
|
+
const res = await fetch(`${baseUrl}${path}`, { method: "POST", headers: { "content-type": "application/json", ...headers }, body: JSON.stringify(body) });
|
|
80
|
+
let json = null;
|
|
81
|
+
try { json = await res.json(); } catch (_) { json = null; }
|
|
82
|
+
return { status: res.status, json, receipt: res.headers.get("payment-response") };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function runUnitChecks() {
|
|
86
|
+
// No-op for a non-deferred verification.
|
|
87
|
+
assert.deepStrictEqual(claimDeferredExecution({ inFlightPaymentAuthorizations: new Map() }, { ok: true }), { ok: true, key: null }, "non-deferred must be a no-op claim");
|
|
88
|
+
// Key derivation.
|
|
89
|
+
const verification = { deferred_settlement: { protocol: "x402_facilitator", authorization: { paymentPayload: { payload: { authorization: { from: "0xABC", nonce: "0xDEAD" } } } } } };
|
|
90
|
+
assert.strictEqual(deferredAuthorizationKey(verification), "x402:0xabc:0xdead", "key must be lowercased from:nonce");
|
|
91
|
+
// Fail open with no store.
|
|
92
|
+
assert.deepStrictEqual(claimDeferredExecution({}, verification), { ok: true, key: null }, "missing store must fail open");
|
|
93
|
+
// In-flight duplicate rejected, then freed on release.
|
|
94
|
+
const state = { inFlightPaymentAuthorizations: new Map() };
|
|
95
|
+
const first = claimDeferredExecution(state, verification);
|
|
96
|
+
assert.strictEqual(first.ok, true, "first claim must succeed");
|
|
97
|
+
const dup = claimDeferredExecution(state, verification);
|
|
98
|
+
assert.strictEqual(dup.ok, false, "second concurrent claim must be rejected");
|
|
99
|
+
assert.strictEqual(dup.status, 409, "duplicate must be 409");
|
|
100
|
+
assert.strictEqual(dup.reason, "payment_authorization_in_progress", "duplicate reason");
|
|
101
|
+
releaseDeferredExecution(state, first.key);
|
|
102
|
+
assert.strictEqual(claimDeferredExecution(state, verification).ok, true, "claim must succeed again after release");
|
|
103
|
+
// TTL prune: an expired entry is forgotten on the next claim.
|
|
104
|
+
const stale = { inFlightPaymentAuthorizations: new Map([["x402:0xabc:0xdead", { status: "in_flight", expires_at: Date.now() - 1 }]]) };
|
|
105
|
+
assert.strictEqual(claimDeferredExecution(stale, verification).ok, true, "an expired slot must be pruned and re-claimable");
|
|
106
|
+
console.log(" ok: unit checks (no-op, key, fail-open, duplicate, release, ttl-prune)");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function main() {
|
|
110
|
+
runUnitChecks();
|
|
111
|
+
|
|
112
|
+
const mock = await startMockServer();
|
|
113
|
+
const mockBase = `http://127.0.0.1:${mock.port}`;
|
|
114
|
+
const prev = {
|
|
115
|
+
mode: process.env.XYTARA_PAYMENT_VERIFICATION_MODE,
|
|
116
|
+
accepts: process.env.XYTARA_X402_ACCEPTS_JSON,
|
|
117
|
+
facUrl: process.env.XYTARA_X402_FACILITATOR_URL,
|
|
118
|
+
targets: process.env.XYTARA_EXECUTION_TARGETS_JSON
|
|
119
|
+
};
|
|
120
|
+
process.env.XYTARA_PAYMENT_VERIFICATION_MODE = "x402_facilitator";
|
|
121
|
+
process.env.XYTARA_X402_ACCEPTS_JSON = JSON.stringify(ACCEPTS);
|
|
122
|
+
process.env.XYTARA_X402_FACILITATOR_URL = mockBase;
|
|
123
|
+
process.env.XYTARA_EXECUTION_TARGETS_JSON = JSON.stringify([
|
|
124
|
+
{ target_id: "exec_slow", task_ref: "exec.slow", endpoint_url: `${mockBase}/exec-slow` },
|
|
125
|
+
{ target_id: "exec_fail", task_ref: "exec.fail", endpoint_url: `${mockBase}/exec-fail` }
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
delete require.cache[require.resolve("../server")];
|
|
129
|
+
const { createServer } = require("../server");
|
|
130
|
+
const server = createServer();
|
|
131
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
132
|
+
const baseUrl = `http://127.0.0.1:${server.address().port}`;
|
|
133
|
+
const execBody = { account_id: "acct_dedupe", command: "slow exec", task_ref: "exec.slow", body: { subject_id: "s-1" } };
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
// Two concurrent requests sharing ONE authorization (same nonce) -> one wins, one 409.
|
|
137
|
+
const sharedNonce = `0x${"ab".repeat(32)}`;
|
|
138
|
+
const [a, b] = await Promise.all([
|
|
139
|
+
post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader(sharedNonce) }),
|
|
140
|
+
post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader(sharedNonce) })
|
|
141
|
+
]);
|
|
142
|
+
const statuses = [a.status, b.status].sort();
|
|
143
|
+
assert.deepStrictEqual(statuses, [200, 409], `exactly one must win and one must 409, got ${statuses}`);
|
|
144
|
+
const winner = a.status === 200 ? a : b;
|
|
145
|
+
const loser = a.status === 409 ? a : b;
|
|
146
|
+
assert.strictEqual(loser.json.reason, "payment_authorization_in_progress", "the duplicate must report in_progress");
|
|
147
|
+
assert.strictEqual(loser.json.payment_settled, false, "the rejected duplicate must not be charged");
|
|
148
|
+
assert.strictEqual(loser.receipt, null, "the rejected duplicate must carry no receipt");
|
|
149
|
+
assert.strictEqual(typeof winner.receipt, "string", "the winner must carry a settlement receipt");
|
|
150
|
+
assert.strictEqual(mock.calls.execSlow, 1, "the external target must have executed exactly once");
|
|
151
|
+
assert.strictEqual(mock.calls.settle, 1, "settlement must have happened exactly once");
|
|
152
|
+
|
|
153
|
+
// A different authorization (fresh nonce) is not blocked.
|
|
154
|
+
const other = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader(`0x${"cd".repeat(32)}`) });
|
|
155
|
+
assert.strictEqual(other.status, 200, "a distinct authorization must not be blocked");
|
|
156
|
+
assert.strictEqual(mock.calls.execSlow, 2, "the distinct authorization must execute");
|
|
157
|
+
|
|
158
|
+
console.log(" ok: concurrent same-authorization -> one execution, one settle, duplicate 409");
|
|
159
|
+
|
|
160
|
+
// Slot is released on a FAILED call, so a legitimate retry (even reusing the same
|
|
161
|
+
// still-unspent nonce) is not wrongly 409'd. Two SEQUENTIAL same-nonce calls to a
|
|
162
|
+
// failing executor must BOTH reach execution and neither be charged.
|
|
163
|
+
const failNonce = `0x${"ef".repeat(32)}`;
|
|
164
|
+
const failBody = { account_id: "acct_dedupe", command: "failing exec", task_ref: "exec.fail", body: { subject_id: "s-2" } };
|
|
165
|
+
const f1 = await post(baseUrl, "/x402/commands/execute", failBody, { "payment-signature": paymentHeader(failNonce) });
|
|
166
|
+
const f2 = await post(baseUrl, "/x402/commands/execute", failBody, { "payment-signature": paymentHeader(failNonce) });
|
|
167
|
+
assert.strictEqual(f1.status, 502, "first failing call must 502 (not 409)");
|
|
168
|
+
assert.strictEqual(f2.status, 502, "retry after failure must reach execution, not be blocked as a duplicate");
|
|
169
|
+
assert.strictEqual(f1.json.payment_settled, false, "failed call must not be charged");
|
|
170
|
+
assert.strictEqual(f2.json.payment_settled, false, "retry must not be charged");
|
|
171
|
+
assert.strictEqual(mock.calls.execFail, 2, "both failing attempts must have executed (slot released each time)");
|
|
172
|
+
console.log(" ok: failed call releases the slot -> retry not blocked, nothing charged");
|
|
173
|
+
|
|
174
|
+
console.log("xytara verify:execution-dedupe passed");
|
|
175
|
+
} finally {
|
|
176
|
+
await new Promise((resolve) => server.close(resolve));
|
|
177
|
+
await new Promise((resolve) => mock.server.close(resolve));
|
|
178
|
+
const restore = (k, v) => { if (v === undefined) delete process.env[k]; else process.env[k] = v; };
|
|
179
|
+
restore("XYTARA_PAYMENT_VERIFICATION_MODE", prev.mode);
|
|
180
|
+
restore("XYTARA_X402_ACCEPTS_JSON", prev.accepts);
|
|
181
|
+
restore("XYTARA_X402_FACILITATOR_URL", prev.facUrl);
|
|
182
|
+
restore("XYTARA_EXECUTION_TARGETS_JSON", prev.targets);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
main().catch((error) => {
|
|
187
|
+
console.error(error && error.message ? error.message : error);
|
|
188
|
+
process.exitCode = 1;
|
|
189
|
+
});
|
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
// Verifies that when a real execution target runs, its actual output is surfaced
|
|
4
|
-
// into the buyer-facing artifacts (command.deliverables + execution.results_by_task_id)
|
|
5
|
-
// — provider-agnostically across the target response shapes — and that the
|
|
6
|
-
// synthetic ("simulated") path is unchanged when no target runs.
|
|
7
|
-
|
|
8
|
-
const assert = require("assert");
|
|
9
|
-
const { createCommandArtifacts } = require("../lib/commerce_artifacts");
|
|
10
|
-
|
|
11
|
-
const basePayload = {
|
|
12
|
-
command: "command.execute",
|
|
13
|
-
tasks: [{ task_id: "t1", task_ref: "adapter.mcp.invoke" }],
|
|
14
|
-
body: { tool_name: "keccak256" }
|
|
15
|
-
};
|
|
16
|
-
const baseQuote = {
|
|
17
|
-
quote_id: "quote_test1",
|
|
18
|
-
account_id: "acct_test",
|
|
19
|
-
amount_minor: 5,
|
|
20
|
-
currency: "USD_CENTS",
|
|
21
|
-
settlement_mode: null,
|
|
22
|
-
integration_ids: [],
|
|
23
|
-
protocols: ["mcp"],
|
|
24
|
-
capabilities: [],
|
|
25
|
-
adapters: ["http"],
|
|
26
|
-
execution_integration_ids: [],
|
|
27
|
-
payment_scheme: "exact",
|
|
28
|
-
payment_protocol: "x402"
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
function run(label, opts, check) {
|
|
32
|
-
const out = createCommandArtifacts(basePayload, baseQuote, opts);
|
|
33
|
-
check({
|
|
34
|
-
out,
|
|
35
|
-
deliv: out.command.deliverables[0],
|
|
36
|
-
taskResult: out.transaction.execution.results_by_task_id.t1
|
|
37
|
-
});
|
|
38
|
-
console.log(" ok:", label);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// 1. Nested MCP-executor shape: { result: { output: <real> } }
|
|
42
|
-
run("nested result.output (MCP executor shape)",
|
|
43
|
-
{ externalExecution: { provider: "mcp", target_id: "tg1", target_response: { ok: true, result: { output: { digest: "0xdeadbeef", length: 8 } }, provider_status: "completed" } } },
|
|
44
|
-
({ deliv, taskResult }) => {
|
|
45
|
-
assert.deepStrictEqual(deliv.output_payload, { digest: "0xdeadbeef", length: 8 }, "deliverable carries real output");
|
|
46
|
-
assert.strictEqual(deliv.invocation_status, "provider_completed");
|
|
47
|
-
assert.strictEqual(deliv.execution_provider, "mcp");
|
|
48
|
-
assert.deepStrictEqual(taskResult.output_payload, { digest: "0xdeadbeef", length: 8 }, "task result carries real output");
|
|
49
|
-
assert.strictEqual(taskResult.invocation_status, "provider_completed");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// 2. Contract-conformant shape: { output_payload, output_ref, provider_status }
|
|
53
|
-
run("conformant output_payload + output_ref",
|
|
54
|
-
{ externalExecution: { provider: "p", target_id: "tg2", target_response: { output_payload: { answer: 42 }, output_ref: "ref_xyz", provider_status: "ok" } } },
|
|
55
|
-
({ deliv, taskResult }) => {
|
|
56
|
-
assert.deepStrictEqual(deliv.output_payload, { answer: 42 });
|
|
57
|
-
assert.strictEqual(deliv.output_ref, "ref_xyz");
|
|
58
|
-
assert.strictEqual(deliv.provider_status, "ok");
|
|
59
|
-
assert.strictEqual(taskResult.output_ref, "ref_xyz");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// 3. Bare output field
|
|
63
|
-
run("bare output field",
|
|
64
|
-
{ externalExecution: { provider: "p", target_id: "tg3", target_response: { output: "hello" } } },
|
|
65
|
-
({ deliv }) => assert.strictEqual(deliv.output_payload, "hello"));
|
|
66
|
-
|
|
67
|
-
// 4. No external target -> synthetic path unchanged
|
|
68
|
-
run("no target -> simulated, no output_payload",
|
|
69
|
-
{},
|
|
70
|
-
({ deliv, taskResult }) => {
|
|
71
|
-
assert.strictEqual(deliv.output_payload, undefined, "no payload when synthetic");
|
|
72
|
-
assert.strictEqual(taskResult.invocation_status, "simulated");
|
|
73
|
-
assert.ok(String(deliv.ref).startsWith("deliverable_"), "placeholder ref preserved");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
console.log("verify_execution_surfacing: all execution output-surfacing checks passed");
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Verifies that when a real execution target runs, its actual output is surfaced
|
|
4
|
+
// into the buyer-facing artifacts (command.deliverables + execution.results_by_task_id)
|
|
5
|
+
// — provider-agnostically across the target response shapes — and that the
|
|
6
|
+
// synthetic ("simulated") path is unchanged when no target runs.
|
|
7
|
+
|
|
8
|
+
const assert = require("assert");
|
|
9
|
+
const { createCommandArtifacts } = require("../lib/commerce_artifacts");
|
|
10
|
+
|
|
11
|
+
const basePayload = {
|
|
12
|
+
command: "command.execute",
|
|
13
|
+
tasks: [{ task_id: "t1", task_ref: "adapter.mcp.invoke" }],
|
|
14
|
+
body: { tool_name: "keccak256" }
|
|
15
|
+
};
|
|
16
|
+
const baseQuote = {
|
|
17
|
+
quote_id: "quote_test1",
|
|
18
|
+
account_id: "acct_test",
|
|
19
|
+
amount_minor: 5,
|
|
20
|
+
currency: "USD_CENTS",
|
|
21
|
+
settlement_mode: null,
|
|
22
|
+
integration_ids: [],
|
|
23
|
+
protocols: ["mcp"],
|
|
24
|
+
capabilities: [],
|
|
25
|
+
adapters: ["http"],
|
|
26
|
+
execution_integration_ids: [],
|
|
27
|
+
payment_scheme: "exact",
|
|
28
|
+
payment_protocol: "x402"
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function run(label, opts, check) {
|
|
32
|
+
const out = createCommandArtifacts(basePayload, baseQuote, opts);
|
|
33
|
+
check({
|
|
34
|
+
out,
|
|
35
|
+
deliv: out.command.deliverables[0],
|
|
36
|
+
taskResult: out.transaction.execution.results_by_task_id.t1
|
|
37
|
+
});
|
|
38
|
+
console.log(" ok:", label);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 1. Nested MCP-executor shape: { result: { output: <real> } }
|
|
42
|
+
run("nested result.output (MCP executor shape)",
|
|
43
|
+
{ externalExecution: { provider: "mcp", target_id: "tg1", target_response: { ok: true, result: { output: { digest: "0xdeadbeef", length: 8 } }, provider_status: "completed" } } },
|
|
44
|
+
({ deliv, taskResult }) => {
|
|
45
|
+
assert.deepStrictEqual(deliv.output_payload, { digest: "0xdeadbeef", length: 8 }, "deliverable carries real output");
|
|
46
|
+
assert.strictEqual(deliv.invocation_status, "provider_completed");
|
|
47
|
+
assert.strictEqual(deliv.execution_provider, "mcp");
|
|
48
|
+
assert.deepStrictEqual(taskResult.output_payload, { digest: "0xdeadbeef", length: 8 }, "task result carries real output");
|
|
49
|
+
assert.strictEqual(taskResult.invocation_status, "provider_completed");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// 2. Contract-conformant shape: { output_payload, output_ref, provider_status }
|
|
53
|
+
run("conformant output_payload + output_ref",
|
|
54
|
+
{ externalExecution: { provider: "p", target_id: "tg2", target_response: { output_payload: { answer: 42 }, output_ref: "ref_xyz", provider_status: "ok" } } },
|
|
55
|
+
({ deliv, taskResult }) => {
|
|
56
|
+
assert.deepStrictEqual(deliv.output_payload, { answer: 42 });
|
|
57
|
+
assert.strictEqual(deliv.output_ref, "ref_xyz");
|
|
58
|
+
assert.strictEqual(deliv.provider_status, "ok");
|
|
59
|
+
assert.strictEqual(taskResult.output_ref, "ref_xyz");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// 3. Bare output field
|
|
63
|
+
run("bare output field",
|
|
64
|
+
{ externalExecution: { provider: "p", target_id: "tg3", target_response: { output: "hello" } } },
|
|
65
|
+
({ deliv }) => assert.strictEqual(deliv.output_payload, "hello"));
|
|
66
|
+
|
|
67
|
+
// 4. No external target -> synthetic path unchanged
|
|
68
|
+
run("no target -> simulated, no output_payload",
|
|
69
|
+
{},
|
|
70
|
+
({ deliv, taskResult }) => {
|
|
71
|
+
assert.strictEqual(deliv.output_payload, undefined, "no payload when synthetic");
|
|
72
|
+
assert.strictEqual(taskResult.invocation_status, "simulated");
|
|
73
|
+
assert.ok(String(deliv.ref).startsWith("deliverable_"), "placeholder ref preserved");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
console.log("verify_execution_surfacing: all execution output-surfacing checks passed");
|