uvd-x402-sdk 2.75.0 → 2.76.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 +145 -1
- package/dist/backend/index.d.mts +348 -52
- package/dist/backend/index.d.ts +348 -52
- package/dist/backend/index.js +470 -235
- package/dist/backend/index.js.map +1 -1
- package/dist/backend/index.mjs +459 -236
- package/dist/backend/index.mjs.map +1 -1
- package/dist/index.d.mts +44 -4
- package/dist/index.d.ts +44 -4
- package/dist/index.js +238 -39
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +228 -40
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/backend/facilitator-error.ts +355 -0
- package/src/backend/index.ts +536 -263
- package/src/dx402.ts +123 -8
- package/src/index.ts +17 -0
package/README.md
CHANGED
|
@@ -869,6 +869,78 @@ try {
|
|
|
869
869
|
}
|
|
870
870
|
```
|
|
871
871
|
|
|
872
|
+
## `503` is not `402` — read the refusal before you re-sign
|
|
873
|
+
|
|
874
|
+
`402` and `503` say opposite things, and the difference is the buyer's money:
|
|
875
|
+
|
|
876
|
+
| status | meaning | what the caller must do |
|
|
877
|
+
|---|---|---|
|
|
878
|
+
| **402** | the payment was **rejected** | sign a **new** authorization |
|
|
879
|
+
| **503** | **no verdict was reached** | resend the **same** credential |
|
|
880
|
+
|
|
881
|
+
Charging a `503` as a `402` makes the buyer sign and broadcast a second payment
|
|
882
|
+
for money that was never refused — and the first authorization is still
|
|
883
|
+
perfectly spendable. Every facilitator edge in this SDK therefore reports the
|
|
884
|
+
refusal as data:
|
|
885
|
+
|
|
886
|
+
```typescript
|
|
887
|
+
const result = await client.settle(payment, requirements);
|
|
888
|
+
|
|
889
|
+
if (!result.success) {
|
|
890
|
+
if (result.retryable) {
|
|
891
|
+
// Nothing was rejected. Do NOT ask for another signature.
|
|
892
|
+
// result.status -> 503
|
|
893
|
+
// result.reason -> 'holder_unknown' | 'forward_failed' | ...
|
|
894
|
+
// result.retryAfterSeconds -> already clamped, never an hour
|
|
895
|
+
// result.safeToReplay -> true only if the facilitator proved nothing ran
|
|
896
|
+
} else {
|
|
897
|
+
// A real refusal. result.errorReason says why.
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
The same fields appear on `verify()`, `verifyAndSettle()`, every `Erc8004Client`
|
|
903
|
+
write, and the gasless escrow calls. `Erc8004LookupError` carries them as
|
|
904
|
+
getters.
|
|
905
|
+
|
|
906
|
+
### Why `reason` matters: the five are not interchangeable
|
|
907
|
+
|
|
908
|
+
The facilitator serialises every EVM write through one process — they share a
|
|
909
|
+
gas wallet whose nonce is allocated in memory. A task that does not hold that
|
|
910
|
+
lease forwards the write; when it cannot, it answers `503` + `Retry-After: 5` +
|
|
911
|
+
a `reason`.
|
|
912
|
+
|
|
913
|
+
| `reason` | did the write run? | replay the same request? |
|
|
914
|
+
|---|---|---|
|
|
915
|
+
| `holder_unknown` | no | yes |
|
|
916
|
+
| `forwarding_disabled` | no | yes |
|
|
917
|
+
| `forwarded_but_not_writer` | no | yes |
|
|
918
|
+
| `body_unreadable` | no | yes |
|
|
919
|
+
| `forward_failed` | **maybe** | **no** |
|
|
920
|
+
|
|
921
|
+
`forward_failed` is emitted *after* the write was handed over: the holder may
|
|
922
|
+
have executed it and the response been lost coming back. It is a timeout wearing
|
|
923
|
+
a status code. The SDK replays the first four automatically and never that one —
|
|
924
|
+
resolve it by **reading state**, with `getIdentityByOwner` (respecting its
|
|
925
|
+
404-vs-503 distinction) or `getRegisterStatus`. Re-POSTing an ambiguous mint is
|
|
926
|
+
what once created five duplicate agents.
|
|
927
|
+
|
|
928
|
+
Automatic replay is bounded and configurable:
|
|
929
|
+
|
|
930
|
+
```typescript
|
|
931
|
+
new FacilitatorClient({ retries: 0 }); // never replay; default is 2 extra attempts
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
`Retry-After` is honoured only up to `MAX_RETRY_AFTER_SECONDS` (15). A
|
|
935
|
+
misconfigured facilitator answering `Retry-After: 3600` would otherwise hang the
|
|
936
|
+
request for an hour.
|
|
937
|
+
|
|
938
|
+
### Middleware
|
|
939
|
+
|
|
940
|
+
`createPaymentMiddleware` and `createHonoMiddleware` answer **503 with a
|
|
941
|
+
`Retry-After` header** — not `402`, not `500` — whenever the facilitator reached
|
|
942
|
+
no verdict, and keep answering `402` for genuine rejections.
|
|
943
|
+
|
|
872
944
|
## ERC-8004 Trustless Agents
|
|
873
945
|
|
|
874
946
|
Build verifiable on-chain reputation for AI agents and services. Supports **21 networks** (19 EVM + 2 Solana).
|
|
@@ -1129,6 +1201,39 @@ await client.releaseViaFacilitator(paymentInfo);
|
|
|
1129
1201
|
await client.refundViaFacilitator(paymentInfo);
|
|
1130
1202
|
```
|
|
1131
1203
|
|
|
1204
|
+
### Recovering an EXPIRED escrow
|
|
1205
|
+
|
|
1206
|
+
A release attempted after `authorizationExpiry` reverts with
|
|
1207
|
+
`AfterAuthorizationExpiry`. It is widely believed — and this SDK's own comments
|
|
1208
|
+
said so until now — that the funds are then movable only by the payer's
|
|
1209
|
+
`reclaim()`. **That is false, and it is why stuck escrows were written off.**
|
|
1210
|
+
|
|
1211
|
+
From `AuthCaptureEscrow.sol`:
|
|
1212
|
+
|
|
1213
|
+
- `partialVoid` is `onlySender(paymentInfo.operator)` — the operator is the
|
|
1214
|
+
**facilitator**, not the payer — it sends the tokens **to the payer**, and it
|
|
1215
|
+
**never reads `authorizationExpiry`**. It works before expiry and after it.
|
|
1216
|
+
- `reclaim` is `onlySender(paymentInfo.payer)` and gated on expiry. It is a
|
|
1217
|
+
payer's self-service escape hatch, which is why the facilitator does not
|
|
1218
|
+
expose it — not the only exit.
|
|
1219
|
+
|
|
1220
|
+
`refundViaFacilitator` sends `action: "refundInEscrow"`, which reaches
|
|
1221
|
+
`partialVoid`. So a stuck escrow is recoverable with no gas, no payer, and no
|
|
1222
|
+
regard for the expiry:
|
|
1223
|
+
|
|
1224
|
+
```typescript
|
|
1225
|
+
const state = await client.queryEscrowState(paymentInfo);
|
|
1226
|
+
if (state.capturableAmount !== '0') {
|
|
1227
|
+
const result = await client.refundViaFacilitator(paymentInfo, state.capturableAmount);
|
|
1228
|
+
if (!result.success && result.retryable) {
|
|
1229
|
+
// No verdict. The tokens are still in escrow — send it again.
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
```
|
|
1233
|
+
|
|
1234
|
+
Widening the release window still matters: it is what pays the **worker**, and a
|
|
1235
|
+
refund does not.
|
|
1236
|
+
|
|
1132
1237
|
### Direct Charge (No Escrow)
|
|
1133
1238
|
|
|
1134
1239
|
```typescript
|
|
@@ -1443,10 +1548,49 @@ pointed at a facilitator that is not ours. `revocable: false` means the
|
|
|
1443
1548
|
`retentionUntil` in the **signed** receipt cannot be honoured — on public IPFS,
|
|
1444
1549
|
unpinning removes the facilitator's copy, not the network's.
|
|
1445
1550
|
|
|
1551
|
+
### Bringing your own storage
|
|
1552
|
+
|
|
1553
|
+
By default the sealed envelope travels **inside** the anchor request and the
|
|
1554
|
+
facilitator stores it — nothing to set up, and bounded by the request limit
|
|
1555
|
+
below. Pass `upload` and the ciphertext goes to your own sink instead; the
|
|
1556
|
+
request then carries only the pointer, so that request limit no longer applies
|
|
1557
|
+
to your body.
|
|
1558
|
+
|
|
1559
|
+
```ts
|
|
1560
|
+
await anchorEvidence(body, {
|
|
1561
|
+
...opts,
|
|
1562
|
+
// called with the SEALED bytes, never the plaintext
|
|
1563
|
+
upload: async (sealed) => {
|
|
1564
|
+
await myBucket.put(key, sealed);
|
|
1565
|
+
return `s3+https://cdn.example.com/${key}`; // what the buyer dereferences
|
|
1566
|
+
},
|
|
1567
|
+
});
|
|
1568
|
+
```
|
|
1569
|
+
|
|
1570
|
+
`upload` is a **callable, not a precomputed pointer**, for the same reason
|
|
1571
|
+
`sign` is: the SDK has to seal first — the buyer must be able to decrypt — and
|
|
1572
|
+
only then is there anything to upload.
|
|
1573
|
+
|
|
1574
|
+
Three things worth knowing:
|
|
1575
|
+
|
|
1576
|
+
- **Sealing is not skipped.** You receive real ciphertext, not the body. The
|
|
1577
|
+
facilitator still cannot read a `direct`-mode envelope either way; what
|
|
1578
|
+
changes is who pays for durability and who can delete it.
|
|
1579
|
+
- **The signature covers your pointer.** The facilitator verifies against the
|
|
1580
|
+
pointer in the request, so `anchorEvidence` signs the one you returned. Doing
|
|
1581
|
+
this by hand with `sellerDigestFor` means passing the pointer as its fifth
|
|
1582
|
+
argument — signing `''` next to a real pointer throws nothing and leaves the
|
|
1583
|
+
anchor permanently *provisional*.
|
|
1584
|
+
- **A failed upload is a skip, never a failed sale.** If your sink throws or
|
|
1585
|
+
hands back an empty pointer, the result is `skipped: 'anchor_failed'` with
|
|
1586
|
+
`stage: 'upload'` and the payment is untouched.
|
|
1587
|
+
|
|
1446
1588
|
### Limits
|
|
1447
1589
|
|
|
1448
1590
|
- Inline anchors cap at **64 KiB of request** (~47 KB of plaintext); the SDK
|
|
1449
|
-
returns `skipped: 'too_large'` before touching the network.
|
|
1591
|
+
returns `skipped: 'too_large'` before touching the network. This bound is a
|
|
1592
|
+
property of the *request*, so it does not apply when you supply your own
|
|
1593
|
+
storage with `upload` — the ciphertext never travels through the anchor call.
|
|
1450
1594
|
- Anchoring with `retention: 'permanent'` is **irrevocable**.
|
|
1451
1595
|
- On Solana, `verified` is not reachable yet — the on-chain gate cannot read
|
|
1452
1596
|
that payment, so `signed: true` is the honest maximum.
|