uvd-x402-sdk 2.75.0 → 2.77.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 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.