uvd-x402-sdk 2.88.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 (70) hide show
  1. package/README.md +172 -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.map +1 -1
  9. package/dist/backend/index.mjs.map +1 -1
  10. package/dist/erc8128/index.js.map +1 -1
  11. package/dist/erc8128/index.mjs.map +1 -1
  12. package/dist/{index-D8buGZWY.d.mts → index-13ZUDF7c.d.mts} +32 -2
  13. package/dist/{index-CWZUj5mz.d.ts → index-B5B9EK3x.d.ts} +32 -2
  14. package/dist/{index-DOBhTF-j.d.mts → index-BWN0Haa0.d.mts} +476 -2
  15. package/dist/{index-DOBhTF-j.d.ts → index-BWN0Haa0.d.ts} +476 -2
  16. package/dist/{index-BMp3kzLX.d.mts → index-CAJg-_cr.d.mts} +1 -1
  17. package/dist/{index-Cc5JUhLY.d.ts → index-Cd1Mpznb.d.ts} +1 -1
  18. package/dist/index.d.mts +4 -4
  19. package/dist/index.d.ts +4 -4
  20. package/dist/index.js +472 -8
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +464 -9
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/providers/algorand/index.d.mts +1 -1
  25. package/dist/providers/algorand/index.d.ts +1 -1
  26. package/dist/providers/algorand/index.js.map +1 -1
  27. package/dist/providers/algorand/index.mjs.map +1 -1
  28. package/dist/providers/evm/index.d.mts +1 -1
  29. package/dist/providers/evm/index.d.ts +1 -1
  30. package/dist/providers/evm/index.js.map +1 -1
  31. package/dist/providers/evm/index.mjs.map +1 -1
  32. package/dist/providers/near/index.d.mts +1 -1
  33. package/dist/providers/near/index.d.ts +1 -1
  34. package/dist/providers/near/index.js.map +1 -1
  35. package/dist/providers/near/index.mjs.map +1 -1
  36. package/dist/providers/solana/index.d.mts +1 -1
  37. package/dist/providers/solana/index.d.ts +1 -1
  38. package/dist/providers/solana/index.js.map +1 -1
  39. package/dist/providers/solana/index.mjs.map +1 -1
  40. package/dist/providers/stellar/index.d.mts +1 -1
  41. package/dist/providers/stellar/index.d.ts +1 -1
  42. package/dist/providers/stellar/index.js.map +1 -1
  43. package/dist/providers/stellar/index.mjs.map +1 -1
  44. package/dist/providers/sui/index.d.mts +1 -1
  45. package/dist/providers/sui/index.d.ts +1 -1
  46. package/dist/providers/sui/index.js.map +1 -1
  47. package/dist/providers/sui/index.mjs.map +1 -1
  48. package/dist/providers/xrpl/index.d.mts +1 -1
  49. package/dist/providers/xrpl/index.d.ts +1 -1
  50. package/dist/providers/xrpl/index.js.map +1 -1
  51. package/dist/providers/xrpl/index.mjs.map +1 -1
  52. package/dist/react/index.d.mts +3 -3
  53. package/dist/react/index.d.ts +3 -3
  54. package/dist/react/index.js +463 -8
  55. package/dist/react/index.js.map +1 -1
  56. package/dist/react/index.mjs +463 -8
  57. package/dist/react/index.mjs.map +1 -1
  58. package/dist/react/picker/index.d.mts +1 -1
  59. package/dist/react/picker/index.d.ts +1 -1
  60. package/dist/utils/index.d.mts +2 -2
  61. package/dist/utils/index.d.ts +2 -2
  62. package/dist/utils/index.js.map +1 -1
  63. package/dist/utils/index.mjs.map +1 -1
  64. package/dist/{validation-mSLW4tcO.d.mts → validation-B95g0MqC.d.mts} +1 -1
  65. package/dist/{validation-C7p3a6Ep.d.ts → validation-_Tcjm2gu.d.ts} +1 -1
  66. package/package.json +1 -1
  67. package/src/client/X402Client.ts +232 -9
  68. package/src/index.ts +28 -0
  69. package/src/policy.ts +804 -0
  70. package/src/types/index.ts +41 -0
