xytara 3.3.0 → 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 +8 -3
- package/RELEASE_NOTES.md +8 -5
- package/client.js +8 -0
- package/lib/delivery_receipt_verify.js +35 -1
- package/package.json +1 -1
- package/scripts/verify_client.js +7 -3
package/README.md
CHANGED
|
@@ -194,9 +194,14 @@ live RPC read, that you can hand to another party.
|
|
|
194
194
|
`subject.kind` is `x402_settlement` (method `base-rpc-logs/1`) or `transaction_status` (method
|
|
195
195
|
`base-rpc-receipt/1`, with `confirmations`). A negative answer (`claim.verified: false` — the
|
|
196
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`
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
200
205
|
(`sig.suite` and `sig.kids` stay signed), with the same keys as the delivery receipts.
|
|
201
206
|
|
|
202
207
|
```js
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,6 +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
|
-
- **
|
|
6
|
-
-
|
|
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
|
@@ -15,6 +15,9 @@ const {
|
|
|
15
15
|
verificationRecipe,
|
|
16
16
|
attestationSignedBytes,
|
|
17
17
|
verifyAttestation,
|
|
18
|
+
attestationSha256,
|
|
19
|
+
normalizeExternalBinding,
|
|
20
|
+
EXTERNAL_BINDING_SCHEMES,
|
|
18
21
|
attestationRecipe
|
|
19
22
|
} = require("./lib/delivery_receipt_verify");
|
|
20
23
|
const { version } = require("./package.json");
|
|
@@ -80,6 +83,11 @@ module.exports = {
|
|
|
80
83
|
attestationSignedBytes,
|
|
81
84
|
verifyAttestation,
|
|
82
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,
|
|
83
91
|
// x402 header helpers for the paid command path.
|
|
84
92
|
X402_PAYMENT_HEADER,
|
|
85
93
|
X402_PAYMENT_REQUIRED_HEADER,
|
|
@@ -34,6 +34,15 @@ const ATTESTATION_VERSION = "naxytra-attestation/1";
|
|
|
34
34
|
// `sig.kids` stay INSIDE the signed bytes, so a stripped algorithm or a swapped key id fails.
|
|
35
35
|
const ATTESTATION_CANONICALIZATION = "JCS (RFC 8785) of the attestation with `sig.signatures` removed (`sig.suite` and `sig.kids` stay signed)";
|
|
36
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}$/;
|
|
37
46
|
|
|
38
47
|
// JCS for the JSON values a receipt carries (strings, finite numbers, booleans, null, arrays,
|
|
39
48
|
// objects). Object keys sorted by UTF-16 code units; `undefined` members dropped like JSON.
|
|
@@ -193,6 +202,27 @@ function verifyAttestation(attestation, trustedKeys) {
|
|
|
193
202
|
};
|
|
194
203
|
}
|
|
195
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
|
+
|
|
196
226
|
// A short, self-contained recipe for verifying an attestation without trusting the issuer's server.
|
|
197
227
|
function attestationRecipe(keysUrl) {
|
|
198
228
|
return {
|
|
@@ -203,8 +233,9 @@ function attestationRecipe(keysUrl) {
|
|
|
203
233
|
"1. Fetch the issuer's public keys; check every kid in attestation.sig.kids is listed (and pin them).",
|
|
204
234
|
"2. Remove attestation.sig.signatures (keep sig.suite and sig.kids) and JCS-canonicalize what is left: these are the signed bytes.",
|
|
205
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 })).",
|
|
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
|
|
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."
|
|
207
237
|
],
|
|
238
|
+
external_bindings: { schemes: EXTERNAL_BINDING_SCHEMES.slice(), digest: "attestation_sha256 = sha256(JCS(attestation)), signatures included" },
|
|
208
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."
|
|
209
240
|
};
|
|
210
241
|
}
|
|
@@ -216,6 +247,7 @@ module.exports = {
|
|
|
216
247
|
ATTESTATION_VERSION,
|
|
217
248
|
ATTESTATION_CANONICALIZATION,
|
|
218
249
|
ATTESTATION_ID_PATTERN,
|
|
250
|
+
EXTERNAL_BINDING_SCHEMES,
|
|
219
251
|
canonicalize,
|
|
220
252
|
sha256Hex,
|
|
221
253
|
hashCanonical,
|
|
@@ -226,5 +258,7 @@ module.exports = {
|
|
|
226
258
|
verificationRecipe,
|
|
227
259
|
attestationSignedBytes,
|
|
228
260
|
verifyAttestation,
|
|
261
|
+
attestationSha256,
|
|
262
|
+
normalizeExternalBinding,
|
|
229
263
|
attestationRecipe
|
|
230
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": {
|
package/scripts/verify_client.js
CHANGED
|
@@ -252,7 +252,7 @@ function runCliAsync(args) {
|
|
|
252
252
|
async function main() {
|
|
253
253
|
check("package_surface", () => {
|
|
254
254
|
assert.strictEqual(typeof CommerceClient, "function", "CommerceClient export missing");
|
|
255
|
-
for (const name of ["verifyDeliveryReceipt", "canonicalize", "hashCanonical", "keyRef", "verificationRecipe", "verifyAttestation", "attestationRecipe", "attestationSignedBytes", "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"]) {
|
|
256
256
|
assert.strictEqual(typeof client[name], "function", `${name} export missing`);
|
|
257
257
|
}
|
|
258
258
|
assert.strictEqual(RECEIPT_VERSION, "naxytra-delivery-receipt/1", "receipt version mismatch");
|
|
@@ -309,7 +309,9 @@ async function main() {
|
|
|
309
309
|
check("attestation_offline_round_trip", () => {
|
|
310
310
|
const issuer = makeSigner();
|
|
311
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",
|
|
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");
|
|
313
315
|
const attestation = signAttestation({
|
|
314
316
|
attestation_id: "att_0123456789ab",
|
|
315
317
|
issued_at_iso: "2026-01-01T00:00:02.000Z",
|
|
@@ -331,6 +333,8 @@ async function main() {
|
|
|
331
333
|
const reordered = JSON.parse(JSON.stringify({ sig: attestation.sig, bindings: attestation.bindings, claim: attestation.claim, ...attestation }));
|
|
332
334
|
assert.strictEqual(verifyAttestation(reordered, [issuer.trustedKey]).valid, true, "key order must not matter (JCS)");
|
|
333
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");
|
|
334
338
|
const negative = signAttestation({ ...attestation, claim: { ...attestation.claim, verified: false, status: "not_found" } }, issuer);
|
|
335
339
|
assert.strictEqual(verifyAttestation(negative, [issuer.trustedKey]).claim_verified, false, "a negative claim (verified:false) is a valid signed attestation");
|
|
336
340
|
// tamper -> reject
|
|
@@ -338,7 +342,7 @@ async function main() {
|
|
|
338
342
|
tamperedClaim.claim.verified = false;
|
|
339
343
|
assert.strictEqual(verifyAttestation(tamperedClaim, [issuer.trustedKey]).valid, false, "tampered claim must fail");
|
|
340
344
|
const tamperedBindings = JSON.parse(JSON.stringify(attestation));
|
|
341
|
-
tamperedBindings.bindings.push({ scheme: "ctp/1",
|
|
345
|
+
tamperedBindings.bindings.push({ scheme: "ctp/1", system_id: "LATER", tag: "00".repeat(32) });
|
|
342
346
|
assert.strictEqual(verifyAttestation(tamperedBindings, [issuer.trustedKey]).valid, false, "bindings are signed: adding one later must fail");
|
|
343
347
|
const removedBindings = JSON.parse(JSON.stringify(attestation));
|
|
344
348
|
removedBindings.bindings = [];
|