uvd-x402-sdk 2.76.0 → 2.78.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uvd-x402-sdk",
3
- "version": "2.76.0",
3
+ "version": "2.78.0",
4
4
  "description": "x402 Payment SDK - Gasless crypto payments across 25 blockchains via Ultravioleta facilitator. Supports EVM (including Scroll, SKALE Base, Robinhood Chain), Solana, Fogo, Stellar, NEAR, Algorand, Sui, and XRP Ledger. Features: ERC-8004 Trustless Agents, Escrow/Refunds, multi-stablecoin (USDC, EURC, AUSD, PYUSD, USDT, USDG).",
5
5
  "author": "Ultravioleta DAO <ultravioletadao@gmail.com>",
6
6
  "license": "MIT",
@@ -579,6 +579,167 @@ export function buildSettleRequest(
579
579
  };
580
580
  }
581
581
 
582
+ // ----------------------------------------------------------------------------
583
+ // CHOOSING THE ENVELOPE
584
+ // ----------------------------------------------------------------------------
585
+ //
586
+ // Everything above emits ONE envelope and makes the caller pick. That is the
587
+ // whole defect: `FacilitatorClient` picked v1 unconditionally, so a seller whose
588
+ // 402 advertised v2 -- which `createHonoMiddleware` does on its own the moment
589
+ // the accepts carry CAIP-2 ids -- could not call the facilitator at all, and
590
+ // every consumer had to port the v2 body by hand. The functions below make the
591
+ // version a decision rather than a constant.
592
+
593
+ /**
594
+ * Networks are CAIP-2 in v2 (`eip155:8453`) and plain names in v1 (`base`).
595
+ *
596
+ * The colon is the whole test, and it is the same one `create402Response` and
597
+ * `normalizeRequirementForVersion` already use. Note `xrpl-mainnet` has no
598
+ * CAIP-2 form -- the v1 string IS its network id -- so XRPL stays on v1 here,
599
+ * which is correct.
600
+ */
601
+ function isCaip2Network(network: string): boolean {
602
+ return network.includes(':');
603
+ }
604
+
605
+ /**
606
+ * Derive the v2 `resource` object from v1-shaped requirements.
607
+ *
608
+ * v2 moved `resource` / `description` / `mimeType` out of the requirements and
609
+ * into an object of their own, and the facilitator requires ALL THREE keys:
610
+ * measured 2026-09-03, a `resource` carrying only `url` is a 400.
611
+ *
612
+ * The `??` defaults are not decoration. `PaymentRequirements` types these as
613
+ * required, but a JavaScript caller can still hand over an object without them,
614
+ * and a missing key does not fail with "description is missing" -- it fails with
615
+ * `data did not match any variant of untagged enum VerifyRequestEnvelope`, which
616
+ * names no field. That error is what cost two teams a day.
617
+ */
618
+ export function toResourceInfoV2(requirements: PaymentRequirements): ResourceInfoV2 {
619
+ return {
620
+ url: requirements.resource,
621
+ description: requirements.description ?? DEFAULT_PAYMENT_DESCRIPTION,
622
+ mimeType: requirements.mimeType ?? DEFAULT_PAYMENT_MIME_TYPE,
623
+ };
624
+ }
625
+
626
+ /**
627
+ * Derive v2 `accepted` requirements from v1-shaped requirements.
628
+ *
629
+ * Two renames do the damage, and neither is reported by name when it is wrong:
630
+ * - `maxAmountRequired` is spelled `amount` in v2.
631
+ * - `network` must be CAIP-2; a plain name inside a v2 body is a 400.
632
+ *
633
+ * `extra` is carried through when present -- it is where the EIP-712 domain
634
+ * `name`/`version` live for tokens the facilitator does not know by address, so
635
+ * dropping it breaks EURC and the bridged USDCs.
636
+ */
637
+ export function toPaymentRequirementsV2(
638
+ requirements: PaymentRequirements
639
+ ): PaymentRequirementsV2 {
640
+ return {
641
+ scheme: requirements.scheme,
642
+ network: isCaip2Network(requirements.network)
643
+ ? requirements.network
644
+ : chainToCAIP2(requirements.network),
645
+ asset: requirements.asset,
646
+ amount: requirements.maxAmountRequired,
647
+ payTo: requirements.payTo,
648
+ // Required by the facilitator: omitting it is a 400, measured the same day.
649
+ maxTimeoutSeconds: requirements.maxTimeoutSeconds ?? DEFAULT_PAYMENT_TIMEOUT_SECONDS,
650
+ ...(requirements.extra !== undefined ? { extra: requirements.extra } : {}),
651
+ };
652
+ }
653
+
654
+ /**
655
+ * Decide which envelope this (payment, requirements) pair has to travel in.
656
+ *
657
+ * `requested` wins when it names a version; `'auto'` (the default) reads the
658
+ * wire.
659
+ *
660
+ * **Auto keys off CAIP-2, NOT off `paymentHeader.x402Version`,** and that is a
661
+ * measured decision rather than a stylistic one. The facilitator's envelope enum
662
+ * is untagged: it matches on SHAPE and ignores the version marker. Measured
663
+ * against production on 2026-09-03:
664
+ *
665
+ * | payload network | requirements network | v1 envelope today |
666
+ * |-----------------|----------------------|-------------------|
667
+ * | `base` | `base` | **200** |
668
+ * | `base` (header says `x402Version: 2`) | `base` | **200** |
669
+ * | `eip155:8453` | `base` | 400 |
670
+ * | `base` | `eip155:8453` | 400 |
671
+ * | `eip155:8453` | `eip155:8453` | 400 (`unknown variant \`eip155:8453\``) |
672
+ *
673
+ * So a header that merely *declares* version 2 while carrying plain names is
674
+ * being served correctly today. Upgrading it on the strength of the marker would
675
+ * change a call that works -- the one thing this must not do. Every CAIP-2
676
+ * combination, by contrast, is already a hard 400, so switching those to v2
677
+ * cannot regress anyone: it can only turn a failure into a payment.
678
+ */
679
+ export function resolveEnvelopeVersion(
680
+ paymentHeader: X402Header,
681
+ requirements: PaymentRequirements,
682
+ requested: X402Version | 'auto' = 'auto'
683
+ ): X402Version {
684
+ if (requested !== 'auto') {
685
+ return requested;
686
+ }
687
+
688
+ return isCaip2Network(paymentHeader.network) || isCaip2Network(requirements.network)
689
+ ? 2
690
+ : 1;
691
+ }
692
+
693
+ /**
694
+ * Build a `/verify` body in whichever envelope `version` names.
695
+ *
696
+ * The v1 return is byte-for-byte what {@link buildVerifyRequest} produces, so
697
+ * pinning `1` is exactly today's behaviour.
698
+ *
699
+ * @example
700
+ * ```ts
701
+ * const version = resolveEnvelopeVersion(payment, requirements);
702
+ * const body = buildVerifyRequestForVersion(payment, requirements, version);
703
+ * ```
704
+ */
705
+ export function buildVerifyRequestForVersion(
706
+ paymentHeader: X402Header,
707
+ requirements: PaymentRequirements,
708
+ version: X402Version
709
+ ): VerifyRequest | VerifyRequestV2 {
710
+ if (version === 2) {
711
+ return buildVerifyRequestV2(
712
+ paymentHeader.payload,
713
+ toResourceInfoV2(requirements),
714
+ toPaymentRequirementsV2(requirements)
715
+ );
716
+ }
717
+
718
+ return buildVerifyRequest(paymentHeader, requirements);
719
+ }
720
+
721
+ /**
722
+ * Build a `/settle` body in whichever envelope `version` names.
723
+ *
724
+ * See {@link buildVerifyRequestForVersion} -- `/settle` takes the same body as
725
+ * `/verify` in both versions.
726
+ */
727
+ export function buildSettleRequestForVersion(
728
+ paymentHeader: X402Header,
729
+ requirements: PaymentRequirements,
730
+ version: X402Version
731
+ ): SettleRequest | SettleRequestV2 {
732
+ if (version === 2) {
733
+ return buildSettleRequestV2(
734
+ paymentHeader.payload,
735
+ toResourceInfoV2(requirements),
736
+ toPaymentRequirementsV2(requirements)
737
+ );
738
+ }
739
+
740
+ return buildSettleRequest(paymentHeader, requirements);
741
+ }
742
+
582
743
  // ============================================================================
