@hyperyai/sdk 1.0.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/LICENSE +13 -0
- package/README.md +391 -0
- package/dist/components/AuthButton.d.ts +51 -0
- package/dist/components/AuthButton.js +60 -0
- package/dist/components/AuthModal.d.ts +20 -0
- package/dist/components/AuthModal.js +62 -0
- package/dist/components/BuyButton.d.ts +47 -0
- package/dist/components/BuyButton.js +74 -0
- package/dist/components/ErrorBoundary.d.ts +13 -0
- package/dist/components/ErrorBoundary.js +24 -0
- package/dist/components/HyperyModals.d.ts +30 -0
- package/dist/components/HyperyModals.js +30 -0
- package/dist/components/InsufficientCreditsAlert.d.ts +11 -0
- package/dist/components/InsufficientCreditsAlert.js +12 -0
- package/dist/components/ModernAuthForm.d.ts +52 -0
- package/dist/components/ModernAuthForm.js +83 -0
- package/dist/components/RestrictionModal.d.ts +43 -0
- package/dist/components/RestrictionModal.js +167 -0
- package/dist/components/SignIn.d.ts +26 -0
- package/dist/components/SignIn.js +47 -0
- package/dist/components/SignInForm.d.ts +41 -0
- package/dist/components/SignInForm.js +86 -0
- package/dist/components/SignUp.d.ts +26 -0
- package/dist/components/SignUp.js +52 -0
- package/dist/components/SpendingLimitAlert.d.ts +12 -0
- package/dist/components/SpendingLimitAlert.js +14 -0
- package/dist/components/UserButton.d.ts +26 -0
- package/dist/components/UserButton.js +35 -0
- package/dist/components/UserProfile.d.ts +22 -0
- package/dist/components/UserProfile.js +26 -0
- package/dist/components/WorkspaceSwitcher.d.ts +29 -0
- package/dist/components/WorkspaceSwitcher.js +66 -0
- package/dist/components/control.d.ts +64 -0
- package/dist/components/control.js +89 -0
- package/dist/components/ui/dialog.d.ts +19 -0
- package/dist/components/ui/dialog.js +53 -0
- package/dist/hooks/index.d.ts +22 -0
- package/dist/hooks/index.js +23 -0
- package/dist/hooks/useBuyerWallet.d.ts +37 -0
- package/dist/hooks/useBuyerWallet.js +66 -0
- package/dist/hooks/useCheckout.d.ts +27 -0
- package/dist/hooks/useCheckout.js +246 -0
- package/dist/hooks/useError.d.ts +19 -0
- package/dist/hooks/useError.js +36 -0
- package/dist/hooks/useMarketplace.d.ts +50 -0
- package/dist/hooks/useMarketplace.js +69 -0
- package/dist/hooks/useMemberships.d.ts +71 -0
- package/dist/hooks/useMemberships.js +138 -0
- package/dist/hooks/useWallet.d.ts +62 -0
- package/dist/hooks/useWallet.js +123 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +52 -0
- package/dist/lib/context.d.ts +50 -0
- package/dist/lib/context.js +409 -0
- package/dist/lib/oauth.d.ts +49 -0
- package/dist/lib/oauth.js +149 -0
- package/dist/lib/parse-error.d.ts +45 -0
- package/dist/lib/parse-error.js +185 -0
- package/dist/lib/parse-stream.d.ts +43 -0
- package/dist/lib/parse-stream.js +89 -0
- package/dist/lib/popup.d.ts +42 -0
- package/dist/lib/popup.js +80 -0
- package/dist/lib/storage.d.ts +43 -0
- package/dist/lib/storage.js +125 -0
- package/dist/lib/utils.d.ts +8 -0
- package/dist/lib/utils.js +11 -0
- package/dist/types/index.d.ts +191 -0
- package/dist/types/index.js +4 -0
- package/package.json +78 -0
- package/src/components/AuthButton.tsx +123 -0
- package/src/components/AuthModal.tsx +240 -0
- package/src/components/BuyButton.tsx +150 -0
- package/src/components/ErrorBoundary.tsx +97 -0
- package/src/components/HyperyModals.tsx +83 -0
- package/src/components/InsufficientCreditsAlert.tsx +73 -0
- package/src/components/ModernAuthForm.tsx +322 -0
- package/src/components/RestrictionModal.tsx +373 -0
- package/src/components/SignIn.tsx +84 -0
- package/src/components/SignInForm.tsx +256 -0
- package/src/components/SignUp.tsx +86 -0
- package/src/components/SpendingLimitAlert.tsx +91 -0
- package/src/components/UserButton.tsx +106 -0
- package/src/components/UserProfile.tsx +106 -0
- package/src/components/WorkspaceSwitcher.tsx +199 -0
- package/src/components/control.tsx +133 -0
- package/src/components/ui/dialog.tsx +151 -0
- package/src/hooks/index.ts +65 -0
- package/src/hooks/useBuyerWallet.ts +117 -0
- package/src/hooks/useCheckout.ts +312 -0
- package/src/hooks/useError.ts +60 -0
- package/src/hooks/useMarketplace.ts +129 -0
- package/src/hooks/useMemberships.ts +203 -0
- package/src/hooks/useWallet.ts +170 -0
- package/src/index.ts +147 -0
- package/src/lib/context.tsx +478 -0
- package/src/lib/oauth.ts +215 -0
- package/src/lib/parse-error.ts +224 -0
- package/src/lib/parse-stream.ts +121 -0
- package/src/lib/popup.ts +98 -0
- package/src/lib/storage.ts +140 -0
- package/src/lib/utils.ts +14 -0
- package/src/types/index.ts +216 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hypery Auth Context
|
|
3
|
+
* Similar to Clerk's <ClerkProvider>
|
|
4
|
+
*/
|
|
5
|
+
'use client';
|
|
6
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
7
|
+
import { createContext, useContext, useEffect, useState, useCallback, useRef, } from 'react';
|
|
8
|
+
import { getAuthorizationUrl, exchangeCodeForToken, refreshAccessToken, getUserInfo, } from './oauth';
|
|
9
|
+
import { parseError } from './parse-error';
|
|
10
|
+
import { openPopup, resolveInteractionMode } from './popup';
|
|
11
|
+
import { TokenStorage } from './storage';
|
|
12
|
+
/** window.open target name; also read back in the callback to detect popup context. */
|
|
13
|
+
const AUTH_POPUP_NAME = 'hypery-sdk-popup';
|
|
14
|
+
const AuthContext = createContext(undefined);
|
|
15
|
+
/**
|
|
16
|
+
* Hypery Auth Provider
|
|
17
|
+
* Wrap your app with this component to enable authentication
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <HyperyProvider config={{
|
|
22
|
+
* clientId: 'your-client-id',
|
|
23
|
+
* redirectUri: 'http://localhost:3000/callback',
|
|
24
|
+
* gatewayUrl: 'https://api.hypery.ai',
|
|
25
|
+
* scopes: ['read', 'write', 'ai:chat']
|
|
26
|
+
* }}>
|
|
27
|
+
* <App />
|
|
28
|
+
* </HyperyProvider>
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function HyperyProvider({ config, children, }) {
|
|
32
|
+
const [user, setUser] = useState(null);
|
|
33
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
34
|
+
const [error, setError] = useState(null);
|
|
35
|
+
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
|
36
|
+
// Turnkey modal state driven by authenticatedFetch (see <HyperyModals>).
|
|
37
|
+
const [restriction, setRestriction] = useState(null);
|
|
38
|
+
const [authRequired, setAuthRequired] = useState(false);
|
|
39
|
+
// Single-flight refresh: N concurrent expired requests share ONE token POST.
|
|
40
|
+
const refreshInFlight = useRef(null);
|
|
41
|
+
const storage = new TokenStorage(config.storage || 'localStorage');
|
|
42
|
+
// Default scopes match the app's default OAuth configuration.
|
|
43
|
+
// Includes ai:images so image generation works without a custom scope list.
|
|
44
|
+
const scopes = config.scopes || ['read', 'write', 'ai:chat', 'ai:completions', 'ai:models', 'ai:images', 'billing:read'];
|
|
45
|
+
/**
|
|
46
|
+
* Get valid access token (refreshes if needed)
|
|
47
|
+
*/
|
|
48
|
+
const getAccessToken = useCallback(async (forceRefresh = false) => {
|
|
49
|
+
const tokens = storage.getTokens();
|
|
50
|
+
if (!tokens)
|
|
51
|
+
return null;
|
|
52
|
+
// Refresh when the local clock says expired, or when forced (the 401 retry
|
|
53
|
+
// path — the server may have revoked a token our clock still thinks valid).
|
|
54
|
+
if (forceRefresh || storage.isTokenExpired()) {
|
|
55
|
+
// Single-flight: if a refresh is already running, await it instead of
|
|
56
|
+
// firing another POST /api/oauth/token for every concurrent request.
|
|
57
|
+
if (!refreshInFlight.current) {
|
|
58
|
+
refreshInFlight.current = (async () => {
|
|
59
|
+
try {
|
|
60
|
+
const newTokens = await refreshAccessToken(tokens.refreshToken, {
|
|
61
|
+
clientId: config.clientId,
|
|
62
|
+
gatewayUrl: config.gatewayUrl,
|
|
63
|
+
});
|
|
64
|
+
storage.saveTokens(newTokens);
|
|
65
|
+
return newTokens.accessToken;
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.error('Failed to refresh token:', err);
|
|
69
|
+
storage.clear();
|
|
70
|
+
setUser(null);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
refreshInFlight.current = null;
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
}
|
|
78
|
+
return refreshInFlight.current;
|
|
79
|
+
}
|
|
80
|
+
return tokens.accessToken;
|
|
81
|
+
}, [config.clientId, config.gatewayUrl]);
|
|
82
|
+
/**
|
|
83
|
+
* fetch() wrapper that injects the bearer token and drives the turnkey modal
|
|
84
|
+
* UX. On 401 it does ONE silent-refresh retry; if still 401 it flags
|
|
85
|
+
* `authRequired` (and calls `config.onUnauthorized`). On a 402/429 billing
|
|
86
|
+
* restriction it parses the canonical error and sets `restriction` (and calls
|
|
87
|
+
* `config.onRestricted`). The Response is always returned so the caller can
|
|
88
|
+
* still read the body / handle other statuses itself.
|
|
89
|
+
*/
|
|
90
|
+
const authenticatedFetch = useCallback(async (input, init = {}) => {
|
|
91
|
+
const withToken = (token) => ({
|
|
92
|
+
...init,
|
|
93
|
+
headers: {
|
|
94
|
+
...init.headers,
|
|
95
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
let token = await getAccessToken();
|
|
99
|
+
let res = await fetch(input, withToken(token));
|
|
100
|
+
if (res.status === 401) {
|
|
101
|
+
// Force one refresh + retry before giving up.
|
|
102
|
+
token = await getAccessToken(true);
|
|
103
|
+
if (token) {
|
|
104
|
+
res = await fetch(input, withToken(token));
|
|
105
|
+
}
|
|
106
|
+
if (res.status === 401) {
|
|
107
|
+
setAuthRequired(true);
|
|
108
|
+
config.onUnauthorized?.();
|
|
109
|
+
return res;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (res.status === 402 || res.status === 429) {
|
|
113
|
+
try {
|
|
114
|
+
const body = await res.clone().json();
|
|
115
|
+
const parsed = parseError({ ...body, status: res.status });
|
|
116
|
+
setRestriction(parsed);
|
|
117
|
+
config.onRestricted?.(parsed);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Non-JSON body — leave it to the caller.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return res;
|
|
124
|
+
}, [getAccessToken, config]);
|
|
125
|
+
/**
|
|
126
|
+
* Load user from storage and validate token
|
|
127
|
+
*/
|
|
128
|
+
const loadUser = useCallback(async () => {
|
|
129
|
+
try {
|
|
130
|
+
setIsLoading(true);
|
|
131
|
+
setError(null);
|
|
132
|
+
setIsLoggingOut(false); // Reset logout flag on fresh load
|
|
133
|
+
const accessToken = await getAccessToken();
|
|
134
|
+
if (!accessToken) {
|
|
135
|
+
setUser(null);
|
|
136
|
+
setIsLoading(false);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Fetch fresh user info
|
|
140
|
+
const userInfo = await getUserInfo(accessToken, config.gatewayUrl);
|
|
141
|
+
storage.saveUser(userInfo);
|
|
142
|
+
setUser(userInfo);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
console.error('Failed to load user:', err);
|
|
146
|
+
setError(err instanceof Error ? err.message : 'Failed to load user');
|
|
147
|
+
storage.clear();
|
|
148
|
+
setUser(null);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
setIsLoading(false);
|
|
152
|
+
}
|
|
153
|
+
}, [config.gatewayUrl, getAccessToken]);
|
|
154
|
+
/**
|
|
155
|
+
* Initiate OAuth login flow
|
|
156
|
+
*/
|
|
157
|
+
const login = useCallback(async () => {
|
|
158
|
+
try {
|
|
159
|
+
// Check if user just logged out (force account selection)
|
|
160
|
+
const justLoggedOut = typeof window !== 'undefined' &&
|
|
161
|
+
localStorage.getItem('hypery_force_reauth') === 'true';
|
|
162
|
+
const authUrl = await getAuthorizationUrl({
|
|
163
|
+
clientId: config.clientId,
|
|
164
|
+
redirectUri: config.redirectUri,
|
|
165
|
+
gatewayUrl: config.gatewayUrl,
|
|
166
|
+
scopes,
|
|
167
|
+
storage: config.storage || 'localStorage',
|
|
168
|
+
// Force account selection after logout to prevent auto-login
|
|
169
|
+
prompt: justLoggedOut ? 'select_account' : undefined,
|
|
170
|
+
});
|
|
171
|
+
// Clear the flag
|
|
172
|
+
if (justLoggedOut) {
|
|
173
|
+
localStorage.removeItem('hypery_force_reauth');
|
|
174
|
+
}
|
|
175
|
+
window.location.href = authUrl;
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
console.error('Failed to initiate login:', err);
|
|
179
|
+
setError(err instanceof Error ? err.message : 'Failed to initiate login');
|
|
180
|
+
}
|
|
181
|
+
}, [config, scopes]);
|
|
182
|
+
/**
|
|
183
|
+
* Initiate OAuth sign-up flow (forces account selection)
|
|
184
|
+
*/
|
|
185
|
+
const signUp = useCallback(async () => {
|
|
186
|
+
try {
|
|
187
|
+
const authUrl = await getAuthorizationUrl({
|
|
188
|
+
clientId: config.clientId,
|
|
189
|
+
redirectUri: config.redirectUri,
|
|
190
|
+
gatewayUrl: config.gatewayUrl,
|
|
191
|
+
scopes,
|
|
192
|
+
storage: config.storage || 'localStorage',
|
|
193
|
+
prompt: 'select_account', // Force account selection for sign-up
|
|
194
|
+
});
|
|
195
|
+
window.location.href = authUrl;
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
console.error('Failed to initiate sign-up:', err);
|
|
199
|
+
setError(err instanceof Error ? err.message : 'Failed to initiate sign-up');
|
|
200
|
+
}
|
|
201
|
+
}, [config, scopes]);
|
|
202
|
+
/**
|
|
203
|
+
* Log in via a centered popup instead of a full-page redirect. The popup runs
|
|
204
|
+
* the normal OAuth+PKCE flow; its callback page (this same provider, mounted
|
|
205
|
+
* at redirectUri) posts the authorization code back here (see handleCallback),
|
|
206
|
+
* and we exchange it with the verifier stored in THIS window. Same-origin
|
|
207
|
+
* redirectUri required — the code is relayed via postMessage to the opener.
|
|
208
|
+
*/
|
|
209
|
+
const loginPopup = useCallback(async () => {
|
|
210
|
+
try {
|
|
211
|
+
const authUrl = await getAuthorizationUrl({
|
|
212
|
+
clientId: config.clientId,
|
|
213
|
+
redirectUri: config.redirectUri,
|
|
214
|
+
gatewayUrl: config.gatewayUrl,
|
|
215
|
+
scopes,
|
|
216
|
+
storage: config.storage || 'localStorage',
|
|
217
|
+
});
|
|
218
|
+
const expectedOrigin = new URL(config.redirectUri).origin;
|
|
219
|
+
const result = await openPopup({
|
|
220
|
+
url: authUrl,
|
|
221
|
+
name: AUTH_POPUP_NAME,
|
|
222
|
+
expectedOrigin,
|
|
223
|
+
messageType: 'hypery:auth',
|
|
224
|
+
});
|
|
225
|
+
if (result.blocked)
|
|
226
|
+
return { ok: false, blocked: true, cancelled: false };
|
|
227
|
+
if (result.cancelled || !result.data?.code) {
|
|
228
|
+
return { ok: false, blocked: false, cancelled: true };
|
|
229
|
+
}
|
|
230
|
+
const tokens = await exchangeCodeForToken(result.data.code, {
|
|
231
|
+
clientId: config.clientId,
|
|
232
|
+
redirectUri: config.redirectUri,
|
|
233
|
+
gatewayUrl: config.gatewayUrl,
|
|
234
|
+
storage: config.storage || 'localStorage',
|
|
235
|
+
});
|
|
236
|
+
storage.saveTokens(tokens);
|
|
237
|
+
const userInfo = await getUserInfo(tokens.accessToken, config.gatewayUrl);
|
|
238
|
+
storage.saveUser(userInfo);
|
|
239
|
+
setUser(userInfo);
|
|
240
|
+
return { ok: true, blocked: false, cancelled: false };
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
console.error('Popup login failed:', err);
|
|
244
|
+
setError(err instanceof Error ? err.message : 'Popup login failed');
|
|
245
|
+
return { ok: false, blocked: false, cancelled: true };
|
|
246
|
+
}
|
|
247
|
+
}, [config, scopes]);
|
|
248
|
+
/**
|
|
249
|
+
* Logout and clear session
|
|
250
|
+
*/
|
|
251
|
+
const logout = useCallback(async () => {
|
|
252
|
+
console.log('🚪 [AUTH] Logout initiated');
|
|
253
|
+
// Set logging out flag to prevent Protect components from triggering login
|
|
254
|
+
setIsLoggingOut(true);
|
|
255
|
+
// Get the current access token before clearing
|
|
256
|
+
const tokens = storage.getTokens();
|
|
257
|
+
console.log('🔑 [AUTH] Found tokens:', tokens ? 'yes' : 'no');
|
|
258
|
+
// Revoke the OAuth token on the server
|
|
259
|
+
if (tokens?.accessToken) {
|
|
260
|
+
try {
|
|
261
|
+
console.log('📡 [AUTH] Calling revoke endpoint:', `${config.gatewayUrl}/api/oauth/revoke`);
|
|
262
|
+
const response = await fetch(`${config.gatewayUrl}/api/oauth/revoke`, {
|
|
263
|
+
method: 'POST',
|
|
264
|
+
headers: { 'Content-Type': 'application/json' },
|
|
265
|
+
body: JSON.stringify({ token: tokens.accessToken }),
|
|
266
|
+
});
|
|
267
|
+
console.log('✅ [AUTH] OAuth token revoked on server, status:', response.status);
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
console.error('❌ [AUTH] Failed to revoke token on server:', err);
|
|
271
|
+
// Continue with logout even if revocation fails
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
console.log('⚠️ [AUTH] No access token found to revoke');
|
|
276
|
+
}
|
|
277
|
+
// Clear local storage
|
|
278
|
+
console.log('🧹 [AUTH] Clearing local storage');
|
|
279
|
+
storage.clear();
|
|
280
|
+
setUser(null);
|
|
281
|
+
setError(null);
|
|
282
|
+
// Also clear OAuth verifier if it exists
|
|
283
|
+
if (typeof window !== 'undefined') {
|
|
284
|
+
const storageInstance = config.storage === 'sessionStorage' ? sessionStorage : localStorage;
|
|
285
|
+
storageInstance.removeItem('hypery_oauth_verifier');
|
|
286
|
+
// Set flag to force account selection on next login
|
|
287
|
+
// This prevents auto-login after logout
|
|
288
|
+
localStorage.setItem('hypery_force_reauth', 'true');
|
|
289
|
+
console.log('✅ [AUTH] Set force reauth flag');
|
|
290
|
+
// Redirect to local landing page
|
|
291
|
+
// Note: This only logs out of THIS app, not the OAuth provider or other apps
|
|
292
|
+
console.log('🔄 [AUTH] Redirecting to /');
|
|
293
|
+
window.location.replace('/');
|
|
294
|
+
}
|
|
295
|
+
}, [config.storage, config.gatewayUrl, storage]);
|
|
296
|
+
/**
|
|
297
|
+
* Manually refresh auth state
|
|
298
|
+
*/
|
|
299
|
+
const refreshAuth = useCallback(async () => {
|
|
300
|
+
await loadUser();
|
|
301
|
+
}, [loadUser]);
|
|
302
|
+
/**
|
|
303
|
+
* Handle OAuth callback
|
|
304
|
+
*/
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
const handleCallback = async () => {
|
|
307
|
+
if (typeof window === 'undefined')
|
|
308
|
+
return;
|
|
309
|
+
const params = new URLSearchParams(window.location.search);
|
|
310
|
+
const code = params.get('code');
|
|
311
|
+
// Popup auth: this callback page is running inside the auth popup we opened.
|
|
312
|
+
// Don't exchange here — relay the code to the opener (which holds the PKCE
|
|
313
|
+
// verifier) and close. The opener validates event.origin (see loginPopup).
|
|
314
|
+
if (code && window.opener && !window.opener.closed && window.name === AUTH_POPUP_NAME) {
|
|
315
|
+
try {
|
|
316
|
+
window.opener.postMessage({ type: 'hypery:auth', code }, window.location.origin);
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
console.error('Failed to relay auth code to opener:', err);
|
|
320
|
+
}
|
|
321
|
+
window.close();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (code) {
|
|
325
|
+
try {
|
|
326
|
+
setIsLoading(true);
|
|
327
|
+
// Exchange code for tokens
|
|
328
|
+
const tokens = await exchangeCodeForToken(code, {
|
|
329
|
+
clientId: config.clientId,
|
|
330
|
+
redirectUri: config.redirectUri,
|
|
331
|
+
gatewayUrl: config.gatewayUrl,
|
|
332
|
+
storage: config.storage || 'localStorage',
|
|
333
|
+
});
|
|
334
|
+
storage.saveTokens(tokens);
|
|
335
|
+
// Fetch user info
|
|
336
|
+
const userInfo = await getUserInfo(tokens.accessToken, config.gatewayUrl);
|
|
337
|
+
storage.saveUser(userInfo);
|
|
338
|
+
setUser(userInfo);
|
|
339
|
+
// Clean up URL
|
|
340
|
+
window.history.replaceState({}, document.title, window.location.pathname);
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
console.error('OAuth callback failed:', err);
|
|
344
|
+
setError(err instanceof Error ? err.message : 'Authentication failed');
|
|
345
|
+
}
|
|
346
|
+
finally {
|
|
347
|
+
setIsLoading(false);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
// Not a callback, load existing session
|
|
352
|
+
await loadUser();
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
handleCallback();
|
|
356
|
+
}, [config, loadUser]);
|
|
357
|
+
const value = {
|
|
358
|
+
user,
|
|
359
|
+
isAuthenticated: !!user,
|
|
360
|
+
isLoading,
|
|
361
|
+
error,
|
|
362
|
+
isLoggingOut,
|
|
363
|
+
login,
|
|
364
|
+
loginPopup,
|
|
365
|
+
interactionMode: resolveInteractionMode(config.interactionMode),
|
|
366
|
+
redirectUri: config.redirectUri,
|
|
367
|
+
signUp,
|
|
368
|
+
logout,
|
|
369
|
+
refreshAuth,
|
|
370
|
+
getAccessToken,
|
|
371
|
+
clientId: config.clientId,
|
|
372
|
+
gatewayUrl: config.gatewayUrl,
|
|
373
|
+
authenticatedFetch,
|
|
374
|
+
restriction,
|
|
375
|
+
clearRestriction: () => setRestriction(null),
|
|
376
|
+
authRequired,
|
|
377
|
+
clearAuthRequired: () => setAuthRequired(false),
|
|
378
|
+
};
|
|
379
|
+
return _jsx(AuthContext.Provider, { value: value, children: children });
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Hook to access auth state and methods
|
|
383
|
+
* Similar to Clerk's useAuth()
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* ```tsx
|
|
387
|
+
* const { user, isAuthenticated, login, logout } = useHyperyAuth();
|
|
388
|
+
* ```
|
|
389
|
+
*/
|
|
390
|
+
export function useHyperyAuth() {
|
|
391
|
+
const context = useContext(AuthContext);
|
|
392
|
+
if (!context) {
|
|
393
|
+
throw new Error('useHyperyAuth must be used within HyperyProvider');
|
|
394
|
+
}
|
|
395
|
+
return context;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Hook to access user data
|
|
399
|
+
* Similar to Clerk's useUser()
|
|
400
|
+
*
|
|
401
|
+
* @example
|
|
402
|
+
* ```tsx
|
|
403
|
+
* const { user, isLoading } = useUser();
|
|
404
|
+
* ```
|
|
405
|
+
*/
|
|
406
|
+
export function useUser() {
|
|
407
|
+
const { user, isLoading } = useHyperyAuth();
|
|
408
|
+
return { user, isLoading };
|
|
409
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth utilities for authenticating with the Hypery
|
|
3
|
+
* Implements PKCE (Proof Key for Code Exchange) flow for secure authentication
|
|
4
|
+
*/
|
|
5
|
+
import type { AuthTokens } from '../types';
|
|
6
|
+
/**
|
|
7
|
+
* Generate PKCE challenge for secure OAuth flow
|
|
8
|
+
*/
|
|
9
|
+
export declare function generatePKCE(): Promise<{
|
|
10
|
+
verifier: string;
|
|
11
|
+
challenge: string;
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* Build OAuth authorization URL
|
|
15
|
+
*/
|
|
16
|
+
export declare function getAuthorizationUrl(config: {
|
|
17
|
+
clientId: string;
|
|
18
|
+
redirectUri: string;
|
|
19
|
+
gatewayUrl: string;
|
|
20
|
+
scopes: string[];
|
|
21
|
+
storage: 'localStorage' | 'sessionStorage' | 'memory';
|
|
22
|
+
state?: string;
|
|
23
|
+
prompt?: 'login' | 'select_account' | 'consent';
|
|
24
|
+
}): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Exchange authorization code for access token
|
|
27
|
+
*/
|
|
28
|
+
export declare function exchangeCodeForToken(code: string, config: {
|
|
29
|
+
clientId: string;
|
|
30
|
+
redirectUri: string;
|
|
31
|
+
gatewayUrl: string;
|
|
32
|
+
storage: 'localStorage' | 'sessionStorage' | 'memory';
|
|
33
|
+
}): Promise<AuthTokens>;
|
|
34
|
+
/**
|
|
35
|
+
* Refresh access token
|
|
36
|
+
*/
|
|
37
|
+
export declare function refreshAccessToken(refreshToken: string, config: {
|
|
38
|
+
clientId: string;
|
|
39
|
+
gatewayUrl: string;
|
|
40
|
+
}): Promise<AuthTokens>;
|
|
41
|
+
/**
|
|
42
|
+
* Get user info from access token
|
|
43
|
+
*/
|
|
44
|
+
export declare function getUserInfo(accessToken: string, gatewayUrl: string): Promise<{
|
|
45
|
+
id: string;
|
|
46
|
+
email: string;
|
|
47
|
+
name: string;
|
|
48
|
+
image?: string;
|
|
49
|
+
}>;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth utilities for authenticating with the Hypery
|
|
3
|
+
* Implements PKCE (Proof Key for Code Exchange) flow for secure authentication
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Generate random string for PKCE verifier
|
|
7
|
+
*/
|
|
8
|
+
function generateRandomString(length) {
|
|
9
|
+
const array = new Uint8Array(length);
|
|
10
|
+
crypto.getRandomValues(array);
|
|
11
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Base64 URL encode
|
|
15
|
+
*/
|
|
16
|
+
function base64UrlEncode(buffer) {
|
|
17
|
+
const bytes = new Uint8Array(buffer);
|
|
18
|
+
let binary = '';
|
|
19
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
20
|
+
binary += String.fromCharCode(bytes[i]);
|
|
21
|
+
}
|
|
22
|
+
return btoa(binary)
|
|
23
|
+
.replace(/\+/g, '-')
|
|
24
|
+
.replace(/\//g, '_')
|
|
25
|
+
.replace(/=/g, '');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Generate PKCE challenge for secure OAuth flow
|
|
29
|
+
*/
|
|
30
|
+
export async function generatePKCE() {
|
|
31
|
+
const verifier = generateRandomString(32);
|
|
32
|
+
// Hash the verifier
|
|
33
|
+
const encoder = new TextEncoder();
|
|
34
|
+
const data = encoder.encode(verifier);
|
|
35
|
+
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
36
|
+
const challenge = base64UrlEncode(hash);
|
|
37
|
+
return { verifier, challenge };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build OAuth authorization URL
|
|
41
|
+
*/
|
|
42
|
+
export async function getAuthorizationUrl(config) {
|
|
43
|
+
const { verifier, challenge } = await generatePKCE();
|
|
44
|
+
// Store verifier (will be used when exchanging code for token)
|
|
45
|
+
if (config.storage !== 'memory' && typeof window !== 'undefined') {
|
|
46
|
+
const storage = config.storage === 'localStorage' ? localStorage : sessionStorage;
|
|
47
|
+
storage.setItem('hypery_oauth_verifier', verifier);
|
|
48
|
+
}
|
|
49
|
+
const state = config.state || generateRandomString(16);
|
|
50
|
+
const params = new URLSearchParams({
|
|
51
|
+
client_id: config.clientId,
|
|
52
|
+
response_type: 'code',
|
|
53
|
+
redirect_uri: config.redirectUri,
|
|
54
|
+
scope: config.scopes.join(' '),
|
|
55
|
+
code_challenge: challenge,
|
|
56
|
+
code_challenge_method: 'S256',
|
|
57
|
+
state,
|
|
58
|
+
});
|
|
59
|
+
// Add prompt parameter if provided (forces account selection or re-authentication)
|
|
60
|
+
if (config.prompt) {
|
|
61
|
+
params.set('prompt', config.prompt);
|
|
62
|
+
}
|
|
63
|
+
return `${config.gatewayUrl}/api/oauth/authorize?${params.toString()}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Exchange authorization code for access token
|
|
67
|
+
*/
|
|
68
|
+
export async function exchangeCodeForToken(code, config) {
|
|
69
|
+
let verifier = null;
|
|
70
|
+
// Get verifier from storage
|
|
71
|
+
if (config.storage !== 'memory' && typeof window !== 'undefined') {
|
|
72
|
+
const storage = config.storage === 'localStorage' ? localStorage : sessionStorage;
|
|
73
|
+
verifier = storage.getItem('hypery_oauth_verifier');
|
|
74
|
+
}
|
|
75
|
+
if (!verifier) {
|
|
76
|
+
throw new Error('OAuth verifier not found');
|
|
77
|
+
}
|
|
78
|
+
// Call core API directly - PKCE flow doesn't need client_secret
|
|
79
|
+
const response = await fetch(`${config.gatewayUrl}/api/oauth/token`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: {
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
grant_type: 'authorization_code',
|
|
86
|
+
code,
|
|
87
|
+
code_verifier: verifier,
|
|
88
|
+
client_id: config.clientId,
|
|
89
|
+
redirect_uri: config.redirectUri,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
const error = await response.json();
|
|
94
|
+
throw new Error(error.error_description || error.error || 'Failed to exchange code for token');
|
|
95
|
+
}
|
|
96
|
+
// Clear verifier
|
|
97
|
+
if (config.storage !== 'memory' && typeof window !== 'undefined') {
|
|
98
|
+
const storage = config.storage === 'localStorage' ? localStorage : sessionStorage;
|
|
99
|
+
storage.removeItem('hypery_oauth_verifier');
|
|
100
|
+
}
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
return {
|
|
103
|
+
accessToken: data.access_token,
|
|
104
|
+
refreshToken: data.refresh_token,
|
|
105
|
+
expiresIn: data.expires_in,
|
|
106
|
+
tokenType: data.token_type,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Refresh access token
|
|
111
|
+
*/
|
|
112
|
+
export async function refreshAccessToken(refreshToken, config) {
|
|
113
|
+
const response = await fetch(`${config.gatewayUrl}/api/oauth/token`, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: {
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
},
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
grant_type: 'refresh_token',
|
|
120
|
+
refresh_token: refreshToken,
|
|
121
|
+
client_id: config.clientId,
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
const error = await response.json();
|
|
126
|
+
throw new Error(error.error_description || error.error || 'Failed to refresh token');
|
|
127
|
+
}
|
|
128
|
+
const data = await response.json();
|
|
129
|
+
return {
|
|
130
|
+
accessToken: data.access_token,
|
|
131
|
+
refreshToken: data.refresh_token,
|
|
132
|
+
expiresIn: data.expires_in,
|
|
133
|
+
tokenType: data.token_type,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get user info from access token
|
|
138
|
+
*/
|
|
139
|
+
export async function getUserInfo(accessToken, gatewayUrl) {
|
|
140
|
+
const response = await fetch(`${gatewayUrl}/api/user/me`, {
|
|
141
|
+
headers: {
|
|
142
|
+
Authorization: `Bearer ${accessToken}`,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
throw new Error('Failed to fetch user info');
|
|
147
|
+
}
|
|
148
|
+
return response.json();
|
|
149
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ParsedError } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* Parse an error (a parsed JSON body, a thrown error, or a Response-like object)
|
|
4
|
+
* from the Hypery API into a normalized, classified shape. Tolerant of both the
|
|
5
|
+
* `{ error: { code } }` envelope and an already-unwrapped object, and falls back
|
|
6
|
+
* to the HTTP status when no recognized `code` is present.
|
|
7
|
+
*/
|
|
8
|
+
export declare function parseError(error: any): ParsedError;
|
|
9
|
+
/**
|
|
10
|
+
* Check if an error is a spending limit error
|
|
11
|
+
*/
|
|
12
|
+
export declare function isSpendingLimitError(error: any): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Check if an error is an insufficient credits error
|
|
15
|
+
*/
|
|
16
|
+
export declare function isInsufficientCreditsError(error: any): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Check if an error means the user must add a payment method (metered billing).
|
|
19
|
+
*/
|
|
20
|
+
export declare function isPaymentMethodRequiredError(error: any): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Check if an error means the user's card was declined (metered billing).
|
|
23
|
+
*/
|
|
24
|
+
export declare function isPaymentDeclinedError(error: any): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Check if an error is an authentication failure (re-auth / open the auth modal).
|
|
27
|
+
*/
|
|
28
|
+
export declare function isAuthError(error: any): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Check if an error is a scope/permission denial (403) — NOT an auth failure,
|
|
31
|
+
* so it should not trigger re-auth.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isPermissionDeniedError(error: any): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Check if an error is a rate-limit denial (429 / RATE_LIMITED).
|
|
36
|
+
*/
|
|
37
|
+
export declare function isRateLimitError(error: any): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Any billing restriction the funds widget should surface a CTA for.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isBillingRestriction(error: any): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Format time until reset
|
|
44
|
+
*/
|
|
45
|
+
export declare function formatTimeUntilReset(resetsAt?: string): string;
|