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 CHANGED
@@ -13,6 +13,10 @@ XYTARA_X402_SETTLE=true
13
13
  # credit units a single self-serve /x402/credits/topup grants (default 1000);
14
14
  # an accepts entry may override per-payment via its extra.credits
15
15
  XYTARA_X402_TOPUP_UNITS=1000
16
+ # Coinbase CDP auth for the CDP-hosted facilitator (Base mainnet); Ed25519 Secret API Key.
17
+ # The runtime mints a fresh CDP Bearer JWT per request when both are set.
18
+ CDP_API_KEY_ID=
19
+ CDP_API_KEY_SECRET=
16
20
  XYTARA_PAYMENT_MAX_AGE_SECONDS=300
17
21
  XYTARA_WALLET_REGISTRY_JSON=[]
18
22
  XYTARA_TREASURY_DESTINATION_ID=
@@ -72,21 +72,25 @@ settle it through a mainnet-capable facilitator.
72
72
 
73
73
  Important: the default `https://x402.org/facilitator` settles **Base Sepolia only** (its
74
74
  `/supported` list has no `eip155:8453`). Pointing a mainnet `accepts` entry at it fails
75
- closed and the route is unpayable. For mainnet you need a mainnet-capable facilitator, such
76
- as Coinbase CDP (`https://api.cdp.coinbase.com/platform/v2/x402`), which requires a CDP API
77
- key passed as auth:
75
+ closed and the route is unpayable. For mainnet use a mainnet-capable facilitator, such as
76
+ Coinbase CDP (`https://api.cdp.coinbase.com/platform/v2/x402`), which is supported natively:
77
+ create a CDP Secret API Key (Ed25519) and set its id and secret, and the runtime mints a
78
+ fresh short-lived CDP Bearer JWT per facilitator call (CDP rejects static tokens).
78
79
 
79
80
  ```env
80
81
  # advertise mainnet (real USDC on Base); you can keep a Sepolia entry in the array too
81
82
  XYTARA_X402_ACCEPTS_JSON=[{"scheme":"exact","network":"eip155:8453","asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913","amount":"10000","payTo":"<your-evm-address>","extra":{"name":"USDC","version":"2"},"maxTimeoutSeconds":60}]
82
- # a mainnet-capable facilitator (CDP shown) and its auth header:
83
+ # point at the CDP-hosted facilitator and supply the CDP Secret API Key:
83
84
  XYTARA_X402_FACILITATOR_URL=https://api.cdp.coinbase.com/platform/v2/x402
84
- XYTARA_X402_FACILITATOR_HEADERS_JSON={"Authorization":"Bearer <cdp-access-token>"}
85
+ CDP_API_KEY_ID=<your-cdp-key-id>
86
+ CDP_API_KEY_SECRET=<your-cdp-ed25519-secret-base64>
85
87
  ```
86
88
 
87
- `amount` is in the asset's base units (USDC has 6 decimals, so `10000` = 0.01 USDC). Confirm
88
- your facilitator lists `eip155:8453` in its `/supported` response before advertising it, and
89
- remember that once mainnet is live, buyers spend real USDC.
89
+ The CDP key must use the **Ed25519** signature algorithm (not ECDSA). `CDP_API_KEY_ID` /
90
+ `CDP_API_KEY_SECRET` are the CDP-standard names; `XYTARA_X402_CDP_KEY_ID` /
91
+ `XYTARA_X402_CDP_KEY_SECRET` are accepted as overrides. `amount` is in the asset's base units
92
+ (USDC has 6 decimals, so `10000` = 0.01 USDC). Verify the CDP auth path locally with
93
+ `npm run verify:cdp-facilitator`. Once mainnet is live, buyers spend real USDC.
90
94
 
91
95
  ## Minimum Production-Oriented Configuration
92
96
 
package/RELEASE_NOTES.md CHANGED
@@ -1,9 +1,10 @@
1
- `xytara` 3.1.4 makes the self-serve credits capability discoverable and documents the path to Base mainnet.
1
+ `xytara` 3.1.6 fixes the x402 facilitator request so strict facilitators like Coinbase CDP accept it.
2
2
 
3
- This is a discovery and documentation release on top of the self-serve credits shipped earlier in the line. No existing behavior changes.
3
+ The facilitator client was adding a `resource` field to the `paymentRequirements` it sends to `/verify` and `/settle`. The x402 v2 `PaymentRequirements` shape has no `resource` field (resource belongs on the payload), so strict facilitators such as the CDP-hosted facilitator rejected the request with HTTP 400, which blocked Base mainnet settlement through CDP. Lenient facilitators ignored the extra field, so the default Base Sepolia path was unaffected.
4
4
 
