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
package/lib/x402_facilitator.js
CHANGED
|
@@ -1,191 +1,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
|
-
const requirementsForFacilitator = { ...paymentRequirements };
|
|
137
|
-
const verification = await facilitator.verify(paymentPayload, requirementsForFacilitator);
|
|
138
|
-
// A facilitator signals an invalid payment either with a 200 and isValid:false (x402.org) or
|
|
139
|
-
// with a non-2xx status and the same body (Coinbase CDP returns HTTP 400). Treat any response
|
|
140
|
-
// that carries an isValid flag as a verification result; only a missing/garbled body is a
|
|
141
|
-
// genuine transport failure.
|
|
142
|
-
if (!verification.json || typeof verification.json.isValid === "undefined") {
|
|
143
|
-
const body = verification.bodyExcerpt ? ` body=${verification.bodyExcerpt}` : "";
|
|
144
|
-
return { ok: false, status: 402, reason: "facilitator_unreachable", detail: verification.error || `facilitator verify returned HTTP ${verification.status}${body}` };
|
|
145
|
-
}
|
|
146
|
-
if (verification.json.isValid !== true) {
|
|
147
|
-
const detail = normalizeString(verification.json.invalidReason) || normalizeString(verification.json.invalidMessage) || "facilitator rejected the payment";
|
|
148
|
-
return { ok: false, status: 402, reason: "payment_verification_failed", detail };
|
|
149
|
-
}
|
|
150
|
-
return {
|
|
151
|
-
ok: true,
|
|
152
|
-
authorization: {
|
|
153
|
-
paymentPayload,
|
|
154
|
-
paymentRequirements: requirementsForFacilitator,
|
|
155
|
-
payer: normalizeString(verification.json.payer) || null
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Settle a verified payment on-chain via the facilitator (unless settlement is disabled).
|
|
161
|
-
// Returns { ok:true, settlement } or { ok:false, status:402, reason, detail }.
|
|
162
|
-
async function settleX402Payment({ authorization, facilitator, settle }) {
|
|
163
|
-
if (settle === false) {
|
|
164
|
-
return { ok: true, settlement: { success: true, skipped: true, reason: "settlement_disabled_by_operator", payer: authorization.payer } };
|
|
165
|
-
}
|
|
166
|
-
const settlement = await facilitator.settle(authorization.paymentPayload, authorization.paymentRequirements);
|
|
167
|
-
// As with verify, a facilitator may report a failed settlement with a non-2xx status and a
|
|
168
|
-
// body carrying success:false; only a missing/garbled body is a genuine transport failure.
|
|
169
|
-
if (!settlement.json || typeof settlement.json.success === "undefined") {
|
|
170
|
-
const body = settlement.bodyExcerpt ? ` body=${settlement.bodyExcerpt}` : "";
|
|
171
|
-
return { ok: false, status: 402, reason: "facilitator_unreachable", detail: settlement.error || `facilitator settle returned HTTP ${settlement.status}${body}` };
|
|
172
|
-
}
|
|
173
|
-
if (settlement.json.success !== true) {
|
|
174
|
-
const detail = normalizeString(settlement.json.errorReason) || normalizeString(settlement.json.errorMessage) || "facilitator could not settle the payment";
|
|
175
|
-
return { ok: false, status: 402, reason: "payment_settlement_failed", detail };
|
|
176
|
-
}
|
|
177
|
-
return { ok: true, settlement: settlement.json };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function encodePaymentResponse(settlement) {
|
|
181
|
-
return Buffer.from(JSON.stringify(settlement), "utf8").toString("base64");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
module.exports = {
|
|
185
|
-
decodePaymentSignatureHeader,
|
|
186
|
-
matchAcceptedRequirements,
|
|
187
|
-
createFacilitatorClient,
|
|
188
|
-
verifyX402Payment,
|
|
189
|
-
settleX402Payment,
|
|
190
|
-
encodePaymentResponse
|
|
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
|
+
const requirementsForFacilitator = { ...paymentRequirements };
|
|
137
|
+
const verification = await facilitator.verify(paymentPayload, requirementsForFacilitator);
|
|
138
|
+
// A facilitator signals an invalid payment either with a 200 and isValid:false (x402.org) or
|
|
139
|
+
// with a non-2xx status and the same body (Coinbase CDP returns HTTP 400). Treat any response
|
|
140
|
+
// that carries an isValid flag as a verification result; only a missing/garbled body is a
|
|
141
|
+
// genuine transport failure.
|
|
142
|
+
if (!verification.json || typeof verification.json.isValid === "undefined") {
|
|
143
|
+
const body = verification.bodyExcerpt ? ` body=${verification.bodyExcerpt}` : "";
|
|
144
|
+
return { ok: false, status: 402, reason: "facilitator_unreachable", detail: verification.error || `facilitator verify returned HTTP ${verification.status}${body}` };
|
|
145
|
+
}
|
|
146
|
+
if (verification.json.isValid !== true) {
|
|
147
|
+
const detail = normalizeString(verification.json.invalidReason) || normalizeString(verification.json.invalidMessage) || "facilitator rejected the payment";
|
|
148
|
+
return { ok: false, status: 402, reason: "payment_verification_failed", detail };
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
ok: true,
|
|
152
|
+
authorization: {
|
|
153
|
+
paymentPayload,
|
|
154
|
+
paymentRequirements: requirementsForFacilitator,
|
|
155
|
+
payer: normalizeString(verification.json.payer) || null
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Settle a verified payment on-chain via the facilitator (unless settlement is disabled).
|
|
161
|
+
// Returns { ok:true, settlement } or { ok:false, status:402, reason, detail }.
|
|
162
|
+
async function settleX402Payment({ authorization, facilitator, settle }) {
|
|
163
|
+
if (settle === false) {
|
|
164
|
+
return { ok: true, settlement: { success: true, skipped: true, reason: "settlement_disabled_by_operator", payer: authorization.payer } };
|
|
165
|
+
}
|
|
166
|
+
const settlement = await facilitator.settle(authorization.paymentPayload, authorization.paymentRequirements);
|
|
167
|
+
// As with verify, a facilitator may report a failed settlement with a non-2xx status and a
|
|
168
|
+
// body carrying success:false; only a missing/garbled body is a genuine transport failure.
|
|
169
|
+
if (!settlement.json || typeof settlement.json.success === "undefined") {
|
|
170
|
+
const body = settlement.bodyExcerpt ? ` body=${settlement.bodyExcerpt}` : "";
|
|
171
|
+
return { ok: false, status: 402, reason: "facilitator_unreachable", detail: settlement.error || `facilitator settle returned HTTP ${settlement.status}${body}` };
|
|
172
|
+
}
|
|
173
|
+
if (settlement.json.success !== true) {
|
|
174
|
+
const detail = normalizeString(settlement.json.errorReason) || normalizeString(settlement.json.errorMessage) || "facilitator could not settle the payment";
|
|
175
|
+
return { ok: false, status: 402, reason: "payment_settlement_failed", detail };
|
|
176
|
+
}
|
|
177
|
+
return { ok: true, settlement: settlement.json };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function encodePaymentResponse(settlement) {
|
|
181
|
+
return Buffer.from(JSON.stringify(settlement), "utf8").toString("base64");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
decodePaymentSignatureHeader,
|
|
186
|
+
matchAcceptedRequirements,
|
|
187
|
+
createFacilitatorClient,
|
|
188
|
+
verifyX402Payment,
|
|
189
|
+
settleX402Payment,
|
|
190
|
+
encodePaymentResponse
|
|
191
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xytara",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.14",
|
|
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",
|
|
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,8 @@
|
|
|
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"
|
|
127
129
|
}
|
|
128
130
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// Off-site, encrypted backups of the runtime's durable state (the Supabase snapshot row).
|
|
5
|
+
//
|
|
6
|
+
// backup <out_dir> read the live row, sanity-check it, gzip + AES-256-GCM encrypt
|
|
7
|
+
// it, verify the ciphertext decrypts back, write <out_dir>/
|
|
8
|
+
// xytara-state-<persisted>.enc + manifest.json (no secrets)
|
|
9
|
+
// decrypt <file.enc> <out.json> decrypt a backup to the plain row { state_key, persisted_at_iso,
|
|
10
|
+
// snapshot }
|
|
11
|
+
// restore <file.enc> --runtime-stopped [--state-key k]
|
|
12
|
+
// write a decrypted backup back as the live row. ONLY with the
|
|
13
|
+
// runtime stopped (suspended on Render): a running runtime would
|
|
14
|
+
// overwrite it with its in-memory state on the next flush.
|
|
15
|
+
// selftest offline round-trip check (used by verify:all)
|
|
16
|
+
//
|
|
17
|
+
// Env: XYTARA_SUPABASE_URL, XYTARA_SUPABASE_SERVICE_ROLE_KEY, XYTARA_SUPABASE_STATE_TABLE
|
|
18
|
+
// (default xytara_runtime_state), XYTARA_SUPABASE_STATE_KEY (default "default"), STATE_BACKUP_KEY
|
|
19
|
+
// (base64, 32 bytes — keep a copy OFF this system: without it no backup can be decrypted),
|
|
20
|
+
// STATE_BACKUP_MIN_PROVIDERS (default 1), STATE_BACKUP_MIN_BYTES (default 10000).
|
|
21
|
+
// The snapshot contains secrets (provider owner secrets, auth tokens): it is only ever written
|
|
22
|
+
// encrypted, and the manifest carries counts and digests only.
|
|
23
|
+
|
|
24
|
+
const crypto = require("crypto");
|
|
25
|
+
const fs = require("fs");
|
|
26
|
+
const path = require("path");
|
|
27
|
+
const zlib = require("zlib");
|
|
28
|
+
|
|
29
|
+
const ENVELOPE_VERSION = "xytara-state-backup/1";
|
|
30
|
+
|
|
31
|
+
function fail(message) {
|
|
32
|
+
console.error(`state backup: ${message}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function backupKey() {
|
|
37
|
+
const raw = String(process.env.STATE_BACKUP_KEY || "").trim();
|
|
38
|
+
if (!raw) fail("STATE_BACKUP_KEY is not set");
|
|
39
|
+
const key = Buffer.from(raw, "base64");
|
|
40
|
+
if (key.length !== 32) fail("STATE_BACKUP_KEY must be 32 bytes, base64-encoded");
|
|
41
|
+
return key;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function encrypt(rowJson, key) {
|
|
45
|
+
const plaintext = zlib.gzipSync(Buffer.from(rowJson, "utf8"), { level: 9 });
|
|
46
|
+
const iv = crypto.randomBytes(12);
|
|
47
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
48
|
+
cipher.setAAD(Buffer.from(ENVELOPE_VERSION, "utf8"));
|
|
49
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
50
|
+
return {
|
|
51
|
+
v: ENVELOPE_VERSION,
|
|
52
|
+
alg: "AES-256-GCM",
|
|
53
|
+
compression: "gzip",
|
|
54
|
+
iv: iv.toString("base64"),
|
|
55
|
+
tag: cipher.getAuthTag().toString("base64"),
|
|
56
|
+
plaintext_sha256: crypto.createHash("sha256").update(rowJson, "utf8").digest("hex"),
|
|
57
|
+
ciphertext: ciphertext.toString("base64")
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function decrypt(envelope, key) {
|
|
62
|
+
if (!envelope || envelope.v !== ENVELOPE_VERSION) throw new Error("not a xytara state backup envelope");
|
|
63
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(envelope.iv, "base64"));
|
|
64
|
+
decipher.setAAD(Buffer.from(ENVELOPE_VERSION, "utf8"));
|
|
65
|
+
decipher.setAuthTag(Buffer.from(envelope.tag, "base64"));
|
|
66
|
+
const plaintext = Buffer.concat([decipher.update(Buffer.from(envelope.ciphertext, "base64")), decipher.final()]);
|
|
67
|
+
const rowJson = zlib.gunzipSync(plaintext).toString("utf8");
|
|
68
|
+
if (crypto.createHash("sha256").update(rowJson, "utf8").digest("hex") !== envelope.plaintext_sha256) {
|
|
69
|
+
throw new Error("decrypted content digest mismatch");
|
|
70
|
+
}
|
|
71
|
+
return rowJson;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function entryCount(value) {
|
|
75
|
+
if (value && value.__runtime_state_kind && Array.isArray(value.value)) return value.value.length;
|
|
76
|
+
if (Array.isArray(value)) return value.length;
|
|
77
|
+
return value && typeof value === "object" ? Object.keys(value).length : 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function summarize(row, rowJson) {
|
|
81
|
+
const snapshot = row.snapshot || {};
|
|
82
|
+
const counts = {};
|
|
83
|
+
for (const key of ["providerRegistry", "providerSettlementLedger", "providerPayouts", "externalCreditGrants", "accountSpendCredentials", "transactions", "deliveryReceipts", "quotes"]) {
|
|
84
|
+
if (snapshot[key] !== undefined) counts[key] = entryCount(snapshot[key]);
|
|
85
|
+
}
|
|
86
|
+
return { state_key: row.state_key, persisted_at_iso: row.persisted_at_iso, snapshot_bytes: Buffer.byteLength(JSON.stringify(snapshot), "utf8"), row_bytes: Buffer.byteLength(rowJson, "utf8"), section_count: Object.keys(snapshot).length, counts };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function supabaseConfig() {
|
|
90
|
+
const url = String(process.env.XYTARA_SUPABASE_URL || "").trim().replace(/\/+$/, "");
|
|
91
|
+
const serviceKey = String(process.env.XYTARA_SUPABASE_SERVICE_ROLE_KEY || "").trim();
|
|
92
|
+
if (!url || !serviceKey) fail("XYTARA_SUPABASE_URL and XYTARA_SUPABASE_SERVICE_ROLE_KEY are required");
|
|
93
|
+
return {
|
|
94
|
+
url,
|
|
95
|
+
serviceKey,
|
|
96
|
+
table: String(process.env.XYTARA_SUPABASE_STATE_TABLE || "xytara_runtime_state").trim(),
|
|
97
|
+
stateKey: String(process.env.XYTARA_SUPABASE_STATE_KEY || "default").trim()
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function fetchRow(cfg) {
|
|
102
|
+
const endpoint = `${cfg.url}/rest/v1/${encodeURIComponent(cfg.table)}?state_key=eq.${encodeURIComponent(cfg.stateKey)}&select=state_key,snapshot,persisted_at_iso&limit=1`;
|
|
103
|
+
let lastError = null;
|
|
104
|
+
for (let attempt = 1; attempt <= 4; attempt += 1) {
|
|
105
|
+
try {
|
|
106
|
+
const response = await fetch(endpoint, {
|
|
107
|
+
headers: { apikey: cfg.serviceKey, authorization: `Bearer ${cfg.serviceKey}` },
|
|
108
|
+
signal: AbortSignal.timeout(45000)
|
|
109
|
+
});
|
|
110
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${(await response.text()).slice(0, 200)}`);
|
|
111
|
+
const rows = await response.json();
|
|
112
|
+
if (!Array.isArray(rows) || !rows[0]) throw new Error(`no row for state_key "${cfg.stateKey}"`);
|
|
113
|
+
return rows[0];
|
|
114
|
+
} catch (error) {
|
|
115
|
+
lastError = error;
|
|
116
|
+
if (attempt < 4) await new Promise((resolve) => setTimeout(resolve, attempt * 15000));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
throw lastError;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function backup(outDir) {
|
|
123
|
+
if (!outDir) fail("usage: backup <out_dir>");
|
|
124
|
+
const key = backupKey();
|
|
125
|
+
const row = await fetchRow(supabaseConfig()).catch((error) => fail(`could not read the state row: ${error.message}`));
|
|
126
|
+
const rowJson = JSON.stringify(row);
|
|
127
|
+
const summary = summarize(row, rowJson);
|
|
128
|
+
// Sanity gate: a near-empty snapshot means something already went wrong — fail loudly (the
|
|
129
|
+
// failed scheduled run is the alert) instead of quietly storing an empty backup.
|
|
130
|
+
const minProviders = Number(process.env.STATE_BACKUP_MIN_PROVIDERS || 1);
|
|
131
|
+
const minBytes = Number(process.env.STATE_BACKUP_MIN_BYTES || 10000);
|
|
132
|
+
if ((summary.counts.providerRegistry || 0) < minProviders) fail(`snapshot has ${summary.counts.providerRegistry || 0} providers (< ${minProviders}): refusing to treat it as healthy`);
|
|
133
|
+
if (summary.snapshot_bytes < minBytes) fail(`snapshot is only ${summary.snapshot_bytes} bytes (< ${minBytes}): refusing to treat it as healthy`);
|
|
134
|
+
const envelope = encrypt(rowJson, key);
|
|
135
|
+
if (decrypt(envelope, key) !== rowJson) fail("round-trip check failed");
|
|
136
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
137
|
+
const stamp = String(row.persisted_at_iso || new Date().toISOString()).replace(/[^0-9A-Za-z]/g, "").slice(0, 15);
|
|
138
|
+
const fileName = `xytara-state-${stamp}.enc`;
|
|
139
|
+
fs.writeFileSync(path.join(outDir, fileName), JSON.stringify(envelope));
|
|
140
|
+
const manifest = { v: ENVELOPE_VERSION, file: fileName, created_at_iso: new Date().toISOString(), plaintext_sha256: envelope.plaintext_sha256, ...summary };
|
|
141
|
+
fs.writeFileSync(path.join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
142
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function decryptCommand(inFile, outFile) {
|
|
146
|
+
if (!inFile || !outFile) fail("usage: decrypt <file.enc> <out.json>");
|
|
147
|
+
const rowJson = decrypt(JSON.parse(fs.readFileSync(inFile, "utf8")), backupKey());
|
|
148
|
+
fs.writeFileSync(outFile, rowJson);
|
|
149
|
+
console.log(JSON.stringify(summarize(JSON.parse(rowJson), rowJson), null, 2));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function restoreCommand(inFile, args) {
|
|
153
|
+
if (!inFile || !args.includes("--runtime-stopped")) {
|
|
154
|
+
fail("usage: restore <file.enc> --runtime-stopped [--state-key k] (suspend the runtime first, or it will overwrite the restored row)");
|
|
155
|
+
}
|
|
156
|
+
const cfg = supabaseConfig();
|
|
157
|
+
const keyIndex = args.indexOf("--state-key");
|
|
158
|
+
const stateKey = keyIndex >= 0 && args[keyIndex + 1] ? args[keyIndex + 1] : cfg.stateKey;
|
|
159
|
+
const row = JSON.parse(decrypt(JSON.parse(fs.readFileSync(inFile, "utf8")), backupKey()));
|
|
160
|
+
const response = await fetch(`${cfg.url}/rest/v1/${encodeURIComponent(cfg.table)}?on_conflict=state_key`, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: { apikey: cfg.serviceKey, authorization: `Bearer ${cfg.serviceKey}`, "content-type": "application/json", prefer: "resolution=merge-duplicates,return=minimal" },
|
|
163
|
+
body: JSON.stringify([{ state_key: stateKey, snapshot: row.snapshot, persisted_at_iso: row.persisted_at_iso }]),
|
|
164
|
+
signal: AbortSignal.timeout(60000)
|
|
165
|
+
});
|
|
166
|
+
if (!response.ok) fail(`restore write failed: HTTP ${response.status}`);
|
|
167
|
+
console.log(`restored ${row.state_key}@${row.persisted_at_iso} into state_key "${stateKey}". Resume the runtime now.`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function selftest() {
|
|
171
|
+
const key = crypto.randomBytes(32);
|
|
172
|
+
const row = { state_key: "default", persisted_at_iso: "2026-10-17T00:00:00.000Z", snapshot: { providerRegistry: { __runtime_state_kind: "Map", value: [["p", { status: "active", owner_secret: "prvown_x" }]] } } };
|
|
173
|
+
const rowJson = JSON.stringify(row);
|
|
174
|
+
const envelope = encrypt(rowJson, key);
|
|
175
|
+
if (decrypt(envelope, key) !== rowJson) fail("selftest: round trip mismatch");
|
|
176
|
+
if (JSON.stringify(envelope).includes("prvown_x")) fail("selftest: plaintext secret visible in the envelope");
|
|
177
|
+
let wrongKeyRejected = false;
|
|
178
|
+
try { decrypt(envelope, crypto.randomBytes(32)); } catch (_) { wrongKeyRejected = true; }
|
|
179
|
+
if (!wrongKeyRejected) fail("selftest: a wrong key decrypted the backup");
|
|
180
|
+
const tampered = { ...envelope, ciphertext: Buffer.from(Buffer.from(envelope.ciphertext, "base64").map((b, i) => (i === 0 ? b ^ 1 : b))).toString("base64") };
|
|
181
|
+
let tamperRejected = false;
|
|
182
|
+
try { decrypt(tampered, key); } catch (_) { tamperRejected = true; }
|
|
183
|
+
if (!tamperRejected) fail("selftest: tampered ciphertext was accepted");
|
|
184
|
+
console.log("xytara state-backup selftest passed (round trip, no plaintext secrets, wrong key + tamper rejected)");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function main() {
|
|
188
|
+
const [command, ...args] = process.argv.slice(2);
|
|
189
|
+
if (command === "backup") return backup(args[0]);
|
|
190
|
+
if (command === "decrypt") return decryptCommand(args[0], args[1]);
|
|
191
|
+
if (command === "restore") return restoreCommand(args[0], args);
|
|
192
|
+
if (command === "selftest") return selftest();
|
|
193
|
+
fail("usage: state_backup_offsite.js backup <out_dir> | decrypt <file.enc> <out.json> | restore <file.enc> --runtime-stopped | selftest");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
main().catch((error) => fail(error && error.message ? error.message : String(error)));
|