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.
- package/.env.example +38 -4
- package/BSV_TERANODE_PLATFORM_DISCONTINUITY_RUNBOOK.md +126 -126
- package/REAL_PAYMENT_SETUP.md +14 -7
- package/RELEASE_NOTES.md +9 -4
- package/fixtures/catalog.js +22 -0
- package/lib/a2a_rpc.js +287 -280
- package/lib/account_auth.js +585 -585
- package/lib/agent_identity.js +162 -162
- package/lib/asyncapi_contract.js +3 -3
- package/lib/bolt_adapter.js +187 -187
- package/lib/bolt_payment_front.js +224 -224
- package/lib/buyer_event_log.js +215 -0
- package/lib/buyer_safety_policy.js +132 -0
- package/lib/checkout_processor_adapter.js +217 -217
- package/lib/command_flow.js +911 -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 +99 -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/openapi_contract.js +4 -0
- package/lib/payment_config.js +257 -253
- package/lib/payment_verification.js +357 -357
- package/lib/provider_payout_execution.js +155 -155
- package/lib/provider_registry.js +852 -842
- package/lib/rate_limiter.js +81 -81
- package/lib/runtime_state_store.js +495 -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 +198 -191
- package/lib/x402_lane_contract.js +5 -3
- package/lib/x402_settlement_recovery.js +175 -0
- package/package.json +9 -3
- package/scripts/buyer_events_daily_report.js +79 -0
- package/scripts/state_backup_offsite.js +196 -0
- package/scripts/verify_a2a_rpc.js +131 -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_buyer_event_log.js +123 -0
- package/scripts/verify_buyer_safety.js +141 -0
- 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_lifecycle_spend_binding.js +88 -0
- 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 +209 -208
- package/scripts/verify_x402_facilitator.js +307 -286
- package/scripts/verify_x402_settlement_recovery.js +153 -0
- 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
|
+
});
|