xytara 3.1.4 → 3.1.5

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,11 @@
1
- `xytara` 3.1.4 makes the self-serve credits capability discoverable and documents the path to Base mainnet.
1
+ `xytara` 3.1.5 adds native Coinbase CDP facilitator support, unlocking Base mainnet settlement.
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 x402_facilitator mode now authenticates to facilitators that require a short-lived CDP Bearer token, so the runtime can settle real USDC on Base mainnet through the CDP-hosted facilitator, not just test USDC on Base Sepolia via the default facilitator.
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
+ - the facilitator client mints a fresh EdDSA (Ed25519) CDP Bearer JWT per `/verify` and `/settle` request, with the `uri` claim bound to each request and a 120-second expiry, because CDP rejects static tokens
8
+ - enable it by setting `CDP_API_KEY_ID` and `CDP_API_KEY_SECRET` (or `XYTARA_X402_CDP_KEY_ID` / `XYTARA_X402_CDP_KEY_SECRET`) and pointing `XYTARA_X402_FACILITATOR_URL` at the CDP facilitator; the Ed25519 secret is the base64 64-byte key CDP issues
9
+ - zero new dependencies: the JWT is signed with Node's built-in Ed25519
10
+ - when no CDP key is configured, behavior is unchanged: static-header facilitators and the default Base Sepolia path work exactly as before
11
+ - adds `npm run verify:cdp-facilitator`, which checks the per-request token minting, claim shape, and signature against a mock facilitator, and documents the Base mainnet setup in `REAL_PAYMENT_SETUP.md`
@@ -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_18742578ec28",
10
+ "receipt_id": "rcpt_fa3b4219d3c4",
11
+ "proof_ref": "proof_rcpt_fa3b4219d3c4",
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
+ };