diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aac4de2..e2e2b0f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,7 @@ jobs: run: | case "${{ matrix.client }}" in js) - xvfb-run -a pnpm exec vitest run \ + pnpm exec vitest run \ $(find js/test -name 'test.*.ts' \ ! -name 'test.ModCDPClient.ts' \ ! -name 'test.NatsUpstreamTransport.ts' \ @@ -52,7 +52,7 @@ jobs: ;; python) cd python - xvfb-run -a uv run python -m unittest \ + uv run python -m unittest \ $(find tests -name 'test_*.py' \ ! -name 'test_ModCDPClient.py' \ ! -name 'test_NatsUpstreamTransport.py' \ @@ -61,7 +61,7 @@ jobs: ;; go) cd go - xvfb-run -a go test -count=1 -p 1 \ + go test -count=1 -p 1 \ ./modcdp \ ./modcdp/injector \ ./modcdp/launcher \ @@ -122,15 +122,15 @@ jobs: run: | case "${{ matrix.client }}" in js) - xvfb-run -a node dist/js/examples/demo.js --${{ matrix.mode }} --upstream=${{ matrix.upstream }} + node dist/js/examples/demo.js --${{ matrix.mode }} --upstream=${{ matrix.upstream }} ;; python) cd python - xvfb-run -a uv run python examples/demo.py --${{ matrix.mode }} --upstream=${{ matrix.upstream }} + uv run python examples/demo.py --${{ matrix.mode }} --upstream=${{ matrix.upstream }} ;; go) cd go - xvfb-run -a go run ./examples/demo --${{ matrix.mode }} --upstream=${{ matrix.upstream }} + go run ./examples/demo --${{ matrix.mode }} --upstream=${{ matrix.upstream }} ;; *) echo "unknown client: ${{ matrix.client }}" >&2 @@ -164,25 +164,41 @@ jobs: cache-dependency-path: go/go.sum - run: pnpm install --frozen-lockfile - run: pnpm run build - - name: Run JS serialized connector tests + - name: Run JS serialized non-reverse connector tests run: | - xvfb-run -a pnpm exec vitest run \ + pnpm exec vitest run \ js/test/test.ModCDPClient.ts \ js/test/test.NatsUpstreamTransport.ts \ + js/test/test.proxy.ts \ + --testNamePattern "^(?!.*reversews).*" \ + --fileParallelism=false --maxWorkers=1 + - name: Run JS serialized reversews tests + run: | + pnpm exec vitest run \ js/test/test.ReverseWebSocketUpstreamTransport.ts \ js/test/test.proxy.ts \ + --testNamePattern "reversews" \ --fileParallelism=false --maxWorkers=1 - - name: Run Python serialized connector tests + - name: Run Python serialized non-reverse connector tests run: | cd python - xvfb-run -a uv run python -m unittest \ + uv run python -m unittest \ tests.test_ModCDPClient \ - tests.test_NatsUpstreamTransport \ + tests.test_NatsUpstreamTransport + - name: Run Python serialized reversews tests + run: | + cd python + uv run python -m unittest \ tests.test_ReverseWebSocketUpstreamTransport - - name: Run Go serialized connector tests + - name: Run Go serialized non-reverse connector tests + run: | + cd go + go test -count=1 -p 1 ./modcdp/client + go test -count=1 -p 1 ./modcdp/transport -run 'Test(UpstreamTransport|WebSocketUpstreamTransport|PipeUpstreamTransport|NativeMessagingUpstreamTransport|NatsUpstreamTransport)' + - name: Run Go serialized reversews tests run: | cd go - xvfb-run -a go test -count=1 -p 1 ./modcdp/client ./modcdp/transport + go test -count=1 -p 1 ./modcdp/transport -run 'TestReverseWebSocketUpstreamTransport' serialized-reversews-demo: name: serialized reversews demos @@ -213,12 +229,12 @@ jobs: - name: Run reversews demos serially run: | for mode in loopback debugger; do - xvfb-run -a node dist/js/examples/demo.js --"$mode" --upstream=reversews + node dist/js/examples/demo.js --"$mode" --upstream=reversews cd python - xvfb-run -a uv run python examples/demo.py --"$mode" --upstream=reversews + uv run python examples/demo.py --"$mode" --upstream=reversews cd .. cd go - xvfb-run -a go run ./examples/demo --"$mode" --upstream=reversews + go run ./examples/demo --"$mode" --upstream=reversews cd .. done @@ -247,4 +263,4 @@ jobs: cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm run build - - run: xvfb-run -a ${{ matrix.command }} + - run: ${{ matrix.command }} diff --git a/README.md b/README.md index 1967245..82bb61c 100644 --- a/README.md +++ b/README.md @@ -150,12 +150,13 @@ Native messaging mode creates the local native host wrapper and browser manifest Use reverse mode when the browser does not expose a public CDP websocket to the final client, but the ModCDP extension can open a websocket back to a local proxy. The proxy still serves a normal-looking CDP endpoint to Playwright, Puppeteer, Stagehand, or any other CDP client: ```sh -pnpm run proxy -- --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29292 --listen 127.0.0.1:9223 -pnpm run proxy -- --launcher-mode=local --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29292 --port 9223 +pnpm run proxy -- --upstream-mode=reversews --port 9223 +pnpm run proxy -- --launcher-mode=local --upstream-mode=reversews --port 9223 +pnpm run proxy -- --launcher-mode=local --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29293 --port 9223 # const browser = await playwright.chromium.connectOverCDP("http://127.0.0.1:9223") ``` -Reverse mode is opt-in. The shipped extension has no generated runtime config; it always tries the fixed local reverse connector at `ws://127.0.0.1:29292`. With `--launcher-mode=local`, use that default bind and the normal `ModCDPClient` launcher, injector, and `ReverseWebSocketUpstreamTransport` path will wake the service worker and accept its connection. With `--launcher-mode=none` or a non-default bind, an already-running extension or test must explicitly call `globalThis.ModCDP.startReverseBridge("ws://host:port", { reconnect_interval_ms: 2000 })` with the chosen endpoint because there is no side channel before the reverse connection exists. `--upstream-reversews-wait-timeout-ms` controls how long the proxy/client waits for that extension connection. Once it connects, it self-identifies as a ModCDP extension service worker and the proxy uses that reverse websocket as its upstream. `Mod.*`, expression-backed `Custom.*` commands, custom event fanout, middleware, and normal CDP commands all stay routed through `globalThis.ModCDP.handleCommand(...)` in the service worker. +Reverse mode is opt-in. The shipped extension auto-connects to the fixed local reverse connector at `ws://127.0.0.1:29292`; the proxy/client listens there and waits for that extension connection. Keep `--upstream-reversews-bind` when using a custom extension build whose compiled autoconnect URL points at a different host or port. `--upstream-reversews-wait-timeout-ms` controls how long the proxy/client waits. Once connected, the extension identifies itself as a ModCDP service worker and the proxy uses that reverse websocket as its upstream. `Mod.*`, expression-backed `Custom.*` commands, custom event fanout, middleware, and normal CDP commands all stay routed through `globalThis.ModCDP.handleCommand(...)` in the service worker. Reverse mode is intentionally scoped to one local browser and one reverse extension connection per proxy process. The browser may still have other extensions installed; ModCDP does not require `--disable-extensions-except`. @@ -230,7 +231,7 @@ dist/ Built JS output used by the extension and Node CLI scr 3. Attach a session to that SW target and `Runtime.enable` on it. 4. Call `globalThis.ModCDP.configure(...)` to push the resolved loopback websocket and any explicit server route overrides into the SW. The clients do this automatically by default. -Reverse proxy mode flips the bootstrap direction: `js/src/proxy/proxy.ts --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29292` listens for the extension, while still serving downstream clients from `--listen`. The extension artifact is fixed, so automatic launch/injection uses the default `127.0.0.1:29292` connector; non-default reverse binds need an external bootstrap call to `globalThis.ModCDP.startReverseBridge(...)` with the same URL. The service worker sends a `modcdp.reverse.hello` message and then accepts CDP-shaped command messages from the proxy. The proxy maps downstream request IDs to reverse request IDs and forwards reverse events back to the downstream CDP client. +Reverse proxy mode flips the bootstrap direction: `js/src/proxy/proxy.ts --upstream-mode=reversews --port 9223` listens for the extension on the configured reverse connector while still serving downstream clients from the proxy port. The service worker sends a `modcdp.reverse.hello` message and then accepts CDP-shaped command messages from the proxy. The proxy maps downstream request IDs to reverse request IDs and forwards reverse events back to the downstream CDP client. ### Send diff --git a/TODO_layout.md b/TODO_layout.md index aa73d53..329c418 100644 --- a/TODO_layout.md +++ b/TODO_layout.md @@ -16,8 +16,6 @@ repo/ pages/ options.html options.ts - wake.html - wake.ts offscreen_keepalive.html offscreen_keepalive.ts diff --git a/extension/src/pages/options.html b/extension/src/pages/options.html index ec32164..57c97cb 100644 --- a/extension/src/pages/options.html +++ b/extension/src/pages/options.html @@ -21,4 +21,4 @@

ModCDP

Loading...
- + diff --git a/extension/src/pages/wake.html b/extension/src/pages/wake.html deleted file mode 100644 index 014ae62..0000000 --- a/extension/src/pages/wake.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - ModCDP Wake - - - - diff --git a/extension/src/pages/wake.ts b/extension/src/pages/wake.ts deleted file mode 100644 index ad8db44..0000000 --- a/extension/src/pages/wake.ts +++ /dev/null @@ -1,3 +0,0 @@ -chrome.runtime.sendMessage({ type: "modcdp.wake", at: Date.now() }, () => { - setTimeout(() => window.close(), 250); -}); diff --git a/extension/src/service_worker.ts b/extension/src/service_worker.ts index 4c9fcb4..954b81c 100644 --- a/extension/src/service_worker.ts +++ b/extension/src/service_worker.ts @@ -92,7 +92,13 @@ const downstreamClient = (session_id?: string | null) => { last_seen: at, }); const id = session_id || "root"; - const session = (client.sessions[id] ??= { id, commands: 0, events: 0, first_seen: at, last_seen: at }); + const session = (client.sessions[id] ??= { + id, + commands: 0, + events: 0, + first_seen: at, + last_seen: at, + }); return { at, client_id, client, session }; }; const logTraffic = (direction: "command" | "event", name: string, payload: unknown, session_id?: string | null) => { @@ -156,7 +162,11 @@ if (bridge) { if (start) { bridge[method] = (...args: unknown[]) => { const result = start(...args); - self_transports[key] = { args: compact(args), result: compact(result), updated_at: new Date().toISOString() }; + self_transports[key] = { + args: compact(args), + result: compact(result), + updated_at: new Date().toISOString(), + }; return result; }; } @@ -190,15 +200,6 @@ chrome.runtime.onInstalled.addListener(startConfiguredTransports); chrome.runtime.onStartup.addListener(startConfiguredTransports); chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { - if (message?.type === "modcdp.wake") { - startConfiguredTransports(); - sendResponse({ - ok: true, - extension_id: chrome.runtime.id, - service_worker_url: chrome.runtime.getURL("modcdp/service_worker.js"), - }); - return false; - } if (message?.type !== "modcdp.options.status") return false; const self = { id: "self", @@ -221,7 +222,10 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { native_bridge_last_error: bridge.native_bridge_last_error, }, ...(Object.keys(self_transports).length ? { transports: self_transports } : {}), - custom: { commands: [...self_custom.commands], events: [...self_custom.events] }, + custom: { + commands: [...self_custom.commands], + events: [...self_custom.events], + }, log: self_log, }; sendResponse({ @@ -240,7 +244,11 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { }, } : {}), - debugger: { ...upstream_servers.debugger, id: "debugger", log: upstream_servers.debugger?.log ?? [] }, + debugger: { + ...upstream_servers.debugger, + id: "debugger", + log: upstream_servers.debugger?.log ?? [], + }, }, }); return false; diff --git a/go/examples/demo/main.go b/go/examples/demo/main.go index 21afb8f..27b010d 100644 --- a/go/examples/demo/main.go +++ b/go/examples/demo/main.go @@ -29,11 +29,23 @@ import ( "golang.org/x/term" ) +const reverseTransportWaitTimeoutMS = 60_000 + func optionsFor(mode, upstreamMode, cdpURL, extensionPath string, launchOptions modcdp.LaunchOptions) modcdp.Options { + upstream := modcdp.UpstreamConfig{UpstreamMode: upstreamMode, UpstreamCDPURL: cdpURL} + if upstreamMode == "reversews" { + upstream.UpstreamReverseWSWaitTimeoutMS = reverseTransportWaitTimeoutMS + } + if upstreamMode == "nativemessaging" { + upstream.UpstreamNativeMessagingWaitTimeoutMS = reverseTransportWaitTimeoutMS + } + if upstreamMode == "nats" { + upstream.UpstreamNATSWaitTimeoutMS = reverseTransportWaitTimeoutMS + } if mode == "direct" { return modcdp.Options{ Launcher: modcdp.LauncherConfig{LauncherMode: map[bool]string{true: "remote", false: "local"}[cdpURL != ""], LauncherOptions: launchOptions}, - Upstream: modcdp.UpstreamConfig{UpstreamMode: upstreamMode, UpstreamCDPURL: cdpURL}, + Upstream: upstream, Injector: modcdp.InjectorConfig{InjectorMode: "auto", InjectorExtensionPath: extensionPath}, Client: modcdp.ClientConfig{ClientRoutes: clientRoutesFor(mode)}, } @@ -43,7 +55,7 @@ func optionsFor(mode, upstreamMode, cdpURL, extensionPath string, launchOptions } return modcdp.Options{ Launcher: modcdp.LauncherConfig{LauncherMode: map[bool]string{true: "remote", false: "local"}[cdpURL != ""], LauncherOptions: launchOptions}, - Upstream: modcdp.UpstreamConfig{UpstreamMode: upstreamMode, UpstreamCDPURL: cdpURL}, + Upstream: upstream, Injector: modcdp.InjectorConfig{InjectorMode: "auto", InjectorExtensionPath: extensionPath}, Client: modcdp.ClientConfig{ClientRoutes: clientRoutesFor(mode)}, Server: server, @@ -199,9 +211,10 @@ func main() { headless = true } launchOptions = modcdp.LaunchOptions{ - ExecutablePath: chromePath, - Headless: &headless, - Sandbox: &sandbox, + ExecutablePath: chromePath, + ChromeReadyTimeoutMS: 60_000, + Headless: &headless, + Sandbox: &sandbox, } } @@ -209,7 +222,7 @@ func main() { var ( eventsMu sync.Mutex targetCreatedEvents []modcdp.TargetTargetCreatedEvent - foregroundEvents []map[string]any + pageTargetEvents []map[string]any ) cdp.Target.On.TargetCreated(func(event modcdp.TargetTargetCreatedEvent) { fmt.Printf("Target.targetCreated -> %s\n", event.TargetID()) @@ -421,34 +434,21 @@ func main() { }) fmt.Println("Custom.demoEvent ->", demoEvent) - foregroundEventRegistrationRaw, err := cdp.Mod.AddCustomEvent(modcdp.CustomEvent{Name: "Custom.foregroundTargetChanged"}) + pageTargetEventRegistrationRaw, err := cdp.Mod.AddCustomEvent(modcdp.CustomEvent{Name: "Custom.pageTargetUpdated"}) if err != nil { - log.Fatalf("Mod.addCustomEvent Custom.foregroundTargetChanged: %v", err) + log.Fatalf("Mod.addCustomEvent Custom.pageTargetUpdated: %v", err) } - foregroundEventRegistration := mustMap(foregroundEventRegistrationRaw, "Mod.addCustomEvent Custom.foregroundTargetChanged") - if foregroundEventRegistration["registered"] != true { - log.Fatalf("unexpected foreground event registration: %v", foregroundEventRegistration) + pageTargetEventRegistration := mustMap(pageTargetEventRegistrationRaw, "Mod.addCustomEvent Custom.pageTargetUpdated") + if pageTargetEventRegistration["registered"] != true { + log.Fatalf("unexpected page target event registration: %v", pageTargetEventRegistration) } - cdp.On("Custom.foregroundTargetChanged", func(p any) { + cdp.On("Custom.pageTargetUpdated", func(p any) { event, _ := p.(map[string]any) - fmt.Printf("Custom.foregroundTargetChanged -> %v\n", event) + fmt.Printf("Custom.pageTargetUpdated -> %v\n", event) eventsMu.Lock() - foregroundEvents = append(foregroundEvents, event) + pageTargetEvents = append(pageTargetEvents, event) eventsMu.Unlock() }) - if _, err := cdp.Mod.Evaluate(map[string]any{ - "expression": `async () => { - chrome.tabs.onActivated.addListener(async ({ tabId }) => { - const targets = await chrome.debugger.getTargets(); - const target = targets.find(target => target.type === "page" && target.tabId === tabId); - const tab = await chrome.tabs.get(tabId).catch(() => null); - await cdp.emit("Custom.foregroundTargetChanged", { tabId, targetId: target?.id ?? null, url: target?.url ?? tab?.url ?? null }); - }); - return true; - }`, - }); err != nil { - log.Fatal(err) - } if _, err := cdp.Target.SetDiscoverTargets(modcdp.TargetSetDiscoverTargetsParams{Discover: true}); err != nil { log.Fatal(err) @@ -485,49 +485,67 @@ func main() { } fmt.Println("normal event matched ->", createdTargetID) + tabFromTargetRaw, err := cdp.Send("Custom.TabIdFromTargetId", map[string]any{"targetId": createdTargetID}) + if err != nil { + log.Fatalf("Custom.TabIdFromTargetId: %v", err) + } + tabFromTarget, _ := tabFromTargetRaw.(map[string]any) + b, _ = json.Marshal(tabFromTarget) + fmt.Println("Custom.TabIdFromTargetId ->", string(b)) + if _, err := cdp.Target.ActivateTarget(modcdp.TargetActivateTargetParams{TargetID: modcdp.TargetTargetID(createdTargetID)}); err != nil { log.Fatalf("Target.activateTarget: %v", err) } + pageTargetEmitRaw, err := cdp.Mod.Evaluate(map[string]any{ + "params": map[string]any{"targetId": createdTargetID}, + "expression": `async ({ targetId }) => { + const targets = await chrome.debugger.getTargets(); + const target = targets.find(target => target.id === targetId); + if (!target?.id) throw new Error(` + "`target ${targetId} not found`" + `); + await cdp.emit("Custom.pageTargetUpdated", { targetId: target.id, url: target.url ?? null }); + return { emitted: true, targetId: target.id }; + }`, + }) + if err != nil { + log.Fatalf("Custom.pageTargetUpdated emit: %v", err) + } + pageTargetEmit := mustMap(pageTargetEmitRaw, "Custom.pageTargetUpdated emit") + if pageTargetEmit["emitted"] != true || pageTargetEmit["targetId"] != createdTargetID { + log.Fatalf("unexpected Custom.pageTargetUpdated emit result: %v", pageTargetEmit) + } deadline = time.Now().Add(3 * time.Second) - var foreground map[string]any + var pageTarget map[string]any for time.Now().Before(deadline) { eventsMu.Lock() - for _, event := range foregroundEvents { + for _, event := range pageTargetEvents { if event["targetId"] == createdTargetID { - foreground = event + pageTarget = event break } } eventsMu.Unlock() - if foreground != nil { + if pageTarget != nil { break } time.Sleep(20 * time.Millisecond) } - if foreground == nil { - log.Fatalf("expected Custom.foregroundTargetChanged for %s", createdTargetID) + if pageTarget == nil { + log.Fatalf("expected Custom.pageTargetUpdated for %s", createdTargetID) } - tabFromTargetRaw, err := cdp.Send("Custom.TabIdFromTargetId", map[string]any{"targetId": createdTargetID}) - if err != nil { - log.Fatalf("Custom.TabIdFromTargetId: %v", err) - } - tabFromTarget, _ := tabFromTargetRaw.(map[string]any) - foregroundTabID, _ := foreground["tabId"].(float64) + pageTargetTabID, _ := pageTarget["tabId"].(float64) tabID, _ := tabFromTarget["tabId"].(float64) - if tabID != foregroundTabID { + if tabID != pageTargetTabID { log.Fatalf("unexpected Custom.TabIdFromTargetId result: %v", tabFromTarget) } - b, _ = json.Marshal(tabFromTarget) - fmt.Println("Custom.TabIdFromTargetId ->", string(b)) - targetFromTabRaw, err := cdp.Send("Custom.targetIdFromTabId", map[string]any{"tabId": foreground["tabId"]}) + targetFromTabRaw, err := cdp.Send("Custom.targetIdFromTabId", map[string]any{"tabId": pageTarget["tabId"]}) if err != nil { log.Fatalf("Custom.targetIdFromTabId: %v", err) } targetFromTab, _ := targetFromTabRaw.(map[string]any) middlewareTabID, _ := targetFromTab["tabId"].(float64) - if targetFromTab["targetId"] != createdTargetID || middlewareTabID != foregroundTabID { + if targetFromTab["targetId"] != createdTargetID || middlewareTabID != pageTargetTabID { log.Fatalf("unexpected Custom.targetIdFromTabId/middleware result: %v", targetFromTab) } b, _ = json.Marshal(targetFromTab) diff --git a/go/go.mod b/go/go.mod index ad58ae9..259e5d1 100644 --- a/go/go.mod +++ b/go/go.mod @@ -3,7 +3,7 @@ module github.com/browserbase/modcdp/go go 1.25.0 require ( - github.com/ArchiveBox/abxbus/abxbus-go v0.0.0-20260508073455-c5b79a4483f1 + github.com/ArchiveBox/abxbus/abxbus-go/v2 v2.5.2 github.com/gobwas/ws v1.4.0 golang.org/x/term v0.6.0 ) diff --git a/go/go.sum b/go/go.sum index 8591e1d..5b1f7d4 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,5 +1,5 @@ -github.com/ArchiveBox/abxbus/abxbus-go v0.0.0-20260508073455-c5b79a4483f1 h1:AMZ2Zr6mfP9aDuKjcJvxHkZvlqqf6jQhyk5Ju3jyfY8= -github.com/ArchiveBox/abxbus/abxbus-go v0.0.0-20260508073455-c5b79a4483f1/go.mod h1:yYB8LZqGpYryBo38Hl2s3dhPZweuUwqQxiMZGrY8XXE= +github.com/ArchiveBox/abxbus/abxbus-go/v2 v2.5.2 h1:qpCQCuZ/Nfi6mHf00Ed9nI88JBaAMOh+PhXEO1hdcfQ= +github.com/ArchiveBox/abxbus/abxbus-go/v2 v2.5.2/go.mod h1:Nk7W3WwqKgQw07gQ6V+UdaUJzJwa0Qj78F/n7sERs6A= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= diff --git a/go/modcdp/client/ModCDPClient.go b/go/modcdp/client/ModCDPClient.go index ed0346e..d3e32a4 100644 --- a/go/modcdp/client/ModCDPClient.go +++ b/go/modcdp/client/ModCDPClient.go @@ -23,14 +23,13 @@ import ( "net" "net/http" "net/url" - "os" "reflect" "regexp" "strings" "sync" "time" - abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/jsonschema" + abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/v2/jsonschema" "github.com/browserbase/modcdp/go/modcdp/injector" "github.com/browserbase/modcdp/go/modcdp/launcher" "github.com/browserbase/modcdp/go/modcdp/router" @@ -110,7 +109,6 @@ var NewAutoSessionRouter = router.NewAutoSessionRouter var DefaultModCDPServiceWorkerURLSuffixes = injector.DefaultModCDPServiceWorkerURLSuffixes -const DefaultModCDPWakePath = injector.DefaultModCDPWakePath const DefaultModCDPExtensionID = injector.DefaultModCDPExtensionID const DefaultUpstreamReverseWSBind = transportpkg.DefaultUpstreamReverseWSBind const DefaultUpstreamReverseWSWaitTimeoutMS = transportpkg.DefaultUpstreamReverseWSWaitTimeoutMS @@ -238,8 +236,6 @@ type InjectorConfig struct { InjectorMode string `json:"injector_mode,omitempty"` InjectorExtensionPath string `json:"injector_extension_path,omitempty"` InjectorExtensionID string `json:"injector_extension_id,omitempty"` - InjectorWakePath string `json:"injector_wake_path,omitempty"` - InjectorWakeURL string `json:"injector_wake_url,omitempty"` InjectorServiceWorkerURLIncludes []string `json:"injector_service_worker_url_includes,omitempty"` InjectorServiceWorkerURLSuffixes []string `json:"injector_service_worker_url_suffixes,omitempty"` InjectorTrustServiceWorkerTarget bool `json:"injector_trust_service_worker_target,omitempty"` @@ -563,6 +559,7 @@ func New(opts Options) *ModCDPClient { if *client.Client.ClientHydrateAliases { initCDPSurface(client) } + client.hydrateNativeProtocolSchemas() client.hydrateCustomSurface() return client } @@ -929,11 +926,6 @@ func (c *ModCDPClient) serverConfigureParams(customCommands []map[string]any, cu if c.Upstream.UpstreamNATSSubjectPrefix != "" { upstream["upstream_nats_subject_prefix"] = c.Upstream.UpstreamNATSSubjectPrefix } - if c.transport != nil { - if reverseWSURL := c.transport.GetInjectorConfig().UpstreamReverseWSURL; reverseWSURL != "" { - upstream["upstream_reversews_url"] = reverseWSURL - } - } return map[string]any{ "upstream": upstream, "client": map[string]any{ @@ -972,6 +964,40 @@ func cloneSchema(schema map[string]any) map[string]any { return normalized } +func nativeResultSchema(schema map[string]any) map[string]any { + normalized := cloneSchema(schema) + allowNativeResultExtensions(normalized) + return normalized +} + +func allowNativeResultExtensions(schema map[string]any) { + if schema == nil { + return + } + if schemaType, _ := schema["type"].(string); schemaType == "object" { + schema["additionalProperties"] = true + if properties, ok := schema["properties"].(map[string]any); ok { + for _, property := range properties { + if propertySchema, ok := property.(map[string]any); ok { + allowNativeResultExtensions(propertySchema) + } + } + } + } + if items, ok := schema["items"].(map[string]any); ok { + allowNativeResultExtensions(items) + } + for _, key := range []string{"anyOf", "oneOf", "allOf"} { + if schemas, ok := schema[key].([]any); ok { + for _, entry := range schemas { + if entrySchema, ok := entry.(map[string]any); ok { + allowNativeResultExtensions(entrySchema) + } + } + } + } +} + func resultUnwrapKeyFromSchema(schema map[string]any) string { properties, _ := schema["properties"].(map[string]any) if len(properties) != 1 { @@ -1146,8 +1172,7 @@ func (c *ModCDPClient) validateEventData(event string, data any) (any, bool) { return data, true } if err := abxjsonschema.Validate(schema, data); err != nil { - fmt.Fprintf(os.Stderr, "[ModCDPClient] %s event did not match event_schema: %v\n", event, err) - return nil, false + panic(fmt.Errorf("%s event did not match event_schema: %w", event, err)) } return data, true } @@ -1444,7 +1469,8 @@ func (c *ModCDPClient) extensionInjectorsForConfig() []extensionInjector { return nil } var injectors []extensionInjector - if c.Injector.InjectorMode == "auto" || c.Injector.InjectorMode == "discover" { + preferLaunchInjection := c.Injector.InjectorMode == "auto" && c.Launcher.LauncherMode == "local" + if (c.Injector.InjectorMode == "auto" || c.Injector.InjectorMode == "discover") && !preferLaunchInjection { injector := NewDiscoveredExtensionInjector(ExtensionInjectorConfig{}) injectors = append(injectors, &injector) } @@ -1460,6 +1486,10 @@ func (c *ModCDPClient) extensionInjectorsForConfig() []extensionInjector { injector := NewExtensionsLoadUnpackedInjector(ExtensionInjectorConfig{}) injectors = append(injectors, &injector) } + if preferLaunchInjection { + injector := NewDiscoveredExtensionInjector(ExtensionInjectorConfig{}) + injectors = append(injectors, &injector) + } if c.Injector.InjectorMode == "auto" || c.Injector.InjectorMode == "borrow" { injector := NewBorrowedExtensionInjector(ExtensionInjectorConfig{}) injectors = append(injectors, &injector) @@ -1497,8 +1527,6 @@ func (c *ModCDPClient) baseExtensionInjectorConfig(send SendCDP) ExtensionInject }, InjectorExtensionPath: c.Injector.InjectorExtensionPath, InjectorExtensionID: c.Injector.InjectorExtensionID, - InjectorWakePath: firstNonEmptyString(c.Injector.InjectorWakePath, DefaultModCDPWakePath), - InjectorWakeURL: c.Injector.InjectorWakeURL, InjectorServiceWorkerURLIncludes: c.Injector.InjectorServiceWorkerURLIncludes, InjectorServiceWorkerURLSuffixes: c.Injector.InjectorServiceWorkerURLSuffixes, InjectorTrustServiceWorkerTarget: trustMatchedServiceWorker, diff --git a/go/modcdp/client/ModCDPClientCustomFlatNamespace_test.go b/go/modcdp/client/ModCDPClientCustomFlatNamespace_test.go index 24a540a..76fc943 100644 --- a/go/modcdp/client/ModCDPClientCustomFlatNamespace_test.go +++ b/go/modcdp/client/ModCDPClientCustomFlatNamespace_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/jsonschema" + abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/v2/jsonschema" ) func TestCustomCommandsInstallFlatNamespaceThroughRealServiceWorker(t *testing.T) { @@ -24,7 +24,6 @@ func TestCustomCommandsInstallFlatNamespaceThroughRealServiceWorker(t *testing.T Launcher: LauncherConfig{LauncherMode: "local", LauncherOptions: LaunchOptions{ Headless: boolPtr(true), - Sandbox: boolPtr(false), }, }, Upstream: UpstreamConfig{UpstreamMode: "ws"}, @@ -84,7 +83,6 @@ func TestCustomEventsValidateRawStringHandlersThroughRealServiceWorker(t *testin Launcher: LauncherConfig{LauncherMode: "local", LauncherOptions: LaunchOptions{ Headless: boolPtr(true), - Sandbox: boolPtr(false), }, }, Upstream: UpstreamConfig{UpstreamMode: "ws"}, @@ -236,7 +234,15 @@ func TestTypedCustomEventRegistrationAndHandler(t *testing.T) { if got := <-seen; got != "ok" { t.Fatalf("unexpected typed event data %q", got) } - if _, ok := cdp.validateEventData("Custom.someEvent", map[string]any{"data": 123}); ok { - t.Fatal("expected typed event schema to reject wrong data type") - } + expectPanic(t, func() { cdp.validateEventData("Custom.someEvent", map[string]any{"data": 123}) }) +} + +func expectPanic(t *testing.T, fn func()) { + t.Helper() + defer func() { + if recover() == nil { + t.Fatal("expected panic") + } + }() + fn() } diff --git a/go/modcdp/client/ModCDPClientRoutedDefaultOverrides_test.go b/go/modcdp/client/ModCDPClientRoutedDefaultOverrides_test.go index 604911a..f91cb10 100644 --- a/go/modcdp/client/ModCDPClientRoutedDefaultOverrides_test.go +++ b/go/modcdp/client/ModCDPClientRoutedDefaultOverrides_test.go @@ -60,25 +60,31 @@ async (payload, next) => { func TestModCDPClientRoutedDefaultOverrides(t *testing.T) { headless := true - sandbox := false extensionPath, err := filepath.Abs("../../../dist/extension") if err != nil { t.Fatal(err) } - chrome, err := NewLocalBrowserLauncher(LaunchOptions{ - Headless: &headless, - Sandbox: &sandbox, - ExtraArgs: []string{"--load-extension=" + extensionPath}, - }).Launch(LaunchOptions{}) - if err != nil { + owner := New(Options{ + Launcher: LauncherConfig{ + LauncherMode: "local", + LauncherOptions: LaunchOptions{Headless: &headless}, + }, + Upstream: UpstreamConfig{UpstreamMode: "ws"}, + Injector: InjectorConfig{ + InjectorMode: "auto", + InjectorExtensionPath: extensionPath, + InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, + InjectorTrustServiceWorkerTarget: true, + }, + }) + if err := owner.Connect(); err != nil { t.Fatal(err) } cdp := New(Options{ Launcher: LauncherConfig{LauncherMode: "remote"}, - Upstream: UpstreamConfig{UpstreamMode: "ws", UpstreamCDPURL: chrome.CDPURL}, + Upstream: UpstreamConfig{UpstreamMode: "ws", UpstreamCDPURL: owner.CDPURL}, Injector: InjectorConfig{ - InjectorMode: "auto", - InjectorExtensionPath: extensionPath, + InjectorMode: "discover", InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, InjectorTrustServiceWorkerTarget: true, }, @@ -90,24 +96,24 @@ func TestModCDPClientRoutedDefaultOverrides(t *testing.T) { }, }, Server: &ServerConfig{ - ServerLoopbackCDPURL: chrome.CDPURL, + ServerLoopbackCDPURL: owner.CDPURL, ServerRoutes: map[string]string{"*.*": "loopback_cdp"}, }, }) - defer chrome.Close() + defer owner.Close() defer cdp.Close() if err := cdp.Connect(); err != nil { t.Fatal(err) } - if cdp.CDPURL != chrome.CDPURL { - t.Fatalf("CDPURL = %q, expected %q", cdp.CDPURL, chrome.CDPURL) + if cdp.CDPURL != owner.CDPURL { + t.Fatalf("CDPURL = %q, expected %q", cdp.CDPURL, owner.CDPURL) } - if cdp.Server.ServerLoopbackCDPURL != chrome.CDPURL { - t.Fatalf("ServerLoopbackCDPURL = %q, expected %q", cdp.Server.ServerLoopbackCDPURL, chrome.CDPURL) + if cdp.Server.ServerLoopbackCDPURL != owner.CDPURL { + t.Fatalf("ServerLoopbackCDPURL = %q, expected %q", cdp.Server.ServerLoopbackCDPURL, owner.CDPURL) } - rawTargets, err := cdp.sendMessage("Target.getTargets", map[string]any{}, "") + rawTargets, err := cdp.Send("Target.getTargets", nil) if err != nil { t.Fatal(err) } diff --git a/go/modcdp/client/ModCDPClient_protocol_validation_test.go b/go/modcdp/client/ModCDPClient_protocol_validation_test.go new file mode 100644 index 0000000..97d0f69 --- /dev/null +++ b/go/modcdp/client/ModCDPClient_protocol_validation_test.go @@ -0,0 +1,145 @@ +package client + +import ( + "testing" + + abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/v2/jsonschema" +) + +type protocolValidationCustomEchoParams struct { + Text string `json:"text"` +} + +type protocolValidationCustomEchoResult struct { + OK bool `json:"ok"` +} + +type protocolValidationCustomReadyEvent struct { + OK bool `json:"ok"` +} + +func TestProtocolValidationCoversNativeMethodsNativeEventsCustomMethodsCustomEventsAndNativeOverrides(t *testing.T) { + cdp := New(Options{}) + + runtimeParams := RuntimeEvaluateParams{Expression: "1 + 1", ReturnByValue: Bool(true)} + runtimeResult := RuntimeEvaluateResult{Result: RuntimeRemoteObject{Type: "number", Value: 2}} + nativeEvent := TargetTargetCreatedEvent{ + TargetInfo: TargetTargetInfo{ + TargetID: TargetTargetID("target-1"), + Type: "page", + Title: "Example", + URL: "https://example.com", + Attached: false, + CanAccessOpener: false, + }, + } + customParams := protocolValidationCustomEchoParams{Text: "ok"} + customResult := protocolValidationCustomEchoResult{OK: true} + customEvent := protocolValidationCustomReadyEvent{OK: true} + + if err := cdp.validateCommandParams("Runtime.evaluate", mustParamsMap(t, runtimeParams)); err != nil { + t.Fatalf("native Runtime.evaluate params should validate: %v", err) + } + if err := cdp.validateCommandResult("Runtime.evaluate", runtimeResult); err != nil { + t.Fatalf("native Runtime.evaluate result should validate: %v", err) + } + if _, ok := cdp.validateEventData("Target.targetCreated", nativeEvent); !ok { + t.Fatal("native Target.targetCreated event should validate") + } + if err := cdp.validateCommandParams("Runtime.evaluate", map[string]any{}); err == nil { + t.Fatal("expected Runtime.evaluate params validation to reject missing expression") + } + if err := cdp.validateCommandResult("Runtime.evaluate", map[string]any{}); err == nil { + t.Fatal("expected Runtime.evaluate result validation to reject missing result") + } + expectPanic(t, func() { cdp.validateEventData("Target.targetCreated", map[string]any{}) }) + + if _, err := cdp.Mod.AddCustomCommand(CustomCommand{ + Name: "Custom.echo", + ParamsSchema: abxjsonschema.SchemaFor[protocolValidationCustomEchoParams](), + ResultSchema: abxjsonschema.SchemaFor[protocolValidationCustomEchoResult](), + }); err != nil { + t.Fatal(err) + } + if _, err := cdp.Mod.AddCustomEvent(CustomEvent{ + Name: "Custom.ready", + EventSchema: abxjsonschema.SchemaFor[protocolValidationCustomReadyEvent](), + }); err != nil { + t.Fatal(err) + } + + if err := cdp.validateCommandParams("Custom.echo", mustParamsMap(t, customParams)); err != nil { + t.Fatalf("custom params should validate: %v", err) + } + if err := cdp.validateCommandResult("Custom.echo", customResult); err != nil { + t.Fatalf("custom result should validate: %v", err) + } + if _, ok := cdp.validateEventData("Custom.ready", customEvent); !ok { + t.Fatal("custom event should validate") + } + if err := cdp.validateCommandParams("Custom.echo", map[string]any{"text": 1}); err == nil { + t.Fatal("expected custom params validation to reject wrong text type") + } + if err := cdp.validateCommandResult("Custom.echo", map[string]any{"ok": "yes"}); err == nil { + t.Fatal("expected custom result validation to reject wrong ok type") + } + expectPanic(t, func() { cdp.validateEventData("Custom.ready", map[string]any{"ok": "yes"}) }) + + if _, err := cdp.Mod.AddCustomCommand(CustomCommand{ + Name: "Target.getTargets", + ResultSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "targetInfos": map[string]any{ + "type": "array", + "items": map[string]any{ + "type": "object", + "properties": map[string]any{ + "targetId": map[string]any{"type": "string"}, + "type": map[string]any{"type": "string"}, + "title": map[string]any{"type": "string"}, + "url": map[string]any{"type": "string"}, + "attached": map[string]any{"type": "boolean"}, + "canAccessOpener": map[string]any{"type": "boolean"}, + "tabId": map[string]any{"type": "integer"}, + }, + "required": []any{"targetId", "type", "title", "url", "attached", "canAccessOpener"}, + "additionalProperties": true, + }, + }, + }, + "required": []any{"targetInfos"}, + "additionalProperties": true, + }, + }); err != nil { + t.Fatal(err) + } + if _, err := cdp.Mod.AddCustomEvent(CustomEvent{Name: "Target.targetCreated"}); err != nil { + t.Fatal(err) + } + + extendedTargetInfo := map[string]any{ + "targetId": "target-1", + "type": "page", + "title": "Example", + "url": "https://example.com", + "attached": false, + "canAccessOpener": false, + "tabId": 7, + } + if err := cdp.validateCommandResult("Target.getTargets", map[string]any{"targetInfos": []any{extendedTargetInfo}}); err != nil { + t.Fatalf("extended native command result should validate: %v", err) + } + if _, ok := cdp.validateEventData("Target.targetCreated", map[string]any{"targetInfo": extendedTargetInfo}); !ok { + t.Fatal("extended native event should validate") + } +} + +func mustParamsMap(t *testing.T, value any) map[string]any { + t.Helper() + params, err := cdpParamsMap(value) + if err != nil { + t.Fatal(err) + } + return params +} diff --git a/go/modcdp/client/ModCDPClient_test.go b/go/modcdp/client/ModCDPClient_test.go index a640044..7dad078 100644 --- a/go/modcdp/client/ModCDPClient_test.go +++ b/go/modcdp/client/ModCDPClient_test.go @@ -3,9 +3,14 @@ package client import ( "context" "encoding/json" + "fmt" "os" "path/filepath" + "reflect" + "regexp" "runtime" + "sort" + "strconv" "strings" "testing" "time" @@ -167,7 +172,14 @@ func TestModCDPClientDispatchesRootEventsBeforeExtensionSessionAttached(t *testi cdp.handleEventMessage(map[string]any{ "method": "Target.targetCreated", "params": map[string]any{ - "targetInfo": map[string]any{"targetId": "target-1", "type": "page", "url": "about:blank"}, + "targetInfo": map[string]any{ + "targetId": "target-1", + "type": "page", + "title": "about:blank", + "url": "about:blank", + "attached": false, + "canAccessOpener": false, + }, }, }) @@ -195,7 +207,14 @@ func TestModCDPClientEventDispatchSnapshotsHandlersWhenOnceRemovesItself(t *test cdp.handleEventMessage(map[string]any{ "method": "Target.targetCreated", "params": map[string]any{ - "targetInfo": map[string]any{"targetId": "target-1", "type": "page", "url": "about:blank"}, + "targetInfo": map[string]any{ + "targetId": "target-1", + "type": "page", + "title": "about:blank", + "url": "about:blank", + "attached": false, + "canAccessOpener": false, + }, }, }) @@ -215,7 +234,14 @@ func TestModCDPClientEventDispatchSnapshotsHandlersWhenOnceRemovesItself(t *test cdp.handleEventMessage(map[string]any{ "method": "Target.targetCreated", "params": map[string]any{ - "targetInfo": map[string]any{"targetId": "target-2", "type": "page", "url": "about:blank"}, + "targetInfo": map[string]any{ + "targetId": "target-2", + "type": "page", + "title": "about:blank", + "url": "about:blank", + "attached": false, + "canAccessOpener": false, + }, }, }) @@ -430,6 +456,38 @@ func TestModCDPClientDefaultsLaunchedModCDPServerUpstreamsToExtensionAuto(t *tes } } +func TestModCDPClientOrdersLocalAutoInjectionAsLaunchFlagThenLoadUnpackedFallback(t *testing.T) { + cdp := New(Options{ + Launcher: LauncherConfig{LauncherMode: "local"}, + Injector: InjectorConfig{InjectorMode: "auto"}, + }) + + got := []string{} + for _, injector := range cdp.extensionInjectorsForConfig() { + switch injector.(type) { + case *LocalBrowserLaunchExtensionInjector: + got = append(got, "LocalBrowserLaunchExtensionInjector") + case *ExtensionsLoadUnpackedInjector: + got = append(got, "ExtensionsLoadUnpackedInjector") + case *DiscoveredExtensionInjector: + got = append(got, "DiscoveredExtensionInjector") + case *BorrowedExtensionInjector: + got = append(got, "BorrowedExtensionInjector") + default: + got = append(got, fmt.Sprintf("%T", injector)) + } + } + want := []string{ + "LocalBrowserLaunchExtensionInjector", + "ExtensionsLoadUnpackedInjector", + "DiscoveredExtensionInjector", + "BorrowedExtensionInjector", + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("injector order = %#v", got) + } +} + func TestModCDPClientRejectsUnknownComponentModesAtTheirOwningFactoryBoundary(t *testing.T) { cases := []struct { name string @@ -489,17 +547,21 @@ func TestModCDPClientOnlyExposesInjectorAttachAfterCDPSendIsAvailable(t *testing func TestModCDPClientConnectsWithLocalLaunchAndInjectorChain(t *testing.T) { headless := runtime.GOOS == "linux" && os.Getenv("DISPLAY") == "" - sandbox := runtime.GOOS != "linux" + extensionPath, err := filepath.Abs("../../../dist/extension") + if err != nil { + t.Fatal(err) + } cdp := New(Options{ Launcher: LauncherConfig{LauncherMode: "local", LauncherOptions: LaunchOptions{ - Headless: boolPtr(headless), - Sandbox: boolPtr(sandbox), + Headless: boolPtr(headless), + ChromeReadyTimeoutMS: 60_000, }, }, Upstream: UpstreamConfig{UpstreamMode: "ws"}, Injector: InjectorConfig{ - InjectorMode: "inject", + InjectorMode: "auto", + InjectorExtensionPath: extensionPath, InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, InjectorTrustServiceWorkerTarget: true, InjectorServiceWorkerProbeTimeoutMS: 30_000, @@ -518,7 +580,9 @@ func TestModCDPClientConnectsWithLocalLaunchAndInjectorChain(t *testing.T) { if err := cdp.Connect(); err != nil { t.Fatal(err) } - if cdp.ConnectTiming["injector_source"] != "local_launch" { + switch cdp.ConnectTiming["injector_source"] { + case "discovered", "local_launch", "extensions_load_unpacked", "borrowed": + default: t.Fatalf("injector_source = %v", cdp.ConnectTiming["injector_source"]) } if cdp.ExtensionID != DefaultModCDPExtensionID { @@ -533,6 +597,24 @@ func TestModCDPClientConnectsWithLocalLaunchAndInjectorChain(t *testing.T) { if result != "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js" { t.Fatalf("Mod.evaluate = %#v", result) } + contextsRaw, err := cdp.Mod.Evaluate(map[string]any{ + "expression": "chrome.runtime.getContexts({}).then((contexts) => contexts.map((context) => ({ type: context.contextType, url: context.documentUrl || context.origin || '' })))", + }) + if err != nil { + t.Fatal(err) + } + contexts, _ := contextsRaw.([]any) + foundOffscreen := false + for _, rawContext := range contexts { + context, _ := rawContext.(map[string]any) + if context["type"] == "OFFSCREEN_DOCUMENT" && + context["url"] == "chrome-extension://"+DefaultModCDPExtensionID+"/offscreen/keepalive.html" { + foundOffscreen = true + } + } + if !foundOffscreen { + t.Fatalf("expected offscreen keepalive context, got %#v", contextsRaw) + } directTargetRaw, err := cdp.Send("Target.createTarget", map[string]any{"url": "about:blank#direct-session-routing"}) if err != nil { t.Fatal(err) @@ -624,15 +706,17 @@ func TestModCDPClientConnectsWithLocalLaunchAndInjectorChain(t *testing.T) { func TestModCDPClientCloseDoesNotCloseRemoteBrowserItDidNotLaunch(t *testing.T) { headless := true - sandbox := false extensionPath, err := filepath.Abs("../../../dist/extension") if err != nil { t.Fatal(err) } chrome, err := NewLocalBrowserLauncher(LaunchOptions{ - Headless: &headless, - Sandbox: &sandbox, - ExtraArgs: []string{"--load-extension=" + extensionPath}, + Headless: &headless, + ChromeReadyTimeoutMS: 60_000, + // This test manually supplies --load-extension, so it intentionally uses + // the launch-flag browser path instead of relying on the client fallback. + ExecutablePath: reverseWSTestBrowserPath(t), + ExtraArgs: []string{"--load-extension=" + extensionPath}, }).Launch(LaunchOptions{}) if err != nil { t.Fatal(err) @@ -646,15 +730,18 @@ func TestModCDPClientCloseDoesNotCloseRemoteBrowserItDidNotLaunch(t *testing.T) t.Fatal(err) } defer rawConn.Close() - cdp := New(Options{ Launcher: LauncherConfig{LauncherMode: "remote"}, Upstream: UpstreamConfig{UpstreamMode: "ws", UpstreamCDPURL: chrome.CDPURL}, Injector: InjectorConfig{ - InjectorMode: "discover", - InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, - InjectorTrustServiceWorkerTarget: true, + InjectorMode: "auto", + InjectorExtensionPath: extensionPath, + InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, + InjectorTrustServiceWorkerTarget: true, + InjectorServiceWorkerReadyTimeoutMS: 30_000, + InjectorServiceWorkerProbeTimeoutMS: 30_000, }, + Client: ClientConfig{ClientRoutes: map[string]string{"*.*": "direct_cdp"}}, }) if err := cdp.Connect(); err != nil { t.Fatal(err) @@ -695,12 +782,14 @@ func TestModCDPClientCloseKeepsInjectorFilesUntilAfterLaunchedBrowserShutdown(t Launcher: LauncherConfig{LauncherMode: "local", LauncherOptions: LaunchOptions{ Headless: boolPtr(true), - Sandbox: boolPtr(false), + // After explicit CHROME_PATH and CI /usr/bin/chromium, this test uses + // Chrome for Testing because Canary rejects --load-extension in this + // local launch injector path. + ExecutablePath: reverseWSTestBrowserPath(t), }, }, Upstream: UpstreamConfig{ - UpstreamMode: "reversews", - UpstreamReverseWSWaitTimeoutMS: 30_000, + UpstreamMode: "ws", }, Injector: InjectorConfig{ InjectorMode: "auto", @@ -715,16 +804,16 @@ func TestModCDPClientCloseKeepsInjectorFilesUntilAfterLaunchedBrowserShutdown(t if err := cdp.Connect(); err != nil { t.Fatal(err) } - var localInjector *LocalBrowserLaunchExtensionInjector + var localLaunchInjector *LocalBrowserLaunchExtensionInjector for _, injector := range cdp.extensionInjectors { if typed, ok := injector.(*LocalBrowserLaunchExtensionInjector); ok { - localInjector = typed + localLaunchInjector = typed } } - if localInjector == nil { + if localLaunchInjector == nil { t.Fatal("expected LocalBrowserLaunchExtensionInjector") } - unpackedExtensionPath := localInjector.UnpackedExtensionPath + unpackedExtensionPath := localLaunchInjector.UnpackedExtensionPath if unpackedExtensionPath == extensionPath { t.Fatalf("UnpackedExtensionPath = %q", unpackedExtensionPath) } @@ -760,7 +849,6 @@ func TestModCDPClientCloseClearsTopLevelConnectionState(t *testing.T) { Launcher: LauncherConfig{LauncherMode: "local", LauncherOptions: LaunchOptions{ Headless: boolPtr(true), - Sandbox: boolPtr(false), }, }, Upstream: UpstreamConfig{UpstreamMode: "ws"}, @@ -789,6 +877,108 @@ func TestModCDPClientCloseClearsTopLevelConnectionState(t *testing.T) { } } +func reverseWSTestBrowserPath(t *testing.T) string { + t.Helper() + explicitCandidates := []string{os.Getenv("CHROME_PATH")} + if runtime.GOOS == "linux" { + explicitCandidates = append(explicitCandidates, "/usr/bin/chromium") + } + for _, candidate := range explicitCandidates { + if candidate == "" { + continue + } + if _, err := os.Stat(candidate); err == nil { + return candidate + } + } + home, err := os.UserHomeDir() + if err != nil || home == "" { + home = "." + } + localAppData := os.Getenv("LOCALAPPDATA") + if localAppData == "" { + localAppData = filepath.Join(home, "AppData", "Local") + } + var patterns []string + switch runtime.GOOS { + case "darwin": + patterns = []string{ + filepath.Join(home, "Library", "Caches", "ms-playwright", "chromium-*", "chrome-mac*", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing"), + filepath.Join(home, "Library", "Caches", "ms-playwright", "chromium-*", "chrome-mac*", "Chromium.app", "Contents", "MacOS", "Chromium"), + filepath.Join(home, "Library", "Caches", "puppeteer", "chrome", "mac*-*", "chrome-mac*", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing"), + } + case "windows": + patterns = []string{ + filepath.Join(localAppData, "ms-playwright", "chromium-*", "chrome-win*", "chrome.exe"), + filepath.Join(home, ".cache", "puppeteer", "chrome", "win*-*", "chrome.exe"), + } + default: + patterns = []string{ + filepath.Join(home, ".cache", "ms-playwright", "chromium-*", "chrome-linux*", "chrome"), + filepath.Join("/opt", "pw-browsers", "chromium-*", "chrome-linux*", "chrome"), + filepath.Join(home, ".cache", "puppeteer", "chrome", "linux-*", "chrome-linux*", "chrome"), + } + } + var candidates []string + for _, pattern := range patterns { + matches, err := filepath.Glob(pattern) + if err != nil { + continue + } + candidates = append(candidates, matches...) + } + candidates = newestChromeForTestingFirst(candidates) + if len(candidates) > 0 { + return candidates[0] + } + t.Fatal("Reversews tests require CHROME_PATH, /usr/bin/chromium, or Chrome for Testing.") + return "" +} + +func newestChromeForTestingFirst(candidates []string) []string { + seen := map[string]bool{} + deduped := make([]string, 0, len(candidates)) + for _, candidate := range candidates { + if candidate == "" || seen[candidate] { + continue + } + seen[candidate] = true + deduped = append(deduped, candidate) + } + sort.SliceStable(deduped, func(i, j int) bool { + leftVersion := maxPathNumber(deduped[i]) + rightVersion := maxPathNumber(deduped[j]) + if leftVersion != rightVersion { + return leftVersion > rightVersion + } + leftStat, leftErr := os.Stat(deduped[i]) + rightStat, rightErr := os.Stat(deduped[j]) + var leftMtime, rightMtime time.Time + if leftErr == nil { + leftMtime = leftStat.ModTime() + } + if rightErr == nil { + rightMtime = rightStat.ModTime() + } + if !leftMtime.Equal(rightMtime) { + return leftMtime.After(rightMtime) + } + return deduped[i] < deduped[j] + }) + return deduped +} + +func maxPathNumber(value string) int { + maxValue := 0 + for _, raw := range regexp.MustCompile(`\d+`).FindAllString(value, -1) { + number, err := strconv.Atoi(raw) + if err == nil && number > maxValue { + maxValue = number + } + } + return maxValue +} + func TestCustomCommandSchemasValidateParamsAndResults(t *testing.T) { cdp := New(Options{ CustomCommands: []CustomCommand{ @@ -842,9 +1032,7 @@ func TestCustomEventSchemasValidatePayloads(t *testing.T) { if _, ok := cdp.validateEventData("Custom.changed", map[string]any{"targetId": "target-1"}); !ok { t.Fatal("expected valid event payload") } - if _, ok := cdp.validateEventData("Custom.changed", map[string]any{"targetId": 1}); ok { - t.Fatal("expected invalid event payload") - } + expectPanic(t, func() { cdp.validateEventData("Custom.changed", map[string]any{"targetId": 1}) }) } func TestTypedCDPSurfaceInitializesAndEncodesParams(t *testing.T) { diff --git a/go/modcdp/client/ModCDPPayloadSchemaNormalization_test.go b/go/modcdp/client/ModCDPPayloadSchemaNormalization_test.go index b039776..b49102c 100644 --- a/go/modcdp/client/ModCDPPayloadSchemaNormalization_test.go +++ b/go/modcdp/client/ModCDPPayloadSchemaNormalization_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/jsonschema" + abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/v2/jsonschema" ) func TestPayloadSchemaNormalizationAcceptsEmptyJSONSchemaObjects(t *testing.T) { diff --git a/go/modcdp/client/generated_protocol_schemas.go b/go/modcdp/client/generated_protocol_schemas.go new file mode 100644 index 0000000..507ff76 --- /dev/null +++ b/go/modcdp/client/generated_protocol_schemas.go @@ -0,0 +1,1576 @@ +// Code generated from generated_domains.go. DO NOT EDIT BY HAND. +package client + +import abxjsonschema "github.com/ArchiveBox/abxbus/abxbus-go/v2/jsonschema" + +func (c *ModCDPClient) hydrateNativeProtocolSchemas() { + c.schemaMu.Lock() + defer c.schemaMu.Unlock() + c.commandParamsSchemas["Accessibility.disable"] = abxjsonschema.SchemaFor[AccessibilityDisableParams]() + c.commandResultSchemas["Accessibility.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityDisableResult]()) + c.commandParamsSchemas["Accessibility.enable"] = abxjsonschema.SchemaFor[AccessibilityEnableParams]() + c.commandResultSchemas["Accessibility.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityEnableResult]()) + c.commandParamsSchemas["Accessibility.getPartialAXTree"] = abxjsonschema.SchemaFor[AccessibilityGetPartialAXTreeParams]() + c.commandResultSchemas["Accessibility.getPartialAXTree"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityGetPartialAXTreeResult]()) + c.commandParamsSchemas["Accessibility.getFullAXTree"] = abxjsonschema.SchemaFor[AccessibilityGetFullAXTreeParams]() + c.commandResultSchemas["Accessibility.getFullAXTree"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityGetFullAXTreeResult]()) + c.commandParamsSchemas["Accessibility.getRootAXNode"] = abxjsonschema.SchemaFor[AccessibilityGetRootAXNodeParams]() + c.commandResultSchemas["Accessibility.getRootAXNode"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityGetRootAXNodeResult]()) + c.commandParamsSchemas["Accessibility.getAXNodeAndAncestors"] = abxjsonschema.SchemaFor[AccessibilityGetAXNodeAndAncestorsParams]() + c.commandResultSchemas["Accessibility.getAXNodeAndAncestors"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityGetAXNodeAndAncestorsResult]()) + c.commandParamsSchemas["Accessibility.getChildAXNodes"] = abxjsonschema.SchemaFor[AccessibilityGetChildAXNodesParams]() + c.commandResultSchemas["Accessibility.getChildAXNodes"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityGetChildAXNodesResult]()) + c.commandParamsSchemas["Accessibility.queryAXTree"] = abxjsonschema.SchemaFor[AccessibilityQueryAXTreeParams]() + c.commandResultSchemas["Accessibility.queryAXTree"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityQueryAXTreeResult]()) + c.commandParamsSchemas["Animation.disable"] = abxjsonschema.SchemaFor[AnimationDisableParams]() + c.commandResultSchemas["Animation.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationDisableResult]()) + c.commandParamsSchemas["Animation.enable"] = abxjsonschema.SchemaFor[AnimationEnableParams]() + c.commandResultSchemas["Animation.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationEnableResult]()) + c.commandParamsSchemas["Animation.getCurrentTime"] = abxjsonschema.SchemaFor[AnimationGetCurrentTimeParams]() + c.commandResultSchemas["Animation.getCurrentTime"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationGetCurrentTimeResult]()) + c.commandParamsSchemas["Animation.getPlaybackRate"] = abxjsonschema.SchemaFor[AnimationGetPlaybackRateParams]() + c.commandResultSchemas["Animation.getPlaybackRate"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationGetPlaybackRateResult]()) + c.commandParamsSchemas["Animation.releaseAnimations"] = abxjsonschema.SchemaFor[AnimationReleaseAnimationsParams]() + c.commandResultSchemas["Animation.releaseAnimations"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationReleaseAnimationsResult]()) + c.commandParamsSchemas["Animation.resolveAnimation"] = abxjsonschema.SchemaFor[AnimationResolveAnimationParams]() + c.commandResultSchemas["Animation.resolveAnimation"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationResolveAnimationResult]()) + c.commandParamsSchemas["Animation.seekAnimations"] = abxjsonschema.SchemaFor[AnimationSeekAnimationsParams]() + c.commandResultSchemas["Animation.seekAnimations"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationSeekAnimationsResult]()) + c.commandParamsSchemas["Animation.setPaused"] = abxjsonschema.SchemaFor[AnimationSetPausedParams]() + c.commandResultSchemas["Animation.setPaused"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationSetPausedResult]()) + c.commandParamsSchemas["Animation.setPlaybackRate"] = abxjsonschema.SchemaFor[AnimationSetPlaybackRateParams]() + c.commandResultSchemas["Animation.setPlaybackRate"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationSetPlaybackRateResult]()) + c.commandParamsSchemas["Animation.setTiming"] = abxjsonschema.SchemaFor[AnimationSetTimingParams]() + c.commandResultSchemas["Animation.setTiming"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationSetTimingResult]()) + c.commandParamsSchemas["Audits.getEncodedResponse"] = abxjsonschema.SchemaFor[AuditsGetEncodedResponseParams]() + c.commandResultSchemas["Audits.getEncodedResponse"] = nativeResultSchema(abxjsonschema.SchemaFor[AuditsGetEncodedResponseResult]()) + c.commandParamsSchemas["Audits.disable"] = abxjsonschema.SchemaFor[AuditsDisableParams]() + c.commandResultSchemas["Audits.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[AuditsDisableResult]()) + c.commandParamsSchemas["Audits.enable"] = abxjsonschema.SchemaFor[AuditsEnableParams]() + c.commandResultSchemas["Audits.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[AuditsEnableResult]()) + c.commandParamsSchemas["Audits.checkFormsIssues"] = abxjsonschema.SchemaFor[AuditsCheckFormsIssuesParams]() + c.commandResultSchemas["Audits.checkFormsIssues"] = nativeResultSchema(abxjsonschema.SchemaFor[AuditsCheckFormsIssuesResult]()) + c.commandParamsSchemas["Autofill.trigger"] = abxjsonschema.SchemaFor[AutofillTriggerParams]() + c.commandResultSchemas["Autofill.trigger"] = nativeResultSchema(abxjsonschema.SchemaFor[AutofillTriggerResult]()) + c.commandParamsSchemas["Autofill.setAddresses"] = abxjsonschema.SchemaFor[AutofillSetAddressesParams]() + c.commandResultSchemas["Autofill.setAddresses"] = nativeResultSchema(abxjsonschema.SchemaFor[AutofillSetAddressesResult]()) + c.commandParamsSchemas["Autofill.disable"] = abxjsonschema.SchemaFor[AutofillDisableParams]() + c.commandResultSchemas["Autofill.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[AutofillDisableResult]()) + c.commandParamsSchemas["Autofill.enable"] = abxjsonschema.SchemaFor[AutofillEnableParams]() + c.commandResultSchemas["Autofill.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[AutofillEnableResult]()) + c.commandParamsSchemas["BackgroundService.startObserving"] = abxjsonschema.SchemaFor[BackgroundServiceStartObservingParams]() + c.commandResultSchemas["BackgroundService.startObserving"] = nativeResultSchema(abxjsonschema.SchemaFor[BackgroundServiceStartObservingResult]()) + c.commandParamsSchemas["BackgroundService.stopObserving"] = abxjsonschema.SchemaFor[BackgroundServiceStopObservingParams]() + c.commandResultSchemas["BackgroundService.stopObserving"] = nativeResultSchema(abxjsonschema.SchemaFor[BackgroundServiceStopObservingResult]()) + c.commandParamsSchemas["BackgroundService.setRecording"] = abxjsonschema.SchemaFor[BackgroundServiceSetRecordingParams]() + c.commandResultSchemas["BackgroundService.setRecording"] = nativeResultSchema(abxjsonschema.SchemaFor[BackgroundServiceSetRecordingResult]()) + c.commandParamsSchemas["BackgroundService.clearEvents"] = abxjsonschema.SchemaFor[BackgroundServiceClearEventsParams]() + c.commandResultSchemas["BackgroundService.clearEvents"] = nativeResultSchema(abxjsonschema.SchemaFor[BackgroundServiceClearEventsResult]()) + c.commandParamsSchemas["BluetoothEmulation.enable"] = abxjsonschema.SchemaFor[BluetoothEmulationEnableParams]() + c.commandResultSchemas["BluetoothEmulation.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationEnableResult]()) + c.commandParamsSchemas["BluetoothEmulation.setSimulatedCentralState"] = abxjsonschema.SchemaFor[BluetoothEmulationSetSimulatedCentralStateParams]() + c.commandResultSchemas["BluetoothEmulation.setSimulatedCentralState"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationSetSimulatedCentralStateResult]()) + c.commandParamsSchemas["BluetoothEmulation.disable"] = abxjsonschema.SchemaFor[BluetoothEmulationDisableParams]() + c.commandResultSchemas["BluetoothEmulation.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationDisableResult]()) + c.commandParamsSchemas["BluetoothEmulation.simulatePreconnectedPeripheral"] = abxjsonschema.SchemaFor[BluetoothEmulationSimulatePreconnectedPeripheralParams]() + c.commandResultSchemas["BluetoothEmulation.simulatePreconnectedPeripheral"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationSimulatePreconnectedPeripheralResult]()) + c.commandParamsSchemas["BluetoothEmulation.simulateAdvertisement"] = abxjsonschema.SchemaFor[BluetoothEmulationSimulateAdvertisementParams]() + c.commandResultSchemas["BluetoothEmulation.simulateAdvertisement"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationSimulateAdvertisementResult]()) + c.commandParamsSchemas["BluetoothEmulation.simulateGATTOperationResponse"] = abxjsonschema.SchemaFor[BluetoothEmulationSimulateGATTOperationResponseParams]() + c.commandResultSchemas["BluetoothEmulation.simulateGATTOperationResponse"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationSimulateGATTOperationResponseResult]()) + c.commandParamsSchemas["BluetoothEmulation.simulateCharacteristicOperationResponse"] = abxjsonschema.SchemaFor[BluetoothEmulationSimulateCharacteristicOperationResponseParams]() + c.commandResultSchemas["BluetoothEmulation.simulateCharacteristicOperationResponse"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationSimulateCharacteristicOperationResponseResult]()) + c.commandParamsSchemas["BluetoothEmulation.simulateDescriptorOperationResponse"] = abxjsonschema.SchemaFor[BluetoothEmulationSimulateDescriptorOperationResponseParams]() + c.commandResultSchemas["BluetoothEmulation.simulateDescriptorOperationResponse"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationSimulateDescriptorOperationResponseResult]()) + c.commandParamsSchemas["BluetoothEmulation.addService"] = abxjsonschema.SchemaFor[BluetoothEmulationAddServiceParams]() + c.commandResultSchemas["BluetoothEmulation.addService"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationAddServiceResult]()) + c.commandParamsSchemas["BluetoothEmulation.removeService"] = abxjsonschema.SchemaFor[BluetoothEmulationRemoveServiceParams]() + c.commandResultSchemas["BluetoothEmulation.removeService"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationRemoveServiceResult]()) + c.commandParamsSchemas["BluetoothEmulation.addCharacteristic"] = abxjsonschema.SchemaFor[BluetoothEmulationAddCharacteristicParams]() + c.commandResultSchemas["BluetoothEmulation.addCharacteristic"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationAddCharacteristicResult]()) + c.commandParamsSchemas["BluetoothEmulation.removeCharacteristic"] = abxjsonschema.SchemaFor[BluetoothEmulationRemoveCharacteristicParams]() + c.commandResultSchemas["BluetoothEmulation.removeCharacteristic"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationRemoveCharacteristicResult]()) + c.commandParamsSchemas["BluetoothEmulation.addDescriptor"] = abxjsonschema.SchemaFor[BluetoothEmulationAddDescriptorParams]() + c.commandResultSchemas["BluetoothEmulation.addDescriptor"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationAddDescriptorResult]()) + c.commandParamsSchemas["BluetoothEmulation.removeDescriptor"] = abxjsonschema.SchemaFor[BluetoothEmulationRemoveDescriptorParams]() + c.commandResultSchemas["BluetoothEmulation.removeDescriptor"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationRemoveDescriptorResult]()) + c.commandParamsSchemas["BluetoothEmulation.simulateGATTDisconnection"] = abxjsonschema.SchemaFor[BluetoothEmulationSimulateGATTDisconnectionParams]() + c.commandResultSchemas["BluetoothEmulation.simulateGATTDisconnection"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationSimulateGATTDisconnectionResult]()) + c.commandParamsSchemas["Browser.setPermission"] = abxjsonschema.SchemaFor[BrowserSetPermissionParams]() + c.commandResultSchemas["Browser.setPermission"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserSetPermissionResult]()) + c.commandParamsSchemas["Browser.grantPermissions"] = abxjsonschema.SchemaFor[BrowserGrantPermissionsParams]() + c.commandResultSchemas["Browser.grantPermissions"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserGrantPermissionsResult]()) + c.commandParamsSchemas["Browser.resetPermissions"] = abxjsonschema.SchemaFor[BrowserResetPermissionsParams]() + c.commandResultSchemas["Browser.resetPermissions"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserResetPermissionsResult]()) + c.commandParamsSchemas["Browser.setDownloadBehavior"] = abxjsonschema.SchemaFor[BrowserSetDownloadBehaviorParams]() + c.commandResultSchemas["Browser.setDownloadBehavior"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserSetDownloadBehaviorResult]()) + c.commandParamsSchemas["Browser.cancelDownload"] = abxjsonschema.SchemaFor[BrowserCancelDownloadParams]() + c.commandResultSchemas["Browser.cancelDownload"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserCancelDownloadResult]()) + c.commandParamsSchemas["Browser.close"] = abxjsonschema.SchemaFor[BrowserCloseParams]() + c.commandResultSchemas["Browser.close"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserCloseResult]()) + c.commandParamsSchemas["Browser.crash"] = abxjsonschema.SchemaFor[BrowserCrashParams]() + c.commandResultSchemas["Browser.crash"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserCrashResult]()) + c.commandParamsSchemas["Browser.crashGpuProcess"] = abxjsonschema.SchemaFor[BrowserCrashGPUProcessParams]() + c.commandResultSchemas["Browser.crashGpuProcess"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserCrashGPUProcessResult]()) + c.commandParamsSchemas["Browser.getVersion"] = abxjsonschema.SchemaFor[BrowserGetVersionParams]() + c.commandResultSchemas["Browser.getVersion"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserGetVersionResult]()) + c.commandParamsSchemas["Browser.getBrowserCommandLine"] = abxjsonschema.SchemaFor[BrowserGetBrowserCommandLineParams]() + c.commandResultSchemas["Browser.getBrowserCommandLine"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserGetBrowserCommandLineResult]()) + c.commandParamsSchemas["Browser.getHistograms"] = abxjsonschema.SchemaFor[BrowserGetHistogramsParams]() + c.commandResultSchemas["Browser.getHistograms"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserGetHistogramsResult]()) + c.commandParamsSchemas["Browser.getHistogram"] = abxjsonschema.SchemaFor[BrowserGetHistogramParams]() + c.commandResultSchemas["Browser.getHistogram"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserGetHistogramResult]()) + c.commandParamsSchemas["Browser.getWindowBounds"] = abxjsonschema.SchemaFor[BrowserGetWindowBoundsParams]() + c.commandResultSchemas["Browser.getWindowBounds"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserGetWindowBoundsResult]()) + c.commandParamsSchemas["Browser.getWindowForTarget"] = abxjsonschema.SchemaFor[BrowserGetWindowForTargetParams]() + c.commandResultSchemas["Browser.getWindowForTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserGetWindowForTargetResult]()) + c.commandParamsSchemas["Browser.setWindowBounds"] = abxjsonschema.SchemaFor[BrowserSetWindowBoundsParams]() + c.commandResultSchemas["Browser.setWindowBounds"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserSetWindowBoundsResult]()) + c.commandParamsSchemas["Browser.setContentsSize"] = abxjsonschema.SchemaFor[BrowserSetContentsSizeParams]() + c.commandResultSchemas["Browser.setContentsSize"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserSetContentsSizeResult]()) + c.commandParamsSchemas["Browser.setDockTile"] = abxjsonschema.SchemaFor[BrowserSetDockTileParams]() + c.commandResultSchemas["Browser.setDockTile"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserSetDockTileResult]()) + c.commandParamsSchemas["Browser.executeBrowserCommand"] = abxjsonschema.SchemaFor[BrowserExecuteBrowserCommandParams]() + c.commandResultSchemas["Browser.executeBrowserCommand"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserExecuteBrowserCommandResult]()) + c.commandParamsSchemas["Browser.addPrivacySandboxEnrollmentOverride"] = abxjsonschema.SchemaFor[BrowserAddPrivacySandboxEnrollmentOverrideParams]() + c.commandResultSchemas["Browser.addPrivacySandboxEnrollmentOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserAddPrivacySandboxEnrollmentOverrideResult]()) + c.commandParamsSchemas["Browser.addPrivacySandboxCoordinatorKeyConfig"] = abxjsonschema.SchemaFor[BrowserAddPrivacySandboxCoordinatorKeyConfigParams]() + c.commandResultSchemas["Browser.addPrivacySandboxCoordinatorKeyConfig"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserAddPrivacySandboxCoordinatorKeyConfigResult]()) + c.commandParamsSchemas["CSS.addRule"] = abxjsonschema.SchemaFor[CSSAddRuleParams]() + c.commandResultSchemas["CSS.addRule"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSAddRuleResult]()) + c.commandParamsSchemas["CSS.collectClassNames"] = abxjsonschema.SchemaFor[CSSCollectClassNamesParams]() + c.commandResultSchemas["CSS.collectClassNames"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSCollectClassNamesResult]()) + c.commandParamsSchemas["CSS.createStyleSheet"] = abxjsonschema.SchemaFor[CSSCreateStyleSheetParams]() + c.commandResultSchemas["CSS.createStyleSheet"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSCreateStyleSheetResult]()) + c.commandParamsSchemas["CSS.disable"] = abxjsonschema.SchemaFor[CSSDisableParams]() + c.commandResultSchemas["CSS.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSDisableResult]()) + c.commandParamsSchemas["CSS.enable"] = abxjsonschema.SchemaFor[CSSEnableParams]() + c.commandResultSchemas["CSS.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSEnableResult]()) + c.commandParamsSchemas["CSS.forcePseudoState"] = abxjsonschema.SchemaFor[CSSForcePseudoStateParams]() + c.commandResultSchemas["CSS.forcePseudoState"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSForcePseudoStateResult]()) + c.commandParamsSchemas["CSS.forceStartingStyle"] = abxjsonschema.SchemaFor[CSSForceStartingStyleParams]() + c.commandResultSchemas["CSS.forceStartingStyle"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSForceStartingStyleResult]()) + c.commandParamsSchemas["CSS.getBackgroundColors"] = abxjsonschema.SchemaFor[CSSGetBackgroundColorsParams]() + c.commandResultSchemas["CSS.getBackgroundColors"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetBackgroundColorsResult]()) + c.commandParamsSchemas["CSS.getComputedStyleForNode"] = abxjsonschema.SchemaFor[CSSGetComputedStyleForNodeParams]() + c.commandResultSchemas["CSS.getComputedStyleForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetComputedStyleForNodeResult]()) + c.commandParamsSchemas["CSS.resolveValues"] = abxjsonschema.SchemaFor[CSSResolveValuesParams]() + c.commandResultSchemas["CSS.resolveValues"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSResolveValuesResult]()) + c.commandParamsSchemas["CSS.getLonghandProperties"] = abxjsonschema.SchemaFor[CSSGetLonghandPropertiesParams]() + c.commandResultSchemas["CSS.getLonghandProperties"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetLonghandPropertiesResult]()) + c.commandParamsSchemas["CSS.getInlineStylesForNode"] = abxjsonschema.SchemaFor[CSSGetInlineStylesForNodeParams]() + c.commandResultSchemas["CSS.getInlineStylesForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetInlineStylesForNodeResult]()) + c.commandParamsSchemas["CSS.getAnimatedStylesForNode"] = abxjsonschema.SchemaFor[CSSGetAnimatedStylesForNodeParams]() + c.commandResultSchemas["CSS.getAnimatedStylesForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetAnimatedStylesForNodeResult]()) + c.commandParamsSchemas["CSS.getMatchedStylesForNode"] = abxjsonschema.SchemaFor[CSSGetMatchedStylesForNodeParams]() + c.commandResultSchemas["CSS.getMatchedStylesForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetMatchedStylesForNodeResult]()) + c.commandParamsSchemas["CSS.getEnvironmentVariables"] = abxjsonschema.SchemaFor[CSSGetEnvironmentVariablesParams]() + c.commandResultSchemas["CSS.getEnvironmentVariables"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetEnvironmentVariablesResult]()) + c.commandParamsSchemas["CSS.getMediaQueries"] = abxjsonschema.SchemaFor[CSSGetMediaQueriesParams]() + c.commandResultSchemas["CSS.getMediaQueries"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetMediaQueriesResult]()) + c.commandParamsSchemas["CSS.getPlatformFontsForNode"] = abxjsonschema.SchemaFor[CSSGetPlatformFontsForNodeParams]() + c.commandResultSchemas["CSS.getPlatformFontsForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetPlatformFontsForNodeResult]()) + c.commandParamsSchemas["CSS.getStyleSheetText"] = abxjsonschema.SchemaFor[CSSGetStyleSheetTextParams]() + c.commandResultSchemas["CSS.getStyleSheetText"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetStyleSheetTextResult]()) + c.commandParamsSchemas["CSS.getLayersForNode"] = abxjsonschema.SchemaFor[CSSGetLayersForNodeParams]() + c.commandResultSchemas["CSS.getLayersForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetLayersForNodeResult]()) + c.commandParamsSchemas["CSS.getLocationForSelector"] = abxjsonschema.SchemaFor[CSSGetLocationForSelectorParams]() + c.commandResultSchemas["CSS.getLocationForSelector"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSGetLocationForSelectorResult]()) + c.commandParamsSchemas["CSS.trackComputedStyleUpdatesForNode"] = abxjsonschema.SchemaFor[CSSTrackComputedStyleUpdatesForNodeParams]() + c.commandResultSchemas["CSS.trackComputedStyleUpdatesForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSTrackComputedStyleUpdatesForNodeResult]()) + c.commandParamsSchemas["CSS.trackComputedStyleUpdates"] = abxjsonschema.SchemaFor[CSSTrackComputedStyleUpdatesParams]() + c.commandResultSchemas["CSS.trackComputedStyleUpdates"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSTrackComputedStyleUpdatesResult]()) + c.commandParamsSchemas["CSS.takeComputedStyleUpdates"] = abxjsonschema.SchemaFor[CSSTakeComputedStyleUpdatesParams]() + c.commandResultSchemas["CSS.takeComputedStyleUpdates"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSTakeComputedStyleUpdatesResult]()) + c.commandParamsSchemas["CSS.setEffectivePropertyValueForNode"] = abxjsonschema.SchemaFor[CSSSetEffectivePropertyValueForNodeParams]() + c.commandResultSchemas["CSS.setEffectivePropertyValueForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetEffectivePropertyValueForNodeResult]()) + c.commandParamsSchemas["CSS.setPropertyRulePropertyName"] = abxjsonschema.SchemaFor[CSSSetPropertyRulePropertyNameParams]() + c.commandResultSchemas["CSS.setPropertyRulePropertyName"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetPropertyRulePropertyNameResult]()) + c.commandParamsSchemas["CSS.setKeyframeKey"] = abxjsonschema.SchemaFor[CSSSetKeyframeKeyParams]() + c.commandResultSchemas["CSS.setKeyframeKey"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetKeyframeKeyResult]()) + c.commandParamsSchemas["CSS.setMediaText"] = abxjsonschema.SchemaFor[CSSSetMediaTextParams]() + c.commandResultSchemas["CSS.setMediaText"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetMediaTextResult]()) + c.commandParamsSchemas["CSS.setContainerQueryText"] = abxjsonschema.SchemaFor[CSSSetContainerQueryTextParams]() + c.commandResultSchemas["CSS.setContainerQueryText"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetContainerQueryTextResult]()) + c.commandParamsSchemas["CSS.setSupportsText"] = abxjsonschema.SchemaFor[CSSSetSupportsTextParams]() + c.commandResultSchemas["CSS.setSupportsText"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetSupportsTextResult]()) + c.commandParamsSchemas["CSS.setNavigationText"] = abxjsonschema.SchemaFor[CSSSetNavigationTextParams]() + c.commandResultSchemas["CSS.setNavigationText"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetNavigationTextResult]()) + c.commandParamsSchemas["CSS.setScopeText"] = abxjsonschema.SchemaFor[CSSSetScopeTextParams]() + c.commandResultSchemas["CSS.setScopeText"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetScopeTextResult]()) + c.commandParamsSchemas["CSS.setRuleSelector"] = abxjsonschema.SchemaFor[CSSSetRuleSelectorParams]() + c.commandResultSchemas["CSS.setRuleSelector"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetRuleSelectorResult]()) + c.commandParamsSchemas["CSS.setStyleSheetText"] = abxjsonschema.SchemaFor[CSSSetStyleSheetTextParams]() + c.commandResultSchemas["CSS.setStyleSheetText"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetStyleSheetTextResult]()) + c.commandParamsSchemas["CSS.setStyleTexts"] = abxjsonschema.SchemaFor[CSSSetStyleTextsParams]() + c.commandResultSchemas["CSS.setStyleTexts"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetStyleTextsResult]()) + c.commandParamsSchemas["CSS.startRuleUsageTracking"] = abxjsonschema.SchemaFor[CSSStartRuleUsageTrackingParams]() + c.commandResultSchemas["CSS.startRuleUsageTracking"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSStartRuleUsageTrackingResult]()) + c.commandParamsSchemas["CSS.stopRuleUsageTracking"] = abxjsonschema.SchemaFor[CSSStopRuleUsageTrackingParams]() + c.commandResultSchemas["CSS.stopRuleUsageTracking"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSStopRuleUsageTrackingResult]()) + c.commandParamsSchemas["CSS.takeCoverageDelta"] = abxjsonschema.SchemaFor[CSSTakeCoverageDeltaParams]() + c.commandResultSchemas["CSS.takeCoverageDelta"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSTakeCoverageDeltaResult]()) + c.commandParamsSchemas["CSS.setLocalFontsEnabled"] = abxjsonschema.SchemaFor[CSSSetLocalFontsEnabledParams]() + c.commandResultSchemas["CSS.setLocalFontsEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSSetLocalFontsEnabledResult]()) + c.commandParamsSchemas["CacheStorage.deleteCache"] = abxjsonschema.SchemaFor[CacheStorageDeleteCacheParams]() + c.commandResultSchemas["CacheStorage.deleteCache"] = nativeResultSchema(abxjsonschema.SchemaFor[CacheStorageDeleteCacheResult]()) + c.commandParamsSchemas["CacheStorage.deleteEntry"] = abxjsonschema.SchemaFor[CacheStorageDeleteEntryParams]() + c.commandResultSchemas["CacheStorage.deleteEntry"] = nativeResultSchema(abxjsonschema.SchemaFor[CacheStorageDeleteEntryResult]()) + c.commandParamsSchemas["CacheStorage.requestCacheNames"] = abxjsonschema.SchemaFor[CacheStorageRequestCacheNamesParams]() + c.commandResultSchemas["CacheStorage.requestCacheNames"] = nativeResultSchema(abxjsonschema.SchemaFor[CacheStorageRequestCacheNamesResult]()) + c.commandParamsSchemas["CacheStorage.requestCachedResponse"] = abxjsonschema.SchemaFor[CacheStorageRequestCachedResponseParams]() + c.commandResultSchemas["CacheStorage.requestCachedResponse"] = nativeResultSchema(abxjsonschema.SchemaFor[CacheStorageRequestCachedResponseResult]()) + c.commandParamsSchemas["CacheStorage.requestEntries"] = abxjsonschema.SchemaFor[CacheStorageRequestEntriesParams]() + c.commandResultSchemas["CacheStorage.requestEntries"] = nativeResultSchema(abxjsonschema.SchemaFor[CacheStorageRequestEntriesResult]()) + c.commandParamsSchemas["Cast.enable"] = abxjsonschema.SchemaFor[CastEnableParams]() + c.commandResultSchemas["Cast.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[CastEnableResult]()) + c.commandParamsSchemas["Cast.disable"] = abxjsonschema.SchemaFor[CastDisableParams]() + c.commandResultSchemas["Cast.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[CastDisableResult]()) + c.commandParamsSchemas["Cast.setSinkToUse"] = abxjsonschema.SchemaFor[CastSetSinkToUseParams]() + c.commandResultSchemas["Cast.setSinkToUse"] = nativeResultSchema(abxjsonschema.SchemaFor[CastSetSinkToUseResult]()) + c.commandParamsSchemas["Cast.startDesktopMirroring"] = abxjsonschema.SchemaFor[CastStartDesktopMirroringParams]() + c.commandResultSchemas["Cast.startDesktopMirroring"] = nativeResultSchema(abxjsonschema.SchemaFor[CastStartDesktopMirroringResult]()) + c.commandParamsSchemas["Cast.startTabMirroring"] = abxjsonschema.SchemaFor[CastStartTabMirroringParams]() + c.commandResultSchemas["Cast.startTabMirroring"] = nativeResultSchema(abxjsonschema.SchemaFor[CastStartTabMirroringResult]()) + c.commandParamsSchemas["Cast.stopCasting"] = abxjsonschema.SchemaFor[CastStopCastingParams]() + c.commandResultSchemas["Cast.stopCasting"] = nativeResultSchema(abxjsonschema.SchemaFor[CastStopCastingResult]()) + c.commandParamsSchemas["Console.clearMessages"] = abxjsonschema.SchemaFor[ConsoleClearMessagesParams]() + c.commandResultSchemas["Console.clearMessages"] = nativeResultSchema(abxjsonschema.SchemaFor[ConsoleClearMessagesResult]()) + c.commandParamsSchemas["Console.disable"] = abxjsonschema.SchemaFor[ConsoleDisableParams]() + c.commandResultSchemas["Console.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[ConsoleDisableResult]()) + c.commandParamsSchemas["Console.enable"] = abxjsonschema.SchemaFor[ConsoleEnableParams]() + c.commandResultSchemas["Console.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[ConsoleEnableResult]()) + c.commandParamsSchemas["DOM.collectClassNamesFromSubtree"] = abxjsonschema.SchemaFor[DOMCollectClassNamesFromSubtreeParams]() + c.commandResultSchemas["DOM.collectClassNamesFromSubtree"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMCollectClassNamesFromSubtreeResult]()) + c.commandParamsSchemas["DOM.copyTo"] = abxjsonschema.SchemaFor[DOMCopyToParams]() + c.commandResultSchemas["DOM.copyTo"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMCopyToResult]()) + c.commandParamsSchemas["DOM.describeNode"] = abxjsonschema.SchemaFor[DOMDescribeNodeParams]() + c.commandResultSchemas["DOM.describeNode"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDescribeNodeResult]()) + c.commandParamsSchemas["DOM.scrollIntoViewIfNeeded"] = abxjsonschema.SchemaFor[DOMScrollIntoViewIfNeededParams]() + c.commandResultSchemas["DOM.scrollIntoViewIfNeeded"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMScrollIntoViewIfNeededResult]()) + c.commandParamsSchemas["DOM.disable"] = abxjsonschema.SchemaFor[DOMDisableParams]() + c.commandResultSchemas["DOM.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDisableResult]()) + c.commandParamsSchemas["DOM.discardSearchResults"] = abxjsonschema.SchemaFor[DOMDiscardSearchResultsParams]() + c.commandResultSchemas["DOM.discardSearchResults"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDiscardSearchResultsResult]()) + c.commandParamsSchemas["DOM.enable"] = abxjsonschema.SchemaFor[DOMEnableParams]() + c.commandResultSchemas["DOM.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMEnableResult]()) + c.commandParamsSchemas["DOM.focus"] = abxjsonschema.SchemaFor[DOMFocusParams]() + c.commandResultSchemas["DOM.focus"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMFocusResult]()) + c.commandParamsSchemas["DOM.getAttributes"] = abxjsonschema.SchemaFor[DOMGetAttributesParams]() + c.commandResultSchemas["DOM.getAttributes"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetAttributesResult]()) + c.commandParamsSchemas["DOM.getBoxModel"] = abxjsonschema.SchemaFor[DOMGetBoxModelParams]() + c.commandResultSchemas["DOM.getBoxModel"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetBoxModelResult]()) + c.commandParamsSchemas["DOM.getContentQuads"] = abxjsonschema.SchemaFor[DOMGetContentQuadsParams]() + c.commandResultSchemas["DOM.getContentQuads"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetContentQuadsResult]()) + c.commandParamsSchemas["DOM.getDocument"] = abxjsonschema.SchemaFor[DOMGetDocumentParams]() + c.commandResultSchemas["DOM.getDocument"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetDocumentResult]()) + c.commandParamsSchemas["DOM.getFlattenedDocument"] = abxjsonschema.SchemaFor[DOMGetFlattenedDocumentParams]() + c.commandResultSchemas["DOM.getFlattenedDocument"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetFlattenedDocumentResult]()) + c.commandParamsSchemas["DOM.getNodesForSubtreeByStyle"] = abxjsonschema.SchemaFor[DOMGetNodesForSubtreeByStyleParams]() + c.commandResultSchemas["DOM.getNodesForSubtreeByStyle"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetNodesForSubtreeByStyleResult]()) + c.commandParamsSchemas["DOM.getNodeForLocation"] = abxjsonschema.SchemaFor[DOMGetNodeForLocationParams]() + c.commandResultSchemas["DOM.getNodeForLocation"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetNodeForLocationResult]()) + c.commandParamsSchemas["DOM.getOuterHTML"] = abxjsonschema.SchemaFor[DOMGetOuterHTMLParams]() + c.commandResultSchemas["DOM.getOuterHTML"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetOuterHTMLResult]()) + c.commandParamsSchemas["DOM.getRelayoutBoundary"] = abxjsonschema.SchemaFor[DOMGetRelayoutBoundaryParams]() + c.commandResultSchemas["DOM.getRelayoutBoundary"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetRelayoutBoundaryResult]()) + c.commandParamsSchemas["DOM.getSearchResults"] = abxjsonschema.SchemaFor[DOMGetSearchResultsParams]() + c.commandResultSchemas["DOM.getSearchResults"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetSearchResultsResult]()) + c.commandParamsSchemas["DOM.hideHighlight"] = abxjsonschema.SchemaFor[DOMHideHighlightParams]() + c.commandResultSchemas["DOM.hideHighlight"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMHideHighlightResult]()) + c.commandParamsSchemas["DOM.highlightNode"] = abxjsonschema.SchemaFor[DOMHighlightNodeParams]() + c.commandResultSchemas["DOM.highlightNode"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMHighlightNodeResult]()) + c.commandParamsSchemas["DOM.highlightRect"] = abxjsonschema.SchemaFor[DOMHighlightRectParams]() + c.commandResultSchemas["DOM.highlightRect"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMHighlightRectResult]()) + c.commandParamsSchemas["DOM.markUndoableState"] = abxjsonschema.SchemaFor[DOMMarkUndoableStateParams]() + c.commandResultSchemas["DOM.markUndoableState"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMMarkUndoableStateResult]()) + c.commandParamsSchemas["DOM.moveTo"] = abxjsonschema.SchemaFor[DOMMoveToParams]() + c.commandResultSchemas["DOM.moveTo"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMMoveToResult]()) + c.commandParamsSchemas["DOM.performSearch"] = abxjsonschema.SchemaFor[DOMPerformSearchParams]() + c.commandResultSchemas["DOM.performSearch"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMPerformSearchResult]()) + c.commandParamsSchemas["DOM.pushNodeByPathToFrontend"] = abxjsonschema.SchemaFor[DOMPushNodeByPathToFrontendParams]() + c.commandResultSchemas["DOM.pushNodeByPathToFrontend"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMPushNodeByPathToFrontendResult]()) + c.commandParamsSchemas["DOM.pushNodesByBackendIdsToFrontend"] = abxjsonschema.SchemaFor[DOMPushNodesByBackendIdsToFrontendParams]() + c.commandResultSchemas["DOM.pushNodesByBackendIdsToFrontend"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMPushNodesByBackendIdsToFrontendResult]()) + c.commandParamsSchemas["DOM.querySelector"] = abxjsonschema.SchemaFor[DOMQuerySelectorParams]() + c.commandResultSchemas["DOM.querySelector"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMQuerySelectorResult]()) + c.commandParamsSchemas["DOM.querySelectorAll"] = abxjsonschema.SchemaFor[DOMQuerySelectorAllParams]() + c.commandResultSchemas["DOM.querySelectorAll"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMQuerySelectorAllResult]()) + c.commandParamsSchemas["DOM.getTopLayerElements"] = abxjsonschema.SchemaFor[DOMGetTopLayerElementsParams]() + c.commandResultSchemas["DOM.getTopLayerElements"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetTopLayerElementsResult]()) + c.commandParamsSchemas["DOM.getElementByRelation"] = abxjsonschema.SchemaFor[DOMGetElementByRelationParams]() + c.commandResultSchemas["DOM.getElementByRelation"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetElementByRelationResult]()) + c.commandParamsSchemas["DOM.redo"] = abxjsonschema.SchemaFor[DOMRedoParams]() + c.commandResultSchemas["DOM.redo"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMRedoResult]()) + c.commandParamsSchemas["DOM.removeAttribute"] = abxjsonschema.SchemaFor[DOMRemoveAttributeParams]() + c.commandResultSchemas["DOM.removeAttribute"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMRemoveAttributeResult]()) + c.commandParamsSchemas["DOM.removeNode"] = abxjsonschema.SchemaFor[DOMRemoveNodeParams]() + c.commandResultSchemas["DOM.removeNode"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMRemoveNodeResult]()) + c.commandParamsSchemas["DOM.requestChildNodes"] = abxjsonschema.SchemaFor[DOMRequestChildNodesParams]() + c.commandResultSchemas["DOM.requestChildNodes"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMRequestChildNodesResult]()) + c.commandParamsSchemas["DOM.requestNode"] = abxjsonschema.SchemaFor[DOMRequestNodeParams]() + c.commandResultSchemas["DOM.requestNode"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMRequestNodeResult]()) + c.commandParamsSchemas["DOM.resolveNode"] = abxjsonschema.SchemaFor[DOMResolveNodeParams]() + c.commandResultSchemas["DOM.resolveNode"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMResolveNodeResult]()) + c.commandParamsSchemas["DOM.setAttributeValue"] = abxjsonschema.SchemaFor[DOMSetAttributeValueParams]() + c.commandResultSchemas["DOM.setAttributeValue"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetAttributeValueResult]()) + c.commandParamsSchemas["DOM.setAttributesAsText"] = abxjsonschema.SchemaFor[DOMSetAttributesAsTextParams]() + c.commandResultSchemas["DOM.setAttributesAsText"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetAttributesAsTextResult]()) + c.commandParamsSchemas["DOM.setFileInputFiles"] = abxjsonschema.SchemaFor[DOMSetFileInputFilesParams]() + c.commandResultSchemas["DOM.setFileInputFiles"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetFileInputFilesResult]()) + c.commandParamsSchemas["DOM.setNodeStackTracesEnabled"] = abxjsonschema.SchemaFor[DOMSetNodeStackTracesEnabledParams]() + c.commandResultSchemas["DOM.setNodeStackTracesEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetNodeStackTracesEnabledResult]()) + c.commandParamsSchemas["DOM.getNodeStackTraces"] = abxjsonschema.SchemaFor[DOMGetNodeStackTracesParams]() + c.commandResultSchemas["DOM.getNodeStackTraces"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetNodeStackTracesResult]()) + c.commandParamsSchemas["DOM.getFileInfo"] = abxjsonschema.SchemaFor[DOMGetFileInfoParams]() + c.commandResultSchemas["DOM.getFileInfo"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetFileInfoResult]()) + c.commandParamsSchemas["DOM.getDetachedDomNodes"] = abxjsonschema.SchemaFor[DOMGetDetachedDOMNodesParams]() + c.commandResultSchemas["DOM.getDetachedDomNodes"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetDetachedDOMNodesResult]()) + c.commandParamsSchemas["DOM.setInspectedNode"] = abxjsonschema.SchemaFor[DOMSetInspectedNodeParams]() + c.commandResultSchemas["DOM.setInspectedNode"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetInspectedNodeResult]()) + c.commandParamsSchemas["DOM.setNodeName"] = abxjsonschema.SchemaFor[DOMSetNodeNameParams]() + c.commandResultSchemas["DOM.setNodeName"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetNodeNameResult]()) + c.commandParamsSchemas["DOM.setNodeValue"] = abxjsonschema.SchemaFor[DOMSetNodeValueParams]() + c.commandResultSchemas["DOM.setNodeValue"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetNodeValueResult]()) + c.commandParamsSchemas["DOM.setOuterHTML"] = abxjsonschema.SchemaFor[DOMSetOuterHTMLParams]() + c.commandResultSchemas["DOM.setOuterHTML"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetOuterHTMLResult]()) + c.commandParamsSchemas["DOM.undo"] = abxjsonschema.SchemaFor[DOMUndoParams]() + c.commandResultSchemas["DOM.undo"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMUndoResult]()) + c.commandParamsSchemas["DOM.getFrameOwner"] = abxjsonschema.SchemaFor[DOMGetFrameOwnerParams]() + c.commandResultSchemas["DOM.getFrameOwner"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetFrameOwnerResult]()) + c.commandParamsSchemas["DOM.getContainerForNode"] = abxjsonschema.SchemaFor[DOMGetContainerForNodeParams]() + c.commandResultSchemas["DOM.getContainerForNode"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetContainerForNodeResult]()) + c.commandParamsSchemas["DOM.getQueryingDescendantsForContainer"] = abxjsonschema.SchemaFor[DOMGetQueryingDescendantsForContainerParams]() + c.commandResultSchemas["DOM.getQueryingDescendantsForContainer"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetQueryingDescendantsForContainerResult]()) + c.commandParamsSchemas["DOM.getAnchorElement"] = abxjsonschema.SchemaFor[DOMGetAnchorElementParams]() + c.commandResultSchemas["DOM.getAnchorElement"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMGetAnchorElementResult]()) + c.commandParamsSchemas["DOM.forceShowPopover"] = abxjsonschema.SchemaFor[DOMForceShowPopoverParams]() + c.commandResultSchemas["DOM.forceShowPopover"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMForceShowPopoverResult]()) + c.commandParamsSchemas["DOMDebugger.getEventListeners"] = abxjsonschema.SchemaFor[DOMDebuggerGetEventListenersParams]() + c.commandResultSchemas["DOMDebugger.getEventListeners"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerGetEventListenersResult]()) + c.commandParamsSchemas["DOMDebugger.removeDOMBreakpoint"] = abxjsonschema.SchemaFor[DOMDebuggerRemoveDOMBreakpointParams]() + c.commandResultSchemas["DOMDebugger.removeDOMBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerRemoveDOMBreakpointResult]()) + c.commandParamsSchemas["DOMDebugger.removeEventListenerBreakpoint"] = abxjsonschema.SchemaFor[DOMDebuggerRemoveEventListenerBreakpointParams]() + c.commandResultSchemas["DOMDebugger.removeEventListenerBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerRemoveEventListenerBreakpointResult]()) + c.commandParamsSchemas["DOMDebugger.removeInstrumentationBreakpoint"] = abxjsonschema.SchemaFor[DOMDebuggerRemoveInstrumentationBreakpointParams]() + c.commandResultSchemas["DOMDebugger.removeInstrumentationBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerRemoveInstrumentationBreakpointResult]()) + c.commandParamsSchemas["DOMDebugger.removeXHRBreakpoint"] = abxjsonschema.SchemaFor[DOMDebuggerRemoveXHRBreakpointParams]() + c.commandResultSchemas["DOMDebugger.removeXHRBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerRemoveXHRBreakpointResult]()) + c.commandParamsSchemas["DOMDebugger.setBreakOnCSPViolation"] = abxjsonschema.SchemaFor[DOMDebuggerSetBreakOnCSPViolationParams]() + c.commandResultSchemas["DOMDebugger.setBreakOnCSPViolation"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerSetBreakOnCSPViolationResult]()) + c.commandParamsSchemas["DOMDebugger.setDOMBreakpoint"] = abxjsonschema.SchemaFor[DOMDebuggerSetDOMBreakpointParams]() + c.commandResultSchemas["DOMDebugger.setDOMBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerSetDOMBreakpointResult]()) + c.commandParamsSchemas["DOMDebugger.setEventListenerBreakpoint"] = abxjsonschema.SchemaFor[DOMDebuggerSetEventListenerBreakpointParams]() + c.commandResultSchemas["DOMDebugger.setEventListenerBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerSetEventListenerBreakpointResult]()) + c.commandParamsSchemas["DOMDebugger.setInstrumentationBreakpoint"] = abxjsonschema.SchemaFor[DOMDebuggerSetInstrumentationBreakpointParams]() + c.commandResultSchemas["DOMDebugger.setInstrumentationBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerSetInstrumentationBreakpointResult]()) + c.commandParamsSchemas["DOMDebugger.setXHRBreakpoint"] = abxjsonschema.SchemaFor[DOMDebuggerSetXHRBreakpointParams]() + c.commandResultSchemas["DOMDebugger.setXHRBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDebuggerSetXHRBreakpointResult]()) + c.commandParamsSchemas["DOMSnapshot.disable"] = abxjsonschema.SchemaFor[DOMSnapshotDisableParams]() + c.commandResultSchemas["DOMSnapshot.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSnapshotDisableResult]()) + c.commandParamsSchemas["DOMSnapshot.enable"] = abxjsonschema.SchemaFor[DOMSnapshotEnableParams]() + c.commandResultSchemas["DOMSnapshot.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSnapshotEnableResult]()) + c.commandParamsSchemas["DOMSnapshot.getSnapshot"] = abxjsonschema.SchemaFor[DOMSnapshotGetSnapshotParams]() + c.commandResultSchemas["DOMSnapshot.getSnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSnapshotGetSnapshotResult]()) + c.commandParamsSchemas["DOMSnapshot.captureSnapshot"] = abxjsonschema.SchemaFor[DOMSnapshotCaptureSnapshotParams]() + c.commandResultSchemas["DOMSnapshot.captureSnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSnapshotCaptureSnapshotResult]()) + c.commandParamsSchemas["DOMStorage.clear"] = abxjsonschema.SchemaFor[DOMStorageClearParams]() + c.commandResultSchemas["DOMStorage.clear"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageClearResult]()) + c.commandParamsSchemas["DOMStorage.disable"] = abxjsonschema.SchemaFor[DOMStorageDisableParams]() + c.commandResultSchemas["DOMStorage.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageDisableResult]()) + c.commandParamsSchemas["DOMStorage.enable"] = abxjsonschema.SchemaFor[DOMStorageEnableParams]() + c.commandResultSchemas["DOMStorage.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageEnableResult]()) + c.commandParamsSchemas["DOMStorage.getDOMStorageItems"] = abxjsonschema.SchemaFor[DOMStorageGetDOMStorageItemsParams]() + c.commandResultSchemas["DOMStorage.getDOMStorageItems"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageGetDOMStorageItemsResult]()) + c.commandParamsSchemas["DOMStorage.removeDOMStorageItem"] = abxjsonschema.SchemaFor[DOMStorageRemoveDOMStorageItemParams]() + c.commandResultSchemas["DOMStorage.removeDOMStorageItem"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageRemoveDOMStorageItemResult]()) + c.commandParamsSchemas["DOMStorage.setDOMStorageItem"] = abxjsonschema.SchemaFor[DOMStorageSetDOMStorageItemParams]() + c.commandResultSchemas["DOMStorage.setDOMStorageItem"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageSetDOMStorageItemResult]()) + c.commandParamsSchemas["Debugger.continueToLocation"] = abxjsonschema.SchemaFor[DebuggerContinueToLocationParams]() + c.commandResultSchemas["Debugger.continueToLocation"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerContinueToLocationResult]()) + c.commandParamsSchemas["Debugger.disable"] = abxjsonschema.SchemaFor[DebuggerDisableParams]() + c.commandResultSchemas["Debugger.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerDisableResult]()) + c.commandParamsSchemas["Debugger.enable"] = abxjsonschema.SchemaFor[DebuggerEnableParams]() + c.commandResultSchemas["Debugger.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerEnableResult]()) + c.commandParamsSchemas["Debugger.evaluateOnCallFrame"] = abxjsonschema.SchemaFor[DebuggerEvaluateOnCallFrameParams]() + c.commandResultSchemas["Debugger.evaluateOnCallFrame"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerEvaluateOnCallFrameResult]()) + c.commandParamsSchemas["Debugger.getPossibleBreakpoints"] = abxjsonschema.SchemaFor[DebuggerGetPossibleBreakpointsParams]() + c.commandResultSchemas["Debugger.getPossibleBreakpoints"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerGetPossibleBreakpointsResult]()) + c.commandParamsSchemas["Debugger.getScriptSource"] = abxjsonschema.SchemaFor[DebuggerGetScriptSourceParams]() + c.commandResultSchemas["Debugger.getScriptSource"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerGetScriptSourceResult]()) + c.commandParamsSchemas["Debugger.disassembleWasmModule"] = abxjsonschema.SchemaFor[DebuggerDisassembleWasmModuleParams]() + c.commandResultSchemas["Debugger.disassembleWasmModule"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerDisassembleWasmModuleResult]()) + c.commandParamsSchemas["Debugger.nextWasmDisassemblyChunk"] = abxjsonschema.SchemaFor[DebuggerNextWasmDisassemblyChunkParams]() + c.commandResultSchemas["Debugger.nextWasmDisassemblyChunk"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerNextWasmDisassemblyChunkResult]()) + c.commandParamsSchemas["Debugger.getWasmBytecode"] = abxjsonschema.SchemaFor[DebuggerGetWasmBytecodeParams]() + c.commandResultSchemas["Debugger.getWasmBytecode"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerGetWasmBytecodeResult]()) + c.commandParamsSchemas["Debugger.getStackTrace"] = abxjsonschema.SchemaFor[DebuggerGetStackTraceParams]() + c.commandResultSchemas["Debugger.getStackTrace"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerGetStackTraceResult]()) + c.commandParamsSchemas["Debugger.pause"] = abxjsonschema.SchemaFor[DebuggerPauseParams]() + c.commandResultSchemas["Debugger.pause"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerPauseResult]()) + c.commandParamsSchemas["Debugger.pauseOnAsyncCall"] = abxjsonschema.SchemaFor[DebuggerPauseOnAsyncCallParams]() + c.commandResultSchemas["Debugger.pauseOnAsyncCall"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerPauseOnAsyncCallResult]()) + c.commandParamsSchemas["Debugger.removeBreakpoint"] = abxjsonschema.SchemaFor[DebuggerRemoveBreakpointParams]() + c.commandResultSchemas["Debugger.removeBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerRemoveBreakpointResult]()) + c.commandParamsSchemas["Debugger.restartFrame"] = abxjsonschema.SchemaFor[DebuggerRestartFrameParams]() + c.commandResultSchemas["Debugger.restartFrame"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerRestartFrameResult]()) + c.commandParamsSchemas["Debugger.resume"] = abxjsonschema.SchemaFor[DebuggerResumeParams]() + c.commandResultSchemas["Debugger.resume"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerResumeResult]()) + c.commandParamsSchemas["Debugger.searchInContent"] = abxjsonschema.SchemaFor[DebuggerSearchInContentParams]() + c.commandResultSchemas["Debugger.searchInContent"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSearchInContentResult]()) + c.commandParamsSchemas["Debugger.setAsyncCallStackDepth"] = abxjsonschema.SchemaFor[DebuggerSetAsyncCallStackDepthParams]() + c.commandResultSchemas["Debugger.setAsyncCallStackDepth"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetAsyncCallStackDepthResult]()) + c.commandParamsSchemas["Debugger.setBlackboxExecutionContexts"] = abxjsonschema.SchemaFor[DebuggerSetBlackboxExecutionContextsParams]() + c.commandResultSchemas["Debugger.setBlackboxExecutionContexts"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetBlackboxExecutionContextsResult]()) + c.commandParamsSchemas["Debugger.setBlackboxPatterns"] = abxjsonschema.SchemaFor[DebuggerSetBlackboxPatternsParams]() + c.commandResultSchemas["Debugger.setBlackboxPatterns"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetBlackboxPatternsResult]()) + c.commandParamsSchemas["Debugger.setBlackboxedRanges"] = abxjsonschema.SchemaFor[DebuggerSetBlackboxedRangesParams]() + c.commandResultSchemas["Debugger.setBlackboxedRanges"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetBlackboxedRangesResult]()) + c.commandParamsSchemas["Debugger.setBreakpoint"] = abxjsonschema.SchemaFor[DebuggerSetBreakpointParams]() + c.commandResultSchemas["Debugger.setBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetBreakpointResult]()) + c.commandParamsSchemas["Debugger.setInstrumentationBreakpoint"] = abxjsonschema.SchemaFor[DebuggerSetInstrumentationBreakpointParams]() + c.commandResultSchemas["Debugger.setInstrumentationBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetInstrumentationBreakpointResult]()) + c.commandParamsSchemas["Debugger.setBreakpointByUrl"] = abxjsonschema.SchemaFor[DebuggerSetBreakpointByURLParams]() + c.commandResultSchemas["Debugger.setBreakpointByUrl"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetBreakpointByURLResult]()) + c.commandParamsSchemas["Debugger.setBreakpointOnFunctionCall"] = abxjsonschema.SchemaFor[DebuggerSetBreakpointOnFunctionCallParams]() + c.commandResultSchemas["Debugger.setBreakpointOnFunctionCall"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetBreakpointOnFunctionCallResult]()) + c.commandParamsSchemas["Debugger.setBreakpointsActive"] = abxjsonschema.SchemaFor[DebuggerSetBreakpointsActiveParams]() + c.commandResultSchemas["Debugger.setBreakpointsActive"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetBreakpointsActiveResult]()) + c.commandParamsSchemas["Debugger.setPauseOnExceptions"] = abxjsonschema.SchemaFor[DebuggerSetPauseOnExceptionsParams]() + c.commandResultSchemas["Debugger.setPauseOnExceptions"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetPauseOnExceptionsResult]()) + c.commandParamsSchemas["Debugger.setReturnValue"] = abxjsonschema.SchemaFor[DebuggerSetReturnValueParams]() + c.commandResultSchemas["Debugger.setReturnValue"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetReturnValueResult]()) + c.commandParamsSchemas["Debugger.setScriptSource"] = abxjsonschema.SchemaFor[DebuggerSetScriptSourceParams]() + c.commandResultSchemas["Debugger.setScriptSource"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetScriptSourceResult]()) + c.commandParamsSchemas["Debugger.setSkipAllPauses"] = abxjsonschema.SchemaFor[DebuggerSetSkipAllPausesParams]() + c.commandResultSchemas["Debugger.setSkipAllPauses"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetSkipAllPausesResult]()) + c.commandParamsSchemas["Debugger.setVariableValue"] = abxjsonschema.SchemaFor[DebuggerSetVariableValueParams]() + c.commandResultSchemas["Debugger.setVariableValue"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerSetVariableValueResult]()) + c.commandParamsSchemas["Debugger.stepInto"] = abxjsonschema.SchemaFor[DebuggerStepIntoParams]() + c.commandResultSchemas["Debugger.stepInto"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerStepIntoResult]()) + c.commandParamsSchemas["Debugger.stepOut"] = abxjsonschema.SchemaFor[DebuggerStepOutParams]() + c.commandResultSchemas["Debugger.stepOut"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerStepOutResult]()) + c.commandParamsSchemas["Debugger.stepOver"] = abxjsonschema.SchemaFor[DebuggerStepOverParams]() + c.commandResultSchemas["Debugger.stepOver"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerStepOverResult]()) + c.commandParamsSchemas["DeviceAccess.enable"] = abxjsonschema.SchemaFor[DeviceAccessEnableParams]() + c.commandResultSchemas["DeviceAccess.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[DeviceAccessEnableResult]()) + c.commandParamsSchemas["DeviceAccess.disable"] = abxjsonschema.SchemaFor[DeviceAccessDisableParams]() + c.commandResultSchemas["DeviceAccess.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[DeviceAccessDisableResult]()) + c.commandParamsSchemas["DeviceAccess.selectPrompt"] = abxjsonschema.SchemaFor[DeviceAccessSelectPromptParams]() + c.commandResultSchemas["DeviceAccess.selectPrompt"] = nativeResultSchema(abxjsonschema.SchemaFor[DeviceAccessSelectPromptResult]()) + c.commandParamsSchemas["DeviceAccess.cancelPrompt"] = abxjsonschema.SchemaFor[DeviceAccessCancelPromptParams]() + c.commandResultSchemas["DeviceAccess.cancelPrompt"] = nativeResultSchema(abxjsonschema.SchemaFor[DeviceAccessCancelPromptResult]()) + c.commandParamsSchemas["DeviceOrientation.clearDeviceOrientationOverride"] = abxjsonschema.SchemaFor[DeviceOrientationClearDeviceOrientationOverrideParams]() + c.commandResultSchemas["DeviceOrientation.clearDeviceOrientationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[DeviceOrientationClearDeviceOrientationOverrideResult]()) + c.commandParamsSchemas["DeviceOrientation.setDeviceOrientationOverride"] = abxjsonschema.SchemaFor[DeviceOrientationSetDeviceOrientationOverrideParams]() + c.commandResultSchemas["DeviceOrientation.setDeviceOrientationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[DeviceOrientationSetDeviceOrientationOverrideResult]()) + c.commandParamsSchemas["Emulation.canEmulate"] = abxjsonschema.SchemaFor[EmulationCanEmulateParams]() + c.commandResultSchemas["Emulation.canEmulate"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationCanEmulateResult]()) + c.commandParamsSchemas["Emulation.clearDeviceMetricsOverride"] = abxjsonschema.SchemaFor[EmulationClearDeviceMetricsOverrideParams]() + c.commandResultSchemas["Emulation.clearDeviceMetricsOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationClearDeviceMetricsOverrideResult]()) + c.commandParamsSchemas["Emulation.clearGeolocationOverride"] = abxjsonschema.SchemaFor[EmulationClearGeolocationOverrideParams]() + c.commandResultSchemas["Emulation.clearGeolocationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationClearGeolocationOverrideResult]()) + c.commandParamsSchemas["Emulation.resetPageScaleFactor"] = abxjsonschema.SchemaFor[EmulationResetPageScaleFactorParams]() + c.commandResultSchemas["Emulation.resetPageScaleFactor"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationResetPageScaleFactorResult]()) + c.commandParamsSchemas["Emulation.setFocusEmulationEnabled"] = abxjsonschema.SchemaFor[EmulationSetFocusEmulationEnabledParams]() + c.commandResultSchemas["Emulation.setFocusEmulationEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetFocusEmulationEnabledResult]()) + c.commandParamsSchemas["Emulation.setAutoDarkModeOverride"] = abxjsonschema.SchemaFor[EmulationSetAutoDarkModeOverrideParams]() + c.commandResultSchemas["Emulation.setAutoDarkModeOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetAutoDarkModeOverrideResult]()) + c.commandParamsSchemas["Emulation.setCPUThrottlingRate"] = abxjsonschema.SchemaFor[EmulationSetCPUThrottlingRateParams]() + c.commandResultSchemas["Emulation.setCPUThrottlingRate"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetCPUThrottlingRateResult]()) + c.commandParamsSchemas["Emulation.setDefaultBackgroundColorOverride"] = abxjsonschema.SchemaFor[EmulationSetDefaultBackgroundColorOverrideParams]() + c.commandResultSchemas["Emulation.setDefaultBackgroundColorOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetDefaultBackgroundColorOverrideResult]()) + c.commandParamsSchemas["Emulation.setSafeAreaInsetsOverride"] = abxjsonschema.SchemaFor[EmulationSetSafeAreaInsetsOverrideParams]() + c.commandResultSchemas["Emulation.setSafeAreaInsetsOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetSafeAreaInsetsOverrideResult]()) + c.commandParamsSchemas["Emulation.setDeviceMetricsOverride"] = abxjsonschema.SchemaFor[EmulationSetDeviceMetricsOverrideParams]() + c.commandResultSchemas["Emulation.setDeviceMetricsOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetDeviceMetricsOverrideResult]()) + c.commandParamsSchemas["Emulation.setDevicePostureOverride"] = abxjsonschema.SchemaFor[EmulationSetDevicePostureOverrideParams]() + c.commandResultSchemas["Emulation.setDevicePostureOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetDevicePostureOverrideResult]()) + c.commandParamsSchemas["Emulation.clearDevicePostureOverride"] = abxjsonschema.SchemaFor[EmulationClearDevicePostureOverrideParams]() + c.commandResultSchemas["Emulation.clearDevicePostureOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationClearDevicePostureOverrideResult]()) + c.commandParamsSchemas["Emulation.setDisplayFeaturesOverride"] = abxjsonschema.SchemaFor[EmulationSetDisplayFeaturesOverrideParams]() + c.commandResultSchemas["Emulation.setDisplayFeaturesOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetDisplayFeaturesOverrideResult]()) + c.commandParamsSchemas["Emulation.clearDisplayFeaturesOverride"] = abxjsonschema.SchemaFor[EmulationClearDisplayFeaturesOverrideParams]() + c.commandResultSchemas["Emulation.clearDisplayFeaturesOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationClearDisplayFeaturesOverrideResult]()) + c.commandParamsSchemas["Emulation.setScrollbarsHidden"] = abxjsonschema.SchemaFor[EmulationSetScrollbarsHiddenParams]() + c.commandResultSchemas["Emulation.setScrollbarsHidden"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetScrollbarsHiddenResult]()) + c.commandParamsSchemas["Emulation.setDocumentCookieDisabled"] = abxjsonschema.SchemaFor[EmulationSetDocumentCookieDisabledParams]() + c.commandResultSchemas["Emulation.setDocumentCookieDisabled"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetDocumentCookieDisabledResult]()) + c.commandParamsSchemas["Emulation.setEmitTouchEventsForMouse"] = abxjsonschema.SchemaFor[EmulationSetEmitTouchEventsForMouseParams]() + c.commandResultSchemas["Emulation.setEmitTouchEventsForMouse"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetEmitTouchEventsForMouseResult]()) + c.commandParamsSchemas["Emulation.setEmulatedMedia"] = abxjsonschema.SchemaFor[EmulationSetEmulatedMediaParams]() + c.commandResultSchemas["Emulation.setEmulatedMedia"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetEmulatedMediaResult]()) + c.commandParamsSchemas["Emulation.setEmulatedVisionDeficiency"] = abxjsonschema.SchemaFor[EmulationSetEmulatedVisionDeficiencyParams]() + c.commandResultSchemas["Emulation.setEmulatedVisionDeficiency"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetEmulatedVisionDeficiencyResult]()) + c.commandParamsSchemas["Emulation.setEmulatedOSTextScale"] = abxjsonschema.SchemaFor[EmulationSetEmulatedOSTextScaleParams]() + c.commandResultSchemas["Emulation.setEmulatedOSTextScale"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetEmulatedOSTextScaleResult]()) + c.commandParamsSchemas["Emulation.setGeolocationOverride"] = abxjsonschema.SchemaFor[EmulationSetGeolocationOverrideParams]() + c.commandResultSchemas["Emulation.setGeolocationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetGeolocationOverrideResult]()) + c.commandParamsSchemas["Emulation.getOverriddenSensorInformation"] = abxjsonschema.SchemaFor[EmulationGetOverriddenSensorInformationParams]() + c.commandResultSchemas["Emulation.getOverriddenSensorInformation"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationGetOverriddenSensorInformationResult]()) + c.commandParamsSchemas["Emulation.setSensorOverrideEnabled"] = abxjsonschema.SchemaFor[EmulationSetSensorOverrideEnabledParams]() + c.commandResultSchemas["Emulation.setSensorOverrideEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetSensorOverrideEnabledResult]()) + c.commandParamsSchemas["Emulation.setSensorOverrideReadings"] = abxjsonschema.SchemaFor[EmulationSetSensorOverrideReadingsParams]() + c.commandResultSchemas["Emulation.setSensorOverrideReadings"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetSensorOverrideReadingsResult]()) + c.commandParamsSchemas["Emulation.setPressureSourceOverrideEnabled"] = abxjsonschema.SchemaFor[EmulationSetPressureSourceOverrideEnabledParams]() + c.commandResultSchemas["Emulation.setPressureSourceOverrideEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetPressureSourceOverrideEnabledResult]()) + c.commandParamsSchemas["Emulation.setPressureStateOverride"] = abxjsonschema.SchemaFor[EmulationSetPressureStateOverrideParams]() + c.commandResultSchemas["Emulation.setPressureStateOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetPressureStateOverrideResult]()) + c.commandParamsSchemas["Emulation.setPressureDataOverride"] = abxjsonschema.SchemaFor[EmulationSetPressureDataOverrideParams]() + c.commandResultSchemas["Emulation.setPressureDataOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetPressureDataOverrideResult]()) + c.commandParamsSchemas["Emulation.setIdleOverride"] = abxjsonschema.SchemaFor[EmulationSetIdleOverrideParams]() + c.commandResultSchemas["Emulation.setIdleOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetIdleOverrideResult]()) + c.commandParamsSchemas["Emulation.clearIdleOverride"] = abxjsonschema.SchemaFor[EmulationClearIdleOverrideParams]() + c.commandResultSchemas["Emulation.clearIdleOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationClearIdleOverrideResult]()) + c.commandParamsSchemas["Emulation.setNavigatorOverrides"] = abxjsonschema.SchemaFor[EmulationSetNavigatorOverridesParams]() + c.commandResultSchemas["Emulation.setNavigatorOverrides"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetNavigatorOverridesResult]()) + c.commandParamsSchemas["Emulation.setPageScaleFactor"] = abxjsonschema.SchemaFor[EmulationSetPageScaleFactorParams]() + c.commandResultSchemas["Emulation.setPageScaleFactor"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetPageScaleFactorResult]()) + c.commandParamsSchemas["Emulation.setScriptExecutionDisabled"] = abxjsonschema.SchemaFor[EmulationSetScriptExecutionDisabledParams]() + c.commandResultSchemas["Emulation.setScriptExecutionDisabled"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetScriptExecutionDisabledResult]()) + c.commandParamsSchemas["Emulation.setTouchEmulationEnabled"] = abxjsonschema.SchemaFor[EmulationSetTouchEmulationEnabledParams]() + c.commandResultSchemas["Emulation.setTouchEmulationEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetTouchEmulationEnabledResult]()) + c.commandParamsSchemas["Emulation.setVirtualTimePolicy"] = abxjsonschema.SchemaFor[EmulationSetVirtualTimePolicyParams]() + c.commandResultSchemas["Emulation.setVirtualTimePolicy"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetVirtualTimePolicyResult]()) + c.commandParamsSchemas["Emulation.setLocaleOverride"] = abxjsonschema.SchemaFor[EmulationSetLocaleOverrideParams]() + c.commandResultSchemas["Emulation.setLocaleOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetLocaleOverrideResult]()) + c.commandParamsSchemas["Emulation.setTimezoneOverride"] = abxjsonschema.SchemaFor[EmulationSetTimezoneOverrideParams]() + c.commandResultSchemas["Emulation.setTimezoneOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetTimezoneOverrideResult]()) + c.commandParamsSchemas["Emulation.setVisibleSize"] = abxjsonschema.SchemaFor[EmulationSetVisibleSizeParams]() + c.commandResultSchemas["Emulation.setVisibleSize"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetVisibleSizeResult]()) + c.commandParamsSchemas["Emulation.setDisabledImageTypes"] = abxjsonschema.SchemaFor[EmulationSetDisabledImageTypesParams]() + c.commandResultSchemas["Emulation.setDisabledImageTypes"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetDisabledImageTypesResult]()) + c.commandParamsSchemas["Emulation.setDataSaverOverride"] = abxjsonschema.SchemaFor[EmulationSetDataSaverOverrideParams]() + c.commandResultSchemas["Emulation.setDataSaverOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetDataSaverOverrideResult]()) + c.commandParamsSchemas["Emulation.setHardwareConcurrencyOverride"] = abxjsonschema.SchemaFor[EmulationSetHardwareConcurrencyOverrideParams]() + c.commandResultSchemas["Emulation.setHardwareConcurrencyOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetHardwareConcurrencyOverrideResult]()) + c.commandParamsSchemas["Emulation.setUserAgentOverride"] = abxjsonschema.SchemaFor[EmulationSetUserAgentOverrideParams]() + c.commandResultSchemas["Emulation.setUserAgentOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetUserAgentOverrideResult]()) + c.commandParamsSchemas["Emulation.setAutomationOverride"] = abxjsonschema.SchemaFor[EmulationSetAutomationOverrideParams]() + c.commandResultSchemas["Emulation.setAutomationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetAutomationOverrideResult]()) + c.commandParamsSchemas["Emulation.setSmallViewportHeightDifferenceOverride"] = abxjsonschema.SchemaFor[EmulationSetSmallViewportHeightDifferenceOverrideParams]() + c.commandResultSchemas["Emulation.setSmallViewportHeightDifferenceOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetSmallViewportHeightDifferenceOverrideResult]()) + c.commandParamsSchemas["Emulation.getScreenInfos"] = abxjsonschema.SchemaFor[EmulationGetScreenInfosParams]() + c.commandResultSchemas["Emulation.getScreenInfos"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationGetScreenInfosResult]()) + c.commandParamsSchemas["Emulation.addScreen"] = abxjsonschema.SchemaFor[EmulationAddScreenParams]() + c.commandResultSchemas["Emulation.addScreen"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationAddScreenResult]()) + c.commandParamsSchemas["Emulation.updateScreen"] = abxjsonschema.SchemaFor[EmulationUpdateScreenParams]() + c.commandResultSchemas["Emulation.updateScreen"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationUpdateScreenResult]()) + c.commandParamsSchemas["Emulation.removeScreen"] = abxjsonschema.SchemaFor[EmulationRemoveScreenParams]() + c.commandResultSchemas["Emulation.removeScreen"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationRemoveScreenResult]()) + c.commandParamsSchemas["Emulation.setPrimaryScreen"] = abxjsonschema.SchemaFor[EmulationSetPrimaryScreenParams]() + c.commandResultSchemas["Emulation.setPrimaryScreen"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationSetPrimaryScreenResult]()) + c.commandParamsSchemas["EventBreakpoints.setInstrumentationBreakpoint"] = abxjsonschema.SchemaFor[EventBreakpointsSetInstrumentationBreakpointParams]() + c.commandResultSchemas["EventBreakpoints.setInstrumentationBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[EventBreakpointsSetInstrumentationBreakpointResult]()) + c.commandParamsSchemas["EventBreakpoints.removeInstrumentationBreakpoint"] = abxjsonschema.SchemaFor[EventBreakpointsRemoveInstrumentationBreakpointParams]() + c.commandResultSchemas["EventBreakpoints.removeInstrumentationBreakpoint"] = nativeResultSchema(abxjsonschema.SchemaFor[EventBreakpointsRemoveInstrumentationBreakpointResult]()) + c.commandParamsSchemas["EventBreakpoints.disable"] = abxjsonschema.SchemaFor[EventBreakpointsDisableParams]() + c.commandResultSchemas["EventBreakpoints.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[EventBreakpointsDisableResult]()) + c.commandParamsSchemas["Extensions.triggerAction"] = abxjsonschema.SchemaFor[ExtensionsTriggerActionParams]() + c.commandResultSchemas["Extensions.triggerAction"] = nativeResultSchema(abxjsonschema.SchemaFor[ExtensionsTriggerActionResult]()) + c.commandParamsSchemas["Extensions.loadUnpacked"] = abxjsonschema.SchemaFor[ExtensionsLoadUnpackedParams]() + c.commandResultSchemas["Extensions.loadUnpacked"] = nativeResultSchema(abxjsonschema.SchemaFor[ExtensionsLoadUnpackedResult]()) + c.commandParamsSchemas["Extensions.getExtensions"] = abxjsonschema.SchemaFor[ExtensionsGetExtensionsParams]() + c.commandResultSchemas["Extensions.getExtensions"] = nativeResultSchema(abxjsonschema.SchemaFor[ExtensionsGetExtensionsResult]()) + c.commandParamsSchemas["Extensions.uninstall"] = abxjsonschema.SchemaFor[ExtensionsUninstallParams]() + c.commandResultSchemas["Extensions.uninstall"] = nativeResultSchema(abxjsonschema.SchemaFor[ExtensionsUninstallResult]()) + c.commandParamsSchemas["Extensions.getStorageItems"] = abxjsonschema.SchemaFor[ExtensionsGetStorageItemsParams]() + c.commandResultSchemas["Extensions.getStorageItems"] = nativeResultSchema(abxjsonschema.SchemaFor[ExtensionsGetStorageItemsResult]()) + c.commandParamsSchemas["Extensions.removeStorageItems"] = abxjsonschema.SchemaFor[ExtensionsRemoveStorageItemsParams]() + c.commandResultSchemas["Extensions.removeStorageItems"] = nativeResultSchema(abxjsonschema.SchemaFor[ExtensionsRemoveStorageItemsResult]()) + c.commandParamsSchemas["Extensions.clearStorageItems"] = abxjsonschema.SchemaFor[ExtensionsClearStorageItemsParams]() + c.commandResultSchemas["Extensions.clearStorageItems"] = nativeResultSchema(abxjsonschema.SchemaFor[ExtensionsClearStorageItemsResult]()) + c.commandParamsSchemas["Extensions.setStorageItems"] = abxjsonschema.SchemaFor[ExtensionsSetStorageItemsParams]() + c.commandResultSchemas["Extensions.setStorageItems"] = nativeResultSchema(abxjsonschema.SchemaFor[ExtensionsSetStorageItemsResult]()) + c.commandParamsSchemas["FedCm.enable"] = abxjsonschema.SchemaFor[FedCmEnableParams]() + c.commandResultSchemas["FedCm.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmEnableResult]()) + c.commandParamsSchemas["FedCm.disable"] = abxjsonschema.SchemaFor[FedCmDisableParams]() + c.commandResultSchemas["FedCm.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmDisableResult]()) + c.commandParamsSchemas["FedCm.selectAccount"] = abxjsonschema.SchemaFor[FedCmSelectAccountParams]() + c.commandResultSchemas["FedCm.selectAccount"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmSelectAccountResult]()) + c.commandParamsSchemas["FedCm.clickDialogButton"] = abxjsonschema.SchemaFor[FedCmClickDialogButtonParams]() + c.commandResultSchemas["FedCm.clickDialogButton"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmClickDialogButtonResult]()) + c.commandParamsSchemas["FedCm.openUrl"] = abxjsonschema.SchemaFor[FedCmOpenURLParams]() + c.commandResultSchemas["FedCm.openUrl"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmOpenURLResult]()) + c.commandParamsSchemas["FedCm.dismissDialog"] = abxjsonschema.SchemaFor[FedCmDismissDialogParams]() + c.commandResultSchemas["FedCm.dismissDialog"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmDismissDialogResult]()) + c.commandParamsSchemas["FedCm.resetCooldown"] = abxjsonschema.SchemaFor[FedCmResetCooldownParams]() + c.commandResultSchemas["FedCm.resetCooldown"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmResetCooldownResult]()) + c.commandParamsSchemas["Fetch.disable"] = abxjsonschema.SchemaFor[FetchDisableParams]() + c.commandResultSchemas["Fetch.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchDisableResult]()) + c.commandParamsSchemas["Fetch.enable"] = abxjsonschema.SchemaFor[FetchEnableParams]() + c.commandResultSchemas["Fetch.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchEnableResult]()) + c.commandParamsSchemas["Fetch.failRequest"] = abxjsonschema.SchemaFor[FetchFailRequestParams]() + c.commandResultSchemas["Fetch.failRequest"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchFailRequestResult]()) + c.commandParamsSchemas["Fetch.fulfillRequest"] = abxjsonschema.SchemaFor[FetchFulfillRequestParams]() + c.commandResultSchemas["Fetch.fulfillRequest"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchFulfillRequestResult]()) + c.commandParamsSchemas["Fetch.continueRequest"] = abxjsonschema.SchemaFor[FetchContinueRequestParams]() + c.commandResultSchemas["Fetch.continueRequest"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchContinueRequestResult]()) + c.commandParamsSchemas["Fetch.continueWithAuth"] = abxjsonschema.SchemaFor[FetchContinueWithAuthParams]() + c.commandResultSchemas["Fetch.continueWithAuth"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchContinueWithAuthResult]()) + c.commandParamsSchemas["Fetch.continueResponse"] = abxjsonschema.SchemaFor[FetchContinueResponseParams]() + c.commandResultSchemas["Fetch.continueResponse"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchContinueResponseResult]()) + c.commandParamsSchemas["Fetch.getResponseBody"] = abxjsonschema.SchemaFor[FetchGetResponseBodyParams]() + c.commandResultSchemas["Fetch.getResponseBody"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchGetResponseBodyResult]()) + c.commandParamsSchemas["Fetch.takeResponseBodyAsStream"] = abxjsonschema.SchemaFor[FetchTakeResponseBodyAsStreamParams]() + c.commandResultSchemas["Fetch.takeResponseBodyAsStream"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchTakeResponseBodyAsStreamResult]()) + c.commandParamsSchemas["FileSystem.getDirectory"] = abxjsonschema.SchemaFor[FileSystemGetDirectoryParams]() + c.commandResultSchemas["FileSystem.getDirectory"] = nativeResultSchema(abxjsonschema.SchemaFor[FileSystemGetDirectoryResult]()) + c.commandParamsSchemas["HeadlessExperimental.beginFrame"] = abxjsonschema.SchemaFor[HeadlessExperimentalBeginFrameParams]() + c.commandResultSchemas["HeadlessExperimental.beginFrame"] = nativeResultSchema(abxjsonschema.SchemaFor[HeadlessExperimentalBeginFrameResult]()) + c.commandParamsSchemas["HeadlessExperimental.disable"] = abxjsonschema.SchemaFor[HeadlessExperimentalDisableParams]() + c.commandResultSchemas["HeadlessExperimental.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[HeadlessExperimentalDisableResult]()) + c.commandParamsSchemas["HeadlessExperimental.enable"] = abxjsonschema.SchemaFor[HeadlessExperimentalEnableParams]() + c.commandResultSchemas["HeadlessExperimental.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[HeadlessExperimentalEnableResult]()) + c.commandParamsSchemas["HeapProfiler.addInspectedHeapObject"] = abxjsonschema.SchemaFor[HeapProfilerAddInspectedHeapObjectParams]() + c.commandResultSchemas["HeapProfiler.addInspectedHeapObject"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerAddInspectedHeapObjectResult]()) + c.commandParamsSchemas["HeapProfiler.collectGarbage"] = abxjsonschema.SchemaFor[HeapProfilerCollectGarbageParams]() + c.commandResultSchemas["HeapProfiler.collectGarbage"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerCollectGarbageResult]()) + c.commandParamsSchemas["HeapProfiler.disable"] = abxjsonschema.SchemaFor[HeapProfilerDisableParams]() + c.commandResultSchemas["HeapProfiler.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerDisableResult]()) + c.commandParamsSchemas["HeapProfiler.enable"] = abxjsonschema.SchemaFor[HeapProfilerEnableParams]() + c.commandResultSchemas["HeapProfiler.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerEnableResult]()) + c.commandParamsSchemas["HeapProfiler.getHeapObjectId"] = abxjsonschema.SchemaFor[HeapProfilerGetHeapObjectIDParams]() + c.commandResultSchemas["HeapProfiler.getHeapObjectId"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerGetHeapObjectIDResult]()) + c.commandParamsSchemas["HeapProfiler.getObjectByHeapObjectId"] = abxjsonschema.SchemaFor[HeapProfilerGetObjectByHeapObjectIDParams]() + c.commandResultSchemas["HeapProfiler.getObjectByHeapObjectId"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerGetObjectByHeapObjectIDResult]()) + c.commandParamsSchemas["HeapProfiler.getSamplingProfile"] = abxjsonschema.SchemaFor[HeapProfilerGetSamplingProfileParams]() + c.commandResultSchemas["HeapProfiler.getSamplingProfile"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerGetSamplingProfileResult]()) + c.commandParamsSchemas["HeapProfiler.startSampling"] = abxjsonschema.SchemaFor[HeapProfilerStartSamplingParams]() + c.commandResultSchemas["HeapProfiler.startSampling"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerStartSamplingResult]()) + c.commandParamsSchemas["HeapProfiler.startTrackingHeapObjects"] = abxjsonschema.SchemaFor[HeapProfilerStartTrackingHeapObjectsParams]() + c.commandResultSchemas["HeapProfiler.startTrackingHeapObjects"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerStartTrackingHeapObjectsResult]()) + c.commandParamsSchemas["HeapProfiler.stopSampling"] = abxjsonschema.SchemaFor[HeapProfilerStopSamplingParams]() + c.commandResultSchemas["HeapProfiler.stopSampling"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerStopSamplingResult]()) + c.commandParamsSchemas["HeapProfiler.stopTrackingHeapObjects"] = abxjsonschema.SchemaFor[HeapProfilerStopTrackingHeapObjectsParams]() + c.commandResultSchemas["HeapProfiler.stopTrackingHeapObjects"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerStopTrackingHeapObjectsResult]()) + c.commandParamsSchemas["HeapProfiler.takeHeapSnapshot"] = abxjsonschema.SchemaFor[HeapProfilerTakeHeapSnapshotParams]() + c.commandResultSchemas["HeapProfiler.takeHeapSnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerTakeHeapSnapshotResult]()) + c.commandParamsSchemas["IO.close"] = abxjsonschema.SchemaFor[IOCloseParams]() + c.commandResultSchemas["IO.close"] = nativeResultSchema(abxjsonschema.SchemaFor[IOCloseResult]()) + c.commandParamsSchemas["IO.read"] = abxjsonschema.SchemaFor[IOReadParams]() + c.commandResultSchemas["IO.read"] = nativeResultSchema(abxjsonschema.SchemaFor[IOReadResult]()) + c.commandParamsSchemas["IO.resolveBlob"] = abxjsonschema.SchemaFor[IOResolveBlobParams]() + c.commandResultSchemas["IO.resolveBlob"] = nativeResultSchema(abxjsonschema.SchemaFor[IOResolveBlobResult]()) + c.commandParamsSchemas["IndexedDB.clearObjectStore"] = abxjsonschema.SchemaFor[IndexedDBClearObjectStoreParams]() + c.commandResultSchemas["IndexedDB.clearObjectStore"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBClearObjectStoreResult]()) + c.commandParamsSchemas["IndexedDB.deleteDatabase"] = abxjsonschema.SchemaFor[IndexedDBDeleteDatabaseParams]() + c.commandResultSchemas["IndexedDB.deleteDatabase"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBDeleteDatabaseResult]()) + c.commandParamsSchemas["IndexedDB.deleteObjectStoreEntries"] = abxjsonschema.SchemaFor[IndexedDBDeleteObjectStoreEntriesParams]() + c.commandResultSchemas["IndexedDB.deleteObjectStoreEntries"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBDeleteObjectStoreEntriesResult]()) + c.commandParamsSchemas["IndexedDB.disable"] = abxjsonschema.SchemaFor[IndexedDBDisableParams]() + c.commandResultSchemas["IndexedDB.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBDisableResult]()) + c.commandParamsSchemas["IndexedDB.enable"] = abxjsonschema.SchemaFor[IndexedDBEnableParams]() + c.commandResultSchemas["IndexedDB.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBEnableResult]()) + c.commandParamsSchemas["IndexedDB.requestData"] = abxjsonschema.SchemaFor[IndexedDBRequestDataParams]() + c.commandResultSchemas["IndexedDB.requestData"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBRequestDataResult]()) + c.commandParamsSchemas["IndexedDB.getMetadata"] = abxjsonschema.SchemaFor[IndexedDBGetMetadataParams]() + c.commandResultSchemas["IndexedDB.getMetadata"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBGetMetadataResult]()) + c.commandParamsSchemas["IndexedDB.requestDatabase"] = abxjsonschema.SchemaFor[IndexedDBRequestDatabaseParams]() + c.commandResultSchemas["IndexedDB.requestDatabase"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBRequestDatabaseResult]()) + c.commandParamsSchemas["IndexedDB.requestDatabaseNames"] = abxjsonschema.SchemaFor[IndexedDBRequestDatabaseNamesParams]() + c.commandResultSchemas["IndexedDB.requestDatabaseNames"] = nativeResultSchema(abxjsonschema.SchemaFor[IndexedDBRequestDatabaseNamesResult]()) + c.commandParamsSchemas["Input.dispatchDragEvent"] = abxjsonschema.SchemaFor[InputDispatchDragEventParams]() + c.commandResultSchemas["Input.dispatchDragEvent"] = nativeResultSchema(abxjsonschema.SchemaFor[InputDispatchDragEventResult]()) + c.commandParamsSchemas["Input.dispatchKeyEvent"] = abxjsonschema.SchemaFor[InputDispatchKeyEventParams]() + c.commandResultSchemas["Input.dispatchKeyEvent"] = nativeResultSchema(abxjsonschema.SchemaFor[InputDispatchKeyEventResult]()) + c.commandParamsSchemas["Input.insertText"] = abxjsonschema.SchemaFor[InputInsertTextParams]() + c.commandResultSchemas["Input.insertText"] = nativeResultSchema(abxjsonschema.SchemaFor[InputInsertTextResult]()) + c.commandParamsSchemas["Input.imeSetComposition"] = abxjsonschema.SchemaFor[InputImeSetCompositionParams]() + c.commandResultSchemas["Input.imeSetComposition"] = nativeResultSchema(abxjsonschema.SchemaFor[InputImeSetCompositionResult]()) + c.commandParamsSchemas["Input.dispatchMouseEvent"] = abxjsonschema.SchemaFor[InputDispatchMouseEventParams]() + c.commandResultSchemas["Input.dispatchMouseEvent"] = nativeResultSchema(abxjsonschema.SchemaFor[InputDispatchMouseEventResult]()) + c.commandParamsSchemas["Input.dispatchTouchEvent"] = abxjsonschema.SchemaFor[InputDispatchTouchEventParams]() + c.commandResultSchemas["Input.dispatchTouchEvent"] = nativeResultSchema(abxjsonschema.SchemaFor[InputDispatchTouchEventResult]()) + c.commandParamsSchemas["Input.cancelDragging"] = abxjsonschema.SchemaFor[InputCancelDraggingParams]() + c.commandResultSchemas["Input.cancelDragging"] = nativeResultSchema(abxjsonschema.SchemaFor[InputCancelDraggingResult]()) + c.commandParamsSchemas["Input.emulateTouchFromMouseEvent"] = abxjsonschema.SchemaFor[InputEmulateTouchFromMouseEventParams]() + c.commandResultSchemas["Input.emulateTouchFromMouseEvent"] = nativeResultSchema(abxjsonschema.SchemaFor[InputEmulateTouchFromMouseEventResult]()) + c.commandParamsSchemas["Input.setIgnoreInputEvents"] = abxjsonschema.SchemaFor[InputSetIgnoreInputEventsParams]() + c.commandResultSchemas["Input.setIgnoreInputEvents"] = nativeResultSchema(abxjsonschema.SchemaFor[InputSetIgnoreInputEventsResult]()) + c.commandParamsSchemas["Input.setInterceptDrags"] = abxjsonschema.SchemaFor[InputSetInterceptDragsParams]() + c.commandResultSchemas["Input.setInterceptDrags"] = nativeResultSchema(abxjsonschema.SchemaFor[InputSetInterceptDragsResult]()) + c.commandParamsSchemas["Input.synthesizePinchGesture"] = abxjsonschema.SchemaFor[InputSynthesizePinchGestureParams]() + c.commandResultSchemas["Input.synthesizePinchGesture"] = nativeResultSchema(abxjsonschema.SchemaFor[InputSynthesizePinchGestureResult]()) + c.commandParamsSchemas["Input.synthesizeScrollGesture"] = abxjsonschema.SchemaFor[InputSynthesizeScrollGestureParams]() + c.commandResultSchemas["Input.synthesizeScrollGesture"] = nativeResultSchema(abxjsonschema.SchemaFor[InputSynthesizeScrollGestureResult]()) + c.commandParamsSchemas["Input.synthesizeTapGesture"] = abxjsonschema.SchemaFor[InputSynthesizeTapGestureParams]() + c.commandResultSchemas["Input.synthesizeTapGesture"] = nativeResultSchema(abxjsonschema.SchemaFor[InputSynthesizeTapGestureResult]()) + c.commandParamsSchemas["Inspector.disable"] = abxjsonschema.SchemaFor[InspectorDisableParams]() + c.commandResultSchemas["Inspector.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[InspectorDisableResult]()) + c.commandParamsSchemas["Inspector.enable"] = abxjsonschema.SchemaFor[InspectorEnableParams]() + c.commandResultSchemas["Inspector.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[InspectorEnableResult]()) + c.commandParamsSchemas["LayerTree.compositingReasons"] = abxjsonschema.SchemaFor[LayerTreeCompositingReasonsParams]() + c.commandResultSchemas["LayerTree.compositingReasons"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeCompositingReasonsResult]()) + c.commandParamsSchemas["LayerTree.disable"] = abxjsonschema.SchemaFor[LayerTreeDisableParams]() + c.commandResultSchemas["LayerTree.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeDisableResult]()) + c.commandParamsSchemas["LayerTree.enable"] = abxjsonschema.SchemaFor[LayerTreeEnableParams]() + c.commandResultSchemas["LayerTree.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeEnableResult]()) + c.commandParamsSchemas["LayerTree.loadSnapshot"] = abxjsonschema.SchemaFor[LayerTreeLoadSnapshotParams]() + c.commandResultSchemas["LayerTree.loadSnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeLoadSnapshotResult]()) + c.commandParamsSchemas["LayerTree.makeSnapshot"] = abxjsonschema.SchemaFor[LayerTreeMakeSnapshotParams]() + c.commandResultSchemas["LayerTree.makeSnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeMakeSnapshotResult]()) + c.commandParamsSchemas["LayerTree.profileSnapshot"] = abxjsonschema.SchemaFor[LayerTreeProfileSnapshotParams]() + c.commandResultSchemas["LayerTree.profileSnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeProfileSnapshotResult]()) + c.commandParamsSchemas["LayerTree.releaseSnapshot"] = abxjsonschema.SchemaFor[LayerTreeReleaseSnapshotParams]() + c.commandResultSchemas["LayerTree.releaseSnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeReleaseSnapshotResult]()) + c.commandParamsSchemas["LayerTree.replaySnapshot"] = abxjsonschema.SchemaFor[LayerTreeReplaySnapshotParams]() + c.commandResultSchemas["LayerTree.replaySnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeReplaySnapshotResult]()) + c.commandParamsSchemas["LayerTree.snapshotCommandLog"] = abxjsonschema.SchemaFor[LayerTreeSnapshotCommandLogParams]() + c.commandResultSchemas["LayerTree.snapshotCommandLog"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeSnapshotCommandLogResult]()) + c.commandParamsSchemas["Log.clear"] = abxjsonschema.SchemaFor[LogClearParams]() + c.commandResultSchemas["Log.clear"] = nativeResultSchema(abxjsonschema.SchemaFor[LogClearResult]()) + c.commandParamsSchemas["Log.disable"] = abxjsonschema.SchemaFor[LogDisableParams]() + c.commandResultSchemas["Log.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[LogDisableResult]()) + c.commandParamsSchemas["Log.enable"] = abxjsonschema.SchemaFor[LogEnableParams]() + c.commandResultSchemas["Log.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[LogEnableResult]()) + c.commandParamsSchemas["Log.startViolationsReport"] = abxjsonschema.SchemaFor[LogStartViolationsReportParams]() + c.commandResultSchemas["Log.startViolationsReport"] = nativeResultSchema(abxjsonschema.SchemaFor[LogStartViolationsReportResult]()) + c.commandParamsSchemas["Log.stopViolationsReport"] = abxjsonschema.SchemaFor[LogStopViolationsReportParams]() + c.commandResultSchemas["Log.stopViolationsReport"] = nativeResultSchema(abxjsonschema.SchemaFor[LogStopViolationsReportResult]()) + c.commandParamsSchemas["Media.enable"] = abxjsonschema.SchemaFor[MediaEnableParams]() + c.commandResultSchemas["Media.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[MediaEnableResult]()) + c.commandParamsSchemas["Media.disable"] = abxjsonschema.SchemaFor[MediaDisableParams]() + c.commandResultSchemas["Media.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[MediaDisableResult]()) + c.commandParamsSchemas["Memory.getDOMCounters"] = abxjsonschema.SchemaFor[MemoryGetDOMCountersParams]() + c.commandResultSchemas["Memory.getDOMCounters"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryGetDOMCountersResult]()) + c.commandParamsSchemas["Memory.getDOMCountersForLeakDetection"] = abxjsonschema.SchemaFor[MemoryGetDOMCountersForLeakDetectionParams]() + c.commandResultSchemas["Memory.getDOMCountersForLeakDetection"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryGetDOMCountersForLeakDetectionResult]()) + c.commandParamsSchemas["Memory.prepareForLeakDetection"] = abxjsonschema.SchemaFor[MemoryPrepareForLeakDetectionParams]() + c.commandResultSchemas["Memory.prepareForLeakDetection"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryPrepareForLeakDetectionResult]()) + c.commandParamsSchemas["Memory.forciblyPurgeJavaScriptMemory"] = abxjsonschema.SchemaFor[MemoryForciblyPurgeJavaScriptMemoryParams]() + c.commandResultSchemas["Memory.forciblyPurgeJavaScriptMemory"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryForciblyPurgeJavaScriptMemoryResult]()) + c.commandParamsSchemas["Memory.setPressureNotificationsSuppressed"] = abxjsonschema.SchemaFor[MemorySetPressureNotificationsSuppressedParams]() + c.commandResultSchemas["Memory.setPressureNotificationsSuppressed"] = nativeResultSchema(abxjsonschema.SchemaFor[MemorySetPressureNotificationsSuppressedResult]()) + c.commandParamsSchemas["Memory.simulatePressureNotification"] = abxjsonschema.SchemaFor[MemorySimulatePressureNotificationParams]() + c.commandResultSchemas["Memory.simulatePressureNotification"] = nativeResultSchema(abxjsonschema.SchemaFor[MemorySimulatePressureNotificationResult]()) + c.commandParamsSchemas["Memory.startSampling"] = abxjsonschema.SchemaFor[MemoryStartSamplingParams]() + c.commandResultSchemas["Memory.startSampling"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryStartSamplingResult]()) + c.commandParamsSchemas["Memory.stopSampling"] = abxjsonschema.SchemaFor[MemoryStopSamplingParams]() + c.commandResultSchemas["Memory.stopSampling"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryStopSamplingResult]()) + c.commandParamsSchemas["Memory.getAllTimeSamplingProfile"] = abxjsonschema.SchemaFor[MemoryGetAllTimeSamplingProfileParams]() + c.commandResultSchemas["Memory.getAllTimeSamplingProfile"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryGetAllTimeSamplingProfileResult]()) + c.commandParamsSchemas["Memory.getBrowserSamplingProfile"] = abxjsonschema.SchemaFor[MemoryGetBrowserSamplingProfileParams]() + c.commandResultSchemas["Memory.getBrowserSamplingProfile"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryGetBrowserSamplingProfileResult]()) + c.commandParamsSchemas["Memory.getSamplingProfile"] = abxjsonschema.SchemaFor[MemoryGetSamplingProfileParams]() + c.commandResultSchemas["Memory.getSamplingProfile"] = nativeResultSchema(abxjsonschema.SchemaFor[MemoryGetSamplingProfileResult]()) + c.commandParamsSchemas["Network.setAcceptedEncodings"] = abxjsonschema.SchemaFor[NetworkSetAcceptedEncodingsParams]() + c.commandResultSchemas["Network.setAcceptedEncodings"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetAcceptedEncodingsResult]()) + c.commandParamsSchemas["Network.clearAcceptedEncodingsOverride"] = abxjsonschema.SchemaFor[NetworkClearAcceptedEncodingsOverrideParams]() + c.commandResultSchemas["Network.clearAcceptedEncodingsOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkClearAcceptedEncodingsOverrideResult]()) + c.commandParamsSchemas["Network.canClearBrowserCache"] = abxjsonschema.SchemaFor[NetworkCanClearBrowserCacheParams]() + c.commandResultSchemas["Network.canClearBrowserCache"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkCanClearBrowserCacheResult]()) + c.commandParamsSchemas["Network.canClearBrowserCookies"] = abxjsonschema.SchemaFor[NetworkCanClearBrowserCookiesParams]() + c.commandResultSchemas["Network.canClearBrowserCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkCanClearBrowserCookiesResult]()) + c.commandParamsSchemas["Network.canEmulateNetworkConditions"] = abxjsonschema.SchemaFor[NetworkCanEmulateNetworkConditionsParams]() + c.commandResultSchemas["Network.canEmulateNetworkConditions"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkCanEmulateNetworkConditionsResult]()) + c.commandParamsSchemas["Network.clearBrowserCache"] = abxjsonschema.SchemaFor[NetworkClearBrowserCacheParams]() + c.commandResultSchemas["Network.clearBrowserCache"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkClearBrowserCacheResult]()) + c.commandParamsSchemas["Network.clearBrowserCookies"] = abxjsonschema.SchemaFor[NetworkClearBrowserCookiesParams]() + c.commandResultSchemas["Network.clearBrowserCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkClearBrowserCookiesResult]()) + c.commandParamsSchemas["Network.continueInterceptedRequest"] = abxjsonschema.SchemaFor[NetworkContinueInterceptedRequestParams]() + c.commandResultSchemas["Network.continueInterceptedRequest"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkContinueInterceptedRequestResult]()) + c.commandParamsSchemas["Network.deleteCookies"] = abxjsonschema.SchemaFor[NetworkDeleteCookiesParams]() + c.commandResultSchemas["Network.deleteCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDeleteCookiesResult]()) + c.commandParamsSchemas["Network.disable"] = abxjsonschema.SchemaFor[NetworkDisableParams]() + c.commandResultSchemas["Network.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDisableResult]()) + c.commandParamsSchemas["Network.emulateNetworkConditions"] = abxjsonschema.SchemaFor[NetworkEmulateNetworkConditionsParams]() + c.commandResultSchemas["Network.emulateNetworkConditions"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkEmulateNetworkConditionsResult]()) + c.commandParamsSchemas["Network.emulateNetworkConditionsByRule"] = abxjsonschema.SchemaFor[NetworkEmulateNetworkConditionsByRuleParams]() + c.commandResultSchemas["Network.emulateNetworkConditionsByRule"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkEmulateNetworkConditionsByRuleResult]()) + c.commandParamsSchemas["Network.overrideNetworkState"] = abxjsonschema.SchemaFor[NetworkOverrideNetworkStateParams]() + c.commandResultSchemas["Network.overrideNetworkState"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkOverrideNetworkStateResult]()) + c.commandParamsSchemas["Network.enable"] = abxjsonschema.SchemaFor[NetworkEnableParams]() + c.commandResultSchemas["Network.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkEnableResult]()) + c.commandParamsSchemas["Network.configureDurableMessages"] = abxjsonschema.SchemaFor[NetworkConfigureDurableMessagesParams]() + c.commandResultSchemas["Network.configureDurableMessages"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkConfigureDurableMessagesResult]()) + c.commandParamsSchemas["Network.getAllCookies"] = abxjsonschema.SchemaFor[NetworkGetAllCookiesParams]() + c.commandResultSchemas["Network.getAllCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkGetAllCookiesResult]()) + c.commandParamsSchemas["Network.getCertificate"] = abxjsonschema.SchemaFor[NetworkGetCertificateParams]() + c.commandResultSchemas["Network.getCertificate"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkGetCertificateResult]()) + c.commandParamsSchemas["Network.getCookies"] = abxjsonschema.SchemaFor[NetworkGetCookiesParams]() + c.commandResultSchemas["Network.getCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkGetCookiesResult]()) + c.commandParamsSchemas["Network.getResponseBody"] = abxjsonschema.SchemaFor[NetworkGetResponseBodyParams]() + c.commandResultSchemas["Network.getResponseBody"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkGetResponseBodyResult]()) + c.commandParamsSchemas["Network.getRequestPostData"] = abxjsonschema.SchemaFor[NetworkGetRequestPostDataParams]() + c.commandResultSchemas["Network.getRequestPostData"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkGetRequestPostDataResult]()) + c.commandParamsSchemas["Network.getResponseBodyForInterception"] = abxjsonschema.SchemaFor[NetworkGetResponseBodyForInterceptionParams]() + c.commandResultSchemas["Network.getResponseBodyForInterception"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkGetResponseBodyForInterceptionResult]()) + c.commandParamsSchemas["Network.takeResponseBodyForInterceptionAsStream"] = abxjsonschema.SchemaFor[NetworkTakeResponseBodyForInterceptionAsStreamParams]() + c.commandResultSchemas["Network.takeResponseBodyForInterceptionAsStream"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkTakeResponseBodyForInterceptionAsStreamResult]()) + c.commandParamsSchemas["Network.replayXHR"] = abxjsonschema.SchemaFor[NetworkReplayXHRParams]() + c.commandResultSchemas["Network.replayXHR"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkReplayXHRResult]()) + c.commandParamsSchemas["Network.searchInResponseBody"] = abxjsonschema.SchemaFor[NetworkSearchInResponseBodyParams]() + c.commandResultSchemas["Network.searchInResponseBody"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSearchInResponseBodyResult]()) + c.commandParamsSchemas["Network.setBlockedURLs"] = abxjsonschema.SchemaFor[NetworkSetBlockedURLsParams]() + c.commandResultSchemas["Network.setBlockedURLs"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetBlockedURLsResult]()) + c.commandParamsSchemas["Network.setBypassServiceWorker"] = abxjsonschema.SchemaFor[NetworkSetBypassServiceWorkerParams]() + c.commandResultSchemas["Network.setBypassServiceWorker"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetBypassServiceWorkerResult]()) + c.commandParamsSchemas["Network.setCacheDisabled"] = abxjsonschema.SchemaFor[NetworkSetCacheDisabledParams]() + c.commandResultSchemas["Network.setCacheDisabled"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetCacheDisabledResult]()) + c.commandParamsSchemas["Network.setCookie"] = abxjsonschema.SchemaFor[NetworkSetCookieParams]() + c.commandResultSchemas["Network.setCookie"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetCookieResult]()) + c.commandParamsSchemas["Network.setCookies"] = abxjsonschema.SchemaFor[NetworkSetCookiesParams]() + c.commandResultSchemas["Network.setCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetCookiesResult]()) + c.commandParamsSchemas["Network.setExtraHTTPHeaders"] = abxjsonschema.SchemaFor[NetworkSetExtraHTTPHeadersParams]() + c.commandResultSchemas["Network.setExtraHTTPHeaders"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetExtraHTTPHeadersResult]()) + c.commandParamsSchemas["Network.setAttachDebugStack"] = abxjsonschema.SchemaFor[NetworkSetAttachDebugStackParams]() + c.commandResultSchemas["Network.setAttachDebugStack"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetAttachDebugStackResult]()) + c.commandParamsSchemas["Network.setRequestInterception"] = abxjsonschema.SchemaFor[NetworkSetRequestInterceptionParams]() + c.commandResultSchemas["Network.setRequestInterception"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetRequestInterceptionResult]()) + c.commandParamsSchemas["Network.setUserAgentOverride"] = abxjsonschema.SchemaFor[NetworkSetUserAgentOverrideParams]() + c.commandResultSchemas["Network.setUserAgentOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetUserAgentOverrideResult]()) + c.commandParamsSchemas["Network.streamResourceContent"] = abxjsonschema.SchemaFor[NetworkStreamResourceContentParams]() + c.commandResultSchemas["Network.streamResourceContent"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkStreamResourceContentResult]()) + c.commandParamsSchemas["Network.getSecurityIsolationStatus"] = abxjsonschema.SchemaFor[NetworkGetSecurityIsolationStatusParams]() + c.commandResultSchemas["Network.getSecurityIsolationStatus"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkGetSecurityIsolationStatusResult]()) + c.commandParamsSchemas["Network.enableReportingApi"] = abxjsonschema.SchemaFor[NetworkEnableReportingAPIParams]() + c.commandResultSchemas["Network.enableReportingApi"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkEnableReportingAPIResult]()) + c.commandParamsSchemas["Network.enableDeviceBoundSessions"] = abxjsonschema.SchemaFor[NetworkEnableDeviceBoundSessionsParams]() + c.commandResultSchemas["Network.enableDeviceBoundSessions"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkEnableDeviceBoundSessionsResult]()) + c.commandParamsSchemas["Network.fetchSchemefulSite"] = abxjsonschema.SchemaFor[NetworkFetchSchemefulSiteParams]() + c.commandResultSchemas["Network.fetchSchemefulSite"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkFetchSchemefulSiteResult]()) + c.commandParamsSchemas["Network.loadNetworkResource"] = abxjsonschema.SchemaFor[NetworkLoadNetworkResourceParams]() + c.commandResultSchemas["Network.loadNetworkResource"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkLoadNetworkResourceResult]()) + c.commandParamsSchemas["Network.setCookieControls"] = abxjsonschema.SchemaFor[NetworkSetCookieControlsParams]() + c.commandResultSchemas["Network.setCookieControls"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSetCookieControlsResult]()) + c.commandParamsSchemas["Overlay.disable"] = abxjsonschema.SchemaFor[OverlayDisableParams]() + c.commandResultSchemas["Overlay.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayDisableResult]()) + c.commandParamsSchemas["Overlay.enable"] = abxjsonschema.SchemaFor[OverlayEnableParams]() + c.commandResultSchemas["Overlay.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayEnableResult]()) + c.commandParamsSchemas["Overlay.getHighlightObjectForTest"] = abxjsonschema.SchemaFor[OverlayGetHighlightObjectForTestParams]() + c.commandResultSchemas["Overlay.getHighlightObjectForTest"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayGetHighlightObjectForTestResult]()) + c.commandParamsSchemas["Overlay.getGridHighlightObjectsForTest"] = abxjsonschema.SchemaFor[OverlayGetGridHighlightObjectsForTestParams]() + c.commandResultSchemas["Overlay.getGridHighlightObjectsForTest"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayGetGridHighlightObjectsForTestResult]()) + c.commandParamsSchemas["Overlay.getSourceOrderHighlightObjectForTest"] = abxjsonschema.SchemaFor[OverlayGetSourceOrderHighlightObjectForTestParams]() + c.commandResultSchemas["Overlay.getSourceOrderHighlightObjectForTest"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayGetSourceOrderHighlightObjectForTestResult]()) + c.commandParamsSchemas["Overlay.hideHighlight"] = abxjsonschema.SchemaFor[OverlayHideHighlightParams]() + c.commandResultSchemas["Overlay.hideHighlight"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayHideHighlightResult]()) + c.commandParamsSchemas["Overlay.highlightFrame"] = abxjsonschema.SchemaFor[OverlayHighlightFrameParams]() + c.commandResultSchemas["Overlay.highlightFrame"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayHighlightFrameResult]()) + c.commandParamsSchemas["Overlay.highlightNode"] = abxjsonschema.SchemaFor[OverlayHighlightNodeParams]() + c.commandResultSchemas["Overlay.highlightNode"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayHighlightNodeResult]()) + c.commandParamsSchemas["Overlay.highlightQuad"] = abxjsonschema.SchemaFor[OverlayHighlightQuadParams]() + c.commandResultSchemas["Overlay.highlightQuad"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayHighlightQuadResult]()) + c.commandParamsSchemas["Overlay.highlightRect"] = abxjsonschema.SchemaFor[OverlayHighlightRectParams]() + c.commandResultSchemas["Overlay.highlightRect"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayHighlightRectResult]()) + c.commandParamsSchemas["Overlay.highlightSourceOrder"] = abxjsonschema.SchemaFor[OverlayHighlightSourceOrderParams]() + c.commandResultSchemas["Overlay.highlightSourceOrder"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayHighlightSourceOrderResult]()) + c.commandParamsSchemas["Overlay.setInspectMode"] = abxjsonschema.SchemaFor[OverlaySetInspectModeParams]() + c.commandResultSchemas["Overlay.setInspectMode"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetInspectModeResult]()) + c.commandParamsSchemas["Overlay.setShowAdHighlights"] = abxjsonschema.SchemaFor[OverlaySetShowAdHighlightsParams]() + c.commandResultSchemas["Overlay.setShowAdHighlights"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowAdHighlightsResult]()) + c.commandParamsSchemas["Overlay.setPausedInDebuggerMessage"] = abxjsonschema.SchemaFor[OverlaySetPausedInDebuggerMessageParams]() + c.commandResultSchemas["Overlay.setPausedInDebuggerMessage"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetPausedInDebuggerMessageResult]()) + c.commandParamsSchemas["Overlay.setShowDebugBorders"] = abxjsonschema.SchemaFor[OverlaySetShowDebugBordersParams]() + c.commandResultSchemas["Overlay.setShowDebugBorders"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowDebugBordersResult]()) + c.commandParamsSchemas["Overlay.setShowFPSCounter"] = abxjsonschema.SchemaFor[OverlaySetShowFPSCounterParams]() + c.commandResultSchemas["Overlay.setShowFPSCounter"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowFPSCounterResult]()) + c.commandParamsSchemas["Overlay.setShowGridOverlays"] = abxjsonschema.SchemaFor[OverlaySetShowGridOverlaysParams]() + c.commandResultSchemas["Overlay.setShowGridOverlays"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowGridOverlaysResult]()) + c.commandParamsSchemas["Overlay.setShowFlexOverlays"] = abxjsonschema.SchemaFor[OverlaySetShowFlexOverlaysParams]() + c.commandResultSchemas["Overlay.setShowFlexOverlays"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowFlexOverlaysResult]()) + c.commandParamsSchemas["Overlay.setShowScrollSnapOverlays"] = abxjsonschema.SchemaFor[OverlaySetShowScrollSnapOverlaysParams]() + c.commandResultSchemas["Overlay.setShowScrollSnapOverlays"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowScrollSnapOverlaysResult]()) + c.commandParamsSchemas["Overlay.setShowContainerQueryOverlays"] = abxjsonschema.SchemaFor[OverlaySetShowContainerQueryOverlaysParams]() + c.commandResultSchemas["Overlay.setShowContainerQueryOverlays"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowContainerQueryOverlaysResult]()) + c.commandParamsSchemas["Overlay.setShowInspectedElementAnchor"] = abxjsonschema.SchemaFor[OverlaySetShowInspectedElementAnchorParams]() + c.commandResultSchemas["Overlay.setShowInspectedElementAnchor"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowInspectedElementAnchorResult]()) + c.commandParamsSchemas["Overlay.setShowPaintRects"] = abxjsonschema.SchemaFor[OverlaySetShowPaintRectsParams]() + c.commandResultSchemas["Overlay.setShowPaintRects"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowPaintRectsResult]()) + c.commandParamsSchemas["Overlay.setShowLayoutShiftRegions"] = abxjsonschema.SchemaFor[OverlaySetShowLayoutShiftRegionsParams]() + c.commandResultSchemas["Overlay.setShowLayoutShiftRegions"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowLayoutShiftRegionsResult]()) + c.commandParamsSchemas["Overlay.setShowScrollBottleneckRects"] = abxjsonschema.SchemaFor[OverlaySetShowScrollBottleneckRectsParams]() + c.commandResultSchemas["Overlay.setShowScrollBottleneckRects"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowScrollBottleneckRectsResult]()) + c.commandParamsSchemas["Overlay.setShowHitTestBorders"] = abxjsonschema.SchemaFor[OverlaySetShowHitTestBordersParams]() + c.commandResultSchemas["Overlay.setShowHitTestBorders"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowHitTestBordersResult]()) + c.commandParamsSchemas["Overlay.setShowWebVitals"] = abxjsonschema.SchemaFor[OverlaySetShowWebVitalsParams]() + c.commandResultSchemas["Overlay.setShowWebVitals"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowWebVitalsResult]()) + c.commandParamsSchemas["Overlay.setShowViewportSizeOnResize"] = abxjsonschema.SchemaFor[OverlaySetShowViewportSizeOnResizeParams]() + c.commandResultSchemas["Overlay.setShowViewportSizeOnResize"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowViewportSizeOnResizeResult]()) + c.commandParamsSchemas["Overlay.setShowHinge"] = abxjsonschema.SchemaFor[OverlaySetShowHingeParams]() + c.commandResultSchemas["Overlay.setShowHinge"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowHingeResult]()) + c.commandParamsSchemas["Overlay.setShowIsolatedElements"] = abxjsonschema.SchemaFor[OverlaySetShowIsolatedElementsParams]() + c.commandResultSchemas["Overlay.setShowIsolatedElements"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowIsolatedElementsResult]()) + c.commandParamsSchemas["Overlay.setShowWindowControlsOverlay"] = abxjsonschema.SchemaFor[OverlaySetShowWindowControlsOverlayParams]() + c.commandResultSchemas["Overlay.setShowWindowControlsOverlay"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlaySetShowWindowControlsOverlayResult]()) + c.commandParamsSchemas["PWA.getOsAppState"] = abxjsonschema.SchemaFor[PWAGetOsAppStateParams]() + c.commandResultSchemas["PWA.getOsAppState"] = nativeResultSchema(abxjsonschema.SchemaFor[PWAGetOsAppStateResult]()) + c.commandParamsSchemas["PWA.install"] = abxjsonschema.SchemaFor[PWAInstallParams]() + c.commandResultSchemas["PWA.install"] = nativeResultSchema(abxjsonschema.SchemaFor[PWAInstallResult]()) + c.commandParamsSchemas["PWA.uninstall"] = abxjsonschema.SchemaFor[PWAUninstallParams]() + c.commandResultSchemas["PWA.uninstall"] = nativeResultSchema(abxjsonschema.SchemaFor[PWAUninstallResult]()) + c.commandParamsSchemas["PWA.launch"] = abxjsonschema.SchemaFor[PWALaunchParams]() + c.commandResultSchemas["PWA.launch"] = nativeResultSchema(abxjsonschema.SchemaFor[PWALaunchResult]()) + c.commandParamsSchemas["PWA.launchFilesInApp"] = abxjsonschema.SchemaFor[PWALaunchFilesInAppParams]() + c.commandResultSchemas["PWA.launchFilesInApp"] = nativeResultSchema(abxjsonschema.SchemaFor[PWALaunchFilesInAppResult]()) + c.commandParamsSchemas["PWA.openCurrentPageInApp"] = abxjsonschema.SchemaFor[PWAOpenCurrentPageInAppParams]() + c.commandResultSchemas["PWA.openCurrentPageInApp"] = nativeResultSchema(abxjsonschema.SchemaFor[PWAOpenCurrentPageInAppResult]()) + c.commandParamsSchemas["PWA.changeAppUserSettings"] = abxjsonschema.SchemaFor[PWAChangeAppUserSettingsParams]() + c.commandResultSchemas["PWA.changeAppUserSettings"] = nativeResultSchema(abxjsonschema.SchemaFor[PWAChangeAppUserSettingsResult]()) + c.commandParamsSchemas["Page.addScriptToEvaluateOnLoad"] = abxjsonschema.SchemaFor[PageAddScriptToEvaluateOnLoadParams]() + c.commandResultSchemas["Page.addScriptToEvaluateOnLoad"] = nativeResultSchema(abxjsonschema.SchemaFor[PageAddScriptToEvaluateOnLoadResult]()) + c.commandParamsSchemas["Page.addScriptToEvaluateOnNewDocument"] = abxjsonschema.SchemaFor[PageAddScriptToEvaluateOnNewDocumentParams]() + c.commandResultSchemas["Page.addScriptToEvaluateOnNewDocument"] = nativeResultSchema(abxjsonschema.SchemaFor[PageAddScriptToEvaluateOnNewDocumentResult]()) + c.commandParamsSchemas["Page.bringToFront"] = abxjsonschema.SchemaFor[PageBringToFrontParams]() + c.commandResultSchemas["Page.bringToFront"] = nativeResultSchema(abxjsonschema.SchemaFor[PageBringToFrontResult]()) + c.commandParamsSchemas["Page.captureScreenshot"] = abxjsonschema.SchemaFor[PageCaptureScreenshotParams]() + c.commandResultSchemas["Page.captureScreenshot"] = nativeResultSchema(abxjsonschema.SchemaFor[PageCaptureScreenshotResult]()) + c.commandParamsSchemas["Page.captureSnapshot"] = abxjsonschema.SchemaFor[PageCaptureSnapshotParams]() + c.commandResultSchemas["Page.captureSnapshot"] = nativeResultSchema(abxjsonschema.SchemaFor[PageCaptureSnapshotResult]()) + c.commandParamsSchemas["Page.clearDeviceMetricsOverride"] = abxjsonschema.SchemaFor[PageClearDeviceMetricsOverrideParams]() + c.commandResultSchemas["Page.clearDeviceMetricsOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[PageClearDeviceMetricsOverrideResult]()) + c.commandParamsSchemas["Page.clearDeviceOrientationOverride"] = abxjsonschema.SchemaFor[PageClearDeviceOrientationOverrideParams]() + c.commandResultSchemas["Page.clearDeviceOrientationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[PageClearDeviceOrientationOverrideResult]()) + c.commandParamsSchemas["Page.clearGeolocationOverride"] = abxjsonschema.SchemaFor[PageClearGeolocationOverrideParams]() + c.commandResultSchemas["Page.clearGeolocationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[PageClearGeolocationOverrideResult]()) + c.commandParamsSchemas["Page.createIsolatedWorld"] = abxjsonschema.SchemaFor[PageCreateIsolatedWorldParams]() + c.commandResultSchemas["Page.createIsolatedWorld"] = nativeResultSchema(abxjsonschema.SchemaFor[PageCreateIsolatedWorldResult]()) + c.commandParamsSchemas["Page.deleteCookie"] = abxjsonschema.SchemaFor[PageDeleteCookieParams]() + c.commandResultSchemas["Page.deleteCookie"] = nativeResultSchema(abxjsonschema.SchemaFor[PageDeleteCookieResult]()) + c.commandParamsSchemas["Page.disable"] = abxjsonschema.SchemaFor[PageDisableParams]() + c.commandResultSchemas["Page.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[PageDisableResult]()) + c.commandParamsSchemas["Page.enable"] = abxjsonschema.SchemaFor[PageEnableParams]() + c.commandResultSchemas["Page.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[PageEnableResult]()) + c.commandParamsSchemas["Page.getAppManifest"] = abxjsonschema.SchemaFor[PageGetAppManifestParams]() + c.commandResultSchemas["Page.getAppManifest"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetAppManifestResult]()) + c.commandParamsSchemas["Page.getInstallabilityErrors"] = abxjsonschema.SchemaFor[PageGetInstallabilityErrorsParams]() + c.commandResultSchemas["Page.getInstallabilityErrors"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetInstallabilityErrorsResult]()) + c.commandParamsSchemas["Page.getManifestIcons"] = abxjsonschema.SchemaFor[PageGetManifestIconsParams]() + c.commandResultSchemas["Page.getManifestIcons"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetManifestIconsResult]()) + c.commandParamsSchemas["Page.getAppId"] = abxjsonschema.SchemaFor[PageGetAppIDParams]() + c.commandResultSchemas["Page.getAppId"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetAppIDResult]()) + c.commandParamsSchemas["Page.getAdScriptAncestry"] = abxjsonschema.SchemaFor[PageGetAdScriptAncestryParams]() + c.commandResultSchemas["Page.getAdScriptAncestry"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetAdScriptAncestryResult]()) + c.commandParamsSchemas["Page.getFrameTree"] = abxjsonschema.SchemaFor[PageGetFrameTreeParams]() + c.commandResultSchemas["Page.getFrameTree"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetFrameTreeResult]()) + c.commandParamsSchemas["Page.getLayoutMetrics"] = abxjsonschema.SchemaFor[PageGetLayoutMetricsParams]() + c.commandResultSchemas["Page.getLayoutMetrics"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetLayoutMetricsResult]()) + c.commandParamsSchemas["Page.getNavigationHistory"] = abxjsonschema.SchemaFor[PageGetNavigationHistoryParams]() + c.commandResultSchemas["Page.getNavigationHistory"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetNavigationHistoryResult]()) + c.commandParamsSchemas["Page.resetNavigationHistory"] = abxjsonschema.SchemaFor[PageResetNavigationHistoryParams]() + c.commandResultSchemas["Page.resetNavigationHistory"] = nativeResultSchema(abxjsonschema.SchemaFor[PageResetNavigationHistoryResult]()) + c.commandParamsSchemas["Page.getResourceContent"] = abxjsonschema.SchemaFor[PageGetResourceContentParams]() + c.commandResultSchemas["Page.getResourceContent"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetResourceContentResult]()) + c.commandParamsSchemas["Page.getResourceTree"] = abxjsonschema.SchemaFor[PageGetResourceTreeParams]() + c.commandResultSchemas["Page.getResourceTree"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetResourceTreeResult]()) + c.commandParamsSchemas["Page.handleJavaScriptDialog"] = abxjsonschema.SchemaFor[PageHandleJavaScriptDialogParams]() + c.commandResultSchemas["Page.handleJavaScriptDialog"] = nativeResultSchema(abxjsonschema.SchemaFor[PageHandleJavaScriptDialogResult]()) + c.commandParamsSchemas["Page.navigate"] = abxjsonschema.SchemaFor[PageNavigateParams]() + c.commandResultSchemas["Page.navigate"] = nativeResultSchema(abxjsonschema.SchemaFor[PageNavigateResult]()) + c.commandParamsSchemas["Page.navigateToHistoryEntry"] = abxjsonschema.SchemaFor[PageNavigateToHistoryEntryParams]() + c.commandResultSchemas["Page.navigateToHistoryEntry"] = nativeResultSchema(abxjsonschema.SchemaFor[PageNavigateToHistoryEntryResult]()) + c.commandParamsSchemas["Page.printToPDF"] = abxjsonschema.SchemaFor[PagePrintToPDFParams]() + c.commandResultSchemas["Page.printToPDF"] = nativeResultSchema(abxjsonschema.SchemaFor[PagePrintToPDFResult]()) + c.commandParamsSchemas["Page.reload"] = abxjsonschema.SchemaFor[PageReloadParams]() + c.commandResultSchemas["Page.reload"] = nativeResultSchema(abxjsonschema.SchemaFor[PageReloadResult]()) + c.commandParamsSchemas["Page.removeScriptToEvaluateOnLoad"] = abxjsonschema.SchemaFor[PageRemoveScriptToEvaluateOnLoadParams]() + c.commandResultSchemas["Page.removeScriptToEvaluateOnLoad"] = nativeResultSchema(abxjsonschema.SchemaFor[PageRemoveScriptToEvaluateOnLoadResult]()) + c.commandParamsSchemas["Page.removeScriptToEvaluateOnNewDocument"] = abxjsonschema.SchemaFor[PageRemoveScriptToEvaluateOnNewDocumentParams]() + c.commandResultSchemas["Page.removeScriptToEvaluateOnNewDocument"] = nativeResultSchema(abxjsonschema.SchemaFor[PageRemoveScriptToEvaluateOnNewDocumentResult]()) + c.commandParamsSchemas["Page.screencastFrameAck"] = abxjsonschema.SchemaFor[PageScreencastFrameAckParams]() + c.commandResultSchemas["Page.screencastFrameAck"] = nativeResultSchema(abxjsonschema.SchemaFor[PageScreencastFrameAckResult]()) + c.commandParamsSchemas["Page.searchInResource"] = abxjsonschema.SchemaFor[PageSearchInResourceParams]() + c.commandResultSchemas["Page.searchInResource"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSearchInResourceResult]()) + c.commandParamsSchemas["Page.setAdBlockingEnabled"] = abxjsonschema.SchemaFor[PageSetAdBlockingEnabledParams]() + c.commandResultSchemas["Page.setAdBlockingEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetAdBlockingEnabledResult]()) + c.commandParamsSchemas["Page.setBypassCSP"] = abxjsonschema.SchemaFor[PageSetBypassCSPParams]() + c.commandResultSchemas["Page.setBypassCSP"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetBypassCSPResult]()) + c.commandParamsSchemas["Page.getPermissionsPolicyState"] = abxjsonschema.SchemaFor[PageGetPermissionsPolicyStateParams]() + c.commandResultSchemas["Page.getPermissionsPolicyState"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetPermissionsPolicyStateResult]()) + c.commandParamsSchemas["Page.getOriginTrials"] = abxjsonschema.SchemaFor[PageGetOriginTrialsParams]() + c.commandResultSchemas["Page.getOriginTrials"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetOriginTrialsResult]()) + c.commandParamsSchemas["Page.setDeviceMetricsOverride"] = abxjsonschema.SchemaFor[PageSetDeviceMetricsOverrideParams]() + c.commandResultSchemas["Page.setDeviceMetricsOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetDeviceMetricsOverrideResult]()) + c.commandParamsSchemas["Page.setDeviceOrientationOverride"] = abxjsonschema.SchemaFor[PageSetDeviceOrientationOverrideParams]() + c.commandResultSchemas["Page.setDeviceOrientationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetDeviceOrientationOverrideResult]()) + c.commandParamsSchemas["Page.setFontFamilies"] = abxjsonschema.SchemaFor[PageSetFontFamiliesParams]() + c.commandResultSchemas["Page.setFontFamilies"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetFontFamiliesResult]()) + c.commandParamsSchemas["Page.setFontSizes"] = abxjsonschema.SchemaFor[PageSetFontSizesParams]() + c.commandResultSchemas["Page.setFontSizes"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetFontSizesResult]()) + c.commandParamsSchemas["Page.setDocumentContent"] = abxjsonschema.SchemaFor[PageSetDocumentContentParams]() + c.commandResultSchemas["Page.setDocumentContent"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetDocumentContentResult]()) + c.commandParamsSchemas["Page.setDownloadBehavior"] = abxjsonschema.SchemaFor[PageSetDownloadBehaviorParams]() + c.commandResultSchemas["Page.setDownloadBehavior"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetDownloadBehaviorResult]()) + c.commandParamsSchemas["Page.setGeolocationOverride"] = abxjsonschema.SchemaFor[PageSetGeolocationOverrideParams]() + c.commandResultSchemas["Page.setGeolocationOverride"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetGeolocationOverrideResult]()) + c.commandParamsSchemas["Page.setLifecycleEventsEnabled"] = abxjsonschema.SchemaFor[PageSetLifecycleEventsEnabledParams]() + c.commandResultSchemas["Page.setLifecycleEventsEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetLifecycleEventsEnabledResult]()) + c.commandParamsSchemas["Page.setTouchEmulationEnabled"] = abxjsonschema.SchemaFor[PageSetTouchEmulationEnabledParams]() + c.commandResultSchemas["Page.setTouchEmulationEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetTouchEmulationEnabledResult]()) + c.commandParamsSchemas["Page.startScreencast"] = abxjsonschema.SchemaFor[PageStartScreencastParams]() + c.commandResultSchemas["Page.startScreencast"] = nativeResultSchema(abxjsonschema.SchemaFor[PageStartScreencastResult]()) + c.commandParamsSchemas["Page.stopLoading"] = abxjsonschema.SchemaFor[PageStopLoadingParams]() + c.commandResultSchemas["Page.stopLoading"] = nativeResultSchema(abxjsonschema.SchemaFor[PageStopLoadingResult]()) + c.commandParamsSchemas["Page.crash"] = abxjsonschema.SchemaFor[PageCrashParams]() + c.commandResultSchemas["Page.crash"] = nativeResultSchema(abxjsonschema.SchemaFor[PageCrashResult]()) + c.commandParamsSchemas["Page.close"] = abxjsonschema.SchemaFor[PageCloseParams]() + c.commandResultSchemas["Page.close"] = nativeResultSchema(abxjsonschema.SchemaFor[PageCloseResult]()) + c.commandParamsSchemas["Page.setWebLifecycleState"] = abxjsonschema.SchemaFor[PageSetWebLifecycleStateParams]() + c.commandResultSchemas["Page.setWebLifecycleState"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetWebLifecycleStateResult]()) + c.commandParamsSchemas["Page.stopScreencast"] = abxjsonschema.SchemaFor[PageStopScreencastParams]() + c.commandResultSchemas["Page.stopScreencast"] = nativeResultSchema(abxjsonschema.SchemaFor[PageStopScreencastResult]()) + c.commandParamsSchemas["Page.produceCompilationCache"] = abxjsonschema.SchemaFor[PageProduceCompilationCacheParams]() + c.commandResultSchemas["Page.produceCompilationCache"] = nativeResultSchema(abxjsonschema.SchemaFor[PageProduceCompilationCacheResult]()) + c.commandParamsSchemas["Page.addCompilationCache"] = abxjsonschema.SchemaFor[PageAddCompilationCacheParams]() + c.commandResultSchemas["Page.addCompilationCache"] = nativeResultSchema(abxjsonschema.SchemaFor[PageAddCompilationCacheResult]()) + c.commandParamsSchemas["Page.clearCompilationCache"] = abxjsonschema.SchemaFor[PageClearCompilationCacheParams]() + c.commandResultSchemas["Page.clearCompilationCache"] = nativeResultSchema(abxjsonschema.SchemaFor[PageClearCompilationCacheResult]()) + c.commandParamsSchemas["Page.setSPCTransactionMode"] = abxjsonschema.SchemaFor[PageSetSPCTransactionModeParams]() + c.commandResultSchemas["Page.setSPCTransactionMode"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetSPCTransactionModeResult]()) + c.commandParamsSchemas["Page.setRPHRegistrationMode"] = abxjsonschema.SchemaFor[PageSetRPHRegistrationModeParams]() + c.commandResultSchemas["Page.setRPHRegistrationMode"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetRPHRegistrationModeResult]()) + c.commandParamsSchemas["Page.generateTestReport"] = abxjsonschema.SchemaFor[PageGenerateTestReportParams]() + c.commandResultSchemas["Page.generateTestReport"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGenerateTestReportResult]()) + c.commandParamsSchemas["Page.waitForDebugger"] = abxjsonschema.SchemaFor[PageWaitForDebuggerParams]() + c.commandResultSchemas["Page.waitForDebugger"] = nativeResultSchema(abxjsonschema.SchemaFor[PageWaitForDebuggerResult]()) + c.commandParamsSchemas["Page.setInterceptFileChooserDialog"] = abxjsonschema.SchemaFor[PageSetInterceptFileChooserDialogParams]() + c.commandResultSchemas["Page.setInterceptFileChooserDialog"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetInterceptFileChooserDialogResult]()) + c.commandParamsSchemas["Page.setPrerenderingAllowed"] = abxjsonschema.SchemaFor[PageSetPrerenderingAllowedParams]() + c.commandResultSchemas["Page.setPrerenderingAllowed"] = nativeResultSchema(abxjsonschema.SchemaFor[PageSetPrerenderingAllowedResult]()) + c.commandParamsSchemas["Page.getAnnotatedPageContent"] = abxjsonschema.SchemaFor[PageGetAnnotatedPageContentParams]() + c.commandResultSchemas["Page.getAnnotatedPageContent"] = nativeResultSchema(abxjsonschema.SchemaFor[PageGetAnnotatedPageContentResult]()) + c.commandParamsSchemas["Performance.disable"] = abxjsonschema.SchemaFor[PerformanceDisableParams]() + c.commandResultSchemas["Performance.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[PerformanceDisableResult]()) + c.commandParamsSchemas["Performance.enable"] = abxjsonschema.SchemaFor[PerformanceEnableParams]() + c.commandResultSchemas["Performance.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[PerformanceEnableResult]()) + c.commandParamsSchemas["Performance.setTimeDomain"] = abxjsonschema.SchemaFor[PerformanceSetTimeDomainParams]() + c.commandResultSchemas["Performance.setTimeDomain"] = nativeResultSchema(abxjsonschema.SchemaFor[PerformanceSetTimeDomainResult]()) + c.commandParamsSchemas["Performance.getMetrics"] = abxjsonschema.SchemaFor[PerformanceGetMetricsParams]() + c.commandResultSchemas["Performance.getMetrics"] = nativeResultSchema(abxjsonschema.SchemaFor[PerformanceGetMetricsResult]()) + c.commandParamsSchemas["PerformanceTimeline.enable"] = abxjsonschema.SchemaFor[PerformanceTimelineEnableParams]() + c.commandResultSchemas["PerformanceTimeline.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[PerformanceTimelineEnableResult]()) + c.commandParamsSchemas["Preload.enable"] = abxjsonschema.SchemaFor[PreloadEnableParams]() + c.commandResultSchemas["Preload.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[PreloadEnableResult]()) + c.commandParamsSchemas["Preload.disable"] = abxjsonschema.SchemaFor[PreloadDisableParams]() + c.commandResultSchemas["Preload.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[PreloadDisableResult]()) + c.commandParamsSchemas["Profiler.disable"] = abxjsonschema.SchemaFor[ProfilerDisableParams]() + c.commandResultSchemas["Profiler.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerDisableResult]()) + c.commandParamsSchemas["Profiler.enable"] = abxjsonschema.SchemaFor[ProfilerEnableParams]() + c.commandResultSchemas["Profiler.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerEnableResult]()) + c.commandParamsSchemas["Profiler.getBestEffortCoverage"] = abxjsonschema.SchemaFor[ProfilerGetBestEffortCoverageParams]() + c.commandResultSchemas["Profiler.getBestEffortCoverage"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerGetBestEffortCoverageResult]()) + c.commandParamsSchemas["Profiler.setSamplingInterval"] = abxjsonschema.SchemaFor[ProfilerSetSamplingIntervalParams]() + c.commandResultSchemas["Profiler.setSamplingInterval"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerSetSamplingIntervalResult]()) + c.commandParamsSchemas["Profiler.start"] = abxjsonschema.SchemaFor[ProfilerStartParams]() + c.commandResultSchemas["Profiler.start"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerStartResult]()) + c.commandParamsSchemas["Profiler.startPreciseCoverage"] = abxjsonschema.SchemaFor[ProfilerStartPreciseCoverageParams]() + c.commandResultSchemas["Profiler.startPreciseCoverage"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerStartPreciseCoverageResult]()) + c.commandParamsSchemas["Profiler.stop"] = abxjsonschema.SchemaFor[ProfilerStopParams]() + c.commandResultSchemas["Profiler.stop"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerStopResult]()) + c.commandParamsSchemas["Profiler.stopPreciseCoverage"] = abxjsonschema.SchemaFor[ProfilerStopPreciseCoverageParams]() + c.commandResultSchemas["Profiler.stopPreciseCoverage"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerStopPreciseCoverageResult]()) + c.commandParamsSchemas["Profiler.takePreciseCoverage"] = abxjsonschema.SchemaFor[ProfilerTakePreciseCoverageParams]() + c.commandResultSchemas["Profiler.takePreciseCoverage"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerTakePreciseCoverageResult]()) + c.commandParamsSchemas["Runtime.awaitPromise"] = abxjsonschema.SchemaFor[RuntimeAwaitPromiseParams]() + c.commandResultSchemas["Runtime.awaitPromise"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeAwaitPromiseResult]()) + c.commandParamsSchemas["Runtime.callFunctionOn"] = abxjsonschema.SchemaFor[RuntimeCallFunctionOnParams]() + c.commandResultSchemas["Runtime.callFunctionOn"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeCallFunctionOnResult]()) + c.commandParamsSchemas["Runtime.compileScript"] = abxjsonschema.SchemaFor[RuntimeCompileScriptParams]() + c.commandResultSchemas["Runtime.compileScript"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeCompileScriptResult]()) + c.commandParamsSchemas["Runtime.disable"] = abxjsonschema.SchemaFor[RuntimeDisableParams]() + c.commandResultSchemas["Runtime.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeDisableResult]()) + c.commandParamsSchemas["Runtime.discardConsoleEntries"] = abxjsonschema.SchemaFor[RuntimeDiscardConsoleEntriesParams]() + c.commandResultSchemas["Runtime.discardConsoleEntries"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeDiscardConsoleEntriesResult]()) + c.commandParamsSchemas["Runtime.enable"] = abxjsonschema.SchemaFor[RuntimeEnableParams]() + c.commandResultSchemas["Runtime.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeEnableResult]()) + c.commandParamsSchemas["Runtime.evaluate"] = abxjsonschema.SchemaFor[RuntimeEvaluateParams]() + c.commandResultSchemas["Runtime.evaluate"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeEvaluateResult]()) + c.commandParamsSchemas["Runtime.getIsolateId"] = abxjsonschema.SchemaFor[RuntimeGetIsolateIDParams]() + c.commandResultSchemas["Runtime.getIsolateId"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeGetIsolateIDResult]()) + c.commandParamsSchemas["Runtime.getHeapUsage"] = abxjsonschema.SchemaFor[RuntimeGetHeapUsageParams]() + c.commandResultSchemas["Runtime.getHeapUsage"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeGetHeapUsageResult]()) + c.commandParamsSchemas["Runtime.getProperties"] = abxjsonschema.SchemaFor[RuntimeGetPropertiesParams]() + c.commandResultSchemas["Runtime.getProperties"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeGetPropertiesResult]()) + c.commandParamsSchemas["Runtime.globalLexicalScopeNames"] = abxjsonschema.SchemaFor[RuntimeGlobalLexicalScopeNamesParams]() + c.commandResultSchemas["Runtime.globalLexicalScopeNames"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeGlobalLexicalScopeNamesResult]()) + c.commandParamsSchemas["Runtime.queryObjects"] = abxjsonschema.SchemaFor[RuntimeQueryObjectsParams]() + c.commandResultSchemas["Runtime.queryObjects"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeQueryObjectsResult]()) + c.commandParamsSchemas["Runtime.releaseObject"] = abxjsonschema.SchemaFor[RuntimeReleaseObjectParams]() + c.commandResultSchemas["Runtime.releaseObject"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeReleaseObjectResult]()) + c.commandParamsSchemas["Runtime.releaseObjectGroup"] = abxjsonschema.SchemaFor[RuntimeReleaseObjectGroupParams]() + c.commandResultSchemas["Runtime.releaseObjectGroup"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeReleaseObjectGroupResult]()) + c.commandParamsSchemas["Runtime.runIfWaitingForDebugger"] = abxjsonschema.SchemaFor[RuntimeRunIfWaitingForDebuggerParams]() + c.commandResultSchemas["Runtime.runIfWaitingForDebugger"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeRunIfWaitingForDebuggerResult]()) + c.commandParamsSchemas["Runtime.runScript"] = abxjsonschema.SchemaFor[RuntimeRunScriptParams]() + c.commandResultSchemas["Runtime.runScript"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeRunScriptResult]()) + c.commandParamsSchemas["Runtime.setAsyncCallStackDepth"] = abxjsonschema.SchemaFor[RuntimeSetAsyncCallStackDepthParams]() + c.commandResultSchemas["Runtime.setAsyncCallStackDepth"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeSetAsyncCallStackDepthResult]()) + c.commandParamsSchemas["Runtime.setCustomObjectFormatterEnabled"] = abxjsonschema.SchemaFor[RuntimeSetCustomObjectFormatterEnabledParams]() + c.commandResultSchemas["Runtime.setCustomObjectFormatterEnabled"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeSetCustomObjectFormatterEnabledResult]()) + c.commandParamsSchemas["Runtime.setMaxCallStackSizeToCapture"] = abxjsonschema.SchemaFor[RuntimeSetMaxCallStackSizeToCaptureParams]() + c.commandResultSchemas["Runtime.setMaxCallStackSizeToCapture"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeSetMaxCallStackSizeToCaptureResult]()) + c.commandParamsSchemas["Runtime.terminateExecution"] = abxjsonschema.SchemaFor[RuntimeTerminateExecutionParams]() + c.commandResultSchemas["Runtime.terminateExecution"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeTerminateExecutionResult]()) + c.commandParamsSchemas["Runtime.addBinding"] = abxjsonschema.SchemaFor[RuntimeAddBindingParams]() + c.commandResultSchemas["Runtime.addBinding"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeAddBindingResult]()) + c.commandParamsSchemas["Runtime.removeBinding"] = abxjsonschema.SchemaFor[RuntimeRemoveBindingParams]() + c.commandResultSchemas["Runtime.removeBinding"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeRemoveBindingResult]()) + c.commandParamsSchemas["Runtime.getExceptionDetails"] = abxjsonschema.SchemaFor[RuntimeGetExceptionDetailsParams]() + c.commandResultSchemas["Runtime.getExceptionDetails"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeGetExceptionDetailsResult]()) + c.commandParamsSchemas["Schema.getDomains"] = abxjsonschema.SchemaFor[SchemaGetDomainsParams]() + c.commandResultSchemas["Schema.getDomains"] = nativeResultSchema(abxjsonschema.SchemaFor[SchemaGetDomainsResult]()) + c.commandParamsSchemas["Security.disable"] = abxjsonschema.SchemaFor[SecurityDisableParams]() + c.commandResultSchemas["Security.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[SecurityDisableResult]()) + c.commandParamsSchemas["Security.enable"] = abxjsonschema.SchemaFor[SecurityEnableParams]() + c.commandResultSchemas["Security.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[SecurityEnableResult]()) + c.commandParamsSchemas["Security.setIgnoreCertificateErrors"] = abxjsonschema.SchemaFor[SecuritySetIgnoreCertificateErrorsParams]() + c.commandResultSchemas["Security.setIgnoreCertificateErrors"] = nativeResultSchema(abxjsonschema.SchemaFor[SecuritySetIgnoreCertificateErrorsResult]()) + c.commandParamsSchemas["Security.handleCertificateError"] = abxjsonschema.SchemaFor[SecurityHandleCertificateErrorParams]() + c.commandResultSchemas["Security.handleCertificateError"] = nativeResultSchema(abxjsonschema.SchemaFor[SecurityHandleCertificateErrorResult]()) + c.commandParamsSchemas["Security.setOverrideCertificateErrors"] = abxjsonschema.SchemaFor[SecuritySetOverrideCertificateErrorsParams]() + c.commandResultSchemas["Security.setOverrideCertificateErrors"] = nativeResultSchema(abxjsonschema.SchemaFor[SecuritySetOverrideCertificateErrorsResult]()) + c.commandParamsSchemas["ServiceWorker.deliverPushMessage"] = abxjsonschema.SchemaFor[ServiceWorkerDeliverPushMessageParams]() + c.commandResultSchemas["ServiceWorker.deliverPushMessage"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerDeliverPushMessageResult]()) + c.commandParamsSchemas["ServiceWorker.disable"] = abxjsonschema.SchemaFor[ServiceWorkerDisableParams]() + c.commandResultSchemas["ServiceWorker.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerDisableResult]()) + c.commandParamsSchemas["ServiceWorker.dispatchSyncEvent"] = abxjsonschema.SchemaFor[ServiceWorkerDispatchSyncEventParams]() + c.commandResultSchemas["ServiceWorker.dispatchSyncEvent"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerDispatchSyncEventResult]()) + c.commandParamsSchemas["ServiceWorker.dispatchPeriodicSyncEvent"] = abxjsonschema.SchemaFor[ServiceWorkerDispatchPeriodicSyncEventParams]() + c.commandResultSchemas["ServiceWorker.dispatchPeriodicSyncEvent"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerDispatchPeriodicSyncEventResult]()) + c.commandParamsSchemas["ServiceWorker.enable"] = abxjsonschema.SchemaFor[ServiceWorkerEnableParams]() + c.commandResultSchemas["ServiceWorker.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerEnableResult]()) + c.commandParamsSchemas["ServiceWorker.setForceUpdateOnPageLoad"] = abxjsonschema.SchemaFor[ServiceWorkerSetForceUpdateOnPageLoadParams]() + c.commandResultSchemas["ServiceWorker.setForceUpdateOnPageLoad"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerSetForceUpdateOnPageLoadResult]()) + c.commandParamsSchemas["ServiceWorker.skipWaiting"] = abxjsonschema.SchemaFor[ServiceWorkerSkipWaitingParams]() + c.commandResultSchemas["ServiceWorker.skipWaiting"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerSkipWaitingResult]()) + c.commandParamsSchemas["ServiceWorker.startWorker"] = abxjsonschema.SchemaFor[ServiceWorkerStartWorkerParams]() + c.commandResultSchemas["ServiceWorker.startWorker"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerStartWorkerResult]()) + c.commandParamsSchemas["ServiceWorker.stopAllWorkers"] = abxjsonschema.SchemaFor[ServiceWorkerStopAllWorkersParams]() + c.commandResultSchemas["ServiceWorker.stopAllWorkers"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerStopAllWorkersResult]()) + c.commandParamsSchemas["ServiceWorker.stopWorker"] = abxjsonschema.SchemaFor[ServiceWorkerStopWorkerParams]() + c.commandResultSchemas["ServiceWorker.stopWorker"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerStopWorkerResult]()) + c.commandParamsSchemas["ServiceWorker.unregister"] = abxjsonschema.SchemaFor[ServiceWorkerUnregisterParams]() + c.commandResultSchemas["ServiceWorker.unregister"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerUnregisterResult]()) + c.commandParamsSchemas["ServiceWorker.updateRegistration"] = abxjsonschema.SchemaFor[ServiceWorkerUpdateRegistrationParams]() + c.commandResultSchemas["ServiceWorker.updateRegistration"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerUpdateRegistrationResult]()) + c.commandParamsSchemas["SmartCardEmulation.enable"] = abxjsonschema.SchemaFor[SmartCardEmulationEnableParams]() + c.commandResultSchemas["SmartCardEmulation.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationEnableResult]()) + c.commandParamsSchemas["SmartCardEmulation.disable"] = abxjsonschema.SchemaFor[SmartCardEmulationDisableParams]() + c.commandResultSchemas["SmartCardEmulation.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationDisableResult]()) + c.commandParamsSchemas["SmartCardEmulation.reportEstablishContextResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportEstablishContextResultParams]() + c.commandResultSchemas["SmartCardEmulation.reportEstablishContextResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportEstablishContextResultResult]()) + c.commandParamsSchemas["SmartCardEmulation.reportReleaseContextResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportReleaseContextResultParams]() + c.commandResultSchemas["SmartCardEmulation.reportReleaseContextResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportReleaseContextResultResult]()) + c.commandParamsSchemas["SmartCardEmulation.reportListReadersResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportListReadersResultParams]() + c.commandResultSchemas["SmartCardEmulation.reportListReadersResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportListReadersResultResult]()) + c.commandParamsSchemas["SmartCardEmulation.reportGetStatusChangeResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportGetStatusChangeResultParams]() + c.commandResultSchemas["SmartCardEmulation.reportGetStatusChangeResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportGetStatusChangeResultResult]()) + c.commandParamsSchemas["SmartCardEmulation.reportBeginTransactionResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportBeginTransactionResultParams]() + c.commandResultSchemas["SmartCardEmulation.reportBeginTransactionResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportBeginTransactionResultResult]()) + c.commandParamsSchemas["SmartCardEmulation.reportPlainResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportPlainResultParams]() + c.commandResultSchemas["SmartCardEmulation.reportPlainResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportPlainResultResult]()) + c.commandParamsSchemas["SmartCardEmulation.reportConnectResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportConnectResultParams]() + c.commandResultSchemas["SmartCardEmulation.reportConnectResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportConnectResultResult]()) + c.commandParamsSchemas["SmartCardEmulation.reportDataResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportDataResultParams]() + c.commandResultSchemas["SmartCardEmulation.reportDataResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportDataResultResult]()) + c.commandParamsSchemas["SmartCardEmulation.reportStatusResult"] = abxjsonschema.SchemaFor[SmartCardEmulationReportStatusResultParams]() + c.commandResultSchemas["SmartCardEmulation.reportStatusResult"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportStatusResultResult]()) + c.commandParamsSchemas["SmartCardEmulation.reportError"] = abxjsonschema.SchemaFor[SmartCardEmulationReportErrorParams]() + c.commandResultSchemas["SmartCardEmulation.reportError"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReportErrorResult]()) + c.commandParamsSchemas["Storage.getStorageKeyForFrame"] = abxjsonschema.SchemaFor[StorageGetStorageKeyForFrameParams]() + c.commandResultSchemas["Storage.getStorageKeyForFrame"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetStorageKeyForFrameResult]()) + c.commandParamsSchemas["Storage.getStorageKey"] = abxjsonschema.SchemaFor[StorageGetStorageKeyParams]() + c.commandResultSchemas["Storage.getStorageKey"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetStorageKeyResult]()) + c.commandParamsSchemas["Storage.clearDataForOrigin"] = abxjsonschema.SchemaFor[StorageClearDataForOriginParams]() + c.commandResultSchemas["Storage.clearDataForOrigin"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageClearDataForOriginResult]()) + c.commandParamsSchemas["Storage.clearDataForStorageKey"] = abxjsonschema.SchemaFor[StorageClearDataForStorageKeyParams]() + c.commandResultSchemas["Storage.clearDataForStorageKey"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageClearDataForStorageKeyResult]()) + c.commandParamsSchemas["Storage.getCookies"] = abxjsonschema.SchemaFor[StorageGetCookiesParams]() + c.commandResultSchemas["Storage.getCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetCookiesResult]()) + c.commandParamsSchemas["Storage.setCookies"] = abxjsonschema.SchemaFor[StorageSetCookiesParams]() + c.commandResultSchemas["Storage.setCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetCookiesResult]()) + c.commandParamsSchemas["Storage.clearCookies"] = abxjsonschema.SchemaFor[StorageClearCookiesParams]() + c.commandResultSchemas["Storage.clearCookies"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageClearCookiesResult]()) + c.commandParamsSchemas["Storage.getUsageAndQuota"] = abxjsonschema.SchemaFor[StorageGetUsageAndQuotaParams]() + c.commandResultSchemas["Storage.getUsageAndQuota"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetUsageAndQuotaResult]()) + c.commandParamsSchemas["Storage.overrideQuotaForOrigin"] = abxjsonschema.SchemaFor[StorageOverrideQuotaForOriginParams]() + c.commandResultSchemas["Storage.overrideQuotaForOrigin"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageOverrideQuotaForOriginResult]()) + c.commandParamsSchemas["Storage.trackCacheStorageForOrigin"] = abxjsonschema.SchemaFor[StorageTrackCacheStorageForOriginParams]() + c.commandResultSchemas["Storage.trackCacheStorageForOrigin"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageTrackCacheStorageForOriginResult]()) + c.commandParamsSchemas["Storage.trackCacheStorageForStorageKey"] = abxjsonschema.SchemaFor[StorageTrackCacheStorageForStorageKeyParams]() + c.commandResultSchemas["Storage.trackCacheStorageForStorageKey"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageTrackCacheStorageForStorageKeyResult]()) + c.commandParamsSchemas["Storage.trackIndexedDBForOrigin"] = abxjsonschema.SchemaFor[StorageTrackIndexedDBForOriginParams]() + c.commandResultSchemas["Storage.trackIndexedDBForOrigin"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageTrackIndexedDBForOriginResult]()) + c.commandParamsSchemas["Storage.trackIndexedDBForStorageKey"] = abxjsonschema.SchemaFor[StorageTrackIndexedDBForStorageKeyParams]() + c.commandResultSchemas["Storage.trackIndexedDBForStorageKey"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageTrackIndexedDBForStorageKeyResult]()) + c.commandParamsSchemas["Storage.untrackCacheStorageForOrigin"] = abxjsonschema.SchemaFor[StorageUntrackCacheStorageForOriginParams]() + c.commandResultSchemas["Storage.untrackCacheStorageForOrigin"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageUntrackCacheStorageForOriginResult]()) + c.commandParamsSchemas["Storage.untrackCacheStorageForStorageKey"] = abxjsonschema.SchemaFor[StorageUntrackCacheStorageForStorageKeyParams]() + c.commandResultSchemas["Storage.untrackCacheStorageForStorageKey"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageUntrackCacheStorageForStorageKeyResult]()) + c.commandParamsSchemas["Storage.untrackIndexedDBForOrigin"] = abxjsonschema.SchemaFor[StorageUntrackIndexedDBForOriginParams]() + c.commandResultSchemas["Storage.untrackIndexedDBForOrigin"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageUntrackIndexedDBForOriginResult]()) + c.commandParamsSchemas["Storage.untrackIndexedDBForStorageKey"] = abxjsonschema.SchemaFor[StorageUntrackIndexedDBForStorageKeyParams]() + c.commandResultSchemas["Storage.untrackIndexedDBForStorageKey"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageUntrackIndexedDBForStorageKeyResult]()) + c.commandParamsSchemas["Storage.getTrustTokens"] = abxjsonschema.SchemaFor[StorageGetTrustTokensParams]() + c.commandResultSchemas["Storage.getTrustTokens"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetTrustTokensResult]()) + c.commandParamsSchemas["Storage.clearTrustTokens"] = abxjsonschema.SchemaFor[StorageClearTrustTokensParams]() + c.commandResultSchemas["Storage.clearTrustTokens"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageClearTrustTokensResult]()) + c.commandParamsSchemas["Storage.getInterestGroupDetails"] = abxjsonschema.SchemaFor[StorageGetInterestGroupDetailsParams]() + c.commandResultSchemas["Storage.getInterestGroupDetails"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetInterestGroupDetailsResult]()) + c.commandParamsSchemas["Storage.setInterestGroupTracking"] = abxjsonschema.SchemaFor[StorageSetInterestGroupTrackingParams]() + c.commandResultSchemas["Storage.setInterestGroupTracking"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetInterestGroupTrackingResult]()) + c.commandParamsSchemas["Storage.setInterestGroupAuctionTracking"] = abxjsonschema.SchemaFor[StorageSetInterestGroupAuctionTrackingParams]() + c.commandResultSchemas["Storage.setInterestGroupAuctionTracking"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetInterestGroupAuctionTrackingResult]()) + c.commandParamsSchemas["Storage.getSharedStorageMetadata"] = abxjsonschema.SchemaFor[StorageGetSharedStorageMetadataParams]() + c.commandResultSchemas["Storage.getSharedStorageMetadata"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetSharedStorageMetadataResult]()) + c.commandParamsSchemas["Storage.getSharedStorageEntries"] = abxjsonschema.SchemaFor[StorageGetSharedStorageEntriesParams]() + c.commandResultSchemas["Storage.getSharedStorageEntries"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetSharedStorageEntriesResult]()) + c.commandParamsSchemas["Storage.setSharedStorageEntry"] = abxjsonschema.SchemaFor[StorageSetSharedStorageEntryParams]() + c.commandResultSchemas["Storage.setSharedStorageEntry"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetSharedStorageEntryResult]()) + c.commandParamsSchemas["Storage.deleteSharedStorageEntry"] = abxjsonschema.SchemaFor[StorageDeleteSharedStorageEntryParams]() + c.commandResultSchemas["Storage.deleteSharedStorageEntry"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageDeleteSharedStorageEntryResult]()) + c.commandParamsSchemas["Storage.clearSharedStorageEntries"] = abxjsonschema.SchemaFor[StorageClearSharedStorageEntriesParams]() + c.commandResultSchemas["Storage.clearSharedStorageEntries"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageClearSharedStorageEntriesResult]()) + c.commandParamsSchemas["Storage.resetSharedStorageBudget"] = abxjsonschema.SchemaFor[StorageResetSharedStorageBudgetParams]() + c.commandResultSchemas["Storage.resetSharedStorageBudget"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageResetSharedStorageBudgetResult]()) + c.commandParamsSchemas["Storage.setSharedStorageTracking"] = abxjsonschema.SchemaFor[StorageSetSharedStorageTrackingParams]() + c.commandResultSchemas["Storage.setSharedStorageTracking"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetSharedStorageTrackingResult]()) + c.commandParamsSchemas["Storage.setStorageBucketTracking"] = abxjsonschema.SchemaFor[StorageSetStorageBucketTrackingParams]() + c.commandResultSchemas["Storage.setStorageBucketTracking"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetStorageBucketTrackingResult]()) + c.commandParamsSchemas["Storage.deleteStorageBucket"] = abxjsonschema.SchemaFor[StorageDeleteStorageBucketParams]() + c.commandResultSchemas["Storage.deleteStorageBucket"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageDeleteStorageBucketResult]()) + c.commandParamsSchemas["Storage.runBounceTrackingMitigations"] = abxjsonschema.SchemaFor[StorageRunBounceTrackingMitigationsParams]() + c.commandResultSchemas["Storage.runBounceTrackingMitigations"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageRunBounceTrackingMitigationsResult]()) + c.commandParamsSchemas["Storage.setAttributionReportingLocalTestingMode"] = abxjsonschema.SchemaFor[StorageSetAttributionReportingLocalTestingModeParams]() + c.commandResultSchemas["Storage.setAttributionReportingLocalTestingMode"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetAttributionReportingLocalTestingModeResult]()) + c.commandParamsSchemas["Storage.setAttributionReportingTracking"] = abxjsonschema.SchemaFor[StorageSetAttributionReportingTrackingParams]() + c.commandResultSchemas["Storage.setAttributionReportingTracking"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetAttributionReportingTrackingResult]()) + c.commandParamsSchemas["Storage.sendPendingAttributionReports"] = abxjsonschema.SchemaFor[StorageSendPendingAttributionReportsParams]() + c.commandResultSchemas["Storage.sendPendingAttributionReports"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSendPendingAttributionReportsResult]()) + c.commandParamsSchemas["Storage.getRelatedWebsiteSets"] = abxjsonschema.SchemaFor[StorageGetRelatedWebsiteSetsParams]() + c.commandResultSchemas["Storage.getRelatedWebsiteSets"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetRelatedWebsiteSetsResult]()) + c.commandParamsSchemas["Storage.getAffectedUrlsForThirdPartyCookieMetadata"] = abxjsonschema.SchemaFor[StorageGetAffectedUrlsForThirdPartyCookieMetadataParams]() + c.commandResultSchemas["Storage.getAffectedUrlsForThirdPartyCookieMetadata"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageGetAffectedUrlsForThirdPartyCookieMetadataResult]()) + c.commandParamsSchemas["Storage.setProtectedAudienceKAnonymity"] = abxjsonschema.SchemaFor[StorageSetProtectedAudienceKAnonymityParams]() + c.commandResultSchemas["Storage.setProtectedAudienceKAnonymity"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSetProtectedAudienceKAnonymityResult]()) + c.commandParamsSchemas["SystemInfo.getInfo"] = abxjsonschema.SchemaFor[SystemInfoGetInfoParams]() + c.commandResultSchemas["SystemInfo.getInfo"] = nativeResultSchema(abxjsonschema.SchemaFor[SystemInfoGetInfoResult]()) + c.commandParamsSchemas["SystemInfo.getFeatureState"] = abxjsonschema.SchemaFor[SystemInfoGetFeatureStateParams]() + c.commandResultSchemas["SystemInfo.getFeatureState"] = nativeResultSchema(abxjsonschema.SchemaFor[SystemInfoGetFeatureStateResult]()) + c.commandParamsSchemas["SystemInfo.getProcessInfo"] = abxjsonschema.SchemaFor[SystemInfoGetProcessInfoParams]() + c.commandResultSchemas["SystemInfo.getProcessInfo"] = nativeResultSchema(abxjsonschema.SchemaFor[SystemInfoGetProcessInfoResult]()) + c.commandParamsSchemas["Target.activateTarget"] = abxjsonschema.SchemaFor[TargetActivateTargetParams]() + c.commandResultSchemas["Target.activateTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetActivateTargetResult]()) + c.commandParamsSchemas["Target.attachToTarget"] = abxjsonschema.SchemaFor[TargetAttachToTargetParams]() + c.commandResultSchemas["Target.attachToTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetAttachToTargetResult]()) + c.commandParamsSchemas["Target.attachToBrowserTarget"] = abxjsonschema.SchemaFor[TargetAttachToBrowserTargetParams]() + c.commandResultSchemas["Target.attachToBrowserTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetAttachToBrowserTargetResult]()) + c.commandParamsSchemas["Target.closeTarget"] = abxjsonschema.SchemaFor[TargetCloseTargetParams]() + c.commandResultSchemas["Target.closeTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetCloseTargetResult]()) + c.commandParamsSchemas["Target.exposeDevToolsProtocol"] = abxjsonschema.SchemaFor[TargetExposeDevToolsProtocolParams]() + c.commandResultSchemas["Target.exposeDevToolsProtocol"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetExposeDevToolsProtocolResult]()) + c.commandParamsSchemas["Target.createBrowserContext"] = abxjsonschema.SchemaFor[TargetCreateBrowserContextParams]() + c.commandResultSchemas["Target.createBrowserContext"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetCreateBrowserContextResult]()) + c.commandParamsSchemas["Target.getBrowserContexts"] = abxjsonschema.SchemaFor[TargetGetBrowserContextsParams]() + c.commandResultSchemas["Target.getBrowserContexts"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetGetBrowserContextsResult]()) + c.commandParamsSchemas["Target.createTarget"] = abxjsonschema.SchemaFor[TargetCreateTargetParams]() + c.commandResultSchemas["Target.createTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetCreateTargetResult]()) + c.commandParamsSchemas["Target.detachFromTarget"] = abxjsonschema.SchemaFor[TargetDetachFromTargetParams]() + c.commandResultSchemas["Target.detachFromTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetDetachFromTargetResult]()) + c.commandParamsSchemas["Target.disposeBrowserContext"] = abxjsonschema.SchemaFor[TargetDisposeBrowserContextParams]() + c.commandResultSchemas["Target.disposeBrowserContext"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetDisposeBrowserContextResult]()) + c.commandParamsSchemas["Target.getTargetInfo"] = abxjsonschema.SchemaFor[TargetGetTargetInfoParams]() + c.commandResultSchemas["Target.getTargetInfo"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetGetTargetInfoResult]()) + c.commandParamsSchemas["Target.getTargets"] = abxjsonschema.SchemaFor[TargetGetTargetsParams]() + c.commandResultSchemas["Target.getTargets"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetGetTargetsResult]()) + c.commandParamsSchemas["Target.sendMessageToTarget"] = abxjsonschema.SchemaFor[TargetSendMessageToTargetParams]() + c.commandResultSchemas["Target.sendMessageToTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetSendMessageToTargetResult]()) + c.commandParamsSchemas["Target.setAutoAttach"] = abxjsonschema.SchemaFor[TargetSetAutoAttachParams]() + c.commandResultSchemas["Target.setAutoAttach"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetSetAutoAttachResult]()) + c.commandParamsSchemas["Target.autoAttachRelated"] = abxjsonschema.SchemaFor[TargetAutoAttachRelatedParams]() + c.commandResultSchemas["Target.autoAttachRelated"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetAutoAttachRelatedResult]()) + c.commandParamsSchemas["Target.setDiscoverTargets"] = abxjsonschema.SchemaFor[TargetSetDiscoverTargetsParams]() + c.commandResultSchemas["Target.setDiscoverTargets"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetSetDiscoverTargetsResult]()) + c.commandParamsSchemas["Target.setRemoteLocations"] = abxjsonschema.SchemaFor[TargetSetRemoteLocationsParams]() + c.commandResultSchemas["Target.setRemoteLocations"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetSetRemoteLocationsResult]()) + c.commandParamsSchemas["Target.getDevToolsTarget"] = abxjsonschema.SchemaFor[TargetGetDevToolsTargetParams]() + c.commandResultSchemas["Target.getDevToolsTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetGetDevToolsTargetResult]()) + c.commandParamsSchemas["Target.openDevTools"] = abxjsonschema.SchemaFor[TargetOpenDevToolsParams]() + c.commandResultSchemas["Target.openDevTools"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetOpenDevToolsResult]()) + c.commandParamsSchemas["Tethering.bind"] = abxjsonschema.SchemaFor[TetheringBindParams]() + c.commandResultSchemas["Tethering.bind"] = nativeResultSchema(abxjsonschema.SchemaFor[TetheringBindResult]()) + c.commandParamsSchemas["Tethering.unbind"] = abxjsonschema.SchemaFor[TetheringUnbindParams]() + c.commandResultSchemas["Tethering.unbind"] = nativeResultSchema(abxjsonschema.SchemaFor[TetheringUnbindResult]()) + c.commandParamsSchemas["Tracing.end"] = abxjsonschema.SchemaFor[TracingEndParams]() + c.commandResultSchemas["Tracing.end"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingEndResult]()) + c.commandParamsSchemas["Tracing.getCategories"] = abxjsonschema.SchemaFor[TracingGetCategoriesParams]() + c.commandResultSchemas["Tracing.getCategories"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingGetCategoriesResult]()) + c.commandParamsSchemas["Tracing.getTrackEventDescriptor"] = abxjsonschema.SchemaFor[TracingGetTrackEventDescriptorParams]() + c.commandResultSchemas["Tracing.getTrackEventDescriptor"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingGetTrackEventDescriptorResult]()) + c.commandParamsSchemas["Tracing.recordClockSyncMarker"] = abxjsonschema.SchemaFor[TracingRecordClockSyncMarkerParams]() + c.commandResultSchemas["Tracing.recordClockSyncMarker"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingRecordClockSyncMarkerResult]()) + c.commandParamsSchemas["Tracing.requestMemoryDump"] = abxjsonschema.SchemaFor[TracingRequestMemoryDumpParams]() + c.commandResultSchemas["Tracing.requestMemoryDump"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingRequestMemoryDumpResult]()) + c.commandParamsSchemas["Tracing.start"] = abxjsonschema.SchemaFor[TracingStartParams]() + c.commandResultSchemas["Tracing.start"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingStartResult]()) + c.commandParamsSchemas["WebAudio.enable"] = abxjsonschema.SchemaFor[WebAudioEnableParams]() + c.commandResultSchemas["WebAudio.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioEnableResult]()) + c.commandParamsSchemas["WebAudio.disable"] = abxjsonschema.SchemaFor[WebAudioDisableParams]() + c.commandResultSchemas["WebAudio.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioDisableResult]()) + c.commandParamsSchemas["WebAudio.getRealtimeData"] = abxjsonschema.SchemaFor[WebAudioGetRealtimeDataParams]() + c.commandResultSchemas["WebAudio.getRealtimeData"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioGetRealtimeDataResult]()) + c.commandParamsSchemas["WebAuthn.enable"] = abxjsonschema.SchemaFor[WebAuthnEnableParams]() + c.commandResultSchemas["WebAuthn.enable"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnEnableResult]()) + c.commandParamsSchemas["WebAuthn.disable"] = abxjsonschema.SchemaFor[WebAuthnDisableParams]() + c.commandResultSchemas["WebAuthn.disable"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnDisableResult]()) + c.commandParamsSchemas["WebAuthn.addVirtualAuthenticator"] = abxjsonschema.SchemaFor[WebAuthnAddVirtualAuthenticatorParams]() + c.commandResultSchemas["WebAuthn.addVirtualAuthenticator"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnAddVirtualAuthenticatorResult]()) + c.commandParamsSchemas["WebAuthn.setResponseOverrideBits"] = abxjsonschema.SchemaFor[WebAuthnSetResponseOverrideBitsParams]() + c.commandResultSchemas["WebAuthn.setResponseOverrideBits"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnSetResponseOverrideBitsResult]()) + c.commandParamsSchemas["WebAuthn.removeVirtualAuthenticator"] = abxjsonschema.SchemaFor[WebAuthnRemoveVirtualAuthenticatorParams]() + c.commandResultSchemas["WebAuthn.removeVirtualAuthenticator"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnRemoveVirtualAuthenticatorResult]()) + c.commandParamsSchemas["WebAuthn.addCredential"] = abxjsonschema.SchemaFor[WebAuthnAddCredentialParams]() + c.commandResultSchemas["WebAuthn.addCredential"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnAddCredentialResult]()) + c.commandParamsSchemas["WebAuthn.getCredential"] = abxjsonschema.SchemaFor[WebAuthnGetCredentialParams]() + c.commandResultSchemas["WebAuthn.getCredential"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnGetCredentialResult]()) + c.commandParamsSchemas["WebAuthn.getCredentials"] = abxjsonschema.SchemaFor[WebAuthnGetCredentialsParams]() + c.commandResultSchemas["WebAuthn.getCredentials"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnGetCredentialsResult]()) + c.commandParamsSchemas["WebAuthn.removeCredential"] = abxjsonschema.SchemaFor[WebAuthnRemoveCredentialParams]() + c.commandResultSchemas["WebAuthn.removeCredential"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnRemoveCredentialResult]()) + c.commandParamsSchemas["WebAuthn.clearCredentials"] = abxjsonschema.SchemaFor[WebAuthnClearCredentialsParams]() + c.commandResultSchemas["WebAuthn.clearCredentials"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnClearCredentialsResult]()) + c.commandParamsSchemas["WebAuthn.setUserVerified"] = abxjsonschema.SchemaFor[WebAuthnSetUserVerifiedParams]() + c.commandResultSchemas["WebAuthn.setUserVerified"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnSetUserVerifiedResult]()) + c.commandParamsSchemas["WebAuthn.setAutomaticPresenceSimulation"] = abxjsonschema.SchemaFor[WebAuthnSetAutomaticPresenceSimulationParams]() + c.commandResultSchemas["WebAuthn.setAutomaticPresenceSimulation"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnSetAutomaticPresenceSimulationResult]()) + c.commandParamsSchemas["WebAuthn.setCredentialProperties"] = abxjsonschema.SchemaFor[WebAuthnSetCredentialPropertiesParams]() + c.commandResultSchemas["WebAuthn.setCredentialProperties"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnSetCredentialPropertiesResult]()) + c.eventSchemas["Accessibility.loadComplete"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityLoadCompleteEvent]()) + c.eventSchemas["Accessibility.nodesUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[AccessibilityNodesUpdatedEvent]()) + c.eventSchemas["Animation.animationCanceled"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationAnimationCanceledEvent]()) + c.eventSchemas["Animation.animationCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationAnimationCreatedEvent]()) + c.eventSchemas["Animation.animationStarted"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationAnimationStartedEvent]()) + c.eventSchemas["Animation.animationUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[AnimationAnimationUpdatedEvent]()) + c.eventSchemas["Audits.issueAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[AuditsIssueAddedEvent]()) + c.eventSchemas["Autofill.addressFormFilled"] = nativeResultSchema(abxjsonschema.SchemaFor[AutofillAddressFormFilledEvent]()) + c.eventSchemas["BackgroundService.recordingStateChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[BackgroundServiceRecordingStateChangedEvent]()) + c.eventSchemas["BackgroundService.backgroundServiceEventReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[BackgroundServiceBackgroundServiceEventReceivedEvent]()) + c.eventSchemas["BluetoothEmulation.gattOperationReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationGattOperationReceivedEvent]()) + c.eventSchemas["BluetoothEmulation.characteristicOperationReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationCharacteristicOperationReceivedEvent]()) + c.eventSchemas["BluetoothEmulation.descriptorOperationReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[BluetoothEmulationDescriptorOperationReceivedEvent]()) + c.eventSchemas["Browser.downloadWillBegin"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserDownloadWillBeginEvent]()) + c.eventSchemas["Browser.downloadProgress"] = nativeResultSchema(abxjsonschema.SchemaFor[BrowserDownloadProgressEvent]()) + c.eventSchemas["CSS.fontsUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSFontsUpdatedEvent]()) + c.eventSchemas["CSS.mediaQueryResultChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSMediaQueryResultChangedEvent]()) + c.eventSchemas["CSS.styleSheetAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSStyleSheetAddedEvent]()) + c.eventSchemas["CSS.styleSheetChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSStyleSheetChangedEvent]()) + c.eventSchemas["CSS.styleSheetRemoved"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSStyleSheetRemovedEvent]()) + c.eventSchemas["CSS.computedStyleUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[CSSComputedStyleUpdatedEvent]()) + c.eventSchemas["Cast.sinksUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[CastSinksUpdatedEvent]()) + c.eventSchemas["Cast.issueUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[CastIssueUpdatedEvent]()) + c.eventSchemas["Console.messageAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[ConsoleMessageAddedEvent]()) + c.eventSchemas["DOM.attributeModified"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMAttributeModifiedEvent]()) + c.eventSchemas["DOM.adoptedStyleSheetsModified"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMAdoptedStyleSheetsModifiedEvent]()) + c.eventSchemas["DOM.attributeRemoved"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMAttributeRemovedEvent]()) + c.eventSchemas["DOM.characterDataModified"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMCharacterDataModifiedEvent]()) + c.eventSchemas["DOM.childNodeCountUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMChildNodeCountUpdatedEvent]()) + c.eventSchemas["DOM.childNodeInserted"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMChildNodeInsertedEvent]()) + c.eventSchemas["DOM.childNodeRemoved"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMChildNodeRemovedEvent]()) + c.eventSchemas["DOM.distributedNodesUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDistributedNodesUpdatedEvent]()) + c.eventSchemas["DOM.documentUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMDocumentUpdatedEvent]()) + c.eventSchemas["DOM.inlineStyleInvalidated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMInlineStyleInvalidatedEvent]()) + c.eventSchemas["DOM.pseudoElementAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMPseudoElementAddedEvent]()) + c.eventSchemas["DOM.topLayerElementsUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMTopLayerElementsUpdatedEvent]()) + c.eventSchemas["DOM.scrollableFlagUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMScrollableFlagUpdatedEvent]()) + c.eventSchemas["DOM.adRelatedStateUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMAdRelatedStateUpdatedEvent]()) + c.eventSchemas["DOM.affectedByStartingStylesFlagUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMAffectedByStartingStylesFlagUpdatedEvent]()) + c.eventSchemas["DOM.pseudoElementRemoved"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMPseudoElementRemovedEvent]()) + c.eventSchemas["DOM.setChildNodes"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMSetChildNodesEvent]()) + c.eventSchemas["DOM.shadowRootPopped"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMShadowRootPoppedEvent]()) + c.eventSchemas["DOM.shadowRootPushed"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMShadowRootPushedEvent]()) + c.eventSchemas["DOMStorage.domStorageItemAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageDOMStorageItemAddedEvent]()) + c.eventSchemas["DOMStorage.domStorageItemRemoved"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageDOMStorageItemRemovedEvent]()) + c.eventSchemas["DOMStorage.domStorageItemUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageDOMStorageItemUpdatedEvent]()) + c.eventSchemas["DOMStorage.domStorageItemsCleared"] = nativeResultSchema(abxjsonschema.SchemaFor[DOMStorageDOMStorageItemsClearedEvent]()) + c.eventSchemas["Debugger.breakpointResolved"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerBreakpointResolvedEvent]()) + c.eventSchemas["Debugger.paused"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerPausedEvent]()) + c.eventSchemas["Debugger.resumed"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerResumedEvent]()) + c.eventSchemas["Debugger.scriptFailedToParse"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerScriptFailedToParseEvent]()) + c.eventSchemas["Debugger.scriptParsed"] = nativeResultSchema(abxjsonschema.SchemaFor[DebuggerScriptParsedEvent]()) + c.eventSchemas["DeviceAccess.deviceRequestPrompted"] = nativeResultSchema(abxjsonschema.SchemaFor[DeviceAccessDeviceRequestPromptedEvent]()) + c.eventSchemas["Emulation.virtualTimeBudgetExpired"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationVirtualTimeBudgetExpiredEvent]()) + c.eventSchemas["Emulation.screenOrientationLockChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[EmulationScreenOrientationLockChangedEvent]()) + c.eventSchemas["FedCm.dialogShown"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmDialogShownEvent]()) + c.eventSchemas["FedCm.dialogClosed"] = nativeResultSchema(abxjsonschema.SchemaFor[FedCmDialogClosedEvent]()) + c.eventSchemas["Fetch.requestPaused"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchRequestPausedEvent]()) + c.eventSchemas["Fetch.authRequired"] = nativeResultSchema(abxjsonschema.SchemaFor[FetchAuthRequiredEvent]()) + c.eventSchemas["HeapProfiler.addHeapSnapshotChunk"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerAddHeapSnapshotChunkEvent]()) + c.eventSchemas["HeapProfiler.heapStatsUpdate"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerHeapStatsUpdateEvent]()) + c.eventSchemas["HeapProfiler.lastSeenObjectId"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerLastSeenObjectIDEvent]()) + c.eventSchemas["HeapProfiler.reportHeapSnapshotProgress"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerReportHeapSnapshotProgressEvent]()) + c.eventSchemas["HeapProfiler.resetProfiles"] = nativeResultSchema(abxjsonschema.SchemaFor[HeapProfilerResetProfilesEvent]()) + c.eventSchemas["Input.dragIntercepted"] = nativeResultSchema(abxjsonschema.SchemaFor[InputDragInterceptedEvent]()) + c.eventSchemas["Inspector.detached"] = nativeResultSchema(abxjsonschema.SchemaFor[InspectorDetachedEvent]()) + c.eventSchemas["Inspector.targetCrashed"] = nativeResultSchema(abxjsonschema.SchemaFor[InspectorTargetCrashedEvent]()) + c.eventSchemas["Inspector.targetReloadedAfterCrash"] = nativeResultSchema(abxjsonschema.SchemaFor[InspectorTargetReloadedAfterCrashEvent]()) + c.eventSchemas["Inspector.workerScriptLoaded"] = nativeResultSchema(abxjsonschema.SchemaFor[InspectorWorkerScriptLoadedEvent]()) + c.eventSchemas["LayerTree.layerPainted"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeLayerPaintedEvent]()) + c.eventSchemas["LayerTree.layerTreeDidChange"] = nativeResultSchema(abxjsonschema.SchemaFor[LayerTreeLayerTreeDidChangeEvent]()) + c.eventSchemas["Log.entryAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[LogEntryAddedEvent]()) + c.eventSchemas["Media.playerPropertiesChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[MediaPlayerPropertiesChangedEvent]()) + c.eventSchemas["Media.playerEventsAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[MediaPlayerEventsAddedEvent]()) + c.eventSchemas["Media.playerMessagesLogged"] = nativeResultSchema(abxjsonschema.SchemaFor[MediaPlayerMessagesLoggedEvent]()) + c.eventSchemas["Media.playerErrorsRaised"] = nativeResultSchema(abxjsonschema.SchemaFor[MediaPlayerErrorsRaisedEvent]()) + c.eventSchemas["Media.playerCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[MediaPlayerCreatedEvent]()) + c.eventSchemas["Network.dataReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDataReceivedEvent]()) + c.eventSchemas["Network.eventSourceMessageReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkEventSourceMessageReceivedEvent]()) + c.eventSchemas["Network.loadingFailed"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkLoadingFailedEvent]()) + c.eventSchemas["Network.loadingFinished"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkLoadingFinishedEvent]()) + c.eventSchemas["Network.requestIntercepted"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkRequestInterceptedEvent]()) + c.eventSchemas["Network.requestServedFromCache"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkRequestServedFromCacheEvent]()) + c.eventSchemas["Network.requestWillBeSent"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkRequestWillBeSentEvent]()) + c.eventSchemas["Network.resourceChangedPriority"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkResourceChangedPriorityEvent]()) + c.eventSchemas["Network.signedExchangeReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkSignedExchangeReceivedEvent]()) + c.eventSchemas["Network.responseReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkResponseReceivedEvent]()) + c.eventSchemas["Network.webSocketClosed"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebSocketClosedEvent]()) + c.eventSchemas["Network.webSocketCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebSocketCreatedEvent]()) + c.eventSchemas["Network.webSocketFrameError"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebSocketFrameErrorEvent]()) + c.eventSchemas["Network.webSocketFrameReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebSocketFrameReceivedEvent]()) + c.eventSchemas["Network.webSocketFrameSent"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebSocketFrameSentEvent]()) + c.eventSchemas["Network.webSocketHandshakeResponseReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebSocketHandshakeResponseReceivedEvent]()) + c.eventSchemas["Network.webSocketWillSendHandshakeRequest"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebSocketWillSendHandshakeRequestEvent]()) + c.eventSchemas["Network.webTransportCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebTransportCreatedEvent]()) + c.eventSchemas["Network.webTransportConnectionEstablished"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebTransportConnectionEstablishedEvent]()) + c.eventSchemas["Network.webTransportClosed"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkWebTransportClosedEvent]()) + c.eventSchemas["Network.directTCPSocketCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectTCPSocketCreatedEvent]()) + c.eventSchemas["Network.directTCPSocketOpened"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectTCPSocketOpenedEvent]()) + c.eventSchemas["Network.directTCPSocketAborted"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectTCPSocketAbortedEvent]()) + c.eventSchemas["Network.directTCPSocketClosed"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectTCPSocketClosedEvent]()) + c.eventSchemas["Network.directTCPSocketChunkSent"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectTCPSocketChunkSentEvent]()) + c.eventSchemas["Network.directTCPSocketChunkReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectTCPSocketChunkReceivedEvent]()) + c.eventSchemas["Network.directUDPSocketJoinedMulticastGroup"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectUDPSocketJoinedMulticastGroupEvent]()) + c.eventSchemas["Network.directUDPSocketLeftMulticastGroup"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectUDPSocketLeftMulticastGroupEvent]()) + c.eventSchemas["Network.directUDPSocketCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectUDPSocketCreatedEvent]()) + c.eventSchemas["Network.directUDPSocketOpened"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectUDPSocketOpenedEvent]()) + c.eventSchemas["Network.directUDPSocketAborted"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectUDPSocketAbortedEvent]()) + c.eventSchemas["Network.directUDPSocketClosed"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectUDPSocketClosedEvent]()) + c.eventSchemas["Network.directUDPSocketChunkSent"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectUDPSocketChunkSentEvent]()) + c.eventSchemas["Network.directUDPSocketChunkReceived"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDirectUDPSocketChunkReceivedEvent]()) + c.eventSchemas["Network.requestWillBeSentExtraInfo"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkRequestWillBeSentExtraInfoEvent]()) + c.eventSchemas["Network.responseReceivedExtraInfo"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkResponseReceivedExtraInfoEvent]()) + c.eventSchemas["Network.responseReceivedEarlyHints"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkResponseReceivedEarlyHintsEvent]()) + c.eventSchemas["Network.trustTokenOperationDone"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkTrustTokenOperationDoneEvent]()) + c.eventSchemas["Network.policyUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkPolicyUpdatedEvent]()) + c.eventSchemas["Network.reportingApiReportAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkReportingAPIReportAddedEvent]()) + c.eventSchemas["Network.reportingApiReportUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkReportingAPIReportUpdatedEvent]()) + c.eventSchemas["Network.reportingApiEndpointsChangedForOrigin"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkReportingAPIEndpointsChangedForOriginEvent]()) + c.eventSchemas["Network.deviceBoundSessionsAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDeviceBoundSessionsAddedEvent]()) + c.eventSchemas["Network.deviceBoundSessionEventOccurred"] = nativeResultSchema(abxjsonschema.SchemaFor[NetworkDeviceBoundSessionEventOccurredEvent]()) + c.eventSchemas["Overlay.inspectNodeRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayInspectNodeRequestedEvent]()) + c.eventSchemas["Overlay.nodeHighlightRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayNodeHighlightRequestedEvent]()) + c.eventSchemas["Overlay.screenshotRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayScreenshotRequestedEvent]()) + c.eventSchemas["Overlay.inspectPanelShowRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayInspectPanelShowRequestedEvent]()) + c.eventSchemas["Overlay.inspectedElementWindowRestored"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayInspectedElementWindowRestoredEvent]()) + c.eventSchemas["Overlay.inspectModeCanceled"] = nativeResultSchema(abxjsonschema.SchemaFor[OverlayInspectModeCanceledEvent]()) + c.eventSchemas["Page.domContentEventFired"] = nativeResultSchema(abxjsonschema.SchemaFor[PageDOMContentEventFiredEvent]()) + c.eventSchemas["Page.fileChooserOpened"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFileChooserOpenedEvent]()) + c.eventSchemas["Page.frameAttached"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameAttachedEvent]()) + c.eventSchemas["Page.frameClearedScheduledNavigation"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameClearedScheduledNavigationEvent]()) + c.eventSchemas["Page.frameDetached"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameDetachedEvent]()) + c.eventSchemas["Page.frameSubtreeWillBeDetached"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameSubtreeWillBeDetachedEvent]()) + c.eventSchemas["Page.frameNavigated"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameNavigatedEvent]()) + c.eventSchemas["Page.documentOpened"] = nativeResultSchema(abxjsonschema.SchemaFor[PageDocumentOpenedEvent]()) + c.eventSchemas["Page.frameResized"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameResizedEvent]()) + c.eventSchemas["Page.frameStartedNavigating"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameStartedNavigatingEvent]()) + c.eventSchemas["Page.frameRequestedNavigation"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameRequestedNavigationEvent]()) + c.eventSchemas["Page.frameScheduledNavigation"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameScheduledNavigationEvent]()) + c.eventSchemas["Page.frameStartedLoading"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameStartedLoadingEvent]()) + c.eventSchemas["Page.frameStoppedLoading"] = nativeResultSchema(abxjsonschema.SchemaFor[PageFrameStoppedLoadingEvent]()) + c.eventSchemas["Page.downloadWillBegin"] = nativeResultSchema(abxjsonschema.SchemaFor[PageDownloadWillBeginEvent]()) + c.eventSchemas["Page.downloadProgress"] = nativeResultSchema(abxjsonschema.SchemaFor[PageDownloadProgressEvent]()) + c.eventSchemas["Page.interstitialHidden"] = nativeResultSchema(abxjsonschema.SchemaFor[PageInterstitialHiddenEvent]()) + c.eventSchemas["Page.interstitialShown"] = nativeResultSchema(abxjsonschema.SchemaFor[PageInterstitialShownEvent]()) + c.eventSchemas["Page.javascriptDialogClosed"] = nativeResultSchema(abxjsonschema.SchemaFor[PageJavascriptDialogClosedEvent]()) + c.eventSchemas["Page.javascriptDialogOpening"] = nativeResultSchema(abxjsonschema.SchemaFor[PageJavascriptDialogOpeningEvent]()) + c.eventSchemas["Page.lifecycleEvent"] = nativeResultSchema(abxjsonschema.SchemaFor[PageLifecycleEventEvent]()) + c.eventSchemas["Page.backForwardCacheNotUsed"] = nativeResultSchema(abxjsonschema.SchemaFor[PageBackForwardCacheNotUsedEvent]()) + c.eventSchemas["Page.loadEventFired"] = nativeResultSchema(abxjsonschema.SchemaFor[PageLoadEventFiredEvent]()) + c.eventSchemas["Page.navigatedWithinDocument"] = nativeResultSchema(abxjsonschema.SchemaFor[PageNavigatedWithinDocumentEvent]()) + c.eventSchemas["Page.screencastFrame"] = nativeResultSchema(abxjsonschema.SchemaFor[PageScreencastFrameEvent]()) + c.eventSchemas["Page.screencastVisibilityChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[PageScreencastVisibilityChangedEvent]()) + c.eventSchemas["Page.windowOpen"] = nativeResultSchema(abxjsonschema.SchemaFor[PageWindowOpenEvent]()) + c.eventSchemas["Page.compilationCacheProduced"] = nativeResultSchema(abxjsonschema.SchemaFor[PageCompilationCacheProducedEvent]()) + c.eventSchemas["Performance.metrics"] = nativeResultSchema(abxjsonschema.SchemaFor[PerformanceMetricsEvent]()) + c.eventSchemas["PerformanceTimeline.timelineEventAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[PerformanceTimelineTimelineEventAddedEvent]()) + c.eventSchemas["Preload.ruleSetUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[PreloadRuleSetUpdatedEvent]()) + c.eventSchemas["Preload.ruleSetRemoved"] = nativeResultSchema(abxjsonschema.SchemaFor[PreloadRuleSetRemovedEvent]()) + c.eventSchemas["Preload.preloadEnabledStateUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[PreloadPreloadEnabledStateUpdatedEvent]()) + c.eventSchemas["Preload.prefetchStatusUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[PreloadPrefetchStatusUpdatedEvent]()) + c.eventSchemas["Preload.prerenderStatusUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[PreloadPrerenderStatusUpdatedEvent]()) + c.eventSchemas["Preload.preloadingAttemptSourcesUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[PreloadPreloadingAttemptSourcesUpdatedEvent]()) + c.eventSchemas["Profiler.consoleProfileFinished"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerConsoleProfileFinishedEvent]()) + c.eventSchemas["Profiler.consoleProfileStarted"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerConsoleProfileStartedEvent]()) + c.eventSchemas["Profiler.preciseCoverageDeltaUpdate"] = nativeResultSchema(abxjsonschema.SchemaFor[ProfilerPreciseCoverageDeltaUpdateEvent]()) + c.eventSchemas["Runtime.bindingCalled"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeBindingCalledEvent]()) + c.eventSchemas["Runtime.consoleAPICalled"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeConsoleAPICalledEvent]()) + c.eventSchemas["Runtime.exceptionRevoked"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeExceptionRevokedEvent]()) + c.eventSchemas["Runtime.exceptionThrown"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeExceptionThrownEvent]()) + c.eventSchemas["Runtime.executionContextCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeExecutionContextCreatedEvent]()) + c.eventSchemas["Runtime.executionContextDestroyed"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeExecutionContextDestroyedEvent]()) + c.eventSchemas["Runtime.executionContextsCleared"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeExecutionContextsClearedEvent]()) + c.eventSchemas["Runtime.inspectRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[RuntimeInspectRequestedEvent]()) + c.eventSchemas["Security.certificateError"] = nativeResultSchema(abxjsonschema.SchemaFor[SecurityCertificateErrorEvent]()) + c.eventSchemas["Security.visibleSecurityStateChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[SecurityVisibleSecurityStateChangedEvent]()) + c.eventSchemas["Security.securityStateChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[SecuritySecurityStateChangedEvent]()) + c.eventSchemas["ServiceWorker.workerErrorReported"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerWorkerErrorReportedEvent]()) + c.eventSchemas["ServiceWorker.workerRegistrationUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerWorkerRegistrationUpdatedEvent]()) + c.eventSchemas["ServiceWorker.workerVersionUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[ServiceWorkerWorkerVersionUpdatedEvent]()) + c.eventSchemas["SmartCardEmulation.establishContextRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationEstablishContextRequestedEvent]()) + c.eventSchemas["SmartCardEmulation.releaseContextRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationReleaseContextRequestedEvent]()) + c.eventSchemas["SmartCardEmulation.listReadersRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationListReadersRequestedEvent]()) + c.eventSchemas["SmartCardEmulation.getStatusChangeRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationGetStatusChangeRequestedEvent]()) + c.eventSchemas["SmartCardEmulation.cancelRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationCancelRequestedEvent]()) + c.eventSchemas["SmartCardEmulation.connectRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationConnectRequestedEvent]()) + c.eventSchemas["SmartCardEmulation.disconnectRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationDisconnectRequestedEvent]()) + c.eventSchemas["SmartCardEmulation.transmitRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationTransmitRequestedEvent]()) + c.eventSchemas["SmartCardEmulation.controlRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationControlRequestedEvent]()) + c.eventSchemas["SmartCardEmulation.getAttribRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationGetAttribRequestedEvent]()) + c.eventSchemas["SmartCardEmulation.setAttribRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationSetAttribRequestedEvent]()) + c.eventSchemas["SmartCardEmulation.statusRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationStatusRequestedEvent]()) + c.eventSchemas["SmartCardEmulation.beginTransactionRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationBeginTransactionRequestedEvent]()) + c.eventSchemas["SmartCardEmulation.endTransactionRequested"] = nativeResultSchema(abxjsonschema.SchemaFor[SmartCardEmulationEndTransactionRequestedEvent]()) + c.eventSchemas["Storage.cacheStorageContentUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageCacheStorageContentUpdatedEvent]()) + c.eventSchemas["Storage.cacheStorageListUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageCacheStorageListUpdatedEvent]()) + c.eventSchemas["Storage.indexedDBContentUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageIndexedDBContentUpdatedEvent]()) + c.eventSchemas["Storage.indexedDBListUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageIndexedDBListUpdatedEvent]()) + c.eventSchemas["Storage.interestGroupAccessed"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageInterestGroupAccessedEvent]()) + c.eventSchemas["Storage.interestGroupAuctionEventOccurred"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageInterestGroupAuctionEventOccurredEvent]()) + c.eventSchemas["Storage.interestGroupAuctionNetworkRequestCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageInterestGroupAuctionNetworkRequestCreatedEvent]()) + c.eventSchemas["Storage.sharedStorageAccessed"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSharedStorageAccessedEvent]()) + c.eventSchemas["Storage.sharedStorageWorkletOperationExecutionFinished"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageSharedStorageWorkletOperationExecutionFinishedEvent]()) + c.eventSchemas["Storage.storageBucketCreatedOrUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageStorageBucketCreatedOrUpdatedEvent]()) + c.eventSchemas["Storage.storageBucketDeleted"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageStorageBucketDeletedEvent]()) + c.eventSchemas["Storage.attributionReportingSourceRegistered"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageAttributionReportingSourceRegisteredEvent]()) + c.eventSchemas["Storage.attributionReportingTriggerRegistered"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageAttributionReportingTriggerRegisteredEvent]()) + c.eventSchemas["Storage.attributionReportingReportSent"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageAttributionReportingReportSentEvent]()) + c.eventSchemas["Storage.attributionReportingVerboseDebugReportSent"] = nativeResultSchema(abxjsonschema.SchemaFor[StorageAttributionReportingVerboseDebugReportSentEvent]()) + c.eventSchemas["Target.attachedToTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetAttachedToTargetEvent]()) + c.eventSchemas["Target.detachedFromTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetDetachedFromTargetEvent]()) + c.eventSchemas["Target.receivedMessageFromTarget"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetReceivedMessageFromTargetEvent]()) + c.eventSchemas["Target.targetCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetTargetCreatedEvent]()) + c.eventSchemas["Target.targetDestroyed"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetTargetDestroyedEvent]()) + c.eventSchemas["Target.targetCrashed"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetTargetCrashedEvent]()) + c.eventSchemas["Target.targetInfoChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[TargetTargetInfoChangedEvent]()) + c.eventSchemas["Tethering.accepted"] = nativeResultSchema(abxjsonschema.SchemaFor[TetheringAcceptedEvent]()) + c.eventSchemas["Tracing.bufferUsage"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingBufferUsageEvent]()) + c.eventSchemas["Tracing.dataCollected"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingDataCollectedEvent]()) + c.eventSchemas["Tracing.tracingComplete"] = nativeResultSchema(abxjsonschema.SchemaFor[TracingTracingCompleteEvent]()) + c.eventSchemas["WebAudio.contextCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioContextCreatedEvent]()) + c.eventSchemas["WebAudio.contextWillBeDestroyed"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioContextWillBeDestroyedEvent]()) + c.eventSchemas["WebAudio.contextChanged"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioContextChangedEvent]()) + c.eventSchemas["WebAudio.audioListenerCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioAudioListenerCreatedEvent]()) + c.eventSchemas["WebAudio.audioListenerWillBeDestroyed"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioAudioListenerWillBeDestroyedEvent]()) + c.eventSchemas["WebAudio.audioNodeCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioAudioNodeCreatedEvent]()) + c.eventSchemas["WebAudio.audioNodeWillBeDestroyed"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioAudioNodeWillBeDestroyedEvent]()) + c.eventSchemas["WebAudio.audioParamCreated"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioAudioParamCreatedEvent]()) + c.eventSchemas["WebAudio.audioParamWillBeDestroyed"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioAudioParamWillBeDestroyedEvent]()) + c.eventSchemas["WebAudio.nodesConnected"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioNodesConnectedEvent]()) + c.eventSchemas["WebAudio.nodesDisconnected"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioNodesDisconnectedEvent]()) + c.eventSchemas["WebAudio.nodeParamConnected"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioNodeParamConnectedEvent]()) + c.eventSchemas["WebAudio.nodeParamDisconnected"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAudioNodeParamDisconnectedEvent]()) + c.eventSchemas["WebAuthn.credentialAdded"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnCredentialAddedEvent]()) + c.eventSchemas["WebAuthn.credentialDeleted"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnCredentialDeletedEvent]()) + c.eventSchemas["WebAuthn.credentialUpdated"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnCredentialUpdatedEvent]()) + c.eventSchemas["WebAuthn.credentialAsserted"] = nativeResultSchema(abxjsonschema.SchemaFor[WebAuthnCredentialAssertedEvent]()) +} diff --git a/go/modcdp/injector/BorrowedExtensionInjector.go b/go/modcdp/injector/BorrowedExtensionInjector.go index e82a852..c6af6ec 100644 --- a/go/modcdp/injector/BorrowedExtensionInjector.go +++ b/go/modcdp/injector/BorrowedExtensionInjector.go @@ -166,9 +166,11 @@ func modcdpServerPathFromExtensionPath(extensionPath string) (string, error) { if extensionPath != "" { candidates = append(candidates, filepath.Join(extensionPath, "ModCDPServer.js")) candidates = append(candidates, filepath.Join(extensionPath, "js", "src", "server", "ModCDPServer.js")) + candidates = append(candidates, filepath.Join(filepath.Dir(extensionPath), "js", "src", "server", "ModCDPServer.js")) } if _, file, _, ok := runtime.Caller(0); ok { for dir := filepath.Dir(file); ; dir = filepath.Dir(dir) { + candidates = append(candidates, filepath.Join(dir, "dist", "js", "src", "server", "ModCDPServer.js")) candidates = append(candidates, filepath.Join(dir, "dist", "extension", "js", "src", "server", "ModCDPServer.js")) candidates = append(candidates, filepath.Join(dir, "dist", "extension", "ModCDPServer.js")) if parent := filepath.Dir(dir); parent == dir { diff --git a/go/modcdp/injector/BorrowedExtensionInjector_test.go b/go/modcdp/injector/BorrowedExtensionInjector_test.go index 0dc5689..9441946 100644 --- a/go/modcdp/injector/BorrowedExtensionInjector_test.go +++ b/go/modcdp/injector/BorrowedExtensionInjector_test.go @@ -12,19 +12,25 @@ func TestBorrowedExtensionInjectorBootstrapsModCDPInsideLiveExtensionServiceWork if err != nil { t.Fatal(err) } - chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{ - Headless: boolPtr(true), - Sandbox: boolPtr(false), - ExtraArgs: []string{"--load-extension=" + extensionPath}, - }).Launch(modcdp.LaunchOptions{}) - if err != nil { + headless := true + owner := modcdp.New(modcdp.Options{ + Launcher: modcdp.LauncherConfig{LauncherMode: "local", LauncherOptions: modcdp.LaunchOptions{Headless: &headless}}, + Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws"}, + Injector: modcdp.InjectorConfig{ + InjectorMode: "auto", + InjectorExtensionPath: extensionPath, + InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, + InjectorTrustServiceWorkerTarget: true, + }, + }) + defer owner.Close() + + if err := owner.Connect(); err != nil { t.Fatal(err) } - defer chrome.Close() - cdp := modcdp.New(modcdp.Options{ Launcher: modcdp.LauncherConfig{LauncherMode: "remote"}, - Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws", UpstreamCDPURL: chrome.CDPURL}, + Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws", UpstreamCDPURL: owner.CDPURL}, Injector: modcdp.InjectorConfig{ InjectorMode: "borrow", InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, @@ -42,12 +48,13 @@ func TestBorrowedExtensionInjectorBootstrapsModCDPInsideLiveExtensionServiceWork if cdp.ExtensionID != DefaultModCDPExtensionID { t.Fatalf("ExtensionID = %q", cdp.ExtensionID) } - result, err := cdp.Send("Target.getTargets", map[string]any{}) + result, err := cdp.Mod.Evaluate(map[string]any{ + "expression": "chrome.runtime.getURL('modcdp/service_worker.js')", + }) if err != nil { t.Fatal(err) } - targetInfos, _ := result.(map[string]any)["targetInfos"].([]any) - if len(targetInfos) == 0 { - t.Fatalf("Target.getTargets = %#v", result) + if result != "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js" { + t.Fatalf("Mod.evaluate = %#v", result) } } diff --git a/go/modcdp/injector/DiscoveredExtensionInjector.go b/go/modcdp/injector/DiscoveredExtensionInjector.go index 886ad71..d83d144 100644 --- a/go/modcdp/injector/DiscoveredExtensionInjector.go +++ b/go/modcdp/injector/DiscoveredExtensionInjector.go @@ -30,15 +30,6 @@ func (i *DiscoveredExtensionInjector) Inject() (*ExtensionInjectionResult, error return waited, err } } - if i.wakeConfiguredExtension() { - waited, err := i.waitForReadyServiceWorker(i.Options.InjectorServiceWorkerProbeTimeoutMS, i.Options.InjectorTrustServiceWorkerTarget) - if err != nil || waited != nil { - if waited != nil { - waited.Source = "discovered" - } - return waited, err - } - } if !i.Options.InjectorRequireServiceWorkerTarget { return nil, nil } diff --git a/go/modcdp/injector/DiscoveredExtensionInjector_test.go b/go/modcdp/injector/DiscoveredExtensionInjector_test.go index bc3db0b..b689838 100644 --- a/go/modcdp/injector/DiscoveredExtensionInjector_test.go +++ b/go/modcdp/injector/DiscoveredExtensionInjector_test.go @@ -1,35 +1,36 @@ package injector_test import ( - "encoding/json" modcdp "github.com/browserbase/modcdp/go/modcdp/client" . "github.com/browserbase/modcdp/go/modcdp/injector" - "os" "path/filepath" "testing" ) -const customDiscoveredExtensionID = "hhklgmbgnbeghnjidampacgmgnhelifg" -const customDiscoveredExtensionPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzG1LUbtH0aHMKjTAUeT0saY8xfnRNENctFJme3C1qnsqT7PAXMxJC4nT7tBZy2gEGRirBb3zIZ3OyAu9a0QR8lTLupDp4qHWOhQ7dl9ZjxjQdYa4Gby0xuXLdQrJIxDbmuv+UVJvYa8vRTwQB8koygbzDDDP5/YiB6mc0hbh8XBb82Ossy7T280k8280o/rS0CXdioUraCHj58PDhfxbs18TBcYfOjuRqua9J2oddxobtGehSD0gDtbvn2IWDtRajOlgZZyuS1vLoSR7C1ulFzpRSYPEMhI2x+wphut7E3QImyJ577YeULVGpt988FcixOou7udjx3/IUWjpq8046wIDAQAB" - func TestDiscoveredExtensionInjectorAttachesToAlreadyLoadedRealModCDPExtension(t *testing.T) { extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension")) if err != nil { t.Fatal(err) } - chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{ - Headless: boolPtr(true), - Sandbox: boolPtr(false), - ExtraArgs: []string{"--load-extension=" + extensionPath}, - }).Launch(modcdp.LaunchOptions{}) - if err != nil { + headless := true + owner := modcdp.New(modcdp.Options{ + Launcher: modcdp.LauncherConfig{LauncherMode: "local", LauncherOptions: modcdp.LaunchOptions{Headless: &headless}}, + Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws"}, + Injector: modcdp.InjectorConfig{ + InjectorMode: "auto", + InjectorExtensionPath: extensionPath, + InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, + InjectorTrustServiceWorkerTarget: true, + }, + }) + defer owner.Close() + + if err := owner.Connect(); err != nil { t.Fatal(err) } - defer chrome.Close() - cdp := modcdp.New(modcdp.Options{ Launcher: modcdp.LauncherConfig{LauncherMode: "remote"}, - Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws", UpstreamCDPURL: chrome.CDPURL}, + Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws", UpstreamCDPURL: owner.CDPURL}, Injector: modcdp.InjectorConfig{ InjectorMode: "discover", InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, @@ -57,96 +58,3 @@ func TestDiscoveredExtensionInjectorAttachesToAlreadyLoadedRealModCDPExtension(t t.Fatalf("Mod.evaluate = %#v", result) } } - -func TestDiscoveredExtensionInjectorSelectsConfiguredExtensionWhenMultipleModCDPWorkersExist(t *testing.T) { - extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension")) - if err != nil { - t.Fatal(err) - } - customExtensionPath := filepath.Join(t.TempDir(), "extension") - if err := copyDir(extensionPath, customExtensionPath); err != nil { - t.Fatal(err) - } - manifestPath := filepath.Join(customExtensionPath, "manifest.json") - manifestBytes, err := os.ReadFile(manifestPath) - if err != nil { - t.Fatal(err) - } - var manifest map[string]any - if err := json.Unmarshal(manifestBytes, &manifest); err != nil { - t.Fatal(err) - } - manifest["key"] = customDiscoveredExtensionPublicKey - manifest["name"] = "ModCDP Bridge Custom Test" - updatedManifest, err := json.MarshalIndent(manifest, "", " ") - if err != nil { - t.Fatal(err) - } - if err := os.WriteFile(manifestPath, append(updatedManifest, '\n'), 0o644); err != nil { - t.Fatal(err) - } - - chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{ - Headless: boolPtr(true), - Sandbox: boolPtr(false), - ExtraArgs: []string{"--load-extension=" + extensionPath + "," + customExtensionPath}, - }).Launch(modcdp.LaunchOptions{}) - if err != nil { - t.Fatal(err) - } - defer chrome.Close() - - cdp := modcdp.New(modcdp.Options{ - Launcher: modcdp.LauncherConfig{LauncherMode: "remote"}, - Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws", UpstreamCDPURL: chrome.CDPURL}, - Injector: modcdp.InjectorConfig{ - InjectorMode: "discover", - InjectorExtensionID: customDiscoveredExtensionID, - InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, - InjectorTrustServiceWorkerTarget: true, - InjectorRequireServiceWorkerTarget: true, - }, - }) - defer cdp.Close() - - if err := cdp.Connect(); err != nil { - t.Fatal(err) - } - if cdp.ConnectTiming["injector_source"] != "discovered" { - t.Fatalf("injector_source = %v", cdp.ConnectTiming["injector_source"]) - } - if cdp.ExtensionID != customDiscoveredExtensionID { - t.Fatalf("ExtensionID = %q", cdp.ExtensionID) - } - result, err := cdp.Mod.Evaluate(map[string]any{"expression": "chrome.runtime.id"}) - if err != nil { - t.Fatal(err) - } - if result != customDiscoveredExtensionID { - t.Fatalf("Mod.evaluate = %#v", result) - } - - targetsResult, err := cdp.SendRaw("Target.getTargets", nil) - if err != nil { - t.Fatal(err) - } - targets, _ := targetsResult["targetInfos"].([]any) - foundCustom := false - foundDefault := false - for _, rawTarget := range targets { - target, _ := rawTarget.(map[string]any) - if target["type"] != "service_worker" { - continue - } - url, _ := target["url"].(string) - switch url { - case "chrome-extension://" + customDiscoveredExtensionID + "/modcdp/service_worker.js": - foundCustom = true - case "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js": - foundDefault = true - } - } - if !foundCustom || !foundDefault { - t.Fatalf("expected custom and default ModCDP workers, custom=%v default=%v targets=%#v", foundCustom, foundDefault, targets) - } -} diff --git a/go/modcdp/injector/ExtensionInjector.go b/go/modcdp/injector/ExtensionInjector.go index 40e08e3..7bfc8aa 100644 --- a/go/modcdp/injector/ExtensionInjector.go +++ b/go/modcdp/injector/ExtensionInjector.go @@ -1,7 +1,17 @@ package injector import ( + "archive/zip" + "bytes" + "crypto/sha256" + _ "embed" + "encoding/base64" + "encoding/json" "fmt" + "io" + "io/fs" + "os" + "path/filepath" "regexp" "strings" "time" @@ -10,7 +20,6 @@ import ( ) const DefaultModCDPExtensionID = "mdedooklbnfejodmnhmkdpkaedafkehf" -const DefaultModCDPWakePath = "/modcdp/wake.html" const DefaultCDPSendTimeoutMS = 10_000 const DefaultExecutionContextTimeoutMS = 10_000 const DefaultServiceWorkerProbeTimeoutMS = 10_000 @@ -21,6 +30,9 @@ const DefaultTargetSessionPollIntervalMS = 20 var DefaultModCDPServiceWorkerURLSuffixes = []string{"/modcdp/service_worker.js"} var extIDFromURL = regexp.MustCompile(`^chrome-extension://([a-z]+)/`) +//go:embed extension.zip +var bundledExtensionZip []byte + const modcdpReadyExpression = `Boolean(globalThis.ModCDP?.__ModCDPServerVersion >= 1 && globalThis.ModCDP?.handleCommand && globalThis.ModCDP?.addCustomEvent)` type SendCDP = types.SendCDP @@ -42,9 +54,6 @@ type ExtensionInjector struct { } func NewExtensionInjector(options ExtensionInjectorConfig) ExtensionInjector { - if options.InjectorWakePath == "" { - options.InjectorWakePath = DefaultModCDPWakePath - } if options.InjectorCDPSendTimeoutMS == 0 { options.InjectorCDPSendTimeoutMS = DefaultCDPSendTimeoutMS } @@ -85,12 +94,6 @@ func (i *ExtensionInjector) Update(config ExtensionInjectorConfig) *ExtensionInj if config.InjectorExtensionID != "" { i.Options.InjectorExtensionID = config.InjectorExtensionID } - if config.InjectorWakePath != "" { - i.Options.InjectorWakePath = config.InjectorWakePath - } - if config.InjectorWakeURL != "" { - i.Options.InjectorWakeURL = config.InjectorWakeURL - } if config.InjectorServiceWorkerURLIncludes != nil { i.Options.InjectorServiceWorkerURLIncludes = append([]string{}, config.InjectorServiceWorkerURLIncludes...) } @@ -130,9 +133,6 @@ func (i *ExtensionInjector) Update(config ExtensionInjectorConfig) *ExtensionInj if config.InjectorBrowserbaseBaseURL != "" { i.Options.InjectorBrowserbaseBaseURL = config.InjectorBrowserbaseBaseURL } - if config.UpstreamReverseWSURL != "" { - i.Options.UpstreamReverseWSURL = config.UpstreamReverseWSURL - } if config.UpstreamNativeMessagingHostName != "" { i.Options.UpstreamNativeMessagingHostName = config.UpstreamNativeMessagingHostName } @@ -258,41 +258,6 @@ func (i ExtensionInjector) targetInfos() ([]map[string]any, error) { return targets, nil } -func (i ExtensionInjector) configuredWakeURL() string { - if i.Options.InjectorWakeURL != "" { - return i.Options.InjectorWakeURL - } - if i.Options.InjectorExtensionID == "" { - return "" - } - wakePath := i.Options.InjectorWakePath - if wakePath == "" { - wakePath = DefaultModCDPWakePath - } - if !strings.HasPrefix(wakePath, "/") { - wakePath = "/" + wakePath - } - return "chrome-extension://" + i.Options.InjectorExtensionID + wakePath -} - -func (i ExtensionInjector) wakeConfiguredExtension() bool { - wakeURL := i.configuredWakeURL() - if wakeURL == "" || i.Options.Send == nil { - return false - } - _, err := i.sendWithTimeout("Target.createTarget", map[string]any{ - "url": wakeURL, - "background": true, - "hidden": true, - "focus": false, - }, "", i.Options.InjectorCDPSendTimeoutMS) - return err == nil -} - -func (i ExtensionInjector) WakeConfiguredExtension() bool { - return i.wakeConfiguredExtension() -} - func (i ExtensionInjector) probeTarget(target map[string]any, sessionTimeoutMS int, allowAttach bool) (*ExtensionInjectionResult, error) { targetID, _ := target["targetId"].(string) targetURL, _ := target["url"].(string) @@ -416,3 +381,187 @@ func (i ExtensionInjector) serviceWorkerTargetMatches(target map[string]any) boo func (i ExtensionInjector) ServiceWorkerTargetMatches(target map[string]any) bool { return i.serviceWorkerTargetMatches(target) } + +func prepareUnpackedExtension(extensionPath string) (string, string, error) { + if extensionPath == "" { + dir, err := os.MkdirTemp("", "modcdp-extension-") + if err != nil { + return "", "", err + } + reader, err := zip.NewReader(bytes.NewReader(bundledExtensionZip), int64(len(bundledExtensionZip))) + if err != nil { + _ = os.RemoveAll(dir) + return "", "", err + } + if err := extractZipFiles(reader.File, dir); err != nil { + _ = os.RemoveAll(dir) + return "", "", err + } + return extensionRoot(dir), dir, nil + } + if !strings.HasSuffix(extensionPath, ".zip") { + dir, err := os.MkdirTemp("", "modcdp-extension-") + if err != nil { + return "", "", err + } + if err := copyDir(extensionPath, dir); err != nil { + _ = os.RemoveAll(dir) + return "", "", err + } + return dir, dir, nil + } + dir, err := os.MkdirTemp("", "modcdp-extension-") + if err != nil { + return "", "", err + } + reader, err := zip.OpenReader(extensionPath) + if err != nil { + _ = os.RemoveAll(dir) + return "", "", err + } + defer reader.Close() + if err := extractZipFiles(reader.File, dir); err != nil { + _ = os.RemoveAll(dir) + return "", "", err + } + return extensionRoot(dir), dir, nil +} + +func extractZipFiles(files []*zip.File, dir string) error { + root, err := filepath.Abs(dir) + if err != nil { + return err + } + for _, file := range files { + targetPath, err := safeZipTarget(root, file.Name) + if err != nil { + return err + } + if file.FileInfo().IsDir() { + if err := os.MkdirAll(targetPath, 0o755); err != nil { + return err + } + continue + } + if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { + return err + } + src, err := file.Open() + if err != nil { + return err + } + dst, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.FileInfo().Mode()) + if err != nil { + _ = src.Close() + return err + } + _, copyErr := io.Copy(dst, src) + srcErr := src.Close() + dstErr := dst.Close() + if copyErr != nil { + return copyErr + } + if srcErr != nil { + return srcErr + } + if dstErr != nil { + return dstErr + } + } + return nil +} + +func safeZipTarget(root string, name string) (string, error) { + cleanName := filepath.Clean(name) + if filepath.IsAbs(cleanName) || cleanName == "." || cleanName == ".." || strings.HasPrefix(cleanName, ".."+string(os.PathSeparator)) { + return "", fmt.Errorf("zip entry %q escapes extension extraction directory", name) + } + targetPath := filepath.Join(root, cleanName) + targetAbs, err := filepath.Abs(targetPath) + if err != nil { + return "", err + } + if targetAbs != root && !strings.HasPrefix(targetAbs, root+string(os.PathSeparator)) { + return "", fmt.Errorf("zip entry %q escapes extension extraction directory", name) + } + return targetAbs, nil +} + +func extensionRoot(unpackedPath string) string { + if _, err := os.Stat(filepath.Join(unpackedPath, "manifest.json")); err == nil { + return unpackedPath + } + nested := filepath.Join(unpackedPath, "extension") + if _, err := os.Stat(filepath.Join(nested, "manifest.json")); err == nil { + return nested + } + return unpackedPath +} + +func extensionIDFromManifestKey(extensionPath string) (string, error) { + manifestBytes, err := os.ReadFile(filepath.Join(extensionPath, "manifest.json")) + if err != nil { + return "", nil + } + var manifest map[string]any + if err := json.Unmarshal(manifestBytes, &manifest); err != nil { + return "", err + } + key, _ := manifest["key"].(string) + if strings.TrimSpace(key) == "" { + return "", nil + } + keyBytes, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return "", err + } + digest := sha256.Sum256(keyBytes) + alphabet := "abcdefghijklmnop" + result := strings.Builder{} + for _, value := range digest[:16] { + result.WriteByte(alphabet[value>>4]) + result.WriteByte(alphabet[value&0x0f]) + } + return result.String(), nil +} + +func copyDir(src string, dst string) error { + return filepath.WalkDir(src, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + relative, err := filepath.Rel(src, path) + if err != nil { + return err + } + target := filepath.Join(dst, relative) + if entry.IsDir() { + return os.MkdirAll(target, 0o755) + } + info, err := entry.Info() + if err != nil { + return err + } + sourceFile, err := os.Open(path) + if err != nil { + return err + } + defer sourceFile.Close() + targetFile, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode()) + if err != nil { + return err + } + defer targetFile.Close() + _, err = io.Copy(targetFile, sourceFile) + return err + }) +} + +func firstNonEmptyString(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return strings.TrimSpace(value) + } + } + return "" +} diff --git a/go/modcdp/injector/ExtensionInjector_test.go b/go/modcdp/injector/ExtensionInjector_test.go index 416c77e..3be2e6a 100644 --- a/go/modcdp/injector/ExtensionInjector_test.go +++ b/go/modcdp/injector/ExtensionInjector_test.go @@ -1,267 +1,13 @@ package injector_test import ( - "context" - "encoding/json" - "fmt" - modcdp "github.com/browserbase/modcdp/go/modcdp/client" - . "github.com/browserbase/modcdp/go/modcdp/injector" - "path/filepath" "strings" "testing" - "time" - "github.com/gobwas/ws" - "github.com/gobwas/ws/wsutil" + . "github.com/browserbase/modcdp/go/modcdp/injector" ) -type probeExtensionInjector struct { - ExtensionInjector -} - -func (i probeExtensionInjector) Inject() (*ExtensionInjectionResult, error) { - return i.WaitForReadyServiceWorker(i.Options.InjectorServiceWorkerReadyTimeoutMS, true) -} - -func TestExtensionInjectorProbesRealExtensionServiceWorkerWithSharedBaseConfig(t *testing.T) { - extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension")) - if err != nil { - t.Fatal(err) - } - chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{ - Headless: boolPtr(true), - Sandbox: boolPtr(false), - ExtraArgs: []string{"--load-extension=" + extensionPath}, - }).Launch(modcdp.LaunchOptions{}) - if err != nil { - t.Fatal(err) - } - defer chrome.Close() - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - conn, _, _, err := ws.Dial(ctx, chrome.CDPURL) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - - nextID := 0 - send := func(method string, params map[string]any, sessionID string) (map[string]any, error) { - nextID++ - message := map[string]any{"id": nextID, "method": method, "params": params} - if sessionID != "" { - message["sessionId"] = sessionID - } - body, err := json.Marshal(message) - if err != nil { - return nil, err - } - if err := wsutil.WriteClientText(conn, body); err != nil { - return nil, err - } - for { - raw, err := wsutil.ReadServerText(conn) - if err != nil { - return nil, err - } - var response map[string]any - if err := json.Unmarshal(raw, &response); err != nil { - return nil, err - } - responseID, _ := response["id"].(float64) - if int(responseID) != nextID { - continue - } - if errorObject, ok := response["error"].(map[string]any); ok { - return nil, fmt.Errorf("%v", errorObject["message"]) - } - result, _ := response["result"].(map[string]any) - if result == nil { - result = map[string]any{} - } - return result, nil - } - } - - injector := probeExtensionInjector{ExtensionInjector: NewExtensionInjector(ExtensionInjectorConfig{ - Send: send, - AttachToTarget: func(targetID string) string { - result, _ := send("Target.attachToTarget", map[string]any{"targetId": targetID, "flatten": true}, "") - sessionID, _ := result["sessionId"].(string) - return sessionID - }, - InjectorExtensionID: DefaultModCDPExtensionID, - InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, - InjectorTrustServiceWorkerTarget: true, - })} - - transportConfig := injector.GetTransportConfig() - if transportConfig["injector_extension_id"] != DefaultModCDPExtensionID { - t.Fatalf("injector_extension_id = %v", transportConfig["injector_extension_id"]) - } - if len(injector.GetLauncherConfig().ExtraArgs) != 0 { - t.Fatalf("expected empty launcher config") - } - result, err := injector.Inject() - if err != nil { - t.Fatal(err) - } - if result == nil { - t.Fatal("expected service worker probe result") - } - if result.ExtensionID != DefaultModCDPExtensionID { - t.Fatalf("ExtensionID = %q", result.ExtensionID) - } - if filepath.Base(result.URL) != "service_worker.js" { - t.Fatalf("URL = %q", result.URL) - } -} - -func TestExtensionInjectorKeepsModCDPServiceWorkerAliveThroughOffscreenKeepalive(t *testing.T) { - extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension")) - if err != nil { - t.Fatal(err) - } - chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{ - Headless: boolPtr(true), - Sandbox: boolPtr(false), - ExtraArgs: []string{"--load-extension=" + extensionPath}, - }).Launch(modcdp.LaunchOptions{}) - if err != nil { - t.Fatal(err) - } - defer chrome.Close() - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - conn, _, _, err := ws.Dial(ctx, chrome.CDPURL) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - - nextID := 0 - send := func(method string, params map[string]any, sessionID string) (map[string]any, error) { - nextID++ - message := map[string]any{"id": nextID, "method": method, "params": params} - if sessionID != "" { - message["sessionId"] = sessionID - } - body, err := json.Marshal(message) - if err != nil { - return nil, err - } - if err := wsutil.WriteClientText(conn, body); err != nil { - return nil, err - } - for { - raw, err := wsutil.ReadServerText(conn) - if err != nil { - return nil, err - } - var response map[string]any - if err := json.Unmarshal(raw, &response); err != nil { - return nil, err - } - responseID, _ := response["id"].(float64) - if int(responseID) != nextID { - continue - } - if errorObject, ok := response["error"].(map[string]any); ok { - return nil, fmt.Errorf("%v", errorObject["message"]) - } - result, _ := response["result"].(map[string]any) - if result == nil { - result = map[string]any{} - } - return result, nil - } - } - - injector := probeExtensionInjector{ExtensionInjector: NewExtensionInjector(ExtensionInjectorConfig{ - Send: send, - AttachToTarget: func(targetID string) string { - result, _ := send("Target.attachToTarget", map[string]any{"targetId": targetID, "flatten": true}, "") - sessionID, _ := result["sessionId"].(string) - return sessionID - }, - InjectorExtensionID: DefaultModCDPExtensionID, - InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, - InjectorTrustServiceWorkerTarget: true, - })} - - result, err := injector.Inject() - if err != nil { - t.Fatal(err) - } - if result == nil { - t.Fatal("expected service worker probe result") - } - if result.ExtensionID != DefaultModCDPExtensionID { - t.Fatalf("ExtensionID = %q", result.ExtensionID) - } - - foundOffscreen := false - var contexts []any - for attempt := 0; attempt < 50; attempt++ { - contextsResult, err := send("Runtime.evaluate", map[string]any{ - "expression": "chrome.runtime.getContexts({}).then((contexts) => contexts.map((context) => ({ type: context.contextType, url: context.documentUrl || context.origin || '' })))", - "awaitPromise": true, - "returnByValue": true, - }, result.SessionID) - if err != nil { - t.Fatal(err) - } - evaluation, _ := contextsResult["result"].(map[string]any) - contexts, _ = evaluation["value"].([]any) - for _, rawContext := range contexts { - context, _ := rawContext.(map[string]any) - if context["type"] == "OFFSCREEN_DOCUMENT" && - context["url"] == "chrome-extension://"+DefaultModCDPExtensionID+"/offscreen/keepalive.html" { - foundOffscreen = true - } - } - if foundOffscreen { - break - } - time.Sleep(100 * time.Millisecond) - } - if !foundOffscreen { - t.Fatalf("expected offscreen keepalive context, got %#v", contexts) - } - - time.Sleep(3 * time.Second) - targetsResult, err := send("Target.getTargets", map[string]any{}, "") - if err != nil { - t.Fatal(err) - } - targets, _ := targetsResult["targetInfos"].([]any) - foundServiceWorker := false - for _, rawTarget := range targets { - target, _ := rawTarget.(map[string]any) - if target["type"] == "service_worker" && - target["url"] == "chrome-extension://"+DefaultModCDPExtensionID+"/modcdp/service_worker.js" { - foundServiceWorker = true - } - } - if !foundServiceWorker { - t.Fatalf("expected service worker target, got %#v", targets) - } - version, err := send("Runtime.evaluate", map[string]any{ - "expression": "globalThis.ModCDP?.__ModCDPServerVersion", - "returnByValue": true, - }, result.SessionID) - if err != nil { - t.Fatal(err) - } - versionResult, _ := version["result"].(map[string]any) - if versionResult["value"] != float64(2) { - t.Fatalf("ModCDP server version = %#v", versionResult["value"]) - } -} - -func TestExtensionInjectorOwnsSharedConfig(t *testing.T) { +func TestExtensionInjectorOwnsSharedInjectorConfig(t *testing.T) { injector := NewExtensionInjector(ExtensionInjectorConfig{ InjectorExtensionID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, @@ -279,173 +25,20 @@ func TestExtensionInjectorOwnsSharedConfig(t *testing.T) { "type": "service_worker", "url": "chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/modcdp/service_worker.js", }) { - t.Fatalf("expected service worker target to match") - } - if _, err := injector.Inject(); err == nil { - t.Fatalf("expected base Inject to fail") - } -} - -func TestExtensionInjectorSendWithTimeoutEnforcesCDPSendTimeout(t *testing.T) { - chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{ - Headless: boolPtr(true), - Sandbox: boolPtr(false), - }).Launch(modcdp.LaunchOptions{}) - if err != nil { - t.Fatal(err) - } - defer chrome.Close() - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - conn, _, _, err := ws.Dial(ctx, chrome.CDPURL) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - - nextID := 0 - send := func(method string, params map[string]any, sessionID string) (map[string]any, error) { - nextID++ - message := map[string]any{"id": nextID, "method": method, "params": params} - if sessionID != "" { - message["sessionId"] = sessionID - } - body, err := json.Marshal(message) - if err != nil { - return nil, err - } - if err := wsutil.WriteClientText(conn, body); err != nil { - return nil, err - } - for { - raw, err := wsutil.ReadServerText(conn) - if err != nil { - return nil, err - } - var response map[string]any - if err := json.Unmarshal(raw, &response); err != nil { - return nil, err - } - responseID, _ := response["id"].(float64) - if int(responseID) != nextID { - continue - } - if errorObject, ok := response["error"].(map[string]any); ok { - return nil, fmt.Errorf("%v", errorObject["message"]) - } - result, _ := response["result"].(map[string]any) - if result == nil { - result = map[string]any{} - } - return result, nil - } - } - - created, err := send("Target.createTarget", map[string]any{"url": "about:blank#modcdp-timeout"}, "") - if err != nil { - t.Fatal(err) + t.Fatal("expected service worker target to match") } - targetID, _ := created["targetId"].(string) - attached, err := send("Target.attachToTarget", map[string]any{"targetId": targetID, "flatten": true}, "") - if err != nil { - t.Fatal(err) - } - sessionID, _ := attached["sessionId"].(string) - if _, err := send("Runtime.enable", map[string]any{}, sessionID); err != nil { - t.Fatal(err) - } - injector := NewExtensionInjector(ExtensionInjectorConfig{ - Send: send, - }) - - if _, err := injector.SendWithTimeout("Runtime.evaluate", map[string]any{ - "expression": "new Promise(() => {})", - "awaitPromise": true, - }, sessionID, 5); err == nil || !strings.Contains(err.Error(), "Runtime.evaluate timed out after 5ms") { - t.Fatalf("SendWithTimeout error = %v", err) + if injector.ServiceWorkerTargetMatches(map[string]any{ + "targetId": "target-1", + "type": "service_worker", + "url": "chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/background.js", + }) { + t.Fatal("expected background target not to match") } } -func TestExtensionInjectorWakesConfiguredExtensionWithHiddenBackgroundTarget(t *testing.T) { - extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension")) - if err != nil { - t.Fatal(err) - } - chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{ - Headless: boolPtr(true), - Sandbox: boolPtr(false), - ExtraArgs: []string{"--load-extension=" + extensionPath}, - }).Launch(modcdp.LaunchOptions{}) - if err != nil { - t.Fatal(err) - } - defer chrome.Close() - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - conn, _, _, err := ws.Dial(ctx, chrome.CDPURL) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - - nextID := 0 - send := func(method string, params map[string]any, sessionID string) (map[string]any, error) { - nextID++ - message := map[string]any{"id": nextID, "method": method, "params": params} - if sessionID != "" { - message["sessionId"] = sessionID - } - body, err := json.Marshal(message) - if err != nil { - return nil, err - } - if err := wsutil.WriteClientText(conn, body); err != nil { - return nil, err - } - for { - raw, err := wsutil.ReadServerText(conn) - if err != nil { - return nil, err - } - var response map[string]any - if err := json.Unmarshal(raw, &response); err != nil { - return nil, err - } - responseID, _ := response["id"].(float64) - if int(responseID) != nextID { - continue - } - if errorObject, ok := response["error"].(map[string]any); ok { - return nil, fmt.Errorf("%v", errorObject["message"]) - } - result, _ := response["result"].(map[string]any) - if result == nil { - result = map[string]any{} - } - return result, nil - } - } - - injector := NewExtensionInjector(ExtensionInjectorConfig{ - InjectorExtensionID: DefaultModCDPExtensionID, - Send: send, - }) - - if !injector.WakeConfiguredExtension() { - t.Fatalf("expected wake to succeed") - } - targetsResult, err := send("Target.getTargets", map[string]any{}, "") - if err != nil { - t.Fatal(err) - } - targets, _ := targetsResult["targetInfos"].([]any) - for _, rawTarget := range targets { - target, _ := rawTarget.(map[string]any) - if target["url"] == "chrome-extension://"+DefaultModCDPExtensionID+"/modcdp/wake.html" { - return - } +func TestExtensionInjectorBaseInjectReportsTheClassName(t *testing.T) { + injector := NewExtensionInjector(ExtensionInjectorConfig{}) + if _, err := injector.Inject(); err == nil || !strings.Contains(err.Error(), "ExtensionInjector.Inject is not implemented") { + t.Fatalf("Inject error = %v", err) } - t.Fatalf("expected wake target, got %#v", targets) } diff --git a/go/modcdp/injector/ExtensionsLoadUnpackedInjector.go b/go/modcdp/injector/ExtensionsLoadUnpackedInjector.go index 2c80efa..d13def9 100644 --- a/go/modcdp/injector/ExtensionsLoadUnpackedInjector.go +++ b/go/modcdp/injector/ExtensionsLoadUnpackedInjector.go @@ -22,7 +22,7 @@ func (i *ExtensionsLoadUnpackedInjector) Prepare() error { if i.UnpackedExtensionPath != "" { return nil } - unpackedPath, cleanupPath, err := prepareUnpackedExtension(extensionPath, true) + unpackedPath, cleanupPath, err := prepareUnpackedExtension(extensionPath) if err != nil { return err } @@ -51,7 +51,6 @@ func (i *ExtensionsLoadUnpackedInjector) Inject() (*ExtensionInjectionResult, er return nil, fmt.Errorf("Extensions.loadUnpacked returned no extension id") } i.Options.InjectorExtensionID = extensionID - i.wakeConfiguredExtension() swURLPrefix := "chrome-extension://" + extensionID + "/" deadline := time.Now().Add(time.Duration(i.Options.InjectorServiceWorkerReadyTimeoutMS) * time.Millisecond) for time.Now().Before(deadline) { diff --git a/go/modcdp/injector/ExtensionsLoadUnpackedInjector_test.go b/go/modcdp/injector/ExtensionsLoadUnpackedInjector_test.go index 3c89bad..9251461 100644 --- a/go/modcdp/injector/ExtensionsLoadUnpackedInjector_test.go +++ b/go/modcdp/injector/ExtensionsLoadUnpackedInjector_test.go @@ -1,103 +1,28 @@ package injector_test import ( - "context" - "encoding/json" - "fmt" - modcdp "github.com/browserbase/modcdp/go/modcdp/client" - . "github.com/browserbase/modcdp/go/modcdp/injector" + "os" "path/filepath" "strings" "testing" - "time" - "github.com/gobwas/ws" - "github.com/gobwas/ws/wsutil" + . "github.com/browserbase/modcdp/go/modcdp/injector" ) -func TestExtensionsLoadUnpackedInjectorExercisesRealCDPLoadUnpackedPath(t *testing.T) { - extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension")) - if err != nil { - t.Fatal(err) - } - chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{ - Headless: boolPtr(true), - Sandbox: boolPtr(false), - }).Launch(modcdp.LaunchOptions{}) - if err != nil { - t.Fatal(err) - } - defer chrome.Close() - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - conn, _, _, err := ws.Dial(ctx, chrome.CDPURL) - if err != nil { - t.Fatal(err) - } - defer conn.Close() - - nextID := 0 - send := func(method string, params map[string]any, sessionID string) (map[string]any, error) { - nextID++ - message := map[string]any{"id": nextID, "method": method, "params": params} - if sessionID != "" { - message["sessionId"] = sessionID - } - body, err := json.Marshal(message) - if err != nil { - return nil, err - } - if err := wsutil.WriteClientText(conn, body); err != nil { - return nil, err - } - for { - raw, err := wsutil.ReadServerText(conn) - if err != nil { - return nil, err - } - var response map[string]any - if err := json.Unmarshal(raw, &response); err != nil { - return nil, err - } - responseID, _ := response["id"].(float64) - if int(responseID) != nextID { - continue - } - if errorObject, ok := response["error"].(map[string]any); ok { - return nil, fmt.Errorf("%v", errorObject["message"]) - } - result, _ := response["result"].(map[string]any) - if result == nil { - result = map[string]any{} - } - return result, nil - } - } - - injector := NewExtensionsLoadUnpackedInjector(ExtensionInjectorConfig{ - Send: send, - InjectorExtensionPath: extensionPath, - }) +func TestExtensionsLoadUnpackedInjectorPreparesDefaultPackagedExtensionZip(t *testing.T) { + injector := NewExtensionsLoadUnpackedInjector(ExtensionInjectorConfig{}) if err := injector.Prepare(); err != nil { t.Fatal(err) } defer injector.Close() - result, err := injector.Inject() - if err != nil { - t.Fatal(err) - } - if result != nil { - t.Fatalf("result = %#v", result) + if injector.UnpackedExtensionPath == "" { + t.Fatal("expected UnpackedExtensionPath") } - if injector.LastError == nil { - t.Fatal("expected LastError from real Extensions.loadUnpacked attempt") + if !strings.Contains(injector.UnpackedExtensionPath, "modcdp-extension-") { + t.Fatalf("UnpackedExtensionPath = %q", injector.UnpackedExtensionPath) } - message := injector.LastError.Error() - if !strings.Contains(message, "Method not available") && - !strings.Contains(message, "Method not found") && - !strings.Contains(message, "wasn't found") { - t.Fatalf("LastError = %v", injector.LastError) + if _, err := os.Stat(filepath.Join(injector.UnpackedExtensionPath, "manifest.json")); err != nil { + t.Fatalf("expected unpacked manifest: %v", err) } } diff --git a/go/modcdp/injector/LocalBrowserLaunchExtensionInjector.go b/go/modcdp/injector/LocalBrowserLaunchExtensionInjector.go index d6f14a5..927bdaf 100644 --- a/go/modcdp/injector/LocalBrowserLaunchExtensionInjector.go +++ b/go/modcdp/injector/LocalBrowserLaunchExtensionInjector.go @@ -1,23 +1,9 @@ package injector import ( - "archive/zip" - "bytes" - "crypto/sha256" - _ "embed" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "io/fs" "os" - "path/filepath" - "strings" ) -//go:embed extension.zip -var bundledExtensionZip []byte - type LocalBrowserLaunchExtensionInjector struct { ExtensionInjector UnpackedExtensionPath string @@ -34,7 +20,7 @@ func (i *LocalBrowserLaunchExtensionInjector) Prepare() error { if i.UnpackedExtensionPath != "" { return nil } - unpackedPath, cleanupPath, err := prepareUnpackedExtension(extensionPath, true) + unpackedPath, cleanupPath, err := prepareUnpackedExtension(extensionPath) if err != nil { return err } @@ -52,8 +38,7 @@ func (i *LocalBrowserLaunchExtensionInjector) GetLauncherConfig() LaunchOptions } func (i *LocalBrowserLaunchExtensionInjector) Inject() (*ExtensionInjectionResult, error) { - timeoutMS := i.Options.InjectorServiceWorkerProbeTimeoutMS - discovered, err := i.waitForReadyServiceWorker(timeoutMS, i.Options.InjectorTrustServiceWorkerTarget) + discovered, err := i.discoverReadyServiceWorker(i.Options.InjectorTrustServiceWorkerTarget) if err != nil || discovered == nil { return discovered, err } @@ -87,190 +72,3 @@ func (i *LocalBrowserLaunchExtensionInjector) resolveExtensionID() (string, erro } return i.ExtensionID, nil } - -func prepareUnpackedExtension(extensionPath string, copyDirectory bool) (string, string, error) { - if extensionPath == "" { - dir, err := os.MkdirTemp("", "modcdp-extension-") - if err != nil { - return "", "", err - } - reader, err := zip.NewReader(bytes.NewReader(bundledExtensionZip), int64(len(bundledExtensionZip))) - if err != nil { - _ = os.RemoveAll(dir) - return "", "", err - } - if err := extractZipFiles(reader.File, dir); err != nil { - _ = os.RemoveAll(dir) - return "", "", err - } - return extensionRoot(dir), dir, nil - } - if !strings.HasSuffix(extensionPath, ".zip") { - if !copyDirectory { - return extensionPath, "", nil - } - dir, err := os.MkdirTemp("", "modcdp-extension-") - if err != nil { - return "", "", err - } - if err := copyDir(extensionPath, dir); err != nil { - _ = os.RemoveAll(dir) - return "", "", err - } - return dir, dir, nil - } - dir, err := os.MkdirTemp("", "modcdp-extension-") - if err != nil { - return "", "", err - } - reader, err := zip.OpenReader(extensionPath) - if err != nil { - _ = os.RemoveAll(dir) - return "", "", err - } - defer reader.Close() - if err := extractZipFiles(reader.File, dir); err != nil { - _ = os.RemoveAll(dir) - return "", "", err - } - return extensionRoot(dir), dir, nil -} - -func extractZipFiles(files []*zip.File, dir string) error { - root, err := filepath.Abs(dir) - if err != nil { - return err - } - for _, file := range files { - targetPath, err := safeZipTarget(root, file.Name) - if err != nil { - return err - } - if file.FileInfo().IsDir() { - if err := os.MkdirAll(targetPath, 0o755); err != nil { - return err - } - continue - } - if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil { - return err - } - src, err := file.Open() - if err != nil { - return err - } - dst, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.FileInfo().Mode()) - if err != nil { - _ = src.Close() - return err - } - _, copyErr := io.Copy(dst, src) - srcErr := src.Close() - dstErr := dst.Close() - if copyErr != nil { - return copyErr - } - if srcErr != nil { - return srcErr - } - if dstErr != nil { - return dstErr - } - } - return nil -} - -func safeZipTarget(root string, name string) (string, error) { - cleanName := filepath.Clean(name) - if filepath.IsAbs(cleanName) || cleanName == "." || cleanName == ".." || strings.HasPrefix(cleanName, ".."+string(os.PathSeparator)) { - return "", fmt.Errorf("zip entry %q escapes extension extraction directory", name) - } - targetPath := filepath.Join(root, cleanName) - targetAbs, err := filepath.Abs(targetPath) - if err != nil { - return "", err - } - if targetAbs != root && !strings.HasPrefix(targetAbs, root+string(os.PathSeparator)) { - return "", fmt.Errorf("zip entry %q escapes extension extraction directory", name) - } - return targetAbs, nil -} - -func extensionRoot(unpackedPath string) string { - if _, err := os.Stat(filepath.Join(unpackedPath, "manifest.json")); err == nil { - return unpackedPath - } - nested := filepath.Join(unpackedPath, "extension") - if _, err := os.Stat(filepath.Join(nested, "manifest.json")); err == nil { - return nested - } - return unpackedPath -} - -func extensionIDFromManifestKey(extensionPath string) (string, error) { - manifestBytes, err := os.ReadFile(filepath.Join(extensionPath, "manifest.json")) - if err != nil { - return "", nil - } - var manifest map[string]any - if err := json.Unmarshal(manifestBytes, &manifest); err != nil { - return "", err - } - key, _ := manifest["key"].(string) - if strings.TrimSpace(key) == "" { - return "", nil - } - keyBytes, err := base64.StdEncoding.DecodeString(key) - if err != nil { - return "", err - } - digest := sha256.Sum256(keyBytes) - alphabet := "abcdefghijklmnop" - result := strings.Builder{} - for _, value := range digest[:16] { - result.WriteByte(alphabet[value>>4]) - result.WriteByte(alphabet[value&0x0f]) - } - return result.String(), nil -} - -func copyDir(src string, dst string) error { - return filepath.WalkDir(src, func(path string, entry fs.DirEntry, walkErr error) error { - if walkErr != nil { - return walkErr - } - relative, err := filepath.Rel(src, path) - if err != nil { - return err - } - target := filepath.Join(dst, relative) - if entry.IsDir() { - return os.MkdirAll(target, 0o755) - } - info, err := entry.Info() - if err != nil { - return err - } - sourceFile, err := os.Open(path) - if err != nil { - return err - } - defer sourceFile.Close() - targetFile, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, info.Mode()) - if err != nil { - return err - } - defer targetFile.Close() - _, err = io.Copy(targetFile, sourceFile) - return err - }) -} - -func firstNonEmptyString(values ...string) string { - for _, value := range values { - if strings.TrimSpace(value) != "" { - return strings.TrimSpace(value) - } - } - return "" -} diff --git a/go/modcdp/injector/LocalBrowserLaunchExtensionInjector_test.go b/go/modcdp/injector/LocalBrowserLaunchExtensionInjector_test.go index 60ea660..3d19046 100644 --- a/go/modcdp/injector/LocalBrowserLaunchExtensionInjector_test.go +++ b/go/modcdp/injector/LocalBrowserLaunchExtensionInjector_test.go @@ -2,13 +2,12 @@ package injector_test import ( "archive/zip" - modcdp "github.com/browserbase/modcdp/go/modcdp/client" . "github.com/browserbase/modcdp/go/modcdp/injector" "os" "path/filepath" - "runtime" "strings" "testing" + "time" ) func TestLocalBrowserLaunchExtensionInjectorRejectsZipEntriesOutsideExtractionDir(t *testing.T) { @@ -42,73 +41,96 @@ func TestLocalBrowserLaunchExtensionInjectorRejectsZipEntriesOutsideExtractionDi } } -func TestLocalBrowserLaunchExtensionInjectorLoadsRealExtensionDuringLocalLaunch(t *testing.T) { +func TestLocalBrowserLaunchExtensionInjectorPreparesUnpackedExtensionDirectoryForLoadExtension(t *testing.T) { extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension")) if err != nil { t.Fatal(err) } - headless := runtime.GOOS == "linux" && os.Getenv("DISPLAY") == "" - sandbox := runtime.GOOS != "linux" - cdp := modcdp.New(modcdp.Options{ - Launcher: modcdp.LauncherConfig{LauncherMode: "local", - LauncherOptions: modcdp.LaunchOptions{ - Headless: boolPtr(headless), - Sandbox: boolPtr(sandbox), - }, - }, - Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws"}, - Injector: modcdp.InjectorConfig{ - InjectorMode: "inject", - InjectorExtensionPath: extensionPath, - InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, - InjectorTrustServiceWorkerTarget: true, - InjectorServiceWorkerProbeTimeoutMS: 30_000, - }, - Client: modcdp.ClientConfig{ - ClientCDPSendTimeoutMS: 30_000, - }, - }) - defer cdp.Close() - - if err := cdp.Connect(); err != nil { + injector := NewLocalBrowserLaunchExtensionInjector(ExtensionInjectorConfig{InjectorExtensionPath: extensionPath}) + if err := injector.Prepare(); err != nil { t.Fatal(err) } - if cdp.ConnectTiming["injector_source"] != "local_launch" { - t.Fatalf("injector_source = %v", cdp.ConnectTiming["injector_source"]) + defer injector.Close() + + if injector.UnpackedExtensionPath == "" { + t.Fatal("expected UnpackedExtensionPath") } - if cdp.ExtensionID != DefaultModCDPExtensionID { - t.Fatalf("ExtensionID = %q", cdp.ExtensionID) + if injector.UnpackedExtensionPath == extensionPath { + t.Fatalf("UnpackedExtensionPath = %q", injector.UnpackedExtensionPath) } - if cdp.ExtSessionID == "" { - t.Fatal("expected ExtSessionID") + if _, err := os.Stat(filepath.Join(injector.UnpackedExtensionPath, "manifest.json")); err != nil { + t.Fatalf("expected unpacked manifest: %v", err) } - result, err := cdp.Mod.Evaluate(map[string]any{ - "expression": "chrome.runtime.getURL('modcdp/service_worker.js')", - }) - if err != nil { + launcherConfig := injector.GetLauncherConfig() + if len(launcherConfig.ExtraArgs) != 1 || launcherConfig.ExtraArgs[0] != "--load-extension="+injector.UnpackedExtensionPath { + t.Fatalf("ExtraArgs = %#v", launcherConfig.ExtraArgs) + } + if injector.Options.InjectorExtensionID != DefaultModCDPExtensionID { + t.Fatalf("InjectorExtensionID = %q", injector.Options.InjectorExtensionID) + } +} + +func TestLocalBrowserLaunchExtensionInjectorPreparesDefaultExtensionZipForLoadExtension(t *testing.T) { + injector := NewLocalBrowserLaunchExtensionInjector(ExtensionInjectorConfig{}) + if err := injector.Prepare(); err != nil { t.Fatal(err) } - if result != "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js" { - t.Fatalf("Mod.evaluate = %#v", result) + defer injector.Close() + + if injector.UnpackedExtensionPath == "" { + t.Fatal("expected UnpackedExtensionPath") + } + if !strings.Contains(injector.UnpackedExtensionPath, "modcdp-extension-") { + t.Fatalf("UnpackedExtensionPath = %q", injector.UnpackedExtensionPath) + } + if _, err := os.Stat(filepath.Join(injector.UnpackedExtensionPath, "manifest.json")); err != nil { + t.Fatalf("expected unpacked manifest: %v", err) + } + launcherConfig := injector.GetLauncherConfig() + if len(launcherConfig.ExtraArgs) != 1 || launcherConfig.ExtraArgs[0] != "--load-extension="+injector.UnpackedExtensionPath { + t.Fatalf("ExtraArgs = %#v", launcherConfig.ExtraArgs) + } + if injector.Options.InjectorExtensionID != DefaultModCDPExtensionID { + t.Fatalf("InjectorExtensionID = %q", injector.Options.InjectorExtensionID) } } -func TestLocalBrowserLaunchExtensionInjectorPreparesLauncherConfig(t *testing.T) { +func TestLocalBrowserLaunchExtensionInjectorReturnsImmediatelyWhenLaunchedExtensionTargetIsAbsent(t *testing.T) { extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension")) if err != nil { t.Fatal(err) } - injector := NewLocalBrowserLaunchExtensionInjector(ExtensionInjectorConfig{InjectorExtensionPath: extensionPath}) + methods := []string{} + injector := NewLocalBrowserLaunchExtensionInjector(ExtensionInjectorConfig{ + InjectorExtensionPath: extensionPath, + InjectorTrustServiceWorkerTarget: true, + Send: func(method string, params map[string]any, sessionID string) (map[string]any, error) { + methods = append(methods, method) + if method == "Target.getTargets" { + return map[string]any{"targetInfos": []any{}}, nil + } + t.Fatalf("unexpected %s", method) + return nil, nil + }, + }) if err := injector.Prepare(); err != nil { t.Fatal(err) } defer injector.Close() - launchConfig := injector.GetLauncherConfig() - if len(launchConfig.ExtraArgs) != 1 || !strings.HasPrefix(launchConfig.ExtraArgs[0], "--load-extension=") { - t.Fatalf("ExtraArgs = %v", launchConfig.ExtraArgs) + startedAt := time.Now() + result, err := injector.Inject() + if err != nil { + t.Fatal(err) + } + elapsed := time.Since(startedAt) + if result != nil { + t.Fatalf("result = %#v", result) + } + if strings.Join(methods, ",") != "Target.getTargets" { + t.Fatalf("methods = %#v", methods) } - if injector.ExtensionID != DefaultModCDPExtensionID { - t.Fatalf("ExtensionID = %q", injector.ExtensionID) + if elapsed >= 200*time.Millisecond { + t.Fatalf("Inject took %s", elapsed) } } diff --git a/go/modcdp/injector/extension.zip b/go/modcdp/injector/extension.zip index 268e970..2f87949 100644 Binary files a/go/modcdp/injector/extension.zip and b/go/modcdp/injector/extension.zip differ diff --git a/go/modcdp/launcher/LocalBrowserLauncher.go b/go/modcdp/launcher/LocalBrowserLauncher.go index dc20755..617e5b5 100644 --- a/go/modcdp/launcher/LocalBrowserLauncher.go +++ b/go/modcdp/launcher/LocalBrowserLauncher.go @@ -52,12 +52,6 @@ func (l *LocalBrowserLauncher) Launch(options LaunchOptions) (*LaunchedBrowser, usePipe := options.RemoteDebugging == "pipe" useLoopbackCDP := !usePipe || options.Port != 0 || (options.LoopbackCDP != nil && *options.LoopbackCDP) port := options.Port - if useLoopbackCDP && port == 0 { - port, err = l.FreePort() - if err != nil { - return nil, err - } - } profileDir := options.UserDataDir ownsProfileDir := false if profileDir == "" { @@ -102,7 +96,11 @@ func (l *LocalBrowserLauncher) Launch(options LaunchOptions) (*LaunchedBrowser, if headless { args = append(args, "--headless=new") } - if options.Sandbox == nil || !*options.Sandbox { + sandbox := runtime.GOOS != "linux" + if options.Sandbox != nil { + sandbox = *options.Sandbox + } + if !sandbox { args = append(args, "--no-sandbox") } args = append(args, options.Args...) @@ -149,6 +147,13 @@ func (l *LocalBrowserLauncher) Launch(options LaunchOptions) (*LaunchedBrowser, } return nil, err } + processDone := make(chan struct{}) + var processState *os.ProcessState + var processWaitErr error + go func() { + processState, processWaitErr = cmd.Process.Wait() + close(processDone) + }() close := func() { if pipeRead != nil { _ = pipeRead.Close() @@ -157,39 +162,60 @@ func (l *LocalBrowserLauncher) Launch(options LaunchOptions) (*LaunchedBrowser, _ = pipeWrite.Close() } if cmd.Process != nil { - if runtime.GOOS != "windows" { - _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM) - } else { - _ = cmd.Process.Kill() - } - done := make(chan struct{}) - go func() { - _, _ = cmd.Process.Wait() - close(done) - }() select { - case <-done: - case <-time.After(2 * time.Second): + case <-processDone: + default: if runtime.GOOS != "windows" { - _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) + _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGTERM) } else { _ = cmd.Process.Kill() } - <-done + select { + case <-processDone: + case <-time.After(2 * time.Second): + if runtime.GOOS != "windows" { + _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) + } else { + _ = cmd.Process.Kill() + } + <-processDone + } } } if cleanupProfileDir { removeProfileDir(profileDir) } } + processExitedError := func() error { + select { + case <-processDone: + if processWaitErr != nil { + return fmt.Errorf("Chrome exited before CDP became ready: %w", processWaitErr) + } + if processState != nil { + return fmt.Errorf("Chrome exited before CDP became ready (exit=%d)", processState.ExitCode()) + } + return fmt.Errorf("Chrome exited before CDP became ready") + default: + return nil + } + } if usePipe { + if err := processExitedError(); err != nil { + close() + return nil, err + } if err := waitForPipeReady(pipeRead, pipeWrite, time.Duration(chromeReadyTimeoutMS)*time.Millisecond); err != nil { close() return nil, err } loopbackCDPURL := "" if useLoopbackCDP { - loopbackCDPURL, err = waitForCdpWebSocketURL(fmt.Sprintf("http://127.0.0.1:%d", port), time.Duration(chromeReadyTimeoutMS)*time.Millisecond, time.Duration(chromeReadyPollIntervalMS)*time.Millisecond) + if port == 0 { + loopbackCDPURL, _, err = waitForBrowserSelectedCdpWebSocketURL(profileDir, time.Duration(chromeReadyTimeoutMS)*time.Millisecond, time.Duration(chromeReadyPollIntervalMS)*time.Millisecond) + } else { + loopbackCDPURL, err = waitForCdpWebSocketURL(fmt.Sprintf("http://127.0.0.1:%d", port), time.Duration(chromeReadyTimeoutMS)*time.Millisecond, time.Duration(chromeReadyPollIntervalMS)*time.Millisecond) + } if err != nil { close() return nil, err @@ -206,10 +232,28 @@ func (l *LocalBrowserLauncher) Launch(options LaunchOptions) (*LaunchedBrowser, l.Launched = launched return launched, nil } - cdpURL := fmt.Sprintf("http://127.0.0.1:%d", port) deadline := time.Now().Add(time.Duration(chromeReadyTimeoutMS) * time.Millisecond) client := &http.Client{Timeout: 2 * time.Second} for time.Now().Before(deadline) { + if err := processExitedError(); err != nil { + close() + return nil, err + } + cdpURL := "" + if port == 0 { + activePort, ready, err := readDevToolsActivePort(profileDir) + if err != nil { + close() + return nil, err + } + if !ready { + time.Sleep(time.Duration(chromeReadyPollIntervalMS) * time.Millisecond) + continue + } + cdpURL = fmt.Sprintf("http://127.0.0.1:%d", activePort) + } else { + cdpURL = fmt.Sprintf("http://127.0.0.1:%d", port) + } resp, err := client.Get(cdpURL + "/json/version") if err == nil { var version map[string]any @@ -229,7 +273,7 @@ func (l *LocalBrowserLauncher) Launch(options LaunchOptions) (*LaunchedBrowser, time.Sleep(time.Duration(chromeReadyPollIntervalMS) * time.Millisecond) } close() - return nil, fmt.Errorf("Chrome at %s did not become ready within %dms", cdpURL, chromeReadyTimeoutMS) + return nil, fmt.Errorf("Chrome did not become ready within %dms", chromeReadyTimeoutMS) } func waitForPipeReady(pipeRead *os.File, pipeWrite *os.File, timeout time.Duration) error { @@ -279,6 +323,49 @@ func waitForCdpWebSocketURL(cdpURL string, timeout time.Duration, pollInterval t return "", fmt.Errorf("Chrome at %s did not expose a WebSocket CDP URL within %s", cdpURL, timeout) } +func readDevToolsActivePort(profileDir string) (int, bool, error) { + body, err := os.ReadFile(filepath.Join(profileDir, "DevToolsActivePort")) + if err != nil { + if os.IsNotExist(err) { + return 0, false, nil + } + return 0, false, err + } + lines := strings.Split(strings.TrimSpace(string(body)), "\n") + if len(lines) < 2 || strings.TrimSpace(lines[0]) == "" || strings.TrimSpace(lines[1]) == "" { + return 0, false, nil + } + port, err := strconv.Atoi(strings.TrimSpace(lines[0])) + if err != nil || port <= 0 { + return 0, false, fmt.Errorf("invalid DevToolsActivePort port: %s", strings.TrimSpace(lines[0])) + } + return port, true, nil +} + +func waitForBrowserSelectedCdpWebSocketURL(profileDir string, timeout time.Duration, pollInterval time.Duration) (string, int, error) { + deadline := time.Now().Add(timeout) + var lastErr error + for time.Now().Before(deadline) { + port, ready, err := readDevToolsActivePort(profileDir) + if err != nil { + return "", 0, err + } + if ready { + cdpURL := fmt.Sprintf("http://127.0.0.1:%d", port) + loopbackCDPURL, err := websocketURLFor(cdpURL) + if err == nil && loopbackCDPURL != "" { + return loopbackCDPURL, port, nil + } + lastErr = err + } + time.Sleep(pollInterval) + } + if lastErr != nil { + return "", 0, fmt.Errorf("Chrome did not expose DevToolsActivePort from %s within %s: %w", profileDir, timeout, lastErr) + } + return "", 0, fmt.Errorf("Chrome did not expose DevToolsActivePort from %s within %s", profileDir, timeout) +} + func removeProfileDir(profileDir string) { for attempt := 0; attempt < 5; attempt++ { if err := os.RemoveAll(profileDir); err == nil { @@ -359,6 +446,7 @@ func candidatePaths() []string { var canary []string var stock []string + var chromium []string switch runtime.GOOS { case "darwin": canary = []string{"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary"} @@ -367,12 +455,14 @@ func candidatePaths() []string { canary = append([]string{filepath.Join(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe")}, joinAll(programFiles, "Google", "Chrome SxS", "Application", "chrome.exe")...) stock = append(joinAll(programFiles, "Google", "Chrome", "Application", "chrome.exe"), filepath.Join(localAppData, "Google", "Chrome", "Application", "chrome.exe")) default: + chromium = []string{"/usr/bin/chromium", "/usr/bin/chromium-browser"} canary = []string{"/usr/bin/google-chrome-canary", "/usr/bin/google-chrome-unstable", "/opt/google/chrome-unstable/chrome"} stock = []string{"/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/opt/google/chrome/chrome"} } - result := append([]string{}, chromeForTestingCandidates()...) + result := append([]string{}, chromium...) result = append(result, canary...) + result = append(result, chromeForTestingCandidates()...) result = append(result, stock...) return result } diff --git a/go/modcdp/launcher/LocalBrowserLauncher_test.go b/go/modcdp/launcher/LocalBrowserLauncher_test.go index 404f8b6..92f463d 100644 --- a/go/modcdp/launcher/LocalBrowserLauncher_test.go +++ b/go/modcdp/launcher/LocalBrowserLauncher_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "os" + "runtime" "strconv" "strings" "testing" @@ -23,9 +24,8 @@ func TestLocalBrowserLauncherClassHelpersMatchLocalLauncherSurface(t *testing.T) } } -func TestLocalBrowserLauncherLaunchesRealBrowserAndSpeaksCDP(t *testing.T) { +func TestLocalBrowserLauncherLaunchesRealBrowserOverChosenCDPPortAndHonorsLaunchOptions(t *testing.T) { headless := true - sandbox := false profileDir := t.TempDir() port, err := freePort() if err != nil { @@ -33,7 +33,6 @@ func TestLocalBrowserLauncherLaunchesRealBrowserAndSpeaksCDP(t *testing.T) { } launcher := NewLocalBrowserLauncher(LaunchOptions{ Headless: &headless, - Sandbox: &sandbox, ChromeReadyTimeoutMS: 45_000, ChromeReadyPollIntervalMS: 50, }) @@ -103,14 +102,42 @@ func TestLocalBrowserLauncherLaunchesRealBrowserAndSpeaksCDP(t *testing.T) { if response.Result.ProtocolVersion == "" { t.Fatal("expected protocolVersion") } + if err := wsutil.WriteClientText(conn, []byte(`{"id":2,"method":"SystemInfo.getInfo","params":{}}`)); err != nil { + t.Fatal(err) + } + systemInfoBody, err := wsutil.ReadServerText(conn) + if err != nil { + t.Fatal(err) + } + var systemInfoResponse struct { + ID int `json:"id"` + Result struct { + CommandLine string `json:"commandLine"` + } `json:"result"` + } + if err := json.Unmarshal(systemInfoBody, &systemInfoResponse); err != nil { + t.Fatal(err) + } + if systemInfoResponse.ID != 2 { + t.Fatalf("unexpected SystemInfo.getInfo response id %d", systemInfoResponse.ID) + } + command := systemInfoResponse.Result.CommandLine + if !strings.Contains(command, "--window-size=900,700") { + t.Fatalf("expected browser command to include --window-size=900,700: %s", command) + } + if runtime.GOOS == "linux" { + if !strings.Contains(command, "--no-sandbox") { + t.Fatalf("expected Linux browser command to include --no-sandbox: %s", command) + } + } else if strings.Contains(command, "--no-sandbox") { + t.Fatalf("expected browser command not to include --no-sandbox: %s", command) + } } func TestLocalBrowserLauncherLaunchesRealBrowserOverRemoteDebuggingPipe(t *testing.T) { headless := true - sandbox := false launcher := NewLocalBrowserLauncher(LaunchOptions{ Headless: &headless, - Sandbox: &sandbox, ChromeReadyTimeoutMS: 45_000, }) chrome, err := launcher.Launch(LaunchOptions{RemoteDebugging: "pipe"}) @@ -159,11 +186,9 @@ func TestLocalBrowserLauncherLaunchesRealBrowserOverRemoteDebuggingPipe(t *testi func TestLocalBrowserLauncherLaunchesPipeBrowserWithAuxiliaryLoopbackOnlyWhenRequested(t *testing.T) { headless := true - sandbox := false loopbackCDP := true chrome, err := NewLocalBrowserLauncher(LaunchOptions{ Headless: &headless, - Sandbox: &sandbox, ChromeReadyTimeoutMS: 45_000, }).Launch(LaunchOptions{RemoteDebugging: "pipe", LoopbackCDP: &loopbackCDP}) if err != nil { @@ -201,7 +226,6 @@ func TestLocalBrowserLauncherLaunchesPipeBrowserWithAuxiliaryLoopbackOnlyWhenReq func TestLocalBrowserLauncherCleansExplicitUserDataDirWhenRequested(t *testing.T) { headless := true - sandbox := false cleanupUserDataDir := true profileDir, err := os.MkdirTemp("", "modcdp-go-local-profile-") if err != nil { @@ -209,7 +233,6 @@ func TestLocalBrowserLauncherCleansExplicitUserDataDirWhenRequested(t *testing.T } chrome, err := NewLocalBrowserLauncher(LaunchOptions{ Headless: &headless, - Sandbox: &sandbox, ChromeReadyTimeoutMS: 45_000, }).Launch(LaunchOptions{ UserDataDir: profileDir, diff --git a/go/modcdp/launcher/RemoteBrowserLauncher_test.go b/go/modcdp/launcher/RemoteBrowserLauncher_test.go index 2268475..20ad381 100644 --- a/go/modcdp/launcher/RemoteBrowserLauncher_test.go +++ b/go/modcdp/launcher/RemoteBrowserLauncher_test.go @@ -19,7 +19,6 @@ func TestRemoteBrowserLauncherConnectsToRealBrowserFromHTTPAndWebSocketCDPEndpoi } local, err := NewLocalBrowserLauncher(LaunchOptions{}).Launch(LaunchOptions{ Headless: boolPtr(true), - Sandbox: boolPtr(false), Port: port, }) if err != nil { diff --git a/go/modcdp/modcdp.go b/go/modcdp/modcdp.go index 8e8cfd0..e744b01 100644 --- a/go/modcdp/modcdp.go +++ b/go/modcdp/modcdp.go @@ -80,7 +80,6 @@ const UpstreamModeNATS = transport.UpstreamModeNATS const UpstreamEndpointKindRawCDP = transport.UpstreamEndpointKindRawCDP const UpstreamEndpointKindModCDPServer = transport.UpstreamEndpointKindModCDPServer const DefaultModCDPExtensionID = injector.DefaultModCDPExtensionID -const DefaultModCDPWakePath = injector.DefaultModCDPWakePath const DefaultUpstreamReverseWSBind = transport.DefaultUpstreamReverseWSBind const DefaultUpstreamReverseWSWaitTimeoutMS = transport.DefaultUpstreamReverseWSWaitTimeoutMS const DefaultUpstreamNATSWaitTimeoutMS = transport.DefaultUpstreamNATSWaitTimeoutMS diff --git a/go/modcdp/router/AutoSessionRouter_test.go b/go/modcdp/router/AutoSessionRouter_test.go index e41fb9d..1ba7fe2 100644 --- a/go/modcdp/router/AutoSessionRouter_test.go +++ b/go/modcdp/router/AutoSessionRouter_test.go @@ -107,10 +107,8 @@ func TestAutoSessionRouterBoundsDetachedSessionGuardsAndClearsThemWhenSessionRea func TestAutoSessionRouterTracksRealTargetSessionsAndExecutionContexts(t *testing.T) { headless := true - sandbox := false chrome, err := launcher.NewLocalBrowserLauncher(launcher.LaunchOptions{ Headless: &headless, - Sandbox: &sandbox, }).Launch(launcher.LaunchOptions{}) if err != nil { t.Fatal(err) @@ -163,7 +161,7 @@ func TestAutoSessionRouterTracksRealTargetSessionsAndExecutionContexts(t *testin return nil, fmt.Errorf("%s timed out", method) } } - router = NewAutoSessionRouter(send, func() int { return 5000 }) + router = NewAutoSessionRouter(send, func() int { return 30000 }) go func() { for { @@ -219,7 +217,7 @@ func TestAutoSessionRouterTracksRealTargetSessionsAndExecutionContexts(t *testin contextResult := make(chan int, 1) contextError := make(chan error, 1) go func() { - contextID, err := router.WaitForExecutionContext(sessionID, 5000) + contextID, err := router.WaitForExecutionContext(sessionID, 30000) if err != nil { contextError <- err return @@ -236,7 +234,7 @@ func TestAutoSessionRouterTracksRealTargetSessionsAndExecutionContexts(t *testin } case err := <-contextError: t.Fatal(err) - case <-time.After(10 * time.Second): + case <-time.After(35 * time.Second): t.Fatal("timed out waiting for execution context") } if _, err := send("Target.detachFromTarget", map[string]any{"sessionId": sessionID}, ""); err != nil { diff --git a/go/modcdp/translate/translate.go b/go/modcdp/translate/translate.go index 1f1d187..12bde56 100644 --- a/go/modcdp/translate/translate.go +++ b/go/modcdp/translate/translate.go @@ -135,10 +135,10 @@ func wrapModCDPAddMiddleware(params map[string]any) map[string]any { } func wrapCustomCommand(method string, params map[string]any, sessionID string) map[string]any { - m, _ := json.Marshal(method) p, _ := json.Marshal(params) - sid, _ := json.Marshal(sessionID) - return callFunctionParams(fmt.Sprintf(`async function() { return JSON.stringify(await globalThis.ModCDP.handleCommand(%s, %s, %s)); }`, string(m), string(p), string(sid))) + runtimeParams := callFunctionParams(`async function(method, paramsJson, cdpSessionId) { return JSON.stringify(await globalThis.ModCDP.handleCommand(method, JSON.parse(paramsJson), cdpSessionId)); }`) + runtimeParams["arguments"] = []map[string]any{{"value": method}, {"value": string(p)}, {"value": sessionID}} + return runtimeParams } func wrapServiceWorkerCommand(method string, params map[string]any, sessionID string, targetSessionID string) []rawStep { diff --git a/go/modcdp/translate/translate_test.go b/go/modcdp/translate/translate_test.go index 768347d..df05a5b 100644 --- a/go/modcdp/translate/translate_test.go +++ b/go/modcdp/translate/translate_test.go @@ -52,6 +52,37 @@ func TestTranslateRoutesWrapsAndUnwrapsModCDPProtocolMessagesDeterministically(t t.Fatalf("configure unwrap = %q", configured.Steps[0].Unwrap) } + custom, err := wrapCommandIfNeeded( + "Custom.echo", + map[string]any{"secret": strings.Repeat("x", 100), "nested": map[string]any{"ok": true}}, + DefaultClientRoutes(), + "session-1", + ) + if err != nil { + t.Fatal(err) + } + customParams := custom.Steps[0].Params + if !strings.Contains(stringValue(customParams["functionDeclaration"]), "JSON.parse(paramsJson)") { + t.Fatalf("functionDeclaration = %s", customParams["functionDeclaration"]) + } + if strings.Contains(stringValue(customParams["functionDeclaration"]), "xxxxxxxxxx") { + t.Fatalf("functionDeclaration includes custom params: %s", customParams["functionDeclaration"]) + } + customArguments := customParams["arguments"].([]map[string]any) + if customArguments[0]["value"] != "Custom.echo" { + t.Fatalf("method argument = %#v", customArguments[0]) + } + var customPayload map[string]any + if err := json.Unmarshal([]byte(customArguments[1]["value"].(string)), &customPayload); err != nil { + t.Fatal(err) + } + if customPayload["secret"] != strings.Repeat("x", 100) || customPayload["nested"].(map[string]any)["ok"] != true { + t.Fatalf("params argument = %#v", customPayload) + } + if customArguments[2]["value"] != "session-1" { + t.Fatalf("session argument = %#v", customArguments[2]) + } + unwrapped, err := unwrapResponseIfNeeded(map[string]any{"result": map[string]any{"type": "object", "value": map[string]any{"ok": true}}}, "runtime") if err != nil { t.Fatal(err) diff --git a/go/modcdp/transport/NativeMessagingUpstreamTransport_test.go b/go/modcdp/transport/NativeMessagingUpstreamTransport_test.go index 0d0cc37..daa108e 100644 --- a/go/modcdp/transport/NativeMessagingUpstreamTransport_test.go +++ b/go/modcdp/transport/NativeMessagingUpstreamTransport_test.go @@ -220,17 +220,31 @@ func waitForNativePeerDisconnect(t *testing.T, transport *NativeMessagingUpstrea func TestNativeMessagingUpstreamTransportInstallsLaunchProfileManifestAndConnectsToRealExtension(t *testing.T) { nativeHostName := "com.modcdp.bridge" headless := runtime.GOOS == "linux" && os.Getenv("DISPLAY") == "" - sandbox := runtime.GOOS != "linux" + extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension")) + if err != nil { + t.Fatal(err) + } + profileDir, err := os.MkdirTemp("", "modcdp.native.") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(profileDir) cdp := modcdp.New(modcdp.Options{ Launcher: modcdp.LauncherConfig{LauncherMode: "local", LauncherOptions: modcdp.LaunchOptions{ - Headless: boolPtr(headless), - Sandbox: boolPtr(sandbox), + Headless: boolPtr(headless), + UserDataDir: profileDir, + CleanupUserDataDir: boolPtr(true), + // Native messaging is browser -> client only. After explicit CHROME_PATH + // and CI /usr/bin/chromium, this test uses Chrome for Testing because + // Canary rejects --load-extension in this local test path. + ExecutablePath: reverseWSTestBrowserPath(t), }, }, Upstream: modcdp.UpstreamConfig{UpstreamMode: "nativemessaging", UpstreamNativeMessagingHostName: nativeHostName}, Injector: modcdp.InjectorConfig{ InjectorMode: "auto", + InjectorExtensionPath: extensionPath, InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, InjectorTrustServiceWorkerTarget: true, }, diff --git a/go/modcdp/transport/PipeUpstreamTransport_test.go b/go/modcdp/transport/PipeUpstreamTransport_test.go index 6332e47..b1fa9a2 100644 --- a/go/modcdp/transport/PipeUpstreamTransport_test.go +++ b/go/modcdp/transport/PipeUpstreamTransport_test.go @@ -4,8 +4,8 @@ import ( modcdp "github.com/browserbase/modcdp/go/modcdp/client" . "github.com/browserbase/modcdp/go/modcdp/transport" "os" + "path/filepath" "regexp" - "runtime" "testing" "time" ) @@ -25,7 +25,7 @@ func TestPipeUpstreamTransportConstructorUpdateLauncherConfigAndUnconnectedError if err := transport.Connect(); err == nil { t.Fatal("expected Connect to require pipe handles") } - if err := transport.Send(map[string]any{"id": 1, "method": "Browser.getVersion"}); err == nil { + if err := transport.Send(map[string]any{"id": 1, "method": "Runtime.evaluate"}); err == nil { t.Fatal("expected Send to require a connected pipe") } } @@ -54,7 +54,7 @@ func TestPipeUpstreamTransportResetsConnectionStateAfterPipeCloses(t *testing.T) if err := transport.Connect(); err != nil { t.Fatal(err) } - if err := transport.Send(map[string]any{"id": 1, "method": "Browser.getVersion"}); err != nil { + if err := transport.Send(map[string]any{"id": 1, "method": "Runtime.evaluate", "params": map[string]any{"expression": "1"}}); err != nil { t.Fatal(err) } _ = pipeReadWriter.Close() @@ -63,27 +63,30 @@ func TestPipeUpstreamTransportResetsConnectionStateAfterPipeCloses(t *testing.T) case <-time.After(2 * time.Second): t.Fatal("timed out waiting for pipe close") } - if err := transport.Send(map[string]any{"id": 2, "method": "Browser.getVersion"}); err == nil { + if err := transport.Send(map[string]any{"id": 2, "method": "Runtime.evaluate", "params": map[string]any{"expression": "1"}}); err == nil { t.Fatal("expected send to fail after pipe close") } } func TestPipeUpstreamTransportLaunchesRealBrowserAndUsesPIDScopedPipeURL(t *testing.T) { - headless := runtime.GOOS == "linux" && os.Getenv("DISPLAY") == "" - sandbox := runtime.GOOS != "linux" + extensionPath, err := filepath.Abs("../../../dist/extension") + if err != nil { + t.Fatal(err) + } cdp := modcdp.New(modcdp.Options{ Launcher: modcdp.LauncherConfig{LauncherMode: "local", LauncherOptions: modcdp.LaunchOptions{ - Headless: boolPtr(headless), - Sandbox: boolPtr(sandbox), + Headless: boolPtr(true), }, }, Upstream: modcdp.UpstreamConfig{UpstreamMode: "pipe"}, Injector: modcdp.InjectorConfig{ - InjectorMode: "auto", + InjectorMode: "inject", + InjectorExtensionPath: extensionPath, InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, InjectorTrustServiceWorkerTarget: true, }, + Server: &modcdp.ServerConfig{ServerRoutes: map[string]string{"*.*": "chrome_debugger"}}, }) defer cdp.Close() @@ -106,11 +109,22 @@ func TestPipeUpstreamTransportLaunchesRealBrowserAndUsesPIDScopedPipeURL(t *test if pipeTransport.URL != cdp.CDPURL { t.Fatalf("pipe transport URL = %q, CDPURL = %q", pipeTransport.URL, cdp.CDPURL) } - version, err := cdp.SendRaw("Browser.getVersion", map[string]any{}) + if _, err := cdp.Mod.AddCustomCommand(modcdp.CustomCommand{ + Name: "Custom.runtimeReadyState", + Expression: "async () => await cdp.send('Runtime.evaluate', { expression: 'document.readyState', returnByValue: true })", + }); err != nil { + t.Fatal(err) + } + runtime, err := cdp.Send("Custom.runtimeReadyState", nil) if err != nil { t.Fatal(err) } - if _, ok := version["product"].(string); !ok { - t.Fatalf("Browser.getVersion product = %#v", version["product"]) + runtimeMap, ok := runtime.(map[string]any) + if !ok { + t.Fatalf("Custom.runtimeReadyState = %#v", runtime) + } + runtimeResult, _ := runtimeMap["result"].(map[string]any) + if runtimeResult["value"] != "complete" { + t.Fatalf("Runtime.evaluate result = %#v", runtime) } } diff --git a/go/modcdp/transport/ReverseWebSocketUpstreamTransport.go b/go/modcdp/transport/ReverseWebSocketUpstreamTransport.go index 2c470d7..d733e25 100644 --- a/go/modcdp/transport/ReverseWebSocketUpstreamTransport.go +++ b/go/modcdp/transport/ReverseWebSocketUpstreamTransport.go @@ -112,7 +112,7 @@ func (t *ReverseWebSocketUpstreamTransport) Send(message map[string]any) error { } func (t *ReverseWebSocketUpstreamTransport) GetInjectorConfig() ExtensionInjectorConfig { - return ExtensionInjectorConfig{UpstreamReverseWSURL: t.URL} + return ExtensionInjectorConfig{} } func (t *ReverseWebSocketUpstreamTransport) WaitForPeer() error { diff --git a/go/modcdp/transport/ReverseWebSocketUpstreamTransport_test.go b/go/modcdp/transport/ReverseWebSocketUpstreamTransport_test.go index cc6de18..a50b6dd 100644 --- a/go/modcdp/transport/ReverseWebSocketUpstreamTransport_test.go +++ b/go/modcdp/transport/ReverseWebSocketUpstreamTransport_test.go @@ -7,7 +7,12 @@ import ( modcdp "github.com/browserbase/modcdp/go/modcdp/client" . "github.com/browserbase/modcdp/go/modcdp/transport" "os" + "path/filepath" + "reflect" + "regexp" "runtime" + "sort" + "strconv" "strings" "testing" "time" @@ -16,19 +21,19 @@ import ( "github.com/gobwas/ws/wsutil" ) -func TestReverseWebSocketUpstreamTransportConfigOwnsBindUpdatesWaitTimeoutAndInjectorConfig(t *testing.T) { +func TestReverseWebSocketUpstreamTransportConfigOwnsBindUpdatesAndWaitTimeout(t *testing.T) { transport := NewReverseWebSocketUpstreamTransport(ReverseWebSocketUpstreamTransportOptions{UpstreamReverseWSBind: "127.0.0.1:29292", UpstreamReverseWSWaitTimeoutMS: 10}) if transport.URL != "ws://127.0.0.1:29292" { t.Fatalf("URL = %q", transport.URL) } - if transport.GetInjectorConfig().UpstreamReverseWSURL != "ws://127.0.0.1:29292" { + if !reflect.DeepEqual(transport.GetInjectorConfig(), ExtensionInjectorConfig{}) { t.Fatalf("injector config = %#v", transport.GetInjectorConfig()) } transport.Update(map[string]any{"upstream_reversews_bind": "127.0.0.1:29293", "upstream_reversews_wait_timeout_ms": 5}) if transport.URL != "ws://127.0.0.1:29293" { t.Fatalf("URL after update = %q", transport.URL) } - if transport.GetInjectorConfig().UpstreamReverseWSURL != "ws://127.0.0.1:29293" { + if !reflect.DeepEqual(transport.GetInjectorConfig(), ExtensionInjectorConfig{}) { t.Fatalf("injector config after update = %#v", transport.GetInjectorConfig()) } transport.Update(map[string]any{"upstream_reversews_bind": "http://127.0.0.1:29294"}) @@ -214,23 +219,30 @@ func waitForReversePeerDisconnect(t *testing.T, transport *ReverseWebSocketUpstr t.Fatal("timed out waiting for reverse peer disconnect") } -func TestReverseWebSocketUpstreamTransportAcceptsRealExtensionReverseConnectionAndRoutesCDPThroughLoopback(t *testing.T) { +func TestReverseWebSocketUpstreamTransportAcceptsRealExtensionReverseConnectionAndRoutesCDPThroughChromeDebugger(t *testing.T) { + extensionPath, err := filepath.Abs(filepath.Join("..", "..", "..", "dist", "extension")) + if err != nil { + t.Fatal(err) + } headless := runtime.GOOS == "linux" && os.Getenv("DISPLAY") == "" - sandbox := runtime.GOOS != "linux" cdp := modcdp.New(modcdp.Options{ Launcher: modcdp.LauncherConfig{LauncherMode: "local", LauncherOptions: modcdp.LaunchOptions{ Headless: boolPtr(headless), - Sandbox: boolPtr(sandbox), + // Reversews is browser -> client only. After explicit CHROME_PATH and + // CI /usr/bin/chromium, these tests use Chrome for Testing because + // Canary rejects --load-extension in this local test path. + ExecutablePath: reverseWSTestBrowserPath(t), }, }, Upstream: modcdp.UpstreamConfig{UpstreamMode: "reversews"}, Injector: modcdp.InjectorConfig{ - InjectorMode: "auto", - InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, - InjectorTrustServiceWorkerTarget: true, + InjectorExtensionPath: extensionPath, + InjectorMode: "auto", + InjectorServiceWorkerURLSuffixes: []string{"/modcdp/service_worker.js"}, + InjectorTrustServiceWorkerTarget: true, + InjectorServiceWorkerProbeTimeoutMS: 1000, }, - Server: &modcdp.ServerConfig{ServerRoutes: map[string]string{"*.*": "loopback_cdp"}}, }) defer cdp.Close() @@ -247,30 +259,137 @@ func TestReverseWebSocketUpstreamTransportAcceptsRealExtensionReverseConnectionA if !ok { t.Fatalf("transport = %T", cdp.Transport()) } + if transport.URL != "ws://127.0.0.1:29292" { + t.Fatalf("transport URL = %q", transport.URL) + } if transport.PeerInfo["extension_id"] != DefaultModCDPExtensionID { t.Fatalf("extension_id = %#v", transport.PeerInfo["extension_id"]) } - result, err := cdp.Send("Browser.getVersion", map[string]any{}) + result, err := cdp.Send("Runtime.evaluate", map[string]any{"expression": "location.href", "returnByValue": true}) if err != nil { t.Fatal(err) } - version, ok := result.(map[string]any) + evaluated, ok := result.(map[string]any) if !ok { - t.Fatalf("Browser.getVersion result = %#v", result) + t.Fatalf("Runtime.evaluate result = %#v", result) } - if _, ok := version["product"].(string); !ok { - t.Fatalf("Browser.getVersion product = %#v", version["product"]) + evaluatedResult, _ := evaluated["result"].(map[string]any) + if evaluatedResult["value"] != "about:blank" { + t.Fatalf("Runtime.evaluate value = %#v", evaluatedResult["value"]) } time.Sleep(1500 * time.Millisecond) - secondResult, err := cdp.Send("Browser.getVersion", map[string]any{}) + secondResult, err := cdp.Send("Runtime.evaluate", map[string]any{"expression": "document.readyState", "returnByValue": true}) if err != nil { t.Fatal(err) } - secondVersion, ok := secondResult.(map[string]any) + secondEvaluated, ok := secondResult.(map[string]any) if !ok { - t.Fatalf("second Browser.getVersion result = %#v", secondResult) + t.Fatalf("second Runtime.evaluate result = %#v", secondResult) + } + secondEvaluatedResult, _ := secondEvaluated["result"].(map[string]any) + if secondEvaluatedResult["value"] != "complete" { + t.Fatalf("second Runtime.evaluate value = %#v", secondEvaluatedResult["value"]) + } +} + +func reverseWSTestBrowserPath(t *testing.T) string { + t.Helper() + explicitCandidates := []string{os.Getenv("CHROME_PATH")} + if runtime.GOOS == "linux" { + explicitCandidates = append(explicitCandidates, "/usr/bin/chromium") + } + for _, candidate := range explicitCandidates { + if candidate == "" { + continue + } + if _, err := os.Stat(candidate); err == nil { + return candidate + } } - if _, ok := secondVersion["product"].(string); !ok { - t.Fatalf("second Browser.getVersion product = %#v", secondVersion["product"]) + home, err := os.UserHomeDir() + if err != nil || home == "" { + home = "." + } + localAppData := os.Getenv("LOCALAPPDATA") + if localAppData == "" { + localAppData = filepath.Join(home, "AppData", "Local") + } + var patterns []string + switch runtime.GOOS { + case "darwin": + patterns = []string{ + filepath.Join(home, "Library", "Caches", "ms-playwright", "chromium-*", "chrome-mac*", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing"), + filepath.Join(home, "Library", "Caches", "ms-playwright", "chromium-*", "chrome-mac*", "Chromium.app", "Contents", "MacOS", "Chromium"), + filepath.Join(home, "Library", "Caches", "puppeteer", "chrome", "mac*-*", "chrome-mac*", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing"), + } + case "windows": + patterns = []string{ + filepath.Join(localAppData, "ms-playwright", "chromium-*", "chrome-win*", "chrome.exe"), + filepath.Join(home, ".cache", "puppeteer", "chrome", "win*-*", "chrome.exe"), + } + default: + patterns = []string{ + filepath.Join(home, ".cache", "ms-playwright", "chromium-*", "chrome-linux*", "chrome"), + filepath.Join("/opt", "pw-browsers", "chromium-*", "chrome-linux*", "chrome"), + filepath.Join(home, ".cache", "puppeteer", "chrome", "linux-*", "chrome-linux*", "chrome"), + } + } + var candidates []string + for _, pattern := range patterns { + matches, err := filepath.Glob(pattern) + if err != nil { + continue + } + candidates = append(candidates, matches...) + } + candidates = newestChromeForTestingFirst(candidates) + if len(candidates) > 0 { + return candidates[0] + } + t.Fatal("Reversews tests require CHROME_PATH, /usr/bin/chromium, or Chrome for Testing.") + return "" +} + +func newestChromeForTestingFirst(candidates []string) []string { + seen := map[string]bool{} + deduped := make([]string, 0, len(candidates)) + for _, candidate := range candidates { + if candidate == "" || seen[candidate] { + continue + } + seen[candidate] = true + deduped = append(deduped, candidate) + } + sort.SliceStable(deduped, func(i, j int) bool { + leftVersion := maxPathNumber(deduped[i]) + rightVersion := maxPathNumber(deduped[j]) + if leftVersion != rightVersion { + return leftVersion > rightVersion + } + leftStat, leftErr := os.Stat(deduped[i]) + rightStat, rightErr := os.Stat(deduped[j]) + var leftMtime, rightMtime time.Time + if leftErr == nil { + leftMtime = leftStat.ModTime() + } + if rightErr == nil { + rightMtime = rightStat.ModTime() + } + if !leftMtime.Equal(rightMtime) { + return leftMtime.After(rightMtime) + } + return deduped[i] < deduped[j] + }) + return deduped +} + +func maxPathNumber(value string) int { + maxValue := 0 + for _, raw := range regexp.MustCompile(`\d+`).FindAllString(value, -1) { + number, err := strconv.Atoi(raw) + if err == nil && number > maxValue { + maxValue = number + } } + return maxValue } diff --git a/go/modcdp/transport/WebSocketUpstreamTransport_test.go b/go/modcdp/transport/WebSocketUpstreamTransport_test.go index b93e5e8..5892ddf 100644 --- a/go/modcdp/transport/WebSocketUpstreamTransport_test.go +++ b/go/modcdp/transport/WebSocketUpstreamTransport_test.go @@ -37,7 +37,6 @@ func TestWebSocketUpstreamTransportLaunchesRealBrowserAndSpeaksRawCDP(t *testing Launcher: modcdp.LauncherConfig{LauncherMode: "local", LauncherOptions: modcdp.LaunchOptions{ Headless: boolPtr(true), - Sandbox: boolPtr(false), }, }, Upstream: modcdp.UpstreamConfig{UpstreamMode: "ws"}, @@ -115,7 +114,6 @@ func TestWebSocketUpstreamTransportLaunchesRealBrowserAndSpeaksRawCDP(t *testing func TestWebSocketUpstreamTransportResolvesRealHTTPCDPEndpointToBrowserWebSocket(t *testing.T) { chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{ Headless: boolPtr(true), - Sandbox: boolPtr(false), }).Launch(modcdp.LaunchOptions{}) if err != nil { t.Fatal(err) @@ -161,7 +159,6 @@ func TestWebSocketUpstreamTransportResolvesRealHTTPCDPEndpointToBrowserWebSocket func TestWebSocketUpstreamTransportCloseClearsConnectionState(t *testing.T) { chrome, err := modcdp.NewLocalBrowserLauncher(modcdp.LaunchOptions{ Headless: boolPtr(true), - Sandbox: boolPtr(false), }).Launch(modcdp.LaunchOptions{}) if err != nil { t.Fatal(err) diff --git a/go/modcdp/types/types.go b/go/modcdp/types/types.go index cf2c8a9..eb446f7 100644 --- a/go/modcdp/types/types.go +++ b/go/modcdp/types/types.go @@ -39,8 +39,6 @@ type ExtensionInjectorConfig struct { WaitForExecutionContext WaitForExecutionContext `json:"-"` InjectorExtensionPath string `json:"injector_extension_path,omitempty"` InjectorExtensionID string `json:"injector_extension_id,omitempty"` - InjectorWakePath string `json:"injector_wake_path,omitempty"` - InjectorWakeURL string `json:"injector_wake_url,omitempty"` InjectorServiceWorkerURLIncludes []string `json:"injector_service_worker_url_includes,omitempty"` InjectorServiceWorkerURLSuffixes []string `json:"injector_service_worker_url_suffixes,omitempty"` InjectorTrustServiceWorkerTarget bool `json:"injector_trust_service_worker_target,omitempty"` @@ -54,7 +52,6 @@ type ExtensionInjectorConfig struct { InjectorTargetSessionPollIntervalMS int `json:"injector_target_session_poll_interval_ms,omitempty"` InjectorBrowserbaseAPIKey string `json:"injector_browserbase_api_key,omitempty"` InjectorBrowserbaseBaseURL string `json:"injector_browserbase_base_url,omitempty"` - UpstreamReverseWSURL string `json:"upstream_reversews_url,omitempty"` UpstreamNativeMessagingHostName string `json:"upstream_nativemessaging_host_name,omitempty"` UpstreamNATSURL string `json:"upstream_nats_url,omitempty"` UpstreamNATSSubjectPrefix string `json:"upstream_nats_subject_prefix,omitempty"` diff --git a/js/examples/demo.ts b/js/examples/demo.ts index 5d4ee2e..f676383 100644 --- a/js/examples/demo.ts +++ b/js/examples/demo.ts @@ -42,10 +42,11 @@ const EXTENSION_PATH = existsSync(path.join(candidate, "modcdp/service_worker.js")), ) ?? path.resolve(HERE, "..", "..", "extension"); const DEFAULT_DEMO_EVENT_TIMEOUT_MS = 10_000; +const DEFAULT_REVERSE_TRANSPORT_WAIT_TIMEOUT_MS = 60_000; const DEFAULT_LIVE_CDP_POLL_INTERVAL_MS = 250; const DEFAULT_LIVE_CDP_ACTIVE_PORT_STALE_MS = 1_000; const DEFAULT_TARGET_EVENT_TIMEOUT_MS = 10_000; -const DEFAULT_FOREGROUND_EVENT_TIMEOUT_MS = 10_000; +const DEFAULT_PAGE_TARGET_EVENT_TIMEOUT_MS = 10_000; const DEFAULT_DEMO_EVENT_POLL_INTERVAL_MS = 20; const UPSTREAM_MODES = new Set(["ws", "pipe", "reversews", "nativemessaging", "nats"]); @@ -121,7 +122,17 @@ function clientOptionsFor(mode, upstream_mode, cdp_url, launch_options = {}) { const launcher = cdp_url ? ({ launcher_mode: "remote" } as const) : ({ launcher_mode: "local", launcher_options: launch_options } as const); - const upstream = { upstream_mode, upstream_cdp_url: cdp_url }; + const upstream = { + upstream_mode, + upstream_cdp_url: cdp_url, + ...(upstream_mode === "reversews" + ? { upstream_reversews_wait_timeout_ms: DEFAULT_REVERSE_TRANSPORT_WAIT_TIMEOUT_MS } + : {}), + ...(upstream_mode === "nativemessaging" + ? { upstream_nativemessaging_wait_timeout_ms: DEFAULT_REVERSE_TRANSPORT_WAIT_TIMEOUT_MS } + : {}), + ...(upstream_mode === "nats" ? { upstream_nats_wait_timeout_ms: DEFAULT_REVERSE_TRANSPORT_WAIT_TIMEOUT_MS } : {}), + }; const injector = { injector_mode: "auto" as const, injector_extension_path: EXTENSION_PATH, @@ -239,13 +250,14 @@ async function main() { } else { cdp_url = null; launch_options = { + chrome_ready_timeout_ms: 60_000, headless: process.platform === "linux" && !process.env.DISPLAY, sandbox: process.platform !== "linux", }; } const cdp = new ModCDPClient(clientOptionsFor(mode, upstream_mode, cdp_url, launch_options)); - const foregroundEvents = []; + const pageTargetEvents = []; const targetCreatedEvents: TargetCreatedPayload[] = []; try { @@ -461,35 +473,24 @@ async function main() { const demoEvent = assertObject(await demoEventPromise, "Custom.demoEvent"); console.log("Custom.demoEvent ->", demoEvent); - const ForegroundTargetChanged = z + const PageTargetUpdated = z .object({ - targetId: cdp.types.zod.Target.TargetID.nullable(), - tabId: z.number(), + targetId: cdp.types.zod.Target.TargetID, + tabId: z.number().optional(), url: z.string().nullable().optional(), }) .passthrough() - .meta({ id: "Custom.foregroundTargetChanged" }); - const foregroundEventRegistration = assertObject( - await cdp.Mod.addCustomEvent(ForegroundTargetChanged), - "Mod.addCustomEvent Custom.foregroundTargetChanged", + .meta({ id: "Custom.pageTargetUpdated" }); + const pageTargetEventRegistration = assertObject( + await cdp.Mod.addCustomEvent(PageTargetUpdated), + "Mod.addCustomEvent Custom.pageTargetUpdated", ); - if (foregroundEventRegistration.registered !== true) { - throw new Error(`unexpected foreground event registration ${JSON.stringify(foregroundEventRegistration)}`); + if (pageTargetEventRegistration.registered !== true) { + throw new Error(`unexpected page target event registration ${JSON.stringify(pageTargetEventRegistration)}`); } - cdp.on(ForegroundTargetChanged, (event) => { - console.log("Custom.foregroundTargetChanged ->", event); - foregroundEvents.push(event); - }); - await cdp.Mod.evaluate({ - expression: `async () => { - chrome.tabs.onActivated.addListener(async ({ tabId }) => { - const targets = await chrome.debugger.getTargets(); - const target = targets.find(target => target.type === "page" && target.tabId === tabId); - const tab = await chrome.tabs.get(tabId).catch(() => null); - await cdp.emit("Custom.foregroundTargetChanged", { tabId, targetId: target?.id ?? null, url: target?.url ?? tab?.url ?? null }); - }); - return true; - }`, + cdp.on(PageTargetUpdated, (event) => { + console.log("Custom.pageTargetUpdated ->", event); + pageTargetEvents.push(event); }); await cdp.Target.setDiscoverTargets({ discover: true }); @@ -523,22 +524,38 @@ async function main() { console.log("Custom.TabIdFromTargetId ->", tabFromTarget); await cdp.Target.activateTarget({ targetId: createdTarget.targetId }); - const foregroundDeadline = Date.now() + DEFAULT_FOREGROUND_EVENT_TIMEOUT_MS; + const pageTargetEmitResult = assertObject( + await cdp.Mod.evaluate({ + params: { targetId: createdTarget.targetId }, + expression: `async ({ targetId }) => { + const targets = await chrome.debugger.getTargets(); + const target = targets.find(target => target.id === targetId); + if (!target?.id) throw new Error(\`target \${targetId} not found\`); + await cdp.emit("Custom.pageTargetUpdated", { targetId: target.id, url: target.url ?? null }); + return { emitted: true, targetId: target.id }; + }`, + }), + "Custom.pageTargetUpdated emit", + ); + if (pageTargetEmitResult.emitted !== true || pageTargetEmitResult.targetId !== createdTarget.targetId) { + throw new Error(`unexpected Custom.pageTargetUpdated emit result ${JSON.stringify(pageTargetEmitResult)}`); + } + const pageTargetDeadline = Date.now() + DEFAULT_PAGE_TARGET_EVENT_TIMEOUT_MS; while ( - !foregroundEvents.some((event) => event.targetId === createdTarget.targetId) && - Date.now() < foregroundDeadline + !pageTargetEvents.some((event) => event.targetId === createdTarget.targetId) && + Date.now() < pageTargetDeadline ) { await sleep(DEFAULT_DEMO_EVENT_POLL_INTERVAL_MS); } - const foreground = foregroundEvents.find((event) => event.targetId === createdTarget.targetId); - if (!foreground) throw new Error(`expected Custom.foregroundTargetChanged for ${createdTarget.targetId}`); - if (foreground.tabId !== tabFromTargetId) - throw new Error(`unexpected Custom.foregroundTargetChanged result ${JSON.stringify(foreground)}`); + const pageTarget = pageTargetEvents.find((event) => event.targetId === createdTarget.targetId); + if (!pageTarget) throw new Error(`expected Custom.pageTargetUpdated for ${createdTarget.targetId}`); + if (pageTarget.tabId !== tabFromTargetId) + throw new Error(`unexpected Custom.pageTargetUpdated result ${JSON.stringify(pageTarget)}`); const targetFromTab = await cdp.send("Custom.targetIdFromTabId", { - tabId: foreground.tabId, + tabId: pageTarget.tabId, }); - if (targetFromTab.targetId !== createdTarget.targetId || targetFromTab.tabId !== foreground.tabId) { + if (targetFromTab.targetId !== createdTarget.targetId || targetFromTab.tabId !== pageTarget.tabId) { throw new Error(`unexpected Custom.targetIdFromTabId/middleware result ${JSON.stringify(targetFromTab)}`); } console.log("Custom.targetIdFromTabId ->", targetFromTab); diff --git a/js/examples/playwright.ts b/js/examples/playwright.ts index 265e89e..b868ccf 100644 --- a/js/examples/playwright.ts +++ b/js/examples/playwright.ts @@ -26,6 +26,7 @@ let browser: Awaited> | null = null; try { chrome = await new LocalBrowserLauncher().launch({ + chrome_ready_timeout_ms: 60_000, headless: process.platform === "linux" && !process.env.DISPLAY, sandbox: process.platform !== "linux", extra_args: [`--load-extension=${extension_path}`], diff --git a/js/examples/puppeteer.ts b/js/examples/puppeteer.ts index e0785ba..0aa8c0a 100644 --- a/js/examples/puppeteer.ts +++ b/js/examples/puppeteer.ts @@ -26,6 +26,7 @@ let browser: Awaited> | null = null; try { chrome = await new LocalBrowserLauncher().launch({ + chrome_ready_timeout_ms: 60_000, headless: process.platform === "linux" && !process.env.DISPLAY, sandbox: process.platform !== "linux", extra_args: [`--load-extension=${extension_path}`], diff --git a/js/scripts/build-extension-assets.mjs b/js/scripts/build-extension-assets.mjs index f267968..aa957c9 100755 --- a/js/scripts/build-extension-assets.mjs +++ b/js/scripts/build-extension-assets.mjs @@ -3,6 +3,7 @@ import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; +import { build } from "esbuild"; const root = process.cwd(); const dist_extension = path.join(root, "dist", "extension"); @@ -18,28 +19,21 @@ const copy = (from, to) => { fs.copyFileSync(path.join(root, from), path.join(root, to)); }; -const write = (to, contents) => { - fs.mkdirSync(path.dirname(path.join(root, to)), { recursive: true }); - fs.writeFileSync(path.join(root, to), contents); -}; - copy("extension/manifest.json", "dist/extension/manifest.json"); copy("extension/src/pages/options.html", "dist/extension/options.html"); copy("dist/extension/src/pages/options.js", "dist/extension/options.js"); -copy("extension/src/pages/wake.html", "dist/extension/modcdp/wake.html"); -copy("dist/extension/src/pages/wake.js", "dist/extension/modcdp/wake.js"); copy("extension/src/pages/offscreen_keepalive.html", "dist/extension/offscreen/keepalive.html"); copy("dist/extension/src/pages/offscreen_keepalive.js", "dist/extension/offscreen/offscreen_keepalive.js"); -copy("dist/js/src/server/ModCDPServer.js", "dist/extension/js/src/server/ModCDPServer.js"); -if (fs.existsSync(path.join(root, "dist/js/src/server/ModCDPServer.js.map"))) { - copy("dist/js/src/server/ModCDPServer.js.map", "dist/extension/js/src/server/ModCDPServer.js.map"); -} - -const service_worker = fs - .readFileSync(path.join(root, "dist/extension/src/service_worker.js"), "utf8") - .replace("../../js/src/server/ModCDPServer.js", "../js/src/server/ModCDPServer.js"); -write("dist/extension/modcdp/service_worker.js", service_worker); -copy("dist/extension/src/service_worker.js.map", "dist/extension/modcdp/service_worker.js.map"); +await build({ + entryPoints: [path.join(root, "extension/src/service_worker.ts")], + outfile: path.join(root, "dist/extension/modcdp/service_worker.js"), + bundle: true, + format: "esm", + platform: "browser", + target: ["chrome116"], + sourcemap: true, + logLevel: "silent", +}); fs.rmSync(path.join(dist_extension, "src"), { recursive: true, force: true }); const fixed_date = new Date("2000-01-01T00:00:00Z"); diff --git a/js/src/client/ModCDPClient.ts b/js/src/client/ModCDPClient.ts index 2e136c7..01462f0 100644 --- a/js/src/client/ModCDPClient.ts +++ b/js/src/client/ModCDPClient.ts @@ -14,6 +14,7 @@ import type { z } from "zod"; import { createCdpAliases, type CdpAliases } from "../types/generated/aliases.js"; export type { CdpAliases } from "../types/generated/aliases.js"; +import { commands as nativeCommandSchemas, events as nativeEventSchemas } from "../types/generated/zod.js"; import { CUSTOM_EVENT_BINDING_NAME, DEFAULT_CLIENT_ROUTES, @@ -69,7 +70,6 @@ export const DEFAULT_SERVICE_WORKER_POLL_INTERVAL_MS = 100; export const DEFAULT_TARGET_SESSION_POLL_INTERVAL_MS = 20; export const DEFAULT_WS_CONNECT_ERROR_SETTLE_TIMEOUT_MS = 250; export const DEFAULT_MODCDP_SERVICE_WORKER_URL_SUFFIXES = ["/modcdp/service_worker.js"]; -export const DEFAULT_MODCDP_WAKE_PATH = "/modcdp/wake.html"; export const DEFAULT_UPSTREAM_REVERSEWS_BIND = "127.0.0.1:29292"; export const DEFAULT_UPSTREAM_REVERSEWS_WAIT_TIMEOUT_MS = 10_000; export const DEFAULT_UPSTREAM_NATIVEMESSAGING_WAIT_TIMEOUT_MS = 10_000; @@ -106,8 +106,6 @@ export type InjectorOptions = { injector_mode?: InjectorMode; injector_extension_path?: string | null; injector_extension_id?: string | null; - injector_wake_path?: string | null; - injector_wake_url?: string | null; injector_service_worker_url_includes?: string[]; injector_service_worker_url_suffixes?: string[] | null; injector_trust_service_worker_target?: boolean; @@ -159,6 +157,10 @@ type ModCDPEventPayload = TEvent extends z.ZodType & { expression?: string | null; }; +type ProtocolCommandSchema = { + params: z.ZodType; + result: z.ZodType; +}; export type ModCDPCommandSpec = { params: Params; @@ -265,8 +267,6 @@ function normalizeClientOptions({ injector_mode, injector_extension_path: injector.injector_extension_path ?? null, injector_extension_id: injector.injector_extension_id ?? null, - injector_wake_path: injector.injector_wake_path ?? DEFAULT_MODCDP_WAKE_PATH, - injector_wake_url: injector.injector_wake_url ?? null, injector_service_worker_url_includes: injector.injector_service_worker_url_includes ?? [], injector_service_worker_url_suffixes: injector.injector_service_worker_url_suffixes ?? DEFAULT_MODCDP_SERVICE_WORKER_URL_SUFFIXES, @@ -285,7 +285,10 @@ function normalizeClientOptions({ injector.injector_target_session_poll_interval_ms ?? DEFAULT_TARGET_SESSION_POLL_INTERVAL_MS, }, client: { - client_routes: { ...DEFAULT_CLIENT_ROUTES, ...(client.client_routes ?? {}) }, + client_routes: { + ...DEFAULT_CLIENT_ROUTES, + ...(client.client_routes ?? {}), + }, client_hydrate_aliases: client.client_hydrate_aliases ?? true, client_mirror_upstream_events: client.client_mirror_upstream_events ?? true, client_cdp_send_timeout_ms: client.client_cdp_send_timeout_ms ?? DEFAULT_CDP_SEND_TIMEOUT_MS, @@ -466,6 +469,7 @@ export class ModCDPClient extends ModCDPEventEmitter { on: (event_name: string | symbol, listener: (...args: unknown[]) => void) => this.on(event_name, listener), once: (event_name: string | symbol, listener: (...args: unknown[]) => void) => this.once(event_name, listener), }; + this._hydrateNativeProtocolSchemas(); void this._hydrateCdpAliases(); this._hydrateCustomSurface(); } @@ -579,7 +583,7 @@ export class ModCDPClient extends ModCDPEventEmitter { completed_at: Date.now(), duration_ms: Date.now() - started_at, }; - return { name, registered: true }; + return this.command_result_schemas.get(method)?.parse({ name, registered: true }) ?? { name, registered: true }; } command_params = { ...parsed, @@ -600,7 +604,7 @@ export class ModCDPClient extends ModCDPEventEmitter { completed_at: Date.now(), duration_ms: Date.now() - started_at, }; - return { name, registered: true }; + return this.command_result_schemas.get(method)?.parse({ name, registered: true }) ?? { name, registered: true }; } command_params = { ...parsed, name, event_schema: null }; } @@ -684,6 +688,30 @@ export class ModCDPClient extends ModCDPEventEmitter { } } + _hydrateNativeProtocolSchemas() { + for (const [method, schema] of Object.entries(nativeCommandSchemas) as [string, ProtocolCommandSchema][]) { + this.command_params_schemas.set(method, schema.params); + this.command_result_schemas.set(method, schema.result); + } + this.command_params_schemas.set("Mod.evaluate", Mod.EvaluateParams); + this.command_result_schemas.set("Mod.evaluate", Mod.EvaluateResponse); + this.command_params_schemas.set("Mod.addCustomCommand", Mod.AddCustomCommandParams); + this.command_result_schemas.set("Mod.addCustomCommand", Mod.AddCustomCommandResponse); + this.command_params_schemas.set("Mod.addCustomEvent", Mod.AddCustomEventParams); + this.command_result_schemas.set("Mod.addCustomEvent", Mod.AddCustomEventResponse); + this.command_params_schemas.set("Mod.addMiddleware", Mod.AddMiddlewareParams); + this.command_result_schemas.set("Mod.addMiddleware", Mod.AddMiddlewareResponse); + this.command_params_schemas.set("Mod.configure", Mod.ConfigureParams); + this.command_result_schemas.set("Mod.configure", Mod.ConfigureResponse); + this.command_params_schemas.set("Mod.ping", Mod.PingParams); + this.command_result_schemas.set("Mod.ping", Mod.PingResponse); + + for (const [event, schema] of Object.entries(nativeEventSchemas) as [string, z.ZodType][]) { + this.event_schemas.set(event, schema); + } + this.event_schemas.set("Mod.pong", Mod.PongEvent); + } + _normalizePayloadSchema(schema: unknown) { return normalizeModCDPPayloadSchema(Mod.PayloadSchemaSpec.parse(schema)); } @@ -695,18 +723,18 @@ export class ModCDPClient extends ModCDPEventEmitter { else this.command_result_unwrap_keys.delete(name); } + _parseEventPayload(method: string, data: unknown) { + return this.event_schemas.get(method)?.parse(data) ?? data; + } + _serviceWorkerUrlSuffixes() { return this.injector.injector_service_worker_url_suffixes; } _serverConfigureParams(): ModCDPConfigureParams { - const transport_injector_config = this.transport?.getInjectorConfig?.() ?? {}; return { upstream: { upstream_mode: this.upstream.upstream_mode, - ...(transport_injector_config.upstream_reversews_url - ? { upstream_reversews_url: transport_injector_config.upstream_reversews_url } - : {}), ...(this.upstream.upstream_nats_url ? { upstream_nats_url: this.upstream.upstream_nats_url } : {}), ...(this.upstream.upstream_nats_subject_prefix ? { @@ -888,7 +916,11 @@ export class ModCDPClient extends ModCDPEventEmitter { async _injectorsForConfig() { if (this.injector.injector_mode === "none") return []; const injectors: ExtensionInjector[] = []; - if (this.injector.injector_mode === "auto" || this.injector.injector_mode === "discover") { + const prefer_launch_injection = this.injector.injector_mode === "auto" && this.launcher.launcher_mode === "local"; + if ( + (this.injector.injector_mode === "auto" || this.injector.injector_mode === "discover") && + !prefer_launch_injection + ) { const { DiscoveredExtensionInjector } = await import("../injector/DiscoveredExtensionInjector.js"); injectors.push(new DiscoveredExtensionInjector()); } @@ -910,6 +942,10 @@ export class ModCDPClient extends ModCDPEventEmitter { ); injectors.push(new ExtensionsLoadUnpackedInjector()); } + if (prefer_launch_injection) { + const { DiscoveredExtensionInjector } = await import("../injector/DiscoveredExtensionInjector.js"); + injectors.push(new DiscoveredExtensionInjector()); + } if (this.injector.injector_mode === "auto" || this.injector.injector_mode === "borrow") { const { BorrowedExtensionInjector } = await import(/* @vite-ignore */ "../injector/BorrowedExtensionInjector.js"); injectors.push(new BorrowedExtensionInjector()); @@ -932,8 +968,6 @@ export class ModCDPClient extends ModCDPEventEmitter { this.auto_sessions.waitForExecutionContext(session_id, { timeout_ms }), injector_extension_path: this.injector.injector_extension_path, injector_extension_id: this.injector.injector_extension_id, - injector_wake_path: this.injector.injector_wake_path, - injector_wake_url: this.injector.injector_wake_url, injector_service_worker_url_includes: this.injector.injector_service_worker_url_includes, injector_service_worker_url_suffixes: service_worker_url_suffixes, injector_trust_service_worker_target: trust_service_worker_target, @@ -1206,15 +1240,17 @@ export class ModCDPClient extends ModCDPEventEmitter { this.ext_session_id, ); if (u) { - this.auto_sessions.recordProtocolEvent(u.event, u.data, u.sessionId); - this.emit(u.event, this.event_schemas.get(u.event)?.parse(u.data) ?? u.data, u.sessionId); + const payload = this._parseEventPayload(u.event, u.data); + this.auto_sessions.recordProtocolEvent(u.event, payload as ProtocolPayload, u.sessionId); + this.emit(u.event, payload, u.sessionId); } return; } if (event.method) { const data = event.params || {}; - this.auto_sessions.recordProtocolEvent(event.method, data, event.sessionId || null); - this.emit(event.method, this.event_schemas.get(event.method)?.parse(data) ?? data, event.sessionId || null); + const payload = this._parseEventPayload(event.method, data); + this.auto_sessions.recordProtocolEvent(event.method, payload as ProtocolPayload, event.sessionId || null); + this.emit(event.method, payload, event.sessionId || null); } } diff --git a/js/src/index.ts b/js/src/index.ts index 15026f2..2ea4a62 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -9,7 +9,6 @@ export { NoopBrowserLauncher } from "./launcher/NoopBrowserLauncher.js"; export { DEFAULT_MODCDP_EXTENSION_ID, DEFAULT_MODCDP_SERVICE_WORKER_URL_SUFFIXES, - DEFAULT_MODCDP_WAKE_PATH, ExtensionInjector, defaultModCDPExtensionPath, } from "./injector/ExtensionInjector.js"; diff --git a/js/src/injector/DiscoveredExtensionInjector.ts b/js/src/injector/DiscoveredExtensionInjector.ts index 5292899..8d91f55 100644 --- a/js/src/injector/DiscoveredExtensionInjector.ts +++ b/js/src/injector/DiscoveredExtensionInjector.ts @@ -13,16 +13,6 @@ export class DiscoveredExtensionInjector extends ExtensionInjector { ); if (waited) return { ...waited, source: "discovered" }; } - const woke = await this.wakeConfiguredExtension(); - if (woke) { - const waited = await this.waitForReadyServiceWorker( - this.options.injector_service_worker_probe_timeout_ms ?? 10_000, - { - matched_only: this.options.injector_trust_service_worker_target, - }, - ); - if (waited) return { ...waited, source: "discovered" }; - } if (!this.options.injector_require_service_worker_target) return null; const waited = await this.waitForReadyServiceWorker( this.options.injector_service_worker_ready_timeout_ms ?? 60_000, diff --git a/js/src/injector/ExtensionInjector.ts b/js/src/injector/ExtensionInjector.ts index 3d1719d..5acf392 100644 --- a/js/src/injector/ExtensionInjector.ts +++ b/js/src/injector/ExtensionInjector.ts @@ -1,3 +1,6 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; import type { BrowserLaunchOptions } from "../launcher/BrowserLauncher.js"; import type { UpstreamTransportConfig } from "../transport/UpstreamTransport.js"; import type { ProtocolParams, ProtocolResult } from "../types/modcdp.js"; @@ -7,7 +10,6 @@ import { commands as TargetCommands } from "../types/generated/zod/Target.js"; const EXT_ID_FROM_URL = /^chrome-extension:\/\/([a-z]+)\//; export const DEFAULT_MODCDP_EXTENSION_ID = "mdedooklbnfejodmnhmkdpkaedafkehf"; export const DEFAULT_MODCDP_SERVICE_WORKER_URL_SUFFIXES = ["/modcdp/service_worker.js"]; -export const DEFAULT_MODCDP_WAKE_PATH = "/modcdp/wake.html"; const MODCDP_READY_EXPRESSION = "Boolean(globalThis.ModCDP?.__ModCDPServerVersion >= 1 && globalThis.ModCDP?.handleCommand && globalThis.ModCDP?.addCustomEvent)"; export const DEFAULT_CDP_SEND_TIMEOUT_MS = 10_000; @@ -27,8 +29,6 @@ export type ExtensionInjectorConfig = { waitForExecutionContext?: ((session_id: string, timeout_ms: number) => Promise) | null; injector_extension_path?: string | null; injector_extension_id?: string | null; - injector_wake_path?: string | null; - injector_wake_url?: string | null; injector_service_worker_url_includes?: string[]; injector_service_worker_url_suffixes?: string[]; injector_trust_service_worker_target?: boolean; @@ -42,7 +42,6 @@ export type ExtensionInjectorConfig = { injector_target_session_poll_interval_ms?: number; injector_browserbase_api_key?: string | null; injector_browserbase_base_url?: string | null; - upstream_reversews_url?: string | null; upstream_nativemessaging_host_name?: string | null; upstream_nats_url?: string | null; upstream_nats_subject_prefix?: string | null; @@ -58,6 +57,11 @@ export type ExtensionInjectionResult = { has_debugger?: boolean; }; +export type PreparedExtension = { + unpacked_extension_path: string; + cleanup: () => Promise; +}; + function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } @@ -72,6 +76,69 @@ export function defaultModCDPExtensionPath() { return "../../../dist/extension.zip"; } +function firstString(...values: unknown[]) { + for (const value of values) { + if (typeof value === "string" && value.trim()) return value.trim(); + } + return null; +} + +export async function prepareUnpackedExtension(extension_path: string): Promise { + const unpacked_path = fs.mkdtempSync(path.join(os.tmpdir(), "modcdp-extension-")); + const cleanup = async () => fs.rmSync(unpacked_path, { recursive: true, force: true }); + try { + if (extension_path.endsWith(".zip")) { + await extractZip(extension_path, unpacked_path); + } else { + fs.cpSync(extension_path, unpacked_path, { recursive: true }); + } + return { unpacked_extension_path: extensionRoot(unpacked_path), cleanup }; + } catch (error) { + await cleanup(); + throw error; + } +} + +export async function extensionIdFromManifestKey(extension_path: string) { + const [crypto, fs, path] = await Promise.all([import("node:crypto"), import("node:fs"), import("node:path")]); + const manifest_path = path.join(extension_path, "manifest.json"); + if (!fs.existsSync(manifest_path)) return null; + const manifest = JSON.parse(fs.readFileSync(manifest_path, "utf8")) as Record; + const key = firstString(manifest.key); + if (!key) return null; + const digest = crypto.createHash("sha256").update(Buffer.from(key, "base64")).digest().subarray(0, 16); + const alphabet = "abcdefghijklmnop"; + return [...digest].map((byte) => alphabet[byte >> 4] + alphabet[byte & 0x0f]).join(""); +} + +function extensionRoot(unpacked_path: string) { + if (fs.existsSync(path.join(unpacked_path, "manifest.json"))) return unpacked_path; + const nested_path = path.join(unpacked_path, "extension"); + if (fs.existsSync(path.join(nested_path, "manifest.json"))) return nested_path; + return unpacked_path; +} + +async function extractZip(zip_path: string, destination: string) { + const { execFileSync } = await import("node:child_process"); + const listing = execFileSync("unzip", ["-Z1", zip_path], { + encoding: "utf8", + }); + for (const raw_name of listing.split(/\r?\n/)) { + if (!raw_name) continue; + const name = raw_name.replaceAll("\\", "/"); + const normalized = path.posix.normalize(name); + if ( + path.posix.isAbsolute(normalized) || + normalized === "." || + normalized === ".." || + normalized.startsWith("../") + ) { + throw new Error(`zip entry ${JSON.stringify(raw_name)} escapes extension extraction directory`); + } + } + execFileSync("unzip", ["-q", zip_path, "-d", destination]); +} + export class ExtensionInjector { options: ExtensionInjectorConfig; protected unusable_target_ids = new Set(); @@ -85,8 +152,6 @@ export class ExtensionInjector { waitForExecutionContext: null, injector_extension_path: null, injector_extension_id: null, - injector_wake_path: DEFAULT_MODCDP_WAKE_PATH, - injector_wake_url: null, injector_service_worker_url_includes: [], injector_service_worker_url_suffixes: [], injector_trust_service_worker_target: false, @@ -100,7 +165,6 @@ export class ExtensionInjector { injector_target_session_poll_interval_ms: DEFAULT_TARGET_SESSION_POLL_INTERVAL_MS, injector_browserbase_api_key: null, injector_browserbase_base_url: null, - upstream_reversews_url: null, upstream_nativemessaging_host_name: null, upstream_nats_url: null, upstream_nats_subject_prefix: null, @@ -194,30 +258,6 @@ export class ExtensionInjector { return TargetCommands["Target.getTargets"].result.parse(await this.send("Target.getTargets")).targetInfos; } - protected configuredWakeUrl() { - if (this.options.injector_wake_url) return this.options.injector_wake_url; - if (!this.options.injector_extension_id) return null; - const wake_path = this.options.injector_wake_path ?? DEFAULT_MODCDP_WAKE_PATH; - if (!wake_path) return null; - return `chrome-extension://${this.options.injector_extension_id}${wake_path.startsWith("/") ? wake_path : `/${wake_path}`}`; - } - - protected async wakeConfiguredExtension() { - const wake_url = this.configuredWakeUrl(); - if (!wake_url || typeof this.options.send !== "function") return false; - try { - await this.sendWithTimeout("Target.createTarget", { - url: wake_url, - background: true, - hidden: true, - focus: false, - }); - return true; - } catch { - return false; - } - } - protected async probeTarget( target: TargetInfo, session_timeout_ms = 0, @@ -283,7 +323,9 @@ export class ExtensionInjector { ) { const deadline = Date.now() + timeout_ms; while (Date.now() < deadline) { - const discovered = await this.discoverReadyServiceWorker({ matched_only }); + const discovered = await this.discoverReadyServiceWorker({ + matched_only, + }); if (discovered) return discovered; await delay(this.options.injector_service_worker_poll_interval_ms ?? DEFAULT_SERVICE_WORKER_POLL_INTERVAL_MS); } diff --git a/js/src/injector/ExtensionsLoadUnpackedInjector.ts b/js/src/injector/ExtensionsLoadUnpackedInjector.ts index 4f61a4f..43043c0 100644 --- a/js/src/injector/ExtensionsLoadUnpackedInjector.ts +++ b/js/src/injector/ExtensionsLoadUnpackedInjector.ts @@ -1,7 +1,9 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { defaultModCDPExtensionPath, ExtensionInjector, type TargetInfo } from "./ExtensionInjector.js"; +import { + defaultModCDPExtensionPath, + ExtensionInjector, + prepareUnpackedExtension, + type TargetInfo, +} from "./ExtensionInjector.js"; export class ExtensionsLoadUnpackedInjector extends ExtensionInjector { private unpacked_extension_path: string | null = null; @@ -13,19 +15,9 @@ export class ExtensionsLoadUnpackedInjector extends ExtensionInjector { await super.prepare(); return; } - if (!extension_path.endsWith(".zip")) { - const unpacked_path = fs.mkdtempSync(path.join(os.tmpdir(), "modcdp-extension-")); - fs.cpSync(extension_path, unpacked_path, { recursive: true }); - this.unpacked_extension_path = extensionRoot(unpacked_path); - this.cleanup = async () => fs.rmSync(unpacked_path, { recursive: true, force: true }); - await super.prepare(); - return; - } - const { execFileSync } = await import("node:child_process"); - const unpacked_path = fs.mkdtempSync(path.join(os.tmpdir(), "modcdp-extension-")); - execFileSync("unzip", ["-q", extension_path, "-d", unpacked_path]); - this.unpacked_extension_path = extensionRoot(unpacked_path); - this.cleanup = async () => fs.rmSync(unpacked_path, { recursive: true, force: true }); + const prepared = await prepareUnpackedExtension(extension_path); + this.unpacked_extension_path = prepared.unpacked_extension_path; + this.cleanup = prepared.cleanup; await super.prepare(); } @@ -34,7 +26,9 @@ export class ExtensionsLoadUnpackedInjector extends ExtensionInjector { if (!extension_path) return null; let load_result: Record; try { - load_result = (await this.send("Extensions.loadUnpacked", { path: extension_path })) as Record; + load_result = (await this.send("Extensions.loadUnpacked", { + path: extension_path, + })) as Record; } catch (error) { const load_error = error instanceof Error ? error : new Error(String(error)); if (/Method not available|Method.*not.*found|wasn't found/i.test(load_error.message)) { @@ -52,7 +46,6 @@ export class ExtensionsLoadUnpackedInjector extends ExtensionInjector { throw new Error(`Extensions.loadUnpacked returned no extension id (got ${JSON.stringify(load_result)})`); } this.options.injector_extension_id = extension_id; - await this.wakeConfiguredExtension(); const sw_url_prefix = `chrome-extension://${extension_id}/`; const deadline = Date.now() + (this.options.injector_service_worker_ready_timeout_ms ?? 60_000); @@ -65,7 +58,12 @@ export class ExtensionsLoadUnpackedInjector extends ExtensionInjector { const probed = await this.probeTarget(target, this.options.injector_service_worker_probe_timeout_ms, { allow_attach: true, }); - if (probed) return { ...probed, source: "extensions_load_unpacked", extension_id }; + if (probed) + return { + ...probed, + source: "extensions_load_unpacked", + extension_id, + }; } await new Promise((resolve) => setTimeout(resolve, this.options.injector_service_worker_poll_interval_ms ?? 100)); } @@ -78,10 +76,3 @@ export class ExtensionsLoadUnpackedInjector extends ExtensionInjector { this.cleanup = null; } } - -function extensionRoot(unpacked_path: string) { - if (fs.existsSync(path.join(unpacked_path, "manifest.json"))) return unpacked_path; - const nested_path = path.join(unpacked_path, "extension"); - if (fs.existsSync(path.join(nested_path, "manifest.json"))) return nested_path; - return unpacked_path; -} diff --git a/js/src/injector/LocalBrowserLaunchExtensionInjector.ts b/js/src/injector/LocalBrowserLaunchExtensionInjector.ts index f407eed..b1ff853 100644 --- a/js/src/injector/LocalBrowserLaunchExtensionInjector.ts +++ b/js/src/injector/LocalBrowserLaunchExtensionInjector.ts @@ -1,15 +1,10 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; import type { BrowserLaunchOptions } from "../launcher/BrowserLauncher.js"; -import { defaultModCDPExtensionPath, ExtensionInjector } from "./ExtensionInjector.js"; - -function firstString(...values: unknown[]) { - for (const value of values) { - if (typeof value === "string" && value.trim()) return value.trim(); - } - return null; -} +import { + defaultModCDPExtensionPath, + extensionIdFromManifestKey, + ExtensionInjector, + prepareUnpackedExtension, +} from "./ExtensionInjector.js"; export class LocalBrowserLaunchExtensionInjector extends ExtensionInjector { private unpacked_extension_path: string | null = null; @@ -22,20 +17,9 @@ export class LocalBrowserLaunchExtensionInjector extends ExtensionInjector { await super.prepare(); return; } - if (!extension_path.endsWith(".zip")) { - const unpacked_path = fs.mkdtempSync(path.join(os.tmpdir(), "modcdp-extension-")); - fs.cpSync(extension_path, unpacked_path, { recursive: true }); - this.unpacked_extension_path = extensionRoot(unpacked_path); - this.cleanup = async () => fs.rmSync(unpacked_path, { recursive: true, force: true }); - await this.resolveExtensionId(); - await super.prepare(); - return; - } - const { execFileSync } = await import("node:child_process"); - const unpacked_path = fs.mkdtempSync(path.join(os.tmpdir(), "modcdp-extension-")); - execFileSync("unzip", ["-q", extension_path, "-d", unpacked_path]); - this.unpacked_extension_path = extensionRoot(unpacked_path); - this.cleanup = async () => fs.rmSync(unpacked_path, { recursive: true, force: true }); + const prepared = await prepareUnpackedExtension(extension_path); + this.unpacked_extension_path = prepared.unpacked_extension_path; + this.cleanup = prepared.cleanup; await this.resolveExtensionId(); await super.prepare(); } @@ -47,8 +31,7 @@ export class LocalBrowserLaunchExtensionInjector extends ExtensionInjector { } async inject() { - const timeout_ms = this.options.injector_service_worker_probe_timeout_ms ?? 10_000; - const discovered = await this.waitForReadyServiceWorker(timeout_ms, { + const discovered = await this.discoverReadyServiceWorker({ matched_only: this.options.injector_trust_service_worker_target, }); return discovered ? { ...discovered, source: "local_launch" } : null; @@ -62,7 +45,10 @@ export class LocalBrowserLaunchExtensionInjector extends ExtensionInjector { private async resolveExtensionId() { if (this.extension_id) return this.extension_id; - this.extension_id = firstString(this.options.injector_extension_id); + this.extension_id = + typeof this.options.injector_extension_id === "string" && this.options.injector_extension_id.trim() + ? this.options.injector_extension_id.trim() + : null; if (!this.extension_id && this.unpacked_extension_path) { this.extension_id = await extensionIdFromManifestKey(this.unpacked_extension_path); } @@ -70,22 +56,3 @@ export class LocalBrowserLaunchExtensionInjector extends ExtensionInjector { return this.extension_id; } } - -function extensionRoot(unpacked_path: string) { - if (fs.existsSync(path.join(unpacked_path, "manifest.json"))) return unpacked_path; - const nested_path = path.join(unpacked_path, "extension"); - if (fs.existsSync(path.join(nested_path, "manifest.json"))) return nested_path; - return unpacked_path; -} - -async function extensionIdFromManifestKey(extension_path: string) { - const [crypto, fs, path] = await Promise.all([import("node:crypto"), import("node:fs"), import("node:path")]); - const manifest_path = path.join(extension_path, "manifest.json"); - if (!fs.existsSync(manifest_path)) return null; - const manifest = JSON.parse(fs.readFileSync(manifest_path, "utf8")) as Record; - const key = firstString(manifest.key); - if (!key) return null; - const digest = crypto.createHash("sha256").update(Buffer.from(key, "base64")).digest().subarray(0, 16); - const alphabet = "abcdefghijklmnop"; - return [...digest].map((byte) => alphabet[byte >> 4] + alphabet[byte & 0x0f]).join(""); -} diff --git a/js/src/launcher/LocalBrowserLauncher.ts b/js/src/launcher/LocalBrowserLauncher.ts index b1a2708..b31f9ce 100644 --- a/js/src/launcher/LocalBrowserLauncher.ts +++ b/js/src/launcher/LocalBrowserLauncher.ts @@ -1,7 +1,7 @@ import { spawn, type ChildProcess } from "node:child_process"; import { once } from "node:events"; import { existsSync, readdirSync, statSync } from "node:fs"; -import { mkdtemp, rm } from "node:fs/promises"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; import net from "node:net"; import type { AddressInfo } from "node:net"; import { homedir, platform, tmpdir } from "node:os"; @@ -121,7 +121,8 @@ function candidatePaths() { ), ] : ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/opt/google/chrome/chrome"]; - return [process.env.CHROME_PATH, ...chromeForTestingCandidates(), ...canary, ...stock].filter( + const chromium = platform() === "linux" ? ["/usr/bin/chromium", "/usr/bin/chromium-browser"] : []; + return [process.env.CHROME_PATH, ...chromium, ...canary, ...chromeForTestingCandidates(), ...stock].filter( (candidate): candidate is string => Boolean(candidate), ); } @@ -222,16 +223,70 @@ async function waitForPipeReady( async function waitForCdpWebSocketUrl(cdp_url: string, timeout_ms: number, poll_interval_ms: number) { const deadline = Date.now() + timeout_ms; + let lastError: unknown = null; while (Date.now() < deadline) { try { return await resolveCdpWebSocketUrl(cdp_url); - } catch { + } catch (error) { + lastError = error; await delay(poll_interval_ms); } } + if (lastError instanceof Error) { + throw new Error( + `Chrome at ${cdp_url} did not expose a WebSocket CDP URL within ${timeout_ms}ms: ${lastError.message}`, + ); + } throw new Error(`Chrome at ${cdp_url} did not expose a WebSocket CDP URL within ${timeout_ms}ms`); } +async function readDevToolsActivePort(profile_dir: string) { + const activePortPath = path.join(profile_dir, "DevToolsActivePort"); + let body: string; + try { + body = await readFile(activePortPath, "utf8"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; + throw error; + } + const [rawPort, websocketPath] = body.trim().split(/\r?\n/); + if (!rawPort || !websocketPath) return null; + const port = Number(rawPort); + if (!Number.isInteger(port) || port <= 0) throw new Error(`Invalid DevToolsActivePort port: ${rawPort}`); + return { port, cdp_url: `http://127.0.0.1:${port}`, websocketPath }; +} + +async function waitForBrowserSelectedCdpWebSocketUrl( + profile_dir: string, + timeout_ms: number, + poll_interval_ms: number, + assertChromeRunning: () => void, +) { + const deadline = Date.now() + timeout_ms; + let lastError: unknown = null; + while (Date.now() < deadline) { + assertChromeRunning(); + const activePort = await readDevToolsActivePort(profile_dir); + if (activePort) { + try { + return { + port: activePort.port, + cdp_url: await resolveCdpWebSocketUrl(activePort.cdp_url), + }; + } catch (error) { + lastError = error; + } + } + await delay(poll_interval_ms); + } + if (lastError instanceof Error) { + throw new Error( + `Chrome did not expose DevToolsActivePort from ${profile_dir} within ${timeout_ms}ms: ${lastError.message}`, + ); + } + throw new Error(`Chrome did not expose DevToolsActivePort from ${profile_dir} within ${timeout_ms}ms`); +} + export class LocalBrowserLauncher extends BrowserLauncher { static findChromeBinary(explicit?: string | null) { const candidates = [explicit, ...candidatePaths()].filter((candidate): candidate is string => Boolean(candidate)); @@ -260,7 +315,7 @@ export class LocalBrowserLauncher extends BrowserLauncher { port, user_data_dir, headless = process.platform === "linux" && !process.env.DISPLAY, - sandbox = false, + sandbox = process.platform !== "linux", args = [], extra_args = [], remote_debugging = "port", @@ -272,7 +327,7 @@ export class LocalBrowserLauncher extends BrowserLauncher { const exe = LocalBrowserLauncher.findChromeBinary(executable_path); const usePipe = remote_debugging === "pipe"; const useLoopbackCdp = !usePipe || loopback_cdp || port != null; - const usePort = useLoopbackCdp ? port || (await LocalBrowserLauncher.freePort()) : null; + const usePort = useLoopbackCdp ? (port ?? 0) : null; const profile_dir = user_data_dir || (await mkdtemp(path.join(tmpdir(), "modcdp."))); const flags = [ ...DEFAULT_FLAGS, @@ -308,6 +363,12 @@ export class LocalBrowserLauncher extends BrowserLauncher { await terminateProcess(proc); if (!user_data_dir || cleanup_user_data_dir) await removeProfileDir(profile_dir); }; + const assertChromeRunning = () => { + if (spawnError) throw spawnError; + if (proc.exitCode !== null || proc.signalCode !== null) { + throw new Error(`Chrome exited before CDP became ready (exit=${proc.exitCode}, signal=${proc.signalCode}).`); + } + }; if (usePipe) { const pipe_write = proc.stdio[3] as NodeJS.WritableStream | null; @@ -316,24 +377,31 @@ export class LocalBrowserLauncher extends BrowserLauncher { await close(); throw new Error("Chrome remote-debugging pipe stdio handles were not created."); } - if (spawnError) { - await close(); - throw spawnError; - } + assertChromeRunning(); await waitForPipeReady(pipe_read, pipe_write, chrome_ready_timeout_ms); - const loopback_cdp_url = + const loopback = usePort == null ? null - : await waitForCdpWebSocketUrl( - `http://127.0.0.1:${usePort}`, - chrome_ready_timeout_ms, - chrome_ready_poll_interval_ms, - ); + : usePort === 0 + ? await waitForBrowserSelectedCdpWebSocketUrl( + profile_dir, + chrome_ready_timeout_ms, + chrome_ready_poll_interval_ms, + assertChromeRunning, + ) + : { + port: usePort, + cdp_url: await waitForCdpWebSocketUrl( + `http://127.0.0.1:${usePort}`, + chrome_ready_timeout_ms, + chrome_ready_poll_interval_ms, + ), + }; this.launched = { proc, - ...(usePort == null ? {} : { port: usePort }), + ...(loopback == null ? {} : { port: loopback.port }), cdp_url: `pipe://${proc.pid}`, - ...(loopback_cdp_url == null ? {} : { loopback_cdp_url }), + ...(loopback == null ? {} : { loopback_cdp_url: loopback.cdp_url }), pipe_read, pipe_write, profile_dir, @@ -342,28 +410,32 @@ export class LocalBrowserLauncher extends BrowserLauncher { return this.launched; } - const cdp_port = usePort as number; - const cdp_url = `http://127.0.0.1:${cdp_port}`; const deadline = Date.now() + chrome_ready_timeout_ms; while (Date.now() < deadline) { - if (spawnError) { + try { + assertChromeRunning(); + } catch (error) { await close(); - throw spawnError; + throw error; } - if (proc.exitCode !== null || proc.signalCode !== null) { - await close(); - throw new Error(`Chrome exited before CDP became ready (exit=${proc.exitCode}, signal=${proc.signalCode}).`); + const activePort = + usePort === 0 + ? await readDevToolsActivePort(profile_dir) + : { port: usePort as number, cdp_url: `http://127.0.0.1:${usePort}` }; + if (!activePort) { + await delay(chrome_ready_poll_interval_ms); + continue; } try { - const response = await fetch(`${cdp_url}/json/version`); + const response = await fetch(`${activePort.cdp_url}/json/version`); if (response.ok) { const version = await response.json(); // cdp_url is resolved from the HTTP discovery endpoint before returning. this.launched = { proc, - port: cdp_port, - cdp_url: version.webSocketDebuggerUrl ?? cdp_url, - loopback_cdp_url: version.webSocketDebuggerUrl ?? cdp_url, + port: activePort.port, + cdp_url: version.webSocketDebuggerUrl ?? activePort.cdp_url, + loopback_cdp_url: version.webSocketDebuggerUrl ?? activePort.cdp_url, profile_dir, close, }; @@ -373,6 +445,6 @@ export class LocalBrowserLauncher extends BrowserLauncher { await new Promise((resolve) => setTimeout(resolve, chrome_ready_poll_interval_ms)); } await close(); - throw new Error(`Chrome at ${cdp_url} did not become ready within ${chrome_ready_timeout_ms}ms`); + throw new Error(`Chrome did not become ready within ${chrome_ready_timeout_ms}ms`); } } diff --git a/js/src/proxy/proxy.ts b/js/src/proxy/proxy.ts index 3065ae1..46186c7 100644 --- a/js/src/proxy/proxy.ts +++ b/js/src/proxy/proxy.ts @@ -1242,8 +1242,6 @@ export function runProxyCli(args = process.argv.slice(2)) { injector_mode: String(argv["injector-mode"] || "auto") as InjectorOptions["injector_mode"], injector_extension_path, injector_extension_id: optionalStringArg(argv, "injector-extension-id"), - injector_wake_path: optionalStringArg(argv, "injector-wake-path"), - injector_wake_url: optionalStringArg(argv, "injector-wake-url"), injector_service_worker_url_includes: optionalStringListArg(argv, "injector-service-worker-url-includes"), injector_service_worker_url_suffixes: optionalStringListArg(argv, "injector-service-worker-url-suffixes"), injector_trust_service_worker_target: optionalBooleanArg(argv, "injector-trust-service-worker-target"), diff --git a/js/src/server/ModCDPServer.ts b/js/src/server/ModCDPServer.ts index 846bce3..6ceac32 100644 --- a/js/src/server/ModCDPServer.ts +++ b/js/src/server/ModCDPServer.ts @@ -7,6 +7,8 @@ // when Chrome refuses Extensions.loadUnpacked. import type { cdp } from "../types/generated/cdp.js"; +import { commands as nativeCommandSchemas, events as nativeEventSchemas } from "../types/generated/zod.js"; +import { normalizeModCDPPayloadSchema } from "../types/modcdp.js"; import type { CdpCommandMessage, CdpEventMessage, @@ -32,6 +34,13 @@ export const DEFAULT_NATS_BRIDGE_RECONNECT_INTERVAL_MS = 2_000; export const DEFAULT_NATS_BRIDGE_SUBJECT_PREFIX = "modcdp.default"; type MiddlewarePhase = "request" | "response" | "event"; +type ProtocolCommandSchema = { + params: { parse(value: unknown): ProtocolParams }; + result: { parse(value: unknown): ProtocolResult }; +}; +type ProtocolEventSchema = { + parse(value: unknown): ProtocolPayload; +}; type ModCDPGlobalScope = typeof globalThis & Record & { ModCDP?: { @@ -81,12 +90,38 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis const attachedDebuggees = new Set(); let runtime_types_promise: Promise | null = null; + function nativeCommandSchema(method: string) { + return (nativeCommandSchemas as Record)[method]; + } + + function nativeEventSchema(eventName: string) { + return (nativeEventSchemas as Record)[eventName]; + } + + function commandParamsSchema(method: string, command: ModCDPCustomCommandRegistration | null) { + return ( + (command?.params_schema as ProtocolCommandSchema["params"] | null) ?? nativeCommandSchema(method)?.params ?? null + ); + } + + function commandResultSchema(method: string, command: ModCDPCustomCommandRegistration | null) { + return ( + (command?.result_schema as ProtocolCommandSchema["result"] | null) ?? nativeCommandSchema(method)?.result ?? null + ); + } + + function eventPayloadSchema(eventName: string, event: ModCDPCustomEventRegistration | null) { + return (event?.event_schema as ProtocolEventSchema | null) ?? nativeEventSchema(eventName) ?? null; + } + async function publishEvent(eventName: string, payload: ProtocolPayload = {}, cdpSessionId: string | null = null) { payload = await ModCDPServer.runMiddleware("event", eventName, payload, { cdpSessionId, event: { name: eventName, payload }, }); if (payload === undefined) return { event: eventName, emitted: false, reason: "middleware_dropped" }; + const event = registryMatch(eventBindings, eventName); + payload = eventPayloadSchema(eventName, event)?.parse(payload) ?? payload; for (const listener of eventListeners) { try { @@ -122,7 +157,10 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis params: (payload ?? {}) as CdpEventMessage["params"], }; if (cdpSessionId) message.sessionId = cdpSessionId; - publishNats(`${nats_bridge_subject_prefix}.browser_to_client`, { type: "modcdp.nats.message", message }); + publishNats(`${nats_bridge_subject_prefix}.browser_to_client`, { + type: "modcdp.nats.message", + message, + }); emittedThroughNatsBridge = true; } @@ -131,13 +169,25 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis if (isCustomEvent) { const customBinding = globalScope[CUSTOM_EVENT_BINDING_NAME]; if (typeof customBinding === "function") { - customBinding(encodeBindingPayload({ event: eventName, data: payload, cdpSessionId })); + customBinding( + encodeBindingPayload({ + event: eventName, + data: payload, + cdpSessionId, + }), + ); emittedThroughBinding = true; } } else { const mirrorBinding = globalScope[UPSTREAM_EVENT_BINDING_NAME]; if (typeof mirrorBinding === "function") { - mirrorBinding(encodeBindingPayload({ event: eventName, data: payload, cdpSessionId })); + mirrorBinding( + encodeBindingPayload({ + event: eventName, + data: payload, + cdpSessionId, + }), + ); emittedThroughBinding = true; } } @@ -218,7 +268,12 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis id?: string; name?: string; meta?: () => - | { cdp_command_name?: unknown; cdp_event_name?: unknown; id?: unknown; name?: unknown } + | { + cdp_command_name?: unknown; + cdp_event_name?: unknown; + id?: unknown; + name?: unknown; + } | undefined; } | string, @@ -464,7 +519,10 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis reverseBridgeSocket?.readyState === WebSocket.OPEN || reverseBridgeSocket?.readyState === WebSocket.CONNECTING ) { - return { upstream_reversews_url: endpoint, connected: reverseBridgeSocket.readyState === WebSocket.OPEN }; + return { + upstream_reversews_url: endpoint, + connected: reverseBridgeSocket.readyState === WebSocket.OPEN, + }; } const ws = new WebSocket(endpoint); @@ -498,7 +556,11 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis const chromeApi = globalScope.chrome; if (!chromeApi?.runtime?.connectNative) { scheduleNativeBridgeReconnect(nativeBridgeReconnectIntervalMs); - return { upstream_nativemessaging_host_name: hostName, connected: false, reason: "native_messaging_unavailable" }; + return { + upstream_nativemessaging_host_name: hostName, + connected: false, + reason: "native_messaging_unavailable", + }; } if (nativeBridgePort) return { upstream_nativemessaging_host_name: hostName, connected: true }; try { @@ -530,7 +592,11 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis ModCDPServer.native_bridge_connected = false; ModCDPServer.native_bridge_last_error = errorMessage(error); scheduleNativeBridgeReconnect(nativeBridgeReconnectIntervalMs); - return { upstream_nativemessaging_host_name: hostName, connected: false, reason: errorMessage(error) }; + return { + upstream_nativemessaging_host_name: hostName, + connected: false, + reason: errorMessage(error), + }; } } @@ -570,7 +636,11 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis if (nats_bridge_socket === ws) nats_bridge_socket = null; scheduleNatsBridgeReconnect(nats_bridge_reconnect_interval_ms); }); - return { upstream_nats_url: endpoint, upstream_nats_subject_prefix: nats_bridge_subject_prefix, connected: false }; + return { + upstream_nats_url: endpoint, + upstream_nats_subject_prefix: nats_bridge_subject_prefix, + connected: false, + }; } function writeNats(data: string) { @@ -614,7 +684,13 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis } function natsConnectOptions() { - return { verbose: false, pedantic: false, lang: "modcdp-extension", version: "1", protocol: 1 }; + return { + verbose: false, + pedantic: false, + lang: "modcdp-extension", + version: "1", + protocol: 1, + }; } function debuggerSendCommand( @@ -822,7 +898,12 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis if (!ModCDPServer.loopback_cdp_url) throw new Error(`No loopback_cdp_url configured for ${method}.`); const ws = await loopbackWS(ModCDPServer.loopback_cdp_url); const id = nextLoopbackId++; - const message: { id: number; method: string; params: ProtocolParams; sessionId?: string } = { + const message: { + id: number; + method: string; + params: ProtocolParams; + sessionId?: string; + } = { id, method, params, @@ -941,7 +1022,11 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis void connectReverseBridge(endpoint).catch(() => { scheduleReverseBridgeReconnect(reverseBridgeReconnectIntervalMs); }); - return { upstream_reversews_url: endpoint, reconnect_interval_ms, connecting: true }; + return { + upstream_reversews_url: endpoint, + reconnect_interval_ms, + connecting: true, + }; }, stopReverseBridge, startNativeBridge( @@ -974,7 +1059,12 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis void connectNatsBridge(endpoint).catch(() => { scheduleNatsBridgeReconnect(nats_bridge_reconnect_interval_ms); }); - return { upstream_nats_url: endpoint, upstream_nats_subject_prefix, reconnect_interval_ms, connecting: true }; + return { + upstream_nats_url: endpoint, + upstream_nats_subject_prefix, + reconnect_interval_ms, + connecting: true, + }; }, ensureOffscreenKeepAlive, @@ -1010,9 +1100,6 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis upstream_nats_subject_prefix: upstream.upstream_nats_subject_prefix ?? DEFAULT_NATS_BRIDGE_SUBJECT_PREFIX, }); } - if (upstream.upstream_mode === "reversews" && upstream.upstream_reversews_url) { - this.startReverseBridge(upstream.upstream_reversews_url); - } if (server_routes) this.routes = { ...defaultRoutes, ...server_routes }; else { this.routes = { ...defaultRoutes }; @@ -1036,18 +1123,32 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis if (!/^[^.]+\.[^.]+$/.test(name)) throw new Error("name must be in Domain.method form."); if (typeof handler !== "function" && typeof expression === "string") { handler = async (params: ProtocolParams = {}, cdpSessionId: string | null = null, method: string = name) => { - return await evaluateUserExpression({ expression, params, cdpSessionId, method }); + return await evaluateUserExpression({ + expression, + params, + cdpSessionId, + method, + }); }; } if (typeof handler !== "function") throw new Error(`Custom command ${name} was registered without a handler.`); - commandHandlers.set(name, { name, handler, params_schema, result_schema, expression }); + commandHandlers.set(name, { + name, + handler, + params_schema: normalizeModCDPPayloadSchema(params_schema), + result_schema: normalizeModCDPPayloadSchema(result_schema), + expression, + }); return { name, registered: true }; }, addCustomEvent({ name, event_schema = null }: ModCDPCustomEventRegistration) { name = normalizeModCDPName(name); if (!/^[^.]+\.[^.]+$/.test(name)) throw new Error("name must be in Domain.event form."); - eventBindings.set(name, { name, event_schema }); + eventBindings.set(name, { + name, + event_schema: normalizeModCDPPayloadSchema(event_schema), + }); return { name, registered: true }; }, @@ -1122,14 +1223,16 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis params = middlewareParams as ProtocolParams; const command = registryMatch(commandHandlers, method); + params = commandParamsSchema(method, command)?.parse(params) ?? params; let result; if (command) { result = await command.handler(params, cdpSessionId, method); - return this.runMiddleware("response", method, result, { + result = await this.runMiddleware("response", method, result, { cdpSessionId, request: { ...request, params }, response: { result }, }); + return commandResultSchema(method, command)?.parse(result) ?? result; } let upstream = "chrome_debugger"; @@ -1162,11 +1265,12 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis else if (upstream === "chrome_debugger") result = await this.sendChromeDebugger(method, params); else throw new Error(`No ModCDP command registered for ${method}.`); - return this.runMiddleware("response", method, result, { + result = await this.runMiddleware("response", method, result, { cdpSessionId, request: { ...request, params }, response: { result }, }); + return commandResultSchema(method, null)?.parse(result) ?? result; }, attachToSession(cdpSessionId: string | null = null) { @@ -1188,7 +1292,13 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis async emit(eventName: string, payload: ProtocolPayload = {}, cdpSessionId: string | null = null) { const event = registryMatch(eventBindings, eventName); - if (!event) return { event: eventName, emitted: false, reason: "event_not_registered" }; + if (!event) + return { + event: eventName, + emitted: false, + reason: "event_not_registered", + }; + payload = eventPayloadSchema(eventName, event)?.parse(payload) ?? payload; const customBinding = globalScope[CUSTOM_EVENT_BINDING_NAME]; if ( typeof customBinding !== "function" && @@ -1196,18 +1306,30 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis !nativeBridgePort && nats_bridge_socket?.readyState !== WebSocket.OPEN ) - return { event: eventName, emitted: false, reason: "binding_not_installed" }; + return { + event: eventName, + emitted: false, + reason: "binding_not_installed", + }; return publishEvent(eventName, payload, cdpSessionId); }, - async discoverLoopbackCDP(): Promise<{ loopback_cdp_url: string | null; verified: boolean; version?: unknown }> { + async discoverLoopbackCDP(): Promise<{ + loopback_cdp_url: string | null; + verified: boolean; + version?: unknown; + }> { if (!this.browser_token) return { loopback_cdp_url: null as null, verified: false }; const url = "http://127.0.0.1:9222"; const previousLoopbackUrl = this.loopback_cdp_url; const fail = (version?: unknown) => { this.loopback_cdp_url = previousLoopbackUrl ?? null; - return { loopback_cdp_url: null as null, verified: false, ...(version ? { version } : {}) }; + return { + loopback_cdp_url: null as null, + verified: false, + ...(version ? { version } : {}), + }; }; try { const version = await fetch(`${url}/json/version`).then((response) => response.ok && response.json()); @@ -1242,7 +1364,11 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis if (result.result?.value !== true) return fail(version); await initializeLoopbackCDP(); - return { loopback_cdp_url: this.loopback_cdp_url, verified: true, version }; + return { + loopback_cdp_url: this.loopback_cdp_url, + verified: true, + version, + }; } catch { return fail(); } @@ -1273,7 +1399,10 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis let resolvedTabUrl: string | null = null; if (!resolvedTabId) { const [tab] = chromeApi.tabs?.query - ? await chromeApi.tabs.query({ active: true, lastFocusedWindow: true }) + ? await chromeApi.tabs.query({ + active: true, + lastFocusedWindow: true, + }) : []; resolvedTabId = tab?.id || null; resolvedTabUrl = tab?.url || tab?.pendingUrl || null; @@ -1331,13 +1460,22 @@ export function installModCDPServer(globalScope: ModCDPGlobalScope = globalThis const resolvedDebuggee = debuggee ?? compactDebuggee({ tabId, targetId, extensionId }); if (Object.keys(resolvedDebuggee).length === 0) { let tab: chrome.tabs.Tab | undefined; - [tab] = await chromeApi.tabs.query({ active: true, lastFocusedWindow: true }); + [tab] = await chromeApi.tabs.query({ + active: true, + lastFocusedWindow: true, + }); if (!tab?.id) [tab] = await chromeApi.tabs.query({}); if (!tab?.id) { try { - tab = await chromeApi.tabs.create({ url: "https://example.com/#modcdp", active: true }); + tab = await chromeApi.tabs.create({ + url: "https://example.com/#modcdp", + active: true, + }); } catch { - const win = await chromeApi.windows.create({ url: "https://example.com/#modcdp", focused: true }); + const win = await chromeApi.windows.create({ + url: "https://example.com/#modcdp", + focused: true, + }); tab = win?.tabs?.[0]; } } diff --git a/js/src/translate/translate.ts b/js/src/translate/translate.ts index 2338f2d..da94e2a 100644 --- a/js/src/translate/translate.ts +++ b/js/src/translate/translate.ts @@ -173,9 +173,11 @@ export function wrapCustomCommand( method: string, params: ProtocolParams = {}, cdpSessionId: string | null = null, -): cdp.types.ts.Runtime.EvaluateParams { +): cdp.types.ts.Runtime.CallFunctionOnParams { return { - functionDeclaration: `async function() { return JSON.stringify(await globalThis.ModCDP.handleCommand(${JSON.stringify(method)}, ${JSON.stringify(params)}, ${JSON.stringify(cdpSessionId)})); }`, + functionDeclaration: + "async function(method, paramsJson, cdpSessionId) { return JSON.stringify(await globalThis.ModCDP.handleCommand(method, JSON.parse(paramsJson), cdpSessionId)); }", + arguments: [{ value: method }, { value: JSON.stringify(params) }, { value: cdpSessionId }], awaitPromise: true, returnByValue: true, }; diff --git a/js/src/transport/ReverseWebSocketUpstreamTransport.ts b/js/src/transport/ReverseWebSocketUpstreamTransport.ts index 6c2b379..1425a52 100644 --- a/js/src/transport/ReverseWebSocketUpstreamTransport.ts +++ b/js/src/transport/ReverseWebSocketUpstreamTransport.ts @@ -51,7 +51,7 @@ export class ReverseWebSocketUpstreamTransport extends UpstreamTransport { } getInjectorConfig() { - return { upstream_reversews_url: this.url }; + return {}; } private setBind(bind: string) { diff --git a/js/src/types/modcdp.ts b/js/src/types/modcdp.ts index 5cb398a..b5602dd 100644 --- a/js/src/types/modcdp.ts +++ b/js/src/types/modcdp.ts @@ -153,7 +153,6 @@ export const ModCDPUpstreamOptionsSchema = z upstream_nats_subject_prefix: z.string().nullable().optional(), upstream_nats_wait_timeout_ms: z.number().positive().optional(), upstream_reversews_bind: z.string().nullable().optional(), - upstream_reversews_url: z.string().nullable().optional(), upstream_reversews_wait_timeout_ms: z.number().positive().optional(), upstream_nativemessaging_manifest: z.string().nullable().optional(), upstream_nativemessaging_manifests: z.array(z.string()).nullable().optional(), diff --git a/js/test/ModCDPClient_protocol_validation.test.ts b/js/test/ModCDPClient_protocol_validation.test.ts new file mode 100644 index 0000000..8d82d43 --- /dev/null +++ b/js/test/ModCDPClient_protocol_validation.test.ts @@ -0,0 +1,113 @@ +import assert from "node:assert/strict"; +import { test } from "vitest"; +import { z } from "zod"; + +import { ModCDPClient, type ModCDPClientInstance } from "../src/client/ModCDPClient.js"; +import type { cdp } from "../src/types/generated/cdp.js"; + +test("protocol validation covers native methods, native events, custom methods, custom events, and native overrides", async () => { + type CustomClient = ModCDPClientInstance< + { + "Custom.echo": { params: { text: string }; result: { ok: boolean } }; + "Target.getTargets": { + params: cdp.types.ts.Target.GetTargetsParams; + result: cdp.types.ts.Target.GetTargetsResult & { + targetInfos: Array; + }; + }; + }, + { "Custom.ready": { ok: boolean } } + >; + + const client = new ModCDPClient() as CustomClient; + + const runtimeParams: cdp.types.ts.Runtime.EvaluateParams = { expression: "1 + 1", returnByValue: true }; + const runtimeResult: cdp.types.ts.Runtime.EvaluateResult = { + result: { type: "number", value: 2, description: "2" }, + }; + const nativeEvent: cdp.types.ts.Target.TargetCreatedEvent = { + targetInfo: { + targetId: "target-1", + type: "page", + title: "Example", + url: "https://example.com", + attached: false, + canAccessOpener: false, + }, + }; + const customParams: Parameters[0] = { text: "ok" }; + const customResult: Awaited> = { ok: true }; + + if (false) { + // @ts-expect-error Runtime.evaluate params require expression. + const badRuntimeParams: cdp.types.ts.Runtime.EvaluateParams = {}; + void badRuntimeParams; + const badNativeEvent: cdp.types.ts.Target.TargetCreatedEvent = { + targetInfo: { + ...nativeEvent.targetInfo, + // @ts-expect-error Target.targetCreated targetInfo.targetId is a string. + targetId: 1, + }, + }; + void badNativeEvent; + // @ts-expect-error Custom.echo requires text. + client.Custom.echo({ id: "wrong" }); + // @ts-expect-error Custom.echo returns ok as boolean. + const badCustomResult: Awaited> = { ok: "yes" }; + void badCustomResult; + await client.Mod.addMiddleware({ + name: client.Target.getTargets, + phase: client.RESPONSE, + expression: "async (value, next) => next(value)", + }); + } + + assert.deepEqual(client.command_params_schemas.get("Runtime.evaluate")?.parse(runtimeParams), runtimeParams); + assert.deepEqual(client.command_result_schemas.get("Runtime.evaluate")?.parse(runtimeResult), runtimeResult); + assert.deepEqual(client.event_schemas.get("Target.targetCreated")?.parse(nativeEvent), nativeEvent); + assert.throws(() => client.command_params_schemas.get("Runtime.evaluate")?.parse({})); + assert.throws(() => client.command_result_schemas.get("Runtime.evaluate")?.parse({})); + assert.throws(() => client.event_schemas.get("Target.targetCreated")?.parse({})); + + await client.Mod.addCustomCommand("Custom.echo", { + params_schema: z.object({ text: z.string() }), + result_schema: z.object({ ok: z.boolean() }), + }); + await client.Mod.addCustomEvent("Custom.ready", { event_schema: z.object({ ok: z.boolean() }) }); + + assert.deepEqual(client.command_params_schemas.get("Custom.echo")?.parse(customParams), customParams); + assert.deepEqual(client.command_result_schemas.get("Custom.echo")?.parse(customResult), customResult); + assert.deepEqual(client.event_schemas.get("Custom.ready")?.parse({ ok: true }), { ok: true }); + assert.throws(() => client.command_params_schemas.get("Custom.echo")?.parse({ text: 1 })); + assert.throws(() => client.command_result_schemas.get("Custom.echo")?.parse({ ok: "yes" })); + assert.throws(() => client.event_schemas.get("Custom.ready")?.parse({ ok: "yes" })); + + await client.Mod.addCustomCommand({ + name: client.Target.getTargets, + result_schema: z.object({ + targetInfos: z.array( + z.object({ + targetId: z.string(), + type: z.string(), + title: z.string(), + url: z.string(), + attached: z.boolean(), + canAccessOpener: z.boolean(), + tabId: z.number().optional(), + }), + ), + }), + }); + await client.Mod.addCustomEvent({ name: client.Target.targetCreated }); + + const extendedTargets: Awaited> = { + targetInfos: [{ ...nativeEvent.targetInfo, tabId: 7 }], + }; + assert.deepEqual(client.command_result_schemas.get("Target.getTargets")?.parse(extendedTargets), extendedTargets); + assert.deepEqual( + client.event_schemas.get("Target.targetCreated")?.parse({ targetInfo: { ...nativeEvent.targetInfo, tabId: 7 } }), + { + targetInfo: { ...nativeEvent.targetInfo, tabId: 7 }, + }, + ); +}); diff --git a/js/test/test.AutoSessionRouter.ts b/js/test/test.AutoSessionRouter.ts index 42921be..b5e7414 100644 --- a/js/test/test.AutoSessionRouter.ts +++ b/js/test/test.AutoSessionRouter.ts @@ -66,7 +66,6 @@ test("AutoSessionRouter bounds detached session guards and clears them when a se test("AutoSessionRouter tracks real target sessions and execution contexts", async () => { const chrome = await new LocalBrowserLauncher({ headless: true, - sandbox: process.platform !== "linux", }).launch(); const ws = new WebSocket(chrome.cdp_url!); await once(ws, "open"); @@ -75,7 +74,7 @@ test("AutoSessionRouter tracks real target sessions and execution contexts", asy const router = new AutoSessionRouter( (method, params = {}, session_id = null) => send(method, params as Record, session_id) as Promise>, - () => 5_000, + () => 30_000, ); function send(method: string, params: Record = {}, session_id: string | null = null) { @@ -126,7 +125,7 @@ test("AutoSessionRouter tracks real target sessions and execution contexts", asy const session_id = router.sessionIdForTarget(target_id)!; const context_promise = router.waitForExecutionContext(session_id, { - timeout_ms: 5_000, + timeout_ms: 30_000, }); await send("Runtime.enable", {}, session_id); await expect(context_promise).resolves.toEqual(expect.any(Number)); diff --git a/js/test/test.BorrowedExtensionInjector.ts b/js/test/test.BorrowedExtensionInjector.ts index 00291b2..c4865b3 100644 --- a/js/test/test.BorrowedExtensionInjector.ts +++ b/js/test/test.BorrowedExtensionInjector.ts @@ -3,21 +3,28 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { test } from "vitest"; -import { LocalBrowserLauncher } from "../src/launcher/LocalBrowserLauncher.js"; import { ModCDPClient } from "../src/client/ModCDPClient.js"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension"); test("BorrowedExtensionInjector bootstraps ModCDP inside a live extension service worker", async () => { - const chrome = await new LocalBrowserLauncher({ - headless: true, - sandbox: process.platform !== "linux", - extra_args: [`--load-extension=${EXTENSION_PATH}`], - }).launch(); + const owner = new ModCDPClient({ + launcher: { + launcher_mode: "local", + launcher_options: { headless: true }, + }, + upstream: { upstream_mode: "ws" }, + injector: { + injector_mode: "auto", + injector_extension_path: EXTENSION_PATH, + injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"], + injector_trust_service_worker_target: true, + }, + }); const cdp = new ModCDPClient({ launcher: { launcher_mode: "remote" }, - upstream: { upstream_mode: "ws", upstream_cdp_url: chrome.cdp_url }, + upstream: { upstream_mode: "ws" }, injector: { injector_mode: "borrow", injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"], @@ -26,13 +33,17 @@ test("BorrowedExtensionInjector bootstraps ModCDP inside a live extension servic }); try { + await owner.connect(); + cdp.upstream.upstream_cdp_url = owner.cdp_url; await cdp.connect(); assert.equal(cdp.connect_timing?.injector_source, "borrowed"); assert.equal(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf"); - const target_infos = ((await cdp.send("Target.getTargets")) as { targetInfos?: unknown[] }).targetInfos ?? []; - assert.ok(target_infos.length > 0); + assert.equal( + await cdp.Mod.evaluate({ expression: "chrome.runtime.getURL('modcdp/service_worker.js')" }), + "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js", + ); } finally { await cdp.close(); - await chrome.close(); + await owner.close(); } }, 60_000); diff --git a/js/test/test.DiscoveredExtensionInjector.ts b/js/test/test.DiscoveredExtensionInjector.ts index c47d861..9c864e2 100644 --- a/js/test/test.DiscoveredExtensionInjector.ts +++ b/js/test/test.DiscoveredExtensionInjector.ts @@ -1,105 +1,49 @@ import assert from "node:assert/strict"; -import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { test } from "vitest"; -import { LocalBrowserLauncher } from "../src/launcher/LocalBrowserLauncher.js"; import { ModCDPClient } from "../src/client/ModCDPClient.js"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension"); -const CUSTOM_EXTENSION_ID = "hhklgmbgnbeghnjidampacgmgnhelifg"; -const CUSTOM_EXTENSION_PUBLIC_KEY = - "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzG1LUbtH0aHMKjTAUeT0saY8xfnRNENctFJme3C1qnsqT7PAXMxJC4nT7tBZy2gEGRirBb3zIZ3OyAu9a0QR8lTLupDp4qHWOhQ7dl9ZjxjQdYa4Gby0xuXLdQrJIxDbmuv+UVJvYa8vRTwQB8koygbzDDDP5/YiB6mc0hbh8XBb82Ossy7T280k8280o/rS0CXdioUraCHj58PDhfxbs18TBcYfOjuRqua9J2oddxobtGehSD0gDtbvn2IWDtRajOlgZZyuS1vLoSR7C1ulFzpRSYPEMhI2x+wphut7E3QImyJ577YeULVGpt988FcixOou7udjx3/IUWjpq8046wIDAQAB"; test("DiscoveredExtensionInjector attaches to an already-loaded real ModCDP extension", async () => { - const chrome = await new LocalBrowserLauncher({ - headless: true, - sandbox: process.platform !== "linux", - extra_args: [`--load-extension=${EXTENSION_PATH}`], - }).launch(); - const cdp = new ModCDPClient({ - launcher: { launcher_mode: "remote" }, - upstream: { upstream_mode: "ws", upstream_cdp_url: chrome.cdp_url }, + const owner = new ModCDPClient({ + launcher: { + launcher_mode: "local", + launcher_options: { headless: true }, + }, + upstream: { upstream_mode: "ws" }, injector: { - injector_mode: "discover", + injector_mode: "auto", + injector_extension_path: EXTENSION_PATH, injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"], injector_trust_service_worker_target: true, }, }); - - try { - await cdp.connect(); - assert.equal(cdp.connect_timing?.injector_source, "discovered"); - assert.equal(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf"); - const service_worker_url = await cdp.Mod.evaluate({ - expression: "chrome.runtime.getURL('modcdp/service_worker.js')", - }); - assert.match( - String(service_worker_url), - /^chrome-extension:\/\/mdedooklbnfejodmnhmkdpkaedafkehf\/modcdp\/service_worker\.js$/, - ); - } finally { - await cdp.close(); - await chrome.close(); - } -}, 60_000); - -test("DiscoveredExtensionInjector selects the configured extension when multiple ModCDP workers exist", async () => { - const custom_extension_path = await mkdtemp(path.join(tmpdir(), "modcdp-custom-extension-")); - await cp(EXTENSION_PATH, custom_extension_path, { recursive: true }); - const manifest_path = path.join(custom_extension_path, "manifest.json"); - const manifest = JSON.parse(await readFile(manifest_path, "utf8")) as Record; - manifest.key = CUSTOM_EXTENSION_PUBLIC_KEY; - manifest.name = "ModCDP Bridge Custom Test"; - await writeFile(manifest_path, `${JSON.stringify(manifest, null, 2)}\n`); - - const chrome = await new LocalBrowserLauncher({ - headless: true, - sandbox: process.platform !== "linux", - extra_args: [`--load-extension=${EXTENSION_PATH},${custom_extension_path}`], - }).launch(); const cdp = new ModCDPClient({ launcher: { launcher_mode: "remote" }, - upstream: { upstream_mode: "ws", upstream_cdp_url: chrome.cdp_url }, + upstream: { upstream_mode: "ws" }, injector: { injector_mode: "discover", - injector_extension_id: CUSTOM_EXTENSION_ID, injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"], injector_trust_service_worker_target: true, - injector_require_service_worker_target: true, }, }); try { + await owner.connect(); + cdp.upstream.upstream_cdp_url = owner.cdp_url; await cdp.connect(); assert.equal(cdp.connect_timing?.injector_source, "discovered"); - assert.equal(cdp.extension_id, CUSTOM_EXTENSION_ID); - assert.equal(await cdp.Mod.evaluate({ expression: "chrome.runtime.id" }), CUSTOM_EXTENSION_ID); - - const targets = (await cdp.sendRaw("Target.getTargets")) as { - targetInfos: { type?: string; url?: string }[]; - }; - const modcdp_workers = targets.targetInfos.filter( - (target) => target.type === "service_worker" && target.url?.endsWith("/modcdp/service_worker.js"), - ); - assert.equal( - modcdp_workers.some( - (target) => target.url === `chrome-extension://${CUSTOM_EXTENSION_ID}/modcdp/service_worker.js`, - ), - true, - ); + assert.equal(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf"); assert.equal( - modcdp_workers.some( - (target) => target.url === "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js", - ), - true, + await cdp.Mod.evaluate({ expression: "chrome.runtime.getURL('modcdp/service_worker.js')" }), + "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js", ); } finally { await cdp.close(); - await chrome.close(); - await rm(custom_extension_path, { recursive: true, force: true }); + await owner.close(); } }, 60_000); diff --git a/js/test/test.ExtensionInjector.ts b/js/test/test.ExtensionInjector.ts index ad22de7..85f3bb8 100644 --- a/js/test/test.ExtensionInjector.ts +++ b/js/test/test.ExtensionInjector.ts @@ -1,148 +1,14 @@ import assert from "node:assert/strict"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; import { test } from "vitest"; -import { ExtensionInjector, type ExtensionInjectionResult } from "../src/injector/ExtensionInjector.js"; -import { LocalBrowserLauncher } from "../src/launcher/LocalBrowserLauncher.js"; -import { CdpSocket } from "./helpers.BrowserLauncher.js"; - -const HERE = path.dirname(fileURLToPath(import.meta.url)); -const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension"); +import { ExtensionInjector } from "../src/injector/ExtensionInjector.js"; class ProbeExtensionInjector extends ExtensionInjector { - async inject(): Promise { - return await this.waitForReadyServiceWorker(this.options.injector_service_worker_ready_timeout_ms ?? 60_000, { - matched_only: true, - }); - } - matches(target: { type?: string; url?: string }) { return this.serviceWorkerTargetMatches(target); } - - async sendTimed(method: string, params: Record, session_id: string | null, timeout_ms: number) { - return await this.sendWithTimeout(method, params, session_id, timeout_ms); - } - - async wake() { - return await this.wakeConfiguredExtension(); - } } -test("ExtensionInjector probes a real extension service worker with shared base config", async () => { - const chrome = await new LocalBrowserLauncher({ - headless: true, - sandbox: process.platform !== "linux", - extra_args: [`--load-extension=${EXTENSION_PATH}`], - }).launch(); - const cdp = await CdpSocket.connect(chrome.cdp_url!); - const injector = new ProbeExtensionInjector({ - send: (method, params = {}, session_id = null) => - cdp.send(method, params as Record, session_id ?? undefined), - attachToTarget: async (target_id) => { - const attached = await cdp.send("Target.attachToTarget", { targetId: target_id, flatten: true }); - return typeof attached.sessionId === "string" ? attached.sessionId : null; - }, - injector_extension_id: "mdedooklbnfejodmnhmkdpkaedafkehf", - injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"], - injector_trust_service_worker_target: true, - }); - - try { - assert.deepEqual(injector.getLauncherConfig(), {}); - assert.deepEqual(injector.getTransportConfig(), { injector_extension_id: "mdedooklbnfejodmnhmkdpkaedafkehf" }); - const result = await injector.inject(); - assert.equal(result?.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf"); - assert.equal(result?.url?.endsWith("/modcdp/service_worker.js"), true); - } finally { - await cdp.close(); - await injector.close(); - await chrome.close(); - } -}, 60_000); - -test("ExtensionInjector keeps the ModCDP service worker alive through offscreen keepalive", async () => { - const chrome = await new LocalBrowserLauncher({ - headless: true, - sandbox: process.platform !== "linux", - extra_args: [`--load-extension=${EXTENSION_PATH}`], - }).launch(); - const cdp = await CdpSocket.connect(chrome.cdp_url!); - const injector = new ProbeExtensionInjector({ - send: (method, params = {}, session_id = null) => - cdp.send(method, params as Record, session_id ?? undefined), - attachToTarget: async (target_id) => { - const attached = await cdp.send("Target.attachToTarget", { targetId: target_id, flatten: true }); - return typeof attached.sessionId === "string" ? attached.sessionId : null; - }, - injector_extension_id: "mdedooklbnfejodmnhmkdpkaedafkehf", - injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"], - injector_trust_service_worker_target: true, - }); - - try { - const result = await injector.inject(); - assert.equal(result?.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf"); - const session_id = result?.session_id; - assert.equal(typeof session_id, "string"); - - let contexts: { type?: string; url?: string }[] = []; - for (let attempt = 0; attempt < 50; attempt++) { - const evaluated = await cdp.send( - "Runtime.evaluate", - { - expression: - "chrome.runtime.getContexts({}).then((contexts) => contexts.map((context) => ({ type: context.contextType, url: context.documentUrl || context.origin || '' })))", - awaitPromise: true, - returnByValue: true, - }, - session_id, - ); - contexts = (evaluated.result as { value?: { type?: string; url?: string }[] }).value ?? []; - if ( - contexts.some( - (context) => - context.type === "OFFSCREEN_DOCUMENT" && - context.url === "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/offscreen/keepalive.html", - ) - ) { - break; - } - await new Promise((resolve) => setTimeout(resolve, 100)); - } - assert.equal( - contexts.some( - (context) => - context.type === "OFFSCREEN_DOCUMENT" && - context.url === "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/offscreen/keepalive.html", - ), - true, - ); - - await new Promise((resolve) => setTimeout(resolve, 3_000)); - const targets = (await cdp.send("Target.getTargets")) as { targetInfos?: { type?: string; url?: string }[] }; - assert.equal( - targets.targetInfos?.some( - (target) => - target.type === "service_worker" && - target.url === "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js", - ), - true, - ); - const version = await cdp.send( - "Runtime.evaluate", - { expression: "globalThis.ModCDP?.__ModCDPServerVersion", returnByValue: true }, - session_id, - ); - assert.equal((version.result as { value?: unknown }).value, 2); - } finally { - await cdp.close(); - await injector.close(); - await chrome.close(); - } -}, 60_000); - test("ExtensionInjector owns shared injector config", async () => { const injector = new ProbeExtensionInjector({ injector_extension_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -171,71 +37,6 @@ test("ExtensionInjector owns shared injector config", async () => { } }); -test("ExtensionInjector sendWithTimeout enforces cdp send timeout", async () => { - const chrome = await new LocalBrowserLauncher({ - headless: true, - sandbox: process.platform !== "linux", - }).launch(); - const cdp = await CdpSocket.connect(chrome.cdp_url!); - let target_id: string | null = null; - const injector = new ProbeExtensionInjector({ - send: (method, params = {}, session_id = null) => - cdp.send(method, params as Record, session_id ?? undefined), - }); - - try { - const created = await cdp.send("Target.createTarget", { url: "about:blank#modcdp-timeout" }); - target_id = created.targetId as string; - const attached = await cdp.send("Target.attachToTarget", { targetId: target_id, flatten: true }); - const session_id = attached.sessionId as string; - await cdp.send("Runtime.enable", {}, session_id); - await assert.rejects( - () => - injector.sendTimed( - "Runtime.evaluate", - { expression: "new Promise(() => {})", awaitPromise: true }, - session_id, - 5, - ), - /Runtime\.evaluate timed out after 5ms/, - ); - } finally { - if (target_id) await cdp.send("Target.closeTarget", { targetId: target_id }).catch(() => ({})); - await injector.close(); - await cdp.close(); - await chrome.close(); - } -}); - -test("ExtensionInjector wakes configured extension with a hidden background target", async () => { - const chrome = await new LocalBrowserLauncher({ - headless: true, - sandbox: process.platform !== "linux", - extra_args: [`--load-extension=${EXTENSION_PATH}`], - }).launch(); - const cdp = await CdpSocket.connect(chrome.cdp_url!); - const injector = new ProbeExtensionInjector({ - injector_extension_id: "mdedooklbnfejodmnhmkdpkaedafkehf", - send: (method, params = {}, session_id = null) => - cdp.send(method, params as Record, session_id ?? undefined), - }); - - try { - assert.equal(await injector.wake(), true); - const targets = (await cdp.send("Target.getTargets")) as { targetInfos?: { url?: string }[] }; - assert.equal( - targets.targetInfos?.some( - (target) => target.url === "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/wake.html", - ), - true, - ); - } finally { - await injector.close(); - await cdp.close(); - await chrome.close(); - } -}); - test("ExtensionInjector base inject reports the subclass name", async () => { await assert.rejects(() => new ExtensionInjector().inject(), /ExtensionInjector\.inject is not implemented/); }); diff --git a/js/test/test.ExtensionsLoadUnpackedInjector.ts b/js/test/test.ExtensionsLoadUnpackedInjector.ts index a514045..65f3c2f 100644 --- a/js/test/test.ExtensionsLoadUnpackedInjector.ts +++ b/js/test/test.ExtensionsLoadUnpackedInjector.ts @@ -1,29 +1,21 @@ import assert from "node:assert/strict"; +import { existsSync } from "node:fs"; +import path from "node:path"; import { test } from "vitest"; import { ExtensionsLoadUnpackedInjector } from "../src/injector/ExtensionsLoadUnpackedInjector.js"; -import { LocalBrowserLauncher } from "../src/launcher/LocalBrowserLauncher.js"; -import { CdpSocket } from "./helpers.BrowserLauncher.js"; -test("ExtensionsLoadUnpackedInjector exercises the real CDP loadUnpacked path", async () => { - const chrome = await new LocalBrowserLauncher({ - headless: true, - sandbox: process.platform !== "linux", - }).launch(); - const cdp = await CdpSocket.connect(chrome.cdp_url!); - const injector = new ExtensionsLoadUnpackedInjector({ - send: (method, params = {}, session_id = null) => - cdp.send(method, params as Record, session_id ?? undefined), - }); +test("ExtensionsLoadUnpackedInjector prepares the default packaged extension zip", async () => { + const injector = new ExtensionsLoadUnpackedInjector(); try { await injector.prepare(); - const result = await injector.inject(); - assert.equal(result, null); - assert.match(injector.last_error?.message ?? "", /Method not available|Method.*not.*found|wasn't found/i); + const unpacked_extension_path = (injector as unknown as { unpacked_extension_path?: string | null }) + .unpacked_extension_path; + assert.equal(typeof unpacked_extension_path, "string"); + assert.match(unpacked_extension_path!, /modcdp-extension-/); + assert.equal(existsSync(path.join(unpacked_extension_path!, "manifest.json")), true); } finally { - await cdp.close(); await injector.close(); - await chrome.close(); } -}, 60_000); +}); diff --git a/js/test/test.LocalBrowserLaunchExtensionInjector.ts b/js/test/test.LocalBrowserLaunchExtensionInjector.ts index d19d448..70800ba 100644 --- a/js/test/test.LocalBrowserLaunchExtensionInjector.ts +++ b/js/test/test.LocalBrowserLaunchExtensionInjector.ts @@ -1,77 +1,131 @@ import assert from "node:assert/strict"; +import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { test } from "vitest"; import { DEFAULT_MODCDP_EXTENSION_ID } from "../src/injector/ExtensionInjector.js"; import { LocalBrowserLaunchExtensionInjector } from "../src/injector/LocalBrowserLaunchExtensionInjector.js"; -import { ModCDPClient } from "../src/client/ModCDPClient.js"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension"); -test("LocalBrowserLaunchExtensionInjector loads the real extension during local launch", async () => { - const cdp = new ModCDPClient({ - launcher: { - launcher_mode: "local", - launcher_options: { - headless: process.platform === "linux" && !process.env.DISPLAY, - sandbox: process.platform !== "linux", - }, - }, - upstream: { upstream_mode: "ws" }, - injector: { - injector_mode: "inject", - injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"], - injector_trust_service_worker_target: true, - injector_service_worker_probe_timeout_ms: 30_000, - }, - client: { - client_cdp_send_timeout_ms: 30_000, - }, +function crc32(data: Buffer) { + let crc = 0xffffffff; + for (const byte of data) { + crc ^= byte; + for (let bit = 0; bit < 8; bit++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1)); + } + return (crc ^ 0xffffffff) >>> 0; +} + +function storedZipEntry(name: string, data: Buffer) { + const name_buffer = Buffer.from(name); + const checksum = crc32(data); + const local = Buffer.alloc(30); + local.writeUInt32LE(0x04034b50, 0); + local.writeUInt16LE(20, 4); + local.writeUInt32LE(checksum, 14); + local.writeUInt32LE(data.length, 18); + local.writeUInt32LE(data.length, 22); + local.writeUInt16LE(name_buffer.length, 26); + const central = Buffer.alloc(46); + central.writeUInt32LE(0x02014b50, 0); + central.writeUInt16LE(20, 4); + central.writeUInt16LE(20, 6); + central.writeUInt32LE(checksum, 16); + central.writeUInt32LE(data.length, 20); + central.writeUInt32LE(data.length, 24); + central.writeUInt16LE(name_buffer.length, 28); + const central_offset = local.length + name_buffer.length + data.length; + const end = Buffer.alloc(22); + end.writeUInt32LE(0x06054b50, 0); + end.writeUInt16LE(1, 8); + end.writeUInt16LE(1, 10); + end.writeUInt32LE(central.length + name_buffer.length, 12); + end.writeUInt32LE(central_offset, 16); + return Buffer.concat([local, name_buffer, data, central, name_buffer, end]); +} + +test("LocalBrowserLaunchExtensionInjector rejects zip entries outside extraction directory", async () => { + const temp_dir = mkdtempSync(path.join(os.tmpdir(), "modcdp-bad-zip-")); + const zip_path = path.join(temp_dir, "extension.zip"); + writeFileSync(zip_path, storedZipEntry("../evil.txt", Buffer.from("evil"))); + const injector = new LocalBrowserLaunchExtensionInjector({ + injector_extension_path: zip_path, }); try { - await cdp.connect(); - assert.equal(cdp.connect_timing?.injector_source, "local_launch"); - assert.equal(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf"); - assert.match(cdp.ext_session_id ?? "", /^.+$/); - const service_worker_url = await cdp.Mod.evaluate({ - expression: "chrome.runtime.getURL('modcdp/service_worker.js')", - }); - assert.match( - String(service_worker_url), - /^chrome-extension:\/\/mdedooklbnfejodmnhmkdpkaedafkehf\/modcdp\/service_worker\.js$/, - ); + await assert.rejects(() => injector.prepare(), /escapes extension extraction directory/); + assert.equal(existsSync(path.join(temp_dir, "evil.txt")), false); } finally { - await cdp.close(); + await injector.close(); + rmSync(temp_dir, { recursive: true, force: true }); } -}, 60_000); +}); -test("LocalBrowserLaunchExtensionInjector prepares launcher config", async () => { - const injector = new LocalBrowserLaunchExtensionInjector({ injector_extension_path: EXTENSION_PATH }); +test("LocalBrowserLaunchExtensionInjector prepares an unpacked extension directory for --load-extension", async () => { + const injector = new LocalBrowserLaunchExtensionInjector({ + injector_extension_path: EXTENSION_PATH, + }); try { await injector.prepare(); - const extra_args = injector.getLauncherConfig().extra_args ?? []; - assert.equal(extra_args.length, 1); - assert.match(extra_args[0], /^--load-extension=/); + const unpacked_extension_path = (injector as unknown as { unpacked_extension_path?: string | null }) + .unpacked_extension_path; + assert.equal(typeof unpacked_extension_path, "string"); + assert.notEqual(unpacked_extension_path, EXTENSION_PATH); + assert.equal(existsSync(path.join(unpacked_extension_path!, "manifest.json")), true); + assert.deepEqual(injector.getLauncherConfig(), { + extra_args: [`--load-extension=${unpacked_extension_path}`], + }); assert.equal(injector.options.injector_extension_id, DEFAULT_MODCDP_EXTENSION_ID); } finally { await injector.close(); } }); -test("LocalBrowserLaunchExtensionInjector falls back to the default extension zip", async () => { +test("LocalBrowserLaunchExtensionInjector prepares the default extension zip for --load-extension", async () => { const injector = new LocalBrowserLaunchExtensionInjector(); try { await injector.prepare(); - const extra_args = injector.getLauncherConfig().extra_args ?? []; - assert.equal(extra_args.length, 1); - assert.match(extra_args[0], /^--load-extension=.*modcdp-extension-/); + const unpacked_extension_path = (injector as unknown as { unpacked_extension_path?: string | null }) + .unpacked_extension_path; + assert.equal(typeof unpacked_extension_path, "string"); + assert.match(unpacked_extension_path!, /modcdp-extension-/); + assert.equal(existsSync(path.join(unpacked_extension_path!, "manifest.json")), true); + assert.deepEqual(injector.getLauncherConfig(), { + extra_args: [`--load-extension=${unpacked_extension_path}`], + }); assert.equal(injector.options.injector_extension_id, DEFAULT_MODCDP_EXTENSION_ID); } finally { await injector.close(); } }); + +test("LocalBrowserLaunchExtensionInjector returns immediately when the launched extension target is absent", async () => { + const methods: string[] = []; + const injector = new LocalBrowserLaunchExtensionInjector({ + injector_extension_path: EXTENSION_PATH, + injector_trust_service_worker_target: true, + send: async (method) => { + methods.push(method); + if (method === "Target.getTargets") return { targetInfos: [] }; + throw new Error(`unexpected ${method}`); + }, + }); + + try { + await injector.prepare(); + const started_at = performance.now(); + const result = await injector.inject(); + const elapsed_ms = performance.now() - started_at; + assert.equal(result, null); + assert.deepEqual(methods, ["Target.getTargets"]); + assert.equal(elapsed_ms < 200, true, `inject() took ${elapsed_ms}ms`); + } finally { + await injector.close(); + } +}); diff --git a/js/test/test.LocalBrowserLauncher.ts b/js/test/test.LocalBrowserLauncher.ts index a644925..2924ba7 100644 --- a/js/test/test.LocalBrowserLauncher.ts +++ b/js/test/test.LocalBrowserLauncher.ts @@ -27,7 +27,6 @@ describe("LocalBrowserLauncher", () => { const port = await LocalBrowserLauncher.freePort(); const chrome = await new LocalBrowserLauncher({ headless: true, - sandbox: process.platform !== "linux", chrome_ready_timeout_ms: 45_000, chrome_ready_poll_interval_ms: 50, }).launch({ @@ -61,9 +60,20 @@ describe("LocalBrowserLauncher", () => { ); if (process.platform === "linux") { expect((chrome.proc as { spawnargs?: string[] }).spawnargs ?? []).toContain("--no-sandbox"); + } else { + expect((chrome.proc as { spawnargs?: string[] }).spawnargs ?? []).not.toContain("--no-sandbox"); } await expect(stat(userDataDir)).resolves.toBeTruthy(); cdp = await CdpSocket.connect(chrome.cdp_url!); + const systemInfo = await cdp.send("SystemInfo.getInfo"); + const commandLine = systemInfo.commandLine; + expect(commandLine).toEqual(expect.any(String)); + expect(commandLine).toContain("--window-size=900,700"); + if (process.platform === "linux") { + expect(commandLine).toContain("--no-sandbox"); + } else { + expect(commandLine).not.toContain("--no-sandbox"); + } await expectCdpBrowserSurface(cdp); } finally { await cdp?.close(); @@ -82,7 +92,6 @@ describe("LocalBrowserLauncher", () => { async () => { const chrome = await new LocalBrowserLauncher().launch({ headless: true, - sandbox: process.platform !== "linux", remote_debugging: "pipe", chrome_ready_timeout_ms: 45_000, }); @@ -115,7 +124,6 @@ describe("LocalBrowserLauncher", () => { async () => { const chrome = await new LocalBrowserLauncher().launch({ headless: true, - sandbox: process.platform !== "linux", remote_debugging: "pipe", loopback_cdp: true, chrome_ready_timeout_ms: 45_000, @@ -142,7 +150,6 @@ describe("LocalBrowserLauncher", () => { const userDataDir = await mkdtemp(path.join(tmpdir(), "modcdp-local-profile-")); const chrome = await new LocalBrowserLauncher({ headless: true, - sandbox: process.platform !== "linux", chrome_ready_timeout_ms: 45_000, }).launch({ user_data_dir: userDataDir, diff --git a/js/test/test.ModCDPClient.ts b/js/test/test.ModCDPClient.ts index 69f3752..71de444 100644 --- a/js/test/test.ModCDPClient.ts +++ b/js/test/test.ModCDPClient.ts @@ -1,8 +1,10 @@ import assert from "node:assert/strict"; -import { existsSync } from "node:fs"; +import { existsSync, readdirSync, statSync } from "node:fs"; +import { homedir, platform } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { test } from "vitest"; +import { z } from "zod"; import { LocalBrowserLauncher } from "../src/launcher/LocalBrowserLauncher.js"; import { ModCDPClient } from "../src/client/ModCDPClient.js"; @@ -10,6 +12,7 @@ import { CdpSocket } from "./helpers.BrowserLauncher.js"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension"); +const REVERSEWS_TEST_BROWSER_PATH = reversewsTestBrowserPath(); function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -106,7 +109,14 @@ test("ModCDPClient dispatches root events before extension session is attached", cdp._onRecv({ method: "Target.targetCreated", params: { - targetInfo: { targetId: "target-1", type: "page", url: "about:blank" }, + targetInfo: { + targetId: "target-1", + type: "page", + title: "about:blank", + url: "about:blank", + attached: false, + canAccessOpener: false, + }, }, }); @@ -126,7 +136,14 @@ test("ModCDPClient event dispatch snapshots handlers when once removes itself", cdp._onRecv({ method: "Target.targetCreated", params: { - targetInfo: { targetId: "target-1", type: "page", url: "about:blank" }, + targetInfo: { + targetId: "target-1", + type: "page", + title: "about:blank", + url: "about:blank", + attached: false, + canAccessOpener: false, + }, }, }); assert.deepEqual(seen, ["once", "persistent"]); @@ -135,17 +152,47 @@ test("ModCDPClient event dispatch snapshots handlers when once removes itself", cdp._onRecv({ method: "Target.targetCreated", params: { - targetInfo: { targetId: "target-2", type: "page", url: "about:blank" }, + targetInfo: { + targetId: "target-2", + type: "page", + title: "about:blank", + url: "about:blank", + attached: false, + canAccessOpener: false, + }, }, }); assert.deepEqual(seen, ["persistent"]); }); +test("ModCDPClient validates native command params before sending", async () => { + const cdp = new ModCDPClient(); + + await assert.rejects(() => cdp.send("Runtime.evaluate", {}), /expression/); +}); + +test("ModCDPClient validates native and registered custom events before dispatch", async () => { + const cdp = new ModCDPClient(); + + assert.throws(() => { + cdp._onRecv({ method: "Target.targetCreated", params: {} }); + }, /targetInfo/); + + await cdp.Mod.addCustomEvent("Custom.ready", { + event_schema: { ok: z.boolean() }, + }); + assert.throws(() => { + cdp._onRecv({ method: "Custom.ready", params: { ok: "yes" } }); + }, /boolean/); +}); + test("ModCDPClient connects with nested launch/upstream/extension/client/server config", async () => { const cdp = new ModCDPClient({ launcher: { launcher_mode: "local", - launcher_options: { headless: true, sandbox: process.platform !== "linux" }, + launcher_options: { + headless: true, + }, }, upstream: { upstream_mode: "ws" }, injector: { @@ -180,26 +227,28 @@ test("ModCDPClient connects with nested launch/upstream/extension/client/server assert.equal(cdp.upstream.upstream_mode, "ws"); assert.equal(cdp.upstream.upstream_reversews_wait_timeout_ms, 10_000); assert.equal(cdp.injector.injector_mode, "auto"); - assert.equal(cdp.client.client_routes["*.*"], "direct_cdp"); - assert.equal(cdp.upstream_endpoint_kind, "raw_cdp"); - assert.match(cdp.cdp_url ?? "", /^ws:\/\//); - await delay(2_000); - const targets = (await cdp.sendRaw("Target.getTargets")) as { - targetInfos: { type?: string; url?: string }[]; - }; assert.equal( - targets.targetInfos.some( - (target) => - target.type === "service_worker" && - target.url === `chrome-extension://${cdp.extension_id}/modcdp/service_worker.js`, + ["discovered", "local_launch", "extensions_load_unpacked", "borrowed"].includes( + String(cdp.connect_timing?.injector_source), ), true, ); + assert.equal(cdp.client.client_routes["*.*"], "direct_cdp"); + assert.equal(cdp.upstream_endpoint_kind, "raw_cdp"); + assert.match(cdp.cdp_url ?? "", /^ws:\/\//); + const service_worker_url = await cdp.Mod.evaluate({ + expression: "chrome.runtime.getURL('modcdp/service_worker.js')", + }); + assert.equal(service_worker_url, `chrome-extension://${cdp.extension_id}/modcdp/service_worker.js`); + const contexts = (await cdp.Mod.evaluate({ + expression: + "chrome.runtime.getContexts({}).then((contexts) => contexts.map((context) => ({ type: context.contextType, url: context.documentUrl || context.origin || '' })))", + })) as { type?: string; url?: string }[]; assert.equal( - targets.targetInfos.some( - (target) => - target.type === "background_page" && - target.url === `chrome-extension://${cdp.extension_id}/offscreen/keepalive.html`, + contexts.some( + (context) => + context.type === "OFFSCREEN_DOCUMENT" && + context.url === `chrome-extension://${cdp.extension_id}/offscreen/keepalive.html`, ), true, ); @@ -251,7 +300,93 @@ test("ModCDPClient preserves explicit empty service worker suffix config", async assert.deepEqual(cdp.injector.injector_service_worker_url_suffixes, []); assert.deepEqual((await cdp._baseInjectorConfig()).injector_service_worker_url_suffixes, []); -}); +}, 60_000); + +function reversewsTestBrowserPath() { + const explicit_candidates = [process.env.CHROME_PATH, platform() === "linux" ? "/usr/bin/chromium" : null].filter( + (candidate): candidate is string => Boolean(candidate), + ); + for (const candidate of explicit_candidates) { + if (existsSync(candidate)) return candidate; + } + const home = homedir(); + const patterns = + platform() === "darwin" + ? [ + path.join( + home, + "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing", + ), + path.join(home, "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium"), + path.join( + home, + "Library/Caches/puppeteer/chrome/mac*-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing", + ), + ] + : platform() === "win32" + ? [ + path.join( + process.env.LOCALAPPDATA || path.join(home, "AppData/Local"), + "ms-playwright/chromium-*/chrome-win*/chrome.exe", + ), + path.join(home, ".cache/puppeteer/chrome/win*-*/chrome-win*/chrome.exe"), + ] + : [ + path.join(home, ".cache/ms-playwright/chromium-*/chrome-linux*/chrome"), + "/opt/pw-browsers/chromium-*/chrome-linux*/chrome", + path.join(home, ".cache/puppeteer/chrome/linux-*/chrome-linux*/chrome"), + ]; + const candidates = newestFirst(patterns.flatMap(expandGlob)); + if (candidates[0]) return candidates[0]; + throw new Error("Reversews tests require CHROME_PATH, /usr/bin/chromium, or Chrome for Testing."); +} + +function expandGlob(pattern: string) { + const normalized = path.normalize(pattern); + const { root } = path.parse(normalized); + const parts = normalized.slice(root.length).split(path.sep).filter(Boolean); + let candidates = [root || "."]; + for (const part of parts) { + const has_wildcard = part.includes("*"); + const matcher = has_wildcard ? wildcardToRegExp(part) : null; + const next: string[] = []; + for (const base of candidates) { + if (!existsSync(base)) continue; + if (!has_wildcard) { + const candidate = path.join(base, part); + if (existsSync(candidate)) next.push(candidate); + continue; + } + for (const child of readdirSync(base)) { + if (matcher!.test(child)) next.push(path.join(base, child)); + } + } + candidates = next; + } + return candidates.filter((candidate) => existsSync(candidate)); +} + +function wildcardToRegExp(value: string) { + return new RegExp(`^${value.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`); +} + +function newestFirst(candidates: string[]) { + return [...new Set(candidates)].sort((a, b) => { + const left = scorePath(a); + const right = scorePath(b); + return right.version - left.version || right.mtime - left.mtime || a.localeCompare(b); + }); +} + +function scorePath(candidate: string) { + const numbers = candidate.match(/\d+/g)?.map(Number) ?? []; + const version = numbers.length > 0 ? Math.max(...numbers) : 0; + let mtime = 0; + try { + mtime = statSync(candidate).mtimeMs; + } catch {} + return { version, mtime }; +} test("ModCDPClient defaults service worker suffix config to the ModCDP worker", async () => { const cdp = new ModCDPClient(); @@ -281,7 +416,10 @@ test("ModCDPClient only exposes injector attach after CDP send is available", () test("ModCDPClient defaults launched ModCDP-server upstreams to extension auto", () => { for (const mode of ["nativemessaging", "reversews", "nats"] as const) { - const launched = new ModCDPClient({ launcher: { launcher_mode: "local" }, upstream: { upstream_mode: mode } }); + const launched = new ModCDPClient({ + launcher: { launcher_mode: "local" }, + upstream: { upstream_mode: mode }, + }); assert.equal(launched.launcher.launcher_mode, "local"); assert.equal(launched.upstream_endpoint_kind, "modcdp_server"); assert.equal(launched.injector.injector_mode, "auto"); @@ -293,6 +431,23 @@ test("ModCDPClient defaults launched ModCDP-server upstreams to extension auto", } }); +test("ModCDPClient orders local auto injection as launch flag then loadUnpacked fallback", async () => { + const cdp = new ModCDPClient({ + launcher: { launcher_mode: "local" }, + injector: { injector_mode: "auto" }, + }); + + assert.deepEqual( + (await cdp._injectorsForConfig()).map((injector) => injector.constructor.name), + [ + "LocalBrowserLaunchExtensionInjector", + "ExtensionsLoadUnpackedInjector", + "DiscoveredExtensionInjector", + "BorrowedExtensionInjector", + ], + ); +}); + test("ModCDPClient rejects unknown component modes at their owning factory boundary", async () => { await assert.rejects( () => @@ -302,11 +457,17 @@ test("ModCDPClient rejects unknown component modes at their owning factory bound /unknown upstream\.upstream_mode=bogus/, ); await assert.rejects( - () => new ModCDPClient({ launcher: { launcher_mode: "bogus" as any } })._browserLauncher(), + () => + new ModCDPClient({ + launcher: { launcher_mode: "bogus" as any }, + })._browserLauncher(), /unknown launcher\.launcher_mode=bogus/, ); await assert.rejects( - () => new ModCDPClient({ injector: { injector_mode: "bogus" as any } })._injectorsForConfig(), + () => + new ModCDPClient({ + injector: { injector_mode: "bogus" as any }, + })._injectorsForConfig(), /unknown injector\.injector_mode=bogus/, ); }); @@ -314,7 +475,10 @@ test("ModCDPClient rejects unknown component modes at their owning factory bound test("ModCDPClient.close does not close a remote browser it did not launch", async () => { const chrome = await new LocalBrowserLauncher({ headless: true, - sandbox: process.platform !== "linux", + chrome_ready_timeout_ms: 60_000, + // This test manually supplies --load-extension, so it intentionally uses + // the launch-flag browser path instead of relying on the client fallback. + executable_path: REVERSEWS_TEST_BROWSER_PATH, extra_args: [`--load-extension=${EXTENSION_PATH}`], }).launch(); const raw_cdp = await CdpSocket.connect(chrome.cdp_url!); @@ -322,10 +486,14 @@ test("ModCDPClient.close does not close a remote browser it did not launch", asy launcher: { launcher_mode: "remote" }, upstream: { upstream_mode: "ws", upstream_cdp_url: chrome.cdp_url }, injector: { - injector_mode: "discover", + injector_mode: "auto", + injector_extension_path: EXTENSION_PATH, injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"], injector_trust_service_worker_target: true, + injector_service_worker_ready_timeout_ms: 30_000, + injector_service_worker_probe_timeout_ms: 30_000, }, + client: { client_routes: { "*.*": "direct_cdp" } }, }); try { @@ -339,17 +507,22 @@ test("ModCDPClient.close does not close a remote browser it did not launch", asy await cdp.close(); await chrome.close(); } -}, 60_000); +}, 180_000); test("ModCDPClient.close keeps injector files until after launched browser shutdown", async () => { const cdp = new ModCDPClient({ launcher: { launcher_mode: "local", - launcher_options: { headless: true, sandbox: process.platform !== "linux" }, + launcher_options: { + headless: true, + // After explicit CHROME_PATH and CI /usr/bin/chromium, this test uses + // Chrome for Testing because Canary rejects --load-extension in this + // local launch injector path. + executable_path: REVERSEWS_TEST_BROWSER_PATH, + }, }, upstream: { - upstream_mode: "reversews", - upstream_reversews_wait_timeout_ms: 30_000, + upstream_mode: "ws", }, injector: { injector_mode: "auto", @@ -396,7 +569,9 @@ test("ModCDPClient.close clears top-level connection state", async () => { const cdp = new ModCDPClient({ launcher: { launcher_mode: "local", - launcher_options: { headless: true, sandbox: process.platform !== "linux" }, + launcher_options: { + headless: true, + }, }, upstream: { upstream_mode: "ws" }, injector: { @@ -413,4 +588,4 @@ test("ModCDPClient.close clears top-level connection state", async () => { assert.equal(cdp.transport, null); await assert.rejects(() => cdp.sendRaw("Browser.getVersion"), /ModCDP upstream is not connected/); -}); +}, 60_000); diff --git a/js/test/test.ModCDPClientCustomFlatNamespace.ts b/js/test/test.ModCDPClientCustomFlatNamespace.ts index a391950..1f8f16f 100644 --- a/js/test/test.ModCDPClientCustomFlatNamespace.ts +++ b/js/test/test.ModCDPClientCustomFlatNamespace.ts @@ -5,6 +5,14 @@ import { test } from "vitest"; import { z } from "zod"; import { ModCDPClient, type ModCDPClientInstance } from "../src/client/ModCDPClient.js"; +import { installModCDPServer } from "../src/server/ModCDPServer.js"; +import type { + ModCDPCustomCommandRegistration, + ModCDPCustomEventRegistration, + ProtocolParams, + ProtocolPayload, + ProtocolResult, +} from "../src/types/modcdp.js"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension"); @@ -16,7 +24,7 @@ test("custom commands install flat namespace methods through a real service work const cdp = new ModCDPClient({ launcher: { launcher_mode: "local", - launcher_options: { headless: true, sandbox: process.platform !== "linux" }, + launcher_options: { headless: true }, }, upstream: { upstream_mode: "ws" }, injector: { @@ -56,7 +64,7 @@ test("custom events validate raw string handlers through a real service worker", const cdp = new ModCDPClient({ launcher: { launcher_mode: "local", - launcher_options: { headless: true, sandbox: process.platform !== "linux" }, + launcher_options: { headless: true }, }, upstream: { upstream_mode: "ws" }, injector: { @@ -180,3 +188,32 @@ test("constructor custom command and event schemas validate nested payloads", () assert.deepEqual(event_schemas.get("Custom.count")?.parse({ value: 3 }), { value: 3 }); assert.throws(() => event_schemas.get("Custom.count")?.parse({ value: 0 })); }); + +test("service worker server validates registered custom command and event schemas", async () => { + const scope = {} as typeof globalThis; + const server = installModCDPServer(scope) as unknown as { + addCustomCommand(registration: ModCDPCustomCommandRegistration): ProtocolResult; + addCustomEvent(registration: ModCDPCustomEventRegistration): ProtocolResult; + handleCommand(method: string, params?: ProtocolParams, cdpSessionId?: string | null): Promise; + emit(eventName: string, payload?: ProtocolPayload, cdpSessionId?: string | null): Promise; + }; + + server.addCustomCommand({ + name: "Custom.double", + params_schema: z.object({ value: z.number() }), + result_schema: z.object({ value: z.number() }), + handler: async (params: { value: number }) => ({ value: params.value * 2 }), + }); + assert.deepEqual(await server.handleCommand("Custom.double", { value: 2 }), { value: 4 }); + await assert.rejects(() => server.handleCommand("Custom.double", { value: "2" })); + + server.addCustomCommand({ + name: "Custom.badResult", + result_schema: z.object({ ok: z.boolean() }), + handler: async () => ({ ok: "yes" }), + }); + await assert.rejects(() => server.handleCommand("Custom.badResult", {})); + + server.addCustomEvent({ name: "Custom.ready", event_schema: z.object({ ok: z.boolean() }) }); + await assert.rejects(() => server.emit("Custom.ready", { ok: "yes" })); +}); diff --git a/js/test/test.ModCDPClientRoutedDefaultOverrides.ts b/js/test/test.ModCDPClientRoutedDefaultOverrides.ts index e0c2ac5..24ba83a 100644 --- a/js/test/test.ModCDPClientRoutedDefaultOverrides.ts +++ b/js/test/test.ModCDPClientRoutedDefaultOverrides.ts @@ -3,9 +3,8 @@ import { test } from "vitest"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { LocalBrowserLauncher } from "../src/launcher/LocalBrowserLauncher.js"; import { ModCDPClient } from "../src/client/ModCDPClient.js"; -import { commands, events } from "../src/types/generated/zod.js"; +import { events } from "../src/types/generated/zod.js"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension"); @@ -73,17 +72,27 @@ test( "service-worker routed standard CDP commands and events can be transformed", { timeout: DEFAULT_ROUTED_OVERRIDES_TEST_TIMEOUT_MS }, async () => { - const chrome = await new LocalBrowserLauncher().launch({ - headless: true, - sandbox: process.platform !== "linux", - extra_args: [`--load-extension=${EXTENSION_PATH}`], + const owner = new ModCDPClient({ + launcher: { + launcher_mode: "local", + launcher_options: { + headless: true, + }, + }, + upstream: { upstream_mode: "ws" }, + injector: { + injector_mode: "auto", + injector_extension_path: EXTENSION_PATH, + injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"], + injector_trust_service_worker_target: true, + }, }); + await owner.connect(); const cdp = new ModCDPClient({ launcher: { launcher_mode: "remote" }, - upstream: { upstream_mode: "ws", upstream_cdp_url: chrome.cdp_url }, + upstream: { upstream_mode: "ws", upstream_cdp_url: owner.cdp_url }, injector: { - injector_mode: "auto", - injector_extension_path: EXTENSION_PATH, + injector_mode: "discover", injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"], injector_trust_service_worker_target: true, }, @@ -95,17 +104,17 @@ test( }, }, server: { - server_loopback_cdp_url: chrome.cdp_url, + server_loopback_cdp_url: owner.cdp_url, server_routes: { "*.*": "loopback_cdp" }, }, }); try { await cdp.connect(); - assert.equal(cdp.cdp_url, chrome.cdp_url); - assert.equal(cdp.server.server_loopback_cdp_url, chrome.cdp_url); + assert.equal(cdp.cdp_url, owner.cdp_url); + assert.equal(cdp.server.server_loopback_cdp_url, owner.cdp_url); - const rawTargets = commands["Target.getTargets"].result.parse(await cdp._sendMessage("Target.getTargets")); + const rawTargets = await cdp.send("Target.getTargets"); assert.ok(rawTargets.targetInfos?.length > 0, "expected raw Target.getTargets targetInfos"); assert.equal( rawTargets.targetInfos.some((targetInfo) => Object.hasOwn(targetInfo, "tabId")), @@ -187,7 +196,7 @@ test( await cdp.Target.setDiscoverTargets({ discover: false }); } catch {} await cdp.close(); - await chrome.close(); + await owner.close(); } }, ); diff --git a/js/test/test.NativeMessagingUpstreamTransport.ts b/js/test/test.NativeMessagingUpstreamTransport.ts index d53e181..f51c600 100644 --- a/js/test/test.NativeMessagingUpstreamTransport.ts +++ b/js/test/test.NativeMessagingUpstreamTransport.ts @@ -1,7 +1,9 @@ import assert from "node:assert/strict"; import { once } from "node:events"; -import { existsSync } from "node:fs"; +import { existsSync, readdirSync, statSync } from "node:fs"; +import { mkdtemp, rm } from "node:fs/promises"; import net from "node:net"; +import { homedir, platform, tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { test } from "vitest"; @@ -11,6 +13,7 @@ import { ModCDPClient } from "../src/client/ModCDPClient.js"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension"); +const NATIVE_MESSAGING_TEST_BROWSER_PATH = extensionLaunchFlagTestBrowserPath(); const upstreamNativeMessagingHostName = (label: string) => `com.modcdp.test.${label}.${process.pid}`; test("nativemessaging upstream config owns manifest, host, wait timeout, loopback, and injector config", async () => { @@ -161,10 +164,19 @@ test("nativemessaging upstream accepts a replacement peer after disconnect", asy test("nativemessaging upstream installs the launch-profile native host manifest and connects to a real extension", async () => { const upstream_nativemessaging_host_name = "com.modcdp.bridge"; + const profile_dir = await mkdtemp(path.join(tmpdir(), "modcdp.native.")); const native_client = new ModCDPClient({ launcher: { launcher_mode: "local", - launcher_options: { headless: true, sandbox: process.platform !== "linux" }, + launcher_options: { + headless: true, + user_data_dir: profile_dir, + cleanup_user_data_dir: true, + // Native messaging is browser -> client only. After explicit CHROME_PATH + // and CI /usr/bin/chromium, this test uses Chrome for Testing because + // Canary rejects --load-extension in this local test path. + executable_path: NATIVE_MESSAGING_TEST_BROWSER_PATH, + }, }, upstream: { upstream_mode: "nativemessaging", @@ -204,6 +216,7 @@ test("nativemessaging upstream installs the launch-profile native host manifest assert.equal(typeof second_version.product, "string"); } finally { await native_client.close(); + await rm(profile_dir, { recursive: true, force: true }); } }, 90_000); @@ -215,3 +228,89 @@ async function waitFor(predicate: () => boolean, timeout_ms = 2_000) { } throw new Error("Timed out waiting for condition"); } + +function extensionLaunchFlagTestBrowserPath() { + const explicit_candidates = [process.env.CHROME_PATH, platform() === "linux" ? "/usr/bin/chromium" : null].filter( + (candidate): candidate is string => Boolean(candidate), + ); + for (const candidate of explicit_candidates) { + if (existsSync(candidate)) return candidate; + } + const home = homedir(); + const patterns = + platform() === "darwin" + ? [ + path.join( + home, + "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing", + ), + path.join(home, "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium"), + path.join( + home, + "Library/Caches/puppeteer/chrome/mac*-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing", + ), + ] + : platform() === "win32" + ? [ + path.join( + process.env.LOCALAPPDATA || path.join(home, "AppData/Local"), + "ms-playwright/chromium-*/chrome-win*/chrome.exe", + ), + path.join(home, ".cache/puppeteer/chrome/win*-*/chrome-win*/chrome.exe"), + ] + : [ + path.join(home, ".cache/ms-playwright/chromium-*/chrome-linux*/chrome"), + "/opt/pw-browsers/chromium-*/chrome-linux*/chrome", + path.join(home, ".cache/puppeteer/chrome/linux-*/chrome-linux*/chrome"), + ]; + const candidates = newestFirst(patterns.flatMap(expandGlob)); + if (candidates[0]) return candidates[0]; + throw new Error("Native messaging tests require CHROME_PATH, /usr/bin/chromium, or Chrome for Testing."); +} + +function expandGlob(pattern: string) { + const normalized = path.normalize(pattern); + const { root } = path.parse(normalized); + const parts = normalized.slice(root.length).split(path.sep).filter(Boolean); + let candidates = [root || "."]; + for (const part of parts) { + const has_wildcard = part.includes("*"); + const matcher = has_wildcard ? wildcardToRegExp(part) : null; + const next: string[] = []; + for (const base of candidates) { + if (!existsSync(base)) continue; + if (!has_wildcard) { + const candidate = path.join(base, part); + if (existsSync(candidate)) next.push(candidate); + continue; + } + for (const child of readdirSync(base)) { + if (matcher!.test(child)) next.push(path.join(base, child)); + } + } + candidates = next; + } + return candidates.filter((candidate) => existsSync(candidate)); +} + +function wildcardToRegExp(value: string) { + return new RegExp(`^${value.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`); +} + +function newestFirst(candidates: string[]) { + return [...new Set(candidates)].sort((a, b) => { + const left = scorePath(a); + const right = scorePath(b); + return right.version - left.version || right.mtime - left.mtime || a.localeCompare(b); + }); +} + +function scorePath(candidate: string) { + const numbers = candidate.match(/\d+/g)?.map(Number) ?? []; + const version = numbers.length > 0 ? Math.max(...numbers) : 0; + let mtime = 0; + try { + mtime = statSync(candidate).mtimeMs; + } catch {} + return { version, mtime }; +} diff --git a/js/test/test.PipeUpstreamTransport.ts b/js/test/test.PipeUpstreamTransport.ts index 7b31e7c..915e8a2 100644 --- a/js/test/test.PipeUpstreamTransport.ts +++ b/js/test/test.PipeUpstreamTransport.ts @@ -19,7 +19,7 @@ test("pipe upstream constructor, update, launcher config, and unconnected errors assert.equal(transport.update({ cdp_url: "pipe://1234" }), transport); assert.equal(transport.url, "pipe://1234"); await assert.rejects(() => transport.connect(), /upstream\.upstream_mode=pipe requires/); - assert.throws(() => transport.send({ id: 1, method: "Browser.getVersion" }), /CDP pipe is not connected/); + assert.throws(() => transport.send({ id: 1, method: "Runtime.evaluate" }), /CDP pipe is not connected/); }); test("pipe upstream resets connection state after pipe end and errors", async () => { @@ -31,7 +31,7 @@ test("pipe upstream resets connection state after pipe end and errors", async () transport.onClose((error) => closed.push(error)); await transport.connect(); - transport.send({ id: 1, method: "Browser.getVersion", params: {} }); + transport.send({ id: 1, method: "Runtime.evaluate", params: { expression: "1" } }); if (event_name === "end") pipe_read.emit("end"); else if (event_name === "read_error") pipe_read.emit("error", new Error("read failed")); @@ -39,7 +39,7 @@ test("pipe upstream resets connection state after pipe end and errors", async () assert.equal(closed.length, 1); assert.throws( - () => transport.send({ id: 2, method: "Browser.getVersion", params: {} }), + () => transport.send({ id: 2, method: "Runtime.evaluate", params: { expression: "1" } }), /CDP pipe is not connected/, ); await transport.close(); @@ -50,15 +50,16 @@ test("pipe upstream launches a real browser and uses a pid-scoped pipe URL", asy const cdp = new ModCDPClient({ launcher: { launcher_mode: "local", - launcher_options: { headless: true, sandbox: process.platform !== "linux" }, + launcher_options: { headless: true }, }, upstream: { upstream_mode: "pipe" }, injector: { - injector_mode: "auto", + injector_mode: "inject", injector_extension_path: EXTENSION_PATH, injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"], injector_trust_service_worker_target: true, }, + server: { server_routes: { "*.*": "chrome_debugger" } }, }); try { @@ -67,8 +68,12 @@ test("pipe upstream launches a real browser and uses a pid-scoped pipe URL", asy assert.equal(cdp.upstream_endpoint_kind, "raw_cdp"); assert.match(cdp.cdp_url ?? "", /^pipe:\/\/\d+$/); assert.equal(cdp.transport?.url, cdp.cdp_url); - const version = (await cdp.sendRaw("Browser.getVersion")) as Record; - assert.equal(typeof version.product, "string"); + await cdp.Mod.addCustomCommand("Custom.runtimeReadyState", { + expression: + "async () => await cdp.send('Runtime.evaluate', { expression: 'document.readyState', returnByValue: true })", + }); + const runtime = (await cdp.send("Custom.runtimeReadyState")) as { result?: { value?: unknown } }; + assert.equal(runtime.result?.value, "complete"); } finally { await cdp.close(); } diff --git a/js/test/test.RemoteBrowserLauncher.ts b/js/test/test.RemoteBrowserLauncher.ts index d245df9..b9bcb64 100644 --- a/js/test/test.RemoteBrowserLauncher.ts +++ b/js/test/test.RemoteBrowserLauncher.ts @@ -20,7 +20,6 @@ describe("RemoteBrowserLauncher", () => { const local = await new LocalBrowserLauncher().launch({ port: await LocalBrowserLauncher.freePort(), headless: true, - sandbox: process.platform !== "linux", chrome_ready_timeout_ms: 45_000, }); let cdp: CdpSocket | null = null; @@ -61,12 +60,10 @@ describe("RemoteBrowserLauncher", () => { const first = await new LocalBrowserLauncher().launch({ port: await LocalBrowserLauncher.freePort(), headless: true, - sandbox: process.platform !== "linux", }); const second = await new LocalBrowserLauncher().launch({ port: await LocalBrowserLauncher.freePort(), headless: true, - sandbox: process.platform !== "linux", }); try { diff --git a/js/test/test.ReverseWebSocketUpstreamTransport.ts b/js/test/test.ReverseWebSocketUpstreamTransport.ts index e3cdff5..3104255 100644 --- a/js/test/test.ReverseWebSocketUpstreamTransport.ts +++ b/js/test/test.ReverseWebSocketUpstreamTransport.ts @@ -1,7 +1,9 @@ import assert from "node:assert/strict"; import { once } from "node:events"; +import { existsSync, readdirSync, statSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { homedir, platform } from "node:os"; import WebSocket from "ws"; import { test } from "vitest"; @@ -11,14 +13,15 @@ import { ModCDPClient } from "../src/client/ModCDPClient.js"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension"); +const REVERSEWS_TEST_BROWSER_PATH = reversewsTestBrowserPath(); -test("reversews upstream config owns bind updates, wait timeout, and injector config", async () => { +test("reversews upstream config owns bind updates and wait timeout", async () => { const transport = new ReverseWebSocketUpstreamTransport({ upstream_reversews_bind: "127.0.0.1:29292", upstream_reversews_wait_timeout_ms: 10, }); assert.equal(transport.url, "ws://127.0.0.1:29292"); - assert.deepEqual(transport.getInjectorConfig(), { upstream_reversews_url: "ws://127.0.0.1:29292" }); + assert.deepEqual(transport.getInjectorConfig(), {}); assert.equal( transport.update({ upstream_reversews_bind: "127.0.0.1:29293", @@ -27,7 +30,7 @@ test("reversews upstream config owns bind updates, wait timeout, and injector co transport, ); assert.equal(transport.url, "ws://127.0.0.1:29293"); - assert.deepEqual(transport.getInjectorConfig(), { upstream_reversews_url: "ws://127.0.0.1:29293" }); + assert.deepEqual(transport.getInjectorConfig(), {}); assert.throws( () => transport.send({ id: 1, method: "Browser.getVersion" }), /No reverse ModCDP extension peer is connected/, @@ -129,48 +132,139 @@ test("reversews upstream accepts a replacement peer after disconnect", async () } }); -test("reversews upstream accepts a real extension reverse connection and routes CDP through loopback", async () => { - const reverse_bind = "127.0.0.1:29292"; - const reverse_url = `ws://${reverse_bind}`; - const reverse = new ModCDPClient({ +test("reversews upstream accepts a real extension reverse connection and routes CDP through chrome debugger", async () => { + const cdp = new ModCDPClient({ launcher: { launcher_mode: "local", launcher_options: { headless: process.platform === "linux" && !process.env.DISPLAY, - sandbox: process.platform !== "linux", + // Reversews is browser -> client only. After explicit CHROME_PATH and + // CI /usr/bin/chromium, these tests use Chrome for Testing because + // Canary rejects --load-extension in this local test path. + executable_path: REVERSEWS_TEST_BROWSER_PATH, }, }, - upstream: { upstream_mode: "reversews", upstream_reversews_bind: reverse_bind }, + upstream: { upstream_mode: "reversews" }, injector: { injector_mode: "auto", injector_extension_path: EXTENSION_PATH, injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"], injector_trust_service_worker_target: true, - }, - server: { - server_routes: { "*.*": "loopback_cdp" }, + injector_service_worker_probe_timeout_ms: 1_000, }, }); try { - await reverse.connect(); - assert.equal(reverse.transport?.mode, "reversews"); - assert.equal(reverse.upstream_endpoint_kind, "modcdp_server"); - assert.equal(reverse.transport?.url, reverse_url); + await cdp.connect(); + assert.equal(cdp.transport?.mode, "reversews"); + assert.equal(cdp.upstream_endpoint_kind, "modcdp_server"); + assert.equal(cdp.transport?.url, "ws://127.0.0.1:29292"); assert.equal( - (reverse.transport as ReverseWebSocketUpstreamTransport).peer_info?.extension_id, + (cdp.transport as ReverseWebSocketUpstreamTransport).peer_info?.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf", ); - const version = (await reverse.send("Browser.getVersion")) as Record; - assert.equal(typeof version.product, "string"); + const evaluated = (await cdp.send("Runtime.evaluate", { + expression: "location.href", + returnByValue: true, + })) as { result?: { value?: unknown } }; + assert.equal(evaluated.result?.value, "about:blank"); await new Promise((resolve) => setTimeout(resolve, 1_500)); - const second_version = (await reverse.send("Browser.getVersion")) as Record; - assert.equal(typeof second_version.product, "string"); + const second_evaluated = (await cdp.send("Runtime.evaluate", { + expression: "document.readyState", + returnByValue: true, + })) as { result?: { value?: unknown } }; + assert.equal(second_evaluated.result?.value, "complete"); } finally { - await reverse.close(); + await cdp.close(); } }, 60_000); +function reversewsTestBrowserPath() { + const explicit_candidates = [process.env.CHROME_PATH, platform() === "linux" ? "/usr/bin/chromium" : null].filter( + (candidate): candidate is string => Boolean(candidate), + ); + for (const candidate of explicit_candidates) { + if (existsSync(candidate)) return candidate; + } + const home = homedir(); + const patterns = + platform() === "darwin" + ? [ + path.join( + home, + "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing", + ), + path.join(home, "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium"), + path.join( + home, + "Library/Caches/puppeteer/chrome/mac*-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing", + ), + ] + : platform() === "win32" + ? [ + path.join( + process.env.LOCALAPPDATA || path.join(home, "AppData/Local"), + "ms-playwright/chromium-*/chrome-win*/chrome.exe", + ), + path.join(home, ".cache/puppeteer/chrome/win*-*/chrome-win*/chrome.exe"), + ] + : [ + path.join(home, ".cache/ms-playwright/chromium-*/chrome-linux*/chrome"), + "/opt/pw-browsers/chromium-*/chrome-linux*/chrome", + path.join(home, ".cache/puppeteer/chrome/linux-*/chrome-linux*/chrome"), + ]; + const candidates = newestFirst(patterns.flatMap(expandGlob)); + if (candidates[0]) return candidates[0]; + throw new Error("Reversews tests require CHROME_PATH, /usr/bin/chromium, or Chrome for Testing."); +} + +function expandGlob(pattern: string) { + const normalized = path.normalize(pattern); + const { root } = path.parse(normalized); + const parts = normalized.slice(root.length).split(path.sep).filter(Boolean); + let candidates = [root || "."]; + for (const part of parts) { + const has_wildcard = part.includes("*"); + const matcher = has_wildcard ? wildcardToRegExp(part) : null; + const next: string[] = []; + for (const base of candidates) { + if (!existsSync(base)) continue; + if (!has_wildcard) { + const candidate = path.join(base, part); + if (existsSync(candidate)) next.push(candidate); + continue; + } + for (const child of readdirSync(base)) { + if (matcher!.test(child)) next.push(path.join(base, child)); + } + } + candidates = next; + } + return candidates.filter((candidate) => existsSync(candidate)); +} + +function wildcardToRegExp(value: string) { + return new RegExp(`^${value.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`); +} + +function newestFirst(candidates: string[]) { + return [...new Set(candidates)].sort((a, b) => { + const left = scorePath(a); + const right = scorePath(b); + return right.version - left.version || right.mtime - left.mtime || a.localeCompare(b); + }); +} + +function scorePath(candidate: string) { + const numbers = candidate.match(/\d+/g)?.map(Number) ?? []; + const version = numbers.length > 0 ? Math.max(...numbers) : 0; + let mtime = 0; + try { + mtime = statSync(candidate).mtimeMs; + } catch {} + return { version, mtime }; +} + async function waitFor(predicate: () => boolean, timeout_ms = 2_000) { const deadline = Date.now() + timeout_ms; while (Date.now() < deadline) { diff --git a/js/test/test.WebSocketUpstreamTransport.ts b/js/test/test.WebSocketUpstreamTransport.ts index 01dc8b2..1f2ef98 100644 --- a/js/test/test.WebSocketUpstreamTransport.ts +++ b/js/test/test.WebSocketUpstreamTransport.ts @@ -26,7 +26,7 @@ test("ws upstream launches a real browser and speaks raw CDP", async () => { const cdp = new ModCDPClient({ launcher: { launcher_mode: "local", - launcher_options: { headless: true, sandbox: process.platform !== "linux" }, + launcher_options: { headless: true }, }, upstream: { upstream_mode: "ws" }, injector: { @@ -43,9 +43,16 @@ test("ws upstream launches a real browser and speaks raw CDP", async () => { assert.equal(cdp.upstream_endpoint_kind, "raw_cdp"); assert.equal(cdp.connect_timing?.upstream_mode, "ws"); assert.equal(cdp.connect_timing?.upstream_endpoint_kind, "raw_cdp"); + const connect_timing = cdp.connect_timing as + | { + transport_connected_at: number; + transport_duration_ms: number; + transport_started_at: number; + } + | undefined; assert.equal( - cdp.connect_timing?.transport_duration_ms, - (cdp.connect_timing?.transport_connected_at ?? 0) - (cdp.connect_timing?.transport_started_at ?? 0), + connect_timing?.transport_duration_ms, + (connect_timing?.transport_connected_at ?? 0) - (connect_timing?.transport_started_at ?? 0), ); assert.match(cdp.cdp_url ?? "", /^ws:\/\//); const version = (await cdp.sendRaw("Browser.getVersion")) as Record; @@ -74,7 +81,6 @@ test("ws upstream launches a real browser and speaks raw CDP", async () => { test("ws upstream resolves a bare host:port CDP endpoint to the browser websocket", async () => { const chrome = await new LocalBrowserLauncher({ headless: true, - sandbox: process.platform !== "linux", }).launch(); const transport = new WebSocketUpstreamTransport({ cdp_url: `127.0.0.1:${chrome.port}` }); @@ -98,7 +104,6 @@ test("ws upstream resolves a bare host:port CDP endpoint to the browser websocke test("ws upstream close clears connection state", async () => { const chrome = await new LocalBrowserLauncher({ headless: true, - sandbox: process.platform !== "linux", }).launch(); const transport = new WebSocketUpstreamTransport({ cdp_url: chrome.cdp_url }); diff --git a/js/test/test.proxy.ts b/js/test/test.proxy.ts index 9a02837..e0c9190 100644 --- a/js/test/test.proxy.ts +++ b/js/test/test.proxy.ts @@ -1,23 +1,24 @@ import assert from "node:assert/strict"; import { spawn, type ChildProcess } from "node:child_process"; import { once } from "node:events"; +import { existsSync, readdirSync, statSync } from "node:fs"; import { mkdtemp, rm } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { homedir, platform, tmpdir } from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { test } from "vitest"; +import { ModCDPClient } from "../src/client/ModCDPClient.js"; import { LocalBrowserLauncher } from "../src/launcher/LocalBrowserLauncher.js"; import { startProxy } from "../src/proxy/proxy.js"; -import { ModCDPClient } from "../src/client/ModCDPClient.js"; import { CdpSocket } from "./helpers.BrowserLauncher.js"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const EXTENSION_PATH = path.resolve(HERE, "..", "..", "dist", "extension"); const LOCAL_TEST_LAUNCH_OPTIONS = { headless: true, - sandbox: process.platform !== "linux", }; +const REVERSEWS_TEST_BROWSER_PATH = reversewsTestBrowserPath(); function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -64,14 +65,34 @@ async function expectProxyCdpWorks(proxy_url: string, transport: string) { const cdp = await CdpSocket.connect(proxy_url); let target_id: string | null = null; try { - const version = await cdp.send("Browser.getVersion"); - assert.equal(typeof version.product, "string"); - const evaluated = await cdp.send("Mod.evaluate", { expression: `({ ok: true, transport: ${JSON.stringify(transport)} })`, }); assert.deepEqual(evaluated, { ok: true, transport }); + if (transport.includes("reversews")) { + const runtime = await cdp.send("Runtime.evaluate", { + expression: "document.readyState", + returnByValue: true, + }); + assert.equal((runtime.result as { value?: unknown } | undefined)?.value, "complete"); + return; + } + + if (transport.includes("pipe")) { + await cdp.send("Mod.addCustomCommand", { + name: "Custom.runtimeReadyState", + expression: + "async () => await cdp.send('Runtime.evaluate', { expression: 'document.readyState', returnByValue: true })", + }); + const runtime = await cdp.send("Custom.runtimeReadyState"); + assert.equal((runtime.result as { value?: unknown } | undefined)?.value, "complete"); + return; + } + + const version = await cdp.send("Browser.getVersion"); + assert.equal(typeof version.product, "string"); + const created = await cdp.send("Target.createTarget", { url: `about:blank#modcdp-proxy-${transport}`, }); @@ -118,11 +139,11 @@ test("proxy upgrades a vanilla CDP websocket to ModCDP against a real browser ov }, upstream: { upstream_mode: "pipe" }, injector: { - injector_mode: "auto", + injector_mode: "inject", injector_extension_path: EXTENSION_PATH, }, server: { - server_routes: { "*.*": "loopback_cdp" }, + server_routes: { "*.*": "chrome_debugger" }, }, }); @@ -144,17 +165,17 @@ test("proxy CLI maps user-facing flags into a real pipe upstream browser session String(proxy_port), "--launcher-mode=local", "--launcher-options", - JSON.stringify({ headless: true, sandbox: process.platform !== "linux" }), + JSON.stringify({ headless: true }), "--upstream-mode=pipe", - "--injector-mode=auto", + "--injector-mode=inject", + "--injector-extension-path", + EXTENSION_PATH, "--injector-service-worker-url-suffixes", JSON.stringify(["/modcdp/service_worker.js"]), "--injector-trust-service-worker-target", "true", - "--injector-service-worker-probe-timeout-ms", - "30000", "--server-routes", - JSON.stringify({ "*.*": "loopback_cdp" }), + JSON.stringify({ "*.*": "chrome_debugger" }), ], { stdio: ["ignore", "pipe", "pipe"] }, ); @@ -184,7 +205,7 @@ test("proxy CLI maps local ws launch without requiring upstream ws url", async ( "--launcher-user-data-dir", user_data_dir, "--launcher-options", - JSON.stringify({ headless: true, sandbox: process.platform !== "linux" }), + JSON.stringify({ headless: true }), "--upstream-mode=ws", "--injector-mode=auto", "--injector-extension-path", @@ -213,11 +234,20 @@ test("proxy CLI maps local ws launch without requiring upstream ws url", async ( }, 60_000); test("proxy CLI maps ws upstream URL and route shorthands into an existing real browser", async () => { - const chrome = await new LocalBrowserLauncher({ - headless: true, - sandbox: process.platform !== "linux", - extra_args: [`--load-extension=${EXTENSION_PATH}`], - }).launch(); + const owner = new ModCDPClient({ + launcher: { + launcher_mode: "local", + launcher_options: LOCAL_TEST_LAUNCH_OPTIONS, + }, + upstream: { upstream_mode: "ws" }, + injector: { + injector_mode: "auto", + injector_extension_path: EXTENSION_PATH, + injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"], + injector_trust_service_worker_target: true, + }, + }); + await owner.connect(); const proxy_port = await LocalBrowserLauncher.freePort(); const proxy_script = path.resolve(HERE, "..", "..", "dist", "js", "src", "proxy", "proxy.js"); const proc = spawn( @@ -229,7 +259,7 @@ test("proxy CLI maps ws upstream URL and route shorthands into an existing real "--launcher-mode=remote", "--upstream-mode=ws", "--upstream-cdp-url", - chrome.cdp_url!, + owner.cdp_url!, "--injector-mode=discover", "--client-routes", JSON.stringify({ @@ -248,11 +278,11 @@ test("proxy CLI maps ws upstream URL and route shorthands into an existing real await expectProxyCdpWorks(`ws://127.0.0.1:${proxy_port}/devtools/browser/proxy`, "cli-ws"); } finally { await closeProcess(proc); - await chrome.close(); + await owner.close(); } }, 60_000); -test("proxy CLI maps user-facing flags into a real reversews local launch", async () => { +test("proxy CLI maps user-facing flags into a real reversews browser session", async () => { const proxy_port = await LocalBrowserLauncher.freePort(); const proxy_script = path.resolve(HERE, "..", "..", "dist", "js", "src", "proxy", "proxy.js"); const proc = spawn( @@ -263,15 +293,25 @@ test("proxy CLI maps user-facing flags into a real reversews local launch", asyn String(proxy_port), "--launcher-mode=local", "--launcher-options", - JSON.stringify({ headless: true, sandbox: process.platform !== "linux" }), + JSON.stringify({ + ...LOCAL_TEST_LAUNCH_OPTIONS, + // Reversews is browser -> client only. After explicit CHROME_PATH and + // CI /usr/bin/chromium, these tests use Chrome for Testing because + // Canary rejects --load-extension in this local test path. + executable_path: REVERSEWS_TEST_BROWSER_PATH, + }), "--upstream-mode=reversews", "--upstream-reversews-wait-timeout-ms", "10000", "--injector-mode=auto", "--injector-extension-path", EXTENSION_PATH, - "--server-routes", - JSON.stringify({ "*.*": "loopback_cdp" }), + "--injector-service-worker-url-suffixes", + JSON.stringify(["/modcdp/service_worker.js"]), + "--injector-trust-service-worker-target", + "true", + "--injector-service-worker-probe-timeout-ms", + "1000", ], { stdio: ["ignore", "pipe", "pipe"] }, ); @@ -286,105 +326,117 @@ test("proxy CLI maps user-facing flags into a real reversews local launch", asyn test("proxy upgrades a vanilla CDP websocket to ModCDP against a real browser over reversews upstream", async () => { const proxy_port = await LocalBrowserLauncher.freePort(); - const reverse_port = await LocalBrowserLauncher.freePort(); - const reverse_bind = `127.0.0.1:${reverse_port}`; - const reverse_url = `ws://${reverse_bind}`; const proxy = await startProxy({ port: proxy_port, - upstream: { upstream_mode: "reversews", upstream_reversews_bind: reverse_bind }, - server: { - server_routes: { "*.*": "loopback_cdp" }, - }, - }); - const bootstrap = new ModCDPClient({ launcher: { launcher_mode: "local", - launcher_options: { headless: true, sandbox: process.platform !== "linux" }, + launcher_options: { + ...LOCAL_TEST_LAUNCH_OPTIONS, + // Reversews is browser -> client only. After explicit CHROME_PATH and + // CI /usr/bin/chromium, these tests use Chrome for Testing because + // Canary rejects --load-extension in this local test path. + executable_path: REVERSEWS_TEST_BROWSER_PATH, + }, }, - upstream: { upstream_mode: "ws" }, + upstream: { upstream_mode: "reversews", upstream_reversews_wait_timeout_ms: 10_000 }, injector: { injector_mode: "auto", injector_extension_path: EXTENSION_PATH, injector_service_worker_url_suffixes: ["/modcdp/service_worker.js"], injector_trust_service_worker_target: true, - }, - server: { - server_routes: { "*.*": "loopback_cdp" }, + injector_service_worker_probe_timeout_ms: 1_000, }, }); try { - await bootstrap.connect(); - await bootstrap.send("Mod.evaluate", { - params: { reverse_url }, - expression: "async ({ reverse_url }) => ModCDP.startReverseBridge(reverse_url)", - }); await expectProxyCdpWorks(proxy.url, "reversews"); } finally { - await bootstrap.close(); await proxy.close(); } }, 90_000); -test("proxy reversews local launch auto-injects the extension through the real client path", async () => { - const proxy_port = await LocalBrowserLauncher.freePort(); - const proxy = await startProxy({ - port: proxy_port, - launcher: { - launcher_mode: "local", - launcher_options: { headless: true, sandbox: process.platform !== "linux" }, - }, - upstream: { - upstream_mode: "reversews", - upstream_reversews_wait_timeout_ms: 10_000, - }, - injector: { - injector_mode: "auto", - injector_extension_path: EXTENSION_PATH, - }, - server: { - server_routes: { "*.*": "loopback_cdp" }, - }, - }); +function reversewsTestBrowserPath() { + const explicit_candidates = [process.env.CHROME_PATH, platform() === "linux" ? "/usr/bin/chromium" : null].filter( + (candidate): candidate is string => Boolean(candidate), + ); + for (const candidate of explicit_candidates) { + if (existsSync(candidate)) return candidate; + } + const home = homedir(); + const patterns = + platform() === "darwin" + ? [ + path.join( + home, + "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing", + ), + path.join(home, "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium"), + path.join( + home, + "Library/Caches/puppeteer/chrome/mac*-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing", + ), + ] + : platform() === "win32" + ? [ + path.join( + process.env.LOCALAPPDATA || path.join(home, "AppData/Local"), + "ms-playwright/chromium-*/chrome-win*/chrome.exe", + ), + path.join(home, ".cache/puppeteer/chrome/win*-*/chrome-win*/chrome.exe"), + ] + : [ + path.join(home, ".cache/ms-playwright/chromium-*/chrome-linux*/chrome"), + "/opt/pw-browsers/chromium-*/chrome-linux*/chrome", + path.join(home, ".cache/puppeteer/chrome/linux-*/chrome-linux*/chrome"), + ]; + const candidates = newestFirst(patterns.flatMap(expandGlob)); + if (candidates[0]) return candidates[0]; + throw new Error("Reversews tests require CHROME_PATH, /usr/bin/chromium, or Chrome for Testing."); +} - try { - await expectProxyCdpWorks(proxy.url, "reversews-local-launch"); - } finally { - await proxy.close(); +function expandGlob(pattern: string) { + const normalized = path.normalize(pattern); + const { root } = path.parse(normalized); + const parts = normalized.slice(root.length).split(path.sep).filter(Boolean); + let candidates = [root || "."]; + for (const part of parts) { + const has_wildcard = part.includes("*"); + const matcher = has_wildcard ? wildcardToRegExp(part) : null; + const next: string[] = []; + for (const base of candidates) { + if (!existsSync(base)) continue; + if (!has_wildcard) { + const candidate = path.join(base, part); + if (existsSync(candidate)) next.push(candidate); + continue; + } + for (const child of readdirSync(base)) { + if (matcher!.test(child)) next.push(path.join(base, child)); + } + } + candidates = next; } -}, 90_000); + return candidates.filter((candidate) => existsSync(candidate)); +} -test("proxy passes custom extension discovery config through to ModCDPClient", async () => { - const proxy_port = await LocalBrowserLauncher.freePort(); - const reverse_port = await LocalBrowserLauncher.freePort(); - await assert.rejects( - () => - startProxy({ - port: proxy_port, - launcher: { - launcher_mode: "local", - launcher_options: { - headless: true, - sandbox: process.platform !== "linux", - extra_args: [`--load-extension=${EXTENSION_PATH}`], - }, - }, - upstream: { - upstream_mode: "reversews", - upstream_reversews_bind: `127.0.0.1:${reverse_port}`, - upstream_reversews_wait_timeout_ms: 1_000, - }, - injector: { - injector_mode: "discover", - injector_extension_id: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - injector_require_service_worker_target: true, - injector_service_worker_probe_timeout_ms: 200, - injector_service_worker_ready_timeout_ms: 200, - }, - server: { - server_routes: { "*.*": "loopback_cdp" }, - }, - }), - /Timed out waiting 1000ms for reverse ModCDP extension connection/, - ); -}, 60_000); +function wildcardToRegExp(value: string) { + return new RegExp(`^${value.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`); +} + +function newestFirst(candidates: string[]) { + return [...new Set(candidates)].sort((a, b) => { + const left = scorePath(a); + const right = scorePath(b); + return right.version - left.version || right.mtime - left.mtime || a.localeCompare(b); + }); +} + +function scorePath(candidate: string) { + const numbers = candidate.match(/\d+/g)?.map(Number) ?? []; + const version = numbers.length > 0 ? Math.max(...numbers) : 0; + let mtime = 0; + try { + mtime = statSync(candidate).mtimeMs; + } catch {} + return { version, mtime }; +} diff --git a/js/test/test.translate.ts b/js/test/test.translate.ts index fa122de..df9981f 100644 --- a/js/test/test.translate.ts +++ b/js/test/test.translate.ts @@ -25,12 +25,30 @@ test("translate routes, wraps, and unwraps ModCDP protocol messages deterministi ); assert.equal(wrapped.target, "service_worker"); assert.equal(wrapped.steps[0]?.method, "Runtime.callFunctionOn"); - assert.match(String(wrapped.steps[0]?.params.functionDeclaration), /attachToSession\("session-1"\)/); + const wrapped_step_params = wrapped.steps[0]?.params as { functionDeclaration?: unknown } | undefined; + assert.match(String(wrapped_step_params?.functionDeclaration), /attachToSession\("session-1"\)/); assert.equal(wrapped.steps[0]?.unwrap, "runtime"); const configured = wrapCommandIfNeeded("Mod.configure", { server: { server_routes: { "*.*": "loopback_cdp" } } }); assert.equal(configured.steps[0]?.unwrap, "runtime_json"); + const custom = wrapCommandIfNeeded( + "Custom.echo", + { secret: "x".repeat(100), nested: { ok: true } }, + { cdpSessionId: "session-1" }, + ); + const custom_step_params = custom.steps[0]?.params as + | { arguments?: Array<{ value?: unknown }>; functionDeclaration?: unknown } + | undefined; + assert.match(String(custom_step_params?.functionDeclaration), /JSON\.parse\(paramsJson\)/); + assert.doesNotMatch(String(custom_step_params?.functionDeclaration), /xxxxxxxxxx/); + assert.equal(custom_step_params?.arguments?.[0]?.value, "Custom.echo"); + assert.deepEqual(JSON.parse(String(custom_step_params?.arguments?.[1]?.value)), { + secret: "x".repeat(100), + nested: { ok: true }, + }); + assert.equal(custom_step_params?.arguments?.[2]?.value, "session-1"); + assert.deepEqual(unwrapResponseIfNeeded({ result: { type: "object", value: { ok: true } } }, "runtime"), { ok: true, }); diff --git a/package.json b/package.json index e706aa5..a9aa013 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modcdp", - "version": "0.0.13", + "version": "0.0.19", "repository": { "type": "git", "url": "git+https://github.com/pirate/modcdp.git" @@ -8,6 +8,8 @@ "files": [ "js/scripts/**/*.mjs", "dist/**/*", + "dist/extension/**/*", + "dist/js/**/*", "dist/extension.zip", "extension/src/**/*.ts", "extension/src/pages/**/*", @@ -132,7 +134,7 @@ "clean": "rm -rf dist", "build": "pnpm run build:package && pnpm run build:extension-assets && pnpm run build:python-readme", "build:clean": "pnpm run clean && pnpm run build", - "build:package": "tsc -p tsconfig.json && rm -rf dist/js/test", + "build:package": "tsc -p tsconfig.json && rm -rf dist/js/test && rm -f dist/.gitignore", "build:extension-assets": "node js/scripts/build-extension-assets.mjs", "build:python-readme": "mkdir -p python && if [ README.md -ef python/README.md ]; then :; else cp README.md python/README.md; fi", "typecheck": "tsc -p tsconfig.json --noEmit", @@ -159,6 +161,7 @@ "@types/chrome": "^0.1.40", "@types/node": "^25.6.0", "@types/ws": "^8.18.1", + "esbuild": "^0.28.0", "oxfmt": "^0.47.0", "oxlint": "^1.62.0", "playwright": "^1.59.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 159662b..58943a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,9 @@ importers: '@types/ws': specifier: ^8.18.1 version: 8.18.1 + esbuild: + specifier: ^0.28.0 + version: 0.28.0 oxfmt: specifier: ^0.47.0 version: 0.47.0 @@ -44,7 +47,7 @@ importers: version: 6.0.3 vitest: specifier: ^4.1.5 - version: 4.1.5(@types/node@25.6.0)(vite@8.0.11(@types/node@25.6.0)) + version: 4.1.5(@types/node@25.6.0)(vite@8.0.11(@types/node@25.6.0)(esbuild@0.28.0)) optionalDependencies: ws: specifier: ^8.18.0 @@ -101,6 +104,162 @@ packages: resolution: {integrity: sha512-23pSHE9+ZEPa5wXGeAcd8tA0Dn8Ba+pVsnjKVk4VcnJ9s1soEDt7197R5p87hPpw4X52uoBzFFlC7HGIPhFYFA==} engines: {bun: '>=1', deno: '>=2', node: '>=22'} + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@j178/prek-darwin-arm64@0.3.10': resolution: {integrity: sha512-7KHluUGx5lAEtRKnleVSmZGsc9JNeVMRLbSngwU25e4XiJNdczVPrD5EcNwjngh4qoAm5xs05LE/waH7Kcowrg==} engines: {node: '>=18'} @@ -772,6 +931,11 @@ packages: es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1308,6 +1472,84 @@ snapshots: '@eplightning/nats-server-win32-arm64': 2.12.5 '@eplightning/nats-server-win32-x64': 2.12.5 + '@esbuild/aix-ppc64@0.28.0': + optional: true + + '@esbuild/android-arm64@0.28.0': + optional: true + + '@esbuild/android-arm@0.28.0': + optional: true + + '@esbuild/android-x64@0.28.0': + optional: true + + '@esbuild/darwin-arm64@0.28.0': + optional: true + + '@esbuild/darwin-x64@0.28.0': + optional: true + + '@esbuild/freebsd-arm64@0.28.0': + optional: true + + '@esbuild/freebsd-x64@0.28.0': + optional: true + + '@esbuild/linux-arm64@0.28.0': + optional: true + + '@esbuild/linux-arm@0.28.0': + optional: true + + '@esbuild/linux-ia32@0.28.0': + optional: true + + '@esbuild/linux-loong64@0.28.0': + optional: true + + '@esbuild/linux-mips64el@0.28.0': + optional: true + + '@esbuild/linux-ppc64@0.28.0': + optional: true + + '@esbuild/linux-riscv64@0.28.0': + optional: true + + '@esbuild/linux-s390x@0.28.0': + optional: true + + '@esbuild/linux-x64@0.28.0': + optional: true + + '@esbuild/netbsd-arm64@0.28.0': + optional: true + + '@esbuild/netbsd-x64@0.28.0': + optional: true + + '@esbuild/openbsd-arm64@0.28.0': + optional: true + + '@esbuild/openbsd-x64@0.28.0': + optional: true + + '@esbuild/openharmony-arm64@0.28.0': + optional: true + + '@esbuild/sunos-x64@0.28.0': + optional: true + + '@esbuild/win32-arm64@0.28.0': + optional: true + + '@esbuild/win32-ia32@0.28.0': + optional: true + + '@esbuild/win32-x64@0.28.0': + optional: true + '@j178/prek-darwin-arm64@0.3.10': optional: true @@ -1619,13 +1861,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.5(vite@8.0.11(@types/node@25.6.0))': + '@vitest/mocker@4.1.5(vite@8.0.11(@types/node@25.6.0)(esbuild@0.28.0))': dependencies: '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.11(@types/node@25.6.0) + vite: 8.0.11(@types/node@25.6.0)(esbuild@0.28.0) '@vitest/pretty-format@4.1.5': dependencies: @@ -1749,6 +1991,35 @@ snapshots: es-module-lexer@2.1.0: {} + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + escalade@3.2.0: {} escodegen@2.1.0: @@ -2156,7 +2427,7 @@ snapshots: undici-types@7.19.2: {} - vite@8.0.11(@types/node@25.6.0): + vite@8.0.11(@types/node@25.6.0)(esbuild@0.28.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -2165,12 +2436,13 @@ snapshots: tinyglobby: 0.2.16 optionalDependencies: '@types/node': 25.6.0 + esbuild: 0.28.0 fsevents: 2.3.3 - vitest@4.1.5(@types/node@25.6.0)(vite@8.0.11(@types/node@25.6.0)): + vitest@4.1.5(@types/node@25.6.0)(vite@8.0.11(@types/node@25.6.0)(esbuild@0.28.0)): dependencies: '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(vite@8.0.11(@types/node@25.6.0)) + '@vitest/mocker': 4.1.5(vite@8.0.11(@types/node@25.6.0)(esbuild@0.28.0)) '@vitest/pretty-format': 4.1.5 '@vitest/runner': 4.1.5 '@vitest/snapshot': 4.1.5 @@ -2187,7 +2459,7 @@ snapshots: tinyexec: 1.1.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.11(@types/node@25.6.0) + vite: 8.0.11(@types/node@25.6.0)(esbuild@0.28.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.6.0 diff --git a/python/README.md b/python/README.md index 1967245..82bb61c 100644 --- a/python/README.md +++ b/python/README.md @@ -150,12 +150,13 @@ Native messaging mode creates the local native host wrapper and browser manifest Use reverse mode when the browser does not expose a public CDP websocket to the final client, but the ModCDP extension can open a websocket back to a local proxy. The proxy still serves a normal-looking CDP endpoint to Playwright, Puppeteer, Stagehand, or any other CDP client: ```sh -pnpm run proxy -- --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29292 --listen 127.0.0.1:9223 -pnpm run proxy -- --launcher-mode=local --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29292 --port 9223 +pnpm run proxy -- --upstream-mode=reversews --port 9223 +pnpm run proxy -- --launcher-mode=local --upstream-mode=reversews --port 9223 +pnpm run proxy -- --launcher-mode=local --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29293 --port 9223 # const browser = await playwright.chromium.connectOverCDP("http://127.0.0.1:9223") ``` -Reverse mode is opt-in. The shipped extension has no generated runtime config; it always tries the fixed local reverse connector at `ws://127.0.0.1:29292`. With `--launcher-mode=local`, use that default bind and the normal `ModCDPClient` launcher, injector, and `ReverseWebSocketUpstreamTransport` path will wake the service worker and accept its connection. With `--launcher-mode=none` or a non-default bind, an already-running extension or test must explicitly call `globalThis.ModCDP.startReverseBridge("ws://host:port", { reconnect_interval_ms: 2000 })` with the chosen endpoint because there is no side channel before the reverse connection exists. `--upstream-reversews-wait-timeout-ms` controls how long the proxy/client waits for that extension connection. Once it connects, it self-identifies as a ModCDP extension service worker and the proxy uses that reverse websocket as its upstream. `Mod.*`, expression-backed `Custom.*` commands, custom event fanout, middleware, and normal CDP commands all stay routed through `globalThis.ModCDP.handleCommand(...)` in the service worker. +Reverse mode is opt-in. The shipped extension auto-connects to the fixed local reverse connector at `ws://127.0.0.1:29292`; the proxy/client listens there and waits for that extension connection. Keep `--upstream-reversews-bind` when using a custom extension build whose compiled autoconnect URL points at a different host or port. `--upstream-reversews-wait-timeout-ms` controls how long the proxy/client waits. Once connected, the extension identifies itself as a ModCDP service worker and the proxy uses that reverse websocket as its upstream. `Mod.*`, expression-backed `Custom.*` commands, custom event fanout, middleware, and normal CDP commands all stay routed through `globalThis.ModCDP.handleCommand(...)` in the service worker. Reverse mode is intentionally scoped to one local browser and one reverse extension connection per proxy process. The browser may still have other extensions installed; ModCDP does not require `--disable-extensions-except`. @@ -230,7 +231,7 @@ dist/ Built JS output used by the extension and Node CLI scr 3. Attach a session to that SW target and `Runtime.enable` on it. 4. Call `globalThis.ModCDP.configure(...)` to push the resolved loopback websocket and any explicit server route overrides into the SW. The clients do this automatically by default. -Reverse proxy mode flips the bootstrap direction: `js/src/proxy/proxy.ts --upstream-mode=reversews --upstream-reversews-bind=127.0.0.1:29292` listens for the extension, while still serving downstream clients from `--listen`. The extension artifact is fixed, so automatic launch/injection uses the default `127.0.0.1:29292` connector; non-default reverse binds need an external bootstrap call to `globalThis.ModCDP.startReverseBridge(...)` with the same URL. The service worker sends a `modcdp.reverse.hello` message and then accepts CDP-shaped command messages from the proxy. The proxy maps downstream request IDs to reverse request IDs and forwards reverse events back to the downstream CDP client. +Reverse proxy mode flips the bootstrap direction: `js/src/proxy/proxy.ts --upstream-mode=reversews --port 9223` listens for the extension on the configured reverse connector while still serving downstream clients from the proxy port. The service worker sends a `modcdp.reverse.hello` message and then accepts CDP-shaped command messages from the proxy. The proxy maps downstream request IDs to reverse request IDs and forwards reverse events back to the downstream CDP client. ### Send diff --git a/python/examples/demo.py b/python/examples/demo.py index d2ebed8..6718ce9 100644 --- a/python/examples/demo.py +++ b/python/examples/demo.py @@ -28,6 +28,7 @@ ROOT = Path(__file__).resolve().parent.parent.parent EXTENSION_PATH = ROOT / "dist" / "extension" +REVERSE_TRANSPORT_WAIT_TIMEOUT_MS = 60_000 LIVE_DEVTOOLS_ACTIVE_PORTS = [ Path.home() / "Library" / "Application Support" / "Google" / "Chrome" / "DevToolsActivePort", Path.home() / "Library" / "Application Support" / "Google" / "Chrome Beta" / "DevToolsActivePort", @@ -99,10 +100,17 @@ def parse_args(argv): def client_options_for(mode, upstream_mode, cdp_url, launch_options=None): + upstream: ProtocolPayload = {"upstream_mode": upstream_mode, "upstream_cdp_url": cdp_url} + if upstream_mode == "reversews": + upstream["upstream_reversews_wait_timeout_ms"] = REVERSE_TRANSPORT_WAIT_TIMEOUT_MS + if upstream_mode == "nativemessaging": + upstream["upstream_nativemessaging_wait_timeout_ms"] = REVERSE_TRANSPORT_WAIT_TIMEOUT_MS + if upstream_mode == "nats": + upstream["upstream_nats_wait_timeout_ms"] = REVERSE_TRANSPORT_WAIT_TIMEOUT_MS if mode == "direct": return { "launcher": {"launcher_mode": "remote" if cdp_url else "local", "launcher_options": launch_options or {}}, - "upstream": {"upstream_mode": upstream_mode, "upstream_cdp_url": cdp_url}, + "upstream": upstream, "injector": {"injector_mode": "auto", "injector_extension_path": str(EXTENSION_PATH)}, "client": {"client_routes": client_routes_for(mode)}, } @@ -111,7 +119,7 @@ def client_options_for(mode, upstream_mode, cdp_url, launch_options=None): } return { "launcher": {"launcher_mode": "remote" if cdp_url else "local", "launcher_options": launch_options or {}}, - "upstream": {"upstream_mode": upstream_mode, "upstream_cdp_url": cdp_url}, + "upstream": upstream, "injector": {"injector_mode": "auto", "injector_extension_path": str(EXTENSION_PATH)}, "client": {"client_routes": client_routes_for(mode)}, "server": server, @@ -149,6 +157,7 @@ def main(): else: cdp_url = None launch_options: dict[str, object] = { + "chrome_ready_timeout_ms": 60_000, "headless": sys.platform.startswith("linux") and not os.environ.get("DISPLAY"), "sandbox": not sys.platform.startswith("linux"), } @@ -156,7 +165,7 @@ def main(): launch_options["executable_path"] = os.environ["CHROME_PATH"] cdp = ModCDPClient(**client_options_for(mode, upstream_mode, cdp_url, launch_options)) - foreground_events = [] + page_target_events = [] target_created_events = [] events_lock = threading.Lock() @@ -165,10 +174,10 @@ def on_target_created(payload, *_): with events_lock: target_created_events.append(payload) - def on_foreground_changed(payload, *_): - print(f"Custom.foregroundTargetChanged -> {payload}") + def on_page_target_updated(payload, *_): + print(f"Custom.pageTargetUpdated -> {payload}") with events_lock: - foreground_events.append(payload) + page_target_events.append(payload) cdp.on("Target.targetCreated", on_target_created) @@ -323,19 +332,10 @@ def on_demo_event(payload, *_): raise RuntimeError("expected Custom.demoEvent") print(f"Custom.demoEvent -> {demo_event}") - foreground_event_registration = expect_object(cdp.send("Mod.addCustomEvent", {"name": "Custom.foregroundTargetChanged"}), "Mod.addCustomEvent Custom.foregroundTargetChanged") - if foreground_event_registration.get("registered") is not True: - raise RuntimeError(f"unexpected foreground event registration {foreground_event_registration}") - cdp.on("Custom.foregroundTargetChanged", on_foreground_changed) - cdp.send("Mod.evaluate", {"expression": '''async () => { - chrome.tabs.onActivated.addListener(async ({ tabId }) => { - const targets = await chrome.debugger.getTargets(); - const target = targets.find(target => target.type === "page" && target.tabId === tabId); - const tab = await chrome.tabs.get(tabId).catch(() => null); - await cdp.emit("Custom.foregroundTargetChanged", { tabId, targetId: target?.id ?? null, url: target?.url ?? tab?.url ?? null }); - }); - return true; - }'''}) + page_target_event_registration = expect_object(cdp.send("Mod.addCustomEvent", {"name": "Custom.pageTargetUpdated"}), "Mod.addCustomEvent Custom.pageTargetUpdated") + if page_target_event_registration.get("registered") is not True: + raise RuntimeError(f"unexpected page target event registration {page_target_event_registration}") + cdp.on("Custom.pageTargetUpdated", on_page_target_updated) cdp.send("Target.setDiscoverTargets", {"discover": True}) created_target = expect_object(cdp.send("Target.createTarget", {"url": "https://example.com", "background": True}), "Target.createTarget") @@ -359,20 +359,32 @@ def on_demo_event(payload, *_): print(f"Custom.TabIdFromTargetId -> {tab_from_target}") cdp.send("Target.activateTarget", {"targetId": created_target_id}) + page_target_emit_result = expect_object(cdp.send("Mod.evaluate", { + "params": {"targetId": created_target_id}, + "expression": '''async ({ targetId }) => { + const targets = await chrome.debugger.getTargets(); + const target = targets.find(target => target.id === targetId); + if (!target?.id) throw new Error(`target ${targetId} not found`); + await cdp.emit("Custom.pageTargetUpdated", { targetId: target.id, url: target.url ?? null }); + return { emitted: true, targetId: target.id }; + }''', + }), "Custom.pageTargetUpdated emit") + if page_target_emit_result.get("emitted") is not True or page_target_emit_result.get("targetId") != created_target_id: + raise RuntimeError(f"unexpected Custom.pageTargetUpdated emit result {page_target_emit_result}") deadline = time.monotonic() + 3.0 while True: with events_lock: - foreground = next((event for event in foreground_events if event.get("targetId") == created_target_id), None) - if foreground or time.monotonic() >= deadline: + page_target = next((event for event in page_target_events if event.get("targetId") == created_target_id), None) + if page_target or time.monotonic() >= deadline: break time.sleep(0.02) - if not foreground: - raise RuntimeError(f"expected Custom.foregroundTargetChanged for {created_target_id}") - if tab_from_target.get("tabId") != foreground.get("tabId"): - raise RuntimeError(f"unexpected Custom.foregroundTargetChanged result {foreground}") + if not page_target: + raise RuntimeError(f"expected Custom.pageTargetUpdated for {created_target_id}") + if tab_from_target.get("tabId") != page_target.get("tabId"): + raise RuntimeError(f"unexpected Custom.pageTargetUpdated result {page_target}") - target_from_tab = expect_object(cdp.send("Custom.targetIdFromTabId", {"tabId": foreground["tabId"]}), "Custom.targetIdFromTabId") - if target_from_tab.get("targetId") != created_target_id or target_from_tab.get("tabId") != foreground.get("tabId"): + target_from_tab = expect_object(cdp.send("Custom.targetIdFromTabId", {"tabId": page_target["tabId"]}), "Custom.targetIdFromTabId") + if target_from_tab.get("targetId") != created_target_id or target_from_tab.get("tabId") != page_target.get("tabId"): raise RuntimeError(f"unexpected Custom.targetIdFromTabId/middleware result {target_from_tab}") print(f"Custom.targetIdFromTabId -> {target_from_tab}") diff --git a/python/modcdp/client/ModCDPClient.py b/python/modcdp/client/ModCDPClient.py index 6639219..88dd819 100644 --- a/python/modcdp/client/ModCDPClient.py +++ b/python/modcdp/client/ModCDPClient.py @@ -13,24 +13,23 @@ import asyncio import inspect -import sys import threading import time from collections.abc import Mapping, Sequence from queue import Queue, Empty -from typing import Any, cast +from typing import Any, Literal, cast from pydantic import BaseModel, TypeAdapter, ValidationError from pydantic_core import to_jsonable_python from ..router.AutoSessionRouter import AutoSessionRouter from ..types.jsonschema import type_adapter_from_json_schema -from ..types.generated.cdp import AwaitableDict, CDPEvent, CDPSurfaceMixin, cdp_event_name, install_cdp_surface +from ..types.generated import cdp as generated_cdp +from ..types.generated.cdp import AwaitableDict, CDPEvent, CDPModel, CDPParams, CDPSurfaceMixin, cdp_event_name, install_cdp_surface from ..launcher.BrowserbaseBrowserLauncher import BrowserbaseBrowserLauncher from ..injector.BBBrowserExtensionInjector import BBBrowserExtensionInjector from ..injector.BorrowedExtensionInjector import BorrowedExtensionInjector from ..injector.DiscoveredExtensionInjector import DiscoveredExtensionInjector from ..injector.ExtensionInjector import ( - DEFAULT_MODCDP_WAKE_PATH, DEFAULT_MODCDP_SERVICE_WORKER_URL_SUFFIXES, ExtensionInjector, ExtensionInjectorConfig, @@ -120,7 +119,13 @@ class _ModDomain: def __init__(self, client: "ModCDPClient") -> None: self._client = client - def evaluate(self, *, expression: str, params: Mapping[str, Any] | None = None, cdpSessionId: str | None = None): + def evaluate( + self, + *, + expression: str, + params: Mapping[str, Any] | None = None, + cdpSessionId: str | None = None, + ) -> AwaitableDict | AwaitableValue: payload: dict[str, Any] = {"expression": expression} if params is not None: payload["params"] = dict(params) @@ -135,7 +140,7 @@ def addCustomCommand( params_schema: Any | None = None, result_schema: Any | None = None, expression: str | None = None, - ): + ) -> AwaitableDict | AwaitableValue: payload: dict[str, Any] = {"name": name} if params_schema is not None: payload["params_schema"] = params_schema @@ -145,22 +150,28 @@ def addCustomCommand( payload["expression"] = expression return self._client._send_command("Mod.addCustomCommand", payload) - def addCustomEvent(self, name: str, *, event_schema: Any | None = None): + def addCustomEvent(self, name: str, *, event_schema: Any | None = None) -> AwaitableDict | AwaitableValue: payload: dict[str, Any] = {"name": name} if event_schema is not None: payload["event_schema"] = event_schema return self._client._send_command("Mod.addCustomEvent", payload) - def addMiddleware(self, *, phase: str, expression: str, name: str | None = None): + def addMiddleware( + self, + *, + phase: Literal["request", "response", "event"], + expression: str, + name: str | None = None, + ) -> AwaitableDict | AwaitableValue: payload: dict[str, Any] = {"phase": phase, "expression": expression} if name is not None: payload["name"] = name return self._client._send_command("Mod.addMiddleware", payload) - def configure(self, **params: Any): + def configure(self, **params: Any) -> AwaitableDict | AwaitableValue: return self._client._send_command("Mod.configure", params) - def ping(self, **params: Any): + def ping(self, **params: Any) -> AwaitableDict | AwaitableValue: return self._client._send_command("Mod.ping", params) MODCDP_READY_EXPRESSION = ( @@ -265,8 +276,6 @@ def __init__( "injector_mode": injector_mode, "injector_extension_path": injector_input.get("injector_extension_path"), "injector_extension_id": injector_input.get("injector_extension_id"), - "injector_wake_path": _defaulted(injector_input.get("injector_wake_path"), DEFAULT_MODCDP_WAKE_PATH), - "injector_wake_url": injector_input.get("injector_wake_url"), "injector_service_worker_url_includes": list(cast(Sequence[str], injector_input.get("injector_service_worker_url_includes") or [])), "injector_service_worker_url_suffixes": list( cast( @@ -354,6 +363,7 @@ def __init__( self._launched_browser: Any | None = None self._extension_injectors: list[ExtensionInjector] = [] self._cdp = _RawCDP(self) + self._hydrate_native_protocol_schemas() self._hydrate_custom_surface() def connect(self) -> "ModCDPClient": @@ -592,7 +602,6 @@ def __getattr__(self, domain: str): def _server_configure_params(self) -> ModCDPServerConfig: server = dict(self.server or {}) - transport_injector_config = self.transport.getInjectorConfig() if self.transport is not None else {} server_routes = server.pop("server_routes", None) server_loopback_cdp_url = server.pop("server_loopback_cdp_url", None) server_browser_token = server.pop("server_browser_token", None) @@ -624,11 +633,6 @@ def _server_configure_params(self) -> ModCDPServerConfig: return cast(ModCDPServerConfig, { "upstream": { "upstream_mode": self.upstream.get("upstream_mode"), - **( - {"upstream_reversews_url": transport_injector_config.get("upstream_reversews_url")} - if transport_injector_config.get("upstream_reversews_url") - else {} - ), **({"upstream_nats_url": self.upstream.get("upstream_nats_url")} if self.upstream.get("upstream_nats_url") else {}), **( {"upstream_nats_subject_prefix": self.upstream.get("upstream_nats_subject_prefix")} @@ -823,7 +827,8 @@ def _extension_injectors_for_config(self) -> list[ExtensionInjector]: if mode == "none": return [] injectors: list[ExtensionInjector] = [] - if mode in ("auto", "discover"): + prefer_launch_injection = mode == "auto" and self.launcher.get("launcher_mode") == "local" + if mode in ("auto", "discover") and not prefer_launch_injection: injectors.append(DiscoveredExtensionInjector()) if mode in ("auto", "inject"): if self.launcher.get("launcher_mode") == "bb": @@ -831,6 +836,8 @@ def _extension_injectors_for_config(self) -> list[ExtensionInjector]: if self.launcher.get("launcher_mode") == "local": injectors.append(LocalBrowserLaunchExtensionInjector()) injectors.append(ExtensionsLoadUnpackedInjector()) + if prefer_launch_injection: + injectors.append(DiscoveredExtensionInjector()) if mode in ("auto", "borrow"): injectors.append(BorrowedExtensionInjector()) if not injectors: @@ -871,8 +878,6 @@ def attach_to_target(target_id: str) -> str | None: "waitForExecutionContext": self.auto_sessions.waitForExecutionContext, "injector_extension_path": cast(str | None, self.injector.get("injector_extension_path")), "injector_extension_id": cast(str | None, self.injector.get("injector_extension_id")), - "injector_wake_path": cast(str | None, self.injector.get("injector_wake_path")), - "injector_wake_url": cast(str | None, self.injector.get("injector_wake_url")), "injector_service_worker_url_includes": cast(list[str], self.injector["injector_service_worker_url_includes"]), "injector_service_worker_url_suffixes": cast(list[str], self.injector["injector_service_worker_url_suffixes"]), "injector_trust_service_worker_target": trust_service_worker_target, @@ -934,6 +939,35 @@ def _hydrate_custom_surface(self) -> None: continue self._register_custom_event(cast(ProtocolParams, event)) + def _hydrate_native_protocol_schemas(self) -> None: + with self._schema_lock: + for domain_name, domain_class in vars(generated_cdp).items(): + if not domain_name.endswith("Domain") or not isinstance(domain_class, type): + continue + domain = domain_name.removesuffix("Domain") + nested_classes = { + name: value + for name, value in vars(domain_class).items() + if isinstance(value, type) and issubclass(value, CDPModel) + } + for class_name, params_class in nested_classes.items(): + if issubclass(params_class, CDPEvent): + event_name = getattr(params_class, "cdp_event_name", None) + if isinstance(event_name, str): + self._event_schemas[event_name] = TypeAdapter(params_class) + continue + if not class_name.startswith("_") or not class_name.endswith("Params"): + continue + command_base = class_name[1:-6] + result_class = nested_classes.get(f"_{command_base}Result") + if result_class is None: + continue + method = f"{domain}.{command_base[:1].lower()}{command_base[1:]}" + if issubclass(params_class, CDPParams): + self._command_params_schemas[method] = TypeAdapter(params_class) + self._command_result_schemas[method] = TypeAdapter(result_class) + self._command_result_model_schemas.add(method) + def _register_custom_command(self, params: ProtocolParams) -> None: name = params.get("name") if not isinstance(name, str) or not name: @@ -996,10 +1030,14 @@ def _validate_command_params(self, method: str, params: ProtocolParams) -> Proto if adapter is None: return params try: - validated = adapter.validate_python(dict(params)) + validated = adapter.validate_python(dict(params), strict=True) except ValidationError as e: raise ValueError(f"{method} params did not match params_schema: {e}") from e - jsonable = to_jsonable_python(validated) + jsonable = ( + validated.model_dump(mode="json", exclude_none=True, by_alias=True) + if isinstance(validated, BaseModel) + else to_jsonable_python(validated) + ) if not isinstance(jsonable, Mapping): raise ValueError(f"{method} params_schema must validate to a JSON object") return cast(ProtocolParams, dict(jsonable)) @@ -1010,9 +1048,11 @@ def _validate_command_result(self, method: str, result: Any) -> Any: if adapter is None: return result try: - validated = adapter.validate_python(result) + validated = adapter.validate_python(result, strict=True) except ValidationError as e: raise ValueError(f"{method} result did not match result_schema: {e}") from e + if isinstance(validated, CDPModel): + return cast(JsonValue, validated.model_dump(mode="json", exclude_none=True, by_alias=True)) if method in self._command_result_model_schemas and isinstance(validated, BaseModel): fields = list(type(validated).model_fields) if len(fields) == 1: @@ -1023,22 +1063,29 @@ def _validate_command_result(self, method: str, result: Any) -> Any: def _validate_event_payload(self, event: str, payload: ProtocolPayload) -> Any | None: with self._schema_lock: adapter = self._event_schemas.get(event) - if adapter is None: + event_class = self._event_classes.get(event) + if adapter is None and event_class is None: return dict(payload) + if adapter is None and event_class is not None: + try: + return cast(ProtocolPayload, event_class.model_validate(dict(payload)).model_dump(mode="json", exclude_none=True, by_alias=True)) + except ValidationError as e: + raise ValueError(f"{event} event did not match native event schema: {e}") from e + assert adapter is not None try: - validated = adapter.validate_python(dict(payload)) + validated = adapter.validate_python(dict(payload), strict=True) except ValidationError as direct_error: if set(payload.keys()) != {"value"}: - print(f"[ModCDPClient] event {event} did not match event_schema: {direct_error}", file=sys.stderr) - return None + raise ValueError(f"{event} event did not match event_schema: {direct_error}") from direct_error try: - validated = adapter.validate_python(payload["value"]) + validated = adapter.validate_python(payload["value"], strict=True) except ValidationError as value_error: - print(f"[ModCDPClient] event {event} did not match event_schema: {value_error}", file=sys.stderr) - return None + raise ValueError(f"{event} event did not match event_schema: {value_error}") from value_error if event in self._event_model_schemas: return cast(ProtocolPayload, validated) return {"value": cast(JsonValue, to_jsonable_python(validated))} + if isinstance(validated, CDPModel): + return cast(ProtocolPayload, validated.model_dump(mode="json", exclude_none=True, by_alias=True)) if event in self._event_model_schemas: return cast(ProtocolPayload, validated) jsonable = to_jsonable_python(validated) diff --git a/python/modcdp/extension.zip b/python/modcdp/extension.zip index 268e970..2f87949 100644 Binary files a/python/modcdp/extension.zip and b/python/modcdp/extension.zip differ diff --git a/python/modcdp/injector/BorrowedExtensionInjector.py b/python/modcdp/injector/BorrowedExtensionInjector.py index e3f6bcd..326b624 100644 --- a/python/modcdp/injector/BorrowedExtensionInjector.py +++ b/python/modcdp/injector/BorrowedExtensionInjector.py @@ -117,6 +117,7 @@ def bootstrap_modcdp_server_expression() -> str: def modcdp_server_source() -> str: candidates: list[Path] = [] for parent in Path(__file__).resolve().parents: + candidates.append(parent / "dist" / "js" / "src" / "server" / "ModCDPServer.js") candidates.append(parent / "dist" / "extension" / "js" / "src" / "server" / "ModCDPServer.js") candidates.append(parent / "dist" / "extension" / "ModCDPServer.js") candidates.append(parent / "ModCDPServer.js") diff --git a/python/modcdp/injector/DiscoveredExtensionInjector.py b/python/modcdp/injector/DiscoveredExtensionInjector.py index 6e5e914..7a550f4 100644 --- a/python/modcdp/injector/DiscoveredExtensionInjector.py +++ b/python/modcdp/injector/DiscoveredExtensionInjector.py @@ -21,14 +21,6 @@ def inject(self) -> ExtensionInjectionResult | None: ) if waited: return {**waited, "source": "discovered"} - woke = self._wakeConfiguredExtension() - if woke: - waited = self._waitForReadyServiceWorker( - _timeout(self.options.get("injector_service_worker_probe_timeout_ms"), DEFAULT_SERVICE_WORKER_PROBE_TIMEOUT_MS), - matched_only=bool(self.options.get("injector_trust_service_worker_target")), - ) - if waited: - return {**waited, "source": "discovered"} if not self.options.get("injector_require_service_worker_target"): return None waited = self._waitForReadyServiceWorker( diff --git a/python/modcdp/injector/ExtensionInjector.py b/python/modcdp/injector/ExtensionInjector.py index 48b0ba7..510abb3 100644 --- a/python/modcdp/injector/ExtensionInjector.py +++ b/python/modcdp/injector/ExtensionInjector.py @@ -1,8 +1,14 @@ from __future__ import annotations +import base64 +import hashlib +import json import re +import shutil +import tempfile import threading import time +import zipfile from collections.abc import Callable, Mapping from pathlib import Path from queue import Empty, Queue @@ -16,7 +22,6 @@ EXT_ID_FROM_URL_RE = re.compile(r"^chrome-extension://([a-z]+)/") DEFAULT_MODCDP_EXTENSION_ID = "mdedooklbnfejodmnhmkdpkaedafkehf" DEFAULT_MODCDP_SERVICE_WORKER_URL_SUFFIXES = ["/modcdp/service_worker.js"] -DEFAULT_MODCDP_WAKE_PATH = "/modcdp/wake.html" MODCDP_READY_EXPRESSION = ( "Boolean(globalThis.ModCDP?.__ModCDPServerVersion >= 1 && globalThis.ModCDP?.handleCommand && globalThis.ModCDP?.addCustomEvent)" ) @@ -40,8 +45,6 @@ class ExtensionInjectorConfig(TypedDict, total=False): waitForExecutionContext: WaitForExecutionContext | None injector_extension_path: str | None injector_extension_id: str | None - injector_wake_path: str | None - injector_wake_url: str | None injector_service_worker_url_includes: list[str] injector_service_worker_url_suffixes: list[str] injector_trust_service_worker_target: bool @@ -55,7 +58,6 @@ class ExtensionInjectorConfig(TypedDict, total=False): injector_target_session_poll_interval_ms: int injector_browserbase_api_key: str | None injector_browserbase_base_url: str | None - upstream_reversews_url: str | None upstream_nativemessaging_host_name: str | None upstream_nats_url: str | None upstream_nats_subject_prefix: str | None @@ -66,6 +68,51 @@ def defaultModCDPExtensionPath() -> str | None: return str(bundled_extension) if bundled_extension.exists() else None +def prepareUnpackedExtension(extension_path: str) -> tuple[str, tempfile.TemporaryDirectory[str]]: + cleanup_dir = tempfile.TemporaryDirectory(prefix="modcdp-extension-") + try: + if extension_path.endswith(".zip"): + with zipfile.ZipFile(extension_path) as archive: + _extract_zip(archive, cleanup_dir.name) + else: + shutil.copytree(extension_path, cleanup_dir.name, dirs_exist_ok=True) + return _extension_root(cleanup_dir.name), cleanup_dir + except BaseException: + cleanup_dir.cleanup() + raise + + +def extensionIdFromManifestKey(extension_path: str) -> str | None: + manifest_path = Path(extension_path) / "manifest.json" + if not manifest_path.exists(): + return None + manifest = json.loads(manifest_path.read_text()) + key = manifest.get("key") if isinstance(manifest, dict) else None + if not isinstance(key, str) or not key.strip(): + return None + digest = hashlib.sha256(base64.b64decode(key)).digest()[:16] + alphabet = "abcdefghijklmnop" + return "".join(alphabet[byte >> 4] + alphabet[byte & 0x0F] for byte in digest) + + +def _extension_root(unpacked_path: str) -> str: + if (Path(unpacked_path) / "manifest.json").exists(): + return unpacked_path + nested = Path(unpacked_path) / "extension" + if (nested / "manifest.json").exists(): + return str(nested) + return unpacked_path + + +def _extract_zip(archive: zipfile.ZipFile, destination: str) -> None: + root = Path(destination).resolve() + for member in archive.infolist(): + target = (root / member.filename).resolve() + if target != root and root not in target.parents: + raise RuntimeError(f'zip entry "{member.filename}" escapes extension extraction directory') + archive.extractall(destination) + + def _defaulted(value: Any, fallback: int) -> int: return fallback if value is None else int(value) @@ -89,8 +136,6 @@ def __init__(self, options: ExtensionInjectorConfig | None = None) -> None: "waitForExecutionContext": None, "injector_extension_path": None, "injector_extension_id": None, - "injector_wake_path": DEFAULT_MODCDP_WAKE_PATH, - "injector_wake_url": None, "injector_service_worker_url_includes": [], "injector_service_worker_url_suffixes": [], "injector_trust_service_worker_target": False, @@ -104,7 +149,6 @@ def __init__(self, options: ExtensionInjectorConfig | None = None) -> None: "injector_target_session_poll_interval_ms": DEFAULT_TARGET_SESSION_POLL_INTERVAL_MS, "injector_browserbase_api_key": None, "injector_browserbase_base_url": None, - "upstream_reversews_url": None, "upstream_nativemessaging_host_name": None, "upstream_nats_url": None, "upstream_nats_subject_prefix": None, @@ -233,35 +277,6 @@ def _targetInfos(self) -> list[TargetInfo]: targets.append({"targetId": target_id, "type": target_type, "url": target_url}) return targets - def _configuredWakeUrl(self) -> str | None: - wake_url = self.options.get("injector_wake_url") - if wake_url: - return wake_url - extension_id = self.options.get("injector_extension_id") - if not extension_id: - return None - wake_path = self.options.get("injector_wake_path") - wake_path = DEFAULT_MODCDP_WAKE_PATH if wake_path is None else wake_path - return f"chrome-extension://{extension_id}{wake_path if wake_path.startswith('/') else f'/{wake_path}'}" - - def _wakeConfiguredExtension(self) -> bool: - wake_url = self._configuredWakeUrl() - if not wake_url or self.options.get("send") is None: - return False - try: - self._sendWithTimeout( - "Target.createTarget", - { - "url": wake_url, - "background": True, - "hidden": True, - "focus": False, - }, - ) - return True - except Exception: - return False - def _probeTarget( self, target: TargetInfo, diff --git a/python/modcdp/injector/ExtensionsLoadUnpackedInjector.py b/python/modcdp/injector/ExtensionsLoadUnpackedInjector.py index 4e72da6..9e4d7d3 100644 --- a/python/modcdp/injector/ExtensionsLoadUnpackedInjector.py +++ b/python/modcdp/injector/ExtensionsLoadUnpackedInjector.py @@ -2,11 +2,8 @@ import tempfile import time -import zipfile -import shutil -from pathlib import Path -from ..injector.ExtensionInjector import DEFAULT_SERVICE_WORKER_POLL_INTERVAL_MS, DEFAULT_SERVICE_WORKER_PROBE_TIMEOUT_MS, DEFAULT_SERVICE_WORKER_READY_TIMEOUT_MS, ExtensionInjector, ExtensionInjectionResult, defaultModCDPExtensionPath +from ..injector.ExtensionInjector import DEFAULT_SERVICE_WORKER_POLL_INTERVAL_MS, DEFAULT_SERVICE_WORKER_PROBE_TIMEOUT_MS, DEFAULT_SERVICE_WORKER_READY_TIMEOUT_MS, ExtensionInjector, ExtensionInjectionResult, defaultModCDPExtensionPath, prepareUnpackedExtension class ExtensionsLoadUnpackedInjector(ExtensionInjector): @@ -21,16 +18,7 @@ def prepare(self) -> None: super().prepare() return self.options["injector_extension_path"] = extension_path - if not extension_path.endswith(".zip"): - self.cleanup_dir = tempfile.TemporaryDirectory(prefix="modcdp-extension-") - shutil.copytree(extension_path, self.cleanup_dir.name, dirs_exist_ok=True) - self.unpacked_extension_path = _extension_root(self.cleanup_dir.name) - super().prepare() - return - self.cleanup_dir = tempfile.TemporaryDirectory(prefix="modcdp-extension-") - with zipfile.ZipFile(extension_path) as archive: - archive.extractall(self.cleanup_dir.name) - self.unpacked_extension_path = _extension_root(self.cleanup_dir.name) + self.unpacked_extension_path, self.cleanup_dir = prepareUnpackedExtension(extension_path) super().prepare() def inject(self) -> ExtensionInjectionResult | None: @@ -51,7 +39,6 @@ def inject(self) -> ExtensionInjectionResult | None: if not isinstance(extension_id, str) or not extension_id: raise RuntimeError(f"Extensions.loadUnpacked returned no extension id (got {load_result})") self.options["injector_extension_id"] = extension_id - self._wakeConfiguredExtension() sw_url_prefix = f"chrome-extension://{extension_id}/" deadline = time.monotonic() + (self.options.get("injector_service_worker_ready_timeout_ms") or DEFAULT_SERVICE_WORKER_READY_TIMEOUT_MS) / 1000 @@ -74,12 +61,3 @@ def close(self) -> None: if self.cleanup_dir: self.cleanup_dir.cleanup() self.cleanup_dir = None - - -def _extension_root(unpacked_path: str) -> str: - if (Path(unpacked_path) / "manifest.json").exists(): - return unpacked_path - nested = Path(unpacked_path) / "extension" - if (nested / "manifest.json").exists(): - return str(nested) - return unpacked_path diff --git a/python/modcdp/injector/LocalBrowserLaunchExtensionInjector.py b/python/modcdp/injector/LocalBrowserLaunchExtensionInjector.py index c3a47f0..79e51dc 100644 --- a/python/modcdp/injector/LocalBrowserLaunchExtensionInjector.py +++ b/python/modcdp/injector/LocalBrowserLaunchExtensionInjector.py @@ -1,15 +1,15 @@ from __future__ import annotations -import base64 -import hashlib -import json -import shutil import tempfile -import zipfile -from pathlib import Path from ..launcher.BrowserLauncher import BrowserLaunchOptions -from ..injector.ExtensionInjector import DEFAULT_SERVICE_WORKER_PROBE_TIMEOUT_MS, ExtensionInjector, ExtensionInjectionResult, defaultModCDPExtensionPath +from ..injector.ExtensionInjector import ( + ExtensionInjector, + ExtensionInjectionResult, + defaultModCDPExtensionPath, + extensionIdFromManifestKey, + prepareUnpackedExtension, +) class LocalBrowserLaunchExtensionInjector(ExtensionInjector): @@ -25,17 +25,7 @@ def prepare(self) -> None: super().prepare() return self.options["injector_extension_path"] = extension_path - if not extension_path.endswith(".zip"): - self.cleanup_dir = tempfile.TemporaryDirectory(prefix="modcdp-extension-") - shutil.copytree(extension_path, self.cleanup_dir.name, dirs_exist_ok=True) - self.unpacked_extension_path = _extension_root(self.cleanup_dir.name) - self._resolveExtensionId() - super().prepare() - return - self.cleanup_dir = tempfile.TemporaryDirectory(prefix="modcdp-extension-") - with zipfile.ZipFile(extension_path) as archive: - archive.extractall(self.cleanup_dir.name) - self.unpacked_extension_path = _extension_root(self.cleanup_dir.name) + self.unpacked_extension_path, self.cleanup_dir = prepareUnpackedExtension(extension_path) self._resolveExtensionId() super().prepare() @@ -45,9 +35,7 @@ def getLauncherConfig(self) -> BrowserLaunchOptions: return {"extra_args": [f"--load-extension={self.unpacked_extension_path}"]} def inject(self) -> ExtensionInjectionResult | None: - timeout_ms = self.options.get("injector_service_worker_probe_timeout_ms") or DEFAULT_SERVICE_WORKER_PROBE_TIMEOUT_MS - discovered = self._waitForReadyServiceWorker( - timeout_ms, + discovered = self._discoverReadyServiceWorker( matched_only=bool(self.options.get("injector_trust_service_worker_target")), ) return {**discovered, "source": "local_launch"} if discovered else None @@ -69,25 +57,3 @@ def _resolveExtensionId(self) -> str | None: if self.extension_id: self.options["injector_extension_id"] = self.extension_id return self.extension_id - - -def extensionIdFromManifestKey(extension_path: str) -> str | None: - manifest_path = Path(extension_path) / "manifest.json" - if not manifest_path.exists(): - return None - manifest = json.loads(manifest_path.read_text()) - key = manifest.get("key") if isinstance(manifest, dict) else None - if not isinstance(key, str) or not key.strip(): - return None - digest = hashlib.sha256(base64.b64decode(key)).digest()[:16] - alphabet = "abcdefghijklmnop" - return "".join(alphabet[byte >> 4] + alphabet[byte & 0x0F] for byte in digest) - - -def _extension_root(unpacked_path: str) -> str: - if (Path(unpacked_path) / "manifest.json").exists(): - return unpacked_path - nested = Path(unpacked_path) / "extension" - if (nested / "manifest.json").exists(): - return str(nested) - return unpacked_path diff --git a/python/modcdp/launcher/LocalBrowserLauncher.py b/python/modcdp/launcher/LocalBrowserLauncher.py index cce641d..8f5569e 100644 --- a/python/modcdp/launcher/LocalBrowserLauncher.py +++ b/python/modcdp/launcher/LocalBrowserLauncher.py @@ -1,7 +1,7 @@ from __future__ import annotations -import json import glob +import json import os import re import select @@ -43,7 +43,8 @@ def launch(self, options: BrowserLaunchOptions | None = None) -> LaunchedBrowser executable_path = self.findChromeBinary(merged.get("executable_path")) use_pipe = merged.get("remote_debugging") == "pipe" use_loopback_cdp = (not use_pipe) or bool(merged.get("loopback_cdp")) or merged.get("port") is not None - port = int(merged.get("port") or LocalBrowserLauncher.freePort()) if use_loopback_cdp else None + requested_port = merged.get("port") + port = int(requested_port) if use_loopback_cdp and requested_port is not None else (0 if use_loopback_cdp else None) temp_profile_dir: tempfile.TemporaryDirectory[str] | None = None profile_dir = merged.get("user_data_dir") if not profile_dir: @@ -75,7 +76,8 @@ def launch(self, options: BrowserLaunchOptions | None = None) -> LaunchedBrowser default_headless = sys.platform.startswith("linux") and not os.environ.get("DISPLAY") if merged.get("headless", default_headless): args.append("--headless=new") - if merged.get("sandbox", False) is False: + default_sandbox = not sys.platform.startswith("linux") + if merged.get("sandbox", default_sandbox) is False: args.append("--no-sandbox") args.extend(list(merged.get("args") or [])) args.extend(list(merged.get("extra_args") or [])) @@ -85,6 +87,8 @@ def launch(self, options: BrowserLaunchOptions | None = None) -> LaunchedBrowser child_read, parent_write = os.pipe() parent_read = _move_fd_if_needed(parent_read, {3, 4}) parent_write = _move_fd_if_needed(parent_write, {3, 4}) + child_read = _move_fd_if_needed(child_read, {3, 4}) + child_write = _move_fd_if_needed(child_write, {3, 4}) process = _spawn_chrome_with_pipe_fds(executable_path, args, child_read, child_write) os.close(child_read) os.close(child_write) @@ -93,10 +97,19 @@ def launch(self, options: BrowserLaunchOptions | None = None) -> LaunchedBrowser try: _wait_for_pipe_ready(pipe_read, pipe_write, int(merged.get("chrome_ready_timeout_ms") or DEFAULT_CHROME_READY_TIMEOUT_MS)) loopback_cdp_url = ( - _wait_for_cdp_websocket_url( - f"http://127.0.0.1:{port}", - int(merged.get("chrome_ready_timeout_ms") or DEFAULT_CHROME_READY_TIMEOUT_MS), - int(merged.get("chrome_ready_poll_interval_ms") or DEFAULT_CHROME_READY_POLL_INTERVAL_MS), + ( + _wait_for_browser_selected_cdp_websocket_url( + str(profile_dir), + int(merged.get("chrome_ready_timeout_ms") or DEFAULT_CHROME_READY_TIMEOUT_MS), + int(merged.get("chrome_ready_poll_interval_ms") or DEFAULT_CHROME_READY_POLL_INTERVAL_MS), + process, + ) + if port == 0 + else _wait_for_cdp_websocket_url( + f"http://127.0.0.1:{port}", + int(merged.get("chrome_ready_timeout_ms") or DEFAULT_CHROME_READY_TIMEOUT_MS), + int(merged.get("chrome_ready_poll_interval_ms") or DEFAULT_CHROME_READY_POLL_INTERVAL_MS), + ) ) if port is not None else None @@ -130,11 +143,22 @@ def launch(self, options: BrowserLaunchOptions | None = None) -> LaunchedBrowser stderr=subprocess.DEVNULL, start_new_session=not sys.platform.startswith("win"), ) - cdp_url = f"http://127.0.0.1:{port}" timeout_s = int(merged.get("chrome_ready_timeout_ms") or DEFAULT_CHROME_READY_TIMEOUT_MS) / 1000 poll_s = int(merged.get("chrome_ready_poll_interval_ms") or DEFAULT_CHROME_READY_POLL_INTERVAL_MS) / 1000 deadline = time.time() + timeout_s while time.time() < deadline: + exit_code = process.poll() + if exit_code is not None: + _close(process, temp_profile_dir, cleanup_profile_dir=cleanup_profile_dir) + raise RuntimeError(f"Chrome exited before CDP became ready (exit={exit_code}).") + if port == 0: + active_port = _read_devtools_active_port(str(profile_dir)) + if active_port is None: + time.sleep(poll_s) + continue + cdp_url = f"http://127.0.0.1:{active_port}" + else: + cdp_url = f"http://127.0.0.1:{port}" try: with urllib.request.urlopen(f"{cdp_url}/json/version", timeout=0.5) as response: version = json.loads(response.read()) @@ -149,7 +173,7 @@ def launch(self, options: BrowserLaunchOptions | None = None) -> LaunchedBrowser except Exception: time.sleep(poll_s) _close(process, temp_profile_dir, cleanup_profile_dir=cleanup_profile_dir) - raise RuntimeError(f"Chrome at {cdp_url} did not become ready within {timeout_s}s") + raise RuntimeError(f"Chrome did not become ready within {timeout_s}s") def _newest_first(candidates: list[str]) -> list[str]: @@ -205,77 +229,41 @@ def _candidate_paths() -> list[str]: str(local_app_data / "Google/Chrome/Application/chrome.exe"), ] else: + chromium = ["/usr/bin/chromium", "/usr/bin/chromium-browser"] canary = ["/usr/bin/google-chrome-canary", "/usr/bin/google-chrome-unstable", "/opt/google/chrome-unstable/chrome"] stock = ["/usr/bin/google-chrome-stable", "/usr/bin/google-chrome", "/opt/google/chrome/chrome"] - return [*_chrome_for_testing_candidates(), *canary, *stock] + return [*chromium, *canary, *_chrome_for_testing_candidates(), *stock] + return [*canary, *_chrome_for_testing_candidates(), *stock] def _move_fd_if_needed(fd: int, reserved: set[int]) -> int: if fd not in reserved: return fd moved = os.dup(fd) + while moved in reserved: + next_fd = os.dup(fd) + os.close(moved) + moved = next_fd os.close(fd) return moved -class _SpawnedProcess: - def __init__(self, pid: int) -> None: - self.pid = pid - self.returncode: int | None = None +def _spawn_chrome_with_pipe_fds(executable_path: str, args: list[str], child_read: int, child_write: int) -> _ChromeProcess: + def map_pipe_fds() -> None: + os.dup2(child_read, 3) + os.dup2(child_write, 4) + os.close(child_read) + os.close(child_write) - def terminate(self) -> None: - self._signal(signal.SIGTERM) - - def kill(self) -> None: - self._signal(signal.SIGKILL) - - def wait(self, timeout: float | None = None) -> int: - deadline = None if timeout is None else time.monotonic() + timeout - while True: - try: - waited_pid, status = os.waitpid(self.pid, os.WNOHANG) - except ChildProcessError: - if self.returncode is None: - self.returncode = 0 - return self.returncode - if waited_pid == self.pid: - self.returncode = os.waitstatus_to_exitcode(status) - return self.returncode - if deadline is not None and time.monotonic() >= deadline: - assert timeout is not None - raise subprocess.TimeoutExpired([str(self.pid)], timeout) - time.sleep(0.05) - - def _signal(self, sig: int) -> None: - if self.returncode is not None: - return - try: - os.kill(self.pid, sig) - except ProcessLookupError: - self.returncode = 0 - - -def _spawn_chrome_with_pipe_fds(executable_path: str, args: list[str], child_read: int, child_write: int) -> _SpawnedProcess: - if not hasattr(os, "posix_spawn"): - raise RuntimeError("remote_debugging='pipe' requires os.posix_spawn support in the Python client.") - os.set_inheritable(child_read, True) - os.set_inheritable(child_write, True) - devnull = os.open(os.devnull, os.O_RDWR) - os.set_inheritable(devnull, True) - file_actions: list[tuple[int, int] | tuple[int, int, int]] = [ - (os.POSIX_SPAWN_DUP2, devnull, 0), - (os.POSIX_SPAWN_DUP2, devnull, 1), - (os.POSIX_SPAWN_DUP2, devnull, 2), - (os.POSIX_SPAWN_DUP2, child_read, 3), - (os.POSIX_SPAWN_DUP2, child_write, 4), - ] - for fd in {devnull, child_read, child_write} - {0, 1, 2, 3, 4}: - file_actions.append((os.POSIX_SPAWN_CLOSE, fd)) - try: - pid = os.posix_spawn(executable_path, [executable_path, *args], os.environ, file_actions=file_actions) - return _SpawnedProcess(pid) - finally: - os.close(devnull) + return subprocess.Popen( + [executable_path, *args], + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + close_fds=sys.platform.startswith("win"), + preexec_fn=None if sys.platform.startswith("win") else map_pipe_fds, + start_new_session=not sys.platform.startswith("win"), + ) class _ChromeProcess(Protocol): @@ -287,6 +275,8 @@ def kill(self) -> None: ... def wait(self, timeout: float | None = None) -> int | None: ... + def poll(self) -> int | None: ... + def _wait_for_pipe_ready(pipe_read, pipe_write, timeout_ms: int) -> None: ready_id = 1 @@ -320,6 +310,7 @@ def _wait_for_pipe_ready(pipe_read, pipe_write, timeout_ms: int) -> None: def _wait_for_cdp_websocket_url(cdp_url: str, timeout_ms: int, poll_interval_ms: int) -> str: deadline = time.time() + timeout_ms / 1000 poll_s = poll_interval_ms / 1000 + last_error: Exception | None = None while time.time() < deadline: try: with urllib.request.urlopen(f"{cdp_url}/json/version", timeout=0.5) as response: @@ -327,12 +318,55 @@ def _wait_for_cdp_websocket_url(cdp_url: str, timeout_ms: int, poll_interval_ms: websocket_url = version.get("webSocketDebuggerUrl") if websocket_url: return str(websocket_url) - except Exception: - pass + except Exception as err: + last_error = err time.sleep(poll_s) + if last_error is not None: + raise RuntimeError(f"Chrome at {cdp_url} did not expose a WebSocket CDP URL within {timeout_ms}ms: {last_error}") raise RuntimeError(f"Chrome at {cdp_url} did not expose a WebSocket CDP URL within {timeout_ms}ms") +def _read_devtools_active_port(profile_dir: str) -> int | None: + active_port_path = Path(profile_dir) / "DevToolsActivePort" + try: + raw_port, websocket_path, *_ = active_port_path.read_text().strip().splitlines() + except FileNotFoundError: + return None + except ValueError: + return None + if not websocket_path: + return None + port = int(raw_port) + if port <= 0: + raise RuntimeError(f"Invalid DevToolsActivePort port: {raw_port}") + return port + + +def _wait_for_browser_selected_cdp_websocket_url( + profile_dir: str, + timeout_ms: int, + poll_interval_ms: int, + process: _ChromeProcess, +) -> str: + deadline = time.time() + timeout_ms / 1000 + poll_s = poll_interval_ms / 1000 + last_error: Exception | None = None + while time.time() < deadline: + exit_code = process.poll() + if exit_code is not None: + raise RuntimeError(f"Chrome exited before CDP became ready (exit={exit_code}).") + active_port = _read_devtools_active_port(profile_dir) + if active_port is not None: + try: + return _wait_for_cdp_websocket_url(f"http://127.0.0.1:{active_port}", poll_interval_ms, poll_interval_ms) + except Exception as err: + last_error = err + time.sleep(poll_s) + if last_error is not None: + raise RuntimeError(f"Chrome did not expose DevToolsActivePort from {profile_dir} within {timeout_ms}ms: {last_error}") + raise RuntimeError(f"Chrome did not expose DevToolsActivePort from {profile_dir} within {timeout_ms}ms") + + def _close( process: _ChromeProcess, temp_profile_dir: tempfile.TemporaryDirectory[str] | None, diff --git a/python/modcdp/translate/translate.py b/python/modcdp/translate/translate.py index 06bbde5..648ba16 100644 --- a/python/modcdp/translate/translate.py +++ b/python/modcdp/translate/translate.py @@ -152,10 +152,13 @@ def _wrap_modcdp_add_middleware(params: ProtocolParams) -> RuntimeCallFunctionOn def _wrap_custom_command(method: str, params: ProtocolParams, session_id: str) -> RuntimeCallFunctionOnParams: - return _call_function_params( - "async function() { return JSON.stringify(await globalThis.ModCDP.handleCommand(" - f"{json.dumps(method)}, {json.dumps(params)}, {json.dumps(session_id)})); }}" + runtime_params = _call_function_params( + "async function(method, paramsJson, cdpSessionId) { " + "return JSON.stringify(await globalThis.ModCDP.handleCommand(method, JSON.parse(paramsJson), cdpSessionId)); " + "}" ) + runtime_params["arguments"] = [{"value": method}, {"value": json.dumps(params)}, {"value": session_id}] + return runtime_params def _wrap_service_worker_command( diff --git a/python/modcdp/transport/ReverseWebSocketUpstreamTransport.py b/python/modcdp/transport/ReverseWebSocketUpstreamTransport.py index 0dffdd6..0fb940b 100644 --- a/python/modcdp/transport/ReverseWebSocketUpstreamTransport.py +++ b/python/modcdp/transport/ReverseWebSocketUpstreamTransport.py @@ -49,7 +49,7 @@ def update(self, config: dict[str, Any] | None = None) -> "ReverseWebSocketUpstr return self def getInjectorConfig(self) -> dict[str, Any]: - return {"upstream_reversews_url": self.url} + return {} def _setBind(self, bind: str) -> None: parsed = urlparse(bind if "://" in bind else f"ws://{bind}") diff --git a/python/pyproject.toml b/python/pyproject.toml index b467f44..b2f0e3d 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "modcdp" -version = "0.0.13" +version = "0.0.19" description = "Python client for ModCDP." readme = "README.md" requires-python = ">=3.11" diff --git a/python/tests/test_AutoSessionRouter.py b/python/tests/test_AutoSessionRouter.py index 177a2fd..0b9cd57 100644 --- a/python/tests/test_AutoSessionRouter.py +++ b/python/tests/test_AutoSessionRouter.py @@ -62,7 +62,7 @@ def test_bounds_detached_session_guards_and_clears_them_when_session_reattaches( self.assertEqual(router.execution_contexts[recent_session_id], 43) def test_tracks_real_target_sessions_and_execution_contexts(self) -> None: - chrome = LocalBrowserLauncher({"headless": True, "sandbox": False}).launch() + chrome = LocalBrowserLauncher({"headless": True}).launch() ws = create_connection(str(chrome["cdp_url"]), timeout=10) lock = threading.Lock() next_id = 0 @@ -86,7 +86,7 @@ def send(method: str, params: dict[str, Any] | None = None, session_id: str | No result = response.get("result") return result if isinstance(result, dict) else {} - router = AutoSessionRouter(send, lambda: 5_000) + router = AutoSessionRouter(send, lambda: 30_000) def reader() -> None: while not closed: @@ -121,11 +121,11 @@ def reader() -> None: session_id = _wait_for(lambda: router.sessionIdForTarget(target_id)) context_result: Queue[int | BaseException] = Queue() threading.Thread( - target=lambda: _put_result(context_result, lambda: router.waitForExecutionContext(session_id, 5_000)), + target=lambda: _put_result(context_result, lambda: router.waitForExecutionContext(session_id, 30_000)), daemon=True, ).start() send("Runtime.enable", {}, session_id) - context_id = context_result.get(timeout=10) + context_id = context_result.get(timeout=35) if isinstance(context_id, BaseException): raise context_id self.assertIsInstance(context_id, int) diff --git a/python/tests/test_BorrowedExtensionInjector.py b/python/tests/test_BorrowedExtensionInjector.py index 90b03f8..38baa76 100644 --- a/python/tests/test_BorrowedExtensionInjector.py +++ b/python/tests/test_BorrowedExtensionInjector.py @@ -4,7 +4,6 @@ from pathlib import Path from modcdp import ModCDPClient -from modcdp.launcher.LocalBrowserLauncher import LocalBrowserLauncher ROOT = Path(__file__).resolve().parents[2] @@ -13,32 +12,39 @@ class BorrowedExtensionInjectorTests(unittest.TestCase): def test_bootstraps_modcdp_inside_live_extension_service_worker(self) -> None: - chrome = LocalBrowserLauncher( - { - "headless": True, - "sandbox": False, - "extra_args": [f"--load-extension={EXTENSION_PATH}"], - } - ).launch() - cdp = ModCDPClient( - launcher={"launcher_mode": "remote"}, - upstream={"upstream_mode": "ws", "upstream_cdp_url": chrome["cdp_url"]}, + owner = ModCDPClient( + launcher={"launcher_mode": "local", "launcher_options": {"headless": True}}, + upstream={"upstream_mode": "ws"}, injector={ - "injector_mode": "borrow", + "injector_mode": "auto", + "injector_extension_path": str(EXTENSION_PATH), "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"], "injector_trust_service_worker_target": True, }, ) - try: - cdp.connect() - self.assertEqual(cdp.connect_timing.get("injector_source") if cdp.connect_timing else None, "borrowed") - self.assertEqual(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf") - target_infos = cdp.send("Target.getTargets")["targetInfos"] - self.assertGreater(len(target_infos), 0) + owner.connect() + cdp = ModCDPClient( + launcher={"launcher_mode": "remote"}, + upstream={"upstream_mode": "ws", "upstream_cdp_url": owner.cdp_url}, + injector={ + "injector_mode": "borrow", + "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"], + "injector_trust_service_worker_target": True, + }, + ) + try: + cdp.connect() + self.assertEqual(cdp.connect_timing.get("injector_source") if cdp.connect_timing else None, "borrowed") + self.assertEqual(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf") + self.assertEqual( + cdp.Mod.evaluate(expression="chrome.runtime.getURL('modcdp/service_worker.js')"), + "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js", + ) + finally: + cdp.close() finally: - cdp.close() - chrome["close"]() + owner.close() if __name__ == "__main__": diff --git a/python/tests/test_DiscoveredExtensionInjector.py b/python/tests/test_DiscoveredExtensionInjector.py index e62c709..46c71a7 100644 --- a/python/tests/test_DiscoveredExtensionInjector.py +++ b/python/tests/test_DiscoveredExtensionInjector.py @@ -1,169 +1,50 @@ from __future__ import annotations -import json -import shutil -import tempfile import unittest from pathlib import Path -from typing import Any, cast from modcdp import ModCDPClient -from modcdp.injector.DiscoveredExtensionInjector import DiscoveredExtensionInjector -from modcdp.launcher.LocalBrowserLauncher import LocalBrowserLauncher ROOT = Path(__file__).resolve().parents[2] EXTENSION_PATH = ROOT / "dist" / "extension" -CUSTOM_EXTENSION_ID = "hhklgmbgnbeghnjidampacgmgnhelifg" -CUSTOM_EXTENSION_PUBLIC_KEY = ( - "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzG1LUbtH0aHMKjTAUeT0saY8xfnRNENctFJme3C1qnsqT7PAXMxJC4nT7tBZy2gEGRirBb3zIZ3OyAu9a0QR8lTLupDp4qHWOhQ7dl9ZjxjQdYa4Gby0xuXLdQrJIxDbmuv+UVJvYa8vRTwQB8koygbzDDDP5/YiB6mc0hbh8XBb82Ossy7T280k8280o/rS0CXdioUraCHj58PDhfxbs18TBcYfOjuRqua9J2oddxobtGehSD0gDtbvn2IWDtRajOlgZZyuS1vLoSR7C1ulFzpRSYPEMhI2x+wphut7E3QImyJ577YeULVGpt988FcixOou7udjx3/IUWjpq8046wIDAQAB" -) class DiscoveredExtensionInjectorTests(unittest.TestCase): - def test_preserves_explicit_zero_probe_timeout(self) -> None: - class ProbeTimeoutInjector(DiscoveredExtensionInjector): - def __init__(self) -> None: - super().__init__( - { - "injector_trust_service_worker_target": True, - "injector_service_worker_probe_timeout_ms": 0, - "injector_require_service_worker_target": False, - } - ) - self.waits: list[tuple[int, bool]] = [] - - def _discoverReadyServiceWorker(self, *, matched_only: bool = False): # type: ignore[override] - return None - - def _wakeConfiguredExtension(self): # type: ignore[override] - return None - - def _waitForReadyServiceWorker(self, timeout_ms: int, matched_only: bool = False): # type: ignore[override] - self.waits.append((timeout_ms, matched_only)) - return None - - injector = ProbeTimeoutInjector() - self.assertIsNone(injector.inject()) - self.assertEqual(injector.waits, [(0, True)]) - - def test_preserves_explicit_zero_ready_timeout(self) -> None: - class ReadyTimeoutInjector(DiscoveredExtensionInjector): - def __init__(self) -> None: - super().__init__( - { - "injector_service_worker_ready_timeout_ms": 0, - "injector_require_service_worker_target": True, - } - ) - self.waits: list[tuple[int, bool]] = [] - - def _discoverReadyServiceWorker(self, *, matched_only: bool = False): # type: ignore[override] - return None - - def _wakeConfiguredExtension(self): # type: ignore[override] - return None - - def _waitForReadyServiceWorker(self, timeout_ms: int, matched_only: bool = False): # type: ignore[override] - self.waits.append((timeout_ms, matched_only)) - return None - - injector = ReadyTimeoutInjector() - with self.assertRaisesRegex(RuntimeError, "Required ModCDP service worker target was not visible"): - injector.inject() - self.assertEqual(injector.waits, [(0, False)]) - def test_attaches_to_already_loaded_real_modcdp_extension(self) -> None: - chrome = LocalBrowserLauncher( - { - "headless": True, - "sandbox": False, - "extra_args": [f"--load-extension={EXTENSION_PATH}"], - } - ).launch() - cdp = ModCDPClient( - launcher={"launcher_mode": "remote"}, - upstream={"upstream_mode": "ws", "upstream_cdp_url": chrome["cdp_url"]}, + owner = ModCDPClient( + launcher={"launcher_mode": "local", "launcher_options": {"headless": True}}, + upstream={"upstream_mode": "ws"}, injector={ - "injector_mode": "discover", + "injector_mode": "auto", + "injector_extension_path": str(EXTENSION_PATH), "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"], "injector_trust_service_worker_target": True, }, ) - try: - cdp.connect() - self.assertEqual(cdp.connect_timing.get("injector_source") if cdp.connect_timing else None, "discovered") - self.assertEqual(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf") - self.assertEqual( - cdp.Mod.evaluate(expression="chrome.runtime.getURL('modcdp/service_worker.js')"), - "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js", - ) - finally: - cdp.close() - chrome["close"]() - - def test_selects_configured_extension_when_multiple_modcdp_workers_exist(self) -> None: - with tempfile.TemporaryDirectory(prefix="modcdp-custom-extension-") as custom_extension_dir: - custom_extension_path = Path(custom_extension_dir) - shutil.copytree(EXTENSION_PATH, custom_extension_path, dirs_exist_ok=True) - manifest_path = custom_extension_path / "manifest.json" - manifest = json.loads(manifest_path.read_text()) - manifest["key"] = CUSTOM_EXTENSION_PUBLIC_KEY - manifest["name"] = "ModCDP Bridge Custom Test" - manifest_path.write_text(json.dumps(manifest, indent=2) + "\n") - - chrome = LocalBrowserLauncher( - { - "headless": True, - "sandbox": False, - "extra_args": [f"--load-extension={EXTENSION_PATH},{custom_extension_path}"], - } - ).launch() + owner.connect() cdp = ModCDPClient( launcher={"launcher_mode": "remote"}, - upstream={"upstream_mode": "ws", "upstream_cdp_url": chrome["cdp_url"]}, + upstream={"upstream_mode": "ws", "upstream_cdp_url": owner.cdp_url}, injector={ "injector_mode": "discover", - "injector_extension_id": CUSTOM_EXTENSION_ID, "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"], "injector_trust_service_worker_target": True, - "injector_require_service_worker_target": True, }, ) - try: cdp.connect() self.assertEqual(cdp.connect_timing.get("injector_source") if cdp.connect_timing else None, "discovered") - self.assertEqual(cdp.extension_id, CUSTOM_EXTENSION_ID) - self.assertEqual(cdp.Mod.evaluate(expression="chrome.runtime.id"), CUSTOM_EXTENSION_ID) - targets = cast(dict[str, Any], cdp.sendRaw("Target.getTargets")) - target_infos = targets.get("targetInfos") - self.assertIsInstance(target_infos, list) - target_infos = cast(list[Any], target_infos) - modcdp_workers = [ - target - for target in target_infos - if isinstance(target, dict) - and target.get("type") == "service_worker" - and str(target.get("url", "")).endswith("/modcdp/service_worker.js") - ] - self.assertTrue( - any( - target.get("url") == f"chrome-extension://{CUSTOM_EXTENSION_ID}/modcdp/service_worker.js" - for target in modcdp_workers - ) - ) - self.assertTrue( - any( - target.get("url") - == "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js" - for target in modcdp_workers - ) + self.assertEqual(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf") + self.assertEqual( + cdp.Mod.evaluate(expression="chrome.runtime.getURL('modcdp/service_worker.js')"), + "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js", ) finally: cdp.close() - chrome["close"]() + finally: + owner.close() if __name__ == "__main__": diff --git a/python/tests/test_ExtensionInjector.py b/python/tests/test_ExtensionInjector.py index 7d50e9a..f1ea3ec 100644 --- a/python/tests/test_ExtensionInjector.py +++ b/python/tests/test_ExtensionInjector.py @@ -1,202 +1,12 @@ from __future__ import annotations -import json -import time import unittest -from pathlib import Path -from typing import Any, cast -import modcdp -from modcdp.injector.ExtensionInjector import ExtensionInjector, ExtensionInjectorConfig -from modcdp.launcher.LocalBrowserLauncher import LocalBrowserLauncher -from modcdp.types import ProtocolParams, ProtocolResult -from websocket import create_connection - - -ROOT = Path(__file__).resolve().parents[2] -EXTENSION_PATH = ROOT / "dist" / "extension" - - -class ProbeExtensionInjector(ExtensionInjector): - def inject(self): - return self._waitForReadyServiceWorker( - self.options.get("injector_service_worker_ready_timeout_ms") or 60_000, - matched_only=True, - ) - - def sendTimed( - self, - method: str, - params: ProtocolParams | None, - session_id: str | None, - timeout_ms: int, - ) -> ProtocolResult: - return self._sendWithTimeout(method, params or {}, session_id, timeout_ms) +from modcdp.injector.ExtensionInjector import ExtensionInjector class ExtensionInjectorTests(unittest.TestCase): - def test_probes_real_extension_service_worker_with_shared_base_config(self) -> None: - chrome = LocalBrowserLauncher( - { - "headless": True, - "sandbox": False, - "extra_args": [f"--load-extension={EXTENSION_PATH}"], - } - ).launch() - ws = create_connection(cast(str, chrome["cdp_url"]), timeout=10) - next_id = 0 - - def send(method: str, params: ProtocolParams | None = None, session_id: str | None = None) -> ProtocolResult: - nonlocal next_id - next_id += 1 - message: dict[str, Any] = {"id": next_id, "method": method, "params": params or {}} - if session_id: - message["sessionId"] = session_id - ws.send(json.dumps(message)) - while True: - response = json.loads(ws.recv()) - if response.get("id") != next_id: - continue - error = response.get("error") - if isinstance(error, dict): - raise RuntimeError(str(error.get("message") or error)) - return cast(ProtocolResult, response.get("result") or {}) - - def attach_to_target(target_id: str) -> str | None: - result = send("Target.attachToTarget", {"targetId": target_id, "flatten": True}) - session_id = result.get("sessionId") - return session_id if isinstance(session_id, str) else None - - injector = ProbeExtensionInjector( - cast(ExtensionInjectorConfig, { - "send": send, - "attachToTarget": attach_to_target, - "injector_extension_id": "mdedooklbnfejodmnhmkdpkaedafkehf", - "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"], - "injector_trust_service_worker_target": True, - }) - ) - - try: - self.assertEqual(injector.getLauncherConfig(), {}) - self.assertEqual(injector.getTransportConfig(), {"injector_extension_id": "mdedooklbnfejodmnhmkdpkaedafkehf"}) - result = injector.inject() - self.assertEqual(result["extension_id"] if result else None, "mdedooklbnfejodmnhmkdpkaedafkehf") - self.assertTrue(str(result["url"] if result else "").endswith("/modcdp/service_worker.js")) - finally: - injector.close() - ws.close() - chrome["close"]() - - def test_keeps_modcdp_service_worker_alive_through_offscreen_keepalive(self) -> None: - chrome = LocalBrowserLauncher( - { - "headless": True, - "sandbox": False, - "extra_args": [f"--load-extension={EXTENSION_PATH}"], - } - ).launch() - ws = create_connection(cast(str, chrome["cdp_url"]), timeout=10) - next_id = 0 - - def send(method: str, params: ProtocolParams | None = None, session_id: str | None = None) -> ProtocolResult: - nonlocal next_id - next_id += 1 - message: dict[str, Any] = {"id": next_id, "method": method, "params": params or {}} - if session_id: - message["sessionId"] = session_id - ws.send(json.dumps(message)) - while True: - response = json.loads(ws.recv()) - if response.get("id") != next_id: - continue - error = response.get("error") - if isinstance(error, dict): - raise RuntimeError(str(error.get("message") or error)) - return cast(ProtocolResult, response.get("result") or {}) - - def attach_to_target(target_id: str) -> str | None: - result = send("Target.attachToTarget", {"targetId": target_id, "flatten": True}) - session_id = result.get("sessionId") - return session_id if isinstance(session_id, str) else None - - injector = ProbeExtensionInjector( - cast(ExtensionInjectorConfig, { - "send": send, - "attachToTarget": attach_to_target, - "injector_extension_id": "mdedooklbnfejodmnhmkdpkaedafkehf", - "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"], - "injector_trust_service_worker_target": True, - }) - ) - - try: - result = injector.inject() - self.assertEqual(result["extension_id"] if result else None, "mdedooklbnfejodmnhmkdpkaedafkehf") - session_id = result["session_id"] if result else None - self.assertIsInstance(session_id, str) - - value: list[Any] = [] - for _ in range(50): - contexts = send( - "Runtime.evaluate", - { - "expression": ( - "chrome.runtime.getContexts({}).then((contexts) => contexts.map((context) => " - "({ type: context.contextType, url: context.documentUrl || context.origin || '' })))" - ), - "awaitPromise": True, - "returnByValue": True, - }, - cast(str, session_id), - ) - raw_value = cast(dict[str, Any], contexts.get("result") or {}).get("value") - value = raw_value if isinstance(raw_value, list) else [] - if any( - isinstance(context, dict) - and context.get("type") == "OFFSCREEN_DOCUMENT" - and context.get("url") - == "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/offscreen/keepalive.html" - for context in value - ): - break - time.sleep(0.1) - self.assertTrue( - any( - isinstance(context, dict) - and context.get("type") == "OFFSCREEN_DOCUMENT" - and context.get("url") - == "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/offscreen/keepalive.html" - for context in value - ) - ) - - time.sleep(3) - targets = cast(dict[str, Any], send("Target.getTargets")) - target_infos = targets.get("targetInfos") - self.assertIsInstance(target_infos, list) - target_infos = cast(list[Any], target_infos) - self.assertTrue( - any( - isinstance(target, dict) - and target.get("type") == "service_worker" - and target.get("url") - == "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js" - for target in target_infos - ) - ) - version = send( - "Runtime.evaluate", - {"expression": "globalThis.ModCDP?.__ModCDPServerVersion", "returnByValue": True}, - cast(str, session_id), - ) - self.assertEqual(cast(dict[str, Any], version.get("result") or {}).get("value"), 2) - finally: - injector.close() - ws.close() - chrome["close"]() - - def test_owns_shared_injector_config_and_runtime_transport_config(self) -> None: + def test_owns_shared_injector_config(self) -> None: injector = ExtensionInjector( { "injector_extension_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", @@ -215,114 +25,19 @@ def test_owns_shared_injector_config_and_runtime_transport_config(self) -> None: } ) ) - - with self.assertRaisesRegex(NotImplementedError, "ExtensionInjector.inject is not implemented"): - injector.inject() - - def test_send_with_timeout_enforces_cdp_send_timeout(self) -> None: - chrome = LocalBrowserLauncher({"headless": True, "sandbox": False}).launch() - ws = create_connection(cast(str, chrome["cdp_url"]), timeout=10) - next_id = 0 - - def send(method: str, params: ProtocolParams | None = None, session_id: str | None = None) -> ProtocolResult: - nonlocal next_id - next_id += 1 - message: dict[str, Any] = {"id": next_id, "method": method, "params": params or {}} - if session_id: - message["sessionId"] = session_id - ws.send(json.dumps(message)) - while True: - response = json.loads(ws.recv()) - if response.get("id") != next_id: - continue - error = response.get("error") - if isinstance(error, dict): - raise RuntimeError(str(error.get("message") or error)) - return cast(ProtocolResult, response.get("result") or {}) - - injector = ProbeExtensionInjector(cast(ExtensionInjectorConfig, {"send": send})) - target_id: str | None = None - - try: - created = send("Target.createTarget", {"url": "about:blank#modcdp-timeout"}) - target_id = cast(str, created["targetId"]) - attached = send("Target.attachToTarget", {"targetId": target_id, "flatten": True}) - session_id = cast(str, attached["sessionId"]) - send("Runtime.enable", {}, session_id) - with self.assertRaisesRegex(TimeoutError, r"Runtime\.evaluate timed out after 5ms"): - injector.sendTimed( - "Runtime.evaluate", - {"expression": "new Promise(() => {})", "awaitPromise": True}, - session_id, - 5, - ) - finally: - if target_id: - try: - send("Target.closeTarget", {"targetId": target_id}) - except Exception: - pass - injector.close() - ws.close() - chrome["close"]() - - def test_wakes_configured_extension_with_hidden_background_target(self) -> None: - chrome = LocalBrowserLauncher( - { - "headless": True, - "sandbox": False, - "extra_args": [f"--load-extension={EXTENSION_PATH}"], - } - ).launch() - ws = create_connection(cast(str, chrome["cdp_url"]), timeout=10) - next_id = 0 - - def send(method: str, params: ProtocolParams | None = None, _session_id: str | None = None) -> ProtocolResult: - nonlocal next_id - next_id += 1 - message: dict[str, Any] = {"id": next_id, "method": method, "params": params or {}} - ws.send(json.dumps(message)) - while True: - response = json.loads(ws.recv()) - if response.get("id") != next_id: - continue - error = response.get("error") - if isinstance(error, dict): - raise RuntimeError(str(error.get("message") or error)) - return cast(ProtocolResult, response.get("result") or {}) - - injector = ProbeExtensionInjector( - cast(ExtensionInjectorConfig, { - "injector_extension_id": "mdedooklbnfejodmnhmkdpkaedafkehf", - "send": send, - }) - ) - - try: - self.assertTrue(injector._wakeConfiguredExtension()) - targets = cast(dict[str, Any], send("Target.getTargets")) - target_infos = targets.get("targetInfos") - self.assertIsInstance(target_infos, list) - target_infos = cast(list[Any], target_infos) - self.assertTrue( - any( - isinstance(target, dict) - and target.get("url") == "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/wake.html" - for target in target_infos - ) + self.assertFalse( + injector._serviceWorkerTargetMatches( + { + "targetId": "target-1", + "type": "service_worker", + "url": "chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/background.js", + } ) - finally: - injector.close() - ws.close() - chrome["close"]() + ) - def test_package_exports_all_injector_classes(self) -> None: - self.assertIs(modcdp.ExtensionInjector, ExtensionInjector) - self.assertEqual(modcdp.DiscoveredExtensionInjector.__name__, "DiscoveredExtensionInjector") - self.assertEqual(modcdp.LocalBrowserLaunchExtensionInjector.__name__, "LocalBrowserLaunchExtensionInjector") - self.assertEqual(modcdp.BBBrowserExtensionInjector.__name__, "BBBrowserExtensionInjector") - self.assertEqual(modcdp.ExtensionsLoadUnpackedInjector.__name__, "ExtensionsLoadUnpackedInjector") - self.assertEqual(modcdp.BorrowedExtensionInjector.__name__, "BorrowedExtensionInjector") + def test_base_inject_reports_the_class_name(self) -> None: + with self.assertRaisesRegex(NotImplementedError, "ExtensionInjector.inject is not implemented"): + ExtensionInjector().inject() if __name__ == "__main__": diff --git a/python/tests/test_ExtensionsLoadUnpackedInjector.py b/python/tests/test_ExtensionsLoadUnpackedInjector.py index 5fd9d55..4a101d8 100644 --- a/python/tests/test_ExtensionsLoadUnpackedInjector.py +++ b/python/tests/test_ExtensionsLoadUnpackedInjector.py @@ -1,73 +1,25 @@ from __future__ import annotations -import json -import re import unittest from pathlib import Path -from typing import Any, cast +from typing import cast from modcdp.injector.ExtensionsLoadUnpackedInjector import ExtensionsLoadUnpackedInjector -from modcdp.launcher.LocalBrowserLauncher import LocalBrowserLauncher -from websocket import create_connection - - -ROOT = Path(__file__).resolve().parents[2] -EXTENSION_PATH = ROOT / "dist" / "extension" class ExtensionsLoadUnpackedInjectorTests(unittest.TestCase): - def test_prepares_default_packaged_extension_zip_when_path_is_omitted(self) -> None: + def test_prepares_default_packaged_extension_zip(self) -> None: injector = ExtensionsLoadUnpackedInjector() try: injector.prepare() unpacked_extension_path = injector.unpacked_extension_path self.assertIsInstance(unpacked_extension_path, str) unpacked_extension_path = cast(str, unpacked_extension_path) + self.assertIn("modcdp-extension-", unpacked_extension_path) self.assertTrue((Path(unpacked_extension_path) / "manifest.json").exists()) - self.assertTrue(str(injector.options.get("injector_extension_path", "")).endswith("extension.zip")) finally: injector.close() - def test_exercises_real_cdp_load_unpacked_path(self) -> None: - chrome = LocalBrowserLauncher({"headless": True, "sandbox": False}).launch() - ws = create_connection(cast(str, chrome["cdp_url"]), timeout=10) - next_id = 0 - - def send(method: str, params: dict[str, Any] | None = None, session_id: str | None = None) -> dict[str, Any]: - nonlocal next_id - next_id += 1 - message: dict[str, Any] = {"id": next_id, "method": method, "params": params or {}} - if session_id: - message["sessionId"] = session_id - ws.send(json.dumps(message)) - while True: - response = json.loads(ws.recv()) - if response.get("id") != next_id: - continue - error = response.get("error") - if isinstance(error, dict): - raise RuntimeError(str(error.get("message") or error)) - return cast(dict[str, Any], response.get("result") or {}) - - injector = ExtensionsLoadUnpackedInjector( - cast(Any, { - "send": send, - "injector_extension_path": str(EXTENSION_PATH), - }) - ) - try: - injector.prepare() - result = injector.inject() - self.assertIsNone(result) - self.assertIsNotNone(injector.last_error) - self.assertRegex( - str(injector.last_error), - re.compile(r"Method not available|Method.*not.*found|wasn't found", re.I), - ) - finally: - injector.close() - ws.close() - chrome["close"]() if __name__ == "__main__": unittest.main() diff --git a/python/tests/test_LocalBrowserLaunchExtensionInjector.py b/python/tests/test_LocalBrowserLaunchExtensionInjector.py index 4f33892..3252463 100644 --- a/python/tests/test_LocalBrowserLaunchExtensionInjector.py +++ b/python/tests/test_LocalBrowserLaunchExtensionInjector.py @@ -1,12 +1,14 @@ from __future__ import annotations import unittest +import time +import tempfile +import zipfile from pathlib import Path -from typing import cast +from typing import Any, cast from modcdp.injector.ExtensionInjector import DEFAULT_MODCDP_EXTENSION_ID from modcdp.injector.LocalBrowserLaunchExtensionInjector import LocalBrowserLaunchExtensionInjector -from modcdp.client.ModCDPClient import ModCDPClient ROOT = Path(__file__).resolve().parents[2] @@ -14,46 +16,35 @@ class LocalBrowserLaunchExtensionInjectorTests(unittest.TestCase): - def test_loads_real_extension_during_local_launch(self) -> None: - cdp = ModCDPClient( - launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}}, - upstream={"upstream_mode": "ws"}, - injector={ - "injector_mode": "inject", - "injector_extension_path": str(EXTENSION_PATH), - "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"], - "injector_trust_service_worker_target": True, - "injector_service_worker_probe_timeout_ms": 30_000, - }, - client={ - "client_cdp_send_timeout_ms": 30_000, - }, - ) + def test_rejects_zip_entries_outside_extraction_directory(self) -> None: + with tempfile.TemporaryDirectory(prefix="modcdp-bad-zip-") as temp_dir: + zip_path = Path(temp_dir) / "extension.zip" + with zipfile.ZipFile(zip_path, "w") as archive: + archive.writestr("../evil.txt", "evil") - try: - cdp.connect() - self.assertEqual(cdp.connect_timing.get("injector_source") if cdp.connect_timing else None, "local_launch") - self.assertEqual(cdp.extension_id, DEFAULT_MODCDP_EXTENSION_ID) - self.assertRegex(cdp.ext_session_id or "", r"^.+$") - self.assertEqual( - cdp.Mod.evaluate(expression="chrome.runtime.getURL('modcdp/service_worker.js')"), - f"chrome-extension://{DEFAULT_MODCDP_EXTENSION_ID}/modcdp/service_worker.js", - ) - finally: - cdp.close() + injector = LocalBrowserLaunchExtensionInjector({"injector_extension_path": str(zip_path)}) + try: + with self.assertRaisesRegex(RuntimeError, "escapes extension extraction directory"): + injector.prepare() + self.assertFalse((Path(temp_dir) / "evil.txt").exists()) + finally: + injector.close() - def test_prepares_launcher_config(self) -> None: + def test_prepares_unpacked_extension_directory_for_load_extension(self) -> None: injector = LocalBrowserLaunchExtensionInjector({"injector_extension_path": str(EXTENSION_PATH)}) try: injector.prepare() - extra_args = injector.getLauncherConfig().get("extra_args") or [] - self.assertEqual(len(extra_args), 1) - self.assertTrue(extra_args[0].startswith("--load-extension=")) + unpacked_extension_path = injector.unpacked_extension_path + self.assertIsInstance(unpacked_extension_path, str) + unpacked_extension_path = cast(str, unpacked_extension_path) + self.assertNotEqual(unpacked_extension_path, str(EXTENSION_PATH)) + self.assertTrue((Path(unpacked_extension_path) / "manifest.json").exists()) + self.assertEqual(injector.getLauncherConfig(), {"extra_args": [f"--load-extension={unpacked_extension_path}"]}) self.assertEqual(injector.options.get("injector_extension_id"), DEFAULT_MODCDP_EXTENSION_ID) finally: injector.close() - def test_prepares_default_packaged_extension_zip_when_path_is_omitted(self) -> None: + def test_prepares_default_extension_zip_for_load_extension(self) -> None: injector = LocalBrowserLaunchExtensionInjector() try: injector.prepare() @@ -61,13 +52,39 @@ def test_prepares_default_packaged_extension_zip_when_path_is_omitted(self) -> N self.assertIsInstance(unpacked_extension_path, str) unpacked_extension_path = cast(str, unpacked_extension_path) self.assertTrue((Path(unpacked_extension_path) / "manifest.json").exists()) - self.assertTrue(str(injector.options.get("injector_extension_path", "")).endswith("extension.zip")) - extra_args = injector.getLauncherConfig().get("extra_args") or [] - self.assertEqual(extra_args, [f"--load-extension={unpacked_extension_path}"]) + self.assertIn("modcdp-extension-", unpacked_extension_path) + self.assertEqual(injector.getLauncherConfig(), {"extra_args": [f"--load-extension={unpacked_extension_path}"]}) self.assertEqual(injector.options.get("injector_extension_id"), DEFAULT_MODCDP_EXTENSION_ID) finally: injector.close() + def test_returns_immediately_when_launched_extension_target_is_absent(self) -> None: + methods: list[str] = [] + + def send(method: str, params: dict[str, Any] | None = None, session_id: str | None = None) -> dict[str, Any]: + methods.append(method) + if method == "Target.getTargets": + return {"targetInfos": []} + raise RuntimeError(f"unexpected {method}") + + injector = LocalBrowserLaunchExtensionInjector( + cast(Any, { + "injector_extension_path": str(EXTENSION_PATH), + "injector_trust_service_worker_target": True, + "send": send, + }) + ) + try: + injector.prepare() + started_at = time.perf_counter() + result = injector.inject() + elapsed_ms = (time.perf_counter() - started_at) * 1000 + self.assertIsNone(result) + self.assertEqual(methods, ["Target.getTargets"]) + self.assertLess(elapsed_ms, 200) + finally: + injector.close() + if __name__ == "__main__": unittest.main() diff --git a/python/tests/test_LocalBrowserLauncher.py b/python/tests/test_LocalBrowserLauncher.py index 678c9ee..2816a22 100644 --- a/python/tests/test_LocalBrowserLauncher.py +++ b/python/tests/test_LocalBrowserLauncher.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import sys import tempfile import unittest from pathlib import Path @@ -15,12 +16,11 @@ def test_class_helpers_match_ts_surface(self) -> None: self.assertIsInstance(LocalBrowserLauncher.findChromeBinary(), str) self.assertIsInstance(LocalBrowserLauncher.freePort(), int) - def test_launches_real_browser_and_speaks_cdp(self) -> None: + def test_launches_real_browser_over_chosen_cdp_port_and_honors_launch_options(self) -> None: with tempfile.TemporaryDirectory(prefix="modcdp-python-local-profile-") as user_data_dir: chrome = LocalBrowserLauncher( { "headless": True, - "sandbox": False, "chrome_ready_timeout_ms": 45_000, "chrome_ready_poll_interval_ms": 50, } @@ -37,6 +37,16 @@ def test_launches_real_browser_and_speaks_cdp(self) -> None: self.assertEqual(version["id"], 1) self.assertIn("Chrome", version["result"]["product"]) self.assertIsInstance(version["result"]["protocolVersion"], str) + ws.send(json.dumps({"id": 2, "method": "SystemInfo.getInfo", "params": {}})) + system_info = json.loads(ws.recv()) + self.assertEqual(system_info["id"], 2) + command_line = system_info["result"]["commandLine"] + self.assertIsInstance(command_line, str) + self.assertIn("--window-size=900,700", command_line) + if sys.platform.startswith("linux"): + self.assertIn("--no-sandbox", command_line) + else: + self.assertNotIn("--no-sandbox", command_line) finally: ws.close() chrome["close"]() @@ -48,7 +58,6 @@ def test_cleanup_user_data_dir_removes_explicit_profile(self) -> None: chrome = LocalBrowserLauncher( { "headless": True, - "sandbox": False, "chrome_ready_timeout_ms": 45_000, } ).launch({"user_data_dir": user_data_dir, "cleanup_user_data_dir": True}) @@ -63,7 +72,6 @@ def test_launches_real_browser_over_remote_debugging_pipe(self) -> None: chrome = LocalBrowserLauncher( { "headless": True, - "sandbox": False, "remote_debugging": "pipe", "chrome_ready_timeout_ms": 45_000, } @@ -88,7 +96,6 @@ def test_launches_pipe_browser_with_auxiliary_loopback_only_when_requested(self) chrome = LocalBrowserLauncher( { "headless": True, - "sandbox": False, "remote_debugging": "pipe", "loopback_cdp": True, "chrome_ready_timeout_ms": 45_000, diff --git a/python/tests/test_ModCDPClient.py b/python/tests/test_ModCDPClient.py index fcbc54f..cd2060c 100644 --- a/python/tests/test_ModCDPClient.py +++ b/python/tests/test_ModCDPClient.py @@ -6,8 +6,6 @@ import time import unittest from collections.abc import Mapping -from contextlib import redirect_stderr -from io import StringIO from pathlib import Path from queue import Empty, Queue from typing import Any, cast @@ -17,6 +15,7 @@ from modcdp import ModCDPClient from modcdp.launcher.LocalBrowserLauncher import LocalBrowserLauncher from modcdp.types import JsonValue +from tests.test_ReverseWebSocketUpstreamTransport import reversews_test_browser_path HERE = Path(__file__).resolve().parent @@ -182,6 +181,22 @@ def test_defaults_launched_modcdp_server_upstreams_to_extension_auto(self) -> No self.assertEqual(attach_only.upstream_endpoint_kind, "modcdp_server") self.assertEqual(attach_only.injector["injector_mode"], "none") + def test_orders_local_auto_injection_as_launch_flag_then_load_unpacked_fallback(self) -> None: + cdp = ModCDPClient( + launcher={"launcher_mode": "local"}, + injector={"injector_mode": "auto"}, + ) + + self.assertEqual( + [type(injector).__name__ for injector in cdp._extension_injectors_for_config()], + [ + "LocalBrowserLaunchExtensionInjector", + "ExtensionsLoadUnpackedInjector", + "DiscoveredExtensionInjector", + "BorrowedExtensionInjector", + ], + ) + def test_rejects_unknown_component_modes_at_their_owning_factory_boundary(self) -> None: with self.assertRaisesRegex(RuntimeError, r"unknown upstream\.upstream_mode=bogus"): ModCDPClient(upstream={"upstream_mode": "bogus"})._upstream_transport() @@ -190,12 +205,13 @@ def test_rejects_unknown_component_modes_at_their_owning_factory_boundary(self) with self.assertRaisesRegex(RuntimeError, r"unknown injector\.injector_mode=bogus"): ModCDPClient(injector={"injector_mode": "bogus"})._extension_injectors_for_config() - def test_connects_with_local_launch_and_injector_chain(self) -> None: + def test_connects_with_local_launch_injector_chain(self) -> None: cdp = ModCDPClient( - launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}}, + launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "chrome_ready_timeout_ms": 60_000}}, upstream={"upstream_mode": "ws"}, injector={ - "injector_mode": "inject", + "injector_mode": "auto", + "injector_extension_path": str(EXTENSION_PATH), "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"], "injector_trust_service_worker_target": True, "injector_service_worker_probe_timeout_ms": 30_000, @@ -208,12 +224,29 @@ def test_connects_with_local_launch_and_injector_chain(self) -> None: try: cdp.connect() - self.assertEqual(cdp.connect_timing.get("injector_source") if cdp.connect_timing else None, "local_launch") + self.assertIn( + cdp.connect_timing.get("injector_source") if cdp.connect_timing else None, + ("discovered", "local_launch", "extensions_load_unpacked", "borrowed"), + ) self.assertEqual(cdp.extension_id, "mdedooklbnfejodmnhmkdpkaedafkehf") self.assertEqual( cdp.Mod.evaluate(expression="chrome.runtime.getURL('modcdp/service_worker.js')"), "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/modcdp/service_worker.js", ) + contexts = cdp.Mod.evaluate( + expression=( + "chrome.runtime.getContexts({}).then((contexts) => contexts.map((context) => " + "({ type: context.contextType, url: context.documentUrl || context.origin || '' })))" + ) + ) + self.assertTrue( + any( + isinstance(context, Mapping) + and context.get("type") == "OFFSCREEN_DOCUMENT" + and context.get("url") == "chrome-extension://mdedooklbnfejodmnhmkdpkaedafkehf/offscreen/keepalive.html" + for context in cast(list[Any], contexts) + ) + ) sent_at = int(time.time() * 1000) pong: Queue[Mapping[str, Any]] = Queue() @@ -251,7 +284,10 @@ def test_close_does_not_close_a_remote_browser_it_did_not_launch(self) -> None: chrome = LocalBrowserLauncher( { "headless": True, - "sandbox": False, + "chrome_ready_timeout_ms": 60_000, + # This test manually supplies --load-extension, so it intentionally uses + # the launch-flag browser path instead of relying on the client fallback. + "executable_path": reversews_test_browser_path(), "extra_args": [f"--load-extension={EXTENSION_PATH}"], } ).launch() @@ -260,10 +296,14 @@ def test_close_does_not_close_a_remote_browser_it_did_not_launch(self) -> None: launcher={"launcher_mode": "remote"}, upstream={"upstream_mode": "ws", "upstream_cdp_url": chrome["cdp_url"]}, injector={ - "injector_mode": "discover", + "injector_mode": "auto", + "injector_extension_path": str(EXTENSION_PATH), "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"], "injector_trust_service_worker_target": True, + "injector_service_worker_ready_timeout_ms": 30_000, + "injector_service_worker_probe_timeout_ms": 30_000, }, + client={"client_routes": {"*.*": "direct_cdp"}}, ) try: @@ -281,11 +321,17 @@ def test_close_does_not_close_a_remote_browser_it_did_not_launch(self) -> None: def test_close_keeps_injector_files_until_after_launched_browser_shutdown(self) -> None: cdp = ModCDPClient( - launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}}, - upstream={ - "upstream_mode": "reversews", - "upstream_reversews_wait_timeout_ms": 30_000, + launcher={ + "launcher_mode": "local", + "launcher_options": { + "headless": True, + # After explicit CHROME_PATH and CI /usr/bin/chromium, this test uses + # Chrome for Testing because Canary rejects --load-extension in this + # local launch injector path. + "executable_path": reversews_test_browser_path(), + }, }, + upstream={"upstream_mode": "ws"}, injector={ "injector_mode": "auto", "injector_extension_path": str(EXTENSION_PATH), @@ -332,7 +378,7 @@ def close_browser() -> None: def test_close_clears_top_level_connection_state(self) -> None: cdp = ModCDPClient( - launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}}, + launcher={"launcher_mode": "local", "launcher_options": {"headless": True}}, upstream={"upstream_mode": "ws"}, injector={ "injector_mode": "auto", @@ -351,7 +397,7 @@ def test_close_clears_top_level_connection_state(self) -> None: def test_generated_cdp_surface_exposes_direct_domain_commands(self) -> None: client = ModCDPClient( - launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}}, + launcher={"launcher_mode": "local", "launcher_options": {"headless": True}}, upstream={"upstream_mode": "ws"}, injector={ "injector_mode": "auto", @@ -404,7 +450,7 @@ async def run_awaited_calls() -> None: self.assertRegex(str(awaited_raw_result["targetId"]), r"^[A-F0-9]+$") client = ModCDPClient( - launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}}, + launcher={"launcher_mode": "local", "launcher_options": {"headless": True}}, upstream={"upstream_mode": "ws"}, injector={ "injector_mode": "auto", @@ -599,18 +645,17 @@ def test_custom_event_schema_validates_payload_before_handlers(self) -> None: payload: dict[str, JsonValue] = {"url": "https://example.com", "ready": True} self.assertEqual(client._validate_event_payload("Custom.ready", payload), payload) - with redirect_stderr(StringIO()): - self.assertIsNone(client._validate_event_payload("Custom.ready", {"url": "http://example.com", "ready": True})) - self.assertIsNone( - client._validate_event_payload("Custom.ready", {"url": "https://example.com", "ready": True, "x": 1}) - ) + with self.assertRaises(ValueError): + client._validate_event_payload("Custom.ready", {"url": "http://example.com", "ready": True}) + with self.assertRaises(ValueError): + client._validate_event_payload("Custom.ready", {"url": "https://example.com", "ready": True, "x": 1}) def test_scalar_event_schema_validates_value_payloads(self) -> None: client = ModCDPClient(custom_events=[{"name": "Custom.count", "event_schema": {"type": "integer", "minimum": 1}}]) self.assertEqual(client._validate_event_payload("Custom.count", {"value": 3}), {"value": 3}) - with redirect_stderr(StringIO()): - self.assertIsNone(client._validate_event_payload("Custom.count", {"value": 0})) + with self.assertRaises(ValueError): + client._validate_event_payload("Custom.count", {"value": 0}) if __name__ == "__main__": diff --git a/python/tests/test_ModCDPClientCustomFlatNamespace.py b/python/tests/test_ModCDPClientCustomFlatNamespace.py index 8bb8128..1872550 100644 --- a/python/tests/test_ModCDPClientCustomFlatNamespace.py +++ b/python/tests/test_ModCDPClientCustomFlatNamespace.py @@ -1,8 +1,6 @@ from __future__ import annotations import asyncio -from contextlib import redirect_stderr -from io import StringIO from queue import Queue import unittest from pathlib import Path @@ -27,7 +25,7 @@ class ResultSchema(BaseModel): client = ModCDPClient( launcher={ "launcher_mode": "local", - "launcher_options": {"headless": True, "sandbox": False}, + "launcher_options": {"headless": True}, }, upstream={"upstream_mode": "ws"}, injector={ @@ -68,7 +66,7 @@ class EventSchema(BaseModel): client = ModCDPClient( launcher={ "launcher_mode": "local", - "launcher_options": {"headless": True, "sandbox": False}, + "launcher_options": {"headless": True}, }, upstream={"upstream_mode": "ws"}, injector={ @@ -117,8 +115,8 @@ def test_schema_only_custom_event_registers_without_websocket(self) -> None: self.assertEqual(result, {"name": "Custom.schemaOnly", "registered": True}) self.assertEqual(client._validate_event_payload("Custom.schemaOnly", {"ok": True}), {"ok": True}) - with redirect_stderr(StringIO()): - self.assertIsNone(client._validate_event_payload("Custom.schemaOnly", {"ok": True, "extra": True})) + with self.assertRaises(ValueError): + client._validate_event_payload("Custom.schemaOnly", {"ok": True, "extra": True}) if __name__ == "__main__": diff --git a/python/tests/test_ModCDPClientRoutedDefaultOverrides.py b/python/tests/test_ModCDPClientRoutedDefaultOverrides.py index 68182b1..24db6c3 100644 --- a/python/tests/test_ModCDPClientRoutedDefaultOverrides.py +++ b/python/tests/test_ModCDPClientRoutedDefaultOverrides.py @@ -5,7 +5,7 @@ from typing import Any, cast import unittest -from modcdp import LocalBrowserLauncher, ModCDPClient +from modcdp import ModCDPClient HERE = Path(__file__).resolve().parent @@ -71,22 +71,28 @@ def target_infos_from_result(result: Any) -> list[dict[str, Any]]: class ModCDPClientRoutedDefaultOverridesTests(unittest.TestCase): def test_service_worker_routed_standard_cdp_commands_and_events_can_be_transformed(self) -> None: - chrome = LocalBrowserLauncher( - { - "headless": True, - "sandbox": False, - "extra_args": [f"--load-extension={EXTENSION_PATH}"], - } - ).launch() - cdp = ModCDPClient( - launcher={"launcher_mode": "remote"}, - upstream={"upstream_mode": "ws", "upstream_cdp_url": chrome["cdp_url"]}, + owner = ModCDPClient( + launcher={ + "launcher_mode": "local", + "launcher_options": {"headless": True}, + }, + upstream={"upstream_mode": "ws"}, injector={ "injector_mode": "auto", "injector_extension_path": str(EXTENSION_PATH), "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"], "injector_trust_service_worker_target": True, }, + ) + owner.connect() + cdp = ModCDPClient( + launcher={"launcher_mode": "remote"}, + upstream={"upstream_mode": "ws", "upstream_cdp_url": owner.cdp_url}, + injector={ + "injector_mode": "discover", + "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"], + "injector_trust_service_worker_target": True, + }, client={ "client_routes": { "Target.getTargets": "service_worker", @@ -94,17 +100,17 @@ def test_service_worker_routed_standard_cdp_commands_and_events_can_be_transform "Target.setDiscoverTargets": "service_worker", } }, - server={"server_loopback_cdp_url": chrome["cdp_url"], "server_routes": {"*.*": "loopback_cdp"}}, + server={"server_loopback_cdp_url": owner.cdp_url, "server_routes": {"*.*": "loopback_cdp"}}, ) try: cdp.connect() - self.assertEqual(cdp.cdp_url, chrome["cdp_url"]) + self.assertEqual(cdp.cdp_url, owner.cdp_url) self.assertIsNotNone(cdp.server) server = cast(dict[str, Any], cdp.server) - self.assertEqual(server["server_loopback_cdp_url"], chrome["cdp_url"]) + self.assertEqual(server["server_loopback_cdp_url"], owner.cdp_url) - raw_targets = cdp._send_message("Target.getTargets", {}) + raw_targets = cdp.send("Target.getTargets") raw_target_infos = target_infos_from_result(raw_targets) self.assertTrue(raw_target_infos) self.assertFalse(any("tabId" in target_info for target_info in raw_target_infos)) @@ -160,7 +166,7 @@ def on_target_created(params): except Exception: pass cdp.close() - chrome["close"]() + owner.close() if __name__ == "__main__": diff --git a/python/tests/test_ModCDPClient_protocol_validation.py b/python/tests/test_ModCDPClient_protocol_validation.py new file mode 100644 index 0000000..b672ea4 --- /dev/null +++ b/python/tests/test_ModCDPClient_protocol_validation.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import unittest +from typing import TYPE_CHECKING, assert_type + +from pydantic import BaseModel + +from modcdp import ModCDPClient +from modcdp.client.ModCDPClient import AwaitableValue +from modcdp.types.generated.cdp import RuntimeDomain, TargetDomain +from modcdp.types.generated.cdp import AwaitableDict +from modcdp.types.modcdp import JsonValue, ProtocolPayload + + +class CustomEchoParams(BaseModel): + text: str + + +class CustomEchoResult(BaseModel): + ok: bool + + +class CustomReadyEvent(BaseModel): + ok: bool + + +class ModCDPClientProtocolValidationTests(unittest.TestCase): + def test_protocol_validation_covers_native_methods_native_events_custom_methods_custom_events_and_native_overrides(self) -> None: + client = ModCDPClient() + + if TYPE_CHECKING: + runtime_result = client.Runtime.evaluate(expression="1 + 1", returnByValue=True) + assert_type(runtime_result, RuntimeDomain._EvaluateResult) + + def on_target_created(event: TargetDomain._TargetCreatedEvent) -> None: + assert_type(event.targetId, str | None) + + assert_type(client.on(client.Target.targetCreated, on_target_created), ModCDPClient) + assert_type( + client.Mod.addCustomCommand("Custom.echo", params_schema=CustomEchoParams, result_schema=CustomEchoResult), + AwaitableDict | AwaitableValue, + ) + assert_type(client.Mod.addCustomEvent("Custom.ready", event_schema=CustomReadyEvent), AwaitableDict | AwaitableValue) + assert_type( + client.Mod.addMiddleware( + name="Target.getTargets", + phase="response", + expression="async (value, next) => next(value)", + ), + AwaitableDict | AwaitableValue, + ) + + runtime_params = {"expression": "1 + 1", "returnByValue": True} + runtime_result_payload = {"result": {"type": "number", "value": 2, "description": "2"}} + native_target_info: dict[str, JsonValue] = { + "targetId": "target-1", + "type": "page", + "title": "Example", + "url": "https://example.com", + "attached": False, + "canAccessOpener": False, + } + native_event_payload: ProtocolPayload = {"targetInfo": native_target_info} + + self.assertEqual(client._validate_command_params("Runtime.evaluate", runtime_params), runtime_params) + self.assertEqual(client._validate_command_result("Runtime.evaluate", runtime_result_payload), runtime_result_payload) + self.assertEqual(client._validate_event_payload("Target.targetCreated", native_event_payload), native_event_payload) + with self.assertRaises(ValueError): + client._validate_command_params("Runtime.evaluate", {}) + with self.assertRaises(ValueError): + client._validate_command_result("Runtime.evaluate", {}) + with self.assertRaises(ValueError): + client._validate_event_payload("Target.targetCreated", {}) + + client.Mod.addCustomCommand("Custom.echo", params_schema=CustomEchoParams, result_schema=CustomEchoResult) + client.Mod.addCustomEvent("Custom.ready", event_schema=CustomReadyEvent) + + self.assertEqual(client._validate_command_params("Custom.echo", {"text": "ok"}), {"text": "ok"}) + self.assertEqual(client._validate_command_result("Custom.echo", {"ok": True}), True) + self.assertEqual(client._validate_event_payload("Custom.ready", {"ok": True}), CustomReadyEvent(ok=True)) + with self.assertRaises(ValueError): + client._validate_command_params("Custom.echo", {"text": 1}) + with self.assertRaises(ValueError): + client._validate_command_result("Custom.echo", {"ok": "yes"}) + with self.assertRaises(ValueError): + client._validate_event_payload("Custom.ready", {"ok": "yes"}) + + client.Mod.addCustomCommand( + "Target.getTargets", + result_schema={ + "type": "object", + "properties": { + "targetInfos": { + "type": "array", + "items": { + "type": "object", + "properties": { + "targetId": {"type": "string"}, + "type": {"type": "string"}, + "title": {"type": "string"}, + "url": {"type": "string"}, + "attached": {"type": "boolean"}, + "canAccessOpener": {"type": "boolean"}, + "tabId": {"type": "integer"}, + }, + "required": ["targetId", "type", "title", "url", "attached", "canAccessOpener"], + "additionalProperties": True, + }, + } + }, + "required": ["targetInfos"], + "additionalProperties": True, + }, + ) + client.Mod.addCustomEvent("Target.targetCreated") + + extended_target_info = {**native_target_info, "tabId": 7} + self.assertEqual(client._validate_command_result("Target.getTargets", {"targetInfos": [extended_target_info]}), {"targetInfos": [extended_target_info]}) + self.assertEqual( + client._validate_event_payload("Target.targetCreated", {"targetInfo": extended_target_info}), + {"targetInfo": extended_target_info}, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/python/tests/test_NativeMessagingUpstreamTransport.py b/python/tests/test_NativeMessagingUpstreamTransport.py index 4c5f3e4..f11bdc9 100644 --- a/python/tests/test_NativeMessagingUpstreamTransport.py +++ b/python/tests/test_NativeMessagingUpstreamTransport.py @@ -3,14 +3,22 @@ import threading import time import unittest +import glob import os +import re import socket +import sys +import tempfile from pathlib import Path from queue import Queue from modcdp import ModCDPClient from modcdp.transport.NativeMessagingUpstreamTransport import NativeMessagingUpstreamTransport +ROOT = Path(__file__).resolve().parents[2] +EXTENSION_PATH = ROOT / "dist" / "extension" +NATIVE_MESSAGING_TEST_BROWSER_PATH: str | None = None + class NativeMessagingUpstreamTransportTests(unittest.TestCase): def test_config_owns_manifest_host_wait_timeout_loopback_and_injector_config(self) -> None: @@ -157,11 +165,24 @@ def wait_for_peer() -> None: def test_installs_launch_profile_native_host_manifest_and_connects_to_real_extension(self) -> None: upstream_nativemessaging_host_name = "com.modcdp.bridge" + temp_profile_dir = tempfile.TemporaryDirectory(prefix="modcdp.native.") cdp = ModCDPClient( - launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}}, + launcher={ + "launcher_mode": "local", + "launcher_options": { + "headless": True, + "user_data_dir": temp_profile_dir.name, + "cleanup_user_data_dir": True, + # Native messaging is browser -> client only. After explicit CHROME_PATH + # and CI /usr/bin/chromium, this test uses Chrome for Testing because + # Canary rejects --load-extension in this local test path. + "executable_path": extension_launch_flag_test_browser_path(), + }, + }, upstream={"upstream_mode": "nativemessaging", "upstream_nativemessaging_host_name": upstream_nativemessaging_host_name}, injector={ "injector_mode": "auto", + "injector_extension_path": str(EXTENSION_PATH), "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"], "injector_trust_service_worker_target": True, }, @@ -174,8 +195,8 @@ def test_installs_launch_profile_native_host_manifest_and_connects_to_real_exten self.assertEqual(cdp.upstream_endpoint_kind, "modcdp_server") transport_url = cdp.transport.url if cdp.transport and cdp.transport.url else "" self.assertRegex(transport_url, rf"^native://{upstream_nativemessaging_host_name}@127\.0\.0\.1:\d+$") - profile_dir = cdp._launched_browser.get("profile_dir") if cdp._launched_browser else "" - self.assertTrue((Path(profile_dir) / "NativeMessagingHosts" / f"{upstream_nativemessaging_host_name}.json").exists()) + launched_profile_dir = cdp._launched_browser.get("profile_dir") if cdp._launched_browser else "" + self.assertTrue((Path(launched_profile_dir) / "NativeMessagingHosts" / f"{upstream_nativemessaging_host_name}.json").exists()) version = cdp.send("Browser.getVersion") self.assertIsInstance(version["product"], str) time.sleep(1.5) @@ -183,6 +204,7 @@ def test_installs_launch_profile_native_host_manifest_and_connects_to_real_exten self.assertIsInstance(second_version["product"], str) finally: cdp.close() + temp_profile_dir.cleanup() def _wait_until(predicate, timeout_s: float = 2.0) -> None: @@ -194,5 +216,53 @@ def _wait_until(predicate, timeout_s: float = 2.0) -> None: raise AssertionError("timed out waiting for condition") +def extension_launch_flag_test_browser_path() -> str: + global NATIVE_MESSAGING_TEST_BROWSER_PATH + if NATIVE_MESSAGING_TEST_BROWSER_PATH is not None: + return NATIVE_MESSAGING_TEST_BROWSER_PATH + explicit_candidates = [ + os.environ.get("CHROME_PATH"), + "/usr/bin/chromium" if sys.platform.startswith("linux") else None, + ] + for candidate in explicit_candidates: + if candidate and Path(candidate).exists(): + NATIVE_MESSAGING_TEST_BROWSER_PATH = candidate + return candidate + + home = Path.home() + if sys.platform == "darwin": + patterns = [ + str(home / "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"), + str(home / "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium"), + str(home / "Library/Caches/puppeteer/chrome/mac*-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"), + ] + elif sys.platform.startswith("win"): + local_app_data = Path(os.environ.get("LOCALAPPDATA") or home / "AppData/Local") + patterns = [ + str(local_app_data / "ms-playwright/chromium-*/chrome-win*/chrome.exe"), + str(home / ".cache/puppeteer/chrome/win*-*/chrome-win*/chrome.exe"), + ] + else: + patterns = [ + str(home / ".cache/ms-playwright/chromium-*/chrome-linux*/chrome"), + "/opt/pw-browsers/chromium-*/chrome-linux*/chrome", + str(home / ".cache/puppeteer/chrome/linux-*/chrome-linux*/chrome"), + ] + candidates = sorted( + {candidate for pattern in patterns for candidate in glob.glob(pattern)}, + key=lambda candidate: (_path_version(candidate), Path(candidate).stat().st_mtime), + reverse=True, + ) + if candidates: + NATIVE_MESSAGING_TEST_BROWSER_PATH = candidates[0] + return candidates[0] + raise RuntimeError("Native messaging tests require CHROME_PATH, /usr/bin/chromium, or Chrome for Testing.") + + +def _path_version(candidate: str) -> int: + numbers = [int(value) for value in re.findall(r"\d+", candidate)] + return max(numbers) if numbers else 0 + + if __name__ == "__main__": unittest.main() diff --git a/python/tests/test_PipeUpstreamTransport.py b/python/tests/test_PipeUpstreamTransport.py index 25f9553..6221984 100644 --- a/python/tests/test_PipeUpstreamTransport.py +++ b/python/tests/test_PipeUpstreamTransport.py @@ -2,10 +2,14 @@ import os import unittest +from pathlib import Path from modcdp import ModCDPClient from modcdp.transport.PipeUpstreamTransport import PipeUpstreamTransport +ROOT = Path(__file__).resolve().parents[2] +EXTENSION_PATH = ROOT / "dist" / "extension" + class PipeUpstreamTransportTests(unittest.TestCase): def test_constructor_update_launcher_config_and_unconnected_errors_match_transport_surface(self) -> None: @@ -19,7 +23,7 @@ def test_constructor_update_launcher_config_and_unconnected_errors_match_transpo with self.assertRaisesRegex(RuntimeError, r"upstream\.upstream_mode=pipe requires"): transport.connect() with self.assertRaisesRegex(RuntimeError, "CDP pipe is not connected"): - transport.send({"id": 1, "method": "Browser.getVersion"}) + transport.send({"id": 1, "method": "Runtime.evaluate"}) def test_resets_connection_state_after_pipe_closes(self) -> None: read_fd, read_writer_fd = os.pipe() @@ -34,24 +38,26 @@ def test_resets_connection_state_after_pipe_closes(self) -> None: try: transport.connect() - transport.send({"id": 1, "method": "Browser.getVersion", "params": {}}) + transport.send({"id": 1, "method": "Runtime.evaluate", "params": {"expression": "1"}}) pipe_read_writer.close() self.assertTrue(_wait_for(lambda: len(closed) == 1)) with self.assertRaisesRegex(RuntimeError, "CDP pipe is not connected"): - transport.send({"id": 2, "method": "Browser.getVersion", "params": {}}) + transport.send({"id": 2, "method": "Runtime.evaluate", "params": {"expression": "1"}}) finally: transport.close() pipe_write_reader.close() def test_launches_real_browser_and_uses_pid_scoped_pipe_url(self) -> None: cdp = ModCDPClient( - launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}}, + launcher={"launcher_mode": "local", "launcher_options": {"headless": True}}, upstream={"upstream_mode": "pipe"}, injector={ - "injector_mode": "auto", + "injector_mode": "inject", + "injector_extension_path": str(EXTENSION_PATH), "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"], "injector_trust_service_worker_target": True, }, + server={"server_routes": {"*.*": "chrome_debugger"}}, ) try: @@ -60,8 +66,12 @@ def test_launches_real_browser_and_uses_pid_scoped_pipe_url(self) -> None: self.assertEqual(cdp.upstream_endpoint_kind, "raw_cdp") self.assertRegex(cdp.cdp_url or "", r"^pipe://\d+$") self.assertEqual(cdp.transport.url if cdp.transport else None, cdp.cdp_url) - version = cdp.sendRaw("Browser.getVersion") - self.assertIsInstance(version["product"], str) + cdp.Mod.addCustomCommand( + "Custom.runtimeReadyState", + expression="async () => await cdp.send('Runtime.evaluate', { expression: 'document.readyState', returnByValue: true })", + ) + runtime = cdp.send("Custom.runtimeReadyState") + self.assertEqual(runtime["result"]["value"], "complete") finally: cdp.close() diff --git a/python/tests/test_RemoteBrowserLauncher.py b/python/tests/test_RemoteBrowserLauncher.py index 9962416..e4d60a5 100644 --- a/python/tests/test_RemoteBrowserLauncher.py +++ b/python/tests/test_RemoteBrowserLauncher.py @@ -17,7 +17,7 @@ def test_requires_upstream_cdp_url(self) -> None: def test_connects_to_real_browser_from_http_and_websocket_cdp_endpoints(self) -> None: port = LocalBrowserLauncher.freePort() local = LocalBrowserLauncher().launch( - {"port": port, "headless": True, "sandbox": False, "chrome_ready_timeout_ms": 45_000} + {"port": port, "headless": True, "chrome_ready_timeout_ms": 45_000} ) ws = None try: diff --git a/python/tests/test_ReverseWebSocketUpstreamTransport.py b/python/tests/test_ReverseWebSocketUpstreamTransport.py index 85a5f0e..734ad29 100644 --- a/python/tests/test_ReverseWebSocketUpstreamTransport.py +++ b/python/tests/test_ReverseWebSocketUpstreamTransport.py @@ -1,12 +1,15 @@ from __future__ import annotations import json +import glob import os +import re import socket import sys import threading import time import unittest +from pathlib import Path from queue import Queue from typing import cast @@ -15,15 +18,19 @@ from modcdp import ModCDPClient from modcdp.transport.ReverseWebSocketUpstreamTransport import ReverseWebSocketUpstreamTransport +ROOT = Path(__file__).resolve().parents[2] +EXTENSION_PATH = ROOT / "dist" / "extension" +REVERSEWS_TEST_BROWSER_PATH = None + class ReverseWebSocketUpstreamTransportTests(unittest.TestCase): - def test_config_owns_bind_updates_wait_timeout_and_injector_config(self) -> None: + def test_config_owns_bind_updates_and_wait_timeout(self) -> None: transport = ReverseWebSocketUpstreamTransport({ "upstream_reversews_bind": "127.0.0.1:29292", "upstream_reversews_wait_timeout_ms": 10, }) self.assertEqual(transport.url, "ws://127.0.0.1:29292") - self.assertEqual(transport.getInjectorConfig(), {"upstream_reversews_url": "ws://127.0.0.1:29292"}) + self.assertEqual(transport.getInjectorConfig(), {}) self.assertIs( transport.update({ "upstream_reversews_bind": "127.0.0.1:29293", @@ -32,7 +39,7 @@ def test_config_owns_bind_updates_wait_timeout_and_injector_config(self) -> None transport, ) self.assertEqual(transport.url, "ws://127.0.0.1:29293") - self.assertEqual(transport.getInjectorConfig(), {"upstream_reversews_url": "ws://127.0.0.1:29293"}) + self.assertEqual(transport.getInjectorConfig(), {}) with self.assertRaisesRegex(RuntimeError, "Timed out waiting 5ms"): transport.waitForPeer() @@ -134,19 +141,26 @@ def test_accepts_replacement_peer_after_disconnect(self) -> None: first_peer.close() transport.close() - def test_accepts_real_extension_reverse_connection_and_routes_cdp_through_loopback(self) -> None: + def test_accepts_real_extension_reverse_connection_and_routes_cdp_through_chrome_debugger(self) -> None: cdp = ModCDPClient( launcher={ "launcher_mode": "local", - "launcher_options": {"headless": sys.platform.startswith("linux") and not os.environ.get("DISPLAY"), "sandbox": False}, + "launcher_options": { + "headless": sys.platform.startswith("linux") and not os.environ.get("DISPLAY"), + # Reversews is browser -> client only. After explicit CHROME_PATH and + # CI /usr/bin/chromium, these tests use Chrome for Testing because + # Canary rejects --load-extension in this local test path. + "executable_path": reversews_test_browser_path(), + }, }, upstream={"upstream_mode": "reversews"}, injector={ "injector_mode": "auto", + "injector_extension_path": str(EXTENSION_PATH), "injector_service_worker_url_suffixes": ["/modcdp/service_worker.js"], "injector_trust_service_worker_target": True, + "injector_service_worker_probe_timeout_ms": 1_000, }, - server={"server_routes": {"*.*": "loopback_cdp"}}, ) try: @@ -155,15 +169,16 @@ def test_accepts_real_extension_reverse_connection_and_routes_cdp_through_loopba self.assertEqual(cdp.upstream_endpoint_kind, "modcdp_server") self.assertIsInstance(cdp.transport, ReverseWebSocketUpstreamTransport) transport = cast(ReverseWebSocketUpstreamTransport, cdp.transport) + self.assertEqual(transport.url, "ws://127.0.0.1:29292") self.assertEqual( transport.peer_info.get("extension_id") if transport.peer_info else None, "mdedooklbnfejodmnhmkdpkaedafkehf", ) - version = cdp.send("Browser.getVersion") - self.assertIsInstance(version["product"], str) + evaluated = cdp.send("Runtime.evaluate", {"expression": "location.href", "returnByValue": True}) + self.assertEqual(evaluated["result"]["value"], "about:blank") time.sleep(1.5) - second_version = cdp.send("Browser.getVersion") - self.assertIsInstance(second_version["product"], str) + second_evaluated = cdp.send("Runtime.evaluate", {"expression": "document.readyState", "returnByValue": True}) + self.assertEqual(second_evaluated["result"]["value"], "complete") finally: cdp.close() @@ -186,5 +201,57 @@ def _wait_until(predicate, timeout_s: float = 2.0) -> None: raise AssertionError("timed out waiting for condition") +def reversews_test_browser_path() -> str: + global REVERSEWS_TEST_BROWSER_PATH + if REVERSEWS_TEST_BROWSER_PATH is not None: + return REVERSEWS_TEST_BROWSER_PATH + explicit_candidates = [ + os.environ.get("CHROME_PATH"), + "/usr/bin/chromium" if sys.platform.startswith("linux") else None, + ] + for candidate in explicit_candidates: + if candidate and Path(candidate).exists(): + REVERSEWS_TEST_BROWSER_PATH = candidate + return candidate + home = Path.home() + if sys.platform == "darwin": + patterns = [ + str(home / "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"), + str(home / "Library/Caches/ms-playwright/chromium-*/chrome-mac*/Chromium.app/Contents/MacOS/Chromium"), + str(home / "Library/Caches/puppeteer/chrome/mac*-*/chrome-mac*/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"), + ] + elif sys.platform.startswith("win"): + local_app_data = Path(os.environ.get("LOCALAPPDATA") or home / "AppData/Local") + patterns = [ + str(local_app_data / "ms-playwright/chromium-*/chrome-win*/chrome.exe"), + str(home / ".cache/puppeteer/chrome/win*-*/chrome-win*/chrome.exe"), + ] + else: + patterns = [ + str(home / ".cache/ms-playwright/chromium-*/chrome-linux*/chrome"), + "/opt/pw-browsers/chromium-*/chrome-linux*/chrome", + str(home / ".cache/puppeteer/chrome/linux-*/chrome-linux*/chrome"), + ] + candidates = newest_first([match for pattern in patterns for match in glob.glob(pattern)]) + if candidates: + REVERSEWS_TEST_BROWSER_PATH = candidates[0] + return candidates[0] + raise RuntimeError("Reversews tests require CHROME_PATH, /usr/bin/chromium, or Chrome for Testing.") + + +def newest_first(candidates: list[str]) -> list[str]: + return sorted(dict.fromkeys(candidates), key=path_score) + + +def path_score(candidate: str) -> tuple[int, float, str]: + numbers = [int(part) for part in re.findall(r"\d+", candidate)] + version = max(numbers) if numbers else 0 + try: + mtime = Path(candidate).stat().st_mtime + except OSError: + mtime = 0.0 + return (-version, -mtime, candidate) + + if __name__ == "__main__": unittest.main() diff --git a/python/tests/test_WebSocketUpstreamTransport.py b/python/tests/test_WebSocketUpstreamTransport.py index a1d524e..f576956 100644 --- a/python/tests/test_WebSocketUpstreamTransport.py +++ b/python/tests/test_WebSocketUpstreamTransport.py @@ -68,7 +68,7 @@ def test_constructor_update_and_server_config_match_ts_shape(self) -> None: def test_launches_real_browser_and_speaks_raw_cdp(self) -> None: cdp = ModCDPClient( - launcher={"launcher_mode": "local", "launcher_options": {"headless": True, "sandbox": False}}, + launcher={"launcher_mode": "local", "launcher_options": {"headless": True}}, upstream={"upstream_mode": "ws"}, injector={ "injector_mode": "auto", @@ -113,7 +113,7 @@ def test_launches_real_browser_and_speaks_raw_cdp(self) -> None: cdp.close() def test_resolves_real_http_cdp_endpoint_to_browser_websocket(self) -> None: - chrome = LocalBrowserLauncher({"headless": True, "sandbox": False}).launch() + chrome = LocalBrowserLauncher({"headless": True}).launch() transport = WebSocketUpstreamTransport({"cdp_url": chrome["cdp_url"]}) received: Queue[dict] = Queue() transport.onRecv(lambda message: received.put(message)) @@ -130,7 +130,7 @@ def test_resolves_real_http_cdp_endpoint_to_browser_websocket(self) -> None: def test_resolves_real_host_port_cdp_endpoint_to_browser_websocket(self) -> None: port = LocalBrowserLauncher.freePort() - chrome = LocalBrowserLauncher({"port": port, "headless": True, "sandbox": False}).launch() + chrome = LocalBrowserLauncher({"port": port, "headless": True}).launch() transport = WebSocketUpstreamTransport({"cdp_url": f"127.0.0.1:{port}"}) received: Queue[dict] = Queue() transport.onRecv(lambda message: received.put(message)) @@ -146,7 +146,7 @@ def test_resolves_real_host_port_cdp_endpoint_to_browser_websocket(self) -> None chrome["close"]() def test_close_clears_connection_state(self) -> None: - chrome = LocalBrowserLauncher({"headless": True, "sandbox": False}).launch() + chrome = LocalBrowserLauncher({"headless": True}).launch() transport = WebSocketUpstreamTransport({"cdp_url": chrome["cdp_url"]}) try: diff --git a/python/tests/test_translate.py b/python/tests/test_translate.py index 3fa640b..fd9ff1c 100644 --- a/python/tests/test_translate.py +++ b/python/tests/test_translate.py @@ -2,6 +2,7 @@ import unittest import json +from typing import cast from modcdp.translate import ( CUSTOM_EVENT_BINDING_NAME, @@ -38,6 +39,19 @@ def test_routes_wraps_and_unwraps_modcdp_protocol_messages_deterministically(sel ) self.assertEqual(configured["steps"][0].get("unwrap"), "runtime_json") + custom = wrap_command_if_needed( + "Custom.echo", + {"secret": "x" * 100, "nested": {"ok": True}}, + cdp_session_id="session-1", + ) + custom_step_params = custom["steps"][0].get("params", {}) + self.assertIn("JSON.parse(paramsJson)", str(custom_step_params.get("functionDeclaration"))) + self.assertNotIn("xxxxxxxxxx", str(custom_step_params.get("functionDeclaration"))) + custom_arguments = cast("list[dict[str, object]]", custom_step_params.get("arguments", [])) + self.assertEqual(custom_arguments[0].get("value"), "Custom.echo") + self.assertEqual(json.loads(str(custom_arguments[1].get("value"))), {"secret": "x" * 100, "nested": {"ok": True}}) + self.assertEqual(custom_arguments[2].get("value"), "session-1") + self.assertEqual(unwrap_response_if_needed({"result": {"type": "object", "value": {"ok": True}}}, "runtime"), {"ok": True}) self.assertEqual(unwrap_response_if_needed({"product": "Chrome/1"}, None), {"product": "Chrome/1"}) diff --git a/python/uv.lock b/python/uv.lock index 1097ad8..68c2ac9 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "modcdp" -version = "0.0.13" +version = "0.0.19" source = { editable = "." } dependencies = [ { name = "pydantic" }, diff --git a/tsconfig.json b/tsconfig.json index 6d66b2b..fa02b7e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,6 @@ "useUnknownInCatchVariables": false, "verbatimModuleSyntax": true }, - "include": ["js/src/**/*.ts", "js/examples/**/*.ts", "extension/src/**/*.ts"], + "include": ["js/src/**/*.ts", "js/examples/**/*.ts", "js/test/**/*.ts", "extension/src/**/*.ts"], "exclude": ["dist", "node_modules"] }