@relai-fi/x402 0.5.8 → 0.5.9

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/dist/index.js CHANGED
@@ -22,6 +22,117 @@ var CHAIN_IDS = {
22
22
  "polygon": 137,
23
23
  "ethereum": 1
24
24
  };
25
+ var NETWORK_TOKENS = {
26
+ "solana": [
27
+ {
28
+ address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
29
+ symbol: "USDC",
30
+ name: "USD Coin",
31
+ decimals: 6
32
+ }
33
+ ],
34
+ "base": [
35
+ {
36
+ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
37
+ symbol: "USDC",
38
+ name: "USD Coin",
39
+ decimals: 6,
40
+ domainVersion: "2",
41
+ isStableUsd: true
42
+ }
43
+ ],
44
+ "avalanche": [
45
+ {
46
+ address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
47
+ symbol: "USDC",
48
+ name: "USD Coin",
49
+ decimals: 6,
50
+ domainVersion: "2",
51
+ isStableUsd: true
52
+ }
53
+ ],
54
+ "skale-base": [
55
+ {
56
+ address: "0x85889c8c714505E0c94b30fcfcF64fE3Ac8FCb20",
57
+ symbol: "USDC",
58
+ name: "Bridged USDC (SKALE Bridge)",
59
+ decimals: 6,
60
+ domainVersion: "2",
61
+ isStableUsd: true
62
+ },
63
+ {
64
+ address: "0x2bF5bF154b515EaA82C31a65ec11554fF5aF7fCA",
65
+ symbol: "USDT",
66
+ name: "Bridged USDT (SKALE Bridge)",
67
+ decimals: 6,
68
+ domainVersion: "2",
69
+ isStableUsd: true
70
+ },
71
+ {
72
+ address: "0x1aeeCFE5454c83B42D8A316246CAc9739E7f690e",
73
+ symbol: "WBTC",
74
+ name: "Wrapped Bitcoin",
75
+ decimals: 8,
76
+ domainVersion: "2",
77
+ isStableUsd: false
78
+ },
79
+ {
80
+ address: "0x7bD39ABBd0Dd13103542cAe3276C7fA332bCA486",
81
+ symbol: "WETH",
82
+ name: "Wrapped Ether",
83
+ decimals: 18,
84
+ domainVersion: "2",
85
+ isStableUsd: false
86
+ }
87
+ ],
88
+ "skale-base-sepolia": [
89
+ {
90
+ address: "0x2e08028E3C4c2356572E096d8EF835cD5C6030bD",
91
+ symbol: "USDC",
92
+ name: "Bridged USDC (SKALE Bridge)",
93
+ decimals: 6,
94
+ domainVersion: "2",
95
+ isStableUsd: true
96
+ }
97
+ ],
98
+ "skale-bite": [
99
+ {
100
+ address: "0xc4083B1E81ceb461Ccef3FDa8A9F24F0d764B6D8",
101
+ symbol: "USDC",
102
+ name: "USDC",
103
+ decimals: 6,
104
+ domainVersion: "1",
105
+ isStableUsd: true
106
+ }
107
+ ],
108
+ "polygon": [
109
+ {
110
+ address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
111
+ symbol: "USDC",
112
+ name: "USD Coin",
113
+ decimals: 6,
114
+ domainVersion: "2",
115
+ isStableUsd: true
116
+ }
117
+ ],
118
+ "ethereum": [
119
+ {
120
+ address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
121
+ symbol: "USDC",
122
+ name: "USD Coin",
123
+ decimals: 6,
124
+ domainVersion: "2",
125
+ isStableUsd: true
126
+ }
127
+ ]
128
+ };
129
+ function resolveToken(network, asset) {
130
+ const tokens = NETWORK_TOKENS[network];
131
+ if (!tokens || tokens.length === 0) return null;
132
+ if (!asset) return tokens[0];
133
+ const normalized = String(asset).toLowerCase();
134
+ return tokens.find((token) => token.address.toLowerCase() === normalized) || null;
135
+ }
25
136
  var USDC_ADDRESSES = {
26
137
  "solana": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
27
138
  "base": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
@@ -96,9 +207,103 @@ function stripePayTo(stripeSecretKey, options) {
96
207
  stripeNetwork: options?.network || "base"
97
208
  };
98
209
  }
