uvd-x402-sdk 2.87.0 → 2.89.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.
Files changed (73) hide show
  1. package/README.md +235 -0
  2. package/dist/adapters/index.d.mts +1 -1
  3. package/dist/adapters/index.d.ts +1 -1
  4. package/dist/adapters/index.js.map +1 -1
  5. package/dist/adapters/index.mjs.map +1 -1
  6. package/dist/backend/index.d.mts +2 -2
  7. package/dist/backend/index.d.ts +2 -2
  8. package/dist/backend/index.js +138 -0
  9. package/dist/backend/index.js.map +1 -1
  10. package/dist/backend/index.mjs +137 -1
  11. package/dist/backend/index.mjs.map +1 -1
  12. package/dist/erc8128/index.js.map +1 -1
  13. package/dist/erc8128/index.mjs.map +1 -1
  14. package/dist/{index-D8buGZWY.d.mts → index-13ZUDF7c.d.mts} +32 -2
  15. package/dist/{index-CWZUj5mz.d.ts → index-B5B9EK3x.d.ts} +32 -2
  16. package/dist/{index-DOBhTF-j.d.mts → index-BWN0Haa0.d.mts} +476 -2
  17. package/dist/{index-DOBhTF-j.d.ts → index-BWN0Haa0.d.ts} +476 -2
  18. package/dist/{index-BHfYxpzI.d.mts → index-CAJg-_cr.d.mts} +181 -2
  19. package/dist/{index-D67eNxY8.d.ts → index-Cd1Mpznb.d.ts} +181 -2
  20. package/dist/index.d.mts +4 -4
  21. package/dist/index.d.ts +4 -4
  22. package/dist/index.js +472 -8
  23. package/dist/index.js.map +1 -1
  24. package/dist/index.mjs +464 -9
  25. package/dist/index.mjs.map +1 -1
  26. package/dist/providers/algorand/index.d.mts +1 -1
  27. package/dist/providers/algorand/index.d.ts +1 -1
  28. package/dist/providers/algorand/index.js.map +1 -1
  29. package/dist/providers/algorand/index.mjs.map +1 -1
  30. package/dist/providers/evm/index.d.mts +1 -1
  31. package/dist/providers/evm/index.d.ts +1 -1
  32. package/dist/providers/evm/index.js.map +1 -1
  33. package/dist/providers/evm/index.mjs.map +1 -1
  34. package/dist/providers/near/index.d.mts +1 -1
  35. package/dist/providers/near/index.d.ts +1 -1
  36. package/dist/providers/near/index.js.map +1 -1
  37. package/dist/providers/near/index.mjs.map +1 -1
  38. package/dist/providers/solana/index.d.mts +1 -1
  39. package/dist/providers/solana/index.d.ts +1 -1
  40. package/dist/providers/solana/index.js.map +1 -1
  41. package/dist/providers/solana/index.mjs.map +1 -1
  42. package/dist/providers/stellar/index.d.mts +1 -1
  43. package/dist/providers/stellar/index.d.ts +1 -1
  44. package/dist/providers/stellar/index.js.map +1 -1
  45. package/dist/providers/stellar/index.mjs.map +1 -1
  46. package/dist/providers/sui/index.d.mts +1 -1
  47. package/dist/providers/sui/index.d.ts +1 -1
  48. package/dist/providers/sui/index.js.map +1 -1
  49. package/dist/providers/sui/index.mjs.map +1 -1
  50. package/dist/providers/xrpl/index.d.mts +1 -1
  51. package/dist/providers/xrpl/index.d.ts +1 -1
  52. package/dist/providers/xrpl/index.js.map +1 -1
  53. package/dist/providers/xrpl/index.mjs.map +1 -1
  54. package/dist/react/index.d.mts +3 -3
  55. package/dist/react/index.d.ts +3 -3
  56. package/dist/react/index.js +463 -8
  57. package/dist/react/index.js.map +1 -1
  58. package/dist/react/index.mjs +463 -8
  59. package/dist/react/index.mjs.map +1 -1
  60. package/dist/react/picker/index.d.mts +1 -1
  61. package/dist/react/picker/index.d.ts +1 -1
  62. package/dist/utils/index.d.mts +2 -2
  63. package/dist/utils/index.d.ts +2 -2
  64. package/dist/utils/index.js.map +1 -1
  65. package/dist/utils/index.mjs.map +1 -1
  66. package/dist/{validation-mSLW4tcO.d.mts → validation-B95g0MqC.d.mts} +1 -1
  67. package/dist/{validation-C7p3a6Ep.d.ts → validation-_Tcjm2gu.d.ts} +1 -1
  68. package/package.json +1 -1
  69. package/src/backend/index.ts +260 -0
  70. package/src/client/X402Client.ts +232 -9
  71. package/src/index.ts +28 -0
  72. package/src/policy.ts +804 -0
  73. package/src/types/index.ts +41 -0
