@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.
- package/dist/adapters/index.d.cts +4 -4
- package/dist/adapters/index.d.ts +4 -4
- package/dist/client/index.cjs +182 -4
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +23 -5
- package/dist/client/index.d.ts +23 -5
- package/dist/client/index.js +182 -4
- package/dist/client/index.js.map +1 -1
- package/dist/{evm-ZDwQi4QL.d.ts → evm-71SZ7cjW.d.ts} +2 -2
- package/dist/{evm-BaoETN1Y.d.cts → evm-BYjwU6ZW.d.cts} +2 -2
- package/dist/react/index.cjs +323 -3
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +108 -4
- package/dist/react/index.d.ts +108 -4
- package/dist/react/index.js +322 -3
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.cjs +253 -2
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +101 -3
- package/dist/server/index.d.ts +101 -3
- package/dist/server/index.js +240 -0
- package/dist/server/index.js.map +1 -1
- package/dist/{types-CQGDK_7X.d.cts → types--r7urkVI.d.cts} +1 -1
- package/dist/{types-DNx7-QUN.d.ts → types-BtpD4ULe.d.ts} +1 -1
- package/dist/{types-B7T6dZ-y.d.cts → types-CcVAaoro.d.cts} +64 -2
- package/dist/{types-B7T6dZ-y.d.ts → types-CcVAaoro.d.ts} +64 -2
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/{x402-client-COrn-FQk.d.ts → x402-client-BxQWcK2Z.d.ts} +8 -1
- package/dist/{x402-client-DHmpVhEK.d.cts → x402-client-Dcm2FQ9f.d.cts} +8 -1
- package/package.json +1 -1
package/dist/react/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { a as X402Client } from '../x402-client-
|
|
2
|
-
import { W as WalletSet, B as BalanceInfo } from '../types-
|
|
3
|
-
|
|
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
|
-
|
|
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 };
|
package/dist/react/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|