xytara 3.1.13 → 3.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.env.example +38 -4
  2. package/BSV_TERANODE_PLATFORM_DISCONTINUITY_RUNBOOK.md +126 -126
  3. package/REAL_PAYMENT_SETUP.md +14 -7
  4. package/RELEASE_NOTES.md +9 -4
  5. package/fixtures/catalog.js +22 -0
  6. package/lib/a2a_rpc.js +287 -280
  7. package/lib/account_auth.js +585 -585
  8. package/lib/agent_identity.js +162 -162
  9. package/lib/asyncapi_contract.js +3 -3
  10. package/lib/bolt_adapter.js +187 -187
  11. package/lib/bolt_payment_front.js +224 -224
  12. package/lib/buyer_event_log.js +215 -0
  13. package/lib/buyer_safety_policy.js +132 -0
  14. package/lib/checkout_processor_adapter.js +217 -217
  15. package/lib/command_flow.js +911 -722
  16. package/lib/commerce_artifacts.js +591 -591
  17. package/lib/commerce_bridge.js +274 -274
  18. package/lib/commerce_cases.js +399 -399
  19. package/lib/commerce_client.js +7694 -7694
  20. package/lib/commerce_quotes.js +29 -0
  21. package/lib/commerce_runtime.js +322 -322
  22. package/lib/execution_dedupe.js +86 -86
  23. package/lib/external_credit_bridge.js +142 -142
  24. package/lib/http_transport.js +99 -81
  25. package/lib/l402_adapter.js +241 -241
  26. package/lib/l402_payment_front.js +265 -265
  27. package/lib/mpp_adapter.js +237 -237
  28. package/lib/openapi_contract.js +4 -0
  29. package/lib/payment_config.js +257 -253
  30. package/lib/payment_verification.js +357 -357
  31. package/lib/provider_payout_execution.js +155 -155
  32. package/lib/provider_registry.js +852 -842
  33. package/lib/rate_limiter.js +81 -81
  34. package/lib/runtime_state_store.js +495 -369
  35. package/lib/settlement_bsv_live.js +600 -600
  36. package/lib/settlement_bsv_teranode.js +117 -117
  37. package/lib/stripe_mpp_payment_front.js +258 -258
  38. package/lib/x402_facilitator.js +198 -191
  39. package/lib/x402_lane_contract.js +5 -3
  40. package/lib/x402_settlement_recovery.js +175 -0
  41. package/package.json +9 -3
  42. package/scripts/buyer_events_daily_report.js +79 -0
  43. package/scripts/state_backup_offsite.js +196 -0
  44. package/scripts/verify_a2a_rpc.js +131 -105
  45. package/scripts/verify_agent_identity.js +153 -153
  46. package/scripts/verify_all.js +1 -1
  47. package/scripts/verify_bolt_adapter.js +128 -128
  48. package/scripts/verify_buyer_event_log.js +123 -0
  49. package/scripts/verify_buyer_safety.js +141 -0
  50. package/scripts/verify_cdp_facilitator.js +110 -110
  51. package/scripts/verify_checkout_processor.js +109 -109
  52. package/scripts/verify_commerce_bridge.js +177 -177
  53. package/scripts/verify_execution_dedupe.js +189 -189
  54. package/scripts/verify_execution_surfacing.js +76 -76
  55. package/scripts/verify_final_activation.js +191 -191
  56. package/scripts/verify_l402_adapter.js +86 -86
  57. package/scripts/verify_lifecycle_spend_binding.js +88 -0
  58. package/scripts/verify_mpp_adapter.js +128 -128
  59. package/scripts/verify_production_readiness.js +268 -268
  60. package/scripts/verify_rate_limiter.js +52 -52
  61. package/scripts/verify_refund_credit.js +106 -106
  62. package/scripts/verify_release_candidate.js +81 -81
  63. package/scripts/verify_runtime_state_guard.js +117 -0
  64. package/scripts/verify_service.js +15421 -15421
  65. package/scripts/verify_verify_execute_settle.js +209 -208
  66. package/scripts/verify_x402_facilitator.js +307 -286
  67. package/scripts/verify_x402_settlement_recovery.js +153 -0
  68. package/server.js +12242 -11974
