UNPKG

freshroute-server

Version:

Local development server for FreshRoute extension with API mocking and extended functionality

1,258 lines (1,257 loc) β€’ 72.8 kB
"use strict"; (function() { let notificationsEnabled = true; let extensionEnabled = true; let compactNotifications = true; let activeNotifications = []; let notificationCounter = 0; let isNotificationAreaHovered = false; let currentNotificationIndex = 0; let floatingToggle = null; let isDragging = false; let dragOffset = { x: 0, y: 0 }; let autoOffTimer = null; let timerInterval = null; let lastActivityTimestamp = null; const INACTIVITY_TIMEOUT = 30 * 60 * 1e3; let quickMenu = null; let isQuickMenuOpen = false; let ruleGroups = []; function waitForBody(callback) { if (document.body) { callback(); return; } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", callback); } else { const observer = new MutationObserver((mutations, obs) => { if (document.body) { obs.disconnect(); callback(); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); if (document.body) { callback(); } else { console.error("🚨 Failed to find document.body after timeout"); } }, 5e3); } } function exposeAllowedDomainsGlobally() { try { if (typeof getAllowedDomains === "function") { window.FRESHROUTE_ALLOWED_DOMAINS = getAllowedDomains(); } else if (typeof ALLOWED_DOMAINS !== "undefined" && Array.isArray(ALLOWED_DOMAINS)) { window.FRESHROUTE_ALLOWED_DOMAINS = [...ALLOWED_DOMAINS]; } else if (typeof window !== "undefined" && window.DomainValidator && typeof window.DomainValidator.getAllowedDomains === "function") { window.FRESHROUTE_ALLOWED_DOMAINS = window.DomainValidator.getAllowedDomains(); } else { window.FRESHROUTE_ALLOWED_DOMAINS = ["localhost", "127.0.0.1"]; console.warn("⚠️ Could not access allowed domains from allowed-domains.js, exposing minimal fallback"); } console.log("πŸ”— Exposed allowed domains globally:", window.FRESHROUTE_ALLOWED_DOMAINS); } catch (error) { console.error("❌ Error exposing allowed domains globally:", error); window.FRESHROUTE_ALLOWED_DOMAINS = ["localhost", "127.0.0.1"]; } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { loadSettings(); exposeAllowedDomainsGlobally(); }); } else { loadSettings(); exposeAllowedDomainsGlobally(); } async function loadSettings() { try { const settings = await safeStorageGet([ "notificationsEnabled", "extensionEnabled", "compactNotifications", "floatingTogglePosition", "floatingToggleVisible", "lastActivityTimestamp" ]); const { groups } = await safeLocalStorageGet(["groups"]); ruleGroups = groups || []; notificationsEnabled = settings.notificationsEnabled !== false; extensionEnabled = settings.extensionEnabled !== false; compactNotifications = settings.compactNotifications !== false; lastActivityTimestamp = settings.lastActivityTimestamp; await checkInactivityStatus(); createFloatingToggle(settings.floatingTogglePosition, settings.floatingToggleVisible); } catch (error) { console.log("Content script: Error loading settings"); createFloatingToggle(); } } chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === "updateNotificationSettings") { notificationsEnabled = message.enabled; } else if (message.action === "updateCompactNotificationSettings") { compactNotifications = message.enabled; if (activeNotifications.length > 0) { if (compactNotifications) { showCompactNotifications(false); } else { showAllNotifications(); } } } else if (message.action === "updateExtensionSettings") { extensionEnabled = message.enabled; updateFloatingToggleState(); if (!extensionEnabled) { clearAllNotifications(); clearInactivityTimer(); } else { startInactivityTimer(); } } else if (message.action === "ruleApplied") { updateLastActivity(); if (notificationsEnabled && extensionEnabled) { showNotification(message); } } }); window.addEventListener("message", (event) => { var _a, _b; if (event.source !== window) return; if (event.data.type === "FRESHROUTE_STATUS_REQUEST") { const manifest = safeGetManifest(); const responseData = { type: "FRESHROUTE_STATUS_REQUEST_RESPONSE", data: { version: manifest.version, enabled: extensionEnabled, notificationsEnabled, compactNotifications, activeNotifications: activeNotifications.length, extensionId: ((_a = chrome.runtime) == null ? void 0 : _a.id) || "unknown", timestamp: Date.now(), lastActivityTimestamp, inactivityTimeoutMinutes: Math.round(INACTIVITY_TIMEOUT / 1e3 / 60) } }; window.postMessage(responseData, "*"); console.log("πŸ“± Extension status requested by setup page", responseData); } else if (event.data.type === "FRESHROUTE_CONNECTION_TEST") { const manifest = safeGetManifest(); window.postMessage({ type: "FRESHROUTE_CONNECTION_TEST_RESPONSE", data: { requestTime: event.data.timestamp, responseTime: Date.now(), activeRules: activeNotifications.length, // Use notifications as proxy for activity extensionId: ((_b = chrome.runtime) == null ? void 0 : _b.id) || "unknown", version: manifest.version, enabled: extensionEnabled } }, "*"); console.log("πŸ“± Connection test requested by setup page"); } else if (event.data.type === "FRESHROUTE_PING") { const manifest = safeGetManifest(); window.postMessage({ type: "FRESHROUTE_PONG", data: { timestamp: Date.now(), version: manifest.version } }, "*"); console.log("πŸ“± Legacy ping received from setup page"); } else if (event.data.type === "FRESHROUTE_GET_ALLOWED_DOMAINS") { let allowedDomains = []; try { if (typeof getAllowedDomains === "function") { allowedDomains = getAllowedDomains(); } else if (typeof ALLOWED_DOMAINS !== "undefined" && Array.isArray(ALLOWED_DOMAINS)) { allowedDomains = [...ALLOWED_DOMAINS]; } else if (typeof window !== "undefined" && window.DomainValidator && typeof window.DomainValidator.getAllowedDomains === "function") { allowedDomains = window.DomainValidator.getAllowedDomains(); } else { allowedDomains = ["localhost", "127.0.0.1"]; console.warn("⚠️ Could not access allowed domains from allowed-domains.js, using minimal fallback"); } } catch (error) { console.error("❌ Error accessing allowed domains:", error); allowedDomains = ["localhost", "127.0.0.1"]; } window.postMessage({ type: "FRESHROUTE_GET_ALLOWED_DOMAINS_RESPONSE", data: { allowedDomains, source: "extension", timestamp: Date.now() } }, "*"); console.log("πŸ“± Allowed domains requested by setup page:", allowedDomains); } }); function showNotification(data) { var _a, _b, _c, _d, _e; const notificationId = `freshroute-notification-${++notificationCounter}`; let bgColor, icon, title, details; switch (data.ruleType) { case "delay_request": bgColor = "#FF9800"; icon = "⏱️"; title = "Request Delayed"; details = { summary: `${data.ruleName} β€’ ${data.delayMs}ms`, expanded: ` <div class="notification-source"><strong>🌐 Full URL:</strong><br><code style="font-size: 9px; word-break: break-all;">${data.url}</code></div> <div class="notification-rule"><strong>πŸ“‹ Rule:</strong> ${data.ruleName}</div> <div class="notification-detail"><strong>⏱️ Delay Details:</strong><br> β€’ Delayed by ${data.delayMs}ms (${(data.delayMs / 1e3).toFixed(1)} seconds)<br> β€’ Request will proceed after delay<br> β€’ Type: Artificial network slowdown simulation </div> <div class="notification-timestamp"><strong>πŸ•’ Applied:</strong> ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}</div> ` }; break; case "modify_headers": case "header_modification": bgColor = "#2196F3"; icon = "πŸ“"; title = "Headers Modified"; let headerDetails = ""; if (data.headers && Array.isArray(data.headers)) { headerDetails = data.headers.map((h) => `β€’ ${h.operation.toUpperCase()}: ${h.name}${h.value ? ` = "${h.value}"` : ""}`).join("<br>"); } else { headerDetails = "β€’ Headers modified for this request<br>β€’ Check browser dev tools Network tab for details"; } details = { summary: `${data.ruleName} β€’ Headers updated`, expanded: ` <div class="notification-source"><strong>🌐 Full URL:</strong><br><code style="font-size: 9px; word-break: break-all;">${data.url}</code></div> <div class="notification-rule"><strong>πŸ“‹ Rule:</strong> ${data.ruleName}</div> <div class="notification-detail"><strong>πŸ“ Header Modifications:</strong><br> ${headerDetails} </div> <div class="notification-technical"><strong>πŸ”§ Technical Info:</strong><br> β€’ Target: ${data.target || "Request"} headers<br> β€’ Method: declarativeNetRequest API<br> β€’ Scope: This request only </div> <div class="notification-timestamp"><strong>πŸ•’ Applied:</strong> ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}</div> ` }; break; case "mock_response": bgColor = "#9C27B0"; icon = "🎭"; title = "Mock Response"; details = { summary: `${data.ruleName} β€’ ${((_a = data.responseType) == null ? void 0 : _a.toUpperCase()) || "JSON"} (${data.statusCode || 200})`, expanded: ` <div class="notification-source"><strong>🌐 Intercepted URL:</strong><br><code style="font-size: 9px; word-break: break-all;">${data.url}</code></div> <div class="notification-rule"><strong>πŸ“‹ Rule:</strong> ${data.ruleName}</div> <div class="notification-detail"><strong>🎭 Mock Response Details:</strong><br> β€’ Method: ${data.method || "ANY"}<br> β€’ Response Type: ${(data.responseType || "json").toUpperCase()}<br> β€’ Status Code: ${data.statusCode || 200}<br> β€’ Mock data returned instead of real API call </div> <div class="notification-technical"><strong>πŸ”§ Technical Info:</strong><br> β€’ Type: API Response Mocking<br> β€’ Method: Fetch/XHR interception<br> β€’ Scope: This request only </div> <div class="notification-timestamp"><strong>πŸ•’ Applied:</strong> ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}</div> ` }; break; case "url_rewrite": default: bgColor = "#4CAF50"; icon = "πŸ”—"; title = "URL Rewritten"; const hasRedirect = data.redirectUrl && data.redirectUrl !== data.url; let rewriteDetails = ""; if (hasRedirect) { const sourceUrl = data.url; const targetUrl = data.redirectUrl; let transformationInfo = ""; if (data.captureGroups) { transformationInfo = `<br><strong>🎯 Capture Groups:</strong><br>${data.captureGroups.map((group, i) => `β€’ $${i + 1}: "${group}"`).join("<br>")}`; } else { const sourceDomain = ((_b = sourceUrl.match(/\/\/([^\/]+)/)) == null ? void 0 : _b[1]) || "unknown"; const targetDomain = ((_c = targetUrl.match(/\/\/([^\/]+)/)) == null ? void 0 : _c[1]) || "unknown"; if (sourceDomain !== targetDomain) { transformationInfo = `<br><strong>πŸ”„ Domain Change:</strong><br>β€’ From: ${sourceDomain}<br>β€’ To: ${targetDomain}`; } const sourcePort = (_d = sourceUrl.match(/:(\d+)\//)) == null ? void 0 : _d[1]; const targetPort = (_e = targetUrl.match(/:(\d+)\//)) == null ? void 0 : _e[1]; if (sourcePort && targetPort && sourcePort !== targetPort) { transformationInfo += `<br><strong>πŸ”Œ Port Change:</strong> ${sourcePort} β†’ ${targetPort}`; } } rewriteDetails = ` <div class="notification-source"><strong>🌐 Original URL:</strong><br><code style="font-size: 9px; word-break: break-all;">${sourceUrl}</code></div> <div class="notification-target"><strong>🎯 Redirected To:</strong><br><code style="font-size: 9px; word-break: break-all;">${targetUrl}</code></div> <div class="notification-rule"><strong>πŸ“‹ Rule:</strong> ${data.ruleName}</div> <div class="notification-detail"><strong>πŸ”„ Transformation Details:</strong>${transformationInfo}</div> <div class="notification-technical"><strong>πŸ”§ Technical Info:</strong><br> β€’ Type: URL Redirect (${data.method || "declarativeNetRequest"})<br> β€’ Status: ${data.statusCode || "Active"}<br> β€’ Scope: Navigation request </div> `; } else { rewriteDetails = ` <div class="notification-source"><strong>🌐 Matched URL:</strong><br><code style="font-size: 9px; word-break: break-all;">${data.url}</code></div> <div class="notification-rule"><strong>πŸ“‹ Rule:</strong> ${data.ruleName}</div> <div class="notification-detail"><strong>βœ… Pattern Matched:</strong><br> β€’ Rule pattern successfully matched this URL<br> β€’ No redirect configured (pattern-only rule)<br> β€’ Rule is active and monitoring </div> `; } details = { summary: `${data.ruleName} β€’ ${hasRedirect ? "Redirected" : "Matched"}`, expanded: ` ${rewriteDetails} <div class="notification-timestamp"><strong>πŸ•’ Applied:</strong> ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}</div> ` }; break; } const notificationData = { id: notificationId, timestamp: Date.now(), bgColor, icon, title, details, autoRemoveTimer: null, remainingTime: 5e3, isPaused: false }; activeNotifications.push(notificationData); if (compactNotifications) { let preserveExpandedState = false; if (activeNotifications.length > 1) { const currentNotification = activeNotifications[currentNotificationIndex]; preserveExpandedState = currentNotification && currentNotification.expanded; } currentNotificationIndex = activeNotifications.length - 1; if (preserveExpandedState) { activeNotifications[currentNotificationIndex].expanded = true; } showCompactNotifications(true); } else { showStandardNotification(notificationData); } console.log(`πŸ“± Notification shown: ${title} (${activeNotifications.length} active, compact: ${compactNotifications}${compactNotifications ? ", no auto-removal" : ", auto-remove: 5s"})`); } function calculateNotificationPosition() { let totalHeight = 20; activeNotifications.forEach((notification) => { const notificationDiv = notification.element.querySelector(".freshroute-notification"); if (notificationDiv) { const isExpanded = notificationDiv.getAttribute("data-expanded") === "true"; totalHeight += isExpanded ? 75 : 42; } }); return totalHeight; } function toggleNotification(notificationId) { const notificationData = activeNotifications.find((n) => n.id === notificationId); if (!notificationData) return; const notificationDiv = notificationData.element.querySelector(".freshroute-notification"); const expandedContent = notificationData.element.querySelector(".expanded-content"); const expandButton = notificationData.element.querySelector(".notification-expand"); if (!notificationDiv || !expandedContent || !expandButton) return; const isExpanded = notificationDiv.getAttribute("data-expanded") === "true"; if (isExpanded) { notificationDiv.setAttribute("data-expanded", "false"); notificationDiv.classList.add("collapsed"); notificationDiv.style.height = "32px"; expandedContent.style.opacity = "0"; expandedContent.style.maxHeight = "0"; expandedContent.style.paddingTop = "0"; expandButton.textContent = "β–Ό"; console.log(`πŸ“± Notification collapsed: ${notificationId}`); } else { notificationDiv.setAttribute("data-expanded", "true"); notificationDiv.classList.remove("collapsed"); notificationDiv.style.height = "65px"; expandedContent.style.opacity = "1"; expandedContent.style.maxHeight = "45px"; expandedContent.style.paddingTop = "6px"; expandButton.textContent = "β–²"; console.log(`πŸ“± Notification expanded: ${notificationId}`); } setTimeout(() => { repositionNotifications(); }, 100); } function removeNotification(notificationId) { const index = activeNotifications.findIndex((n) => n.id === notificationId); if (index !== -1) { const notification = activeNotifications[index]; console.log(`πŸ“± Removing notification: ${notificationId} (${activeNotifications.length - 1} will remain)`); if (notification.autoRemoveTimer) { clearTimeout(notification.autoRemoveTimer); notification.autoRemoveTimer = null; } if (compactNotifications) { if (currentNotificationIndex === index) { if (activeNotifications.length > 1) { if (currentNotificationIndex >= activeNotifications.length - 1) { currentNotificationIndex = Math.max(0, activeNotifications.length - 2); } } } else if (currentNotificationIndex > index) { currentNotificationIndex--; } activeNotifications.splice(index, 1); if (activeNotifications.length > 0) { showCompactNotifications(false); } else { clearCompactNotification(); currentNotificationIndex = 0; } } else { if (notification.element) { const notificationDiv = notification.element.querySelector(".freshroute-notification"); if (notificationDiv) { notificationDiv.style.transform = "translateX(100%)"; notificationDiv.style.opacity = "0"; } } activeNotifications.splice(index, 1); repositionNotifications(); setTimeout(() => { if (notification.element && notification.element.parentNode) { notification.element.remove(); console.log(`πŸ“± Notification DOM removed: ${notificationId}`); } }, 300); } } else { console.log(`πŸ“± Notification not found: ${notificationId}`); } } function repositionNotifications() { let currentTop = 20; activeNotifications.forEach((notification, index) => { const notificationDiv = notification.element.querySelector(".freshroute-notification"); if (notificationDiv) { notificationDiv.style.top = `${currentTop}px`; const isExpanded = notificationDiv.getAttribute("data-expanded") === "true"; currentTop += isExpanded ? 75 : 42; } }); console.log(`πŸ“± Repositioned ${activeNotifications.length} notifications with variable heights`); } function clearAllNotifications() { activeNotifications.forEach((notification) => { if (notification.autoRemoveTimer) { clearTimeout(notification.autoRemoveTimer); notification.autoRemoveTimer = null; } if (notification.element && notification.element.parentNode) { notification.element.remove(); } }); clearCompactNotification(); activeNotifications = []; currentNotificationIndex = 0; console.log("πŸ“± All notifications cleared (timers stopped)"); } window.FreshRouteNotifications = { clear: clearAllNotifications, count: () => activeNotifications.length, list: () => activeNotifications.map((n) => ({ id: n.id, timestamp: n.timestamp })), mode: () => compactNotifications ? "compact" : "standard", currentIndex: () => compactNotifications ? currentNotificationIndex : -1, clearAll: () => { console.log(`πŸ—‘οΈ Manually clearing all ${activeNotifications.length} notifications`); clearAllNotifications(); }, test: (type = "url_rewrite") => { const testData = { "url_rewrite": { action: "ruleApplied", ruleName: "Development Environment Redirect", ruleType: "url_rewrite", url: "https://production-api.example.com/v1/users/12345/profile", redirectUrl: "https://localhost:3000/api/v1/users/12345/profile" }, "modify_headers": { action: "ruleApplied", ruleName: "API Authentication Headers", ruleType: "modify_headers", url: "https://api.freshservice.com/api/v2/tickets?per_page=100&filter=status:open" }, "delay_request": { action: "ruleApplied", ruleName: "Slow Network Simulation", ruleType: "delay_request", url: "https://api.github.com/repos/octocat/Hello-World/issues", delayMs: 2500 } }; showNotification(testData[type] || testData.url_rewrite); }, testMultiple: (count = 5) => { const types = ["url_rewrite", "modify_headers", "delay_request"]; for (let i = 0; i < count; i++) { setTimeout(() => { const type = types[i % types.length]; const testData = { ruleName: `Test Rule ${i + 1}`, ruleType: type, url: `https://example${i + 1}.com/api/test`, redirectUrl: type === "url_rewrite" ? `https://localhost:300${i}/api/test` : void 0, delayMs: type === "delay_request" ? 1e3 + i * 500 : void 0 }; showNotification(testData); }, i * 300); } }, toggleMode: () => { compactNotifications = !compactNotifications; console.log(`πŸ“± Switched to ${compactNotifications ? "compact" : "standard"} mode`); if (activeNotifications.length > 0) { if (compactNotifications) { showCompactNotifications(false); } else { showAllNotifications(); } } } }; window.addEventListener("beforeunload", () => { clearAllNotifications(); clearInactivityTimer(); saveFloatingTogglePosition(); }); async function safeStorageSet(data) { try { if (chrome && chrome.storage && chrome.storage.sync) { await chrome.storage.sync.set(data); return true; } } catch (error) { if (error.message.includes("Extension context invalidated")) { console.log("πŸ”„ Extension context invalidated - storage operation skipped"); return false; } console.error("Storage error:", error); return false; } return false; } function safeSendMessage(message) { try { if (chrome && chrome.runtime && chrome.runtime.sendMessage) { chrome.runtime.sendMessage(message); return true; } } catch (error) { if (error.message.includes("Extension context invalidated")) { console.log("πŸ”„ Extension context invalidated - message sending skipped"); return false; } console.error("Message sending error:", error); return false; } return false; } async function safeLocalStorageSet(data) { try { if (chrome && chrome.storage && chrome.storage.local) { await chrome.storage.local.set(data); return true; } } catch (error) { if (error.message.includes("Extension context invalidated")) { console.log("πŸ”„ Extension context invalidated - local storage operation skipped"); return false; } console.error("Local storage error:", error); return false; } return false; } async function safeStorageGet(keys) { try { if (chrome && chrome.storage && chrome.storage.sync) { return await chrome.storage.sync.get(keys); } } catch (error) { if (error.message.includes("Extension context invalidated")) { console.log("πŸ”„ Extension context invalidated - storage get operation skipped"); return {}; } console.error("Storage get error:", error); return {}; } return {}; } async function safeLocalStorageGet(keys) { try { if (chrome && chrome.storage && chrome.storage.local) { return await chrome.storage.local.get(keys); } } catch (error) { if (error.message.includes("Extension context invalidated")) { console.log("πŸ”„ Extension context invalidated - local storage get operation skipped"); return {}; } console.error("Local storage get error:", error); return {}; } return {}; } function safeGetManifest() { try { if (chrome && chrome.runtime && chrome.runtime.getManifest) { return chrome.runtime.getManifest(); } } catch (error) { if (error.message.includes("Extension context invalidated")) { console.log("πŸ”„ Extension context invalidated - manifest access skipped"); return { version: "unknown" }; } console.error("Manifest access error:", error); return { version: "unknown" }; } return { version: "unknown" }; } function updateLastActivity() { const now = Date.now(); lastActivityTimestamp = now; safeStorageSet({ lastActivityTimestamp: now }); startInactivityTimer(); console.log("🎯 Rule activity recorded - inactivity timer reset"); } async function checkInactivityStatus() { if (!extensionEnabled || !lastActivityTimestamp) { if (extensionEnabled) { startInactivityTimer(); } return; } const now = Date.now(); const timeSinceLastActivity = now - lastActivityTimestamp; if (timeSinceLastActivity >= INACTIVITY_TIMEOUT) { await turnOffExtension(); console.log("πŸ•’ Extension auto-disabled after 30 minutes of inactivity"); } else { const remainingTime = INACTIVITY_TIMEOUT - timeSinceLastActivity; startInactivityTimer(remainingTime); } } function startInactivityTimer(customDuration = null) { if (!extensionEnabled) return; clearInactivityTimer(); const duration = customDuration || INACTIVITY_TIMEOUT; autoOffTimer = setTimeout(async () => { await turnOffExtension(); console.log("πŸ•’ Extension auto-disabled after 30 minutes of inactivity"); }, duration); startTimerDisplay(); console.log(`⏰ Inactivity timer started: ${Math.round(duration / 1e3 / 60)} minutes until auto-off`); } function startTimerDisplay() { clearInterval(timerInterval); timerInterval = setInterval(() => { if (!extensionEnabled || !lastActivityTimestamp) { clearInterval(timerInterval); return; } updateFloatingToggleTimer(); }, 1e3); } function clearInactivityTimer() { if (autoOffTimer) { clearTimeout(autoOffTimer); autoOffTimer = null; } if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } console.log("⏰ Inactivity timer cleared"); } async function turnOffExtension() { extensionEnabled = false; await safeStorageSet({ extensionEnabled: false }); clearInactivityTimer(); updateFloatingToggleState(); clearAllNotifications(); safeSendMessage({ action: "updateExtensionEnabled", enabled: false }); console.log("πŸ”΄ Extension turned off automatically due to inactivity"); } function formatTimeRemaining() { if (!extensionEnabled || !lastActivityTimestamp) return ""; const now = Date.now(); const timeSinceLastActivity = now - lastActivityTimestamp; const timeRemaining = INACTIVITY_TIMEOUT - timeSinceLastActivity; if (timeRemaining <= 0) return "0m"; const minutes = Math.floor(timeRemaining / (1e3 * 60)); const seconds = Math.floor(timeRemaining % (1e3 * 60) / 1e3); if (minutes > 0) { return `${minutes}m`; } else { return `${seconds}s`; } } function createFloatingToggle(savedPosition = null, isVisible = true) { if (floatingToggle) { floatingToggle.remove(); } if (window.location.href.includes("chrome://") || window.location.href.includes("chrome-extension://") || window.location.href.includes("moz-extension://")) { return; } floatingToggle = document.createElement("div"); floatingToggle.id = "freshroute-floating-toggle"; floatingToggle.className = "freshroute-floating-toggle"; floatingToggle.setAttribute("data-freshroute-extension", "active"); const timeRemaining = formatTimeRemaining(); const toggleWidth = extensionEnabled && timeRemaining ? 130 : 80; const defaultPosition = { x: window.innerWidth - toggleWidth - 20, // 20px padding from right edge y: 20 // 20px padding from top edge }; const position = savedPosition || defaultPosition; const timerDisplay = extensionEnabled && timeRemaining ? ` β€’ ${timeRemaining}` : ""; floatingToggle.innerHTML = ` <div class="floating-toggle-content" style=" position: fixed; top: ${position.y}px; left: ${position.x}px; width: ${extensionEnabled && timeRemaining ? "130px" : "80px"}; height: 32px; background: ${extensionEnabled ? "#4CAF50" : "#f44336"}; border-radius: 16px; display: flex; align-items: center; justify-content: center; cursor: grab; box-shadow: 0 2px 10px rgba(0,0,0,0.3); z-index: 999999; transition: all 0.3s ease; user-select: none; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; opacity: ${isVisible !== false ? "0.8" : "0.3"}; padding: 0 8px; "> <div class="toggle-icon" style=" color: white; font-size: 12px; font-weight: bold; pointer-events: none; margin-left: 4px; ">${extensionEnabled ? "πŸ”—" : "❌"}</div> <div class="toggle-text" style=" color: white; font-size: 8px; font-weight: 600; margin-left: 3px; pointer-events: none; white-space: nowrap; ">${extensionEnabled ? "ON" : "OFF"}${timerDisplay}</div> </div> `; const toggleContent = floatingToggle.querySelector(".floating-toggle-content"); floatingToggle.addEventListener("mouseenter", () => { if (!isDragging) { toggleContent.style.opacity = "1"; toggleContent.style.transform = "scale(1.05)"; toggleContent.title = "Left-click: Toggle ON/OFF\nRight-click: Quick Menu\nDouble-click: Hide/Show\nDrag: Move position\n\nAuto-off: After 30min of inactivity"; } }); floatingToggle.addEventListener("mouseleave", () => { if (!isDragging) { toggleContent.style.opacity = isVisible !== false ? "0.8" : "0.3"; toggleContent.style.transform = "scale(1)"; } }); addDragFunctionality(floatingToggle); floatingToggle.addEventListener("click", (e) => { if (!isDragging) { toggleExtension(); } }); floatingToggle.addEventListener("contextmenu", (e) => { e.preventDefault(); if (!isDragging) { toggleQuickMenu(); } }); floatingToggle.addEventListener("dblclick", (e) => { e.preventDefault(); toggleFloatingToggleVisibility(); }); if (document.body) { document.body.appendChild(floatingToggle); if (extensionEnabled) { startTimerDisplay(); } console.log("πŸŽ›οΈ Floating toggle created"); } else { console.log("⚠️ DOM not ready, waiting for body..."); waitForBody(() => { if (document.body && floatingToggle) { document.body.appendChild(floatingToggle); if (extensionEnabled) { startTimerDisplay(); } console.log("πŸŽ›οΈ Floating toggle created (after DOM ready)"); } }); } } function addDragFunctionality(element) { const toggleContent = element.querySelector(".floating-toggle-content"); element.addEventListener("mousedown", (e) => { isDragging = false; const rect = toggleContent.getBoundingClientRect(); dragOffset.x = e.clientX - rect.left; dragOffset.y = e.clientY - rect.top; toggleContent.style.cursor = "grabbing"; toggleContent.style.transition = "none"; document.addEventListener("mousemove", handleDrag); document.addEventListener("mouseup", handleDragEnd); e.preventDefault(); }); } function handleDrag(e) { if (!floatingToggle) return; isDragging = true; const toggleContent = floatingToggle.querySelector(".floating-toggle-content"); let newX = e.clientX - dragOffset.x; let newY = e.clientY - dragOffset.y; const toggleWidth = toggleContent.offsetWidth; const maxX = window.innerWidth - toggleWidth; const maxY = window.innerHeight - 32; newX = Math.max(0, Math.min(newX, maxX)); newY = Math.max(0, Math.min(newY, maxY)); toggleContent.style.left = newX + "px"; toggleContent.style.top = newY + "px"; } function handleDragEnd(e) { const toggleContent = floatingToggle == null ? void 0 : floatingToggle.querySelector(".floating-toggle-content"); if (toggleContent) { toggleContent.style.cursor = "grab"; toggleContent.style.transition = "all 0.3s ease"; } document.removeEventListener("mousemove", handleDrag); document.removeEventListener("mouseup", handleDragEnd); saveFloatingTogglePosition(); setTimeout(() => { isDragging = false; }, 100); } function toggleExtension() { extensionEnabled = !extensionEnabled; updateFloatingToggleState(); safeStorageSet({ extensionEnabled }); if (extensionEnabled) { updateLastActivity(); } else { clearInactivityTimer(); clearAllNotifications(); } safeSendMessage({ action: "updateExtensionEnabled", enabled: extensionEnabled }); console.log(`πŸŽ›οΈ Extension ${extensionEnabled ? "enabled" : "disabled"} via floating toggle`); } function updateFloatingToggleState() { if (!floatingToggle) return; const toggleContent = floatingToggle.querySelector(".floating-toggle-content"); const toggleIcon = floatingToggle.querySelector(".toggle-icon"); const toggleText = floatingToggle.querySelector(".toggle-text"); if (toggleContent && toggleIcon && toggleText) { const timeRemaining = formatTimeRemaining(); const timerDisplay = extensionEnabled && timeRemaining ? ` β€’ ${timeRemaining}` : ""; let backgroundColor = extensionEnabled ? "#4CAF50" : "#f44336"; if (isQuickMenuOpen) { backgroundColor = "#667eea"; } toggleContent.style.background = backgroundColor; toggleContent.style.width = extensionEnabled && timeRemaining ? "130px" : "80px"; toggleIcon.textContent = extensionEnabled ? "πŸ”—" : "❌"; toggleText.innerHTML = `${extensionEnabled ? "ON" : "OFF"}${timerDisplay}`; } } function updateFloatingToggleTimer() { if (!floatingToggle || !extensionEnabled) return; const toggleText = floatingToggle.querySelector(".toggle-text"); const toggleContent = floatingToggle.querySelector(".floating-toggle-content"); if (toggleText && toggleContent) { const timeRemaining = formatTimeRemaining(); const timerDisplay = timeRemaining ? ` β€’ ${timeRemaining}` : ""; toggleContent.style.width = timeRemaining ? "130px" : "80px"; toggleText.innerHTML = `ON${timerDisplay}`; } } function toggleFloatingToggleVisibility() { if (!floatingToggle) return; const toggleContent = floatingToggle.querySelector(".floating-toggle-content"); const currentOpacity = parseFloat(toggleContent.style.opacity); const isVisible = currentOpacity > 0.5; const newOpacity = isVisible ? "0.3" : "0.8"; toggleContent.style.opacity = newOpacity; safeStorageSet({ floatingToggleVisible: !isVisible }); console.log(`πŸŽ›οΈ Floating toggle ${isVisible ? "hidden" : "shown"}`); } function saveFloatingTogglePosition() { if (!floatingToggle) return; const toggleContent = floatingToggle.querySelector(".floating-toggle-content"); if (toggleContent) { const rect = toggleContent.getBoundingClientRect(); const position = { x: rect.left, y: rect.top }; safeStorageSet({ floatingTogglePosition: position }); } } function toggleQuickMenu() { if (isQuickMenuOpen) { closeQuickMenu(); } else { openQuickMenu(); } } function openQuickMenu() { if (!floatingToggle || isQuickMenuOpen) return; closeQuickMenu(); const toggleContent = floatingToggle.querySelector(".floating-toggle-content"); const toggleRect = toggleContent.getBoundingClientRect(); quickMenu = document.createElement("div"); quickMenu.id = "freshroute-quick-menu"; quickMenu.className = "freshroute-quick-menu"; const menuTop = toggleRect.bottom + 8; const menuLeft = toggleRect.left; quickMenu.innerHTML = ` <div class="quick-menu-content" style=" position: fixed; top: ${menuTop}px; left: ${menuLeft}px; background: #ffffff; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.2); z-index: 1000000; min-width: 280px; max-width: 320px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; border: 1px solid rgba(0,0,0,0.1); overflow: hidden; transform: scale(0.95) translateY(-10px); opacity: 0; transition: all 0.2s ease; "> <div class="menu-header" style=" padding: 16px 20px 12px; border-bottom: 1px solid rgba(0,0,0,0.08); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; "> <div style="font-size: 14px; font-weight: 600; margin-bottom: 4px;"> πŸ”— FreshRoute Quick Menu </div> <div style="font-size: 11px; opacity: 0.9;"> Right-click to toggle groups β€’ Click outside to close </div> </div> <div class="menu-body" style=" max-height: 300px; overflow-y: auto; "> ${generateGroupsList()} </div> </div> `; if (document.body) { document.body.appendChild(quickMenu); } else { waitForBody(() => { if (document.body && quickMenu) { document.body.appendChild(quickMenu); } }); } setTimeout(() => { const content = quickMenu.querySelector(".quick-menu-content"); if (content) { content.style.transform = "scale(1) translateY(0)"; content.style.opacity = "1"; } }, 10); addQuickMenuListeners(); isQuickMenuOpen = true; updateFloatingToggleState(); console.log("πŸŽ›οΈ Quick menu opened"); } function generateGroupsList() { if (!ruleGroups || ruleGroups.length === 0) { return ` <div class="no-groups" style=" padding: 32px 20px; text-align: center; color: #666; "> <div style="font-size: 24px; margin-bottom: 8px;">πŸ“‹</div> <div style="font-size: 13px; font-weight: 500; margin-bottom: 4px;">No Rule Groups</div> <div style="font-size: 11px; color: #999;">Create groups in the main settings</div> </div> `; } return ruleGroups.map((group) => { const isEnabled = group.enabled !== false; const ruleCount = group.rules ? group.rules.length : 0; const activeRules = group.rules ? group.rules.filter((r) => r.enabled !== false).length : 0; return ` <div class="group-item" data-group-id="${group.id}" style=" padding: 14px 20px; border-bottom: 1px solid rgba(0,0,0,0.05); cursor: pointer; transition: background 0.2s ease; display: flex; align-items: center; justify-content: space-between; "> <div class="group-info" style="flex: 1; min-width: 0;"> <div class="group-name" style=" font-size: 13px; font-weight: 500; color: ${isEnabled ? "#333" : "#999"}; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; ">${group.name || "Unnamed Group"}</div> <div class="group-stats" style=" font-size: 10px; color: #666; ">${activeRules}/${ruleCount} rules active</div> </div> <div class="group-toggle" style=" width: 36px; height: 20px; border-radius: 10px; background: ${isEnabled ? "#4CAF50" : "#ddd"}; position: relative; transition: background 0.3s ease; margin-left: 12px; flex-shrink: 0; "> <div style=" width: 16px; height: 16px; border-radius: 8px; background: white; position: absolute; top: 2px; left: ${isEnabled ? "18px" : "2px"}; transition: left 0.3s ease; box-shadow: 0 1px 3px rgba(0,0,0,0.2); "></div> </div> </div> `; }).join(""); } function addQuickMenuListeners() { if (!quickMenu) return; const groupItems = quickMenu.querySelectorAll(".group-item"); groupItems.forEach((item) => { item.addEventListener("click", (e) => { e.stopPropagation(); const groupId = item.dataset.groupId; toggleRuleGroup(groupId); }); item.addEventListener("mouseenter", () => { item.style.background = "rgba(102, 126, 234, 0.05)"; }); item.addEventListener("mouseleave", () => { item.style.background = "transparent"; }); }); document.addEventListener("click", handleOutsideClick); document.addEventListener("contextmenu", handleOutsideClick); } function handleOutsideClick(e) { if (quickMenu && !quickMenu.contains(e.target) && !floatingToggle.contains(e.target)) { closeQuickMenu(); } } async function toggleRuleGroup(groupId) { if (!groupId) return; const groupIndex = ruleGroups.findIndex((g) => g.id === groupId); if (groupIndex === -1) return; ruleGroups[groupIndex].enabled = !ruleGroups[groupIndex].enabled; await safeLocalStorageSet({ groups: ruleGroups }); safeSendMessage({ action: "updateRules" }); updateQuickMenuDisplay(); console.log(`πŸŽ›οΈ Group "${ruleGroups[groupIndex].name}" ${ruleGroups[groupIndex].enabled ? "enabled" : "disabled"}`); } function updateQuickMenuDisplay() { if (!quickMenu) return; const menuBody = quickMenu.querySelector(".menu-body"); if (menuBody) { menuBody.innerHTML = generateGroupsList(); addQuickMenuListeners(); } } function closeQuickMenu() { if (!quickMenu || !isQuickMenuOpen) return; document.removeEventListener("click", handleOutsideClick); document.removeEventListener("contextmenu", handleOutsideClick); const content = quickMenu.querySelector(".quick-menu-content"); if (content) { content.style.transform = "scale(0.95) translateY(-10px)"; content.style.opacity = "0"; } setTimeout(() => { if (quickMenu && quickMenu.parentNode) { quickMenu.remove(); } quickMenu = null; isQuickMenuOpen = false; updateFloatingToggleState(); }, 200); console.log("πŸŽ›οΈ Quick menu closed"); } function injectResponseHeaderDebugger() { window.debugExtensionHeaders = function() { console.log("πŸ› οΈ Extension CORS Handler Active"); console.log("πŸ“Š Current document headers:", document.location.href); fetch(document.location.href, { method: "HEAD" }).then((response) => { console.log("πŸ“¦ Response headers:"); const extensionHeaders = []; const allHeaders = []; for (const [key, value] of response.headers.entries()) { allHeaders.push(` ${key}: ${value}`); if (key.toLowerCase().startsWith("x-extension") || key.toLowerCase().startsWith("x-debug") || key.toLowerCase() === "cache-control") { extensionHeaders.push(` ${key}: ${value}`); } } console.log("All headers:"); allHeaders.forEach((header) => console.log(header)); if (extensionHeaders.length > 0) { console.log("🎯 Extension-modified headers:"); extensionHeaders.forEach((header) => console.log(header)); } else { console.log("⚠️ No extension-modified headers found"); } return { allHeaders, extensionHeaders }; }).catch((e) => { console.log("❌ Could not fetch headers:", e.message); return { error: e.message }; }); }; window.testExtensionHeaders = async function(url) { const testUrl = url || document.location.href; console.log(`πŸ§ͺ Testing headers for: ${testUrl}`); try { const response = await fetch(testUrl, { method: "HEAD", mode: "cors", credentials: "include" }); const result = { url: response.url, status: response.status, extensionHeaders: {}, allHeaders: {} }; for (const [key, value] of response.headers.entries()) { result.allHeaders[key] = value; if (key.toLowerCase().startsWith("x-") || key.toLowerCase() === "cache-control") { result.extensionHeaders[key] = value; } } console.table(result.extensionHeaders); return result; } catch (error) { console.error("❌ Header test failed:", error); return { error: error.message }; } }; console.log("πŸ” Debug functions available:"); console.log(" - debugExtensionHeaders() - Detailed header analysis"); console.log(" - testExtensionHeaders(url) - Test specific URL headers"); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", injectResponseHeaderDebugger); } else { injectResponseHeaderDebugger(); } console.log("πŸš€ URL Rewriter Content Script loaded"); function startNotificationTimer(notificationId) { const notificationData = activeNotifications.find((n) => n.id === notificationId); if (!notificationData) return; console.log(`⏱️ Starting timer for notification: ${notificationId} (${notificationData.remainingTime}ms)`); notificationData.autoRemoveTimer = setTimeout(() => { removeNotification(notificationId); }, notificationData.remainingTime); notificationData.startTime = Date.now(); notificationData.isPaused = false; } function pauseAllNotifications() { if (isNotificationAreaHovered) return; isNotificationAreaHovered = true; console.log(`⏸️ Pausing ALL notifications (${activeNotifications.length} active)`); activeNotifications.forEach((notificationData) => { if (notificationData.isPaused) return; if (notificationData.autoRemoveTimer) { clearTimeout(notificationData.autoRemoveTimer); notificationData.autoRemoveTimer = null; } const elapsed = Date.now() - notificationData.startTime; notificationData.remainingTime = Math.max(0, notificationData.remainingTime - elapsed); notificationData.isPaused = true; const notificationDiv = notificationData.element.querySelector(".freshroute-notification"); if (notificationDiv) { notificationDiv.style.boxShadow = "0 3px 12px rgba(0,0,0,0.3), 0 0 0 2px rgba(255,255,255,0.3)"; } }); } function resumeAllNotifications() { if (!isNotificationAreaHovered) return; setTimeout(() => { const anyHovered = activeNotifications.some((notificationData) => { const notificationDiv = notificationData.element.querySelector(".freshroute-notification"); return notificationDiv && notificationDiv.matches(":hover"); }); if (anyHovered) return; isNotificationAreaHovered = false; console.log(`▢️ Resuming ALL notifications (${activeNotifications.length} active)`); activeNotifications.forEach((notificationData) => { if (!notificationData.isPaused) return; const notificationDiv = notificationData.element.querySelector(".freshroute-notification"); if (notificationDiv) { notificationDiv.style.boxShadow = "0 3px 12px rgba(0,0,0,0.2)"; } if (notificationData.remainingTime > 0) { notificationData.autoRemoveTimer = setTimeout(() => { removeNotification(notificationData.id); }, notificationData.remainingTime); notificationData.startTime = Date.now(); notificationData.isPaused = false;