xytara 3.1.13 → 3.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -1
- package/BSV_TERANODE_PLATFORM_DISCONTINUITY_RUNBOOK.md +126 -126
- package/RELEASE_NOTES.md +6 -4
- package/lib/a2a_rpc.js +280 -280
- package/lib/account_auth.js +585 -585
- package/lib/agent_identity.js +162 -162
- package/lib/bolt_adapter.js +187 -187
- package/lib/bolt_payment_front.js +224 -224
- package/lib/checkout_processor_adapter.js +217 -217
- package/lib/command_flow.js +722 -722
- package/lib/commerce_artifacts.js +591 -591
- package/lib/commerce_bridge.js +274 -274
- package/lib/commerce_cases.js +399 -399
- package/lib/commerce_client.js +7694 -7694
- package/lib/commerce_quotes.js +29 -0
- package/lib/commerce_runtime.js +322 -322
- package/lib/execution_dedupe.js +86 -86
- package/lib/external_credit_bridge.js +142 -142
- package/lib/http_transport.js +81 -81
- package/lib/l402_adapter.js +241 -241
- package/lib/l402_payment_front.js +265 -265
- package/lib/mpp_adapter.js +237 -237
- package/lib/payment_config.js +253 -253
- package/lib/payment_verification.js +357 -357
- package/lib/provider_payout_execution.js +155 -155
- package/lib/provider_registry.js +842 -842
- package/lib/rate_limiter.js +81 -81
- package/lib/runtime_state_store.js +491 -369
- package/lib/settlement_bsv_live.js +600 -600
- package/lib/settlement_bsv_teranode.js +117 -117
- package/lib/stripe_mpp_payment_front.js +258 -258
- package/lib/x402_facilitator.js +191 -191
- package/package.json +5 -3
- package/scripts/state_backup_offsite.js +196 -0
- package/scripts/verify_a2a_rpc.js +105 -105
- package/scripts/verify_agent_identity.js +153 -153
- package/scripts/verify_all.js +1 -1
- package/scripts/verify_bolt_adapter.js +128 -128
- package/scripts/verify_cdp_facilitator.js +110 -110
- package/scripts/verify_checkout_processor.js +109 -109
- package/scripts/verify_commerce_bridge.js +177 -177
- package/scripts/verify_execution_dedupe.js +189 -189
- package/scripts/verify_execution_surfacing.js +76 -76
- package/scripts/verify_final_activation.js +191 -191
- package/scripts/verify_l402_adapter.js +86 -86
- package/scripts/verify_mpp_adapter.js +128 -128
- package/scripts/verify_production_readiness.js +268 -268
- package/scripts/verify_rate_limiter.js +52 -52
- package/scripts/verify_refund_credit.js +106 -106
- package/scripts/verify_release_candidate.js +81 -81
- package/scripts/verify_runtime_state_guard.js +117 -0
- package/scripts/verify_service.js +15421 -15421
- package/scripts/verify_verify_execute_settle.js +208 -208
- package/scripts/verify_x402_facilitator.js +286 -286
- package/server.js +11976 -11974
package/lib/command_flow.js
CHANGED
|
@@ -1,722 +1,722 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const {
|
|
4
|
-
createCommandResult,
|
|
5
|
-
createQuote,
|
|
6
|
-
hydrateQuoteStates
|
|
7
|
-
} = require("./commerce_runtime");
|
|
8
|
-
const { consumeAccountCredits, buildUsageMeterPreview, previewCreditSpend } = require("./commerce_economics");
|
|
9
|
-
const { resolveSpendCredential, evaluateAuthorityBindingAction } = require("./account_auth");
|
|
10
|
-
const { validateIntegrationSelection } = require("../integrations/dispatch");
|
|
11
|
-
const { markPaymentAccepted, settleDeferredPayment, verifyPaymentAcceptance } = require("./payment_verification");
|
|
12
|
-
const { executeExternalTarget, resolveExecutionTarget } = require("./external_execution_runtime");
|
|
13
|
-
const { externalExecutionBudgetMs } = require("./external_execution_policy");
|
|
14
|
-
const { hashCanonical, issueDeliveryReceipt, loadDeliveryReceiptSigners } = require("./delivery_receipt");
|
|
15
|
-
|
|
16
|
-
const DELIVERY_RECEIPT_ISSUER = "xytara-runtime";
|
|
17
|
-
const DELIVERY_RECEIPT_RETENTION = 5000;
|
|
18
|
-
|
|
19
|
-
// The payment leg of a delivery receipt: on-chain facts for x402, the spend record for credits,
|
|
20
|
-
// and the verification mode for any other lane. Never the buyer's signature or secrets.
|
|
21
|
-
function deliveryReceiptPayment(verification, quote) {
|
|
22
|
-
const v = verification && typeof verification === "object" ? verification : {};
|
|
23
|
-
const paymentPayload = v.payment_payload && typeof v.payment_payload === "object" ? v.payment_payload : {};
|
|
24
|
-
if (v.verification_mode === "x402_facilitator") {
|
|
25
|
-
const settlement = v.settlement && typeof v.settlement === "object" ? v.settlement : null;
|
|
26
|
-
return {
|
|
27
|
-
protocol: "x402",
|
|
28
|
-
scheme: "exact",
|
|
29
|
-
network: paymentPayload.x402_network || (settlement && settlement.network) || null,
|
|
30
|
-
asset: paymentPayload.x402_asset || null,
|
|
31
|
-
amount: paymentPayload.x402_amount !== undefined && paymentPayload.x402_amount !== null ? String(paymentPayload.x402_amount) : null,
|
|
32
|
-
payer: paymentPayload.x402_payer || (settlement && settlement.payer) || null,
|
|
33
|
-
pay_to: paymentPayload.x402_pay_to || null,
|
|
34
|
-
settlement_tx: settlement ? (settlement.transaction || settlement.txid || null) : null,
|
|
35
|
-
settled: Boolean(settlement && settlement.success !== false && (settlement.transaction || settlement.txid))
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
if (v.verification_mode === "account_credits" || paymentPayload.credit_spend_id) {
|
|
39
|
-
return {
|
|
40
|
-
protocol: "account_credits",
|
|
41
|
-
credit_spend_id: paymentPayload.credit_spend_id || null,
|
|
42
|
-
quoted_amount_minor: quote && Number.isFinite(Number(quote.amount_minor)) ? Number(quote.amount_minor) : null,
|
|
43
|
-
currency: (quote && quote.currency) || null
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
protocol: v.verification_mode || "unspecified",
|
|
48
|
-
quoted_amount_minor: quote && Number.isFinite(Number(quote.amount_minor)) ? Number(quote.amount_minor) : null,
|
|
49
|
-
currency: (quote && quote.currency) || null
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Issues the signed delivery receipt for a completed paid call, attaches it to the response, and
|
|
54
|
-
// keeps it retrievable by id (bounded). Fail-safe: a receipt problem never breaks a completed call.
|
|
55
|
-
function attachDeliveryReceipt(state, result, payload, quote, verification, externalExecutionOptions) {
|
|
56
|
-
try {
|
|
57
|
-
if (!result || !result.receipt) return;
|
|
58
|
-
const execution = externalExecutionOptions && externalExecutionOptions.externalExecution
|
|
59
|
-
? externalExecutionOptions.externalExecution
|
|
60
|
-
: null;
|
|
61
|
-
const deliverables = result.command && Array.isArray(result.command.deliverables) ? result.command.deliverables : [];
|
|
62
|
-
const outputs = deliverables.map((deliverable) => (deliverable && deliverable.output_payload !== undefined ? deliverable.output_payload : null));
|
|
63
|
-
const body = payload && payload.body && typeof payload.body === "object" && !Array.isArray(payload.body) ? payload.body : {};
|
|
64
|
-
const taskRef = (payload && payload.task_ref)
|
|
65
|
-
|| (Array.isArray(payload && payload.tasks) && payload.tasks[0] && payload.tasks[0].task_ref)
|
|
66
|
-
|| null;
|
|
67
|
-
const { signers } = loadDeliveryReceiptSigners("XYTARA_DELIVERY_RECEIPT");
|
|
68
|
-
const deliveryReceipt = issueDeliveryReceipt({
|
|
69
|
-
receipt_id: `drcpt_${result.receipt.receipt_id.replace(/^rcpt_/, "")}`,
|
|
70
|
-
issuer: DELIVERY_RECEIPT_ISSUER,
|
|
71
|
-
call: {
|
|
72
|
-
quote_id: quote && quote.quote_id ? quote.quote_id : null,
|
|
73
|
-
transaction_id: result.transaction ? result.transaction.transaction_id : null,
|
|
74
|
-
runtime_receipt_id: result.receipt.receipt_id,
|
|
75
|
-
task_ref: taskRef,
|
|
76
|
-
tool_name: typeof body.tool_name === "string" ? body.tool_name : null
|
|
77
|
-
},
|
|
78
|
-
payment: deliveryReceiptPayment(verification, quote),
|
|
79
|
-
delivery: {
|
|
80
|
-
status: "completed",
|
|
81
|
-
provider_id: execution && execution.registered_provider_id ? execution.registered_provider_id : null,
|
|
82
|
-
routing: execution
|
|
83
|
-
? (execution.registered_provider_id ? "open_provider_registry" : "operator_env_target")
|
|
84
|
-
: "runtime_native",
|
|
85
|
-
execution_target_id: execution && execution.target_id ? execution.target_id : null,
|
|
86
|
-
request_sha256: hashCanonical(body),
|
|
87
|
-
request_hash_input: "the request `body` object (e.g. { tool_name, arguments })",
|
|
88
|
-
output_sha256: hashCanonical(outputs.length === 1 ? outputs[0] : outputs),
|
|
89
|
-
output_hash_input: outputs.length === 1
|
|
90
|
-
? "command.deliverables[0].output_payload (null when the task returns no payload)"
|
|
91
|
-
: "the array of command.deliverables[*].output_payload",
|
|
92
|
-
completed_at: (result.receipt.timing && result.receipt.timing.completed_at_iso) || result.receipt.created_at_iso || null
|
|
93
|
-
}
|
|
94
|
-
}, signers);
|
|
95
|
-
result.delivery_receipt = deliveryReceipt;
|
|
96
|
-
if (!(state.deliveryReceipts instanceof Map)) state.deliveryReceipts = new Map();
|
|
97
|
-
state.deliveryReceipts.set(deliveryReceipt.receipt_id, deliveryReceipt);
|
|
98
|
-
while (state.deliveryReceipts.size > DELIVERY_RECEIPT_RETENTION) {
|
|
99
|
-
state.deliveryReceipts.delete(state.deliveryReceipts.keys().next().value);
|
|
100
|
-
}
|
|
101
|
-
} catch (_) { /* non-fatal: never break a completed, possibly settled call */ }
|
|
102
|
-
}
|
|
103
|
-
const { resolveRegisteredTargets, hasActiveMatchingProvider, recordProviderHealth, recordProviderRouting, recordProviderSettlement } = require("./provider_registry");
|
|
104
|
-
const { claimDeferredExecution, releaseDeferredExecution } = require("./execution_dedupe");
|
|
105
|
-
|
|
106
|
-
function hasPaymentHeaders(headers, requiredHeaders) {
|
|
107
|
-
return requiredHeaders.every((headerName) => headers[headerName]);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function parseBearerToken(headers) {
|
|
111
|
-
const authorization = headers && typeof headers.authorization === "string"
|
|
112
|
-
? headers.authorization.trim()
|
|
113
|
-
: "";
|
|
114
|
-
const match = authorization.match(/^Bearer\s+(.+)$/i);
|
|
115
|
-
return match ? match[1].trim() : null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Extract an agent-identity proof (for key-bound spend credentials) from request
|
|
119
|
-
// headers: X-Agent-Signature (base64 ed25519 sig) + X-Agent-Timestamp (ms).
|
|
120
|
-
function extractAgentProof(headers) {
|
|
121
|
-
const h = headers || {};
|
|
122
|
-
return {
|
|
123
|
-
signature: typeof h["x-agent-signature"] === "string" ? h["x-agent-signature"] : null,
|
|
124
|
-
timestamp: typeof h["x-agent-timestamp"] === "string" ? h["x-agent-timestamp"] : null
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function buildCreditCommitPayload(payload, quote, requiredUnits) {
|
|
129
|
-
return {
|
|
130
|
-
account_id: quote.account_id,
|
|
131
|
-
agent_id: payload.agent_id || null,
|
|
132
|
-
budget_id: payload.budget_id || null,
|
|
133
|
-
pack_id: payload.pack_id || null,
|
|
134
|
-
entitlement_id: payload.entitlement_id || null,
|
|
135
|
-
quote_id: quote.quote_id,
|
|
136
|
-
task_ref: payload.task_ref || null,
|
|
137
|
-
workflow_ref: payload.workflow_ref || null,
|
|
138
|
-
units: requiredUnits,
|
|
139
|
-
reason: "credit_first_execution"
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function buildCreditRejectionResponse(creditDecision, quote) {
|
|
144
|
-
const failureReason = creditDecision && creditDecision.credit_spend_preview
|
|
145
|
-
? creditDecision.credit_spend_preview.failure_reason
|
|
146
|
-
: null;
|
|
147
|
-
const reason = failureReason
|
|
148
|
-
|| (creditDecision && creditDecision.policy_guard && creditDecision.policy_guard.reason)
|
|
149
|
-
|| (creditDecision && creditDecision.entitlement_guard && creditDecision.entitlement_guard.reason)
|
|
150
|
-
|| (creditDecision && creditDecision.reason)
|
|
151
|
-
|| "credit_spend_not_allowed";
|
|
152
|
-
|
|
153
|
-
if (reason === "insufficient_credits") {
|
|
154
|
-
const preview = creditDecision && creditDecision.credit_spend_preview ? creditDecision.credit_spend_preview : {};
|
|
155
|
-
return {
|
|
156
|
-
status: 402,
|
|
157
|
-
payload: {
|
|
158
|
-
ok: false,
|
|
159
|
-
reason: "insufficient_credits",
|
|
160
|
-
insufficient_credits: {
|
|
161
|
-
account_id: quote.account_id,
|
|
162
|
-
required_units: preview.required_units,
|
|
163
|
-
available_units: preview.available_units,
|
|
164
|
-
recommended_next_surfaces: {
|
|
165
|
-
credits_topup: "/v1/checkout/purchase-intents",
|
|
166
|
-
direct_payment: "/v1/payment-quotes"
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
quote
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
status: 403,
|
|
176
|
-
payload: {
|
|
177
|
-
ok: false,
|
|
178
|
-
reason,
|
|
179
|
-
account_id: quote.account_id,
|
|
180
|
-
policy_guard: creditDecision && creditDecision.policy_guard ? creditDecision.policy_guard : null,
|
|
181
|
-
entitlement_guard: creditDecision && creditDecision.entitlement_guard ? creditDecision.entitlement_guard : null,
|
|
182
|
-
quote
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// What a provider is allowed to see of the buyer's payment context: the verification outcome and
|
|
188
|
-
// scalar payment facts only. NEVER the signed x402 authorization / deferred-settlement material
|
|
189
|
-
// (that would hand a third-party provider the buyer's signature before settlement), and never
|
|
190
|
-
// signature-, secret- or raw-looking keys or nested objects from any lane's payload.
|
|
191
|
-
const SENSITIVE_CONTEXT_KEY = /signature|secret|private|authorization|raw|base64|header|token/i;
|
|
192
|
-
function sanitizeAuthorizationContextForProvider(context) {
|
|
193
|
-
if (!context || typeof context !== "object") return null;
|
|
194
|
-
const payload = context.payment_payload && typeof context.payment_payload === "object" ? context.payment_payload : null;
|
|
195
|
-
const safePayload = payload
|
|
196
|
-
? Object.fromEntries(Object.entries(payload).filter(([key, value]) => !SENSITIVE_CONTEXT_KEY.test(key) && (value === null || typeof value !== "object")))
|
|
197
|
-
: null;
|
|
198
|
-
return {
|
|
199
|
-
ok: context.ok === true,
|
|
200
|
-
verification_mode: context.verification_mode || null,
|
|
201
|
-
wallet_id: context.wallet_id || null,
|
|
202
|
-
age_seconds: Number.isFinite(Number(context.age_seconds)) ? Number(context.age_seconds) : null,
|
|
203
|
-
payment_payload: safePayload
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async function buildExternalExecutionOptions(state, payload, quote, authorizationContext) {
|
|
208
|
-
// One wall-clock budget for the whole call (every retry of every candidate), inside the x402
|
|
209
|
-
// authorization window — see external_execution_policy.js.
|
|
210
|
-
const executionOptions = { deadline_ms: Date.now() + externalExecutionBudgetMs() };
|
|
211
|
-
const forwardedContext = sanitizeAuthorizationContextForProvider(authorizationContext);
|
|
212
|
-
// Operator-pinned env target resolves first and is used as-is (an explicit single target,
|
|
213
|
-
// no failover). Empty env + empty registry -> {} (unchanged behavior).
|
|
214
|
-
const envTarget = resolveExecutionTarget(payload, quote);
|
|
215
|
-
if (envTarget) {
|
|
216
|
-
const execution = await executeExternalTarget(envTarget, payload, quote, forwardedContext, executionOptions);
|
|
217
|
-
if (!execution || !execution.ok) {
|
|
218
|
-
return { error: execution || { ok: false, status: 502, reason: "external_execution_failed" } };
|
|
219
|
-
}
|
|
220
|
-
return { externalExecution: execution };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Open-network path: try health-ranked registered providers (healthy first, then half-open)
|
|
224
|
-
// in order, FAILING OVER to the next when one is down — so a single unhealthy/throttled
|
|
225
|
-
// provider doesn't fail the buyer when another serves the same capability. Each attempt
|
|
226
|
-
// updates the provider's health; accounting is wrapped so it can never break execution.
|
|
227
|
-
const candidates = resolveRegisteredTargets(state, payload, quote);
|
|
228
|
-
if (candidates.length < 1) {
|
|
229
|
-
// No HEALTHY candidate. If providers DO serve this capability but are all in cooldown,
|
|
230
|
-
// fail closed (503) so the buyer is NOT charged for undelivered work — rather than falling
|
|
231
|
-
// through to the synthetic no-op. If nothing serves it, keep the legacy local path ({}).
|
|
232
|
-
if (hasActiveMatchingProvider(state, payload, quote)) {
|
|
233
|
-
return { error: { ok: false, status: 503, reason: "no_healthy_provider" } };
|
|
234
|
-
}
|
|
235
|
-
return {};
|
|
236
|
-
}
|
|
237
|
-
let lastError = null;
|
|
238
|
-
for (const target of candidates) {
|
|
239
|
-
if (Date.now() >= executionOptions.deadline_ms) {
|
|
240
|
-
// Budget spent on earlier candidates: stop here rather than start a call that cannot settle.
|
|
241
|
-
lastError = lastError || { ok: false, status: 504, reason: "external_execution_budget_exhausted" };
|
|
242
|
-
break;
|
|
243
|
-
}
|
|
244
|
-
const execution = await executeExternalTarget(target, payload, quote, forwardedContext, executionOptions);
|
|
245
|
-
try { recordProviderRouting(state, target.registered_provider_id); } catch (_) { /* non-fatal */ }
|
|
246
|
-
if (execution && execution.ok) {
|
|
247
|
-
try { recordProviderHealth(state, target.registered_provider_id, { ok: true }); } catch (_) { /* non-fatal */ }
|
|
248
|
-
execution.registered_provider_id = target.registered_provider_id;
|
|
249
|
-
return { externalExecution: execution };
|
|
250
|
-
}
|
|
251
|
-
try { recordProviderHealth(state, target.registered_provider_id, { ok: false }); } catch (_) { /* non-fatal */ }
|
|
252
|
-
lastError = execution || { ok: false, status: 502, reason: "external_execution_failed" };
|
|
253
|
-
// Fail over to the NEXT provider ONLY when this one provably did NOT process the request
|
|
254
|
-
// (rejected / unreachable). Never on an ambiguous failure (timeout, reset, 5xx) — that
|
|
255
|
-
// could make a side-effectful provider execute twice. Health still marks the failure.
|
|
256
|
-
if (!(execution && execution.failover_safe)) break;
|
|
257
|
-
}
|
|
258
|
-
return { error: lastError };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Assets the x402 lane can settle in, with their on-chain decimals (needed to turn settled base
|
|
262
|
-
// units into the ledger's cents). Anything else cannot be reconciled and must not accrue on the quote.
|
|
263
|
-
const X402_ASSET_DECIMALS = new Map([
|
|
264
|
-
["0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", 6], // USDC, Base mainnet (eip155:8453)
|
|
265
|
-
["0x036cbd53842c5426634e7929541ec2318f3dcf7e", 6] // USDC, Base Sepolia (eip155:84532)
|
|
266
|
-
]);
|
|
267
|
-
|
|
268
|
-
// The amount a provider's revenue share is computed from. The quote is what we ASKED for; on the
|
|
269
|
-
// x402 lane the facilitator settles exactly the advertised `accepts` amount, which may differ from
|
|
270
|
-
// the catalog quote — so accrue on min(quote, settled) and never on money that was not collected.
|
|
271
|
-
// Other lanes bind the paid amount to the quote at verification time (local_signed) or charge the
|
|
272
|
-
// quote directly (credits / hosted checkout), so the quote is the settled amount there.
|
|
273
|
-
function reconcileProviderSettlementAmount(quote, verification) {
|
|
274
|
-
const quoted = quote && Number.isFinite(Number(quote.amount_minor)) ? Math.max(0, Math.round(Number(quote.amount_minor))) : 0;
|
|
275
|
-
const mode = verification && typeof verification.verification_mode === "string" ? verification.verification_mode : null;
|
|
276
|
-
if (mode !== "x402_facilitator") {
|
|
277
|
-
return { amount_minor: quoted, quoted_minor: quoted, settled_minor: null, settlement_basis: mode ? `${mode}_quote` : "quote", reconciled: true };
|
|
278
|
-
}
|
|
279
|
-
const paymentPayload = (verification && verification.payment_payload) || {};
|
|
280
|
-
const decimals = X402_ASSET_DECIMALS.get(String(paymentPayload.x402_asset || "").toLowerCase());
|
|
281
|
-
const baseUnits = /^\d{1,30}$/.test(String(paymentPayload.x402_amount)) ? Number(paymentPayload.x402_amount) : NaN;
|
|
282
|
-
const centsCurrency = /^USD(_CENTS)?$/i.test(String((quote && quote.currency) || "USD_CENTS"));
|
|
283
|
-
if (decimals === undefined || !Number.isFinite(baseUnits) || !centsCurrency) {
|
|
284
|
-
return { amount_minor: 0, quoted_minor: quoted, settled_minor: null, settlement_basis: "x402_unreconciled_asset", reconciled: false };
|
|
285
|
-
}
|
|
286
|
-
const settledCents = Math.floor(baseUnits / Math.pow(10, decimals - 2));
|
|
287
|
-
return { amount_minor: Math.min(quoted, settledCents), quoted_minor: quoted, settled_minor: settledCents, settlement_basis: "x402_settled_min_quote", reconciled: true };
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Records the provider revenue-share split for a settled call routed to a registered
|
|
291
|
-
// provider. Fail-safe: metering must NEVER break or reverse a settled/charged response.
|
|
292
|
-
function attributeProviderSettlement(state, externalExecutionOptions, payload, quote, extra, verification) {
|
|
293
|
-
const execution = externalExecutionOptions && externalExecutionOptions.externalExecution;
|
|
294
|
-
const providerId = execution && execution.registered_provider_id;
|
|
295
|
-
if (!providerId) return;
|
|
296
|
-
try {
|
|
297
|
-
const taskRef = (payload && payload.task_ref)
|
|
298
|
-
|| (Array.isArray(payload && payload.tasks) && payload.tasks[0] && payload.tasks[0].task_ref)
|
|
299
|
-
|| null;
|
|
300
|
-
const reconciled = reconcileProviderSettlementAmount(quote, verification);
|
|
301
|
-
recordProviderSettlement(state, {
|
|
302
|
-
provider_id: providerId,
|
|
303
|
-
amount_minor: reconciled.amount_minor,
|
|
304
|
-
quoted_minor: reconciled.quoted_minor,
|
|
305
|
-
settled_minor: reconciled.settled_minor,
|
|
306
|
-
settlement_basis: reconciled.settlement_basis,
|
|
307
|
-
reconciled: reconciled.reconciled,
|
|
308
|
-
currency: quote && quote.currency ? quote.currency : null,
|
|
309
|
-
transaction_id: (extra && extra.transaction_id) || (quote && quote.quote_id) || null,
|
|
310
|
-
settlement_ref: (extra && extra.settlement_ref) || null,
|
|
311
|
-
task_ref: taskRef
|
|
312
|
-
});
|
|
313
|
-
} catch (_) { /* non-fatal: a settled call is never undone by an accounting error */ }
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async function executeCommandRequest(state, payload, headers, options) {
|
|
317
|
-
const opts = options || {};
|
|
318
|
-
const requiredHeaders = Array.isArray(opts.requiredHeaders) && opts.requiredHeaders.length > 0
|
|
319
|
-
? opts.requiredHeaders
|
|
320
|
-
: ["x-wallet-id", "x-payment", "x-payment-signature"];
|
|
321
|
-
const challengePayload = typeof opts.challengePayload === "function"
|
|
322
|
-
? opts.challengePayload
|
|
323
|
-
: (quote) => ({ ok: false, quote });
|
|
324
|
-
|
|
325
|
-
hydrateQuoteStates(state);
|
|
326
|
-
const validation = validateIntegrationSelection(payload);
|
|
327
|
-
if (!validation.ok) {
|
|
328
|
-
return {
|
|
329
|
-
status: 400,
|
|
330
|
-
payload: {
|
|
331
|
-
ok: false,
|
|
332
|
-
reason: validation.reason,
|
|
333
|
-
errors: validation.errors,
|
|
334
|
-
selection: validation.selection
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
const prefersCredits = payload && payload.payment_preference === "credits_first";
|
|
339
|
-
if (!hasPaymentHeaders(headers || {}, requiredHeaders)) {
|
|
340
|
-
const bearerToken = parseBearerToken(headers);
|
|
341
|
-
const delegatedSpendRequested = typeof bearerToken === "string" && bearerToken.startsWith("xytara_spend_");
|
|
342
|
-
const delegatedSpend = delegatedSpendRequested
|
|
343
|
-
? resolveSpendCredential(state, bearerToken, extractAgentProof(headers), {
|
|
344
|
-
account_id: payload && payload.account_id,
|
|
345
|
-
task_ref: payload && payload.task_ref,
|
|
346
|
-
units: payload && payload.quoted_units,
|
|
347
|
-
// Strict binding is scoped to this execute path (where a binding is supplied); the
|
|
348
|
-
// lifecycle paths pass no binding and are intentionally unaffected.
|
|
349
|
-
require: String(process.env.XYTARA_REQUIRE_SPEND_PROOF_BINDING || "").trim() === "1"
|
|
350
|
-
})
|
|
351
|
-
: { ok: false, reason: "spend_credential_not_requested" };
|
|
352
|
-
if (delegatedSpendRequested && !delegatedSpend.ok) {
|
|
353
|
-
return {
|
|
354
|
-
status: 403,
|
|
355
|
-
payload: {
|
|
356
|
-
ok: false,
|
|
357
|
-
reason: delegatedSpend.reason
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
if (delegatedSpend.ok) {
|
|
362
|
-
// Pin the command to the credential's own account — a delegated spend token must
|
|
363
|
-
// never be aimed at another account by supplying a different account_id.
|
|
364
|
-
if (payload && typeof payload.account_id === "string" && payload.account_id && payload.account_id !== delegatedSpend.credential.account_id) {
|
|
365
|
-
return { status: 403, payload: { ok: false, reason: "delegated_account_mismatch" } };
|
|
366
|
-
}
|
|
367
|
-
payload = {
|
|
368
|
-
...payload,
|
|
369
|
-
account_id: delegatedSpend.credential.account_id,
|
|
370
|
-
agent_id: payload && payload.agent_id ? payload.agent_id : delegatedSpend.credential.agent_id,
|
|
371
|
-
budget_id: payload && payload.budget_id ? payload.budget_id : delegatedSpend.credential.budget_id,
|
|
372
|
-
pack_id: delegatedSpend.credential.pack_id || (payload && payload.pack_id) || null,
|
|
373
|
-
entitlement_id: delegatedSpend.credential.entitlement_id || (payload && payload.entitlement_id) || null
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
const quote = createQuote(state, payload);
|
|
377
|
-
if (prefersCredits) {
|
|
378
|
-
const creditPayload = {
|
|
379
|
-
...payload,
|
|
380
|
-
account_id: quote.account_id,
|
|
381
|
-
quoted_units: Number.isFinite(Number(payload.quoted_units)) ? Number(payload.quoted_units) : undefined
|
|
382
|
-
};
|
|
383
|
-
const meteringPreview = buildUsageMeterPreview(state, creditPayload);
|
|
384
|
-
const requiredUnits = meteringPreview.usage_units;
|
|
385
|
-
if (delegatedSpend.ok && delegatedSpend.binding) {
|
|
386
|
-
const authorityDecision = evaluateAuthorityBindingAction(delegatedSpend.binding, "delegated_credit_spend", requiredUnits);
|
|
387
|
-
if (!authorityDecision.ok) {
|
|
388
|
-
return {
|
|
389
|
-
status: 403,
|
|
390
|
-
payload: {
|
|
391
|
-
ok: false,
|
|
392
|
-
reason: authorityDecision.reason,
|
|
393
|
-
binding_id: authorityDecision.binding_id || null,
|
|
394
|
-
required_scope: authorityDecision.required_scope || null,
|
|
395
|
-
authority_scope: authorityDecision.authority_scope || [],
|
|
396
|
-
required_consequence_family: authorityDecision.required_consequence_family || null,
|
|
397
|
-
allowed_consequence_families: authorityDecision.allowed_consequence_families || [],
|
|
398
|
-
max_units: authorityDecision.max_units || null,
|
|
399
|
-
attempted_units: authorityDecision.attempted_units || requiredUnits
|
|
400
|
-
}
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const creditDecision = previewCreditSpend(state, creditPayload);
|
|
406
|
-
if (!creditDecision.ok || !creditDecision.credit_spend_preview || creditDecision.credit_spend_preview.executable_now !== true) {
|
|
407
|
-
return buildCreditRejectionResponse(creditDecision, quote);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const preCommitAuthorizationContext = delegatedSpend.ok
|
|
411
|
-
? delegatedSpend.authorization_context
|
|
412
|
-
: {
|
|
413
|
-
ok: true,
|
|
414
|
-
verification_mode: "account_credits",
|
|
415
|
-
wallet_id: quote.account_id,
|
|
416
|
-
age_seconds: 0,
|
|
417
|
-
payment_payload: {
|
|
418
|
-
credit_spend_status: "precommit"
|
|
419
|
-
}
|
|
420
|
-
};
|
|
421
|
-
const externalExecutionOptions = await buildExternalExecutionOptions(state, payload, quote, preCommitAuthorizationContext);
|
|
422
|
-
if (externalExecutionOptions.error) {
|
|
423
|
-
return {
|
|
424
|
-
status: externalExecutionOptions.error.status || 502,
|
|
425
|
-
payload: {
|
|
426
|
-
ok: false,
|
|
427
|
-
reason: externalExecutionOptions.error.reason || "external_execution_failed",
|
|
428
|
-
details: externalExecutionOptions.error.details || null,
|
|
429
|
-
credit_spend_status: "not_committed",
|
|
430
|
-
credit_spend_preview: creditDecision.credit_spend_preview,
|
|
431
|
-
quote
|
|
432
|
-
}
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const creditSpend = consumeAccountCredits(state, buildCreditCommitPayload(payload, quote, requiredUnits));
|
|
437
|
-
if (creditSpend.ok) {
|
|
438
|
-
const paymentVerification = {
|
|
439
|
-
...(delegatedSpend.ok
|
|
440
|
-
? delegatedSpend.authorization_context
|
|
441
|
-
: {
|
|
442
|
-
ok: true,
|
|
443
|
-
verification_mode: "account_credits",
|
|
444
|
-
wallet_id: quote.account_id,
|
|
445
|
-
age_seconds: 0,
|
|
446
|
-
payment_payload: {}
|
|
447
|
-
}),
|
|
448
|
-
payment_payload: {
|
|
449
|
-
...((delegatedSpend.ok && delegatedSpend.authorization_context && delegatedSpend.authorization_context.payment_payload) || {}),
|
|
450
|
-
credit_spend_id: creditSpend.credit_spend.credit_spend_id
|
|
451
|
-
}
|
|
452
|
-
};
|
|
453
|
-
attributeProviderSettlement(state, externalExecutionOptions, payload, quote, {
|
|
454
|
-
transaction_id: creditSpend.credit_spend.credit_spend_id
|
|
455
|
-
}, paymentVerification);
|
|
456
|
-
const creditResult = createCommandResult(state, {
|
|
457
|
-
...payload,
|
|
458
|
-
payment_preference: "credits_first"
|
|
459
|
-
}, quote, {
|
|
460
|
-
paymentVerification,
|
|
461
|
-
creditSpend: creditSpend.credit_spend,
|
|
462
|
-
skipPaymentLedger: true,
|
|
463
|
-
externalExecution: externalExecutionOptions.externalExecution || null
|
|
464
|
-
});
|
|
465
|
-
attachDeliveryReceipt(state, creditResult, payload, quote, paymentVerification, externalExecutionOptions);
|
|
466
|
-
return {
|
|
467
|
-
status: 200,
|
|
468
|
-
payload: creditResult
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
if (creditSpend.reason !== "insufficient_credits") {
|
|
472
|
-
return {
|
|
473
|
-
status: 403,
|
|
474
|
-
payload: {
|
|
475
|
-
ok: false,
|
|
476
|
-
reason: creditSpend.reason,
|
|
477
|
-
account_id: quote.account_id,
|
|
478
|
-
provider_execution_completed_but_credit_commit_failed: Boolean(externalExecutionOptions.externalExecution),
|
|
479
|
-
policy_guard: {
|
|
480
|
-
agent_id: creditSpend.agent_id || null,
|
|
481
|
-
budget_id: creditSpend.budget_id || null,
|
|
482
|
-
task_domain: creditSpend.task_domain || null,
|
|
483
|
-
policy_ref: creditSpend.policy_ref || null,
|
|
484
|
-
max_reserve_units: creditSpend.max_reserve_units || null,
|
|
485
|
-
allowed_job_domains: creditSpend.allowed_job_domains || null,
|
|
486
|
-
budget_status: creditSpend.budget_status || null,
|
|
487
|
-
agent_status: creditSpend.agent_status || null
|
|
488
|
-
},
|
|
489
|
-
quote
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
return {
|
|
494
|
-
status: 402,
|
|
495
|
-
payload: {
|
|
496
|
-
ok: false,
|
|
497
|
-
reason: "insufficient_credits",
|
|
498
|
-
provider_execution_completed_but_credit_commit_failed: Boolean(externalExecutionOptions.externalExecution),
|
|
499
|
-
insufficient_credits: {
|
|
500
|
-
account_id: quote.account_id,
|
|
501
|
-
required_units: creditSpend.required_units,
|
|
502
|
-
available_units: creditSpend.available_units,
|
|
503
|
-
recommended_next_surfaces: {
|
|
504
|
-
credits_topup: "/v1/checkout/purchase-intents",
|
|
505
|
-
direct_payment: "/v1/payment-quotes"
|
|
506
|
-
}
|
|
507
|
-
},
|
|
508
|
-
quote
|
|
509
|
-
}
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
return {
|
|
513
|
-
status: 402,
|
|
514
|
-
payload: await challengePayload(quote)
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
const quote = payload.quote_id && state.quotes.get(payload.quote_id)
|
|
519
|
-
? state.quotes.get(payload.quote_id)
|
|
520
|
-
: createQuote(state, payload);
|
|
521
|
-
|
|
522
|
-
if (quote && quote.status === "expired") {
|
|
523
|
-
return {
|
|
524
|
-
status: 409,
|
|
525
|
-
payload: {
|
|
526
|
-
ok: false,
|
|
527
|
-
reason: "quote_expired",
|
|
528
|
-
quote
|
|
529
|
-
}
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
if (quote && (quote.status === "executed" || quote.status === "accepted")) {
|
|
534
|
-
return {
|
|
535
|
-
status: 409,
|
|
536
|
-
payload: {
|
|
537
|
-
ok: false,
|
|
538
|
-
reason: "quote_already_used",
|
|
539
|
-
quote
|
|
540
|
-
}
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Verify-execute-settle: for the x402 facilitator path, settlement is DEFERRED
|
|
545
|
-
// until execution succeeds, so a failed call never settles/charges. Other paths
|
|
546
|
-
// (native/credits, and the external rails L402/BOLT/MPP whose funds already moved
|
|
547
|
-
// before the proof) have nothing to defer and are unaffected.
|
|
548
|
-
const paymentVerification = await verifyPaymentAcceptance(state, headers || {}, requiredHeaders, quote, {
|
|
549
|
-
payment_protocol: opts.paymentProtocol || (payload && payload.payment_protocol) || null,
|
|
550
|
-
authorization_scheme: opts.authorizationScheme || null,
|
|
551
|
-
resource_url: opts.resourceUrl || null,
|
|
552
|
-
deferSettlement: true
|
|
553
|
-
});
|
|
554
|
-
if (!paymentVerification.ok) {
|
|
555
|
-
return {
|
|
556
|
-
status: paymentVerification.status || 403,
|
|
557
|
-
payload: {
|
|
558
|
-
ok: false,
|
|
559
|
-
reason: paymentVerification.reason,
|
|
560
|
-
...(paymentVerification.detail ? { detail: paymentVerification.detail } : {}),
|
|
561
|
-
quote
|
|
562
|
-
}
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
markPaymentAccepted(state, paymentVerification);
|
|
567
|
-
const settlementDeferred = Boolean(paymentVerification.deferred_settlement);
|
|
568
|
-
|
|
569
|
-
// Reserve an execution slot so the same signed x402 authorization cannot execute
|
|
570
|
-
// twice concurrently (the deferred settle no longer fronts the on-chain nonce as an
|
|
571
|
-
// implicit pre-execution guard). No-op for non-deferred lanes; fails open if the
|
|
572
|
-
// store is unavailable. The slot is released in the finally below, whatever happens.
|
|
573
|
-
const executionClaim = claimDeferredExecution(state, paymentVerification);
|
|
574
|
-
if (!executionClaim.ok) {
|
|
575
|
-
return {
|
|
576
|
-
status: executionClaim.status || 409,
|
|
577
|
-
payload: {
|
|
578
|
-
ok: false,
|
|
579
|
-
reason: executionClaim.reason || "payment_authorization_in_progress",
|
|
580
|
-
...(executionClaim.detail ? { detail: executionClaim.detail } : {}),
|
|
581
|
-
payment_settled: false,
|
|
582
|
-
quote
|
|
583
|
-
}
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
try {
|
|
588
|
-
const externalExecutionOptions = await buildExternalExecutionOptions(state, payload, quote, paymentVerification);
|
|
589
|
-
if (externalExecutionOptions.error) {
|
|
590
|
-
// Execution failed. Report the money state by what actually happened, not by lane:
|
|
591
|
-
// - deferred x402: we never settled, so the caller is NOT charged.
|
|
592
|
-
// - a concrete receipt is in hand (an x402 settlement already captured): the caller
|
|
593
|
-
// WAS charged, so surface the receipt and a refund path.
|
|
594
|
-
// - everything else (native/local_signed, presence, and the access-token rails
|
|
595
|
-
// whose proof we only validated): no receipt was produced on this request, so we
|
|
596
|
-
// do not claim a charge — a bare retry hint, never a false refund pointer.
|
|
597
|
-
const hasReceipt = Boolean(paymentVerification.settlement || paymentVerification.payment_response_base64);
|
|
598
|
-
return {
|
|
599
|
-
status: externalExecutionOptions.error.status || 502,
|
|
600
|
-
payload: {
|
|
601
|
-
ok: false,
|
|
602
|
-
reason: externalExecutionOptions.error.reason || "external_execution_failed",
|
|
603
|
-
details: externalExecutionOptions.error.details || null,
|
|
604
|
-
payment_settled: hasReceipt,
|
|
605
|
-
...(paymentVerification.settlement ? { settlement: paymentVerification.settlement } : {}),
|
|
606
|
-
remediation: settlementDeferred
|
|
607
|
-
? "execution failed before settlement; you were not charged — retry with corrected input"
|
|
608
|
-
: hasReceipt
|
|
609
|
-
? "payment was accepted but execution failed; retry with corrected input or request a refund with your payment proof"
|
|
610
|
-
: "execution failed; retry with corrected input",
|
|
611
|
-
quote
|
|
612
|
-
},
|
|
613
|
-
...(hasReceipt && paymentVerification.payment_response_base64
|
|
614
|
-
? { paymentResponse: paymentVerification.payment_response_base64 }
|
|
615
|
-
: {})
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// Execution succeeded — settle the deferred payment now (no-op for non-deferred).
|
|
620
|
-
const settleResult = await settleDeferredPayment(paymentVerification);
|
|
621
|
-
if (!settleResult.ok) {
|
|
622
|
-
return {
|
|
623
|
-
status: settleResult.status || 402,
|
|
624
|
-
payload: {
|
|
625
|
-
ok: false,
|
|
626
|
-
reason: settleResult.reason || "settlement_failed",
|
|
627
|
-
...(settleResult.detail ? { detail: settleResult.detail } : {}),
|
|
628
|
-
payment_settled: false,
|
|
629
|
-
remediation: "the work executed but settlement failed; you were not charged — retry",
|
|
630
|
-
quote
|
|
631
|
-
}
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
// settleDeferredPayment is a no-op for non-deferred paths (returns {ok:true} with no
|
|
635
|
-
// settlement) — in that case keep the verification untouched; only when a deferred
|
|
636
|
-
// x402 settlement actually ran do we fold its receipt in and drop the deferred marker.
|
|
637
|
-
const settlementOccurred = Boolean(settleResult.settlement || settleResult.payment_response_base64);
|
|
638
|
-
const settledVerification = settlementOccurred
|
|
639
|
-
? { ...paymentVerification, settlement: settleResult.settlement, payment_response_base64: settleResult.payment_response_base64, deferred_settlement: undefined }
|
|
640
|
-
: paymentVerification;
|
|
641
|
-
|
|
642
|
-
attributeProviderSettlement(state, externalExecutionOptions, payload, quote, {
|
|
643
|
-
settlement_ref: (settledVerification.settlement && (settledVerification.settlement.transaction || settledVerification.settlement.txid)) || null
|
|
644
|
-
}, settledVerification);
|
|
645
|
-
const paidResult = createCommandResult(state, payload, quote, {
|
|
646
|
-
paymentVerification: settledVerification,
|
|
647
|
-
externalExecution: externalExecutionOptions.externalExecution || null
|
|
648
|
-
});
|
|
649
|
-
attachDeliveryReceipt(state, paidResult, payload, quote, settledVerification, externalExecutionOptions);
|
|
650
|
-
return {
|
|
651
|
-
status: 200,
|
|
652
|
-
payload: paidResult,
|
|
653
|
-
...(settledVerification.payment_response_base64
|
|
654
|
-
? { paymentResponse: settledVerification.payment_response_base64 }
|
|
655
|
-
: {})
|
|
656
|
-
};
|
|
657
|
-
} finally {
|
|
658
|
-
releaseDeferredExecution(state, executionClaim.key);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
async function executeAuthorizedCommandRequest(state, payload, authorizationContext) {
|
|
663
|
-
const validation = validateIntegrationSelection(payload);
|
|
664
|
-
if (!validation.ok) {
|
|
665
|
-
return {
|
|
666
|
-
status: 400,
|
|
667
|
-
payload: {
|
|
668
|
-
ok: false,
|
|
669
|
-
reason: validation.reason,
|
|
670
|
-
errors: validation.errors,
|
|
671
|
-
selection: validation.selection
|
|
672
|
-
}
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
hydrateQuoteStates(state);
|
|
677
|
-
|
|
678
|
-
const quote = createQuote(state, payload);
|
|
679
|
-
const paymentVerification = {
|
|
680
|
-
ok: true,
|
|
681
|
-
verification_mode: authorizationContext && authorizationContext.verification_mode
|
|
682
|
-
? authorizationContext.verification_mode
|
|
683
|
-
: "hosted_checkout",
|
|
684
|
-
wallet_id: authorizationContext && authorizationContext.wallet_id
|
|
685
|
-
? authorizationContext.wallet_id
|
|
686
|
-
: "hosted_checkout",
|
|
687
|
-
age_seconds: 0,
|
|
688
|
-
payment_payload: authorizationContext && authorizationContext.payment_payload
|
|
689
|
-
? authorizationContext.payment_payload
|
|
690
|
-
: null
|
|
691
|
-
};
|
|
692
|
-
const externalExecutionOptions = await buildExternalExecutionOptions(state, payload, quote, paymentVerification);
|
|
693
|
-
if (externalExecutionOptions.error) {
|
|
694
|
-
return {
|
|
695
|
-
status: externalExecutionOptions.error.status || 502,
|
|
696
|
-
payload: {
|
|
697
|
-
ok: false,
|
|
698
|
-
reason: externalExecutionOptions.error.reason || "external_execution_failed",
|
|
699
|
-
details: externalExecutionOptions.error.details || null
|
|
700
|
-
}
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
attributeProviderSettlement(state, externalExecutionOptions, payload, quote, undefined, paymentVerification);
|
|
705
|
-
const authorizedResult = createCommandResult(state, payload, quote, {
|
|
706
|
-
paymentVerification,
|
|
707
|
-
externalExecution: externalExecutionOptions.externalExecution || null
|
|
708
|
-
});
|
|
709
|
-
attachDeliveryReceipt(state, authorizedResult, payload, quote, paymentVerification, externalExecutionOptions);
|
|
710
|
-
return {
|
|
711
|
-
status: 200,
|
|
712
|
-
payload: authorizedResult
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
module.exports = {
|
|
717
|
-
executeAuthorizedCommandRequest,
|
|
718
|
-
executeCommandRequest,
|
|
719
|
-
buildExternalExecutionOptions,
|
|
720
|
-
reconcileProviderSettlementAmount,
|
|
721
|
-
sanitizeAuthorizationContextForProvider
|
|
722
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
createCommandResult,
|
|
5
|
+
createQuote,
|
|
6
|
+
hydrateQuoteStates
|
|
7
|
+
} = require("./commerce_runtime");
|
|
8
|
+
const { consumeAccountCredits, buildUsageMeterPreview, previewCreditSpend } = require("./commerce_economics");
|
|
9
|
+
const { resolveSpendCredential, evaluateAuthorityBindingAction } = require("./account_auth");
|
|
10
|
+
const { validateIntegrationSelection } = require("../integrations/dispatch");
|
|
11
|
+
const { markPaymentAccepted, settleDeferredPayment, verifyPaymentAcceptance } = require("./payment_verification");
|
|
12
|
+
const { executeExternalTarget, resolveExecutionTarget } = require("./external_execution_runtime");
|
|
13
|
+
const { externalExecutionBudgetMs } = require("./external_execution_policy");
|
|
14
|
+
const { hashCanonical, issueDeliveryReceipt, loadDeliveryReceiptSigners } = require("./delivery_receipt");
|
|
15
|
+
|
|
16
|
+
const DELIVERY_RECEIPT_ISSUER = "xytara-runtime";
|
|
17
|
+
const DELIVERY_RECEIPT_RETENTION = 5000;
|
|
18
|
+
|
|
19
|
+
// The payment leg of a delivery receipt: on-chain facts for x402, the spend record for credits,
|
|
20
|
+
// and the verification mode for any other lane. Never the buyer's signature or secrets.
|
|
21
|
+
function deliveryReceiptPayment(verification, quote) {
|
|
22
|
+
const v = verification && typeof verification === "object" ? verification : {};
|
|
23
|
+
const paymentPayload = v.payment_payload && typeof v.payment_payload === "object" ? v.payment_payload : {};
|
|
24
|
+
if (v.verification_mode === "x402_facilitator") {
|
|
25
|
+
const settlement = v.settlement && typeof v.settlement === "object" ? v.settlement : null;
|
|
26
|
+
return {
|
|
27
|
+
protocol: "x402",
|
|
28
|
+
scheme: "exact",
|
|
29
|
+
network: paymentPayload.x402_network || (settlement && settlement.network) || null,
|
|
30
|
+
asset: paymentPayload.x402_asset || null,
|
|
31
|
+
amount: paymentPayload.x402_amount !== undefined && paymentPayload.x402_amount !== null ? String(paymentPayload.x402_amount) : null,
|
|
32
|
+
payer: paymentPayload.x402_payer || (settlement && settlement.payer) || null,
|
|
33
|
+
pay_to: paymentPayload.x402_pay_to || null,
|
|
34
|
+
settlement_tx: settlement ? (settlement.transaction || settlement.txid || null) : null,
|
|
35
|
+
settled: Boolean(settlement && settlement.success !== false && (settlement.transaction || settlement.txid))
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (v.verification_mode === "account_credits" || paymentPayload.credit_spend_id) {
|
|
39
|
+
return {
|
|
40
|
+
protocol: "account_credits",
|
|
41
|
+
credit_spend_id: paymentPayload.credit_spend_id || null,
|
|
42
|
+
quoted_amount_minor: quote && Number.isFinite(Number(quote.amount_minor)) ? Number(quote.amount_minor) : null,
|
|
43
|
+
currency: (quote && quote.currency) || null
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
protocol: v.verification_mode || "unspecified",
|
|
48
|
+
quoted_amount_minor: quote && Number.isFinite(Number(quote.amount_minor)) ? Number(quote.amount_minor) : null,
|
|
49
|
+
currency: (quote && quote.currency) || null
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Issues the signed delivery receipt for a completed paid call, attaches it to the response, and
|
|
54
|
+
// keeps it retrievable by id (bounded). Fail-safe: a receipt problem never breaks a completed call.
|
|
55
|
+
function attachDeliveryReceipt(state, result, payload, quote, verification, externalExecutionOptions) {
|
|
56
|
+
try {
|
|
57
|
+
if (!result || !result.receipt) return;
|
|
58
|
+
const execution = externalExecutionOptions && externalExecutionOptions.externalExecution
|
|
59
|
+
? externalExecutionOptions.externalExecution
|
|
60
|
+
: null;
|
|
61
|
+
const deliverables = result.command && Array.isArray(result.command.deliverables) ? result.command.deliverables : [];
|
|
62
|
+
const outputs = deliverables.map((deliverable) => (deliverable && deliverable.output_payload !== undefined ? deliverable.output_payload : null));
|
|
63
|
+
const body = payload && payload.body && typeof payload.body === "object" && !Array.isArray(payload.body) ? payload.body : {};
|
|
64
|
+
const taskRef = (payload && payload.task_ref)
|
|
65
|
+
|| (Array.isArray(payload && payload.tasks) && payload.tasks[0] && payload.tasks[0].task_ref)
|
|
66
|
+
|| null;
|
|
67
|
+
const { signers } = loadDeliveryReceiptSigners("XYTARA_DELIVERY_RECEIPT");
|
|
68
|
+
const deliveryReceipt = issueDeliveryReceipt({
|
|
69
|
+
receipt_id: `drcpt_${result.receipt.receipt_id.replace(/^rcpt_/, "")}`,
|
|
70
|
+
issuer: DELIVERY_RECEIPT_ISSUER,
|
|
71
|
+
call: {
|
|
72
|
+
quote_id: quote && quote.quote_id ? quote.quote_id : null,
|
|
73
|
+
transaction_id: result.transaction ? result.transaction.transaction_id : null,
|
|
74
|
+
runtime_receipt_id: result.receipt.receipt_id,
|
|
75
|
+
task_ref: taskRef,
|
|
76
|
+
tool_name: typeof body.tool_name === "string" ? body.tool_name : null
|
|
77
|
+
},
|
|
78
|
+
payment: deliveryReceiptPayment(verification, quote),
|
|
79
|
+
delivery: {
|
|
80
|
+
status: "completed",
|
|
81
|
+
provider_id: execution && execution.registered_provider_id ? execution.registered_provider_id : null,
|
|
82
|
+
routing: execution
|
|
83
|
+
? (execution.registered_provider_id ? "open_provider_registry" : "operator_env_target")
|
|
84
|
+
: "runtime_native",
|
|
85
|
+
execution_target_id: execution && execution.target_id ? execution.target_id : null,
|
|
86
|
+
request_sha256: hashCanonical(body),
|
|
87
|
+
request_hash_input: "the request `body` object (e.g. { tool_name, arguments })",
|
|
88
|
+
output_sha256: hashCanonical(outputs.length === 1 ? outputs[0] : outputs),
|
|
89
|
+
output_hash_input: outputs.length === 1
|
|
90
|
+
? "command.deliverables[0].output_payload (null when the task returns no payload)"
|
|
91
|
+
: "the array of command.deliverables[*].output_payload",
|
|
92
|
+
completed_at: (result.receipt.timing && result.receipt.timing.completed_at_iso) || result.receipt.created_at_iso || null
|
|
93
|
+
}
|
|
94
|
+
}, signers);
|
|
95
|
+
result.delivery_receipt = deliveryReceipt;
|
|
96
|
+
if (!(state.deliveryReceipts instanceof Map)) state.deliveryReceipts = new Map();
|
|
97
|
+
state.deliveryReceipts.set(deliveryReceipt.receipt_id, deliveryReceipt);
|
|
98
|
+
while (state.deliveryReceipts.size > DELIVERY_RECEIPT_RETENTION) {
|
|
99
|
+
state.deliveryReceipts.delete(state.deliveryReceipts.keys().next().value);
|
|
100
|
+
}
|
|
101
|
+
} catch (_) { /* non-fatal: never break a completed, possibly settled call */ }
|
|
102
|
+
}
|
|
103
|
+
const { resolveRegisteredTargets, hasActiveMatchingProvider, recordProviderHealth, recordProviderRouting, recordProviderSettlement } = require("./provider_registry");
|
|
104
|
+
const { claimDeferredExecution, releaseDeferredExecution } = require("./execution_dedupe");
|
|
105
|
+
|
|
106
|
+
function hasPaymentHeaders(headers, requiredHeaders) {
|
|
107
|
+
return requiredHeaders.every((headerName) => headers[headerName]);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseBearerToken(headers) {
|
|
111
|
+
const authorization = headers && typeof headers.authorization === "string"
|
|
112
|
+
? headers.authorization.trim()
|
|
113
|
+
: "";
|
|
114
|
+
const match = authorization.match(/^Bearer\s+(.+)$/i);
|
|
115
|
+
return match ? match[1].trim() : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Extract an agent-identity proof (for key-bound spend credentials) from request
|
|
119
|
+
// headers: X-Agent-Signature (base64 ed25519 sig) + X-Agent-Timestamp (ms).
|
|
120
|
+
function extractAgentProof(headers) {
|
|
121
|
+
const h = headers || {};
|
|
122
|
+
return {
|
|
123
|
+
signature: typeof h["x-agent-signature"] === "string" ? h["x-agent-signature"] : null,
|
|
124
|
+
timestamp: typeof h["x-agent-timestamp"] === "string" ? h["x-agent-timestamp"] : null
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildCreditCommitPayload(payload, quote, requiredUnits) {
|
|
129
|
+
return {
|
|
130
|
+
account_id: quote.account_id,
|
|
131
|
+
agent_id: payload.agent_id || null,
|
|
132
|
+
budget_id: payload.budget_id || null,
|
|
133
|
+
pack_id: payload.pack_id || null,
|
|
134
|
+
entitlement_id: payload.entitlement_id || null,
|
|
135
|
+
quote_id: quote.quote_id,
|
|
136
|
+
task_ref: payload.task_ref || null,
|
|
137
|
+
workflow_ref: payload.workflow_ref || null,
|
|
138
|
+
units: requiredUnits,
|
|
139
|
+
reason: "credit_first_execution"
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildCreditRejectionResponse(creditDecision, quote) {
|
|
144
|
+
const failureReason = creditDecision && creditDecision.credit_spend_preview
|
|
145
|
+
? creditDecision.credit_spend_preview.failure_reason
|
|
146
|
+
: null;
|
|
147
|
+
const reason = failureReason
|
|
148
|
+
|| (creditDecision && creditDecision.policy_guard && creditDecision.policy_guard.reason)
|
|
149
|
+
|| (creditDecision && creditDecision.entitlement_guard && creditDecision.entitlement_guard.reason)
|
|
150
|
+
|| (creditDecision && creditDecision.reason)
|
|
151
|
+
|| "credit_spend_not_allowed";
|
|
152
|
+
|
|
153
|
+
if (reason === "insufficient_credits") {
|
|
154
|
+
const preview = creditDecision && creditDecision.credit_spend_preview ? creditDecision.credit_spend_preview : {};
|
|
155
|
+
return {
|
|
156
|
+
status: 402,
|
|
157
|
+
payload: {
|
|
158
|
+
ok: false,
|
|
159
|
+
reason: "insufficient_credits",
|
|
160
|
+
insufficient_credits: {
|
|
161
|
+
account_id: quote.account_id,
|
|
162
|
+
required_units: preview.required_units,
|
|
163
|
+
available_units: preview.available_units,
|
|
164
|
+
recommended_next_surfaces: {
|
|
165
|
+
credits_topup: "/v1/checkout/purchase-intents",
|
|
166
|
+
direct_payment: "/v1/payment-quotes"
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
quote
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
status: 403,
|
|
176
|
+
payload: {
|
|
177
|
+
ok: false,
|
|
178
|
+
reason,
|
|
179
|
+
account_id: quote.account_id,
|
|
180
|
+
policy_guard: creditDecision && creditDecision.policy_guard ? creditDecision.policy_guard : null,
|
|
181
|
+
entitlement_guard: creditDecision && creditDecision.entitlement_guard ? creditDecision.entitlement_guard : null,
|
|
182
|
+
quote
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// What a provider is allowed to see of the buyer's payment context: the verification outcome and
|
|
188
|
+
// scalar payment facts only. NEVER the signed x402 authorization / deferred-settlement material
|
|
189
|
+
// (that would hand a third-party provider the buyer's signature before settlement), and never
|
|
190
|
+
// signature-, secret- or raw-looking keys or nested objects from any lane's payload.
|
|
191
|
+
const SENSITIVE_CONTEXT_KEY = /signature|secret|private|authorization|raw|base64|header|token/i;
|
|
192
|
+
function sanitizeAuthorizationContextForProvider(context) {
|
|
193
|
+
if (!context || typeof context !== "object") return null;
|
|
194
|
+
const payload = context.payment_payload && typeof context.payment_payload === "object" ? context.payment_payload : null;
|
|
195
|
+
const safePayload = payload
|
|
196
|
+
? Object.fromEntries(Object.entries(payload).filter(([key, value]) => !SENSITIVE_CONTEXT_KEY.test(key) && (value === null || typeof value !== "object")))
|
|
197
|
+
: null;
|
|
198
|
+
return {
|
|
199
|
+
ok: context.ok === true,
|
|
200
|
+
verification_mode: context.verification_mode || null,
|
|
201
|
+
wallet_id: context.wallet_id || null,
|
|
202
|
+
age_seconds: Number.isFinite(Number(context.age_seconds)) ? Number(context.age_seconds) : null,
|
|
203
|
+
payment_payload: safePayload
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function buildExternalExecutionOptions(state, payload, quote, authorizationContext) {
|
|
208
|
+
// One wall-clock budget for the whole call (every retry of every candidate), inside the x402
|
|
209
|
+
// authorization window — see external_execution_policy.js.
|
|
210
|
+
const executionOptions = { deadline_ms: Date.now() + externalExecutionBudgetMs() };
|
|
211
|
+
const forwardedContext = sanitizeAuthorizationContextForProvider(authorizationContext);
|
|
212
|
+
// Operator-pinned env target resolves first and is used as-is (an explicit single target,
|
|
213
|
+
// no failover). Empty env + empty registry -> {} (unchanged behavior).
|
|
214
|
+
const envTarget = resolveExecutionTarget(payload, quote);
|
|
215
|
+
if (envTarget) {
|
|
216
|
+
const execution = await executeExternalTarget(envTarget, payload, quote, forwardedContext, executionOptions);
|
|
217
|
+
if (!execution || !execution.ok) {
|
|
218
|
+
return { error: execution || { ok: false, status: 502, reason: "external_execution_failed" } };
|
|
219
|
+
}
|
|
220
|
+
return { externalExecution: execution };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Open-network path: try health-ranked registered providers (healthy first, then half-open)
|
|
224
|
+
// in order, FAILING OVER to the next when one is down — so a single unhealthy/throttled
|
|
225
|
+
// provider doesn't fail the buyer when another serves the same capability. Each attempt
|
|
226
|
+
// updates the provider's health; accounting is wrapped so it can never break execution.
|
|
227
|
+
const candidates = resolveRegisteredTargets(state, payload, quote);
|
|
228
|
+
if (candidates.length < 1) {
|
|
229
|
+
// No HEALTHY candidate. If providers DO serve this capability but are all in cooldown,
|
|
230
|
+
// fail closed (503) so the buyer is NOT charged for undelivered work — rather than falling
|
|
231
|
+
// through to the synthetic no-op. If nothing serves it, keep the legacy local path ({}).
|
|
232
|
+
if (hasActiveMatchingProvider(state, payload, quote)) {
|
|
233
|
+
return { error: { ok: false, status: 503, reason: "no_healthy_provider" } };
|
|
234
|
+
}
|
|
235
|
+
return {};
|
|
236
|
+
}
|
|
237
|
+
let lastError = null;
|
|
238
|
+
for (const target of candidates) {
|
|
239
|
+
if (Date.now() >= executionOptions.deadline_ms) {
|
|
240
|
+
// Budget spent on earlier candidates: stop here rather than start a call that cannot settle.
|
|
241
|
+
lastError = lastError || { ok: false, status: 504, reason: "external_execution_budget_exhausted" };
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
const execution = await executeExternalTarget(target, payload, quote, forwardedContext, executionOptions);
|
|
245
|
+
try { recordProviderRouting(state, target.registered_provider_id); } catch (_) { /* non-fatal */ }
|
|
246
|
+
if (execution && execution.ok) {
|
|
247
|
+
try { recordProviderHealth(state, target.registered_provider_id, { ok: true }); } catch (_) { /* non-fatal */ }
|
|
248
|
+
execution.registered_provider_id = target.registered_provider_id;
|
|
249
|
+
return { externalExecution: execution };
|
|
250
|
+
}
|
|
251
|
+
try { recordProviderHealth(state, target.registered_provider_id, { ok: false }); } catch (_) { /* non-fatal */ }
|
|
252
|
+
lastError = execution || { ok: false, status: 502, reason: "external_execution_failed" };
|
|
253
|
+
// Fail over to the NEXT provider ONLY when this one provably did NOT process the request
|
|
254
|
+
// (rejected / unreachable). Never on an ambiguous failure (timeout, reset, 5xx) — that
|
|
255
|
+
// could make a side-effectful provider execute twice. Health still marks the failure.
|
|
256
|
+
if (!(execution && execution.failover_safe)) break;
|
|
257
|
+
}
|
|
258
|
+
return { error: lastError };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Assets the x402 lane can settle in, with their on-chain decimals (needed to turn settled base
|
|
262
|
+
// units into the ledger's cents). Anything else cannot be reconciled and must not accrue on the quote.
|
|
263
|
+
const X402_ASSET_DECIMALS = new Map([
|
|
264
|
+
["0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", 6], // USDC, Base mainnet (eip155:8453)
|
|
265
|
+
["0x036cbd53842c5426634e7929541ec2318f3dcf7e", 6] // USDC, Base Sepolia (eip155:84532)
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
// The amount a provider's revenue share is computed from. The quote is what we ASKED for; on the
|
|
269
|
+
// x402 lane the facilitator settles exactly the advertised `accepts` amount, which may differ from
|
|
270
|
+
// the catalog quote — so accrue on min(quote, settled) and never on money that was not collected.
|
|
271
|
+
// Other lanes bind the paid amount to the quote at verification time (local_signed) or charge the
|
|
272
|
+
// quote directly (credits / hosted checkout), so the quote is the settled amount there.
|
|
273
|
+
function reconcileProviderSettlementAmount(quote, verification) {
|
|
274
|
+
const quoted = quote && Number.isFinite(Number(quote.amount_minor)) ? Math.max(0, Math.round(Number(quote.amount_minor))) : 0;
|
|
275
|
+
const mode = verification && typeof verification.verification_mode === "string" ? verification.verification_mode : null;
|
|
276
|
+
if (mode !== "x402_facilitator") {
|
|
277
|
+
return { amount_minor: quoted, quoted_minor: quoted, settled_minor: null, settlement_basis: mode ? `${mode}_quote` : "quote", reconciled: true };
|
|
278
|
+
}
|
|
279
|
+
const paymentPayload = (verification && verification.payment_payload) || {};
|
|
280
|
+
const decimals = X402_ASSET_DECIMALS.get(String(paymentPayload.x402_asset || "").toLowerCase());
|
|
281
|
+
const baseUnits = /^\d{1,30}$/.test(String(paymentPayload.x402_amount)) ? Number(paymentPayload.x402_amount) : NaN;
|
|
282
|
+
const centsCurrency = /^USD(_CENTS)?$/i.test(String((quote && quote.currency) || "USD_CENTS"));
|
|
283
|
+
if (decimals === undefined || !Number.isFinite(baseUnits) || !centsCurrency) {
|
|
284
|
+
return { amount_minor: 0, quoted_minor: quoted, settled_minor: null, settlement_basis: "x402_unreconciled_asset", reconciled: false };
|
|
285
|
+
}
|
|
286
|
+
const settledCents = Math.floor(baseUnits / Math.pow(10, decimals - 2));
|
|
287
|
+
return { amount_minor: Math.min(quoted, settledCents), quoted_minor: quoted, settled_minor: settledCents, settlement_basis: "x402_settled_min_quote", reconciled: true };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Records the provider revenue-share split for a settled call routed to a registered
|
|
291
|
+
// provider. Fail-safe: metering must NEVER break or reverse a settled/charged response.
|
|
292
|
+
function attributeProviderSettlement(state, externalExecutionOptions, payload, quote, extra, verification) {
|
|
293
|
+
const execution = externalExecutionOptions && externalExecutionOptions.externalExecution;
|
|
294
|
+
const providerId = execution && execution.registered_provider_id;
|
|
295
|
+
if (!providerId) return;
|
|
296
|
+
try {
|
|
297
|
+
const taskRef = (payload && payload.task_ref)
|
|
298
|
+
|| (Array.isArray(payload && payload.tasks) && payload.tasks[0] && payload.tasks[0].task_ref)
|
|
299
|
+
|| null;
|
|
300
|
+
const reconciled = reconcileProviderSettlementAmount(quote, verification);
|
|
301
|
+
recordProviderSettlement(state, {
|
|
302
|
+
provider_id: providerId,
|
|
303
|
+
amount_minor: reconciled.amount_minor,
|
|
304
|
+
quoted_minor: reconciled.quoted_minor,
|
|
305
|
+
settled_minor: reconciled.settled_minor,
|
|
306
|
+
settlement_basis: reconciled.settlement_basis,
|
|
307
|
+
reconciled: reconciled.reconciled,
|
|
308
|
+
currency: quote && quote.currency ? quote.currency : null,
|
|
309
|
+
transaction_id: (extra && extra.transaction_id) || (quote && quote.quote_id) || null,
|
|
310
|
+
settlement_ref: (extra && extra.settlement_ref) || null,
|
|
311
|
+
task_ref: taskRef
|
|
312
|
+
});
|
|
313
|
+
} catch (_) { /* non-fatal: a settled call is never undone by an accounting error */ }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function executeCommandRequest(state, payload, headers, options) {
|
|
317
|
+
const opts = options || {};
|
|
318
|
+
const requiredHeaders = Array.isArray(opts.requiredHeaders) && opts.requiredHeaders.length > 0
|
|
319
|
+
? opts.requiredHeaders
|
|
320
|
+
: ["x-wallet-id", "x-payment", "x-payment-signature"];
|
|
321
|
+
const challengePayload = typeof opts.challengePayload === "function"
|
|
322
|
+
? opts.challengePayload
|
|
323
|
+
: (quote) => ({ ok: false, quote });
|
|
324
|
+
|
|
325
|
+
hydrateQuoteStates(state);
|
|
326
|
+
const validation = validateIntegrationSelection(payload);
|
|
327
|
+
if (!validation.ok) {
|
|
328
|
+
return {
|
|
329
|
+
status: 400,
|
|
330
|
+
payload: {
|
|
331
|
+
ok: false,
|
|
332
|
+
reason: validation.reason,
|
|
333
|
+
errors: validation.errors,
|
|
334
|
+
selection: validation.selection
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const prefersCredits = payload && payload.payment_preference === "credits_first";
|
|
339
|
+
if (!hasPaymentHeaders(headers || {}, requiredHeaders)) {
|
|
340
|
+
const bearerToken = parseBearerToken(headers);
|
|
341
|
+
const delegatedSpendRequested = typeof bearerToken === "string" && bearerToken.startsWith("xytara_spend_");
|
|
342
|
+
const delegatedSpend = delegatedSpendRequested
|
|
343
|
+
? resolveSpendCredential(state, bearerToken, extractAgentProof(headers), {
|
|
344
|
+
account_id: payload && payload.account_id,
|
|
345
|
+
task_ref: payload && payload.task_ref,
|
|
346
|
+
units: payload && payload.quoted_units,
|
|
347
|
+
// Strict binding is scoped to this execute path (where a binding is supplied); the
|
|
348
|
+
// lifecycle paths pass no binding and are intentionally unaffected.
|
|
349
|
+
require: String(process.env.XYTARA_REQUIRE_SPEND_PROOF_BINDING || "").trim() === "1"
|
|
350
|
+
})
|
|
351
|
+
: { ok: false, reason: "spend_credential_not_requested" };
|
|
352
|
+
if (delegatedSpendRequested && !delegatedSpend.ok) {
|
|
353
|
+
return {
|
|
354
|
+
status: 403,
|
|
355
|
+
payload: {
|
|
356
|
+
ok: false,
|
|
357
|
+
reason: delegatedSpend.reason
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (delegatedSpend.ok) {
|
|
362
|
+
// Pin the command to the credential's own account — a delegated spend token must
|
|
363
|
+
// never be aimed at another account by supplying a different account_id.
|
|
364
|
+
if (payload && typeof payload.account_id === "string" && payload.account_id && payload.account_id !== delegatedSpend.credential.account_id) {
|
|
365
|
+
return { status: 403, payload: { ok: false, reason: "delegated_account_mismatch" } };
|
|
366
|
+
}
|
|
367
|
+
payload = {
|
|
368
|
+
...payload,
|
|
369
|
+
account_id: delegatedSpend.credential.account_id,
|
|
370
|
+
agent_id: payload && payload.agent_id ? payload.agent_id : delegatedSpend.credential.agent_id,
|
|
371
|
+
budget_id: payload && payload.budget_id ? payload.budget_id : delegatedSpend.credential.budget_id,
|
|
372
|
+
pack_id: delegatedSpend.credential.pack_id || (payload && payload.pack_id) || null,
|
|
373
|
+
entitlement_id: delegatedSpend.credential.entitlement_id || (payload && payload.entitlement_id) || null
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const quote = createQuote(state, payload);
|
|
377
|
+
if (prefersCredits) {
|
|
378
|
+
const creditPayload = {
|
|
379
|
+
...payload,
|
|
380
|
+
account_id: quote.account_id,
|
|
381
|
+
quoted_units: Number.isFinite(Number(payload.quoted_units)) ? Number(payload.quoted_units) : undefined
|
|
382
|
+
};
|
|
383
|
+
const meteringPreview = buildUsageMeterPreview(state, creditPayload);
|
|
384
|
+
const requiredUnits = meteringPreview.usage_units;
|
|
385
|
+
if (delegatedSpend.ok && delegatedSpend.binding) {
|
|
386
|
+
const authorityDecision = evaluateAuthorityBindingAction(delegatedSpend.binding, "delegated_credit_spend", requiredUnits);
|
|
387
|
+
if (!authorityDecision.ok) {
|
|
388
|
+
return {
|
|
389
|
+
status: 403,
|
|
390
|
+
payload: {
|
|
391
|
+
ok: false,
|
|
392
|
+
reason: authorityDecision.reason,
|
|
393
|
+
binding_id: authorityDecision.binding_id || null,
|
|
394
|
+
required_scope: authorityDecision.required_scope || null,
|
|
395
|
+
authority_scope: authorityDecision.authority_scope || [],
|
|
396
|
+
required_consequence_family: authorityDecision.required_consequence_family || null,
|
|
397
|
+
allowed_consequence_families: authorityDecision.allowed_consequence_families || [],
|
|
398
|
+
max_units: authorityDecision.max_units || null,
|
|
399
|
+
attempted_units: authorityDecision.attempted_units || requiredUnits
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const creditDecision = previewCreditSpend(state, creditPayload);
|
|
406
|
+
if (!creditDecision.ok || !creditDecision.credit_spend_preview || creditDecision.credit_spend_preview.executable_now !== true) {
|
|
407
|
+
return buildCreditRejectionResponse(creditDecision, quote);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const preCommitAuthorizationContext = delegatedSpend.ok
|
|
411
|
+
? delegatedSpend.authorization_context
|
|
412
|
+
: {
|
|
413
|
+
ok: true,
|
|
414
|
+
verification_mode: "account_credits",
|
|
415
|
+
wallet_id: quote.account_id,
|
|
416
|
+
age_seconds: 0,
|
|
417
|
+
payment_payload: {
|
|
418
|
+
credit_spend_status: "precommit"
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
const externalExecutionOptions = await buildExternalExecutionOptions(state, payload, quote, preCommitAuthorizationContext);
|
|
422
|
+
if (externalExecutionOptions.error) {
|
|
423
|
+
return {
|
|
424
|
+
status: externalExecutionOptions.error.status || 502,
|
|
425
|
+
payload: {
|
|
426
|
+
ok: false,
|
|
427
|
+
reason: externalExecutionOptions.error.reason || "external_execution_failed",
|
|
428
|
+
details: externalExecutionOptions.error.details || null,
|
|
429
|
+
credit_spend_status: "not_committed",
|
|
430
|
+
credit_spend_preview: creditDecision.credit_spend_preview,
|
|
431
|
+
quote
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const creditSpend = consumeAccountCredits(state, buildCreditCommitPayload(payload, quote, requiredUnits));
|
|
437
|
+
if (creditSpend.ok) {
|
|
438
|
+
const paymentVerification = {
|
|
439
|
+
...(delegatedSpend.ok
|
|
440
|
+
? delegatedSpend.authorization_context
|
|
441
|
+
: {
|
|
442
|
+
ok: true,
|
|
443
|
+
verification_mode: "account_credits",
|
|
444
|
+
wallet_id: quote.account_id,
|
|
445
|
+
age_seconds: 0,
|
|
446
|
+
payment_payload: {}
|
|
447
|
+
}),
|
|
448
|
+
payment_payload: {
|
|
449
|
+
...((delegatedSpend.ok && delegatedSpend.authorization_context && delegatedSpend.authorization_context.payment_payload) || {}),
|
|
450
|
+
credit_spend_id: creditSpend.credit_spend.credit_spend_id
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
attributeProviderSettlement(state, externalExecutionOptions, payload, quote, {
|
|
454
|
+
transaction_id: creditSpend.credit_spend.credit_spend_id
|
|
455
|
+
}, paymentVerification);
|
|
456
|
+
const creditResult = createCommandResult(state, {
|
|
457
|
+
...payload,
|
|
458
|
+
payment_preference: "credits_first"
|
|
459
|
+
}, quote, {
|
|
460
|
+
paymentVerification,
|
|
461
|
+
creditSpend: creditSpend.credit_spend,
|
|
462
|
+
skipPaymentLedger: true,
|
|
463
|
+
externalExecution: externalExecutionOptions.externalExecution || null
|
|
464
|
+
});
|
|
465
|
+
attachDeliveryReceipt(state, creditResult, payload, quote, paymentVerification, externalExecutionOptions);
|
|
466
|
+
return {
|
|
467
|
+
status: 200,
|
|
468
|
+
payload: creditResult
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
if (creditSpend.reason !== "insufficient_credits") {
|
|
472
|
+
return {
|
|
473
|
+
status: 403,
|
|
474
|
+
payload: {
|
|
475
|
+
ok: false,
|
|
476
|
+
reason: creditSpend.reason,
|
|
477
|
+
account_id: quote.account_id,
|
|
478
|
+
provider_execution_completed_but_credit_commit_failed: Boolean(externalExecutionOptions.externalExecution),
|
|
479
|
+
policy_guard: {
|
|
480
|
+
agent_id: creditSpend.agent_id || null,
|
|
481
|
+
budget_id: creditSpend.budget_id || null,
|
|
482
|
+
task_domain: creditSpend.task_domain || null,
|
|
483
|
+
policy_ref: creditSpend.policy_ref || null,
|
|
484
|
+
max_reserve_units: creditSpend.max_reserve_units || null,
|
|
485
|
+
allowed_job_domains: creditSpend.allowed_job_domains || null,
|
|
486
|
+
budget_status: creditSpend.budget_status || null,
|
|
487
|
+
agent_status: creditSpend.agent_status || null
|
|
488
|
+
},
|
|
489
|
+
quote
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
status: 402,
|
|
495
|
+
payload: {
|
|
496
|
+
ok: false,
|
|
497
|
+
reason: "insufficient_credits",
|
|
498
|
+
provider_execution_completed_but_credit_commit_failed: Boolean(externalExecutionOptions.externalExecution),
|
|
499
|
+
insufficient_credits: {
|
|
500
|
+
account_id: quote.account_id,
|
|
501
|
+
required_units: creditSpend.required_units,
|
|
502
|
+
available_units: creditSpend.available_units,
|
|
503
|
+
recommended_next_surfaces: {
|
|
504
|
+
credits_topup: "/v1/checkout/purchase-intents",
|
|
505
|
+
direct_payment: "/v1/payment-quotes"
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
quote
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
status: 402,
|
|
514
|
+
payload: await challengePayload(quote)
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const quote = payload.quote_id && state.quotes.get(payload.quote_id)
|
|
519
|
+
? state.quotes.get(payload.quote_id)
|
|
520
|
+
: createQuote(state, payload);
|
|
521
|
+
|
|
522
|
+
if (quote && quote.status === "expired") {
|
|
523
|
+
return {
|
|
524
|
+
status: 409,
|
|
525
|
+
payload: {
|
|
526
|
+
ok: false,
|
|
527
|
+
reason: "quote_expired",
|
|
528
|
+
quote
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (quote && (quote.status === "executed" || quote.status === "accepted")) {
|
|
534
|
+
return {
|
|
535
|
+
status: 409,
|
|
536
|
+
payload: {
|
|
537
|
+
ok: false,
|
|
538
|
+
reason: "quote_already_used",
|
|
539
|
+
quote
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Verify-execute-settle: for the x402 facilitator path, settlement is DEFERRED
|
|
545
|
+
// until execution succeeds, so a failed call never settles/charges. Other paths
|
|
546
|
+
// (native/credits, and the external rails L402/BOLT/MPP whose funds already moved
|
|
547
|
+
// before the proof) have nothing to defer and are unaffected.
|
|
548
|
+
const paymentVerification = await verifyPaymentAcceptance(state, headers || {}, requiredHeaders, quote, {
|
|
549
|
+
payment_protocol: opts.paymentProtocol || (payload && payload.payment_protocol) || null,
|
|
550
|
+
authorization_scheme: opts.authorizationScheme || null,
|
|
551
|
+
resource_url: opts.resourceUrl || null,
|
|
552
|
+
deferSettlement: true
|
|
553
|
+
});
|
|
554
|
+
if (!paymentVerification.ok) {
|
|
555
|
+
return {
|
|
556
|
+
status: paymentVerification.status || 403,
|
|
557
|
+
payload: {
|
|
558
|
+
ok: false,
|
|
559
|
+
reason: paymentVerification.reason,
|
|
560
|
+
...(paymentVerification.detail ? { detail: paymentVerification.detail } : {}),
|
|
561
|
+
quote
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
markPaymentAccepted(state, paymentVerification);
|
|
567
|
+
const settlementDeferred = Boolean(paymentVerification.deferred_settlement);
|
|
568
|
+
|
|
569
|
+
// Reserve an execution slot so the same signed x402 authorization cannot execute
|
|
570
|
+
// twice concurrently (the deferred settle no longer fronts the on-chain nonce as an
|
|
571
|
+
// implicit pre-execution guard). No-op for non-deferred lanes; fails open if the
|
|
572
|
+
// store is unavailable. The slot is released in the finally below, whatever happens.
|
|
573
|
+
const executionClaim = claimDeferredExecution(state, paymentVerification);
|
|
574
|
+
if (!executionClaim.ok) {
|
|
575
|
+
return {
|
|
576
|
+
status: executionClaim.status || 409,
|
|
577
|
+
payload: {
|
|
578
|
+
ok: false,
|
|
579
|
+
reason: executionClaim.reason || "payment_authorization_in_progress",
|
|
580
|
+
...(executionClaim.detail ? { detail: executionClaim.detail } : {}),
|
|
581
|
+
payment_settled: false,
|
|
582
|
+
quote
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
try {
|
|
588
|
+
const externalExecutionOptions = await buildExternalExecutionOptions(state, payload, quote, paymentVerification);
|
|
589
|
+
if (externalExecutionOptions.error) {
|
|
590
|
+
// Execution failed. Report the money state by what actually happened, not by lane:
|
|
591
|
+
// - deferred x402: we never settled, so the caller is NOT charged.
|
|
592
|
+
// - a concrete receipt is in hand (an x402 settlement already captured): the caller
|
|
593
|
+
// WAS charged, so surface the receipt and a refund path.
|
|
594
|
+
// - everything else (native/local_signed, presence, and the access-token rails
|
|
595
|
+
// whose proof we only validated): no receipt was produced on this request, so we
|
|
596
|
+
// do not claim a charge — a bare retry hint, never a false refund pointer.
|
|
597
|
+
const hasReceipt = Boolean(paymentVerification.settlement || paymentVerification.payment_response_base64);
|
|
598
|
+
return {
|
|
599
|
+
status: externalExecutionOptions.error.status || 502,
|
|
600
|
+
payload: {
|
|
601
|
+
ok: false,
|
|
602
|
+
reason: externalExecutionOptions.error.reason || "external_execution_failed",
|
|
603
|
+
details: externalExecutionOptions.error.details || null,
|
|
604
|
+
payment_settled: hasReceipt,
|
|
605
|
+
...(paymentVerification.settlement ? { settlement: paymentVerification.settlement } : {}),
|
|
606
|
+
remediation: settlementDeferred
|
|
607
|
+
? "execution failed before settlement; you were not charged — retry with corrected input"
|
|
608
|
+
: hasReceipt
|
|
609
|
+
? "payment was accepted but execution failed; retry with corrected input or request a refund with your payment proof"
|
|
610
|
+
: "execution failed; retry with corrected input",
|
|
611
|
+
quote
|
|
612
|
+
},
|
|
613
|
+
...(hasReceipt && paymentVerification.payment_response_base64
|
|
614
|
+
? { paymentResponse: paymentVerification.payment_response_base64 }
|
|
615
|
+
: {})
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Execution succeeded — settle the deferred payment now (no-op for non-deferred).
|
|
620
|
+
const settleResult = await settleDeferredPayment(paymentVerification);
|
|
621
|
+
if (!settleResult.ok) {
|
|
622
|
+
return {
|
|
623
|
+
status: settleResult.status || 402,
|
|
624
|
+
payload: {
|
|
625
|
+
ok: false,
|
|
626
|
+
reason: settleResult.reason || "settlement_failed",
|
|
627
|
+
...(settleResult.detail ? { detail: settleResult.detail } : {}),
|
|
628
|
+
payment_settled: false,
|
|
629
|
+
remediation: "the work executed but settlement failed; you were not charged — retry",
|
|
630
|
+
quote
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
// settleDeferredPayment is a no-op for non-deferred paths (returns {ok:true} with no
|
|
635
|
+
// settlement) — in that case keep the verification untouched; only when a deferred
|
|
636
|
+
// x402 settlement actually ran do we fold its receipt in and drop the deferred marker.
|
|
637
|
+
const settlementOccurred = Boolean(settleResult.settlement || settleResult.payment_response_base64);
|
|
638
|
+
const settledVerification = settlementOccurred
|
|
639
|
+
? { ...paymentVerification, settlement: settleResult.settlement, payment_response_base64: settleResult.payment_response_base64, deferred_settlement: undefined }
|
|
640
|
+
: paymentVerification;
|
|
641
|
+
|
|
642
|
+
attributeProviderSettlement(state, externalExecutionOptions, payload, quote, {
|
|
643
|
+
settlement_ref: (settledVerification.settlement && (settledVerification.settlement.transaction || settledVerification.settlement.txid)) || null
|
|
644
|
+
}, settledVerification);
|
|
645
|
+
const paidResult = createCommandResult(state, payload, quote, {
|
|
646
|
+
paymentVerification: settledVerification,
|
|
647
|
+
externalExecution: externalExecutionOptions.externalExecution || null
|
|
648
|
+
});
|
|
649
|
+
attachDeliveryReceipt(state, paidResult, payload, quote, settledVerification, externalExecutionOptions);
|
|
650
|
+
return {
|
|
651
|
+
status: 200,
|
|
652
|
+
payload: paidResult,
|
|
653
|
+
...(settledVerification.payment_response_base64
|
|
654
|
+
? { paymentResponse: settledVerification.payment_response_base64 }
|
|
655
|
+
: {})
|
|
656
|
+
};
|
|
657
|
+
} finally {
|
|
658
|
+
releaseDeferredExecution(state, executionClaim.key);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function executeAuthorizedCommandRequest(state, payload, authorizationContext) {
|
|
663
|
+
const validation = validateIntegrationSelection(payload);
|
|
664
|
+
if (!validation.ok) {
|
|
665
|
+
return {
|
|
666
|
+
status: 400,
|
|
667
|
+
payload: {
|
|
668
|
+
ok: false,
|
|
669
|
+
reason: validation.reason,
|
|
670
|
+
errors: validation.errors,
|
|
671
|
+
selection: validation.selection
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
hydrateQuoteStates(state);
|
|
677
|
+
|
|
678
|
+
const quote = createQuote(state, payload);
|
|
679
|
+
const paymentVerification = {
|
|
680
|
+
ok: true,
|
|
681
|
+
verification_mode: authorizationContext && authorizationContext.verification_mode
|
|
682
|
+
? authorizationContext.verification_mode
|
|
683
|
+
: "hosted_checkout",
|
|
684
|
+
wallet_id: authorizationContext && authorizationContext.wallet_id
|
|
685
|
+
? authorizationContext.wallet_id
|
|
686
|
+
: "hosted_checkout",
|
|
687
|
+
age_seconds: 0,
|
|
688
|
+
payment_payload: authorizationContext && authorizationContext.payment_payload
|
|
689
|
+
? authorizationContext.payment_payload
|
|
690
|
+
: null
|
|
691
|
+
};
|
|
692
|
+
const externalExecutionOptions = await buildExternalExecutionOptions(state, payload, quote, paymentVerification);
|
|
693
|
+
if (externalExecutionOptions.error) {
|
|
694
|
+
return {
|
|
695
|
+
status: externalExecutionOptions.error.status || 502,
|
|
696
|
+
payload: {
|
|
697
|
+
ok: false,
|
|
698
|
+
reason: externalExecutionOptions.error.reason || "external_execution_failed",
|
|
699
|
+
details: externalExecutionOptions.error.details || null
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
attributeProviderSettlement(state, externalExecutionOptions, payload, quote, undefined, paymentVerification);
|
|
705
|
+
const authorizedResult = createCommandResult(state, payload, quote, {
|
|
706
|
+
paymentVerification,
|
|
707
|
+
externalExecution: externalExecutionOptions.externalExecution || null
|
|
708
|
+
});
|
|
709
|
+
attachDeliveryReceipt(state, authorizedResult, payload, quote, paymentVerification, externalExecutionOptions);
|
|
710
|
+
return {
|
|
711
|
+
status: 200,
|
|
712
|
+
payload: authorizedResult
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
module.exports = {
|
|
717
|
+
executeAuthorizedCommandRequest,
|
|
718
|
+
executeCommandRequest,
|
|
719
|
+
buildExternalExecutionOptions,
|
|
720
|
+
reconcileProviderSettlementAmount,
|
|
721
|
+
sanitizeAuthorizationContextForProvider
|
|
722
|
+
};
|