xytara 3.1.13 → 3.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.env.example +38 -4
  2. package/BSV_TERANODE_PLATFORM_DISCONTINUITY_RUNBOOK.md +126 -126
  3. package/REAL_PAYMENT_SETUP.md +14 -7
  4. package/RELEASE_NOTES.md +9 -4
  5. package/fixtures/catalog.js +22 -0
  6. package/lib/a2a_rpc.js +287 -280
  7. package/lib/account_auth.js +585 -585
  8. package/lib/agent_identity.js +162 -162
  9. package/lib/asyncapi_contract.js +3 -3
  10. package/lib/bolt_adapter.js +187 -187
  11. package/lib/bolt_payment_front.js +224 -224
  12. package/lib/buyer_event_log.js +215 -0
  13. package/lib/buyer_safety_policy.js +132 -0
  14. package/lib/checkout_processor_adapter.js +217 -217
  15. package/lib/command_flow.js +911 -722
  16. package/lib/commerce_artifacts.js +591 -591
  17. package/lib/commerce_bridge.js +274 -274
  18. package/lib/commerce_cases.js +399 -399
  19. package/lib/commerce_client.js +7694 -7694
  20. package/lib/commerce_quotes.js +29 -0
  21. package/lib/commerce_runtime.js +322 -322
  22. package/lib/execution_dedupe.js +86 -86
  23. package/lib/external_credit_bridge.js +142 -142
  24. package/lib/http_transport.js +99 -81
  25. package/lib/l402_adapter.js +241 -241
  26. package/lib/l402_payment_front.js +265 -265
  27. package/lib/mpp_adapter.js +237 -237
  28. package/lib/openapi_contract.js +4 -0
  29. package/lib/payment_config.js +257 -253
  30. package/lib/payment_verification.js +357 -357
  31. package/lib/provider_payout_execution.js +155 -155
  32. package/lib/provider_registry.js +852 -842
  33. package/lib/rate_limiter.js +81 -81
  34. package/lib/runtime_state_store.js +495 -369
  35. package/lib/settlement_bsv_live.js +600 -600
  36. package/lib/settlement_bsv_teranode.js +117 -117
  37. package/lib/stripe_mpp_payment_front.js +258 -258
  38. package/lib/x402_facilitator.js +198 -191
  39. package/lib/x402_lane_contract.js +5 -3
  40. package/lib/x402_settlement_recovery.js +175 -0
  41. package/package.json +9 -3
  42. package/scripts/buyer_events_daily_report.js +79 -0
  43. package/scripts/state_backup_offsite.js +196 -0
  44. package/scripts/verify_a2a_rpc.js +131 -105
  45. package/scripts/verify_agent_identity.js +153 -153
  46. package/scripts/verify_all.js +1 -1
  47. package/scripts/verify_bolt_adapter.js +128 -128
  48. package/scripts/verify_buyer_event_log.js +123 -0
  49. package/scripts/verify_buyer_safety.js +141 -0
  50. package/scripts/verify_cdp_facilitator.js +110 -110
  51. package/scripts/verify_checkout_processor.js +109 -109
  52. package/scripts/verify_commerce_bridge.js +177 -177
  53. package/scripts/verify_execution_dedupe.js +189 -189
  54. package/scripts/verify_execution_surfacing.js +76 -76
  55. package/scripts/verify_final_activation.js +191 -191
  56. package/scripts/verify_l402_adapter.js +86 -86
  57. package/scripts/verify_lifecycle_spend_binding.js +88 -0
  58. package/scripts/verify_mpp_adapter.js +128 -128
  59. package/scripts/verify_production_readiness.js +268 -268
  60. package/scripts/verify_rate_limiter.js +52 -52
  61. package/scripts/verify_refund_credit.js +106 -106
  62. package/scripts/verify_release_candidate.js +81 -81
  63. package/scripts/verify_runtime_state_guard.js +117 -0
  64. package/scripts/verify_service.js +15421 -15421
  65. package/scripts/verify_verify_execute_settle.js +209 -208
  66. package/scripts/verify_x402_facilitator.js +307 -286
  67. package/scripts/verify_x402_settlement_recovery.js +153 -0
  68. package/server.js +12242 -11974
