@btraut/browser-bridge 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/site-permissions.ts", "../src/options-ui.ts"],
4
- "sourcesContent": ["export const SITE_ALLOWLIST_KEY = 'siteAllowlist';\nexport const PERMISSION_PROMPT_WAIT_MS_KEY = 'permissionPromptWaitMs';\nexport const DEFAULT_PERMISSION_PROMPT_WAIT_MS = 30_000;\nexport const SITE_PERMISSIONS_MODE_KEY = 'sitePermissionsMode';\n\nexport type SitePermissionsMode = 'granular' | 'bypass';\nexport const DEFAULT_SITE_PERMISSIONS_MODE: SitePermissionsMode = 'granular';\n\nexport type SiteAllowlistEntry = {\n createdAt: string; // ISO\n lastUsedAt: string; // ISO\n};\n\nexport type SiteAllowlist = Record<string, SiteAllowlistEntry>;\n\nexport const siteKeyFromUrl = (rawUrl: string): string | null => {\n if (!rawUrl || typeof rawUrl !== 'string') {\n return null;\n }\n\n try {\n const parsed = new URL(rawUrl);\n // Only gate \"real web\" pages for now.\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n return null;\n }\n\n // URL.hostname is lowercased by the platform.\n if (!parsed.hostname) {\n return null;\n }\n\n return parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname;\n } catch {\n return null;\n }\n};\n\nconst isAllowlistEntry = (value: unknown): value is SiteAllowlistEntry => {\n if (!value || typeof value !== 'object') {\n return false;\n }\n const v = value as Record<string, unknown>;\n return typeof v.createdAt === 'string' && typeof v.lastUsedAt === 'string';\n};\n\nconst normalizeSiteKey = (siteKey: string): string => siteKey.toLowerCase();\n\nconst readAllowlistRaw = async (): Promise<SiteAllowlist> => {\n return await new Promise<SiteAllowlist>((resolve) => {\n chrome.storage.local.get(\n [SITE_ALLOWLIST_KEY],\n (result: Record<string, unknown>) => {\n const raw = result?.[SITE_ALLOWLIST_KEY];\n if (!raw || typeof raw !== 'object') {\n resolve({});\n return;\n }\n\n const out: SiteAllowlist = {};\n for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof k !== 'string') {\n continue;\n }\n if (!isAllowlistEntry(v)) {\n continue;\n }\n out[normalizeSiteKey(k)] = v;\n }\n\n resolve(out);\n }\n );\n });\n};\n\nconst writeAllowlistRaw = async (allowlist: SiteAllowlist): Promise<void> => {\n return await new Promise<void>((resolve) => {\n chrome.storage.local.set({ [SITE_ALLOWLIST_KEY]: allowlist }, () =>\n resolve()\n );\n });\n};\n\nexport const readSitePermissionsMode =\n async (): Promise<SitePermissionsMode> => {\n return await new Promise<SitePermissionsMode>((resolve) => {\n chrome.storage.local.get(\n [SITE_PERMISSIONS_MODE_KEY],\n (result: Record<string, unknown>) => {\n const raw = result?.[SITE_PERMISSIONS_MODE_KEY];\n if (raw === 'granular' || raw === 'bypass') {\n resolve(raw);\n return;\n }\n\n // Self-heal legacy/invalid storage to a safe default, so UIs never\n // render with \"no mode selected\" and other callers don't have to\n // special-case missing values.\n try {\n chrome.storage.local.set({\n [SITE_PERMISSIONS_MODE_KEY]: DEFAULT_SITE_PERMISSIONS_MODE,\n });\n } catch {\n // ignore\n }\n resolve(DEFAULT_SITE_PERMISSIONS_MODE);\n }\n );\n });\n };\n\nexport const writeSitePermissionsMode = async (\n mode: SitePermissionsMode\n): Promise<void> => {\n return await new Promise<void>((resolve) => {\n chrome.storage.local.set({ [SITE_PERMISSIONS_MODE_KEY]: mode }, () =>\n resolve()\n );\n });\n};\n\nexport const readPermissionPromptWaitMs = async (): Promise<number> => {\n return await new Promise<number>((resolve) => {\n chrome.storage.local.get(\n [PERMISSION_PROMPT_WAIT_MS_KEY],\n (result: Record<string, unknown>) => {\n const raw = result?.[PERMISSION_PROMPT_WAIT_MS_KEY];\n if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {\n resolve(raw);\n return;\n }\n if (typeof raw === 'string') {\n const parsed = Number(raw);\n if (Number.isFinite(parsed) && parsed > 0) {\n resolve(parsed);\n return;\n }\n }\n\n resolve(DEFAULT_PERMISSION_PROMPT_WAIT_MS);\n }\n );\n });\n};\n\nexport const getAllowlistedSites = async (): Promise<SiteAllowlist> => {\n return await readAllowlistRaw();\n};\n\nexport const isSiteAllowed = async (siteKey: string): Promise<boolean> => {\n const key = normalizeSiteKey(siteKey);\n const allowlist = await readAllowlistRaw();\n return Boolean(allowlist[key]);\n};\n\nexport const allowSiteAlways = async (\n siteKey: string,\n now: Date = new Date()\n): Promise<void> => {\n const key = normalizeSiteKey(siteKey);\n const allowlist = await readAllowlistRaw();\n const nowIso = now.toISOString();\n\n const existing = allowlist[key];\n allowlist[key] = {\n createdAt: existing?.createdAt ?? nowIso,\n lastUsedAt: nowIso,\n };\n\n await writeAllowlistRaw(allowlist);\n};\n\nexport const upsertAllowlistedSites = async (\n entries: SiteAllowlist\n): Promise<void> => {\n const allowlist = await readAllowlistRaw();\n let changed = false;\n\n for (const [k, v] of Object.entries(entries ?? {})) {\n if (typeof k !== 'string') {\n continue;\n }\n if (!isAllowlistEntry(v)) {\n continue;\n }\n allowlist[normalizeSiteKey(k)] = v;\n changed = true;\n }\n\n if (!changed) {\n return;\n }\n\n await writeAllowlistRaw(allowlist);\n};\n\nexport const touchSiteLastUsed = async (\n siteKey: string,\n now: Date = new Date()\n): Promise<void> => {\n const key = normalizeSiteKey(siteKey);\n const allowlist = await readAllowlistRaw();\n const existing = allowlist[key];\n if (!existing) {\n return;\n }\n\n allowlist[key] = { ...existing, lastUsedAt: now.toISOString() };\n await writeAllowlistRaw(allowlist);\n};\n\nexport const revokeSite = async (siteKey: string): Promise<void> => {\n const key = normalizeSiteKey(siteKey);\n const allowlist = await readAllowlistRaw();\n if (!allowlist[key]) {\n return;\n }\n delete allowlist[key];\n await writeAllowlistRaw(allowlist);\n};\n", "import {\n allowSiteAlways,\n getAllowlistedSites,\n readSitePermissionsMode,\n revokeSite,\n type SitePermissionsMode,\n upsertAllowlistedSites,\n writeSitePermissionsMode,\n} from './site-permissions.js';\n\ntype Row = {\n site: string;\n createdAt: string;\n lastUsedAt: string;\n};\n\ntype ModeEls = {\n granular: HTMLInputElement;\n bypass: HTMLInputElement;\n sitesDetails: HTMLDetailsElement;\n sitesSummary: HTMLElement;\n};\n\nconst ACTIVATION_FLAG_PARAM = 'bb_activate';\nconst ACTIVATION_PORT_PARAM = 'corePort';\n\nconst byId = (id: string): HTMLElement => {\n const el = document.getElementById(id);\n if (!el) {\n throw new Error(`Missing element: ${id}`);\n }\n return el;\n};\n\nconst elFromHtml = (html: string): HTMLElement => {\n const tpl = document.createElement('template');\n tpl.innerHTML = html.trim();\n const node = tpl.content.firstElementChild;\n if (!node) {\n throw new Error('Expected element from template.');\n }\n return node as HTMLElement;\n};\n\nconst formatTime = (iso: string): string => {\n const d = new Date(iso);\n if (!Number.isFinite(d.getTime())) {\n return iso;\n }\n try {\n return new Intl.DateTimeFormat(undefined, {\n year: 'numeric',\n month: 'numeric',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n }).format(d);\n } catch {\n return iso;\n }\n};\n\nconst parseActivationPort = (value: string | null): number | null => {\n if (!value) {\n return null;\n }\n const parsed = Number.parseInt(value, 10);\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return null;\n }\n return Math.floor(parsed);\n};\n\nconst clearActivationQueryParams = (): void => {\n const url = new URL(window.location.href);\n if (!url.search) {\n return;\n }\n url.search = '';\n window.history.replaceState(\n null,\n document.title,\n `${url.pathname}${url.hash}`\n );\n};\n\nconst writeCorePort = async (corePort: number): Promise<void> => {\n await new Promise<void>((resolve, reject) => {\n chrome.storage.local.set({ corePort }, () => {\n const error = chrome.runtime.lastError;\n if (error) {\n reject(new Error(error.message));\n return;\n }\n resolve();\n });\n });\n};\n\nconst applyActivationQueryParams = async (): Promise<void> => {\n const url = new URL(window.location.href);\n const params = url.searchParams;\n if (params.get(ACTIVATION_FLAG_PARAM) !== '1') {\n return;\n }\n\n const corePort = parseActivationPort(params.get(ACTIVATION_PORT_PARAM));\n try {\n if (corePort !== null) {\n await writeCorePort(corePort);\n } else {\n console.warn('Ignoring dev activation request with invalid corePort.');\n }\n } catch (error) {\n console.warn('Failed to apply dev activation corePort.', error);\n } finally {\n clearActivationQueryParams();\n }\n};\n\nconst createToast = (): {\n showUndo: (opts: { message: string; onUndo: () => Promise<void> }) => void;\n} => {\n const wrap = document.createElement('div');\n wrap.className = 'bb-toast-wrap';\n document.body.appendChild(wrap);\n\n let activeTimer: ReturnType<typeof globalThis.setTimeout> | null = null;\n\n const clear = (): void => {\n if (activeTimer !== null) {\n globalThis.clearTimeout(activeTimer);\n activeTimer = null;\n }\n wrap.innerHTML = '';\n };\n\n return {\n showUndo: ({ message, onUndo }): void => {\n clear();\n\n const toast = elFromHtml(`\n <div class=\"bb-toast\" role=\"status\" aria-live=\"polite\">\n <div class=\"bb-toast-msg\"></div>\n <button class=\"bb-link-button\" type=\"button\">Undo</button>\n </div>\n `);\n const msgEl = toast.querySelector('.bb-toast-msg') as HTMLElement | null;\n const undoBtn = toast.querySelector('button') as HTMLButtonElement | null;\n if (!msgEl || !undoBtn) {\n throw new Error('Toast missing required elements.');\n }\n\n msgEl.textContent = message;\n undoBtn.addEventListener('click', () => {\n undoBtn.disabled = true;\n void (async () => {\n try {\n await onUndo();\n } finally {\n clear();\n }\n })();\n });\n\n wrap.appendChild(toast);\n activeTimer = globalThis.setTimeout(() => clear(), 6000);\n },\n };\n};\n\nconst toast = createToast();\n\nconst getModeEls = (): ModeEls => {\n const granular = byId('bb-mode-granular') as HTMLInputElement;\n const bypass = byId('bb-mode-bypass') as HTMLInputElement;\n const sitesDetails = byId('bb-sites-details') as HTMLDetailsElement;\n const sitesSummary = byId('bb-sites-summary');\n\n if (granular.type !== 'radio' || bypass.type !== 'radio') {\n throw new Error('Expected radio inputs for permissions mode.');\n }\n if (sitesDetails.tagName.toLowerCase() !== 'details') {\n throw new Error('Expected a <details> for the sites disclosure.');\n }\n\n return {\n granular,\n bypass,\n sitesDetails,\n sitesSummary,\n };\n};\n\nlet lastMode: SitePermissionsMode | null = null;\nlet modeWriteInProgress = false;\n\nconst applyMode = (mode: SitePermissionsMode): void => {\n const els = getModeEls();\n els.granular.checked = mode === 'granular';\n els.bypass.checked = mode === 'bypass';\n\n // Always show the disclosure + allowlist UI in both modes.\n els.sitesSummary.textContent = 'Approved sites';\n if (mode === 'bypass') {\n if (lastMode !== 'bypass') {\n els.sitesDetails.open = false;\n }\n } else if (lastMode !== 'granular') {\n els.sitesDetails.open = true;\n }\n\n lastMode = mode;\n};\n\nconst refreshMode = async (): Promise<void> => {\n applyMode(await readSitePermissionsMode());\n};\n\nconst focusSiteRow = (site: string): void => {\n const container = byId('bb-sites');\n const rows = Array.from(container.querySelectorAll('.bb-site-row'));\n for (const rowEl of rows) {\n const el = rowEl as HTMLElement;\n if (el.dataset.site !== site) {\n continue;\n }\n\n try {\n el.scrollIntoView({ block: 'nearest' });\n } catch {\n // ignore\n }\n\n const btn = el.querySelector('button') as HTMLButtonElement | null;\n btn?.focus();\n return;\n }\n};\n\nconst render = (rows: Row[]): void => {\n const container = byId('bb-sites');\n container.innerHTML = '';\n container.hidden = false;\n\n if (rows.length === 0) {\n const empty = document.createElement('div');\n empty.className = 'bb-site-empty';\n\n const line1 = document.createElement('div');\n const title = document.createElement('strong');\n title.textContent = 'No approved sites yet.';\n line1.appendChild(title);\n empty.appendChild(line1);\n\n const line2 = document.createElement('div');\n line2.textContent =\n 'Sites show up here after you approve them in a permission prompt.';\n empty.appendChild(line2);\n container.appendChild(empty);\n return;\n }\n\n for (const row of rows) {\n const item = elFromHtml(`\n <div class=\"bb-site-row\" role=\"listitem\">\n <div class=\"bb-site-main\">\n <div class=\"bb-site-key\"></div>\n <div class=\"bb-site-meta\"></div>\n </div>\n <button class=\"bb-link-button bb-link-button-danger\" type=\"button\">\n Revoke\n </button>\n </div>\n `);\n (item as HTMLElement).dataset.site = row.site;\n\n const key = item.querySelector('.bb-site-key') as HTMLElement | null;\n const meta = item.querySelector('.bb-site-meta') as HTMLElement | null;\n const revokeBtn = item.querySelector('button') as HTMLButtonElement | null;\n if (!key || !meta || !revokeBtn) {\n throw new Error('List row missing required elements.');\n }\n\n key.textContent = row.site;\n meta.textContent = `Last used: ${formatTime(row.lastUsedAt)}`;\n meta.title = `Approved: ${formatTime(\n row.createdAt\n )}\\nLast used: ${formatTime(row.lastUsedAt)}`;\n\n revokeBtn.addEventListener('click', () => {\n revokeBtn.disabled = true;\n void (async () => {\n const before = await getAllowlistedSites();\n const entry = before[row.site] ?? before[row.site.toLowerCase()];\n\n try {\n await revokeSite(row.site);\n } finally {\n await refresh();\n revokeBtn.disabled = false;\n }\n\n if (entry) {\n toast.showUndo({\n message: `Revoked ${row.site}.`,\n onUndo: async () => {\n try {\n await upsertAllowlistedSites({ [row.site]: entry });\n const after = await getAllowlistedSites();\n if (!after[row.site] && !after[row.site.toLowerCase()]) {\n await allowSiteAlways(row.site);\n }\n } catch (err) {\n // If restore fails for any reason, fall back to re-adding the site.\n // This preserves the intended user outcome even if timestamps change.\n console.warn(\n 'Undo revoke failed; falling back to allowSiteAlways.',\n err\n );\n await allowSiteAlways(row.site);\n }\n await refresh();\n focusSiteRow(row.site);\n },\n });\n }\n })();\n });\n\n container.appendChild(item);\n }\n};\n\nconst refresh = async (): Promise<void> => {\n const allowlist = await getAllowlistedSites();\n const rows: Row[] = Object.entries(allowlist).map(([site, entry]) => ({\n site,\n createdAt: entry.createdAt,\n lastUsedAt: entry.lastUsedAt,\n }));\n\n rows.sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));\n render(rows);\n};\n\nconst setMode = async (mode: SitePermissionsMode): Promise<void> => {\n if (modeWriteInProgress) {\n return;\n }\n\n modeWriteInProgress = true;\n try {\n await writeSitePermissionsMode(mode);\n applyMode(mode);\n await refresh();\n } finally {\n modeWriteInProgress = false;\n }\n};\n\nconst refreshAll = async (): Promise<void> => {\n // Mode impacts how we want to render the empty state, so apply it first.\n await refreshMode();\n await refresh();\n};\n\nconst main = (): void => {\n void (async () => {\n await applyActivationQueryParams();\n await refreshAll();\n })();\n\n const { granular, bypass } = getModeEls();\n granular.addEventListener('change', () => {\n if (!granular.checked) {\n return;\n }\n void setMode('granular');\n });\n bypass.addEventListener('change', () => {\n if (!bypass.checked) {\n return;\n }\n void setMode('bypass');\n });\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (chrome as any).storage?.onChanged?.addListener?.(() => {\n void refreshAll();\n });\n};\n\nmain();\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["export const SITE_ALLOWLIST_KEY = 'siteAllowlist';\nexport const PERMISSION_PROMPT_WAIT_MS_KEY = 'permissionPromptWaitMs';\nexport const DEFAULT_PERMISSION_PROMPT_WAIT_MS = 30_000;\nexport const SITE_PERMISSIONS_MODE_KEY = 'sitePermissionsMode';\nexport const DEBUGGER_CAPABILITY_ENABLED_KEY = 'debuggerCapabilityEnabled';\n\nexport type SitePermissionsMode = 'granular' | 'bypass';\nexport const DEFAULT_SITE_PERMISSIONS_MODE: SitePermissionsMode = 'granular';\nexport const DEFAULT_DEBUGGER_CAPABILITY_ENABLED = false;\n\nexport type SiteAllowlistEntry = {\n createdAt: string; // ISO\n lastUsedAt: string; // ISO\n};\n\nexport type SiteAllowlist = Record<string, SiteAllowlistEntry>;\n\nexport const siteKeyFromUrl = (rawUrl: string): string | null => {\n if (!rawUrl || typeof rawUrl !== 'string') {\n return null;\n }\n\n try {\n const parsed = new URL(rawUrl);\n // Only gate \"real web\" pages for now.\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n return null;\n }\n\n // URL.hostname is lowercased by the platform.\n if (!parsed.hostname) {\n return null;\n }\n\n return parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname;\n } catch {\n return null;\n }\n};\n\nconst isAllowlistEntry = (value: unknown): value is SiteAllowlistEntry => {\n if (!value || typeof value !== 'object') {\n return false;\n }\n const v = value as Record<string, unknown>;\n return typeof v.createdAt === 'string' && typeof v.lastUsedAt === 'string';\n};\n\nconst normalizeSiteKey = (siteKey: string): string => siteKey.toLowerCase();\n\nconst readAllowlistRaw = async (): Promise<SiteAllowlist> => {\n return await new Promise<SiteAllowlist>((resolve) => {\n chrome.storage.local.get(\n [SITE_ALLOWLIST_KEY],\n (result: Record<string, unknown>) => {\n const raw = result?.[SITE_ALLOWLIST_KEY];\n if (!raw || typeof raw !== 'object') {\n resolve({});\n return;\n }\n\n const out: SiteAllowlist = {};\n for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof k !== 'string') {\n continue;\n }\n if (!isAllowlistEntry(v)) {\n continue;\n }\n out[normalizeSiteKey(k)] = v;\n }\n\n resolve(out);\n }\n );\n });\n};\n\nconst writeAllowlistRaw = async (allowlist: SiteAllowlist): Promise<void> => {\n return await new Promise<void>((resolve) => {\n chrome.storage.local.set({ [SITE_ALLOWLIST_KEY]: allowlist }, () =>\n resolve()\n );\n });\n};\n\nexport const readSitePermissionsMode =\n async (): Promise<SitePermissionsMode> => {\n return await new Promise<SitePermissionsMode>((resolve) => {\n chrome.storage.local.get(\n [SITE_PERMISSIONS_MODE_KEY],\n (result: Record<string, unknown>) => {\n const raw = result?.[SITE_PERMISSIONS_MODE_KEY];\n if (raw === 'granular' || raw === 'bypass') {\n resolve(raw);\n return;\n }\n\n // Self-heal legacy/invalid storage to a safe default, so UIs never\n // render with \"no mode selected\" and other callers don't have to\n // special-case missing values.\n try {\n chrome.storage.local.set({\n [SITE_PERMISSIONS_MODE_KEY]: DEFAULT_SITE_PERMISSIONS_MODE,\n });\n } catch {\n // ignore\n }\n resolve(DEFAULT_SITE_PERMISSIONS_MODE);\n }\n );\n });\n };\n\nexport const writeSitePermissionsMode = async (\n mode: SitePermissionsMode\n): Promise<void> => {\n return await new Promise<void>((resolve) => {\n chrome.storage.local.set({ [SITE_PERMISSIONS_MODE_KEY]: mode }, () =>\n resolve()\n );\n });\n};\n\nexport const readPermissionPromptWaitMs = async (): Promise<number> => {\n return await new Promise<number>((resolve) => {\n chrome.storage.local.get(\n [PERMISSION_PROMPT_WAIT_MS_KEY],\n (result: Record<string, unknown>) => {\n const raw = result?.[PERMISSION_PROMPT_WAIT_MS_KEY];\n if (typeof raw === 'number' && Number.isFinite(raw) && raw > 0) {\n resolve(raw);\n return;\n }\n if (typeof raw === 'string') {\n const parsed = Number(raw);\n if (Number.isFinite(parsed) && parsed > 0) {\n resolve(parsed);\n return;\n }\n }\n\n resolve(DEFAULT_PERMISSION_PROMPT_WAIT_MS);\n }\n );\n });\n};\n\nexport const readDebuggerCapabilityEnabled = async (): Promise<boolean> => {\n return await new Promise<boolean>((resolve) => {\n chrome.storage.local.get(\n [DEBUGGER_CAPABILITY_ENABLED_KEY],\n (result: Record<string, unknown>) => {\n const raw = result?.[DEBUGGER_CAPABILITY_ENABLED_KEY];\n if (typeof raw === 'boolean') {\n resolve(raw);\n return;\n }\n\n // Keep storage canonical so UI/background can rely on deterministic\n // booleans after first read.\n try {\n chrome.storage.local.set({\n [DEBUGGER_CAPABILITY_ENABLED_KEY]:\n DEFAULT_DEBUGGER_CAPABILITY_ENABLED,\n });\n } catch {\n // ignore\n }\n resolve(DEFAULT_DEBUGGER_CAPABILITY_ENABLED);\n }\n );\n });\n};\n\nexport const writeDebuggerCapabilityEnabled = async (\n enabled: boolean\n): Promise<void> => {\n return await new Promise<void>((resolve) => {\n chrome.storage.local.set(\n { [DEBUGGER_CAPABILITY_ENABLED_KEY]: Boolean(enabled) },\n () => resolve()\n );\n });\n};\n\nexport const getAllowlistedSites = async (): Promise<SiteAllowlist> => {\n return await readAllowlistRaw();\n};\n\nexport const isSiteAllowed = async (siteKey: string): Promise<boolean> => {\n const key = normalizeSiteKey(siteKey);\n const allowlist = await readAllowlistRaw();\n return Boolean(allowlist[key]);\n};\n\nexport const allowSiteAlways = async (\n siteKey: string,\n now: Date = new Date()\n): Promise<void> => {\n const key = normalizeSiteKey(siteKey);\n const allowlist = await readAllowlistRaw();\n const nowIso = now.toISOString();\n\n const existing = allowlist[key];\n allowlist[key] = {\n createdAt: existing?.createdAt ?? nowIso,\n lastUsedAt: nowIso,\n };\n\n await writeAllowlistRaw(allowlist);\n};\n\nexport const upsertAllowlistedSites = async (\n entries: SiteAllowlist\n): Promise<void> => {\n const allowlist = await readAllowlistRaw();\n let changed = false;\n\n for (const [k, v] of Object.entries(entries ?? {})) {\n if (typeof k !== 'string') {\n continue;\n }\n if (!isAllowlistEntry(v)) {\n continue;\n }\n allowlist[normalizeSiteKey(k)] = v;\n changed = true;\n }\n\n if (!changed) {\n return;\n }\n\n await writeAllowlistRaw(allowlist);\n};\n\nexport const touchSiteLastUsed = async (\n siteKey: string,\n now: Date = new Date()\n): Promise<void> => {\n const key = normalizeSiteKey(siteKey);\n const allowlist = await readAllowlistRaw();\n const existing = allowlist[key];\n if (!existing) {\n return;\n }\n\n allowlist[key] = { ...existing, lastUsedAt: now.toISOString() };\n await writeAllowlistRaw(allowlist);\n};\n\nexport const revokeSite = async (siteKey: string): Promise<void> => {\n const key = normalizeSiteKey(siteKey);\n const allowlist = await readAllowlistRaw();\n if (!allowlist[key]) {\n return;\n }\n delete allowlist[key];\n await writeAllowlistRaw(allowlist);\n};\n", "import {\n allowSiteAlways,\n DEFAULT_DEBUGGER_CAPABILITY_ENABLED,\n getAllowlistedSites,\n readDebuggerCapabilityEnabled,\n readSitePermissionsMode,\n revokeSite,\n type SitePermissionsMode,\n upsertAllowlistedSites,\n writeDebuggerCapabilityEnabled,\n writeSitePermissionsMode,\n} from './site-permissions.js';\n\ntype Row = {\n site: string;\n createdAt: string;\n lastUsedAt: string;\n};\n\ntype ModeEls = {\n granular: HTMLInputElement;\n bypass: HTMLInputElement;\n sitesDetails: HTMLDetailsElement;\n sitesSummary: HTMLElement;\n};\n\ntype DebuggerEls = {\n enabled: HTMLInputElement;\n status: HTMLElement;\n};\n\nconst ACTIVATION_FLAG_PARAM = 'bb_activate';\nconst ACTIVATION_PORT_PARAM = 'corePort';\n\nconst byId = (id: string): HTMLElement => {\n const el = document.getElementById(id);\n if (!el) {\n throw new Error(`Missing element: ${id}`);\n }\n return el;\n};\n\nconst elFromHtml = (html: string): HTMLElement => {\n const tpl = document.createElement('template');\n tpl.innerHTML = html.trim();\n const node = tpl.content.firstElementChild;\n if (!node) {\n throw new Error('Expected element from template.');\n }\n return node as HTMLElement;\n};\n\nconst formatTime = (iso: string): string => {\n const d = new Date(iso);\n if (!Number.isFinite(d.getTime())) {\n return iso;\n }\n try {\n return new Intl.DateTimeFormat(undefined, {\n year: 'numeric',\n month: 'numeric',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n }).format(d);\n } catch {\n return iso;\n }\n};\n\nconst parseActivationPort = (value: string | null): number | null => {\n if (!value) {\n return null;\n }\n const parsed = Number.parseInt(value, 10);\n if (!Number.isFinite(parsed) || parsed <= 0) {\n return null;\n }\n return Math.floor(parsed);\n};\n\nconst clearActivationQueryParams = (): void => {\n const url = new URL(window.location.href);\n if (!url.search) {\n return;\n }\n url.search = '';\n window.history.replaceState(\n null,\n document.title,\n `${url.pathname}${url.hash}`\n );\n};\n\nconst writeCorePort = async (corePort: number): Promise<void> => {\n await new Promise<void>((resolve, reject) => {\n chrome.storage.local.set({ corePort }, () => {\n const error = chrome.runtime.lastError;\n if (error) {\n reject(new Error(error.message));\n return;\n }\n resolve();\n });\n });\n};\n\nconst applyActivationQueryParams = async (): Promise<void> => {\n const url = new URL(window.location.href);\n const params = url.searchParams;\n if (params.get(ACTIVATION_FLAG_PARAM) !== '1') {\n return;\n }\n\n const corePort = parseActivationPort(params.get(ACTIVATION_PORT_PARAM));\n try {\n if (corePort !== null) {\n await writeCorePort(corePort);\n } else {\n console.warn('Ignoring dev activation request with invalid corePort.');\n }\n } catch (error) {\n console.warn('Failed to apply dev activation corePort.', error);\n } finally {\n clearActivationQueryParams();\n }\n};\n\nconst createToast = (): {\n showUndo: (opts: { message: string; onUndo: () => Promise<void> }) => void;\n} => {\n const wrap = document.createElement('div');\n wrap.className = 'bb-toast-wrap';\n document.body.appendChild(wrap);\n\n let activeTimer: ReturnType<typeof globalThis.setTimeout> | null = null;\n\n const clear = (): void => {\n if (activeTimer !== null) {\n globalThis.clearTimeout(activeTimer);\n activeTimer = null;\n }\n wrap.innerHTML = '';\n };\n\n return {\n showUndo: ({ message, onUndo }): void => {\n clear();\n\n const toast = elFromHtml(`\n <div class=\"bb-toast\" role=\"status\" aria-live=\"polite\">\n <div class=\"bb-toast-msg\"></div>\n <button class=\"bb-link-button\" type=\"button\">Undo</button>\n </div>\n `);\n const msgEl = toast.querySelector('.bb-toast-msg') as HTMLElement | null;\n const undoBtn = toast.querySelector('button') as HTMLButtonElement | null;\n if (!msgEl || !undoBtn) {\n throw new Error('Toast missing required elements.');\n }\n\n msgEl.textContent = message;\n undoBtn.addEventListener('click', () => {\n undoBtn.disabled = true;\n void (async () => {\n try {\n await onUndo();\n } finally {\n clear();\n }\n })();\n });\n\n wrap.appendChild(toast);\n activeTimer = globalThis.setTimeout(() => clear(), 6000);\n },\n };\n};\n\nconst toast = createToast();\n\nconst getModeEls = (): ModeEls => {\n const granular = byId('bb-mode-granular') as HTMLInputElement;\n const bypass = byId('bb-mode-bypass') as HTMLInputElement;\n const sitesDetails = byId('bb-sites-details') as HTMLDetailsElement;\n const sitesSummary = byId('bb-sites-summary');\n\n if (granular.type !== 'radio' || bypass.type !== 'radio') {\n throw new Error('Expected radio inputs for permissions mode.');\n }\n if (sitesDetails.tagName.toLowerCase() !== 'details') {\n throw new Error('Expected a <details> for the sites disclosure.');\n }\n\n return {\n granular,\n bypass,\n sitesDetails,\n sitesSummary,\n };\n};\n\nconst getDebuggerEls = (): DebuggerEls => {\n const enabled = byId('bb-debugger-enabled') as HTMLInputElement;\n const status = byId('bb-debugger-status');\n if (enabled.type !== 'checkbox') {\n throw new Error('Expected checkbox input for debugger capability.');\n }\n return { enabled, status };\n};\n\nlet lastMode: SitePermissionsMode | null = null;\nlet modeWriteInProgress = false;\nlet debuggerWriteInProgress = false;\nlet pendingDebuggerCapability: boolean | null = null;\n\nconst applyMode = (mode: SitePermissionsMode): void => {\n const els = getModeEls();\n els.granular.checked = mode === 'granular';\n els.bypass.checked = mode === 'bypass';\n\n // Always show the disclosure + allowlist UI in both modes.\n els.sitesSummary.textContent = 'Approved sites';\n if (mode === 'bypass') {\n if (lastMode !== 'bypass') {\n els.sitesDetails.open = false;\n }\n } else if (lastMode !== 'granular') {\n els.sitesDetails.open = true;\n }\n\n lastMode = mode;\n};\n\nconst refreshMode = async (): Promise<void> => {\n applyMode(await readSitePermissionsMode());\n};\n\nconst applyDebuggerCapability = (enabled: boolean): void => {\n const els = getDebuggerEls();\n els.enabled.checked = enabled;\n els.status.textContent = enabled\n ? 'Enabled. inspect tools can attach through the debugger bridge.'\n : 'Disabled by default. Enable only when you need inspect tools.';\n};\n\nconst refreshDebuggerCapability = async (): Promise<void> => {\n applyDebuggerCapability(await readDebuggerCapabilityEnabled());\n};\n\nconst focusSiteRow = (site: string): void => {\n const container = byId('bb-sites');\n const rows = Array.from(container.querySelectorAll('.bb-site-row'));\n for (const rowEl of rows) {\n const el = rowEl as HTMLElement;\n if (el.dataset.site !== site) {\n continue;\n }\n\n try {\n el.scrollIntoView({ block: 'nearest' });\n } catch {\n // ignore\n }\n\n const btn = el.querySelector('button') as HTMLButtonElement | null;\n btn?.focus();\n return;\n }\n};\n\nconst render = (rows: Row[]): void => {\n const container = byId('bb-sites');\n container.innerHTML = '';\n container.hidden = false;\n\n if (rows.length === 0) {\n const empty = document.createElement('div');\n empty.className = 'bb-site-empty';\n\n const line1 = document.createElement('div');\n const title = document.createElement('strong');\n title.textContent = 'No approved sites yet.';\n line1.appendChild(title);\n empty.appendChild(line1);\n\n const line2 = document.createElement('div');\n line2.textContent =\n 'Sites show up here after you approve them in a permission prompt.';\n empty.appendChild(line2);\n container.appendChild(empty);\n return;\n }\n\n for (const row of rows) {\n const item = elFromHtml(`\n <div class=\"bb-site-row\" role=\"listitem\">\n <div class=\"bb-site-main\">\n <div class=\"bb-site-key\"></div>\n <div class=\"bb-site-meta\"></div>\n </div>\n <button class=\"bb-link-button bb-link-button-danger\" type=\"button\">\n Revoke\n </button>\n </div>\n `);\n (item as HTMLElement).dataset.site = row.site;\n\n const key = item.querySelector('.bb-site-key') as HTMLElement | null;\n const meta = item.querySelector('.bb-site-meta') as HTMLElement | null;\n const revokeBtn = item.querySelector('button') as HTMLButtonElement | null;\n if (!key || !meta || !revokeBtn) {\n throw new Error('List row missing required elements.');\n }\n\n key.textContent = row.site;\n meta.textContent = `Last used: ${formatTime(row.lastUsedAt)}`;\n meta.title = `Approved: ${formatTime(\n row.createdAt\n )}\\nLast used: ${formatTime(row.lastUsedAt)}`;\n\n revokeBtn.addEventListener('click', () => {\n revokeBtn.disabled = true;\n void (async () => {\n const before = await getAllowlistedSites();\n const entry = before[row.site] ?? before[row.site.toLowerCase()];\n\n try {\n await revokeSite(row.site);\n } finally {\n await refresh();\n revokeBtn.disabled = false;\n }\n\n if (entry) {\n toast.showUndo({\n message: `Revoked ${row.site}.`,\n onUndo: async () => {\n try {\n await upsertAllowlistedSites({ [row.site]: entry });\n const after = await getAllowlistedSites();\n if (!after[row.site] && !after[row.site.toLowerCase()]) {\n await allowSiteAlways(row.site);\n }\n } catch (err) {\n // If restore fails for any reason, fall back to re-adding the site.\n // This preserves the intended user outcome even if timestamps change.\n console.warn(\n 'Undo revoke failed; falling back to allowSiteAlways.',\n err\n );\n await allowSiteAlways(row.site);\n }\n await refresh();\n focusSiteRow(row.site);\n },\n });\n }\n })();\n });\n\n container.appendChild(item);\n }\n};\n\nconst refresh = async (): Promise<void> => {\n const allowlist = await getAllowlistedSites();\n const rows: Row[] = Object.entries(allowlist).map(([site, entry]) => ({\n site,\n createdAt: entry.createdAt,\n lastUsedAt: entry.lastUsedAt,\n }));\n\n rows.sort((a, b) => b.lastUsedAt.localeCompare(a.lastUsedAt));\n render(rows);\n};\n\nconst setMode = async (mode: SitePermissionsMode): Promise<void> => {\n if (modeWriteInProgress) {\n return;\n }\n\n modeWriteInProgress = true;\n try {\n await writeSitePermissionsMode(mode);\n applyMode(mode);\n await refresh();\n } finally {\n modeWriteInProgress = false;\n }\n};\n\nconst setDebuggerCapability = async (enabled: boolean): Promise<void> => {\n pendingDebuggerCapability = enabled;\n applyDebuggerCapability(enabled);\n if (debuggerWriteInProgress) {\n return;\n }\n debuggerWriteInProgress = true;\n try {\n while (pendingDebuggerCapability !== null) {\n const next = pendingDebuggerCapability;\n pendingDebuggerCapability = null;\n await writeDebuggerCapabilityEnabled(next);\n await new Promise<void>((resolve) => {\n chrome.runtime.sendMessage(\n { action: 'drive.refresh_capabilities' },\n () => resolve()\n );\n });\n }\n } catch {\n applyDebuggerCapability(DEFAULT_DEBUGGER_CAPABILITY_ENABLED);\n } finally {\n debuggerWriteInProgress = false;\n await refreshDebuggerCapability();\n }\n};\n\nconst refreshAll = async (): Promise<void> => {\n // Mode impacts how we want to render the empty state, so apply it first.\n await refreshMode();\n await refreshDebuggerCapability();\n await refresh();\n};\n\nconst main = (): void => {\n void (async () => {\n await applyActivationQueryParams();\n await refreshAll();\n })();\n\n const { granular, bypass } = getModeEls();\n const { enabled } = getDebuggerEls();\n granular.addEventListener('change', () => {\n if (!granular.checked) {\n return;\n }\n void setMode('granular');\n });\n bypass.addEventListener('change', () => {\n if (!bypass.checked) {\n return;\n }\n void setMode('bypass');\n });\n enabled.addEventListener('change', () => {\n void setDebuggerCapability(enabled.checked);\n });\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (chrome as any).storage?.onChanged?.addListener?.(() => {\n void refreshAll();\n });\n};\n\nmain();\n"],
5
+ "mappings": "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",
6
6
  "names": ["toast"]
7
7
  }
@@ -8,23 +8,15 @@
8
8
  }
