UNPKG

@nibssplc/cams-sdk

Version:

Central Authentication Management Service (CAMS) SDK for popup-based authentication with Azure AD + custom 2FA

815 lines (806 loc) 35.3 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('zod')) : typeof define === 'function' && define.amd ? define(['exports', 'zod'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.CAMS = {}, global.zod)); })(this, (function (exports, zod) { 'use strict'; exports.CAMSErrorType = void 0; (function (CAMSErrorType) { CAMSErrorType["POPUP_BLOCKED"] = "POPUP_BLOCKED"; CAMSErrorType["TIMEOUT"] = "TIMEOUT"; CAMSErrorType["INVALID_ORIGIN"] = "INVALID_ORIGIN"; CAMSErrorType["USER_CANCELLED"] = "USER_CANCELLED"; CAMSErrorType["INVALID_URL"] = "INVALID_URL"; CAMSErrorType["MAX_RETRIES_EXCEEDED"] = "MAX_RETRIES_EXCEEDED"; CAMSErrorType["API_VALIDATION_ERROR"] = "VALIDATION_ERROR"; })(exports.CAMSErrorType || (exports.CAMSErrorType = {})); class CAMSError extends Error { type; constructor(type, message) { super(message); this.type = type; this.name = this.constructor.name; } } // Validate the returned token - non-empty string const ProfileSchema = zod.z.object({ type: zod.z.literal("AUTH_SUCCESS").or(zod.z.literal("AUTH_FAILURE")), userProfile: zod.z.object({ name: zod.z.string(), email: zod.z.email(), mfaIsEnabled: zod.z.boolean(), message: zod.z.string(), isAuthenticated: zod.z.boolean(), userType: zod.z.enum(["SUPER_ADMIN", "ADMIN", "USER"]), roles: zod.z.array(zod.z.string()).optional(), tokens: zod.z.object({ Nonce: zod.z.string(), Bearer: zod.z.string(), }), }), }); const ErrorMessageSchema = zod.z.object({ error: zod.z.string().min(1, "Error message cannot be empty"), }); const AuthFailureMessageSchema = zod.z.object({ type: zod.z.literal("AUTH_FAILURE"), error: zod.z.string().min(1, "Error message cannot be empty"), errorCode: zod.z.string(), errorDetails: zod.z.object().or(zod.z.any()), }); const URLSchema = zod.z.url(); exports.LogLevel = void 0; (function (LogLevel) { LogLevel[LogLevel["ERROR"] = 0] = "ERROR"; LogLevel[LogLevel["WARN"] = 1] = "WARN"; LogLevel[LogLevel["INFO"] = 2] = "INFO"; LogLevel[LogLevel["DEBUG"] = 3] = "DEBUG"; })(exports.LogLevel || (exports.LogLevel = {})); class Logger { static level = exports.LogLevel.DEBUG; static prefix = '[CAMS-SDK]'; static isTestEnv = typeof process !== 'undefined' && process.env && process.env.NODE_ENV === 'test'; static setLevel(level) { Logger.level = level; } static error(message, context) { if (!Logger.isTestEnv && Logger.level >= exports.LogLevel.ERROR) { console.error(`${Logger.prefix} ERROR:`, message, context || ''); } } static warn(message, context) { if (!Logger.isTestEnv && Logger.level >= exports.LogLevel.WARN) { console.warn(`${Logger.prefix} WARN:`, message, context || ''); } } static info(message, context) { if (!Logger.isTestEnv && Logger.level >= exports.LogLevel.INFO) { console.info(`${Logger.prefix} INFO:`, message, context || ''); } } static debug(message, context) { if (!Logger.isTestEnv && Logger.level >= exports.LogLevel.DEBUG) { console.debug(`${Logger.prefix} DEBUG:`, message, context || ''); } } } // Constants const DEFAULT_AUTH_TIMEOUT = 300000; // 5 minutes const DEFAULT_IDLE_TIMEOUT = 120000; // 2 minutes const INITIAL_POLL_INTERVAL = 1000; const WINDOW_FEATURES = "scrollbars=yes,resizable=yes"; // Default allowed Microsoft domains for Azure AD redirects const DEFAULT_MICROSOFT_DOMAINS = [ ".microsoft.com", ".microsoftonline.com", ".live.com", "login.microsoftonline.com" ]; // Secure reference to native functions with proper binding const NativeFunctions = { addEventListener: typeof window !== "undefined" ? window.addEventListener.bind(window) : null, removeEventListener: typeof window !== "undefined" ? window.removeEventListener.bind(window) : null, setTimeout: typeof window !== "undefined" && window.setTimeout ? window.setTimeout.bind(window) : setTimeout, clearTimeout: typeof window !== "undefined" && window.clearTimeout ? window.clearTimeout.bind(window) : clearTimeout, }; /** * Generate a secure random window name to prevent targeting */ const generateSecureWindowName = () => `cams_auth_${Math.random().toString(36).slice(2, 9)}_${Date.now()}`; /** * Detect if current window is a popup to prevent recursive popups */ const isPopupWindow = () => { if (typeof window === "undefined") return false; // Skip popup detection in test environments if (typeof process !== "undefined" && process.env && process.env.NODE_ENV === "test") { return false; } try { return window.opener !== null && window.opener !== window; } catch { return false; } }; /** * Safely close a window with error handling for cross-origin restrictions */ const safeWindowClose = (window) => { if (!window) return; try { if (!window.closed) { window.close(); } } catch (error) { Logger.debug("Error closing window (cross-origin restriction)", { error: error instanceof Error ? error.message : "Unknown error" }); } }; /** * Center popup window on screen */ const getPopupPosition = (width, height) => { if (typeof window === "undefined") return { left: 0, top: 0 }; const left = Math.max(0, (window.screen.width - width) / 2); const top = Math.max(0, (window.screen.height - height) / 2); return { left, top }; }; /** * Validate origin against expected domain with support for subdomains and Microsoft redirects */ const isValidOrigin = (eventOrigin, expectedOrigin, microsoftDomains = DEFAULT_MICROSOFT_DOMAINS) => { try { const originUrl = new URL(eventOrigin); const expectedHostname = expectedOrigin.startsWith("http") ? new URL(expectedOrigin).hostname : expectedOrigin; // Check Microsoft domains for Azure AD redirects const isMicrosoftDomain = microsoftDomains.some(domain => originUrl.hostname === domain || originUrl.hostname.endsWith(domain)); // Allow exact match, subdomains, or Microsoft domains return (originUrl.hostname === expectedHostname || originUrl.hostname.endsWith("." + expectedHostname) || isMicrosoftDomain); } catch { return false; } }; /** * Safely check if window is closed with cross-origin handling */ const isWindowClosed = (window) => { if (!window) return true; try { return window.closed; } catch (error) { // Cross-origin error means we can't access the property // This typically happens when redirected to external auth providers Logger.debug("Cannot access window.closed due to cross-origin restrictions - assuming window is still open", error); return false; } }; /** * Safely add message event listener with error handling */ const addMessageListener = (listener) => { if (!NativeFunctions.addEventListener) { throw new CAMSError(exports.CAMSErrorType.POPUP_BLOCKED, "Message listener not available in current environment"); } const wrappedListener = (event) => { try { listener(event); } catch (error) { Logger.error("Error in message listener", { error: error instanceof Error ? error.message : "Unknown error" }); } }; NativeFunctions.addEventListener("message", wrappedListener); return () => { if (NativeFunctions.removeEventListener) { NativeFunctions.removeEventListener("message", wrappedListener); } }; }; /** * Validate configuration and return parsed URL */ function validateConfig(config) { const urlValidation = URLSchema.safeParse(config.camsUrl); if (!urlValidation.success) { throw new CAMSError(exports.CAMSErrorType.INVALID_URL, "Invalid CAMS URL format"); } if (!config.messageOrigin) { throw new CAMSError(exports.CAMSErrorType.INVALID_URL, "Message Origin must be specified for security"); } return new URL(config.camsUrl); } /** * Open CAMS authentication popup and handle the authentication flow */ function openCAMSPopUpLogin(config) { return new Promise((resolve, reject) => { // Environment validation if (typeof window === "undefined") { reject(new CAMSError(exports.CAMSErrorType.POPUP_BLOCKED, "Cannot open popup in server-side environment")); return; } if (isPopupWindow()) { Logger.warn("Attempted to open popup from within popup window"); reject(new CAMSError(exports.CAMSErrorType.POPUP_BLOCKED, "Cannot open authentication popup from within popup window")); return; } // Initialize logging if (config.debug) Logger.setLevel(exports.LogLevel.DEBUG); Logger.info("Starting CAMS authentication", { url: config.camsUrl }); // Validate configuration let validatedUrl; try { validatedUrl = validateConfig(config); Logger.debug("Configuration validation passed"); } catch (error) { Logger.error("Configuration validation failed", { error: error instanceof Error ? error.message : "Unknown error" }); reject(error); return; } // Extract configuration with defaults const { messageOrigin, windowHeight = 600, windowWidth = 500, authTimeout = DEFAULT_AUTH_TIMEOUT, idleTimeout = DEFAULT_IDLE_TIMEOUT, allowedRedirectDomains = [...DEFAULT_MICROSOFT_DOMAINS, messageOrigin], } = config; // State management let authWindow = null; let authTimeoutId = null; let idleTimeoutId = null; let checkClosedInterval = null; let removeMessageListener = null; let hasReceivedValidMessage = false; /** * Clean up all resources and timeouts */ const cleanup = () => { if (removeMessageListener) { removeMessageListener(); removeMessageListener = null; } if (idleTimeoutId) NativeFunctions.clearTimeout(idleTimeoutId); if (authTimeoutId) NativeFunctions.clearTimeout(authTimeoutId); if (checkClosedInterval) NativeFunctions.clearTimeout(checkClosedInterval); idleTimeoutId = null; authTimeoutId = null; checkClosedInterval = null; }; /** * Clean up and close the popup window, then reject with error */ const cleanupAndClose = (error) => { cleanup(); safeWindowClose(authWindow); if (error) { reject(error); } }; /** * Reset idle timeout on user activity */ const resetIdleTimeout = () => { if (idleTimeoutId) NativeFunctions.clearTimeout(idleTimeoutId); Logger.debug("Resetting idle timeout", { idleTimeout }); idleTimeoutId = NativeFunctions.setTimeout(() => { Logger.warn("Authentication idle timeout reached", { idleTimeout }); cleanupAndClose(new CAMSError(exports.CAMSErrorType.TIMEOUT, `Authentication timed out due to inactivity after ${idleTimeout}ms`)); }, idleTimeout); }; /** * Set up popup window closure detection with cross-origin handling */ const setupClosureDetection = () => { let pollInterval = INITIAL_POLL_INTERVAL; const checkClosed = () => { // Skip closure detection when on Microsoft domains if (authWindow) { try { const currentUrl = authWindow.location.href; const isMicrosoftDomain = allowedRedirectDomains.some(domain => currentUrl.includes(domain)); if (isMicrosoftDomain) { Logger.debug("Skipping closure check - on Microsoft domain"); checkClosedInterval = NativeFunctions.setTimeout(checkClosed, pollInterval); return; } } catch (error) { // Cross-origin error - assume we're on Microsoft domain and skip check Logger.debug("Cross-origin error - skipping closure check"); checkClosedInterval = NativeFunctions.setTimeout(checkClosed, pollInterval); return; } } const isClosed = isWindowClosed(authWindow); if (isClosed && !hasReceivedValidMessage) { Logger.info("Authentication window closed by user"); cleanupAndClose(new CAMSError(exports.CAMSErrorType.USER_CANCELLED, "Authentication window was closed")); return; } checkClosedInterval = NativeFunctions.setTimeout(checkClosed, pollInterval); }; checkClosedInterval = NativeFunctions.setTimeout(checkClosed, pollInterval); }; /** * Handle incoming message events from the popup */ const handleMessage = (event) => { Logger.debug("Received message", { origin: event.origin, expectedOrigin: messageOrigin, data: event.data, }); // Security: Validate origin if (!isValidOrigin(event.origin, messageOrigin, allowedRedirectDomains)) { Logger.warn("Blocked message from unauthorized origin", { origin: event.origin, expected: messageOrigin, data: event.data, }); return; } // Security: Validate message source if (event.source !== authWindow) { Logger.warn("Message from unexpected source", { source: event.source, origin: event.origin, expected: messageOrigin, data: event.data, }); return; } // Mark that we've received a valid message (prevents false closure detection) hasReceivedValidMessage = true; // Handle heartbeat messages (reset idle timeout) if (event.data?.type === "heartbeat") { Logger.debug("Received heartbeat message"); resetIdleTimeout(); return; } // Process authentication messages processAuthenticationMessage(event.data); }; /** * Process and validate authentication message payloads */ const processAuthenticationMessage = (data) => { // Success case: Valid profile received const profileValidation = ProfileSchema.safeParse(data); if (profileValidation.success) { Logger.info("Authentication successful"); cleanupAndClose(); resolve(profileValidation.data); return; } // Authentication failure case const authFailureValidation = AuthFailureMessageSchema.safeParse(data); if (authFailureValidation.success) { const { error, errorCode, errorDetails } = authFailureValidation.data; Logger.warn("Authentication failure received", { error, errorCode, errorDetails, }); cleanupAndClose(new CAMSError(exports.CAMSErrorType.API_VALIDATION_ERROR, `Authentication failed: ${error}${errorCode ? ` (${errorCode})` : ''}`)); return; } // Generic error case const errorValidation = ErrorMessageSchema.safeParse(data); if (errorValidation.success) { Logger.warn("Authentication error received", { error: errorValidation.data.error, }); cleanupAndClose(new CAMSError(exports.CAMSErrorType.USER_CANCELLED, `Authentication failed: ${errorValidation.data.error}`)); return; } // Invalid message format Logger.error("Invalid message format received", { messageData: data, dataType: typeof data, profileValidationError: profileValidation.error?.issues, authFailureValidationError: authFailureValidation.error?.issues, errorValidationError: errorValidation.error?.issues, }); cleanupAndClose(new CAMSError(exports.CAMSErrorType.INVALID_ORIGIN, "Invalid message format")); }; // Main authentication flow execution try { // Open popup window const { left, top } = getPopupPosition(windowWidth, windowHeight); const secureWindowName = generateSecureWindowName(); Logger.debug("Opening popup window", { width: windowWidth, height: windowHeight, left, top, }); authWindow = window.open(validatedUrl.toString(), secureWindowName, `width=${windowWidth},height=${windowHeight},left=${left},top=${top},${WINDOW_FEATURES}`); // Check for popup blocking if (!authWindow) { Logger.error("Popup window blocked or failed to open"); reject(new CAMSError(exports.CAMSErrorType.POPUP_BLOCKED, "Popup blocked by browser. Please allow popups and try again.")); return; } // Check for immediate closure (aggressive popup blockers) if (isWindowClosed(authWindow)) { Logger.error("Popup immediately closed by blocker"); reject(new CAMSError(exports.CAMSErrorType.POPUP_BLOCKED, "Popup blocked by browser. Please allow popups and try again.")); return; } Logger.debug("Popup window opened successfully"); // Set up message listener removeMessageListener = addMessageListener(handleMessage); Logger.debug("Message listener attached"); // Set up timeouts Logger.debug("Setting authentication timeout", { authTimeout }); authTimeoutId = NativeFunctions.setTimeout(() => { Logger.warn("Authentication timeout reached", { authTimeout }); cleanupAndClose(new CAMSError(exports.CAMSErrorType.TIMEOUT, `Authentication process exceeded time limit of ${authTimeout}ms`)); }, authTimeout); resetIdleTimeout(); setupClosureDetection(); // Focus popup for better UX try { authWindow.focus(); } catch (error) { Logger.debug("Could not focus popup window (cross-origin restriction)"); } } catch (error) { Logger.error("Failed to initialize authentication popup", { error: error instanceof Error ? error.message : "Unknown error" }); cleanup(); reject(new CAMSError(exports.CAMSErrorType.POPUP_BLOCKED, "Failed to initialize authentication: " + (error instanceof Error ? error.message : "Unknown error"))); } }); } class CAMSSessionManager { storage; keyPrefix; events; token = null; profile = null; constructor(storage = null, keyPrefix, events = {}) { this.storage = storage || getDefaultStorage(); this.keyPrefix = keyPrefix ?? "CAMS-SDK"; this.events = events; } setItem(key, value) { this.storage.setItem(`${this.keyPrefix}:${key}`, value); } getItem(key) { return this.storage.getItem(`${this.keyPrefix}:${key}`); } removeItem(key) { this.storage.removeItem(`${this.keyPrefix}:${key}`); } clear() { // clear only CAMS-prefixed items Object.keys(this.storage).forEach((k) => { if (k.startsWith(this.keyPrefix)) { this.storage.removeItem(k); } }); } async login(config) { try { Logger.info("Session login started"); // If we're in a popup, don't attempt to open another popup if (isPopupWindow()) { Logger.warn("Cannot initiate login from popup window"); throw new CAMSError(exports.CAMSErrorType.POPUP_BLOCKED, "Cannot initiate authentication from within popup window"); } if (this.events.onAuthStart) this.events.onAuthStart(); let attempts = 0; const maxAttempts = (config.retryAttempts ?? 0) + 1; Logger.debug("Login retry configuration", { maxAttempts }); while (attempts < maxAttempts) { try { attempts++; Logger.debug("Login attempt", { attempt: attempts, maxAttempts }); // Add exponential backoff delay for retries if (attempts > 1) { const delay = Math.min(1000 * Math.pow(2, attempts - 2), 10000); // Max 10s delay Logger.debug("Retry delay", { delay }); await new Promise((resolve) => setTimeout(resolve, delay)); } const response = await openCAMSPopUpLogin(config); Logger.debug("Login Response", response); this.profile = response; this.setToken(response.userProfile.tokens.Bearer); Logger.info("Session login successful"); if (this.events.onAuthSuccess) this.events.onAuthSuccess(response); return response; } catch (error) { Logger.warn("Login attempt failed", { attempt: attempts, error: error instanceof Error ? error.message : "Unknown error", }); if (attempts >= maxAttempts || !(error instanceof CAMSError)) { throw error; } // Only retry on specific error types if (![exports.CAMSErrorType.TIMEOUT, exports.CAMSErrorType.POPUP_BLOCKED].includes(error.type)) { throw error; } Logger.info("Retrying login", { nextAttempt: attempts + 1 }); } } throw new CAMSError(exports.CAMSErrorType.MAX_RETRIES_EXCEEDED, "Max retry attempts exceeded"); } catch (error) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; const camsError = error instanceof CAMSError ? error : new CAMSError(exports.CAMSErrorType.USER_CANCELLED, errorMessage); Logger.error("Session login failed", { error: camsError.message, type: camsError.type, }); if (this.events.onAuthError) this.events.onAuthError(camsError); throw camsError; } } async logout() { Logger.info("Session logout"); this.clearToken(); } setToken(token) { this.token = token; this.storage.setItem(this.keyPrefix, token); Logger.debug("Token stored", { storageKey: this.keyPrefix }); } getAccessToken() { // Return cached token if available, avoiding redundant storage access if (this.token) return this.token; const raw = this.storage.getItem(this.keyPrefix); if (!raw) return null; this.token = raw; Logger.debug("Token loaded from storage"); return this.token; } clearToken() { this.token = null; this.storage.removeItem(this.keyPrefix); Logger.debug("Token cleared"); } isExpired() { const token = this.getAccessToken(); if (!token) return true; try { const parts = token.split("."); if (parts.length < 3) return true; // Invalid JWT format const payload = JSON.parse(atob(parts[1])); if (typeof payload.exp !== "number") return true; const exp = payload.exp * 1000; return Date.now() > exp; } catch (error) { Logger.warn("Failed to parse JWT token", { error: error instanceof Error ? error.message : "Unknown error", }); return true; } } isPaddedExpired() { const token = this.getAccessToken(); if (!token) return true; try { const parts = token.split("."); if (parts.length < 3) return true; // Invalid JWT format const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/"); const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "="); const payload = JSON.parse(atob(padded)); if (typeof payload.exp !== "number") return true; const exp = payload.exp * 1000; return Date.now() > exp; } catch (error) { Logger.warn("Failed to parse JWT token", { error: error instanceof Error ? error.message : "Unknown error", }); return true; } } checkTokenExpiration() { if (this.isExpired()) { Logger.info("Token expired"); if (this.events.onTokenExpired) this.events.onTokenExpired(); return true; } return false; } isAuthenticated() { const token = this.getAccessToken(); if (!token) { Logger.debug("No token found"); return false; } if (this.checkTokenExpiration()) { return false; } Logger.debug("Token is valid"); return true; } async getProfile() { const storedToken = this.getAccessToken(); if (!storedToken) return null; // Check token expiration before processing if (this.checkTokenExpiration()) { return null; } return this.profile; } getSessionError() { return null; } /** * Complete authentication in popup window * This should be called by popup apps when authentication is successful */ completePopupAuth(profile, targetOrigin) { if (!isPopupWindow()) { Logger.warn("completePopupAuth called outside of popup window"); return; } try { // Store the authentication data locally in the popup this.profile = profile; this.setToken(profile.userProfile.tokens.Bearer); Logger.info("Authentication completed in popup, forwarding to parent"); } catch (error) { Logger.error("Failed to complete popup authentication", { error: error instanceof Error ? error.message : "Unknown error" }); } } /** * Report authentication error in popup window */ errorPopupAuth(error, targetOrigin) { if (!isPopupWindow()) { Logger.warn("errorPopupAuth called outside of popup window"); return; } Logger.warn("Reporting authentication error from popup"); } } // 🔹 Safe fallback for Node/SSR function getDefaultStorage() { if (typeof window !== "undefined" && window.localStorage) { return window.localStorage; } const memoryStore = {}; return { getItem: (key) => (key in memoryStore ? memoryStore[key] : null), setItem: (key, value) => { memoryStore[key] = value; }, removeItem: (key) => { delete memoryStore[key]; }, key: (i) => Object.keys(memoryStore)[i] ?? null, get length() { return Object.keys(memoryStore).length; }, }; } class CAMSMFAAuthenticator { config; attemptCount = 0; maxAttempts = 3; constructor(config) { this.config = config; } async sendEmailOTP() { try { Logger.debug("Sending Email OTP"); // Implementation would call your API to send email OTP // For now, return true as placeholder return true; } catch (error) { Logger.error("Failed to send Email OTP", { error }); return false; } } async verifyOTP(code, type) { if (this.attemptCount >= this.maxAttempts) { throw new CAMSError(exports.CAMSErrorType.MAX_RETRIES_EXCEEDED, "Maximum MFA attempts exceeded"); } this.attemptCount++; try { Logger.debug("Verifying MFA code", { type, attempt: this.attemptCount }); const response = await fetch(this.config.apiEndpoint || '/api/auth/verify-mfa', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.config.accessToken}`, }, body: JSON.stringify({ provider: this.config.provider, accessToken: this.config.accessToken, idToken: this.config.idToken, authenticationType: type, MFACode: code, appCode: this.config.appCode, }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); console.log("MFA Response >>>", data); if (data.isAuthenticated) { Logger.info("MFA verification successful"); this.resetAttempts(); return data; } else { throw new CAMSError(exports.CAMSErrorType.API_VALIDATION_ERROR, data.message || "MFA verification failed"); } } catch (error) { Logger.error("MFA verification failed", { error, attempt: this.attemptCount }); if (error instanceof CAMSError) { throw error; } throw new CAMSError(exports.CAMSErrorType.API_VALIDATION_ERROR, `MFA verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } getRemainingAttempts() { return Math.max(0, this.maxAttempts - this.attemptCount); } resetAttempts() { this.attemptCount = 0; } } exports.AuthFailureMessageSchema = AuthFailureMessageSchema; exports.CAMSError = CAMSError; exports.CAMSMFAAuthenticator = CAMSMFAAuthenticator; exports.CAMSSessionManager = CAMSSessionManager; exports.ErrorMessageSchema = ErrorMessageSchema; exports.Logger = Logger; exports.ProfileSchema = ProfileSchema; exports.URLSchema = URLSchema; exports.generateSecureWindowName = generateSecureWindowName; exports.isPopupWindow = isPopupWindow; exports.openCAMSPopUpLogin = openCAMSPopUpLogin; exports.validateConfig = validateConfig; })); //# sourceMappingURL=index.umd.js.map