@@ -1631,8 +1631,366 @@ var init_solana = __esm({
1631
1631
 
1632
1632
  // src/client/X402Client.ts
1633
1633
  init_types();
1634
+
1635
+ // src/policy.ts
1636
+ init_types();
1637
+ var OFFER_VALIDITY_EXTENSION = "offer-receipt/1";
1638
+ function offerValidUntil(extensions) {
1639
+ if (!extensions || typeof extensions !== "object") return void 0;
1640
+ const entry = extensions[OFFER_VALIDITY_EXTENSION];
1641
+ if (!entry || typeof entry !== "object") return void 0;
1642
+ const info = entry.info;
1643
+ if (!info || typeof info !== "object") return void 0;
1644
+ const value = info.validUntil;
1645
+ if (typeof value !== "number") return void 0;
1646
+ if (!Number.isSafeInteger(value) || value < 0) return void 0;
1647
+ return value;
1648
+ }
1649
+ function canonicalRecipient(address) {
1650
+ const trimmed = address.trim();
1651
+ const rest = trimmed.startsWith("0x") || trimmed.startsWith("0X") ? trimmed.slice(2) : null;
1652
+ const isHex = rest !== null && rest.length > 0 && /^[0-9a-fA-F]+$/.test(rest);
1653
+ return isHex ? trimmed.toLowerCase() : trimmed;
1654
+ }
1655
+ function assetKey(asset) {
1656
+ return `${asset.network.trim().toLowerCase()}|${canonicalRecipient(asset.address)}`;
1657
+ }
1658
+ function assetLabel(asset) {
1659
+ return asset.address ? `${asset.address} on ${asset.network}` : `(no asset declared) on ${asset.network}`;
1660
+ }
1661
+ function offerAsset(offer) {
1662
+ return {
1663
+ network: offer.chainName ?? offer.network,
1664
+ address: typeof offer.asset === "string" ? offer.asset : ""
1665
+ };
1666
+ }
1667
+ function noReadableOffer(offered) {
1668
+ return {
1669
+ code: "no-readable-offer",
1670
+ offered,
1671
+ message: "no offer in this challenge is one this build can pay; offered: " + JSON.stringify(offered)
1672
+ };
1673
+ }
1674
+ var PolicyRefusedError = class extends X402Error {
1675
+ refusal;
1676
+ constructor(refusal) {
1677
+ super(`payment refused by policy: ${refusal.message}`, "POLICY_REFUSED", refusal);
1678
+ this.name = "PolicyRefusedError";
1679
+ this.refusal = refusal;
1680
+ }
1681
+ /** Stable kebab code of the refusal, for branching. */
1682
+ get policyCode() {
1683
+ return this.refusal.code;
1684
+ }
1685
+ };
1686
+ var PurchasePolicy = class _PurchasePolicy {
1687
+ perPaymentLimits;
1688
+ cumulativeLimits;
1689
+ /** Shared by reference across every copy: one purse, many holders. */
1690
+ spentTotals;
1691
+ recipients;
1692
+ assetsByKey;
1693
+ unlistedAssetsAllowed;
1694
+ constructor(perPaymentLimits, cumulativeLimits, spentTotals, recipients, assetsByKey, unlistedAssetsAllowed) {
1695
+ this.perPaymentLimits = perPaymentLimits;
1696
+ this.cumulativeLimits = cumulativeLimits;
1697
+ this.spentTotals = spentTotals;
1698
+ this.recipients = recipients;
1699
+ this.assetsByKey = assetsByKey;
1700
+ this.unlistedAssetsAllowed = unlistedAssetsAllowed;
1701
+ }
1702
+ /**
1703
+ * A policy that pays nothing until it is told what it may pay.
1704
+ *
1705
+ * # Why the default is deny
1706
+ *
1707
+ * The limits are a map keyed by asset, and a map answers "no entry" for every
1708
+ * asset nobody thought of. Permitting on a missing entry means a budget in
1709
+ * USDC is **no budget at all** for any other token: a seller offering the same
1710
+ * resource priced in something unlisted walks straight past the ceiling, and
1711
+ * the wallet will sign it, because the EVM signer takes the EIP-712 domain
1712
+ * from the seller's own `extra` and will happily sign for a token and a
1713
+ * network it has never heard of.
1714
+ *
1715
+ * So an asset with no stated ceiling is refused. A caller that genuinely wants
1716
+ * to pay anything says so once, by name, with
1717
+ * {@link PurchasePolicy.allowUnlistedAssets}, and that sentence is then in
1718
+ * their code where a reader can find it.
1719
+ */
1720
+ static create() {
1721
+ return new _PurchasePolicy(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), null, /* @__PURE__ */ new Map(), false);
1722
+ }
1723
+ /**
1724
+ * A policy that permits an asset it was never told about.
1725
+ *
1726
+ * This is what {@link X402Client} holds when the caller supplied no policy,
1727
+ * and it exists for exactly one reason: this SDK had no budget before 2.89.0,
1728
+ * and turning one on silently would refuse payments that callers are making
1729
+ * today. Named rather than defaulted, so choosing it is visible. The asymmetry
1730
+ * is deliberate -- whoever sits down to WRITE a policy deserves the safe
1731
+ * default.
1732
+ */
1733
+ static permissive() {
1734
+ return new _PurchasePolicy(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), null, /* @__PURE__ */ new Map(), true);
1735
+ }
1736
+ /** Permit assets with no configured ceiling. */
1737
+ allowUnlistedAssets() {
1738
+ return this.with({ unlistedAssetsAllowed: true });
1739
+ }
1740
+ /**
1741
+ * Refuse assets with no configured ceiling. The default; here so the intent
1742
+ * can be written down at a call site that wants it explicit.
1743
+ */
1744
+ denyUnlistedAssets() {
1745
+ return this.with({ unlistedAssetsAllowed: false });
1746
+ }
1747
+ /** Most this policy will pay in ONE payment of `asset`, in atomic units. */
1748
+ perPayment(asset, amount) {
1749
+ const key = this.register(asset);
1750
+ const perPaymentLimits = new Map(this.perPaymentLimits);
1751
+ perPaymentLimits.set(key, requireCeiling(amount, "perPayment"));
1752
+ return this.with({ perPaymentLimits });
1753
+ }
1754
+ /**
1755
+ * Most this policy will pay in `asset` in TOTAL, across every payment it
1756
+ * approves for as long as it lives.
1757
+ */
1758
+ cumulative(asset, amount) {
1759
+ const key = this.register(asset);
1760
+ const cumulativeLimits = new Map(this.cumulativeLimits);
1761
+ cumulativeLimits.set(key, requireCeiling(amount, "cumulative"));
1762
+ return this.with({ cumulativeLimits });
1763
+ }
1764
+ /**
1765
+ * Restrict payment to a set of recipients.
1766
+ *
1767
+ * Addresses are canonicalised **by family**, not by lowercasing the string:
1768
+ * see {@link canonicalRecipient}.
1769
+ */
1770
+ onlyPay(recipients) {
1771
+ const set = /* @__PURE__ */ new Set();
1772
+ for (const r of recipients) set.add(canonicalRecipient(r));
1773
+ return this.with({ recipients: set });
1774
+ }
1775
+ /**
1776
+ * Total recorded so far for `asset`, in atomic units.
1777
+ *
1778
+ * A total that is not a sane bigint means something corrupted the purse.
1779
+ * Reporting zero would silently restore the caller's whole budget, so this
1780
+ * reports the CEILING instead: for money, the safe direction is to refuse,
1781
+ * never to permit.
1782
+ */
1783
+ spent(asset) {
1784
+ const key = assetKey(asset);
1785
+ const value = this.spentTotals.get(key);
1786
+ if (value === void 0) return 0n;
1787
+ if (typeof value !== "bigint" || value < 0n) {
1788
+ return this.cumulativeLimits.get(key) ?? 0n;
1789
+ }
1790
+ return value;
1791
+ }
1792
+ /**
1793
+ * Record that a payment actually happened.
1794
+ *
1795
+ * Separate from {@link PurchasePolicy.evaluate} on purpose. Signing can still
1796
+ * fail and a settlement can still be refused; a cumulative limit that counted
1797
+ * attempts rather than payments would lock a caller out of money it never
1798
+ * spent. Call this when a payment settled.
1799
+ *
1800
+ * A negative amount is ignored: handing budget back is not something a
1801
+ * settlement can do, and treating it as one would be a way to widen a policy
1802
+ * from outside.
1803
+ */
1804
+ recordSpend(asset, amount) {
1805
+ if (typeof amount !== "bigint" || amount <= 0n) return;
1806
+ const key = assetKey(asset);
1807
+ if (!this.assetsByKey.has(key)) this.assetsByKey.set(key, asset);
1808
+ const current = this.spent(asset);
1809
+ this.spentTotals.set(key, current + amount);
1810
+ }
1811
+ /**
1812
+ * Whether this policy pays an asset it holds no ceiling for. Read-only: there
1813
+ * is no setter that flips it from inside an evaluation.
1814
+ */
1815
+ get permitsUnlistedAssets() {
1816
+ return this.unlistedAssetsAllowed;
1817
+ }
1818
+ /**
1819
+ * Decide whether this offer may be signed. Steps 2 through 6 of the contract;
1820
+ * step 1 (`no-readable-offer`) is decided on the challenge, before an offer
1821
+ * exists to evaluate -- see {@link decideOnChallenge}.
1822
+ *
1823
+ * **Approving does not record the spend.** See
1824
+ * {@link PurchasePolicy.recordSpend}.
1825
+ */
1826
+ evaluate(offer, options) {
1827
+ const { now, validUntil, quote } = options;
1828
+ if (validUntil !== void 0 && now > validUntil) {
1829
+ return {
1830
+ ok: false,
1831
+ refusal: {
1832
+ code: "offer-expired",
1833
+ validUntil,
1834
+ now,
1835
+ message: `this offer expired at ${validUntil} (now ${now}); ask the seller for new terms`
1836
+ }
1837
+ };
1838
+ }
1839
+ if (this.recipients !== null) {
1840
+ const payTo = canonicalRecipient(offer.payTo);
1841
+ if (!this.recipients.has(payTo)) {
1842
+ return {
1843
+ ok: false,
1844
+ refusal: {
1845
+ code: "recipient-not-permitted",
1846
+ payTo: offer.payTo,
1847
+ message: `this policy does not pay ${offer.payTo}`
1848
+ }
1849
+ };
1850
+ }
1851
+ }
1852
+ const asset = offerAsset(offer);
1853
+ const key = assetKey(asset);
1854
+ const requested = readAmount(offer.amount);
1855
+ if (!this.unlistedAssetsAllowed && !this.perPaymentLimits.has(key) && !this.cumulativeLimits.has(key)) {
1856
+ return {
1857
+ ok: false,
1858
+ refusal: {
1859
+ code: "asset-not-budgeted",
1860
+ asset,
1861
+ message: `this policy has no budget for ${assetLabel(asset)}; it pays only what it was told it may pay`
1862
+ }
1863
+ };
1864
+ }
1865
+ const perPaymentAllowed = this.perPaymentLimits.get(key);
1866
+ if (perPaymentAllowed !== void 0 && requested > perPaymentAllowed) {
1867
+ return {
1868
+ ok: false,
1869
+ refusal: {
1870
+ code: "per-payment-limit",
1871
+ requested,
1872
+ allowed: perPaymentAllowed,
1873
+ asset,
1874
+ message: `offer of ${requested} exceeds the per-payment limit of ${perPaymentAllowed} for ${assetLabel(asset)}`
1875
+ }
1876
+ };
1877
+ }
1878
+ const cumulativeAllowed = this.cumulativeLimits.get(key);
1879
+ if (cumulativeAllowed !== void 0) {
1880
+ const spent = this.spent(asset);
1881
+ const wouldTotal = spent + requested;
1882
+ if (wouldTotal > cumulativeAllowed) {
1883
+ return {
1884
+ ok: false,
1885
+ refusal: {
1886
+ code: "cumulative-limit",
1887
+ requested,
1888
+ spent,
1889
+ wouldTotal,
1890
+ allowed: cumulativeAllowed,
1891
+ asset,
1892
+ message: `offer of ${requested} would take spend to ${wouldTotal}, past the cumulative limit of ${cumulativeAllowed} for ${assetLabel(asset)} (already spent ${spent})`
1893
+ }
1894
+ };
1895
+ }
1896
+ }
1897
+ return {
1898
+ ok: true,
1899
+ approval: { asset, amount: requested, versusQuote: compareToQuote(asset, requested, quote) }
1900
+ };
1901
+ }
1902
+ /** Copy sharing the same purse. Cloning is what a per-request client does. */
1903
+ clone() {
1904
+ return this.with({});
1905
+ }
1906
+ /**
1907
+ * Rebuild with some fields replaced, **always sharing `spentTotals`**. This is
1908
+ * the one place that decides copies share a purse, so it is the one place to
1909
+ * read when asking whether they do.
1910
+ */
1911
+ with(changes) {
1912
+ return new _PurchasePolicy(
1913
+ changes.perPaymentLimits ?? this.perPaymentLimits,
1914
+ changes.cumulativeLimits ?? this.cumulativeLimits,
1915
+ this.spentTotals,
1916
+ changes.recipients !== void 0 ? changes.recipients : this.recipients,
1917
+ this.assetsByKey,
1918
+ changes.unlistedAssetsAllowed ?? this.unlistedAssetsAllowed
1919
+ );
1920
+ }
1921
+ /** Remember an asset's written form and reject one no budget could match. */
1922
+ register(asset) {
1923
+ if (!asset || typeof asset.address !== "string" || asset.address.trim() === "") {
1924
+ throw new X402Error(
1925
+ "A budget needs a token address; an asset with none can never match an offer",
1926
+ "INVALID_CONFIG",
1927
+ { asset }
1928
+ );
1929
+ }
1930
+ if (typeof asset.network !== "string" || asset.network.trim() === "") {
1931
+ throw new X402Error(
1932
+ "A budget needs a network; the same address on two networks is two assets",
1933
+ "INVALID_CONFIG",
1934
+ { asset }
1935
+ );
1936
+ }
1937
+ const key = assetKey(asset);
1938
+ this.assetsByKey.set(key, asset);
1939
+ return key;
1940
+ }
1941
+ };
1942
+ function requireCeiling(amount, field) {
1943
+ if (typeof amount !== "bigint" || amount < 0n) {
1944
+ throw new X402Error(
1945
+ `${field} must be a non-negative bigint in atomic units`,
1946
+ "INVALID_AMOUNT",
1947
+ { amount: String(amount) }
1948
+ );
1949
+ }
1950
+ return amount;
1951
+ }
1952
+ function readAmount(amount) {
1953
+ const text = String(amount).trim();
1954
+ if (!/^\d+$/.test(text)) return UNREADABLE_AMOUNT;
1955
+ return BigInt(text);
1956
+ }
1957
+ var UNREADABLE_AMOUNT = (1n << 256n) - 1n;
1958
+ function compareToQuote(asset, requested, quote) {
1959
+ if (!quote) return { code: "not-compared" };
1960
+ if (assetKey(quote.asset) !== assetKey(asset)) {
1961
+ return { code: "different-asset", advertised: quote.asset, offered: asset };
1962
+ }
1963
+ if (quote.amount !== requested) {
1964
+ return { code: "amount-differs", advertised: quote.amount, offered: requested };
1965
+ }
1966
+ return { code: "matches" };
1967
+ }
1968
+ function decideOnChallenge(policy, challenge, offer, options) {
1969
+ const unreadableCount = challenge.unreadableCount ?? challenge.unreadable.length;
1970
+ if (challenge.offers.length === 0 && unreadableCount > 0) {
1971
+ return { ok: false, refusal: noReadableOffer(challenge.unreadable) };
1972
+ }
1973
+ if (!offer) {
1974
+ return { ok: false, refusal: noReadableOffer(challenge.unreadable) };
1975
+ }
1976
+ return policy.evaluate(offer, {
1977
+ now: options.now,
1978
+ validUntil: offerValidUntil(challenge.extensions),
1979
+ quote: options.quote
1980
+ });
1981
+ }
1982
+
1983
+ // src/client/X402Client.ts
1634
1984
  init_chains();
