@dexterai/x402 1.4.1 → 1.5.1

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.
@@ -1,5 +1,5 @@
1
- import { A as AdapterConfig, C as ChainAdapter, S as SignedTransaction } from './types-DNx7-QUN.js';
2
- import { P as PaymentAccept } from './types-B7T6dZ-y.js';
1
+ import { A as AdapterConfig, C as ChainAdapter, S as SignedTransaction } from './types-BtpD4ULe.js';
2
+ import { P as PaymentAccept } from './types-CcVAaoro.js';
3
3
 
4
4
  /**
5
5
  * Solana Chain Adapter
@@ -1,5 +1,5 @@
1
- import { A as AdapterConfig, C as ChainAdapter, S as SignedTransaction } from './types-CQGDK_7X.cjs';
2
- import { P as PaymentAccept } from './types-B7T6dZ-y.cjs';
1
+ import { A as AdapterConfig, C as ChainAdapter, S as SignedTransaction } from './types--r7urkVI.cjs';
2
+ import { P as PaymentAccept } from './types-CcVAaoro.cjs';
3
3
 
4
4
  /**
5
5
  * Solana Chain Adapter
@@ -416,6 +416,7 @@ var init_evm = __esm({
416
416
  var react_exports = {};
417
417
  __export(react_exports, {
418
418
  X402Error: () => X402Error,
419
+ useAccessPass: () => useAccessPass,
419
420
  useX402Payment: () => useX402Payment
420
421
  });
421
422
  module.exports = __toCommonJS(react_exports);
@@ -456,10 +457,41 @@ function createX402Client(config) {
456
457
  rpcUrls = {},
457
458
  maxAmountAtomic,
458
459
  fetch: customFetch = globalThis.fetch,
459
- verbose = false
460
+ verbose = false,
461
+ accessPass: accessPassConfig
460
462
  } = config;
461
463
  const log = verbose ? console.log.bind(console, "[x402]") : () => {
462
464
  };
465
+ const passCache = /* @__PURE__ */ new Map();
466
+ function getCachedPass(url) {
467
+ try {
468
+ const host = new URL(url).host;
469
+ const cached = passCache.get(host);
470
+ if (cached && cached.expiresAt > Date.now() / 1e3 + 10) {
471
+ return cached.jwt;
472
+ }
473
+ if (cached) {
474
+ passCache.delete(host);
475
+ }
476
+ } catch {
477
+ }
478
+ return null;
479
+ }
480
+ function cachePass(url, jwt) {
481
+ try {
482
+ const host = new URL(url).host;
483
+ const parts = jwt.split(".");
484
+ if (parts.length === 3) {
485
+ const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
486
+ if (payload.exp) {
487
+ passCache.set(host, { jwt, expiresAt: payload.exp });
488
+ log("Access pass cached for", host, "| expires:", new Date(payload.exp * 1e3).toISOString());
489
+ }
490
+ }
491
+ } catch {
492
+ log("Failed to cache access pass");
493
+ }
494
+ }
463
495
  const wallets = walletSet || {};
