xytara 3.1.11 → 3.1.13
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 +5 -0
- package/README.md +28 -5
- package/RELEASE_NOTES.md +4 -10
- package/lib/adapter_depth_contract.js +70 -8
- package/lib/command_flow.js +112 -17
- package/lib/commerce_artifacts.js +81 -20
- package/lib/delivery_receipt.js +225 -0
- package/lib/payment_verification.js +2 -1
- package/lib/provider_network_contract.js +1 -0
- package/package.json +1 -1
- package/scripts/verify_provider_network.js +32 -1
- package/scripts/verify_x402_facilitator.js +70 -0
- package/server.js +55 -2
package/.env.example
CHANGED
|
@@ -98,6 +98,11 @@ XYTARA_PROVIDER_FAILOVER_MAX=3
|
|
|
98
98
|
# routing reputation recency decay, factor in (0,1): fades older observations so recent performance
|
|
99
99
|
# weighs more. 1 (default) = no decay (lifetime counts). e.g. 0.95 once the network has real history.
|
|
100
100
|
XYTARA_PROVIDER_REPUTATION_DECAY=1
|
|
101
|
+
# Signed delivery receipts (one per paid call; public keys at GET /.well-known/delivery-receipt-keys).
|
|
102
|
+
# base64 PKCS#8 DER (or PEM) private keys. Ed25519 is the base signature; ML-DSA-65 adds the
|
|
103
|
+
# post-quantum leg (needs Node >= 24). Unset -> receipts are still issued but honestly marked unsigned.
|
|
104
|
+
XYTARA_DELIVERY_RECEIPT_ED25519_KEY=
|
|
105
|
+
XYTARA_DELIVERY_RECEIPT_MLDSA65_KEY=
|
|
101
106
|
# Provider payout auto-execution via an EXTERNAL signer (the runtime stays key-less). OFF unless the
|
|
102
107
|
# URL is set; otherwise payouts are recorded and the operator sends + confirms them. See services/payout-signer.
|
|
103
108
|
XYTARA_PAYOUT_SIGNER_URL=
|
package/README.md
CHANGED
|
@@ -4,7 +4,8 @@ Listed on [Smithery](https://smithery.ai/server/naxytra/xytara-mcp-executor) ·
|
|
|
4
4
|
|
|
5
5
|
**x402 pay-per-call agent-commerce runtime** — the payment and execution layer of the naxytra
|
|
6
6
|
machine stack. An agent discovers a capability, pays once with x402 (gasless USDC on Base), and gets
|
|
7
|
-
a real deliverable, with
|
|
7
|
+
a real deliverable, settled on-chain, with the revenue split reconciled in the ledger and paid out
|
|
8
|
+
on-chain, plus a signed delivery receipt. The proof-side half is
|
|
8
9
|
[xoonya](https://naxytra.com/xoonya).
|
|
9
10
|
|
|
10
11
|
## Proven live on Base mainnet
|
|
@@ -13,8 +14,8 @@ All demonstrated end to end on Base mainnet (provider-payout transactions are pu
|
|
|
13
14
|
see **Proof** below):
|
|
14
15
|
|
|
15
16
|
- **Open settling provider network** — a paid x402 call routes to an independently-registered
|
|
16
|
-
execution provider, returns a real deliverable, and splits revenue
|
|
17
|
-
platform keeps the remainder).
|
|
17
|
+
execution provider, returns a real deliverable, and splits revenue — reconciled in the ledger and
|
|
18
|
+
paid out on-chain (platform keeps the remainder).
|
|
18
19
|
- **Automated on-chain provider payouts** — a key-less runtime dispatches to an external signer that
|
|
19
20
|
sends USDC on Base; reconciled, idempotent, double-pay-safe.
|
|
20
21
|
- **Cryptographic bounded delegated spend** — a stolen bearer token cannot spend; each spend requires
|
|
@@ -30,10 +31,32 @@ endpoint: https://xytara-mcp-executor.onrender.com/mcp
|
|
|
30
31
|
tools/list -> free (17 deterministic tools)
|
|
31
32
|
tools/call -> 402 Payment Required with a price
|
|
32
33
|
-> sign ONE gasless USDC authorization (x402 / EIP-3009)
|
|
33
|
-
-> the Coinbase CDP facilitator verifies
|
|
34
|
-
-> the tool
|
|
34
|
+
-> the Coinbase CDP facilitator verifies it; the tool runs
|
|
35
|
+
-> only if the tool succeeds is the payment settled on Base (a failed call is not charged)
|
|
36
|
+
-> the response carries the result, the on-chain settlement receipt, and a signed
|
|
37
|
+
delivery receipt in result._meta["naxytra.com/delivery-receipt"]
|
|
35
38
|
```
|
|
36
39
|
|
|
40
|
+
## Signed delivery receipts
|
|
41
|
+
|
|
42
|
+
Every paid call returns a `naxytra-delivery-receipt/1`: the payment leg (for x402, the network,
|
|
43
|
+
asset, amount, payer, pay_to, and settlement transaction), the provider that delivered, and the
|
|
44
|
+
SHA-256 of the canonical request and of the delivered output — signed over its JCS (RFC 8785)
|
|
45
|
+
canonical form with Ed25519, plus ML-DSA-65 (FIPS-204) where the issuer has a post-quantum key. The
|
|
46
|
+
signed suite is part of the signed bytes, so stripping a signature fails verification.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
GET /.well-known/delivery-receipt-keys # the issuer's public keys (pin them)
|
|
50
|
+
GET /v1/delivery-receipts/:receipt_id # fetch a receipt the runtime issued
|
|
51
|
+
POST /v1/delivery-receipts/verify # free signature check
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Verify independently: recompute `sha256(JCS(output you received))`, check the settlement
|
|
55
|
+
transaction on-chain, and verify each signature over the canonical bytes with `node:crypto` or
|
|
56
|
+
xoonya's `verifyExternalProofSignature` (the receipt's `kid` is the key ref xoonya reports).
|
|
57
|
+
A receipt proves who delivered what for which paid call, unaltered since signing — **not** whether
|
|
58
|
+
the output was correct or good.
|
|
59
|
+
|
|
37
60
|
Tools: `summarize`, `text.extract`, `html.to-text`, `text.chunk`, `json.validate`, `zones.lookup`,
|
|
38
61
|
`keccak256`, `address.validate`, `abi.encode`, `abi.decode`, `merkle.verify`, `eip712.hash`,
|
|
39
62
|
`x402.decode`, `x402.verify-receipt`, `tx.status`, `reply.draft`, `catalog`.
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
`xytara` 3.1.
|
|
1
|
+
`xytara` 3.1.13 adds signed delivery receipts and makes the direct MCP path charge only on success.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
- **
|
|
6
|
-
- **Registration-scoped ledgers** — ledger rows and payouts belong to one registration of a `provider_id`. A removed and re-registered id never sees, inherits, or is credited by a previous owner's rows, and removal cancels that registration's pending payouts.
|
|
7
|
-
- **In-flight payout guard** — a payout being sent by the external signer cannot be cancelled mid-dispatch (so no freed reservation can ever be re-sent); a payout cancelled before a late tx report is reinstated for reconciliation instead of being lost.
|
|
8
|
-
- **One execution budget per call** — cold-start ride-out retries and failover candidates share a single wall-clock budget inside the x402 authorization window, and per-target timeouts are capped at 30s, so a routed call can always still settle.
|
|
9
|
-
- **Provider endpoint policy** — on hosted deploys, provider endpoints must be public `https` (loopback, private, link-local and metadata hosts are refused and re-checked by DNS at dispatch); redirects are never followed. Providers receive only a sanitized payment context — never the buyer's signed authorization.
|
|
10
|
-
- The MCP executor's bearer check is timing-safe and fails closed on a hosted deploy; adapter execution truth counts only enabled targets; rate-limit identity compares are timing-safe.
|
|
11
|
-
- Repository hygiene: a complete `.gitignore`, the publish workflow waits for the live deploy before registry publication, and `.env.example` documents every runtime knob.
|
|
3
|
+
- **Signed delivery receipts** (`naxytra-delivery-receipt/1`) on every paid call, from the runtime and from the MCP executor. A receipt binds the payment leg (for x402: network, asset, amount, payer, pay_to, settlement transaction), the provider that delivered, and the SHA-256 of the canonical request and of the delivered output. It is signed over its JCS (RFC 8785) canonical form with Ed25519, plus ML-DSA-65 (FIPS-204) where the issuer has a post-quantum key; the signed suite is inside the signed bytes, so stripping a signature fails verification. Public keys: `GET /.well-known/delivery-receipt-keys`; runtime receipts are retrievable at `GET /v1/delivery-receipts/:id`; free check at `POST /v1/delivery-receipts/verify`. Key ids use the xoonya key-ref format, so xoonya's verifier checks them too. Without a configured key a receipt is still issued and marked unsigned — never a fake signature. Boundary: a receipt proves who delivered what for which paid call, unaltered — not whether the output was correct.
|
|
4
|
+
- **Real x402 settlement records** — for an x402-paid call the runtime's settlement, treasury, and default-path records now carry the x402 rail, network, amount, payer, pay_to, and settlement transaction, instead of the quote's unsubmitted default rail.
|
|
5
|
+
- **MCP executor charges only on success** — `tools/call` now verifies the payment, runs the tool, and settles only if the call succeeded. A tool error, unknown tool, or bad params is never charged; if settlement fails after the work, the output is withheld and the buyer is not charged. A concurrent replay of the same signed authorization is refused (`409 payment_authorization_in_use`).
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const packageJson = require("../package.json");
|
|
4
4
|
const { catalog } = require("../fixtures/catalog");
|
|
5
5
|
const { loadExecutionTargetRegistry } = require("./external_execution_runtime");
|
|
6
|
+
const { listProviders } = require("./provider_registry");
|
|
6
7
|
|
|
7
8
|
function normalizeString(value, fallback = "") {
|
|
8
9
|
const raw = typeof value === "string" ? value.trim() : "";
|
|
@@ -59,10 +60,60 @@ function summarizeTarget(target) {
|
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
// ACTIVE open-network providers that declared this task_ref. Env targets resolve before the
|
|
64
|
+
// registry at call time, so a provider is shadowed (not routed) when an enabled env target pins
|
|
65
|
+
// the WHOLE lane (task_ref with no tool/integration selector); it is then listed but not counted.
|
|
66
|
+
function listRegistryProvidersForTask(state, taskRef, envTargets) {
|
|
67
|
+
if (!state) return [];
|
|
68
|
+
const normalizedTaskRef = normalizeString(taskRef);
|
|
69
|
+
const wholeLanePinned = envTargets.some((target) =>
|
|
70
|
+
target.enabled !== false
|
|
71
|
+
&& normalizeString(target.task_ref) === normalizedTaskRef
|
|
72
|
+
&& !normalizeString(target.tool_name)
|
|
73
|
+
&& !normalizeString(target.integration_id)
|
|
74
|
+
);
|
|
75
|
+
return listProviders(state, { status: "active" })
|
|
76
|
+
.filter((provider) => Array.isArray(provider.task_refs) && provider.task_refs.includes(normalizedTaskRef))
|
|
77
|
+
.map((provider) => {
|
|
78
|
+
const health = (provider.health && provider.health.status) || "healthy";
|
|
79
|
+
return {
|
|
80
|
+
target_id: `registry:${provider.provider_id}`,
|
|
81
|
+
provider: provider.provider_id,
|
|
82
|
+
routing: "open_provider_registry",
|
|
83
|
+
task_ref: normalizedTaskRef,
|
|
84
|
+
tool_name: null,
|
|
85
|
+
tool_names: Array.isArray(provider.tool_names) ? provider.tool_names.slice() : [],
|
|
86
|
+
integration_id: null,
|
|
87
|
+
enabled: !wholeLanePinned,
|
|
88
|
+
shadowed_by_env_target: wholeLanePinned,
|
|
89
|
+
health_status: health,
|
|
90
|
+
timeout_ms: provider.timeout_ms,
|
|
91
|
+
endpoint_configured: Boolean(normalizeString(provider.endpoint_url)),
|
|
92
|
+
auth_configured: Boolean(normalizeString(provider.auth_token)),
|
|
93
|
+
operational_coverage: {
|
|
94
|
+
execute_endpoint: Boolean(normalizeString(provider.endpoint_url)),
|
|
95
|
+
bearer_auth: Boolean(normalizeString(provider.auth_token)),
|
|
96
|
+
bounded_timeout: Number(provider.timeout_ms || 0) >= 500,
|
|
97
|
+
// The provider-network execution contract fixes the deliverable shape
|
|
98
|
+
// (result.output / output_payload), and activation is operator-vetted against it.
|
|
99
|
+
proof_fact_compatible: true,
|
|
100
|
+
deterministic_retry_surface: true,
|
|
101
|
+
tool_level_binding: Array.isArray(provider.tool_names) && provider.tool_names.length > 0,
|
|
102
|
+
operator_health_surface: "runtime_tracked_provider_health",
|
|
103
|
+
operator_tool_catalog_surface: null
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function summarizeAdapterTaskDepth(task, state) {
|
|
110
|
+
const envTargets = listTargetsForTask(task.task_ref).map(summarizeTarget);
|
|
111
|
+
const registryTargets = listRegistryProvidersForTask(state, task.task_ref, loadExecutionTargetRegistry());
|
|
112
|
+
const targets = envTargets.concat(registryTargets);
|
|
64
113
|
const enabledTargets = targets.filter((target) => target.enabled);
|
|
65
|
-
const toolNames = Array.from(new Set(enabledTargets
|
|
114
|
+
const toolNames = Array.from(new Set(enabledTargets
|
|
115
|
+
.flatMap((target) => [target.tool_name].concat(target.tool_names || []))
|
|
116
|
+
.filter(Boolean))).sort();
|
|
66
117
|
const allTargetsCovered = enabledTargets.length > 0 && enabledTargets.every((target) =>
|
|
67
118
|
target.operational_coverage.execute_endpoint
|
|
68
119
|
&& target.operational_coverage.bounded_timeout
|
|
@@ -80,6 +131,8 @@ function summarizeAdapterTaskDepth(task) {
|
|
|
80
131
|
provider_live: enabledTargets.length > 0,
|
|
81
132
|
configured_target_count: targets.length,
|
|
82
133
|
enabled_target_count: enabledTargets.length,
|
|
134
|
+
env_target_count: envTargets.length,
|
|
135
|
+
registry_provider_count: registryTargets.filter((target) => target.enabled).length,
|
|
83
136
|
configured_tool_count: toolNames.length,
|
|
84
137
|
configured_tools: toolNames,
|
|
85
138
|
execution_targets: targets,
|
|
@@ -89,11 +142,17 @@ function summarizeAdapterTaskDepth(task) {
|
|
|
89
142
|
};
|
|
90
143
|
}
|
|
91
144
|
|
|
92
|
-
|
|
93
|
-
|
|
145
|
+
// `state` (optional): the live runtime state. When given, ACTIVE open-network providers count as
|
|
146
|
+
// provider-live paths alongside operator env targets (each labelled routing:open_provider_registry).
|
|
147
|
+
// Without it the pack is env-target-only, exactly as before.
|
|
148
|
+
function buildAdapterDepthPack(state) {
|
|
149
|
+
const adapterTasks = listAdapterTasks().map((task) => summarizeAdapterTaskDepth(task, state));
|
|
94
150
|
const providerLiveTasks = adapterTasks.filter((task) => task.provider_live);
|
|
95
151
|
const deepenedTasks = adapterTasks.filter((task) => task.provider_depth_state === "provider_live_path_deepened");
|
|
96
152
|
const configuredTargets = loadExecutionTargetRegistry().filter((target) => target && target.enabled !== false);
|
|
153
|
+
const registryProviderIds = new Set(adapterTasks.flatMap((task) =>
|
|
154
|
+
task.execution_targets.filter((target) => target.routing === "open_provider_registry" && target.enabled).map((target) => target.provider)
|
|
155
|
+
));
|
|
97
156
|
return {
|
|
98
157
|
ok: true,
|
|
99
158
|
product: packageJson.name,
|
|
@@ -110,8 +169,10 @@ function buildAdapterDepthPack() {
|
|
|
110
169
|
deepened_provider_live_task_count: deepenedTasks.length,
|
|
111
170
|
configured_execution_target_count: configuredTargets.length,
|
|
112
171
|
configured_provider_count: new Set(configuredTargets.map((target) => normalizeString(target.provider))).size,
|
|
113
|
-
configured_tool_count: new Set(configuredTargets.map((target) => normalizeString(target.tool_name)).filter(Boolean)).size
|
|
172
|
+
configured_tool_count: new Set(configuredTargets.map((target) => normalizeString(target.tool_name)).filter(Boolean)).size,
|
|
173
|
+
registry_provider_count: registryProviderIds.size
|
|
114
174
|
},
|
|
175
|
+
depth_sources: state ? ["operator_env_targets", "open_provider_registry"] : ["operator_env_targets"],
|
|
115
176
|
adapter_tasks: adapterTasks,
|
|
116
177
|
production_depth_requirements: [
|
|
117
178
|
"at_least_one_enabled_execution_target_for_provider_backed_task",
|
|
@@ -136,8 +197,8 @@ function buildAdapterDepthPack() {
|
|
|
136
197
|
};
|
|
137
198
|
}
|
|
138
199
|
|
|
139
|
-
function summarizeAdapterDepthPack() {
|
|
140
|
-
const pack = buildAdapterDepthPack();
|
|
200
|
+
function summarizeAdapterDepthPack(state) {
|
|
201
|
+
const pack = buildAdapterDepthPack(state);
|
|
141
202
|
return {
|
|
142
203
|
ok: true,
|
|
143
204
|
product: pack.product,
|
|
@@ -145,6 +206,7 @@ function summarizeAdapterDepthPack() {
|
|
|
145
206
|
summary_version: "xytara-adapter-depth-summary-v1",
|
|
146
207
|
adapter_depth_state: pack.adapter_depth_state,
|
|
147
208
|
counts: pack.counts,
|
|
209
|
+
depth_sources: pack.depth_sources,
|
|
148
210
|
public_claim_boundary: pack.public_claim_boundary,
|
|
149
211
|
linked_surfaces: pack.linked_surfaces
|
|
150
212
|
};
|
package/lib/command_flow.js
CHANGED
|
@@ -11,6 +11,95 @@ const { validateIntegrationSelection } = require("../integrations/dispatch");
|
|
|
11
11
|
const { markPaymentAccepted, settleDeferredPayment, verifyPaymentAcceptance } = require("./payment_verification");
|
|
12
12
|
const { executeExternalTarget, resolveExecutionTarget } = require("./external_execution_runtime");
|
|
13
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
|
+
}
|
|
14
103
|
const { resolveRegisteredTargets, hasActiveMatchingProvider, recordProviderHealth, recordProviderRouting, recordProviderSettlement } = require("./provider_registry");
|
|
15
104
|
const { claimDeferredExecution, releaseDeferredExecution } = require("./execution_dedupe");
|
|
16
105
|
|
|
@@ -364,17 +453,19 @@ async function executeCommandRequest(state, payload, headers, options) {
|
|
|
364
453
|
attributeProviderSettlement(state, externalExecutionOptions, payload, quote, {
|
|
365
454
|
transaction_id: creditSpend.credit_spend.credit_spend_id
|
|
366
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);
|
|
367
466
|
return {
|
|
368
467
|
status: 200,
|
|
369
|
-
payload:
|
|
370
|
-
...payload,
|
|
371
|
-
payment_preference: "credits_first"
|
|
372
|
-
}, quote, {
|
|
373
|
-
paymentVerification,
|
|
374
|
-
creditSpend: creditSpend.credit_spend,
|
|
375
|
-
skipPaymentLedger: true,
|
|
376
|
-
externalExecution: externalExecutionOptions.externalExecution || null
|
|
377
|
-
})
|
|
468
|
+
payload: creditResult
|
|
378
469
|
};
|
|
379
470
|
}
|
|
380
471
|
if (creditSpend.reason !== "insufficient_credits") {
|
|
@@ -551,12 +642,14 @@ async function executeCommandRequest(state, payload, headers, options) {
|
|
|
551
642
|
attributeProviderSettlement(state, externalExecutionOptions, payload, quote, {
|
|
552
643
|
settlement_ref: (settledVerification.settlement && (settledVerification.settlement.transaction || settledVerification.settlement.txid)) || null
|
|
553
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);
|
|
554
650
|
return {
|
|
555
651
|
status: 200,
|
|
556
|
-
payload:
|
|
557
|
-
paymentVerification: settledVerification,
|
|
558
|
-
externalExecution: externalExecutionOptions.externalExecution || null
|
|
559
|
-
}),
|
|
652
|
+
payload: paidResult,
|
|
560
653
|
...(settledVerification.payment_response_base64
|
|
561
654
|
? { paymentResponse: settledVerification.payment_response_base64 }
|
|
562
655
|
: {})
|
|
@@ -609,12 +702,14 @@ async function executeAuthorizedCommandRequest(state, payload, authorizationCont
|
|
|
609
702
|
}
|
|
610
703
|
|
|
611
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);
|
|
612
710
|
return {
|
|
613
711
|
status: 200,
|
|
614
|
-
payload:
|
|
615
|
-
paymentVerification,
|
|
616
|
-
externalExecution: externalExecutionOptions.externalExecution || null
|
|
617
|
-
})
|
|
712
|
+
payload: authorizedResult
|
|
618
713
|
};
|
|
619
714
|
}
|
|
620
715
|
|
|
@@ -26,6 +26,26 @@ function assessLatency(declaredLatencyClass, observedLatencyClass) {
|
|
|
26
26
|
return observed <= declared ? "within_posture" : "outside_posture";
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// The x402 facts of a payment verified (and, once settled, settled) through the facilitator, or
|
|
30
|
+
// null for any other lane. For an x402-paid call these replace the quote's default settlement
|
|
31
|
+
// rail in the artifacts, so a consumer never reads an unsubmitted placeholder rail as the real one.
|
|
32
|
+
function x402PaymentFacts(paymentVerification) {
|
|
33
|
+
const verification = paymentVerification && typeof paymentVerification === "object" ? paymentVerification : null;
|
|
34
|
+
if (!verification || verification.verification_mode !== "x402_facilitator") return null;
|
|
35
|
+
const paymentPayload = verification.payment_payload && typeof verification.payment_payload === "object" ? verification.payment_payload : {};
|
|
36
|
+
const settlement = verification.settlement && typeof verification.settlement === "object" ? verification.settlement : null;
|
|
37
|
+
const txid = settlement && (settlement.transaction || settlement.txid) ? String(settlement.transaction || settlement.txid) : null;
|
|
38
|
+
return {
|
|
39
|
+
network: paymentPayload.x402_network || (settlement && settlement.network) || null,
|
|
40
|
+
asset: paymentPayload.x402_asset || null,
|
|
41
|
+
amount: paymentPayload.x402_amount !== undefined && paymentPayload.x402_amount !== null ? String(paymentPayload.x402_amount) : null,
|
|
42
|
+
payer: paymentPayload.x402_payer || (settlement && settlement.payer) || verification.wallet_id || null,
|
|
43
|
+
pay_to: paymentPayload.x402_pay_to || null,
|
|
44
|
+
txid,
|
|
45
|
+
settled: Boolean(settlement && settlement.success !== false && txid)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
function createCommandArtifacts(payload, quote, options) {
|
|
30
50
|
const opts = options || {};
|
|
31
51
|
const transactionId = `txn_${crypto.randomBytes(6).toString("hex")}`;
|
|
@@ -241,28 +261,61 @@ function createCommandArtifacts(payload, quote, options) {
|
|
|
241
261
|
? opts.paymentVerification.payment_payload
|
|
242
262
|
: null
|
|
243
263
|
);
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
264
|
+
const x402 = x402PaymentFacts(opts.paymentVerification);
|
|
265
|
+
const settlement = x402
|
|
266
|
+
? {
|
|
267
|
+
settlement_id: `settlement_${transactionId}`,
|
|
268
|
+
mode: "x402_facilitator",
|
|
269
|
+
preference: quote.settlement_preference || null,
|
|
270
|
+
adapter: "x402_facilitator",
|
|
271
|
+
integration_id: "builtin.payment.x402",
|
|
272
|
+
binding_hash: quote.settlement_binding_hash || null,
|
|
273
|
+
rail: "x402",
|
|
274
|
+
network: x402.network,
|
|
275
|
+
asset: x402.asset,
|
|
276
|
+
amount: x402.amount,
|
|
277
|
+
payer: x402.payer,
|
|
278
|
+
pay_to: x402.pay_to,
|
|
279
|
+
hints: null,
|
|
280
|
+
txid: x402.txid,
|
|
281
|
+
raw_tx: null,
|
|
282
|
+
status: x402.settled ? "settled" : deferredCompletion ? "pending_completion" : "verified_not_settled",
|
|
283
|
+
recorded_at_iso: deferredCompletion ? null : completedAt.toISOString()
|
|
284
|
+
}
|
|
285
|
+
: {
|
|
286
|
+
settlement_id: `settlement_${transactionId}`,
|
|
287
|
+
mode: quote.settlement_mode || null,
|
|
288
|
+
preference: quote.settlement_preference || null,
|
|
289
|
+
adapter: quote.settlement_adapter || null,
|
|
290
|
+
integration_id: quote.settlement_integration_id || null,
|
|
291
|
+
binding_hash: quote.settlement_binding_hash || null,
|
|
292
|
+
rail: isBsvTeranodeSettlementMode(quote.settlement_mode) ? "bsv_teranode" : null,
|
|
293
|
+
network: bsvHints ? bsvHints.network : null,
|
|
294
|
+
hints: bsvHints,
|
|
295
|
+
txid: bsvPaymentReference && bsvPaymentReference.txid ? bsvPaymentReference.txid : null,
|
|
296
|
+
raw_tx: bsvPaymentReference && (bsvPaymentReference.raw_tx || bsvPaymentReference.rawTx)
|
|
297
|
+
? (bsvPaymentReference.raw_tx || bsvPaymentReference.rawTx)
|
|
298
|
+
: null,
|
|
299
|
+
status: deferredCompletion ? "pending_completion" : "recorded",
|
|
300
|
+
recorded_at_iso: deferredCompletion ? null : completedAt.toISOString()
|
|
301
|
+
};
|
|
302
|
+
const treasuryDestination = x402
|
|
303
|
+
? {
|
|
304
|
+
destination_id: `x402_${x402.network || "network"}_${x402.pay_to || "pay_to"}`,
|
|
305
|
+
destination_kind: "x402_pay_to_address",
|
|
306
|
+
account_id: quote.account_id,
|
|
307
|
+
settlement_mode: "x402_facilitator",
|
|
308
|
+
external_ref: x402.pay_to,
|
|
309
|
+
network: x402.network,
|
|
310
|
+
rail: "x402"
|
|
311
|
+
}
|
|
312
|
+
: buildTreasuryDestination(quote.account_id, quote.settlement_mode);
|
|
262
313
|
const treasury = {
|
|
263
314
|
...treasuryDestination,
|
|
264
315
|
treasury_receipt_id: `treasury_${receiptId}`,
|
|
265
|
-
status:
|
|
316
|
+
status: x402
|
|
317
|
+
? (x402.settled ? "settled" : deferredCompletion ? "pending_settlement" : "recorded")
|
|
318
|
+
: deferredCompletion ? "pending_settlement" : "recorded",
|
|
266
319
|
amount_minor: quote.amount_minor,
|
|
267
320
|
currency: quote.currency,
|
|
268
321
|
payment_id: payment.payment_id,
|
|
@@ -271,7 +324,7 @@ function createCommandArtifacts(payload, quote, options) {
|
|
|
271
324
|
rail: treasuryDestination.rail || settlement.rail || null,
|
|
272
325
|
recorded_at_iso: deferredCompletion ? null : completedAt.toISOString()
|
|
273
326
|
};
|
|
274
|
-
const
|
|
327
|
+
const quotedDefaultPath = quote.default_path || buildDefaultEconomicPath(
|
|
275
328
|
Array.isArray(quote.integration_ids)
|
|
276
329
|
? quote.integration_ids.find((integrationId) => typeof integrationId === "string" && integrationId.startsWith("builtin.payment."))
|
|
277
330
|
: null,
|
|
@@ -283,6 +336,14 @@ function createCommandArtifacts(payload, quote, options) {
|
|
|
283
336
|
status: deferredCompletion ? "pending_completion" : "recorded"
|
|
284
337
|
}
|
|
285
338
|
);
|
|
339
|
+
const defaultPath = x402
|
|
340
|
+
? {
|
|
341
|
+
...quotedDefaultPath,
|
|
342
|
+
payment_protocol: "x402",
|
|
343
|
+
settlement_mode: "x402_facilitator",
|
|
344
|
+
treasury_destination_id: treasuryDestination.destination_id
|
|
345
|
+
}
|
|
346
|
+
: quotedDefaultPath;
|
|
286
347
|
const transaction = {
|
|
287
348
|
transaction_id: transactionId,
|
|
288
349
|
account_id: quote.account_id,
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Signed delivery receipts ("naxytra-delivery-receipt/1"). One per paid call, binding WHAT was
|
|
4
|
+
// paid (the x402 settlement or other payment fact), WHO delivered (provider), and WHAT was
|
|
5
|
+
// delivered (sha256 of the canonical request and of the canonical output) — signed so anyone can
|
|
6
|
+
// re-verify it offline against the issuer's published keys.
|
|
7
|
+
//
|
|
8
|
+
// Boundary: a valid receipt proves this issuer attests that this provider returned this exact
|
|
9
|
+
// output for this paid call, unaltered since signing. It does NOT judge whether the output was
|
|
10
|
+
// correct or good — that is the consumer's (e.g. an arbitrator's) decision.
|
|
11
|
+
//
|
|
12
|
+
// Signed bytes = JCS (RFC 8785) canonical JSON of the receipt WITHOUT its `sig` member.
|
|
13
|
+
// Signatures = one per algorithm in `signing.suite` (Ed25519 always; ML-DSA-65 when configured
|
|
14
|
+
// and the runtime supports it). The suite and key ids are INSIDE the signed bytes,
|
|
15
|
+
// so stripping a signature (downgrade) fails verification.
|
|
16
|
+
// Key ids = "<key type>-spki-sha256:<hex>", the same key-ref format the xoonya verifier
|
|
17
|
+
// reports, so a receipt verifies with xoonya's verifyExternalProofSignature too.
|
|
18
|
+
//
|
|
19
|
+
// Dependency-free (node:crypto). This file is shared verbatim by the xytara runtime
|
|
20
|
+
// (lib/delivery_receipt.js) and the MCP executor (services/mcp-execution-target/delivery_receipt.js);
|
|
21
|
+
// a verifier asserts the two copies stay byte-identical.
|
|
22
|
+
|
|
23
|
+
const crypto = require("crypto");
|
|
24
|
+
|
|
25
|
+
const RECEIPT_VERSION = "naxytra-delivery-receipt/1";
|
|
26
|
+
const CANONICALIZATION = "JCS (RFC 8785) of the receipt without `sig`";
|
|
27
|
+
const SUPPORTED_ALGS = { ed25519: "EdDSA", "ml-dsa-65": "ML-DSA-65" };
|
|
28
|
+
|
|
29
|
+
// JCS for the JSON values a receipt carries (strings, finite numbers, booleans, null, arrays,
|
|
30
|
+
// objects). Object keys sorted by UTF-16 code units; `undefined` members dropped like JSON.
|
|
31
|
+
function canonicalize(value) {
|
|
32
|
+
if (value === null || typeof value !== "object") {
|
|
33
|
+
if (typeof value === "number" && !Number.isFinite(value)) return "null";
|
|
34
|
+
return value === undefined ? "null" : JSON.stringify(value);
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(value)) return "[" + value.map((item) => canonicalize(item === undefined ? null : item)).join(",") + "]";
|
|
37
|
+
return "{" + Object.keys(value)
|
|
38
|
+
.filter((key) => value[key] !== undefined)
|
|
39
|
+
.sort()
|
|
40
|
+
.map((key) => JSON.stringify(key) + ":" + canonicalize(value[key]))
|
|
41
|
+
.join(",") + "}";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sha256Hex(input) {
|
|
45
|
+
return crypto.createHash("sha256").update(Buffer.isBuffer(input) ? input : Buffer.from(String(input), "utf8")).digest("hex");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The hash a consumer recomputes from the JSON value they hold (request body or delivered output).
|
|
49
|
+
function hashCanonical(value) {
|
|
50
|
+
return sha256Hex(canonicalize(value === undefined ? null : value));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function keyRef(publicKey) {
|
|
54
|
+
const spki = publicKey.export({ type: "spki", format: "der" });
|
|
55
|
+
return `${publicKey.asymmetricKeyType}-spki-sha256:${sha256Hex(spki)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parsePrivateKey(raw) {
|
|
59
|
+
const text = String(raw || "").trim();
|
|
60
|
+
if (!text) return null;
|
|
61
|
+
if (text.includes("BEGIN ")) return crypto.createPrivateKey({ key: text, format: "pem" });
|
|
62
|
+
return crypto.createPrivateKey({ key: Buffer.from(text, "base64"), format: "der", type: "pkcs8" });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Loads the issuer's signing keys from environment variables `${prefix}_ED25519_KEY` and
|
|
66
|
+
// `${prefix}_MLDSA65_KEY` (base64 PKCS#8 DER, or PEM). Never throws: a missing or unusable key is
|
|
67
|
+
// reported in `problems` and simply not used, so receipts degrade to fewer signatures (or to
|
|
68
|
+
// unsigned) and say so — they never claim a signature that was not made.
|
|
69
|
+
const signerCache = new Map();
|
|
70
|
+
function loadDeliveryReceiptSigners(prefix, env = process.env) {
|
|
71
|
+
const sources = [
|
|
72
|
+
{ variable: `${prefix}_ED25519_KEY`, expected: "ed25519" },
|
|
73
|
+
{ variable: `${prefix}_MLDSA65_KEY`, expected: "ml-dsa-65" }
|
|
74
|
+
];
|
|
75
|
+
const cacheKey = sources.map((source) => `${source.variable}=${env[source.variable] || ""}`).join("\n");
|
|
76
|
+
if (signerCache.has(cacheKey)) return signerCache.get(cacheKey);
|
|
77
|
+
const signers = [];
|
|
78
|
+
const problems = [];
|
|
79
|
+
for (const source of sources) {
|
|
80
|
+
const raw = env[source.variable];
|
|
81
|
+
if (!raw || !String(raw).trim()) continue;
|
|
82
|
+
try {
|
|
83
|
+
const privateKey = parsePrivateKey(raw);
|
|
84
|
+
if (privateKey.asymmetricKeyType !== source.expected) {
|
|
85
|
+
problems.push({ variable: source.variable, reason: "key_type_mismatch", detail: `expected ${source.expected}, got ${privateKey.asymmetricKeyType}` });
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const publicKey = crypto.createPublicKey(privateKey);
|
|
89
|
+
signers.push({ alg: SUPPORTED_ALGS[source.expected], kid: keyRef(publicKey), privateKey, publicKey });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
problems.push({
|
|
92
|
+
variable: source.variable,
|
|
93
|
+
reason: source.expected === "ml-dsa-65" ? "ml_dsa_key_unusable_or_unsupported_on_this_runtime" : "key_unparseable",
|
|
94
|
+
detail: error && error.message ? error.message.slice(0, 160) : "unparseable"
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Deterministic suite order: EdDSA first, then ML-DSA-65.
|
|
99
|
+
signers.sort((a, b) => (a.alg === "EdDSA" ? -1 : 1) - (b.alg === "EdDSA" ? -1 : 1));
|
|
100
|
+
const loaded = { signers, problems };
|
|
101
|
+
signerCache.set(cacheKey, loaded);
|
|
102
|
+
return loaded;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let mlDsaSupport = null;
|
|
106
|
+
function runtimeSupportsMlDsa() {
|
|
107
|
+
if (mlDsaSupport === null) {
|
|
108
|
+
try {
|
|
109
|
+
mlDsaSupport = Boolean(crypto.generateKeyPairSync("ml-dsa-44").publicKey);
|
|
110
|
+
} catch (_) {
|
|
111
|
+
mlDsaSupport = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return mlDsaSupport;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Builds and signs a receipt. `fields` = { receipt_id, issuer, call, payment, delivery }.
|
|
118
|
+
function issueDeliveryReceipt(fields, signers, now = new Date()) {
|
|
119
|
+
const f = fields && typeof fields === "object" ? fields : {};
|
|
120
|
+
const activeSigners = Array.isArray(signers) ? signers : [];
|
|
121
|
+
const body = {
|
|
122
|
+
v: RECEIPT_VERSION,
|
|
123
|
+
receipt_id: f.receipt_id || `drcpt_${crypto.randomBytes(8).toString("hex")}`,
|
|
124
|
+
issuer: f.issuer || null,
|
|
125
|
+
issued_at: now.toISOString(),
|
|
126
|
+
call: f.call || null,
|
|
127
|
+
payment: f.payment || null,
|
|
128
|
+
delivery: f.delivery || null,
|
|
129
|
+
hashing: { algorithm: "sha256", canonicalization: "JCS (RFC 8785)" },
|
|
130
|
+
signing: {
|
|
131
|
+
canonicalization: CANONICALIZATION,
|
|
132
|
+
suite: activeSigners.map((signer) => signer.alg),
|
|
133
|
+
kids: activeSigners.map((signer) => signer.kid),
|
|
134
|
+
policy: "every_listed_signature_must_verify"
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
if (activeSigners.length === 0) {
|
|
138
|
+
return { ...body, sig: { state: "unsigned_signing_key_not_configured", signatures: [] } };
|
|
139
|
+
}
|
|
140
|
+
const signedBytes = Buffer.from(canonicalize(body), "utf8");
|
|
141
|
+
return {
|
|
142
|
+
...body,
|
|
143
|
+
sig: {
|
|
144
|
+
state: "signed",
|
|
145
|
+
signatures: activeSigners.map((signer) => ({
|
|
146
|
+
alg: signer.alg,
|
|
147
|
+
kid: signer.kid,
|
|
148
|
+
value: crypto.sign(null, signedBytes, signer.privateKey).toString("base64")
|
|
149
|
+
}))
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// The issuer's public key set (safe to publish).
|
|
155
|
+
function publicKeySet(signers) {
|
|
156
|
+
return (Array.isArray(signers) ? signers : []).map((signer) => ({
|
|
157
|
+
kid: signer.kid,
|
|
158
|
+
alg: signer.alg,
|
|
159
|
+
public_key_spki_der_base64: signer.publicKey.export({ type: "spki", format: "der" }).toString("base64"),
|
|
160
|
+
...(signer.alg === "EdDSA" ? { jwk: signer.publicKey.export({ format: "jwk" }) } : {})
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Verifies a receipt against a set of trusted public keys ([{ kid, alg, public_key_spki_der_base64 }]).
|
|
165
|
+
// Valid only if the receipt is signed, every algorithm in the SIGNED suite has a signature from the
|
|
166
|
+
// SIGNED kid, every such kid is in the trusted set, and every signature verifies.
|
|
167
|
+
function verifyDeliveryReceipt(receipt, trustedKeys) {
|
|
168
|
+
const r = receipt && typeof receipt === "object" && !Array.isArray(receipt) ? receipt : null;
|
|
169
|
+
if (!r) return { valid: false, reason: "receipt_required" };
|
|
170
|
+
if (r.v !== RECEIPT_VERSION) return { valid: false, reason: "unsupported_receipt_version", version: r.v || null };
|
|
171
|
+
const signing = r.signing && typeof r.signing === "object" ? r.signing : null;
|
|
172
|
+
const sig = r.sig && typeof r.sig === "object" ? r.sig : null;
|
|
173
|
+
if (!signing || !Array.isArray(signing.suite) || !Array.isArray(signing.kids) || signing.suite.length === 0) {
|
|
174
|
+
return { valid: false, reason: "receipt_unsigned" };
|
|
175
|
+
}
|
|
176
|
+
if (!sig || sig.state !== "signed" || !Array.isArray(sig.signatures)) return { valid: false, reason: "receipt_unsigned" };
|
|
177
|
+
const { sig: _omit, ...unsigned } = r;
|
|
178
|
+
const signedBytes = Buffer.from(canonicalize(unsigned), "utf8");
|
|
179
|
+
const trusted = new Map((Array.isArray(trustedKeys) ? trustedKeys : []).map((key) => [key.kid, key]));
|
|
180
|
+
const results = signing.suite.map((alg, index) => {
|
|
181
|
+
const kid = signing.kids[index];
|
|
182
|
+
const entry = sig.signatures.find((item) => item && item.alg === alg && item.kid === kid);
|
|
183
|
+
if (!entry) return { alg, kid, valid: false, reason: "signature_missing_for_signed_suite" };
|
|
184
|
+
const key = trusted.get(kid);
|
|
185
|
+
if (!key || key.alg !== alg) return { alg, kid, valid: false, reason: "kid_not_in_trusted_key_set" };
|
|
186
|
+
try {
|
|
187
|
+
const publicKey = crypto.createPublicKey({ key: Buffer.from(key.public_key_spki_der_base64, "base64"), format: "der", type: "spki" });
|
|
188
|
+
if (keyRef(publicKey) !== kid) return { alg, kid, valid: false, reason: "kid_does_not_match_key" };
|
|
189
|
+
const ok = crypto.verify(null, signedBytes, publicKey, Buffer.from(String(entry.value || ""), "base64"));
|
|
190
|
+
return { alg, kid, valid: ok, reason: ok ? null : "signature_invalid" };
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return { alg, kid, valid: false, reason: "verification_error", detail: error && error.message ? error.message.slice(0, 120) : null };
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
const valid = results.length > 0 && results.every((item) => item.valid);
|
|
196
|
+
return { valid, reason: valid ? null : "signature_check_failed", signatures: results, signed_bytes_sha256: sha256Hex(signedBytes) };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// A short, self-contained recipe for verifying a receipt without trusting the issuer's server.
|
|
200
|
+
function verificationRecipe(keysUrl) {
|
|
201
|
+
return {
|
|
202
|
+
signed_bytes: "JCS (RFC 8785) canonical JSON of the receipt with its `sig` member removed (UTF-8).",
|
|
203
|
+
keys: keysUrl || null,
|
|
204
|
+
steps: [
|
|
205
|
+
"1. Fetch the issuer's public keys; check each kid in receipt.signing.kids is listed (and pin them).",
|
|
206
|
+
"2. For every alg in receipt.signing.suite, take the signature with the matching kid from receipt.sig.signatures.",
|
|
207
|
+
"3. Verify it over the signed bytes with node:crypto (crypto.verify(null, bytes, spkiPublicKey, signature)) or xoonya's verifyExternalProofSignature({ material, signature, algorithm, public_key, expected_key_ref: kid }).",
|
|
208
|
+
"4. Recompute sha256(JCS(output you received)) and compare with delivery.output_sha256; for x402 calls check payment.settlement_tx on-chain (amount, payer, pay_to)."
|
|
209
|
+
],
|
|
210
|
+
boundary: "Proves who delivered what for which paid call, unaltered since signing — not whether the output was correct or good."
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = {
|
|
215
|
+
RECEIPT_VERSION,
|
|
216
|
+
canonicalize,
|
|
217
|
+
hashCanonical,
|
|
218
|
+
keyRef,
|
|
219
|
+
loadDeliveryReceiptSigners,
|
|
220
|
+
runtimeSupportsMlDsa,
|
|
221
|
+
issueDeliveryReceipt,
|
|
222
|
+
publicKeySet,
|
|
223
|
+
verifyDeliveryReceipt,
|
|
224
|
+
verificationRecipe
|
|
225
|
+
};
|
|
@@ -196,7 +196,8 @@ async function verifyPaymentAcceptance(state, headers, requiredHeaders, quote, o
|
|
|
196
196
|
x402_payer: auth.authorization.payer,
|
|
197
197
|
x402_network: auth.authorization.paymentRequirements.network,
|
|
198
198
|
x402_asset: auth.authorization.paymentRequirements.asset,
|
|
199
|
-
x402_amount: auth.authorization.paymentRequirements.amount
|
|
199
|
+
x402_amount: auth.authorization.paymentRequirements.amount,
|
|
200
|
+
x402_pay_to: auth.authorization.paymentRequirements.payTo || null
|
|
200
201
|
}
|
|
201
202
|
};
|
|
202
203
|
// Deferred settlement: verify now, settle only AFTER execution succeeds, so a
|
|
@@ -121,6 +121,7 @@ function buildProviderNetworkContract() {
|
|
|
121
121
|
economics: {
|
|
122
122
|
settlement: "Buyers pay via x402 (gasless USDC on Base). The runtime settles on-chain only AFTER your execution succeeds — a failed call does not charge the buyer.",
|
|
123
123
|
revenue_share: "Your provider_share = floor(settled_amount * revenue_share_bps / 10000); the platform keeps the remainder (incl. rounding). settled_amount = min(quote, the amount actually settled on-chain for the call) — never money that was not collected; each ledger row records its settlement_basis.",
|
|
124
|
+
delivery_receipts: "Every paid call routed to you returns the buyer a signed delivery receipt (naxytra-delivery-receipt/1) naming your provider_id and the sha256 of the request and of the output you returned, bound to the payment (for x402, the settlement tx). Keys: GET /.well-known/delivery-receipt-keys. It attests what you delivered — never whether it was correct.",
|
|
124
125
|
payouts: "Your accrued share is paid to your payout_address by the operator (reconciled ledger: create -> on-chain send -> confirm). Amounts are tracked in the quote's minor unit (USD_CENTS). Ledger rows and payouts are scoped to your registration: a removed-and-re-registered provider_id starts a fresh ledger and never inherits another owner's rows."
|
|
125
126
|
},
|
|
126
127
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xytara",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.13",
|
|
4
4
|
"description": "x402 pay-per-call agent-commerce runtime: gasless USDC on Base, an open settling provider network with a ledger-reconciled revenue split and automated on-chain payouts (key-less runtime, delegated signer), and bounded cryptographic delegated spend. Proven live on Base mainnet.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -769,7 +769,38 @@ async function hardeningChecks() {
|
|
|
769
769
|
} finally {
|
|
770
770
|
global.fetch = orig;
|
|
771
771
|
}
|
|
772
|
-
|
|
772
|
+
// 19. ADAPTER DEPTH counts active open-network providers (labelled registry-routed) when given
|
|
773
|
+
// state; without state it stays env-target-only; a whole-lane env pin shadows the registry.
|
|
774
|
+
const { buildAdapterDepthPack, summarizeAdapterDepthPack } = require("../lib/adapter_depth_contract");
|
|
775
|
+
const prevTargets = process.env.XYTARA_EXECUTION_TARGETS_JSON;
|
|
776
|
+
delete process.env.XYTARA_EXECUTION_TARGETS_JSON;
|
|
777
|
+
try {
|
|
778
|
+
const ad = {};
|
|
779
|
+
assert.strictEqual(summarizeAdapterDepthPack(ad).adapter_depth_state, "provider_wiring_required", "no env targets + no providers -> wiring required");
|
|
780
|
+
reg.registerProvider(ad, { provider_id: "ad_mcp", endpoint_url: "https://ad.example/execute", task_refs: ["adapter.mcp.invoke"], tool_names: ["keccak256"], revenue_share_bps: 7000, auth_token: "t" });
|
|
781
|
+
assert.strictEqual(summarizeAdapterDepthPack(ad).adapter_depth_state, "provider_wiring_required", "a PENDING provider is not a live path");
|
|
782
|
+
reg.setProviderStatus(ad, "ad_mcp", "active");
|
|
783
|
+
let pack = buildAdapterDepthPack(ad);
|
|
784
|
+
assert.strictEqual(pack.adapter_depth_state, "initial_provider_live_path", "one active registry provider -> initial live path");
|
|
785
|
+
const invoke = pack.adapter_tasks.find((t) => t.task_ref === "adapter.mcp.invoke");
|
|
786
|
+
assert.strictEqual(invoke.registry_provider_count, 1, "registry provider counted on its declared task");
|
|
787
|
+
assert.strictEqual(invoke.execution_targets[0].routing, "open_provider_registry", "labelled registry-routed");
|
|
788
|
+
assert.strictEqual(invoke.execution_targets[0].auth_token, undefined, "no auth_token leaks into the depth pack");
|
|
789
|
+
assert.deepStrictEqual(pack.public_claim_boundary.live_task_refs, ["adapter.mcp.invoke"], "claim limited to the declared task");
|
|
790
|
+
assert.strictEqual(buildAdapterDepthPack().adapter_depth_state, "provider_wiring_required", "no state -> env-only (unchanged for internal callers)");
|
|
791
|
+
reg.registerProvider(ad, { provider_id: "ad_mcp2", endpoint_url: "https://ad2.example/execute", task_refs: ["adapter.mcp.invoke"], revenue_share_bps: 7000 });
|
|
792
|
+
reg.setProviderStatus(ad, "ad_mcp2", "active");
|
|
793
|
+
assert.strictEqual(summarizeAdapterDepthPack(ad).adapter_depth_state, "provider_live_path_deepened", "two covered live paths -> deepened");
|
|
794
|
+
process.env.XYTARA_EXECUTION_TARGETS_JSON = JSON.stringify([{ target_id: "pin", task_ref: "adapter.mcp.invoke", endpoint_url: "https://pin.example/execute", provider: "pin_provider", enabled: true }]);
|
|
795
|
+
pack = buildAdapterDepthPack(ad);
|
|
796
|
+
const pinned = pack.adapter_tasks.find((t) => t.task_ref === "adapter.mcp.invoke");
|
|
797
|
+
assert.ok(pinned.execution_targets.filter((t) => t.routing === "open_provider_registry").every((t) => t.shadowed_by_env_target && !t.enabled), "whole-lane env pin shadows registry providers");
|
|
798
|
+
assert.strictEqual(pinned.registry_provider_count, 0, "shadowed providers are not counted");
|
|
799
|
+
} finally {
|
|
800
|
+
if (prevTargets === undefined) delete process.env.XYTARA_EXECUTION_TARGETS_JSON; else process.env.XYTARA_EXECUTION_TARGETS_JSON = prevTargets;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
console.log(" hardening: epoch-isolation | in-flight guard | timeout cap | settlement basis | endpoint policy | budget | adapter-depth registry");
|
|
773
804
|
}
|
|
774
805
|
|
|
775
806
|
async function main() {
|
|
@@ -139,6 +139,12 @@ async function main() {
|
|
|
139
139
|
assert.strictEqual(settleFails.json.reason, "payment_settlement_failed", "settlement failure reason");
|
|
140
140
|
|
|
141
141
|
// 6. Verified + settled -> 200 with a result and a PAYMENT-RESPONSE receipt.
|
|
142
|
+
// (Delivery-receipt signing keys for this run only: Ed25519 + ML-DSA-65 hybrid.)
|
|
143
|
+
const crypto = require("crypto");
|
|
144
|
+
const receiptEd = crypto.generateKeyPairSync("ed25519");
|
|
145
|
+
const receiptMl = crypto.generateKeyPairSync("ml-dsa-65");
|
|
146
|
+
process.env.XYTARA_DELIVERY_RECEIPT_ED25519_KEY = receiptEd.privateKey.export({ type: "pkcs8", format: "der" }).toString("base64");
|
|
147
|
+
process.env.XYTARA_DELIVERY_RECEIPT_MLDSA65_KEY = receiptMl.privateKey.export({ type: "pkcs8", format: "der" }).toString("base64");
|
|
142
148
|
const settleBefore = facilitator.calls.settle;
|
|
143
149
|
const paid = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("ok") });
|
|
144
150
|
assert.strictEqual(paid.status, 200, "verified+settled payment must 200");
|
|
@@ -152,6 +158,70 @@ async function main() {
|
|
|
152
158
|
assert.strictEqual(receipt.success, true, "settlement receipt must report success");
|
|
153
159
|
assert.strictEqual(receipt.transaction, "0xmocktx", "settlement receipt transaction mismatch");
|
|
154
160
|
|
|
161
|
+
// 6a. The runtime records the REAL x402 settlement (not the quote's default placeholder rail).
|
|
162
|
+
const recorded = paid.json.receipt.settlement;
|
|
163
|
+
assert.strictEqual(recorded.rail, "x402", "x402-paid call records the x402 rail");
|
|
164
|
+
assert.strictEqual(recorded.mode, "x402_facilitator", "x402 settlement mode recorded");
|
|
165
|
+
assert.strictEqual(recorded.txid, "0xmocktx", "settlement record carries the settled tx");
|
|
166
|
+
assert.strictEqual(recorded.network, "eip155:84532", "settlement record carries the network");
|
|
167
|
+
assert.strictEqual(recorded.pay_to, ACCEPTS[0].payTo, "settlement record carries pay_to");
|
|
168
|
+
assert.strictEqual(recorded.status, "settled", "settlement record status");
|
|
169
|
+
assert.strictEqual(recorded.live, undefined, "no placeholder BSV settlement record for an x402 call");
|
|
170
|
+
assert.strictEqual(paid.json.receipt.treasury.rail, "x402", "treasury record on the x402 rail");
|
|
171
|
+
assert.strictEqual(paid.json.receipt.default_path.settlement_mode, "x402_facilitator", "default path reflects x402");
|
|
172
|
+
|
|
173
|
+
// 6b. Signed delivery receipt: binds the settlement, verifies against the published keys,
|
|
174
|
+
// is retrievable by id, and rejects tampering and signature stripping.
|
|
175
|
+
const delivery = paid.json.delivery_receipt;
|
|
176
|
+
assert.ok(delivery, "paid result carries a delivery receipt");
|
|
177
|
+
assert.strictEqual(delivery.v, "naxytra-delivery-receipt/1", "delivery receipt version");
|
|
178
|
+
assert.deepStrictEqual(delivery.signing.suite, ["EdDSA", "ML-DSA-65"], "hybrid suite");
|
|
179
|
+
assert.strictEqual(delivery.payment.protocol, "x402", "receipt payment protocol");
|
|
180
|
+
assert.strictEqual(delivery.payment.settlement_tx, "0xmocktx", "receipt binds the settlement tx");
|
|
181
|
+
assert.strictEqual(delivery.payment.pay_to, ACCEPTS[0].payTo, "receipt carries pay_to");
|
|
182
|
+
assert.strictEqual(delivery.payment.amount, ACCEPTS[0].amount, "receipt carries the settled amount");
|
|
183
|
+
assert.strictEqual(delivery.call.quote_id, paid.json.transaction.quote_id, "receipt binds the quote");
|
|
184
|
+
assert.strictEqual(delivery.account_id, undefined, "receipt carries no account id");
|
|
185
|
+
const receiptLib = require("../lib/delivery_receipt");
|
|
186
|
+
assert.strictEqual(delivery.delivery.request_sha256, receiptLib.hashCanonical(execBody.body), "request hash recomputes from the request body");
|
|
187
|
+
const keysRes = await fetch(`${baseUrl}/.well-known/delivery-receipt-keys`);
|
|
188
|
+
const keys = await keysRes.json();
|
|
189
|
+
assert.strictEqual(keys.signing_state, "signing", "keys endpoint reports signing");
|
|
190
|
+
assert.strictEqual(receiptLib.verifyDeliveryReceipt(delivery, keys.keys).valid, true, "receipt verifies against the published keys");
|
|
191
|
+
const verifyRes = await post(baseUrl, "/v1/delivery-receipts/verify", { receipt: delivery });
|
|
192
|
+
assert.strictEqual(verifyRes.json.valid, true, "verify endpoint accepts the genuine receipt");
|
|
193
|
+
assert.strictEqual(verifyRes.json.issuer_matches, true, "verify endpoint matches the issuer");
|
|
194
|
+
const tamperedDelivery = JSON.parse(JSON.stringify(delivery));
|
|
195
|
+
tamperedDelivery.payment.amount = "1";
|
|
196
|
+
assert.strictEqual((await post(baseUrl, "/v1/delivery-receipts/verify", { receipt: tamperedDelivery })).json.valid, false, "tampered receipt rejected");
|
|
197
|
+
const strippedDelivery = JSON.parse(JSON.stringify(delivery));
|
|
198
|
+
strippedDelivery.sig.signatures = strippedDelivery.sig.signatures.filter((entry) => entry.alg !== "ML-DSA-65");
|
|
199
|
+
assert.strictEqual(receiptLib.verifyDeliveryReceipt(strippedDelivery, keys.keys).valid, false, "stripping the ML-DSA leg (downgrade) rejected");
|
|
200
|
+
const fetched = await fetch(`${baseUrl}/v1/delivery-receipts/${encodeURIComponent(delivery.receipt_id)}`);
|
|
201
|
+
assert.strictEqual(fetched.status, 200, "receipt retrievable by id");
|
|
202
|
+
assert.deepStrictEqual((await fetched.json()).delivery_receipt, delivery, "retrieved receipt is identical");
|
|
203
|
+
assert.strictEqual((await fetch(`${baseUrl}/v1/delivery-receipts/drcpt_nope`)).status, 404, "unknown receipt 404");
|
|
204
|
+
// The xoonya verifier accepts the same signatures (kid == xoonya key ref). Checked when the
|
|
205
|
+
// sibling xoonya checkout is present (workspace); skipped in a standalone xytara checkout.
|
|
206
|
+
let verifyExternalProofSignature = null;
|
|
207
|
+
try { ({ verifyExternalProofSignature } = require("../../xoonya/proof_sdk")); } catch (_) { verifyExternalProofSignature = null; }
|
|
208
|
+
const { sig: _omit, ...unsignedDelivery } = delivery;
|
|
209
|
+
for (const entry of verifyExternalProofSignature ? delivery.sig.signatures : []) {
|
|
210
|
+
const key = keys.keys.find((item) => item.kid === entry.kid);
|
|
211
|
+
const viaXoonya = verifyExternalProofSignature({ material: receiptLib.canonicalize(unsignedDelivery), signature: entry.value, algorithm: entry.alg, public_key: key.public_key_spki_der_base64, expected_key_ref: entry.kid });
|
|
212
|
+
assert.strictEqual(viaXoonya.signature_valid && viaXoonya.trust_anchored, true, `xoonya verifies + anchors the ${entry.alg} leg`);
|
|
213
|
+
}
|
|
214
|
+
delete process.env.XYTARA_DELIVERY_RECEIPT_ED25519_KEY;
|
|
215
|
+
delete process.env.XYTARA_DELIVERY_RECEIPT_MLDSA65_KEY;
|
|
216
|
+
// Without keys the receipt is still issued but honestly unsigned (never a fake signature).
|
|
217
|
+
const unsignedPaid = await post(baseUrl, "/x402/commands/execute", execBody, { "payment-signature": paymentHeader("ok") });
|
|
218
|
+
assert.strictEqual(unsignedPaid.json.delivery_receipt.sig.state, "unsigned_signing_key_not_configured", "no keys -> honestly unsigned");
|
|
219
|
+
assert.strictEqual(receiptLib.verifyDeliveryReceipt(unsignedPaid.json.delivery_receipt, keys.keys).valid, false, "unsigned receipt never verifies");
|
|
220
|
+
// The shared module is byte-identical in the runtime and the MCP executor.
|
|
221
|
+
const fs = require("fs");
|
|
222
|
+
const path = require("path");
|
|
223
|
+
assert.ok(fs.readFileSync(path.join(__dirname, "..", "lib", "delivery_receipt.js")).equals(fs.readFileSync(path.join(__dirname, "..", "services", "mcp-execution-target", "delivery_receipt.js"))), "delivery_receipt.js copies must stay byte-identical");
|
|
224
|
+
|
|
155
225
|
// The facilitator must receive a clean v2 PaymentRequirements: no `resource` field (which
|
|
156
226
|
// strict facilitators like CDP reject), and it must equal the payment payload's `accepted`.
|
|
157
227
|
const verifyBody = facilitator.calls.lastVerifyBody;
|
package/server.js
CHANGED
|
@@ -198,6 +198,14 @@ const {
|
|
|
198
198
|
buildProviderEarningsView
|
|
199
199
|
} = require("./lib/provider_registry");
|
|
200
200
|
const { loadPayoutSignerConfig, dispatchProviderPayout } = require("./lib/provider_payout_execution");
|
|
201
|
+
const {
|
|
202
|
+
RECEIPT_VERSION: DELIVERY_RECEIPT_VERSION,
|
|
203
|
+
loadDeliveryReceiptSigners,
|
|
204
|
+
publicKeySet: deliveryReceiptPublicKeySet,
|
|
205
|
+
runtimeSupportsMlDsa,
|
|
206
|
+
verifyDeliveryReceipt,
|
|
207
|
+
verificationRecipe: deliveryReceiptVerificationRecipe
|
|
208
|
+
} = require("./lib/delivery_receipt");
|
|
201
209
|
const { buildProviderNetworkContract } = require("./lib/provider_network_contract");
|
|
202
210
|
const {
|
|
203
211
|
createEconomicsBudget,
|
|
@@ -4945,12 +4953,12 @@ async function routeRequest(req, res) {
|
|
|
4945
4953
|
}
|
|
4946
4954
|
|
|
4947
4955
|
if (req.method === "GET" && url.pathname === "/v1/adapter-depth") {
|
|
4948
|
-
sendJson(res, 200, buildAdapterDepthPack());
|
|
4956
|
+
sendJson(res, 200, buildAdapterDepthPack(state));
|
|
4949
4957
|
return;
|
|
4950
4958
|
}
|
|
4951
4959
|
|
|
4952
4960
|
if (req.method === "GET" && url.pathname === "/v1/adapter-depth/summary") {
|
|
4953
|
-
sendJson(res, 200, summarizeAdapterDepthPack());
|
|
4961
|
+
sendJson(res, 200, summarizeAdapterDepthPack(state));
|
|
4954
4962
|
return;
|
|
4955
4963
|
}
|
|
4956
4964
|
|
|
@@ -7808,6 +7816,51 @@ async function routeRequest(req, res) {
|
|
|
7808
7816
|
return;
|
|
7809
7817
|
}
|
|
7810
7818
|
|
|
7819
|
+
// Signed delivery receipts: the issuer's PUBLIC key set (pin these), fetch a receipt by id, and a
|
|
7820
|
+
// free stateless verify. Receipts carry only public payment facts + hashes (no account, no secrets).
|
|
7821
|
+
if (req.method === "GET" && (url.pathname === "/.well-known/delivery-receipt-keys" || url.pathname === "/v1/delivery-receipts/keys")) {
|
|
7822
|
+
const { signers, problems } = loadDeliveryReceiptSigners("XYTARA_DELIVERY_RECEIPT");
|
|
7823
|
+
sendJson(res, 200, {
|
|
7824
|
+
ok: true,
|
|
7825
|
+
issuer: "xytara-runtime",
|
|
7826
|
+
receipt_version: DELIVERY_RECEIPT_VERSION,
|
|
7827
|
+
signing_state: signers.length > 0 ? "signing" : "unsigned_signing_key_not_configured",
|
|
7828
|
+
suite: signers.map((signer) => signer.alg),
|
|
7829
|
+
keys: deliveryReceiptPublicKeySet(signers),
|
|
7830
|
+
runtime_supports_ml_dsa: runtimeSupportsMlDsa(),
|
|
7831
|
+
key_problems: problems.map((problem) => ({ variable: problem.variable, reason: problem.reason })),
|
|
7832
|
+
verification: deliveryReceiptVerificationRecipe("/.well-known/delivery-receipt-keys")
|
|
7833
|
+
});
|
|
7834
|
+
return;
|
|
7835
|
+
}
|
|
7836
|
+
|
|
7837
|
+
if (req.method === "POST" && url.pathname === "/v1/delivery-receipts/verify") {
|
|
7838
|
+
const body = await readJsonBody(req);
|
|
7839
|
+
const receipt = body && typeof body === "object" && body.receipt && typeof body.receipt === "object" ? body.receipt : body;
|
|
7840
|
+
const { signers } = loadDeliveryReceiptSigners("XYTARA_DELIVERY_RECEIPT");
|
|
7841
|
+
const result = verifyDeliveryReceipt(receipt, deliveryReceiptPublicKeySet(signers));
|
|
7842
|
+
sendJson(res, 200, {
|
|
7843
|
+
ok: true,
|
|
7844
|
+
...result,
|
|
7845
|
+
issuer_expected: "xytara-runtime",
|
|
7846
|
+
issuer_matches: Boolean(receipt && receipt.issuer === "xytara-runtime"),
|
|
7847
|
+
checked_against: "this runtime's current published keys (see /.well-known/delivery-receipt-keys)",
|
|
7848
|
+
boundary: "signature validity only: settle the payment leg on-chain and recompute output_sha256 from the output you hold yourself"
|
|
7849
|
+
});
|
|
7850
|
+
return;
|
|
7851
|
+
}
|
|
7852
|
+
|
|
7853
|
+
if (req.method === "GET" && url.pathname.startsWith("/v1/delivery-receipts/")) {
|
|
7854
|
+
const receiptId = getDecodedSuffixId(url.pathname, "/v1/delivery-receipts/");
|
|
7855
|
+
const stored = state.deliveryReceipts instanceof Map ? state.deliveryReceipts.get(receiptId) : null;
|
|
7856
|
+
if (!stored) {
|
|
7857
|
+
sendJson(res, 404, { ok: false, reason: "delivery_receipt_not_found" });
|
|
7858
|
+
return;
|
|
7859
|
+
}
|
|
7860
|
+
sendJson(res, 200, { ok: true, delivery_receipt: stored });
|
|
7861
|
+
return;
|
|
7862
|
+
}
|
|
7863
|
+
|
|
7811
7864
|
// Public, machine-readable integration contract — how to become a provider. No auth, no secrets.
|
|
7812
7865
|
if (req.method === "GET" && url.pathname === "/v1/provider-network/contract") {
|
|
7813
7866
|
sendJson(res, 200, buildProviderNetworkContract());
|