210
+ var USD_PRICE_CACHE_TTL_MS = 60 * 1e3;
211
+ var usdPriceCache = /* @__PURE__ */ new Map();
212
+ var COINGECKO_ID_BY_SYMBOL = {
213
+ WETH: "ethereum",
214
+ WBTC: "bitcoin"
215
+ };
216
+ function isStableUsdToken(token) {
217
+ if (token.isStableUsd === true) return true;
218
+ const symbol = String(token.symbol || "").toUpperCase();
219
+ return symbol === "USDC" || symbol === "USDT";
220
+ }
221
+ async function fetchUsdPriceFromCoinGecko(coinId) {
222
+ const now = Date.now();
223
+ const cached = usdPriceCache.get(coinId);
224
+ if (cached && cached.expiresAt > now) {
225
+ return cached.usd;
226
+ }
227
+ const url = `https://api.coingecko.com/api/v3/simple/price?ids=${encodeURIComponent(coinId)}&vs_currencies=usd`;
228
+ const res = await fetch(url, {
229
+ method: "GET",
230
+ headers: {
231
+ Accept: "application/json"
232
+ }
233
+ });
234
+ if (!res.ok) {
235
+ throw new Error(`CoinGecko price request failed: HTTP ${res.status}`);
236
+ }
237
+ const payload = await res.json();
238
+ const usd = Number(payload?.[coinId]?.usd);
239
+ if (!Number.isFinite(usd) || usd <= 0) {
240
+ throw new Error(`CoinGecko returned invalid usd price for ${coinId}`);
241
+ }
242
+ usdPriceCache.set(coinId, {
243
+ usd,
244
+ expiresAt: now + USD_PRICE_CACHE_TTL_MS
245
+ });
246
+ return usd;
247
+ }
248
+ async function resolveAmountAtomicFromUsd({
249
+ priceUsd,
250
+ token,
251
+ network
252
+ }) {
253
+ const decimals = Number(token.decimals);
254
+ if (!Number.isFinite(decimals) || decimals < 0) {
255
+ throw new Error(`Invalid token decimals for ${token.symbol || token.address}`);
256
+ }
257
+ if (isStableUsdToken(token)) {
258
+ const units = Math.floor(priceUsd * Math.pow(10, decimals));
259
+ return String(Math.max(1, units));
260
+ }
261
+ const symbol = String(token.symbol || "").toUpperCase();
262
+ const coinGeckoId = COINGECKO_ID_BY_SYMBOL[symbol];
263
+ if (!coinGeckoId) {
264
+ throw new Error(`No USD quote source configured for token ${symbol || token.address} on ${network}`);
265
+ }
266
+ const overrideEnv = process.env[`EVM_TOKEN_PRICE_${symbol}_USD`];
267
+ const usdPerToken = overrideEnv && Number(overrideEnv) > 0 ? Number(overrideEnv) : await fetchUsdPriceFromCoinGecko(coinGeckoId);
268
+ const tokenAmount = priceUsd / usdPerToken;
269
+ const rawUnits = tokenAmount * Math.pow(10, decimals);
270
+ if (!Number.isFinite(rawUnits) || rawUnits <= 0) {
271
+ throw new Error(
272
+ `Invalid conversion result for token ${symbol || token.address}: priceUsd=${priceUsd}, usdPerToken=${usdPerToken}`
273
+ );
274
+ }
275
+ return String(Math.max(1, Math.floor(rawUnits)));
276
+ }
99
277
  function isStripePayTo(payTo) {
100
278
  return typeof payTo === "object" && payTo !== null && payTo.__brand === "stripePayTo";
101
279
  }
