PageSourceSearch

https://xpathlabs.ai/firebase-config.js

js xpathlabs.ai collected 2026-10-05 13:20:29 UTC 28,294 bytes, 858 lines download raw bytes

1/**
2 * Firebase Client Configuration
3 *
4 * This config is safe to expose publicly - Firebase security is handled by:
5 * 1. Firebase Security Rules
6 * 2. Domain restrictions in Firebase Console
7 * 3. Server-side token verification
8 *
9 * Auto-initializes on script load. Use `await FirebaseAuth.ready` to wait for auth.
10 * Includes automatic token refresh via Firebase SDK and visibility change handling.
11 */
12
13// Firebase configuration - loaded from environment via backend
14let firebaseConfig = null;
15let firebaseInitialized = false;
16let refreshIntervalId = null;
17let visibilityHandlerAdded = false;
18
19// Promise that resolves when Firebase auth state is ready
20// Components can await this before making authenticated API calls
21let authReadyResolve;
22const authReadyPromise = new Promise(resolve => {
23  authReadyResolve = resolve;
24});
25
26/**
27 * Initialize Firebase with config from backend
28 * Waits for auth state to be restored before returning
29 */
30async function initializeFirebaseClient() {
31  if (firebaseInitialized) {
32    return firebase;
33  }
34
35  try {
36    // Fetch config from backend (keeps API key out of static files)
37    const response = await fetch('/api/firebase-config');
38    if (!response.ok) {
39      throw new Error('Failed to load Firebase config');
40    }
41
42    firebaseConfig = await response.json();
43
44    if (!firebaseConfig.apiKey) {
45      console.warn('Firebase not configured - using legacy auth');
46      authReadyResolve(null);
47      return null;
48    }
49
50    firebase.initializeApp(firebaseConfig);
51    firebaseInitialized = true;
52
53    // Wait for auth state to be restored (important for page reloads)
54    firebase.auth().onAuthStateChanged(async (user) => {
55      if (user) {
56        // Store initial token
57        try {
58          const token = await user.getIdToken();
59          storeToken(token);
60
61          // Set up proactive token refresh (every 45 minutes)
62          setupTokenRefresh();
63
64          // Add visibility handler for tab wake-up (only once)
65          if (!visibilityHandlerAdded) {
66            addVisibilityHandler();
67            visibilityHandlerAdded = true;
68          }
69        } catch (error) {
70          console.error('Failed to get initial token:', error);
71        }
72      } else {
73        // User signed out - clear token and refresh interval
74        clearStoredToken();
75        clearRefreshInterval();
76      }
77
78      authReadyResolve(user);
79    });
80
81    return firebase;
82  } catch (error) {
83    console.error('Failed to initialize Firebase:', error);
84    authReadyResolve(null);
85    return null;
86  }
87}
88
89/**
90 * Get the current Firebase user
91 */
92function getCurrentUser() {
93  return firebase.auth?.().currentUser || null;
94}
95
96// Token refresh state - prevents concurrent refresh attempts
97let tokenRefreshPromise = null;
98
99/**
100 * Get Firebase ID token for API calls
101 * Automatically waits for auth to be ready before getting token
102 * Proactively refreshes if token is expired or about to expire
103 * Uses mutex pattern to prevent concurrent refresh attempts
104 */
105async function getFirebaseToken() {
106  // Wait for Firebase auth to be ready (auto-initialized on page load)
107  await authReadyPromise;
108
109  const user = getCurrentUser();
110  if (!user) return null;
111
112  // Check if stored token is expired or expiring soon (within 5 minutes)
113  const storedExpiry = localStorage.getItem('authTokenExpiry');
114  const expiryTime = storedExpiry ? parseInt(storedExpiry, 10) : 0;
115  const now = Date.now();
116  const fiveMinutes = 5 * 60 * 1000;
117  const needsRefresh = expiryTime === 0 || (expiryTime - now) < fiveMinutes;
118
119  // If refresh needed, use mutex to prevent concurrent refreshes
120  if (needsRefresh) {
121    if (!tokenRefreshPromise) {
122      tokenRefreshPromise = (async () => {
123        try {
124          const token = await user.getIdToken(true);
125          storeToken(token);
126          return token;
127        } finally {
128          tokenRefreshPromise = null;
129        }
130      })();
131    }
132    return tokenRefreshPromise;
133  }
134
135  // Token is still valid, use Firebase SDK's cached token
136  try {
137    return await user.getIdToken();
138  } catch (error) {
139    // Token fetch failed - try force refresh once
140    if (!tokenRefreshPromise) {
141      tokenRefreshPromise = (async () => {
142        try {
143          const token = await user.getIdToken(true);
144          storeToken(token);
145          return token;
146        } catch (refreshError) {
147          // Session may be completely invalid - clear state
148          clearStoredToken();
149          return null;
150        } finally {
151          tokenRefreshPromise = null;
152        }
153      })();
154    }
155    return tokenRefreshPromise;
156  }
157}
158
159// Auto-initialize Firebase when script loads (non-blocking)
160// This starts the initialization process immediately
161initializeFirebaseClient();
162
163/**
164 * Sign in with email and password
165 */
166async function firebaseSignIn(email, password) {
167  if (!firebaseInitialized) {
168    throw new Error('Firebase not initialized');
169  }
170
171  return firebase.auth().signInWithEmailAndPassword(email, password);
172}
173
174/**
175 * Sign out
176 */
177async function firebaseSignOut() {
178  if (!firebaseInitialized) {
179    return;
180  }
181
182  return firebase.auth().signOut();
183}
184
185/**
186 * Send password reset email with custom action URL
187 */
188async function firebaseSendPasswordReset(email) {
189  if (!firebaseInitialized) {
190    throw new Error('Firebase not initialized');
191  }
192
193  // Use custom action URL to redirect to our branded password setup page
194  const actionCodeSettings = {
195    url: `${window.location.origin}/set-password.html`,
196    handleCodeInApp: false
197  };
198
199  return firebase.auth().sendPasswordResetEmail(email, actionCodeSettings);
200}
201
202/**
203 * Create user with email and password
204 */
205async function firebaseCreateUser(email, password) {
206  if (!firebaseInitialized) {
207    throw new Error('Firebase not initialized');
208  }
209
210  return firebase.auth().createUserWithEmailAndPassword(email, password);
211}
212
213/**
214 * Listen for auth state changes
215 */
216function onAuthStateChanged(callback) {
217  if (!firebaseInitialized) {
218    return () => {};
219  }
220
221  return firebase.auth().onAuthStateChanged(callback);
222}
223
224// ==========================================
225// API Guardrail Notification
226// ==========================================
227
228/**
229 * Show guardrail notification banner for circuit breaker / rate limit errors
230 * @param {string}
230 code - Error code (CIRCUIT_OPEN, RATE_LIMITED, ERROR_RATE_EXCEEDED)
231 * @param {number} retryAfter - Seconds until retry is allowed
232 * @param {string} platform - Platform name
233 */
234function showGuardrailNotification(code, retryAfter, platform = 'API') {
235    // Remove existing notification if any
236    const existing = document.querySelector('.guardrail-notification');
237    if (existing) existing.remove();
238
239    let title, message, icon;
240    switch (code) {
241        case 'CIRCUIT_OPEN':
242            title = `${platform} Temporarily Paused`;
243            message = `We've detected repeated errors from ${platform} and temporarily paused requests to protect your account.`;
244            icon = '🛡️';
245            break;
246        case 'RATE_LIMITED':
247            title = `${platform} Rate Limit Reached`;
248            message = `You've made too many requests. ${platform} requires a brief cooldown.`;
249            icon = '⏱️';
250            break;
251        case 'ERROR_RATE_EXCEEDED':
252            title = `High Error Rate Detected`;
253            message = `Multiple ${platform} errors occurred. Taking a brief pause to recover.`;
254            icon = '⚠️';
255            break;
256        default:
257            title = `${platform} Issue`;
258            message = `Temporary issue with ${platform}.`;
259            icon = 'ℹ️';
260    }
261
262    const notification = document.createElement('div');
263    notification.className = 'guardrail-notification';
264    notification.setAttribute('role', 'alert');
265    notification.setAttribute('aria-live', 'assertive');
266    notification.setAttribute('aria-atomic', 'true');
267    notification.innerHTML = `
268        <div class="guardrail-notification-content">
269            <div class="guardrail-notification-icon">${icon}</div>
270            <div class="guardrail-notification-text">
271                <div class="guardrail-notification-title">${title}</div>
272                <div class="guardrail-notification-message">${message}</div>
273                ${retryAfter ? `
274                    <div class="guardrail-notification-countdown">
275                        Ready to retry in: <span class="countdown-timer">${retryAfter}</span> seconds
276                    </div>
277                ` : ''}
278                <div class="guardrail-notification-footer">
279                    <span class="guardrail-help-text">Need help?</span>
280                    <button class="guardrail-contact-btn" onclick="if(window.Tawk_API){Tawk_API.maximize();}">
281                        💬 Contact Support
282                    </button>
283                </div>
284            </div>
285            <button class="guardrail-notification-close" aria-label="Close notification">×</button>
286        </div>
287    `;
288
289    // Add close button handler that clears timer
290    const closeBtn = notification.querySelector('.guardrail-notification-close');
291    closeBtn.addEventListener('click', () => {
292        if (notification._timerId) clearInterval(notification._timerId);
293        notification.remove();
294    });
295
296    // Add styles if not already present
297    if (!document.querySelector('#guardrail-notification-styles')) {
298        const styles = document.createElement('style');
299        styles.id = 'guardrail-notification-styles';
300        styles.textContent = `
301            .guardrail-notification {
302                position: fixed;
303                top: 20px;
304                right: 20px;
305                z-index: 10000;
306                background: #ffffff;
307                border: 1px solid #e5e7eb;
308                border-left: 4px solid #6366f1;
309                border-radius: 12px;
310                box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04);
311                padding: 16px 20px;
312                max-width: 400px;
313                animation: slideIn 0.3s ease-out;
314            }
315            @keyframes slideIn {
316                from { transform: translateX(20px); opacity: 0; }
317                to { transform: translateX(0); opacity: 1; }
318            }
319            .guardrail-notification-content {
320                display: flex;
321                align-items: flex-start;
322                gap: 12px;
323            }
324            .guardrail-notification-icon {
325                font-size: 28px;
326                flex-shrink: 0;
327            }
328            .guardrail-notification-text {
329                flex: 1;
330            }
331            .guardrail-notification-title {
332                font-size: 16px;
333                font-weight: 600;
334                color: #111827;
335                margin-bottom: 4px;
336            }
337            .guardrail-notification-message {
338                font-size: 14px;
339                color: #6b7280;
340                line-height: 1.4;
341            }
342            .guardrail-notification-countdown {
343                margin-top: 10px;
344                font-size: 14px;
345                color: #6366f1;
346                font-weight: 500;
347            }
348            .countdown-timer {
349                font-family: monospace;
350                font-size: 16px;
351                background: rgba(99, 102, 241, 0.1);
352                padding: 2px 8px;
353                border-radius: 4px;
354                color: #4f46e5;
355            }
356            .guardrail-notification-close {
357                background: none;
358                border: none;
359                color: #9ca3af;
360                font-size: 24px;
361                cursor: pointer;
362                padding: 0;
363                line-height: 1;
364                transition: color 0.2s;
365            }
366            .guardrail-notification-close:hover {
367                color: #374151;
368            }
369            .guardrail-notification-footer {
370                margin-top: 12px;
371                padding-top: 12px;
372                border-top: 1px solid #e5e7eb;
373                display: flex;
374                align-items: center;
375                gap: 10px;
376            }
377            .guardrail-help-text {
378                font-size: 13px;
379                color: #6b7280;
380            }
381            .guardrail-contact-btn {
382                background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
383                border: none;
384                color: white;
385                padding: 8px 16px;
386                border-radius: 6px;
387                font-size: 13px;
388                font-weight: 500;
389                cursor: pointer;
390                transition: all 0.2s;
391                display: inline-flex;
392                align-items: center;
393                justify-content: center;
394                gap: 6px;
395                line-height: 1;
396                vertical-align: middle;
397            }
398            .guardrail-contact-btn:hover {
399                background: linear-gradient(135deg, #4f46e5 0%, #4338ca 100%);
400                transform: translateY(-1px);
401            }
402            @media (max-width: 440px) {
403                .guardrail-notification {
404                    right: 10px;
405                    left: 10px;
406                    max-width: none;
407                }
408            }
409        `;
410        document.head.appendChild(styles);
411    }
412
413    document.body.appendChild(notification);
414
415    // Start countdown timer if retryAfter is provided
416    if (retryAfter && retryAfter > 0) {
417        let remaining = retryAfter;
418        notification._timerId = setInterval(() => {
419            remaining--;
420            const timerEl = notification.querySelector('.countdown-timer');
421            if (timerEl) {
422                timerEl.textContent = remaining;
423            }
424            if (remaining <= 0) {
425                clearInterval(notification._timerId);
426                notification._timerId = null;
427                const countdownEl = notification.querySelector('.guardrail-notification-countdown');
428                if (countdownEl) {
429                    countdownEl.innerHTML = '<span style="color: #10b981;">✓ Ready to retry</span>';
430                }
431                // Auto-remove after 3 more seconds
432                setTimeout(() => {
433                    if (notification._timerId) clearInterval(notification._timerId);
434                    notification.remove();
435                }, 3000);
436            }
437        }, 1000);
438    } else {
439        // Auto-remove after 10 seconds if no countdown
440        setTimeout(() => {
441            if (notification._timerId) clearInterval(notification._timerId);
442            notification.remove();
443        }, 10000);
444    }
445}
446
447/**
448 * Show error notification for non-blocking errors (Meta API errors, deployment failures, etc.)
449 * @param {string} title - Error title
450 * @param {string} message - Error message
451 * @param {string} type - 'error' | 'warning' | 'info' | 'success'
452 */
453function showErrorNotification(title, message, type = 'error') {
454    // Remove existing notification if any
455    const existing = document.querySelector('.error-notification');
456    if (existing) existing.remove();
457
458    const config = {
459        error: { icon: '❌', color: '#ef4444', showSupport: true },
460        warning: { icon: '⚠️', color: '#f59e0b', showSupport: false },
461        info: { icon: 'ℹ️', color: '#3b82f6', showSupport: false },
462        success: { icon: '✅', color: '#10b981', showSupport: false }
463    }[type] || { icon: '❌', color: '#ef4444', showSupport: true };
464
465    const notification = document.createElement('div');
466    notification.className = 'error-notification';
467    notification.setAttribute('role', type === 'error' ? 'alert' : 'status');
468    notification.setAttribute('aria-live', type === 'error' ? 'assertive' : 'polite');
469    notification.setAttribute('aria-atomic', 'true');
470    notification.innerHTML = `
471        <div class="error-notification-content">
472            <div class="error-notification-icon" aria-hidden="true">${config.icon}</div>
473            <div class="error-notification-text">
474                <div class="error-notification-title">${title}</div>
475                <div class="error-notification-message">${message}</div>
476                ${config.showSupport ? `
477                    <div class="error-notification-footer">
478                        <button class="error-contact-btn" onclick="if(window.Tawk_API){Tawk_API.maximize();}">
479                            💬 Contact Support
480                        </button>
481                    </div>
482                ` : ''}
483            </div>
484            <button class="error-notification-close" aria-label="Close notification">×</button>
485        </div>
486    `;
487
488    // Add styles if not already present
489    if (!document.querySelector('#error-notification-styles')) {
490        const styles = document.createElement('style');
491        styles.id = 'error-notification-styles';
492        styles.textContent = `
493            .error-notification {
494                position: fixed;
495                top: 20px;
496                right: 20px;
497                z-index: 10000;
498                background: #ffffff;
499                border: 1px solid #e5e7eb;
500                border-left: 4px solid var(--error-color, #ef4444);
501                border-radius: 12px;
502                box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04);
503                padding: 16px 20px;
504                max-width: 400px;
505                animation: slideInError 0.3s ease-out;
506            }
507            @keyframes slideInError {
508                from { transform: translateX(20px); opacity: 0; }
509                to { transform: translateX(0); opacity: 1; }
510            }
511            .error-notification-content {
512                display: flex;
513                align-items: flex-start;
514                gap: 12px;
515            }
516            .error-notification-icon {
517                font-size: 24px;
518                flex-shrink: 0;
519            }
520            .error-notification-text {
521                flex: 1;
522            }
523            .error-notification-title {
524                font-size: 15px;
525                font-weight: 600;
526                color: #111827;
527                margin-bottom: 4px;
528            }
529            .error-notification-message {
530                font-size: 13px;
531                color: #6b7280;
532                line-height: 1.4;
533            }
534            .error-notification-close {
535                background: none;
536                border: none;
537                color: #9ca3af;
538                font-size: 24px;
539                cursor: pointer;
540                padding: 0;
541                line-height: 1;
542                transition: color 0.2s;
543            }
544            .error-notification-close:hover {
545                color: #374151;
546            }
547            .error-notification-footer {
548                margin-top: 10px;
549            }
550            .error-contact-btn {
551                background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
552                border: none;
553                color: white;
554                padding: 6px 12px;
555                border-radius: 6px;
556                font-size: 12px;
557                font-weight: 500;
558                cursor: pointer;
559                transition: all 0.2s;
560                display: inline-flex;
561                align-items: center;
562                justify-content: center;
563                gap: 6px;
564                line-height: 1;
565            }
566            .error-contact-btn:hover {
567                background: linear-gradient(135deg, #4f46e5 0%, #4338ca 100%);
568            }
569            @media (max-width: 440px) {
570                .error-notification {
571                    right: 10px;
572                    left: 10px;
573                    max-width: none;
574                }
575            }
576        `;
577        document.head.appendChild(styles);
578    }
579
580    // Set the accent color
581    notification.style.setProperty('--error-color', config.color);
582
583    document.body.appendChild(notification);
584
585    // Add close button handler
586    const closeBtn = notification.querySelector('.error-notification-close');
587    closeBtn.addEventListener('click', () => notification.remove());
588
589    // Auto-remove based on type (success is quick, errors need more reading time)
590    const timeouts = { success: 3000, info: 5000, warning: 6000, error: 8000 };
591    setTimeout(() => notification.remove(), timeouts[type] || 5000);
592}
593
594// Export notification functions globally
595window.showGuardrailNotification = showGuardrailNotification;
596window.showErrorNotification = showErrorNotification;
597
598/**
599 * Make an authenticated API request
600 * Automatically includes Firebase token in Authorization header
601 * Auto-initializes Firebase on first use (once per session)
602 * Retries once with fresh token on 401 (handles token expiration)
603 * Redirects to login on persistent auth failure
604 * @param {string} url - API endpoint URL
605 * @param {object} options - Fetch options (method, body, etc.)
606 * @returns {Promise<Response>}
606 - Fetch response (parsed as JSON)
607 */
608async function authenticatedFetch(url, options = {}, _isRetry = false) {
609  // Auto-initialize Firebase on first API call
610  if (!firebaseInitialized) {
611    await initializeFirebaseClient();
612  }
613
614  // Use effective email (impersonated if active)
615  const effectiveEmail = window.AuthHelper?.getEffectiveUserEmail?.() || sessionStorage.getItem('userEmail') || '';
616
617  // FormData bodies must NOT get a manual Content-Type — fetch() needs to
618  // generate its own multipart boundary.
619  const headers = {
620    'X-User-Email': effectiveEmail,
621    ...(options.body instanceof FormData ? {} : { 'Content-Type': 'application/json' }),
622    ...options.headers
623  };
624
625  // Get Firebase token for authentication (force refresh on retry)
626  let token;
627  if (_isRetry && firebase.auth?.().currentUser) {
628    // Force refresh on retry - bypass cache
629    try {
630      token = await firebase.auth().currentUser.getIdToken(true);
631      storeToken(token);
632    } catch (e) {
633      // Force refresh failed - session is invalid
634      clearStoredToken();
635      token = null;
636    }
637  } else {
638    token = await getFirebaseToken();
639  }
640
641  if (token) {
642    headers['Authorization'] = `Bearer ${token}`;
643  }
644
645  // Add impersonation audit header when impersonating
646  if (window.AuthHelper?.isImpersonating?.()) {
647    headers['X-Impersonated-By'] = window.AuthHelper.getRealAdminEmail();
648  }
649
650  const response = await fetch(url, {
651    ...options,
652    headers
653  });
654
655  // Handle 401 - token might be expired, retry once with fresh token
656  if (response.status === 401 && !_isRetry && firebase.auth?.().currentUser) {
657    return authenticatedFetch(url, options, true);
658  }
659
660  // Handle persistent 401 after retry - return error, let page handle redirect
661  if (response.status === 401 && _isRetry) {
662    clearStoredToken();
663    try {
664      return await response.json();
665    } catch (e) {
666      return { error: 'Authentication failed', code: 'AUTH_FAILED' };
667    }
668  }
669
670  // Handle 429 - check for guardrail blocks (circuit breaker, rate limit)
671  if (response.status === 429) {
672    try {
673      const errorData = await response.clone().json();
674      if (errorData.code === 'CIRCUIT_OPEN' || errorData.code === 'RATE_LIMITED' || errorData.code === 'ERROR_RATE_EXCEEDED') {
675        // Determine platform from URL
676        const platform = url.includes('/api/google') ? 'Google Ads' : url.includes('/api/meta') ? 'Meta' : 'API';
677        showGuardrailNotification(errorData.code, errorData.retryAfter, platform);
678        return errorData;
679      }
680    } catch (e) {
681      // Ignore JSON parse errors
682    }
683  }
684
685  return response.json();
686}
687
688/**
689 * Handle auth failure - call this from pages that need redirect on session expiry
690 * @param {Object} response - API response to check
691 * @returns {boolean} - true if redirected, false if no auth error
692 */
693function handleAuthFailure(response) {
694  const authErrorCodes = ['UNAUTHORIZED', 'SESSION_EXPIRED', 'USER_NOT_REGISTERED', 'AUTH_FAILED'];
695  if (response?.code && authErrorCodes.includes(response.code)) {
696    sessionStorage.setItem('authRedirectReason', response.code);
697    window.location.href = '/index.html';
698    return true;
699  }
700  return false;
701}
702
703// ==========================================
704// Token Refresh Helpers
705// ==========================================
706
707/**
708 * Store token in localStorage (for other modules to access)
709 */
710function storeToken(token) {
711  localStorage.setItem('authToken', token);
712  localStorage.setItem('authTokenExpiry', (Date.now() + 3600000).toString());
713}
714
715/**
716 * Clear stored token
717 */
718function clearStoredToken() {
719  localStorage.removeItem('authToken');
720  localStorage.removeItem('authTokenExpiry');
721}
722
723/**
724 * Set up proactive token refresh (every 45 minutes)
725 * Firebase SDK handles auto-refresh, but we add extra safety
726 * Uses currentUser at execution time (not closure) to handle sign-out/sign-in
727 */
728function setupTokenRefresh() {
729  clearRefreshInterval();
730
731  refreshIntervalId = setInterval(async () => {
732    // Always get fresh currentUser reference (handles multi-tab sign out)
733    const currentUser = firebase.auth?.().currentUser;
734    if (currentUser) {
735      try {
736        const token = await currentUser.getIdToken();
737        storeToken(token);
738      } catch (e) {
739        // Silent fail - next API call will trigger refresh
740      }
741    }
742  }, 45 * 60 * 1000); // Every 45 minutes
743}
744
745/**
746 * Clear refresh interval
747 */
748function clearRefreshInterval() {
749  if (refreshIntervalId) {
750    clearInterval(refreshIntervalId);
751    refreshIntervalId = null;
752  }
753}
754
755/**
756 * Add visibility change handler - refresh token when tab becomes visible
757 * Handles sleep/background scenarios with debounce to prevent rapid refreshes
758 */
759let lastVisibilityRefresh = 0;
760function addVisibilityHandler() {
761  document.addEventListener('visibilitychange', async () => {
762    if (document.visibilityState === 'visible') {
763      const currentUser = firebase.auth?.().currentUser;
764      if (!currentUser) return;
765
766      // Debounce: only refresh if last refresh was > 30 seconds ago
767      const now = Date.now();
768      if (now - lastVisibilityRefresh < 30000) return;
769      lastVisibilityRefresh = now;
770
771      try {
772        const token = await currentUser.getIdToken(true); // Force refresh
773        storeToken(token);
774      } catch (error) {
775        // Token refresh failed - clear stored token, next API call will handle
776        clearStoredToken();
777      }
778    }
779  });
780}
781
782/**
783 * Get stored token synchronously (for backward compatibility)
784 */
785function getStoredToken() {
786  return localStorage.getItem('authToken');
787}
788
789/**
790 * Check if user is authenticated
791 */
792function isAuthenticated() {
793  return !!firebase.auth?.().currentUser;
794}
795
796// Export for use in other scripts
797window.FirebaseAuth = {
798  initialize: initializeFirebaseClient,
799  ready: authReadyPromise,  // Await this to ensure auth is ready
800  getCurrentUser,
801  getToken: getFirebaseToken,  // Auto-waits for auth to be ready
802  getStoredToken,  // Synchronous access to stored token
803  isAuthenticated,  // Check if user is logged in
804  signIn: firebaseSignIn,
805  signOut: firebaseSignOut,
806  sendPasswordReset: firebaseSendPasswordReset,
807  createUser: firebaseCreateUser,
808  onAuthStateChanged,
809  fetch: authenticatedFetch,
810  handleAuthFailure,  // Check response and redirect if auth error
811  forceRefresh: async () => {
812    const user = firebase.auth?.().currentUser;
813    if (!user) return null;
814    const token = await user.getIdToken(true);
815    storeToken(token);
816    return token;
817  }
818};
819
820// Convenient API client with shorthand methods
821window.api = {
822  fetch: authenticatedFetch,
823  getToken: getFirebaseToken,
824  handleAuthFailure,  // Check response and redirect if auth error
825
826  async get(url) {
827    return authenticatedFetch(url);
828  },
829
830  async post(url, data) {
831    return authenticatedFetch(url, {
832      method: 'POST',
833      body: JSON.stringify(data)
834    });
835  },
836
837  async put(url, data) {
838    return authenticatedFetch(url, {
839      method: 'PUT',
840      body: JSON.stringify(data)
841    });
842  },
843
844  async delete(url) {
845    return authenticatedFetch(url, {
846      method: 'DELETE'
847    });
848  },
849
850  // POST a FormData body (file uploads) — no JSON stringify, no Content-Type
851  // override (see authenticatedFetch() above for why).
852  async upload(url, formData) {
853    return authenticatedFetch(url, {
854      method: 'POST',
855      body: formData
856    });
857  }
858};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.