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 +80 -0
- package/RELEASE_NOTES.md +8 -4
- package/client.js +19 -1
- package/lib/delivery_receipt_verify.js +126 -1
- package/package.json +3 -2
- package/scripts/verify_client.js +83 -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,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.
|
|
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
|
-
-
|
|
4
|
-
-
|
|
5
|
-
-
|
|
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.
|
|
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
|
}
|
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", "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) });
|