uvd-x402-sdk 2.62.0 → 2.64.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 +89 -0
- package/dist/backend/index.d.mts +4 -1
- package/dist/backend/index.d.ts +4 -1
- package/dist/backend/index.js +29 -21
- package/dist/backend/index.js.map +1 -1
- package/dist/backend/index.mjs +29 -21
- package/dist/backend/index.mjs.map +1 -1
- package/dist/index.d.mts +43 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +15 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/backend/index.ts +18 -2
- package/src/dx402.ts +59 -0
- package/src/escrow-preauth.ts +2 -2
- package/src/index.ts +2 -0
package/README.md
CHANGED
|
@@ -1280,3 +1280,92 @@ const isHealthy = await client.healthCheck();
|
|
|
1280
1280
|
## License
|
|
1281
1281
|
|
|
1282
1282
|
MIT
|
|
1283
|
+
|
|
1284
|
+
## DX402 — evidence that outlives the session
|
|
1285
|
+
|
|
1286
|
+
x402 settles payment on-chain forever but delivers the resource **once** and
|
|
1287
|
+
keeps nothing. DX402 seals a copy of the response to the payer's own public key
|
|
1288
|
+
— recovered from the payment signature itself — and anchors it. No registration,
|
|
1289
|
+
no extra round trip: paying *is* publishing your encryption key.
|
|
1290
|
+
|
|
1291
|
+
### Seller: one call
|
|
1292
|
+
|
|
1293
|
+
```ts
|
|
1294
|
+
import { anchorEvidence, evidenceHeader } from 'uvd-x402-sdk';
|
|
1295
|
+
|
|
1296
|
+
const result = await anchorEvidence(body, {
|
|
1297
|
+
paymentId, network: 'base', txHash,
|
|
1298
|
+
payer: payerAddr, payee: myAddr, payerKey: payerPubkey,
|
|
1299
|
+
sign: (digest) => myCustodian.sign(digest), // a callable, not a key
|
|
1300
|
+
});
|
|
1301
|
+
res.setHeader('X-Durable-Evidence', evidenceHeader(result));
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
**It never throws.** Every failure resolves to `result.skipped`, because
|
|
1305
|
+
evidence is an addition to the payment path and must never be a gate in front of
|
|
1306
|
+
it. An unreachable facilitator costs the receipt, never the sale.
|
|
1307
|
+
|
|
1308
|
+
`sign` takes a **callable rather than a private key** so a custodian can sign:
|
|
1309
|
+
it receives the 32-byte digest and returns a signature without the seed ever
|
|
1310
|
+
leaving it.
|
|
1311
|
+
|
|
1312
|
+
### Buyer: come back months later
|
|
1313
|
+
|
|
1314
|
+
```ts
|
|
1315
|
+
import { recoverEvidence, evidenceFromHeaders } from 'uvd-x402-sdk';
|
|
1316
|
+
|
|
1317
|
+
const evidence = evidenceFromHeaders(res.headers);
|
|
1318
|
+
const body = await recoverEvidence(evidence, myPrivateKey);
|
|
1319
|
+
```
|
|
1320
|
+
|
|
1321
|
+
This needs permission from nobody. The ciphertext was sealed to the wallet that
|
|
1322
|
+
paid, so recovery is arithmetic rather than an access-control decision anyone
|
|
1323
|
+
could refuse. The `contentHash` check runs automatically and throws
|
|
1324
|
+
`ContentHashMismatch` — it is what catches a seller who anchored something other
|
|
1325
|
+
than what it served.
|
|
1326
|
+
|
|
1327
|
+
### `verified` vs `signed` — read this before you branch on either
|
|
1328
|
+
|
|
1329
|
+
Since facilitator **1.87.0** a signature alone does not make an anchor final:
|
|
1330
|
+
|
|
1331
|
+
| field | means | supersedable by |
|
|
1332
|
+
|---|---|---|
|
|
1333
|
+
| `verified: true` | the **chain** confirmed this address is the payee | nothing — final |
|
|
1334
|
+
| `signed: true` | the claimant controls the address it *declared* | a verified anchor |
|
|
1335
|
+
| neither | anyone could have written it | either of the above |
|
|
1336
|
+
|
|
1337
|
+
To reach `verified` you must send `proofOfPayment`. Without it the facilitator
|
|
1338
|
+
has checked no chain and answers `notVerifiedReason: "dx402_proof_missing"` —
|
|
1339
|
+
your signature was still accepted (`signed: true`), authorship simply was not
|
|
1340
|
+
certified.
|
|
1341
|
+
|
|
1342
|
+
Why the split: `verified` was previously decided against the `payee` field *in
|
|
1343
|
+
the request*, which the caller supplies. Proving "I control the address I typed
|
|
1344
|
+
into my own request" was enough to own a stranger's evidence permanently.
|
|
1345
|
+
|
|
1346
|
+
### Choosing where evidence is stored
|
|
1347
|
+
|
|
1348
|
+
```ts
|
|
1349
|
+
import { availableBackends } from 'uvd-x402-sdk';
|
|
1350
|
+
|
|
1351
|
+
for (const b of await availableBackends()) {
|
|
1352
|
+
console.log(b.id, b.retention, b.revocable ? 'deletable' : 'IRREVERSIBLE');
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
await anchorEvidence(body, { ...opts, storage: 'ipfs-private' });
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
Ask rather than assume: what exists depends on the deployment, and you may be
|
|
1359
|
+
pointed at a facilitator that is not ours. `revocable: false` means the
|
|
1360
|
+
`retentionUntil` in the **signed** receipt cannot be honoured — on public IPFS,
|
|
1361
|
+
unpinning removes the facilitator's copy, not the network's.
|
|
1362
|
+
|
|
1363
|
+
### Limits
|
|
1364
|
+
|
|
1365
|
+
- Inline anchors cap at **64 KiB of request** (~47 KB of plaintext); the SDK
|
|
1366
|
+
returns `skipped: 'too_large'` before touching the network.
|
|
1367
|
+
- Anchoring with `retention: 'permanent'` is **irrevocable**.
|
|
1368
|
+
- On Solana, `verified` is not reachable yet — the on-chain gate cannot read
|
|
1369
|
+
that payment, so `signed: true` is the honest maximum.
|
|
1370
|
+
|
|
1371
|
+
Full guide: [DX402.md](https://github.com/UltravioletaDAO/x402-rs/blob/main/docs/DX402.md)
|
package/dist/backend/index.d.mts
CHANGED
|
@@ -2342,7 +2342,10 @@ declare class AdvancedEscrowClient {
|
|
|
2342
2342
|
* @param tier - Task tier determines timing parameters
|
|
2343
2343
|
* @param salt - Random salt (auto-generated if not provided)
|
|
2344
2344
|
*/
|
|
2345
|
-
buildPaymentInfo(receiver: string, amount: string, tier?: AdvancedEscrowTaskTier, salt?: string
|
|
2345
|
+
buildPaymentInfo(receiver: string, amount: string, tier?: AdvancedEscrowTaskTier, salt?: string, opts?: {
|
|
2346
|
+
deadline?: number;
|
|
2347
|
+
reviewWindowSec?: number;
|
|
2348
|
+
}): AdvancedPaymentInfo;
|
|
2346
2349
|
/**
|
|
2347
2350
|
* Compute the correct nonce (with PAYMENT_INFO_TYPEHASH).
|
|
2348
2351
|
* Matches the on-chain AuthCaptureEscrow nonce derivation.
|
package/dist/backend/index.d.ts
CHANGED
|
@@ -2342,7 +2342,10 @@ declare class AdvancedEscrowClient {
|
|
|
2342
2342
|
* @param tier - Task tier determines timing parameters
|
|
2343
2343
|
* @param salt - Random salt (auto-generated if not provided)
|
|
2344
2344
|
*/
|
|
2345
|
-
buildPaymentInfo(receiver: string, amount: string, tier?: AdvancedEscrowTaskTier, salt?: string
|
|
2345
|
+
buildPaymentInfo(receiver: string, amount: string, tier?: AdvancedEscrowTaskTier, salt?: string, opts?: {
|
|
2346
|
+
deadline?: number;
|
|
2347
|
+
reviewWindowSec?: number;
|
|
2348
|
+
}): AdvancedPaymentInfo;
|
|
2346
2349
|
/**
|
|
2347
2350
|
* Compute the correct nonce (with PAYMENT_INFO_TYPEHASH).
|
|
2348
2351
|
* Matches the on-chain AuthCaptureEscrow nonce derivation.
|
package/dist/backend/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
require('ethers');
|
|
4
|
+
|
|
3
5
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
4
6
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
5
7
|
}) : x)(function(x) {
|
|
@@ -1088,6 +1090,8 @@ function decodeX402Header(encoded) {
|
|
|
1088
1090
|
const json = decodeBase64Utf8(encoded);
|
|
1089
1091
|
return JSON.parse(json);
|
|
1090
1092
|
}
|
|
1093
|
+
var REVIEW_WINDOW_SEC = 7 * 24 * 3600;
|
|
1094
|
+
var REFUND_WINDOW_SEC = 7 * 24 * 3600;
|
|
1091
1095
|
|
|
1092
1096
|
// src/backend/index.ts
|
|
1093
1097
|
function parsePaymentHeader(headerValue) {
|
|
@@ -3504,9 +3508,13 @@ var AdvancedEscrowClient = class {
|
|
|
3504
3508
|
* @param tier - Task tier determines timing parameters
|
|
3505
3509
|
* @param salt - Random salt (auto-generated if not provided)
|
|
3506
3510
|
*/
|
|
3507
|
-
buildPaymentInfo(receiver, amount, tier = "standard", salt) {
|
|
3511
|
+
buildPaymentInfo(receiver, amount, tier = "standard", salt, opts) {
|
|
3508
3512
|
const now = Math.floor(Date.now() / 1e3);
|
|
3509
3513
|
const t = TIER_TIMINGS[tier];
|
|
3514
|
+
const reviewWindow = opts?.reviewWindowSec ?? REVIEW_WINDOW_SEC;
|
|
3515
|
+
const reviewBase = Math.max(now, opts?.deadline ?? now);
|
|
3516
|
+
const authorizationExpiry = Math.max(now + t.auth, reviewBase + reviewWindow);
|
|
3517
|
+
const refundExpiry = Math.max(now + t.refund, authorizationExpiry + REFUND_WINDOW_SEC);
|
|
3510
3518
|
let generatedSalt = salt;
|
|
3511
3519
|
if (!generatedSalt) {
|
|
3512
3520
|
const bytes = new Uint8Array(32);
|
|
@@ -3525,8 +3533,8 @@ var AdvancedEscrowClient = class {
|
|
|
3525
3533
|
token: this.contracts.usdc,
|
|
3526
3534
|
maxAmount: amount,
|
|
3527
3535
|
preApprovalExpiry: now + t.pre,
|
|
3528
|
-
authorizationExpiry
|
|
3529
|
-
refundExpiry
|
|
3536
|
+
authorizationExpiry,
|
|
3537
|
+
refundExpiry,
|
|
3530
3538
|
minFeeBps: 0,
|
|
3531
3539
|
maxFeeBps: 800,
|
|
3532
3540
|
feeReceiver: this.contracts.operator,
|
|
@@ -3538,8 +3546,8 @@ var AdvancedEscrowClient = class {
|
|
|
3538
3546
|
* Matches the on-chain AuthCaptureEscrow nonce derivation.
|
|
3539
3547
|
*/
|
|
3540
3548
|
async computeNonce(paymentInfo) {
|
|
3541
|
-
const { ethers } = await import('ethers');
|
|
3542
|
-
const piTuple =
|
|
3549
|
+
const { ethers: ethers2 } = await import('ethers');
|
|
3550
|
+
const piTuple = ethers2.AbiCoder.defaultAbiCoder().encode(
|
|
3543
3551
|
["bytes32", "tuple(address,address,address,address,uint120,uint48,uint48,uint48,uint16,uint16,address,uint256)"],
|
|
3544
3552
|
[
|
|
3545
3553
|
PAYMENT_INFO_TYPEHASH,
|
|
@@ -3560,12 +3568,12 @@ var AdvancedEscrowClient = class {
|
|
|
3560
3568
|
]
|
|
3561
3569
|
]
|
|
3562
3570
|
);
|
|
3563
|
-
const piHash =
|
|
3564
|
-
const finalEncoded =
|
|
3571
|
+
const piHash = ethers2.keccak256(piTuple);
|
|
3572
|
+
const finalEncoded = ethers2.AbiCoder.defaultAbiCoder().encode(
|
|
3565
3573
|
["uint256", "address", "bytes32"],
|
|
3566
3574
|
[this.chainId, this.contracts.escrow, piHash]
|
|
3567
3575
|
);
|
|
3568
|
-
return
|
|
3576
|
+
return ethers2.keccak256(finalEncoded);
|
|
3569
3577
|
}
|
|
3570
3578
|
/**
|
|
3571
3579
|
* Sign ReceiveWithAuthorization for ERC-3009.
|
|
@@ -3722,17 +3730,17 @@ var AdvancedEscrowClient = class {
|
|
|
3722
3730
|
async release(paymentInfo, amount) {
|
|
3723
3731
|
if (!this.payerAddress) await this.init();
|
|
3724
3732
|
try {
|
|
3725
|
-
const { ethers } = await import('ethers');
|
|
3733
|
+
const { ethers: ethers2 } = await import('ethers');
|
|
3726
3734
|
const isCreate3 = CREATE3_CHAIN_IDS.has(this.chainId);
|
|
3727
3735
|
const abi = isCreate3 ? OPERATOR_ABI_CREATE3 : OPERATOR_ABI;
|
|
3728
3736
|
const amt = amount || paymentInfo.maxAmount;
|
|
3729
3737
|
const tuple = this.buildTuple(paymentInfo);
|
|
3730
3738
|
if (this.walletAdapter) {
|
|
3731
|
-
return this.sendViaAdapter(
|
|
3739
|
+
return this.sendViaAdapter(ethers2, abi, (iface) => {
|
|
3732
3740
|
return isCreate3 ? iface.encodeFunctionData("release", [tuple, amt, "0x"]) : iface.encodeFunctionData("release", [tuple, amt]);
|
|
3733
3741
|
});
|
|
3734
3742
|
}
|
|
3735
|
-
const contract = new
|
|
3743
|
+
const contract = new ethers2.Contract(this.contracts.operator, abi, this.signer);
|
|
3736
3744
|
const tx = isCreate3 ? await contract.release(tuple, amt, "0x", { gasLimit: this.gasLimit }) : await contract.release(tuple, amt, { gasLimit: this.gasLimit });
|
|
3737
3745
|
const receipt = await tx.wait();
|
|
3738
3746
|
return {
|
|
@@ -3756,17 +3764,17 @@ var AdvancedEscrowClient = class {
|
|
|
3756
3764
|
async refundInEscrow(paymentInfo, amount) {
|
|
3757
3765
|
if (!this.payerAddress) await this.init();
|
|
3758
3766
|
try {
|
|
3759
|
-
const { ethers } = await import('ethers');
|
|
3767
|
+
const { ethers: ethers2 } = await import('ethers');
|
|
3760
3768
|
const isCreate3 = CREATE3_CHAIN_IDS.has(this.chainId);
|
|
3761
3769
|
const abi = isCreate3 ? OPERATOR_ABI_CREATE3 : OPERATOR_ABI;
|
|
3762
3770
|
const amt = amount || paymentInfo.maxAmount;
|
|
3763
3771
|
const tuple = this.buildTuple(paymentInfo);
|
|
3764
3772
|
if (this.walletAdapter) {
|
|
3765
|
-
return this.sendViaAdapter(
|
|
3773
|
+
return this.sendViaAdapter(ethers2, abi, (iface) => {
|
|
3766
3774
|
return isCreate3 ? iface.encodeFunctionData("refundInEscrow", [tuple, amt, "0x"]) : iface.encodeFunctionData("refundInEscrow", [tuple, amt]);
|
|
3767
3775
|
});
|
|
3768
3776
|
}
|
|
3769
|
-
const contract = new
|
|
3777
|
+
const contract = new ethers2.Contract(this.contracts.operator, abi, this.signer);
|
|
3770
3778
|
const tx = isCreate3 ? await contract.refundInEscrow(tuple, amt, "0x", { gasLimit: this.gasLimit }) : await contract.refundInEscrow(tuple, amt, { gasLimit: this.gasLimit });
|
|
3771
3779
|
const receipt = await tx.wait();
|
|
3772
3780
|
return {
|
|
@@ -4029,7 +4037,7 @@ var AdvancedEscrowClient = class {
|
|
|
4029
4037
|
async charge(paymentInfo, amount) {
|
|
4030
4038
|
if (!this.payerAddress) await this.init();
|
|
4031
4039
|
try {
|
|
4032
|
-
const { ethers } = await import('ethers');
|
|
4040
|
+
const { ethers: ethers2 } = await import('ethers');
|
|
4033
4041
|
const nonce = await this.computeNonce(paymentInfo);
|
|
4034
4042
|
const amt = amount || paymentInfo.maxAmount;
|
|
4035
4043
|
const auth = {
|
|
@@ -4041,10 +4049,10 @@ var AdvancedEscrowClient = class {
|
|
|
4041
4049
|
nonce
|
|
4042
4050
|
};
|
|
4043
4051
|
const signature = await this.signErc3009(auth);
|
|
4044
|
-
const collectorData =
|
|
4052
|
+
const collectorData = ethers2.getBytes(signature);
|
|
4045
4053
|
const tuple = this.buildTuple(paymentInfo);
|
|
4046
4054
|
if (this.walletAdapter) {
|
|
4047
|
-
return this.sendViaAdapter(
|
|
4055
|
+
return this.sendViaAdapter(ethers2, OPERATOR_ABI, (iface) => {
|
|
4048
4056
|
return iface.encodeFunctionData("charge", [
|
|
4049
4057
|
tuple,
|
|
4050
4058
|
amt,
|
|
@@ -4053,7 +4061,7 @@ var AdvancedEscrowClient = class {
|
|
|
4053
4061
|
]);
|
|
4054
4062
|
});
|
|
4055
4063
|
}
|
|
4056
|
-
const contract = new
|
|
4064
|
+
const contract = new ethers2.Contract(this.contracts.operator, OPERATOR_ABI, this.signer);
|
|
4057
4065
|
const tx = await contract.charge(
|
|
4058
4066
|
tuple,
|
|
4059
4067
|
amt,
|
|
@@ -4095,11 +4103,11 @@ var AdvancedEscrowClient = class {
|
|
|
4095
4103
|
async refundPostEscrow(paymentInfo, amount, tokenCollector, collectorData) {
|
|
4096
4104
|
if (!this.payerAddress) await this.init();
|
|
4097
4105
|
try {
|
|
4098
|
-
const { ethers } = await import('ethers');
|
|
4106
|
+
const { ethers: ethers2 } = await import('ethers');
|
|
4099
4107
|
const amt = amount || paymentInfo.maxAmount;
|
|
4100
4108
|
const tuple = this.buildTuple(paymentInfo);
|
|
4101
4109
|
if (this.walletAdapter) {
|
|
4102
|
-
return this.sendViaAdapter(
|
|
4110
|
+
return this.sendViaAdapter(ethers2, OPERATOR_ABI, (iface) => {
|
|
4103
4111
|
return iface.encodeFunctionData("refundPostEscrow", [
|
|
4104
4112
|
tuple,
|
|
4105
4113
|
amt,
|
|
@@ -4108,7 +4116,7 @@ var AdvancedEscrowClient = class {
|
|
|
4108
4116
|
]);
|
|
4109
4117
|
});
|
|
4110
4118
|
}
|
|
4111
|
-
const contract = new
|
|
4119
|
+
const contract = new ethers2.Contract(this.contracts.operator, OPERATOR_ABI, this.signer);
|
|
4112
4120
|
const tx = await contract.refundPostEscrow(
|
|
4113
4121
|
tuple,
|
|
4114
4122
|
amt,
|