Files
BridgeSwarm/extension/background.js
T
snxraven b711ba0447
CI / Build & Test (push) Failing after 53s
Add QVAC
2026-09-02 14:17:10 -04:00

901 lines
28 KiB
JavaScript

/**
* 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' });
});