1635
1985
  init_utils();
1986
+ var KNOWN_SCHEMES = /* @__PURE__ */ new Set([
1987
+ "exact",
1988
+ "upto",
1989
+ "escrow",
1990
+ "commerce",
1991
+ "fhe-transfer"
1992
+ ]);
1993
+ var CLIENT_PAYABLE_SCHEMES = /* @__PURE__ */ new Set(["exact"]);
1636
1994
  var X402Client = class _X402Client {
1637
1995
  // Configuration
1638
1996
  config;
@@ -1651,6 +2009,16 @@ var X402Client = class _X402Client {
1651
2009
  walletAdapter = null;
1652
2010
  // Event emitter
1653
2011
  eventHandlers = /* @__PURE__ */ new Map();
2012
+ /**
2013
+ * What this buyer is allowed to sign, evaluated against the offer in hand
2014
+ * before anything is signed.
2015
+ *
2016
+ * `permissive()` when the caller supplied none, and that asymmetry is
2017
+ * deliberate: this SDK had no budget before 2.89.0, so switching one on
2018
+ * silently would refuse payments consumers are making today, while whoever
2019
+ * sits down to WRITE a policy gets the deny-by-default one.
2020
+ */
2021
+ purchasePolicy;
1654
2022
  constructor(config = {}) {
1655
2023
  this.config = {
1656
2024
  ...DEFAULT_CONFIG,
@@ -1670,8 +2038,18 @@ var X402Client = class _X402Client {
1670
2038
  }
1671
2039
  }
1672
2040
  }
2041
+ this.purchasePolicy = config.policy ?? PurchasePolicy.permissive();
1673
2042
  this.log("X402Client initialized", { config: this.config });
1674
2043
  }
2044
+ /**
2045
+ * The policy in force, for a caller that wants to record a settled payment
2046
+ * against it with {@link PurchasePolicy.recordSpend}.
2047
+ *
2048
+ * Nothing on the payment path ever widens it.
2049
+ */
2050
+ get policy() {
2051
+ return this.purchasePolicy;
2052
+ }
1675
2053
  // ============================================================================
1676
2054
  // PUBLIC API - Wallet Connection
1677
2055
  // ============================================================================
@@ -1892,8 +2270,13 @@ var X402Client = class _X402Client {
1892
2270
  );
1893
2271
  }
