const REQUEST_TYPE = 'AB_TAB_GROUP_REQUEST'; const PANEL_GET_STATE = 'AB_PANEL_GET_STATE'; const PANEL_CLOSE_OTHER_TABS = 'AB_PANEL_CLOSE_OTHER_SESSION_TABS'; const PANEL_FOCUS_SESSION = 'AB_PANEL_FOCUS_SESSION'; const PANEL_CLEAN_EMPTY_GROUPS = 'AB_PANEL_CLEAN_EMPTY_GROUPS'; const PANEL_SET_POLICY = 'AB_PANEL_SET_POLICY'; const DEFAULT_GROUP_TITLE = 'Agent Browser Stealth'; const DOWNLOAD_ARCHIVE_ROOT = 'agent-browser-stealth'; const STORAGE_POLICY_KEY = 'abSessionPoliciesV1'; const GROUP_COLORS = ['blue', 'green', 'pink', 'orange', 'purple', 'cyan', 'red', 'yellow']; const RISKY_TLDS = new Set(['zip', 'mov', 'click', 'top', 'gq', 'tk', 'country']); const RISKY_HOST_KEYWORDS = ['secure-login', 'account-verify', 'wallet-verify', 'airdrop-claim']; const sessionGroupCache = new Map(); const sessionWindowMap = new Map(); const tabSessionMap = new Map(); const tabMetaById = new Map(); const downloadEvents = []; const sessionPolicies = new Map(); let policyLoadPromise = loadPolicies(); function normalizeSession(session) { if (typeof session !== 'string') return 'default'; const trimmed = session.trim(); return trimmed.length > 0 ? trimmed.slice(0, 64) : 'default'; } function normalizeGroupTitle(title) { if (typeof title !== 'string') return DEFAULT_GROUP_TITLE; const trimmed = title.trim(); return trimmed.length > 0 ? trimmed.slice(0, 80) : DEFAULT_GROUP_TITLE; } function normalizeAllowedDomains(domains) { if (!Array.isArray(domains)) return []; return domains .map((item) => (typeof item === 'string' ? item.trim().toLowerCase() : '')) .filter((item) => item.length > 0) .slice(0, 256); } function parseHostname(rawUrl) { if (typeof rawUrl !== 'string' || rawUrl.length === 0) return null; try { const parsed = new URL(rawUrl); if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null; return parsed.hostname.toLowerCase(); } catch { return null; } } function domainMatches(hostname, pattern) { if (!hostname || !pattern) return false; if (pattern.startsWith('*.')) { const suffix = pattern.slice(2); return hostname === suffix || hostname.endsWith(`.${suffix}`); } if (pattern.startsWith('.')) { const suffix = pattern.slice(1); return hostname === suffix || hostname.endsWith(pattern); } return hostname === pattern || hostname.endsWith(`.${pattern}`); } function isDomainAllowed(hostname, patterns) { if (!hostname) return true; if (!patterns || patterns.length === 0) return true; return patterns.some((pattern) => domainMatches(hostname, pattern)); } function collectRiskHints(rawUrl, allowedDomains) { const hints = []; const hostname = parseHostname(rawUrl); if (!hostname) return hints; if (allowedDomains.length > 0 && !isDomainAllowed(hostname, allowedDomains)) { hints.push(`domain-not-allowed:${hostname}`); } const tld = hostname.split('.').pop(); if (tld && RISKY_TLDS.has(tld)) { hints.push(`high-risk-tld:.${tld}`); } for (const keyword of RISKY_HOST_KEYWORDS) { if (hostname.includes(keyword)) { hints.push(`suspicious-host-keyword:${keyword}`); } } return [...new Set(hints)].slice(0, 10); } function cacheKey(windowId, session) { return `${windowId}:${session}`; } function sanitizeSegment(input, fallback = 'default') { const raw = typeof input === 'string' ? input : ''; const cleaned = raw .replace(/[\\/:*?"<>|\u0000-\u001f]/g, '-') .replace(/\s+/g, '_') .replace(/\.+/g, '.') .trim(); if (!cleaned) return fallback; return cleaned.slice(0, 80); } function sanitizeFilename(filename, fallback = 'download.bin') { const name = typeof filename === 'string' ? filename.split('/').pop() : ''; return sanitizeSegment(name, fallback); } function pickColorForSession(session) { let hash = 0; for (let i = 0; i < session.length; i += 1) { hash = (hash * 31 + session.charCodeAt(i)) >>> 0; } return GROUP_COLORS[hash % GROUP_COLORS.length]; } function shouldCollapseGroup(session) { return session !== 'default'; } async function loadPolicies() { try { const result = await chrome.storage.local.get([STORAGE_POLICY_KEY]); const entries = result?.[STORAGE_POLICY_KEY]; if (!entries || typeof entries !== 'object') return; for (const [session, domains] of Object.entries(entries)) { const normalizedSession = normalizeSession(session); sessionPolicies.set(normalizedSession, normalizeAllowedDomains(domains)); } } catch { // Ignore storage load failures. } } async function persistPolicies() { const serialized = {}; for (const [session, domains] of sessionPolicies.entries()) { serialized[session] = [...domains]; } await chrome.storage.local.set({ [STORAGE_POLICY_KEY]: serialized }); } async function setSessionPolicy(session, allowedDomains) { const normalizedSession = normalizeSession(session); const normalizedDomains = normalizeAllowedDomains(allowedDomains); sessionPolicies.set(normalizedSession, normalizedDomains); await persistPolicies(); } function getSessionPolicy(session) { const normalizedSession = normalizeSession(session); return sessionPolicies.get(normalizedSession) ?? []; } function updateTabMeta(tab) { if (!tab || typeof tab.id !== 'number') return; tabMetaById.set(tab.id, { id: tab.id, windowId: typeof tab.windowId === 'number' ? tab.windowId : -1, url: typeof tab.url === 'string' ? tab.url : '', title: typeof tab.title === 'string' ? tab.title : '', groupId: typeof tab.groupId === 'number' ? tab.groupId : -1, active: tab.active === true, lastSeenAt: Date.now(), }); } function pruneDownloadEvents() { const maxSize = 100; if (downloadEvents.length > maxSize) { downloadEvents.splice(0, downloadEvents.length - maxSize); } } function recordDownloadEvent(event) { downloadEvents.push({ ...event, timestamp: Date.now() }); pruneDownloadEvents(); } function removeWindowCaches(windowId) { for (const key of [...sessionGroupCache.keys()]) { if (key.startsWith(`${windowId}:`)) { sessionGroupCache.delete(key); } } for (const [session, mappedWindowId] of [...sessionWindowMap.entries()]) { if (mappedWindowId === windowId) { sessionWindowMap.delete(session); } } } async function ensureSessionWindow(tabId, currentWindowId, session) { let targetWindowId = sessionWindowMap.get(session); if (typeof targetWindowId === 'number') { try { await chrome.windows.get(targetWindowId); } catch { sessionWindowMap.delete(session); targetWindowId = undefined; } } if (typeof targetWindowId !== 'number') { sessionWindowMap.set(session, currentWindowId); return currentWindowId; } if (targetWindowId === currentWindowId) { return targetWindowId; } await chrome.tabs.move(tabId, { windowId: targetWindowId, index: -1 }); await chrome.tabs.update(tabId, { active: false }).catch(() => {}); return targetWindowId; } async function findExistingGroup(windowId, groupTitle) { const tabs = await chrome.tabs.query({ windowId }); const checked = new Set(); for (const tab of tabs) { if (typeof tab.groupId !== 'number' || tab.groupId < 0 || checked.has(tab.groupId)) { continue; } checked.add(tab.groupId); try { const group = await chrome.tabGroups.get(tab.groupId); if (group.title === groupTitle) { return tab.groupId; } } catch { // Ignore stale group references. } } return null; } async function ensureSessionGroup(tabId, windowId, session, groupTitle) { const targetWindowId = await ensureSessionWindow(tabId, windowId, session); const key = cacheKey(targetWindowId, session); let groupId = sessionGroupCache.get(key); if (typeof groupId === 'number') { try { await chrome.tabGroups.get(groupId); } catch { groupId = undefined; } } if (typeof groupId !== 'number') { const existing = await findExistingGroup(targetWindowId, groupTitle); if (typeof existing === 'number') { groupId = existing; } } if (typeof groupId === 'number') { await chrome.tabs.group({ groupId, tabIds: [tabId] }); } else { groupId = await chrome.tabs.group({ tabIds: [tabId], createProperties: { windowId: targetWindowId }, }); } const color = pickColorForSession(session); const collapsed = shouldCollapseGroup(session); await chrome.tabGroups.update(groupId, { title: groupTitle, color, collapsed, }); sessionGroupCache.set(key, groupId); sessionWindowMap.set(session, targetWindowId); return { groupId, windowId: targetWindowId, color, collapsed, }; } async function applySessionDomainFallback(tabId, session) { const allowedDomains = getSessionPolicy(session); if (allowedDomains.length === 0) { return { enforced: false, blocked: false }; } let tab; try { tab = await chrome.tabs.get(tabId); } catch { return { enforced: true, blocked: false }; } const hostname = parseHostname(tab.url); if (!hostname) { return { enforced: true, blocked: false }; } if (isDomainAllowed(hostname, allowedDomains)) { return { enforced: true, blocked: false }; } await chrome.tabs.update(tabId, { url: 'about:blank' }).catch(() => {}); return { enforced: true, blocked: true, reason: `${hostname} is not in allowed domains`, }; } function getManagedSessionForTab(tabId) { if (typeof tabId !== 'number') return undefined; return tabSessionMap.get(tabId); } function collectSessionTabIds(session) { const result = []; for (const [tabId, tabSession] of tabSessionMap.entries()) { if (tabSession === session) { result.push(tabId); } } return result; } async function closeOtherSessionTabs(session) { const normalized = normalizeSession(session); const closeIds = []; for (const [tabId, tabSession] of tabSessionMap.entries()) { if (tabSession !== normalized) { closeIds.push(tabId); } } if (closeIds.length > 0) { await chrome.tabs.remove(closeIds); } return { closed: closeIds.length }; } async function focusSession(session) { const normalized = normalizeSession(session); const tabIds = collectSessionTabIds(normalized); if (tabIds.length === 0) { return { focused: false }; } let tab; try { tab = await chrome.tabs.get(tabIds[0]); } catch { return { focused: false }; } if (typeof tab.windowId === 'number') { await chrome.windows.update(tab.windowId, { focused: true }).catch(() => {}); } await chrome.tabs.update(tab.id, { active: true }).catch(() => {}); return { focused: true, tabId: tab.id }; } async function cleanEmptyGroups() { let removedGroups = 0; let removedWindows = 0; for (const [key, groupId] of [...sessionGroupCache.entries()]) { const [windowIdRaw] = key.split(':'); const windowId = Number(windowIdRaw); let groupExists = true; try { await chrome.tabGroups.get(groupId); } catch { groupExists = false; } if (!groupExists) { sessionGroupCache.delete(key); removedGroups += 1; continue; } const tabs = await chrome.tabs.query({ windowId }).catch(() => []); const hasMembers = tabs.some((tab) => tab.groupId === groupId); if (!hasMembers) { sessionGroupCache.delete(key); removedGroups += 1; } } for (const [session, windowId] of [...sessionWindowMap.entries()]) { try { await chrome.windows.get(windowId); } catch { sessionWindowMap.delete(session); removedWindows += 1; } } return { removedGroups, removedWindows }; } async function buildPanelState() { const allTabs = await chrome.tabs.query({}); for (const tab of allTabs) { updateTabMeta(tab); } const sessionMap = new Map(); for (const tab of allTabs) { if (typeof tab.id !== 'number') continue; const session = getManagedSessionForTab(tab.id); if (!session) continue; if (!sessionMap.has(session)) { sessionMap.set(session, { session, windowId: sessionWindowMap.get(session) ?? tab.windowId, allowedDomains: getSessionPolicy(session), tabs: [], riskHints: [], }); } const entry = sessionMap.get(session); entry.tabs.push({ id: tab.id, windowId: tab.windowId, title: tab.title ?? '', url: tab.url ?? '', active: tab.active === true, groupId: typeof tab.groupId === 'number' ? tab.groupId : -1, }); const hints = collectRiskHints(tab.url, entry.allowedDomains); for (const hint of hints) { if (!entry.riskHints.includes(hint)) { entry.riskHints.push(hint); } } } const sessions = []; for (const sessionEntry of sessionMap.values()) { sessionEntry.tabs.sort((a, b) => Number(b.active) - Number(a.active)); const key = cacheKey(sessionEntry.windowId, sessionEntry.session); const cachedGroupId = sessionGroupCache.get(key); let group; if (typeof cachedGroupId === 'number') { try { const groupInfo = await chrome.tabGroups.get(cachedGroupId); group = { id: cachedGroupId, title: groupInfo.title, color: groupInfo.color, collapsed: groupInfo.collapsed, }; } catch { // Group may no longer exist. } } sessions.push({ ...sessionEntry, group, }); } sessions.sort((a, b) => a.session.localeCompare(b.session)); return { extensionId: chrome.runtime.id, totals: { sessions: sessions.length, tabs: sessions.reduce((sum, session) => sum + session.tabs.length, 0), }, sessions, downloads: downloadEvents.slice(-25).reverse(), }; } async function handleTabGroupRequest(message, sender) { await policyLoadPromise; const tabId = sender.tab?.id; const windowId = sender.tab?.windowId; const nonce = typeof message.nonce === 'string' ? message.nonce : undefined; if (typeof tabId !== 'number' || typeof windowId !== 'number') { return { ok: false, error: 'missing-tab-context', extensionId: chrome.runtime.id, nonce, }; } if (typeof message.pluginId === 'string' && message.pluginId !== chrome.runtime.id) { return { ok: false, error: 'plugin-id-mismatch', extensionId: chrome.runtime.id, nonce, }; } const session = normalizeSession(message.session); const groupTitle = normalizeGroupTitle(message.groupTitle); const allowedDomains = normalizeAllowedDomains(message.allowedDomains); if (allowedDomains.length > 0) { await setSessionPolicy(session, allowedDomains); } tabSessionMap.set(tabId, session); updateTabMeta(sender.tab); const grouping = await ensureSessionGroup(tabId, windowId, session, groupTitle); const policy = await applySessionDomainFallback(tabId, session); const riskHints = collectRiskHints(sender.tab?.url, getSessionPolicy(session)); if (policy.blocked && policy.reason) { riskHints.push(`policy-blocked:${policy.reason}`); } return { ok: true, extensionId: chrome.runtime.id, nonce, ...grouping, policy, riskHints: [...new Set(riskHints)].slice(0, 10), }; } chrome.runtime.onInstalled.addListener(() => { chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {}); }); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (!message || typeof message !== 'object') { return; } const type = message.type; if (type === REQUEST_TYPE) { handleTabGroupRequest(message, sender) .then((response) => sendResponse(response)) .catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error); sendResponse({ ok: false, error: errorMessage, extensionId: chrome.runtime.id, nonce: typeof message.nonce === 'string' ? message.nonce : undefined, }); }); return true; } if (type === PANEL_GET_STATE) { buildPanelState() .then((state) => sendResponse({ ok: true, state })) .catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error); sendResponse({ ok: false, error: errorMessage }); }); return true; } if (type === PANEL_CLOSE_OTHER_TABS) { closeOtherSessionTabs(message.session) .then((result) => sendResponse({ ok: true, result })) .catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error); sendResponse({ ok: false, error: errorMessage }); }); return true; } if (type === PANEL_FOCUS_SESSION) { focusSession(message.session) .then((result) => sendResponse({ ok: true, result })) .catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error); sendResponse({ ok: false, error: errorMessage }); }); return true; } if (type === PANEL_CLEAN_EMPTY_GROUPS) { cleanEmptyGroups() .then((result) => sendResponse({ ok: true, result })) .catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error); sendResponse({ ok: false, error: errorMessage }); }); return true; } if (type === PANEL_SET_POLICY) { setSessionPolicy(message.session, message.allowedDomains) .then(() => sendResponse({ ok: true })) .catch((error) => { const errorMessage = error instanceof Error ? error.message : String(error); sendResponse({ ok: false, error: errorMessage }); }); return true; } }); chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { updateTabMeta(tab); const session = getManagedSessionForTab(tabId); if (!session) return; if (typeof tab.windowId === 'number') { sessionWindowMap.set(session, tab.windowId); } if (changeInfo.status === 'complete') { applySessionDomainFallback(tabId, session).catch(() => {}); } }); chrome.tabs.onRemoved.addListener((tabId, removeInfo) => { const session = getManagedSessionForTab(tabId); tabSessionMap.delete(tabId); tabMetaById.delete(tabId); if (removeInfo.isWindowClosing) { removeWindowCaches(removeInfo.windowId); return; } if (!session) return; const remaining = collectSessionTabIds(session); if (remaining.length === 0) { sessionWindowMap.delete(session); } }); chrome.tabs.onDetached.addListener((tabId) => { const session = getManagedSessionForTab(tabId); if (!session) return; tabMetaById.delete(tabId); }); chrome.tabs.onAttached.addListener((tabId, attachInfo) => { const session = getManagedSessionForTab(tabId); if (!session) return; sessionWindowMap.set(session, attachInfo.newWindowId); }); chrome.windows.onRemoved.addListener((windowId) => { removeWindowCaches(windowId); }); chrome.downloads.onDeterminingFilename.addListener((item, suggest) => { const session = getManagedSessionForTab(item.tabId); if (!session) { suggest(); return; } const safeSession = sanitizeSegment(session, 'default'); const safeFilename = sanitizeFilename(item.filename, `download-${item.id}.bin`); const filename = `${DOWNLOAD_ARCHIVE_ROOT}/${safeSession}/${safeFilename}`; recordDownloadEvent({ id: item.id, tabId: item.tabId, session, state: 'routing', filename, }); suggest({ filename, conflictAction: 'uniquify', }); }); chrome.downloads.onChanged.addListener((delta) => { if (!delta || typeof delta.id !== 'number') return; const state = delta.state?.current; if (!state) return; recordDownloadEvent({ id: delta.id, state, filename: delta.filename?.current, }); });