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,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
+ };