xytara 3.2.2 → 3.4.0

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/README.md CHANGED
@@ -22,6 +22,7 @@ This package ships:
22
22
 
23
23
  - `CommerceClient` — discover the catalog, quote, run the paid command path, read results
24
24
  - the delivery-receipt verifier — `verifyDeliveryReceipt`, `canonicalize`, `hashCanonical`, `keyRef`
25
+ - the attestation verifier — `verifyAttestation`, `attestationRecipe`, `ATTESTATION_VERSION`
25
26
  - x402 header helpers — `encodeX402PaymentHeader`, `decodeX402Header`
26
27
  - the `xytara` / `xytara-run` CLIs (quote-only first, then funded execution)
27
28
  - `endpoints` (runtime, mcp, website) and `version`
@@ -117,6 +118,36 @@ tools/call -> 402 Payment Required with a price
117
118
  Point any MCP client at the endpoint; the public integration guide is
118
119
  [github.com/naxytra/naxytra-mcp](https://github.com/naxytra/naxytra-mcp).
119
120
 
121
+ ## Teams: one owner key, many agent keys
122
+
123
+ The spend credential a top-up returns is the **owner key** for its account. It can mint capped,
124
+ revocable **agent keys** for each of your agents; every spend is audited. All routes take a spend
125
+ credential as `Authorization: Bearer xytara_spend_...`.
126
+
127
+ ```
128
+ POST /v1/credits/agents owner key only -> 201 { agent, bearer_token } (token shown once)
129
+ { label, max_units_per_call?, max_units_per_day?, max_units_total?, expires_at_iso?,
130
+ allowed_tools?: ["keccak256", ...], agent_public_key?: <ed25519 SPKI PEM> }
131
+ GET /v1/credits/agents owner: every agent key (never tokens) with bounds, status,
132
+ spent_today_units / spent_total_units / last_used_at_iso;
133
+ an agent key sees only itself
134
+ POST /v1/credits/agents/:id/revoke owner key only; immediate; { reason } is stored
135
+ GET /v1/credits/activity?limit=100&since=<iso>
136
+ recent spends with agent id/label, units, task_ref, tool_name,
137
+ credit_spend_id; owner sees the account, an agent sees its own
138
+ ```
139
+
140
+ An agent key spends exactly like the owner key (`/v1/economics/rails/credits/consume`, or
141
+ `credits_first` execution), but every spend is checked against its per-call, per-UTC-day and
142
+ lifetime caps, its tool allow-list, its expiry and revocation. Refusals are explicit and never
143
+ debit: `agent_call_cap_exceeded`, `agent_daily_cap_exceeded`, `agent_total_cap_exceeded`,
144
+ `agent_tool_not_allowed`, `spend_credential_expired`, `spend_credential_inactive`. Owner keys have
145
+ no caps. Agent keys cannot mint (`agent_key_cannot_mint`) or see other agents. A key minted with
146
+ `agent_public_key` is key-bound: each spend must also carry `x-agent-signature` /
147
+ `x-agent-timestamp` over `naxytra-spend-auth:<agent_credential_id>:<ts>:<sha256(JSON
148
+ ["naxytra-spend-binding/v1", account_id, String(units), task_ref])>`, so a captured signature cannot
149
+ be replayed or stretched to a larger spend.
150
+
120
151
  ## Signed delivery receipts
121
152
 
122
153
  Every paid call returns a `naxytra-delivery-receipt/1`: the payment leg (network, asset, amount,
@@ -144,6 +175,55 @@ POST /v1/delivery-receipts/verify # free signature check
144
175
  A receipt proves who delivered what for which paid call, unaltered since signing — **not** whether
145
176
  the output was correct or good. Check the settlement transaction on-chain yourself.
146
177
 
178
+ ## Attestations (verified answers)
179
+
180
+ The MCP server's `x402.verify-receipt` and `tx.status` tools cost **$0.02 per call** (20000 USDC
181
+ base units on Base mainnet, 2000 on Base Sepolia; 6 credits with a spend key — the free allowance
182
+ covers them too) and return, next to the tool output, a signed `naxytra-attestation/1` in
183
+ `result._meta["naxytra.com/attestation"]`: a statement of what the server observed on-chain from a
184
+ live RPC read, that you can hand to another party.
185
+
186
+ ```json
187
+ { "version": "naxytra-attestation/1", "attestation_id": "att_…", "issued_at_iso": "…", "issuer": "xytara-mcp-executor",
188
+ "subject": { "kind": "x402_settlement", "network": "eip155:8453", "tx": "0x…", "payer": "0x…", "pay_to": "0x…", "asset": "0x…", "amount": "10000" },
189
+ "claim": { "verified": true, "method": "base-rpc-logs/1", "observed_at_iso": "…", "block_number": 4080, "status": "success", "details": { "verdict": "confirmed", "checks": { … }, "transfer": { … } } },
190
+ "evidence_sha256": "<sha256 of JCS { tool, arguments, output }>", "bindings": [],
191
+ "sig": { "suite": ["EdDSA", "ML-DSA-65"], "kids": { "EdDSA": "ed25519-spki-sha256:…", "ML-DSA-65": "ml-dsa-65-spki-sha256:…" }, "signatures": { "EdDSA": "…", "ML-DSA-65": "…" } } }
192
+ ```
193
+
194
+ `subject.kind` is `x402_settlement` (method `base-rpc-logs/1`) or `transaction_status` (method
195
+ `base-rpc-receipt/1`, with `confirmations`). A negative answer (`claim.verified: false` — the
196
+ receipt does not match the chain, the transaction is unknown) is attested too; a tool error (RPC
197
+ unavailable) carries no attestation and is not charged. `bindings` holds external bindings and
198
+ must be carried unchanged — it is inside the signed bytes. The one known scheme is a
199
+ chronology-protocol binding, `{ "scheme": "ctp/1", "system_id": "NAXYTRA-ATTESTATION/1", "tag":
200
+ "<64 hex>" }`: the tag derives from a proof-of-work block that did not exist before the issuer's
201
+ anchoring session opened, so an attestation signed with it inside cannot predate that block; the
202
+ anchor then commits to `attestation_sha256` = SHA-256 of the JCS form of the whole attestation
203
+ (`attestationSha256` in the client; also returned by `GET /attestations/<id>` and
204
+ `POST /attestations/verify`). Signed bytes = JCS of the attestation with `sig.signatures` removed
205
+ (`sig.suite` and `sig.kids` stay signed), with the same keys as the delivery receipts.
206
+
207
+ ```js
208
+ const { verifyAttestation, hashCanonical, attestationRecipe } = require("xytara");
209
+
210
+ const keys = (await fetch("https://xytara-mcp-executor.onrender.com/.well-known/delivery-receipt-keys").then((r) => r.json())).keys;
211
+ const check = verifyAttestation(attestation, keys); // { valid, claim_verified, signatures: [{ alg, kid, valid }], ... }
212
+ const evidenceMatches = hashCanonical({ tool, arguments, output }) === attestation.evidence_sha256;
213
+ console.log(attestationRecipe("/.well-known/delivery-receipt-keys").steps);
214
+ ```
215
+
216
+ ```
217
+ GET /.well-known/delivery-receipt-keys # keys + attestation_recipe (same keys as receipts)
218
+ GET /attestations/:attestation_id # one of the last 5000 issued
219
+ POST /attestations/verify # free signature check: { attestation } -> { valid, kids_known, ... }
220
+ ```
221
+
222
+ Key-bound spend credentials: send your `X-Agent-Signature` / `X-Agent-Timestamp` headers with the
223
+ MCP call; the executor forwards them to the credits debit unchanged. A request-bound signature
224
+ commits to `{ account_id, units, task_ref: "adapter.mcp.invoke" }` with `units` = the tool's
225
+ `credits_per_call` (3, or 6 for an attesting tool).
226
+
147
227
  ## Verify the package
148
228
 
149
229
  ```bash
package/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,9 @@
1
- `xytara` 3.2.2 adds a per-request opt-out from the MCP server's free allowance and repairs the registry publish.
1
+ `xytara` 3.4.0 makes the open provider network pay correctly at the current price, anchors attestations in time, and gives buyers a console.
2
2
 
3
- - **`X-Naxytra-Allowance: off`** (or `?allowance=off` on the MCP URL) skips the free daily allowance for that request, so x402-native clients, payment-flow tests and release probes get deterministic pay-per-call behaviour (the payment-required result). Opting out never spends the allowance.
4
- - The MCP registry publish workflow now waits for the live deploy without tripping on transient errors and probes the payment challenge with the allowance switched off.
5
- - Client package (thin): unchanged surface.
3
+ - **Provider revenue at the $0.01 price** — a provider's exact share (`settled × bps / 10000`) is now credited in whole cents with the sub-cent remainder carried into its next settlement (`rollup.share_carry_bp`; ledger rows record `share_carry_before_bp` / `share_carry_after_bp`), so a 70% provider earns 7 cents over ten $0.01 calls instead of flooring to zero on every call. Nothing changes for whole-cent amounts.
4
+ - **Payout sweep** — `POST /v1/provider-network/payouts/sweep` (operator) pays every active provider whose available balance reaches `XYTARA_PROVIDER_PAYOUT_MIN_MINOR` (default 100 cents) through the signer, one idempotent dispatch each; a weekly job runs it (Mondays, `state-backup.yml`). Activation through the operator route now requires a `payout_address` on the record (`payout_address_required_for_activation`).
5
+ - **Delivery rule** — a provider's 2xx without `result.output`, `output_payload` or `output` is `external_execution_empty_output`: not charged, nothing accrues, not retried elsewhere. Routing prefers the most specific capability match (tool name over task ref over integration), then reputation. Providers rotate their `owner_secret` at `POST /v1/provider-network/providers/:id/rotate-secret`. The public contract (`/v1/provider-network/contract`) states all of this.
6
+ - **Operator visibility** — the daily report lists pending provider registrations (and flags a missing payout address) and the network's owed/accrued/paid totals, and fails (emails) while a registration waits.
7
+ - **Attestation external bindings** — `bindings[]` inside the signed bytes may carry `{ scheme: "ctp/1", system_id, tag }`, a chronology-protocol binding tag set with `MCP_EXECUTOR_EXTERNAL_BINDING_JSON` on the MCP executor while an anchoring session is open; the active binding is shown on `/.well-known/delivery-receipt-keys` and `/health`. `GET /attestations/<id>` and `POST /attestations/verify` return `attestation_sha256` (SHA-256 of the JCS form, signatures included), the digest an external anchor commits to.
8
+ - **Buyer console** — `https://www.naxytra.com/console`: paste a spend key to read the balance, the credit activity and the agent keys straight from the runtime, and mint or revoke agent keys; the key stays in the browser.
9
+ - Client package (thin): adds `attestationSha256`, `normalizeExternalBinding`, `EXTERNAL_BINDING_SCHEMES`; tarball otherwise unchanged.
package/client.js CHANGED
@@ -7,11 +7,18 @@
7
7
  const commerceClient = require("./lib/commerce_client");
8
8
  const {
9
9
  RECEIPT_VERSION,
10
+ ATTESTATION_VERSION,
10
11
  canonicalize,
11
12
  hashCanonical,
12
13
  keyRef,
13
14
  verifyDeliveryReceipt,
14
- verificationRecipe
15
+ verificationRecipe,
16
+ attestationSignedBytes,
17
+ verifyAttestation,
18
+ attestationSha256,
19
+ normalizeExternalBinding,
20
+ EXTERNAL_BINDING_SCHEMES,
21
+ attestationRecipe
15
22
  } = require("./lib/delivery_receipt_verify");
16
23
  const { version } = require("./package.json");
17
24
 
@@ -70,6 +77,17 @@ module.exports = {
70
77
  keyRef,
71
78
  verifyDeliveryReceipt,
72
79
  verificationRecipe,
80
+ // Signed attestations (the MCP executor's verified answers): offline verification with the
81
+ // same published keys.
82
+ ATTESTATION_VERSION,
83
+ attestationSignedBytes,
84
+ verifyAttestation,
85
+ attestationRecipe,
86
+ // External bindings (chronology anchoring): the digest an anchor commits to, the binding shape
87
+ // check, and the known schemes.
88
+ attestationSha256,
89
+ normalizeExternalBinding,
90
+ EXTERNAL_BINDING_SCHEMES,
73
91
  // x402 header helpers for the paid command path.
74
92
  X402_PAYMENT_HEADER,
75
93
  X402_PAYMENT_REQUIRED_HEADER,
@@ -12,6 +12,12 @@
12
12
  // Key ids = "<key type>-spki-sha256:<hex>", the same key-ref format the xoonya verifier
13
13
  // reports, so a receipt verifies with xoonya's verifyExternalProofSignature too.
14
14
  //
15
+ // Signed attestations ("naxytra-attestation/1") share the same keys, kid format and verifier
16
+ // pattern: a short, signed statement of an on-chain fact (an x402 settlement, a transaction's
17
+ // status) that the MCP executor observed from a live RPC read, which the buyer can hand to a third
18
+ // party who verifies it offline. The verification side (verifyAttestation, attestationRecipe) is
19
+ // here too; issuing lives in delivery_receipt.js.
20
+ //
15
21
  // Dependency-free (node:crypto). This file is shared verbatim by the xytara runtime
16
22
  // (lib/delivery_receipt_verify.js) and the MCP executor
17
23
  // (services/mcp-execution-target/delivery_receipt_verify.js); a verifier asserts the two copies
@@ -23,6 +29,21 @@ const RECEIPT_VERSION = "naxytra-delivery-receipt/1";
23
29
  const CANONICALIZATION = "JCS (RFC 8785) of the receipt without `sig`";
24
30
  const SUPPORTED_ALGS = { ed25519: "EdDSA", "ml-dsa-65": "ML-DSA-65" };
25
31
 
32
+ const ATTESTATION_VERSION = "naxytra-attestation/1";
33
+ // Attestation signed bytes: the attestation with `sig.signatures` removed. `sig.suite` and
34
+ // `sig.kids` stay INSIDE the signed bytes, so a stripped algorithm or a swapped key id fails.
35
+ const ATTESTATION_CANONICALIZATION = "JCS (RFC 8785) of the attestation with `sig.signatures` removed (`sig.suite` and `sig.kids` stay signed)";
36
+ const ATTESTATION_ID_PATTERN = /^att_[0-9a-f]{12}$/;
37
+ // External bindings an attestation may carry INSIDE its signed bytes (attestation.bindings[]).
38
+ // "ctp/1": a chronology-protocol binding tag — a value derived from a proof-of-work block that did
39
+ // not exist when the attestation's session opened, so an attestation signed with it inside cannot
40
+ // predate that block (the lower time bound); the anchor then commits to attestation_sha256 (the
41
+ // upper bound). The tag is the derived binding tag, never the raw challenge, so records bound in
42
+ // different systems cannot be correlated. Shape: { scheme: "ctp/1", system_id, tag: <64 hex> }.
43
+ const EXTERNAL_BINDING_SCHEMES = ["ctp/1"];
44
+ const EXTERNAL_BINDING_TAG_PATTERN = /^[0-9a-f]{64}$/;
45
+ const EXTERNAL_BINDING_SYSTEM_ID_PATTERN = /^[\x21-\x7e]{1,64}$/;
46
+
26
47
  // JCS for the JSON values a receipt carries (strings, finite numbers, booleans, null, arrays,
27
48
  // objects). Object keys sorted by UTF-16 code units; `undefined` members dropped like JSON.
28
49
  function canonicalize(value) {
@@ -124,10 +145,109 @@ function verificationRecipe(keysUrl) {
124
145
  };
125
146
  }
126
147
 
148
+ // --- attestations ------------------------------------------------------------------------------
149
+
150
+ function isPlainObject(value) {
151
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
152
+ }
153
+
154
+ // The bytes every attestation signature covers (see ATTESTATION_CANONICALIZATION).
155
+ function attestationSignedBytes(attestation) {
156
+ const { sig, ...rest } = attestation;
157
+ const { signatures: _omit, ...signedSig } = isPlainObject(sig) ? sig : {};
158
+ return Buffer.from(canonicalize({ ...rest, sig: signedSig }), "utf8");
159
+ }
160
+
161
+ // Verifies an attestation against a set of trusted public keys ([{ kid, alg, public_key_spki_der_base64 }]).
162
+ // Valid only if the version is known, `bindings` is an array (reserved; carried unchanged), every
163
+ // algorithm in the SIGNED suite has a kid in sig.kids and a signature in sig.signatures, every such
164
+ // kid is in the trusted set, and every signature verifies over the signed bytes.
165
+ function verifyAttestation(attestation, trustedKeys) {
166
+ const a = isPlainObject(attestation) ? attestation : null;
167
+ if (!a) return { valid: false, reason: "attestation_required" };
168
+ if (a.version !== ATTESTATION_VERSION) return { valid: false, reason: "unsupported_attestation_version", version: a.version || null };
169
+ if (!Array.isArray(a.bindings)) return { valid: false, reason: "bindings_must_be_an_array" };
170
+ const sig = isPlainObject(a.sig) ? a.sig : null;
171
+ if (!sig || !Array.isArray(sig.suite) || sig.suite.length === 0 || !isPlainObject(sig.kids) || !isPlainObject(sig.signatures)) {
172
+ return { valid: false, reason: "attestation_unsigned" };
173
+ }
174
+ const signedBytes = attestationSignedBytes(a);
175
+ const trusted = new Map((Array.isArray(trustedKeys) ? trustedKeys : []).map((key) => [key.kid, key]));
176
+ const results = sig.suite.map((alg) => {
177
+ const kid = typeof sig.kids[alg] === "string" ? sig.kids[alg] : null;
178
+ const value = typeof sig.signatures[alg] === "string" ? sig.signatures[alg] : null;
179
+ if (!kid) return { alg, kid: null, valid: false, reason: "kid_missing_for_signed_suite" };
180
+ if (!value) return { alg, kid, valid: false, reason: "signature_missing_for_signed_suite" };
181
+ const key = trusted.get(kid);
182
+ if (!key || key.alg !== alg) return { alg, kid, valid: false, reason: "kid_not_in_trusted_key_set" };
183
+ try {
184
+ const publicKey = crypto.createPublicKey({ key: Buffer.from(key.public_key_spki_der_base64, "base64"), format: "der", type: "spki" });
185
+ if (keyRef(publicKey) !== kid) return { alg, kid, valid: false, reason: "kid_does_not_match_key" };
186
+ const ok = crypto.verify(null, signedBytes, publicKey, Buffer.from(value, "base64"));
187
+ return { alg, kid, valid: ok, reason: ok ? null : "signature_invalid" };
188
+ } catch (error) {
189
+ return { alg, kid, valid: false, reason: "verification_error", detail: error && error.message ? error.message.slice(0, 120) : null };
190
+ }
191
+ });
192
+ const valid = results.length > 0 && results.every((item) => item.valid);
193
+ return {
194
+ valid,
195
+ reason: valid ? null : "signature_check_failed",
196
+ attestation_id: typeof a.attestation_id === "string" ? a.attestation_id : null,
197
+ issuer: typeof a.issuer === "string" ? a.issuer : null,
198
+ subject_kind: isPlainObject(a.subject) && typeof a.subject.kind === "string" ? a.subject.kind : null,
199
+ claim_verified: isPlainObject(a.claim) && typeof a.claim.verified === "boolean" ? a.claim.verified : null,
200
+ signatures: results,
201
+ signed_bytes_sha256: sha256Hex(signedBytes)
202
+ };
203
+ }
204
+
205
+ // The digest an external anchor (e.g. a chronology-protocol checkpoint) commits to: sha256 of the
206
+ // JCS form of the WHOLE attestation, signatures included. Stable across re-serialisation because
207
+ // it is computed over the canonical form, not the bytes a transport happened to send.
208
+ function attestationSha256(attestation) {
209
+ return hashCanonical(attestation);
210
+ }
211
+
212
+ // Normalizes one external binding ({ scheme, system_id, tag }) for issuing or inspection.
213
+ // Returns { binding } or { reason }; unknown schemes and malformed tags are refused so that an
214
+ // attestation never carries a binding a verifier could not interpret.
215
+ function normalizeExternalBinding(candidate) {
216
+ const c = isPlainObject(candidate) ? candidate : null;
217
+ if (!c) return { reason: "binding_must_be_an_object" };
218
+ if (!EXTERNAL_BINDING_SCHEMES.includes(c.scheme)) return { reason: "unsupported_binding_scheme", scheme: c.scheme === undefined ? null : c.scheme };
219
+ const systemId = typeof c.system_id === "string" ? c.system_id : "";
220
+ if (!EXTERNAL_BINDING_SYSTEM_ID_PATTERN.test(systemId)) return { reason: "binding_system_id_invalid" };
221
+ const tag = typeof c.tag === "string" ? c.tag.toLowerCase() : "";
222
+ if (!EXTERNAL_BINDING_TAG_PATTERN.test(tag)) return { reason: "binding_tag_must_be_64_hex" };
223
+ return { binding: { scheme: c.scheme, system_id: systemId, tag } };
224
+ }
225
+
226
+ // A short, self-contained recipe for verifying an attestation without trusting the issuer's server.
227
+ function attestationRecipe(keysUrl) {
228
+ return {
229
+ version: ATTESTATION_VERSION,
230
+ signed_bytes: `${ATTESTATION_CANONICALIZATION} (UTF-8).`,
231
+ keys: keysUrl || null,
232
+ steps: [
233
+ "1. Fetch the issuer's public keys; check every kid in attestation.sig.kids is listed (and pin them).",
234
+ "2. Remove attestation.sig.signatures (keep sig.suite and sig.kids) and JCS-canonicalize what is left: these are the signed bytes.",
235
+ "3. For every alg in sig.suite, verify sig.signatures[alg] over the signed bytes with the key whose kid is sig.kids[alg] (node:crypto crypto.verify(null, bytes, spkiPublicKey, signature), or xoonya's verifyExternalProofSignature({ material, signature, algorithm, public_key, expected_key_ref: kid })).",
236
+ "4. Read subject (what was checked), claim.verified / claim.status / claim.details (what was observed) and claim.observed_at_iso (when). evidence_sha256 = sha256(JCS({ tool, arguments, output })) of the tool call that produced it; recompute it when you hold that call. bindings[] (inside the signed bytes; carry unchanged) may hold external bindings: { scheme: \"ctp/1\", system_id, tag } is a chronology-protocol binding tag, proving the attestation was created after the proof-of-work block that minted the tag; the external anchor commits to attestation_sha256 = sha256(JCS(attestation)) for the upper bound."
237
+ ],
238
+ external_bindings: { schemes: EXTERNAL_BINDING_SCHEMES.slice(), digest: "attestation_sha256 = sha256(JCS(attestation)), signatures included" },
239
+ boundary: "Proves this issuer observed this claim about this subject from a live chain read at observed_at_iso, unaltered since signing — not that the chain cannot reorganize, and nothing beyond the stated claim."
240
+ };
241
+ }
242
+
127
243
  module.exports = {
128
244
  RECEIPT_VERSION,
129
245
  CANONICALIZATION,
130
246
  SUPPORTED_ALGS,
247
+ ATTESTATION_VERSION,
248
+ ATTESTATION_CANONICALIZATION,
249
+ ATTESTATION_ID_PATTERN,
250
+ EXTERNAL_BINDING_SCHEMES,
131
251
  canonicalize,
132
252
  sha256Hex,
133
253
  hashCanonical,
@@ -135,5 +255,10 @@ module.exports = {
135
255
  runtimeSupportsMlDsa,
136
256
  publicKeySet,
137
257
  verifyDeliveryReceipt,
138
- verificationRecipe
258
+ verificationRecipe,
259
+ attestationSignedBytes,
260
+ verifyAttestation,
261
+ attestationSha256,
262
+ normalizeExternalBinding,
263
+ attestationRecipe
139
264
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xytara",
3
- "version": "3.2.2",
3
+ "version": "3.4.0",
4
4
  "description": "Client for the hosted naxytra runtime: discover a capability, pay per call with x402 (gasless USDC on Base) or credits, get the deliverable and a signed delivery receipt you can verify offline. The runtime is a hosted service; this package is the client.",
5
5
  "main": "client.js",
6
6
  "exports": {
@@ -95,7 +95,7 @@
95
95
  "verify:production-readiness": "node scripts/verify_production_readiness.js",
96
96
  "verify:final-activation": "node scripts/verify_final_activation.js",
97
97
  "verify:x402-facilitator": "node scripts/verify_x402_facilitator.js",
98
- "verify:all": "node scripts/verify_all.js && node scripts/verify_integrations.js && node scripts/verify_adapters.js && node scripts/verify_control_plane_contracts.js && node scripts/verify_control_plane_routes.js && node scripts/verify_framework_provider_promotion.js && node scripts/verify_operator_observability_boundary.js && node scripts/verify_pricing_experiment_plan.js && node scripts/verify_treasury_public_boundary.js && node scripts/verify_market_lanes.js && node scripts/verify_market_lane_depth.js && node scripts/verify_market_lane_traces.js && node scripts/verify_market_lane_promotion.js && node scripts/verify_provider_runtime_breadth.js && node scripts/verify_provider_runtime_journeys.js && node scripts/verify_lifecycle_continuity.js && node scripts/verify_first_run_conversions.js && node scripts/verify_delivery_summary.js && node scripts/verify_runtime_durability.js && node scripts/verify_revenue_launch_paths.js && node scripts/verify_full_catalog.js && node scripts/verify_tooling.js && node scripts/verify_examples.js && node scripts/verify_service.js && node scripts/verify_x402_facilitator.js && node scripts/verify_cdp_facilitator.js && node scripts/verify_a2a_rpc.js && node scripts/verify_checkout_processor.js && node scripts/verify_l402_adapter.js && node scripts/verify_bolt_adapter.js && node scripts/verify_mpp_adapter.js && node scripts/verify_rate_limiter.js && node scripts/verify_execution_surfacing.js && node scripts/verify_agent_identity.js && node scripts/verify_verify_execute_settle.js && node scripts/verify_execution_dedupe.js && node scripts/verify_refund_credit.js && node scripts/verify_commerce_bridge.js && node scripts/verify_settlement.js && node scripts/verify_provider_network.js && node scripts/verify_external_execution_retry.js && node scripts/verify_runtime_state_guard.js && node scripts/state_backup_offsite.js selftest && node scripts/verify_lifecycle_spend_binding.js && node scripts/verify_buyer_safety.js && node scripts/verify_buyer_event_log.js && node scripts/verify_x402_settlement_recovery.js",
98
+ "verify:all": "node scripts/verify_all.js && node scripts/verify_integrations.js && node scripts/verify_adapters.js && node scripts/verify_control_plane_contracts.js && node scripts/verify_control_plane_routes.js && node scripts/verify_framework_provider_promotion.js && node scripts/verify_operator_observability_boundary.js && node scripts/verify_pricing_experiment_plan.js && node scripts/verify_treasury_public_boundary.js && node scripts/verify_market_lanes.js && node scripts/verify_market_lane_depth.js && node scripts/verify_market_lane_traces.js && node scripts/verify_market_lane_promotion.js && node scripts/verify_provider_runtime_breadth.js && node scripts/verify_provider_runtime_journeys.js && node scripts/verify_lifecycle_continuity.js && node scripts/verify_first_run_conversions.js && node scripts/verify_delivery_summary.js && node scripts/verify_runtime_durability.js && node scripts/verify_revenue_launch_paths.js && node scripts/verify_full_catalog.js && node scripts/verify_tooling.js && node scripts/verify_examples.js && node scripts/verify_service.js && node scripts/verify_x402_facilitator.js && node scripts/verify_cdp_facilitator.js && node scripts/verify_a2a_rpc.js && node scripts/verify_checkout_processor.js && node scripts/verify_l402_adapter.js && node scripts/verify_bolt_adapter.js && node scripts/verify_mpp_adapter.js && node scripts/verify_rate_limiter.js && node scripts/verify_execution_surfacing.js && node scripts/verify_agent_identity.js && node scripts/verify_verify_execute_settle.js && node scripts/verify_execution_dedupe.js && node scripts/verify_refund_credit.js && node scripts/verify_commerce_bridge.js && node scripts/verify_settlement.js && node scripts/verify_provider_network.js && node scripts/verify_external_execution_retry.js && node scripts/verify_runtime_state_guard.js && node scripts/state_backup_offsite.js selftest && node scripts/verify_lifecycle_spend_binding.js && node scripts/verify_buyer_safety.js && node scripts/verify_buyer_event_log.js && node scripts/verify_x402_settlement_recovery.js && node scripts/verify_team_spend.js",
99
99
  "prepublishOnly": "npm run verify:release-candidate",
100
100
  "verify:cdp-facilitator": "node scripts/verify_cdp_facilitator.js",
101
101
  "verify:a2a-rpc": "node scripts/verify_a2a_rpc.js",
@@ -119,6 +119,7 @@
119
119
  "verify:buyer-safety": "node scripts/verify_buyer_safety.js",
120
120
  "verify:buyer-event-log": "node scripts/verify_buyer_event_log.js",
121
121
  "verify:x402-settlement-recovery": "node scripts/verify_x402_settlement_recovery.js",
122
+ "verify:team-spend": "node scripts/verify_team_spend.js",
122
123
  "verify:client": "node scripts/verify_client.js"
123
124
  }
124
125
  }
@@ -6,6 +6,8 @@
6
6
  // the hosted endpoints and the package version.
7
7
  // 2. Signed delivery receipts: a receipt built in the documented canonical form and signed with an
8
8
  // Ed25519 key generated here verifies; tampered, stripped, unsigned and wrong-key copies do not.
9
+ // Signed attestations (naxytra-attestation/1): the same round trip, including the reserved
10
+ // `bindings` member surviving canonicalization and verification unchanged.
9
11
  // 3. The paid command path against a local stub of the hosted runtime: discover -> 402 challenge
10
12
  // with x402 accepts -> pay with an x402 payment header -> 200 with the result, the settlement
11
13
  // response and a signed delivery receipt that verifies against the stub's published keys.
@@ -25,11 +27,15 @@ const client = require(path.join(PACKAGE_DIR, "client.js"));
25
27
  const {
26
28
  CommerceClient,
27
29
  RECEIPT_VERSION,
30
+ ATTESTATION_VERSION,
28
31
  canonicalize,
29
32
  hashCanonical,
30
33
  keyRef,
31
34
  verifyDeliveryReceipt,
32
35
  verificationRecipe,
36
+ attestationSignedBytes,
37
+ verifyAttestation,
38
+ attestationRecipe,
33
39
  encodeX402PaymentHeader,
34
40
  decodeX402Header,
35
41
  summarizeCommandResultPayload,
@@ -67,6 +73,22 @@ function signReceipt(body, signer) {
67
73
  };
68
74
  }
69
75
 
76
+ // Attestation issuing, as the MCP executor does it (kept in-test: the client never issues them).
77
+ // Signed bytes = JCS of the attestation with sig.signatures removed (sig.suite / sig.kids signed).
78
+ function signAttestation(body, signer) {
79
+ const unsigned = {
80
+ version: ATTESTATION_VERSION,
81
+ bindings: [],
82
+ ...body,
83
+ sig: { suite: [signer.alg], kids: { [signer.alg]: signer.kid } }
84
+ };
85
+ const signedBytes = Buffer.from(canonicalize(unsigned), "utf8");
86
+ return {
87
+ ...unsigned,
88
+ sig: { ...unsigned.sig, signatures: { [signer.alg]: crypto.sign(null, signedBytes, signer.privateKey).toString("base64") } }
89
+ };
90
+ }
91
+
70
92
  function makeSigner() {
71
93
  const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519");
72
94
  const kid = keyRef(publicKey);
@@ -230,10 +252,11 @@ function runCliAsync(args) {
230
252
  async function main() {
231
253
  check("package_surface", () => {
232
254
  assert.strictEqual(typeof CommerceClient, "function", "CommerceClient export missing");
233
- for (const name of ["verifyDeliveryReceipt", "canonicalize", "hashCanonical", "keyRef", "verificationRecipe", "encodeX402PaymentHeader", "decodeX402Header", "summarizeCommandResultPayload", "summarizeCatalogPayload", "summarizeReceiptPayload"]) {
255
+ for (const name of ["verifyDeliveryReceipt", "canonicalize", "hashCanonical", "keyRef", "verificationRecipe", "verifyAttestation", "attestationRecipe", "attestationSignedBytes", "attestationSha256", "normalizeExternalBinding", "encodeX402PaymentHeader", "decodeX402Header", "summarizeCommandResultPayload", "summarizeCatalogPayload", "summarizeReceiptPayload"]) {
234
256
  assert.strictEqual(typeof client[name], "function", `${name} export missing`);
235
257
  }
236
258
  assert.strictEqual(RECEIPT_VERSION, "naxytra-delivery-receipt/1", "receipt version mismatch");
259
+ assert.strictEqual(ATTESTATION_VERSION, "naxytra-attestation/1", "attestation version mismatch");
237
260
  assert.strictEqual(endpoints.runtime, "https://xytara.onrender.com", "runtime endpoint mismatch");
238
261
  assert.strictEqual(endpoints.mcp, "https://xytara-mcp-executor.onrender.com/mcp", "mcp endpoint mismatch");
239
262
  assert.strictEqual(endpoints.website, "https://www.naxytra.com", "website endpoint mismatch");
@@ -283,6 +306,65 @@ async function main() {
283
306
  assert.strictEqual(verifyDeliveryReceipt(null, [signer.trustedKey]).reason, "receipt_required", "missing receipt must fail");
284
307
  });
285
308
 
309
+ check("attestation_offline_round_trip", () => {
310
+ const issuer = makeSigner();
311
+ const toolCall = { tool: "tx.status", arguments: { tx_hash: "0x" + "9b".repeat(32), network: "base" }, output: { ok: true, found: true, status: "success", block_number: 4080, confirmations: 17 } };
312
+ const bindings = [{ scheme: "ctp/1", system_id: "NAXYTRA-ATTESTATION/1", tag: "7d".repeat(32) }];
313
+ assert.deepStrictEqual(client.normalizeExternalBinding(bindings[0]).binding, bindings[0], "a ctp/1 binding { scheme, system_id, tag } normalizes to itself");
314
+ assert.deepStrictEqual(client.EXTERNAL_BINDING_SCHEMES, ["ctp/1"], "known binding schemes");
315
+ const attestation = signAttestation({
316
+ attestation_id: "att_0123456789ab",
317
+ issued_at_iso: "2026-01-01T00:00:02.000Z",
318
+ issuer: "xytara-mcp-executor",
319
+ subject: { kind: "transaction_status", network: "eip155:8453", tx: toolCall.arguments.tx_hash },
320
+ claim: { verified: true, method: "base-rpc-receipt/1", observed_at_iso: "2026-01-01T00:00:01.000Z", block_number: 4080, confirmations: 17, status: "success", details: { from: "0x" + "11".repeat(20), to: null } },
321
+ evidence_sha256: hashCanonical(toolCall),
322
+ bindings
323
+ }, issuer);
324
+ assert.strictEqual(attestation.version, ATTESTATION_VERSION, "attestation version");
325
+ assert.deepStrictEqual(Object.keys(attestation.sig).sort(), ["kids", "signatures", "suite"], "sig = { suite, kids, signatures }");
326
+ const result = verifyAttestation(attestation, [issuer.trustedKey]);
327
+ assert.strictEqual(result.valid, true, `genuine attestation must verify: ${JSON.stringify(result)}`);
328
+ assert.deepStrictEqual([result.attestation_id, result.subject_kind, result.claim_verified, result.signatures[0].alg, result.signed_bytes_sha256.length], ["att_0123456789ab", "transaction_status", true, "EdDSA", 64], "verify result reports id, subject kind, claim and signature");
329
+ assert.strictEqual(result.signed_bytes_sha256, crypto.createHash("sha256").update(attestationSignedBytes(attestation)).digest("hex"), "signed bytes are the attestation with sig.signatures removed");
330
+ assert.ok(canonicalize({ ...attestation, sig: { suite: attestation.sig.suite, kids: attestation.sig.kids } }) === attestationSignedBytes(attestation).toString("utf8") && canonicalize(attestation).includes("\"suite\":[\"EdDSA\"]"), "sig.suite and sig.kids are inside the signed bytes");
331
+ assert.strictEqual(hashCanonical(toolCall), attestation.evidence_sha256, "evidence hash recomputes from { tool, arguments, output }");
332
+ // bindings: reserved, round-trips unchanged through canonicalization + verification
333
+ const reordered = JSON.parse(JSON.stringify({ sig: attestation.sig, bindings: attestation.bindings, claim: attestation.claim, ...attestation }));
334
+ assert.strictEqual(verifyAttestation(reordered, [issuer.trustedKey]).valid, true, "key order must not matter (JCS)");
335
+ assert.deepStrictEqual(reordered.bindings, bindings, "bindings survive the round trip unchanged");
336
+ assert.strictEqual(client.attestationSha256(reordered), client.attestationSha256(attestation), "attestation_sha256 is the same digest whatever the key order (JCS, signatures included)");
337
+ assert.notStrictEqual(client.attestationSha256(attestation), result.signed_bytes_sha256, "attestation_sha256 (signatures included) differs from the signed-bytes hash");
338
+ const negative = signAttestation({ ...attestation, claim: { ...attestation.claim, verified: false, status: "not_found" } }, issuer);
339
+ assert.strictEqual(verifyAttestation(negative, [issuer.trustedKey]).claim_verified, false, "a negative claim (verified:false) is a valid signed attestation");
340
+ // tamper -> reject
341
+ const tamperedClaim = JSON.parse(JSON.stringify(attestation));
342
+ tamperedClaim.claim.verified = false;
343
+ assert.strictEqual(verifyAttestation(tamperedClaim, [issuer.trustedKey]).valid, false, "tampered claim must fail");
344
+ const tamperedBindings = JSON.parse(JSON.stringify(attestation));
345
+ tamperedBindings.bindings.push({ scheme: "ctp/1", system_id: "LATER", tag: "00".repeat(32) });
346
+ assert.strictEqual(verifyAttestation(tamperedBindings, [issuer.trustedKey]).valid, false, "bindings are signed: adding one later must fail");
347
+ const removedBindings = JSON.parse(JSON.stringify(attestation));
348
+ removedBindings.bindings = [];
349
+ assert.strictEqual(verifyAttestation(removedBindings, [issuer.trustedKey]).valid, false, "removing a binding must fail");
350
+ assert.strictEqual(verifyAttestation({ ...attestation, bindings: null }, [issuer.trustedKey]).reason, "bindings_must_be_an_array", "bindings must be an array");
351
+ const swappedKid = JSON.parse(JSON.stringify(attestation));
352
+ swappedKid.sig.kids.EdDSA = makeSigner().kid;
353
+ assert.strictEqual(verifyAttestation(swappedKid, [issuer.trustedKey]).signatures[0].reason, "kid_not_in_trusted_key_set", "swapped kid must fail");
354
+ assert.strictEqual(verifyAttestation(attestation, [makeSigner().trustedKey]).signatures[0].reason, "kid_not_in_trusted_key_set", "untrusted key set must fail");
355
+ const stripped = JSON.parse(JSON.stringify(attestation));
356
+ stripped.sig.signatures = {};
357
+ assert.strictEqual(verifyAttestation(stripped, [issuer.trustedKey]).signatures[0].reason, "signature_missing_for_signed_suite", "stripped signature must fail");
358
+ const noSuite = JSON.parse(JSON.stringify(attestation));
359
+ delete noSuite.sig.suite;
360
+ assert.strictEqual(verifyAttestation(noSuite, [issuer.trustedKey]).reason, "attestation_unsigned", "missing suite must fail");
361
+ assert.strictEqual(verifyAttestation({ ...attestation, version: "naxytra-attestation/0" }, [issuer.trustedKey]).reason, "unsupported_attestation_version", "unknown version must fail");
362
+ assert.strictEqual(verifyAttestation(null, [issuer.trustedKey]).reason, "attestation_required", "missing attestation must fail");
363
+ const recipe = attestationRecipe("/.well-known/delivery-receipt-keys");
364
+ assert.deepStrictEqual([recipe.version, recipe.keys, recipe.steps.length], [ATTESTATION_VERSION, "/.well-known/delivery-receipt-keys", 4], "attestation recipe");
365
+ assert.ok(recipe.signed_bytes.includes("sig.signatures"), "recipe states the signed form");
366
+ });
367
+
286
368
  check("x402_header_helpers", () => {
287
369
  const authorization = { from: PAYER, to: ACCEPTS[0].payTo, value: ACCEPTS[0].amount, validAfter: "0", validBefore: "9999999999", nonce: "0x" + "00".repeat(32) };
288
370
  const header = encodeX402PaymentHeader({ accepted: ACCEPTS[0], authorization, signature: "0x" + "11".repeat(65) });