280
+ function parseBooleanHeader(value) {
281
+ if (value == null) return null;
282
+ const normalized = String(value).trim().toLowerCase();
283
+ if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") {
284
+ return true;
285
+ }
286
+ if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "off") {
287
+ return false;
288
+ }
289
+ return null;
290
+ }
291
+ function normalizeIntegritasFlow(value) {
292
+ const normalized = String(value || "").trim().toLowerCase();
293
+ if (normalized === "single") return "single";
294
+ if (normalized === "dual") return "dual";
295
+ return void 0;
296
+ }
297
+ function normalizeIntegritasOptions(value) {
298
+ if (value === true) return { enabled: true };
299
+ if (value === false || value == null) return { enabled: false };
300
+ const flow = normalizeIntegritasFlow(value.flow);
301
+ const enabled = typeof value.enabled === "boolean" ? value.enabled : true;
302
+ return {
303
+ enabled,
304
+ ...flow ? { flow } : {}
305
+ };
306
+ }
102
307
  async function createStripeDepositAddress(secretKey, amountUsdCents, network = "base") {
103
308
  const params = new URLSearchParams();
104
309
  params.append("amount", String(amountUsdCents));
@@ -193,8 +398,45 @@ var Relai = class {
193
398
  const stripeConfig = isStripePayTo(options.payTo) ? options.payTo : null;
194
399
  const network = stripeConfig ? stripeConfig.stripeNetwork || "base" : options.network || self.network;
195
400
  const caip2 = NETWORK_CAIP2[network];
196
- const asset = USDC_ADDRESSES[network];
197
- const amount = String(Math.floor(resolvedPrice * 1e6));
401
+ const requestedAsset = typeof options.asset === "string" && options.asset.trim() !== "" ? options.asset.trim() : void 0;
402
+ const explicitToken = requestedAsset ? resolveToken(network, requestedAsset) : null;
403
+ if (requestedAsset && !explicitToken) {
404
+ return res.status(400).json({
405
+ error: `Unsupported asset ${requestedAsset} for network ${network}`
406
+ });
407
+ }
408
+ const fallbackToken = {
409
+ address: USDC_ADDRESSES[network],
410
+ symbol: "USDC",
411
+ name: network === "skale-bite" ? "USDC" : "USD Coin",
412
+ decimals: 6,
413
+ domainVersion: network === "skale-bite" ? "1" : "2",
414
+ isStableUsd: true
415
+ };
416
+ const token = explicitToken || resolveToken(network) || fallbackToken;
417
+ const asset = token.address;
418
+ const tokenName = token.name || "USD Coin";
419
+ const tokenVersion = token.domainVersion || (network === "skale-bite" ? "1" : "2");
420
+ const tokenDecimals = Number.isFinite(Number(token.decimals)) ? Number(token.decimals) : 6;
421
+ let amount;
422
+ try {
423
+ amount = await resolveAmountAtomicFromUsd({
424
+ priceUsd: resolvedPrice,
425
+ token,
426
+ network
427
+ });
428
+ } catch (err) {
429
+ console.error("[Relai] Failed to convert USD amount to token units:", err);
430
+ return res.status(500).json({
431
+ error: "Failed to quote token amount for payment requirements"
432
+ });
433
+ }
434
+ const configuredIntegritas = normalizeIntegritasOptions(options.integritas);
435
+ const headerIntegritasEnabled = parseBooleanHeader(req.headers["x-integritas"]);
436
+ const headerIntegritasFlow = normalizeIntegritasFlow(req.headers["x-integritas-flow"]);
437
+ const integritasEnabled = headerIntegritasEnabled === null ? configuredIntegritas.enabled : headerIntegritasEnabled;
438
+ const integritasFlow = headerIntegritasFlow || configuredIntegritas.flow;
439
+ const integritasMode = integritasFlow === "single" ? "single_signature_fee_included" : integritasFlow === "dual" ? "dual_signature_split" : void 0;
198
440
  const paymentHeader = req.headers["x-payment"] || req.headers["payment-signature"] || req.headers["x-payment-signature"];
199
441
  if (!paymentHeader) {
200
442
  options.onPaymentRequired?.(req, { price: resolvedPrice, network });
@@ -210,23 +452,6 @@ var Relai = class {
210
452
  resolvedPayTo = options.payTo;
211
453
  }
212
454
  const feePayer = await self.getFeePayer(caip2);
213
- const tokenMetadata = {
214
- "eip155:103698795": { name: "USDC", version: "1" },
215
- // SKALE BITE
216
- "eip155:1187947933": { name: "Bridged USDC (SKALE Bridge)", version: "2" },
217
- // SKALE Base
218
- "eip155:324705682": { name: "Bridged USDC (SKALE Bridge)", version: "2" },
219
- // SKALE Base Sepolia
220
- "eip155:8453": { name: "USD Coin", version: "2" },
221
- // Base
222
- "eip155:43114": { name: "USD Coin", version: "2" },
223
- // Avalanche
224
- "eip155:137": { name: "USD Coin", version: "2" },
225
- // Polygon
226
- "eip155:1": { name: "USD Coin", version: "2" }
227
- // Ethereum
228
- };
229
- const metadata = tokenMetadata[caip2] || { name: "USDC", version: "1" };
230
455
  return res.status(402).json({
231
456
  x402Version: 2,
232
457
  error: "Payment required",
@@ -243,13 +468,28 @@ var Relai = class {
243
468
  payTo: resolvedPayTo,
244
469
  maxTimeoutSeconds: options.maxTimeoutSeconds || 60,
245
470
  extra: {
246
- name: metadata.name,
247
- version: metadata.version,
248
- decimals: 6,
249
- ...feePayer && { feePayer }
471
+ name: tokenName,
472
+ version: tokenVersion,
473
+ decimals: tokenDecimals,
474
+ symbol: token.symbol,
475
+ ...feePayer && { feePayer },
250
476
  // Add feePayer if available
477
+ ...integritasEnabled ? { integritasEnabled: true } : {},
478
+ ...integritasFlow ? { integritasFlow } : {},
479
+ ...integritasMode ? { integritasMode } : {},
480
+ ...integritasFlow === "single" ? { integritasSingleSignature: true } : {}
481
+ }
482
+ }],
483
+ ...configuredIntegritas.enabled || integritasEnabled || !!integritasFlow ? {
484
+ extensions: {
485
+ integritas: {
486
+ available: true,
487
+ selectedFlow: integritasFlow || null,
488
+ availableFlows: ["single", "dual"],
489
+ enabled: integritasEnabled
490
+ }
251
491
  }
252
- }]
492
+ } : {}
253
493
  });
254
494
  }
255
495
  let paymentProof;
@@ -284,7 +524,15 @@ var Relai = class {
284
524
  amount,
285
525
  asset,
286
526
  payTo: settlePayTo,
287
- maxTimeoutSeconds: options.maxTimeoutSeconds || 60
527
+ maxTimeoutSeconds: options.maxTimeoutSeconds || 60,
528
+ ...integritasEnabled ? {
529
+ extra: {
530
+ integritasEnabled: true,
531
+ ...integritasFlow ? { integritasFlow } : {},
532
+ ...integritasMode ? { integritasMode } : {},
533
+ ...integritasFlow === "single" ? { integritasSingleSignature: true } : {}
534
+ }
535
+ } : {}
288
536
  };
289
537
  const settleUrl = `${self.facilitatorUrl}/settle`;