1894
2272
  const tokenType = options.tokenType || "usdc";
1895
- const { version, offers } = this.parse402(body, tokenType);
2273
+ const challenge = this.parse402(body, tokenType);
2274
+ const { version, offers } = challenge;
1896
2275
  if (offers.length === 0) {
2276
+ const unreadableCount = challenge.unreadableCount ?? challenge.unreadable.length;
2277
+ if (unreadableCount > 0) {
2278
+ throw new PolicyRefusedError(noReadableOffer(challenge.unreadable));
2279
+ }
1897
2280
  throw new X402Error(
1898
2281
  "402 response offered no usable payment options",
1899
2282
  "NO_ACCEPTABLE_PAYMENT"
@@ -1920,12 +2303,42 @@ var X402Client = class _X402Client {
1920
2303
  );
1921
2304
  }
1922
2305
  }
2306
+ const decision = decideOnChallenge(this.purchasePolicy, challenge, chosen, {
2307
+ now: Math.floor(Date.now() / 1e3),
2308
+ quote: options.advertised
2309
+ });
2310
+ if (!decision.ok) {
2311
+ this.log("policy refused this offer", { cause: decision.refusal.code });
2312
+ this.emit("paymentFailed", {
2313
+ error: decision.refusal.message,
2314
+ code: "POLICY_REFUSED"
2315
+ });
2316
+ throw new PolicyRefusedError(decision.refusal);
2317
+ }
2318
+ const approval = decision.approval;
2319
+ this.log("policy approved this offer", {
2320
+ versusQuote: approval.versusQuote.code,
2321
+ amount: String(approval.amount)
2322
+ });
1923
2323
  if (!chosen.chainName) {
1924
2324
  throw new X402Error(
1925
2325
  `402 asked for payment on unknown network '${chosen.network}'`,
1926
2326
  "CHAIN_NOT_SUPPORTED"
1927
2327
  );
1928
2328
  }
