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.
- package/README.md +235 -0
- package/dist/adapters/index.d.mts +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/index.mjs.map +1 -1
- package/dist/backend/index.d.mts +2 -2
- package/dist/backend/index.d.ts +2 -2
- package/dist/backend/index.js +138 -0
- package/dist/backend/index.js.map +1 -1
- package/dist/backend/index.mjs +137 -1
- package/dist/backend/index.mjs.map +1 -1
- package/dist/erc8128/index.js.map +1 -1
- package/dist/erc8128/index.mjs.map +1 -1
- package/dist/{index-D8buGZWY.d.mts → index-13ZUDF7c.d.mts} +32 -2
- package/dist/{index-CWZUj5mz.d.ts → index-B5B9EK3x.d.ts} +32 -2
- package/dist/{index-DOBhTF-j.d.mts → index-BWN0Haa0.d.mts} +476 -2
- package/dist/{index-DOBhTF-j.d.ts → index-BWN0Haa0.d.ts} +476 -2
- package/dist/{index-BHfYxpzI.d.mts → index-CAJg-_cr.d.mts} +181 -2
- package/dist/{index-D67eNxY8.d.ts → index-Cd1Mpznb.d.ts} +181 -2
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +472 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +464 -9
- package/dist/index.mjs.map +1 -1
- package/dist/providers/algorand/index.d.mts +1 -1
- package/dist/providers/algorand/index.d.ts +1 -1
- package/dist/providers/algorand/index.js.map +1 -1
- package/dist/providers/algorand/index.mjs.map +1 -1
- package/dist/providers/evm/index.d.mts +1 -1
- package/dist/providers/evm/index.d.ts +1 -1
- package/dist/providers/evm/index.js.map +1 -1
- package/dist/providers/evm/index.mjs.map +1 -1
- package/dist/providers/near/index.d.mts +1 -1
- package/dist/providers/near/index.d.ts +1 -1
- package/dist/providers/near/index.js.map +1 -1
- package/dist/providers/near/index.mjs.map +1 -1
- package/dist/providers/solana/index.d.mts +1 -1
- package/dist/providers/solana/index.d.ts +1 -1
- package/dist/providers/solana/index.js.map +1 -1
- package/dist/providers/solana/index.mjs.map +1 -1
- package/dist/providers/stellar/index.d.mts +1 -1
- package/dist/providers/stellar/index.d.ts +1 -1
- package/dist/providers/stellar/index.js.map +1 -1
- package/dist/providers/stellar/index.mjs.map +1 -1
- package/dist/providers/sui/index.d.mts +1 -1
- package/dist/providers/sui/index.d.ts +1 -1
- package/dist/providers/sui/index.js.map +1 -1
- package/dist/providers/sui/index.mjs.map +1 -1
- package/dist/providers/xrpl/index.d.mts +1 -1
- package/dist/providers/xrpl/index.d.ts +1 -1
- package/dist/providers/xrpl/index.js.map +1 -1
- package/dist/providers/xrpl/index.mjs.map +1 -1
- package/dist/react/index.d.mts +3 -3
- package/dist/react/index.d.ts +3 -3
- package/dist/react/index.js +463 -8
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +463 -8
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/picker/index.d.mts +1 -1
- package/dist/react/picker/index.d.ts +1 -1
- package/dist/utils/index.d.mts +2 -2
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/index.mjs.map +1 -1
- package/dist/{validation-mSLW4tcO.d.mts → validation-B95g0MqC.d.mts} +1 -1
- package/dist/{validation-C7p3a6Ep.d.ts → validation-_Tcjm2gu.d.ts} +1 -1
- package/package.json +1 -1
- package/src/backend/index.ts +260 -0
- package/src/client/X402Client.ts +232 -9
- package/src/index.ts +28 -0
- package/src/policy.ts +804 -0
- package/src/types/index.ts +41 -0
package/dist/react/index.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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")
|
|
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)
|
|
1967
|
-
|
|
1968
|
-
|
|
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
|
-
|
|
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.
|