/** * Wire protocol between the relay server and the Chrome extension. * * The extension dials out to `ws://127.0.0.1:/ext` and exchanges JSON * messages. The relay drives the extension with numbered RPCs; the extension * pushes tab lifecycle and `chrome.debugger` events as they happen. */ /** Minimal view of a Chrome tab shared between extension and relay. */ export interface TabSnapshot { tabId: number; url: string; title: string; active: boolean; /** Chrome discarded this tab; it reloads when activated. */ discarded: boolean; windowId: number; /** Pinned tabs are never grouped (Chrome would silently unpin them). */ pinned: boolean; /** Chrome tab group id; -1 when ungrouped. */ groupId: number; } /** RPCs the relay may ask the extension to perform. */ export type RelayRpcRequest = /** `clearForeignFrames`: the tab is one omp drives, so another extension's frames may be emptied past a refusal. */ | { op: "attach"; tabId: number; clearForeignFrames?: boolean } | { op: "detach"; tabId: number } | { op: "send"; tabId: number; sessionId?: string; method: string; params?: Record; clearForeignFrames?: boolean; } /** `active: false` opens the tab without selecting it; absent keeps Chrome's default (selected). */ | { op: "createTab"; url: string; active?: boolean } | { op: "removeTab"; tabId: number } | { op: "activateTab"; tabId: number } /** Add tabs to the per-window omp group (created/reused by title), remembering prior membership. */ | { op: "group"; tabIds: number[]; title: string; color: string } /** Return tabs to their pre-omp group (or ungroup); no-op for tabs the relay never grouped. */ | { op: "ungroup"; tabIds: number[] } /** * Where Chrome saved the download a driven page reported as `guid` (`Page.downloadWillBegin`), as a * {@link FoundDownload}. */ | { op: "findDownload"; guid: string }; /** The extension's answer to `findDownload`: null when it paired no Chrome download with that report. */ export type FoundDownload = { filename: string; state: "in_progress" | "interrupted" | "complete" } | null; /** * Messages sent relay → extension. `reload` asks the extension to run `chrome.runtime.reload()`, which * re-reads its files from disk; the relay sends it only after rewriting them (extension self-update). */ export type RelayToExtMessage = ({ t: "rpc"; id: number } & RelayRpcRequest) | { t: "pong" } | { t: "reload" }; /** Required capability for safe relay target discovery and auto-attach. */ export const DISCARDED_TABS_PROTOCOL_VERSION = 1; /** Hello capability of an extension that reloads itself on a relay `reload` message. */ export const EXTENSION_RELOAD_PROTOCOL_VERSION = 1; /** Chrome's refusal of a debugger request while another extension's frame (a password manager's menu) is in the tab. */ export const FOREIGN_FRAME_REFUSAL = /chrome-extension:\/\/ URL of different extension/i; /** Hello capability of an extension that answers `findDownload` (it holds Chrome's `downloads` permission). */ export const DOWNLOADS_PROTOCOL_VERSION = 1; /** * Whether a relay or extension reported a well-formed omp release version. * Stricter than `Bun.semver.order`, which ranks strings with trailing text; * peers supply these and results quote them. */ export function isOmpVersion(version: string): boolean { return /^(?=.{5,64}$)\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?$/.test(version); } /** * Whether a reported omp version is provably older than `than`: unreported ("") * is older than anything reported; otherwise both must be omp versions, since * anything else has no known age. */ export function isOlderOmpVersion(version: string, than: string): boolean { if (version === "") return than !== ""; return isOmpVersion(version) && isOmpVersion(than) && Bun.semver.order(version, than) < 0; } /** * The extension's service worker script with `ompVersion` declared above it as `OMP_INSTALL_VERSION`, which the * extension reports in its hello. The stamp sits in the script, not only the manifest, because after the files * change a Chrome restart re-reads manifest.json but keeps running the service worker it registered before. */ export function stampRelayExtensionScript(script: string, ompVersion: string): string { return `const OMP_INSTALL_VERSION = ${JSON.stringify(ompVersion)};\n${script}`; } /** The extension's manifest text with `ompVersion` as its `version_name`, which chrome://extensions shows. */ export function stampRelayExtensionManifest(manifestJson: string, ompVersion: string): string { return `${JSON.stringify({ ...JSON.parse(manifestJson), version_name: ompVersion }, null, "\t")}\n`; } /** Messages sent extension → relay. */ export type ExtToRelayMessage = | { t: "hello"; userAgent: string; browserVersion: string; tabs: TabSnapshot[]; /** Tabs that already have a `chrome.debugger` attachment (relay reconciles after a service-worker restart). */ attachedTabIds: number[]; /** Present when snapshots include Chrome's discarded state. Absent in older extensions. */ discardedTabsProtocol?: number; /** * Stable per-install browser identity (persisted in `chrome.storage.local`). * Lets the relay serve several browsers at once: tabs are namespaced per * instance, and a service-worker restart with the same id reuses the * existing tab registry instead of replacing another browser's connection. * Absent on older extensions, which share one legacy instance with * latest-wins socket replacement. */ instanceId?: string; /** * Identity of this browser run (`chrome.storage.session`), new after a browser restart, when Chrome * reuses tab ids: target ids embed it, so an old target id never names a new tab. Absent on older extensions. */ browserSession?: string; /** The omp version that installed the running script ({@link stampRelayExtensionScript}). Absent on builds without the stamp. */ ompVersion?: string; /** {@link EXTENSION_RELOAD_PROTOCOL_VERSION} when the extension reloads itself on `reload`. Absent in older extensions. */ reloadProtocol?: number; /** {@link DOWNLOADS_PROTOCOL_VERSION} when the extension answers `findDownload`. Absent in older extensions. */ downloadsProtocol?: number; } | { t: "cdpEvent"; tabId: number; sessionId?: string; method: string; params?: Record } | { t: "detached"; tabId: number; reason: string; relayInitiated?: boolean } | { t: "tabCreated"; tab: TabSnapshot } | { t: "tabUpdated"; tab: TabSnapshot } | { t: "tabRemoved"; tabId: number } | { t: "rpcResult"; id: number; ok: boolean; result?: unknown; error?: string } | { t: "ping" };