2329
+ const configuredToken = getTokenConfig(chosen.chainName, tokenType);
2330
+ if (chosen.asset && configuredToken && canonicalRecipient(configuredToken.address) !== canonicalRecipient(chosen.asset)) {
2331
+ throw new X402Error(
2332
+ `402 wants ${chosen.asset} on ${chosen.chainName}, but this client signs ${configuredToken.address} (tokenType '${tokenType}'). Pass the tokenType that matches the offer; signing the configured token would pay a different asset than the one that was approved.`,
2333
+ "NO_ACCEPTABLE_PAYMENT",
2334
+ {
2335
+ offered: chosen.asset,
2336
+ wouldSign: configuredToken.address,
2337
+ tokenType,
2338
+ network: chosen.chainName
2339
+ }
2340
+ );
2341
+ }
1929
2342
  if (chosen.chainName !== this.currentChainName) {
1930
2343
  await this.switchChain(chosen.chainName);
1931
2344
  }
@@ -1943,29 +2356,70 @@ var X402Client = class _X402Client {
1943
2356
  if (version === 2) {
1944
2357
  paidHeaders["PAYMENT-SIGNATURE"] = payment.paymentHeader;
1945
2358
  }
1946
- return doFetch(url, { ...options.init, method, headers: paidHeaders });
2359
+ const paid = await doFetch(url, { ...options.init, method, headers: paidHeaders });
2360
+ if (paid.status !== 402) {
2361
+ options.onPaid?.(approval);
2362
+ }
2363
+ return paid;
1947
2364
  }
