xytara 3.1.13 → 3.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.env.example +38 -4
  2. package/BSV_TERANODE_PLATFORM_DISCONTINUITY_RUNBOOK.md +126 -126
  3. package/REAL_PAYMENT_SETUP.md +14 -7
  4. package/RELEASE_NOTES.md +9 -4
  5. package/fixtures/catalog.js +22 -0
  6. package/lib/a2a_rpc.js +287 -280
  7. package/lib/account_auth.js +585 -585
  8. package/lib/agent_identity.js +162 -162
  9. package/lib/asyncapi_contract.js +3 -3
  10. package/lib/bolt_adapter.js +187 -187
  11. package/lib/bolt_payment_front.js +224 -224
  12. package/lib/buyer_event_log.js +215 -0
  13. package/lib/buyer_safety_policy.js +132 -0
  14. package/lib/checkout_processor_adapter.js +217 -217
  15. package/lib/command_flow.js +911 -722
  16. package/lib/commerce_artifacts.js +591 -591
  17. package/lib/commerce_bridge.js +274 -274
  18. package/lib/commerce_cases.js +399 -399
  19. package/lib/commerce_client.js +7694 -7694
  20. package/lib/commerce_quotes.js +29 -0
  21. package/lib/commerce_runtime.js +322 -322
  22. package/lib/execution_dedupe.js +86 -86
  23. package/lib/external_credit_bridge.js +142 -142
  24. package/lib/http_transport.js +99 -81
  25. package/lib/l402_adapter.js +241 -241
  26. package/lib/l402_payment_front.js +265 -265
  27. package/lib/mpp_adapter.js +237 -237
  28. package/lib/openapi_contract.js +4 -0
  29. package/lib/payment_config.js +257 -253
  30. package/lib/payment_verification.js +357 -357
  31. package/lib/provider_payout_execution.js +155 -155
  32. package/lib/provider_registry.js +852 -842
  33. package/lib/rate_limiter.js +81 -81
  34. package/lib/runtime_state_store.js +495 -369
  35. package/lib/settlement_bsv_live.js +600 -600
  36. package/lib/settlement_bsv_teranode.js +117 -117
  37. package/lib/stripe_mpp_payment_front.js +258 -258
  38. package/lib/x402_facilitator.js +198 -191
  39. package/lib/x402_lane_contract.js +5 -3
  40. package/lib/x402_settlement_recovery.js +175 -0
  41. package/package.json +9 -3
  42. package/scripts/buyer_events_daily_report.js +79 -0
  43. package/scripts/state_backup_offsite.js +196 -0
  44. package/scripts/verify_a2a_rpc.js +131 -105
  45. package/scripts/verify_agent_identity.js +153 -153
  46. package/scripts/verify_all.js +1 -1
  47. package/scripts/verify_bolt_adapter.js +128 -128
  48. package/scripts/verify_buyer_event_log.js +123 -0
  49. package/scripts/verify_buyer_safety.js +141 -0
  50. package/scripts/verify_cdp_facilitator.js +110 -110
  51. package/scripts/verify_checkout_processor.js +109 -109
  52. package/scripts/verify_commerce_bridge.js +177 -177
  53. package/scripts/verify_execution_dedupe.js +189 -189
  54. package/scripts/verify_execution_surfacing.js +76 -76
  55. package/scripts/verify_final_activation.js +191 -191
  56. package/scripts/verify_l402_adapter.js +86 -86
  57. package/scripts/verify_lifecycle_spend_binding.js +88 -0
  58. package/scripts/verify_mpp_adapter.js +128 -128
  59. package/scripts/verify_production_readiness.js +268 -268
  60. package/scripts/verify_rate_limiter.js +52 -52
  61. package/scripts/verify_refund_credit.js +106 -106
  62. package/scripts/verify_release_candidate.js +81 -81
  63. package/scripts/verify_runtime_state_guard.js +117 -0
  64. package/scripts/verify_service.js +15421 -15421
  65. package/scripts/verify_verify_execute_settle.js +209 -208
  66. package/scripts/verify_x402_facilitator.js +307 -286
  67. package/scripts/verify_x402_settlement_recovery.js +153 -0
  68. package/server.js +12242 -11974
