@dexterai/x402 1.4.1 → 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,6 +1,7 @@
1
- import { a as X402Client } from '../x402-client-COrn-FQk.js';
2
- import { W as WalletSet, B as BalanceInfo } from '../types-DNx7-QUN.js';
3
- export { X as X402Error } from '../types-B7T6dZ-y.js';
1
+ import { a as X402Client } from '../x402-client-BxQWcK2Z.js';
2
+ import { W as WalletSet, B as BalanceInfo } from '../types-BtpD4ULe.js';
3
+ import { a as AccessPassTier } from '../types-CcVAaoro.js';
4
+ export { A as AccessPassClientConfig, b as AccessPassInfo, X as X402Error } from '../types-CcVAaoro.js';
4
5
 
5
6
  /**
6
7
  * React Hook for x402 v2 Payments
@@ -99,10 +100,113 @@ interface UseX402PaymentReturn {
99
100
  reset: () => void;
100
101
  /** Refresh balances manually */
101
102
  refreshBalances: () => Promise<void>;
103
+ /**
104
+ * Access pass state (null if server doesn't support access passes
105
+ * or no pass has been purchased in this session).
106
+ */
107
+ accessPass: {
108
+ /** Whether a valid, non-expired pass is active */
109
+ active: boolean;
110
+ /** Tier ID of the active pass */
111
+ tier: string | null;
112
+ /** ISO expiration timestamp */
113
+ expiresAt: string | null;
114
+ /** Seconds remaining on the pass */
115
+ remainingSeconds: number | null;
116
+ } | null;
102
117
  }
103
118
  /**
104
119
  * React hook for managing x402 v2 payments across chains
105
120
  */
106
121
  declare function useX402Payment(config: UseX402PaymentConfig): UseX402PaymentReturn;
107
122
 
108
- export { BalanceInfo, type ConnectedChains, type PaymentStatus, type UseX402PaymentConfig, type UseX402PaymentReturn, WalletSet, useX402Payment };
123
+ /**
124
+ * React Hook for x402 Access Pass
125
+ *
126
+ * Dedicated hook for managing the access pass lifecycle:
127
+ * tier discovery, pass purchase, token caching, and auto-fetch with pass.
128
+ *
129
+ * @example
130
+ * ```tsx
131
+ * import { useAccessPass } from '@dexterai/x402/react';
132
+ *
133
+ * function DataDashboard() {
134
+ * const {
135
+ * tiers,
136
+ * pass,
137
+ * isPassValid,
138
+ * purchasePass,
139
+ * isPurchasing,
140
+ * fetch: apFetch,
141
+ * } = useAccessPass({
142
+ * wallets: { solana: solanaWallet },
143
+ * resourceUrl: 'https://api.example.com',
144
+ * });
145
+ *
146
+ * return (
147
+ * <div>
148
+ * {!isPassValid && tiers && (
149
+ * <div>
150
+ * {tiers.map(t => (
151
+ * <button key={t.id} onClick={() => purchasePass(t.id)}>
152
+ * {t.label} — ${t.price}
153
+ * </button>
154
+ * ))}
155
+ * </div>
156
+ * )}
157
+ * {isPassValid && <p>Pass active! Expires: {pass?.expiresAt}</p>}
158
+ * <button onClick={() => apFetch('/api/data').then(r => r.json()).then(console.log)}>
159
+ * Fetch Data
160
+ * </button>
161
+ * </div>
162
+ * );
163
+ * }
164
+ * ```
165
+ */
166
+
167
+ interface UseAccessPassConfig {
168
+ /** Wallets for each chain type */
169
+ wallets?: WalletSet;
170
+ /** Legacy: Single Solana wallet */
171
+ wallet?: unknown;
172
+ /** Preferred network */
173
+ preferredNetwork?: string;
174
+ /** Custom RPC URLs by network */
175
+ rpcUrls?: Record<string, string>;
176
+ /** The base URL of the x402 resource */
177
+ resourceUrl: string;
178
+ /** Auto-fetch tier info on mount @default true */
179
+ autoConnect?: boolean;
180
+ /** Enable verbose logging */
181
+ verbose?: boolean;
182
+ }
183
+ interface UseAccessPassReturn {
184
+ /** Available tiers from the server (null until fetched) */
185
+ tiers: AccessPassTier[] | null;
186
+ /** Custom rate per hour (if server supports custom durations) */
187
+ customRatePerHour: string | null;
188
+ /** Whether tier info is being loaded */
189
+ isLoadingTiers: boolean;
190
+ /** Current active pass (null if no valid pass) */
191
+ pass: {
192
+ jwt: string;
193
+ tier: string;
194
+ expiresAt: string;
195
+ remainingSeconds: number;
196
+ } | null;
197
+ /** Whether the current pass is valid and not expired */
198
+ isPassValid: boolean;
199
+ /** Fetch tier info from the server */
200
+ fetchTiers: () => Promise<void>;
201
+ /** Purchase a pass for a specific tier or custom duration */
202
+ purchasePass: (tier?: string, durationSeconds?: number) => Promise<void>;
203
+ /** Whether a pass purchase is in progress */
204
+ isPurchasing: boolean;
205
+ /** Error from the last purchase attempt */
206
+ purchaseError: Error | null;
207
+ /** Fetch with automatic pass inclusion */
208
+ fetch: (path: string, init?: RequestInit) => Promise<Response>;
209
+ }
210
+ declare function useAccessPass(config: UseAccessPassConfig): UseAccessPassReturn;
211
+
212
+ export { AccessPassTier, BalanceInfo, type ConnectedChains, type PaymentStatus, type UseAccessPassConfig, type UseAccessPassReturn, type UseX402PaymentConfig, type UseX402PaymentReturn, WalletSet, useAccessPass, useX402Payment };
@@ -444,10 +444,41 @@ function createX402Client(config) {
444
444
  rpcUrls = {},
445
445
  maxAmountAtomic,
446
446
  fetch: customFetch = globalThis.fetch,
447
- verbose = false
447
+ verbose = false,
448
+ accessPass: accessPassConfig
448
449
  } = config;