1948
2365
  /**
1949
- * Normalise a 402 body into its envelope version and its payment offers.
2366
+ * Normalise a 402 body into its envelope version, its payment offers, the ones
2367
+ * it could not read, and the challenge's `extensions`.
1950
2368
  *
1951
2369
  * Reads both spec shapes -- `{x402Version, accepts: [...]}` and the non-spec
1952
2370
  * one where a lone requirement sits at the top level -- and both dialects of
1953
2371
  * the price field (`maxAmountRequired` in v1, `amount` in v2).
2372
+ *
2373
+ * **`accepts` is a list, so one unreadable entry does not sink the list.** A
2374
+ * seller offering `exact` alongside a scheme this build does not implement used
2375
+ * to be unpayable in the Rust crate, and the buyer never learned there was a
2376
+ * perfectly payable offer right there. The readable ones are kept and the
2377
+ * others are counted by scheme name, so a refusal can say what the seller
2378
+ * offered instead of "could not parse". Discovering the service keeps working
2379
+ * even when buying it automatically does not.
2380
+ *
2381
+ * Returns the WHOLE challenge on purpose: handing `offers` alone downstream is
2382
+ * what dropped the seller's `validUntil` on the floor in Rust for a full commit
2383
+ * with every unit test green.
1954
2384
  */