583
744
  // CORS CONFIGURATION
584
745
  // ============================================================================
@@ -661,6 +822,16 @@ export interface FacilitatorClientOptions {
661
822
  * -- is never replayed here, at any setting.
662
823
  */
663
824
  retries?: number;
825
+ /**
826
+ * Which envelope to send to `/verify` and `/settle`. Default `'auto'`.
827
+ *
828
+ * `'auto'` reads the wire: CAIP-2 networks get the v2 envelope, plain names
829
+ * get v1. See {@link resolveEnvelopeVersion} for the measurements behind that
830
+ * rule. Pin `1` or `2` to take the decision yourself -- a pin is honoured
831
+ * even when it contradicts the wire, because choosing the version is the
832
+ * point of the option.
833
+ */
834
+ x402Version?: X402Version | 'auto';
664
835
  }
665
836
 
666
837
  /**
@@ -688,12 +859,14 @@ export class FacilitatorClient {
688
859
  private readonly timeout: number;
689
860
  private readonly explicitTimeout: boolean;
690
861
  private readonly retries: number | undefined;
862
+ private readonly x402Version: X402Version | 'auto';
691
863
 
692
864
  constructor(options: FacilitatorClientOptions = {}) {
693
865
  this.baseUrl = options.baseUrl || 'https://facilitator.ultravioletadao.xyz';
694
866
  this.explicitTimeout = options.timeout !== undefined;
695
867
  this.timeout = options.timeout || 30000;
696
868
  this.retries = options.retries;
869
+ this.x402Version = options.x402Version ?? 'auto';
697
870
  }
698
871
 
699
872
  /**
@@ -726,7 +899,15 @@ export class FacilitatorClient {
726
899
  paymentHeader: X402Header,
727
900
  requirements: PaymentRequirements
728
901
  ): Promise<VerifyResponse> {
729
- const body = buildVerifyRequest(paymentHeader, requirements);
902
+ // Not `buildVerifyRequest` any more. That one only speaks v1, so a seller
903
+ // advertising CAIP-2 -- which this SDK's own Hono middleware does by itself
904
+ // -- could not reach the facilitator at all: the body came back 400 with
905
+ // `unknown variant \`eip155:8453\``, and the buyer saw a broken checkout.
906
+ const body = buildVerifyRequestForVersion(
907
+ paymentHeader,
908
+ requirements,
909
+ resolveEnvelopeVersion(paymentHeader, requirements, this.x402Version)
910
+ );
730
911
 
731
912
  try {
732
913
  const { response, error } = await facilitatorFetch(
@@ -780,7 +961,11 @@ export class FacilitatorClient {
780
961
  paymentHeader: X402Header,
781
962
  requirements: PaymentRequirements
782
963
  ): Promise<SettleResponse> {
783
- const body = buildSettleRequest(paymentHeader, requirements);
964
+ const body = buildSettleRequestForVersion(
965
+ paymentHeader,
966
+ requirements,
967
+ resolveEnvelopeVersion(paymentHeader, requirements, this.x402Version)
968
+ );
784
969
  const settleTimeout = this.getTimeout(requirements.network);
785
970
 
786
971
  try {
package/src/dx402.ts CHANGED
@@ -1000,11 +1000,10 @@ export function sellerDigestFor(
1000
1000
  /**
1001
1001
  * Largest `POST /dx402/anchor` request the facilitator accepts, mirroring its
1002
1002
  * `MAX_REQUEST_BODY_BYTES` (default 64 KiB, an anti-OOM bound on every route).
1003
- * After base64 inflation and ~600 bytes of metadata this leaves ~47 KB of
1004
- * plaintext.
1005
1003
  */