5
5
  Highlights:
6
6
 
7
- - the x402 lane summary (`/v1/x402/summary`) and lane pack now advertise `credits_topup_ref` (`/x402/credits/topup`) and a `self_serve_credits` first-run flow, so agents discovering the runtime see the pay-once-get-credits path alongside direct command execution and paid MCP tooling
8
- - documents going to Base mainnet in `REAL_PAYMENT_SETUP.md`: the mainnet `accepts` profile (real USDC on `eip155:8453`) and the requirement for a mainnet-capable facilitator, since the default `x402.org` facilitator settles Base Sepolia only
9
- - no change to payment verification, settlement, the credits ledger, or any existing route
7
+ - sends a clean x402 v2 `paymentRequirements` (`scheme`, `network`, `asset`, `amount`, `payTo`, `maxTimeoutSeconds`, `extra`) with no extra `resource` field, so it matches `paymentPayload.accepted` and passes strict facilitator validation
8
+ - this unblocks the CDP facilitator path added in the previous release, including Base mainnet
9
+ - adds a regression check that the facilitator receives `paymentRequirements` without a `resource` field and equal to the payment payload's `accepted`
10
+ - no change to payment semantics, settlement, or any route; lenient-facilitator and Base Sepolia behavior is unchanged
@@ -6,9 +6,9 @@
6
6
  "source_ref": "spine.job.commitment",
7
7
  "committed_ref": "transaction.job.commitment",
8
8
  "precommit_pack_ref": "precommit.job.commitment.readiness",
9
- "transaction_id": "txn_01b29e65db7d",
10
- "receipt_id": "rcpt_b40aa20be45e",
11
- "proof_ref": "proof_rcpt_b40aa20be45e",
9
+ "transaction_id": "txn_2061435e6b7b",
10
+ "receipt_id": "rcpt_4451d9ab898f",
11
+ "proof_ref": "proof_rcpt_4451d9ab898f",
12
12
  "payment_protocol": "x402",
13
13
  "settlement_mode": "evm_payment",
14
14
  "treasury_destination_id": "treasury_acct_demo",