9
9
  return el;
10
10
  };
11
- var formatTime = (iso) => {
12
- if (!iso) {
13
- return "Never";
14
- }
15
- const parsed = new Date(iso);
16
- if (Number.isNaN(parsed.getTime())) {
17
- return iso;
18
- }
19
- return parsed.toLocaleString();
20
- };
21
- var setText = (id, value) => {
22
- byId(id).textContent = value;
23
- };
24
- var setStateBadge = (state) => {
25
- const badge = byId("bb-conn-state");
26
- badge.textContent = state;
27
- badge.setAttribute("data-state", state);
11
+ var setConnectedIndicator = (state) => {
12
+ const connected = state === "connected";
13
+ const indicator = byId("bb-conn-indicator");
14
+ indicator.setAttribute("data-connected", connected ? "true" : "false");
15
+ indicator.setAttribute(
16
+ "aria-label",
17
+ connected ? "Connected" : "Disconnected"
18
+ );
19
+ indicator.setAttribute("title", connected ? "Connected" : state);
28
20
  };
29
21
  var getConnectionStatus = async () => {
30
22
  const response = await new Promise(
@@ -42,35 +34,8 @@
42
34
  }
43
35
  return response.result;
44
36
  };
45
- var copyToClipboard = async (text) => {
46
- if (navigator.clipboard?.writeText) {
47
- await navigator.clipboard.writeText(text);
48
- return;
49
- }
50
- const area = document.createElement("textarea");
51
- area.value = text;
52
- area.setAttribute("readonly", "true");
53
- area.style.position = "fixed";
54
- area.style.left = "-9999px";
55
- document.body.appendChild(area);
56
- area.select();
57
- document.execCommand("copy");
58
- document.body.removeChild(area);
59
- };
60
37
  var renderStatus = (status) => {
61
- setStateBadge(status.state);
62
- setText(
63
- "bb-conn-endpoint",
64
- status.ws_url ?? (status.endpoint ? `ws://${status.endpoint.host}:${status.endpoint.port}/drive` : "Unknown")
65
- );
66
- const source = status.endpoint?.portSource ?? "unknown";
67
- setText("bb-conn-source", source);
68
- const lastConnected = formatTime(status.last_connected_at);
69
- setText("bb-conn-last-ok", lastConnected);
70
- const lastFailure = status.last_error_message ? `${status.last_error_message} (${formatTime(status.last_error_at)})` : "None";
71
- setText("bb-conn-last-fail", lastFailure);
72
- const nextRetry = status.retry_at ? formatTime(status.retry_at) : "n/a";
73
- setText("bb-conn-next-retry", nextRetry);
38
+ setConnectedIndicator(status.state);
74
39
  };
75
40
  var openOptionsPopupWindow = async () => {
76
41
  const chromeAny = chrome;
@@ -114,23 +79,12 @@
114
79
  window.open(url, "_blank", "noopener");
115
80
  };
116
81
  var main = () => {
117
- const copyButton = byId("bb-copy-diagnostics");
118
- let latestStatus = null;
119
82
  const refreshStatus = async () => {
120
83
  try {
121
84
  const status = await getConnectionStatus();
122
- latestStatus = status;
123
85
  renderStatus(status);
124
- setText("bb-conn-error", "");
125
- } catch (error) {
126
- const message = error instanceof Error ? error.message : "Failed to read connection status.";
127
- latestStatus = null;
128
- renderStatus({
129
- state: "disconnected",
130
- consecutive_failures: 0
131
- });
132
- setStateBadge("disconnected");
133
- setText("bb-conn-error", message);
86
+ } catch {
87
+ renderStatus({ state: "disconnected" });
134
88
  }
135
89
  };
136
90
  const interval = window.setInterval(() => {
@@ -139,26 +93,6 @@
139
93
  window.addEventListener("unload", () => {
140
94
  clearInterval(interval);
141
95
  });
142
- copyButton.addEventListener("click", () => {
143
- void (async () => {
144
- if (!latestStatus) {
145
- setText("bb-copy-status", "Connection status unavailable.");
146
- return;
147
- }
148
- const payload = {
149
- generated_at: (/* @__PURE__ */ new Date()).toISOString(),
150
- extension_version: chrome.runtime.getManifest().version,
151
- connection: latestStatus,
152
- user_agent: navigator.userAgent
153
- };
154
- try {
155
- await copyToClipboard(JSON.stringify(payload, null, 2));
156
- setText("bb-copy-status", "Copied diagnostics.");
157
- } catch {
158
- setText("bb-copy-status", "Copy failed.");
159
- }
160
- })();
161
- });
162
96
  byId("bb-settings").addEventListener("click", (e) => {
163
97
  e.preventDefault();
164
98
  void openOptionsPopupWindow().finally(() => window.close());
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/popup-ui.ts"],
4
- "sourcesContent": ["type DriveConnectionState =\n | 'connecting'\n | 'connected'\n | 'disconnected'\n | 'backoff';\n\ntype DriveConnectionStatus = {\n state: DriveConnectionState;\n endpoint?: {\n host: string;\n port: number;\n portSource: 'default' | 'storage';\n };\n ws_url?: string;\n reconnect_delay_ms?: number;\n retry_at?: string;\n last_connected_at?: string;\n last_disconnected_at?: string;\n last_error_at?: string;\n last_error_message?: string;\n consecutive_failures: number;\n};\n\ntype DriveConnectionStatusResponse = {\n ok: boolean;\n result?: DriveConnectionStatus;\n};\n\nconst byId = <T extends HTMLElement>(id: string): T => {\n const el = document.getElementById(id);\n if (!el) {\n throw new Error(`Missing element: ${id}`);\n }\n return el as T;\n};\n\nconst formatTime = (iso?: string): string => {\n if (!iso) {\n return 'Never';\n }\n const parsed = new Date(iso);\n if (Number.isNaN(parsed.getTime())) {\n return iso;\n }\n return parsed.toLocaleString();\n};\n\nconst setText = (id: string, value: string): void => {\n byId<HTMLElement>(id).textContent = value;\n};\n\nconst setStateBadge = (state: DriveConnectionState): void => {\n const badge = byId<HTMLElement>('bb-conn-state');\n badge.textContent = state;\n badge.setAttribute('data-state', state);\n};\n\nconst getConnectionStatus = async (): Promise<DriveConnectionStatus> => {\n const response = await new Promise<DriveConnectionStatusResponse>(\n (resolve) => {\n chrome.runtime.sendMessage(\n { action: 'drive.connection_status' },\n (message: DriveConnectionStatusResponse) => {\n resolve(message);\n }\n );\n }\n );\n\n if (!response?.ok || !response.result) {\n throw new Error('Connection status is unavailable.');\n }\n\n return response.result;\n};\n\nconst copyToClipboard = async (text: string): Promise<void> => {\n if (navigator.clipboard?.writeText) {\n await navigator.clipboard.writeText(text);\n return;\n }\n\n const area = document.createElement('textarea');\n area.value = text;\n area.setAttribute('readonly', 'true');\n area.style.position = 'fixed';\n area.style.left = '-9999px';\n document.body.appendChild(area);\n area.select();\n document.execCommand('copy');\n document.body.removeChild(area);\n};\n\nconst renderStatus = (status: DriveConnectionStatus): void => {\n setStateBadge(status.state);\n setText(\n 'bb-conn-endpoint',\n status.ws_url ??\n (status.endpoint\n ? `ws://${status.endpoint.host}:${status.endpoint.port}/drive`\n : 'Unknown')\n );\n\n const source = status.endpoint?.portSource ?? 'unknown';\n setText('bb-conn-source', source);\n\n const lastConnected = formatTime(status.last_connected_at);\n setText('bb-conn-last-ok', lastConnected);\n\n const lastFailure = status.last_error_message\n ? `${status.last_error_message} (${formatTime(status.last_error_at)})`\n : 'None';\n setText('bb-conn-last-fail', lastFailure);\n\n const nextRetry = status.retry_at ? formatTime(status.retry_at) : 'n/a';\n setText('bb-conn-next-retry', nextRetry);\n};\n\nconst openOptionsPopupWindow = async (): Promise<void> => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const chromeAny = chrome as any;\n const url = chromeAny.runtime.getURL('options.html');\n\n if (chromeAny.windows?.create) {\n await new Promise<void>((resolve) => {\n chromeAny.windows.create(\n {\n type: 'popup',\n url,\n focused: true,\n width: 900,\n height: 720,\n },\n () => resolve()\n );\n });\n return;\n }\n\n if (chromeAny.runtime?.openOptionsPage) {\n await chromeAny.runtime.openOptionsPage();\n return;\n }\n if (chromeAny.tabs?.create) {\n await new Promise<void>((resolve) => {\n chromeAny.tabs.create({ url }, () => resolve());\n });\n return;\n }\n\n window.open(url, '_blank', 'noopener');\n};\n\nconst openGithub = async (): Promise<void> => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const chromeAny = chrome as any;\n const url = 'https://github.com/btraut/browser-bridge';\n if (chromeAny.tabs?.create) {\n await new Promise<void>((resolve) => {\n chromeAny.tabs.create({ url }, () => resolve());\n });\n return;\n }\n window.open(url, '_blank', 'noopener');\n};\n\nconst main = (): void => {\n const copyButton = byId<HTMLButtonElement>('bb-copy-diagnostics');\n let latestStatus: DriveConnectionStatus | null = null;\n\n const refreshStatus = async (): Promise<void> => {\n try {\n const status = await getConnectionStatus();\n latestStatus = status;\n renderStatus(status);\n setText('bb-conn-error', '');\n } catch (error) {\n const message =\n error instanceof Error\n ? error.message\n : 'Failed to read connection status.';\n latestStatus = null;\n renderStatus({\n state: 'disconnected',\n consecutive_failures: 0,\n });\n setStateBadge('disconnected');\n setText('bb-conn-error', message);\n }\n };\n\n const interval = window.setInterval(() => {\n void refreshStatus();\n }, 1500);\n\n window.addEventListener('unload', () => {\n clearInterval(interval);\n });\n\n copyButton.addEventListener('click', () => {\n void (async () => {\n if (!latestStatus) {\n setText('bb-copy-status', 'Connection status unavailable.');\n return;\n }\n const payload = {\n generated_at: new Date().toISOString(),\n extension_version: chrome.runtime.getManifest().version,\n connection: latestStatus,\n user_agent: navigator.userAgent,\n };\n try {\n await copyToClipboard(JSON.stringify(payload, null, 2));\n setText('bb-copy-status', 'Copied diagnostics.');\n } catch {\n setText('bb-copy-status', 'Copy failed.');\n }\n })();\n });\n\n byId<HTMLAnchorElement>('bb-settings').addEventListener('click', (e) => {\n e.preventDefault();\n void openOptionsPopupWindow().finally(() => window.close());\n });\n\n byId<HTMLAnchorElement>('bb-about').addEventListener('click', (e) => {\n e.preventDefault();\n void openGithub().finally(() => window.close());\n });\n\n void refreshStatus();\n};\n\nmain();\n\nexport {};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["type DriveConnectionState =\n | 'connecting'\n | 'connected'\n | 'disconnected'\n | 'backoff';\n\ntype DriveConnectionStatus = {\n state: DriveConnectionState;\n};\n\ntype DriveConnectionStatusResponse = {\n ok: boolean;\n result?: DriveConnectionStatus;\n};\n\nconst byId = <T extends HTMLElement>(id: string): T => {\n const el = document.getElementById(id);\n if (!el) {\n throw new Error(`Missing element: ${id}`);\n }\n return el as T;\n};\n\nconst setConnectedIndicator = (state: DriveConnectionState): void => {\n const connected = state === 'connected';\n const indicator = byId<HTMLElement>('bb-conn-indicator');\n indicator.setAttribute('data-connected', connected ? 'true' : 'false');\n indicator.setAttribute(\n 'aria-label',\n connected ? 'Connected' : 'Disconnected'\n );\n indicator.setAttribute('title', connected ? 'Connected' : state);\n};\n\nconst getConnectionStatus = async (): Promise<DriveConnectionStatus> => {\n const response = await new Promise<DriveConnectionStatusResponse>(\n (resolve) => {\n chrome.runtime.sendMessage(\n { action: 'drive.connection_status' },\n (message: DriveConnectionStatusResponse) => {\n resolve(message);\n }\n );\n }\n );\n\n if (!response?.ok || !response.result) {\n throw new Error('Connection status is unavailable.');\n }\n\n return response.result;\n};\n\nconst renderStatus = (status: DriveConnectionStatus): void => {\n setConnectedIndicator(status.state);\n};\n\nconst openOptionsPopupWindow = async (): Promise<void> => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const chromeAny = chrome as any;\n const url = chromeAny.runtime.getURL('options.html');\n\n if (chromeAny.windows?.create) {\n await new Promise<void>((resolve) => {\n chromeAny.windows.create(\n {\n type: 'popup',\n url,\n focused: true,\n width: 900,\n height: 720,\n },\n () => resolve()\n );\n });\n return;\n }\n\n if (chromeAny.runtime?.openOptionsPage) {\n await chromeAny.runtime.openOptionsPage();\n return;\n }\n if (chromeAny.tabs?.create) {\n await new Promise<void>((resolve) => {\n chromeAny.tabs.create({ url }, () => resolve());\n });\n return;\n }\n\n window.open(url, '_blank', 'noopener');\n};\n\nconst openGithub = async (): Promise<void> => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const chromeAny = chrome as any;\n const url = 'https://github.com/btraut/browser-bridge';\n if (chromeAny.tabs?.create) {\n await new Promise<void>((resolve) => {\n chromeAny.tabs.create({ url }, () => resolve());\n });\n return;\n }\n window.open(url, '_blank', 'noopener');\n};\n\nconst main = (): void => {\n const refreshStatus = async (): Promise<void> => {\n try {\n const status = await getConnectionStatus();\n renderStatus(status);\n } catch {\n renderStatus({ state: 'disconnected' });\n }\n };\n\n const interval = window.setInterval(() => {\n void refreshStatus();\n }, 1500);\n\n window.addEventListener('unload', () => {\n clearInterval(interval);\n });\n\n byId<HTMLAnchorElement>('bb-settings').addEventListener('click', (e) => {\n e.preventDefault();\n void openOptionsPopupWindow().finally(() => window.close());\n });\n\n byId<HTMLAnchorElement>('bb-about').addEventListener('click', (e) => {\n e.preventDefault();\n void openGithub().finally(() => window.close());\n });\n\n void refreshStatus();\n};\n\nmain();\n\nexport {};\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Browser Bridge",
4
- "version": "0.12.0",
4
+ "version": "0.13.0",
5
5
  "description": "Control Chrome for Browser Bridge: drive tabs and inspect pages for coding agents.",
6
6
  "icons": {
7
7
  "16": "assets/icons/icon-16.png",
@@ -25,11 +25,21 @@
25
25
  "page": "options.html",
26
26
  "open_in_tab": true
27
27
  },
28
- "permissions": ["tabs", "storage", "webNavigation", "debugger", "tabGroups"],
29
- "host_permissions": ["<all_urls>"],
28
+ "permissions": ["tabs", "storage", "webNavigation", "debugger"],
29
+ "host_permissions": ["http://*/*", "https://*/*"],
30
+ "web_accessible_resources": [
31
+ {
32
+ "resources": [
33
+ "assets/icons/icon-16.png",
34
+ "assets/icons/icon-32.png",
35
+ "assets/icons/icon-48.png"
36
+ ],
37
+ "matches": ["http://*/*", "https://*/*"]
38
+ }
39
+ ],
30
40
  "content_scripts": [
31
41
  {
32
- "matches": ["<all_urls>"],
42
+ "matches": ["http://*/*", "https://*/*"],
33
43
  "js": ["dist/content.js"],
34
44
  "run_at": "document_idle"
35
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btraut/browser-bridge",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "license": "MIT",
5
5
  "engines": {
6
6
  "node": ">=20"
@@ -41,10 +41,9 @@ Global option (works on every command):
41
41
  Quickstart:
42
42
 
43
43
  ```bash
44
- browser-bridge session create
45
- # Use the session_id from the output for subsequent commands.
44
+ browser-bridge drive navigate --url https://example.com --json
45
+ # Use result.session_id from the navigate output for subsequent session-scoped commands.
46
46
 
47
- browser-bridge drive navigate --session-id <id> --url https://example.com
48
47
  browser-bridge drive wait-for --session-id <id> --kind url_matches --value example.com
49
48
 
50
49
  browser-bridge inspect dom-snapshot --session-id <id> --format ax --interactive --compact --max-nodes 2000
@@ -101,7 +100,7 @@ Note: MCP still requires `browser-bridge` to be on PATH, since the client invoke
101
100
 
102
101
  ## Practical Guidance (MCP or CLI)
103
102
 
104
- - Always store and reuse the `session_id` for subsequent calls.
103
+ - `drive.navigate` accepts optional `session_id`; omit it to let Core auto-create a session, then store/reuse the returned `session_id` for subsequent calls.
105
104
  - Drive operations are single-flight; do not overlap drive calls.
106
105
  - Drive actions are permission-gated per site (safe-by-default). The first time you target a new site, Chrome will open a permission prompt that the user must approve.
107
106
  - After navigation/clicks that trigger page loads, wait for the page to settle:
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "name": "browser-bridge",
3
- "version": "0.12.0"
3
+ "version": "0.13.0"
4
4
  }