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.
@@ -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
- const auth = await verifyX402Payment({
182
- paymentHeader: h["payment-signature"],
183
- accepts: config.x402.accepts,
184
- resourceUrl: opts.resource_url || null,
185
- facilitator
186
- });
187
- if (!auth.ok) {
188
- return { ok: false, status: auth.status || 402, reason: auth.reason, detail: auth.detail || null };
189
- }
190
- const settled = await settleX402Payment({ authorization: auth.authorization, facilitator, settle: config.x402.settle });
191
- if (!settled.ok) {
192
- return { ok: false, status: settled.status || 402, reason: settled.reason, detail: settled.detail || null };
193
- }
194
- return {
195
- ok: true,
196
- verification_mode: "x402_facilitator",
197
- wallet_id: auth.authorization.payer,
198
- payment_payload: {
199
- x402_payer: auth.authorization.payer,
200
- x402_network: auth.authorization.paymentRequirements.network,
201
- x402_asset: auth.authorization.paymentRequirements.asset,
202
- x402_amount: auth.authorization.paymentRequirements.amount
203
- },
204
- settlement: settled.settlement,
205
- payment_response_base64: encodePaymentResponse(settled.settlement)
206
- };
207
- }
208
- if (required.includes("authorization")) {
209
- if (opts.authorization_scheme === "mpp" || opts.payment_protocol === "mpp") {
210
- return verifyStripeMppAccessToken(headers || {}, quote);
211
- }
212
- if (opts.authorization_scheme === "bolt" || opts.payment_protocol === "bolt") {
213
- return verifyBoltAccessToken(headers || {}, quote);
214
- }
215
- return verifyL402AccessToken(headers || {}, quote);
216
- }
217
- const payment = parsePayment(headers || {}, requiredHeaders, opts);
218
- // x402_facilitator only governs the x402 (payment-signature) routes, handled above.
219
- // On the native and instant routes it falls back to local_signed, so enabling the mode
220
- // adds trustless on-chain settlement without disabling the existing paid lanes.
221
- const nativeMode = config.verification_mode === "x402_facilitator" ? "local_signed" : config.verification_mode;
222
- if (nativeMode === "presence") {
223
- return {
224
- ok: true,
225
- verification_mode: "presence",
226
- wallet_id: payment && payment.wallet_id ? payment.wallet_id : null,
227
- payment_payload: payment && payment.encoded ? decodePaymentPayload(payment.encoded) : null
228
- };
229
- }
230
-
231
- if (nativeMode !== "local_signed") {
232
- return {
233
- ok: false,
234
- status: 400,
235
- reason: "payment_verification_mode_unsupported"
236
- };
237
- }
238
-
239
- if (!payment || !payment.wallet_id || !payment.encoded || !payment.signature) {
240
- return {
241
- ok: false,
242
- status: 402,
243
- reason: "payment_headers_incomplete"
244
- };
245
- }
246
-
247
- const walletEntry = config.wallet_registry.get(payment.wallet_id);
248
- if (!walletEntry || walletEntry.enabled === false) {
249
- return {
250
- ok: false,
251
- status: 403,
252
- reason: "wallet_not_registered"
253
- };
254
- }
255
-
256
- const expectedSignature = crypto.createHmac("sha256", walletEntry.secret).update(payment.encoded).digest("hex");
257
- if (!safeEqualHex(expectedSignature, payment.signature)) {
258
- return {
259
- ok: false,
260
- status: 403,
261
- reason: "payment_signature_invalid"
262
- };
263
- }
264
-
265
- const paymentPayload = decodePaymentPayload(payment.encoded);
266
- const payloadValidation = verifyPaymentPayloadAgainstQuote(
267
- paymentPayload,
268
- quote,
269
- payment.wallet_id,
270
- config.payment_max_age_seconds
271
- );
272
- if (!payloadValidation.ok) {
273
- return {
274
- ok: false,
275
- status: 403,
276
- reason: payloadValidation.reason
277
- };
278
- }
279
-
280
- if (state && state.acceptedPaymentNonces && state.acceptedPaymentNonces.has(paymentPayload.nonce)) {
281
- return {
282
- ok: false,
283
- status: 409,
284
- reason: "payment_nonce_replayed"
285
- };
286
- }
287
-
288
- return {
289
- ok: true,
290
- verification_mode: "local_signed",
291
- wallet_id: payment.wallet_id,
292
- payment_payload: paymentPayload,
293
- age_seconds: payloadValidation.age_seconds
294
- };
295
- }
296
-
297
- function markPaymentAccepted(state, verification) {
298
- if (
299
- state
300
- && state.acceptedPaymentNonces
301
- && verification
302
- && verification.payment_payload
303
- && verification.payment_payload.nonce
304
- ) {
305
- state.acceptedPaymentNonces.add(verification.payment_payload.nonce);
306
- }
307
- }
308
-
309
- module.exports = {
310
- markPaymentAccepted,
311
- verifyPaymentAcceptance
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
+ };