@@ -1,187 +1,187 @@
1
- "use strict";
2
-
3
- // Provider-agnostic BSV / UTXO (BOLT) adapter.
4
- //
5
- // Makes the BOLT lane a REAL on-chain rail: it verifies that a presented BSV
6
- // transaction (txid) pays the operator's address for at least the required
7
- // amount, by fetching the transaction from a configured BSV chain API. The txid
8
- // is the proof of payment; replay is prevented by treating each txid as
9
- // single-use. Engages only when env configures a provider; otherwise the caller
10
- // preserves the existing bespoke token scheme. Dependency-free (node http/https).
11
- //
12
- // Env:
13
- // XYTARA_BOLT_PROVIDER = "whatsonchain" | "generic" (unset => not configured)
14
- // XYTARA_BOLT_PAYTO_ADDRESS (operator BSV address that must be paid)
15
- // XYTARA_BOLT_PRICE_SATS (minimum amount, satoshis)
16
- // XYTARA_BOLT_MIN_CONFIRMATIONS (default 0 => accept mempool)
17
- // XYTARA_BOLT_NETWORK = "main" | "test" (default main)
18
- // XYTARA_BOLT_ROOT_KEY | XYTARA_BOLT_SHARED_SECRET (HMAC key for the per-quote
19
- // amount offset; recommended)
20
- // whatsonchain: API base defaults to https://api.whatsonchain.com/v1/bsv/<network>
21
- // generic: XYTARA_BOLT_API_URL (tx-by-hash base; "/tx/hash/<txid>" appended)
22
-
23
- // REQUEST BINDING (production-safe) & LIMITATIONS:
24
- // - FLAT-RATE lane: the base price is a flat XYTARA_BOLT_PRICE_SATS (BSV sats
25
- // cannot be derived from a USD quote without a rate oracle). Use for flat-priced
26
- // calls; do not expose high-value, variably-priced commands on this lane.
27
- // - Per-quote binding: the front advertises a UNIQUE required amount
28
- // (priceSats + an unpredictable HMAC-derived offset) per quote and asks the
29
- // payer to pay it EXACTLY. verifyBsvPayment with options.requiredSats then
30
- // requires paidSats === requiredSats, so a pre-existing/unrelated payment to
31
- // the operator address cannot be reused. Each txid is also single-use.
32
- // (When called without requiredSats the legacy ">= priceSats" check applies.)
33
-
34
- const https = require("https");
35
- const http = require("http");
36
- const crypto = require("crypto");
37
- const { URL } = require("url");
38
-
39
- function env(name) {
40
- const v = process.env[name];
41
- return typeof v === "string" && v.trim() ? v.trim() : "";
42
- }
43
-
44
- function loadBoltConfig() {
45
- const provider = env("XYTARA_BOLT_PROVIDER").toLowerCase();
46
- if (provider !== "whatsonchain" && provider !== "generic") return { provider: null };
47
- const network = (env("XYTARA_BOLT_NETWORK") || env("XYTARA_BSV_TERANODE_NETWORK") || "main")
48
- .replace("mainnet", "main")
49
- .replace("testnet", "test");
50
- return {
51
- provider,
52
- network,
53
- apiUrl: env("XYTARA_BOLT_API_URL") || (provider === "whatsonchain" ? `https://api.whatsonchain.com/v1/bsv/${network}` : ""),
54
- payToAddress: env("XYTARA_BOLT_PAYTO_ADDRESS"),
55
- priceSats: Number(env("XYTARA_BOLT_PRICE_SATS") || "") || null,
56
- // Default to 1 confirmation so an unconfirmed (double-spendable) mempool tx is
57
- // not accepted as proof of payment. An operator may explicitly set 0 to accept
58
- // 0-conf at their own risk.
59
- minConfirmations: env("XYTARA_BOLT_MIN_CONFIRMATIONS") === "" ? 1 : Math.max(0, Number(env("XYTARA_BOLT_MIN_CONFIRMATIONS")) || 0),
60
- // Secret + window for the per-quote amount offset (request binding). A secret
61
- // key is recommended so the offset is unpredictable; it falls back to the
62
- // (public) payToAddress, which still binds via exact-amount + single-use.
63
- bindingSecret: env("XYTARA_BOLT_ROOT_KEY") || env("XYTARA_BOLT_SHARED_SECRET") || (env("XYTARA_BOLT_PAYTO_ADDRESS") + "|" + network),
64
- bindingWindowSats: Math.max(1, Number(env("XYTARA_BOLT_BINDING_WINDOW_SATS")) || 10000)
65
- };
66
- }
67
-
68
- function isConfigured(config) {
69
- return Boolean(config && config.provider);
70
- }
71
-
72
- // Deterministic unique required amount for a quote: base price + an HMAC-derived
73
- // offset in [0, bindingWindowSats). Both challenge and verify derive the same
74
- // value from the (stored, stable) quote, so the payer is told an exact amount and
75
- // the tx must pay it precisely.
76
- function computeRequiredSats(config, quote) {
77
- const base = Number(config && config.priceSats) || 0;
78
- if (!quote || !quote.quote_id) return base;
79
- const mac = crypto.createHmac("sha256", String(config.bindingSecret || "")).update(String(quote.quote_id)).digest("hex");
80
- const offset = parseInt(mac.slice(0, 8), 16) % Math.max(1, Number(config.bindingWindowSats) || 10000);
81
- return base + offset;
82
- }
83
-
84
- function httpGetJson(urlStr, timeoutMs) {
85
- return new Promise((resolve, reject) => {
86
- let u;
87
- try {
88
- u = new URL(urlStr);
89
- } catch (e) {
90
- return reject(new Error("invalid_url"));
91
- }
92
- const lib = u.protocol === "http:" ? http : https;
93
- const req = lib.request(
94
- { method: "GET", hostname: u.hostname, port: u.port || (u.protocol === "http:" ? 80 : 443), path: u.pathname + u.search, headers: { Accept: "application/json" } },
95
- (res) => {
96
- let d = "";
97
- res.on("data", (c) => (d += c));
98
- res.on("end", () => {
99
- let j = null;
100
- try {
101
- j = d ? JSON.parse(d) : null;
102
- } catch (_) {
103
- j = null;
104
- }
105
- resolve({ status: res.statusCode, json: j, raw: d });
106
- });
107
- }
108
- );
109
- req.on("error", reject);
110
- req.setTimeout(timeoutMs || 15000, () => req.destroy(new Error("bolt_api_timeout")));
111
- req.end();
112
- });
113
- }
114
-
115
- // Collect the payout addresses declared on a vout's scriptPubKey across the
116
- // shapes different BSV APIs use.
117
- function voutAddresses(vout) {
118
- const spk = (vout && vout.scriptPubKey) || {};
119
- if (Array.isArray(spk.addresses)) return spk.addresses;
120
- if (spk.address) return [spk.address];
121
- if (Array.isArray(vout && vout.addresses)) return vout.addresses;
122
- return [];
123
- }
124
-
125
- // Verify a BSV txid pays >= priceSats to payToAddress. Returns
126
- // { ok, txid, paid_sats, confirmations, network } | { ok:false, reason, detail? }.
127
- async function verifyBsvPayment(config, txid, fetchTxOverride, options) {
128
- if (!isConfigured(config)) return { ok: false, reason: "bolt_provider_not_configured" };
129
- if (!config.payToAddress || !config.priceSats) return { ok: false, reason: "bolt_payto_or_price_missing" };
130
- if (!config.apiUrl) return { ok: false, reason: "bolt_api_url_missing" };
131
- if (!/^[0-9a-fA-F]{64}$/.test(String(txid || ""))) return { ok: false, reason: "bolt_txid_invalid" };
132
- const fetchTx = typeof fetchTxOverride === "function" ? fetchTxOverride : (u) => httpGetJson(u, 15000);
133
- let res;
134
- try {
135
- res = await fetchTx(config.apiUrl.replace(/\/$/, "") + "/tx/hash/" + txid);
136
- } catch (e) {
137
- return { ok: false, reason: "bolt_api_unreachable", detail: e && e.message ? e.message : "request_error" };
138
- }
139
- if (!res.json || res.status >= 400) return { ok: false, reason: "bolt_tx_not_found", detail: `HTTP ${res.status}` };
140
- const j = res.json;
141
- const confirmations = Number(j.confirmations || 0);
142
- if (config.minConfirmations > 0 && confirmations < config.minConfirmations) {
143
- return { ok: false, reason: "bolt_insufficient_confirmations", detail: `${confirmations}/${config.minConfirmations}` };
144
- }
145
- const vouts = Array.isArray(j.vout) ? j.vout : [];
146
- let paidSats = 0;
147
- for (const o of vouts) {
148
- if (voutAddresses(o).includes(config.payToAddress)) {
149
- // BSV APIs report value in BSV; convert to satoshis.
150
- paidSats += Math.round(Number(o.value || 0) * 1e8);
151
- }
152
- }
153
- // Per-quote binding: when a unique required amount is supplied, the tx must pay
154
- // it EXACTLY (not merely >=), so an unrelated payment to the address cannot be
155
- // reused. Without it, fall back to the flat ">= priceSats" check.
156
- const requiredSats = options && Number.isFinite(Number(options.requiredSats)) ? Math.round(Number(options.requiredSats)) : null;
157
- if (requiredSats !== null) {
158
- if (paidSats !== requiredSats) {
159
- return { ok: false, reason: "bolt_amount_mismatch", detail: `${paidSats}/${requiredSats}` };
160
- }
161
- } else if (paidSats < config.priceSats) {
162
- return { ok: false, reason: "bolt_amount_insufficient", detail: `${paidSats}/${config.priceSats}` };
163
- }
164
- return { ok: true, txid: String(txid), paid_sats: paidSats, confirmations, network: config.network, required_sats: requiredSats };
165
- }
166
-
167
- // Parse "BOLT <txid>" (64-hex). Returns { txid } | null.
168
- function parseBoltHeader(authorization) {
169
- const m = String(authorization || "").match(/^BOLT\s+([0-9a-fA-F]{64})\s*$/);
170
- return m ? { txid: m[1] } : null;
171
- }
172
-
173
- // The challenge an unpaid caller needs: where to pay and how much. When a
174
- // per-quote required amount is supplied, advertise that exact amount (the payer
175
- // must pay it precisely) instead of the flat base price.
176
- function buildBoltChallenge(config, options) {
177
- const requiredSats = options && Number.isFinite(Number(options.requiredSats)) ? Math.round(Number(options.requiredSats)) : null;
178
- return {
179
- address: config.payToAddress,
180
- amount_sats: requiredSats !== null ? requiredSats : config.priceSats,
181
- exact: requiredSats !== null,
182
- network: config.network,
183
- min_confirmations: config.minConfirmations
184
- };
185
- }
186
-
187
- module.exports = { loadBoltConfig, isConfigured, verifyBsvPayment, parseBoltHeader, buildBoltChallenge, computeRequiredSats };
1
+ "use strict";
2
+
3
+ // Provider-agnostic BSV / UTXO (BOLT) adapter.
4
+ //
5
+ // Makes the BOLT lane a REAL on-chain rail: it verifies that a presented BSV
6
+ // transaction (txid) pays the operator's address for at least the required
7
+ // amount, by fetching the transaction from a configured BSV chain API. The txid
8
+ // is the proof of payment; replay is prevented by treating each txid as
9
+ // single-use. Engages only when env configures a provider; otherwise the caller
10
+ // preserves the existing bespoke token scheme. Dependency-free (node http/https).
11
+ //
12
+ // Env:
13
+ // XYTARA_BOLT_PROVIDER = "whatsonchain" | "generic" (unset => not configured)
14
+ // XYTARA_BOLT_PAYTO_ADDRESS (operator BSV address that must be paid)
15
+ // XYTARA_BOLT_PRICE_SATS (minimum amount, satoshis)
16
+ // XYTARA_BOLT_MIN_CONFIRMATIONS (default 0 => accept mempool)
17
+ // XYTARA_BOLT_NETWORK = "main" | "test" (default main)
18
+ // XYTARA_BOLT_ROOT_KEY | XYTARA_BOLT_SHARED_SECRET (HMAC key for the per-quote
19
+ // amount offset; recommended)
20
+ // whatsonchain: API base defaults to https://api.whatsonchain.com/v1/bsv/<network>
21
+ // generic: XYTARA_BOLT_API_URL (tx-by-hash base; "/tx/hash/<txid>" appended)
22
+
23
+ // REQUEST BINDING (production-safe) & LIMITATIONS:
24
+ // - FLAT-RATE lane: the base price is a flat XYTARA_BOLT_PRICE_SATS (BSV sats
25
+ // cannot be derived from a USD quote without a rate oracle). Use for flat-priced
26
+ // calls; do not expose high-value, variably-priced commands on this lane.
27
+ // - Per-quote binding: the front advertises a UNIQUE required amount
28
+ // (priceSats + an unpredictable HMAC-derived offset) per quote and asks the
29
+ // payer to pay it EXACTLY. verifyBsvPayment with options.requiredSats then
30
+ // requires paidSats === requiredSats, so a pre-existing/unrelated payment to
31
+ // the operator address cannot be reused. Each txid is also single-use.
32
+ // (When called without requiredSats the legacy ">= priceSats" check applies.)
33
+
34
+ const https = require("https");
35
+ const http = require("http");
36
+ const crypto = require("crypto");
37
+ const { URL } = require("url");
38
+
39
+ function env(name) {
40
+ const v = process.env[name];
41
+ return typeof v === "string" && v.trim() ? v.trim() : "";
42
+ }
43
+
44
+ function loadBoltConfig() {
45
+ const provider = env("XYTARA_BOLT_PROVIDER").toLowerCase();
46
+ if (provider !== "whatsonchain" && provider !== "generic") return { provider: null };
47
+ const network = (env("XYTARA_BOLT_NETWORK") || env("XYTARA_BSV_TERANODE_NETWORK") || "main")
48
+ .replace("mainnet", "main")
49
+ .replace("testnet", "test");
50
+ return {
51
+ provider,
52
+ network,
53
+ apiUrl: env("XYTARA_BOLT_API_URL") || (provider === "whatsonchain" ? `https://api.whatsonchain.com/v1/bsv/${network}` : ""),
54
+ payToAddress: env("XYTARA_BOLT_PAYTO_ADDRESS"),
55
+ priceSats: Number(env("XYTARA_BOLT_PRICE_SATS") || "") || null,
56
+ // Default to 1 confirmation so an unconfirmed (double-spendable) mempool tx is
57
+ // not accepted as proof of payment. An operator may explicitly set 0 to accept
58
+ // 0-conf at their own risk.
59
+ minConfirmations: env("XYTARA_BOLT_MIN_CONFIRMATIONS") === "" ? 1 : Math.max(0, Number(env("XYTARA_BOLT_MIN_CONFIRMATIONS")) || 0),
60
+ // Secret + window for the per-quote amount offset (request binding). A secret
61
+ // key is recommended so the offset is unpredictable; it falls back to the
62
+ // (public) payToAddress, which still binds via exact-amount + single-use.
63
+ bindingSecret: env("XYTARA_BOLT_ROOT_KEY") || env("XYTARA_BOLT_SHARED_SECRET") || (env("XYTARA_BOLT_PAYTO_ADDRESS") + "|" + network),
64
+ bindingWindowSats: Math.max(1, Number(env("XYTARA_BOLT_BINDING_WINDOW_SATS")) || 10000)
65
+ };
66
+ }
67
+
68
+ function isConfigured(config) {
69
+ return Boolean(config && config.provider);
70
+ }
71
+
72
+ // Deterministic unique required amount for a quote: base price + an HMAC-derived
73
+ // offset in [0, bindingWindowSats). Both challenge and verify derive the same
74
+ // value from the (stored, stable) quote, so the payer is told an exact amount and
75
+ // the tx must pay it precisely.
76
+ function computeRequiredSats(config, quote) {
77
+ const base = Number(config && config.priceSats) || 0;
78
+ if (!quote || !quote.quote_id) return base;
79
+ const mac = crypto.createHmac("sha256", String(config.bindingSecret || "")).update(String(quote.quote_id)).digest("hex");
80
+ const offset = parseInt(mac.slice(0, 8), 16) % Math.max(1, Number(config.bindingWindowSats) || 10000);
81
+ return base + offset;
82
+ }
83
+
84
+ function httpGetJson(urlStr, timeoutMs) {
85
+ return new Promise((resolve, reject) => {
86
+ let u;
87
+ try {
88
+ u = new URL(urlStr);
89
+ } catch (e) {
90
+ return reject(new Error("invalid_url"));
91
+ }
92
+ const lib = u.protocol === "http:" ? http : https;
93
+ const req = lib.request(
94
+ { method: "GET", hostname: u.hostname, port: u.port || (u.protocol === "http:" ? 80 : 443), path: u.pathname + u.search, headers: { Accept: "application/json" } },
95
+ (res) => {
96
+ let d = "";
97
+ res.on("data", (c) => (d += c));
98
+ res.on("end", () => {
99
+ let j = null;
100
+ try {
101
+ j = d ? JSON.parse(d) : null;
102
+ } catch (_) {
103
+ j = null;
104
+ }
105
+ resolve({ status: res.statusCode, json: j, raw: d });
106
+ });
107
+ }
108
+ );
109
+ req.on("error", reject);
110
+ req.setTimeout(timeoutMs || 15000, () => req.destroy(new Error("bolt_api_timeout")));
111
+ req.end();
112
+ });
113
+ }
114
+
115
+ // Collect the payout addresses declared on a vout's scriptPubKey across the
116
+ // shapes different BSV APIs use.
117
+ function voutAddresses(vout) {
118
+ const spk = (vout && vout.scriptPubKey) || {};
119
+ if (Array.isArray(spk.addresses)) return spk.addresses;
120
+ if (spk.address) return [spk.address];
121
+ if (Array.isArray(vout && vout.addresses)) return vout.addresses;
122
+ return [];
123
+ }
124
+
125
+ // Verify a BSV txid pays >= priceSats to payToAddress. Returns
126
+ // { ok, txid, paid_sats, confirmations, network } | { ok:false, reason, detail? }.
127
+ async function verifyBsvPayment(config, txid, fetchTxOverride, options) {
128
+ if (!isConfigured(config)) return { ok: false, reason: "bolt_provider_not_configured" };
129
+ if (!config.payToAddress || !config.priceSats) return { ok: false, reason: "bolt_payto_or_price_missing" };
130
+ if (!config.apiUrl) return { ok: false, reason: "bolt_api_url_missing" };
131
+ if (!/^[0-9a-fA-F]{64}$/.test(String(txid || ""))) return { ok: false, reason: "bolt_txid_invalid" };
132
+ const fetchTx = typeof fetchTxOverride === "function" ? fetchTxOverride : (u) => httpGetJson(u, 15000);
133
+ let res;
134
+ try {
135
+ res = await fetchTx(config.apiUrl.replace(/\/$/, "") + "/tx/hash/" + txid);
136
+ } catch (e) {
137
+ return { ok: false, reason: "bolt_api_unreachable", detail: e && e.message ? e.message : "request_error" };
138
+ }
139
+ if (!res.json || res.status >= 400) return { ok: false, reason: "bolt_tx_not_found", detail: `HTTP ${res.status}` };
140
+ const j = res.json;
141
+ const confirmations = Number(j.confirmations || 0);
142
+ if (config.minConfirmations > 0 && confirmations < config.minConfirmations) {
143
+ return { ok: false, reason: "bolt_insufficient_confirmations", detail: `${confirmations}/${config.minConfirmations}` };
144
+ }
145
+ const vouts = Array.isArray(j.vout) ? j.vout : [];
146
+ let paidSats = 0;
147
+ for (const o of vouts) {
148
+ if (voutAddresses(o).includes(config.payToAddress)) {
149
+ // BSV APIs report value in BSV; convert to satoshis.
150
+ paidSats += Math.round(Number(o.value || 0) * 1e8);
151
+ }
152
+ }
153
+ // Per-quote binding: when a unique required amount is supplied, the tx must pay
154
+ // it EXACTLY (not merely >=), so an unrelated payment to the address cannot be
155
+ // reused. Without it, fall back to the flat ">= priceSats" check.
156
+ const requiredSats = options && Number.isFinite(Number(options.requiredSats)) ? Math.round(Number(options.requiredSats)) : null;
157
+ if (requiredSats !== null) {
158
+ if (paidSats !== requiredSats) {
159
+ return { ok: false, reason: "bolt_amount_mismatch", detail: `${paidSats}/${requiredSats}` };
160
+ }
161
+ } else if (paidSats < config.priceSats) {
162
+ return { ok: false, reason: "bolt_amount_insufficient", detail: `${paidSats}/${config.priceSats}` };
163
+ }
164
+ return { ok: true, txid: String(txid), paid_sats: paidSats, confirmations, network: config.network, required_sats: requiredSats };
165
+ }
166
+
167
+ // Parse "BOLT <txid>" (64-hex). Returns { txid } | null.
168
+ function parseBoltHeader(authorization) {
169
+ const m = String(authorization || "").match(/^BOLT\s+([0-9a-fA-F]{64})\s*$/);
170
+ return m ? { txid: m[1] } : null;
171
+ }
172
+
173
+ // The challenge an unpaid caller needs: where to pay and how much. When a
174
+ // per-quote required amount is supplied, advertise that exact amount (the payer
175
+ // must pay it precisely) instead of the flat base price.
176
+ function buildBoltChallenge(config, options) {
177
+ const requiredSats = options && Number.isFinite(Number(options.requiredSats)) ? Math.round(Number(options.requiredSats)) : null;
178
+ return {
179
+ address: config.payToAddress,
180
+ amount_sats: requiredSats !== null ? requiredSats : config.priceSats,
181
+ exact: requiredSats !== null,
182
+ network: config.network,
183
+ min_confirmations: config.minConfirmations
184
+ };
185
+ }
186
+
187
+ module.exports = { loadBoltConfig, isConfigured, verifyBsvPayment, parseBoltHeader, buildBoltChallenge, computeRequiredSats };