package/lib/cdp_jwt.js ADDED
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+
3
+ // Zero-dependency Coinbase CDP Bearer JWT signer, used to authenticate requests to the
4
+ // CDP-hosted x402 facilitator (https://api.cdp.coinbase.com/platform/v2/x402), which
5
+ // settles Base mainnet. CDP authenticates each request with a short-lived (<=120s) EdDSA
6
+ // (Ed25519) JWT generated from a CDP Secret API Key, so a static Authorization header is
7
+ // not usable; a fresh token is minted per request with a `uri` claim bound to that request.
8
+ //
9
+ // Secret format: base64-encoded Ed25519, 64 bytes (first 32 = seed, last 32 = public key).
10
+ // Reference: https://docs.cdp.coinbase.com/get-started/docs/cdp-api-keys
11
+
12
+ const crypto = require("crypto");
13
+
14
+ // DER PKCS#8 prefix for an Ed25519 private key carrying a 32-byte seed.
15
+ const ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
16
+
17
+ function base64url(input) {
18
+ const buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
19
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
20
+ }
21
+
22
+ function privateKeyFromSecret(keySecret) {
23
+ const decoded = Buffer.from(String(keySecret || "").trim(), "base64");
24
+ if (decoded.length !== 64) {
25
+ throw new Error("cdp_key_secret_invalid: expected a base64 Ed25519 key of 64 bytes");
26
+ }
27
+ const seed = decoded.subarray(0, 32);
28
+ const pkcs8 = Buffer.concat([ED25519_PKCS8_PREFIX, seed]);
29
+ return crypto.createPrivateKey({ key: pkcs8, format: "der", type: "pkcs8" });
30
+ }
31
+
32
+ // Build a CDP Bearer JWT for a single request.
33
+ // keyId CDP API key id (UUID)
34
+ // keySecret base64 Ed25519 secret (64 bytes)
35
+ // method HTTP method, e.g. "POST"
36
+ // host request host, e.g. "api.cdp.coinbase.com"
37
+ // path request path, e.g. "/platform/v2/x402/verify"
38
+ function buildCdpBearerToken({ keyId, keySecret, method, host, path }) {
39
+ const id = String(keyId || "").trim();
40
+ if (!id) throw new Error("cdp_key_id_missing");
41
+ const privateKey = privateKeyFromSecret(keySecret);
42
+ const now = Math.floor(Date.now() / 1000);
43
+ const header = { alg: "EdDSA", typ: "JWT", kid: id, nonce: crypto.randomBytes(16).toString("hex") };
44
+ const claims = {
45
+ sub: id,
46
+ iss: "cdp",
47
+ aud: ["cdp_service"],
48
+ nbf: now,
49
+ exp: now + 120,
50
+ uri: `${String(method || "POST").toUpperCase()} ${host}${path}`
51
+ };
52
+ const signingInput = `${base64url(JSON.stringify(header))}.${base64url(JSON.stringify(claims))}`;
53
+ const signature = crypto.sign(null, Buffer.from(signingInput), privateKey);
54
+ return `${signingInput}.${base64url(signature)}`;
55
+ }
56
+
57
+ module.exports = { buildCdpBearerToken, base64url };
@@ -1,242 +1,253 @@
1
- "use strict";
2
-
3
- const fs = require("fs");
4
- const {
5
- buildBsvTeranodeTreasuryDestination,
6
- normalizeSettlementMode
7
- } = require("./settlement_bsv_teranode");
8
-
9
- function safeParseJson(text, fallback) {
10
- try {
11
- return JSON.parse(text);
12
- } catch (_) {
13
- return fallback;
14
- }
15
- }
16
-
17
- function readJsonFromEnvOrPath(jsonValue, pathValue, fallback) {
18
- if (typeof jsonValue === "string" && jsonValue.trim()) {
19
- return safeParseJson(jsonValue, fallback);
20
- }
21
- if (typeof pathValue === "string" && pathValue.trim()) {
22
- try {
23
- return safeParseJson(fs.readFileSync(pathValue, "utf8"), fallback);
24
- } catch (_) {
25
- return fallback;
26
- }
27
- }
28
- return fallback;
29
- }
30
-
31
- function loadWalletRegistry() {
32
- const raw = readJsonFromEnvOrPath(
33
- process.env.XYTARA_WALLET_REGISTRY_JSON,
34
- process.env.XYTARA_WALLET_REGISTRY_PATH,
35
- []
36
- );
37
- const entries = Array.isArray(raw)
38
- ? raw
39
- : Array.isArray(raw && raw.wallets) ? raw.wallets : [];
40
- const byWalletId = new Map();
41
- for (const entry of entries) {
42
- if (!entry || typeof entry !== "object") continue;
43
- const walletId = typeof entry.wallet_id === "string" ? entry.wallet_id.trim() : "";
44
- const secret = typeof entry.secret === "string" ? entry.secret : "";
45
- if (!walletId || !secret) continue;
46
- byWalletId.set(walletId, {
47
- wallet_id: walletId,
48
- secret,
49
- account_id: typeof entry.account_id === "string" && entry.account_id.trim() ? entry.account_id.trim() : null,
50
- enabled: entry.enabled !== false
51
- });
52
- }
53
- return byWalletId;
54
- }
55
-
56
- function loadTreasuryDestinationMap() {
57
- const raw = readJsonFromEnvOrPath(
58
- process.env.XYTARA_TREASURY_DESTINATIONS_JSON,
59
- process.env.XYTARA_TREASURY_DESTINATIONS_PATH,
60
- {}
61
- );
62
- const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
63
- const normalized = {};
64
- for (const [settlementMode, entry] of Object.entries(input)) {
65
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
66
- normalized[normalizeSettlementMode(settlementMode)] = {
67
- destination_id: typeof entry.destination_id === "string" && entry.destination_id.trim()
68
- ? entry.destination_id.trim()
69
- : null,
70
- destination_kind: typeof entry.destination_kind === "string" && entry.destination_kind.trim()
71
- ? entry.destination_kind.trim()
72
- : null,
73
- account_id: typeof entry.account_id === "string" && entry.account_id.trim()
74
- ? entry.account_id.trim()
75
- : null,
76
- external_ref: typeof entry.external_ref === "string" && entry.external_ref.trim()
77
- ? entry.external_ref.trim()
78
- : null,
79
- custody_ref: typeof entry.custody_ref === "string" && entry.custody_ref.trim()
80
- ? entry.custody_ref.trim()
81
- : null,
82
- reporting_ref: typeof entry.reporting_ref === "string" && entry.reporting_ref.trim()
83
- ? entry.reporting_ref.trim()
84
- : null,
85
- wallet_ref: typeof entry.wallet_ref === "string" && entry.wallet_ref.trim()
86
- ? entry.wallet_ref.trim()
87
- : null,
88
- provider_ref: typeof entry.provider_ref === "string" && entry.provider_ref.trim()
89
- ? entry.provider_ref.trim()
90
- : null,
91
- network: typeof entry.network === "string" && entry.network.trim()
92
- ? entry.network.trim()
93
- : null,
94
- chain_id: typeof entry.chain_id === "string" && entry.chain_id.trim()
95
- ? entry.chain_id.trim()
96
- : null,
97
- asset_symbol: typeof entry.asset_symbol === "string" && entry.asset_symbol.trim()
98
- ? entry.asset_symbol.trim()
99
- : null,
100
- policy_scope: typeof entry.policy_scope === "string" && entry.policy_scope.trim()
101
- ? entry.policy_scope.trim()
102
- : null,
103
- storage_class: typeof entry.storage_class === "string" && entry.storage_class.trim()
104
- ? entry.storage_class.trim()
105
- : null,
106
- enabled: entry.enabled !== false,
107
- accept_inbound: entry.accept_inbound !== false
108
- };
109
- }
110
- return normalized;
111
- }
112
-
113
- function loadPaymentConfig() {
114
- // Default is fail-closed: signed payment acceptance. "presence" (accept any non-empty payment header)
115
- // must now be opted into explicitly and is only appropriate for local development.
116
- const verificationModeInput = String(process.env.XYTARA_PAYMENT_VERIFICATION_MODE || "local_signed").trim() || "local_signed";
117
- const verificationMode = verificationModeInput === "signed" ? "local_signed" : verificationModeInput;
118
- const maxAgeSeconds = Number(process.env.XYTARA_PAYMENT_MAX_AGE_SECONDS || 300);
119
- return {
120
- verification_mode: verificationMode,
121
- verification_mode_input: verificationModeInput,
122
- payment_max_age_seconds: Number.isFinite(maxAgeSeconds) && maxAgeSeconds > 0 ? maxAgeSeconds : 300,
123
- wallet_registry: loadWalletRegistry(),
124
- x402: loadX402FacilitatorConfig(),
125
- treasury_destination_id: String(process.env.XYTARA_TREASURY_DESTINATION_ID || "").trim() || null,
126
- treasury_destination_kind: String(process.env.XYTARA_TREASURY_DESTINATION_KIND || "").trim() || null,
127
- treasury_account_id: String(process.env.XYTARA_TREASURY_ACCOUNT_ID || "").trim() || null,
128
- treasury_external_ref: String(process.env.XYTARA_TREASURY_EXTERNAL_REF || "").trim() || null,
129
- treasury_destinations_by_settlement_mode: loadTreasuryDestinationMap()
130
- };
131
- }
132
-
133
- // Configuration for the x402_facilitator verification mode: the on-chain, trustless
134
- // pay-per-call path. The advertised accepts tell a buyer how to pay; the facilitator
135
- // verifies and settles the signed payment before any work runs.
136
- function loadX402FacilitatorConfig() {
137
- let accepts = [];
138
- const rawAccepts = String(process.env.XYTARA_X402_ACCEPTS_JSON || "").trim();
139
- if (rawAccepts) {
140
- try {
141
- const parsed = JSON.parse(rawAccepts);
142
- if (Array.isArray(parsed)) {
143
- accepts = parsed
144
- .map((entry) => ({
145
- scheme: String((entry && entry.scheme) || "exact").trim(),
146
- network: String((entry && entry.network) || "").trim(),
147
- asset: String((entry && entry.asset) || "").trim(),
148
- amount: String((entry && entry.amount) || "").trim(),
149
- payTo: String((entry && entry.payTo) || "").trim(),
150
- ...(entry && entry.extra && typeof entry.extra === "object" ? { extra: entry.extra } : {}),
151
- ...(Number(entry && entry.maxTimeoutSeconds) > 0 ? { maxTimeoutSeconds: Number(entry.maxTimeoutSeconds) } : {})
152
- }))
153
- .filter((entry) => entry.network && entry.asset && entry.amount && entry.payTo);
154
- }
155
- } catch (_) {
156
- accepts = [];
157
- }
158
- }
159
- let headers = {};
160
- const rawHeaders = String(process.env.XYTARA_X402_FACILITATOR_HEADERS_JSON || "").trim();
161
- if (rawHeaders) {
162
- try {
163
- const parsed = JSON.parse(rawHeaders);
164
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) headers = parsed;
165
- } catch (_) {
166
- headers = {};
167
- }
168
- }
169
- return {
170
- accepts,
171
- facilitator_url: String(process.env.XYTARA_X402_FACILITATOR_URL || "https://x402.org/facilitator").trim().replace(/\/$/, ""),
172
- facilitator_headers: headers,
173
- facilitator_timeout_ms: Math.max(1000, Number(process.env.XYTARA_X402_FACILITATOR_TIMEOUT_MS || 15000) || 15000),
174
- settle: String(process.env.XYTARA_X402_SETTLE || "true").trim().toLowerCase() !== "false",
175
- // Credit units a single self-serve x402 top-up grants, unless the matched accepts
176
- // entry carries its own `extra.credits`.
177
- topup_units_default: Math.max(0, Number(process.env.XYTARA_X402_TOPUP_UNITS || 1000) || 0)
178
- };
179
- }
180
-
181
- function buildConfiguredTreasuryDestination(accountId, settlementMode) {
182
- const config = loadPaymentConfig();
183
- const normalizedSettlementMode = normalizeSettlementMode(settlementMode);
184
- const mapEntry = normalizedSettlementMode && config.treasury_destinations_by_settlement_mode[normalizedSettlementMode]
185
- ? config.treasury_destinations_by_settlement_mode[normalizedSettlementMode]
186
- : null;
187
- const destinationId = mapEntry && typeof mapEntry.destination_id === "string" && mapEntry.destination_id.trim()
188
- ? mapEntry.destination_id.trim()
189
- : config.treasury_destination_id;
190
- if (!destinationId) {
191
- return buildBsvTeranodeTreasuryDestination(accountId, normalizedSettlementMode);
192
- }
193
- return {
194
- destination_id: destinationId,
195
- destination_kind:
196
- (mapEntry && typeof mapEntry.destination_kind === "string" && mapEntry.destination_kind.trim())
197
- || config.treasury_destination_kind
198
- || "merchant_treasury",
199
- account_id:
200
- (mapEntry && typeof mapEntry.account_id === "string" && mapEntry.account_id.trim())
201
- || config.treasury_account_id
202
- || accountId
203
- || null,
204
- settlement_mode: normalizedSettlementMode || null,
205
- external_ref:
206
- (mapEntry && typeof mapEntry.external_ref === "string" && mapEntry.external_ref.trim())
207
- || config.treasury_external_ref
208
- || null,
209
- custody_ref: mapEntry && typeof mapEntry.custody_ref === "string" && mapEntry.custody_ref.trim()
210
- ? mapEntry.custody_ref.trim()
211
- : null,
212
- reporting_ref: mapEntry && typeof mapEntry.reporting_ref === "string" && mapEntry.reporting_ref.trim()
213
- ? mapEntry.reporting_ref.trim()
214
- : null,
215
- wallet_ref: mapEntry && typeof mapEntry.wallet_ref === "string" && mapEntry.wallet_ref.trim()
216
- ? mapEntry.wallet_ref.trim()
217
- : null,
218
- provider_ref: mapEntry && typeof mapEntry.provider_ref === "string" && mapEntry.provider_ref.trim()
219
- ? mapEntry.provider_ref.trim()
220
- : null,
221
- network: mapEntry && typeof mapEntry.network === "string" && mapEntry.network.trim()
222
- ? mapEntry.network.trim()
223
- : null,
224
- chain_id: mapEntry && typeof mapEntry.chain_id === "string" && mapEntry.chain_id.trim()
225
- ? mapEntry.chain_id.trim()
226
- : null,
227
- asset_symbol: mapEntry && typeof mapEntry.asset_symbol === "string" && mapEntry.asset_symbol.trim()
228
- ? mapEntry.asset_symbol.trim()
229
- : null,
230
- policy_scope: mapEntry && typeof mapEntry.policy_scope === "string" && mapEntry.policy_scope.trim()
231
- ? mapEntry.policy_scope.trim()
232
- : null,
233
- storage_class: mapEntry && typeof mapEntry.storage_class === "string" && mapEntry.storage_class.trim()
234
- ? mapEntry.storage_class.trim()
235
- : null
236
- };
237
- }
238
-
239
- module.exports = {
240
- buildConfiguredTreasuryDestination,
241
- loadPaymentConfig
242
- };
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const {
5
+ buildBsvTeranodeTreasuryDestination,
6
+ normalizeSettlementMode
7
+ } = require("./settlement_bsv_teranode");
8
+
9
+ function safeParseJson(text, fallback) {
10
+ try {
11
+ return JSON.parse(text);
12
+ } catch (_) {
13
+ return fallback;
14
+ }
15
+ }
16
+
17
+ function readJsonFromEnvOrPath(jsonValue, pathValue, fallback) {
18
+ if (typeof jsonValue === "string" && jsonValue.trim()) {
19
+ return safeParseJson(jsonValue, fallback);
20
+ }
21
+ if (typeof pathValue === "string" && pathValue.trim()) {
22
+ try {
23
+ return safeParseJson(fs.readFileSync(pathValue, "utf8"), fallback);
24
+ } catch (_) {
25
+ return fallback;
26
+ }
27
+ }
28
+ return fallback;
29
+ }
30
+
31
+ function loadWalletRegistry() {
32
+ const raw = readJsonFromEnvOrPath(
33
+ process.env.XYTARA_WALLET_REGISTRY_JSON,
34
+ process.env.XYTARA_WALLET_REGISTRY_PATH,
35
+ []
36
+ );
37
+ const entries = Array.isArray(raw)
38
+ ? raw
39
+ : Array.isArray(raw && raw.wallets) ? raw.wallets : [];
40
+ const byWalletId = new Map();
41
+ for (const entry of entries) {
42
+ if (!entry || typeof entry !== "object") continue;
43
+ const walletId = typeof entry.wallet_id === "string" ? entry.wallet_id.trim() : "";
44
+ const secret = typeof entry.secret === "string" ? entry.secret : "";
45
+ if (!walletId || !secret) continue;
46
+ byWalletId.set(walletId, {
47
+ wallet_id: walletId,
48
+ secret,
49
+ account_id: typeof entry.account_id === "string" && entry.account_id.trim() ? entry.account_id.trim() : null,
50
+ enabled: entry.enabled !== false
51
+ });
52
+ }
53
+ return byWalletId;
54
+ }
55
+
56
+ function loadTreasuryDestinationMap() {
57
+ const raw = readJsonFromEnvOrPath(
58
+ process.env.XYTARA_TREASURY_DESTINATIONS_JSON,
59
+ process.env.XYTARA_TREASURY_DESTINATIONS_PATH,
60
+ {}
61
+ );
62
+ const input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
63
+ const normalized = {};
64
+ for (const [settlementMode, entry] of Object.entries(input)) {
65
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
66
+ normalized[normalizeSettlementMode(settlementMode)] = {
67
+ destination_id: typeof entry.destination_id === "string" && entry.destination_id.trim()
68
+ ? entry.destination_id.trim()
69
+ : null,
70
+ destination_kind: typeof entry.destination_kind === "string" && entry.destination_kind.trim()
71
+ ? entry.destination_kind.trim()
72
+ : null,
73
+ account_id: typeof entry.account_id === "string" && entry.account_id.trim()
74
+ ? entry.account_id.trim()
75
+ : null,
76
+ external_ref: typeof entry.external_ref === "string" && entry.external_ref.trim()
77
+ ? entry.external_ref.trim()
78
+ : null,
79
+ custody_ref: typeof entry.custody_ref === "string" && entry.custody_ref.trim()
80
+ ? entry.custody_ref.trim()
81
+ : null,
82
+ reporting_ref: typeof entry.reporting_ref === "string" && entry.reporting_ref.trim()
83
+ ? entry.reporting_ref.trim()
84
+ : null,
85
+ wallet_ref: typeof entry.wallet_ref === "string" && entry.wallet_ref.trim()
86
+ ? entry.wallet_ref.trim()
87
+ : null,
88
+ provider_ref: typeof entry.provider_ref === "string" && entry.provider_ref.trim()
89
+ ? entry.provider_ref.trim()
90
+ : null,
91
+ network: typeof entry.network === "string" && entry.network.trim()
92
+ ? entry.network.trim()
93
+ : null,
94
+ chain_id: typeof entry.chain_id === "string" && entry.chain_id.trim()
95
+ ? entry.chain_id.trim()
96
+ : null,
97
+ asset_symbol: typeof entry.asset_symbol === "string" && entry.asset_symbol.trim()
98
+ ? entry.asset_symbol.trim()
99
+ : null,
100
+ policy_scope: typeof entry.policy_scope === "string" && entry.policy_scope.trim()
101
+ ? entry.policy_scope.trim()
102
+ : null,
103
+ storage_class: typeof entry.storage_class === "string" && entry.storage_class.trim()
104
+ ? entry.storage_class.trim()
105
+ : null,
106
+ enabled: entry.enabled !== false,
107
+ accept_inbound: entry.accept_inbound !== false
108
+ };
109
+ }
110
+ return normalized;
111
+ }
112
+
113
+ function loadPaymentConfig() {
114
+ // Default is fail-closed: signed payment acceptance. "presence" (accept any non-empty payment header)
115
+ // must now be opted into explicitly and is only appropriate for local development.
116
+ const verificationModeInput = String(process.env.XYTARA_PAYMENT_VERIFICATION_MODE || "local_signed").trim() || "local_signed";
117
+ const verificationMode = verificationModeInput === "signed" ? "local_signed" : verificationModeInput;
118
+ const maxAgeSeconds = Number(process.env.XYTARA_PAYMENT_MAX_AGE_SECONDS || 300);
119
+ return {
120
+ verification_mode: verificationMode,
121
+ verification_mode_input: verificationModeInput,
122
+ payment_max_age_seconds: Number.isFinite(maxAgeSeconds) && maxAgeSeconds > 0 ? maxAgeSeconds : 300,
123
+ wallet_registry: loadWalletRegistry(),
124
+ x402: loadX402FacilitatorConfig(),
125
+ treasury_destination_id: String(process.env.XYTARA_TREASURY_DESTINATION_ID || "").trim() || null,
126
+ treasury_destination_kind: String(process.env.XYTARA_TREASURY_DESTINATION_KIND || "").trim() || null,
127
+ treasury_account_id: String(process.env.XYTARA_TREASURY_ACCOUNT_ID || "").trim() || null,
128
+ treasury_external_ref: String(process.env.XYTARA_TREASURY_EXTERNAL_REF || "").trim() || null,
129
+ treasury_destinations_by_settlement_mode: loadTreasuryDestinationMap()
130
+ };
131
+ }
132
+
133
+ // Configuration for the x402_facilitator verification mode: the on-chain, trustless
134
+ // pay-per-call path. The advertised accepts tell a buyer how to pay; the facilitator
135
+ // verifies and settles the signed payment before any work runs.
136
+ function loadX402FacilitatorConfig() {
137
+ let accepts = [];
138
+ const rawAccepts = String(process.env.XYTARA_X402_ACCEPTS_JSON || "").trim();
139
+ if (rawAccepts) {
140
+ try {
141
+ const parsed = JSON.parse(rawAccepts);
142
+ if (Array.isArray(parsed)) {
143
+ accepts = parsed
144
+ .map((entry) => ({
145
+ scheme: String((entry && entry.scheme) || "exact").trim(),
146
+ network: String((entry && entry.network) || "").trim(),
147
+ asset: String((entry && entry.asset) || "").trim(),
148
+ amount: String((entry && entry.amount) || "").trim(),
149
+ payTo: String((entry && entry.payTo) || "").trim(),
150
+ ...(entry && entry.extra && typeof entry.extra === "object" ? { extra: entry.extra } : {}),
151
+ ...(Number(entry && entry.maxTimeoutSeconds) > 0 ? { maxTimeoutSeconds: Number(entry.maxTimeoutSeconds) } : {})
152
+ }))
153
+ .filter((entry) => entry.network && entry.asset && entry.amount && entry.payTo);
154
+ }
155
+ } catch (_) {
156
+ accepts = [];
157
+ }
158
+ }
159
+ let headers = {};
160
+ const rawHeaders = String(process.env.XYTARA_X402_FACILITATOR_HEADERS_JSON || "").trim();
161
+ if (rawHeaders) {
162
+ try {
163
+ const parsed = JSON.parse(rawHeaders);
164
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) headers = parsed;
165
+ } catch (_) {
166
+ headers = {};
167
+ }
168
+ }
169
+ return {
170
+ accepts,
171
+ facilitator_url: String(process.env.XYTARA_X402_FACILITATOR_URL || "https://x402.org/facilitator").trim().replace(/\/$/, ""),
172
+ facilitator_headers: headers,
173
+ facilitator_timeout_ms: Math.max(1000, Number(process.env.XYTARA_X402_FACILITATOR_TIMEOUT_MS || 15000) || 15000),
174
+ settle: String(process.env.XYTARA_X402_SETTLE || "true").trim().toLowerCase() !== "false",
175
+ // Credit units a single self-serve x402 top-up grants, unless the matched accepts
176
+ // entry carries its own `extra.credits`.
177
+ topup_units_default: Math.max(0, Number(process.env.XYTARA_X402_TOPUP_UNITS || 1000) || 0),
178
+ // Optional Coinbase CDP auth for the CDP-hosted facilitator (Base mainnet). When both a
179
+ // key id and secret are present, the facilitator client mints a fresh EdDSA Bearer JWT
180
+ // per request. Accepts the CDP-standard env names or XYTARA_X402_CDP_* overrides.
181
+ cdp: loadCdpFacilitatorAuth()
182
+ };
183
+ }
184
+
185
+ function loadCdpFacilitatorAuth() {
186
+ const keyId = String(process.env.XYTARA_X402_CDP_KEY_ID || process.env.CDP_API_KEY_ID || "").trim();
187
+ const keySecret = String(process.env.XYTARA_X402_CDP_KEY_SECRET || process.env.CDP_API_KEY_SECRET || "").trim();
188
+ if (!keyId || !keySecret) return null;
189
+ return { key_id: keyId, key_secret: keySecret };
190
+ }
191
+
192
+ function buildConfiguredTreasuryDestination(accountId, settlementMode) {
193
+ const config = loadPaymentConfig();
194
+ const normalizedSettlementMode = normalizeSettlementMode(settlementMode);
195
+ const mapEntry = normalizedSettlementMode && config.treasury_destinations_by_settlement_mode[normalizedSettlementMode]
196
+ ? config.treasury_destinations_by_settlement_mode[normalizedSettlementMode]
197
+ : null;
198
+ const destinationId = mapEntry && typeof mapEntry.destination_id === "string" && mapEntry.destination_id.trim()
199
+ ? mapEntry.destination_id.trim()
200
+ : config.treasury_destination_id;
201
+ if (!destinationId) {
202
+ return buildBsvTeranodeTreasuryDestination(accountId, normalizedSettlementMode);
203
+ }
204
+ return {
205
+ destination_id: destinationId,
206
+ destination_kind:
207
+ (mapEntry && typeof mapEntry.destination_kind === "string" && mapEntry.destination_kind.trim())
208
+ || config.treasury_destination_kind
209
+ || "merchant_treasury",
210
+ account_id:
211
+ (mapEntry && typeof mapEntry.account_id === "string" && mapEntry.account_id.trim())
212
+ || config.treasury_account_id
213
+ || accountId
214
+ || null,
215
+ settlement_mode: normalizedSettlementMode || null,
216
+ external_ref:
217
+ (mapEntry && typeof mapEntry.external_ref === "string" && mapEntry.external_ref.trim())
218
+ || config.treasury_external_ref
219
+ || null,
220
+ custody_ref: mapEntry && typeof mapEntry.custody_ref === "string" && mapEntry.custody_ref.trim()
221
+ ? mapEntry.custody_ref.trim()
222
+ : null,
223
+ reporting_ref: mapEntry && typeof mapEntry.reporting_ref === "string" && mapEntry.reporting_ref.trim()
224
+ ? mapEntry.reporting_ref.trim()
225
+ : null,
226
+ wallet_ref: mapEntry && typeof mapEntry.wallet_ref === "string" && mapEntry.wallet_ref.trim()
227
+ ? mapEntry.wallet_ref.trim()
228
+ : null,
229
+ provider_ref: mapEntry && typeof mapEntry.provider_ref === "string" && mapEntry.provider_ref.trim()
230
+ ? mapEntry.provider_ref.trim()
231
+ : null,
232
+ network: mapEntry && typeof mapEntry.network === "string" && mapEntry.network.trim()
233
+ ? mapEntry.network.trim()
234
+ : null,
235
+ chain_id: mapEntry && typeof mapEntry.chain_id === "string" && mapEntry.chain_id.trim()
236
+ ? mapEntry.chain_id.trim()
237
+ : null,
238
+ asset_symbol: mapEntry && typeof mapEntry.asset_symbol === "string" && mapEntry.asset_symbol.trim()
239
+ ? mapEntry.asset_symbol.trim()
240
+ : null,
241
+ policy_scope: mapEntry && typeof mapEntry.policy_scope === "string" && mapEntry.policy_scope.trim()
242
+ ? mapEntry.policy_scope.trim()
243
+ : null,
244
+ storage_class: mapEntry && typeof mapEntry.storage_class === "string" && mapEntry.storage_class.trim()
245
+ ? mapEntry.storage_class.trim()
246
+ : null
247
+ };
248
+ }
249
+
250
+ module.exports = {
251
+ buildConfiguredTreasuryDestination,
252
+ loadPaymentConfig
253
+ };