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.
- package/README.md +172 -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.map +1 -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-BMp3kzLX.d.mts → index-CAJg-_cr.d.mts} +1 -1
- package/dist/{index-Cc5JUhLY.d.ts → index-Cd1Mpznb.d.ts} +1 -1
- 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/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/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-
|
|
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
|
|
package/dist/adapters/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { X as X402Version, a as PaymentResult } from '../index-
|
|
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
|
|