290
538
  const settleRes = await fetch(settleUrl, {
@@ -374,12 +622,32 @@ function createX402Client(config) {
374
622
  wallets = {},
375
623
  wallet: legacyWallet,
376
624
  facilitatorUrl = RELAI_FACILITATOR_URL,
625
+ relayWs,
377
626
  preferredNetwork,
378
627
  solanaRpcUrl = "https://api.mainnet-beta.solana.com",
379
628
  evmRpcUrls = {},
380
629
  maxAmountAtomic,
630
+ integritas,
381
631
  verbose = false
382
632
  } = config;
633
+ const relayWsEnabled = relayWs?.enabled === true;
634
+ const relayWsPreflightTimeoutMs = relayWs?.preflightTimeoutMs ?? 5e3;
635
+ const relayWsPaymentTimeoutMs = relayWs?.paymentTimeoutMs ?? 1e4;
636
+ const relayWsFallbackToHttp = relayWs?.fallbackToHttp ?? true;
637
+ const defaultIntegritas = normalizeIntegritasOptions2(integritas);
638
+ const relayWsReservedSubdomains = /* @__PURE__ */ new Set([
639
+ "www",
640
+ "api",
641
+ "localhost",
642
+ "admin",
643
+ "app",
644
+ "dashboard",
645
+ "docs",
646
+ "documentation",
647
+ "status",
648
+ "blog",
649
+ "facilitator"
650
+ ]);
383
651
  const log = verbose ? console.log.bind(console, "[relai-x402]") : () => {
384
652
  };
385
653
  const effectiveWallets = { ...wallets };
@@ -390,6 +658,408 @@ function createX402Client(config) {
390
658
  effectiveWallets.solana?.publicKey && effectiveWallets.solana?.signTransaction
391
659
  );
392
660
  if (hasSolanaWallet) log("Solana wallet ready");
661
+ function isRecord(value) {
662
+ return typeof value === "object" && value !== null;
663
+ }
664
+ function parseJsonSafe(value) {
665
+ try {
666
+ return JSON.parse(value);
667
+ } catch {
668
+ return null;
669
+ }
670
+ }
671
+ function addSocketListener(socket, eventName, listener) {
672
+ if (socket.addEventListener) {
673
+ socket.addEventListener(eventName, listener);
674
+ return;
675
+ }
676
+ if (socket.on) {
677
+ socket.on(eventName, listener);
678
+ }
679
+ }
680
+ function removeSocketListener(socket, eventName, listener) {
681
+ if (socket.removeEventListener) {
682
+ socket.removeEventListener(eventName, listener);
683
+ return;
684
+ }
685
+ if (socket.off) {
686
+ socket.off(eventName, listener);
687
+ return;
688
+ }
689
+ if (socket.removeListener) {
690
+ socket.removeListener(eventName, listener);
691
+ }
692
+ }
693
+ function resolveRelayWsUrl(relayUrl) {
694
+ if (relayWs?.wsUrl && relayWs.wsUrl.trim() !== "") {
695
+ return relayWs.wsUrl.trim();
696
+ }
697
+ const parsedRelayUrl = new URL(relayUrl);
698
+ const wsProtocol = parsedRelayUrl.protocol === "https:" ? "wss:" : "ws:";
699
+ return `${wsProtocol}//${parsedRelayUrl.host}/api/ws/relay`;
700
+ }
701
+ function resolveRelayWhitelabel(parsedRelayUrl) {
702
+ const hostParts = parsedRelayUrl.hostname.toLowerCase().split(".").filter(Boolean);
703
+ if (hostParts.length < 2) {
704
+ return null;
705
+ }
706
+ const candidate = decodeURIComponent(hostParts[0] || "").trim();
707
+ if (!candidate) {
708
+ return null;
709
+ }
710
+ if (relayWsReservedSubdomains.has(candidate.toLowerCase())) {
711
+ return null;
712
+ }
713
+ const lastPart = hostParts[hostParts.length - 1];
714
+ const secondLastPart = hostParts[hostParts.length - 2];
715
+ const isX402WhitelabelHost = hostParts.length >= 3 && secondLastPart === "x402" && lastPart === "fi";
716
+ const isLocalWhitelabelHost = hostParts.length === 2 && lastPart === "localhost";
717
+ if (!isX402WhitelabelHost && !isLocalWhitelabelHost) {
718
+ return null;
719
+ }
720
+ return candidate;
721
+ }
722
+ function resolveRelayTarget(relayUrl) {
723
+ const parsedRelayUrl = new URL(relayUrl);
724
+ const match = parsedRelayUrl.pathname.match(/\/relay\/([^/]+)(\/.*)?$/);
725
+ if (match) {
726
+ const apiId = decodeURIComponent(match[1]);
727
+ const pathPart2 = match[2] || "/";
728
+ return {
729
+ apiId,
730
+ path: `${pathPart2}${parsedRelayUrl.search || ""}`
731
+ };
732
+ }
733
+ const whitelabel = resolveRelayWhitelabel(parsedRelayUrl);
734
+ if (!whitelabel) {
735
+ throw new Error(
736
+ `[relai-x402] Unsupported relay URL format for WS transport: ${relayUrl}. Expected /relay/:apiId/... or <whitelabel>.x402.fi/...`
737
+ );
738
+ }
739
+ const pathPart = parsedRelayUrl.pathname && parsedRelayUrl.pathname !== "" ? parsedRelayUrl.pathname : "/";
740
+ return {
741
+ apiId: whitelabel,
742
+ path: `${pathPart}${parsedRelayUrl.search || ""}`
743
+ };
744
+ }
745
+ function isRelayRequestUrl(requestUrl) {
746
+ try {
747
+ resolveRelayTarget(requestUrl);
748
+ return true;
749
+ } catch {
750
+ return false;
751
+ }
752
+ }
753
+ function headersToRecord(headersInit) {
754
+ if (!headersInit) return {};
755
+ const output = {};
756
+ if (typeof Headers !== "undefined" && headersInit instanceof Headers) {
757
+ headersInit.forEach((value, key) => {
758
+ output[key] = value;
759
+ });
760
+ return output;
761
+ }
762
+ if (Array.isArray(headersInit)) {
763
+ for (const [key, value] of headersInit) {
764
+ output[key] = value;
765
+ }
766
+ return output;
767
+ }
768
+ for (const [key, value] of Object.entries(headersInit)) {
769
+ if (typeof value === "string") {
770
+ output[key] = value;
771
+ } else if (Array.isArray(value)) {
772
+ output[key] = value.join(", ");
773
+ } else if (value !== void 0 && value !== null) {
774
+ output[key] = String(value);
775
+ }
776
+ }
777
+ return output;
778
+ }
779
+ function hasHeaderCaseInsensitive(headers, headerName) {
780
+ const normalized = headerName.toLowerCase();
781
+ return Object.keys(headers).some((key) => key.toLowerCase() === normalized);
782
+ }
783
+ function normalizeIntegritasFlow2(value) {
784
+ const normalized = String(value || "").trim().toLowerCase();
785
+ if (normalized === "single") return "single";
786
+ if (normalized === "dual") return "dual";
787
+ return void 0;
788
+ }
789
+ function normalizeIntegritasOptions2(value) {
790
+ if (value === true) return { enabled: true };
791
+ if (value === false || value == null) return { enabled: false };
792
+ const flow = normalizeIntegritasFlow2(value.flow);
793
+ const enabled = typeof value.enabled === "boolean" ? value.enabled : true;
794
+ return {
795
+ enabled,
796
+ ...flow ? { flow } : {}
797
+ };
798
+ }
799
+ function resolveIntegritasOptions(override) {
800
+ if (override === void 0) {
801
+ return defaultIntegritas;
802
+ }
803
+ if (typeof override === "boolean") {
804
+ return {
805
+ enabled: override,
806
+ ...override && defaultIntegritas.flow ? { flow: defaultIntegritas.flow } : {}
807
+ };
808
+ }
809
+ const flow = normalizeIntegritasFlow2(override.flow) || defaultIntegritas.flow;
810
+ const enabled = typeof override.enabled === "boolean" ? override.enabled : defaultIntegritas.enabled;
811
+ return {
812
+ enabled,
813
+ ...enabled && flow ? { flow } : {}
814
+ };
815
+ }
816
+ function stripInternalInit(init) {
817
+ if (!init) return void 0;
818
+ const { x402: _x402, ...requestInit } = init;
819
+ return requestInit;
820
+ }
821
+ function applyIntegritasHeaders(headers, options) {
822
+ if (!options.enabled) return headers;
823
+ if (!hasHeaderCaseInsensitive(headers, "x-integritas")) {
824
+ headers["X-Integritas"] = "true";
825
+ }
826
+ if (options.flow && !hasHeaderCaseInsensitive(headers, "x-integritas-flow")) {
827
+ headers["X-Integritas-Flow"] = options.flow;
828
+ }
829
+ return headers;
830
+ }
831
+ function getRequestMethod(input, init) {
832
+ const inputMethod = input instanceof Request ? input.method : void 0;
833
+ return (init?.method || inputMethod || "GET").toUpperCase();
834
+ }
835
+ async function bodyInitToWsPayload(bodyInit) {
836
+ if (bodyInit === void 0 || bodyInit === null) {
837
+ return void 0;
838
+ }
839
+ if (typeof bodyInit === "string") {
840
+ const parsed = parseJsonSafe(bodyInit);
841
+ return parsed === null ? bodyInit : parsed;
842
+ }
843
+ if (typeof URLSearchParams !== "undefined" && bodyInit instanceof URLSearchParams) {
844
+ return bodyInit.toString();
845
+ }
846
+ if (typeof FormData !== "undefined" && bodyInit instanceof FormData) {
847
+ const entries = {};
848
+ for (const [key, value] of bodyInit.entries()) {
849
+ entries[key] = typeof value === "string" ? value : value.name;
850
+ }
851
+ return entries;
852
+ }
853
+ if (typeof Blob !== "undefined" && bodyInit instanceof Blob) {
854
+ const text = await bodyInit.text();
855
+ if (!text) return void 0;
856
+ const parsed = parseJsonSafe(text);
857
+ return parsed === null ? text : parsed;
858
+ }
859
+ if (bodyInit instanceof ArrayBuffer) {
860
+ return Array.from(new Uint8Array(bodyInit));
861
+ }
862
+ if (ArrayBuffer.isView(bodyInit)) {
863
+ return Array.from(new Uint8Array(bodyInit.buffer, bodyInit.byteOffset, bodyInit.byteLength));
864
+ }
865
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(bodyInit)) {
866
+ return Array.from(bodyInit.values());
867
+ }
868
+ if (isRecord(bodyInit)) {
869
+ return bodyInit;
870
+ }
871
+ return String(bodyInit);
872
+ }
873
+ async function resolveRequestBody(input, init) {
874
+ if (init && Object.prototype.hasOwnProperty.call(init, "body")) {
875
+ return bodyInitToWsPayload(init.body);
876
+ }
877
+ if (input instanceof Request) {
878
+ const method = getRequestMethod(input, init);
879
+ if (method === "GET" || method === "HEAD") {
880
+ return void 0;
881
+ }
882
+ try {
883
+ const cloned = input.clone();
884
+ const text = await cloned.text();
885
+ if (!text) return void 0;
886
+ const parsed = parseJsonSafe(text);
887
+ return parsed === null ? text : parsed;
888
+ } catch {
889
+ return void 0;
890
+ }
891
+ }
892
+ return void 0;
893
+ }
894
+ function getRequestHeaders(input, init) {
895
+ const fromInput = input instanceof Request ? headersToRecord(input.headers) : {};
896
+ const fromInit = headersToRecord(init?.headers);
897
+ const merged = {
898
+ ...fromInput,
899
+ ...fromInit
900
+ };
901
+ if (!merged.Accept && !merged.accept) {
902
+ merged.Accept = "application/json";
903
+ }
904
+ return merged;
905
+ }
906
+ function toMessageString(data) {
907
+ if (typeof data === "string") {
908
+ return data;
909
+ }
910
+ if (isRecord(data) && typeof data.data !== "undefined") {
911
+ return toMessageString(data.data);
912
+ }
913
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
914
+ return data.toString("utf8");
915
+ }
916
+ if (data instanceof ArrayBuffer) {
917
+ const bytes = new Uint8Array(data);
918
+ if (typeof Buffer !== "undefined") {
919
+ return Buffer.from(bytes).toString("utf8");
920
+ }
921
+ if (typeof TextDecoder !== "undefined") {
922
+ return new TextDecoder().decode(bytes);
923
+ }
924
+ throw new Error("Unsupported WebSocket message data type");
925
+ }
926
+ if (ArrayBuffer.isView(data)) {
927
+ const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
928
+ if (typeof Buffer !== "undefined") {
929
+ return Buffer.from(bytes).toString("utf8");
930
+ }
931
+ if (typeof TextDecoder !== "undefined") {
932
+ return new TextDecoder().decode(bytes);
933
+ }
934
+ throw new Error("Unsupported WebSocket message data type");
935
+ }
936
+ throw new Error("Unsupported WebSocket message data type");
937
+ }
938
+ function getWebSocketFactory() {
939
+ if (relayWs?.webSocketFactory) {
940
+ return relayWs.webSocketFactory;
941
+ }
942
+ if (typeof WebSocket !== "undefined") {
943
+ return (wsUrl) => new WebSocket(wsUrl);
944
+ }
945
+ throw new Error(
946
+ "[relai-x402] WebSocket is not available in this runtime. Provide relayWs.webSocketFactory."
947
+ );
948
+ }
949
+ async function relayCallOverWebSocket(request) {
950
+ const wsFactory = getWebSocketFactory();
951
+ const wsUrl = resolveRelayWsUrl(request.relayUrl);
952
+ const target = resolveRelayTarget(request.relayUrl);
953
+ const requestId = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
954
+ const socket = wsFactory(wsUrl);
955
+ return new Promise((resolve, reject) => {
956
+ let settled = false;
957
+ const settleResolve = (value) => {
958
+ if (settled) return;
959
+ settled = true;
960
+ cleanup();
961
+ try {
962
+ socket.close();
963
+ } catch {
964
+ }
965
+ resolve(value);
966
+ };
967
+ const settleReject = (error) => {
968
+ if (settled) return;
969
+ settled = true;
970
+ cleanup();
971
+ try {
972
+ socket.close();
973
+ } catch {
974
+ }
975
+ reject(error);
976
+ };
977
+ const timeoutId = setTimeout(() => {
978
+ settleReject(new Error(`[relai-x402] Timed out waiting for WS relay response after ${request.timeoutMs}ms`));
979
+ }, request.timeoutMs);
980
+ const cleanup = () => {
981
+ clearTimeout(timeoutId);
982
+ removeSocketListener(socket, "open", onOpen);
983
+ removeSocketListener(socket, "message", onMessage);
984
+ removeSocketListener(socket, "error", onError);
985
+ removeSocketListener(socket, "close", onClose);
986
+ };
987
+ const onOpen = () => {
988
+ const envelope = {
989
+ id: requestId,
990
+ method: "relay.call",
991
+ params: {
992
+ apiId: target.apiId,
993
+ path: target.path,
994
+ requestMethod: request.requestMethod,
995
+ requestHeaders: request.requestHeaders,
996
+ ...request.requestBody !== void 0 ? { requestBody: request.requestBody } : {}
997
+ }
998
+ };
999
+ if (request.paymentPayload !== void 0) {
1000
+ envelope.payment = request.paymentPayload;
1001
+ }
1002
+ try {
1003
+ socket.send(JSON.stringify(envelope));
1004
+ } catch {
1005
+ settleReject(new Error("[relai-x402] Failed to send WS relay request"));
1006
+ }
1007
+ };
1008
+ const onMessage = (...args) => {
1009
+ const payload = args.length > 0 ? args[0] : void 0;
1010
+ let parsed;
1011
+ try {
1012
+ parsed = parseJsonSafe(toMessageString(payload));
1013
+ } catch {
1014
+ return;
1015
+ }
1016
+ if (!isRecord(parsed)) return;
1017
+ const responseId = typeof parsed.id === "string" || typeof parsed.id === "number" ? String(parsed.id) : "";
1018
+ if (responseId !== requestId) return;
1019
+ settleResolve(parsed);
1020
+ };
1021
+ const onError = () => {
1022
+ settleReject(new Error("[relai-x402] WebSocket relay transport error"));
1023
+ };
1024
+ const onClose = () => {
1025
+ settleReject(new Error("[relai-x402] WebSocket relay connection closed before response"));
1026
+ };
1027
+ addSocketListener(socket, "open", onOpen);
1028
+ addSocketListener(socket, "message", onMessage);
1029
+ addSocketListener(socket, "error", onError);
1030
+ addSocketListener(socket, "close", onClose);
1031
+ });
1032
+ }
1033
+ function extractPaymentRequirementsFromWsError(error) {
1034
+ const candidates = [error.paymentRequired, error.data];
1035
+ for (const candidate of candidates) {
1036
+ if (!isRecord(candidate)) continue;
1037
+ if (Array.isArray(candidate.accepts)) {
1038
+ return candidate;
1039
+ }
1040
+ if (isRecord(candidate.paymentRequired) && Array.isArray(candidate.paymentRequired.accepts)) {
1041
+ return candidate.paymentRequired;
1042
+ }
1043
+ if (isRecord(candidate.data) && Array.isArray(candidate.data.accepts)) {
1044
+ return candidate.data;
1045
+ }
1046
+ }
1047
+ return null;
1048
+ }
1049
+ function buildWsResponse(wsResponse) {
1050
+ const statusFromMetadata = isRecord(wsResponse.metadata) && typeof wsResponse.metadata.status === "number" ? wsResponse.metadata.status : 200;
1051
+ const status = Number.isInteger(statusFromMetadata) && statusFromMetadata >= 100 && statusFromMetadata <= 599 ? statusFromMetadata : 200;
1052
+ const headers = new Headers();
1053
+ headers.set("Content-Type", "application/json");
1054
+ if (wsResponse.paymentResponse !== void 0) {
1055
+ headers.set("PAYMENT-RESPONSE", encodeBase64Json(wsResponse.paymentResponse));
1056
+ }
1057
+ const bodyPayload = wsResponse.result === void 0 ? null : wsResponse.result;
1058
+ return new Response(JSON.stringify(bodyPayload), {
1059
+ status,
1060
+ headers
1061
+ });
1062
+ }
393
1063
  function selectAccept(accepts) {
394
1064
  if (preferredNetwork) {
395
1065
  const caip2 = NETWORK_CAIP2[preferredNetwork];
@@ -501,7 +1171,7 @@ function createX402Client(config) {
501
1171
  amount: paymentAmount
502
1172
  }
503
1173
  };
504
- return btoa(JSON.stringify(paymentPayload));
1174
+ return encodeBase64Json(paymentPayload);
505
1175
  }
