From ad8914108955d77067f3a48e2bc3fb20e9775729 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 20 Jul 2026 14:58:24 -0400 Subject: [PATCH 1/2] fix(relay): pin Chrome Web Store origin --- .changeset/tidy-shrimps-listen.md | 6 ++++++ AGENTS.md | 2 +- PLAN.md | 2 +- docs/CHROME_WEB_STORE.md | 6 +++--- extension/manifest.json | 2 +- src/relay-helpers.ts | 9 +++++++-- src/relay.ts | 6 ++++-- test/relay-helpers.test.ts | 11 +++++++++-- 8 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 .changeset/tidy-shrimps-listen.md diff --git a/.changeset/tidy-shrimps-listen.md b/.changeset/tidy-shrimps-listen.md new file mode 100644 index 0000000..cb2059d --- /dev/null +++ b/.changeset/tidy-shrimps-listen.md @@ -0,0 +1,6 @@ +--- +"@opencode-ai/browser-control": patch +--- + +Pin production WebSocket access to the assigned Chrome Web Store extension ID +while retaining an explicit source-development path for unpacked extensions. diff --git a/AGENTS.md b/AGENTS.md index 33cc108..06121a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -242,7 +242,7 @@ browser-control skill - Load `extension/dist` as the unpacked extension. - The relay listens on `127.0.0.1:19989` by default. -- Current shim version is `0.0.22` and extension protocol version is `1`. +- Current shim version is `0.0.23` and extension protocol version is `1`. - Store and npm versions may differ when protocol `1` remains compatible. - On socket open the shim sends `hello` and then re-announces every tab it still has `chrome.debugger` attached to (`debugger.attached` events), so a restarted diff --git a/PLAN.md b/PLAN.md index b343625..20548bd 100644 --- a/PLAN.md +++ b/PLAN.md @@ -167,7 +167,7 @@ require a new extension capture protocol and permission model. link`. - Until the first Store review completes, the browser extension is loaded unpacked from the npm package's `extension/dist` directory or a source build. - Its current shim version is `0.0.22`. + Its current shim version is `0.0.23`. - Extension and npm releases are independently versioned. The extension hello reports an explicit protocol version, and compatibility rather than exact package-version equality determines whether the local driver may use it. diff --git a/docs/CHROME_WEB_STORE.md b/docs/CHROME_WEB_STORE.md index 4169c68..ee86343 100644 --- a/docs/CHROME_WEB_STORE.md +++ b/docs/CHROME_WEB_STORE.md @@ -111,6 +111,6 @@ pnpm package:extension Upload `artifacts/browser-control-extension-.zip`. Record the printed SHA-256 digest with the release notes. -Before submission, add the Store-assigned extension ID to the relay's accepted -production extension origins while preserving an explicit source-development -path. +The production relay accepts Store extension ID +`gmjpoplfomnnjipeiojccjbpjlodkjhn`. Source-mode relays additionally accept +unpacked development extension origins. diff --git a/extension/manifest.json b/extension/manifest.json index 9cc4306..de30df5 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Browser Control", - "version": "0.0.22", + "version": "0.0.23", "description": "Connect browser tabs to the local Browser Control driver.", "minimum_chrome_version": "120", "permissions": ["activeTab", "alarms", "debugger", "offscreen", "tabCapture", "tabGroups"], diff --git a/src/relay-helpers.ts b/src/relay-helpers.ts index f467cc3..26dbc2f 100644 --- a/src/relay-helpers.ts +++ b/src/relay-helpers.ts @@ -9,6 +9,7 @@ import { RelayErrorCode } from "./relay-schema.ts" export const defaultHost = "127.0.0.1" export const defaultPort = 19989 +export const chromeWebStoreExtensionOrigin = "chrome-extension://gmjpoplfomnnjipeiojccjbpjlodkjhn" const maxCliBodyBytes = 1_000_000 @@ -54,16 +55,20 @@ export function validateBrowserFetchSite(request: http.IncomingMessage): string export function validateWebSocketOrigin(options: { readonly origin: string | undefined + readonly allowAnyChromeExtension?: boolean readonly requireChromeExtension?: boolean }): string | undefined { if (!options.origin) { return options.requireChromeExtension ? "Extension WebSocket origin is required" : undefined } - if (options.origin.startsWith("chrome-extension://")) { + if (options.origin === chromeWebStoreExtensionOrigin) { + return undefined + } + if (options.allowAnyChromeExtension && options.origin.startsWith("chrome-extension://")) { return undefined } if (options.requireChromeExtension) { - return "Extension WebSocket origin must be chrome-extension://" + return "Extension WebSocket origin is not allowed" } return "WebSocket origin is not allowed" } diff --git a/src/relay.ts b/src/relay.ts index 7aea172..245b8cf 100644 --- a/src/relay.ts +++ b/src/relay.ts @@ -58,6 +58,7 @@ import { RecordingRelay } from "./recording-relay.ts" import { appendManagedRelayProcessLog } from "./relay-log.ts" import { boundedToken, runtimeFailureKind, summarizeDiagnosticUrl, summarizeRuntimeEvaluate } from "./runtime-diagnostics.ts" import { resolveTargetInfoTarget, shouldExposeChildTarget, TargetRegistry, type RootTargetChange, type TargetOwnershipChange } from "./target-registry.ts" +import { browserControlVersion } from "./version.ts" export type { RelayServer } from "./relay-types.ts" @@ -147,6 +148,7 @@ const makeRelay = Effect.fnUntraced(function* (options: { const releaseTargetGraceMs = Math.max(0, options.releaseTargetGraceMs ?? 10_000) const browserId = crypto.randomUUID() const endpointUrl = `http://${formatHostForUrl(host)}:${port}` + const allowAnyChromeExtension = browserControlVersion === "0.0.0-dev" const sessionCatalog = options.sessionCatalogPath === null ? undefined : new SessionCatalog(options.sessionCatalogPath ?? defaultSessionCatalogPath(port)) @@ -602,7 +604,7 @@ const makeRelay = Effect.fnUntraced(function* (options: { const requestUrl = new URL(request.url ?? "/", endpointUrl) const origin = Array.isArray(request.headers.origin) ? request.headers.origin[0] : request.headers.origin if (requestUrl.pathname === "/extension") { - const originError = validateWebSocketOrigin({ origin, requireChromeExtension: true }) + const originError = validateWebSocketOrigin({ origin, requireChromeExtension: true, allowAnyChromeExtension }) if (originError) { sendUpgradeError({ socket, status: 403, message: originError }) return @@ -613,7 +615,7 @@ const makeRelay = Effect.fnUntraced(function* (options: { return } if (requestUrl.pathname.startsWith("/devtools/browser/")) { - const originError = validateWebSocketOrigin({ origin }) + const originError = validateWebSocketOrigin({ origin, allowAnyChromeExtension }) if (originError) { sendUpgradeError({ socket, status: 403, message: originError }) return diff --git a/test/relay-helpers.test.ts b/test/relay-helpers.test.ts index 1b5c474..3663239 100644 --- a/test/relay-helpers.test.ts +++ b/test/relay-helpers.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest" import { + chromeWebStoreExtensionOrigin, generateSessionId, getTargetInfo, isRestrictedTarget, @@ -48,15 +49,21 @@ describe("validateBrowserFetchSite", () => { }) describe("validateWebSocketOrigin", () => { - it("accepts extension origins and missing origins for non-extension clients", () => { - expect(validateWebSocketOrigin({ origin: "chrome-extension://abc" })).toBeUndefined() + it("accepts the Store extension and missing origins for non-extension clients", () => { + expect(validateWebSocketOrigin({ origin: chromeWebStoreExtensionOrigin })).toBeUndefined() expect(validateWebSocketOrigin({ origin: undefined })).toBeUndefined() }) + it("accepts unpacked extension origins only in source development", () => { + expect(validateWebSocketOrigin({ origin: "chrome-extension://unpacked", allowAnyChromeExtension: true })).toBeUndefined() + expect(validateWebSocketOrigin({ origin: "chrome-extension://unpacked", requireChromeExtension: true })).toBeDefined() + }) + it("rejects web origins for the extension endpoint", () => { expect(validateWebSocketOrigin({ origin: undefined, requireChromeExtension: true })).toBeDefined() expect(validateWebSocketOrigin({ origin: "https://example.com", requireChromeExtension: true })).toBeDefined() expect(validateWebSocketOrigin({ origin: "https://example.com" })).toBeDefined() + expect(validateWebSocketOrigin({ origin: "chrome-extension://different-extension" })).toBeDefined() }) }) From 362afe58ac6d3a0061c2fd95957adb38a7938977 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 20 Jul 2026 15:18:22 -0400 Subject: [PATCH 2/2] docs(store): add product screenshot --- docs/CHROME_WEB_STORE.md | 4 +--- .../browser-control-1280x800.jpg | Bin 0 -> 22419 bytes 2 files changed, 1 insertion(+), 3 deletions(-) create mode 100644 docs/chrome-web-store/browser-control-1280x800.jpg diff --git a/docs/CHROME_WEB_STORE.md b/docs/CHROME_WEB_STORE.md index ee86343..c8f0439 100644 --- a/docs/CHROME_WEB_STORE.md +++ b/docs/CHROME_WEB_STORE.md @@ -97,11 +97,9 @@ requires a separate explicit CLI request. Store assets live under `docs/chrome-web-store/`: - `icon-128.png` +- `browser-control-1280x800.jpg` - `small-promo-440x280.png` -A 1280x800 or 640x400 product screenshot is still required before submission; -capture it from the final Store-ID-pinned build rather than staging a mock. - Run: ```bash diff --git a/docs/chrome-web-store/browser-control-1280x800.jpg b/docs/chrome-web-store/browser-control-1280x800.jpg new file mode 100644 index 0000000000000000000000000000000000000000..66b837bb7a442a815424cca985195490b374e069 GIT binary patch literal 22419 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 literal 0 HcmV?d00001