@@ -1,109 +1,109 @@
1
- "use strict";
2
-
3
- // Verifies the provider-agnostic hosted-checkout processor adapter
4
- // (lib/checkout_processor_adapter.js). Offline: no network, no real Stripe
5
- // calls. Exercises config gating, webhook signature verify (round-trip),
6
- // event->grant mapping, and the not-configured safety behavior.
7
-
8
- const assert = require("assert");
9
-
10
- function withEnv(env, fn) {
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
- // Re-require fresh so env is read on load.
18
- delete require.cache[require.resolve("../lib/checkout_processor_adapter")];
19
- const mod = require("../lib/checkout_processor_adapter");
20
- try {
21
- return fn(mod);
22
- } finally {
23
- for (const k in saved) {
24
- if (saved[k] === undefined) delete process.env[k];
25
- else process.env[k] = saved[k];
26
- }
27
- delete require.cache[require.resolve("../lib/checkout_processor_adapter")];
28
- }
29
- }
30
-
31
- async function main() {
32
- // 1. Not configured => provider null, everything degrades safely.
33
- await withEnv({ XYTARA_CHECKOUT_PROCESSOR: undefined }, async (m) => {
34
- const cfg = m.loadCheckoutProcessorConfig();
35
- assert.strictEqual(cfg.provider, null);
36
- assert.strictEqual(m.isConfigured(cfg), false);
37
- const s = await m.createProcessorCheckoutSession(cfg, { amountMinor: 100 });
38
- assert.strictEqual(s.ok, false);
39
- assert.strictEqual(s.reason, "processor_not_configured");
40
- assert.strictEqual(m.verifyProcessorWebhook(cfg, {}, "{}").ok, false);
41
- assert.strictEqual(m.mapProcessorEventToGrant(cfg, { type: "checkout.session.completed" }), null);
42
- });
43
-
44
- // 2. Stripe configured => webhook verify round-trip + event mapping.
45
- await withEnv(
46
- { XYTARA_CHECKOUT_PROCESSOR: "stripe", STRIPE_SECRET_KEY: "sk_test_x", STRIPE_WEBHOOK_SECRET: "whsec_test" },
47
- async (m) => {
48
- const cfg = m.loadCheckoutProcessorConfig();
49
- assert.strictEqual(cfg.provider, "stripe");
50
- assert.strictEqual(m.isConfigured(cfg), true);
51
-
52
- const event = {
53
- id: "evt_1",
54
- type: "checkout.session.completed",
55
- data: {
56
- object: {
57
- id: "cs_1",
58
- payment_status: "paid",
59
- payment_intent: "pi_1",
60
- amount_total: 500,
61
- currency: "usd",
62
- metadata: { account_id: "acct_x", credits_units: "1000", purchase_intent_id: "pi_intent_1" }
63
- }
64
- }
65
- };
66
- const raw = JSON.stringify(event);
67
- const sig = m.buildStripeSignatureHeader(cfg.webhookSecret, raw);
68
-
69
- // Valid signature -> ok + parsed event.
70
- const okRes = m.verifyProcessorWebhook(cfg, { "Stripe-Signature": sig }, raw);
71
- assert.strictEqual(okRes.ok, true);
72
- assert.strictEqual(okRes.event.id, "evt_1");
73
-
74
- // Tampered body -> invalid.
75
- assert.strictEqual(m.verifyProcessorWebhook(cfg, { "Stripe-Signature": sig }, raw + " ").ok, false);
76
- // Missing signature header -> malformed.
77
- assert.strictEqual(m.verifyProcessorWebhook(cfg, {}, raw).reason, "stripe_signature_malformed");
78
- // Stale timestamp -> out of tolerance.
79
- const oldSig = m.buildStripeSignatureHeader(cfg.webhookSecret, raw, Math.floor(Date.now() / 1000) - 99999);
80
- assert.strictEqual(m.verifyProcessorWebhook(cfg, { "Stripe-Signature": oldSig }, raw).reason, "stripe_timestamp_out_of_tolerance");
81
-
82
- // Event -> grant mapping.
83
- const grant = m.mapProcessorEventToGrant(cfg, event);
84
- assert.strictEqual(grant.account_id, "acct_x");
85
- assert.strictEqual(grant.units, 1000);
86
- assert.strictEqual(grant.source, "stripe");
87
- assert.strictEqual(grant.provider_reference, "pi_1");
88
- assert.strictEqual(grant.idempotency_key, "stripe:evt_1");
89
- assert.strictEqual(grant.purchase_intent_id, "pi_intent_1");
90
-
91
- // Non-paid / wrong-type events -> null.
92
- assert.strictEqual(m.mapProcessorEventToGrant(cfg, { type: "checkout.session.completed", data: { object: { payment_status: "unpaid" } } }), null);
93
- assert.strictEqual(m.mapProcessorEventToGrant(cfg, { type: "payment_intent.created", data: { object: {} } }), null);
94
-
95
- // Session creation missing secret key -> explicit reason (no network).
96
- const noKeyCfg = Object.assign({}, cfg, { secretKey: "" });
97
- const s = await m.createProcessorCheckoutSession(noKeyCfg, { amountMinor: 500, currency: "usd", successUrl: "https://x", cancelUrl: "https://y" });
98
- assert.strictEqual(s.ok, false);
99
- assert.strictEqual(s.reason, "stripe_secret_key_missing");
100
- }
101
- );
102
-
103
- console.log("verify_checkout_processor: all hosted-checkout processor adapter checks passed");
104
- }
105
-
106
- main().catch((error) => {
107
- console.error("verify_checkout_processor failed:", error && error.message ? error.message : error);
108
- process.exitCode = 1;
109
- });
1
+ "use strict";
2
+
3
+ // Verifies the provider-agnostic hosted-checkout processor adapter
4
+ // (lib/checkout_processor_adapter.js). Offline: no network, no real Stripe
5
+ // calls. Exercises config gating, webhook signature verify (round-trip),
6
+ // event->grant mapping, and the not-configured safety behavior.
7
+
8
+ const assert = require("assert");
9
+
10
+ function withEnv(env, fn) {
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
+ // Re-require fresh so env is read on load.
18
+ delete require.cache[require.resolve("../lib/checkout_processor_adapter")];
19
+ const mod = require("../lib/checkout_processor_adapter");
20
+ try {
21
+ return fn(mod);
22
+ } finally {
23
+ for (const k in saved) {
24
+ if (saved[k] === undefined) delete process.env[k];
25
+ else process.env[k] = saved[k];
26
+ }
27
+ delete require.cache[require.resolve("../lib/checkout_processor_adapter")];
28
+ }
29
+ }
30
+
31
+ async function main() {
32
+ // 1. Not configured => provider null, everything degrades safely.
33
+ await withEnv({ XYTARA_CHECKOUT_PROCESSOR: undefined }, async (m) => {
34
+ const cfg = m.loadCheckoutProcessorConfig();
35
+ assert.strictEqual(cfg.provider, null);
36
+ assert.strictEqual(m.isConfigured(cfg), false);
37
+ const s = await m.createProcessorCheckoutSession(cfg, { amountMinor: 100 });
38
+ assert.strictEqual(s.ok, false);
39
+ assert.strictEqual(s.reason, "processor_not_configured");
40
+ assert.strictEqual(m.verifyProcessorWebhook(cfg, {}, "{}").ok, false);
41
+ assert.strictEqual(m.mapProcessorEventToGrant(cfg, { type: "checkout.session.completed" }), null);
42
+ });
43
+
44
+ // 2. Stripe configured => webhook verify round-trip + event mapping.
45
+ await withEnv(
46
+ { XYTARA_CHECKOUT_PROCESSOR: "stripe", STRIPE_SECRET_KEY: "sk_test_x", STRIPE_WEBHOOK_SECRET: "whsec_test" },
47
+ async (m) => {
48
+ const cfg = m.loadCheckoutProcessorConfig();
49
+ assert.strictEqual(cfg.provider, "stripe");
50
+ assert.strictEqual(m.isConfigured(cfg), true);
51
+
52
+ const event = {
53
+ id: "evt_1",
54
+ type: "checkout.session.completed",
55
+ data: {
56
+ object: {
57
+ id: "cs_1",
58
+ payment_status: "paid",
59
+ payment_intent: "pi_1",
60
+ amount_total: 500,
61
+ currency: "usd",
62
+ metadata: { account_id: "acct_x", credits_units: "1000", purchase_intent_id: "pi_intent_1" }
63
+ }
64
+ }
65
+ };
66
+ const raw = JSON.stringify(event);
67
+ const sig = m.buildStripeSignatureHeader(cfg.webhookSecret, raw);
68
+
69
+ // Valid signature -> ok + parsed event.
70
+ const okRes = m.verifyProcessorWebhook(cfg, { "Stripe-Signature": sig }, raw);
71
+ assert.strictEqual(okRes.ok, true);
72
+ assert.strictEqual(okRes.event.id, "evt_1");
73
+
74
+ // Tampered body -> invalid.
75
+ assert.strictEqual(m.verifyProcessorWebhook(cfg, { "Stripe-Signature": sig }, raw + " ").ok, false);
76
+ // Missing signature header -> malformed.
77
+ assert.strictEqual(m.verifyProcessorWebhook(cfg, {}, raw).reason, "stripe_signature_malformed");
78
+ // Stale timestamp -> out of tolerance.
79
+ const oldSig = m.buildStripeSignatureHeader(cfg.webhookSecret, raw, Math.floor(Date.now() / 1000) - 99999);
80
+ assert.strictEqual(m.verifyProcessorWebhook(cfg, { "Stripe-Signature": oldSig }, raw).reason, "stripe_timestamp_out_of_tolerance");
81
+
82
+ // Event -> grant mapping.
83
+ const grant = m.mapProcessorEventToGrant(cfg, event);
84
+ assert.strictEqual(grant.account_id, "acct_x");
85
+ assert.strictEqual(grant.units, 1000);
86
+ assert.strictEqual(grant.source, "stripe");
87
+ assert.strictEqual(grant.provider_reference, "pi_1");
88
+ assert.strictEqual(grant.idempotency_key, "stripe:evt_1");
89
+ assert.strictEqual(grant.purchase_intent_id, "pi_intent_1");
90
+
91
+ // Non-paid / wrong-type events -> null.
92
+ assert.strictEqual(m.mapProcessorEventToGrant(cfg, { type: "checkout.session.completed", data: { object: { payment_status: "unpaid" } } }), null);
93
+ assert.strictEqual(m.mapProcessorEventToGrant(cfg, { type: "payment_intent.created", data: { object: {} } }), null);
94
+
95
+ // Session creation missing secret key -> explicit reason (no network).
96
+ const noKeyCfg = Object.assign({}, cfg, { secretKey: "" });
97
+ const s = await m.createProcessorCheckoutSession(noKeyCfg, { amountMinor: 500, currency: "usd", successUrl: "https://x", cancelUrl: "https://y" });
98
+ assert.strictEqual(s.ok, false);
99
+ assert.strictEqual(s.reason, "stripe_secret_key_missing");
100
+ }
101
+ );
102
+
103
+ console.log("verify_checkout_processor: all hosted-checkout processor adapter checks passed");
104
+ }
105
+
106
+ main().catch((error) => {
107
+ console.error("verify_checkout_processor failed:", error && error.message ? error.message : error);
108
+ process.exitCode = 1;
109
+ });
@@ -1,177 +1,177 @@
1
- "use strict";
2
-
3
- // Verifies the generic external-commerce bridge (/v1/commerce/*): an upstream fiat front
4
- // mints internal units for a confirmed purchase, then prepares a delegated-spend
5
- // credential for the paying customer's agent. Covers: bearer fail-closed/forbidden,
6
- // grants mint the mapped units + are idempotent per event (no double-mint on retry),
7
- // unknown package -> 422, prepare-execution authorizes + issues a credential when funded
8
- // and refuses when under-funded, and state reports the balance.
9
-
10
- const assert = require("assert");
11
- const http = require("http");
12
-
13
- async function req(baseUrl, method, path, body, headers = {}) {
14
- const res = await fetch(`${baseUrl}${path}`, {
15
- method,
16
- headers: { "content-type": "application/json", ...headers },
17
- ...(body !== undefined ? { body: JSON.stringify(body) } : {})
18
- });
19
- let json = null;
20
- try { json = await res.json(); } catch (_) { json = null; }
21
- return { status: res.status, json };
22
- }
23
-
24
- async function main() {
25
- const prev = {
26
- token: process.env.XYTARA_COMMERCE_BRIDGE_BEARER_TOKEN,
27
- packMap: process.env.XYTARA_COMMERCE_PACK_MAP_JSON
28
- };
29
- const TOKEN = "test-commerce-bridge-token";
30
- process.env.XYTARA_COMMERCE_BRIDGE_BEARER_TOKEN = TOKEN;
31
- process.env.XYTARA_COMMERCE_PACK_MAP_JSON = JSON.stringify({ pkg_starter: 100, pkg_pro: 500 });
32
- const auth = { authorization: `Bearer ${TOKEN}` };
33
-
34
- delete require.cache[require.resolve("../server")];
35
- const { createServer } = require("../server");
36
- const server = createServer();
37
- await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
38
- const baseUrl = `http://127.0.0.1:${server.address().port}`;
39
- const ACCOUNT = "acct_commerce_test";
40
-
41
- try {
42
- // 1. Auth mandatory: no bearer / wrong bearer -> 403.
43
- let r = await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT, packageRef: "pkg_starter", eventRef: "evt_1" });
44
- assert.strictEqual(r.status, 403, "grants without bearer must 403");
45
- r = await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT, packageRef: "pkg_starter", eventRef: "evt_1" }, { authorization: "Bearer wrong" });
46
- assert.strictEqual(r.status, 403, "grants with wrong bearer must 403");
47
-
48
- // 2. Grant mints the mapped units; response carries the upstream-read fields.
49
- r = await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT, packageRef: "pkg_starter", purchaseRef: "ord_1", providerRef: "txn_1", eventRef: "evt_1", orderType: "one_time", metadata: { checkoutSurface: "public_checkout" } }, auth);
50
- assert.strictEqual(r.status, 200, "grant must 200");
51
- assert.strictEqual(r.json.ok, true, "grant ok");
52
- assert.strictEqual(r.json.units, 100, "mapped units minted");
53
- assert.ok(typeof r.json.grantId === "string" && r.json.grantId.length > 0, "grantId returned");
54
- assert.strictEqual(r.json.hiddenAccountRef, ACCOUNT, "hiddenAccountRef echoed");
55
-
56
- // 3. State reflects the balance.
57
- r = await req(baseUrl, "GET", `/v1/commerce/state?accountRef=${ACCOUNT}`, undefined, auth);
58
- assert.strictEqual(r.status, 200, "state 200");
59
- assert.strictEqual(r.json.availableUnits, 100, "balance = 100 after grant");
60
-
61
- // 4. Idempotent per event: a webhook retry (same eventRef) must NOT double-mint.
62
- r = await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT, packageRef: "pkg_starter", purchaseRef: "ord_1", providerRef: "txn_1", eventRef: "evt_1" }, auth);
63
- assert.strictEqual(r.status, 200, "replayed grant still 200");
64
- const afterReplay = (await req(baseUrl, "GET", `/v1/commerce/state?accountRef=${ACCOUNT}`, undefined, auth)).json.availableUnits;
65
- assert.strictEqual(afterReplay, 100, "replay must not double-mint (still 100)");
66
-
67
- // 5. Unknown package -> 422.
68
- r = await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT, packageRef: "pkg_does_not_exist", eventRef: "evt_2" }, auth);
69
- assert.strictEqual(r.status, 422, "unknown package must 422");
70
- assert.strictEqual(r.json.reason, "unknown_package", "422 reason");
71
-
72
- // 6. prepare-execution authorizes + issues a BOUND delegated-spend credential.
73
- r = await req(baseUrl, "POST", "/v1/commerce/prepare-execution", { accountRef: ACCOUNT, featureRef: "doc.analyze", quotedUnits: 40 }, auth);
74
- assert.strictEqual(r.status, 200, "prepare 200");
75
- assert.strictEqual(r.json.authorized, true, "authorized when funded");
76
- assert.ok(typeof r.json.spendAuthorityRef === "string" && r.json.spendAuthorityRef.length > 0, "spendAuthorityRef (binding) issued");
77
- assert.ok(typeof r.json.spendAuthorityToken === "string" && r.json.spendAuthorityToken.length > 0, "agent-presentable token issued");
78
- assert.strictEqual(r.json.boundUnits, 40, "credential is BOUND to the quote (not the whole balance)");
79
- assert.ok(typeof r.json.validUntil === "string", "validUntil set");
80
- assert.ok(Array.isArray(r.json.capabilitySummary) && r.json.capabilitySummary.includes("doc.analyze"), "capability summarized");
81
-
82
- // 6b. A positive quote is required (an unbounded authorization is never issued).
83
- r = await req(baseUrl, "POST", "/v1/commerce/prepare-execution", { accountRef: ACCOUNT, featureRef: "doc.analyze" }, auth);
84
- assert.strictEqual(r.json.authorized, false, "no quote -> not authorized");
85
- assert.strictEqual(r.json.authorityDecision, "quoted_units_required", "quote-required decision");
86
-
87
- // 6c. A malformed agent key REFUSES rather than silently downgrading to bearer-only.
88
- r = await req(baseUrl, "POST", "/v1/commerce/prepare-execution", { accountRef: ACCOUNT, featureRef: "doc.analyze", quotedUnits: 10, delegatedSpend: { agent_public_key: "not-a-real-key" } }, auth);
89
- assert.strictEqual(r.json.authorized, false, "invalid agent key -> refuse");
90
- assert.strictEqual(r.json.authorityDecision, "invalid_agent_key", "invalid_agent_key decision");
91
-
92
- // 7. prepare-execution refuses when the quote exceeds the balance.
93
- r = await req(baseUrl, "POST", "/v1/commerce/prepare-execution", { accountRef: ACCOUNT, featureRef: "doc.analyze", quotedUnits: 999999 }, auth);
94
- assert.strictEqual(r.status, 200, "under-funded prepare still 200 (soft decision)");
95
- assert.strictEqual(r.json.authorized, false, "not authorized when under-funded");
96
- assert.strictEqual(r.json.authorityDecision, "insufficient_units", "decision surfaced");
97
- assert.strictEqual(r.json.spendAuthorityRef, null, "no credential issued when refused");
98
-
99
- // 8. account_ref required.
100
- r = await req(baseUrl, "POST", "/v1/commerce/grants", { packageRef: "pkg_starter", eventRef: "evt_3" }, auth);
101
- assert.strictEqual(r.status, 400, "missing accountRef must 400");
102
- assert.strictEqual(r.json.reason, "account_ref_required", "400 reason");
103
-
104
- // 8b. A SHARED eventRef across two accounts must NOT collide (each account minted).
105
- const ACCOUNT_B = "acct_commerce_test_B";
106
- await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT, packageRef: "pkg_pro", eventRef: "shared_evt" }, auth);
107
- await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT_B, packageRef: "pkg_pro", eventRef: "shared_evt" }, auth);
108
- const balB = (await req(baseUrl, "GET", `/v1/commerce/state?accountRef=${ACCOUNT_B}`, undefined, auth)).json.availableUnits;
109
- assert.strictEqual(balB, 500, "account B minted its own units despite sharing eventRef with A (no grant_id collision)");
110
-
111
- // 9. ENFORCEMENT: a credential bounded to a quote must actually be bounded at SPEND
112
- // time on the consume route, and cannot target another account.
113
- const prep = await req(baseUrl, "POST", "/v1/commerce/prepare-execution", { accountRef: ACCOUNT, featureRef: "doc.analyze", quotedUnits: 40 }, auth);
114
- const spendToken = prep.json.spendAuthorityToken;
115
- assert.ok(spendToken && spendToken.startsWith("xytara_spend_"), "prepare issued a delegated spend token");
116
- const spendAuth = { authorization: `Bearer ${spendToken}` };
117
-
118
- // 9a. Over-quota spend is REJECTED (the quote is enforced, not just echoed).
119
- let sp = await req(baseUrl, "POST", "/v1/economics/rails/credits/consume", { account_id: ACCOUNT, units: 50, task_ref: "doc.analyze" }, spendAuth);
120
- assert.strictEqual(sp.status, 403, "consuming > quoted units must be rejected (bound enforced on consume)");
121
- assert.strictEqual(sp.json.reason, "authority_consequence_bound_exceeded", "bound-exceeded reason");
122
-
123
- // 9b. Targeting ANOTHER account with this credential is REJECTED (account pinned).
124
- sp = await req(baseUrl, "POST", "/v1/economics/rails/credits/consume", { account_id: ACCOUNT_B, units: 10, task_ref: "doc.analyze" }, spendAuth);
125
- assert.strictEqual(sp.status, 403, "a credential for account A cannot spend account B");
126
- assert.strictEqual(sp.json.reason, "delegated_account_mismatch", "account-mismatch reason");
127
-
128
- // 9c. A within-quote spend on the OWN account SUCCEEDS.
129
- sp = await req(baseUrl, "POST", "/v1/economics/rails/credits/consume", { account_id: ACCOUNT, units: 30, task_ref: "doc.analyze" }, spendAuth);
130
- assert.strictEqual(sp.status, 200, `within-quote spend must succeed (got ${sp.status}: ${JSON.stringify(sp.json).slice(0,160)})`);
131
-
132
- // 10. SETTLE: trusted server-side consumption via the commerce bearer (the upstream
133
- // seam burns units after executing paid work; no delegated token involved).
134
- const ACCOUNT_S = "acct_commerce_settle";
135
- await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT_S, packageRef: "pkg_pro", eventRef: "evt_settle" }, auth); // +500
136
- // 10a. Settle requires the commerce bearer.
137
- r = await req(baseUrl, "POST", "/v1/commerce/settle", { accountRef: ACCOUNT_S, units: 10 });
138
- assert.strictEqual(r.status, 403, "settle without bearer must 403");
139
- // 10b. account_ref + a positive unit count are required.
140
- r = await req(baseUrl, "POST", "/v1/commerce/settle", { units: 10 }, auth);
141
- assert.strictEqual(r.status, 400, "settle without accountRef must 400");
142
- assert.strictEqual(r.json.reason, "account_ref_required", "settle 400 reason");
143
- r = await req(baseUrl, "POST", "/v1/commerce/settle", { accountRef: ACCOUNT_S, units: 0 }, auth);
144
- assert.strictEqual(r.status, 400, "settle without positive units must 400");
145
- assert.strictEqual(r.json.reason, "units_required", "settle units reason");
146
- // 10c. Over-balance settle is refused (balance is the hard cap).
147
- r = await req(baseUrl, "POST", "/v1/commerce/settle", { accountRef: ACCOUNT_S, units: 999999, taskRef: "doc.analyze" }, auth);
148
- assert.strictEqual(r.status, 409, "over-balance settle must 409");
149
- assert.strictEqual(r.json.reason, "insufficient_credits", "settle insufficient reason");
150
- // 10d. Within-balance settle burns units and returns the credit_spend/usage_meter shape.
151
- r = await req(baseUrl, "POST", "/v1/commerce/settle", { accountRef: ACCOUNT_S, units: 200, taskRef: "doc.analyze", transactionRef: "txn_settle_1" }, auth);
152
- assert.strictEqual(r.status, 200, `within-balance settle must 200 (got ${r.status}: ${JSON.stringify(r.json).slice(0,160)})`);
153
- assert.strictEqual(r.json.ok, true, "settle ok");
154
- assert.ok(r.json.credit_spend && typeof r.json.credit_spend.credit_spend_id === "string", "settle returns credit_spend");
155
- assert.ok(typeof r.json.credit_spend.usage_meter_id === "string", "settle returns usage_meter_id");
156
- const afterSettle = (await req(baseUrl, "GET", `/v1/commerce/state?accountRef=${ACCOUNT_S}`, undefined, auth)).json.availableUnits;
157
- assert.strictEqual(afterSettle, 300, "settle burned 200 of 500 (300 left)");
158
-
159
- // 9. Fail-closed when the bridge token is unset.
160
- delete process.env.XYTARA_COMMERCE_BRIDGE_BEARER_TOKEN;
161
- r = await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT, packageRef: "pkg_starter", eventRef: "evt_4" }, auth);
162
- assert.strictEqual(r.status, 403, "unset token must fail closed (403)");
163
- assert.strictEqual(r.json.reason, "commerce_bridge_auth_not_configured", "fail-closed reason");
164
-
165
- console.log("xytara verify:commerce-bridge passed");
166
- } finally {
167
- await new Promise((resolve) => server.close(resolve));
168
- const restore = (k, v) => { if (v === undefined) delete process.env[k]; else process.env[k] = v; };
169
- restore("XYTARA_COMMERCE_BRIDGE_BEARER_TOKEN", prev.token);
170
- restore("XYTARA_COMMERCE_PACK_MAP_JSON", prev.packMap);
171
- }
172
- }
173
-
174
- main().catch((error) => {
175
- console.error(error && error.message ? error.message : error);
176
- process.exitCode = 1;
177
- });
1
+ "use strict";
2
+
3
+ // Verifies the generic external-commerce bridge (/v1/commerce/*): an upstream fiat front
4
+ // mints internal units for a confirmed purchase, then prepares a delegated-spend
5
+ // credential for the paying customer's agent. Covers: bearer fail-closed/forbidden,
6
+ // grants mint the mapped units + are idempotent per event (no double-mint on retry),
7
+ // unknown package -> 422, prepare-execution authorizes + issues a credential when funded
8
+ // and refuses when under-funded, and state reports the balance.
9
+
10
+ const assert = require("assert");
11
+ const http = require("http");
12
+
13
+ async function req(baseUrl, method, path, body, headers = {}) {
14
+ const res = await fetch(`${baseUrl}${path}`, {
15
+ method,
16
+ headers: { "content-type": "application/json", ...headers },
17
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {})
18
+ });
19
+ let json = null;
20
+ try { json = await res.json(); } catch (_) { json = null; }
21
+ return { status: res.status, json };
22
+ }
23
+
24
+ async function main() {
25
+ const prev = {
26
+ token: process.env.XYTARA_COMMERCE_BRIDGE_BEARER_TOKEN,
27
+ packMap: process.env.XYTARA_COMMERCE_PACK_MAP_JSON
28
+ };
29
+ const TOKEN = "test-commerce-bridge-token";
30
+ process.env.XYTARA_COMMERCE_BRIDGE_BEARER_TOKEN = TOKEN;
31
+ process.env.XYTARA_COMMERCE_PACK_MAP_JSON = JSON.stringify({ pkg_starter: 100, pkg_pro: 500 });
32
+ const auth = { authorization: `Bearer ${TOKEN}` };
33
+
34
+ delete require.cache[require.resolve("../server")];
35
+ const { createServer } = require("../server");
36
+ const server = createServer();
37
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
38
+ const baseUrl = `http://127.0.0.1:${server.address().port}`;
39
+ const ACCOUNT = "acct_commerce_test";
40
+
41
+ try {
42
+ // 1. Auth mandatory: no bearer / wrong bearer -> 403.
43
+ let r = await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT, packageRef: "pkg_starter", eventRef: "evt_1" });
44
+ assert.strictEqual(r.status, 403, "grants without bearer must 403");
45
+ r = await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT, packageRef: "pkg_starter", eventRef: "evt_1" }, { authorization: "Bearer wrong" });
46
+ assert.strictEqual(r.status, 403, "grants with wrong bearer must 403");
47
+
48
+ // 2. Grant mints the mapped units; response carries the upstream-read fields.
49
+ r = await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT, packageRef: "pkg_starter", purchaseRef: "ord_1", providerRef: "txn_1", eventRef: "evt_1", orderType: "one_time", metadata: { checkoutSurface: "public_checkout" } }, auth);
50
+ assert.strictEqual(r.status, 200, "grant must 200");
51
+ assert.strictEqual(r.json.ok, true, "grant ok");
52
+ assert.strictEqual(r.json.units, 100, "mapped units minted");
53
+ assert.ok(typeof r.json.grantId === "string" && r.json.grantId.length > 0, "grantId returned");
54
+ assert.strictEqual(r.json.hiddenAccountRef, ACCOUNT, "hiddenAccountRef echoed");
55
+
56
+ // 3. State reflects the balance.
57
+ r = await req(baseUrl, "GET", `/v1/commerce/state?accountRef=${ACCOUNT}`, undefined, auth);
58
+ assert.strictEqual(r.status, 200, "state 200");
59
+ assert.strictEqual(r.json.availableUnits, 100, "balance = 100 after grant");
60
+
61
+ // 4. Idempotent per event: a webhook retry (same eventRef) must NOT double-mint.
62
+ r = await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT, packageRef: "pkg_starter", purchaseRef: "ord_1", providerRef: "txn_1", eventRef: "evt_1" }, auth);
63
+ assert.strictEqual(r.status, 200, "replayed grant still 200");
64
+ const afterReplay = (await req(baseUrl, "GET", `/v1/commerce/state?accountRef=${ACCOUNT}`, undefined, auth)).json.availableUnits;
65
+ assert.strictEqual(afterReplay, 100, "replay must not double-mint (still 100)");
66
+
67
+ // 5. Unknown package -> 422.
68
+ r = await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT, packageRef: "pkg_does_not_exist", eventRef: "evt_2" }, auth);
69
+ assert.strictEqual(r.status, 422, "unknown package must 422");
70
+ assert.strictEqual(r.json.reason, "unknown_package", "422 reason");
71
+
72
+ // 6. prepare-execution authorizes + issues a BOUND delegated-spend credential.
73
+ r = await req(baseUrl, "POST", "/v1/commerce/prepare-execution", { accountRef: ACCOUNT, featureRef: "doc.analyze", quotedUnits: 40 }, auth);
74
+ assert.strictEqual(r.status, 200, "prepare 200");
75
+ assert.strictEqual(r.json.authorized, true, "authorized when funded");
76
+ assert.ok(typeof r.json.spendAuthorityRef === "string" && r.json.spendAuthorityRef.length > 0, "spendAuthorityRef (binding) issued");
77
+ assert.ok(typeof r.json.spendAuthorityToken === "string" && r.json.spendAuthorityToken.length > 0, "agent-presentable token issued");
78
+ assert.strictEqual(r.json.boundUnits, 40, "credential is BOUND to the quote (not the whole balance)");
79
+ assert.ok(typeof r.json.validUntil === "string", "validUntil set");
80
+ assert.ok(Array.isArray(r.json.capabilitySummary) && r.json.capabilitySummary.includes("doc.analyze"), "capability summarized");
81
+
82
+ // 6b. A positive quote is required (an unbounded authorization is never issued).
83
+ r = await req(baseUrl, "POST", "/v1/commerce/prepare-execution", { accountRef: ACCOUNT, featureRef: "doc.analyze" }, auth);
84
+ assert.strictEqual(r.json.authorized, false, "no quote -> not authorized");
85
+ assert.strictEqual(r.json.authorityDecision, "quoted_units_required", "quote-required decision");
86
+
87
+ // 6c. A malformed agent key REFUSES rather than silently downgrading to bearer-only.
88
+ r = await req(baseUrl, "POST", "/v1/commerce/prepare-execution", { accountRef: ACCOUNT, featureRef: "doc.analyze", quotedUnits: 10, delegatedSpend: { agent_public_key: "not-a-real-key" } }, auth);
89
+ assert.strictEqual(r.json.authorized, false, "invalid agent key -> refuse");
90
+ assert.strictEqual(r.json.authorityDecision, "invalid_agent_key", "invalid_agent_key decision");
91
+
92
+ // 7. prepare-execution refuses when the quote exceeds the balance.
93
+ r = await req(baseUrl, "POST", "/v1/commerce/prepare-execution", { accountRef: ACCOUNT, featureRef: "doc.analyze", quotedUnits: 999999 }, auth);
94
+ assert.strictEqual(r.status, 200, "under-funded prepare still 200 (soft decision)");
95
+ assert.strictEqual(r.json.authorized, false, "not authorized when under-funded");
96
+ assert.strictEqual(r.json.authorityDecision, "insufficient_units", "decision surfaced");
97
+ assert.strictEqual(r.json.spendAuthorityRef, null, "no credential issued when refused");
98
+
99
+ // 8. account_ref required.
100
+ r = await req(baseUrl, "POST", "/v1/commerce/grants", { packageRef: "pkg_starter", eventRef: "evt_3" }, auth);
101
+ assert.strictEqual(r.status, 400, "missing accountRef must 400");
102
+ assert.strictEqual(r.json.reason, "account_ref_required", "400 reason");
103
+
104
+ // 8b. A SHARED eventRef across two accounts must NOT collide (each account minted).
105
+ const ACCOUNT_B = "acct_commerce_test_B";
106
+ await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT, packageRef: "pkg_pro", eventRef: "shared_evt" }, auth);
107
+ await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT_B, packageRef: "pkg_pro", eventRef: "shared_evt" }, auth);
108
+ const balB = (await req(baseUrl, "GET", `/v1/commerce/state?accountRef=${ACCOUNT_B}`, undefined, auth)).json.availableUnits;
109
+ assert.strictEqual(balB, 500, "account B minted its own units despite sharing eventRef with A (no grant_id collision)");
110
+
111
+ // 9. ENFORCEMENT: a credential bounded to a quote must actually be bounded at SPEND
112
+ // time on the consume route, and cannot target another account.
113
+ const prep = await req(baseUrl, "POST", "/v1/commerce/prepare-execution", { accountRef: ACCOUNT, featureRef: "doc.analyze", quotedUnits: 40 }, auth);
114
+ const spendToken = prep.json.spendAuthorityToken;
115
+ assert.ok(spendToken && spendToken.startsWith("xytara_spend_"), "prepare issued a delegated spend token");
116
+ const spendAuth = { authorization: `Bearer ${spendToken}` };
117
+
118
+ // 9a. Over-quota spend is REJECTED (the quote is enforced, not just echoed).
119
+ let sp = await req(baseUrl, "POST", "/v1/economics/rails/credits/consume", { account_id: ACCOUNT, units: 50, task_ref: "doc.analyze" }, spendAuth);
120
+ assert.strictEqual(sp.status, 403, "consuming > quoted units must be rejected (bound enforced on consume)");
121
+ assert.strictEqual(sp.json.reason, "authority_consequence_bound_exceeded", "bound-exceeded reason");
122
+
123
+ // 9b. Targeting ANOTHER account with this credential is REJECTED (account pinned).
124
+ sp = await req(baseUrl, "POST", "/v1/economics/rails/credits/consume", { account_id: ACCOUNT_B, units: 10, task_ref: "doc.analyze" }, spendAuth);
125
+ assert.strictEqual(sp.status, 403, "a credential for account A cannot spend account B");
126
+ assert.strictEqual(sp.json.reason, "delegated_account_mismatch", "account-mismatch reason");
127
+
128
+ // 9c. A within-quote spend on the OWN account SUCCEEDS.
129
+ sp = await req(baseUrl, "POST", "/v1/economics/rails/credits/consume", { account_id: ACCOUNT, units: 30, task_ref: "doc.analyze" }, spendAuth);
130
+ assert.strictEqual(sp.status, 200, `within-quote spend must succeed (got ${sp.status}: ${JSON.stringify(sp.json).slice(0,160)})`);
131
+
132
+ // 10. SETTLE: trusted server-side consumption via the commerce bearer (the upstream
133
+ // seam burns units after executing paid work; no delegated token involved).
134
+ const ACCOUNT_S = "acct_commerce_settle";
135
+ await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT_S, packageRef: "pkg_pro", eventRef: "evt_settle" }, auth); // +500
136
+ // 10a. Settle requires the commerce bearer.
137
+ r = await req(baseUrl, "POST", "/v1/commerce/settle", { accountRef: ACCOUNT_S, units: 10 });
138
+ assert.strictEqual(r.status, 403, "settle without bearer must 403");
139
+ // 10b. account_ref + a positive unit count are required.
140
+ r = await req(baseUrl, "POST", "/v1/commerce/settle", { units: 10 }, auth);
141
+ assert.strictEqual(r.status, 400, "settle without accountRef must 400");
142
+ assert.strictEqual(r.json.reason, "account_ref_required", "settle 400 reason");
143
+ r = await req(baseUrl, "POST", "/v1/commerce/settle", { accountRef: ACCOUNT_S, units: 0 }, auth);
144
+ assert.strictEqual(r.status, 400, "settle without positive units must 400");
145
+ assert.strictEqual(r.json.reason, "units_required", "settle units reason");
146
+ // 10c. Over-balance settle is refused (balance is the hard cap).
147
+ r = await req(baseUrl, "POST", "/v1/commerce/settle", { accountRef: ACCOUNT_S, units: 999999, taskRef: "doc.analyze" }, auth);
148
+ assert.strictEqual(r.status, 409, "over-balance settle must 409");
149
+ assert.strictEqual(r.json.reason, "insufficient_credits", "settle insufficient reason");
150
+ // 10d. Within-balance settle burns units and returns the credit_spend/usage_meter shape.
151
+ r = await req(baseUrl, "POST", "/v1/commerce/settle", { accountRef: ACCOUNT_S, units: 200, taskRef: "doc.analyze", transactionRef: "txn_settle_1" }, auth);
152
+ assert.strictEqual(r.status, 200, `within-balance settle must 200 (got ${r.status}: ${JSON.stringify(r.json).slice(0,160)})`);
153
+ assert.strictEqual(r.json.ok, true, "settle ok");
154
+ assert.ok(r.json.credit_spend && typeof r.json.credit_spend.credit_spend_id === "string", "settle returns credit_spend");
155
+ assert.ok(typeof r.json.credit_spend.usage_meter_id === "string", "settle returns usage_meter_id");
156
+ const afterSettle = (await req(baseUrl, "GET", `/v1/commerce/state?accountRef=${ACCOUNT_S}`, undefined, auth)).json.availableUnits;
157
+ assert.strictEqual(afterSettle, 300, "settle burned 200 of 500 (300 left)");
158
+
159
+ // 9. Fail-closed when the bridge token is unset.
160
+ delete process.env.XYTARA_COMMERCE_BRIDGE_BEARER_TOKEN;
161
+ r = await req(baseUrl, "POST", "/v1/commerce/grants", { accountRef: ACCOUNT, packageRef: "pkg_starter", eventRef: "evt_4" }, auth);
162
+ assert.strictEqual(r.status, 403, "unset token must fail closed (403)");
163
+ assert.strictEqual(r.json.reason, "commerce_bridge_auth_not_configured", "fail-closed reason");
164
+
165
+ console.log("xytara verify:commerce-bridge passed");
166
+ } finally {
167
+ await new Promise((resolve) => server.close(resolve));
168
+ const restore = (k, v) => { if (v === undefined) delete process.env[k]; else process.env[k] = v; };
169
+ restore("XYTARA_COMMERCE_BRIDGE_BEARER_TOKEN", prev.token);
170
+ restore("XYTARA_COMMERCE_PACK_MAP_JSON", prev.packMap);
171
+ }
172
+ }
173
+
174
+ main().catch((error) => {
175
+ console.error(error && error.message ? error.message : error);
176
+ process.exitCode = 1;
177
+ });