xytara 3.1.4 → 3.1.6
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 +4 -0
- package/REAL_PAYMENT_SETUP.md +12 -8
- package/RELEASE_NOTES.md +6 -5
- package/examples/generated.carried-handoff.json +3 -3
- package/lib/cdp_jwt.js +57 -0
- package/lib/payment_config.js +253 -242
- package/lib/payment_verification.js +313 -312
- package/lib/x402_facilitator.js +181 -155
- package/package.json +4 -3
- package/scripts/verify_cdp_facilitator.js +110 -0
- package/scripts/verify_x402_facilitator.js +202 -194
- package/server.js +11227 -11226
|
@@ -1,312 +1,313 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const crypto = require("crypto");
|
|
4
|
-
const { loadPaymentConfig } = require("./payment_config");
|
|
5
|
-
const {
|
|
6
|
-
createFacilitatorClient,
|
|
7
|
-
verifyX402Payment,
|
|
8
|
-
settleX402Payment,
|
|
9
|
-
encodePaymentResponse
|
|
10
|
-
} = require("./x402_facilitator");
|
|
11
|
-
const { parseL402Authorization, verifyL402AccessToken } = require("./l402_payment_front");
|
|
12
|
-
const { parseBoltAuthorization, verifyBoltAccessToken } = require("./bolt_payment_front");
|
|
13
|
-
const { parseStripeMppAuthorization, verifyStripeMppAccessToken } = require("./stripe_mpp_payment_front");
|
|
14
|
-
const {
|
|
15
|
-
buildBsvTeranodeSettlementHints,
|
|
16
|
-
extractBsvTeranodePaymentReference,
|
|
17
|
-
isBsvTeranodeSettlementMode,
|
|
18
|
-
normalizeSettlementMode
|
|
19
|
-
} = require("./settlement_bsv_teranode");
|
|
20
|
-
|
|
21
|
-
function decodeBase64UrlJson(value) {
|
|
22
|
-
try {
|
|
23
|
-
return JSON.parse(Buffer.from(String(value || ""), "base64url").toString("utf8"));
|
|
24
|
-
} catch (_) {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function safeEqualHex(left, right) {
|
|
30
|
-
const a = Buffer.from(String(left || ""), "hex");
|
|
31
|
-
const b = Buffer.from(String(right || ""), "hex");
|
|
32
|
-
if (a.length === 0 || b.length === 0 || a.length !== b.length) return false;
|
|
33
|
-
return crypto.timingSafeEqual(a, b);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function parseNativePaymentHeaders(headers) {
|
|
37
|
-
return {
|
|
38
|
-
wallet_id: headers["x-wallet-id"] || null,
|
|
39
|
-
encoded: headers["x-payment"] || null,
|
|
40
|
-
signature: headers["x-payment-signature"] || null,
|
|
41
|
-
scheme: "native"
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function parseInstantPaymentHeaders(headers) {
|
|
46
|
-
const decoded = decodeBase64UrlJson(headers["payment-signature"]);
|
|
47
|
-
if (!decoded || typeof decoded !== "object") return null;
|
|
48
|
-
return {
|
|
49
|
-
wallet_id: decoded.wallet_id || null,
|
|
50
|
-
encoded: decoded.payload || null,
|
|
51
|
-
signature: decoded.signature || null,
|
|
52
|
-
scheme: decoded.scheme || "exact"
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function parsePayment(headers, requiredHeaders, options) {
|
|
57
|
-
const required = Array.isArray(requiredHeaders) ? requiredHeaders : [];
|
|
58
|
-
const opts = options || {};
|
|
59
|
-
if (required.includes("authorization")) {
|
|
60
|
-
if (opts.authorization_scheme === "mpp" || opts.payment_protocol === "mpp") return parseStripeMppAuthorization(headers);
|
|
61
|
-
if (opts.authorization_scheme === "bolt" || opts.payment_protocol === "bolt") return parseBoltAuthorization(headers);
|
|
62
|
-
return parseL402Authorization(headers);
|
|
63
|
-
}
|
|
64
|
-
if (required.includes("payment-signature")) return parseInstantPaymentHeaders(headers);
|
|
65
|
-
return parseNativePaymentHeaders(headers);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function decodePaymentPayload(encoded) {
|
|
69
|
-
try {
|
|
70
|
-
return JSON.parse(Buffer.from(String(encoded || ""), "base64url").toString("utf8"));
|
|
71
|
-
} catch (_) {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function verifyPaymentPayloadAgainstQuote(paymentPayload, quote, walletId, maxAgeSeconds) {
|
|
77
|
-
if (!paymentPayload || typeof paymentPayload !== "object") {
|
|
78
|
-
return { ok: false, reason: "payment_payload_invalid" };
|
|
79
|
-
}
|
|
80
|
-
if (!paymentPayload.quote_id || paymentPayload.quote_id !== quote.quote_id) {
|
|
81
|
-
return { ok: false, reason: "payment_quote_mismatch" };
|
|
82
|
-
}
|
|
83
|
-
if (paymentPayload.amount_minor !== quote.amount_minor) {
|
|
84
|
-
return { ok: false, reason: "payment_amount_mismatch" };
|
|
85
|
-
}
|
|
86
|
-
if (paymentPayload.currency !== quote.currency) {
|
|
87
|
-
return { ok: false, reason: "payment_currency_mismatch" };
|
|
88
|
-
}
|
|
89
|
-
if (paymentPayload.account_id && paymentPayload.account_id !== quote.account_id) {
|
|
90
|
-
return { ok: false, reason: "payment_account_mismatch" };
|
|
91
|
-
}
|
|
92
|
-
if (walletId && paymentPayload.wallet_id && paymentPayload.wallet_id !== walletId) {
|
|
93
|
-
return { ok: false, reason: "payment_wallet_mismatch" };
|
|
94
|
-
}
|
|
95
|
-
if (
|
|
96
|
-
paymentPayload.settlement_mode &&
|
|
97
|
-
normalizeSettlementMode(paymentPayload.settlement_mode) !== normalizeSettlementMode(quote.settlement_mode)
|
|
98
|
-
) {
|
|
99
|
-
return { ok: false, reason: "payment_settlement_mode_mismatch" };
|
|
100
|
-
}
|
|
101
|
-
if (
|
|
102
|
-
quote.settlement_binding_hash
|
|
103
|
-
&& paymentPayload.settlement_binding_hash
|
|
104
|
-
&& paymentPayload.settlement_binding_hash !== quote.settlement_binding_hash
|
|
105
|
-
) {
|
|
106
|
-
return { ok: false, reason: "payment_settlement_binding_mismatch" };
|
|
107
|
-
}
|
|
108
|
-
if (isBsvTeranodeSettlementMode(quote.settlement_mode)) {
|
|
109
|
-
const settlementHints = buildBsvTeranodeSettlementHints({
|
|
110
|
-
settlementMode: quote.settlement_mode,
|
|
111
|
-
amountMinor: quote.amount_minor,
|
|
112
|
-
currency: quote.currency,
|
|
113
|
-
settlementBindingHash: quote.settlement_binding_hash,
|
|
114
|
-
treasuryDestination: quote.treasury
|
|
115
|
-
});
|
|
116
|
-
const bsvReference = extractBsvTeranodePaymentReference(paymentPayload);
|
|
117
|
-
if (bsvReference) {
|
|
118
|
-
if (bsvReference.network && settlementHints && bsvReference.network !== settlementHints.network) {
|
|
119
|
-
return { ok: false, reason: "payment_bsv_network_mismatch" };
|
|
120
|
-
}
|
|
121
|
-
if (
|
|
122
|
-
settlementHints &&
|
|
123
|
-
settlementHints.merchant_address &&
|
|
124
|
-
bsvReference.merchant_address &&
|
|
125
|
-
bsvReference.merchant_address !== settlementHints.merchant_address
|
|
126
|
-
) {
|
|
127
|
-
return { ok: false, reason: "payment_bsv_merchant_address_mismatch" };
|
|
128
|
-
}
|
|
129
|
-
if (
|
|
130
|
-
settlementHints &&
|
|
131
|
-
settlementHints.merchant_paymail &&
|
|
132
|
-
bsvReference.merchant_paymail &&
|
|
133
|
-
bsvReference.merchant_paymail !== settlementHints.merchant_paymail
|
|
134
|
-
) {
|
|
135
|
-
return { ok: false, reason: "payment_bsv_merchant_paymail_mismatch" };
|
|
136
|
-
}
|
|
137
|
-
if (
|
|
138
|
-
settlementHints &&
|
|
139
|
-
Number.isFinite(Number(bsvReference.amount_satoshis)) &&
|
|
140
|
-
Number(bsvReference.amount_satoshis) !== settlementHints.amount_satoshis
|
|
141
|
-
) {
|
|
142
|
-
return { ok: false, reason: "payment_bsv_amount_mismatch" };
|
|
143
|
-
}
|
|
144
|
-
if (
|
|
145
|
-
settlementHints &&
|
|
146
|
-
bsvReference.settlement_reference &&
|
|
147
|
-
bsvReference.settlement_reference !== settlementHints.settlement_reference
|
|
148
|
-
) {
|
|
149
|
-
return { ok: false, reason: "payment_bsv_reference_mismatch" };
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (!paymentPayload.nonce || typeof paymentPayload.nonce !== "string") {
|
|
154
|
-
return { ok: false, reason: "payment_nonce_missing" };
|
|
155
|
-
}
|
|
156
|
-
if (!paymentPayload.issued_at_iso || Number.isNaN(Date.parse(paymentPayload.issued_at_iso))) {
|
|
157
|
-
return { ok: false, reason: "payment_issued_at_invalid" };
|
|
158
|
-
}
|
|
159
|
-
const ageSeconds = Math.max(0, Math.floor((Date.now() - Date.parse(paymentPayload.issued_at_iso)) / 1000));
|
|
160
|
-
if (ageSeconds > maxAgeSeconds) {
|
|
161
|
-
return { ok: false, reason: "payment_expired" };
|
|
162
|
-
}
|
|
163
|
-
return { ok: true, age_seconds: ageSeconds };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function verifyPaymentAcceptance(state, headers, requiredHeaders, quote, options) {
|
|
167
|
-
const config = loadPaymentConfig();
|
|
168
|
-
const required = Array.isArray(requiredHeaders) ? requiredHeaders : [];
|
|
169
|
-
const opts = options || {};
|
|
170
|
-
// Trustless on-chain pay-per-call: the PAYMENT-SIGNATURE carries an x402 v2 payment,
|
|
171
|
-
// which a facilitator verifies and settles on-chain before any work runs. This path is
|
|
172
|
-
// opt-in (XYTARA_PAYMENT_VERIFICATION_MODE=x402_facilitator) and fails closed if the
|
|
173
|
-
// facilitator is unreachable or the payment does not settle.
|
|
174
|
-
if (config.verification_mode === "x402_facilitator" && required.includes("payment-signature")) {
|
|
175
|
-
const h = headers || {};
|
|
176
|
-
const facilitator = createFacilitatorClient({
|
|
177
|
-
url: config.x402.facilitator_url,
|
|
178
|
-
headers: config.x402.facilitator_headers,
|
|
179
|
-
timeoutMs: config.x402.facilitator_timeout_ms
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
&&
|
|
302
|
-
&& verification
|
|
303
|
-
&& verification.payment_payload
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const { loadPaymentConfig } = require("./payment_config");
|
|
5
|
+
const {
|
|
6
|
+
createFacilitatorClient,
|
|
7
|
+
verifyX402Payment,
|
|
8
|
+
settleX402Payment,
|
|
9
|
+
encodePaymentResponse
|
|
10
|
+
} = require("./x402_facilitator");
|
|
11
|
+
const { parseL402Authorization, verifyL402AccessToken } = require("./l402_payment_front");
|
|
12
|
+
const { parseBoltAuthorization, verifyBoltAccessToken } = require("./bolt_payment_front");
|
|
13
|
+
const { parseStripeMppAuthorization, verifyStripeMppAccessToken } = require("./stripe_mpp_payment_front");
|
|
14
|
+
const {
|
|
15
|
+
buildBsvTeranodeSettlementHints,
|
|
16
|
+
extractBsvTeranodePaymentReference,
|
|
17
|
+
isBsvTeranodeSettlementMode,
|
|
18
|
+
normalizeSettlementMode
|
|
19
|
+
} = require("./settlement_bsv_teranode");
|
|
20
|
+
|
|
21
|
+
function decodeBase64UrlJson(value) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(Buffer.from(String(value || ""), "base64url").toString("utf8"));
|
|
24
|
+
} catch (_) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function safeEqualHex(left, right) {
|
|
30
|
+
const a = Buffer.from(String(left || ""), "hex");
|
|
31
|
+
const b = Buffer.from(String(right || ""), "hex");
|
|
32
|
+
if (a.length === 0 || b.length === 0 || a.length !== b.length) return false;
|
|
33
|
+
return crypto.timingSafeEqual(a, b);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseNativePaymentHeaders(headers) {
|
|
37
|
+
return {
|
|
38
|
+
wallet_id: headers["x-wallet-id"] || null,
|
|
39
|
+
encoded: headers["x-payment"] || null,
|
|
40
|
+
signature: headers["x-payment-signature"] || null,
|
|
41
|
+
scheme: "native"
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseInstantPaymentHeaders(headers) {
|
|
46
|
+
const decoded = decodeBase64UrlJson(headers["payment-signature"]);
|
|
47
|
+
if (!decoded || typeof decoded !== "object") return null;
|
|
48
|
+
return {
|
|
49
|
+
wallet_id: decoded.wallet_id || null,
|
|
50
|
+
encoded: decoded.payload || null,
|
|
51
|
+
signature: decoded.signature || null,
|
|
52
|
+
scheme: decoded.scheme || "exact"
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parsePayment(headers, requiredHeaders, options) {
|
|
57
|
+
const required = Array.isArray(requiredHeaders) ? requiredHeaders : [];
|
|
58
|
+
const opts = options || {};
|
|
59
|
+
if (required.includes("authorization")) {
|
|
60
|
+
if (opts.authorization_scheme === "mpp" || opts.payment_protocol === "mpp") return parseStripeMppAuthorization(headers);
|
|
61
|
+
if (opts.authorization_scheme === "bolt" || opts.payment_protocol === "bolt") return parseBoltAuthorization(headers);
|
|
62
|
+
return parseL402Authorization(headers);
|
|
63
|
+
}
|
|
64
|
+
if (required.includes("payment-signature")) return parseInstantPaymentHeaders(headers);
|
|
65
|
+
return parseNativePaymentHeaders(headers);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function decodePaymentPayload(encoded) {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(Buffer.from(String(encoded || ""), "base64url").toString("utf8"));
|
|
71
|
+
} catch (_) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function verifyPaymentPayloadAgainstQuote(paymentPayload, quote, walletId, maxAgeSeconds) {
|
|
77
|
+
if (!paymentPayload || typeof paymentPayload !== "object") {
|
|
78
|
+
return { ok: false, reason: "payment_payload_invalid" };
|
|
79
|
+
}
|
|
80
|
+
if (!paymentPayload.quote_id || paymentPayload.quote_id !== quote.quote_id) {
|
|
81
|
+
return { ok: false, reason: "payment_quote_mismatch" };
|
|
82
|
+
}
|
|
83
|
+
if (paymentPayload.amount_minor !== quote.amount_minor) {
|
|
84
|
+
return { ok: false, reason: "payment_amount_mismatch" };
|
|
85
|
+
}
|
|
86
|
+
if (paymentPayload.currency !== quote.currency) {
|
|
87
|
+
return { ok: false, reason: "payment_currency_mismatch" };
|
|
88
|
+
}
|
|
89
|
+
if (paymentPayload.account_id && paymentPayload.account_id !== quote.account_id) {
|
|
90
|
+
return { ok: false, reason: "payment_account_mismatch" };
|
|
91
|
+
}
|
|
92
|
+
if (walletId && paymentPayload.wallet_id && paymentPayload.wallet_id !== walletId) {
|
|
93
|
+
return { ok: false, reason: "payment_wallet_mismatch" };
|
|
94
|
+
}
|
|
95
|
+
if (
|
|
96
|
+
paymentPayload.settlement_mode &&
|
|
97
|
+
normalizeSettlementMode(paymentPayload.settlement_mode) !== normalizeSettlementMode(quote.settlement_mode)
|
|
98
|
+
) {
|
|
99
|
+
return { ok: false, reason: "payment_settlement_mode_mismatch" };
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
quote.settlement_binding_hash
|
|
103
|
+
&& paymentPayload.settlement_binding_hash
|
|
104
|
+
&& paymentPayload.settlement_binding_hash !== quote.settlement_binding_hash
|
|
105
|
+
) {
|
|
106
|
+
return { ok: false, reason: "payment_settlement_binding_mismatch" };
|
|
107
|
+
}
|
|
108
|
+
if (isBsvTeranodeSettlementMode(quote.settlement_mode)) {
|
|
109
|
+
const settlementHints = buildBsvTeranodeSettlementHints({
|
|
110
|
+
settlementMode: quote.settlement_mode,
|
|
111
|
+
amountMinor: quote.amount_minor,
|
|
112
|
+
currency: quote.currency,
|
|
113
|
+
settlementBindingHash: quote.settlement_binding_hash,
|
|
114
|
+
treasuryDestination: quote.treasury
|
|
115
|
+
});
|
|
116
|
+
const bsvReference = extractBsvTeranodePaymentReference(paymentPayload);
|
|
117
|
+
if (bsvReference) {
|
|
118
|
+
if (bsvReference.network && settlementHints && bsvReference.network !== settlementHints.network) {
|
|
119
|
+
return { ok: false, reason: "payment_bsv_network_mismatch" };
|
|
120
|
+
}
|
|
121
|
+
if (
|
|
122
|
+
settlementHints &&
|
|
123
|
+
settlementHints.merchant_address &&
|
|
124
|
+
bsvReference.merchant_address &&
|
|
125
|
+
bsvReference.merchant_address !== settlementHints.merchant_address
|
|
126
|
+
) {
|
|
127
|
+
return { ok: false, reason: "payment_bsv_merchant_address_mismatch" };
|
|
128
|
+
}
|
|
129
|
+
if (
|
|
130
|
+
settlementHints &&
|
|
131
|
+
settlementHints.merchant_paymail &&
|
|
132
|
+
bsvReference.merchant_paymail &&
|
|
133
|
+
bsvReference.merchant_paymail !== settlementHints.merchant_paymail
|
|
134
|
+
) {
|
|
135
|
+
return { ok: false, reason: "payment_bsv_merchant_paymail_mismatch" };
|
|
136
|
+
}
|
|
137
|
+
if (
|
|
138
|
+
settlementHints &&
|
|
139
|
+
Number.isFinite(Number(bsvReference.amount_satoshis)) &&
|
|
140
|
+
Number(bsvReference.amount_satoshis) !== settlementHints.amount_satoshis
|
|
141
|
+
) {
|
|
142
|
+
return { ok: false, reason: "payment_bsv_amount_mismatch" };
|
|
143
|
+
}
|
|
144
|
+
if (
|
|
145
|
+
settlementHints &&
|
|
146
|
+
bsvReference.settlement_reference &&
|
|
147
|
+
bsvReference.settlement_reference !== settlementHints.settlement_reference
|
|
148
|
+
) {
|
|
149
|
+
return { ok: false, reason: "payment_bsv_reference_mismatch" };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!paymentPayload.nonce || typeof paymentPayload.nonce !== "string") {
|
|
154
|
+
return { ok: false, reason: "payment_nonce_missing" };
|
|
155
|
+
}
|
|
156
|
+
if (!paymentPayload.issued_at_iso || Number.isNaN(Date.parse(paymentPayload.issued_at_iso))) {
|
|
157
|
+
return { ok: false, reason: "payment_issued_at_invalid" };
|
|
158
|
+
}
|
|
159
|
+
const ageSeconds = Math.max(0, Math.floor((Date.now() - Date.parse(paymentPayload.issued_at_iso)) / 1000));
|
|
160
|
+
if (ageSeconds > maxAgeSeconds) {
|
|
161
|
+
return { ok: false, reason: "payment_expired" };
|
|
162
|
+
}
|
|
163
|
+
return { ok: true, age_seconds: ageSeconds };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function verifyPaymentAcceptance(state, headers, requiredHeaders, quote, options) {
|
|
167
|
+
const config = loadPaymentConfig();
|
|
168
|
+
const required = Array.isArray(requiredHeaders) ? requiredHeaders : [];
|
|
169
|
+
const opts = options || {};
|
|
170
|
+
// Trustless on-chain pay-per-call: the PAYMENT-SIGNATURE carries an x402 v2 payment,
|
|
171
|
+
// which a facilitator verifies and settles on-chain before any work runs. This path is
|
|
172
|
+
// opt-in (XYTARA_PAYMENT_VERIFICATION_MODE=x402_facilitator) and fails closed if the
|
|
173
|
+
// facilitator is unreachable or the payment does not settle.
|
|
174
|
+
if (config.verification_mode === "x402_facilitator" && required.includes("payment-signature")) {
|
|
175
|
+
const h = headers || {};
|
|
176
|
+
const facilitator = createFacilitatorClient({
|
|
177
|
+
url: config.x402.facilitator_url,
|
|
178
|
+
headers: config.x402.facilitator_headers,
|
|
179
|
+
timeoutMs: config.x402.facilitator_timeout_ms,
|
|
180
|
+
cdp: config.x402.cdp
|
|
181
|
+
});
|
|
182
|
+
const auth = await verifyX402Payment({
|
|
183
|
+
paymentHeader: h["payment-signature"],
|
|
184
|
+
accepts: config.x402.accepts,
|
|
185
|
+
resourceUrl: opts.resource_url || null,
|
|
186
|
+
facilitator
|
|
187
|
+
});
|
|
188
|
+
if (!auth.ok) {
|
|
189
|
+
return { ok: false, status: auth.status || 402, reason: auth.reason, detail: auth.detail || null };
|
|
190
|
+
}
|
|
191
|
+
const settled = await settleX402Payment({ authorization: auth.authorization, facilitator, settle: config.x402.settle });
|
|
192
|
+
if (!settled.ok) {
|
|
193
|
+
return { ok: false, status: settled.status || 402, reason: settled.reason, detail: settled.detail || null };
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
ok: true,
|
|
197
|
+
verification_mode: "x402_facilitator",
|
|
198
|
+
wallet_id: auth.authorization.payer,
|
|
199
|
+
payment_payload: {
|
|
200
|
+
x402_payer: auth.authorization.payer,
|
|
201
|
+
x402_network: auth.authorization.paymentRequirements.network,
|
|
202
|
+
x402_asset: auth.authorization.paymentRequirements.asset,
|
|
203
|
+
x402_amount: auth.authorization.paymentRequirements.amount
|
|
204
|
+
},
|
|
205
|
+
settlement: settled.settlement,
|
|
206
|
+
payment_response_base64: encodePaymentResponse(settled.settlement)
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (required.includes("authorization")) {
|
|
210
|
+
if (opts.authorization_scheme === "mpp" || opts.payment_protocol === "mpp") {
|
|
211
|
+
return verifyStripeMppAccessToken(headers || {}, quote);
|
|
212
|
+
}
|
|
213
|
+
if (opts.authorization_scheme === "bolt" || opts.payment_protocol === "bolt") {
|
|
214
|
+
return verifyBoltAccessToken(headers || {}, quote);
|
|
215
|
+
}
|
|
216
|
+
return verifyL402AccessToken(headers || {}, quote);
|
|
217
|
+
}
|
|
218
|
+
const payment = parsePayment(headers || {}, requiredHeaders, opts);
|
|
219
|
+
// x402_facilitator only governs the x402 (payment-signature) routes, handled above.
|
|
220
|
+
// On the native and instant routes it falls back to local_signed, so enabling the mode
|
|
221
|
+
// adds trustless on-chain settlement without disabling the existing paid lanes.
|
|
222
|
+
const nativeMode = config.verification_mode === "x402_facilitator" ? "local_signed" : config.verification_mode;
|
|
223
|
+
if (nativeMode === "presence") {
|
|
224
|
+
return {
|
|
225
|
+
ok: true,
|
|
226
|
+
verification_mode: "presence",
|
|
227
|
+
wallet_id: payment && payment.wallet_id ? payment.wallet_id : null,
|
|
228
|
+
payment_payload: payment && payment.encoded ? decodePaymentPayload(payment.encoded) : null
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (nativeMode !== "local_signed") {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
status: 400,
|
|
236
|
+
reason: "payment_verification_mode_unsupported"
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!payment || !payment.wallet_id || !payment.encoded || !payment.signature) {
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
status: 402,
|
|
244
|
+
reason: "payment_headers_incomplete"
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const walletEntry = config.wallet_registry.get(payment.wallet_id);
|
|
249
|
+
if (!walletEntry || walletEntry.enabled === false) {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
status: 403,
|
|
253
|
+
reason: "wallet_not_registered"
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const expectedSignature = crypto.createHmac("sha256", walletEntry.secret).update(payment.encoded).digest("hex");
|
|
258
|
+
if (!safeEqualHex(expectedSignature, payment.signature)) {
|
|
259
|
+
return {
|
|
260
|
+
ok: false,
|
|
261
|
+
status: 403,
|
|
262
|
+
reason: "payment_signature_invalid"
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const paymentPayload = decodePaymentPayload(payment.encoded);
|
|
267
|
+
const payloadValidation = verifyPaymentPayloadAgainstQuote(
|
|
268
|
+
paymentPayload,
|
|
269
|
+
quote,
|
|
270
|
+
payment.wallet_id,
|
|
271
|
+
config.payment_max_age_seconds
|
|
272
|
+
);
|
|
273
|
+
if (!payloadValidation.ok) {
|
|
274
|
+
return {
|
|
275
|
+
ok: false,
|
|
276
|
+
status: 403,
|
|
277
|
+
reason: payloadValidation.reason
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (state && state.acceptedPaymentNonces && state.acceptedPaymentNonces.has(paymentPayload.nonce)) {
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
status: 409,
|
|
285
|
+
reason: "payment_nonce_replayed"
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
ok: true,
|
|
291
|
+
verification_mode: "local_signed",
|
|
292
|
+
wallet_id: payment.wallet_id,
|
|
293
|
+
payment_payload: paymentPayload,
|
|
294
|
+
age_seconds: payloadValidation.age_seconds
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function markPaymentAccepted(state, verification) {
|
|
299
|
+
if (
|
|
300
|
+
state
|
|
301
|
+
&& state.acceptedPaymentNonces
|
|
302
|
+
&& verification
|
|
303
|
+
&& verification.payment_payload
|
|
304
|
+
&& verification.payment_payload.nonce
|
|
305
|
+
) {
|
|
306
|
+
state.acceptedPaymentNonces.add(verification.payment_payload.nonce);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = {
|
|
311
|
+
markPaymentAccepted,
|
|
312
|
+
verifyPaymentAcceptance
|
|
313
|
+
};
|