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.
Files changed (55) hide show
  1. package/.env.example +8 -1
  2. package/BSV_TERANODE_PLATFORM_DISCONTINUITY_RUNBOOK.md +126 -126
  3. package/RELEASE_NOTES.md +6 -4
  4. package/lib/a2a_rpc.js +280 -280
  5. package/lib/account_auth.js +585 -585
  6. package/lib/agent_identity.js +162 -162
  7. package/lib/bolt_adapter.js +187 -187
  8. package/lib/bolt_payment_front.js +224 -224
  9. package/lib/checkout_processor_adapter.js +217 -217
  10. package/lib/command_flow.js +722 -722
  11. package/lib/commerce_artifacts.js +591 -591
  12. package/lib/commerce_bridge.js +274 -274
  13. package/lib/commerce_cases.js +399 -399
  14. package/lib/commerce_client.js +7694 -7694
  15. package/lib/commerce_quotes.js +29 -0
  16. package/lib/commerce_runtime.js +322 -322
  17. package/lib/execution_dedupe.js +86 -86
  18. package/lib/external_credit_bridge.js +142 -142
  19. package/lib/http_transport.js +81 -81
  20. package/lib/l402_adapter.js +241 -241
  21. package/lib/l402_payment_front.js +265 -265
  22. package/lib/mpp_adapter.js +237 -237
  23. package/lib/payment_config.js +253 -253
  24. package/lib/payment_verification.js +357 -357
  25. package/lib/provider_payout_execution.js +155 -155
  26. package/lib/provider_registry.js +842 -842
  27. package/lib/rate_limiter.js +81 -81
  28. package/lib/runtime_state_store.js +491 -369
  29. package/lib/settlement_bsv_live.js +600 -600
  30. package/lib/settlement_bsv_teranode.js +117 -117
  31. package/lib/stripe_mpp_payment_front.js +258 -258
  32. package/lib/x402_facilitator.js +191 -191
  33. package/package.json +5 -3
  34. package/scripts/state_backup_offsite.js +196 -0
  35. package/scripts/verify_a2a_rpc.js +105 -105
  36. package/scripts/verify_agent_identity.js +153 -153
  37. package/scripts/verify_all.js +1 -1
  38. package/scripts/verify_bolt_adapter.js +128 -128
  39. package/scripts/verify_cdp_facilitator.js +110 -110
  40. package/scripts/verify_checkout_processor.js +109 -109
  41. package/scripts/verify_commerce_bridge.js +177 -177
  42. package/scripts/verify_execution_dedupe.js +189 -189
  43. package/scripts/verify_execution_surfacing.js +76 -76
  44. package/scripts/verify_final_activation.js +191 -191
  45. package/scripts/verify_l402_adapter.js +86 -86
  46. package/scripts/verify_mpp_adapter.js +128 -128
  47. package/scripts/verify_production_readiness.js +268 -268
  48. package/scripts/verify_rate_limiter.js +52 -52
  49. package/scripts/verify_refund_credit.js +106 -106
  50. package/scripts/verify_release_candidate.js +81 -81
  51. package/scripts/verify_runtime_state_guard.js +117 -0
  52. package/scripts/verify_service.js +15421 -15421
  53. package/scripts/verify_verify_execute_settle.js +208 -208
  54. package/scripts/verify_x402_facilitator.js +286 -286
  55. package/server.js +11976 -11974
@@ -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.13",
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)));