UNPKG

magically-sdk

Version:

Official SDK for Magically - Build mobile apps with AI

1,056 lines (1,055 loc) 43.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MagicallyAuth = void 0; const platform_1 = require("./platform"); const Logger_1 = require("./Logger"); const APIClient_1 = require("./APIClient"); // SDK Version - should match package.json const SDK_VERSION = '1.0.6'; class MagicallyAuth { constructor(config) { this.authState = { user: null, isAuthenticated: false, isLoading: false, error: null, }; this.listeners = []; this.refreshTimer = null; this.tokenKey = 'magically_tokens'; // Whitelist of allowed origins for postMessage this.allowedOrigins = [ 'http://192.168.1.6:3000', 'http://localhost:3000', 'https://trymagically.com', 'https://trymagically.com', 'https://magically-aarida-reduxpress.vercel.app', 'https://a0fb-116-73-140-18.ngrok-free.app' ]; this.config = config; this.logger = new Logger_1.Logger(config.debug || false, 'MagicallyAuth'); this.apiClient = new APIClient_1.APIClient(config, 'MagicallyAuth'); this.logger.info('Initializing MagicallyAuth', { projectId: config.projectId }); this.tokenKey = this.tokenKey + '_' + config.projectId; this.initializeAuth(); } /** * Simple helper - is user authenticated? */ get isAuthenticated() { return this.authState.isAuthenticated; } /** * Initialize authentication - check for stored tokens */ async initializeAuth() { this.logger.debug('Starting auth initialization'); this.setLoading(true); try { this.logger.debug('Checking for stored tokens'); const storedTokens = await this.getStoredTokens(); if (storedTokens) { this.logger.debug('Found stored tokens, validating'); const user = await this.validateToken(storedTokens.accessToken); if (user) { this.logger.success('User authenticated from stored tokens', { userId: user.id, email: user.email }); this.setAuthState({ user, isAuthenticated: true, isLoading: false, error: null, // Preserve existing token if any (from previous session) token: this.authState.token }); return; } else { this.logger.warn('Stored tokens are invalid, clearing'); await this.clearStoredTokens(); } } else { this.logger.debug('No stored tokens found'); } } catch (error) { this.logger.error('Auth initialization failed', error); } this.logger.debug('Auth initialization complete - user not authenticated'); this.setLoading(false); } /** * Sign up with email and password */ async signUpWithEmail(params) { this.logger.info('Starting email sign up', { email: params.email }); this.setLoading(true); this.setError(null); try { const data = await this.apiClient.request(`/api/project/${this.config.projectId}/auth/email`, { method: 'POST', body: { action: 'signup', ...params, }, operation: 'email-signup', }); // Store tokens const tokens = { accessToken: data.accessToken, refreshToken: data.refreshToken || '', expiresIn: 3600, tokenType: 'Bearer', }; await this.storeTokens(tokens); // Update auth state const user = data.user; user._id = user.id; // MongoDB compatibility // Ensure user exists in project database await this.ensureUserInDatabase(data.accessToken, user, 'email'); this.setAuthState({ user, isAuthenticated: true, isLoading: false, error: null, token: data.accessToken // Store the access token }); this.startBackgroundRefresh(); this.logger.success('Email sign up successful', { userId: user.id }); return { accessToken: data.accessToken, user }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Sign up failed'; this.logger.error('Email sign up failed', error); this.setError(errorMessage); throw new Error(errorMessage); } finally { this.setLoading(false); } } /** * Sign in with email and password */ async signInWithEmail(params) { this.logger.info('Starting email sign in', { email: params.email }); this.setLoading(true); this.setError(null); try { const data = await this.apiClient.request(`/api/project/${this.config.projectId}/auth/email`, { method: 'POST', body: { action: 'signin', ...params, }, operation: 'email-signin', }); // Store tokens const tokens = { accessToken: data.accessToken, refreshToken: data.refreshToken || '', expiresIn: 3600, tokenType: 'Bearer', }; await this.storeTokens(tokens); // Update auth state const user = data.user; user._id = user.id; // MongoDB compatibility // Ensure user exists in project database await this.ensureUserInDatabase(data.accessToken, user, 'email'); this.setAuthState({ user, isAuthenticated: true, isLoading: false, error: null, token: data.accessToken // Store the access token }); this.startBackgroundRefresh(); this.logger.success('Email sign in successful', { userId: user.id }); return { accessToken: data.accessToken, user }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Sign in failed'; this.logger.error('Email sign in failed', error); this.setError(errorMessage); throw new Error(errorMessage); } finally { this.setLoading(false); } } /** * Send email verification code * @param email - User's email address * @param type - Must be 'signup' for new users or 'password_reset' for existing users. Defaults to 'signup' * @throws Error if type is invalid or API request fails */ async sendVerificationCode(email, type = 'signup') { // Validate type parameter for LLM guidance if (type !== 'signup' && type !== 'password_reset') { throw new Error(`Invalid type '${type}'. Must be 'signup' for new users or 'password_reset' for existing users`); } this.logger.info('Sending verification code', { email, type }); try { await this.apiClient.request(`/api/project/${this.config.projectId}/auth/send-verification`, { method: 'POST', body: { email, type }, operation: 'send-verification', }); this.logger.success('Verification code sent', { email }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to send code'; this.logger.error('Send verification failed', error); throw new Error(errorMessage); } } /** * Verify email with code */ async verifyCode(email, code) { this.logger.info('Verifying email code', { email }); try { await this.apiClient.request(`/api/project/${this.config.projectId}/auth/verify-code`, { method: 'POST', body: { email, code }, operation: 'verify-code', }); this.logger.success('Email verified', { email }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Verification failed'; this.logger.error('Email verification failed', error); throw new Error(errorMessage); } } /** * Reset password with verification code * Single call that handles verification and password update internally */ async resetPassword(params) { this.logger.info('Resetting password', { email: params.email }); try { await this.apiClient.request(`/api/project/${this.config.projectId}/auth/reset-password`, { method: 'POST', body: params, operation: 'reset-password', }); this.logger.success('Password reset successful', { email: params.email }); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Reset failed'; this.logger.error('Password reset failed', error); throw new Error(errorMessage); } } /** * Internal method that handles both Google and email authentication flows * Contains the core authentication logic shared by both methods */ async _performAuthenticationFlow(provider = 'google') { this.logger.info(`Starting ${provider} authentication`); this.setLoading(true); this.setError(null); try { let authData; if (platform_1.Platform.OS === 'web') { // WEB ONLY: Use open-then-navigate pattern to prevent popup blocking this.logger.debug('Web platform detected - using open-then-navigate pattern to prevent popup blocking'); // Step 1: Open popup IMMEDIATELY with loading page (maintains user gesture context) const loadingUrl = `${this.getApiUrl()}/auth/loading?provider=${provider}`; this.logger.debug('Opening popup with loading page', { loadingUrl }); const popupFeatures = 'width=500,height=650,left=100,top=100,toolbar=no,menubar=no'; const popupWindow = window.open(loadingUrl, 'magically_auth', popupFeatures); if (!popupWindow) { throw new Error('Unable to open authentication window. Please check your popup blocker settings and try again.'); } try { // Set up postMessage listener const authPromise = this.setupPostMessageListener(); // Step 2: Get OAuth client info (popup already open) this.logger.debug('Getting OAuth client info (popup already open)'); const clientInfo = await this.getOAuthClient(); this.logger.success('OAuth client info retrieved', { clientId: clientInfo.clientId }); // Step 3: Build OAuth URL const authUrl = this.buildOAuthUrl(clientInfo.clientId, clientInfo.redirectUri, provider); // Step 4: Navigate the already-open popup if (!popupWindow.closed) { this.logger.debug('Navigating popup to OAuth URL'); popupWindow.location.href = authUrl; try { popupWindow.focus(); } catch (e) { // Ignore focus errors } } else { throw new Error('Authentication window was closed'); } // Wait for callback this.logger.debug('Waiting for postMessage callback'); authData = await authPromise; this.logger.success('Authentication callback received via postMessage', { userId: authData.user.id, email: authData.user.email }); } catch (error) { if (popupWindow && !popupWindow.closed) { popupWindow.close(); } throw error; } } else { // NATIVE PLATFORMS: Use original flow (fetch config first, then open browser) this.logger.debug('Native platform detected - using standard OAuth flow'); // Step 1: Get OAuth client info first this.logger.debug('Step 1: Getting OAuth client info'); const clientInfo = await this.getOAuthClient(); this.logger.success('OAuth client info retrieved', { clientId: clientInfo.clientId }); // Step 2: Build OAuth URL this.logger.debug('Step 2: Building OAuth URL'); const authUrl = this.buildOAuthUrl(clientInfo.clientId, clientInfo.redirectUri, provider); this.logger.debug('Opening OAuth browser session', { authUrl }); // Step 3: Open browser with OAuth URL this.logger.debug('Step 3: Using native WebBrowser flow'); this.logger.debug(`Listening for redirectUri: ${clientInfo.redirectUri}`); const result = await platform_1.WebBrowser.openAuthSessionAsync?.(authUrl, clientInfo.redirectUri) || { type: 'cancel' }; console.log('result.type', result); if (result.type === 'cancel') { this.logger.warn('OAuth browser session was cancelled by user'); throw new Error('Authentication was cancelled'); } if (result.type !== 'success' || !result.url) { this.logger.error('OAuth browser session failed', { result }); throw new Error('Authentication failed - no result URL'); } // Parse the auth data from the result URL this.logger.debug('OAuth browser session completed, parsing result URL', { url: result.url }); authData = this.parseAuthDataFromUrl(result.url); this.logger.success('Authentication data parsed from URL', { userId: authData.user.id, email: authData.user.email }); } // 4. Store tokens and set user state (hidden complexity) this.logger.debug('Step 4: Storing tokens and setting user state'); const tokens = { accessToken: authData.accessToken, refreshToken: authData.refreshToken || '', // Store refresh token if provided expiresIn: 3600, // Default 1 hour tokenType: 'Bearer' }; await this.storeTokens(tokens); // 5. Ensure user exists in project database this.logger.debug('Step 5: Ensuring user exists in project database'); await this.ensureUserInDatabase(authData.accessToken, authData.user); const user = authData.user; // Adding for mongo backwards compatibility and LLM keeps using this pattern user._id = user.id; this.logger.success('User info received from callback', { userId: user.id, email: user.email }); // 6. Update internal state this.setAuthState({ user, isAuthenticated: true, isLoading: false, error: null, token: authData.accessToken // Store the access token }); // 7. Start background token refresh this.startBackgroundRefresh(); this.logger.success(`${provider} authentication completed successfully`, { userName: user.name }); return user; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Authentication failed'; this.logger.error(`${provider} authentication failed`, error); this.setError(errorMessage); throw new Error(errorMessage); } finally { this.setLoading(false); } } /** * Sign in with Google OAuth * Handles ALL complexity internally - just returns user or throws error */ async signInWithGoogle() { return this._performAuthenticationFlow('google'); } /** * Trigger authentication flow for specified provider * Supports both Google OAuth and email/password authentication */ async triggerAuthenticationFlow(provider) { return this._performAuthenticationFlow(provider); } /** * Sign out user * Handles ALL complexity internally - just clears everything */ async signOut() { this.logger.info('Starting sign out process'); try { // Stop background refresh this.stopBackgroundRefresh(); this.logger.debug('Clearing stored tokens'); await this.clearStoredTokens(); this.logger.debug('Tokens cleared successfully'); // Reset state to clean slate this.setAuthState({ user: null, isAuthenticated: false, isLoading: false, error: null, token: null }); this.logger.success('Sign out completed successfully'); } catch (error) { this.logger.error('Error during sign out, but continuing with state reset', error); // Even if clearing fails, reset the state this.setAuthState({ user: null, isAuthenticated: false, isLoading: false, error: null, token: null }); } } /** * Get current user - simple getter */ get currentUser() { return this.authState.user; } /** * Get current user - LLM friendly method as LLM keeps looking for this for some reason */ getCurrentUser() { return this.authState.user; } /** * Parse user from JWT token (Supabase-like pattern for edge functions) * Automatically sets the user in auth state for subsequent operations * @param authHeaderOrToken - Either full "Bearer xxx" header or just the JWT token * @returns User object or null if invalid/expired */ async getUser(authHeaderOrToken) { try { let token = null; // Handle different input types if (!authHeaderOrToken) { // Clear auth state if no token provided this.setAuthState({ user: null, isAuthenticated: false, isLoading: false, error: null, token: null }); return { user: null }; } if (typeof authHeaderOrToken === 'string') { // If it's a Bearer token header if (authHeaderOrToken.startsWith('Bearer ')) { token = authHeaderOrToken.substring(7); } else { // Assume it's just the token token = authHeaderOrToken; } } else if (authHeaderOrToken && typeof authHeaderOrToken === 'object' && 'headers' in authHeaderOrToken) { // It's a Request object const authHeader = authHeaderOrToken.headers.get('Authorization'); if (authHeader?.startsWith('Bearer ')) { token = authHeader.substring(7); } } if (!token) { // Clear auth state if no valid token this.setAuthState({ user: null, isAuthenticated: false, isLoading: false, error: null, token: null }); return { user: null }; } // Skip API keys if (token.startsWith('mg_')) { this.logger.debug('API key detected in getUser, returning null'); // Don't set auth state for API keys return { user: null }; } // Decode and validate JWT const payload = this.decodeJWT(token); const now = Math.floor(Date.now() / 1000); // Check expiration if (payload.exp && payload.exp < now) { this.logger.debug('Token expired in getUser'); // Clear auth state for expired token this.setAuthState({ user: null, isAuthenticated: false, isLoading: false, error: null, token: null }); return { user: null }; } // Parse user from JWT payload const user = { _id: payload.sub || payload.id, id: payload.sub || payload.id, email: payload.email, name: payload.name, firstName: payload.given_name, lastName: payload.family_name, }; // Store the token for future API calls (edge function scenario) // This allows data operations to use the user's token instead of API key if (typeof authHeaderOrToken === 'string' && !authHeaderOrToken.startsWith('Bearer ')) { // We have the raw token, store it const tokens = { accessToken: token, refreshToken: '', // No refresh token in edge context expiresIn: payload.exp ? payload.exp - now : 3600, tokenType: 'Bearer' }; await this.storeTokens(tokens); } // UPDATE AUTH STATE - Store the token in authState for edge environments this.setAuthState({ user, isAuthenticated: true, isLoading: false, error: null, token: token // Store the JWT token in auth state }); this.logger.debug('User parsed from JWT and auth state updated', { userId: user.id, email: user.email }); return { user }; } catch (error) { this.logger.error('Failed to parse user from token', error); // Clear auth state on error this.setAuthState({ user: null, isAuthenticated: false, isLoading: false, error: null, token: null }); return { user: null }; } } /** * Get current auth state - for debugging/UI */ get state() { return { ...this.authState }; // Return copy to prevent mutation } /** * Get a valid access token for API calls * Automatically refreshes if needed */ async getValidToken() { if (!this.isAuthenticated) { throw new Error('User is not authenticated'); } return await this.ensureValidToken(); } /** * Subscribe to auth state changes */ onAuthStateChanged(callback) { this.listeners.push(callback); // Return unsubscribe function return () => { const index = this.listeners.indexOf(callback); if (index > -1) { this.listeners.splice(index, 1); } }; } /** * Refresh access token */ async refreshToken() { this.logger.debug('Starting token refresh'); const storedTokens = await this.getStoredTokens(); if (!storedTokens?.accessToken) { this.logger.error('No access token available for refresh'); throw new Error('No access token available'); } try { let response; // If we have a refresh token, use the new method if (storedTokens.refreshToken) { this.logger.debug('Using refresh token for token refresh'); response = await fetch(`${this.getApiUrl()}/api/project/${this.config.projectId}/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ refresh_token: storedTokens.refreshToken, }), }); } else { // Fallback to JWT-only refresh for backward compatibility this.logger.debug('Using JWT for token refresh (no refresh token available)'); response = await fetch(`${this.getApiUrl()}/api/project/${this.config.projectId}/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${storedTokens.accessToken}`, }, body: JSON.stringify({ projectId: this.config.projectId, }), }); } if (!response.ok) { this.logger.error('Token refresh failed', { status: response.status }); throw new Error('Token refresh failed'); } const tokenData = await response.json(); const newTokens = { accessToken: tokenData.access_token, refreshToken: tokenData.refresh_token || storedTokens.refreshToken, expiresIn: tokenData.expires_in || 3600, tokenType: tokenData.token_type || 'Bearer', }; await this.storeTokens(newTokens); this.logger.success('Token refreshed successfully'); return newTokens.accessToken; } catch (error) { this.logger.error('Token refresh failed, signing out user', error); // If refresh fails, sign out user await this.signOut(); throw error; } } /** * Check if token needs refresh and refresh if necessary */ async ensureValidToken() { // First check if we have a token in authState (edge environment) if (this.authState.token) { try { // Validate the token from authState const payload = this.decodeJWT(this.authState.token); const now = Math.floor(Date.now() / 1000); const expiresAt = payload.exp; // Check if token is still valid (with 5 minute buffer) const refreshThreshold = 5 * 60; // 5 minutes if (expiresAt && (expiresAt - now) >= refreshThreshold) { // Token from authState is valid return this.authState.token; } // Token in authState is expired, clear it this.authState.token = null; } catch (error) { // Token in authState is invalid, clear it this.authState.token = null; } } // Fall back to stored tokens (client environment) const storedTokens = await this.getStoredTokens(); if (!storedTokens?.accessToken) { throw new Error('No access token available'); } try { // Decode JWT to check expiration const payload = this.decodeJWT(storedTokens.accessToken); const now = Math.floor(Date.now() / 1000); const expiresAt = payload.exp; // Refresh if token expires in the next 5 minutes const refreshThreshold = 5 * 60; // 5 minutes if (expiresAt && (expiresAt - now) < refreshThreshold) { this.logger.debug('Token expires soon, refreshing', { expiresAt, now, timeLeft: expiresAt - now }); return await this.refreshToken(); } return storedTokens.accessToken; } catch (error) { this.logger.error('Token validation failed', error); throw error; } } // Private helper methods setAuthState(newState) { this.authState = { ...this.authState, ...newState }; this.notifyListeners(); } setLoading(isLoading) { this.setAuthState({ isLoading }); } setError(error) { this.setAuthState({ error }); } notifyListeners() { this.listeners.forEach(callback => callback(this.authState)); } getApiUrl() { return this.config.apiUrl || 'https://trymagically.com'; } async getRedirectUri() { // Get redirect URI from OAuth client info (server determines platform-specific URI) const clientInfo = await this.getOAuthClient(); return clientInfo.redirectUri; } async getClientId() { const clientInfo = await this.getOAuthClient(); return clientInfo.clientId; } async getOAuthClient() { // Determine platform for redirect URI const platform = this.getPlatform(); const params = new URLSearchParams({ platform }); const url = `${this.getApiUrl()}/api/project/${this.config.projectId}/oauth-client?${params.toString()}`; this.logger.debug('Fetching OAuth client info', { url, platform }); const response = await fetch(url); if (!response.ok) { this.logger.error('Failed to fetch OAuth client info', { status: response.status, statusText: response.statusText }); throw new Error('Failed to get OAuth client information'); } const clientInfo = await response.json(); this.logger.debug('OAuth client info received', { clientId: clientInfo.clientId, redirectUri: clientInfo.redirectUri }); return clientInfo; } /** * Determine platform for OAuth redirect URI */ getPlatform() { // @ts-ignore - Platform.OS can be 'web' in React Native Web if (platform_1.Platform.OS === 'web') { return 'web'; } // Check if running in Expo Go // In Expo Go, the app bundle identifier contains 'host.exp.exponent' if (typeof navigator !== 'undefined' && navigator.userAgent?.includes('Expo')) { return 'expo-go'; } // For React Native, check if we're in development mode (Expo Go) if (typeof __DEV__ !== 'undefined' && __DEV__) { return 'expo-go'; } return 'native'; } buildOAuthUrl(clientId, redirectUri, provider) { const params = new URLSearchParams({ client_id: clientId, redirect_uri: redirectUri, response_type: 'code', state: this.generateState(), }); // Add provider-specific parameters for backward compatibility if (provider === 'google') { params.set('auth_type', 'google'); // Existing parameter name } else if (provider === 'email') { params.set('provider', 'email'); // New parameter for email flow } return `${this.getApiUrl()}/api/oauth/authorize?${params.toString()}`; } generateState() { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); } extractCodeFromUrl(url) { const urlObj = new URL(url); const code = urlObj.searchParams.get('code'); if (!code) { throw new Error('No authorization code found in callback URL'); } return code; } /** * Parse authentication data from URL (supports both hash fragments and query params) */ parseAuthDataFromUrl(url) { try { this.logger.debug('Parsing auth data from URL', { url }); const urlObj = new URL(url); // Try hash fragments first (more secure), then query params let accessToken = null; let refreshToken = null; let userDataString = null; let success = null; if (urlObj.hash) { // Parse hash fragments (format: #access_token=...&refresh_token=...&user_data=...) const hashParams = new URLSearchParams(urlObj.hash.substring(1)); accessToken = hashParams.get('access_token'); refreshToken = hashParams.get('refresh_token'); userDataString = hashParams.get('user_data'); success = hashParams.get('success'); this.logger.debug('Parsed from hash fragments', { hasAccessToken: !!accessToken, hasRefreshToken: !!refreshToken, hasUserData: !!userDataString }); } if (!accessToken) { // Fallback to query params (format: ?access_token=...&refresh_token=...&user_data=...) accessToken = urlObj.searchParams.get('access_token'); refreshToken = urlObj.searchParams.get('refresh_token'); userDataString = urlObj.searchParams.get('user_data'); success = urlObj.searchParams.get('success'); this.logger.debug('Parsed from query params', { hasAccessToken: !!accessToken, hasRefreshToken: !!refreshToken, hasUserData: !!userDataString }); } if (!success || success !== 'true') { throw new Error('Authentication was not successful'); } if (!accessToken) { throw new Error('No access token found in callback URL'); } if (!userDataString) { throw new Error('No user data found in callback URL'); } const userData = JSON.parse(userDataString); this.logger.debug('Successfully parsed auth data from URL', { hasAccessToken: !!accessToken, hasRefreshToken: !!refreshToken, userId: userData.id, email: userData.email }); return { accessToken, refreshToken: refreshToken || undefined, user: userData }; } catch (error) { this.logger.error('Failed to parse auth data from URL', { url, error }); throw new Error(`Failed to parse authentication data: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getUserFromToken(accessToken) { // Decode JWT to get user info (JWT contains user data from your existing implementation) const payload = this.decodeJWT(accessToken); return { _id: payload.sub, id: payload.sub, email: payload.email, name: payload.name, firstName: payload.given_name, lastName: payload.family_name, }; } decodeJWT(token) { try { const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const jsonPayload = decodeURIComponent(atob(base64) .split('') .map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) .join('')); return JSON.parse(jsonPayload); } catch (error) { throw new Error('Invalid JWT token'); } } async validateToken(accessToken) { try { // Check if token is expired const payload = this.decodeJWT(accessToken); const now = Math.floor(Date.now() / 1000); if (payload.exp && payload.exp < now) { // Token expired, try to refresh await this.refreshToken(); const newTokens = await this.getStoredTokens(); if (newTokens) { return this.getUserFromToken(newTokens.accessToken); } return null; } return this.getUserFromToken(accessToken); } catch (error) { console.error('Token validation error:', error); return null; } } async storeTokens(tokens) { try { await platform_1.AsyncStorage.setItem(this.tokenKey, JSON.stringify(tokens)); } catch (error) { console.error('Error storing tokens:', error); } } async getStoredTokens() { try { const stored = await platform_1.AsyncStorage.getItem(this.tokenKey); return stored ? JSON.parse(stored) : null; } catch (error) { console.error('Error getting stored tokens:', error); return null; } } async clearStoredTokens() { try { await platform_1.AsyncStorage.removeItem(this.tokenKey); } catch (error) { console.error('Error clearing tokens:', error); } } /** * Set up postMessage listener for OAuth callback (web only) */ setupPostMessageListener() { if (platform_1.Platform.OS !== "web") { this.logger.error('setupPostMessageListener called on non-web platform'); return Promise.reject(new Error('PostMessage listener only works on web platforms')); } return new Promise((resolve, reject) => { this.logger.debug('Setting up postMessage listener', { allowedOrigins: this.allowedOrigins }); const handleMessage = (event) => { this.logger.debug('Received postMessage', { origin: event.origin, type: event.data?.type }); // Validate origin if (!this.allowedOrigins.includes(event.origin)) { this.logger.warn('Ignoring postMessage from unauthorized origin', { origin: event.origin, allowedOrigins: this.allowedOrigins }); return; } // Check message type if (event.data?.type === 'magically-auth-callback') { this.logger.success('Valid auth callback received', { userId: event.data.user?.id, email: event.data.user?.email }); // Clean up listener window.removeEventListener('message', handleMessage); // Resolve with auth data resolve({ accessToken: event.data.accessToken, user: event.data.user, refreshToken: event.data.refreshToken }); } }; // Set up listener window.addEventListener('message', handleMessage); // Set up timeout to prevent hanging const timeout = setTimeout(() => { this.logger.error('OAuth callback timeout - no postMessage received'); window.removeEventListener('message', handleMessage); reject(new Error('Authentication timeout - no callback received')); }, 60000); // 60 second timeout // Clear timeout when resolved const originalResolve = resolve; resolve = (value) => { clearTimeout(timeout); originalResolve(value); }; }); } /** * Start background token refresh */ startBackgroundRefresh() { this.logger.debug('Starting background token refresh'); // Clear any existing timer this.stopBackgroundRefresh(); // Check token every 10 minutes this.refreshTimer = setInterval(async () => { try { if (this.isAuthenticated) { await this.ensureValidToken(); } } catch (error) { this.logger.error('Background token refresh failed', error); // Don't sign out automatically on background refresh failure // Let the next API call handle it } }, 10 * 60 * 1000); // 10 minutes } /** * Stop background token refresh */ stopBackgroundRefresh() { if (this.refreshTimer) { this.logger.debug('Stopping background token refresh'); clearInterval(this.refreshTimer); this.refreshTimer = null; } } /** * Ensure user exists in project database */ async ensureUserInDatabase(accessToken, user, provider = 'google') { try { this.logger.debug('Creating/updating user in project database', { userId: user.id, provider }); // Detect platform const platformName = platform_1.Platform.OS === 'web' ? 'web' : platform_1.Platform.OS === 'ios' ? 'ios' : platform_1.Platform.OS === 'android' ? 'android' : 'react-native'; const response = await fetch(`${this.getApiUrl()}/api/project/${this.config.projectId}/data/users`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, }, body: JSON.stringify({ provider: provider === 'email' ? 'credentials' : 'google', origin: provider === 'email' ? 'email' : 'oauth', emailVerified: true, metadata: { sdkVersion: SDK_VERSION, platform: platformName, loginTimestamp: new Date().toISOString(), userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'React Native', }, }), }); if (response.ok) { this.logger.success('User created/updated in project database'); } else { const errorText = await response.text(); this.logger.warn('Failed to create/update user in project database', { status: response.status, statusText: response.statusText, error: errorText }); // Log the error but don't throw - OAuth should still succeed if (response.status === 409) { this.logger.info('User already exists in project database (duplicate email)'); } } } catch (error) { this.logger.error('Error ensuring user in database', error); // Don't throw - this shouldn't break the auth flow } } } exports.MagicallyAuth = MagicallyAuth;