xytara 3.2.1 → 3.3.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 +75 -0
- package/RELEASE_NOTES.md +5 -7
- package/client.js +11 -1
- package/lib/delivery_receipt_verify.js +92 -1
- package/package.json +3 -2
- package/scripts/verify_client.js +79 -1
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,50 @@ 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` is reserved for external
|
|
198
|
+
bindings (for example `{ "scheme": "ctp/1", "q": "…" }`) and must be carried unchanged — it is
|
|
199
|
+
inside the signed bytes. Signed bytes = JCS of the attestation with `sig.signatures` removed
|
|
200
|
+
(`sig.suite` and `sig.kids` stay signed), with the same keys as the delivery receipts.
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
const { verifyAttestation, hashCanonical, attestationRecipe } = require("xytara");
|
|
204
|
+
|
|
205
|
+
const keys = (await fetch("https://xytara-mcp-executor.onrender.com/.well-known/delivery-receipt-keys").then((r) => r.json())).keys;
|
|
206
|
+
const check = verifyAttestation(attestation, keys); // { valid, claim_verified, signatures: [{ alg, kid, valid }], ... }
|
|
207
|
+
const evidenceMatches = hashCanonical({ tool, arguments, output }) === attestation.evidence_sha256;
|
|
208
|
+
console.log(attestationRecipe("/.well-known/delivery-receipt-keys").steps);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
GET /.well-known/delivery-receipt-keys # keys + attestation_recipe (same keys as receipts)
|
|
213
|
+
GET /attestations/:attestation_id # one of the last 5000 issued
|
|
214
|
+
POST /attestations/verify # free signature check: { attestation } -> { valid, kids_known, ... }
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Key-bound spend credentials: send your `X-Agent-Signature` / `X-Agent-Timestamp` headers with the
|
|
218
|
+
MCP call; the executor forwards them to the credits debit unchanged. A request-bound signature
|
|
219
|
+
commits to `{ account_id, units, task_ref: "adapter.mcp.invoke" }` with `units` = the tool's
|
|
220
|
+
`credits_per_call` (3, or 6 for an attesting tool).
|
|
221
|
+
|
|
147
222
|
## Verify the package
|
|
148
223
|
|
|
149
224
|
```bash
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
`xytara` 3.
|
|
1
|
+
`xytara` 3.3.0 introduces the first paid products beyond pay-per-call tools: signed attestations and team spend authority.
|
|
2
2
|
|
|
3
|
-
- **
|
|
4
|
-
- **
|
|
5
|
-
- **
|
|
6
|
-
-
|
|
7
|
-
- **Payment-required results** now explain the allowance, the two ways to continue (spend key or x402) and the price. The server card no longer says "no API keys".
|
|
8
|
-
- Client package (thin): unchanged surface; the MCP server itself is not distributed.
|
|
3
|
+
- **Signed attestations (verified answers)** — `x402.verify-receipt` and `tx.status` now return, in `_meta["naxytra.com/attestation"]`, a signed `naxytra-attestation/1` statement of the on-chain fact they verified (settlement matched or not; transaction status, block, confirmations), JCS-canonical and signed Ed25519 + ML-DSA-65 with the same keys as delivery receipts, with an `evidence_sha256` and a reserved `bindings` field. Negative findings are attested too; nothing is attested without a live chain read. Price $0.02 per call via x402 (20000 units on Base mainnet, 2000 on Base Sepolia) or 6 credits with a spend key; still within the free daily allowance. Free: `GET /attestations/<id>`, `POST /attestations/verify`, and offline verification with the client (`verifyAttestation`).
|
|
4
|
+
- **Teams (beta)** — an owner key (the key a top-up returns) mints capped agent keys: `POST /v1/credits/agents` (per-call, per-day and total caps, expiry, tool allow-list, optional key-binding so every spend needs a fresh request-bound signature), `GET /v1/credits/agents`, `POST /v1/credits/agents/:id/revoke`, `GET /v1/credits/activity` (audit trail). Caps are enforced before any provider work runs and again at commit; an agent key cannot mint keys, see other agents, or borrow another agent's caps.
|
|
5
|
+
- **Fixes** — an uncapped authority binding no longer refuses every delegated spend; the daily report lists the most common detail behind each frequent failure reason and names unidentified callers by their user-agent token.
|
|
6
|
+
- Client package (thin): adds `verifyAttestation`, `attestationRecipe`, `attestationSignedBytes`, `ATTESTATION_VERSION`; tarball otherwise unchanged.
|
package/client.js
CHANGED
|
@@ -7,11 +7,15 @@
|
|
|
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
|
+
attestationRecipe
|
|
15
19
|
} = require("./lib/delivery_receipt_verify");
|
|
16
20
|
const { version } = require("./package.json");
|
|
17
21
|
|
|
@@ -70,6 +74,12 @@ module.exports = {
|
|
|
70
74
|
keyRef,
|
|
71
75
|
verifyDeliveryReceipt,
|
|
72
76
|
verificationRecipe,
|
|
77
|
+
// Signed attestations (the MCP executor's verified answers): offline verification with the
|
|
78
|
+
// same published keys.
|
|
79
|
+
ATTESTATION_VERSION,
|
|
80
|
+
attestationSignedBytes,
|
|
81
|
+
verifyAttestation,
|
|
82
|
+
attestationRecipe,
|
|
73
83
|
// x402 header helpers for the paid command path.
|
|
74
84
|
X402_PAYMENT_HEADER,
|
|
75
85
|
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,12 @@ 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
|
+
|
|
26
38
|
// JCS for the JSON values a receipt carries (strings, finite numbers, booleans, null, arrays,
|
|
27
39
|
// objects). Object keys sorted by UTF-16 code units; `undefined` members dropped like JSON.
|
|
28
40
|
function canonicalize(value) {
|
|
@@ -124,10 +136,86 @@ function verificationRecipe(keysUrl) {
|
|
|
124
136
|
};
|
|
125
137
|
}
|
|
126
138
|
|
|
139
|
+
// --- attestations ------------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
function isPlainObject(value) {
|
|
142
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// The bytes every attestation signature covers (see ATTESTATION_CANONICALIZATION).
|
|
146
|
+
function attestationSignedBytes(attestation) {
|
|
147
|
+
const { sig, ...rest } = attestation;
|
|
148
|
+
const { signatures: _omit, ...signedSig } = isPlainObject(sig) ? sig : {};
|
|
149
|
+
return Buffer.from(canonicalize({ ...rest, sig: signedSig }), "utf8");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Verifies an attestation against a set of trusted public keys ([{ kid, alg, public_key_spki_der_base64 }]).
|
|
153
|
+
// Valid only if the version is known, `bindings` is an array (reserved; carried unchanged), every
|
|
154
|
+
// algorithm in the SIGNED suite has a kid in sig.kids and a signature in sig.signatures, every such
|
|
155
|
+
// kid is in the trusted set, and every signature verifies over the signed bytes.
|
|
156
|
+
function verifyAttestation(attestation, trustedKeys) {
|
|
157
|
+
const a = isPlainObject(attestation) ? attestation : null;
|
|
158
|
+
if (!a) return { valid: false, reason: "attestation_required" };
|
|
159
|
+
if (a.version !== ATTESTATION_VERSION) return { valid: false, reason: "unsupported_attestation_version", version: a.version || null };
|
|
160
|
+
if (!Array.isArray(a.bindings)) return { valid: false, reason: "bindings_must_be_an_array" };
|
|
161
|
+
const sig = isPlainObject(a.sig) ? a.sig : null;
|
|
162
|
+
if (!sig || !Array.isArray(sig.suite) || sig.suite.length === 0 || !isPlainObject(sig.kids) || !isPlainObject(sig.signatures)) {
|
|
163
|
+
return { valid: false, reason: "attestation_unsigned" };
|
|
164
|
+
}
|
|
165
|
+
const signedBytes = attestationSignedBytes(a);
|
|
166
|
+
const trusted = new Map((Array.isArray(trustedKeys) ? trustedKeys : []).map((key) => [key.kid, key]));
|
|
167
|
+
const results = sig.suite.map((alg) => {
|
|
168
|
+
const kid = typeof sig.kids[alg] === "string" ? sig.kids[alg] : null;
|
|
169
|
+
const value = typeof sig.signatures[alg] === "string" ? sig.signatures[alg] : null;
|
|
170
|
+
if (!kid) return { alg, kid: null, valid: false, reason: "kid_missing_for_signed_suite" };
|
|
171
|
+
if (!value) return { alg, kid, valid: false, reason: "signature_missing_for_signed_suite" };
|
|
172
|
+
const key = trusted.get(kid);
|
|
173
|
+
if (!key || key.alg !== alg) return { alg, kid, valid: false, reason: "kid_not_in_trusted_key_set" };
|
|
174
|
+
try {
|
|
175
|
+
const publicKey = crypto.createPublicKey({ key: Buffer.from(key.public_key_spki_der_base64, "base64"), format: "der", type: "spki" });
|
|
176
|
+
if (keyRef(publicKey) !== kid) return { alg, kid, valid: false, reason: "kid_does_not_match_key" };
|
|
177
|
+
const ok = crypto.verify(null, signedBytes, publicKey, Buffer.from(value, "base64"));
|
|
178
|
+
return { alg, kid, valid: ok, reason: ok ? null : "signature_invalid" };
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return { alg, kid, valid: false, reason: "verification_error", detail: error && error.message ? error.message.slice(0, 120) : null };
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
const valid = results.length > 0 && results.every((item) => item.valid);
|
|
184
|
+
return {
|
|
185
|
+
valid,
|
|
186
|
+
reason: valid ? null : "signature_check_failed",
|
|
187
|
+
attestation_id: typeof a.attestation_id === "string" ? a.attestation_id : null,
|
|
188
|
+
issuer: typeof a.issuer === "string" ? a.issuer : null,
|
|
189
|
+
subject_kind: isPlainObject(a.subject) && typeof a.subject.kind === "string" ? a.subject.kind : null,
|
|
190
|
+
claim_verified: isPlainObject(a.claim) && typeof a.claim.verified === "boolean" ? a.claim.verified : null,
|
|
191
|
+
signatures: results,
|
|
192
|
+
signed_bytes_sha256: sha256Hex(signedBytes)
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// A short, self-contained recipe for verifying an attestation without trusting the issuer's server.
|
|
197
|
+
function attestationRecipe(keysUrl) {
|
|
198
|
+
return {
|
|
199
|
+
version: ATTESTATION_VERSION,
|
|
200
|
+
signed_bytes: `${ATTESTATION_CANONICALIZATION} (UTF-8).`,
|
|
201
|
+
keys: keysUrl || null,
|
|
202
|
+
steps: [
|
|
203
|
+
"1. Fetch the issuer's public keys; check every kid in attestation.sig.kids is listed (and pin them).",
|
|
204
|
+
"2. Remove attestation.sig.signatures (keep sig.suite and sig.kids) and JCS-canonicalize what is left: these are the signed bytes.",
|
|
205
|
+
"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 })).",
|
|
206
|
+
"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 is reserved for external bindings and must be carried unchanged."
|
|
207
|
+
],
|
|
208
|
+
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."
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
127
212
|
module.exports = {
|
|
128
213
|
RECEIPT_VERSION,
|
|
129
214
|
CANONICALIZATION,
|
|
130
215
|
SUPPORTED_ALGS,
|
|
216
|
+
ATTESTATION_VERSION,
|
|
217
|
+
ATTESTATION_CANONICALIZATION,
|
|
218
|
+
ATTESTATION_ID_PATTERN,
|
|
131
219
|
canonicalize,
|
|
132
220
|
sha256Hex,
|
|
133
221
|
hashCanonical,
|
|
@@ -135,5 +223,8 @@ module.exports = {
|
|
|
135
223
|
runtimeSupportsMlDsa,
|
|
136
224
|
publicKeySet,
|
|
137
225
|
verifyDeliveryReceipt,
|
|
138
|
-
verificationRecipe
|
|
226
|
+
verificationRecipe,
|
|
227
|
+
attestationSignedBytes,
|
|
228
|
+
verifyAttestation,
|
|
229
|
+
attestationRecipe
|
|
139
230
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xytara",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.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
|
}
|
package/scripts/verify_client.js
CHANGED
|
@@ -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", "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,61 @@ 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", q: "cid:example" }];
|
|
313
|
+
const attestation = signAttestation({
|
|
314
|
+
attestation_id: "att_0123456789ab",
|
|
315
|
+
issued_at_iso: "2026-01-01T00:00:02.000Z",
|
|
316
|
+
issuer: "xytara-mcp-executor",
|
|
317
|
+
subject: { kind: "transaction_status", network: "eip155:8453", tx: toolCall.arguments.tx_hash },
|
|
318
|
+
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 } },
|
|
319
|
+
evidence_sha256: hashCanonical(toolCall),
|
|
320
|
+
bindings
|
|
321
|
+
}, issuer);
|
|
322
|
+
assert.strictEqual(attestation.version, ATTESTATION_VERSION, "attestation version");
|
|
323
|
+
assert.deepStrictEqual(Object.keys(attestation.sig).sort(), ["kids", "signatures", "suite"], "sig = { suite, kids, signatures }");
|
|
324
|
+
const result = verifyAttestation(attestation, [issuer.trustedKey]);
|
|
325
|
+
assert.strictEqual(result.valid, true, `genuine attestation must verify: ${JSON.stringify(result)}`);
|
|
326
|
+
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");
|
|
327
|
+
assert.strictEqual(result.signed_bytes_sha256, crypto.createHash("sha256").update(attestationSignedBytes(attestation)).digest("hex"), "signed bytes are the attestation with sig.signatures removed");
|
|
328
|
+
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");
|
|
329
|
+
assert.strictEqual(hashCanonical(toolCall), attestation.evidence_sha256, "evidence hash recomputes from { tool, arguments, output }");
|
|
330
|
+
// bindings: reserved, round-trips unchanged through canonicalization + verification
|
|
331
|
+
const reordered = JSON.parse(JSON.stringify({ sig: attestation.sig, bindings: attestation.bindings, claim: attestation.claim, ...attestation }));
|
|
332
|
+
assert.strictEqual(verifyAttestation(reordered, [issuer.trustedKey]).valid, true, "key order must not matter (JCS)");
|
|
333
|
+
assert.deepStrictEqual(reordered.bindings, bindings, "bindings survive the round trip unchanged");
|
|
334
|
+
const negative = signAttestation({ ...attestation, claim: { ...attestation.claim, verified: false, status: "not_found" } }, issuer);
|
|
335
|
+
assert.strictEqual(verifyAttestation(negative, [issuer.trustedKey]).claim_verified, false, "a negative claim (verified:false) is a valid signed attestation");
|
|
336
|
+
// tamper -> reject
|
|
337
|
+
const tamperedClaim = JSON.parse(JSON.stringify(attestation));
|
|
338
|
+
tamperedClaim.claim.verified = false;
|
|
339
|
+
assert.strictEqual(verifyAttestation(tamperedClaim, [issuer.trustedKey]).valid, false, "tampered claim must fail");
|
|
340
|
+
const tamperedBindings = JSON.parse(JSON.stringify(attestation));
|
|
341
|
+
tamperedBindings.bindings.push({ scheme: "ctp/1", q: "added-later" });
|
|
342
|
+
assert.strictEqual(verifyAttestation(tamperedBindings, [issuer.trustedKey]).valid, false, "bindings are signed: adding one later must fail");
|
|
343
|
+
const removedBindings = JSON.parse(JSON.stringify(attestation));
|
|
344
|
+
removedBindings.bindings = [];
|
|
345
|
+
assert.strictEqual(verifyAttestation(removedBindings, [issuer.trustedKey]).valid, false, "removing a binding must fail");
|
|
346
|
+
assert.strictEqual(verifyAttestation({ ...attestation, bindings: null }, [issuer.trustedKey]).reason, "bindings_must_be_an_array", "bindings must be an array");
|
|
347
|
+
const swappedKid = JSON.parse(JSON.stringify(attestation));
|
|
348
|
+
swappedKid.sig.kids.EdDSA = makeSigner().kid;
|
|
349
|
+
assert.strictEqual(verifyAttestation(swappedKid, [issuer.trustedKey]).signatures[0].reason, "kid_not_in_trusted_key_set", "swapped kid must fail");
|
|
350
|
+
assert.strictEqual(verifyAttestation(attestation, [makeSigner().trustedKey]).signatures[0].reason, "kid_not_in_trusted_key_set", "untrusted key set must fail");
|
|
351
|
+
const stripped = JSON.parse(JSON.stringify(attestation));
|
|
352
|
+
stripped.sig.signatures = {};
|
|
353
|
+
assert.strictEqual(verifyAttestation(stripped, [issuer.trustedKey]).signatures[0].reason, "signature_missing_for_signed_suite", "stripped signature must fail");
|
|
354
|
+
const noSuite = JSON.parse(JSON.stringify(attestation));
|
|
355
|
+
delete noSuite.sig.suite;
|
|
356
|
+
assert.strictEqual(verifyAttestation(noSuite, [issuer.trustedKey]).reason, "attestation_unsigned", "missing suite must fail");
|
|
357
|
+
assert.strictEqual(verifyAttestation({ ...attestation, version: "naxytra-attestation/0" }, [issuer.trustedKey]).reason, "unsupported_attestation_version", "unknown version must fail");
|
|
358
|
+
assert.strictEqual(verifyAttestation(null, [issuer.trustedKey]).reason, "attestation_required", "missing attestation must fail");
|
|
359
|
+
const recipe = attestationRecipe("/.well-known/delivery-receipt-keys");
|
|
360
|
+
assert.deepStrictEqual([recipe.version, recipe.keys, recipe.steps.length], [ATTESTATION_VERSION, "/.well-known/delivery-receipt-keys", 4], "attestation recipe");
|
|
361
|
+
assert.ok(recipe.signed_bytes.includes("sig.signatures"), "recipe states the signed form");
|
|
362
|
+
});
|
|
363
|
+
|
|
286
364
|
check("x402_header_helpers", () => {
|
|
287
365
|
const authorization = { from: PAYER, to: ACCEPTS[0].payTo, value: ACCEPTS[0].amount, validAfter: "0", validBefore: "9999999999", nonce: "0x" + "00".repeat(32) };
|
|
288
366
|
const header = encodeX402PaymentHeader({ accepted: ACCEPTS[0], authorization, signature: "0x" + "11".repeat(65) });
|