UNPKG

@frank-auth/react

Version:

Flexible and customizable React UI components for Frank Authentication

647 lines (554 loc) 18.3 kB
import type {FrankAuthError, JSONObject} from '../types'; // Base error classes export class FrankAuthBaseError extends Error { public readonly code: string; public readonly statusCode?: number; public readonly details?: JSONObject; public readonly timestamp: string; public readonly context?: string; constructor( message: string, code = 'FRANK_AUTH_ERROR', statusCode?: number, details?: JSONObject, context?: string ) { super(message); this.name = 'FrankAuthError'; this.code = code; this.statusCode = statusCode; this.details = details; this.timestamp = new Date().toISOString(); this.context = context; // Ensure the error stack is captured correctly if (Error.captureStackTrace) { Error.captureStackTrace(this, this.constructor); } } toJSON(): FrankAuthError { return { code: this.code, message: this.message, details: this.details, statusCode: this.statusCode, }; } } // Specific error classes export class AuthenticationError extends FrankAuthBaseError { constructor(message = 'Authentication failed', details?: JSONObject) { super(message, 'AUTHENTICATION_ERROR', 401, details, 'authentication'); } } export class AuthorizationError extends FrankAuthBaseError { constructor(message = 'Access denied', details?: JSONObject) { super(message, 'AUTHORIZATION_ERROR', 403, details, 'authorization'); } } export class ValidationError extends FrankAuthBaseError { public readonly fieldErrors: Record<string, string[]>; constructor( message = 'Validation failed', fieldErrors: Record<string, string[]> = {}, details?: JSONObject ) { super(message, 'VALIDATION_ERROR', 400, details, 'validation'); this.fieldErrors = fieldErrors; } getFieldError(field: string): string | undefined { const errors = this.fieldErrors[field]; return errors && errors.length > 0 ? errors[0] : undefined; } hasFieldError(field: string): boolean { return Boolean(this.fieldErrors[field]?.length); } getAllFieldErrors(): string[] { return Object.values(this.fieldErrors).flat(); } } export class NetworkError extends FrankAuthBaseError { public readonly isRetryable: boolean; constructor( message = 'Network request failed', isRetryable = true, details?: JSONObject ) { super(message, 'NETWORK_ERROR', undefined, details, 'network'); this.isRetryable = isRetryable; } } export class TimeoutError extends FrankAuthBaseError { constructor(message = 'Request timed out', details?: JSONObject) { super(message, 'TIMEOUT_ERROR', 408, details, 'timeout'); } } export class RateLimitError extends FrankAuthBaseError { public readonly retryAfter?: number; constructor( message = 'Rate limit exceeded', retryAfter?: number, details?: JSONObject ) { super(message, 'RATE_LIMIT_ERROR', 429, details, 'rate_limit'); this.retryAfter = retryAfter; } } export class ServerError extends FrankAuthBaseError { constructor(message = 'Internal server error', details?: JSONObject) { super(message, 'SERVER_ERROR', 500, details, 'server'); } } export class ConfigurationError extends FrankAuthBaseError { constructor(message = 'Configuration error', details?: JSONObject) { super(message, 'CONFIGURATION_ERROR', undefined, details, 'configuration'); } } export class SessionError extends FrankAuthBaseError { constructor(message = 'Session error', details?: JSONObject) { super(message, 'SESSION_ERROR', 401, details, 'session'); } } export class MFAError extends FrankAuthBaseError { public readonly challenge?: any; constructor( message = 'Multi-factor authentication required', challenge?: any, details?: JSONObject ) { super(message, 'MFA_ERROR', 428, details, 'mfa'); this.challenge = challenge; } } export class PasskeyError extends FrankAuthBaseError { constructor(message = 'Passkey operation failed', details?: JSONObject) { super(message, 'PASSKEY_ERROR', undefined, details, 'passkey'); } } export class OAuthError extends FrankAuthBaseError { public readonly provider?: string; public readonly errorCode?: string; constructor( message = 'OAuth authentication failed', provider?: string, errorCode?: string, details?: JSONObject ) { super(message, 'OAUTH_ERROR', 400, details, 'oauth'); this.provider = provider; this.errorCode = errorCode; } } export class OrganizationError extends FrankAuthBaseError { constructor(message = 'Organization error', details?: JSONObject) { super(message, 'ORGANIZATION_ERROR', 400, details, 'organization'); } } export class InvitationError extends FrankAuthBaseError { constructor(message = 'Invitation error', details?: JSONObject) { super(message, 'INVITATION_ERROR', 400, details, 'invitation'); } } // Error factory functions export const createError = ( type: string, message: string, details?: JSONObject ): FrankAuthBaseError => { switch (type) { case 'AUTHENTICATION_ERROR': return new AuthenticationError(message, details); case 'AUTHORIZATION_ERROR': return new AuthorizationError(message, details); case 'VALIDATION_ERROR': return new ValidationError(message, {}, details); case 'NETWORK_ERROR': return new NetworkError(message, true, details); case 'TIMEOUT_ERROR': return new TimeoutError(message, details); case 'RATE_LIMIT_ERROR': return new RateLimitError(message, undefined, details); case 'SERVER_ERROR': return new ServerError(message, details); case 'CONFIGURATION_ERROR': return new ConfigurationError(message, details); case 'SESSION_ERROR': return new SessionError(message, details); case 'MFA_ERROR': return new MFAError(message, undefined, details); case 'PASSKEY_ERROR': return new PasskeyError(message, details); case 'OAUTH_ERROR': return new OAuthError(message, undefined, undefined, details); case 'ORGANIZATION_ERROR': return new OrganizationError(message, details); case 'INVITATION_ERROR': return new InvitationError(message, details); default: return new FrankAuthBaseError(message, type, undefined, details); } }; export const createAuthenticationError = (message?: string, details?: JSONObject) => new AuthenticationError(message, details); export const createAuthorizationError = (message?: string, details?: JSONObject) => new AuthorizationError(message, details); export const createValidationError = ( message?: string, fieldErrors?: Record<string, string[]>, details?: JSONObject ) => new ValidationError(message, fieldErrors, details); export const createNetworkError = ( message?: string, isRetryable?: boolean, details?: JSONObject ) => new NetworkError(message, isRetryable, details); export const createSessionError = (message?: string, details?: JSONObject) => new SessionError(message, details); export const createMFAError = ( message?: string, challenge?: any, details?: JSONObject ) => new MFAError(message, challenge, details); // Error type guards export const isFrankAuthError = (error: any): error is FrankAuthBaseError => { return error instanceof FrankAuthBaseError || (error && typeof error === 'object' && error.code && error.message); }; export const isAuthenticationError = (error: any): error is AuthenticationError => { return error instanceof AuthenticationError || (isFrankAuthError(error) && error.code === 'AUTHENTICATION_ERROR'); }; export const isAuthorizationError = (error: any): error is AuthorizationError => { return error instanceof AuthorizationError || (isFrankAuthError(error) && error.code === 'AUTHORIZATION_ERROR'); }; export const isValidationError = (error: any): error is ValidationError => { return error instanceof ValidationError || (isFrankAuthError(error) && error.code === 'VALIDATION_ERROR'); }; export const isNetworkError = (error: any): error is NetworkError => { return error instanceof NetworkError || (isFrankAuthError(error) && error.code === 'NETWORK_ERROR'); }; export const isTimeoutError = (error: any): error is TimeoutError => { return error instanceof TimeoutError || (isFrankAuthError(error) && error.code === 'TIMEOUT_ERROR'); }; export const isRateLimitError = (error: any): error is RateLimitError => { return error instanceof RateLimitError || (isFrankAuthError(error) && error.code === 'RATE_LIMIT_ERROR'); }; export const isServerError = (error: any): error is ServerError => { return error instanceof ServerError || (isFrankAuthError(error) && error.code === 'SERVER_ERROR'); }; export const isSessionError = (error: any): error is SessionError => { return error instanceof SessionError || (isFrankAuthError(error) && error.code === 'SESSION_ERROR'); }; export const isMFAError = (error: any): error is MFAError => { return error instanceof MFAError || (isFrankAuthError(error) && error.code === 'MFA_ERROR'); }; export const isPasskeyError = (error: any): error is PasskeyError => { return error instanceof PasskeyError || (isFrankAuthError(error) && error.code === 'PASSKEY_ERROR'); }; export const isOAuthError = (error: any): error is OAuthError => { return error instanceof OAuthError || (isFrankAuthError(error) && error.code === 'OAUTH_ERROR'); }; export const isRetryableError = (error: any): boolean => { if (isNetworkError(error)) { return error.isRetryable; } if (isTimeoutError(error) || isRateLimitError(error) || isServerError(error)) { return true; } return false; }; // Error handling utilities export const handleError = ( error: any, context?: string, defaultMessage?: string ): FrankAuthBaseError => { if (isFrankAuthError(error)) { return error; } if (error instanceof Error) { return new FrankAuthBaseError( error.message || defaultMessage || 'An error occurred', 'UNKNOWN_ERROR', undefined, { originalError: error.name }, context ); } if (typeof error === 'string') { return new FrankAuthBaseError( error || defaultMessage || 'An error occurred', 'UNKNOWN_ERROR', undefined, undefined, context ); } return new FrankAuthBaseError( defaultMessage || 'An unknown error occurred', 'UNKNOWN_ERROR', undefined, { originalError: error }, context ); }; export const parseAPIError = (response: any): FrankAuthBaseError => { if (!response) { return new ServerError('No response received'); } const { status, data } = response; // Handle different status codes switch (status) { case 400: if (data?.errors) { return new ValidationError( data.message || 'Validation failed', data.errors, data ); } return new FrankAuthBaseError( data?.message || 'Bad request', 'BAD_REQUEST', 400, data ); case 401: return new AuthenticationError( data?.message || 'Authentication required', data ); case 403: return new AuthorizationError( data?.message || 'Access denied', data ); case 404: return new FrankAuthBaseError( data?.message || 'Resource not found', 'NOT_FOUND', 404, data ); case 408: return new TimeoutError( data?.message || 'Request timeout', data ); case 429: return new RateLimitError( data?.message || 'Rate limit exceeded', data?.retryAfter, data ); case 500: case 502: case 503: case 504: return new ServerError( data?.message || 'Server error', data ); default: return new FrankAuthBaseError( data?.message || `HTTP ${status} error`, `HTTP_${status}`, status, data ); } }; export const formatErrorMessage = (error: any): string => { if (isFrankAuthError(error)) { return error.message; } if (error instanceof Error) { return error.message; } if (typeof error === 'string') { return error; } return 'An unknown error occurred'; }; export const getErrorCode = (error: any): string => { if (isFrankAuthError(error)) { return error.code; } if (error instanceof Error) { return error.name; } return 'UNKNOWN_ERROR'; }; export const getErrorDetails = (error: any): JSONObject | undefined => { if (isFrankAuthError(error)) { return error.details; } return undefined; }; // Error logging utilities export interface ErrorLogger { error(message: string, error?: any, context?: JSONObject): void; warn(message: string, context?: JSONObject): void; info(message: string, context?: JSONObject): void; debug(message: string, context?: JSONObject): void; } export const createConsoleLogger = (): ErrorLogger => ({ error: (message, error, context) => { console.error(message, error, context); }, warn: (message, context) => { console.warn(message, context); }, info: (message, context) => { console.info(message, context); }, debug: (message, context) => { console.debug(message, context); }, }); export const logError = ( error: any, logger: ErrorLogger = createConsoleLogger(), context?: JSONObject ): void => { const frankError = handleError(error); logger.error( `[${frankError.code}] ${frankError.message}`, { error: frankError, stack: frankError.stack, timestamp: frankError.timestamp, context: frankError.context, ...context, } ); }; // Error retry utilities export interface RetryOptions { maxAttempts: number; delay: number; backoff: 'fixed' | 'exponential' | 'linear'; shouldRetry?: (error: any, attempt: number) => boolean; onRetry?: (error: any, attempt: number) => void; } export const withRetry = async <T>( operation: () => Promise<T>, options: Partial<RetryOptions> = {} ): Promise<T> => { const { maxAttempts = 3, delay = 1000, backoff = 'exponential', shouldRetry = isRetryableError, onRetry, } = options; let lastError: any; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await operation(); } catch (error) { lastError = error; if (attempt === maxAttempts || !shouldRetry(error, attempt)) { throw handleError(error); } if (onRetry) { onRetry(error, attempt); } // Calculate delay based on backoff strategy let actualDelay = delay; switch (backoff) { case 'exponential': actualDelay = delay * Math.pow(2, attempt - 1); break; case 'linear': actualDelay = delay * attempt; break; case 'fixed': default: actualDelay = delay; break; } // Add some jitter to prevent thundering herd actualDelay += Math.random() * 1000; await new Promise(resolve => setTimeout(resolve, actualDelay)); } } throw handleError(lastError); }; // Error boundary helpers for React export interface ErrorBoundaryState { hasError: boolean; error?: FrankAuthBaseError; } export const createErrorBoundaryState = (): ErrorBoundaryState => ({ hasError: false, error: undefined, }); export const handleErrorBoundaryError = ( error: any, errorInfo?: any ): ErrorBoundaryState => ({ hasError: true, error: handleError(error, 'error_boundary', 'Component error occurred'), }); // Export utilities object export const ErrorUtils = { // Error classes FrankAuthBaseError, AuthenticationError, AuthorizationError, ValidationError, NetworkError, TimeoutError, RateLimitError, ServerError, ConfigurationError, SessionError, MFAError, PasskeyError, OAuthError, OrganizationError, InvitationError, // Factory functions createError, createAuthenticationError, createAuthorizationError, createValidationError, createNetworkError, createSessionError, createMFAError, // Type guards isFrankAuthError, isAuthenticationError, isAuthorizationError, isValidationError, isNetworkError, isTimeoutError, isRateLimitError, isServerError, isSessionError, isMFAError, isPasskeyError, isOAuthError, isRetryableError, // Utilities handleError, parseAPIError, formatErrorMessage, getErrorCode, getErrorDetails, logError, withRetry, // Error boundary createErrorBoundaryState, handleErrorBoundaryError, };