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 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` 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
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.3.0 introduces the first paid products beyond pay-per-call tools: signed attestations and team spend authority.
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
- - **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.
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 is reserved for external bindings and must be carried unchanged."
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.0",
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": {
@@ -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", q: "cid:example" }];
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", q: "added-later" });
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 = [];