/** * fetch-wrapper.js - Hardened Fetch Wrapper for Qualora Enterprise * * AUTOMATIC FEATURES: * - Automatic HTTP-Only Cookie propagation (Secure JWT handling) * - CSRF Token injection from 'csrf_token' cookie to 'X-CSRF-Token' header * - 401 Unauthorized handling (Clears local UI state and redirects) * - 429 Rate Limit handling (Exponential backoff retry) * - Strict Path Traversal and Network Error resilience */ (function() { 'use strict'; const originalFetch = window.fetch; /** * Helper: Extract CSRF token from browser cookies. * Aligned with security.py cookie naming. */ function getCsrfToken() { const match = document.cookie.match(/csrf_token=([^;]+)/); return match ? match[1] : null; } /** * Handle session expiration. * Synchronized with auth-init.js to use the 'user' key. */ function handleAuthFailure() { console.warn('[Qualora] Session expired or invalid. Cleaning up.'); // Remove the UI Hint (JS can't touch the HTTP-Only cookie anyway) localStorage.removeItem('user'); sessionStorage.clear(); // Only redirect if the user is currently on a protected page const publicPaths = ['/', '/index.html', '/login', '/register']; if (!publicPaths.includes(window.location.pathname)) { window.location.href = '/'; } } /** * Internal: Enhanced fetch with exponential backoff for 429 Rate Limits */ async function fetchWithRetry(url, options, retries = 2) { for (let i = 0; i <= retries; i++) { try { const response = await originalFetch(url, options); // Handle 429 (Rate Limit) from our hardened security.py logic if (response.status === 429 && i < retries) { const delay = Math.pow(2, i) * 1000; console.warn(`[Qualora] Rate limited. Retrying in ${delay}ms...`); await new Promise(res => setTimeout(res, delay)); continue; } return response; } catch (error) { if (i === retries) throw error; const delay = Math.pow(2, i) * 500; await new Promise(res => setTimeout(res, delay)); } } } /** * Global Fetch Override */ window.fetch = async function(url, options = {}) { options.headers = options.headers || {}; // 1. JWT Security: We DO NOT inject Bearer headers. // The browser handles the HTTP-Only 'access_token' cookie automatically // because we set credentials: 'include' below. // 2. CSRF Protection: Inject header for state-changing requests const method = (options.method || 'GET').toUpperCase(); const needsCsrf = ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method); if (needsCsrf) { const csrfToken = getCsrfToken(); if (csrfToken) { options.headers['X-CSRF-Token'] = csrfToken; } } // 3. Credentials: MUST be 'include' to pass HTTP-Only cookies across origins options.credentials = 'include'; try { const response = await fetchWithRetry(url, options); // 4. Session Management if (response.status === 401) { handleAuthFailure(); } return response; } catch (error) { console.error('[Qualora] Network request failed:', error); throw error; } }; console.log('✅ Qualora Fetch Wrapper: Secure Cookie & CSRF mode active.'); })();