/** * Capability origin allowlist. Shared by the MV3 service worker (importScripts) * and Node unit tests (module.exports). * * Default allow: the examples server origin(s). There is no "allow all" mode — * an empty user list still includes the examples origin. */ (function (root, factory) { if (typeof module === 'object' && module.exports) { module.exports = factory(); } else { root.BridgeSwarmOriginAllowlist = factory(); } })(typeof globalThis !== 'undefined' ? globalThis : this, function () { const DEFAULT_EXAMPLES_PORT = 4173; const CAP_PACK_PREFIX = /^(media|fs|sqlite|net|qvac|agent|discord)\.[a-zA-Z0-9_-]+$/; function defaultOrigins(examplesPort) { const port = Number(examplesPort) > 0 ? Number(examplesPort) : DEFAULT_EXAMPLES_PORT; return [ 'http://127.0.0.1:' + port, 'http://localhost:' + port, 'http://[::1]:' + port, ]; } function originFromUrl(raw) { if (!raw || typeof raw !== 'string') return ''; try { const u = new URL(raw); if (u.protocol === 'http:' || u.protocol === 'https:') return u.origin; if (u.protocol === 'chrome-extension:' || u.protocol === 'moz-extension:') { return u.origin; } if (u.protocol === 'file:') return 'file://'; return u.origin || ''; } catch (_) { return ''; } } function normalizeOrigin(value) { if (!value || typeof value !== 'string') return ''; const trimmed = value.trim().replace(/\/+$/, ''); if (!trimmed) return ''; if (trimmed === 'file://') return 'file://'; try { if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) { return originFromUrl(trimmed) || trimmed; } return originFromUrl('https://' + trimmed) || trimmed; } catch (_) { return trimmed; } } function parseOriginList(raw) { if (Array.isArray(raw)) { return raw.map(normalizeOrigin).filter(Boolean); } if (typeof raw !== 'string') return []; return raw .split(/\r?\n/) .map(normalizeOrigin) .filter(Boolean); } function allowedOrigins(settings) { const s = settings || {}; const port = s.examplesServerPort; const extras = parseOriginList(s.capabilityOrigins); const seen = new Set(); const out = []; for (const o of defaultOrigins(port).concat(extras)) { if (!seen.has(o)) { seen.add(o); out.push(o); } } return out; } function isExtensionOrigin(origin, extensionOrigin) { if (!origin) return false; if (extensionOrigin && origin === extensionOrigin) return true; return origin.startsWith('chrome-extension://') || origin.startsWith('moz-extension://'); } function isOriginAllowed(origin, settings, opts) { const o = normalizeOrigin(origin); if (!o) return false; if (isExtensionOrigin(o, opts && opts.extensionOrigin)) return true; const list = allowedOrigins(settings); if (list.indexOf(o) !== -1) return true; return false; } function isAlwaysApproveOrigin(origin, settings) { const o = normalizeOrigin(origin); if (!o) return false; const list = parseOriginList(settings && settings.agentAlwaysApproveOrigins); return list.indexOf(o) !== -1; } function isCapabilityType(type) { if (type === 'capability') return true; return typeof type === 'string' && CAP_PACK_PREFIX.test(type); } const QVAC_META_CMDS = { detect: 1, status: 1, catalog: 1, openaiStatus: 1 }; const AGENT_META_CMDS = { status: 1, setGrants: 1, list: 1 }; const DISCORD_META_CMDS = { surface: 1, status: 1 }; function qvacPackAndCmd(msg) { if (!msg || !msg.type) return null; if (msg.type === 'capability' && msg.payload) { return { pack: msg.payload.pack, cmd: msg.payload.cmd }; } const m = typeof msg.type === 'string' && msg.type.match(/^(qvac|agent|discord)\.([a-zA-Z0-9_-]+)$/); if (m) return { pack: m[1], cmd: m[2] }; return null; } /** True when QVAC/agent inference must be blocked (user has not enabled QVAC). * qvac.setEnabled is never allowed from pages (background send bypasses this gate). */ function isQvacDisabled(msg, settings) { const pc = qvacPackAndCmd(msg); if (pc && pc.pack === 'qvac' && pc.cmd === 'setEnabled') return true; if (settings && settings.qvacEnabled === true) return false; if (!pc || (pc.pack !== 'qvac' && pc.pack !== 'agent')) return false; if (pc.pack === 'qvac' && QVAC_META_CMDS[pc.cmd]) return false; if (pc.pack === 'agent' && AGENT_META_CMDS[pc.cmd]) return false; return true; } /** True when Discord construct/login must be blocked (user has not enabled Discord). * discord.setEnabled is never allowed from pages. surface/status stay available. */ function isDiscordDisabled(msg, settings) { const pc = qvacPackAndCmd(msg); if (pc && pc.pack === 'discord' && pc.cmd === 'setEnabled') return true; if (settings && settings.discordEnabled === true) return false; if (!pc || pc.pack !== 'discord') return false; if (DISCORD_META_CMDS[pc.cmd]) return false; return true; } return { DEFAULT_EXAMPLES_PORT, defaultOrigins, originFromUrl, normalizeOrigin, parseOriginList, allowedOrigins, isExtensionOrigin, isOriginAllowed, isAlwaysApproveOrigin, isCapabilityType, qvacPackAndCmd, isQvacDisabled, isDiscordDisabled, }; });