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
|
@@ -1,265 +1,265 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const crypto = require("crypto");
|
|
4
|
-
const l402Adapter = require("./l402_adapter");
|
|
5
|
-
|
|
6
|
-
function normalizeString(value) {
|
|
7
|
-
return typeof value === "string" ? value.trim() : "";
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function safeEqualHex(left, right) {
|
|
11
|
-
const a = Buffer.from(String(left || ""), "hex");
|
|
12
|
-
const b = Buffer.from(String(right || ""), "hex");
|
|
13
|
-
if (a.length === 0 || b.length === 0 || a.length !== b.length) return false;
|
|
14
|
-
return crypto.timingSafeEqual(a, b);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function decodeBase64UrlJson(value) {
|
|
18
|
-
try {
|
|
19
|
-
return JSON.parse(Buffer.from(String(value || ""), "base64url").toString("utf8"));
|
|
20
|
-
} catch (_) {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function loadL402FrontConfig() {
|
|
26
|
-
const sharedSecret = normalizeString(process.env.XYTARA_L402_SHARED_SECRET);
|
|
27
|
-
return {
|
|
28
|
-
enabled: String(process.env.XYTARA_L402_ENABLED || (sharedSecret ? "true" : "false")).trim().toLowerCase() !== "false",
|
|
29
|
-
shared_secret: sharedSecret || null,
|
|
30
|
-
max_age_seconds: Math.max(30, Number(process.env.XYTARA_L402_MAX_AGE_SECONDS || 300) || 300),
|
|
31
|
-
required_headers: ["authorization"],
|
|
32
|
-
auth_scheme: "L402",
|
|
33
|
-
token_shape: "base64url_json_with_macaroon_and_hmac_signed_payload"
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function parseL402Authorization(headers) {
|
|
38
|
-
const authorization = headers && typeof headers.authorization === "string"
|
|
39
|
-
? headers.authorization.trim()
|
|
40
|
-
: "";
|
|
41
|
-
const match = authorization.match(/^L402\s+(.+)$/i);
|
|
42
|
-
if (!match) return null;
|
|
43
|
-
const decoded = decodeBase64UrlJson(match[1]);
|
|
44
|
-
if (!decoded || typeof decoded !== "object") return null;
|
|
45
|
-
return {
|
|
46
|
-
wallet_id: decoded.wallet_id || decoded.gateway_id || "l402_gateway",
|
|
47
|
-
encoded: decoded.payload || null,
|
|
48
|
-
signature: decoded.signature || null,
|
|
49
|
-
macaroon: decoded.macaroon || null,
|
|
50
|
-
invoice_id: decoded.invoice_id || null,
|
|
51
|
-
preimage_hash: decoded.preimage_hash || null,
|
|
52
|
-
scheme: "l402"
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function verifyL402PaymentPayload(paymentPayload, quote, walletId, maxAgeSeconds) {
|
|
57
|
-
if (!paymentPayload || typeof paymentPayload !== "object") {
|
|
58
|
-
return { ok: false, reason: "l402_payload_invalid" };
|
|
59
|
-
}
|
|
60
|
-
if (!paymentPayload.quote_id || paymentPayload.quote_id !== quote.quote_id) {
|
|
61
|
-
return { ok: false, reason: "l402_quote_mismatch" };
|
|
62
|
-
}
|
|
63
|
-
if (paymentPayload.amount_minor !== quote.amount_minor) {
|
|
64
|
-
return { ok: false, reason: "l402_amount_mismatch" };
|
|
65
|
-
}
|
|
66
|
-
if (paymentPayload.currency !== quote.currency) {
|
|
67
|
-
return { ok: false, reason: "l402_currency_mismatch" };
|
|
68
|
-
}
|
|
69
|
-
if (paymentPayload.account_id && paymentPayload.account_id !== quote.account_id) {
|
|
70
|
-
return { ok: false, reason: "l402_account_mismatch" };
|
|
71
|
-
}
|
|
72
|
-
if (walletId && paymentPayload.wallet_id && paymentPayload.wallet_id !== walletId) {
|
|
73
|
-
return { ok: false, reason: "l402_wallet_mismatch" };
|
|
74
|
-
}
|
|
75
|
-
if (!paymentPayload.nonce || typeof paymentPayload.nonce !== "string") {
|
|
76
|
-
return { ok: false, reason: "l402_nonce_missing" };
|
|
77
|
-
}
|
|
78
|
-
if (!paymentPayload.issued_at_iso || Number.isNaN(Date.parse(paymentPayload.issued_at_iso))) {
|
|
79
|
-
return { ok: false, reason: "l402_issued_at_invalid" };
|
|
80
|
-
}
|
|
81
|
-
const ageSeconds = Math.max(0, Math.floor((Date.now() - Date.parse(paymentPayload.issued_at_iso)) / 1000));
|
|
82
|
-
if (ageSeconds > maxAgeSeconds) {
|
|
83
|
-
return { ok: false, reason: "l402_token_expired" };
|
|
84
|
-
}
|
|
85
|
-
if (!normalizeString(paymentPayload.macaroon_id || paymentPayload.macaroon)) {
|
|
86
|
-
return { ok: false, reason: "l402_macaroon_missing" };
|
|
87
|
-
}
|
|
88
|
-
if (!normalizeString(paymentPayload.invoice_id || paymentPayload.invoice_reference)) {
|
|
89
|
-
return { ok: false, reason: "l402_invoice_reference_missing" };
|
|
90
|
-
}
|
|
91
|
-
return { ok: true, age_seconds: ageSeconds };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function verifyL402AccessToken(headers, quote, state) {
|
|
95
|
-
// Real Lightning (L402) path: when a Lightning provider is configured, verify
|
|
96
|
-
// the standard "L402 <macaroon>:<preimage>" authorization cryptographically
|
|
97
|
-
// (HMAC-bound macaroon + sha256(preimage) === payment_hash) with single-use
|
|
98
|
-
// replay protection via the runtime nonce set. Otherwise fall through to the
|
|
99
|
-
// existing gateway-signed token scheme below (unchanged).
|
|
100
|
-
const lightningConfig = l402Adapter.loadL402Config();
|
|
101
|
-
if (l402Adapter.isConfigured(lightningConfig)) {
|
|
102
|
-
const authorization = headers && typeof headers.authorization === "string" ? headers.authorization : "";
|
|
103
|
-
const result = l402Adapter.verifyL402Authorization(lightningConfig, authorization);
|
|
104
|
-
if (!result.ok) {
|
|
105
|
-
return { ok: false, status: result.reason === "l402_authorization_malformed" ? 402 : 403, reason: result.reason };
|
|
106
|
-
}
|
|
107
|
-
if (state && state.acceptedPaymentNonces && state.acceptedPaymentNonces.has(result.payment_hash)) {
|
|
108
|
-
return { ok: false, status: 403, reason: "l402_preimage_replayed" };
|
|
109
|
-
}
|
|
110
|
-
return {
|
|
111
|
-
ok: true,
|
|
112
|
-
verification_mode: "l402_lightning",
|
|
113
|
-
wallet_id: "l402",
|
|
114
|
-
payment_payload: {
|
|
115
|
-
nonce: result.payment_hash,
|
|
116
|
-
payment_hash: result.payment_hash,
|
|
117
|
-
preimage: result.preimage,
|
|
118
|
-
quote_id: quote && quote.quote_id,
|
|
119
|
-
issued_at_iso: new Date().toISOString()
|
|
120
|
-
},
|
|
121
|
-
l402: { payment_hash: result.payment_hash, preimage: result.preimage }
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const config = loadL402FrontConfig();
|
|
126
|
-
if (!config.enabled) {
|
|
127
|
-
return {
|
|
128
|
-
ok: false,
|
|
129
|
-
status: 503,
|
|
130
|
-
reason: "l402_not_enabled"
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
if (!config.shared_secret) {
|
|
134
|
-
return {
|
|
135
|
-
ok: false,
|
|
136
|
-
status: 503,
|
|
137
|
-
reason: "l402_shared_secret_missing"
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
const payment = parseL402Authorization(headers || {});
|
|
141
|
-
if (!payment || !payment.encoded || !payment.signature || !payment.macaroon) {
|
|
142
|
-
return {
|
|
143
|
-
ok: false,
|
|
144
|
-
status: 402,
|
|
145
|
-
reason: "l402_headers_incomplete"
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
const expectedSignature = crypto.createHmac("sha256", config.shared_secret).update(payment.encoded).digest("hex");
|
|
149
|
-
if (!safeEqualHex(expectedSignature, payment.signature)) {
|
|
150
|
-
return {
|
|
151
|
-
ok: false,
|
|
152
|
-
status: 403,
|
|
153
|
-
reason: "l402_signature_invalid"
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
const payload = decodeBase64UrlJson(payment.encoded);
|
|
157
|
-
const validation = verifyL402PaymentPayload(payload, quote, payment.wallet_id, config.max_age_seconds);
|
|
158
|
-
if (!validation.ok) {
|
|
159
|
-
return {
|
|
160
|
-
ok: false,
|
|
161
|
-
status: 403,
|
|
162
|
-
reason: validation.reason
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
return {
|
|
166
|
-
ok: true,
|
|
167
|
-
verification_mode: "l402_gateway_signed",
|
|
168
|
-
wallet_id: payment.wallet_id,
|
|
169
|
-
payment_payload: payload,
|
|
170
|
-
age_seconds: validation.age_seconds,
|
|
171
|
-
l402: {
|
|
172
|
-
macaroon: payment.macaroon,
|
|
173
|
-
invoice_id: payment.invoice_id || payload.invoice_id || null,
|
|
174
|
-
preimage_hash: payment.preimage_hash || payload.preimage_hash || null
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function buildL402SecuritySummary() {
|
|
180
|
-
const config = loadL402FrontConfig();
|
|
181
|
-
const blockers = [];
|
|
182
|
-
if (config.enabled && !config.shared_secret) blockers.push("l402_shared_secret_missing");
|
|
183
|
-
return {
|
|
184
|
-
ok: true,
|
|
185
|
-
category: "machine-commerce-l402-security-summary",
|
|
186
|
-
summary_version: "xytara-l402-security-summary-v1",
|
|
187
|
-
enabled: config.enabled,
|
|
188
|
-
shared_secret_configured: Boolean(config.shared_secret),
|
|
189
|
-
max_age_seconds: config.max_age_seconds,
|
|
190
|
-
auth_scheme: config.auth_scheme,
|
|
191
|
-
token_shape: config.token_shape,
|
|
192
|
-
ready: config.enabled && blockers.length === 0,
|
|
193
|
-
blockers,
|
|
194
|
-
linked_surfaces: {
|
|
195
|
-
l402_lane_ref: "/v1/l402",
|
|
196
|
-
commands_execute_ref: "/l402/commands/execute",
|
|
197
|
-
mcp_invoke_ref: "/l402/mcp/tools/invoke"
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async function buildL402ChallengePayload(quote) {
|
|
203
|
-
// Real Lightning (L402) challenge: when a Lightning provider is configured,
|
|
204
|
-
// issue a real bolt11 invoice + an HMAC-bound macaroon and advertise the
|
|
205
|
-
// standard L402 WWW-Authenticate value. Otherwise return the existing
|
|
206
|
-
// token-shape descriptor (unchanged).
|
|
207
|
-
const lightningConfig = l402Adapter.loadL402Config();
|
|
208
|
-
if (l402Adapter.isConfigured(lightningConfig)) {
|
|
209
|
-
const invoice = await l402Adapter.createLightningInvoice(lightningConfig, {
|
|
210
|
-
amountSats: lightningConfig.priceSats,
|
|
211
|
-
memo: quote && quote.quote_id ? `naxytra ${quote.quote_id}` : "naxytra L402"
|
|
212
|
-
});
|
|
213
|
-
if (!invoice.ok) {
|
|
214
|
-
return {
|
|
215
|
-
ok: false,
|
|
216
|
-
payment_required: {
|
|
217
|
-
scheme: "l402",
|
|
218
|
-
quote,
|
|
219
|
-
error: invoice.reason,
|
|
220
|
-
operator_requirements: { auth_scheme: "L402", security_ref: "/v1/l402/security" }
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
const macaroon = l402Adapter.issueL402Macaroon(lightningConfig, {
|
|
225
|
-
payment_hash: invoice.payment_hash,
|
|
226
|
-
amountSats: lightningConfig.priceSats
|
|
227
|
-
});
|
|
228
|
-
return {
|
|
229
|
-
ok: false,
|
|
230
|
-
payment_required: {
|
|
231
|
-
scheme: "l402",
|
|
232
|
-
quote,
|
|
233
|
-
invoice: invoice.payment_request,
|
|
234
|
-
payment_hash: invoice.payment_hash,
|
|
235
|
-
macaroon,
|
|
236
|
-
www_authenticate: l402Adapter.buildWwwAuthenticate(macaroon, invoice.payment_request),
|
|
237
|
-
operator_requirements: {
|
|
238
|
-
auth_scheme: "L402",
|
|
239
|
-
token_shape: "authorization_header_L402_macaroon_colon_preimage",
|
|
240
|
-
security_ref: "/v1/l402/security"
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
return {
|
|
246
|
-
ok: false,
|
|
247
|
-
payment_required: {
|
|
248
|
-
scheme: "l402",
|
|
249
|
-
quote,
|
|
250
|
-
operator_requirements: {
|
|
251
|
-
auth_scheme: "L402",
|
|
252
|
-
token_shape: "base64url_json_with_macaroon_and_hmac_signed_payload",
|
|
253
|
-
security_ref: "/v1/l402/security"
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
module.exports = {
|
|
260
|
-
buildL402ChallengePayload,
|
|
261
|
-
buildL402SecuritySummary,
|
|
262
|
-
loadL402FrontConfig,
|
|
263
|
-
parseL402Authorization,
|
|
264
|
-
verifyL402AccessToken
|
|
265
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const l402Adapter = require("./l402_adapter");
|
|
5
|
+
|
|
6
|
+
function normalizeString(value) {
|
|
7
|
+
return typeof value === "string" ? value.trim() : "";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function safeEqualHex(left, right) {
|
|
11
|
+
const a = Buffer.from(String(left || ""), "hex");
|
|
12
|
+
const b = Buffer.from(String(right || ""), "hex");
|
|
13
|
+
if (a.length === 0 || b.length === 0 || a.length !== b.length) return false;
|
|
14
|
+
return crypto.timingSafeEqual(a, b);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function decodeBase64UrlJson(value) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(Buffer.from(String(value || ""), "base64url").toString("utf8"));
|
|
20
|
+
} catch (_) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadL402FrontConfig() {
|
|
26
|
+
const sharedSecret = normalizeString(process.env.XYTARA_L402_SHARED_SECRET);
|
|
27
|
+
return {
|
|
28
|
+
enabled: String(process.env.XYTARA_L402_ENABLED || (sharedSecret ? "true" : "false")).trim().toLowerCase() !== "false",
|
|
29
|
+
shared_secret: sharedSecret || null,
|
|
30
|
+
max_age_seconds: Math.max(30, Number(process.env.XYTARA_L402_MAX_AGE_SECONDS || 300) || 300),
|
|
31
|
+
required_headers: ["authorization"],
|
|
32
|
+
auth_scheme: "L402",
|
|
33
|
+
token_shape: "base64url_json_with_macaroon_and_hmac_signed_payload"
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseL402Authorization(headers) {
|
|
38
|
+
const authorization = headers && typeof headers.authorization === "string"
|
|
39
|
+
? headers.authorization.trim()
|
|
40
|
+
: "";
|
|
41
|
+
const match = authorization.match(/^L402\s+(.+)$/i);
|
|
42
|
+
if (!match) return null;
|
|
43
|
+
const decoded = decodeBase64UrlJson(match[1]);
|
|
44
|
+
if (!decoded || typeof decoded !== "object") return null;
|
|
45
|
+
return {
|
|
46
|
+
wallet_id: decoded.wallet_id || decoded.gateway_id || "l402_gateway",
|
|
47
|
+
encoded: decoded.payload || null,
|
|
48
|
+
signature: decoded.signature || null,
|
|
49
|
+
macaroon: decoded.macaroon || null,
|
|
50
|
+
invoice_id: decoded.invoice_id || null,
|
|
51
|
+
preimage_hash: decoded.preimage_hash || null,
|
|
52
|
+
scheme: "l402"
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function verifyL402PaymentPayload(paymentPayload, quote, walletId, maxAgeSeconds) {
|
|
57
|
+
if (!paymentPayload || typeof paymentPayload !== "object") {
|
|
58
|
+
return { ok: false, reason: "l402_payload_invalid" };
|
|
59
|
+
}
|
|
60
|
+
if (!paymentPayload.quote_id || paymentPayload.quote_id !== quote.quote_id) {
|
|
61
|
+
return { ok: false, reason: "l402_quote_mismatch" };
|
|
62
|
+
}
|
|
63
|
+
if (paymentPayload.amount_minor !== quote.amount_minor) {
|
|
64
|
+
return { ok: false, reason: "l402_amount_mismatch" };
|
|
65
|
+
}
|
|
66
|
+
if (paymentPayload.currency !== quote.currency) {
|
|
67
|
+
return { ok: false, reason: "l402_currency_mismatch" };
|
|
68
|
+
}
|
|
69
|
+
if (paymentPayload.account_id && paymentPayload.account_id !== quote.account_id) {
|
|
70
|
+
return { ok: false, reason: "l402_account_mismatch" };
|
|
71
|
+
}
|
|
72
|
+
if (walletId && paymentPayload.wallet_id && paymentPayload.wallet_id !== walletId) {
|
|
73
|
+
return { ok: false, reason: "l402_wallet_mismatch" };
|
|
74
|
+
}
|
|
75
|
+
if (!paymentPayload.nonce || typeof paymentPayload.nonce !== "string") {
|
|
76
|
+
return { ok: false, reason: "l402_nonce_missing" };
|
|
77
|
+
}
|
|
78
|
+
if (!paymentPayload.issued_at_iso || Number.isNaN(Date.parse(paymentPayload.issued_at_iso))) {
|
|
79
|
+
return { ok: false, reason: "l402_issued_at_invalid" };
|
|
80
|
+
}
|
|
81
|
+
const ageSeconds = Math.max(0, Math.floor((Date.now() - Date.parse(paymentPayload.issued_at_iso)) / 1000));
|
|
82
|
+
if (ageSeconds > maxAgeSeconds) {
|
|
83
|
+
return { ok: false, reason: "l402_token_expired" };
|
|
84
|
+
}
|
|
85
|
+
if (!normalizeString(paymentPayload.macaroon_id || paymentPayload.macaroon)) {
|
|
86
|
+
return { ok: false, reason: "l402_macaroon_missing" };
|
|
87
|
+
}
|
|
88
|
+
if (!normalizeString(paymentPayload.invoice_id || paymentPayload.invoice_reference)) {
|
|
89
|
+
return { ok: false, reason: "l402_invoice_reference_missing" };
|
|
90
|
+
}
|
|
91
|
+
return { ok: true, age_seconds: ageSeconds };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function verifyL402AccessToken(headers, quote, state) {
|
|
95
|
+
// Real Lightning (L402) path: when a Lightning provider is configured, verify
|
|
96
|
+
// the standard "L402 <macaroon>:<preimage>" authorization cryptographically
|
|
97
|
+
// (HMAC-bound macaroon + sha256(preimage) === payment_hash) with single-use
|
|
98
|
+
// replay protection via the runtime nonce set. Otherwise fall through to the
|
|
99
|
+
// existing gateway-signed token scheme below (unchanged).
|
|
100
|
+
const lightningConfig = l402Adapter.loadL402Config();
|
|
101
|
+
if (l402Adapter.isConfigured(lightningConfig)) {
|
|
102
|
+
const authorization = headers && typeof headers.authorization === "string" ? headers.authorization : "";
|
|
103
|
+
const result = l402Adapter.verifyL402Authorization(lightningConfig, authorization);
|
|
104
|
+
if (!result.ok) {
|
|
105
|
+
return { ok: false, status: result.reason === "l402_authorization_malformed" ? 402 : 403, reason: result.reason };
|
|
106
|
+
}
|
|
107
|
+
if (state && state.acceptedPaymentNonces && state.acceptedPaymentNonces.has(result.payment_hash)) {
|
|
108
|
+
return { ok: false, status: 403, reason: "l402_preimage_replayed" };
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
verification_mode: "l402_lightning",
|
|
113
|
+
wallet_id: "l402",
|
|
114
|
+
payment_payload: {
|
|
115
|
+
nonce: result.payment_hash,
|
|
116
|
+
payment_hash: result.payment_hash,
|
|
117
|
+
preimage: result.preimage,
|
|
118
|
+
quote_id: quote && quote.quote_id,
|
|
119
|
+
issued_at_iso: new Date().toISOString()
|
|
120
|
+
},
|
|
121
|
+
l402: { payment_hash: result.payment_hash, preimage: result.preimage }
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const config = loadL402FrontConfig();
|
|
126
|
+
if (!config.enabled) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
status: 503,
|
|
130
|
+
reason: "l402_not_enabled"
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (!config.shared_secret) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
status: 503,
|
|
137
|
+
reason: "l402_shared_secret_missing"
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const payment = parseL402Authorization(headers || {});
|
|
141
|
+
if (!payment || !payment.encoded || !payment.signature || !payment.macaroon) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
status: 402,
|
|
145
|
+
reason: "l402_headers_incomplete"
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const expectedSignature = crypto.createHmac("sha256", config.shared_secret).update(payment.encoded).digest("hex");
|
|
149
|
+
if (!safeEqualHex(expectedSignature, payment.signature)) {
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
status: 403,
|
|
153
|
+
reason: "l402_signature_invalid"
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const payload = decodeBase64UrlJson(payment.encoded);
|
|
157
|
+
const validation = verifyL402PaymentPayload(payload, quote, payment.wallet_id, config.max_age_seconds);
|
|
158
|
+
if (!validation.ok) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
status: 403,
|
|
162
|
+
reason: validation.reason
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
ok: true,
|
|
167
|
+
verification_mode: "l402_gateway_signed",
|
|
168
|
+
wallet_id: payment.wallet_id,
|
|
169
|
+
payment_payload: payload,
|
|
170
|
+
age_seconds: validation.age_seconds,
|
|
171
|
+
l402: {
|
|
172
|
+
macaroon: payment.macaroon,
|
|
173
|
+
invoice_id: payment.invoice_id || payload.invoice_id || null,
|
|
174
|
+
preimage_hash: payment.preimage_hash || payload.preimage_hash || null
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function buildL402SecuritySummary() {
|
|
180
|
+
const config = loadL402FrontConfig();
|
|
181
|
+
const blockers = [];
|
|
182
|
+
if (config.enabled && !config.shared_secret) blockers.push("l402_shared_secret_missing");
|
|
183
|
+
return {
|
|
184
|
+
ok: true,
|
|
185
|
+
category: "machine-commerce-l402-security-summary",
|
|
186
|
+
summary_version: "xytara-l402-security-summary-v1",
|
|
187
|
+
enabled: config.enabled,
|
|
188
|
+
shared_secret_configured: Boolean(config.shared_secret),
|
|
189
|
+
max_age_seconds: config.max_age_seconds,
|
|
190
|
+
auth_scheme: config.auth_scheme,
|
|
191
|
+
token_shape: config.token_shape,
|
|
192
|
+
ready: config.enabled && blockers.length === 0,
|
|
193
|
+
blockers,
|
|
194
|
+
linked_surfaces: {
|
|
195
|
+
l402_lane_ref: "/v1/l402",
|
|
196
|
+
commands_execute_ref: "/l402/commands/execute",
|
|
197
|
+
mcp_invoke_ref: "/l402/mcp/tools/invoke"
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function buildL402ChallengePayload(quote) {
|
|
203
|
+
// Real Lightning (L402) challenge: when a Lightning provider is configured,
|
|
204
|
+
// issue a real bolt11 invoice + an HMAC-bound macaroon and advertise the
|
|
205
|
+
// standard L402 WWW-Authenticate value. Otherwise return the existing
|
|
206
|
+
// token-shape descriptor (unchanged).
|
|
207
|
+
const lightningConfig = l402Adapter.loadL402Config();
|
|
208
|
+
if (l402Adapter.isConfigured(lightningConfig)) {
|
|
209
|
+
const invoice = await l402Adapter.createLightningInvoice(lightningConfig, {
|
|
210
|
+
amountSats: lightningConfig.priceSats,
|
|
211
|
+
memo: quote && quote.quote_id ? `naxytra ${quote.quote_id}` : "naxytra L402"
|
|
212
|
+
});
|
|
213
|
+
if (!invoice.ok) {
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
payment_required: {
|
|
217
|
+
scheme: "l402",
|
|
218
|
+
quote,
|
|
219
|
+
error: invoice.reason,
|
|
220
|
+
operator_requirements: { auth_scheme: "L402", security_ref: "/v1/l402/security" }
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const macaroon = l402Adapter.issueL402Macaroon(lightningConfig, {
|
|
225
|
+
payment_hash: invoice.payment_hash,
|
|
226
|
+
amountSats: lightningConfig.priceSats
|
|
227
|
+
});
|
|
228
|
+
return {
|
|
229
|
+
ok: false,
|
|
230
|
+
payment_required: {
|
|
231
|
+
scheme: "l402",
|
|
232
|
+
quote,
|
|
233
|
+
invoice: invoice.payment_request,
|
|
234
|
+
payment_hash: invoice.payment_hash,
|
|
235
|
+
macaroon,
|
|
236
|
+
www_authenticate: l402Adapter.buildWwwAuthenticate(macaroon, invoice.payment_request),
|
|
237
|
+
operator_requirements: {
|
|
238
|
+
auth_scheme: "L402",
|
|
239
|
+
token_shape: "authorization_header_L402_macaroon_colon_preimage",
|
|
240
|
+
security_ref: "/v1/l402/security"
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
payment_required: {
|
|
248
|
+
scheme: "l402",
|
|
249
|
+
quote,
|
|
250
|
+
operator_requirements: {
|
|
251
|
+
auth_scheme: "L402",
|
|
252
|
+
token_shape: "base64url_json_with_macaroon_and_hmac_signed_payload",
|
|
253
|
+
security_ref: "/v1/l402/security"
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
buildL402ChallengePayload,
|
|
261
|
+
buildL402SecuritySummary,
|
|
262
|
+
loadL402FrontConfig,
|
|
263
|
+
parseL402Authorization,
|
|
264
|
+
verifyL402AccessToken
|
|
265
|
+
};
|