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/mpp_adapter.js
CHANGED
|
@@ -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
|
+
};
|