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,128 +1,128 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
// Verifies the BSV/UTXO (BOLT) adapter (lib/bolt_adapter.js). Offline: the chain
|
|
4
|
-
// fetch is injected as a mock, so no network. Exercises config gating, header
|
|
5
|
-
// parsing, and the on-chain amount/address verification + confirmations logic.
|
|
6
|
-
|
|
7
|
-
const assert = require("assert");
|
|
8
|
-
const m = require("../lib/bolt_adapter");
|
|
9
|
-
|
|
10
|
-
function setEnv(env) {
|
|
11
|
-
const saved = {};
|
|
12
|
-
for (const k in env) {
|
|
13
|
-
saved[k] = process.env[k];
|
|
14
|
-
if (env[k] === undefined) delete process.env[k];
|
|
15
|
-
else process.env[k] = env[k];
|
|
16
|
-
}
|
|
17
|
-
return () => {
|
|
18
|
-
for (const k in saved) {
|
|
19
|
-
if (saved[k] === undefined) delete process.env[k];
|
|
20
|
-
else process.env[k] = saved[k];
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const PAYTO = "1NaxytraPayToAddressExampleXXXXXXXXXX";
|
|
26
|
-
const TXID = "a".repeat(64);
|
|
27
|
-
|
|
28
|
-
function mockTx(voutValueBsv, address, confirmations) {
|
|
29
|
-
return async () => ({ status: 200, json: { confirmations, vout: [{ value: voutValueBsv, scriptPubKey: { addresses: [address] } }] } });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function main() {
|
|
33
|
-
// 1. Not configured.
|
|
34
|
-
let restore = setEnv({ XYTARA_BOLT_PROVIDER: undefined });
|
|
35
|
-
let cfg = m.loadBoltConfig();
|
|
36
|
-
assert.strictEqual(cfg.provider, null);
|
|
37
|
-
assert.strictEqual(m.isConfigured(cfg), false);
|
|
38
|
-
assert.strictEqual((await m.verifyBsvPayment(cfg, TXID)).reason, "bolt_provider_not_configured");
|
|
39
|
-
restore();
|
|
40
|
-
|
|
41
|
-
// 2. Configured (whatsonchain).
|
|
42
|
-
restore = setEnv({
|
|
43
|
-
XYTARA_BOLT_PROVIDER: "whatsonchain",
|
|
44
|
-
XYTARA_BOLT_PAYTO_ADDRESS: PAYTO,
|
|
45
|
-
XYTARA_BOLT_PRICE_SATS: "1000",
|
|
46
|
-
XYTARA_BOLT_MIN_CONFIRMATIONS: "1",
|
|
47
|
-
XYTARA_BOLT_NETWORK: "main"
|
|
48
|
-
});
|
|
49
|
-
cfg = m.loadBoltConfig();
|
|
50
|
-
assert.strictEqual(cfg.provider, "whatsonchain");
|
|
51
|
-
assert.ok(cfg.apiUrl.includes("whatsonchain.com"));
|
|
52
|
-
|
|
53
|
-
// Header parsing.
|
|
54
|
-
assert.deepStrictEqual(m.parseBoltHeader(`BOLT ${TXID}`), { txid: TXID });
|
|
55
|
-
assert.strictEqual(m.parseBoltHeader("BOLT not-a-txid"), null);
|
|
56
|
-
assert.strictEqual(m.parseBoltHeader("Bearer x"), null);
|
|
57
|
-
|
|
58
|
-
// Challenge shape.
|
|
59
|
-
const ch = m.buildBoltChallenge(cfg);
|
|
60
|
-
assert.strictEqual(ch.address, PAYTO);
|
|
61
|
-
assert.strictEqual(ch.amount_sats, 1000);
|
|
62
|
-
|
|
63
|
-
// Invalid txid -> rejected before any fetch.
|
|
64
|
-
assert.strictEqual((await m.verifyBsvPayment(cfg, "xyz")).reason, "bolt_txid_invalid");
|
|
65
|
-
|
|
66
|
-
// Sufficient payment (0.00002 BSV = 2000 sats to PAYTO, 3 confs) -> ok.
|
|
67
|
-
let r = await m.verifyBsvPayment(cfg, TXID, mockTx(0.00002, PAYTO, 3));
|
|
68
|
-
assert.strictEqual(r.ok, true);
|
|
69
|
-
assert.strictEqual(r.paid_sats, 2000);
|
|
70
|
-
assert.strictEqual(r.confirmations, 3);
|
|
71
|
-
|
|
72
|
-
// --- Per-quote binding: unique exact amount ---
|
|
73
|
-
const reqSats = m.computeRequiredSats(cfg, { quote_id: "quote_x" });
|
|
74
|
-
assert.ok(reqSats >= 1000 && reqSats < 1000 + cfg.bindingWindowSats, "required = price + bounded offset");
|
|
75
|
-
assert.strictEqual(reqSats, m.computeRequiredSats(cfg, { quote_id: "quote_x" }), "deterministic");
|
|
76
|
-
// Paying the EXACT bound amount -> ok.
|
|
77
|
-
r = await m.verifyBsvPayment(cfg, TXID, mockTx(reqSats / 1e8, PAYTO, 3), { requiredSats: reqSats });
|
|
78
|
-
assert.strictEqual(r.ok, true);
|
|
79
|
-
assert.strictEqual(r.paid_sats, reqSats);
|
|
80
|
-
assert.strictEqual(r.required_sats, reqSats);
|
|
81
|
-
// Paying a DIFFERENT amount (even to the right address) -> mismatch, so an
|
|
82
|
-
// unrelated payment cannot be reused to satisfy this quote.
|
|
83
|
-
r = await m.verifyBsvPayment(cfg, TXID, mockTx((reqSats + 1) / 1e8, PAYTO, 3), { requiredSats: reqSats });
|
|
84
|
-
assert.strictEqual(r.reason, "bolt_amount_mismatch");
|
|
85
|
-
// Challenge advertises the exact amount.
|
|
86
|
-
const boundChallenge = m.buildBoltChallenge(cfg, { requiredSats: reqSats });
|
|
87
|
-
assert.strictEqual(boundChallenge.amount_sats, reqSats);
|
|
88
|
-
assert.strictEqual(boundChallenge.exact, true);
|
|
89
|
-
|
|
90
|
-
// Insufficient amount (500 sats < 1000) -> rejected.
|
|
91
|
-
r = await m.verifyBsvPayment(cfg, TXID, mockTx(0.000005, PAYTO, 3));
|
|
92
|
-
assert.strictEqual(r.ok, false);
|
|
93
|
-
assert.strictEqual(r.reason, "bolt_amount_insufficient");
|
|
94
|
-
|
|
95
|
-
// Paid to the wrong address -> counts as zero -> insufficient.
|
|
96
|
-
r = await m.verifyBsvPayment(cfg, TXID, mockTx(0.001, "1SomeOtherAddressXXXXXXXXXXXXXXXXXXX", 3));
|
|
97
|
-
assert.strictEqual(r.reason, "bolt_amount_insufficient");
|
|
98
|
-
|
|
99
|
-
// Too few confirmations -> rejected.
|
|
100
|
-
r = await m.verifyBsvPayment(cfg, TXID, mockTx(0.00002, PAYTO, 0));
|
|
101
|
-
assert.strictEqual(r.reason, "bolt_insufficient_confirmations");
|
|
102
|
-
|
|
103
|
-
// Default min_confirmations is 1 (not 0) when unset -> mempool (0-conf) rejected.
|
|
104
|
-
const dflt = setEnv({ XYTARA_BOLT_PROVIDER: "whatsonchain", XYTARA_BOLT_PAYTO_ADDRESS: PAYTO, XYTARA_BOLT_PRICE_SATS: "1000", XYTARA_BOLT_MIN_CONFIRMATIONS: undefined });
|
|
105
|
-
const dcfg = m.loadBoltConfig();
|
|
106
|
-
assert.strictEqual(dcfg.minConfirmations, 1);
|
|
107
|
-
r = await m.verifyBsvPayment(dcfg, TXID, mockTx(0.00002, PAYTO, 0));
|
|
108
|
-
assert.strictEqual(r.reason, "bolt_insufficient_confirmations");
|
|
109
|
-
dflt();
|
|
110
|
-
|
|
111
|
-
// Tx not found (404) -> rejected.
|
|
112
|
-
r = await m.verifyBsvPayment(cfg, TXID, async () => ({ status: 404, json: null }));
|
|
113
|
-
assert.strictEqual(r.reason, "bolt_tx_not_found");
|
|
114
|
-
restore();
|
|
115
|
-
|
|
116
|
-
// 3. Configured but missing payto/price -> explicit reason (no fetch).
|
|
117
|
-
restore = setEnv({ XYTARA_BOLT_PROVIDER: "whatsonchain", XYTARA_BOLT_PAYTO_ADDRESS: undefined, XYTARA_BOLT_PRICE_SATS: undefined });
|
|
118
|
-
cfg = m.loadBoltConfig();
|
|
119
|
-
assert.strictEqual((await m.verifyBsvPayment(cfg, TXID)).reason, "bolt_payto_or_price_missing");
|
|
120
|
-
restore();
|
|
121
|
-
|
|
122
|
-
console.log("verify_bolt_adapter: all BSV/UTXO (BOLT) adapter checks passed");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
main().catch((error) => {
|
|
126
|
-
console.error("verify_bolt_adapter failed:", error && error.message ? error.message : error);
|
|
127
|
-
process.exitCode = 1;
|
|
128
|
-
});
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Verifies the BSV/UTXO (BOLT) adapter (lib/bolt_adapter.js). Offline: the chain
|
|
4
|
+
// fetch is injected as a mock, so no network. Exercises config gating, header
|
|
5
|
+
// parsing, and the on-chain amount/address verification + confirmations logic.
|
|
6
|
+
|
|
7
|
+
const assert = require("assert");
|
|
8
|
+
const m = require("../lib/bolt_adapter");
|
|
9
|
+
|
|
10
|
+
function setEnv(env) {
|
|
11
|
+
const saved = {};
|
|
12
|
+
for (const k in env) {
|
|
13
|
+
saved[k] = process.env[k];
|
|
14
|
+
if (env[k] === undefined) delete process.env[k];
|
|
15
|
+
else process.env[k] = env[k];
|
|
16
|
+
}
|
|
17
|
+
return () => {
|
|
18
|
+
for (const k in saved) {
|
|
19
|
+
if (saved[k] === undefined) delete process.env[k];
|
|
20
|
+
else process.env[k] = saved[k];
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const PAYTO = "1NaxytraPayToAddressExampleXXXXXXXXXX";
|
|
26
|
+
const TXID = "a".repeat(64);
|
|
27
|
+
|
|
28
|
+
function mockTx(voutValueBsv, address, confirmations) {
|
|
29
|
+
return async () => ({ status: 200, json: { confirmations, vout: [{ value: voutValueBsv, scriptPubKey: { addresses: [address] } }] } });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function main() {
|
|
33
|
+
// 1. Not configured.
|
|
34
|
+
let restore = setEnv({ XYTARA_BOLT_PROVIDER: undefined });
|
|
35
|
+
let cfg = m.loadBoltConfig();
|
|
36
|
+
assert.strictEqual(cfg.provider, null);
|
|
37
|
+
assert.strictEqual(m.isConfigured(cfg), false);
|
|
38
|
+
assert.strictEqual((await m.verifyBsvPayment(cfg, TXID)).reason, "bolt_provider_not_configured");
|
|
39
|
+
restore();
|
|
40
|
+
|
|
41
|
+
// 2. Configured (whatsonchain).
|
|
42
|
+
restore = setEnv({
|
|
43
|
+
XYTARA_BOLT_PROVIDER: "whatsonchain",
|
|
44
|
+
XYTARA_BOLT_PAYTO_ADDRESS: PAYTO,
|
|
45
|
+
XYTARA_BOLT_PRICE_SATS: "1000",
|
|
46
|
+
XYTARA_BOLT_MIN_CONFIRMATIONS: "1",
|
|
47
|
+
XYTARA_BOLT_NETWORK: "main"
|
|
48
|
+
});
|
|
49
|
+
cfg = m.loadBoltConfig();
|
|
50
|
+
assert.strictEqual(cfg.provider, "whatsonchain");
|
|
51
|
+
assert.ok(cfg.apiUrl.includes("whatsonchain.com"));
|
|
52
|
+
|
|
53
|
+
// Header parsing.
|
|
54
|
+
assert.deepStrictEqual(m.parseBoltHeader(`BOLT ${TXID}`), { txid: TXID });
|
|
55
|
+
assert.strictEqual(m.parseBoltHeader("BOLT not-a-txid"), null);
|
|
56
|
+
assert.strictEqual(m.parseBoltHeader("Bearer x"), null);
|
|
57
|
+
|
|
58
|
+
// Challenge shape.
|
|
59
|
+
const ch = m.buildBoltChallenge(cfg);
|
|
60
|
+
assert.strictEqual(ch.address, PAYTO);
|
|
61
|
+
assert.strictEqual(ch.amount_sats, 1000);
|
|
62
|
+
|
|
63
|
+
// Invalid txid -> rejected before any fetch.
|
|
64
|
+
assert.strictEqual((await m.verifyBsvPayment(cfg, "xyz")).reason, "bolt_txid_invalid");
|
|
65
|
+
|
|
66
|
+
// Sufficient payment (0.00002 BSV = 2000 sats to PAYTO, 3 confs) -> ok.
|
|
67
|
+
let r = await m.verifyBsvPayment(cfg, TXID, mockTx(0.00002, PAYTO, 3));
|
|
68
|
+
assert.strictEqual(r.ok, true);
|
|
69
|
+
assert.strictEqual(r.paid_sats, 2000);
|
|
70
|
+
assert.strictEqual(r.confirmations, 3);
|
|
71
|
+
|
|
72
|
+
// --- Per-quote binding: unique exact amount ---
|
|
73
|
+
const reqSats = m.computeRequiredSats(cfg, { quote_id: "quote_x" });
|
|
74
|
+
assert.ok(reqSats >= 1000 && reqSats < 1000 + cfg.bindingWindowSats, "required = price + bounded offset");
|
|
75
|
+
assert.strictEqual(reqSats, m.computeRequiredSats(cfg, { quote_id: "quote_x" }), "deterministic");
|
|
76
|
+
// Paying the EXACT bound amount -> ok.
|
|
77
|
+
r = await m.verifyBsvPayment(cfg, TXID, mockTx(reqSats / 1e8, PAYTO, 3), { requiredSats: reqSats });
|
|
78
|
+
assert.strictEqual(r.ok, true);
|
|
79
|
+
assert.strictEqual(r.paid_sats, reqSats);
|
|
80
|
+
assert.strictEqual(r.required_sats, reqSats);
|
|
81
|
+
// Paying a DIFFERENT amount (even to the right address) -> mismatch, so an
|
|
82
|
+
// unrelated payment cannot be reused to satisfy this quote.
|
|
83
|
+
r = await m.verifyBsvPayment(cfg, TXID, mockTx((reqSats + 1) / 1e8, PAYTO, 3), { requiredSats: reqSats });
|
|
84
|
+
assert.strictEqual(r.reason, "bolt_amount_mismatch");
|
|
85
|
+
// Challenge advertises the exact amount.
|
|
86
|
+
const boundChallenge = m.buildBoltChallenge(cfg, { requiredSats: reqSats });
|
|
87
|
+
assert.strictEqual(boundChallenge.amount_sats, reqSats);
|
|
88
|
+
assert.strictEqual(boundChallenge.exact, true);
|
|
89
|
+
|
|
90
|
+
// Insufficient amount (500 sats < 1000) -> rejected.
|
|
91
|
+
r = await m.verifyBsvPayment(cfg, TXID, mockTx(0.000005, PAYTO, 3));
|
|
92
|
+
assert.strictEqual(r.ok, false);
|
|
93
|
+
assert.strictEqual(r.reason, "bolt_amount_insufficient");
|
|
94
|
+
|
|
95
|
+
// Paid to the wrong address -> counts as zero -> insufficient.
|
|
96
|
+
r = await m.verifyBsvPayment(cfg, TXID, mockTx(0.001, "1SomeOtherAddressXXXXXXXXXXXXXXXXXXX", 3));
|
|
97
|
+
assert.strictEqual(r.reason, "bolt_amount_insufficient");
|
|
98
|
+
|
|
99
|
+
// Too few confirmations -> rejected.
|
|
100
|
+
r = await m.verifyBsvPayment(cfg, TXID, mockTx(0.00002, PAYTO, 0));
|
|
101
|
+
assert.strictEqual(r.reason, "bolt_insufficient_confirmations");
|
|
102
|
+
|
|
103
|
+
// Default min_confirmations is 1 (not 0) when unset -> mempool (0-conf) rejected.
|
|
104
|
+
const dflt = setEnv({ XYTARA_BOLT_PROVIDER: "whatsonchain", XYTARA_BOLT_PAYTO_ADDRESS: PAYTO, XYTARA_BOLT_PRICE_SATS: "1000", XYTARA_BOLT_MIN_CONFIRMATIONS: undefined });
|
|
105
|
+
const dcfg = m.loadBoltConfig();
|
|
106
|
+
assert.strictEqual(dcfg.minConfirmations, 1);
|
|
107
|
+
r = await m.verifyBsvPayment(dcfg, TXID, mockTx(0.00002, PAYTO, 0));
|
|
108
|
+
assert.strictEqual(r.reason, "bolt_insufficient_confirmations");
|
|
109
|
+
dflt();
|
|
110
|
+
|
|
111
|
+
// Tx not found (404) -> rejected.
|
|
112
|
+
r = await m.verifyBsvPayment(cfg, TXID, async () => ({ status: 404, json: null }));
|
|
113
|
+
assert.strictEqual(r.reason, "bolt_tx_not_found");
|
|
114
|
+
restore();
|
|
115
|
+
|
|
116
|
+
// 3. Configured but missing payto/price -> explicit reason (no fetch).
|
|
117
|
+
restore = setEnv({ XYTARA_BOLT_PROVIDER: "whatsonchain", XYTARA_BOLT_PAYTO_ADDRESS: undefined, XYTARA_BOLT_PRICE_SATS: undefined });
|
|
118
|
+
cfg = m.loadBoltConfig();
|
|
119
|
+
assert.strictEqual((await m.verifyBsvPayment(cfg, TXID)).reason, "bolt_payto_or_price_missing");
|
|
120
|
+
restore();
|
|
121
|
+
|
|
122
|
+
console.log("verify_bolt_adapter: all BSV/UTXO (BOLT) adapter checks passed");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
main().catch((error) => {
|
|
126
|
+
console.error("verify_bolt_adapter failed:", error && error.message ? error.message : error);
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
});
|
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
// Verifies the Coinbase CDP auth path for the x402 facilitator client: when a CDP Secret
|
|
4
|
-
// API Key is configured, the client must mint a fresh, valid EdDSA Bearer JWT per request
|
|
5
|
-
// (CDP rejects static tokens). A mock facilitator stands in for CDP and cryptographically
|
|
6
|
-
// verifies the token's signature and claims before accepting the call. No real network/chain.
|
|
7
|
-
|
|
8
|
-
const assert = require("assert");
|
|
9
|
-
const http = require("http");
|
|
10
|
-
const crypto = require("crypto");
|
|
11
|
-
|
|
12
|
-
const { createFacilitatorClient } = require("../lib/x402_facilitator");
|
|
13
|
-
const { loadPaymentConfig } = require("../lib/payment_config");
|
|
14
|
-
|
|
15
|
-
// A throwaway Ed25519 key, exported as the base64 64-byte (seed||public) secret CDP uses.
|
|
16
|
-
function makeCdpKey() {
|
|
17
|
-
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
18
|
-
const seed = privateKey.export({ format: "der", type: "pkcs8" }).subarray(16); // last 32 bytes
|
|
19
|
-
const pub = publicKey.export({ format: "der", type: "spki" }).subarray(12); // last 32 bytes
|
|
20
|
-
return { publicKey, keySecret: Buffer.concat([seed, pub]).toString("base64") };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function startCdpMock(expectedPublicKey, seen) {
|
|
24
|
-
const server = http.createServer((req, res) => {
|
|
25
|
-
const chunks = [];
|
|
26
|
-
req.on("data", (c) => chunks.push(c));
|
|
27
|
-
req.on("end", () => {
|
|
28
|
-
const reply = (code, payload) => {
|
|
29
|
-
const text = JSON.stringify(payload);
|
|
30
|
-
res.writeHead(code, { "content-type": "application/json", "content-length": Buffer.byteLength(text) });
|
|
31
|
-
res.end(text);
|
|
32
|
-
};
|
|
33
|
-
const auth = String((req.headers.authorization || ""));
|
|
34
|
-
if (!auth.startsWith("Bearer ")) return reply(401, { error: "missing_bearer" });
|
|
35
|
-
const token = auth.slice("Bearer ".length);
|
|
36
|
-
const parts = token.split(".");
|
|
37
|
-
if (parts.length !== 3) return reply(401, { error: "malformed_jwt" });
|
|
38
|
-
const header = JSON.parse(Buffer.from(parts[0], "base64").toString("utf8"));
|
|
39
|
-
const claims = JSON.parse(Buffer.from(parts[1], "base64").toString("utf8"));
|
|
40
|
-
const sig = Buffer.from(parts[2].replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
41
|
-
const signatureValid = crypto.verify(null, Buffer.from(`${parts[0]}.${parts[1]}`), expectedPublicKey, sig);
|
|
42
|
-
// Record what the facilitator observed so the test can assert on it.
|
|
43
|
-
seen.push({ header, claims, signatureValid, path: req.url, nonce: header.nonce });
|
|
44
|
-
if (header.alg !== "EdDSA") return reply(401, { error: "bad_alg" });
|
|
45
|
-
if (!signatureValid) return reply(401, { error: "bad_signature" });
|
|
46
|
-
if (claims.iss !== "cdp") return reply(401, { error: "bad_iss" });
|
|
47
|
-
if (!Array.isArray(claims.aud) || claims.aud[0] !== "cdp_service") return reply(401, { error: "bad_aud" });
|
|
48
|
-
const now = Math.floor(Date.now() / 1000);
|
|
49
|
-
if (!(claims.exp > now && claims.exp - claims.nbf <= 120)) return reply(401, { error: "bad_exp" });
|
|
50
|
-
if (!(claims.uri.startsWith("POST 127.0.0.1:") && claims.uri.endsWith(req.url))) return reply(401, { error: "bad_uri", uri: claims.uri });
|
|
51
|
-
if (req.url === "/verify") return reply(200, { isValid: true, payer: "0x2222222222222222222222222222222222222222" });
|
|
52
|
-
if (req.url === "/settle") return reply(200, { success: true, transaction: "0xcdpmocktx", network: "eip155:8453" });
|
|
53
|
-
return reply(404, { error: "not_found" });
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
return new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve({ server, port: server.address().port })));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const PAYLOAD = { x402Version: 2, accepted: { scheme: "exact" }, payload: { signature: "ok", authorization: {} } };
|
|
60
|
-
const REQS = { scheme: "exact", network: "eip155:8453", asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", amount: "10000", payTo: "0xabc", resource: "https://x/y" };
|
|
61
|
-
|
|
62
|
-
async function main() {
|
|
63
|
-
const { publicKey, keySecret } = makeCdpKey();
|
|
64
|
-
const seen = [];
|
|
65
|
-
const mock = await startCdpMock(publicKey, seen);
|
|
66
|
-
const baseUrl = `http://127.0.0.1:${mock.port}`;
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
// 1. Config loader picks up the CDP env into config.x402.cdp.
|
|
70
|
-
const prev = { id: process.env.XYTARA_X402_CDP_KEY_ID, sec: process.env.XYTARA_X402_CDP_KEY_SECRET };
|
|
71
|
-
process.env.XYTARA_X402_CDP_KEY_ID = "11111111-2222-3333-4444-555555555555";
|
|
72
|
-
process.env.XYTARA_X402_CDP_KEY_SECRET = keySecret;
|
|
73
|
-
const cfg = loadPaymentConfig();
|
|
74
|
-
assert.ok(cfg.x402.cdp && cfg.x402.cdp.key_id === "11111111-2222-3333-4444-555555555555", "config must load CDP key id");
|
|
75
|
-
assert.strictEqual(cfg.x402.cdp.key_secret, keySecret, "config must load CDP key secret");
|
|
76
|
-
|
|
77
|
-
// 2. The client attaches a valid per-request Bearer that the mock CDP accepts.
|
|
78
|
-
const client = createFacilitatorClient({ url: baseUrl, cdp: cfg.x402.cdp, timeoutMs: 5000 });
|
|
79
|
-
const verify = await client.verify(PAYLOAD, REQS);
|
|
80
|
-
assert.strictEqual(verify.ok, true, "verify must succeed with a valid CDP Bearer");
|
|
81
|
-
assert.strictEqual(verify.json.isValid, true, "mock CDP must accept the token on /verify");
|
|
82
|
-
const settle = await client.settle(PAYLOAD, REQS);
|
|
83
|
-
assert.strictEqual(settle.ok, true, "settle must succeed with a valid CDP Bearer");
|
|
84
|
-
assert.strictEqual(settle.json.transaction, "0xcdpmocktx", "settle must return the mock tx");
|
|
85
|
-
|
|
86
|
-
// 3. Each request minted its own token (fresh nonce), signature valid, uri bound to the path.
|
|
87
|
-
assert.strictEqual(seen.length, 2, "mock must have seen two authenticated calls");
|
|
88
|
-
assert.ok(seen.every((e) => e.signatureValid), "every token signature must verify");
|
|
89
|
-
assert.notStrictEqual(seen[0].nonce, seen[1].nonce, "each request must mint a fresh token (distinct nonce)");
|
|
90
|
-
assert.ok(seen[0].claims.uri.endsWith("/verify") && seen[1].claims.uri.endsWith("/settle"), "uri claim must bind to each request path");
|
|
91
|
-
|
|
92
|
-
// 4. Without CDP creds, no Authorization header is sent (static-facilitator path unaffected).
|
|
93
|
-
const seen2 = [];
|
|
94
|
-
const mock2 = await startCdpMock(publicKey, seen2);
|
|
95
|
-
const plainClient = createFacilitatorClient({ url: `http://127.0.0.1:${mock2.port}`, timeoutMs: 5000 });
|
|
96
|
-
const plain = await plainClient.verify(PAYLOAD, REQS);
|
|
97
|
-
assert.strictEqual(plain.status, 401, "without CDP creds the mock sees no Bearer and rejects");
|
|
98
|
-
assert.strictEqual(seen2.length, 0, "no JWT should be minted when CDP is not configured");
|
|
99
|
-
await new Promise((r) => mock2.server.close(r));
|
|
100
|
-
|
|
101
|
-
if (prev.id === undefined) delete process.env.XYTARA_X402_CDP_KEY_ID; else process.env.XYTARA_X402_CDP_KEY_ID = prev.id;
|
|
102
|
-
if (prev.sec === undefined) delete process.env.XYTARA_X402_CDP_KEY_SECRET; else process.env.XYTARA_X402_CDP_KEY_SECRET = prev.sec;
|
|
103
|
-
|
|
104
|
-
console.log("xytara verify:cdp-facilitator passed");
|
|
105
|
-
} finally {
|
|
106
|
-
await new Promise((r) => mock.server.close(r));
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
main().catch((e) => { console.error(e && e.message ? e.message : e); process.exitCode = 1; });
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Verifies the Coinbase CDP auth path for the x402 facilitator client: when a CDP Secret
|
|
4
|
+
// API Key is configured, the client must mint a fresh, valid EdDSA Bearer JWT per request
|
|
5
|
+
// (CDP rejects static tokens). A mock facilitator stands in for CDP and cryptographically
|
|
6
|
+
// verifies the token's signature and claims before accepting the call. No real network/chain.
|
|
7
|
+
|
|
8
|
+
const assert = require("assert");
|
|
9
|
+
const http = require("http");
|
|
10
|
+
const crypto = require("crypto");
|
|
11
|
+
|
|
12
|
+
const { createFacilitatorClient } = require("../lib/x402_facilitator");
|
|
13
|
+
const { loadPaymentConfig } = require("../lib/payment_config");
|
|
14
|
+
|
|
15
|
+
// A throwaway Ed25519 key, exported as the base64 64-byte (seed||public) secret CDP uses.
|
|
16
|
+
function makeCdpKey() {
|
|
17
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
18
|
+
const seed = privateKey.export({ format: "der", type: "pkcs8" }).subarray(16); // last 32 bytes
|
|
19
|
+
const pub = publicKey.export({ format: "der", type: "spki" }).subarray(12); // last 32 bytes
|
|
20
|
+
return { publicKey, keySecret: Buffer.concat([seed, pub]).toString("base64") };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function startCdpMock(expectedPublicKey, seen) {
|
|
24
|
+
const server = http.createServer((req, res) => {
|
|
25
|
+
const chunks = [];
|
|
26
|
+
req.on("data", (c) => chunks.push(c));
|
|
27
|
+
req.on("end", () => {
|
|
28
|
+
const reply = (code, payload) => {
|
|
29
|
+
const text = JSON.stringify(payload);
|
|
30
|
+
res.writeHead(code, { "content-type": "application/json", "content-length": Buffer.byteLength(text) });
|
|
31
|
+
res.end(text);
|
|
32
|
+
};
|
|
33
|
+
const auth = String((req.headers.authorization || ""));
|
|
34
|
+
if (!auth.startsWith("Bearer ")) return reply(401, { error: "missing_bearer" });
|
|
35
|
+
const token = auth.slice("Bearer ".length);
|
|
36
|
+
const parts = token.split(".");
|
|
37
|
+
if (parts.length !== 3) return reply(401, { error: "malformed_jwt" });
|
|
38
|
+
const header = JSON.parse(Buffer.from(parts[0], "base64").toString("utf8"));
|
|
39
|
+
const claims = JSON.parse(Buffer.from(parts[1], "base64").toString("utf8"));
|
|
40
|
+
const sig = Buffer.from(parts[2].replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
41
|
+
const signatureValid = crypto.verify(null, Buffer.from(`${parts[0]}.${parts[1]}`), expectedPublicKey, sig);
|
|
42
|
+
// Record what the facilitator observed so the test can assert on it.
|
|
43
|
+
seen.push({ header, claims, signatureValid, path: req.url, nonce: header.nonce });
|
|
44
|
+
if (header.alg !== "EdDSA") return reply(401, { error: "bad_alg" });
|
|
45
|
+
if (!signatureValid) return reply(401, { error: "bad_signature" });
|
|
46
|
+
if (claims.iss !== "cdp") return reply(401, { error: "bad_iss" });
|
|
47
|
+
if (!Array.isArray(claims.aud) || claims.aud[0] !== "cdp_service") return reply(401, { error: "bad_aud" });
|
|
48
|
+
const now = Math.floor(Date.now() / 1000);
|
|
49
|
+
if (!(claims.exp > now && claims.exp - claims.nbf <= 120)) return reply(401, { error: "bad_exp" });
|
|
50
|
+
if (!(claims.uri.startsWith("POST 127.0.0.1:") && claims.uri.endsWith(req.url))) return reply(401, { error: "bad_uri", uri: claims.uri });
|
|
51
|
+
if (req.url === "/verify") return reply(200, { isValid: true, payer: "0x2222222222222222222222222222222222222222" });
|
|
52
|
+
if (req.url === "/settle") return reply(200, { success: true, transaction: "0xcdpmocktx", network: "eip155:8453" });
|
|
53
|
+
return reply(404, { error: "not_found" });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
return new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve({ server, port: server.address().port })));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const PAYLOAD = { x402Version: 2, accepted: { scheme: "exact" }, payload: { signature: "ok", authorization: {} } };
|
|
60
|
+
const REQS = { scheme: "exact", network: "eip155:8453", asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", amount: "10000", payTo: "0xabc", resource: "https://x/y" };
|
|
61
|
+
|
|
62
|
+
async function main() {
|
|
63
|
+
const { publicKey, keySecret } = makeCdpKey();
|
|
64
|
+
const seen = [];
|
|
65
|
+
const mock = await startCdpMock(publicKey, seen);
|
|
66
|
+
const baseUrl = `http://127.0.0.1:${mock.port}`;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// 1. Config loader picks up the CDP env into config.x402.cdp.
|
|
70
|
+
const prev = { id: process.env.XYTARA_X402_CDP_KEY_ID, sec: process.env.XYTARA_X402_CDP_KEY_SECRET };
|
|
71
|
+
process.env.XYTARA_X402_CDP_KEY_ID = "11111111-2222-3333-4444-555555555555";
|
|
72
|
+
process.env.XYTARA_X402_CDP_KEY_SECRET = keySecret;
|
|
73
|
+
const cfg = loadPaymentConfig();
|
|
74
|
+
assert.ok(cfg.x402.cdp && cfg.x402.cdp.key_id === "11111111-2222-3333-4444-555555555555", "config must load CDP key id");
|
|
75
|
+
assert.strictEqual(cfg.x402.cdp.key_secret, keySecret, "config must load CDP key secret");
|
|
76
|
+
|
|
77
|
+
// 2. The client attaches a valid per-request Bearer that the mock CDP accepts.
|
|
78
|
+
const client = createFacilitatorClient({ url: baseUrl, cdp: cfg.x402.cdp, timeoutMs: 5000 });
|
|
79
|
+
const verify = await client.verify(PAYLOAD, REQS);
|
|
80
|
+
assert.strictEqual(verify.ok, true, "verify must succeed with a valid CDP Bearer");
|
|
81
|
+
assert.strictEqual(verify.json.isValid, true, "mock CDP must accept the token on /verify");
|
|
82
|
+
const settle = await client.settle(PAYLOAD, REQS);
|
|
83
|
+
assert.strictEqual(settle.ok, true, "settle must succeed with a valid CDP Bearer");
|
|
84
|
+
assert.strictEqual(settle.json.transaction, "0xcdpmocktx", "settle must return the mock tx");
|
|
85
|
+
|
|
86
|
+
// 3. Each request minted its own token (fresh nonce), signature valid, uri bound to the path.
|
|
87
|
+
assert.strictEqual(seen.length, 2, "mock must have seen two authenticated calls");
|
|
88
|
+
assert.ok(seen.every((e) => e.signatureValid), "every token signature must verify");
|
|
89
|
+
assert.notStrictEqual(seen[0].nonce, seen[1].nonce, "each request must mint a fresh token (distinct nonce)");
|
|
90
|
+
assert.ok(seen[0].claims.uri.endsWith("/verify") && seen[1].claims.uri.endsWith("/settle"), "uri claim must bind to each request path");
|
|
91
|
+
|
|
92
|
+
// 4. Without CDP creds, no Authorization header is sent (static-facilitator path unaffected).
|
|
93
|
+
const seen2 = [];
|
|
94
|
+
const mock2 = await startCdpMock(publicKey, seen2);
|
|
95
|
+
const plainClient = createFacilitatorClient({ url: `http://127.0.0.1:${mock2.port}`, timeoutMs: 5000 });
|
|
96
|
+
const plain = await plainClient.verify(PAYLOAD, REQS);
|
|
97
|
+
assert.strictEqual(plain.status, 401, "without CDP creds the mock sees no Bearer and rejects");
|
|
98
|
+
assert.strictEqual(seen2.length, 0, "no JWT should be minted when CDP is not configured");
|
|
99
|
+
await new Promise((r) => mock2.server.close(r));
|
|
100
|
+
|
|
101
|
+
if (prev.id === undefined) delete process.env.XYTARA_X402_CDP_KEY_ID; else process.env.XYTARA_X402_CDP_KEY_ID = prev.id;
|
|
102
|
+
if (prev.sec === undefined) delete process.env.XYTARA_X402_CDP_KEY_SECRET; else process.env.XYTARA_X402_CDP_KEY_SECRET = prev.sec;
|
|
103
|
+
|
|
104
|
+
console.log("xytara verify:cdp-facilitator passed");
|
|
105
|
+
} finally {
|
|
106
|
+
await new Promise((r) => mock.server.close(r));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main().catch((e) => { console.error(e && e.message ? e.message : e); process.exitCode = 1; });
|