449
450
  const log = verbose ? console.log.bind(console, "[x402]") : () => {
450
451
  };
452
+ const passCache = /* @__PURE__ */ new Map();
453
+ function getCachedPass(url) {
454
+ try {
455
+ const host = new URL(url).host;
456
+ const cached = passCache.get(host);
457
+ if (cached && cached.expiresAt > Date.now() / 1e3 + 10) {
458
+ return cached.jwt;
459
+ }
460
+ if (cached) {
461
+ passCache.delete(host);
462
+ }
463
+ } catch {
464
+ }
465
+ return null;
466
+ }
467
+ function cachePass(url, jwt) {
468
+ try {
469
+ const host = new URL(url).host;
470
+ const parts = jwt.split(".");
471
+ if (parts.length === 3) {
472
+ const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
473
+ if (payload.exp) {
474
+ passCache.set(host, { jwt, expiresAt: payload.exp });
475
+ log("Access pass cached for", host, "| expires:", new Date(payload.exp * 1e3).toISOString());
476
+ }
477
+ }
478
+ } catch {
479
+ log("Failed to cache access pass");
480
+ }
481
+ }
451
482
  const wallets = walletSet || {};
452
483
  if (legacyWallet && !wallets.solana && isSolanaWallet(legacyWallet)) {
453
484
  wallets.solana = legacyWallet;
@@ -482,13 +513,158 @@ function createX402Client(config) {
482
513
  function getRpcUrl(network, adapter) {
483
514
  return rpcUrls[network] || adapter.getDefaultRpcUrl(network);
484
515
  }
516
+ async function purchaseAccessPass(input, init, originalResponse, passInfo, url) {
517
+ let tierQuery = "";
518
+ if (accessPassConfig?.preferTier && passInfo.tiers) {
519
+ const match2 = passInfo.tiers.find((t) => t.id === accessPassConfig.preferTier);
520
+ if (match2) {
521
+ if (accessPassConfig.maxSpend && parseFloat(match2.price) > parseFloat(accessPassConfig.maxSpend)) {
522
+ throw new X402Error(
523
+ "access_pass_exceeds_max_spend",
524
+ `Access pass tier "${match2.id}" costs $${match2.price}, exceeds max spend $${accessPassConfig.maxSpend}`
525
+ );
526
+ }
527
+ tierQuery = `tier=${match2.id}`;
528
+ }
529
+ } else if (accessPassConfig?.preferDuration && passInfo.ratePerHour) {
530
+ tierQuery = `duration=${accessPassConfig.preferDuration}`;
531
+ } else if (passInfo.tiers && passInfo.tiers.length > 0) {
532
+ const cheapest = passInfo.tiers[0];
533
+ if (accessPassConfig?.maxSpend && parseFloat(cheapest.price) > parseFloat(accessPassConfig.maxSpend)) {
534
+ throw new X402Error(
535
+ "access_pass_exceeds_max_spend",
536
+ `Cheapest access pass costs $${cheapest.price}, exceeds max spend $${accessPassConfig?.maxSpend}`
537
+ );
538
+ }
539
+ tierQuery = `tier=${cheapest.id}`;
540
+ }
541
+ const passUrl = tierQuery ? url.includes("?") ? `${url}&${tierQuery}` : `${url}?${tierQuery}` : url;
542
+ log("Purchasing access pass:", tierQuery || "default tier");
543
+ const paymentRequiredHeader = originalResponse.headers.get("PAYMENT-REQUIRED");
544
+ if (!paymentRequiredHeader) return null;
545
+ let requirements;
546
+ try {
547
+ requirements = JSON.parse(atob(paymentRequiredHeader));
548
+ } catch {
549
+ return null;
550
+ }
551
+ const match = findPaymentOption(requirements.accepts);
552
+ if (!match) return null;
553
+ const { accept, adapter, wallet } = match;
554
+ if (adapter.name === "Solana" && !accept.extra?.feePayer) return null;
555
+ const USDC_MINTS = [
556
+ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
557
+ "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
558
+ "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
559
+ "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
560
+ ];
561
+ const decimals = accept.extra?.decimals ?? (USDC_MINTS.includes(accept.asset) ? 6 : void 0);
562
+ if (typeof decimals !== "number") return null;
563
+ const paymentAmount = accept.amount || accept.maxAmountRequired;
564
+ if (!paymentAmount) return null;
565
+ const rpcUrl = getRpcUrl(accept.network, adapter);
566
+ const balance = await adapter.getBalance(accept, wallet, rpcUrl);
567
+ const requiredAmount = Number(paymentAmount) / Math.pow(10, decimals);
568
+ if (balance < requiredAmount) {
569
+ throw new X402Error(
570
+ "insufficient_balance",
571
+ `Insufficient balance for access pass. Have $${balance.toFixed(4)}, need $${requiredAmount.toFixed(4)}`
572
+ );
573
+ }
574
+ const signedTx = await adapter.buildTransaction(accept, wallet, rpcUrl);
575
+ let payload;
576
+ if (adapter.name === "EVM") {
577
+ payload = JSON.parse(signedTx.serialized);
578
+ } else {
579
+ payload = { transaction: signedTx.serialized };
580
+ }
581
+ const originalUrl = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
582
+ let resolvedResource = requirements.resource;
583
+ if (typeof requirements.resource === "string") {
584
+ try {
585
+ resolvedResource = new URL(requirements.resource, originalUrl).toString();
586
+ } catch {
587
+ }
588
+ } else if (requirements.resource && typeof requirements.resource === "object" && "url" in requirements.resource) {
589
+ const rObj = requirements.resource;
590
+ try {
591
+ resolvedResource = { ...rObj, url: new URL(rObj.url, originalUrl).toString() };
592
+ } catch {
593
+ }
594
+ }
595
+ const paymentSignature = {
596
+ x402Version: accept.x402Version ?? 2,
597
+ resource: resolvedResource,
598
+ accepted: accept,
599
+ payload
600
+ };
601
+ const paymentSignatureHeader = btoa(JSON.stringify(paymentSignature));
602
+ const passResponse = await customFetch(passUrl, {
603
+ ...init,
604
+ headers: {
605
+ ...init?.headers || {},
606
+ "PAYMENT-SIGNATURE": paymentSignatureHeader
607
+ }
608
+ });
609
+ if (!passResponse.ok) {
610
+ log("Pass purchase failed:", passResponse.status);
611
+ return null;
612
+ }
613
+ const accessPassJwt = passResponse.headers.get("ACCESS-PASS");
614
+ if (!accessPassJwt) {
615
+ return passResponse;
616
+ }
617
+ cachePass(url, accessPassJwt);
618
+ log("Access pass purchased and cached");
619
+ const retryResponse = await customFetch(input, {
620
+ ...init,
621
+ headers: {
622
+ ...init?.headers || {},
623
+ "Authorization": `Bearer ${accessPassJwt}`
624
+ }
625
+ });
626
+ return retryResponse;
627
+ }
485
628
  async function x402Fetch(input, init) {
486
- log("Making request:", input);
629
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
630
+ log("Making request:", url);
631
+ if (accessPassConfig) {
632
+ const cachedJwt = getCachedPass(url);
633
+ if (cachedJwt) {
634
+ log("Using cached access pass");
635
+ const passResponse = await customFetch(input, {
636
+ ...init,
637
+ headers: {
638
+ ...init?.headers || {},
639
+ "Authorization": `Bearer ${cachedJwt}`
640
+ }
641
+ });
642
+ if (passResponse.status !== 401 && passResponse.status !== 402) {
643
+ return passResponse;
644
+ }
645
+ log("Cached pass rejected (status", passResponse.status, "), purchasing new pass");
646
+ try {
647
+ passCache.delete(new URL(url).host);
648
+ } catch {
649
+ }
650
+ }
651
+ }
487
652
  const response = await customFetch(input, init);
488
653
  if (response.status !== 402) {
489
654
  return response;
490
655
  }
491
656
  log("Received 402 Payment Required");
657
+ const passTiersHeader = response.headers.get("X-ACCESS-PASS-TIERS");
658
+ if (accessPassConfig && passTiersHeader) {
659
+ log("Server offers access passes, purchasing...");
660
+ try {
661
+ const passInfo = JSON.parse(atob(passTiersHeader));
662
+ const passResponse = await purchaseAccessPass(input, init, response, passInfo, url);
663
+ if (passResponse) return passResponse;
664
+ } catch (e) {
665
+ log("Access pass purchase failed, falling back to per-request payment:", e);
666
+ }
667
+ }
492
668
  const paymentRequiredHeader = response.headers.get("PAYMENT-REQUIRED");
493
669
  if (!paymentRequiredHeader) {
494
670
  throw new X402Error(
@@ -865,11 +1041,154 @@ function useX402Payment(config) {
865
1041
  connectedChains,
866
1042
  isAnyWalletConnected,
867
1043
  reset,
868
- refreshBalances
1044
+ refreshBalances,
1045
+ accessPass: null
1046
+ // Access pass state managed by useAccessPass hook for granular control
1047
+ };
1048
+ }
1049
+
1050
+ // src/react/useAccessPass.ts
1051
+ import { useState as useState2, useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2 } from "react";
1052
+ function useAccessPass(config) {
1053
+ const {
1054
+ wallets: walletSet,
1055
+ wallet: legacyWallet,
1056
+ preferredNetwork,
1057
+ rpcUrls = {},
1058
+ resourceUrl,
1059
+ autoConnect = true,
1060
+ verbose = false
1061
+ } = config;
1062
+ const [tiers, setTiers] = useState2(null);
1063
+ const [customRatePerHour, setCustomRatePerHour] = useState2(null);
1064
+ const [isLoadingTiers, setIsLoadingTiers] = useState2(false);
1065
+ const [passJwt, setPassJwt] = useState2(null);
1066
+ const [passInfo, setPassInfo] = useState2(null);
1067
+ const [isPurchasing, setIsPurchasing] = useState2(false);
1068
+ const [purchaseError, setPurchaseError] = useState2(null);
1069
+ const log = useCallback2((...args) => {
1070
+ if (verbose) console.log("[useAccessPass]", ...args);
1071
+ }, [verbose]);
1072
+ const wallets = useMemo2(() => {
1073
+ const w = { ...walletSet };
1074
+ if (legacyWallet && !w.solana && isSolanaWallet(legacyWallet)) {
1075
+ w.solana = legacyWallet;
1076
+ }
1077
+ if (legacyWallet && !w.evm && isEvmWallet(legacyWallet)) {
1078
+ w.evm = legacyWallet;
1079
+ }
1080
+ return w;
1081
+ }, [walletSet, legacyWallet]);
1082
+ const client = useMemo2(() => createX402Client({
1083
+ adapters: [createSolanaAdapter({ verbose, rpcUrls }), createEvmAdapter({ verbose, rpcUrls })],
1084
+ wallets,
1085
+ preferredNetwork,
1086
+ rpcUrls,
1087
+ verbose,
1088
+ accessPass: { enabled: true, autoRenew: true }
1089
+ }), [wallets, preferredNetwork, rpcUrls, verbose]);
1090
+ const pass = useMemo2(() => {
1091
+ if (!passJwt || !passInfo) return null;
1092
+ const expiresAtMs = new Date(passInfo.expiresAt).getTime();
1093
+ const remaining = Math.max(0, Math.floor((expiresAtMs - Date.now()) / 1e3));
1094
+ if (remaining <= 0) return null;
1095
+ return { jwt: passJwt, tier: passInfo.tier, expiresAt: passInfo.expiresAt, remainingSeconds: remaining };
1096
+ }, [passJwt, passInfo]);
1097
+ const isPassValid = pass !== null && pass.remainingSeconds > 0;
1098
+ const [, setTick] = useState2(0);
1099
+ useEffect2(() => {
1100
+ if (!isPassValid) return;
1101
+ const interval = setInterval(() => setTick((t) => t + 1), 1e3);
1102
+ return () => clearInterval(interval);
1103
+ }, [isPassValid]);
1104
+ const fetchTiers = useCallback2(async () => {
1105
+ setIsLoadingTiers(true);
1106
+ try {
1107
+ const res = await fetch(resourceUrl);
1108
+ if (res.status === 402) {
1109
+ const tiersHeader = res.headers.get("X-ACCESS-PASS-TIERS");
1110
+ if (tiersHeader) {
1111
+ const info = JSON.parse(atob(tiersHeader));
1112
+ setTiers(info.tiers || null);
1113
+ setCustomRatePerHour(info.ratePerHour || null);
1114
+ log("Tier info loaded:", info);
1115
+ }
1116
+ }
1117
+ } catch (e) {
1118
+ log("Failed to fetch tiers:", e);
1119
+ } finally {
1120
+ setIsLoadingTiers(false);
1121
+ }
1122
+ }, [resourceUrl, log]);
1123
+ useEffect2(() => {
1124
+ if (autoConnect) fetchTiers();
1125
+ }, [autoConnect, fetchTiers]);
1126
+ const purchasePass = useCallback2(async (tier, durationSeconds) => {
1127
+ setIsPurchasing(true);
1128
+ setPurchaseError(null);
1129
+ try {
1130
+ let url = resourceUrl;
1131
+ if (tier) url += (url.includes("?") ? "&" : "?") + `tier=${tier}`;
1132
+ else if (durationSeconds) url += (url.includes("?") ? "&" : "?") + `duration=${durationSeconds}`;
1133
+ const res = await client.fetch(url);
1134
+ const jwt = res.headers.get("ACCESS-PASS");
1135
+ if (jwt) {
1136
+ setPassJwt(jwt);
1137
+ try {
1138
+ const body = await res.json();
1139
+ setPassInfo({
1140
+ tier: body.accessPass?.tier || tier || "unknown",
1141
+ expiresAt: body.accessPass?.expiresAt || ""
1142
+ });
1143
+ } catch {
1144
+ const parts = jwt.split(".");
1145
+ if (parts.length === 3) {
1146
+ const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
1147
+ setPassInfo({
1148
+ tier: payload.tier || tier || "unknown",
1149
+ expiresAt: new Date(payload.exp * 1e3).toISOString()
1150
+ });
1151
+ }
1152
+ }
1153
+ log("Pass purchased:", tier);
1154
+ }
1155
+ } catch (err) {
1156
+ const error = err instanceof Error ? err : new Error(String(err));
1157
+ setPurchaseError(error);
1158
+ throw error;
1159
+ } finally {
1160
+ setIsPurchasing(false);
1161
+ }
1162
+ }, [resourceUrl, client, log]);
1163
+ const fetchWithPass = useCallback2(async (path, init) => {
1164
+ const url = path.startsWith("http") ? path : `${resourceUrl.replace(/\/$/, "")}${path.startsWith("/") ? "" : "/"}${path}`;
1165
+ if (isPassValid && pass) {
1166
+ return fetch(url, {
1167
+ ...init,
1168
+ headers: {
1169
+ ...init?.headers || {},
1170
+ "Authorization": `Bearer ${pass.jwt}`
1171
+ }
1172
+ });
1173
+ }
1174
+ return client.fetch(url, init);
1175
+ }, [resourceUrl, isPassValid, pass, client]);
1176
+ return {
1177
+ tiers,
1178
+ customRatePerHour,
1179
+ isLoadingTiers,
1180
+ pass,
1181
+ isPassValid,
1182
+ fetchTiers,
1183
+ purchasePass,
1184
+ isPurchasing,
1185
+ purchaseError,
1186
+ fetch: fetchWithPass
869
1187
  };
870
1188
  }
871
1189
  export {
872
1190
  X402Error,
1191
+ useAccessPass,
873
1192
  useX402Payment
874
1193
  };
875
1194
  //# sourceMappingURL=index.js.map