uvd-x402-sdk 2.63.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 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/index.d.mts CHANGED
@@ -283,6 +283,17 @@ interface AnchorOptions {
283
283
  * proves more may supersede it.
284
284
  */
285
285
  proofOfPayment?: Record<string, unknown>;
286
+ /**
287
+ * Which backend to anchor to (`"s3"`, `"ipfs-private"`, `"ipfs-public"`).
288
+ * Omit to take the facilitator's default.
289
+ *
290
+ * Ask {@link availableBackends} what a given facilitator offers rather than
291
+ * assuming — it depends on the deployment, and you may be pointed at one that
292
+ * is not ours. **`ipfs-public` is irreversible**: the bytes become permanently
293
+ * resolvable by anyone, and nobody, the facilitator included, can take them
294
+ * down.
295
+ */
296
+ storage?: string;
286
297
  /**
287
298
  * `(digest) => "0x..."`. A callable rather than a private key is what lets a
288
299
  * custodian sign: it receives the digest and returns the signature without the
@@ -323,6 +334,37 @@ declare function sellerDigestFor(paymentId: string, contentHash: string, payee:
323
334
  */
324
335
  declare const ANCHOR_MAX_REQUEST_BYTES: number;
325
336
  declare function anchorEvidence(body: Uint8Array, opts: AnchorOptions): Promise<Record<string, unknown>>;
337
+ /** One storage option a facilitator offers, from `GET /dx402/stats`. */
338
+ interface BackendOffer {
339
+ id: string;
340
+ retention: string;
341
+ /**
342
+ * Whether the bytes can actually be removed when retention expires.
343
+ *
344
+ * `false` means the `retentionUntil` in the signed receipt **cannot be
345
+ * honoured**: on public IPFS, unpinning removes the facilitator's copy, not
346
+ * the network's.
347
+ */
348
+ revocable: boolean;
349
+ /** Whether anyone resolves the bytes without going through the facilitator. */
350
+ public: boolean;
351
+ enabled: boolean;
352
+ disabledReason?: string;
353
+ }
354
+ /**
355
+ * Ask a facilitator which storage backends it actually offers.
356
+ *
357
+ * Ask instead of assuming. What exists depends on the deployment — one without
358
+ * a Pinata credential offers only `s3` — and an integrator may be pointed at a
359
+ * facilitator that is not ours. A hardcoded list in your code is a promise
360
+ * somebody else has to keep.
361
+ *
362
+ * Resolves to `[]` rather than rejecting when the facilitator is unreachable or
363
+ * does not run DX402 — same discipline as {@link anchorEvidence}.
364
+ */
365
+ declare function availableBackends(facilitator?: string, opts?: {
366
+ fetch?: typeof fetch;
367
+ }): Promise<BackendOffer[]>;
326
368
  /** Encode an anchor result for the `X-Durable-Evidence` response header. */
327
369
  declare function evidenceHeader(evidence: unknown): string;
328
370
 
@@ -973,4 +1015,4 @@ interface EscrowPreAuthParams {
973
1015
  */
974
1016
  declare function buildEscrowPreAuth(wallet: EscrowPreAuthSigner, params: EscrowPreAuthParams): Promise<string>;
975
1017
 
976
- export { ANCHOR_MAX_REQUEST_BYTES, type AnchorOptions, type AnchoredEvidence, ContentHashMismatch, type CreateSignedFetchConfig, DX402Error, type ERC8128RequestOptions, ESCROW_DEPOSIT_LIMIT_USD, ESCROW_TIER_WINDOWS, EVENT_KINDS, EVIDENCE_HEADER, type EscrowNetworkConfig, type EscrowPaymentInfo, type EscrowPreAuthParams, type EscrowPreAuthSigner, type EscrowTierWindows, type EvidenceMode, EvidenceSkipped, FACILITATOR_ADDRESSES, type FacilitatorAddresses, KEEPALIVE_INTERVAL_MS, OPERATOR_FEE_BPS, type RecipientRole, type SSEFrame, SSEParser, type SignRequestOptions, type SignRequestWithSignerOptions, type SignatureBaseParams, type SignatureHeaders, type SignatureParamsInput, SigningWalletAdapter, type StreamTrafficEventsOptions, type TrafficEvent, type TrafficEventKind, TrafficStreamError, ZERO_ADDRESS, anchorDigest, anchorEvidence, buildEscrowPreAuth, buildSignatureBase, buildSignatureParams, computeEscrowNonce, contentHash, createSignedFetch, dereferencePointer, paymentId as dx402PaymentId, ed25519ToX25519, evidenceFromHeaders, evidenceHeader, fetchNonce, getFacilitatorAddress, isEndToEnd, matchesFilters, parseEvidenceHeader, parseSealed, parseTrafficEvent, payerKeyFromEvmSignature, payerKeyFromSolanaAddress, recoverEvidence, sealEvidence, sealEvidenceTo, sealedRoles, sellerDigestFor, signAnchorEd25519, signAnchorEvm, signRequest, signRequestWithSigner, signRequestWithWallet, streamTrafficEvents, unseal };
1018
+ export { ANCHOR_MAX_REQUEST_BYTES, type AnchorOptions, type AnchoredEvidence, type BackendOffer, ContentHashMismatch, type CreateSignedFetchConfig, DX402Error, type ERC8128RequestOptions, ESCROW_DEPOSIT_LIMIT_USD, ESCROW_TIER_WINDOWS, EVENT_KINDS, EVIDENCE_HEADER, type EscrowNetworkConfig, type EscrowPaymentInfo, type EscrowPreAuthParams, type EscrowPreAuthSigner, type EscrowTierWindows, type EvidenceMode, EvidenceSkipped, FACILITATOR_ADDRESSES, type FacilitatorAddresses, KEEPALIVE_INTERVAL_MS, OPERATOR_FEE_BPS, type RecipientRole, type SSEFrame, SSEParser, type SignRequestOptions, type SignRequestWithSignerOptions, type SignatureBaseParams, type SignatureHeaders, type SignatureParamsInput, SigningWalletAdapter, type StreamTrafficEventsOptions, type TrafficEvent, type TrafficEventKind, TrafficStreamError, ZERO_ADDRESS, anchorDigest, anchorEvidence, availableBackends, buildEscrowPreAuth, buildSignatureBase, buildSignatureParams, computeEscrowNonce, contentHash, createSignedFetch, dereferencePointer, paymentId as dx402PaymentId, ed25519ToX25519, evidenceFromHeaders, evidenceHeader, fetchNonce, getFacilitatorAddress, isEndToEnd, matchesFilters, parseEvidenceHeader, parseSealed, parseTrafficEvent, payerKeyFromEvmSignature, payerKeyFromSolanaAddress, recoverEvidence, sealEvidence, sealEvidenceTo, sealedRoles, sellerDigestFor, signAnchorEd25519, signAnchorEvm, signRequest, signRequestWithSigner, signRequestWithWallet, streamTrafficEvents, unseal };
package/dist/index.d.ts CHANGED
@@ -283,6 +283,17 @@ interface AnchorOptions {
283
283
  * proves more may supersede it.
284
284
  */
285
285
  proofOfPayment?: Record<string, unknown>;
286
+ /**
287
+ * Which backend to anchor to (`"s3"`, `"ipfs-private"`, `"ipfs-public"`).
288
+ * Omit to take the facilitator's default.
289
+ *
290
+ * Ask {@link availableBackends} what a given facilitator offers rather than
291
+ * assuming — it depends on the deployment, and you may be pointed at one that
292
+ * is not ours. **`ipfs-public` is irreversible**: the bytes become permanently
293
+ * resolvable by anyone, and nobody, the facilitator included, can take them
294
+ * down.
295
+ */
296
+ storage?: string;
286
297
  /**
287
298
  * `(digest) => "0x..."`. A callable rather than a private key is what lets a
288
299
  * custodian sign: it receives the digest and returns the signature without the
@@ -323,6 +334,37 @@ declare function sellerDigestFor(paymentId: string, contentHash: string, payee:
323
334
  */
324
335
  declare const ANCHOR_MAX_REQUEST_BYTES: number;
325
336
  declare function anchorEvidence(body: Uint8Array, opts: AnchorOptions): Promise<Record<string, unknown>>;
337
+ /** One storage option a facilitator offers, from `GET /dx402/stats`. */
338
+ interface BackendOffer {
339
+ id: string;
340
+ retention: string;
341
+ /**
342
+ * Whether the bytes can actually be removed when retention expires.
343
+ *
344
+ * `false` means the `retentionUntil` in the signed receipt **cannot be
345
+ * honoured**: on public IPFS, unpinning removes the facilitator's copy, not
346
+ * the network's.
347
+ */
348
+ revocable: boolean;
349
+ /** Whether anyone resolves the bytes without going through the facilitator. */
350
+ public: boolean;
351
+ enabled: boolean;
352
+ disabledReason?: string;
353
+ }
354
+ /**
355
+ * Ask a facilitator which storage backends it actually offers.
356
+ *
357
+ * Ask instead of assuming. What exists depends on the deployment — one without
358
+ * a Pinata credential offers only `s3` — and an integrator may be pointed at a
359
+ * facilitator that is not ours. A hardcoded list in your code is a promise
360
+ * somebody else has to keep.
361
+ *
362
+ * Resolves to `[]` rather than rejecting when the facilitator is unreachable or
363
+ * does not run DX402 — same discipline as {@link anchorEvidence}.
364
+ */
365
+ declare function availableBackends(facilitator?: string, opts?: {
366
+ fetch?: typeof fetch;
367
+ }): Promise<BackendOffer[]>;
326
368
  /** Encode an anchor result for the `X-Durable-Evidence` response header. */
327
369
  declare function evidenceHeader(evidence: unknown): string;
328
370
 
@@ -973,4 +1015,4 @@ interface EscrowPreAuthParams {
973
1015
  */
974
1016
  declare function buildEscrowPreAuth(wallet: EscrowPreAuthSigner, params: EscrowPreAuthParams): Promise<string>;
975
1017
 
976
- export { ANCHOR_MAX_REQUEST_BYTES, type AnchorOptions, type AnchoredEvidence, ContentHashMismatch, type CreateSignedFetchConfig, DX402Error, type ERC8128RequestOptions, ESCROW_DEPOSIT_LIMIT_USD, ESCROW_TIER_WINDOWS, EVENT_KINDS, EVIDENCE_HEADER, type EscrowNetworkConfig, type EscrowPaymentInfo, type EscrowPreAuthParams, type EscrowPreAuthSigner, type EscrowTierWindows, type EvidenceMode, EvidenceSkipped, FACILITATOR_ADDRESSES, type FacilitatorAddresses, KEEPALIVE_INTERVAL_MS, OPERATOR_FEE_BPS, type RecipientRole, type SSEFrame, SSEParser, type SignRequestOptions, type SignRequestWithSignerOptions, type SignatureBaseParams, type SignatureHeaders, type SignatureParamsInput, SigningWalletAdapter, type StreamTrafficEventsOptions, type TrafficEvent, type TrafficEventKind, TrafficStreamError, ZERO_ADDRESS, anchorDigest, anchorEvidence, buildEscrowPreAuth, buildSignatureBase, buildSignatureParams, computeEscrowNonce, contentHash, createSignedFetch, dereferencePointer, paymentId as dx402PaymentId, ed25519ToX25519, evidenceFromHeaders, evidenceHeader, fetchNonce, getFacilitatorAddress, isEndToEnd, matchesFilters, parseEvidenceHeader, parseSealed, parseTrafficEvent, payerKeyFromEvmSignature, payerKeyFromSolanaAddress, recoverEvidence, sealEvidence, sealEvidenceTo, sealedRoles, sellerDigestFor, signAnchorEd25519, signAnchorEvm, signRequest, signRequestWithSigner, signRequestWithWallet, streamTrafficEvents, unseal };
1018
+ export { ANCHOR_MAX_REQUEST_BYTES, type AnchorOptions, type AnchoredEvidence, type BackendOffer, ContentHashMismatch, type CreateSignedFetchConfig, DX402Error, type ERC8128RequestOptions, ESCROW_DEPOSIT_LIMIT_USD, ESCROW_TIER_WINDOWS, EVENT_KINDS, EVIDENCE_HEADER, type EscrowNetworkConfig, type EscrowPaymentInfo, type EscrowPreAuthParams, type EscrowPreAuthSigner, type EscrowTierWindows, type EvidenceMode, EvidenceSkipped, FACILITATOR_ADDRESSES, type FacilitatorAddresses, KEEPALIVE_INTERVAL_MS, OPERATOR_FEE_BPS, type RecipientRole, type SSEFrame, SSEParser, type SignRequestOptions, type SignRequestWithSignerOptions, type SignatureBaseParams, type SignatureHeaders, type SignatureParamsInput, SigningWalletAdapter, type StreamTrafficEventsOptions, type TrafficEvent, type TrafficEventKind, TrafficStreamError, ZERO_ADDRESS, anchorDigest, anchorEvidence, availableBackends, buildEscrowPreAuth, buildSignatureBase, buildSignatureParams, computeEscrowNonce, contentHash, createSignedFetch, dereferencePointer, paymentId as dx402PaymentId, ed25519ToX25519, evidenceFromHeaders, evidenceHeader, fetchNonce, getFacilitatorAddress, isEndToEnd, matchesFilters, parseEvidenceHeader, parseSealed, parseTrafficEvent, payerKeyFromEvmSignature, payerKeyFromSolanaAddress, recoverEvidence, sealEvidence, sealEvidenceTo, sealedRoles, sellerDigestFor, signAnchorEd25519, signAnchorEvm, signRequest, signRequestWithSigner, signRequestWithWallet, streamTrafficEvents, unseal };
package/dist/index.js CHANGED
@@ -1628,6 +1628,9 @@ async function anchorEvidence(body, opts) {
1628
1628
  if (opts.proofOfPayment) {
1629
1629
  payload.proofOfPayment = opts.proofOfPayment;
1630
1630
  }
1631
+ if (opts.storage) {
1632
+ payload.storage = opts.storage;
1633
+ }
1631
1634
  if (opts.sign) {
1632
1635
  const digest = sellerDigestFor(opts.paymentId, hash, opts.payee, opts.network);
1633
1636
  if (digest === void 0) {
@@ -1665,6 +1668,17 @@ async function anchorEvidence(body, opts) {
1665
1668
  return { v: 1, skipped: "anchor_failed" };
1666
1669
  }
1667
1670
  }
1671
+ async function availableBackends(facilitator = "https://facilitator.ultravioletadao.xyz", opts = {}) {
1672
+ try {
1673
+ const doFetch = opts.fetch ?? fetch;
1674
+ const res = await doFetch(`${facilitator.replace(/\/+$/, "")}/dx402/stats`);
1675
+ if (!res.ok) return [];
1676
+ const body = await res.json();
1677
+ return body.backends ?? [];
1678
+ } catch {
1679
+ return [];
1680
+ }
1681
+ }
1668
1682
  function evidenceHeader(evidence) {
1669
1683
  const bytes = new TextEncoder().encode(JSON.stringify(evidence));
1670
1684
  return toBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -4331,6 +4345,7 @@ exports.X402_HEADER_NAMES = X402_HEADER_NAMES;
4331
4345
  exports.ZERO_ADDRESS = ZERO_ADDRESS;
4332
4346
  exports.anchorDigest = anchorDigest;
4333
4347
  exports.anchorEvidence = anchorEvidence;
4348
+ exports.availableBackends = availableBackends;
4334
4349
  exports.buildEscrowPreAuth = buildEscrowPreAuth;
4335
4350
  exports.buildPaymentRequirements = buildPaymentRequirements;
4336
4351
  exports.buildSettleRequest = buildSettleRequest;