magically-sdk
Version:
Official SDK for Magically - Build mobile apps with AI
1,056 lines (1,055 loc) • 43.9 kB
JavaScript
"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;