1955
2385
  parse402(body, tokenType) {
1956
2386
  const doc = body ?? {};
1957
2387
  const version = Number(doc.x402Version) === 2 ? 2 : 1;
1958
2388
  const rawAccepts = Array.isArray(doc.accepts) ? doc.accepts : doc.payTo ? [doc] : [];
1959
2389
  const offers = [];
2390
+ const unreadable = [];
2391
+ let unreadableCount = 0;
2392
+ const cannotRead = (accept) => {
2393
+ unreadableCount += 1;
2394
+ const scheme = accept?.scheme;
2395
+ if (typeof scheme === "string" && scheme) unreadable.push(scheme);
2396
+ };
1960
2397
  for (const entry of rawAccepts) {
1961
- if (!entry || typeof entry !== "object") continue;
2398
+ if (!entry || typeof entry !== "object") {
2399
+ cannotRead(null);
2400
+ continue;
2401
+ }
1962
2402
  const accept = entry;
2403
+ const declaredScheme = accept.scheme;
2404
+ if (typeof declaredScheme !== "string" || !declaredScheme || !KNOWN_SCHEMES.has(declaredScheme) || !CLIENT_PAYABLE_SCHEMES.has(declaredScheme)) {
2405
+ cannotRead(accept);
2406
+ continue;
2407
+ }
1963
2408
  const amount = accept.amount ?? accept.maxAmountRequired;
1964
2409
  const payTo = accept.payTo;
1965
2410
  const network = accept.network;
1966
- if (amount === void 0 || amount === null) continue;
1967
- if (typeof payTo !== "string" || !payTo) continue;
1968
- if (typeof network !== "string" || !network) continue;
2411
+ if (amount === void 0 || amount === null) {
2412
+ cannotRead(accept);
2413
+ continue;
2414
+ }
2415
+ if (typeof payTo !== "string" || !payTo) {
2416
+ cannotRead(accept);
2417
+ continue;
2418
+ }
2419
+ if (typeof network !== "string" || !network) {
2420
+ cannotRead(accept);
2421
+ continue;
2422
+ }
1969
2423
  const chainName = getChainByName(network) ? network : caip2ToChain(network);
1970
2424
  offers.push({
1971
2425
  network,
@@ -1977,7 +2431,8 @@ var X402Client = class _X402Client {
1977
2431
  raw: accept
1978
2432
  });
1979
2433
  }
1980
- return { version, offers };
2434
+ const extensions = doc.extensions && typeof doc.extensions === "object" && !Array.isArray(doc.extensions) ? doc.extensions : void 0;
2435
+ return { version, offers, unreadable, unreadableCount, extensions };
1981
2436
  }
1982
2437
  /**
1983
2438
  * Decimals to read an offer's atomic `amount` with.