1006
1004
  export const ANCHOR_MAX_REQUEST_BYTES = 64 * 1024;
1007
1005
 
1006
+
1008
1007
  /** base64 without spreading the array into arguments -- a large blob would
1009
1008
  * otherwise overflow the call stack and surface as a generic failure. */
1010
1009
  function toBase64(bytes: Uint8Array): string {
@@ -1075,6 +1074,12 @@ export async function anchorEvidence(
1075
1074
  // `sealed` -> it hosts and derives the pointer; `pointer` alone -> it uses
1076
1075
  // yours; neither -> error (x402-rs `dx402/service.rs`).
1077
1076
  ...(pointer !== undefined ? { pointer } : { sealed: toBase64(blob) }),
1077
+ // Declared, not measured. Since x402-rs 2.3.0 the facilitator records
1078
+ // the store that ACTUALLY took the bytes and returns that, so a response
1079
+ // may name a different backend than the request did -- `ipfs` where you
1080
+ // said `s3`, because a deployment whose primary is Pinata wrote there.
1081
+ // That is the facilitator correcting the record, not a mismatch to
1082
+ // assert on.
1078
1083
  backend: opts.backend ?? (pointer !== undefined ? backendForPointer(pointer) : 's3'),
1079
1084
  contentHash: hash,
1080
1085
  keyAlg: opts.payerKey.length === 32 ? 'ECIES-X25519' : 'ECIES-secp256k1',
package/src/index.ts CHANGED
@@ -376,6 +376,13 @@ export {
376
376
  buildSettleRequest,
377
377
  buildVerifyRequestV2,
378
378
  buildSettleRequestV2,
379
+ // Choosing the envelope instead of assuming v1. `FacilitatorClient` now does
380
+ // this on its own; these are for callers that build the body themselves.
381
+ resolveEnvelopeVersion,
382
+ buildVerifyRequestForVersion,
383
+ buildSettleRequestForVersion,
384
+ toResourceInfoV2,
385
+ toPaymentRequirementsV2,
379
386
  getCorsHeaders,
380
387
  X402_CORS_HEADERS,
381
388
  X402_HEADER_NAMES,