xytara 3.1.13 → 3.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.env.example +38 -4
  2. package/BSV_TERANODE_PLATFORM_DISCONTINUITY_RUNBOOK.md +126 -126
  3. package/REAL_PAYMENT_SETUP.md +14 -7
  4. package/RELEASE_NOTES.md +9 -4
  5. package/fixtures/catalog.js +22 -0
  6. package/lib/a2a_rpc.js +287 -280
  7. package/lib/account_auth.js +585 -585
  8. package/lib/agent_identity.js +162 -162
  9. package/lib/asyncapi_contract.js +3 -3
  10. package/lib/bolt_adapter.js +187 -187
  11. package/lib/bolt_payment_front.js +224 -224
  12. package/lib/buyer_event_log.js +215 -0
  13. package/lib/buyer_safety_policy.js +132 -0
  14. package/lib/checkout_processor_adapter.js +217 -217
  15. package/lib/command_flow.js +911 -722
  16. package/lib/commerce_artifacts.js +591 -591
  17. package/lib/commerce_bridge.js +274 -274
  18. package/lib/commerce_cases.js +399 -399
  19. package/lib/commerce_client.js +7694 -7694
  20. package/lib/commerce_quotes.js +29 -0
  21. package/lib/commerce_runtime.js +322 -322
  22. package/lib/execution_dedupe.js +86 -86
  23. package/lib/external_credit_bridge.js +142 -142
  24. package/lib/http_transport.js +99 -81
  25. package/lib/l402_adapter.js +241 -241
  26. package/lib/l402_payment_front.js +265 -265
  27. package/lib/mpp_adapter.js +237 -237
  28. package/lib/openapi_contract.js +4 -0
  29. package/lib/payment_config.js +257 -253
  30. package/lib/payment_verification.js +357 -357
  31. package/lib/provider_payout_execution.js +155 -155
  32. package/lib/provider_registry.js +852 -842
  33. package/lib/rate_limiter.js +81 -81
  34. package/lib/runtime_state_store.js +495 -369
  35. package/lib/settlement_bsv_live.js +600 -600
  36. package/lib/settlement_bsv_teranode.js +117 -117
  37. package/lib/stripe_mpp_payment_front.js +258 -258
  38. package/lib/x402_facilitator.js +198 -191
  39. package/lib/x402_lane_contract.js +5 -3
  40. package/lib/x402_settlement_recovery.js +175 -0
  41. package/package.json +9 -3
  42. package/scripts/buyer_events_daily_report.js +79 -0
  43. package/scripts/state_backup_offsite.js +196 -0
  44. package/scripts/verify_a2a_rpc.js +131 -105
  45. package/scripts/verify_agent_identity.js +153 -153
  46. package/scripts/verify_all.js +1 -1
  47. package/scripts/verify_bolt_adapter.js +128 -128
  48. package/scripts/verify_buyer_event_log.js +123 -0
  49. package/scripts/verify_buyer_safety.js +141 -0
  50. package/scripts/verify_cdp_facilitator.js +110 -110
  51. package/scripts/verify_checkout_processor.js +109 -109
  52. package/scripts/verify_commerce_bridge.js +177 -177
  53. package/scripts/verify_execution_dedupe.js +189 -189
  54. package/scripts/verify_execution_surfacing.js +76 -76
  55. package/scripts/verify_final_activation.js +191 -191
  56. package/scripts/verify_l402_adapter.js +86 -86
  57. package/scripts/verify_lifecycle_spend_binding.js +88 -0
  58. package/scripts/verify_mpp_adapter.js +128 -128
  59. package/scripts/verify_production_readiness.js +268 -268
  60. package/scripts/verify_rate_limiter.js +52 -52
  61. package/scripts/verify_refund_credit.js +106 -106
  62. package/scripts/verify_release_candidate.js +81 -81
  63. package/scripts/verify_runtime_state_guard.js +117 -0
  64. package/scripts/verify_service.js +15421 -15421
  65. package/scripts/verify_verify_execute_settle.js +209 -208
  66. package/scripts/verify_x402_facilitator.js +307 -286
  67. package/scripts/verify_x402_settlement_recovery.js +153 -0
  68. package/server.js +12242 -11974
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+
3
+ // End-to-end check (real local server) of the buyer event log: failed buyer attempts are recorded
4
+ // with a stage/reason/fault and a request id the buyer also receives; normal 402 challenges are
5
+ // counted, not logged; malformed JSON is a 400 (not a 500); executor events can be ingested with
6
+ // the dedicated token only; reads are operator-only; nothing sensitive (payment signature, full
7
+ // payer address) is ever stored.
8
+
9
+ const assert = require("assert");
10
+
11
+ const PAYER = "0x1111111111111111111111111111111111111111";
12
+ const ACCEPTS = [{
13
+ scheme: "exact",
14
+ network: "eip155:8453",
15
+ asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
16
+ amount: "10000",
17
+ payTo: "0x7cd2E0Ed2721319B20FAac4109774d030833F84D",
18
+ extra: { name: "USD Coin", version: "2" },
19
+ maxTimeoutSeconds: 60
20
+ }];
21
+ const FAKE_SIGNATURE = `0x${"9f".repeat(65)}`;
22
+
23
+ async function request(baseUrl, method, path, { body, raw, headers = {} } = {}) {
24
+ const res = await fetch(`${baseUrl}${path}`, {
25
+ method,
26
+ headers: { "content-type": "application/json", ...headers },
27
+ body: raw !== undefined ? raw : body !== undefined ? JSON.stringify(body) : undefined
28
+ });
29
+ let json = null;
30
+ try { json = await res.json(); } catch (_) { json = null; }
31
+ return { status: res.status, json, headers: res.headers };
32
+ }
33
+
34
+ async function main() {
35
+ const OP = "event-log-operator-token";
36
+ const INGEST = "event-log-ingest-token";
37
+ const keys = ["XYTARA_ACCOUNT_AUTH_BEARER_TOKEN", "XYTARA_EVENT_INGEST_TOKEN", "XYTARA_PAID_EXECUTION_POLICY", "XYTARA_PAYMENT_VERIFICATION_MODE", "XYTARA_X402_ACCEPTS_JSON", "XYTARA_X402_FACILITATOR_URL"];
38
+ const previous = Object.fromEntries(keys.map((key) => [key, process.env[key]]));
39
+ process.env.XYTARA_ACCOUNT_AUTH_BEARER_TOKEN = OP;
40
+ process.env.XYTARA_EVENT_INGEST_TOKEN = INGEST;
41
+ process.env.XYTARA_PAID_EXECUTION_POLICY = "strict";
42
+ process.env.XYTARA_PAYMENT_VERIFICATION_MODE = "x402_facilitator";
43
+ process.env.XYTARA_X402_ACCEPTS_JSON = JSON.stringify(ACCEPTS);
44
+ process.env.XYTARA_X402_FACILITATOR_URL = "http://127.0.0.1:9";
45
+
46
+ delete require.cache[require.resolve("../server")];
47
+ const { createServer } = require("../server");
48
+ const server = createServer();
49
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
50
+ const baseUrl = `http://127.0.0.1:${server.address().port}`;
51
+ const operator = { authorization: `Bearer ${OP}` };
52
+ try {
53
+ // 1. Malformed JSON is the buyer's error (400 invalid_json), with a request id, and is logged.
54
+ const malformed = await request(baseUrl, "POST", "/x402/commands/execute", { raw: "{not json" });
55
+ assert.strictEqual(malformed.status, 400, "malformed JSON must be a 400, not a 500");
56
+ assert.strictEqual(malformed.json.reason, "invalid_json");
57
+ assert.ok(/^req_[0-9a-f]{16}$/.test(malformed.headers.get("x-request-id") || ""), "every response carries X-Request-Id");
58
+
59
+ // 2. A non-executable task (strict) is refused and logged as a route failure.
60
+ const refused = await request(baseUrl, "POST", "/x402/commands/execute", { body: { task_ref: "trust.verify" } });
61
+ assert.strictEqual(refused.status, 501);
62
+
63
+ // 3. A payment attempt with a fake signature: logged with network/amount and a payer HASH only.
64
+ const header = Buffer.from(JSON.stringify({
65
+ x402Version: 2,
66
+ accepted: ACCEPTS[0],
67
+ payload: { signature: FAKE_SIGNATURE, authorization: { from: PAYER, to: ACCEPTS[0].payTo, value: "10000", validAfter: "0", validBefore: String(Math.floor(Date.now() / 1000) + 600), nonce: `0x${"ab".repeat(32)}` } }
68
+ })).toString("base64");
69
+ const executable = { task_ref: "adapter.mcp.invoke", body: { tool_name: "keccak256", arguments: { data: "abc" } } };
70
+ // No executable target here, so register nothing: the strict gate refuses before payment -> still an event.
71
+ await request(baseUrl, "POST", "/x402/commands/execute", { body: executable, headers: { "payment-signature": header } });
72
+
73
+ // 4. Reads are operator-only.
74
+ const anonymous = await request(baseUrl, "GET", "/v1/operator/buyer-events");
75
+ assert.strictEqual(anonymous.status, 403, "buyer events are operator-only");
76
+ const listed = await request(baseUrl, "GET", "/v1/operator/buyer-events?limit=50", { headers: operator });
77
+ assert.strictEqual(listed.status, 200);
78
+ const events = listed.json.events;
79
+ assert.ok(events.some((event) => event.reason === "invalid_json" && event.stage === "parse" && event.fault === "buyer" && event.request_id === malformed.headers.get("x-request-id")), "malformed JSON event recorded with the buyer's request id");
80
+ assert.ok(events.some((event) => event.reason === "capability_not_executable" && event.task_or_tool === "trust.verify"), "non-executable attempt recorded with its task");
81
+ const paid = events.find((event) => event.task_or_tool === "keccak256");
82
+ assert.ok(paid && paid.network === "eip155:8453" && paid.amount_atomic === "10000" && /^[0-9a-f]{12}$/.test(paid.payer_hash), "payment attempt recorded with network, amount and payer hash");
83
+ const serialized = JSON.stringify(listed.json);
84
+ assert.ok(!serialized.includes(PAYER.slice(2)) && !serialized.includes("9f9f9f9f9f"), "no full payer address or signature is ever stored");
85
+
86
+ // 5. Executor events: dedicated ingest token only.
87
+ const forbidden = await request(baseUrl, "POST", "/v1/operator/buyer-events/ingest", { body: { source: "xytara-mcp-executor", events: [] }, headers: operator });
88
+ assert.strictEqual(forbidden.status, 403, "the operator token cannot ingest; only the ingest token");
89
+ const ingested = await request(baseUrl, "POST", "/v1/operator/buyer-events/ingest", {
90
+ headers: { authorization: `Bearer ${INGEST}` },
91
+ body: {
92
+ source: "xytara-mcp-executor",
93
+ events: [
94
+ { stage: "challenge_issued", tool: "keccak256" },
95
+ { stage: "paid_ok", tool: "keccak256" },
96
+ { stage: "payment_settle", reason: "settle_outcome_unknown", fault: "upstream", tool: "summarize", money_moved: true, http_status: 200 }
97
+ ]
98
+ }
99
+ });
100
+ assert.strictEqual(ingested.status, 200);
101
+ assert.strictEqual(ingested.json.accepted, 3);
102
+
103
+ // 6. Summary: denominators and the attention flag.
104
+ const summary = await request(baseUrl, "GET", "/v1/operator/buyer-events/summary?days=7", { headers: operator });
105
+ assert.strictEqual(summary.status, 200);
106
+ assert.ok(summary.json.totals.failures >= 4, "failures counted");
107
+ assert.ok(summary.json.totals.challenges_issued >= 1 && summary.json.totals.paid_ok >= 1, "steps counted as denominators");
108
+ assert.strictEqual(summary.json.totals.money_moved, 1, "money moved without delivery is surfaced");
109
+ assert.strictEqual(summary.json.attention, true, "attention raised");
110
+
111
+ console.log("xytara verify:buyer-event-log passed");
112
+ } finally {
113
+ await new Promise((resolve) => server.close(resolve));
114
+ for (const key of keys) {
115
+ if (previous[key] === undefined) delete process.env[key]; else process.env[key] = previous[key];
116
+ }
117
+ }
118
+ }
119
+
120
+ main().catch((error) => {
121
+ console.error(error && error.stack ? error.stack : error);
122
+ process.exit(1);
123
+ });
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+
3
+ // End-to-end check (real local server) of the hosted-deploy money-safety policy:
4
+ // - strict paid execution: a task nothing can execute is refused up front (501, no 402, no charge),
5
+ // a missing task_ref is a 400, and an executable task still gets the normal 402 challenge;
6
+ // - strict credit spend: a bare account_id can never spend credits; the account's spend credential can;
7
+ // - credit spends accrue provider revenue on the units' purchase value, never on the catalog quote.
8
+
9
+ const assert = require("assert");
10
+ const { reconcileProviderSettlementAmount } = require("../lib/command_flow");
11
+ const { buildTopupAccepts, topupUnitsForAccept } = require("../lib/buyer_safety_policy");
12
+
13
+ const ACCEPTS = [
14
+ {
15
+ scheme: "exact",
16
+ network: "eip155:8453",
17
+ asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
18
+ amount: "10000",
19
+ payTo: "0x7cd2E0Ed2721319B20FAac4109774d030833F84D",
20
+ extra: { name: "USD Coin", version: "2" },
21
+ maxTimeoutSeconds: 60
22
+ },
23
+ {
24
+ scheme: "exact",
25
+ network: "eip155:84532",
26
+ asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
27
+ amount: "1000",
28
+ payTo: "0x7cd2E0Ed2721319B20FAac4109774d030833F84D",
29
+ extra: { name: "USDC", version: "2" },
30
+ maxTimeoutSeconds: 60
31
+ }
32
+ ];
33
+
34
+ async function post(baseUrl, path, body, headers = {}) {
35
+ const res = await fetch(`${baseUrl}${path}`, { method: "POST", headers: { "content-type": "application/json", ...headers }, body: JSON.stringify(body) });
36
+ let json = null;
37
+ try { json = await res.json(); } catch (_) { json = null; }
38
+ return { status: res.status, json };
39
+ }
40
+
41
+ async function main() {
42
+ // Pure policy: top-up pricing and credit derivation.
43
+ const previousTestnet = process.env.XYTARA_X402_TESTNET_TOPUP;
44
+ delete process.env.XYTARA_X402_TESTNET_TOPUP;
45
+ const topupAccepts = buildTopupAccepts(ACCEPTS);
46
+ assert.deepStrictEqual(topupAccepts.map((a) => a.network), ["eip155:8453"], "testnet accepts cannot buy credits by default");
47
+ assert.strictEqual(topupAccepts[0].amount, "1000000", "a top-up costs 100x the per-call price ($1.00)");
48
+ assert.strictEqual(topupUnitsForAccept(topupAccepts[0]), 400, "$1.00 buys 400 credits at $0.0025");
49
+ assert.strictEqual(topupUnitsForAccept({ ...topupAccepts[0], extra: { credits: 1000000 } }), 400, "extra.credits can never exceed the paid value");
50
+ assert.strictEqual(topupUnitsForAccept({ ...topupAccepts[0], extra: { credits: 100 } }), 100, "extra.credits may lower the grant");
51
+ if (previousTestnet === undefined) delete process.env.XYTARA_X402_TESTNET_TOPUP; else process.env.XYTARA_X402_TESTNET_TOPUP = previousTestnet;
52
+
53
+ // Pure policy: credit-spend accrual basis.
54
+ const quote = { amount_minor: 40, currency: "USD_CENTS" };
55
+ const creditBasis = reconcileProviderSettlementAmount(quote, {
56
+ verification_mode: "account_credits",
57
+ payment_payload: { credit_spend_id: "cs_1", credit_units: 8, credit_unit_price_atomic: 2500 }
58
+ });
59
+ assert.strictEqual(creditBasis.settlement_basis, "credits_unit_value_min_quote");
60
+ assert.strictEqual(creditBasis.amount_minor, 2, "8 credits x $0.0025 = 2 cents accrue, not the 40-cent quote");
61
+
62
+ const OP = "buyer-safety-operator-token";
63
+ const BRIDGE = "buyer-safety-bridge-token";
64
+ const previous = {
65
+ exec: process.env.XYTARA_PAID_EXECUTION_POLICY,
66
+ credit: process.env.XYTARA_CREDIT_SPEND_POLICY,
67
+ op: process.env.XYTARA_ACCOUNT_AUTH_BEARER_TOKEN,
68
+ bridge: process.env.XYTARA_CREDIT_BRIDGE_BEARER_TOKEN,
69
+ mode: process.env.XYTARA_PAYMENT_VERIFICATION_MODE,
70
+ accepts: process.env.XYTARA_X402_ACCEPTS_JSON,
71
+ targets: process.env.XYTARA_EXECUTION_TARGETS_JSON
72
+ };
73
+ process.env.XYTARA_PAID_EXECUTION_POLICY = "strict";
74
+ process.env.XYTARA_CREDIT_SPEND_POLICY = "strict";
75
+ process.env.XYTARA_ACCOUNT_AUTH_BEARER_TOKEN = OP;
76
+ process.env.XYTARA_CREDIT_BRIDGE_BEARER_TOKEN = BRIDGE;
77
+ process.env.XYTARA_PAYMENT_VERIFICATION_MODE = "x402_facilitator";
78
+ process.env.XYTARA_X402_ACCEPTS_JSON = JSON.stringify(ACCEPTS);
79
+ // One operator-pinned target makes exactly one task executable (no network call happens before 402).
80
+ process.env.XYTARA_EXECUTION_TARGETS_JSON = JSON.stringify([
81
+ { target_id: "safety_target", task_ref: "adapter.mcp.invoke", endpoint_url: "http://127.0.0.1:9/execute" }
82
+ ]);
83
+
84
+ delete require.cache[require.resolve("../server")];
85
+ const { createServer } = require("../server");
86
+ const server = createServer();
87
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
88
+ const baseUrl = `http://127.0.0.1:${server.address().port}`;
89
+ try {
90
+ // 1. A catalog task with no execution target: refused before any payment is requested.
91
+ const notExecutable = await post(baseUrl, "/x402/commands/execute", { task_ref: "trust.verify", body: { subject_id: "s-1" } });
92
+ assert.strictEqual(notExecutable.status, 501, "non-executable task must be refused, not quoted");
93
+ assert.strictEqual(notExecutable.json.reason, "capability_not_executable");
94
+ assert.ok(notExecutable.json.executable_task_refs.includes("adapter.mcp.invoke"), "the refusal lists what IS executable");
95
+
96
+ // 2. Unknown task and missing task_ref.
97
+ const unknown = await post(baseUrl, "/x402/commands/execute", { task_ref: "nope.task" });
98
+ assert.strictEqual(unknown.status, 501, "unknown task must not be payable");
99
+ const missing = await post(baseUrl, "/x402/commands/execute", {});
100
+ assert.strictEqual(missing.status, 400, "missing task_ref must be a 400");
101
+ assert.strictEqual(missing.json.reason, "task_ref_required");
102
+
103
+ // 3. An executable task still gets the normal x402 challenge.
104
+ const executable = await post(baseUrl, "/x402/commands/execute", { task_ref: "adapter.mcp.invoke", body: { tool_name: "keccak256", arguments: { data: "abc" } } });
105
+ assert.strictEqual(executable.status, 402, "executable task must be quoted for payment");
106
+
107
+ // 4. Credits: a bare account_id can't spend, even with a balance.
108
+ const account = "acct_safety_owner";
109
+ const grant = await post(baseUrl, "/v1/credit-bridge/grants", { account_id: account, units: 50, source: "verify" }, { authorization: `Bearer ${BRIDGE}` });
110
+ assert.strictEqual(grant.status, 200, "operator grant must succeed");
111
+ const creditBody = { task_ref: "adapter.mcp.invoke", account_id: account, payment_preference: "credits_first", body: { tool_name: "keccak256", arguments: { data: "abc" } } };
112
+ const theft = await post(baseUrl, "/v1/commands/execute", creditBody);
113
+ assert.strictEqual(theft.status, 401, "spending another account's credits without its credential must be refused");
114
+ assert.strictEqual(theft.json.reason, "credit_spend_credential_required");
115
+
116
+ // 5. The account's own spend credential clears the credit gate (it then reaches execution,
117
+ // which fails here because the pinned target is unreachable — and no credits are consumed).
118
+ const created = await post(baseUrl, "/v1/account-auth/spend-credentials", { account_id: account }, { authorization: `Bearer ${OP}` });
119
+ const cred = (created.json && created.json.spend_credential) || created.json;
120
+ assert.ok(cred && cred.bearer_token, "spend credential created");
121
+ const owned = await post(baseUrl, "/v1/commands/execute", creditBody, { authorization: `Bearer ${cred.bearer_token}` });
122
+ assert.notStrictEqual(owned.status, 401, "the owner's credential must clear the credit gate");
123
+ assert.strictEqual(owned.json && owned.json.credit_spend_status, "not_committed", "a failed execution must not consume credits");
124
+
125
+ // 6. A credential for one account can't be aimed at another.
126
+ const misaimed = await post(baseUrl, "/v1/commands/execute", { ...creditBody, account_id: "acct_someone_else" }, { authorization: `Bearer ${cred.bearer_token}` });
127
+ assert.strictEqual(misaimed.status, 403, "credential pinned to its own account");
128
+
129
+ console.log("xytara verify:buyer-safety passed");
130
+ } finally {
131
+ await new Promise((resolve) => server.close(resolve));
132
+ for (const [key, env] of [["exec", "XYTARA_PAID_EXECUTION_POLICY"], ["credit", "XYTARA_CREDIT_SPEND_POLICY"], ["op", "XYTARA_ACCOUNT_AUTH_BEARER_TOKEN"], ["bridge", "XYTARA_CREDIT_BRIDGE_BEARER_TOKEN"], ["mode", "XYTARA_PAYMENT_VERIFICATION_MODE"], ["accepts", "XYTARA_X402_ACCEPTS_JSON"], ["targets", "XYTARA_EXECUTION_TARGETS_JSON"]]) {
133
+ if (previous[key] === undefined) delete process.env[env]; else process.env[env] = previous[key];
134
+ }
135
+ }
136
+ }
137
+
138
+ main().catch((error) => {
139
+ console.error(error && error.stack ? error.stack : error);
140
+ process.exit(1);
141
+ });
@@ -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; });