@dexterai/x402 1.4.0 → 1.5.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.
@@ -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,158 @@ 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
+ headers: {
618
+ ...init?.headers || {},
619
+ "PAYMENT-SIGNATURE": paymentSignatureHeader
620
+ }
621
+ });
622
+ if (!passResponse.ok) {
623
+ log("Pass purchase failed:", passResponse.status);
624
+ return null;
625
+ }
626
+ const accessPassJwt = passResponse.headers.get("ACCESS-PASS");
627
+ if (!accessPassJwt) {
628
+ return passResponse;
629
+ }
630
+ cachePass(url, accessPassJwt);
631
+ log("Access pass purchased and cached");
632
+ const retryResponse = await customFetch(input, {
633
+ ...init,
634
+ headers: {
635
+ ...init?.headers || {},
636
+ "Authorization": `Bearer ${accessPassJwt}`
637
+ }
638
+ });
639
+ return retryResponse;
640
+ }
497
641
  async function x402Fetch(input, init) {
498
- log("Making request:", input);
642
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
643
+ log("Making request:", url);
644
+ if (accessPassConfig) {
645
+ const cachedJwt = getCachedPass(url);
646
+ if (cachedJwt) {
647
+ log("Using cached access pass");
648
+ const passResponse = await customFetch(input, {
649
+ ...init,
650
+ headers: {
651
+ ...init?.headers || {},
652
+ "Authorization": `Bearer ${cachedJwt}`
653
+ }
654
+ });
655
+ if (passResponse.status !== 401 && passResponse.status !== 402) {
656
+ return passResponse;
657
+ }
658
+ log("Cached pass rejected (status", passResponse.status, "), purchasing new pass");
659
+ try {
660
+ passCache.delete(new URL(url).host);
661
+ } catch {
662
+ }
663
+ }
664
+ }
499
665
  const response = await customFetch(input, init);
500
666
  if (response.status !== 402) {
501
667
  return response;
502
668
  }
503
669
  log("Received 402 Payment Required");
670
+ const passTiersHeader = response.headers.get("X-ACCESS-PASS-TIERS");
671
+ if (accessPassConfig && passTiersHeader) {
672
+ log("Server offers access passes, purchasing...");
673
+ try {
674
+ const passInfo = JSON.parse(atob(passTiersHeader));
675
+ const passResponse = await purchaseAccessPass(input, init, response, passInfo, url);
676
+ if (passResponse) return passResponse;
677
+ } catch (e) {
678
+ log("Access pass purchase failed, falling back to per-request payment:", e);
679
+ }
680
+ }
504
681
  const paymentRequiredHeader = response.headers.get("PAYMENT-REQUIRED");
505
682
  if (!paymentRequiredHeader) {
506
683
  throw new X402Error(
@@ -877,12 +1054,155 @@ function useX402Payment(config) {
877
1054
  connectedChains,
878
1055
  isAnyWalletConnected,
879
1056
  reset,
880
- refreshBalances
1057
+ refreshBalances,
1058
+ accessPass: null
1059
+ // Access pass state managed by useAccessPass hook for granular control
1060
+ };
1061
+ }
1062
+
1063
+ // src/react/useAccessPass.ts
1064
+ var import_react2 = require("react");
1065
+ function useAccessPass(config) {
1066
+ const {
1067
+ wallets: walletSet,
1068
+ wallet: legacyWallet,
1069
+ preferredNetwork,
1070
+ rpcUrls = {},
1071
+ resourceUrl,
1072
+ autoConnect = true,
1073
+ verbose = false
1074
+ } = config;
1075
+ const [tiers, setTiers] = (0, import_react2.useState)(null);
1076
+ const [customRatePerHour, setCustomRatePerHour] = (0, import_react2.useState)(null);
1077
+ const [isLoadingTiers, setIsLoadingTiers] = (0, import_react2.useState)(false);
1078
+ const [passJwt, setPassJwt] = (0, import_react2.useState)(null);
1079
+ const [passInfo, setPassInfo] = (0, import_react2.useState)(null);
1080
+ const [isPurchasing, setIsPurchasing] = (0, import_react2.useState)(false);
1081
+ const [purchaseError, setPurchaseError] = (0, import_react2.useState)(null);
1082
+ const log = (0, import_react2.useCallback)((...args) => {
1083
+ if (verbose) console.log("[useAccessPass]", ...args);
1084
+ }, [verbose]);
1085
+ const wallets = (0, import_react2.useMemo)(() => {
1086
+ const w = { ...walletSet };
1087
+ if (legacyWallet && !w.solana && isSolanaWallet(legacyWallet)) {
1088
+ w.solana = legacyWallet;
1089
+ }
1090
+ if (legacyWallet && !w.evm && isEvmWallet(legacyWallet)) {
1091
+ w.evm = legacyWallet;
1092
+ }
1093
+ return w;
1094
+ }, [walletSet, legacyWallet]);
1095
+ const client = (0, import_react2.useMemo)(() => createX402Client({
1096
+ adapters: [createSolanaAdapter({ verbose, rpcUrls }), createEvmAdapter({ verbose, rpcUrls })],
1097
+ wallets,
1098
+ preferredNetwork,
1099
+ rpcUrls,
1100
+ verbose,
1101
+ accessPass: { enabled: true, autoRenew: true }
1102
+ }), [wallets, preferredNetwork, rpcUrls, verbose]);
1103
+ const pass = (0, import_react2.useMemo)(() => {
1104
+ if (!passJwt || !passInfo) return null;
1105
+ const expiresAtMs = new Date(passInfo.expiresAt).getTime();
1106
+ const remaining = Math.max(0, Math.floor((expiresAtMs - Date.now()) / 1e3));
1107
+ if (remaining <= 0) return null;
1108
+ return { jwt: passJwt, tier: passInfo.tier, expiresAt: passInfo.expiresAt, remainingSeconds: remaining };
1109
+ }, [passJwt, passInfo]);
1110
+ const isPassValid = pass !== null && pass.remainingSeconds > 0;
1111
+ const [, setTick] = (0, import_react2.useState)(0);
1112
+ (0, import_react2.useEffect)(() => {
1113
+ if (!isPassValid) return;
1114
+ const interval = setInterval(() => setTick((t) => t + 1), 1e3);
1115
+ return () => clearInterval(interval);
1116
+ }, [isPassValid]);
1117
+ const fetchTiers = (0, import_react2.useCallback)(async () => {
1118
+ setIsLoadingTiers(true);
1119
+ try {
1120
+ const res = await fetch(resourceUrl);
1121
+ if (res.status === 402) {
1122
+ const tiersHeader = res.headers.get("X-ACCESS-PASS-TIERS");
1123
+ if (tiersHeader) {
1124
+ const info = JSON.parse(atob(tiersHeader));
1125
+ setTiers(info.tiers || null);
1126
+ setCustomRatePerHour(info.ratePerHour || null);
1127
+ log("Tier info loaded:", info);
1128
+ }
1129
+ }
1130
+ } catch (e) {
1131
+ log("Failed to fetch tiers:", e);
1132
+ } finally {
1133
+ setIsLoadingTiers(false);
1134
+ }
1135
+ }, [resourceUrl, log]);
1136
+ (0, import_react2.useEffect)(() => {
1137
+ if (autoConnect) fetchTiers();
1138
+ }, [autoConnect, fetchTiers]);
1139
+ const purchasePass = (0, import_react2.useCallback)(async (tier, durationSeconds) => {
1140
+ setIsPurchasing(true);
1141
+ setPurchaseError(null);
1142
+ try {
1143
+ let url = resourceUrl;
1144
+ if (tier) url += (url.includes("?") ? "&" : "?") + `tier=${tier}`;
1145
+ else if (durationSeconds) url += (url.includes("?") ? "&" : "?") + `duration=${durationSeconds}`;
1146
+ const res = await client.fetch(url);
1147
+ const jwt = res.headers.get("ACCESS-PASS");
1148
+ if (jwt) {
1149
+ setPassJwt(jwt);
1150
+ try {
1151
+ const body = await res.json();
1152
+ setPassInfo({
1153
+ tier: body.accessPass?.tier || tier || "unknown",
1154
+ expiresAt: body.accessPass?.expiresAt || ""
1155
+ });
1156
+ } catch {
1157
+ const parts = jwt.split(".");
1158
+ if (parts.length === 3) {
1159
+ const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
1160
+ setPassInfo({
1161
+ tier: payload.tier || tier || "unknown",
1162
+ expiresAt: new Date(payload.exp * 1e3).toISOString()
1163
+ });
1164
+ }
1165
+ }
1166
+ log("Pass purchased:", tier);
1167
+ }
1168
+ } catch (err) {
1169
+ const error = err instanceof Error ? err : new Error(String(err));
1170
+ setPurchaseError(error);
1171
+ throw error;
1172
+ } finally {
1173
+ setIsPurchasing(false);
1174
+ }
1175
+ }, [resourceUrl, client, log]);
1176
+ const fetchWithPass = (0, import_react2.useCallback)(async (path, init) => {
1177
+ const url = path.startsWith("http") ? path : `${resourceUrl.replace(/\/$/, "")}${path.startsWith("/") ? "" : "/"}${path}`;
1178
+ if (isPassValid && pass) {
1179
+ return fetch(url, {
1180
+ ...init,
1181
+ headers: {
1182
+ ...init?.headers || {},
1183
+ "Authorization": `Bearer ${pass.jwt}`
1184
+ }
1185
+ });
1186
+ }
1187
+ return client.fetch(url, init);
1188
+ }, [resourceUrl, isPassValid, pass, client]);
1189
+ return {
1190
+ tiers,
1191
+ customRatePerHour,
1192
+ isLoadingTiers,
1193
+ pass,
1194
+ isPassValid,
1195
+ fetchTiers,
1196
+ purchasePass,
1197
+ isPurchasing,
1198
+ purchaseError,
1199
+ fetch: fetchWithPass
881
1200
  };
882
1201
  }
883
1202
  // Annotate the CommonJS export names for ESM import in node:
884
1203
  0 && (module.exports = {
885
1204
  X402Error,
1205
+ useAccessPass,
886
1206
  useX402Payment
887
1207
  });
888
1208
  //# sourceMappingURL=index.cjs.map