506
1176
  async function buildEvmPayment(accept, requirements, url) {
507
1177
  const evmWallet = effectiveWallets.evm;
@@ -569,7 +1239,7 @@ function createX402Client(config) {
569
1239
  },
570
1240
  facilitatorUrl
571
1241
  };
572
- return btoa(JSON.stringify(paymentPayload));
1242
+ return encodeBase64Json(paymentPayload);
573
1243
  }
574
1244
  async function buildSolanaPayment(accept, requirements, url) {
575
1245
  const solWallet = effectiveWallets.solana;
@@ -633,7 +1303,17 @@ function createX402Client(config) {
633
1303
  transaction: serializedTx
634
1304
  }
635
1305
  };
636
- return btoa(JSON.stringify(paymentPayload));
1306
+ return encodeBase64Json(paymentPayload);
1307
+ }
1308
+ function encodeBase64Json(payload) {
1309
+ const serialized = JSON.stringify(payload);
1310
+ if (typeof Buffer !== "undefined") {
1311
+ return Buffer.from(serialized, "utf8").toString("base64");
1312
+ }
1313
+ if (typeof btoa !== "undefined") {
1314
+ return btoa(serialized);
1315
+ }
1316
+ throw new Error("[relai-x402] Base64 encoding is not available in this runtime");
637
1317
  }