package/README.md CHANGED
@@ -10,6 +10,7 @@ Users sign a message or transaction, and the Ultravioleta facilitator handles on
10
10
  - **Multi-Stablecoin**: USDC, EURC, AUSD, PYUSD, USDT, USDG (Robinhood Chain)
11
11
  - **x402 v1 & v2**: Both protocol versions with auto-detection
12
12
  - **Gasless**: Facilitator pays all network fees
13
+ - **Buyer Policy**: Per-payment and cumulative budgets, payee allowlist and offer expiry, evaluated against the offer in hand **before signing** — six closed refusal codes in a fixed order
13
14
  - **Type-Safe**: Full TypeScript support
14
15
  - **React & Wagmi**: First-class integrations
15
16
  - **Signing Wallet Adapters**: EnvKeyAdapter (server/CLI), OWSWalletAdapter (Open Wallet Standard), or bring your own
@@ -869,6 +870,177 @@ try {
869
870
  }
870
871
  ```
871
872
 
873
+ ## Buyer policy — what this buyer is allowed to sign, decided before it signs
874
+
875
+ A catalog listing is a claim somebody else made about their own price. The `402`
876
+ that comes back from the actual request is the offer, and they can differ
877
+ legitimately: a seller may have repriced, and the listing may be a copy of a
878
+ copy. So the buying decision is made against **the offer in hand**, every time,
879
+ before anything is signed. Same contract the facilitator fixed in Rust
880
+ (`x402-reqwest`, release 2.25.0), so a buyer in either language refuses the same
881
+ payments for the same stated reasons.
882
+
883
+ ```ts
884
+ import { X402Client, PurchasePolicy, PolicyRefusedError } from 'uvd-x402-sdk';
885
+
886
+ const USDC_BASE = {
887
+ network: 'base',
888
+ address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
889
+ };
890
+
891
+ // create() DENIES any asset it was not given a ceiling for.
892
+ const policy = PurchasePolicy.create()
893
+ .perPayment(USDC_BASE, 50_000n) // 0.05 USDC, atomic units, bigint
894
+ .cumulative(USDC_BASE, 1_000_000n) // 1 USDC total, for this policy's life
895
+ .onlyPay(['0xe4dc963c56979E0260fc146b87eE24F18220e545']);
896
+
897
+ const client = new X402Client({ defaultChain: 'base', policy });
898
+ await client.connectWithPrivateKey(process.env.PRIVATE_KEY!, 'base');
899
+
900
+ try {
901
+ const res = await client.fetch('https://api.example.com/data', {
902
+ // What the catalog advertised, if you read one. It is REPORTED, never a gate.
903
+ advertised: { asset: USDC_BASE, amount: 10_000n },
904
+ // Evaluating does not spend. This is where a settled payment gets recorded.
905
+ onPaid: (approval) => client.policy.recordSpend(approval.asset, approval.amount),
906
+ });
907
+ const data = await res.json();
908
+ } catch (err) {
909
+ if (err instanceof PolicyRefusedError) {
910
+ switch (err.refusal.code) {
911
+ case 'offer-expired': // ask the seller for new terms
912
+ case 'asset-not-budgeted': // budget that asset
913
+ case 'per-payment-limit': // one payment is too big
914
+ case 'cumulative-limit': // the budget is spent
915
+ case 'recipient-not-permitted': // not a payee you allowed
916
+ case 'no-readable-offer': // err.refusal.offered names the schemes
917
+ }
918
+ }
919
+ }
920
+ ```
921
+
922
+ **Nothing changes for a caller who never writes a policy.** A client without one
923
+ holds `PurchasePolicy.permissive()`: this SDK had no budget before 2.89.0 and
924
+ switching one on silently would refuse payments consumers are making today. The
925
+ asymmetry is deliberate — whoever sits down to **write** a policy gets the
926
+ deny-by-default one. `maxAmount` is untouched and still runs before the policy.
927
+
928
+ ### The fields
929
+
930
+ | field | type | meaning |
931
+ |---|---|---|
932
+ | `perPayment(asset, amount)` | `bigint`, atomic units | most this policy pays in ONE payment of that asset |
933
+ | `cumulative(asset, amount)` | `bigint`, atomic units | most it pays in that asset in TOTAL, for as long as it lives |
934
+ | `spent(asset)` | `bigint` | what has been recorded; **only `recordSpend` moves it** |
935
+ | `onlyPay([...])` | addresses | permitted recipients, canonicalised **by family** |
936
+ | `allowUnlistedAssets()` | boolean, **false by default** | whether an asset with no declared ceiling may be paid |
937
+
938
+ An `asset` is a `{ network, address }` pair, and both halves matter: the same
939
+ contract address on two networks is two different assets. Use the SDK chain name
940
+ (`'base'`), not the CAIP-2 string — the client resolves a v2 challenge's
941
+ `eip155:8453` to it, so one written policy covers both 402 dialects.
942
+
943
+ ### The order of evaluation is part of the contract
944
+
945
+ The FIRST failing check is the one reported, because a caller branches on it.
946
+ Reporting `per-payment-limit` for an expired offer to a payee nobody allowed
947
+ would tell the caller to raise a ceiling when the real fix is to ask the seller
948
+ for new terms.
949
+
950
+ ```
951
+ no-readable-offer → offer-expired → recipient-not-permitted
952
+ → asset-not-budgeted → per-payment-limit → cumulative-limit
953
+ ```
954
+
955
+ The six are a closed kebab-case vocabulary (`PolicyRefusalCode`), typed as a
956
+ literal union so you branch without parsing English. There is no `other`: a
957
+ refusal a caller cannot interpret is one it will paper over. Each carries the
958
+ numbers that caused it — `requested`, `allowed`, `spent`, `wouldTotal`, `asset`,
959
+ `payTo`, `validUntil`, `now`, `offered[]`.
960
+
961
+ `asset-not-budgeted` runs **before** the ceilings on purpose. The ceilings are a
962
+ map, and a map has no opinion about a key it does not hold — which is exactly how
963
+ an unlisted token sails past a budget that looks complete. And the EVM signer
964
+ would have signed it: it takes its EIP-712 domain from the seller's own `extra`,
965
+ for a token and a network it has never seen.
966
+
967
+ ### The seven rules
968
+
969
+ 1. **Evaluating does not spend.** Signing can fail and a settlement can be
970
+ refused; a limit that counted attempts would lock you out of money you never
971
+ spent. `recordSpend` is a separate call, after the settlement resolved — use
972
+ `onPaid` for it.
973
+ 2. **A policy is never widened from inside an evaluation.** No method raises a
974
+ ceiling: every builder returns a NEW policy and leaves the receiver exactly as
975
+ strict as it was.
976
+ 3. **No human confirmation when the policy already covers the operation.** A
977
+ divergence from the listing is not, by itself, a refusal: an offer that costs
978
+ more than the catalog said but sits inside an authorised policy is paid.
979
+ Halting there would turn every ordinary reprice into a stop, and an agent that
980
+ halts on ordinary commerce is one nobody can leave running. There is no
981
+ confirmation hook on this path.
982
+ 4. **A different asset is not the same price.** No numbers are compared across
983
+ assets. An asset with no declared ceiling is **denied by default**; the
984
+ permissive mode has to be asked for by name.
985
+ 5. **A network name's case never decides anything** (`'Base'` and `'base'` are one
986
+ network written twice), and **addresses are canonicalised by family, never with
987
+ `toLowerCase()`.** Hex is
988
+ folded; base58 (Solana, XRPL) is compared exactly. Folding a base58 address
989
+ does not produce the same address spelled differently — it produces a string
990
+ that is not an address, so an allowlist written in the seller's own spelling
991
+ would never match. And in the dangerous direction, two distinct base58
992
+ addresses can fold to the same lowercase string, letting in one nobody listed.
993
+ 6. **`validUntil` is read from `extensions["offer-receipt/1"].info.validUntil`**,
994
+ in Unix seconds. Absent means no declared expiry. Unreadable means absent,
995
+ **never zero**: "the seller said something we could not read" must not become
996
+ "this offer expired in 1970".
997
+ 7. **`validUntil === now` still stands** — it is the last instant the offer is up.
998
+ And an `accepts` with one unreadable entry **keeps** the readable ones and
999
+ counts the others by scheme name, so a refusal says what the seller offered:
1000
+ `offered: ["batch-settlement","agent-pay"]`. Discovering a service keeps
1001
+ working even when buying it automatically does not.
1002
+
1003
+ Two more worth knowing: **a copy of a policy spends from the same purse** (a
1004
+ client is copied per request, and a per-copy total would make a cumulative limit
1005
+ meaningless), and **a corrupt purse reports the ceiling, never zero** — for money
1006
+ the safe direction is to refuse.
1007
+
1008
+ ### The scheme decides, and so does the asset
1009
+
1010
+ Two checks sit beside the policy on the buyer path, because approving a payment you
1011
+ cannot honestly present is not an approval:
1012
+
1013
+ **Schemes.** `KNOWN_SCHEMES` is the vocabulary shared with the Rust facilitator's
1014
+ closed `Scheme` enum and the Python SDK: `exact`, `upto`, `escrow`, `commerce`,
1015
+ `fhe-transfer`. `CLIENT_PAYABLE_SCHEMES` is the subset this buyer path can sign —
1016
+ `exact` alone, because the payload builder stamps `scheme: 'exact'` into everything
1017
+ it produces. **Recognising a scheme is not being able to pay it:** a well-formed
1018
+ `escrow` offer read as payable would be signed as `exact`, offering the seller a
1019
+ payment under a scheme it never asked for. Either way the entry is excluded and
1020
+ counted by its scheme name, which is what lets a refusal say where to go look.
1021
+
1022
+ **A missing `scheme` is unreadable, not `exact`.** Rust requires the field, and a
1023
+ buyer that guessed would sign under a scheme the seller never named. This is
1024
+ deliberately asymmetric with the **seller** side of this SDK, where a missing scheme
1025
+ reads as `exact`: a seller is lenient about what it accepts, a buyer is strict about
1026
+ what it signs.
1027
+
1028
+ **Assets.** The policy judges the offer's own `asset`, but the signature is built for
1029
+ whatever `tokenType` resolves to on that chain, at that token's decimals. With USDC on
1030
+ both sides they coincide. They do not have to — so an offer naming a different token
1031
+ is refused rather than silently re-pointed, because which token to pay with is your
1032
+ decision and guessing it from the seller's 402 is how a wallet signs for a token
1033
+ nobody chose. Pass the `tokenType` that matches the offer.
1034
+
1035
+ ### Deciding without a network stack
1036
+
1037
+ `decideOnChallenge(policy, challenge, offer, { now })` runs all six steps and is
1038
+ callable directly, which is the point: a decision that can only be exercised by
1039
+ driving a real HTTP client is a decision nobody tests. It takes the challenge
1040
+ WHOLE — passing the offers alone is exactly what dropped a seller's `validUntil`
1041
+ on the floor in Rust for a full commit with every unit test green. `now` is
1042
+ passed rather than read, so a money decision can be pinned to an exact instant.
1043
+
872
1044
  ## `503` is not `402` — read the refusal before you re-sign
873
1045
 
874
1046
  `402` and `503` say opposite things, and the difference is the buyer's money:
@@ -1,4 +1,4 @@
1
- import { X as X402Version, a as PaymentResult } from '../index-DOBhTF-j.mjs';
1
+ import { X as X402Version, a as PaymentResult } from '../index-BWN0Haa0.mjs';
2
2
  export { E as EnvKeyAdapter, O as OWSWallet, a as OWSWalletAdapter } from '../ows-Z9v4GxOZ.mjs';
3
3
  import '../wallet-w7BnImDG.mjs';
4
4
 
@@ -1,4 +1,4 @@
1
- import { X as X402Version, a as PaymentResult } from '../index-DOBhTF-j.js';
1
+ import { X as X402Version, a as PaymentResult } from '../index-BWN0Haa0.js';
2
2
  export { E as EnvKeyAdapter, O as OWSWallet, a as OWSWalletAdapter } from '../ows-C-KmORG9.js';
3
3
  import '../wallet-w7BnImDG.js';
4
4