@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.
Files changed (102) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +391 -0
  3. package/dist/components/AuthButton.d.ts +51 -0
  4. package/dist/components/AuthButton.js +60 -0
  5. package/dist/components/AuthModal.d.ts +20 -0
  6. package/dist/components/AuthModal.js +62 -0
  7. package/dist/components/BuyButton.d.ts +47 -0
  8. package/dist/components/BuyButton.js +74 -0
  9. package/dist/components/ErrorBoundary.d.ts +13 -0
  10. package/dist/components/ErrorBoundary.js +24 -0
  11. package/dist/components/HyperyModals.d.ts +30 -0
  12. package/dist/components/HyperyModals.js +30 -0
  13. package/dist/components/InsufficientCreditsAlert.d.ts +11 -0
  14. package/dist/components/InsufficientCreditsAlert.js +12 -0
  15. package/dist/components/ModernAuthForm.d.ts +52 -0
  16. package/dist/components/ModernAuthForm.js +83 -0
  17. package/dist/components/RestrictionModal.d.ts +43 -0
  18. package/dist/components/RestrictionModal.js +167 -0
  19. package/dist/components/SignIn.d.ts +26 -0
  20. package/dist/components/SignIn.js +47 -0
  21. package/dist/components/SignInForm.d.ts +41 -0
  22. package/dist/components/SignInForm.js +86 -0
  23. package/dist/components/SignUp.d.ts +26 -0
  24. package/dist/components/SignUp.js +52 -0
  25. package/dist/components/SpendingLimitAlert.d.ts +12 -0
  26. package/dist/components/SpendingLimitAlert.js +14 -0
  27. package/dist/components/UserButton.d.ts +26 -0
  28. package/dist/components/UserButton.js +35 -0
  29. package/dist/components/UserProfile.d.ts +22 -0
  30. package/dist/components/UserProfile.js +26 -0
  31. package/dist/components/WorkspaceSwitcher.d.ts +29 -0
  32. package/dist/components/WorkspaceSwitcher.js +66 -0
  33. package/dist/components/control.d.ts +64 -0
  34. package/dist/components/control.js +89 -0
  35. package/dist/components/ui/dialog.d.ts +19 -0
  36. package/dist/components/ui/dialog.js +53 -0
  37. package/dist/hooks/index.d.ts +22 -0
  38. package/dist/hooks/index.js +23 -0
  39. package/dist/hooks/useBuyerWallet.d.ts +37 -0
  40. package/dist/hooks/useBuyerWallet.js +66 -0
  41. package/dist/hooks/useCheckout.d.ts +27 -0
  42. package/dist/hooks/useCheckout.js +246 -0
  43. package/dist/hooks/useError.d.ts +19 -0
  44. package/dist/hooks/useError.js +36 -0
  45. package/dist/hooks/useMarketplace.d.ts +50 -0
  46. package/dist/hooks/useMarketplace.js +69 -0
  47. package/dist/hooks/useMemberships.d.ts +71 -0
  48. package/dist/hooks/useMemberships.js +138 -0
  49. package/dist/hooks/useWallet.d.ts +62 -0
  50. package/dist/hooks/useWallet.js +123 -0
  51. package/dist/index.d.ts +55 -0
  52. package/dist/index.js +52 -0
  53. package/dist/lib/context.d.ts +50 -0
  54. package/dist/lib/context.js +409 -0
  55. package/dist/lib/oauth.d.ts +49 -0
  56. package/dist/lib/oauth.js +149 -0
  57. package/dist/lib/parse-error.d.ts +45 -0
  58. package/dist/lib/parse-error.js +185 -0
  59. package/dist/lib/parse-stream.d.ts +43 -0
  60. package/dist/lib/parse-stream.js +89 -0
  61. package/dist/lib/popup.d.ts +42 -0
  62. package/dist/lib/popup.js +80 -0
  63. package/dist/lib/storage.d.ts +43 -0
  64. package/dist/lib/storage.js +125 -0
  65. package/dist/lib/utils.d.ts +8 -0
  66. package/dist/lib/utils.js +11 -0
  67. package/dist/types/index.d.ts +191 -0
  68. package/dist/types/index.js +4 -0
  69. package/package.json +78 -0
  70. package/src/components/AuthButton.tsx +123 -0
  71. package/src/components/AuthModal.tsx +240 -0
  72. package/src/components/BuyButton.tsx +150 -0
  73. package/src/components/ErrorBoundary.tsx +97 -0
  74. package/src/components/HyperyModals.tsx +83 -0
  75. package/src/components/InsufficientCreditsAlert.tsx +73 -0
  76. package/src/components/ModernAuthForm.tsx +322 -0
  77. package/src/components/RestrictionModal.tsx +373 -0
  78. package/src/components/SignIn.tsx +84 -0
  79. package/src/components/SignInForm.tsx +256 -0
  80. package/src/components/SignUp.tsx +86 -0
  81. package/src/components/SpendingLimitAlert.tsx +91 -0
  82. package/src/components/UserButton.tsx +106 -0
  83. package/src/components/UserProfile.tsx +106 -0
  84. package/src/components/WorkspaceSwitcher.tsx +199 -0
  85. package/src/components/control.tsx +133 -0
  86. package/src/components/ui/dialog.tsx +151 -0
  87. package/src/hooks/index.ts +65 -0
  88. package/src/hooks/useBuyerWallet.ts +117 -0
  89. package/src/hooks/useCheckout.ts +312 -0
  90. package/src/hooks/useError.ts +60 -0
  91. package/src/hooks/useMarketplace.ts +129 -0
  92. package/src/hooks/useMemberships.ts +203 -0
  93. package/src/hooks/useWallet.ts +170 -0
  94. package/src/index.ts +147 -0
  95. package/src/lib/context.tsx +478 -0
  96. package/src/lib/oauth.ts +215 -0
  97. package/src/lib/parse-error.ts +224 -0
  98. package/src/lib/parse-stream.ts +121 -0
  99. package/src/lib/popup.ts +98 -0
  100. package/src/lib/storage.ts +140 -0
  101. package/src/lib/utils.ts +14 -0
  102. 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;