638
1318
  function decodeBase64Json(encoded) {
639
1319
  try {
@@ -674,7 +1354,101 @@ function createX402Client(config) {
674
1354
  async function x402Fetch(input, init) {
675
1355
  const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
676
1356
  log("Request:", url);
677
- const response = await fetch(input, init);
1357
+ const requestInit = stripInternalInit(init);
1358
+ const integritasOptions = resolveIntegritasOptions(init?.x402?.integritas);
1359
+ const requestMethod = getRequestMethod(input, requestInit);
1360
+ const requestHeaders = applyIntegritasHeaders(
1361
+ getRequestHeaders(input, requestInit),
1362
+ integritasOptions
1363
+ );
1364
+ const requestInitWithHeaders = {
1365
+ ...requestInit || {},
1366
+ headers: requestHeaders
1367
+ };
1368
+ const requestBody = await resolveRequestBody(input, requestInitWithHeaders);
1369
+ if (relayWsEnabled && isRelayRequestUrl(url)) {
1370
+ let wsPaymentPhaseStarted = false;
1371
+ try {
1372
+ log("Using WebSocket relay transport");
1373
+ const wsPreflightResponse = await relayCallOverWebSocket({
1374
+ relayUrl: url,
1375
+ requestMethod,
1376
+ requestHeaders,
1377
+ requestBody,
1378
+ timeoutMs: relayWsPreflightTimeoutMs
1379
+ });
1380
+ if (!wsPreflightResponse.error) {
1381
+ return buildWsResponse(wsPreflightResponse);
1382
+ }
1383
+ if (Number(wsPreflightResponse.error.code) !== 402) {
1384
+ throw new Error(wsPreflightResponse.error.message || "[relai-x402] WebSocket relay request failed");
1385
+ }
1386
+ const wsRequirements = extractPaymentRequirementsFromWsError(wsPreflightResponse.error);
1387
+ if (!wsRequirements) {
1388
+ throw new Error(
1389
+ wsPreflightResponse.error.message || "[relai-x402] No payment requirements in WS 402 response"
1390
+ );
1391
+ }
1392
+ const wsAccepts = getAccepts(wsRequirements);
1393
+ if (!wsAccepts.length) {
1394
+ throw new Error("[relai-x402] No payment options in WS 402 response");
1395
+ }
1396
+ if (wsAccepts.length > 1) {
1397
+ throw new Error(
1398
+ "[relai-x402] WS relay currently supports a single payment payload; use HTTP flow for multi-accept payments"
1399
+ );
1400
+ }
1401
+ const wsSelected = selectAccept(wsAccepts);
1402
+ if (!wsSelected) {
1403
+ const networks = wsAccepts.map((a) => a.network).join(", ");
1404
+ throw new Error(`[relai-x402] No wallet available for WS networks: ${networks}`);
1405
+ }
1406
+ const { accept: accept2, chain: chain2 } = wsSelected;
1407
+ const amount2 = accept2.amount || accept2.maxAmountRequired;
1408
+ if (maxAmountAtomic && BigInt(amount2) > BigInt(maxAmountAtomic)) {
1409
+ throw new Error(`[relai-x402] Amount ${amount2} exceeds max ${maxAmountAtomic}`);
1410
+ }
1411
+ wsPaymentPhaseStarted = true;
1412
+ let paymentHeader = null;
1413
+ if (chain2 === "solana" && hasSolanaWallet) {
1414
+ paymentHeader = await buildSolanaPayment(accept2, wsRequirements, url);
1415
+ } else if (chain2 === "evm") {
1416
+ const evmNetwork = normalizeNetwork(accept2.network || "");
1417
+ const usePermit = evmNetwork && PERMIT_NETWORKS.has(evmNetwork);
1418
+ paymentHeader = usePermit ? await buildEvmPermitPayment(accept2, wsRequirements, url) : await buildEvmPayment(accept2, wsRequirements, url);
1419
+ }
1420
+ if (!paymentHeader) {
1421
+ throw new Error("[relai-x402] Unexpected state - no WS payment handler matched");
1422
+ }
1423
+ const paymentPayload = decodeBase64Json(paymentHeader);
1424
+ if (!paymentPayload) {
1425
+ throw new Error("[relai-x402] Failed to decode payment payload for WS relay call");
1426
+ }
1427
+ const wsPaidResponse = await relayCallOverWebSocket({
1428
+ relayUrl: url,
1429
+ requestMethod,
1430
+ requestHeaders,
1431
+ requestBody,
1432
+ paymentPayload,
1433
+ timeoutMs: relayWsPaymentTimeoutMs
1434
+ });
1435
+ if (wsPaidResponse.error) {
1436
+ throw new Error(wsPaidResponse.error.message || "[relai-x402] WebSocket paid relay request failed");
1437
+ }
1438
+ return buildWsResponse(wsPaidResponse);
1439
+ } catch (wsError) {
1440
+ const wsMessage = wsError instanceof Error ? wsError.message : String(wsError);
1441
+ log("WebSocket relay transport failed:", wsMessage);
1442
+ if (wsPaymentPhaseStarted) {
1443
+ throw wsError instanceof Error ? wsError : new Error(`[relai-x402] ${wsMessage}`);
1444
+ }
1445
+ if (!relayWsFallbackToHttp) {
1446
+ throw wsError instanceof Error ? wsError : new Error(`[relai-x402] ${wsMessage}`);
1447
+ }
1448
+ log("Falling back to HTTP x402 flow");
1449
+ }
1450
+ }
1451
+ const response = await fetch(input, requestInitWithHeaders);
678
1452
  if (response.status !== 402) return response;
679
1453
  log("Got 402 Payment Required");
680
1454
  let requirementsFromBody = null;
@@ -712,9 +1486,9 @@ function createX402Client(config) {
712
1486
  const paymentHeader = await buildSolanaPayment(accept, requirements, url);
713
1487
  log("Retrying with X-PAYMENT header (Solana)");
714
1488
  return fetch(input, {
715
- ...init,
1489
+ ...requestInitWithHeaders,
716
1490
  headers: {
717
- ...init?.headers || {},
1491
+ ...requestHeaders,
718
1492
  "X-PAYMENT": paymentHeader
719
1493
  }
720
1494
  });
@@ -725,14 +1499,14 @@ function createX402Client(config) {
725
1499
  const paymentHeader = usePermit ? await buildEvmPermitPayment(accept, requirements, url) : await buildEvmPayment(accept, requirements, url);
726
1500
  log("Retrying with X-PAYMENT header");
727
1501
  return fetch(input, {
728
- ...init,
1502
+ ...requestInitWithHeaders,
729
1503
  headers: {
730
- ...init?.headers || {},
1504
+ ...requestHeaders,
731
1505
  "X-PAYMENT": paymentHeader
732
1506
  }
733
1507
  });
734
1508
  }
735
- throw new Error("[relai-x402] Unexpected state \u2014 no payment handler matched");
1509
+ throw new Error("[relai-x402] Unexpected state - no payment handler matched");
736
1510
  }
737
1511
  return { fetch: x402Fetch };
738
1512
  }
@@ -881,6 +1655,7 @@ export {
881
1655
  EXPLORER_TX_URL,
882
1656
  NETWORK_CAIP2,
883
1657
  NETWORK_LABELS,
1658
+ NETWORK_TOKENS,
884
1659
  NETWORK_V1_TO_V2,
885
1660
  NETWORK_V2_TO_V1,
886
1661
  RELAI_FACILITATOR_URL,
@@ -906,6 +1681,7 @@ export {
906
1681
  networkV2ToV1,
907
1682
  normalizeNetwork,
908
1683
  normalizePaymentHeader,
1684
+ resolveToken,
909
1685
  stripePayTo,
910
1686
  toAtomicUnits
911
1687
  };