/** * Background service worker: maintains native messaging port and routes messages * between content scripts and the native host. Reconnects with backoff on disconnect. */ importScripts('origin-allowlist.js'); const HOST_NAME = 'com.bridgeswarm'; const MAX_RECONNECT_DELAY = 30000; const INITIAL_RECONNECT_DELAY = 100; const MAX_LOGS = 500; const SETTINGS_KEY = 'bridgeSwarmSettings'; const DASHBOARD_PORT = 'bridgeswarm-dashboard'; const browser = typeof chrome !== 'undefined' && chrome.runtime?.connectNative ? chrome : typeof browser !== 'undefined' ? browser : chrome; let port = null; let reconnectDelay = INITIAL_RECONNECT_DELAY; let reconnectTimer = null; let reconnectAttempt = 0; let lastDisconnectReason = null; let hostSnapshot = null; let hostSnapshotAt = 0; const pending = new Map(); const subscribedTabs = new Set(); const tabSwarms = new Map(); const swarmRefCount = new Map(); /** swarmId -> { swarmId, appName, createdAt } */ const swarmMeta = new Map(); /** connId -> { connId, swarmId, peerKey, topics, createdAt } */ const activeConnections = new Map(); const logs = []; let logSeq = 0; const dashboardPorts = new Set(); let notifyOnDisconnect = false; let debugMode = false; let examplesServerEnabled = false; let qvacEnabled = false; let examplesServerPort = 4173; let defaultFirewall = { mode: 'off', keys: [] }; let cachedSettings = {}; let capabilityOrigins = []; let agentAlwaysApproveOrigins = []; let agentWorkspaceRoots = []; function examplesUrl() { return `http://127.0.0.1:${examplesServerPort || 4173}/`; } let examplesServerState = { running: false, url: null, error: null, rootFound: null, }; const extensionState = { hostConnected: false, swarms: new Map(), stats: { totalConnections: 0, totalSwarms: 0, uptime: Date.now(), }, }; /** * Expected Hyperswarm/UDX peer churn. Must not use console.error — Chrome's * extension Errors page only lists console.error and makes the extension look broken. */ const BENIGN_CONN_RE = /connection reset by peer|connection timed out|stream timed out|duplicate connection|swarm has been destroyed|ECONNRESET|ETIMEDOUT|EPIPE|ENOTCONN|ECONNABORTED|socket hang up|not connected|Write after end|destroyed|HOLEPUNCH_|CANNOT_HOLEPUNCH|REMOTE_NOT_HOLEPUNCHABLE|CHANNEL_CLOSED|STREAM_NOT_CONNECTED/i; function isBenignConnectionMessage(message) { return BENIGN_CONN_RE.test(String(message || '')); } function log(levelOrMsg, ...rest) { let level = 'info'; let args; if (typeof levelOrMsg === 'string' && ['debug', 'info', 'warn', 'error'].includes(levelOrMsg) && rest.length) { level = levelOrMsg; args = rest; } else { args = [levelOrMsg, ...rest]; } if (level === 'debug' && !debugMode) return; const message = args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' '); // Downgrade expected peer disconnects so they never flood chrome://extensions errors if (level === 'error' && isBenignConnectionMessage(message)) { level = 'info'; } const meta = {}; const swarmMatch = message.match(/swarm[_-][a-zA-Z0-9]+/); const connMatch = message.match(/conn_[a-zA-Z0-9]+/); const reqMatch = message.match(/req_[a-zA-Z0-9]+/); const jobMatch = message.match(/(?:job|session)_[a-zA-Z0-9]+/); if (swarmMatch) meta.swarmId = swarmMatch[0]; if (connMatch) meta.connId = connMatch[0]; if (reqMatch) meta.requestId = reqMatch[0]; if (jobMatch) meta.jobId = jobMatch[0]; let category = 'host'; const lower = message.toLowerCase(); if (lower.includes('examples')) category = 'examples'; else if (lower.includes('cap-') || lower.includes('capability') || lower.includes('media')) category = 'cap'; else if (lower.includes('connection') || lower.includes('conn_')) category = 'conn'; else if (lower.includes('swarm') || lower.includes('register')) category = 'swarm'; else if (lower.includes('setting') || lower.includes('firewall')) category = 'settings'; const entry = { id: `log_${Date.now()}_${++logSeq}`, ts: Date.now(), timestamp: Date.now(), level, category, message, meta, }; logs.push(entry); if (logs.length > MAX_LOGS) logs.shift(); if (level === 'error') console.error('[BridgeSwarm-bg]', message); else if (level === 'warn') console.warn('[BridgeSwarm-bg]', message); else console.log('[BridgeSwarm-bg]', message); broadcastLogs(entry); } function broadcastLogs(entry) { const msg = entry ? { type: 'bridge-swarm-logs', entry, logs: logs.slice() } : { type: 'bridge-swarm-logs', logs: logs.slice() }; for (const p of dashboardPorts) { try { p.postMessage(msg); } catch (_) { dashboardPorts.delete(p); } } } function updateExtensionState() { extensionState.hostConnected = !!port; extensionState.stats.totalConnections = activeConnections.size; extensionState.swarms.clear(); for (const [swarmId, count] of swarmRefCount) { const meta = swarmMeta.get(swarmId) || {}; const connections = []; for (const conn of activeConnections.values()) { if (conn.swarmId === swarmId) connections.push(conn); } const snap = hostSnapshot?.swarms?.find((s) => s.swarmId === swarmId); extensionState.swarms.set(swarmId, { swarmId, appName: meta.appName || snap?.appName || 'bridge-swarm', tabCount: count, connections, createdAt: meta.createdAt || Date.now(), publicKey: snap?.publicKey || null, firewall: snap?.firewall || null, autoReplicate: snap?.autoReplicate || false, peerCount: snap?.peerCount ?? connections.length, }); } extensionState.stats.totalSwarms = extensionState.swarms.size; } function clearSwarmLocal(swarmId) { swarmRefCount.delete(swarmId); swarmMeta.delete(swarmId); for (const [connId, conn] of [...activeConnections]) { if (conn.swarmId === swarmId) activeConnections.delete(connId); } for (const [tabId, swarmIds] of tabSwarms) { swarmIds.delete(swarmId); if (swarmIds.size === 0) tabSwarms.delete(tabId); } updateExtensionState(); } function applySettingsFromStorage(s) { cachedSettings = s || {}; notifyOnDisconnect = s.notifyOnDisconnect === true; debugMode = s.debug === true; examplesServerPort = typeof s.examplesServerPort === 'number' && s.examplesServerPort > 0 ? s.examplesServerPort : 4173; defaultFirewall = { mode: s.defaultFirewallMode || 'off', keys: Array.isArray(s.defaultFirewallKeys) ? s.defaultFirewallKeys.filter(Boolean) : [], }; const allow = self.BridgeSwarmOriginAllowlist; capabilityOrigins = allow ? allow.allowedOrigins(s) : []; agentAlwaysApproveOrigins = allow ? allow.parseOriginList(s.agentAlwaysApproveOrigins) : []; agentWorkspaceRoots = Array.isArray(s.agentWorkspaceRoots) ? s.agentWorkspaceRoots.map((p) => String(p || '').trim()).filter(Boolean) : allow ? allow.parseOriginList(s.agentWorkspaceRoots).filter((x) => x && !x.startsWith('http')) : []; if (typeof s.agentWorkspaceRoots === 'string') { agentWorkspaceRoots = s.agentWorkspaceRoots .split(/\r?\n/) .map((l) => l.trim()) .filter(Boolean); } syncAgentGrants().catch(() => {}); const wantExamples = s.examplesServerEnabled === true; if (wantExamples !== examplesServerEnabled) { examplesServerEnabled = wantExamples; syncExamplesServer().catch((err) => log('error', 'examples server sync failed:', err.message || err)); } else if (wantExamples && port) { examplesServerEnabled = wantExamples; syncExamplesServer().catch(() => {}); } else { examplesServerEnabled = wantExamples; } const wantQvac = s.qvacEnabled === true; if (wantQvac !== qvacEnabled) { qvacEnabled = wantQvac; log('info', wantQvac ? 'QVAC enabled' : 'QVAC disabled'); syncQvacEnabled().catch((err) => log('error', 'QVAC enable sync failed:', err.message || err)); } else if (port) { qvacEnabled = wantQvac; syncQvacEnabled().catch(() => {}); } else { qvacEnabled = wantQvac; } } function loadSettings() { browser.storage.local.get(SETTINGS_KEY, (result) => { applySettingsFromStorage(result[SETTINGS_KEY] || {}); }); } async function syncExamplesServer() { const url = examplesUrl(); if (!port) { examplesServerState = { running: false, url: examplesServerEnabled ? url : null, error: examplesServerEnabled ? 'Native host not connected' : null, rootFound: null, }; return examplesServerState; } try { if (examplesServerEnabled) { const res = await send( { type: 'examplesServer.start', payload: { host: '127.0.0.1', port: examplesServerPort || 4173 } }, { quiet: !debugMode } ); examplesServerState = { running: !!res?.running, url: res?.url || url, error: res?.ok === false ? res.error || 'Failed to start' : null, rootFound: res?.rootFound, }; if (res?.ok !== false) log('info', 'Examples server:', examplesServerState.url); else log('error', 'Examples server failed:', examplesServerState.error); } else { const res = await send({ type: 'examplesServer.stop', payload: {} }, { quiet: !debugMode }); examplesServerState = { running: false, url: null, error: res?.ok === false ? res.error || null : null, rootFound: res?.rootFound, }; log('info', 'Examples server stopped'); } } catch (err) { examplesServerState = { running: false, url: examplesServerEnabled ? url : null, error: err.message || String(err), rootFound: null, }; log('error', 'Examples server error:', err.message || err); } return examplesServerState; } async function syncQvacEnabled() { if (!port) return; try { const res = await send( { type: 'capability', payload: { pack: 'qvac', cmd: 'setEnabled', payload: { enabled: qvacEnabled === true }, }, }, { quiet: true } ); if (res && res.ok === false) { log('warn', 'QVAC setEnabled:', res.error || 'failed'); } } catch (err) { log('warn', 'QVAC setEnabled failed:', err.message || err); } } async function refreshHostSnapshot(force) { if (!port) { hostSnapshot = null; return null; } if (!force && hostSnapshot && Date.now() - hostSnapshotAt < 2000) return hostSnapshot; try { const snap = await send({ type: 'host.snapshot', payload: {} }, { quiet: true }); if (snap && snap.ok !== false) { hostSnapshot = snap; hostSnapshotAt = Date.now(); if (snap.examples) { examplesServerState = { running: !!snap.examples.running, url: snap.examples.url || (examplesServerEnabled ? examplesUrl() : null), error: snap.examples.error || null, rootFound: snap.examples.rootFound, }; } } } catch (_) { /* ignore */ } return hostSnapshot; } async function applyDefaultFirewall(swarmId) { if (!defaultFirewall || defaultFirewall.mode === 'off') return; try { await send( { type: 'setFirewall', payload: { swarmId, mode: defaultFirewall.mode, keys: defaultFirewall.keys || [], }, }, { quiet: !debugMode } ); log('info', 'Applied default firewall', defaultFirewall.mode, 'to', swarmId); } catch (err) { log('warn', 'Failed to apply default firewall:', err.message || err); } } function senderPageOrigin(sender) { const allow = self.BridgeSwarmOriginAllowlist; if (!allow) return ''; const url = (sender && sender.url) || (sender && sender.tab && sender.tab.url) || ''; if (sender && sender.origin && (sender.origin.startsWith('http') || sender.origin.startsWith('chrome-extension') || sender.origin.startsWith('moz-extension'))) { return allow.normalizeOrigin(sender.origin); } return allow.originFromUrl(url); } function extensionOrigin() { try { return new URL(browser.runtime.getURL('')).origin; } catch (_) { return ''; } } /** * Deny pack commands from origins not on the capability allowlist. * Attaches _origin / _alwaysApprove onto the inner payload for the host. * Returns an error response object, or null if the message may proceed. */ function gateCapabilityPayload(msg, sender) { const allow = self.BridgeSwarmOriginAllowlist; if (!allow || !msg || !allow.isCapabilityType(msg.type)) return null; const origin = senderPageOrigin(sender); const extOrigin = extensionOrigin(); if (!allow.isOriginAllowed(origin, cachedSettings, { extensionOrigin: extOrigin })) { log('warn', 'Blocked capability from origin', origin || '(unknown)', msg.type); return { ok: false, error: 'origin not allowed for capabilities: ' + (origin || 'unknown') }; } if (allow.isQvacDisabled(msg, cachedSettings)) { log('warn', 'Blocked QVAC/agent (disabled)', msg.type); return { ok: false, error: 'QVAC is disabled. Enable it in BridgeSwarm Settings.' }; } const inner = msg.payload && typeof msg.payload === 'object' ? msg.payload : {}; const always = allow.isAlwaysApproveOrigin(origin, cachedSettings); if (msg.type === 'capability') { const nested = inner.payload && typeof inner.payload === 'object' ? inner.payload : {}; nested._origin = origin; nested._alwaysApprove = always; inner.payload = nested; inner._origin = origin; msg.payload = inner; } else { inner._origin = origin; inner._alwaysApprove = always; msg.payload = inner; } return null; } async function syncAgentGrants() { if (!port) return; try { await send( { type: 'capability', payload: { pack: 'agent', cmd: 'setGrants', payload: { roots: agentWorkspaceRoots, alwaysApproveOrigins: agentAlwaysApproveOrigins, _origin: extensionOrigin(), _alwaysApprove: true, }, }, }, { quiet: true } ); } catch (_) { /* agent pack may not be registered yet */ } } browser.storage.onChanged.addListener((changes, areaName) => { if (areaName === 'local' && changes[SETTINGS_KEY]) { applySettingsFromStorage(changes[SETTINGS_KEY].newValue || {}); } }); function connect() { try { port = browser.runtime.connectNative(HOST_NAME); log('info', 'Connected to native host'); reconnectAttempt = 0; lastDisconnectReason = null; } catch (e) { log('error', 'connectNative failed:', e.message || e); scheduleReconnect(); return; } reconnectDelay = INITIAL_RECONNECT_DELAY; if (examplesServerEnabled) { syncExamplesServer().catch(() => {}); } syncQvacEnabled().catch(() => {}); syncAgentGrants().catch(() => {}); refreshHostSnapshot(true).catch(() => {}); port.onMessage.addListener((msg) => { if (debugMode) log('debug', 'Received from native:', msg.type, msg.event || ''); if (msg.type === 'event') { const payload = msg.payload || {}; if (msg.event === 'connection') { const connId = payload.connId; const swarmId = payload.swarmId; const peerInfo = payload.peerInfo || {}; log('info', 'Connection established:', connId, 'peer:', (peerInfo.publicKey || '').slice(0, 8)); activeConnections.set(connId, { connId, swarmId, peerKey: peerInfo.publicKey || '', topics: peerInfo.topics || [], createdAt: Date.now(), }); updateExtensionState(); broadcastState(); } else if (msg.event === 'error') { const connId = payload.connId; const errMsg = payload.message || 'Unknown error'; // Host only emits error for non-transient failures; still guard Chrome error spam if (isBenignConnectionMessage(errMsg)) { log('info', 'Connection closed:', connId, errMsg); } else { log('error', 'Connection ERROR:', connId, errMsg); } activeConnections.delete(connId); updateExtensionState(); broadcastState(); } else if (msg.event === 'end') { const reason = payload.reason ? ` (${payload.reason})` : ''; log('info', 'Connection ended:', payload.connId + reason); activeConnections.delete(payload.connId); updateExtensionState(); broadcastState(); } else if (msg.event === 'cap-error') { log('error', 'Capability error:', payload.pack, payload.message || ''); } else if (msg.event === 'cap-end') { if (debugMode) log('debug', 'Capability end:', payload.pack, payload.jobId || payload.sessionId || ''); } } if (msg.type === 'response' && msg.id != null) { const p = pending.get(msg.id); if (p) { pending.delete(msg.id); if (msg.payload && msg.payload.error) { p.reject(new Error(msg.payload.error)); } else { p.resolve(msg.payload); } } return; } if (msg.type === 'event') { const eventSwarmId = msg.payload?.swarmId; const isCapEvent = msg.event === 'cap-chunk' || msg.event === 'cap-end' || msg.event === 'cap-error' || !!msg.payload?.pack; if (debugMode) { log('debug', 'Broadcasting event to tabs:', msg.event, 'connId:', msg.payload?.connId, 'swarmId:', eventSwarmId); } if (isCapEvent || eventSwarmId == null) { for (const tabId of subscribedTabs) { browser.tabs.sendMessage(tabId, { type: 'bridge-swarm-event', payload: msg }).catch(() => {}); } } else { for (const [tabId, swarmIds] of tabSwarms) { if (swarmIds.has(eventSwarmId)) { browser.tabs.sendMessage(tabId, { type: 'bridge-swarm-event', payload: msg }).catch(() => {}); } } } } }); port.onDisconnect.addListener((p) => { const reason = p?.error?.message || browser.runtime.lastError?.message || null; lastDisconnectReason = reason || 'Native host disconnected'; if (reason) log('warn', 'onDisconnect reason:', reason); else log('warn', 'Native host disconnected'); extensionState.hostConnected = false; activeConnections.clear(); hostSnapshot = null; updateExtensionState(); port = null; examplesServerState = { running: false, url: examplesServerEnabled ? examplesUrl() : null, error: examplesServerEnabled ? 'Native host disconnected' : null, rootFound: examplesServerState.rootFound, }; broadcastState(); if (notifyOnDisconnect && browser.notifications) { browser.notifications .create('bridgeswarm-host-disconnect', { type: 'basic', title: 'BridgeSwarm', message: 'Native host disconnected.', iconUrl: browser.runtime.getURL('icons/48.png'), }) .catch(() => {}); } const err = new Error(reason || 'Native host disconnected'); for (const [, pendingReq] of pending) { pendingReq.reject(err); } pending.clear(); for (const tabId of subscribedTabs) { browser.tabs.sendMessage(tabId, { type: 'bridge-swarm-host-disconnect' }).catch(() => {}); } scheduleReconnect(); }); } function scheduleReconnect() { if (reconnectTimer) return; reconnectAttempt += 1; const delay = reconnectDelay; reconnectTimer = setTimeout(() => { reconnectTimer = null; connect(); reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY); }, delay); broadcastState(); } function send(msg, opts = {}) { if (!opts.quiet && debugMode) { log('debug', 'Sending to native:', msg.type, msg.payload?.swarmId || '', msg.payload?.connId || ''); } else if (!opts.quiet && !debugMode && !['host.snapshot', 'examplesServer.status'].includes(msg.type)) { /* skip chatty */ } return new Promise((resolve, reject) => { if (!port) { reject(new Error('Native host not connected')); return; } const id = msg.id || `req_${Date.now()}_${Math.random().toString(36).slice(2)}`; msg.id = id; pending.set(id, { resolve, reject }); try { port.postMessage(msg); } catch (e) { pending.delete(id); reject(e); } }); } function buildStatePayload() { updateExtensionState(); const tabsInfo = []; for (const [tabId, swarmIds] of tabSwarms) { tabsInfo.push({ tabId, swarmIds: Array.from(swarmIds) }); } const manifest = browser.runtime.getManifest?.() || {}; return { hostConnected: extensionState.hostConnected, swarms: Array.from(extensionState.swarms.values()), tabs: tabsInfo, stats: { totalConnections: extensionState.stats.totalConnections, totalSwarms: extensionState.stats.totalSwarms, uptime: extensionState.stats.uptime, pendingRequests: pending.size, reconnectAttempt, reconnectDelayMs: port ? 0 : reconnectDelay, }, health: { lastDisconnectReason, reconnectAttempt, reconnectDelayMs: port ? 0 : reconnectDelay, extensionVersion: manifest.version || '1.0.0', hostVersion: hostSnapshot?.version || null, storagePath: hostSnapshot?.storagePath || null, packs: hostSnapshot?.packs || [], qvac: hostSnapshot?.qvac || null, agent: hostSnapshot?.agent || null, openai: hostSnapshot?.openai || null, }, examples: { enabled: examplesServerEnabled, ...examplesServerState, url: examplesServerState.url || (examplesServerEnabled ? examplesUrl() : null), port: examplesServerPort, }, hostSnapshot, }; } function broadcastState() { const base = buildStatePayload(); enrichTabs(base.tabs) .then((tabs) => { const state = { ...base, tabs }; for (const p of dashboardPorts) { try { p.postMessage({ type: 'bridge-swarm-state', state }); } catch (_) { dashboardPorts.delete(p); } } }) .catch(() => { for (const p of dashboardPorts) { try { p.postMessage({ type: 'bridge-swarm-state', state: base }); } catch (_) { dashboardPorts.delete(p); } } }); } async function enrichTabs(tabsInfo) { const enriched = []; for (const t of tabsInfo) { let title = null; let url = null; try { const tab = await browser.tabs.get(t.tabId); title = tab?.title || null; url = tab?.url || null; } catch (_) {} enriched.push({ ...t, title, url }); } return enriched; } loadSettings(); connect(); browser.tabs.onRemoved.addListener((tabId) => { log('info', 'Tab closed:', tabId); const swarmIds = tabSwarms.get(tabId); if (swarmIds) { for (const swarmId of swarmIds) { const count = (swarmRefCount.get(swarmId) || 1) - 1; swarmRefCount.set(swarmId, count); log('debug', 'Swarm', swarmId, 'refcount now:', count); if (count <= 0) { log('info', 'Last tab for swarm', swarmId, '- destroying'); send({ type: 'destroy', payload: { swarmId } }).catch(() => {}); clearSwarmLocal(swarmId); } } tabSwarms.delete(tabId); } subscribedTabs.delete(tabId); broadcastState(); }); browser.runtime.onConnect.addListener((p) => { if (p.name !== DASHBOARD_PORT) return; dashboardPorts.add(p); log('info', 'Dashboard port connected'); p.postMessage({ type: 'bridge-swarm-logs', logs: logs.slice() }); p.postMessage({ type: 'bridge-swarm-state', state: buildStatePayload() }); p.onDisconnect.addListener(() => { dashboardPorts.delete(p); }); p.onMessage.addListener((msg) => { if (!msg || msg.target !== 'bridge-swarm-native') return; handleDashboardMessage(msg, (response) => { try { p.postMessage({ type: 'bridge-swarm-response', id: msg.id, response }); } catch (_) {} }); }); }); function handleDashboardMessage(message, sendResponse) { if (message.action === 'getLogs') { sendResponse({ ok: true, logs: logs.slice() }); return false; } if (message.action === 'clearLogs') { logs.length = 0; broadcastLogs(); sendResponse({ ok: true, logs: [] }); return false; } if (message.action === 'getState') { refreshHostSnapshot(false) .then(() => enrichTabs(buildStatePayload().tabs)) .then((tabs) => { const state = buildStatePayload(); state.tabs = tabs; sendResponse({ ok: true, state }); }) .catch(() => sendResponse({ ok: true, state: buildStatePayload() })); return true; } if (message.action === 'destroySwarm' && message.payload?.swarmId) { const swarmId = message.payload.swarmId; send({ type: 'destroy', payload: { swarmId } }) .then((res) => { clearSwarmLocal(swarmId); broadcastState(); sendResponse(res && res.ok === false ? res : { ok: true }); }) .catch((err) => sendResponse({ ok: false, error: err.message })); return true; } if (message.action === 'examplesServerStatus') { syncExamplesServer() .then((state) => sendResponse({ ok: !state.error, enabled: examplesServerEnabled, ...state, url: state.url || (examplesServerEnabled ? examplesUrl() : null), port: examplesServerPort, }) ) .catch((err) => sendResponse({ ok: false, enabled: examplesServerEnabled, running: false, url: examplesServerEnabled ? examplesUrl() : null, error: err.message, port: examplesServerPort, }) ); return true; } if (message.action === 'focusTab' && message.payload?.tabId != null) { browser.tabs .update(message.payload.tabId, { active: true }) .then(() => sendResponse({ ok: true })) .catch((err) => sendResponse({ ok: false, error: err.message })); return true; } return false; } browser.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.target !== 'bridge-swarm-native') { return; } if (message.action === 'send' && message.payload?.type === 'registerSwarm') { const swarmId = message.payload?.payload?.swarmId; const appName = message.payload?.payload?.appName || 'bridge-swarm'; if (swarmId && sender.tab?.id != null) { if (!tabSwarms.has(sender.tab.id)) { tabSwarms.set(sender.tab.id, new Set()); } const set = tabSwarms.get(sender.tab.id); const already = set.has(swarmId); set.add(swarmId); if (!already) { const count = swarmRefCount.get(swarmId) || 0; swarmRefCount.set(swarmId, count + 1); } if (!swarmMeta.has(swarmId)) { swarmMeta.set(swarmId, { swarmId, appName, createdAt: Date.now() }); } else if (appName) { swarmMeta.get(swarmId).appName = appName; } log('info', 'Registered swarm', swarmId, 'appName=', appName, 'for tab', sender.tab.id); applyDefaultFirewall(swarmId).catch(() => {}); updateExtensionState(); broadcastState(); } sendResponse({ ok: true }); return false; } if (message.action === 'send') { const payload = message.payload; const capGate = gateCapabilityPayload(payload, sender); if (capGate) { sendResponse(capGate); return false; } send(payload) .then((res) => { if (payload?.type === 'init' && payload?.payload?.swarmId) { const swarmId = payload.payload.swarmId; const appName = payload.payload?.options?.appName; if (!swarmMeta.has(swarmId)) { swarmMeta.set(swarmId, { swarmId, appName: appName || 'bridge-swarm', createdAt: Date.now(), }); } applyDefaultFirewall(swarmId).catch(() => {}); } sendResponse(res); }) .catch((err) => sendResponse({ error: err.message })); return true; } if (message.action === 'subscribe' && sender.tab && sender.tab.id != null) { subscribedTabs.add(sender.tab.id); sendResponse({ ok: true }); return false; } if (message.action === 'unsubscribe' && sender.tab && sender.tab.id != null) { subscribedTabs.delete(sender.tab.id); sendResponse({ ok: true }); return false; } // Legacy dashboard registration (prefer Port) if (message.action === 'registerDashboard') { sendResponse({ ok: true, logs: logs.slice() }); return false; } if (message.action === 'unregisterDashboard') { sendResponse({ ok: true }); return false; } const handled = handleDashboardMessage(message, sendResponse); if (handled === true) return true; if (message.action === 'getLogs' || message.action === 'clearLogs' || message.action === 'getState' || message.action === 'destroySwarm' || message.action === 'examplesServerStatus' || message.action === 'focusTab') { return handled; } }); browser.action?.onClicked?.addListener(() => { browser.tabs.create({ url: 'dashboard.html' }); });