@zeroxyz/sdk 0.13.0 → 0.14.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/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ All notable changes to `@zeroxyz/sdk` will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — with the pre-1.0 caveat that minor versions may include breaking changes.
6
6
 
7
+ ## 0.14.0
8
+
9
+ ### Added
10
+
11
+ - **Explicit payment-protocol selection** — `client.fetch(url, { paymentProtocol: "x402" | "mpp" })` forces one advertised rail instead of using the default x402-first negotiation order. Selection is strict: when the requested protocol is absent, the fetch fails without silently falling back. The CLI exposes the same control as `zero fetch --protocol x402|mpp`. (#785)
12
+
13
+ ### Fixed
14
+
15
+ - **ZeroClick-style x402 identity-then-charge flows** — `client.fetch()` now handles one zero-value identity challenge before exactly one positive-value payment attempt, reparsing the fresh post-identity 402 instead of signing the stale `$0` challenge. `maxPay` still gates the real charge before signing, and any second positive-value challenge is refused before another payment authorization is created. (#785)
16
+
7
17
  ## 0.13.0
8
18
 
9
19
  ### Added
package/README.md CHANGED
@@ -95,7 +95,7 @@ The SDK refreshes the access token automatically after a `401`, and every refres
95
95
  `client.fetch()` is the call you'll reach for most. Given a URL, it:
96
96
 
97
97
  1. Sends the request.
98
- 2. If the server answers `402 Payment Required` (x402 or MPP), reads the payment challenge and pays it.
98
+ 2. If the server answers `402 Payment Required` (x402 or MPP), reads the payment challenge and pays it. For x402 endpoints that use a zero-dollar identity challenge before the real charge, it handles one free identity stage followed by exactly one positive-value payment attempt.
99
99
  3. Replays the request with proof of payment.
100
100
  4. Returns the body, status, and payment metadata together.
101
101
  5. Records a run on Zero when you pass a `capabilityId`, so the capability's reliability signal stays current and the call stays reviewable.
@@ -131,7 +131,8 @@ const result = await client.fetch(url, {
131
131
  method: "POST",
132
132
  headers: { "content-type": "application/json" },
133
133
  body: JSON.stringify({ city: "tokyo" }),
134
- maxPay: "0.01", // USDC ceiling; an over-cap challenge aborts before signing
134
+ maxPay: "0.01", // USDC ceiling; an over-cap challenge aborts before signing
135
+ paymentProtocol: "mpp", // optional: force "x402" or "mpp" when both are offered
135
136
  capabilityId: "cap_abc", // attribute the run to a known capability
136
137
  walletAddress: "0x…", // session mode: pay from this profile wallet (default: primary)
137
138
  signal: controller.signal,
@@ -195,7 +196,7 @@ for (const warning of result.warnings ?? []) {
195
196
 
196
197
  ### `maxPay`
197
198
 
198
- `maxPay` is your hard per-call ceiling, in USDC. The challenge amount is checked against it **before** anything is signed; an over-cap challenge aborts with `outcome: "payment_failed"` and no on-chain spend. Set it on every paid call — it's the guardrail that keeps a mispriced or hostile endpoint from draining the wallet.
199
+ `maxPay` is your hard per-call ceiling, in USDC. The single positive-value challenge is checked **before** it is signed; a preceding zero-value identity handshake does not consume the cap. An over-cap challenge aborts with `outcome: "payment_failed"`, and a second positive-value challenge is refused before signing. Set `maxPay` on every paid call — it's the guardrail that keeps a mispriced or hostile endpoint from draining the wallet.
199
200
 
200
201
  To put a human in the loop before any spend, read the price up front, show it to your user, and only call `fetch()` with `maxPay` once they approve. Prefer `capabilities.get()`'s **`pricing`** object when present: `pricing.summary` is the server-resolved display string (render it verbatim — don't re-derive from raw fields), `pricing.kind` classifies the shape (`static | dynamic | session | metered | subscription | free | unknown`), and `pricing.primary.amountUsd` / `depositUsd` give the structured numbers for sizing `maxPay`. `cost.amount` remains on every search result as the raw advertised scalar, but it flattens usage-priced caps — a `dynamic` cap's real cost scales with input.
201
202
 
@@ -8,7 +8,6 @@ var client$1 = require('@x402/core/client');
8
8
  var http = require('@x402/core/http');
9
9
  var client = require('@x402/evm/exact/client');
10
10
  var signInWithX = require('@x402/extensions/sign-in-with-x');
11
- var fetch = require('@x402/fetch');
12
11
  var mppx = require('mppx');
13
12
  var client$2 = require('mppx/client');
14
13
  var crypto$1 = require('crypto');
@@ -700,6 +699,9 @@ var Payments = class {
700
699
  input.walletAddress
701
700
  );
702
701
  let capturedAmount = null;
702
+ let pendingAmountUnits = 0n;
703
+ let identityChallengeHandled = false;
704
+ let paidChallengeHandled = false;
703
705
  let capturedNetwork;
704
706
  let preflightError = null;
705
707
  const x402 = client.registerExactEvmScheme(new client$1.x402Client(), { signer: account });
@@ -728,11 +730,25 @@ var Payments = class {
728
730
  };
729
731
  }
730
732
  const rawAmountUnits = BigInt(rawAmount);
731
- capturedAmount = viem.formatUnits(rawAmountUnits, 6);
733
+ pendingAmountUnits = rawAmountUnits;
734
+ const challengeAmount = viem.formatUnits(rawAmountUnits, 6);
735
+ if (rawAmountUnits === 0n) {
736
+ if (identityChallengeHandled || paidChallengeHandled) {
737
+ return {
738
+ abort: true,
739
+ reason: "Only one zero-value identity challenge is allowed, and it must precede payment."
740
+ };
741
+ }
742
+ } else if (paidChallengeHandled) {
743
+ return {
744
+ abort: true,
745
+ reason: "A positive-value x402 payment was already attempted for this fetch; refusing a second charge."
746
+ };
747
+ }
732
748
  if (maxPayUnits !== null && rawAmountUnits > maxPayUnits) {
733
749
  return {
734
750
  abort: true,
735
- reason: `Payment of ${capturedAmount} USDC exceeds maxPay ${input.maxPay}`
751
+ reason: `Payment of ${challengeAmount} USDC exceeds maxPay ${input.maxPay}`
736
752
  };
737
753
  }
738
754
  const detected = networkToChain(capturedNetwork);
@@ -752,9 +768,10 @@ var Payments = class {
752
768
  }
753
769
  if (balance !== null && balance < rawAmountUnits) {
754
770
  const have = viem.formatUnits(balance, 6);
771
+ const requiredAmount = viem.formatUnits(rawAmountUnits, 6);
755
772
  preflightError = new ZeroInsufficientFundsError(
756
- `wallet has ${have} USDC on Base, needs ${capturedAmount}.`,
757
- { have, need: capturedAmount }
773
+ `wallet has ${have} USDC on Base, needs ${requiredAmount}.`,
774
+ { have, need: requiredAmount }
758
775
  );
759
776
  return { abort: true, reason: preflightError.message };
760
777
  }
@@ -765,18 +782,64 @@ var Payments = class {
765
782
  const httpClient = new http.x402HTTPClient(x402).onPaymentRequired(
766
783
  signInWithX.createSIWxClientHook(account)
767
784
  );
768
- const wrappedFetch = fetch.wrapFetchWithPayment(
769
- withSynthesizedX402Header(configuredFetch),
770
- httpClient
771
- );
785
+ const paymentFetch = withSynthesizedX402Header(configuredFetch);
772
786
  let response;
773
787
  try {
774
- response = await wrappedFetch(input.url, {
788
+ const request2 = new Request(input.url, {
775
789
  method: input.method ?? "GET",
776
790
  headers: input.headers,
777
791
  body: input.body,
778
792
  signal: input.signal
779
793
  });
794
+ response = await paymentFetch(request2.clone());
795
+ for (let paymentStage = 0; paymentStage < 2; paymentStage++) {
796
+ if (response.status !== 402) break;
797
+ const parseChallenge = async (challengeResponse) => {
798
+ let body;
799
+ try {
800
+ const text = await challengeResponse.clone().text();
801
+ if (text) body = JSON.parse(text);
802
+ } catch {
803
+ }
804
+ return httpClient.getPaymentRequiredResponse(
805
+ (name) => challengeResponse.headers.get(name),
806
+ body
807
+ );
808
+ };
809
+ let paymentRequired = await parseChallenge(response);
810
+ const hookHeaders = await httpClient.handlePaymentRequired(paymentRequired);
811
+ if (hookHeaders) {
812
+ const hookRequest = request2.clone();
813
+ for (const [key, value] of Object.entries(hookHeaders)) {
814
+ hookRequest.headers.set(key, value);
815
+ }
816
+ response = await paymentFetch(hookRequest);
817
+ if (response.status === 402) {
818
+ paymentRequired = await parseChallenge(response);
819
+ } else {
820
+ break;
821
+ }
822
+ }
823
+ pendingAmountUnits = 0n;
824
+ const paymentPayload = await x402.createPaymentPayload(paymentRequired);
825
+ if (pendingAmountUnits === 0n) {
826
+ identityChallengeHandled = true;
827
+ capturedAmount = "0";
828
+ } else {
829
+ paidChallengeHandled = true;
830
+ capturedAmount = viem.formatUnits(pendingAmountUnits, 6);
831
+ }
832
+ const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
833
+ const paidRequest = request2.clone();
834
+ for (const [key, value] of Object.entries(paymentHeaders)) {
835
+ paidRequest.headers.set(key, value);
836
+ }
837
+ paidRequest.headers.set(
838
+ "Access-Control-Expose-Headers",
839
+ "PAYMENT-RESPONSE,X-PAYMENT-RESPONSE"
840
+ );
841
+ response = await paymentFetch(paidRequest);
842
+ }
780
843
  } catch (err) {
781
844
  if (preflightError) throw preflightError;
782
845
  if (err instanceof ZeroError) throw err;
@@ -1344,8 +1407,17 @@ var cancelBody2 = (response) => {
1344
1407
  } catch {
1345
1408
  }
1346
1409
  };
1347
- var detectPaymentRequirement = async (response) => {
1410
+ var detectPaymentRequirement = async (response, preferredProtocol) => {
1348
1411
  if (response.status !== 402) return null;
1412
+ const wwwAuth = response.headers.get("www-authenticate");
1413
+ const advertisesMpp = wwwAuth ? splitWwwAuthenticateChallenges(wwwAuth).some((challenge) => {
1414
+ const scheme = challenge.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
1415
+ return MPP_SCHEME_TOKENS.has(scheme);
1416
+ }) : false;
1417
+ if (preferredProtocol === "mpp") {
1418
+ cancelBody2(response);
1419
+ return advertisesMpp ? { protocol: "mpp", raw: { "www-authenticate": wwwAuth } } : { protocol: "unknown", raw: { requestedProtocol: "mpp" } };
1420
+ }
1349
1421
  const x402Header = response.headers.get("payment-required") ?? response.headers.get("x-payment-required");
1350
1422
  if (x402Header) {
1351
1423
  cancelBody2(response);
@@ -1375,15 +1447,13 @@ var detectPaymentRequirement = async (response) => {
1375
1447
  } catch {
1376
1448
  }
1377
1449
  }
1378
- const wwwAuth = response.headers.get("www-authenticate");
1379
- if (wwwAuth) {
1380
- const schemes = splitWwwAuthenticateChallenges(wwwAuth).map(
1381
- (challenge) => challenge.trim().split(/\s+/)[0]?.toLowerCase() ?? ""
1382
- );
1383
- if (schemes.some((scheme) => MPP_SCHEME_TOKENS.has(scheme))) {
1384
- cancelBody2(response);
1385
- return { protocol: "mpp", raw: { "www-authenticate": wwwAuth } };
1386
- }
1450
+ if (preferredProtocol === "x402") {
1451
+ cancelBody2(response);
1452
+ return { protocol: "unknown", raw: { requestedProtocol: "x402" } };
1453
+ }
1454
+ if (advertisesMpp) {
1455
+ cancelBody2(response);
1456
+ return { protocol: "mpp", raw: { "www-authenticate": wwwAuth } };
1387
1457
  }
1388
1458
  cancelBody2(response);
1389
1459
  return { protocol: "unknown", raw: {} };
@@ -1709,7 +1779,10 @@ var clientFetch = async (client, url, opts = {}) => {
1709
1779
  return result2;
1710
1780
  }
1711
1781
  if (response.status === 402) {
1712
- const requirement = await detectPaymentRequirement(response);
1782
+ const requirement = await detectPaymentRequirement(
1783
+ response,
1784
+ opts.paymentProtocol
1785
+ );
1713
1786
  if (requirement?.protocol === "x402" || requirement?.protocol === "mpp") {
1714
1787
  const payCall = requirement.protocol === "x402" ? client.payments.pay : client.payments.payMpp;
1715
1788
  try {
@@ -1781,7 +1854,7 @@ var clientFetch = async (client, url, opts = {}) => {
1781
1854
  } else if (requirement?.protocol === "unknown") {
1782
1855
  const result2 = errorFetchResult(
1783
1856
  startedAt,
1784
- "Capability returned 402 with an unrecognized payment protocol. Neither x402 nor MPP markers were detected in the response headers or body.",
1857
+ opts.paymentProtocol ? `Capability did not advertise the requested ${opts.paymentProtocol} payment protocol.` : "Capability returned 402 with an unrecognized payment protocol. Neither x402 nor MPP markers were detected in the response headers or body.",
1785
1858
  "unrecognized_402_protocol",
1786
1859
  capabilityIdRequested,
1787
1860
  "payment_failed",
@@ -1965,7 +2038,7 @@ var clientFetch = async (client, url, opts = {}) => {
1965
2038
 
1966
2039
  // package.json
1967
2040
  var package_default = {
1968
- version: "0.13.0"};
2041
+ version: "0.14.0"};
1969
2042
 
1970
2043
  // src/version.ts
1971
2044
  var SDK_VERSION = package_default.version;
@@ -3955,5 +4028,5 @@ exports.normalizeTransportEnvelopeRequest = normalizeTransportEnvelopeRequest;
3955
4028
  exports.paymentHasAnchor = paymentHasAnchor;
3956
4029
  exports.tempoChainLabelFromId = tempoChainLabelFromId;
3957
4030
  exports.unwrapTransportEnvelope = unwrapTransportEnvelope;
3958
- //# sourceMappingURL=chunk-2A2ZZ5XR.cjs.map
3959
- //# sourceMappingURL=chunk-2A2ZZ5XR.cjs.map
4031
+ //# sourceMappingURL=chunk-AM5RDERD.cjs.map
4032
+ //# sourceMappingURL=chunk-AM5RDERD.cjs.map