@@ -1,237 +1,237 @@
1
- "use strict";
2
-
3
- // Provider-agnostic MPP (per-call card/stablecoin payment) adapter.
4
- //
5
- // Makes the MPP/SPT lane real by verifying a Stripe PaymentIntent or Checkout
6
- // Session referenced in the authorization: it must be paid/succeeded and cover
7
- // at least the quoted amount. The provider reference is the proof; replay is
8
- // prevented by treating each reference as single-use. Engages only when env
9
- // configures Stripe; otherwise the caller preserves the existing token scheme.
10
- // Dependency-free (node https + an injectable fetcher for tests).
11
- //
12
- // Env:
13
- // XYTARA_MPP_PROVIDER = "stripe" (anything else => not configured)
14
- // XYTARA_MPP_STRIPE_SECRET_KEY | STRIPE_SECRET_KEY
15
- // XYTARA_MPP_ROOT_KEY | STRIPE_WEBHOOK_SECRET (HMAC key for the binding ref;
16
- // falls back to the secret key)
17
-
18
- // REQUEST BINDING (production-safe):
19
- // - The runtime CREATES the PaymentIntent server-side for a specific quote and
20
- // stamps metadata.naxytra_ref = HMAC(bindingSecret, quote). verifyMppPayment
21
- // requires the presented object to carry that exact ref (timing-safe), so a
22
- // paid Stripe object the caller controls for some OTHER purpose cannot be
23
- // replayed to authorize this call. Each reference is also single-use.
24
- // - Still a flat per-object check otherwise: it confirms paid + USD + covers the
25
- // quote amount. Processor minimums apply (Stripe will reject amounts below its
26
- // per-charge floor, ~$0.50 USD); price MPP calls at or above that floor.
27
-
28
- const https = require("https");
29
-
30
- function env(name) {
31
- const v = process.env[name];
32
- return typeof v === "string" && v.trim() ? v.trim() : "";
33
- }
34
-
35
- function loadMppConfig() {
36
- const provider = env("XYTARA_MPP_PROVIDER").toLowerCase();
37
- if (provider !== "stripe") return { provider: null };
38
- const secretKey = env("XYTARA_MPP_STRIPE_SECRET_KEY") || env("STRIPE_SECRET_KEY");
39
- return {
40
- provider: "stripe",
41
- secretKey,
42
- bindingSecret: env("XYTARA_MPP_ROOT_KEY") || env("STRIPE_WEBHOOK_SECRET") || secretKey
43
- };
44
- }
45
-
46
- function isConfigured(config) {
47
- return Boolean(config && config.provider);
48
- }
49
-
50
- const crypto = require("crypto");
51
-
52
- // Deterministic, server-only reference binding a payment to one quote. The caller
53
- // cannot forge it without bindingSecret, so only a payment created by the runtime
54
- // for THIS quote carries a matching metadata.naxytra_ref.
55
- function computeBindingRef(bindingSecret, quote) {
56
- const q = quote || {};
57
- const basis = `${q.quote_id || ""}:${q.amount_minor || ""}:${q.currency || ""}`;
58
- const mac = crypto.createHmac("sha256", String(bindingSecret || "")).update(basis).digest("hex");
59
- return "nx_" + mac.slice(0, 32);
60
- }
61
-
62
- function refEquals(a, b) {
63
- const x = Buffer.from(String(a || ""));
64
- const y = Buffer.from(String(b || ""));
65
- if (x.length === 0 || x.length !== y.length) return false;
66
- try {
67
- return crypto.timingSafeEqual(x, y);
68
- } catch (_) {
69
- return false;
70
- }
71
- }
72
-
73
- function stripeGet(path, secretKey, timeoutMs) {
74
- return new Promise((resolve, reject) => {
75
- const req = https.request(
76
- { method: "GET", hostname: "api.stripe.com", path, headers: { Authorization: "Bearer " + secretKey } },
77
- (res) => {
78
- let d = "";
79
- res.on("data", (c) => (d += c));
80
- res.on("end", () => {
81
- let j = null;
82
- try {
83
- j = d ? JSON.parse(d) : null;
84
- } catch (_) {
85
- j = null;
86
- }
87
- resolve({ status: res.statusCode, json: j });
88
- });
89
- }
90
- );
91
- req.on("error", reject);
92
- req.setTimeout(timeoutMs || 15000, () => req.destroy(new Error("stripe_get_timeout")));
93
- req.end();
94
- });
95
- }
96
-
97
- function stripePost(path, formParams, secretKey, timeoutMs) {
98
- const body = new URLSearchParams(formParams).toString();
99
- return new Promise((resolve, reject) => {
100
- const req = https.request(
101
- {
102
- method: "POST",
103
- hostname: "api.stripe.com",
104
- path,
105
- headers: {
106
- Authorization: "Bearer " + secretKey,
107
- "Content-Type": "application/x-www-form-urlencoded",
108
- "Content-Length": Buffer.byteLength(body)
109
- }
110
- },
111
- (res) => {
112
- let d = "";
113
- res.on("data", (c) => (d += c));
114
- res.on("end", () => {
115
- let j = null;
116
- try {
117
- j = d ? JSON.parse(d) : null;
118
- } catch (_) {
119
- j = null;
120
- }
121
- resolve({ status: res.statusCode, json: j });
122
- });
123
- }
124
- );
125
- req.on("error", reject);
126
- req.setTimeout(timeoutMs || 15000, () => req.destroy(new Error("stripe_post_timeout")));
127
- req.write(body);
128
- req.end();
129
- });
130
- }
131
-
132
- // Create a PaymentIntent for a quote, stamping the binding ref in metadata.
133
- // `intent` = { amountMinor, currency, ref, quoteId }. `postOverride` lets tests
134
- // inject a fetcher. Returns { ok, reference, client_secret } | { ok:false, reason, detail? }.
135
- async function createMppPaymentIntent(config, intent, postOverride) {
136
- if (!isConfigured(config)) return { ok: false, reason: "mpp_provider_not_configured" };
137
- if (config.provider !== "stripe") return { ok: false, reason: "unsupported_provider" };
138
- if (!config.secretKey) return { ok: false, reason: "stripe_secret_key_missing" };
139
- const amountMinor = Math.round(Number(intent && intent.amountMinor));
140
- if (!Number.isFinite(amountMinor) || amountMinor <= 0) return { ok: false, reason: "invalid_amount" };
141
- const currency = String((intent && intent.currency) || "usd").toLowerCase();
142
- const params = {
143
- amount: String(amountMinor),
144
- currency: currency === "usd_cents" ? "usd" : currency,
145
- "automatic_payment_methods[enabled]": "true",
146
- "metadata[naxytra_ref]": String((intent && intent.ref) || ""),
147
- "metadata[quote_id]": String((intent && intent.quoteId) || "")
148
- };
149
- const post = typeof postOverride === "function" ? postOverride : (p, f) => stripePost(p, f, config.secretKey, 15000);
150
- let res;
151
- try {
152
- res = await post("/v1/payment_intents", params);
153
- } catch (e) {
154
- return { ok: false, reason: "stripe_unreachable", detail: e && e.message ? e.message : "request_error" };
155
- }
156
- if (!res.json || res.status >= 400) {
157
- return { ok: false, reason: "stripe_payment_intent_create_failed", detail: (res.json && res.json.error && res.json.error.message) || `HTTP ${res.status}` };
158
- }
159
- return { ok: true, reference: res.json.id, client_secret: res.json.client_secret };
160
- }
161
-
162
- // Parse "SPT <stripe_reference>" (pi_... or cs_...). Returns { reference } | null.
163
- function parseMppHeader(authorization) {
164
- const m = String(authorization || "").match(/^SPT\s+((?:pi|cs)_[A-Za-z0-9_]+)\s*$/);
165
- return m ? { reference: m[1] } : null;
166
- }
167
-
168
- // Verify a Stripe reference is paid and covers requiredAmountMinor.
169
- // `getOverride` lets tests inject a fetcher. Returns
170
- // { ok, reference, amount_minor, currency, status } | { ok:false, reason, detail? }.
171
- async function verifyMppPayment(config, reference, requiredAmountMinor, getOverride, options) {
172
- if (!isConfigured(config)) return { ok: false, reason: "mpp_provider_not_configured" };
173
- if (config.provider === "stripe") {
174
- if (!config.secretKey) return { ok: false, reason: "stripe_secret_key_missing" };
175
- const ref = String(reference || "");
176
- const path = ref.startsWith("pi_") ? "/v1/payment_intents/" + ref : ref.startsWith("cs_") ? "/v1/checkout/sessions/" + ref : null;
177
- if (!path) return { ok: false, reason: "mpp_reference_invalid" };
178
- const get = typeof getOverride === "function" ? getOverride : (p) => stripeGet(p, config.secretKey, 15000);
179
- let res;
180
- try {
181
- res = await get(path);
182
- } catch (e) {
183
- return { ok: false, reason: "stripe_unreachable", detail: e && e.message ? e.message : "request_error" };
184
- }
185
- if (!res.json || res.status >= 400) {
186
- return { ok: false, reason: "stripe_reference_not_found", detail: (res.json && res.json.error && res.json.error.message) || `HTTP ${res.status}` };
187
- }
188
- const obj = res.json;
189
- const paid = ref.startsWith("pi_") ? obj.status === "succeeded" : obj.payment_status === "paid";
190
- if (!paid) return { ok: false, reason: "mpp_not_paid", detail: obj.status || obj.payment_status || "unknown" };
191
- const amount = Number(ref.startsWith("pi_") ? obj.amount_received || obj.amount : obj.amount_total);
192
- const required = Number(requiredAmountMinor);
193
- // Never authorize for free: a known, positive required amount is mandatory.
194
- if (!Number.isFinite(required) || required <= 0) {
195
- return { ok: false, reason: "mpp_required_amount_unknown" };
196
- }
197
- if (!Number.isFinite(amount) || amount < required) {
198
- return { ok: false, reason: "mpp_amount_insufficient", detail: `${amount}/${required}` };
199
- }
200
- // The runtime prices in USD minor units; reject a payment in another currency
201
- // (e.g. a high nominal amount in a zero-decimal currency) that would not match.
202
- const currency = String(obj.currency || "").toLowerCase();
203
- if (currency && currency !== "usd") {
204
- return { ok: false, reason: "mpp_currency_mismatch", detail: currency };
205
- }
206
- // Request binding: when the caller expects a server-issued ref (it always
207
- // does in production), the paid object must carry the matching
208
- // metadata.naxytra_ref, proving it was created for THIS quote.
209
- const expectedRef = options && options.expectedRef;
210
- if (expectedRef) {
211
- const presented = obj.metadata && obj.metadata.naxytra_ref;
212
- if (!presented) return { ok: false, reason: "mpp_binding_ref_missing" };
213
- if (!refEquals(presented, expectedRef)) return { ok: false, reason: "mpp_binding_ref_mismatch" };
214
- }
215
- return { ok: true, reference: ref, amount_minor: amount, currency: (obj.currency || "usd").toUpperCase(), status: "paid" };
216
- }
217
- return { ok: false, reason: "unsupported_provider" };
218
- }
219
-
220
- function buildMppChallenge(quote) {
221
- return {
222
- provider: "stripe",
223
- amount_minor: quote && quote.amount_minor,
224
- currency: quote && quote.currency,
225
- instructions: "Create a Stripe PaymentIntent (or Checkout Session) for the quoted amount, then retry with 'Authorization: SPT <pi_... | cs_...>'."
226
- };
227
- }
228
-
229
- module.exports = {
230
- loadMppConfig,
231
- isConfigured,
232
- parseMppHeader,
233
- verifyMppPayment,
234
- buildMppChallenge,
235
- createMppPaymentIntent,
236
- computeBindingRef
237
- };
1
+ "use strict";
2
+
3
+ // Provider-agnostic MPP (per-call card/stablecoin payment) adapter.
4
+ //
5
+ // Makes the MPP/SPT lane real by verifying a Stripe PaymentIntent or Checkout
6
+ // Session referenced in the authorization: it must be paid/succeeded and cover
7
+ // at least the quoted amount. The provider reference is the proof; replay is
8
+ // prevented by treating each reference as single-use. Engages only when env
9
+ // configures Stripe; otherwise the caller preserves the existing token scheme.
10
+ // Dependency-free (node https + an injectable fetcher for tests).
11
+ //
12
+ // Env:
13
+ // XYTARA_MPP_PROVIDER = "stripe" (anything else => not configured)
14
+ // XYTARA_MPP_STRIPE_SECRET_KEY | STRIPE_SECRET_KEY
15
+ // XYTARA_MPP_ROOT_KEY | STRIPE_WEBHOOK_SECRET (HMAC key for the binding ref;
16
+ // falls back to the secret key)
17
+
18
+ // REQUEST BINDING (production-safe):
19
+ // - The runtime CREATES the PaymentIntent server-side for a specific quote and
20
+ // stamps metadata.naxytra_ref = HMAC(bindingSecret, quote). verifyMppPayment
21
+ // requires the presented object to carry that exact ref (timing-safe), so a
22
+ // paid Stripe object the caller controls for some OTHER purpose cannot be
23
+ // replayed to authorize this call. Each reference is also single-use.
24
+ // - Still a flat per-object check otherwise: it confirms paid + USD + covers the
25
+ // quote amount. Processor minimums apply (Stripe will reject amounts below its
26
+ // per-charge floor, ~$0.50 USD); price MPP calls at or above that floor.
27
+
28
+ const https = require("https");
29
+
30
+ function env(name) {
31
+ const v = process.env[name];
32
+ return typeof v === "string" && v.trim() ? v.trim() : "";
33
+ }
34
+
35
+ function loadMppConfig() {
36
+ const provider = env("XYTARA_MPP_PROVIDER").toLowerCase();
37
+ if (provider !== "stripe") return { provider: null };
38
+ const secretKey = env("XYTARA_MPP_STRIPE_SECRET_KEY") || env("STRIPE_SECRET_KEY");
39
+ return {
40
+ provider: "stripe",
41
+ secretKey,
42
+ bindingSecret: env("XYTARA_MPP_ROOT_KEY") || env("STRIPE_WEBHOOK_SECRET") || secretKey
43
+ };
44
+ }
45
+
46
+ function isConfigured(config) {
47
+ return Boolean(config && config.provider);
48
+ }
49
+
50
+ const crypto = require("crypto");
51
+
52
+ // Deterministic, server-only reference binding a payment to one quote. The caller
53
+ // cannot forge it without bindingSecret, so only a payment created by the runtime
54
+ // for THIS quote carries a matching metadata.naxytra_ref.
55
+ function computeBindingRef(bindingSecret, quote) {
56
+ const q = quote || {};
57
+ const basis = `${q.quote_id || ""}:${q.amount_minor || ""}:${q.currency || ""}`;
58
+ const mac = crypto.createHmac("sha256", String(bindingSecret || "")).update(basis).digest("hex");
59
+ return "nx_" + mac.slice(0, 32);
60
+ }
61
+
62
+ function refEquals(a, b) {
63
+ const x = Buffer.from(String(a || ""));
64
+ const y = Buffer.from(String(b || ""));
65
+ if (x.length === 0 || x.length !== y.length) return false;
66
+ try {
67
+ return crypto.timingSafeEqual(x, y);
68
+ } catch (_) {
69
+ return false;
70
+ }
71
+ }
72
+
73
+ function stripeGet(path, secretKey, timeoutMs) {
74
+ return new Promise((resolve, reject) => {
75
+ const req = https.request(
76
+ { method: "GET", hostname: "api.stripe.com", path, headers: { Authorization: "Bearer " + secretKey } },
77
+ (res) => {
78
+ let d = "";
79
+ res.on("data", (c) => (d += c));
80
+ res.on("end", () => {
81
+ let j = null;
82
+ try {
83
+ j = d ? JSON.parse(d) : null;
84
+ } catch (_) {
85
+ j = null;
86
+ }
87
+ resolve({ status: res.statusCode, json: j });
88
+ });
89
+ }
90
+ );
91
+ req.on("error", reject);
92
+ req.setTimeout(timeoutMs || 15000, () => req.destroy(new Error("stripe_get_timeout")));
93
+ req.end();
94
+ });
95
+ }
96
+
97
+ function stripePost(path, formParams, secretKey, timeoutMs) {
98
+ const body = new URLSearchParams(formParams).toString();
99
+ return new Promise((resolve, reject) => {
100
+ const req = https.request(
101
+ {
102
+ method: "POST",
103
+ hostname: "api.stripe.com",
104
+ path,
105
+ headers: {
106
+ Authorization: "Bearer " + secretKey,
107
+ "Content-Type": "application/x-www-form-urlencoded",
108
+ "Content-Length": Buffer.byteLength(body)
109
+ }
110
+ },
111
+ (res) => {
112
+ let d = "";
113
+ res.on("data", (c) => (d += c));
114
+ res.on("end", () => {
115
+ let j = null;
116
+ try {
117
+ j = d ? JSON.parse(d) : null;
118
+ } catch (_) {
119
+ j = null;
120
+ }
121
+ resolve({ status: res.statusCode, json: j });
122
+ });
123
+ }
124
+ );
125
+ req.on("error", reject);
126
+ req.setTimeout(timeoutMs || 15000, () => req.destroy(new Error("stripe_post_timeout")));
127
+ req.write(body);
128
+ req.end();
129
+ });
130
+ }
131
+
132
+ // Create a PaymentIntent for a quote, stamping the binding ref in metadata.
133
+ // `intent` = { amountMinor, currency, ref, quoteId }. `postOverride` lets tests
134
+ // inject a fetcher. Returns { ok, reference, client_secret } | { ok:false, reason, detail? }.
135
+ async function createMppPaymentIntent(config, intent, postOverride) {
136
+ if (!isConfigured(config)) return { ok: false, reason: "mpp_provider_not_configured" };
137
+ if (config.provider !== "stripe") return { ok: false, reason: "unsupported_provider" };
138
+ if (!config.secretKey) return { ok: false, reason: "stripe_secret_key_missing" };
139
+ const amountMinor = Math.round(Number(intent && intent.amountMinor));
140
+ if (!Number.isFinite(amountMinor) || amountMinor <= 0) return { ok: false, reason: "invalid_amount" };
141
+ const currency = String((intent && intent.currency) || "usd").toLowerCase();
142
+ const params = {
143
+ amount: String(amountMinor),
144
+ currency: currency === "usd_cents" ? "usd" : currency,
145
+ "automatic_payment_methods[enabled]": "true",
146
+ "metadata[naxytra_ref]": String((intent && intent.ref) || ""),
147
+ "metadata[quote_id]": String((intent && intent.quoteId) || "")
148
+ };
149
+ const post = typeof postOverride === "function" ? postOverride : (p, f) => stripePost(p, f, config.secretKey, 15000);
150
+ let res;
151
+ try {
152
+ res = await post("/v1/payment_intents", params);
153
+ } catch (e) {
154
+ return { ok: false, reason: "stripe_unreachable", detail: e && e.message ? e.message : "request_error" };
155
+ }
156
+ if (!res.json || res.status >= 400) {
157
+ return { ok: false, reason: "stripe_payment_intent_create_failed", detail: (res.json && res.json.error && res.json.error.message) || `HTTP ${res.status}` };
158
+ }
159
+ return { ok: true, reference: res.json.id, client_secret: res.json.client_secret };
160
+ }
161
+
162
+ // Parse "SPT <stripe_reference>" (pi_... or cs_...). Returns { reference } | null.
163
+ function parseMppHeader(authorization) {
164
+ const m = String(authorization || "").match(/^SPT\s+((?:pi|cs)_[A-Za-z0-9_]+)\s*$/);
165
+ return m ? { reference: m[1] } : null;
166
+ }
167
+
168
+ // Verify a Stripe reference is paid and covers requiredAmountMinor.
169
+ // `getOverride` lets tests inject a fetcher. Returns
170
+ // { ok, reference, amount_minor, currency, status } | { ok:false, reason, detail? }.
171
+ async function verifyMppPayment(config, reference, requiredAmountMinor, getOverride, options) {
172
+ if (!isConfigured(config)) return { ok: false, reason: "mpp_provider_not_configured" };
173
+ if (config.provider === "stripe") {
174
+ if (!config.secretKey) return { ok: false, reason: "stripe_secret_key_missing" };
175
+ const ref = String(reference || "");
176
+ const path = ref.startsWith("pi_") ? "/v1/payment_intents/" + ref : ref.startsWith("cs_") ? "/v1/checkout/sessions/" + ref : null;
177
+ if (!path) return { ok: false, reason: "mpp_reference_invalid" };
178
+ const get = typeof getOverride === "function" ? getOverride : (p) => stripeGet(p, config.secretKey, 15000);
179
+ let res;
180
+ try {
181
+ res = await get(path);
182
+ } catch (e) {
183
+ return { ok: false, reason: "stripe_unreachable", detail: e && e.message ? e.message : "request_error" };
184
+ }
185
+ if (!res.json || res.status >= 400) {
186
+ return { ok: false, reason: "stripe_reference_not_found", detail: (res.json && res.json.error && res.json.error.message) || `HTTP ${res.status}` };
187
+ }
188
+ const obj = res.json;
189
+ const paid = ref.startsWith("pi_") ? obj.status === "succeeded" : obj.payment_status === "paid";
190
+ if (!paid) return { ok: false, reason: "mpp_not_paid", detail: obj.status || obj.payment_status || "unknown" };
191
+ const amount = Number(ref.startsWith("pi_") ? obj.amount_received || obj.amount : obj.amount_total);
192
+ const required = Number(requiredAmountMinor);
193
+ // Never authorize for free: a known, positive required amount is mandatory.
194
+ if (!Number.isFinite(required) || required <= 0) {
195
+ return { ok: false, reason: "mpp_required_amount_unknown" };
196
+ }
197
+ if (!Number.isFinite(amount) || amount < required) {
198
+ return { ok: false, reason: "mpp_amount_insufficient", detail: `${amount}/${required}` };
199
+ }
200
+ // The runtime prices in USD minor units; reject a payment in another currency
201
+ // (e.g. a high nominal amount in a zero-decimal currency) that would not match.
202
+ const currency = String(obj.currency || "").toLowerCase();
203
+ if (currency && currency !== "usd") {
204
+ return { ok: false, reason: "mpp_currency_mismatch", detail: currency };
205
+ }
206
+ // Request binding: when the caller expects a server-issued ref (it always
207
+ // does in production), the paid object must carry the matching
208
+ // metadata.naxytra_ref, proving it was created for THIS quote.
209
+ const expectedRef = options && options.expectedRef;
210
+ if (expectedRef) {
211
+ const presented = obj.metadata && obj.metadata.naxytra_ref;
212
+ if (!presented) return { ok: false, reason: "mpp_binding_ref_missing" };
213
+ if (!refEquals(presented, expectedRef)) return { ok: false, reason: "mpp_binding_ref_mismatch" };
214
+ }
215
+ return { ok: true, reference: ref, amount_minor: amount, currency: (obj.currency || "usd").toUpperCase(), status: "paid" };
216
+ }
217
+ return { ok: false, reason: "unsupported_provider" };
218
+ }
219
+
220
+ function buildMppChallenge(quote) {
221
+ return {
222
+ provider: "stripe",
223
+ amount_minor: quote && quote.amount_minor,
224
+ currency: quote && quote.currency,
225
+ instructions: "Create a Stripe PaymentIntent (or Checkout Session) for the quoted amount, then retry with 'Authorization: SPT <pi_... | cs_...>'."
226
+ };
227
+ }
228
+
229
+ module.exports = {
230
+ loadMppConfig,
231
+ isConfigured,
232
+ parseMppHeader,
233
+ verifyMppPayment,
234
+ buildMppChallenge,
235
+ createMppPaymentIntent,
236
+ computeBindingRef
237
+ };
@@ -16,6 +16,10 @@ function buildOpenApiDocument() {
16
16
  },
17
17
  jsonSchemaDialect: "https://spec.openapis.org/oas/3.1/dialect/base",
18
18
  servers: [
19
+ {
20
+ url: String(process.env.XYTARA_PUBLIC_BASE_URL || "https://xytara.onrender.com").trim().replace(/\/+$/, ""),
21
+ description: "Hosted xytara runtime"
22
+ },
19
23
  {
20
24
  url: "http://127.0.0.1:4320",
21
25
  description: "Local xytara service"