464
496
  if (legacyWallet && !wallets.solana && isSolanaWallet(legacyWallet)) {
465
497
  wallets.solana = legacyWallet;
@@ -494,13 +526,160 @@ function createX402Client(config) {
494
526
  function getRpcUrl(network, adapter) {
495
527
  return rpcUrls[network] || adapter.getDefaultRpcUrl(network);
496
528
  }
529
+ async function purchaseAccessPass(input, init, originalResponse, passInfo, url) {
530
+ let tierQuery = "";
531
+ if (accessPassConfig?.preferTier && passInfo.tiers) {
532
+ const match2 = passInfo.tiers.find((t) => t.id === accessPassConfig.preferTier);
533
+ if (match2) {
534
+ if (accessPassConfig.maxSpend && parseFloat(match2.price) > parseFloat(accessPassConfig.maxSpend)) {
535
+ throw new X402Error(
536
+ "access_pass_exceeds_max_spend",
537
+ `Access pass tier "${match2.id}" costs $${match2.price}, exceeds max spend $${accessPassConfig.maxSpend}`
538
+ );
539
+ }
540
+ tierQuery = `tier=${match2.id}`;
541
+ }
542
+ } else if (accessPassConfig?.preferDuration && passInfo.ratePerHour) {
543
+ tierQuery = `duration=${accessPassConfig.preferDuration}`;
544
+ } else if (passInfo.tiers && passInfo.tiers.length > 0) {
545
+ const cheapest = passInfo.tiers[0];
546
+ if (accessPassConfig?.maxSpend && parseFloat(cheapest.price) > parseFloat(accessPassConfig.maxSpend)) {
547
+ throw new X402Error(
548
+ "access_pass_exceeds_max_spend",
549
+ `Cheapest access pass costs $${cheapest.price}, exceeds max spend $${accessPassConfig?.maxSpend}`
550
+ );
551
+ }
552
+ tierQuery = `tier=${cheapest.id}`;
553
+ }
554
+ const passUrl = tierQuery ? url.includes("?") ? `${url}&${tierQuery}` : `${url}?${tierQuery}` : url;
555
+ log("Purchasing access pass:", tierQuery || "default tier");
556
+ const paymentRequiredHeader = originalResponse.headers.get("PAYMENT-REQUIRED");
557
+ if (!paymentRequiredHeader) return null;
558
+ let requirements;
559
+ try {
560
+ requirements = JSON.parse(atob(paymentRequiredHeader));
561
+ } catch {
562
+ return null;
563
+ }
564
+ const match = findPaymentOption(requirements.accepts);
565
+ if (!match) return null;
566
+ const { accept, adapter, wallet } = match;
567
+ if (adapter.name === "Solana" && !accept.extra?.feePayer) return null;
568
+ const USDC_MINTS = [
569
+ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
570
+ "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
571
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
572
+ "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
573
+ ];
574
+ const decimals = accept.extra?.decimals ?? (USDC_MINTS.includes(accept.asset) ? 6 : void 0);
575
+ if (typeof decimals !== "number") return null;
576
+ const paymentAmount = accept.amount || accept.maxAmountRequired;
577
+ if (!paymentAmount) return null;
578
+ const rpcUrl = getRpcUrl(accept.network, adapter);
579
+ const balance = await adapter.getBalance(accept, wallet, rpcUrl);
580
+ const requiredAmount = Number(paymentAmount) / Math.pow(10, decimals);
581
+ if (balance < requiredAmount) {
582
+ throw new X402Error(
583
+ "insufficient_balance",
584
+ `Insufficient balance for access pass. Have $${balance.toFixed(4)}, need $${requiredAmount.toFixed(4)}`
585
+ );
586
+ }
587
+ const signedTx = await adapter.buildTransaction(accept, wallet, rpcUrl);
588
+ let payload;
589
+ if (adapter.name === "EVM") {
590
+ payload = JSON.parse(signedTx.serialized);
591
+ } else {
592
+ payload = { transaction: signedTx.serialized };
593
+ }
594
+ const originalUrl = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
595
+ let resolvedResource = requirements.resource;
596
+ if (typeof requirements.resource === "string") {
597
+ try {
598
+ resolvedResource = new URL(requirements.resource, originalUrl).toString();
599
+ } catch {
600
+ }
601
+ } else if (requirements.resource && typeof requirements.resource === "object" && "url" in requirements.resource) {
602
+ const rObj = requirements.resource;
603
+ try {
604
+ resolvedResource = { ...rObj, url: new URL(rObj.url, originalUrl).toString() };
605
+ } catch {
606
+ }
607
+ }
608
+ const paymentSignature = {
609
+ x402Version: accept.x402Version ?? 2,
610
+ resource: resolvedResource,
611
+ accepted: accept,
612
+ payload
613
+ };
614
+ const paymentSignatureHeader = btoa(JSON.stringify(paymentSignature));
615
+ const passResponse = await customFetch(passUrl, {
616
+ ...init,
617
+ method: "POST",
618
+ headers: {
619
+ ...init?.headers || {},
620
+ "Content-Type": "application/json",
621
+ "PAYMENT-SIGNATURE": paymentSignatureHeader
622
+ }
623
+ });
624
+ if (!passResponse.ok) {
625
+ log("Pass purchase failed:", passResponse.status);
626
+ return null;
627
+ }
628
+ const accessPassJwt = passResponse.headers.get("ACCESS-PASS");
629
+ if (!accessPassJwt) {
630
+ return passResponse;
631
+ }
632
+ cachePass(url, accessPassJwt);
633
+ log("Access pass purchased and cached");
634
+ const retryResponse = await customFetch(input, {
635
+ ...init,
636
+ headers: {
637
+ ...init?.headers || {},
638
+ "Authorization": `Bearer ${accessPassJwt}`
639
+ }
640
+ });
641
+ return retryResponse;
642
+ }
497
643
  async function x402Fetch(input, init) {
498
- log("Making request:", input);
644
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
645
+ log("Making request:", url);
646
+ if (accessPassConfig) {
647
+ const cachedJwt = getCachedPass(url);
648
+ if (cachedJwt) {
649
+ log("Using cached access pass");
650
+ const passResponse = await customFetch(input, {
651
+ ...init,
652
+ headers: {
653
+ ...init?.headers || {},
654
+ "Authorization": `Bearer ${cachedJwt}`
655
+ }
656
+ });
657
+ if (passResponse.status !== 401 && passResponse.status !== 402) {
658
+ return passResponse;
659
+ }
660
+ log("Cached pass rejected (status", passResponse.status, "), purchasing new pass");
661
+ try {
662
+ passCache.delete(new URL(url).host);
663
+ } catch {
664
+ }
665
+ }
666
+ }
499
667
  const response = await customFetch(input, init);
500
668
  if (response.status !== 402) {
501
669
  return response;
502
670
  }
503
671
  log("Received 402 Payment Required");
672
+ const passTiersHeader = response.headers.get("X-ACCESS-PASS-TIERS");
673
+ if (accessPassConfig && passTiersHeader) {
674
+ log("Server offers access passes, purchasing...");
675
+ try {
676
+ const passInfo = JSON.parse(atob(passTiersHeader));
677
+ const passResponse = await purchaseAccessPass(input, init, response, passInfo, url);
678
+ if (passResponse) return passResponse;
679
+ } catch (e) {
680
+ log("Access pass purchase failed, falling back to per-request payment:", e);
681
+ }
682
+ }
504
683
  const paymentRequiredHeader = response.headers.get("PAYMENT-REQUIRED");
505
684
  if (!paymentRequiredHeader) {
506
685
  throw new X402Error(
@@ -877,12 +1056,155 @@ function useX402Payment(config) {
877
1056
  connectedChains,
878
1057
  isAnyWalletConnected,
879
1058
  reset,
880
- refreshBalances
1059
+ refreshBalances,
1060
+ accessPass: null
1061
+ // Access pass state managed by useAccessPass hook for granular control
1062
+ };
1063
+ }
1064
+
1065
+ // src/react/useAccessPass.ts
1066
+ var import_react2 = require("react");
1067
+ function useAccessPass(config) {
1068
+ const {
1069
+ wallets: walletSet,
1070
+ wallet: legacyWallet,
1071
+ preferredNetwork,
1072
+ rpcUrls = {},
1073
+ resourceUrl,
1074
+ autoConnect = true,
1075
+ verbose = false
1076
+ } = config;
1077
+ const [tiers, setTiers] = (0, import_react2.useState)(null);
1078
+ const [customRatePerHour, setCustomRatePerHour] = (0, import_react2.useState)(null);
1079
+ const [isLoadingTiers, setIsLoadingTiers] = (0, import_react2.useState)(false);
1080
+ const [passJwt, setPassJwt] = (0, import_react2.useState)(null);
1081
+ const [passInfo, setPassInfo] = (0, import_react2.useState)(null);
1082
+ const [isPurchasing, setIsPurchasing] = (0, import_react2.useState)(false);
1083
+ const [purchaseError, setPurchaseError] = (0, import_react2.useState)(null);
1084
+ const log = (0, import_react2.useCallback)((...args) => {
1085
+ if (verbose) console.log("[useAccessPass]", ...args);
1086
+ }, [verbose]);
1087
+ const wallets = (0, import_react2.useMemo)(() => {
1088
+ const w = { ...walletSet };
1089
+ if (legacyWallet && !w.solana && isSolanaWallet(legacyWallet)) {
1090
+ w.solana = legacyWallet;
1091
+ }
1092
+ if (legacyWallet && !w.evm && isEvmWallet(legacyWallet)) {
1093
+ w.evm = legacyWallet;
1094
+ }
1095
+ return w;
1096
+ }, [walletSet, legacyWallet]);
1097
+ const client = (0, import_react2.useMemo)(() => createX402Client({
1098
+ adapters: [createSolanaAdapter({ verbose, rpcUrls }), createEvmAdapter({ verbose, rpcUrls })],
1099
+ wallets,
1100
+ preferredNetwork,
1101
+ rpcUrls,
1102
+ verbose,
1103
+ accessPass: { enabled: true, autoRenew: true }
1104
+ }), [wallets, preferredNetwork, rpcUrls, verbose]);
1105
+ const pass = (0, import_react2.useMemo)(() => {
1106
+ if (!passJwt || !passInfo) return null;
1107
+ const expiresAtMs = new Date(passInfo.expiresAt).getTime();
1108
+ const remaining = Math.max(0, Math.floor((expiresAtMs - Date.now()) / 1e3));
1109
+ if (remaining <= 0) return null;
1110
+ return { jwt: passJwt, tier: passInfo.tier, expiresAt: passInfo.expiresAt, remainingSeconds: remaining };
1111
+ }, [passJwt, passInfo]);
1112
+ const isPassValid = pass !== null && pass.remainingSeconds > 0;
1113
+ const [, setTick] = (0, import_react2.useState)(0);
1114
+ (0, import_react2.useEffect)(() => {
1115
+ if (!isPassValid) return;
1116
+ const interval = setInterval(() => setTick((t) => t + 1), 1e3);
1117
+ return () => clearInterval(interval);
1118
+ }, [isPassValid]);
1119
+ const fetchTiers = (0, import_react2.useCallback)(async () => {
1120
+ setIsLoadingTiers(true);
1121
+ try {
1122
+ const res = await fetch(resourceUrl);
1123
+ if (res.status === 402) {
1124
+ const tiersHeader = res.headers.get("X-ACCESS-PASS-TIERS");
1125
+ if (tiersHeader) {
1126
+ const info = JSON.parse(atob(tiersHeader));
1127
+ setTiers(info.tiers || null);
1128
+ setCustomRatePerHour(info.ratePerHour || null);
1129
+ log("Tier info loaded:", info);
1130
+ }
1131
+ }
1132
+ } catch (e) {
1133
+ log("Failed to fetch tiers:", e);
1134
+ } finally {
1135
+ setIsLoadingTiers(false);
1136
+ }
1137
+ }, [resourceUrl, log]);
1138
+ (0, import_react2.useEffect)(() => {
1139
+ if (autoConnect) fetchTiers();
1140
+ }, [autoConnect, fetchTiers]);
1141
+ const purchasePass = (0, import_react2.useCallback)(async (tier, durationSeconds) => {
1142
+ setIsPurchasing(true);
1143
+ setPurchaseError(null);
1144
+ try {
1145
+ let url = resourceUrl;
1146
+ if (tier) url += (url.includes("?") ? "&" : "?") + `tier=${tier}`;
1147
+ else if (durationSeconds) url += (url.includes("?") ? "&" : "?") + `duration=${durationSeconds}`;
1148
+ const res = await client.fetch(url, { method: "POST" });
1149
+ const jwt = res.headers.get("ACCESS-PASS");
1150
+ if (jwt) {
1151
+ setPassJwt(jwt);
1152
+ try {
1153
+ const body = await res.json();
1154
+ setPassInfo({
1155
+ tier: body.accessPass?.tier || tier || "unknown",
1156
+ expiresAt: body.accessPass?.expiresAt || ""
1157
+ });
1158
+ } catch {
1159
+ const parts = jwt.split(".");
1160
+ if (parts.length === 3) {
1161
+ const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
1162
+ setPassInfo({
1163
+ tier: payload.tier || tier || "unknown",
1164
+ expiresAt: new Date(payload.exp * 1e3).toISOString()
1165
+ });
1166
+ }
1167
+ }
1168
+ log("Pass purchased:", tier);
1169
+ }
1170
+ } catch (err) {
1171
+ const error = err instanceof Error ? err : new Error(String(err));
1172
+ setPurchaseError(error);
1173
+ throw error;
1174
+ } finally {
1175
+ setIsPurchasing(false);
1176
+ }
1177
+ }, [resourceUrl, client, log]);
1178
+ const fetchWithPass = (0, import_react2.useCallback)(async (path, init) => {
1179
+ const url = path.startsWith("http") ? path : `${resourceUrl.replace(/\/$/, "")}${path.startsWith("/") ? "" : "/"}${path}`;
1180
+ if (isPassValid && pass) {
1181
+ return fetch(url, {
1182
+ ...init,
1183
+ headers: {
1184
+ ...init?.headers || {},
1185
+ "Authorization": `Bearer ${pass.jwt}`
1186
+ }
1187
+ });
1188
+ }
1189
+ return client.fetch(url, init);
1190
+ }, [resourceUrl, isPassValid, pass, client]);
1191
+ return {
1192
+ tiers,
1193
+ customRatePerHour,
1194
+ isLoadingTiers,
1195
+ pass,
1196
+ isPassValid,
1197
+ fetchTiers,
1198
+ purchasePass,
1199
+ isPurchasing,
1200
+ purchaseError,
1201
+ fetch: fetchWithPass
881
1202
  };
882
1203
  }
883
1204
  // Annotate the CommonJS export names for ESM import in node:
884
1205
  0 && (module.exports = {
885
1206
  X402Error,
1207
+ useAccessPass,
886
1208
  useX402Payment
887
1209
  });
888
1210
  //# sourceMappingURL=index.cjs.map